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:
2026-06-25 23:13:42 +08:00
parent 5a8aaccb6c
commit b684b4e12d
16 changed files with 1069 additions and 1126 deletions

View File

@@ -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;