Compare commits

...

11 Commits

Author SHA1 Message Date
‘Liammcl’
fa91bdee65 feat 2025-02-08 01:21:43 +08:00
‘Liammcl’
12bda4ab8f fix 2025-01-19 16:59:17 +08:00
‘Liammcl’
ec73e51a2d Merge branch 'main' of github.com:xuqssq/uppmkt-admin 2025-01-19 12:53:40 +08:00
‘Liammcl’
705af6bfa8 fix 2025-01-19 12:52:58 +08:00
liamzi
0e71c0e8b2 pdf 2025-01-17 17:47:21 +08:00
liamzi
c229f2dbc4 fix 2025-01-17 17:39:14 +08:00
liamzi
f1fe46b11a fix 2025-01-17 17:18:53 +08:00
liamzi
5f0ec367e0 模块增加分页 2025-01-17 16:21:21 +08:00
qian
2237aecdf2 fix 2025-01-17 11:17:40 +08:00
liamzi
8dccf8e554 黑暗模式fix 2025-01-16 18:41:30 +08:00
‘Liammcl’
027a8e5493 路由修复 2025-01-15 00:59:16 +08:00
55 changed files with 1078 additions and 1002 deletions

View File

@@ -16,26 +16,32 @@
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@heroicons/react": "^2.2.0",
"@mantine/code-highlight": "^7.15.2",
"@mantine/core": "^7.15.2",
"@mantine/hooks": "^7.15.2",
"@material-tailwind/react": "^2.1.10",
"@monaco-editor/react": "^4.6.0",
"@supabase/supabase-js": "^2.38.4",
"ai": "^4.0.22",
"antd": "^5.11.0",
"apexcharts": "^4.3.0",
"code-highlight": "^1.0.0",
"dayjs": "^1.11.13",
"dnd-kit": "^0.0.2",
"heroicons": "^2.2.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-apexcharts": "^1.7.0",
"react-dom": "^18.2.0",
"react-icons": "^5.4.0",
"react-infinite-scroll-component": "^6.1.0",
"react-router-dom": "^6.18.0",
"react-use": "^17.6.0",
"recharts": "^2.9.0",
"recharts": "^2.15.0",
"remixicon": "^4.6.0",
"styled-components": "^6.1.0",
"uuid": "^11.0.3",
"zod": "^3.24.1"

View File

@@ -0,0 +1,111 @@
import {
Card,
CardBody,
CardHeader,
Typography,
} from "@material-tailwind/react";
import Chart from "react-apexcharts";
const chartConfig = {
type: "bar",
height: 240,
series: [
{
name: "Sales",
data: [50, 40, 300, 320, 500, 350, 200, 230, 500],
},
],
options: {
chart: {
toolbar: {
show: false,
},
},
title: {
show: "",
},
dataLabels: {
enabled: false,
},
colors: ["#020617"],
plotOptions: {
bar: {
columnWidth: "40%",
borderRadius: 2,
},
},
xaxis: {
axisTicks: {
show: false,
},
axisBorder: {
show: false,
},
labels: {
style: {
colors: "#616161",
fontSize: "12px",
fontFamily: "inherit",
fontWeight: 400,
},
},
categories: [
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
],
},
yaxis: {
labels: {
style: {
colors: "#616161",
fontSize: "12px",
fontFamily: "inherit",
fontWeight: 400,
},
},
},
grid: {
show: true,
borderColor: "#dddddd",
strokeDashArray: 5,
xaxis: {
lines: {
show: true,
},
},
padding: {
top: 5,
right: 20,
},
},
fill: {
opacity: 0.8,
},
tooltip: {
theme: "dark",
},
},
};
export default function Example(props) {
return (
<Card {...props}>
<CardHeader
floated={false}
shadow={false}
color="transparent"
className="flex flex-col gap-4 rounded-none md:flex-row md:items-center"
>
</CardHeader>
<CardBody className="px-2 pb-0">
<Chart {...chartConfig} />
</CardBody>
</Card>
);
}

View File

@@ -1,220 +0,0 @@
import { Button, Drawer, Input, Space, message } from 'antd';
import { useChat } from "ai/react";
import { CodeHighlight } from "@mantine/code-highlight";
import { DownloadOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { useRef, useEffect, useState } from 'react';
import { useSessionStorage } from 'react-use';
import Editor from "@monaco-editor/react";
export default function ChatAIDrawer({ open, onClose, onExport }) {
const STORAGE_KEY = 'chat_history';
const [storedMessages, setStoredMessages] = useSessionStorage(STORAGE_KEY, []);
const { messages, input, handleSubmit, handleInputChange, isLoading, setMessages } = useChat({
api: "https://test-ai-quirkyai.vercel.app/api/chat",
initialMessages: storedMessages.length>0? JSON.parse(storedMessages):[],
});
const messagesEndRef = useRef(null);
useEffect(() => {
setStoredMessages(JSON.stringify(messages));
}, [messages, setStoredMessages]);
// 新消息时自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 修改导出函数,接收单条消息内容
const handleExport = (content) => {
try {
const jsonContent = JSON.parse(content);
onExport?.(jsonContent);
} catch (error) {
console.log(error);
message.error('导出失败,请重试');
}
};
const [editingMessageId, setEditingMessageId] = useState(null);
const [editingContent, setEditingContent] = useState('');
// 优化编辑处理函数
const handleEdit = (message) => {
if(isLoading) return;
setEditingContent(message.content);
setEditingMessageId(message.id);
};
const handleSaveEdit = () => {
try {
JSON.parse(editingContent);
setMessages(messages.map(msg =>
msg.id === editingMessageId
? { ...msg, content: editingContent }
: msg
));
setEditingMessageId(null);
message.success('编辑成功');
} catch (error) {
message.error('请输入有效的 JSON 格式');
}
};
const handleCancelEdit = () => {
setEditingMessageId(null);
setEditingContent('');
};
return (
<Drawer
title={
<div className="flex justify-between items-center">
<span className="text-lg font-medium text-gray-800">AI 助手</span>
<Button
size="small"
className="hover:bg-gray-100"
onClick={() => {
setMessages([]);
setStoredMessages('[]');
message.success('历史记录已清空');
}}
>
清空历史
</Button>
</div>
}
placement="right"
width={800}
open={open}
onClose={onClose}
>
<div className="flex flex-col h-[calc(100vh-108px)]">
<div className="flex-1 overflow-y-auto px-4 space-y-6">
{messages.map((message) => (
<div
key={message.id}
className={`rounded-lg p-4 transition-all ${
message.role === 'assistant'
? 'bg-blue-50 hover:bg-blue-100'
: 'bg-gray-50 hover:bg-gray-100'
}`}
>
<div className="flex justify-between items-center mb-3">
<span className={`font-medium ${
message.role === 'assistant' ? 'text-blue-600' : 'text-gray-600'
}`}>
{message.role === 'assistant' ? 'AI 助手' : '用户'}
</span>
{message.role === 'assistant' && (
<Space>
<Button
type="text"
size="small"
icon={<EditOutlined />}
disabled={isLoading}
onClick={() => handleEdit(message)}
className="text-gray-500 hover:text-blue-600"
>
编辑
</Button>
<Button
type="text"
size="small"
icon={<DownloadOutlined />}
onClick={() => handleExport(message.content)}
className="text-gray-500 hover:text-blue-600"
>
导出
</Button>
</Space>
)}
</div>
{message.role === "assistant" ? (
<div className="relative">
{editingMessageId === message.id ? (
<div className="rounded-lg border border-blue-200">
<Editor
height="300px"
defaultLanguage="json"
value={editingContent}
theme="vs-light"
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: 'on',
renderLineHighlight: 'none',
roundedSelection: true,
}}
onChange={setEditingContent}
onMount={(editor) => {
editor.getModel()?.updateOptions({ tabSize: 2 });
editor.focus();
}}
/>
<div className="flex justify-end gap-2 p-2 bg-gray-50 border-t">
<Button
size="small"
icon={<CloseOutlined />}
onClick={handleCancelEdit}
className="hover:bg-gray-200"
>
取消
</Button>
<Button
type="primary"
size="small"
icon={<CheckOutlined />}
onClick={handleSaveEdit}
className="bg-blue-600 hover:bg-blue-700"
>
保存
</Button>
</div>
</div>
) : (
<CodeHighlight
code={message.content}
language="json"
copyLabel="复制代码"
copiedLabel="已复制!"
withLineNumbers
className="rounded-lg"
/>
)}
</div>
) : (
<div className="text-gray-700 whitespace-pre-wrap break-words">
{message.content}
</div>
)}
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="border-t bg-white p-4">
<form onSubmit={handleSubmit} className="flex gap-2">
<Input
value={input}
placeholder="请输入您的问题..."
onChange={handleInputChange}
disabled={isLoading}
className="flex-1 rounded-lg border-gray-300 hover:border-blue-400 focus:border-blue-600 focus:shadow-blue-100"
/>
<Button
type="primary"
htmlType="submit"
loading={isLoading}
className="rounded-lg bg-blue-600 hover:bg-blue-700"
>
发送
</Button>
</form>
</div>
</div>
</Drawer>
);
}

View File

@@ -14,8 +14,24 @@ const Sidebar = ({ collapsed }) => {
const { isDarkMode } = useTheme();
const { user } = useAuth();
const getSelectedKeys = useMemo(() => {
const pathname = location.pathname.substring(1);
const pathParts = pathname.split('/');
let key = '';
if (pathParts.length >= 2) {
const baseKey = `${pathParts[0]}/${pathParts[1]}`;
key = pathname.includes('/') ? baseKey : pathname;
} else {
key = pathname;
}
return [key];
}, [location.pathname]);
const menuClient = useMemo(() => {
if (!user?.id||user.menukeys?.length===0) return [];
if (!user?.id || user.menukeys?.length === 0) return [];
return getMenuItems(user?.menukeys || []);
}, [user]);
@@ -44,7 +60,7 @@ const Sidebar = ({ collapsed }) => {
theme={isDarkMode ? "dark" : "light"}
width={256}
collapsedWidth={80}
className={`app-sidebar ${collapsed ? "collapsed" : ""}`}
className={`app-sidebar ${collapsed ? "collapsed" : ""} overflow-auto`}
>
<Logo collapsed={collapsed} isDarkMode={isDarkMode} />
<Menu
@@ -54,7 +70,6 @@ const Sidebar = ({ collapsed }) => {
defaultOpenKeys={defaultOpenKeys}
items={menuClient}
onClick={handleMenuClick}
style={{ overflow: 'auto' }}
/>
</Sider>
);

View File

@@ -0,0 +1,49 @@
import React from 'react'
import {
Card,
CardBody,
CardHeader,
Typography,
} from "@material-tailwind/react";
import Chart from "react-apexcharts";
export default function index(props) {
const chartConfig = {
type: "pie",
width: 280,
height: 280,
series: [44, 55, 13, 43, 22],
options: {
chart: {
toolbar: {
show: false,
},
},
title: {
show: "",
},
dataLabels: {
enabled: false,
},
colors: ["#020617", "#ff8f00", "#00897b", "#1e88e5", "#d81b60"],
legend: {
show: false,
},
},
};
return (
<Card {...props}>
<CardHeader
floated={false}
shadow={false}
color="transparent"
className="flex flex-col gap-4 rounded-none md:flex-row md:items-center"
>
</CardHeader>
<CardBody className="px-2 pb-0">
<Chart {...chartConfig} />
</CardBody>
</Card>
)
}

View File

@@ -187,50 +187,44 @@ const SectionList = ({
{availableSections.map((section) => (
<div
key={section.id}
className="group relative bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 cursor-pointer"
className="group relative bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all duration-300 cursor-pointer"
onClick={() => handleUseTemplate(section, add)}
>
<div className="p-6">
<div className="text-center mb-4">
<h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-500 transition-colors">
{section.attributes.name}
</h3>
<div className="text-sm text-gray-500 mt-1">
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{section.attributes.items?.length || 0} 个项目
</div>
</div>
<div className="space-y-2 mt-4 border-t pt-4">
{(section.attributes.items || [])
.slice(0, 3)
.map((item, index) => (
<div
key={index}
className="flex justify-between items-center"
>
<span className="text-sm text-gray-600 truncate flex-1">
{item.name}
</span>
<span className="text-sm text-gray-500 ml-2">
{formatExchangeRate(currentCurrency, item.price)}
</span>
</div>
))}
<div className="space-y-2 mt-4 border-t dark:border-gray-700 pt-4">
{(section.attributes.items || []).slice(0, 3).map((item, index) => (
<div key={index} className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-300 truncate flex-1">
{item.name}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
{formatExchangeRate(currentCurrency, item.price)}
</span>
</div>
))}
{(section.attributes.items || []).length > 3 && (
<div className="text-sm text-gray-500 text-center">
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
还有 {section.attributes.items.length - 3} 个项目...
</div>
)}
</div>
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<span className="text-sm text-gray-600">总金额</span>
<span className="text-base font-medium text-blue-500">
<div className="mt-4 pt-4 border-t dark:border-gray-700 flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-300">总金额</span>
<span className="text-base font-medium text-blue-500 dark:text-blue-400">
{formatExchangeRate(
currentCurrency,
(section.attributes.items || []).reduce(
(sum, item) =>
sum + (item.price * (item.quantity || 1) || 0),
(sum, item) => sum + (item.price * (item.quantity || 1) || 0),
0
)
)}
@@ -241,12 +235,12 @@ const SectionList = ({
))}
</div>
<div className="flex justify-center">
<div className="flex justify-center mt-6">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleCreateCustom(add, fieldsLength)}
className="bg-blue-600 hover:bg-blue-700 border-0 shadow-md hover:shadow-lg transition-all duration-200 h-10 px-6 rounded-lg flex items-center gap-2"
className="bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 border-0 shadow-md hover:shadow-lg transition-all duration-200 h-10 px-6 rounded-lg flex items-center gap-2"
>
<span className="font-medium">自定义模块</span>
</Button>
@@ -256,7 +250,7 @@ const SectionList = ({
<div className="flex flex-col items-center justify-center py-16 px-4">
<div className="w-48 h-48 mb-8">
<svg
className="w-full h-full text-gray-200"
className="w-full h-full text-gray-200 dark:text-gray-700"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -270,10 +264,10 @@ const SectionList = ({
/>
</svg>
</div>
<h3 className="text-xl font-medium text-gray-900 mb-2">
<h3 className="text-xl font-medium text-gray-900 dark:text-gray-100 mb-2">
暂无可用模板
</h3>
<p className="text-gray-500 text-center max-w-sm mb-8">
<p className="text-gray-500 dark:text-gray-400 text-center max-w-sm mb-8">
您可以选择创建一个自定义模块开始使用
</p>
<Button
@@ -281,7 +275,7 @@ const SectionList = ({
icon={<PlusOutlined />}
onClick={() => handleCreateCustom(add, fieldsLength)}
size="large"
className="shadow-md hover:shadow-lg transition-shadow"
className="shadow-md hover:shadow-lg transition-shadow dark:bg-blue-500 dark:hover:bg-blue-600"
>
创建自定义模块
</Button>

View File

@@ -199,7 +199,7 @@ const SectionList = ({ form, isView, formValues, type }) => {
{availableSections.map((section) => (
<div
key={section.id}
className="group relative bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 cursor-pointer"
className="group relative bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all duration-300 cursor-pointer"
onClick={() => handleUseTemplate(section, add)}
>
<div className="p-6">
@@ -241,7 +241,7 @@ const SectionList = ({ form, isView, formValues, type }) => {
))}
</div>
<div className="flex justify-center">
<div className="flex justify-center mt-6">
<Button
type="primary"
icon={<PlusOutlined />}

View File

@@ -3,20 +3,33 @@ import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext";
const PUBLIC_PATHS = ['login', '404','home'];
export const ProtectedRoute = ({ children }) => {
const { user } = useAuth();
const location = useLocation();
const currentPath = location.pathname.replace(/^\//, '');
// 如果是公共路径,直接显示
if (PUBLIC_PATHS.includes(currentPath) || currentPath === '*') {
return children;
}
// 如果用户未登录,重定向到登录页面,并携带当前路径
if (!user?.id) {
return <Navigate
to={`/login?redirectTo=${encodeURIComponent(location.pathname + location.search)}`}
replace
/>;
}
// 如果用户已登录,检查权限
if (user?.id) {
const hasPermission = user.menukeys?.some(key => {
return currentPath === key || currentPath.startsWith(`${key}/`);
});
if (!hasPermission) {
return <Navigate to="/home" replace />;
return <Navigate to="/dashboard" replace />;
}
return children;

View File

@@ -8,31 +8,33 @@ const StatCard = ({ icon, title, count, amount, color = '#1677ff' }) => {
<Card
styles={{
body: {
padding: '20px',
padding: '0',
height: '100%',
background: 'var(--color-bg-container)',
}
}}
className="h-full hover:shadow-md transition-shadow duration-300"
>
<div className="flex items-start gap-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-lg"
style={{
backgroundColor: `${color}15`,
}}
>
<span style={{ color }}>{icon}</span>
</div>
<div className="flex-1">
<h3 className="text-base text-gray-500 dark:text-gray-400 m-0">
{title}
</h3>
<div className="text-sm text-gray-400 dark:text-gray-500 mt-1">
{count} invoices
<div className="p-3 sm:p-5">
<div className="flex items-start gap-2 sm:gap-4">
<div
className="w-10 h-10 sm:w-12 sm:h-12 rounded-full flex items-center justify-center text-base sm:text-lg"
style={{
backgroundColor: `${color}15`,
}}
>
<span style={{ color }}>{icon}</span>
</div>
<div className="text-xl font-semibold mt-2 dark:text-white">
{formatCurrency(amount)}
<div className="flex-1">
<h3 className="text-sm sm:text-base text-gray-500 dark:text-gray-400 m-0">
{title}
</h3>
<div className="text-xs sm:text-sm text-gray-400 dark:text-gray-500 mt-0.5 sm:mt-1">
{count} invoices
</div>
<div className="text-lg sm:text-xl font-semibold mt-1 sm:mt-2 dark:text-white">
{formatCurrency(amount)}
</div>
</div>
</div>
</div>

View File

@@ -42,7 +42,7 @@ const stats = [
const StatisticsOverview = () => {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-4">
{stats.map((stat, index) => (
<StatCard key={index} {...stat} />
))}

View File

@@ -29,8 +29,6 @@ export default function DifyChatDrawer({ open, onClose, onExport }) {
useEffect(() => {
if (storedMessages && storedMessages.length > 0) {
console.log(storedMessages,'storedMessages');
setMessages(storedMessages);
}
}, []);
@@ -106,10 +104,11 @@ export default function DifyChatDrawer({ open, onClose, onExport }) {
width={800}
open={open}
onClose={onClose}
className="rounded-l-xl"
>
<div className="flex flex-col h-[calc(100vh-108px)]">
<div className="flex-1 overflow-y-auto px-4 space-y-6">
{messages.map((message) => (
{messages.length > 0 && messages.map((message) => (
<div
key={message.id}
className={`rounded-lg p-4 transition-all ${

View File

@@ -8,14 +8,13 @@ import { v4 as uuidv4 } from "uuid";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
const [user, setUser] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
//处理google登录
const hash = window.location.hash.substring(1);
const hashParams = new URLSearchParams(hash);
const accessToken = hashParams.get("access_token");
@@ -62,12 +61,7 @@ export const AuthProvider = ({ children }) => {
}
}, [user]);
// useEffect(() => {
// const redirectTo = searchParams.get("redirectTo");
// if (redirectTo) {
// navigate(redirectTo);
// }
// }, [location.pathname]);
//检查时候在管理模块团队中,没有就自动加入
const checkInTeam = async (user) => {
@@ -121,7 +115,20 @@ export const AuthProvider = ({ children }) => {
message.error(error.message || "登录失败,请稍后重试");
return;
}
setUser(data.user);
const role = await checkInTeam(data.user);
const menukey = await fetchMenuList(role);
setUser({ ...data.user, adminRole: role, menukeys: menukey });
// 获取重定向路径
const redirectTo = searchParams.get("redirectTo");
if (redirectTo) {
// 如果有重定向路径,则导航到该路径
navigate(decodeURIComponent(redirectTo), { replace: true });
} else {
navigate("/dashboard", { replace: true });
}
return data;
} catch (error) {
message.error(error.message || "登录失败,请稍后重试");
@@ -134,13 +141,12 @@ export const AuthProvider = ({ children }) => {
const signInWithGoogle = async () => {
try {
setLoading(true);
const redirectTo = searchParams.get("redirectTo");
const redirectTo = searchParams.get("redirectTo") || "/dashboard";
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/login?redirectTo=${
redirectTo ?? ""
}`,
redirectTo: `${window.location.origin}/login?redirectTo=${encodeURIComponent(redirectTo)}`,
},
});
@@ -182,17 +188,17 @@ export const AuthProvider = ({ children }) => {
}
};
const fetchMenuList = async (role) => {
console.log(role,'role');
if(!role) return;
try {
const { data, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'menuKey')
.eq('attributes->>roleName', role) // 添加这行来筛选 OWNER 角色
.eq('attributes->>roleName', role)
.single();
if (error) throw error;
if(data?.attributes?.menuKeys){
return data.attributes.menuKeys;
return data.attributes.menuKeys;
}else{
return [];
}
@@ -214,7 +220,8 @@ export const AuthProvider = ({ children }) => {
}
setUser({});
message.success("已成功登出");
navigate(`/login?redirectTo=${location.pathname}`, { replace: true });
// 保存当前完整路径作为重定向 URL
navigate(`/login?redirectTo=${encodeURIComponent(location.pathname + location.search)}`, { replace: true });
} catch (error) {
message.error(error.message || "登出失败,请稍后重试");
} finally {

View File

@@ -6,8 +6,6 @@ class SupabaseService {
let query = supabase
.from(table)
.select(options.select || "*", { count: "exact" });
// 处理精确匹配条件
if (options.match) {
query = query.match(options.match);
}

View File

@@ -1,8 +1,10 @@
import React from 'react';
import { Card } from 'antd';
import StatisticsOverview from '@/components/dashboard/StatisticsOverview';
import Bar from '@/components/Barchart'
import Pie from '@/components/Piechart'
const Dashboard = () => {
return (
<div className="space-y-6">
<Card
@@ -14,8 +16,13 @@ const Dashboard = () => {
}
}}
>
<div>测试member可见最低权限</div>
{/* <StatisticsOverview /> */}
<StatisticsOverview />
<div className='flex w-full mt-20 gap-5'>
<Bar className="flex-1 flex justify-center items-center"/>
<Pie className="flex-1 flex justify-center items-center"/>
</div>
</Card>
</div>
);

View File

@@ -158,7 +158,7 @@ const CustomerPage = () => {
columns={columns}
dataSource={customers}
rowKey="id"
scroll={{ x: true }}
scroll={{ x: true}}
loading={loading}
onChange={handleTableChange}
pagination={{

View File

@@ -256,7 +256,7 @@ export default function ProjectDetail() {
}, [id, templateId]);
return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<div className="bg-gradient-to-b min-h-screen p-2">
<Card
className="shadow-lg rounded-lg border-0"
title={

View File

@@ -338,7 +338,7 @@ const ProjectPage = () => {
columns={columns}
dataSource={projects}
rowKey="id"
scroll={{ x: true }}
scroll={{ x: true}}
loading={loadingProjects}
onChange={handleTableChange}
pagination={{

View File

@@ -22,7 +22,6 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import SectionList from '@/components/SectionList'
import DifyChatDrawer from '@/components/difyChatAi';
import ChatAIDrawer from '@/components/ChatAi';
const { Title } = Typography;
// 添加货币符号映射
@@ -59,27 +58,25 @@ const QuotationForm = () => {
// 计算小节总额
const calculateSectionTotal = useMemo(
() =>
(items = []) => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
return sum + calculateItemAmount(item.quantity, item.price);
}, 0);
},
() => (items = []) => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
return sum + calculateItemAmount(item.quantity, item.price);
}, 0);
},
[calculateItemAmount]
);
// 计算总金额
const calculateTotalAmount = useMemo(
() =>
(sections = []) => {
if (!Array.isArray(sections)) return 0;
return sections.reduce((sum, section) => {
if (!section) return sum;
return sum + calculateSectionTotal(section.items);
}, 0);
},
() => (sections = []) => {
if (!sections || !Array.isArray(sections)) return 0;
return sections.reduce((sum, section) => {
if (!section || !section.items) return sum;
return sum + calculateSectionTotal(section.items);
}, 0);
},
[calculateSectionTotal]
);
@@ -318,51 +315,59 @@ const QuotationForm = () => {
const [vercelOpen, setVercelOpen] = useState(false);
const handleExport = (data) => {
if(data?.activityName&&data?.currency){
const quotationData = {
quataName: data.activityName,
description: data.description,
currency: data.currency || "TWD",
sections: data.sections.map((section) => ({
key: uuidv4(),
sectionName: section.sectionName,
items: section.items.map((item) => ({
try {
const jsonData = typeof data === 'string' ? JSON.parse(data) : data;
if (jsonData?.quataName && jsonData?.currency) {
const quotationData = {
quataName: jsonData.quataName,
description: jsonData.description || '',
currency: jsonData.currency || "TWD",
sections: jsonData.sections.map((section) => ({
key: uuidv4(),
name: item.name,
quantity: item.quantity,
price: item.price,
description: item.description,
unit: item.unit,
sectionName: section.sectionName,
items: section.items.map((item) => ({
key: uuidv4(),
name: item.name,
quantity: Number(item.quantity) || 0,
price: Number(item.price) || 0,
description: item.description || "",
unit: item.unit || "",
})),
})),
})),
};
setCurrentCurrency(data.currency || "TWD");
form.setFieldsValue(quotationData);
setFormValues(quotationData);
setTaxRate(data.taxRate || 0);
message.success('已添加报价单');
};
setCurrentCurrency(jsonData.currency || "TWD");
form.setFieldsValue(quotationData);
setFormValues(quotationData);
setTaxRate(Number(jsonData.taxRate) || 0);
message.success('已添加报价单');
} else {
const newSection = {
...jsonData,
key: uuidv4(),
};
const currentSections = form.getFieldValue('sections') || [];
const newSections = [...currentSections, newSection];
}else{
const _data={
...data,
key: uuidv4(),
form.setFieldValue('sections', newSections);
const currentFormValues = form.getFieldsValue();
setFormValues({
...currentFormValues,
sections: newSections
});
message.success('已添加新的服务项目');
}
const newSections = [...formValues.sections, _data];
form.setFieldValue('sections', newSections);
const currentFormValues = form.getFieldsValue();
setFormValues({
...currentFormValues,
sections: newSections
});
message.success('已添加新的服务项目');
}
setDifyOpen(false);
setVercelOpen(false);
setDifyOpen(false);
setVercelOpen(false);
} catch (error) {
console.error('Export error:', error);
message.error('导出失败:数据格式错误');
}
};
return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<div className="bg-gradient-to-b min-h-screen p-2">
<Card
className="shadow-lg rounded-lg border-0"
title={
@@ -401,9 +406,9 @@ const QuotationForm = () => {
<Button onClick={() => setDifyOpen(true)}>
AI for Dify
</Button>
<Button onClick={() => setVercelOpen(true)}>
{/* <Button onClick={() => setVercelOpen(true)}>
AI for Vercel
</Button>
</Button> */}
</>
)}
@@ -511,11 +516,7 @@ const QuotationForm = () => {
onClose={() => setDifyOpen(false)}
onExport={handleExport}
/>
<ChatAIDrawer
open={vercelOpen}
onClose={() => setVercelOpen(false)}
onExport={handleExport}
/>
</div>
);
};

View File

@@ -381,7 +381,7 @@ useEffect(()=>{
columns={columns}
dataSource={quotations}
rowKey="id"
scroll={{ x: true }}
scroll={{ x: true}}
loading={loadingQuotations}
onChange={handleTableChange}
pagination={{
@@ -405,7 +405,7 @@ useEffect(()=>{
<Button
key="custom"
onClick={() => handleConfirm()}
className=" gap-2 px-6 rounded-full hover:bg-gray-100"
className="gap-2 px-6 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-200"
>
<FileAddOutlined /> 自定义创建
</Button>,
@@ -414,28 +414,28 @@ useEffect(()=>{
type="primary"
disabled={!selectedTemplateId}
onClick={handleConfirm}
className="gap-2 px-6 rounded-full bg-blue-600 hover:bg-blue-700"
className="gap-2 px-6 rounded-full "
>
<AppstoreOutlined /> 使用选中模板
</Button>,
]}
width={900}
className="template-modal"
className="template-modal "
>
{loading ? (
<div className="flex justify-center items-center h-[400px]">
<div className="flex justify-center items-center h-[400px] ">
<Spin size="large" />
</div>
) : templates.length === 0 ? (
<Empty description="暂无可用模板" />
<Empty description={<span className="dark:text-gray-400">暂无可用模板</span>} />
) : (
<div className="max-h-[600px] overflow-y-auto px-4">
<div className="max-h-[600px] overflow-y-auto px-4 ">
{getTemplatesByCategory().map((group, groupIndex) => (
<div key={groupIndex} className="mb-10 last:mb-2">
<div className="flex items-center gap-3 mb-6">
<h3 className="text-xl font-medium text-gray-900">
<h3 className="text-xl font-medium text-gray-900 dark:text-gray-100">
{group.name}
<span className="ml-2 text-sm text-gray-500">
<span className="ml-2 text-sm text-gray-500 dark:text-gray-400">
({group.templates.length})
</span>
</h3>
@@ -450,18 +450,18 @@ useEffect(()=>{
relative p-6 rounded-xl cursor-pointer transition-all duration-200
${
selectedTemplateId === template.id
? "ring-2 ring-blue-500 bg-blue-50/40"
: "hover:shadow-lg border border-gray-200 hover:border-blue-200"
? "ring-2 ring-blue-500 dark:ring-blue-400 bg-blue-50/40 dark:bg-blue-900/30"
: "hover:shadow-lg border border-gray-200 dark:border-gray-700 hover:border-blue-200 dark:hover:border-blue-700"
}
transform hover:-translate-y-1
transform hover:-translate-y-1 dark:bg-gray-800
`}
>
<div className="flex justify-between items-start gap-3 mb-4">
<div className="flex-1 min-w-0">
<h4 className="text-lg font-medium text-gray-900 truncate">
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100 truncate">
{template.attributes.templateName}
</h4>
<p className="text-sm text-gray-600 mt-2 line-clamp-2">
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2 line-clamp-2">
{template.attributes.description || "暂无描述"}
</p>
</div>
@@ -475,16 +475,16 @@ useEffect(()=>{
rounded-lg p-3 text-sm border
${
selectedTemplateId === template.id
? "bg-white border-blue-200"
: "bg-gray-50 border-gray-100"
? "bg-white dark:bg-gray-700 border-blue-200 dark:border-blue-600"
: "bg-gray-50 dark:bg-gray-800 border-gray-100 dark:border-gray-700"
}
`}
>
<div className="flex justify-between items-center">
<span className="font-medium text-gray-700 truncate flex-1">
<span className="font-medium text-gray-700 dark:text-gray-200 truncate flex-1">
{section.sectionName}
</span>
<span className="text-gray-500 ml-2 text-xs px-2 py-0.5 bg-gray-100 rounded-full">
<span className="text-gray-500 dark:text-gray-400 ml-2 text-xs px-2 py-0.5 bg-gray-100 dark:bg-gray-600 rounded-full">
{section.items.length}
</span>
</div>
@@ -494,7 +494,7 @@ useEffect(()=>{
{selectedTemplateId === template.id && (
<div className="absolute top-4 right-4">
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center shadow-md">
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center shadow-md">
<svg
className="w-4 h-4 text-white"
fill="currentColor"

View File

@@ -7,7 +7,7 @@ import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
const { Title, Text } = Typography;
import { EXCHANGE_RATE,defaultSymbol } from '@/utils/exchange_rate';
import { EXCHANGE_RATE, defaultSymbol } from '@/utils/exchange_rate';
const QuotationPreview = () => {
const { id } = useParams();
@@ -114,17 +114,6 @@ const QuotationPreview = () => {
}
};
// 导出按钮组件
const ExportPDFButton = () => (
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={exportPDF}
>
导出PDF
</Button>
);
if (loading) {
return (
<div className="flex justify-center items-center h-full">
@@ -139,52 +128,69 @@ const QuotationPreview = () => {
const currencySymbol = EXCHANGE_RATE[attributes.currency]?.symbol || defaultSymbol;
return (
<div className="max-w-4xl mx-auto p-6">
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 p-6 transition-colors duration-200">
<Card
title={
className="max-w-4xl mx-auto shadow-lg rounded-2xl bg-white dark:bg-gray-800 transition-colors duration-200"
bodyStyle={{ padding: 0 }}
>
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center">
<Space>
<FileTextOutlined className="text-blue-500" />
<span>报价单预览</span>
<FileTextOutlined className="text-blue-500 dark:text-blue-400" />
<span className="text-gray-800 dark:text-gray-200 font-medium">报价单预览</span>
</Space>
<ExportPDFButton />
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={exportPDF}
className="bg-blue-500 hover:bg-blue-600 border-none rounded-full shadow-md hover:shadow-lg transition-all duration-200"
>
导出PDF
</Button>
</div>
}
>
<div id="quotation-content" className="p-6">
</div>
<div id="quotation-content" className="p-8 bg-white" style={{ width: '210mm', margin: '0 auto' }}>
<div className="text-center mb-8">
<Title level={2}>{attributes.quataName}</Title>
<Text type="secondary">创建日期{new Date(quotation.created_at).toLocaleDateString()}</Text>
<Title level={2} className="dark:text-gray-200">{attributes.quataName}</Title>
<Text type="secondary" className="dark:text-gray-400">
创建日期{new Date(quotation.created_at).toLocaleDateString()}
</Text>
</div>
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<Title level={4}>基本信息</Title>
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-2xl mb-6">
<Title level={4} className="dark:text-gray-200 mb-4">基本信息</Title>
<div className="grid grid-cols-2 gap-6">
<div>
<Text type="secondary">客户</Text>
<Space>
<Text type="secondary" className="dark:text-gray-400">客户</Text>
<Space className="mt-2">
{attributes.customers?.map(customer => (
<Tag key={customer.id} color="blue">{customer.name}</Tag>
<Tag
key={customer.id}
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-200 border-none"
>
{customer.name}
</Tag>
))}
</Space>
</div>
<div>
<Text type="secondary">货币类型</Text>
<Text>{attributes.currency}</Text>
<Text type="secondary" className="dark:text-gray-400">货币类型</Text>
<Text className="dark:text-gray-200 ml-2">{attributes.currency}</Text>
</div>
</div>
</div>
{attributes.sections?.map((section, sIndex) => (
<div key={sIndex} className="mb-6">
<div className="flex items-center gap-2 h-full">
<div key={sIndex} className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="h-4 w-1 bg-blue-500 rounded-full" />
<h2>{section.sectionName}</h2>
<h2 className="text-lg font-medium dark:text-gray-200">{section.sectionName}</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<div className="overflow-x-auto rounded-2xl border border-gray-200 dark:border-gray-700">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">项目明细</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">描述/备注</th>
@@ -194,9 +200,9 @@ const QuotationPreview = () => {
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">小计</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{section.items.map((item, iIndex) => (
<tr key={iIndex}>
<tr key={iIndex} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150">
<td className="px-4 py-3 text-sm text-gray-900">{item.name}</td>
<td className="px-4 py-3 text-sm text-gray-500">{item.description}</td>
<td className="px-4 py-3 text-sm text-gray-500">{item.unit}</td>
@@ -216,9 +222,9 @@ const QuotationPreview = () => {
))}
{/* 金额汇总 */}
<div className="mt-8 border-t pt-4">
<div className="flex justify-end space-y-2">
<div className="w-64 space-y-2">
<div className="mt-8 border-t border-gray-200 dark:border-gray-700 pt-6">
<div className="flex justify-end">
<div className="w-64 space-y-3">
<div className="flex justify-between">
<Text>税前总计</Text>
<Text>{currencySymbol}{attributes.beforeTaxAmount?.toLocaleString()}</Text>
@@ -238,9 +244,9 @@ const QuotationPreview = () => {
</div>
)}
<Divider className="my-2" />
<div className="flex justify-between">
<Text strong>最终金额</Text>
<Text strong className="text-blue-500 text-xl">
<div className="flex justify-between items-center">
<Text strong className="dark:text-gray-200">最终金额</Text>
<Text strong className="text-xl text-blue-500 dark:text-blue-400">
{currencySymbol}{(attributes.discount || attributes.afterTaxAmount)?.toLocaleString()}
</Text>
</div>
@@ -251,9 +257,9 @@ const QuotationPreview = () => {
{/* 补充说明 */}
{attributes.description && (
<div className="mt-8">
<Title level={4}>补充说明</Title>
<div className="bg-gray-50 p-4 rounded-lg">
<Text>{attributes.description}</Text>
<Title level={4} className="dark:text-gray-200">补充说明</Title>
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-2xl">
<Text className="dark:text-gray-300">{attributes.description}</Text>
</div>
</div>
)}

View File

@@ -36,7 +36,7 @@ const ServiceForm = () => {
return <div>无效的模板类型</div>;
}
return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2" >
<div className="bg-gradient-to-b min-h-screen p-2" >
<Card
className="shadow-lg rounded-lg border-0"
title={

View File

@@ -68,11 +68,18 @@ const ServicePage = () => {
},
};
// 添加分页相关状态
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
// 获取服务模板列表
const fetchServices = async () => {
const fetchServices = async (page = pagination.current, pageSize = pagination.pageSize) => {
try {
setLoading(true);
const { data: services } = await supabaseService.select("resources", {
const { data: services, total } = await supabaseService.select("resources", {
filter: {
type: { eq: "serviceTemplate" },
...(selectedType
@@ -83,9 +90,17 @@ const ServicePage = () => {
column: "created_at",
ascending: false,
},
page,
pageSize,
});
setData(services || []);
setPagination((prev) => ({
...prev,
current: page,
pageSize,
total,
}));
} catch (error) {
console.error("获取服务模板失败:", error);
message.error("获取服务模板失败");
@@ -93,12 +108,15 @@ const ServicePage = () => {
setLoading(false);
}
};
useEffect(() => {
setPagination((prev) => ({ ...prev, current: 1 }));
fetchServices(1, pagination.pageSize);
}, [selectedType]);
useEffect(() => {
fetchUnits();
}, []);
useEffect(() => {
fetchServices();
}, [selectedType]);
const fetchUnits = async () => {
setloadingUnits(true);
@@ -263,7 +281,6 @@ const ServicePage = () => {
title: "名称",
dataIndex: "name",
key: "name",
width: "15%",
render: (text, item, index) => {
const isEditing =
editingKey === `${record.id}-${section.key}-${index}`;
@@ -286,7 +303,6 @@ const ServicePage = () => {
title: "单位",
dataIndex: "unit",
key: "unit",
width: "10%",
render: (text, item, index) => {
const isEditing =
editingKey === `${record.id}-${section.key}-${index}`;
@@ -320,7 +336,6 @@ const ServicePage = () => {
title: "单价",
dataIndex: "price",
key: "price",
width: "10%",
render: (text, item, index) => {
const isEditing =
editingKey === `${record.id}-${section.key}-${index}`;
@@ -343,7 +358,6 @@ const ServicePage = () => {
title: "数量",
dataIndex: "quantity",
key: "quantity",
width: "10%",
render: (text, item, index) => {
const isEditing =
editingKey === `${record.id}-${section.key}-${index}`;
@@ -677,7 +691,7 @@ const ServicePage = () => {
</Popconfirm>
</div>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
dataSource={section.items}
pagination={false}
size="small"
@@ -714,7 +728,6 @@ const ServicePage = () => {
title: "模板名称",
dataIndex: ["attributes", "templateName"],
key: "templateName",
className: "min-w-[200px]",
},
{
title: "模板类型",
@@ -766,6 +779,7 @@ const ServicePage = () => {
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
width: 220,
render: (text) => new Date(text).toLocaleString(),
sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at),
},
@@ -868,8 +882,19 @@ const ServicePage = () => {
rowExpandable: (record) => record.attributes.sections?.length > 0,
}}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
onChange: (page, pageSize) => {
// 页码或页大小改变时触发
if (pageSize !== pagination.pageSize) {
// 如果是页大小改变,重置到第一页
fetchServices(1, pageSize);
} else {
fetchServices(page, pageSize);
}
},
}}
className="rounded-lg"
/>

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Form, Input, Space, message, Popconfirm, Drawer, Select, Segmented, Badge } from 'antd';
import { PlusOutlined, FileTextOutlined, ProjectOutlined, CheckSquareOutlined } from '@ant-design/icons';
import { Table, Button, Form, Input, Space, message, Popconfirm, Select, Segmented } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { supabaseService } from '@/hooks/supabaseService';
const Classify = ({activeType,typeList}) => {
const [data, setData] = useState([]);
@@ -8,8 +8,13 @@ const Classify = ({activeType,typeList}) => {
const [editingKey, setEditingKey] = useState('');
const [form] = Form.useForm();
const [filterType, setFilterType] = useState('all'); // 'all', 'common', 'current'
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const fetchCategories = async (type = activeType, filterTypeValue = filterType) => {
const fetchCategories = async (type = activeType, filterTypeValue = filterType, page = pagination.current, pageSize = pagination.pageSize) => {
setLoading(true);
try {
let filterCondition;
@@ -25,7 +30,7 @@ const Classify = ({activeType,typeList}) => {
filterCondition = { in: `(${type},common)` };
}
const { data: categories } = await supabaseService.select('resources', {
const { data: categories, total } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'categories' },
'attributes->>template_type': filterCondition
@@ -33,10 +38,18 @@ const Classify = ({activeType,typeList}) => {
order: {
column: 'created_at',
ascending: false
}
},
page,
pageSize
});
setData(categories || []);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total
}));
} catch (error) {
message.error('获取分类数据失败');
console.error(error);
@@ -46,8 +59,9 @@ const Classify = ({activeType,typeList}) => {
};
useEffect(() => {
fetchCategories(activeType, filterType);
}, [activeType]);
setPagination(prev => ({ ...prev, current: 1 }));
fetchCategories(activeType, filterType, 1, pagination.pageSize);
}, [activeType, filterType]);
// 新增分类
@@ -267,15 +281,23 @@ const Classify = ({activeType,typeList}) => {
<div className="rounded-lg shadow-sm">
<Form form={form}>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
onChange: (page, pageSize) => {
if (pageSize !== pagination.pageSize) {
fetchCategories(activeType, filterType, 1, pageSize);
} else {
fetchCategories(activeType, filterType, page, pageSize);
}
}
}}
className="rounded-lg"
/>

View File

@@ -9,8 +9,10 @@ export default function SectionComponent({ activeType }) {
return <QuataSections />;
case "task":
return <TaskSections />;
case "project":
return <Project/>
default:
return <Project></Project>;
return <div>暂无数据</div>
}
};
return <>{renderFn(activeType)}</>;

View File

@@ -24,7 +24,6 @@ import { supabaseService } from '@/hooks/supabaseService';
import { v4 as uuidv4 } from 'uuid';
import { supabase } from "@/config/supabase";
const { Text } = Typography;
const TYPE = 'project';
const ProjectSections = () => {
@@ -37,11 +36,15 @@ const ProjectSections = () => {
const [formValues, setFormValues] = useState({});
const [uploadModalVisible, setUploadModalVisible] = useState(false);
const [currentAddItem, setCurrentAddItem] = useState(null);
const fetchSections = async () => {
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const fetchSections = async (page = pagination.current, pageSize = pagination.pageSize) => {
setLoading(true);
try {
const { data: sections } = await supabaseService.select('resources', {
const { data: sections, total } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'sections' },
'attributes->>template_type': { eq: TYPE }
@@ -49,9 +52,18 @@ const ProjectSections = () => {
order: {
column: 'created_at',
ascending: false
}
},
page,
pageSize
});
setData(sections || []);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total
}));
} catch (error) {
message.error('获取模块数据失败');
console.error(error);
@@ -83,9 +95,9 @@ const ProjectSections = () => {
};
useEffect(() => {
fetchSections();
fetchSections(1, pagination.pageSize);
fetchUnits();
}, []);
}, [TYPE]);
const handleAdd = () => {
const newData = {
@@ -151,7 +163,7 @@ const ProjectSections = () => {
message.success('保存成功');
setEditingKey('');
fetchSections();
fetchSections(pagination.current, pagination.pageSize);
} catch (error) {
message.error('保存失败');
console.error(error);
@@ -162,7 +174,11 @@ const ProjectSections = () => {
try {
await supabaseService.delete('resources', { id: record.id });
message.success('删除成功');
fetchSections();
if (data.length === 1 && pagination.current > 1) {
fetchSections(pagination.current - 1, pagination.pageSize);
} else {
fetchSections(pagination.current, pagination.pageSize);
}
} catch (error) {
message.error('删除失败');
console.error(error);
@@ -528,9 +544,19 @@ const ProjectSections = () => {
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
if (pageSize !== pagination.pageSize) {
setPagination(prev => ({ ...prev, pageSize }));
fetchSections(1, pageSize);
} else {
fetchSections(page, pageSize);
}
}
}}
className="rounded-lg"
/>

View File

@@ -27,22 +27,34 @@ const SectionsManagement = () => {
const [loadingUnits,setLoadingUnits]=useState(false)
const [units,setUnit]=useState([])
const [formValues, setFormValues] = useState({});
const fetchSections = async () => {
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const fetchSections = async (page = pagination.current, pageSize = pagination.pageSize) => {
setLoading(true);
try {
const { data: sections } = await supabaseService.select('resources', {
const { data: sections, total } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'sections' },
'attributes->>template_type': {eq:TYPE}
'attributes->>template_type': { eq: TYPE }
},
order: {
column: 'created_at',
ascending: false
}
},
page,
pageSize
});
setData(sections || []);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total
}));
} catch (error) {
message.error('获取模块数据失败');
console.error(error);
@@ -72,8 +84,7 @@ const [formValues, setFormValues] = useState({});
}
};
useEffect(() => {
fetchSections();
fetchSections(1, pagination.pageSize);
}, [TYPE]);
const handleAdd = () => {
@@ -115,7 +126,7 @@ const [formValues, setFormValues] = useState({});
attributes: {
name: values.name,
template_type: TYPE,
items: items.filter(item => item.name), // 只保存有名称的项目
items: items.filter(item => item.name),
},
schema_version: 1
});
@@ -135,7 +146,7 @@ const [formValues, setFormValues] = useState({});
message.success('保存成功');
setEditingKey('');
fetchSections();
fetchSections(pagination.current, pagination.pageSize);
} catch (error) {
message.error('保存失败');
console.error(error);
@@ -146,7 +157,11 @@ const [formValues, setFormValues] = useState({});
try {
await supabaseService.delete('resources', { id: record.id });
message.success('删除成功');
fetchSections();
if (data.length === 1 && pagination.current > 1) {
fetchSections(pagination.current - 1, pagination.pageSize);
} else {
fetchSections(pagination.current, pagination.pageSize);
}
} catch (error) {
message.error('删除失败');
console.error(error);
@@ -450,9 +465,17 @@ const [formValues, setFormValues] = useState({});
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
onChange: (page, pageSize) => {
if (pageSize !== pagination.pageSize) {
fetchSections(1, pageSize);
} else {
fetchSections(page, pageSize);
}
}
}}
className="rounded-lg"
/>

View File

@@ -18,7 +18,6 @@ import { supabaseService } from '@/hooks/supabaseService';
import { v4 as uuidv4 } from 'uuid';
import dayjs from 'dayjs';
const { Text } = Typography;
const TYPE = 'task';
const TaskSections = () => {
@@ -29,11 +28,16 @@ const TaskSections = () => {
const [loadingUnits, setLoadingUnits] = useState(false);
const [units, setUnit] = useState([]);
const [formValues, setFormValues] = useState({});
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const fetchSections = async () => {
const fetchSections = async (page = pagination.current, pageSize = pagination.pageSize) => {
setLoading(true);
try {
const { data: sections } = await supabaseService.select('resources', {
const { data: sections, total } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'sections' },
'attributes->>template_type': { eq: TYPE }
@@ -41,9 +45,18 @@ const TaskSections = () => {
order: {
column: 'created_at',
ascending: false
}
},
page,
pageSize
});
setData(sections || []);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total
}));
} catch (error) {
message.error('获取模块数据失败');
console.error(error);
@@ -75,9 +88,9 @@ const TaskSections = () => {
};
useEffect(() => {
fetchSections();
fetchSections(1, pagination.pageSize);
fetchUnits();
}, []);
}, [TYPE]);
const handleAdd = () => {
const newData = {
@@ -144,7 +157,7 @@ const TaskSections = () => {
message.success('保存成功');
setEditingKey('');
fetchSections();
fetchSections(pagination.current, pagination.pageSize);
} catch (error) {
message.error('保存失败');
console.error(error);
@@ -155,7 +168,11 @@ const TaskSections = () => {
try {
await supabaseService.delete('resources', { id: record.id });
message.success('删除成功');
fetchSections();
if (data.length === 1 && pagination.current > 1) {
fetchSections(pagination.current - 1, pagination.pageSize);
} else {
fetchSections(pagination.current, pagination.pageSize);
}
} catch (error) {
message.error('删除失败');
console.error(error);
@@ -440,9 +457,19 @@ const TaskSections = () => {
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
if (pageSize !== pagination.pageSize) {
setPagination(prev => ({ ...prev, pageSize }));
fetchSections(1, pageSize);
} else {
fetchSections(page, pageSize);
}
}
}}
className="rounded-lg"
/>

View File

@@ -9,8 +9,13 @@ const UnitManagement = ({ activeType, typeList }) => {
const [editingKey, setEditingKey] = useState('');
const [form] = Form.useForm();
const [filterType, setFilterType] = useState('all');
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const fetchUnits = async (type = activeType, filterTypeValue = filterType) => {
const fetchUnits = async (type = activeType, filterTypeValue = filterType, page = pagination.current, pageSize = pagination.pageSize) => {
setLoading(true);
try {
let filterCondition;
@@ -26,7 +31,7 @@ const UnitManagement = ({ activeType, typeList }) => {
filterCondition = { in: `(${type},common)` };
}
const { data: units } = await supabaseService.select('resources', {
const { data: units, total } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'units' },
'attributes->>template_type': filterCondition
@@ -34,10 +39,18 @@ const UnitManagement = ({ activeType, typeList }) => {
order: {
column: 'created_at',
ascending: false
}
},
page,
pageSize
});
setData(units || []);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total
}));
} catch (error) {
message.error('获取单位数据失败');
console.error(error);
@@ -47,8 +60,9 @@ const UnitManagement = ({ activeType, typeList }) => {
};
useEffect(() => {
fetchUnits(activeType, filterType);
}, [activeType]);
setPagination(prev => ({ ...prev, current: 1 }));
fetchUnits(activeType, filterType, 1, pagination.pageSize);
}, [activeType, filterType]);
const handleAdd = () => {
const newData = {
@@ -263,15 +277,23 @@ const UnitManagement = ({ activeType, typeList }) => {
<div className="rounded-lg shadow-sm">
<Form form={form}>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
onChange: (page, pageSize) => {
if (pageSize !== pagination.pageSize) {
fetchUnits(activeType, filterType, 1, pageSize);
} else {
fetchUnits(activeType, filterType, page, pageSize);
}
}
}}
className="rounded-lg"
/>

View File

@@ -144,7 +144,7 @@ const SupplierPage = () => {
}
>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
columns={columns}
dataSource={suppliers}
rowKey="id"

View File

@@ -264,7 +264,7 @@ export default function TaskForm() {
}, [id, templateId]);
return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<div className="bg-gradient-to-b min-h-screen p-2">
<Card
className="shadow-lg rounded-lg border-0"
title={

View File

@@ -344,7 +344,7 @@ const TaskPage = () => {
</Tag>
</Space>
}
className="h-full w-full overflow-auto dark:bg-gray-800 dark:border-gray-700"
className="h-full w-full overflow-auto "
extra={
<Space>
<Select
@@ -385,7 +385,7 @@ const TaskPage = () => {
columns={columns}
dataSource={tasks}
rowKey="id"
scroll={{ x: true }}
scroll={{ x: true}}
loading={loadingTasks}
onChange={handleTableChange}
pagination={{
@@ -423,7 +423,7 @@ const TaskPage = () => {
type="primary"
disabled={!selectedTemplateId}
onClick={handleConfirm}
className="gap-2 px-6 rounded-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700"
className="gap-2 px-6 rounded-full "
>
<AppstoreOutlined /> 使用选中模板
</Button>,

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const MarketingCampaign = () => {
return <Outlet />;
};
export default MarketingCampaign;

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { Card, DatePicker } from 'antd';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
const CampaignPerformance = () => {
const data = [
{ name: '1月', 点击率: 400, 转化率: 240 },
{ name: '2月', 点击率: 300, 转化率: 139 },
{ name: '3月', 点击率: 200, 转化率: 980 },
];
return (
<Card title="活动成效分析">
<div className="mb-4">
<DatePicker.RangePicker />
</div>
<div style={{ width: '100%', height: 400 }}>
<ResponsiveContainer>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="点击率" stroke="#8884d8" />
<Line type="monotone" dataKey="转化率" stroke="#82ca9d" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
);
};
export default CampaignPerformance;

View File

@@ -1,48 +0,0 @@
import React from 'react';
import { Card, Table, Button, Tag } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const CampaignPlan = () => {
const columns = [
{
title: '计划名称',
dataIndex: 'name',
key: 'name',
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: status => (
<Tag color={status === '进行中' ? 'green' : 'default'}>
{status}
</Tag>
),
},
];
return (
<Card
title="活动计划"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增计划
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default CampaignPlan;

View File

@@ -1,45 +0,0 @@
import React from 'react';
import { Form, Input, DatePicker, Select } from 'antd';
import { STATUS } from '@/constants/status';
export const ProjectForm = ({ form }) => (
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="专案名称"
rules={[{ required: true, message: '请输入专案名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="manager"
label="负责人"
rules={[{ required: true, message: '请选择负责人' }]}
>
<Select placeholder="请选择负责人" />
</Form.Item>
<Form.Item
name="startDate"
label="开始日期"
rules={[{ required: true, message: '请选择开始日期' }]}
>
<DatePicker />
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select>
{Object.values(STATUS).map(status => (
<Select.Option key={status} value={status}>
{status}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
);

View File

@@ -1,71 +0,0 @@
import React, { useState } from 'react';
import { Card, Modal, Form } from 'antd';
import { PageHeader } from '@/components/common/PageHeader';
import { BaseTable } from '@/components/common/BaseTable';
import { ProjectForm } from './components/ProjectForm';
import { useProjectData } from './hooks/useProjectData';
import { getStatusColumn, getDateColumn } from '@/utils/tableColumns';
const CampaignProject = () => {
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const { loading, data, pagination, loadData } = useProjectData();
const columns = [
{
title: '专案名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'manager',
key: 'manager',
},
getDateColumn('开始日期', 'startDate'),
getStatusColumn(),
];
const handleAdd = () => {
setVisible(true);
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
console.log('Success:', values);
setVisible(false);
form.resetFields();
loadData();
} catch (error) {
console.error('Failed:', error);
}
};
return (
<Card>
<PageHeader
title="行销活动专案"
onAdd={handleAdd}
addButtonText="新增专案"
/>
<BaseTable
columns={columns}
dataSource={data}
loading={loading}
pagination={pagination}
onChange={loadData}
/>
<Modal
title="新增专案"
open={visible}
onOk={handleSubmit}
onCancel={() => setVisible(false)}
>
<ProjectForm form={form} />
</Modal>
</Card>
);
};
export default CampaignProject;

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const Communication = () => {
return <Outlet />;
};
export default Communication;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { Card, Timeline, Button } from 'antd';
import { ClockCircleOutlined } from '@ant-design/icons';
const CommunicationJourney = () => {
return (
<Card
title="沟通历程"
extra={
<Button type="primary">导出记录</Button>
}
>
<Timeline
mode="alternate"
items={[
{
children: '创建营销活动 2023-10-25 10:00:00',
},
{
children: '发送邮件通知 2023-10-25 10:30:00',
color: 'green',
},
{
dot: <ClockCircleOutlined style={{ fontSize: '16px' }} />,
children: '客户查看邮件 2023-10-25 11:00:00',
},
{
color: 'red',
children: '系统提醒跟进 2023-10-25 14:00:00',
},
{
children: '完成跟进 2023-10-25 16:00:00',
},
]}
/>
</Card>
);
};
export default CommunicationJourney;

View File

@@ -1,48 +0,0 @@
import React from 'react';
import { Card, Table, Button, Input } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const { Search } = Input;
const CommunicationList = () => {
const columns = [
{
title: '名单名称',
dataIndex: 'name',
key: 'name',
},
{
title: '联系人数量',
dataIndex: 'contactCount',
key: 'contactCount',
},
{
title: '创建日期',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '更新日期',
dataIndex: 'updatedAt',
key: 'updatedAt',
},
];
return (
<Card
title="名单管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增名单
</Button>
}
>
<div className="mb-4">
<Search placeholder="搜索名单" allowClear enterButton />
</div>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default CommunicationList;

View File

@@ -1,42 +0,0 @@
import React from 'react';
import { Card, Tabs, Button } from 'antd';
import { SendOutlined } from '@ant-design/icons';
const CommunicationPreview = () => {
const items = [
{
key: '1',
label: '邮件预览',
children: (
<div className="p-4 border rounded">
<h2>邮件主题</h2>
<div className="mt-4">邮件内容预览区域</div>
</div>
),
},
{
key: '2',
label: '短信预览',
children: (
<div className="p-4 border rounded">
<div className="mt-4">短信内容预览区域</div>
</div>
),
},
];
return (
<Card
title="消息预览"
extra={
<Button type="primary" icon={<SendOutlined />}>
发送测试
</Button>
}
>
<Tabs items={items} />
</Card>
);
};
export default CommunicationPreview;

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
const MarketingCenter = () => {
return <Outlet />;
};
export default MarketingCenter;

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { Card, Table, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
const TemplatePage = () => {
const columns = [
{
title: '模版名称',
dataIndex: 'name',
key: 'name',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '创建日期',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
];
return (
<Card
title="模版管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增模版
</Button>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />
</Card>
);
};
export default TemplatePage;

View File

@@ -0,0 +1 @@

View File

@@ -13,7 +13,7 @@ import {
TreeSelect
} from 'antd';
import { getAllRouteOptions, allRoutes } from '@/routes/routes';
import { supabase } from '@/config/supabase';
import { supabaseService } from '@/hooks/supabaseService';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
const { Title } = Typography;
@@ -25,6 +25,11 @@ export default function MenuManagement() {
const [menuList, setMenuList] = useState([]);
const [form] = Form.useForm();
const [roles, setRoles] = useState([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
// 获取所有路由选项
const routeOptions = getAllRouteOptions();
@@ -57,6 +62,7 @@ export default function MenuManagement() {
{
title: '操作',
key: 'action',
fixed:'right',
render: (_, record) => (
<Space>
<Button
@@ -80,19 +86,31 @@ export default function MenuManagement() {
];
// 获取菜单数据
const fetchMenuList = async () => {
const fetchMenuList = async (page = pagination.current, pageSize = pagination.pageSize) => {
setLoading(true);
try {
setLoading(true);
const { data, error } = await supabase
.from('resources')
.select('*')
.eq('type', TYPE)
.order('created_at', { ascending: false });
const { data: menus, total } = await supabaseService.select('resources', {
filter: {
'type': { eq: TYPE }
},
order: {
column: 'created_at',
ascending: false
},
page,
pageSize
});
if (error) throw error;
setMenuList(data);
setMenuList(menus || []);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total
}));
} catch (error) {
message.error('获取数据失败' + error.message);
message.error('获取菜单数据失败');
console.error(error);
} finally {
setLoading(false);
}
@@ -101,12 +119,12 @@ export default function MenuManagement() {
// 获取角色列表
const fetchRoles = async () => {
try {
const { data, error } = await supabase
.from('roles')
.select('*')
.order('name');
if (error) throw error;
const { data } = await supabaseService.select('roles', {
order: {
column: 'name',
ascending: true
}
});
setRoles(data || []);
} catch (error) {
message.error('获取角色数据失败');
@@ -127,27 +145,20 @@ export default function MenuManagement() {
}
};
let result;
if (form.getFieldValue('id')) {
result = await supabase
.from('resources')
.update(menuData)
.eq('id', form.getFieldValue('id'))
.select();
await supabaseService.update('resources',
{ id: form.getFieldValue('id') },
menuData
);
} else {
result = await supabase
.from('resources')
.insert([menuData])
.select();
await supabaseService.insert('resources', menuData);
}
if (result.error) throw result.error;
message.success('保存成功');
setIsModalVisible(false);
fetchMenuList();
fetchMenuList(pagination.current, pagination.pageSize);
} catch (error) {
message.error('保存失败:' + error.message);
message.error('保存失败:' + (error.message || '未知错误'));
} finally {
setLoading(false);
}
@@ -168,19 +179,17 @@ export default function MenuManagement() {
const handleDelete = async (id) => {
try {
setLoading(true);
const {data, error } = await supabase
.from('resources')
.delete()
.eq('id', id);
if (error) throw error;
if(data.length>0){
message.success('删除成功');
fetchMenuList();
}else{
message.error('删除失败');
await supabaseService.delete('resources', { id });
message.success('删除成功');
// 如果当前页只有一条数据且不是第一页,删除后自动跳转到上一页
if (menuList.length === 1 && pagination.current > 1) {
fetchMenuList(pagination.current - 1, pagination.pageSize);
} else {
fetchMenuList(pagination.current, pagination.pageSize);
}
} catch (error) {
message.error('删除失败:' + error.message);
message.error('删除失败:' + (error.message || '未知错误'));
} finally {
setLoading(false);
}
@@ -196,7 +205,7 @@ export default function MenuManagement() {
};
useEffect(() => {
fetchMenuList();
fetchMenuList(1, pagination.pageSize);
fetchRoles();
}, []);
@@ -221,6 +230,23 @@ export default function MenuManagement() {
columns={columns}
dataSource={menuList}
loading={loading}
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
if (pageSize !== pagination.pageSize) {
// 如果是改变每页条数,重置到第一页
setPagination(prev => ({ ...prev, pageSize }));
fetchMenuList(1, pageSize);
} else {
// 如果是改变页码,获取对应页数据
fetchMenuList(page, pageSize);
}
}
}}
rowKey="id"
/>

View File

@@ -261,7 +261,7 @@ const ResourceTaskForm = () => {
bordered={false}
>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
dataSource={dataSource}
columns={columns}
pagination={false}

View File

@@ -148,7 +148,7 @@ const ResourceTask = () => {
}
>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
columns={columns}
dataSource={quotations}
rowKey="id"

View File

@@ -52,7 +52,6 @@ export default function PermissionManagement() {
resources:resource_id(*)
`, { count: 'exact' });
// 添加排序
if (params.field && params.order) {
const ascending = params.order === 'ascend';
query = query.order(params.field, { ascending });
@@ -366,17 +365,18 @@ export default function PermissionManagement() {
{
title: '操作',
key: 'operation',
fixed: 'right',
render: (_, record) => (
<div className="space-x-2">
<Button
type="text"
type="link"
icon={<EditOutlined />}
onClick={() => {
setModalType('edit');
form.setFieldsValue(record);
setModalVisible(true);
}}
/>
>编辑</Button>
<Popconfirm
title="确认删除"
description="确定要删除这条权限记录吗?"
@@ -385,10 +385,10 @@ export default function PermissionManagement() {
onConfirm={() => handleDelete(record.id)}
>
<Button
type="text"
type="link"
danger
icon={<DeleteOutlined />}
/>
>删除</Button>
</Popconfirm>
</div>
),
@@ -522,7 +522,7 @@ export default function PermissionManagement() {
return (
<App>
<Card title="权限管理" bordered={false}>
<Card title="权限管理" >
<RoleHeader
onAdd={() => {
setModalType('add');
@@ -538,12 +538,23 @@ export default function PermissionManagement() {
/>
<Table
scroll={{ x: true}}
columns={columns}
dataSource={permissions}
rowKey="id"
loading={loading}
pagination={pagination}
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
setPagination(prev => ({ ...prev,page, pageSize }));
}
}}
onChange={handleTableChange}
className='w-full'
/>
<Modal

View File

@@ -212,7 +212,7 @@ export const MembershipTable = ({ memberships, onUpdate, onDelete, onAdd }) => {
</Button>
<Form form={form} component={false}>
<Table
scroll={{ x: true }}
scroll={{ x: true}}
components={{
body: {
cell: EditableMembershipCell,

View File

@@ -3,7 +3,7 @@ import { Table, Form, Button, Space, Popconfirm, Tag, message } from 'antd';
import { EditOutlined, DeleteOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
import { EditableCell } from './EditableCell';
import { ExpandedMemberships } from './ExpandedMemberships';
export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,onDelete ,onUpdate}) => {
export const TeamTable = ({ tableLoading,pagination,dataSource,setPagination, onTableChange,onDelete ,onUpdate}) => {
const [form] = Form.useForm();
const [editingKey, setEditingKey] = useState('');
const [loading, setLoading] = useState(false);
@@ -106,6 +106,7 @@ export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,on
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
render: (_, record) => {
const editable = isEditing(record);
return editable ? (
@@ -180,8 +181,7 @@ export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,on
return (
<Form form={form} component={false}>
<Table
scroll={{ x: true }}
pagination={pagination}
scroll={{ x: true}}
loading={loading||tableLoading}
components={{
body: {
@@ -192,6 +192,16 @@ export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,on
dataSource={dataSource}
columns={mergedColumns}
rowKey="id"
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
setPagination(prev => ({ ...prev,page, pageSize }));
}
}}
expandable={{
expandedRowRender: (record) => (
<ExpandedMemberships

View File

@@ -183,9 +183,10 @@ const TeamManagement = () => {
return (
<App>
<Card title="团队管理" bordered={false}>
<Card title="团队管理" >
<TeamHeader onSearch={handleSearch} onAdd={handleAdd} />
<TeamTable
setPagination={setPagination}
loading={loading}
dataSource={teams}
pagination={pagination}

View File

@@ -69,7 +69,7 @@ const AppRoutes = () => {
path="/login"
element={
user?.id ? (
<Navigate to="/home" replace />
<Navigate to="/dashboard" replace />
) : (
<Login />
)
@@ -86,7 +86,7 @@ const AppRoutes = () => {
>
<Route
index
element={<Navigate to="/home" replace />}
element={<Navigate to="/dashboard" replace />}
/>
{renderRoutes(allRoutes)}
<Route path="*" element={<NotFound />} />

View File

@@ -5,6 +5,7 @@
:root {
// Light mode colors
--primary-color: #1677ff;
--primary-gradient: linear-gradient(45deg, #1677ff, #36cff0);
--success-color: #52c41a;
@@ -12,6 +13,25 @@
--error-color: #ff4d4f;
--font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif;
// Google-style scrollbar colors (Light mode)
--scrollbar-thumb: rgba(0, 0, 0, .2);
--scrollbar-thumb-hover: rgba(0, 0, 0, .4);
--scrollbar-track: transparent;
}
:root.dark {
// Dark mode colors
--primary-color: #4f9eff; // 更亮的蓝色适配暗色模式
--primary-gradient: linear-gradient(45deg, #4f9eff, #60deff);
--success-color: #6ede3b;
--warning-color: #ffc53d;
--error-color: #ff7875;
// Google-style scrollbar colors (Dark mode)
--scrollbar-thumb: rgba(255, 255, 255, .2);
--scrollbar-thumb-hover: rgba(255, 255, 255, .4);
--scrollbar-track: transparent;
}
body {
@@ -152,42 +172,36 @@ body {
}
}
/* 滚动条基础样式 */
/* 自定义滚动条样式 */
::-webkit-scrollbar {
width: 8px;
/* 垂直滚动条宽度 */
height: 8px;
/* 水平滚动条高度 */
}
/* 亮色模式滚动条样式 */
::-webkit-scrollbar-track {
@apply bg-gray-100;
/* 轨道背景色 */
background: var(--scrollbar-track);
}
::-webkit-scrollbar-thumb {
@apply bg-gray-300 rounded-full hover:bg-gray-400 transition-colors;
/* 滑块样式 */
background-color: var(--scrollbar-thumb);
border-radius: 4px;
transition: background-color 0.2s ease;
}
/* 暗色模式滚动条样式 */
.dark {
::-webkit-scrollbar-track {
@apply bg-gray-800;
/* 暗色模式轨道背景 */
}
::-webkit-scrollbar-thumb {
@apply bg-gray-600 hover:bg-gray-500;
/* 暗色模式滑块样式 */
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover);
}
/* Firefox 滚动条样式 */
* {
scrollbar-width: thin;
scrollbar-color: theme('colors.gray.300') theme('colors.gray.100');
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
@media (prefers-color-scheme: dark) {
* {
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
}
.dark * {

View File

@@ -1,5 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
const withMT = require("@material-tailwind/react/utils/withMT");
module.exports = withMT({
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
@@ -133,4 +135,4 @@ export default {
corePlugins: {
preflight: false,
},
}
})

View File

@@ -1,9 +1,8 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
// 使用 __dirname 需要添加 import.meta.url 的支持
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -19,13 +18,13 @@ export default defineConfig({
'@contexts': path.resolve(__dirname, 'src/contexts'),
'@assets': path.resolve(__dirname, 'src/assets'),
},
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] // 添加文件扩展名自动解析
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
},
server: {
port: 3000,
open: true,
host: true,
strictPort: true,
optimizeDeps: {
include: ['react-apexcharts', 'apexcharts'],
esbuildOptions: {
target: 'es2020',
},
},
build: {
outDir: 'dist',
@@ -34,8 +33,19 @@ export default defineConfig({
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom', 'antd'],
charts: ['react-apexcharts', 'apexcharts'],
},
},
},
target: 'es2020',
commonjsOptions: {
include: [/node_modules/],
},
},
server: {
port: 3000,
open: true,
host: true,
strictPort: true,
},
});

273
yarn.lock
View File

@@ -537,6 +537,18 @@
dependencies:
"@emotion/memoize" "^0.8.1"
"@emotion/is-prop-valid@^0.8.2":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/memoize@0.7.4":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@emotion/memoize@^0.8.1":
version "0.8.1"
resolved "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17"
@@ -709,6 +721,21 @@
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.8"
"@floating-ui/dom@^1.2.1":
version "1.6.13"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34"
integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==
dependencies:
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.9"
"@floating-ui/react-dom@^1.2.2":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3"
integrity sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==
dependencies:
"@floating-ui/dom" "^1.2.1"
"@floating-ui/react-dom@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31"
@@ -716,6 +743,15 @@
dependencies:
"@floating-ui/dom" "^1.0.0"
"@floating-ui/react@0.19.0":
version "0.19.0"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1"
integrity sha512-fgYvN4ksCi5OvmPXkyOT8o5a8PSKHMzPHt+9mR6KYWdF16IAjWRLZPAAziI2sznaWT23drRFrYw64wdvYqqaQw==
dependencies:
"@floating-ui/react-dom" "^1.2.2"
aria-hidden "^1.1.3"
tabbable "^6.0.1"
"@floating-ui/react@^0.26.28":
version "0.26.28"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.28.tgz#93f44ebaeb02409312e9df9507e83aab4a8c0dc7"
@@ -730,6 +766,16 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@floating-ui/utils@^0.2.9":
version "0.2.9"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
"@heroicons/react@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.2.0.tgz#0c05124af50434a800773abec8d3af6a297d904b"
integrity sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==
"@humanwhocodes/config-array@^0.13.0":
version "0.13.0"
resolved "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
@@ -818,6 +864,21 @@
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.15.2.tgz#f583ecfb7b92cad17c67f7825f20f23b72e6d7ff"
integrity sha512-p8dsW0fdJxzYhULbm1noFYRHuBvJHleYviC0BlwbkVySC8AsvFI8AmC3sMssWV3dQ3yQ/SidYo9U+K/czpDpZw==
"@material-tailwind/react@^2.1.10":
version "2.1.10"
resolved "https://registry.yarnpkg.com/@material-tailwind/react/-/react-2.1.10.tgz#e4ff8b8a5cf1a39209d408dda3217f632c080e9a"
integrity sha512-xGU/mLDKDBp/qZ8Dp2XR7fKcTpDuFeZEBqoL9Bk/29kakKxNxjUGYSRHEFLsyOFf4VIhU6WGHdIS7tOA3QGJHA==
dependencies:
"@floating-ui/react" "0.19.0"
classnames "2.3.2"
deepmerge "4.2.2"
framer-motion "6.5.1"
material-ripple-effects "2.0.1"
prop-types "15.8.1"
react "18.2.0"
react-dom "18.2.0"
tailwind-merge "1.8.1"
"@monaco-editor/loader@^1.4.0":
version "1.4.0"
resolved "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
@@ -832,6 +893,59 @@
dependencies:
"@monaco-editor/loader" "^1.4.0"
"@motionone/animation@^10.12.0":
version "10.18.0"
resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.18.0.tgz#868d00b447191816d5d5cf24b1cafa144017922b"
integrity sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==
dependencies:
"@motionone/easing" "^10.18.0"
"@motionone/types" "^10.17.1"
"@motionone/utils" "^10.18.0"
tslib "^2.3.1"
"@motionone/dom@10.12.0":
version "10.12.0"
resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.12.0.tgz#ae30827fd53219efca4e1150a5ff2165c28351ed"
integrity sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==
dependencies:
"@motionone/animation" "^10.12.0"
"@motionone/generators" "^10.12.0"
"@motionone/types" "^10.12.0"
"@motionone/utils" "^10.12.0"
hey-listen "^1.0.8"
tslib "^2.3.1"
"@motionone/easing@^10.18.0":
version "10.18.0"
resolved "https://registry.yarnpkg.com/@motionone/easing/-/easing-10.18.0.tgz#7b82f6010dfee3a1bb0ee83abfbaff6edae0c708"
integrity sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==
dependencies:
"@motionone/utils" "^10.18.0"
tslib "^2.3.1"
"@motionone/generators@^10.12.0":
version "10.18.0"
resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.18.0.tgz#fe09ab5cfa0fb9a8884097feb7eb60abeb600762"
integrity sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==
dependencies:
"@motionone/types" "^10.17.1"
"@motionone/utils" "^10.18.0"
tslib "^2.3.1"
"@motionone/types@^10.12.0", "@motionone/types@^10.17.1":
version "10.17.1"
resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.17.1.tgz#cf487badbbdc9da0c2cb86ffc1e5d11147c6e6fb"
integrity sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==
"@motionone/utils@^10.12.0", "@motionone/utils@^10.18.0":
version "10.18.0"
resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.18.0.tgz#a59ff8932ed9009624bca07c56b28ef2bb2f885e"
integrity sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==
dependencies:
"@motionone/types" "^10.17.1"
hey-listen "^1.0.8"
tslib "^2.3.1"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1096,6 +1210,33 @@
"@supabase/realtime-js" "2.11.2"
"@supabase/storage-js" "2.7.1"
"@svgdotjs/svg.draggable.js@^3.0.4":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.5.tgz#804fa627cbe850a137350009bd03d2c8423b39a7"
integrity sha512-ljL/fB0tAjRfFOJGhXpr7rEx9DJ6D7Pxt3AXvgxjEM17g6wK3Ho9nXhntraOMx8JLZdq4NBMjokeXMvnQzJVYA==
"@svgdotjs/svg.filter.js@^3.0.8":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.8.tgz#998cb2481a871fa70d7dbaa891c886b335c562d7"
integrity sha512-YshF2YDaeRA2StyzAs5nUPrev7npQ38oWD0eTRwnsciSL2KrRPMoUw8BzjIXItb3+dccKGTX3IQOd2NFzmHkog==
dependencies:
"@svgdotjs/svg.js" "^3.1.1"
"@svgdotjs/svg.js@^3.1.1", "@svgdotjs/svg.js@^3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz#4716be92a64c66b29921b63f7235fcfb953fb13a"
integrity sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==
"@svgdotjs/svg.resize.js@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz#732e4cae15d09ad3021adeac63bc9fad0dc7255a"
integrity sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==
"@svgdotjs/svg.select.js@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.select.js/-/svg.select.js-4.0.2.tgz#80a10409e6c73206218690eac5c9f94f8c8909b5"
integrity sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==
"@types/babel__core@^7.20.5":
version "7.20.5"
resolved "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
@@ -1268,6 +1409,11 @@
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@yr/monotone-cubic-spline@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
integrity sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -1398,6 +1544,18 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
apexcharts@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-4.3.0.tgz#eccf28e830ce1b5e018cfc0e99d1c6af0076c9c7"
integrity sha512-PfvZQpv91T68hzry9l5zP3Gip7sQvF0nFK91uCBrswIKX7rbIdbVNS4fOks9m9yP3Ppgs6LHgU2M/mjoG4NM0A==
dependencies:
"@svgdotjs/svg.draggable.js" "^3.0.4"
"@svgdotjs/svg.filter.js" "^3.0.8"
"@svgdotjs/svg.js" "^3.2.4"
"@svgdotjs/svg.resize.js" "^2.0.2"
"@svgdotjs/svg.select.js" "^4.0.1"
"@yr/monotone-cubic-spline" "^1.0.3"
arg@^5.0.2:
version "5.0.2"
resolved "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
@@ -1408,6 +1566,13 @@ argparse@^2.0.1:
resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.1.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
dependencies:
tslib "^2.0.0"
array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b"
@@ -1655,6 +1820,11 @@ chokidar@^4.0.0:
dependencies:
readdirp "^4.0.1"
classnames@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2, classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
@@ -1889,6 +2059,11 @@ deep-is@^0.1.3:
resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deepmerge@4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
define-data-property@^1.0.1, define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
@@ -2403,6 +2578,27 @@ fraction.js@^4.3.7:
resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
framer-motion@6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7"
integrity sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==
dependencies:
"@motionone/dom" "10.12.0"
framesync "6.0.1"
hey-listen "^1.0.8"
popmotion "11.0.3"
style-value-types "5.0.0"
tslib "^2.1.0"
optionalDependencies:
"@emotion/is-prop-valid" "^0.8.2"
framesync@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20"
integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==
dependencies:
tslib "^2.1.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2581,6 +2777,16 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
heroicons@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/heroicons/-/heroicons-2.2.0.tgz#f1f554155152b4ec4d1b7165363d7c583690f77d"
integrity sha512-yOwvztmNiBWqR946t+JdgZmyzEmnRMC2nxvHFC90bF1SUttwB6yJKYeme1JeEcBfobdOs827nCyiWBS2z/brog==
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
highlight.js@^11.10.0:
version "11.11.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.11.1.tgz#fca06fa0e5aeecf6c4d437239135fabc15213585"
@@ -3033,6 +3239,11 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
material-ripple-effects@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/material-ripple-effects/-/material-ripple-effects-2.0.1.tgz#47803d2ab1561698d930e2524a7a9a19fb2829b7"
integrity sha512-hHlUkZAuXbP94lu02VgrPidbZ3hBtgXBtjlwR8APNqOIgDZMV8MCIcsclL8FmGJQHvnORyvoQgC965vPsiyXLQ==
math-intrinsics@^1.0.0, math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@@ -3302,6 +3513,16 @@ pirates@^4.0.1:
resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
popmotion@11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9"
integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==
dependencies:
framesync "6.0.1"
hey-listen "^1.0.8"
style-value-types "5.0.0"
tslib "^2.1.0"
possible-typed-array-names@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
@@ -3374,7 +3595,7 @@ prelude-ls@^1.2.1:
resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.8.1:
prop-types@15.8.1, prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -3771,6 +3992,21 @@ rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
rc-resize-observer "^1.0.0"
rc-util "^5.36.0"
react-apexcharts@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.7.0.tgz#bbd08425674224adb27c9f2c62477d43bd5de539"
integrity sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==
dependencies:
prop-types "^15.8.1"
react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-dom@^18.2.0:
version "18.3.1"
resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
@@ -3911,6 +4147,13 @@ react-use@^17.6.0:
ts-easing "^0.2.0"
tslib "^2.1.0"
react@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
react@^18.2.0:
version "18.3.1"
resolved "https://registry.npmmirror.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
@@ -3951,9 +4194,9 @@ recharts-scale@^0.4.4:
dependencies:
decimal.js-light "^2.4.1"
recharts@^2.9.0:
recharts@^2.15.0:
version "2.15.0"
resolved "https://registry.npmmirror.com/recharts/-/recharts-2.15.0.tgz#0b77bff57a43885df9769ae649a14cb1a7fe19aa"
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.0.tgz#0b77bff57a43885df9769ae649a14cb1a7fe19aa"
integrity sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==
dependencies:
clsx "^2.0.0"
@@ -3999,6 +4242,11 @@ regexp.prototype.flags@^1.5.3:
es-errors "^1.3.0"
set-function-name "^2.0.2"
remixicon@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-4.6.0.tgz#f2adafd18aaa983d61f8c9f7274fa17706da4ddc"
integrity sha512-bKM5odjqE1yzVxEZGJE7F79WHhNrJFIKHXR+GG+P1IWXn8AnJZhl8SbIRDJsNAvIqx4VPkNwjuHfc42tutMDpQ==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -4101,7 +4349,7 @@ sass@^1.69.5:
optionalDependencies:
"@parcel/watcher" "^2.4.1"
scheduler@^0.23.2:
scheduler@^0.23.0, scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
@@ -4390,6 +4638,14 @@ strip-json-comments@^3.1.1:
resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-value-types@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad"
integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==
dependencies:
hey-listen "^1.0.8"
tslib "^2.1.0"
styled-components@^6.1.0:
version "6.1.13"
resolved "https://registry.npmmirror.com/styled-components/-/styled-components-6.1.13.tgz#2d777750b773b31469bd79df754a32479e9f475e"
@@ -4453,11 +4709,16 @@ swr@^2.0.0, swr@^2.2.5:
dequal "^2.0.3"
use-sync-external-store "^1.4.0"
tabbable@^6.0.0:
tabbable@^6.0.0, tabbable@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwind-merge@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.8.1.tgz#0e56c8afbab2491f72e06381043ffec8b720ba04"
integrity sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww==
tailwindcss@^3.3.5:
version "3.4.17"
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
@@ -4574,7 +4835,7 @@ tslib@2.6.2:
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@^2.0.0, tslib@^2.1.0:
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.1:
version "2.8.1"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==