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,351 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Table, Button, Modal, message, Form, Select, Tabs, Input } from 'antd';
3
+ import { LinkOutlined, PlusOutlined } from '@ant-design/icons';
4
+ import { bindingApi, cardApi, vehicleApi } from '../services/api';
5
+ import { useAppStore } from '../store/appStore';
6
+
7
+ interface PersonCardBinding {
8
+ personId: string;
9
+ cardNo: string;
10
+ bindTime?: string;
11
+ }
12
+
13
+ interface PersonVehicleBinding {
14
+ personId: string;
15
+ vehicleId: string;
16
+ bindTime?: string;
17
+ }
18
+
19
+ interface Card {
20
+ cardNo: string;
21
+ personId?: string;
22
+ personName?: string;
23
+ }
24
+
25
+ interface Vehicle {
26
+ vehicleId: string;
27
+ plateNo: string;
28
+ personId?: string;
29
+ personName?: string;
30
+ }
31
+
32
+ export default function BindingPage() {
33
+ const { persons, loadPersons } = useAppStore();
34
+ const [activeTab, setActiveTab] = useState('person-card');
35
+ const [modalVisible, setModalVisible] = useState(false);
36
+ const [form] = Form.useForm();
37
+ const [personCardBindings, setPersonCardBindings] = useState<PersonCardBinding[]>([]);
38
+ const [personVehicleBindings, setPersonVehicleBindings] = useState<PersonVehicleBinding[]>([]);
39
+ const [cards, setCards] = useState<Card[]>([]);
40
+ const [vehicles, setVehicles] = useState<Vehicle[]>([]);
41
+ const [personCardSearch, setPersonCardSearch] = useState('');
42
+ const [personVehicleSearch, setPersonVehicleSearch] = useState('');
43
+
44
+ useEffect(() => {
45
+ loadPersons();
46
+ loadCards();
47
+ loadVehicles();
48
+ }, []);
49
+
50
+ const loadCards = async () => {
51
+ try {
52
+ const result: any = await cardApi.list({ pageNo: 1, pageSize: 100 });
53
+ setCards(result?.data?.list || result?.list || []);
54
+ } catch (error) {
55
+ console.error('加载卡片失败:', error);
56
+ }
57
+ };
58
+
59
+ const loadVehicles = async () => {
60
+ try {
61
+ const result: any = await vehicleApi.list({ pageNo: 1, pageSize: 100 });
62
+ setVehicles(result?.data?.list || result?.list || []);
63
+ } catch (error) {
64
+ console.error('加载车辆失败:', error);
65
+ }
66
+ };
67
+
68
+ const loadBindings = async () => {
69
+ if (activeTab === 'person-card') {
70
+ try {
71
+ const result: any = await bindingApi.listPersonCardBindings();
72
+ setPersonCardBindings(result?.data?.list || result?.list || []);
73
+ } catch (error) {
74
+ console.error('加载人员卡片绑定失败:', error);
75
+ }
76
+ } else {
77
+ try {
78
+ const result: any = await bindingApi.listPersonVehicleBindings();
79
+ setPersonVehicleBindings(result?.data?.list || result?.list || []);
80
+ } catch (error) {
81
+ console.error('加载人员车辆绑定失败:', error);
82
+ }
83
+ }
84
+ };
85
+
86
+ useEffect(() => {
87
+ loadBindings();
88
+ }, [activeTab]);
89
+
90
+ const handleAdd = () => {
91
+ form.resetFields();
92
+ setModalVisible(true);
93
+ };
94
+
95
+ const handleSubmit = async () => {
96
+ try {
97
+ const values = await form.validateFields();
98
+ if (activeTab === 'person-card') {
99
+ await bindingApi.bindPersonCard(values.personId, values.cardNo!);
100
+ message.success('人员-卡片绑定成功');
101
+ } else {
102
+ await bindingApi.bindPersonVehicle(values.personId, values.vehicleId!);
103
+ message.success('人员-车辆绑定成功');
104
+ }
105
+ setModalVisible(false);
106
+ loadBindings();
107
+ } catch (error: any) {
108
+ message.error(`绑定失败: ${error.message || error}`);
109
+ }
110
+ };
111
+
112
+ const handleUnbind = async (personId: string, cardNo?: string, vehicleId?: string) => {
113
+ try {
114
+ if (activeTab === 'person-card') {
115
+ await bindingApi.unbindPersonCard(personId, cardNo!);
116
+ message.success('解绑成功');
117
+ } else {
118
+ await bindingApi.unbindPersonVehicle(personId, vehicleId!);
119
+ message.success('解绑成功');
120
+ }
121
+ loadBindings();
122
+ } catch (error: any) {
123
+ message.error(`解绑失败: ${error.message || error}`);
124
+ }
125
+ };
126
+
127
+ const getPersonName = (personId: string, personName?: string) => {
128
+ // 优先用传入的 personName(服务端返回的绑定数据已有),其次查 store
129
+ if (personName) return personName;
130
+ const person = persons.find(p => p.personId === personId);
131
+ return person?.personName || personId;
132
+ };
133
+
134
+
135
+ const getVehiclePlate = (vehicleId: string) => {
136
+ const vehicle = vehicles.find(v => v.vehicleId === vehicleId);
137
+ return vehicle?.plateNo || vehicleId;
138
+ };
139
+
140
+ // 过滤后的绑定数据
141
+ const filteredPersonCardBindings = personCardBindings.filter(b => {
142
+ if (!personCardSearch) return true;
143
+ const s = personCardSearch.toLowerCase();
144
+ const personName = b.personId ? (b as any).personName || '' : '';
145
+ return personName.toLowerCase().includes(s) || b.cardNo?.includes(s) || b.personId?.toLowerCase().includes(s);
146
+ });
147
+
148
+ const filteredPersonVehicleBindings = personVehicleBindings.filter(b => {
149
+ if (!personVehicleSearch) return true;
150
+ const s = personVehicleSearch.toLowerCase();
151
+ const personName = b.personId ? (b as any).personName || '' : '';
152
+ const vehicle = vehicles.find(v => v.vehicleId === b.vehicleId);
153
+ return personName.toLowerCase().includes(s) || vehicle?.plateNo?.toLowerCase().includes(s) || b.personId?.toLowerCase().includes(s);
154
+ });
155
+
156
+ const personCardColumns = [
157
+ {
158
+ title: '人员',
159
+ dataIndex: 'personId',
160
+ key: 'person',
161
+ width: 150,
162
+ render: (personId: string, record: any) => getPersonName(personId, record.personName),
163
+ },
164
+ {
165
+ title: '卡片号',
166
+ dataIndex: 'cardNo',
167
+ key: 'cardNo',
168
+ width: 150,
169
+ },
170
+ {
171
+ title: '绑定时间',
172
+ dataIndex: 'bindTime',
173
+ key: 'bindTime',
174
+ width: 180,
175
+ render: (time: string) => time ? new Date(time).toLocaleString('zh-CN') : '-',
176
+ },
177
+ {
178
+ title: '操作',
179
+ key: 'actions',
180
+ width: 100,
181
+ render: (_: any, record: PersonCardBinding) => (
182
+ <Button
183
+ type="link"
184
+ danger
185
+ icon={<LinkOutlined />}
186
+ size="small"
187
+ onClick={() => handleUnbind(record.personId, record.cardNo)}
188
+ >
189
+ 解绑
190
+ </Button>
191
+ ),
192
+ },
193
+ ];
194
+
195
+ const personVehicleColumns = [
196
+ {
197
+ title: '人员',
198
+ dataIndex: 'personId',
199
+ key: 'person',
200
+ width: 150,
201
+ render: (personId: string, record: any) => getPersonName(personId, record.personName),
202
+ },
203
+ {
204
+ title: '车牌号',
205
+ key: 'plateNo',
206
+ width: 150,
207
+ render: (_: any, record: PersonVehicleBinding) => getVehiclePlate(record.vehicleId),
208
+ },
209
+ {
210
+ title: '绑定时间',
211
+ dataIndex: 'bindTime',
212
+ key: 'bindTime',
213
+ width: 180,
214
+ render: (time: string) => time ? new Date(time).toLocaleString('zh-CN') : '-',
215
+ },
216
+ {
217
+ title: '操作',
218
+ key: 'actions',
219
+ width: 100,
220
+ render: (_: any, record: PersonVehicleBinding) => (
221
+ <Button
222
+ type="link"
223
+ danger
224
+ icon={<LinkOutlined />}
225
+ size="small"
226
+ onClick={() => handleUnbind(record.personId, undefined, record.vehicleId)}
227
+ >
228
+ 解绑
229
+ </Button>
230
+ ),
231
+ },
232
+ ];
233
+
234
+ return (
235
+ <div>
236
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
237
+ <h2 className="page-title" style={{ marginBottom: 0 }}>绑定关系管理</h2>
238
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加绑定</Button>
239
+ </div>
240
+
241
+ <Tabs
242
+ activeKey={activeTab}
243
+ onChange={(key) => { setActiveTab(key); loadBindings(); }}
244
+ items={[
245
+ {
246
+ key: 'person-card',
247
+ label: '人员-卡片绑定',
248
+ children: (
249
+ <>
250
+ <div style={{ marginBottom: 12 }}>
251
+ <Input.Search
252
+ placeholder="搜索人员姓名 / 卡号 / ID"
253
+ value={personCardSearch}
254
+ onChange={e => setPersonCardSearch(e.target.value)}
255
+ style={{ width: 300 }}
256
+ allowClear
257
+ />
258
+ </div>
259
+ <Table
260
+ columns={personCardColumns}
261
+ dataSource={filteredPersonCardBindings}
262
+ rowKey={(record: PersonCardBinding) => `${record.personId}-${record.cardNo}`}
263
+ pagination={{ pageSize: 20, showTotal: (t) => `共 ${t} 条` }}
264
+ />
265
+ </>
266
+ ),
267
+ },
268
+ {
269
+ key: 'person-vehicle',
270
+ label: '人员-车辆绑定',
271
+ children: (
272
+ <>
273
+ <div style={{ marginBottom: 12 }}>
274
+ <Input.Search
275
+ placeholder="搜索人员姓名 / 车牌 / ID"
276
+ value={personVehicleSearch}
277
+ onChange={e => setPersonVehicleSearch(e.target.value)}
278
+ style={{ width: 300 }}
279
+ allowClear
280
+ />
281
+ </div>
282
+ <Table
283
+ columns={personVehicleColumns}
284
+ dataSource={filteredPersonVehicleBindings}
285
+ rowKey={(record: PersonVehicleBinding) => `${record.personId}-${record.vehicleId}`}
286
+ pagination={{ pageSize: 20, showTotal: (t) => `共 ${t} 条` }}
287
+ />
288
+ </>
289
+ ),
290
+ },
291
+ ]}
292
+ />
293
+
294
+ <Modal
295
+ title={activeTab === 'person-card' ? '人员-卡片绑定' : '人员-车辆绑定'}
296
+ open={modalVisible}
297
+ onOk={handleSubmit}
298
+ onCancel={() => setModalVisible(false)}
299
+ okText="绑定"
300
+ >
301
+ <Form form={form} layout="vertical">
302
+ <Form.Item
303
+ name="personId"
304
+ label="人员"
305
+ rules={[{ required: true, message: '请选择人员' }]}
306
+ >
307
+ <Select
308
+ placeholder="选择人员"
309
+ showSearch
310
+ filterOption={(input, option) =>
311
+ (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
312
+ }
313
+ options={persons.map(p => ({ value: p.personId, label: `${p.personName} (${p.phoneNo || '无手机号'})` }))}
314
+ />
315
+ </Form.Item>
316
+ {activeTab === 'person-card' ? (
317
+ <Form.Item
318
+ name="cardNo"
319
+ label="卡片号"
320
+ rules={[{ required: true, message: '请选择卡片' }]}
321
+ >
322
+ <Select
323
+ placeholder="选择卡片"
324
+ showSearch
325
+ filterOption={(input, option) =>
326
+ (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
327
+ }
328
+ options={cards.map(c => ({ value: c.cardNo, label: `${c.cardNo} (${c.personName || '未分配'})` }))}
329
+ />
330
+ </Form.Item>
331
+ ) : (
332
+ <Form.Item
333
+ name="vehicleId"
334
+ label="车辆"
335
+ rules={[{ required: true, message: '请选择车辆' }]}
336
+ >
337
+ <Select
338
+ placeholder="选择车辆"
339
+ showSearch
340
+ filterOption={(input, option) =>
341
+ (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
342
+ }
343
+ options={vehicles.map(v => ({ value: v.vehicleId, label: `${v.plateNo} (${v.personName || '无车主'})` }))}
344
+ />
345
+ </Form.Item>
346
+ )}
347
+ </Form>
348
+ </Modal>
349
+ </div>
350
+ );
351
+ }
@@ -0,0 +1,173 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useNavigate, useSearchParams } from 'react-router-dom';
3
+ import { Form, Input, Select, Button, message, Card, Spin, Space, Table } from 'antd';
4
+ import { ArrowLeftOutlined, SaveOutlined, ReloadOutlined } from '@ant-design/icons';
5
+ import { cardApi, personApi } from '../services/api';
6
+ import { useAppStore } from '../store/appStore';
7
+
8
+ const { Option } = Select;
9
+
10
+ interface PersonItem {
11
+ personId: string;
12
+ personName: string;
13
+ orgName?: string;
14
+ jobNo?: string;
15
+ }
16
+
17
+ export default function CardForm() {
18
+ const navigate = useNavigate();
19
+ const [searchParams] = useSearchParams();
20
+ const refCardNo = searchParams.get('ref');
21
+ const [form] = Form.useForm();
22
+ const [loading] = useState(false);
23
+ const [submitting, setSubmitting] = useState(false);
24
+ const [persons, setPersons] = useState<PersonItem[]>([]);
25
+ const [personSearch, setPersonSearch] = useState('');
26
+ const { loadOrganizations } = useAppStore();
27
+
28
+ useEffect(() => {
29
+ loadOrganizations();
30
+ loadPersons();
31
+ if (refCardNo) {
32
+ loadRefCard();
33
+ }
34
+ }, [refCardNo]);
35
+
36
+ const loadPersons = async (name?: string) => {
37
+ try {
38
+ const result: any = await personApi.list({ pageNo: 1, pageSize: 100, personName: name });
39
+ setPersons((result?.data?.list || result?.list || []).map((p: any) => ({
40
+ personId: p.personId,
41
+ personName: p.personName,
42
+ orgName: p.orgName,
43
+ jobNo: p.jobNo,
44
+ })));
45
+ } catch (error: any) {
46
+ console.error('加载人员失败:', error);
47
+ }
48
+ };
49
+
50
+ const loadRefCard = async () => {
51
+ if (!refCardNo) return;
52
+ try {
53
+ const result: any = await cardApi.getByCardNo(refCardNo);
54
+ const card = result.card || {};
55
+ form.setFieldsValue({
56
+ cardNo: card.cardNo,
57
+ cardType: card.cardType,
58
+ personId: card.personId,
59
+ });
60
+ } catch (error: any) {
61
+ console.error('加载参考卡片失败:', error);
62
+ }
63
+ };
64
+
65
+ const handleSubmit = async (values: any) => {
66
+ setSubmitting(true);
67
+ try {
68
+ const dto = {
69
+ cards: [{
70
+ cardNo: values.cardNo,
71
+ personId: values.personId,
72
+ cardType: values.cardType as '0' | '1' | '2',
73
+ }]
74
+ };
75
+
76
+ await cardApi.batchIssue(dto as any);
77
+ message.success('开卡成功!');
78
+ navigate('/card');
79
+ } catch (error: any) {
80
+ message.error(`开卡失败: ${error.message || error}`);
81
+ } finally {
82
+ setSubmitting(false);
83
+ }
84
+ };
85
+
86
+ if (loading) {
87
+ return <div style={{ padding: 40, textAlign: 'center' }}><Spin size="large" /></div>;
88
+ }
89
+
90
+ return (
91
+ <div>
92
+ <div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
93
+ <Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/card')}>返回</Button>
94
+ <h2>开卡</h2>
95
+ </div>
96
+
97
+ <Card style={{ maxWidth: 600 }}>
98
+ <Form
99
+ form={form}
100
+ layout="vertical"
101
+ onFinish={handleSubmit}
102
+ initialValues={{ cardType: '0' }}
103
+ >
104
+ <Form.Item
105
+ name="cardNo"
106
+ label="卡号"
107
+ rules={[{ required: true, message: '请输入卡号' }]}
108
+ >
109
+ <Input placeholder="请输入卡号" addonBefore="卡号" />
110
+ </Form.Item>
111
+
112
+ <Form.Item
113
+ name="cardType"
114
+ label="卡片类型"
115
+ rules={[{ required: true, message: '请选择卡片类型' }]}
116
+ >
117
+ <Select placeholder="请选择卡片类型">
118
+ <Option value="0">普通卡</Option>
119
+ <Option value="1">临时卡</Option>
120
+ <Option value="2">特殊卡</Option>
121
+ </Select>
122
+ </Form.Item>
123
+
124
+ <Form.Item
125
+ name="personId"
126
+ label="持卡人"
127
+ rules={[{ required: true, message: '请选择持卡人' }]}
128
+ >
129
+ <Select
130
+ placeholder="搜索并选择持卡人"
131
+ showSearch
132
+ filterOption={false}
133
+ onSearch={(v) => { setPersonSearch(v); loadPersons(v); }}
134
+ notFoundContent={personSearch ? '未找到匹配人员' : null}
135
+ >
136
+ {persons.map((p) => (
137
+ <Option key={p.personId} value={p.personId}>
138
+ {p.personName} - {p.orgName || '-'} {p.jobNo ? `(${p.jobNo})` : ''}
139
+ </Option>
140
+ ))}
141
+ </Select>
142
+ </Form.Item>
143
+
144
+ <Form.Item style={{ marginTop: 24 }}>
145
+ <Space>
146
+ <Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={submitting}>
147
+ 确认开卡
148
+ </Button>
149
+ <Button icon={<ReloadOutlined />} onClick={() => form.resetFields()}>重置</Button>
150
+ <Button onClick={() => navigate('/card')}>取消</Button>
151
+ </Space>
152
+ </Form.Item>
153
+ </Form>
154
+ </Card>
155
+
156
+ {persons.length > 0 && (
157
+ <Card title="选择的人员" style={{ marginTop: 16 }}>
158
+ <Table
159
+ dataSource={persons}
160
+ rowKey="personId"
161
+ pagination={false}
162
+ size="small"
163
+ columns={[
164
+ { title: '姓名', dataIndex: 'personName' },
165
+ { title: '组织', dataIndex: 'orgName' },
166
+ { title: '工号', dataIndex: 'jobNo' },
167
+ ]}
168
+ />
169
+ </Card>
170
+ )}
171
+ </div>
172
+ );
173
+ }