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
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:
291
src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx
Normal file
291
src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user