Files
shz-admin/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingCompaniesTab.tsx

607 lines
20 KiB
TypeScript
Raw Normal View History

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;