From 13256db26d58c6c7ce4bbddadf37a2f3c11ba617 Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Tue, 21 Jul 2026 16:17:06 +0800 Subject: [PATCH] feat: add job fair position functionality with detailed card display and data fetching --- src/pages/JobPortal/JobFair/Detail/index.less | 84 ++++++++++ src/pages/JobPortal/JobFair/Detail/index.tsx | 145 +++++++++++++++++- src/services/jobportal/jobFair.ts | 57 +++++++ 3 files changed, 285 insertions(+), 1 deletion(-) diff --git a/src/pages/JobPortal/JobFair/Detail/index.less b/src/pages/JobPortal/JobFair/Detail/index.less index f06e381..2192560 100644 --- a/src/pages/JobPortal/JobFair/Detail/index.less +++ b/src/pages/JobPortal/JobFair/Detail/index.less @@ -47,6 +47,90 @@ line-height: 1.85; } } + + .job-fair-position-section { + margin-top: 28px; + } + + .job-fair-position-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 16px; + + .ant-typography { + margin: 0; + color: @jp-text-primary; + } + } + + .job-fair-position-card { + height: 100%; + .jp-card-base(); + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: @jp-primary-border; + box-shadow: @jp-shadow-hover; + } + + .job-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; + + .ant-typography { + flex: 1; + margin: 0; + color: @jp-text-primary; + font-size: 16px; + font-weight: 600; + } + + .job-salary { + .jp-salary-text(); + flex-shrink: 0; + font-size: 15px; + white-space: nowrap; + } + } + + .job-company { + display: block; + margin-bottom: 10px; + color: @jp-text-muted; + font-size: 13px; + } + + .job-info { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; + + .job-location, + .job-experience, + .job-education { + color: @jp-text-muted; + font-size: 12px; + } + } + + .job-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + + .job-tag { + .jp-job-tag(); + padding: 2px 8px; + font-size: 12px; + } + } + } } @media (max-width: 576px) { diff --git a/src/pages/JobPortal/JobFair/Detail/index.tsx b/src/pages/JobPortal/JobFair/Detail/index.tsx index 3d279a3..28dbc1e 100644 --- a/src/pages/JobPortal/JobFair/Detail/index.tsx +++ b/src/pages/JobPortal/JobFair/Detail/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Button, Card, Descriptions, Empty, Spin, Tag, Typography } from 'antd'; +import { Button, Card, Col, Descriptions, Empty, Row, Spin, Tag, Typography } from 'antd'; import { ArrowLeftOutlined, CalendarOutlined, @@ -13,8 +13,10 @@ import dayjs from 'dayjs'; import JobPortalHeader from '@/components/JobPortalHeader'; import { getPortalJobFairDetail, + getPortalJobFairPositions, type JobFairChannel, type PortalJobFairItem, + type PortalJobFairPosition, } from '@/services/jobportal/jobFair'; import '../index.less'; import './index.less'; @@ -33,6 +35,45 @@ function formatDateTime(value?: string) { return date.isValid() ? date.format('YYYY年MM月DD日 HH:mm') : value; } +const educationMap: Record = { + '0': '学历不限', + '1': '大专', + '2': '本科', + '3': '硕士', + '4': '博士', +}; + +const experienceMap: Record = { + '0': '经验不限', + '1': '1年以下', + '2': '1-3年', + '3': '3-5年', + '4': '5-10年', + '5': '10年以上', +}; + +function formatSalary(position: PortalJobFairPosition) { + const { minSalary, maxSalary } = position; + if (minSalary && maxSalary) { + return `${minSalary / 1000}K-${maxSalary / 1000}K`; + } + if (minSalary) { + return `${minSalary / 1000}K起`; + } + if (maxSalary) { + return `${maxSalary / 1000}K以下`; + } + return '薪资面议'; +} + +function formatRequirement( + value: string | undefined, + map: Record, + fallback: string, +) { + return value ? map[value] || value : fallback; +} + const PortalJobFairDetailPage: React.FC = () => { const location = useLocation(); const fallbackDetail = (location.state as JobFairDetailLocationState | null)?.fair; @@ -45,6 +86,8 @@ const PortalJobFairDetailPage: React.FC = () => { }, [location.search]); const [detail, setDetail] = useState(); const [loading, setLoading] = useState(true); + const [positions, setPositions] = useState([]); + const [positionsLoading, setPositionsLoading] = useState(false); useEffect(() => { if (!jobFairId) { @@ -79,8 +122,49 @@ const PortalJobFairDetailPage: React.FC = () => { }; }, [channel, fallbackDetail, jobFairId]); + useEffect(() => { + if (!jobFairId || channel === 'outdoor') { + setPositions([]); + setPositionsLoading(false); + return; + } + + let cancelled = false; + const loadPositions = async () => { + setPositionsLoading(true); + try { + const response = await getPortalJobFairPositions(jobFairId); + if (!cancelled) { + setPositions(response?.code === 200 ? response.data || [] : []); + } + } catch (error) { + if (!cancelled) { + setPositions([]); + } + } finally { + if (!cancelled) { + setPositionsLoading(false); + } + } + }; + + void loadPositions(); + return () => { + cancelled = true; + }; + }, [channel, jobFairId]); + const backToList = () => history.push('/job-portal/job-fair'); + const handlePositionClick = (position: PortalJobFairPosition) => { + if (!position.jobId) { + return; + } + history.push(`/job-portal/detail?jobId=${encodeURIComponent(String(position.jobId))}`, { + jobData: position, + }); + }; + return (
@@ -174,6 +258,65 @@ const PortalJobFairDetailPage: React.FC = () => { ) )} + {channel === 'online' && detail && ( +
+
+ 招聘岗位 + 共 {positions.length} 个岗位 +
+ + {positions.length ? ( + + {positions.map((position, index) => ( + + handlePositionClick(position)} + > +
+ {position.jobTitle || '未命名岗位'} + + {formatSalary(position)} + +
+ + {position.companyName || '招聘企业暂未公布'} + +
+ + {' '} + {position.jobLocation || position.jobAddress || '地点不限'} + + + {formatRequirement(position.experience, experienceMap, '经验不限')} + + + {formatRequirement(position.education, educationMap, '学历不限')} + +
+ {position.vacancies && ( +
+ 招聘 {position.vacancies} 人 +
+ )} +
+ + ))} +
+ ) : ( + !positionsLoading && ( + + ) + )} +
+
+ )}
); diff --git a/src/services/jobportal/jobFair.ts b/src/services/jobportal/jobFair.ts index cb46e1b..91da573 100644 --- a/src/services/jobportal/jobFair.ts +++ b/src/services/jobportal/jobFair.ts @@ -95,6 +95,40 @@ export interface PortalJobFairDetailResult { data?: PortalJobFairItem; } +/** 招聘会关联岗位;由公共接口中的企业岗位列表展平得到。 */ +export interface PortalJobFairPosition { + jobId?: number | string; + fairRelationId?: string; + jobTitle?: string; + minSalary?: number; + maxSalary?: number; + education?: string; + experience?: string; + companyId?: number | string; + companyName?: string; + jobLocation?: string; + jobAddress?: string; + vacancies?: number | string; +} + +interface PortalJobFairCompanyWithJobs { + companyId?: number | string; + companyName?: string; + jobInfoList?: PortalJobFairPosition[]; +} + +interface PortalJobFairCompaniesWithJobsResult { + code: number; + msg?: string; + data?: PortalJobFairCompanyWithJobs[]; +} + +export interface PortalJobFairPositionResult { + code: number; + msg?: string; + data?: PortalJobFairPosition[]; +} + const FAIR_API = { online: '/app/jobfair/public/jobfair', outdoor: '/app/outdoor-fair', @@ -172,3 +206,26 @@ export async function getPortalJobFairDetail(channel: JobFairChannel, jobFairId: }, } as PortalJobFairDetailResult; } + +/** 获取线上招聘会的关联岗位,并将企业分组数据转换为岗位卡片数据。 */ +export async function getPortalJobFairPositions(jobFairId: string) { + const response = await request( + `${FAIR_API.online}/enterprises-with-jobs-by-job-fair-id`, + { + method: 'GET', + params: { jobFairId }, + }, + ); + + return { + code: response.code, + msg: response.msg, + data: (response.data || []).flatMap((company) => + (company.jobInfoList || []).map((position) => ({ + ...position, + companyId: position.companyId || company.companyId, + companyName: position.companyName || company.companyName, + })), + ), + } as PortalJobFairPositionResult; +}