60 Commits

Author SHA1 Message Date
lip
9cf00a1449 岗位模块切换 2025-11-04 11:52:58 +08:00
0b3c32348b 修改 2025-11-04 10:33:49 +08:00
lip
78d3bd8eb3 上班之后确认下实习实训和社区实践 2025-11-04 08:35:50 +08:00
lip
0787fc6b44 人才首页接口对接 2025-11-03 18:58:44 +08:00
262f398772 Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-11-03 17:48:18 +08:00
3a1bd54878 修改页面样式等 2025-11-03 17:48:14 +08:00
2fbd8ade5b 招聘会模块 2025-11-03 15:08:39 +08:00
4ba6539850 修改功能 2025-11-03 14:37:07 +08:00
lip
33a33bb7b0 添加跳转 2025-11-03 13:26:16 +08:00
lip
a1b37ba39a 修改bug 2025-11-03 13:09:32 +08:00
lip
8c25a3ab4f 更改地址使平台可以正常启动 2025-11-03 13:00:49 +08:00
lip
22cb414232 合并代码更改可以正常启动 2025-11-03 12:55:41 +08:00
ec01a81c64 删除无用代码 2025-11-03 12:33:46 +08:00
1c6d5c7a15 合并人才集团代码 2025-11-03 12:30:37 +08:00
233e5fecdc 图片基础url 2025-11-03 09:19:46 +08:00
e24901222d 图片基础url修改 2025-11-03 09:11:55 +08:00
4c40e609c1 模拟考试列表 2025-11-01 17:20:31 +08:00
ef0a65c09c 专项练习 2025-11-01 15:50:25 +08:00
361afe206c 修改 2025-10-31 17:49:09 +08:00
3160dad246 修改 2025-10-31 17:47:51 +08:00
7ffab64a82 修改 2025-10-31 17:46:14 +08:00
5a70129c70 修改 2025-10-31 17:39:46 +08:00
45c59a2d36 修改 2025-10-31 17:36:33 +08:00
cc0d8a2f70 视频详情 2025-10-31 17:31:54 +08:00
b963268639 Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-10-31 17:28:33 +08:00
dc96d856a2 删除unpackage 2025-10-31 17:28:29 +08:00
9a6ad76000 招聘会模块 2025-10-31 17:24:07 +08:00
99d932a453 视频学习 2025-10-31 17:18:43 +08:00
1b1b5f7ac7 培训首页 2025-10-31 11:14:45 +08:00
c98f54ade6 专项练习 2025-10-31 10:18:44 +08:00
2cc5d98763 Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-10-31 09:28:27 +08:00
lip
438a6f3938 t添加高校毕业生模块 2025-10-30 20:38:17 +08:00
lip
05fd135b3c 添加新图标 2025-10-30 20:32:31 +08:00
25fb1fa2f1 培训 2025-10-30 20:26:34 +08:00
9c22cc4444 configjs回滚 2025-10-30 19:34:57 +08:00
4a1aa68245 添加后端请求接口
分包packageB
2025-10-30 17:09:26 +08:00
冯辉
95b9c1cc29 111 2025-10-30 15:34:42 +08:00
冯辉
e048ba6b0b 企业详细信息页面编辑联系人功能开发 2025-10-27 13:35:02 +08:00
冯辉
55cc52c046 111 2025-10-27 12:39:33 +08:00
冯辉
c71ad5f98c bug修复 2025-10-24 18:49:03 +08:00
冯辉
476e44f400 111 2025-10-24 18:10:16 +08:00
冯辉
d80baf6f0b 首页岗位列表接口调用两次的问题修改 2025-10-24 17:53:53 +08:00
冯辉
5a31d56c9a 首页切换导航bug修复 2025-10-24 17:37:38 +08:00
冯辉
12906b65ec 发布岗位回显企业名称 2025-10-24 17:30:19 +08:00
冯辉
2d6370b796 首页卡片开发 2025-10-24 16:54:52 +08:00
冯辉
92ee5c5311 企业我的页面开发 2025-10-24 11:34:11 +08:00
冯辉
0b339ee061 发布职位网格员模糊查询企业开发 2025-10-23 18:30:40 +08:00
冯辉
4450aa21bc Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-10-23 17:16:20 +08:00
冯辉
b72b7dd7d6 岗位发布开发 2025-10-23 17:16:16 +08:00
3e58a71658 删除功能 2025-10-23 14:45:08 +08:00
d605e940ee Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-10-22 14:23:56 +08:00
e287209dcf 静态页面 2025-10-22 14:23:38 +08:00
冯辉
20f2038f8c 接口乱码问题修改 2025-10-22 13:22:06 +08:00
冯辉
4f94090b42 企业信息补全接口联调 2025-10-22 13:15:10 +08:00
冯辉
8bb3c424e2 企业信息补全页面开发 2025-10-21 22:58:47 +08:00
4d87af9c3c 新增静态页面 2025-10-21 17:42:35 +08:00
冯辉
968e6b4091 阿里图标库引入 2025-10-20 16:15:29 +08:00
冯辉
d9c1f83693 微信授权登录功能开发联调 2025-10-20 11:43:44 +08:00
冯辉
ae91ded327 微信登录调试 2025-10-20 11:05:11 +08:00
冯辉
959e9ee9e4 登录注册逻辑开发 2025-10-17 13:15:14 +08:00
294 changed files with 2823151 additions and 2798 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/unpackage/
/node_modules/

43
App.vue
View File

@@ -3,32 +3,37 @@ import { reactive, inject, onMounted } from 'vue';
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
import useUserStore from './stores/useUserStore';
import useDictStore from './stores/useDictStore';
import { tabbarManager } from './utils/tabbarManager';
const { $api, navTo, appendScriptTagElement } = inject('globalFunction');
import config from '@/config.js';
onLaunch((options) => {
useUserStore().initSeesionId(); //更新
useDictStore().getDictData();
// uni.hideTabBar();
// 登录
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
})
.catch(() => {
uni.redirectTo({
url: '/pages/login/login',
// 先尝试从缓存恢复用户信息
const restored = useUserStore().restoreUserInfo();
// 用户信息恢复后再初始化自定义tabbar
tabbarManager.initTabBar();
if (restored) {
// 如果成功恢复用户信息验证token是否有效
let token = uni.getStorageSync('token') || '';
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
console.log('用户登录状态已恢复');
})
.catch(() => {
// token无效清除缓存不跳转登录页
console.log('token已过期需要重新登录');
useUserStore().logOut(false);
});
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
}
}
// 不再强制跳转到登录页,而是在需要登录时弹出授权弹窗
});
onMounted(() => {
@@ -53,6 +58,8 @@ onHide(() => {
/*每个页面公共css */
@import '@/common/animation.css';
@import '@/common/common.css';
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
/* 修改pages tabbar样式 H5有效 */
.uni-tabbar .uni-tabbar__item:nth-child(4) .uni-tabbar__bd .uni-tabbar__icon {
height: 110rpx !important;
@@ -81,6 +88,7 @@ uni-modal,
font-display: swap;
}
/* #ifdef H5 */
@font-face {
font-family: PingFangSC-Regular;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Regular.woff2') format('woff2');
@@ -98,6 +106,7 @@ uni-modal,
src: url('https://qd.zhaopinzao8dian.com/file/csn/DIN-Medium.woff2') format('woff2');
font-display: swap;
}
/* #endif */
body {
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;

514
apiRc/company/index.js Normal file
View File

@@ -0,0 +1,514 @@
// 获取人员基本信息详情
// import { post, get } from '../../utils/request.js'
// export function getPersonInfo(id) {
// return get({
// url: `personnel/personBaseInfo/${id}`,
// method: 'get'
// })
// }
import request from '@/utilsRc/request'
// 根据 userId 获取企业详情
export function companyDetails(userId) {
return request({
method: 'get',
url: `/company/unitBaseInfo/user/${userId}`,
})
}
// 企业-推荐人员信息
export function recommendedPerson(params) {
return request({
url: '/company/unitBaseInfo/recommend/person',
method: 'get',
params
})
}
// 人员邀请
export function invitePerson(params) {
return request({
url: '/company/unitBaseInfo/invite',
method: 'get',
params
})
}
// 获取企业招聘岗位列表
export function jobList(params) {
return request({
url: '/company/unitPostInfo/list',
method: 'get',
params
})
}
// 查找已投递、已推荐、已邀请的人员信息
export function listMatch(query) {
return request({
url: '/company/unitBaseInfo/relevance',
method: 'get',
params: query
})
}
// 添加企业基本信息
export function addJobBase(data) {
return request({
url: '/company/unitBaseInfo',
method: 'post',
data: data
})
}
// 查询部门下拉树结构
export function deptTreeSelect() {
return request({
url: '/system/center/user/deptTree',
method: 'get'
})
}
// 企业发布招聘岗位
export function addJob(data) {
return request({
url: '/company/unitPostInfo',
method: 'post',
data: data
})
}
// 获取招聘工种列表
export function jobTypeList(params) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params
})
}
// 企业基本信息列表
export function jobBaseList(query) {
return request({
url: '/company/unitBaseInfo/list',
method: 'get',
params: query
})
}
// 获取企业招聘岗位信息详细信息
export function getJob(id) {
return request({
url: `/company/unitPostInfo/${id}`,
method: 'get'
})
}
// 修改企业招聘岗位信息
export function updateJob(data) {
return request({
url: '/company/unitPostInfo',
method: 'put',
data: data
})
}
// 修改企业基本信息
export function updateJobBase(data) {
return request({
url: '/company/unitBaseInfo',
method: 'put',
data: data
})
}
// 查询角色详细
export function getJobService(id) {
return request({
url: '/personnel/personBaseInfo/' + id,
method: 'get'
})
}
// 查询推荐人员、已推荐、已邀请 详情
export function getUnitBaseInfo(id) {
return request({
url: '/manage/personDemand/' + id,
method: 'get'
})
}
// 查询工种列表
export function listJobType(query) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params: query
})
}
// 人员基本信息 - 列表
export function personInfoList(query) {
return request({
url: '/personnel/personBaseInfo/list',
method: 'get',
params: query
})
}
// 获取人员基本信息详情
export function getPersonInfo(id) {
return request({
url: `/personnel/personBaseInfo/${id}`,
method: 'get'
})
}
// 删除人员基本信息
export function delPersonInfo(ids) {
return request({
url: '/personnel/personBaseInfo/' + ids,
method: 'delete'
})
}
// 删除录入人员 公用 type = 1 失业中 2 就业困难 3 离校生 4 其他人员
export function delPersonUser(ids) {
return request({
url: `/personnel/personInputInfo/${ids}`,
method: 'delete',
})
}
// 新增人员基本信息
export function addPersonInfo(data) {
return request({
url: '/personnel/personBaseInfo',
method: 'post',
data: data
})
}
// 修改人员基本信息
export function updatePersonInfo(data) {
return request({
url: '/personnel/personBaseInfo',
method: 'put',
data
})
}
//社区人员审核
export function personInfoAudit(data) {
return request({
url: '/personnel/personBaseInfo/audit',
method: 'post',
data: data
})
}
//记录查看身份证
export function recordLookIdCard(params) {
return request({
url: '/personnel/personBaseInfo/recordLookIdCard',
method: 'get',
params
})
}
/* 失业人员 --------------------------------------------- start */
// 新增失业人员
export function addPersonUnemployed(data) {
return request({
url: '/person/unemployment',
method: 'post',
data
})
}
// 失业人员修改
export function updatePersonUnemployed(data) {
return request({
url: '/person/unemployment',
method: 'put',
data,
})
}
// 失业人员列表
export function unemployment(params) {
return request({
url: '/person/unemployment/list',
method: 'get',
params
})
}
// 失业人员详情
export function unemploymentDetails(id) {
return request({
url: `/person/unemployment/${id}`,
method: 'get',
})
}
// 失业人员删除
export function unemploymentDelete(id) {
return request({
url: `/person/unemployment/${id}`,
method: 'delete',
})
}
/* 失业人员 --------------------------------------------- end */
/* 就业困难人员 --------------------------------------------- start */
// 新增就业困难
export function addPersonDifficult(data) {
return request({
url: '/person/findingEmployment',
method: 'post',
data
})
}
// 修改就业困难
export function updatePersonDifficult(data) {
return request({
url: '/person/findingEmployment',
method: 'put',
data
})
}
// 就业困难列表
export function findingEmployment(params) {
return request({
url: '/person/findingEmployment/list',
method: 'get',
params
})
}
// 就业困难详情
export function findingEmploymentDetails(id) {
return request({
url: `/person/findingEmployment/${id}`,
method: 'get',
})
}
// 就业困难删除
export function findingEmploymentDelete(id) {
return request({
url: `/person/findingEmployment/${id}`,
method: 'delete',
})
}
/* 就业困难人员 --------------------------------------------- end */
/* 离校未就业高校生 --------------------------------------------- start */
// 新增离校未就业高校生
export function addLeaveSchool(data) {
return request({
url: '/person/leavingSchoolInfo',
method: 'post',
data
})
}
// 修改离校未就业高校生
export function updateLeaveSchool(data) {
return request({
url: '/person/leavingSchoolInfo',
method: 'put',
data,
})
}
// 高校未就业列表
export function leavingSchoolInfo(params) {
return request({
url: '/person/leavingSchoolInfo/list',
method: 'get',
params
})
}
// 高校未就业详情
export function leavingSchoolInfoDetails(id) {
return request({
url: `/person/leavingSchoolInfo/${id}`,
method: 'get',
})
}
// 高校未就业删除
export function leavingSchoolInfoDelete(id) {
return request({
url: `/person/leavingSchoolInfo/${id}`,
method: 'delete',
})
}
/* 离校未就业高校生 --------------------------------------------- end */
/* 其他人员 --------------------------------------------- start */
// 新增其他人员
export function addOther(data) {
return request({
url: '/person/other',
method: 'post',
data
})
}
// 其他人员修改
export function updateOther(data) {
return request({
url: '/person/other',
method: 'post',
data,
})
}
// 其他人员列表
export function other(params) {
return request({
url: '/person/other/list',
method: 'get',
params
})
}
// 其他人员详情
export function otherDetails(id) {
return request({
url: `/person/other/${id}`,
method: 'get',
})
}
// 其他人员删除
export function otherDelete(id) {
return request({
url: `/person/other/${id}`,
method: 'delete',
})
}
/* 其他人员 --------------------------------------------- end */
// 需求预警列表
export function personAlertList(params) {
return request({
url: '/manage/personDemand/warningList',
method: 'get',
params
})
}
export function personDealList(params) {
return request({
url: '/manage/personDemand/dealingList',
method: 'get',
params
})
}
// 服务追踪 服务类型/服务id
export function serviceTraceability({
demandType,
id
}) {
return request({
// url: `/system/personRequirementsRecords/serviceTraceability/${demandType}/${id}`,
url: `/timelime/timelime/fwzs/${id}`,
method: 'get',
})
}
// 需求办结
export function requirementCompletion(url, data) {
return request({
url,
method: 'post',
data
})
}
//岗位审核
export function jobAudit(data) {
return request({
url: '/company/unitPostInfo/audit',
method: 'post',
data: data
})
}
//社群 首页未完成数
// export function getPeopleCount() {
// return request({
// url: '/pc/index/getPeopleCount',
// method: 'get',
// })
// }
//社群 首页未完成数
export function getDemandUnfinished() {
return request({
url: '/pc/index/todo',
method: 'get',
})
}
// 删除企业招聘岗位信息
export function delJob(ids) {
return request({
url: '/company/unitPostInfo/' + ids,
method: 'delete'
})
}
// 所在社区列表
export function deptList(params) {
return request({
'url': `/system/center/user/deptList`,
'method': 'get',
params
})
}
// 所在社区列表
export function returnPerson(params) {
return request({
'url': `/personnel/personBaseInfo/returnPerson`,
'method': 'get',
params
})
}
// 根据人的身份证查询人的详细信息
export function getIdNumberInfo(params) {
return request({
'url': `/personnel/personBaseInfo/getIdNumberInfo`,
'method': 'get',
params
})
}

15
apiRc/jobType/index.js Normal file
View File

@@ -0,0 +1,15 @@
/*
* @Date: 2025-10-31 11:06:15
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:48:22
*/
// import { post, get } from '@/utilsRc/request'
import request from '@/utilsRc/request'
export function listJobType(query) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params: query
})
}

53
apiRc/login.js Normal file
View File

@@ -0,0 +1,53 @@
/*
* @Date: 2025-10-31 11:06:15
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 15:51:28
*/
import request from '@/utilsRc/request'
// 登录方法
export function login(data) {
return request({
method: 'get',
url: '/not/login/person/zkrLogin',
params: data,
})
}
export function smsLogin(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}
export function wechatLogin(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}
export function register(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}
// 获取用户详细信息
export function getInfo() {
return request({
url: '/getInfo',
method: 'get'
})
}

34
apiRc/needs/person.js Normal file
View File

@@ -0,0 +1,34 @@
/*
* @Date: 2025-10-31 11:06:15
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:48:30
*/
// 人员接口
// import { post, get } from '@/utilsRc/request'
import request from '@/utilsRc/request'
export function getPersonBase(params) {
return request({
url: '/personnel/personBaseInfo/list',
method: 'get',
params
})
}
export function getPersonList(params) {
return request({
url: '/personnel/personBaseInfo/list',
method: 'get',
params
})
}
// 新增角色
export function addInvestigate(data) {
return request({
// url: '//process/processInterview',
url: '//timelime/timelime',
method: 'post',
data: data
})
}

View File

@@ -0,0 +1,52 @@
/*
* @Date: 2025-11-03 08:48:44
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:48:41
*/
// 查询个人需求信息列表
// import { post, get } from '@/utilsRc/request'
import request from '@/utilsRc/request'
export function listPersonDemand(query) {
return request({
method: 'get',
url: '/manage/personDemand/list',
params: query
})
}
export function delPersonDemand(id) {
return request({
url: '/manage/personDemand/' + id,
method: 'delete'
})
}
// 查询个人需求信息详细
export function getPersonDemand(id) {
return request({
method: 'get',
url: '/manage/personDemand/' + id,
})
}
// 新增个人需求信息
export function addPersonDemand(data) {
// 确保传递数据前进行日志输出
console.log('addPersonDemand函数接收到的数据:', data);
return request({
url: '/manage/personDemand',
method: 'post', // 修改为大写POST确保请求参数正确传递
data: data
})
}
// 修改个人需求信息
export function updatePersonDemand(data) {
return request({
url: '/manage/personDemand',
method: 'put',
data: data
})
}

84
apiRc/person.js Normal file
View File

@@ -0,0 +1,84 @@
/*
* @Date: 2025-10-31 13:50:15
* @LastEditors: shirlwang
* @LastEditTime: 2025-10-31 14:30:31
*/
import request from '@/utilsRc/request'
// 人员信息保存
export function savePersonBase(data) {
return request({
'url': '/personnel/personBaseInfo',
'method': 'put',
'data': data
})
}
// 人员信息查询
export function getPersonBase(userId) {
return request({
'url': `/personnel/personBaseInfo/user/${userId}`,
'method': 'get',
})
}
// 获取行政区划列表
export function getQUList() {
return request({
url: `/manage/xzqh//xzqhTree`,
method: 'get'
})
}
// 查询部门下拉树结构
export function deptTreeSelect() {
return request({
url: '/system/center/user/deptTree',
method: 'get'
})
}
// 社群端 根据所在社区 获取姓名
export function generateUserName(deptId) {
return request({
url: `/generateUserName/${deptId}`,
method: 'get'
})
}
// 获取部门列表
export function getDeptList(name,personId) {
return request({
url: `/system/center/user/getDeptList?name=${name}&parentId=${personId}`,
method: 'get'
})
}
// 求职工种列表
export function touristWork() {
return request({
url: `/basicdata/workType/workTypeTree`,
method: 'get'
})
}
// 获取招聘工种列表
export function jobTypeList(params) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params
})
}
// 未读消息数量
export function unreadNum() {
return request({
url: '/manage/tjgw/notReadNum',
method: 'get',
})
}
// 地图类型列表
export function jyshdt(cyfhjd) {
return request({
url: `/jyshdt/jyshdt/queryList?lx=${cyfhjd}`,
method: 'get',
})
}

49
apiRc/personinfo/index.js Normal file
View File

@@ -0,0 +1,49 @@
/*
* @Descripttion:
* @Author: lip
* @Date: 2025-11-03 12:35:56
* @LastEditors: lip
*/
// import { post, get } from '../../utils/request.js'
import request from '@/utilsRc/request'
// 登录方法
export function personInfoList(data) {
return request({
method: 'get',
url: '/personnel/personBaseInfo/list',
params: data,
})
}
// 需求预警列表
export function personAlertList(params) {
return request({
method: 'get',
url: '/manage/personDemand/warningList',
params
})
}
//经办人数据获取
export function getJbrInfo() {
return request({
method: 'get',
url: `system/center/user/selectHxjbr`,
method: 'get'
})
}
export function getPersonBase() {
return request({
method: 'get',
url: `system/center/user/selectHxjbr`,
method: 'get'
})
}
export function returnPerson(params) {
return request({
method: 'get',
'url': `/personnel/personBaseInfo/returnPerson`,
params
})
}

63
apiRc/system/dict.js Normal file
View File

@@ -0,0 +1,63 @@
/*
* @Date: 2025-10-31 15:06:34
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 12:20:28
*/
import request from '@/utilsRc/request'
// 查询字典数据列表
export function listData (query) {
return request({
url: '/system/dict/data/list',
method: 'get',
params: query
})
}
// 查询字典数据详细
export function getData (dictCode) {
return request({
url: '/system/dict/data/' + dictCode,
method: 'get'
})
}
// 根据字典类型查询字典数据信息
export function getDicts (dictType) {
return request({
url: '/system/dict/data/type/' + dictType,
method: 'get'
})
}
// 根据字典类型查询字典数据信息
export function getDict (dictType) {
return request({
url: '/system/dict/data/type/' + dictType,
method: 'get'
})
}
// 新增字典数据
export function addData (data) {
return request({
url: '/system/dict/data/add',
method: 'post',
data: data
})
}
// 修改字典数据
export function updateData (data) {
return request({
url: '/system/dict/data',
method: 'post',
data: data
})
}
// 删除字典数据
export function delData (dictCode) {
return request({
url: '/system/dict/data/remove' + dictCode,
method: 'get'
})
}

87
clean-build-cache.bat Normal file
View File

@@ -0,0 +1,87 @@
@echo off
REM 清理 uni-app 项目编译缓存脚本
echo ================================================
echo uni-app 项目缓存清理工具
echo ================================================
echo.
echo [提示] 此脚本将清理以下内容:
echo 1. unpackage 编译输出目录
echo 2. node_modules 依赖目录(如果需要)
echo 3. HBuilderX 临时文件
echo.
set /p confirm="确认清理缓存吗?(Y/N): "
if /i not "%confirm%"=="Y" (
echo 已取消操作
pause
exit
)
echo.
echo ================================================
echo 开始清理...
echo ================================================
echo.
REM 1. 清理 unpackage 目录
if exist "unpackage" (
echo [1/3] 正在删除 unpackage 目录...
rd /s /q "unpackage"
echo 已删除 unpackage 目录
) else (
echo [1/3] unpackage 目录不存在,跳过
)
echo.
REM 2. 询问是否清理 node_modules
if exist "node_modules" (
set /p cleanNodeModules="[2/3] 检测到 node_modules 目录,是否删除?(Y/N): "
if /i "!cleanNodeModules!"=="Y" (
echo 正在删除 node_modules 目录...
rd /s /q "node_modules"
echo 已删除 node_modules 目录
echo 提示:如需重新安装,请运行 npm install
) else (
echo 跳过 node_modules 目录
)
) else (
echo [2/3] node_modules 目录不存在,跳过
)
echo.
REM 3. 清理 HBuilderX 临时文件
echo [3/3] 清理 HBuilderX 临时文件...
REM 删除可能的临时文件和缓存
if exist ".hbuilderx" (
rd /s /q ".hbuilderx"
echo 已删除 .hbuilderx 目录
)
if exist ".temp" (
rd /s /q ".temp"
echo 已删除 .temp 目录
)
if exist ".cache" (
rd /s /q ".cache"
echo 已删除 .cache 目录
)
echo 完成
echo.
echo ================================================
echo 清理完成!
echo ================================================
echo.
echo 建议操作:
echo 1. 重启 HBuilderX
echo 2. 重新编译项目
echo 3. 如果清理了 node_modules请先运行 npm install
echo.
echo 按任意键退出...
pause >nul

View File

@@ -111,6 +111,23 @@ class UniStorageHelper {
return storeData.filter(item => item[fieldName] === value);
}
async getRecordCount(storeName) {
const storeData = this._storageGet(this._getStoreKey(storeName)) || [];
return storeData.length;
}
async deleteOldestRecord(storeName) {
const storeKey = this._getStoreKey(storeName);
const storeData = this._storageGet(storeKey) || [];
if (storeData.length > 0) {
// 删除第一条记录(最早的记录)
const newData = storeData.slice(1);
this._storageSet(storeKey, newData);
this._log(`删除最早的记录,剩余${newData.length}条记录`);
}
return Promise.resolve();
}
/*==================
更新/删除方法
==================*/

View File

@@ -7,6 +7,42 @@ page {
overflow: hidden;
}
/* 安全区域适配 - 通用解决方案 */
/* #ifdef MP-WEIXIN */
.safe-area-container {
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-left {
padding-left: env(safe-area-inset-left);
}
.safe-area-right {
padding-right: env(safe-area-inset-right);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-horizontal {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.safe-area-vertical {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
/* #endif */
/* 禁止页面回弹 */
/* html,
body,

View File

@@ -2,7 +2,8 @@ import useUserStore from "../stores/useUserStore";
import {
request,
createRequest,
uploadFile
uploadFile,
myRequest
} from "../utils/request";
import streamRequest, {
chatRequest
@@ -49,7 +50,7 @@ const prePage = () => {
return prePage.$vm;
}
export const urls ='http://10.110.145.145/images/train/'
/**
* 页面跳转封装,支持 query 参数传递和返回回调
@@ -68,7 +69,7 @@ export const navTo = function(url, {
if (needLogin && !userStore.hasLogin) {
uni.navigateTo({
url: '/pages/login/login'
url: '/pages/complete-info/complete-info'
});
return;
}
@@ -885,13 +886,15 @@ export const $api = {
uploadFile,
formatFileSize,
sendingMiniProgramMessage,
copyText
copyText,
myRequest
}
export default {
$api,
urls,
navTo,
navBack,
cloneDeep,
@@ -916,4 +919,4 @@ export default {
insertSortData,
isInWechatMiniProgramWebview,
isEmptyObject,
}
}

View File

@@ -6,6 +6,7 @@
>
<!-- 顶部头部区域 -->
<view
v-if="title"
class="container-header"
:style="border ? { borderBottom: `2rpx solid ${borderColor}` } : { borderBottom: 'none' }"
>
@@ -49,7 +50,7 @@ const emit = defineEmits(['onScrollBottom']);
defineProps({
title: {
type: String,
default: '标题',
default: '',
},
border: {
type: Boolean,

View File

@@ -0,0 +1,337 @@
<template>
<view class="custom-tabbar">
<view
class="tabbar-item"
v-for="(item, index) in tabbarList"
:key="index"
@click="switchTab(item, index)"
>
<view class="tabbar-icon">
<image
:src="currentItem === item.id ? item.selectedIconPath : item.iconPath"
mode="aspectFit"
/>
</view>
<view class="badge" v-if="item.badge && item.badge > 0">{{ item.badge }}</view>
<view class="tabbar-text" :class="{ 'active': currentItem === item.id }">
{{ item.text }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import { useReadMsg } from '@/stores/useReadMsg';
const props = defineProps({
currentPage: {
type: Number,
default: 0
}
});
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const readMsg = useReadMsg();
const currentItem = ref(props.currentPage);
// 监听props变化
watch(() => props.currentPage, (newPage) => {
currentItem.value = newPage;
});
// 生成tabbar配置的函数
const generateTabbarList = () => {
const baseItems = [
{
id: 0,
text: '职位',
path: '/pages/index/index',
iconPath: '/static/tabbar/calendar.png',
selectedIconPath: '/static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0]?.count || 0,
},
{
id: 2,
text: 'AI+',
path: '/pages/chat/chat',
iconPath: '/static/tabbar/logo3.png',
selectedIconPath: '/static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2]?.count || 0,
},
{
id: 3,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '/static/tabbar/chat4.png',
selectedIconPath: '/static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3]?.count || 0,
},
{
id: 4,
text: '我的',
path: '/pages/mine/mine',
iconPath: '/static/tabbar/mine.png',
selectedIconPath: '/static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4]?.count || 0,
},
];
// 获取用户类型统一使用isCompanyUser字段0=企业用户1=求职者, 3=网格员)
// 优先从store获取如果为空则直接从缓存获取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
// 获取isCompanyUser字段
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
// 获取用户类型的逻辑:
// 1. 优先使用store中的isCompanyUser
// 2. 如果store中没有使用缓存中的isCompanyUser
// 3. 最后默认为1求职者
const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1));
if (userType === 0 || userType === 2) {
// 企业用户:显示发布岗位
baseItems.splice(1, 0, {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
iconPath: '/static/tabbar/post.png',
selectedIconPath: '/static/tabbar/posted.png',
centerItem: false,
badge: 0,
});
} else {
// 求职者用户(包括未登录状态):显示招聘会
baseItems.splice(1, 0, {
id: 1,
text: '招聘会',
path: '/pages/careerfair/careerfair',
iconPath: '/static/tabbar/post.png',
selectedIconPath: '/static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1]?.count || 0,
});
}
return baseItems;
};
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
return generateTabbarList();
});
// 强制刷新tabbar的方法
const forceRefresh = () => {
// 触发响应式更新
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const currentUserType = userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : (cachedUserInfo.isCompanyUser !== undefined ? cachedUserInfo.isCompanyUser : 1);
};
// 监听用户类型变化只监听isCompanyUser字段
watch(() => userInfo.value?.isCompanyUser, (newIsCompanyUser, oldIsCompanyUser) => {
if (newIsCompanyUser !== oldIsCompanyUser) {
// 强制触发computed重新计算
forceRefresh();
}
}, { immediate: true });
// 监听用户信息变化(包括登录状态)
watch(() => userInfo.value, (newUserInfo, oldUserInfo) => {
if (newUserInfo !== oldUserInfo) {
// 强制触发computed重新计算
forceRefresh();
}
}, { immediate: true, deep: true });
// 切换tab
const switchTab = (item, index) => {
// 检查是否为"发布岗位"页面,需要判断企业信息是否完整
if (item.path === '/pages/job/publishJob') {
// 检查用户是否已登录
const token = uni.getStorageSync('token') || '';
const hasLogin = userStore.hasLogin;
if (!token || !hasLogin) {
// 未登录,发送事件显示登录弹窗
uni.$emit('showLoginModal');
return; // 不进行页面跳转
}
// 已登录,检查企业信息是否完整
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const storeUserInfo = userInfo.value || {};
const currentUserInfo = storeUserInfo.id ? storeUserInfo : cachedUserInfo;
// 判断企业信息字段company是否为null或undefined
if (!currentUserInfo.company || currentUserInfo.company === null) {
// 企业信息为空,跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info',
});
} else {
// 企业信息完整,跳转到发布岗位页面
uni.navigateTo({
url: '/pages/job/publishJob',
});
}
currentItem.value = item.id;
return;
}
// 检查是否为"我的"页面,需要登录验证和用户类型判断
if (item.path === '/pages/mine/mine') {
// 检查用户是否已登录
const token = uni.getStorageSync('token') || '';
const hasLogin = userStore.hasLogin;
if (!token || !hasLogin) {
// 未登录,发送事件显示登录弹窗
uni.$emit('showLoginModal');
return; // 不进行页面跳转
}
// 已登录,根据用户类型跳转到不同的"我的"页面
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
// 获取用户类型
const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1));
let targetPath = '/pages/mine/mine'; // 默认求职者页面
if (userType === 0) {
// 企业用户,跳转到企业我的页面
targetPath = '/pages/mine/company-mine';
} else {
// 求职者或其他用户类型,跳转到普通我的页面
targetPath = '/pages/mine/mine';
}
// 跳转到对应的页面
uni.navigateTo({
url: targetPath,
});
currentItem.value = item.id;
return;
}
// 判断是否为 tabBar 页面
const tabBarPages = [
'/pages/index/index',
'/pages/careerfair/careerfair',
'/pages/chat/chat',
'/pages/msglog/msglog',
'/pages/mine/mine'
];
if (tabBarPages.includes(item.path)) {
// TabBar 页面使用 redirectTo 避免页面栈溢出
uni.redirectTo({
url: item.path,
});
} else {
// 非 TabBar 页面使用 navigateTo
uni.navigateTo({
url: item.path,
});
}
currentItem.value = item.id;
};
onMounted(() => {
currentItem.value = props.currentPage;
// 调试信息显示当前用户状态和tabbar配置
forceRefresh();
});
</script>
<style lang="scss" scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 88rpx;
background-color: #ffffff;
border-top: 1rpx solid #e5e5e5;
display: flex;
align-items: center;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #5E5F60;
font-size: 22rpx;
position: relative;
cursor: pointer;
}
.tabbar-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 4rpx;
position: relative;
}
.tabbar-icon image {
width: 100%;
height: 100%;
}
.tabbar-text {
font-size: 20rpx;
line-height: 1;
transition: color 0.3s ease;
}
.tabbar-text.active {
color: #256BFA;
font-weight: 500;
}
.badge {
position: absolute;
top: 4rpx;
right: 20rpx;
min-width: 30rpx;
height: 30rpx;
background-color: #ff4444;
color: #fff;
font-size: 18rpx;
border-radius: 15rpx;
text-align: center;
line-height: 30rpx;
padding: 0 10rpx;
transform: scale(0.8);
}
/* 中间按钮特殊样式 */
.tabbar-item:has(.center-item) {
.tabbar-icon {
width: 60rpx;
height: 60rpx;
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<text class="iconfont" :class="iconClass" :style="iconStyle" @click="handleClick"></text>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
// 图标名称home、user、search
name: {
type: String,
required: true
},
// 图标大小单位rpx
size: {
type: [String, Number],
default: 32
},
// 图标颜色
color: {
type: String,
default: ''
},
// 是否粗体
bold: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
// 图标类名
const iconClass = computed(() => {
const prefix = props.name.startsWith('icon-') ? '' : 'icon-'
return `${prefix}${props.name}`
})
// 图标样式
const iconStyle = computed(() => {
const style = {
fontSize: `${props.size}rpx`
}
if (props.color) {
style.color = props.color
}
if (props.bold) {
style.fontWeight = 'bold'
}
return style
})
// 点击事件
const handleClick = (e) => {
emit('click', e)
}
</script>
<style scoped>
.iconfont {
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: inline-block;
}
</style>

View File

@@ -13,10 +13,10 @@
</view>
<text class="text-content button-click">{{ content }}</text>
<template v-if="showButton">
<uni-button class="popup-button button-click" v-if="isTip" @click="close">{{ buttonText }}</uni-button>
<button class="popup-button button-click" v-if="isTip" @click="close">{{ buttonText }}</button>
<view v-else class="confirm-btns">
<uni-button class="popup-button button-click" @click="close">{{ cancelText }}</uni-button>
<uni-button class="popup-button button-click" @click="confirm">{{ confirmText }}</uni-button>
<button class="popup-button button-click" @click="close">{{ cancelText }}</button>
<button class="popup-button button-click" @click="confirm">{{ confirmText }}</button>
</view>
</template>
</view>
@@ -137,4 +137,18 @@ export default {
}
}
}
// 重置button样式
button {
padding: 0;
margin: 0;
border: none;
background: none;
font-size: inherit;
line-height: inherit;
}
button::after {
border: none;
}
</style>

View File

@@ -31,13 +31,13 @@ const userTypes = [
{ value: 3, label: '政府人员' }
];
const currentUserType = computed(() => userInfo.value?.userType || 0);
const currentUserType = computed(() => userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 0);
const switchUserType = (userType) => {
console.log('切换用户类型:', userType);
console.log('切换前 userInfo:', userInfo.value);
userInfo.value.userType = userType;
userInfo.value.isCompanyUser = userType;
console.log('切换后 userInfo:', userInfo.value);

View File

@@ -0,0 +1,250 @@
# 五级联动地址选择器组件
## 组件简介
`area-cascade-picker` 是一个支持省市区县街道社区的五级联动地址选择组件,适用于需要选择详细地址的场景。
## 功能特点
- ✅ 五级联动选择(省/市/区/街道/社区)
- ✅ 自动级联更新
- ✅ 支持取消和确认操作
- ✅ 底部弹出式交互
- ✅ 支持自定义标题
- ✅ 返回完整的地址信息和各级代码
## 使用方法
### 1. 引入组件
```vue
<template>
<view>
<button @click="openPicker">选择地址</button>
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</view>
</template>
<script setup>
import { ref } from 'vue'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const areaPicker = ref(null)
</script>
```
### 2. 打开选择器
```javascript
const openPicker = () => {
areaPicker.value?.open({
title: '选择地址',
maskClick: true,
success: (addressData) => {
console.log('选择的地址:', addressData)
// 处理选择结果
},
cancel: () => {
console.log('取消选择')
},
change: (addressData) => {
console.log('地址变化:', addressData)
}
})
}
```
## API 说明
### open(config)
打开地址选择器
#### 参数 config
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| title | String | 否 | '选择地址' | 选择器标题 |
| maskClick | Boolean | 否 | false | 是否允许点击遮罩关闭 |
| success | Function | 否 | - | 确认选择的回调函数 |
| cancel | Function | 否 | - | 取消选择的回调函数 |
| change | Function | 否 | - | 选择变化的回调函数 |
| defaultValue | Object | 否 | null | 默认选中的地址(暂未实现) |
#### success 回调参数
```javascript
{
address: "新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会",
province: {
code: "650000",
name: "新疆维吾尔自治区"
},
city: {
code: "653100",
name: "喀什地区"
},
district: {
code: "653101",
name: "喀什市"
},
street: {
code: "65310101",
name: "学府街道"
},
community: {
code: "6531010101",
name: "学府社区居委会"
}
}
```
### close()
关闭地址选择器
```javascript
areaPicker.value?.close()
```
## 数据格式
组件使用树形结构的地址数据,格式如下:
```javascript
[
{
code: '650000', // 行政区划代码
name: '新疆维吾尔自治区', // 名称
children: [ // 下级行政区
{
code: '653100',
name: '喀什地区',
children: [
{
code: '653101',
name: '喀什市',
children: [
{
code: '65310101',
name: '学府街道',
children: [
{
code: '6531010101',
name: '学府社区居委会'
}
]
}
]
}
]
}
]
}
]
```
## 完整示例
### 企业注册地址选择
```vue
<template>
<view class="form-item" @click="selectLocation">
<text class="label">企业注册地点</text>
<view class="input-content">
<text :class="{ placeholder: !formData.address }">
{{ formData.address || '请选择注册地点' }}
</text>
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</template>
<script setup>
import { ref, reactive } from 'vue'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const areaPicker = ref(null)
const formData = reactive({
address: '',
provinceCode: '',
provinceName: '',
cityCode: '',
cityName: '',
districtCode: '',
districtName: '',
streetCode: '',
streetName: '',
communityCode: '',
communityName: ''
})
const selectLocation = () => {
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// 保存完整地址
formData.address = addressData.address
// 保存各级信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
formData.cityCode = addressData.city?.code
formData.cityName = addressData.city?.name
formData.districtCode = addressData.district?.code
formData.districtName = addressData.district?.name
formData.streetCode = addressData.street?.code
formData.streetName = addressData.street?.name
formData.communityCode = addressData.community?.code
formData.communityName = addressData.community?.name
console.log('已选择地址:', formData)
}
})
}
</script>
```
## 注意事项
1. **数据来源**当前使用本地模拟数据生产环境建议接入后端API
2. **数据更新**如需接入后端API修改 `loadAreaData` 方法即可
3. **性能优化**:地址数据量大时,建议使用懒加载
4. **兼容性**:支持 H5、微信小程序等多端
## 接入后端API
在组件的 `loadAreaData` 方法中取消注释并配置API
```javascript
async loadAreaData() {
try {
const resp = await uni.request({
url: '/app/common/area/cascade',
method: 'GET'
});
if (resp.statusCode === 200 && resp.data && resp.data.data) {
this.areaData = resp.data.data;
return;
}
} catch (error) {
console.error('加载地区数据失败:', error);
}
// 失败时使用模拟数据
this.areaData = this.getMockData();
}
```
## 更新日志
### v1.0.0 (2025-10-21)
- ✨ 初始版本
- ✅ 实现五级联动选择功能
- ✅ 支持省市区县街道社区选择
- ✅ 提供完整的地址信息返回

View File

@@ -0,0 +1,426 @@
<template>
<uni-popup
ref="popup"
type="bottom"
borderRadius="10px 10px 0 0"
background-color="#FFFFFF"
:mask-click="maskClick"
>
<view class="popup-content">
<view class="popup-header">
<view class="btn-cancel" @click="cancel">取消</view>
<view class="title">{{ title }}</view>
<view class="btn-confirm" @click="confirm">确认</view>
</view>
<view class="popup-list">
<picker-view
indicator-style="height: 84rpx;"
:value="selectedIndex"
@change="bindChange"
class="picker-view"
>
<!-- -->
<picker-view-column>
<view
v-for="(item, index) in provinceList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[0] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- -->
<picker-view-column>
<view
v-for="(item, index) in cityList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[1] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 区县 -->
<picker-view-column>
<view
v-for="(item, index) in districtList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[2] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 街道 -->
<picker-view-column>
<view
v-for="(item, index) in streetList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[3] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 社区/居委会 -->
<picker-view-column>
<view
v-for="(item, index) in communityList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[4] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</uni-popup>
</template>
<script>
import addressJson from '@/static/json/xinjiang.json';
export default {
name: 'AreaCascadePicker',
data() {
return {
maskClick: false,
title: '选择地址',
confirmCallback: null,
cancelCallback: null,
changeCallback: null,
selectedIndex: [0, 0, 0, 0, 0],
// 原始数据
areaData: [],
// 各级列表
provinceList: [],
cityList: [],
districtList: [],
streetList: [],
communityList: [],
// 当前选中的项
selectedProvince: null,
selectedCity: null,
selectedDistrict: null,
selectedStreet: null,
selectedCommunity: null,
};
},
methods: {
async open(newConfig = {}) {
const {
title,
success,
cancel,
change,
maskClick = false,
defaultValue = null,
} = newConfig;
this.reset();
if (title) this.title = title;
if (typeof success === 'function') this.confirmCallback = success;
if (typeof cancel === 'function') this.cancelCallback = cancel;
if (typeof change === 'function') this.changeCallback = change;
this.maskClick = maskClick;
// 加载地区数据
await this.loadAreaData();
// 初始化列表
this.initLists();
this.$nextTick(() => {
this.$refs.popup.open();
});
},
async loadAreaData() {
try {
// 尝试调用后端API获取地区数据
// 如果后端API不存在将使用模拟数据
console.log('正在加载地区数据...');
// const resp = await uni.request({
// url: '/app/common/area/cascade',
// method: 'GET'
// });
// if (resp.statusCode === 200 && resp.data && resp.data.data) {
// this.areaData = resp.data.data;
// }
// 暂时使用模拟数据
this.areaData = this.getMockData();
} catch (error) {
console.error('加载地区数据失败:', error);
// 如果后端API不存在使用模拟数据
this.areaData = this.getMockData();
}
},
initLists() {
// 初始化省列表
this.provinceList = this.areaData;
if (this.provinceList.length > 0) {
this.selectedProvince = this.provinceList[0];
this.updateCityList();
}
},
updateCityList() {
if (!this.selectedProvince || !this.selectedProvince.children) {
this.cityList = [];
this.districtList = [];
this.streetList = [];
this.communityList = [];
return;
}
this.cityList = this.selectedProvince.children;
this.selectedIndex[1] = 0;
if (this.cityList.length > 0) {
this.selectedCity = this.cityList[0];
this.updateDistrictList();
}
},
updateDistrictList() {
if (!this.selectedCity || !this.selectedCity.children) {
this.districtList = [];
this.streetList = [];
this.communityList = [];
return;
}
this.districtList = this.selectedCity.children;
this.selectedIndex[2] = 0;
if (this.districtList.length > 0) {
this.selectedDistrict = this.districtList[0];
this.updateStreetList();
}
},
updateStreetList() {
if (!this.selectedDistrict || !this.selectedDistrict.children) {
this.streetList = [];
this.communityList = [];
return;
}
this.streetList = this.selectedDistrict.children;
this.selectedIndex[3] = 0;
if (this.streetList.length > 0) {
this.selectedStreet = this.streetList[0];
this.updateCommunityList();
}
},
updateCommunityList() {
if (!this.selectedStreet || !this.selectedStreet.children) {
this.communityList = [];
return;
}
this.communityList = this.selectedStreet.children;
this.selectedIndex[4] = 0;
if (this.communityList.length > 0) {
this.selectedCommunity = this.communityList[0];
}
},
bindChange(e) {
const newIndex = e.detail.value;
// 检查哪一列发生了变化
for (let i = 0; i < 5; i++) {
if (newIndex[i] !== this.selectedIndex[i]) {
this.selectedIndex[i] = newIndex[i];
// 根据变化的列更新后续列
if (i === 0) {
// 省变化
this.selectedProvince = this.provinceList[newIndex[0]];
this.updateCityList();
} else if (i === 1) {
// 市变化
this.selectedCity = this.cityList[newIndex[1]];
this.updateDistrictList();
} else if (i === 2) {
// 区县变化
this.selectedDistrict = this.districtList[newIndex[2]];
this.updateStreetList();
} else if (i === 3) {
// 街道变化
this.selectedStreet = this.streetList[newIndex[3]];
this.updateCommunityList();
} else if (i === 4) {
// 社区变化
this.selectedCommunity = this.communityList[newIndex[4]];
}
break;
}
}
if (this.changeCallback) {
this.changeCallback(this.getSelectedAddress());
}
},
getSelectedAddress() {
const parts = [];
if (this.selectedProvince) parts.push(this.selectedProvince.name);
if (this.selectedCity) parts.push(this.selectedCity.name);
if (this.selectedDistrict) parts.push(this.selectedDistrict.name);
if (this.selectedStreet) parts.push(this.selectedStreet.name);
if (this.selectedCommunity) parts.push(this.selectedCommunity.name);
return {
address: parts.join('/'),
province: this.selectedProvince,
city: this.selectedCity,
district: this.selectedDistrict,
street: this.selectedStreet,
community: this.selectedCommunity,
};
},
close() {
this.$refs.popup.close();
},
cancel() {
this.clickCallback(this.cancelCallback);
},
confirm() {
this.clickCallback(this.confirmCallback);
},
async clickCallback(callback) {
if (typeof callback !== 'function') {
this.$refs.popup.close();
return;
}
try {
const result = await callback(this.getSelectedAddress());
if (result !== false) {
this.$refs.popup.close();
}
} catch (error) {
console.error('callback 执行出错:', error);
}
},
reset() {
this.maskClick = false;
this.confirmCallback = null;
this.cancelCallback = null;
this.changeCallback = null;
this.selectedIndex = [0, 0, 0, 0, 0];
this.provinceList = [];
this.cityList = [];
this.districtList = [];
this.streetList = [];
this.communityList = [];
},
// 模拟数据(用于演示)
getMockData() {
return addressJson
}
},
};
</script>
<style lang="scss" scoped>
.popup-content {
color: #000000;
height: 60vh;
}
.popup-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
flex: 1;
overflow: hidden;
.picker-view {
width: 100%;
height: calc(60vh - 100rpx);
margin-top: 20rpx;
.uni-picker-view-mask {
background: rgba(0, 0, 0, 0);
}
.item {
line-height: 84rpx;
height: 84rpx;
text-align: center;
font-weight: 400;
font-size: 28rpx;
color: #cccccc;
padding: 0 4rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-active {
color: #333333;
font-weight: 500;
}
.uni-picker-view-indicator:after {
border-color: #e3e3e3;
}
.uni-picker-view-indicator:before {
border-color: #e3e3e3;
}
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx 40rpx 10rpx 40rpx;
.title {
font-weight: 500;
font-size: 36rpx;
color: #333333;
text-align: center;
}
.btn-cancel {
font-weight: 400;
font-size: 32rpx;
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
}
</style>

View File

@@ -116,6 +116,7 @@ const handleItemClick = (e) => {
}
/* 表格 */
/* #ifndef MP-WEIXIN */
table {
// display: block; /* 让表格可滚动 */
// width: 100%;
@@ -147,6 +148,7 @@ tr:hover {
background-color: #f1f1f1;
transition: 0.3s;
}
/* #endif */
/* 代码块 */
pre,
@@ -207,11 +209,13 @@ pre code:empty {
cursor: pointer;
border-radius: 6rpx;
}
/* #ifndef MP-WEIXIN */
.copy-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: #259939;
text-decoration: underline;
}
/* #endif */
pre.hljs {
padding: 0 24rpx;
@@ -246,6 +250,7 @@ ol {
text-align: right;
}
/* #ifndef MP-WEIXIN */
#markdown-content ::v-deep div > pre:first-of-type {
margin-top: 20rpx;
}
@@ -254,6 +259,7 @@ ol {
display: flex;
flex-direction: column;
}
/* #endif */
.markdownRich > div {
display: flex;
flex-direction: column;

View File

@@ -77,7 +77,7 @@ function nextDetail(job) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
</script>

View File

@@ -62,7 +62,7 @@
</template>
<script setup>
import { inject, computed, toRaw, ref, defineExpose } from 'vue';
import { inject, computed, toRaw, ref } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
@@ -103,7 +103,7 @@ function nextDetail(job) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
function toggleSelect(jobId) {
@@ -122,7 +122,7 @@ function handleCardClick(job, e) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
// 新增:提供选中状态和切换方法给父组件

View File

@@ -75,10 +75,10 @@ const { getTransformChildren } = useDictStore();
// 岗位类型数据
const getJobTypeData = () => {
return [
{ label: '常规岗位', value: 'regular', text: '常规岗位' },
{ label: '就业见习岗位', value: 'internship', text: '就业见习岗位' },
{ label: '实习实训岗位', value: 'training', text: '实习实训岗位' },
{ label: '社区实践岗位', value: 'community', text: '社区实践岗位' }
{ label: '常规岗位', value: 0, text: '常规岗位' },
{ label: '就业见习岗位', value: 1, text: '就业见习岗位' },
{ label: '实习实训岗位', value: 2, text: '实习实训岗位' },
{ label: '社区实践岗位', value: 3, text: '社区实践岗位' }
];
};
@@ -201,13 +201,7 @@ defineExpose({
<style lang="scss" scoped>
.popup-fix {
position: fixed !important;
left: 0;
right: 0;
bottom: 0;
top: 0;
height: 100vh;
z-index: 9999;
z-index: 9999 !important;
}
.popup-content {
color: #000000;

View File

@@ -32,7 +32,7 @@
</template>
<script setup>
import { ref, reactive, computed, inject, nextTick, defineExpose, onMounted } from 'vue';
import { ref, reactive, computed, inject, nextTick, onMounted } from 'vue';
const { $api, navTo, setCheckedNodes, cloneDeep } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import { storeToRefs } from 'pinia';

View File

@@ -13,7 +13,30 @@
<view class="btn-confirm" @click="confirm">确认</view>
</view>
<view class="popup-list">
<!-- 多选模式 -->
<view v-if="multiSelect" class="multi-select-list">
<view v-if="!processedListData[0] || processedListData[0].length === 0" class="empty-tip">
暂无数据
</view>
<view
v-else
class="skill-tags-container"
>
<view
v-for="(item, index) in processedListData[0]"
:key="index"
class="skill-tag"
:class="{ 'skill-tag-active': selectedValues.includes(item[this.rowKey]) }"
@click.stop="toggleSelect(item)"
@touchstart.stop="toggleSelect(item)"
>
<text class="skill-tag-text">{{ getLabel(item) }}</text>
</view>
</view>
</view>
<!-- 单选模式 -->
<picker-view
v-else
indicator-style="height: 84rpx;"
:value="selectedIndex"
@change="bindChange"
@@ -54,6 +77,8 @@ export default {
rowKey: 'value',
selectedItems: [],
unit: '',
multiSelect: false,
selectedValues: [],
};
},
computed: {
@@ -66,6 +91,13 @@ export default {
});
});
},
// 计算选中的项目
computedSelectedItems() {
if (!this.multiSelect) return this.selectedItems;
return this.processedListData[0] ? this.processedListData[0].filter(item =>
this.selectedValues.includes(item[this.rowKey])
) : [];
},
},
methods: {
open(newConfig = {}) {
@@ -80,7 +112,10 @@ export default {
rowKey = 'value',
maskClick = false,
defaultIndex = [],
multiSelect = false,
defaultValues = [],
} = newConfig;
this.reset();
if (title) this.title = title;
if (typeof success === 'function') this.confirmCallback = success;
@@ -92,10 +127,16 @@ export default {
this.rowKey = rowKey;
this.maskClick = maskClick;
this.unit = unit;
this.multiSelect = multiSelect;
this.selectedIndex =
defaultIndex.length === this.listData.length ? defaultIndex : new Array(this.listData.length).fill(0);
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
if (multiSelect) {
this.selectedValues = defaultValues || [];
} else {
this.selectedIndex =
defaultIndex.length === this.listData.length ? defaultIndex : new Array(this.listData.length).fill(0);
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
}
this.$nextTick(() => {
this.$refs.popup.open();
});
@@ -117,6 +158,22 @@ export default {
getLabel(item) {
return item?.[this.rowLabel] ?? '';
},
toggleSelect(item) {
if (!item || !this.rowKey || !item[this.rowKey]) {
return;
}
const value = item[this.rowKey];
const index = this.selectedValues.indexOf(value);
if (index > -1) {
// 取消选中
this.selectedValues.splice(index, 1);
} else {
// 选中
this.selectedValues.push(value);
}
},
setColunm(index, list) {
if (index > this.listData.length) {
return console.warn('最长' + this.listData.length);
@@ -135,7 +192,14 @@ export default {
}
try {
const result = await callback(this.selectedIndex, this.selectedItems); // 无论是 async 还是返回 Promise 的函数都可以 await
let result;
if (this.multiSelect) {
// 多选模式:传递 selectedValues 和 selectedItems
result = await callback(this.selectedValues, this.computedSelectedItems);
} else {
// 单选模式:传递 selectedIndex 和 selectedItems
result = await callback(this.selectedIndex, this.selectedItems);
}
if (result !== false) {
this.$refs.popup.close();
}
@@ -154,6 +218,8 @@ export default {
this.rowKey = 'value';
this.selectedItems = [];
this.unit = '';
this.multiSelect = false;
this.selectedValues = [];
},
},
};
@@ -224,10 +290,83 @@ export default {
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
}
.multi-select-list {
padding: 20rpx 30rpx;
max-height: calc(60vh - 120rpx);
overflow-y: auto;
}
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #999999;
font-size: 28rpx;
}
.skill-tags-container {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
align-items: flex-start;
}
.skill-tag {
display: inline-flex;
align-items: center;
padding: 12rpx 20rpx;
border-radius: 20rpx;
background-color: #f8f9fa;
border: 2rpx solid #e8eaee;
cursor: pointer;
transition: all 0.3s ease;
font-size: 24rpx;
color: #333333;
white-space: nowrap;
user-select: none;
&:hover {
background-color: #e9ecef;
border-color: #d0d0d0;
transform: translateY(-1rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
background-color: #dee2e6;
}
.skill-tag-text {
font-size: 24rpx;
color: inherit;
line-height: 1.2;
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
&.skill-tag-active {
background-color: #256bfa;
border-color: #256bfa;
color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(37, 107, 250, 0.3);
.skill-tag-text {
color: #ffffff;
font-weight: 500;
}
&:hover {
background-color: #1e5ce6;
border-color: #1e5ce6;
transform: translateY(-1rpx);
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.4);
}
}
}
</style>

View File

@@ -10,7 +10,7 @@
>
<image :src="currentItem == item.id ? item.selectedIconPath : item.iconPath"></image>
</view>
<view class="badge" v-if="item.badge">{{ item.badge }}</view>
<view class="badge" v-if="item.badge && item.badge > 0">{{ item.badge }}</view>
<view class="item-bottom" :class="[currentItem == item.id ? 'item-active' : '']">
<text>{{ item.text }}</text>
</view>
@@ -19,7 +19,7 @@
</template>
<script setup>
import { ref, defineProps, onMounted, computed } from 'vue';
import { ref, onMounted, computed, watch, nextTick } from 'vue';
import { useReadMsg } from '@/stores/useReadMsg';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
@@ -36,35 +36,60 @@ const readMsg = useReadMsg();
const { userInfo } = storeToRefs(useUserStore());
const currentItem = ref(0);
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
// 监听用户类型变化重新生成tabbar配置
watch(() => userInfo.value?.isCompanyUser, (newIsCompanyUser, oldIsCompanyUser) => {
console.log('midell-box用户类型变化监听:', { newIsCompanyUser, oldIsCompanyUser, userInfo: userInfo.value });
if (newIsCompanyUser !== oldIsCompanyUser) {
console.log('用户类型发生变化重新生成tabbar:', newIsCompanyUser);
// tabbarList是computed会自动重新计算
// 强制触发响应式更新
nextTick(() => {
console.log('tabbar配置已更新:', tabbarList.value);
});
}
}, { immediate: true });
// 监听用户信息变化(包括登录状态)
watch(() => userInfo.value, (newUserInfo, oldUserInfo) => {
console.log('midell-box用户信息变化监听:', { newUserInfo, oldUserInfo });
if (newUserInfo !== oldUserInfo) {
console.log('用户信息发生变化重新生成tabbar');
// 强制触发响应式更新
nextTick(() => {
console.log('tabbar配置已更新:', tabbarList.value);
});
}
}, { immediate: true, deep: true });
// 生成tabbar配置的函数
const generateTabbarList = () => {
const baseItems = [
{
id: 0,
text: '首页',
text: '职位2',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0].count,
badge: readMsg.badges[0]?.count || 0,
},
{
id: 2,
text: '',
text: 'AI+',
path: '/pages/chat/chat',
iconPath: '../../static/tabbar/logo3.png',
selectedIconPath: '../../static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2].count,
badge: readMsg.badges[2]?.count || 0,
},
{
id: 3,
text: '消息',
text: '消息2',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3].count,
badge: readMsg.badges[3]?.count || 0,
},
{
id: 4,
@@ -73,12 +98,12 @@ const tabbarList = computed(() => {
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4].count,
badge: readMsg.badges[4]?.count || 0,
},
];
// 根据用户类型添加不同的导航项
const userType = userInfo.value?.userType || 0;
// 根据用户类型添加不同的导航项,未登录时默认为求职者
const userType = userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 1;
if (userType === 0) {
// 企业用户:显示发布岗位,隐藏招聘会
@@ -86,13 +111,13 @@ const tabbarList = computed(() => {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
iconPath: '../../static/tabbar/publish-job.svg',
selectedIconPath: '../../static/tabbar/publish-job-selected.svg',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: 0,
});
} else {
// 普通用户、网格员、政府人员:显示招聘会
// 求职者用户(包括未登录状态):显示招聘会
baseItems.splice(1, 0, {
id: 1,
text: '招聘会',
@@ -100,20 +125,26 @@ const tabbarList = computed(() => {
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1].count,
badge: readMsg.badges[1]?.count || 0,
});
}
return baseItems;
};
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
return generateTabbarList();
});
onMounted(() => {
uni.hideTabBar();
// 自定义TabBar不需要调用hideTabBar因为已经在pages.json中设置了custom: true
// uni.hideTabBar(); // 移除这行避免在自定义TabBar模式下调用
currentItem.value = props.currentpage;
});
const changeItem = (item) => {
// 判断是否为 tabBar 页面
// 判断是否为 TabBar 页面
const tabBarPages = [
'/pages/index/index',
'/pages/careerfair/careerfair',
@@ -123,12 +154,12 @@ const changeItem = (item) => {
];
if (tabBarPages.includes(item.path)) {
// tabBar 页面使用 switchTab
uni.switchTab({
// TabBar 页面使用 redirectTo 避免页面栈溢出
uni.redirectTo({
url: item.path,
});
} else {
// 非 tabBar 页面使用 navigateTo
// 非 TabBar 页面使用 navigateTo
uni.navigateTo({
url: item.path,
});

View File

@@ -0,0 +1,564 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="auth-modal">
<view class="modal-content">
<!-- 关闭按钮 -->
<view class="close-btn" @click="close">
<uni-icons type="closeempty" size="24" color="#999"></uni-icons>
</view>
<!-- Logo和标题 -->
<view class="auth-header">
<image class="auth-logo" src="@/static/logo.png" mode="aspectFit"></image>
<view class="auth-title">欢迎使用就业服务</view>
<view class="auth-subtitle">需要您授权手机号登录</view>
</view>
<!-- 角色选择 -->
<view class="role-select">
<view class="role-title">请选择您的角色</view>
<view class="role-options">
<view
class="role-item"
:class="{ active: userType === 1 }"
@click="selectRole(1)"
>
<view class="role-icon">
<uni-icons type="person" size="32" :color="userType === 1 ? '#256BFA' : '#999'"></uni-icons>
</view>
<view class="role-text">我是求职者</view>
</view>
<view
class="role-item"
:class="{ active: userType === 0 }"
@click="selectRole(0)"
>
<view class="role-icon">
<uni-icons type="shop" size="32" :color="userType === 0 ? '#256BFA' : '#999'"></uni-icons>
</view>
<view class="role-text">我是招聘者</view>
</view>
</view>
</view>
<!-- 授权说明 -->
<view class="auth-tips">
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>保护您的个人信息安全</text>
</view>
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>为您推荐更合适的岗位</text>
</view>
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>享受完整的就业服务</text>
</view>
</view>
<!-- 授权按钮 -->
<view class="auth-actions">
<!-- 微信小程序使用 open-type="getPhoneNumber" -->
<!-- #ifdef MP-WEIXIN -->
<button
class="auth-btn primary"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
<text>微信授权登录</text>
</button>
<!-- #endif -->
<!-- H5和App使用普通按钮 -->
<!-- #ifndef MP-WEIXIN -->
<button class="auth-btn primary" @click="wxLogin">
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
<text>微信授权登录</text>
</button>
<!-- #endif -->
<!-- 测试登录按钮仅开发环境 -->
<!-- #ifdef APP-PLUS || H5 -->
<button class="auth-btn secondary" @click="testLogin">
<text>测试账号登录</text>
</button>
<!-- #endif -->
</view>
<!-- 用户协议 -->
<view class="auth-agreement">
<text>登录即表示同意</text>
<text class="link" @click="openAgreement('user')">用户协议</text>
<text></text>
<text class="link" @click="openAgreement('privacy')">隐私政策</text>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, inject } from 'vue';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const { $api } = inject('globalFunction');
const { loginSetToken } = useUserStore();
const popup = ref(null);
const userType = ref(null); // 用户角色1-求职者0-企业
const emit = defineEmits(['success', 'cancel']);
// 打开弹窗
const open = () => {
popup.value?.open();
userType.value = null; // 重置角色选择
};
// 关闭弹窗
const close = () => {
popup.value?.close();
emit('cancel');
};
// 选择角色
const selectRole = (type) => {
userType.value = type;
};
// 验证角色是否已选择
const validateRole = () => {
if (userType.value === null) {
$api.msg('请先选择您的角色');
return false;
}
return true;
};
const getPhoneNumber = (e) => {
console.log('获取手机号:', e);
// 验证角色是否已选择
if (!validateRole()) {
return;
}
if (e.detail.errMsg === 'getPhoneNumber:ok') {
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录code获取成功', loginRes.code);
const { encryptedData, iv } = e.detail;
const code = loginRes.code; // 使用wx.login返回的code
// 调用后端接口进行登录
uni.showLoading({ title: '登录中...' });
$api.createRequest('/app/appLogin', {
code,
encryptedData,
iv,
userType: userType.value
}, 'post').then((resData) => {
uni.hideLoading();
console.log(resData, 'resume.idCard');
if (resData.token) {
// 登录成功存储token
loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
console.log(resData.isCompanyUser, 'resData.isCompanyUser');
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = Number(resData.isCompanyUser); // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('登录成功');
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
close();
emit('success');
// 根据用户类型跳转到不同的信息补全页面
if (!resume.jobTitleId) {
console.log(resume, 'resume.idCard');
if (userType.value === 1 && !resData.idCard) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0 && !resData.idCard) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
}).catch(() => {
$api.msg('获取用户信息失败');
});
} else {
$api.msg('登录失败,请重试');
}
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '登录失败,请重试');
});
},
fail: (err) => {
console.error('获取微信登录code失败', err);
$api.msg('获取登录信息失败,请重试');
}
});
} else if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
$api.msg('您取消了授权');
} else {
$api.msg('获取手机号失败');
}
};
// H5/App 微信登录
const wxLogin = () => {
// 验证角色是否已选择
if (!validateRole()) {
return;
}
// #ifdef H5
// H5网页微信登录逻辑
uni.showLoading({ title: '登录中...' });
// 获取微信授权code
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录成功:', loginRes);
// 调用后端接口进行登录
$api.createRequest('/app/appLogin', {
code: loginRes.code,
userType: userType.value
}, 'post').then((resData) => {
uni.hideLoading();
if (resData.token) {
loginSetToken(resData.token).then((resume) => {
console.log(resData, 'resData.isCompanyUser');
// 更新用户类型到缓存
if (resData.isCompanyUser) {
console.log(resData.isCompanyUser, 'resData.isCompanyUser');
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = Number(resData.isCompanyUser); // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('登录成功');
close();
emit('success');
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
});
} else {
$api.msg('登录失败,请重试');
}
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '登录失败,请重试');
});
},
fail: (err) => {
uni.hideLoading();
console.error('微信登录失败:', err);
$api.msg('微信登录失败');
}
});
// #endif
// #ifdef APP-PLUS
// App微信登录逻辑
uni.getProvider({
service: 'oauth',
success: (res) => {
if (~res.provider.indexOf('weixin')) {
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录成功:', loginRes);
// 调用后端接口进行登录
$api.createRequest('/app/appLogin', {
code: loginRes.code,
userType: userType.value
}, 'post').then((resData) => {
if (resData.token) {
loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = resData.isCompanyUser ? 0 : 1; // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('登录成功');
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
close();
emit('success');
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
});
}
});
},
fail: (err) => {
console.error('微信登录失败:', err);
$api.msg('微信登录失败');
}
});
}
}
});
// #endif
};
// 测试账号登录(仅开发环境)
const testLogin = () => {
uni.showLoading({ title: '登录中...' });
const params = {
username: 'test',
password: 'test',
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
uni.hideLoading();
loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = resData.isCompanyUser ? 0 : 1; // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('测试登录成功');
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
close();
emit('success');
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
}).catch(() => {
$api.msg('获取用户信息失败');
});
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '登录失败');
});
};
// 打开用户协议
const openAgreement = (type) => {
const urls = {
user: '/pages/agreement/user',
privacy: '/pages/agreement/privacy'
};
if (urls[type]) {
uni.navigateTo({
url: urls[type]
});
}
};
// 暴露方法供父组件调用
defineExpose({
open,
close
});
</script>
<style lang="stylus" scoped>
.auth-modal
width: 620rpx
background: #FFFFFF
border-radius: 24rpx
overflow: hidden
.modal-content
padding: 60rpx 40rpx 40rpx
position: relative
.close-btn
position: absolute
right: 20rpx
top: 20rpx
width: 60rpx
height: 60rpx
display: flex
align-items: center
justify-content: center
z-index: 10
.auth-header
text-align: center
margin-bottom: 40rpx
.auth-logo
width: 120rpx
height: 120rpx
margin: 0 auto 24rpx
.auth-title
font-size: 36rpx
font-weight: 600
color: #333333
margin-bottom: 12rpx
.auth-subtitle
font-size: 28rpx
color: #666666
.role-select
margin-bottom: 32rpx
.role-title
font-size: 28rpx
font-weight: 500
color: #333333
margin-bottom: 20rpx
text-align: center
.role-options
display: flex
justify-content: space-between
gap: 20rpx
.role-item
flex: 1
background: #F7F8FA
border: 2rpx solid #E5E5E5
border-radius: 16rpx
padding: 32rpx 20rpx
display: flex
flex-direction: column
align-items: center
position: relative
transition: all 0.3s ease
cursor: pointer
&.active
background: #F0F5FF
border-color: #256BFA
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.15)
.role-icon
margin-bottom: 16rpx
.role-text
font-size: 28rpx
color: #333333
font-weight: 500
.auth-tips
background: #F7F8FA
border-radius: 16rpx
padding: 24rpx
margin-bottom: 40rpx
.tip-item
display: flex
align-items: center
margin-bottom: 16rpx
font-size: 26rpx
color: #666666
&:last-child
margin-bottom: 0
text
margin-left: 12rpx
.auth-actions
margin-bottom: 32rpx
.auth-btn
width: 100%
height: 88rpx
border-radius: 44rpx
display: flex
align-items: center
justify-content: center
font-size: 32rpx
font-weight: 500
border: none
margin-bottom: 20rpx
&:last-child
margin-bottom: 0
&.primary
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
color: #FFFFFF
box-shadow: 0 8rpx 20rpx rgba(37, 107, 250, 0.3)
&.secondary
background: #F7F8FA
color: #666666
text
margin-left: 12rpx
.auth-agreement
text-align: center
font-size: 24rpx
color: #999999
line-height: 1.6
.link
color: #256BFA
text-decoration: underline
// 按钮重置样式
button::after
border: none
</style>

View File

@@ -1,6 +1,10 @@
export default {
// baseUrl: 'http://39.98.44.136:8080', // 测试
baseUrl: 'http://ks.zhaopinzao8dian.com/api/ks', // 测试
LCBaseUrl:'http://10.110.145.145:9100',//招聘、培训、帮扶
LCBaseUrlInner:'http://10.110.145.145:10100',//内网端口
imgBaseUrl:'http://10.110.145.145/images', //图片基础url
// sseAI+
// StreamBaseURl: 'http://39.98.44.136:8000',
StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai',
@@ -70,4 +74,4 @@ export default {
desc: '融合海量岗位、智能简历匹配、竞争力分析,助你精准锁定理想职位!',
imgUrl: 'https://qd.zhaopinzao8dian.com/file/csn/qd_shareLogo.jpg',
}
}
}

102
config/icons.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* 图标配置文件
* 统一管理项目中使用的所有图标名称
* 使用方式import { ICONS } from '@/config/icons'
*/
export const ICONS = {
// 导航类
HOME: 'home',
SEARCH: 'search',
USER: 'user',
SETTING: 'setting',
BACK: 'back',
CLOSE: 'close',
MENU: 'menu',
MORE: 'more',
// 操作类
ADD: 'add',
EDIT: 'edit',
DELETE: 'delete',
SAVE: 'save',
REFRESH: 'refresh',
UPLOAD: 'upload',
DOWNLOAD: 'download',
SHARE: 'share',
FILTER: 'filter',
SORT: 'sort',
// 通讯类
PHONE: 'phone',
MESSAGE: 'message',
EMAIL: 'email',
CHAT: 'chat',
NOTIFICATION: 'notification',
// 位置类
LOCATION: 'location',
MAP: 'map',
NAVIGATE: 'navigate',
// 状态类
SUCCESS: 'success',
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
LOADING: 'loading',
// 媒体类
IMAGE: 'image',
VIDEO: 'video',
AUDIO: 'audio',
FILE: 'file',
CAMERA: 'camera',
// 交互类
LIKE: 'like',
STAR: 'star',
COLLECT: 'collect',
COMMENT: 'comment',
EYE: 'eye',
HEART: 'heart',
// 箭头类
ARROW_UP: 'arrow-up',
ARROW_DOWN: 'arrow-down',
ARROW_LEFT: 'arrow-left',
ARROW_RIGHT: 'arrow-right',
// 其他
TIME: 'time',
DATE: 'date',
CALENDAR: 'calendar',
LOCK: 'lock',
UNLOCK: 'unlock',
HELP: 'help',
QUESTION: 'question',
}
// 图标尺寸预设
export const ICON_SIZES = {
MINI: 24,
SMALL: 28,
NORMAL: 32,
LARGE: 40,
XLARGE: 48,
}
// 图标颜色预设
export const ICON_COLORS = {
PRIMARY: '#13C57C',
SECONDARY: '#256BFA',
SUCCESS: '#13C57C',
WARNING: '#FF9800',
DANGER: '#F44336',
INFO: '#2196F3',
TEXT: '#333333',
TEXT_SECONDARY: '#666666',
TEXT_PLACEHOLDER: '#999999',
WHITE: '#FFFFFF',
}

View File

@@ -0,0 +1,318 @@
# H5端CSS引入问题解决方案
## ❌ 错误提示
```
iconfont.css:1 Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/css". Strict MIME type checking is enforced for module scripts per HTML spec.
```
## 🔍 问题原因
`main.js` 中使用了 **错误的方式** 引入CSS文件
```javascript
// ❌ 错误尝试将CSS作为JavaScript模块导入
import './static/iconfont/iconfont.css'
```
这种 `import` 语法在H5端会导致浏览器尝试将CSS文件作为JavaScript模块加载从而产生MIME类型错误。
## ✅ 解决方案
### 方案一:在 App.vue 中使用 @import推荐
`App.vue``<style>` 标签中使用CSS的 `@import` 语法:
```vue
<style>
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
</style>
```
**优点:**
- ✅ 所有平台兼容H5、小程序、App
- ✅ 符合CSS规范
- ✅ 全局生效
### 方案二:条件编译(如果必须在 main.js 中引入)
如果确实需要在 `main.js` 中引入,使用条件编译:
```javascript
// #ifndef H5
import './static/iconfont/iconfont.css'
// #endif
```
然后在 `App.vue` 中单独为H5引入
```vue
<style>
/* #ifdef H5 */
@import url("/static/iconfont/iconfont.css");
/* #endif */
</style>
```
## 📋 CSS引入方式对比
### JavaScript import不推荐用于CSS
```javascript
// ❌ 在 main.js 中
import './static/iconfont/iconfont.css'
```
**问题:**
- H5端会报MIME类型错误
- 将CSS当作JavaScript模块处理
### CSS @import推荐
```vue
<!-- App.vue 或其他 .vue 文件中 -->
<style>
@import url("/static/iconfont/iconfont.css");
</style>
```
**优点:**
- 所有平台兼容
- 符合CSS标准
- 不会产生MIME类型错误
### 使用 <style> 标签的 src 属性(可选)
```vue
<style src="/static/iconfont/iconfont.css"></style>
```
## 🔧 修复步骤
### Step 1: 删除 main.js 中的CSS import
打开 `main.js`,找到并删除或注释掉:
```javascript
// import './static/iconfont/iconfont.css' // 删除这行
```
### Step 2: 确认 App.vue 中的引入
确保 `App.vue` 中有正确的CSS引入
```vue
<style>
@import '@/common/animation.css';
@import '@/common/common.css';
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
</style>
```
### Step 3: 清除缓存并重新编译
1. 关闭开发服务器
2. 清除浏览器缓存
3. 重新运行 `npm run dev:h5` 或点击HBuilderX的运行按钮
## 🎯 最佳实践
### 全局CSS引入位置
**推荐顺序:**
```vue
<!-- App.vue -->
<style>
/* 1. 重置样式 / 通用样式 */
@import '@/common/reset.css';
@import '@/common/common.css';
/* 2. 第三方库样式 */
@import url("/static/iconfont/iconfont.css");
/* 3. 动画效果 */
@import '@/common/animation.css';
/* 4. 项目全局样式 */
/* 自定义全局样式 */
</style>
```
### 路径写法
**绝对路径(推荐):**
```css
@import url("/static/iconfont/iconfont.css");
```
**相对路径:**
```css
@import url("./static/iconfont/iconfont.css");
@import url("@/static/iconfont/iconfont.css");
```
**注意:** 在不同平台上路径解析可能有差异,推荐使用绝对路径。
## 🚫 常见错误
### 错误1在 main.js 中 import CSS
```javascript
// ❌ 错误
import './styles/global.css'
import '@/static/iconfont/iconfont.css'
```
**解决:** 改用 App.vue 的 `@import`
### 错误2路径不正确
```css
/* ❌ 错误:路径错误 */
@import url("static/iconfont/iconfont.css");
/* ✅ 正确:使用正确的路径 */
@import url("/static/iconfont/iconfont.css");
```
### 错误3缺少分号
```css
/* ❌ 错误:缺少分号 */
@import url("/static/iconfont/iconfont.css")
/* ✅ 正确:添加分号 */
@import url("/static/iconfont/iconfont.css");
```
### 错误4在 scoped 样式中引入
```vue
<!-- 不推荐 scoped 样式中引入全局CSS -->
<style scoped>
@import url("/static/iconfont/iconfont.css");
</style>
<!-- 推荐全局样式不要加 scoped -->
<style>
@import url("/static/iconfont/iconfont.css");
</style>
```
## 📊 平台兼容性
| 引入方式 | H5 | 小程序 | App | 推荐 |
|---------|----|----|-----|-----|
| main.js import | ❌ | ✅ | ✅ | ❌ |
| App.vue @import | ✅ | ✅ | ✅ | ✅ |
| style src | ✅ | ✅ | ✅ | ✅ |
| 条件编译 | ✅ | ✅ | ✅ | ⚠️ |
## 🔍 调试方法
### 1. 检查CSS是否加载
在浏览器开发者工具中:
```
F12 → Network → Filter: CSS → 查找 iconfont.css
```
**成功标志:**
- 状态码200
- Type: stylesheet
- Size: 文件大小正常
### 2. 检查字体文件
```
F12 → Network → Filter: Font → 查找 iconfont.ttf/woff
```
### 3. 检查控制台错误
```
F12 → Console → 查看是否有错误信息
```
### 4. 验证样式生效
```javascript
// 在控制台执行
document.querySelector('.iconfont')
// 应该能找到使用了 iconfont 类的元素
```
## ✅ 验证成功
修复后,应该看到:
1. ✅ H5端控制台无CSS加载错误
2. ✅ 图标正常显示
3. ✅ Network 中 iconfont.css 状态码为 200
4. ✅ 字体文件正常加载
## 📝 注意事项
### uni-app 项目特点
1. **多平台编译**
- H5端使用浏览器标准
- 小程序有自己的规范
- App使用原生渲染
2. **路径处理**
- `@/` 代表项目根目录
- `/static/` 代表静态资源目录
- 不同平台路径解析略有差异
3. **样式隔离**
- `scoped` 样式只在当前组件生效
- 全局样式在 App.vue 中引入
- 不要在 scoped 中引入全局CSS
### Vite 项目特点
如果使用 Vite 构建HBuilderX 3.2+
```javascript
// main.js 中可以使用(但不推荐)
import './static/iconfont/iconfont.css'
```
但为了兼容性,仍然推荐在 App.vue 中使用 `@import`
## 🎉 总结
### 问题
`main.js` 中使用 `import` 引入CSS导致H5端报错。
### 解决
1. ✅ 删除 `main.js` 中的 CSS import
2. ✅ 在 `App.vue``<style>` 中使用 `@import`
3. ✅ 重启开发服务器
### 最佳实践
- 所有全局CSS在 `App.vue` 中通过 `@import` 引入
- 使用绝对路径:`/static/...`
- 不要在 `scoped` 样式中引入全局CSS
- 保持引入顺序:重置 → 第三方 → 动画 → 自定义
## 📚 相关文档
- [uni-app 样式导入](https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#%E6%A0%B7%E5%BC%8F%E5%AF%BC%E5%85%A5)
- [CSS @import](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@import)
- [Vite 静态资源处理](https://cn.vitejs.dev/guide/assets.html)
---
**该问题已解决!** 🎉
现在H5端应该可以正常加载CSS文件了。如果还有问题请检查
1. 文件路径是否正确
2. 是否清除了浏览器缓存
3. 是否重启了开发服务器

View File

@@ -0,0 +1,207 @@
# 企业地址选择功能说明
## 功能概述
在企业信息补全页面company-info.vue点击"企业注册地点"字段的箭头时,会弹出一个五级联动选择器,用户可以选择省市区县街道社区的完整地址。
## 实现的功能
### 1. 五级联动选择
- **省级选择**:选择省/直辖市/自治区
- **市级选择**:根据选择的省,显示对应的市/地区
- **区县选择**:根据选择的市,显示对应的区/县
- **街道选择**:根据选择的区县,显示对应的街道/乡镇
- **社区选择**:根据选择的街道,显示对应的社区/居委会
### 2. 联动逻辑
- 当选择省级时,自动更新并重置市级及以下选项
- 当选择市级时,自动更新并重置区县及以下选项
- 当选择区县时,自动更新并重置街道及以下选项
- 当选择街道时,自动更新社区选项
- 所有联动过程均实时响应,无需额外操作
### 3. 地址格式
- 选择完成后,地址格式为:`省/市/区/街道/社区`
- 示例:`新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会`
- 地址各级信息都会保存,包括代码和名称
### 4. 数据存储
- 保存完整的地址字符串(`registeredAddress`
- 保存各级行政区划代码和名称:
- 省:`provinceCode``provinceName`
- 市:`cityCode``cityName`
- 区县:`districtCode``districtName`
- 街道:`streetCode``streetName`
- 社区:`communityCode``communityName`
## 技术实现
### 组件架构
- **组件名称**`area-cascade-picker`
- **组件位置**`components/area-cascade-picker/area-cascade-picker.vue`
- **依赖组件**`uni-popup`uni-app官方弹窗组件
### 核心技术
- **picker-view**uni-app的多列选择器组件
- **五级联动**:通过监听选择变化,动态更新下级选项
- **树形数据结构**:地区数据采用嵌套的树形结构,每级都有`children`属性
### 数据格式
```javascript
{
code: '650000', // 行政区划代码
name: '新疆维吾尔自治区', // 名称
children: [ // 下级行政区
{
code: '653100',
name: '喀什地区',
children: [...]
}
]
}
```
### 数据流程
1. **打开地址选择器**
```javascript
// company-info.vue
const selectLocation = () => {
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// 处理选择结果
formData.registeredAddress = addressData.address
// 保存各级信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
// ... 其他字段
}
})
}
```
2. **选择地址过程**
```javascript
// area-cascade-picker.vue
bindChange(e) {
// 检测哪一级发生变化
// 更新对应级别及其下级的选项列表
// 触发change回调可选
}
```
3. **确认选择**
```javascript
// area-cascade-picker.vue
confirm() {
const addressData = {
address: '新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会',
province: { code: '650000', name: '新疆维吾尔自治区' },
city: { code: '653100', name: '喀什地区' },
district: { code: '653101', name: '喀什市' },
street: { code: '65310101', name: '学府街道' },
community: { code: '6531010101', name: '学府社区居委会' }
}
// 调用success回调
callback(addressData)
}
```
## 文件结构
```
components/
└── area-cascade-picker/
└── area-cascade-picker.vue # 五级联动地址选择组件
pages/complete-info/
└── company-info.vue # 企业信息补全页面
```
## 数据字段
在 `company-info.vue` 的 `formData` 中的地址相关字段:
- `registeredAddress`: string - 完整地址(格式:省/市/区/街道/社区)
- `registeredAddressName`: string - 地址名称
- `provinceCode`: string - 省级行政区划代码
- `provinceName`: string - 省级名称
- `cityCode`: string - 市级行政区划代码
- `cityName`: string - 市级名称
- `districtCode`: string - 区县行政区划代码
- `districtName`: string - 区县名称
- `streetCode`: string - 街道行政区划代码
- `streetName`: string - 街道名称
- `communityCode`: string - 社区行政区划代码
- `communityName`: string - 社区名称
## 使用方法
### 用户操作流程
1. 在企业信息页面,点击"企业注册地点"右侧的箭头图标
2. 弹出五级联动地址选择器
3. 依次选择:
- 第一列:选择省/直辖市/自治区
- 第二列:选择市/地区(根据第一列自动更新)
- 第三列:选择区/县(根据第二列自动更新)
- 第四列:选择街道/乡镇(根据第三列自动更新)
- 第五列:选择社区/居委会(根据第四列自动更新)
4. 确认选择无误后,点击右上角"确认"按钮
5. 选择器关闭,地址自动填充到表单中
### 界面说明
- **顶部标题栏**
- 左侧"取消"按钮:关闭选择器,不保存
- 中间标题:显示"选择企业注册地点"
- 右侧"确认"按钮:确认选择并保存
- **五列选择器**
- 每列显示当前级别的所有选项
- 可上下滑动选择
- 选中项高亮显示
- 各列之间自动联动
## 注意事项
1. **数据来源**
- 当前使用本地模拟数据,包含主要城市的五级地址
- 生产环境建议接入后端API提供完整的全国地址数据
- 后端API接口`/app/common/area/cascade`(需要实现)
2. **数据格式要求**
- 必须是树形结构,每级通过`children`属性嵌套
- 每个节点必须包含`code`(行政区划代码)和`name`(名称)
- 最深五级:省 → 市 → 区县 → 街道 → 社区
3. **性能考虑**
- 地址数据量大时,建议使用懒加载方式
- 可以先加载省市区,街道和社区按需加载
- 考虑使用缓存机制,避免重复加载
4. **兼容性**
- 支持H5、微信小程序等多端
- 使用uni-app原生组件兼容性好
## 后续优化建议
1. **数据优化**
- 接入后端API提供完整的全国地址数据
- 实现地址数据的懒加载
- 添加地址数据缓存机制
2. **功能增强**
- 支持默认值回显(编辑时显示已选地址)
- 添加搜索功能,快速定位地址
- 支持手动输入详细地址(如门牌号)
- 增加常用地址保存功能
3. **用户体验**
- 优化滑动选择的流畅度
- 添加选择预览功能
- 支持快速选择最近使用的地址
4. **数据扩展**
- 支持国际地址选择
- 添加邮政编码自动填充
- 提供经纬度坐标(结合地理编码服务)

View File

@@ -0,0 +1,117 @@
# 企业我的页面功能说明
## 功能概述
本功能为企业用户提供了专门的"我的"页面和企业信息展示页面,实现了根据用户类型显示不同内容的功能。
## 页面结构
### 1. 企业我的页面 (`pages/mine/company-mine.vue`)
- **功能**: 企业用户的个人中心页面
- **特点**:
- 显示企业头像、名称和信息完整度
- 包含服务专区(实名认证、通知与提醒)
- 提供退出登录功能
- 点击头像区域可跳转到企业信息页面
### 2. 企业信息展示页面 (`pages/mine/company-info.vue`)
- **功能**: 显示详细的企业信息
- **特点**:
- 显示企业头像编辑功能
- 展示完整的企业信息(名称、统一社会代码、注册地点等)
- 支持编辑各项企业信息
- 包含企业联系人和法人信息
### 3. 修改后的我的页面 (`pages/mine/mine.vue`)
- **功能**: 根据用户类型显示不同的内容
- **特点**:
- 企业用户显示企业信息卡片
- 求职者用户显示个人简历信息
- 自动根据 `userInfo.isCompanyUser` 字段判断用户类型
## 用户类型判断
系统通过 `userInfo.isCompanyUser` 字段来判断用户类型:
- `0` = 企业用户
- `1` = 求职者
- `2` = 网格员
- `3` = 政府人员
## 页面跳转逻辑
### 从我的页面跳转
- **企业用户**: 点击头像区域 → 跳转到企业信息页面 (`/pages/mine/company-info`)
- **求职者用户**: 点击头像区域 → 跳转到简历页面 (`/packageA/pages/myResume/myResume`)
### 企业信息页面功能
- 点击头像 → 编辑头像(调用相册选择图片)
- 点击各项信息 → 跳转到对应的编辑页面(需要后续开发)
## 路由配置
新增的路由配置:
```json
{
"path": "pages/mine/company-mine",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/mine/company-info",
"style": {
"navigationBarTitleText": "企业信息",
"navigationStyle": "custom"
}
}
```
## 测试页面
创建了测试页面 `pages/test/company-mine-test.vue` 用于测试功能:
- 用户类型切换测试
- 页面跳转测试
- 用户信息显示
## 样式特点
### 企业信息卡片
- 白色背景,圆角设计
- 阴影效果现代化UI
- 头像圆形显示
- 信息完整度显示
### 企业信息页面
- 清晰的信息层级
- 可点击的编辑区域
- 统一的视觉风格
## 数据流
1. 用户登录时设置 `userInfo.isCompanyUser` 字段
2. 我的页面根据此字段判断显示内容
3. 企业用户点击头像跳转到企业信息页面
4. 企业信息页面展示详细的企业数据
## 后续开发建议
1. **编辑功能**: 为每个信息项创建对应的编辑页面
2. **数据接口**: 连接真实的企业信息API
3. **头像上传**: 完善头像上传功能
4. **表单验证**: 添加企业信息编辑的表单验证
5. **权限控制**: 根据用户权限控制可编辑的字段
## 使用方法
1. 在测试页面切换用户类型为企业用户
2. 访问我的页面,查看企业信息卡片
3. 点击头像区域跳转到企业信息页面
4. 在企业信息页面查看详细的企业信息
## 注意事项
- 确保用户类型字段正确设置
- 企业信息数据需要从后端API获取
- 头像上传功能需要配置服务器接口
- 编辑页面需要根据实际需求进行开发

View File

@@ -0,0 +1,187 @@
# 企业搜索功能实现说明
## 功能概述
根据用户类型对发布岗位页面的"招聘公司"输入框进行不同的交互处理:
- **企业用户**:直接输入公司名称
- **网格员**:点击输入框跳转到企业搜索页面,支持模糊查询
## 实现细节
### 1. 用户类型判断
通过 `userInfo.isCompanyUser` 字段判断用户类型:
- `0`: 企业用户
- `1`: 求职者
- `2`: 网格员
- `3`: 政府人员
### 2. 页面修改
#### 发布岗位页面 (`pages/job/publishJob.vue`)
**模板修改:**
```vue
<!-- 企业用户直接输入 -->
<input
v-if="isCompanyUser"
class="input"
placeholder="请输入公司名称"
v-model="formData.companyName"
/>
<!-- 网格员点击跳转到搜索页面 -->
<view
v-else
class="company-selector"
@click="openCompanySearch"
>
<view class="selector-text" :class="{ 'placeholder': !formData.companyName }">
{{ formData.companyName || '请选择企业' }}
</view>
<view class="selector-icon">
<view class="arrow-icon">></view>
</view>
</view>
```
**脚本修改:**
- 添加用户类型判断逻辑
- 添加打开企业搜索页面的方法
- 添加页面显示时处理返回数据的逻辑
#### 企业搜索页面 (`pages/job/companySearch.vue`)
**功能特性:**
- 搜索框支持实时输入
- 防抖节流500ms延迟执行搜索
- 调用接口:`/app/company/likeList`,参数:`name`
- 支持企业选择和数据回传
- 空状态和加载状态处理
**核心代码:**
```javascript
// 防抖搜索
const onSearchInput = () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
if (searchKeyword.value.trim()) {
searchCompanies();
} else {
searchResults.value = [];
}
}, 500);
};
// 搜索企业
const searchCompanies = async () => {
const response = await createRequest('/app/company/likeList', {
name: searchKeyword.value.trim()
}, 'GET', false);
if (response.code === 200) {
searchResults.value = response.data || [];
}
};
// 选择企业
const selectCompany = (company) => {
uni.navigateBack({
success: () => {
getApp().globalData = getApp().globalData || {};
getApp().globalData.selectedCompany = company;
}
});
};
```
### 3. 数据传递
使用全局数据传递选中的企业信息:
**企业搜索页面:**
```javascript
// 选择企业后设置全局数据
getApp().globalData.selectedCompany = company;
```
**发布岗位页面:**
```javascript
// 页面显示时检查全局数据
onShow(() => {
const app = getApp();
if (app.globalData && app.globalData.selectedCompany) {
const selectedCompany = app.globalData.selectedCompany;
formData.companyName = selectedCompany.name;
formData.companyId = selectedCompany.id;
// 清除全局数据
app.globalData.selectedCompany = null;
}
});
```
### 4. 页面配置
`pages.json` 中添加了企业搜索页面配置:
```json
{
"path": "pages/job/companySearch",
"style": {
"navigationBarTitleText": "选择企业",
"navigationStyle": "custom",
"disableScroll": false,
"enablePullDownRefresh": false,
"backgroundColor": "#f5f5f5"
}
}
```
### 5. 测试页面
创建了测试页面 `pages/test/company-search-test.vue` 用于验证功能:
- 用户类型切换
- 功能说明展示
- 直接跳转到发布岗位页面测试
## 使用说明
### 企业用户
1. 进入发布岗位页面
2. 招聘公司输入框为普通输入框
3. 直接输入公司名称
### 网格员
1. 进入发布岗位页面
2. 点击"招聘公司"输入框
3. 跳转到企业搜索页面
4. 输入企业名称进行搜索(支持防抖)
5. 选择企业后自动返回
6. 企业名称显示在输入框中
## 技术特点
1. **防抖节流**搜索输入500ms延迟避免频繁请求
2. **用户类型判断**:根据 `isCompanyUser` 字段动态显示不同交互
3. **数据传递**:使用全局数据实现页面间数据传递
4. **响应式设计**:支持不同屏幕尺寸
5. **错误处理**:完善的错误提示和空状态处理
## 接口说明
**搜索企业接口:**
- 地址:`/app/company/likeList`
- 方法:`GET`
- 参数:`name` (企业名称)
- 返回:企业列表数据
## 注意事项
1. 确保用户类型字段 `isCompanyUser` 正确设置
2. 搜索接口需要支持模糊查询
3. 企业数据需要包含 `id``name` 字段
4. 防抖时间可根据实际需求调整

View File

@@ -0,0 +1,284 @@
# 微信小程序组件依赖问题解决方案
## 问题描述
```
components/IconfontIcon/IconfontIcon.js 已被代码依赖分析忽略,无法被其他模块引用。
你可根据控制台中的【代码依赖分析】告警信息修改代码,或关闭【过滤无依赖文件】功能。
```
## 问题原因
1. **组件未被正确引用** - 组件文件存在但没有被任何页面或组件引用
2. **缺少 easycom 配置** - uni-app 项目需要在 `pages.json` 中配置组件自动引入
3. **文件路径问题** - 组件路径不正确或文件名不匹配
## ✅ 解决方案
### 方案一:配置 easycom 自动引入(推荐)✨
`pages.json` 中添加 `easycom` 配置:
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue",
"^WxAuthLogin$": "@/components/WxAuthLogin/WxAuthLogin.vue"
}
}
}
```
**配置说明:**
- `autoscan: true` - 自动扫描 `components` 目录
- `custom` - 自定义组件路径映射
- `^IconfontIcon$` - 组件名称(大小写敏感)
- 配置后无需 import直接在模板中使用
**使用方式:**
```vue
<template>
<!-- 无需 import直接使用 -->
<IconfontIcon name="home" :size="48" />
<WxAuthLogin ref="loginRef" />
</template>
```
### 方案二:手动引入组件
如果不想使用 easycom可以在需要的页面手动引入
```vue
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
</script>
<template>
<IconfontIcon name="home" />
</template>
```
### 方案三:关闭"过滤无依赖文件"功能
如果组件确实暂时不需要使用,可以在微信开发者工具中关闭此功能:
1. 打开微信开发者工具
2. 点击右上角"详情"
3. 找到"本地设置"标签
4. 取消勾选"过滤无依赖文件"
**注意:** 不推荐此方法,因为会增加打包体积。
## 🔧 完整操作步骤
### Step 1: 确认文件结构
确保组件文件存在且路径正确:
```
components/
├── IconfontIcon/
│ └── IconfontIcon.vue ✅ 文件存在
└── WxAuthLogin/
└── WxAuthLogin.vue ✅ 文件存在
```
### Step 2: 修改 pages.json
已为你自动添加了 easycom 配置,位置在 `globalStyle` 后面。
### Step 3: 重启微信开发者工具
1. 关闭微信开发者工具
2. 重新打开项目
3. 等待编译完成
### Step 4: 清除缓存
如果问题仍然存在:
1. 点击顶部菜单"工具" → "清除缓存"
2. 选择"清除文件缓存"
3. 重新编译项目
### Step 5: 验证组件可用
在任意页面中测试:
```vue
<template>
<view>
<IconfontIcon name="home" :size="48" color="#13C57C" />
</view>
</template>
<script setup>
// 使用 easycom 后无需 import
</script>
```
## 📋 配置详解
### easycom 规则说明
```json
{
"easycom": {
// 是否自动扫描 components 目录
"autoscan": true,
// 自定义规则
"custom": {
// 格式: "匹配规则": "组件路径"
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue"
}
}
}
```
**匹配规则说明:**
- `^` - 字符串开始
- `$` - 字符串结束
- `^IconfontIcon$` - 精确匹配 `IconfontIcon`
- `^uni-(.*)` - 匹配所有 `uni-` 开头的组件
### 组件命名规范
**推荐命名:**
-`IconfontIcon` - 大驼峰命名
-`WxAuthLogin` - 大驼峰命名
-`MyCustomComponent` - 大驼峰命名
**不推荐:**
-`iconfontIcon` - 小驼峰
-`iconfont-icon` - 短横线
-`Iconfont_Icon` - 下划线
## 🎯 常见问题
### Q1: 配置后仍然报错?
**解决方法:**
1. 检查 `pages.json` 语法是否正确JSON格式
2. 确认组件路径是否正确
3. 重启微信开发者工具
4. 清除缓存后重新编译
### Q2: 组件找不到?
**检查清单:**
- [ ] 文件路径是否正确:`@/components/IconfontIcon/IconfontIcon.vue`
- [ ] 文件名大小写是否一致
- [ ] 组件名称是否与配置匹配
- [ ] 是否重启了开发者工具
### Q3: 在页面中使用组件报错?
**常见原因:**
```vue
<!-- 错误使用了短横线命名 -->
<iconfont-icon name="home" />
<!-- 正确使用大驼峰命名 -->
<IconfontIcon name="home" />
```
### Q4: 多个组件如何配置?
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue",
"^WxAuthLogin$": "@/components/WxAuthLogin/WxAuthLogin.vue",
"^CustomButton$": "@/components/CustomButton/CustomButton.vue"
}
}
}
```
### Q5: autoscan 和 custom 的区别?
**autoscan自动扫描**
```
components/
├── CustomButton/
│ └── CustomButton.vue → 自动识别为 <CustomButton>
├── MyCard/
│ └── MyCard.vue → 自动识别为 <MyCard>
```
**custom自定义规则**
```json
{
"custom": {
"^Button$": "@/components/CustomButton/CustomButton.vue"
}
}
```
使用 `<Button>` 会映射到 `CustomButton.vue`
## 🔍 调试方法
### 1. 查看编译日志
在微信开发者工具控制台查看编译信息:
```
点击顶部"编译" → 查看控制台输出
```
### 2. 检查组件是否被打包
1. 打开"详情" → "本地设置"
2. 查看"代码依赖分析"信息
3. 确认组件是否在依赖树中
### 3. 手动引入测试
```vue
<script setup>
// 临时测试:手动引入
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
console.log('组件加载成功:', IconfontIcon)
</script>
```
## ✅ 验证成功标志
配置成功后,应该看到:
1. ✅ 微信开发者工具控制台无警告
2. ✅ 组件可以正常显示
3. ✅ 无需 import 即可使用
4. ✅ 组件出现在代码依赖分析中
## 📚 相关文档
- [uni-app easycom 文档](https://uniapp.dcloud.net.cn/collocation/pages.html#easycom)
- [微信小程序代码依赖分析](https://developers.weixin.qq.com/miniprogram/dev/devtools/codecompile.html)
- [组件化开发文档](https://uniapp.dcloud.net.cn/tutorial/vue3-components.html)
## 🎉 总结
该问题已通过以下方式解决:
1. ✅ 在 `pages.json` 中添加了 `easycom` 配置
2. ✅ 配置了 `IconfontIcon``WxAuthLogin` 组件的自动引入
3. ✅ 组件现在可以在任何页面中直接使用,无需 import
**下一步:**
- 重启微信开发者工具
- 清除缓存
- 开始使用组件
如果问题仍然存在,请检查:
1. 文件路径是否正确
2. 文件名大小写是否一致
3. pages.json 语法是否正确
4. 是否已重启开发者工具

View File

@@ -0,0 +1,321 @@
# 微信授权登录功能说明
## 功能概述
本次开发实现了微信授权登录功能,当用户点击首页的特定功能时,会检查用户是否已登录,如果未登录则弹出授权弹窗,而不是直接跳转到登录页面。
## 主要功能点
### 1. 需要登录验证的功能入口
以下功能在点击时会进行登录验证:
- **附近工作** - 点击后跳转到附近工作列表页
- **九宫格服务功能** - 包含9个服务项
- 服务指导
- 事业单位招录
- 简历制作
- 劳动政策指引
- 技能培训信息
- 技能评价指引
- 题库和考试
- 素质测评
- AI智能面试
- **职位列表** - 点击任意职位卡片查看详情
### 2. 登录弹窗功能
#### 弹窗特性
- 使用 `uni-popup` 组件实现弹窗效果
- 弹窗居中显示,支持关闭按钮
- 不可点击遮罩关闭,确保用户必须做出选择
#### 弹窗内容
- **Logo和标题** - 显示应用logo和欢迎信息
- **授权说明** - 列出三个要点:
- 保护您的个人信息安全
- 为您推荐更合适的岗位
- 享受完整的就业服务
- **授权按钮**
- 微信小程序:使用 `open-type="getPhoneNumber"` 获取手机号
- H5/App使用微信登录接口
- 测试登录按钮仅H5/App环境显示
- **用户协议** - 显示用户协议和隐私政策链接
### 3. 登录流程
#### 微信小程序登录流程
1. 用户点击"微信授权登录"按钮
2. 触发微信小程序的手机号授权
3. 获取到 `code``encryptedData``iv`
4. 调用后端 `/app/wxLogin` 接口
5. 后端返回 `token`
6. 存储 `token` 并获取用户信息
7. 如果用户信息不完整,跳转到完善信息页面
8. 关闭弹窗,继续用户之前的操作
#### H5/App登录流程
1. 用户点击"微信授权登录"按钮
2. 调用 `uni.login` 获取微信授权 `code`
3. 调用后端 `/app/wxLogin` 接口
4. 后续流程同上
#### 测试登录流程(仅开发环境)
1. 用户点击"测试账号登录"按钮
2. 使用测试账号密码登录
3. 后续流程同上
### 4. 登录状态管理
#### 状态恢复
- 应用启动时自动从本地缓存恢复用户信息
- 验证 `token` 是否有效
- 如果 `token` 失效,清除缓存但不跳转登录页
#### 状态检查
- 使用 `checkLogin()` 函数统一检查登录状态
- 检查 `token` 是否存在
- 检查 `hasLogin` 状态
- 如果未登录,自动打开授权弹窗
## 文件结构
```
ks-app-employment-service/
├── components/
│ └── WxAuthLogin/
│ └── WxAuthLogin.vue # 微信授权登录弹窗组件
├── pages/
│ └── index/
│ └── components/
│ └── index-one.vue # 首页组件(已修改)
├── stores/
│ └── useUserStore.js # 用户状态管理(已修改)
├── App.vue # 应用入口(已修改)
└── docs/
└── 微信授权登录功能说明.md # 本文档
```
## 核心代码说明
### 1. WxAuthLogin.vue 组件
这是一个可复用的微信授权登录弹窗组件,提供以下接口:
**Props**
-
**Events**
- `success` - 登录成功时触发
- `cancel` - 取消登录时触发
**Methods**
- `open()` - 打开弹窗
- `close()` - 关闭弹窗
**使用示例**
```vue
<template>
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess" />
</template>
<script setup>
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const wxAuthLoginRef = ref(null);
const handleLoginSuccess = () => {
console.log('登录成功');
// 执行登录后的操作
};
// 打开登录弹窗
const showLogin = () => {
wxAuthLoginRef.value?.open();
};
</script>
```
### 2. 登录检查函数
`index-one.vue` 中添加了统一的登录检查函数:
```javascript
// 登录检查函数
const checkLogin = () => {
const tokenValue = uni.getStorageSync('token') || '';
if (!tokenValue || !hasLogin.value) {
// 未登录,打开授权弹窗
wxAuthLoginRef.value?.open();
return false;
}
return true;
};
```
### 3. 点击事件处理
所有需要登录的功能都使用统一的检查逻辑:
```javascript
// 处理附近工作点击
const handleNearbyClick = () => {
if (checkLogin()) {
navTo('/pages/nearby/nearby');
}
};
// 处理服务功能点击
const handleServiceClick = (serviceType) => {
if (checkLogin()) {
navToService(serviceType);
}
};
// 处理职位详情点击
function nextDetail(job) {
if (checkLogin()) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
}
```
### 4. 状态管理优化
`useUserStore.js` 中优化了 `logOut` 函数:
```javascript
const logOut = (redirect = true) => {
hasLogin.value = false;
token.value = ''
resume.value = {}
userInfo.value = {}
role.value = {}
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
// 只有在明确需要跳转时才跳转到补全信息页
if (redirect) {
uni.redirectTo({
url: '/pages/complete-info/complete-info',
});
}
}
```
## 后端接口要求
### 1. 微信登录接口
**接口地址**: `/app/appLogin`
**请求方法**: `POST`
**请求参数**:
#### 微信小程序
```json
{
"code": "string", // 微信登录凭证
"encryptedData": "string", // 加密数据
"iv": "string" // 加密算法初始向量
}
```
#### H5/App
```json
{
"code": "string" // 微信登录凭证
}
```
**返回数据**:
```json
{
"token": "string", // 用户token
"msg": "string", // 返回消息
"code": 200 // 状态码
}
```
### 2. 获取用户信息接口
**接口地址**: `/app/user/resume`
**请求方法**: `GET`
**请求头**: `Authorization: Bearer {token}`
**返回数据**:
```json
{
"code": 200,
"data": {
"name": "string",
"phone": "string",
"jobTitle": ["string"],
"jobTitleId": "string",
// ... 其他用户信息
}
}
```
## 注意事项
1. **小程序配置**
- 需要在微信小程序后台配置服务器域名
- 需要申请手机号授权权限
2. **H5配置**
- 需要配置微信公众号的授权回调域名
- 需要引入微信JSSDK
3. **安全性**
- Token存储在本地缓存中注意加密
- 敏感操作前需要重新验证token有效性
4. **用户体验**
- 登录弹窗不可通过点击遮罩关闭,确保用户必须做出选择
- 提供测试登录按钮方便开发调试
- 登录成功后自动刷新数据
5. **兼容性**
- 使用条件编译确保在不同平台上正常运行
- 小程序、H5、App使用不同的登录逻辑
## 测试建议
### 功能测试
1. 未登录状态点击"附近工作",应弹出登录弹窗
2. 未登录状态点击九宫格任意服务,应弹出登录弹窗
3. 未登录状态点击职位列表,应弹出登录弹窗
4. 登录成功后,能够正常访问所有功能
5. 关闭登录弹窗后,不会自动跳转到登录页
### 登录流程测试
1. 微信小程序:测试手机号授权流程
2. H5测试微信网页授权流程
3. 测试账号登录功能(开发环境)
4. 测试登录失败的错误提示
5. 测试用户取消授权的处理
### 状态管理测试
1. 测试应用重启后登录状态的恢复
2. 测试token失效后的处理
3. 测试退出登录功能
4. 测试多次登录的状态切换
## 更新日志
### v1.0.0 (2024-10-20)
- 创建微信授权登录弹窗组件
- 添加登录状态检查逻辑
- 优化用户状态管理
- 更新首页各功能的登录验证
- 完善登录流程和错误处理
## 开发者
- 开发时间: 2024-10-20
- 涉及模块: 登录模块、首页模块、用户状态管理

View File

@@ -0,0 +1,138 @@
# 编译器内存溢出解决方案
## 问题描述
编译时出现 `FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory` 错误,表示 Node.js 内存不足。
## 解决方案
### 方案一:增加 Node.js 内存限制(推荐)
#### 在 HBuilderX 中设置
1. **修改 HBuilderX 配置文件**
- 关闭 HBuilderX
- 找到 HBuilderX 安装目录
- 打开 `HBuilderX\plugins\node\node_modules\@dcloudio\vite-plugin-uni\dist` 目录
- 或者在项目根目录创建 `vue.config.js` 文件
2. **创建或修改项目根目录下的 `vue.config.js`**
```javascript
module.exports = {
transpileDependencies: [],
// 增加 Node.js 内存限制
configureWebpack: {
devServer: {
disableHostCheck: true
}
}
}
```
3. **修改 HBuilderX 启动配置**
- 找到 HBuilderX 安装目录
- 编辑 `HBuilderX.exe` 的启动参数
- 创建一个批处理文件 `start-hbuilderx.bat`
```bat
@echo off
set NODE_OPTIONS=--max-old-space-size=8192
start "" "HBuilderX安装路径\HBuilderX.exe"
```
- 将内存设置为 8GB8192MB可根据实际情况调整为 4096、8192 或更大
#### 在命令行中运行(如果使用 CLI
如果您使用命令行方式编译,可以设置环境变量:
**Windows PowerShell**
```powershell
$env:NODE_OPTIONS="--max-old-space-size=8192"
```
**Windows CMD**
```cmd
set NODE_OPTIONS=--max-old-space-size=8192
```
**永久设置Windows 系统环境变量):**
1. 右键"此电脑" → "属性" → "高级系统设置" → "环境变量"
2. 在"用户变量"或"系统变量"中新建变量:
- 变量名:`NODE_OPTIONS`
- 变量值:`--max-old-space-size=8192`
3. 重启 HBuilderX
### 方案二:清理缓存
1. **清理 HBuilderX 缓存**
- 在 HBuilderX 中:运行 → 清理项目缓存
- 或者手动删除 `unpackage` 目录
2. **删除 node_modules 并重新安装**
```powershell
Remove-Item -Recurse -Force node_modules
# 如果有 package.json重新安装依赖
npm install
```
### 方案三:优化项目
1. **检查大文件**
- 检查 `static` 目录下是否有过大的图片或资源文件
- 当前项目中有 85 个 icon 图片,建议:
- 压缩图片文件
- 使用雪碧图或字体图标代替多个小图标
- 将不常用的资源移到云存储
2. **检查第三方库**
- 检查 `lib` 目录中的第三方库是否必需
- 当前已引入的库:
- dompurify@3.2.4es.js
- markdown-it.min.js
- highlight-uni.min.js
- lunar-javascript@1.7.2.js
- string-similarity.min.js
- 考虑按需引入或延迟加载
3. **优化编译配置**
在 `manifest.json` 中的 `h5.optimization` 已启用 `treeShaking`,这很好。
4. **分包加载**
- 已使用 `packageA` 分包,继续保持
- 考虑将更多页面移到分包中
### 方案四:升级 HBuilderX
确保使用最新版本的 HBuilderX新版本通常有更好的内存管理。
## 推荐操作步骤
1. **立即执行:** 设置 `NODE_OPTIONS` 环境变量为 `--max-old-space-size=8192`
2. **清理缓存:** 在 HBuilderX 中清理项目缓存
3. **重启 HBuilderX** 使用新的环境变量启动
4. **长期优化:** 压缩静态资源,优化第三方库引入
## 验证
设置完成后,重新编译项目,查看是否还会出现内存溢出错误。
## 参考资料
- [uni-app 官方文档 - 内存溢出问题](https://uniapp.dcloud.net.cn/tutorial/run/OOM.html)
- Node.js 内存限制说明:
- 默认限制:约 1.4GB32位或 1.7GB64位
- 建议设置4096MB4GB或 8192MB8GB
- 最大可设置:取决于系统可用内存
## 常见问题
**Q: 设置后仍然内存溢出?**
A: 尝试增大内存限制值,如 `--max-old-space-size=16384`16GB
**Q: 如何检查当前 Node.js 内存限制?**
A: 在命令行运行:`node -e "console.log(require('v8').getHeapStatistics().heap_size_limit/(1024*1024))"`
**Q: 编译特别慢?**
A: 内存充足但编译慢,可能是 CPU 性能问题,考虑:
- 关闭不必要的后台程序
- 使用 SSD 硬盘
- 升级硬件配置

View File

@@ -0,0 +1,129 @@
# 自定义TabBar使用说明
## 功能概述
本项目实现了基于用户类型的动态自定义TabBar支持根据用户登录状态和类型显示不同的导航项
- **未登录状态**默认显示求职者tabbar职位 + 招聘会 + AI+ + 消息 + 我的)
- **企业用户userType=0**:显示"发布岗位"导航
- **求职者用户userType=1,2,3**:显示"招聘会"导航
## 实现方案
### 1. 微信小程序原生自定义TabBar
`custom-tab-bar/` 目录下创建了微信小程序原生自定义TabBar组件
- `index.js` - 组件逻辑根据用户类型动态生成TabBar配置
- `index.wxml` - 模板文件
- `index.wxss` - 样式文件
- `index.json` - 组件配置文件
### 2. UniApp兼容的自定义TabBar组件
创建了 `components/CustomTabBar/CustomTabBar.vue` 组件,支持多端兼容:
- 支持微信小程序、H5、APP等多端
- 响应式设计,根据用户类型动态显示
- 支持消息徽章显示
- 支持页面跳转逻辑
### 3. 配置修改
`pages.json` 中启用了自定义TabBar
```json
"tabBar": {
"custom": true,
// ... 其他配置
}
```
## 使用方法
### 1. 在页面中引入自定义TabBar
```vue
<template>
<view class="page">
<!-- 页面内容 -->
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="0" />
</view>
</template>
```
### 2. 用户类型判断
组件会自动从 `useUserStore` 中获取用户信息,根据用户登录状态和 `userInfo.userType` 字段判断用户类型:
```javascript
// 用户类型说明
// 未登录: 默认显示求职者tabbar
// 0: 企业用户 - 显示"发布岗位"
// 1: 求职者 - 显示"招聘会"
// 2: 网格员 - 显示"招聘会"
// 3: 政府人员 - 显示"招聘会"
```
### 3. 动态切换用户类型
当用户登录状态或类型发生变化时TabBar会自动更新
```javascript
// 未登录状态自动显示求职者tabbar
// 登录后根据用户类型显示对应tabbar
// 切换用户类型
userInfo.value.userType = 1; // 切换到求职者
uni.setStorageSync('userInfo', userInfo.value);
// 登出时清除用户信息,自动回到未登录状态
userStore.logOut(false);
```
## 页面配置
### 已配置的页面
- `pages/index/index.vue` - 首页currentPage: 0
- `pages/careerfair/careerfair.vue` - 招聘会页面currentPage: 1
- `pages/chat/chat.vue` - AI+页面currentPage: 2
- `pages/msglog/msglog.vue` - 消息页面currentPage: 3
- `pages/mine/mine.vue` - 我的页面currentPage: 4
### 测试页面
- `pages/test/tabbar-test.vue` - TabBar功能测试页面
## 技术特点
1. **响应式设计**:根据用户类型动态显示不同的导航项
2. **多端兼容**支持微信小程序、H5、APP等平台
3. **消息徽章**:支持显示未读消息数量
4. **页面跳转**智能判断tabBar页面和普通页面的跳转方式
5. **用户类型监听**实时监听用户类型变化并更新TabBar
## 注意事项
1. 确保在 `pages.json` 中设置了 `"custom": true`
2. 每个页面的 `currentPage` 参数需要正确设置
3. 用户类型存储在 `userInfo.userType` 字段中
4. 组件会自动监听用户类型变化并更新显示
## 测试方法
1. 访问测试页面:`/pages/test/tabbar-test`
2. 切换不同的用户类型
3. 观察底部TabBar的变化
4. 测试页面跳转功能
## 故障排除
如果TabBar不显示或显示异常
1. 检查 `pages.json` 中的 `custom: true` 配置
2. 确认用户信息是否正确存储
3. 检查组件是否正确引入
4. 查看控制台是否有错误信息

View File

@@ -0,0 +1,429 @@
# 阿里图标库iconfont引入指南
## 📦 方式一:使用字体文件(推荐)
### 第一步:下载图标资源
1. 访问 [阿里图标库](https://www.iconfont.cn/)
2. 注册/登录账号
3. 搜索需要的图标,点击"添加入库"
4. 点击右上角购物车图标
5. 点击"添加至项目"(如果没有项目,先创建一个)
6. 进入"我的项目"
7. 点击"下载至本地"按钮
### 第二步:解压并复制文件
下载的压缩包中包含以下文件:
```
iconfont.css
iconfont.ttf
iconfont.woff
iconfont.woff2
iconfont.json
demo_index.html
demo.css
```
**需要的文件:**
- `iconfont.css` - 样式文件
- `iconfont.ttf` - 字体文件
- `iconfont.woff` - 字体文件
- `iconfont.woff2` - 字体文件
### 第三步:创建项目目录
在项目中创建 `static/iconfont/` 目录(如果不存在):
```
ks-app-employment-service/
├── static/
│ ├── iconfont/ ← 新建此目录
│ │ ├── iconfont.css
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ └── iconfont.woff2
│ └── ...
```
### 第四步:修改 CSS 文件
打开 `static/iconfont/iconfont.css`,修改字体文件路径:
**原始路径:**
```css
@font-face {
font-family: "iconfont";
src: url('iconfont.woff2?t=1234567890') format('woff2'),
url('iconfont.woff?t=1234567890') format('woff'),
url('iconfont.ttf?t=1234567890') format('truetype');
}
```
**修改为(相对路径):**
```css
@font-face {
font-family: "iconfont";
src: url('./iconfont.woff2?t=1234567890') format('woff2'),
url('./iconfont.woff?t=1234567890') format('woff'),
url('./iconfont.ttf?t=1234567890') format('truetype');
}
```
**或修改为(绝对路径,推荐):**
```css
@font-face {
font-family: "iconfont";
src: url('/static/iconfont/iconfont.woff2?t=1234567890') format('woff2'),
url('/static/iconfont/iconfont.woff?t=1234567890') format('woff'),
url('/static/iconfont/iconfont.ttf?t=1234567890') format('truetype');
}
```
### 第五步:在项目中引入
#### 方法 A全局引入App.vue
`App.vue` 中引入:
```vue
<style>
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
/* 其他全局样式 */
@import '@/common/animation.css';
@import '@/common/common.css';
</style>
```
#### 方法 B在 main.js 中引入
```javascript
// main.js
import './static/iconfont/iconfont.css'
```
### 第六步:使用图标
#### 使用方式 1Unicode 方式
```vue
<template>
<view class="icon">&#xe600;</view>
</template>
<style>
.icon {
font-family: "iconfont" !important;
font-size: 32rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
```
#### 使用方式 2Font Class 方式(推荐)
```vue
<template>
<view class="iconfont icon-home"></view>
<view class="iconfont icon-user"></view>
<view class="iconfont icon-search"></view>
</template>
<style scoped>
.iconfont {
font-size: 32rpx;
color: #333;
}
</style>
```
#### 使用方式 3封装为组件
创建 `components/IconfontIcon/IconfontIcon.vue`
```vue
<template>
<text class="iconfont" :class="iconClass" :style="iconStyle"></text>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
size: {
type: [String, Number],
default: 32
},
color: {
type: String,
default: '#333'
}
})
const iconClass = computed(() => `icon-${props.name}`)
const iconStyle = computed(() => ({
fontSize: `${props.size}rpx`,
color: props.color
}))
</script>
<style scoped>
.iconfont {
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
```
**使用组件:**
```vue
<template>
<IconfontIcon name="home" :size="48" color="#13C57C" />
<IconfontIcon name="user" :size="36" color="#256BFA" />
</template>
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
</script>
```
---
## 📦 方式二:使用在线链接(不推荐小程序)
### 第一步:获取在线链接
1. 在阿里图标库"我的项目"中
2. 点击"Font class"
3. 点击"查看在线链接"
4. 复制 CSS 链接
### 第二步:引入在线 CSS
`App.vue` 中:
```vue
<style>
/* 注意:小程序不支持在线字体 */
@import url("//at.alicdn.com/t/c/font_xxxxx.css");
</style>
```
**⚠️ 注意:** 微信小程序不支持外部字体文件,必须使用方式一!
---
## 📦 方式三:使用 Symbol 方式SVG
### 第一步:获取 Symbol 代码
1. 在"我的项目"中
2. 点击"Symbol"
3. 点击"生成代码"
4. 复制生成的 JS 链接
### 第二步:下载 JS 文件
将 JS 文件下载到 `static/iconfont/iconfont.js`
### 第三步:引入并使用
`App.vue` 或需要的页面中:
```vue
<template>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-home"></use>
</svg>
</template>
<script>
// 引入 Symbol 脚本
// 注意:需要在 main.js 中引入 iconfont.js
</script>
<style>
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
```
**⚠️ 注意:** 小程序对 SVG 支持有限,推荐使用方式一!
---
## 🎯 最佳实践建议
### 1. 使用 Font Class 方式(方式一)
**优点:**
- ✅ 兼容性好,支持所有平台
- ✅ 可以自定义颜色和大小
- ✅ 语义化强,易于维护
- ✅ 体积小,加载快
**缺点:**
- ❌ 只支持单色图标
### 2. 创建图标组件库
```
components/
├── IconfontIcon/
│ └── IconfontIcon.vue # 通用图标组件
```
### 3. 统一管理图标名称
创建 `config/icons.js`
```javascript
// 图标配置
export const ICONS = {
HOME: 'home',
USER: 'user',
SEARCH: 'search',
LOCATION: 'location',
PHONE: 'phone',
// ... 更多图标
}
```
使用:
```vue
<script setup>
import { ICONS } from '@/config/icons'
</script>
<template>
<IconfontIcon :name="ICONS.HOME" />
</template>
```
---
## 🔧 常见问题
### Q1: 小程序中图标不显示?
**解决方案:**
- 确保使用本地字体文件,不要使用在线链接
- 检查 CSS 中的字体路径是否正确
- 确保字体文件已正确复制到 `static/iconfont/` 目录
### Q2: 图标显示为方框?
**解决方案:**
- 检查字体文件是否完整
- 检查 `@font-face``font-family` 名称是否一致
- 清除缓存重新编译
### Q3: 如何更新图标库?
1. 在阿里图标库添加新图标到项目
2. 重新下载至本地
3. 替换 `static/iconfont/` 下的所有文件
4. 清除缓存,重新编译
### Q4: H5 和小程序路径不一致?
**解决方案:**
使用条件编译:
```css
@font-face {
font-family: "iconfont";
/* #ifdef H5 */
src: url('/static/iconfont/iconfont.woff2') format('woff2');
/* #endif */
/* #ifdef MP-WEIXIN */
src: url('./iconfont.ttf') format('truetype');
/* #endif */
}
```
---
## 📝 示例代码
### 完整示例:登录按钮
```vue
<template>
<button class="login-btn">
<text class="iconfont icon-phone"></text>
<text>手机号登录</text>
</button>
</template>
<style scoped>
.login-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background: #13C57C;
border-radius: 12rpx;
color: #fff;
}
.iconfont {
font-size: 32rpx;
margin-right: 12rpx;
}
</style>
```
---
## 🎨 推荐使用的图标
### 常用图标
- `icon-home` - 首页
- `icon-user` - 用户
- `icon-search` - 搜索
- `icon-location` - 位置
- `icon-phone` - 电话
- `icon-message` - 消息
- `icon-setting` - 设置
- `icon-star` - 收藏
- `icon-share` - 分享
- `icon-close` - 关闭
---
## 📚 相关资源
- [阿里图标库官网](https://www.iconfont.cn/)
- [uni-app 字体图标文档](https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#%E5%AD%97%E4%BD%93%E5%9B%BE%E6%A0%87)
- [CSS @font-face](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@font-face)
---
## ✅ 检查清单
- [ ] 已下载图标文件到 `static/iconfont/` 目录
- [ ] 已修改 CSS 中的字体文件路径
- [ ] 已在 App.vue 中引入 iconfont.css
- [ ] 已测试图标显示正常
- [ ] 已封装图标组件(可选)
- [ ] 已统一管理图标名称(可选)

View File

@@ -0,0 +1,407 @@
# 阿里图标库快速开始 🚀
## 一、5分钟快速上手
### Step 1: 下载图标文件2分钟
1. 访问 https://www.iconfont.cn/
2. 登录后搜索图标,点击"添加入库"
3. 购物车 → 添加至项目(没有项目先创建)
4. 我的项目 → 下载至本地
### Step 2: 放置文件1分钟
解压下载的文件将以下4个文件复制到 `static/iconfont/` 目录:
```
✅ iconfont.css
✅ iconfont.ttf
✅ iconfont.woff
✅ iconfont.woff2
```
### Step 3: 修改CSS路径1分钟
打开 `static/iconfont/iconfont.css`,将字体路径修改为相对路径:
```css
@font-face {
font-family: "iconfont";
src: url('./iconfont.woff2?t=xxx') format('woff2'),
url('./iconfont.woff?t=xxx') format('woff'),
url('./iconfont.ttf?t=xxx') format('truetype');
}
```
### Step 4: 全局引入1分钟
`App.vue``<style>` 标签中添加:
```vue
<style>
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
</style>
```
### Step 5: 开始使用 ✨
```vue
<template>
<!-- 直接使用 -->
<text class="iconfont icon-home"></text>
<!-- 或使用组件 -->
<IconfontIcon name="home" :size="48" color="#13C57C" />
</template>
```
---
## 二、推荐使用方式
### 方式 A使用封装的组件最推荐👍
```vue
<template>
<IconfontIcon name="phone" :size="40" color="#13C57C" />
</template>
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
</script>
```
**优点:**
- ✅ 统一管理,易于维护
- ✅ 支持动态修改大小和颜色
- ✅ 语义清晰
- ✅ 支持点击事件
### 方式 B使用配置常量推荐
```vue
<template>
<IconfontIcon
:name="ICONS.HOME"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.PRIMARY"
/>
</template>
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
import { ICONS, ICON_SIZES, ICON_COLORS } from '@/config/icons'
</script>
```
**优点:**
- ✅ 统一图标名称
- ✅ 避免拼写错误
- ✅ IDE 自动补全
- ✅ 便于重构
### 方式 C直接使用类名
```vue
<template>
<text class="iconfont icon-home" style="font-size: 32rpx; color: #333;"></text>
</template>
```
---
## 三、常用场景示例
### 场景1导航栏图标
```vue
<template>
<view class="navbar">
<IconfontIcon name="arrow-left" :size="40" @click="goBack" />
<text class="title">页面标题</text>
<IconfontIcon name="share" :size="36" @click="share" />
</view>
</template>
<script setup>
const goBack = () => {
uni.navigateBack()
}
const share = () => {
// 分享逻辑
}
</script>
```
### 场景2按钮图标
```vue
<template>
<button class="primary-btn">
<IconfontIcon name="phone" :size="32" color="#FFFFFF" />
<text>手机号登录</text>
</button>
</template>
<style>
.primary-btn {
display: flex;
align-items: center;
gap: 12rpx;
}
</style>
```
### 场景3列表项图标
```vue
<template>
<view class="list-item">
<IconfontIcon name="location" :size="36" color="#13C57C" />
<text class="text">工作地点</text>
<IconfontIcon name="arrow-right" :size="28" color="#999" />
</view>
</template>
```
### 场景4状态图标
```vue
<template>
<view class="status-box">
<IconfontIcon
:name="status.icon"
:size="64"
:color="status.color"
/>
<text>{{ status.text }}</text>
</view>
</template>
<script setup>
import { computed } from 'vue'
const orderStatus = ref('success')
const status = computed(() => {
const map = {
success: { icon: 'success', color: '#13C57C', text: '提交成功' },
error: { icon: 'error', color: '#F44336', text: '提交失败' },
loading: { icon: 'loading', color: '#256BFA', text: '处理中...' }
}
return map[orderStatus.value]
})
</script>
```
---
## 四、组件API说明
### IconfontIcon 组件
**Props:**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| name | String | - | 图标名称(必填),如:'home' 或 'icon-home' |
| size | String/Number | 32 | 图标大小单位rpx |
| color | String | - | 图标颜色支持十六进制、rgb等 |
| bold | Boolean | false | 是否加粗 |
**Events:**
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| click | 点击图标时触发 | event |
**使用示例:**
```vue
<IconfontIcon
name="home"
:size="48"
color="#13C57C"
:bold="true"
@click="handleClick"
/>
```
---
## 五、配置说明
### 图标名称配置config/icons.js
```javascript
export const ICONS = {
HOME: 'home',
USER: 'user',
SEARCH: 'search',
// ... 更多图标
}
```
### 尺寸预设
```javascript
export const ICON_SIZES = {
MINI: 24, // 24rpx
SMALL: 28, // 28rpx
NORMAL: 32, // 32rpx默认
LARGE: 40, // 40rpx
XLARGE: 48, // 48rpx
}
```
### 颜色预设
```javascript
export const ICON_COLORS = {
PRIMARY: '#13C57C', // 主色调
SECONDARY: '#256BFA', // 次要色
SUCCESS: '#13C57C', // 成功
WARNING: '#FF9800', // 警告
DANGER: '#F44336', // 危险
TEXT: '#333333', // 文本色
}
```
---
## 六、常见问题
### Q1: 图标不显示?
**检查清单:**
- [ ] 文件是否已复制到 `static/iconfont/` 目录
- [ ] CSS路径是否正确修改
- [ ] 是否已在 App.vue 中引入
- [ ] 图标类名是否正确(如:`icon-home`
- [ ] 清除缓存并重新编译
### Q2: 如何查看可用的图标?
1. 打开下载包中的 `demo_index.html`
2. 或查看 `iconfont.css` 中的类名
3. 类名格式通常为 `.icon-xxx:before`
### Q3: 如何更新图标?
1. 在阿里图标库添加新图标到项目
2. 重新下载至本地
3. 替换 `static/iconfont/` 下的所有文件
4. 清除缓存,重新编译
### Q4: 小程序能用在线链接吗?
❌ 不能!微信小程序必须使用本地字体文件。
---
## 七、最佳实践
### ✅ 推荐做法
1. **统一管理图标名称**
```javascript
// 使用配置文件
import { ICONS } from '@/config/icons'
```
2. **使用封装的组件**
```vue
<IconfontIcon name="home" />
```
3. **预设常用尺寸和颜色**
```javascript
import { ICON_SIZES, ICON_COLORS } from '@/config/icons'
```
4. **语义化命名**
```javascript
const ICONS = {
HOME: 'home', // ✅ 语义清晰
USER_CENTER: 'user', // ✅ 明确用途
}
```
### ❌ 不推荐做法
1. **硬编码图标名称**
```vue
<text class="iconfont icon-home"></text> <!-- ❌ 不推荐 -->
```
2. **使用在线链接(小程序)**
```css
@import url("//at.alicdn.com/xxx.css"); /* ❌ 小程序不支持 */
```
3. **直接使用 Unicode**
```vue
<text class="iconfont">&#xe600;</text> <!-- ❌ 不直观 -->
```
---
## 八、测试页面
已为你创建了测试页面,可以查看各种使用方式:
**路径:** `pages/demo/iconfont-demo.vue`
在 `pages.json` 中添加页面配置即可访问:
```json
{
"path": "pages/demo/iconfont-demo",
"style": {
"navigationBarTitleText": "图标示例"
}
}
```
---
## 九、相关文件
```
项目结构:
├── components/
│ └── IconfontIcon/
│ └── IconfontIcon.vue # 图标组件
├── config/
│ └── icons.js # 图标配置
├── static/
│ └── iconfont/
│ ├── iconfont.css # 样式文件
│ ├── iconfont.ttf # 字体文件
│ ├── iconfont.woff # 字体文件
│ ├── iconfont.woff2 # 字体文件
│ └── README.md # 说明文档
├── pages/
│ └── demo/
│ └── iconfont-demo.vue # 测试页面
└── docs/
├── 阿里图标库引入指南.md # 详细文档
└── 阿里图标库快速开始.md # 本文档
```
---
## 十、总结
✅ **记住这三步:**
1. **下载** - 从阿里图标库下载文件
2. **放置** - 复制到 `static/iconfont/` 目录
3. **引入** - 在 `App.vue` 中引入 CSS
🎉 **就是这么简单!**
如有问题,请参考详细文档:`docs/阿里图标库引入指南.md`

View File

@@ -20,7 +20,16 @@ export function useColumnCount(onChange = () => {}) {
// }
// }
const calcColumn = () => {
const width = uni.getSystemInfoSync().windowWidth
// 使用新的API替代已废弃的getSystemInfoSync
let width
// #ifdef MP-WEIXIN
const mpSystemInfo = uni.getWindowInfo()
width = mpSystemInfo.windowWidth
// #endif
// #ifndef MP-WEIXIN
const otherSystemInfo = uni.getSystemInfoSync()
width = otherSystemInfo.windowWidth
// #endif
let count = 2
if (width >= 1000) {
@@ -46,15 +55,20 @@ export function useColumnCount(onChange = () => {}) {
onMounted(() => {
columnCount.value = 2
calcColumn()
// if (process.client) {
window.addEventListener('resize', calcColumn)
// }
// 只在H5环境下添加resize监听器
// #ifdef H5
if (typeof window !== 'undefined') {
window.addEventListener('resize', calcColumn)
}
// #endif
})
onUnmounted(() => {
// if (process.client) {
window.removeEventListener('resize', calcColumn)
// }
// #ifdef H5
if (typeof window !== 'undefined') {
window.removeEventListener('resize', calcColumn)
}
// #endif
})
// 列数变化时执行回调

View File

@@ -149,7 +149,19 @@ export function useAudioRecorder() {
const startRecording = async () => {
if (isRecording.value) return
// #ifdef MP-WEIXIN
$api.msg('小程序暂不支持语音识别功能');
return;
// #endif
// #ifdef H5
try {
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
$api.msg('当前环境不支持录音功能');
return;
}
recognizedText.value = ''
lastFinalText.value = ''
await connectWebSocket()
@@ -191,6 +203,7 @@ export function useAudioRecorder() {
console.error('启动失败:', err)
cleanup()
}
// #endif
}
const stopRecording = () => {

View File

@@ -4,39 +4,69 @@ import {
export function useScrollDirection(options = {}) {
const {
threshold = 200, // 滚动偏移阈值
throttleTime = 100, // 节流时间(毫秒)
onChange = null // 滚动方向变化的回调
threshold = 50, // 滚动偏移阈值,降低以更敏感
throttleTime = 16, // 节流时间(毫秒)约60fps
onChange = null, // 滚动方向变化的回调
hideThreshold = 100, // 隐藏区域的滚动阈值
enablePerformanceMode = true // 启用性能优化模式
} = options
const lastScrollTop = ref(0)
const accumulatedScroll = ref(0)
const isScrollingDown = ref(false)
const shouldHideTop = ref(false) // 控制顶部区域隐藏
const shouldStickyFilter = ref(false) // 控制筛选区域吸顶
let lastInvoke = 0
function handleScroll(e) {
const now = Date.now()
if (now - lastInvoke < throttleTime) return
if (enablePerformanceMode && now - lastInvoke < throttleTime) return
lastInvoke = now
const scrollTop = e.detail.scrollTop
const delta = scrollTop - lastScrollTop.value
accumulatedScroll.value += delta
if (accumulatedScroll.value > threshold) {
if (!isScrollingDown.value) {
isScrollingDown.value = true
onChange?.(true) // 通知变更为向下
// 控制顶部区域隐藏
if (scrollTop > hideThreshold) {
if (!shouldHideTop.value) {
shouldHideTop.value = true
}
} else {
if (shouldHideTop.value) {
shouldHideTop.value = false
}
accumulatedScroll.value = 0
}
if (accumulatedScroll.value < -threshold) {
if (isScrollingDown.value) {
isScrollingDown.value = false
onChange?.(false) // 通知变更为向上
// 控制筛选区域吸顶(当顶部区域隐藏时)
if (scrollTop > hideThreshold + 50) { // 稍微延迟吸顶
if (!shouldStickyFilter.value) {
shouldStickyFilter.value = true
}
} else {
if (shouldStickyFilter.value) {
shouldStickyFilter.value = false
}
}
// 滚动方向检测(仅在性能模式下使用阈值)
if (!enablePerformanceMode || Math.abs(accumulatedScroll.value) > threshold) {
if (accumulatedScroll.value > 0) {
// 向下滚动
if (!isScrollingDown.value) {
isScrollingDown.value = true
onChange?.(true) // 通知变更为向下
}
} else {
// 向上滚动
if (isScrollingDown.value) {
isScrollingDown.value = false
onChange?.(false) // 通知变更为向上
}
}
if (enablePerformanceMode) {
accumulatedScroll.value = 0
}
accumulatedScroll.value = 0
}
lastScrollTop.value = scrollTop
@@ -44,6 +74,8 @@ export function useScrollDirection(options = {}) {
return {
isScrollingDown,
shouldHideTop,
shouldStickyFilter,
handleScroll
}
}

View File

@@ -15,8 +15,17 @@ export function useTTSPlayer(wsUrl) {
const isPaused = ref(false)
const isComplete = ref(false)
const audioContext = new(window.AudioContext || window.webkitAudioContext)()
let playTime = audioContext.currentTime
// #ifdef H5
const audioContext = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext)
? new(window.AudioContext || window.webkitAudioContext)()
: null
// #endif
// #ifdef MP-WEIXIN
const audioContext = null // 微信小程序不支持 AudioContext
// #endif
let playTime = audioContext ? audioContext.currentTime : 0
let sourceNodes = []
let socket = null
let sampleRate = 16000
@@ -28,6 +37,11 @@ export function useTTSPlayer(wsUrl) {
let activePlayId = 0
const speak = (text) => {
if (!audioContext) {
console.warn('⚠️ TTS not supported in current environment');
return;
}
console.log('🎤 TTS speak function called');
console.log('📝 Text to synthesize:', text ? text.substring(0, 100) + '...' : 'No text');
console.log('🔗 WebSocket URL:', wsUrl);
@@ -44,6 +58,11 @@ export function useTTSPlayer(wsUrl) {
}
const pause = () => {
if (!audioContext) {
console.warn('⚠️ TTS not supported in current environment');
return;
}
console.log('⏸️ TTS pause called');
console.log('🔊 AudioContext state:', audioContext.state);
console.log('🔊 Is speaking before pause:', isSpeaking.value);
@@ -63,6 +82,11 @@ export function useTTSPlayer(wsUrl) {
}
const resume = () => {
if (!audioContext) {
console.warn('⚠️ TTS not supported in current environment');
return;
}
console.log('▶️ TTS resume called');
console.log('🔊 AudioContext state:', audioContext.state);
console.log('🔊 Is speaking before resume:', isSpeaking.value);
@@ -89,7 +113,7 @@ export function useTTSPlayer(wsUrl) {
isSpeaking.value = false
isPaused.value = false
isComplete.value = false
playTime = audioContext.currentTime
playTime = audioContext ? audioContext.currentTime : 0
sourceNodes.forEach(node => {
try {
@@ -113,11 +137,16 @@ export function useTTSPlayer(wsUrl) {
isSpeaking.value = false
isPaused.value = false
isComplete.value = false
playTime = audioContext.currentTime
playTime = audioContext ? audioContext.currentTime : 0
initWebSocket()
}
const initWebSocket = () => {
if (!audioContext) {
console.warn('⚠️ WebSocket TTS not supported in current environment');
return;
}
const thisPlayId = currentPlayId
console.log('🔌 Initializing WebSocket connection');
console.log('🔗 WebSocket URL:', wsUrl);
@@ -167,7 +196,7 @@ export function useTTSPlayer(wsUrl) {
console.log('✅ TTS synthesis completed');
isComplete.value = true
// 计算剩余播放时间,确保播放完整
const remainingTime = Math.max(0, (playTime - audioContext.currentTime) * 1000);
const remainingTime = audioContext ? Math.max(0, (playTime - audioContext.currentTime) * 1000) : 0;
console.log('⏱️ Remaining play time:', remainingTime + 'ms');
setTimeout(() => {
if (thisPlayId === activePlayId) {
@@ -205,6 +234,8 @@ export function useTTSPlayer(wsUrl) {
}
const pcmToAudioBuffer = (pcm, sampleRate, numChannels) => {
if (!audioContext) return null;
const length = pcm.length / numChannels
const audioBuffer = audioContext.createBuffer(numChannels, length, sampleRate)
for (let ch = 0; ch < numChannels; ch++) {
@@ -218,6 +249,8 @@ export function useTTSPlayer(wsUrl) {
}
const playBuffer = (audioBuffer) => {
if (!audioContext || !audioBuffer) return;
console.log('🎵 playBuffer called, duration:', audioBuffer.duration + 's');
if (!isSpeaking.value) {
playTime = audioContext.currentTime
@@ -259,7 +292,10 @@ export function useTTSPlayer(wsUrl) {
onHide(cancelAudio)
onUnload(cancelAudio)
initWebSocket()
// 只在支持 AudioContext 的环境中初始化 WebSocket
if (audioContext) {
initWebSocket()
}
return {
speak,

36
main.js
View File

@@ -1,3 +1,8 @@
/*
* @Date: 2025-11-03 10:52:09
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 17:26:29
*/
import App from '@/App'
import * as Pinia from 'pinia'
import globalFunction from '@/common/globalFunction'
@@ -13,6 +18,13 @@ import SelectPopup from '@/components/selectPopup/selectPopup.vue'
import SelectPopupPlugin from '@/components/selectPopup/selectPopupPlugin';
import RenderJobs from '@/components/renderJobs/renderJobs.vue';
import RenderCompanys from '@/components/renderCompanys/renderCompanys.vue';
import uniIcons from './uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
import uniPopup from './uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import uniDataSelect from './uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue'
import uniSwipeAction from './uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue'
import uniSwipeActionItem from './uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue'
import storeRc from './utilsRc/store/index.js'
// iconfont.css 已在 App.vue 中通过 @import 引入,无需在此处重复引入
// import Tabbar from '@/components/tabbar/midell-box.vue'
// 自动导入 directives 目录下所有指令
const directives = import.meta.glob('./directives/*.js', {
@@ -23,9 +35,10 @@ import {
createSSRApp,
} from 'vue'
const foldFeature = window.visualViewport && 'segments' in window.visualViewport
console.log('是否支持多段屏幕:', foldFeature)
// const foldFeature = window.visualViewport && 'segments' in window.visualViewport
// console.log('是否支持多段屏幕:', foldFeature)
import { getDict } from '@/apiRc/system/dict.js';
// 全局组件
export function createApp() {
const app = createSSRApp(App)
@@ -37,6 +50,23 @@ export function createApp() {
app.component('SelectPopup', SelectPopup)
app.component('RenderJobs', RenderJobs)
app.component('RenderCompanys', RenderCompanys)
app.component('uni-icons', uniIcons)
app.component('uni-popup', uniPopup)
app.component('uni-data-select', uniDataSelect)
app.component('uni-swipe-action', uniSwipeAction)
app.component('uni-swipe-action-item', uniSwipeActionItem)
app.config.globalProperties.$getDict = getDict;
app.config.globalProperties.$store = storeRc;
app.config.globalProperties.$getDictSelectOption = async (dictType, isDigital = false, forceRefresh = false) => {
const dictData = await getDict(dictType, forceRefresh);
return dictData.map(item => ({
value: isDigital ? Number(item.dictValue || item.dictvalue) : (item.dictValue || item.dictvalue),
label: item.dictLabel || item.dictlabel,
...item
}));
};
// app.component('tabbar-custom', Tabbar)
for (const path in directives) {
@@ -55,6 +85,8 @@ export function createApp() {
app.use(SelectPopupPlugin);
app.use(Pinia.createPinia());
// 注册vuex
app.use(storeRc);
return {
app,

View File

@@ -1,102 +1,103 @@
{
"name": "qingdao-employment-service",
"appid": "__UNI__C939371",
"description": "招聘",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios": {},
/* SDK */
"sdkConfigs": {}
}
},
/* */
"quickapp": {},
/* */
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "用于用户选择地图查看位置"
}
}
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"locale": "zh-Hans",
"h5": {
"router": {
"base": "/ks_app/",
"mode": "hash"
},
"title": "青岛智慧就业服务",
"optimization": {
"treeShaking": {
"enable": true
}
},
"sdkConfigs": {
"maps": {
"amap": {
"key": "9cfc9370bd8a941951da1cea0308e9e3",
"securityJsCode": "7b16386c7f744c3ca05595965f2b037f",
"serviceHost": ""
}
}
}
}
}
{
"name" : "qingdao-employment-service",
"appid" : "__UNI__F0ABFDF",
"description" : "招聘",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx9d1cbc11c8c40ba7",
"setting" : {
"urlCheck" : false,
"es6" : true,
"postcss" : true,
"minified" : true
},
"usingComponents" : true,
"permission" : {
"scope.userLocation" : {
"desc" : "用于用户选择地图查看位置"
}
},
"libVersion" : "3.5.7"
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"locale" : "zh-Hans",
"h5" : {
"router" : {
"base" : "/ks_app/",
"mode" : "hash"
},
"title" : "青岛智慧就业服务",
"optimization" : {
"treeShaking" : {
"enable" : true
}
},
"sdkConfigs" : {
"maps" : {
"amap" : {
"key" : "9cfc9370bd8a941951da1cea0308e9e3",
"securityJsCode" : "7b16386c7f744c3ca05595965f2b037f",
"serviceHost" : ""
}
}
}
}
}

62
package-lock.json generated Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "ks-app-employment-service",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"@dcloudio/uni-ui": "^1.5.11",
"dayjs": "^1.11.19",
"sm-crypto": "^0.3.13"
}
},
"node_modules/@dcloudio/uni-ui": {
"version": "1.5.11",
"resolved": "https://registry.npmjs.org/@dcloudio/uni-ui/-/uni-ui-1.5.11.tgz",
"integrity": "sha512-DBtk046ofmeFd82zRI7d89SoEwrAxYzUN3WVPm1DIBkpLPG5F5QDNkHMnZGu2wNrMEmGBjBpUh3vqEY1L3jaMw=="
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/sm-crypto": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/sm-crypto/-/sm-crypto-0.3.13.tgz",
"integrity": "sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==",
"dependencies": {
"jsbn": "^1.1.0"
}
}
},
"dependencies": {
"@dcloudio/uni-ui": {
"version": "1.5.11",
"resolved": "https://registry.npmjs.org/@dcloudio/uni-ui/-/uni-ui-1.5.11.tgz",
"integrity": "sha512-DBtk046ofmeFd82zRI7d89SoEwrAxYzUN3WVPm1DIBkpLPG5F5QDNkHMnZGu2wNrMEmGBjBpUh3vqEY1L3jaMw=="
},
"dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
},
"jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"sm-crypto": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/sm-crypto/-/sm-crypto-0.3.13.tgz",
"integrity": "sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==",
"requires": {
"jsbn": "^1.1.0"
}
}
}
}

7
package.json Normal file
View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"@dcloudio/uni-ui": "^1.5.11",
"dayjs": "^1.11.19",
"sm-crypto": "^0.3.13"
}
}

View File

@@ -32,7 +32,7 @@ const pageState = reactive({
onLoad(() => {
console.log('onLoad');
// $api.sleep(2000).then(() => {
// navTo('/pages/login/login');
// navTo('/pages/complete-info/complete-info');
// });
getJobList();
});
@@ -42,7 +42,7 @@ onReachBottom(() => {
});
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
function getJobList(type = 'add') {

View File

@@ -1,320 +1,353 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<template #headerright>
<view class="btn mar_ri10">
<image
src="@/static/icon/collect3.png"
v-if="!companyInfo.isCollection"
@click="companyCollection"
></image>
<image src="@/static/icon/collect2.png" v-else @click="companyCollection"></image>
</view>
</template>
<view class="content">
<view class="content-top">
<view class="companyinfo-left">
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
<view class="companyinfo-right">
<view class="row1">{{ companyInfo?.name }}</view>
<view class="row2">
<dict-tree-Label
v-if="companyInfo?.industry"
dictType="industry"
:value="companyInfo?.industry"
></dict-tree-Label>
<span v-if="companyInfo?.industry">&nbsp;</span>
<dict-Label dictType="scale" :value="companyInfo?.scale"></dict-Label>
</view>
</view>
</view>
<view class="conetent-info" :class="{ expanded: isExpanded }">
<view class="info-title">公司介绍</view>
<view class="info-desirption">{{ companyInfo.description }}</view>
<!-- <view class="info-title title2">公司地址</view>
<view class="locationCompany"></view> -->
</view>
<view class="expand" @click="expand">
<text>{{ isExpanded ? '收起' : '展开' }}</text>
<image
class="expand-img"
:class="{ 'expand-img-active': !isExpanded }"
src="@/static/icon/downs.png"
></image>
</view>
<scroll-view scroll-y class="Detailscroll-view" @scrolltolower="getJobsList('add')">
<view class="views">
<view class="Detail-title"><text class="title">在招职位</text></view>
<renderJobs
v-if="pageState.list.length"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
</view>
</scroll-view>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<template #headerright>
<view class="btn mar_ri10">
<image
src="@/static/icon/collect3.png"
v-if="!companyInfo.isCollection"
></image>
<image src="@/static/icon/collect2.png" v-else></image>
</view>
</template>
<view class="content">
<view class="content-top">
<view class="companyinfo-left">
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
</AppLayout>
<view class="companyinfo-right">
<view class="row1">{{ companyInfo?.companyName }}</view>
<view class="row2">
{{ companyInfo?.scale }}
</view>
</view>
</view>
<view class="conetent-info" :class="{ expanded: isExpanded }">
<view class="info-title">公司介绍</view>
<view class="info-desirption">{{
companyInfo.companyIntroduction
}}</view>
<!-- <view class="info-title title2">公司地址</view>
<view class="locationCompany"></view> -->
</view>
<view class="expand" @click="expand">
<text>{{ isExpanded ? "收起" : "展开" }}</text>
<image
class="expand-img"
:class="{ 'expand-img-active': !isExpanded }"
src="@/static/icon/downs.png"
></image>
</view>
<scroll-view scroll-y class="Detailscroll-view">
<view class="views">
<view class="Detail-title"><text class="title">在招职位</text></view>
<template v-if="companyInfo.jobInfoList.length != 0">
<view v-for="job in companyInfo.jobInfoList" :key="job.id">
<!-- @click="navTo(`/packageA/pages/post/post?jobId=${JSON.stringify(job)}`)" -->
<view class="cards" :style="getItemBackgroundStyle('bj2.png')">
<view class="card-company">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary"> {{ job.salaryRange }}/ </view>
</view>
<view class="card-tags">
<view class="tag jy">
<image :src="`${baseUrl}/jobfair/jy.png`" mode=""></image>
{{ job.experienceRequirement }}
</view>
<view class="tag xl">
<image :src="`${baseUrl}/jobfair/xx.png`" mode=""></image>
{{ job.educationRequirement }}
</view>
<view class="tag yd" v-if="job.jobRequirement">
<image :src="`${baseUrl}/jobfair/lx-1.png`" mode=""></image>
{{ job.jobRequirement }}
</view>
</view>
<view class="card-companyName">
<image :src="`${baseUrl}/jobfair/hd.png`" mode=""></image>
{{ job.jobDescription }}
</view>
</view>
</view>
</template>
<empty v-else pdTop="200"></empty>
</view>
</scroll-view>
</view>
</AppLayout>
</template>
<script setup>
import point from '@/static/icon/point.png';
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import { reactive, inject, watch, ref, onMounted, computed } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import dictLabel from "@/components/dict-Label/dict-Label.vue";
import config from "@/config.js";
import { storeToRefs } from "pinia";
import useLocationStore from "@/stores/useLocationStore";
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
const { $api, navTo, vacanciesTo, navBack } = inject("globalFunction");
const isExpanded = ref(false);
const pageState = reactive({
page: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 10,
page: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 10,
});
const companyInfo = ref({
jobInfoList: [],
});
const baseUrl = config.imgBaseUrl;
const getItemBackgroundStyle = (imageName) => ({
backgroundImage: `url(${baseUrl}/jobfair/${imageName})`,
backgroundSize: "100% 100%", // 覆盖整个容器
backgroundPosition: "center", // 居中
backgroundRepeat: "no-repeat",
});
const companyInfo = ref({});
onLoad((options) => {
console.log(options);
getCompanyInfo(options.companyId || options.bussinessId);
companyInfo.value = JSON.parse(options.job);
console.log(companyInfo.value, "companyInfo.value");
});
function companyCollection() {
const companyId = companyInfo.value.companyId;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId);
$api.msg('收藏成功');
});
}
}
function getCompanyInfo(id) {
$api.createRequest(`/app/company/${id}`).then((resData) => {
companyInfo.value = resData.data;
getJobsList();
});
}
function getJobsList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
};
$api.createRequest(`/app/company/job/${companyInfo.value.companyId}`, params).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
});
}
function expand() {
isExpanded.value = !isExpanded.value;
isExpanded.value = !isExpanded.value;
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
.btnback {
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
width: 100%;
}
.content{
height: 100%
display: flex;
flex-direction: column
.content-top{
padding: 28rpx
padding-top: 50rpx
display: flex
flex-direction: row
flex-wrap: nowrap
.companyinfo-left{
width: 96rpx;
height: 96rpx;
margin-right: 24rpx
}
.companyinfo-right{
.row1{
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.row2{
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
line-height: 45rpx;
}
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
width: 100%;
}
.content {
height: 100%;
display: flex;
flex-direction: column;
.content-top {
padding: 28rpx;
padding-top: 50rpx;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
.companyinfo-left {
width: 96rpx;
height: 96rpx;
margin-right: 24rpx;
}
.conetent-info{
padding: 0 28rpx
overflow: hidden;
max-height: 0rpx;
transition: max-height 0.3s ease;
.info-title{
font-weight: 600;
font-size: 28rpx;
color: #000000;
}
.info-desirption{
margin-top: 12rpx
font-weight: 400;
font-size: 28rpx;
color: #495265;
text-align: justified;
}
.title2{
margin-top: 48rpx
}
}
.expanded {
max-height: 1000rpx; // 足够显示完整内容
}
.expand{
display: flex
flex-wrap: nowrap
white-space: nowrap
justify-content: center
margin-top: 20rpx
margin-bottom: 28rpx
.companyinfo-right {
.row1 {
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.row2 {
font-weight: 400;
font-size: 28rpx;
color: #256BFA;
.expand-img{
width: 40rpx;
height: 40rpx;
}
.expand-img-active{
transform: rotate(180deg)
}
color: #6C7282;
line-height: 45rpx;
}
}
}
.Detailscroll-view{
flex: 1;
}
.conetent-info {
padding: 0 28rpx;
overflow: hidden;
background: #F4F4F4;
.views{
padding: 28rpx
.Detail-title{
font-weight: 600;
font-size: 32rpx;
color: #000000;
position: relative;
display: flex
justify-content: space-between
.title{
position: relative;
z-index: 2;
}
}
.Detail-title::before{
position: absolute
content: '';
left: -14rpx
bottom: 0
height: 16rpx;
width: 108rpx;
background: linear-gradient(to right, #CBDEFF, #FFFFFF);
border-radius: 8rpx;
z-index: 1;
}
.cards{
padding: 32rpx;
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
margin-top: 22rpx;
.card-company{
display: flex
justify-content: space-between
align-items: flex-start
.company{
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.salary{
font-weight: 500;
font-size: 28rpx;
color: #4C6EFB;
white-space: nowrap
line-height: 48rpx
}
}
.card-companyName{
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
}
.card-tags{
display: flex
flex-wrap: wrap
.tag{
width: fit-content;
height: 30rpx;
background: #F4F4F4;
border-radius: 4rpx;
padding: 6rpx 20rpx;
line-height: 30rpx;
font-weight: 400;
font-size: 24rpx;
color: #6C7282;
text-align: center;
margin-top: 14rpx;
white-space: nowrap
margin-right: 20rpx
}
}
.card-bottom{
margin-top: 32rpx
display: flex
justify-content: space-between
font-size: 28rpx;
color: #6C7282;
}
}
max-height: 0rpx;
transition: max-height 0.3s ease;
.info-title {
font-weight: 600;
font-size: 28rpx;
color: #000000;
}
.info-desirption {
margin-top: 12rpx;
font-weight: 400;
font-size: 28rpx;
color: #495265;
text-align: justified;
}
.title2 {
margin-top: 48rpx;
}
}
.expanded {
max-height: 1000rpx; // 足够显示完整内容
}
.expand {
display: flex;
flex-wrap: nowrap;
white-space: nowrap;
justify-content: center;
margin-top: 20rpx;
margin-bottom: 28rpx;
font-weight: 400;
font-size: 28rpx;
color: #256BFA;
.expand-img {
width: 40rpx;
height: 40rpx;
}
.expand-img-active {
transform: rotate(180deg);
}
}
}
.Detailscroll-view {
flex: 1;
overflow: hidden;
background: #F4F4F4;
.views {
padding: 28rpx;
.Detail-title {
font-weight: 600;
font-size: 32rpx;
color: #000000;
position: relative;
display: flex;
justify-content: space-between;
.title {
position: relative;
z-index: 2;
}
}
.Detail-title::before {
position: absolute;
content: '';
left: -14rpx;
bottom: 0;
height: 16rpx;
width: 108rpx;
background: linear-gradient(to right, #CBDEFF, #FFFFFF);
border-radius: 8rpx;
z-index: 1;
}
.cards {
padding: 32rpx;
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0, 0, 0, 0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
margin-top: 22rpx;
padding-bottom: 18rpx;
.card-company {
display: flex;
justify-content: space-between;
align-items: flex-start;
.company {
font-weight: 600;
font-size: 32rpx;
color: #333333;
}
.salary {
font-weight: 600;
font-size: 28rpx;
color: #F83A3C;
white-space: nowrap;
line-height: 48rpx;
}
}
.card-companyName {
font-weight: 400;
font-size: 28rpx;
color: #fff;
margin-top: 23rpx;
display: flex;
align-items: center;
image{
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
}
.card-tags {
display: flex;
flex-wrap: wrap;
margin: 25rpx 0 35rpx;
image{
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.jy {
background: #D9EDFF;
color: #0086FF;
}
.xl {
background: #FFF1D5;
color: #FF7F01;
}
.yd {
background: #FFD8D8;
color: #F83A3C;
}
.tag {
width: fit-content;
height: 30rpx;
border-radius: 4rpx;
padding: 6rpx 20rpx;
line-height: 30rpx;
font-weight: 400;
font-size: 24rpx;
text-align: center;
white-space: nowrap;
margin-right: 20rpx;
display: flex;
align-items: center;
}
}
.card-bottom {
margin-top: 32rpx;
display: flex;
justify-content: space-between;
font-size: 28rpx;
color: #6C7282;
}
}
}
}
</style>

View File

@@ -1,16 +1,5 @@
<template>
<AppLayout
title="添加工作经历"
border
back-gorund-color="#ffffff"
:show-bg-image="false"
>
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click" @click="handleConfirm">确认</view>
</template>
<view class="page-container">
<view class="content">
<view class="content-input">
<view class="input-titile">公司名称</view>
@@ -44,8 +33,12 @@
<textarea class="textarea-con" v-model="formData.description" placeholder-style="font-size: 16px" maxlength="500" placeholder="请输入工作描述"/>
</view>
</view>
</AppLayout>
<!-- 底部确认按钮 -->
<view class="bottom-confirm-btn">
<view class="confirm-btn" @click="handleConfirm">确认</view>
</view>
</view>
</template>
<script setup>
@@ -206,7 +199,6 @@
console.log('页面类型:', pageType.value);
let resData;
alert(editData.value.id)
// 根据页面类型调用不同的接口
if (pageType.value === 'edit' && editData.value?.id) {
// 编辑模式:调用更新接口
@@ -234,12 +226,18 @@
</script>
<style lang="stylus" scoped>
.page-container {
min-height: 100vh;
background-color: #ffffff;
position: relative;
}
.content{
padding: 28rpx;
display: flex;
flex-direction: column;
justify-content: flex-start
height: calc(100% - 120rpx)
justify-content: flex-start;
padding-bottom: 120rpx;
}
.content-input
margin-bottom: 52rpx
@@ -328,6 +326,30 @@
line-height: 20rpx
display: flex
align-items: center
// 底部确认按钮样式
.bottom-confirm-btn
position: fixed
bottom: 0
left: 0
right: 0
background-color: #ffffff
padding: 20rpx 28rpx
border-top: 2rpx solid #EBEBEB
z-index: 999
.confirm-btn
width: 100%
height: 90rpx
background: #256BFA
border-radius: 12rpx
font-weight: 500
font-size: 32rpx
color: #FFFFFF
text-align: center
line-height: 90rpx
button-click: true
// .content-sex
// height: 110rpx;
// display: flex

View File

@@ -89,7 +89,7 @@ function toSelectDate() {
}
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
function searchCollection(e) {

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
<input
class="input-con triangle"
disabled
v-if="!state.jobsText.length"
v-if="!state.jobsText || !state.jobsText.length"
placeholder="请选择您的求职岗位"
/>
<view class="input-nx" @click="changeJobs" v-else>
@@ -40,6 +40,7 @@
</view>
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<SelectPopup ref="selectPopupRef"></SelectPopup>
</AppLayout>
</template>
@@ -47,16 +48,24 @@
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import SelectPopup from '@/components/selectPopup/selectPopup.vue';
const { $api, navTo, navBack, config } = inject('globalFunction');
const openSelectPopup = inject('openSelectPopup');
// 创建本地的 openSelectPopup 函数
const openSelectPopup = (config) => {
if (selectPopupRef.value) {
selectPopupRef.value.open(config);
}
};
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
const { dictLabel, oneDictData, getDictData } = useDictStore();
const selectJobsModel = ref();
const selectPopupRef = ref();
const percent = ref('0%');
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
const state = reactive({
@@ -72,7 +81,9 @@ const fromValue = reactive({
area: '',
jobTitleId: [],
});
onLoad(() => {
onLoad(async () => {
// 初始化字典数据
await getDictData();
initLoad();
});
const confirm = () => {
@@ -99,8 +110,12 @@ function initLoad() {
fromValue.jobTitleId = userInfo.value.jobTitleId;
// 回显
state.areaText = dictLabel('area', Number(userInfo.value.area));
state.salayText = `${userInfo.value.salaryMin}-${userInfo.value.salaryMax}`;
state.jobsText = userInfo.value.jobTitle;
if (userInfo.value.salaryMin && userInfo.value.salaryMax) {
state.salayText = `${userInfo.value.salaryMin}-${userInfo.value.salaryMax}`;
} else {
state.salayText = '';
}
state.jobsText = userInfo.value.jobTitle || [];
const result = getFormCompletionPercent(fromValue);
percent.value = result;
}
@@ -123,7 +138,8 @@ const changeSalary = () => {
const copyri = JSON.parse(JSON.stringify(salay));
const [lf, ri] = e.detail.value;
const risalay = copyri.slice(lf, copyri.length);
this.setColunm(1, risalay);
// 更新右侧选项
state.risalay = risalay;
leftIndex = salayData[0];
}
},

View File

@@ -122,28 +122,23 @@
</view>
<!-- 4. 新增简历上传区域固定在页面底部 -->
<view class="resume-upload-section">
<!-- 上传按钮 -->
<!-- <view class="resume-upload-section">
<button class="upload-btn" @click="handleResumeUpload" :loading="isUploading" :disabled="isUploading">
<uni-icons type="cloud-upload" size="20"></uni-icons>
<!-- <image class="upload-icon" src="/static/icons/upload-file.png" mode="widthFix"></image> -->
<text class="upload-text">
{{ uploadedResumeName || '上传简历' }}
</text>
<!-- 已上传时显示重新上传文字 -->
<text class="reupload-text" v-if="uploadedResumeName">重新上传</text>
</button>
<!-- 上传说明 -->
<text class="upload-tip">支持 PDFWord 格式文件大小不超过 20MB</text>
<!-- 已上传文件信息可选 -->
<view class="uploaded-file-info" v-if="uploadedResumeName">
<image class="file-icon" src="/static/icons/file-icon.png" mode="widthFix"></image>
<text class="file-name">{{ uploadedResumeName }}</text>
<button class="delete-file-btn" size="mini" @click.stop="handleDeleteResume">删除</button>
</view>
</view>
</view> -->
</view>
</template>

View File

@@ -16,27 +16,29 @@
</template>
<view class="content" v-show="!isEmptyObject(jobInfo)">
<view class="content-top btn-feel">
<view class="top-salary">
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="top-name">{{ jobInfo.jobTitle }}</view>
<view class="top-info">
<view class="info-img"><image src="/static/icon/post12.png"></image></view>
<view class="info-text">
<dict-Label dictType="experience" :value="jobInfo.experience"></dict-Label>
<view style="background: #ffffff;padding: 24rpx;box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);border-radius: 20rpx 20rpx 20rpx 20rpx;position: relative;overflow: hidden;">
<view class="top-salary">
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="info-img mar_le20"><image src="/static/icon/post13.png"></image></view>
<view class="info-text">
<dict-Label dictType="education" :value="jobInfo.education"></dict-Label>
<view class="top-name">{{ jobInfo.jobTitle }}</view>
<view class="top-info">
<view class="info-img"><image src="/static/icon/post12.png"></image></view>
<view class="info-text">
<dict-Label dictType="experience" :value="jobInfo.experience"></dict-Label>
</view>
<view class="info-img mar_le20"><image src="/static/icon/post13.png"></image></view>
<view class="info-text">
<dict-Label dictType="education" :value="jobInfo.education"></dict-Label>
</view>
</view>
<view class="position-source">
<text>来源&nbsp;</text>
{{ jobInfo.dataSource }}
</view>
</view>
<view class="position-source">
<text>来源&nbsp;</text>
{{ jobInfo.dataSource }}
</view>
</view>
<view class="ai-explain" v-if="jobInfo.isExplain">
@@ -98,7 +100,7 @@
></map>
</view>
</view>
<view class="content-card" v-if="!userInfo.isCompanyUser">
<view class="content-card" v-if="currentUserType !== 0">
<view class="card-title">
<text class="title">竞争力分析</text>
</view>
@@ -191,6 +193,12 @@ import RadarMap from './component/radarMap.vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
// 与首页一致的用户类型获取优先store兜底缓存
const currentUserType = computed(() => {
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = (uni.getStorageSync('userInfo') || {}).isCompanyUser;
return Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : cachedIsCompanyUser);
});
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';
const matchingDegree = ref(['一般', '良好', '优秀', '极好']);
@@ -245,7 +253,7 @@ onShow(() => {
});
function initLoad(option) {
const jobId = atob(option.jobId);
const jobId = decodeURIComponent(option.jobId);
if (jobId !== jobIdRef.value) {
jobIdRef.value = jobId;
getDetail(jobId);
@@ -263,11 +271,11 @@ function seeExplain() {
function getDetail(jobId) {
return new Promise((reslove, reject) => {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId, isCompanyUser } = resData.data;
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(companyId);
if (isCompanyUser) {
if (currentUserType.value !== 0) {
getCompetivetuveness(jobId);
}
// getCompetivetuveness(jobId);
@@ -503,11 +511,11 @@ for i in 0..100
.content{
padding: 0 28rpx
height: 100%
padding-top: 28rpx
.content-top{
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
padding: 52rpx 32rpx 34rpx 32rpx
padding: 24rpx
position: relative
overflow: hidden
.top-salary{

View File

@@ -53,7 +53,7 @@ function nextDetail() {
recommedIndexDb.addRecord(recordData);
}
console.log(job.jobId);
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
function getNextVideoSrc(num) {

559
packageB/jobFair/detail.vue Normal file
View File

@@ -0,0 +1,559 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view class="content">
<view class="content-top">
<view class="companyinfo-left">
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
<view class="companyinfo-right">
<view class="row1 line_2">{{ fairInfo?.name }}</view>
<view class="row2">
<text>{{ fairInfo.location }}</text>
<convert-distance
:alat="fairInfo.latitude"
:along="fairInfo.longitude"
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance>
</view>
</view>
</view>
<view class="locations">
<image class="location-img" src="/static/icon/mapLine.png"></image>
<view class="location-info">
<view class="info">
<text class="info-title">{{ fairInfo.address }}</text>
<text class="info-text">位置</text>
</view>
</view>
</view>
<view class="conetent-info" :class="{ expanded: isExpanded }">
<view class="info-title">内容描述</view>
<view class="info-desirption">{{ fairInfo.description }}</view>
<!-- <view class="info-title title2">公司地址</view>
<view class="locationCompany"></view> -->
<view class="company-times">
<view class="info-title">内容描述</view>
<view class="card-times">
<view class="time-left">
<view class="left-date">{{ parseDateTime(fairInfo.startTime).time }}</view>
<view class="left-dateDay">{{ parseDateTime(fairInfo.startTime).date }}</view>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(fairInfo.startTime, fairInfo.endTime).statusText }}
</view>
<view class="center-dateDay">
{{ getHoursBetween(fairInfo.startTime, fairInfo.endTime) }}小时
</view>
</view>
<view class="line"></view>
<view class="time-right">
<view class="left-date">{{ parseDateTime(fairInfo.endTime).time }}</view>
<view class="left-dateDay">{{ parseDateTime(fairInfo.endTime).date }}</view>
</view>
</view>
</view>
</view>
<view class="expand" @click="expand">
<text>{{ isExpanded ? '收起' : '展开' }}</text>
<image
class="expand-img"
:class="{ 'expand-img-active': !isExpanded }"
src="@/static/icon/downs.png"
></image>
</view>
<scroll-view scroll-y class="Detailscroll-view">
<view class="views">
<view class="Detail-title">
<text class="title">参会单位{{ companyList.length }}</text>
</view>
<renderCompanys
v-if="companyList.length"
:list="companyList"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanys>
<empty v-else pdTop="200"></empty>
</view>
</scroll-view>
</view>
<template #footer>
<view class="footer" v-if="hasnext">
<view
class="btn-wq button-click"
:class="{ 'btn-desbel': fairInfo.isCollection }"
@click="applyExhibitors"
>
{{ fairInfo.isCollection ? '已预约招聘会' : '预约招聘会' }}
</view>
</view>
</template>
</AppLayout>
</template>
<script setup>
import point from '@/static/icon/point.png';
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import useLocationStore from '@/stores/useLocationStore';
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const isExpanded = ref(false);
const fairInfo = ref({});
const companyList = ref([]);
const hasnext = ref(true);
onLoad((options) => {
getCompanyInfo(options.jobFairId);
});
function getCompanyInfo(id) {
$api.createRequest(`/app/fair/${id}`).then((resData) => {
fairInfo.value = resData.data;
companyList.value = resData.data.companyList;
hasAppointment();
});
}
const hasAppointment = () => {
const isTimePassed = (timeStr) => {
const targetTime = new Date(timeStr.replace(/-/g, '/')).getTime(); // 兼容格式
const now = Date.now();
return now < targetTime;
};
hasnext.value = isTimePassed(fairInfo.value.startTime);
};
function openMap(lat, lng, name = '位置') {
const isConfirmed = window.confirm('是否打开地图查看位置?');
if (!isConfirmed) return;
// 使用高德地图或百度地图的 H5 链接打开
const url = `https://uri.amap.com/marker?position=${lng},${lat}&name=${encodeURIComponent(name)}`;
window.location.href = url;
}
function expand() {
isExpanded.value = !isExpanded.value;
}
// 取消/收藏岗位
function applyExhibitors() {
const fairId = fairInfo.value.jobFairId;
if (fairInfo.value.isCollection) {
// $api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
// getCompanyInfo(fairId);
// $api.msg('取消预约成功');
// });
$api.msg('已预约成功');
} else {
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'POST').then((resData) => {
getCompanyInfo(fairId);
$api.msg('预约成功');
});
}
}
function toIOSDate(input) {
if (!input) return null;
if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
if (typeof input === 'number') {
const d = new Date(input);
return isNaN(d.getTime()) ? null : d;
}
if (typeof input !== 'string') return null;
let s = input.trim();
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(:\d{2})?$/.test(s)) {
s = s.replace(' ', 'T');
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s)) {
s = s + ':00';
}
}
const d = new Date(s);
return isNaN(d.getTime()) ? null : d;
}
function parseDateTime(datetimeStr) {
if (!datetimeStr) return { time: '', date: '' };
const dateObj = toIOSDate(datetimeStr);
if (isNaN(dateObj.getTime())) return { time: '', date: '' }; // 无效时间
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const day = String(dateObj.getDate()).padStart(2, '0');
const hours = String(dateObj.getHours()).padStart(2, '0');
const minutes = String(dateObj.getMinutes()).padStart(2, '0');
return {
time: `${hours}:${minutes}`,
date: `${year}${month}${day}`,
};
}
function getTimeStatus(startTimeStr, endTimeStr) {
const now = new Date();
const startTime = toIOSDate(startTimeStr);
const endTime = toIOSDate(endTimeStr);
if (!startTime || !endTime) {
return { status: 1, statusText: '时间异常' };
}
let status = 0;
let statusText = '开始中';
if (now < startTime) {
status = 2;
statusText = '待开始';
} else if (now > endTime) {
status = 1;
statusText = '已过期';
} else {
status = 0;
statusText = '进行中';
}
return { status, statusText };
}
function getHoursBetween(startTimeStr, endTimeStr) {
const start = toIOSDate(startTimeStr);
const end = toIOSDate(endTimeStr);
if (!start || !end) return 0;
const diffMs = end - start;
const diffHours = diffMs / (1000 * 60 * 60);
return +diffHours.toFixed(2);
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
width: 100%;
}
.content{
height: 100%
display: flex;
flex-direction: column
.content-top{
padding: 28rpx
padding-top: 50rpx
display: flex
flex-direction: row
flex-wrap: nowrap
.companyinfo-left{
width: 96rpx;
height: 96rpx;
margin-right: 24rpx
}
.companyinfo-right{
flex: 1
.row1{
font-weight: 500;
font-size: 32rpx;
color: #333333;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
.row2{
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
line-height: 45rpx;
display: flex
justify-content: space-between
}
}
}
.locations{
padding: 0 28rpx
height: 86rpx;
position: relative
margin-bottom: 36rpx
.location-img{
border-radius: 8rpx 8rpx 8rpx 8rpx;
border: 2rpx solid #EFEFEF;
}
.location-info{
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.info{
padding: 0 60rpx
height: 100%
display: flex
align-items: center
justify-content: space-between
white-space: nowrap
padding-top: rpx
.info-title{
font-weight: 600;
font-size: 28rpx;
color: #333333;
}
.info-text{
font-weight: 400;
font-size: 28rpx;
color: #9B9B9B;
position: relative;
padding-right: 20rpx
}
.info-text::before{
position: absolute;
right: 0;
top: 50%;
content: '';
width: 4rpx;
height: 16rpx;
border-radius: 2rpx
background: #9B9B9B;
transform: translate(0, -75%) rotate(-45deg)
}
.info-text::after {
position: absolute;
right: 0;
top: 50%;
content: '';
width: 4rpx;
height: 16rpx;
border-radius: 2rpx
background: #9B9B9B;
transform: translate(0, -25%) rotate(45deg)
}
}
}
}
.conetent-info{
padding: 0 28rpx
overflow: hidden;
max-height: 0rpx;
transition: max-height 0.3s ease;
.info-title{
font-weight: 600;
font-size: 28rpx;
color: #000000;
}
.info-desirption{
margin-top: 12rpx
font-weight: 400;
font-size: 28rpx;
color: #495265;
text-align: justified;
}
.title2{
margin-top: 48rpx
}
}
.company-times{
padding-top: 40rpx
.info-title{
font-weight: 600;
font-size: 28rpx;
color: #000000;
}
}
.expanded {
max-height: 1000rpx; // 足够显示完整内容
}
.expand{
display: flex
flex-wrap: nowrap
white-space: nowrap
justify-content: center
margin-bottom: 46rpx
font-weight: 400;
font-size: 28rpx;
color: #256BFA;
.expand-img{
width: 40rpx;
height: 40rpx;
}
.expand-img-active{
transform: rotate(180deg)
}
}
}
.Detailscroll-view{
flex: 1;
overflow: hidden;
background: #F4F4F4;
.views{
padding: 28rpx
.Detail-title{
font-weight: 600;
font-size: 32rpx;
color: #000000;
position: relative;
display: flex
justify-content: space-between
.title{
position: relative;
z-index: 2;
}
}
.Detail-title::before{
position: absolute
content: '';
left: -14rpx
bottom: 0
height: 16rpx;
width: 108rpx;
background: linear-gradient(to right, #CBDEFF, #FFFFFF);
border-radius: 8rpx;
z-index: 1;
}
.cards{
padding: 32rpx;
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
margin-top: 22rpx;
.card-company{
display: flex
justify-content: space-between
align-items: flex-start
.company{
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.salary{
font-weight: 500;
font-size: 28rpx;
color: #4C6EFB;
white-space: nowrap
line-height: 48rpx
}
}
.card-companyName{
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
}
.card-tags{
display: flex
flex-wrap: wrap
.tag{
width: fit-content;
height: 30rpx;
background: #F4F4F4;
border-radius: 4rpx;
padding: 6rpx 20rpx;
line-height: 30rpx;
font-weight: 400;
font-size: 24rpx;
color: #6C7282;
text-align: center;
margin-top: 14rpx;
white-space: nowrap
margin-right: 20rpx
}
}
.card-bottom{
margin-top: 32rpx
display: flex
justify-content: space-between
font-size: 28rpx;
color: #6C7282;
}
}
}
}
.card-times{
display: flex;
justify-content: space-between
align-items: center
padding: 24rpx 30rpx 10rpx 30rpx
.time-left,
.time-right{
text-align: center
.left-date{
font-weight: 500;
font-size: 48rpx;
color: #333333;
}
.left-dateDay{
font-weight: 400;
font-size: 24rpx;
color: #333333;
margin-top: 12rpx
}
}
.line{
width: 40rpx;
height: 0rpx;
border: 2rpx solid #D4D4D4;
margin-top: 64rpx
}
.time-center{
text-align: center;
display: flex
flex-direction: column
justify-content: center
align-items: center
.center-date{
font-weight: 400;
font-size: 28rpx;
color: #FF881A;
padding-top: 10rpx
}
.center-dateDay{
font-weight: 400;
font-size: 24rpx;
color: #333333;
margin-top: 6rpx
line-height: 48rpx;
width: 104rpx;
height: 48rpx;
background: #F9F9F9;
border-radius: 8rpx 8rpx 8rpx 8rpx;
}
}
}
.footer{
background: #FFFFFF;
box-shadow: 0rpx -4rpx 24rpx 0rpx rgba(11,44,112,0.12);
border-radius: 0rpx 0rpx 0rpx 0rpx;
padding: 40rpx 28rpx 20rpx 28rpx
.btn-wq{
height: 90rpx;
background: #256BFA;
border-radius: 12rpx 12rpx 12rpx 12rpx;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
text-align: center;
line-height: 90rpx
}
.btn-desbel{
background: #6697FB;
box-shadow: 0rpx -4rpx 24rpx 0rpx rgba(11,44,112,0.12);
}
}
</style>

237
packageB/login.vue Normal file
View File

@@ -0,0 +1,237 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<view class="wrap">
<view class="login_index">
<input class="input" placeholder="请输入账号" placeholder-class="inputplace" v-model="form.username" />
<view class="login_yzm">
<input class="input" type="password" placeholder="请输入密码" placeholder-class="inputplace"
v-model="form.password" />
</view>
<view class="login_yzm">
<input class="input" placeholder="请输入验证码" placeholder-class="inputplace" v-model="form.code" />
<image class="yzm" :src="codeUrl" @click="getCodeImg"></image>
</view>
<button class="com-btn" @click="register"> </button>
</view>
</view>
</AppLayout>
</template>
<script setup>
import {
reactive,
inject,
watch,
ref,
onMounted,
onUnmounted
} from 'vue'
import {
onLoad,
onShow
} from '@dcloudio/uni-app';
const {
$api,
navTo,
vacanciesTo,
navBack
} = inject("globalFunction");
const placeholderStyle = 'font-size:30rpx'
const checked = ref(true)
const codeUrl = ref('')
const form = reactive({
username: 'langchaojituan',
password: 'Aa123456?',
rememberMe: false,
code: '',
uuid: ''
})
onLoad(() => {
})
onMounted(() => {
getCodeImg()
})
function register() {
if (!form.username) {
uni.showToast({
icon: 'none',
title: '请输入用户名'
})
return
}
if (!form.password) {
uni.showToast({
icon: 'none',
title: '请输入密码'
})
return
}
if (!form.uuid) {
uni.showToast({
icon: 'none',
title: '请输入验证码'
})
return
}
uni.showLoading({
title: '登录中...',
mask: true
})
$api.myRequest('/auth/login',form,'post',10100).then((res) => {
console.log(res, 'res')
uni.setStorageSync('Padmin-Token', res.data.access_token)
uni.reLaunch({
url: '/pages/index/index'
})
codeUrl.value = 'data:image/gif;base64,' + res.img
}).catch(() => {
uni.hideLoading()
uni.showToast({
icon: 'none',
title: '登录失败,请重试'
})
})
}
function getCodeImg() {
$api.myRequest('/code',{},'get',10100).then((resData) => {
codeUrl.value = 'data:image/gif;base64,' + resData.img
form.uuid = resData.uuid
});
}
</script>
<style scoped lang="stylus">
.wrap {
background-color: #ffffff;
height: 100vh;
position: relative;
.lg-head {
height: 480rpx;
background: #46ca98;
position: relative;
.view_logo {
text-align: center;
.login_logo {
width: 300rpx;
height: 300rpx;
margin-top: 100rpx;
}
}
.bg-cover {
position: absolute;
bottom: -4rpx;
left: 0;
right: 0;
height: 30rpx;
background-size: 100% 100%;
z-index: 1;
}
}
}
.login_index {
font-size: 36rpx;
font-weight: 500;
width: 596rpx;
margin: 0 auto;
::v-deep .is-input-border {
border: 0;
border-bottom: 1px solid #dcdfe6 !important;
border-radius: 0;
}
::v-deep .uni-input-input {
font-size: 32rpx;
padding-left: 10rpx;
}
::v-deep .uniui-contact-filled:before {
color: #46ca98;
font-size: 50rpx;
}
::v-deep .uniui-locked-filled:before {
color: #46ca98;
font-size: 50rpx;
}
.login_yzm {
margin-top: 40rpx;
display: flex;
align-items: center;
.yzm {
width: 200rpx;
height: 80rpx;
}
}
.com-btn {
height: 100rpx;
background: #46ca98;
border-radius: 50rpx;
color: #fff;
margin-top: 100rpx;
}
.login_wt {
margin: 0 auto;
text-align: right;
font-size: 24rpx;
color: rgba(134, 134, 136, 1);
}
}
.lg-bottom {
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
.bottom-svg {
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
}
}
.login_tongyi {
font-size: 26rpx;
color: rgba(196, 196, 196, 1);
width: 620rpx;
margin: 32rpx auto;
text-align: center;
text {
color: rgba(86, 176, 236, 1);
}
}
.input {
padding: 0 30rpx 0 80rpx;
height: 80rpx;
background: #FFFFFF;
border-radius: 75rpx 75rpx 75rpx 75rpx;
font-size: 28rpx;
}
.inputplace {
font-weight: 400;
font-size: 28rpx;
color: #B5B5B5;
}
</style>

138
packageB/train/index.vue Normal file
View File

@@ -0,0 +1,138 @@
<template>
<!-- <AppLayout title=""> -->
<view class="tab-container">
<image src="../../static/images/train/bj.jpg" mode=""></image>
<view>
<view class="btns" @click="jumps('/packageB/train/video/videoList')">
<image src="/static/images/train/spxx-k.png" mode=""></image>
<view>
<text>培训视频</text>
<view class="btn">
<text>立即查看</text>
<image src="/static/images/train/arrow.png" mode=""></image>
</view>
</view>
</view>
<view class="btns" @click="jumps('/packageB/train/practice/startPracticing')">
<image src="/static/images/train/zxxl-k.png" mode=""></image>
<view>
<text>专项练习</text>
<view class="btn">
<text>立即查看</text>
<image src="/static/images/train/arrow.png" mode=""></image>
</view>
</view>
</view>
<view class="btns" @click="jumps('/packageB/train/mockExam/examList')">
<image src="/static/images/train/mnks-k.png" mode=""></image>
<view>
<text>模拟考试</text>
<view class="btn">
<text>立即查看</text>
<image src="/static/images/train/arrow.png" mode=""></image>
</view>
</view>
</view>
<view class="btns" @click="jumps('')">
<image src="/static/images/train/ctb-k.png" mode=""></image>
<view>
<text>错题本 </text>
<view class="btn" style="margin-left: 13%;">
<text>立即查看</text>
<image src="/static/images/train/arrow.png" mode=""></image>
</view>
</view>
</view>
</view>
</view>
<!-- </AppLayout> -->
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, vacanciesTo, formatTotal, config } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
function jumps(url){
navTo(url);
}
</script>
<style lang="stylus" scoped>
.tab-container{
height: 100vh;
width: 100%;
position: relative;
// background: url('@/static/images/train/bj.jpg') center center no-repeat;
// background-size: 100% 100%;
image{
position: absolute;
width: 100%;
height: 100%;
}
view{
padding: 100rpx 28rpx 0;
.btns{
width: 100%
height: 170rpx;
border-radius: 5rpx
padding: 20rpx;
box-sizing: border-box
color: #000
text-align: center
margin-bottom: 50rpx
position: relative
image{
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
view{
padding: 0 0 0 10%;
box-sizing: border-box;
position: absolute
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 100;
display: flex;
align-items: center
font-weight: bold;
font-size: 36rpx;
color: #595959;
.btn{
margin-left: 8%;
background-color: #3A92FF;
position: static;
color: #fff;
font-size: 22rpx;
display: flex;
align-items: center;
justify-content: center
width: 140rpx;
height: 50rpx;
border-radius: 5rpx
font-weight: 500;
padding: 0;
image{
position: static;
width: 14rpx;
height: 22rpx;
margin-left: 10rpx;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<div class="app-box">
<div class="con-box">
<template #headContent>
<view class="collection-search">
<view class="search-content">
<view class="header-input button-click">
<uni-icons class="iconsearch" color="#6A6A6A" type="search" size="22"></uni-icons>
<input
class="input"
v-model="searchKeyword"
@confirm="searchVideo"
placeholder="输入考试名称"
placeholder-class="inputplace"
/>
<uni-icons
v-if="searchKeyword"
class="clear-icon"
type="clear"
size="24"
color="#999"
@click="clearSearch"
/>
</view>
</view>
</view>
</template>
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<div class="cards">
<div class="cardHead">
<div class="cardHeadLeft">
<div class="cardTitle">2025年注册会计师证</div>
<div class="titleType primary">未开始</div>
</div>
<div class="rightBtn">立即练习</div>
</div>
<div class="heng"></div>
<div class="cardCon">
<div class="conten">考试时长120分钟</div>
<div class="conten">题目数量88</div>
<div class="conten">及格分数60</div>
<div class="conten">截止日期2025-12-31</div>
</div>
<div class="flooter">
<div @click="jumps('/packageB/train/mockExam/viewGrades')">查看成绩</div>
<div>详情</div>
<div>收藏</div>
</div>
</div>
<div class="cards"></div>
<div class="cards"></div>
</scroll-view>
</div>
<div class="cards2" v-if="dialogVisible">
<div class="cardCon">
<div class="cardHead">
<div></div>
<div style="font-size: 40rpx;" @click="clones()">×</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { $api, navTo, navBack } = inject('globalFunction');
import config from "@/config.js"
const searchKeyword = ref('');
const pageState = reactive({
page: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 12,
search: {},
});
const baseUrl = config.imgBaseUrl
const dialogVisible = ref(false);
const handleScrollToLower = () => {
};
function jumps(url){
navTo(url);
}
function clones(){
dialogVisible.value=false
}
</script>
<style lang="stylus" scoped>
.app-box{
width: 100%;
height: 100vh;
position: relative;
.con-box{
position: absolute;
width: 100%;
height: 100%;
left: 0;
top:0;
z-index: 10;
padding: 20rpx 28rpx;
box-sizing: border-box;
overflow: hidden;
.collection-search{
padding: 10rpx 20rpx;
.search-content{
position: relative
display: flex
align-items: center
padding: 14rpx 0
.header-input{
padding: 0
width: calc(100%);
position: relative
.iconsearch{
position: absolute
left: 30rpx;
top: 50%
transform: translate(0, -50%)
z-index: 1
}
.input{
padding: 0 80rpx 0 80rpx
height: 80rpx;
background: #FFFFFF;
border-radius: 75rpx 75rpx 75rpx 75rpx;
border: 2rpx solid #ECECEC
font-size: 28rpx;
}
.clear-icon{
position: absolute
right: 30rpx;
top: 50%
transform: translate(0, -50%)
z-index: 1
cursor: pointer
}
.inputplace{
font-weight: 400;
font-size: 28rpx;
color: #B5B5B5;
}
}
}
}
.main-scroll {
width: 100%;
height: 100%;
.cards{
width: 100%;
height: 260rpx;
background: linear-gradient(0deg, #E3EFFF 0%, #FBFDFF 100%);
// box-shadow: 0px 0px 6px 0px rgba(0,71,200,0.32);
border-radius: 12rpx;
border: 2px solid #EDF5FF;
margin-bottom: 30rpx;
padding: 30rpx 40rpx 0;
box-sizing: border-box
.cardHead{
display: flex;
align-items: center;
justify-content: space-between;
.cardHeadLeft{
display: flex;
align-items: center
width: 75%;
.cardTitle{
font-weight: bold;
font-size: 28rpx;
color: #0069CB;
max-width: 75%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.titleType{
border-radius: 4px;
font-size: 22rpx;
color: #157EFF;
width: 100rpx;
height: 38rpx;
text-align: center;
line-height: 38rpx;
margin-left: 10rpx;
}
.primary{
border: 1px solid #157EFF;
color: #157EFF
}
.success{
border: 1px solid #05A636;
color: #05A636
}
.info{
border: 1px solid #898989;
color: #898989
}
}
.rightBtn{
width: 140rpx;
height: 44rpx;
line-height: 44rpx;
background: linear-gradient(90deg, #00C0FA 0%, #1271FF 100%);
border-radius: 4px;
color: #fff;
font-size: 24rpx;
text-align: center
}
}
.heng{
width: 120rpx;
height: 4rpx;
background: linear-gradient(88deg, #015EEA 0%, #00C0FA 100%);
margin: 10rpx 0 30rpx;
}
.cardCon{
display: flex;
flex-wrap: wrap;
.conten{
width: 50%;
font-size: 22rpx;
color: #666666;
display: flex;
align-items: center
margin-bottom: 20rpx;
}
}
.flooter{
border-top: 1px solid #ccc;
display: flex;
justify-content: flex-end;
align-items: center;
view{
font-size: 28rpx;
margin-left: 30rpx;
color: #2175F3;
}
}
}
}
}
.cards2{
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
background-color: rgba(0,0,0,0.5);
z-index: 10000;
padding: 100rpx 50rpx;
box-sizing: border-box;
.cardCon{
height: 100%;
background-color: #fff;
padding: 20rpx;
box-sizing: border-box;
.cardHead{
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
font-weight: 600;
}
}
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="app-box">
<div class="con-box">
<div class="tabCon">
<div class="tabLeft">
<div><span>考试名称</span>456546456</div>
<div><span>考试时间</span>456546456</div>
<div><span>考试成绩</span>456546456</div>
</div>
<div class="tabRight">查看</div>
<div class="tabLeft"></div>
<div class="tabRight">查看</div>
</div>
</div>
</div>
</template>
<script>
</script>
<style lang="stylus" scoped>
.app-box{
width: 100%;
height: 100vh;
position: relative;
.con-box{
position: absolute;
width: 100%;
height: 100%;
left: 0;
top:0;
z-index: 10;
padding: 20rpx 28rpx;
box-sizing: border-box;
overflow-y: auto;
.tabCon{
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
display: flex;
flex-wrap: wrap;
.tabLeft{
width: 80%;
height: 140rpx;
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
box-sizing: border-box;
view{
line-height: 45rpx
}
}
.tabRight{
width: 20%;
height: 140rpx;
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
box-sizing: border-box;
font-size: 30rpx;
text-align: center;
line-height: 140rpx;
color: #2175F3;
}
}
}
}
</style>

View File

@@ -0,0 +1,434 @@
<template>
<div class="app-box">
<image src="../../../static/images/train/bj.jpg" class="bjImg" mode=""></image>
<div class="con-box">
<div class="header">
<div>正确率0%</div>
<div>用时00:00</div>
<div class="headBtn">暂停</div>
</div>
<div class="problemCard">
<div v-for="(item,index) in problemData" :key="index">
<template v-if="questionIndex==(index+1)">
<div class="problemTitle">
<span class="titleType" v-if="item.type=='single'">单选题</span>
<span class="titleType" v-if="item.type=='multiple'">多选题</span>
<span class="titleType" v-if="item.type=='judge'">判断题</span>
<span>{{item.content}}</span>
</div>
<div class="options" v-if="item.type=='single'">
<div class="opt" @click="selected(i)" :class="radio!==''&&i==radio?'active':''" v-for="(val,i) in parseOptions(item.trainChooses)">
<div class="optLab">{{indexToLetter(i)}}</div>
<span>{{val}}</span>
</div>
</div>
<div class="options" v-if="item.type=='multiple'">
<div class="opt" @click="selected2(i)" :class="judgment(i)?'active':''" v-for="(val,i) in parseOptions(item.trainChooses)">
<div class="optLab">{{indexToLetter(i)}}</div>
<span>{{val}}</span>
</div>
</div>
<div class="options" v-if="item.type=='judge'">
<div class="opt" @click="selected3('正确')" :class="radio2=='正确'?'active':''">
<span>正确</span>
</div>
<div class="opt" @click="selected3('错误')" :class="radio2=='错误'?'active':''">
<span>错误</span>
</div>
</div>
<div class="analysis">
<div class="analysisHead correct">
<div>回答正确</div>
<div></div>
</div>
<!-- <div class="analysisHead errors" v-if="judgWhether=='错误'">
<div>回答错误</div>
<div></div>
</div> -->
<div class="analysisCon">
<div class="parse1">正确答案</div>
<div class="parse2" v-if="item.type=='single'" style="color: #30A0FF;font-weight: bold;">{{String.fromCharCode(65 + Number(item.answer))}}.</div>
<div class="parse2" v-if="item.type=='multiple'" style="color: #30A0FF;font-weight: bold;">
<span v-for="(val,i) in parseOptions(item.answer)">{{indexToLetter(val-1)}}.</span>
</div>
<div class="parse2" v-if="item.type=='judge'" style="color: #30A0FF;font-weight: bold;">{{item.answer}}</div>
</div>
<div class="analysisCon">
<div class="parse1">答案解析</div>
<div class="parse2">{{item.answerDesc}}</div>
</div>
<div class="analysisCon">
<div class="parse1">知识点</div>
<div>
<el-tag style="margin-right: 10px;">{{item.knowledgePoint}}</el-tag>
</div>
</div>
</div>
<div class="problemBtns">
<div class="events">提交答案</div>
<div>下一题</div>
</div>
</template>
</div>
</div>
<div class="footer">
<div class="footerLeft">
<div class="zuo events"></div>
<div class="you"></div>
<div style="text-align: center;font-size: 24rpx;">
<image :src="urls+'wsc.png'" mode=""></image>
<div>收藏</div>
</div>
<!-- <div style="text-align: center;font-size: 24rpx;">
<image :src="urls+'video-sc.png'" mode=""></image>
<div>取消</div>
</div> -->
</div>
<div class="footerBtn">完成练习</div>
<div class="footerLeft">
<div>
<div class="icons" style="background-color: #1CADF5;"></div>
<div class="texts" style="color: #1CADF5;">1</div>
</div>
<div>
<div class="icons" style="background-color: #FF6668;">×</div>
<div class="texts" style="color: #FF6668;">0</div>
</div>
<div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>
<div class="cards" v-if="dialogVisible">
<div class="cardCon">
<div class="cardHead">
<div>题号</div>
<div style="font-size: 40rpx;" @click="clones()">×</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api,urls , navTo, vacanciesTo, formatTotal, config } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const radio = ref('');
const radio2 = ref('');
const checkList = ref([]);
const questionIndex = ref(1);
const correctIndex = ref(0);
const errorsIndex = ref(0);
const accuracyRate = ref(0);
const dialogVisible = ref(true);
const problemData = reactive([
{
type:'single',
content:"君不见黄河之水天上来,下一句是?",
fraction:5,
trainChooses:"奔流到海不复回,朝如青丝暮成雪,人生得意须尽欢,莫使金樽空对月",
},{
type:'multiple',
content:"以下哪些是欧姆定律的适用条件?",
fraction:8,
trainChooses:"线性电阻,恒定温度,金属导体,半导体材料",
},{
type:'judge',
content:"功率越大的电器,其电阻值越小",
fraction:2,
trainChooses:[
"正确",
"错误",
]
}
]);
function selected(i){
radio.value=i
};
//多选
function selected2(i){
let arr=checkList.value.join(",")
if(arr.indexOf(i) !== -1){
const index = checkList.value.indexOf(i);
if (index !== -1) {
checkList.value.splice(index, 1);
}
}else{
checkList.value.push(i)
}
};
function judgment(i){
let arr=checkList.value.join(",")
if(arr.indexOf(i) !== -1){
return true
}else{
return false
}
};
function selected3(i){
radio2.value=i
};
// 解析选项
function parseOptions(options) {
if (!options) return [];
// 假设options是字符串格式以分号分隔
if (typeof options === 'string') {
return options.split(',').filter(opt => opt.trim());
}
// 如果是数组,直接返回
if (Array.isArray(options)) {
return options;
}
return [];
};
function indexToLetter(index) {
// 将索引转换为对应的字母
return String.fromCharCode(65 + index);
};
function clones(){
dialogVisible.value=false
}
</script>
<style lang="stylus" scoped>
.app-box{
width: 100%;
height: 100vh;
position: relative;
.bjImg{
position: absolute;
width: 100%;
height: 100%;
}
.con-box{
position: absolute;
width: 100%;
height: 100%;
left: 0;
top:0;
z-index: 10;
padding: 20rpx 28rpx;
box-sizing: border-box;
.header{
height: 100rpx;
padding: 0 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(0deg, #4285EC 0%, #0BBAFB 100%);
color: #fff;
font-size: 26rpx;
border-radius: 10rpx
.headBtn{
background: #499FFF;
border-radius: 4px;
width: 100rpx;
text-align: center;
height: 50rpx;
line-height: 50rpx;
}
}
.problemCard{
margin-top: 30rpx;
.problemTitle{
font-size: 30rpx;
.titleType{
display: inline-block;
background-color: #499FFF;
border-radius: 10rpx 10rpx 10rpx 0;
padding: 8rpx 12rpx;
color: #fff;
font-size: 26rpx;
margin-right: 20rpx;
}
}
.options{
margin-top: 30rpx;
.opt{
height: 60rpx;
/* background-color: #F8F9FA; */
border-radius: 5px;
margin-bottom: 15px;
display: flex;
align-items: center;
padding-left: 30rpx;
box-sizing: border-box;
color: #808080;
font-size: 30rpx;
.optLab{
width: 40rpx;
height: 40rpx;
text-align: center;
line-height: 40rpx;
border-radius: 50%;
background-color: #8C8C8C;
color: #fff;
font-weight: 600;
font-size: 32rpx;
margin-right: 20rpx;
}
}
.active{
background: linear-gradient(90deg, #25A9F5 0%, #B1DBFF 100%);
color: #fff!important;
font-weight: bold;
}
.active>view{
background-color: #fff!important;
color: #25A9F5!important;
}
}
.analysis{
margin-top: 30rpx;
background-color: #fff;
border-radius: 6px;
margin-bottom: 15rpx;
border: 1px solid #10A8FF;
padding: 20rpx;
box-sizing: border-box;
.analysisHead{
display: flex;
align-items: center;
justify-content: space-between;
font-size: 32rpx;
font-family: Microsoft YaHei;
font-weight: bold;
letter-spacing: 2px;
}
.correct{
color: #67C23A;
}
.errors{
color: #F06A6A;
}
.analysisCon{
margin-top: 30rpx;
.parse1{
font-size: 30rpx;
font-family: Microsoft YaHei;
font-weight: bold;
}
.parse2{
font-size: 26rpx;
color: #333;
margin-top: 10px;
}
}
}
.problemBtns{
display: flex
align-items: center
justify-content: center
view{
width: 140rpx
height: 50rpx
text-align: center
line-height: 50rpx
background-color: #10A8FF
color: #fff
font-size: 28rpx
border-radius: 5rpx
margin-right: 10rpx;
}
}
}
.footer{
width: 100%;
height: 120rpx;
border-top: 1px solid #ddd
position: fixed
bottom: 0
left: 0
display: flex
align-items: center
justify-content: space-between
.footerLeft{
display: flex
align-items: center
font-size: 30rpx;
text-align: center
.zuo{
width: 24rpx;
height: 24rpx;
border-top: 2px solid #666
border-left: 2px solid #666
transform: rotate(-45deg); /* 鼠标悬停时旋转360度 */
margin-left: 50rpx;
}
.you{
width: 24rpx;
height: 24rpx;
border-right: 2px solid #666
border-bottom: 2px solid #666
transform: rotate(-45deg); /* 鼠标悬停时旋转360度 */
margin-left: 30rpx;
margin-right: 50rpx;
}
.icons{
width: 30rpx;
height: 30rpx;
color: #fff;
text-align: center;
line-height: 30rpx;
border-radius: 50%
}
.texts{
font-size: 24rpx;
}
}
.footerLeft>view{
margin-right: 40rpx;
}
.footerBtn{
width: 140rpx
height: 50rpx
text-align: center
line-height: 50rpx
background-color: #67C23A
color: #fff
font-size: 28rpx
border-radius: 5rpx
}
}
}
.cards{
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
background-color: rgba(0,0,0,0.5);
z-index: 1000;
padding: 100rpx 50rpx;
box-sizing: border-box;
.cardCon{
height: 100%;
background-color: #fff;
padding: 20rpx;
box-sizing: border-box;
.cardHead{
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
font-weight: 600;
}
}
}
}
.events{
pointer-events: none; /* 这会禁用所有指针事件 */
opacity: 0.5; /* 可选:改变透明度以视觉上表示不可点击 */
cursor: not-allowed; /* 可选:改变鼠标光标样式 */
}
</style>

View File

@@ -0,0 +1,316 @@
<template>
<AppLayout :title="title" :show-bg-image="false" @onScrollBottom="getDataList('add')">
<!-- <template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template> -->
<template #headContent>
<view class="collection-search">
<view class="search-content">
<view class="header-input button-click">
<uni-icons class="iconsearch" color="#6A6A6A" type="search" size="22"></uni-icons>
<input
class="input"
v-model="searchKeyword"
@confirm="searchVideo"
placeholder="输入视频名称"
placeholder-class="inputplace"
/>
<uni-icons
v-if="searchKeyword"
class="clear-icon"
type="clear"
size="24"
color="#999"
@click="clearSearch"
/>
</view>
</view>
</view>
</template>
<view class="main-list">
<view class="list-title">
<text>视频列表</text>
<view class="title-line"></view>
</view>
<view class="video-grid" v-if="pageState.list.length">
<view
v-for="video in pageState.list"
:key="video.id || video.videoId"
class="video-item"
:style="getItemBackgroundStyle('video-bg.png')"
@click="playVideo(video)"
>
<view class="video-cover">
<image
:src="video.coverImage || video.videoCover || '/static/icon/video.png'"
mode="aspectFill"
></image>
</view>
<view class="video-info">
{{ video.title || video.videoName || '未命名视频' }}
</view>
</view>
</view>
<empty v-else pdTop="200"></empty>
</view>
</AppLayout>
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { $api, navTo, navBack } = inject('globalFunction');
// state
const title = ref('');
const searchKeyword = ref('');
const pageState = reactive({
page: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 12,
search: {},
});
const baseUrl = 'http://10.110.145.145/images/train/';
const getItemBackgroundStyle = (imageName) => ({
backgroundImage: `url(${baseUrl + imageName})`,
backgroundSize: 'cover', // 覆盖整个容器
backgroundPosition: 'center', // 居中
backgroundRepeat: 'no-repeat'
});
// 模拟视频数据
const mockVideoData = [
{
id: '1',
title: '职业技能培训基础课程',
coverImage: '/static/icon/server1.png',
videoUrl: ''
},
{
id: '2',
title: '面试技巧分享',
coverImage: '/static/icon/server2.png',
videoUrl: ''
},
{
id: '3',
title: '简历制作指南',
coverImage: '/static/icon/server3.png',
videoUrl: ''
},
{
id: '4',
title: '职场沟通技巧',
coverImage: '/static/icon/server4.png',
videoUrl: ''
},
{
id: '5',
title: '职业规划讲座',
coverImage: '/static/icon/flame.png',
videoUrl: ''
},
{
id: '6',
title: '行业趋势分析',
coverImage: '/static/icon/flame2.png',
videoUrl: ''
}
];
onLoad(() => {
getDataList('refresh');
});
// 搜索视频
function searchVideo() {
getDataList('refresh');
}
// 清除搜索内容
function clearSearch() {
searchKeyword.value = '';
getDataList('refresh');
}
// 获取视频列表
function getDataList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
// 模拟API请求延迟
setTimeout(() => {
// 在实际项目中这里应该调用真实的API接口
// 目前使用模拟数据
let filteredList = [...mockVideoData];
// 如果有搜索关键词,进行过滤
if (searchKeyword.value.trim()) {
filteredList = filteredList.filter(video =>
video.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
}
const start = 0;
const end = pageState.pageSize;
const pageData = filteredList.slice(start, end);
if (type === 'add') {
pageState.list = [...pageState.list, ...pageData];
} else {
pageState.list = pageData;
}
pageState.total = filteredList.length;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
}, 300);
}
// 播放视频
function playVideo(video) {
// 在实际项目中,这里应该导航到视频播放页面
// 或者调用视频播放组件
console.log('播放视频:', video.title);
// 示例navTo(`/pages/videoPlayer/videoPlayer?id=${video.id}`);
$api.msg(`准备播放: ${video.title}`);
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
width: 100%;
}
.collection-search{
padding: 10rpx 20rpx;
.search-content{
position: relative
display: flex
align-items: center
padding: 14rpx 0
.header-input{
padding: 0
width: calc(100%);
position: relative
.iconsearch{
position: absolute
left: 30rpx;
top: 50%
transform: translate(0, -50%)
z-index: 1
}
.input{
padding: 0 80rpx 0 80rpx
height: 80rpx;
background: #FFFFFF;
border-radius: 75rpx 75rpx 75rpx 75rpx;
border: 2rpx solid #ECECEC
font-size: 28rpx;
}
.clear-icon{
position: absolute
right: 30rpx;
top: 50%
transform: translate(0, -50%)
z-index: 1
cursor: pointer
}
.inputplace{
font-weight: 400;
font-size: 28rpx;
color: #B5B5B5;
}
}
}
}
.main-list{
background-color: #ffffff;
padding: 20rpx 20rpx 28rpx 20rpx;
margin:10rpx 30rpx ;
box-shadow: 0px 3px 20px 0px rgba(0,105,234,0.1);
border-radius: 12px;
}
.list-title{
font-weight: bold;
font-size: 36rpx;
color: #404040;
position: relative;
margin-bottom: 20px;
}
.title-line{
position: absolute;
bottom: -10rpx;
left: 36rpx;
width: 70rpx;
height: 8rpx;
background: linear-gradient(90deg, #FFAD58 0%, #FF7A5B 100%);
border-radius: 2px;
}
.video-grid{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.video-item{
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
padding: 20rpx
}
.video-item:active{
transform: scale(0.98);
}
.video-cover{
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 比例 */
background: #f0f0f0;
border-radius: 4rpx;
}
.video-cover image{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.video-info{
padding: 16rpx 16rpx 0 16rpx;
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.video-title{
font-size: 28rpx;
color: #333;
line-height: 40rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,317 @@
<template>
<AppLayout :title="title" :show-bg-image="false" @onScrollBottom="getDataList('add')">
<!-- <template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template> -->
<template #headContent>
<view class="collection-search">
<view class="search-content">
<view class="header-input button-click">
<uni-icons class="iconsearch" color="#6A6A6A" type="search" size="22"></uni-icons>
<input
class="input"
v-model="searchKeyword"
@confirm="searchVideo"
placeholder="输入视频名称"
placeholder-class="inputplace"
/>
<uni-icons
v-if="searchKeyword"
class="clear-icon"
type="clear"
size="24"
color="#999"
@click="clearSearch"
/>
</view>
</view>
</view>
</template>
<view class="main-list">
<view class="list-title">
<text>视频列表</text>
<view class="title-line"></view>
</view>
<view class="video-grid" v-if="pageState.list.length">
<view
v-for="video in pageState.list"
:key="video.id || video.videoId"
class="video-item"
:style="getItemBackgroundStyle('video-bg.png')"
@click="playVideo(video)"
>
<view class="video-cover">
<image
:src="video.coverImage || video.videoCover || '/static/icon/video.png'"
mode="aspectFill"
></image>
</view>
<view class="video-info">
{{ video.title || video.videoName || '未命名视频' }}
</view>
</view>
</view>
<empty v-else pdTop="200"></empty>
</view>
</AppLayout>
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { $api, navTo, navBack } = inject('globalFunction');
import config from "@/config.js"
// state
const title = ref('');
const searchKeyword = ref('');
const pageState = reactive({
page: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 12,
search: {},
});
const baseUrl = config.imgBaseUrl
const getItemBackgroundStyle = (imageName) => ({
backgroundImage: `url(${baseUrl}/train/${imageName})`,
backgroundSize: 'cover', // 覆盖整个容器
backgroundPosition: 'center', // 居中
backgroundRepeat: 'no-repeat'
});
// 模拟视频数据
const mockVideoData = [
{
id: '1',
title: '职业技能培训基础课程',
coverImage: '/static/icon/server1.png',
videoUrl: ''
},
{
id: '2',
title: '面试技巧分享',
coverImage: '/static/icon/server2.png',
videoUrl: ''
},
{
id: '3',
title: '简历制作指南',
coverImage: '/static/icon/server3.png',
videoUrl: ''
},
{
id: '4',
title: '职场沟通技巧',
coverImage: '/static/icon/server4.png',
videoUrl: ''
},
{
id: '5',
title: '职业规划讲座',
coverImage: '/static/icon/flame.png',
videoUrl: ''
},
{
id: '6',
title: '行业趋势分析',
coverImage: '/static/icon/flame2.png',
videoUrl: ''
}
];
onLoad(() => {
getDataList('refresh');
});
// 搜索视频
function searchVideo() {
getDataList('refresh');
}
// 清除搜索内容
function clearSearch() {
searchKeyword.value = '';
getDataList('refresh');
}
// 获取视频列表
function getDataList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
// 模拟API请求延迟
setTimeout(() => {
// 在实际项目中这里应该调用真实的API接口
// 目前使用模拟数据
let filteredList = [...mockVideoData];
// 如果有搜索关键词,进行过滤
if (searchKeyword.value.trim()) {
filteredList = filteredList.filter(video =>
video.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
}
const start = 0;
const end = pageState.pageSize;
const pageData = filteredList.slice(start, end);
if (type === 'add') {
pageState.list = [...pageState.list, ...pageData];
} else {
pageState.list = pageData;
}
pageState.total = filteredList.length;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
}, 300);
}
// 播放视频
function playVideo(video) {
// 在实际项目中,这里应该导航到视频播放页面
// 或者调用视频播放组件
console.log('播放视频:', video);
navTo(`/packageB/train/video/videoDetail?id=${video.id}`);
// $api.msg(`准备播放: ${video.title}`);
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
width: 100%;
}
.collection-search{
padding: 10rpx 20rpx;
.search-content{
position: relative
display: flex
align-items: center
padding: 14rpx 0
.header-input{
padding: 0
width: calc(100%);
position: relative
.iconsearch{
position: absolute
left: 30rpx;
top: 50%
transform: translate(0, -50%)
z-index: 1
}
.input{
padding: 0 80rpx 0 80rpx
height: 80rpx;
background: #FFFFFF;
border-radius: 75rpx 75rpx 75rpx 75rpx;
border: 2rpx solid #ECECEC
font-size: 28rpx;
}
.clear-icon{
position: absolute
right: 30rpx;
top: 50%
transform: translate(0, -50%)
z-index: 1
cursor: pointer
}
.inputplace{
font-weight: 400;
font-size: 28rpx;
color: #B5B5B5;
}
}
}
}
.main-list{
background-color: #ffffff;
padding: 20rpx 20rpx 28rpx 20rpx;
margin:10rpx 30rpx ;
box-shadow: 0px 3px 20px 0px rgba(0,105,234,0.1);
border-radius: 12px;
}
.list-title{
font-weight: bold;
font-size: 36rpx;
color: #404040;
position: relative;
margin-bottom: 20px;
}
.title-line{
position: absolute;
bottom: -10rpx;
left: 36rpx;
width: 70rpx;
height: 8rpx;
background: linear-gradient(90deg, #FFAD58 0%, #FF7A5B 100%);
border-radius: 2px;
}
.video-grid{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.video-item{
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
padding: 20rpx
}
.video-item:active{
transform: scale(0.98);
}
.video-cover{
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 比例 */
background: #f0f0f0;
border-radius: 4rpx;
}
.video-cover image{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.video-info{
padding: 16rpx 16rpx 0 16rpx;
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.video-title{
font-size: 28rpx;
color: #333;
line-height: 40rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,514 @@
// 获取人员基本信息详情
// import { post, get } from '../../utils/request.js'
// export function getPersonInfo(id) {
// return get({
// url: `personnel/personBaseInfo/${id}`,
// method: 'get'
// })
// }
import request from '@/utilsRc/request'
// 根据 userId 获取企业详情
export function companyDetails(userId) {
return request({
method: 'get',
url: `/company/unitBaseInfo/user/${userId}`,
})
}
// 企业-推荐人员信息
export function recommendedPerson(params) {
return request({
url: '/company/unitBaseInfo/recommend/person',
method: 'get',
params
})
}
// 人员邀请
export function invitePerson(params) {
return request({
url: '/company/unitBaseInfo/invite',
method: 'get',
params
})
}
// 获取企业招聘岗位列表
export function jobList(params) {
return request({
url: '/company/unitPostInfo/list',
method: 'get',
params
})
}
// 查找已投递、已推荐、已邀请的人员信息
export function listMatch(query) {
return request({
url: '/company/unitBaseInfo/relevance',
method: 'get',
params: query
})
}
// 添加企业基本信息
export function addJobBase(data) {
return request({
url: '/company/unitBaseInfo',
method: 'post',
data: data
})
}
// 查询部门下拉树结构
export function deptTreeSelect() {
return request({
url: '/system/center/user/deptTree',
method: 'get'
})
}
// 企业发布招聘岗位
export function addJob(data) {
return request({
url: '/company/unitPostInfo',
method: 'post',
data: data
})
}
// 获取招聘工种列表
export function jobTypeList(params) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params
})
}
// 企业基本信息列表
export function jobBaseList(query) {
return request({
url: '/company/unitBaseInfo/list',
method: 'get',
params: query
})
}
// 获取企业招聘岗位信息详细信息
export function getJob(id) {
return request({
url: `/company/unitPostInfo/${id}`,
method: 'get'
})
}
// 修改企业招聘岗位信息
export function updateJob(data) {
return request({
url: '/company/unitPostInfo',
method: 'put',
data: data
})
}
// 修改企业基本信息
export function updateJobBase(data) {
return request({
url: '/company/unitBaseInfo',
method: 'put',
data: data
})
}
// 查询角色详细
export function getJobService(id) {
return request({
url: '/personnel/personBaseInfo/' + id,
method: 'get'
})
}
// 查询推荐人员、已推荐、已邀请 详情
export function getUnitBaseInfo(id) {
return request({
url: '/manage/personDemand/' + id,
method: 'get'
})
}
// 查询工种列表
export function listJobType(query) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params: query
})
}
// 人员基本信息 - 列表
export function personInfoList(query) {
return request({
url: '/personnel/personBaseInfo/list',
method: 'get',
params: query
})
}
// 获取人员基本信息详情
export function getPersonInfo(id) {
return request({
url: `/personnel/personBaseInfo/${id}`,
method: 'get'
})
}
// 删除人员基本信息
export function delPersonInfo(ids) {
return request({
url: '/personnel/personBaseInfo/' + ids,
method: 'delete'
})
}
// 删除录入人员 公用 type = 1 失业中 2 就业困难 3 离校生 4 其他人员
export function delPersonUser(ids) {
return request({
url: `/personnel/personInputInfo/${ids}`,
method: 'delete',
})
}
// 新增人员基本信息
export function addPersonInfo(data) {
return request({
url: '/personnel/personBaseInfo',
method: 'post',
data: data
})
}
// 修改人员基本信息
export function updatePersonInfo(data) {
return request({
url: '/personnel/personBaseInfo',
method: 'put',
data
})
}
//社区人员审核
export function personInfoAudit(data) {
return request({
url: '/personnel/personBaseInfo/audit',
method: 'post',
data: data
})
}
//记录查看身份证
export function recordLookIdCard(params) {
return request({
url: '/personnel/personBaseInfo/recordLookIdCard',
method: 'get',
params
})
}
/* 失业人员 --------------------------------------------- start */
// 新增失业人员
export function addPersonUnemployed(data) {
return request({
url: '/person/unemployment',
method: 'post',
data
})
}
// 失业人员修改
export function updatePersonUnemployed(data) {
return request({
url: '/person/unemployment',
method: 'put',
data,
})
}
// 失业人员列表
export function unemployment(params) {
return request({
url: '/person/unemployment/list',
method: 'get',
params
})
}
// 失业人员详情
export function unemploymentDetails(id) {
return request({
url: `/person/unemployment/${id}`,
method: 'get',
})
}
// 失业人员删除
export function unemploymentDelete(id) {
return request({
url: `/person/unemployment/${id}`,
method: 'delete',
})
}
/* 失业人员 --------------------------------------------- end */
/* 就业困难人员 --------------------------------------------- start */
// 新增就业困难
export function addPersonDifficult(data) {
return request({
url: '/person/findingEmployment',
method: 'post',
data
})
}
// 修改就业困难
export function updatePersonDifficult(data) {
return request({
url: '/person/findingEmployment',
method: 'put',
data
})
}
// 就业困难列表
export function findingEmployment(params) {
return request({
url: '/person/findingEmployment/list',
method: 'get',
params
})
}
// 就业困难详情
export function findingEmploymentDetails(id) {
return request({
url: `/person/findingEmployment/${id}`,
method: 'get',
})
}
// 就业困难删除
export function findingEmploymentDelete(id) {
return request({
url: `/person/findingEmployment/${id}`,
method: 'delete',
})
}
/* 就业困难人员 --------------------------------------------- end */
/* 离校未就业高校生 --------------------------------------------- start */
// 新增离校未就业高校生
export function addLeaveSchool(data) {
return request({
url: '/person/leavingSchoolInfo',
method: 'post',
data
})
}
// 修改离校未就业高校生
export function updateLeaveSchool(data) {
return request({
url: '/person/leavingSchoolInfo',
method: 'put',
data,
})
}
// 高校未就业列表
export function leavingSchoolInfo(params) {
return request({
url: '/person/leavingSchoolInfo/list',
method: 'get',
params
})
}
// 高校未就业详情
export function leavingSchoolInfoDetails(id) {
return request({
url: `/person/leavingSchoolInfo/${id}`,
method: 'get',
})
}
// 高校未就业删除
export function leavingSchoolInfoDelete(id) {
return request({
url: `/person/leavingSchoolInfo/${id}`,
method: 'delete',
})
}
/* 离校未就业高校生 --------------------------------------------- end */
/* 其他人员 --------------------------------------------- start */
// 新增其他人员
export function addOther(data) {
return request({
url: '/person/other',
method: 'post',
data
})
}
// 其他人员修改
export function updateOther(data) {
return request({
url: '/person/other',
method: 'post',
data,
})
}
// 其他人员列表
export function other(params) {
return request({
url: '/person/other/list',
method: 'get',
params
})
}
// 其他人员详情
export function otherDetails(id) {
return request({
url: `/person/other/${id}`,
method: 'get',
})
}
// 其他人员删除
export function otherDelete(id) {
return request({
url: `/person/other/${id}`,
method: 'delete',
})
}
/* 其他人员 --------------------------------------------- end */
// 需求预警列表
export function personAlertList(params) {
return request({
url: '/manage/personDemand/warningList',
method: 'get',
params
})
}
export function personDealList(params) {
return request({
url: '/manage/personDemand/dealingList',
method: 'get',
params
})
}
// 服务追踪 服务类型/服务id
export function serviceTraceability({
demandType,
id
}) {
return request({
// url: `/system/personRequirementsRecords/serviceTraceability/${demandType}/${id}`,
url: `/timelime/timelime/fwzs/${id}`,
method: 'get',
})
}
// 需求办结
export function requirementCompletion(url, data) {
return request({
url,
method: 'post',
data
})
}
//岗位审核
export function jobAudit(data) {
return request({
url: '/company/unitPostInfo/audit',
method: 'post',
data: data
})
}
//社群 首页未完成数
// export function getPeopleCount() {
// return request({
// url: '/pc/index/getPeopleCount',
// method: 'get',
// })
// }
//社群 首页未完成数
export function getDemandUnfinished() {
return request({
url: '/pc/index/todo',
method: 'get',
})
}
// 删除企业招聘岗位信息
export function delJob(ids) {
return request({
url: '/company/unitPostInfo/' + ids,
method: 'delete'
})
}
// 所在社区列表
export function deptList(params) {
return request({
'url': `/system/center/user/deptList`,
'method': 'get',
params
})
}
// 所在社区列表
export function returnPerson(params) {
return request({
'url': `/personnel/personBaseInfo/returnPerson`,
'method': 'get',
params
})
}
// 根据人的身份证查询人的详细信息
export function getIdNumberInfo(params) {
return request({
'url': `/personnel/personBaseInfo/getIdNumberInfo`,
'method': 'get',
params
})
}

View File

@@ -0,0 +1,15 @@
/*
* @Date: 2025-10-31 11:06:15
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:48:22
*/
// import { post, get } from '@/utilsRc/request'
import request from '@/utilsRc/request'
export function listJobType(query) {
return request({
url: '/basicdata/workType/list',
method: 'get',
params: query
})
}

50
packageRc/api/login.js Normal file
View File

@@ -0,0 +1,50 @@
/*
* @Date: 2025-10-31 11:06:15
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:51:45
*/
import request from '@/utilsRc/request'
// 登录方法
export function login(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}
export function smsLogin(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}
export function wechatLogin(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}
export function register(data) {
return request({
method: 'post',
url: '/personnel/personBaseInfo/loginGrAndQy',
data,
headers: {
isToken: false
}
})
}

View File

@@ -0,0 +1,34 @@
/*
* @Date: 2025-10-31 11:06:15
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:48:30
*/
// 人员接口
// import { post, get } from '@/utilsRc/request'
import request from '@/utilsRc/request'
export function getPersonBase(params) {
return request({
url: '/personnel/personBaseInfo/list',
method: 'get',
params
})
}
export function getPersonList(params) {
return request({
url: '/personnel/personBaseInfo/list',
method: 'get',
params
})
}
// 新增角色
export function addInvestigate(data) {
return request({
// url: '//process/processInterview',
url: '//timelime/timelime',
method: 'post',
data: data
})
}

View File

@@ -0,0 +1,52 @@
/*
* @Date: 2025-11-03 08:48:44
* @LastEditors: lip
* @LastEditTime: 2025-11-03 12:48:41
*/
// 查询个人需求信息列表
// import { post, get } from '@/utilsRc/request'
import request from '@/utilsRc/request'
export function listPersonDemand(query) {
return request({
method: 'get',
url: '/manage/personDemand/list',
params: query
})
}
export function delPersonDemand(id) {
return request({
url: '/manage/personDemand/' + id,
method: 'delete'
})
}
// 查询个人需求信息详细
export function getPersonDemand(id) {
return request({
method: 'get',
url: '/manage/personDemand/' + id,
})
}
// 新增个人需求信息
export function addPersonDemand(data) {
// 确保传递数据前进行日志输出
console.log('addPersonDemand函数接收到的数据:', data);
return request({
url: '/manage/personDemand',
method: 'post', // 修改为大写POST确保请求参数正确传递
data: data
})
}
// 修改个人需求信息
export function updatePersonDemand(data) {
return request({
url: '/manage/personDemand',
method: 'put',
data: data
})
}

View File

@@ -0,0 +1,49 @@
/*
* @Descripttion:
* @Author: lip
* @Date: 2025-11-03 12:35:56
* @LastEditors: lip
*/
// import { post, get } from '../../utils/request.js'
import request from '@/utilsRc/request'
// 登录方法
export function personInfoList(data) {
return request({
method: 'get',
url: '/personnel/personBaseInfo/list',
params: data,
})
}
// 需求预警列表
export function personAlertList(params) {
return request({
method: 'get',
url: '/manage/personDemand/warningList',
params
})
}
//经办人数据获取
export function getJbrInfo() {
return request({
method: 'get',
url: `system/center/user/selectHxjbr`,
method: 'get'
})
}
export function getPersonBase() {
return request({
method: 'get',
url: `system/center/user/selectHxjbr`,
method: 'get'
})
}
export function returnPerson(params) {
return request({
method: 'get',
'url': `/personnel/personBaseInfo/returnPerson`,
params
})
}

View File

@@ -0,0 +1,374 @@
<template>
<view>
<view class="popupAll">
<view class="tabList dispalyF" v-if="!allCheckShow">
<scroll-view scroll-x style="white-space: nowrap;width: calc(100% - 144rpx);margin-left: 32rpx">
<view class="rightView">
<view
@click="getTopActive(newCkeckData[0], index, item)"
:class="
index == newCkeckData[0].activeIndex
? 'popupItem-active tabItem rightView'
: 'popupItem tabItem rightView'
"
v-for="(item, index) in newCkeckData[0] && newCkeckData[0].data"
:key="index"
style="display: inline-block; margin-right: 15rpx;margin-bottom: 0"
>
{{ item.dictLabel }}
</view>
</view>
</scroll-view>
<view class="tabrightBtnOut" @click="allCheckShowChange">
<image
class="tabrightBtn"
src="https://rc.jinan.gov.cn/qcwjyH5/static/images/getMoreCheck.png"
mode=""
></image>
</view>
</view>
<view class="popupListAll" v-if="allCheckShow">
<view
class="popupList"
v-for="(item, index) in newCkeckData"
:key="index"
>
<view class="tabTitle">
{{ item.name }}
</view>
<view
class="dispalyF"
:style="[
{
position: 'sticky',
top: '-2rpx',
zIndex: 1,
flexWrap: 'wrap',
},
]"
>
<template>
<template v-for="(itm, idx) in item.data">
<view :key="idx" v-if="!itm.mode"
@click="getActive(itm, index, idx)"
:class="
idx == item.activeIndex
? 'popupItem-active'
: 'popupItem'
"
style="margin-right: 20rpx"
>
{{ itm.dictLabel }}
</view>
<!-- <view v-if="itm.mode == 'timerange'">
{{itm.start||'开始时间'}} - {{ item.end||'结束时间' }}
</view> -->
</template>
</template>
</view>
</view>
<!-- <view class="search_btn" @click="search">
查询
</view> -->
<view class="bottom-search">
<view class="search-left" @click="clearAll">
清空
</view>
<view class="search-right" @click="search">查询</view>
</view>
<view class="popupPic" @click="close">
<u-icon name="arrow-up"></u-icon>
</view>
</view>
<view class="gropo" v-if="allCheckShow"></view>
</view>
</view>
</template>
<script>
export default {
name: "popupList",
props: {
checkData: {
type: Array,
default: [],
},
},
data() {
return {
newCkeckData: this.checkData,
allCheckShow: false,
params: "",
fijItem: "",
outData: [],
nickName: ''
};
},
watch: {
checkData(newVal) {
this.newCkeckData = newVal;
},
allCheckShow(newVal) {
this.$parent.zctopShow = newVal;
},
},
mounted() {
if (uni.getStorageSync("userInfo")) {
let userInfo = JSON.parse(uni.getStorageSync("userInfo"));
this.nickName = userInfo.userName ? userInfo.userName : "";
}
},
methods: {
allCheckShowChange() {
this.allCheckShow = !this.allCheckShow;
},
search() {
this.allCheckShow = false;
let param = [];
this.$emit('popupSearch', this.newCkeckData)
},
clearAll(){
this.allCheckShow = false;
this.newCkeckData.forEach((item, index) => {
item.activeIndex=0
});
this.$emit('popupSearch', this.newCkeckData)
},
close() {
this.allCheckShow = false;
},
getActive(item, index, idx) {
if(item.checkLogin&&!this.nickName){
uni.showToast({
title: item.noLoginText,
icon: 'none'
})
return
}
this.newCkeckData[index].activeIndex = idx;
},
getTopActive(list, index, item) {
if(item.checkLogin&&!this.nickName){
uni.showToast({
title: item.noLoginText,
icon: 'none'
})
return
}
this.$set(this.newCkeckData[0], "activeIndex", index);
this.search();
},
},
};
</script>
<style lang="scss">
.dispalyF {
display: flex;
align-items: center;
}
.popupAll {
width: 100%;
position: relative;
z-index: 9;
}
.popupListAll {
background: #F2F4F7;
position: relative;
z-index: 99;
width: 100%;
padding-top: 8rpx;
}
.popupList {
background: #F2F4F7;
margin: 0 32rpx;
.tabTitle {
font-family: Source Han Sans;
font-size: 24rpx;
color: #282828;
margin: 16rpx 0;
}
.popupItem {
padding: 0 32rpx;
text-align: center;
background: #fff;
border-radius: 4rpx;
font-family: Source Han Sans;
font-size: 24rpx;
font-feature-settings: "kern" on;
color: #282828;
margin-right: 15rpx;
flex-shrink: 0;
margin-bottom: 20rpx;
line-height: 56rpx;
min-width: 144rpx;
border: 1rpx solid #CAD4E2;
box-sizing: border-box;
}
.popupItem-active {
padding: 0 32rpx;
text-align: center;
background: #1A62CE;
border-radius: 4rpx;
font-family: Source Han Sans;
font-size: 24rpx;
font-feature-settings: "kern" on;
color: #fff;
margin-right: 15rpx;
flex-shrink: 0;
margin-bottom: 20rpx;
line-height: 56rpx;
min-width: 144rpx;
border: 1rpx solid #1A62CE;
box-sizing: border-box;
}
}
.popupPic {
width: 100%;
padding-top: 16rpx;
padding-bottom: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.gropo {
position: fixed;
left: 0rpx;
top: 0rpx;
width: 100vw;
height: 100vh;
opacity: 0.6;
background: rgba(0, 0, 0, 0.23);
}
.tabList {
padding: 32rpx 0;
position: relative;
overflow: hidden;
flex-wrap: nowrap;
width: 100%;
z-index: 9;
.tabItem {
line-height: 56rpx;
padding: 0 32rpx;
background: #fff;
border-radius: 4rpx;
font-family: Source Han Sans;
font-size: 24rpx;
font-feature-settings: "kern" on;
color: #282828;
margin-right: 15rpx;
flex-shrink: 0;
min-width: 144rpx;
box-sizing: border-box;
text-align: center;
}
.tabrightBtnOut {
position: absolute;
right: 0;
width: 87rpx;
height: 56rpx;
// background: linear-gradient(270deg, #2A51DF 0%, rgba(66, 110, 230, 0) 100%);
}
.tabrightBtn {
position: absolute;
right: 36rpx;
top: 50%;
transform: translateY(-50%);
width: 32rpx;
height: 32rpx;
z-index: 99;
}
.popupItem {
padding: 0 32rpx;
text-align: center;
background: #fff;
border-radius: 4rpx;
font-family: Source Han Sans;
font-size: 24rpx;
font-feature-settings: "kern" on;
color: #282828;
margin-right: 15rpx;
flex-shrink: 0;
margin-bottom: 20rpx;
line-height: 56rpx;
min-width: 144rpx;
border: 1rpx solid #CAD4E2;
box-sizing: border-box;
}
.popupItem-active {
padding: 0 32rpx;
text-align: center;
background: #1A62CE;
border-radius: 4rpx;
font-family: Source Han Sans;
font-size: 24rpx;
font-feature-settings: "kern" on;
color: #fff;
margin-right: 15rpx;
flex-shrink: 0;
margin-bottom: 20rpx;
line-height: 56rpx;
min-width: 144rpx;
border: 1rpx solid #1A62CE;
box-sizing: border-box;
}
}
.dispalyF {
display: flex;
align-items: center;
}
.rightView :last-child {
margin-right: 100rpx !important;
}
.bottom-search {
margin-top:56rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin: 16rpx 32rpx 0;
.search-left {
font-family: Source Han Sans;
font-size: 28rpx;
font-weight: bold;
line-height: 70rpx;
background: #fff;
color: #3D3D3D;
border-radius: 4rpx;
height: 62rpx;
flex-grow: 1;
text-align: center;
}
.search-right {
margin-right: 30rpx;
width: 472rpx;
line-height: 64rpx;
border-radius: 4rpx;
background: linear-gradient(270deg, #53A0EA 10%, #1A62CE 100%);
font-family: Source Han Sans;
font-size: 28rpx;
font-weight: bold;
margin-left: 24rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<u-popup :show="showExitPopup" @close="closeExitPopup" closeOnClickOverlay>
<view class="exit_popup df_flex df__direction_column ">
<view class="popup-header">
<u-icon name="warning" size="60rpx" color="#E33C3C"></u-icon>
<view class="popup-title">退出登录</view>
</view>
<view class="df_flex df_justify_center name">{{nick || '--'}}</view>
<view class="popup-content">确定要退出当前账号吗</view>
<view class="df_flex">
<u-button class="custom_btn" text="退出登录" @tap="logOut"></u-button>
<u-button class="cancel_btn" text="取 消" @tap="closeExitPopup"></u-button>
</view>
</view>
</u-popup>
</template>
<script>
// import {
// mapGetters
// } from 'vuex'
export default {
name: "exitPopup",
// computed: {
// ...mapGetters(['showExitPopup', 'nick'])
// },
data() {
return {
showExitPopup: false,
nick: ''
}
},
onload() {
this.nick = this.$store.state.user.nick
this.showExitPopup = this.$store.state.user.showExitPopup
},
methods: {
closeExitPopup() {
this.$store.commit('SET_SHOWEXITPOPUP', false)
},
// 退出
logOut() {
this.$store.dispatch('LogOut').then((res) => {
this.closeExitPopup()
uni.navigateTo({
url: '/pages/login/login-one'
})
})
}
}
}
</script>
<style lang="scss" scoped>
.exit_popup {
padding: 48rpx 60rpx 60rpx 60rpx;
background: #ffffff;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
&>view {
width: 100%;
}
.popup-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
.popup-title {
margin-top: 16rpx;
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
}
.popup-content {
text-align: center;
font-size: 28rpx;
color: #666666;
margin-bottom: 48rpx;
line-height: 1.5;
display: flex;
justify-content: center;
align-items: center;
}
.name {
margin: 16rpx auto 32rpx auto;
font-size: 32rpx;
font-weight: 500;
color: #333333;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.custom_btn {
margin: 16rpx;
color: #ffffff;
border: none;
border-radius: 12rpx;
background: linear-gradient(135deg, #E33C3C 0%, #ff4757 100%);
box-shadow: 0 4rpx 12rpx rgba(227, 60, 60, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx rgba(227, 60, 60, 0.4);
}
}
.cancel_btn {
margin: 16rpx;
color: #666666;
border: 2rpx solid #e5e5e5;
border-radius: 12rpx;
background-color: #ffffff;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background-color: #f8f9fa;
}
}
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<view>
<u-popup :show="visible">
<view style="position: relative;height: 100vh;width: 100%;">
<view class="button-area" style="padding: 8vh 32rpx 24rpx 32rpx; margin: 0;border: 0;position: relative;z-index: 2;display: block; top: 10rpx; border-radius: 0;">
<u-input style="margin-bottom: 16px;" v-model="placeInput" @change="getLocations" placeholder="请输入并选择相应地点"></u-input>
<view v-if="checkedMarker&&checkedMarker.name" class="selected">您已选择:{{ checkedMarker.name }}<text v-if="checkedMarker.address">{{ checkedMarker.address }}</text></view>
<view v-for="(item, index) in placeList" :key="index" :label="item.name" :value="item.name" @click="addIcon(item.name)" class="place-list">
<view style="display: flex;justify-content: space-between;">{{ item.name }}
<view style="color: #8492a6; font-size: 13px;width: 50%">{{ item.address }}</view>
</view>
</view>
</view>
<view class="map" id="map"></view>
<view class="button-area">
<view class="btn" @click="cancel"> </view>
<view class="btn save" @click="submitForm"> </view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
//import { jsonp } from "vue-jsonp";
export default {
data() {
return {
map: '',
placeList: [],
placeInput: '',
markerList: [],
checkedMarker: '',
visible: false,
}
},
mounted() {
// this.openDialog();
},
methods: {
submitForm() {
if(this.checkedMarker&&this.checkedMarker.name){
this.$emit('selected', this.checkedMarker)
this.visible = false;
}else{
this.$message.warning('您尚未选择地点!')
}
},
cancel() {
this.visible = false;
if (this.map) {
this.map.removeEventListener('click', this.handleMapClick);
}
},
openDialog() {
this.visible = true;
this.$nextTick(() => {
this.map = new BMapGL.Map("map");
var point = new BMapGL.Point(117.123237,36.657017);
this.map.centerAndZoom(point, 15);
this.map.enableScrollWheelZoom();
var locationCtrl = new BMapGL.LocationControl();
this.map.addControl(locationCtrl)
this.map.addEventListener('click', this.handleMapClick);
})
},
handleMapClick(e) {
const lng = e.latlng.lng;
const lat = e.latlng.lat;
// 逆地理编码
jsonp("https://api.map.baidu.com/reverse_geocoding/v3/", {
ak: "qr93Dm5Ph6Vb4n1aTfvHG9KZkvG8S4YU",
output: "json",
location: `${lat},${lng}`,
}).then(res => {
if (res.status === 0) {
// 清除原有标记
this.removeOverlay();
// 新建标记
const point = new BMapGL.Point(lng, lat);
const marker = new BMapGL.Marker(point);
this.map.addOverlay(marker);
this.markerList = [marker];
// 保存选中信息
this.checkedMarker = {
name: res.result.formatted_address,
address: res.result.sematic_description,
location: { lng, lat }
};
this.$forceUpdate();
}
});
},
getLocations(place) {
jsonp("https://api.map.baidu.com/place/v2/suggestion", {
q: place,
ak: "qr93Dm5Ph6Vb4n1aTfvHG9KZkvG8S4YU",
region: '济南市',
output: "json",
})
.then((json) => {
console.log(json,23423434)
this.placeList = json.result
})
.catch((err) => {
console.log(err);
});
},
addIcon(e) {
let arr = this.placeList.filter(ele => ele.name == e)
if(arr.length){
this.addMaker(arr)
this.clickMaker(arr[0]);
}else{
this.addMaker(JSON.parse(JSON.stringify(this.placeList)))
}
this.placeList = []
},
removeOverlay() {
this.markerList.forEach(ele => {
this.map.removeOverlay(ele)
})
},
addMaker(list) {
this.removeOverlay()
list.forEach((ele, index) => {
let point = new BMapGL.Point(ele.location.lng, ele.location.lat);
if(index == 0){
this.map.centerAndZoom(point, 15)
}
let marker = new BMapGL.Marker(point); // 创建标注
this.map.addOverlay(marker);
let that = this;
this.markerList.push(marker)
marker.addEventListener("click", function(){
that.clickMaker(ele)
});
})
},
clickMaker(e){
this.checkedMarker = e;
this.$forceUpdate();
}
}
}
</script>
<style lang="scss" scoped>
.selected {
margin-bottom: 16px;
position: relative;z-index: 2;
background: #DCE2E9;
border-radius: 8rpx;
padding: 12rpx 24rpx;
}
.map{
width: 100%;
margin-top: 16px;
height: 100%;
position: absolute;
z-index: 1;
left: 0;
top: 0;
}
.button-area{
position: absolute;
z-index: 2;
bottom: 0;
padding: 24rpx 32rpx 68rpx;
width: 100%;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn{
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset{
background: #DCE2E9;
}
.save{
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
.place-list{
line-height: 32rpx;
padding: 16rpx 0;
border-bottom: 1px solid #DCE2E9;
}
</style>

View File

@@ -0,0 +1,969 @@
<template>
<view class="page">
<u-navbar
title="帮扶登记"
:autoBack="true"
bgColor="transparent"
leftIconColor="#fff"
:titleStyle="{ color: '#fff' }"
></u-navbar>
<view class="input-outer-part">
<scroll-view scroll-y="true" style="height: calc(100vh - 100px)">
<view class="inner">
<div class="self-form">
<view class="inner-part">
<div class="form-item required">
<label class="form-label">被帮扶对象</label>
<input
v-model="serviceForm.serviceObjectName"
style="border: none; width: 100%; padding: 10px 0;"
placeholder="请输入"
disabled
/>
</div>
<div class="form-item required">
<label class="form-label">帮扶方式</label>
<div class="form-select-wrapper" style="position: relative;">
<!-- 显示区域 -->
<div
class="form-value"
:class="{ 'noValue': !serviceForm.demandType }"
@click="showDemandTypePicker = !showDemandTypePicker"
style="width: 100%; height: 40px; padding: 0 10px; border: 1px solid #e6e6e6; border-radius: 8rpx; background-color: #fff; display: flex; align-items: center; justify-content: space-between; cursor: pointer;"
>
{{ getDemandTypeLabel(serviceForm.demandType) || "请选择" }}
<span class="arrow-down"></span>
</div>
<!-- 选项列表 -->
<div
v-if="showDemandTypePicker"
class="options-list"
style="position: absolute; top: 45px; left: 0; right: 0; background-color: #fff; border: 1px solid #e6e6e6; border-radius: 8rpx; z-index: 1000; max-height: 200px; overflow-y: auto;"
>
<div
v-for="option in demandTypeOptions"
:key="option.value"
@click="selectDemandType(option.value)"
style="padding: 10px; border-bottom: 1px solid #f0f0f0; cursor: pointer;"
>
{{ option.label }}
</div>
</div>
</div>
</div>
<div class="form-item required" v-if="serviceForm.demandType == '4' || serviceForm.demandType == '5'">
<label class="form-label">帮扶时间 <text class="required">*</text></label>
<picker mode="date" :value="serviceForm.practicalSolutionTime" start="1900-01-01" end="2100-12-31" @change="onDateChange">
<view class="date-picker-wrapper" :class="{ noValue: !serviceForm.practicalSolutionTime }">
<view v-if="serviceForm.practicalSolutionTime" class="date-value">{{ serviceForm.practicalSolutionTime }}</view>
<view v-else>请选择</view>
<view class="icon-right"></view>
</view>
</picker>
</div>
<div class="form-item required" v-if="serviceForm.demandType == '4' || serviceForm.demandType == '5'">
<label class="form-label">经办人</label>
<div class="form-select-wrapper">
<el-select
style="width: 100%"
v-model="serviceForm.agentUserId"
placeholder="请选择经办人"
@change="handleAgentChange"
>
<el-option
v-for="item in jingbrList1"
:key="item.userId"
:label="item.nickName"
:value="item.userId"
></el-option>
</el-select>
</div>
</div>
<div class="form-item required" v-if="serviceForm.demandType == '5'">
<label class="form-label">电话沟通结果</label>
<div class="form-value" @click="showPicker('dhgtjg')" :class="{ noValue: !serviceForm.dhgtjg }">
{{ getDhgtjgLabel(serviceForm.dhgtjg) || "请选择" }}
<span class="arrow-down"></span>
</div>
</div>
<div class="form-item required" v-if="serviceForm.demandType == '4' || serviceForm.demandType == '5'">
<label class="form-label">帮扶内容</label>
<div class="date-picker-wrapper" :class="{ noValue: !serviceForm.serviceContent }">
<picker
@change="onServiceContentChange"
:range="serviceContentOptions.map(item => item.label)"
:value="getServiceContentIndex(serviceForm.serviceContent)"
mode="selector"
>
<view class="date-value">
{{ getServiceContentLabel(serviceForm.serviceContent) || "请选择" }}
<span class="arrow-down"></span>
</view>
</picker>
</div>
</div>
</view>
<view class="inner-part">
<div class="form-item required" v-if="serviceForm.demandType == '4' || serviceForm.demandType == '5'">
<label class="form-label">帮扶情况说明</label>
<textarea
v-model="serviceForm.blqksm"
style="width: 100%; border: none; padding: 10px 0; min-height: 100px; resize: none;"
placeholder="请输入"
></textarea>
</div>
</view>
<view class="inner-part">
<div class="form-item required" v-if="serviceForm.demandType == '4' || serviceForm.demandType == '5'">
<label class="form-label">帮扶情况说明</label>
<div class="file-upload-container">
<button class="upload-btn" @click="chooseImages" :disabled="fileList.length >= 6">
+ 添加图片 ({{ fileList.length }}/6)
</button>
<div class="file-list" v-if="fileList.length > 0">
<div v-for="(file, index) in fileList" :key="index" class="file-item">
<image :src="file.url" mode="aspectFill" class="uploaded-image"></image>
<span class="delete-btn" @click="removeImage(index)">删除</span>
</div>
</div>
</div>
</div>
</view>
<view class="inner-part" v-if="serviceForm.demandType == '4' || serviceForm.demandType == '5'">
<div class="form-item">
<label class="form-label">附件</label>
<div style="width: 100%; padding: 10px 0;">
<button @click="triggerFileUpload" style="padding: 8px 16px; background: #f0f0f0; border: none; border-radius: 4px;">
上传附件 (最多6个)
</button>
<!-- 简单的文件列表显示 -->
<div v-if="serviceForm.fileUrl.length > 0" class="file-list">
<div v-for="(file, index) in serviceForm.fileUrl" :key="index" class="file-item">
{{ file.name }}
<span @click="removeFile(index)" style="margin-left: 10px; cursor: pointer; color: #ff4444;">删除</span>
</div>
</div>
</div>
</div>
</view>
</div>
</view>
</scroll-view>
<view class="button-area">
<view class="btn" @click="cancelPage">取消</view>
<view class="btn save" @click="submitServiceForm">保存</view>
</view>
</view>
<!-- 通用选择器 -->
<div v-if="currentPicker" class="datetime-picker-overlay">
<div class="datetime-picker">
<div class="picker-header">
<span @click="cancelPicker" style="padding: 10px;">取消</span>
<span style="font-weight: bold;">{{ getPickerTitle(currentPicker) }}</span>
<span @click="confirmPicker" style="padding: 10px; color: #007AFF;">确定</span>
</div>
<div class="picker-content">
<div v-if="currentPicker === 'practicalSolutionTime'">
<input
type="datetime-local"
v-model="manualDateTime"
style="width: 100%; padding: 15px; box-sizing: border-box;"
/>
</div>
<div v-else class="picker-options">
<div
v-for="option in getPickerOptions(currentPicker)"
:key="option.value"
@click="selectPickerOption(option.value)"
:class="{ 'selected': selectedOption === option.value }"
>
{{ option.label }}
</div>
</div>
</div>
</div>
</div>
</view>
</template>
<script>
import {getJbrInfo} from "../../api/personinfo/index"
import { getDicts } from '@/apiRc/system/dict.js';
export default {
data() {
return {
serviceForm: {
serviceObjectName: '',
userId: '',
demandType: '', // 帮扶方式
practicalSolutionTime: '', // 帮扶时间
agentUserId: '', // 经办人ID
agentUserName: '', // 经办人名称
dhgtjg: '', // 电话沟通结果
serviceContent: '', // 帮扶内容
blqksm: '', // 帮扶情况说明
personStatus: '', // 人员状态
fileUrl: [] // 附件
},
serviceContentMap: {
'1': '就业政策咨询',
'2': '职业指导',
'3': '技能培训',
'4': '岗位推荐',
'5': '创业指导',
'6': '其他'
},
// 帮扶内容选项picker组件需要的格式
serviceContentOptions: [
{ value: '1', label: '就业政策咨询' },
{ value: '2', label: '职业指导' },
{ value: '3', label: '技能培训' },
{ value: '4', label: '岗位推荐' },
{ value: '5', label: '创业指导' },
{ value: '6', label: '其他' }
],
showDemandTypePicker: false, // 控制帮扶方式选择器显示
jingbrList1:[],
currentPicker: null,
selectedOption: '',
manualDateTime: this.formatDateTime(new Date()),
fileList: [], // 初始化文件列表
// 帮扶方式选项 (4: 上门服务, 5: 电话回访)
demandTypeOptions: [
{ value: '4', label: '上门服务' },
{ value: '5', label: '电话回访' }
],
// 经办人选项(模拟数据)
jingbrList: [
{ userId: '1', nickName: '张三' },
{ userId: '2', nickName: '李四' },
{ userId: '3', nickName: '王五' }
],
// 电话沟通结果选项
dhgtjgOptions: [
{ value: '1', label: '已沟通' },
{ value: '2', label: '未接通' },
{ value: '3', label: '拒绝沟通' }
],
// 帮扶内容选项(通过字典获取)
serviceContentOptions: [],
// 人员状态选项(通过字典获取)
personStatusOptions: []
};
},
created(){
this.getJbrInfo11();
// 使用getDicts API获取帮扶内容字典数据
getDicts('qcjy_fwnr').then(res => {
if (res.data && Array.isArray(res.data)) {
// 将字典数据转换为picker组件需要的格式
this.serviceContentOptions = res.data.map(item => ({
value: item.dictValue,
label: item.dictLabel
}));
}
});
// 使用getDicts API获取人员状态字典数据
getDicts('qcjy_ryzt').then(res => {
if (res.data && Array.isArray(res.data)) {
this.personStatusOptions = res.data.map(item => ({
value: item.dictValue,
label: item.dictLabel
}));
}
});
},
onLoad(options) {
if (options.name) {
this.serviceForm.serviceObjectName = options.name;
}
if (options.id) {
this.serviceForm.userId = options.id;
}
},
methods: {
// 选择图片
chooseImages() {
const remainingCount = 6 - this.fileList.length;
if (remainingCount <= 0) {
uni.showToast({ title: '最多只能上传6张图片', icon: 'none' });
return;
}
uni.chooseImage({
count: remainingCount,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// 添加新选择的图片到列表
res.tempFilePaths.forEach((path, index) => {
this.fileList.push({
url: path,
file: res.tempFiles[index]
});
});
// 更新 serviceForm.fileUrl
this.updateFileUrls();
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
},
// 移除图片
removeImage(index) {
this.fileList.splice(index, 1);
// 更新 serviceForm.fileUrl
this.updateFileUrls();
},
// 更新文件URL数组
updateFileUrls() {
this.serviceForm.fileUrl = this.fileList.map(file => file.url);
},
async getJbrInfo11(){
const res=await getJbrInfo()
this.jingbrList1=res
},
// 显示选择器
showPicker(type) {
this.currentPicker = type;
if (type !== 'practicalSolutionTime') {
this.selectedOption = this.serviceForm[type] || '';
}
},
// 取消选择器
cancelPicker() {
this.currentPicker = null;
},
// 确认选择器
confirmPicker() {
if (this.currentPicker === 'practicalSolutionTime') {
this.manualConfirmDate();
} else {
this.serviceForm[this.currentPicker] = this.selectedOption;
// 特殊处理经办人,同时保存名称
if (this.currentPicker === 'agentUserId') {
const agent = this.jingbrList1.find(item => item.userId === this.selectedOption);
this.serviceForm.agentUserName = agent ? agent.nickName : '';
}
}
this.currentPicker = null;
},
// 选择选项
selectPickerOption(value) {
this.selectedOption = value;
},
// 获取选择器标题
getPickerTitle(type) {
const titles = {
demandType: '选择帮扶方式',
practicalSolutionTime: '选择帮扶时间',
agentUserId: '选择经办人',
dhgtjg: '选择电话沟通结果',
serviceContent: '选择帮扶内容'
};
return titles[type] || '请选择';
},
// 获取选择器选项
getPickerOptions(type) {
const options = {
demandType: this.demandTypeOptions || [],
agentUserId: this.jingbrList1 && this.jingbrList1.length > 0 ?
this.jingbrList1.map(item => ({ value: item.userId, label: item.nickName })) : [],
dhgtjg: this.dhgtjgOptions || [],
serviceContent: this.serviceContentOptions || []
};
return options[type] || [];
},
// 获取帮扶方式标签
getDemandTypeLabel(value) {
const option = this.demandTypeOptions.find(item => item.value === value);
return option ? option.label : '';
},
// 获取经办人名称
getAgentUserName(userId) {
const agent = this.jingbrList1.find(item => item.userId === userId);
return agent ? agent.nickName : '';
},
// 处理经办人选择变化
handleAgentChange(value) {
if (!value || !this.jingbrList1 || !this.jingbrList1.length) {
this.serviceForm.agentUserName = '';
return;
}
const user = this.jingbrList1.find(item => item.userId === value);
if (user) {
this.serviceForm.agentUserName = user.nickName;
} else {
this.serviceForm.agentUserName = '';
}
},
// 处理帮扶方式选择变化
handleDemandTypeChange(event) {
this.serviceForm.demandType = event.target.value;
},
// 选择帮扶方式
selectDemandType(value) {
this.serviceForm.demandType = value;
this.showDemandTypePicker = false;
},
// 获取电话沟通结果标签
getDhgtjgLabel(value) {
const option = this.dhgtjgOptions.find(item => item.value === value);
return option ? option.label : '';
},
// 获取帮扶内容标签
getServiceContentLabel(value) {
return this.serviceContentMap[value] || '';
},
// 处理帮扶内容选择变化
onServiceContentChange(e) {
const index = e.detail.value;
if (this.serviceContentOptions && this.serviceContentOptions[index]) {
this.serviceForm.serviceContent = this.serviceContentOptions[index].value;
}
},
// 获取帮扶内容当前索引
getServiceContentIndex(value) {
if (!value) return 0;
const index = this.serviceContentOptions.findIndex(item => item.value === value);
return index !== -1 ? index : 0;
},
// 格式化日期时间为YYYY-MM-DDTHH:MM格式datetime-local输入框需要
formatDateTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
},
// 日期选择变化处理
onDateChange(e) {
this.serviceForm.practicalSolutionTime = e.detail.value;
},
// 手动确认日期选择
manualConfirmDate() {
// 将datetime-local格式转换为显示格式
const date = new Date(this.manualDateTime);
this.serviceForm.practicalSolutionTime = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
},
// 触发文件上传
triggerFileUpload() {
if (this.serviceForm.fileUrl.length < 6) {
// 模拟添加文件
const mockFiles = ['文档.pdf', '图片.jpg', '表格.xlsx', '报告.docx'];
const randomFile = mockFiles[Math.floor(Math.random() * mockFiles.length)];
this.serviceForm.fileUrl.push({
name: randomFile,
url: `mock-url/${Date.now()}/${randomFile}`
});
} else {
uni.showToast({ title: '最多只能上传6个文件', icon: 'none' });
}
},
// 删除文件
removeFile(index) {
this.serviceForm.fileUrl.splice(index, 1);
},
// 取消页面
cancelPage() {
uni.navigateBack();
},
// 验证表单
validateForm() {
if (!this.serviceForm.serviceObjectName) {
uni.showToast({ title: '请填写被帮扶对象', icon: 'none' });
return false;
}
if (!this.serviceForm.demandType) {
uni.showToast({ title: '请选择帮扶方式', icon: 'none' });
return false;
}
// 如果是上门服务或电话回访,需要验证更多字段
if (this.serviceForm.demandType === '4' || this.serviceForm.demandType === '5') {
if (!this.serviceForm.practicalSolutionTime) {
uni.showToast({ title: '请选择帮扶时间', icon: 'none' });
return false;
}
if (!this.serviceForm.agentUserId) {
uni.showToast({ title: '请选择经办人', icon: 'none' });
return false;
}
if (this.serviceForm.demandType === '5' && !this.serviceForm.dhgtjg) {
uni.showToast({ title: '请选择电话沟通结果', icon: 'none' });
return false;
}
if (!this.serviceForm.serviceContent) {
uni.showToast({ title: '请选择帮扶内容', icon: 'none' });
return false;
}
if (!this.serviceForm.blqksm) {
uni.showToast({ title: '请填写帮扶情况说明', icon: 'none' });
return false;
}
if (!this.serviceForm.personStatus) {
uni.showToast({ title: '请选择人员状态', icon: 'none' });
return false;
}
}
return true;
},
setName(){
this.serviceForm.serviceObjectName = this.name
this.serviceForm.serviceObjectId = this.userId
this.serviceForm.userId = this.userId
this.$forceUpdate();
},
// 提交表单
async submitServiceForm() {
try {
// 验证表单
if (!this.validateForm()) {
return;
}
// 模拟加载状态
uni.showLoading({ title: '保存中...' });
// 准备提交数据
const submitData = {
...this.serviceForm,
// 格式化文件数据
fileUrl: JSON.stringify(this.serviceForm.fileUrl)
};
// 打印提交数据
console.log('提交数据:', submitData);
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1000));
uni.showToast({ title: '保存成功', icon: 'success' });
uni.navigateBack();
} catch (error) {
console.error(error);
uni.showToast({ title: '保存失败', icon: 'none' });
} finally {
uni.hideLoading();
}
}
}
};
</script>
<style lang="scss" scoped>
.page {
height: 100vh;
background-color: #eef1f5 !important;
background-image: url("~@/packageRc/static/images/top.png");
background-repeat: no-repeat;
background-size: 100% auto;
}
.input-outer-part {
background: #eef1f5;
padding: 32rpx;
padding-top: 100rpx; /* 增加顶部内边距,防止内容被遮挡 */
position: relative;
top: -80rpx;
border-radius: 32rpx 32rpx 0 0;
}
.inner {
.inner-part {
background: #fff;
padding: 0 32rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
}
}
.self-form {
// 表单样式
}
/* 表单项目样式 */
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
/* 选择器容器样式 */
.form-select-wrapper {
flex: 1;
padding: 5px 0;
position: relative;
}
.form-select {
width: 100%;
padding: 20rpx;
border: 1px solid #e6e6e6;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background-color: #fff;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
}
.form-select.noValue {
color: #999;
}
.form-select-wrapper::after {
content: '▼';
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
color: #999;
font-size: 20rpx;
}
.form-item.required .form-label::after {
content: '*';
color: #ff4444;
margin-left: 4px;
}
.form-label {
width: 110px;
flex-shrink: 0;
font-size: 14px;
color: #333;
padding: 10px 0;
}
.form-value {
flex: 1;
padding: 10px 0;
font-size: 14px;
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-value.noValue {
color: #999;
}
.arrow-down {
font-size: 12px;
color: #A6A6A6;
}
.edit-icon {
font-size: 14px;
color: #A6A6A6;
}
.form-input-wrapper {
flex: 1;
position: relative;
}
.form-input-wrapper input {
width: 100%;
padding: 10px 0;
border: none;
outline: none;
font-size: 14px;
}
.form-input-wrapper .edit-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
/* 单选按钮样式 */
.radio-group {
flex: 1;
display: flex;
flex-wrap: wrap;
padding: 10px 0;
}
.radio-item {
display: flex;
align-items: center;
margin-right: 20px;
margin-bottom: 10px;
cursor: pointer;
}
.radio-item input[type="radio"] {
margin-right: 6px;
}
.radio-label {
font-size: 14px;
color: #666;
}
/* 文件列表样式 */
.file-list {
margin-top: 10px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 14px;
color: #666;
}
/* 文件上传样式 */
.file-upload-container {
width: 100%;
padding: 10px 0;
}
.upload-btn {
padding: 10px 20px;
background-color: #f0f0f0;
border: 1px dashed #ddd;
border-radius: 4px;
color: #666;
margin-bottom: 10px;
width: 100%;
}
.upload-btn:disabled {
opacity: 0.6;
}
.file-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.file-item {
position: relative;
width: 100px;
height: 100px;
}
.uploaded-image {
width: 100%;
height: 100%;
border-radius: 4px;
}
.delete-btn {
position: absolute;
top: -8px;
right: -8px;
background-color: #ff4444;
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
/* 选择器样式 */
.datetime-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.datetime-picker {
background-color: #fff;
border-radius: 12px;
width: 85%;
max-width: 420px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f5f5f5;
font-size: 17px;
background-color: #fafafa;
}
.picker-header > span:first-child {
color: #666;
font-size: 16px;
}
.picker-header > span:last-child {
color: #1d64cf;
font-size: 16px;
font-weight: 500;
}
.picker-content {
max-height: 280px;
overflow-y: auto;
}
.picker-options {
padding: 5px 0;
}
.picker-options > div {
padding: 16px 20px;
font-size: 16px;
color: #333;
position: relative;
transition: all 0.2s ease;
display: flex;
align-items: center;
}
.picker-options > div:not(:last-child) {
border-bottom: 1px solid #f8f8f8;
}
.picker-options > div:active {
background-color: #f5f5f5;
}
.picker-options > div.selected {
background-color: #f0f7ff;
color: #1d64cf;
font-weight: 500;
}
/* 添加选中状态的对勾图标 */
.picker-options > div.selected::after {
content: '✓';
position: absolute;
right: 20px;
font-size: 18px;
font-weight: bold;
color: #1d64cf;
}
/* 日期选择器样式 */
.date-picker-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.date-picker-wrapper.noValue {
background-color: #f9f9f9;
}
.date-value {
color: #333;
}
.icon-right {
color: #999;
font-size: 16px;
}
.noValue {
color: #c0c4cc;
}
.button-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 24rpx 32rpx;
background: #fff;
display: flex;
box-sizing: border-box;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #b8c5d4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.save {
background: linear-gradient(103deg, #1d64cf 0%, #1590d4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
</style>

View File

@@ -0,0 +1,617 @@
<!--
* @Date: 2025-10-16 15:15:47
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 17:41:33
-->
<template>
<view>
<!-- @scroll="handleScroll" @scrolltolower="scrollBottom" -->
<scroll-view :scroll-y="true" class="container" style="background-image: url('../../../packageRc/static/pageBg.png');">
<view style="padding: 40rpx 28rpx;">
<view class="kinggang">
<view>
<view class="num-title" style="color: #1A62CE">重点毕业生数</view>
<view>1120</view>
</view>
<text style="color: #B5C1D1;"></text>
<view>
<view class="num-title" style="color: #16ACB7">累计需求数</view>
<view>1120</view>
</view>
<text style="color: #B5C1D1;"></text>
<view>
<view class="num-title" style="color: #6A57D1">累计服务数</view>
<view>1120</view>
</view>
</view>
<view class="trace-line">
<view class="trace">
<view class="title">
<image src="../../../packageRc/static/trace.png"/>
毕业生追踪
</view>
<view style="display: flex;justify-content: space-between;" @click="goPersonalList">
<view>点击查看</view>
<uni-icons color="#639AEB" type="arrow-right" size="16"></uni-icons>
</view>
</view>
<view class="sendManager">
<view class="title">
<image src="../../../packageRc/static/sendManager.png"/>
任务下发管理员
</view>
<view style="display: flex;justify-content: space-between;">
<view>点击查看</view>
<uni-icons color="#DBAA4E" type="arrow-right" size="16"></uni-icons>
</view>
</view>
</view>
<view class="titles">
<view class="title-item active"><view>待办需求预警列表</view></view>
<view> {{jobList1count}}条信息</view>
</view>
<view v-for="(item, index) in jobList1" :key="index" class="job-list">
<view class="title">{{ item.personName }} {{ item.deptName }}</view>
<view class="info">
{{getDictLabel(item.demandType, qcjy_xqlx)}}_{{ item.jobDescription }}
</view>
<view class="bottom-line">
<view>发起时间{{item.createTime}}</view>
<view style="color: #EF7325;">{{getDictLabel(item.currentStatus, qcjy_xqlc)}}</view>
</view>
</view>
<view class="titles">
<view class="title-item active"><view>待服务毕业生列表</view></view>
<view> {{jobListcount}} 条信息</view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="person-list">
<view class="top-info">
<image v-if="index%2==0" src="../../../packageRc/static/personIcon.png"/>
<image v-else src="../../../packageRc/static/personIconFe.png"/>
<view class="top-right">
<view class="name-line">
<view class="name">{{item.name}}<view class="tag">{{item.zy}}</view></view>
<view class="service-status" v-if="item.fwzt == 0">未服务</view>
<view class="service-status" v-if="item.fwzt == 1">已服务</view>
<view class="service-status" v-if="item.fwzt == 2">联系不上</view>
<view class="service-status" v-if="item.fwzt == 3">拒绝服务</view>
<view class="service-status" v-if="item.isReturn == 1">被退回</view>
</view>
<view class="info-line" style="display: flex;">
<view style="margin-right: 24rpx;"><text>年龄</text>{{item.age}}</view>
<view><text>服务次数</text>{{item.operateNum}}</view>
</view>
</view>
</view>
<view class="info-line">
<view><text>联系电话</text>{{item.phone || '--'}}</view>
<view><text>详细地址</text>{{item.xxdz}}</view>
</view>
<view class="services">
<view @click="showReturnReasonPopup(item.id)">退回</view>
<view @click="tiao(item)">服务</view>
</view>
</view>
</view>
</scroll-view>
<!-- 退回原因弹窗 -->
<uni-popup ref="returnReasonPopup" position="center" round>
<view class="popup-content" style="background:rgb(248, 248, 248);">
<textarea
v-model="returnReason"
class="reason-textarea"
placeholder="请输入退回原因"
placeholder-class="placeholder-style"
rows="5"
maxlength="200"
></textarea>
<view class="popup-footer">
<button class="cancel-btn" @click="cancelReturn">取消</button>
<button class="confirm-btn" @click="confirmReturn">确认退回</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { personInfoList,personAlertList } from '../../api/personinfo/index'
import { returnPerson} from '@/packageRc/api/personinfo/index'
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
let activeTab = ref(1)
let activeTitle = ref(1)
let jobList = ref([{},{},{},{},{}])
let jobListcount = ref()
let returnItemData = ref()
let jobList1 = ref([{},{}])
let jobList1count = ref()
// 退回原因弹窗相关
let returnReasonPopup = ref(null)
let returnReason = ref('')
let currentItemIndex = ref(-1)
function goPersonalList() {
console.log('goPersonalList')
uni.navigateTo({
url: '/packageRc/pages/personalList/personalList'
});
}
function back() {
uni.navigateBack({
delta: 1
});
};
let qcjy_xqlx = ref([])
let qcjy_xqlc = ref([])
import {getDicts} from '@/apiRc/system/dict'
onMounted(() => {
getlist();
getlistyujing();
getDicts('qcjy_xqlx').then(res => {
qcjy_xqlx.value = res.data
});
getDicts('qcjy_xqlc').then(res => {
qcjy_xqlc.value = res.data
});
});
function getDictLabel(value, list) {
const item = list.find(item => item.dictValue === value);
return item ? item.dictLabel : '';
}
async function getlist(){
try {
const res = await personInfoList();
console.log("res", res);
jobList.value = res.rows || [];
jobListcount.value=res.total || 0
} catch (error) {
console.error("获取数据失败:", error);
jobList.value = [];
}
};
async function getlistyujing(){
try {
const res = await personAlertList();
console.log("res", res);
jobList1.value = res.rows || [];
jobList1count.value=res.total || 0
} catch (error) {
console.error("获取数据失败:", error);
jobList1.value = [];
}
};
// 显示退回原因弹窗
function showReturnReasonPopup(dd) {
this.returnItemData.id=dd
console.log("退回")
returnReason.value = ''
// 使用 ref 控制弹窗显示
if (returnReasonPopup.value) {
returnReasonPopup.value.open()
}
}
function tiao(item){
console.log('尝试导航到待办详情页面ID:', item.id, item.userId);
// 尝试直接使用uni.navigateTo使用正确的格式并传递id参数
uni.navigateTo({
url: `/packageRc/pages/daiban/daibandetail?id=${item.id}&name=${item.name}&userId=${item.userId}`,
success: function() {
console.log('导航成功');
},
fail: function(err) {
console.error('导航失败:', err);
}
});
}
// 确认退回
function confirmReturn() {
if (!returnReason.value.trim()) {
uni.showToast({
title: '请填写退回原因',
icon: 'none'
})
return
}
const params = {
id: this.returnItemData.id,
opinion: this.returnReason
}
const { code, msg } = returnPerson(params);
if (code == 200) {
this.returnDialog = false;
this.$u.toast(msg);
this.getList(true);
}
// 这里可以添加提交退回原因的API调用
console.log('退回原因:', returnReason.value, '项目索引:', currentItemIndex.value)
// 模拟提交成功
uni.showToast({
title: '退回成功'
})
// 使用 ref 控制弹窗关闭
if (returnReasonPopup.value) {
returnReasonPopup.value.close()
}
}
// 取消退回
function cancelReturn() {
// 使用 ref 控制弹窗关闭
if (returnReasonPopup.value) {
returnReasonPopup.value.close()
}
}
function viewMore() {
// uni.navigateTo({
// url: '/pages/jobList/jobList'
// })
}
</script>
<style lang="less" scoped>
view{box-sizing: border-box;display: block;}
.container{
background-color: #F4F4F4;background-position: top center;background-size: 100% auto;
height: 100vh;
min-width: 100vw;
padding-bottom: 0;
background-repeat: no-repeat;
box-sizing: border-box;
}
.kinggang{
display: flex;
justify-content: space-around;
align-items: center;
padding: 36rpx 32rpx 33rpx 32rpx;
background: #FFFFFF;
border-radius: 16rpx;
border: 3rpx solid #FFFFFF;
margin-bottom: 24rpx;background: linear-gradient(180deg, #EDF4FF 0%, #FFFFFF 52%);
>view{
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
font-weight: bold;
>view{font-size: 50rpx;}
.num-title{
font-size: 28rpx;
margin-top: 16rpx;
font-weight: normal;
}
// text{
// font-size: 28rpx;
// }
}
image{
width: 78rpx;
// margin-bottom: 15rpx;
height: 78rpx;
}
/* 退回原因弹窗样式 */
.popup-content {
background: #FFFF;
border-radius: 24rpx;
padding: 48rpx;
width: 88%;
box-sizing: border-box;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.15);
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
color: #282828;
margin-bottom: 36rpx;
text-align: center;
}
.reason-textarea {
width: 100%;
border: 2rpx solid #D8D8D8;
border-radius: 12rpx;
padding: 24rpx;
min-height: 220rpx;
font-size: 30rpx;
color: #333333;
box-sizing: border-box;
background: #FAFAFA;
}
.placeholder-style {
color: #999999;
}
.popup-footer {
display: flex;
justify-content: space-between;
margin-top: 40rpx;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 90rpx;
line-height: 90rpx;
border-radius: 16rpx;
font-size: 32rpx;
text-align: center;
margin: 0 15rpx;
font-weight: 500;
}
.cancel-btn {
background: #F8F8F8;
color: #666666;
border: none;
}
.confirm-btn {
background: #1A62CE;
color: #FFFFFF;
border: none;
}
/* 弹窗背景遮罩层样式 */
::v-deep(.uni-popup__wrapper) {
background-color: rgba(0, 0, 0, 0.5);
}
}
.trace-line{
width: 100%;
margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
>view{
padding: 37.5rpx 32rpx;
width: calc(50% - 12rpx);
.title{
font-size: 28rpx;
font-weight: bold;
color: #1D6AD7;
margin-bottom: 16rpx;
display: flex;
image{
width: 46rpx;
height: 46rpx;
margin-right: 11rpx;
}
}
.more{
display: flex;
justify-content: space-between;
}
}
.trace{
color: #2E77DF;
border: 2rpx solid #78ADFF;
background: linear-gradient(0deg, #FFFFFF 1%, #D0E1FF 100%);
box-shadow: inset 0px 4rpx 10rpx 0px rgba(255, 255, 255, 0.302);
.title{
color: #1D6AD7;
}
}
.sendManager{
border: 2rpx solid #FFC34B;
color: #C68412;
box-shadow: inset 0px 4px 10px 0px rgba(255, 255, 255, 0.302);
background: linear-gradient(0deg, #FFFFFF 0%, #FBF4D1 100%);
.title{color: #CE9523;}
}
}
.tabs{
margin-bottom: 29rpx;
border-radius: 16rpx;
display: flex;
background: #fff;
color: #878787;
text-align: center;
width: 100%;
.tab{
width: 50%;
border: 4rpx solid transparent;
border-radius: 16rpx;
line-height: 64rpx;
position: relative;
&.active{
border: 4rpx solid #fff;
color: #fff;
background: linear-gradient(180deg, #79AFFF 1%, #A2B3FE 100%);
box-shadow: 0px 4rpx 10rpx 0px rgba(40, 102, 194, 0.4);
}
}
}
.titles{
display: flex;
margin-bottom: 44rpx;
display: flex;
justify-content: space-between;
.title-item{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-right: 32rpx;
position: relative;
>view{
position: relative;
z-index: 2;
padding: 0 16rpx;
}
&.active::after{
content: '';
position: absolute;
z-index: 1;
bottom: -12rpx;
left: 0;
width: 120%;
height: 24rpx;
border-radius: 50px 0px 0px 50px;
background: linear-gradient(90deg, #78AEFF 0%, rgba(120, 174, 255, 0.31) 52%, rgba(24, 116, 255, 0) 100%);
}
}
}
.job-list{
width: 100%;
margin: 0 auto;
color: #333333;
border-radius: 24rpx;
background: #FFFFFF;
padding: 32rpx;
margin-bottom: 24rpx;
position: relative;
.sign{
position: absolute;
font-size: 24rpx;
right: 0;
top: 0;
padding: 4rpx 14rpx;
border: 1rpx solid #EC4827;
background: rgba(227, 79, 49, 0.09);
border-top-right-radius: 24rpx;
border-bottom-left-radius: 24rpx;
color: #EC4827;
}
.top-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-bottom: 16rpx;
.salary{
font-size: 32rpx;
color: #4C6EFB;
font-weight: bold;
}
}
.title{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-bottom: 16rpx;
display: flex;
image{
width: 46rpx;
height: 46rpx;
margin-right: 11rpx;
}
}
.info{
font-size: 24rpx;
margin-bottom: 16rpx;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.bottom-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-top: 12rpx;
}
}
.view-more-btn{
padding: 10rpx 56rpx;
background: #FFFFFF;
color: #4C6EFB;
border: 1rpx solid #4C6EFB;
text-align: center;
border-radius: 40rpx;
width: fit-content;
margin: 0 auto;
margin-bottom: 20rpx;
}
.person-list{
padding-top: 32rpx;
margin-bottom: 24rpx;
border-radius: 16rpx;
background: #FFFFFF;
.top-info{
padding: 0 32rpx;
margin-bottom: 24rpx;
display: flex;
image{
width: 86rpx;
height: 86rpx;
margin-right: 19rpx;
display: block;
}
.top-right{
flex-grow: 1;
flex-shrink: 1;
.name-line{
display: flex;
justify-content: space-between;
align-items: center;
.name{
font-size: 32rpx;
display: flex;
margin-right: 15rpx;
margin-bottom: 3rpx;
align-items: center;
.tag{
font-size: 24rpx;
line-height: 32rpx;
padding: 0 12rpx;
margin-left: 15rpx;
border-radius: 4rpx;
background: #4D89E3;
color: #fff;
flex-shrink: 0;
}
}
.service-status{
color: #E0A61F;
font-weight: bold;
font-size: 26rpx;
flex-shrink: 0;
}
}
.info-line{
padding: 0;
border: 0;
}
}
}
.info-line{
padding: 0 32rpx 32rpx;
color: #3D3D3D;
border-bottom: 1px solid #E8E8E8;
>view{
text{
color: #8E8E8E;
}
font-size: 26rpx;
}
}
.services{
line-height: 40rpx;
padding: 28rpx 32rpx;
display: flex;
>view{
flex-grow: 1;
font-size: 26rpx;
color: #1A62CE;
text-align: center;
&:first-child{
color: #E04020;
border-right: 1px solid #D8D8D8;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
<template>
<view class="upload-container">
<u-upload :disabled="disabled" :width="width" :height="height" :fileList="internalFileList" :name="name" :multiple="multiple"
:maxCount="maxCount" @afterRead="handleAfterRead" @delete="handleRemove">
</u-upload>
</view>
</template>
<script>
// import {
// uploadImg
// } from '@/api/company'
import config from '@/config'
// import {
// getToken
// } from '@/utils/auth'
export default {
props: {
maxSize: {
type: Number,
default: 5, // 最大文件大小MB
},
allowedFormats: {
type: Array,
default: () => [], // 允许的文件格式
},
maxImageSize: {
type: Object,
default: () => ({
width: 2048,
height: 2048
}), // 图片最大宽度和高度
},
width: {
type: String,
default: '100rpx', // 默认宽度
},
height: {
type: String,
default: '100rpx', // 默认高度
},
name: {
type: String,
default: 'file', // 默认name字段
},
multiple: {
type: Boolean,
default: false, // 是否允许多选,默认不允许
},
maxCount: {
type: Number,
default: 1, // 默认最大上传数量为1
},
fileList: {
type: Array,
default: () => [], // 默认的文件列表为空
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
internalFileList: Array.isArray(this.fileList) ? [...this.fileList] : [], // 内部的文件列表,确保与父组件的同步
};
},
watch: {
// 监听 fileList 的变化,确保内外部数据同步
fileList(newVal) {
this.internalFileList = Array.isArray(newVal) ? [...newVal] : [];
},
},
methods: {
// 新增图片
async handleAfterRead(event) {
let lists = [].concat(event.file);
let fileListLen = this.internalFileList.length;
lists.map((item) => {
this.internalFileList.push({
...item,
status: "uploading",
message: "上传中",
});
});
for (let i = 0; i < lists.length; i++) {
if (this.allowedFormats.length > 0) {
let fileType = lists[i].name.split('.').pop().toLowerCase();
if (!this.allowedFormats.includes(fileType)) {
// this.$emit('error', '不支持的文件格式');
uni.showToast({
title: '不支持的文件格式',
icon: 'none',
});
this.internalFileList.splice(fileListLen, 1);
this.$emit('update', this.internalFileList); // 通知父组件文件列表更新
return;
}
}
const result = await this.uploadFilePromise(lists[i].url);
let item = this.internalFileList[fileListLen];
this.internalFileList.splice(
fileListLen,
1,
Object.assign(item, {
status: "success",
message: "",
data: result,
})
);
fileListLen++;
this.$emit('update', this.internalFileList); // 通知父组件文件列表更新
}
},
uploadFilePromise(url) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: config.baseUrl + '/system/oss/upload',
filePath: url,
name: "file",
header: {
Authorization: "Bearer " + getToken(),
},
success: (uploadFileRes) => {
let res = JSON.parse(uploadFileRes.data);
resolve(res.data);
},
fail: (err) => {
console.log(err);
},
});
});
},
handleRemove({file, index}) {
this.internalFileList.splice(index, 1); // 从文件列表中移除指定文件
this.$emit('update', this.internalFileList); // 通知父组件文件列表更新
},
},
};
</script>
<style scoped>
.upload-container {
display: flex;
align-items: center;
justify-content: center;
}
.upload-slot {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #ccc;
border-radius: 4px;
padding: 20px;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<view>
<u-popup :show="showPersonChooser" closeOnClickOverlay @close="showPersonChooser=false">
<view style="padding: 32rpx 32rpx 0">
<u--input @change="searchChange" placeholder="搜索选择人员" v-model="searchPerson"></u--input>
<scroll-view style="height: 500rpx;" :scroll-y="true">
<view v-for="(item, index) in personList" :key="index" :label="item.name" :value="item.name"
@click="bindPerson(item)" class="person-list" :class="{active: activePerson.id == item.id}">
<view style="display: flex;justify-content: space-between;font-size: 32rpx;font-weight: bold;">
{{ item.name }}
<view style="color: #8492a6; font-size: 13px;width: 50%;text-align: right;">{{ item.phone }}
</view>
</view>
<view style="color: #8492a6;margin-top: 7rpx;">现居住地{{item.currentResidentialAddress}}</view>
</view>
</scroll-view>
<view class="button-area">
<view class="btn" @click="showPersonChooser=false">取消</view>
<view class="btn reset" @click="resetData">重置</view>
<view class="btn save" @click="saveInfo">确定</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import {
getPersonList
} from '../../../api/needs/person'
export default {
data() {
return {
showPersonChooser: false,
activePerson: {},
searchPerson: '',
personList: [],
searcher: '',
}
},
mounted() {
this.doSearch()
},
methods: {
open() {
this.showPersonChooser = true;
this.activePerson = {}
},
saveInfo() {
this.$emit('confirm', this.activePerson)
this.showPersonChooser = false
},
searchChange() {
if (this.searcher) {
clearTimeout(this.searcher)
this.doSearch()
} else {
this.doSearch()
}
},
doSearch() {
this.searcher = setTimeout(() => {
getPersonList({
name: this.searchPerson,
pageSize: 100,
pageNum: 1
}).then(res => {
this.personList = res.rows
console.log("人眼",res.row)
clearTimeout(this.searcher)
})
}, 200)
},
resetData(){
this.searchPerson = '';
this.personList = [];
this.activePerson = {}
},
bindPerson(item) {
this.activePerson = item;
this.$forceUpdate();
},
}
}
</script>
<style lang="scss" scoped>
.person-list {
padding: 24rpx 32rpx;
border-radius: 8rpx;
box-sizing: border-box;
border: 1px solid #e4e4e4;
margin-top: 32rpx;
&.active {
border: 1px solid #1890ff;
}
}
.button-area {
box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
padding: 24rpx 32rpx 68rpx;
width: calc(100% + 64rpx);
margin-left: -32rpx;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset {
background: #DCE2E9;
}
.save {
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
</style>

View File

@@ -0,0 +1,638 @@
<!--
* @Date: 2024-10-08 14:29:36
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 15:43:51
-->
<template>
<view class="container">
<scroll-view scroll-y="true" :style="{height: edit?'calc(90vh - 150px)':'calc(100vh - 144px)'}">
<view class="inner">
<view class="part-title" style="display: flex;justify-content: space-between;">创业需求信息
<view v-if="!edit&&formData.id&&formData.currentStatus!=3" class="btn"
style="font-weight: normal;display: flex;" @click="edit=true">编辑<view class="icon-right"></view></view>
</view>
<view class="inner-part">
<view class="self-form">
<view class="form-item">
<view class="form-label">姓名 <text class="required">*</text></view>
<view v-if="name" style="width: 100%;"
class="disabledLine">
{{ formData.personName || '请选择' }}
</view>
<view v-else style="width: 100%;" @click="openPersonChooser"
:class="{disabledLine: !edit||!canChoosePerson, noValue: !formData.personName}">
{{ formData.personName || '请选择' }}
</view>
<view class="icon-right"></view>
</view>
<view class="form-item">
<view class="form-label">有无场地需求 <text class="required">*</text></view>
<view class="radio-group">
<view
:class="['radio-item', { 'radio-disabled': !edit }]"
@click="formData.ywcdxq = '是'"
v-if="edit">
<view :class="['radio', { 'radio-checked': formData.ywcdxq === '是' }]"></view>
<text></text>
</view>
<view
:class="['radio-item', { 'radio-disabled': !edit }]"
@click="formData.ywcdxq = '否'"
v-if="edit">
<view :class="['radio', { 'radio-checked': formData.ywcdxq === '否' }]"></view>
<text></text>
</view>
<view v-else>{{ formData.ywcdxq }}</view>
</view>
</view>
<view class="form-item">
<view class="form-label">场地面积</view>
<input type="number" :disabled="!edit" v-model="formData.requiredOfficeSpace" placeholder="请输入" :class="['form-input', { 'form-input-disabled': !edit }]"/>
<view class="icon-right"></view>
</view>
<view class="form-item">
<view class="form-label">办公人数</view>
<input type="number" :disabled="!edit" v-model="formData.bgrs" placeholder="请输入" :class="['form-input', { 'form-input-disabled': !edit }]"/>
<view class="icon-right"></view>
</view>
<view class="form-item">
<view class="form-label">办公位置</view>
<input type="text" :disabled="!edit" v-model="formData.bgdd" placeholder="请输入" :class="['form-input', { 'form-input-disabled': !edit }]"/>
<view class="icon-right"></view>
</view>
<view class="form-item">
<view class="form-label">有无创业培训需求 <text class="required">*</text></view>
<view class="radio-group">
<view
:class="['radio-item', { 'radio-disabled': !edit }]"
@click="formData.ywcypxxq = '是'"
v-if="edit">
<view :class="['radio', { 'radio-checked': formData.ywcypxxq === '是' }]"></view>
<text></text>
</view>
<view
:class="['radio-item', { 'radio-disabled': !edit }]"
@click="formData.ywcypxxq = '否'"
v-if="edit">
<view :class="['radio', { 'radio-checked': formData.ywcypxxq === '否' }]"></view>
<text></text>
</view>
<view v-else>{{ formData.ywcypxxq }}</view>
</view>
</view>
<!-- <u-form-item label="是否意向接受创业培训" prop="isInterestedEntrepreneurshipGuidance" required>-->
<!-- <u-radio-group :disabled="!edit" v-model="formData.isInterestedEntrepreneurshipGuidance"-->
<!-- placement="row">-->
<!-- <u-radio :customStyle="{marginRight: '16px'}" label="是" name="是"></u-radio>-->
<!-- <u-radio :customStyle="{marginRight: '16px'}" label="否" name="否"></u-radio>-->
<!-- </u-radio-group>-->
<!-- </u-form-item>-->
<view class="form-item">
<view class="form-label">有无资金需求 <text class="required">*</text></view>
<view class="radio-group">
<view
:class="['radio-item', { 'radio-disabled': !edit }]"
@click="formData.ywzjxq = '是'"
v-if="edit">
<view :class="['radio', { 'radio-checked': formData.ywzjxq === '是' }]"></view>
<text>是</text>
</view>
<view
:class="['radio-item', { 'radio-disabled': !edit }]"
@click="formData.ywzjxq = '否'"
v-if="edit">
<view :class="['radio', { 'radio-checked': formData.ywzjxq === '否' }]"></view>
<text>否</text>
</view>
<view v-else>{{ formData.ywzjxq }}</view>
</view>
</view>
<view class="form-item">
<view class="form-label">需求说明 <text class="required">*</text></view>
<textarea :disabled="!edit" v-model="formData.jobDescription" placeholder="请输入" :class="['form-textarea', { 'form-textarea-disabled': !edit }]"></textarea>
</view>
</view>
</view>
</view>
<!-- <view class="inner" style="margin-top: 32rpx;">-->
<!-- <view class="inner-part">-->
<!-- <u&#45;&#45;form labelPosition="left" class="self-form" labelWidth="110">-->
<!-- <u-form-item label="需求说明" prop="qtxqsm">-->
<!-- <u-textarea :disabled="!edit" v-model="formData.qtxqsm" placeholder="请输入"></u-textarea>-->
<!-- </u-form-item>-->
<!-- </u&#45;&#45;form>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="inner">
<view class="part-title" style="margin-top: 32rpx;">附件信息</view>
<view class="inner-part">
<u--form labelPosition="left" class="self-form" labelWidth="110">
<u-form-item label="附件" prop="fileUrl">
<ImageUpload :fileList="fileList" @update="changeFile" :maxCount="6" />
</u-form-item>
</u--form>
</view>
</view> -->
<!-- 办理完成后 需求说明 -->
<req-comp :form="{
actualSolveDate: formData.actualSolveDate,
actualSolvePeople: formData.actualSolvePeople,
solveDesc: formData.solveDesc,
fileUrl: formData.fileUrl
}" />
</scroll-view>
<view class="button-area" v-if="edit">
<view class="btn" @click="cancelPage">取消</view>
<view class="btn reset" @click="formData.id ? getDetail(formData.id) : setDefaultValues()">重置</view>
<view class="btn save" @click="saveInfo">保存</view>
</view>
</view>
</template>
<script>
import {
getPersonBase
} from "@/packageRc/api/personinfo/index";
import {
addPersonDemand,
updatePersonDemand,
getPersonDemand
} from "@/packageRc/api/needs/personDemand";
import ChoosePerson from './choosePerson';
import dayjs from "dayjs";
export default {
components: {
// ChoosePerson,
// ImageUpload
},
props: {
needId: {
type: String,
default: ''
},
name: {
type: String,
default: ''
}
},
data() {
return {
edit: true,
loading: false,
canChoosePerson: false,
formData: {
id: '',
personId: '',
userId: '',
personName: '',
ywcdxq: '',
requiredOfficeSpace: '',
bgrs: '',
bgdd: '',
ywcypxxq: '',
ywzjxq: '',
jobDescription: '',
actualSolveDate: '',
solveDesc: '',
actualSolvePeople: '',
fileUrl: '',
currentStatus: ''
}
}
},
onReady() {
this.$refs.uForm.setRules(this.rules);
},
async created() {
this.loading = true;
// await this.$delay(600)
this.setDefaultValues()
setTimeout(() => {
this.setName()
}, 0);
},
methods: {
cancelPage() {
if (this.formData.id) {
this.edit = false;
this.getDetail(this.formData.id)
} else {
uni.navigateBack()
}
},
setName(){
// 只有在name有值时才设置人员信息
if(this.name) {
this.formData.personName = this.name
this.formData.personId = this.needId
this.formData.userId = this.needId
}
console.log("this",this)
},
openPersonChooser() {
if (this.edit && this.canChoosePerson) {
this.$refs.personChooser.open();
}
},
personNameConfirm(event) {
this.formData.personName = event.name
this.formData.personId = event.id
this.formData.userId = event.userId
this.formNameChange();
this.$forceUpdate();
},
changeFile(e) {
// 清空当前的 fileUrl 数组
this.formData.fileUrl = [];
// 如果 e 有长度(即用户选择了文件)
if (e.length) {
// 遍历每个文件对象并获取其 url
for (let data of e) {
const url = data.data ? data.data.url : data.url;
this.formData.fileUrl.push(url);
}
}
this.formData.fileUrl = this.$arrayToString(this.formData.fileUrl)
},
addOne() {
this.formData = {}
this.setDefaultValues()
this.getPersonInfo()
if(this.name){
this.formData.personName = this.name
this.formData.personId = this.needId
this.formData.userId = this.needId
}
this.edit = true
},
getDetail(id) {
getPersonDemand(id).then(res => {
this.formData = res.data;
this.edit = false
this.fileList = this.$processFileUrl(this.formData.fileUrl)
})
},
confirmDate(type, e) {
this.show[type] = false;
let date = new Date(e.value)
this.formData[type] =
`${date.getFullYear()}-${(date.getMonth()+1)>9?(date.getMonth()+1):('0'+(date.getMonth()+1))}-${date.getDate()>9?date.getDate():('0'+date.getDate())}`
this.$forceUpdate();
},
goBack() {
uni.navigateBack();
},
cancelPicker(type) {
this.show[type] = false
this.$forceUpdate();
},
getDictLabel(value, list) {
if (list) {
let arr = list.filter(ele => ele.dictValue == value)
if (arr.length) {
return arr[0].dictLabel
} else {
return '请选择'
}
}
},
pickerConfirm(type, event) {
this.show[type] = false
this.formData[type] = event.value[0].dictValue
this.$forceUpdate();
},
showPicker(type) {
if (this.edit) {
this.show[type] = true
this.$forceUpdate()
}
},
getPersonInfo() {
this.loading = true;
this.$store.dispatch("GetInfo").then((res) => {
if (res.data.roles.indexOf('qunzhong') == -1) {
this.canChoosePerson = true;
} else {
this.canChoosePerson = false;
getPersonBase(res.data.user.userId).then(resp => {
this.formData.personId = resp.data.id
this.formData.userId = resp.data.userId
this.formData.personName = resp.data.name
this.formNameChange();
this.$forceUpdate();
})
}
})
},
formNameChange() {
let date = new Date()
// let day =
// `${date.getFullYear()}-${(date.getMonth()+1) + 1 > 9 ? (date.getMonth()+1) + 1: '0'+((date.getMonth()+1) + 1)}-${date.getDate() > 9 ? date.getDate(): '0'+date.getDate()}`
const dayNew = dayjs(date).format("YYYY-MM-DD");
this.formData.serviceRequirementTitle = `${this.formData.personName}_于${dayNew}_提出创业需求`
},
async saveInfo() {
this.setName()
try {
// 验证必填项
if (!this.formData.personName || this.formData.personName.trim() === '') {
uni.showToast({ title: '请选择姓名!', icon: 'none' });
return;
}
if (!this.formData.ywcdxq) {
uni.showToast({ title: '请选择有无场地需求!', icon: 'none' });
return;
}
if (!this.formData.ywcypxxq) {
uni.showToast({ title: '请选择有无创业培训需求!', icon: 'none' });
return;
}
if (!this.formData.ywzjxq) {
uni.showToast({ title: '请选择有无资金需求!', icon: 'none' });
return;
}
if (!this.formData.jobDescription || this.formData.jobDescription.trim() === '') {
uni.showToast({ title: '请填写需求说明!', icon: 'none' });
return;
}
// 显示全局加载
uni.showLoading({ title: '保存中...' });
// 根据 formData 是否有 id 来决定是更新还是新增
let response;
this.formData.demandType = 2;
let successMessage;
if (this.formData.id) {
response = await updatePersonDemand(this.formData);
successMessage = '修改成功';
} else {
response = await addPersonDemand(this.formData);
successMessage = '保存成功';
}
// 检查响应码是否为200
if (response.code === 200) {
uni.showToast({ title: successMessage, icon: 'success' });
// 如果是编辑模式,关闭编辑状态;否则返回上一页
if (this.formData.id) {
this.edit = false;
} else {
// 延迟1秒后返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
} catch (error) {
console.error('保存失败:', error);
uni.showToast({ title: '系统错误,请联系管理员!', icon: 'none' });
} finally {
// 确保加载页总是会被隐藏
uni.hideLoading();
}
},
// 设置默认选中
setDefaultValues(){
this.$set(this.formData, 'ywcdxq', '是')
this.$set(this.formData, 'ywcypxxq', '是')
this.$set(this.formData, 'isInterestedEntrepreneurshipGuidance', '是')
this.$set(this.formData, 'ywzjxq', '是')
},
// 获取详情数据
async getDetail(id) {
try {
this.loading = true;
const response = await getPersonDemand(id);
if (response.code === 200) {
this.formData = { ...response.data };
}
} catch (error) {
console.error('获取详情失败:', error);
uni.showToast({ title: '获取详情失败', icon: 'none' });
} finally {
this.loading = false;
}
}
// saveInfo() {
// this.$refs.uForm.validate().then(res => {
// if (this.formData.id) {
// updateEntrepreneurshipService(this.formData).then(res => {
// if (res.code == 200) {
// uni.showToast({
// title: '修改成功'
// })
// this.edit = false;
// }
// })
// } else {
// addEntrepreneurshipService(this.formData).then(res => {
// if (res.code == 200) {
// uni.showToast({
// title: '保存成功'
// })
// uni.navigateBack();
// }
// })
// }
// }).catch(() => {
// uni.showToast({
// title: '请检查必填项填写',
// icon: 'none'
// })
// })
// }
}
}
</script>
<style lang="scss">
.page ::v-deep .u-navbar__content {
background-color: transparent !important;
}
.page {
background-color: #3161c7;
height: 100vh;
background-image: url('https://rc.jinan.gov.cn/qcwjyH5/static/images/top.png');
background-repeat: no-repeat;
background-size: 100% auto;
}
.inner {
background: #fff;
width: calc(100% - 64rpx);
box-sizing: border-box;
margin: 0 auto;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.inner-part {
width: 100%;
}
/* 为表单元素添加一些间距 */
.self-form {
width: 100%;
}
/* 表单项目样式 */
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx 0;
}
.form-label {
width: 200rpx;
font-size: 28rpx;
color: #333;
flex-shrink: 0;
line-height: 48rpx;
}
.required {
color: #f56c6c;
margin-left: 4rpx;
}
.form-input {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
height: 48rpx;
line-height: 48rpx;
}
.form-input-disabled {
color: #909399;
background-color: #f5f7fa;
}
.form-textarea {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
min-height: 120rpx;
line-height: 40rpx;
}
.form-textarea-disabled {
color: #909399;
background-color: #f5f7fa;
}
.icon-right {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #A6A6A6;
margin-left: 16rpx;
}
/* 单选框样式 */
.radio-group {
flex: 1;
display: flex;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
margin-right: 32rpx;
cursor: pointer;
}
.radio-item.radio-disabled {
cursor: not-allowed;
}
.radio {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
border: 2rpx solid #dcdfe6;
margin-right: 12rpx;
position: relative;
}
.radio.radio-checked {
border-color: #409eff;
background-color: #409eff;
}
.radio.radio-checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #fff;
}
/* 调整按钮区域样式 */
.button-area {
margin-top: 24rpx;
}
.button-area {
padding: 24rpx 32rpx 68rpx;
width: 100%;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset {
background: #DCE2E9;
}
.save {
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
.noValue {
color: rgb(192, 196, 204);
}
.disabledLine {
background: rgb(245, 247, 250);
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,910 @@
<!--
* @Date: 2024-10-08 14:29:36
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 16:11:03
-->
<template>
<view class="container">
<scroll-view scroll-y="true" >
<view class="inner">
<view class="part-title" style="display: flex;justify-content: space-between;">求职需求信息
<view v-if="!edit&&formData.id&&formData.currentStatus!=3" class="btn"
style="font-weight: normal;display: flex;" @click="edit=true">编辑<view class="icon-right"></view></view>
</view>
<view class="inner-part">
<view class="self-form">
<view class="form-item">
<view class="form-label">姓名 <text class="required">*</text></view>
<view v-if="name" style="width: 100%;"
class="disabledLine">
{{ formData.personName || '请选择' }}
</view>
<view v-else style="width: 100%;" @click="openPersonChooser"
:class="{disabledLine: !edit||!canChoosePerson, noValue: !formData.personName}">
{{ formData.personName || '请选择' }}
</view>
<view class="icon-right"></view>
</view>
<!-- <u-form-item label="姓名" prop="personName" required>
<u--input :disabled="!edit" v-model="formData.name" border="none"
placeholder="请输入"></u--input>
<view class="icon-right" style="width: 40rpx; height: 40rpx;"></view>
</u-form-item> -->
<!-- <u-form-item label="求职工种" prop="jobWorkType" required>
<view style="width: 100%;" @click="showPicker('jobWorkType')"
:class="{disabledLine: !edit, noValue: !formData.jobWorkTypeName}">
{{ formData.jobWorkTypeName ||'请选择' }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item> -->
<!-- 新增工种选择 -->
<view class="form-item">
<view class="form-label">求职工种</view>
<picker
mode="multiSelector"
:range="workTypeColumns"
range-key="workTypeName"
:value="workTypeIndexes"
@change="onWorkTypePickerChange"
@columnchange="onWorkTypeColumnChange"
v-if="workTypeColumns[0] && workTypeColumns[0].length"
>
<view class="picker-view">
<view class="picker-view-text">{{ formData.jobWorkTypeName || '请选择工种' }}</view>
<view class="icon-right"></view>
</view>
</picker>
<view v-else class="picker-view">
<view class="picker-view-text">工种数据加载中...</view>
</view>
</view>
<!-- <u-form-item label="工种选择" prop="selectedWorkType">
<uni-data-picker
v-model="selectedWorkType"
:localdata="workTypeTreeList"
:popup-title="'请选择工种'"
:clear-icon="false"
:map="{text:'label', value:'id', children:'children'}"
@change="onWorkTypeChange"
:placeholder="'请选择工种'"
:step-searh="false"
:popup="true"
:multiple="false"
:self-field="false"
/>
</u-form-item> -->
<!-- <u-form-item label="希望解决日期" prop="hopeSolveDate" required>
<view style="width: 100%;" @click="showPicker('hopeSolveDate')"
:class="{disabledLine: !edit, noValue: !formData.hopeSolveDate}">
{{ formData.hopeSolveDate||'请选择' }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item> -->
<!-- <u-form-item label="从业年数" prop="emplymentYear" required>
<view style="width: 100%;" @click="showPicker('emplymentYear')"
:class="{disabledLine: !edit, noValue: getDictLabel(formData.emplymentYear, dict.emplymentYear)=='请选择'}">
{{ getDictLabel(formData.emplymentYear, dict.emplymentYear) }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item>
<u-form-item label="工资类型" prop="salaryType" required>
<view style="width: 100%;" @click="showPicker('salaryType')"
:class="{disabledLine: !edit, noValue: getDictLabel(formData.salaryType, dict.salaryType)=='请选择'}">
{{ getDictLabel(formData.salaryType, dict.salaryType) }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item> -->
<view class="form-item">
<view class="form-label">最低薪酬 <text class="required">*</text></view>
<input type="number" :disabled="!edit" v-model="formData.minRecruitmentSalary" placeholder="请输入" :class="['form-input', { 'form-input-disabled': !edit }]"/>
<view class="icon-right" style="width: 40rpx; height: 40rpx;"></view>
</view>
<view class="form-item">
<view class="form-label">最高薪酬 <text class="required">*</text></view>
<input type="number" :disabled="!edit" v-model="formData.highRecruitmentSalary" placeholder="请输入" :class="['form-input', { 'form-input-disabled': !edit }]"/>
<view class="icon-right" style="width: 40rpx; height: 40rpx;"></view>
</view>
<!-- <u-form-item label="单位性质" prop="unitNature" required>
<view style="width: 100%;" @click="showPicker('unitNature')"
:class="{disabledLine: !edit, noValue: getDictLabel(formData.unitNature, dict.unitNature)=='请选择'}">
{{ getDictLabel(formData.unitNature, dict.unitNature) }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item> -->
<!-- <u-form-item label="用工形式" prop="employmentType" required>
<view style="width: 100%;" @click="showPicker('employmentType')"
:class="{disabledLine: !edit, noValue: getDictLabel(formData.employmentType, dict.employmentType)=='请选择'}">
{{ getDictLabel(formData.employmentType, dict.employmentType) }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item> -->
<view class="form-item">
<view class="form-label">希望工作地点</view>
<input type="text" placeholder="请输入" v-model="formData.addressDesc" class="form-input ellipsis_1" @focus="$refs.placePicker.openDialog()"/>
</view>
<view class="form-item">
<view class="form-label">求职说明 <text class="required">*</text></view>
<textarea :disabled="!edit" v-model="formData.jobDescription" placeholder="请输入" :class="['form-textarea', { 'form-textarea-disabled': !edit }]"></textarea>
</view>
<!-- <u-form-item label="就业意愿" prop="employmentWillingness">
<view style="width: 100%;" @click="showPicker('employmentWillingness')"
:class="{disabledLine: !edit, noValue: getDictLabel(formData.employmentWillingness, dict.employmentWillingness)=='请选择'}">
{{ getDictLabel(formData.employmentWillingness, dict.employmentWillingness) }}
</view>
<u-icon slot="right" name="arrow-down" color="#A6A6A6"></u-icon>
</u-form-item> -->
</view>
</view>
</view>
<!-- 办理完成后 需求说明 -->
<req-comp :form="{
actualSolveDate: formData.actualSolveDate,
actualSolvePeople: formData.actualSolvePeople,
solveDesc: formData.solveDesc,
fileUrl: formData.fileUrl
}" />
<!-- <view class="inner">
<view class="part-title" style="margin-top: 32rpx;">附件信息</view>
<view class="inner-part">
<u--form labelPosition="left" class="self-form" labelWidth="110">
<u-form-item label="附件" prop="fileUrl">
<ImageUpload :fileList="fileList" @update="changeFile" :maxCount="6" />
</u-form-item>
</u--form>
</view>
</view>
<!-- 使用原生picker组件代替uView的选择器 -->
<!-- 工种选择器弹窗 -->
</scroll-view>
<view class="button-area" v-if="edit">
<view class="btn" @click="cancelPage">取消</view>
<view class="btn reset" @click="getDetail(formData.id)">重置</view>
<view class="btn save" @click="saveInfo">保存</view>
</view>
</view>
</template>
<script>
import {
getPersonBase
} from "@/packageRc/api/personinfo/index";
import {
addPersonDemand,
updatePersonDemand,
getPersonDemand
} from "@/packageRc/api/needs/personDemand";
import {
listJobType
} from "@/packageRc/api/jobType/index";
import ImageUpload from './ImageUpload'
import ChoosePerson from './choosePerson';
import PlacePicker from "/packageRc/components/placePicker";
//import uPopup from 'uview-ui/components/u-popup/u-popup.vue'
export default {
components: {
ChoosePerson,
ImageUpload,
PlacePicker,
// uPopup
},
props: {
needId: {
type: String,
default: ''
},
name: {
type: String,
default: ''
}
},
data() {
return {
edit: true,
personBase: {},
dates: {},
currentCommunityId: '',
showPickerPicker: false,
canChoosePerson: true,
canChoosePerson: true,
formData: {
demandType:"1",
personName: '',
personId:"",
userId:"",
jobWorkType: '',
jobWorkTypeName: '',
selectedWorkType: '',
selectedWorkTypeName: '',
highRecruitmentSalary: '',
minRecruitmentSalary: '',
employmentType: '',
currentCommunity: '',
addressDesc: '',
jobDescription: '',
// fileUrl: []
},
bfnrzd:[],
rules: {
personName: [{
required: true,
message: '请填写姓名',
trigger: ['blur', 'change'],
}],
jobWorkType: [{
required: true,
message: '请选择求职工种',
trigger: ['blur', 'change'],
validator: (rule, value) => {
return (Array.isArray(value) && value.length > 0) || (!!value);
}
}],
highRecruitmentSalary: [{
required: true,
message: '请选择最高薪酬',
trigger: ['blur', 'change'],
}, ],
minRecruitmentSalary: [{
required: true,
message: '请选择最低薪酬',
trigger: ['blur', 'change'],
}, ],
jobDescription: [{
required: true,
message: '请填写求职说明',
trigger: ['blur', 'change'],
}, ]
// employmentType: [{
// required: true,
// message: '请选择用工形式',
// trigger: ['blur', 'change'],
// }, ],
// addressDesc: [{
// required: true,
// message: '请选择希望工作地点',
// trigger: ['blur', 'change'],
// }, ],
},
dict: {
emplymentYear: [],
salaryType: [],
highRecruitmentSalary: [],
minRecruitmentSalary: [],
unitNature: [],
employmentWillingness: []
},
show: {
hopeSolveDate: false,
jobWorkType: false,
emplymentYear: false,
salaryType: false,
highRecruitmentSalary: false,
minRecruitmentSalary: false,
unitNature: false,
employmentType: false,
employmentWillingness: false
},
currentCity: '请选择',
bysj: '',
loading: false,
jobTypeList: [],
route: {},
canChoosePerson: false,
fileList: [],
workTypeList: [],
workTypeColumns: [[], [], []],
workTypeIndexes: [0, 0, 0],
}
},
// 移除uView表单验证相关代码
created() {
this.workTypeRemoteMethod('');
console.log('qcjy_fwnr' );
this.$getDict('qcjy_fwnr').then(res => {
this.bfnrzd = res.data;
});
this.setName()
this.loading = true;
let arr = [{
key: 'qcjy_gznx',
prop: 'emplymentYear'
},
{
key: 'qcjy_gzlx',
prop: 'salaryType'
},
{
key: 'qcjy_zgzpgz',
prop: 'highRecruitmentSalary'
},
{
key: 'qcjy_zgzpgz',
prop: 'minRecruitmentSalary'
},
{
key: 'qcjy_dwxz',
prop: 'unitNature'
},
// {
// key: 'qcjy_ygxs',
// prop: 'employmentType'
// },
{
key: 'qcjt_jyyy',
prop: 'employmentWillingness'
},
]
// 使用 Promise.all 确保所有字典数据加载完成
Promise.all(arr.map(ele => this.getDicts(ele.key)))
.then(responses => {
responses.forEach((res, index) => {
this.dict[arr[index].prop] = res.data || [];
});
this.loading = false;
})
.catch(error => {
console.error('加载字典数据失败:', error);
// 设置默认空数组
arr.forEach(ele => {
this.dict[ele.prop] = [];
});
this.loading = false;
});
},
methods: {
// 获取字典数据
getDicts(dictType) {
return this.$getDict(dictType)
// .then(res => {
// this.dict[dictType] = res.data;
// });
},
cancelPage() {
if (this.formData.id) {
this.edit = false;
this.getDetail(this.formData.id)
} else {
uni.navigateBack()
}
},
openPersonChooser() {
if (this.edit && this.canChoosePerson) {
this.$refs.personChooser.open();
}
},
setName(){
// 只有在name有值时才设置人员信息避免覆盖已有数据
if(this.name) {
this.formData.personName = this.name
this.formData.personId = this.needId
this.formData.userId = this.needId
}
},
personNameConfirm(event) {
// this.formData.personName = event.name
this.formData.personId = event.id
this.formData.userId = event.userId
},
changeFile(e) {
// 清空当前的 fileUrl 数组
this.formData.fileUrl = [];
// 如果 e 有长度(即用户选择了文件)
if (e.length) {
// 遍历每个文件对象并获取其 url
for (let data of e) {
const url = data.data ? data.data.url : data.url;
this.formData.fileUrl.push(url);
}
}
this.formData.fileUrl = this.$arrayToString(this.formData.fileUrl)
},
addOne() {
this.formData = {}
this.getPersonInfo()
if(this.name){
this.formData.personName = this.name
this.formData.userId = this.needId
}
this.edit = true
},
getDetail(id) {
getPersonDemand(id).then(res => {
this.formData = res.data;
// 设置工种索引(需要等工种数据加载完成后)
if (this.formData.jobWorkType && this.workTypeColumns[0].length) {
this.setWorkTypeIndexes(this.formData.jobWorkType);
}
this.currentCommunityId = +res.data.currentCommunity
this.formData.currentCommunity = res.data.currentCommunity
this.edit = false;
this.fileList = this.$processFileUrl(this.formData.fileUrl)
}).catch(error => {
console.error('Error fetching job detail:', error);
});
},
confirmDate(type, e) {
this.show[type] = false;
let date = new Date(e.value)
this.formData[type] =
`${date.getFullYear()}-${(date.getMonth()+1)>9?(date.getMonth()+1):('0'+(date.getMonth()+1))}-${date.getDate()>9?date.getDate():('0'+date.getDate())}`
},
workTypeRemoteMethod(key) {
listJobType({
workTypeName: key,
pageNum: 1,
pageSize: 50
}).then(
(res) => {
console.log('工种数据加载成功:', res.rows);
this.jobTypeList = res.rows;
// 处理树形数据为级联选择器格式
this.processWorkTypeTree(res.rows);
}
).catch(error => {
console.error('获取工种列表失败:', error);
this.workTypeList = [];
this.workTypeColumns = [[], [], []];
});
},
goBack() {
uni.navigateBack();
},
cancelPicker(type) {
this.show[type] = false
},
getCityOptis(data) {
if (data && data[0] && data[0].children) {
return [data].concat(this.getCityOptions(data[0].children))
} else {
return [data]
}
},
getDictLabel(value, list) {
if (list && Array.isArray(list)) {
let arr = list.filter(ele => ele.dictValue == value)
if (arr.length) {
return arr[0].dictLabel || '请选择'
} else {
return '请选择'
}
}
return '请选择'
},
pickerConfirm(type, event) {
this.show[type] = false
this.formData[type] = event.value[0].dictValue
},
jobTypeConfirm(type, event) {
this.show[type] = false
this.formData[type] = event.value[0].id
this.formData.jobWorkTypeName = event.value[0].workTypeName
},
showPicker(type) {
if (this.edit) {
if(type === 'workTypeTree') {
if (!this.workTypeTreeColumns[0] || !this.workTypeTreeColumns[0].length) {
this.$u.toast('工种数据未加载,请稍后重试');
return;
}
// 弹窗打开时初始化临时columns和index
this.tempWorkTypeTreeColumns = JSON.parse(JSON.stringify(this.workTypeTreeColumns));
this.tempWorkTypeTreeIndex = [...this.workTypeTreeIndex];
}
this.show[type] = true;
}
},
getPersonInfo() {
this.loading = true;
this.$store.dispatch("GetInfo").then((res) => {
if (res.data.roles.indexOf('qunzhong') == -1) {
this.canChoosePerson = true;
} else {
this.canChoosePerson = false;
getPersonBase(res.data.user.userId).then(resp => {
this.formData.personId = resp.data.id
this.formData.userId = resp.data.userId
this.formData.personName = resp.data.name
})
}
})
},
async saveInfo() {
this.setName()
try {
console.log("this.formData.personName",this.formData.personName)
// 先检查求职说明是否为空,如果为空直接提示
if (!this.formData.jobDescription || this.formData.jobDescription.trim() === '') {
uni.showToast({ title: '请填写求职说明!', icon: 'none' });
return;
}
// 简单的表单验证
console.log(this.formData)
if (!this.formData.personName) {
uni.showToast({ title: '请填写姓名!', icon: 'none' });
return;
}
if (!this.formData.jobWorkType) {
uni.showToast({ title: '请选择求职工种!', icon: 'none' });
return;
}
if (!this.formData.minRecruitmentSalary) {
uni.showToast({ title: '请填写最低薪酬!', icon: 'none' });
return;
}
if (!this.formData.highRecruitmentSalary) {
uni.showToast({ title: '请填写最高薪酬!', icon: 'none' });
return;
}
// 显示全局加载
uni.showLoading({ title: '加载中...' });
this.formData.demandType = 1;
// this.formData.userId = this.formData.personId
// 根据 formData 是否有 id 来决定是更新还是新增
let response;
let successMessage;
if (this.formData.id) {
response = await updatePersonDemand(this.formData);
successMessage = '修改成功';
} else {
response = await addPersonDemand(this.formData);
successMessage = '保存成功';
}
// 检查响应码是否为200
if (response.code === 200) {
uni.showToast({ title: successMessage });
// 如果是编辑模式,关闭编辑状态;否则返回上一页
if (this.formData.id) {
this.edit = false;
} else {
// 延迟1秒后返回
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
} catch (error) {
if(error && error.length){
uni.showToast({ title: '请填写完整信息!', icon: 'none' });
} else {
uni.showToast({ title: '系统错误,请联系管理员!', icon: 'none' });
}
}
finally {
// 确保加载页总是会被隐藏
uni.hideLoading();
}
},
popupclosed() {
this.showPickerPicker = false
},
onChange(e) {
const arr = e.detail.value
this.formData.currentCity = arr[0].value || "";
this.formData.currentArea = arr[1].value || "";
this.formData.currentStreet = arr[2].value || "";
this.formData.currentCommunity = arr[3].value + '' || "";
this.$forceUpdate();
},
// 接收地图数据
handleSelected(marker) {
this.$set(this.formData, "addressDesc", marker.address);
this.$set(this.formData, "latitude", marker.location.lat);
this.$set(this.formData, "longitude", marker.location.lng);
},
// 处理树形数据为级联选择器格式
processWorkTypeTree(treeData) {
if (!treeData || !Array.isArray(treeData)) {
this.workTypeColumns = [[], [], []];
return;
}
// 第一级
const level1 = treeData.filter(item => item.level === "1");
// 第二级
const level2 = treeData.filter(item => item.level === "2");
// 第三级
const level3 = treeData.filter(item => item.level === "3");
// 构建级联数据
const columns = [];
columns[0] = level1;
// 根据第一级选择,过滤第二级
if (level1.length > 0) {
const firstLevelId = level1[0].id;
columns[1] = level2.filter(item => item.parentId === firstLevelId);
} else {
columns[1] = [];
}
// 根据第二级选择,过滤第三级
if (columns[1].length > 0) {
const secondLevelId = columns[1][0].id;
columns[2] = level3.filter(item => item.parentId === secondLevelId);
} else {
columns[2] = [];
}
this.workTypeColumns = columns;
console.log('级联数据构建完成:', this.workTypeColumns);
},
// 级联选择器列变化事件
onWorkTypeColumnChange(e) {
const { column, value } = e.detail;
const newIndexes = [...this.workTypeIndexes];
newIndexes[column] = value;
// 重置后续列的数据
if (column === 0) {
// 第一列变化,重置第二、三列
const selectedLevel1 = this.workTypeColumns[0][value];
if (selectedLevel1) {
const level2 = this.jobTypeList.filter(item =>
item.level === "2" && item.parentId === selectedLevel1.id
);
this.workTypeColumns[1] = level2;
this.workTypeColumns[2] = [];
newIndexes[1] = 0;
newIndexes[2] = 0;
}
} else if (column === 1) {
// 第二列变化,重置第三列
const selectedLevel2 = this.workTypeColumns[1][value];
if (selectedLevel2) {
const level3 = this.jobTypeList.filter(item =>
item.level === "3" && item.parentId === selectedLevel2.id
);
this.workTypeColumns[2] = level3;
newIndexes[2] = 0;
}
}
this.workTypeIndexes = newIndexes;
},
// 级联选择器确认事件
onWorkTypePickerChange(e) {
const indexes = e.detail.value;
const selectedLevel1 = this.workTypeColumns[0][indexes[0]];
const selectedLevel2 = this.workTypeColumns[1][indexes[1]];
const selectedLevel3 = this.workTypeColumns[2][indexes[2]];
if (selectedLevel3) {
// 选择第三级
this.formData.jobWorkType = selectedLevel3.id;
this.formData.jobWorkTypeName = `${selectedLevel1.workTypeName}/${selectedLevel2.workTypeName}/${selectedLevel3.workTypeName}`;
} else if (selectedLevel2) {
// 选择第二级
this.formData.jobWorkType = selectedLevel2.id;
this.formData.jobWorkTypeName = `${selectedLevel1.workTypeName}/${selectedLevel2.workTypeName}`;
} else if (selectedLevel1) {
// 选择第一级
this.formData.jobWorkType = selectedLevel1.id;
this.formData.jobWorkTypeName = selectedLevel1.workTypeName;
}
this.workTypeIndexes = indexes;
},
// 根据工种ID设置索引
setWorkTypeIndexes(workTypeId) {
// 在工种列表中查找对应的工种
const targetWorkType = this.jobTypeList.find(item => item.id == workTypeId);
if (!targetWorkType) return;
// 根据level确定是哪一级
if (targetWorkType.level === "1") {
const index = this.workTypeColumns[0].findIndex(item => item.id == workTypeId);
if (index !== -1) {
this.workTypeIndexes = [index, 0, 0];
}
} else if (targetWorkType.level === "2") {
// 需要先找到父级
const parent = this.jobTypeList.find(item => item.id == targetWorkType.parentId);
if (parent) {
const parentIndex = this.workTypeColumns[0].findIndex(item => item.id == parent.id);
const childIndex = this.workTypeColumns[1].findIndex(item => item.id == workTypeId);
if (parentIndex !== -1 && childIndex !== -1) {
this.workTypeIndexes = [parentIndex, childIndex, 0];
}
}
} else if (targetWorkType.level === "3") {
// 需要找到祖父级和父级
const parent = this.jobTypeList.find(item => item.id == targetWorkType.parentId);
const grandparent = this.jobTypeList.find(item => item.id == parent.parentId);
if (parent && grandparent) {
const grandparentIndex = this.workTypeColumns[0].findIndex(item => item.id == grandparent.id);
const parentIndex = this.workTypeColumns[1].findIndex(item => item.id == parent.id);
const childIndex = this.workTypeColumns[2].findIndex(item => item.id == workTypeId);
if (grandparentIndex !== -1 && parentIndex !== -1 && childIndex !== -1) {
this.workTypeIndexes = [grandparentIndex, parentIndex, childIndex];
}
}
}
}
}
}
</script>
<style lang="scss">
.inner {
background: #fff;
width: calc(100% - 64rpx);
margin: 0 auto;
box-sizing: border-box;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.inner-part {
width: 100%;
}
/* 为表单元素添加一些间距 */
.self-form {
width: 100%;
}
/* 表单项目样式 */
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx 0;
}
.form-label {
width: 200rpx;
font-size: 28rpx;
color: #333;
flex-shrink: 0;
line-height: 48rpx;
}
.required {
color: #f56c6c;
margin-left: 4rpx;
}
.form-input {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
height: 48rpx;
line-height: 48rpx;
}
.form-input-disabled {
color: #909399;
background-color: #f5f7fa;
}
.form-textarea {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
min-height: 120rpx;
line-height: 40rpx;
}
.form-textarea-disabled {
color: #909399;
background-color: #f5f7fa;
}
.icon-right {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #A6A6A6;
margin-left: 16rpx;
}
.picker-view {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
height: 48rpx;
line-height: 48rpx;
}
.picker-view-text {
font-size: 28rpx;
color: #333;
}
/* 按钮区域样式 */
.button-area {
padding: 24rpx 32rpx 68rpx;
width: 100%;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset {
background: #DCE2E9;
}
.save {
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
.noValue {
color: rgb(192, 196, 204);
}
.disabledLine {
background: rgb(245, 247, 250);
cursor: not-allowed;
}
.ellipsis_1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 图标样式优化 */
.icon-right {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #A6A6A6;
margin-left: 16rpx;
}
</style>

View File

@@ -0,0 +1,406 @@
<!--
* @Date: 2024-10-08 14:29:36
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 16:07:40
-->
<template>
<view class="container">
<scroll-view scroll-y="true" >
<view class="inner">
<view class="part-title" style="display: flex;justify-content: space-between;">需求信息
<view v-if="!edit&&formData.id&&formData.currentStatus!=3" class="btn"
style="font-weight: normal;display: flex;" @click="edit=true">编辑<view class="icon-right"></view></view>
</view>
<view class="inner-part">
<view class="self-form">
<view class="form-item">
<view class="form-label">姓名 <text class="required">*</text></view>
<view v-if="name" style="width: 100%;" class="disabledLine">
{{ formData.personName || '请选择' }}
</view>
<view v-else style="width: 100%;" @click="openPersonChooser"
:class="{disabledLine: !edit||!canChoosePerson, noValue: !formData.personName}">
{{ formData.personName || '请选择' }}
</view>
<view class="icon-right"></view>
</view>
<view class="form-item">
<view class="form-label">需求说明 <text class="required">*</text></view>
<textarea :disabled="!edit" v-model="formData.jobDescription" placeholder="请输入" :class="['form-textarea', { 'form-textarea-disabled': !edit }]"></textarea>
</view>
</view>
</view>
</view>
<!-- 办理完成后 需求说明 -->
</scroll-view>
<choose-person ref="personChooser" @confirm="personNameConfirm" />
<view class="button-area" v-if="edit">
<view class="btn" @click="cancelPage">取消</view>
<view class="btn reset" @click="formData.id ? getDetail(formData.id) : setDefaultValues()">重置</view>
<view class="btn save" @click="saveInfo">保存</view>
</view>
</view>
</template>
<script>
import { getPersonBase } from "@/packageRc/api/personinfo/index";
import { addPersonDemand, updatePersonDemand, getPersonDemand } from "@/packageRc/api/needs/personDemand";
//import reqComp from './req-comp';
export default {
components: {
// reqComp
},
props: {
needId: {
type: String,
default: ''
},
name: {
type: String,
default: ''
}
},
data() {
return {
edit: true,
canChoosePerson: false,
formData: {
id: '',
personName: '',
personId: '',
demandType:"9",
userId: '',
demandTitle: '',
jobDescription: '',
actualSolveDate: '',
actualSolvePeople: '',
solveDesc: '',
fileUrl: '',
currentStatus: ''
}
}
},
onReady() {
// 组件已准备就绪
},
created() {
this.loading = true;
// 直接使用传入的参数设置姓名信息
if(this.name && this.needId) {
this.formData.personName = this.name;
this.formData.personId = this.needId;
this.formData.userId = this.needId;
}
// 如果需要获取详情
if (this.needId) {
this.getDetail(this.needId);
}
},
methods: {
cancelPage() {
if (!this.formData.id) {
this.$u.navigateBack();
} else {
this.edit = false;
this.getDetail(this.formData.id);
}
},
// workTypeRemoteMethod(key) {
// listJobType({
// workTypeName: key,
// pageNum: 1,
// pageSize: 50
// }).then(
// (res) => {
// this.jobTypeList = res.rows;
// }
// );
// },
openPersonChooser() {
if (this.edit && this.canChoosePerson) {
this.$refs.personChooser.open();
}
},
personNameConfirm(event) {
this.formData.personName = event.name
this.formData.personId = event.id
this.formData.userId = event.userId
this.$forceUpdate();
},
getDetail(id) {
this.$request({
url: '/mini/demand/getPersonDemandById',
method: 'GET',
data: {
id: id
}
}).then(res => {
if (res.code == 200) {
this.formData = res.data;
}
}).catch(err => {
console.log(err);
});
},
goBack() {
uni.navigateBack();
},
setName() {
// 确保personName存在
if (!this.formData.personName) {
this.formData.personName = '未知用户';
}
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;
month = month < 10 ? '0' + month : month;
let day = date.getDate();
day = day < 10 ? '0' + day : day;
this.formData.demandTitle = `${this.formData.personName}_于${year}-${month}-${day}_提出其他需求`;
},
saveInfo() {
console.log("执行")
try {
// 验证必填项
if (!this.formData.personName) {
uni.showToast({
title: '请选择姓名',
icon: 'none'
});
return;
}
if (!this.formData.jobDescription) {
uni.showToast({
title: '请输入需求说明',
icon: 'none'
});
return;
}
uni.showLoading({
title: '保存中...'
});
this.setName();
let params = {
...this.formData,
type: '3'
};
const requestFn = this.formData.id ? updatePersonDemand : addPersonDemand;
requestFn(params).then(res => {
if (res.code == 200) {
uni.showToast({
title: '保存成功',
icon: 'success'
});
this.edit = false;
if (!this.formData.id) {
this.formData.id = res.data;
}
} else {
uni.showToast({
title: res.message,
icon: 'none'
});
}
}).catch(err => {
if (err.message) {
uni.showToast({
title: err.message,
icon: 'none'
});
}
});
} catch (error) {
console.error('保存失败:', error);
} finally {
uni.hideLoading();
}
}
}
}
</script>
<style lang="scss">
.container {
height: 100vh;
overflow: hidden;
background-color: #f7f7f7;
}
.inner {
background: #fff;
width: calc(100% - 64rpx);
margin: 0 auto;
box-sizing: border-box;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.inner-part {
width: 100%;
}
.self-form {
width: 100%;
}
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx 0;
}
.form-label {
width: 200rpx;
font-size: 28rpx;
color: #333;
flex-shrink: 0;
line-height: 48rpx;
}
.required {
color: #f56c6c;
margin-left: 4rpx;
}
.form-textarea {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
min-height: 120rpx;
line-height: 40rpx;
}
.form-textarea-disabled {
color: #909399;
background-color: #f5f7fa;
}
.icon-right {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #A6A6A6;
margin-left: 16rpx;
}
.button-area {
padding: 24rpx 32rpx 68rpx;
width: 100%;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset {
background: #DCE2E9;
}
.save {
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
.container {
background-color: #f7f7f7;
}
.inner {
background-color: #f7f7f7;
padding: 24rpx 32rpx;
}
.self-form {
width: 100%;
}
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx;
}
.form-label {
width: 200rpx;
font-size: 28rpx;
color: #333;
flex-shrink: 0;
line-height: 48rpx;
}
.required {
color: #f56c6c;
margin-left: 4rpx;
}
.form-textarea {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
min-height: 120rpx;
line-height: 40rpx;
}
.form-textarea-disabled {
color: #909399;
background-color: #f5f7fa;
}
.icon-right {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #A6A6A6;
margin-left: 16rpx;
}
.noValue {
color: rgb(192, 196, 204);
}
.disabledLine {
background: rgb(245, 247, 250);
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,764 @@
<!--
* @Date: 2024-10-08 14:29:36
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 16:07:22
-->
<template>
<view class="container">
<scroll-view scroll-y="true" :style="{height: edit?'calc(90vh - 150px)':'calc(100vh - 144px)'}">
<view class="inner">
<view class="part-title" style="display: flex;justify-content: space-between;">培训需求信息
<view v-if="!edit&&formData.id&&formData.currentStatus!=3" class="btn"
style="font-weight: normal;display: flex;" @click="edit=true">编辑<view class="icon-right"></view></view>
</view>
<view class="inner-part">
<view class="self-form">
<view class="form-item">
<view class="form-label">姓名 <text class="required">*</text></view>
<view v-if="name" style="width: 100%;"
class="disabledLine">
{{ formData.personName || '请选择' }}
</view>
<view v-else style="width: 100%;" @click="openPersonChooser"
:class="{disabledLine: !edit||!canChoosePerson, noValue: !formData.personName}">
{{ formData.personName || '请选择' }}
</view>
<view class="icon-right"></view>
</view>
<view class="form-item">
<view class="form-label">培训意愿工种 <text class="required">*</text></view>
<picker
mode="multiSelector"
:range="workTypeColumns"
range-key="workTypeName"
:value="workTypeIndexes"
@change="onWorkTypePickerChange"
@columnchange="onWorkTypeColumnChange"
v-if="workTypeColumns[0] && workTypeColumns[0].length"
>
<view class="picker-view">
<view class="picker-view-text">{{ formData.qwpxgzName || '请选择工种' }}</view>
<view class="icon-right"></view>
</view>
</picker>
<view v-else class="picker-view">
<view class="picker-view-text">工种数据加载中...</view>
</view>
</view>
<view class="form-item">
<view class="form-label">期望培训时间 <text class="required">*</text></view>
<picker mode="date" :value="formData.qwpxsj" start="1900-01-01" end="2100-12-31" @change="onDateChange">
<view class="date-picker-wrapper" :class="{ noValue: !formData.qwpxsj }">
<view v-if="formData.qwpxsj" class="date-value">{{ formData.qwpxsj }}</view>
<view v-else>请选择</view>
<view class="icon-right"></view>
</view>
</picker>
</view>
<view class="form-item">
<view class="form-label">需求说明 <text class="required">*</text></view>
<textarea :disabled="!edit" v-model="formData.jobDescription" placeholder="请输入" :class="['form-textarea', { 'form-textarea-disabled': !edit }]"></textarea>
</view>
</view>
</view>
</view>
<!-- 办理完成后 需求说明 -->
<req-comp :form="{
actualSolveDate: formData.actualSolveDate,
actualSolvePeople: formData.actualSolvePeople,
solveDesc: formData.solveDesc,
fileUrl: formData.fileUrl
}" />
</scroll-view>
<view class="button-area" v-if="edit">
<view class="btn" @click="cancelPage">取消</view>
<view class="btn reset" @click="getDetail(formData.id)">重置</view>
<view class="btn save" @click="saveInfo">保存</view>
</view>
</view>
</template>
<script>
import { getPersonBase } from "@/packageRc/api/personinfo/index";
import { addPersonDemand, updatePersonDemand, getPersonDemand } from "@/packageRc/api/needs/personDemand";
import { listJobType } from "@/packageRc/api/jobType/index";
import uniDateformat from '@/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue';
export default {
components: {
// ChoosePerson
},
props: {
needId: {
type: String,
default: ''
},
name: {
type: String,
default: ''
}
},
data() {
return {
edit: true,
personBase: {},
formData: {
userId:"",
personName:"",
demandType: "3",
personName: '',
personId: "",
userId: "",
demandTitle: '',
qwpxsj: '',
qwpxgz: '',
qwpxgzName: '',
jobDescription: ''
},
rules: {
personName: [{
required: true,
message: '请填写姓名',
trigger: ['blur', 'change'],
}, ],
qwpxgz: [{
required: true,
message: '请选择培训意愿工种',
trigger: ['blur', 'change'],
validator: (rule, value) => {
// 允许数组且有值,或字符串有值
return (Array.isArray(value) && value.length > 0) || (!!value);
}
}, ],
qwpxsj: [{
required: true,
message: '请选择期望培训时间',
trigger: ['blur', 'change'],
}, ],
},
dict: {},
show: {},
currentCityArr: [],
originalDept: [],
currentCity: '请选择',
bysj: '',
loading: false,
jobTypeList: [],
route: {},
canChoosePerson: false,
workTypeTreeList: [],
searchKeyword: '',
workTypeList: [],
workTypeColumns: [[], [], []],
workTypeIndexes: [0, 0, 0],
}
},
onReady() {
this.$refs.uForm.setRules(this.rules)
},
created() {
this.setName();
this.loading = true;
this.workTypeRemoteMethod('');
},
methods: {
setName() {
// 直接设置人员信息
this.formData.personName = this.name;
this.formData.personId = this.needId;
this.formData.userId = this.needId;
// 设置需求标题
if (this.formData.personName) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day}`;
this.formData.demandTitle = `${this.formData.personName}_于${formattedDate}_提出培训需求`;
}
},
cancelPage() {
if (this.formData.id) {
this.edit = false;
this.getDetail(this.formData.id)
} else {
uni.navigateBack()
}
},
openPersonChooser() {
if (this.edit && this.canChoosePerson) {
this.$refs.personChooser.open();
}
},
personNameConfirm(event) {
this.formData.personName = event.name
this.formData.personId = event.id
this.formData.userId = event.userId
this.formNameChange();
this.$forceUpdate();
},
// 使用picker组件的日期选择
onDateChange(e) {
this.formData.qwpxsj = e.detail.value;
},
changeFile(e) {
// 清空当前的 fileUrl 数组
this.formData.fileUrl = [];
// 如果 e 有长度(即用户选择了文件)
if (e.length) {
// 遍历每个文件对象并获取其 url
for (let data of e) {
const url = data.data ? data.data.url : data.url;
this.formData.fileUrl.push(url);
}
}
this.formData.fileUrl = this.$arrayToString(this.formData.fileUrl)
},
addOne() {
this.formData = {}
this.getPersonInfo()
if(this.name){
this.formData.personName = this.name
this.formData.userId = this.needId
}
this.edit = true
},
workTypeRemoteMethod(key) {
listJobType({
workTypeName: key,
pageNum: 1,
pageSize: 50
}).then(
(res) => {
console.log('工种数据加载成功:', res.rows);
this.jobTypeList = res.rows;
// 处理树形数据为级联选择器格式
this.processWorkTypeTree(res.rows);
}
).catch(error => {
console.error('获取工种列表失败:', error);
this.workTypeList = [];
this.workTypeColumns = [[], [], []];
});
},
jobTypeConfirm(type, event) {
this.show[type] = false
this.formData[type] = event.value[0].id
this.formData.qwpxgzName = event.value[0].workTypeName
this.$forceUpdate();
},
getDetail(id) {
getPersonDemand(id).then(res => {
this.formData = res.data;
// 设置工种索引(需要等工种数据加载完成后)
if (this.formData.qwpxgz && this.workTypeColumns[0].length) {
this.setWorkTypeIndexes(this.formData.qwpxgz);
}
this.edit = false;
}).catch(error => {
console.error('Error fetching training detail:', error);
});
},
goBack() {
uni.navigateBack();
},
cancelPicker(type) {
this.$set(this.show, type, false)
this.$forceUpdate()
},
getDictLabel(value, list) {
if (list) {
let arr = list.filter(ele => ele.dictValue == value)
if (arr.length) {
return arr[0].dictLabel
} else {
return '请选择'
}
}
},
pickerConfirm(type, event) {
this.show[type] = false
this.formData[type] = event.value[0].dictValue
this.$forceUpdate();
},
showPicker(type) {
if (this.edit) {
this.show[type] = true
this.$forceUpdate()
}
},
getPersonInfo() {
this.loading = true;
this.$store.dispatch("GetInfo").then((res) => {
if (res.data.roles.indexOf('qunzhong') == -1) {
this.canChoosePerson = true;
this.setName() // 确保非群众角色也能获取姓名
} else {
this.canChoosePerson = false;
getPersonBase(res.data.user.userId).then(resp => {
this.formData.personId = resp.data.id
this.formData.userId = resp.data.userId
this.formData.personName = resp.data.name
this.formNameChange();
this.$forceUpdate();
})
}
})
},
setName() {
this.formData.personName = this.name;
this.formData.personId = this.needId;
this.formData.userId = this.needId;
let date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dayNew = `${year}-${month}-${day}`;
this.formData.demandTitle = `${this.formData.personName}_于${dayNew}_提出培训需求`;
},
async saveInfo() {
try {
this.setName();
console.log("执行保存,表单数据:", this.formData);
console.log("personName:", this.formData.personName);
console.log("personId:", this.formData.personId);
console.log("userId:", this.formData.userId);
// 手动检查培训意愿工种是否已选择
if (!this.formData.qwpxgz || (typeof this.formData.qwpxgz === 'string' && this.formData.qwpxgz.trim() === '')) {
uni.showToast({
title: '请选择培训意愿工种!',
icon: 'none'
});
return;
}
// 手动检查期望培训时间是否已选择
if (!this.formData.qwpxsj || (typeof this.formData.qwpxsj === 'string' && this.formData.qwpxsj.trim() === '')) {
uni.showToast({
title: '请选择期望培训时间!',
icon: 'none'
});
return;
}
// 表单字段已通过手动检查,无需额外验证
uni.showLoading({
title: '加载中...'
});
let response;
let successMessage;
this.formData.demandType = 3;
if (this.formData.id) {
response = await updatePersonDemand(this.formData);
successMessage = '修改成功';
} else {
response = await addPersonDemand(this.formData);
successMessage = '保存成功';
}
// 检查响应码是否为200
if (response.code === 200) {
uni.showToast({
title: successMessage,
icon: 'success'
});
// 如果是编辑模式,关闭编辑状态;否则返回上一页
if (this.formData.id) {
this.edit = false;
} else {
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
} else {
// 响应码不为200时显示服务器返回的错误信息或默认错误信息
uni.showToast({
title: response.msg || '操作失败,请重试!',
icon: 'none'
});
}
} catch (error) {
console.error('保存失败:', error);
// 检查错误是否为字符串类型或有特定消息
if(typeof error === 'string' || (error && error.message && error.message.includes('请检查'))){
uni.showToast({
title: '请填写完整信息!',
icon: 'none'
});
} else {
uni.showToast({
title: '系统错误,请联系管理员!',
icon: 'none'
});
}
} finally {
// 确保加载页总是会被隐藏
uni.hideLoading();
}
},
// getWorkTypeTree() {
// listJobType({
// workTypeName: '',
// pageNum: 1,
// pageSize: 9999
// }).then(res => {
// console.log("11111",this.workTypeTreeList)
// this.workTypeTreeList = Array.isArray(res.rows) ? res.rows : [];
// });
// },
// 处理树形数据为级联选择器格式
processWorkTypeTree(treeData) {
if (!treeData || !Array.isArray(treeData)) {
this.workTypeColumns = [[], [], []];
return;
}
// 第一级
const level1 = treeData.filter(item => item.level === "1");
// 第二级
const level2 = treeData.filter(item => item.level === "2");
// 第三级
const level3 = treeData.filter(item => item.level === "3");
// 构建级联数据
const columns = [];
columns[0] = level1;
// 根据第一级选择,过滤第二级
if (level1.length > 0) {
const firstLevelId = level1[0].id;
columns[1] = level2.filter(item => item.parentId === firstLevelId);
} else {
columns[1] = [];
}
// 根据第二级选择,过滤第三级
if (columns[1].length > 0) {
const secondLevelId = columns[1][0].id;
columns[2] = level3.filter(item => item.parentId === secondLevelId);
} else {
columns[2] = [];
}
this.workTypeColumns = columns;
console.log('级联数据构建完成:', this.workTypeColumns);
},
// 级联选择器列变化事件
onWorkTypeColumnChange(e) {
const { column, value } = e.detail;
const newIndexes = [...this.workTypeIndexes];
newIndexes[column] = value;
// 重置后续列的数据
if (column === 0) {
// 第一列变化,重置第二、三列
const selectedLevel1 = this.workTypeColumns[0][value];
if (selectedLevel1) {
const level2 = this.jobTypeList.filter(item =>
item.level === "2" && item.parentId === selectedLevel1.id
);
this.workTypeColumns[1] = level2;
this.workTypeColumns[2] = [];
newIndexes[1] = 0;
newIndexes[2] = 0;
}
} else if (column === 1) {
// 第二列变化,重置第三列
const selectedLevel2 = this.workTypeColumns[1][value];
if (selectedLevel2) {
const level3 = this.jobTypeList.filter(item =>
item.level === "3" && item.parentId === selectedLevel2.id
);
this.workTypeColumns[2] = level3;
newIndexes[2] = 0;
}
}
this.workTypeIndexes = newIndexes;
},
// 级联选择器确认事件
onWorkTypePickerChange(e) {
const indexes = e.detail.value;
const selectedLevel1 = this.workTypeColumns[0][indexes[0]];
const selectedLevel2 = this.workTypeColumns[1][indexes[1]];
const selectedLevel3 = this.workTypeColumns[2][indexes[2]];
// 直接赋值确保响应式更新
if (selectedLevel3) {
// 选择第三级
this.formData.qwpxgz = selectedLevel3.id;
this.formData.qwpxgzName = `${selectedLevel1.workTypeName}/${selectedLevel2.workTypeName}/${selectedLevel3.workTypeName}`;
} else if (selectedLevel2) {
// 选择第二级
this.formData.qwpxgz = selectedLevel2.id;
this.formData.qwpxgzName = `${selectedLevel1.workTypeName}/${selectedLevel2.workTypeName}`;
} else if (selectedLevel1) {
// 选择第一级
this.formData.qwpxgz = selectedLevel1.id;
this.formData.qwpxgzName = selectedLevel1.workTypeName;
}
this.workTypeIndexes = indexes;
// 强制重新渲染组件
this.$forceUpdate();
},
// 根据工种ID设置索引
setWorkTypeIndexes(workTypeId) {
// 在工种列表中查找对应的工种
const targetWorkType = this.jobTypeList.find(item => item.id == workTypeId);
if (!targetWorkType) return;
// 根据level确定是哪一级
if (targetWorkType.level === "1") {
const index = this.workTypeColumns[0].findIndex(item => item.id == workTypeId);
if (index !== -1) {
this.workTypeIndexes = [index, 0, 0];
}
} else if (targetWorkType.level === "2") {
// 需要先找到父级
const parent = this.jobTypeList.find(item => item.id == targetWorkType.parentId);
if (parent) {
const parentIndex = this.workTypeColumns[0].findIndex(item => item.id == parent.id);
const childIndex = this.workTypeColumns[1].findIndex(item => item.id == workTypeId);
if (parentIndex !== -1 && childIndex !== -1) {
this.workTypeIndexes = [parentIndex, childIndex, 0];
}
}
} else if (targetWorkType.level === "3") {
// 需要找到祖父级和父级
const parent = this.jobTypeList.find(item => item.id == targetWorkType.parentId);
const grandparent = this.jobTypeList.find(item => item.id == parent.parentId);
if (parent && grandparent) {
const grandparentIndex = this.workTypeColumns[0].findIndex(item => item.id == grandparent.id);
const parentIndex = this.workTypeColumns[1].findIndex(item => item.id == parent.id);
const childIndex = this.workTypeColumns[2].findIndex(item => item.id == workTypeId);
if (grandparentIndex !== -1 && parentIndex !== -1 && childIndex !== -1) {
this.workTypeIndexes = [grandparentIndex, parentIndex, childIndex];
}
}
}
},
}
}
</script>
<style lang="scss">
.inner {
background: #fff;
width: calc(100% - 64rpx);
margin: 0 auto;
box-sizing: border-box;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.inner-part {
width: 100%;
}
/* 为表单元素添加一些间距 */
.self-form {
width: 100%;
}
/* 表单项目样式 */
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx 0;
}
.form-label {
width: 200rpx;
font-size: 28rpx;
color: #333;
flex-shrink: 0;
line-height: 48rpx;
}
.required {
color: #f56c6c;
margin-left: 4rpx;
}
.form-input {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
height: 48rpx;
line-height: 48rpx;
}
.form-input-disabled {
color: #909399;
background-color: #f5f7fa;
}
.form-textarea {
flex: 1;
border: none;
outline: none;
font-size: 28rpx;
color: #333;
background: none;
min-height: 120rpx;
line-height: 40rpx;
}
.form-textarea-disabled {
color: #909399;
background-color: #f5f7fa;
}
.icon-right {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #A6A6A6;
margin-left: 16rpx;
}
/* 单选框样式 */
.radio-group {
flex: 1;
display: flex;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
margin-right: 32rpx;
cursor: pointer;
}
.radio-item.radio-disabled {
cursor: not-allowed;
}
.radio {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
border: 2rpx solid #dcdfe6;
margin-right: 12rpx;
position: relative;
}
.radio.radio-checked {
border-color: #409eff;
background-color: #409eff;
}
.radio.radio-checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #fff;
}
/* 调整按钮区域样式 */
.button-area {
margin-top: 24rpx;
}
.picker-cover {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 99;
}
.button-area {
padding: 24rpx 32rpx 68rpx;
width: 100%;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset {
background: #DCE2E9;
}
.save {
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
.noValue {
color: rgb(192, 196, 204);
}
.disabledLine {
background: rgb(245, 247, 250);
cursor: not-allowed;
}
.bordered {
border: 1rpx solid #dadbde;
padding: 9px;
border-radius: 4px;
}
.picker-view {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.picker-view-text {
color: #333333;
flex: 1;
text-align: left; font-size: 28rpx;
flex: 1;
}
</style>

View File

@@ -0,0 +1,188 @@
<!--
* @Date: 2024-10-08 14:29:36
* @LastEditors: shirlwang
* @LastEditTime: 2025-11-03 16:29:26
-->
<template>
<view class="page" style="background-image: url('../../../packageRc/static/pageBg.png');">
<view class="page-header df_flex" style="display:flex;align-items:center;justify-content:space-between;padding:20rpx 0rpx;">
<u-icon class="back-icon" name="arrow-left" color="#fff" size="16" @click="goBack()"></u-icon>
<view class="title df_flex_1" style="padding-left: 32rpx;" >{{isAdd ? '需求新增' : '需求维护'}}</view>
<u-icon style="margin-right: 32rpx;" @tap="$store.commit('SET_SHOWEXITPOPUP', true)" name="list" size="44rpx" color="#fff"></u-icon>
</view>
<view class="tab-list" v-if="showTab != 1">
<view class="tab" :class="{active: activeType == 1}" @click="canChangeType ? changeType(1) : ''">求职<br>需求
</view>
<view class="tab" :class="{active: activeType == 3}" @click="canChangeType ? changeType(3) : ''">创业<br>需求
</view>
<view class="tab" :class="{active: activeType == 4}" @click="canChangeType ? changeType(4) : ''">培训<br>需求
</view>
<view class="tab" :class="{active: activeType == 5}" @click="canChangeType ? changeType(5) : ''">其他<br>需求
</view>
</view>
<jobService v-if="activeType == 1" :needId="id" :name="name" ref="type1" />
<entrepreneurshipService :needId="id" :name="name" v-if="activeType == 3" ref="type3" />
<trainService v-if="activeType == 4" :needId="id" :name="name" ref="type4" />
<otherService v-if="activeType == 5" :needId="id" :name="name" ref="type5" />
<!-- 社区端 - 显示隐藏退出组件 -->
<exitPopup />
</view>
</template>
<script>
import jobService from './components/jobService.vue';
import entrepreneurshipService from './components/entrepreneurshipService.vue';
import trainService from './components/trainService.vue';
import otherService from './components/otherService.vue';
export default {
components: {
jobService,
entrepreneurshipService,
trainService,
otherService,
},
data() {
return {
isAdd: true,
activeType: 1,
canChangeType: true,
id: '',
name:"",
}
},
onLoad(options) {
this.showTab = options.showTab
this.id = options.id
console.log("this.id",this.id)
this.name = options.name
if (options.id && options.type) {
this.isAdd = false
this.activeType = options.type
this.canChangeType = false;
this.$nextTick(() => {
this.$refs['type' + options.type].getDetail(options.id)
})
} else {
// 添加需求的时候根据传入的类型 判断对应的表单
this.changeType(options.activeType || 1)
}
},
methods: {
changeType(type) {
this.activeType = type
this.$nextTick(() => {
this.$refs['type' + type].addOne()
})
},
goBack() {
uni.navigateBack();
},
}
}
</script>
<style lang="scss">
.page {
background-color: #F4F4F4 !important;
height: 100vh;
background-repeat: no-repeat;
background-size: 100% auto;
}
.button-area {
padding: 24rpx 32rpx 68rpx;
width: 100%;
background: #fff;
display: flex;
box-sizing: border-box;
margin-top: 40rpx;
border-radius: 16px 16px 0px 0px;
.btn {
line-height: 72rpx;
width: 176rpx;
margin-right: 16rpx;
font-size: 28rpx;
border: 1px solid #B8C5D4;
color: #282828;
text-align: center;
border-radius: 8rpx;
}
.reset {
background: #DCE2E9;
}
.save {
background: linear-gradient(103deg, #1D64CF 0%, #1590D4 99%);
color: #fff;
border: 0;
flex-grow: 1;
}
}
.noValue {
color: rgb(192, 196, 204);
}
.disabledLine {
background: rgb(245, 247, 250);
cursor: not-allowed;
}
.tab-list {
display: flex;
width: calc(100% - 64rpx);
margin: 16rpx auto 30rpx;
text-align: center;
border-radius: 16rpx;
background: #fff;
;
.tab {
width: 25%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #FFFFFF;
background: #fff;
border-radius: 16rpx;
font-size: 28rpx;
color: #878787;
height: 106rpx;
&.active {
background: #1A62CE;
color: #fff;
position: relative;
font-weight: bold;
&::before {
content: '';
position: absolute;
bottom: -13rpx;
border-top: 14rpx solid #1A62CE;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
left: calc(50% - 7rpx);
z-index: 2;
}
&::after {
content: '';
position: absolute;
z-index: 1;
bottom: -18rpx;
border-top: 14rpx solid #fff;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
left: calc(50% - 7rpx);
}
}
}
}
</style>

View File

@@ -0,0 +1,332 @@
<!--
* @Date: 2025-10-16 15:15:47
* @LastEditors: lip
* @LastEditTime: 2025-11-04 11:50:05
-->
<template>
<!-- @scroll="handleScroll" @scrolltolower="scrollBottom" -->
<scroll-view :scroll-y="true" class="container" style="background-image: url('../../../packageRc/static/pageBg.png');">
<view style="padding: 40rpx 28rpx;">
<view class="kinggang">
<view>
<image src="../../../packageRc/static/kinggang1.png"/>
<view>信息维护</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang5.png"/>
<view>投递记录</view>
</view>
<view @click="tiao()">
<image src="../../../packageRc/static/kinggang2.png"/>
<view>需求上报</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang3.png"/>
<view>虚拟面试</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang4.png"/>
<view>素质测评</view>
</view>
</view>
<view class="tabs">
<view class="tab" :class="{active: pageState.type == ''}" @click="changeJobType('')">岗位列表</view>
<view class="tab" :class="{active: pageState.type == 2}" @click="changeJobType(2)">实习实训</view>
<view class="tab" :class="{active: pageState.type == 3}" @click="changeJobType(3)">社区实践</view>
</view>
<view class="titles">
<view class="title-item" :class="{active: activeTitle == 1}" @click="activeTitle = 1,getJobRecommed()"><view>推荐岗位</view></view>
<view class="title-item" :class="{active: activeTitle == 2}" @click="activeTitle = 2,getJobList()"><view>热门岗位</view></view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="job-list">
<view class="top-line">
<view class="salary">{{item.minSalary}}-{{item.maxSalary}}/</view>
<view class="time"><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期{{ item.postingDate }}</view>
</view>
<view class="title">{{ item.jobTitle }}</view>
<view class="infos">
<view>
<dict-Label dictType="education" :value="item.education"></dict-Label>
</view>
<view>
<dict-Label dictType="experience" :value="item.experience"></dict-Label>
</view>
<view>{{ item.jobLocation }}</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="person" size="12"></uni-icons>{{item.vacancies}}</view>
<view>{{ item.companyName }}</view>
</view>
</view>
<view class="view-more-btn" @click="viewMore">查看更多内容</view>
<view class="titles" style="justify-content: space-between;">
<view class="title-item active"><view>政策专区</view></view>
<view>{{'查看更多 >'}}</view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="job-list">
<view class="sign">推荐</view>
<view class="title">
<image src="../../../packageRc/static/zcLeft.png"/>
销售顾问</view>
<view class="infos">
<view>大专</view>
<view>1-3</view>
<view>喀什 市南区</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期2025-10-20</view>
<view>浏览数<text style="color: #6AA7E8">99+</text></view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
const { $api, navTo, vacanciesTo, formatTotal, config } = inject('globalFunction');
let activeTab = ref(1)
let activeTitle = ref(1)
function back() {
uni.navigateBack({
delta: 1
})
}
onMounted(()=>{
getJobRecommed()
})
function viewMore() {
// uni.navigateTo({
// url: '/pages/jobList/jobList'
// })
}
function tiao(){
console.log('尝试导航到待办详情页面');
// 尝试直接使用uni.navigateTo使用正确的格式
uni.navigateTo({
url: '/packageRc/pages/demand/index',
success: function() {
console.log('导航成功');
},
fail: function(err) {
console.error('导航失败:', err);
}
});
}
import useUserStore from '@/stores/useUserStore';
const conditionSearch = ref({});
const pageState = reactive({
page: 0,
total: 0,
maxPage: 2,
pageSize: 10,
search: {
order: 0,
},
type:'',
});
let jobList = ref([])
// 获取推荐岗位
function getJobRecommed(){
let params = {
pageSize: pageState.pageSize,
sessionId: useUserStore().seesionId,
...pageState.search,
...conditionSearch.value,
isPublish: 1,
type:pageState.type
};
$api.createRequest('/app/job/recommend', params).then((resData) => {
jobList.value = resData.data
pageState.total = 0;
});
}
// 获取岗位列表
function getJobList(){
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
...pageState.search,
// ...conditionSearch.value,
type:pageState.type
};
$api.createRequest('/app/job/list', params).then((resData) => {
jobList.value = resData.rows
pageState.total = 0;
});
}
// 更改实习实训等
function changeJobType(val){
pageState.type = val
if(activeTitle.value == 1){
getJobRecommed()
}else{
getJobList()
}
}
</script>
<style lang="less" scoped>
view{box-sizing: border-box;display: block;}
.container{
background-color: #F4F4F4;background-position: top center;background-size: 100% auto;
height: 100vh;
min-width: 100vw;
padding-bottom: 0;
background-repeat: no-repeat;
box-sizing: border-box;
}
.kinggang{
display: flex;
justify-content: space-around;
align-items: center;
padding: 16rpx 16rpx 32rpx 16rpx;
font-size: 24rpx;
background: #FFFFFF;
border-radius: 16rpx;
margin-bottom: 24rpx;
>view{
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
}
image{
width: 78rpx;
// margin-bottom: 15rpx;
height: 78rpx;
}
}
.tabs{
margin-bottom: 29rpx;
border-radius: 16rpx;
display: flex;
background: #fff;
color: #878787;
text-align: center;
width: 100%;
.tab{
width: 50%;
border: 4rpx solid transparent;
border-radius: 16rpx;
line-height: 64rpx;
position: relative;
&.active{
border: 4rpx solid #fff;
color: #fff;
background: linear-gradient(180deg, #79AFFF 1%, #A2B3FE 100%);
box-shadow: 0px 4rpx 10rpx 0px rgba(40, 102, 194, 0.4);
}
}
}
.titles{
display: flex;
margin-bottom: 44rpx;
.title-item{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-right: 32rpx;
position: relative;
>view{
position: relative;
z-index: 2;
padding: 0 16rpx;
}
&.active::after{
content: '';
position: absolute;
z-index: 1;
bottom: -12rpx;
left: 0;
width: 120%;
height: 24rpx;
border-radius: 50px 0px 0px 50px;
background: linear-gradient(90deg, #78AEFF 0%, rgba(120, 174, 255, 0.31) 52%, rgba(24, 116, 255, 0) 100%);
}
}
}
.job-list{
width: 100%;
margin: 0 auto;
color: #333333;
border-radius: 24rpx;
background: #FFFFFF;
padding: 32rpx;
margin-bottom: 24rpx;
position: relative;
.sign{
position: absolute;
font-size: 24rpx;
right: 0;
top: 0;
padding: 4rpx 14rpx;
border: 1rpx solid #EC4827;
background: rgba(227, 79, 49, 0.09);
border-top-right-radius: 24rpx;
border-bottom-left-radius: 24rpx;
color: #EC4827;
}
.top-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-bottom: 16rpx;
.salary{
font-size: 32rpx;
color: #4C6EFB;
font-weight: bold;
}
}
.title{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-bottom: 16rpx;
display: flex;
image{
width: 46rpx;
height: 46rpx;
margin-right: 11rpx;
}
}
.infos{
display: flex;
flex-wrap: wrap;
font-size: 24rpx;
margin-bottom: 16rpx;
line-height: 42rpx;
view{
padding: 0 16rpx;
margin-right: 10rpx;
background: #F2F2F2;
}
}
.bottom-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-top: 12rpx;
}
}
.view-more-btn{
padding: 10rpx 56rpx;
background: #FFFFFF;
color: #4C6EFB;
border: 1rpx solid #4C6EFB;
text-align: center;
border-radius: 40rpx;
width: fit-content;
margin: 0 auto;
margin-bottom: 20rpx;
}
</style>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Some files were not shown because too many files have changed in this diff Show More