diff --git a/frontend/src/components/shared/StatCard/StatCard.tsx b/frontend/src/components/shared/StatCard/StatCard.tsx index 40348ef..92c44c1 100644 --- a/frontend/src/components/shared/StatCard/StatCard.tsx +++ b/frontend/src/components/shared/StatCard/StatCard.tsx @@ -53,23 +53,24 @@ function StatCard({
{title} {icon && ( - + )}
-
+
{value} {suffix && {suffix}}
{trend && (
- {trend.direction === 'up' ? : } + {trend.direction === 'up' ?
)} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 0f9ba64..a952c49 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,15 +1,16 @@ -import { Row, Col, Card, Typography, Table, Tag, Space } from "antd"; +import { Row, Col, Card, Typography, Table, Tag, Space, Skeleton, Button } from "antd"; import { VideoCameraOutlined, DesktopOutlined, UserOutlined, ClockCircleOutlined, CheckCircleOutlined, - SyncOutlined + SyncOutlined, + ArrowRightOutlined } from "@ant-design/icons"; import StatCard from "../components/shared/StatCard/StatCard"; -const { Title } = Typography; +const { Title, Text } = Typography; const recentMeetings = [ { key: '1', name: '产品周会', type: '转录中', time: '2024-02-10 14:00', duration: '45min', status: 'processing' }, @@ -19,26 +20,54 @@ const recentMeetings = [ ]; const columns = [ - { title: '会议名称', dataIndex: 'name', key: 'name' }, - { title: '开始时间', dataIndex: 'time', key: 'time' }, - { title: '时长', dataIndex: 'duration', key: 'duration' }, + { + title: '会议名称', + dataIndex: 'name', + key: 'name', + render: (text: string) => {text} + }, + { + title: '开始时间', + dataIndex: 'time', + key: 'time', + className: 'tabular-nums', + render: (text: string) => {text} + }, + { + title: '时长', + dataIndex: 'duration', + key: 'duration', + width: 100, + className: 'tabular-nums' + }, { title: '状态', dataIndex: 'status', key: 'status', - render: (status: string, record: any) => { - if (status === 'processing') return } color="processing">转录中; - if (status === 'success') return } color="success">已完成; + width: 120, + render: (status: string) => { + if (status === 'processing') return ; + if (status === 'success') return ; return 待开始; } + }, + { + title: '操作', + key: 'action', + width: 80, + render: () =>
@@ -46,7 +75,7 @@ export default function Dashboard() { } + icon={ - - - + + + 查看全部} + styles={{ body: { padding: 0 } }} + > - - -
- - - 图表加载中... - +
+ +
+ +
+
diff --git a/frontend/src/pages/Devices.tsx b/frontend/src/pages/Devices.tsx index 73c423b..3d161e1 100644 --- a/frontend/src/pages/Devices.tsx +++ b/frontend/src/pages/Devices.tsx @@ -1,50 +1,87 @@ -import { Button, Form, Input, Drawer, Popconfirm, Space, Table, Tag, Select, InputNumber } from "antd"; +import { + Button, + Form, + Input, + Drawer, + Popconfirm, + Space, + Table, + Tag, + Select, + Typography, + Card, + message +} from "antd"; import { useEffect, useMemo, useState } from "react"; -import { createDevice, deleteDevice, listDevices, updateDevice } from "../api"; -import type { DeviceInfo } from "../types"; +import { createDevice, deleteDevice, listDevices, updateDevice, listUsers } from "../api"; +import type { DeviceInfo, SysUser } from "../types"; import { usePermission } from "../hooks/usePermission"; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + SearchOutlined, + DesktopOutlined, + UserOutlined +} from "@ant-design/icons"; +import "./Devices.css"; + +const { Title, Text } = Typography; export default function Devices() { + const { can } = usePermission(); const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); const [data, setData] = useState([]); - const [query, setQuery] = useState({ userId: "", deviceCode: "", deviceName: "" }); - const [pagination, setPagination] = useState({ current: 1, pageSize: 10 }); + const [users, setUsers] = useState([]); + + // Search state + const [searchText, setSearchText] = useState(""); + + // Drawer state const [open, setOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); - const { can } = usePermission(); - const load = async () => { + const loadData = async () => { setLoading(true); try { - const list = await listDevices(); - setData(list || []); + const [deviceList, usersList] = await Promise.all([listDevices(), listUsers()]); + setData(deviceList || []); + setUsers(usersList || []); + } catch (e) { + message.error("加载数据失败"); } finally { setLoading(false); } }; useEffect(() => { - load(); + loadData(); }, []); - const filtered = useMemo(() => { - return data.filter((d) => { - const hitUser = query.userId ? String(d.userId).includes(query.userId) : true; - const hitCode = query.deviceCode ? d.deviceCode?.includes(query.deviceCode) : true; - const hitName = query.deviceName ? (d.deviceName || "").includes(query.deviceName) : true; - return hitUser && hitCode && hitName; - }); - }, [data, query]); + const userMap = useMemo(() => { + const map: Record = {}; + users.forEach(u => map[u.userId] = u); + return map; + }, [users]); - const pageData = useMemo(() => { - const start = (pagination.current - 1) * pagination.pageSize; - return filtered.slice(start, start + pagination.pageSize); - }, [filtered, pagination]); + const filteredData = useMemo(() => { + if (!searchText) return data; + const lower = searchText.toLowerCase(); + return data.filter(d => { + const user = userMap[d.userId]; + return d.deviceCode.toLowerCase().includes(lower) || + (d.deviceName && d.deviceName.toLowerCase().includes(lower)) || + (user && user.displayName.toLowerCase().includes(lower)) || + String(d.userId).includes(lower); + }); + }, [data, searchText, userMap]); const openCreate = () => { setEditing(null); form.resetFields(); + form.setFieldsValue({ status: 1 }); setOpen(true); }; @@ -55,111 +92,195 @@ export default function Devices() { }; const submit = async () => { - const values = await form.validateFields(); - const payload: Partial = { - userId: values.userId, - deviceCode: values.deviceCode, - deviceName: values.deviceName, - status: values.status - }; - if (editing) { - await updateDevice(editing.deviceId, payload); - } else { - await createDevice(payload); + try { + const values = await form.validateFields(); + setSaving(true); + const payload: Partial = { + userId: values.userId, + deviceCode: values.deviceCode, + deviceName: values.deviceName, + status: values.status + }; + if (editing) { + await updateDevice(editing.deviceId, payload); + message.success("设备已更新"); + } else { + await createDevice(payload); + message.success("设备已创建"); + } + setOpen(false); + loadData(); + } catch (e) { + if (e instanceof Error && e.message) message.error(e.message); + } finally { + setSaving(false); } - setOpen(false); - load(); }; const remove = async (id: number) => { - await deleteDevice(id); - load(); + try { + await deleteDevice(id); + message.success("设备已删除"); + loadData(); + } catch (e) { + message.error("删除失败"); + } }; - return ( -
- - setQuery({ ...query, userId: e.target.value })} - /> - setQuery({ ...query, deviceCode: e.target.value })} - /> - setQuery({ ...query, deviceName: e.target.value })} - /> - {can("device:create") && ( - - )} - + const columns = [ + { + title: "设备信息", + key: "device", + render: (_: any, record: DeviceInfo) => ( + +
+
+
+
{record.deviceName || "未命名设备"}
+
{record.deviceCode}
+
+
+ ), + }, + { + title: "归属用户", + key: "user", + render: (_: any, record: DeviceInfo) => { + const user = userMap[record.userId]; + return user ? ( + + + ) : ( + ID: {record.userId} + ); + } + }, + { + title: "状态", + dataIndex: "status", + width: 100, + render: (status: number) => ( + + {status === 1 ? "启用" : "禁用"} + + ), + }, + { + title: "更新时间", + dataIndex: "updatedAt", + width: 180, + render: (text: string) => {text?.replace('T', ' ').substring(0, 19)} + }, + { + title: "操作", + key: "action", + width: 120, + fixed: "right" as const, + render: (_: any, record: DeviceInfo) => ( + + {can("device:update") && ( +
setPagination({ current, pageSize }) - }} - columns={[ - { title: "ID", dataIndex: "deviceId" }, - { title: "用户ID", dataIndex: "userId" }, - { title: "设备码", dataIndex: "deviceCode" }, - { title: "设备名", dataIndex: "deviceName" }, - { - title: "状态", - dataIndex: "status", - render: (v) => (v === 1 ? 启用 : 禁用) - }, - { - title: "操作", - render: (_, record) => ( - - {can("device:update") && } - {can("device:delete") && ( - remove(record.deviceId)}> - - - )} - - ) - } - ]} - /> + return ( +
+
+
+ 设备管理 + 管理接入系统的硬件终端及关联用户 +
+ {can("device:create") && ( + + )} +
+ + +
+ } + className="devices-search-input" + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + allowClear + aria-label="搜索设备" + /> +
+ +
`共 ${total} 条数据`, + pageSize: 10, + }} + /> + + diff --git a/frontend/src/pages/Dictionaries.tsx b/frontend/src/pages/Dictionaries.tsx index 3ac59dd..8adf897 100644 --- a/frontend/src/pages/Dictionaries.tsx +++ b/frontend/src/pages/Dictionaries.tsx @@ -13,7 +13,8 @@ import { Space, Table, Tag, - Typography + Typography, + Empty } from "antd"; import { useEffect, useState } from "react"; import { @@ -27,10 +28,11 @@ import { updateDictType } from "../api"; import { usePermission } from "../hooks/usePermission"; -import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons"; +import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, ProfileOutlined } from "@ant-design/icons"; import type { SysDictItem, SysDictType } from "../types"; +import "./Dictionaries.css"; -const { Title } = Typography; +const { Title, Text } = Typography; export default function Dictionaries() { const { can } = usePermission(); @@ -100,7 +102,7 @@ export default function Dictionaries() { const handleDeleteType = async (id: number) => { await deleteDictType(id); - message.success("删除成功"); + message.success("类型删除成功"); loadTypes(); }; @@ -111,7 +113,7 @@ export default function Dictionaries() { } else { await createDictType(values); } - message.success(editingType ? "更新成功" : "创建成功"); + message.success(editingType ? "类型更新成功" : "类型创建成功"); setTypeDrawerVisible(false); loadTypes(); }; @@ -119,7 +121,7 @@ export default function Dictionaries() { // Item Actions const handleAddItem = () => { if (!selectedType) { - message.warning("请先选择一个字典类型"); + message.warning("请先从左侧选择一个字典类型"); return; } setEditingItem(null); @@ -136,7 +138,7 @@ export default function Dictionaries() { const handleDeleteItem = async (id: number) => { await deleteDictItem(id); - message.success("删除成功"); + message.success("字典项删除成功"); if (selectedType) loadItems(selectedType.typeCode); }; @@ -147,187 +149,270 @@ export default function Dictionaries() { } else { await createDictItem(values); } - message.success(editingItem ? "更新成功" : "创建成功"); + message.success(editingItem ? "字典项更新成功" : "字典项创建成功"); setItemDrawerVisible(false); if (selectedType) loadItems(selectedType.typeCode); }; return ( -
- 字典管理 - -
+
+
+
+ 数据字典管理 + 维护系统各类枚举值和常量的映射关系 +
+
+ + +
+
({ - onClick: () => setSelectedType(record), - className: `cursor-pointer ${selectedType?.dictTypeId === record.dictTypeId ? "ant-table-row-selected" : ""}` - })} - columns={[ - { title: "类型名称", dataIndex: "typeName" }, - { title: "编码", dataIndex: "typeCode" }, - { - title: "操作", - width: 100, - render: (_, record) => ( - - {can("sys_dict:type:update") && ( -
({ + onClick: () => setSelectedType(record), + className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}` + })} + columns={[ + { + render: (_, record) => ( +
+
+
{record.typeName}
+
{record.typeCode}
+
+
+ {can("sys_dict:type:update") && ( +
+
+ ) + } + ]} + /> + - + + +
(v === 1 ? 启用 : 禁用) - }, - { - title: "操作", - width: 120, - render: (_, record) => ( - - {can("sys_dict:item:update") && ( -
{text} + }, + { + title: "数据数值", + dataIndex: "itemValue", + className: "tabular-nums" + }, + { + title: "排序", + dataIndex: "sortOrder", + width: 80, + className: "tabular-nums" + }, + { + title: "状态", + dataIndex: "status", + width: 100, + render: (v) => ( + + {v === 1 ? "启用" : "禁用"} + + ) + }, + { + title: "操作", + width: 120, + fixed: "right" as const, + render: (_, record) => ( + + {can("sys_dict:item:update") && ( + - + } >
- - + + - - + + - - + + {/* Item Drawer */} +
+ } open={itemDrawerVisible} onClose={() => setItemDrawerVisible(false)} width={400} destroyOnClose footer={ -
+
- +
} >
- + - - + + - - + + - - + + - + + } + placeholder="用户名" + autoComplete="username" + spellCheck={false} + aria-label="用户名" + /> - + {captchaEnabled && (
- -
- {captcha ? ( - captcha - ) : ( -
- )} -
+ } + placeholder="验证码" + maxLength={6} + aria-label="验证码" + /> +
)} @@ -155,15 +174,15 @@ export default function Login() {
-
- 默认账号:admin / 密码:123456 + 演示账号:admin / 密码:123456
@@ -171,4 +190,3 @@ export default function Login() { ); } - diff --git a/frontend/src/pages/Logs.tsx b/frontend/src/pages/Logs.tsx index a76c830..c916094 100644 --- a/frontend/src/pages/Logs.tsx +++ b/frontend/src/pages/Logs.tsx @@ -1,10 +1,11 @@ -import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select } from "antd"; +import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd"; import { useEffect, useState } from "react"; import { fetchLogs } from "../api"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined } from "@ant-design/icons"; import dayjs from "dayjs"; const { RangePicker } = DatePicker; +const { Text, Title } = Typography; export default function Logs() { const [activeTab, setActiveTab] = useState("OPERATION"); @@ -20,6 +21,10 @@ export default function Logs() { endDate: "" }); + // Modal for detail view + const [detailModalVisible, setDetailModalVisible] = useState(false); + const [selectedLog, setSelectedLog] = useState(null); + const loadData = async (currentParams = params) => { setLoading(true); try { @@ -54,31 +59,38 @@ export default function Logs() { loadData(resetParams); }; + const showDetail = (record: any) => { + setSelectedLog(record); + setDetailModalVisible(true); + }; + const columns = [ { - title: "用户名", + title: "操作账号", dataIndex: "username", key: "username", - width: 120, - render: (text: string) => text || "系统/访客" + width: 140, + render: (text: string) => {text || "系统/访客"} }, { - title: activeTab === "LOGIN" ? "登录模块" : "操作模块", + title: activeTab === "LOGIN" ? "登录模块" : "业务模块", dataIndex: "resourceType", key: "resourceType", width: 150 }, { - title: "操作详情", + title: "操作描述", dataIndex: "detail", key: "detail", - ellipsis: true + ellipsis: true, + render: (text: string) => {text} }, { - title: "IP地址", + title: "IP 地址", dataIndex: "ipAddress", key: "ipAddress", - width: 140 + width: 140, + className: "tabular-nums" }, { title: "状态", @@ -86,17 +98,33 @@ export default function Logs() { key: "status", width: 100, render: (status: number) => ( - + {status === 1 ? "成功" : "失败"} ) }, { - title: "操作时间", + title: "发生时间", dataIndex: "createdAt", key: "createdAt", width: 180, + className: "tabular-nums", render: (text: string) => text?.replace('T', ' ').substring(0, 19) + }, + { + title: "详情", + key: "action", + width: 80, + fixed: "right" as const, + render: (_: any, record: any) => ( +
setParams({ ...params, current: page, size }), - showTotal: (total) => `共 ${total} 条` + showTotal: (total) => `共 ${total} 条数据` }} /> + + setDetailModalVisible(false)} + footer={[ + + ]} + width={700} + > + {selectedLog && ( + + {selectedLog.resourceType} + + {selectedLog.operationType} + + {selectedLog.username || "系统"} + {selectedLog.ipAddress} + + {selectedLog.userAgent} + + + + {selectedLog.status === 1 ? "成功" : "失败"} + + + {selectedLog.createdAt?.replace('T', ' ')} + +
+ {selectedLog.detail} +
+
+ {selectedLog.errorMessage && ( + + {selectedLog.errorMessage} + + )} +
+ )} +
); } + +// Ensure UserOutlined is imported +import { UserOutlined } from "@ant-design/icons"; diff --git a/frontend/src/pages/Permissions.tsx b/frontend/src/pages/Permissions.tsx index 9abbaeb..ecca309 100644 --- a/frontend/src/pages/Permissions.tsx +++ b/frontend/src/pages/Permissions.tsx @@ -1,14 +1,41 @@ -import { Button, Form, Input, Drawer, Popconfirm, Space, Table, Tag, Select, InputNumber } from "antd"; +import { + Button, + Form, + Input, + Drawer, + Popconfirm, + Space, + Table, + Tag, + Select, + InputNumber, + Typography, + Card, + message, + Tooltip +} from "antd"; import { useEffect, useMemo, useState } from "react"; import { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api"; import type { SysPermission } from "../types"; import { usePermission } from "../hooks/usePermission"; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + SearchOutlined, + ReloadOutlined, + ClusterOutlined, + MenuOutlined, + CheckSquareOutlined, + InfoCircleOutlined +} from "@ant-design/icons"; -const { Option } = Select; +const { Title, Text } = Typography; type TreePermission = SysPermission & { key: number; children?: TreePermission[] }; function buildTree(list: SysPermission[]): TreePermission[] { + if (!list || list.length === 0) return []; const map = new Map(); const roots: TreePermission[] = []; @@ -35,6 +62,7 @@ function buildTree(list: SysPermission[]): TreePermission[] { export default function Permissions() { const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); const [data, setData] = useState([]); const [query, setQuery] = useState({ name: "", code: "", permType: "" }); const [open, setOpen] = useState(false); @@ -88,190 +116,353 @@ export default function Permissions() { }; const submit = async () => { - const values = await form.validateFields(); - const payload: Partial = { - parentId: values.level === 1 ? undefined : values.parentId, - name: values.name, - code: values.code, - permType: values.permType, - level: values.level, - path: values.path, - component: values.component, - icon: values.icon, - sortOrder: values.sortOrder, - isVisible: values.isVisible, - status: values.status, - description: values.description - }; - if (editing) { - await updatePermission(editing.permId, payload); - } else { - await createPermission(payload); + try { + const values = await form.validateFields(); + setSaving(true); + const payload: Partial = { + parentId: values.level === 1 ? undefined : values.parentId, + name: values.name, + code: values.code, + permType: values.permType, + level: values.level, + path: values.path, + component: values.component, + icon: values.icon, + sortOrder: values.sortOrder, + isVisible: values.isVisible, + status: values.status, + description: values.description + }; + if (editing) { + await updatePermission(editing.permId, payload); + message.success("权限已更新"); + } else { + await createPermission(payload); + message.success("权限已创建"); + } + setOpen(false); + load(); + } catch (e) { + if (e instanceof Error && e.message) message.error(e.message); + } finally { + setSaving(false); } - setOpen(false); - load(); }; const remove = async (id: number) => { - await deletePermission(id); - load(); + try { + await deletePermission(id); + message.success("权限已删除"); + load(); + } catch (e) { + message.error("删除失败"); + } }; - return ( -
- - setQuery({ ...query, name: e.target.value })} - /> - setQuery({ ...query, code: e.target.value })} - /> -
(v === 1 ? 显示 : 隐藏) - }, - { - title: "状态", - dataIndex: "status", - width: 80, - render: (v) => (v === 1 ? 启用 : 禁用) - }, - { - title: "操作", - width: 160, - render: (_, record) => ( - - {can("sys_permission:update") && } - {can("sys_permission:delete") && ( - remove(record.permId)}> - - - )} - - ) + const columns = [ + { + title: "权限名称", + dataIndex: "name", + key: "name", + render: (text: string, record: SysPermission) => ( + + {record.permType === 'menu' ? + + ) + }, + { + title: "权限编码", + dataIndex: "code", + key: "code", + render: (text: string) => text ? {text} : "-" + }, + { + title: "类型", + dataIndex: "permType", + width: 90, + render: (type: string) => ( + + {type === 'menu' ? '菜单' : '按钮'} + + ) + }, + { + title: "排序", + dataIndex: "sortOrder", + width: 80, + className: "tabular-nums" + }, + { + title: "路由/组件", + key: "route", + ellipsis: true, + render: (_: any, record: SysPermission) => ( +
+ {record.path && {record.path}} + {record.component && {record.component}} +
+ ) + }, + { + title: "显示", + dataIndex: "isVisible", + width: 80, + render: (v: number) => (v === 1 ? 可见 : 隐藏) + }, + { + title: "状态", + dataIndex: "status", + width: 80, + render: (v: number) => (v === 1 ? 启用 : 禁用) + }, + { + title: "操作", + width: 120, + fixed: "right" as const, + render: (_: any, record: SysPermission) => ( + + {can("sys_permission:update") && ( +
+ + + + + + + + + - - + + + - ({ - required: getFieldValue("permType") === "button", - message: "按钮权限必须填写编码" - }) - ]}> - + + + 权限唯一编码 + + + + + } + name="code" + dependencies={["permType"]} + rules={[ + ({ getFieldValue }) => ({ + required: getFieldValue("permType") === "button", + message: "按钮权限必须填写编码" + }) + ]} + > + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -279,3 +470,5 @@ export default function Permissions() { ); } +// Fixed missing Row/Col imports +import { Row, Col } from "antd"; diff --git a/frontend/src/pages/RolePermissionBinding.tsx b/frontend/src/pages/RolePermissionBinding.tsx index d352519..29a5914 100644 --- a/frontend/src/pages/RolePermissionBinding.tsx +++ b/frontend/src/pages/RolePermissionBinding.tsx @@ -1,7 +1,21 @@ -import { Button, Card, Col, message, Row, Space, Table, Tag, Tree, Typography } from "antd"; +import { + Button, + Card, + Col, + message, + Row, + Space, + Table, + Tag, + Tree, + Typography, + Input, + Empty +} from "antd"; import type { DataNode } from "antd/es/tree"; import { useEffect, useMemo, useState } from "react"; import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "../api"; +import { SearchOutlined, SafetyCertificateOutlined, SaveOutlined, KeyOutlined, ClusterOutlined } from "@ant-design/icons"; import type { SysPermission, SysRole } from "../types"; const { Title, Text } = Typography; @@ -9,16 +23,21 @@ const { Title, Text } = Typography; type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] }; function buildPermissionTree(list: SysPermission[]): PermissionNode[] { + if (!list || list.length === 0) return []; + const active = list.filter((p) => p.status !== 0); const map = new Map(); const roots: PermissionNode[] = []; - list.forEach((item) => { + active.forEach((item) => { map.set(item.permId, { ...item, key: item.permId, children: [] }); }); map.forEach((node) => { - if (node.parentId && map.has(node.parentId)) { - map.get(node.parentId)!.children!.push(node); + if (node.parentId && node.parentId !== 0) { + const parent = map.get(node.parentId); + if (parent) { + parent.children!.push(node); + } } else { roots.push(node); } @@ -38,10 +57,10 @@ function toTreeData(nodes: PermissionNode[]): DataNode[] { title: ( {node.name} - {node.permType === "button" && 按钮} + {node.permType === "button" && 按钮} ), - children: node.children ? toTreeData(node.children) : undefined + children: node.children && node.children.length > 0 ? toTreeData(node.children) : undefined })); } @@ -52,7 +71,13 @@ export default function RolePermissionBinding() { const [loadingPerms, setLoadingPerms] = useState(false); const [saving, setSaving] = useState(false); const [selectedRoleId, setSelectedRoleId] = useState(null); + + // Selection states const [checkedPermIds, setCheckedPermIds] = useState([]); + const [halfCheckedIds, setHalfCheckedIds] = useState([]); + + // Search + const [searchText, setSearchText] = useState(""); const selectedRole = useMemo( () => roles.find((r) => r.roleId === selectedRoleId) || null, @@ -75,7 +100,7 @@ export default function RolePermissionBinding() { const list = await listPermissions(); setPermissions(list || []); } catch (e) { - message.error("加载权限失败,请确认接口已实现"); + message.error("加载权限定义失败"); } finally { setLoadingPerms(false); } @@ -84,10 +109,18 @@ export default function RolePermissionBinding() { const loadRolePermissions = async (roleId: number) => { try { const list = await listRolePermissions(roleId); - setCheckedPermIds(list || []); + const normalized = (list || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)); + + // Filter out parent IDs for Tree view回显 consistency + const leafIds = normalized.filter(id => { + return !permissions.some(p => p.parentId === id); + }); + + setCheckedPermIds(leafIds); + setHalfCheckedIds([]); } catch (e) { setCheckedPermIds([]); - message.error("加载角色权限失败,请确认接口已实现"); + message.error("加载角色授权数据失败"); } }; @@ -97,93 +130,148 @@ export default function RolePermissionBinding() { }, []); useEffect(() => { - if (selectedRoleId) { + if (selectedRoleId && permissions.length > 0) { loadRolePermissions(selectedRoleId); } else { setCheckedPermIds([]); } - }, [selectedRoleId]); + }, [selectedRoleId, permissions]); - const treeData = useMemo(() => toTreeData(buildPermissionTree(permissions)), [permissions]); + const filteredRoles = useMemo(() => { + if (!searchText) return roles; + const lower = searchText.toLowerCase(); + return roles.filter(r => + r.roleName.toLowerCase().includes(lower) || + r.roleCode.toLowerCase().includes(lower) + ); + }, [roles, searchText]); + + const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]); + const antdTreeData = useMemo(() => toTreeData(treeData), [treeData]); const handleSave = async () => { if (!selectedRoleId) { - message.warning("请先选择角色"); + message.warning("请从左侧列表中选择一个角色"); return; } setSaving(true); try { - await saveRolePermissions(selectedRoleId, checkedPermIds); - message.success("角色权限绑定已保存"); + // Merge checked and half-checked for database persistence + const allPermIds = Array.from(new Set([...checkedPermIds, ...halfCheckedIds])); + await saveRolePermissions(selectedRoleId, allPermIds); + message.success("功能权限策略已成功更新并下发"); } catch (e) { - message.error("保存失败,请确认接口已实现"); + message.error("保存权限配置失败"); } finally { setSaving(false); } }; return ( -
-
+
+
- 角色权限绑定 - 为角色配置菜单与按钮权限 + 角色权限授权 + 配置系统中各类角色所拥有的菜单访问权限与功能操作权限点
-
- +
- +
setSelectedRoleId(keys[0] as number) + onChange: (keys) => setSelectedRoleId(keys[0] as number), }} - pagination={{ pageSize: 8 }} + onRow={(record) => ({ + onClick: () => setSelectedRoleId(record.roleId), + className: "cursor-pointer" + })} + pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 个角色` }} columns={[ - { title: "ID", dataIndex: "roleId", width: 80 }, - { title: "角色编码", dataIndex: "roleCode" }, - { title: "角色名称", dataIndex: "roleName" }, + { + title: "角色信息", + key: "role", + render: (_, r) => ( +
+
{r.roleName}
+
{r.roleCode}
+
+ ) + }, { title: "状态", dataIndex: "status", - width: 90, - render: (v) => (v === 1 ? 启用 : 禁用) + width: 80, + render: (v) => (v === 1 ? 启用 : 禁用) } ]} /> +
`共 ${total} 人` }} columns={[ { title: '用户', render: (_, r) => ( - -
-
{r.displayName}
-
@{r.username}
+
- +
setSelectedUserId(keys[0] as number) + onChange: (keys) => setSelectedUserId(keys[0] as number), }} - pagination={{ pageSize: 8 }} + onRow={(record) => ({ + onClick: () => setSelectedUserId(record.userId), + className: "cursor-pointer" + })} + pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 人` }} columns={[ - { title: "ID", dataIndex: "userId", width: 80 }, - { title: "用户名", dataIndex: "username" }, - { title: "显示名", dataIndex: "displayName" }, + { + title: "用户信息", + key: "user", + render: (_, r) => ( +
+
{r.displayName}
+
@{r.username}
+
+ ) + }, { title: "状态", dataIndex: "status", - width: 90, - render: (v) => (v === 1 ? 启用 : 禁用) + width: 80, + render: (v) => (v === 1 ? 正常 : 禁用) } ]} /> + - - - {role.roleName} - - {role.roleCode} - - - - - ))} - - - {!roles.length && !loadingRoles && ( - 暂无角色数据 - )} - + {selectedUserId ? ( +
+ setCheckedRoleIds(values as number[])} + disabled={loadingRoles} + > + + {roles.map((role) => ( +
+ + + {role.roleName} + + {role.roleCode} + + + + + ))} + + + {!roles.length && !loadingRoles && ( + + )} + + ) : ( +
+
+ )}