feat: add enterprise outdoor fair viewer
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
This commit is contained in:
@@ -231,6 +231,9 @@ const PortalJobFairDetailPage: React.FC = () => {
|
||||
{detail.boothNum && (
|
||||
<Descriptions.Item label="展位数量">{detail.boothNum}</Descriptions.Item>
|
||||
)}
|
||||
{channel === 'outdoor' && detail.boothMapCount !== undefined && (
|
||||
<Descriptions.Item label="摊位数量">{detail.boothMapCount}</Descriptions.Item>
|
||||
)}
|
||||
{detail.jobFairPhone && (
|
||||
<Descriptions.Item
|
||||
label={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useAccess } from '@umijs/max';
|
||||
import { Button, Card, Descriptions, Image, Tag, message } from 'antd';
|
||||
import { Button, Card, Descriptions, Divider, Image, Tag, message } from 'antd';
|
||||
import { FormOutlined } from '@ant-design/icons';
|
||||
import type { OutdoorFairDictForm, OutdoorFairItem } from '@/services/jobportal/outdoorFair';
|
||||
import { addOutdoorFairDictOption, updateOutdoorFair } from '@/services/jobportal/outdoorFair';
|
||||
@@ -164,6 +164,17 @@ const FairInfoTab: React.FC<Props> = ({ fairInfo, onFairUpdate }) => {
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>{fairInfo.fairProcess || '--'}</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider orientation="left" plain>
|
||||
企业参会专属信息(仅报名企业可见)
|
||||
</Divider>
|
||||
<Descriptions column={3} bordered size="small">
|
||||
<Descriptions.Item label="需携带资料" span={3}>
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>{fairInfo.requiredMaterials || '--'}</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="到场须知" span={3}>
|
||||
<div style={{ whiteSpace: 'pre-wrap' }}>{fairInfo.attendanceNotes || '--'}</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card title="场地信息" style={{ marginTop: 16 }}>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Empty, Modal, Space, Spin, Tag, Tooltip, message } from 'antd';
|
||||
import {
|
||||
getCurrentCompanyOutdoorFairBoothMap,
|
||||
type OutdoorFairCompanyBoothMap,
|
||||
} from '@/services/jobportal/outdoorFair';
|
||||
import type { VenueBoothItem } from '@/services/jobportal/venueInfo';
|
||||
|
||||
interface Props {
|
||||
fairId?: number;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const BOOTH_WIDTH = 76;
|
||||
const BOOTH_HEIGHT = 48;
|
||||
const BOOTH_PADDING = 44;
|
||||
|
||||
const CompanyBoothMapModal: React.FC<Props> = ({ fairId, open, onCancel }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mapData, setMapData] = useState<OutdoorFairCompanyBoothMap>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !fairId) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const loadBooths = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getCurrentCompanyOutdoorFairBoothMap(fairId);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (res.code === 200 && res.data) {
|
||||
setMapData(res.data);
|
||||
} else {
|
||||
setMapData(undefined);
|
||||
message.error(res.msg || '展位图加载失败');
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setMapData(undefined);
|
||||
message.error('展位图加载失败');
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
void loadBooths();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fairId, open]);
|
||||
|
||||
const canvasSize = useMemo(() => {
|
||||
const booths = mapData?.booths || [];
|
||||
const maxX = booths.reduce((max, booth) => Math.max(max, booth.positionX || 0), 0);
|
||||
const maxY = booths.reduce((max, booth) => Math.max(max, booth.positionY || 0), 0);
|
||||
return {
|
||||
width: Math.max(920, maxX + BOOTH_WIDTH + BOOTH_PADDING),
|
||||
height: Math.max(480, maxY + BOOTH_HEIGHT + BOOTH_PADDING),
|
||||
};
|
||||
}, [mapData?.booths]);
|
||||
|
||||
const renderBooth = (booth: VenueBoothItem, index: number) => {
|
||||
const reserved = booth.status === 'reserved';
|
||||
const isCurrentCompany = reserved && booth.companyId === mapData?.currentCompanyId;
|
||||
const color = isCurrentCompany ? '#f5222d' : reserved ? '#52c41a' : '#1890ff';
|
||||
const stateText = isCurrentCompany ? '本企业' : reserved ? '已预定' : '未预定';
|
||||
const tooltip = reserved ? `${booth.boothNumber}:${booth.companyName || stateText}` : `${booth.boothNumber}:未预定`;
|
||||
return (
|
||||
<Tooltip title={tooltip} key={booth.boothId || booth.id || `${booth.boothNumber}-${index}`}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: booth.positionX || 0,
|
||||
top: booth.positionY || 0,
|
||||
width: BOOTH_WIDTH,
|
||||
height: BOOTH_HEIGHT,
|
||||
padding: '4px 2px',
|
||||
borderRadius: 4,
|
||||
color: '#fff',
|
||||
background: color,
|
||||
boxShadow: isCurrentCompany ? '0 0 0 3px rgba(245, 34, 45, 0.22)' : undefined,
|
||||
fontSize: 12,
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<div>{booth.boothNumber}</div>
|
||||
<div style={{ fontSize: 11 }}>{stateText}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="展位图(红色为本企业展位)"
|
||||
open={open}
|
||||
width={1180}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Space wrap size={12} style={{ marginBottom: 16 }}>
|
||||
<Tag color="red">本企业展位</Tag>
|
||||
<Tag color="green">其他已预定展位</Tag>
|
||||
<Tag color="blue">未预定展位</Tag>
|
||||
<span style={{ color: '#999' }}>展位图仅供查看,不能编辑或调整。</span>
|
||||
</Space>
|
||||
{mapData?.booths?.length ? (
|
||||
<div
|
||||
role="img"
|
||||
aria-label="招聘会只读展位图"
|
||||
style={{
|
||||
maxHeight: '65vh',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
position: 'relative',
|
||||
backgroundSize: '20px 20px',
|
||||
backgroundImage:
|
||||
'linear-gradient(to right, rgba(0,0,0,0.04) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.04) 1px, transparent 1px)',
|
||||
}}
|
||||
>
|
||||
{mapData.booths.map(renderBooth)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
!loading && <Empty description="当前招聘会暂未配置展位图" />
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyBoothMapModal;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Descriptions, Divider, Image, Modal, Tag } from 'antd';
|
||||
import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair';
|
||||
|
||||
interface Props {
|
||||
fair?: OutdoorFairItem | null;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const content = (value?: string) => <div style={{ whiteSpace: 'pre-wrap' }}>{value || '--'}</div>;
|
||||
|
||||
const CompanyFairDetailModal: React.FC<Props> = ({ fair, open, onCancel }) => (
|
||||
<Modal
|
||||
title="招聘会详情"
|
||||
open={open}
|
||||
width={920}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
onCancel={onCancel}
|
||||
>
|
||||
{fair && (
|
||||
<>
|
||||
<Descriptions bordered column={2} size="small">
|
||||
<Descriptions.Item label="招聘会标题" span={2}>
|
||||
{fair.title || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="举办单位">{fair.hostUnit || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="招聘会类型">{fair.fairType || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="举办区域">{fair.region || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="绑定场地">{fair.venueName || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="举办地址" span={2}>
|
||||
{fair.address || '--'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="展位数量">{fair.boothCount ?? '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="已预定摊位数">{fair.reservedBoothCount ?? 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="举办时间">{fair.holdTime || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="截止时间">{fair.endTime || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="开放申请">{fair.applyStartTime || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="截止申请">{fair.applyEndTime || '--'}</Descriptions.Item>
|
||||
<Descriptions.Item label="线上申请">
|
||||
<Tag color={fair.onlineApply ? 'green' : 'default'}>{fair.onlineApply ? '是' : '否'}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="招聘会照片">
|
||||
{fair.photoUrl ? (
|
||||
<Image src={fair.photoUrl} alt={fair.title} width={180} style={{ objectFit: 'cover' }} />
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="招聘会简介" span={2}>
|
||||
{content(fair.fairIntroduction)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="招聘会整体流程" span={2}>
|
||||
{content(fair.fairProcess)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider orientation="left" plain>
|
||||
企业参会专属信息
|
||||
</Divider>
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label="需携带资料">{content(fair.requiredMaterials)}</Descriptions.Item>
|
||||
<Descriptions.Item label="到场须知">{content(fair.attendanceNotes)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export default CompanyFairDetailModal;
|
||||
@@ -310,6 +310,21 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
placeholder="请输入招聘会整体流程"
|
||||
fieldProps={{ maxLength: 5000, showCount: true, autoSize: { minRows: 4, maxRows: 8 } }}
|
||||
/>
|
||||
<Divider orientation="left" plain>
|
||||
企业参会专属信息(仅报名企业可见)
|
||||
</Divider>
|
||||
<ProFormTextArea
|
||||
name="requiredMaterials"
|
||||
label="需携带资料"
|
||||
placeholder="请输入企业参加招聘会需携带的资料"
|
||||
fieldProps={{ maxLength: 5000, showCount: true, autoSize: { minRows: 3, maxRows: 8 } }}
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="attendanceNotes"
|
||||
label="到场须知"
|
||||
placeholder="请输入企业到场须知"
|
||||
fieldProps={{ maxLength: 5000, showCount: true, autoSize: { minRows: 3, maxRows: 8 } }}
|
||||
/>
|
||||
<ProForm.Item label="招聘会照片" required>
|
||||
<Space direction="vertical" size={12}>
|
||||
<Upload
|
||||
|
||||
@@ -31,6 +31,8 @@ import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
|
||||
import { getVenueInfoList } from '@/services/jobportal/venueInfo';
|
||||
import { normalizePublicUrl } from '@/utils/publicUrl';
|
||||
import EditModal from './components/EditModal';
|
||||
import CompanyBoothMapModal from './components/CompanyBoothMapModal';
|
||||
import CompanyFairDetailModal from './components/CompanyFairDetailModal';
|
||||
import JobEditModal from './Detail/components/JobEditModal';
|
||||
|
||||
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
|
||||
@@ -51,6 +53,8 @@ const OutdoorFairList: React.FC = () => {
|
||||
const [qrInfo, setQrInfo] = useState<OutdoorFairCompanyQrCodeInfo | null>(null);
|
||||
const [rejectReasonModalOpen, setRejectReasonModalOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [companyDetailFair, setCompanyDetailFair] = useState<OutdoorFairItem | null>(null);
|
||||
const [companyBoothFair, setCompanyBoothFair] = useState<OutdoorFairItem | null>(null);
|
||||
const [fairTypeValueEnum, setFairTypeValueEnum] = useState<Record<string, any>>({});
|
||||
const [fairTypeOptions, setFairTypeOptions] = useState<any[]>([]);
|
||||
const [regionValueEnum, setRegionValueEnum] = useState<Record<string, any>>({});
|
||||
@@ -202,6 +206,14 @@ const OutdoorFairList: React.FC = () => {
|
||||
setRejectReasonModalOpen(true);
|
||||
};
|
||||
|
||||
const openCompanyDetail = (record: OutdoorFairItem) => {
|
||||
setCompanyDetailFair(record);
|
||||
};
|
||||
|
||||
const openCompanyBoothMap = (record: OutdoorFairItem) => {
|
||||
setCompanyBoothFair(record);
|
||||
};
|
||||
|
||||
const jobColumns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
@@ -362,7 +374,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: isEnterprise ? 280 : 160,
|
||||
width: isEnterprise ? 520 : 160,
|
||||
fixed: 'right',
|
||||
render: (_, record) => [
|
||||
isEnterprise && !record.signedUp ? (
|
||||
@@ -397,6 +409,16 @@ const OutdoorFairList: React.FC = () => {
|
||||
查看报名岗位
|
||||
</Button>
|
||||
) : null,
|
||||
isEnterprise ? (
|
||||
<Button key="companyDetail" type="link" size="small" onClick={() => openCompanyDetail(record)}>
|
||||
招聘会详情
|
||||
</Button>
|
||||
) : null,
|
||||
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
|
||||
<Button key="boothMap" type="link" size="small" onClick={() => openCompanyBoothMap(record)}>
|
||||
查看展位图
|
||||
</Button>
|
||||
) : null,
|
||||
<Button
|
||||
key="detail"
|
||||
type="link"
|
||||
@@ -544,6 +566,16 @@ const OutdoorFairList: React.FC = () => {
|
||||
{rejectReason}
|
||||
</Typography.Paragraph>
|
||||
</Modal>
|
||||
<CompanyFairDetailModal
|
||||
fair={companyDetailFair}
|
||||
open={!!companyDetailFair}
|
||||
onCancel={() => setCompanyDetailFair(null)}
|
||||
/>
|
||||
<CompanyBoothMapModal
|
||||
fairId={companyBoothFair?.id}
|
||||
open={!!companyBoothFair}
|
||||
onCancel={() => setCompanyBoothFair(null)}
|
||||
/>
|
||||
<Modal
|
||||
title={selectedFair ? `${selectedFair.title} - 报名岗位` : '报名岗位'}
|
||||
open={jobListModalOpen}
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface PortalJobFairItem {
|
||||
jobFairImage?: string;
|
||||
enterpriseNum?: string;
|
||||
boothNum?: string;
|
||||
/** 户外招聘会展位图中的实际摊位数量 */
|
||||
boothMapCount?: number;
|
||||
divisionName?: string;
|
||||
jobFairCategory?: string;
|
||||
jobFairRegion?: string;
|
||||
@@ -69,6 +71,7 @@ interface PortalOutdoorFairDetailData extends Partial<PortalJobFairItem> {
|
||||
venueName?: string;
|
||||
address?: string;
|
||||
boothCount?: number;
|
||||
boothMapCount?: number;
|
||||
holdTime?: string;
|
||||
endTime?: string;
|
||||
photoUrl?: string;
|
||||
@@ -206,6 +209,7 @@ export async function getPortalJobFairDetail(channel: JobFairChannel, jobFairId:
|
||||
jobFairImage: fair.jobFairImage || fair.photoUrl,
|
||||
boothNum:
|
||||
fair.boothNum || (fair.boothCount === undefined ? undefined : String(fair.boothCount)),
|
||||
boothMapCount: fair.boothMapCount,
|
||||
jobFairRegion: fair.jobFairRegion || fair.region,
|
||||
jobFairVenueName: fair.jobFairVenueName || fair.venueName,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '@umijs/max';
|
||||
import type { VenueBoothItem } from './venueInfo';
|
||||
|
||||
/** 户外招聘会列表项 */
|
||||
export interface OutdoorFairItem {
|
||||
@@ -37,6 +38,10 @@ export interface OutdoorFairItem {
|
||||
fairIntroduction?: string;
|
||||
/** 招聘会整体流程 */
|
||||
fairProcess?: string;
|
||||
/** 企业参加招聘会需携带的资料 */
|
||||
requiredMaterials?: string;
|
||||
/** 企业到场须知 */
|
||||
attendanceNotes?: string;
|
||||
/** 当前企业ID */
|
||||
companyId?: number;
|
||||
/** 当前企业名称 */
|
||||
@@ -96,6 +101,10 @@ export interface OutdoorFairForm {
|
||||
fairIntroduction?: string;
|
||||
/** 招聘会整体流程 */
|
||||
fairProcess?: string;
|
||||
/** 企业参加招聘会需携带的资料 */
|
||||
requiredMaterials?: string;
|
||||
/** 企业到场须知 */
|
||||
attendanceNotes?: string;
|
||||
/** 更换场地时确认清空旧展位图与预定,并按新模板重建 */
|
||||
resetBoothMap?: boolean;
|
||||
}
|
||||
@@ -125,6 +134,12 @@ export interface OutdoorFairCompanyQrCodeInfo {
|
||||
qrCodeContent: string;
|
||||
}
|
||||
|
||||
/** 企业审核通过后可读取的只读展位图。 */
|
||||
export interface OutdoorFairCompanyBoothMap {
|
||||
currentCompanyId: number;
|
||||
booths: VenueBoothItem[];
|
||||
}
|
||||
|
||||
/** 户外招聘会字典新增表单 */
|
||||
export interface OutdoorFairDictForm {
|
||||
dictType: 'outdoor_fair_type' | 'outdoor_fair_region';
|
||||
@@ -169,6 +184,17 @@ export async function getOutdoorFairInfo(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前企业查看户外招聘会展位图。
|
||||
* 服务端会校验当前企业已报名且审核通过,接口只提供读取能力。
|
||||
*/
|
||||
export async function getCurrentCompanyOutdoorFairBoothMap(fairId: number) {
|
||||
return request<{ code: number; msg?: string; data?: OutdoorFairCompanyBoothMap }>(
|
||||
`${CMS_OUTDOOR_FAIR_BASE}/${fairId}/company/booth-map`,
|
||||
{ method: 'GET' },
|
||||
);
|
||||
}
|
||||
|
||||
/** 招聘会统计-时间桶序列项(曲线图用) */
|
||||
export interface OutdoorFairStatisticsSeriesItem {
|
||||
time: string;
|
||||
|
||||
Reference in New Issue
Block a user