feat: Enhance Outdoor Fair and Public Job Fair features

- Added QR code functionality for companies signing up for outdoor fairs.
- Implemented job management for outdoor fairs, including job listing and editing.
- Updated Public Job Fair detail view to reflect new data structure.
- Refactored EditModal to support dynamic lists for host, co-organizer, and organizing units.
- Introduced cross-domain city selection for job fairs.
- Enhanced API services for managing cross-domain jobs and cities.
- Improved data handling for outdoor fair items, including review status and QR code content.
- Updated type definitions to accommodate new fields and structures.
This commit is contained in:
2026-06-27 22:45:16 +08:00
parent 7b9e8426de
commit 9c8bfb617b
16 changed files with 1613 additions and 758 deletions

View File

@@ -1,38 +1,74 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useAccess, history } from '@umijs/max';
import { Button, message, Modal } from 'antd';
import { useAccess, history, useModel } from '@umijs/max';
import { Button, message, Modal, QRCode, Tag, Typography } from 'antd';
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
import { PlusOutlined, DeleteOutlined, FormOutlined, EyeOutlined } from '@ant-design/icons';
import {
PlusOutlined,
DeleteOutlined,
FormOutlined,
EyeOutlined,
QrcodeOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import {
getOutdoorFairList,
addOutdoorFair,
updateOutdoorFair,
deleteOutdoorFair,
addOutdoorFairDictOption,
signupOutdoorFair,
getMyOutdoorFairQrCode,
} from '@/services/jobportal/outdoorFair';
import type {
OutdoorFairItem,
OutdoorFairListParams,
OutdoorFairForm,
OutdoorFairDictForm,
OutdoorFairCompanyQrCodeInfo,
} from '@/services/jobportal/outdoorFair';
import { getParticipatingJobs } from '@/services/jobportal/outdoorFairDetail';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import { getVenueInfoList } from '@/services/jobportal/venueInfo';
import EditModal from './components/EditModal';
import JobEditModal from './Detail/components/JobEditModal';
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region';
const OutdoorFairList: React.FC = () => {
const access = useAccess();
const { initialState } = useModel('@@initialState');
const isEnterprise = initialState?.currentUser?.userType === 'view';
const actionRef = useRef<ActionType>();
const jobActionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<OutdoorFairItem>();
const [selectedFair, setSelectedFair] = useState<OutdoorFairItem | null>(null);
const [jobListModalOpen, setJobListModalOpen] = useState(false);
const [jobEditModalOpen, setJobEditModalOpen] = useState(false);
const [qrModalOpen, setQrModalOpen] = useState(false);
const [qrInfo, setQrInfo] = useState<OutdoorFairCompanyQrCodeInfo | null>(null);
const [rejectReasonModalOpen, setRejectReasonModalOpen] = useState(false);
const [rejectReason, setRejectReason] = useState('');
const [fairTypeValueEnum, setFairTypeValueEnum] = useState<Record<string, any>>({});
const [fairTypeOptions, setFairTypeOptions] = useState<any[]>([]);
const [regionValueEnum, setRegionValueEnum] = useState<Record<string, any>>({});
const [regionOptions, setRegionOptions] = useState<any[]>([]);
const [venueOptions, setVenueOptions] = useState<any[]>([]);
const [educationEnum, setEducationEnum] = useState<any>({});
const [experienceEnum, setExperienceEnum] = useState<any>({});
const [areaEnum, setAreaEnum] = useState<any>({});
const [jobTypeEnum, setJobTypeEnum] = useState<any>({});
const renderReviewStatus = (status?: string) => {
const map: Record<string, { color: string; text: string }> = {
'0': { color: 'processing', text: '待审核' },
'1': { color: 'success', text: '已通过' },
'2': { color: 'error', text: '已驳回' },
};
const item = map[status || '0'] || map['0'];
return <Tag color={item.color}>{item.text}</Tag>;
};
const refreshDictOptions = useCallback(async () => {
const [typeValueEnum, typeOptions, regionEnum, regionSelectOptions] = await Promise.all([
@@ -51,6 +87,20 @@ const OutdoorFairList: React.FC = () => {
refreshDictOptions();
}, [refreshDictOptions]);
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 refreshVenueOptions = useCallback(async () => {
const res = await getVenueInfoList({ current: 1, pageSize: 999 });
if (res.code === 200) {
@@ -66,8 +116,10 @@ const OutdoorFairList: React.FC = () => {
}, []);
useEffect(() => {
refreshVenueOptions();
}, [refreshVenueOptions]);
if (!isEnterprise) {
refreshVenueOptions();
}
}, [isEnterprise, refreshVenueOptions]);
const handleCreateDictOption = async (values: OutdoorFairDictForm) => {
const res = await addOutdoorFairDictOption(values);
@@ -96,6 +148,87 @@ const OutdoorFairList: React.FC = () => {
});
};
const getCurrentCompany = (
record: OutdoorFairItem,
): API.OutdoorFairDetail.ParticipatingCompany => ({
id: 0,
fairId: record.id,
companyId: record.companyId!,
companyName: record.companyName || '本企业',
industry: '',
scale: '',
contactPerson: '',
contactPhone: '',
jobCount: 0,
});
const handleSignup = async (record: OutdoorFairItem) => {
const res = await signupOutdoorFair(record.id);
if (res.code === 200) {
message.success('报名成功,等待审核');
actionRef.current?.reload();
} else {
message.error(res.msg || '报名失败');
}
};
const openQrCode = async (record: OutdoorFairItem) => {
const res = await getMyOutdoorFairQrCode(record.id);
if (res.code === 200) {
setQrInfo(res.data);
setQrModalOpen(true);
} else {
message.error(res.msg || '二维码加载失败');
}
};
const openJobList = (record: OutdoorFairItem) => {
setSelectedFair(record);
setJobListModalOpen(true);
};
const openJobEditor = (record: OutdoorFairItem) => {
setSelectedFair(record);
setJobEditModalOpen(true);
};
const openRejectReason = (record: OutdoorFairItem) => {
setRejectReason(record.reviewRemark || '暂无驳回原因');
setRejectReasonModalOpen(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: 'fairReviewStatus',
hideInSearch: true,
width: 100,
render: (_, record) => renderReviewStatus(record.fairReviewStatus),
},
{ title: '工作地点', dataIndex: 'jobLocation', hideInSearch: true, ellipsis: true },
];
const columns: ProColumns<OutdoorFairItem>[] = [
{
title: '招聘会标题',
@@ -122,7 +255,7 @@ const OutdoorFairList: React.FC = () => {
valueEnum: regionValueEnum,
},
{
title: '绑定场地',
title: '场地名称',
dataIndex: 'venueName',
ellipsis: true,
valueType: 'select',
@@ -135,18 +268,7 @@ const OutdoorFairList: React.FC = () => {
search: {
transform: (value) => ({ venueId: value }),
},
render: (_, record) =>
record.venueId && record.venueName ? (
<Button
type="link"
size="small"
onClick={() => history.push(`/jobfair/venue-info/detail?id=${record.venueId}`)}
>
{record.venueName}
</Button>
) : (
'--'
),
render: (_, record) => record.venueName || '--',
},
{
title: '举办地址',
@@ -188,11 +310,29 @@ const OutdoorFairList: React.FC = () => {
dataIndex: 'onlineApply',
valueType: 'select',
valueEnum: {
'true': { text: '是' },
'false': { text: '否' },
true: { text: '是' },
false: { text: '否' },
},
render: (_, record) => (record.onlineApply ? '是' : '否'),
},
{
title: '报名审核状态',
dataIndex: 'reviewStatus',
hideInSearch: true,
hideInTable: !isEnterprise,
render: (_, record) =>
record.signedUp ? (
record.reviewStatus === '2' ? (
<Button type="link" size="small" danger onClick={() => openRejectReason(record)}>
{renderReviewStatus(record.reviewStatus)}
</Button>
) : (
renderReviewStatus(record.reviewStatus)
)
) : (
'--'
),
},
{
title: '招聘会照片',
dataIndex: 'photoUrl',
@@ -211,14 +351,47 @@ const OutdoorFairList: React.FC = () => {
{
title: '操作',
valueType: 'option',
width: 160,
width: isEnterprise ? 280 : 160,
fixed: 'right',
render: (_, record) => [
isEnterprise && !record.signedUp ? (
<Button key="signup" type="link" size="small" onClick={() => handleSignup(record)}>
</Button>
) : null,
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
<Button
key="qr"
type="link"
size="small"
icon={<QrcodeOutlined />}
onClick={() => openQrCode(record)}
>
</Button>
) : null,
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
<Button key="addJob" type="link" size="small" onClick={() => openJobEditor(record)}>
</Button>
) : null,
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
<Button
key="jobs"
type="link"
size="small"
icon={<UnorderedListOutlined />}
onClick={() => openJobList(record)}
>
</Button>
) : null,
<Button
key="detail"
type="link"
size="small"
icon={<EyeOutlined />}
hidden={isEnterprise}
onClick={() => history.push(`/jobfair/outdoor-fair/detail?id=${record.id}`)}
>
@@ -228,7 +401,7 @@ const OutdoorFairList: React.FC = () => {
type="link"
size="small"
icon={<FormOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:edit')}
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:edit')}
onClick={() => {
setCurrentRow(record);
setModalVisible(true);
@@ -242,7 +415,7 @@ const OutdoorFairList: React.FC = () => {
size="small"
danger
icon={<DeleteOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:remove')}
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:remove')}
onClick={() => handleDelete(record.id)}
>
@@ -277,7 +450,7 @@ const OutdoorFairList: React.FC = () => {
key="add"
type="primary"
icon={<PlusOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:add')}
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:add')}
onClick={() => {
setCurrentRow(undefined);
setModalVisible(true);
@@ -310,6 +483,115 @@ const OutdoorFairList: React.FC = () => {
}
}}
/>
<Modal
title="企业二维码"
open={qrModalOpen}
width={420}
footer={[
<Button
key="open"
type="primary"
onClick={() => qrInfo?.h5Url && window.open(qrInfo.h5Url)}
>
</Button>,
]}
destroyOnClose
onCancel={() => {
setQrModalOpen(false);
setQrInfo(null);
}}
>
{qrInfo?.qrCodeContent && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
<QRCode value={qrInfo.qrCodeContent} size={220} />
<Typography.Paragraph
copyable={{ text: qrInfo.qrCodeContent }}
style={{
width: '100%',
marginBottom: 0,
wordBreak: 'break-all',
textAlign: 'center',
}}
>
{qrInfo.qrCodeContent}
</Typography.Paragraph>
</div>
)}
</Modal>
<Modal
title="驳回原因"
open={rejectReasonModalOpen}
footer={null}
destroyOnClose
onCancel={() => {
setRejectReasonModalOpen(false);
setRejectReason('');
}}
>
<Typography.Paragraph style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
{rejectReason}
</Typography.Paragraph>
</Modal>
<Modal
title={selectedFair ? `${selectedFair.title} - 报名岗位` : '报名岗位'}
open={jobListModalOpen}
width={960}
footer={null}
destroyOnClose
onCancel={() => {
setJobListModalOpen(false);
setSelectedFair(null);
}}
>
{selectedFair && (
<ProTable<API.OutdoorFairDetail.PostedJob>
actionRef={jobActionRef}
rowKey="jobId"
options={false}
search={{ labelWidth: 'auto', defaultCollapsed: false }}
pagination={{ pageSize: 10 }}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={() => openJobEditor(selectedFair)}
>
</Button>,
]}
request={async (params) => {
const res = await getParticipatingJobs(selectedFair.id, {
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>
<JobEditModal
open={jobEditModalOpen}
fairId={selectedFair?.id || 0}
company={selectedFair ? getCurrentCompany(selectedFair) : null}
job={null}
companyScoped
educationEnum={educationEnum}
experienceEnum={experienceEnum}
areaEnum={areaEnum}
jobTypeEnum={jobTypeEnum}
onCancel={() => setJobEditModalOpen(false)}
onSuccess={() => {
setJobEditModalOpen(false);
jobActionRef.current?.reload();
actionRef.current?.reload();
}}
/>
</PageContainer>
);
};