11
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

This commit is contained in:
francis-fh
2026-06-18 02:30:11 +08:00
parent 4f484f1d00
commit e7616b0c8e
35 changed files with 5815 additions and 19 deletions

View File

@@ -0,0 +1,291 @@
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;