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,15 +1,18 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Modal, Tag, message } from 'antd';
|
||||
import { Button, Form, Input, Modal, QRCode, Select, Tag, Typography, 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 {
|
||||
getParticipatingCompanyQrCode,
|
||||
getParticipatingCompanies,
|
||||
getParticipatingJobDetail,
|
||||
getParticipatingJobs,
|
||||
removeJobFromCompany,
|
||||
removeParticipatingCompany,
|
||||
reviewParticipatingCompany,
|
||||
reviewParticipatingJob,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
import CompanyAddModal from './CompanyAddModal';
|
||||
import JobEditModal from './JobEditModal';
|
||||
@@ -18,20 +21,46 @@ interface Props {
|
||||
fairId: number;
|
||||
}
|
||||
|
||||
type ReviewTarget =
|
||||
| {
|
||||
type: 'company';
|
||||
record: API.OutdoorFairDetail.ParticipatingCompany;
|
||||
}
|
||||
| {
|
||||
type: 'job';
|
||||
record: API.OutdoorFairDetail.PostedJob;
|
||||
};
|
||||
|
||||
const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
const companyActionRef = useRef<ActionType>();
|
||||
const jobActionRef = useRef<ActionType>();
|
||||
const [reviewForm] = Form.useForm();
|
||||
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 [qrModalOpen, setQrModalOpen] = useState(false);
|
||||
const [qrInfo, setQrInfo] = useState<API.OutdoorFairDetail.CompanyQrCodeInfo | null>(null);
|
||||
const [qrCompanyName, setQrCompanyName] = useState('');
|
||||
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>({});
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
||||
const [reviewTarget, setReviewTarget] = useState<ReviewTarget | null>(null);
|
||||
|
||||
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 || '1'] || map['1'];
|
||||
return <Tag color={item.color}>{item.text}</Tag>;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -63,6 +92,67 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
setJobEditModalOpen(true);
|
||||
};
|
||||
|
||||
const openQrCodeModal = async (record: API.OutdoorFairDetail.ParticipatingCompany) => {
|
||||
const res = await getParticipatingCompanyQrCode(fairId, record.companyId);
|
||||
if (res.code !== 200) {
|
||||
message.error(res.msg || '二维码内容获取失败');
|
||||
return;
|
||||
}
|
||||
setQrInfo(res.data);
|
||||
setQrCompanyName(record.companyName);
|
||||
setQrModalOpen(true);
|
||||
};
|
||||
|
||||
const openReviewModal = (target: ReviewTarget) => {
|
||||
setReviewTarget(target);
|
||||
reviewForm.setFieldsValue({
|
||||
reviewStatus:
|
||||
target.type === 'company'
|
||||
? target.record.reviewStatus || '0'
|
||||
: target.record.fairReviewStatus || '0',
|
||||
reviewRemark:
|
||||
target.type === 'company'
|
||||
? target.record.reviewRemark || ''
|
||||
: target.record.fairReviewRemark || '',
|
||||
});
|
||||
setReviewModalOpen(true);
|
||||
};
|
||||
|
||||
const submitReview = async () => {
|
||||
const values = await reviewForm.validateFields();
|
||||
if (!reviewTarget) return;
|
||||
const payload = {
|
||||
reviewStatus: values.reviewStatus as '0' | '1' | '2',
|
||||
reviewRemark: values.reviewRemark,
|
||||
};
|
||||
const res =
|
||||
reviewTarget.type === 'company'
|
||||
? await reviewParticipatingCompany({
|
||||
fairId,
|
||||
id: reviewTarget.record.id,
|
||||
...payload,
|
||||
})
|
||||
: await reviewParticipatingJob({
|
||||
fairId,
|
||||
jobId: reviewTarget.record.jobId!,
|
||||
...payload,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('审核成功');
|
||||
setReviewModalOpen(false);
|
||||
setReviewTarget(null);
|
||||
reviewForm.resetFields();
|
||||
if (reviewTarget.type === 'company') {
|
||||
companyActionRef.current?.reload();
|
||||
} else {
|
||||
jobActionRef.current?.reload();
|
||||
companyActionRef.current?.reload();
|
||||
}
|
||||
} else {
|
||||
message.error(res.msg || '审核失败');
|
||||
}
|
||||
};
|
||||
|
||||
const jobColumns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
@@ -85,6 +175,13 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
width: 110,
|
||||
},
|
||||
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'fairReviewStatus',
|
||||
hideInSearch: true,
|
||||
width: 100,
|
||||
render: (_, record) => renderReviewStatus(record.fairReviewStatus),
|
||||
},
|
||||
{
|
||||
title: '发布状态',
|
||||
dataIndex: 'isPublish',
|
||||
@@ -99,11 +196,19 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 130,
|
||||
width: 240,
|
||||
render: (_, record) => [
|
||||
<Button key="edit" type="link" size="small" onClick={() => openJobEditor(record)}>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button
|
||||
key="review"
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => openReviewModal({ type: 'job', record })}
|
||||
>
|
||||
审核
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="link"
|
||||
@@ -140,7 +245,12 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCompanyModalOpen(true)}>
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCompanyModalOpen(true)}
|
||||
>
|
||||
添加企业
|
||||
</Button>,
|
||||
]}
|
||||
@@ -180,6 +290,12 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
},
|
||||
{ title: '联系人', dataIndex: 'contactPerson', hideInSearch: true },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', hideInSearch: true },
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'reviewStatus',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => renderReviewStatus(record.reviewStatus),
|
||||
},
|
||||
{
|
||||
title: '展位号',
|
||||
dataIndex: 'boothNumber',
|
||||
@@ -190,7 +306,7 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 220,
|
||||
width: 460,
|
||||
render: (_, record) => [
|
||||
<Button
|
||||
key="jobs"
|
||||
@@ -214,6 +330,17 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
>
|
||||
添加岗位
|
||||
</Button>,
|
||||
<Button key="qrcode" type="link" size="small" onClick={() => openQrCodeModal(record)}>
|
||||
查看二维码
|
||||
</Button>,
|
||||
<Button
|
||||
key="review"
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => openReviewModal({ type: 'company', record })}
|
||||
>
|
||||
审核
|
||||
</Button>,
|
||||
<Button
|
||||
key="remove"
|
||||
type="link"
|
||||
@@ -256,7 +383,12 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openJobEditor()}>
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => openJobEditor()}
|
||||
>
|
||||
添加岗位
|
||||
</Button>,
|
||||
]}
|
||||
@@ -276,6 +408,97 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={qrCompanyName ? `${qrCompanyName} — 企业二维码` : '企业二维码'}
|
||||
open={qrModalOpen}
|
||||
width={420}
|
||||
footer={[
|
||||
<Button
|
||||
key="open"
|
||||
type="primary"
|
||||
onClick={() => qrInfo?.h5Url && window.open(qrInfo.h5Url)}
|
||||
>
|
||||
打开链接
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setQrModalOpen(false);
|
||||
setQrInfo(null);
|
||||
setQrCompanyName('');
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
{!qrInfo.deviceCode && (
|
||||
<Typography.Text type="secondary">
|
||||
当前企业未绑定设备码,二维码使用企业公共页链接。
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
reviewTarget?.type === 'company'
|
||||
? `审核报名 — ${reviewTarget.record.companyName}`
|
||||
: `审核岗位 — ${reviewTarget?.record.jobTitle || ''}`
|
||||
}
|
||||
open={reviewModalOpen}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setReviewModalOpen(false);
|
||||
setReviewTarget(null);
|
||||
reviewForm.resetFields();
|
||||
}}
|
||||
onOk={submitReview}
|
||||
>
|
||||
<Form form={reviewForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="reviewStatus"
|
||||
label="审核状态"
|
||||
rules={[{ required: true, message: '请选择审核状态' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '待审核', value: '0' },
|
||||
{ label: '已通过', value: '1' },
|
||||
{ label: '已驳回', value: '2' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.reviewStatus !== cur.reviewStatus}>
|
||||
{({ getFieldValue }) => (
|
||||
<Form.Item
|
||||
name="reviewRemark"
|
||||
label="审核原因"
|
||||
rules={[
|
||||
{
|
||||
required: getFieldValue('reviewStatus') === '2',
|
||||
message: '驳回时请填写原因',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="驳回时请输入原因" maxLength={500} showCount />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<CompanyAddModal
|
||||
open={companyModalOpen}
|
||||
fairId={fairId}
|
||||
|
||||
Reference in New Issue
Block a user