Files
ks-app-employment-service/pages/service/components/SkillDevelopment.vue
2025-12-04 14:38:54 +08:00

603 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="skill-development">
<!-- 职业技能查询区域 -->
<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="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 v-if="isLoadingSkills" 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
class="skill-item"
v-for="(skill, index) in skillList"
:key="index"
>
<view class="skill-header">
<text class="skill-name">{{ skill.name }}</text>
<view class="skill-info">
<text class="skill-score">技能分数: {{ skill.score }}</text>
<text class="skill-weight">权重: {{ skill.weight }}</text>
</view>
</view>
<view class="skill-tags" v-if="skill.tags && skill.tags.length > 0">
<view
class="skill-tag"
v-for="(tag, tagIndex) in skill.tags"
:key="tagIndex"
>
{{ tag }}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/jobPath.js';
import { getJobSkillWeight } from '@/apiRc/jobSkill.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 skillList = ref([]);
const isLoadingSkills = ref(false);
// 是否已经查询过(用于区分未查询和已查询但无数据)
const hasQueried = ref(false);
function parseSkillList(skillString) {
if (!skillString) {
return [];
}
return skillString
.split(/[,]/)
.map(item => item.trim())
.filter(item => item.length > 0);
}
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'
});
localPathData.value = {
start: { title: '暂无数据', skills: [] },
steps: [],
end: { title: '暂无数据', skills: [] }
};
return;
}
try {
const requestParams = {
jobPathId: jobPathId
};
const response = await getJobPathDetail(requestParams);
const details = Array.isArray(response?.data) ? response.data : [];
if (details.length === 0) {
localPathData.value = {
start: { title: '暂无数据', skills: [] },
steps: [],
end: { title: '暂无数据', skills: [] }
};
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);
localPathData.value = {
start,
steps,
end
};
// 通知父组件路径数据已更新
emit('path-data-updated', {
pathData: localPathData.value,
targetCareer: targetCareerOptions.value[selectedTargetIndex.value]?.label || ''
});
} catch (error) {
uni.showToast({
title: '获取路径详情失败',
icon: 'none'
});
localPathData.value = {
start: { title: '暂无数据', skills: [] },
steps: [],
end: { title: '暂无数据', skills: [] }
};
emit('path-data-updated', {
pathData: localPathData.value,
targetCareer: ''
});
}
}
function handleTargetChange(e) {
const index = Number(e.detail.value);
selectedTargetIndex.value = index;
const option = targetCareerOptions.value[index];
selectedJobPathId.value = option ? option.value : null;
// 重新选择目标职业时,重置查询状态和技能列表
hasQueried.value = false;
skillList.value = [];
}
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;
}
// 获取技能权重数据
await fetchSkillWeight();
}
// 获取技能权重数据
async function fetchSkillWeight() {
// 获取当前职位(使用界面上显示的值)
const currentJob = currentPosition.value || props.currentJobName || '';
if (!currentJob) {
skillList.value = [];
uni.showToast({
title: '当前职位信息缺失',
icon: 'none'
});
return;
}
// 获取目标职业(使用界面上选择的值)
const targetCareer = selectedTargetIndex.value >= 0
? targetCareerOptions.value[selectedTargetIndex.value]?.label
: '';
if (!targetCareer) {
skillList.value = [];
uni.showToast({
title: '请选择目标职业',
icon: 'none'
});
return;
}
isLoadingSkills.value = true;
uni.showLoading({
title: '加载中...',
mask: true
});
try {
const response = await getJobSkillWeight({
currentJobName: currentJob,
targetJobName: targetCareer
});
// 标记已经查询过
hasQueried.value = true;
// 处理接口返回的数据
const responseData = response?.data || response || [];
const dataItem = Array.isArray(responseData) ? responseData[0] : responseData;
// 合并当前职位和目标职位的技能列表
const currentSkills = Array.isArray(dataItem?.currentSkillDetList) ? dataItem.currentSkillDetList : [];
const targetSkills = Array.isArray(dataItem?.targetSkillDetList) ? dataItem.targetSkillDetList : [];
const allSkills = [...currentSkills, ...targetSkills];
// 转换为组件需要的格式
skillList.value = allSkills.map(item => ({
name: item?.skillName || item?.name || '',
weight: item?.skillWeight || item?.weight || '0',
score: item?.skillScore !== undefined && item?.skillScore !== null ? item.skillScore : 0,
tags: item?.tags || [],
currentLevel: item?.currentLevel || item?.level || 0
})).filter(item => item.name).sort((a, b) => {
// 按技能分数降序排序skillScore
const scoreA = parseFloat(a.score) || 0;
const scoreB = parseFloat(b.score) || 0;
return scoreB - scoreA;
});
if (skillList.value.length === 0) {
uni.showToast({
title: '暂无技能数据',
icon: 'none'
});
}
} catch (error) {
// 查询失败也标记为已查询
hasQueried.value = true;
uni.showToast({
title: '获取技能权重失败',
icon: 'none'
});
skillList.value = [];
} finally {
isLoadingSkills.value = false;
uni.hideLoading();
}
}
onMounted(async () => {
await Promise.all([
fetchTargetCareerOptions(),
fetchPathCount()
]);
});
</script>
<style lang="scss" scoped>
.skill-development {
padding: 10rpx 28rpx 20rpx;
background-color: #EBF4FF;
min-height:95%;
}
.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 {
margin-top: 0;
}
}
.content-section {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 28rpx;
box-sizing: border-box;
overflow: visible;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.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;
}
button::after {
border: none;
}
.section-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
.content-section & {
margin-top: 0;
}
}
.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-info {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
margin-left: 16rpx;
}
.skill-score {
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;
}
.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;
}
.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>