import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams, history, useLocation, useSearchParams } from '@umijs/max'; import { Card, Typography, Row, Col, Tag, Space, Button, Progress, Divider, Avatar, message, Modal } from 'antd'; import { ArrowLeftOutlined, HeartOutlined, HeartFilled, EnvironmentOutlined, ClockCircleOutlined, UserOutlined, TrophyOutlined, FireOutlined, StarOutlined, FlagOutlined, StopOutlined } from '@ant-design/icons'; import { Radar } from '@ant-design/charts'; import JobPortalHeader from '@/components/JobPortalHeader'; import './index.less'; import { getJobCompetitiveness, getEmptyCompetitivenessRadar, } from '@/services/jobportal/competitiveness'; import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth'; import { favoriteJob, unfavoriteJob, applyJob, browseJob, getJobDetail, blockCompany } from '@/services/jobportal/user'; import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth'; import { getDictLabel, resolveIndustryLabel } from '@/utils/jobPortalDict'; import JobComplaintModal from '@/components/JobComplaintModal'; import { getDictValueEnum } from '@/services/system/dict'; import { getCmsIndustryTreeList } from '@/services/classify/industry'; import type { DictValueEnumObj } from '@/components/DictTag'; import { parseCommaSeparatedTags } from '@/utils/jobDetailTags'; 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, companyNature: jobData.companyNature || jobData.company?.companyNature || '', experience: jobData.experience ? experienceMap[jobData.experience] || jobData.experience : '不限', education: jobData.education ? educationMap[jobData.education] || jobData.education : '不限', tags: Array.isArray(jobData.tags) ? jobData.tags : [], jobFeature: jobData.jobFeature, salaryComposition: parseCommaSeparatedTags(jobData.salaryComposition), welfareBenefits: parseCommaSeparatedTags(jobData.welfareBenefits), workSchedule: parseCommaSeparatedTags(jobData.workSchedule), publishTime: jobData.publishTime || new Date().toISOString().split('T')[0], description: jobData.description || `

职位职责:

1. 负责相关业务系统的设计与开发;

2. 参与系统架构设计和技术选型;

3. 与团队协作完成项目开发;

任职要求:

1. 具备相关工作经验;

2. 熟悉相关技术栈;

3. 具备良好的团队协作能力;

`, companyInfo: { name: jobData.companyName || jobData.companyVo?.name || jobData.company, scale: jobData.scale ?? '', industry: jobData.industry ?? '', description: jobData.companyVo?.companyDescription || jobData.companyDescription || '暂无公司描述信息', }, 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: '本科', companyNature: '', jobFeature: '', tags: [], salaryComposition: [] as string[], welfareBenefits: [] as string[], workSchedule: [] as string[], publishTime: '2024-03-15', description: `

职位职责:

1. 负责后端系统架构设计与优化,提升系统性能和稳定性;

2. 参与核心业务模块的设计与开发,使用Java、Spring Boot等框架;

3. 协作完成微服务架构,解决分布式系统相关问题;

4. 进行代码审查,确保代码质量和规范性;

5. 与前端、产品、测试等团队协作,推动项目快速迭代;

任职要求:

1. 本科及以上学历,计算机相关专业,3年以上Java开发经验;

2. 扎实的Java基础,熟悉Spring、Spring Boot、MyBatis等框架;

3. 熟悉MySQL、Redis等数据库,具备SQL优化能力;

4. 了解分布式系统设计,有微服务开发经验;

5. 具备良好的团队协作能力和学习能力;

`, 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(); const [searchParams] = useSearchParams(); const [jobDetail, setJobDetail] = useState(mockJobDetail); const [loading, setLoading] = useState(false); const [isFavorited, setIsFavorited] = useState(false); const [originalJobData, setOriginalJobData] = useState(null); // 保存原始数据引用 const [overallScore, setOverallScore] = useState(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 } ]); const [scaleEnum, setScaleEnum] = useState({}); const [companyNatureEnum, setCompanyNatureEnum] = useState({}); const [jobFeatureEnum, setJobFeatureEnum] = useState({}); const [industryTree, setIndustryTree] = useState([]); const [complaintVisible, setComplaintVisible] = useState(false); // 行为上报:浏览岗位时长统计 const enterTimeRef = useRef(0); const reportedRef = useRef(false); const jobIdRef = useRef(''); // 页面进入/离开时上报浏览行为 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()); }; }, []); 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], ); useEffect(() => { Promise.all([ getDictValueEnum('scale', true, true), getDictValueEnum('company_nature', false, true), getDictValueEnum('job_feature', false, true), getCmsIndustryTreeList(), ]).then(([scaleData, companyNatureData, jobFeatureData, industryRes]) => { setScaleEnum(scaleData); setCompanyNatureEnum(companyNatureData); setJobFeatureEnum(jobFeatureData); if (industryRes?.code === 200 && industryRes?.data) { setIndustryTree(industryRes.data); } }); }, []); const companyMetaItems = useMemo(() => { if (!originalJobData) { return []; } const rawScale = originalJobData.scale; const rawIndustry = originalJobData.industry; const items: string[] = []; if (rawScale != null && rawScale !== '') { const scaleLabel = getDictLabel(scaleEnum, rawScale) || String(rawScale); if (scaleLabel) items.push(scaleLabel); } const industryLabel = resolveIndustryLabel(industryTree, rawIndustry); const rawStr = rawIndustry == null ? '' : String(rawIndustry).trim(); // 行业 id 未解析成名称时不展示裸数字,避免与行业名称重复 if (industryLabel && !(/^\d+$/.test(rawStr) && industryLabel === rawStr)) { items.push(industryLabel); } return items; }, [originalJobData, scaleEnum, industryTree]); 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); // 列表卡片不携带详情标签;进入详情页后始终请求详情接口补齐完整岗位数据。 const passedJobId = passedJobData.jobId || passedJobData.id || id; if (passedJobId) { fetchJobDetailFromApi(String(passedJobId)); } } else { // 直接刷新页面时,从 URL 参数或 route param 获取 jobId,调 API 获取数据 const jobIdFromUrl = searchParams.get('jobId'); if (jobIdFromUrl) { fetchJobDetailFromApi(jobIdFromUrl); } else { setOriginalJobData(null); setIsFavorited(false); } } }, [id, location.state]); // 页面直接刷新时调接口获取岗位详情 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); } }; // 仅登录且有真实岗位 id 时请求竞争力 useEffect(() => { if (!isJobPortalLoggedIn()) { setOverallScore(0); setRadarData(getEmptyCompetitivenessRadar()); return; } const jobIdRaw = originalJobData?.jobId ?? originalJobData?.id ?? id; const jobIdNum = Number(jobIdRaw); if (!Number.isNaN(jobIdNum) && jobIdNum > 0) { fetchCompetitiveness(jobIdNum); } }, [originalJobData?.jobId, originalJobData?.id, id]); // 仅登录且有真实岗位 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]); 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) { setOverallScore(0); setRadarData(getEmptyCompetitivenessRadar()); return; } 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 { setRadarData(getEmptyCompetitivenessRadar()); } } catch (e) { setOverallScore(0); setRadarData(getEmptyCompetitivenessRadar()); } }; const handleCollect = async () => { const userId = await requireJobPortalUserId('收藏职位'); if (!userId) { 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 () => { const userId = await requireJobPortalUserId('申请职位'); 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('申请成功'); if (originalJobData) { setOriginalJobData({ ...originalJobData, isApply: 1 }); } } else { message.error(response?.msg || '申请失败'); } } catch (error) { console.error('申请失败:', error); message.error('申请失败,请重试'); } }; 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('操作失败,请稍后重试'); } }, }); }; return (
{/* 职位头部信息 */}
{jobDetail.title} {jobDetail.jobFeature && ( <Tag color="orange" style={{ marginLeft: 10, fontSize: 13, lineHeight: '24px', verticalAlign: 'middle' }} > {getDictLabel(jobFeatureEnum, jobDetail.jobFeature)} </Tag> )} {jobDetail.salary}
{jobDetail.location} {jobDetail.experience} {jobDetail.education} {jobDetail.companyNature && ( {getDictLabel(companyNatureEnum, jobDetail.companyNature)} )} {jobDetail.tags.map((tag, index) => ( {tag} ))}
{/* 左侧内容区 */} {detailTagGroups.length > 0 && (
{detailTagGroups.map((group) => (
{group.label}
{group.values.map((value) => ( {value} ))}
))}
)} {/* 职位描述 */}
{/* 公司信息 */}
{jobDetail.companyInfo.name} {companyMetaItems.length > 0 && (
{companyMetaItems.map((label, index) => ( {index > 0 && |} {label} ))}
)} {(originalJobData?.jobLocation || jobDetail.location) && (
{originalJobData?.jobLocation || jobDetail.location}
)}
公司介绍
{jobDetail.companyInfo.description}
{/* 右侧侧边栏 */} {/* 竞争力分析 */} 竞争力分析 } className="competitiveness-card" > {isJobPortalLoggedIn() ? (
{overallScore}
综合评分
) : (
登录后可查看您与该岗位的匹配度分析 基于您的简历信息进行多维度对比
)}
{/* 岗位投诉弹窗 */} setComplaintVisible(false)} />
); }; export default JobDetailPage;