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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user