优化个人中心页面

This commit is contained in:
冯辉
2025-11-10 15:27:34 +08:00
parent 1fcfcb0475
commit 1a204549c4
38 changed files with 3017 additions and 618 deletions

View File

@@ -22,157 +22,180 @@
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="title-ri">
<text style="color: #256bfa">1</text>
<text>/2</text>
</view>
<!-- 固定标题 -->
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="content-input" :class="{ 'input-error': nameError }">
<view class="input-titile">姓名</view>
<input
class="input-con2"
v-model="fromValue.name"
maxlength="18"
placeholder="请输入姓名"
@input="validateName"
/>
<view v-if="nameError" class="error-message">{{ nameError }}</view>
<view class="title-ri">
<text style="color: #256bfa">1</text>
<text>/2</text>
</view>
<view class="content-sex" :class="{ 'input-error': sexError }">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 0 }"
@click="changeSex(0)"
>
</view>
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 1 }"
@click="changeSex(1)"
>
</view>
<!-- 可滚动内容区域 -->
<scroll-view class="scroll-content" scroll-y>
<view class="scroll-inner">
<view class="content-input" :class="{ 'input-error': nameError }">
<view class="input-titile">姓名</view>
<input
class="input-con2"
v-model="fromValue.name"
maxlength="18"
placeholder="请输入姓名"
@input="validateName"
/>
<view v-if="nameError" class="error-message">{{ nameError }}</view>
</view>
<view class="content-input" :class="{ 'input-error': passwordError }">
<view class="input-titile">密码</view>
<input
class="input-con2"
v-model="fromValue.ytjPassword"
type="password"
placeholder="请输入密码"
maxlength="8"
@input="validatePassword"
/>
<view v-if="passwordError" class="error-message">{{ passwordError }}</view>
<view v-if="fromValue.ytjPassword && !passwordError" class="success-message"> 密码格式正确</view>
</view>
<view class="content-sex" :class="{ 'input-error': sexError }">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 0 }"
@click="changeSex(0)"
>
</view>
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 1 }"
@click="changeSex(1)"
>
</view>
</view>
</view>
<view v-if="sexError" class="error-message">{{ sexError }}</view>
<view class="content-input" :class="{ 'input-error': ageError }">
<view class="input-titile">年龄</view>
<input
class="input-con2"
v-model="fromValue.age"
maxlength="3"
placeholder="请输入年龄"
@input="validateAge"
/>
<view v-if="ageError" class="error-message">{{ ageError }}</view>
</view>
<view class="content-input" :class="{ 'input-error': experienceError }" @click="changeExperience">
<view class="input-titile">工作经验</view>
<input
class="input-con"
v-model="state.workExperience"
disabled
placeholder="请选择您的工作经验"
/>
<view v-if="experienceError" class="error-message">{{ experienceError }}</view>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con" v-model="state.educationText" disabled placeholder="本科" />
</view>
<view class="content-input" :class="{ 'input-error': idCardError }">
<view class="input-titile">身份证</view>
<input
class="input-con2"
v-model="fromValue.idCard"
maxlength="18"
placeholder="请输入身份证号码"
@input="validateIdCard"
/>
<view v-if="idCardError" class="error-message">{{ idCardError }}</view>
<view v-if="fromValue.idCard && !idCardError" class="success-message"> 身份证格式正确</view>
</view>
</view>
<view v-if="sexError" class="error-message">{{ sexError }}</view>
<view class="content-input" :class="{ 'input-error': ageError }">
<view class="input-titile">年龄</view>
<input
class="input-con2"
v-model="fromValue.age"
maxlength="3"
placeholder="请输入年龄"
@input="validateAge"
/>
<view v-if="ageError" class="error-message">{{ ageError }}</view>
</view>
<view class="content-input" :class="{ 'input-error': experienceError }" @click="changeExperience">
<view class="input-titile">工作经验</view>
<input
class="input-con"
v-model="state.workExperience"
disabled
placeholder="请选择您的工作经验"
/>
<view v-if="experienceError" class="error-message">{{ experienceError }}</view>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con" v-model="state.educationText" disabled placeholder="本科" />
</view>
<view class="content-input" :class="{ 'input-error': idCardError }">
<view class="input-titile">身份证</view>
<input
class="input-con2"
v-model="fromValue.idCard"
maxlength="18"
placeholder="请输入身份证号码"
@input="validateIdCard"
/>
<view v-if="idCardError" class="error-message">{{ idCardError }}</view>
<view v-if="fromValue.idCard && !idCardError" class="success-message"> 身份证格式正确</view>
</view>
</view>
<view class="next-btn" @tap="nextStep">下一步</view>
</view>
</scroll-view>
<!-- 固定按钮 -->
<view class="next-btn" @tap="nextStep">下一步</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="title-ri">
<text style="color: #256bfa">2</text>
<text>/2</text>
</view>
<!-- 固定标题 -->
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="content-input" @click="changeArea">
<view class="input-titile">求职区域</view>
<input
class="input-con"
v-model="state.areaText"
disabled
placeholder="请选择您的求职区域"
/>
</view>
<view class="content-input" @click="changeJobs">
<view class="input-titile">求职岗位</view>
<input
class="input-con"
disabled
v-if="!state.jobsText.length"
placeholder="请选择您的求职岗位"
/>
<view class="input-nx" @click="changeJobs" v-else>
<view class="nx-item" v-for="item in state.jobsText">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSkillLevel">
<view class="input-titile">技能等级</view>
<input
class="input-con"
v-model="state.skillLevelText"
disabled
placeholder="请选择您的技能等级"
/>
</view>
<view class="content-input" @click="changeSkills">
<view class="input-titile">技能名称</view>
<input
class="input-con"
disabled
v-if="!state.skillsText.length"
placeholder="请选择您的技能名称"
/>
<view class="input-nx" @click="changeSkills" v-else>
<view class="nx-item" v-for="(item, index) in state.skillsText" :key="index">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSalay">
<view class="input-titile">期望薪资</view>
<input
class="input-con"
v-model="state.salayText"
disabled
placeholder="请选择您的期望薪资"
/>
<view class="title-ri">
<text style="color: #256bfa">2</text>
<text>/2</text>
</view>
</view>
<view class="next-btn" @tap="complete">开启求职之旅</view>
<!-- 可滚动内容区域 -->
<scroll-view class="scroll-content" scroll-y>
<view class="scroll-inner">
<view class="content-input" @click="changeArea">
<view class="input-titile">求职区域</view>
<input
class="input-con"
v-model="state.areaText"
disabled
placeholder="请选择您的求职区域"
/>
</view>
<view class="content-input" @click="changeJobs">
<view class="input-titile">求职岗位</view>
<input
class="input-con"
disabled
v-if="!state.jobsText.length"
placeholder="请选择您的求职岗位"
/>
<view class="input-nx" @click="changeJobs" v-else>
<view class="nx-item" v-for="(item, index) in state.jobsText" :key="index">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSkillLevel">
<view class="input-titile">技能等级</view>
<input
class="input-con"
v-model="state.skillLevelText"
disabled
placeholder="请选择您的技能等级"
/>
</view>
<view class="content-input" @click="changeSkills">
<view class="input-titile">技能名称</view>
<input
class="input-con"
disabled
v-if="!state.skillsText.length"
placeholder="请选择您的技能名称"
/>
<view class="input-nx" @click="changeSkills" v-else>
<view class="nx-item" v-for="(item, index) in state.skillsText" :key="index">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSalay">
<view class="input-titile">期望薪资</view>
<input
class="input-con"
v-model="state.salayText"
disabled
placeholder="请选择您的期望薪资"
/>
</view>
</view>
</swiper-item>
</scroll-view>
<!-- 固定按钮 -->
<view class="next-btn" @tap="complete">开启求职之旅</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
@@ -184,13 +207,14 @@
<script setup>
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import SelectPopup from '@/components/selectPopup/selectPopup.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { $api, navTo, config, IdCardValidator } = inject('globalFunction');
const { loginSetToken, getUserResume } = useUserStore();
const { getDictSelectOption, oneDictData } = useDictStore();
const dictStore = useDictStore();
const { getDictSelectOption, oneDictData, complete: dictComplete, getDictData } = dictStore;
// #ifdef H5
const injectedOpenSelectPopup = inject('openSelectPopup', null);
@@ -242,6 +266,7 @@ const fromValue = reactive({
age: '',
skillLevel: '',
skills: '',
ytjPassword: '',
});
// 输入校验相关
@@ -250,9 +275,73 @@ const nameError = ref('');
const ageError = ref('');
const sexError = ref('');
const experienceError = ref('');
const passwordError = ref('');
onLoad((parmas) => {
getTreeselect();
// 初始化学历显示文本
initEducationText();
});
// 初始化学历显示文本
function initEducationText() {
// 等待字典数据加载完成
const checkDictData = () => {
if (complete.value && dictStore.state.education && dictStore.state.education.length > 0) {
// 字典数据已加载,设置学历显示文本
if (fromValue.education) {
const educationValue = Number(fromValue.education);
state.educationText = dictStore.dictLabel('education', educationValue);
}
} else {
// 如果字典数据未加载,等待一段时间后重试
setTimeout(() => {
if (complete.value && dictStore.state.education && dictStore.state.education.length > 0) {
if (fromValue.education) {
const educationValue = Number(fromValue.education);
state.educationText = dictStore.dictLabel('education', educationValue);
}
} else {
// 如果还是未加载,尝试加载字典数据
getDictSelectOption('education').then((data) => {
dictStore.state.education = data;
if (fromValue.education) {
const educationValue = Number(fromValue.education);
state.educationText = dictStore.dictLabel('education', educationValue);
}
}).catch((error) => {
console.error('加载学历字典数据失败:', error);
});
}
}, 500);
}
};
checkDictData();
}
// 技能选择回调函数
const handleSkillSelected = (skills) => {
if (Array.isArray(skills) && skills.length > 0) {
// 更新技能显示和值技能字段值传name
state.skillsText = skills;
fromValue.skills = skills.join(',');
} else {
// 如果返回空数组,清空选择
state.skillsText = [];
fromValue.skills = '';
}
};
// 监听页面显示,接收从技能查询页面返回的数据
onShow(() => {
// 通过事件总线接收技能选择结果
uni.$on('skillSelected', handleSkillSelected);
});
// 页面卸载时移除事件监听
onUnmounted(() => {
uni.$off('skillSelected', handleSkillSelected);
});
onMounted(() => {});
@@ -300,15 +389,72 @@ function validateIdCard() {
return;
}
// 使用身份证校验器进行校验
const result = IdCardValidator.validate(idCard);
if (result.valid) {
idCardError.value = '';
// 检查IdCardValidator是否存在如果不存在提供简单的替代验证
if (IdCardValidator && typeof IdCardValidator.validate === 'function') {
const result = IdCardValidator.validate(idCard);
if (result.valid) {
idCardError.value = '';
} else {
idCardError.value = result.message;
}
} else {
idCardError.value = result.message;
// 简单的身份证验证规则18位最后一位可以是X
const idCardRegex = /(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
if (idCardRegex.test(idCard)) {
idCardError.value = '';
} else {
idCardError.value = '身份证号码格式不正确';
}
}
}
// 密码实时校验
function validatePassword() {
const password = (fromValue.ytjPassword || '').trim();
// 如果为空,清除错误信息
if (!password) {
passwordError.value = '';
return;
}
// 校验规则长度8位包含大小写字母和数字至少各有一个
if (password.length !== 8) {
passwordError.value = '密码长度必须为8位';
return;
}
// 检查是否包含大写字母
const hasUpperCase = /[A-Z]/.test(password);
// 检查是否包含小写字母
const hasLowerCase = /[a-z]/.test(password);
// 检查是否包含数字
const hasNumber = /[0-9]/.test(password);
if (!hasUpperCase) {
passwordError.value = '密码必须包含至少一个大写字母';
return;
}
if (!hasLowerCase) {
passwordError.value = '密码必须包含至少一个小写字母';
return;
}
if (!hasNumber) {
passwordError.value = '密码必须包含至少一个数字';
return;
}
// 检查是否只包含字母和数字
if (!/^[A-Za-z0-9]+$/.test(password)) {
passwordError.value = '密码只能包含大小写字母和数字';
return;
}
// 校验通过
passwordError.value = '';
}
function changeExperience() {
openSelectPopup({
title: '工作经验',
@@ -327,13 +473,51 @@ function changeExperience() {
});
}
function changeEducation() {
async function changeEducation() {
// 确保字典数据已加载
if (!complete.value || !dictStore.state.education || dictStore.state.education.length === 0) {
// 如果字典数据未加载,先加载数据
try {
await getDictSelectOption('education').then((data) => {
dictStore.state.education = data;
});
} catch (error) {
console.error('加载学历字典数据失败:', error);
}
}
// 等待数据加载完成后再获取数据
let educationData = oneDictData('education');
// 如果数据还是为空,等待一下再试
if (!educationData || educationData.length === 0) {
// 使用 nextTick 确保数据已渲染
await new Promise(resolve => setTimeout(resolve, 100));
educationData = oneDictData('education');
if (!educationData || educationData.length === 0) {
$api.msg('学历数据加载中,请稍后再试');
return;
}
}
// 将当前学历值转换为数字类型,用于查找默认索引
const currentEducation = fromValue.education ? Number(fromValue.education) : null;
// 查找当前学历在数据中的索引
let defaultIndex = [0];
if (currentEducation !== null && educationData && educationData.length > 0) {
const index = educationData.findIndex(item => Number(item.value) === currentEducation);
if (index >= 0) {
defaultIndex = [index];
}
}
openSelectPopup({
title: '学历',
maskClick: true,
data: [oneDictData('education')],
data: [educationData],
defaultIndex: defaultIndex,
success: (_, [value]) => {
fromValue.area = value.value;
fromValue.education = String(value.value); // 确保存储为字符串
state.educationText = value.label;
},
});
@@ -405,93 +589,12 @@ function changeSkillLevel() {
});
}
// 技能名称选择
// 技能名称选择 - 跳转到模糊查询页面
function changeSkills() {
const skills = [
// 前端开发
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' },
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'jQuery', value: 'jquery' },
{ label: 'Bootstrap', value: 'bootstrap' },
{ label: 'Sass/Less', value: 'sass' },
{ label: 'Webpack', value: 'webpack' },
{ label: 'Vite', value: 'vite' },
// 后端开发
{ label: 'Java', value: 'java' },
{ label: 'Python', value: 'python' },
{ label: 'Node.js', value: 'nodejs' },
{ label: 'PHP', value: 'php' },
{ label: 'C#', value: 'csharp' },
{ label: 'Go', value: 'go' },
{ label: 'Ruby', value: 'ruby' },
{ label: 'Spring Boot', value: 'springboot' },
{ label: 'Django', value: 'django' },
{ label: 'Express', value: 'express' },
{ label: 'Laravel', value: 'laravel' },
// 数据库
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'postgresql' },
{ label: 'MongoDB', value: 'mongodb' },
{ label: 'Redis', value: 'redis' },
{ label: 'Oracle', value: 'oracle' },
{ label: 'SQL Server', value: 'sqlserver' },
// 移动开发
{ label: 'React Native', value: 'reactnative' },
{ label: 'Flutter', value: 'flutter' },
{ label: 'iOS开发', value: 'ios' },
{ label: 'Android开发', value: 'android' },
{ label: '微信小程序', value: 'miniprogram' },
{ label: 'uni-app', value: 'uniapp' },
// 云计算与运维
{ label: 'Docker', value: 'docker' },
{ label: 'Kubernetes', value: 'kubernetes' },
{ label: 'AWS', value: 'aws' },
{ label: '阿里云', value: 'aliyun' },
{ label: 'Linux', value: 'linux' },
{ label: 'Nginx', value: 'nginx' },
// 设计工具
{ label: 'Photoshop', value: 'photoshop' },
{ label: 'Figma', value: 'figma' },
{ label: 'Sketch', value: 'sketch' },
{ label: 'Adobe XD', value: 'adobexd' },
// 其他技能
{ label: 'Git', value: 'git' },
{ label: 'Jenkins', value: 'jenkins' },
{ label: 'Jira', value: 'jira' },
{ label: '项目管理', value: 'projectmanagement' },
{ label: '数据分析', value: 'dataanalysis' },
{ label: '人工智能', value: 'ai' },
{ label: '机器学习', value: 'machinelearning' }
];
// 获取当前已选中的技能
const currentSelectedValues = fromValue.skills ? fromValue.skills.split(',') : [];
openSelectPopup({
title: '技能名称',
maskClick: true,
data: [skills],
multiSelect: true,
rowLabel: 'label',
rowKey: 'value',
defaultValues: currentSelectedValues,
success: (selectedValues, selectedItems) => {
const selectedSkills = selectedItems.map(item => item.value);
const selectedLabels = selectedItems.map(item => item.label);
fromValue.skills = selectedSkills.join(',');
state.skillsText = selectedLabels;
},
// 将当前已选中的技能名称传递给查询页面
const selectedSkills = state.skillsText || [];
uni.navigateTo({
url: `/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify(selectedSkills))}`
});
}
@@ -500,6 +603,7 @@ function nextStep() {
validateName();
validateAge();
validateIdCard();
validatePassword();
if (fromValue.sex !== 0 && fromValue.sex !== 1) {
sexError.value = '请选择性别';
@@ -518,6 +622,16 @@ function nextStep() {
return;
}
// 密码校验
if (!fromValue.ytjPassword) {
$api.msg('请输入密码');
return;
}
if (passwordError.value) {
$api.msg(passwordError.value);
return;
}
// 检查所有错误状态
if (nameError.value) return;
if (sexError.value) return;
@@ -528,10 +642,23 @@ function nextStep() {
return;
}
const result = IdCardValidator.validate(fromValue.idCard);
if (!result.valid) {
$api.msg(result.message);
return;
// 检查IdCardValidator是否存在如果不存在提供简单的替代验证
let isValid = false;
if (IdCardValidator && typeof IdCardValidator.validate === 'function') {
const result = IdCardValidator.validate(fromValue.idCard);
isValid = result.valid;
if (!isValid) {
$api.msg(result.message);
return;
}
} else {
// 简单的身份证验证规则18位最后一位可以是X
const idCardRegex = /(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
if (!idCardRegex.test(fromValue.idCard)) {
$api.msg('身份证号码格式不正确');
return;
}
isValid = true;
}
tabCurrent.value += 1;
@@ -579,8 +706,26 @@ function loginTest() {
}
function complete() {
const result = IdCardValidator.validate(fromValue.idCard);
if (result.valid) {
// 检查IdCardValidator是否存在如果不存在提供简单的替代验证
let isValid = false;
if (IdCardValidator && typeof IdCardValidator.validate === 'function') {
const result = IdCardValidator.validate(fromValue.idCard);
isValid = result.valid;
if (!isValid) {
$api.msg('身份证号码格式不正确');
return;
}
} else {
// 简单的身份证验证规则18位最后一位可以是X
const idCardRegex = /(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
if (!idCardRegex.test(fromValue.idCard)) {
$api.msg('身份证号码格式不正确');
return;
}
isValid = true;
}
if (isValid) {
// 构建 experiencesList 数组
const experiencesList = [];
if (fromValue.skills && fromValue.skillLevel) {
@@ -608,7 +753,8 @@ function complete() {
area: fromValue.area,
jobTitleId: fromValue.jobTitleId,
salaryMin: fromValue.salaryMin,
salaryMax: fromValue.salaryMax
salaryMax: fromValue.salaryMax,
ytjPassword: fromValue.ytjPassword
},
experiencesList: experiencesList
};
@@ -739,16 +885,17 @@ function complete() {
margin-top: 70rpx;
text-align: center;
.content-one
padding: 60rpx 28rpx;
padding: 0;
display: flex;
flex-direction: column;
justify-content: space-between
height: calc(100% - 120rpx)
height: 100%
.content-title
flex-shrink: 0
padding: 60rpx 28rpx 0
display: flex
justify-content: space-between;
align-items: center
margin-bottom: 70rpx
margin-bottom: 40rpx
.title-lf
font-size: 44rpx;
color: #000000;
@@ -841,9 +988,18 @@ function complete() {
color: #256BFA
background: rgba(37,107,250,0.1);
border: 2rpx solid #256BFA;
.scroll-content
flex: 1
overflow: hidden
height: 0 // 关键让flex布局正确计算高度
.scroll-inner
padding: 0 28rpx
padding-bottom: 40rpx
.next-btn
width: 100%;
flex-shrink: 0
width: calc(100% - 56rpx);
height: 90rpx;
margin: 0 28rpx 40rpx;
background: #256BFA;
border-radius: 12rpx 12rpx 12rpx 12rpx;
font-weight: 500;

View File

@@ -0,0 +1,388 @@
<template>
<AppLayout>
<view class="skill-search-container">
<!-- 固定顶部区域 -->
<view class="fixed-header">
<!-- 搜索框 -->
<view class="search-box">
<input
class="search-input"
v-model="searchKeyword"
placeholder="请输入技能名称进行搜索"
@input="handleSearch"
confirm-type="search"
/>
<view class="search-icon" @click="handleSearch">搜索</view>
</view>
<!-- 已选技能提示 -->
<view class="selected-tip" v-if="selectedSkills.length > 0">
已选择 {{ selectedSkills.length }}/3 个技能
</view>
</view>
<!-- 可滚动内容区域 -->
<scroll-view class="scroll-content" scroll-y>
<view class="scroll-inner">
<!-- 搜索结果列表 -->
<view class="result-list" v-if="searchResults.length > 0">
<view
class="result-item"
v-for="(item, index) in searchResults"
:key="index"
@click="toggleSelect(item)"
>
<view class="item-content">
<text class="item-name">{{ item.name }}</text>
</view>
<view
class="item-checkbox"
:class="{ 'checked': isSelected(item) }"
>
<text v-if="isSelected(item)" class="check-icon"></text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!isSearching && searchResults.length === 0 && searchKeyword">
<text class="empty-text">未找到相关技能</text>
</view>
<!-- 初始提示 -->
<view class="empty-state" v-if="!isSearching && searchResults.length === 0 && !searchKeyword">
<text class="empty-text">请输入技能名称进行搜索</text>
</view>
</view>
</scroll-view>
<!-- 固定底部操作栏 -->
<view class="bottom-bar">
<view class="selected-skills" v-if="selectedSkills.length > 0">
<view
class="skill-tag"
v-for="(skill, index) in selectedSkills"
:key="index"
>
<text class="tag-text">{{ skill }}</text>
<text class="tag-close" @click.stop="removeSkill(index)">×</text>
</view>
</view>
<view class="action-buttons">
<button class="btn-cancel" @click="handleCancel">取消</button>
<button
class="btn-confirm"
:class="{ 'disabled': selectedSkills.length === 0 }"
@click="handleConfirm"
:disabled="selectedSkills.length === 0"
>
确定
</button>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { inject } from 'vue';
const { $api } = inject('globalFunction');
const searchKeyword = ref('');
const searchResults = ref([]);
const selectedSkills = ref([]);
const isSearching = ref(false);
let searchTimer = null;
onLoad((options) => {
// 接收已选中的技能
if (options.selected) {
try {
const skills = JSON.parse(decodeURIComponent(options.selected));
if (Array.isArray(skills)) {
selectedSkills.value = skills;
}
} catch (e) {
console.error('解析已选技能失败:', e);
}
}
});
// 搜索处理(防抖)
function handleSearch() {
const keyword = searchKeyword.value.trim();
if (!keyword) {
searchResults.value = [];
return;
}
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer);
}
// 防抖处理500ms后执行搜索
searchTimer = setTimeout(() => {
performSearch(keyword);
}, 500);
}
// 执行搜索
async function performSearch(keyword) {
if (!keyword.trim()) {
searchResults.value = [];
return;
}
isSearching.value = true;
try {
const response = await $api.createRequest('/cms/dict/jobCategory', { name: keyword }, 'GET');
// 处理接口返回的数据,支持多种可能的返回格式
let results = [];
if (response) {
if (Array.isArray(response)) {
// 如果直接返回数组
results = response;
} else if (response.data) {
// 如果返回 { data: [...] }
results = Array.isArray(response.data) ? response.data : [];
} else if (response.list) {
// 如果返回 { list: [...] }
results = Array.isArray(response.list) ? response.list : [];
}
}
// 确保每个结果都有name字段
searchResults.value = results.map(item => {
if (typeof item === 'string') {
return { name: item };
}
return item;
});
} catch (error) {
console.error('搜索技能失败:', error);
$api.msg('搜索失败,请稍后重试');
searchResults.value = [];
} finally {
isSearching.value = false;
}
}
// 判断技能是否已选中
function isSelected(item) {
return selectedSkills.value.includes(item.name);
}
// 切换技能选择状态
function toggleSelect(item) {
const skillName = item.name;
const index = selectedSkills.value.indexOf(skillName);
if (index > -1) {
// 已选中,取消选择
selectedSkills.value.splice(index, 1);
} else {
// 未选中,检查是否已达到最大数量
if (selectedSkills.value.length >= 3) {
$api.msg('最多只能选择3个技能');
return;
}
// 添加选择
selectedSkills.value.push(skillName);
}
}
// 移除技能
function removeSkill(index) {
selectedSkills.value.splice(index, 1);
}
// 取消操作
function handleCancel() {
uni.navigateBack();
}
// 确定操作
function handleConfirm() {
if (selectedSkills.value.length === 0) {
$api.msg('请至少选择一个技能');
return;
}
// 通过事件总线传递选中的技能技能字段值传name
uni.$emit('skillSelected', selectedSkills.value);
// 返回上一页
uni.navigateBack();
}
onUnmounted(() => {
// 清理定时器
if (searchTimer) {
clearTimeout(searchTimer);
}
});
</script>
<style lang="stylus" scoped>
.skill-search-container
display: flex
flex-direction: column
height: 100%
background-color: #f5f5f5
.fixed-header
flex-shrink: 0
background-color: #fff
border-bottom: 2rpx solid #ebebeb
.search-box
display: flex
align-items: center
padding: 24rpx
background-color: #fff
.search-input
flex: 1
height: 72rpx
padding: 0 24rpx
background-color: #f5f5f5
border-radius: 36rpx
font-size: 28rpx
color: #333
.search-icon
margin-left: 24rpx
padding: 16rpx 32rpx
background-color: #256bfa
color: #fff
border-radius: 36rpx
font-size: 28rpx
.selected-tip
padding: 16rpx 24rpx
background-color: #fff3cd
color: #856404
font-size: 24rpx
text-align: center
.scroll-content
flex: 1
overflow: hidden
height: 0 // 关键让flex布局正确计算高度
background-color: #fff
.scroll-inner
padding-bottom: 40rpx
.result-list
background-color: #fff
padding: 0 24rpx
.result-item
display: flex
align-items: center
justify-content: space-between
padding: 32rpx 0
border-bottom: 2rpx solid #ebebeb
.item-content
flex: 1
.item-name
font-size: 32rpx
color: #333
.item-checkbox
width: 48rpx
height: 48rpx
border: 2rpx solid #ddd
border-radius: 50%
display: flex
align-items: center
justify-content: center
margin-left: 24rpx
&.checked
background-color: #256bfa
border-color: #256bfa
.check-icon
color: #fff
font-size: 32rpx
font-weight: bold
.empty-state
display: flex
align-items: center
justify-content: center
min-height: 400rpx
.empty-text
font-size: 28rpx
color: #999
.bottom-bar
flex-shrink: 0
background-color: #fff
border-top: 2rpx solid #ebebeb
padding: 24rpx
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05) // 添加阴影,让底部栏更明显
.selected-skills
display: flex
flex-wrap: wrap
margin-bottom: 24rpx
min-height: 60rpx
.skill-tag
display: inline-flex
align-items: center
padding: 12rpx 24rpx
margin-right: 16rpx
margin-bottom: 16rpx
background-color: #e8f4ff
border-radius: 32rpx
border: 2rpx solid #256bfa
.tag-text
font-size: 26rpx
color: #256bfa
margin-right: 12rpx
.tag-close
font-size: 32rpx
color: #256bfa
line-height: 1
cursor: pointer
.action-buttons
display: flex
gap: 24rpx
.btn-cancel, .btn-confirm
flex: 1
height: 88rpx
border-radius: 12rpx
font-size: 32rpx
border: none
.btn-cancel
background-color: #f5f5f5
color: #666
.btn-confirm
background-color: #256bfa
color: #fff
&.disabled
background-color: #ccc
color: #999
</style>

View File

@@ -1,272 +0,0 @@
<template>
<view class="demo-page">
<view class="demo-title">阿里图标库使用示例</view>
<!-- 方式一直接使用 iconfont -->
<view class="demo-section">
<view class="section-title">方式一直接使用</view>
<view class="icon-list">
<view class="icon-item">
<text class="iconfont icon-home"></text>
<text class="icon-name">icon-home</text>
</view>
<view class="icon-item">
<text class="iconfont icon-user"></text>
<text class="icon-name">icon-user</text>
</view>
<view class="icon-item">
<text class="iconfont icon-search"></text>
<text class="icon-name">icon-search</text>
</view>
</view>
</view>
<!-- 方式二使用封装的组件 -->
<view class="demo-section">
<view class="section-title">方式二使用封装组件</view>
<view class="icon-list">
<view class="icon-item">
<IconfontIcon name="home" :size="48" color="#13C57C" />
<text class="icon-name">绿色 48rpx</text>
</view>
<view class="icon-item">
<IconfontIcon name="user" :size="36" color="#256BFA" />
<text class="icon-name">蓝色 36rpx</text>
</view>
<view class="icon-item">
<IconfontIcon name="search" :size="32" color="#FF9800" />
<text class="icon-name">橙色 32rpx</text>
</view>
</view>
</view>
<!-- 方式三使用配置常量 -->
<view class="demo-section">
<view class="section-title">方式三使用配置常量</view>
<view class="icon-list">
<view class="icon-item">
<IconfontIcon
:name="ICONS.PHONE"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.PRIMARY"
/>
<text class="icon-name">电话</text>
</view>
<view class="icon-item">
<IconfontIcon
:name="ICONS.MESSAGE"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.SECONDARY"
/>
<text class="icon-name">消息</text>
</view>
<view class="icon-item">
<IconfontIcon
:name="ICONS.LOCATION"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.DANGER"
/>
<text class="icon-name">位置</text>
</view>
</view>
</view>
<!-- 方式四按钮中使用 -->
<view class="demo-section">
<view class="section-title">方式四在按钮中使用</view>
<button class="demo-button primary">
<IconfontIcon name="phone" :size="32" color="#FFFFFF" />
<text>手机号登录</text>
</button>
<button class="demo-button secondary">
<IconfontIcon name="user" :size="32" color="#256BFA" />
<text>个人中心</text>
</button>
<button class="demo-button success">
<IconfontIcon name="star" :size="32" color="#FFFFFF" />
<text>收藏职位</text>
</button>
</view>
<!-- 方式五列表中使用 -->
<view class="demo-section">
<view class="section-title">方式五在列表中使用</view>
<view class="demo-list">
<view class="list-item" v-for="item in menuList" :key="item.id">
<IconfontIcon :name="item.icon" :size="40" :color="item.color" />
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-desc">{{ item.desc }}</view>
</view>
<IconfontIcon name="arrow-right" :size="28" color="#999" />
</view>
</view>
</view>
<!-- 注意事项 -->
<view class="demo-section">
<view class="section-title"> 注意事项</view>
<view class="tips-box">
<view class="tip-item">1. 确保已从阿里图标库下载字体文件到 static/iconfont/ 目录</view>
<view class="tip-item">2. 确保已在 App.vue 中引入 iconfont.css</view>
<view class="tip-item">3. 图标名称需要与阿里图标库中的类名保持一致</view>
<view class="tip-item">4. 推荐使用封装的 IconfontIcon 组件便于统一管理</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
import { ICONS, ICON_SIZES, ICON_COLORS } from '@/config/icons'
const menuList = ref([
{
id: 1,
icon: 'home',
title: '首页',
desc: '查看推荐职位',
color: '#13C57C'
},
{
id: 2,
icon: 'search',
title: '搜索',
desc: '搜索心仪职位',
color: '#256BFA'
},
{
id: 3,
icon: 'message',
title: '消息',
desc: '查看聊天消息',
color: '#FF9800'
},
{
id: 4,
icon: 'user',
title: '我的',
desc: '个人中心',
color: '#9C27B0'
}
])
</script>
<style lang="stylus" scoped>
.demo-page
padding: 40rpx
background: #F5F5F5
min-height: 100vh
.demo-title
font-size: 48rpx
font-weight: bold
color: #333
text-align: center
margin-bottom: 40rpx
.demo-section
background: #FFFFFF
border-radius: 16rpx
padding: 32rpx
margin-bottom: 32rpx
.section-title
font-size: 32rpx
font-weight: 600
color: #333
margin-bottom: 24rpx
padding-bottom: 16rpx
border-bottom: 2rpx solid #F0F0F0
.icon-list
display: flex
flex-wrap: wrap
gap: 32rpx
.icon-item
display: flex
flex-direction: column
align-items: center
gap: 12rpx
min-width: 120rpx
.iconfont
font-size: 48rpx
color: #333
.icon-name
font-size: 24rpx
color: #666
text-align: center
.demo-button
width: 100%
height: 88rpx
border-radius: 44rpx
display: flex
align-items: center
justify-content: center
gap: 12rpx
font-size: 32rpx
margin-bottom: 20rpx
border: none
&.primary
background: linear-gradient(135deg, #13C57C 0%, #0FA368 100%)
color: #FFFFFF
&.secondary
background: #F7F8FA
color: #256BFA
&.success
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%)
color: #FFFFFF
.demo-list
display: flex
flex-direction: column
gap: 20rpx
.list-item
display: flex
align-items: center
padding: 24rpx
background: #F7F8FA
border-radius: 12rpx
gap: 20rpx
.item-content
flex: 1
.item-title
font-size: 30rpx
color: #333
font-weight: 500
margin-bottom: 8rpx
.item-desc
font-size: 24rpx
color: #999
.tips-box
padding: 24rpx
background: #FFF3E0
border-radius: 12rpx
.tip-item
font-size: 26rpx
color: #E65100
line-height: 1.8
margin-bottom: 12rpx
&:last-child
margin-bottom: 0
// 按钮重置样式
button::after
border: none
</style>