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
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:
@@ -68,6 +68,7 @@ export function clearAllClientStorage() {
|
||||
clearTokenCache();
|
||||
const knownKeys = [
|
||||
'userInfo',
|
||||
'getInfoCache',
|
||||
'appUserId',
|
||||
'resume_userId',
|
||||
'seesionId',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
exchangeThirdPartyCredential,
|
||||
isThirdPartyTransitionPage,
|
||||
} from './utils/thirdPartyLogin';
|
||||
import { saveGetInfoCache } from './utils/jobPortalAuth';
|
||||
import {
|
||||
getRemoteMenu,
|
||||
getRoutersInfo,
|
||||
@@ -59,6 +60,7 @@ export async function getInitialState(): Promise<{
|
||||
response.user.avatar =
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
|
||||
}
|
||||
saveGetInfoCache(response as unknown as Record<string, unknown>);
|
||||
return {
|
||||
...response.user,
|
||||
permissions: response.permissions,
|
||||
|
||||
@@ -19,8 +19,10 @@ import {
|
||||
import { history, useLocation, useModel } from '@umijs/max';
|
||||
import {
|
||||
ensureJobPortalLogin,
|
||||
getGetInfoCache,
|
||||
isJobPortalLoggedIn,
|
||||
PORTAL_LOGIN_URL,
|
||||
prefetchJobPortalUserInfo,
|
||||
} from '@/utils/jobPortalAuth';
|
||||
import { getJobRecommend } from '@/services/common/jobTitle';
|
||||
import { getMessageTotal } from '@/services/jobportal/user';
|
||||
@@ -56,12 +58,18 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const currentUser = initialState?.currentUser;
|
||||
|
||||
// 获取用户名,优先使用 initialState,其次使用 localStorage 中的 userInfo
|
||||
// 获取用户名,优先 initialState,其次 getInfo 缓存,最后 appUser 缓存
|
||||
const getUserName = (): string => {
|
||||
if (currentUser?.nickName) {
|
||||
return currentUser.nickName;
|
||||
}
|
||||
// 尝试从 localStorage 获取用户信息
|
||||
const getInfoUser = getGetInfoCache()?.user;
|
||||
if (getInfoUser?.nickName) {
|
||||
return String(getInfoUser.nickName);
|
||||
}
|
||||
if (getInfoUser?.userName) {
|
||||
return String(getInfoUser.userName);
|
||||
}
|
||||
try {
|
||||
const cachedUserInfo = localStorage.getItem('userInfo');
|
||||
if (cachedUserInfo) {
|
||||
@@ -79,6 +87,13 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
const userName = getUserName();
|
||||
const loggedIn = isJobPortalLoggedIn();
|
||||
|
||||
// SSO 仅有 token、本地尚无 userInfo 时预拉求职者资料
|
||||
useEffect(() => {
|
||||
if (loggedIn) {
|
||||
prefetchJobPortalUserInfo();
|
||||
}
|
||||
}, [loggedIn]);
|
||||
|
||||
// 判断激活导航(简化为 startsWith 匹配)
|
||||
const isHome = /^\/job-portal(\/(list|detail))?$/.test(location.pathname);
|
||||
const isResume = location.pathname.startsWith('/job-portal/resume');
|
||||
|
||||
@@ -26,9 +26,11 @@ export default function useAppUserModel() {
|
||||
const response = await getAppUserInfo();
|
||||
if (response?.code === 200 && response?.data) {
|
||||
setUserInfo(response.data);
|
||||
// 保存到缓存
|
||||
try {
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data));
|
||||
if (response.data.userId) {
|
||||
localStorage.setItem('appUserId', String(response.data.userId));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户信息到缓存失败:', error);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
.score-label {
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
|
||||
.competitiveness-login-placeholder {
|
||||
.jp-competitiveness-login-placeholder();
|
||||
}
|
||||
}
|
||||
|
||||
.skills-card .skill-item {
|
||||
@@ -64,6 +68,89 @@
|
||||
border-radius: @jp-radius;
|
||||
border: 1px solid @jp-primary-border;
|
||||
}
|
||||
|
||||
.company-info-card {
|
||||
.company-overview {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.company-avatar {
|
||||
flex-shrink: 0;
|
||||
background: @jp-primary-light !important;
|
||||
color: @jp-primary;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.company-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
margin: 0 0 10px !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.company-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: @jp-text-muted;
|
||||
line-height: 1.5;
|
||||
|
||||
.meta-separator {
|
||||
margin: 0 10px;
|
||||
color: @jp-border;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.company-location {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: @jp-text-secondary;
|
||||
line-height: 1.55;
|
||||
|
||||
.anticon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 3px;
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.company-section-divider {
|
||||
margin: 18px 0 16px;
|
||||
}
|
||||
|
||||
.company-description-section {
|
||||
.company-section-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.company-description {
|
||||
margin: 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
color: @jp-text-secondary;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -110,13 +110,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.job-detail-card,
|
||||
.company-info-card {
|
||||
.job-detail-card {
|
||||
.jp-card-base();
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.job-detail-card {
|
||||
.job-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -235,6 +232,130 @@
|
||||
.score-value {
|
||||
color: @jp-primary;
|
||||
}
|
||||
|
||||
.competitiveness-login-placeholder {
|
||||
.jp-competitiveness-login-placeholder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.company-info-card {
|
||||
.jp-card-base();
|
||||
margin-top: 16px;
|
||||
|
||||
.ant-card-head {
|
||||
min-height: 48px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid @jp-border;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.company-overview {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.company-avatar {
|
||||
flex-shrink: 0;
|
||||
background: @jp-primary-light !important;
|
||||
color: @jp-primary;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.company-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
margin: 0 0 10px !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.company-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: @jp-text-muted;
|
||||
line-height: 1.5;
|
||||
|
||||
.meta-separator {
|
||||
margin: 0 10px;
|
||||
color: @jp-border;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.company-location {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: @jp-text-secondary;
|
||||
line-height: 1.55;
|
||||
|
||||
.anticon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 3px;
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.company-section-divider {
|
||||
margin: 18px 0 16px;
|
||||
}
|
||||
|
||||
.company-description-section {
|
||||
.company-section-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.company-description {
|
||||
margin: 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
color: @jp-text-secondary;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.company-description-empty {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,18 +371,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.company-info-card {
|
||||
margin-top: 16px;
|
||||
|
||||
.company-name {
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.company-meta .ant-typography {
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation } from '@umijs/max';
|
||||
import { getJobList } from '@/services/common/jobTitle';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import { favoriteJob, unfavoriteJob, applyJob } from '@/services/jobportal/user';
|
||||
import { ensureJobPortalLogin, PORTAL_LOGIN_URL } from '@/utils/jobPortalAuth';
|
||||
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getDictLabel, findTreeLabelById, resolveIndustryLabel } from '@/utils/jobPortalDict';
|
||||
import { getJobTitleTreeSelect } from '@/services/common/jobTitle';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
@@ -64,19 +64,6 @@ const experienceMap: { [key: string]: string } = {
|
||||
};
|
||||
|
||||
// 从缓存获取用户信息
|
||||
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 mockJobs = [
|
||||
{
|
||||
@@ -264,8 +251,12 @@ const JobListPage: 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();
|
||||
if (industryLabel && !(/^\d+$/.test(rawStr) && industryLabel === rawStr)) {
|
||||
items.push(industryLabel);
|
||||
}
|
||||
return items;
|
||||
}, [selectedJob, scaleEnum, industryTree]);
|
||||
|
||||
@@ -316,13 +307,17 @@ const JobListPage: React.FC = () => {
|
||||
}
|
||||
}, [selectedJob]);
|
||||
|
||||
// 当 selectedJob 变化时,获取竞争力分析数据
|
||||
// 当 selectedJob 变化时,获取竞争力分析数据(仅登录)
|
||||
useEffect(() => {
|
||||
if (selectedJob) {
|
||||
const jobIdNum = Number(selectedJob?.jobId || selectedJob?.id);
|
||||
if (!Number.isNaN(jobIdNum) && jobIdNum) {
|
||||
fetchCompetitiveness(jobIdNum);
|
||||
}
|
||||
if (!selectedJob) return;
|
||||
if (!isJobPortalLoggedIn()) {
|
||||
setOverallScore(0);
|
||||
setRadarData(getEmptyCompetitivenessRadar());
|
||||
return;
|
||||
}
|
||||
const jobIdNum = Number(selectedJob?.jobId || selectedJob?.id);
|
||||
if (!Number.isNaN(jobIdNum) && jobIdNum) {
|
||||
fetchCompetitiveness(jobIdNum);
|
||||
}
|
||||
}, [selectedJob]);
|
||||
|
||||
@@ -404,17 +399,12 @@ const JobListPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleCollect = async () => {
|
||||
if (!ensureJobPortalLogin('收藏职位')) {
|
||||
const userId = await requireJobPortalUserId('收藏职位');
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (favorited) {
|
||||
const userId = getUserIdFromCache();
|
||||
if (!userId) {
|
||||
message.warning('无法获取用户信息,请重新登录');
|
||||
window.location.href = PORTAL_LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取职位ID
|
||||
const jobId = selectedJob?.jobId || selectedJob?.id;
|
||||
@@ -449,13 +439,6 @@ const JobListPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getUserIdFromCache();
|
||||
if (!userId) {
|
||||
message.warning('无法获取用户信息,请重新登录');
|
||||
window.location.href = PORTAL_LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取职位ID
|
||||
const jobId = selectedJob?.jobId || selectedJob?.id;
|
||||
if (!jobId) {
|
||||
@@ -489,14 +472,8 @@ const JobListPage: 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;
|
||||
}
|
||||
|
||||
@@ -854,53 +831,69 @@ const JobListPage: 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>
|
||||
</Row>
|
||||
@@ -909,37 +902,43 @@ const JobListPage: React.FC = () => {
|
||||
|
||||
{/* 公司信息卡片 */}
|
||||
{(selectedJob?.companyInfo || selectedJob?.companyName || selectedJob?.company) && (
|
||||
<Card className="company-info-card">
|
||||
<Title level={4} style={{ marginBottom: 16 }}>公司信息</Title>
|
||||
<Card className="company-info-card" title="公司信息">
|
||||
<div className="company-overview">
|
||||
<Avatar size={80} style={{ backgroundColor: '#f0f0f0' }}>
|
||||
公司
|
||||
<Avatar size={56} className="company-avatar">
|
||||
企
|
||||
</Avatar>
|
||||
<div className="company-info-right">
|
||||
<Title level={4} className="company-name">
|
||||
<div className="company-main">
|
||||
<Title level={5} className="company-name">
|
||||
{selectedJob?.companyInfo?.name || selectedJob?.companyName || selectedJob?.company}
|
||||
</Title>
|
||||
<div className="company-meta">
|
||||
{selectedCompanyMetaItems.length > 0 ? (
|
||||
selectedCompanyMetaItems.map((label, index) => (
|
||||
{selectedCompanyMetaItems.length > 0 && (
|
||||
<div className="company-meta-row">
|
||||
{selectedCompanyMetaItems.map((label, index) => (
|
||||
<React.Fragment key={`${label}-${index}`}>
|
||||
{index > 0 && <Divider type="vertical" />}
|
||||
<Text>{label}</Text>
|
||||
{index > 0 && <span className="meta-separator">|</span>}
|
||||
<span>{label}</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">暂无公司信息</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="company-meta">
|
||||
<Text><EnvironmentOutlined /> {selectedJob?.jobLocation || '暂无地址信息'}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedJob?.jobLocation && (
|
||||
<div className="company-location">
|
||||
<EnvironmentOutlined />
|
||||
<span>{selectedJob.jobLocation}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider className="company-section-divider" />
|
||||
<div className="company-description-section">
|
||||
<Paragraph className="company-description">
|
||||
{selectedJob?.companyVo?.companyDescription || '暂无公司描述'}
|
||||
</Paragraph>
|
||||
<div className="company-section-label">公司介绍</div>
|
||||
{selectedJob?.companyVo?.companyDescription ? (
|
||||
<Paragraph className="company-description">
|
||||
{selectedJob.companyVo.companyDescription}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph className="company-description-empty">暂无公司描述</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -49,6 +49,28 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jp-competitiveness-login-placeholder() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
gap: 12px;
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.placeholder-hint {
|
||||
font-size: 12px;
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// 门户页面根容器
|
||||
.job-portal-page-root {
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -1,23 +1,163 @@
|
||||
import { Modal } from 'antd';
|
||||
import { Modal, message } from 'antd';
|
||||
import { getAccessToken } from '@/access';
|
||||
import { getAppUserInfo } from '@/services/jobportal/user';
|
||||
|
||||
/** 人社门户登录页 */
|
||||
export const PORTAL_LOGIN_URL = 'http://218.31.252.15:9081/hrss-web-vue/login';
|
||||
|
||||
/** /api/getInfo 缓存结构(不含 msg、code) */
|
||||
export interface GetInfoCache {
|
||||
permissions?: string[];
|
||||
roles?: string[];
|
||||
user?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const GET_INFO_CACHE_KEY = 'getInfoCache';
|
||||
const DEFAULT_AVATAR =
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
|
||||
|
||||
let appUserInfoPromise: Promise<number | null> | null = null;
|
||||
|
||||
/** 读取 /api/getInfo 完整缓存 */
|
||||
export function getGetInfoCache(): GetInfoCache | null {
|
||||
try {
|
||||
const cached = localStorage.getItem(GET_INFO_CACHE_KEY);
|
||||
if (cached) {
|
||||
return JSON.parse(cached) as GetInfoCache;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 将 /api/getInfo 返回体(除 msg、code)写入 localStorage */
|
||||
export function saveGetInfoCache(response: Record<string, unknown>): void {
|
||||
try {
|
||||
const { msg: _msg, code: _code, ...rest } = response;
|
||||
const user = rest.user
|
||||
? { ...(rest.user as Record<string, unknown>) }
|
||||
: undefined;
|
||||
|
||||
if (user && (user.avatar === '' || user.avatar == null)) {
|
||||
user.avatar = DEFAULT_AVATAR;
|
||||
}
|
||||
|
||||
const payload: GetInfoCache = {
|
||||
permissions: (rest.permissions as string[]) ?? [],
|
||||
roles: (rest.roles as string[]) ?? [],
|
||||
user,
|
||||
};
|
||||
|
||||
localStorage.setItem(GET_INFO_CACHE_KEY, JSON.stringify(payload));
|
||||
|
||||
if (user) {
|
||||
const raw = user.appUserId ?? user.userId;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = Number(raw);
|
||||
if (!Number.isNaN(n) && n > 0) {
|
||||
localStorage.setItem('appUserId', String(n));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存 getInfo 缓存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除 getInfo 缓存 */
|
||||
export function clearGetInfoCache(): void {
|
||||
localStorage.removeItem(GET_INFO_CACHE_KEY);
|
||||
}
|
||||
|
||||
/** 从本地缓存读取求职者 userId(同步) */
|
||||
export const getJobPortalUserId = (): number | null => {
|
||||
try {
|
||||
const cached = localStorage.getItem('userInfo');
|
||||
if (cached) {
|
||||
const userInfo = JSON.parse(cached);
|
||||
const raw = userInfo?.userId ?? userInfo?.id;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = Number(raw);
|
||||
if (!Number.isNaN(n) && n > 0) return n;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const getInfo = getGetInfoCache();
|
||||
const sessionUser = getInfo?.user;
|
||||
if (sessionUser) {
|
||||
const raw = sessionUser.appUserId ?? sessionUser.userId;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = Number(raw);
|
||||
if (!Number.isNaN(n) && n > 0) return n;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const appUserId = localStorage.getItem('appUserId');
|
||||
if (appUserId) {
|
||||
const n = Number(appUserId);
|
||||
if (!Number.isNaN(n) && n > 0) return n;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isJobPortalLoggedIn = (): boolean => {
|
||||
if (getAccessToken()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const cached = localStorage.getItem('userInfo');
|
||||
if (cached) {
|
||||
const userInfo = JSON.parse(cached);
|
||||
return !!userInfo?.userId;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
return !!getJobPortalUserId();
|
||||
};
|
||||
|
||||
/**
|
||||
* 已登录但本地无 userId 时,拉取 appUser 信息并写入 userInfo
|
||||
*/
|
||||
export const ensureJobPortalUserId = async (): Promise<number | null> => {
|
||||
const cached = getJobPortalUserId();
|
||||
if (cached) return cached;
|
||||
|
||||
if (!getAccessToken()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!appUserInfoPromise) {
|
||||
appUserInfoPromise = (async () => {
|
||||
try {
|
||||
const response = await getAppUserInfo();
|
||||
if (response?.code === 200 && response?.data) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data));
|
||||
const raw = response.data.userId ?? (response.data as { id?: number }).id;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = Number(raw);
|
||||
if (!Number.isNaN(n) && n > 0) {
|
||||
localStorage.setItem('appUserId', String(n));
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取求职者用户信息失败:', error);
|
||||
}
|
||||
return null;
|
||||
})().finally(() => {
|
||||
appUserInfoPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return appUserInfoPromise;
|
||||
};
|
||||
|
||||
/** 门户进入后预拉求职者信息,避免有 token 无 userId */
|
||||
export const prefetchJobPortalUserInfo = (): void => {
|
||||
if (getAccessToken() && !getJobPortalUserId()) {
|
||||
void ensureJobPortalUserId();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** 未登录时弹窗提示并跳转登录页,已登录返回 true */
|
||||
@@ -36,3 +176,16 @@ export const ensureJobPortalLogin = (actionHint = '该操作'): boolean => {
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
/** 收藏/申请等需要 userId 的操作:先校验登录,再确保 userId 可用 */
|
||||
export const requireJobPortalUserId = async (actionHint: string): Promise<number | null> => {
|
||||
if (!ensureJobPortalLogin(actionHint)) {
|
||||
return null;
|
||||
}
|
||||
const userId = await ensureJobPortalUserId();
|
||||
if (!userId) {
|
||||
message.warning('无法获取用户信息,请稍后重试');
|
||||
return null;
|
||||
}
|
||||
return userId;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user