2026-07-24 20:58:01 +08:00
|
|
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2026-06-24 18:18:42 +08:00
|
|
|
|
import { useParams, history, useLocation, useSearchParams } from '@umijs/max';
|
2025-11-10 16:28:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Progress,
|
|
|
|
|
|
Divider,
|
|
|
|
|
|
Avatar,
|
2026-07-12 10:49:52 +08:00
|
|
|
|
message,
|
|
|
|
|
|
Modal
|
2025-11-10 16:28:01 +08:00
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
HeartOutlined,
|
|
|
|
|
|
HeartFilled,
|
|
|
|
|
|
EnvironmentOutlined,
|
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
TrophyOutlined,
|
|
|
|
|
|
FireOutlined,
|
2026-07-10 17:57:31 +08:00
|
|
|
|
StarOutlined,
|
2026-07-12 10:49:52 +08:00
|
|
|
|
FlagOutlined,
|
|
|
|
|
|
StopOutlined
|
2025-11-10 16:28:01 +08:00
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
|
import { Radar } from '@ant-design/charts';
|
|
|
|
|
|
import JobPortalHeader from '@/components/JobPortalHeader';
|
|
|
|
|
|
import './index.less';
|
2026-06-04 16:25:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getJobCompetitiveness,
|
|
|
|
|
|
getEmptyCompetitivenessRadar,
|
|
|
|
|
|
} from '@/services/jobportal/competitiveness';
|
|
|
|
|
|
import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth';
|
2026-07-12 10:49:52 +08:00
|
|
|
|
import { favoriteJob, unfavoriteJob, applyJob, browseJob, getJobDetail, blockCompany } from '@/services/jobportal/user';
|
2026-06-04 18:47:59 +08:00
|
|
|
|
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
|
2026-06-04 17:15:41 +08:00
|
|
|
|
import { getDictLabel, resolveIndustryLabel } from '@/utils/jobPortalDict';
|
2026-07-10 17:57:31 +08:00
|
|
|
|
import JobComplaintModal from '@/components/JobComplaintModal';
|
2026-06-04 16:25:38 +08:00
|
|
|
|
import { getDictValueEnum } from '@/services/system/dict';
|
|
|
|
|
|
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
|
|
|
|
|
import type { DictValueEnumObj } from '@/components/DictTag';
|
2026-07-20 11:53:43 +08:00
|
|
|
|
import { parseCommaSeparatedTags } from '@/utils/jobDetailTags';
|
2025-11-10 16:28:01 +08:00
|
|
|
|
|
|
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
// 学历字典映射
|
|
|
|
|
|
const educationMap: { [key: string]: string } = {
|
|
|
|
|
|
'1': '大专',
|
|
|
|
|
|
'2': '本科',
|
|
|
|
|
|
'3': '硕士',
|
|
|
|
|
|
'4': '博士',
|
|
|
|
|
|
'0': '不限'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 工作经验字典映射
|
|
|
|
|
|
const experienceMap: { [key: string]: string } = {
|
|
|
|
|
|
'1': '1年以下',
|
|
|
|
|
|
'2': '1-3年',
|
|
|
|
|
|
'3': '3-5年',
|
|
|
|
|
|
'4': '5-10年',
|
|
|
|
|
|
'5': '10年以上',
|
|
|
|
|
|
'0': '不限'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化薪资显示
|
|
|
|
|
|
const formatSalary = (minSalary: number, maxSalary: number) => {
|
|
|
|
|
|
if (minSalary && maxSalary) {
|
|
|
|
|
|
return `${(minSalary / 1000).toFixed(0)}k-${(maxSalary / 1000).toFixed(0)}k`;
|
|
|
|
|
|
} else if (minSalary) {
|
|
|
|
|
|
return `${(minSalary / 1000).toFixed(0)}k+`;
|
|
|
|
|
|
} else if (maxSalary) {
|
|
|
|
|
|
return `面议-${(maxSalary / 1000).toFixed(0)}k`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return '面议';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 数据转换函数
|
|
|
|
|
|
const transformJobData = (jobData: any) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: jobData.jobId || jobData.id,
|
|
|
|
|
|
title: jobData.jobTitle || jobData.title,
|
|
|
|
|
|
company: jobData.companyName || jobData.company,
|
|
|
|
|
|
companyLogo: jobData.companyLogo || 'https://via.placeholder.com/80',
|
|
|
|
|
|
salary: jobData.minSalary && jobData.maxSalary
|
|
|
|
|
|
? formatSalary(jobData.minSalary, jobData.maxSalary)
|
|
|
|
|
|
: (jobData.salary || '面议'),
|
|
|
|
|
|
location: jobData.jobLocation || jobData.location,
|
2026-07-20 11:53:43 +08:00
|
|
|
|
companyNature: jobData.companyNature || jobData.company?.companyNature || '',
|
2025-11-10 16:28:01 +08:00
|
|
|
|
experience: jobData.experience ? experienceMap[jobData.experience] || jobData.experience : '不限',
|
|
|
|
|
|
education: jobData.education ? educationMap[jobData.education] || jobData.education : '不限',
|
2026-07-20 11:53:43 +08:00
|
|
|
|
tags: Array.isArray(jobData.tags) ? jobData.tags : [],
|
2026-07-26 18:45:43 +08:00
|
|
|
|
jobFeature: jobData.jobFeature,
|
2026-07-20 11:53:43 +08:00
|
|
|
|
salaryComposition: parseCommaSeparatedTags(jobData.salaryComposition),
|
|
|
|
|
|
welfareBenefits: parseCommaSeparatedTags(jobData.welfareBenefits),
|
|
|
|
|
|
workSchedule: parseCommaSeparatedTags(jobData.workSchedule),
|
2025-11-10 16:28:01 +08:00
|
|
|
|
publishTime: jobData.publishTime || new Date().toISOString().split('T')[0],
|
|
|
|
|
|
description: jobData.description || `
|
|
|
|
|
|
<h3>职位职责:</h3>
|
|
|
|
|
|
<p>1. 负责相关业务系统的设计与开发;</p>
|
|
|
|
|
|
<p>2. 参与系统架构设计和技术选型;</p>
|
|
|
|
|
|
<p>3. 与团队协作完成项目开发;</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3>任职要求:</h3>
|
|
|
|
|
|
<p>1. 具备相关工作经验;</p>
|
|
|
|
|
|
<p>2. 熟悉相关技术栈;</p>
|
|
|
|
|
|
<p>3. 具备良好的团队协作能力;</p>
|
|
|
|
|
|
`,
|
|
|
|
|
|
companyInfo: {
|
2026-06-04 17:15:41 +08:00
|
|
|
|
name: jobData.companyName || jobData.companyVo?.name || jobData.company,
|
|
|
|
|
|
scale: jobData.scale ?? '',
|
|
|
|
|
|
industry: jobData.industry ?? '',
|
2026-06-04 16:25:38 +08:00
|
|
|
|
description:
|
|
|
|
|
|
jobData.companyVo?.companyDescription
|
|
|
|
|
|
|| jobData.companyDescription
|
|
|
|
|
|
|| '暂无公司描述信息',
|
2025-11-10 16:28:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
competitiveness: {
|
|
|
|
|
|
overall: 75,
|
|
|
|
|
|
requirements: 70,
|
|
|
|
|
|
salary: 80,
|
|
|
|
|
|
company: 75,
|
|
|
|
|
|
benefits: 80
|
|
|
|
|
|
},
|
|
|
|
|
|
marketTrend: [
|
|
|
|
|
|
{ name: 'Java', demand: 85, salary: 90 },
|
|
|
|
|
|
{ name: 'Spring Boot', demand: 75, salary: 85 },
|
|
|
|
|
|
{ name: 'MySQL', demand: 70, salary: 75 },
|
|
|
|
|
|
{ name: 'Redis', demand: 75, salary: 80 }
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 职位详情数据(模拟数据)
|
|
|
|
|
|
const mockJobDetail = {
|
|
|
|
|
|
id: 1,
|
|
|
|
|
|
title: 'Java高级开发工程师',
|
|
|
|
|
|
company: '青岛科技发展有限公司',
|
|
|
|
|
|
companyLogo: 'https://via.placeholder.com/80',
|
|
|
|
|
|
salary: '15k-25k',
|
|
|
|
|
|
location: '青岛·李沧区',
|
|
|
|
|
|
experience: '3-5年',
|
|
|
|
|
|
education: '本科',
|
2026-07-20 11:53:43 +08:00
|
|
|
|
companyNature: '',
|
2026-07-26 18:45:43 +08:00
|
|
|
|
jobFeature: '',
|
2026-07-20 11:53:43 +08:00
|
|
|
|
tags: [],
|
|
|
|
|
|
salaryComposition: [] as string[],
|
|
|
|
|
|
welfareBenefits: [] as string[],
|
|
|
|
|
|
workSchedule: [] as string[],
|
2025-11-10 16:28:01 +08:00
|
|
|
|
publishTime: '2024-03-15',
|
|
|
|
|
|
description: `
|
|
|
|
|
|
<h3>职位职责:</h3>
|
|
|
|
|
|
<p>1. 负责后端系统架构设计与优化,提升系统性能和稳定性;</p>
|
|
|
|
|
|
<p>2. 参与核心业务模块的设计与开发,使用Java、Spring Boot等框架;</p>
|
|
|
|
|
|
<p>3. 协作完成微服务架构,解决分布式系统相关问题;</p>
|
|
|
|
|
|
<p>4. 进行代码审查,确保代码质量和规范性;</p>
|
|
|
|
|
|
<p>5. 与前端、产品、测试等团队协作,推动项目快速迭代;</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3>任职要求:</h3>
|
|
|
|
|
|
<p>1. 本科及以上学历,计算机相关专业,3年以上Java开发经验;</p>
|
|
|
|
|
|
<p>2. 扎实的Java基础,熟悉Spring、Spring Boot、MyBatis等框架;</p>
|
|
|
|
|
|
<p>3. 熟悉MySQL、Redis等数据库,具备SQL优化能力;</p>
|
|
|
|
|
|
<p>4. 了解分布式系统设计,有微服务开发经验;</p>
|
|
|
|
|
|
<p>5. 具备良好的团队协作能力和学习能力;</p>
|
|
|
|
|
|
`,
|
|
|
|
|
|
companyInfo: {
|
|
|
|
|
|
name: '青岛科技发展有限公司',
|
|
|
|
|
|
scale: '500-999人',
|
|
|
|
|
|
industry: '计算机/互联网',
|
|
|
|
|
|
description: '青岛科技发展有限公司是一家专注于软件开发、技术服务的科技企业。公司致力于为客户提供优质的IT解决方案,业务涵盖企业信息化、移动应用开发等领域。我们拥有一支年轻、专业的团队,注重技术创新和人才培养,为员工提供良好的发展平台。'
|
|
|
|
|
|
},
|
|
|
|
|
|
competitiveness: {
|
|
|
|
|
|
overall: 75,
|
|
|
|
|
|
requirements: 70,
|
|
|
|
|
|
salary: 80,
|
|
|
|
|
|
company: 75,
|
|
|
|
|
|
benefits: 80
|
|
|
|
|
|
},
|
|
|
|
|
|
marketTrend: [
|
|
|
|
|
|
{ name: 'Java', demand: 85, salary: 90 },
|
|
|
|
|
|
{ name: 'Spring Boot', demand: 75, salary: 85 },
|
|
|
|
|
|
{ name: 'MySQL', demand: 70, salary: 75 },
|
|
|
|
|
|
{ name: 'Redis', demand: 75, salary: 80 }
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const JobDetailPage: React.FC = () => {
|
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
|
const location = useLocation();
|
2026-06-24 18:18:42 +08:00
|
|
|
|
const [searchParams] = useSearchParams();
|
2025-11-10 16:28:01 +08:00
|
|
|
|
const [jobDetail, setJobDetail] = useState(mockJobDetail);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [isFavorited, setIsFavorited] = useState(false);
|
|
|
|
|
|
const [originalJobData, setOriginalJobData] = useState<any>(null); // 保存原始数据引用
|
|
|
|
|
|
const [overallScore, setOverallScore] = useState<number>(0);
|
|
|
|
|
|
const [radarData, setRadarData] = useState<{ item: string; score: number }[]>([
|
|
|
|
|
|
{ item: '技能', score: 0 },
|
|
|
|
|
|
{ item: '工作经验', score: 0 },
|
|
|
|
|
|
{ item: '学历', score: 0 },
|
|
|
|
|
|
{ item: '薪资', score: 0 },
|
|
|
|
|
|
{ item: '年龄', score: 0 },
|
|
|
|
|
|
{ item: '工作地', score: 0 }
|
|
|
|
|
|
]);
|
2026-06-04 16:25:38 +08:00
|
|
|
|
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
|
2026-07-20 11:53:43 +08:00
|
|
|
|
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
|
2026-07-26 18:45:43 +08:00
|
|
|
|
const [jobFeatureEnum, setJobFeatureEnum] = useState<DictValueEnumObj>({});
|
2026-06-04 16:25:38 +08:00
|
|
|
|
const [industryTree, setIndustryTree] = useState<any[]>([]);
|
2026-07-10 17:57:31 +08:00
|
|
|
|
const [complaintVisible, setComplaintVisible] = useState(false);
|
2026-06-04 16:25:38 +08:00
|
|
|
|
|
2026-07-24 20:58:01 +08:00
|
|
|
|
// 行为上报:浏览岗位时长统计
|
|
|
|
|
|
const enterTimeRef = useRef<number>(0);
|
|
|
|
|
|
const reportedRef = useRef<boolean>(false);
|
|
|
|
|
|
const jobIdRef = useRef<string>('');
|
|
|
|
|
|
|
|
|
|
|
|
// 页面进入/离开时上报浏览行为
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
enterTimeRef.current = Date.now();
|
|
|
|
|
|
reportedRef.current = false;
|
|
|
|
|
|
jobIdRef.current = String(originalJobData?.jobId || originalJobData?.id || id || '');
|
|
|
|
|
|
|
|
|
|
|
|
const calcDuration = () =>
|
|
|
|
|
|
Math.max(1, Math.floor((Date.now() - enterTimeRef.current) / 1000));
|
|
|
|
|
|
|
|
|
|
|
|
const doReport = (durationSeconds: number) => {
|
|
|
|
|
|
if (reportedRef.current) return;
|
|
|
|
|
|
reportedRef.current = true;
|
|
|
|
|
|
|
|
|
|
|
|
const actorId = localStorage.getItem('appUserId');
|
|
|
|
|
|
const targetId = jobIdRef.current;
|
|
|
|
|
|
|
|
|
|
|
|
if (!actorId || !targetId || durationSeconds <= 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
fetch('/api/cms/behavior/report', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
actorType: '1',
|
|
|
|
|
|
actorId: String(actorId),
|
|
|
|
|
|
targetType: '2',
|
|
|
|
|
|
targetId: String(targetId),
|
|
|
|
|
|
viewDuration: String(durationSeconds),
|
|
|
|
|
|
}),
|
|
|
|
|
|
keepalive: true,
|
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBeforeUnload = () => {
|
|
|
|
|
|
doReport(calcDuration());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
|
|
|
|
doReport(calcDuration());
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-07-20 11:53:43 +08:00
|
|
|
|
const detailTagGroups = useMemo(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{ label: '薪资范围与构成', values: jobDetail.salaryComposition },
|
|
|
|
|
|
{ label: '福利待遇', values: jobDetail.welfareBenefits },
|
|
|
|
|
|
{ label: '工作时间安排', values: jobDetail.workSchedule },
|
|
|
|
|
|
].filter((group) => group.values.length > 0),
|
|
|
|
|
|
[jobDetail.salaryComposition, jobDetail.welfareBenefits, jobDetail.workSchedule],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-06-04 16:25:38 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
Promise.all([
|
|
|
|
|
|
getDictValueEnum('scale', true, true),
|
2026-07-20 11:53:43 +08:00
|
|
|
|
getDictValueEnum('company_nature', false, true),
|
2026-07-26 18:45:43 +08:00
|
|
|
|
getDictValueEnum('job_feature', false, true),
|
2026-06-04 16:25:38 +08:00
|
|
|
|
getCmsIndustryTreeList(),
|
2026-07-26 18:45:43 +08:00
|
|
|
|
]).then(([scaleData, companyNatureData, jobFeatureData, industryRes]) => {
|
2026-06-04 16:25:38 +08:00
|
|
|
|
setScaleEnum(scaleData);
|
2026-07-20 11:53:43 +08:00
|
|
|
|
setCompanyNatureEnum(companyNatureData);
|
2026-07-26 18:45:43 +08:00
|
|
|
|
setJobFeatureEnum(jobFeatureData);
|
2026-06-04 16:25:38 +08:00
|
|
|
|
if (industryRes?.code === 200 && industryRes?.data) {
|
|
|
|
|
|
setIndustryTree(industryRes.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const companyMetaItems = useMemo(() => {
|
2026-06-04 17:15:41 +08:00
|
|
|
|
if (!originalJobData) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
const rawScale = originalJobData.scale;
|
|
|
|
|
|
const rawIndustry = originalJobData.industry;
|
2026-06-04 16:25:38 +08:00
|
|
|
|
|
|
|
|
|
|
const items: string[] = [];
|
2026-06-04 17:15:41 +08:00
|
|
|
|
if (rawScale != null && rawScale !== '') {
|
|
|
|
|
|
const scaleLabel = getDictLabel(scaleEnum, rawScale) || String(rawScale);
|
|
|
|
|
|
if (scaleLabel) items.push(scaleLabel);
|
|
|
|
|
|
}
|
2026-06-04 18:47:59 +08:00
|
|
|
|
|
2026-06-04 17:15:41 +08:00
|
|
|
|
const industryLabel = resolveIndustryLabel(industryTree, rawIndustry);
|
2026-06-04 18:47:59 +08:00
|
|
|
|
const rawStr = rawIndustry == null ? '' : String(rawIndustry).trim();
|
|
|
|
|
|
// 行业 id 未解析成名称时不展示裸数字,避免与行业名称重复
|
|
|
|
|
|
if (industryLabel && !(/^\d+$/.test(rawStr) && industryLabel === rawStr)) {
|
|
|
|
|
|
items.push(industryLabel);
|
|
|
|
|
|
}
|
2026-06-04 16:25:38 +08:00
|
|
|
|
|
|
|
|
|
|
return items;
|
2026-06-04 17:15:41 +08:00
|
|
|
|
}, [originalJobData, scaleEnum, industryTree]);
|
2025-11-10 16:28:01 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 首先尝试从传递的数据中获取职位信息
|
|
|
|
|
|
const passedJobData = (location.state as any)?.jobData;
|
|
|
|
|
|
|
|
|
|
|
|
if (passedJobData) {
|
|
|
|
|
|
// 保存原始数据引用
|
|
|
|
|
|
setOriginalJobData(passedJobData);
|
|
|
|
|
|
// 将传递过来的数据转换为详情页面需要的格式
|
|
|
|
|
|
const transformedJobData = transformJobData(passedJobData);
|
|
|
|
|
|
setJobDetail(transformedJobData);
|
|
|
|
|
|
// 根据 isCollection 字段设置收藏状态
|
|
|
|
|
|
setIsFavorited(passedJobData?.isCollection !== null && passedJobData?.isCollection !== 0 && passedJobData?.isCollection !== undefined);
|
2026-07-20 11:53:43 +08:00
|
|
|
|
// 列表卡片不携带详情标签;进入详情页后始终请求详情接口补齐完整岗位数据。
|
|
|
|
|
|
const passedJobId = passedJobData.jobId || passedJobData.id || id;
|
|
|
|
|
|
if (passedJobId) {
|
|
|
|
|
|
fetchJobDetailFromApi(String(passedJobId));
|
|
|
|
|
|
}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
} else {
|
2026-06-24 18:18:42 +08:00
|
|
|
|
// 直接刷新页面时,从 URL 参数或 route param 获取 jobId,调 API 获取数据
|
|
|
|
|
|
const jobIdFromUrl = searchParams.get('jobId');
|
|
|
|
|
|
if (jobIdFromUrl) {
|
|
|
|
|
|
fetchJobDetailFromApi(jobIdFromUrl);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setOriginalJobData(null);
|
|
|
|
|
|
setIsFavorited(false);
|
|
|
|
|
|
}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}, [id, location.state]);
|
|
|
|
|
|
|
2026-06-24 18:18:42 +08:00
|
|
|
|
// 页面直接刷新时调接口获取岗位详情
|
|
|
|
|
|
const fetchJobDetailFromApi = async (jobId: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const res = await getJobDetail(jobId);
|
|
|
|
|
|
if (res?.code === 200 && res?.data) {
|
|
|
|
|
|
const jobData = res.data;
|
|
|
|
|
|
setOriginalJobData(jobData);
|
|
|
|
|
|
const transformed = transformJobData(jobData);
|
|
|
|
|
|
setJobDetail(transformed);
|
|
|
|
|
|
setIsFavorited(jobData?.isCollection !== null && jobData?.isCollection !== 0 && jobData?.isCollection !== undefined);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setOriginalJobData(null);
|
|
|
|
|
|
setIsFavorited(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取岗位详情失败:', error);
|
|
|
|
|
|
setOriginalJobData(null);
|
|
|
|
|
|
setIsFavorited(false);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-04 18:47:59 +08:00
|
|
|
|
// 仅登录且有真实岗位 id 时请求竞争力
|
2025-11-10 16:28:01 +08:00
|
|
|
|
useEffect(() => {
|
2026-06-04 18:47:59 +08:00
|
|
|
|
if (!isJobPortalLoggedIn()) {
|
|
|
|
|
|
setOverallScore(0);
|
|
|
|
|
|
setRadarData(getEmptyCompetitivenessRadar());
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const jobIdRaw =
|
|
|
|
|
|
originalJobData?.jobId
|
|
|
|
|
|
?? originalJobData?.id
|
|
|
|
|
|
?? id;
|
|
|
|
|
|
const jobIdNum = Number(jobIdRaw);
|
|
|
|
|
|
if (!Number.isNaN(jobIdNum) && jobIdNum > 0) {
|
2025-11-10 16:28:01 +08:00
|
|
|
|
fetchCompetitiveness(jobIdNum);
|
|
|
|
|
|
}
|
2026-06-04 18:47:59 +08:00
|
|
|
|
}, [originalJobData?.jobId, originalJobData?.id, id]);
|
2025-11-10 16:28:01 +08:00
|
|
|
|
|
2026-06-16 19:11:36 +08:00
|
|
|
|
// 仅登录且有真实岗位 id 时上报浏览记录
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!isJobPortalLoggedIn()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const jobIdRaw =
|
|
|
|
|
|
originalJobData?.jobId
|
|
|
|
|
|
?? originalJobData?.id
|
|
|
|
|
|
?? id;
|
|
|
|
|
|
const jobIdNum = Number(jobIdRaw);
|
|
|
|
|
|
if (!Number.isNaN(jobIdNum) && jobIdNum > 0) {
|
|
|
|
|
|
browseJob({ jobId: jobIdNum }).catch(() => {
|
|
|
|
|
|
// 浏览上报失败不阻塞页面
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [originalJobData?.jobId, originalJobData?.id, id]);
|
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
|
const fetchCompetitiveness = async (jobIdNum: number) => {
|
2026-06-04 16:25:38 +08:00
|
|
|
|
if (!isJobPortalLoggedIn()) {
|
|
|
|
|
|
setOverallScore(0);
|
|
|
|
|
|
setRadarData(getEmptyCompetitivenessRadar());
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const res = await getJobCompetitiveness(jobIdNum);
|
|
|
|
|
|
const data = res as any;
|
2026-06-04 16:25:38 +08:00
|
|
|
|
if (!data) {
|
|
|
|
|
|
setOverallScore(0);
|
|
|
|
|
|
setRadarData(getEmptyCompetitivenessRadar());
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
if (typeof data.matchScore === 'number') {
|
|
|
|
|
|
const clamped = Math.max(0, Math.min(100, data.matchScore));
|
|
|
|
|
|
setOverallScore(clamped);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setOverallScore(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.radarChart) {
|
|
|
|
|
|
const r = data.radarChart as any;
|
|
|
|
|
|
const mapped = [
|
|
|
|
|
|
{ item: '技能', score: r.skill ?? 0 },
|
|
|
|
|
|
{ item: '工作经验', score: r.experience ?? 0 },
|
|
|
|
|
|
{ item: '学历', score: r.education ?? 0 },
|
|
|
|
|
|
{ item: '薪资', score: r.salary ?? 0 },
|
|
|
|
|
|
{ item: '年龄', score: r.age ?? 0 },
|
|
|
|
|
|
{ item: '工作地', score: r.location ?? 0 }
|
|
|
|
|
|
].map(d => ({ ...d, score: Math.max(0, Math.min(100, d.score)) }));
|
|
|
|
|
|
setRadarData(mapped);
|
|
|
|
|
|
} else {
|
2026-06-04 16:25:38 +08:00
|
|
|
|
setRadarData(getEmptyCompetitivenessRadar());
|
2025-11-10 16:28:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-06-04 16:25:38 +08:00
|
|
|
|
setOverallScore(0);
|
|
|
|
|
|
setRadarData(getEmptyCompetitivenessRadar());
|
2025-11-10 16:28:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCollect = async () => {
|
2026-06-04 18:47:59 +08:00
|
|
|
|
const userId = await requireJobPortalUserId('收藏职位');
|
|
|
|
|
|
if (!userId) {
|
2025-11-10 16:28:01 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是取消收藏,调用取消收藏接口
|
|
|
|
|
|
if (isFavorited) {
|
|
|
|
|
|
|
|
|
|
|
|
// 获取职位ID(从原始数据或转换后的数据中获取)
|
|
|
|
|
|
const jobId = originalJobData?.jobId || originalJobData?.id || (jobDetail as any)?.id || id;
|
|
|
|
|
|
if (!jobId) {
|
|
|
|
|
|
message.error('职位信息不完整,无法取消收藏');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await unfavoriteJob({ jobId: Number(jobId), userId });
|
|
|
|
|
|
if (response?.code === 200) {
|
|
|
|
|
|
setIsFavorited(false);
|
|
|
|
|
|
// 更新原始数据的 isCollection 字段
|
|
|
|
|
|
if (originalJobData) {
|
|
|
|
|
|
setOriginalJobData({ ...originalJobData, isCollection: 0 });
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success('取消收藏成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response?.msg || '取消收藏失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('取消收藏失败:', error);
|
|
|
|
|
|
message.error('取消收藏失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取职位ID
|
|
|
|
|
|
const jobId = originalJobData?.jobId || originalJobData?.id || (jobDetail as any)?.id || id;
|
|
|
|
|
|
if (!jobId) {
|
|
|
|
|
|
message.error('职位信息不完整');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await favoriteJob({ jobId: Number(jobId), userId });
|
|
|
|
|
|
if (response?.code === 200) {
|
|
|
|
|
|
setIsFavorited(true);
|
|
|
|
|
|
// 更新原始数据的 isCollection 字段
|
|
|
|
|
|
if (originalJobData) {
|
|
|
|
|
|
setOriginalJobData({ ...originalJobData, isCollection: 1 });
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success('收藏成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response?.msg || '收藏失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('收藏操作失败:', error);
|
|
|
|
|
|
message.error('收藏操作失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleApply = async () => {
|
2026-06-04 18:47:59 +08:00
|
|
|
|
const userId = await requireJobPortalUserId('申请职位');
|
2025-11-10 16:28:01 +08:00
|
|
|
|
if (!userId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取职位ID(从原始数据或转换后的数据中获取)
|
|
|
|
|
|
const jobId = originalJobData?.jobId || originalJobData?.id || (jobDetail as any)?.id || id;
|
|
|
|
|
|
if (!jobId) {
|
|
|
|
|
|
message.error('职位信息不完整,无法申请');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await applyJob({
|
|
|
|
|
|
jobId: String(jobId),
|
|
|
|
|
|
userId: String(userId)
|
|
|
|
|
|
});
|
|
|
|
|
|
if (response?.code === 200) {
|
|
|
|
|
|
message.success('申请成功');
|
2026-06-16 19:11:36 +08:00
|
|
|
|
if (originalJobData) {
|
|
|
|
|
|
setOriginalJobData({ ...originalJobData, isApply: 1 });
|
|
|
|
|
|
}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response?.msg || '申请失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('申请失败:', error);
|
|
|
|
|
|
message.error('申请失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-12 10:49:52 +08:00
|
|
|
|
const handleBlockCompany = async () => {
|
|
|
|
|
|
const companyId = originalJobData?.companyId || originalJobData?.companyVo?.companyId;
|
|
|
|
|
|
if (!companyId) {
|
|
|
|
|
|
message.error('未获取到企业信息');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const userId = await requireJobPortalUserId('屏蔽企业');
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '屏蔽该企业',
|
|
|
|
|
|
content: '确定要屏蔽该企业吗?屏蔽后将不再收到该企业的招聘信息。',
|
|
|
|
|
|
okText: '确定屏蔽',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
okButtonProps: { danger: true },
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await blockCompany({ userId, companyId });
|
|
|
|
|
|
if (res?.code === 200) {
|
|
|
|
|
|
message.success('已屏蔽该企业');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(res?.msg || '屏蔽失败,请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('操作失败,请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="job-detail-page">
|
|
|
|
|
|
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
|
|
|
|
|
|
|
|
|
|
|
<div className="job-detail-container">
|
|
|
|
|
|
|
|
|
|
|
|
{/* 职位头部信息 */}
|
|
|
|
|
|
<Card className="job-header-card">
|
|
|
|
|
|
<Row gutter={24}>
|
2026-07-10 17:57:31 +08:00
|
|
|
|
<Col span={16}>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
<div className="job-title-section">
|
|
|
|
|
|
<Space direction="vertical" size={8}>
|
2026-07-26 18:45:43 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: '16px', flexWrap: 'wrap' }}>
|
|
|
|
|
|
<Title level={2} className="job-title" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
{jobDetail.title}
|
|
|
|
|
|
{jobDetail.jobFeature && (
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color="orange"
|
|
|
|
|
|
style={{ marginLeft: 10, fontSize: 13, lineHeight: '24px', verticalAlign: 'middle' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{getDictLabel(jobFeatureEnum, jobDetail.jobFeature)}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text className="job-salary-display">{jobDetail.salary}</Text>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<Space size={16}>
|
|
|
|
|
|
<Text className="job-meta">
|
|
|
|
|
|
<EnvironmentOutlined /> {jobDetail.location}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text className="job-meta">
|
|
|
|
|
|
<ClockCircleOutlined /> {jobDetail.experience}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text className="job-meta">
|
|
|
|
|
|
<UserOutlined /> {jobDetail.education}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<Space wrap>
|
2026-07-20 11:53:43 +08:00
|
|
|
|
{jobDetail.companyNature && (
|
|
|
|
|
|
<Tag color="blue">
|
|
|
|
|
|
{getDictLabel(companyNatureEnum, jobDetail.companyNature)}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
{jobDetail.tags.map((tag, index) => (
|
|
|
|
|
|
<Tag key={index} color="blue">{tag}</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
2026-07-10 17:57:31 +08:00
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<div className="job-actions" style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={isFavorited ? <HeartFilled /> : <HeartOutlined />}
|
|
|
|
|
|
danger={isFavorited}
|
|
|
|
|
|
onClick={handleCollect}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={isFavorited ? {} : { borderColor: '#1890ff', color: '#1890ff' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isFavorited ? '已收藏' : '收藏'}
|
|
|
|
|
|
</Button>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
2026-06-16 19:11:36 +08:00
|
|
|
|
disabled={originalJobData?.isApply === 1}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
onClick={handleApply}
|
|
|
|
|
|
>
|
2026-06-16 19:11:36 +08:00
|
|
|
|
{originalJobData?.isApply === 1 ? '已投递' : '立即申请'}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</Button>
|
2026-07-10 17:57:31 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<FlagOutlined />}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={() => setComplaintVisible(true)}
|
|
|
|
|
|
>
|
|
|
|
|
|
投诉
|
|
|
|
|
|
</Button>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
{/* 左侧内容区 */}
|
|
|
|
|
|
<Col span={16}>
|
2026-07-20 11:53:43 +08:00
|
|
|
|
{detailTagGroups.length > 0 && (
|
|
|
|
|
|
<Card title="岗位待遇与工作安排" className="info-card job-detail-tags-card">
|
|
|
|
|
|
<div className="job-detail-tag-groups">
|
|
|
|
|
|
{detailTagGroups.map((group) => (
|
|
|
|
|
|
<div className="job-detail-tag-group" key={group.label}>
|
|
|
|
|
|
<div className="job-detail-tag-label">{group.label}</div>
|
|
|
|
|
|
<Space wrap size={[8, 8]}>
|
|
|
|
|
|
{group.values.map((value) => (
|
|
|
|
|
|
<Tag color="blue" key={`${group.label}-${value}`}>{value}</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
|
{/* 职位描述 */}
|
|
|
|
|
|
<Card title="职位描述" className="info-card">
|
|
|
|
|
|
<div
|
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: jobDetail.description }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 公司信息 */}
|
2026-06-04 18:47:59 +08:00
|
|
|
|
<Card title="公司信息" className="info-card company-info-card">
|
2025-11-10 16:28:01 +08:00
|
|
|
|
<div className="company-intro">
|
2026-06-04 18:47:59 +08:00
|
|
|
|
<div className="company-overview">
|
|
|
|
|
|
<Avatar size={56} className="company-avatar" src={jobDetail.companyLogo}>
|
|
|
|
|
|
企
|
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
<div className="company-main">
|
|
|
|
|
|
<Title level={5} className="company-name">
|
2025-11-10 16:28:01 +08:00
|
|
|
|
{jobDetail.companyInfo.name}
|
|
|
|
|
|
</Title>
|
2026-06-04 18:47:59 +08:00
|
|
|
|
{companyMetaItems.length > 0 && (
|
|
|
|
|
|
<div className="company-meta-row">
|
|
|
|
|
|
{companyMetaItems.map((label, index) => (
|
|
|
|
|
|
<React.Fragment key={`${label}-${index}`}>
|
|
|
|
|
|
{index > 0 && <span className="meta-separator">|</span>}
|
|
|
|
|
|
<span>{label}</span>
|
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{(originalJobData?.jobLocation || jobDetail.location) && (
|
|
|
|
|
|
<div className="company-location">
|
|
|
|
|
|
<EnvironmentOutlined />
|
|
|
|
|
|
<span>{originalJobData?.jobLocation || jobDetail.location}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</div>
|
2026-07-12 10:49:52 +08:00
|
|
|
|
<div className="company-block-action">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<StopOutlined />}
|
|
|
|
|
|
onClick={handleBlockCompany}
|
|
|
|
|
|
>
|
|
|
|
|
|
屏蔽该企业
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2026-06-04 18:47:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<Divider className="company-section-divider" />
|
|
|
|
|
|
<div className="company-description-section">
|
|
|
|
|
|
<div className="company-section-label">公司介绍</div>
|
|
|
|
|
|
<Paragraph className="company-description">
|
|
|
|
|
|
{jobDetail.companyInfo.description}
|
|
|
|
|
|
</Paragraph>
|
|
|
|
|
|
</div>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 右侧侧边栏 */}
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
{/* 竞争力分析 */}
|
|
|
|
|
|
<Card
|
|
|
|
|
|
title={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<TrophyOutlined />
|
|
|
|
|
|
竞争力分析
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
className="competitiveness-card"
|
|
|
|
|
|
>
|
2026-06-04 18:47:59 +08:00
|
|
|
|
{isJobPortalLoggedIn() ? (
|
|
|
|
|
|
<div className="radar-chart-container">
|
|
|
|
|
|
<Radar
|
|
|
|
|
|
data={radarData}
|
|
|
|
|
|
xField="item"
|
|
|
|
|
|
yField="score"
|
|
|
|
|
|
area={{
|
|
|
|
|
|
style: {
|
|
|
|
|
|
fill: 'rgba(24, 144, 255, 0.25)',
|
|
|
|
|
|
fillOpacity: 0.5,
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
point={{
|
|
|
|
|
|
size: 3,
|
|
|
|
|
|
style: {
|
|
|
|
|
|
fill: '#1890ff',
|
|
|
|
|
|
stroke: '#1890ff',
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
line={{
|
|
|
|
|
|
style: {
|
|
|
|
|
|
stroke: '#1890ff',
|
|
|
|
|
|
lineWidth: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
2026-06-09 16:08:39 +08:00
|
|
|
|
scale={{
|
|
|
|
|
|
y: {
|
|
|
|
|
|
domainMin: 0,
|
|
|
|
|
|
domainMax: 100,
|
2026-06-04 18:47:59 +08:00
|
|
|
|
nice: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
2026-06-09 16:08:39 +08:00
|
|
|
|
axis={{
|
|
|
|
|
|
y: {
|
|
|
|
|
|
label: false,
|
|
|
|
|
|
tick: false,
|
|
|
|
|
|
line: false,
|
|
|
|
|
|
},
|
2026-06-04 18:47:59 +08:00
|
|
|
|
}}
|
|
|
|
|
|
height={300}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="radar-summary">
|
|
|
|
|
|
<div className="summary-score">
|
|
|
|
|
|
<div className="score-value">{overallScore}</div>
|
|
|
|
|
|
<div className="score-label">综合评分</div>
|
|
|
|
|
|
</div>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-04 18:47:59 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="competitiveness-login-placeholder">
|
|
|
|
|
|
<TrophyOutlined className="placeholder-icon" />
|
|
|
|
|
|
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
登录后可查看您与该岗位的匹配度分析
|
|
|
|
|
|
</Paragraph>
|
|
|
|
|
|
<Text className="placeholder-hint">基于您的简历信息进行多维度对比</Text>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
onClick={() => ensureJobPortalLogin('查看竞争力分析')}
|
|
|
|
|
|
>
|
|
|
|
|
|
去登录
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
2026-07-10 17:57:31 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 岗位投诉弹窗 */}
|
|
|
|
|
|
<JobComplaintModal
|
|
|
|
|
|
open={complaintVisible}
|
|
|
|
|
|
jobId={originalJobData?.jobId || originalJobData?.id || id || ''}
|
|
|
|
|
|
onCancel={() => setComplaintVisible(false)}
|
|
|
|
|
|
/>
|
2025-11-10 16:28:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default JobDetailPage;
|