- 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.
605 lines
18 KiB
TypeScript
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;
|