111
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 18:47:59 +08:00
parent 971e2b8be5
commit 134c1ecece
13 changed files with 646 additions and 20186 deletions

View File

@@ -33,7 +33,7 @@ import {
} from '@/services/jobportal/competitiveness';
import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth';
import { favoriteJob, unfavoriteJob, applyJob } from '@/services/jobportal/user';
import { ensureJobPortalLogin, PORTAL_LOGIN_URL } from '@/utils/jobPortalAuth';
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
import { getDictLabel, resolveIndustryLabel } from '@/utils/jobPortalDict';
import { getDictValueEnum } from '@/services/system/dict';
import { getCmsIndustryTreeList } from '@/services/classify/industry';
@@ -60,20 +60,6 @@ const experienceMap: { [key: string]: string } = {
'0': '不限'
};
// 从缓存获取用户信息
const getUserIdFromCache = (): number | null => {
try {
const cached = localStorage.getItem('userInfo');
if (cached) {
const userInfo = JSON.parse(cached);
return userInfo?.userId || null;
}
} catch (error) {
console.error('读取缓存用户信息失败:', error);
}
return null;
};
// 格式化薪资显示
const formatSalary = (minSalary: number, maxSalary: number) => {
if (minSalary && maxSalary) {
@@ -228,8 +214,13 @@ const JobDetailPage: React.FC = () => {
const scaleLabel = getDictLabel(scaleEnum, rawScale) || String(rawScale);
if (scaleLabel) items.push(scaleLabel);
}
const industryLabel = resolveIndustryLabel(industryTree, rawIndustry);
if (industryLabel) items.push(industryLabel);
const rawStr = rawIndustry == null ? '' : String(rawIndustry).trim();
// 行业 id 未解析成名称时不展示裸数字,避免与行业名称重复
if (industryLabel && !(/^\d+$/.test(rawStr) && industryLabel === rawStr)) {
items.push(industryLabel);
}
return items;
}, [originalJobData, scaleEnum, industryTree]);
@@ -257,13 +248,22 @@ const JobDetailPage: React.FC = () => {
}, [id, location.state]);
// 基于详情中的 id 再拉一次(避免从路由未传 id 的情况)
// 仅登录且有真实岗位 id 时请求竞争力
useEffect(() => {
const jobIdNum = Number((jobDetail as any)?.id);
if (!Number.isNaN(jobIdNum) && jobIdNum) {
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);
}
}, [jobDetail?.id]);
}, [originalJobData?.jobId, originalJobData?.id, id]);
const fetchCompetitiveness = async (jobIdNum: number) => {
if (!isJobPortalLoggedIn()) {
@@ -306,18 +306,13 @@ const JobDetailPage: React.FC = () => {
};
const handleCollect = async () => {
if (!ensureJobPortalLogin('收藏职位')) {
const userId = await requireJobPortalUserId('收藏职位');
if (!userId) {
return;
}
// 如果是取消收藏,调用取消收藏接口
if (isFavorited) {
const userId = getUserIdFromCache();
if (!userId) {
message.warning('无法获取用户信息,请重新登录');
window.location.href = PORTAL_LOGIN_URL;
return;
}
// 获取职位ID从原始数据或转换后的数据中获取
const jobId = originalJobData?.jobId || originalJobData?.id || (jobDetail as any)?.id || id;
@@ -345,13 +340,6 @@ const JobDetailPage: React.FC = () => {
return;
}
const userId = getUserIdFromCache();
if (!userId) {
message.warning('无法获取用户信息,请重新登录');
window.location.href = PORTAL_LOGIN_URL;
return;
}
// 获取职位ID
const jobId = originalJobData?.jobId || originalJobData?.id || (jobDetail as any)?.id || id;
if (!jobId) {
@@ -378,14 +366,8 @@ const JobDetailPage: React.FC = () => {
};
const handleApply = async () => {
if (!ensureJobPortalLogin('申请职位')) {
return;
}
const userId = getUserIdFromCache();
const userId = await requireJobPortalUserId('申请职位');
if (!userId) {
message.warning('无法获取用户信息,请重新登录');
window.location.href = PORTAL_LOGIN_URL;
return;
}
@@ -494,23 +476,41 @@ const JobDetailPage: React.FC = () => {
</Card>
{/* 公司信息 */}
<Card title="公司信息" className="info-card">
<Card title="公司信息" className="info-card company-info-card">
<div className="company-intro">
<Space size={16}>
<Avatar size={64} src={jobDetail.companyLogo} />
<div>
<Title level={4} style={{ margin: 0 }}>
<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>
<Space className="company-meta" split={<Divider type="vertical" />}>
{companyMetaItems.map((label) => (
<Text key={label}>{label}</Text>
))}
</Space>
{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>
</Space>
<Divider />
<Paragraph>{jobDetail.companyInfo.description}</Paragraph>
</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>
@@ -527,53 +527,69 @@ const JobDetailPage: React.FC = () => {
}
className="competitiveness-card"
>
<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,
},
}}
meta={{
score: {
alias: '评分',
min: 0,
max: 100,
nice: true,
},
}}
yAxis={{
label: false,
tickLine: null,
line: null,
grid: null,
}}
height={300}
/>
<div className="radar-summary">
<div className="summary-score">
<div className="score-value">{overallScore}</div>
<div className="score-label"></div>
{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,
},
}}
meta={{
score: {
alias: '评分',
min: 0,
max: 100,
nice: true,
},
}}
yAxis={{
label: false,
tickLine: null,
line: null,
grid: null,
}}
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>
) : (
<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>