11
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:
francis-fh
2026-06-04 16:25:38 +08:00
parent 3815dd63da
commit 03f26885c5
32 changed files with 2642 additions and 3062 deletions

View File

@@ -1,222 +1,74 @@
@import '../theme.less';
.job-detail-page {
min-height: 100vh;
background-color: #f0f2f5;
// padding: 20px;
.job-detail-container {
margin-top: 20px!important;
max-width: 1200px;
margin: 0 auto;
background-color: @jp-bg-page;
.back-button {
.job-detail-container {
.jp-page-container();
margin-top: 20px !important;
padding-bottom: 40px;
.back-button .ant-btn {
color: @jp-primary;
padding: 0;
&:hover {
color: @jp-primary-dark;
}
}
.job-header-card,
.info-card,
.competitiveness-card,
.skills-card {
.jp-card-base();
margin-bottom: 16px;
.ant-btn {
color: #1890ff;
padding: 0;
&:hover {
color: #40a9ff;
}
.ant-card-head-title {
.jp-section-title();
font-size: 18px;
}
}
.job-header-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.job-title-section {
.job-title {
color: #262626;
font-weight: 600;
margin-bottom: 0;
}
.job-salary-display {
color: #ff4d4f;
font-size: 24px;
font-weight: 600;
margin-left: 16px;
}
.job-meta {
color: #8c8c8c;
font-size: 14px;
.anticon {
margin-right: 4px;
}
}
.job-salary-display {
.jp-salary-text();
font-size: 24px;
}
.job-actions {
display: flex;
flex-direction: column;
gap: 12px;
.action-buttons {
width: 100%;
display: flex;
.ant-btn {
flex: 1;
}
}
}
}
.info-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.ant-card-head-title {
font-weight: 600;
font-size: 18px;
.job-meta {
color: @jp-text-muted;
}
.company-intro {
.company-meta {
color: #8c8c8c;
font-size: 14px;
}
.ant-typography {
margin-top: 16px;
line-height: 1.8;
color: #595959;
}
.job-actions .ant-btn-primary {
background: @jp-primary;
border-color: @jp-primary;
}
}
.competitiveness-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.ant-card-head-title {
font-weight: 600;
font-size: 18px;
.score-value {
color: @jp-primary;
font-size: 32px;
font-weight: bold;
}
.radar-chart-container {
position: relative;
width: 100%;
.radar-summary {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
.summary-score {
.score-value {
font-size: 32px;
font-weight: bold;
color: #1890ff;
line-height: 1;
}
.score-label {
font-size: 14px;
color: #8c8c8c;
margin-top: 8px;
}
}
}
.score-label {
color: @jp-text-muted;
}
}
.skills-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.ant-card-head-title {
font-weight: 600;
font-size: 18px;
}
.skill-item {
padding: 12px;
background: #fafafa;
border-radius: 4px;
.skill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.anticon {
color: #faad14;
}
}
.salary-indicator {
display: block;
margin-top: 4px;
font-size: 12px;
color: #8c8c8c;
}
}
}
// 响应式设计
@media (max-width: 992px) {
.job-header-card {
.job-title-section {
margin-bottom: 16px;
}
.job-actions {
width: 100%;
.action-buttons {
flex-direction: column;
}
}
}
}
@media (max-width: 768px) {
padding: 10px;
.job-header-card {
.job-title-section {
.job-title {
font-size: 20px;
}
.job-salary-display {
font-size: 18px;
margin-left: 12px;
}
}
.job-actions {
margin-top: 16px;
}
}
.competitiveness-card {
.radar-chart-container {
.radar-summary {
.summary-score {
.score-value {
font-size: 24px;
}
.score-label {
font-size: 12px;
}
}
}
}
}
.skills-card .skill-item {
background: @jp-primary-light;
border-radius: @jp-radius;
border: 1px solid @jp-primary-border;
}
}
}
@media (max-width: 768px) {
.job-detail-page .job-detail-container {
padding: 0 12px;
}
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { useParams, history, useLocation } from '@umijs/max';
import {
Card,
@@ -27,10 +27,17 @@ import {
import { Radar } from '@ant-design/charts';
import JobPortalHeader from '@/components/JobPortalHeader';
import './index.less';
import { getJobCompetitiveness } from '@/services/jobportal/competitiveness';
import {
getJobCompetitiveness,
getEmptyCompetitivenessRadar,
} from '@/services/jobportal/competitiveness';
import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth';
import { favoriteJob, unfavoriteJob, applyJob } from '@/services/jobportal/user';
import { getAccessToken } from '@/access';
import { PageEnum } from '@/enums/pagesEnums';
import { ensureJobPortalLogin, PORTAL_LOGIN_URL } from '@/utils/jobPortalAuth';
import { getDictLabel, findTreeLabelById } from '@/utils/jobPortalDict';
import { getDictValueEnum } from '@/services/system/dict';
import { getCmsIndustryTreeList } from '@/services/classify/industry';
import type { DictValueEnumObj } from '@/components/DictTag';
const { Title, Text, Paragraph } = Typography;
@@ -106,11 +113,13 @@ const transformJobData = (jobData: any) => {
<p>3. 具备良好的团队协作能力;</p>
`,
companyInfo: {
name: jobData.companyName || jobData.company,
type: jobData.industry || '互联网/电子商务',
scale: jobData.scale || '100-499人',
industry: jobData.industry || '计算机/互联网',
description: jobData.companyVo?.companyDescription || jobData.companyDescription || '暂无公司描述信息'
name: jobData.companyName || jobData.company || jobData.companyVo?.name,
scale: jobData.companyVo?.scale ?? jobData.scale ?? '',
industry: jobData.companyVo?.industry ?? jobData.industry ?? '',
description:
jobData.companyVo?.companyDescription
|| jobData.companyDescription
|| '暂无公司描述信息',
},
competitiveness: {
overall: 75,
@@ -157,7 +166,6 @@ const mockJobDetail = {
`,
companyInfo: {
name: '青岛科技发展有限公司',
type: '互联网/电子商务',
scale: '500-999人',
industry: '计算机/互联网',
description: '青岛科技发展有限公司是一家专注于软件开发、技术服务的科技企业。公司致力于为客户提供优质的IT解决方案业务涵盖企业信息化、移动应用开发等领域。我们拥有一支年轻、专业的团队注重技术创新和人才培养为员工提供良好的发展平台。'
@@ -193,6 +201,36 @@ const JobDetailPage: React.FC = () => {
{ item: '年龄', score: 0 },
{ item: '工作地', score: 0 }
]);
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
const [industryTree, setIndustryTree] = useState<any[]>([]);
useEffect(() => {
Promise.all([
getDictValueEnum('scale', true, true),
getCmsIndustryTreeList(),
]).then(([scaleData, industryRes]) => {
setScaleEnum(scaleData);
if (industryRes?.code === 200 && industryRes?.data) {
setIndustryTree(industryRes.data);
}
});
}, []);
const companyMetaItems = useMemo(() => {
const company = originalJobData?.companyVo || {};
const rawScale = company.scale ?? originalJobData?.scale ?? jobDetail.companyInfo?.scale;
const rawIndustry =
company.industry ?? originalJobData?.industry ?? jobDetail.companyInfo?.industry;
const items: string[] = [];
const scaleLabel = getDictLabel(scaleEnum, rawScale) || rawScale;
const industryLabel = findTreeLabelById(industryTree, rawIndustry) || rawIndustry;
if (scaleLabel) items.push(scaleLabel);
if (industryLabel) items.push(industryLabel);
return items;
}, [originalJobData, jobDetail.companyInfo, scaleEnum, industryTree]);
useEffect(() => {
// 首先尝试从传递的数据中获取职位信息
@@ -226,10 +264,19 @@ const JobDetailPage: React.FC = () => {
}, [jobDetail?.id]);
const fetchCompetitiveness = async (jobIdNum: number) => {
if (!isJobPortalLoggedIn()) {
setOverallScore(0);
setRadarData(getEmptyCompetitivenessRadar());
return;
}
try {
const res = await getJobCompetitiveness(jobIdNum);
const data = res as any;
if (!data) return;
if (!data) {
setOverallScore(0);
setRadarData(getEmptyCompetitivenessRadar());
return;
}
if (typeof data.matchScore === 'number') {
const clamped = Math.max(0, Math.min(100, data.matchScore));
setOverallScore(clamped);
@@ -248,36 +295,25 @@ const JobDetailPage: React.FC = () => {
].map(d => ({ ...d, score: Math.max(0, Math.min(100, d.score)) }));
setRadarData(mapped);
} else {
setRadarData([
{ item: '技能', score: 0 },
{ item: '工作经验', score: 0 },
{ item: '学历', score: 0 },
{ item: '薪资', score: 0 },
{ item: '年龄', score: 0 },
{ item: '工作地', score: 0 }
]);
setRadarData(getEmptyCompetitivenessRadar());
}
} catch (e) {
// 保持默认mock数据
setOverallScore(0);
setRadarData(getEmptyCompetitivenessRadar());
}
};
const handleCollect = async () => {
// 检查用户是否登录
const token = getAccessToken();
if (!token) {
message.warning('请先登录');
history.push(PageEnum.LOGIN);
if (!ensureJobPortalLogin('收藏职位')) {
return;
}
// 如果是取消收藏,调用取消收藏接口
if (isFavorited) {
// 获取用户ID
const userId = getUserIdFromCache();
if (!userId) {
message.warning('无法获取用户信息,请重新登录');
history.push(PageEnum.LOGIN);
window.location.href = PORTAL_LOGIN_URL;
return;
}
@@ -307,12 +343,10 @@ const JobDetailPage: React.FC = () => {
return;
}
// 如果是收藏,调用收藏接口
// 获取用户ID
const userId = getUserIdFromCache();
if (!userId) {
message.warning('无法获取用户信息,请重新登录');
history.push(PageEnum.LOGIN);
window.location.href = PORTAL_LOGIN_URL;
return;
}
@@ -342,19 +376,14 @@ const JobDetailPage: React.FC = () => {
};
const handleApply = async () => {
// 检查用户是否登录
const token = getAccessToken();
if (!token) {
message.warning('请先登录');
history.push(PageEnum.LOGIN);
if (!ensureJobPortalLogin('申请职位')) {
return;
}
// 获取用户ID
const userId = getUserIdFromCache();
if (!userId) {
message.warning('无法获取用户信息,请重新登录');
history.push(PageEnum.LOGIN);
window.location.href = PORTAL_LOGIN_URL;
return;
}
@@ -471,12 +500,10 @@ const JobDetailPage: React.FC = () => {
<Title level={4} style={{ margin: 0 }}>
{jobDetail.companyInfo.name}
</Title>
<Space className="company-meta">
<Text>{jobDetail.companyInfo.type}</Text>
<Divider type="vertical" />
<Text>{jobDetail.companyInfo.scale}</Text>
<Divider type="vertical" />
<Text>{jobDetail.companyInfo.industry}</Text>
<Space className="company-meta" split={<Divider type="vertical" />}>
{companyMetaItems.map((label) => (
<Text key={label}>{label}</Text>
))}
</Space>
</div>
</Space>