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,563 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { Table, Button, Input, Space, Modal, message, Tree, Spin, Card, Form, Popconfirm } from 'antd';
|
|
3
|
+
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, TeamOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
4
|
+
import { useAppStore } from '../store/appStore';
|
|
5
|
+
import { personApi, orgApi } from '../services/api';
|
|
6
|
+
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
7
|
+
import type { DataNode } from 'antd/es/tree';
|
|
8
|
+
|
|
9
|
+
const { Search } = Input;
|
|
10
|
+
|
|
11
|
+
interface OrgNode {
|
|
12
|
+
orgIndexCode: string;
|
|
13
|
+
orgName: string;
|
|
14
|
+
parentOrgIndexCode?: string;
|
|
15
|
+
children: OrgNode[];
|
|
16
|
+
personCount?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function PersonTreePage() {
|
|
20
|
+
const {
|
|
21
|
+
loadOrganizations,
|
|
22
|
+
organizations,
|
|
23
|
+
} = useAppStore();
|
|
24
|
+
|
|
25
|
+
const [searchName, setSearchName] = useState('');
|
|
26
|
+
const [page, setPage] = useState(1);
|
|
27
|
+
const [pageSize, setPageSize] = useState(20);
|
|
28
|
+
const [deleteModal, setDeleteModal] = useState<{ visible: boolean; personId?: string; personName?: string }>({ visible: false });
|
|
29
|
+
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
30
|
+
const [batchDeleteModal, setBatchDeleteModal] = useState<{ visible: boolean; count: number }>({ visible: false, count: 0 });
|
|
31
|
+
|
|
32
|
+
// 左侧组织树相关
|
|
33
|
+
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
|
34
|
+
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
|
35
|
+
const [selectedOrg, setSelectedOrg] = useState<string | null>(null);
|
|
36
|
+
const navigate = useNavigate();
|
|
37
|
+
const [selectedOrgName, setSelectedOrgName] = useState<string>('全部');
|
|
38
|
+
const [treeLoading, setTreeLoading] = useState(false);
|
|
39
|
+
|
|
40
|
+
// 按需加载:只保存当前选中组织的人员数据
|
|
41
|
+
const [currentPersons, setCurrentPersons] = useState<any[]>([]);
|
|
42
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
43
|
+
const [orgPersonCounts, setOrgPersonCounts] = useState<Record<string, number>>({});
|
|
44
|
+
|
|
45
|
+
// 使用 useRef 存储已加载的组织计数,避免重复请求
|
|
46
|
+
const countsLoadedRef = useRef<Set<string>>(new Set());
|
|
47
|
+
// 使用 ref 存储取消标志
|
|
48
|
+
const cancelRef = useRef(false);
|
|
49
|
+
|
|
50
|
+
// 组织管理相关
|
|
51
|
+
const [orgModalVisible, setOrgModalVisible] = useState(false);
|
|
52
|
+
const [orgModalType, setOrgModalType] = useState<'add' | 'edit'>('add');
|
|
53
|
+
const [editingOrg, setEditingOrg] = useState<any>(null);
|
|
54
|
+
const [orgForm] = Form.useForm();
|
|
55
|
+
const [selectedParentOrg, setSelectedParentOrg] = useState<string>('root00000000');
|
|
56
|
+
const [searchParams] = useSearchParams();
|
|
57
|
+
const initialOrgCode = searchParams.get('orgCode');
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
loadOrganizations();
|
|
61
|
+
}, [loadOrganizations]);
|
|
62
|
+
|
|
63
|
+
// 初始化时恢复之前的组织选择
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (organizations.length > 0 && initialOrgCode) {
|
|
66
|
+
const org = organizations.find((o: any) => o.orgIndexCode === initialOrgCode);
|
|
67
|
+
if (org) {
|
|
68
|
+
setSelectedOrg(initialOrgCode);
|
|
69
|
+
setSelectedOrgName(org.orgName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [organizations, initialOrgCode]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 按需加载指定组织的人员数量(用于统计)
|
|
76
|
+
* 只在首次访问时加载,后续使用缓存
|
|
77
|
+
*/
|
|
78
|
+
const loadOrgPersonCount = useCallback(async (orgCode: string) => {
|
|
79
|
+
if (countsLoadedRef.current.has(orgCode)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const res: any = await personApi.list({ pageNo: 1, pageSize: 1, orgIndexCode: orgCode });
|
|
85
|
+
const total = res?.data?.total || res?.total || 0;
|
|
86
|
+
setOrgPersonCounts(prev => ({ ...prev, [orgCode]: total }));
|
|
87
|
+
countsLoadedRef.current.add(orgCode);
|
|
88
|
+
} catch {
|
|
89
|
+
setOrgPersonCounts(prev => ({ ...prev, [orgCode]: 0 }));
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 加载当前选中组织的人员列表(分页)
|
|
95
|
+
*/
|
|
96
|
+
const loadCurrentOrgPersons = useCallback(async (orgCode: string | null, pageNum: number, pageSz: number, name?: string) => {
|
|
97
|
+
setTreeLoading(true);
|
|
98
|
+
cancelRef.current = false;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const res: any = await personApi.list({
|
|
102
|
+
pageNo: pageNum,
|
|
103
|
+
pageSize: pageSz,
|
|
104
|
+
personName: name || undefined,
|
|
105
|
+
orgIndexCode: orgCode || undefined,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (cancelRef.current) return;
|
|
109
|
+
|
|
110
|
+
const list = res?.data?.list || res?.list || [];
|
|
111
|
+
const total = res?.data?.total || res?.total || 0;
|
|
112
|
+
setCurrentPersons(list);
|
|
113
|
+
setTotalCount(total);
|
|
114
|
+
} catch (error: any) {
|
|
115
|
+
if (!cancelRef.current) {
|
|
116
|
+
message.error(`加载人员数据失败: ${error.message}`);
|
|
117
|
+
setCurrentPersons([]);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
if (!cancelRef.current) {
|
|
121
|
+
setTreeLoading(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
// 初始化时加载所有组织的人员数量
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (organizations.length > 0) {
|
|
129
|
+
// 加载所有组织的人员数量(用于树节点显示)
|
|
130
|
+
organizations.forEach((org: any) => {
|
|
131
|
+
if (!countsLoadedRef.current.has(org.orgIndexCode)) {
|
|
132
|
+
loadOrgPersonCount(org.orgIndexCode);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}, [organizations, loadOrgPersonCount]);
|
|
137
|
+
|
|
138
|
+
// 当选中组织变化时,加载该组织的人员列表
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
// 延迟执行,避免与树节点统计冲突
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
loadCurrentOrgPersons(selectedOrg, page, pageSize, searchName);
|
|
143
|
+
}, 100);
|
|
144
|
+
return () => clearTimeout(timer);
|
|
145
|
+
}, [selectedOrg, page, pageSize, searchName, loadCurrentOrgPersons]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 构建组织树(添加人员统计)
|
|
149
|
+
*/
|
|
150
|
+
const buildTreeData = useCallback(() => {
|
|
151
|
+
if (!organizations || organizations.length === 0) {
|
|
152
|
+
setTreeData([]);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const calcPersonCount = (node: OrgNode): number => {
|
|
157
|
+
const directCount = node.personCount || 0;
|
|
158
|
+
const childrenCount = node.children.reduce((sum, child) => sum + calcPersonCount(child), 0);
|
|
159
|
+
return directCount + childrenCount;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const convertToTreeData = (nodes: OrgNode[]): DataNode[] => {
|
|
163
|
+
return nodes.map((node) => {
|
|
164
|
+
// 计算该组织的人员数量(包含子组织)
|
|
165
|
+
const calcTotalCount = (n: OrgNode): number => {
|
|
166
|
+
const directCount = orgPersonCounts[n.orgIndexCode] || 0;
|
|
167
|
+
const childrenCount = n.children.reduce((sum, child) => sum + calcTotalCount(child), 0);
|
|
168
|
+
return directCount + childrenCount;
|
|
169
|
+
};
|
|
170
|
+
const totalCount = calcTotalCount(node);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
title: (
|
|
174
|
+
<span>
|
|
175
|
+
<TeamOutlined style={{ marginRight: 8, color: '#1890ff' }} />
|
|
176
|
+
{node.orgName}
|
|
177
|
+
<span style={{ color: '#999', marginLeft: 8, fontSize: 12 }}>
|
|
178
|
+
({totalCount} 人)
|
|
179
|
+
</span>
|
|
180
|
+
</span>
|
|
181
|
+
),
|
|
182
|
+
key: node.orgIndexCode,
|
|
183
|
+
children: node.children.length > 0 ? convertToTreeData(node.children) : undefined,
|
|
184
|
+
// isLeaf 表示是否为叶子节点(没有子组织),但允许展开加载子组织人员
|
|
185
|
+
isLeaf: node.children.length === 0,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// 构建组织节点
|
|
191
|
+
const orgMap = new Map<string, OrgNode>();
|
|
192
|
+
|
|
193
|
+
organizations.forEach((org: any) => {
|
|
194
|
+
orgMap.set(org.orgIndexCode, {
|
|
195
|
+
orgIndexCode: org.orgIndexCode,
|
|
196
|
+
orgName: org.orgName,
|
|
197
|
+
parentOrgIndexCode: org.parentOrgIndexCode,
|
|
198
|
+
children: [],
|
|
199
|
+
personCount: orgPersonCounts[org.orgIndexCode] || 0,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const roots: OrgNode[] = [];
|
|
204
|
+
orgMap.forEach((node) => {
|
|
205
|
+
if (node.parentOrgIndexCode && node.parentOrgIndexCode !== '-1' && orgMap.has(node.parentOrgIndexCode)) {
|
|
206
|
+
orgMap.get(node.parentOrgIndexCode)!.children.push(node);
|
|
207
|
+
} else {
|
|
208
|
+
roots.push(node);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (roots.length === 0) {
|
|
213
|
+
setTreeData([]);
|
|
214
|
+
} else {
|
|
215
|
+
setTreeData(convertToTreeData(roots));
|
|
216
|
+
// 不再这里设置 expandedKeys,避免选中节点时树收缩
|
|
217
|
+
}
|
|
218
|
+
}, [organizations, orgPersonCounts]);
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
buildTreeData();
|
|
222
|
+
}, [buildTreeData]);
|
|
223
|
+
|
|
224
|
+
// 初始化时设置展开的键(只执行一次)
|
|
225
|
+
const [initialExpandSet, setInitialExpandSet] = useState(false);
|
|
226
|
+
|
|
227
|
+
// 初始化时设置展开状态
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (treeData.length > 0 && !initialExpandSet) {
|
|
230
|
+
setExpandedKeys(treeData.map(n => n.key as string));
|
|
231
|
+
setInitialExpandSet(true);
|
|
232
|
+
}
|
|
233
|
+
}, [treeData, initialExpandSet]);
|
|
234
|
+
|
|
235
|
+
// 点击树节点
|
|
236
|
+
const handleTreeSelect = (selectedKeys: React.Key[]) => {
|
|
237
|
+
if (selectedKeys.length === 0) {
|
|
238
|
+
setSelectedOrg(null);
|
|
239
|
+
setSelectedOrgName('全部');
|
|
240
|
+
} else {
|
|
241
|
+
const orgCode = selectedKeys[0] as string;
|
|
242
|
+
setSelectedOrg(orgCode);
|
|
243
|
+
setPage(1); // 重置到第一页
|
|
244
|
+
const org = organizations.find((o: any) => o.orgIndexCode === orgCode);
|
|
245
|
+
setSelectedOrgName(org?.orgName || '全部');
|
|
246
|
+
}
|
|
247
|
+
// loadCurrentOrgPersons 会在 useEffect 中被调用
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleSearch = () => {
|
|
251
|
+
setPage(1); // 搜索时重置到第一页
|
|
252
|
+
loadCurrentOrgPersons(selectedOrg, 1, pageSize, searchName);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const handleDelete = async () => {
|
|
256
|
+
if (!deleteModal.personId) return;
|
|
257
|
+
try {
|
|
258
|
+
await personApi.delete(deleteModal.personId);
|
|
259
|
+
message.success('删除成功');
|
|
260
|
+
setDeleteModal({ visible: false });
|
|
261
|
+
// 刷新当前列表
|
|
262
|
+
loadCurrentOrgPersons(selectedOrg, page, pageSize, searchName);
|
|
263
|
+
} catch (error: any) {
|
|
264
|
+
message.error(`删除失败: ${error.message || error}`);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// 批量删除人员
|
|
269
|
+
const handleBatchDelete = async () => {
|
|
270
|
+
if (selectedRowKeys.length === 0) {
|
|
271
|
+
message.warning('请先选择要删除的人员');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
await personApi.batchDelete(selectedRowKeys as string[]);
|
|
276
|
+
message.success(`成功删除 ${selectedRowKeys.length} 名人员`);
|
|
277
|
+
setBatchDeleteModal({ visible: false, count: 0 });
|
|
278
|
+
setSelectedRowKeys([]);
|
|
279
|
+
// 刷新当前列表
|
|
280
|
+
loadCurrentOrgPersons(selectedOrg, page, pageSize, searchName);
|
|
281
|
+
} catch (error: any) {
|
|
282
|
+
message.error(`批量删除失败: ${error.message || error}`);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// 当前显示的人员数据
|
|
287
|
+
const displayPersons = currentPersons;
|
|
288
|
+
|
|
289
|
+
// ========== 组织管理功能 ==========
|
|
290
|
+
|
|
291
|
+
const handleAddOrg = () => {
|
|
292
|
+
setOrgModalType('add');
|
|
293
|
+
setEditingOrg(null);
|
|
294
|
+
setSelectedParentOrg(selectedOrg || 'root00000000');
|
|
295
|
+
orgForm.resetFields();
|
|
296
|
+
setOrgModalVisible(true);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const handleEditOrg = () => {
|
|
300
|
+
if (!selectedOrg) {
|
|
301
|
+
message.warning('请先选择一个组织');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const org = organizations.find((o: any) => o.orgIndexCode === selectedOrg);
|
|
305
|
+
if (!org) return;
|
|
306
|
+
|
|
307
|
+
setOrgModalType('edit');
|
|
308
|
+
setEditingOrg(org);
|
|
309
|
+
orgForm.setFieldsValue({ orgName: org.orgName });
|
|
310
|
+
setOrgModalVisible(true);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const handleDeleteOrg = async () => {
|
|
314
|
+
if (!selectedOrg) {
|
|
315
|
+
message.warning('请先选择一个组织');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await orgApi.delete([selectedOrg]);
|
|
320
|
+
message.success('删除成功');
|
|
321
|
+
loadOrganizations();
|
|
322
|
+
setSelectedOrg(null);
|
|
323
|
+
setSelectedOrgName('全部');
|
|
324
|
+
} catch (error: any) {
|
|
325
|
+
message.error(`删除失败: ${error.message || error}`);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const handleOrgSubmit = async () => {
|
|
330
|
+
try {
|
|
331
|
+
const values = await orgForm.validateFields();
|
|
332
|
+
if (orgModalType === 'add') {
|
|
333
|
+
await orgApi.create([{ orgName: values.orgName, parentIndexCode: selectedParentOrg }]);
|
|
334
|
+
message.success('添加成功');
|
|
335
|
+
} else if (editingOrg) {
|
|
336
|
+
await orgApi.update(editingOrg.orgIndexCode, { orgName: values.orgName });
|
|
337
|
+
message.success('更新成功');
|
|
338
|
+
}
|
|
339
|
+
setOrgModalVisible(false);
|
|
340
|
+
loadOrganizations();
|
|
341
|
+
} catch (error: any) {
|
|
342
|
+
if (error.errorFields) return;
|
|
343
|
+
message.error(`操作失败: ${error.message || error}`);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// 处理刷新
|
|
348
|
+
const handleRefresh = () => {
|
|
349
|
+
cancelRef.current = true;
|
|
350
|
+
loadCurrentOrgPersons(selectedOrg, page, pageSize, searchName);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const columns = [
|
|
354
|
+
{
|
|
355
|
+
title: '姓名',
|
|
356
|
+
dataIndex: 'personName',
|
|
357
|
+
key: 'personName',
|
|
358
|
+
width: 100,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
title: '性别',
|
|
362
|
+
dataIndex: 'gender',
|
|
363
|
+
key: 'gender',
|
|
364
|
+
width: 60,
|
|
365
|
+
render: (g: string | number) => {
|
|
366
|
+
const map: Record<string, string> = { '1': '男', '2': '女' };
|
|
367
|
+
return map[String(g)] || '未知';
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
title: '组织',
|
|
372
|
+
dataIndex: 'orgName',
|
|
373
|
+
key: 'orgName',
|
|
374
|
+
width: 150,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
title: '手机号',
|
|
378
|
+
dataIndex: 'phoneNo',
|
|
379
|
+
key: 'phoneNo',
|
|
380
|
+
width: 120,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
title: '操作',
|
|
384
|
+
key: 'actions',
|
|
385
|
+
width: 120,
|
|
386
|
+
render: (_: any, record: any) => (
|
|
387
|
+
<Space size="small">
|
|
388
|
+
<Link to={`/person/edit/${record.personId}`}>
|
|
389
|
+
<Button type="link" icon={<EditOutlined />} size="small">编辑</Button>
|
|
390
|
+
</Link>
|
|
391
|
+
<Link to={`/person/detail/${record.personId}`}>
|
|
392
|
+
<Button type="link" icon={<EyeOutlined />} size="small">详情</Button>
|
|
393
|
+
</Link>
|
|
394
|
+
<Button
|
|
395
|
+
type="link"
|
|
396
|
+
danger
|
|
397
|
+
icon={<DeleteOutlined />}
|
|
398
|
+
size="small"
|
|
399
|
+
onClick={() => setDeleteModal({ visible: true, personId: record.personId, personName: record.personName })}
|
|
400
|
+
>
|
|
401
|
+
删除
|
|
402
|
+
</Button>
|
|
403
|
+
</Space>
|
|
404
|
+
),
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div style={{ display: 'flex', gap: 16 }}>
|
|
410
|
+
{/* 左侧:组织树 + 组织管理 */}
|
|
411
|
+
<div style={{ width: 320, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
412
|
+
{/* 组织架构 */}
|
|
413
|
+
<Card
|
|
414
|
+
title="📁 组织架构"
|
|
415
|
+
extra={
|
|
416
|
+
<Space>
|
|
417
|
+
<Button size="small" icon={<ReloadOutlined />} onClick={handleRefresh} />
|
|
418
|
+
{selectedOrg && (
|
|
419
|
+
<>
|
|
420
|
+
<Button size="small" icon={<EditOutlined />} onClick={handleEditOrg}>编辑</Button>
|
|
421
|
+
<Popconfirm
|
|
422
|
+
title="确认删除"
|
|
423
|
+
description={`确定要删除组织"${selectedOrgName}"吗?`}
|
|
424
|
+
onConfirm={handleDeleteOrg}
|
|
425
|
+
okText="确认"
|
|
426
|
+
cancelText="取消"
|
|
427
|
+
>
|
|
428
|
+
<Button size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
|
429
|
+
</Popconfirm>
|
|
430
|
+
</>
|
|
431
|
+
)}
|
|
432
|
+
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAddOrg}>新增</Button>
|
|
433
|
+
</Space>
|
|
434
|
+
}
|
|
435
|
+
bodyStyle={{ padding: 12 }}
|
|
436
|
+
>
|
|
437
|
+
{treeLoading ? (
|
|
438
|
+
<div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
|
|
439
|
+
) : (
|
|
440
|
+
<Tree
|
|
441
|
+
showLine
|
|
442
|
+
checkStrictly
|
|
443
|
+
expandedKeys={expandedKeys}
|
|
444
|
+
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
|
445
|
+
onSelect={handleTreeSelect}
|
|
446
|
+
selectedKeys={selectedOrg ? [selectedOrg] : []}
|
|
447
|
+
treeData={treeData}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
</Card>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* 右侧:人员列表 */}
|
|
454
|
+
<div style={{ flex: 1 }}>
|
|
455
|
+
<Card
|
|
456
|
+
title={`👥 ${selectedOrgName} (${totalCount} 人)`}
|
|
457
|
+
extra={
|
|
458
|
+
<Space>
|
|
459
|
+
{selectedRowKeys.length > 0 && (
|
|
460
|
+
<Button danger size="small" onClick={() => setBatchDeleteModal({ visible: true, count: selectedRowKeys.length })}>
|
|
461
|
+
批量删除 ({selectedRowKeys.length})
|
|
462
|
+
</Button>
|
|
463
|
+
)}
|
|
464
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={() => navigate(`/person/add${selectedOrg ? `?orgCode=${selectedOrg}` : ''}`)}>添加人员</Button>
|
|
465
|
+
</Space>
|
|
466
|
+
}
|
|
467
|
+
>
|
|
468
|
+
<div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
|
|
469
|
+
<Search
|
|
470
|
+
placeholder="搜索姓名"
|
|
471
|
+
value={searchName}
|
|
472
|
+
onChange={(e) => setSearchName(e.target.value)}
|
|
473
|
+
onPressEnter={handleSearch}
|
|
474
|
+
style={{ width: 200 }}
|
|
475
|
+
/>
|
|
476
|
+
<Button icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
{displayPersons.length === 0 && !treeLoading ? (
|
|
480
|
+
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}>
|
|
481
|
+
{selectedOrg ? '该组织下暂无人员' : '暂无人员数据'}
|
|
482
|
+
</div>
|
|
483
|
+
) : (
|
|
484
|
+
<Table
|
|
485
|
+
columns={columns}
|
|
486
|
+
dataSource={displayPersons}
|
|
487
|
+
rowKey="personId"
|
|
488
|
+
loading={treeLoading}
|
|
489
|
+
rowSelection={{
|
|
490
|
+
selectedRowKeys,
|
|
491
|
+
onChange: (keys) => setSelectedRowKeys(keys),
|
|
492
|
+
}}
|
|
493
|
+
pagination={{
|
|
494
|
+
total: totalCount,
|
|
495
|
+
current: page,
|
|
496
|
+
pageSize,
|
|
497
|
+
showSizeChanger: true,
|
|
498
|
+
showTotal: (t) => `共 ${t} 条`,
|
|
499
|
+
onChange: (p, ps) => { setPage(p); setPageSize(ps); },
|
|
500
|
+
}}
|
|
501
|
+
size="small"
|
|
502
|
+
/>
|
|
503
|
+
)}
|
|
504
|
+
</Card>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
{/* 组织管理弹窗 */}
|
|
508
|
+
<Modal
|
|
509
|
+
title={orgModalType === 'add' ? '新增组织' : '编辑组织'}
|
|
510
|
+
open={orgModalVisible}
|
|
511
|
+
onOk={handleOrgSubmit}
|
|
512
|
+
onCancel={() => setOrgModalVisible(false)}
|
|
513
|
+
okText="保存"
|
|
514
|
+
cancelText="取消"
|
|
515
|
+
>
|
|
516
|
+
<Form form={orgForm} layout="vertical">
|
|
517
|
+
<Form.Item
|
|
518
|
+
name="orgName"
|
|
519
|
+
label="组织名称"
|
|
520
|
+
rules={[{ required: true, message: '请输入组织名称' }]}
|
|
521
|
+
>
|
|
522
|
+
<Input placeholder="请输入组织名称" />
|
|
523
|
+
</Form.Item>
|
|
524
|
+
{orgModalType === 'add' && (
|
|
525
|
+
<Form.Item label="父组织">
|
|
526
|
+
<Tree
|
|
527
|
+
showLine
|
|
528
|
+
treeData={treeData}
|
|
529
|
+
selectedKeys={[selectedParentOrg]}
|
|
530
|
+
onSelect={(keys) => setSelectedParentOrg(keys[0] as string)}
|
|
531
|
+
/>
|
|
532
|
+
</Form.Item>
|
|
533
|
+
)}
|
|
534
|
+
</Form>
|
|
535
|
+
</Modal>
|
|
536
|
+
|
|
537
|
+
<Modal
|
|
538
|
+
title="确认删除"
|
|
539
|
+
open={deleteModal.visible}
|
|
540
|
+
onOk={handleDelete}
|
|
541
|
+
onCancel={() => setDeleteModal({ visible: false })}
|
|
542
|
+
okText="确认删除"
|
|
543
|
+
okButtonProps={{ danger: true }}
|
|
544
|
+
>
|
|
545
|
+
<p>确定要删除人员 <strong>{deleteModal.personName}</strong> 吗?</p>
|
|
546
|
+
<p style={{ color: '#ff4d4f' }}>此操作不可撤销!</p>
|
|
547
|
+
</Modal>
|
|
548
|
+
|
|
549
|
+
{/* 批量删除确认弹窗 */}
|
|
550
|
+
<Modal
|
|
551
|
+
title="确认批量删除"
|
|
552
|
+
open={batchDeleteModal.visible}
|
|
553
|
+
onOk={handleBatchDelete}
|
|
554
|
+
onCancel={() => setBatchDeleteModal({ visible: false, count: 0 })}
|
|
555
|
+
okText="确认删除"
|
|
556
|
+
okButtonProps={{ danger: true }}
|
|
557
|
+
>
|
|
558
|
+
<p>确定要删除选中的 <strong>{batchDeleteModal.count}</strong> 名人员吗?</p>
|
|
559
|
+
<p style={{ color: '#ff4d4f' }}>此操作不可撤销!</p>
|
|
560
|
+
</Modal>
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Card, Descriptions } from 'antd';
|
|
2
|
+
|
|
3
|
+
export default function SyncPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div>
|
|
6
|
+
<h2 className="page-title" style={{ marginBottom: 24 }}>数据同步</h2>
|
|
7
|
+
|
|
8
|
+
<Card title="当前同步状态" style={{ marginBottom: 24 }}>
|
|
9
|
+
<Descriptions bordered column={3}>
|
|
10
|
+
<Descriptions.Item label="上次同步时间">—</Descriptions.Item>
|
|
11
|
+
<Descriptions.Item label="同步类型">—</Descriptions.Item>
|
|
12
|
+
<Descriptions.Item label="状态">—</Descriptions.Item>
|
|
13
|
+
</Descriptions>
|
|
14
|
+
</Card>
|
|
15
|
+
|
|
16
|
+
<Card title="同步操作" style={{ marginBottom: 24 }}>
|
|
17
|
+
<div style={{ color: '#999' }}>
|
|
18
|
+
同步功能开发中,请稍候...
|
|
19
|
+
</div>
|
|
20
|
+
</Card>
|
|
21
|
+
|
|
22
|
+
<Card title="同步记录">
|
|
23
|
+
<div style={{ color: '#999', textAlign: 'center', padding: 40 }}>
|
|
24
|
+
暂无同步记录
|
|
25
|
+
</div>
|
|
26
|
+
</Card>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|