4 Commits
main ... yxl

Author SHA1 Message Date
dcbb3e02a7 = 职业规划推荐 2026-01-22 01:15:41 +08:00
7480651af2 = 职业规划推荐 2026-01-21 16:12:19 +08:00
3f7664f017 = 职业规划推荐 2026-01-21 14:26:34 +08:00
a5e30ac7f5 = 职业规划推荐 2026-01-21 09:47:09 +08:00
32 changed files with 3932 additions and 1396 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'
})
}

View File

@@ -76,11 +76,16 @@ export function getAddedJobs(params) {
})
}
// // 获取推荐岗位
// export function getAddedJobs(params) {
// return request({
// url: '/personnel/personBaseInfo/postRecommend',
// method: 'get',
// params,
// })
// }
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'
})
}

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

@@ -45,7 +45,7 @@
---
### 2. ``getJobPathById`- 根据职业路径ID获取详情
### 2. `getJobPathById`- 根据职业路径ID获取详情
- **接口路径**: `/jobPath/getJobPathById`
- **请求方法**: `GET`
- **接口类型**: `zytp`

View File

@@ -56,8 +56,8 @@ export function createApp() {
app.component('uni-data-select', uniDataSelect)
app.component('uni-swipe-action', uniSwipeAction)
app.component('uni-swipe-action-item', uniSwipeActionItem)
app.config.globalProperties.$processFileUrl = processFileUrl;
app.config.globalProperties.$getDict = getDict;
app.config.globalProperties.$store = storeRc;
@@ -94,4 +94,4 @@ export function createApp() {
app,
Pinia
}
}
}

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>
<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>
<!--suppress HtmlUnknownTag, NpmUsedModulesInstalled, JSFileReferences -->
<script setup>
import { ref, inject, nextTick, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
@@ -89,15 +26,9 @@ 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: {
@@ -153,13 +84,13 @@ 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') || {};
@@ -167,22 +98,22 @@ async function getRemindInfo() {
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 ??
'';
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 不为空)
@@ -190,14 +121,14 @@ async function getRemindInfo() {
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 = [];
@@ -217,55 +148,55 @@ async function getRemindInfo() {
currentJobName.value = jobTitles[0];
} else {
currentJobName.value = jobName;
currentJobId.value = userInfo?.jobId ??
userInfo?.currentJobId ??
userInfo?.resume?.jobId ??
userInfo?.resume?.currentJobId ??
null;
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 ??
'';
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 = [];
@@ -285,17 +216,17 @@ async function getRemindInfo() {
currentJobName.value = cachedJobTitles[0];
} else {
currentJobName.value = jobName;
currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
currentJobId.value = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
}
// 信息完整,直接显示页面内容
showContent.value = true;
return;
}
setTimeout(() => {
openRemindPopup();
}, 500);
@@ -311,18 +242,18 @@ function handleCancel() {
// 确认按钮
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');
@@ -336,32 +267,32 @@ async function handleConfirm() {
// 切换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 ??
'市场专员');
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;
const newJobId = cachedUserInfo?.jobId ??
cachedUserInfo?.currentJobId ??
cachedUserInfo?.resume?.jobId ??
cachedUserInfo?.resume?.currentJobId ??
null;
currentJobId.value = newJobId;
currentJobName.value = newJobName;
}
@@ -382,121 +313,6 @@ 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 = {
@@ -532,6 +348,122 @@ onMounted(() => {
});
</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"
/>
<!-- 页面内容 -->
<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"/>
<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">
.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;
}
.empty-text {
font-size: 26rpx;
text-align: center;
line-height: 1.5;
}
.input-group {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.input-item {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.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;
}
</style>
<style lang="scss" scoped>
.career-planning-page {
width: 100vw;

View File

@@ -1,374 +1,107 @@
<!--suppress JSFileReferences, NpmUsedModulesInstalled, VueMissingComponentImportInspection, HtmlUnknownTag -->
<script setup>
import { useCareerPathStore } from '@/stores/useCareerPathStore';
const store = useCareerPathStore();
</script>
<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>
<div class="career-path">
<!-- 职业路径查询区域 -->
<div class="query-section">
<div class="section-title">
<uni-icons color="#286BFA" size="18" type="search"></uni-icons>
<span class="title-text">职业路径查询</span>
</div>
<div class="input-group">
<div class="input-item">
<span class="label">当前职位</span>
<picker @change="store.eventProfession" :value="store.professionIndex" :range="store.professionsRef" range-key="label">
<div class="picker-field">
<span :class="store.professionLabel ? 'picker-text' : 'picker-placeholder'">
{{ store.professionLabel || '请选择职位' }}
</span>
<uni-icons color="#999999" size="16" type="down"></uni-icons>
</div>
</picker>
</div>
<div class="input-item">
<span class="label">目标职业</span>
<picker :range="store.pathsRef" :value="store.targetCareerIndex" range-key="label" @change="store.eventTargetCareer">
<div class="picker-field">
<span :class="store.targetCareerLabel ? 'picker-text' : 'picker-placeholder'">
{{ store.targetCareerLabel || '请选择目标职业' }}
</span>
<uni-icons color="#999999" size="16" type="down"></uni-icons>
</div>
</picker>
</div>
<button class="query-btn" @click="store.eventSearch">
<span>查询职业发展路径</span>
<uni-icons type="search" size="18" color="#FFFFFF"></uni-icons>
</button>
<view v-if="totalPathCount > 0" class="path-summary">
系统已收录 {{ totalPathCount }} 条职业路径
</view>
</view>
</div>
</div>
<!-- 职业发展路径区域 -->
<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>
<!-- 职业发展路径区域 -->
<div class="path-section">
<div class="section-title">
<uni-icons color="#000000" size="24" type="person-filled"></uni-icons>
<text class="title-text">职业发展路径</text>
</div>
<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>
<div class="timeline">
<div v-for="(step, index) in store.result" :key="index" class="timeline-item" :class="[step.type]">
<div class="timeline-marker" :class="[`${step.type}-marker`]"></div>
<div class="timeline-content">
<div v-if="step.type === 'start'" class="step-title">起点: {{ step.title }}</div>
<div v-else-if="step.type === 'end'" class="step-title">: {{ step.title }}</div>
<div v-else class="step-title">{{ step.step }}: {{ step.title }}</div>
<div class="skill-tags">
<div v-for="(skill, sIndex) in step.tags" :key="sIndex" class="skill-tag">{{ skill }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</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,
startJobId: item.startJobId,
endJobId: item.endJobId
}));
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(startJobId, endJobId) {
if (startJobId === null || startJobId === undefined || startJobId === '' || endJobId === null || endJobId === undefined || endJobId === '') {
uni.showToast({
title: '职业路径ID无效',
icon: 'none'
});
resetPathData();
return;
}
try {
const requestParams = {
startJobId,
endJobId
};
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) {
console.warn(error, 22222);
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;
}
isLoadingPath.value = true;
uni.showLoading({
title: '加载中...',
mask: true
});
console.log(option);
try {
let { value: jobPathId, startJobId, endJobId } = option;
/*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(startJobId, endJobId);
} catch (error) {
console.warn(error, 11111);
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;
}
.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;
&.placeholder {
font-weight: 500;
color: #ccc;
}
}
}
.query-section {
background-color: #FFFFFF;
border-radius: 16rpx;
@@ -390,19 +123,6 @@ onMounted(async () => {
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;
@@ -418,44 +138,6 @@ onMounted(async () => {
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;
@@ -491,11 +173,11 @@ onMounted(async () => {
width: 2rpx;
height: 100%;
background: repeating-linear-gradient(
to bottom,
#E0E0E0 0,
#E0E0E0 6rpx,
transparent 6rpx,
transparent 12rpx
to bottom,
#E0E0E0 0,
#E0E0E0 6rpx,
transparent 6rpx,
transparent 12rpx
);
transform: translateX(-50%);
z-index: 1;

View File

@@ -1,225 +1,84 @@
<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>
<text v-if="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)"
>
<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>
<!--suppress JSFileReferences, NpmUsedModulesInstalled, VueMissingComponentImportInspection, HtmlUnknownTag -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { recommendJob } from '@/apiRc/jobRecommend.js';
import { appUserInfo } from '@/apiRc/user/user.js';
import { useCareerRecommendationStore } from '@/stores/useCareerRecommendationStore';
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 || '市场专员');
// 从 appSkillsList 中提取技能名称
function extractSkillsFromAppSkillsList(appSkillsList = []) {
return (Array.isArray(appSkillsList) ? appSkillsList : [])
.map(item => item?.name || item?.nameStr || '')
.filter(name => !!name && name.trim().length > 0);
}
// 从技能列表中提取技能名称用于显示(用于推荐职位数据)
function extractSkillNames(skillList = []) {
return (Array.isArray(skillList) ? skillList : [])
.map(item => item?.skillName || '')
.filter(name => !!name && name.trim().length > 0);
}
// 获取当前职位的技能标签
async function fetchCurrentJobSkills() {
isLoadingSkillTags.value = true;
try {
// 优先从 appUserInfo 接口获取技能标签
const response = await appUserInfo();
const userInfo = response?.data || {};
// 从 appSkillsList 中提取技能名称
const appSkillsList = Array.isArray(userInfo?.appSkillsList) ? userInfo.appSkillsList : [];
const apiSkills = extractSkillsFromAppSkillsList(appSkillsList);
// 如果接口返回了技能数据,使用接口数据
if (apiSkills.length > 0) {
skillTags.value = apiSkills;
} else {
// 如果接口没有返回技能数据,从缓存中读取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const cachedAppSkills = Array.isArray(cachedUserInfo?.appSkillsList) ? cachedUserInfo.appSkillsList : [];
const cachedSkills = extractSkillsFromAppSkillsList(cachedAppSkills);
if (cachedSkills.length > 0) {
skillTags.value = cachedSkills;
} else {
skillTags.value = [];
}
}
// 通知父组件技能数据已更新
emit('skills-updated', {
currentJobSkills: skillTags.value,
recommendedJobs: recommendedJobs.value
});
} catch (error) {
// appUserInfo 接口调用失败时,从缓存中读取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const cachedAppSkills = Array.isArray(cachedUserInfo?.appSkillsList) ? cachedUserInfo.appSkillsList : [];
const cachedSkills = extractSkillsFromAppSkillsList(cachedAppSkills);
if (cachedSkills.length > 0) {
skillTags.value = 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],
() => {
if (props.currentJobName) {
fetchCurrentJobSkills();
fetchRecommendedJobs();
}
},
{ immediate: true }
);
// 事件处理
function handleJobCardClick(job) {
emit('job-card-click', job);
}
const store = useCareerRecommendationStore();
</script>
<template>
<div class="career-recommend">
<div class="info-card">
<div class="section-title">
<uni-icons color="#286BFA" size="18" type="search"></uni-icons>
<span class="title-text">我的职业</span>
</div>
<div class="input-group">
<div class="input-item">
<picker @change="store.eventProfession" :value="store.professionIndex" :range="store.professionsRef" range-key="label">
<div class="picker-field">
<span :class="store.professionLabel ? 'picker-text' : 'picker-placeholder'">
{{ store.professionLabel || '请选择职业' }}
</span>
<uni-icons color="#999999" size="16" type="down"></uni-icons>
</div>
</picker>
</div>
<button class="query-btn" @click="store.eventSearch">
<span>搜索</span>
<uni-icons type="search" size="18" color="#FFFFFF"></uni-icons>
</button>
</div>
</div>
<div class="info-card">
<div class="card-title">我的技能标签</div>
<div class="skill-tags">
<div v-for="(skill, index) in store.skillTags" :key="index" class="skill-tag">
{{ skill }}
</div>
<span v-if="!store.skillTags.length" class="empty-text">暂无技能数据</span>
</div>
</div>
<div class="section-title">
相似推荐职位
</div>
<div v-if="!store.result && store.result.length === 0" class="empty-text">暂无推荐职位</div>
<div v-for="(job, index) in store.result" :key="index" class="job-item-card">
<div class="job-header">
<span class="job-title">{{ job.title }}</span>
</div>
<div class="job-header">
<span>职业相似度{{ job.percentage }}%</span>
</div>
<div class="job-skills">
<div v-for="tag in job.tags" :key="tag" class="job-skill-tag">
{{ tag }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.career-recommend {
padding: 10rpx 28rpx 20rpx;
}
.section-title {
font-size: 32rpx;
color: #000000;
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #157DF0;
}
.info-card {
background-color: #FFFFFF;
border-radius: 16rpx;
@@ -239,7 +98,7 @@ function handleJobCardClick(job) {
display: flex;
justify-content: space-between;
align-items: center;
.label {
font-size: 24rpx;
line-height: 34rpx;
@@ -248,7 +107,7 @@ function handleJobCardClick(job) {
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
}
.value {
font-size: 32rpx;
line-height: 46rpx;
@@ -274,14 +133,6 @@ function handleJobCardClick(job) {
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;
@@ -290,7 +141,6 @@ function handleJobCardClick(job) {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
border-left: 6rpx solid #409EFF;
position: relative;
cursor: pointer;
}
.job-header {

View File

@@ -1,382 +1,85 @@
<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>
<!--suppress JSFileReferences, NpmUsedModulesInstalled, VueMissingComponentImportInspection, HtmlUnknownTag -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { getJobPathPage, getJobPathDetail, getJobPathNum } from '@/apiRc/jobPath.js';
import { getJobSkillWeight } from '@/apiRc/jobSkill.js';
import { useSkillDevelopmentStore } from '@/stores/useSkillDevelopmentStore';
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()
]);
});
const store = useSkillDevelopmentStore();
</script>
<template>
<div class="skill-development">
<!-- 职业技能查询区域 -->
<div class="query-section">
<div class="section-title">
<uni-icons color="#286BFA" size="18" type="search"></uni-icons>
<span class="title-text">技能发展路径查询</span>
</div>
<div class="input-group">
<div class="input-item">
<span class="input-label">当前职位</span>
<picker @change="store.eventProfession" :value="store.professionIndex" :range="store.professionsRef" range-key="label">
<div class="picker-field">
<span :class="store.professionLabel ? 'picker-text' : 'picker-placeholder'">
{{ store.professionLabel || '请选择目标职业' }}
</span>
<uni-icons color="#999999" size="16" type="down"></uni-icons>
</div>
</picker>
</div>
<div class="input-item">
<span class="input-label">目标职业</span>
<picker :range="store.pathsRef" :value="store.targetCareerIndex" range-key="label" @change="store.eventTargetCareer">
<div class="picker-field">
<span :class="store.targetCareerLabel ? 'picker-text' : 'picker-placeholder'">
{{ store.targetCareerLabel || '请选择目标职业' }}
</span>
<uni-icons color="#999999" size="16" type="down"></uni-icons>
</div>
</picker>
</div>
<button class="query-btn" @click="store.eventSearch">
<span>查询技能发展路径</span>
<uni-icons color="#FFFFFF" size="18" type="search"></uni-icons>
</button>
</div>
</div>
<div class="content-section">
<div class="section-title">
<uni-icons color="#000000" size="18" type="person-filled"></uni-icons>
<span class="title-text">技能发展路径</span>
</div>
<div class="intro-text">
基于您的当前职业和目标职业,以下是您需要重点发展的技能:
</div>
<div class="skill-list">
<div v-if="store.result.length === 0" class="empty-text">暂无数据</div>
<div v-for="(skill, index) in store.result" v-else :key="index" class="skill-item">
<div class="skill-header">
<span class="skill-name">{{ skill.name }}</span>
<div class="skill-info">
<span class="skill-score">技能分数: {{ skill.score }}</span>
<span class="skill-weight">权重: {{ skill.weight }}</span>
</div>
</div>
<div v-if="skill.tags && skill.tags.length > 0" class="skill-tags">
<div v-for="(tag, tagIndex) in skill.tags" :key="tagIndex" class="skill-tag">
{{ tag }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.skill-development {
padding: 10rpx 28rpx 20rpx;
background-color: #EBF4FF;
min-height:95%;
min-height: 95%;
}
.query-section {
@@ -385,7 +88,7 @@ onMounted(async () => {
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title {
margin-top: 0;
}
@@ -407,12 +110,6 @@ onMounted(async () => {
margin-bottom: 24rpx;
}
.input-item {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.input-label {
font-size: 28rpx;
color: #000000;
@@ -428,44 +125,6 @@ onMounted(async () => {
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;
@@ -482,7 +141,7 @@ button::after {
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
.content-section & {
margin-top: 0;
}

View File

@@ -0,0 +1,214 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { getCurrentPosition, getPath, getPathDetail } from '@/apiRc/service/careerPath';
export const useCareerPathStore = defineStore('career-path', () => {
const userInfo = ref({
userName: '',
professions: [],
skills: []
});
try {
const data = uni.getStorageSync('userInfo');
userInfo.value.professions = data.jobTitle.map((d) => {
return {
label: d,
value: d
};
});
userInfo.value.skills = data.appSkillsList.map((d) => {
return {
label: d.name,
value: d.name
};
});
} catch (e) {
console.warn(e);
}
const professionIndex = ref(0);
const profession = ref('');
const professionLabel = ref('');
const professions = ref([]);
const professionsRef = computed(() => {
if (!userInfo.value || !userInfo.value.professions || userInfo.value.professions.length === 0) {
return professions.value;
}
const userProfessionsLabels = userInfo.value.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.value.professions;
professionsB = professions.value;
}
return [ ...professionsA, ...professionsB ];
});
const targetCareerIndex = ref(0);
const targetCareer = ref('');
const targetCareerLabel = 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' : 'step',
step: i,
title: d.name,
tags: d.skillNameList.split(',')
};
});
} catch (e) {
console.warn(e);
}
};
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();
};
const eventProfession = (e) => {
professionIndex.value = Number(e.detail.value);
const item = professionsRef.value[e.detail.value]
profession.value = item.value;
professionLabel.value = item.label;
targetCareer.value = '';
targetCareerLabel.value = '';
result.value = [];
};
const eventTargetCareer = (e) => {
targetCareerIndex.value = Number(e.detail.value);
const item = pathsRef.value[e.detail.value]
targetCareer.value = item.value;
targetCareerLabel.value = item.label;
result.value = [];
};
void fetchData();
void fetchDataPath();
watch(
() => professionsRef.value,
() => {
if (professionsRef.value[ 0 ]) {
profession.value = professionsRef.value[ 0 ].value;
professionLabel.value = professionsRef.value[ 0 ].label;
}
}
);
return {
professionIndex,
professionLabel,
profession,
professionsRef,
targetCareerIndex,
targetCareer,
targetCareerLabel,
pathsRef,
result,
eventProfession,
eventTargetCareer,
eventSearch
};
});

View File

@@ -0,0 +1,178 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { getProfessions, getRecommend, getSkillTags } from '@/apiRc/service/careerRecommendation';
export const useCareerRecommendationStore = defineStore('career-recommendation', () => {
const userInfo = ref({
userName: '',
professions: [],
skills: []
});
try {
const data = uni.getStorageSync('userInfo');
userInfo.value.professions = data.jobTitle.map((d) => {
return {
label: d,
value: d
};
});
userInfo.value.skills = data.appSkillsList.map((d) => {
return {
label: d.name,
value: d.name
};
});
} catch (e) {
console.warn(e);
}
const professionIndex = ref(0);
const profession = ref('');
const professionLabel = ref('');
const professions = ref([]);
const professionsRef = computed(() => {
if (!userInfo.value || !userInfo.value.professions || userInfo.value.professions.length === 0) {
return professions.value;
}
const userProfessionsLabels = userInfo.value.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.value.professions;
professionsB = professions.value;
}
return [ ...professionsA, ...professionsB ];
});
const skills = ref([]);
const skillTags = computed(() => {
if (userInfo.value.professions[ 0 ] && professionLabel.value === userInfo.value.professions[ 0 ].value) {
return userInfo.value.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: professionLabel.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: professionLabel.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();
};
const eventProfession = (e) => {
professionIndex.value = Number(e.detail.value);
const item = professionsRef.value[e.detail.value]
profession.value = item.value;
professionLabel.value = item.label;
};
void fetchData();
watch(
() => profession.value,
() => {
if (profession.value) {
void fetchSkillTags();
result.value = [];
}
},
{
immediate: true
}
);
watch(
() => professionsRef.value,
() => {
if (professionsRef.value[ 0 ]) {
profession.value = professionsRef.value[ 0 ].value;
professionLabel.value = professionsRef.value[ 0 ].label;
}
}
);
return {
professionIndex,
profession,
professionLabel,
professionsRef,
skillTags,
result,
eventProfession,
eventSearch
};
});

View File

@@ -0,0 +1,222 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { getCurrentPosition, getPath } from '@/apiRc/service/careerPath';
import { getSkill } from '@/apiRc/service/skillDevelopment';
export const useSkillDevelopmentStore = defineStore('skill-development', () => {
const userInfo = ref({
userName: '',
professions: [],
skills: []
});
try {
const data = uni.getStorageSync('userInfo');
userInfo.value.professions = data.jobTitle.map((d) => {
return {
label: d,
value: d
};
});
userInfo.value.skills = data.appSkillsList.map((d) => {
return {
label: d.name,
value: d.name
};
});
} catch (e) {
console.warn(e);
}
const professionIndex = ref(0);
const profession = ref('');
const professionLabel = ref('');
const professions = ref([]);
const professionsRef = computed(() => {
if (!userInfo.value || !userInfo.value.professions || userInfo.value.professions.length === 0) {
return professions.value;
}
const userProfessionsLabels = userInfo.value.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.value.professions;
professionsB = professions.value;
}
return [ ...professionsA, ...professionsB ];
});
const targetCareerIndex = ref(0);
const targetCareer = ref('');
const targetCareerLabel = 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 () => {
if (!targetCareer.value) {
return;
}
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 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();
};
const eventProfession = (e) => {
professionIndex.value = Number(e.detail.value);
const item = professionsRef.value[ e.detail.value ];
profession.value = item.value;
professionLabel.value = item.label;
targetCareer.value = '';
targetCareerLabel.value = '';
result.value = [];
};
const eventTargetCareer = (e) => {
targetCareerIndex.value = Number(e.detail.value);
const item = pathsRef.value[ e.detail.value ];
targetCareer.value = item.value;
targetCareerLabel.value = item.label;
result.value = [];
};
void fetchData();
void fetchDataPath();
watch(
() => professionsRef.value,
() => {
if (professionsRef.value[ 0 ]) {
profession.value = professionsRef.value[ 0 ].value;
professionLabel.value = professionsRef.value[ 0 ].label;
}
}
);
return {
professionIndex,
professionLabel,
profession,
professionsRef,
targetCareerIndex,
targetCareer,
targetCareerLabel,
pathsRef,
result,
eventProfession,
eventTargetCareer,
eventSearch
};
});

View File

@@ -0,0 +1,33 @@
## 1.2.22023-01-28
- 修复 运行/打包 控制台警告问题
## 1.2.12022-09-05
- 修复 当 text 超过 max-num 时badge 的宽度计算是根据 text 的长度计算,更改为 css 计算实际展示宽度,详见:[https://ask.dcloud.net.cn/question/150473](https://ask.dcloud.net.cn/question/150473)
## 1.2.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge)
## 1.1.72021-11-08
- 优化 升级ui
- 修改 size 属性默认值调整为 small
- 修改 type 属性,默认值调整为 errorinfo 替换 default
## 1.1.62021-09-22
- 修复 在字节小程序上样式不生效的 bug
## 1.1.52021-07-30
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.42021-07-29
- 修复 去掉 nvue 不支持css 的 align-self 属性nvue 下不暂支持 absolute 属性
## 1.1.32021-06-24
- 优化 示例项目
## 1.1.12021-05-12
- 新增 组件示例地址
## 1.1.02021-05-12
- 新增 uni-badge 的 absolute 属性,支持定位
- 新增 uni-badge 的 offset 属性,支持定位偏移
- 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点
- 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+
- 优化 uni-badge 属性 custom-style 支持以对象形式自定义样式
## 1.0.72021-05-07
- 修复 uni-badge 在 App 端数字小于10时不是圆形的bug
- 修复 uni-badge 在父元素不是 flex 布局时宽度缩小的bug
- 新增 uni-badge 属性 custom-style 支持自定义样式
## 1.0.62021-02-04
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,268 @@
<template>
<view class="uni-badge--x">
<slot />
<text v-if="text" :class="classNames" :style="[positionStyle, customStyle, dotStyle]"
class="uni-badge" @click="onClick()">{{displayValue}}</text>
</view>
</template>
<script>
/**
* Badge 数字角标
* @description 数字角标一般和其它控件列表、9宫格等配合使用用于进行数量提示默认为实心灰色背景
* @tutorial https://ext.dcloud.net.cn/plugin?id=21
* @property {String} text 角标内容
* @property {String} size = [normal|small] 角标内容
* @property {String} type = [info|primary|success|warning|error] 颜色类型
* @value info 灰色
* @value primary 蓝色
* @value success 绿色
* @value warning 黄色
* @value error 红色
* @property {String} inverted = [true|false] 是否无需背景颜色
* @property {Number} maxNum 展示封顶的数字值,超过 99 显示 99+
* @property {String} absolute = [rightTop|rightBottom|leftBottom|leftTop] 开启绝对定位, 角标将定位到其包裹的标签的四角上
* @value rightTop 右上
* @value rightBottom 右下
* @value leftTop 左上
* @value leftBottom 左下
* @property {Array[number]} offset 距定位角中心点的偏移量,只有存在 absolute 属性时有效,例如:[-10, -10] 表示向外偏移 10px[10, 10] 表示向 absolute 指定的内偏移 10px
* @property {String} isDot = [true|false] 是否显示为一个小点
* @event {Function} click 点击 Badge 触发事件
* @example <uni-badge text="1"></uni-badge>
*/
export default {
name: 'UniBadge',
emits: ['click'],
props: {
type: {
type: String,
default: 'error'
},
inverted: {
type: Boolean,
default: false
},
isDot: {
type: Boolean,
default: false
},
maxNum: {
type: Number,
default: 99
},
absolute: {
type: String,
default: ''
},
offset: {
type: Array,
default () {
return [0, 0]
}
},
text: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'small'
},
customStyle: {
type: Object,
default () {
return {}
}
}
},
data() {
return {};
},
computed: {
width() {
return String(this.text).length * 8 + 12
},
classNames() {
const {
inverted,
type,
size,
absolute
} = this
return [
inverted ? 'uni-badge--' + type + '-inverted' : '',
'uni-badge--' + type,
'uni-badge--' + size,
absolute ? 'uni-badge--absolute' : ''
].join(' ')
},
positionStyle() {
if (!this.absolute) return {}
let w = this.width / 2,
h = 10
if (this.isDot) {
w = 5
h = 5
}
const x = `${- w + this.offset[0]}px`
const y = `${- h + this.offset[1]}px`
const whiteList = {
rightTop: {
right: x,
top: y
},
rightBottom: {
right: x,
bottom: y
},
leftBottom: {
left: x,
bottom: y
},
leftTop: {
left: x,
top: y
}
}
const match = whiteList[this.absolute]
return match ? match : whiteList['rightTop']
},
dotStyle() {
if (!this.isDot) return {}
return {
width: '10px',
minWidth: '0',
height: '10px',
padding: '0',
borderRadius: '10px'
}
},
displayValue() {
const {
isDot,
text,
maxNum
} = this
return isDot ? '' : (Number(text) > maxNum ? `${maxNum}+` : text)
}
},
methods: {
onClick() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;
$uni-success: #4cd964 !default;
$uni-warning: #f0ad4e !default;
$uni-error: #dd524d !default;
$uni-info: #909399 !default;
$bage-size: 12px;
$bage-small: scale(0.8);
.uni-badge--x {
/* #ifdef APP-NVUE */
// align-self: flex-start;
/* #endif */
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
position: relative;
}
.uni-badge--absolute {
position: absolute;
}
.uni-badge--small {
transform: $bage-small;
transform-origin: center center;
}
.uni-badge {
/* #ifndef APP-NVUE */
display: flex;
overflow: hidden;
box-sizing: border-box;
font-feature-settings: "tnum";
min-width: 20px;
/* #endif */
justify-content: center;
flex-direction: row;
height: 20px;
padding: 0 4px;
line-height: 18px;
color: #fff;
border-radius: 100px;
background-color: $uni-info;
background-color: transparent;
border: 1px solid #fff;
text-align: center;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: $bage-size;
/* #ifdef H5 */
z-index: 999;
cursor: pointer;
/* #endif */
&--info {
color: #fff;
background-color: $uni-info;
}
&--primary {
background-color: $uni-primary;
}
&--success {
background-color: $uni-success;
}
&--warning {
background-color: $uni-warning;
}
&--error {
background-color: $uni-error;
}
&--inverted {
padding: 0 5px 0 0;
color: $uni-info;
}
&--info-inverted {
color: $uni-info;
background-color: transparent;
}
&--primary-inverted {
color: $uni-primary;
background-color: transparent;
}
&--success-inverted {
color: $uni-success;
background-color: transparent;
}
&--warning-inverted {
color: $uni-warning;
background-color: transparent;
}
&--error-inverted {
color: $uni-error;
background-color: transparent;
}
}
</style>

View File

@@ -0,0 +1,85 @@
{
"id": "uni-badge",
"displayName": "uni-badge 数字角标",
"version": "1.2.2",
"description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。",
"keywords": [
"",
"badge",
"uni-ui",
"uniui",
"数字角标",
"徽章"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
## Badge 数字角标
> **组件名uni-badge**
> 代码块: `uBadge`
数字角标一般和其它控件列表、9宫格等配合使用用于进行数量提示默认为实心灰色背景
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,52 @@
## 1.2.172025-08-20
- 修复 右侧箭头类型错误的问题
## 1.2.162025-04-14
- 修复 可触发点击反馈的 uni-list-item 没有hover效果的问题
## 1.2.152025-01-08
- 修复 示例中过期图片地址
## 1.2.142023-04-14
- 优化 uni-list-chat 具名插槽`header` 非app端套一层元素方便使用时通过外层元素定位实现样式修改
## 1.2.132023-03-03
- uni-list-chat 新增 支持具名插槽`header`
## 1.2.122023-02-01
- 新增 列表图标新增 customPrefix 属性 ,用法 [详见](https://uniapp.dcloud.net.cn/component/uniui/uni-icons.html#icons-props)
## 1.2.112023-01-31
- 修复 无反馈效果呈现的bug
## 1.2.92022-11-22
- 修复 uni-list-chat 在vue3下跳转报错的bug
## 1.2.82022-11-21
- 修复 uni-list-chat avatar属性 值为本地路径时错误的问题
## 1.2.72022-11-21
- 修复 uni-list-chat avatar属性 在腾讯云版uniCloud下错误的问题
## 1.2.62022-11-18
- 修复 uni-list-chat note属性 支持:“草稿”字样功能 文本少1位的问题
## 1.2.52022-11-15
- 修复 uni-list-item 的 customStyle 属性 padding值在 H5端 无效的bug
## 1.2.42022-11-15
- 修复 uni-list-item 的 customStyle 属性 padding值在nvuevue2下无效的bug
## 1.2.32022-11-14
- uni-list-chat 新增 avatar 支持 fileId
## 1.2.22022-11-11
- uni-list 新增属性 render-reverse 详情参考:[https://uniapp.dcloud.net.cn/component/list.html](https://uniapp.dcloud.net.cn/component/list.html)
- uni-list-chat note属性 支持:“草稿”字样 加红显示 详情参考uni-im[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im)
- uni-list-item 新增属性 customStyle 支持设置padding、backgroundColor
## 1.2.12022-03-30
- 删除无用文件
## 1.2.02021-11-23
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-list](https://uniapp.dcloud.io/component/uniui/uni-list)
## 1.1.32021-08-30
- 修复 在vue3中to属性在发行应用的时候报错的bug
## 1.1.22021-07-30
- 优化 vue3下事件警告的问题
## 1.1.12021-07-21
- 修复 与其他组件嵌套使用时点击失效的Bug
## 1.1.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.0.172021-05-12
- 新增 组件示例地址
## 1.0.162021-02-05
- 优化 组件引用关系通过uni_modules引用组件
## 1.0.152021-02-05
- 调整为uni_modules目录规范
- 修复 uni-list-chat 角标显示不正常的问题

View File

@@ -0,0 +1,107 @@
<template>
<!-- #ifdef APP-NVUE -->
<cell>
<!-- #endif -->
<view class="uni-list-ad">
<view v-if="borderShow" :class="{'uni-list--border':border,'uni-list-item--first':isFirstChild}"></view>
<ad style="width: 200px;height: 300px;border-width: 1px;border-color: red;border-style: solid;" adpid="1111111111"
unit-id="" appid="" apid="" type="feed" @error="aderror" @close="closeAd"></ad>
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<!-- #endif -->
</template>
<script>
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom');
// #endif
export default {
name: 'UniListAd',
props: {
title: {
type: String,
default: '',
}
},
// inject: ['list'],
data() {
return {
isFirstChild: false,
border: false,
borderShow: true,
}
},
mounted() {
this.list = this.getForm()
if (this.list) {
if (!this.list.firstChildAppend) {
this.list.firstChildAppend = true
this.isFirstChild = true
}
this.border = this.list.border
}
},
methods: {
/**
* 获取父元素实例
*/
getForm(name = 'uniList') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
aderror(e) {
console.log("aderror: " + JSON.stringify(e.detail));
},
closeAd(e) {
this.borderShow = false
}
}
}
</script>
<style lang="scss" >
.uni-list-ad {
position: relative;
border: 1px red solid;
}
.uni-list--border {
position: relative;
padding-bottom: 1px;
/* #ifdef APP-PLUS */
border-top-color: $uni-border-color;
border-top-style: solid;
border-top-width: 0.5px;
/* #endif */
margin-left: $uni-spacing-row-lg;
}
/* #ifndef APP-NVUE */
.uni-list--border:after {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(.5);
transform: scaleY(.5);
background-color: $uni-border-color;
}
.uni-list-item--first:after {
height: 0px;
}
/* #endif */
</style>

View File

@@ -0,0 +1,58 @@
/**
* 这里是 uni-list 组件内置的常用样式变量
* 如果需要覆盖样式,这里提供了基本的组件样式变量,您可以尝试修改这里的变量,去完成样式替换,而不用去修改源码
*
*/
// 背景色
$background-color : #fff;
// 分割线颜色
$divide-line-color : #e5e5e5;
// 默认头像大小,如需要修改此值,注意同步修改 js 中的值 const avatarWidth = xx ,目前只支持方形头像
// nvue 页面不支持修改头像大小
$avatar-width : 45px ;
// 头像边框
$avatar-border-radius: 5px;
$avatar-border-color: #eee;
$avatar-border-width: 1px;
// 标题文字样式
$title-size : 16px;
$title-color : #3b4144;
$title-weight : normal;
// 描述文字样式
$note-size : 12px;
$note-color : #999;
$note-weight : normal;
// 右侧额外内容默认样式
$right-text-size : 12px;
$right-text-color : #999;
$right-text-weight : normal;
// 角标样式
// nvue 页面不支持修改圆点位置以及大小
// 角标在左侧时,角标的位置,默认为 0 ,负数左/下移动,正数右/上移动
$badge-left: 0px;
$badge-top: 0px;
// 显示圆点时,圆点大小
$dot-width: 10px;
$dot-height: 10px;
// 显示角标时,角标大小和字体大小
$badge-size : 18px;
$badge-font : 12px;
// 显示角标时,角标前景色
$badge-color : #fff;
// 显示角标时,角标背景色
$badge-background-color : #ff5a5f;
// 显示角标时,角标左右间距
$badge-space : 6px;
// 状态样式
// 选中颜色
$hover : #f5f5f5;

View File

@@ -0,0 +1,593 @@
<template>
<!-- #ifdef APP-NVUE -->
<cell>
<!-- #endif -->
<view :hover-class="!clickable && !link ? '' : 'uni-list-chat--hover'" class="uni-list-chat" @click.stop="onClick">
<view :class="{ 'uni-list--border': border, 'uni-list-chat--first': isFirstChild }"></view>
<view class="uni-list-chat__container">
<view class="uni-list-chat__header-warp">
<view v-if="avatarCircle || avatarList.length === 0" class="uni-list-chat__header" :class="{ 'header--circle': avatarCircle }">
<image class="uni-list-chat__header-image" :class="{ 'header--circle': avatarCircle }" :src="avatarUrl" mode="aspectFill"></image>
</view>
<!-- 头像组 -->
<view v-else class="uni-list-chat__header">
<view v-for="(item, index) in avatarList" :key="index" class="uni-list-chat__header-box" :class="computedAvatar"
:style="{ width: imageWidth + 'px', height: imageWidth + 'px' }">
<image class="uni-list-chat__header-image" :style="{ width: imageWidth + 'px', height: imageWidth + 'px' }" :src="item.url"
mode="aspectFill"></image>
</view>
</view>
</view>
<!-- #ifndef APP -->
<view class="slot-header">
<!-- #endif -->
<slot name="header"></slot>
<!-- #ifndef APP -->
</view>
<!-- #endif -->
<view v-if="badgeText && badgePositon === 'left'" class="uni-list-chat__badge uni-list-chat__badge-pos" :class="[isSingle]">
<text class="uni-list-chat__badge-text">{{ badgeText === 'dot' ? '' : badgeText }}</text>
</view>
<view class="uni-list-chat__content">
<view class="uni-list-chat__content-main">
<text class="uni-list-chat__content-title uni-ellipsis">{{ title }}</text>
<view style="flex-direction: row;">
<text class="draft" v-if="isDraft">[草稿]</text>
<text class="uni-list-chat__content-note uni-ellipsis">{{isDraft?note.slice(14):note}}</text>
</view>
</view>
<view class="uni-list-chat__content-extra">
<slot>
<text class="uni-list-chat__content-extra-text">{{ time }}</text>
<view v-if="badgeText && badgePositon === 'right'" class="uni-list-chat__badge" :class="[isSingle, badgePositon === 'right' ? 'uni-list-chat--right' : '']">
<text class="uni-list-chat__badge-text">{{ badgeText === 'dot' ? '' : badgeText }}</text>
</view>
</slot>
</view>
</view>
</view>
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<!-- #endif -->
</template>
<script>
// 头像大小
const avatarWidth = 45;
/**
* ListChat 聊天列表
* @description 聊天列表,用于创建聊天类列表
* @tutorial https://ext.dcloud.net.cn/plugin?id=24
* @property {String} title 标题
* @property {String} note 描述
* @property {Boolean} clickable = [true|false] 是否开启点击反馈默认为false
* @property {String} badgeText 数字角标内容
* @property {String} badgePositon = [left|right] 角标位置,默认为 right
* @property {String} link = [falsenavigateTo|redirectTo|reLaunch|switchTab] 是否展示右侧箭头并开启点击反馈默认为false
* @value false 不开启
* @value navigateTo 同 uni.navigateTo()
* @value redirectTo 同 uni.redirectTo()
* @value reLaunch 同 uni.reLaunch()
* @value switchTab 同 uni.switchTab()
* @property {String | PageURIString} to 跳转目标页面
* @property {String} time 右侧时间显示
* @property {Boolean} avatarCircle = [true|false] 是否显示圆形头像默认为false
* @property {String} avatar 头像地址avatarCircle 不填时生效
* @property {Array} avatarList 头像组,格式为 [{url:''}]
* @event {Function} click 点击 uniListChat 触发事件
*/
export default {
name: 'UniListChat',
emits:['click'],
props: {
title: {
type: String,
default: ''
},
note: {
type: String,
default: ''
},
clickable: {
type: Boolean,
default: false
},
link: {
type: [Boolean, String],
default: false
},
to: {
type: String,
default: ''
},
badgeText: {
type: [String, Number],
default: ''
},
badgePositon: {
type: String,
default: 'right'
},
time: {
type: String,
default: ''
},
avatarCircle: {
type: Boolean,
default: false
},
avatar: {
type: String,
default: ''
},
avatarList: {
type: Array,
default () {
return [];
}
}
},
// inject: ['list'],
computed: {
isDraft(){
return this.note.slice(0,14) == '[uni-im-draft]'
},
isSingle() {
if (this.badgeText === 'dot') {
return 'uni-badge--dot';
} else {
const badgeText = this.badgeText.toString();
if (badgeText.length > 1) {
return 'uni-badge--complex';
} else {
return 'uni-badge--single';
}
}
},
computedAvatar() {
if (this.avatarList.length > 4) {
this.imageWidth = avatarWidth * 0.31;
return 'avatarItem--3';
} else if (this.avatarList.length > 1) {
this.imageWidth = avatarWidth * 0.47;
return 'avatarItem--2';
} else {
this.imageWidth = avatarWidth;
return 'avatarItem--1';
}
}
},
watch: {
avatar:{
handler(avatar) {
if(avatar.substr(0,8) == 'cloud://'){
uniCloud.getTempFileURL({
fileList: [avatar]
}).then(res=>{
// console.log(res);
// 兼容uniCloud私有化部署
let fileList = res.fileList || res.result.fileList
this.avatarUrl = fileList[0].tempFileURL
})
}else{
this.avatarUrl = avatar
}
},
immediate: true
}
},
data() {
return {
isFirstChild: false,
border: true,
// avatarList: 3,
imageWidth: 50,
avatarUrl:''
};
},
mounted() {
this.list = this.getForm()
if (this.list) {
if (!this.list.firstChildAppend) {
this.list.firstChildAppend = true;
this.isFirstChild = true;
}
this.border = this.list.border;
}
},
methods: {
/**
* 获取父元素实例
*/
getForm(name = 'uniList') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
onClick() {
if (this.to !== '') {
this.openPage();
return;
}
if (this.clickable || this.link) {
this.$emit('click', {
data: {}
});
}
},
openPage() {
if (['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].indexOf(this.link) !== -1) {
this.pageApi(this.link);
} else {
this.pageApi('navigateTo');
}
},
pageApi(api) {
let callback = {
url: this.to,
success: res => {
this.$emit('click', {
data: res
});
},
fail: err => {
this.$emit('click', {
data: err
});
}
}
switch (api) {
case 'navigateTo':
uni.navigateTo(callback)
break
case 'redirectTo':
uni.redirectTo(callback)
break
case 'reLaunch':
uni.reLaunch(callback)
break
case 'switchTab':
uni.switchTab(callback)
break
default:
uni.navigateTo(callback)
}
}
}
};
</script>
<style lang="scss" >
$uni-font-size-lg:16px;
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
$background-color: #fff;
$divide-line-color: #e5e5e5;
$avatar-width: 45px;
$avatar-border-radius: 5px;
$avatar-border-color: #eee;
$avatar-border-width: 1px;
$title-size: 16px;
$title-color: #3b4144;
$title-weight: normal;
$note-size: 12px;
$note-color: #999;
$note-weight: normal;
$right-text-size: 12px;
$right-text-color: #999;
$right-text-weight: normal;
$badge-left: 0px;
$badge-top: 0px;
$dot-width: 10px;
$dot-height: 10px;
$badge-size: 18px;
$badge-font: 12px;
$badge-color: #fff;
$badge-background-color: #ff5a5f;
$badge-space: 6px;
$hover: #f5f5f5;
.uni-list-chat {
font-size: $uni-font-size-lg;
position: relative;
flex-direction: column;
justify-content: space-between;
background-color: $background-color;
}
// .uni-list-chat--disabled {
// opacity: 0.3;
// }
.uni-list-chat--hover {
background-color: $hover;
}
.uni-list--border {
position: relative;
margin-left: $uni-spacing-row-lg;
/* #ifdef APP-PLUS */
border-top-color: $divide-line-color;
border-top-style: solid;
border-top-width: 0.5px;
/* #endif */
}
/* #ifndef APP-NVUE */
.uni-list--border:after {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: $divide-line-color;
}
.uni-list-item--first:after {
height: 0px;
}
/* #endif */
.uni-list-chat--first {
border-top-width: 0px;
}
.uni-ellipsis {
/* #ifndef APP-NVUE */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
/* #endif */
}
.uni-ellipsis-2 {
/* #ifndef APP-NVUE */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* #endif */
/* #ifdef APP-NVUE */
lines: 2;
/* #endif */
}
.uni-list-chat__container {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex: 1;
padding: $uni-spacing-row-base $uni-spacing-row-lg;
position: relative;
overflow: hidden;
}
.uni-list-chat__header-warp {
position: relative;
}
.uni-list-chat__header {
/* #ifndef APP-NVUE */
display: flex;
align-content: center;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap-reverse;
/* #ifdef APP-NVUE */
width: 50px;
height: 50px;
/* #endif */
/* #ifndef APP-NVUE */
width: $avatar-width;
height: $avatar-width;
/* #endif */
border-radius: $avatar-border-radius;
border-color: $avatar-border-color;
border-width: $avatar-border-width;
border-style: solid;
overflow: hidden;
}
.uni-list-chat__header-box {
/* #ifndef APP-PLUS */
box-sizing: border-box;
display: flex;
width: $avatar-width;
height: $avatar-width;
/* #endif */
/* #ifdef APP-NVUE */
width: 50px;
height: 50px;
/* #endif */
overflow: hidden;
border-radius: 2px;
}
.uni-list-chat__header-image {
margin: 1px;
/* #ifdef APP-NVUE */
width: 50px;
height: 50px;
/* #endif */
/* #ifndef APP-NVUE */
width: $avatar-width;
height: $avatar-width;
/* #endif */
}
/* #ifndef APP-NVUE */
.uni-list-chat__header-image {
display: block;
width: 100%;
height: 100%;
}
.avatarItem--1 {
width: 100%;
height: 100%;
}
.avatarItem--2 {
width: 47%;
height: 47%;
}
.avatarItem--3 {
width: 32%;
height: 32%;
}
/* #endif */
.header--circle {
border-radius: 50%;
}
.uni-list-chat__content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex: 1;
overflow: hidden;
padding: 2px 0;
}
.uni-list-chat__content-main {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: space-between;
padding-left: $uni-spacing-row-base;
flex: 1;
overflow: hidden;
}
.uni-list-chat__content-title {
font-size: $title-size;
color: $title-color;
font-weight: $title-weight;
overflow: hidden;
}
.draft ,.uni-list-chat__content-note {
margin-top: 3px;
color: $note-color;
font-size: $note-size;
font-weight: $title-weight;
overflow: hidden;
}
.draft{
color: #eb3a41;
/* #ifndef APP-NVUE */
flex-shrink: 0;
/* #endif */
padding-right: 3px;
}
.uni-list-chat__content-extra {
/* #ifndef APP-NVUE */
flex-shrink: 0;
display: flex;
/* #endif */
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
margin-left: 5px;
}
.uni-list-chat__content-extra-text {
color: $right-text-color;
font-size: $right-text-size;
font-weight: $right-text-weight;
overflow: hidden;
}
.uni-list-chat__badge-pos {
position: absolute;
/* #ifdef APP-NVUE */
left: 55px;
top: 3px;
/* #endif */
/* #ifndef APP-NVUE */
left: calc(#{$avatar-width} + 10px - #{$badge-space} + #{$badge-left});
top: calc(#{$uni-spacing-row-base}/ 2 + 1px + #{$badge-top});
/* #endif */
}
.uni-list-chat__badge {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
border-radius: 100px;
background-color: $badge-background-color;
}
.uni-list-chat__badge-text {
color: $badge-color;
font-size: $badge-font;
}
.uni-badge--single {
/* #ifndef APP-NVUE */
// left: calc(#{$avatar-width} + 7px + #{$badge-left});
/* #endif */
width: $badge-size;
height: $badge-size;
}
.uni-badge--complex {
/* #ifdef APP-NVUE */
left: 50px;
/* #endif */
/* #ifndef APP-NVUE */
width: auto;
/* #endif */
height: $badge-size;
padding: 0 $badge-space;
}
.uni-badge--dot {
/* #ifdef APP-NVUE */
left: 60px;
top: 6px;
/* #endif */
/* #ifndef APP-NVUE */
left: calc(#{$avatar-width} + 15px - #{$dot-width}/ 2 + 1px + #{$badge-left});
/* #endif */
width: $dot-width;
height: $dot-height;
padding: 0;
}
.uni-list-chat--right {
/* #ifdef APP-NVUE */
left: 0;
/* #endif */
}
</style>

View File

@@ -0,0 +1,542 @@
<template>
<!-- #ifdef APP-NVUE -->
<cell :keep-scroll-position="keepScrollPosition">
<!-- #endif -->
<view :class="{ 'uni-list-item--disabled': disabled }" :style="{'background-color':customStyle.backgroundColor}"
:hover-class="(!clickable && !link) || disabled || showSwitch ? '' : 'uni-list-item--hover'"
class="uni-list-item" @click="onClick">
<view v-if="!isFirstChild" class="border--left" :class="{ 'uni-list--border': border }"></view>
<view class="uni-list-item__container"
:class="{ 'container--right': showArrow || link, 'flex--direction': direction === 'column'}"
:style="{paddingTop:padding.top,paddingLeft:padding.left,paddingRight:padding.right,paddingBottom:padding.bottom}">
<slot name="header">
<view class="uni-list-item__header">
<view v-if="thumb" class="uni-list-item__icon">
<image :src="thumb" class="uni-list-item__icon-img" :class="['uni-list--' + thumbSize]" />
</view>
<view v-else-if="showExtraIcon" class="uni-list-item__icon">
<uni-icons :customPrefix="extraIcon.customPrefix" :color="extraIcon.color" :size="extraIcon.size" :type="extraIcon.type" />
</view>
</view>
</slot>
<slot name="body">
<view class="uni-list-item__content"
:class="{ 'uni-list-item__content--center': thumb || showExtraIcon || showBadge || showSwitch }">
<text v-if="title" class="uni-list-item__content-title"
:class="[ellipsis !== 0 && ellipsis <= 2 ? 'uni-ellipsis-' + ellipsis : '']">{{ title }}</text>
<text v-if="note" class="uni-list-item__content-note">{{ note }}</text>
</view>
</slot>
<slot name="footer">
<view v-if="rightText || showBadge || showSwitch" class="uni-list-item__extra"
:class="{ 'flex--justify': direction === 'column' }">
<text v-if="rightText" class="uni-list-item__extra-text">{{ rightText }}</text>
<uni-badge v-if="showBadge" :type="badgeType" :text="badgeText" :custom-style="badgeStyle" />
<switch v-if="showSwitch" :disabled="disabled" :checked="switchChecked"
@change="onSwitchChange" />
</view>
</slot>
</view>
<uni-icons v-if="showArrow || link" :size="16" class="uni-icon-wrapper" color="#bbb" type="right" />
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<!-- #endif -->
</template>
<script>
/**
* ListItem 列表子组件
* @description 列表子组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=24
* @property {String} title 标题
* @property {String} note 描述
* @property {String} thumb 左侧缩略图若thumb有值则不会显示扩展图标
* @property {String} thumbSize = [lg|base|sm] 略缩图大小
* @value lg 大图
* @value base 一般
* @value sm 小图
* @property {String} badgeText 数字角标内容
* @property {String} badgeType 数字角标类型,参考[uni-icons](https://ext.dcloud.net.cn/plugin?id=21)
* @property {Object} badgeStyle 数字角标样式
* @property {String} rightText 右侧文字内容
* @property {Boolean} disabled = [true|false] 是否禁用
* @property {Boolean} clickable = [true|false] 是否开启点击反馈
* @property {String} link = [navigateTo|redirectTo|reLaunch|switchTab] 是否展示右侧箭头并开启点击反馈
* @value navigateTo 同 uni.navigateTo()
* @value redirectTo 同 uni.redirectTo()
* @value reLaunch 同 uni.reLaunch()
* @value switchTab 同 uni.switchTab()
* @property {String | PageURIString} to 跳转目标页面
* @property {Boolean} showBadge = [true|false] 是否显示数字角标
* @property {Boolean} showSwitch = [true|false] 是否显示Switch
* @property {Boolean} switchChecked = [true|false] Switch是否被选中
* @property {Boolean} showExtraIcon = [true|false] 左侧是否显示扩展图标
* @property {Object} extraIcon 扩展图标参数,格式为 {color: '#4cd964',size: '22',type: 'spinner'}
* @property {String} direction = [row|column] 排版方向
* @value row 水平排列
* @value column 垂直排列
* @event {Function} click 点击 uniListItem 触发事件
* @event {Function} switchChange 点击切换 Switch 时触发
*/
export default {
name: 'UniListItem',
emits: ['click', 'switchChange'],
props: {
direction: {
type: String,
default: 'row'
},
title: {
type: String,
default: ''
},
note: {
type: String,
default: ''
},
ellipsis: {
type: [Number, String],
default: 0
},
disabled: {
type: [Boolean, String],
default: false
},
clickable: {
type: Boolean,
default: false
},
showArrow: {
type: [Boolean, String],
default: false
},
link: {
type: [Boolean, String],
default: false
},
to: {
type: String,
default: ''
},
showBadge: {
type: [Boolean, String],
default: false
},
showSwitch: {
type: [Boolean, String],
default: false
},
switchChecked: {
type: [Boolean, String],
default: false
},
badgeText: {
type: String,
default: ''
},
badgeType: {
type: String,
default: 'success'
},
badgeStyle: {
type: Object,
default () {
return {}
}
},
rightText: {
type: String,
default: ''
},
thumb: {
type: String,
default: ''
},
thumbSize: {
type: String,
default: 'base'
},
showExtraIcon: {
type: [Boolean, String],
default: false
},
extraIcon: {
type: Object,
default () {
return {
type: '',
color: '#000000',
size: 20,
customPrefix: ''
};
}
},
border: {
type: Boolean,
default: true
},
customStyle: {
type: Object,
default () {
return {
padding: '',
backgroundColor: '#FFFFFF'
}
}
},
keepScrollPosition: {
type: Boolean,
default: false
}
},
watch: {
'customStyle.padding': {
handler(padding) {
if(typeof padding == 'number'){
padding += ''
}
let paddingArr = padding.split(' ')
if (paddingArr.length === 1) {
const allPadding = paddingArr[0]
this.padding = {
"top": allPadding,
"right": allPadding,
"bottom": allPadding,
"left": allPadding
}
} else if (paddingArr.length === 2) {
const [verticalPadding, horizontalPadding] = paddingArr;
this.padding = {
"top": verticalPadding,
"right": horizontalPadding,
"bottom": verticalPadding,
"left": horizontalPadding
}
} else if(paddingArr.length === 3) {
const [topPadding, horizontalPadding, bottomPadding] = paddingArr;
this.padding = {
"top": topPadding,
"right": horizontalPadding,
"bottom": bottomPadding,
"left": horizontalPadding
}
} else if (paddingArr.length === 4) {
const [topPadding, rightPadding, bottomPadding, leftPadding] = paddingArr;
this.padding = {
"top": topPadding,
"right": rightPadding,
"bottom": bottomPadding,
"left": leftPadding
}
}
},
immediate: true
}
},
// inject: ['list'],
data() {
return {
isFirstChild: false,
padding: {
top: "",
right: "",
bottom: "",
left: ""
}
};
},
mounted() {
this.list = this.getForm()
// 判断是否存在 uni-list 组件
if (this.list) {
if (!this.list.firstChildAppend) {
this.list.firstChildAppend = true;
this.isFirstChild = true;
}
}
},
methods: {
/**
* 获取父元素实例
*/
getForm(name = 'uniList') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
onClick() {
if (this.to !== '') {
this.openPage();
return;
}
if (this.clickable || this.link) {
this.$emit('click', {
data: {}
});
}
},
onSwitchChange(e) {
this.$emit('switchChange', e.detail);
},
openPage() {
if (['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].indexOf(this.link) !== -1) {
this.pageApi(this.link);
} else {
this.pageApi('navigateTo');
}
},
pageApi(api) {
let callback = {
url: this.to,
success: res => {
this.$emit('click', {
data: res
});
},
fail: err => {
this.$emit('click', {
data: err
});
}
}
switch (api) {
case 'navigateTo':
uni.navigateTo(callback)
break
case 'redirectTo':
uni.redirectTo(callback)
break
case 'reLaunch':
uni.reLaunch(callback)
break
case 'switchTab':
uni.switchTab(callback)
break
default:
uni.navigateTo(callback)
}
}
}
};
</script>
<style lang="scss">
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
$uni-spacing-col-lg: 12px;
$uni-spacing-row-lg: 15px;
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
$uni-border-color:#e5e5e5;
$uni-bg-color-hover:#f1f1f1;
$uni-text-color-grey:#999;
$list-item-pd: $uni-spacing-col-lg $uni-spacing-row-lg;
.uni-list-item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
font-size: $uni-font-size-lg;
position: relative;
justify-content: space-between;
align-items: center;
background-color: #fff;
flex-direction: row;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-list-item--disabled {
opacity: 0.3;
}
.uni-list-item--hover {
background-color: $uni-bg-color-hover !important;
}
.uni-list-item__container {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: $list-item-pd;
padding-left: $uni-spacing-row-lg;
flex: 1;
overflow: hidden;
// align-items: center;
}
.container--right {
padding-right: 0;
}
// .border--left {
// margin-left: $uni-spacing-row-lg;
// }
.uni-list--border {
position: absolute;
top: 0;
right: 0;
left: 0;
/* #ifdef APP-NVUE */
border-top-color: $uni-border-color;
border-top-style: solid;
border-top-width: 0.5px;
/* #endif */
}
/* #ifndef APP-NVUE */
.uni-list--border:after {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: $uni-border-color;
}
/* #endif */
.uni-list-item__content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
padding-right: 8px;
flex: 1;
color: #3b4144;
// overflow: hidden;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.uni-list-item__content--center {
justify-content: center;
}
.uni-list-item__content-title {
font-size: $uni-font-size-base;
color: #3b4144;
overflow: hidden;
}
.uni-list-item__content-note {
margin-top: 6rpx;
color: $uni-text-color-grey;
font-size: $uni-font-size-sm;
overflow: hidden;
}
.uni-list-item__extra {
// width: 25%;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.uni-list-item__header {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
}
.uni-list-item__icon {
margin-right: 18rpx;
flex-direction: row;
justify-content: center;
align-items: center;
}
.uni-list-item__icon-img {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
height: $uni-img-size-base;
width: $uni-img-size-base;
margin-right: 10px;
}
.uni-icon-wrapper {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
padding: 0 10px;
}
.flex--direction {
flex-direction: column;
/* #ifndef APP-NVUE */
align-items: initial;
/* #endif */
}
.flex--justify {
/* #ifndef APP-NVUE */
justify-content: initial;
/* #endif */
}
.uni-list--lg {
height: $uni-img-size-lg;
width: $uni-img-size-lg;
}
.uni-list--base {
height: $uni-img-size-base;
width: $uni-img-size-base;
}
.uni-list--sm {
height: $uni-img-size-sm;
width: $uni-img-size-sm;
}
.uni-list-item__extra-text {
color: $uni-text-color-grey;
font-size: $uni-font-size-sm;
}
.uni-ellipsis-1 {
/* #ifndef APP-NVUE */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
text-overflow: ellipsis;
/* #endif */
}
.uni-ellipsis-2 {
/* #ifndef APP-NVUE */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* #endif */
/* #ifdef APP-NVUE */
lines: 2;
text-overflow: ellipsis;
/* #endif */
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<!-- #ifndef APP-NVUE -->
<view class="uni-list uni-border-top-bottom">
<view v-if="border" class="uni-list--border-top"></view>
<slot />
<view v-if="border" class="uni-list--border-bottom"></view>
</view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<list :bounce="false" :scrollable="true" show-scrollbar :render-reverse="renderReverse" @scroll="scroll" class="uni-list" :class="{ 'uni-list--border': border }" :enableBackToTop="enableBackToTop"
loadmoreoffset="15">
<slot />
</list>
<!-- #endif -->
</template>
<script>
/**
* List 列表
* @description 列表组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=24
* @property {String} border = [true|false] 标题
*/
export default {
name: 'uniList',
'mp-weixin': {
options: {
multipleSlots: false
}
},
props: {
stackFromEnd:{
type: Boolean,
default:false
},
enableBackToTop: {
type: [Boolean, String],
default: false
},
scrollY: {
type: [Boolean, String],
default: false
},
border: {
type: Boolean,
default: true
},
renderReverse:{
type: Boolean,
default: false
}
},
// provide() {
// return {
// list: this
// };
// },
created() {
this.firstChildAppend = false;
},
methods: {
loadMore(e) {
this.$emit('scrolltolower');
},
scroll(e) {
this.$emit('scroll', e);
}
}
};
</script>
<style lang="scss">
$uni-bg-color:#ffffff;
$uni-border-color:#e5e5e5;
.uni-list {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
background-color: $uni-bg-color;
position: relative;
flex-direction: column;
}
.uni-list--border {
position: relative;
/* #ifdef APP-NVUE */
border-top-color: $uni-border-color;
border-top-style: solid;
border-top-width: 0.5px;
border-bottom-color: $uni-border-color;
border-bottom-style: solid;
border-bottom-width: 0.5px;
/* #endif */
z-index: -1;
}
/* #ifndef APP-NVUE */
.uni-list--border-top {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: $uni-border-color;
z-index: 1;
}
.uni-list--border-bottom {
position: absolute;
bottom: 0;
right: 0;
left: 0;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: $uni-border-color;
}
/* #endif */
</style>

View File

@@ -0,0 +1,65 @@
<template>
<!-- #ifdef APP-NVUE -->
<refresh :display="display" @refresh="onrefresh" @pullingdown="onpullingdown">
<slot />
</refresh>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<view ref="uni-refresh" class="uni-refresh" v-show="isShow">
<slot />
</view>
<!-- #endif -->
</template>
<script>
export default {
name: 'UniRefresh',
props: {
display: {
type: [String],
default: "hide"
}
},
data() {
return {
pulling: false
}
},
computed: {
isShow() {
if (this.display === "show" || this.pulling === true) {
return true;
}
return false;
}
},
created() {},
methods: {
onchange(value) {
this.pulling = value;
},
onrefresh(e) {
this.$emit("refresh", e);
},
onpullingdown(e) {
// #ifdef APP-NVUE
this.$emit("pullingdown", e);
// #endif
// #ifndef APP-NVUE
var detail = {
viewHeight: 90,
pullingDistance: e.height
}
this.$emit("pullingdown", detail);
// #endif
}
}
}
</script>
<style>
.uni-refresh {
height: 0;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,87 @@
var pullDown = {
threshold: 95,
maxHeight: 200,
callRefresh: 'onrefresh',
callPullingDown: 'onpullingdown',
refreshSelector: '.uni-refresh'
};
function ready(newValue, oldValue, ownerInstance, instance) {
var state = instance.getState()
state.canPullDown = newValue;
// console.log(newValue);
}
function touchStart(e, instance) {
var state = instance.getState();
state.refreshInstance = instance.selectComponent(pullDown.refreshSelector);
state.canPullDown = (state.refreshInstance != null && state.refreshInstance != undefined);
if (!state.canPullDown) {
return
}
// console.log("touchStart");
state.height = 0;
state.touchStartY = e.touches[0].pageY || e.changedTouches[0].pageY;
state.refreshInstance.setStyle({
'height': 0
});
state.refreshInstance.callMethod("onchange", true);
}
function touchMove(e, ownerInstance) {
var instance = e.instance;
var state = instance.getState();
if (!state.canPullDown) {
return
}
var oldHeight = state.height;
var endY = e.touches[0].pageY || e.changedTouches[0].pageY;
var height = endY - state.touchStartY;
if (height > pullDown.maxHeight) {
return;
}
var refreshInstance = state.refreshInstance;
refreshInstance.setStyle({
'height': height + 'px'
});
height = height < pullDown.maxHeight ? height : pullDown.maxHeight;
state.height = height;
refreshInstance.callMethod(pullDown.callPullingDown, {
height: height
});
}
function touchEnd(e, ownerInstance) {
var state = e.instance.getState();
if (!state.canPullDown) {
return
}
state.refreshInstance.callMethod("onchange", false);
var refreshInstance = state.refreshInstance;
if (state.height > pullDown.threshold) {
refreshInstance.callMethod(pullDown.callRefresh);
return;
}
refreshInstance.setStyle({
'height': 0
});
}
function propObserver(newValue, oldValue, instance) {
pullDown = newValue;
}
module.exports = {
touchmove: touchMove,
touchstart: touchStart,
touchend: touchEnd,
propObserver: propObserver
}

View File

@@ -0,0 +1,108 @@
{
"id": "uni-list",
"displayName": "uni-list 列表",
"version": "1.2.17",
"description": "List 组件 ,帮助使用者快速构建列表。",
"keywords": [
"",
"uni-ui",
"uniui",
"列表",
"",
"list"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "",
"uni-app": "^4.08",
"uni-app-x": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [
"uni-badge",
"uni-icons"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "-",
"android": "√",
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "√",
"toutiao": "√",
"baidu": "√",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "√",
"lark": "-"
},
"quickapp": {
"huawei": "√",
"union": "√"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
## List 列表
> **组件名uni-list**
> 代码块: `uList`、`uListItem`
> 关联组件:`uni-list-item`、`uni-badge`、`uni-icons`、`uni-list-chat`、`uni-list-ad`
List 列表组件,包含基本列表样式、可扩展插槽机制、长列表性能优化、多端兼容。
在vue页面里它默认使用页面级滚动。在app-nvue页面里它默认使用原生list组件滚动。这样的长列表在滚动出屏幕外后系统会回收不可见区域的渲染内存资源不会造成滚动越长手机越卡的问题。
uni-list组件是父容器里面的核心是uni-list-item子组件它代表列表中的一个可重复行子组件可以无限循环。
uni-list-item有很多风格uni-list-item组件通过内置的属性满足一些常用的场景。当内置属性不满足需求时可以通过扩展插槽来自定义列表内容。
内置属性可以覆盖的场景包括:导航列表、设置列表、小图标列表、通信录列表、聊天记录列表。
涉及很多大图或丰富内容的列表,比如类今日头条的新闻列表、类淘宝的电商列表,需要通过扩展插槽实现。
下文均有样例给出。
uni-list不包含下拉刷新和上拉翻页。上拉翻页另见组件[uni-load-more](https://ext.dcloud.net.cn/plugin?id=29)
### [点击查看详细文档](https://uniapp.dcloud.io/component/uniui/uni-indexed-list)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839
## 基于uni-list扩展的页面模板
通过扩展插槽,可实现多种常见样式的列表
**新闻列表类**
1. 云端一体混合布局:[https://ext.dcloud.net.cn/plugin?id=2546](https://ext.dcloud.net.cn/plugin?id=2546)
2. 云端一体垂直布局,大图模式:[https://ext.dcloud.net.cn/plugin?id=2583](https://ext.dcloud.net.cn/plugin?id=2583)
3. 云端一体垂直布局,多行图文混排:[https://ext.dcloud.net.cn/plugin?id=2584](https://ext.dcloud.net.cn/plugin?id=2584)
4. 云端一体垂直布局,多图模式:[https://ext.dcloud.net.cn/plugin?id=2585](https://ext.dcloud.net.cn/plugin?id=2585)
5. 云端一体水平布局,左图右文:[https://ext.dcloud.net.cn/plugin?id=2586](https://ext.dcloud.net.cn/plugin?id=2586)
6. 云端一体水平布局,左文右图:[https://ext.dcloud.net.cn/plugin?id=2587](https://ext.dcloud.net.cn/plugin?id=2587)
7. 云端一体垂直布局,无图模式,主标题+副标题:[https://ext.dcloud.net.cn/plugin?id=2588](https://ext.dcloud.net.cn/plugin?id=2588)
**商品列表类**
1. 云端一体列表/宫格视图互切:[https://ext.dcloud.net.cn/plugin?id=2651](https://ext.dcloud.net.cn/plugin?id=2651)
2. 云端一体列表(宫格模式):[https://ext.dcloud.net.cn/plugin?id=2671](https://ext.dcloud.net.cn/plugin?id=2671)
3. 云端一体列表(列表模式):[https://ext.dcloud.net.cn/plugin?id=2672](https://ext.dcloud.net.cn/plugin?id=2672)