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.
@@ -0,0 +1,336 @@
1
+ /**
2
+ * 车辆群组管理页面
3
+ *
4
+ * 重要概念澄清(2026-05-23):
5
+ * - 车辆群组 (car-group):停车场功能,用于车辆出入控制
6
+ * - 车辆分类 (category):系统预置分类(固定车、临时车等)+ 自定义群组
7
+ *
8
+ * 注意:车辆群组仅用于停车场管理,与人员通行无关
9
+ */
10
+
11
+ import { useEffect, useState } from 'react';
12
+ import { Table, Button, Input, Space, Modal, message, Form, Select, Tag, Radio } from 'antd';
13
+ import {
14
+ PlusOutlined,
15
+ EditOutlined,
16
+ DeleteOutlined,
17
+ ReloadOutlined,
18
+ CarOutlined,
19
+ } from '@ant-design/icons';
20
+ import { vehicleGroupApi, vehicleApi } from '../services/api';
21
+ import { SYSTEM_CATEGORY_ID_ARRAY } from '../utils/constants';
22
+
23
+ interface CarGroup {
24
+ groupId: string;
25
+ groupName: string;
26
+ groupType: '0' | '1';
27
+ description?: string;
28
+ regionIndexCode?: string;
29
+ type?: 'system' | 'custom'; // system=系统分类, custom=自定义群组
30
+ }
31
+
32
+ interface Vehicle {
33
+ vehicleId: string;
34
+ plateNo: string;
35
+ personName?: string;
36
+ }
37
+
38
+ export default function CarGroupPage() {
39
+ const [groups, setGroups] = useState<CarGroup[]>([]);
40
+ const [vehicles, setVehicles] = useState<Vehicle[]>([]);
41
+ const [isLoading, setIsLoading] = useState(false);
42
+ const [searchName, setSearchName] = useState('');
43
+ const [typeFilter, setTypeFilter] = useState<'all' | 'system' | 'custom'>('all');
44
+ const [addModal, setAddModal] = useState<{ visible: false } | { visible: true; editingGroup?: CarGroup }>({ visible: false });
45
+ const [form] = Form.useForm();
46
+ const [selectedGroup, setSelectedGroup] = useState<CarGroup | null>(null);
47
+ const [assignVehicleModal, setAssignVehicleModal] = useState(false);
48
+ const [assignForm] = Form.useForm();
49
+
50
+ const loadGroups = async () => {
51
+ setIsLoading(true);
52
+ try {
53
+ const res: any = await vehicleGroupApi.list();
54
+ const list: CarGroup[] = res?.data || res || [];
55
+ // 添加 type 标识
56
+ const groupsWithType = list.map(g => ({
57
+ ...g,
58
+ type: SYSTEM_CATEGORY_ID_ARRAY.includes(g.groupId) ? 'system' as const : 'custom' as const,
59
+ }));
60
+ setGroups(groupsWithType);
61
+ } catch (error: any) {
62
+ message.error(`加载群组失败: ${error.message || error}`);
63
+ } finally {
64
+ setIsLoading(false);
65
+ }
66
+ };
67
+
68
+ const loadVehicles = async () => {
69
+ try {
70
+ const res: any = await vehicleApi.list({ pageNo: 1, pageSize: 100 });
71
+ const list = res?.data?.list || res?.list || [];
72
+ setVehicles(list);
73
+ } catch (error) {
74
+ console.error('加载车辆失败:', error);
75
+ }
76
+ };
77
+
78
+ useEffect(() => {
79
+ loadGroups();
80
+ loadVehicles();
81
+ }, []);
82
+
83
+ const handleAdd = () => {
84
+ form.resetFields();
85
+ setAddModal({ visible: true });
86
+ };
87
+
88
+ const handleEdit = (record: CarGroup) => {
89
+ form.setFieldsValue({ groupName: record.groupName, groupType: record.groupType });
90
+ setAddModal({ visible: true, editingGroup: record });
91
+ };
92
+
93
+ const handleDelete = (record: CarGroup) => {
94
+ // 系统分类不允许删除
95
+ if (SYSTEM_CATEGORY_ID_ARRAY.includes(record.groupId)) {
96
+ message.warning('系统预置分类不允许删除');
97
+ return;
98
+ }
99
+
100
+ Modal.confirm({
101
+ title: '确认删除',
102
+ content: `确定要删除群组"${record.groupName}"吗?`,
103
+ okText: '确认删除',
104
+ okType: 'danger',
105
+ cancelText: '取消',
106
+ onOk: async () => {
107
+ try {
108
+ await vehicleGroupApi.delete(record.groupId);
109
+ message.success('删除成功');
110
+ loadGroups();
111
+ } catch (error: any) {
112
+ message.error(`删除失败: ${error.message || error}`);
113
+ }
114
+ },
115
+ });
116
+ };
117
+
118
+ const handleSubmit = async () => {
119
+ try {
120
+ const values = await form.validateFields();
121
+ if ((addModal as any).editingGroup) {
122
+ await vehicleGroupApi.update((addModal as any).editingGroup.groupId, values);
123
+ message.success('更新成功');
124
+ } else {
125
+ await vehicleGroupApi.create(values);
126
+ message.success('创建成功');
127
+ }
128
+ setAddModal({ visible: false });
129
+ loadGroups();
130
+ } catch (error: any) {
131
+ if (error.errorFields) return;
132
+ message.error(`${(addModal as any).editingGroup ? '更新' : '创建'}失败: ${error.message || error}`);
133
+ }
134
+ };
135
+
136
+ const handleAssignVehicle = (record: CarGroup) => {
137
+ setSelectedGroup(record);
138
+ assignForm.resetFields();
139
+ setAssignVehicleModal(true);
140
+ };
141
+
142
+ const handleAssignSubmit = async () => {
143
+ try {
144
+ await assignForm.validateFields();
145
+ message.success(`已分配车辆到群组"${selectedGroup?.groupName}"`);
146
+ setAssignVehicleModal(false);
147
+ } catch (error: any) {
148
+ if (error.errorFields) return;
149
+ message.error(`分配失败: ${error.message || error}`);
150
+ }
151
+ };
152
+
153
+ // 过滤群组
154
+ const filteredGroups = groups.filter((g) => {
155
+ const matchName = g.groupName.toLowerCase().includes(searchName.toLowerCase());
156
+ const matchType = typeFilter === 'all' || g.type === typeFilter;
157
+ return matchName && matchType;
158
+ });
159
+
160
+ // 系统分类名称映射(中文)
161
+ const getCategoryName = (groupId: string): string => {
162
+ const nameMap: Record<string, string> = {
163
+ '9': '黑名单',
164
+ '10': '固定车',
165
+ '11': '临时车',
166
+ '12': '预约车',
167
+ '13': '群组车',
168
+ '14': '特殊车',
169
+ };
170
+ return nameMap[groupId] || '';
171
+ };
172
+
173
+ const columns = [
174
+ {
175
+ title: '群组编号',
176
+ dataIndex: 'groupId',
177
+ key: 'groupId',
178
+ width: 200,
179
+ render: (id: string) => <code style={{ fontSize: 12 }}>{id}</code>,
180
+ },
181
+ {
182
+ title: '群组名称',
183
+ dataIndex: 'groupName',
184
+ key: 'groupName',
185
+ width: 150,
186
+ },
187
+ {
188
+ title: '中文名称',
189
+ key: 'chineseName',
190
+ width: 100,
191
+ render: (_: any, record: CarGroup) => {
192
+ if (record.type === 'system') {
193
+ return getCategoryName(record.groupId) || '-';
194
+ }
195
+ return '-';
196
+ },
197
+ },
198
+ {
199
+ title: '类型',
200
+ dataIndex: 'type',
201
+ key: 'type',
202
+ width: 100,
203
+ render: (type: 'system' | 'custom') => (
204
+ <Tag color={type === 'system' ? 'blue' : 'green'}>
205
+ {type === 'system' ? '系统分类' : '自定义群组'}
206
+ </Tag>
207
+ ),
208
+ },
209
+ {
210
+ title: '描述',
211
+ dataIndex: 'description',
212
+ key: 'description',
213
+ width: 150,
214
+ render: (desc: string) => desc || '-',
215
+ },
216
+ {
217
+ title: '操作',
218
+ key: 'action',
219
+ width: 250,
220
+ render: (_: any, record: CarGroup) => (
221
+ <Space size="small">
222
+ <Button size="small" type="text" icon={<CarOutlined />} onClick={() => handleAssignVehicle(record)}>
223
+ 分配车辆
224
+ </Button>
225
+ {record.type === 'custom' && (
226
+ <>
227
+ <Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
228
+ 编辑
229
+ </Button>
230
+ <Button size="small" type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
231
+ 删除
232
+ </Button>
233
+ </>
234
+ )}
235
+ </Space>
236
+ ),
237
+ },
238
+ ];
239
+
240
+ return (
241
+ <div style={{ padding: 24 }}>
242
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
243
+ <h2>车辆群组管理</h2>
244
+ <Space>
245
+ <Button icon={<ReloadOutlined />} onClick={loadGroups}>刷新</Button>
246
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加群组</Button>
247
+ </Space>
248
+ </div>
249
+
250
+ <div style={{ marginBottom: 16, display: 'flex', gap: 16, alignItems: 'center' }}>
251
+ <Input.Search
252
+ placeholder="搜索群组名称"
253
+ value={searchName}
254
+ onChange={(e) => setSearchName(e.target.value)}
255
+ style={{ width: 300 }}
256
+ allowClear
257
+ />
258
+ <Radio.Group
259
+ value={typeFilter}
260
+ onChange={(e) => setTypeFilter(e.target.value as 'all' | 'system' | 'custom')}
261
+ optionType="button"
262
+ buttonStyle="solid"
263
+ >
264
+ <Radio.Button value="all">全部</Radio.Button>
265
+ <Radio.Button value="system">系统分类</Radio.Button>
266
+ <Radio.Button value="custom">自定义群组</Radio.Button>
267
+ </Radio.Group>
268
+ </div>
269
+
270
+ <Table
271
+ columns={columns}
272
+ dataSource={filteredGroups}
273
+ rowKey="groupId"
274
+ loading={isLoading}
275
+ pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `共 ${t} 条` }}
276
+ />
277
+
278
+ {/* 添加/编辑群组弹窗 */}
279
+ <Modal
280
+ title={(addModal as any).editingGroup ? '编辑群组' : '添加群组'}
281
+ open={(addModal as any).visible}
282
+ onOk={handleSubmit}
283
+ onCancel={() => setAddModal({ visible: false })}
284
+ okText={(addModal as any).editingGroup ? '保存' : '添加'}
285
+ cancelText="取消"
286
+ >
287
+ <Form form={form} layout="vertical">
288
+ <Form.Item
289
+ name="groupName"
290
+ label="群组名称"
291
+ rules={[{ required: true, message: '请输入群组名称' }]}
292
+ >
293
+ <Input placeholder="请输入群组名称" />
294
+ </Form.Item>
295
+ <Form.Item
296
+ name="groupType"
297
+ label="群组类型"
298
+ initialValue="0"
299
+ rules={[{ required: true, message: '请选择群组类型' }]}
300
+ >
301
+ <Select>
302
+ <Select.Option value="0">内部群组</Select.Option>
303
+ <Select.Option value="1">外部群组</Select.Option>
304
+ </Select>
305
+ </Form.Item>
306
+ </Form>
307
+ </Modal>
308
+
309
+ {/* 分配车辆弹窗 */}
310
+ <Modal
311
+ title={`分配车辆到 "${selectedGroup?.groupName}"`}
312
+ open={assignVehicleModal}
313
+ onOk={handleAssignSubmit}
314
+ onCancel={() => setAssignVehicleModal(false)}
315
+ okText="确认分配"
316
+ cancelText="取消"
317
+ >
318
+ <Form form={assignForm} layout="vertical">
319
+ <Form.Item
320
+ name="vehicleIds"
321
+ label="选择车辆"
322
+ rules={[{ required: true, message: '请选择车辆' }]}
323
+ >
324
+ <Select mode="multiple" placeholder="请选择车辆" allowClear>
325
+ {vehicles.map((v) => (
326
+ <Select.Option key={v.vehicleId} value={v.vehicleId}>
327
+ {v.plateNo} {v.personName ? `- ${v.personName}` : ''}
328
+ </Select.Option>
329
+ ))}
330
+ </Select>
331
+ </Form.Item>
332
+ </Form>
333
+ </Modal>
334
+ </div>
335
+ );
336
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 组织管理页面
3
+ */
4
+
5
+ import { useEffect, useState } from 'react';
6
+ import { Table, Button, Input, Space, Modal, message, Select, Tag } from 'antd';
7
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
8
+ import { orgApi } from '../services/api';
9
+
10
+ interface Organization {
11
+ orgIndexCode: string;
12
+ orgName: string;
13
+ parentOrgIndexCode?: string;
14
+ level?: number;
15
+ }
16
+
17
+ export default function OrgPage() {
18
+ const [orgs, setOrgs] = useState<Organization[]>([]);
19
+ const [loading, setLoading] = useState(false);
20
+ const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
21
+ const [isModalVisible, setIsModalVisible] = useState(false);
22
+ const [modalType, setModalType] = useState<'add' | 'edit'>('add');
23
+ const [formData, setFormData] = useState({ orgName: '', parentIndexCode: 'root000000' });
24
+
25
+ useEffect(() => {
26
+ loadOrgs();
27
+ }, []);
28
+
29
+ const loadOrgs = async () => {
30
+ setLoading(true);
31
+ try {
32
+ const result: any = await orgApi.list();
33
+ const list = result?.data || result || [];
34
+ setOrgs(Array.isArray(list) ? list : []);
35
+ } catch (error: any) {
36
+ message.error(`加载组织失败: ${error.message || error}`);
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+
42
+ const handleAdd = () => {
43
+ setModalType('add');
44
+ setFormData({ orgName: '', parentIndexCode: 'root000000' });
45
+ setSelectedOrg(null);
46
+ setIsModalVisible(true);
47
+ };
48
+
49
+ const handleEdit = (record: Organization) => {
50
+ setModalType('edit');
51
+ setSelectedOrg(record);
52
+ setFormData({ orgName: record.orgName, parentIndexCode: record.parentOrgIndexCode || 'root000000' });
53
+ setIsModalVisible(true);
54
+ };
55
+
56
+ const handleDelete = async (record: Organization) => {
57
+ Modal.confirm({
58
+ title: '确认删除',
59
+ content: `确定要删除组织"${record.orgName}"吗?删除后无法恢复。`,
60
+ okText: '确认删除',
61
+ okType: 'danger',
62
+ cancelText: '取消',
63
+ onOk: async () => {
64
+ try {
65
+ await orgApi.delete([record.orgIndexCode]);
66
+ message.success('删除成功');
67
+ loadOrgs();
68
+ } catch (error: any) {
69
+ message.error(`删除失败: ${error.message || error}`);
70
+ }
71
+ },
72
+ });
73
+ };
74
+
75
+ const handleSubmit = async () => {
76
+ if (!formData.orgName.trim()) {
77
+ message.warning('请输入组织名称');
78
+ return;
79
+ }
80
+
81
+ try {
82
+ if (modalType === 'add') {
83
+ await orgApi.create([{ orgName: formData.orgName, parentIndexCode: formData.parentIndexCode }]);
84
+ message.success('添加成功');
85
+ } else if (selectedOrg) {
86
+ await orgApi.update(selectedOrg.orgIndexCode, { orgName: formData.orgName });
87
+ message.success('更新成功');
88
+ }
89
+ setIsModalVisible(false);
90
+ loadOrgs();
91
+ } catch (error: any) {
92
+ message.error(`${modalType === 'add' ? '添加' : '更新'}失败: ${error.message || error}`);
93
+ }
94
+ };
95
+
96
+ const columns = [
97
+ {
98
+ title: '组织名称',
99
+ dataIndex: 'orgName',
100
+ key: 'orgName',
101
+ render: (text: string, record: Organization) => (
102
+ <Space>
103
+ <span>{text}</span>
104
+ {record.level === 0 && <Tag color="blue">根组织</Tag>}
105
+ </Space>
106
+ ),
107
+ },
108
+ {
109
+ title: '组织编号',
110
+ dataIndex: 'orgIndexCode',
111
+ key: 'orgIndexCode',
112
+ width: 300,
113
+ ellipsis: true,
114
+ },
115
+ {
116
+ title: '层级',
117
+ dataIndex: 'level',
118
+ key: 'level',
119
+ width: 80,
120
+ render: (level?: number) => level ?? '-',
121
+ },
122
+ {
123
+ title: '操作',
124
+ key: 'action',
125
+ width: 200,
126
+ render: (_: any, record: Organization) => (
127
+ <Space>
128
+ <Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
129
+ 编辑
130
+ </Button>
131
+ <Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
132
+ 删除
133
+ </Button>
134
+ </Space>
135
+ ),
136
+ },
137
+ ];
138
+
139
+ return (
140
+ <div style={{ padding: 24 }}>
141
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
142
+ <h2>组织管理</h2>
143
+ <Space>
144
+ <Button icon={<ReloadOutlined />} onClick={loadOrgs}>
145
+ 刷新
146
+ </Button>
147
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
148
+ 添加组织
149
+ </Button>
150
+ </Space>
151
+ </div>
152
+
153
+ <Table
154
+ columns={columns}
155
+ dataSource={orgs}
156
+ rowKey="orgIndexCode"
157
+ loading={loading}
158
+ pagination={{ pageSize: 20, showSizeChanger: true }}
159
+ scroll={{ x: 800 }}
160
+ />
161
+
162
+ <Modal
163
+ title={modalType === 'add' ? '添加组织' : '编辑组织'}
164
+ open={isModalVisible}
165
+ onOk={handleSubmit}
166
+ onCancel={() => setIsModalVisible(false)}
167
+ okText={modalType === 'add' ? '添加' : '保存'}
168
+ cancelText="取消"
169
+ >
170
+ <div style={{ padding: 16 }}>
171
+ <div style={{ marginBottom: 16 }}>
172
+ <label style={{ display: 'block', marginBottom: 8 }}>组织名称</label>
173
+ <Input
174
+ value={formData.orgName}
175
+ onChange={(e) => setFormData({ ...formData, orgName: e.target.value })}
176
+ placeholder="请输入组织名称"
177
+ />
178
+ </div>
179
+ <div style={{ marginBottom: 16 }}>
180
+ <label style={{ display: 'block', marginBottom: 8 }}>父组织</label>
181
+ <Select
182
+ value={formData.parentIndexCode}
183
+ onChange={(value) => setFormData({ ...formData, parentIndexCode: value })}
184
+ style={{ width: '100%' }}
185
+ >
186
+ <Select.Option value="root000000">根组织</Select.Option>
187
+ {orgs.map((org) => (
188
+ <Select.Option key={org.orgIndexCode} value={org.orgIndexCode}>
189
+ {org.orgName}
190
+ </Select.Option>
191
+ ))}
192
+ </Select>
193
+ </div>
194
+ </div>
195
+ </Modal>
196
+ </div>
197
+ );
198
+ }