Files
shz-admin/src/pages/JobPortal/Detail/index.tsx
francis-fh 9acb13c4c0
Some checks are pending
Node CI / build (14.x, macOS-latest) (push) Waiting to run
Node CI / build (14.x, ubuntu-latest) (push) Waiting to run
Node CI / build (14.x, windows-latest) (push) Waiting to run
Node CI / build (16.x, macOS-latest) (push) Waiting to run
Node CI / build (16.x, ubuntu-latest) (push) Waiting to run
Node CI / build (16.x, windows-latest) (push) Waiting to run
coverage CI / build (push) Waiting to run
Node pnpm CI / build (16.x, macOS-latest) (push) Waiting to run
Node pnpm CI / build (16.x, ubuntu-latest) (push) Waiting to run
Node pnpm CI / build (16.x, windows-latest) (push) Waiting to run
11
2026-07-26 18:45:43 +08:00

810 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 || `
<h3>职位职责:</h3>
<p>1. 负责相关业务系统的设计与开发;</p>
<p>2. 参与系统架构设计和技术选型;</p>
<p>3. 与团队协作完成项目开发;</p>
<h3>任职要求:</h3>
<p>1. 具备相关工作经验;</p>
<p>2. 熟悉相关技术栈;</p>
<p>3. 具备良好的团队协作能力;</p>
`,
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: `
<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();
const [searchParams] = useSearchParams();
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 }
]);
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
const [jobFeatureEnum, setJobFeatureEnum] = useState<DictValueEnumObj>({});
const [industryTree, setIndustryTree] = useState<any[]>([]);
const [complaintVisible, setComplaintVisible] = useState(false);
// 行为上报:浏览岗位时长统计
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());
};
}, []);
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 (
<div className="job-detail-page">
<JobPortalHeader showSearch={false} showHotJobs={false} />
<div className="job-detail-container">
{/* 职位头部信息 */}
<Card className="job-header-card">
<Row gutter={24}>
<Col span={16}>
<div className="job-title-section">
<Space direction="vertical" size={8}>
<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>
</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>
{jobDetail.companyNature && (
<Tag color="blue">
{getDictLabel(companyNatureEnum, jobDetail.companyNature)}
</Tag>
)}
{jobDetail.tags.map((tag, index) => (
<Tag key={index} color="blue">{tag}</Tag>
))}
</Space>
</Space>
</div>
</Col>
<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>
<Button
type="primary"
size="large"
disabled={originalJobData?.isApply === 1}
onClick={handleApply}
>
{originalJobData?.isApply === 1 ? '已投递' : '立即申请'}
</Button>
<Button
danger
icon={<FlagOutlined />}
size="large"
onClick={() => setComplaintVisible(true)}
>
</Button>
</div>
</Col>
</Row>
</Card>
<Row gutter={16}>
{/* 左侧内容区 */}
<Col span={16}>
{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>
)}
{/* 职位描述 */}
<Card title="职位描述" className="info-card">
<div
dangerouslySetInnerHTML={{ __html: jobDetail.description }}
/>
</Card>
{/* 公司信息 */}
<Card title="公司信息" className="info-card company-info-card">
<div className="company-intro">
<div className="company-overview">
<Avatar size={56} className="company-avatar" src={jobDetail.companyLogo}>
</Avatar>
<div className="company-main">
<Title level={5} className="company-name">
{jobDetail.companyInfo.name}
</Title>
{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>
)}
</div>
<div className="company-block-action">
<Button
danger
icon={<StopOutlined />}
onClick={handleBlockCompany}
>
</Button>
</div>
</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>
</div>
</Card>
</Col>
{/* 右侧侧边栏 */}
<Col span={8}>
{/* 竞争力分析 */}
<Card
title={
<Space>
<TrophyOutlined />
</Space>
}
className="competitiveness-card"
>
{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,
},
}}
scale={{
y: {
domainMin: 0,
domainMax: 100,
nice: true,
},
}}
axis={{
y: {
label: false,
tick: false,
line: false,
},
}}
height={300}
/>
<div className="radar-summary">
<div className="summary-score">
<div className="score-value">{overallScore}</div>
<div className="score-label"></div>
</div>
</div>
</div>
) : (
<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>
)}
</Card>
</Col>
</Row>
</div>
{/* 岗位投诉弹窗 */}
<JobComplaintModal
open={complaintVisible}
jobId={originalJobData?.jobId || originalJobData?.id || id || ''}
onCancel={() => setComplaintVisible(false)}
/>
</div>
);
};
export default JobDetailPage;