feat: implement participating companies and jobs tabs in outdoor fair detail
- Add ParticipatingCompaniesTab component to manage participating companies. - Add ParticipatingJobsTab component to manage jobs associated with participating companies. - Remove StatisticsTab component as it is no longer needed. - Update OutdoorFairDetail to include new tabs for participating companies and jobs. - Modify API services to support fetching and managing participating companies and jobs. - Update types to reflect changes in job and company data structures.
This commit is contained in:
@@ -150,6 +150,9 @@ export default defineConfig({
|
||||
],
|
||||
mfsu: {
|
||||
strategy: 'normal',
|
||||
// react-rnd 由招聘会展位图按需加载。避免旧的 MFSU 容器缺少该模块时,
|
||||
// 详情页运行时报 “Module ./react-rnd does not exist in container”。
|
||||
exclude: ['react-rnd'],
|
||||
},
|
||||
outputPath: 'shihezi',
|
||||
base: '/shihezi/',
|
||||
|
||||
@@ -236,6 +236,11 @@ export default [
|
||||
path: '/jobfair/venue-info',
|
||||
component: './Jobfair/Venueinfo',
|
||||
},
|
||||
{
|
||||
name: '场地信息详情',
|
||||
path: '/jobfair/venue-info/detail',
|
||||
component: './Jobfair/Venueinfo/Detail',
|
||||
},
|
||||
{
|
||||
name: '户外招聘会详情',
|
||||
path: '/jobfair/outdoor-fair/detail',
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
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,
|
||||
hideInTable: true,
|
||||
fieldProps: { placeholder: '请输入姓名' },
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
width: 130,
|
||||
hideInTable: true,
|
||||
fieldProps: { placeholder: '请输入手机号' },
|
||||
},
|
||||
{
|
||||
title: '面试状态',
|
||||
dataIndex: 'interviewStatus',
|
||||
hideInTable: true,
|
||||
valueType: 'select',
|
||||
options: [
|
||||
{ label: '未面试', value: 'none' },
|
||||
{ label: '已预约', value: 'scheduled' },
|
||||
{ label: '面试中', value: 'in_progress' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已通过', value: 'passed' },
|
||||
{ label: '未通过', value: 'failed' },
|
||||
],
|
||||
},
|
||||
{ title: '姓名', dataIndex: 'name', width: 100, hideInSearch: true },
|
||||
{ title: '性别', dataIndex: 'gender', width: 60, hideInSearch: true },
|
||||
{ title: '手机号', dataIndex: 'phone', width: 130, hideInSearch: true },
|
||||
{ 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, hideInSearch: true, 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, hideInSearch: true,
|
||||
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;
|
||||
@@ -1,196 +0,0 @@
|
||||
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,
|
||||
hideInTable: true,
|
||||
fieldProps: { placeholder: '请输入企业名称' },
|
||||
},
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'reviewStatus',
|
||||
hideInTable: true,
|
||||
valueType: 'select',
|
||||
options: [
|
||||
{ label: '待审核', value: 'pending' },
|
||||
{ label: '已通过', value: 'approved' },
|
||||
{ label: '已拒绝', value: 'rejected' },
|
||||
],
|
||||
},
|
||||
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true, hideInSearch: 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, hideInSearch: true,
|
||||
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, hideInSearch: true,
|
||||
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;
|
||||
@@ -1,291 +0,0 @@
|
||||
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;
|
||||
@@ -1,54 +1,43 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { history, 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 { Button, Card, Descriptions, Image, Tag, message } from 'antd';
|
||||
import { FormOutlined } from '@ant-design/icons';
|
||||
import type { OutdoorFairDictForm, OutdoorFairItem } from '@/services/jobportal/outdoorFair';
|
||||
import { addOutdoorFairDictOption, updateOutdoorFair } from '@/services/jobportal/outdoorFair';
|
||||
import { getVenueInfoList } from '@/services/jobportal/venueInfo';
|
||||
import { getDictSelectOption } from '@/services/system/dict';
|
||||
import {
|
||||
getParticipatingCompanies,
|
||||
addParticipatingCompany,
|
||||
removeParticipatingCompany,
|
||||
addJobForCompany,
|
||||
updateJobForCompany,
|
||||
removeJobFromCompany,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
import { getVenueInfo, getVenueInfoList } from '@/services/jobportal/venueInfo';
|
||||
import type { VenueInfoItem } from '@/services/jobportal/venueInfo';
|
||||
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
|
||||
import EditModal from '@/pages/Jobfair/Outdoorfair/components/EditModal';
|
||||
import CompanyAddModal from './CompanyAddModal';
|
||||
import JobEditModal from './JobEditModal';
|
||||
|
||||
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
|
||||
const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region';
|
||||
const VENUE_INFO_TYPE_DICT = 'venue_info_type';
|
||||
|
||||
interface Props {
|
||||
fairId: number;
|
||||
fairInfo: OutdoorFairItem;
|
||||
refreshKey?: number;
|
||||
onFairUpdate: () => void;
|
||||
}
|
||||
|
||||
const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpdate }) => {
|
||||
const FairInfoTab: React.FC<Props> = ({ 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 [fairTypeOptions, setFairTypeOptions] = useState<any[]>([]);
|
||||
const [regionOptions, setRegionOptions] = useState<any[]>([]);
|
||||
const [venueOptions, setVenueOptions] = useState<any[]>([]);
|
||||
const [venueDetail, setVenueDetail] = useState<VenueInfoItem>();
|
||||
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
|
||||
|
||||
const refreshEditModalOptions = useCallback(async () => {
|
||||
const [typeOptions, regionSelectOptions, venueRes] = await Promise.all([
|
||||
const [typeOptions, regionSelectOptions, venueRes, venueTypeValueEnum] = await Promise.all([
|
||||
getDictSelectOption(OUTDOOR_FAIR_TYPE_DICT),
|
||||
getDictSelectOption(OUTDOOR_FAIR_REGION_DICT),
|
||||
getVenueInfoList({ current: 1, pageSize: 999 }),
|
||||
getDictValueEnum(VENUE_INFO_TYPE_DICT),
|
||||
]);
|
||||
setFairTypeOptions(typeOptions);
|
||||
setRegionOptions(regionSelectOptions);
|
||||
setVenueTypeValueEnum(venueTypeValueEnum);
|
||||
if (venueRes.code === 200) {
|
||||
setVenueOptions(
|
||||
(venueRes.rows || []).map((item) => ({
|
||||
@@ -65,9 +54,26 @@ const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpda
|
||||
refreshEditModalOptions();
|
||||
}, [refreshEditModalOptions]);
|
||||
|
||||
// 拉取绑定场地的详细信息
|
||||
useEffect(() => {
|
||||
actionRef.current?.reload();
|
||||
}, [refreshKey]);
|
||||
const fetchVenueDetail = async () => {
|
||||
if (!fairInfo.venueId) {
|
||||
setVenueDetail(undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getVenueInfo(fairInfo.venueId);
|
||||
if (res.code === 200 && res.data) {
|
||||
setVenueDetail(res.data);
|
||||
} else {
|
||||
setVenueDetail(undefined);
|
||||
}
|
||||
} catch {
|
||||
setVenueDetail(undefined);
|
||||
}
|
||||
};
|
||||
fetchVenueDetail();
|
||||
}, [fairInfo.venueId]);
|
||||
|
||||
const handleCreateDictOption = async (values: OutdoorFairDictForm) => {
|
||||
const res = await addOutdoorFairDictOption(values);
|
||||
@@ -91,8 +97,7 @@ const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpda
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 基本信息卡片 */}
|
||||
<>
|
||||
<Card
|
||||
title="基本信息"
|
||||
extra={
|
||||
@@ -106,7 +111,6 @@ const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpda
|
||||
编辑信息
|
||||
</Button>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Descriptions column={3} bordered size="small">
|
||||
<Descriptions.Item label="招聘会标题" span={3}>
|
||||
@@ -141,171 +145,66 @@ const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpda
|
||||
<Descriptions.Item label="线上申请">
|
||||
{fairInfo.onlineApply ? '是' : '否'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="招聘会照片" span={3}>
|
||||
{fairInfo.photoUrl ? (
|
||||
<Image
|
||||
src={fairInfo.photoUrl}
|
||||
alt={fairInfo.title}
|
||||
width={240}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</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, fairId);
|
||||
if (res.code === 200) {
|
||||
message.success('移除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
移除
|
||||
</Button>,
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card title="场地信息" style={{ marginTop: 16 }}>
|
||||
{venueDetail ? (
|
||||
<Descriptions column={3} bordered size="small">
|
||||
<Descriptions.Item label="场地名称" span={3}>
|
||||
{venueDetail.venueName || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="场地类型">
|
||||
{venueTypeValueEnum[venueDetail.venueType]?.text ||
|
||||
venueTypeValueEnum[venueDetail.venueType]?.label ||
|
||||
venueDetail.venueType ||
|
||||
'--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="场地面积">
|
||||
{venueDetail.venueArea != null ? `${venueDetail.venueArea} ㎡` : '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="展位数量">
|
||||
{venueDetail.floorCount ?? '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="场地地址" span={2}>
|
||||
{venueDetail.venueAddress || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">
|
||||
{venueDetail.contactPhone || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="乘坐线路" span={3}>
|
||||
{venueDetail.routeLineNames || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经办日期">
|
||||
{venueDetail.handleDate || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经办机构">
|
||||
{venueDetail.handleOrg || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经办人">
|
||||
{venueDetail.handler || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注" span={3}>
|
||||
{venueDetail.remark || '--'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>该招聘会未绑定场地信息</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 编辑招聘会基本信息弹窗 */}
|
||||
<EditModal
|
||||
open={editModalOpen}
|
||||
values={fairInfo}
|
||||
@@ -325,36 +224,7 @@ const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpda
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 添加企业弹窗 */}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,54 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { ModalForm, ProForm, ProFormText, ProFormDigit, ProFormSelect } from '@ant-design/pro-components';
|
||||
import { Form } from 'antd';
|
||||
import {
|
||||
ModalForm,
|
||||
ProForm,
|
||||
ProFormDigit,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Button, Form, Input, message } from 'antd';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
import { addJobForCompany, updateJobForCompany } from '@/services/jobportal/outdoorFairDetail';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
fairId: number;
|
||||
company: any;
|
||||
job: any;
|
||||
company: API.OutdoorFairDetail.ParticipatingCompany | null;
|
||||
job: API.OutdoorFairDetail.PostedJob | null;
|
||||
educationEnum: DictValueEnumObj;
|
||||
experienceEnum: DictValueEnumObj;
|
||||
areaEnum: DictValueEnumObj;
|
||||
jobTypeEnum: DictValueEnumObj;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const JobEditModal: React.FC<Props> = ({ open, fairId, company, job, onCancel, onSuccess }) => {
|
||||
const JobEditModal: React.FC<Props> = ({
|
||||
open,
|
||||
fairId,
|
||||
company,
|
||||
job,
|
||||
educationEnum,
|
||||
experienceEnum,
|
||||
areaEnum,
|
||||
jobTypeEnum,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
if (job) {
|
||||
form.setFieldsValue(job);
|
||||
}
|
||||
if (!open) return;
|
||||
form.resetFields();
|
||||
if (job) {
|
||||
form.setFieldsValue({
|
||||
...job,
|
||||
jobLocationAreaCode: String(job.jobLocationAreaCode || ''),
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({ isPublish: 1, jobContactList: [{}] });
|
||||
}
|
||||
}, [open, job, form]);
|
||||
|
||||
@@ -29,35 +57,156 @@ const JobEditModal: React.FC<Props> = ({ open, fairId, company, job, onCancel, o
|
||||
title={job ? '编辑岗位' : `为「${company?.companyName || ''}」添加岗位`}
|
||||
form={form}
|
||||
open={open}
|
||||
width={500}
|
||||
modalProps={{ destroyOnClose: true, onCancel }}
|
||||
width={900}
|
||||
grid
|
||||
rowProps={{ gutter: [16, 16] }}
|
||||
colProps={{ span: 12 }}
|
||||
modalProps={{
|
||||
destroyOnClose: true,
|
||||
onCancel,
|
||||
style: { height: '80vh' },
|
||||
styles: {
|
||||
body: {
|
||||
height: 'calc(80vh - 120px)',
|
||||
overflowY: 'auto',
|
||||
padding: '16px 24px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
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; }
|
||||
const payload = {
|
||||
...values,
|
||||
companyId: company?.companyId,
|
||||
companyName: company?.companyName,
|
||||
jobContactList: values.jobContactList || [],
|
||||
};
|
||||
const res = job
|
||||
? await updateJobForCompany(fairId, { ...job, ...payload })
|
||||
: await addJobForCompany({ fairId, companyId: company!.companyId, ...payload });
|
||||
if (res.code === 200) {
|
||||
message.success(job ? '岗位修改成功' : '岗位添加成功');
|
||||
onSuccess();
|
||||
return true;
|
||||
}
|
||||
message.error(res.msg || '操作失败');
|
||||
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 }} />
|
||||
<ProFormText
|
||||
name="jobTitle"
|
||||
label="岗位名称"
|
||||
placeholder="请输入岗位名称"
|
||||
rules={[{ required: true, message: '请输入岗位名称' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
name="minSalary"
|
||||
min={0}
|
||||
label="最小薪资(元/月)"
|
||||
placeholder="请输入最小薪资"
|
||||
rules={[{ required: true, message: '请输入最小薪资' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
name="maxSalary"
|
||||
min={0}
|
||||
label="最大薪资(元/月)"
|
||||
placeholder="请输入最大薪资"
|
||||
rules={[{ required: true, message: '请输入最大薪资' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="education"
|
||||
label="学历要求"
|
||||
valueEnum={educationEnum}
|
||||
placeholder="请选择学历要求"
|
||||
rules={[{ required: true, message: '请选择学历要求' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="experience"
|
||||
label="工作经验"
|
||||
valueEnum={experienceEnum}
|
||||
placeholder="请选择工作经验"
|
||||
rules={[{ required: true, message: '请选择工作经验' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="jobLocationAreaCode"
|
||||
label="工作区县"
|
||||
valueEnum={areaEnum}
|
||||
placeholder="请选择区县"
|
||||
rules={[{ required: true, message: '请选择区县' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
name="vacancies"
|
||||
min={0}
|
||||
label="招聘人数"
|
||||
placeholder="请输入招聘人数"
|
||||
rules={[{ required: true, message: '请输入招聘人数' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="jobType"
|
||||
label="岗位类型"
|
||||
valueEnum={jobTypeEnum}
|
||||
placeholder="请选择岗位类型"
|
||||
rules={[{ required: true, message: '请选择岗位类型' }]}
|
||||
/>
|
||||
<ProFormText name="jobLocation" label="工作地点" placeholder="请输入工作地点" />
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="岗位描述"
|
||||
placeholder="请输入岗位描述"
|
||||
colProps={{ span: 24 }}
|
||||
/>
|
||||
|
||||
<div style={{ width: '100%', marginBottom: 16, gridColumn: '1 / -1' }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>岗位联系人</div>
|
||||
<Form.List name="jobContactList">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{ display: 'flex', width: '100%', marginBottom: 8, alignItems: 'center', gap: 8 }}
|
||||
>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'contactPerson']}
|
||||
label="联系人姓名"
|
||||
rules={[{ required: true, message: '请输入联系人姓名' }]}
|
||||
style={{ marginBottom: 0, flex: 1 }}
|
||||
>
|
||||
<Input placeholder="请输入联系人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'contactPersonPhone']}
|
||||
label="联系电话"
|
||||
rules={[
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' },
|
||||
]}
|
||||
style={{ marginBottom: 0, flex: 1 }}
|
||||
>
|
||||
<Input placeholder="请输入联系电话" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'position']}
|
||||
label="职务"
|
||||
rules={[{ required: true, message: '请输入职务' }]}
|
||||
style={{ marginBottom: 0, flex: 1 }}
|
||||
>
|
||||
<Input placeholder="请输入职务" />
|
||||
</Form.Item>
|
||||
<Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />
|
||||
</div>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
添加联系人
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Modal, Tag, message } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import {
|
||||
getParticipatingCompanies,
|
||||
getParticipatingJobDetail,
|
||||
getParticipatingJobs,
|
||||
removeJobFromCompany,
|
||||
removeParticipatingCompany,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
import CompanyAddModal from './CompanyAddModal';
|
||||
import JobEditModal from './JobEditModal';
|
||||
|
||||
interface Props {
|
||||
fairId: number;
|
||||
}
|
||||
|
||||
const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
const companyActionRef = useRef<ActionType>();
|
||||
const jobActionRef = useRef<ActionType>();
|
||||
const [companyModalOpen, setCompanyModalOpen] = useState(false);
|
||||
const [jobListModalOpen, setJobListModalOpen] = useState(false);
|
||||
const [jobEditModalOpen, setJobEditModalOpen] = useState(false);
|
||||
const [selectedCompany, setSelectedCompany] =
|
||||
useState<API.OutdoorFairDetail.ParticipatingCompany | null>(null);
|
||||
const [editingJob, setEditingJob] = useState<API.OutdoorFairDetail.PostedJob | null>(null);
|
||||
const [educationEnum, setEducationEnum] = useState<any>({});
|
||||
const [experienceEnum, setExperienceEnum] = useState<any>({});
|
||||
const [areaEnum, setAreaEnum] = useState<any>({});
|
||||
const [jobTypeEnum, setJobTypeEnum] = useState<any>({});
|
||||
const [scaleEnum, setScaleEnum] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getDictValueEnum('education', true, true),
|
||||
getDictValueEnum('experience', true, true),
|
||||
getDictValueEnum('area', true, true),
|
||||
getDictValueEnum('job_type', true, true),
|
||||
getDictValueEnum('scale', true, true),
|
||||
]).then(([education, experience, area, jobType, scale]) => {
|
||||
setEducationEnum(education);
|
||||
setExperienceEnum(experience);
|
||||
setAreaEnum(area);
|
||||
setJobTypeEnum(jobType);
|
||||
setScaleEnum(scale);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openJobEditor = async (job?: API.OutdoorFairDetail.PostedJob) => {
|
||||
if (job?.jobId) {
|
||||
const res = await getParticipatingJobDetail(fairId, job.jobId);
|
||||
if (res.code !== 200) {
|
||||
message.error(res.msg || '岗位详情加载失败');
|
||||
return;
|
||||
}
|
||||
setEditingJob(res.data);
|
||||
} else {
|
||||
setEditingJob(null);
|
||||
}
|
||||
setJobEditModalOpen(true);
|
||||
};
|
||||
|
||||
const jobColumns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
title: '薪资范围',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => `${record.minSalary}-${record.maxSalary} 元/月`,
|
||||
},
|
||||
{
|
||||
title: '学历要求',
|
||||
dataIndex: 'education',
|
||||
valueType: 'select',
|
||||
valueEnum: educationEnum,
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
title: '工作经验',
|
||||
dataIndex: 'experience',
|
||||
valueType: 'select',
|
||||
valueEnum: experienceEnum,
|
||||
width: 110,
|
||||
},
|
||||
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
|
||||
{
|
||||
title: '发布状态',
|
||||
dataIndex: 'isPublish',
|
||||
hideInSearch: true,
|
||||
width: 90,
|
||||
render: (_, record) => (
|
||||
<Tag color={record.isPublish === 1 ? 'green' : 'red'}>
|
||||
{record.isPublish === 1 ? '已发布' : '未发布'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 130,
|
||||
render: (_, record) => [
|
||||
<Button key="edit" type="link" size="small" onClick={() => openJobEditor(record)}>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
onClick={() =>
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定删除岗位「${record.jobTitle}」吗?`,
|
||||
onOk: async () => {
|
||||
const res = await removeJobFromCompany(fairId, record.jobId!);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
jobActionRef.current?.reload();
|
||||
companyActionRef.current?.reload();
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable<API.OutdoorFairDetail.ParticipatingCompany>
|
||||
actionRef={companyActionRef}
|
||||
rowKey="id"
|
||||
headerTitle="参会企业"
|
||||
search={{ labelWidth: 'auto' }}
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCompanyModalOpen(true)}>
|
||||
添加企业
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getParticipatingCompanies(fairId, {
|
||||
current: params.current,
|
||||
pageSize: params.pageSize,
|
||||
companyName: params.companyName,
|
||||
});
|
||||
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true },
|
||||
{
|
||||
title: '行业',
|
||||
dataIndex: 'industry',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
const v = record.industry;
|
||||
if (!v) return '--';
|
||||
// 行业分类 id 均为 6 位编号,纯数字裸码(如 4、6)无对应分类项,兜底为“其他”
|
||||
if (/^\d+$/.test(String(v))) return <Tag>其他</Tag>;
|
||||
return <Tag>{v}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '规模',
|
||||
dataIndex: 'scale',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
const v = record.scale;
|
||||
if (v === undefined || v === null || v === '') return '其他';
|
||||
// 规模为字典码,渲染为标签;匹配不到时兜底“其他”,不显示裸数字
|
||||
const label = scaleEnum[v]?.label || scaleEnum[v]?.text;
|
||||
return label ? <Tag>{label}</Tag> : '其他';
|
||||
},
|
||||
},
|
||||
{ title: '联系人', dataIndex: 'contactPerson', hideInSearch: true },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', hideInSearch: true },
|
||||
{
|
||||
title: '展位号',
|
||||
dataIndex: 'boothNumber',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record.boothNumber || '--',
|
||||
},
|
||||
{ title: '岗位数', dataIndex: 'jobCount', hideInSearch: true, width: 80 },
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 220,
|
||||
render: (_, record) => [
|
||||
<Button
|
||||
key="jobs"
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedCompany(record);
|
||||
setJobListModalOpen(true);
|
||||
}}
|
||||
>
|
||||
查看岗位
|
||||
</Button>,
|
||||
<Button
|
||||
key="addJob"
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedCompany(record);
|
||||
openJobEditor();
|
||||
}}
|
||||
>
|
||||
添加岗位
|
||||
</Button>,
|
||||
<Button
|
||||
key="remove"
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
onClick={() =>
|
||||
Modal.confirm({
|
||||
title: '确认移除',
|
||||
content: `确定移除「${record.companyName}」吗?`,
|
||||
onOk: async () => {
|
||||
const res = await removeParticipatingCompany(record.id, fairId);
|
||||
if (res.code === 200) {
|
||||
message.success('移除成功');
|
||||
companyActionRef.current?.reload();
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
移除
|
||||
</Button>,
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={selectedCompany ? `${selectedCompany.companyName} — 发布岗位` : '企业岗位'}
|
||||
open={jobListModalOpen}
|
||||
width={1100}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
onCancel={() => setJobListModalOpen(false)}
|
||||
>
|
||||
{selectedCompany && (
|
||||
<ProTable<API.OutdoorFairDetail.PostedJob>
|
||||
actionRef={jobActionRef}
|
||||
rowKey="jobId"
|
||||
search={{ labelWidth: 'auto' }}
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openJobEditor()}>
|
||||
添加岗位
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getParticipatingJobs(fairId, {
|
||||
companyId: selectedCompany.companyId,
|
||||
current: params.current,
|
||||
pageSize: params.pageSize,
|
||||
jobTitle: params.jobTitle,
|
||||
education: params.education,
|
||||
experience: params.experience,
|
||||
});
|
||||
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
|
||||
}}
|
||||
columns={jobColumns}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<CompanyAddModal
|
||||
open={companyModalOpen}
|
||||
fairId={fairId}
|
||||
onCancel={() => setCompanyModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setCompanyModalOpen(false);
|
||||
companyActionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
|
||||
<JobEditModal
|
||||
open={jobEditModalOpen}
|
||||
fairId={fairId}
|
||||
company={selectedCompany}
|
||||
job={editingJob}
|
||||
educationEnum={educationEnum}
|
||||
experienceEnum={experienceEnum}
|
||||
areaEnum={areaEnum}
|
||||
jobTypeEnum={jobTypeEnum}
|
||||
onCancel={() => {
|
||||
setJobEditModalOpen(false);
|
||||
setEditingJob(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setJobEditModalOpen(false);
|
||||
setEditingJob(null);
|
||||
jobActionRef.current?.reload();
|
||||
companyActionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticipatingCompaniesTab;
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Modal, Tag, message } from 'antd';
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import {
|
||||
getParticipatingCompanies,
|
||||
getParticipatingJobDetail,
|
||||
getParticipatingJobs,
|
||||
removeJobFromCompany,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
import JobEditModal from './JobEditModal';
|
||||
|
||||
interface Props {
|
||||
fairId: number;
|
||||
}
|
||||
|
||||
const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [editingJob, setEditingJob] = useState<API.OutdoorFairDetail.PostedJob | null>(null);
|
||||
const [editingCompany, setEditingCompany] =
|
||||
useState<API.OutdoorFairDetail.ParticipatingCompany | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [educationEnum, setEducationEnum] = useState<any>({});
|
||||
const [experienceEnum, setExperienceEnum] = useState<any>({});
|
||||
const [areaEnum, setAreaEnum] = useState<any>({});
|
||||
const [jobTypeEnum, setJobTypeEnum] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getDictValueEnum('education', true, true),
|
||||
getDictValueEnum('experience', true, true),
|
||||
getDictValueEnum('area', true, true),
|
||||
getDictValueEnum('job_type', true, true),
|
||||
]).then(([education, experience, area, jobType]) => {
|
||||
setEducationEnum(education);
|
||||
setExperienceEnum(experience);
|
||||
setAreaEnum(area);
|
||||
setJobTypeEnum(jobType);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const editJob = async (record: API.OutdoorFairDetail.PostedJob) => {
|
||||
const res = await getParticipatingJobDetail(fairId, record.jobId!);
|
||||
if (res.code !== 200) {
|
||||
message.error(res.msg || '岗位详情加载失败');
|
||||
return;
|
||||
}
|
||||
setEditingJob(res.data);
|
||||
setEditingCompany({
|
||||
id: 0,
|
||||
fairId,
|
||||
companyId: record.companyId,
|
||||
companyName: record.companyName || '',
|
||||
industry: '',
|
||||
scale: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
jobCount: 0,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
title: '企业名称',
|
||||
dataIndex: 'companyId',
|
||||
valueType: 'select',
|
||||
request: async ({ keyWords }) => {
|
||||
const res = await getParticipatingCompanies(fairId, {
|
||||
current: 1,
|
||||
pageSize: 100,
|
||||
companyName: keyWords,
|
||||
});
|
||||
return Array.from(
|
||||
new Map(
|
||||
(res.rows || []).map((company) => [
|
||||
company.companyId,
|
||||
{ label: company.companyName, value: company.companyId },
|
||||
]),
|
||||
).values(),
|
||||
);
|
||||
},
|
||||
fieldProps: {
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
placeholder: '请选择或搜索企业',
|
||||
},
|
||||
render: (_, record) => record.companyName || '--',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '薪资范围',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => `${record.minSalary}-${record.maxSalary} 元/月`,
|
||||
},
|
||||
{
|
||||
title: '学历要求',
|
||||
dataIndex: 'education',
|
||||
valueType: 'select',
|
||||
valueEnum: educationEnum,
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
title: '工作经验',
|
||||
dataIndex: 'experience',
|
||||
valueType: 'select',
|
||||
valueEnum: experienceEnum,
|
||||
width: 110,
|
||||
},
|
||||
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
|
||||
{ title: '工作地点', dataIndex: 'jobLocation', hideInSearch: true, ellipsis: true },
|
||||
{
|
||||
title: '发布状态',
|
||||
dataIndex: 'isPublish',
|
||||
hideInSearch: true,
|
||||
width: 90,
|
||||
render: (_, record) => (
|
||||
<Tag color={record.isPublish === 1 ? 'green' : 'red'}>
|
||||
{record.isPublish === 1 ? '已发布' : '未发布'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 130,
|
||||
render: (_, record) => [
|
||||
<Button key="edit" type="link" size="small" onClick={() => editJob(record)}>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
onClick={() =>
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定删除岗位「${record.jobTitle}」吗?`,
|
||||
onOk: async () => {
|
||||
const res = await removeJobFromCompany(fairId, record.jobId!);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable<API.OutdoorFairDetail.PostedJob>
|
||||
actionRef={actionRef}
|
||||
rowKey="jobId"
|
||||
headerTitle="参会岗位"
|
||||
search={{ labelWidth: 'auto', defaultCollapsed: false }}
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
request={async (params) => {
|
||||
const res = await getParticipatingJobs(fairId, {
|
||||
current: params.current,
|
||||
pageSize: params.pageSize,
|
||||
companyId: params.companyId,
|
||||
jobTitle: params.jobTitle,
|
||||
education: params.education,
|
||||
experience: params.experience,
|
||||
});
|
||||
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
|
||||
}}
|
||||
columns={columns}
|
||||
/>
|
||||
|
||||
<JobEditModal
|
||||
open={modalOpen}
|
||||
fairId={fairId}
|
||||
company={editingCompany}
|
||||
job={editingJob}
|
||||
educationEnum={educationEnum}
|
||||
experienceEnum={experienceEnum}
|
||||
areaEnum={areaEnum}
|
||||
jobTypeEnum={jobTypeEnum}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setModalOpen(false);
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticipatingJobsTab;
|
||||
@@ -1,108 +0,0 @@
|
||||
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;
|
||||
@@ -7,10 +7,8 @@ import { getOutdoorFairInfo } from '@/services/jobportal/outdoorFair';
|
||||
import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair';
|
||||
import FairInfoTab from './components/FairInfoTab';
|
||||
import BoothTab from './components/BoothTab';
|
||||
import CompanyReviewTab from './components/CompanyReviewTab';
|
||||
import AttendeeTab from './components/AttendeeTab';
|
||||
import DeviceTab from './components/DeviceTab';
|
||||
import StatisticsTab from './components/StatisticsTab';
|
||||
import ParticipatingCompaniesTab from './components/ParticipatingCompaniesTab';
|
||||
import ParticipatingJobsTab from './components/ParticipatingJobsTab';
|
||||
|
||||
const OutdoorFairDetail: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -18,7 +16,6 @@ const OutdoorFairDetail: React.FC = () => {
|
||||
const [fairInfo, setFairInfo] = useState<OutdoorFairItem | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('fair-info');
|
||||
const [fairInfoRefreshKey, setFairInfoRefreshKey] = useState(0);
|
||||
|
||||
const fetchFairInfo = async () => {
|
||||
setLoading(true);
|
||||
@@ -42,18 +39,13 @@ const OutdoorFairDetail: React.FC = () => {
|
||||
|
||||
const tabItems = [
|
||||
{ key: 'fair-info', label: '招聘会管理' },
|
||||
{ key: 'participating-companies', label: '参会企业' },
|
||||
{ key: 'participating-jobs', label: '参会岗位' },
|
||||
{ key: 'booth', label: '展位图' },
|
||||
{ key: 'company-review', label: '参会单位管理' },
|
||||
{ key: 'attendee', label: '参会人员管理' },
|
||||
{ key: 'device', label: '入场设备管理' },
|
||||
{ key: 'statistics', label: '数据统计' },
|
||||
];
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key);
|
||||
if (key === 'fair-info') {
|
||||
setFairInfoRefreshKey((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -83,20 +75,15 @@ const OutdoorFairDetail: React.FC = () => {
|
||||
<FairInfoTab
|
||||
fairId={fairId}
|
||||
fairInfo={fairInfo}
|
||||
refreshKey={fairInfoRefreshKey}
|
||||
onFairUpdate={fetchFairInfo}
|
||||
/>
|
||||
);
|
||||
case 'booth':
|
||||
return <BoothTab fairId={fairId} fairInfo={fairInfo} />;
|
||||
case 'company-review':
|
||||
return <CompanyReviewTab fairId={fairId} />;
|
||||
case 'attendee':
|
||||
return <AttendeeTab fairId={fairId} />;
|
||||
case 'device':
|
||||
return <DeviceTab fairId={fairId} />;
|
||||
case 'statistics':
|
||||
return <StatisticsTab fairId={fairId} />;
|
||||
case 'participating-companies':
|
||||
return <ParticipatingCompaniesTab fairId={fairId} />;
|
||||
case 'participating-jobs':
|
||||
return <ParticipatingJobsTab fairId={fairId} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,16 @@ const OutdoorFairList: React.FC = () => {
|
||||
title: '绑定场地',
|
||||
dataIndex: 'venueName',
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
valueType: 'select',
|
||||
fieldProps: {
|
||||
options: venueOptions,
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
},
|
||||
search: {
|
||||
transform: (value) => ({ venueId: value }),
|
||||
},
|
||||
render: (_, record) =>
|
||||
record.venueId && record.venueName ? (
|
||||
<Button
|
||||
@@ -177,7 +186,11 @@ const OutdoorFairList: React.FC = () => {
|
||||
{
|
||||
title: '线上申请',
|
||||
dataIndex: 'onlineApply',
|
||||
hideInSearch: true,
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
'true': { text: '是' },
|
||||
'false': { text: '否' },
|
||||
},
|
||||
render: (_, record) => (record.onlineApply ? '是' : '否'),
|
||||
},
|
||||
{
|
||||
@@ -255,6 +268,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
fairType: params.fairType,
|
||||
region: params.region,
|
||||
venueId: params.venueId,
|
||||
onlineApply: params.onlineApply,
|
||||
} as OutdoorFairListParams);
|
||||
return { data: res.rows, total: res.total, success: true };
|
||||
}}
|
||||
|
||||
175
src/pages/Jobfair/Venueinfo/Detail/index.tsx
Normal file
175
src/pages/Jobfair/Venueinfo/Detail/index.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useAccess, useSearchParams } from '@umijs/max';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Button, Descriptions, Spin, message } from 'antd';
|
||||
import { ArrowLeftOutlined, FormOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
addVenueInfoDictOption,
|
||||
getVenueInfo,
|
||||
updateVenueInfo,
|
||||
} from '@/services/jobportal/venueInfo';
|
||||
import type { VenueInfoDictForm, VenueInfoForm, VenueInfoItem } from '@/services/jobportal/venueInfo';
|
||||
import { getCmsLineList } from '@/services/area/subway';
|
||||
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
|
||||
import EditModal from '../components/EditModal';
|
||||
|
||||
const VENUE_TYPE_DICT = 'venue_info_type';
|
||||
|
||||
const VenueInfoDetail: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const [searchParams] = useSearchParams();
|
||||
const venueId = Number(searchParams.get('id'));
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [venueInfo, setVenueInfo] = useState<VenueInfoItem>();
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
|
||||
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
|
||||
const [lineOptions, setLineOptions] = useState<any[]>([]);
|
||||
|
||||
const fetchVenueInfo = useCallback(async () => {
|
||||
if (!venueId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getVenueInfo(venueId);
|
||||
if (res.code === 200 && res.data) {
|
||||
setVenueInfo(res.data);
|
||||
} else {
|
||||
message.error(res.msg || '未找到该场地信息');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [venueId]);
|
||||
|
||||
const refreshOptions = useCallback(async () => {
|
||||
const [valueEnum, options, lineRes] = await Promise.all([
|
||||
getDictValueEnum(VENUE_TYPE_DICT),
|
||||
getDictSelectOption(VENUE_TYPE_DICT),
|
||||
getCmsLineList({ current: '1', pageSize: '999' } as any),
|
||||
]);
|
||||
setVenueTypeValueEnum(valueEnum);
|
||||
setVenueTypeOptions(options);
|
||||
if (lineRes.code === 200) {
|
||||
setLineOptions(
|
||||
(lineRes.rows || []).map((item) => ({
|
||||
label: item.lineName,
|
||||
value: String(item.lineId),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVenueInfo();
|
||||
refreshOptions();
|
||||
}, [fetchVenueInfo, refreshOptions]);
|
||||
|
||||
const handleCreateDictOption = async (values: VenueInfoDictForm) => {
|
||||
const res = await addVenueInfoDictOption(values);
|
||||
if (res.code === 200) {
|
||||
await refreshOptions();
|
||||
message.success('场地类型新增成功');
|
||||
return true;
|
||||
}
|
||||
message.error(res.msg || '场地类型新增失败');
|
||||
return false;
|
||||
};
|
||||
|
||||
const venueTypeLabel = (val?: string) => {
|
||||
if (!val) return '--';
|
||||
return (
|
||||
venueTypeValueEnum[val]?.text ||
|
||||
venueTypeValueEnum[val]?.label ||
|
||||
val
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
header={{
|
||||
title: venueInfo ? `场地信息详情 — ${venueInfo.venueName}` : '加载中...',
|
||||
onBack: () => history.back(),
|
||||
extra: [
|
||||
<Button key="back" icon={<ArrowLeftOutlined />} onClick={() => history.back()}>
|
||||
返回列表
|
||||
</Button>,
|
||||
<Button
|
||||
key="edit"
|
||||
type="primary"
|
||||
icon={<FormOutlined />}
|
||||
hidden={!access.hasPerms('cms:venueInfo:edit')}
|
||||
onClick={() => setEditModalOpen(true)}
|
||||
>
|
||||
编辑信息
|
||||
</Button>,
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{venueInfo ? (
|
||||
<Descriptions column={3} bordered size="small">
|
||||
<Descriptions.Item label="场地名称" span={3}>
|
||||
{venueInfo.venueName || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="场地类型">
|
||||
{venueTypeLabel(venueInfo.venueType)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="场地面积">
|
||||
{venueInfo.venueArea != null ? `${venueInfo.venueArea} ㎡` : '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="展位数量">
|
||||
{venueInfo.floorCount ?? '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="场地地址" span={2}>
|
||||
{venueInfo.venueAddress || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">
|
||||
{venueInfo.contactPhone || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="乘坐线路" span={3}>
|
||||
{venueInfo.routeLineNames || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经办日期">
|
||||
{venueInfo.handleDate || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经办机构">
|
||||
{venueInfo.handleOrg || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经办人">
|
||||
{venueInfo.handler || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注" span={3}>
|
||||
{venueInfo.remark || '--'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
!loading && <div style={{ color: '#999' }}>未找到该场地信息</div>
|
||||
)}
|
||||
</Spin>
|
||||
|
||||
<EditModal
|
||||
open={editModalOpen}
|
||||
values={venueInfo}
|
||||
venueTypeOptions={venueTypeOptions}
|
||||
lineOptions={lineOptions}
|
||||
onCreateDictOption={handleCreateDictOption}
|
||||
onCancel={() => setEditModalOpen(false)}
|
||||
onSubmit={async (values: VenueInfoForm) => {
|
||||
const res = await updateVenueInfo(values);
|
||||
if (res.code === 200) {
|
||||
message.success('修改成功');
|
||||
setEditModalOpen(false);
|
||||
fetchVenueInfo();
|
||||
} else {
|
||||
message.error(res.msg || '操作失败');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default VenueInfoDetail;
|
||||
@@ -42,6 +42,7 @@ export interface OutdoorFairListParams {
|
||||
fairType?: string;
|
||||
region?: string;
|
||||
venueId?: number;
|
||||
onlineApply?: boolean;
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
@@ -1011,7 +1011,11 @@ function paginate<T>(arr: T[], current: number, pageSize: number) {
|
||||
/** 获取参会企业列表 */
|
||||
export async function getParticipatingCompanies(
|
||||
fairId: number,
|
||||
params?: { current?: number; pageSize?: number; companyName?: string },
|
||||
params?: {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
companyName?: string;
|
||||
},
|
||||
) {
|
||||
return request<{ code: number; msg?: string; total: number; rows: ParticipatingCompany[] }>(
|
||||
`/api/cms/outdoor-fair/${fairId}/companies`,
|
||||
@@ -1022,6 +1026,36 @@ export async function getParticipatingCompanies(
|
||||
);
|
||||
}
|
||||
|
||||
/** 分页获取招聘会参会岗位,可按企业懒加载。 */
|
||||
export async function getParticipatingJobs(
|
||||
fairId: number,
|
||||
params?: {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
companyId?: number;
|
||||
companyName?: string;
|
||||
jobTitle?: string;
|
||||
education?: string;
|
||||
experience?: string;
|
||||
},
|
||||
) {
|
||||
return request<{ code: number; msg?: string; total: number; rows: PostedJob[] }>(
|
||||
`/api/cms/outdoor-fair/${fairId}/jobs`,
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取招聘会岗位详情(编辑时才加载联系人等完整字段)。 */
|
||||
export async function getParticipatingJobDetail(fairId: number, jobId: number) {
|
||||
return request<{ code: number; msg?: string; data: PostedJob }>(
|
||||
`/api/cms/outdoor-fair/${fairId}/jobs/${jobId}`,
|
||||
{ method: 'GET' },
|
||||
);
|
||||
}
|
||||
|
||||
/** 添加参会企业 */
|
||||
export async function addParticipatingCompany(data: {
|
||||
fairId: number;
|
||||
@@ -1058,46 +1092,40 @@ export async function removeParticipatingCompany(id: number, fairId?: number) {
|
||||
export async function addJobForCompany(data: {
|
||||
fairId: number;
|
||||
companyId: number;
|
||||
jobTitle: string;
|
||||
minSalary: number;
|
||||
maxSalary: number;
|
||||
education: string;
|
||||
experience: string;
|
||||
vacancies: number;
|
||||
[key: string]: any;
|
||||
}) {
|
||||
const company = MOCK_COMPANIES.find((c) => c.id === data.companyId);
|
||||
if (!company) return { code: 404, msg: '企业未找到' };
|
||||
const job: PostedJob = {
|
||||
...data,
|
||||
id: nextPostedJobId++,
|
||||
status: 'active',
|
||||
};
|
||||
company.jobList.push(job);
|
||||
return { code: 200, msg: '岗位添加成功', data: job };
|
||||
const { fairId, companyId, ...job } = data;
|
||||
return request<{ code: number; msg?: string; data?: PostedJob }>(
|
||||
`/api/cms/outdoor-fair/${fairId}/companies/${companyId}/jobs`,
|
||||
{
|
||||
method: 'POST',
|
||||
data: job,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 编辑岗位 */
|
||||
export async function updateJobForCompany(data: PostedJob) {
|
||||
for (const c of MOCK_COMPANIES) {
|
||||
const idx = c.jobList.findIndex((j) => j.id === data.id);
|
||||
if (idx !== -1) {
|
||||
c.jobList[idx] = { ...c.jobList[idx], ...data };
|
||||
return { code: 200, msg: '修改成功' };
|
||||
}
|
||||
export async function updateJobForCompany(fairId: number, data: PostedJob) {
|
||||
if (!data.jobId) {
|
||||
return { code: 400, msg: '缺少岗位ID' };
|
||||
}
|
||||
return { code: 404, msg: '未找到' };
|
||||
return request<{ code: number; msg?: string }>(
|
||||
`/api/cms/outdoor-fair/${fairId}/jobs/${data.jobId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除岗位 */
|
||||
export async function removeJobFromCompany(jobId: number) {
|
||||
for (const c of MOCK_COMPANIES) {
|
||||
const idx = c.jobList.findIndex((j) => j.id === jobId);
|
||||
if (idx !== -1) {
|
||||
c.jobList.splice(idx, 1);
|
||||
return { code: 200, msg: '删除成功' };
|
||||
}
|
||||
}
|
||||
return { code: 404, msg: '未找到' };
|
||||
export async function removeJobFromCompany(fairId: number, jobId: number) {
|
||||
return request<{ code: number; msg?: string }>(
|
||||
`/api/cms/outdoor-fair/${fairId}/jobs/${jobId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 模块2: 展位管理 API ====================
|
||||
|
||||
19
src/types/jobfair/outdoorFairDetail.d.ts
vendored
19
src/types/jobfair/outdoorFairDetail.d.ts
vendored
@@ -17,21 +17,32 @@ declare namespace API.OutdoorFairDetail {
|
||||
contactPhone: string;
|
||||
boothId?: number;
|
||||
boothNumber?: string;
|
||||
jobList: PostedJob[];
|
||||
jobCount?: number;
|
||||
/** 仅兼容旧 mock 数据,真实企业接口不再返回岗位列表。 */
|
||||
jobList?: PostedJob[];
|
||||
}
|
||||
|
||||
/** 已发布岗位 */
|
||||
export interface PostedJob {
|
||||
id: number;
|
||||
fairId: number;
|
||||
/** 真实岗位主键;旧 mock 数据仅使用 id。 */
|
||||
jobId?: number;
|
||||
id?: number;
|
||||
fairId?: number;
|
||||
companyId: number;
|
||||
companyName?: string;
|
||||
jobTitle: string;
|
||||
minSalary: number;
|
||||
maxSalary: number;
|
||||
education: string;
|
||||
experience: string;
|
||||
vacancies: number;
|
||||
status: string; // 'active' | 'closed'
|
||||
jobLocationAreaCode?: string;
|
||||
jobType?: string;
|
||||
jobLocation?: string;
|
||||
description?: string;
|
||||
isPublish?: number;
|
||||
status?: string;
|
||||
jobContactList?: API.ManagementList.ContactPerson[];
|
||||
}
|
||||
|
||||
// ==================== 模块2: 展位管理 ====================
|
||||
|
||||
Reference in New Issue
Block a user