组织架构优化

This commit is contained in:
mmc
2026-04-24 15:28:16 +08:00
parent 4cbc6e03f7
commit 218602870a
7 changed files with 1158 additions and 366 deletions

View File

@@ -36,6 +36,7 @@ const { Header, Sider, Content } = Layout;
export default function AppLayout() {
const [collapsed, setCollapsed] = useState(false);
const [time, setTime] = useState(new Date().toLocaleString());
const [menuList, setMenuList] = useState([]);
const navigate = useNavigate();
const location = useLocation();
const { userInfo } = useSelector((state: RootState) => state.user);
@@ -43,7 +44,7 @@ export default function AppLayout() {
const getMenu = () => {
getMenuList().then(res => {
if (res.code == 200) {
message.success("获取菜单成功")
setMenuList(res.data);
} else {
message.error("获取菜单失败:" + res.msg)
}
@@ -74,14 +75,28 @@ export default function AppLayout() {
{ key: '/video', icon: <VideoCameraOutlined />, label: '在线视频', roles: ['admin', 'operator', 'viewer'] },
{ key: '/alerts', icon: <BellOutlined />, label: '警报中心', roles: ['admin', 'operator'] },
{ key: '/downloads', icon: <DownloadOutlined />, label: '视频下载', roles: ['admin', 'operator'] },
{ key: '/users', icon: <TeamOutlined />, label: '用户管理', roles: ['admin'] },
{ key: '/roles', icon: <UserOutlined />, label: '角色权限', roles: ['admin'] },
{ key: '/serialNumGenerate', icon: <UserOutlined />, label: '序列号生成', roles: ['admin'] },
{ key: '/users', icon: <TeamOutlined />, label: '用户管理', roles: ['admin'] },
{ 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']
}
];

View File

@@ -1,166 +1,160 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
Select,
TreeSelect,
Space,
Popconfirm,
message,
Card,
Typography
Table, Button, Typography, Space, Modal, Form, Tree, message,
Input, Popconfirm, Select
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import type { MenuItem, RoleItem } from '@/types/menu';
import {
PlusOutlined, EditOutlined, DeleteOutlined, SettingOutlined
} from '@ant-design/icons';
const { Title } = Typography;
const { Option } = Select;
// 模拟系统角色
const roleOptions: RoleItem[] = [
{ label: '系统管理员', value: 'admin' },
{ label: '操作员', value: 'operator' },
{ label: '观察员', value: 'viewer' },
];
// 模拟菜单数据
const mockMenuData: MenuItem[] = [
// 1. 完整菜单树
const menuData = [
{
id: '1',
name: '控制台',
path: '/',
parentId: null,
sort: 1,
roles: ['admin', 'operator', 'viewer'],
children: [],
id: 1,
title: '控制台',
path: '/dashboard',
parentId: 0,
children: [
{ id: 11, title: '概览', path: '/dashboard/overview', parentId: 1 },
],
},
{
id: '2',
name: '设备管理',
path: '/devices',
parentId: null,
sort: 2,
roles: ['admin', 'operator'],
children: [],
id: 2,
title: '设备管理',
path: '/device',
parentId: 0,
children: [
{ id: 21, title: '机器人管理', path: '/device/robot', parentId: 2 },
{ id: 22, title: '视频监控', path: '/device/video', parentId: 2 },
],
},
{
id: '3',
name: '视频监控',
path: '/video',
parentId: '2',
sort: 1,
roles: ['admin', 'operator', 'viewer'],
children: [],
id: 3,
title: '系统管理',
path: '/system',
parentId: 0,
children: [
{ id: 31, title: '用户管理', path: '/system/user', parentId: 3 },
{ id: 32, title: '角色管理', path: '/system/role', parentId: 3 },
{ id: 33, title: '菜单管理', path: '/system/menu', parentId: 3 },
],
},
];
const MenuList: React.FC = () => {
const [form] = Form.useForm();
const [menuList, setMenuList] = useState<MenuItem[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [title, setTitle] = useState('新增菜单');
const [currentRecord, setCurrentRecord] = useState<MenuItem | null>(null);
// 2. 角色列表
const roleList = [
{ name: '平台admin', key: 'admin' },
{ name: '区域机构管理员', key: 'region_admin' },
{ name: '单个场站管理员', key: 'site_admin' },
{ name: '场站员工', key: 'site_staff' },
];
// 加载菜单
useEffect(() => {
setLoading(true);
setTimeout(() => {
setMenuList(mockMenuData);
setLoading(false);
}, 300);
}, []);
// 打开新增
const handleAdd = () => {
form.resetFields();
setTitle('新增菜单');
setCurrentRecord(null);
setModalVisible(true);
// 3. 扁平化菜单
const flattenMenu = (menus) => {
let arr = [];
const loop = (list, parentPath = '') => {
list.forEach(item => {
const fullPath = parentPath ? `${parentPath}/${item.path}` : item.path;
arr.push({ ...item, fullPath });
if (item.children?.length) loop(item.children, item.path);
});
};
loop(menus);
return arr;
};
// 存储角色权限
const rolePermissionMap = {};
export default function Roles() {
const [menus, setMenus] = useState(flattenMenu(menuData));
const [menuTree, setMenuTree] = useState(menuData);
// 新增/编辑菜单
const [menuModal, setMenuModal] = useState(false);
const [menuForm] = Form.useForm();
const [editMenu, setEditMenu] = useState(null);
// 权限配置
const [permModal, setPermModal] = useState(false);
const [selectedRole, setSelectedRole] = useState('');
const [checkedKeys, setCheckedKeys] = useState([]);
// 打开编辑
const handleEdit = (record: MenuItem) => {
form.setFieldsValue(record);
setTitle('编辑菜单');
setCurrentRecord(record);
setModalVisible(true);
const handleEdit = (record) => {
setEditMenu(record);
menuForm.setFieldsValue(record);
setMenuModal(true);
};
// 删除
const handleDelete = (id: string) => {
setMenuList(prev => prev.filter(item => item.id !== id));
message.success('删除成功');
};
// 提交保存
const handleSubmit = () => {
form.validateFields().then(values => {
const params: MenuItem = {
...values,
id: currentRecord?.id || Date.now().toString(),
};
if (currentRecord) {
// 保存菜单(新增+编辑)
const handleSaveMenu = () => {
menuForm.validateFields().then(values => {
if (editMenu) {
// 编辑
setMenuList(prev => prev.map(m => m.id === currentRecord.id ? params : m));
setMenus(menus.map(m => m.id === editMenu.id ? { ...m, ...values } : m));
message.success('编辑成功');
} else {
// 新增
setMenuList(prev => [...prev, params]);
message.success('新增成功');
const newMenu = {
...values,
id: Date.now(),
parentId: 0,
};
setMenus([...menus, newMenu]);
message.success('新增菜单成功');
}
setModalVisible(false);
setMenuModal(false);
});
};
// 表格列
// 删除菜单
const handleDelete = (id) => {
setMenus(menus.filter(m => m.id !== id));
message.success('删除成功');
};
// 打开权限配置
const handlePermOpen = () => {
if (!selectedRole) {
message.warning('请先选择角色');
return;
}
// 回显已保存权限
const saved = rolePermissionMap[selectedRole] || [];
setCheckedKeys(saved);
setPermModal(true);
};
// 保存权限
const handlePermSave = () => {
rolePermissionMap[selectedRole] = checkedKeys;
message.success(`【${selectedRole}】权限保存成功`);
setPermModal(false);
};
const columns = [
{
title: '菜单名称',
dataIndex: 'name',
key: 'name',
},
{
title: '路由地址',
dataIndex: 'path',
key: 'path',
},
{
title: '父级菜单',
key: 'parentName',
render: (_, record) => {
if (!record.parentId) return '一级菜单';
const p = menuList.find(m => m.id === record.parentId);
return p?.name || '-';
},
},
{
title: '关联角色',
key: 'roles',
render: (_, record) => (
<div>
{record.roles.map(r => {
const ro = roleOptions.find(o => o.value === r);
return <span key={r} style={{ marginRight: 8 }}>{ro?.label}</span>;
})}
</div>
),
},
{ title: '菜单名称', dataIndex: 'title', key: 'title', width: 180 },
{ title: '路由地址', dataIndex: 'fullPath', key: 'fullPath' },
{
title: '操作',
key: 'action',
width: 220,
render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
编辑
</Button>
<Popconfirm
title="确定删除?"
title="确定删除该菜单?"
onConfirm={() => handleDelete(record.id)}
>
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
<Button type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
@@ -168,75 +162,87 @@ const MenuList: React.FC = () => {
];
return (
<Card>
<div className="flex justify-between mb-4">
<Title level={5}>菜单管理</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增菜单</Button>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Title level={5} style={{ margin: 0 }}>菜单权限管理</Title>
<Space>
<Select
placeholder="选择角色"
style={{ width: 180 }}
value={selectedRole}
onChange={setSelectedRole}
>
{roleList.map(r => (
<Option key={r.key} value={r.key}>{r.name}</Option>
))}
</Select>
<Button
type="primary"
icon={<SettingOutlined />}
onClick={handlePermOpen}
>
配置权限
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditMenu(null);
menuForm.resetFields();
setMenuModal(true);
}}
>
新增菜单
</Button>
</Space>
</div>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={menuList}
dataSource={menus}
rowKey="id"
pagination={false}
bordered
/>
{/* 菜单 新增/编辑 弹窗 */}
<Modal
title={title}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSubmit}
maskClosable={false}
title={editMenu ? '编辑菜单' : '新增菜单'}
open={menuModal}
onCancel={() => setMenuModal(false)}
onOk={handleSaveMenu}
width={500}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="菜单名称"
rules={[{ required: true, message: '请输入菜单名称' }]}
>
<Input placeholder="请输入" />
<Form form={menuForm} layout="vertical">
<Form.Item name="title" label="菜单名称" rules={[{ required: true }]}>
<Input placeholder="请输入菜单名称" />
</Form.Item>
<Form.Item
name="path"
label="路由地址"
rules={[{ required: true, message: '请输入路由' }]}
>
<Input placeholder="/devices" />
<Form.Item name="path" label="路由地址" rules={[{ required: true }]}>
<Input placeholder="/system/xxx" />
</Form.Item>
<Form.Item name="parentId" label="上级菜单">
<TreeSelect
placeholder="请选择"
allowClear
treeData={menuList.map(m => ({
title: m.name,
value: m.id,
disabled: m.id === currentRecord?.id,
}))}
/>
</Form.Item>
<Form.Item
name="roles"
label="关联角色"
rules={[{ required: true, message: '请选择至少一个角色' }]}
>
<Select mode="multiple" placeholder="请选择">
{roleOptions.map(o => (
<Option key={o.value} value={o.value}>{o.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="sort" label="排序号">
<Input type="number" placeholder="数字越小越靠前" />
</Form.Item>
</Form>
</Modal>
</Card>
);
};
export default MenuList;
{/* 权限配置弹窗 */}
<Modal
title={`【${selectedRole}】菜单权限配置`}
open={permModal}
width={520}
onCancel={() => setPermModal(false)}
onOk={handlePermSave}
>
<Tree
checkable
treeData={menuTree}
checkedKeys={checkedKeys}
onCheck={setCheckedKeys}
defaultExpandAll
fieldNames={{ title: 'title', key: 'id', children: 'children' }}
/>
</Modal>
</div>
);
}

159
src/pages/Organization.tsx Normal file
View File

@@ -0,0 +1,159 @@
import React, { useState } from 'react';
import {
Card, Table, Button, Typography, Space, Modal, Form, Tree, message,
Input, Popconfirm, Select, TreeSelect
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined, BuildOutlined
} from '@ant-design/icons';
const { Title } = Typography;
const { Option } = Select;
// 模拟组织数据
const orgData = [
{
id: 1,
name: '华东区域总部',
code: 'EAST_REGION',
parentId: 0,
status: '0',
},
{
id: 2,
name: '华北区域总部',
code: 'NORTH_REGION',
parentId: 0,
status: '0',
},
];
// 转平数组用于表格
const flatten = (list) => {
let arr = [];
const loop = (data) => {
data.forEach(item => {
arr.push(item);
if (item.children) loop(item.children);
});
};
loop(list);
return arr;
};
export default function Organization() {
const [orgList, setOrgList] = useState(flatten(orgData));
const [treeData] = useState(orgData);
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
const [editRecord, setEditRecord] = useState(null);
// 打开新增
const handleAdd = () => {
form.resetFields();
setEditRecord(null);
setModalVisible(true);
};
// 打开编辑
const handleEdit = (record) => {
form.setFieldsValue(record);
setEditRecord(record);
setModalVisible(true);
};
// 删除
const handleDelete = (id) => {
setOrgList(orgList.filter(i => i.id !== id));
message.success('删除成功');
};
// 保存
const handleSave = () => {
form.validateFields().then(values => {
if (editRecord) {
setOrgList(orgList.map(i => i.id === editRecord.id ? { ...i, ...values } : i));
message.success('编辑成功');
} else {
setOrgList([...orgList, { ...values, id: Date.now() }]);
message.success('新增成功');
}
setModalVisible(false);
});
};
const columns = [
{ title: '组织名称', dataIndex: 'name' },
{ title: '组织编码', dataIndex: 'code' },
{
title: '状态',
dataIndex: 'status',
render: s => s === '0' ?
<Button type="text" size="small">正常</Button> :
<Button type="text" danger size="small">禁用</Button>
},
{
title: '操作',
render: (_, r) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(r)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.id)}>
<Button type="link" danger>删除</Button>
</Popconfirm>
</Space>
)
}
];
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Title level={5}>组织管理</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
新增组织
</Button>
</div>
<Card>
<Table
rowKey="id"
columns={columns}
dataSource={orgList}
pagination={false}
/>
</Card>
<Modal
title={editRecord ? '编辑组织' : '新增组织'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSave}
width={500}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="组织名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="组织编码" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="parentId" label="上级组织">
<TreeSelect
treeData={treeData}
placeholder="选择上级"
fieldNames={{ label: 'name', value: 'id' }}
/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select>
<Option value="0">正常</Option>
<Option value="1">禁用</Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@@ -1,103 +1,220 @@
import React from 'react';
import {
Card,
Row,
Col,
Button,
Typography,
Tag,
Space,
Divider,
List
import React, { useState, useEffect } from 'react';
import {
Card, Row, Col, Button, Typography, Space, Modal, Form, Tree, message,
Input, Popconfirm, Divider
} from 'antd';
import {
SafetyCertificateOutlined,
LockOutlined,
CheckCircleOutlined,
PlusOutlined,
TeamOutlined
import {
PlusOutlined, LockOutlined, SettingOutlined, EditOutlined, DeleteOutlined
} from '@ant-design/icons';
const { Text, Title } = Typography;
// 默认角色
const defaultRoles = [
{ name: '超级管理员', key: 'admin', desc: '系统全权限', level: 'Level 1' },
{ name: '机构管理员', key: 'operator', desc: '设备/警报/视频管理', level: 'Level 2' },
{ name: '场站管理员', key: 'site_admin', desc: '场站管理权限', level: 'Level 2' },
{ name: '场站操作员', key: 'viewer', desc: '只读查看', level: 'Level 3' },
];
// 模拟菜单树
const defaultMenuTree = [
{ title: '控制台', value: 'dashboard', key: '1' },
{
title: '设备管理', value: 'device', key: '2',
children: [
{ title: '机器人管理', value: 'robot', key: '2-1' },
{ title: '视频监控', value: 'video', key: '2-2' },
]
},
{ title: '告警管理', value: 'alarm', key: '3' },
{ title: '用户管理', value: 'user', key: '4' },
{ title: '角色管理', value: 'role', key: '5' },
{ title: '菜单管理', value: 'menu', key: '6' },
];
export default function Roles() {
const roles = [
{ name: '超级管理员', key: 'admin', desc: '系统全权限,支持资源分配与权限审计', level: 'Level 1' },
{ name: '系统操作员', key: 'operator', desc: '设备策略下发,实时指令干预及警报处理', level: 'Level 2' },
{ name: '访客观察员', key: 'viewer', desc: '只读权限,支持实时视频预览与报表查阅', level: 'Level 3' },
];
const [roles, setRoles] = useState(defaultRoles);
const [menuTree, setMenuTree] = useState(defaultMenuTree);
const [checkedKeys, setCheckedKeys] = useState([]);
const [permVisible, setPermVisible] = useState(false);
const [currentRole, setCurrentRole] = useState(null);
const permissions = [
{ name: '设备控制权限', sectors: ['远程重启', '策略部署', '紧急停机'] },
{ name: '视频管理权限', sectors: ['实时监控', '云台控制', '录像下载'] },
{ name: '核心准入权限', sectors: ['审计日志', '成本报表', '用户配置'] },
];
// 新增/编辑角色弹窗
const [roleModalVisible, setRoleModalVisible] = useState(false);
const [roleForm] = Form.useForm();
const [editRole, setEditRole] = useState(null);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Title level={5} style={{ margin: 0 }}>系统角色权限矩阵</Title>
<Button type="primary" icon={<PlusOutlined />} style={{ borderRadius: 2 }}>
新增角色组
</Button>
</div>
// 打开权限配置
const handleOpenPerm = async (role) => {
setCurrentRole(role);
setCheckedKeys([]);
setPermVisible(true);
};
<Row gutter={16}>
{roles.map((role) => (
<Col span={8} key={role.key}>
<Card
hoverable
style={{ borderRadius: 2 }}
actions={[
<Button type="link" size="small" icon={<TeamOutlined />}>管理成员</Button>
]}
>
<Card.Meta
avatar={
<div className="w-10 h-10 bg-blue-50 text-blue-500 rounded flex items-center justify-center border border-blue-100">
<SafetyCertificateOutlined style={{ fontSize: 20 }} />
</div>
}
title={<span style={{ fontSize: 14 }}>{role.name}</span>}
description={
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace', textTransform: 'uppercase', fontStyle: 'italic' }}>{role.level}</Text>
<Text type="secondary" style={{ fontSize: 12, lineHeight: '1.5', height: 40, display: 'block' }}>{role.desc}</Text>
</Space>
}
/>
</Card>
</Col>
))}
</Row>
// 保存权限
const handleSavePerm = () => {
message.success(`【${currentRole.name}】菜单权限配置成功!`);
setPermVisible(false);
};
<Card
size="small"
title={<Space><LockOutlined style={{ color: 'rgba(0,0,0,0.45)' }} /><Text strong style={{ fontSize: 14 }}>功能模块权限分配 (细分原子操作)</Text></Space>}
style={{ borderRadius: 2 }}
>
<Row gutter={24} style={{ padding: '8px 0' }}>
{permissions.map((p) => (
<Col span={8} key={p.name}>
<div className="space-y-3">
<Text strong style={{ fontSize: 11, borderLeft: '2px solid #1890ff', paddingLeft: 8, textTransform: 'uppercase', letterSpacing: 1 }}>{p.name}</Text>
<List
size="small"
dataSource={p.sectors}
renderItem={(item) => (
<List.Item style={{ padding: '8px 4px', border: 'none' }}>
<div className="flex items-center justify-between w-full p-2 bg-gray-50/50 rounded-sm hover:bg-gray-50 border border-transparent hover:border-gray-200 transition-all">
<Text type="secondary" style={{ fontSize: 12 }}>{item}</Text>
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 12 }} />
</div>
</List.Item>
)}
/>
</div>
</Col>
))}
</Row>
</Card>
</div>
);
// 打开新增角色
const handleAddRole = () => {
roleForm.resetFields();
setCheckedKeys([]);
setEditRole(null);
setRoleModalVisible(true);
};
// 打开编辑角色
const handleEditRole = (record) => {
setEditRole(record);
roleForm.setFieldsValue(record);
setCheckedKeys(record.menuKeys || []);
setRoleModalVisible(true);
};
// 删除角色
const handleDeleteRole = (key) => {
setRoles(roles.filter(item => item.key !== key));
message.success('删除成功');
};
// 提交保存角色
const handleSaveRole = () => {
roleForm.validateFields().then(values => {
const roleData = {
...values,
menuKeys: checkedKeys, // 保存菜单权限
level: editRole ? editRole.level : 'Level Custom'
};
if (editRole) {
// 编辑
setRoles(roles.map(r => r.key === editRole.key ? { ...r, ...roleData } : r));
message.success('编辑成功');
} else {
// 新增
const newRole = {
...roleData,
key: values.key || Date.now().toString(),
};
setRoles([...roles, newRole]);
message.success('新增角色成功');
}
setRoleModalVisible(false);
});
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Title level={5} style={{ margin: 0 }}>角色管理</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddRole}>
新增角色
</Button>
</div>
<Row gutter={16}>
{roles.map(role => (
<Col span={8} key={role.key}>
<Card
hoverable
actions={[
<Button
type="link"
icon={<SettingOutlined />}
onClick={() => handleOpenPerm(role)}
>
菜单权限
</Button>,
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEditRole(role)}
>
编辑
</Button>,
<Popconfirm
title="确定删除该角色?"
onConfirm={() => handleDeleteRole(role.key)}
>
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
]}
>
<Card.Meta
title={role.name}
description={
<Space direction="vertical" size={2}>
<Text type="secondary" style={{ fontSize: 10 }}>{role.level}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{role.desc}</Text>
</Space>
}
/>
</Card>
</Col>
))}
</Row>
{/* ========== 菜单权限配置弹窗 ========== */}
<Modal
title={`【${currentRole?.name}】菜单权限`}
open={permVisible}
width={520}
onCancel={() => setPermVisible(false)}
onOk={handleSavePerm}
>
<Tree
checkable
treeData={menuTree}
checkedKeys={checkedKeys}
onCheck={setCheckedKeys}
defaultExpandAll
/>
</Modal>
{/* ========== 新增/编辑角色弹窗(带菜单选择) ========== */}
<Modal
title={editRole ? '编辑角色' : '新增角色'}
open={roleModalVisible}
onCancel={() => setRoleModalVisible(false)}
onOk={handleSaveRole}
maskClosable={false}
width={550}
>
<Form form={roleForm} layout="vertical">
<Form.Item
name="name"
label="角色名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input placeholder="例如:运维管理员" />
</Form.Item>
<Form.Item
name="key"
label="角色标识"
rules={[{ required: true, message: '请输入角色标识' }]}
>
<Input placeholder="例如:operation" />
</Form.Item>
<Form.Item name="desc" label="角色描述">
<Input.TextArea rows={3} placeholder="描述该角色的权限范围" />
</Form.Item>
{/* ========== 新增角色时直接选择菜单权限 ========== */}
<Divider orientation="left">菜单权限配置</Divider>
<Tree
checkable
treeData={menuTree}
checkedKeys={checkedKeys}
onCheck={setCheckedKeys}
defaultExpandAll
/>
</Form>
</Modal>
</div>
);
}

290
src/pages/StationManage.tsx Normal file
View File

@@ -0,0 +1,290 @@
import React, { useState } from 'react';
import {
Card, Table, Button, Typography, Space, Modal, Form,
Input, Popconfirm, Select, Switch, Divider, Upload, message,
Row, Col, Tag
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined,
BgColorsOutlined, UserOutlined, SettingOutlined
} from '@ant-design/icons';
const { Title } = Typography;
const { Option } = Select;
// 模拟场站数据
const stationData = [
{
id: 1,
name: '上海浦东光伏场站',
code: 'PD001',
address: '上海市浦东新区光伏大道',
status: 0,
contact: '张三',
phone: '13800138000',
maintainCompany: '上海新能源运维公司',
areaCount: 3,
hasMap: true,
},
{
id: 2,
name: '苏州昆山储能电站',
code: 'KS001',
address: '苏州市昆山市能源路',
status: 1,
contact: '李四',
phone: '13900139000',
maintainCompany: '苏州智慧运维有限公司',
areaCount: 2,
hasMap: false,
},
];
// 场站分区模拟
const areaData = [
{ id: 1, stationId: 1, name: 'A区', type: '清扫区', status: 0 },
{ id: 2, stationId: 1, name: 'B区', type: '巡检区', status: 0 },
{ id: 3, stationId: 1, name: 'C区', type: '禁飞区', status: 1 },
];
export default function StationManage() {
const [stationList, setStationList] = useState(stationData);
const [currentStation, setCurrentStation] = useState(null);
// 主弹窗
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
const [editRecord, setEditRecord] = useState(null);
// 分区弹窗
const [areaVisible, setAreaVisible] = useState(false);
// 边界绘制弹窗
const [mapVisible, setMapVisible] = useState(false);
// 打开新增
const handleAdd = () => {
form.resetFields();
setEditRecord(null);
setModalVisible(true);
};
// 打开编辑
const handleEdit = (record) => {
form.setFieldsValue(record);
setEditRecord(record);
setModalVisible(true);
};
// 删除场站
const handleDelete = (id) => {
setStationList(stationList.filter(s => s.id !== id));
message.success('删除成功');
};
// 保存场站
const handleSave = () => {
form.validateFields().then(values => {
const data = { ...values, id: editRecord?.id || Date.now() };
if (editRecord) {
setStationList(stationList.map(s => s.id === editRecord.id ? data : s));
message.success('编辑成功');
} else {
setStationList([...stationList, data]);
message.success('新增场站成功');
}
setModalVisible(false);
});
};
// 打开分区
const handleOpenArea = (record) => {
setCurrentStation(record);
setAreaVisible(true);
};
// 打开地图边界
const handleOpenMap = (record) => {
setCurrentStation(record);
setMapVisible(true);
};
const columns = [
{ title: '场站名称', dataIndex: 'name' },
{ title: '场站编号', dataIndex: 'code' },
{ title: '地址', dataIndex: 'address' },
{
title: '状态',
dataIndex: 'status',
render: (s) => s === 0 ?
<Tag color="green">启用</Tag> :
<Tag color="red">停用</Tag>
},
{ title: '负责人', dataIndex: 'contact' },
{ title: '运维单位', dataIndex: 'maintainCompany' },
{
title: '分区数量',
dataIndex: 'areaCount',
},
{
title: '地图数据',
render: (_, r) => r.hasMap ?
<Tag color="blue">已维护</Tag> :
<Tag>未维护</Tag>
},
{
title: '操作',
width: 400,
render: (_, record) => (
<Space wrap size="small">
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" icon={<BgColorsOutlined />} onClick={() => handleOpenArea(record)}>分区设置</Button>
<Button type="link" onClick={() => handleOpenMap(record)}>边界绘制</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger>删除</Button>
</Popconfirm>
</Space>
)
}
];
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Title level={5}>场站管理</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
新增场站
</Button>
</div>
<Card>
<Table
rowKey="id"
columns={columns}
dataSource={stationList}
pagination={{ pageSize: 10 }}
/>
</Card>
{/* ====================== 1. 新增/编辑场站 ====================== */}
<Modal
title={editRecord ? '编辑场站' : '新增场站'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSave}
width={700}
destroyOnClose
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="name" label="场站名称" rules={[{ required: true }]}>
<Input placeholder="请输入场站名称" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="code" label="场站编号" rules={[{ required: true }]}>
<Input placeholder="唯一编号" />
</Form.Item>
</Col>
</Row>
<Form.Item name="address" label="场站地址">
<Input placeholder="详细地址" />
</Form.Item>
<Divider orientation="left">联系人与运维单位</Divider>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="contact" label="场站联系人">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="phone" label="联系电话">
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item name="maintainCompany" label="运维单位">
<Input placeholder="负责运维的公司" />
</Form.Item>
<Divider />
<Form.Item name="status" label="是否启用" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="停用" />
</Form.Item>
</Form>
</Modal>
{/* ====================== 2. 场站分区设置 ====================== */}
<Modal
title={`【${currentStation?.name}】分区设置`}
open={areaVisible}
width={600}
onCancel={() => setAreaVisible(false)}
footer={null}
>
<div className="mb-2">
<Button size="small" type="primary">新增分区</Button>
</div>
<Table
size="small"
rowKey="id"
pagination={false}
columns={[
{ title: '分区名称', dataIndex: 'name' },
{ title: '类型', dataIndex: 'type' },
{
title: '状态', render: (_, r) => r.status === 0 ? '正常' : '禁用'
},
{
title: '操作', render: () => (
<Space size="small">
<Button size="small" type="link">编辑</Button>
<Button size="small" type="link" danger>删除</Button>
</Space>
)
}
]}
dataSource={areaData.filter(a => a.stationId === currentStation?.id)}
/>
</Modal>
{/* ====================== 3. 场站边界绘制 & 地图维护 ====================== */}
<Modal
title={`【${currentStation?.name}】边界绘制 & 地图维护`}
open={mapVisible}
width={800}
onCancel={() => setMapVisible(false)}
footer={null}
>
<div style={{
height: 400,
background: '#f5f5f5',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 16
}}>
{/*<MapOutlined style={{ fontSize: 48, color: '#1677ff' }} />*/}
<div>地图组件 / 百度地图 / 高德地图 区域绘制</div>
<div style={{ color: '#888' }}>支持:边界点绘制、电子围栏、导入KML、保存GeoJSON</div>
<Space>
<Button type="primary">开始绘制</Button>
<Button>清除边界</Button>
<Button>保存地图数据</Button>
</Space>
</div>
</Modal>
</div>
);
}

View File

@@ -1,97 +1,288 @@
import React from 'react';
import {
Table,
Tag,
Space,
Button,
Input,
Typography,
Card,
Avatar
import React, { useState } from 'react';
import {
Card, Table, Button, Typography, Space, Modal, Form,
Input, Select, Popconfirm, message, Tag, TreeSelect, Divider
} from 'antd';
import {
PlusOutlined,
SearchOutlined,
EditOutlined,
DeleteOutlined,
UserOutlined
import {
PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined,
KeyOutlined, TeamOutlined
} from '@ant-design/icons';
import { MANAGED_USERS } from '../constants';
const { Text, Title } = Typography;
const { Title } = Typography;
const { Option } = Select;
export default function Users() {
const columns = [
{
title: '用户信息',
key: 'userinfo',
render: (record: any) => (
<Space size="middle">
<Avatar src={record.avatar} icon={<UserOutlined />} />
<Space direction="vertical" size={0}>
<Text strong>{record.name}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{record.id}</Text>
</Space>
</Space>
),
},
{
title: '系统权限',
dataIndex: 'role',
key: 'role',
render: (role: string) => (
<Tag color={role === 'admin' ? 'blue' : 'gray'} style={{ borderRadius: 2, fontWeight: 'bold', textTransform: 'uppercase' }}>
{role}
</Tag>
),
},
{
title: '账户状态',
key: 'status',
render: () => <Tag color="success">ACTIVE</Tag>,
},
{
title: '最近登录',
key: 'lastLogin',
render: () => <Text type="secondary" style={{ fontSize: 12, fontFamily: 'monospace' }}>2024-03-24 10:22:45</Text>,
},
{
title: '操作',
key: 'action',
align: 'right' as const,
render: () => (
<Space>
<Button icon={<EditOutlined />} size="small" type="link">编辑</Button>
<Button icon={<DeleteOutlined />} size="small" type="link" danger>删除</Button>
</Space>
),
},
];
// ---------------------- 1. 角色配置(和你的权限表对齐) ----------------------
const roleOptions = [
{
label: '平台admin',
value: 'admin',
desc: '全局配置、机构/菜单/设备权限分配、核心参数配置、平台级异常审核'
},
{
label: '区域机构管理员',
value: 'region_admin',
desc: '区域场站/账号管理、跨站机器人调度、区域报表统计'
},
{
label: '单个场站管理员',
value: 'site_admin',
desc: '场站配置、设备/机器人管理、场站告警处置、运维记录审核'
},
{
label: '场站员工',
value: 'site_staff',
desc: '机器人启停/参数调整、任务执行、巡检文件上传、故障处置'
},
];
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Title level={5} style={{ margin: 0 }}>用户管理权限控制</Title>
<Space>
<Input
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,0.25)' }} />}
placeholder="搜索用户名或工号..."
style={{ width: 250, borderRadius: 2 }}
/>
<Button type="primary" icon={<PlusOutlined />} style={{ borderRadius: 2 }}>
新增人员
</Button>
</Space>
</div>
// 模拟机构/场站数据(数据权限用)
const orgTreeData = [
{
title: '华东区域',
value: 'region_east',
children: [
{ title: '上海光伏场站', value: 'site_shanghai' },
{ title: '苏州风电场站', value: 'site_suzhou' },
]
},
{
title: '华北区域',
value: 'region_north',
children: [
{ title: '张家口光伏场站', value: 'site_zhangjiakou' },
]
}
];
<Card styles={{ body: { padding: 0 } }} className="overflow-hidden">
<Table
size="small"
columns={columns}
dataSource={MANAGED_USERS.map(u => ({ ...u, key: u.id }))}
pagination={false}
/>
</Card>
</div>
);
// 模拟用户数据(带角色+数据权限)
const initUserList = [
{
id: 1,
username: 'admin',
nickName: '平台超级管理员',
role: 'admin',
roleLabel: '平台admin',
dataScope: ['region_east', 'region_north'],
status: '0'
},
{
id: 2,
username: 'region_shanghai',
nickName: '华东区域管理员',
role: 'region_admin',
roleLabel: '区域机构管理员',
dataScope: ['region_east'],
status: '0'
},
{
id: 3,
username: 'site_shanghai01',
nickName: '上海场站管理员',
role: 'site_admin',
roleLabel: '单个场站管理员',
dataScope: ['site_shanghai'],
status: '0'
},
{
id: 4,
username: 'staff_zhang',
nickName: '上海场站运维员',
role: 'site_staff',
roleLabel: '场站员工',
dataScope: ['site_shanghai'],
status: '0'
},
];
export default function UserList() {
const [userList, setUserList] = useState(initUserList);
const [loading, setLoading] = useState(false);
// 新增/编辑弹窗
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
const [currentUser, setCurrentUser] = useState(null);
// 打开新增
const handleAdd = () => {
form.resetFields();
setCurrentUser(null);
setModalVisible(true);
};
// 打开编辑
const handleEdit = (record) => {
form.setFieldsValue(record);
setCurrentUser(record);
setModalVisible(true);
};
// 删除用户
const handleDelete = (id) => {
setUserList(userList.filter(u => u.id !== id));
message.success('删除成功');
};
// 提交保存
const handleSubmit = () => {
form.validateFields().then(values => {
// 角色标签回填,方便表格展示
const roleInfo = roleOptions.find(r => r.value === values.role);
const payload = {
...values,
roleLabel: roleInfo?.label
};
if (currentUser) {
// 编辑
setUserList(userList.map(u => u.id === currentUser.id ? { ...u, ...payload } : u));
message.success('编辑成功');
} else {
// 新增
const newUser = { ...payload, id: Date.now() };
setUserList([...userList, newUser]);
message.success('新增用户成功');
}
setModalVisible(false);
});
};
// 表格列(增强角色+数据权限展示)
const columns = [
{
title: '用户信息',
key: 'userInfo',
render: (_, record) => (
<Space>
<UserOutlined style={{ fontSize: 16 }} />
<div>
<div style={{ fontWeight: 500 }}>{record.nickName}</div>
<div style={{ fontSize: 12, color: '#888' }}>账号:{record.username}</div>
</div>
</Space>
)
},
{
title: '角色',
dataIndex: 'roleLabel',
key: 'roleLabel',
render: (text) => <Tag color="blue">{text}</Tag>
},
{
title: '数据权限',
dataIndex: 'dataScope',
key: 'dataScope',
render: (dataScope) => {
if (!dataScope?.length) return '-';
const count = dataScope.length;
return <Tag color="cyan">已分配 {count} 个机构/场站</Tag>;
}
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (s) => s === '0' ?
<Tag color="green">正常</Tag> :
<Tag color="red">禁用</Tag>
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Popconfirm title="确定删除该用户?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
</Space>
)
}
];
return (
<Card>
<div className="flex justify-between mb-4">
<Title level={5} style={{ margin: 0 }}>用户管理(角色+数据权限)</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增用户</Button>
</div>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={userList}
pagination={{ pageSize: 10 }}
/>
{/* 新增/编辑弹窗:角色+数据权限双配置 */}
<Modal
title={currentUser ? '编辑用户' : '新增用户'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSubmit}
maskClosable={false}
width={600}
>
<Form form={form} layout="vertical">
<Form.Item
name="username"
label="登录账号"
rules={[{ required: true, message: '请输入账号' }]}
>
<Input placeholder="请输入登录账号" />
</Form.Item>
<Form.Item
name="nickName"
label="用户昵称"
rules={[{ required: true, message: '请输入昵称' }]}
>
<Input placeholder="请输入用户昵称" />
</Form.Item>
<Divider orientation="left" style={{ margin: '16px 0' }}>权限配置</Divider>
<Form.Item
name="role"
label="分配角色"
rules={[{ required: true, message: '请选择角色' }]}
extra="角色决定菜单和操作权限,数据权限决定可见的机构/场站"
>
<Select placeholder="选择用户角色">
{roleOptions.map(item => (
<Option key={item.value} value={item.value}>{item.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="dataScope"
label="数据权限(可见机构/场站)"
rules={[{ required: true, message: '请选择数据权限' }]}
>
<TreeSelect
treeData={orgTreeData}
placeholder="选择用户可见的机构/场站"
multiple
treeCheckable
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item
name="status"
label="用户状态"
rules={[{ required: true }]}
initialValue="0"
>
<Select>
<Option value="0">正常</Option>
<Option value="1">禁用</Option>
</Select>
</Form.Item>
</Form>
</Modal>
</Card>
);
}

View File

@@ -12,6 +12,8 @@ import Roles from '../pages/Roles';
import Users from '../pages/Users';
import SerialNumGenerate from '../pages/SerialNumGenerate';
import MenuList from '../pages/MenuList';
import Organization from '../pages/Organization';
import StationManage from '../pages/StationManage';
export const router = createBrowserRouter([
{
@@ -61,6 +63,18 @@ export const router = createBrowserRouter([
path: 'menuList',
element: <MenuList />,
},
{
path: 'menuList',
element: <MenuList />,
},
{
path: 'organization',
element: <Organization />,
},
{
path: 'station',
element: <StationManage />,
},
{
path: '*',
element: <div className="p-10 text-center font-bold text-gray-400">模块开发中...</div>,