607 lines
20 KiB
TypeScript
607 lines
20 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
||
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 { normalizePublicUrl } from '@/utils/publicUrl';
|
||
import {
|
||
getParticipatingCompanyQrCode,
|
||
getParticipatingCompanies,
|
||
getParticipatingJobDetail,
|
||
getParticipatingJobs,
|
||
removeJobFromCompany,
|
||
removeParticipatingCompany,
|
||
reviewParticipatingCompany,
|
||
reviewParticipatingJob,
|
||
} from '@/services/jobportal/outdoorFairDetail';
|
||
import CompanyAddModal from './CompanyAddModal';
|
||
import JobEditModal from './JobEditModal';
|
||
|
||
interface Props {
|
||
fairId: number;
|
||
}
|
||
|
||
type ReviewTarget =
|
||
| {
|
||
type: 'company';
|
||
record: API.OutdoorFairDetail.ParticipatingCompany;
|
||
}
|
||
| {
|
||
type: 'job';
|
||
record: API.OutdoorFairDetail.PostedJob;
|
||
};
|
||
|
||
const REVIEW_STATUS_ENUM = {
|
||
'0': { text: '待审核', status: 'Processing' },
|
||
'1': { text: '已通过', status: 'Success' },
|
||
'2': { text: '已驳回', status: 'Error' },
|
||
};
|
||
|
||
const formatSearchDateTime = (value: any) =>
|
||
value && typeof value.format === 'function' ? value.format('YYYY-MM-DD HH:mm:ss') : value;
|
||
|
||
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([
|
||
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 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,
|
||
h5Url: normalizePublicUrl(res.data.h5Url) || res.data.h5Url,
|
||
qrCodeContent: normalizePublicUrl(res.data.qrCodeContent) || res.data.qrCodeContent,
|
||
});
|
||
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 },
|
||
{
|
||
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: 'isPublish',
|
||
hideInSearch: true,
|
||
width: 90,
|
||
render: (_, record) => (
|
||
<Tag color={record.isPublish === 1 ? 'green' : 'red'}>
|
||
{record.isPublish === 1 ? '已发布' : '未发布'}
|
||
</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
valueType: 'option',
|
||
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"
|
||
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,
|
||
reviewStatus: params.reviewStatus,
|
||
startTime: formatSearchDateTime(params.startTime),
|
||
endTime: formatSearchDateTime(params.endTime),
|
||
});
|
||
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
|
||
}}
|
||
columns={[
|
||
{
|
||
title: '企业名称',
|
||
dataIndex: 'companyName',
|
||
ellipsis: true,
|
||
fieldProps: {
|
||
id: 'outdoor-participating-company-name-search',
|
||
name: 'companyName',
|
||
'aria-label': '企业名称',
|
||
autoComplete: 'off',
|
||
},
|
||
},
|
||
{
|
||
title: '报名开始时间',
|
||
dataIndex: 'startTime',
|
||
hideInTable: true,
|
||
valueType: 'dateTime',
|
||
fieldProps: {
|
||
id: 'outdoor-participating-company-start-time-search',
|
||
name: 'startTime',
|
||
'aria-label': '报名开始时间',
|
||
format: 'YYYY-MM-DD HH:mm:ss',
|
||
},
|
||
search: {
|
||
transform: (value) => ({ startTime: formatSearchDateTime(value) }),
|
||
},
|
||
},
|
||
{
|
||
title: '报名结束时间',
|
||
dataIndex: 'endTime',
|
||
hideInTable: true,
|
||
valueType: 'dateTime',
|
||
fieldProps: {
|
||
id: 'outdoor-participating-company-end-time-search',
|
||
name: 'endTime',
|
||
'aria-label': '报名结束时间',
|
||
format: 'YYYY-MM-DD HH:mm:ss',
|
||
},
|
||
search: {
|
||
transform: (value) => ({ endTime: formatSearchDateTime(value) }),
|
||
},
|
||
},
|
||
{
|
||
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: 'reviewStatus',
|
||
valueType: 'select',
|
||
valueEnum: REVIEW_STATUS_ENUM,
|
||
fieldProps: {
|
||
id: 'outdoor-participating-company-review-status-search',
|
||
name: 'reviewStatus',
|
||
'aria-label': '审核状态',
|
||
},
|
||
render: (_, record) => renderReviewStatus(record.reviewStatus),
|
||
},
|
||
{
|
||
title: '报名时间',
|
||
dataIndex: 'registrationTime',
|
||
valueType: 'dateTime',
|
||
hideInSearch: true,
|
||
width: 170,
|
||
},
|
||
{
|
||
title: '展位号',
|
||
dataIndex: 'boothNumber',
|
||
hideInSearch: true,
|
||
render: (_, record) => record.boothNumber || '--',
|
||
},
|
||
{ title: '岗位数', dataIndex: 'jobCount', hideInSearch: true, width: 80 },
|
||
{
|
||
title: '操作',
|
||
valueType: 'option',
|
||
width: 460,
|
||
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="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"
|
||
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>
|
||
|
||
<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}
|
||
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;
|