Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
292 lines
13 KiB
TypeScript
292 lines
13 KiB
TypeScript
import React, { useRef, useState, useEffect } from 'react';
|
||
import { useAccess } from '@umijs/max';
|
||
import { Button, message, Modal, Tag, Row, Col, Card, Switch, Statistic, Badge, Dropdown, Space } from 'antd';
|
||
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
|
||
import { PlusOutlined, DeleteOutlined, FormOutlined, QrcodeOutlined, SwapOutlined, ApartmentOutlined, ScanOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||
import {
|
||
getDeviceList,
|
||
addDevice,
|
||
updateDevice,
|
||
deleteDevice,
|
||
assignDevice,
|
||
toggleDeviceStatus,
|
||
getGateStatuses,
|
||
toggleGate,
|
||
getGateEntryLogs,
|
||
simulateGateScan,
|
||
} from '@/services/jobportal/outdoorFairDetail';
|
||
import { getBoothList } from '@/services/jobportal/outdoorFairDetail';
|
||
|
||
interface Props {
|
||
fairId: number;
|
||
}
|
||
|
||
const deviceTypeMap: Record<string, string> = {
|
||
mobile_terminal: '移动终端', qr_sticker: '展位二维码',
|
||
};
|
||
|
||
const DeviceTab: React.FC<Props> = ({ fairId }) => {
|
||
const access = useAccess();
|
||
const mobileActionRef = useRef<ActionType>();
|
||
const qrActionRef = useRef<ActionType>();
|
||
const gateLogActionRef = useRef<ActionType>();
|
||
const [deviceModalOpen, setDeviceModalOpen] = useState(false);
|
||
const [editingDevice, setEditingDevice] = useState<any>(null);
|
||
const [defaultType, setDefaultType] = useState('mobile_terminal');
|
||
const [assignModalOpen, setAssignModalOpen] = useState(false);
|
||
const [assigningDevice, setAssigningDevice] = useState<any>(null);
|
||
const [gateStatuses, setGateStatuses] = useState<any[]>([]);
|
||
const [scanModalOpen, setScanModalOpen] = useState(false);
|
||
const [scanGate, setScanGate] = useState<any>(null);
|
||
|
||
const fetchGateStatuses = async () => {
|
||
const res = await getGateStatuses(fairId);
|
||
setGateStatuses(res.rows || []);
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchGateStatuses();
|
||
}, [fairId]);
|
||
|
||
const handleToggleGate = async (id: number) => {
|
||
const res = await toggleGate(id);
|
||
if (res.code === 200) { message.success(res.msg); fetchGateStatuses(); }
|
||
};
|
||
|
||
const deviceColumns = (type: string): ProColumns<any>[] => [
|
||
{ title: '设备名称', dataIndex: 'deviceName', width: 120, ellipsis: true },
|
||
{ title: '设备编号', dataIndex: 'deviceCode', width: 180, hideInSearch: true },
|
||
{ title: '分配展位', dataIndex: 'assignedBoothNumber', width: 100, hideInSearch: true, render: (_, r) => r.assignedBoothNumber || '--' },
|
||
{ title: '分配企业', dataIndex: 'assignedCompanyName', width: 180, ellipsis: true, hideInSearch: true, render: (_, r) => r.assignedCompanyName || '--' },
|
||
{ title: '状态', dataIndex: 'status', width: 80, hideInSearch: true,
|
||
render: (_, r) => <Badge status={r.status === 'online' ? 'success' : 'default'} text={r.status === 'online' ? '在线' : '离线'} />,
|
||
},
|
||
{ title: '最后活跃', dataIndex: 'lastActiveTime', width: 170, hideInSearch: true, render: (_, r) => r.lastActiveTime || '--' },
|
||
{
|
||
title: '操作', valueType: 'option', width: 120,
|
||
render: (_, record) => {
|
||
const isQrType = type === 'qr_sticker';
|
||
const menuItems = [
|
||
{
|
||
key: 'toggle',
|
||
icon: record.status === 'online' ? <DeleteOutlined /> : <PlusOutlined />,
|
||
label: record.status === 'online' ? '禁用' : '启用',
|
||
onClick: async () => {
|
||
const res = await toggleDeviceStatus(record.id);
|
||
if (res.code === 200) { message.success(res.msg); type === 'mobile_terminal' ? mobileActionRef.current?.reload() : qrActionRef.current?.reload(); }
|
||
},
|
||
},
|
||
{
|
||
key: 'assign',
|
||
icon: <ApartmentOutlined />,
|
||
label: '分配展位',
|
||
onClick: () => { setAssigningDevice(record); setAssignModalOpen(true); },
|
||
},
|
||
...(isQrType ? [{
|
||
key: 'qrcode',
|
||
icon: <QrcodeOutlined />,
|
||
label: '重新生成二维码',
|
||
onClick: () => message.success(`已为「${record.deviceName}」重新生成二维码`),
|
||
}] : []),
|
||
{
|
||
key: 'edit',
|
||
icon: <FormOutlined />,
|
||
label: '编辑',
|
||
onClick: () => { setEditingDevice(record); setDefaultType(record.type); setDeviceModalOpen(true); },
|
||
},
|
||
{ type: 'divider' as const },
|
||
{
|
||
key: 'delete',
|
||
icon: <DeleteOutlined />,
|
||
label: '删除',
|
||
danger: true,
|
||
onClick: () => {
|
||
Modal.confirm({
|
||
title: '确认删除', content: `确定删除「${record.deviceName}」吗?`,
|
||
onOk: async () => {
|
||
const res = await deleteDevice(record.id);
|
||
if (res.code === 200) { message.success('删除成功'); type === 'mobile_terminal' ? mobileActionRef.current?.reload() : qrActionRef.current?.reload(); }
|
||
},
|
||
});
|
||
},
|
||
},
|
||
];
|
||
return (
|
||
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||
<Button type="link" size="small" icon={<EllipsisOutlined />}>操作</Button>
|
||
</Dropdown>
|
||
);
|
||
},
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||
<Col span={24}>
|
||
<Card
|
||
title="移动终端设备"
|
||
extra={
|
||
<Button type="primary" size="small" icon={<PlusOutlined />}
|
||
onClick={() => { setEditingDevice(null); setDefaultType('mobile_terminal'); setDeviceModalOpen(true); }}
|
||
>新增</Button>
|
||
}
|
||
bodyStyle={{ padding: 0 }}
|
||
>
|
||
<ProTable
|
||
actionRef={mobileActionRef}
|
||
rowKey="id"
|
||
search={false}
|
||
options={false}
|
||
pagination={{ pageSize: 5 }}
|
||
columns={deviceColumns('mobile_terminal')}
|
||
request={async (params) => {
|
||
const res = await getDeviceList(fairId, { current: params.current, pageSize: params.pageSize, type: 'mobile_terminal' });
|
||
return { data: res.rows, total: res.total, success: true };
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={24}>
|
||
<Card
|
||
title="展位二维码"
|
||
extra={
|
||
<Button type="primary" size="small" icon={<PlusOutlined />}
|
||
onClick={() => { setEditingDevice(null); setDefaultType('qr_sticker'); setDeviceModalOpen(true); }}
|
||
>新增</Button>
|
||
}
|
||
bodyStyle={{ padding: 0 }}
|
||
>
|
||
<ProTable
|
||
actionRef={qrActionRef}
|
||
rowKey="id"
|
||
search={false}
|
||
options={false}
|
||
pagination={{ pageSize: 5 }}
|
||
columns={deviceColumns('qr_sticker')}
|
||
request={async (params) => {
|
||
const res = await getDeviceList(fairId, { current: params.current, pageSize: params.pageSize, type: 'qr_sticker' });
|
||
return { data: res.rows, total: res.total, success: true };
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
{/* 闸机管理 */}
|
||
<Card title="闸机管理" style={{ marginBottom: 16 }}>
|
||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||
{gateStatuses.map((gate: any) => (
|
||
<Col span={12} key={gate.id}>
|
||
<Card size="small">
|
||
<Space direction="vertical" style={{ width: '100%' }}>
|
||
<Space align="center">
|
||
<span style={{ fontWeight: 'bold' }}>{gate.gateName}</span>
|
||
<Switch checked={gate.isOpen} onChange={() => handleToggleGate(gate.id)}
|
||
checkedChildren="开" unCheckedChildren="关" />
|
||
<Tag color={gate.isOpen ? 'green' : 'default'}>{gate.isOpen ? '已开启' : '已关闭'}</Tag>
|
||
</Space>
|
||
<Statistic title="累计通过人数" value={gate.totalScans} />
|
||
<Button type="primary" size="small" icon={<ScanOutlined />}
|
||
disabled={!gate.isOpen}
|
||
onClick={() => { setScanGate(gate); setScanModalOpen(true); }}
|
||
>扫码</Button>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
<ProTable
|
||
headerTitle="入场记录"
|
||
actionRef={gateLogActionRef}
|
||
rowKey="id"
|
||
search={false}
|
||
options={false}
|
||
pagination={{ pageSize: 8 }}
|
||
columns={[
|
||
{ title: '人员姓名', dataIndex: 'attendeeName' },
|
||
{ title: '通过时间', dataIndex: 'scanTime', valueType: 'dateTime', width: 170 },
|
||
{ title: '闸机名称', dataIndex: 'gateName', width: 130 },
|
||
{ title: '扫码方式', dataIndex: 'scanMethod', width: 100,
|
||
render: (_, r) => r.scanMethod === 'qr' ? '二维码' : '身份证' },
|
||
]}
|
||
request={async (params) => {
|
||
const res = await getGateEntryLogs(fairId, { current: params.current, pageSize: params.pageSize });
|
||
return { data: res.rows, total: res.total, success: true };
|
||
}}
|
||
/>
|
||
</Card>
|
||
|
||
{/* 新增/编辑设备 */}
|
||
<ModalForm
|
||
title={editingDevice ? '编辑设备' : '新增设备'}
|
||
open={deviceModalOpen}
|
||
width={450}
|
||
modalProps={{ destroyOnClose: true, onCancel: () => setDeviceModalOpen(false) }}
|
||
initialValues={editingDevice || { type: defaultType }}
|
||
onFinish={async (values) => {
|
||
const res = editingDevice
|
||
? await updateDevice({ id: editingDevice.id, ...values })
|
||
: await addDevice({ fairId, ...values });
|
||
if (res.code === 200) { message.success(res.msg); setDeviceModalOpen(false); mobileActionRef.current?.reload(); qrActionRef.current?.reload(); return true; }
|
||
message.error(res.msg); return false;
|
||
}}
|
||
>
|
||
<ProFormSelect name="type" label="设备类型" width="md"
|
||
options={[{ label: '移动终端', value: 'mobile_terminal' }, { label: '展位二维码', value: 'qr_sticker' }]}
|
||
disabled={!!editingDevice}
|
||
rules={[{ required: true }]}
|
||
/>
|
||
<ProFormText name="deviceName" label="设备名称" rules={[{ required: true }]} placeholder="如:移动终端-01" />
|
||
<ProFormText name="deviceCode" label="设备编号" rules={[{ required: true }]} placeholder="如:MT-20260315-001" />
|
||
</ModalForm>
|
||
|
||
{/* 分配展位 */}
|
||
<ModalForm
|
||
title={`分配展位 — ${assigningDevice?.deviceName || ''}`}
|
||
open={assignModalOpen}
|
||
width={500}
|
||
modalProps={{ destroyOnClose: true, onCancel: () => setAssignModalOpen(false) }}
|
||
onFinish={async (values) => {
|
||
if (!assigningDevice) return;
|
||
const booth = (await getBoothList(fairId, { current: 1, pageSize: 100 })).rows.find((b: any) => b.id === values.boothId);
|
||
const res = await assignDevice(assigningDevice.id, values.boothId, booth?.boothNumber || '', booth?.companyName || '');
|
||
if (res.code === 200) { message.success(res.msg); setAssignModalOpen(false); mobileActionRef.current?.reload(); qrActionRef.current?.reload(); return true; }
|
||
message.error(res.msg); return false;
|
||
}}
|
||
>
|
||
<ProFormSelect name="boothId" label="选择展位" width="md"
|
||
rules={[{ required: true, message: '请选择展位' }]}
|
||
request={async () => {
|
||
const res = await getBoothList(fairId);
|
||
return res.rows.map((b: any) => ({ label: `${b.boothNumber} (${b.companyName || '未分配'})`, value: b.id }));
|
||
}}
|
||
placeholder="请搜索并选择展位"
|
||
/>
|
||
</ModalForm>
|
||
|
||
{/* 闸机扫码 */}
|
||
<ModalForm
|
||
title={`闸机扫码 — ${scanGate?.gateName || ''}`}
|
||
open={scanModalOpen}
|
||
width={400}
|
||
modalProps={{ destroyOnClose: true, onCancel: () => setScanModalOpen(false) }}
|
||
onFinish={async (values) => {
|
||
if (!scanGate) return;
|
||
const res = await simulateGateScan({ fairId, gateName: scanGate.gateName, ...values });
|
||
if (res.code === 200) { message.success(res.msg); setScanModalOpen(false); gateLogActionRef.current?.reload(); fetchGateStatuses(); return true; }
|
||
message.error(res.msg); return false;
|
||
}}
|
||
>
|
||
<ProFormText name="attendeeName" label="人员姓名" rules={[{ required: true }]} placeholder="请输入姓名" />
|
||
<ProFormSelect name="scanMethod" label="扫码方式" initialValue="qr"
|
||
options={[{ label: '二维码扫码', value: 'qr' }, { label: '身份证刷卡', value: 'id_card' }]}
|
||
rules={[{ required: true }]}
|
||
/>
|
||
</ModalForm>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DeviceTab;
|