Files
shz-admin/src/pages/Jobfair/Outdoorfair/Detail/components/AttendeeTab.tsx
francis-fh e7616b0c8e
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
11
2026-06-18 02:30:11 +08:00

191 lines
8.8 KiB
TypeScript

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;