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,190 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, message, Modal, Tag, Drawer, Space } from 'antd';
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect, ProFormDateTimePicker, ProFormTextArea } from '@ant-design/pro-components';
import { ScanOutlined, EditOutlined, ScheduleOutlined, ExportOutlined } from '@ant-design/icons';
import {
getAttendeeList,
scanAttendeeEntry,
updateAttendeeResume,
trackInterview,
} from '@/services/jobportal/outdoorFairDetail';
interface Props {
fairId: number;
}
const checkInMethodMap: Record<string, string> = {
qr_scan: '扫码入场', id_card: '刷身份证', manual: '手动登记',
};
const interviewStatusMap: Record<string, { text: string; color: string }> = {
none: { text: '未面试', color: 'default' },
scheduled: { text: '已预约', color: 'blue' },
in_progress: { text: '面试中', color: 'processing' },
completed: { text: '已完成', color: 'green' },
passed: { text: '已通过', color: 'green' },
failed: { text: '未通过', color: 'red' },
};
const AttendeeTab: React.FC<Props> = ({ fairId }) => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [scanModalOpen, setScanModalOpen] = useState(false);
const [resumeDrawerOpen, setResumeDrawerOpen] = useState(false);
const [currentAttendee, setCurrentAttendee] = useState<any>(null);
const [interviewModalOpen, setInterviewModalOpen] = useState(false);
const columns: ProColumns<any>[] = [
{ title: '姓名', dataIndex: 'name', width: 100 },
{ title: '性别', dataIndex: 'gender', width: 60, hideInSearch: true },
{ title: '手机号', dataIndex: 'phone', width: 130 },
{ title: '身份证号', dataIndex: 'idCard', width: 180, hideInSearch: true },
{ title: '学历', dataIndex: 'education', width: 80, hideInSearch: true },
{ title: '毕业院校', dataIndex: 'graduationSchool', width: 150, hideInSearch: true, ellipsis: true },
{ title: '入场时间', dataIndex: 'checkInTime', valueType: 'dateTime', width: 170, render: (_, r) => r.checkInTime || '未入场' },
{ title: '入场方式', dataIndex: 'checkInMethod', width: 100, hideInSearch: true,
render: (_, r) => checkInMethodMap[r.checkInMethod] || r.checkInMethod },
{ title: '意向企业', dataIndex: 'targetCompanies', width: 180, ellipsis: true, hideInSearch: true },
{ title: '面试状态', dataIndex: 'interviewStatus', width: 100,
render: (_, r) => {
const s = interviewStatusMap[r.interviewStatus] || { text: r.interviewStatus, color: 'default' };
return <Tag color={s.color}>{s.text}</Tag>;
},
},
{
title: '操作', valueType: 'option', width: 220,
render: (_, record) => [
<Button key="scan" type="link" size="small" icon={<ScanOutlined />}
onClick={() => {
if (record.checkInTime) { message.info('该人员已入场'); return; }
Modal.confirm({
title: '确认入场', content: `确认${record.name}扫码入场?`,
onOk: async () => {
const res = await scanAttendeeEntry({
name: record.name, phone: record.phone, idCard: record.idCard, checkInMethod: 'qr_scan',
});
if (res.code === 200) { message.success('扫码入场成功'); actionRef.current?.reload(); }
},
});
}}
>{record.checkInTime ? '已入场' : '扫码入场'}</Button>,
<Button key="resume" type="link" size="small" icon={<EditOutlined />}
onClick={() => { setCurrentAttendee(record); setResumeDrawerOpen(true); }}
></Button>,
<Button key="interview" type="link" size="small" icon={<ScheduleOutlined />}
onClick={() => { setCurrentAttendee(record); setInterviewModalOpen(true); }}
></Button>,
],
},
];
return (
<div>
<ProTable
headerTitle="参会人员列表"
actionRef={actionRef}
rowKey="id"
columns={columns}
search={{ labelWidth: 80 }}
request={async (params) => {
const res = await getAttendeeList(fairId, {
current: params.current, pageSize: params.pageSize,
name: params.name, phone: params.phone, interviewStatus: params.interviewStatus,
});
return { data: res.rows, total: res.total, success: true };
}}
toolBarRender={() => [
<Button key="simulate" type="primary" icon={<ScanOutlined />}
onClick={() => setScanModalOpen(true)}
></Button>,
<Button key="export" icon={<ExportOutlined />}></Button>,
]}
/>
{/* 模拟扫码入场弹窗 */}
<ModalForm
title="模拟扫码入场"
open={scanModalOpen}
width={450}
modalProps={{ destroyOnClose: true, onCancel: () => setScanModalOpen(false) }}
onFinish={async (values) => {
const res = await scanAttendeeEntry(values);
if (res.code === 200) { message.success(res.msg); setScanModalOpen(false); actionRef.current?.reload(); return true; }
message.error(res.msg); return false;
}}
>
<ProFormText name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]} placeholder="请输入姓名" />
<ProFormText name="phone" label="手机号" rules={[{ required: true, message: '请输入手机号' }]} placeholder="请输入手机号" />
<ProFormText name="idCard" label="身份证号" rules={[{ required: true }]} placeholder="请输入身份证号" />
<ProFormSelect name="checkInMethod" label="入场方式" initialValue="qr_scan"
options={[
{ label: '扫码入场', value: 'qr_scan' },
{ label: '刷身份证', value: 'id_card' },
{ label: '手动登记', value: 'manual' },
]}
rules={[{ required: true }]}
/>
</ModalForm>
{/* 简历编辑抽屉 */}
<Drawer
title={`编辑简历 — ${currentAttendee?.name || ''}`}
width={500}
open={resumeDrawerOpen}
onClose={() => setResumeDrawerOpen(false)}
destroyOnClose
>
<ModalForm
title={null}
open
submitter={{ searchConfig: { submitText: '保存简历' } }}
modalProps={{ footer: null }}
initialValues={currentAttendee}
onFinish={async (values) => {
if (!currentAttendee) return;
const res = await updateAttendeeResume(currentAttendee.id, values);
if (res.code === 200) { message.success('简历更新成功'); setResumeDrawerOpen(false); actionRef.current?.reload(); return true; }
message.error(res.msg); return false;
}}
>
<ProFormSelect name="education" label="学历" width="md"
options={['高中', '大专', '本科', '硕士', '博士'].map(v => ({ label: v, value: v }))}
/>
<ProFormText name="graduationSchool" label="毕业院校" width="md" placeholder="请输入毕业院校" />
<ProFormText name="major" label="专业" width="md" placeholder="请输入专业" />
<ProFormTextArea name="resumeContent" label="简历内容" placeholder="请输入简历内容" />
</ModalForm>
</Drawer>
{/* 面试跟踪弹窗 */}
<ModalForm
title={`面试跟踪 — ${currentAttendee?.name || ''}`}
open={interviewModalOpen}
width={500}
modalProps={{ destroyOnClose: true, onCancel: () => setInterviewModalOpen(false) }}
initialValues={currentAttendee}
onFinish={async (values) => {
if (!currentAttendee) return;
const res = await trackInterview({ attendeeId: currentAttendee.id, ...values });
if (res.code === 200) { message.success('面试信息更新成功'); setInterviewModalOpen(false); actionRef.current?.reload(); return true; }
message.error(res.msg); return false;
}}
>
<ProFormSelect name="interviewStatus" label="面试状态" width="md"
options={[
{ label: '未面试', value: 'none' }, { label: '已预约', value: 'scheduled' },
{ label: '面试中', value: 'in_progress' }, { label: '已完成', value: 'completed' },
{ label: '已通过', value: 'passed' }, { label: '未通过', value: 'failed' },
]}
rules={[{ required: true }]}
/>
<ProFormText name="interviewCompany" label="面试企业" width="md" placeholder="请输入面试企业" />
<ProFormDateTimePicker name="interviewTime" label="面试时间" width="md" fieldProps={{ format: 'YYYY-MM-DD HH:mm:ss' }} />
<ProFormTextArea name="interviewResult" label="面试结果" placeholder="请输入面试结果或备注" />
</ModalForm>
</div>
);
};
export default AttendeeTab;

View File

@@ -0,0 +1,181 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, message, Modal, Tag, Dropdown } from 'antd';
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components';
import { PlusOutlined, DeleteOutlined, FormOutlined, CheckOutlined, CloseOutlined, SendOutlined, EllipsisOutlined, ApartmentOutlined } from '@ant-design/icons';
import {
getBoothList,
addBooth,
updateBooth,
deleteBooth,
reviewBooth,
assignBooth,
} from '@/services/jobportal/outdoorFairDetail';
import { getCompanyRegistrationList } from '@/services/jobportal/outdoorFairDetail';
interface Props {
fairId: number;
}
const reviewStatusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待审核', color: 'default' },
approved: { text: '已通过', color: 'green' },
rejected: { text: '已拒绝', color: 'red' },
};
const BoothTab: React.FC<Props> = ({ fairId }) => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [modalOpen, setModalOpen] = useState(false);
const [editingBooth, setEditingBooth] = useState<any>(null);
const [assignModalOpen, setAssignModalOpen] = useState(false);
const [assigningBooth, setAssigningBooth] = useState<any>(null);
const columns: ProColumns<any>[] = [
{ title: '展位号', dataIndex: 'boothNumber', width: 100 },
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true, render: (_, r) => r.companyName || '--' },
{ title: '岗位数', dataIndex: 'jobCount', width: 80, hideInSearch: true },
{ title: '审核状态', dataIndex: 'reviewStatus', width: 100,
render: (_, r) => {
const s = reviewStatusMap[r.reviewStatus] || { text: r.reviewStatus, color: 'default' };
return <Tag color={s.color}>{s.text}</Tag>;
},
},
{ title: '分配时间', dataIndex: 'assignedTime', valueType: 'dateTime', width: 170, hideInSearch: true, render: (_, r) => r.assignedTime || '--' },
{
title: '操作', valueType: 'option', width: 150,
render: (_, record) => {
const menuItems = [
{
key: 'assign',
icon: <ApartmentOutlined />,
label: '分配展位',
onClick: () => { setAssigningBooth(record); setAssignModalOpen(true); },
},
{
key: 'edit',
icon: <FormOutlined />,
label: '编辑',
onClick: () => { setEditingBooth(record); setModalOpen(true); },
},
{
key: 'notify',
icon: <SendOutlined />,
label: '发送通知',
disabled: !record.companyName,
onClick: () => {
if (!record.companyName) { message.warning('该展位尚未分配企业'); return; }
message.success(`已向「${record.companyName}」发送展位信息通知(模拟)`);
},
},
{ type: 'divider' as const },
{
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
danger: true,
onClick: () => {
Modal.confirm({
title: '确认删除', content: `确定删除展位「${record.boothNumber}」吗?`,
onOk: async () => {
const res = await deleteBooth(record.id);
if (res.code === 200) { message.success('删除成功'); actionRef.current?.reload(); }
},
});
},
},
];
return [
record.reviewStatus === 'pending' && (
<Button key="approve" type="link" size="small" style={{ color: 'green' }}
onClick={() => handleReview(record.id, 'approved')}
></Button>
),
record.reviewStatus === 'pending' && (
<Button key="reject" type="link" size="small" danger
onClick={() => handleReview(record.id, 'rejected')}
></Button>
),
<Dropdown key="more" menu={{ items: menuItems }} trigger={['click']}>
<Button type="link" size="small" icon={<EllipsisOutlined />} />
</Dropdown>,
];
},
},
];
const handleReview = async (id: number, status: string) => {
const res = await reviewBooth(id, status);
if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); }
};
return (
<div>
<ProTable
headerTitle="展位列表"
actionRef={actionRef}
rowKey="id"
columns={columns}
search={{ labelWidth: 100 }}
request={async (params) => {
const res = await getBoothList(fairId, {
current: params.current, pageSize: params.pageSize,
boothNumber: params.boothNumber, reviewStatus: params.reviewStatus,
});
return { data: res.rows, total: res.total, success: true };
}}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:add')}
onClick={() => { setEditingBooth(null); setModalOpen(true); }}
></Button>,
]}
/>
{/* 新增/编辑展位 */}
<ModalForm
title={editingBooth ? '编辑展位' : '新增展位'}
open={modalOpen}
width={400}
modalProps={{ destroyOnClose: true, onCancel: () => setModalOpen(false) }}
onFinish={async (values) => {
const res = editingBooth
? await updateBooth({ id: editingBooth.id, ...values })
: await addBooth({ fairId, ...values });
if (res.code === 200) { message.success(res.msg); setModalOpen(false); actionRef.current?.reload(); return true; }
message.error(res.msg); return false;
}}
initialValues={editingBooth}
>
<ProFormText name="boothNumber" label="展位号" rules={[{ required: true, message: '请输入展位号' }]} placeholder="如A01" />
</ModalForm>
{/* 分配展位给企业 */}
<ModalForm
title={`分配展位 — ${assigningBooth?.boothNumber || ''}`}
open={assignModalOpen}
width={500}
modalProps={{ destroyOnClose: true, onCancel: () => setAssignModalOpen(false) }}
onFinish={async (values) => {
if (!assigningBooth) return false;
const res = await assignBooth({
fairId, boothId: assigningBooth.id, companyId: values.companyId, boothNumber: assigningBooth.boothNumber,
});
if (res.code === 200) { message.success(res.msg); setAssignModalOpen(false); actionRef.current?.reload(); return true; }
message.error(res.msg); return false;
}}
>
<ProFormSelect name="companyId" label="选择企业" width="md"
rules={[{ required: true, message: '请选择企业' }]}
request={async () => {
const res = await getCompanyRegistrationList(fairId, { reviewStatus: 'approved' });
return res.rows.map((r: any) => ({ label: r.companyName, value: r.companyId }));
}}
placeholder="请搜索并选择企业"
/>
</ModalForm>
</div>
);
};
export default BoothTab;

View File

@@ -0,0 +1,80 @@
import React, { useRef, useState } from 'react';
import { Modal, message } from 'antd';
import { ActionType, ProTable, ProColumns } from '@ant-design/pro-components';
import { addParticipatingCompany } from '@/services/jobportal/outdoorFairDetail';
interface Props {
open: boolean;
fairId: number;
onCancel: () => void;
onSuccess: () => void;
}
/** 模拟可添加的企业列表 */
const MOCK_AVAILABLE_COMPANIES = [
{ companyId: 1013, companyName: '石河子市天富集团', industry: '能源', scale: '1000人以上', contactPerson: '黄经理', contactPhone: '13309930001' },
{ companyId: 1014, companyName: '新疆兵团勘察设计院', industry: '建筑/工程', scale: '200-500人', contactPerson: '曹院长', contactPhone: '13309930002' },
{ companyId: 1015, companyName: '石河子市商业银行', industry: '金融', scale: '500-1000人', contactPerson: '钱行长', contactPhone: '13309930003' },
{ companyId: 1016, companyName: '兵团广播电视大学', industry: '教育/科研', scale: '200-500人', contactPerson: '杨校长', contactPhone: '13309930004' },
{ companyId: 1017, companyName: '新疆如意纺织服装有限公司', industry: '纺织/服装', scale: '1000人以上', contactPerson: '林经理', contactPhone: '13309930005' },
{ companyId: 1018, companyName: '石河子市人民医院', industry: '医疗/卫生', scale: '1000人以上', contactPerson: '郝主任', contactPhone: '13309930006' },
{ companyId: 1019, companyName: '兵团信息技术服务中心', industry: 'IT/互联网', scale: '100-200人', contactPerson: '孙主管', contactPhone: '13309930007' },
{ companyId: 1020, companyName: '新疆燕京啤酒有限公司', industry: '快消/食品', scale: '500-1000人', contactPerson: '吕厂长', contactPhone: '13309930008' },
];
const CompanyAddModal: React.FC<Props> = ({ open, fairId, onCancel, onSuccess }) => {
const actionRef = useRef<ActionType>();
const [selectedRows, setSelectedRows] = useState<any[]>([]);
const columns: ProColumns<any>[] = [
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true },
{ title: '行业', dataIndex: 'industry', width: 80 },
{ title: '规模', dataIndex: 'scale', width: 100 },
{ title: '联系人', dataIndex: 'contactPerson', width: 80 },
{ title: '联系电话', dataIndex: 'contactPhone', width: 130 },
];
const handleOk = async () => {
if (selectedRows.length === 0) {
message.warning('请选择至少一家企业');
return;
}
let success = 0;
for (const row of selectedRows) {
const res = await addParticipatingCompany({ fairId, ...row });
if (res.code === 200) success++;
}
message.success(`成功添加 ${success} 家企业`);
setSelectedRows([]);
onSuccess();
};
return (
<Modal
title="添加参会企业"
open={open}
width={800}
destroyOnClose
onCancel={() => { setSelectedRows([]); onCancel(); }}
onOk={handleOk}
okText="确认添加"
>
<ProTable
actionRef={actionRef}
rowKey="companyId"
search={false}
options={false}
pagination={{ pageSize: 6 }}
columns={columns}
dataSource={MOCK_AVAILABLE_COMPANIES}
rowSelection={{
selectedRowKeys: selectedRows.map(r => r.companyId),
onChange: (_, rows) => setSelectedRows(rows),
}}
tableAlertRender={false}
/>
</Modal>
);
};
export default CompanyAddModal;

View File

@@ -0,0 +1,178 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, message, Modal, Tag, Space } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { CheckOutlined, CloseOutlined, QrcodeOutlined, SendOutlined } from '@ant-design/icons';
import {
getCompanyRegistrationList,
reviewCompanyRegistration,
reviewRegisteredJob,
qrCheckIn,
} from '@/services/jobportal/outdoorFairDetail';
interface Props {
fairId: number;
}
const reviewStatusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待审核', color: 'default' },
approved: { text: '已通过', color: 'green' },
rejected: { text: '已拒绝', color: 'red' },
};
const CompanyReviewTab: React.FC<Props> = ({ fairId }) => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [rejectModalOpen, setRejectModalOpen] = useState(false);
const [rejectTarget, setRejectTarget] = useState<any>(null);
const handleReviewCompany = async (id: number, status: string) => {
if (status === 'rejected') {
Modal.confirm({
title: '确认拒绝', content: '确定拒绝该单位的参会申请吗?',
onOk: async () => {
const res = await reviewCompanyRegistration(id, status);
if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); }
},
});
} else {
const res = await reviewCompanyRegistration(id, status);
if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); }
}
};
const handleReviewJob = async (regId: number, jobId: number, status: string) => {
const res = await reviewRegisteredJob(regId, jobId, status);
if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); }
};
const handleQrCheckIn = async (id: number) => {
Modal.confirm({
title: '模拟扫码签到',
content: '确认该单位已完成现场扫码签到吗?(模拟操作)',
onOk: async () => {
const res = await qrCheckIn(id);
if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); }
},
});
};
const handleNotify = (companyName: string, method: string) => {
message.success(`已通过${method}向「${companyName}」发送审核结果通知(模拟)`);
};
const columns: ProColumns<any>[] = [
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true },
{ title: '行业', dataIndex: 'industry', width: 80, hideInSearch: true, render: (_, r) => <Tag>{r.industry}</Tag> },
{ title: '规模', dataIndex: 'scale', width: 100, hideInSearch: true },
{ title: '联系人', dataIndex: 'contactPerson', width: 100, hideInSearch: true },
{ title: '联系电话', dataIndex: 'contactPhone', width: 130, hideInSearch: true },
{ title: '审核状态', dataIndex: 'reviewStatus', width: 100,
render: (_, r) => {
const s = reviewStatusMap[r.reviewStatus] || { text: r.reviewStatus, color: 'default' };
return <Tag color={s.color}>{s.text}</Tag>;
},
},
{ title: '签到状态', dataIndex: 'qrCheckInStatus', width: 100,
render: (_, r) => <Tag color={r.qrCheckInStatus === 'checked_in' ? 'green' : 'default'}>
{r.qrCheckInStatus === 'checked_in' ? '已签到' : '未签到'}
</Tag>,
},
{ title: '签到时间', dataIndex: 'checkInTime', valueType: 'dateTime', width: 170, hideInSearch: true, render: (_, r) => r.checkInTime || '--' },
{
title: '操作', valueType: 'option', width: 300,
render: (_, record) => [
record.reviewStatus === 'pending' && (
<Button key="approve" type="link" size="small" icon={<CheckOutlined />} style={{ color: 'green' }}
onClick={() => handleReviewCompany(record.id, 'approved')}
></Button>
),
record.reviewStatus === 'pending' && (
<Button key="reject" type="link" size="small" danger icon={<CloseOutlined />}
onClick={() => handleReviewCompany(record.id, 'rejected')}
></Button>
),
record.reviewStatus === 'approved' && (
<>
<Button key="notifySms" type="link" size="small" icon={<SendOutlined />}
onClick={() => handleNotify(record.companyName, '短信')}
></Button>
<Button key="notifyMsg" type="link" size="small" icon={<SendOutlined />}
onClick={() => handleNotify(record.companyName, '站内信')}
></Button>
{record.qrCheckInStatus !== 'checked_in' && (
<Button key="qrcode" type="link" size="small" icon={<QrcodeOutlined />}
onClick={() => handleQrCheckIn(record.id)}
></Button>
)}
</>
),
],
},
];
// 展开行:岗位审核
const expandedRowRender = (record: any) => {
if (!record.registeredJobs || record.registeredJobs.length === 0) {
return <div style={{ padding: '16px 24px', color: '#999' }}></div>;
}
return (
<ProTable
rowKey="id"
headerTitle={false}
search={false}
options={false}
pagination={false}
dataSource={record.registeredJobs}
columns={[
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
{ title: '薪资范围', width: 150,
render: (_, r: any) => `${r.minSalary}K-${r.maxSalary}K` },
{ title: '学历要求', dataIndex: 'education', width: 80 },
{ title: '招聘人数', dataIndex: 'vacancies', width: 80 },
{ title: '审核状态', dataIndex: 'reviewStatus', width: 100,
render: (_, r: any) => {
const s = reviewStatusMap[r.reviewStatus] || { text: r.reviewStatus, color: 'default' };
return <Tag color={s.color}>{s.text}</Tag>;
},
},
{
title: '操作', valueType: 'option', width: 150,
render: (_, jobRecord: any) => [
jobRecord.reviewStatus === 'pending' && (
<Button key="approveJob" type="link" size="small" icon={<CheckOutlined />} style={{ color: 'green' }}
onClick={() => handleReviewJob(record.id, jobRecord.id, 'approved')}
></Button>
),
jobRecord.reviewStatus === 'pending' && (
<Button key="rejectJob" type="link" size="small" danger icon={<CloseOutlined />}
onClick={() => handleReviewJob(record.id, jobRecord.id, 'rejected')}
></Button>
),
],
},
]}
/>
);
};
return (
<ProTable
headerTitle="参会单位审核"
actionRef={actionRef}
rowKey="id"
columns={columns}
search={{ labelWidth: 100 }}
expandable={{ expandedRowRender }}
request={async (params) => {
const res = await getCompanyRegistrationList(fairId, {
current: params.current, pageSize: params.pageSize,
companyName: params.companyName, reviewStatus: params.reviewStatus,
});
return { data: res.rows, total: res.total, success: true };
}}
/>
);
};
export default CompanyReviewTab;

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;

View File

@@ -0,0 +1,234 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, Card, Descriptions, List, Tag, message, Modal, Space } from 'antd';
import { ActionType, ProTable } from '@ant-design/pro-components';
import { PlusOutlined, DeleteOutlined, FormOutlined } from '@ant-design/icons';
import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair';
import { updateOutdoorFair } from '@/services/jobportal/outdoorFair';
import {
getParticipatingCompanies,
addParticipatingCompany,
removeParticipatingCompany,
addJobForCompany,
updateJobForCompany,
removeJobFromCompany,
} from '@/services/jobportal/outdoorFairDetail';
import EditModal from '@/pages/Jobfair/Outdoorfair/components/EditModal';
import CompanyAddModal from './CompanyAddModal';
import JobEditModal from './JobEditModal';
interface Props {
fairId: number;
fairInfo: OutdoorFairItem;
onFairUpdate: () => void;
}
const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, onFairUpdate }) => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [editModalOpen, setEditModalOpen] = useState(false);
const [companyModalOpen, setCompanyModalOpen] = useState(false);
const [jobModalOpen, setJobModalOpen] = useState(false);
const [selectedCompany, setSelectedCompany] = useState<any>(null);
const [editingJob, setEditingJob] = useState<any>(null);
const fairTypeColors: Record<string, string> = {
'综合类': 'blue', '校园招聘': 'green', '行业专场': 'orange',
'高新技术专场': 'purple', '智能制造专场': 'cyan', '文旅专场': 'magenta',
'其他': 'default',
};
return (
<div>
{/* 基本信息卡片 */}
<Card
title="基本信息"
extra={
<Button
type="primary"
size="small"
icon={<FormOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:edit')}
onClick={() => setEditModalOpen(true)}
>
</Button>
}
style={{ marginBottom: 16 }}
>
<Descriptions column={3} bordered size="small">
<Descriptions.Item label="招聘会标题" span={3}>{fairInfo.title}</Descriptions.Item>
<Descriptions.Item label="举办单位">{fairInfo.hostUnit}</Descriptions.Item>
<Descriptions.Item label="招聘会类型">
<Tag color={fairTypeColors[fairInfo.fairType] || 'default'}>{fairInfo.fairType}</Tag>
</Descriptions.Item>
<Descriptions.Item label="举办区域">{fairInfo.region}</Descriptions.Item>
<Descriptions.Item label="举办地址" span={2}>{fairInfo.address}</Descriptions.Item>
<Descriptions.Item label="展位数量">{fairInfo.boothCount}</Descriptions.Item>
<Descriptions.Item label="举办时间">{fairInfo.holdTime}</Descriptions.Item>
<Descriptions.Item label="截止时间">{fairInfo.endTime}</Descriptions.Item>
<Descriptions.Item label="开放申请">{fairInfo.applyStartTime}</Descriptions.Item>
<Descriptions.Item label="截止申请">{fairInfo.applyEndTime}</Descriptions.Item>
<Descriptions.Item label="线上申请">{fairInfo.onlineApply ? '是' : '否'}</Descriptions.Item>
</Descriptions>
</Card>
{/* 参会企业及岗位 */}
<Card
title="参会企业及岗位"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCompanyModalOpen(true)}
>
</Button>
}
>
<ProTable
actionRef={actionRef}
rowKey="id"
search={false}
options={false}
pagination={{ pageSize: 10 }}
request={async (params) => {
const res = await getParticipatingCompanies(fairId, {
current: params.current,
pageSize: params.pageSize,
companyName: params.companyName,
});
return { data: res.rows, total: res.total, success: true };
}}
expandable={{
expandedRowRender: (record) => (
<div style={{ padding: '0 24px' }}>
{record.jobList.length === 0 ? (
<p style={{ color: '#999' }}></p>
) : (
<List
dataSource={record.jobList}
renderItem={(job: any) => (
<List.Item
actions={[
<Button
key="edit" type="link" size="small"
onClick={() => { setEditingJob(job); setSelectedCompany(record); setJobModalOpen(true); }}
>
</Button>,
<Button
key="delete" type="link" size="small" danger
onClick={() => {
Modal.confirm({
title: '确认删除',
content: `确定删除岗位「${job.jobTitle}」吗?`,
onOk: async () => {
const res = await removeJobFromCompany(job.id);
if (res.code === 200) { message.success('删除成功'); actionRef.current?.reload(); }
},
});
}}
>
</Button>,
]}
>
<List.Item.Meta
title={job.jobTitle}
description={
<Space>
<Tag color="blue"> {job.minSalary}-{job.maxSalary}K</Tag>
<Tag>{job.education}</Tag>
<Tag>{job.experience}</Tag>
<Tag> {job.vacancies} </Tag>
<Tag color={job.status === 'active' ? 'green' : 'red'}>
{job.status === 'active' ? '在招' : '已关闭'}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</div>
),
}}
columns={[
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true },
{ title: '行业', dataIndex: 'industry', width: 100, hideInSearch: true,
render: (_, r: any) => <Tag>{r.industry}</Tag> },
{ title: '规模', dataIndex: 'scale', width: 110, hideInSearch: true },
{ title: '联系人', dataIndex: 'contactPerson', width: 100, hideInSearch: true },
{ title: '联系电话', dataIndex: 'contactPhone', width: 130, hideInSearch: true },
{ title: '展位号', dataIndex: 'boothNumber', width: 80, hideInSearch: true,
render: (_, r: any) => r.boothNumber || '--' },
{ title: '岗位数', width: 80, hideInSearch: true,
render: (_, r: any) => r.jobList?.length || 0 },
{
title: '操作', valueType: 'option', width: 200,
render: (_, record: any) => [
<Button key="addJob" type="link" size="small"
onClick={() => { setEditingJob(null); setSelectedCompany(record); setJobModalOpen(true); }}
>
</Button>,
<Button key="remove" type="link" size="small" danger
onClick={() => {
Modal.confirm({
title: '确认移除', content: `确定移除「${record.companyName}」吗?`,
onOk: async () => {
const res = await removeParticipatingCompany(record.id);
if (res.code === 200) { message.success('移除成功'); actionRef.current?.reload(); }
},
});
}}
>
</Button>,
],
},
]}
/>
</Card>
{/* 编辑招聘会基本信息弹窗 */}
<EditModal
open={editModalOpen}
values={fairInfo}
onCancel={() => setEditModalOpen(false)}
onSubmit={async (values) => {
const res = await updateOutdoorFair(values as any);
if (res.code === 200) {
message.success('修改成功');
setEditModalOpen(false);
onFairUpdate();
} else {
message.error(res.msg || '操作失败');
}
}}
/>
{/* 添加企业弹窗 */}
<CompanyAddModal
open={companyModalOpen}
fairId={fairId}
onCancel={() => setCompanyModalOpen(false)}
onSuccess={() => { setCompanyModalOpen(false); actionRef.current?.reload(); }}
/>
{/* 添加/编辑岗位弹窗 */}
<JobEditModal
open={jobModalOpen}
fairId={fairId}
company={selectedCompany}
job={editingJob}
onCancel={() => { setJobModalOpen(false); setEditingJob(null); setSelectedCompany(null); }}
onSuccess={() => { setJobModalOpen(false); setEditingJob(null); actionRef.current?.reload(); }}
/>
</div>
);
};
export default FairInfoTab;

View File

@@ -0,0 +1,65 @@
import React, { useEffect } from 'react';
import { ModalForm, ProForm, ProFormText, ProFormDigit, ProFormSelect } from '@ant-design/pro-components';
import { Form } from 'antd';
import { addJobForCompany, updateJobForCompany } from '@/services/jobportal/outdoorFairDetail';
interface Props {
open: boolean;
fairId: number;
company: any;
job: any;
onCancel: () => void;
onSuccess: () => void;
}
const JobEditModal: React.FC<Props> = ({ open, fairId, company, job, onCancel, onSuccess }) => {
const [form] = Form.useForm();
useEffect(() => {
if (open) {
form.resetFields();
if (job) {
form.setFieldsValue(job);
}
}
}, [open, job, form]);
return (
<ModalForm
title={job ? '编辑岗位' : `为「${company?.companyName || ''}」添加岗位`}
form={form}
open={open}
width={500}
modalProps={{ destroyOnClose: true, onCancel }}
onFinish={async (values) => {
if (job) {
const res = await updateJobForCompany({ ...job, ...values });
if (res.code === 200) { onSuccess(); return true; }
} else if (company) {
const res = await addJobForCompany({ fairId, companyId: company.id, ...values });
if (res.code === 200) { onSuccess(); return true; }
}
return false;
}}
>
<ProFormText name="jobTitle" label="岗位名称" rules={[{ required: true, message: '请输入岗位名称' }]} placeholder="请输入岗位名称" />
<ProForm.Group>
<ProFormDigit name="minSalary" label="最低薪资(K)" width="sm" min={0} rules={[{ required: true }]} fieldProps={{ precision: 0 }} />
<ProFormDigit name="maxSalary" label="最高薪资(K)" width="sm" min={0} rules={[{ required: true }]} fieldProps={{ precision: 0 }} />
</ProForm.Group>
<ProForm.Group>
<ProFormSelect name="education" label="学历要求" width="sm"
options={['高中', '大专', '本科', '硕士', '博士', '不限'].map(v => ({ label: v, value: v }))}
rules={[{ required: true }]}
/>
<ProFormSelect name="experience" label="经验要求" width="sm"
options={['不限', '应届生', '1-3年', '3-5年', '5-10年', '10年以上'].map(v => ({ label: v, value: v }))}
rules={[{ required: true }]}
/>
</ProForm.Group>
<ProFormDigit name="vacancies" label="招聘人数" width="md" min={1} rules={[{ required: true }]} fieldProps={{ precision: 0 }} />
</ModalForm>
);
};
export default JobEditModal;

View File

@@ -0,0 +1,108 @@
import React from 'react';
import { Card, Col, Row, Statistic, Empty, Spin, Table } from 'antd';
import { TeamOutlined, FileTextOutlined, UserOutlined, LinkOutlined } from '@ant-design/icons';
interface Props {
fairId: number;
}
const StatisticsTab: React.FC<Props> = () => {
// 静态演示数据
const companyCount = 25;
const jobCount = 86;
const demandCount = 520;
const intentionCount = 180;
const dailyData = [
{ key: '1', date: '2026-03-15', count: 320 },
];
const industryData = [
{ key: '1', industry: '化工', count: 5 },
{ key: '2', industry: '政府', count: 2 },
{ key: '3', industry: '农业', count: 4 },
{ key: '4', industry: '医疗', count: 3 },
{ key: '5', industry: '教育', count: 3 },
{ key: '6', industry: '制造', count: 4 },
];
const jobTypeData = [
{ key: '1', type: '技术类', count: 30 },
{ key: '2', type: '管理类', count: 15 },
{ key: '3', type: '销售类', count: 12 },
{ key: '4', type: '行政类', count: 10 },
{ key: '5', type: '一线工人', count: 14 },
{ key: '6', type: '其他', count: 5 },
];
const timeSlotData = [
{ key: '1', slot: '08:00-09:00', count: 80 },
{ key: '2', slot: '09:00-10:00', count: 120 },
{ key: '3', slot: '10:00-11:00', count: 65 },
{ key: '4', slot: '11:00-12:00', count: 30 },
];
return (
<div>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic title="参会单位数" value={companyCount} prefix={<TeamOutlined />} suffix="家" />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="岗位数量" value={jobCount} prefix={<FileTextOutlined />} suffix="个" />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="需求人数" value={demandCount} prefix={<UserOutlined />} suffix="人" />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="意向达成数" value={intentionCount} prefix={<LinkOutlined />} suffix="人" />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Card title="每日入场人数趋势">
<Table rowKey="key" columns={[
{ title: '日期', dataIndex: 'date' },
{ title: '入场人数', dataIndex: 'count', render: (v: number) => `${v}` },
]} dataSource={dailyData} pagination={false} size="small" />
</Card>
</Col>
<Col span={12}>
<Card title="行业分布">
<Table rowKey="key" columns={[
{ title: '行业', dataIndex: 'industry' },
{ title: '企业数', dataIndex: 'count', render: (v: number) => `${v}` },
]} dataSource={industryData} pagination={false} size="small" />
</Card>
</Col>
<Col span={12}>
<Card title="岗位类型分布">
<Table rowKey="key" columns={[
{ title: '岗位类型', dataIndex: 'type' },
{ title: '数量', dataIndex: 'count', render: (v: number) => `${v}` },
]} dataSource={jobTypeData} pagination={false} size="small" />
</Card>
</Col>
<Col span={12}>
<Card title="时段人流量分布">
<Table rowKey="key" columns={[
{ title: '时段', dataIndex: 'slot' },
{ title: '人数', dataIndex: 'count', render: (v: number) => `${v}` },
]} dataSource={timeSlotData} pagination={false} size="small" />
</Card>
</Col>
</Row>
</div>
);
};
export default StatisticsTab;