- {sidebarCollapsed ? 'E' : 'ERP Platform'}
+ {/* 现代深色侧边栏 */}
+
+ {/* Logo 区域 */}
+ navigate('/')}>
+
E
+ {!sidebarCollapsed && (
+
ERP Platform
+ )}
-
-
-
diff --git a/apps/web/src/pages/Home.tsx b/apps/web/src/pages/Home.tsx
index 411bcaa..3915a60 100644
--- a/apps/web/src/pages/Home.tsx
+++ b/apps/web/src/pages/Home.tsx
@@ -1,11 +1,23 @@
-import { useEffect, useState } from 'react';
-import { Typography, Card, Row, Col, Statistic, Spin } from 'antd';
+import { useEffect, useState, useCallback, useRef } from 'react';
+import { Row, Col, Spin, theme } from 'antd';
import {
UserOutlined,
- TeamOutlined,
+ SafetyCertificateOutlined,
FileTextOutlined,
BellOutlined,
+ ThunderboltOutlined,
+ SettingOutlined,
+ PartitionOutlined,
+ ClockCircleOutlined,
+ ApartmentOutlined,
+ CheckCircleOutlined,
+ TeamOutlined,
+ FileProtectOutlined,
+ RiseOutlined,
+ FallOutlined,
+ RightOutlined,
} from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
import client from '../api/client';
import { useMessageStore } from '../stores/message';
@@ -16,6 +28,76 @@ interface DashboardStats {
unreadMessages: number;
}
+interface TrendData {
+ value: string;
+ direction: 'up' | 'down' | 'neutral';
+ label: string;
+}
+
+interface StatCardConfig {
+ key: string;
+ title: string;
+ value: number;
+ icon: React.ReactNode;
+ gradient: string;
+ iconBg: string;
+ delay: string;
+ trend: TrendData;
+ sparkline: number[];
+ onClick?: () => void;
+}
+
+interface TaskItem {
+ id: string;
+ title: string;
+ priority: 'high' | 'medium' | 'low';
+ assignee: string;
+ dueText: string;
+ color: string;
+ icon: React.ReactNode;
+ path: string;
+}
+
+interface ActivityItem {
+ id: string;
+ text: string;
+ time: string;
+ icon: React.ReactNode;
+}
+
+function useCountUp(end: number, duration = 800) {
+ const [count, setCount] = useState(0);
+ const prevEnd = useRef(end);
+
+ useEffect(() => {
+ if (end === prevEnd.current && count > 0) return;
+ prevEnd.current = end;
+
+ if (end === 0) { setCount(0); return; }
+
+ const startTime = performance.now();
+ const startVal = 0;
+
+ function tick(now: number) {
+ const elapsed = now - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+ const eased = 1 - Math.pow(1 - progress, 3);
+ setCount(Math.round(startVal + (end - startVal) * eased));
+ if (progress < 1) requestAnimationFrame(tick);
+ }
+
+ requestAnimationFrame(tick);
+ }, [end, duration]);
+
+ return count;
+}
+
+function StatValue({ value, loading }: { value: number; loading: boolean }) {
+ const animatedValue = useCountUp(value);
+ if (loading) return
;
+ return
{animatedValue.toLocaleString()};
+}
+
export default function Home() {
const [stats, setStats] = useState
({
userCount: 0,
@@ -24,19 +106,27 @@ export default function Home() {
unreadMessages: 0,
});
const [loading, setLoading] = useState(true);
- const { unreadCount, fetchUnreadCount } = useMessageStore();
+ const unreadCount = useMessageStore((s) => s.unreadCount);
+ const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount);
+ const { token } = theme.useToken();
+ const navigate = useNavigate();
+
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
useEffect(() => {
+ let cancelled = false;
+
async function loadStats() {
setLoading(true);
try {
- // 并行请求各模块统计数据
const [usersRes, rolesRes, instancesRes] = await Promise.allSettled([
client.get('/users', { params: { page: 1, page_size: 1 } }),
client.get('/roles', { params: { page: 1, page_size: 1 } }),
client.get('/workflow/instances', { params: { page: 1, page_size: 1 } }),
]);
+ if (cancelled) return;
+
const extractTotal = (res: PromiseSettledResult<{ data: { data?: { total?: number } } }>) =>
res.status === 'fulfilled' ? (res.value.data?.data?.total ?? 0) : 0;
@@ -47,51 +137,282 @@ export default function Home() {
unreadMessages: unreadCount,
});
} catch {
- // 静默处理,显示默认值
+ // 静默处理
} finally {
- setLoading(false);
+ if (!cancelled) setLoading(false);
}
}
fetchUnreadCount();
loadStats();
+
+ return () => { cancelled = true; };
}, [fetchUnreadCount, unreadCount]);
+ const handleNavigate = useCallback((path: string) => {
+ navigate(path);
+ }, [navigate]);
+
+ const statCards: StatCardConfig[] = [
+ {
+ key: 'users',
+ title: '用户总数',
+ value: stats.userCount,
+ icon: ,
+ gradient: 'linear-gradient(135deg, #4F46E5, #6366F1)',
+ iconBg: 'rgba(79, 70, 229, 0.12)',
+ delay: 'erp-fade-in erp-fade-in-delay-1',
+ trend: { value: '+2', direction: 'up', label: '较上周' },
+ sparkline: [30, 45, 35, 50, 40, 55, 60, 50, 65, 70],
+ onClick: () => handleNavigate('/users'),
+ },
+ {
+ key: 'roles',
+ title: '角色数量',
+ value: stats.roleCount,
+ icon: ,
+ gradient: 'linear-gradient(135deg, #059669, #10B981)',
+ iconBg: 'rgba(5, 150, 105, 0.12)',
+ delay: 'erp-fade-in erp-fade-in-delay-2',
+ trend: { value: '+1', direction: 'up', label: '较上月' },
+ sparkline: [20, 25, 30, 28, 35, 40, 38, 42, 45, 50],
+ onClick: () => handleNavigate('/roles'),
+ },
+ {
+ key: 'processes',
+ title: '流程实例',
+ value: stats.processInstanceCount,
+ icon: ,
+ gradient: 'linear-gradient(135deg, #D97706, #F59E0B)',
+ iconBg: 'rgba(217, 119, 6, 0.12)',
+ delay: 'erp-fade-in erp-fade-in-delay-3',
+ trend: { value: '0', direction: 'neutral', label: '较昨日' },
+ sparkline: [10, 15, 12, 20, 18, 25, 22, 28, 24, 20],
+ onClick: () => handleNavigate('/workflow'),
+ },
+ {
+ key: 'messages',
+ title: '未读消息',
+ value: stats.unreadMessages,
+ icon: ,
+ gradient: 'linear-gradient(135deg, #E11D48, #F43F5E)',
+ iconBg: 'rgba(225, 29, 72, 0.12)',
+ delay: 'erp-fade-in erp-fade-in-delay-4',
+ trend: { value: '0', direction: 'neutral', label: '全部已读' },
+ sparkline: [5, 8, 3, 10, 6, 12, 8, 4, 7, 5],
+ onClick: () => handleNavigate('/messages'),
+ },
+ ];
+
+ const quickActions = [
+ { icon: , label: '用户管理', path: '/users', color: '#4F46E5' },
+ { icon: , label: '权限管理', path: '/roles', color: '#059669' },
+ { icon: , label: '组织架构', path: '/organizations', color: '#D97706' },
+ { icon: , label: '工作流', path: '/workflow', color: '#7C3AED' },
+ { icon: , label: '消息中心', path: '/messages', color: '#E11D48' },
+ { icon: , label: '系统设置', path: '/settings', color: '#64748B' },
+ ];
+
+ const pendingTasks: TaskItem[] = [
+ { id: '1', title: '审核新用户注册申请', priority: 'high', assignee: '系统', dueText: '待处理', color: '#DC2626', icon: , path: '/users' },
+ { id: '2', title: '配置工作流审批节点', priority: 'medium', assignee: '管理员', dueText: '进行中', color: '#D97706', icon: , path: '/workflow' },
+ { id: '3', title: '更新角色权限策略', priority: 'low', assignee: '管理员', dueText: '计划中', color: '#059669', icon: , path: '/roles' },
+ ];
+
+ const recentActivities: ActivityItem[] = [
+ { id: '1', text: '系统管理员 创建了 管理员角色', time: '刚刚', icon: },
+ { id: '2', text: '系统管理员 配置了 工作流模板', time: '5 分钟前', icon: },
+ { id: '3', text: '系统管理员 更新了 组织架构', time: '10 分钟前', icon: },
+ { id: '4', text: '系统管理员 设置了 消息通知偏好', time: '30 分钟前', icon: },
+ ];
+
+ const priorityLabel: Record = { high: '紧急', medium: '一般', low: '低' };
+
return (
-
工作台
-
-
-
-
- } />
-
-
-
-
- } />
-
-
-
-
- }
- />
-
-
-
-
- }
- />
-
-
-
-
+ {/* 欢迎语 */}
+
+
+ 工作台
+
+
+ 欢迎回来,这是您的系统概览
+
+
+
+ {/* 统计卡片行 */}
+
+ {statCards.map((card) => {
+ const maxSpark = Math.max(...card.sparkline, 1);
+ return (
+
+ { if (e.key === 'Enter') card.onClick?.(); }}
+ >
+
+
+
+
{card.title}
+
+
+
+
+ {card.trend.direction === 'up' && }
+ {card.trend.direction === 'down' && }
+ {card.trend.value}
+ {card.trend.label}
+
+
+
{card.icon}
+
+
+ {card.sparkline.map((v, i) => (
+
+ ))}
+
+
+
+ );
+ })}
+
+
+ {/* 待办任务 + 最近活动 */}
+
+ {/* 待办任务 */}
+
+
+
+
+ 待办任务
+
+ {pendingTasks.length} 项待处理
+
+
+
+ {pendingTasks.map((task) => (
+
handleNavigate(task.path)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(task.path); }}
+ >
+
{task.icon}
+
+
{task.title}
+
+ {task.assignee}
+ {task.dueText}
+
+
+
+ {priorityLabel[task.priority]}
+
+
+
+ ))}
+
+
+
+
+ {/* 最近活动 */}
+
+
+
+
+ 最近动态
+
+
+ {recentActivities.map((activity) => (
+
+ ))}
+
+
+
+
+
+ {/* 快捷入口 + 系统信息 */}
+
+
+
+
+
+ 快捷入口
+
+
+ {quickActions.map((action) => (
+
+ handleNavigate(action.path)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(action.path); }}
+ >
+
{action.icon}
+
{action.label}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ 系统信息
+
+
+ {[
+ { label: '系统版本', value: 'v0.1.0' },
+ { label: '后端框架', value: 'Axum 0.8 + Tokio' },
+ { label: '数据库', value: 'PostgreSQL 16' },
+ { label: '缓存', value: 'Redis 7' },
+ { label: '前端框架', value: 'React 19 + Ant Design 6' },
+ { label: '模块数量', value: '5 个业务模块' },
+ ].map((item) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+
+
);
}
diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx
index 5d12831..941a456 100644
--- a/apps/web/src/pages/Login.tsx
+++ b/apps/web/src/pages/Login.tsx
@@ -1,11 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Form, Input, Button, Card, message, Typography } from 'antd';
-import { UserOutlined, LockOutlined } from '@ant-design/icons';
+import { Form, Input, Button, message, Divider } from 'antd';
+import { UserOutlined, LockOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import { useAuthStore } from '../stores/auth';
-const { Title } = Typography;
-
export default function Login() {
const navigate = useNavigate();
const login = useAuthStore((s) => s.login);
@@ -26,43 +24,186 @@ export default function Login() {
};
return (
-
+
{contextHolder}
-
-
-
+
+ {/* 左侧品牌展示区 */}
+
+ {/* 装饰性背景元素 */}
+
+
+
+ {/* 品牌内容 */}
+
+
+
+
+
+
ERP Platform
-
- 企业资源管理平台
+
+
+ 新一代模块化企业资源管理平台
+
+
+ 身份权限 · 工作流引擎 · 消息中心 · 系统配置
+
+
+ {/* 底部特性点 */}
+
+ {[
+ { label: '多租户架构', value: 'SaaS' },
+ { label: '模块化设计', value: '可插拔' },
+ { label: '事件驱动', value: '可扩展' },
+ ].map((item) => (
+
+
+ {item.value}
+
+
+ {item.label}
+
+
+ ))}
+
-
- } placeholder="用户名" />
-
-
- } placeholder="密码" />
-
-
-
-
-
-
+
+
+ {/* 右侧登录表单区 */}
+
+
+
+ 欢迎回来
+
+
+ 请登录您的账户以继续
+
+
+
+
+
+ }
+ placeholder="用户名"
+ style={{ height: 44, borderRadius: 10 }}
+ />
+
+
+ }
+ placeholder="密码"
+ style={{ height: 44, borderRadius: 10 }}
+ />
+
+
+
+
+
+
+
+
+ ERP Platform v0.1.0 · Powered by Rust + React
+
+
+
+
);
}
diff --git a/apps/web/src/pages/Messages.tsx b/apps/web/src/pages/Messages.tsx
index 38258e7..7311610 100644
--- a/apps/web/src/pages/Messages.tsx
+++ b/apps/web/src/pages/Messages.tsx
@@ -1,11 +1,11 @@
import { useState } from 'react';
import { Tabs } from 'antd';
+import { BellOutlined, MailOutlined, FileTextOutlined, SettingOutlined } from '@ant-design/icons';
import NotificationList from './messages/NotificationList';
import MessageTemplates from './messages/MessageTemplates';
import NotificationPreferences from './messages/NotificationPreferences';
import type { MessageQuery } from '../api/messages';
-/** 预定义的过滤器,避免每次渲染创建新引用导致子组件无限重渲染。 */
const UNREAD_FILTER: MessageQuery = { is_read: false };
export default function Messages() {
@@ -13,28 +13,56 @@ export default function Messages() {
return (
+
+
+
消息中心
+
管理站内消息、模板和通知偏好
+
+
+
+
+ 全部消息
+
+ ),
children: ,
},
{
key: 'unread',
- label: '未读消息',
+ label: (
+
+
+ 未读消息
+
+ ),
children: ,
},
{
key: 'templates',
- label: '消息模板',
+ label: (
+
+
+ 消息模板
+
+ ),
children: ,
},
{
key: 'preferences',
- label: '通知设置',
+ label: (
+
+
+ 通知设置
+
+ ),
children: ,
},
]}
diff --git a/apps/web/src/pages/Organizations.tsx b/apps/web/src/pages/Organizations.tsx
index b07bda4..c2e25a4 100644
--- a/apps/web/src/pages/Organizations.tsx
+++ b/apps/web/src/pages/Organizations.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import {
Tree,
Button,
@@ -14,6 +14,7 @@ import {
Card,
Empty,
Tag,
+ theme,
} from 'antd';
import {
PlusOutlined,
@@ -39,6 +40,15 @@ import {
} from '../api/orgs';
export default function Organizations() {
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
+
+ const cardStyle = {
+ background: isDark ? '#111827' : '#FFFFFF',
+ borderRadius: 12,
+ border: `1px solid ${isDark ? '#1E293B' : '#F1F5F9'}`,
+ };
+
// --- Org tree state ---
const [orgTree, setOrgTree] = useState([]);
const [selectedOrg, setSelectedOrg] = useState(null);
@@ -67,7 +77,6 @@ export default function Organizations() {
try {
const tree = await listOrgTree();
setOrgTree(tree);
- // Clear selection if org no longer exists
if (selectedOrg) {
const stillExists = findOrgInTree(tree, selectedOrg.id);
if (!stillExists) {
@@ -152,8 +161,7 @@ export default function Organizations() {
fetchOrgTree();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '操作失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '操作失败';
message.error(errorMsg);
}
};
@@ -168,8 +176,7 @@ export default function Organizations() {
fetchOrgTree();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '删除失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '删除失败';
message.error(errorMsg);
}
};
@@ -194,8 +201,7 @@ export default function Organizations() {
fetchDeptTree();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '操作失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '操作失败';
message.error(errorMsg);
}
};
@@ -209,8 +215,7 @@ export default function Organizations() {
fetchDeptTree();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '删除失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '删除失败';
message.error(errorMsg);
}
};
@@ -236,8 +241,7 @@ export default function Organizations() {
fetchPositions();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '操作失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '操作失败';
message.error(errorMsg);
}
};
@@ -259,7 +263,13 @@ export default function Organizations() {
title: (
{item.name}{' '}
- {item.code && {item.code}}
+ {item.code && {item.code}}
),
children: convertOrgTree(item.children),
@@ -271,13 +281,18 @@ export default function Organizations() {
title: (
{item.name}{' '}
- {item.code && {item.code}}
+ {item.code && {item.code}}
),
children: convertDeptTree(item.children),
}));
- // --- Helper to find node in tree ---
const onSelectOrg = (selectedKeys: React.Key[]) => {
if (selectedKeys.length === 0) {
setSelectedOrg(null);
@@ -315,7 +330,7 @@ export default function Organizations() {
title="确定删除此岗位?"
onConfirm={() => handleDeletePosition(record.id)}
>
- }>
+ }>
删除
@@ -325,41 +340,45 @@ export default function Organizations() {
return (
-
-
-
- 组织架构管理
-
+ {/* 页面标题 */}
+
+
+
+
+ 组织架构管理
+
+
管理组织、部门和岗位的层级结构
+
+ {/* 三栏布局 */}
- {/* Left: Organization Tree */}
-
+ {/* 左栏:组织树 */}
+
+
+ 组织
+
}
onClick={() => {
setEditOrg(null);
orgForm.resetFields();
setOrgModalOpen(true);
}}
- >
- 新建
-
+ />
{selectedOrg && (
<>
}
onClick={() => {
setEditOrg(selectedOrg);
@@ -375,80 +394,96 @@ export default function Organizations() {
title="确定删除此组织?"
onConfirm={() => handleDeleteOrg(selectedOrg.id)}
>
- } />
+ } />
>
)}
- }
- >
- {orgTree.length > 0 ? (
-
- ) : (
-
- )}
-
+
+
+ {orgTree.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
- {/* Middle: Department Tree */}
-
+ {/* 中栏:部门树 */}
+
+
+
+ {selectedOrg ? `${selectedOrg.name} · 部门` : '部门'}
+
+ {selectedOrg && (
+
}
onClick={() => {
deptForm.resetFields();
setDeptModalOpen(true);
}}
- >
- 新建
-
+ />
{selectedDept && (
handleDeleteDept(selectedDept.id)}
>
- } />
+ } />
)}
- ) : null
- }
- >
- {selectedOrg ? (
- deptTree.length > 0 ? (
-
+ )}
+
+
+ {selectedOrg ? (
+ deptTree.length > 0 ? (
+
+ ) : (
+
+ )
) : (
-
- )
- ) : (
-
- )}
-
+
+ )}
+
+
- {/* Right: Positions */}
-
+
+
+ {selectedDept ? `${selectedDept.name} · 岗位` : '岗位'}
+
+ {selectedDept && (
}
onClick={() => {
positionForm.resetFields();
@@ -457,21 +492,24 @@ export default function Organizations() {
>
新建岗位
- ) : null
- }
- >
- {selectedDept ? (
-
- ) : (
-
- )}
-
+ )}
+
+
+ {selectedDept ? (
+
+ ) : (
+
+
+
+ )}
+
+
{/* Org Modal */}
@@ -484,7 +522,7 @@ export default function Organizations() {
}}
onOk={() => orgForm.submit()}
>
-
setDeptModalOpen(false)}
onOk={() => deptForm.submit()}
>
- setPositionModalOpen(false)}
onOk={() => positionForm.submit()}
>
- (null);
const [selectedPermIds, setSelectedPermIds] = useState([]);
const [form] = Form.useForm();
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchRoles = useCallback(async () => {
setLoading(true);
@@ -51,7 +53,7 @@ export default function Roles() {
try {
setPermissions(await listPermissions());
} catch {
- // Permissions may not be seeded yet; silently ignore
+ // 静默处理
}
}, []);
@@ -79,8 +81,7 @@ export default function Roles() {
fetchRoles();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '操作失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '操作失败';
message.error(errorMsg);
}
};
@@ -140,41 +141,110 @@ export default function Roles() {
};
const columns = [
- { title: '名称', dataIndex: 'name', key: 'name' },
- { title: '编码', dataIndex: 'code', key: 'code' },
+ {
+ title: '角色名称',
+ dataIndex: 'name',
+ key: 'name',
+ render: (v: string, record: RoleInfo) => (
+
+ ),
+ },
+ {
+ title: '编码',
+ dataIndex: 'code',
+ key: 'code',
+ render: (v: string) => (
+
+ {v}
+
+ ),
+ },
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
+ render: (v: string | undefined) => (
+ {v || '-'}
+ ),
},
{
title: '类型',
dataIndex: 'is_system',
key: 'is_system',
- render: (v: boolean) =>
- v ? 系统 : 自定义,
+ width: 100,
+ render: (v: boolean) => (
+
+ {v ? '系统' : '自定义'}
+
+ ),
},
{
title: '操作',
key: 'actions',
+ width: 180,
render: (_: unknown, record: RoleInfo) => (
-
-
{!record.is_system && (
<>
- openEditModal(record)}>
- 编辑
-
+ }
+ onClick={() => openEditModal(record)}
+ style={{ color: isDark ? '#94A3B8' : '#64748B' }}
+ />
handleDelete(record.id)}
>
-
- 删除
-
+ }
+ danger
+ />
>
)}
@@ -183,7 +253,6 @@ export default function Roles() {
},
];
- // Group permissions by resource for better UX
const groupedPermissions = permissions.reduce>(
(acc, p) => {
if (!acc[p.resource]) acc[p.resource] = [];
@@ -195,36 +264,42 @@ export default function Roles() {
return (
-
-
- 角色管理
-
+ {/* 页面标题和工具栏 */}
+
+
} onClick={openCreateModal}>
新建角色
-
+ {/* 表格容器 */}
+
+
`共 ${t} 条记录` }}
+ />
+
+ {/* 新建/编辑角色弹窗 */}
form.submit()}
+ width={480}
>
-
+ {/* 权限分配弹窗 */}
- {Object.entries(groupedPermissions).map(([resource, perms]) => (
-
-
- {resource}
-
-
+
+ {Object.entries(groupedPermissions).map(([resource, perms]) => (
+
+
+ {resource}
+
setSelectedPermIds(values as string[])}
- options={perms.map((p) => ({ label: p.name, value: p.id }))}
- />
+ style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}
+ >
+ {perms.map((p) => (
+
+ {p.name}
+
+ ))}
+
-
- ))}
+ ))}
+
);
diff --git a/apps/web/src/pages/Users.tsx b/apps/web/src/pages/Users.tsx
index fd3ff41..f635f0f 100644
--- a/apps/web/src/pages/Users.tsx
+++ b/apps/web/src/pages/Users.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
@@ -10,9 +10,18 @@ import {
Popconfirm,
Checkbox,
message,
- Typography,
+ theme,
} from 'antd';
-import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import {
+ PlusOutlined,
+ SearchOutlined,
+ EditOutlined,
+ DeleteOutlined,
+ UserOutlined,
+ SafetyCertificateOutlined,
+ StopOutlined,
+ CheckCircleOutlined,
+} from '@ant-design/icons';
import {
listUsers,
createUser,
@@ -26,9 +35,15 @@ import { listRoles, type RoleInfo } from '../api/roles';
import type { UserInfo } from '../api/auth';
const STATUS_COLOR_MAP: Record = {
- active: 'green',
- disabled: 'red',
- locked: 'orange',
+ active: '#059669',
+ disabled: '#DC2626',
+ locked: '#D97706',
+};
+
+const STATUS_BG_MAP: Record = {
+ active: '#ECFDF5',
+ disabled: '#FEF2F2',
+ locked: '#FFFBEB',
};
const STATUS_LABEL_MAP: Record = {
@@ -43,15 +58,15 @@ export default function Users() {
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
-
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editUser, setEditUser] = useState(null);
const [roleModalOpen, setRoleModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [allRoles, setAllRoles] = useState([]);
const [selectedRoleIds, setSelectedRoleIds] = useState([]);
-
const [form] = Form.useForm();
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchUsers = useCallback(async (p = page) => {
setLoading(true);
@@ -70,7 +85,7 @@ export default function Users() {
const result = await listRoles();
setAllRoles(result.data);
} catch {
- // Roles may not be seeded yet; silently ignore
+ // 静默处理
}
}, []);
@@ -112,8 +127,7 @@ export default function Users() {
fetchUsers();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '操作失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '操作失败';
message.error(errorMsg);
}
};
@@ -179,25 +193,68 @@ export default function Users() {
setRoleModalOpen(true);
};
- // Server-side search is handled by fetchUsers — no client filtering needed.
const filteredUsers = users;
const columns = [
- { title: '用户名', dataIndex: 'username', key: 'username' },
{
- title: '显示名',
- dataIndex: 'display_name',
- key: 'display_name',
+ title: '用户',
+ dataIndex: 'username',
+ key: 'username',
+ render: (v: string, record: UserInfo) => (
+
+
+ {(record.display_name?.[0] || v?.[0] || 'U').toUpperCase()}
+
+
+
{v}
+ {record.display_name && (
+
+ {record.display_name}
+
+ )}
+
+
+ ),
+ },
+ {
+ title: '邮箱',
+ dataIndex: 'email',
+ key: 'email',
+ render: (v: string | undefined) => v || '-',
+ },
+ {
+ title: '电话',
+ dataIndex: 'phone',
+ key: 'phone',
render: (v: string | undefined) => v || '-',
},
- { title: '邮箱', dataIndex: 'email', key: 'email' },
- { title: '电话', dataIndex: 'phone', key: 'phone' },
{
title: '状态',
dataIndex: 'status',
key: 'status',
+ width: 100,
render: (status: string) => (
-
+
{STATUS_LABEL_MAP[status] || status}
),
@@ -208,44 +265,68 @@ export default function Users() {
key: 'roles',
render: (roles: RoleInfo[]) =>
roles.length > 0
- ? roles.map((r) => {r.name})
- : '-',
+ ? roles.map((r) => (
+
+ {r.name}
+
+ ))
+ : -,
},
{
title: '操作',
key: 'actions',
+ width: 240,
render: (_: unknown, record: UserInfo) => (
-
- openEditModal(record)}>
- 编辑
-
- openRoleModal(record)}>
- 分配角色
-
+
+ }
+ onClick={() => openEditModal(record)}
+ style={{ color: isDark ? '#94A3B8' : '#64748B' }}
+ />
+ }
+ onClick={() => openRoleModal(record)}
+ style={{ color: isDark ? '#94A3B8' : '#64748B' }}
+ />
{record.status === 'active' ? (
handleToggleStatus(record.id, 'disabled')}
>
-
- 禁用
-
+ }
+ danger
+ />
) : (
}
onClick={() => handleToggleStatus(record.id, 'active')}
- >
- 启用
-
+ style={{ color: '#059669' }}
+ />
)}
handleDelete(record.id)}
>
-
- 删除
-
+ }
+ danger
+ />
),
@@ -254,23 +335,20 @@ export default function Users() {
return (
-
-
- 用户管理
-
-
+ {/* 页面标题和工具栏 */}
+
+
+
用户管理
+
管理系统用户账户、角色分配和状态
+
+
}
+ placeholder="搜索用户名..."
+ prefix={}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
+ style={{ width: 220, borderRadius: 8 }}
/>
- {
- setPage(p);
- fetchUsers(p);
- },
- }}
- />
+ {/* 表格容器 */}
+
+
{
+ setPage(p);
+ fetchUsers(p);
+ },
+ showTotal: (t) => `共 ${t} 条记录`,
+ style: { padding: '12px 16px', margin: 0 },
+ }}
+ />
+
+ {/* 新建/编辑用户弹窗 */}
form.submit()}
+ width={480}
>
-
-
+ } disabled={!!editUser} />
{!editUser && (
+ {/* 角色分配弹窗 */}
setRoleModalOpen(false)}
onOk={handleAssignRoles}
+ width={480}
>
- setSelectedRoleIds(values as string[])}
- options={allRoles.map((r) => ({
- label: `${r.name} (${r.code})`,
- value: r.id,
- }))}
- />
+
+
setSelectedRoleIds(values as string[])}
+ style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
+ >
+ {allRoles.map((r) => (
+
+
+ {r.name}
+
+ {r.code}
+
+
+
+ ))}
+
+
);
diff --git a/apps/web/src/pages/Workflow.tsx b/apps/web/src/pages/Workflow.tsx
index 84b2630..75eedb1 100644
--- a/apps/web/src/pages/Workflow.tsx
+++ b/apps/web/src/pages/Workflow.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
-import { Tabs } from 'antd';
+import { Tabs, theme } from 'antd';
+import { PartitionOutlined, FileSearchOutlined, CheckSquareOutlined, MonitorOutlined } from '@ant-design/icons';
import ProcessDefinitions from './workflow/ProcessDefinitions';
import PendingTasks from './workflow/PendingTasks';
import CompletedTasks from './workflow/CompletedTasks';
@@ -7,17 +8,63 @@ import InstanceMonitor from './workflow/InstanceMonitor';
export default function Workflow() {
const [activeKey, setActiveKey] = useState('definitions');
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
return (
+
+
+
工作流引擎
+
管理流程定义、审批任务和流程监控
+
+
+
},
- { key: 'pending', label: '我的待办', children:
},
- { key: 'completed', label: '我的已办', children:
},
- { key: 'instances', label: '流程监控', children:
},
+ {
+ key: 'definitions',
+ label: (
+
+
+ 流程定义
+
+ ),
+ children:
,
+ },
+ {
+ key: 'pending',
+ label: (
+
+
+ 我的待办
+
+ ),
+ children:
,
+ },
+ {
+ key: 'completed',
+ label: (
+
+
+ 我的已办
+
+ ),
+ children:
,
+ },
+ {
+ key: 'instances',
+ label: (
+
+
+ 流程监控
+
+ ),
+ children:
,
+ },
]}
/>
diff --git a/apps/web/src/pages/messages/MessageTemplates.tsx b/apps/web/src/pages/messages/MessageTemplates.tsx
index 959475b..eabd628 100644
--- a/apps/web/src/pages/messages/MessageTemplates.tsx
+++ b/apps/web/src/pages/messages/MessageTemplates.tsx
@@ -1,8 +1,16 @@
-import { useEffect, useState } from 'react';
-import { Table, Button, Modal, Form, Input, Select, message } from 'antd';
+import { useEffect, useState, useCallback } from 'react';
+import { Table, Button, Modal, Form, Input, Select, message, theme, Tag } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { listTemplates, createTemplate, type MessageTemplateInfo } from '../../api/messageTemplates';
+const channelMap: Record = {
+ in_app: { label: '站内', color: '#4F46E5' },
+ email: { label: '邮件', color: '#059669' },
+ sms: { label: '短信', color: '#D97706' },
+ wechat: { label: '微信', color: '#7C3AED' },
+};
+
export default function MessageTemplates() {
const [data, setData] = useState([]);
const [total, setTotal] = useState(0);
@@ -10,8 +18,10 @@ export default function MessageTemplates() {
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
- const fetchData = async (p = page) => {
+ const fetchData = useCallback(async (p = page) => {
setLoading(true);
try {
const result = await listTemplates(p, 20);
@@ -22,12 +32,11 @@ export default function MessageTemplates() {
} finally {
setLoading(false);
}
- };
+ }, [page]);
useEffect(() => {
fetchData(1);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [fetchData]);
const handleCreate = async () => {
try {
@@ -43,47 +52,115 @@ export default function MessageTemplates() {
};
const columns: ColumnsType = [
- { title: '名称', dataIndex: 'name', key: 'name' },
- { title: '编码', dataIndex: 'code', key: 'code' },
+ {
+ title: '名称',
+ dataIndex: 'name',
+ key: 'name',
+ render: (v: string) => {v},
+ },
+ {
+ title: '编码',
+ dataIndex: 'code',
+ key: 'code',
+ render: (v: string) => (
+
+ {v}
+
+ ),
+ },
{
title: '通道',
dataIndex: 'channel',
key: 'channel',
+ width: 90,
render: (c: string) => {
- const map: Record = { in_app: '站内', email: '邮件', sms: '短信', wechat: '微信' };
- return map[c] || c;
+ const info = channelMap[c] || { label: c, color: '#64748B' };
+ return (
+
+ {info.label}
+
+ );
},
},
- { title: '标题模板', dataIndex: 'title_template', key: 'title_template', ellipsis: true },
- { title: '语言', dataIndex: 'language', key: 'language', width: 80 },
- { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
+ {
+ title: '标题模板',
+ dataIndex: 'title_template',
+ key: 'title_template',
+ ellipsis: true,
+ },
+ {
+ title: '语言',
+ dataIndex: 'language',
+ key: 'language',
+ width: 80,
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ key: 'created_at',
+ width: 180,
+ render: (v: string) => (
+ {v}
+ ),
+ },
];
return (
-
-
setModalOpen(true)}>新建模板
+
+
+ 共 {total} 个模板
+
+ } onClick={() => setModalOpen(true)}>
+ 新建模板
+
+
+
+
+
{ setPage(p); fetchData(p); },
+ showTotal: (t) => `共 ${t} 条记录`,
+ }}
+ />
- { setPage(p); fetchData(p); },
- }}
- />
{ setModalOpen(false); form.resetFields(); }}
+ width={520}
>
-
diff --git a/apps/web/src/pages/messages/NotificationList.tsx b/apps/web/src/pages/messages/NotificationList.tsx
index c984feb..a3a6b40 100644
--- a/apps/web/src/pages/messages/NotificationList.tsx
+++ b/apps/web/src/pages/messages/NotificationList.tsx
@@ -1,5 +1,6 @@
-import { useEffect, useState, useMemo, useCallback } from 'react';
-import { Table, Button, Tag, Space, Modal, Typography, message } from 'antd';
+import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
+import { Table, Button, Tag, Space, Modal, Typography, message, theme } from 'antd';
+import { CheckOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { listMessages, markRead, markAllRead, deleteMessage, type MessageInfo, type MessageQuery } from '../../api/messages';
@@ -9,11 +10,19 @@ interface Props {
queryFilter?: MessageQuery;
}
+const priorityStyles: Record = {
+ urgent: { bg: '#FEF2F2', color: '#DC2626', text: '紧急' },
+ important: { bg: '#FFFBEB', color: '#D97706', text: '重要' },
+ normal: { bg: '#EEF2FF', color: '#4F46E5', text: '普通' },
+};
+
export default function NotificationList({ queryFilter }: Props) {
const [data, setData] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchData = useCallback(async (p = page, filter?: MessageQuery) => {
setLoading(true);
@@ -28,11 +37,14 @@ export default function NotificationList({ queryFilter }: Props) {
}
}, [page]);
- // 使用 JSON 序列化比较确保只在 filter 内容变化时触发
const filterKey = useMemo(() => JSON.stringify(queryFilter), [queryFilter]);
+ const isFirstRender = useRef(true);
useEffect(() => {
- fetchData(1, queryFilter);
+ if (isFirstRender.current) {
+ isFirstRender.current = false;
+ fetchData(1, queryFilter);
+ }
}, [filterKey, fetchData, queryFilter]);
const handleMarkRead = async (id: string) => {
@@ -71,7 +83,7 @@ export default function NotificationList({ queryFilter }: Props) {
content: (
{record.body}
-
@@ -82,19 +94,30 @@ export default function NotificationList({ queryFilter }: Props) {
}
};
- const priorityColor: Record
= {
- urgent: 'red',
- important: 'orange',
- normal: 'blue',
- };
-
const columns: ColumnsType = [
{
title: '标题',
dataIndex: 'title',
key: 'title',
render: (text: string, record) => (
- showDetail(record)}>
+ showDetail(record)}
+ >
+ {!record.is_read && (
+
+ )}
{text}
),
@@ -103,43 +126,82 @@ export default function NotificationList({ queryFilter }: Props) {
title: '优先级',
dataIndex: 'priority',
key: 'priority',
- width: 100,
- render: (p: string) => {p},
+ width: 90,
+ render: (p: string) => {
+ const info = priorityStyles[p] || { bg: '#F1F5F9', color: '#64748B', text: p };
+ return (
+
+ {info.text}
+
+ );
+ },
},
{
title: '发送者',
dataIndex: 'sender_type',
key: 'sender_type',
width: 80,
- render: (s: string) => (s === 'system' ? '系统' : '用户'),
+ render: (s: string) => {s === 'system' ? '系统' : '用户'},
},
{
title: '状态',
dataIndex: 'is_read',
key: 'is_read',
width: 80,
- render: (r: boolean) => (r ? 已读 : 未读),
+ render: (r: boolean) => (
+
+ {r ? '已读' : '未读'}
+
+ ),
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
+ render: (v: string) => (
+ {v}
+ ),
},
{
title: '操作',
key: 'actions',
width: 120,
render: (_: unknown, record) => (
-
+
{!record.is_read && (
- handleMarkRead(record.id)}>
- 标记已读
-
+ }
+ onClick={() => handleMarkRead(record.id)}
+ style={{ color: '#4F46E5' }}
+ />
)}
- handleDelete(record.id)}>
- 删除
-
+ }
+ onClick={() => showDetail(record)}
+ style={{ color: isDark ? '#64748B' : '#94A3B8' }}
+ />
+ }
+ onClick={() => handleDelete(record.id)}
+ />
),
},
@@ -147,22 +209,40 @@ export default function NotificationList({ queryFilter }: Props) {
return (
-
-
共 {total} 条消息
-
全部标记已读
+
+
+ 共 {total} 条消息
+
+ } onClick={handleMarkAllRead}>
+ 全部标记已读
+
+
+
+
+
{ setPage(p); fetchData(p, queryFilter); },
+ showTotal: (t) => `共 ${t} 条记录`,
+ }}
+ />
- { setPage(p); fetchData(p, queryFilter); },
- }}
- />
);
}
diff --git a/apps/web/src/pages/messages/NotificationPreferences.tsx b/apps/web/src/pages/messages/NotificationPreferences.tsx
index d18e3eb..7f4873d 100644
--- a/apps/web/src/pages/messages/NotificationPreferences.tsx
+++ b/apps/web/src/pages/messages/NotificationPreferences.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
-import { Form, Switch, TimePicker, Button, Card, message } from 'antd';
+import { Form, Switch, TimePicker, Button, message, theme } from 'antd';
+import { BellOutlined } from '@ant-design/icons';
import client from '../../api/client';
interface PreferencesData {
@@ -12,12 +13,11 @@ export default function NotificationPreferences() {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dndEnabled, setDndEnabled] = useState(false);
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
useEffect(() => {
- // 加载当前偏好设置
- form.setFieldsValue({
- dnd_enabled: false,
- });
+ form.setFieldsValue({ dnd_enabled: false });
}, [form]);
const handleSave = async () => {
@@ -45,7 +45,18 @@ export default function NotificationPreferences() {
};
return (
-
+
+
+
+ 通知偏好设置
+
+
@@ -53,7 +64,7 @@ export default function NotificationPreferences() {
{dndEnabled && (
-
+
)}
@@ -63,6 +74,6 @@ export default function NotificationPreferences() {
-
+
);
}
diff --git a/apps/web/src/pages/settings/AuditLogViewer.tsx b/apps/web/src/pages/settings/AuditLogViewer.tsx
index 9dd9397..1cf2d35 100644
--- a/apps/web/src/pages/settings/AuditLogViewer.tsx
+++ b/apps/web/src/pages/settings/AuditLogViewer.tsx
@@ -1,5 +1,5 @@
-import { useState, useEffect, useCallback } from 'react';
-import { Table, Select, Input, Space, Card, Typography, Tag, message } from 'antd';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { Table, Select, Input, Space, Tag, message, theme } from 'antd';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs';
@@ -16,10 +16,10 @@ const RESOURCE_TYPE_OPTIONS = [
{ value: 'numbering_rule', label: '编号规则' },
];
-const ACTION_COLOR_MAP: Record = {
- create: 'green',
- update: 'blue',
- delete: 'red',
+const ACTION_STYLES: Record = {
+ create: { bg: '#ECFDF5', color: '#059669', text: '创建' },
+ update: { bg: '#EEF2FF', color: '#4F46E5', text: '更新' },
+ delete: { bg: '#FEF2F2', color: '#DC2626', text: '删除' },
};
function formatDateTime(value: string): string {
@@ -38,6 +38,8 @@ export default function AuditLogViewer() {
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState({ page: 1, page_size: 20 });
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchLogs = useCallback(async (params: AuditLogQuery) => {
setLoading(true);
@@ -51,8 +53,12 @@ export default function AuditLogViewer() {
setLoading(false);
}, []);
+ const isFirstRender = useRef(true);
useEffect(() => {
- fetchLogs(query);
+ if (isFirstRender.current) {
+ isFirstRender.current = false;
+ fetchLogs(query);
+ }
}, [query, fetchLogs]);
const handleFilterChange = (field: keyof AuditLogQuery, value: string | undefined) => {
@@ -76,16 +82,35 @@ export default function AuditLogViewer() {
title: '操作',
dataIndex: 'action',
key: 'action',
- width: 120,
- render: (action: string) => (
- {action}
- ),
+ width: 100,
+ render: (action: string) => {
+ const info = ACTION_STYLES[action] || { bg: '#F1F5F9', color: '#64748B', text: action };
+ return (
+
+ {info.text}
+
+ );
+ },
},
{
title: '资源类型',
dataIndex: 'resource_type',
key: 'resource_type',
- width: 140,
+ width: 120,
+ render: (v: string) => (
+
+ {v}
+
+ ),
},
{
title: '资源 ID',
@@ -93,6 +118,11 @@ export default function AuditLogViewer() {
key: 'resource_id',
width: 200,
ellipsis: true,
+ render: (v: string) => (
+
+ {v}
+
+ ),
},
{
title: '操作用户',
@@ -100,57 +130,81 @@ export default function AuditLogViewer() {
key: 'user_id',
width: 200,
ellipsis: true,
+ render: (v: string) => (
+
+ {v}
+
+ ),
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
- width: 200,
- render: (value: string) => formatDateTime(value),
+ width: 180,
+ render: (value: string) => (
+
+ {formatDateTime(value)}
+
+ ),
},
];
return (
-
- 审计日志
-
+ {/* 筛选工具栏 */}
+
+
-
-
-
-
-
-
`共 ${t} 条`,
- }}
- scroll={{ x: 900 }}
- />
+ {/* 表格 */}
+
+
`共 ${t} 条`,
+ }}
+ scroll={{ x: 900 }}
+ />
+
);
}
diff --git a/apps/web/src/pages/settings/DictionaryManager.tsx b/apps/web/src/pages/settings/DictionaryManager.tsx
index 7af9340..55b7fa2 100644
--- a/apps/web/src/pages/settings/DictionaryManager.tsx
+++ b/apps/web/src/pages/settings/DictionaryManager.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
diff --git a/apps/web/src/pages/settings/LanguageManager.tsx b/apps/web/src/pages/settings/LanguageManager.tsx
index fb693bf..8562228 100644
--- a/apps/web/src/pages/settings/LanguageManager.tsx
+++ b/apps/web/src/pages/settings/LanguageManager.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import {
Table,
Switch,
diff --git a/apps/web/src/pages/settings/MenuConfig.tsx b/apps/web/src/pages/settings/MenuConfig.tsx
index 4efc6c7..42fceb5 100644
--- a/apps/web/src/pages/settings/MenuConfig.tsx
+++ b/apps/web/src/pages/settings/MenuConfig.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
diff --git a/apps/web/src/pages/settings/NumberingRules.tsx b/apps/web/src/pages/settings/NumberingRules.tsx
index be84d67..2886483 100644
--- a/apps/web/src/pages/settings/NumberingRules.tsx
+++ b/apps/web/src/pages/settings/NumberingRules.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
diff --git a/apps/web/src/pages/settings/SystemSettings.tsx b/apps/web/src/pages/settings/SystemSettings.tsx
index 08d6b9b..3f817ad 100644
--- a/apps/web/src/pages/settings/SystemSettings.tsx
+++ b/apps/web/src/pages/settings/SystemSettings.tsx
@@ -6,32 +6,31 @@ import {
Space,
Popconfirm,
message,
- Typography,
Table,
Modal,
+ Tag,
+ theme,
} from 'antd';
-import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import { PlusOutlined, SearchOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import {
getSetting,
updateSetting,
deleteSetting,
} from '../../api/settings';
-// --- Types ---
-
interface SettingEntry {
key: string;
value: string;
}
-// --- Component ---
-
export default function SystemSettings() {
const [entries, setEntries] = useState([]);
const [searchKey, setSearchKey] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [editEntry, setEditEntry] = useState(null);
const [form] = Form.useForm();
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const handleSearch = async () => {
if (!searchKey.trim()) {
@@ -42,7 +41,6 @@ export default function SystemSettings() {
const result = await getSetting(searchKey.trim());
const value = String(result.setting_value ?? '');
- // Check if already in local list
setEntries((prev) => {
const exists = prev.findIndex((e) => e.key === searchKey.trim());
if (exists >= 0) {
@@ -67,7 +65,6 @@ export default function SystemSettings() {
const key = values.setting_key.trim();
const value = values.setting_value;
try {
- // Validate JSON
try {
JSON.parse(value);
} catch {
@@ -91,8 +88,7 @@ export default function SystemSettings() {
closeModal();
} catch (err: unknown) {
const errorMsg =
- (err as { response?: { data?: { message?: string } } })?.response?.data
- ?.message || '保存失败';
+ (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '保存失败';
message.error(errorMsg);
}
};
@@ -129,29 +125,55 @@ export default function SystemSettings() {
};
const columns = [
- { title: '键', dataIndex: 'key', key: 'key', width: 250 },
+ {
+ title: '键',
+ dataIndex: 'key',
+ key: 'key',
+ width: 250,
+ render: (v: string) => (
+
+ {v}
+
+ ),
+ },
{
title: '值 (JSON)',
dataIndex: 'value',
key: 'value',
ellipsis: true,
+ render: (v: string) => (
+ {v}
+ ),
},
{
title: '操作',
key: 'actions',
- width: 180,
+ width: 120,
render: (_: unknown, record: SettingEntry) => (
-
- openEdit(record)}>
- 编辑
-
+
+ }
+ onClick={() => openEdit(record)}
+ style={{ color: isDark ? '#94A3B8' : '#64748B' }}
+ />
handleDelete(record.key)}
>
-
- 删除
-
+ }
+ />
),
@@ -160,41 +182,43 @@ export default function SystemSettings() {
return (
-
-
- 系统参数
-
+
+
+ }
+ value={searchKey}
+ onChange={(e) => setSearchKey(e.target.value)}
+ onPressEnter={handleSearch}
+ style={{ width: 300, borderRadius: 8 }}
+ />
+ 查询
+
} onClick={openCreate}>
添加设置
-
- setSearchKey(e.target.value)}
- onPressEnter={handleSearch}
- style={{ width: 300 }}
+
+
-
} onClick={handleSearch}>
- 查询
-
-
-
-
+
form.submit()}
width={560}
>
-
-
+
diff --git a/apps/web/src/pages/settings/ThemeSettings.tsx b/apps/web/src/pages/settings/ThemeSettings.tsx
index b4d0d11..ebd88fb 100644
--- a/apps/web/src/pages/settings/ThemeSettings.tsx
+++ b/apps/web/src/pages/settings/ThemeSettings.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import { Form, Input, Select, Button, ColorPicker, message, Typography } from 'antd';
import {
getTheme,
diff --git a/apps/web/src/pages/workflow/CompletedTasks.tsx b/apps/web/src/pages/workflow/CompletedTasks.tsx
index 315f38f..d5296fe 100644
--- a/apps/web/src/pages/workflow/CompletedTasks.tsx
+++ b/apps/web/src/pages/workflow/CompletedTasks.tsx
@@ -1,12 +1,12 @@
-import { useCallback, useEffect, useState } from 'react';
-import { Table, Tag } from 'antd';
+import { useEffect, useCallback, useState } from 'react';
+import { Table, Tag, theme } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { listCompletedTasks, type TaskInfo } from '../../api/workflowTasks';
-const outcomeLabels: Record = {
- approved: { color: 'green', text: '同意' },
- rejected: { color: 'red', text: '拒绝' },
- delegated: { color: 'blue', text: '已委派' },
+const outcomeStyles: Record = {
+ approved: { bg: '#ECFDF5', color: '#059669', text: '同意' },
+ rejected: { bg: '#FEF2F2', color: '#DC2626', text: '拒绝' },
+ delegated: { bg: '#EEF2FF', color: '#4F46E5', text: '已委派' },
};
export default function CompletedTasks() {
@@ -14,6 +14,8 @@ export default function CompletedTasks() {
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchData = useCallback(async () => {
setLoading(true);
@@ -29,28 +31,71 @@ export default function CompletedTasks() {
useEffect(() => { fetchData(); }, [fetchData]);
const columns: ColumnsType = [
- { title: '任务名称', dataIndex: 'node_name', key: 'node_name' },
- { title: '流程', dataIndex: 'definition_name', key: 'definition_name' },
- { title: '业务键', dataIndex: 'business_key', key: 'business_key' },
{
- title: '结果', dataIndex: 'outcome', key: 'outcome', width: 100,
+ title: '任务名称',
+ dataIndex: 'node_name',
+ key: 'node_name',
+ render: (v: string) => {v},
+ },
+ { title: '流程', dataIndex: 'definition_name', key: 'definition_name' },
+ {
+ title: '业务键',
+ dataIndex: 'business_key',
+ key: 'business_key',
+ render: (v: string | undefined) => v || '-',
+ },
+ {
+ title: '结果',
+ dataIndex: 'outcome',
+ key: 'outcome',
+ width: 100,
render: (o: string) => {
- const info = outcomeLabels[o] || { color: 'default', text: o };
- return {info.text};
+ const info = outcomeStyles[o] || { bg: '#F1F5F9', color: '#64748B', text: o };
+ return (
+
+ {info.text}
+
+ );
},
},
- { title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 180,
- render: (v: string) => v ? new Date(v).toLocaleString() : '-',
+ {
+ title: '完成时间',
+ dataIndex: 'completed_at',
+ key: 'completed_at',
+ width: 180,
+ render: (v: string) => (
+
+ {v ? new Date(v).toLocaleString() : '-'}
+
+ ),
},
];
return (
-
+
+
`共 ${t} 条记录`,
+ }}
+ />
+
);
}
diff --git a/apps/web/src/pages/workflow/InstanceMonitor.tsx b/apps/web/src/pages/workflow/InstanceMonitor.tsx
index 6bb187f..f03f505 100644
--- a/apps/web/src/pages/workflow/InstanceMonitor.tsx
+++ b/apps/web/src/pages/workflow/InstanceMonitor.tsx
@@ -1,5 +1,6 @@
-import { useCallback, useEffect, useState } from 'react';
-import { Button, message, Modal, Table, Tag } from 'antd';
+import { useEffect, useCallback, useState } from 'react';
+import { Button, message, Modal, Table, Tag, theme } from 'antd';
+import { EyeOutlined, PauseCircleOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import {
listInstances,
@@ -11,11 +12,11 @@ import {
import { getProcessDefinition, type NodeDef, type EdgeDef } from '../../api/workflowDefinitions';
import ProcessViewer from './ProcessViewer';
-const statusColors: Record = {
- running: 'processing',
- suspended: 'warning',
- completed: 'green',
- terminated: 'red',
+const statusStyles: Record = {
+ running: { bg: '#EEF2FF', color: '#4F46E5', text: '运行中' },
+ suspended: { bg: '#FFFBEB', color: '#D97706', text: '已挂起' },
+ completed: { bg: '#ECFDF5', color: '#059669', text: '已完成' },
+ terminated: { bg: '#FEF2F2', color: '#DC2626', text: '已终止' },
};
export default function InstanceMonitor() {
@@ -24,12 +25,13 @@ export default function InstanceMonitor() {
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
- // ProcessViewer state
const [viewerOpen, setViewerOpen] = useState(false);
const [viewerNodes, setViewerNodes] = useState([]);
const [viewerEdges, setViewerEdges] = useState([]);
const [activeNodeIds, setActiveNodeIds] = useState([]);
const [viewerLoading, setViewerLoading] = useState(false);
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchData = useCallback(async () => {
setLoading(true);
@@ -109,54 +111,127 @@ export default function InstanceMonitor() {
};
const columns: ColumnsType = [
- { title: '流程', dataIndex: 'definition_name', key: 'definition_name' },
- { title: '业务键', dataIndex: 'business_key', key: 'business_key' },
{
- title: '状态', dataIndex: 'status', key: 'status', width: 100,
- render: (s: string) => {s},
+ title: '流程',
+ dataIndex: 'definition_name',
+ key: 'definition_name',
+ render: (v: string) => {v},
},
- { title: '当前节点', key: 'current_nodes', width: 150,
+ {
+ title: '业务键',
+ dataIndex: 'business_key',
+ key: 'business_key',
+ render: (v: string | undefined) => v || '-',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 100,
+ render: (s: string) => {
+ const info = statusStyles[s] || { bg: '#F1F5F9', color: '#64748B', text: s };
+ return (
+
+ {info.text}
+
+ );
+ },
+ },
+ {
+ title: '当前节点',
+ key: 'current_nodes',
+ width: 150,
render: (_, record) => record.active_tokens.map(t => t.node_id).join(', ') || '-',
},
- { title: '发起时间', dataIndex: 'started_at', key: 'started_at', width: 180,
- render: (v: string) => new Date(v).toLocaleString(),
+ {
+ title: '发起时间',
+ dataIndex: 'started_at',
+ key: 'started_at',
+ width: 180,
+ render: (v: string) => (
+
+ {new Date(v).toLocaleString()}
+
+ ),
},
{
- title: '操作', key: 'action', width: 220,
+ title: '操作',
+ key: 'action',
+ width: 240,
render: (_, record) => (
- <>
- handleViewFlow(record)} style={{ marginRight: 8 }}>
+
+ }
+ onClick={() => handleViewFlow(record)}
+ >
流程图
{record.status === 'running' && (
<>
- handleSuspend(record.id)} style={{ marginRight: 8 }}>
+ }
+ onClick={() => handleSuspend(record.id)}
+ >
挂起
- handleTerminate(record.id)}>
+ }
+ onClick={() => handleTerminate(record.id)}
+ >
终止
>
)}
{record.status === 'suspended' && (
- handleResume(record.id)}>
+ }
+ onClick={() => handleResume(record.id)}
+ >
恢复
)}
- >
+
),
},
];
return (
<>
-
+
+
`共 ${t} 条记录`,
+ }}
+ />
+
+
= {
- pending: 'processing',
-};
-
export default function PendingTasks() {
const [data, setData] = useState([]);
const [total, setTotal] = useState(0);
@@ -21,6 +18,8 @@ export default function PendingTasks() {
const [outcome, setOutcome] = useState('approved');
const [delegateModal, setDelegateModal] = useState(null);
const [delegateTo, setDelegateTo] = useState('');
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const fetchData = useCallback(async () => {
setLoading(true);
@@ -64,24 +63,76 @@ export default function PendingTasks() {
};
const columns: ColumnsType = [
- { title: '任务名称', dataIndex: 'node_name', key: 'node_name' },
+ {
+ title: '任务名称',
+ dataIndex: 'node_name',
+ key: 'node_name',
+ render: (v: string) => {v},
+ },
{ title: '流程', dataIndex: 'definition_name', key: 'definition_name' },
- { title: '业务键', dataIndex: 'business_key', key: 'business_key' },
{
- title: '状态', dataIndex: 'status', key: 'status', width: 100,
- render: (s: string) => {s},
- },
- { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180,
- render: (v: string) => new Date(v).toLocaleString(),
+ title: '业务键',
+ dataIndex: 'business_key',
+ key: 'business_key',
+ render: (v: string | undefined) => v ? (
+
+ {v}
+
+ ) : '-',
},
{
- title: '操作', key: 'action', width: 160,
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 100,
+ render: (s: string) => (
+
+ {s}
+
+ ),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ key: 'created_at',
+ width: 180,
+ render: (v: string) => (
+
+ {new Date(v).toLocaleString()}
+
+ ),
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 160,
render: (_, record) => (
-
- { setCompleteModal(record); setOutcome('approved'); }}>
+
+ }
+ onClick={() => { setCompleteModal(record); setOutcome('approved'); }}
+ >
审批
- { setDelegateModal(record); setDelegateTo(''); }}>
+ }
+ onClick={() => { setDelegateModal(record); setDelegateTo(''); }}
+ >
委派
@@ -91,29 +142,58 @@ export default function PendingTasks() {
return (
<>
-
+
+
`共 ${t} 条记录`,
+ }}
+ />
+
+
setCompleteModal(null)}
>
- 任务: {completeModal?.node_name}
-
- setOutcome('approved')} ghost={outcome !== 'approved'}>
- 同意
-
- setOutcome('rejected')} ghost={outcome !== 'rejected'}>
- 拒绝
-
-
+
+
+ 任务: {completeModal?.node_name}
+
+
+ }
+ onClick={() => setOutcome('approved')}
+ ghost={outcome !== 'approved'}
+ >
+ 同意
+
+ }
+ onClick={() => setOutcome('rejected')}
+ ghost={outcome !== 'rejected'}
+ >
+ 拒绝
+
+
+
+
{ setDelegateModal(null); setDelegateTo(''); }}
okText="确认委派"
>
- 任务: {delegateModal?.node_name}
- setDelegateTo(e.target.value)}
- />
+
+
+ 任务: {delegateModal?.node_name}
+
+
setDelegateTo(e.target.value)}
+ />
+
>
);
diff --git a/apps/web/src/pages/workflow/ProcessDefinitions.tsx b/apps/web/src/pages/workflow/ProcessDefinitions.tsx
index a918bdd..3d71ad4 100644
--- a/apps/web/src/pages/workflow/ProcessDefinitions.tsx
+++ b/apps/web/src/pages/workflow/ProcessDefinitions.tsx
@@ -1,5 +1,6 @@
-import { useEffect, useState } from 'react';
-import { Button, message, Modal, Space, Table, Tag } from 'antd';
+import { useEffect, useState, useCallback } from 'react';
+import { Button, message, Modal, Space, Table, Tag, theme } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import {
listProcessDefinitions,
@@ -11,10 +12,10 @@ import {
} from '../../api/workflowDefinitions';
import ProcessDesigner from './ProcessDesigner';
-const statusColors: Record = {
- draft: 'default',
- published: 'green',
- deprecated: 'red',
+const statusColors: Record = {
+ draft: { bg: '#F1F5F9', color: '#64748B', text: '草稿' },
+ published: { bg: '#ECFDF5', color: '#059669', text: '已发布' },
+ deprecated: { bg: '#FEF2F2', color: '#DC2626', text: '已弃用' },
};
export default function ProcessDefinitions() {
@@ -24,19 +25,23 @@ export default function ProcessDefinitions() {
const [loading, setLoading] = useState(false);
const [designerOpen, setDesignerOpen] = useState(false);
const [editingId, setEditingId] = useState(null);
+ const { token } = theme.useToken();
+ const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
- const fetch = async () => {
+ const fetchData = useCallback(async (p = page) => {
setLoading(true);
try {
- const res = await listProcessDefinitions(page, 20);
+ const res = await listProcessDefinitions(p, 20);
setData(res.data);
setTotal(res.total);
} finally {
setLoading(false);
}
- };
+ }, [page]);
- useEffect(() => { fetch(); }, [page]);
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
const handleCreate = () => {
setEditingId(null);
@@ -52,7 +57,7 @@ export default function ProcessDefinitions() {
try {
await publishProcessDefinition(id);
message.success('发布成功');
- fetch();
+ fetchData();
} catch {
message.error('发布失败');
}
@@ -68,29 +73,70 @@ export default function ProcessDefinitions() {
message.success('创建成功');
}
setDesignerOpen(false);
- fetch();
+ fetchData();
} catch {
message.error(id ? '更新失败' : '创建失败');
}
};
const columns: ColumnsType = [
- { title: '名称', dataIndex: 'name', key: 'name' },
- { title: '编码', dataIndex: 'key', key: 'key' },
+ {
+ title: '名称',
+ dataIndex: 'name',
+ key: 'name',
+ render: (v: string) => {v},
+ },
+ {
+ title: '编码',
+ dataIndex: 'key',
+ key: 'key',
+ render: (v: string) => (
+
+ {v}
+
+ ),
+ },
{ title: '版本', dataIndex: 'version', key: 'version', width: 80 },
{ title: '分类', dataIndex: 'category', key: 'category', width: 120 },
{
- title: '状态', dataIndex: 'status', key: 'status', width: 100,
- render: (s: string) => {s},
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 100,
+ render: (s: string) => {
+ const info = statusColors[s] || { bg: '#F1F5F9', color: '#64748B', text: s };
+ return (
+
+ {info.text}
+
+ );
+ },
},
{
- title: '操作', key: 'action', width: 200,
+ title: '操作',
+ key: 'action',
+ width: 200,
render: (_, record) => (
-
+
{record.status === 'draft' && (
<>
- handleEdit(record.id)}>编辑
- handlePublish(record.id)}>发布
+ handleEdit(record.id)}>
+ 编辑
+
+ handlePublish(record.id)}>
+ 发布
+
>
)}
@@ -100,23 +146,48 @@ export default function ProcessDefinitions() {
return (
<>
-
-
新建流程
+
+
+ 共 {total} 个流程定义
+
+ } onClick={handleCreate}>
+ 新建流程
+
-
+
+
+
`共 ${t} 条记录`,
+ }}
+ />
+
+
setDesignerOpen(false)}
footer={null}
width={1200}
- destroyOnClose
+ destroyOnHidden
>
Promise;
}
+// 请求去重:记录正在进行的请求,防止并发重复调用
+let unreadCountPromise: Promise | null = null;
+let recentMessagesPromise: Promise | null = null;
+
export const useMessageStore = create((set) => ({
unreadCount: 0,
recentMessages: [],
fetchUnreadCount: async () => {
- try {
- const result = await getUnreadCount();
- set({ unreadCount: result.count });
- } catch {
- // 静默失败,不影响用户体验
+ // 如果已有进行中的请求,复用该 Promise
+ if (unreadCountPromise) {
+ await unreadCountPromise;
+ return;
}
+ unreadCountPromise = (async () => {
+ try {
+ const result = await getUnreadCount();
+ set({ unreadCount: result.count });
+ } catch {
+ // 静默失败,不影响用户体验
+ } finally {
+ unreadCountPromise = null;
+ }
+ })();
+ await unreadCountPromise;
},
fetchRecentMessages: async () => {
- try {
- const result = await listMessages({ page: 1, page_size: 5 });
- set({ recentMessages: result.data });
- } catch {
- // 静默失败
+ if (recentMessagesPromise) {
+ await recentMessagesPromise;
+ return;
}
+ recentMessagesPromise = (async () => {
+ try {
+ const result = await listMessages({ page: 1, page_size: 5 });
+ set({ recentMessages: result.data });
+ } catch {
+ // 静默失败
+ } finally {
+ recentMessagesPromise = null;
+ }
+ })();
+ await recentMessagesPromise;
},
markAsRead: async (id: string) => {
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 0d2abe4..7bb6587 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -17,4 +17,39 @@ export default defineConfig({
},
},
},
+ build: {
+ target: "es2023",
+ cssTarget: "chrome120",
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ "vendor-react": ["react", "react-dom", "react-router-dom"],
+ "vendor-antd": ["antd", "@ant-design/icons"],
+ "vendor-utils": ["axios", "zustand"],
+ },
+ },
+ },
+ minify: "terser",
+ terserOptions: {
+ compress: {
+ drop_console: true,
+ drop_debugger: true,
+ pure_funcs: ["console.log", "console.info", "console.debug"],
+ },
+ },
+ sourcemap: false,
+ reportCompressedSize: false,
+ chunkSizeWarningLimit: 600,
+ },
+ optimizeDeps: {
+ include: [
+ "react",
+ "react-dom",
+ "react-router-dom",
+ "antd",
+ "@ant-design/icons",
+ "axios",
+ "zustand",
+ ],
+ },
});
diff --git a/crates/erp-message/src/handler/template_handler.rs b/crates/erp-message/src/handler/template_handler.rs
index 1e22fe0..3a7bbdf 100644
--- a/crates/erp-message/src/handler/template_handler.rs
+++ b/crates/erp-message/src/handler/template_handler.rs
@@ -28,7 +28,7 @@ where
MessageState: FromRef,
S: Clone + Send + Sync + 'static,
{
- require_permission(&ctx, "message.template:list")?;
+ require_permission(&ctx, "message.template.list")?;
let page = query.page.unwrap_or(1);
let page_size = query.page_size.unwrap_or(20);
@@ -56,7 +56,7 @@ where
MessageState: FromRef,
S: Clone + Send + Sync + 'static,
{
- require_permission(&ctx, "message.template:create")?;
+ require_permission(&ctx, "message.template.create")?;
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
diff --git a/crates/erp-server/config/default.toml b/crates/erp-server/config/default.toml
index d2267ee..001a9fc 100644
--- a/crates/erp-server/config/default.toml
+++ b/crates/erp-server/config/default.toml
@@ -23,4 +23,4 @@ level = "info"
[cors]
# Comma-separated allowed origins. Use "*" for development only.
-allowed_origins = "http://localhost:5173,http://localhost:3000"
+allowed_origins = "http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176,http://localhost:3000"
diff --git a/crates/erp-server/src/handlers/audit_log.rs b/crates/erp-server/src/handlers/audit_log.rs
index 396a0cd..2fc34e5 100644
--- a/crates/erp-server/src/handlers/audit_log.rs
+++ b/crates/erp-server/src/handlers/audit_log.rs
@@ -3,11 +3,11 @@ use axum::response::Json;
use axum::routing::get;
use axum::Router;
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
use erp_core::entity::audit_log;
use erp_core::error::AppError;
-use erp_core::types::TenantContext;
+use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
/// 审计日志查询参数。
#[derive(Debug, Deserialize)]
@@ -18,15 +18,6 @@ pub struct AuditLogQuery {
pub page_size: Option,
}
-/// 审计日志分页响应。
-#[derive(Debug, Serialize)]
-pub struct AuditLogResponse {
- pub items: Vec,
- pub total: u64,
- pub page: u64,
- pub page_size: u64,
-}
-
/// GET /audit-logs
///
/// 分页查询审计日志,支持按 resource_type 和 user_id 过滤。
@@ -35,7 +26,7 @@ pub async fn list_audit_logs(
State(db): State,
Extension(ctx): Extension,
Query(params): Query,
-) -> Result, AppError>
+) -> Result>>, AppError>
where
sea_orm::DatabaseConnection: FromRef,
S: Clone + Send + Sync + 'static,
@@ -68,12 +59,15 @@ where
.await
.map_err(|e| AppError::Internal(format!("查询审计日志失败: {e}")))?;
- Ok(Json(AuditLogResponse {
- items,
+ let total_pages = total.div_ceil(page_size);
+
+ Ok(Json(ApiResponse::ok(PaginatedResponse {
+ data: items,
total,
page,
page_size,
- }))
+ total_pages,
+ })))
}
pub fn audit_log_router() -> Router
diff --git a/docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md b/docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md
index e5a2cf3..9dd61e0 100644
--- a/docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md
+++ b/docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md
@@ -1338,3 +1338,4 @@ git commit -m "chore: add .gitignore and README"
6. 安全审查(OWASP top 10)
7. API 文档完善(Swagger UI)
8. 项目文档
+vc aq
\ No newline at end of file