= 职业规划推荐

This commit is contained in:
2026-01-21 14:26:34 +08:00
parent a5e30ac7f5
commit 3f7664f017
19 changed files with 1543 additions and 306 deletions

View File

@@ -1,33 +0,0 @@
/*
* @Date: 2025-11-12
* @Description: 职业推荐相关接口
*/
import request from '@/utilsRc/request'
function createFormData(payload = {}) {
if (typeof FormData !== 'undefined') {
const formData = new FormData()
Object.keys(payload).forEach(key => {
const value = payload[key]
if (value !== undefined && value !== null && value !== '') {
formData.append(key, value)
}
})
return formData
}
return payload
}
export function recommendJob(data) {
const params = {};
if (data?.jobName !== undefined && data?.jobName !== null && data?.jobName !== '') {
params.jobName = String(data.jobName);
}
return request({
url: '/job/recommendJobByJobName',
method: 'get',
params: params,
baseUrlType: 'zytp'
})
}

View File

@@ -1,55 +0,0 @@
/*
* @Date: 2025-11-12
* @Description: 职业技能相关接口
*/
import request from '@/utilsRc/request'
export function getJobSkillDetail(params) {
return request({
url: '/jobSkillDet/getJobSkillDet',
method: 'get',
params,
baseUrlType: 'zytp'
})
}
// 获取技能权重
export function getJobSkillWeight(params) {
return request({
url: '/jobSkillDet/getJobSkillWeight',
method: 'get',
params,
baseUrlType: 'zytp'
})
}
// 暂未使用 - 如果需要在 CareerPath.vue 中点击路径职位查看详细技能信息时使用
// 使用场景:获取职业路径中某个职位的详细技能信息(包含技能分数、类型等)
// export function getJobPathSkill(data) {
// let formData
// if (typeof FormData !== 'undefined') {
// formData = new FormData()
// if (data?.pathId !== undefined && data?.pathId !== null) {
// formData.append('pathId', data.pathId)
// }
// if (data?.currentJobName !== undefined && data?.currentJobName !== null) {
// formData.append('currentJobName', data.currentJobName)
// }
// } else {
// formData = {
// pathId: data?.pathId ?? '',
// currentJobName: data?.currentJobName ?? ''
// }
// }
// return request({
// url: '/jobSkillDet/getJobPathSkill',
// method: 'post',
// data: formData,
// baseUrlType: 'zytp',
// header: {
// 'content-type': 'multipart/form-data'
// }
// })
// }

View File

@@ -0,0 +1,37 @@
/*
* @Date: 2024-09-25 11:14:29
* @LastEditors: shirlwang
* @LastEditTime: 2025-12-23 17:40:11
* @Description: 职业路径相关接口
*/
import request from '@/utilsRc/request'
// 获取当前职位
export function getCurrentPosition(query) {
return request({
url: '/jobPath/getJob',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}
// 获取路径列表
export function getPath(query) {
return request({
url: '/jobPath/getJobPathList',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}
// 获取路径详情
export function getPathDetail(query) {
return request({
url: '/jobPath/getJobPathById',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}

View File

@@ -0,0 +1,37 @@
/*
* @Date: 2024-09-25 11:14:29
* @LastEditors: shirlwang
* @LastEditTime: 2025-12-23 17:40:11
* @Description: 职业推荐相关接口
*/
import request from '@/utilsRc/request'
// 获取职业列表
export function getProfessions(query) {
return request({
url: '/jobSimilarity/getJob',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}
// 获取技能标签
export function getSkillTags(query) {
return request({
url: '/jobSkillDet/getJobSkill',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}
// 获取推荐职业
export function getRecommend(query) {
return request({
url: '/jobSimilarity/recommendJobByJobName',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}

3
apiRc/service/index.js Normal file
View File

@@ -0,0 +1,3 @@
export * from './career-recommendation.js';
export * from './career-path.js';
export * from './skill-development.js';

View File

@@ -76,11 +76,16 @@ export function getAddedJobs(params) {
}) })
} }
// // 获取推荐岗位 export function recommendJob(data) {
// export function getAddedJobs(params) { const params = {};
// return request({ if (data?.jobName !== undefined && data?.jobName !== null && data?.jobName !== '') {
// url: '/personnel/personBaseInfo/postRecommend', params.jobName = String(data.jobName);
// method: 'get', }
// params,
// }) return request({
// } url: '/job/recommendJobByJobName',
method: 'get',
params: params,
baseUrlType: 'zytp'
})
}

24
apiRc/service/jobSkill.js Normal file
View File

@@ -0,0 +1,24 @@
/*
* @Date: 2025-11-12
* @Description: 职业技能相关接口
*/
import request from '@/utilsRc/request'
export function getJobSkillDetail(params) {
return request({
url: '/jobSkillDet/getJobSkillDet',
method: 'get',
params,
baseUrlType: 'zytp'
})
}
// 获取技能权重
export function getJobSkillWeight(params) {
return request({
url: '/jobSkillDet/getJobSkillWeight',
method: 'get',
params,
baseUrlType: 'zytp'
})
}

View File

@@ -0,0 +1,17 @@
/*
* @Date: 2024-09-25 11:14:29
* @LastEditors: shirlwang
* @LastEditTime: 2025-12-23 17:40:11
* @Description: 技能发展相关接口
*/
import request from '@/utilsRc/request'
// 获取技能信息
export function getSkill(query) {
return request({
url: '/jobSkillDet/getJobSkillWeight',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}

View File

@@ -0,0 +1,590 @@
<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-name="currentJobName"
@path-data-updated="handlePathDataUpdated"
/>
</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, onShow } from '@dcloudio/uni-app';
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;
// 保存缺失信息的标识
const missingInfo = ref({
hasJobInfo: false,
hasSkills: 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;
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
if (!idCard || idCard === null || idCard === '') {
idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
}
const hasIdCard = idCard !== null && idCard !== undefined && idCard !== '';
// 检查职位信息:优先从 jobTitles 数组获取
const jobTitles = Array.isArray(userInfo?.jobTitles) ? userInfo.jobTitles : [];
const hasJobTitle = jobTitles.length > 0;
// 如果 jobTitles 为空,尝试从其他字段获取
let jobName = '';
if (!hasJobTitle) {
jobName = userInfo?.jobName ??
userInfo?.currentJobName ??
userInfo?.resume?.jobName ??
userInfo?.resume?.currentJobName ??
'';
}
const hasJobInfo = hasJobTitle || (jobName && jobName.trim() !== '');
// 检查技能标签:从 appSkillsList 获取
const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : [];
// 检查是否有有效的技能name 或 nameStr 不为空)
const hasSkills = appSkillsList.some(skill => {
const skillName = skill?.name || skill?.nameStr;
return skillName && skillName.trim() !== '';
});
// 保存缺失信息标识(只保存职位信息和技能标签,身份证信息跳转到个人信息页面)
missingInfo.value.hasJobInfo = hasJobInfo;
missingInfo.value.hasSkills = hasSkills;
// 判断信息是否完整idCard、职位信息、技能标签都必须有
const isComplete = hasIdCard && hasJobInfo && hasSkills;
if (!isComplete) {
// 收集缺失的信息提示
const missingItems = [];
if (!hasIdCard) {
missingItems.push('身份证信息');
}
if (!hasJobInfo) {
missingItems.push('职位信息');
}
if (!hasSkills) {
missingItems.push('技能标签');
}
remindList.value = [`请完善${missingItems.join('、')}`];
} else {
// 信息完整,设置职位信息
if (hasJobTitle) {
currentJobName.value = jobTitles[0];
} else {
currentJobName.value = jobName;
currentJobId.value = userInfo?.jobId ??
userInfo?.currentJobId ??
userInfo?.resume?.jobId ??
userInfo?.resume?.currentJobId ??
null;
}
// 信息完整,直接显示页面内容
showContent.value = true;
return;
}
setTimeout(() => {
openRemindPopup();
}, 500);
} catch (error) {
// 接口调用失败时,使用缓存作为降级方案
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
// 检查 idCard
const idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
const hasIdCard = idCard !== null && idCard !== undefined && idCard !== '';
// 检查职位信息
const cachedJobTitles = Array.isArray(cachedUserInfo?.jobTitles) ? cachedUserInfo.jobTitles : [];
const hasJobTitle = cachedJobTitles.length > 0;
let jobName = '';
if (!hasJobTitle) {
jobName = cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ??
'';
}
const hasJobInfo = hasJobTitle || (jobName && jobName.trim() !== '');
// 检查技能标签
const cachedAppSkillsList = Array.isArray(cachedUserInfo?.appSkillsList) ? cachedUserInfo.appSkillsList : [];
const hasSkills = cachedAppSkillsList.some(skill => {
const skillName = skill?.name || skill?.nameStr;
return skillName && skillName.trim() !== '';
});
// 保存缺失信息标识
missingInfo.value.hasJobInfo = hasJobInfo;
missingInfo.value.hasSkills = hasSkills;
// 判断信息是否完整idCard、职位信息、技能标签都必须有
const isComplete = hasIdCard && hasJobInfo && hasSkills;
if (!isComplete) {
// 收集缺失的信息提示
const missingItems = [];
if (!hasIdCard) {
missingItems.push('身份证信息');
}
if (!hasJobInfo) {
missingItems.push('职位信息');
}
if (!hasSkills) {
missingItems.push('技能标签');
}
remindList.value = [`请完善${missingItems.join('、')}`];
} else {
// 信息完整,设置职位信息
if (hasJobTitle) {
currentJobName.value = cachedJobTitles[0];
} else {
currentJobName.value = jobName;
currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
}
// 信息完整,直接显示页面内容
showContent.value = true;
return;
}
setTimeout(() => {
openRemindPopup();
}, 500);
}
}
// 取消按钮
function handleCancel() {
remindPopup.value?.close();
navBack();
}
// 确认按钮
async function handleConfirm() {
remindPopup.value?.close();
const { hasJobInfo, hasSkills } = missingInfo.value;
// 如果同时缺少职位信息和技能标签:先跳转到职位信息页面,并传递参数表示完成后需要继续跳转到技能页面
if (!hasJobInfo && !hasSkills) {
// 跳转到职位信息页面,传递参数表示完成后需要继续跳转到技能页面
navTo('/packageA/pages/jobExpect/jobExpect?needSkill=true');
}
// 如果只缺少技能标签:直接跳转到技能页面(个人信息页面的技能部分)
else if (!hasSkills) {
navTo('/packageA/pages/personalInfo/personalInfo');
}
// 如果只缺少职位信息:直接跳转到职位信息页面
else if (!hasJobInfo) {
navTo('/packageA/pages/jobExpect/jobExpect');
}
// 如果只缺少身份证信息:跳转到个人信息页面
else {
navTo('/packageA/pages/personalInfo/personalInfo');
}
}
// 切换tab
function switchTab(index) {
activeTab.value = index;
if (index === 0 && !currentJobId.value) {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
// 优先从缓存中的 jobTitles 数组获取职位信息(取第一个)
const cachedJobTitles = Array.isArray(cachedUserInfo?.jobTitles) ? cachedUserInfo.jobTitles : [];
let newJobName = '';
if (cachedJobTitles.length > 0) {
newJobName = cachedJobTitles[0];
} else {
// 如果缓存中没有 jobTitles从其他字段获取
newJobName = currentJobName.value ||
(cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ??
'市场专员');
}
const newJobId = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
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
});
try {
// 从 appUserInfo 接口获取技能数据
const response = await appUserInfo();
const userInfo = response?.data || {};
const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : [];
// 将 appSkillsList 转换为 splitSkillListByScore 需要的格式
const skillList = appSkillsList.map(item => ({
skillName: item?.name || item?.nameStr || '',
skillScore: item?.levels || item?.levelStr || 0
})).filter(item => item.skillName);
const { possessed, improvement } = splitSkillListByScore(skillList);
if (possessed.length === 0 && improvement.length === 0) {
// 如果 appUserInfo 中没有技能数据,尝试使用推荐职位数据中的技能信息
const fallbackSkills = Array.isArray(job?.rawSkills) ? job.rawSkills : [];
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) {
// 接口调用失败,尝试使用推荐职位数据中的技能信息
const fallbackSkills = Array.isArray(job?.rawSkills) ? job.rawSkills : [];
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();
});
onShow(() => {
// 返回本页后,如果之前因为信息缺失未展示内容,则重新检查
if (!showContent.value) {
hasCheckedRemindInfo = false;
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

@@ -1,67 +1,4 @@
<template> <!--suppress HtmlUnknownTag, NpmUsedModulesInstalled, JSFileReferences -->
<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-name="currentJobName"
@path-data-updated="handlePathDataUpdated"
/>
</scroll-view>
</view>
<!-- 底部导航栏 -->
<view class="tabbar-wrapper" v-if="showContent">
<CustomTabBar :currentPage="0" />
</view>
</view>
</template>
<script setup> <script setup>
import { ref, inject, nextTick, onMounted } from 'vue'; import { ref, inject, nextTick, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app'; import { onLoad, onShow } from '@dcloudio/uni-app';
@@ -153,13 +90,13 @@ async function getRemindInfo() {
if (hasCheckedRemindInfo) { if (hasCheckedRemindInfo) {
return; return;
} }
hasCheckedRemindInfo = true; hasCheckedRemindInfo = true;
try { try {
const response = await appUserInfo(); const response = await appUserInfo();
const userInfo = response?.data || {}; const userInfo = response?.data || {};
// 检查 idCard身份证- 必须项 // 检查 idCard身份证- 必须项
let idCard = userInfo?.resume?.idCard ?? userInfo?.idCard ?? null; let idCard = userInfo?.resume?.idCard ?? userInfo?.idCard ?? null;
const cachedUserInfo = uni.getStorageSync('userInfo') || {}; const cachedUserInfo = uni.getStorageSync('userInfo') || {};
@@ -167,22 +104,22 @@ async function getRemindInfo() {
idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null; idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
} }
const hasIdCard = idCard !== null && idCard !== undefined && idCard !== ''; const hasIdCard = idCard !== null && idCard !== undefined && idCard !== '';
// 检查职位信息:优先从 jobTitles 数组获取 // 检查职位信息:优先从 jobTitles 数组获取
const jobTitles = Array.isArray(userInfo?.jobTitles) ? userInfo.jobTitles : []; const jobTitles = Array.isArray(userInfo?.jobTitles) ? userInfo.jobTitles : [];
const hasJobTitle = jobTitles.length > 0; const hasJobTitle = jobTitles.length > 0;
// 如果 jobTitles 为空,尝试从其他字段获取 // 如果 jobTitles 为空,尝试从其他字段获取
let jobName = ''; let jobName = '';
if (!hasJobTitle) { if (!hasJobTitle) {
jobName = userInfo?.jobName ?? jobName = userInfo?.jobName ??
userInfo?.currentJobName ?? userInfo?.currentJobName ??
userInfo?.resume?.jobName ?? userInfo?.resume?.jobName ??
userInfo?.resume?.currentJobName ?? userInfo?.resume?.currentJobName ??
''; '';
} }
const hasJobInfo = hasJobTitle || (jobName && jobName.trim() !== ''); const hasJobInfo = hasJobTitle || (jobName && jobName.trim() !== '');
// 检查技能标签:从 appSkillsList 获取 // 检查技能标签:从 appSkillsList 获取
const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : []; const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : [];
// 检查是否有有效的技能name 或 nameStr 不为空) // 检查是否有有效的技能name 或 nameStr 不为空)
@@ -190,14 +127,14 @@ async function getRemindInfo() {
const skillName = skill?.name || skill?.nameStr; const skillName = skill?.name || skill?.nameStr;
return skillName && skillName.trim() !== ''; return skillName && skillName.trim() !== '';
}); });
// 保存缺失信息标识(只保存职位信息和技能标签,身份证信息跳转到个人信息页面) // 保存缺失信息标识(只保存职位信息和技能标签,身份证信息跳转到个人信息页面)
missingInfo.value.hasJobInfo = hasJobInfo; missingInfo.value.hasJobInfo = hasJobInfo;
missingInfo.value.hasSkills = hasSkills; missingInfo.value.hasSkills = hasSkills;
// 判断信息是否完整idCard、职位信息、技能标签都必须有 // 判断信息是否完整idCard、职位信息、技能标签都必须有
const isComplete = hasIdCard && hasJobInfo && hasSkills; const isComplete = hasIdCard && hasJobInfo && hasSkills;
if (!isComplete) { if (!isComplete) {
// 收集缺失的信息提示 // 收集缺失的信息提示
const missingItems = []; const missingItems = [];
@@ -217,55 +154,55 @@ async function getRemindInfo() {
currentJobName.value = jobTitles[0]; currentJobName.value = jobTitles[0];
} else { } else {
currentJobName.value = jobName; currentJobName.value = jobName;
currentJobId.value = userInfo?.jobId ?? currentJobId.value = userInfo?.jobId ??
userInfo?.currentJobId ?? userInfo?.currentJobId ??
userInfo?.resume?.jobId ?? userInfo?.resume?.jobId ??
userInfo?.resume?.currentJobId ?? userInfo?.resume?.currentJobId ??
null; null;
} }
// 信息完整,直接显示页面内容 // 信息完整,直接显示页面内容
showContent.value = true; showContent.value = true;
return; return;
} }
setTimeout(() => { setTimeout(() => {
openRemindPopup(); openRemindPopup();
}, 500); }, 500);
} catch (error) { } catch (error) {
// 接口调用失败时,使用缓存作为降级方案 // 接口调用失败时,使用缓存作为降级方案
const cachedUserInfo = uni.getStorageSync('userInfo') || {}; const cachedUserInfo = uni.getStorageSync('userInfo') || {};
// 检查 idCard // 检查 idCard
const idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null; const idCard = cachedUserInfo?.resume?.idCard ?? cachedUserInfo?.idCard ?? null;
const hasIdCard = idCard !== null && idCard !== undefined && idCard !== ''; const hasIdCard = idCard !== null && idCard !== undefined && idCard !== '';
// 检查职位信息 // 检查职位信息
const cachedJobTitles = Array.isArray(cachedUserInfo?.jobTitles) ? cachedUserInfo.jobTitles : []; const cachedJobTitles = Array.isArray(cachedUserInfo?.jobTitles) ? cachedUserInfo.jobTitles : [];
const hasJobTitle = cachedJobTitles.length > 0; const hasJobTitle = cachedJobTitles.length > 0;
let jobName = ''; let jobName = '';
if (!hasJobTitle) { if (!hasJobTitle) {
jobName = cachedUserInfo?.jobName ?? jobName = cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ?? cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ?? cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ?? cachedUserInfo?.resume?.currentJobName ??
''; '';
} }
const hasJobInfo = hasJobTitle || (jobName && jobName.trim() !== ''); const hasJobInfo = hasJobTitle || (jobName && jobName.trim() !== '');
// 检查技能标签 // 检查技能标签
const cachedAppSkillsList = Array.isArray(cachedUserInfo?.appSkillsList) ? cachedUserInfo.appSkillsList : []; const cachedAppSkillsList = Array.isArray(cachedUserInfo?.appSkillsList) ? cachedUserInfo.appSkillsList : [];
const hasSkills = cachedAppSkillsList.some(skill => { const hasSkills = cachedAppSkillsList.some(skill => {
const skillName = skill?.name || skill?.nameStr; const skillName = skill?.name || skill?.nameStr;
return skillName && skillName.trim() !== ''; return skillName && skillName.trim() !== '';
}); });
// 保存缺失信息标识 // 保存缺失信息标识
missingInfo.value.hasJobInfo = hasJobInfo; missingInfo.value.hasJobInfo = hasJobInfo;
missingInfo.value.hasSkills = hasSkills; missingInfo.value.hasSkills = hasSkills;
// 判断信息是否完整idCard、职位信息、技能标签都必须有 // 判断信息是否完整idCard、职位信息、技能标签都必须有
const isComplete = hasIdCard && hasJobInfo && hasSkills; const isComplete = hasIdCard && hasJobInfo && hasSkills;
if (!isComplete) { if (!isComplete) {
// 收集缺失的信息提示 // 收集缺失的信息提示
const missingItems = []; const missingItems = [];
@@ -285,17 +222,17 @@ async function getRemindInfo() {
currentJobName.value = cachedJobTitles[0]; currentJobName.value = cachedJobTitles[0];
} else { } else {
currentJobName.value = jobName; currentJobName.value = jobName;
currentJobId.value = cachedUserInfo?.jobId ?? currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ?? cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ?? cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ?? cachedUserInfo?.resume?.currentJobId ??
null; null;
} }
// 信息完整,直接显示页面内容 // 信息完整,直接显示页面内容
showContent.value = true; showContent.value = true;
return; return;
} }
setTimeout(() => { setTimeout(() => {
openRemindPopup(); openRemindPopup();
}, 500); }, 500);
@@ -311,18 +248,18 @@ function handleCancel() {
// 确认按钮 // 确认按钮
async function handleConfirm() { async function handleConfirm() {
remindPopup.value?.close(); remindPopup.value?.close();
const { hasJobInfo, hasSkills } = missingInfo.value; const { hasJobInfo, hasSkills } = missingInfo.value;
// 如果同时缺少职位信息和技能标签:先跳转到职位信息页面,并传递参数表示完成后需要继续跳转到技能页面 // 如果同时缺少职位信息和技能标签:先跳转到职位信息页面,并传递参数表示完成后需要继续跳转到技能页面
if (!hasJobInfo && !hasSkills) { if (!hasJobInfo && !hasSkills) {
// 跳转到职位信息页面,传递参数表示完成后需要继续跳转到技能页面 // 跳转到职位信息页面,传递参数表示完成后需要继续跳转到技能页面
navTo('/packageA/pages/jobExpect/jobExpect?needSkill=true'); navTo('/packageA/pages/jobExpect/jobExpect?needSkill=true');
} }
// 如果只缺少技能标签:直接跳转到技能页面(个人信息页面的技能部分) // 如果只缺少技能标签:直接跳转到技能页面(个人信息页面的技能部分)
else if (!hasSkills) { else if (!hasSkills) {
navTo('/packageA/pages/personalInfo/personalInfo'); navTo('/packageA/pages/personalInfo/personalInfo');
} }
// 如果只缺少职位信息:直接跳转到职位信息页面 // 如果只缺少职位信息:直接跳转到职位信息页面
else if (!hasJobInfo) { else if (!hasJobInfo) {
navTo('/packageA/pages/jobExpect/jobExpect'); navTo('/packageA/pages/jobExpect/jobExpect');
@@ -336,32 +273,32 @@ async function handleConfirm() {
// 切换tab // 切换tab
function switchTab(index) { function switchTab(index) {
activeTab.value = index; activeTab.value = index;
if (index === 0 && !currentJobId.value) { if (index === 0 && !currentJobId.value) {
const cachedUserInfo = uni.getStorageSync('userInfo') || {}; const cachedUserInfo = uni.getStorageSync('userInfo') || {};
// 优先从缓存中的 jobTitles 数组获取职位信息(取第一个) // 优先从缓存中的 jobTitles 数组获取职位信息(取第一个)
const cachedJobTitles = Array.isArray(cachedUserInfo?.jobTitles) ? cachedUserInfo.jobTitles : []; const cachedJobTitles = Array.isArray(cachedUserInfo?.jobTitles) ? cachedUserInfo.jobTitles : [];
let newJobName = ''; let newJobName = '';
if (cachedJobTitles.length > 0) { if (cachedJobTitles.length > 0) {
newJobName = cachedJobTitles[0]; newJobName = cachedJobTitles[0];
} else { } else {
// 如果缓存中没有 jobTitles从其他字段获取 // 如果缓存中没有 jobTitles从其他字段获取
newJobName = currentJobName.value || newJobName = currentJobName.value ||
(cachedUserInfo?.jobName ?? (cachedUserInfo?.jobName ??
cachedUserInfo?.currentJobName ?? cachedUserInfo?.currentJobName ??
cachedUserInfo?.resume?.jobName ?? cachedUserInfo?.resume?.jobName ??
cachedUserInfo?.resume?.currentJobName ?? cachedUserInfo?.resume?.currentJobName ??
'市场专员'); '市场专员');
} }
const newJobId = cachedUserInfo?.jobId ?? const newJobId = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ?? cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ?? cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ?? cachedUserInfo?.resume?.currentJobId ??
null; null;
currentJobId.value = newJobId; currentJobId.value = newJobId;
currentJobName.value = newJobName; currentJobName.value = newJobName;
} }
@@ -437,13 +374,13 @@ async function handleJobCardClick(job) {
const response = await appUserInfo(); const response = await appUserInfo();
const userInfo = response?.data || {}; const userInfo = response?.data || {};
const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : []; const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : [];
// 将 appSkillsList 转换为 splitSkillListByScore 需要的格式 // 将 appSkillsList 转换为 splitSkillListByScore 需要的格式
const skillList = appSkillsList.map(item => ({ const skillList = appSkillsList.map(item => ({
skillName: item?.name || item?.nameStr || '', skillName: item?.name || item?.nameStr || '',
skillScore: item?.levels || item?.levelStr || 0 skillScore: item?.levels || item?.levelStr || 0
})).filter(item => item.skillName); })).filter(item => item.skillName);
const { possessed, improvement } = splitSkillListByScore(skillList); const { possessed, improvement } = splitSkillListByScore(skillList);
if (possessed.length === 0 && improvement.length === 0) { if (possessed.length === 0 && improvement.length === 0) {
@@ -532,6 +469,70 @@ onMounted(() => {
}); });
</script> </script>
<template>
<div 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"
/>
<!-- 页面内容 -->
<div 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-name="currentJobName"
@path-data-updated="handlePathDataUpdated"
/>
</scroll-view>
</div>
<!-- 底部导航栏 -->
<div class="tabbar-wrapper" v-if="showContent">
<CustomTabBar :currentPage="0" />
</div>
</div>
</template>
<style lang="scss" scoped> <style lang="scss" scoped>
.career-planning-page { .career-planning-page {
width: 100vw; width: 100vw;

View File

@@ -116,7 +116,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/jobPath.js'; import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/service/jobPath.js';
// 接收父组件传递的当前职位名称 // 接收父组件传递的当前职位名称
const props = defineProps({ const props = defineProps({

View File

@@ -1,11 +1,14 @@
<!--suppress JSFileReferences, NpmUsedModulesInstalled, VueMissingComponentImportInspection -->
<script setup> <script setup>
import { ref, computed, watch, onMounted } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { recommendJob } from '@/apiRc/jobRecommend.js'; import { useCareerRecommendationStore } from './store';
import { recommendJob } from '@/apiRc/service/jobRecommend.js';
import { appUserInfo } from '@/apiRc/user/user.js'; import { appUserInfo } from '@/apiRc/user/user.js';
const props = defineProps({ const props = defineProps({
currentJobId: { currentJobId: {
type: [Number, String], type: [ Number, String ],
default: null default: null
}, },
currentJobName: { currentJobName: {
@@ -14,7 +17,11 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['job-card-click', 'skills-updated']); const store = useCareerRecommendationStore();
const emit = defineEmits([ 'job-card-click', 'skills-updated' ]);
const popupRef = ref();
// 数据状态 // 数据状态
const skillTags = ref([]); const skillTags = ref([]);
@@ -110,7 +117,7 @@ async function fetchRecommendedJobs() {
return { return {
id: item?.jobId ?? index, id: item?.jobId ?? index,
jobId: item?.jobId ?? null, jobId: item?.jobId ?? null,
title: item?.jobName || `推荐职位${index + 1}`, title: item?.jobName || `推荐职位${ index + 1 }`,
jobName: item?.jobName || '', jobName: item?.jobName || '',
skills: skillNames, skills: skillNames,
rawSkills: skillList rawSkills: skillList
@@ -145,7 +152,7 @@ onMounted(() => {
// 监听 props 变化,自动获取推荐职位和技能标签 // 监听 props 变化,自动获取推荐职位和技能标签
watch( watch(
() => [props.currentJobId, props.currentJobName], () => [ props.currentJobId, props.currentJobName ],
() => { () => {
if (props.currentJobName) { if (props.currentJobName) {
fetchCurrentJobSkills(); fetchCurrentJobSkills();
@@ -159,60 +166,60 @@ watch(
function handleJobCardClick(job) { function handleJobCardClick(job) {
emit('job-card-click', job); emit('job-card-click', job);
} }
const eventSelectCurrentJob = () => {
popupRef.value?.open('bottom');
};
const eventCloseCurrentJob = () => {
popupRef.value?.close('bottom');
};
</script> </script>
<template> <template>
<div class="career-recommend"> <div class="career-recommend">
<!-- 当前职位信息卡片 --> <!-- 当前职位信息卡片 -->
<div class="info-card"> <div class="info-card">
<div class="card-title">当前职位信息</div> <div class="card-title">当前职位信息</div>
<div class="card-content"> <div class="card-content">
<text class="label">当前职位</text> <span class="label">当前职位</span>
<text class="value">{{ currentJobDisplay }}</text> <span class="value" @click="eventSelectCurrentJob">{{ currentJobDisplay }}</span>
</div>
</div> </div>
</div>
<!-- 我的技能标签卡片 --> <!-- 我的技能标签卡片 -->
<div class="info-card"> <div class="info-card">
<div class="card-title">我的技能标签</div> <div class="card-title">我的技能标签</div>
<div class="skill-tags"> <div class="skill-tags">
<div <div v-for="(skill, index) in skillTags" :key="index" class="skill-tag">
class="skill-tag" {{ skill }}
v-for="(skill, index) in skillTags"
:key="index"
>
{{ skill }}
</div>
<text v-if="!skillTags.length && !isLoadingSkillTags" class="empty-text">暂无技能数据</text>
<text v-if="isLoadingSkillTags" class="empty-text">加载中...</text>
</div> </div>
<span v-if="!skillTags.length && !isLoadingSkillTags" class="empty-text">暂无技能数据</span>
<span v-if="isLoadingSkillTags" class="empty-text">加载中...</span>
</div> </div>
</div>
<!-- 相似推荐职位 --> <!-- 相似推荐职位 -->
<div class="section-title"> <div class="section-title">
相似推荐职位 相似推荐职位
</div>
<div v-if="!isLoadingRecommend && recommendedJobs.length === 0" class="empty-text">暂无推荐职位</div>
<div v-for="(job, index) in recommendedJobs" :key="index" class="job-item-card" @click="handleJobCardClick(job)">
<div class="job-header">
<span class="job-title">{{ job.title }}</span>
</div> </div>
<div v-if="!isLoadingRecommend && recommendedJobs.length === 0" class="empty-text">暂无推荐职位</div> <div class="job-skills">
<div <div v-for="(skill, skillIndex) in job.skills" :key="skillIndex" class="job-skill-tag">
class="job-item-card" {{ skill }}
v-for="(job, index) in recommendedJobs"
:key="index"
@click="handleJobCardClick(job)"
>
<div class="job-header">
<text class="job-title">{{ job.title }}</text>
</div>
<div class="job-skills">
<div
class="job-skill-tag"
v-for="(skill, skillIndex) in job.skills"
:key="skillIndex"
>
{{ skill }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<uni-popup ref="popupRef" :border-radius="'20rpx 20rpx 0 0'" :is-mask-click="true" background-color="#FFFFFF" mask-background-color="rgba(255, 255, 255, 0.6)" type="bottom" @mask-click="eventCloseCurrentJob">
<div class="">
</div>
</uni-popup>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -6,22 +6,22 @@
<uni-icons type="search" size="18" color="#286BFA"></uni-icons> <uni-icons type="search" size="18" color="#286BFA"></uni-icons>
<text class="title-text">职业技能查询</text> <text class="title-text">职业技能查询</text>
</view> </view>
<view class="input-group"> <view class="input-group">
<view class="input-item"> <view class="input-item">
<text class="input-label">当前职位</text> <text class="input-label">当前职位</text>
<input <input
class="input-field" class="input-field"
:value="currentPosition" :value="currentPosition"
placeholder="市场专员" placeholder="市场专员"
placeholder-style="color: #999999" placeholder-style="color: #999999"
disabled disabled
/> />
</view> </view>
<view class="input-item"> <view class="input-item">
<text class="input-label">目标职业</text> <text class="input-label">目标职业</text>
<picker <picker
mode="selector" mode="selector"
:range="targetCareerOptions" :range="targetCareerOptions"
range-key="label" range-key="label"
@@ -37,7 +37,7 @@
</picker> </picker>
</view> </view>
</view> </view>
<button class="query-btn" @click="handleQuery"> <button class="query-btn" @click="handleQuery">
<text>查询技能发展路径</text> <text>查询技能发展路径</text>
<uni-icons type="search" size="18" color="#FFFFFF"></uni-icons> <uni-icons type="search" size="18" color="#FFFFFF"></uni-icons>
@@ -52,19 +52,19 @@
<uni-icons type="person-filled" size="18" color="#000000"></uni-icons> <uni-icons type="person-filled" size="18" color="#000000"></uni-icons>
<text class="title-text">技能发展路径</text> <text class="title-text">技能发展路径</text>
</view> </view>
<view class="intro-text"> <view class="intro-text">
基于您的当前职业和目标职业,以下是您需要重点发展的技能: 基于您的当前职业和目标职业,以下是您需要重点发展的技能:
</view> </view>
<view class="skill-list"> <view class="skill-list">
<view v-if="isLoadingSkills" class="empty-text">加载中...</view> <view v-if="isLoadingSkills" class="empty-text">加载中...</view>
<view v-else-if="!hasQueried" class="empty-text">请先查询职业路径以获取技能发展数据</view> <view v-else-if="!hasQueried" class="empty-text">请先查询职业路径以获取技能发展数据</view>
<view v-else-if="skillList.length === 0" class="empty-text">暂无数据</view> <view v-else-if="skillList.length === 0" class="empty-text">暂无数据</view>
<view <view
v-else v-else
class="skill-item" class="skill-item"
v-for="(skill, index) in skillList" v-for="(skill, index) in skillList"
:key="index" :key="index"
> >
<view class="skill-header"> <view class="skill-header">
@@ -75,9 +75,9 @@
</view> </view>
</view> </view>
<view class="skill-tags" v-if="skill.tags && skill.tags.length > 0"> <view class="skill-tags" v-if="skill.tags && skill.tags.length > 0">
<view <view
class="skill-tag" class="skill-tag"
v-for="(tag, tagIndex) in skill.tags" v-for="(tag, tagIndex) in skill.tags"
:key="tagIndex" :key="tagIndex"
> >
{{ tag }} {{ tag }}
@@ -90,9 +90,9 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/jobPath.js'; import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/service/jobPath.js';
import { getJobSkillWeight } from '@/apiRc/jobSkill.js'; import { getJobSkillWeight } from '@/apiRc/service/jobSkill.js';
const props = defineProps({ const props = defineProps({
// 当前职位名称 // 当前职位名称
@@ -138,9 +138,9 @@ async function fetchTargetCareerOptions(keyword = '') {
pageNo: 1, pageNo: 1,
pageSize: 100 pageSize: 100
}); });
const list = response?.data?.list || response?.list || []; const list = response?.data?.list || response?.list || [];
targetCareerOptions.value = list.map(item => ({ targetCareerOptions.value = list.map(item => ({
label: item.endJob || item.startJob || '未知职位', label: item.endJob || item.startJob || '未知职位',
value: item.id, value: item.id,
@@ -186,16 +186,16 @@ async function loadPathDetail(jobPathId) {
}; };
return; return;
} }
try { try {
const requestParams = { const requestParams = {
jobPathId: jobPathId jobPathId: jobPathId
}; };
const response = await getJobPathDetail(requestParams); const response = await getJobPathDetail(requestParams);
const details = Array.isArray(response?.data) ? response.data : []; const details = Array.isArray(response?.data) ? response.data : [];
if (details.length === 0) { if (details.length === 0) {
localPathData.value = { localPathData.value = {
start: { title: '暂无数据', skills: [] }, start: { title: '暂无数据', skills: [] },
@@ -223,7 +223,7 @@ async function loadPathDetail(jobPathId) {
steps, steps,
end end
}; };
// 通知父组件路径数据已更新 // 通知父组件路径数据已更新
emit('path-data-updated', { emit('path-data-updated', {
pathData: localPathData.value, pathData: localPathData.value,
@@ -282,7 +282,7 @@ async function handleQuery() {
async function fetchSkillWeight() { async function fetchSkillWeight() {
// 获取当前职位(使用界面上显示的值) // 获取当前职位(使用界面上显示的值)
const currentJob = currentPosition.value || props.currentJobName || ''; const currentJob = currentPosition.value || props.currentJobName || '';
if (!currentJob) { if (!currentJob) {
skillList.value = []; skillList.value = [];
uni.showToast({ uni.showToast({
@@ -291,12 +291,12 @@ async function fetchSkillWeight() {
}); });
return; return;
} }
// 获取目标职业(使用界面上选择的值) // 获取目标职业(使用界面上选择的值)
const targetCareer = selectedTargetIndex.value >= 0 const targetCareer = selectedTargetIndex.value >= 0
? targetCareerOptions.value[selectedTargetIndex.value]?.label ? targetCareerOptions.value[selectedTargetIndex.value]?.label
: ''; : '';
if (!targetCareer) { if (!targetCareer) {
skillList.value = []; skillList.value = [];
uni.showToast({ uni.showToast({
@@ -305,31 +305,31 @@ async function fetchSkillWeight() {
}); });
return; return;
} }
isLoadingSkills.value = true; isLoadingSkills.value = true;
uni.showLoading({ uni.showLoading({
title: '加载中...', title: '加载中...',
mask: true mask: true
}); });
try { try {
const response = await getJobSkillWeight({ const response = await getJobSkillWeight({
currentJobName: currentJob, currentJobName: currentJob,
targetJobName: targetCareer targetJobName: targetCareer
}); });
// 标记已经查询过 // 标记已经查询过
hasQueried.value = true; hasQueried.value = true;
// 处理接口返回的数据 // 处理接口返回的数据
const responseData = response?.data || response || []; const responseData = response?.data || response || [];
const dataItem = Array.isArray(responseData) ? responseData[0] : responseData; const dataItem = Array.isArray(responseData) ? responseData[0] : responseData;
// 合并当前职位和目标职位的技能列表 // 合并当前职位和目标职位的技能列表
const currentSkills = Array.isArray(dataItem?.currentSkillDetList) ? dataItem.currentSkillDetList : []; const currentSkills = Array.isArray(dataItem?.currentSkillDetList) ? dataItem.currentSkillDetList : [];
const targetSkills = Array.isArray(dataItem?.targetSkillDetList) ? dataItem.targetSkillDetList : []; const targetSkills = Array.isArray(dataItem?.targetSkillDetList) ? dataItem.targetSkillDetList : [];
const allSkills = [...currentSkills, ...targetSkills]; const allSkills = [...currentSkills, ...targetSkills];
// 转换为组件需要的格式 // 转换为组件需要的格式
skillList.value = allSkills.map(item => ({ skillList.value = allSkills.map(item => ({
name: item?.skillName || item?.name || '', name: item?.skillName || item?.name || '',
@@ -343,7 +343,7 @@ async function fetchSkillWeight() {
const scoreB = parseFloat(b.score) || 0; const scoreB = parseFloat(b.score) || 0;
return scoreB - scoreA; return scoreB - scoreA;
}); });
if (skillList.value.length === 0) { if (skillList.value.length === 0) {
uni.showToast({ uni.showToast({
title: '暂无技能数据', title: '暂无技能数据',
@@ -385,7 +385,7 @@ onMounted(async () => {
padding: 28rpx; padding: 28rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title { .section-title {
margin-top: 0; margin-top: 0;
} }
@@ -482,7 +482,7 @@ button::after {
align-items: center; align-items: center;
gap: 12rpx; gap: 12rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
.content-section & { .content-section & {
margin-top: 0; margin-top: 0;
} }

View File

@@ -0,0 +1,188 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthUserStore, useBasicStore } from './index';
import { getCurrentPosition, getPath, getPathDetail } from '@/apiRc/service';
export const useCareerPathStore = defineStore('career-path', () => {
const storeBasic = useBasicStore();
const storeUser = useAuthUserStore();
const profession = ref('');
const professions = ref([]);
const professionsRef = computed(() => {
const userInfo = storeUser.userInfo;
if (!userInfo || !userInfo.professions || userInfo.professions.length === 0) {
return professions.value;
}
const userProfessionsLabels = userInfo.professions.map((d) => d.label);
let professionsA = [];
let professionsB = [];
professions.value.filter((d) => userProfessionsLabels.includes(d.label));
for (const d of professions.value) {
if (userProfessionsLabels.includes(d.label)) {
professionsA.push(d);
} else {
professionsB.push(d);
}
}
if (professionsA.length === 0) {
professionsA = userInfo.professions;
professionsB = professions.value;
}
return [...professionsA, ...professionsB];
});
const targetCareer = ref('');
const paths = ref([]);
const pathsRef = computed(() => {
return paths.value.filter((d) => {
return `${d.startJobId}` === profession.value;
});
});
const result = ref([]);
const fetchData = async () => {
try {
const { code, msg, data } = await getCurrentPosition();
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
professions.value = data.map((d) => {
return {
label: d.name,
value: `${d.jobId}`
};
});
} catch (e) {
console.warn(e);
}
};
const fetchDataPath = async () => {
try {
const { code, msg, data } = await getPath();
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
paths.value = data.map((d) => {
return {
label: d.endJob,
value: `${d.startJobId}-${d.endJobId}`,
startJobId: d.startJobId
};
});
} catch (e) {
console.warn(e);
}
};
const fetchResult = async () => {
if (!targetCareer.value) {
return;
}
const [startJobId, endJobId] = targetCareer.value.split('-');
const params = {
startJobId: Number(startJobId),
endJobId: Number(endJobId)
};
try {
const { code, msg, data } = await getPathDetail(params);
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
result.value = data.map((d, i) => {
return {
type: i === 0 ? 'start' : i === data.length - 1 ? 'end' : 'normal',
step: i,
title: d.name,
tags: d.skillNameList.split(',')
};
});
} catch (e) {
console.warn(e);
}
};
const eventChange = () => {
targetCareer.value = '';
};
const eventSearch = () => {
if (pathsRef.value.length === 0) {
ElMessage.warning({
message: '当前职业暂无发展路径,敬请期待!',
duration: 5000
});
return;
}
if (!profession.value) {
ElMessage.warning({
message: '请选择当前职位!',
duration: 5000
});
return;
}
if (!targetCareer.value) {
ElMessage.warning({
message: '请选择目标职业!',
duration: 5000
});
return;
}
void fetchResult();
};
watch(
() => storeBasic.loaded,
() => {
if (storeBasic.loaded) {
void fetchData();
void fetchDataPath();
}
}
);
watch(
() => professionsRef.value,
() => {
if (typeof professionsRef.value[0] !== 'undefined') {
if (professionsRef.value[0].value) {
profession.value = professionsRef.value[0].value;
}
}
}
);
watch(
() => profession.value,
() => {
const userInfo = storeUser.userInfo;
if (userInfo.professions[0] && profession.value === userInfo.professions[0].value) {
targetCareer.value = '';
}
}
);
return {
profession,
professionsRef,
targetCareer,
pathsRef,
result,
eventChange,
eventSearch
};
});

View File

@@ -0,0 +1,154 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useBasicStore, useAuthUserStore } from './index';
import { getProfessions, getSkillTags, getRecommend } from '@/apiRc/service';
export const useCareerRecommendationStore = defineStore('career-recommendation', () => {
const storeBasic = useBasicStore();
const storeUser = useAuthUserStore();
const profession = ref('');
const professions = ref([]);
const professionsRef = computed(() => {
const userInfo = storeUser.userInfo;
if (!userInfo || !userInfo.professions || userInfo.professions.length === 0) {
return professions.value;
}
const userProfessionsLabels = userInfo.professions.map((d) => d.label);
let professionsA = [];
let professionsB = [];
professions.value.filter((d) => userProfessionsLabels.includes(d.label));
for (const d of professions.value) {
if (userProfessionsLabels.includes(d.label)) {
professionsA.push(d);
} else {
professionsB.push(d);
}
}
if (professionsA.length === 0) {
professionsA = userInfo.professions;
professionsB = professions.value;
}
return [...professionsA, ...professionsB];
});
const skills = ref([]);
const skillTags = computed(() => {
const userInfo = storeUser.userInfo;
if (userInfo.professions[0] && profession.value === userInfo.professions[0].value) {
return userInfo.skills.map((d) => d.label);
}
return skills.value;
});
const result = ref([]);
const fetchData = async () => {
try {
const { code, msg, data } = await getProfessions();
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
professions.value = data.map((d) => {
return {
label: d.name,
value: `${d.jobId}`
};
});
} catch (e) {
console.warn(e);
}
};
const fetchSkillTags = async () => {
const params = {
jobName: profession.value
};
try {
const { code, msg, data } = await getSkillTags(params);
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (typeof data !== 'undefined' && Array.isArray(data) && data.length > 0 && data[0]) {
skills.value = data[0].skillDetList.map((d) => d.skillName);
}
} catch (e) {
console.warn(e);
}
};
const fetchRecommend = async () => {
const params = {
jobName: profession.value
};
try {
const { code, msg, data } = await getRecommend(params);
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
result.value = data.map((d) => {
return {
title: d.jobName,
tags: d.skillList.map((d) => d.skillName),
percentage: d.similarityScore ?? 0
};
});
} catch (e) {
console.warn(e);
}
};
const eventSearch = () => {
void fetchRecommend();
};
watch(
() => storeBasic.loaded,
() => {
if (storeBasic.loaded) {
void fetchData();
}
}
);
watch(
() => profession.value,
() => {
if (profession.value) {
void fetchSkillTags();
}
},
{
immediate: true
}
);
watch(
() => professionsRef.value,
() => {
if (professionsRef.value[0]) {
profession.value = professionsRef.value[0].label;
}
}
);
return {
profession,
professions,
professionsRef,
skillTags,
result,
eventSearch
};
});

View File

@@ -0,0 +1,4 @@
export * from './user';
export * from './career-recommendation';
export * from './career-path';
export * from './skill-development';

View File

@@ -0,0 +1,192 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthUserStore, useBasicStore } from './index';
import { getCurrentPosition, getPath, getSkill } from '@/apiRc/service';
export const useSkillDevelopmentStore = defineStore('skill-development', () => {
const storeBasic = useBasicStore();
const storeUser = useAuthUserStore();
const profession = ref('');
const professions = ref([]);
const professionsRef = computed(() => {
const userInfo = storeUser.userInfo;
if (!userInfo || !userInfo.professions || userInfo.professions.length === 0) {
return professions.value;
}
const userProfessionsLabels = userInfo.professions.map((d) => d.label);
let professionsA = [];
let professionsB = [];
professions.value.filter((d) => userProfessionsLabels.includes(d.label));
for (const d of professions.value) {
if (userProfessionsLabels.includes(d.label)) {
professionsA.push(d);
} else {
professionsB.push(d);
}
}
if (professionsA.length === 0) {
professionsA = userInfo.professions;
professionsB = professions.value;
}
return [...professionsA, ...professionsB];
});
const targetCareer = ref('');
const paths = ref([]);
const pathsRef = computed(() => {
return paths.value.filter((d) => {
return `${d.startJobId}` === profession.value;
});
});
const result = ref([]);
const fetchData = async () => {
try {
const { code, msg, data } = await getCurrentPosition();
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
professions.value = data.map((d) => {
return {
label: d.name,
value: `${d.jobId}`
};
});
} catch (e) {
console.warn(e);
}
};
const fetchDataPath = async () => {
try {
const { code, msg, data } = await getPath();
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
paths.value = data.map((d) => {
return {
label: d.endJob,
value: d.endJob,
startJobId: d.startJobId
};
});
} catch (e) {
console.warn(e);
}
};
const fetchResult = async () => {
const current = professionsRef.value.find((d) => d.value === profession.value);
const target = pathsRef.value.find((d) => d.value === targetCareer.value);
if (!current || !target) {
return;
}
const params = {
currentJobName: current.label,
targetJobName: target.label
};
try {
const { code, msg, data } = await getSkill(params);
if (code !== 0) {
$emitter.emit('error-message', msg);
return;
}
if (typeof data !== 'undefined' && Array.isArray(data) && data.length > 0 && data[0]) {
const excludes = data[0].currentSkillDetList.map((d) => d.skillId);
result.value = data[0].targetSkillDetList
.filter((d) => !excludes.includes(d.skillId))
.map((d) => {
return {
type: d.skillType,
title: d.skillName,
name: d.skillName,
weight: d.skillWeight,
score: d.skillScore
};
});
}
} catch (e) {
console.warn(e);
}
};
const eventChange = () => {
targetCareer.value = '';
};
const eventSearch = () => {
if (pathsRef.value.length === 0) {
ElMessage.warning({
message: '当前职业暂无发展路径,敬请期待!',
duration: 5000
});
return;
}
if (!profession.value) {
ElMessage.warning({
message: '请选择当前职位!',
duration: 5000
});
return;
}
if (!targetCareer.value) {
ElMessage.warning({
message: '请选择目标职业!',
duration: 5000
});
return;
}
void fetchResult();
};
watch(
() => storeBasic.loaded,
() => {
if (storeBasic.loaded) {
void fetchData();
void fetchDataPath();
}
}
);
watch(
() => professionsRef.value,
() => {
if (typeof professionsRef.value[0] !== 'undefined') {
if (professionsRef.value[0].value) {
profession.value = professionsRef.value[0].value;
}
}
}
);
watch(
() => profession.value,
() => {
const userInfo = storeUser.userInfo;
if (userInfo.professions[0] && profession.value === userInfo.professions[0].value) {
targetCareer.value = '';
}
}
);
return {
profession,
professionsRef,
targetCareer,
pathsRef,
result,
eventChange,
eventSearch
};
});

View File

@@ -0,0 +1,66 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { getUserInfo, getUserName } from '@/apiRc/service';
export const useAuthUserStore = defineStore('auth-user', () => {
const token = ref('');
const userLoaded = ref(false);
const userInfoRef = ref({
userName: '',
professions: [],
skills: []
});
const fetchUserInfo = async () => {
const tokenA = await $getItem('tokenA');
const cryptogram = await $getItem('cryptogram');
if (!cryptogram) {
return;
}
token.value = cryptogram;
try {
const { code, msg, data } = await getUserInfo(cryptogram);
if (code !== 200) {
$emitter.emit('error-message', msg);
return;
}
if (!data) {
return;
}
userInfoRef.value.professions = data.jobTitles.map((d) => {
return {
label: d,
value: d
};
});
userInfoRef.value.skills = data.appSkillsList.map((d) => {
return {
label: d.name,
value: d.name
};
});
} catch (e) {
console.warn(e);
}
try {
const { code: c, msg: m, data: userName } = await getUserName({ accessToken: tokenA });
if (c !== 0 || !userName) {
$emitter.emit('error-message', m);
return;
}
userInfoRef.value.userName = userName.name;
userLoaded.value = true;
} catch (e) {
console.warn(e);
}
};
return {
token,
userInfo: userInfoRef,
userLoaded,
fetchUserInfo
};
});