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

607 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;