Files
ks-app-employment-service/pages/service/components/CareerPath.vue
2025-11-12 12:20:58 +08:00

530 lines
14 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="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, onMounted } from 'vue';
import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/jobPath.js';
// 当前职位(从接口获取,只读)
const currentPosition = ref('前端开发工程师');
// 目标职业选项列表
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 || [];
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) {
console.error('获取职业路径列表失败:', 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) {
console.error('获取职业路径数量失败:', error);
totalPathCount.value = 0;
}
}
async function loadPathDetail(jobPathId) {
try {
const response = await getJobPathDetail({
jobPathId
});
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
};
} catch (error) {
console.error('获取职业路径详情失败:', error);
uni.showToast({
title: '获取路径详情失败',
icon: 'none'
});
}
}
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) {
console.error('查询职业路径失败:', 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>