布局修改

This commit is contained in:
mmc
2026-04-29 14:26:01 +08:00
parent 935b2eb46a
commit 5d57d45f16
7 changed files with 990 additions and 56 deletions

14
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@ant-design/charts": "^2.6.7",
"@ant-design/icons": "^6.1.1",
"@ant-design/icons": "^6.2.2",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
@@ -141,14 +141,14 @@
}
},
"node_modules/@ant-design/icons": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.1.tgz",
"integrity": "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz",
"integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.0",
"@ant-design/icons-svg": "^4.4.0",
"@rc-component/util": "^1.3.0",
"@ant-design/colors": "^8.0.1",
"@ant-design/icons-svg": "^4.4.2",
"@rc-component/util": "^1.10.1",
"clsx": "^2.1.1"
},
"engines": {

View File

@@ -12,7 +12,7 @@
},
"dependencies": {
"@ant-design/charts": "^2.6.7",
"@ant-design/icons": "^6.1.1",
"@ant-design/icons": "^6.2.2",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",

View File

@@ -63,7 +63,8 @@ export default function AppLayout() {
{ key: '/roles', icon: <UserOutlined />, label: '角色管理', roles: ['admin'] },
{ key: '/menuList', icon: <MenuOutlined />, label: '菜单管理', roles: ['admin'] },
{ key: '/organization', icon: <MenuOutlined />, label: '组织管理', roles: ['admin'] },
{ key: '/station', icon: <MenuOutlined />, label: '场站管理', roles: ['admin'] }
{ key: '/station', icon: <MenuOutlined />, label: '场站管理', roles: ['admin'] },
{ key: '/system', icon: <MenuOutlined />, label: '系统设置', roles: ['admin'] }
];
const filteredMenu = menuItems;

575
src/pages/SystemSetting.tsx Normal file
View File

@@ -0,0 +1,575 @@
import React, { useState } from 'react';
import {
Layout, Menu, Card, Row, Col, Button, Form, Input, Select, Switch, Tag, Space, Typography, Statistic, message, Table, Divider
} from 'antd';
import {
SettingOutlined, UserOutlined, BellOutlined, LinkOutlined, SafetyOutlined, SyncOutlined,
CloudOutlined, TeamOutlined, FileTextOutlined, DatabaseOutlined, CalendarOutlined,
CheckCircleOutlined, CloseCircleOutlined, ReloadOutlined, DownloadOutlined, DeleteOutlined,
KeyOutlined,
AppstoreOutlined,
BankOutlined
} from '@ant-design/icons';
import UserRolePage from './UserPage';
import MenuList from './MenuList';
import Organization from './Organization';
import StationManage from './StationManage';
const { Header, Content } = Layout;
const { Title, Text } = Typography;
const { Option } = Select;
const AntdCard = Card as any;
// 子页面组件,你可以替换成自己的
const BasicSettings = () => {
const [form] = Form.useForm();
const handleSave = () => {
message.success('设置已保存');
};
return (
<div style={{ padding: 24 }}>
{/* 顶部概览卡片 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={5}>
<AntdCard size="small">
<Space>
<div style={{ background: '#e6f4ff', borderRadius: 8, padding: 8 }}>
<CloudOutlined style={{ color: '#1677ff', fontSize: 20 }} />
</div>
<div>
<div style={{ fontSize: 13, color: '#666' }}>当前版本</div>
<div style={{ fontWeight: 600 }}>v2.8.1 <Tag color="blue">最新版</Tag></div>
</div>
</Space>
</AntdCard>
</Col>
<Col span={5}>
<AntdCard size="small">
<Space>
<div style={{ background: '#f6ffed', borderRadius: 8, padding: 8 }}>
<TeamOutlined style={{ color: '#52c41a', fontSize: 20 }} />
</div>
<div>
<div style={{ fontSize: 13, color: '#666' }}>在线用户</div>
<div style={{ fontWeight: 600 }}>12 <span style={{ color: '#52c41a', fontSize: 12 }}>↑ 2</span></div>
</div>
</Space>
</AntdCard>
</Col>
<Col span={5}>
<AntdCard size="small">
<Space>
<div style={{ background: '#fff2f0', borderRadius: 8, padding: 8 }}>
<BellOutlined style={{ color: '#ff4d4f', fontSize: 20 }} />
</div>
<div>
<div style={{ fontSize: 13, color: '#666' }}>告警通道</div>
<div style={{ fontWeight: 600 }}>3 / 4 <span style={{ color: '#52c41a', fontSize: 12 }}>正常</span></div>
</div>
</Space>
</AntdCard>
</Col>
<Col span={5}>
<AntdCard size="small">
<Space>
<div style={{ background: '#e6fffb', borderRadius: 8, padding: 8 }}>
<DatabaseOutlined style={{ color: '#13c2c2', fontSize: 20 }} />
</div>
<div>
<div style={{ fontSize: 13, color: '#666' }}>数据备份状态</div>
<div style={{ fontWeight: 600 }}>正常 <span style={{ color: '#52c41a', fontSize: 12 }}>●</span></div>
</div>
</Space>
</AntdCard>
</Col>
<Col span={4}>
<AntdCard size="small">
<Space>
<div style={{ background: '#f9f0ff', borderRadius: 8, padding: 8 }}>
<CalendarOutlined style={{ color: '#722ed1', fontSize: 20 }} />
</div>
<div>
<div style={{ fontSize: 13, color: '#666' }}>最近更新</div>
<div style={{ fontWeight: 600 }}>2025-05-20</div>
</div>
</Space>
</AntdCard>
</Col>
</Row>
{/* 基础配置 */}
<AntdCard title="基础配置" style={{ marginBottom: 24 }}>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={8}>
<Form.Item label="系统名称" name="systemName">
<Input defaultValue="光伏电站智能监控与运维系统" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="默认电站" name="defaultSite">
<Select defaultValue="1MW 光伏电站">
<Option value="1MW">1MW 光伏电站</Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="默认语言" name="defaultLang">
<Select defaultValue="中文">
<Option value="zh">中文</Option>
<Option value="en">English</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="时区" name="timezone">
<Select defaultValue="UTC+08:00">
<Option value="UTC+08:00">UTC+08:00</Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="首页默认页面" name="homePage">
<Select defaultValue="总览首页">
<Option value="overview">总览首页</Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="数据刷新频率" name="refreshRate">
<Select defaultValue="5分钟">
<Option value="5">5分钟</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="地图服务" name="mapService">
<Select defaultValue="高德地图">
<Option value="amap">高德地图</Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="工单自动编号" name="autoWorkOrder" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="AI建议自动生成" name="autoAiSuggest" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="移动端同步" name="mobileSync" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
</Row>
<Space>
<Button type="primary" onClick={handleSave}>保存设置</Button>
<Button>恢复默认</Button>
</Space>
</Form>
</AntdCard>
{/* 通知与告警策略 */}
<AntdCard title="通知与告警策略" style={{ marginBottom: 24 }}>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Space>
<div style={{ background: '#f6ffed', borderRadius: 8, padding: 8 }}>
<BellOutlined style={{ color: '#52c41a', fontSize: 20 }} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text strong>短信</Text>
<Switch defaultChecked />
</div>
<Text type="secondary">已启用</Text>
</div>
</Space>
</Col>
<Col span={6}>
<Space>
<div style={{ background: '#e6f4ff', borderRadius: 8, padding: 8 }}>
<BellOutlined style={{ color: '#1677ff', fontSize: 20 }} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text strong>邮件</Text>
<Switch defaultChecked />
</div>
<Text type="secondary">已启用</Text>
</div>
</Space>
</Col>
<Col span={6}>
<Space>
<div style={{ background: '#fff7e6', borderRadius: 8, padding: 8 }}>
<BellOutlined style={{ color: '#fa8c16', fontSize: 20 }} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text strong>Telegram</Text>
<Switch defaultChecked />
</div>
<Text type="secondary">已启用</Text>
</div>
</Space>
</Col>
<Col span={6}>
<Space>
<div style={{ background: '#fff0f6', borderRadius: 8, padding: 8 }}>
<BellOutlined style={{ color: '#eb2f96', fontSize: 20 }} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text strong>系统通知</Text>
<Switch defaultChecked />
</div>
<Text type="secondary">已启用</Text>
</div>
</Space>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="严重告警立即通知" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="发电下降超过15%" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="通信中断通知" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item label="每日汇总推送" valuePropName="checked">
<Switch defaultChecked />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="夜间免打扰">
<Select defaultValue="23:00-07:00">
<Option value="23:00-07:00">23:00-07:00</Option>
</Select>
</Form.Item>
</Col>
</Row>
</AntdCard>
{/* 用户与权限预览 */}
<AntdCard title="用户与权限预览">
<Table
size="small"
pagination={{ pageSize: 4 }}
columns={[
{ title: '用户名', dataIndex: 'name' },
{ title: '角色', dataIndex: 'role', render: t => <Tag color="blue">{t}</Tag> },
{ title: '手机号', dataIndex: 'phone' },
{ title: '邮箱', dataIndex: 'email' },
{ title: '状态', dataIndex: 'status', render: () => <span style={{ color: '#52c41a' }}>● 在线</span> },
{ title: '操作', render: () => <Button type="link">查看</Button> }
]}
dataSource={[
{ name: '张运维', role: '超级管理员', phone: '138****0001', email: 'zhangyw@pvsrmart.com' },
{ name: '李工程师', role: '运维工程师', phone: '139****0002', email: 'li.gongcheng@pvsrmart.com' },
{ name: '王班长', role: '巡检主管', phone: '137****0003', email: 'wang.banzhang@pvsrmart.com' },
{ name: '赵分析师', role: '数据分析师', phone: '136****0004', email: 'zhao.fx@pvsrmart.com' },
]}
/>
</AntdCard>
</div>
);
};
// 右侧面板组件
const RightPanel = () => {
return (
<div style={{ padding: 24 }}>
{/* 角色权限概览 */}
<AntdCard title="角色权限概览" style={{ marginBottom: 24 }}>
<Row gutter={16}>
<Col span={12}>
<AntdCard size="small" hoverable style={{ background: '#f0f5ff' }}>
<Space direction="vertical">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ background: '#1677ff', borderRadius: '50%', padding: 4 }}>
<SafetyOutlined style={{ color: '#fff', fontSize: 16 }} />
</div>
<Text strong>超级管理员</Text>
</div>
<Text type="secondary">用户2人 | 权限28项</Text>
<Space>
<Tag color="blue">全部权限</Tag>
<Tag color="blue">系统管理</Tag>
</Space>
</Space>
</AntdCard>
</Col>
<Col span={12}>
<AntdCard size="small" hoverable style={{ background: '#f6ffed' }}>
<Space direction="vertical">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ background: '#52c41a', borderRadius: '50%', padding: 4 }}>
<UserOutlined style={{ color: '#fff', fontSize: 16 }} />
</div>
<Text strong>运维工程师</Text>
</div>
<Text type="secondary">用户5人 | 权限18项</Text>
<Space>
<Tag color="green">设备管理</Tag>
<Tag color="green">工单管理</Tag>
</Space>
</Space>
</AntdCard>
</Col>
<Col span={12} style={{ marginTop: 16 }}>
<AntdCard size="small" hoverable style={{ background: '#fff7e6' }}>
<Space direction="vertical">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ background: '#fa8c16', borderRadius: '50%', padding: 4 }}>
<BellOutlined style={{ color: '#fff', fontSize: 16 }} />
</div>
<Text strong>巡检主管</Text>
</div>
<Text type="secondary">用户8人 | 权限12项</Text>
<Space>
<Tag color="orange">巡检管理</Tag>
<Tag color="orange">告警处理</Tag>
</Space>
</Space>
</AntdCard>
</Col>
<Col span={12} style={{ marginTop: 16 }}>
<AntdCard size="small" hoverable style={{ background: '#f9f0ff' }}>
<Space direction="vertical">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ background: '#722ed1', borderRadius: '50%', padding: 4 }}>
<DatabaseOutlined style={{ color: '#fff', fontSize: 16 }} />
</div>
<Text strong>数据分析师</Text>
</div>
<Text type="secondary">用户3人 | 权限15项</Text>
<Space>
<Tag color="purple">数据分析</Tag>
<Tag color="purple">报表查看</Tag>
</Space>
</Space>
</AntdCard>
</Col>
</Row>
</AntdCard>
{/* 设备接入状态 */}
<AntdCard title="设备接入状态" style={{ marginBottom: 24 }} extra={<Button type="link">查看详情 </Button>}>
<Row gutter={16}>
<Col span={12}>
<Space direction="vertical">
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> 逆变器</Space>
<Text>5 / 5 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> 汇流箱</Space>
<Text>20 / 20 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> 气象站</Space>
<Text>1 / 1 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> 摄像头</Space>
<Text>12 / 12 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> 无人机</Space>
<Text>2 / 2 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> 机器人</Space>
<Text>5 / 5 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> API接口</Space>
<Text>正常 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CheckCircleOutlined style={{ color: '#52c41a' }} /> Modbus</Space>
<Text>正常 <span style={{ color: '#52c41a' }}>● 正常</span></Text>
</div>
</Space>
</Col>
<Col span={12}>
{/* 这里可以放拓扑图,先用占位 */}
<div style={{ height: 240, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px dashed #ddd', borderRadius: 8 }}>
<div style={{ textAlign: 'center' }}>
<DatabaseOutlined style={{ fontSize: 48, color: '#1677ff' }} />
<div style={{ marginTop: 8 }}>设备拓扑图</div>
</div>
</div>
</Col>
</Row>
</AntdCard>
{/* 数据与安全 */}
<AntdCard title="数据与安全" style={{ marginBottom: 24 }}>
<Row gutter={16}>
<Col span={12}>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><DatabaseOutlined /> 数据保留</Space>
<Text>3年</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><ReloadOutlined /> 每日自动备份</Space>
<Text>02:00</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><CalendarOutlined /> 最近备份时间</Space>
<Text>2025-05-24 02:00</Text>
</div>
</Space>
</Col>
<Col span={12}>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><SafetyOutlined /> 登录审计</Space>
<Tag color="green">已开启</Tag>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><KeyOutlined /> API密钥管理</Space>
<Tag color="green">正常</Tag>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><FileTextOutlined /> SSL证书</Space>
<Text>有效期至 2026-12-31</Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Space><SafetyOutlined /> 双因素认证</Space>
<Tag color="green">已开启</Tag>
</div>
</Space>
</Col>
</Row>
</AntdCard>
{/* 系统维护 */}
<AntdCard title="系统维护" extra={<Text type="danger">操作需管理员权限</Text>}>
<Row gutter={16}>
<Col span={6}>
<Button block size="large" icon={<DeleteOutlined />}>清理缓存</Button>
</Col>
<Col span={6}>
<Button block size="large" icon={<SyncOutlined />}>重启服务</Button>
</Col>
<Col span={6}>
<Button block size="large" icon={<DownloadOutlined />}>备份下载</Button>
</Col>
<Col span={6}>
<Button block size="large" icon={<FileTextOutlined />}>日志导出</Button>
</Col>
</Row>
</AntdCard>
</div>
);
};
// 主页面组件
export default function SystemSettings() {
const [activeTab, setActiveTab] = useState('basic');
const tabItems = [
{ key: 'basic', label: '基础设置', icon: <SettingOutlined /> },
{ key: 'user-role', label: '用户与角色', icon: <UserOutlined /> },
{ key: 'menu', label: '菜单管理', icon: <AppstoreOutlined /> },
{ key: 'org', label: '组织管理', icon: <TeamOutlined /> }, //
{ key: 'station', label: '场站管理', icon: <BankOutlined /> },
{ key: 'notification', label: '通知策略', icon: <BellOutlined /> },
{ key: 'device', label: '设备接入', icon: <LinkOutlined /> },
{ key: 'security', label: '数据与安全', icon: <SafetyOutlined /> },
{ key: 'maintenance', label: '系统维护', icon: <SyncOutlined /> },
];
const renderTabContent = () => {
switch (activeTab) {
case 'basic':
return (
<Row gutter={24}>
<Col span={16}><BasicSettings /></Col>
<Col span={8}><RightPanel /></Col>
</Row>
);
case 'user-role':
return <UserRolePage />;
case 'menu':
return <MenuList />; // 菜单管理
case 'org':
return <Organization />; // 组织管理
case 'station':
return <StationManage />; // 场站管理
case 'notification':
return <div style={{ padding: 24 }}>通知策略页面</div>;
case 'device':
return <div style={{ padding: 24 }}>设备接入页面</div>;
case 'security':
return <div style={{ padding: 24 }}>数据与安全页面</div>;
case 'maintenance':
return <div style={{ padding: 24 }}>系统维护页面</div>;
default:
return null;
}
};
return (
<Layout style={{ minHeight: '100vh', background: '#fff', overflow: 'hidden' }}>
{/* 固定在当前组件内部顶部,不随滚动 */}
<Header style={{
background: '#fff',
padding: 0,
borderBottom: '1px solid #f0f0f0',
position: 'sticky',
top: 0,
left: 0,
zIndex: 10,
width: '100%'
}}>
<Menu
mode="horizontal"
selectedKeys={[activeTab]}
onClick={({ key }) => setActiveTab(key)}
items={tabItems}
style={{ border: 'none', height: 64, lineHeight: '64px', fontSize: 14 }}
/>
</Header>
{/* 内容区域可滚动 */}
<Content style={{
background: '#f5f7fa',
maxHeight: 'calc(100vh - 64px)',
overflowY: 'auto',
paddingBottom: 20,
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}>
{renderTabContent()}
</Content>
</Layout>
);
}

338
src/pages/UserPage.tsx Normal file
View File

@@ -0,0 +1,338 @@
import React, { useState, useEffect } from 'react';
import {
Layout, Tabs, Card, Row, Col, Button, Typography, Space, Modal, Form, Tree, message,
Input, Popconfirm, Divider, InputNumber, Switch, Pagination, Table, Select,
Tag
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined
} from '@ant-design/icons';
import { getMenuList } from '../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../lib/utils';
import { addRole, deleteRole, editRoleApi, getRoleList } from '../api/role';
import { addUser, deleteUser, editUser, getUser } from '../api/user';
import EmptyData from '../components/Empty';
const { Text } = Typography;
const { TabPane } = Tabs;
const AntdCard = Card as any;
// 用户 + 角色 合并页面
export default function UserRolePage() {
// ====================== 角色管理状态 ======================
const [roles, setRoles] = useState([]);
const [menuTree, setMenuTree] = useState([]);
const [checkedKeys, setCheckedKeys] = useState<number[]>([]);
const [roleModalVisible, setRoleModalVisible] = useState(false);
const [roleForm] = Form.useForm();
const [editRole, setEditRole] = useState(null);
// ====================== 用户管理状态 ======================
const [userList, setUserList] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
const [currentUser, setCurrentUser] = useState(null);
const [rolesOption, setRolesOption] = useState([]);
// ====================== 统一分页 ======================
const [rolePagination, setRolePagination] = useState({
pageNum: 1, pageSize: 12, total: 0
});
const [userPagination, setUserPagination] = useState({
pageNum: 1, pageSize: 10, total: 0
});
// ====================== 获取角色列表 ======================
const getRoleData = () => {
const params = {
pageNum: rolePagination.pageNum,
pageSize: rolePagination.pageSize
};
getRoleList(params).then(res => {
if (res.code === 200) {
setRoles(res.data || []);
setRolePagination({
...rolePagination,
total: res.total || res.data?.length || 0
});
}
});
};
// ====================== 获取菜单树 ======================
const getMenuData = () => {
getMenuList({ pageNum: 1, pageSize: 1000 }).then(res => {
if (res.code === 200) {
const treeData = buildTreeFromFlat(res.data || []);
setMenuTree(buildPermTree(treeData));
}
});
};
// ====================== 获取用户列表 ======================
const getUserData = () => {
const params = {
pageNum: userPagination.pageNum,
pageSize: userPagination.pageSize
};
getUser(params).then(res => {
if (res.code === 200) {
setUserList(res.rows || []);
setUserPagination({
...userPagination,
total: res.total || res.rows?.length || 0
});
}
});
};
// ====================== 获取角色下拉框 ======================
const getRoleForSelect = () => {
getRoleList({ pageNum: 1, pageSize: 1000 }).then(res => {
if (res.code === 200) setRolesOption(res.data || []);
});
};
// ====================== 加载数据 ======================
useEffect(() => {
getRoleData();
getMenuData();
}, [rolePagination.pageNum, rolePagination.pageSize]);
useEffect(() => {
getUserData();
getRoleForSelect();
}, [userPagination.pageNum, userPagination.pageSize]);
// ====================== 角色操作 ======================
const handleAddRole = () => {
roleForm.resetFields();
setCheckedKeys([]);
setEditRole(null);
roleForm.setFieldsValue({ status: true, roleSort: 1 });
setRoleModalVisible(true);
};
const handleEditRole = (record) => {
setEditRole(record);
roleForm.setFieldsValue({ ...record, status: record.status == 1 });
setCheckedKeys((record.menuIds || []).map(Number));
setRoleModalVisible(true);
};
const handleDeleteRole = (roleId) => {
deleteRole([roleId]).then(res => {
if (res.code === 200) { message.success('删除成功'); getRoleData(); }
else message.error(res.msg || '删除失败');
});
};
const handleSaveRole = () => {
roleForm.validateFields().then(values => {
const params = {
...values,
menuIds: checkedKeys,
status: values.status ? 1 : 0,
roleSort: values.roleSort || 1,
};
if (editRole) params.roleId = editRole.roleId;
const api = editRole ? editRoleApi : addRole;
api(params).then(res => {
if (res.code === 200) {
message.success(editRole ? '编辑成功' : '新增成功');
setRoleModalVisible(false);
getRoleData();
getRoleForSelect();
} else message.error(res.msg || '保存失败');
});
});
};
// ====================== 用户操作 ======================
const handleAddUser = () => {
form.resetFields();
setCurrentUser(null);
setModalVisible(true);
};
const handleEditUser = (record) => {
setCurrentUser(record);
form.setFieldsValue({ ...record, roleIds: record.roles?.map(i => i.roleId) || [] });
setModalVisible(true);
};
const handleDeleteUser = (userId) => {
deleteUser(userId).then(res => {
if (res.code === 200) { message.success('删除成功'); getUserData(); }
else message.error(res.msg || '删除失败');
});
};
const handleSaveUser = () => {
form.validateFields().then(values => {
const params = { ...values, roleIds: values.roleIds || [] };
if (currentUser) params.userId = currentUser.userId;
const api = currentUser ? editUser : addUser;
api(params).then(res => {
if (res.code === 200) {
message.success(currentUser ? '编辑成功' : '新增成功');
setModalVisible(false);
getUserData();
} else message.error(res.msg || '保存失败');
});
});
};
// ====================== 页面主体 ======================
return (
<div style={{
height: '100%',
maxHeight: 'calc(100vh - 64px)',
overflowY: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
padding: '20px',
background: '#f5f7fa'
}}>
{/* 整体卡片 */}
<Card bodyStyle={{ padding: '20px 24px' }} style={{ borderRadius: 12, boxShadow: '0 2px 10px rgba(0,0,0,0.06)' }}>
<Tabs defaultActiveKey="user" type="card" size="large" style={{ marginBottom: 20 }}>
{/* ========== 用户管理 ========== */}
<TabPane tab="用户管理" key="user">
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<div></div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddUser}>新增用户</Button>
</div>
<Table
rowKey="userId"
columns={[
{ title: '用户账号', dataIndex: 'userName' },
{ title: '用户昵称', dataIndex: 'nickName' },
{ title: '手机号', dataIndex: 'phonenumber' },
{ title: '邮箱', dataIndex: 'email' },
{
title: '角色', render: (_, r) => (r.roles || []).map(i => (
<Tag color="blue" key={i.roleId}>{i.roleName || i.roleKey}</Tag>
))
},
{
title: '状态', render: (s) => s == '0' ?
<Tag color="green">正常</Tag> : <Tag color="red">停用</Tag>
},
{
title: '操作', render: (_, r) => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEditUser(r)}>编辑</Button>
<Popconfirm onConfirm={() => handleDeleteUser(r.userId)} title="确定删除?">
<Button type="text" danger>删除</Button>
</Popconfirm>
</Space>
)
}
]}
dataSource={userList}
pagination={{
current: userPagination.pageNum,
pageSize: userPagination.pageSize,
total: userPagination.total,
showSizeChanger: true,
showTotal: t => `共 ${t} 条`
}}
onChange={(p) => setUserPagination({ ...userPagination, pageNum: p.current, pageSize: p.pageSize })}
bordered
size="middle"
/>
</TabPane>
{/* ========== 角色管理 ========== */}
<TabPane tab="角色管理" key="role">
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<div></div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddRole}>新增角色</Button>
</div>
<Row gutter={[16, 16]}>
{roles.length > 0 ? roles.map(r => (
<Col xs={24} sm={12} md={8} lg={6} key={r.roleId}>
<Card hoverable style={{ borderRadius: 10 }}
actions={[
<Button type="text" icon={<EditOutlined />} onClick={() => handleEditRole(r)}>编辑</Button>,
<Popconfirm onConfirm={() => handleDeleteRole(r.roleId)} title="确定删除?">
<Button type="text" danger>删除</Button>
</Popconfirm>
]}
>
<Card.Meta
title={<strong>{r.roleName}</strong>}
description={
<Space direction="vertical" size={2}>
<Text type="secondary">权限:{r.roleKey}</Text>
<Text type="secondary">状态:{r.status == 1 ? '正常' : '停用'}</Text>
{r.admin && <Tag color="red">超级管理员</Tag>}
</Space>
}
/>
</Card>
</Col>
)) : <EmptyData height={260} />}
</Row>
<div style={{ textAlign: 'right', marginTop: 20 }}>
<Pagination
current={rolePagination.pageNum}
pageSize={rolePagination.pageSize}
total={rolePagination.total}
showSizeChanger
showTotal={(t) => `共 ${t} 条`}
onChange={(p, s) => setRolePagination({ pageNum: p, pageSize: s, total: rolePagination.total })}
/>
</div>
</TabPane>
</Tabs>
</Card>
{/* ========== 角色弹窗 ========== */}
<Modal title={editRole ? '编辑角色' : '新增角色'} open={roleModalVisible}
onCancel={() => setRoleModalVisible(false)} onOk={handleSaveRole} width={560}>
<Form form={roleForm} layout="vertical">
<Form.Item name="roleName" label="角色名称" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="roleKey" label="权限标识" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="roleSort" label="排序"><InputNumber style={{ width: '100%' }} /></Form.Item>
<Form.Item name="status" label="状态" valuePropName="checked"><Switch checkedChildren="正常" unCheckedChildren="停用" /></Form.Item>
<Divider>菜单权限</Divider>
<Tree checkable treeData={menuTree} checkedKeys={checkedKeys} onCheck={setCheckedKeys} defaultExpandAll />
</Form>
</Modal>
{/* ========== 用户弹窗 ========== */}
<Modal title={currentUser ? '编辑用户' : '新增用户'} open={modalVisible}
onCancel={() => setModalVisible(false)} onOk={handleSaveUser} width={680} destroyOnClose>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}><Form.Item name="userName" label="账号" rules={[{ required: true }]}><Input /></Form.Item></Col>
<Col span={12}><Form.Item name="nickName" label="昵称" rules={[{ required: true }]}><Input /></Form.Item></Col>
</Row>
<Row gutter={16}>
<Col span={12}><Form.Item name="phonenumber" label="手机"><Input /></Form.Item></Col>
<Col span={12}><Form.Item name="email" label="邮箱"><Input /></Form.Item></Col>
</Row>
<Row gutter={16}>
<Col span={12}><Form.Item name="password" label="密码"><Input.Password placeholder="不填不修改" /></Form.Item></Col>
<Col span={12}><Form.Item name="language" label="语言"><Select><Select.Option value="zh-CN">中文</Select.Option><Select.Option value="en">英语</Select.Option></Select></Form.Item></Col>
</Row>
<Row gutter={16}>
<Col span={12}><Form.Item name="roleIds" label="分配角色"><Select mode="multiple" placeholder="选择角色">{rolesOption.map(i => <Select.Option key={i.roleId} value={i.roleId}>{i.roleName}</Select.Option>)}</Select></Form.Item></Col>
<Col span={12}><Form.Item name="status" label="状态" initialValue="0"><Select><Select.Option value="0">正常</Select.Option><Select.Option value="1">停用</Select.Option></Select></Form.Item></Col>
</Row>
<Form.Item name="siteId" label="场站ID"><Input /></Form.Item>
<Form.Item name="sex" label="性别" initialValue="2"><Select><Select.Option value="0">男</Select.Option><Select.Option value="1">女</Select.Option><Select.Option value="2">未知</Select.Option></Select></Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react';
import {
Card, Table, Button, Typography, Space, Modal, Form,
Input, Select, Popconfirm, message, Tag, Row, Col, Switch
Input, Select, Popconfirm, message, Tag, Row, Col
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined
PlusOutlined, EditOutlined, DeleteOutlined
} from '@ant-design/icons';
import { addUser, deleteUser, editUser, getUser } from '../api/user';
import { getRoleList } from '../api/role';
const { Title } = Typography;
const { Option } = Select;
@@ -17,6 +18,7 @@ export default function UserList() {
const [form] = Form.useForm();
const [currentUser, setCurrentUser] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
const [roles, setRoles] = useState([]);
// 统一分页
const [pagination, setPagination] = useState({
@@ -25,6 +27,16 @@ export default function UserList() {
total: 0
});
// 获取角色列表
const getRole = () => {
const params = { pageNum: 1, pageSize: 1000 };
getRoleList(params).then(res => {
if (res.code === 200) {
setRoles(res.data || []);
}
});
};
// 获取用户列表
const getUserList = () => {
const params = {
@@ -53,6 +65,7 @@ export default function UserList() {
useEffect(() => {
getUserList();
getRole();
}, [pagination.pageNum, pagination.pageSize]);
// 新增
@@ -65,8 +78,11 @@ export default function UserList() {
// 编辑
const handleEdit = (record) => {
setCurrentUser(record);
const roleIds = record.roles?.map(item => item.roleId) || [];
form.setFieldsValue({
...record
...record,
roleIds: roleIds, // 回填多选角色
});
setModalVisible(true);
};
@@ -80,62 +96,66 @@ export default function UserList() {
} else {
messageApi.error(res.msg || '删除失败');
}
});
}).catch(() => {
messageApi.error('删除失败');
})
};
// 提交保存(完全匹配后端字段)
// 提交保存
const handleSubmit = () => {
form.validateFields().then(values => {
// 构建后端需要的完整参数
const params = {
...values,
roleIds: values.roleId ? [values.roleId] : [] // 角色ID数组
roleIds: values.roleIds || [], // 直接提交数组
};
if (currentUser) {
// 编辑
params.userId = currentUser.userId;
editUser(params).then(res => {
if (res.code === 200) {
messageApi.success('编辑成功');
getUserList();
setModalVisible(false);
} else {
messageApi.error(res.msg || '编辑失败');
}
}).catch(() => {
messageApi.error('编辑失败');
});
} else {
// 新增
addUser(params).then(res => {
if (res.code === 200) {
messageApi.success('新增成功');
getUserList();
setModalVisible(false);
} else {
messageApi.error(res.msg || '新增失败');
}
});
}).catch(() => {
messageApi.error('新增失败');
})
}
});
};
// 表格列
// 表格列(支持多角色展示)
const columns = [
{
title: '用户账号',
dataIndex: 'userName'
},
{
title: '用户昵称',
dataIndex: 'nickName'
},
{
title: '手机号',
dataIndex: 'phonenumber'
},
{
title: '邮箱',
dataIndex: 'email'
},
{ title: '用户账号', dataIndex: 'userName' },
{ title: '用户昵称', dataIndex: 'nickName' },
{ title: '手机号', dataIndex: 'phonenumber' },
{ title: '邮箱', dataIndex: 'email' },
{
title: '角色',
dataIndex: 'roleNames',
render: (t) => <Tag color="blue">{t || '未分配'}</Tag>
render: (_, record) => {
const roles = record.roles || [];
if (roles.length === 0) return <Tag>未分配</Tag>;
return roles.map(item => (
<Tag color="blue" key={item.roleId}>
{item.roleName || item.roleKey || '角色'}
</Tag>
));
}
},
{
title: '状态',
@@ -183,7 +203,7 @@ export default function UserList() {
/>
</Card>
{/* 新增/编辑弹窗(完全匹配后端字段) */}
{/* 新增/编辑弹窗 */}
<Modal
title={currentUser ? '编辑用户' : '新增用户'}
open={modalVisible}
@@ -226,16 +246,26 @@ export default function UserList() {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="displayPassword" label="明文密码">
<Input />
<Form.Item name="language" label="语言">
<Select placeholder="请选择语言">
<Option value="zh-CN">中文</Option>
<Option value="en">英语</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="roleId" label="分配角色">
<Input placeholder="角色ID" />
{/* 多角色选择 */}
<Form.Item name="roleIds" label="分配角色">
<Select mode="multiple" placeholder="请选择角色">
{roles.map(item => (
<Option key={item.roleId} value={item.roleId}>
{item.roleName || item.roleKey}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
@@ -248,19 +278,6 @@ export default function UserList() {
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="deptId" label="部门ID">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="orgId" label="组织ID">
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item name="siteId" label="场站ID">
<Input />
</Form.Item>

View File

@@ -17,6 +17,8 @@ const MenuList = lazy(() => import('../pages/MenuList'));
const Organization = lazy(() => import('../pages/Organization'));
const StationManage = lazy(() => import('../pages/StationManage'));
const SerialNumGenerate = lazy(() => import('../pages/SerialNumGenerate'));
const SystemSetting = lazy(() => import('../pages/SystemSetting'));
function PageFallback() {
return (
@@ -78,6 +80,7 @@ const routes: RouteObject[] = [
{ path: 'menuList', element: <MenuList /> },
{ path: 'organization', element: <Organization /> },
{ path: 'station', element: <StationManage /> },
{ path: 'system', element: <SystemSetting /> },
{
path: '*',
element: (