职业推荐
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:
@@ -80,6 +80,10 @@ export default [
|
||||
path: '/job-portal/message',// 消息通知页面
|
||||
component: './JobPortal/Message',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/career-recommendation',// 职业推荐
|
||||
component: './JobPortal/CareerRecommendation',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/policy',// 政策列表
|
||||
component: './JobPortal/Policy',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
BellOutlined,
|
||||
ReadOutlined,
|
||||
LoginOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation, useModel } from '@umijs/max';
|
||||
import {
|
||||
@@ -100,6 +101,7 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
const isMine = location.pathname.startsWith('/job-portal/personal-center') || location.pathname.startsWith('/job-portal/profile');
|
||||
const isMessage = location.pathname.startsWith('/job-portal/message');
|
||||
const isPolicy = location.pathname.startsWith('/job-portal/policy');
|
||||
const isCareerRecommend = location.pathname.startsWith('/job-portal/career-recommendation');
|
||||
|
||||
// 获取未读消息数量(仅登录用户)
|
||||
useEffect(() => {
|
||||
@@ -277,6 +279,14 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
>
|
||||
简历
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FundProjectionScreenOutlined />}
|
||||
onClick={() => handleNavClick('/job-portal/career-recommendation')}
|
||||
className={`nav-btn${isCareerRecommend ? ' active' : ''}`}
|
||||
>
|
||||
职业推荐
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReadOutlined />}
|
||||
|
||||
337
src/pages/JobPortal/CareerRecommendation/index.less
Normal file
337
src/pages/JobPortal/CareerRecommendation/index.less
Normal file
@@ -0,0 +1,337 @@
|
||||
@import '../theme.less';
|
||||
|
||||
.career-recommend-page {
|
||||
min-height: 100vh;
|
||||
background: #eef4fa;
|
||||
|
||||
.career-content {
|
||||
padding: 20px 20px 40px;
|
||||
|
||||
.career-container {
|
||||
.jp-page-container();
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
.career-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.career-card {
|
||||
.jp-card-base();
|
||||
border-radius: @jp-radius-lg;
|
||||
padding: 16px;
|
||||
min-height: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: @jp-primary;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 当前职位
|
||||
.position-card {
|
||||
.position-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
|
||||
.position-info {
|
||||
flex: 1;
|
||||
|
||||
.position-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.position-salary {
|
||||
font-size: 14px;
|
||||
color: @jp-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.position-tag {
|
||||
align-self: flex-start;
|
||||
background: @jp-primary-light;
|
||||
border: 1px solid @jp-primary-border;
|
||||
color: @jp-primary;
|
||||
border-radius: 4px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 学历时间轴
|
||||
.education-timeline {
|
||||
padding: 16px 8px 8px;
|
||||
|
||||
.timeline-track {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, #91caff 0%, #1890ff 100%);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
transform: translateX(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #fff;
|
||||
border: 3px solid #ff7a45;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba(255, 122, 69, 0.4);
|
||||
}
|
||||
|
||||
.timeline-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.timeline-item {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
|
||||
.level-name {
|
||||
font-size: 13px;
|
||||
color: @jp-text-primary;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.active {
|
||||
color: #ff7a45;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.level-salary {
|
||||
font-size: 12px;
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 薪酬分析滑块
|
||||
.salary-gauge {
|
||||
padding: 16px 8px 8px;
|
||||
|
||||
.gauge-track-wrap {
|
||||
position: relative;
|
||||
padding-top: 36px;
|
||||
}
|
||||
|
||||
.gauge-track {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: #e8ecf0;
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
|
||||
.gauge-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: #1890ff;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gauge-marker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
.marker-bubble {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #ff7a45;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #ff7a45;
|
||||
}
|
||||
}
|
||||
|
||||
.marker-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #ff7a45;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba(255, 122, 69, 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gauge-value-row {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
margin-top: 10px;
|
||||
|
||||
.marker-value {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: @jp-text-secondary;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.gauge-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// 学历分析底部
|
||||
.education-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed @jp-border;
|
||||
font-size: 12px;
|
||||
color: @jp-text-secondary;
|
||||
|
||||
span strong {
|
||||
color: @jp-primary;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 技能分析
|
||||
.skill-analysis {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 200px;
|
||||
|
||||
.skill-categories {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid @jp-border;
|
||||
overflow-y: auto;
|
||||
|
||||
.category-item {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: @jp-text-secondary;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: @jp-primary-light;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: @jp-primary;
|
||||
background: @jp-primary-light;
|
||||
border-left-color: @jp-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skill-children {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
|
||||
.skill-tag {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid @jp-border;
|
||||
border-radius: 4px;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 差距分析
|
||||
.gap-list {
|
||||
.gap-item {
|
||||
margin-bottom: 14px;
|
||||
|
||||
.gap-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.ant-progress-inner {
|
||||
background: #e8ecf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
369
src/pages/JobPortal/CareerRecommendation/index.tsx
Normal file
369
src/pages/JobPortal/CareerRecommendation/index.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Avatar, Progress, Spin } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Bar, Heatmap, Line, Pie } from '@ant-design/charts';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import defaultAvatar from '@/assets/avatar.jpg';
|
||||
import { getAppUserInfo } from '@/services/jobportal/user';
|
||||
import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth';
|
||||
import {
|
||||
DEFAULT_EDUCATION,
|
||||
DEFAULT_POSITION,
|
||||
DEFAULT_SALARY_K,
|
||||
educationLevels,
|
||||
educationPieData,
|
||||
educationSummary,
|
||||
fixedHeatmapData,
|
||||
gapAnalysisData,
|
||||
positionExperienceData,
|
||||
salaryLineData,
|
||||
skillCategories,
|
||||
} from './mockData';
|
||||
import './index.less';
|
||||
|
||||
interface UserProfile {
|
||||
positionName: string;
|
||||
salaryK: number;
|
||||
education: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
const educationDictMap: Record<string, string> = {
|
||||
'0': '不限',
|
||||
'1': '专科及以下',
|
||||
'2': '本科',
|
||||
'3': '硕士',
|
||||
'4': '博士',
|
||||
};
|
||||
|
||||
const educationKeyMap: Record<string, number> = {
|
||||
专科: 0,
|
||||
专科及以下: 0,
|
||||
本科: 1,
|
||||
硕士: 2,
|
||||
博士: 3,
|
||||
};
|
||||
|
||||
const parseSalaryK = (min?: string, max?: string): number => {
|
||||
const val = min || max;
|
||||
if (!val) return DEFAULT_SALARY_K;
|
||||
const num = parseInt(String(val).replace(/[^\d]/g, ''), 10);
|
||||
if (Number.isNaN(num)) return DEFAULT_SALARY_K;
|
||||
const k = num >= 1000 ? Math.round(num / 1000) : num;
|
||||
return Math.min(30, Math.max(0, k));
|
||||
};
|
||||
|
||||
const CareerCard: React.FC<{ title: string; className?: string; children: React.ReactNode }> = ({
|
||||
title,
|
||||
className = '',
|
||||
children,
|
||||
}) => (
|
||||
<div className={`career-card ${className}`}>
|
||||
<div className="card-title">{title}</div>
|
||||
<div className="card-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CareerRecommendationPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [profile, setProfile] = useState<UserProfile>({
|
||||
positionName: DEFAULT_POSITION,
|
||||
salaryK: DEFAULT_SALARY_K,
|
||||
education: DEFAULT_EDUCATION,
|
||||
avatar: '',
|
||||
});
|
||||
const [activeSkillKey, setActiveSkillKey] = useState(skillCategories[0].key);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isJobPortalLoggedIn()) return;
|
||||
|
||||
const fetchProfile = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getAppUserInfo();
|
||||
if (res?.code === 200 && res.data) {
|
||||
const data = res.data as Record<string, string>;
|
||||
const positionName =
|
||||
data.jobTitle || data.field_19 || data.desiredTitle || DEFAULT_POSITION;
|
||||
setProfile({
|
||||
positionName,
|
||||
salaryK: parseSalaryK(data.salaryMin, data.salaryMax),
|
||||
education: educationDictMap[data.education] || data.education || DEFAULT_EDUCATION,
|
||||
avatar: data.avatar || '',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// 使用 mock 默认值
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProfile();
|
||||
}, []);
|
||||
|
||||
const educationIndex = useMemo(() => {
|
||||
const edu = profile.education;
|
||||
for (const [key, idx] of Object.entries(educationKeyMap)) {
|
||||
if (edu.includes(key)) return idx;
|
||||
}
|
||||
return 1;
|
||||
}, [profile.education]);
|
||||
|
||||
const educationMarkerLeft = `${(educationIndex / (educationLevels.length - 1)) * 100}%`;
|
||||
const salaryMarkerLeft = `${(profile.salaryK / 30) * 100}%`;
|
||||
const highlightSalary = `${profile.salaryK}k`;
|
||||
|
||||
const activeSkill = skillCategories.find((c) => c.key === activeSkillKey) || skillCategories[0];
|
||||
|
||||
const barConfig = {
|
||||
data: positionExperienceData,
|
||||
xField: 'range',
|
||||
yField: 'count',
|
||||
height: 200,
|
||||
autoFit: true,
|
||||
legend: false,
|
||||
axis: {
|
||||
x: { title: false },
|
||||
y: { title: false, grid: true },
|
||||
},
|
||||
style: {
|
||||
fill: (datum: { range: string }) =>
|
||||
datum.range === '5-8年' ? '#ff7a45' : '#91caff',
|
||||
radiusTopLeft: 4,
|
||||
radiusTopRight: 4,
|
||||
},
|
||||
label: {
|
||||
text: (d: { count: number; range: string }) =>
|
||||
d.range === '5-8年' ? `当前\n${d.count}` : String(d.count),
|
||||
position: 'top' as const,
|
||||
style: { fill: '#595959', fontSize: 11 },
|
||||
},
|
||||
};
|
||||
|
||||
const pieConfig = {
|
||||
data: educationPieData,
|
||||
angleField: 'value',
|
||||
colorField: 'type',
|
||||
height: 160,
|
||||
autoFit: true,
|
||||
innerRadius: 0.6,
|
||||
legend: {
|
||||
color: {
|
||||
position: 'right' as const,
|
||||
layout: { justifyContent: 'center' },
|
||||
},
|
||||
},
|
||||
label: {
|
||||
text: (d: { type: string; value: number }) => `${d.type} ${d.value}%`,
|
||||
style: { fontSize: 10 },
|
||||
},
|
||||
color: ['#91caff', '#69b1ff', '#1890ff', '#d9d9d9'],
|
||||
};
|
||||
|
||||
const lineConfig = {
|
||||
data: salaryLineData,
|
||||
xField: 'salary',
|
||||
yField: 'percent',
|
||||
height: 200,
|
||||
autoFit: true,
|
||||
smooth: true,
|
||||
point: {
|
||||
shapeField: 'point',
|
||||
sizeField: 3,
|
||||
style: (datum: { salary: string }) => ({
|
||||
fill: datum.salary === highlightSalary ? '#ff7a45' : '#1890ff',
|
||||
stroke: datum.salary === highlightSalary ? '#ff7a45' : '#1890ff',
|
||||
lineWidth: datum.salary === highlightSalary ? 2 : 0,
|
||||
}),
|
||||
},
|
||||
style: { stroke: '#1890ff', lineWidth: 2 },
|
||||
axis: {
|
||||
y: { title: false, labelFormatter: (v: number) => `${v}%` },
|
||||
x: { title: false },
|
||||
},
|
||||
};
|
||||
|
||||
const heatmapConfig = {
|
||||
data: fixedHeatmapData,
|
||||
xField: 'position',
|
||||
yField: 'skill',
|
||||
colorField: 'value',
|
||||
height: 200,
|
||||
autoFit: true,
|
||||
mark: 'cell',
|
||||
style: { inset: 2 },
|
||||
scale: {
|
||||
color: { range: ['#fff1f0', '#ffccc7', '#ffa39e', '#ff7875', '#f5222d'] },
|
||||
},
|
||||
axis: {
|
||||
x: { title: false },
|
||||
y: { title: false },
|
||||
},
|
||||
legend: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="career-recommend-page">
|
||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||
|
||||
<div className="career-content">
|
||||
<Spin spinning={loading}>
|
||||
<div className="career-container">
|
||||
<div className="career-grid">
|
||||
{/* 当前职位 */}
|
||||
<CareerCard title="当前职位" className="position-card">
|
||||
<div className="position-profile">
|
||||
<Avatar
|
||||
size={64}
|
||||
src={profile.avatar || defaultAvatar}
|
||||
icon={<UserOutlined />}
|
||||
/>
|
||||
<div className="position-info">
|
||||
<div className="position-name">{profile.positionName}</div>
|
||||
<div className="position-salary">薪资: {profile.salaryK}千元/月</div>
|
||||
</div>
|
||||
<span className="position-tag">当前职位</span>
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 学历情况 */}
|
||||
<CareerCard title="学历情况">
|
||||
<div className="education-timeline">
|
||||
<div className="timeline-track">
|
||||
<div
|
||||
className="timeline-marker"
|
||||
style={{ left: educationMarkerLeft }}
|
||||
/>
|
||||
</div>
|
||||
<div className="timeline-labels">
|
||||
{educationLevels.map((item, idx) => (
|
||||
<div key={item.key} className="timeline-item">
|
||||
<div className={`level-name${idx === educationIndex ? ' active' : ''}`}>
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="level-salary">{item.salaryRange}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 薪酬分析 */}
|
||||
<CareerCard title="薪酬分析">
|
||||
<div className="salary-gauge">
|
||||
<div className="gauge-track-wrap">
|
||||
<div className="gauge-track">
|
||||
<div className="gauge-fill" style={{ width: salaryMarkerLeft }} />
|
||||
<div className="gauge-marker" style={{ left: salaryMarkerLeft }}>
|
||||
<div className="marker-bubble">您的位置</div>
|
||||
<div className="marker-dot" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="gauge-value-row">
|
||||
<span
|
||||
className="marker-value"
|
||||
style={{ left: salaryMarkerLeft }}
|
||||
>
|
||||
{profile.salaryK}千元
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gauge-labels">
|
||||
<span>0千元</span>
|
||||
<span>30千元</span>
|
||||
</div>
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 职位情况 */}
|
||||
<CareerCard title="职位情况">
|
||||
<div className="chart-wrap">
|
||||
<Bar {...barConfig} />
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 学历分析 */}
|
||||
<CareerCard title="学历分析">
|
||||
<div className="chart-wrap">
|
||||
<Pie {...pieConfig} />
|
||||
</div>
|
||||
<div className="education-footer">
|
||||
<span>
|
||||
入行门槛: <strong>{educationSummary.entryThreshold}</strong>
|
||||
</span>
|
||||
<span>
|
||||
平均学历: <strong>{educationSummary.averageEducation}</strong>
|
||||
</span>
|
||||
<span>
|
||||
学历溢价: <strong>{educationSummary.premium}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 薪资情况 */}
|
||||
<CareerCard title="薪资情况">
|
||||
<div className="chart-wrap">
|
||||
<Line {...lineConfig} />
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 技能热力图 */}
|
||||
<CareerCard title="目标职位技能热力图分析">
|
||||
<div className="chart-wrap">
|
||||
<Heatmap {...heatmapConfig} />
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 技能分析 */}
|
||||
<CareerCard title="技能分析">
|
||||
<div className="skill-analysis">
|
||||
<div className="skill-categories">
|
||||
{skillCategories.map((cat) => (
|
||||
<div
|
||||
key={cat.key}
|
||||
className={`category-item${activeSkillKey === cat.key ? ' active' : ''}`}
|
||||
onClick={() => setActiveSkillKey(cat.key)}
|
||||
>
|
||||
{cat.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="skill-children">
|
||||
{activeSkill.children.map((skill) => (
|
||||
<span key={skill} className="skill-tag">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CareerCard>
|
||||
|
||||
{/* 差距分析 */}
|
||||
<CareerCard title="目标职位差距分析">
|
||||
<div className="gap-list">
|
||||
{gapAnalysisData.map((item) => (
|
||||
<div key={item.name} className="gap-item">
|
||||
<div className="gap-label">
|
||||
<span>{item.name}</span>
|
||||
<span>{item.percent}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={item.percent}
|
||||
showInfo={false}
|
||||
strokeColor="#1890ff"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CareerCard>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerRecommendationPage;
|
||||
104
src/pages/JobPortal/CareerRecommendation/mockData.ts
Normal file
104
src/pages/JobPortal/CareerRecommendation/mockData.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/** 职业推荐页 mock 数据 */
|
||||
|
||||
export const DEFAULT_POSITION = '人力资源总监';
|
||||
export const DEFAULT_SALARY_K = 7;
|
||||
export const DEFAULT_EDUCATION = '本科';
|
||||
export const educationLevels = [
|
||||
{ key: 'junior', label: '专科及以下', salaryRange: '7-10K' },
|
||||
{ key: 'bachelor', label: '本科', salaryRange: '8-20K' },
|
||||
{ key: 'master', label: '硕士', salaryRange: '12-15K' },
|
||||
{ key: 'doctor', label: '博士', salaryRange: '15-30K' },
|
||||
];
|
||||
|
||||
export const positionExperienceData = [
|
||||
{ range: '1-3年', count: 34 },
|
||||
{ range: '3-5年', count: 37 },
|
||||
{ range: '5-8年', count: 369, isCurrent: true },
|
||||
{ range: '8-10年', count: 16 },
|
||||
{ range: '10年以上', count: 3 },
|
||||
];
|
||||
|
||||
export const educationPieData = [
|
||||
{ type: '专科及以下', value: 18 },
|
||||
{ type: '硕士', value: 2 },
|
||||
{ type: '本科', value: 76 },
|
||||
{ type: '不详', value: 5 },
|
||||
];
|
||||
|
||||
export const educationSummary = {
|
||||
entryThreshold: '本科',
|
||||
averageEducation: '本科',
|
||||
premium: '0',
|
||||
};
|
||||
|
||||
export const salaryLineData = [
|
||||
{ salary: '5k', percent: 2 },
|
||||
{ salary: '7k', percent: 8 },
|
||||
{ salary: '9k', percent: 12 },
|
||||
{ salary: '11k', percent: 15 },
|
||||
{ salary: '13k', percent: 18 },
|
||||
{ salary: '15k', percent: 16 },
|
||||
{ salary: '17k', percent: 12 },
|
||||
{ salary: '19k', percent: 8 },
|
||||
{ salary: '21k', percent: 5 },
|
||||
{ salary: '25k', percent: 3 },
|
||||
{ salary: '30k', percent: 1 },
|
||||
];
|
||||
|
||||
export const fixedHeatmapData: { skill: string; position: string; value: number }[] = [
|
||||
{ skill: '沟通协作', position: 'COO', value: 72 },
|
||||
{ skill: '沟通协作', position: '副总经理', value: 85 },
|
||||
{ skill: '沟通协作', position: 'HRM', value: 90 },
|
||||
{ skill: '沟通协作', position: 'HRD', value: 88 },
|
||||
{ skill: '沟通协作', position: '人事经理', value: 78 },
|
||||
{ skill: '沟通协作', position: '招聘经理', value: 65 },
|
||||
{ skill: '团队协作', position: 'COO', value: 68 },
|
||||
{ skill: '团队协作', position: '副总经理', value: 82 },
|
||||
{ skill: '团队协作', position: 'HRM', value: 95 },
|
||||
{ skill: '团队协作', position: 'HRD', value: 92 },
|
||||
{ skill: '团队协作', position: '人事经理', value: 80 },
|
||||
{ skill: '团队协作', position: '招聘经理', value: 70 },
|
||||
{ skill: '跨部门协作', position: 'COO', value: 90 },
|
||||
{ skill: '跨部门协作', position: '副总经理', value: 88 },
|
||||
{ skill: '跨部门协作', position: 'HRM', value: 75 },
|
||||
{ skill: '跨部门协作', position: 'HRD', value: 82 },
|
||||
{ skill: '跨部门协作', position: '人事经理', value: 68 },
|
||||
{ skill: '跨部门协作', position: '招聘经理', value: 55 },
|
||||
{ skill: '沟通表达', position: 'COO', value: 78 },
|
||||
{ skill: '沟通表达', position: '副总经理', value: 80 },
|
||||
{ skill: '沟通表达', position: 'HRM', value: 88 },
|
||||
{ skill: '沟通表达', position: 'HRD', value: 85 },
|
||||
{ skill: '沟通表达', position: '人事经理', value: 72 },
|
||||
{ skill: '沟通表达', position: '招聘经理', value: 60 },
|
||||
];
|
||||
|
||||
export const skillCategories = [
|
||||
{
|
||||
key: 'strategy',
|
||||
label: '战略管理',
|
||||
children: ['战略规划', '企业管理', '风险管理', '组织发展'],
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: '行政事务',
|
||||
children: ['行政管理', '后勤保障', '会务接待', '档案管理'],
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
label: '财务分析',
|
||||
children: ['预算管理', '成本控制', '财务报告', '税务筹划'],
|
||||
},
|
||||
{
|
||||
key: 'hr',
|
||||
label: '人力资源',
|
||||
children: ['招聘配置', '绩效管理', '薪酬福利', '员工关系'],
|
||||
},
|
||||
];
|
||||
|
||||
export const gapAnalysisData = [
|
||||
{ name: '人力资源管理', percent: 100 },
|
||||
{ name: '信息化管理', percent: 40 },
|
||||
{ name: '战略与业务发展', percent: 89 },
|
||||
{ name: '组织文化建设', percent: 72 },
|
||||
{ name: '劳动法规合规', percent: 95 },
|
||||
];
|
||||
Reference in New Issue
Block a user