hikvision-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +12 -0
- package/package.json +27 -0
- package/src/App.css +48 -0
- package/src/App.css.new +48 -0
- package/src/App.tsx +157 -0
- package/src/App.tsx.new +132 -0
- package/src/index.css +39 -0
- package/src/main.tsx +14 -0
- package/src/pages/BindingPage.tsx +351 -0
- package/src/pages/CardForm.tsx +173 -0
- package/src/pages/CardList.tsx +193 -0
- package/src/pages/DashboardPage.tsx +300 -0
- package/src/pages/GroupPage.tsx +336 -0
- package/src/pages/OrgPage.tsx +198 -0
- package/src/pages/OrgTreePage.tsx +203 -0
- package/src/pages/PersonDetail.tsx +146 -0
- package/src/pages/PersonForm.tsx +199 -0
- package/src/pages/PersonList.tsx +174 -0
- package/src/pages/PersonTreePage.tsx +563 -0
- package/src/pages/SyncPage.tsx +29 -0
- package/src/pages/SystemPage.tsx +758 -0
- package/src/pages/VehicleForm.tsx +231 -0
- package/src/pages/VehicleList.tsx +199 -0
- package/src/services/api.ts +128 -0
- package/src/store/appStore.ts +159 -0
- package/src/utils/constants.ts +105 -0
- package/tsconfig.json +21 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +16 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 组织架构 - 树形多级显示
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { Tree, Button, Space, Modal, message, Input, Form, Spin } from 'antd';
|
|
7
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
8
|
+
import { orgApi } from '../services/api';
|
|
9
|
+
import type { DataNode } from 'antd/es/tree';
|
|
10
|
+
|
|
11
|
+
interface Organization {
|
|
12
|
+
orgIndexCode: string;
|
|
13
|
+
orgName: string;
|
|
14
|
+
parentOrgIndexCode?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function OrgTreePage() {
|
|
18
|
+
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
|
21
|
+
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
22
|
+
const [modalType, setModalType] = useState<'add' | 'edit'>('add');
|
|
23
|
+
const [editingOrg, setEditingOrg] = useState<Organization | null>(null);
|
|
24
|
+
const [form] = Form.useForm();
|
|
25
|
+
const [orgList, setOrgList] = useState<Organization[]>([]);
|
|
26
|
+
const [selectedParent, setSelectedParent] = useState<string>('root00000000');
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
loadOrgs();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const loadOrgs = async () => {
|
|
33
|
+
setLoading(true);
|
|
34
|
+
try {
|
|
35
|
+
const res: any = await orgApi.list();
|
|
36
|
+
const list: Organization[] = res?.data || res || [];
|
|
37
|
+
setOrgList(list);
|
|
38
|
+
|
|
39
|
+
// 构建树形数据
|
|
40
|
+
const map = new Map<string, DataNode>();
|
|
41
|
+
list.forEach((org) => {
|
|
42
|
+
map.set(org.orgIndexCode, {
|
|
43
|
+
title: org.orgName,
|
|
44
|
+
key: org.orgIndexCode,
|
|
45
|
+
children: [],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const roots: DataNode[] = [];
|
|
50
|
+
list.forEach((org) => {
|
|
51
|
+
const node = map.get(org.orgIndexCode)!;
|
|
52
|
+
if (org.parentOrgIndexCode && org.parentOrgIndexCode !== '-1' && map.has(org.parentOrgIndexCode)) {
|
|
53
|
+
const parent = map.get(org.parentOrgIndexCode)!;
|
|
54
|
+
parent.children = parent.children || [];
|
|
55
|
+
parent.children.push(node);
|
|
56
|
+
} else {
|
|
57
|
+
roots.push(node);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
setTreeData(roots);
|
|
62
|
+
setExpandedKeys(roots.map((r) => r.key as string));
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
message.error(`加载组织失败: ${error.message || error}`);
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleAdd = () => {
|
|
71
|
+
setModalType('add');
|
|
72
|
+
setEditingOrg(null);
|
|
73
|
+
setSelectedParent('root00000000');
|
|
74
|
+
form.resetFields();
|
|
75
|
+
setIsModalVisible(true);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleEdit = (org: Organization) => {
|
|
79
|
+
setModalType('edit');
|
|
80
|
+
setEditingOrg(org);
|
|
81
|
+
form.setFieldsValue({ orgName: org.orgName });
|
|
82
|
+
setIsModalVisible(true);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleDelete = async (org: Organization) => {
|
|
86
|
+
Modal.confirm({
|
|
87
|
+
title: '确认删除',
|
|
88
|
+
content: `确定要删除组织"${org.orgName}"吗?删除后无法恢复。`,
|
|
89
|
+
okText: '确认删除',
|
|
90
|
+
okType: 'danger',
|
|
91
|
+
cancelText: '取消',
|
|
92
|
+
onOk: async () => {
|
|
93
|
+
try {
|
|
94
|
+
await orgApi.delete([org.orgIndexCode]);
|
|
95
|
+
message.success('删除成功');
|
|
96
|
+
loadOrgs();
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
message.error(`删除失败: ${error.message || error}`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleSubmit = async () => {
|
|
105
|
+
try {
|
|
106
|
+
const values = await form.validateFields();
|
|
107
|
+
if (modalType === 'add') {
|
|
108
|
+
await orgApi.create([{
|
|
109
|
+
orgName: values.orgName,
|
|
110
|
+
parentIndexCode: selectedParent
|
|
111
|
+
}]);
|
|
112
|
+
message.success('添加成功');
|
|
113
|
+
} else if (editingOrg) {
|
|
114
|
+
await orgApi.update(editingOrg.orgIndexCode, { orgName: values.orgName });
|
|
115
|
+
message.success('更新成功');
|
|
116
|
+
}
|
|
117
|
+
setIsModalVisible(false);
|
|
118
|
+
loadOrgs();
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
if (error.errorFields) return;
|
|
121
|
+
message.error(`${modalType === 'add' ? '添加' : '更新'}失败: ${error.message || error}`);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const renderTreeActions = (org: Organization): React.ReactNode => (
|
|
126
|
+
<Space size="small">
|
|
127
|
+
<Button size="small" type="text" icon={<PlusOutlined />} onClick={() => {
|
|
128
|
+
setModalType('add');
|
|
129
|
+
setEditingOrg(null);
|
|
130
|
+
setSelectedParent(org.orgIndexCode);
|
|
131
|
+
form.resetFields();
|
|
132
|
+
setIsModalVisible(true);
|
|
133
|
+
}}>
|
|
134
|
+
添加
|
|
135
|
+
</Button>
|
|
136
|
+
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEdit(org)}>
|
|
137
|
+
编辑
|
|
138
|
+
</Button>
|
|
139
|
+
<Button size="small" type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(org)}>
|
|
140
|
+
删除
|
|
141
|
+
</Button>
|
|
142
|
+
</Space>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div style={{ padding: 24 }}>
|
|
147
|
+
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
|
148
|
+
<h2>组织架构(树形)</h2>
|
|
149
|
+
<Space>
|
|
150
|
+
<Button icon={<ReloadOutlined />} onClick={loadOrgs}>刷新</Button>
|
|
151
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加组织</Button>
|
|
152
|
+
</Space>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{loading ? (
|
|
156
|
+
<div style={{ textAlign: 'center', padding: 40 }}><Spin size="large" /></div>
|
|
157
|
+
) : (
|
|
158
|
+
<Tree
|
|
159
|
+
showLine
|
|
160
|
+
defaultExpandAll
|
|
161
|
+
expandedKeys={expandedKeys}
|
|
162
|
+
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
|
163
|
+
treeData={treeData}
|
|
164
|
+
titleRender={(node) => (
|
|
165
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: 12 }}>
|
|
166
|
+
<span>{node.title as string}</span>
|
|
167
|
+
<span style={{ opacity: 0.6 }}>{renderTreeActions(orgList.find((o) => o.orgIndexCode === node.key) || { orgIndexCode: node.key as string, orgName: node.title as string } as Organization)}</span>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
/>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<Modal
|
|
174
|
+
title={modalType === 'add' ? '添加组织' : '编辑组织'}
|
|
175
|
+
open={isModalVisible}
|
|
176
|
+
onOk={handleSubmit}
|
|
177
|
+
onCancel={() => setIsModalVisible(false)}
|
|
178
|
+
okText={modalType === 'add' ? '添加' : '保存'}
|
|
179
|
+
cancelText="取消"
|
|
180
|
+
>
|
|
181
|
+
<Form form={form} layout="vertical">
|
|
182
|
+
<Form.Item
|
|
183
|
+
name="orgName"
|
|
184
|
+
label="组织名称"
|
|
185
|
+
rules={[{ required: true, message: '请输入组织名称' }]}
|
|
186
|
+
>
|
|
187
|
+
<Input placeholder="请输入组织名称" />
|
|
188
|
+
</Form.Item>
|
|
189
|
+
{modalType === 'add' && (
|
|
190
|
+
<Form.Item label="父组织">
|
|
191
|
+
<Tree
|
|
192
|
+
showLine
|
|
193
|
+
treeData={treeData}
|
|
194
|
+
selectedKeys={[selectedParent]}
|
|
195
|
+
onSelect={(keys) => setSelectedParent(keys[0] as string)}
|
|
196
|
+
/>
|
|
197
|
+
</Form.Item>
|
|
198
|
+
)}
|
|
199
|
+
</Form>
|
|
200
|
+
</Modal>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Card, Descriptions, Button, message, Spin, Table, Tag } from 'antd';
|
|
4
|
+
import { EditOutlined, DeleteOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
5
|
+
import { personApi, bindingApi, vehicleGroupApi, vehicleApi } from '../services/api';
|
|
6
|
+
|
|
7
|
+
export default function PersonDetail() {
|
|
8
|
+
const { personId } = useParams();
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const [person, setPerson] = useState<any>(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [relatedCards, setRelatedCards] = useState<any[]>([]);
|
|
13
|
+
const [relatedVehicles, setRelatedVehicles] = useState<any[]>([]);
|
|
14
|
+
const [vehicleGroups, setVehicleGroups] = useState<any[]>([]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!personId) return;
|
|
18
|
+
loadPerson();
|
|
19
|
+
loadRelatedBindings(personId);
|
|
20
|
+
loadVehicleGroups();
|
|
21
|
+
}, [personId]);
|
|
22
|
+
|
|
23
|
+
const loadVehicleGroups = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const res: any = await vehicleGroupApi.list();
|
|
26
|
+
setVehicleGroups(res.data || []);
|
|
27
|
+
} catch (e) { /* ignore */ }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getGroupName = (groupId: string) => {
|
|
31
|
+
if (!groupId) return '—';
|
|
32
|
+
const g = vehicleGroups.find((g: any) => g.groupId === groupId);
|
|
33
|
+
return g?.groupName || groupId;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const loadPerson = async () => {
|
|
37
|
+
if (!personId) return;
|
|
38
|
+
setLoading(true);
|
|
39
|
+
try {
|
|
40
|
+
const data = await personApi.getById(personId);
|
|
41
|
+
setPerson(data.data);
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
message.error(`加载失败: ${error.message || error}`);
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const loadRelatedBindings = async (pid: string) => {
|
|
50
|
+
if (!pid) return;
|
|
51
|
+
try {
|
|
52
|
+
const res: any = await bindingApi.getPersonBindings(pid);
|
|
53
|
+
if (res.success !== false) {
|
|
54
|
+
setRelatedCards(res.data?.cards || []);
|
|
55
|
+
|
|
56
|
+
// Fetch vehicle details to get groupId/groupName for each vehicle
|
|
57
|
+
const vehicles = res.data?.vehicles || [];
|
|
58
|
+
const vehiclesWithGroup = await Promise.all(
|
|
59
|
+
vehicles.map(async (v: any) => {
|
|
60
|
+
try {
|
|
61
|
+
const detail: any = await vehicleApi.getById(v.vehicleId);
|
|
62
|
+
const full = detail.data || {};
|
|
63
|
+
return {
|
|
64
|
+
...v,
|
|
65
|
+
// 保留原始 personId,不被 getById 返回值覆盖
|
|
66
|
+
personId: v.personId,
|
|
67
|
+
groupId: full.vehicleGroup || full.groupId || v.groupId || '',
|
|
68
|
+
groupName: full.groupName || '',
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return { ...v, personId: v.personId, groupId: v.groupId || '', groupName: '' };
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
setRelatedVehicles(vehiclesWithGroup);
|
|
76
|
+
}
|
|
77
|
+
} catch (e) { /* ignore */ }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleDelete = async () => {
|
|
81
|
+
if (!personId || !person) return;
|
|
82
|
+
if (!confirm(`确定要删除人员 ${person.personName} 吗?`)) return;
|
|
83
|
+
try {
|
|
84
|
+
await personApi.delete(personId);
|
|
85
|
+
message.success('删除成功');
|
|
86
|
+
navigate('/person');
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
message.error(`删除失败: ${error.message || error}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (loading) {
|
|
93
|
+
return <div style={{ padding: 40, textAlign: 'center' }}><Spin size="large" /></div>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!person) {
|
|
97
|
+
return <div style={{ padding: 40, textAlign: 'center' }}>人员不存在</div>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const genderMap: Record<string, string> = { '1': '男', '2': '女' };
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div>
|
|
104
|
+
<div style={{ marginBottom: 16 }}>
|
|
105
|
+
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/person')}>返回</Button>
|
|
106
|
+
<Button icon={<EditOutlined />} style={{ marginLeft: 8 }} onClick={() => navigate(`/person/edit/${personId}`)}>编辑</Button>
|
|
107
|
+
<Button icon={<DeleteOutlined />} danger style={{ marginLeft: 8 }} onClick={handleDelete}>删除</Button>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<Card title="基本信息" style={{ marginBottom: 16 }}>
|
|
111
|
+
<Descriptions bordered column={2}>
|
|
112
|
+
<Descriptions.Item label="姓名">{person.personName}</Descriptions.Item>
|
|
113
|
+
<Descriptions.Item label="工号">{person.jobNo || '—'}</Descriptions.Item>
|
|
114
|
+
<Descriptions.Item label="性别">{genderMap[person.gender] || '—'}</Descriptions.Item>
|
|
115
|
+
<Descriptions.Item label="所属组织">{person.orgName || '—'}</Descriptions.Item>
|
|
116
|
+
<Descriptions.Item label="手机号">{person.phoneNo || '—'}</Descriptions.Item>
|
|
117
|
+
<Descriptions.Item label="证件号">{person.certificateNo || '—'}</Descriptions.Item>
|
|
118
|
+
<Descriptions.Item label="邮箱">{person.email || '—'}</Descriptions.Item>
|
|
119
|
+
<Descriptions.Item label="更新时间">{person.updateTime ? new Date(person.updateTime).toLocaleString('zh-CN') : '—'}</Descriptions.Item>
|
|
120
|
+
</Descriptions>
|
|
121
|
+
</Card>
|
|
122
|
+
|
|
123
|
+
<Card title="绑定信息" extra={<Button icon={<ReloadOutlined />} size="small" onClick={() => personId && loadRelatedBindings(personId)}>刷新</Button>}>
|
|
124
|
+
<div style={{ marginBottom: 24 }}>
|
|
125
|
+
<h4 style={{ marginBottom: 12 }}>绑定卡片({relatedCards.length}张)</h4>
|
|
126
|
+
<Table size="small" dataSource={relatedCards} rowKey="cardNo" pagination={false} style={{ background: '#fff' }}>
|
|
127
|
+
<Table.Column title="卡号" dataIndex="cardNo" />
|
|
128
|
+
<Table.Column title="卡片类型" dataIndex="cardType" render={(t: number) => ({ 1: '普通卡', 2: '来宾卡', 3: '巡逻卡', 4: '管理员卡' }[t] || t)} />
|
|
129
|
+
<Table.Column title="状态" dataIndex="useStatus" render={(t: number) => <Tag color={t === 1 ? 'green' : 'red'}>{t === 1 ? '正常' : '禁用'}</Tag>} />
|
|
130
|
+
</Table>
|
|
131
|
+
{relatedCards.length === 0 && <div style={{ color: '#999', textAlign: 'center', padding: 16 }}>暂未绑定卡片</div>}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
<h4 style={{ marginBottom: 12 }}>绑定车辆({relatedVehicles.length}辆)</h4>
|
|
136
|
+
<Table size="small" dataSource={relatedVehicles} rowKey="vehicleId" pagination={false} style={{ background: '#fff' }}>
|
|
137
|
+
<Table.Column title="车牌号" dataIndex="plateNo" />
|
|
138
|
+
<Table.Column title="车辆类型" dataIndex="vehicleType" render={(t: number) => t === 1 ? '小型车' : t === 2 ? '大型车' : t} />
|
|
139
|
+
<Table.Column title="车辆群组" dataIndex="groupId" render={(g: string) => getGroupName(g)} />
|
|
140
|
+
</Table>
|
|
141
|
+
{relatedVehicles.length === 0 && <div style={{ color: '#999', textAlign: 'center', padding: 16 }}>暂未绑定车辆</div>}
|
|
142
|
+
</div>
|
|
143
|
+
</Card>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { Form, Input, Select, TreeSelect, Button, message, Card, Spin, Space } from 'antd';
|
|
4
|
+
const { Option } = Select;
|
|
5
|
+
import { ArrowLeftOutlined, SaveOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
6
|
+
import { personApi } from '../services/api';
|
|
7
|
+
import { useAppStore } from '../store/appStore';
|
|
8
|
+
|
|
9
|
+
export default function PersonForm() {
|
|
10
|
+
const { personId } = useParams();
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
const [searchParams] = useSearchParams();
|
|
13
|
+
const defaultOrgCode = searchParams.get('orgCode');
|
|
14
|
+
const isEdit = !!personId;
|
|
15
|
+
const [form] = Form.useForm();
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [submitting, setSubmitting] = useState(false);
|
|
18
|
+
const { organizations, loadOrganizations, refreshPerson } = useAppStore();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
loadOrganizations();
|
|
22
|
+
if (isEdit && personId) {
|
|
23
|
+
loadPerson();
|
|
24
|
+
}
|
|
25
|
+
}, [personId]);
|
|
26
|
+
|
|
27
|
+
const loadPerson = async () => {
|
|
28
|
+
if (!personId) return;
|
|
29
|
+
setLoading(true);
|
|
30
|
+
try {
|
|
31
|
+
const data = await personApi.getById(personId);
|
|
32
|
+
form.setFieldsValue({
|
|
33
|
+
personName: data.data.personName,
|
|
34
|
+
gender: data.data.gender,
|
|
35
|
+
jobNo: data.data.jobNo,
|
|
36
|
+
orgIndexCode: data.data.orgIndexCode,
|
|
37
|
+
phoneNo: data.data.phoneNo,
|
|
38
|
+
email: data.data.email,
|
|
39
|
+
certificateNo: data.data.certificateNo,
|
|
40
|
+
personEnableStartTime: data.data.personEnableStartTime,
|
|
41
|
+
personEnableEndTime: data.data.personEnableEndTime,
|
|
42
|
+
});
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
message.error(`加载失败: ${error.message || error}`);
|
|
45
|
+
} finally {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 将组织列表转换为 TreeSelect 格式
|
|
51
|
+
const treeData = useMemo(() => {
|
|
52
|
+
if (!organizations.length) return [];
|
|
53
|
+
|
|
54
|
+
// Build a map of indexCode -> node
|
|
55
|
+
const nodeMap = new Map<string, any>();
|
|
56
|
+
organizations.forEach(org => {
|
|
57
|
+
nodeMap.set(org.orgIndexCode, {
|
|
58
|
+
label: org.orgName,
|
|
59
|
+
value: org.orgIndexCode,
|
|
60
|
+
children: [],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Build tree by linking children to parents
|
|
65
|
+
const roots: any[] = [];
|
|
66
|
+
organizations.forEach(org => {
|
|
67
|
+
const node = nodeMap.get(org.orgIndexCode);
|
|
68
|
+
if (!node) return;
|
|
69
|
+
|
|
70
|
+
const parentCode = org.parentOrgIndexCode || '-1';
|
|
71
|
+
if (parentCode === '-1' || parentCode === 'root00000000') {
|
|
72
|
+
roots.push(node);
|
|
73
|
+
} else {
|
|
74
|
+
const parent = nodeMap.get(parentCode);
|
|
75
|
+
if (parent) {
|
|
76
|
+
parent.children.push(node);
|
|
77
|
+
} else {
|
|
78
|
+
roots.push(node);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return roots;
|
|
84
|
+
}, [organizations]);
|
|
85
|
+
|
|
86
|
+
const handleSubmit = async (values: any) => {
|
|
87
|
+
setSubmitting(true);
|
|
88
|
+
try {
|
|
89
|
+
const dto = {
|
|
90
|
+
personName: values.personName,
|
|
91
|
+
gender: values.gender as '1' | '2',
|
|
92
|
+
jobNo: values.jobNo,
|
|
93
|
+
orgIndexCode: values.orgIndexCode,
|
|
94
|
+
phoneNo: values.phoneNo,
|
|
95
|
+
email: values.email,
|
|
96
|
+
certificateNo: values.certificateNo,
|
|
97
|
+
personEnableStartTime: values.personEnableStartTime,
|
|
98
|
+
personEnableEndTime: values.personEnableEndTime,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (isEdit && personId) {
|
|
102
|
+
await personApi.update(personId, dto);
|
|
103
|
+
await refreshPerson(personId);
|
|
104
|
+
message.success('更新成功');
|
|
105
|
+
} else {
|
|
106
|
+
await personApi.create(dto);
|
|
107
|
+
message.success('添加成功');
|
|
108
|
+
}
|
|
109
|
+
// 返回树状视图,并保留之前的组织选择
|
|
110
|
+
const backUrl = defaultOrgCode ? `/person-tree?orgCode=${defaultOrgCode}` : '/person-tree';
|
|
111
|
+
navigate(backUrl);
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
message.error(`保存失败: ${error.message || error}`);
|
|
114
|
+
} finally {
|
|
115
|
+
setSubmitting(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (loading) {
|
|
120
|
+
return <div style={{ padding: 40, textAlign: 'center' }}><Spin size="large" /></div>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
126
|
+
<Button icon={<ArrowLeftOutlined />} onClick={() => {
|
|
127
|
+
const backUrl = defaultOrgCode ? `/person-tree?orgCode=${defaultOrgCode}` : '/person-tree';
|
|
128
|
+
navigate(backUrl);
|
|
129
|
+
}}>返回</Button>
|
|
130
|
+
<h2>{isEdit ? '编辑人员' : '添加人员'}</h2>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<Card>
|
|
134
|
+
<Form
|
|
135
|
+
form={form}
|
|
136
|
+
layout="vertical"
|
|
137
|
+
onFinish={handleSubmit}
|
|
138
|
+
initialValues={{ gender: '1', orgIndexCode: defaultOrgCode || undefined }}
|
|
139
|
+
>
|
|
140
|
+
<Form.Item name="personName" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
|
141
|
+
<Input placeholder="请输入姓名" />
|
|
142
|
+
</Form.Item>
|
|
143
|
+
|
|
144
|
+
<Form.Item name="gender" label="性别" rules={[{ required: true, message: '请选择性别' }]}>
|
|
145
|
+
<Select placeholder="请选择性别">
|
|
146
|
+
<Option value="1">男</Option>
|
|
147
|
+
<Option value="2">女</Option>
|
|
148
|
+
</Select>
|
|
149
|
+
</Form.Item>
|
|
150
|
+
|
|
151
|
+
<Form.Item name="jobNo" label="工号">
|
|
152
|
+
<Input placeholder="请输入工号" />
|
|
153
|
+
</Form.Item>
|
|
154
|
+
|
|
155
|
+
<Form.Item
|
|
156
|
+
name="orgIndexCode"
|
|
157
|
+
label="所属组织"
|
|
158
|
+
rules={[{ required: true, message: '请选择组织' }]}
|
|
159
|
+
>
|
|
160
|
+
<TreeSelect
|
|
161
|
+
placeholder="请选择组织"
|
|
162
|
+
showSearch
|
|
163
|
+
allowClear
|
|
164
|
+
treeDefaultExpandAll
|
|
165
|
+
treeData={treeData}
|
|
166
|
+
treeNodeFilterProp="label"
|
|
167
|
+
style={{ width: '100%' }}
|
|
168
|
+
/>
|
|
169
|
+
</Form.Item>
|
|
170
|
+
|
|
171
|
+
<Form.Item name="phoneNo" label="手机号">
|
|
172
|
+
<Input placeholder="请输入手机号" />
|
|
173
|
+
</Form.Item>
|
|
174
|
+
|
|
175
|
+
<Form.Item name="email" label="邮箱">
|
|
176
|
+
<Input placeholder="请输入邮箱" />
|
|
177
|
+
</Form.Item>
|
|
178
|
+
|
|
179
|
+
<Form.Item name="certificateNo" label="证件号">
|
|
180
|
+
<Input placeholder="请输入证件号" />
|
|
181
|
+
</Form.Item>
|
|
182
|
+
|
|
183
|
+
<Form.Item style={{ marginTop: 24 }}>
|
|
184
|
+
<Space>
|
|
185
|
+
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={submitting}>
|
|
186
|
+
{isEdit ? '保存修改' : '添加人员'}
|
|
187
|
+
</Button>
|
|
188
|
+
<Button icon={<ReloadOutlined />} onClick={() => form.resetFields()}>重置</Button>
|
|
189
|
+
<Button onClick={() => {
|
|
190
|
+
const backUrl = defaultOrgCode ? `/person-tree?orgCode=${defaultOrgCode}` : '/person-tree';
|
|
191
|
+
navigate(backUrl);
|
|
192
|
+
}}>取消</Button>
|
|
193
|
+
</Space>
|
|
194
|
+
</Form.Item>
|
|
195
|
+
</Form>
|
|
196
|
+
</Card>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|