feat(i18n): 实现国际化功能支持多语言切换
- 集成 react-i18next 库实现国际化框架 - 在 AppLayout 中添加语言切换下拉菜单和切换功能 - 添加 GlobalOutlined 图标用于语言切换入口 - 将所有页面中的硬编码文本替换为国际化键值 - 在 Dashboard 页面实现标题和按钮的国际化 - 在 Devices 页面实现表格列名和操作按钮的国际化 - 在 Dictionaries 页面实现所有界面元素的国际化 - 在 Login 页面实现登录表单和提示信息的国际化 - 在 Logs 页面实现日志表格和筛选器的国际化 - 统一错误提示和成功提示为国际化消息master
parent
a497deacfc
commit
5fe3b53680
|
|
@ -25,16 +25,7 @@
|
|||
- **反馈及时**:操作有明确状态反馈
|
||||
- **容错友好**:预防错误,提示明确
|
||||
|
||||
---
|
||||
|
||||
## 颜色系统
|
||||
|
||||
| 类型 | 用途 | 颜色值 |
|
||||
|------|------|-------|
|
||||
| 主色调 | 关键按钮、重要信息、链接 | `#b8178d` |
|
||||
| 辅助色 | 信息提示 | `#1677ff` |
|
||||
| 功能色 | Success / Warning / Error / Info | 按语义使用 |
|
||||
| 中性色 | 文本、背景、边框 | - |
|
||||
|
||||
**使用规范**:
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@
|
|||
"@ant-design/icons": "^6.1.0",
|
||||
"antd": "^5.13.2",
|
||||
"axios": "^1.6.7",
|
||||
"i18next": "^25.8.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
|
@ -2051,6 +2054,55 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.8.6",
|
||||
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.8.6.tgz",
|
||||
"integrity": "sha512-HsS6p2yr/Vo5EPljWuBJ9OxKVFok2Q/Oa6PvFTpv2bMcDt2sQMOnKDQ7FTDDdME+3d1YULQjKj7aVSZP1bCouQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/is-mobile": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-mobile/-/is-mobile-5.0.0.tgz",
|
||||
|
|
@ -2856,6 +2908,33 @@
|
|||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.5.4.tgz",
|
||||
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 25.6.2",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||
|
|
@ -3024,7 +3103,7 @@
|
|||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -3134,6 +3213,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@
|
|||
"@ant-design/icons": "^6.1.0",
|
||||
"antd": "^5.13.2",
|
||||
"axios": "^1.6.7",
|
||||
"i18next": "^25.8.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Layout, Menu, Button, Space, Avatar, Dropdown, message, type MenuProps } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
DashboardOutlined,
|
||||
VideoCameraOutlined,
|
||||
|
|
@ -12,7 +13,8 @@ import {
|
|||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
BellOutlined,
|
||||
SettingOutlined
|
||||
SettingOutlined,
|
||||
GlobalOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
|
|
@ -31,6 +33,7 @@ const iconMap: Record<string, any> = {
|
|||
};
|
||||
|
||||
export default function AppLayout() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [menus, setMenus] = useState<SysPermission[]>([]);
|
||||
const location = useLocation();
|
||||
|
|
@ -50,7 +53,7 @@ export default function AppLayout() {
|
|||
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
||||
setMenus(filtered);
|
||||
} catch (e) {
|
||||
message.error("获取菜单失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -63,6 +66,11 @@ export default function AppLayout() {
|
|||
navigate("/login");
|
||||
};
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
message.success(lng === 'zh-CN' ? '已切换至中文' : 'Switched to English');
|
||||
};
|
||||
|
||||
const buildMenuTree = (list: SysPermission[]) => {
|
||||
const map = new Map<number, SysPermission & { children?: SysPermission[] }>();
|
||||
const roots: (SysPermission & { children?: SysPermission[] })[] = [];
|
||||
|
|
@ -104,10 +112,15 @@ export default function AppLayout() {
|
|||
const menuItems = toMenuItems(buildMenuTree(menus));
|
||||
|
||||
const userMenuItems: MenuProps["items"] = [
|
||||
{ key: 'profile', label: '个人信息', icon: <UserOutlined /> },
|
||||
{ key: 'settings', label: '系统设置', icon: <SettingOutlined /> },
|
||||
{ key: 'profile', label: t('layout.profile'), icon: <UserOutlined /> },
|
||||
{ key: 'settings', label: t('layout.settings'), icon: <SettingOutlined /> },
|
||||
{ type: 'divider' },
|
||||
{ key: 'logout', label: '退出登录', icon: <LogoutOutlined />, onClick: handleLogout },
|
||||
{ key: 'logout', label: t('layout.logout'), icon: <LogoutOutlined />, onClick: handleLogout },
|
||||
];
|
||||
|
||||
const langMenuItems: MenuProps["items"] = [
|
||||
{ key: 'zh-CN', label: '简体中文', onClick: () => changeLanguage('zh-CN') },
|
||||
{ key: 'en-US', label: 'English', onClick: () => changeLanguage('en-US') },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -167,6 +180,9 @@ export default function AppLayout() {
|
|||
style={{ fontSize: '16px', width: 64, height: 64 }}
|
||||
/>
|
||||
<Space size={20}>
|
||||
<Dropdown menu={{ items: langMenuItems }} placement="bottomRight">
|
||||
<GlobalOutlined style={{ fontSize: '18px', color: '#666', cursor: 'pointer' }} />
|
||||
</Dropdown>
|
||||
<BellOutlined style={{ fontSize: '18px', color: '#666', cursor: 'pointer' }} />
|
||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<Space style={{ cursor: 'pointer' }}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
|
|||
import { BrowserRouter } from "react-router-dom";
|
||||
import "antd/dist/reset.css";
|
||||
import "./index.css";
|
||||
import "./i18n";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import {
|
|||
SyncOutlined,
|
||||
ArrowRightOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import StatCard from "../components/shared/StatCard/StatCard";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const recentMeetings = [
|
||||
{ key: '1', name: '产品周会', type: '转录中', time: '2024-02-10 14:00', duration: '45min', status: 'processing' },
|
||||
{ key: '2', name: '技术分享', type: '已完成', time: '2024-02-10 10:00', duration: '60min', status: 'success' },
|
||||
|
|
@ -21,27 +25,27 @@ const recentMeetings = [
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: '会议名称',
|
||||
title: t('dashboard.meetingName'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string) => <Text strong>{text}</Text>
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
title: t('dashboard.startTime'),
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
className: 'tabular-nums',
|
||||
render: (text: string) => <Text type="secondary">{text}</Text>
|
||||
},
|
||||
{
|
||||
title: '时长',
|
||||
title: t('dashboard.duration'),
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
width: 100,
|
||||
className: 'tabular-nums'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('common.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
|
|
@ -52,28 +56,27 @@ const columns = [
|
|||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('common.action'),
|
||||
key: 'action',
|
||||
width: 80,
|
||||
render: () => <Button type="link" size="small" icon={<ArrowRightOutlined aria-hidden="true" />} aria-label="查看会议详情" />
|
||||
render: () => <Button type="link" size="small" icon={<ArrowRightOutlined aria-hidden="true" />} aria-label={t('dashboard.viewAll')} />
|
||||
}
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="dashboard-page p-6">
|
||||
<div className="mb-6 flex justify-between items-end">
|
||||
<div>
|
||||
<Title level={4} className="mb-1">系统总览</Title>
|
||||
<Text type="secondary">实时监控会议转录状态与系统关键指标</Text>
|
||||
<Title level={4} className="mb-1">{t('dashboard.title')}</Title>
|
||||
<Text type="secondary">{t('dashboard.subtitle')}</Text>
|
||||
</div>
|
||||
<Button icon={<SyncOutlined aria-hidden="true" />} size="small">刷新数据</Button>
|
||||
<Button icon={<SyncOutlined aria-hidden="true" />} size="small">{t('common.refresh')}</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<StatCard
|
||||
title="今日会议"
|
||||
title={t('dashboard.todayMeetings')}
|
||||
value={12}
|
||||
icon={<VideoCameraOutlined aria-hidden="true" />}
|
||||
color="blue"
|
||||
|
|
@ -82,7 +85,7 @@ export default function Dashboard() {
|
|||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<StatCard
|
||||
title="活跃设备"
|
||||
title={t('dashboard.activeDevices')}
|
||||
value={45}
|
||||
icon={<DesktopOutlined aria-hidden="true" />}
|
||||
color="green"
|
||||
|
|
@ -91,7 +94,7 @@ export default function Dashboard() {
|
|||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<StatCard
|
||||
title="转录时长"
|
||||
title={t('dashboard.transcriptionDuration')}
|
||||
value={1280}
|
||||
suffix="min"
|
||||
icon={<ClockCircleOutlined aria-hidden="true" />}
|
||||
|
|
@ -101,7 +104,7 @@ export default function Dashboard() {
|
|||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<StatCard
|
||||
title="总用户数"
|
||||
title={t('dashboard.totalUsers')}
|
||||
value={320}
|
||||
icon={<UserOutlined aria-hidden="true" />}
|
||||
color="purple"
|
||||
|
|
@ -113,10 +116,10 @@ export default function Dashboard() {
|
|||
<Row gutter={[24, 24]} className="mt-6">
|
||||
<Col xs={24} xl={16}>
|
||||
<Card
|
||||
title="最近会议"
|
||||
title={t('dashboard.recentMeetings')}
|
||||
bordered={false}
|
||||
className="shadow-sm"
|
||||
extra={<Button type="link" size="small">查看全部</Button>}
|
||||
extra={<Button type="link" size="small">{t('dashboard.viewAll')}</Button>}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<Table
|
||||
|
|
@ -129,7 +132,7 @@ export default function Dashboard() {
|
|||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} xl={8}>
|
||||
<Card title="设备负载" bordered={false} className="shadow-sm">
|
||||
<Card title={t('dashboard.deviceLoad')} bordered={false} className="shadow-sm">
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
<div className="mt-4 text-gray-400 flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
message
|
||||
} from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createDevice, deleteDevice, listDevices, updateDevice, listUsers } from "../api";
|
||||
import type { DeviceInfo, SysUser } from "../types";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
|
|
@ -29,6 +30,7 @@ import "./Devices.css";
|
|||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Devices() {
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -50,7 +52,7 @@ export default function Devices() {
|
|||
setData(deviceList || []);
|
||||
setUsers(usersList || []);
|
||||
} catch (e) {
|
||||
message.error("加载数据失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -103,10 +105,10 @@ export default function Devices() {
|
|||
};
|
||||
if (editing) {
|
||||
await updateDevice(editing.deviceId, payload);
|
||||
message.success("设备已更新");
|
||||
message.success(t('common.success'));
|
||||
} else {
|
||||
await createDevice(payload);
|
||||
message.success("设备已创建");
|
||||
message.success(t('common.success'));
|
||||
}
|
||||
setOpen(false);
|
||||
loadData();
|
||||
|
|
@ -120,16 +122,16 @@ export default function Devices() {
|
|||
const remove = async (id: number) => {
|
||||
try {
|
||||
await deleteDevice(id);
|
||||
message.success("设备已删除");
|
||||
message.success(t('common.success'));
|
||||
loadData();
|
||||
} catch (e) {
|
||||
message.error("删除失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "设备信息",
|
||||
title: t('devices.deviceInfo'),
|
||||
key: "device",
|
||||
render: (_: any, record: DeviceInfo) => (
|
||||
<Space>
|
||||
|
|
@ -137,14 +139,14 @@ export default function Devices() {
|
|||
<DesktopOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="device-name">{record.deviceName || "未命名设备"}</div>
|
||||
<div className="device-code tabular-nums">{record.deviceCode}</div>
|
||||
<div className="device-name font-medium">{record.deviceName || "未命名设备"}</div>
|
||||
<div className="device-code text-xs text-gray-400 tabular-nums">{record.deviceCode}</div>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "归属用户",
|
||||
title: t('devices.owner'),
|
||||
key: "user",
|
||||
render: (_: any, record: DeviceInfo) => {
|
||||
const user = userMap[record.userId];
|
||||
|
|
@ -152,7 +154,7 @@ export default function Devices() {
|
|||
<Space>
|
||||
<UserOutlined aria-hidden="true" style={{ color: '#8c8c8c' }} />
|
||||
<span>{user.displayName}</span>
|
||||
<Text type="secondary" size="small" className="tabular-nums">(ID: {record.userId})</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }} className="tabular-nums">(ID: {record.userId})</Text>
|
||||
</Space>
|
||||
) : (
|
||||
<span className="tabular-nums">ID: {record.userId}</span>
|
||||
|
|
@ -160,7 +162,7 @@ export default function Devices() {
|
|||
}
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 100,
|
||||
render: (status: number) => (
|
||||
|
|
@ -170,13 +172,13 @@ export default function Devices() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
title: t('devices.updateTime'),
|
||||
dataIndex: "updatedAt",
|
||||
width: 180,
|
||||
render: (text: string) => <Text type="secondary" className="tabular-nums">{text?.replace('T', ' ').substring(0, 19)}</Text>
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: t('common.action'),
|
||||
key: "action",
|
||||
width: 120,
|
||||
fixed: "right" as const,
|
||||
|
|
@ -187,7 +189,7 @@ export default function Devices() {
|
|||
type="text"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
onClick={() => openEdit(record)}
|
||||
aria-label={`编辑设备 ${record.deviceName || record.deviceCode}`}
|
||||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("device:delete") && (
|
||||
|
|
@ -196,7 +198,7 @@ export default function Devices() {
|
|||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined aria-hidden="true" />}
|
||||
aria-label={`删除设备 ${record.deviceName || record.deviceCode}`}
|
||||
aria-label={t('common.delete')}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
@ -206,29 +208,29 @@ export default function Devices() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="devices-page">
|
||||
<div className="devices-header">
|
||||
<div className="devices-page p-6">
|
||||
<div className="devices-header flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<Title level={4} className="devices-title">设备管理</Title>
|
||||
<Text type="secondary">管理接入系统的硬件终端及关联用户</Text>
|
||||
<Title level={4} className="mb-1">{t('devices.title')}</Title>
|
||||
<Text type="secondary">{t('devices.subtitle')}</Text>
|
||||
</div>
|
||||
{can("device:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||
接入设备
|
||||
{t('devices.drawerTitleCreate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="devices-table-card shadow-sm">
|
||||
<div className="devices-table-toolbar">
|
||||
<div className="devices-table-toolbar mb-4">
|
||||
<Input
|
||||
placeholder="搜索设备名称、编码或归属用户…"
|
||||
placeholder={t('devices.searchPlaceholder')}
|
||||
prefix={<SearchOutlined aria-hidden="true" />}
|
||||
className="devices-search-input"
|
||||
style={{ width: 350 }}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
aria-label="搜索设备"
|
||||
aria-label={t('common.search')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -237,8 +239,9 @@ export default function Devices() {
|
|||
columns={columns}
|
||||
dataSource={filteredData}
|
||||
loading={loading}
|
||||
size="middle"
|
||||
pagination={{
|
||||
showTotal: (total) => `共 ${total} 条数据`,
|
||||
showTotal: (total) => t('common.total', { total }),
|
||||
pageSize: 10,
|
||||
}}
|
||||
/>
|
||||
|
|
@ -248,7 +251,7 @@ export default function Devices() {
|
|||
title={
|
||||
<div className="device-drawer-title">
|
||||
<DesktopOutlined className="mr-2" aria-hidden="true" />
|
||||
{editing ? "修改设备信息" : "接入新设备"}
|
||||
{editing ? t('devices.drawerTitleEdit') : t('devices.drawerTitleCreate')}
|
||||
</div>
|
||||
}
|
||||
open={open}
|
||||
|
|
@ -257,15 +260,15 @@ export default function Devices() {
|
|||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setOpen(false)}>取消</Button>
|
||||
<Button onClick={() => setOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>
|
||||
确认保存
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="归属用户" name="userId" rules={[{ required: true, message: "请选择归属用户" }]}>
|
||||
<Form.Item label={t('devices.owner')} name="userId" rules={[{ required: true, message: t('devices.owner') }]}>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="搜索并选择用户"
|
||||
|
|
@ -273,13 +276,13 @@ export default function Devices() {
|
|||
options={users.map(u => ({ label: `${u.displayName} (@${u.username})`, value: u.userId }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="设备识别码" name="deviceCode" rules={[{ required: true, message: "请输入设备识别码" }]}>
|
||||
<Form.Item label={t('devices.deviceCode')} name="deviceCode" rules={[{ required: true, message: t('devices.deviceCode') }]}>
|
||||
<Input placeholder="由硬件生成的唯一识别码" />
|
||||
</Form.Item>
|
||||
<Form.Item label="设备名称" name="deviceName">
|
||||
<Form.Item label={t('devices.deviceName')} name="deviceName">
|
||||
<Input placeholder="例如:会议室 A 转录仪" />
|
||||
</Form.Item>
|
||||
<Form.Item label="设备状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{ value: 1, label: "正常启用" }, { value: 0, label: "禁用接入" }]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Empty
|
||||
} from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
createDictItem,
|
||||
createDictType,
|
||||
|
|
@ -35,6 +36,7 @@ import "./Dictionaries.css";
|
|||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Dictionaries() {
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const [types, setTypes] = useState<SysDictType[]>([]);
|
||||
const [items, setItems] = useState<SysDictItem[]>([]);
|
||||
|
|
@ -102,7 +104,7 @@ export default function Dictionaries() {
|
|||
|
||||
const handleDeleteType = async (id: number) => {
|
||||
await deleteDictType(id);
|
||||
message.success("类型删除成功");
|
||||
message.success(t('common.success'));
|
||||
loadTypes();
|
||||
};
|
||||
|
||||
|
|
@ -113,7 +115,7 @@ export default function Dictionaries() {
|
|||
} else {
|
||||
await createDictType(values);
|
||||
}
|
||||
message.success(editingType ? "类型更新成功" : "类型创建成功");
|
||||
message.success(t('common.success'));
|
||||
setTypeDrawerVisible(false);
|
||||
loadTypes();
|
||||
};
|
||||
|
|
@ -121,7 +123,7 @@ export default function Dictionaries() {
|
|||
// Item Actions
|
||||
const handleAddItem = () => {
|
||||
if (!selectedType) {
|
||||
message.warning("请先从左侧选择一个字典类型");
|
||||
message.warning(t('dicts.selectType'));
|
||||
return;
|
||||
}
|
||||
setEditingItem(null);
|
||||
|
|
@ -138,7 +140,7 @@ export default function Dictionaries() {
|
|||
|
||||
const handleDeleteItem = async (id: number) => {
|
||||
await deleteDictItem(id);
|
||||
message.success("字典项删除成功");
|
||||
message.success(t('common.success'));
|
||||
if (selectedType) loadItems(selectedType.typeCode);
|
||||
};
|
||||
|
||||
|
|
@ -149,27 +151,27 @@ export default function Dictionaries() {
|
|||
} else {
|
||||
await createDictItem(values);
|
||||
}
|
||||
message.success(editingItem ? "字典项更新成功" : "字典项创建成功");
|
||||
message.success(t('common.success'));
|
||||
setItemDrawerVisible(false);
|
||||
if (selectedType) loadItems(selectedType.typeCode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dictionaries-page">
|
||||
<div className="dictionaries-header">
|
||||
<div className="dictionaries-page p-6">
|
||||
<div className="dictionaries-header mb-6">
|
||||
<div>
|
||||
<Title level={4} className="dictionaries-title">数据字典管理</Title>
|
||||
<Text type="secondary">维护系统各类枚举值和常量的映射关系</Text>
|
||||
<Title level={4} className="mb-1">{t('dicts.title')}</Title>
|
||||
<Text type="secondary">{t('dicts.subtitle')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Row gutter={24} className="dictionaries-content">
|
||||
<Col span={8} className="full-height">
|
||||
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
|
||||
<Col span={8} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<BookOutlined aria-hidden="true" />
|
||||
<span>字典类型</span>
|
||||
<span>{t('dicts.dictType')}</span>
|
||||
</Space>
|
||||
}
|
||||
className="full-height-card shadow-sm"
|
||||
|
|
@ -180,14 +182,13 @@ export default function Dictionaries() {
|
|||
size="small"
|
||||
icon={<PlusOutlined aria-hidden="true" />}
|
||||
onClick={handleAddType}
|
||||
aria-label="新增字典类型"
|
||||
>
|
||||
新增
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="scroll-container">
|
||||
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
|
||||
<Table
|
||||
rowKey="dictTypeId"
|
||||
loading={loadingTypes}
|
||||
|
|
@ -202,12 +203,12 @@ export default function Dictionaries() {
|
|||
columns={[
|
||||
{
|
||||
render: (_, record) => (
|
||||
<div className="dict-type-item">
|
||||
<div className="dict-type-item flex justify-between items-center p-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="dict-type-name truncate">{record.typeName}</div>
|
||||
<div className="dict-type-code truncate tabular-nums">{record.typeCode}</div>
|
||||
<div className="dict-type-name font-medium truncate">{record.typeName}</div>
|
||||
<div className="dict-type-code text-xs text-gray-400 truncate tabular-nums">{record.typeCode}</div>
|
||||
</div>
|
||||
<div className="dict-type-actions">
|
||||
<div className="dict-type-actions flex gap-1">
|
||||
{can("sys_dict:type:update") && (
|
||||
<Button
|
||||
type="text"
|
||||
|
|
@ -217,7 +218,6 @@ export default function Dictionaries() {
|
|||
e.stopPropagation();
|
||||
handleEditType(record);
|
||||
}}
|
||||
aria-label={`编辑类型 ${record.typeName}`}
|
||||
/>
|
||||
)}
|
||||
{can("sys_dict:type:delete") && (
|
||||
|
|
@ -234,7 +234,6 @@ export default function Dictionaries() {
|
|||
danger
|
||||
icon={<DeleteOutlined aria-hidden="true" />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`删除类型 ${record.typeName}`}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
@ -248,12 +247,12 @@ export default function Dictionaries() {
|
|||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={16} className="full-height">
|
||||
<Col span={16} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<ProfileOutlined aria-hidden="true" />
|
||||
<span>字典项内容{selectedType ? ` - ${selectedType.typeName}` : ""}</span>
|
||||
<span>{t('dicts.dictItem')}{selectedType ? ` - ${selectedType.typeName}` : ""}</span>
|
||||
</Space>
|
||||
}
|
||||
className="full-height-card shadow-sm"
|
||||
|
|
@ -265,15 +264,14 @@ export default function Dictionaries() {
|
|||
icon={<PlusOutlined aria-hidden="true" />}
|
||||
onClick={handleAddItem}
|
||||
disabled={!selectedType}
|
||||
aria-label="新增字典项"
|
||||
>
|
||||
新增字典项
|
||||
{t('dicts.drawerTitleItemCreate')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectedType ? (
|
||||
<div className="scroll-container">
|
||||
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
|
||||
<Table
|
||||
rowKey="dictItemId"
|
||||
loading={loadingItems}
|
||||
|
|
@ -282,23 +280,23 @@ export default function Dictionaries() {
|
|||
size="middle"
|
||||
columns={[
|
||||
{
|
||||
title: "展示标签",
|
||||
title: t('dicts.itemLabel'),
|
||||
dataIndex: "itemLabel",
|
||||
render: (text) => <Text strong>{text}</Text>
|
||||
},
|
||||
{
|
||||
title: "数据数值",
|
||||
title: t('dicts.itemValue'),
|
||||
dataIndex: "itemValue",
|
||||
className: "tabular-nums"
|
||||
},
|
||||
{
|
||||
title: "排序",
|
||||
title: t('dicts.sort'),
|
||||
dataIndex: "sortOrder",
|
||||
width: 80,
|
||||
className: "tabular-nums"
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 100,
|
||||
render: (v) => (
|
||||
|
|
@ -308,7 +306,7 @@ export default function Dictionaries() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: t('common.action'),
|
||||
width: 120,
|
||||
fixed: "right" as const,
|
||||
render: (_, record) => (
|
||||
|
|
@ -319,7 +317,7 @@ export default function Dictionaries() {
|
|||
size="small"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
onClick={() => handleEditItem(record)}
|
||||
aria-label={`编辑字典项 ${record.itemLabel}`}
|
||||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("sys_dict:item:delete") && (
|
||||
|
|
@ -329,7 +327,7 @@ export default function Dictionaries() {
|
|||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined aria-hidden="true" />}
|
||||
aria-label={`删除字典项 ${record.itemLabel}`}
|
||||
aria-label={t('common.delete')}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
@ -340,8 +338,8 @@ export default function Dictionaries() {
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-center h-full">
|
||||
<Empty description="请先从左侧选择一个字典类型" />
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Empty description={t('dicts.selectType')} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
@ -353,7 +351,7 @@ export default function Dictionaries() {
|
|||
title={
|
||||
<Space>
|
||||
<BookOutlined aria-hidden="true" />
|
||||
<span>{editingType ? "编辑字典类型" : "新增字典类型"}</span>
|
||||
<span>{editingType ? t('dicts.drawerTitleTypeEdit') : t('dicts.drawerTitleTypeCreate')}</span>
|
||||
</Space>
|
||||
}
|
||||
open={typeDrawerVisible}
|
||||
|
|
@ -361,20 +359,20 @@ export default function Dictionaries() {
|
|||
width={400}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={() => setTypeDrawerVisible(false)}>取消</Button>
|
||||
<Button type="primary" onClick={handleTypeSubmit}>确认提交</Button>
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setTypeDrawerVisible(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" onClick={handleTypeSubmit}>{t('common.confirm')}</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={typeForm} layout="vertical">
|
||||
<Form.Item label="类型编码" name="typeCode" rules={[{ required: true, message: "请输入类型编码" }]}>
|
||||
<Form.Item label={t('dicts.typeCode')} name="typeCode" rules={[{ required: true, message: t('dicts.typeCode') }]}>
|
||||
<Input disabled={!!editingType} placeholder="例如:user_status…" />
|
||||
</Form.Item>
|
||||
<Form.Item label="类型名称" name="typeName" rules={[{ required: true, message: "请输入类型名称" }]}>
|
||||
<Form.Item label={t('dicts.typeName')} name="typeName" rules={[{ required: true, message: t('dicts.typeName') }]}>
|
||||
<Input placeholder="例如:用户状态…" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注说明" name="remark">
|
||||
<Form.Item label={t('common.remark')} name="remark">
|
||||
<Input.TextArea placeholder="该字典类型的用途描述…" rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
@ -385,7 +383,7 @@ export default function Dictionaries() {
|
|||
title={
|
||||
<Space>
|
||||
<ProfileOutlined aria-hidden="true" />
|
||||
<span>{editingItem ? "编辑字典项" : "新增字典项"}</span>
|
||||
<span>{editingItem ? t('dicts.drawerTitleItemEdit') : t('dicts.drawerTitleItemCreate')}</span>
|
||||
</Space>
|
||||
}
|
||||
open={itemDrawerVisible}
|
||||
|
|
@ -393,26 +391,26 @@ export default function Dictionaries() {
|
|||
width={400}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={() => setItemDrawerVisible(false)}>取消</Button>
|
||||
<Button type="primary" onClick={handleItemSubmit}>确认保存</Button>
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setItemDrawerVisible(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" onClick={handleItemSubmit}>{t('common.confirm')}</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={itemForm} layout="vertical">
|
||||
<Form.Item label="所属类型" name="typeCode">
|
||||
<Form.Item label={t('dicts.typeCode')} name="typeCode">
|
||||
<Input disabled className="tabular-nums" />
|
||||
</Form.Item>
|
||||
<Form.Item label="显示标签" name="itemLabel" rules={[{ required: true, message: "请输入展示标签" }]}>
|
||||
<Form.Item label={t('dicts.itemLabel')} name="itemLabel" rules={[{ required: true, message: t('dicts.itemLabel') }]}>
|
||||
<Input placeholder="例如:正常、禁用…" />
|
||||
</Form.Item>
|
||||
<Form.Item label="存储数值" name="itemValue" rules={[{ required: true, message: "请输入数值" }]}>
|
||||
<Form.Item label={t('dicts.itemValue')} name="itemValue" rules={[{ required: true, message: t('dicts.itemValue') }]}>
|
||||
<Input placeholder="例如:1、0…" className="tabular-nums" />
|
||||
</Form.Item>
|
||||
<Form.Item label="显示排序" name="sortOrder" initialValue={0}>
|
||||
<Form.Item label={t('dicts.sort')} name="sortOrder" initialValue={0}>
|
||||
<InputNumber className="w-full tabular-nums" />
|
||||
</Form.Item>
|
||||
<Form.Item label="当前状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "启用", value: 1 },
|
||||
|
|
@ -420,7 +418,7 @@ export default function Dictionaries() {
|
|||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="备注说明" name="remark">
|
||||
<Form.Item label={t('common.remark')} name="remark">
|
||||
<Input.TextArea placeholder="可选项,备注详细信息…" rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Button, Checkbox, Form, Input, message, Typography } from "antd";
|
||||
import { Button, Checkbox, Form, Input, message, Typography, Space } from "antd";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
||||
import { getCurrentUser, getSystemParamValue } from "../api";
|
||||
import { UserOutlined, LockOutlined, SafetyOutlined, ReloadOutlined, ShopOutlined } from "@ant-design/icons";
|
||||
|
|
@ -8,6 +9,7 @@ import "./Login.css";
|
|||
const { Title, Text, Link } = Typography;
|
||||
|
||||
export default function Login() {
|
||||
const { t } = useTranslation();
|
||||
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
||||
const [captchaEnabled, setCaptchaEnabled] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -21,9 +23,9 @@ export default function Login() {
|
|||
const data = await fetchCaptcha();
|
||||
setCaptcha(data);
|
||||
} catch (e) {
|
||||
message.error("加载验证码失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
}, [captchaEnabled]);
|
||||
}, [captchaEnabled, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
|
|
@ -61,10 +63,10 @@ export default function Login() {
|
|||
} catch (e) {
|
||||
sessionStorage.removeItem("userProfile");
|
||||
}
|
||||
message.success("登录成功");
|
||||
message.success(t('common.success'));
|
||||
window.location.href = "/";
|
||||
} catch (e: any) {
|
||||
message.error(e.message || "登录失败");
|
||||
message.error(e.message || t('common.error'));
|
||||
if (captchaEnabled) {
|
||||
loadCaptcha();
|
||||
}
|
||||
|
|
@ -83,28 +85,27 @@ export default function Login() {
|
|||
|
||||
<div className="login-hero">
|
||||
<h1 className="hero-title">
|
||||
智能会议<br />
|
||||
<span className="hero-accent">实时语音处理</span><br />
|
||||
系统
|
||||
{t('login.heroTitle1')}<br />
|
||||
<span className="hero-accent">{t('login.heroTitle2')}</span><br />
|
||||
{t('login.heroTitle3')}
|
||||
</h1>
|
||||
<p className="hero-desc">
|
||||
全流程自动化会议记录,声纹识别与智能摘要<br />
|
||||
提升团队协作效率的新一代解决方案。
|
||||
{t('login.heroDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-left-footer">
|
||||
<div className="footer-item">企业级安全</div>
|
||||
<div className="footer-item">{t('login.enterpriseSecurity')}</div>
|
||||
<div className="footer-divider" aria-hidden="true" />
|
||||
<div className="footer-item">多语言支持</div>
|
||||
<div className="footer-item">{t('login.multiLang')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-right">
|
||||
<div className="login-container">
|
||||
<div className="login-header">
|
||||
<Title level={2}>账号登录</Title>
|
||||
<Text type="secondary">欢迎回来,请登录您的账号</Text>
|
||||
<Title level={2}>{t('login.welcome')}</Title>
|
||||
<Text type="secondary">{t('login.subtitle')}</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
|
|
@ -122,50 +123,50 @@ export default function Login() {
|
|||
<Input
|
||||
size="large"
|
||||
prefix={<ShopOutlined className="text-gray-400" aria-hidden="true" />}
|
||||
placeholder="租户编码 (平台管理可留空)"
|
||||
aria-label="租户编码"
|
||||
placeholder={t('login.tenantCodePlaceholder')}
|
||||
aria-label={t('login.tenantCode')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: "请输入用户名" }]}
|
||||
rules={[{ required: true, message: t('login.username') }]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<UserOutlined className="text-gray-400" aria-hidden="true" />}
|
||||
placeholder="用户名"
|
||||
placeholder={t('login.username')}
|
||||
autoComplete="username"
|
||||
spellCheck={false}
|
||||
aria-label="用户名"
|
||||
aria-label={t('login.username')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
rules={[{ required: true, message: t('login.password') }]}
|
||||
>
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<LockOutlined className="text-gray-400" aria-hidden="true" />}
|
||||
placeholder="密码"
|
||||
placeholder={t('login.password')}
|
||||
autoComplete="current-password"
|
||||
aria-label="密码"
|
||||
aria-label={t('login.password')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{captchaEnabled && (
|
||||
<Form.Item
|
||||
name="captchaCode"
|
||||
rules={[{ required: true, message: "请输入验证码" }]}
|
||||
rules={[{ required: true, message: t('login.captcha') }]}
|
||||
>
|
||||
<div className="captcha-wrapper">
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<SafetyOutlined className="text-gray-400" aria-hidden="true" />}
|
||||
placeholder="验证码"
|
||||
placeholder={t('login.captcha')}
|
||||
maxLength={6}
|
||||
aria-label="验证码"
|
||||
aria-label={t('login.captcha')}
|
||||
/>
|
||||
<Button
|
||||
className="captcha-image-btn"
|
||||
|
|
@ -181,21 +182,21 @@ export default function Login() {
|
|||
|
||||
<div className="login-extra">
|
||||
<Form.Item name="remember" valuePropName="checked" noStyle>
|
||||
<Checkbox>记住我</Checkbox>
|
||||
<Checkbox>{t('login.rememberMe')}</Checkbox>
|
||||
</Form.Item>
|
||||
<Link className="forgot-password">忘记密码?</Link>
|
||||
<Link className="forgot-password">{t('login.forgotPassword')}</Link>
|
||||
</div>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block size="large" className="login-submit-btn">
|
||||
{loading ? "登录中…" : "立即登录"}
|
||||
{loading ? t('login.loggingIn') : t('login.submit')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="login-footer">
|
||||
<Text type="secondary">
|
||||
演示账号:<Text strong className="tabular-nums">admin</Text> / 密码:<Text strong className="tabular-nums">123456</Text>
|
||||
{t('login.demoAccount')}:<Text strong className="tabular-nums">admin</Text> / {t('login.password')}:<Text strong className="tabular-nums">123456</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchLogs } from "../api";
|
||||
import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { SysLog, UserProfile } from "../types";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Logs() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState("OPERATION");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<SysLog[]>([]);
|
||||
|
|
@ -109,35 +110,35 @@ export default function Logs() {
|
|||
|
||||
const columns = [
|
||||
...(isPlatformAdmin ? [{
|
||||
title: "所属租户",
|
||||
title: t('users.tenant'),
|
||||
dataIndex: "tenantName",
|
||||
key: "tenantName",
|
||||
width: 150,
|
||||
render: (text: string) => <Text type="warning">{text || "系统平台"}</Text>
|
||||
}] : []),
|
||||
{
|
||||
title: "操作账号",
|
||||
title: t('logs.opAccount'),
|
||||
dataIndex: "username",
|
||||
key: "username",
|
||||
width: 120,
|
||||
render: (text: string) => <Text strong>{text || "系统"}</Text>
|
||||
},
|
||||
{
|
||||
title: "操作详情",
|
||||
title: t('logs.opDetail'),
|
||||
dataIndex: "operation",
|
||||
key: "operation",
|
||||
ellipsis: true,
|
||||
render: (text: string) => <Text type="secondary">{text}</Text>
|
||||
},
|
||||
{
|
||||
title: "IP 地址",
|
||||
title: t('logs.ip'),
|
||||
dataIndex: "ip",
|
||||
key: "ip",
|
||||
width: 130,
|
||||
className: "tabular-nums"
|
||||
},
|
||||
{
|
||||
title: "耗时",
|
||||
title: t('logs.duration'),
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: 100,
|
||||
|
|
@ -146,7 +147,7 @@ export default function Logs() {
|
|||
render: renderDuration
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 90,
|
||||
|
|
@ -157,7 +158,7 @@ export default function Logs() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "发生时间",
|
||||
title: t('logs.time'),
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
|
|
@ -167,17 +168,17 @@ export default function Logs() {
|
|||
render: (text: string) => text?.replace('T', ' ').substring(0, 19)
|
||||
},
|
||||
{
|
||||
title: "详情",
|
||||
title: t('common.action'),
|
||||
key: "action",
|
||||
width: 60,
|
||||
fixed: "right" as const,
|
||||
render: (_: any, record: SysLog) => (
|
||||
<Button
|
||||
type="link"
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined aria-hidden="true" />}
|
||||
onClick={() => showDetail(record)}
|
||||
aria-label="查看详细日志信息"
|
||||
aria-label={t('common.view')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -185,7 +186,7 @@ export default function Logs() {
|
|||
|
||||
if (activeTab === "OPERATION") {
|
||||
columns.splice(isPlatformAdmin ? 2 : 1, 0, {
|
||||
title: "请求方法",
|
||||
title: t('logs.method'),
|
||||
dataIndex: "method",
|
||||
key: "method",
|
||||
width: 180,
|
||||
|
|
@ -196,14 +197,14 @@ export default function Logs() {
|
|||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<Title level={4} className="mb-1">系统日志管理</Title>
|
||||
<Text type="secondary">追踪系统内的每一次重要操作,保障系统安全与可追溯性</Text>
|
||||
<Title level={4} className="mb-1">{t('logs.title')}</Title>
|
||||
<Text type="secondary">{t('logs.subtitle')}</Text>
|
||||
</div>
|
||||
|
||||
<Card className="mb-4 shadow-sm">
|
||||
<Space wrap size="middle">
|
||||
<Input
|
||||
placeholder="搜索操作内容…"
|
||||
placeholder={t('logs.searchPlaceholder')}
|
||||
style={{ width: 180 }}
|
||||
value={params.operation}
|
||||
onChange={e => setParams({ ...params, operation: e.target.value })}
|
||||
|
|
@ -211,7 +212,7 @@ export default function Logs() {
|
|||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="执行状态"
|
||||
placeholder={t('common.status')}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
value={params.status}
|
||||
|
|
@ -220,7 +221,7 @@ export default function Logs() {
|
|||
{ label: "成功", value: 1 },
|
||||
{ label: "失败", value: 0 }
|
||||
]}
|
||||
aria-label="筛选执行状态"
|
||||
aria-label={t('common.status')}
|
||||
/>
|
||||
<RangePicker
|
||||
onChange={(dates) => {
|
||||
|
|
@ -237,13 +238,13 @@ export default function Logs() {
|
|||
icon={<SearchOutlined aria-hidden="true" />}
|
||||
onClick={handleSearch}
|
||||
>
|
||||
查询
|
||||
{t('common.search')}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined aria-hidden="true" />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
|
@ -251,11 +252,11 @@ export default function Logs() {
|
|||
<Card className="shadow-sm" styles={{ body: { paddingTop: 0 } }}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
||||
<Tabs.TabPane
|
||||
tab={<span><InfoCircleOutlined aria-hidden="true" />操作日志</span>}
|
||||
tab={<span><InfoCircleOutlined aria-hidden="true" />{t('logs.opLog')}</span>}
|
||||
key="OPERATION"
|
||||
/>
|
||||
<Tabs.TabPane
|
||||
tab={<span><UserOutlined aria-hidden="true" />登录日志</span>}
|
||||
tab={<span><UserOutlined aria-hidden="true" />{t('logs.loginLog')}</span>}
|
||||
key="LOGIN"
|
||||
/>
|
||||
</Tabs>
|
||||
|
|
@ -272,18 +273,18 @@ export default function Logs() {
|
|||
pageSize: params.size,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条数据`
|
||||
showTotal: (total) => t('common.total', { total })
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="日志详细信息"
|
||||
title={t('logs.detailTitle')}
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||||
关闭
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
]}
|
||||
width={700}
|
||||
|
|
@ -291,24 +292,24 @@ export default function Logs() {
|
|||
{selectedLog && (
|
||||
<Descriptions bordered column={1} size="small">
|
||||
{isPlatformAdmin && (
|
||||
<Descriptions.Item label="所属租户">
|
||||
<Descriptions.Item label={t('users.tenant')}>
|
||||
<Text type="warning">{selectedLog.tenantName || "系统平台"}</Text>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="操作详情">{selectedLog.operation}</Descriptions.Item>
|
||||
<Descriptions.Item label="请求方法">
|
||||
<Descriptions.Item label={t('logs.opDetail')}>{selectedLog.operation}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('logs.method')}>
|
||||
<Tag color="blue">{selectedLog.method || "N/A"}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作账号">{selectedLog.username || "系统"}</Descriptions.Item>
|
||||
<Descriptions.Item label="IP 地址" className="tabular-nums">{selectedLog.ip}</Descriptions.Item>
|
||||
<Descriptions.Item label="耗时">{selectedLog.duration ? `${selectedLog.duration}ms` : "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Descriptions.Item label={t('logs.opAccount')}>{selectedLog.username || "系统"}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('logs.ip')} className="tabular-nums">{selectedLog.ip}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('logs.duration')}>{selectedLog.duration ? `${selectedLog.duration}ms` : "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('common.status')}>
|
||||
<Tag color={selectedLog.status === 1 ? "green" : "red"}>
|
||||
{selectedLog.status === 1 ? "成功" : "失败"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="时间" className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item>
|
||||
<Descriptions.Item label="请求参数">
|
||||
<Descriptions.Item label={t('logs.time')} className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('logs.params')}>
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '12px',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Empty
|
||||
} from "antd";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createOrg, deleteOrg, listOrgs, updateOrg, listTenants } from "../api";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
import {
|
||||
|
|
@ -24,7 +25,6 @@ import {
|
|||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ApartmentOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
ShopOutlined
|
||||
} from "@ant-design/icons";
|
||||
|
|
@ -57,6 +57,7 @@ function buildOrgTree(list: SysOrg[]): OrgNode[] {
|
|||
}
|
||||
|
||||
export default function Orgs() {
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -77,7 +78,7 @@ export default function Orgs() {
|
|||
setSelectedTenantId(list[0].id);
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("加载租户列表失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -88,7 +89,7 @@ export default function Orgs() {
|
|||
const list = await listOrgs(selectedTenantId);
|
||||
setData(list || []);
|
||||
} catch (e) {
|
||||
message.error("加载组织架构失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -129,10 +130,10 @@ export default function Orgs() {
|
|||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteOrg(id);
|
||||
message.success("组织已删除");
|
||||
message.success(t('common.success'));
|
||||
loadOrgs();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || "删除失败");
|
||||
message.error(e.message || t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -142,10 +143,10 @@ export default function Orgs() {
|
|||
setSaving(true);
|
||||
if (editing) {
|
||||
await updateOrg(editing.id, values);
|
||||
message.success("更新成功");
|
||||
message.success(t('common.success'));
|
||||
} else {
|
||||
await createOrg(values);
|
||||
message.success("创建成功");
|
||||
message.success(t('common.success'));
|
||||
}
|
||||
setDrawerOpen(false);
|
||||
loadOrgs();
|
||||
|
|
@ -158,45 +159,45 @@ export default function Orgs() {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: "组织名称",
|
||||
title: t('orgs.orgName'),
|
||||
dataIndex: "orgName",
|
||||
key: "orgName",
|
||||
render: (text: string) => <Text strong>{text}</Text>
|
||||
},
|
||||
{
|
||||
title: "组织编码",
|
||||
title: t('orgs.orgCode'),
|
||||
dataIndex: "orgCode",
|
||||
key: "orgCode",
|
||||
width: 150,
|
||||
render: (text: string) => <Tag className="tabular-nums">{text || "-"}</Tag>
|
||||
},
|
||||
{
|
||||
title: "排序",
|
||||
title: t('orgs.sort'),
|
||||
dataIndex: "sortOrder",
|
||||
width: 100,
|
||||
className: "tabular-nums"
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 100,
|
||||
render: (s: number) => <Tag color={s === 1 ? "green" : "red"}>{s === 1 ? "启用" : "禁用"}</Tag>
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: t('common.action'),
|
||||
key: "action",
|
||||
width: 180,
|
||||
render: (_: any, record: SysOrg) => (
|
||||
<Space>
|
||||
{can("sys_org:create") && (
|
||||
<Button type="link" size="small" onClick={() => openCreate(record.id)}>添加下级</Button>
|
||||
<Button type="link" size="small" onClick={() => openCreate(record.id)}>{t('orgs.addSub')}</Button>
|
||||
)}
|
||||
{can("sys_org:update") && (
|
||||
<Button type="text" size="small" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label="编辑组织" />
|
||||
<Button type="text" size="small" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label={t('common.edit')} />
|
||||
)}
|
||||
{can("sys_org:delete") && (
|
||||
<Popconfirm title={`确定删除 "${record.orgName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label="删除组织" />
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -208,28 +209,28 @@ export default function Orgs() {
|
|||
<div className="p-6">
|
||||
<div className="mb-6 flex justify-between items-end">
|
||||
<div>
|
||||
<Title level={4} className="mb-1">组织架构管理</Title>
|
||||
<Text type="secondary">维护企业内部部门层级关系,支持多租户架构隔离</Text>
|
||||
<Title level={4} className="mb-1">{t('orgs.title')}</Title>
|
||||
<Text type="secondary">{t('orgs.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_org:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
|
||||
新增根组织
|
||||
{t('orgs.createRoot')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="shadow-sm mb-4">
|
||||
<Space>
|
||||
<Text strong>所属租户:</Text>
|
||||
<Text strong>{t('users.tenant')}:</Text>
|
||||
<Select
|
||||
style={{ width: 220 }}
|
||||
placeholder="切换租户查看架构"
|
||||
placeholder={t('orgs.selectTenant')}
|
||||
value={selectedTenantId}
|
||||
onChange={setSelectedTenantId}
|
||||
options={tenants.map(t => ({ label: t.tenantName, value: t.id }))}
|
||||
suffixIcon={<ShopOutlined aria-hidden="true" />}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadOrgs}>刷新</Button>
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadOrgs}>{t('common.refresh')}</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
|
|
@ -246,7 +247,7 @@ export default function Orgs() {
|
|||
/>
|
||||
) : (
|
||||
<div className="py-20 flex justify-center">
|
||||
<Empty description="请先选择一个租户以查看其组织架构" />
|
||||
<Empty description={t('orgs.selectTenant')} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
@ -255,7 +256,7 @@ export default function Orgs() {
|
|||
title={
|
||||
<Space>
|
||||
<ApartmentOutlined aria-hidden="true" />
|
||||
<span>{editing ? "编辑组织节点" : "新增组织部门"}</span>
|
||||
<span>{editing ? t('orgs.drawerTitleEdit') : t('orgs.drawerTitleCreate')}</span>
|
||||
</Space>
|
||||
}
|
||||
open={drawerOpen}
|
||||
|
|
@ -264,19 +265,19 @@ export default function Orgs() {
|
|||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>确认提交</Button>
|
||||
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>{t('common.confirm')}</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="所属租户" name="tenantId" rules={[{ required: true }]}>
|
||||
<Form.Item label={t('users.tenant')} name="tenantId" rules={[{ required: true }]}>
|
||||
<Select disabled options={tenants.map(t => ({ label: t.tenantName, value: t.id }))} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="上级部门" name="parentId">
|
||||
<Form.Item label={t('orgs.parentOrg')} name="parentId">
|
||||
<Select
|
||||
placeholder="顶级部门"
|
||||
placeholder={t('orgs.rootOrg')}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
|
|
@ -284,22 +285,22 @@ export default function Orgs() {
|
|||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="部门名称" name="orgName" rules={[{ required: true, message: "请输入部门/组织名称" }]}>
|
||||
<Form.Item label={t('orgs.orgName')} name="orgName" rules={[{ required: true, message: t('orgs.orgName') }]}>
|
||||
<Input placeholder="例如:技术部、财务处…" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="部门编码" name="orgCode">
|
||||
<Form.Item label={t('orgs.orgCode')} name="orgCode">
|
||||
<Input placeholder="例如:DEPT_TECH" className="tabular-nums" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="显示排序" name="sortOrder" initialValue={0}>
|
||||
<Form.Item label={t('dicts.sort')} name="sortOrder" initialValue={0}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ import {
|
|||
Typography,
|
||||
Card,
|
||||
message,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Row,
|
||||
Col
|
||||
} from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api";
|
||||
import type { SysPermission } from "../types";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
|
|
@ -61,6 +64,7 @@ function buildTree(list: SysPermission[]): TreePermission[] {
|
|||
}
|
||||
|
||||
export default function Permissions() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [data, setData] = useState<SysPermission[]>([]);
|
||||
|
|
@ -135,10 +139,10 @@ export default function Permissions() {
|
|||
};
|
||||
if (editing) {
|
||||
await updatePermission(editing.permId, payload);
|
||||
message.success("权限已更新");
|
||||
message.success(t('common.success'));
|
||||
} else {
|
||||
await createPermission(payload);
|
||||
message.success("权限已创建");
|
||||
message.success(t('common.success'));
|
||||
}
|
||||
setOpen(false);
|
||||
load();
|
||||
|
|
@ -152,10 +156,10 @@ export default function Permissions() {
|
|||
const remove = async (id: number) => {
|
||||
try {
|
||||
await deletePermission(id);
|
||||
message.success("权限已删除");
|
||||
message.success(t('common.success'));
|
||||
load();
|
||||
} catch (e) {
|
||||
message.error("删除失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -165,7 +169,7 @@ export default function Permissions() {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: "权限名称",
|
||||
title: t('permissions.permName'),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text: string, record: SysPermission) => (
|
||||
|
|
@ -179,13 +183,13 @@ export default function Permissions() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "权限编码",
|
||||
title: t('permissions.permCode'),
|
||||
dataIndex: "code",
|
||||
key: "code",
|
||||
render: (text: string) => text ? <Tag color="blue" className="tabular-nums">{text}</Tag> : "-"
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
title: t('permissions.permType'),
|
||||
dataIndex: "permType",
|
||||
width: 90,
|
||||
render: (type: string) => (
|
||||
|
|
@ -195,36 +199,36 @@ export default function Permissions() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "排序",
|
||||
title: t('permissions.sort'),
|
||||
dataIndex: "sortOrder",
|
||||
width: 80,
|
||||
className: "tabular-nums"
|
||||
},
|
||||
{
|
||||
title: "路由/组件",
|
||||
title: t('permissions.route'),
|
||||
key: "route",
|
||||
ellipsis: true,
|
||||
render: (_: any, record: SysPermission) => (
|
||||
<div className="flex flex-col">
|
||||
{record.path && <Text type="secondary" size="small" className="tabular-nums">{record.path}</Text>}
|
||||
{record.component && <Text type="secondary" size="small" style={{ fontSize: '11px' }}>{record.component}</Text>}
|
||||
{record.path && <Text type="secondary" style={{ fontSize: '12px' }} className="tabular-nums">{record.path}</Text>}
|
||||
{record.component && <Text type="secondary" style={{ fontSize: '11px' }}>{record.component}</Text>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "显示",
|
||||
title: t('permissions.visible'),
|
||||
dataIndex: "isVisible",
|
||||
width: 80,
|
||||
render: (v: number) => (v === 1 ? <Tag color="blue">可见</Tag> : <Tag>隐藏</Tag>)
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 80,
|
||||
render: (v: number) => (v === 1 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: t('common.action'),
|
||||
width: 120,
|
||||
fixed: "right" as const,
|
||||
render: (_: any, record: SysPermission) => (
|
||||
|
|
@ -235,7 +239,7 @@ export default function Permissions() {
|
|||
size="small"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
onClick={() => openEdit(record)}
|
||||
aria-label={`编辑权限 ${record.name}`}
|
||||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("sys_permission:delete") && (
|
||||
|
|
@ -245,7 +249,7 @@ export default function Permissions() {
|
|||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined aria-hidden="true" />}
|
||||
aria-label={`删除权限 ${record.name}`}
|
||||
aria-label={t('common.delete')}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
@ -256,32 +260,43 @@ export default function Permissions() {
|
|||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<Title level={4} className="mb-1">功能权限管理</Title>
|
||||
<Text type="secondary">配置系统的菜单结构与功能按钮的操作权限点</Text>
|
||||
<div className="mb-6 flex justify-between items-end">
|
||||
<div>
|
||||
<Title level={4} className="mb-1">{t('permissions.title')}</Title>
|
||||
<Text type="secondary">{t('permissions.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_permission:create") && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined aria-hidden="true" />}
|
||||
onClick={openCreate}
|
||||
>
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="mb-4 shadow-sm">
|
||||
<Space wrap size="middle">
|
||||
<Input
|
||||
placeholder="搜索权限名称…"
|
||||
placeholder={t('permissions.permName')}
|
||||
value={query.name}
|
||||
onChange={(e) => setQuery({ ...query, name: e.target.value })}
|
||||
prefix={<SearchOutlined className="text-gray-400" aria-hidden="true" />}
|
||||
style={{ width: 180 }}
|
||||
allowClear
|
||||
aria-label="搜索权限名称"
|
||||
aria-label={t('permissions.permName')}
|
||||
/>
|
||||
<Input
|
||||
placeholder="权限编码…"
|
||||
placeholder={t('permissions.permCode')}
|
||||
value={query.code}
|
||||
onChange={(e) => setQuery({ ...query, code: e.target.value })}
|
||||
style={{ width: 180 }}
|
||||
allowClear
|
||||
aria-label="搜索权限编码"
|
||||
aria-label={t('permissions.permCode')}
|
||||
/>
|
||||
<Select
|
||||
placeholder="权限类型"
|
||||
placeholder={t('permissions.permType')}
|
||||
allowClear
|
||||
value={query.permType || undefined}
|
||||
onChange={(v) => setQuery({ ...query, permType: v || "" })}
|
||||
|
|
@ -290,31 +305,21 @@ export default function Permissions() {
|
|||
{ value: "button", label: "按钮" }
|
||||
]}
|
||||
style={{ width: 120 }}
|
||||
aria-label="筛选权限类型"
|
||||
aria-label={t('permissions.permType')}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined aria-hidden="true" />}
|
||||
onClick={load}
|
||||
>
|
||||
查询
|
||||
{t('common.search')}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined aria-hidden="true" />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
{can("sys_permission:create") && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined aria-hidden="true" />}
|
||||
onClick={openCreate}
|
||||
style={{ background: '#52c41a', borderColor: '#52c41a' }}
|
||||
>
|
||||
新增权限
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
|
|
@ -334,7 +339,7 @@ export default function Permissions() {
|
|||
title={
|
||||
<Space>
|
||||
<ClusterOutlined aria-hidden="true" />
|
||||
<span>{editing ? "修改权限点信息" : "新增功能权限"}</span>
|
||||
<span>{editing ? t('permissions.drawerTitleEdit') : t('permissions.drawerTitleCreate')}</span>
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
|
|
@ -343,9 +348,9 @@ export default function Permissions() {
|
|||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setOpen(false)}>取消</Button>
|
||||
<Button onClick={() => setOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>
|
||||
确认并同步
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -362,22 +367,22 @@ export default function Permissions() {
|
|||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="权限层级" name="level" rules={[{ required: true }]}>
|
||||
<Select aria-label="选择层级">
|
||||
<Form.Item label={t('permissions.level')} name="level" rules={[{ required: true }]}>
|
||||
<Select aria-label={t('permissions.level')}>
|
||||
<Select.Option value={1}>一级入口</Select.Option>
|
||||
<Select.Option value={2}>二级子项</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="权限类型" name="permType" rules={[{ required: true }]}>
|
||||
<Select options={[{ value: "menu", label: "菜单" }, { value: "button", label: "按钮" }]} aria-label="选择类型" />
|
||||
<Form.Item label={t('permissions.permType')} name="permType" rules={[{ required: true }]}>
|
||||
<Select options={[{ value: "menu", label: "菜单" }, { value: "button", label: "按钮" }]} aria-label={t('permissions.permType')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="上级权限"
|
||||
label={t('permissions.parentId')}
|
||||
name="parentId"
|
||||
dependencies={["level"]}
|
||||
rules={[
|
||||
|
|
@ -393,18 +398,18 @@ export default function Permissions() {
|
|||
placeholder={level === 1 ? "一级入口无须父级" : "请选择父级菜单…"}
|
||||
options={parentOptions}
|
||||
disabled={level !== 2}
|
||||
aria-label="选择上级权限"
|
||||
aria-label={t('permissions.parentId')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="名称" name="name" rules={[{ required: true, message: "请输入权限展示名称" }]}>
|
||||
<Form.Item label={t('permissions.permName')} name="name" rules={[{ required: true, message: t('permissions.permName') }]}>
|
||||
<Input placeholder="例如:用户管理、导出报表…" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<span>权限唯一编码</span>
|
||||
<span>{t('permissions.permCode')}</span>
|
||||
<Tooltip title="用于后端 @PreAuthorize 鉴权和前端按钮控制">
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
|
|
@ -424,12 +429,12 @@ export default function Permissions() {
|
|||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="路由路径" name="path">
|
||||
<Form.Item label={t('permissions.path')} name="path">
|
||||
<Input placeholder="/users…" className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="组件路径" name="component">
|
||||
<Form.Item label={t('permissions.component')} name="component">
|
||||
<Input placeholder="pages/Users…" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -437,12 +442,12 @@ export default function Permissions() {
|
|||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="图标名称" name="icon">
|
||||
<Form.Item label={t('permissions.icon')} name="icon">
|
||||
<Input placeholder="AntD 图标名…" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="显示排序" name="sortOrder" initialValue={0}>
|
||||
<Form.Item label={t('permissions.sort')} name="sortOrder" initialValue={0}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -450,18 +455,18 @@ export default function Permissions() {
|
|||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="是否在菜单可见" name="isVisible" initialValue={1}>
|
||||
<Form.Item label={t('permissions.isVisible')} name="isVisible" initialValue={1}>
|
||||
<Select options={[{ value: 1, label: "显示" }, { value: 0, label: "隐藏" }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{ value: 1, label: "启用" }, { value: 0, label: "禁用" }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item label="描述说明" name="description">
|
||||
<Form.Item label={t('permissions.description')} name="description">
|
||||
<Input.TextArea rows={2} placeholder="简要描述权限用途…" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
@ -469,6 +474,3 @@ export default function Permissions() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fixed missing Row/Col imports
|
||||
import { Row, Col } from "antd";
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from "antd";
|
||||
import type { DataNode } from "antd/es/tree";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "../api";
|
||||
import { SearchOutlined, SafetyCertificateOutlined, SaveOutlined, KeyOutlined, ClusterOutlined } from "@ant-design/icons";
|
||||
import type { SysPermission, SysRole } from "../types";
|
||||
|
|
@ -51,20 +52,21 @@ function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
|
|||
return roots;
|
||||
}
|
||||
|
||||
function toTreeData(nodes: PermissionNode[]): DataNode[] {
|
||||
function toTreeData(nodes: PermissionNode[], t: any): DataNode[] {
|
||||
return nodes.map((node) => ({
|
||||
key: node.permId,
|
||||
title: (
|
||||
<Space>
|
||||
<span>{node.name}</span>
|
||||
{node.permType === "button" && <Tag color="blue" size="small" style={{ marginLeft: 4 }}>按钮</Tag>}
|
||||
{node.permType === "button" && <Tag color="blue" size="small" style={{ marginLeft: 4 }}>{t('permissions.permType') === '按钮' ? '按钮' : 'Button'}</Tag>}
|
||||
</Space>
|
||||
),
|
||||
children: node.children && node.children.length > 0 ? toTreeData(node.children) : undefined
|
||||
children: node.children && node.children.length > 0 ? toTreeData(node.children, t) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
export default function RolePermissionBinding() {
|
||||
const { t } = useTranslation();
|
||||
const [roles, setRoles] = useState<SysRole[]>([]);
|
||||
const [permissions, setPermissions] = useState<SysPermission[]>([]);
|
||||
const [loadingRoles, setLoadingRoles] = useState(false);
|
||||
|
|
@ -100,7 +102,7 @@ export default function RolePermissionBinding() {
|
|||
const list = await listPermissions();
|
||||
setPermissions(list || []);
|
||||
} catch (e) {
|
||||
message.error("加载权限定义失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setLoadingPerms(false);
|
||||
}
|
||||
|
|
@ -111,7 +113,6 @@ export default function RolePermissionBinding() {
|
|||
const list = await listRolePermissions(roleId);
|
||||
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);
|
||||
});
|
||||
|
|
@ -120,7 +121,7 @@ export default function RolePermissionBinding() {
|
|||
setHalfCheckedIds([]);
|
||||
} catch (e) {
|
||||
setCheckedPermIds([]);
|
||||
message.error("加载角色授权数据失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -147,21 +148,20 @@ export default function RolePermissionBinding() {
|
|||
}, [roles, searchText]);
|
||||
|
||||
const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]);
|
||||
const antdTreeData = useMemo(() => toTreeData(treeData), [treeData]);
|
||||
const antdTreeData = useMemo(() => toTreeData(treeData, t), [treeData, t]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedRoleId) {
|
||||
message.warning("请从左侧列表中选择一个角色");
|
||||
message.warning(t('rolePerm.selectRole'));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
// Merge checked and half-checked for database persistence
|
||||
const allPermIds = Array.from(new Set([...checkedPermIds, ...halfCheckedIds]));
|
||||
await saveRolePermissions(selectedRoleId, allPermIds);
|
||||
message.success("功能权限策略已成功更新并下发");
|
||||
message.success(t('common.success'));
|
||||
} catch (e) {
|
||||
message.error("保存权限配置失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -171,8 +171,8 @@ export default function RolePermissionBinding() {
|
|||
<div className="p-6">
|
||||
<div className="mb-6 flex justify-between items-end">
|
||||
<div>
|
||||
<Title level={4} className="mb-1">角色权限授权</Title>
|
||||
<Text type="secondary">配置系统中各类角色所拥有的菜单访问权限与功能操作权限点</Text>
|
||||
<Title level={4} className="mb-1">{t('rolePerm.title')}</Title>
|
||||
<Text type="secondary">{t('rolePerm.subtitle')}</Text>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -181,26 +181,27 @@ export default function RolePermissionBinding() {
|
|||
loading={saving}
|
||||
disabled={!selectedRoleId}
|
||||
>
|
||||
{saving ? "正在同步…" : "保存权限策略"}
|
||||
{saving ? t('common.loading') : t('rolePerm.savePolicy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} lg={10}>
|
||||
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
|
||||
<Col xs={24} lg={10} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>系统角色列表</span></Space>}
|
||||
title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>{t('rolePerm.roleList')}</span></Space>}
|
||||
className="shadow-sm full-height-card"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder="搜索角色名称或编码…"
|
||||
placeholder={t('rolePerm.searchRole')}
|
||||
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
aria-label="搜索角色"
|
||||
aria-label={t('rolePerm.searchRole')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100% - 60px)', overflowY: 'auto' }}>
|
||||
<Table
|
||||
rowKey="roleId"
|
||||
size="middle"
|
||||
|
|
@ -215,10 +216,10 @@ export default function RolePermissionBinding() {
|
|||
onClick: () => setSelectedRoleId(record.roleId),
|
||||
className: "cursor-pointer"
|
||||
})}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 个角色` }}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => t('common.total', { total }) }}
|
||||
columns={[
|
||||
{
|
||||
title: "角色信息",
|
||||
title: t('roles.roleName'),
|
||||
key: "role",
|
||||
render: (_, r) => (
|
||||
<div className="min-w-0">
|
||||
|
|
@ -228,28 +229,29 @@ export default function RolePermissionBinding() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 80,
|
||||
render: (v) => (v === 1 ? <Tag color="green" className="m-0">启用</Tag> : <Tag className="m-0">禁用</Tag>)
|
||||
render: (v) => (v === 1 ? <Tag color="green" className="m-0">正常</Tag> : <Tag className="m-0">禁用</Tag>)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} lg={14}>
|
||||
<Col xs={24} lg={14} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title={<Space><KeyOutlined aria-hidden="true" /><span>功能权限配置</span></Space>}
|
||||
title={<Space><KeyOutlined aria-hidden="true" /><span>{t('rolePerm.permConfig')}</span></Space>}
|
||||
className="shadow-sm full-height-card"
|
||||
extra={
|
||||
selectedRole && (
|
||||
<Tag color="blue">当前角色: {selectedRole.roleName}</Tag>
|
||||
<Tag color="blue">{t('rolePerm.currentRole')}: {selectedRole.roleName}</Tag>
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectedRoleId ? (
|
||||
<div className="role-permission-tree-container" style={{ padding: '8px 0', maxHeight: '600px', overflowY: 'auto' }}>
|
||||
<div className="role-permission-tree-container" style={{ padding: '8px 0', height: '100%', overflowY: 'auto' }}>
|
||||
<Tree
|
||||
checkable
|
||||
selectable={false}
|
||||
|
|
@ -259,7 +261,7 @@ export default function RolePermissionBinding() {
|
|||
onCheck={(keys, info) => {
|
||||
const checked = Array.isArray(keys) ? keys : keys.checked;
|
||||
const halfChecked = info.halfCheckedKeys || [];
|
||||
setCheckedPermIds(checked.map(k => Number(k)));
|
||||
setSelectedPermIds(checked.map(k => Number(k)));
|
||||
setHalfCheckedIds(halfChecked.map(k => Number(k)));
|
||||
}}
|
||||
defaultExpandAll
|
||||
|
|
@ -271,7 +273,7 @@ export default function RolePermissionBinding() {
|
|||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200">
|
||||
<ClusterOutlined style={{ fontSize: 40, color: '#bfbfbf', marginBottom: 16 }} aria-hidden="true" />
|
||||
<Text type="secondary">请先从左侧列表中选择一个角色</Text>
|
||||
<Text type="secondary">{t('rolePerm.selectRole')}</Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@ import {
|
|||
Row,
|
||||
Col,
|
||||
Tabs,
|
||||
Empty
|
||||
Empty,
|
||||
Select
|
||||
} from "antd";
|
||||
import type { DataNode } from "antd/es/tree";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
createRole,
|
||||
listPermissions,
|
||||
|
|
@ -77,21 +79,22 @@ const buildPermissionTree = (list: SysPermission[]): PermissionNode[] => {
|
|||
return roots;
|
||||
};
|
||||
|
||||
const toTreeData = (nodes: PermissionNode[]): DataNode[] =>
|
||||
const toTreeData = (nodes: PermissionNode[], t: any): DataNode[] =>
|
||||
nodes.map((node) => ({
|
||||
key: node.permId,
|
||||
title: (
|
||||
<span className="role-permission-node">
|
||||
<span>{node.name}</span>
|
||||
{node.permType === "button" && <Tag color="blue" style={{ marginLeft: 8 }}>按钮</Tag>}
|
||||
{node.permType === "button" && <Tag color="blue" style={{ marginLeft: 8 }}>{t('permissions.permType') === '按钮' ? '按钮' : 'Button'}</Tag>}
|
||||
</span>
|
||||
),
|
||||
children: node.children && node.children.length > 0 ? toTreeData(node.children) : undefined
|
||||
children: node.children && node.children.length > 0 ? toTreeData(node.children, t) : undefined
|
||||
}));
|
||||
|
||||
const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`;
|
||||
|
||||
export default function Roles() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [data, setData] = useState<SysRole[]>([]);
|
||||
|
|
@ -115,8 +118,8 @@ export default function Roles() {
|
|||
const { can } = usePermission();
|
||||
|
||||
const permissionTreeData = useMemo(
|
||||
() => toTreeData(buildPermissionTree(permissions)),
|
||||
[permissions]
|
||||
() => toTreeData(buildPermissionTree(permissions), t),
|
||||
[permissions, t]
|
||||
);
|
||||
|
||||
const loadPermissions = async () => {
|
||||
|
|
@ -165,7 +168,7 @@ export default function Roles() {
|
|||
const users = await fetchUsersByRoleId(role.roleId);
|
||||
setRoleUsers(users || []);
|
||||
} catch (e) {
|
||||
message.error("加载角色详情失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setLoadingUsers(false);
|
||||
}
|
||||
|
|
@ -178,8 +181,6 @@ export default function Roles() {
|
|||
// Reload role detail if permissions list loaded later
|
||||
useEffect(() => {
|
||||
if (selectedRole && permissions.length > 0) {
|
||||
// We don't want to infinite loop, but we need to ensure leafIds are correct
|
||||
// after permissions are loaded.
|
||||
const leafIds = selectedPermIds.filter(id => {
|
||||
return !permissions.some(p => p.parentId === id);
|
||||
});
|
||||
|
|
@ -216,11 +217,11 @@ export default function Roles() {
|
|||
e.stopPropagation();
|
||||
try {
|
||||
await deleteRole(id);
|
||||
message.success("角色已删除");
|
||||
message.success(t('common.success'));
|
||||
if (selectedRole?.roleId === id) setSelectedRole(null);
|
||||
loadRoles();
|
||||
} catch (e) {
|
||||
message.error("删除失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -237,10 +238,10 @@ export default function Roles() {
|
|||
|
||||
if (editing) {
|
||||
await updateRole(editing.roleId, payload);
|
||||
message.success("角色已更新");
|
||||
message.success(t('common.success'));
|
||||
} else {
|
||||
await createRole(payload);
|
||||
message.success("角色已创建");
|
||||
message.success(t('common.success'));
|
||||
}
|
||||
|
||||
setDrawerOpen(false);
|
||||
|
|
@ -258,52 +259,51 @@ export default function Roles() {
|
|||
try {
|
||||
const allPermIds = Array.from(new Set([...selectedPermIds, ...halfCheckedIds]));
|
||||
await saveRolePermissions(selectedRole.roleId, allPermIds);
|
||||
message.success("权限已保存并生效");
|
||||
message.success(t('common.success'));
|
||||
} catch (e) {
|
||||
message.error("保存权限失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="roles-page-v2">
|
||||
<Row gutter={24} style={{ height: 'calc(100vh - 120px)' }}>
|
||||
<div className="roles-page-v2 p-6">
|
||||
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
|
||||
{/* Left: Role List */}
|
||||
<Col span={8} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title="系统角色"
|
||||
className="full-height-card"
|
||||
title={t('roles.title')}
|
||||
className="full-height-card shadow-sm"
|
||||
extra={can("sys_role:create") && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined aria-hidden="true" />}
|
||||
onClick={openCreate}
|
||||
aria-label="新增角色"
|
||||
>
|
||||
新增
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder="搜索角色…"
|
||||
placeholder={t('roles.searchPlaceholder')}
|
||||
prefix={<SearchOutlined aria-hidden="true" />}
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
aria-label="搜索角色"
|
||||
aria-label={t('roles.searchPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="role-list-container">
|
||||
<div className="role-list-container" style={{ height: 'calc(100% - 60px)', overflowY: 'auto' }}>
|
||||
<Table
|
||||
rowKey="roleId"
|
||||
showHeader={false}
|
||||
dataSource={filteredData}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
locale={{ emptyText: <Empty description="暂无角色数据" /> }}
|
||||
locale={{ emptyText: <Empty description={t('roles.selectRole')} /> }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => selectRole(record),
|
||||
className: `cursor-pointer role-row ${selectedRole?.roleId === record.roleId ? 'role-row-selected' : ''}`
|
||||
|
|
@ -312,19 +312,18 @@ export default function Roles() {
|
|||
{
|
||||
title: '角色',
|
||||
render: (_, record) => (
|
||||
<div className="role-item-content">
|
||||
<div className="role-item-content flex justify-between items-center p-2">
|
||||
<div className="role-item-main min-w-0">
|
||||
<div className="role-item-name truncate">{record.roleName}</div>
|
||||
<div className="role-item-code truncate">{record.roleCode}</div>
|
||||
<div className="role-item-name font-medium truncate">{record.roleName}</div>
|
||||
<div className="role-item-code text-xs text-gray-400 truncate">{record.roleCode}</div>
|
||||
</div>
|
||||
<div className="role-item-actions">
|
||||
<div className="role-item-actions flex gap-1">
|
||||
{can("sys_role:update") && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
onClick={e => openEditBasic(e, record)}
|
||||
aria-label={`编辑 ${record.roleName} 基础信息`}
|
||||
/>
|
||||
)}
|
||||
{can("sys_role:delete") && record.roleCode !== 'ADMIN' && (
|
||||
|
|
@ -338,7 +337,6 @@ export default function Roles() {
|
|||
danger
|
||||
icon={<DeleteOutlined aria-hidden="true" />}
|
||||
onClick={e => e.stopPropagation()}
|
||||
aria-label={`删除 ${record.roleName} 角色`}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
@ -356,7 +354,7 @@ export default function Roles() {
|
|||
<Col span={16} style={{ height: '100%' }}>
|
||||
{selectedRole ? (
|
||||
<Card
|
||||
className="full-height-card"
|
||||
className="full-height-card shadow-sm"
|
||||
title={
|
||||
<Space>
|
||||
<SafetyCertificateOutlined style={{ color: '#1890ff' }} aria-hidden="true" />
|
||||
|
|
@ -372,16 +370,16 @@ export default function Roles() {
|
|||
onClick={savePermissions}
|
||||
disabled={!can("sys_role:permission:save")}
|
||||
>
|
||||
{saving ? "保存中…" : "保存权限更改"}
|
||||
{t('roles.savePerms')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Tabs defaultActiveKey="permissions" className="role-tabs">
|
||||
<Tabs.TabPane
|
||||
tab={<Space><KeyOutlined aria-hidden="true" />功能权限</Space>}
|
||||
tab={<Space><KeyOutlined aria-hidden="true" />{t('roles.funcPerms')}</Space>}
|
||||
key="permissions"
|
||||
>
|
||||
<div className="role-permission-tree-v2">
|
||||
<div className="role-permission-tree-v2" style={{ maxHeight: 'calc(100vh - 400px)', overflowY: 'auto' }}>
|
||||
<Tree
|
||||
checkable
|
||||
selectable={false}
|
||||
|
|
@ -399,7 +397,7 @@ export default function Roles() {
|
|||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
tab={<Space><UserOutlined aria-hidden="true" />关联用户 ({roleUsers.length})</Space>}
|
||||
tab={<Space><UserOutlined aria-hidden="true" />{t('roles.assignedUsers')} ({roleUsers.length})</Space>}
|
||||
key="users"
|
||||
>
|
||||
<Table
|
||||
|
|
@ -407,10 +405,10 @@ export default function Roles() {
|
|||
size="small"
|
||||
loading={loadingUsers}
|
||||
dataSource={roleUsers}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 人` }}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => t('common.total', { total }) }}
|
||||
columns={[
|
||||
{
|
||||
title: '用户',
|
||||
title: t('users.userInfo'),
|
||||
render: (_, r) => (
|
||||
<Space>
|
||||
<UserOutlined aria-hidden="true" />
|
||||
|
|
@ -421,10 +419,10 @@ export default function Roles() {
|
|||
</Space>
|
||||
)
|
||||
},
|
||||
{ title: '手机号', dataIndex: 'phone', className: 'tabular-nums' },
|
||||
{ title: '邮箱', dataIndex: 'email' },
|
||||
{ title: t('users.phone'), dataIndex: 'phone', className: 'tabular-nums' },
|
||||
{ title: t('users.email'), dataIndex: 'email' },
|
||||
{
|
||||
title: '状态',
|
||||
title: t('common.status'),
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
render: s => <Tag color={s === 1 ? 'green' : 'red'}>{s === 1 ? '正常' : '禁用'}</Tag>
|
||||
|
|
@ -435,8 +433,8 @@ export default function Roles() {
|
|||
</Tabs>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="full-height-card flex-center">
|
||||
<Empty description="请从左侧列表选择一个角色进行管理" />
|
||||
<Card className="full-height-card flex items-center justify-center shadow-sm">
|
||||
<Empty description={t('roles.selectRole')} />
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
|
|
@ -444,29 +442,29 @@ export default function Roles() {
|
|||
|
||||
{/* Basic Info Drawer */}
|
||||
<Drawer
|
||||
title={editing ? "修改角色基础信息" : "新增系统角色"}
|
||||
title={editing ? t('roles.drawerTitleEdit') : t('roles.drawerTitleCreate')}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
width={400}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
||||
<Button type="primary" loading={saving} onClick={submitBasic}>提交</Button>
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={saving} onClick={submitBasic}>{t('common.confirm')}</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="角色名称" name="roleName" rules={[{ required: true }]}>
|
||||
<Input placeholder="输入名称" />
|
||||
<Form.Item label={t('roles.roleName')} name="roleName" rules={[{ required: true }]}>
|
||||
<Input placeholder={t('roles.roleName')} />
|
||||
</Form.Item>
|
||||
<Form.Item label="角色编码" name="roleCode" rules={[{ required: true }]}>
|
||||
<Input placeholder="输入唯一编码" disabled={!!editing} />
|
||||
<Form.Item label={t('roles.roleCode')} name="roleCode" rules={[{ required: true }]}>
|
||||
<Input placeholder={t('roles.roleCode')} disabled={!!editing} />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{label: '启用', value: 1}, {label: '禁用', value: 0}]} />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="remark">
|
||||
<Form.Item label={t('common.remark')} name="remark">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
@ -474,5 +472,3 @@ export default function Roles() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import { Select } from "antd";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import {
|
|||
Col,
|
||||
Select
|
||||
} from "antd";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createTenant, deleteTenant, listTenants, updateTenant } from "../api";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
import {
|
||||
|
|
@ -35,6 +36,7 @@ import dayjs from "dayjs";
|
|||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Tenants() {
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -58,7 +60,7 @@ export default function Tenants() {
|
|||
setData(result.records || []);
|
||||
setTotal(result.total || 0);
|
||||
} catch (e) {
|
||||
message.error("加载租户列表失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -103,10 +105,10 @@ export default function Tenants() {
|
|||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteTenant(id);
|
||||
message.success("租户已删除");
|
||||
message.success(t('common.success'));
|
||||
loadData();
|
||||
} catch (e) {
|
||||
message.error("删除失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -121,10 +123,10 @@ export default function Tenants() {
|
|||
|
||||
if (editing) {
|
||||
await updateTenant(editing.id, payload);
|
||||
message.success("租户信息已更新");
|
||||
message.success(t('common.success'));
|
||||
} else {
|
||||
await createTenant(payload);
|
||||
message.success("租户已成功创建");
|
||||
message.success(t('common.success'));
|
||||
}
|
||||
setDrawerOpen(false);
|
||||
loadData();
|
||||
|
|
@ -137,7 +139,7 @@ export default function Tenants() {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: "租户信息",
|
||||
title: t('tenants.tenantInfo'),
|
||||
key: "tenant",
|
||||
render: (_: any, record: SysTenant) => (
|
||||
<Space>
|
||||
|
|
@ -152,7 +154,7 @@ export default function Tenants() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "联系人",
|
||||
title: t('tenants.contact'),
|
||||
key: "contact",
|
||||
render: (_: any, record: SysTenant) => (
|
||||
<div>
|
||||
|
|
@ -162,7 +164,7 @@ export default function Tenants() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 100,
|
||||
render: (status: number) => (
|
||||
|
|
@ -172,18 +174,18 @@ export default function Tenants() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "过期时间",
|
||||
title: t('tenants.expireTime'),
|
||||
dataIndex: "expireTime",
|
||||
width: 180,
|
||||
render: (text: string) => (
|
||||
<Space>
|
||||
<CalendarOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Text className="tabular-nums">{text ? text.substring(0, 10) : "永久有效"}</Text>
|
||||
<Text className="tabular-nums">{text ? text.substring(0, 10) : t('tenants.forever')}</Text>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: t('common.action'),
|
||||
key: "action",
|
||||
width: 120,
|
||||
fixed: "right" as const,
|
||||
|
|
@ -194,12 +196,12 @@ export default function Tenants() {
|
|||
type="text"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
onClick={() => openEdit(record)}
|
||||
aria-label={`编辑租户 ${record.tenantName}`}
|
||||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("sys_tenant:delete") && (
|
||||
<Popconfirm title={`确定删除租户 "${record.tenantName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={`删除租户 ${record.tenantName}`} />
|
||||
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -211,12 +213,12 @@ export default function Tenants() {
|
|||
<div className="p-6">
|
||||
<div className="mb-6 flex justify-between items-end">
|
||||
<div>
|
||||
<Title level={4} className="mb-1">租户管理</Title>
|
||||
<Text type="secondary">管理系统多租户基础信息、授权状态及过期时间</Text>
|
||||
<Title level={4} className="mb-1">{t('tenants.title')}</Title>
|
||||
<Text type="secondary">{t('tenants.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_tenant:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||
新增租户
|
||||
{t('tenants.drawerTitleCreate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -224,7 +226,7 @@ export default function Tenants() {
|
|||
<Card className="shadow-sm mb-4">
|
||||
<Space wrap size="middle">
|
||||
<Input
|
||||
placeholder="租户名称…"
|
||||
placeholder={t('tenants.tenantName')}
|
||||
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
||||
style={{ width: 200 }}
|
||||
value={params.name}
|
||||
|
|
@ -232,14 +234,14 @@ export default function Tenants() {
|
|||
allowClear
|
||||
/>
|
||||
<Input
|
||||
placeholder="租户编码…"
|
||||
placeholder={t('tenants.tenantCode')}
|
||||
style={{ width: 180 }}
|
||||
value={params.code}
|
||||
onChange={e => setParams({ ...params, code: e.target.value })}
|
||||
allowClear
|
||||
/>
|
||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>查询</Button>
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}>重置</Button>
|
||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t('common.search')}</Button>
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}>{t('common.reset')}</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
|
|
@ -249,13 +251,14 @@ export default function Tenants() {
|
|||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
size="middle"
|
||||
pagination={{
|
||||
current: params.current,
|
||||
pageSize: params.size,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
onChange: (page, size) => setParams({ ...params, current: page, size }),
|
||||
showTotal: (total) => `共 ${total} 条数据`
|
||||
showTotal: (total) => t('common.total', { total })
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
|
@ -264,7 +267,7 @@ export default function Tenants() {
|
|||
title={
|
||||
<Space>
|
||||
<ShopOutlined aria-hidden="true" />
|
||||
<span>{editing ? "编辑租户信息" : "创建新租户"}</span>
|
||||
<span>{editing ? t('tenants.drawerTitleEdit') : t('tenants.drawerTitleCreate')}</span>
|
||||
</Space>
|
||||
}
|
||||
open={drawerOpen}
|
||||
|
|
@ -273,20 +276,20 @@ export default function Tenants() {
|
|||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>确认提交</Button>
|
||||
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>{t('common.confirm')}</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="租户名称" name="tenantName" rules={[{ required: true, message: "请输入租户名称" }]}>
|
||||
<Form.Item label={t('tenants.tenantName')} name="tenantName" rules={[{ required: true, message: t('tenants.tenantName') }]}>
|
||||
<Input placeholder="例如:云合智慧" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="租户编码" name="tenantCode" rules={[{ required: true, message: "请输入租户编码" }]}>
|
||||
<Form.Item label={t('tenants.tenantCode')} name="tenantCode" rules={[{ required: true, message: t('tenants.tenantCode') }]}>
|
||||
<Input placeholder="例如:UNIS" disabled={!!editing} className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -294,26 +297,26 @@ export default function Tenants() {
|
|||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="联系人姓名" name="contactName">
|
||||
<Form.Item label={t('tenants.contactName')} name="contactName">
|
||||
<Input placeholder="姓名" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="联系电话" name="contactPhone">
|
||||
<Form.Item label={t('tenants.contactPhone')} name="contactPhone">
|
||||
<Input placeholder="手机或座机" className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item label="过期时间" name="expireTime">
|
||||
<DatePicker style={{ width: "100%" }} placeholder="留空为永久有效" />
|
||||
<Form.Item label={t('tenants.expireTime')} name="expireTime">
|
||||
<DatePicker style={{ width: "100%" }} placeholder={t('tenants.forever')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="租户状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{ label: "正常启用", value: 1 }, { label: "禁止访问", value: 0 }]} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="备注说明" name="remark">
|
||||
<Form.Item label={t('common.remark')} name="remark">
|
||||
<Input.TextArea rows={3} placeholder="选填,租户详细背景说明…" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Empty
|
||||
} from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { listRoles, listUserRoles, listUsers, saveUserRoles } from "../api";
|
||||
import { SearchOutlined, UserOutlined, SaveOutlined, TeamOutlined } from "@ant-design/icons";
|
||||
import type { SysRole, SysUser } from "../types";
|
||||
|
|
@ -20,6 +21,7 @@ import type { SysRole, SysUser } from "../types";
|
|||
const { Title, Text } = Typography;
|
||||
|
||||
export default function UserRoleBinding() {
|
||||
const { t } = useTranslation();
|
||||
const [users, setUsers] = useState<SysUser[]>([]);
|
||||
const [roles, setRoles] = useState<SysRole[]>([]);
|
||||
const [loadingUsers, setLoadingUsers] = useState(false);
|
||||
|
|
@ -62,7 +64,7 @@ export default function UserRoleBinding() {
|
|||
setCheckedRoleIds(list || []);
|
||||
} catch (e) {
|
||||
setCheckedRoleIds([]);
|
||||
message.error("加载用户角色数据失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -90,15 +92,15 @@ export default function UserRoleBinding() {
|
|||
|
||||
const handleSave = async () => {
|
||||
if (!selectedUserId) {
|
||||
message.warning("请先在左侧列表中选择一个用户");
|
||||
message.warning(t('userRole.selectUser'));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveUserRoles(selectedUserId, checkedRoleIds);
|
||||
message.success("角色权限已成功授予用户");
|
||||
message.success(t('common.success'));
|
||||
} catch (e) {
|
||||
message.error("更新绑定关系失败");
|
||||
message.error(t('common.error'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -108,8 +110,8 @@ export default function UserRoleBinding() {
|
|||
<div className="p-6">
|
||||
<div className="mb-6 flex justify-between items-end">
|
||||
<div>
|
||||
<Title level={4} className="mb-1">用户角色授权</Title>
|
||||
<Text type="secondary">为指定用户分配系统访问角色,控制其操作权限边界</Text>
|
||||
<Title level={4} className="mb-1">{t('userRole.title')}</Title>
|
||||
<Text type="secondary">{t('userRole.subtitle')}</Text>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -118,26 +120,27 @@ export default function UserRoleBinding() {
|
|||
loading={saving}
|
||||
disabled={!selectedUserId}
|
||||
>
|
||||
{saving ? "保存中…" : "保存权限更改"}
|
||||
{saving ? t('common.loading') : t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
|
||||
<Col xs={24} lg={12} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title={<Space><UserOutlined aria-hidden="true" /><span>用户选择列表</span></Space>}
|
||||
title={<Space><UserOutlined aria-hidden="true" /><span>{t('userRole.userList')}</span></Space>}
|
||||
className="shadow-sm full-height-card"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder="搜索用户名或显示名…"
|
||||
placeholder={t('userRole.searchUser')}
|
||||
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
aria-label="搜索用户"
|
||||
aria-label={t('userRole.searchUser')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100% - 60px)', overflowY: 'auto' }}>
|
||||
<Table
|
||||
rowKey="userId"
|
||||
size="middle"
|
||||
|
|
@ -152,10 +155,10 @@ export default function UserRoleBinding() {
|
|||
onClick: () => setSelectedUserId(record.userId),
|
||||
className: "cursor-pointer"
|
||||
})}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 人` }}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => t('common.total', { total }) }}
|
||||
columns={[
|
||||
{
|
||||
title: "用户信息",
|
||||
title: t('users.userInfo'),
|
||||
key: "user",
|
||||
render: (_, r) => (
|
||||
<div className="min-w-0">
|
||||
|
|
@ -165,28 +168,29 @@ export default function UserRoleBinding() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 80,
|
||||
render: (v) => (v === 1 ? <Tag color="green" className="m-0">正常</Tag> : <Tag className="m-0">禁用</Tag>)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} lg={12}>
|
||||
<Col xs={24} lg={12} style={{ height: '100%' }}>
|
||||
<Card
|
||||
title={<Space><TeamOutlined aria-hidden="true" /><span>授予角色权限</span></Space>}
|
||||
title={<Space><TeamOutlined aria-hidden="true" /><span>{t('userRole.grantRoles')}</span></Space>}
|
||||
className="shadow-sm full-height-card"
|
||||
extra={
|
||||
selectedUser && (
|
||||
<Tag color="blue">正在编辑: {selectedUser.displayName}</Tag>
|
||||
<Tag color="blue">{t('userRole.editing')}: {selectedUser.displayName}</Tag>
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectedUserId ? (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ padding: '8px 0', height: '100%', overflowY: 'auto' }}>
|
||||
<Checkbox.Group
|
||||
style={{ width: "100%" }}
|
||||
value={checkedRoleIds}
|
||||
|
|
@ -215,7 +219,7 @@ export default function UserRoleBinding() {
|
|||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200">
|
||||
<UserOutlined style={{ fontSize: 40, color: '#bfbfbf', marginBottom: 16 }} aria-hidden="true" />
|
||||
<Text type="secondary">请先从左侧选择一个用户</Text>
|
||||
<Text type="secondary">{t('userRole.selectUser')}</Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
TreeSelect
|
||||
} from "antd";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
|
|
@ -39,7 +40,7 @@ import {
|
|||
ApartmentOutlined,
|
||||
ReloadOutlined
|
||||
} from "@ant-design/icons";
|
||||
import type { SysRole, SysUser, SysTenant, SysOrg, OrgNode } from "../types";
|
||||
import type { SysRole, SysUser, SysTenant, SysOrg } from "../types";
|
||||
import "./Users.css";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
|
@ -65,6 +66,7 @@ function buildOrgTree(list: SysOrg[]): any[] {
|
|||
}
|
||||
|
||||
export default function Users() {
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -93,7 +95,7 @@ export default function Users() {
|
|||
setRoles(rolesList || []);
|
||||
setTenants(tenantsResp.records || []);
|
||||
} catch (e) {
|
||||
message.error("加载基础数据失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -169,17 +171,17 @@ export default function Users() {
|
|||
});
|
||||
setDrawerOpen(true);
|
||||
} catch (e) {
|
||||
message.error("获取用户信息详情失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteUser(id);
|
||||
message.success("用户已移除");
|
||||
message.success(t('common.success'));
|
||||
loadUsersData();
|
||||
} catch (e) {
|
||||
message.error("操作失败");
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -217,7 +219,7 @@ export default function Users() {
|
|||
await saveUserRoles(userId, values.roleIds || []);
|
||||
}
|
||||
|
||||
message.success(editing ? "更新成功" : "创建成功");
|
||||
message.success(t('common.success'));
|
||||
setDrawerOpen(false);
|
||||
loadUsersData();
|
||||
} catch (e) {
|
||||
|
|
@ -229,7 +231,7 @@ export default function Users() {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: "用户信息",
|
||||
title: t('users.userInfo'),
|
||||
key: "user",
|
||||
render: (_: any, record: SysUser) => (
|
||||
<Space>
|
||||
|
|
@ -239,7 +241,7 @@ export default function Users() {
|
|||
<div>
|
||||
<Space size={4}>
|
||||
<div className="user-display-name">{record.displayName}</div>
|
||||
{record.isPlatformAdmin && <Tag color="gold" size="small" style={{ fontSize: 10 }}>平台管理</Tag>}
|
||||
{record.isPlatformAdmin && <Tag color="gold" size="small" style={{ fontSize: 10 }}>{t('users.platformAdmin')}</Tag>}
|
||||
</Space>
|
||||
<div className="user-username tabular-nums">@{record.username}</div>
|
||||
</div>
|
||||
|
|
@ -247,7 +249,7 @@ export default function Users() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "所属租户/组织",
|
||||
title: t('users.org'),
|
||||
key: "org",
|
||||
render: (_: any, record: SysUser) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
@ -265,7 +267,7 @@ export default function Users() {
|
|||
)
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: t('common.status'),
|
||||
dataIndex: "status",
|
||||
width: 80,
|
||||
render: (status: number) => (
|
||||
|
|
@ -275,7 +277,7 @@ export default function Users() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: t('common.action'),
|
||||
key: "action",
|
||||
width: 100,
|
||||
fixed: "right" as const,
|
||||
|
|
@ -286,12 +288,12 @@ export default function Users() {
|
|||
type="text"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
onClick={() => openEdit(record)}
|
||||
aria-label={`编辑用户 ${record.displayName}`}
|
||||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("sys_user:delete") && record.userId !== 1 && (
|
||||
<Popconfirm title="确定注销该用户吗?" onConfirm={() => handleDelete(record.userId)}>
|
||||
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={`删除用户 ${record.displayName}`} />
|
||||
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -300,24 +302,24 @@ export default function Users() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="users-page">
|
||||
<div className="users-header">
|
||||
<div className="users-page p-6">
|
||||
<div className="users-header flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<Title level={4} className="users-title">系统用户管理</Title>
|
||||
<Text type="secondary">维护系统多租户下的用户信息、组织归属及权限角色</Text>
|
||||
<Title level={4} className="mb-1">{t('users.title')}</Title>
|
||||
<Text type="secondary">{t('users.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_user:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||
新增用户
|
||||
{t('users.drawerTitleCreate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="users-table-card shadow-sm">
|
||||
<div className="users-table-toolbar">
|
||||
<Space size="middle">
|
||||
<div className="users-table-toolbar mb-4">
|
||||
<Space size="middle" wrap>
|
||||
<Select
|
||||
placeholder="按租户筛选…"
|
||||
placeholder={t('users.tenantFilter')}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
value={filterTenantId}
|
||||
|
|
@ -326,15 +328,16 @@ export default function Users() {
|
|||
suffixIcon={<ShopOutlined aria-hidden="true" />}
|
||||
/>
|
||||
<Input
|
||||
placeholder="搜索用户名、姓名或邮箱…"
|
||||
placeholder={t('users.searchPlaceholder')}
|
||||
prefix={<SearchOutlined aria-hidden="true" />}
|
||||
className="users-search-input"
|
||||
style={{ width: 300 }}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
aria-label="搜索用户"
|
||||
aria-label={t('common.search')}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadUsersData}>刷新</Button>
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadUsersData}>{t('common.refresh')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
|
@ -345,7 +348,7 @@ export default function Users() {
|
|||
loading={loading}
|
||||
size="middle"
|
||||
pagination={{
|
||||
showTotal: (total) => `共 ${total} 条数据`,
|
||||
showTotal: (total) => t('common.total', { total }),
|
||||
pageSize: 10,
|
||||
}}
|
||||
/>
|
||||
|
|
@ -355,7 +358,7 @@ export default function Users() {
|
|||
title={
|
||||
<div className="user-drawer-title">
|
||||
<UserOutlined className="mr-2" aria-hidden="true" />
|
||||
{editing ? "修改用户信息" : "创建系统用户"}
|
||||
{editing ? t('users.drawerTitleEdit') : t('users.drawerTitleCreate')}
|
||||
</div>
|
||||
}
|
||||
open={drawerOpen}
|
||||
|
|
@ -364,9 +367,9 @@ export default function Users() {
|
|||
destroyOnClose
|
||||
footer={
|
||||
<div className="flex justify-end gap-2 p-2">
|
||||
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
||||
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit}>
|
||||
保存更改
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -374,9 +377,9 @@ export default function Users() {
|
|||
<Form form={form} layout="vertical" className="user-form">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="所属租户" name="tenantId" rules={[{ required: true, message: "请选择所属租户" }]}>
|
||||
<Form.Item label={t('users.tenant')} name="tenantId" rules={[{ required: true, message: t('users.tenant') }]}>
|
||||
<Select
|
||||
placeholder="选择租户"
|
||||
placeholder={t('users.tenant')}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={tenants.map(t => ({ label: t.tenantName, value: t.id }))}
|
||||
|
|
@ -384,9 +387,9 @@ export default function Users() {
|
|||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="所属组织" name="orgId">
|
||||
<Form.Item label={t('users.orgNode')} name="orgId">
|
||||
<TreeSelect
|
||||
placeholder="请选择组织节点"
|
||||
placeholder={t('users.orgNode')}
|
||||
allowClear
|
||||
treeData={orgTreeData}
|
||||
disabled={!selectedTenantId}
|
||||
|
|
@ -397,54 +400,54 @@ export default function Users() {
|
|||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="用户名" name="username" rules={[{ required: true, message: "请输入用户名" }]}>
|
||||
<Input placeholder="登录账号" disabled={!!editing} className="tabular-nums" />
|
||||
<Form.Item label={t('users.username')} name="username" rules={[{ required: true, message: t('users.username') }]}>
|
||||
<Input placeholder={t('users.username')} disabled={!!editing} className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="显示姓名" name="displayName" rules={[{ required: true, message: "请输入显示姓名" }]}>
|
||||
<Input placeholder="真实姓名" />
|
||||
<Form.Item label={t('users.displayName')} name="displayName" rules={[{ required: true, message: t('users.displayName') }]}>
|
||||
<Input placeholder={t('users.displayName')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="邮箱地址" name="email">
|
||||
<Form.Item label={t('users.email')} name="email">
|
||||
<Input placeholder="example@domain.com" className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="手机号码" name="phone">
|
||||
<Input placeholder="联系电话" className="tabular-nums" />
|
||||
<Form.Item label={t('users.phone')} name="phone">
|
||||
<Input placeholder={t('users.phone')} className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="登录密码"
|
||||
label={t('users.password')}
|
||||
name="password"
|
||||
rules={[{ required: !editing, message: "请输入登录密码" }]}
|
||||
rules={[{ required: !editing, message: t('users.password') }]}
|
||||
>
|
||||
<Input.Password placeholder={editing ? "留空表示不修改" : "设置初始密码"} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="授予角色" name="roleIds" rules={[{ required: true, message: "请至少选择一个角色" }]}>
|
||||
<Form.Item label={t('users.roles')} name="roleIds" rules={[{ required: true, message: t('users.roles') }]}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择系统角色"
|
||||
placeholder={t('users.roles')}
|
||||
options={roles.map(r => ({ label: r.roleName, value: r.roleId }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="账号状态" name="status" initialValue={1}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="平台管理员" name="isPlatformAdmin" valuePropName="checked">
|
||||
<Form.Item label={t('users.platformAdmin')} name="isPlatformAdmin" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
|
|||
Loading…
Reference in New Issue