Files
shz-admin/src/pages/Jobfair/Outdoorfair/index.tsx
lapuda 00ba967685 Add utility functions for handling public URLs and update API base URL logic
- Implement getProductionApiBaseUrl and normalizePublicUrl functions to manage API URLs based on the browser's origin.
- Update API base URL in request configuration to use getProductionApiBaseUrl.
- Enhance QR code handling in ParticipatingCompaniesTab and OutdoorFairList components to normalize URLs.
- Create a deployment script for frontend builds and server uploads.
- Add tests for the new URL utility functions.
2026-07-15 08:46:40 +08:00

605 lines
18 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react';
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,
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 { normalizePublicUrl } from '@/utils/publicUrl';
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([
getDictValueEnum(OUTDOOR_FAIR_TYPE_DICT),
getDictSelectOption(OUTDOOR_FAIR_TYPE_DICT),
getDictValueEnum(OUTDOOR_FAIR_REGION_DICT),
getDictSelectOption(OUTDOOR_FAIR_REGION_DICT),
]);
setFairTypeValueEnum(typeValueEnum);
setFairTypeOptions(typeOptions);
setRegionValueEnum(regionEnum);
setRegionOptions(regionSelectOptions);
}, []);
useEffect(() => {
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) {
setVenueOptions(
(res.rows || []).map((item) => ({
label: item.venueName,
value: item.id,
venueAddress: item.venueAddress,
floorCount: item.floorCount,
})),
);
}
}, []);
useEffect(() => {
if (!isEnterprise) {
refreshVenueOptions();
}
}, [isEnterprise, refreshVenueOptions]);
const handleCreateDictOption = async (values: OutdoorFairDictForm) => {
const res = await addOutdoorFairDictOption(values);
if (res.code === 200) {
await refreshDictOptions();
message.success('字典项新增成功');
return true;
}
message.error(res.msg || '字典项新增失败');
return false;
};
const handleDelete = async (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该户外招聘会吗?',
onOk: async () => {
const res = await deleteOutdoorFair(id);
if (res.code === 200) {
message.success('删除成功');
actionRef.current?.reload();
} else {
message.error(res.msg || '删除失败');
}
},
});
};
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,
h5Url: normalizePublicUrl(res.data.h5Url) || res.data.h5Url,
qrCodeContent: normalizePublicUrl(res.data.qrCodeContent) || res.data.qrCodeContent,
});
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: '招聘会标题',
dataIndex: 'title',
ellipsis: true,
},
{
title: '举办单位',
dataIndex: 'hostUnit',
ellipsis: true,
},
{
title: '招聘会类型',
dataIndex: 'fairType',
ellipsis: true,
valueType: 'select',
valueEnum: fairTypeValueEnum,
},
{
title: '举办区域',
dataIndex: 'region',
ellipsis: true,
valueType: 'select',
valueEnum: regionValueEnum,
},
{
title: '场地名称',
dataIndex: 'venueName',
ellipsis: true,
valueType: 'select',
fieldProps: {
options: venueOptions,
allowClear: true,
showSearch: true,
optionFilterProp: 'label',
},
search: {
transform: (value) => ({ venueId: value }),
},
render: (_, record) => record.venueName || '--',
},
{
title: '举办地址',
dataIndex: 'address',
ellipsis: true,
hideInSearch: true,
},
{
title: '展位数量',
dataIndex: 'boothCount',
hideInSearch: true,
},
{
title: '举办时间',
dataIndex: 'holdTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '截止时间',
dataIndex: 'endTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '开放申请',
dataIndex: 'applyStartTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '截止申请',
dataIndex: 'applyEndTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '线上申请',
dataIndex: 'onlineApply',
valueType: 'select',
valueEnum: {
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',
hideInSearch: true,
render: (_, record) =>
record.photoUrl ? (
<img
src={record.photoUrl}
alt={record.title}
style={{ width: 60, height: 45, objectFit: 'cover', borderRadius: 4 }}
/>
) : (
'--'
),
},
{
title: '操作',
valueType: 'option',
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}`)}
>
</Button>,
<Button
key="edit"
type="link"
size="small"
icon={<FormOutlined />}
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:edit')}
onClick={() => {
setCurrentRow(record);
setModalVisible(true);
}}
>
</Button>,
<Button
key="delete"
type="link"
size="small"
danger
icon={<DeleteOutlined />}
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:remove')}
onClick={() => handleDelete(record.id)}
>
</Button>,
],
},
];
return (
<PageContainer>
<ProTable<OutdoorFairItem>
headerTitle="户外招聘会管理列表"
actionRef={actionRef}
rowKey="id"
columns={columns}
scroll={{ x: 'max-content' }}
request={async (params) => {
const res = await getOutdoorFairList({
current: params.current,
pageSize: params.pageSize,
title: params.title,
hostUnit: params.hostUnit,
fairType: params.fairType,
region: params.region,
venueId: params.venueId,
onlineApply: params.onlineApply,
} as OutdoorFairListParams);
return { data: res.rows, total: res.total, success: true };
}}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:add')}
onClick={() => {
setCurrentRow(undefined);
setModalVisible(true);
}}
>
</Button>,
]}
/>
<EditModal
open={modalVisible}
values={currentRow}
fairTypeOptions={fairTypeOptions}
regionOptions={regionOptions}
venueOptions={venueOptions}
onCreateDictOption={handleCreateDictOption}
onCancel={() => {
setModalVisible(false);
setCurrentRow(undefined);
}}
onSubmit={async (values: OutdoorFairForm) => {
const res = values.id ? await updateOutdoorFair(values) : await addOutdoorFair(values);
if (res.code === 200) {
message.success(values.id ? '修改成功' : '新增成功');
setModalVisible(false);
setCurrentRow(undefined);
actionRef.current?.reload();
} else {
message.error(res.msg || '操作失败');
}
}}
/>
<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>
);
};
export default OutdoorFairList;