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

@@ -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>
</>
);
};