Files
shz-admin/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx
francis-fh 3b1877f302
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
招聘会三个模块开发
2026-06-24 09:06:09 +08:00

292 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;