merge: 合并 CareerMap 分支到 main

This commit is contained in:
2025-11-12 19:33:53 +08:00
26 changed files with 8307 additions and 937 deletions

View File

@@ -177,7 +177,7 @@ footer-height = 98rpx
/* 页面容器 */
.container {
position: fixed;
z-index: 1000;
z-index: 1;
width: 100vw;
height: calc(100% - var(--window-bottom));
overflow: hidden;

View File

@@ -106,17 +106,17 @@
</view>
<view class="service-title">题库和考试</view>
</view>
<view class="service-item press-button" @click="goCa()">
<view class="service-item press-button" @click="handleServiceClick('quality-assessment')">
<view class="service-icon service-icon-8">
<IconfontIcon name="suzhicepingtiku" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">素质测评</view>
</view>
<view class="service-item press-button" @click="goAiAu()">
<view class="service-item press-button" @click="handleServiceClick('ai-interview')">
<view class="service-icon service-icon-9">
<IconfontIcon name="ai" :size="68" color="#FFFFFF" />
</view>
<view class="service-title">虚拟面试</view>
<view class="service-title">AI智能面试</view>
</view>
<view class="service-item press-button" style="justify-content:normal" @click="goRc()">
<view class="service-icon service-icon-9">
@@ -124,11 +124,17 @@
</view>
<view class="service-title" style="overflow:unset">高校毕业生<br/>智慧就业服务</view>
</view>
<view class="service-item press-button" @click="navToPage">
<view class="service-item press-button" @click="handleServiceClick('career-planning')">
<view class="service-icon service-icon-11">
<image class="service-icon-img" src="/static/icon/antOutline.png" mode="aspectFit"></image>
</view>
<view class="service-title">职业规划推荐</view>
</view>
<view class="service-item press-button" @click="navToTestPage">
<view class="service-icon service-icon-10">
<uni-icons type="gear-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="service-title">帮扶</view>
<view class="service-title">测试页面</view>
</view>
</view>
</view>
@@ -593,6 +599,8 @@ const { columnCount, columnSpace } = useColumnCount(() => {
const getCompanyInfo = () => {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
console.log('缓存中的userInfo:', cachedUserInfo);
// 重置企业信息
companyInfo.name = '';
companyInfo.avatar = '';
@@ -699,49 +707,15 @@ const handleServiceClick = (serviceType) => {
}
};
// 处理直播按钮点击 - 跳转微信视频号
// 处理直播按钮点击
const handleLiveClick = () => {
// #ifdef MP-WEIXIN
const feedId = 'sphKH1AEeLfTJJE';
// 使用微信原生 API 打开视频号直播
if (typeof wx !== 'undefined' && wx.openChannelsUserProfile) {
wx.openChannelsUserProfile({
// feedId: feedId,
finderUserName: feedId, // 视频号 finderUserName如果feedId足够可以留空
success: (res) => {
console.log('打开视频号成功', res);
},
fail: (err) => {
console.error('打开视频号失败', err);
$api.msg(err.errMsg || '无法打开直播,请稍后重试');
}
});
} else {
// 如果 API 不存在,尝试使用 uni API
uni.openChannelsLive({
feedId: feedId,
success: (res) => {
console.log('打开视频号成功', res);
},
fail: (err) => {
console.error('打开视频号失败', err);
$api.msg('无法打开直播,请检查微信版本或稍后重试');
}
});
}
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境提示
$api.msg('该功能仅在微信小程序中可用');
// #endif
$api.msg('该功能正在开发中');
};
const navToPage = () =>{
// navTo('/packageB/login?flag=nw');
navTo('/packageB/priority/helpFilter');
}
// 跳转到测试页面
const navToTestPage = () => {
navTo('/pages/test/homepage-test');
};
async function loadData() {
try {
@@ -1034,49 +1008,17 @@ function dataToImg(data) {
// import { loginRc } from '@/apiRc/login/login.js';
import storeRc from '@/utilsRc/store/index.js';
import { getToken } from '@/utilsRc/auth.js';
// 跳转到高校毕业页面
function goRc(){
if (checkLogin()) {
let token = getToken();
console.log(token,'t123312')
if(!token){
console.log(111)
let userInfo = uni.getStorageSync('userInfo')
storeRc.dispatch('LoginByUserInfo', userInfo).then(res => {
// console.log(res, "'res");
storeRc.dispatch('GetInfo').then(res => {
if(res.data.user.userType == 'person'){
navTo('/packageRc/pages/index/index');
}else{
navTo('/packageRc/pages/daiban/daiban');
}
});
});
}else{
if(storeRc.state.user.type == 'person'){
navTo('/packageRc/pages/index/index');
}else{
navTo('/packageRc/pages/daiban/daiban');
}
}
let userInfo = uni.getStorageSync('userInfo')
console.log(uni.getStorageSync('userInfo'), "uni.getStorageSync('userInfo')");
storeRc.dispatch('LoginByUserInfo', userInfo).then(res => {
// console.log(res, "'res");
navTo('/packageRc/pages/index/index');
});
}
}
// 跳转素质测评页面(在线测评,职业库,职业生涯规划)
function goCa(){
if (checkLogin()) {
const userInfo = uni.getStorageSync('userInfo')
navTo(`/packageCa/search/search?userId=${userInfo.userId}&name=${userInfo.name}`);
}
}
// 跳转AI智能面试
function goAiAu(){
if (checkLogin()) {
const userInfo = uni.getStorageSync('userInfo')
navTo(`/packageCa/search/AIAudition?userId=${userInfo.userId}&name=${userInfo.name}`);
}
}
defineExpose({ loadData });
@@ -1383,15 +1325,17 @@ defineExpose({ loadData });
align-items: center
justify-content: center
.service-icon-11
background: linear-gradient(135deg, #FF9800 0%, #FFB74D 100%)
background-color: #72A4FC
position: relative
&::before
content: '📈'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
display: flex
align-items: center
justify-content: center
.service-icon-img
width: 42rpx
height: 42rpx
position: relative
z-index: 1
filter: brightness(1.8) contrast(1.4)
.service-icon-12
background: linear-gradient(135deg, #4CAF50 0%, #81C784 100%)
position: relative

View File

@@ -0,0 +1,531 @@
<template>
<view class="career-planning-page">
<!-- 提醒弹窗 -->
<RemindPopup
ref="remindPopup"
:remind-list="remindList"
@cancel="handleCancel"
@confirm="handleConfirm"
/>
<!-- 技能详情弹出层 -->
<SkillDetailPopup
ref="skillDetailPopup"
:job-title="selectedJobTitle"
:possessed-skills="selectedJobPossessedSkills"
:improvement-skills="selectedJobImprovementSkills"
@close="handleSkillPopupClose"
/>
<!-- 页面内容 -->
<view class="page-content" v-if="showContent">
<!-- #ifdef MP-WEIXIN -->
<!-- 小程序背景图片 -->
<image class="mp-background" src="/static/icon/background2.png" mode="aspectFill"></image>
<!-- #endif -->
<!-- 头部区域 -->
<PageHeader
:active-tab="activeTab"
@tab-change="switchTab"
@search-click="handleSearchClick"
@menu-click="handleMenuClick"
@more-click="handleMoreClick"
/>
<!-- 内容区域 -->
<scroll-view scroll-y class="content-scroll">
<CareerRecommend
v-if="activeTab === 0"
:current-job-id="currentJobId"
:current-job-name="currentJobName"
@job-card-click="handleJobCardClick"
@skills-updated="handleRecommendSkillsUpdated"
/>
<CareerPath
v-else-if="activeTab === 1"
:current-job-name="currentJobName"
@path-data-updated="handlePathDataUpdated"
/>
<SkillDevelopment
v-else
:current-job-skills="recommendSkillsData.currentJobSkills"
:recommended-jobs="recommendSkillsData.recommendedJobs"
:path-data="pathSkillsData.pathData"
:target-career="pathSkillsData.targetCareer"
/>
</scroll-view>
</view>
<!-- 底部导航栏 -->
<view class="tabbar-wrapper" v-if="showContent">
<CustomTabBar :currentPage="0" />
</view>
</view>
</template>
<script setup>
import { ref, inject, nextTick, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getJobSkillDetail } from '@/apiRc/jobSkill.js';
import { appUserInfo } from '@/apiRc/user/user.js';
import RemindPopup from './components/RemindPopup.vue';
import PageHeader from './components/PageHeader.vue';
import SkillDetailPopup from './components/SkillDetailPopup.vue';
import CareerRecommend from './components/CareerRecommend.vue';
import CareerPath from './components/CareerPath.vue';
import SkillDevelopment from './components/SkillDevelopment.vue';
import CustomTabBar from '@/components/CustomTabBar/CustomTabBar.vue';
const { navBack, navTo } = inject('globalFunction');
// 弹窗引用
const remindPopup = ref(null);
const skillDetailPopup = ref(null);
// 提醒列表(由接口返回)
const remindList = ref([]);
// 是否显示页面内容
const showContent = ref(false);
// 当前激活的tab
const activeTab = ref(0);
// 选中的职位信息
const selectedJobTitle = ref('');
const selectedJobPossessedSkills = ref([]);
const selectedJobImprovementSkills = ref([]);
const isLoadingJobSkill = ref(false);
const currentJobId = ref(null);
const currentJobName = ref('');
// 技能发展所需的数据
const recommendSkillsData = ref({
currentJobSkills: [],
recommendedJobs: []
});
const pathSkillsData = ref({
pathData: {
start: { title: '', skills: [] },
steps: [],
end: { title: '', skills: [] }
},
targetCareer: ''
});
// 打开弹窗
function openRemindPopup() {
nextTick(() => {
if (remindPopup.value) {
try {
remindPopup.value.open();
} catch (error) {
// 静默处理错误
}
} else {
setTimeout(() => {
if (remindPopup.value) {
try {
remindPopup.value.open();
} catch (error) {
// 静默处理错误
}
} else {
setTimeout(() => {
if (remindPopup.value) {
try {
remindPopup.value.open();
} catch (error) {
// 静默处理错误
}
}
}, 500);
}
}, 500);
}
});
}
// 检查用户是否完善了个人信息(调用接口获取)
let hasCheckedRemindInfo = false;
async function getRemindInfo() {
if (hasCheckedRemindInfo) {
return;
}
hasCheckedRemindInfo = true;
try {
const response = await appUserInfo();
const userInfo = response?.data || {};
// 优先从接口数据中获取 idCard
let idCard = userInfo?.resume?.idCard ?? userInfo?.idCard ?? null;
// 如果接口返回的数据中没有 idCard则从缓存中读取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
if (!idCard || idCard === null || idCard === '') {
idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
}
// 优先从接口数据中获取职位信息
currentJobId.value = userInfo?.jobId ??
userInfo?.currentJobId ??
userInfo?.resume?.jobId ??
userInfo?.resume?.currentJobId ??
null;
currentJobName.value = userInfo?.jobName ??
userInfo?.currentJobName ??
userInfo?.resume?.jobName ??
userInfo?.resume?.currentJobName ??
'';
// 如果接口数据中没有职位信息,从缓存中读取
if (!currentJobId.value && !currentJobName.value) {
currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
currentJobName.value = cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ??
'';
}
// 如果还是没有职位信息,使用默认值
if (!currentJobName.value) {
currentJobName.value = '市场专员';
}
// 判断 idCard 是否存在包括空字符串、undefined、null 的情况)
const hasIdCard = idCard !== null && idCard !== undefined && idCard !== '';
if (!hasIdCard) {
remindList.value = ['请完善个人信息'];
} else {
remindList.value = ['暂无待完善信息'];
}
setTimeout(() => {
openRemindPopup();
}, 500);
} catch (error) {
// 接口调用失败时,使用缓存作为降级方案
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
// 从缓存中获取职位信息
currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
currentJobName.value = cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ??
'';
// 如果缓存中没有职位信息,使用默认值
if (!currentJobName.value) {
currentJobName.value = '市场专员';
}
if (!idCard || idCard === null || idCard === '') {
remindList.value = ['请完善个人信息'];
} else {
remindList.value = ['暂无待完善信息'];
}
setTimeout(() => {
openRemindPopup();
}, 500);
}
}
// 取消按钮
function handleCancel() {
remindPopup.value?.close();
navBack();
}
// 确认按钮
async function handleConfirm() {
remindPopup.value?.close();
// 直接从缓存中读取 idCard因为接口返回的数据中没有 idCard
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
let idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
// 如果缓存中也没有,尝试从接口获取(虽然接口通常也没有)
if (!idCard || idCard === null || idCard === '') {
try {
const response = await appUserInfo();
const userInfo = response?.data || {};
idCard = userInfo?.resume?.idCard ?? userInfo?.idCard ?? null;
} catch (error) {
// 接口调用失败,继续使用缓存数据
}
}
// 如果 idCard 为空,才跳转到完善信息页面
if (!idCard || idCard === null || idCard === '') {
navTo('/pages/complete-info/complete-info');
return;
}
// 如果 idCard 存在,说明已经完善了信息,直接显示页面内容
// 从缓存中更新职位信息
currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
currentJobName.value = cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ??
'';
if (!currentJobName.value) {
currentJobName.value = '市场专员';
}
showContent.value = true;
}
// 切换tab
function switchTab(index) {
activeTab.value = index;
if (index === 0 && !currentJobId.value) {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const newJobId = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
const newJobName = currentJobName.value ||
(cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ??
'市场专员');
currentJobId.value = newJobId;
currentJobName.value = newJobName;
}
}
// 搜索点击
function handleSearchClick() {
navTo('/pages/search/search');
}
// 菜单点击
function handleMenuClick() {
// TODO: 实现菜单功能
}
// 更多点击
function handleMoreClick() {
// TODO: 实现更多功能
}
function normalizeSkillLevel(score) {
const numericScore = Number(score);
if (Number.isNaN(numericScore)) {
return 0;
}
const rounded = Math.round(numericScore);
return Math.max(1, Math.min(6, rounded));
}
function splitSkillListByScore(skills = []) {
if (!Array.isArray(skills) || skills.length === 0) {
return {
possessed: [],
improvement: []
};
}
const sorted = [...skills].sort((a, b) => (Number(b.skillScore) || 0) - (Number(a.skillScore) || 0));
const midpoint = Math.ceil(sorted.length / 2);
const mapSkill = (item) => ({
name: item?.skillName || '',
level: normalizeSkillLevel(item?.skillScore)
});
return {
possessed: sorted.slice(0, midpoint).map(mapSkill),
improvement: sorted.slice(midpoint).map(mapSkill)
};
}
// 处理职位卡片点击
async function handleJobCardClick(job) {
if (!job) {
return;
}
selectedJobTitle.value = job.title || job.jobName || '';
selectedJobPossessedSkills.value = [];
selectedJobImprovementSkills.value = [];
if (isLoadingJobSkill.value) {
return;
}
isLoadingJobSkill.value = true;
uni.showLoading({
title: '加载中...',
mask: true
});
const fallbackSkills = Array.isArray(job?.rawSkills) ? job.rawSkills : [];
const params = {};
if (job?.jobId) {
params.jobId = job.jobId;
}
if (job?.title) {
params.jobName = job.title;
} else if (job?.jobName) {
params.jobName = job.jobName;
}
try {
const response = await getJobSkillDetail(params);
const skillList = Array.isArray(response?.data) ? response.data : [];
const { possessed, improvement } = splitSkillListByScore(skillList);
if (possessed.length === 0 && improvement.length === 0) {
if (fallbackSkills.length === 0) {
uni.showToast({
title: '暂无技能数据',
icon: 'none'
});
return;
}
const fallbackSplit = splitSkillListByScore(fallbackSkills);
selectedJobPossessedSkills.value = fallbackSplit.possessed;
selectedJobImprovementSkills.value = fallbackSplit.improvement;
} else {
selectedJobPossessedSkills.value = possessed;
selectedJobImprovementSkills.value = improvement;
}
skillDetailPopup.value?.open();
} catch (error) {
if (fallbackSkills.length > 0) {
const fallbackSplit = splitSkillListByScore(fallbackSkills);
selectedJobPossessedSkills.value = fallbackSplit.possessed;
selectedJobImprovementSkills.value = fallbackSplit.improvement;
skillDetailPopup.value?.open();
} else {
uni.showToast({
title: '获取技能信息失败',
icon: 'none'
});
}
} finally {
isLoadingJobSkill.value = false;
uni.hideLoading();
}
}
// 处理技能弹出层关闭
function handleSkillPopupClose() {
// 可以在这里处理关闭后的逻辑
}
// 处理职业推荐技能数据更新
function handleRecommendSkillsUpdated(data) {
recommendSkillsData.value = {
currentJobSkills: data.currentJobSkills || [],
recommendedJobs: data.recommendedJobs || []
};
}
// 处理职业路径数据更新
function handlePathDataUpdated(data) {
pathSkillsData.value = {
pathData: data.pathData || {
start: { title: '', skills: [] },
steps: [],
end: { title: '', skills: [] }
},
targetCareer: data.targetCareer || ''
};
}
onLoad(() => {
getRemindInfo();
});
onMounted(() => {
if (remindList.value.length > 0 && !showContent.value) {
setTimeout(() => {
if (remindPopup.value) {
openRemindPopup();
}
}, 300);
}
});
</script>
<style lang="scss" scoped>
.career-planning-page {
width: 100vw;
/* #ifdef H5 */
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
/* #endif */
/* #ifdef MP-WEIXIN */
height: 100vh;
position: relative;
/* #endif */
background-color: #FFFFFF;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* #ifdef MP-WEIXIN */
.mp-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 728rpx;
z-index: 0;
}
/* #endif */
.page-content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
overflow: hidden;
}
.content-scroll {
flex: 1;
height: 0;
width: 100%;
padding-bottom: calc(88rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.tabbar-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
}
</style>

View File

@@ -0,0 +1,561 @@
<template>
<view class="career-path">
<!-- 职业路径查询区域 -->
<view class="query-section">
<view class="section-title">
<uni-icons type="search" size="18" color="#286BFA"></uni-icons>
<text class="title-text">职业路径查询</text>
</view>
<view class="input-group">
<view class="input-item">
<text class="input-label">当前职位</text>
<input
class="input-field"
:value="currentPosition"
placeholder="市场专员"
placeholder-style="color: #999999"
disabled
/>
</view>
<view class="input-item">
<text class="input-label">目标职业</text>
<picker
mode="selector"
:range="targetCareerOptions"
range-key="label"
:value="selectedTargetIndex"
@change="handleTargetChange"
>
<view class="picker-field">
<text :class="selectedTargetIndex >= 0 ? 'picker-text' : 'picker-placeholder'">
{{ selectedTargetIndex >= 0 ? targetCareerOptions[selectedTargetIndex].label : '请选择目标职业' }}
</text>
<uni-icons type="arrowdown" size="16" color="#999999"></uni-icons>
</view>
</picker>
</view>
</view>
<button class="query-btn" @click="handleQuery">
<text>查询职业发展路径</text>
<uni-icons type="search" size="18" color="#FFFFFF"></uni-icons>
</button>
<view v-if="totalPathCount > 0" class="path-summary">
系统已收录 {{ totalPathCount }} 条职业路径
</view>
</view>
<!-- 职业发展路径区域 -->
<view class="path-section">
<view class="section-title">
<uni-icons type="person-filled" size="24" color="#000000"></uni-icons>
<text class="title-text">职业发展路径</text>
</view>
<view class="timeline">
<!-- 起点 -->
<view class="timeline-item start">
<view class="timeline-marker start-marker"></view>
<view class="timeline-content">
<view class="step-title">起点: {{ pathData.start.title }}</view>
<view class="skill-tags">
<view
class="skill-tag"
v-for="(skill, index) in pathData.start.skills"
:key="index"
>
{{ skill }}
</view>
</view>
</view>
</view>
<!-- 步骤 -->
<view
class="timeline-item step"
v-for="(step, index) in pathData.steps"
:key="index"
>
<view class="timeline-marker step-marker"></view>
<view class="timeline-content">
<view class="step-title">{{ index + 1 }}: {{ step.title }}</view>
<view class="skill-tags">
<view
class="skill-tag"
v-for="(skill, sIndex) in step.skills"
:key="sIndex"
>
{{ skill }}
</view>
</view>
</view>
</view>
<!-- 终点 -->
<view class="timeline-item end">
<view class="timeline-marker end-marker"></view>
<view class="timeline-content">
<view class="step-title">终点: {{ pathData.end.title }}</view>
<view class="skill-tags">
<view
class="skill-tag"
v-for="(skill, index) in pathData.end.skills"
:key="index"
>
{{ skill }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/jobPath.js';
// 接收父组件传递的当前职位名称
const props = defineProps({
currentJobName: {
type: String,
default: ''
}
});
const emit = defineEmits(['path-data-updated']);
// 当前职位(从父组件获取,统一使用)
const currentPosition = computed(() => props.currentJobName || '市场专员');
// 目标职业选项列表
const targetCareerOptions = ref([]);
const selectedTargetIndex = ref(-1);
const selectedJobPathId = ref(null);
// 职业路径数量
const totalPathCount = ref(0);
// 初始路径数据
const emptyPathData = {
start: {
title: '暂无数据',
skills: []
},
steps: [],
end: {
title: '暂无数据',
skills: []
}
};
const pathData = ref({ ...emptyPathData });
const isLoadingPath = ref(false);
function parseSkillList(skillString) {
if (!skillString) {
return [];
}
return skillString
.split(/[,]/)
.map(item => item.trim())
.filter(item => item.length > 0);
}
function resetPathData() {
pathData.value = {
start: { ...emptyPathData.start },
steps: [],
end: { ...emptyPathData.end }
};
}
async function fetchTargetCareerOptions(keyword = '') {
try {
const response = await getJobPathPage({
jobName: keyword,
pageNo: 1,
pageSize: 100
});
const list = response?.data?.list || response?.list || [];
targetCareerOptions.value = list.map(item => ({
label: item.endJob || item.startJob || '未知职位',
value: item.id,
startJob: item.startJob,
endJob: item.endJob,
jobOrder: item.jobOrder
}));
if (targetCareerOptions.value.length === 0) {
selectedTargetIndex.value = -1;
selectedJobPathId.value = null;
}
} catch (error) {
targetCareerOptions.value = [];
selectedTargetIndex.value = -1;
selectedJobPathId.value = null;
uni.showToast({
title: '职业路径列表获取失败',
icon: 'none'
});
}
}
async function fetchPathCount() {
try {
const response = await getJobPathNum();
totalPathCount.value = response?.data ?? 0;
} catch (error) {
totalPathCount.value = 0;
}
}
async function loadPathDetail(jobPathId) {
if (!jobPathId || jobPathId === null || jobPathId === undefined || jobPathId === '') {
uni.showToast({
title: '职业路径ID无效',
icon: 'none'
});
resetPathData();
return;
}
try {
const requestParams = {
jobPathId: jobPathId
};
const response = await getJobPathDetail(requestParams);
const details = Array.isArray(response?.data) ? response.data : [];
if (details.length === 0) {
resetPathData();
uni.showToast({
title: '暂无职业路径数据',
icon: 'none'
});
return;
}
const normalized = details.map(item => ({
title: item?.name || '未命名职位',
skills: parseSkillList(item?.skillNameList)
}));
const start = normalized[0] || { title: '暂无数据', skills: [] };
const end = normalized[normalized.length - 1] || { title: '暂无数据', skills: [] };
const steps = normalized.slice(1, normalized.length - 1);
pathData.value = {
start,
steps,
end
};
// 通知父组件路径数据已更新
emit('path-data-updated', {
pathData: pathData.value,
targetCareer: targetCareerOptions.value[selectedTargetIndex.value]?.label || ''
});
} catch (error) {
uni.showToast({
title: '获取路径详情失败',
icon: 'none'
});
resetPathData();
emit('path-data-updated', {
pathData: { ...emptyPathData },
targetCareer: ''
});
}
}
function handleTargetChange(e) {
const index = Number(e.detail.value);
selectedTargetIndex.value = index;
const option = targetCareerOptions.value[index];
selectedJobPathId.value = option ? option.value : null;
}
async function handleQuery() {
if (selectedTargetIndex.value < 0) {
uni.showToast({
title: '请选择目标职业',
icon: 'none'
});
return;
}
const option = targetCareerOptions.value[selectedTargetIndex.value];
if (!option) {
uni.showToast({
title: '目标职业数据异常',
icon: 'none'
});
return;
}
let jobPathId = option.value;
isLoadingPath.value = true;
uni.showLoading({
title: '加载中...',
mask: true
});
try {
if (!jobPathId) {
const response = await getJobPathPage({
jobName: option.label,
pageNo: 1,
pageSize: 100
});
jobPathId = response?.data?.list?.[0]?.id || null;
}
if (!jobPathId) {
uni.showToast({
title: '未找到职业路径',
icon: 'none'
});
resetPathData();
return;
}
selectedJobPathId.value = jobPathId;
await loadPathDetail(jobPathId);
} catch (error) {
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
});
} finally {
isLoadingPath.value = false;
uni.hideLoading();
}
}
// 获取当前职位信息(从接口获取)
async function getCurrentPosition() {
// TODO: 调用接口获取当前职位
// const response = await getCareerCurrentPosition();
// if (response && response.code === 200) {
// currentPosition.value = response.data?.position || '';
// }
}
onMounted(async () => {
await getCurrentPosition();
await Promise.all([
fetchTargetCareerOptions(),
fetchPathCount()
]);
});
</script>
<style lang="scss" scoped>
.career-path {
padding: 10rpx 28rpx 20rpx;
}
.query-section {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.section-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #157DF0;
}
.input-group {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 24rpx;
}
.input-item {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.input-label {
font-size: 28rpx;
color: #000000;
font-weight: 500;
}
.input-field {
background-color: #F5F5F5;
border: 1rpx solid #E0E0E0;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #666666;
}
.picker-field {
background-color: #F5F5F5;
border: 1rpx solid #E0E0E0;
border-radius: 12rpx;
padding: 20rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-text {
font-size: 28rpx;
color: #000000;
}
.picker-placeholder {
font-size: 28rpx;
color: #999999;
}
.query-btn {
width: 100%;
height: 80rpx;
line-height: 40rpx;
border-radius: 20rpx;
background: linear-gradient(180deg, rgba(18, 125, 240, 1) 0%, rgba(59, 14, 123, 0.71) 100%);
color: rgba(255, 255, 255, 1);
font-size: 28rpx;
text-align: center;
font-family: '阿里巴巴普惠体3.0-regular', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
border: 2rpx solid rgba(187, 187, 187, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 0;
}
.path-summary {
margin-top: 16rpx;
font-size: 24rpx;
color: #666666;
text-align: center;
}
.path-section {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.timeline {
position: relative;
padding-left: 40rpx;
}
.timeline-item {
position: relative;
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
&:not(:last-child)::before {
content: '';
position: absolute;
left: -32rpx;
top: 0;
width: 2rpx;
height: 100%;
background: repeating-linear-gradient(
to bottom,
#E0E0E0 0,
#E0E0E0 6rpx,
transparent 6rpx,
transparent 12rpx
);
transform: translateX(-50%);
z-index: 1;
}
}
.timeline-marker {
position: absolute;
left: -32rpx;
top: 0;
transform: translateX(-50%);
width: 32rpx;
height: 32rpx;
border-radius: 50%;
z-index: 2;
}
.start-marker {
background-color: #FF4444;
border: 4rpx solid #FFFFFF;
box-shadow: 0 0 0 2rpx #FF4444;
}
.step-marker {
background-color: #286BFA;
border: 4rpx solid #FFFFFF;
box-shadow: 0 0 0 2rpx #286BFA;
}
.end-marker {
background-color: #52C41A;
border: 4rpx solid #FFFFFF;
box-shadow: 0 0 0 2rpx #52C41A;
}
.timeline-content {
background-color: #F5F5F5;
border-radius: 12rpx;
padding: 24rpx;
margin-left: 20rpx;
}
.step-title {
font-size: 30rpx;
font-weight: 600;
color: #000000;
margin-bottom: 16rpx;
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.skill-tag {
background-color: #F0F0F0;
color: #286BFA;
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
white-space: nowrap;
}
button::after {
border: none;
}
</style>

View File

@@ -0,0 +1,388 @@
<template>
<view class="career-recommend">
<!-- 当前职位信息卡片 -->
<view class="info-card">
<view class="card-title">当前职位信息</view>
<view class="card-content">
<text class="label">当前职位</text>
<text class="value">{{ currentJobDisplay }}</text>
</view>
</view>
<!-- 我的技能标签卡片 -->
<view class="info-card">
<view class="card-title">我的技能标签</view>
<view class="skill-tags">
<view
class="skill-tag"
v-for="(skill, index) in skillTags"
:key="index"
>
{{ skill }}
</view>
<text v-if="!skillTags.length && !isLoadingSkillTags" class="empty-text">暂无技能数据</text>
</view>
</view>
<!-- 相似推荐职位 -->
<view class="section-title">
相似推荐职位
</view>
<view v-if="!isLoadingRecommend && recommendedJobs.length === 0" class="empty-text">暂无推荐职位</view>
<view
class="job-item-card"
v-for="(job, index) in recommendedJobs"
:key="index"
@click="handleJobCardClick(job)"
>
<button class="search-btn" @click.stop="handleJobSearch(job)">职位搜索</button>
<view class="job-header">
<text class="job-title">{{ job.title }}</text>
</view>
<view class="job-skills">
<view
class="job-skill-tag"
v-for="(skill, skillIndex) in job.skills"
:key="skillIndex"
>
{{ skill }}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { recommendJob } from '@/apiRc/jobRecommend.js';
import { getJobSkillDetail } from '@/apiRc/jobSkill.js';
const props = defineProps({
currentJobId: {
type: [Number, String],
default: null
},
currentJobName: {
type: String,
default: ''
}
});
const emit = defineEmits(['job-card-click', 'skills-updated']);
// 数据状态
const skillTags = ref([]);
const recommendedJobs = ref([]);
const isLoadingSkillTags = ref(false);
const isLoadingRecommend = ref(false);
// 计算属性
const currentJobDisplay = computed(() => props.currentJobName || '市场专员');
// 从技能列表中提取技能名称用于显示
function extractSkillNames(skillList = []) {
return (Array.isArray(skillList) ? skillList : [])
.map(item => item?.skillName || '')
.filter(name => !!name && name.trim().length > 0);
}
// 获取当前职位的技能标签
async function fetchCurrentJobSkills() {
// 如果没有职位名称,从缓存中获取技能标签
if (!props.currentJobName || props.currentJobName === '市场专员') {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const cachedSkills = cachedUserInfo?.resume?.skillList ??
cachedUserInfo?.skillList ??
cachedUserInfo?.appSkillsList ??
[];
// 如果缓存中有技能数据,使用缓存的数据
if (Array.isArray(cachedSkills) && cachedSkills.length > 0) {
skillTags.value = extractSkillNames(cachedSkills);
} else {
skillTags.value = [];
}
// 通知父组件技能数据已更新
emit('skills-updated', {
currentJobSkills: skillTags.value,
recommendedJobs: recommendedJobs.value
});
return;
}
// 优先调用接口获取技能数据
isLoadingSkillTags.value = true;
try {
const response = await getJobSkillDetail({
jobName: props.currentJobName
});
const skillList = Array.isArray(response?.data) ? response.data : [];
const apiSkills = extractSkillNames(skillList);
// 如果接口返回了技能数据,使用接口数据
if (apiSkills.length > 0) {
skillTags.value = apiSkills;
} else {
// 如果接口没有返回技能数据,从缓存中读取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const cachedSkills = cachedUserInfo?.resume?.skillList ??
cachedUserInfo?.skillList ??
cachedUserInfo?.appSkillsList ??
[];
if (Array.isArray(cachedSkills) && cachedSkills.length > 0) {
skillTags.value = extractSkillNames(cachedSkills);
} else {
skillTags.value = [];
}
}
// 通知父组件技能数据已更新
emit('skills-updated', {
currentJobSkills: skillTags.value,
recommendedJobs: recommendedJobs.value
});
} catch (error) {
// 接口调用失败时,从缓存中读取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const cachedSkills = cachedUserInfo?.resume?.skillList ??
cachedUserInfo?.skillList ??
cachedUserInfo?.appSkillsList ??
[];
if (Array.isArray(cachedSkills) && cachedSkills.length > 0) {
skillTags.value = extractSkillNames(cachedSkills);
} else {
skillTags.value = [];
}
emit('skills-updated', {
currentJobSkills: skillTags.value,
recommendedJobs: recommendedJobs.value
});
} finally {
isLoadingSkillTags.value = false;
}
}
// 获取推荐职位列表
async function fetchRecommendedJobs() {
isLoadingRecommend.value = true;
try {
const response = await recommendJob({
jobName: props.currentJobName
});
const list = Array.isArray(response?.data) ? response.data : [];
recommendedJobs.value = list.map((item, index) => {
const skillList = Array.isArray(item?.skillList) ? item.skillList : [];
const skillNames = extractSkillNames(skillList);
return {
id: item?.jobId ?? index,
jobId: item?.jobId ?? null,
title: item?.jobName || `推荐职位${index + 1}`,
jobName: item?.jobName || '',
skills: skillNames,
rawSkills: skillList
};
});
// 通知父组件推荐职位数据已更新
emit('skills-updated', {
currentJobSkills: skillTags.value,
recommendedJobs: recommendedJobs.value
});
} catch (error) {
recommendedJobs.value = [];
emit('skills-updated', {
currentJobSkills: skillTags.value,
recommendedJobs: []
});
} finally {
isLoadingRecommend.value = false;
}
}
// 组件挂载时检查并调用
onMounted(() => {
setTimeout(() => {
if (props.currentJobName) {
fetchCurrentJobSkills();
fetchRecommendedJobs();
}
}, 100);
});
// 监听 props 变化,自动获取推荐职位和技能标签
watch(
() => [props.currentJobId, props.currentJobName],
() => {
fetchCurrentJobSkills();
fetchRecommendedJobs();
},
{ immediate: true }
);
// 事件处理
function handleJobSearch(job) {
// TODO: 实现职位搜索跳转
}
function handleJobCardClick(job) {
emit('job-card-click', job);
}
</script>
<style lang="scss" scoped>
.career-recommend {
padding: 10rpx 28rpx 20rpx;
}
.info-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #000000;
margin-bottom: 24rpx;
}
.card-content {
display: flex;
justify-content: space-between;
align-items: center;
.label {
font-size: 24rpx;
line-height: 34rpx;
color: rgba(154, 154, 154, 1);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
}
.value {
font-size: 32rpx;
line-height: 46rpx;
color: rgb(16, 16, 16);
font-weight: 600;
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.skill-tag {
background-color: #EAEFFE;
color: rgba(44, 101, 247, 1);
padding: 8rpx 20rpx;
border-radius: 0;
font-size: 24rpx;
white-space: nowrap;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #000000;
margin-bottom: 20rpx;
margin-top: 8rpx;
}
.job-item-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
border-left: 6rpx solid #409EFF;
position: relative;
cursor: pointer;
}
.search-btn {
position: absolute;
top: 16rpx;
right: 16rpx;
background-color: #FFDAB9;
color: #FF6347;
font-size: 20rpx;
padding: 6rpx 14rpx;
border-radius: 6rpx;
border: 1rpx solid #FFA07A;
white-space: nowrap;
z-index: 10;
line-height: 1.2;
}
.job-header {
margin-bottom: 16rpx;
padding-right: 100rpx;
}
.job-title {
font-size: 32rpx;
font-weight: 600;
color: rgb(16, 16, 16);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
line-height: 46rpx;
width: 100%;
}
.job-skills {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.job-skill-tag {
width: 130rpx;
height: 36rpx;
line-height: 34rpx;
background-color: rgba(49, 100, 239, 0.1);
color: rgba(44, 101, 247, 1);
font-size: 24rpx;
text-align: center;
font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
border-radius: 8rpx;
white-space: nowrap;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.recommend-count {
margin-left: 12rpx;
font-size: 22rpx;
color: #666666;
font-weight: 400;
}
.empty-text {
font-size: 24rpx;
color: #999999;
line-height: 34rpx;
}
button::after {
border: none;
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<view class="page-header">
<view class="header-top">
<view class="title-section">
<text class="main-title">职业规划推荐指导平台</text>
<text class="sub-title">智能匹配·精准推荐·职业发展</text>
</view>
<view class="header-icons">
<view class="icon-dot" @click="handleMenuClick">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
<view class="icon-single-dot" @click="handleMoreClick"></view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar" @click.stop="handleSearchClick" @tap.stop="handleSearchClick">
<uni-icons class="search-icon" type="search" size="18" color="#999999"></uni-icons>
<text class="search-placeholder">职位名称薪资要求等</text>
</view>
<!-- Tab导航 -->
<view class="tab-nav-wrapper">
<view class="tab-nav">
<view
class="tab-item"
:class="{ 'active': activeTab === 0 }"
@click.stop="handleTabClick(0)"
@tap.stop="handleTabClick(0)"
>
职业推荐
</view>
<view
class="tab-item"
:class="{ 'active': activeTab === 1 }"
@click.stop="handleTabClick(1)"
@tap.stop="handleTabClick(1)"
>
职业路径
</view>
<view
class="tab-item"
:class="{ 'active': activeTab === 2 }"
@click.stop="handleTabClick(2)"
@tap.stop="handleTabClick(2)"
>
技能发展
</view>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
activeTab: {
type: Number,
default: 0
}
});
const emit = defineEmits(['tab-change', 'search-click', 'menu-click', 'more-click']);
function handleTabClick(index) {
emit('tab-change', index);
}
function handleSearchClick() {
emit('search-click');
}
function handleMenuClick() {
emit('menu-click');
}
function handleMoreClick() {
emit('more-click');
}
</script>
<style lang="scss" scoped>
.page-header {
padding: 40rpx 28rpx 0;
flex-shrink: 0;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15rpx;
}
.title-section {
flex: 1;
display: flex;
flex-direction: column;
}
.main-title {
font-size: 40rpx;
font-weight: 600;
color: #000000;
margin-bottom: 8rpx;
line-height: 1.2;
}
.sub-title {
font-size: 24rpx;
line-height: 28rpx;
color: rgba(51, 51, 51, 1);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
}
.header-icons {
display: flex;
gap: 16rpx;
align-items: center;
}
.icon-dot {
width: 44rpx;
height: 44rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4rpx;
.dot {
width: 6rpx;
height: 6rpx;
background-color: #666666;
border-radius: 50%;
}
}
.icon-single-dot {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background-color: #666666;
}
.search-bar {
background-color: #EBF1FF;
border-radius: 40rpx;
padding: 0 28rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
height: 88rpx;
box-shadow: none;
position: relative;
z-index: 10;
pointer-events: auto;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
}
.search-icon {
flex-shrink: 0;
}
.search-placeholder {
flex: 1;
font-size: 28rpx;
color: #999999;
text-align: center;
}
.tab-nav-wrapper {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 6rpx 20rpx;
margin-bottom: 10rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
position: relative;
z-index: 10;
pointer-events: auto;
}
.tab-nav {
display: flex;
gap: 8rpx;
}
.tab-item {
flex: 1;
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 28rpx;
color: #000000;
background-color: transparent;
text-align: center;
transition: all 0.3s;
position: relative;
pointer-events: auto;
-webkit-tap-highlight-color: transparent;
z-index: 10;
}
.tab-item.active {
background: linear-gradient(135deg, #9974FD 0%, #286BFA 100%);
color: #FFFFFF;
font-weight: 500;
border-radius: 20rpx;
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<uni-popup
ref="popupRef"
type="center"
borderRadius="10px 10px 10px 10px"
background-color="#FFFFFF"
:mask-click="false"
>
<view class="remind-popup-content">
<view class="remind-title">提醒</view>
<view class="remind-message">请先完善您的个人信息:</view>
<view class="remind-list">
<view
class="remind-item"
v-for="(item, index) in remindList"
:key="index"
>
{{ index + 1 }}{{ item }}
</view>
</view>
<view class="remind-btns">
<button class="remind-btn cancel-btn" @click="handleCancel">取消</button>
<button class="remind-btn confirm-btn" @click="handleConfirm">确认</button>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, defineExpose } from 'vue';
const props = defineProps({
remindList: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['cancel', 'confirm']);
const popupRef = ref(null);
// 打开弹窗
function open() {
if (popupRef.value) {
try {
popupRef.value.open();
} catch (error) {
// 静默处理错误
}
}
}
// 关闭弹窗
function close() {
popupRef.value?.close();
}
// 取消按钮
function handleCancel() {
emit('cancel');
}
// 确认按钮
function handleConfirm() {
emit('confirm');
}
defineExpose({
open,
close
});
</script>
<style lang="scss" scoped>
.remind-popup-content {
padding: 40rpx;
width: 630rpx;
background-color: #FFFFFF;
border-radius: 20rpx;
box-sizing: border-box;
}
.remind-title {
font-weight: 600;
font-size: 32rpx;
color: #000000;
margin-bottom: 24rpx;
}
.remind-message {
font-size: 28rpx;
color: #000000;
margin-bottom: 20rpx;
line-height: 40rpx;
}
.remind-list {
margin-bottom: 32rpx;
min-height: 40rpx;
}
.remind-item {
font-size: 28rpx;
color: #000000;
line-height: 40rpx;
margin-bottom: 8rpx;
}
.remind-btns {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.remind-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 8rpx;
font-size: 28rpx;
padding: 0;
box-sizing: border-box;
font-weight: 400;
}
.cancel-btn {
background-color: #FFFFFF;
color: #666666;
border: 1rpx solid #E5E7EB;
}
.confirm-btn {
background-color: #256BFA;
color: #FFFFFF;
border: none;
}
button::after {
border: none;
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<uni-popup
ref="popupRef"
type="bottom"
:borderRadius="'20rpx 20rpx 0 0'"
background-color="#FFFFFF"
maskBackgroundColor="rgba(255, 255, 255, 0.6)"
:isMaskClick="true"
@maskClick="handleClose"
>
<view class="skill-popup-content">
<!-- 头部标题和关闭按钮 -->
<view class="popup-header">
<text class="popup-title">{{ jobTitle }}</text>
<view class="close-btn" @click="handleClose">
<uni-icons type="closeempty" size="20" color="#000000"></uni-icons>
</view>
</view>
<!-- 内容区域 -->
<scroll-view scroll-y class="popup-body" :show-scrollbar="false">
<view
class="skill-section"
v-for="(section, sIndex) in skillSections"
:key="sIndex"
>
<view class="section-header">
<view class="section-icon"></view>
<text class="section-title">{{ section.title }}</text>
</view>
<view class="skill-list">
<view
class="skill-item"
v-for="(skill, index) in section.skills"
:key="index"
>
<text class="skill-name">{{ skill.name }}</text>
<view class="skill-dots">
<view
class="dot"
:class="{ 'active': i - 1 < skill.level }"
v-for="i in 6"
:key="i"
></view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, computed, defineExpose } from 'vue';
const props = defineProps({
jobTitle: {
type: String,
default: ''
},
possessedSkills: {
type: Array,
default: () => []
},
improvementSkills: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['close']);
const popupRef = ref(null);
// 技能区块数据
const skillSections = computed(() => [
{ title: '已具备技能', skills: props.possessedSkills },
{ title: '需要提升的技能', skills: props.improvementSkills }
]);
function open() {
popupRef.value?.open('bottom');
}
function close() {
popupRef.value?.close('bottom');
}
function handleClose() {
close();
emit('close');
}
defineExpose({ open, close });
</script>
<style lang="scss" scoped>
.skill-popup-content {
height: calc(100vh - 330rpx);
background-color: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 28rpx;
border-bottom: 1rpx solid #F0F0F0;
flex-shrink: 0;
position: relative;
}
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #000000;
flex: 1;
}
.close-btn {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #F5F5F5;
position: absolute;
right: 28rpx;
top: 50%;
transform: translateY(-50%);
}
.popup-body {
flex: 1;
padding: 0 28rpx 28rpx;
overflow-y: auto;
box-sizing: border-box;
}
.skill-section {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
gap: 12rpx;
}
.section-icon {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: linear-gradient(135deg, #9974FD 0%, #286BFA 100%);
flex-shrink: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #286BFA;
}
.skill-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.skill-item {
background-color: #F5F5F5;
border-radius: 12rpx;
padding: 20rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 64rpx;
box-sizing: border-box;
}
.skill-name {
font-size: 28rpx;
color: #000000;
flex: 1;
margin-right: 20rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.skill-dots {
display: flex;
gap: 8rpx;
align-items: center;
flex-shrink: 0;
width: 112rpx;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #E0E0E0;
&.active {
background-color: #286BFA;
}
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<view class="skill-development">
<view class="content-section">
<view class="section-title">
<uni-icons type="person-filled" size="18" color="#000000"></uni-icons>
<text class="title-text">技能发展路径</text>
</view>
<view class="intro-text">
基于您的当前职业和目标职业,以下是您需要重点发展的技能:
</view>
<view class="skill-list">
<view
class="skill-item"
v-for="(skill, index) in skillList"
:key="index"
@click="handleSkillItemClick(skill)"
>
<view class="skill-header">
<text class="skill-name">{{ skill.name }}</text>
<text class="skill-weight">权重: {{ skill.weight }}</text>
</view>
<view class="skill-tags">
<view
class="skill-tag"
v-for="(tag, tagIndex) in skill.tags"
:key="tagIndex"
>
{{ tag }}
</view>
</view>
</view>
</view>
</view>
<!-- 技能权重弹出层 -->
<SkillWeightPopup
ref="skillWeightPopup"
:skill-name="selectedSkillName"
:weight="selectedSkillWeight"
:current-level="selectedSkillLevel"
@close="handlePopupClose"
/>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import SkillWeightPopup from './SkillWeightPopup.vue';
const props = defineProps({
// 来自职业推荐 tab 的数据
currentJobSkills: {
type: Array,
default: () => []
},
recommendedJobs: {
type: Array,
default: () => []
},
// 来自职业路径 tab 的数据
pathData: {
type: Object,
default: () => ({
start: { title: '', skills: [] },
steps: [],
end: { title: '', skills: [] }
})
},
targetCareer: {
type: String,
default: ''
}
});
// 综合技能列表(基于前两个 tab 的数据)
const skillList = computed(() => {
const skillMap = new Map();
// 1. 收集当前职位技能(已有技能,权重较低)
props.currentJobSkills.forEach(skill => {
if (skill && skill.trim()) {
const existing = skillMap.get(skill) || { name: skill, weight: 0, tags: [], currentLevel: 0 };
existing.weight = Math.max(existing.weight, 0.3); // 已有技能权重较低
existing.tags.push('当前技能');
skillMap.set(skill, existing);
}
});
// 2. 收集推荐职位的技能(需要发展的技能,权重较高)
props.recommendedJobs.forEach(job => {
if (Array.isArray(job.skills)) {
job.skills.forEach(skill => {
if (skill && skill.trim()) {
const existing = skillMap.get(skill) || { name: skill, weight: 0, tags: [], currentLevel: 0 };
existing.weight = Math.max(existing.weight, 0.7); // 推荐职位技能权重较高
if (!existing.tags.includes('推荐技能')) {
existing.tags.push('推荐技能');
}
skillMap.set(skill, existing);
}
});
}
});
// 3. 收集职业路径中的技能(发展路径技能,权重最高)
const pathSkills = [];
if (props.pathData.start && Array.isArray(props.pathData.start.skills)) {
pathSkills.push(...props.pathData.start.skills);
}
if (Array.isArray(props.pathData.steps)) {
props.pathData.steps.forEach(step => {
if (Array.isArray(step.skills)) {
pathSkills.push(...step.skills);
}
});
}
if (props.pathData.end && Array.isArray(props.pathData.end.skills)) {
pathSkills.push(...props.pathData.end.skills);
}
pathSkills.forEach(skill => {
if (skill && skill.trim()) {
const existing = skillMap.get(skill) || { name: skill, weight: 0, tags: [], currentLevel: 0 };
existing.weight = Math.max(existing.weight, 0.9); // 路径技能权重最高
if (!existing.tags.includes('路径技能')) {
existing.tags.push('路径技能');
}
skillMap.set(skill, existing);
}
});
// 转换为数组并按权重排序
const result = Array.from(skillMap.values())
.map(item => ({
name: item.name,
weight: item.weight.toFixed(2),
tags: [...new Set(item.tags)], // 去重
currentLevel: item.currentLevel || Math.floor(item.weight * 5) + 1 // 根据权重计算等级
}))
.sort((a, b) => parseFloat(b.weight) - parseFloat(a.weight)); // 按权重降序
return result;
});
// 弹出层引用
const skillWeightPopup = ref(null);
// 选中的技能信息
const selectedSkillName = ref('');
const selectedSkillWeight = ref('');
const selectedSkillLevel = ref(0);
// 处理技能项点击
function handleSkillItemClick(skill) {
selectedSkillName.value = skill.name || '';
selectedSkillWeight.value = skill.weight || '0';
selectedSkillLevel.value = skill.currentLevel || 0;
skillWeightPopup.value?.open();
}
// 处理弹出层关闭
function handlePopupClose() {
// 可以在这里处理关闭后的逻辑
}
</script>
<style lang="scss" scoped>
.skill-development {
padding: 10rpx 0 20rpx;
background-color: #EBF4FF;
min-height:95%;
}
.content-section {
background-color: #EBF4FF;
padding: 28rpx;
box-sizing: border-box;
overflow: visible;
}
.section-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
margin-top: -20rpx;
}
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #167CF1;
}
.intro-text {
font-size: 24rpx;
line-height: 34rpx;
color: rgba(154, 154, 154, 1);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
margin-bottom: 90rpx;
width: 672rpx;
}
.skill-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.skill-item {
width: 100%;
max-width: 796rpx;
min-height: 162rpx;
line-height: 40rpx;
border-radius: 20rpx;
background-color: rgba(239, 239, 239, 1);
color: rgba(16, 16, 16, 1);
font-size: 28rpx;
text-align: center;
padding: 24rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
overflow: visible;
position: relative;
}
.skill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
width: 100%;
box-sizing: border-box;
}
.skill-name {
font-size: 32rpx;
line-height: 46rpx;
color: rgb(16, 16, 16);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.skill-weight {
font-size: 24rpx;
line-height: 34rpx;
border-radius: 10rpx;
background-color: rgba(49, 100, 239, 0.1);
color: rgba(44, 101, 247, 1);
text-align: center;
padding: 6rpx 12rpx;
white-space: nowrap;
flex-shrink: 0;
margin-left: 16rpx;
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.skill-tag {
background-color: rgba(49, 100, 239, 0.1);
color: rgba(44, 101, 247, 1);
padding: 6rpx 12rpx;
border-radius: 8rpx;
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<uni-popup
ref="popupRef"
type="bottom"
:borderRadius="'20rpx 20rpx 0 0'"
background-color="#FFFFFF"
maskBackgroundColor="rgba(255, 255, 255, 0.6)"
:isMaskClick="true"
@maskClick="handleClose"
>
<view class="skill-weight-popup-content">
<!-- 头部标题和关闭按钮 -->
<view class="popup-header">
<text class="popup-title">{{ skillName }}</text>
<view class="close-btn" @click="handleClose">
<uni-icons type="closeempty" size="20" color="#000000"></uni-icons>
</view>
</view>
<!-- 内容区域 -->
<scroll-view scroll-y class="popup-body" :show-scrollbar="false">
<!-- 技能权重 -->
<view class="info-section">
<view class="section-header">
<view class="section-icon"></view>
<text class="section-title">技能权重</text>
</view>
<view class="info-item">
<text class="info-label">{{ skillName }}</text>
<text class="info-value">{{ weight }}</text>
</view>
</view>
<!-- 当前水平 -->
<view class="info-section">
<view class="section-header">
<view class="section-icon"></view>
<text class="section-title">当前水平</text>
</view>
<view class="info-item">
<text class="info-label">能力等级</text>
<view class="level-dots">
<view
class="dot"
:class="{ 'active': i - 1 < currentLevel }"
v-for="i in 5"
:key="i"
></view>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, defineExpose } from 'vue';
const props = defineProps({
skillName: {
type: String,
default: ''
},
weight: {
type: String,
default: '0'
},
currentLevel: {
type: Number,
default: 0
}
});
const emit = defineEmits(['close']);
const popupRef = ref(null);
function open() {
popupRef.value?.open('bottom');
}
function close() {
popupRef.value?.close('bottom');
}
function handleClose() {
close();
emit('close');
}
defineExpose({ open, close });
</script>
<style lang="scss" scoped>
.skill-weight-popup-content {
height: calc(100vh - 330rpx);
background-color: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 28rpx;
border-bottom: 1rpx solid #F0F0F0;
flex-shrink: 0;
position: relative;
}
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #000000;
flex: 1;
}
.close-btn {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #F5F5F5;
position: absolute;
right: 28rpx;
top: 50%;
transform: translateY(-50%);
}
.popup-body {
flex: 1;
padding: 0 28rpx 28rpx;
overflow-y: auto;
box-sizing: border-box;
}
.info-section {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
gap: 12rpx;
}
.section-icon {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: linear-gradient(135deg, #9974FD 0%, #286BFA 100%);
flex-shrink: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #286BFA;
}
.info-item {
background-color: #F5F5F5;
border-radius: 12rpx;
padding: 20rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.info-label {
font-size: 28rpx;
color: #000000;
flex: 1;
}
.info-value {
font-size: 28rpx;
color: #286BFA;
font-weight: 500;
}
.level-dots {
display: flex;
gap: 8rpx;
align-items: center;
flex-shrink: 0;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #E0E0E0;
&.active {
background-color: #286BFA;
}
}
</style>