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:
190
src/pages/Jobfair/Outdoorfair/Detail/components/AttendeeTab.tsx
Normal file
190
src/pages/Jobfair/Outdoorfair/Detail/components/AttendeeTab.tsx
Normal 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;
|
||||
181
src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx
Normal file
181
src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
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;
|
||||
234
src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx
Normal file
234
src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user