feat: add job fair position functionality with detailed card display and data fetching
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:
@@ -47,6 +47,90 @@
|
|||||||
line-height: 1.85;
|
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) {
|
@media (max-width: 576px) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
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 {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
@@ -13,8 +13,10 @@ import dayjs from 'dayjs';
|
|||||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||||
import {
|
import {
|
||||||
getPortalJobFairDetail,
|
getPortalJobFairDetail,
|
||||||
|
getPortalJobFairPositions,
|
||||||
type JobFairChannel,
|
type JobFairChannel,
|
||||||
type PortalJobFairItem,
|
type PortalJobFairItem,
|
||||||
|
type PortalJobFairPosition,
|
||||||
} from '@/services/jobportal/jobFair';
|
} from '@/services/jobportal/jobFair';
|
||||||
import '../index.less';
|
import '../index.less';
|
||||||
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;
|
return date.isValid() ? date.format('YYYY年MM月DD日 HH:mm') : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const educationMap: Record<string, string> = {
|
||||||
|
'0': '学历不限',
|
||||||
|
'1': '大专',
|
||||||
|
'2': '本科',
|
||||||
|
'3': '硕士',
|
||||||
|
'4': '博士',
|
||||||
|
};
|
||||||
|
|
||||||
|
const experienceMap: Record<string, string> = {
|
||||||
|
'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<string, string>,
|
||||||
|
fallback: string,
|
||||||
|
) {
|
||||||
|
return value ? map[value] || value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
const PortalJobFairDetailPage: React.FC = () => {
|
const PortalJobFairDetailPage: React.FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const fallbackDetail = (location.state as JobFairDetailLocationState | null)?.fair;
|
const fallbackDetail = (location.state as JobFairDetailLocationState | null)?.fair;
|
||||||
@@ -45,6 +86,8 @@ const PortalJobFairDetailPage: React.FC = () => {
|
|||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
const [detail, setDetail] = useState<PortalJobFairItem>();
|
const [detail, setDetail] = useState<PortalJobFairItem>();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [positions, setPositions] = useState<PortalJobFairPosition[]>([]);
|
||||||
|
const [positionsLoading, setPositionsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!jobFairId) {
|
if (!jobFairId) {
|
||||||
@@ -79,8 +122,49 @@ const PortalJobFairDetailPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [channel, fallbackDetail, jobFairId]);
|
}, [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 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 (
|
return (
|
||||||
<div className="portal-job-fair-page portal-job-fair-detail-page job-portal-page-root">
|
<div className="portal-job-fair-page portal-job-fair-detail-page job-portal-page-root">
|
||||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||||
@@ -174,6 +258,65 @@ const PortalJobFairDetailPage: React.FC = () => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
|
{channel === 'online' && detail && (
|
||||||
|
<section className="job-fair-position-section" aria-live="polite">
|
||||||
|
<div className="job-fair-position-heading">
|
||||||
|
<Title level={3}>招聘岗位</Title>
|
||||||
|
<Typography.Text type="secondary">共 {positions.length} 个岗位</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Spin spinning={positionsLoading}>
|
||||||
|
{positions.length ? (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{positions.map((position, index) => (
|
||||||
|
<Col
|
||||||
|
key={position.fairRelationId || position.jobId || index}
|
||||||
|
md={6}
|
||||||
|
sm={12}
|
||||||
|
xs={24}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className="job-fair-position-card"
|
||||||
|
hoverable
|
||||||
|
onClick={() => handlePositionClick(position)}
|
||||||
|
>
|
||||||
|
<div className="job-header">
|
||||||
|
<Title level={4}>{position.jobTitle || '未命名岗位'}</Title>
|
||||||
|
<Typography.Text className="job-salary">
|
||||||
|
{formatSalary(position)}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="job-company">
|
||||||
|
{position.companyName || '招聘企业暂未公布'}
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="job-info">
|
||||||
|
<Typography.Text className="job-location">
|
||||||
|
<EnvironmentOutlined />{' '}
|
||||||
|
{position.jobLocation || position.jobAddress || '地点不限'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="job-experience">
|
||||||
|
{formatRequirement(position.experience, experienceMap, '经验不限')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="job-education">
|
||||||
|
{formatRequirement(position.education, educationMap, '学历不限')}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
{position.vacancies && (
|
||||||
|
<div className="job-tags">
|
||||||
|
<Tag className="job-tag">招聘 {position.vacancies} 人</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
!positionsLoading && (
|
||||||
|
<Empty description="暂未发布招聘岗位" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,6 +95,40 @@ export interface PortalJobFairDetailResult {
|
|||||||
data?: PortalJobFairItem;
|
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 = {
|
const FAIR_API = {
|
||||||
online: '/app/jobfair/public/jobfair',
|
online: '/app/jobfair/public/jobfair',
|
||||||
outdoor: '/app/outdoor-fair',
|
outdoor: '/app/outdoor-fair',
|
||||||
@@ -172,3 +206,26 @@ export async function getPortalJobFairDetail(channel: JobFairChannel, jobFairId:
|
|||||||
},
|
},
|
||||||
} as PortalJobFairDetailResult;
|
} as PortalJobFairDetailResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取线上招聘会的关联岗位,并将企业分组数据转换为岗位卡片数据。 */
|
||||||
|
export async function getPortalJobFairPositions(jobFairId: string) {
|
||||||
|
const response = await request<PortalJobFairCompaniesWithJobsResult>(
|
||||||
|
`${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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user