101 Commits

Author SHA1 Message Date
Apcallover
0d5e3024bc flat:合并冲突 2025-12-03 11:08:53 +08:00
Apcallover
268648868f flat: 合并 2025-12-03 11:04:38 +08:00
Apcallover
c5955959c5 flat: ces 2025-12-03 11:01:58 +08:00
Apcallover
16b8ca84cd flat: 性能优化,animation 等\preload等 2025-12-01 20:29:19 +08:00
Apcallover
ecfacd13e3 flat: 优化2 2025-11-30 17:14:41 +08:00
Apcallover
9a38bbd298 flat: 优化 2025-11-30 16:47:06 +08:00
Apcallover
8cf55d3925 flat: 性能优化 2025-11-30 14:26:36 +08:00
Apcallover
0dec1618fa flat: 优化,还是使用原生Tabbar,empty优化 2025-11-30 14:08:16 +08:00
Apcallover
63d0cdb5ad flat: 性能优化,招聘会时间筛选性能优化、精选企业性能优化,封装缓存request、indexDb方法,主要用于不常更新的接口,以达到毫秒级性能 2025-11-29 16:31:34 +08:00
Apcallover
636818361c flat:暂存 2025-11-29 11:48:05 +08:00
Apcallover
23a2b84b4a 合并性能优化缓存备份分支 2025-11-28 19:54:18 +08:00
Apcallover
d84fd90a11 flat: 缓存 2025-11-28 19:47:42 +08:00
7ce14fa7e2 日期选择样式优化 2025-11-28 18:17:44 +08:00
e5afbcedb1 Merge branch 'main' of http://124.243.245.42:3000/sdz/qingdao-employment-service 2025-11-28 18:17:23 +08:00
78661c12af 日期选择样式优化 2025-11-28 18:17:22 +08:00
Apcallover
550173c82d flat: 修改错别字 2025-11-28 17:53:26 +08:00
b53d8196b4 竞争力分析超过3个才显示, 简历完成度加入工作经历 2025-11-28 17:49:05 +08:00
983405cabe Merge branch 'main' of http://124.243.245.42:3000/sdz/qingdao-employment-service 2025-11-28 17:22:37 +08:00
b447026f99 岗位推荐固定插入改为随机插入 2025-11-28 17:22:36 +08:00
9a5bffae85 岗位推荐固定位置插入改为随机插入 2025-11-28 17:22:05 +08:00
Apcallover
6eb0767a88 flatL暂存 2025-11-28 14:33:08 +08:00
Apcallover
dfd79646d6 flat: 过期状态 2025-11-27 21:45:15 +08:00
Apcallover
4563fa90af flat: 优化搜索 2025-11-27 21:26:32 +08:00
51e67a8c8f fix 2025-11-27 11:46:59 +08:00
531681b74e fix 2025-11-27 11:20:45 +08:00
7e0ec650b1 fix 2025-11-27 10:59:39 +08:00
3a5d8ccb1a fix 2025-11-27 10:56:46 +08:00
34bad16bf4 fix 2025-11-27 10:41:05 +08:00
f5099e9cc0 fix pixi 2025-11-27 10:22:45 +08:00
4295199887 static 2025-11-27 10:03:51 +08:00
Apcallover
7322e0854e Merge branch 'main' into bin 2025-11-27 09:08:22 +08:00
Apcallover
b6588d421f flat: 更改后面 2025-11-26 21:14:25 +08:00
Apcallover
0172f47628 flat: 体验优化,swiper优化,添加uploadfile Class方法 2025-11-26 21:11:12 +08:00
Apcallover
d260e24265 flat: 修复语法错误 2025-11-25 16:23:00 +08:00
99a3fe41c5 feat 对接外部数据投递功能 2025-11-25 16:20:34 +08:00
Apcallover
fe6fe43636 flat: login点 2025-11-24 22:48:02 +08:00
Apcallover
a4233b03d7 flat: 对接经纬度2 2025-11-24 18:41:52 +08:00
Apcallover
378f71f3c7 flat: 对接经纬度 2025-11-24 18:40:59 +08:00
Apcallover
c75653b7a3 flat: 备份 2025-11-24 15:06:15 +08:00
Apcallover
9f92fc47cb flat: 暂存 2025-11-24 14:33:23 +08:00
6af1a5def7 style 2025-11-24 09:47:03 +08:00
ca47a45d33 fix 内部数据收藏传参bug 2025-11-24 09:38:56 +08:00
xiebin
abd91e2cb7 分类渲染数据类型 : 岗位详情 公司详情 岗位收藏 公司收藏 浏览记录 预约列表 2025-11-23 18:20:28 +08:00
xiebin
06fb492cbd 修改公司详情接口地址 2025-11-22 19:23:20 +08:00
xiebin
c01abfdd64 Merge branch 'main' of http://124.243.245.42:3000/sdz/qingdao-employment-service 2025-11-22 18:53:53 +08:00
xiebin
99f02927ac 对接招聘会接口 2025-11-22 18:53:52 +08:00
Apcallover
f515d07d2a Merge branch 'copy' 2025-11-22 17:12:19 +08:00
Apcallover
42d0451869 flat:切换应用 2025-11-22 17:11:11 +08:00
xiebin
4b8056b716 fix : 雷达图渲染 2025-11-21 15:41:47 +08:00
xiebin
a3d592eb02 remove : 职位详情-竞争力分析-雷达图的技能项 2025-11-21 15:34:38 +08:00
xiebin
805b384958 fix 2025-11-21 15:05:32 +08:00
xiebin
41196466af 隐藏电子名片没有的字段 2025-11-21 14:50:59 +08:00
Apcallover
77f97892bc flat: 合并 2025-11-21 10:47:43 +08:00
Apcallover
20191c9454 flat: pixjs 2025-11-21 10:41:34 +08:00
Apcallover
97a5c34e70 flat:tabbar 2025-11-21 09:39:53 +08:00
6024ae44a4 feat : 每次进入简历详情页,刷新简历信息 2025-11-20 18:58:40 +08:00
ab63143792 feat : 新增上传简历功能 2025-11-20 18:49:53 +08:00
Apcallover
29fe2aff0e flat: 暂存 2025-11-20 18:14:36 +08:00
Apcallover
90591289d0 flat: 更好aes.js 2025-11-20 17:20:55 +08:00
Apcallover
e5c5902322 flat: 合并 2025-11-20 16:34:00 +08:00
Apcallover
f1b18203ae flat:合并 2025-11-20 15:56:45 +08:00
fc2d0f90ec feat : 视频提示遮罩的显示.复用之前判断逻辑 2025-11-20 15:35:48 +08:00
6b20c045a9 style 简历匹配职位的点击emit 2025-11-20 14:30:01 +08:00
183c71da3c style 2025-11-20 14:12:42 +08:00
5e2f8ac169 style 2025-11-20 14:10:00 +08:00
bca67b7f25 style 2025-11-20 13:51:34 +08:00
83a1078a4d Merge remote-tracking branch 'origin/main' into bin 2025-11-20 13:50:49 +08:00
e4d100242b refactor : 重构首页的简历匹配职位 2025-11-20 13:50:09 +08:00
Apcallover
5497398498 flat: ai对话提交 2025-11-20 09:17:29 +08:00
Apcallover
3f49c11caf flat: 暂存 2025-11-19 16:44:47 +08:00
Apcallover
044b88dbf7 flat: 登陆对接,等待加密 2025-11-18 21:55:38 +08:00
Apcallover
ca4b038e14 flat: 登陆对接 2025-11-18 20:38:05 +08:00
Apcallover
e67c53404b flat: 暂存 2025-11-18 19:53:55 +08:00
Apcallover
60a0448aa7 flat: 暂存 2025-11-18 19:43:15 +08:00
Apcallover
d2e77e66fc flat: 暂存 2025-11-18 17:25:39 +08:00
3eca164bde refactor : 重构首页样式 2025-11-18 15:24:35 +08:00
Apcallover
ab3d9985c8 flat: 部署 2025-11-18 13:57:07 +08:00
a7d6b8709c 首页重构列表已完成 2025-11-17 15:03:20 +08:00
ca7273f152 style : 样式优化 2025-11-13 10:57:01 +08:00
6e09702db5 简历相关 2025-11-11 16:37:00 +08:00
ec477fe7c1 Merge branch 'main' of http://124.243.245.42:3000/sdz/qingdao-employment-service 2025-11-11 15:10:41 +08:00
fa267c9796 feat : 新增编辑,添加,删除工作经历功能, 修改我的简历页,
perf : 全局navTo方法新增延迟(更好的表现动画)
2025-11-11 15:10:39 +08:00
Apcallover
a03b54a406 Clean up tracked files based on .gitignore2 2025-11-11 14:13:25 +08:00
Apcallover
6a3f84c4f4 Clean up tracked files based on .gitignore 2025-11-11 14:12:43 +08:00
Apcallover
9d4a7f1172 feat: Add or update .gitignore rules 2025-11-11 14:11:47 +08:00
e19230dae5 fix 文案错误 2025-11-10 18:04:01 +08:00
bin
544cae7cb1 feat : 新增 点子名片页, 修改我的简历页, 编辑个人信息页 2025-11-10 17:09:05 +08:00
Apcallover
e12241b0e4 flat: 暂存 2025-11-07 11:30:07 +08:00
史典卓
7d2faa6c1b flat:update 未知 2025-09-23 14:56:30 +08:00
史典卓
07b2aa5f80 flat: 修改 2025-08-20 13:38:47 +08:00
史典卓
58c36c01a0 flat:语音功能优化 2025-07-22 15:20:21 +08:00
史典卓
ea04387b58 flat: 修复bug 2025-07-21 14:49:45 +08:00
史典卓
ec2dc5f659 flat: 添加微信分享卡片 2025-07-14 15:38:39 +08:00
史典卓
645c2552f6 flat: 修改字体和添加图标 2025-07-09 15:15:37 +08:00
史典卓
36798d3054 flat: 视频版本0.1 2025-06-26 08:56:42 +08:00
史典卓
857dedad01 flat: 暂存 2025-06-20 22:03:24 +08:00
史典卓
d97a712fd1 flat:6.20添加视频板块存档 2025-06-20 10:10:46 +08:00
史典卓
b7b43c0b42 flat:优化视频播放显示 2025-06-10 10:50:25 +08:00
史典卓
02c3c7366b flat:显示性能优化 2025-06-10 09:36:04 +08:00
史典卓
9f47ea0e53 flat: 删除无用js和插件 2025-05-19 15:31:49 +08:00
Apcallover
6c478a9d0b !1 重新修改后提交
Merge pull request !1 from Apcallover/ycode
2025-05-19 04:22:04 +00:00
176 changed files with 13312 additions and 3911 deletions

BIN
.DS_Store vendored

Binary file not shown.

21
.gitignore vendored
View File

@@ -1 +1,22 @@
# 编译/打包输出目录
/unpackage/
# 依赖包目录
/node_modules/
# IDE/编辑器配置
.vscode/
.idea/
# macOS 系统文件
.DS_Store
# Windows 系统文件
Thumbs.db
# 日志文件
npm-debug.log
yarn-debug.log
# HBuilderX 运行时生成的文件
.hbuilderx

View File

@@ -1,16 +0,0 @@
{ // launch.json 配置了启动调试时相关设置configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

176
App.vue
View File

@@ -2,44 +2,37 @@
import { reactive, inject, onMounted } from 'vue';
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
import useUserStore from './stores/useUserStore';
import usePageAnimation from './hook/usePageAnimation';
import useDictStore from './stores/useDictStore';
const { $api, navTo, appendScriptTagElement } = inject('globalFunction');
const { $api, navTo, appendScriptTagElement, aes_Decrypt, sm2_Decrypt } = inject('globalFunction');
import config from '@/config.js';
usePageAnimation();
const appword = 'aKd20dbGdFvmuwrt'; // 固定值
onLaunch((options) => {
// uni.hideTabBar();
useDictStore().getDictData();
// uni.onTabBarMidButtonTap(() => {
// uni.navigateTo({
// url: '/pages/chat/chat',
// });
// });
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
try {
getUserInfo();
} catch {
console.log('不是爱山东平台,使用测试登陆');
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
}
}
});
onMounted(() => {
// #ifndef MP-WEIXIN
if (process.env.NODE_ENV === 'development') {
appendScriptTagElement('./static/js/jweixin-1.4.0.js').then(() => {
console.log('✅ 微信 JSSDK 加载完成');
});
} else {
appendScriptTagElement('/static/js/jweixin-1.4.0.js').then(() => {
console.log('✅ 微信 JSSDK 加载完成');
});
}
// #endif
});
onMounted(() => {});
onShow(() => {
console.log('App Show');
@@ -48,16 +41,106 @@ onShow(() => {
onHide(() => {
console.log('App Hide');
});
function getUserInfo() {
lightAppJssdk.user.getUserInfoWithEncryptedParamByAppId({
appId: config.appInfo.loveShandong, // 接入方在成功创建应用后自动生成
success: function (data) {
if (data == '未登录') onLoginApp();
else {
if (typeof data == 'string') data = JSON.parse(data);
const sm2_privateKey = config.appInfo.sm2PrivateKey;
let sm2_encrypt_result = data.data;
let sm2_decrypt_result = sm2_Decrypt(sm2_encrypt_result, sm2_privateKey);
if (typeof sm2_decrypt_result == 'string') sm2_decrypt_result = JSON.parse(sm2_decrypt_result);
// 其次,对sm2解密后的结果进行 aes解密
// aes解密需要用到 appword , 为固定值,使用示例代码中的即可
let aes_encrypt_result = sm2_decrypt_result.data;
let aes_decrypt_result = aes_Decrypt(aes_encrypt_result, appword);
// 加密
loginCallback(aes_decrypt_result);
}
},
fail: function (data) {
console.log('err', data);
},
});
}
/**
* 使用jssdk调用登录页面
*/
function onLoginApp() {
lightAppJssdk.user.loginapp({
success: function (data) {
if (data == '未登录') {
//取消登录或登录失败,关闭页面
oncloseWindow();
} else {
getUserInfo();
}
},
fail: function (data) {
//关闭页面
oncloseWindow();
},
});
}
/**
* 关闭容器
*/
function oncloseWindow() {
lightAppJssdk.navigation.close({
success: function (data) {},
fail: function (data) {},
});
}
function loginCallback(userInfo) {
let params = {
userInfo,
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
useUserStore()
.loginSetToken(resData.token)
.then((resume) => {
if (resume.data.jobTitleId) {
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
}
});
});
}
</script>
<style>
/*每个页面公共css */
@import '@/common/animation.css';
@import '@/common/common.css';
/* 修改pages tabbar样式 H5有效 */
/* 修改pages tabbar样式 H5才有效 */
.uni-tabbar .uni-tabbar__item:nth-child(4) .uni-tabbar__bd .uni-tabbar__icon {
height: 110rpx !important;
width: 122rpx !important;
margin-top: 6rpx;
width: 108rpx !important;
height: 98rpx !important;
margin-top: 0rpx;
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform-origin: center center;
/* transition: transform 0.15s ease-in-out; */
/* transform-origin: center center; */
}
.uni-tabbar .uni-tabbar__item:nth-child(4) .uni-tabbar__bd .uni-tabbar__icon:active {
transform: scale(0.8);
transition: transform 0.1s ease-out;
/* animation: jelly 0.5s; */
}
.uni-tabbar-border {
@@ -77,6 +160,29 @@ uni-modal,
@font-face {
font-family: DingTalk JinBuTi;
src: url('@/static/font/DingTalk JinBuTi_min.ttf');
src: url('/static/font/DingTalk JinBuTi_min.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: PingFangSC-Regular;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Regular.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: PingFangSC-Medium;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Medium.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: DIN-Medium;
src: url('https://qd.zhaopinzao8dian.com/file/csn/DIN-Medium.woff2') format('woff2');
font-display: swap;
}
body {
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
</style>

BIN
common/.DS_Store vendored

Binary file not shown.

227
common/animation.css Normal file
View File

@@ -0,0 +1,227 @@
/*base code*/
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
/*the animation definition*/
@-webkit-keyframes tada {
0% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
10%,
20% {
-webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg)
}
30%,
50%,
70%,
90% {
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg)
}
40%,
60%,
80% {
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg)
}
100% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
}
@keyframes tada {
0% {
-webkit-transform: scale3d(1, 1, 1);
-ms-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
10%,
20% {
-webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
-ms-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg)
}
30%,
50%,
70%,
90% {
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
-ms-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg)
}
40%,
60%,
80% {
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
-ms-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg)
}
100% {
-webkit-transform: scale3d(1, 1, 1);
-ms-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
}
.tada {
-webkit-animation-name: tada;
animation-name: tada
}
.btn-tada:active {
-webkit-animation-name: tada;
animation-name: tada
}
/*the animation definition*/
@-webkit-keyframes rubberBand {
0% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
30% {
-webkit-transform: scale3d(1.25, .75, 1);
transform: scale3d(1.25, .75, 1)
}
40% {
-webkit-transform: scale3d(0.75, 1.25, 1);
transform: scale3d(0.75, 1.25, 1)
}
50% {
-webkit-transform: scale3d(1.15, .85, 1);
transform: scale3d(1.15, .85, 1)
}
65% {
-webkit-transform: scale3d(.95, 1.05, 1);
transform: scale3d(.95, 1.05, 1)
}
75% {
-webkit-transform: scale3d(1.05, .95, 1);
transform: scale3d(1.05, .95, 1)
}
100% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
}
@keyframes rubberBand {
0% {
-webkit-transform: scale3d(1, 1, 1);
-ms-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
30% {
-webkit-transform: scale3d(1.25, .75, 1);
-ms-transform: scale3d(1.25, .75, 1);
transform: scale3d(1.25, .75, 1)
}
40% {
-webkit-transform: scale3d(0.75, 1.25, 1);
-ms-transform: scale3d(0.75, 1.25, 1);
transform: scale3d(0.75, 1.25, 1)
}
50% {
-webkit-transform: scale3d(1.15, .85, 1);
-ms-transform: scale3d(1.15, .85, 1);
transform: scale3d(1.15, .85, 1)
}
65% {
-webkit-transform: scale3d(.95, 1.05, 1);
-ms-transform: scale3d(.95, 1.05, 1);
transform: scale3d(.95, 1.05, 1)
}
75% {
-webkit-transform: scale3d(1.05, .95, 1);
-ms-transform: scale3d(1.05, .95, 1);
transform: scale3d(1.05, .95, 1)
}
100% {
-webkit-transform: scale3d(1, 1, 1);
-ms-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1)
}
}
.rubberBand {
-webkit-animation-name: rubberBand;
animation-name: rubberBand
}
.btn-rubberBand:active {
-webkit-animation-name: tada;
animation-name: tada
}
@keyframes jelly {
0% {
transform: scale(1);
}
30% {
transform: scale(1.25, 0.75);
}
/* 压扁 */
40% {
transform: scale(0.75, 1.25);
}
/* 拉长 */
50% {
transform: scale(1.15, 0.85);
}
/* 稍微压扁 */
65% {
transform: scale(0.95, 1.05);
}
/* 稍微拉长 */
75% {
transform: scale(1.05, 0.95);
}
100% {
transform: scale(1);
}
}

View File

@@ -56,6 +56,7 @@ html {
background-color: rgba(189, 197, 254, 0.15);
}
.btn-incline {
transition: transform 0.2s ease;
transform-style: preserve-3d;
@@ -66,7 +67,7 @@ html {
}
.btn-feel {
transition: transform 0.2s ease;
transition: transform 0.15s ease;
transform-style: preserve-3d;
}
@@ -74,6 +75,23 @@ html {
transform: perspective(600px) rotateX(6deg) scale(0.98);
}
.press-button {
padding: 10px 20px;
background: #3A4750;
/* 深灰蓝 */
color: #ffffff;
font-size: 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
/* box-shadow: 0 4px 0 #2C3E50; */
}
.press-button:active {
transform: scale(0.95) translateY(2px);
/* box-shadow: 0 2px 0 #1C2833; */
}
/* 动画效果 */
.btn-shaky:active {
@@ -446,4 +464,12 @@ html {
/* 隐藏超出的文本 */
text-overflow: ellipsis;
/* 使用省略号 */
}
.grayscale {
filter: grayscale(100%) opacity(0.6);
}
.height-100 {
height: 100%;
}

View File

@@ -1,6 +1,7 @@
import '@/lib/encryption/sm4.min.js'
import useUserStore from "../stores/useUserStore";
import {
request,
createRequestWithCache,
createRequest,
uploadFile
} from "../utils/request";
@@ -51,6 +52,7 @@ const prePage = () => {
/**
* 页面跳转封装,支持 query 参数传递和返回回调
* @param {string} url - 跳转路径
@@ -59,17 +61,22 @@ const prePage = () => {
* @param {object} options.query - 携带参数
* @param {function} options.onBack - 页面返回时的回调(目标页调用 uni.navigateBack 时传递数据)
*/
let isJumping = false
export const navTo = function(url, {
needLogin = false,
query = {},
onBack = null
} = {}) {
const userStore = useUserStore();
if (isJumping) return
isJumping = true
if (needLogin && !userStore.hasLogin) {
uni.navigateTo({
url: '/pages/login/login'
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/login/login'
});
isJumping = false
}, 170);
return;
}
@@ -84,9 +91,12 @@ export const navTo = function(url, {
currentPage.__onBackCallback__ = onBack;
}
uni.navigateTo({
url: finalUrl
});
setTimeout(() => {
uni.navigateTo({
url: finalUrl
});
isJumping = false
}, 170);
};
export const navBack = function({
@@ -539,11 +549,73 @@ function isInWechatMiniProgramWebview() {
return ua.includes('miniprogram') || window.__wxjs_environment === 'miniprogram'
}
function isEmptyObject(obj) {
return obj && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 0;
}
function aes_Decrypt(word, key) {
var key = CryptoJS.enc.Utf8.parse(key) //转为128bit
var srcs = CryptoJS.enc.Hex.parse(word) //转为16进制
var str = CryptoJS.enc.Base64.stringify(srcs) //变为Base64编码的字符串
var decrypt = CryptoJS.AES.decrypt(str, key, {
mode: CryptoJS.mode.ECB,
spadding: CryptoJS.pad.Pkcs7
})
return decrypt.toString(CryptoJS.enc.Utf8)
}
export function sm2_Decrypt(word, key) {
return SM.decrypt(word, key);
}
export function sm2_Encrypt(word, key) {
return SM.encrypt(word, key);
}
export function sm4Decrypt(key, value, mode = "hex") {
try {
if (key.length !== 32) {
alert('密钥必须是32位16进制字符串128位');
return;
}
const decrypted = sm4.decrypt(value, key, {
mode: 'ecb',
cipherType: mode === 'hex' ? 'hex' : 'base64',
padding: 'pkcs#5'
});
return decrypted
} catch (e) {
console.log('解密失败', e)
}
}
export function sm4Encrypt(key, value, mode = "hex") {
try {
if (key.length !== 32) {
alert('密钥必须是32位16进制字符串128位');
return;
}
const encrypted = sm4.encrypt(value, key, {
mode: 'ecb',
cipherType: mode === 'hex' ? 'hex' : 'base64',
padding: 'pkcs#5'
});
return encrypted
} catch (e) {
console.log('加密失败')
}
}
export const $api = {
msg,
prePage,
sleep,
request,
createRequest,
streamRequest,
chatRequest,
@@ -551,7 +623,9 @@ export const $api = {
uploadFile,
formatFileSize,
sendingMiniProgramMessage,
copyText
copyText,
aes_Decrypt,
createRequestWithCache
}
@@ -579,5 +653,10 @@ export default {
parseQueryParams,
appendScriptTagElement,
insertSortData,
isInWechatMiniProgramWebview
isInWechatMiniProgramWebview,
isEmptyObject,
sm4Decrypt,
aes_Decrypt,
sm2_Decrypt,
sm2_Encrypt
}

View File

@@ -1 +0,0 @@
const _count=Symbol("count");const _lowestCount=Symbol("lowestCount");const _items=Symbol("items");class Queue{constructor(){this[_count]=0;this[_lowestCount]=0;this[_items]={}}enqueue(element){this[_items][this[_count]]=element;this[_count]++}dequeue(){if(this.isEmpty())return undefined;const result=this[_items][this[_lowestCount]];delete this[_items][this[_lowestCount]];this[_lowestCount]++;return result}peek(){return this.isEmpty()?undefined:this[_items][this[_lowestCount]]}isEmpty(){return this[_count]-this[_lowestCount]===0}size(){return this[_count]-this[_lowestCount]}clear(){this[_count]=0;this[_lowestCount]=0;this[_items]={}}toString(){return Object.values(this[_items]).join(",")}}Object.freeze(Queue.prototype);const _dequeItems=Symbol("dequeItems");class Deque{constructor(){this[_items]={};this[_lowestCount]=0;this[_count]=0}addFront(element){if(this.isEmpty()){this.addBack(element)}else if(this[_lowestCount]>0){this[_lowestCount]--;this[_items][this[_lowestCount]]=element}else{for(let i=this[_count];i>0;i--){this[_items][i]=this[_items][i-1]}this[_items][0]=element;this[_count]++}}addBack(element){this[_items][this[_count]]=element;this[_count]++}removeFront(){if(this.isEmpty())return undefined;const result=this[_items][this[_lowestCount]];delete this[_items][this[_lowestCount]];this[_lowestCount]++;return result}removeBack(){if(this.isEmpty())return undefined;this[_count]--;const result=this[_items][this[_count]];delete this[_items][this[_count]];return result}peekFront(){return this.isEmpty()?undefined:this[_items][this[_lowestCount]]}peekBack(){return this.isEmpty()?undefined:this[_items][this[_count]-1]}isEmpty(){return this[_count]-this[_lowestCount]===0}size(){return this[_count]-this[_lowestCount]}clear(){this[_items]={};this[_lowestCount]=0;this[_count]=0}toString(){return Object.values(this[_items]).join(",")}}Object.freeze(Deque.prototype);export{Queue,Deque};

BIN
components/.DS_Store vendored

Binary file not shown.

View File

@@ -12,7 +12,7 @@
<view class="header-btnLf">
<slot name="headerleft"></slot>
</view>
<view class="header-title">
<view class="header-title" :style="{ color: titleColor }">
<view>{{ title }}</view>
<view v-show="subTitle" class="subtitle-text">{{ subTitle }}</view>
</view>
@@ -45,12 +45,15 @@
<script setup>
import img from '@/static/icon/background2.png';
const emit = defineEmits(['onScrollBottom']);
defineProps({
title: {
type: String,
default: '标题',
},
titleColor: {
type: String,
default: '#333333',
},
border: {
type: Boolean,
default: false,
@@ -110,9 +113,13 @@ const handleScrollToLower = () => {
align-items: center;
padding: 7rpx 3rpx;
.header-title {
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei',
sans-serif;
color: #000000;
font-weight: bold;
.subtitle-text {
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial,
sans-serif;
font-weight: 400;
font-size: 28rpx;
color: #333333;

View File

@@ -0,0 +1,327 @@
<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="(year, index) in years"
:key="index"
class="item"
:class="{ 'item-active': selectedIndex[0] === index }"
>
<text>{{ year }}</text>
<text></text>
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(month, index) in months"
:key="index"
class="item"
:class="{ 'item-active': selectedIndex[1] === index }"
>
<text>{{ month }}</text>
<text></text>
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(day, index) in days"
:key="index"
class="item"
:class="{ 'item-active': selectedIndex[2] === index }"
>
<text>{{ day }}</text>
<text></text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: 'datePicker',
data() {
return {
maskClick: false,
title: '选择日期',
confirmCallback: null,
cancelCallback: null,
changeCallback: null,
selectedIndex: [0, 0, 0],
selectedDate: '',
// 日期数据
years: [],
months: [],
days: [],
// 配置
startYear: 0,
endYear: 0,
};
},
created() {
this.initDateData();
},
methods: {
// 初始化日期数据
initDateData() {
const currentYear = new Date().getFullYear();
this.startYear = currentYear - 50; // 往前50年
this.endYear = currentYear + 10; // 往后10年
// 生成年份
this.years = [];
for (let i = this.startYear; i <= this.endYear; i++) {
this.years.push(i);
}
// 生成月份
this.months = [];
for (let i = 1; i <= 12; i++) {
this.months.push(i);
}
// 初始天数(默认当前年月)
this.updateDays(this.years[0], this.months[0]);
},
// 根据年月更新天数
updateDays(year, month) {
const daysInMonth = new Date(year, month, 0).getDate();
this.days = [];
for (let i = 1; i <= daysInMonth; i++) {
this.days.push(i);
}
},
open(config = {}) {
const {
title = '选择日期',
success,
cancel,
change,
maskClick = false,
defaultDate = '',
} = config;
this.reset();
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;
// 设置默认选中
this.setDefaultDate(defaultDate);
this.$nextTick(() => {
this.$refs.popup.open();
});
},
close() {
this.$refs.popup.close();
},
// 设置默认日期
setDefaultDate(dateStr) {
if (!dateStr) {
// 没有默认日期,使用当前日期
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
this.selectedIndex = [
this.years.findIndex(y => y === year),
this.months.findIndex(m => m === month),
this.days.findIndex(d => d === day)
];
} else {
// 解析日期字符串 (支持 YYYY-MM-DD 格式)
const [year, month, day] = dateStr.split('-').map(Number);
this.selectedIndex = [
this.years.findIndex(y => y === year),
this.months.findIndex(m => m === month),
this.days.findIndex(d => d === day)
];
}
// 确保索引有效
this.selectedIndex = this.selectedIndex.map((index, i) =>
index === -1 ? 0 : index
);
this.updateSelectedDate();
},
bindChange(e) {
this.selectedIndex = e.detail.value;
// 检查是否需要更新天数
const oldDaysLength = this.days.length;
const selectedYear = this.years[this.selectedIndex[0]];
const selectedMonth = this.months[this.selectedIndex[1]];
this.updateDays(selectedYear, selectedMonth);
// 如果天数变化且当前选择的日期超过新月份的天数,调整日期索引
if (this.days.length !== oldDaysLength && this.selectedIndex[2] >= this.days.length) {
this.selectedIndex[2] = this.days.length - 1;
}
this.updateSelectedDate();
// 触发change回调
this.changeCallback && this.changeCallback(this.selectedDate, this.selectedIndex);
},
// 更新选中的日期字符串
updateSelectedDate() {
const year = this.years[this.selectedIndex[0]];
const month = this.months[this.selectedIndex[1]].toString().padStart(2, '0');
const day = this.days[this.selectedIndex[2]].toString().padStart(2, '0');
this.selectedDate = `${year}-${month}-${day}`;
},
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.selectedDate, this.selectedIndex);
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];
this.selectedDate = '';
this.title = '选择日期';
},
// 设置日期范围
setDateRange(startYear, endYear) {
this.startYear = startYear;
this.endYear = endYear;
// 重新生成年份
this.years = [];
for (let i = this.startYear; i <= this.endYear; i++) {
this.years.push(i);
}
// 重置选中索引
this.selectedIndex[0] = 0;
this.updateDays(this.years[0], this.months[0]);
},
},
};
</script>
<style lang="scss" scoped>
.popup-content {
color: #000000;
height: 50vh;
}
.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(50vh - 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: 32rpx;
color: #cccccc;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.item-active {
color: #333333;
}
.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

@@ -1,17 +1,37 @@
<template>
<view>{{ salaryText }}</view>
<view>
<view v-if="!minSalary || !maxSalary">面议</view>
<view v-else class="texts">
<text class="num">{{ minSalary / 1000 }}</text>
<text class="unit">k</text>
<text class="gap">~</text>
<text class="num">{{ maxSalary / 1000 }}</text>
<text class="unit">k</text>
</view>
</view>
</template>
<script setup>
import { inject, computed } from 'vue';
import useDictStore from '../../stores/useDictStore';
const { minSalary, maxSalary, isMonth } = defineProps(['minSalary', 'maxSalary', 'isMonth']);
const salaryText = computed(() => {
if (!minSalary || !maxSalary) return '面议';
if (isMonth) {
return `${minSalary}-${maxSalary}/月`;
}
return `${minSalary / 1000}k-${maxSalary / 1000}k`;
});
import { inject, computed } from "vue";
import useDictStore from "../../stores/useDictStore";
const { minSalary, maxSalary } = defineProps(["minSalary", "maxSalary"]);
</script>
<style lang="scss" scoped>
.texts{
letter-spacing: 1rpx;
}
.num{
font-size: 32rpx;
font-weight: 500;
}
.unit{
font-size: 24rpx;
font-weight: 500;
}
.gap{
font-size: 32rpx;
font-weight: 500;
margin-left: 5rpx;
}
</style>

View File

@@ -0,0 +1,312 @@
<template>
<swiper
class="m-tiktok-video-swiper"
circular
@change="swiperChange"
:current="state.current"
:vertical="true"
duration="300"
>
<swiper-item v-for="(item, index) in state.displaySwiperList" :key="index">
<view class="swiper-item" @click="(e) => handleClick(index, e)">
<video
:id="`video__${index}`"
:controls="controls"
:autoplay="false"
:loop="loop"
@ended="ended"
@controlstoggle="controlstoggle"
@play="onPlay"
@error="emits('error')"
class="m-tiktok-video-player"
:src="item.src || item.explainUrl"
v-if="index === 0 || !state.isFirstLoad"
></video>
<view class="cover-triangle" v-if="pause"></view>
<image
v-if="item.poster && state.displayIndex != index"
:src="item.poster"
class="m-tiktok-video-poster"
mode="aspectFit"
></image>
<slot :item="item"></slot>
</view>
</swiper-item>
</swiper>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, watch, nextTick } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
const _this = getCurrentInstance();
const emits = defineEmits(['play', 'error', 'loadMore', 'change', 'controlstoggle', 'click', 'ended']);
const lastTapTime = ref(0);
const pause = ref(false);
const props = defineProps({
/**
* 视频列表
*/
videoList: {
type: Array,
default: () => [],
},
/**
* 是否循环播放一个视频
*/
loop: {
type: Boolean,
default: true,
},
/**
* 显示原生控制栏
*/
controls: {
type: Boolean,
default: true,
},
/**
* 是否自动播放
*/
autoplay: {
type: Boolean,
default: true,
},
/**
* 是否自动滚动播放
*/
autoChange: {
type: Boolean,
default: false,
},
/**
* 滚动加载阈值(即播放到剩余多少个之后触发加载更多
*/
loadMoreOffsetCount: {
type: Number,
default: 2,
},
/**
* 暂停 1 单机暂停; 2 双击暂停; 0关闭
*/
pauseType: {
type: Number,
default: 2,
},
});
const state = reactive({
originList: [], // 源数据
displaySwiperList: [], // swiper需要的数据
displayIndex: 0, // 用于显示swiper的真正的下标数值只有012。
originIndex: 0, // 记录源数据的下标
current: 0,
oid: 0,
showControls: '',
toggleShow: true, // 显示面板
videoContexts: [],
isFirstLoad: true,
});
const initVideoContexts = () => {
state.videoContexts = [
uni.createVideoContext('video__0', _this),
uni.createVideoContext('video__1', _this),
uni.createVideoContext('video__2', _this),
];
};
const onPlay = (e) => {
emits('play', e);
};
const setVideoRef = (el, index) => {
if (el) {
videoRefs.value[index] = el;
}
};
function handleClick(index, e) {
const now = Date.now();
switch (props.pauseType) {
case 1:
if (pause.value) {
state.videoContexts[index].play();
pause.value = false;
} else {
state.videoContexts[index].pause();
pause.value = true;
}
break;
case 2:
if (now - lastTapTime.value < 300) {
if (pause.value) {
state.videoContexts[index].play();
pause.value = false;
} else {
state.videoContexts[index].pause();
pause.value = true;
}
}
break;
}
lastTapTime.value = now;
state.toggleShow = !state.toggleShow;
emits('click', e);
}
function ended() {
// 自动切换下一个视频
if (props.autoChange) {
if (state.displayIndex < 2) {
state.current = state.displayIndex + 1;
} else {
state.current = 0;
}
}
emits('ended');
}
/**
* 初始一个显示的swiper数据
* @originIndex 从源数据的哪个开始显示默认0如从其他页面跳转进来要显示第n个这个参数就是他的下标
*/
function initSwiperData(originIndex = state.originIndex) {
const originListLength = state.originList.length; // 源数据长度
const displayList = [];
displayList[state.displayIndex] = state.originList[originIndex];
displayList[state.displayIndex - 1 == -1 ? 2 : state.displayIndex - 1] =
state.originList[originIndex - 1 == -1 ? originListLength - 1 : originIndex - 1];
displayList[state.displayIndex + 1 == 3 ? 0 : state.displayIndex + 1] =
state.originList[originIndex + 1 == originListLength ? 0 : originIndex + 1];
state.displaySwiperList = displayList;
if (state.oid >= state.originList.length) {
state.oid = 0;
}
if (state.oid < 0) {
state.oid = state.originList.length - 1;
}
// 暂停所有视频
state.videoContexts.map((item) => item?.stop());
setTimeout(() => {
// 当前视频
if (props.autoplay) {
uni.createVideoContext(`video__${state.displayIndex}`, _this).play();
}
}, 500);
// 数据改变
emits('change', {
index: originIndex,
detail: state.originList[originIndex],
});
// 加载更多
var pCount = state.originList.length - props.loadMoreOffsetCount;
if (originIndex == pCount) {
emits('loadMore');
}
}
/**
* swiper滑动时候
*/
function swiperChange(event) {
const { current } = event.detail;
state.isFirstLoad = false;
const originListLength = state.originList.length; // 源数据长度
// 向后滚动
if (state.displayIndex - current == 2 || state.displayIndex - current == -1) {
state.originIndex = state.originIndex + 1 == originListLength ? 0 : state.originIndex + 1;
state.displayIndex = state.displayIndex + 1 == 3 ? 0 : state.displayIndex + 1;
state.oid = state.originIndex - 1;
initSwiperData(state.originIndex);
}
// 如果两者的差为-2或者1则是向前滑动
else if (state.displayIndex - current == -2 || state.displayIndex - current == 1) {
state.originIndex = state.originIndex - 1 == -1 ? originListLength - 1 : state.originIndex - 1;
state.displayIndex = state.displayIndex - 1 == -1 ? 2 : state.displayIndex - 1;
state.oid = state.originIndex + 1;
initSwiperData(state.originIndex);
}
state.toggleShow = true;
}
function controlstoggle(e) {
state.showControls = e.detail.show;
emits('controlstoggle', e);
}
watch(
() => props.videoList,
() => {
if (props.videoList?.length) {
state.originList = props.videoList;
if (state.isFirstLoad || !state.videoContexts?.length) {
initSwiperData();
initVideoContexts();
}
}
},
{
immediate: true,
}
);
let loadTimer = null;
onLoad(() => {
// 为了首次只加载一条视频(提高首次加载性能),延迟加载后续视频
loadTimer = setTimeout(() => {
state.isFirstLoad = false;
clearTimeout(loadTimer);
}, 5000);
});
onUnload(() => {
clearTimeout(loadTimer);
});
defineExpose({
initSwiperData,
});
</script>
<style lang="stylus" scoped>
.m-tiktok-video-swiper,
.m-tiktok-video-player {
width: 100%;
height: 100%;
background-color: #000;
}
.m-tiktok-video-swiper {
.swiper-item {
position: relative;
height: 100%;
}
.m-tiktok-video-poster {
background-color: #000;
position: absolute;
width: 100%;
height: 100%;
}
}
.cover-triangle{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
width: 132rpx
height: 132rpx
border-radius: 50%;
background: rgba(0,0,0,0.3)
}
.cover-triangle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-40%, -50%) rotate(90deg);
width: 0;
height: 0;
border-left: 40rpx solid transparent;
border-right: 40rpx solid transparent;
border-bottom: 60rpx solid #fff;
}
</style>

View File

@@ -1,7 +1,12 @@
<template>
<view class="empty" :style="{ background: bgcolor, marginTop: mrTop + 'rpx' }">
<view
class="empty"
:class="{ 'position-center': isPosition }"
:style="{ background: bgcolor, marginTop: mrTop + 'rpx' }"
>
<view class="ty_content" :style="{ paddingTop: pdTop + 'rpx' }">
<view class="content_top btn-shaky">
<view class="content_top">
<!-- <view class="content_top btn-shaky"> -->
<image v-if="pictrue" :src="pictrue" mode=""></image>
<image v-else src="@/static/icon/empty.png" mode=""></image>
</view>
@@ -29,18 +34,23 @@ export default {
pdTop: {
type: String,
required: false,
default: '80',
default: '0',
},
mrTop: {
type: String,
required: false,
default: '20',
default: '0',
},
pictrue: {
type: String,
required: false,
default: '',
},
isPosition: {
type: Boolean,
required: false,
default: false,
},
},
methods: {},
};
@@ -51,19 +61,33 @@ image {
width: 100%;
height: 100%;
}
.position-center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.empty {
width: 100%;
min-height: 100vh;
position: relative;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// min-height: 100vh;
// height: 400rpx;
// position: relative;
.ty_content {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, 0);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// position: absolute;
// left: 50%;
// top: 0;
// transform: translate(-50%, 0);
.content_top {
width: 450rpx;
height: 322rpx;

View File

@@ -24,11 +24,13 @@ const renderedHtml = computed(() => parseMarkdown(props.content));
const handleItemClick = (e) => {
let { attrs } = e.detail.node;
console.log(attrs);
let { 'data-copy-index': codeDataIndex, 'data-job-id': jobId, class: className } = attrs;
switch (className) {
case 'custom-card':
navTo('/packageA/pages/post/post?jobId=' + jobId);
return;
return navTo('/packageA/pages/post/post?jobId=' + jobId);
case 'custom-more':
return navTo('/packageA/pages/moreJobs/moreJobs?jobId=' + jobId);
case 'copy-btn':
uni.setClipboardData({
data: codeDataList[codeDataIndex],
@@ -40,6 +42,7 @@ const handleItemClick = (e) => {
});
},
});
break;
}
};
</script>
@@ -258,6 +261,19 @@ ol {
</style>
<style lang="stylus">
.custom-more{
display: flex
justify-content: flex-end
color: #256BFA
padding-top: 5rpx
padding-bottom: 14rpx
.more-icon{
width: 60rpx;
height: 40rpx;
background: url('@/static/svg/seemore.svg') center center no-repeat;
background-size: 100% 100%
}
}
.custom-card
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
@@ -276,11 +292,13 @@ ol {
align-items: center;
justify-content: space-between
.title-text
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
max-width: calc(100% - 160rpx);
overflow: hidden
text-overflow: ellipsis
font-size: 30rpx
.card-salary
font-family: DIN-Medium;
font-size: 28rpx;
color: #FF6E1C;

View File

@@ -0,0 +1,167 @@
<template>
<view v-for="company in listData" :key="company.id">
<view
v-if="company.dataType == 2"
:class="{ grayscale: company.isPublish }"
class="cards"
@click="nextDetail(company)"
>
<view class="card-company">
<text class="company line_1">{{ company.name }}</text>
</view>
<view class="card-bottom">
<view class="fl_box fs_14">
<view class="mar_ri10">{{ company.industry }}</view>
<view>{{ company.scale }}</view>
</view>
<view class="ris">
<text class="fs_14">
在招职位·
<text class="color_256BFA">{{ company.totalRecruitment || '-' }}</text>
</text>
</view>
</view>
<view class="card-tags">
<view class="tag" v-if="company.nature">
{{ company.nature }}
</view>
</view>
</view>
<view v-else class="cards" :class="{ grayscale: company.isPublish }" @click="nextDetail(company)">
<view class="card-company">
<text class="company line_1">{{ company.name }}</text>
</view>
<view class="card-bottom">
<view class="fl_box fs_14">
<view class="mar_ri10">{{ company.industry }}</view>
<view>{{ company.scale }}</view>
</view>
<view class="ris">
<text class="fs_14">
在招职位·
<text class="color_256BFA">{{ company.totalRecruitment || '-' }}</text>
</text>
</view>
</view>
<view class="card-tags">
<view class="tag" v-if="company.nature">
{{ company.nature }}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
const { $api, insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const props = defineProps({
list: {
type: Array,
default: '标题',
},
longitude: {
type: Number,
default: 120.382665,
},
latitude: {
type: Number,
default: 36.066938,
},
seeDate: {
type: String,
default: '',
},
});
const listData = computed(() => {
return props.list;
});
function nextDetail(company) {
if (company.isPublish) {
return $api.msg('已过期');
}
if (company.dataType == 2) {
navTo(
`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.gsID}&companyName=${company.name}&zphId=${company.zphID}&dataType=2`
);
} else {
navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.companyId}`);
}
}
</script>
<style lang="stylus" scoped>
.date-jobTitle{
font-weight: 400;
font-size: 28rpx;
color: #495265;
padding: 28rpx 0 0 20rpx
}
.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-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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: 15rpx
margin-bottom: 10rpx
display: flex
justify-content: space-between
font-size: 28rpx;
color: #6C7282;
}
}
.ris{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
</style>

View File

@@ -9,8 +9,12 @@
<dict-tree-Label class="mar_ri10" dictType="industry" :value="job.industry"></dict-tree-Label>
<dict-Label dictType="scale" :value="job.scale"></dict-Label>
</view>
<view>
<text class="color_256BFA fs_14">在招职位·{{ job.totalRecruitment || '-' }}</text>
<view class="ris">
<text class="fs_14">
在招职位·
<text class="color_256BFA">{{ job.totalRecruitment || '-' }}</text>
</text>
</view>
</view>
<view class="card-tags">
@@ -76,6 +80,7 @@ function nextDetail(company) {
justify-content: space-between
align-items: flex-start
.company{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
color: #333333;
@@ -97,6 +102,7 @@ function nextDetail(company) {
display: flex
flex-wrap: wrap
.tag{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
width: fit-content;
height: 30rpx;
background: #F4F4F4;
@@ -121,4 +127,7 @@ function nextDetail(company) {
color: #6C7282;
}
}
</style>
.ris{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<view v-for="job in listData" :key="job.id">
<view class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company line_1">{{ job.gsmc }}</text>
</view>
<view class="card-bottom" >
<view class="fl_box fs_14" >
<!-- <dict-tree-Label class="mar_ri10" dictType="industry" :value="job.industry"></dict-tree-Label>
<dict-Label dictType="scale" :value="job.scale"></dict-Label> -->
<view>{{job.gsxy}}</view>
</view>
<view class="ris" >
<text class="fs_14">
在招职位·
<text class="color_256BFA">{{ job.zzgwsl || '-' }}</text>
</text>
</view>
</view>
<view class="card-tags">
<view class="tag" v-if="job.nature">
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
<view class="tag" v-if="job.qyxz">
{{job.qyxz}}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const props = defineProps({
list: {
type: Array,
default: '标题',
},
longitude: {
type: Number,
default: 120.382665,
},
latitude: {
type: Number,
default: 36.066938,
},
seeDate: {
type: String,
default: '',
},
zphId: {
type: String,
default: '',
},
});
const listData = computed(() => {
return props.list;
});
function nextDetail(company) {
navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.gsID}&companyName=${company.gsmc}&zphId=${props.zphId}&dataType=2`);
}
</script>
<style lang="stylus" scoped>
.date-jobTitle{
font-weight: 400;
font-size: 28rpx;
color: #495265;
padding: 28rpx 0 0 20rpx
}
.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-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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: 15rpx
margin-bottom: 10rpx
display: flex
justify-content: space-between
font-size: 28rpx;
color: #6C7282;
}
}
.ris{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<!-- 数据类型2的完整模块 -->
<view v-if="job.dataType == 2">
<view class="card-company" :class="{ grayscale: job.isPublish }">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
{{ job.education == '不限' ? '学历不限' : job.education }}
</view>
<view class="tag">
{{ job.experience == '不限' ? '经验不限' : job.experience }}
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ parseDateTime(job.createTime).date }}</view>
<view>
<!-- <convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label> -->
</view>
</view>
</view>
<!-- 数据类型1的完整模块 -->
<view v-else>
<view class="card-company" :class="{ grayscale: job.isPublish }">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
{{ job.education == '不限' ? '学历不限' : job.education }}
</view>
<view class="tag">
{{ job.experience == '不限' ? '经验不限' : job.experience }}
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ job.postingDate }}</view>
<view>
<convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
</view>
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
const { $api, insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const props = defineProps({
list: {
type: Array,
default: '标题',
},
longitude: {
type: Number,
default: 120.382665,
},
latitude: {
type: Number,
default: 36.066938,
},
seeDate: {
type: String,
default: '',
},
});
const listData = computed(() => {
if (props.seeDate && props.list.length) {
const ulist = toRaw(props.list);
const [reslist, lastDate] = insertSortData(ulist, props.seeDate);
return reslist;
}
return props.list;
});
// 解析日期时间用于数据类型2
function parseDateTime(datetimeStr) {
if (!datetimeStr) return { time: '', date: '' };
const dateObj = new Date(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 nextDetail(job) {
if (job.isPublish) {
return $api.msg('已过期');
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}&dataType=${job.dataType}`);
}
</script>
<style lang="stylus" scoped>
.date-jobTitle{
font-weight: 400;
font-size: 28rpx;
color: #495265;
padding: 28rpx 0 0 20rpx
}
.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-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
color: #333333;
}
.salary{
font-family: DIN-Medium;
font-weight: 500;
font-size: 26rpx;
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{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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;
}
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<!-- 数据类型2的完整模块 -->
<view v-if="job.dataType == 2">
<view class="card-company" :class="{ grayscale: job.isPublish }">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
{{ job.education == '不限' ? '学历不限' : job.education }}
</view>
<view class="tag">
{{ job.experience == '不限' ? '经验不限' : job.experience }}
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ parseDateTime(job.createTime).date }}</view>
<view>
<!-- <convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label> -->
</view>
</view>
</view>
<!-- 数据类型1的完整模块 -->
<view v-else>
<view class="card-company" :class="{ grayscale: job.isPublish }">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
{{ job.education == '不限' ? '学历不限' : job.education }}
</view>
<view class="tag">
{{ job.experience == '不限' ? '经验不限' : job.experience }}
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ job.postingDate }}</view>
<view>
<convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
</view>
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
const { $api, insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const props = defineProps({
list: {
type: Array,
default: '标题',
},
longitude: {
type: Number,
default: 120.382665,
},
latitude: {
type: Number,
default: 36.066938,
},
seeDate: {
type: String,
default: '',
},
});
const listData = computed(() => {
if (props.seeDate && props.list.length) {
const ulist = toRaw(props.list);
const [reslist, lastDate] = insertSortData(ulist, props.seeDate);
return reslist;
}
return props.list;
});
// 解析日期时间用于数据类型2
function parseDateTime(datetimeStr) {
if (!datetimeStr) return { time: '', date: '' };
const dateObj = new Date(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 nextDetail(job) {
if (job.isPublish) {
return $api.msg('已过期');
}
// 根据数据类型跳转到不同的详情页
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}&dataType=${job.dataType}`);
}
</script>
<style lang="stylus" scoped>
.date-jobTitle{
font-weight: 400;
font-size: 28rpx;
color: #495265;
padding: 28rpx 0 0 20rpx
}
.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-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
color: #333333;
}
.salary{
font-family: DIN-Medium;
font-weight: 500;
font-size: 26rpx;
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{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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;
}
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<!-- 数据类型2的完整模块 -->
<view v-if="job.dataType == 2">
<view class="card-company" :class="{ grayscale: job.isPublish }">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
{{ job.education == '不限' ? '学历不限' : job.education }}
</view>
<view class="tag">
{{ job.experience == '不限' ? '经验不限' : job.experience }}
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ parseDateTime(job.createTime).date }}</view>
<view>
<!-- <convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label> -->
</view>
</view>
</view>
<!-- 数据类型1的完整模块 -->
<view v-else>
<view class="card-company" :class="{ grayscale: job.isPublish }">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
{{ job.education == '不限' ? '学历不限' : job.education }}
</view>
<view class="tag">
{{ job.experience == '不限' ? '经验不限' : job.experience }}
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ job.postingDate }}</view>
<view>
<convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
</view>
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
const { $api, insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const props = defineProps({
list: {
type: Array,
default: '标题',
},
longitude: {
type: Number,
default: 120.382665,
},
latitude: {
type: Number,
default: 36.066938,
},
seeDate: {
type: String,
default: '',
},
});
const listData = computed(() => {
if (props.seeDate && props.list.length) {
const ulist = toRaw(props.list);
const [reslist, lastDate] = insertSortData(ulist, props.seeDate);
return reslist;
}
return props.list;
});
// 解析日期时间用于数据类型2
function parseDateTime(datetimeStr) {
if (!datetimeStr) return { time: '', date: '' };
const dateObj = new Date(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 nextDetail(job) {
if (job.isPublish) {
return $api.msg('已过期');
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}&dataType=${job.dataType}`);
}
</script>
<style lang="stylus" scoped>
.date-jobTitle{
font-weight: 400;
font-size: 28rpx;
color: #495265;
padding: 28rpx 0 0 20rpx
}
.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-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
color: #333333;
}
.salary{
font-family: DIN-Medium;
font-weight: 500;
font-size: 26rpx;
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{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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;
}
}
</style>

View File

@@ -7,7 +7,7 @@
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.companyName }}</view>
<view class="card-companyName">{{ job.gwmc }}</view>
<view class="card-tags">
<view class="tag">
<dict-Label dictType="education" :value="job.education"></dict-Label>
@@ -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=${btoa(job.jobId)}&dataType=1`);
}
</script>
@@ -99,13 +99,15 @@ function nextDetail(job) {
justify-content: space-between
align-items: flex-start
.company{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
font-size: 30rpx;
color: #333333;
}
.salary{
font-family: DIN-Medium;
font-weight: 500;
font-size: 28rpx;
font-size: 26rpx;
color: #4C6EFB;
white-space: nowrap
line-height: 48rpx
@@ -120,6 +122,7 @@ function nextDetail(job) {
display: flex
flex-wrap: wrap
.tag{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
width: fit-content;
height: 30rpx;
background: #F4F4F4;

View File

@@ -0,0 +1,167 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.gwmc }}</text>
<view class="salary">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.gsmc }}</view>
<view class="card-tags">
<view class="tag">
{{job.xlyq == '不限' ? '学历不限' : job.xlyq}}
</view>
<view class="tag">
{{job.gwgzjy == '不限' ? '经验不限' : job.gwgzjy}}
</view>
<view class="tag">
{{ vacanciesTo(job.zprs) }}
</view>
</view>
<view class="card-bottom">
<view>{{ parseDateTime(job.createTime).date }}</view>
<view>
<!-- <convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label> -->
</view>
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const props = defineProps({
list: {
type: Array,
default: '标题',
},
longitude: {
type: Number,
default: 120.382665,
},
latitude: {
type: Number,
default: 36.066938,
},
seeDate: {
type: String,
default: '',
},
});
const listData = computed(() => {
if (props.seeDate && props.list.length) {
const ulist = toRaw(props.list);
const [reslist, lastDate] = insertSortData(ulist, props.seeDate);
return reslist;
}
return props.list;
});
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.id)}&dataType=2`);
}
function parseDateTime(datetimeStr) {
if (!datetimeStr) return { time: '', date: '' };
const dateObj = new Date(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}`,
};
}
</script>
<style lang="stylus" scoped>
.date-jobTitle{
font-weight: 400;
font-size: 28rpx;
color: #495265;
padding: 28rpx 0 0 20rpx
}
.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-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
color: #333333;
}
.salary{
font-family: DIN-Medium;
font-weight: 500;
font-size: 26rpx;
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{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
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;
}
}
</style>

View File

@@ -6,6 +6,7 @@
background-color="#FFFFFF"
@maskClick="maskClickFn"
:mask-click="maskClick"
class="popup-fix"
>
<view class="popup-content">
<view class="popup-header">
@@ -148,6 +149,7 @@ const cleanup = () => {
Object.keys(selectedValues).forEach((key) => {
delete selectedValues[key];
});
count.value = 0;
};
const scrollTo = (key) => {
@@ -160,6 +162,7 @@ function getoptions() {
getTransformChildren('experience', '工作经验'),
getTransformChildren('scale', '公司规模'),
];
console.log(arr);
if (area.value) {
arr.push(getTransformChildren('area', '区域'));
}
@@ -183,6 +186,15 @@ defineExpose({
</script>
<style lang="scss" scoped>
.popup-fix {
position: fixed !important;
left: 0;
right: 0;
bottom: 0;
top: 0;
height: 100vh;
z-index: 9999;
}
.popup-content {
color: #000000;
height: 80vh;
@@ -320,7 +332,7 @@ defineExpose({
font-weight: 400;
font-size: 28rpx;
color: #333333;
margin-bottom: 15rpx;
margin-bottom: 15rpx;
}
}
.content-item:first-child {
@@ -329,8 +341,8 @@ defineExpose({
.check-content {
display: grid;
gap:16rpx;
grid-template-columns: repeat(auto-fill, minmax(180rpx, 1fr));
gap: 16rpx;
grid-template-columns: repeat(auto-fill, minmax(180rpx, 1fr));
place-items: stretch;
.checkbox-item {
@@ -338,9 +350,9 @@ defineExpose({
align-items: center;
text-align: center;
background-color: #d9d9d9;
min-width: 0;
padding: 0 10rpx;
min-width: 0;
padding: 0 10rpx;
height: 80rpx;
background: #e8eaee;
border-radius: 12rpx 12rpx 12rpx 12rpx;
@@ -348,12 +360,11 @@ defineExpose({
.option-label {
font-size: 28rpx;
width: 100%;
white-space: nowrap;
overflow: hidden;
white-space: nowrap;
overflow: hidden;
}
}
.checkedstyle {
height: 76rpx;
background: rgba(37, 107, 250, 0.06);
border-radius: 12rpx 12rpx 12rpx 12rpx;
@@ -362,4 +373,4 @@ defineExpose({
}
}
}
</style>
</style>

View File

@@ -0,0 +1,345 @@
<template>
<uni-popup
ref="popup"
type="bottom"
borderRadius="10px 10px 0 0"
background-color="#FFFFFF"
@maskClick="maskClickFn"
:mask-click="maskClick"
class="popup-fix"
>
<view class="popup-content">
<view class="popup-header">
<view class="btn-cancel" @click="cancel">取消</view>
<view class="title">
<text>{{ title }}</text>
</view>
<view class="btn-confirm"></view>
</view>
<view class="popup-list">
<view class="content-wrapper">
<scroll-view class="filter-nav" scroll-y>
<view
v-for="(item, index) in filterOptions"
:key="index"
class="nav-item button-click"
:class="{ active: activeTab === item.key }"
@click="scrollTo(item.key)"
>
{{ item.label }}
</view>
</scroll-view>
<scroll-view class="filter-content" :scroll-into-view="activeTab" scroll-y>
<template v-for="(item, index) in filterOptions" :key="index">
<view class="content-item">
<view class="item-title" :id="item.key">{{ item.label }}</view>
<view class="check-content">
<view
v-for="option in item.options"
:key="option.value"
class="checkbox-item button-click"
:class="{
checkedstyle: activeValue === option.value,
}"
@click="handleItemClick(option)"
>
<text class="option-label">{{ option.label }}</text>
</view>
</view>
</view>
</template>
</scroll-view>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, reactive, nextTick, onBeforeMount } from 'vue';
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren } = useDictStore();
const area = ref(true);
const maskClick = ref(false);
const maskClickFn = ref(null);
const title = ref('标题');
const confirmCallback = ref(null);
const cancelCallback = ref(null);
const changeCallback = ref(null);
const popup = ref(null);
// MODIFIED: 新增 ref用于存储当前激活的选项值
const activeValue = ref(null);
const activeTab = ref('');
const filterOptions = ref([]);
const listData = ref([]);
// MODIFIED: open 方法增加一个 currentValue 参数
const open = (newConfig = {}) => {
const {
title: configTitle,
success,
cancel,
change,
data,
maskClick: configMaskClick = false,
currentValue, // MODIFIED: 接收父组件传入的当前值
} = newConfig;
// reset();
if (configTitle) title.value = configTitle;
if (typeof success === 'function') confirmCallback.value = success;
if (typeof cancel === 'function') cancelCallback.value = cancel;
if (typeof change === 'function') changeCallback.value = change;
if (Array.isArray(data)) listData.value = data;
// MODIFIED: 将父组件传入的值
activeValue.value = currentValue;
if (configMaskClick) {
maskClick.value = configMaskClick;
maskClickFn.value = cancel;
}
getoptions();
nextTick(() => {
popup.value?.open();
});
};
const close = () => {
popup.value?.close();
};
const cancel = () => {
handleClick(cancelCallback.value, null);
};
const handleClick = async (callback, selectedItem) => {
if (typeof callback !== 'function') {
close();
return;
}
try {
const result = await callback(selectedItem);
if (result !== false) close();
} catch (error) {
console.error('Callback execution error:', error);
}
};
const handleItemClick = (option) => {
// MODIFIED: 点击时,更新本地的 activeValue
activeValue.value = option.value;
// 立即调用回调并传递所选的完整 option
handleClick(confirmCallback.value, option);
};
const scrollTo = (key) => {
activeTab.value = key;
};
function getoptions() {
filterOptions.value = transformRegionalData(listData.value);
activeTab.value = listData.value[0].key;
}
const reset = () => {
maskClick.value = false;
confirmCallback.value = null;
cancelCallback.value = null;
changeCallback.value = null;
// MODIFIED: 重置时也清空 activeValue
activeValue.value = null;
};
function transformRegionalData(sourceData) {
const options = sourceData.map((region, index) => {
const ls = region.areaList.map((commercial) => ({
...commercial,
text: commercial.commercialAreaName,
label: commercial.commercialAreaName,
value: commercial.commercialAreaId,
key: commercial.commercialAreaId,
listClass: 'default',
status: 'default',
}));
return {
label: region.regionalName,
key: 'lx' + region.regionalId,
options: ls,
};
});
return options;
}
// 暴露方法给父组件
defineExpose({
open,
close,
});
</script>
<style lang="scss" scoped>
/* 样式表无变化,保持原样即可 */
.popup-fix {
position: fixed !important;
left: 0;
right: 0;
bottom: 0;
top: 0;
height: 100vh;
z-index: 9999;
}
.popup-content {
color: #000000;
height: 80vh;
}
.popup-bottom {
display: none;
}
.popup-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
height: calc(80vh - 100rpx);
overflow: hidden;
.picker-view {
width: 100%;
height: 500rpx;
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: 32rpx;
color: #cccccc;
}
.item-active {
color: #333333;
}
.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;
min-width: 60rpx;
}
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
}
.filter-nav {
width: 200rpx;
background-color: #ffffff;
.nav-item {
height: 100rpx;
line-height: 100rpx;
text-align: center;
font-weight: 400;
font-size: 28rpx;
color: #666d7f;
&.active {
font-weight: 500;
font-size: 28rpx;
color: #256bfa;
}
}
}
.filter-content {
flex: 1;
padding: 20rpx;
background-color: #f6f6f6;
.content-item {
margin-top: 30rpx;
.item-title {
font-weight: 400;
font-size: 28rpx;
color: #333333;
margin-bottom: 15rpx;
}
}
.content-item:first-child {
margin-top: 0rpx;
}
.check-content {
display: grid;
gap: 16rpx;
grid-template-columns: repeat(auto-fill, minmax(180rpx, 1fr));
place-items: stretch;
.checkbox-item {
display: flex;
align-items: center;
text-align: center;
background-color: #d9d9d9;
min-width: 0;
padding: 0 10rpx;
height: 80rpx;
background: #e8eaee;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.option-label {
font-size: 28rpx;
width: 100%;
white-space: nowrap;
overflow: hidden;
}
}
/* 这个样式现在会根据 activeValue 动态应用 */
.checkedstyle {
height: 76rpx;
background: rgba(37, 107, 250, 0.06);
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #256bfa;
color: #256bfa;
}
}
}
</style>

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';
@@ -58,9 +58,9 @@ const state = reactive({
visible: false,
});
onMounted(() => {
serchforIt();
});
// onMounted(() => {
// serchforIt();
// });
// 统一处理二维数组格式
const processedListData = computed(() => {
@@ -82,18 +82,17 @@ const open = (newConfig = {}) => {
rowLabel: configRowLabel = 'label',
rowKey: configRowKey = 'value',
maskClick: configMaskClick = false,
defaultIndex = [],
defaultId = '',
} = newConfig;
reset();
serchforIt();
if (configTitle) title.value = configTitle;
if (typeof success === 'function') confirmCallback.value = success;
if (typeof cancel === 'function') cancelCallback.value = cancel;
if (typeof change === 'function') changeCallback.value = change;
if (Array.isArray(data)) listData.value = data;
serchforIt(defaultId);
rowLabel.value = configRowLabel;
rowKey.value = configRowKey;
maskClick.value = configMaskClick;
@@ -143,16 +142,30 @@ const handleClick = async (callback) => {
console.error('confirmCallback 执行出错:', error);
}
};
function serchforIt() {
function serchforIt(defaultId) {
if (state.stations.length) {
const ids = userInfo.value.jobTitleId.split(',').map((id) => Number(id));
const ids = defaultId
? defaultId.split(',').map((id) => Number(id))
: userInfo.value.jobTitleId.split(',').map((id) => Number(id));
count.value = ids.length;
state.jobTitleId = userInfo.value.jobTitleId;
state.jobTitleId = defaultId ? defaultId : userInfo.value.jobTitleId;
setCheckedNodes(state.stations, ids);
state.visible = true;
return;
}
$api.createRequest('/app/common/jobTitle/treeselect', {}, 'GET').then((resData) => {
if (listData.value.length) {
if (userInfo.value.jobTitleId) {
const ids = userInfo.value.jobTitleId.split(',').map((id) => Number(id));
count.value = ids.length;
setCheckedNodes(listData.value, ids);
}
state.jobTitleId = userInfo.value.jobTitleId;
state.stations = listData.value;
state.visible = true;
return;
}
const LoadCache = (resData) => {
if (userInfo.value.jobTitleId) {
const ids = userInfo.value.jobTitleId.split(',').map((id) => Number(id));
count.value = ids.length;
@@ -161,19 +174,20 @@ function serchforIt() {
state.jobTitleId = userInfo.value.jobTitleId;
state.stations = resData.data;
state.visible = true;
});
};
$api.createRequestWithCache('/app/common/jobTitle/treeselect', {}, 'GET', false, LoadCache).then(LoadCache);
}
const reset = () => {
maskClick.value = false;
confirmCallback.value = null;
cancelCallback.value = null;
changeCallback.value = null;
listData.value = [];
selectedIndex.value = [0, 0, 0];
rowLabel.value = 'label';
rowKey.value = 'value';
selectedItems.value = [];
JobsIdsValue.value = '';
JobsLabelValue.value = '';
};
// 暴露方法给父组件

View File

@@ -1,9 +1,16 @@
<template>
<view class="tabbar_container">
<view class="tabbar_item" v-for="(item, index) in tabbarList" :key="index" @click="changeItem(item)">
<view class="item-top" :class="[item.centerItem ? 'center-item-img' : '']">
<view
class="item-top"
:class="[
item.centerItem ? 'center-item-img ' : '',
item.centerItem && currentItem === item.id ? 'rubberBand animated' : '',
]"
>
<image :src="currentItem == item.id ? item.selectedIconPath : item.iconPath"></image>
</view>
<view class="badge" v-if="item.badge">{{ item.badge }}</view>
<view class="item-bottom" :class="[currentItem == item.id ? 'item-active' : '']">
<text>{{ item.text }}</text>
</view>
@@ -11,90 +18,107 @@
</view>
</template>
<script>
export default {
data() {
return {
currentItem: 0,
tabbarList: [
{
id: 0,
text: '首页',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
},
{
id: 1,
text: '招聘会',
path: '/pages/careerfair/careerfair',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
},
{
id: 2,
text: '',
path: '/pages/chat/chat',
iconPath: '../../static/tabbar/logo2copy.png',
selectedIconPath: '../../static/tabbar/logo2copy.png',
centerItem: true,
},
{
id: 3,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
},
{
id: 4,
text: '我的',
path: '/pages/mine/mine',
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
},
],
};
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useReadMsg } from '@/stores/useReadMsg';
const props = defineProps({
currentpage: {
type: Number,
required: true,
default: 0,
},
props: {
currentpage: {
type: Number,
required: true,
default: 0,
},
});
const readMsg = useReadMsg();
const currentItem = ref(0);
const tabbarList = computed(() => [
{
id: 0,
text: '首页',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0].count,
},
mounted() {
this.currentItem = this.currentpage;
uni.hideTabBar();
{
id: 1,
text: '招聘会',
path: '/pages/careerfair/careerfair',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1].count,
},
methods: {
changeItem(item) {
uni.switchTab({
url: item.path,
});
},
{
id: 2,
text: '',
path: '/pages/chat/chat',
iconPath: '../../static/tabbar/logo3.png',
selectedIconPath: '../../static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2].count,
},
{
id: 3,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3].count,
},
{
id: 4,
text: '我的',
path: '/pages/mine/mine',
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4].count,
},
]);
onMounted(() => {
uni.hideTabBar();
currentItem.value = props.currentpage;
});
const changeItem = (item) => {
uni.switchTab({
url: item.path,
});
};
</script>
<style lang="scss" scoped>
.badge {
position: absolute;
top: 4rpx;
right: 20rpx;
min-width: 30rpx;
height: 30rpx;
background-color: red;
color: #fff;
font-size: 18rpx;
border-radius: 15rpx;
text-align: center;
line-height: 30rpx;
padding: 0 10rpx;
}
.tabbar_container {
background-color: #ffffff;
position: fixed;
bottom: 0rpx;
left: 0rpx;
width: 100%;
height: 126rpx;
// box-shadow: 0 0 5px #999;
height: 88rpx;
display: flex;
align-items: center;
padding: 5rpx 0;
padding-bottom: env(safe-area-inset-bottom);
z-index: 998;
overflow: hidden;
// position: fixed;
// bottom: 0rpx;
// left: 0rpx;
// box-shadow: 0 0 5px #999;
// padding-bottom: env(safe-area-inset-bottom);
// z-index: 998;
.tabbar_item {
width: 33.33%;
height: 100rpx;
@@ -120,12 +144,12 @@ export default {
}
}
.center-item-img {
position: absolute;
top: 0rpx;
left: 50%;
transform: translate(-50%, 0);
width: 96rpx !important;
height: 96rpx !important;
// position: absolute;
// top: 0rpx;
// left: 50%;
// transform: translate(-50%, 0);
width: 108rpx !important;
height: 98rpx !important;
}
.item-active {
color: #256bfa;

View File

@@ -1,10 +1,8 @@
export default {
// baseUrl: 'http://39.98.44.136:8080', // 测试
// baseUrl: 'https://fw.rc.qingdao.gov.cn/rgpp-api/api', // 内网
baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// sseAI+
// StreamBaseURl: 'http://39.98.44.136:8000',
StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai',
// StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai/test',
// baseUrl: 'http://192.168.3.29:8081',
// baseUrl: 'http://10.213.6.207:19010/api',
// 语音转文字
// vioceBaseURl: 'ws://39.98.44.136:8080/speech-recognition',
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
@@ -13,11 +11,17 @@ export default {
// indexedDB
DBversion: 2,
// 只使用本地缓寸的数据
OnlyUseCachedDB: true,
OnlyUseCachedDB: false,
// 使用模拟定位
UsingSimulatedPositioning: true,
// 应用信息
appInfo: {
// 应用名称
name: "青岛市就业服务",
// 爱山东应用标识
loveShandong: 'szjxrgznqzzp',
// 爱山东应用Key
sm2PrivateKey: '0d152c849f10e4469f2af8cedea62004e4f1db7be23c2f7270c1441d8050799d',
// 地区名
areaName: '青岛市',
// AI名称
@@ -39,7 +43,9 @@ export default {
}
]
},
// AI -> 上传文件数量
allowedFileNumber: 2,
// AI -> 上传文件类型
allowedFileTypes: [
"text/plain", // .txt
"text/markdown", // .md
@@ -52,5 +58,24 @@ export default {
"text/csv", // .csv
"application/vnd.ms-excel", // .xls
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" // .xlsx
]
],
// 首页询问 -> 推荐权重
weights: {
categories: 1, //岗位
experience: 0.3, //经验
salary: 0.5, // 薪资
areas: 0.5 // 区域
},
shareConfig: {
baseUrl: 'https://qd.zhaopinzao8dian.com',
title: '找工作,用 AI 更高效|青岛市智能求职平台',
desc: '融合海量岗位、智能简历匹配、竞争力分析,助你精准锁定理想职位!',
imgUrl: 'https://qd.zhaopinzao8dian.com/file/csn/qd_shareLogo.jpg',
},
sm4Config: {
key: '86C63180C1306ABC4D8F989E0A0BC9F3',
mode: 'ECB', // default
iv: 'UISwD9fW6cFh9SNS', // default is null
cipherType: 'base64' // default is base64
}
}

BIN
hook/.DS_Store vendored

Binary file not shown.

30
hook/page-animation.css Normal file
View File

@@ -0,0 +1,30 @@
/* #ifdef H5 */
uni-page {
opacity: 1;
will-change: opacity;
}
/* --- 进场 (Enter) --- */
uni-page.animation-enter-from {
opacity: 0;
}
uni-page.animation-enter-active {
transition: opacity 0.2s ease-out;
}
/* --- 离场 (Leave) --- */
uni-page.animation-leave-active {
transition: opacity 0.15s ease-in;
}
uni-page.animation-leave-to {
opacity: 0;
}
/* --- 稳态 --- */
uni-page.animation-show {
opacity: 1;
}
/* #endif */

66
hook/usePageAnimation.js Normal file
View File

@@ -0,0 +1,66 @@
import {
onLaunch
} from '@dcloudio/uni-app'
import {
getCurrentInstance
} from 'vue'
import './page-animation.css'
const DURATION = 130
export default function usePageAnimation() {
// #ifdef H5
const show = () => {
const page = document.querySelector('uni-page')
if (!page) return
const cl = page.classList
cl.add('animation-enter-from')
cl.remove('animation-leave-to', 'animation-leave-active')
requestAnimationFrame(() => {
requestAnimationFrame(() => {
cl.remove('animation-enter-from')
cl.add('animation-enter-active', 'animation-show')
setTimeout(() => {
cl.remove('animation-enter-active')
}, DURATION)
})
})
}
const hide = (next) => {
const page = document.querySelector('uni-page')
if (!page) {
next()
return
}
const cl = page.classList
cl.add('animation-leave-active')
requestAnimationFrame(() => {
cl.remove('animation-show')
cl.add('animation-leave-to')
setTimeout(() => {
cl.remove('animation-leave-active', 'animation-leave-to')
next()
}, DURATION - 50)
})
}
onLaunch(() => {
const instance = getCurrentInstance()
const router = instance?.proxy?.$router
if (router) {
show()
router.beforeEach((to, from, next) => hide(next))
router.afterEach(() => show())
}
})
// #endif
}

172
hook/usePagination.js Normal file
View File

@@ -0,0 +1,172 @@
import {
ref,
reactive,
watch,
isRef,
nextTick
} from 'vue'
export function usePagination(
requestFn,
transformFn,
options = {}
) {
const list = ref([])
const loading = ref(false)
const error = ref(false)
const finished = ref(false)
const firstLoading = ref(true)
const empty = ref(false)
const {
pageSize = 10,
search = {},
autoWatchSearch = false,
debounceTime = 300,
autoFetch = false,
// 字段映射
dataKey = 'rows',
totalKey = 'total',
// 分页字段名映射
pageField = 'current',
sizeField = 'pageSize',
onBeforeRequest,
onAfterRequest
} = options
const pageState = reactive({
page: 1,
pageSize: isRef(pageSize) ? pageSize.value : pageSize,
total: 0,
maxPage: 1,
search: isRef(search) ? search.value : search
})
let debounceTimer = null
const fetchData = async (type = 'refresh') => {
if (loading.value) return Promise.resolve()
console.log(type)
loading.value = true
error.value = false
if (typeof onBeforeRequest === 'function') {
try {
onBeforeRequest(type, pageState)
} catch (err) {
console.warn('onBeforeRequest 执行异常:', err)
}
}
if (type === 'refresh') {
pageState.page = 1
finished.value = false
if (list.value.length === 0) {
firstLoading.value = true
}
} else if (type === 'loadMore') {
if (pageState.page >= pageState.maxPage) {
loading.value = false
finished.value = true
return Promise.resolve('no more')
}
pageState.page += 1
}
const params = {
...pageState.search,
[pageField]: pageState.page,
[sizeField]: pageState.pageSize,
}
try {
const res = await requestFn(params)
const rawData = res[dataKey]
const data = typeof transformFn === 'function' ? transformFn(rawData) : rawData
if (type === 'refresh') {
list.value = data
} else {
list.value.push(...data)
}
const total = res[totalKey] || list.value?.length
pageState.total = total
pageState.maxPage = Math.ceil(total / pageState.pageSize)
finished.value = list.value.length >= total
empty.value = list.value.length === 0
} catch (err) {
console.error('分页请求失败:', err)
error.value = true
} finally {
loading.value = false
firstLoading.value = false
if (typeof onAfterRequest === 'function') {
try {
onAfterRequest(type, pageState, {
error: error.value
})
} catch (err) {
console.warn('onAfterRequest 执行异常:', err)
}
}
}
}
const refresh = () => fetchData('refresh')
const loadMore = () => fetchData('loadMore')
const resetPagination = () => {
list.value = []
pageState.page = 1
pageState.total = 0
pageState.maxPage = 1
finished.value = false
error.value = false
firstLoading.value = true
empty.value = false
}
if (autoWatchSearch && isRef(search)) {
watch(search, (newVal) => {
pageState.search = newVal
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
refresh()
}, debounceTime)
}, {
deep: true
})
}
watch(pageSize, (newVal) => {
pageState.pageSize = newVal
}, {
deep: true
})
if (autoFetch) {
nextTick(() => {
refresh()
})
}
return {
list,
loading,
error,
finished,
firstLoading,
empty,
pageState,
refresh,
loadMore,
resetPagination
}
}

View File

@@ -1,387 +1,246 @@
import {
ref,
onUnmounted
} from 'vue';
} from 'vue'
import {
$api,
function mergeText(prevText, newText) {
if (newText.startsWith(prevText)) {
return newText; // 直接替换,避免重复拼接
} from '../common/globalFunction';
import config from '@/config'
export function useAudioRecorder() {
const isRecording = ref(false)
const isStopping = ref(false)
const isSocketConnected = ref(false)
const recordingDuration = ref(0)
const audioDataForDisplay = ref(new Array(16).fill(0))
const volumeLevel = ref(0)
const recognizedText = ref('')
const lastFinalText = ref('')
let audioStream = null
let audioContext = null
let audioInput = null
let scriptProcessor = null
let websocket = null
let durationTimer = null
const generateUUID = () => {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11)
.replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
).replace(/-/g, '')
}
return prevText + newText; // 兼容意外情况
}
export function useAudioRecorder(wsUrl) {
// 状态变量
const isRecording = ref(false);
const isStopping = ref(false);
const isSocketConnected = ref(false);
const recordingDuration = ref(0);
const audioDataForDisplay = ref(new Array(16).fill(0.01));
const volumeLevel = ref(0);
const fetchWsUrl = async () => {
const res = await $api.createRequest('/app/speech/getToken')
if (res.code !== 200) throw new Error('无法获取语音识别 wsUrl')
const wsUrl = res.msg
return wsUrl
}
// 音频相关
const audioContext = ref(null);
const mediaStream = ref(null);
const workletNode = ref(null);
const analyser = ref(null);
// 网络相关
const socket = ref(null);
// 配置常量
const SAMPLE_RATE = 16000;
const SILENCE_THRESHOLD = 0.02; // 静音阈值 (0-1)
const SILENCE_DURATION = 100; // 静音持续时间(ms)后切片
const MIN_SOUND_DURATION = 200; // 最小有效声音持续时间(ms)
// 音频处理变量
const lastSoundTime = ref(0);
const audioChunks = ref([]);
const currentChunkStartTime = ref(0);
const silenceStartTime = ref(0);
// 语音识别结果
const recognizedText = ref('');
const lastFinalText = ref(''); // 保存最终确认的文本
// AudioWorklet处理器代码
const workletProcessorCode = `
class AudioProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
this.silenceThreshold = options.processorOptions.silenceThreshold;
this.sampleRate = options.processorOptions.sampleRate;
this.samplesPerChunk = Math.floor(this.sampleRate * 0.05); // 50ms的块
this.buffer = new Int16Array(this.samplesPerChunk);
this.index = 0;
this.lastUpdate = 0;
}
calculateVolume(inputs) {
const input = inputs[0];
if (!input || input.length === 0) return 0;
let sum = 0;
const inputChannel = input[0];
for (let i = 0; i < inputChannel.length; i++) {
sum += inputChannel[i] * inputChannel[i];
}
return Math.sqrt(sum / inputChannel.length);
}
process(inputs) {
const now = currentTime;
const volume = this.calculateVolume(inputs);
// 每50ms发送一次分析数据
if (now - this.lastUpdate > 0.05) {
this.lastUpdate = now;
// 简单的频率分析 (模拟16个频段)
const simulatedFreqData = [];
for (let i = 0; i < 16; i++) {
simulatedFreqData.push(
Math.min(1, volume * 10 + (Math.random() * 0.2 - 0.1))
);
}
this.port.postMessage({
type: 'analysis',
volume: volume,
frequencyData: simulatedFreqData,
isSilent: volume < this.silenceThreshold,
timestamp: now
});
}
// 原始音频处理
const input = inputs[0];
if (input && input.length > 0) {
const inputChannel = input[0];
for (let i = 0; i < inputChannel.length; i++) {
this.buffer[this.index++] = Math.max(-32768, Math.min(32767, inputChannel[i] * 32767));
if (this.index >= this.samplesPerChunk) {
this.port.postMessage({
type: 'audio',
audioData: this.buffer.buffer,
timestamp: now
}, [this.buffer.buffer]);
this.buffer = new Int16Array(this.samplesPerChunk);
this.index = 0;
}
}
}
return true;
function extractWsParams(wsUrl) {
const url = new URL(wsUrl)
const appkey = url.searchParams.get('appkey')
const token = url.searchParams.get('token')
return {
appkey,
token
}
}
registerProcessor('audio-processor', AudioProcessor);
`;
// 初始化WebSocket连接
const initSocket = (wsUrl) => {
const connectWebSocket = async () => {
const wsUrl = await fetchWsUrl()
const {
appkey,
token
} = extractWsParams(wsUrl)
return new Promise((resolve, reject) => {
socket.value = new WebSocket(wsUrl);
websocket = new WebSocket(wsUrl)
websocket.binaryType = 'arraybuffer'
socket.value.onopen = () => {
console.log('open')
isSocketConnected.value = true;
resolve();
};
websocket.onopen = () => {
isSocketConnected.value = true
socket.value.onerror = (error) => {
reject(error);
};
// 发送 StartTranscription 消息(参考 demo.html
const startTranscriptionMessage = {
header: {
appkey: appkey, // 不影响使用,可留空或由 wsUrl 带入
namespace: 'SpeechTranscriber',
name: 'StartTranscription',
task_id: generateUUID(),
message_id: generateUUID()
},
payload: {
format: 'pcm',
sample_rate: 16000,
enable_intermediate_result: true,
enable_punctuation_prediction: true,
enable_inverse_text_normalization: true
}
}
websocket.send(JSON.stringify(startTranscriptionMessage))
resolve()
}
socket.value.onclose = () => {
isSocketConnected.value = false;
};
websocket.onerror = (e) => {
isSocketConnected.value = false
reject(e)
}
socket.value.onmessage = handleMessage;
});
};
websocket.onclose = () => {
isSocketConnected.value = false
}
const handleMessage = (values) => {
try {
const data = JSON.parse(event.data);
if (data.text) {
const {
asrEnd,
text
} = data
if (asrEnd === 'true') {
recognizedText.value += data.text;
} else {
lastFinalText.value = '';
websocket.onmessage = (e) => {
const msg = JSON.parse(e.data)
const name = msg?.header?.name
const payload = msg?.payload
switch (name) {
case 'TranscriptionResultChanged': {
// 中间识别文本(可选:使用 stash_result.unfixedText 更精确)
const text = payload?.unfixed_result || payload?.result || ''
lastFinalText.value = text
break
}
case 'SentenceBegin': {
// 可选:开始新的一句,重置状态
// console.log('开始新的句子识别')
break
}
case 'SentenceEnd': {
const text = payload?.result || ''
const confidence = payload?.confidence || 0
if (text && confidence > 0.5) {
recognizedText.value += text
lastFinalText.value = ''
// console.log('识别完成:', {
// text,
// confidence
// })
}
break
}
case 'TranscriptionStarted': {
// console.log('识别任务已开始')
break
}
case 'TranscriptionCompleted': {
lastFinalText.value = ''
// console.log('识别全部完成')
break
}
case 'TaskFailed': {
console.error('识别失败:', msg?.header?.status_text)
break
}
default:
console.log('未知消息类型:', name, msg)
break
}
}
} catch (error) {
console.error('解析识别结果失败:', error);
})
}
const startRecording = async () => {
if (isRecording.value) return
try {
recognizedText.value = ''
lastFinalText.value = ''
await connectWebSocket()
audioStream = await navigator.mediaDevices.getUserMedia({
audio: true
})
audioContext = new(window.AudioContext || window.webkitAudioContext)({
sampleRate: 16000
})
audioInput = audioContext.createMediaStreamSource(audioStream)
scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1)
scriptProcessor.onaudioprocess = (event) => {
const input = event.inputBuffer.getChannelData(0)
const pcm = new Int16Array(input.length)
let sum = 0
for (let i = 0; i < input.length; ++i) {
const s = Math.max(-1, Math.min(1, input[i]))
pcm[i] = s * 0x7FFF
sum += s * s
}
volumeLevel.value = Math.sqrt(sum / input.length)
audioDataForDisplay.value = Array(16).fill(volumeLevel.value)
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(pcm.buffer)
}
}
audioInput.connect(scriptProcessor)
scriptProcessor.connect(audioContext.destination)
isRecording.value = true
recordingDuration.value = 0
durationTimer = setInterval(() => recordingDuration.value++, 1000)
} catch (err) {
console.error('启动失败:', err)
cleanup()
}
}
// 处理音频切片
const processAudioChunk = (isSilent) => {
const now = Date.now();
const stopRecording = () => {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
if (!isSilent) {
// 检测到声音
lastSoundTime.value = now;
if (silenceStartTime.value > 0) {
// 从静音恢复到有声音
silenceStartTime.value = 0;
}
} else {
// 静音状态
if (silenceStartTime.value === 0) {
silenceStartTime.value = now;
}
// 检查是否达到静音切片条件
if (now - silenceStartTime.value >= SILENCE_DURATION &&
now - currentChunkStartTime.value >= MIN_SOUND_DURATION) {
sendCurrentChunk();
}
}
};
// 发送当前音频块
const sendCurrentChunk = () => {
if (audioChunks.value.length === 0 || !socket.value || socket.value.readyState !== WebSocket.OPEN) {
return;
}
try {
// 合并所有块
const totalBytes = audioChunks.value.reduce((total, chunk) => total + chunk.byteLength, 0);
const combined = new Int16Array(totalBytes / 2);
let offset = 0;
audioChunks.value.forEach(chunk => {
const samples = new Int16Array(chunk);
combined.set(samples, offset);
offset += samples.length;
});
// 发送合并后的数据
socket.value.send(combined.buffer);
audioChunks.value = [];
// 记录新块的开始时间
currentChunkStartTime.value = Date.now();
silenceStartTime.value = 0;
} catch (error) {
console.error('发送音频数据时出错:', error);
}
};
// 开始录音
const startRecording = async () => {
if (isRecording.value) return;
try {
// 重置状态
recognizedText.value = '';
lastFinalText.value = '';
// 重置状态
recordingDuration.value = 0;
audioChunks.value = [];
lastSoundTime.value = 0;
currentChunkStartTime.value = Date.now();
silenceStartTime.value = 0;
// 初始化WebSocket
await initSocket(wsUrl);
// 获取音频流
mediaStream.value = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: SAMPLE_RATE,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false
},
video: false
});
// 创建音频上下文
audioContext.value = new(window.AudioContext || window.webkitAudioContext)({
sampleRate: SAMPLE_RATE
});
// 注册AudioWorklet
const blob = new Blob([workletProcessorCode], {
type: 'application/javascript'
});
const workletUrl = URL.createObjectURL(blob);
await audioContext.value.audioWorklet.addModule(workletUrl);
URL.revokeObjectURL(workletUrl);
// 创建AudioWorkletNode
workletNode.value = new AudioWorkletNode(audioContext.value, 'audio-processor', {
processorOptions: {
silenceThreshold: SILENCE_THRESHOLD,
sampleRate: SAMPLE_RATE
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify({
header: {
namespace: 'SpeechTranscriber',
name: 'StopTranscription',
message_id: generateUUID()
}
});
// 处理音频数据
workletNode.value.port.onmessage = (e) => {
if (e.data.type === 'audio') {
audioChunks.value.push(e.data.audioData);
} else if (e.data.type === 'analysis') {
audioDataForDisplay.value = e.data.frequencyData;
volumeLevel.value = e.data.volume;
processAudioChunk(e.data.isSilent);
}
};
// 连接音频节点
const source = audioContext.value.createMediaStreamSource(mediaStream.value);
source.connect(workletNode.value);
workletNode.value.connect(audioContext.value.destination);
isRecording.value = true;
} catch (error) {
console.error('启动录音失败:', error);
cleanup();
throw error;
}))
websocket.close()
}
};
// 停止录音
const stopRecording = async () => {
if (!isRecording.value || isStopping.value) return;
cleanup()
isStopping.value = false
}
isStopping.value = true;
const cancelRecording = () => {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
websocket?.close()
cleanup()
isStopping.value = false
}
try {
// 发送最后一个音频块(无论是否静音)
sendCurrentChunk();
// 发送结束标记
if (socket.value?.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify({
action: 'end',
duration: recordingDuration.value
}));
await new Promise(resolve => {
if (socket.value.bufferedAmount === 0) {
resolve();
} else {
const timer = setInterval(() => {
if (socket.value.bufferedAmount === 0) {
clearInterval(timer);
resolve();
}
}, 50);
}
});
socket.value.close();
}
cleanup();
} catch (error) {
console.error('停止录音时出错:', error);
throw error;
} finally {
isStopping.value = false;
}
};
// 清理资源
const cleanup = () => {
if (mediaStream.value) {
mediaStream.value.getTracks().forEach(track => track.stop());
mediaStream.value = null;
}
clearInterval(durationTimer)
if (workletNode.value) {
workletNode.value.disconnect();
workletNode.value = null;
}
scriptProcessor?.disconnect()
audioInput?.disconnect()
audioStream?.getTracks().forEach(track => track.stop())
audioContext?.close()
if (audioContext.value && audioContext.value.state !== 'closed') {
audioContext.value.close();
audioContext.value = null;
}
audioStream = null
audioContext = null
audioInput = null
scriptProcessor = null
websocket = null
audioChunks.value = [];
isRecording.value = false;
isSocketConnected.value = false;
};
/// 取消录音
const cancelRecording = async () => {
if (!isRecording.value || isStopping.value) return;
isStopping.value = true;
try {
if (socket.value?.readyState === WebSocket.OPEN) {
console.log('发送结束标记...');
socket.value.send(JSON.stringify({
action: 'cancel'
}));
socket.value.close();
}
cleanup()
} catch (error) {
console.error('取消录音时出错:', error);
throw error;
} finally {
isStopping.value = false;
}
};
isRecording.value = false
isSocketConnected.value = false
}
onUnmounted(() => {
if (isRecording.value) {
stopRecording();
}
});
if (isRecording.value) stopRecording()
})
return {
isRecording,
@@ -390,10 +249,10 @@ export function useAudioRecorder(wsUrl) {
recordingDuration,
audioDataForDisplay,
volumeLevel,
startRecording,
stopRecording,
recognizedText,
lastFinalText,
startRecording,
stopRecording,
cancelRecording
};
}
}

View File

@@ -1,136 +0,0 @@
import {
ref,
onBeforeUnmount,
onMounted
} from 'vue'
import {
onHide,
onUnload
} from '@dcloudio/uni-app'
export function useSpeechReader() {
const isSpeaking = ref(false)
const isPaused = ref(false)
let utterance = null
const cleanMarkdown = (text) => {
return formatTextForSpeech(text)
}
const speak = (text, options = {
lang: 'zh-CN',
rate: 0.9,
pitch: 1.2
}) => {
cancelAudio() // 重置之前的
// const voices = speechSynthesis.getVoices()
// const chineseVoices = voices.filter(v => v.lang.includes('zh'))
const speechText = extractSpeechText(text);
utterance = new SpeechSynthesisUtterance(speechText)
// utterance.lang = options.lang || 'zh'
utterance.rate = options.rate || 1
utterance.pitch = options.pitch || 1.1 // 音调0 - 2偏高比较柔和
utterance.onend = () => {
isSpeaking.value = false
isPaused.value = false
}
speechSynthesis.speak(utterance)
isSpeaking.value = true
isPaused.value = false
}
const pause = () => {
if (isSpeaking.value && !isPaused.value) {
speechSynthesis.pause()
isPaused.value = true
}
}
const resume = () => {
if (isSpeaking.value && isPaused.value) {
speechSynthesis.resume()
isPaused.value = false
}
}
const cancelAudio = () => {
speechSynthesis.cancel()
isSpeaking.value = false
isPaused.value = false
}
// 页面刷新/关闭时
onMounted(() => {
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', cancelAudio)
}
})
onBeforeUnmount(() => {
cancelAudio()
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', cancelAudio)
}
})
onHide(cancelAudio)
onUnload(cancelAudio)
return {
speak,
pause,
resume,
cancelAudio,
isSpeaking,
isPaused,
}
}
function extractSpeechText(markdown) {
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
const jobs = [];
let match;
let lastJobEndIndex = 0;
let firstJobStartIndex = -1;
// 提取岗位 json 数据及前后位置
while ((match = jobRegex.exec(markdown)) !== null) {
const jobStr = match[1];
try {
const job = JSON.parse(jobStr);
jobs.push(job);
if (firstJobStartIndex === -1) {
firstJobStartIndex = match.index;
}
lastJobEndIndex = jobRegex.lastIndex;
} catch (e) {
console.warn('JSON 解析失败', e);
}
}
// 提取引导语(第一个 job-json 之前的文字)
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() :
'';
// 提取结束语(最后一个 job-json 之后的文字)
const endingText = lastJobEndIndex < markdown.length ?
markdown.slice(lastJobEndIndex).trim() :
'';
// 岗位信息格式化为语音文本
const jobTexts = jobs.map((job, index) => {
return `${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}`;
});
// 拼接总语音内容
const finalTextParts = [];
if (guideText) finalTextParts.push(guideText);
finalTextParts.push(...jobTexts);
if (endingText) finalTextParts.push(endingText);
return finalTextParts.join('\n');
}

158
hook/useSystemPlayer.js Normal file
View File

@@ -0,0 +1,158 @@
import {
ref,
onUnmounted,
readonly
} from 'vue';
const defaultExtractSpeechText = (text) => text;
export function useTTSPlayer() {
const synth = window.speechSynthesis;
const isSpeaking = ref(false);
const isPaused = ref(false);
const utteranceRef = ref(null);
const cleanup = () => {
isSpeaking.value = false;
isPaused.value = false;
utteranceRef.value = null;
};
/**
* @param {string} text - The text to be spoken.
* @param {object} [options] - Optional settings for the speech.
* @param {string} [options.lang] - Language (e.g., 'en-US', 'es-ES').
* @param {number} [options.rate] - Speed (0.1 to 10, default 1).
* @param {number} [options.pitch] - Pitch (0 to 2, default 1).
* @param {SpeechSynthesisVoice} [options.voice] - A specific voice object.
* @param {function(string): string} [options.extractSpeechText] - A function to filter/clean the text before speaking.
*/
const speak = (text, options = {}) => {
if (!synth) {
console.error('SpeechSynthesis API is not supported in this browser.');
return;
}
if (isSpeaking.value) {
synth.cancel();
}
const filteredText = extractSpeechText(text);
if (!filteredText || typeof filteredText !== 'string' || filteredText.trim() === '') {
console.warn('Text to speak is empty after filtering.');
cleanup(); // Ensure state is clean
return;
}
const newUtterance = new SpeechSynthesisUtterance(filteredText); // Use filtered text
utteranceRef.value = newUtterance;
newUtterance.lang = 'zh-CN';
newUtterance.rate = options.rate || 1;
newUtterance.pitch = options.pitch || 1;
if (options.voice) {
newUtterance.voice = options.voice;
}
newUtterance.onstart = () => {
isSpeaking.value = true;
isPaused.value = false;
};
newUtterance.onpause = () => {
isPaused.value = true;
};
newUtterance.onresume = () => {
isPaused.value = false;
};
newUtterance.onend = () => {
cleanup();
};
newUtterance.onerror = (event) => {
console.error('SpeechSynthesis Error:', event.error);
cleanup();
};
synth.speak(newUtterance);
};
const pause = () => {
if (synth && isSpeaking.value && !isPaused.value) {
synth.pause();
}
};
const resume = () => {
if (synth && isPaused.value) {
synth.resume();
}
};
const cancelAudio = () => {
if (synth) {
synth.cancel();
}
cleanup();
};
onUnmounted(() => {
cancelAudio();
});
return {
speak,
pause,
resume,
cancelAudio,
isSpeaking: readonly(isSpeaking),
isPaused: readonly(isPaused),
};
}
function extractSpeechText(markdown) {
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
const jobs = [];
let match;
let lastJobEndIndex = 0;
let firstJobStartIndex = -1;
// 提取岗位 json 数据及前后位置
while ((match = jobRegex.exec(markdown)) !== null) {
const jobStr = match[1];
try {
const job = JSON.parse(jobStr);
jobs.push(job);
if (firstJobStartIndex === -1) {
firstJobStartIndex = match.index;
}
lastJobEndIndex = jobRegex.lastIndex;
} catch (e) {
console.warn('JSON 解析失败', e);
}
}
// 提取引导语(第一个 job-json 之前的文字)
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() :
'';
// 提取结束语(最后一个 job-json 之后的文字)
const endingText = lastJobEndIndex < markdown.length ?
markdown.slice(lastJobEndIndex).trim() :
'';
// 岗位信息格式化为语音文本
const jobTexts = jobs.map((job, index) => {
return `${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}`;
});
// 拼接总语音内容
const finalTextParts = [];
if (guideText) finalTextParts.push(guideText);
finalTextParts.push(...jobTexts);
if (endingText) finalTextParts.push(endingText);
return finalTextParts.join('\n');
}

View File

@@ -0,0 +1,203 @@
import {
ref,
readonly,
onUnmounted
} from 'vue';
// 检查 API 兼容性
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const isApiSupported = !!SpeechRecognition && !!navigator.mediaDevices && !!window.AudioContext;
/**
* @param {object} [options]
* @param {string} [options.lang] - Language code (e.g., 'zh-CN', 'en-US')
* @returns {object}
*/
export function useAudioRecorder(options = {}) {
const lang = options.lang || 'zh-CN'; // 默认使用中文
const isRecording = ref(false);
const recognizedText = ref(''); // 完整的识别文本(包含临时的)
const lastFinalText = ref(''); // 最后一段已确定的文本
const volumeLevel = ref(0); // 音量 (0-100)
const audioDataForDisplay = ref(new Uint8Array()); // 波形数据
let recognition = null;
let audioContext = null;
let analyser = null;
let mediaStreamSource = null;
let mediaStream = null;
let dataArray = null; // 用于音量和波形
let animationFrameId = null;
if (!isApiSupported) {
console.warn(
'此浏览器不支持Web语音API或Web音频API。钩子无法正常工作。'
);
return {
isRecording: readonly(isRecording),
startRecording: () => console.error('Audio recording not supported.'),
stopRecording: () => {},
cancelRecording: () => {},
audioDataForDisplay: readonly(audioDataForDisplay),
volumeLevel: readonly(volumeLevel),
recognizedText: readonly(recognizedText),
lastFinalText: readonly(lastFinalText),
};
}
const setupRecognition = () => {
recognition = new SpeechRecognition();
recognition.lang = lang;
recognition.continuous = true; // 持续识别
recognition.interimResults = true; // 返回临时结果
recognition.onstart = () => {
isRecording.value = true;
};
recognition.onend = () => {
isRecording.value = false;
stopAudioAnalysis(); // 语音识别停止时,也停止音频分析
};
recognition.onerror = (event) => {
console.error('SpeechRecognition Error:', event.error);
isRecording.value = false;
stopAudioAnalysis();
};
recognition.onresult = (event) => {
let interim = '';
let final = '';
for (let i = 0; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
final += transcript;
lastFinalText.value = transcript; // 存储最后一段确定的文本
} else {
interim += transcript;
}
}
recognizedText.value = final + interim; // 组合为完整文本
};
};
const startAudioAnalysis = async () => {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: true
});
audioContext = new AudioContext();
analyser = audioContext.createAnalyser();
mediaStreamSource = audioContext.createMediaStreamSource(mediaStream);
// 设置 Analyser
analyser.fftSize = 512; // 必须是 2 的幂
const bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength); // 用于波形
// 连接节点
mediaStreamSource.connect(analyser);
// 开始循环分析
updateAudioData();
} catch (err) {
console.error('Failed to get media stream or setup AudioContext:', err);
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
alert('麦克风权限被拒绝。请在浏览器设置中允许访问麦克风。');
}
}
};
const updateAudioData = () => {
if (!isRecording.value) return; // 如果停止了就退出循环
// 获取时域数据 (波形)
analyser.getByteTimeDomainData(dataArray);
audioDataForDisplay.value = new Uint8Array(dataArray); // 复制数组以触发响应式
// 计算音量 (RMS)
let sumSquares = 0.0;
for (const amplitude of dataArray) {
const normalized = (amplitude / 128.0) - 1.0; // 转换为 -1.0 到 1.0
sumSquares += normalized * normalized;
}
const rms = Math.sqrt(sumSquares / dataArray.length);
volumeLevel.value = Math.min(100, Math.floor(rms * 250)); // 放大 RMS 值到 0-100 范围
animationFrameId = requestAnimationFrame(updateAudioData);
};
const stopAudioAnalysis = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 停止麦克风轨道
mediaStream?.getTracks().forEach((track) => track.stop());
// 关闭 AudioContext
audioContext?.close().catch((e) => console.error('Error closing AudioContext', e));
mediaStream = null;
audioContext = null;
analyser = null;
mediaStreamSource = null;
volumeLevel.value = 0;
audioDataForDisplay.value = new Uint8Array();
};
const startRecording = async () => {
if (isRecording.value) return;
// 重置状态
recognizedText.value = '';
lastFinalText.value = '';
try {
// 必须先启动音频分析以获取麦克风权限
await startAudioAnalysis();
// 如果音频启动成功 (mediaStream 存在),则启动语音识别
if (mediaStream) {
setupRecognition();
recognition.start();
}
} catch (error) {
console.error("Error starting recording:", error);
}
};
const stopRecording = () => {
if (!isRecording.value || !recognition) return;
recognition.stop(); // 这将触发 onend 事件,自动停止音频分析
};
const cancelRecording = () => {
if (!recognition) return;
isRecording.value = false; // 立即设置状态
recognition.abort(); // 这也会触发 onend
recognizedText.value = '';
lastFinalText.value = '';
};
onUnmounted(() => {
if (recognition) {
recognition.abort();
}
stopAudioAnalysis();
});
return {
isRecording: readonly(isRecording),
startRecording,
stopRecording,
cancelRecording,
audioDataForDisplay: readonly(audioDataForDisplay),
volumeLevel: readonly(volumeLevel),
recognizedText: readonly(recognizedText),
lastFinalText: readonly(lastFinalText),
isApiSupported, // 导出支持状态
};
}

View File

@@ -17,12 +17,22 @@
})();
</script>
<title></title>
<!-- vconsole -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<!-- eruda -->
<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
var vConsole = new window.VConsole();
vConsole.destroy();
eruda.init();
</script> -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
</script> -->
<!-- 爱山东jssdk 本sdk存在性能问题 -->
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
<!-- 只在内网有效 -->
<script type="text/javascript" src="./static/js/SM.js"></script>
<script type="text/javascript" src="./static/js/pixi.min.js"></script>
</head>
<body>
<div id="app"><!--app-html--></div>

BIN
lib/.DS_Store vendored

Binary file not shown.

7
lib/encryption/sm2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
lib/encryption/sm4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

22
main.js
View File

@@ -1,8 +1,12 @@
import App from '@/App'
import * as Pinia from 'pinia'
import {
createUnistorage
} from "./uni_modules/pinia-plugin-unistorage";
import globalFunction from '@/common/globalFunction'
import '@/lib/string-similarity.min.js'
import similarityJobs from '@/utils/similarity_Job.js';
// 组件
import AppLayout from './components/AppLayout/AppLayout.vue';
import Empty from './components/empty/empty.vue';
@@ -12,8 +16,15 @@ 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 RenderJobsOutData from '@/components/renderJobsOutData/renderJobsOutData.vue';
import RenderCompanysOutData from '@/components/renderCompanysOutData/renderCompanysOutData.vue';
import renderDeliveryRecord from '@/components/renderDeliveryRecord/renderDeliveryRecord.vue';
import renderJobCollectionRecord from '@/components/renderJobCollectionRecord/renderJobCollectionRecord.vue';
import renderCompanyCollectionRecord from '@/components/renderCompanyCollectionRecord/renderCompanyCollectionRecord.vue';
import renderJobViewRecord from '@/components/renderJobViewRecord/renderJobViewRecord.vue';
// import Tabbar from '@/components/tabbar/midell-box.vue'
// 自动导入 directives 目录下所有指令
console.log(lightAppJssdk)
const directives = import.meta.glob('./directives/*.js', {
eager: true
});
@@ -36,6 +47,12 @@ export function createApp() {
app.component('SelectPopup', SelectPopup)
app.component('RenderJobs', RenderJobs)
app.component('RenderCompanys', RenderCompanys)
app.component('RenderJobsOutData', RenderJobsOutData) //渲染外部岗位数据列表
app.component('RenderCompanysOutData', RenderCompanysOutData) //渲染外部公司数据列表
app.component('renderDeliveryRecord', renderDeliveryRecord) //渲染岗位投递记录
app.component('renderJobCollectionRecord', renderJobCollectionRecord) //渲染岗位收藏记录
app.component('renderCompanyCollectionRecord', renderCompanyCollectionRecord) //渲染公司收藏记录
app.component('renderJobViewRecord', renderJobViewRecord) //渲染岗位浏览记录
// app.component('tabbar-custom', Tabbar)
for (const path in directives) {
@@ -52,7 +69,10 @@ export function createApp() {
app.provide('deviceInfo', globalFunction.getdeviceInfo());
app.use(SelectPopupPlugin);
app.use(Pinia.createPinia());
const store = Pinia.createPinia();
store.use(createUnistorage());
app.use(store);
return {
app,

View File

@@ -97,6 +97,9 @@
"serviceHost": ""
}
}
},
"devServer": {
"https": false
}
}
}

View File

@@ -1,13 +1,13 @@
<template>
<view class="collection-content">
<renderJobs
seeDate="applyTime"
<renderDeliveryRecord
v-if="pageState.list.length"
seeDate="applyTime"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
></renderDeliveryRecord>
<empty v-else :is-position="true"></empty>
</view>
</template>
@@ -41,10 +41,6 @@ onReachBottom(() => {
getJobList();
});
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
}
function getJobList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
@@ -57,7 +53,7 @@ function getJobList(type = 'add') {
current: pageState.page,
pageSize: pageState.pageSize,
};
$api.createRequest('/app/user/apply/job', params).then((resData) => {
const LoadCache = (resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
@@ -70,8 +66,9 @@ function getJobList(type = 'add') {
// pageState.list = resData.rows;
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
console.log(pageState.list);
});
};
$api.createRequestWithCache('/app/user/apply/job', params, 'GET', false, {}, LoadCache).then(LoadCache);
}
</script>
@@ -79,7 +76,8 @@ function getJobList(type = 'add') {
.collection-content{
padding: 1rpx 28rpx 20rpx 28rpx;
background: #F4F4F4;
height: 100%
height: 100%;
min-height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
position: relative;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btn">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -21,7 +21,7 @@
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
<view class="companyinfo-right">
<view class="row1">{{ companyInfo?.name }}</view>
<view class="row1">{{ dataType === 2 ? companyInfo?.gsmc : companyInfo?.name }}</view>
<view class="row2">
<dict-tree-Label
v-if="companyInfo?.industry"
@@ -30,14 +30,13 @@
></dict-tree-Label>
<span v-if="companyInfo?.industry">&nbsp;</span>
<dict-Label dictType="scale" :value="companyInfo?.scale"></dict-Label>
<span v-if="dataType === 2">{{ companyInfo.gsxy }}</span>
</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 class="info-desirption">{{ dataType === 2 ? companyInfo.qyxz : companyInfo.description }}</view>
</view>
<view class="expand" @click="expand">
<text>{{ isExpanded ? '收起' : '展开' }}</text>
@@ -50,13 +49,20 @@
<scroll-view scroll-y class="Detailscroll-view" @scrolltolower="getJobsList('add')">
<view class="views">
<view class="Detail-title"><text class="title">在招职位</text></view>
<!-- 根据 dataType 使用不同的职位列表组件 -->
<renderJobsOutData
v-if="dataType === 2 && pageState.list.length"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobsOutData>
<renderJobs
v-if="pageState.list.length"
v-else-if="dataType !== 2 && pageState.list.length"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
<empty v-else :is-position="true"></empty>
</view>
</scroll-view>
</view>
@@ -75,6 +81,7 @@ const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
const isExpanded = ref(false);
const pageState = reactive({
current: 0,
page: 0,
list: [],
total: 0,
@@ -82,35 +89,113 @@ const pageState = reactive({
pageSize: 10,
});
const companyInfo = ref({});
const pageOptions = ref({});
const dataType = ref(1); // 1: 原数据, 2: 第三方数据
onLoad((options) => {
console.log(options);
getCompanyInfo(options.companyId || options.bussinessId);
// console.log(options);
dataType.value = options.dataType ? parseInt(options.dataType) : 1;
pageOptions.value = options;
if (dataType.value === 2) {
// 第三方数据
getCompanyInfo(options.companyId, options.zphId);
getJobsList('refresh');
} else {
// 原数据
getCompanyInfo(options.companyId || options.bussinessId);
}
});
function companyCollection() {
const companyId = companyInfo.value.companyId;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId);
$api.msg('取消收藏成功');
if (dataType.value === 2) {
// 第三方数据收藏逻辑
const id = companyInfo.value.id;
const companyId = companyInfo.value.gsID;
const zphId = companyInfo.value.zphID;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${id}/2`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId, zphId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/company/collection/${id}/2`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId, zphId);
$api.msg('收藏成功');
});
}
} else {
// 原数据收藏逻辑
const companyId = companyInfo.value.companyId;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId);
$api.msg('收藏成功');
});
}
}
}
function getCompanyInfo(...args) {
if (dataType.value === 2) {
// 第三方数据接口
const [companyId, zphId] = args;
$api.createRequest(`/app/internal/companyThirdPart/${companyId}/${zphId}`, {}, 'GET', true).then((resData) => {
companyInfo.value = resData.data;
});
} else {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId);
$api.msg('收藏成功');
// 原数据接口
const [companyId] = args;
$api.createRequest(`/app/company/${companyId}`, {}, 'GET', true).then((resData) => {
companyInfo.value = resData.data;
getJobsList();
});
}
}
function getCompanyInfo(id) {
$api.createRequest(`/app/company/${id}`).then((resData) => {
companyInfo.value = resData.data;
getJobsList();
function getJobsList(type = 'add') {
if (dataType.value === 2) {
// 第三方数据职位列表
getThirdPartyJobsList(type);
} else {
// 原数据职位列表
getOriginalJobsList(type);
}
}
function getThirdPartyJobsList(type = 'add') {
const { companyId, companyName, zphId } = pageOptions.value;
if (type === 'refresh') {
pageState.current = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.current < pageState.maxPage) {
pageState.current += 1;
}
let params = {
current: pageState.current,
pageSize: pageState.pageSize,
};
$api.createRequest(
`/app/internal/jobThirdPart?gsID=${companyId}&gsmc=${companyName}&zphID=${zphId}`,
params,
'GET',
true
).then((resData) => {
const { rows, total } = resData;
handleJobsListResponse(type, rows, total, 'current');
});
}
function getJobsList(type = 'add') {
function getOriginalJobsList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
@@ -118,37 +203,47 @@ function getJobsList(type = 'add') {
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) => {
$api.createRequest(`/app/company/job/${companyInfo.value.companyId}`, params, 'GET', true).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);
handleJobsListResponse(type, rows, total, 'page');
});
}
function handleJobsListResponse(type, rows, total, pageKey) {
if (type === 'add') {
const str = pageState.pageSize * (pageState[pageKey] - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}
pageState.total = total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
}
function expand() {
isExpanded.value = !isExpanded.value;
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
@@ -170,7 +265,6 @@ image {
margin-right: 24rpx
}
.companyinfo-right{
.row1{
font-weight: 500;
font-size: 32rpx;
@@ -206,7 +300,7 @@ image {
}
}
.expanded {
max-height: 1000rpx; // 足够显示完整内容
max-height: 1000rpx;
}
.expand{
display: flex
@@ -233,6 +327,8 @@ image {
background: #F4F4F4;
.views{
padding: 28rpx
min-height: calc(100% - 56rpx)
position: relative
.Detail-title{
font-weight: 600;
font-size: 32rpx;
@@ -281,7 +377,7 @@ image {
}
.card-companyName{
font-weight: 400;
font-size: 28rpx;
font-size: 28rpx;
color: #6C7282;
}
.card-tags{

View File

@@ -12,11 +12,33 @@
<view class="lf-text">选择想找的工作我的将在首页为你推荐</view>
</view>
<view class="title-ri">
<!-- <text style="color: #256bfa">2</text>
<!-- <text style="color: #256bfa">2</text>
<text>/2</text> -->
</view>
</view>
<view class="content-list">
<view class="list-search">
<uni-icons type="search" color="#333333" size="24"></uni-icons>
<input
class="search-input"
v-model="inputVal"
placeholder="请输入岗位名称"
@input="handelChangeInpute"
@blur="handleBlur"
@focus="handleFocus"
/>
<view class="search-container" v-show="filterList.length">
<view
class="list-item"
v-for="(item, index) in filterList"
@click="handelClickItem(item)"
:key="item.id"
>
<view class="item-txt line_1">{{ item.lable }}</view>
</view>
</view>
</view>
<view class="list-row" v-for="(item, index) in userInfo.jobTitle" :key="index">
<text>{{ item }}</text>
<image
@@ -31,18 +53,85 @@
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
</AppLayout>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
mode="base"
title="确定添加该期望岗位吗"
type="info"
:duration="2000"
:before-close="true"
@confirm="confirm"
@close="close"
></uni-popup-dialog>
</uni-popup>
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
const { $api, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { getUserResume } = useUserStore();
const { userInfo } = storeToRefs(useUserStore());
const popup = ref(null);
const selectJobsModel = ref(null);
const treeDataList = ref([]);
const dataSource = ref([]);
const filterList = ref([]);
const dataItem = ref(null);
const inputVal = ref('');
onLoad(() => {
getTree();
});
function close() {
popup.value.close();
}
function handleBlur() {
setTimeout(() => {
filterList.value = [];
}, 100);
}
function handleFocus() {
const val = inputVal.value.toLowerCase();
if (val && dataSource.value) {
filterList.value = dataSource.value.filter((item) => item.lable.toLowerCase().search(val) !== -1);
} else {
filterList.value = [];
}
}
function confirm() {
const { id } = dataItem.value;
let ids = userInfo.value.jobTitleId + `,${id}`;
const result = dedupeAndCheck(ids);
if (result.hasDuplicate) {
popup.value.close();
$api.msg('期望岗位已重复');
return;
}
complete({ jobTitleId: result.deduplicated });
inputVal.value = '';
popup.value.close();
}
function dedupeAndCheck(str) {
const items = str.split(',').map((s) => s.trim());
const uniqueItems = [...new Set(items)];
const hasDuplicate = uniqueItems.length !== items.length;
return {
deduplicated: uniqueItems.join(','),
hasDuplicate,
};
}
function deleteItem(item, index) {
const ids = userInfo.value.jobTitleId
.split(',')
@@ -55,12 +144,27 @@ function changeJobs() {
selectJobsModel.value?.open({
title: '添加岗位',
maskClick: true,
data: treeDataList.value,
success: (ids, labels) => {
complete({ jobTitleId: ids });
},
});
}
function handelChangeInpute(e) {
const val = e.detail.value.toLowerCase();
if (val && dataSource.value) {
filterList.value = dataSource.value.filter((item) => item.lable.toLowerCase().search(val) !== -1);
} else {
filterList.value = [];
}
}
function handelClickItem(item) {
dataItem.value = item;
popup.value.open();
}
function complete(values) {
if (!values.jobTitleId.length) {
return $api.msg('至少添加一份期望岗位');
@@ -70,9 +174,82 @@ function complete(values) {
getUserResume();
});
}
function getTree() {
const LoadCache = (resData) => {
if (resData.code === 200) {
dataSource.value = flattenTree(resData.data);
treeDataList.value = resData.data;
}
};
$api.createRequestWithCache('/app/common/jobTitle/treeselect', {}, 'GET', false, LoadCache).then(LoadCache);
}
function flattenTree(treeData, parentPath = '') {
let result = [];
treeData.forEach((node) => {
const currentName = node.lable || node.label;
const fullPath = parentPath ? `${parentPath}-${currentName}` : currentName;
const children = node.children || node.chidren;
if (children && children.length > 0) {
result = result.concat(flattenTree(children, fullPath));
} else {
result.push({
id: node.id,
lable: fullPath,
currentName,
});
}
});
return result;
}
</script>
<style lang="stylus" scoped>
.list-search{
height: 76rpx;
background: #FFFFFF;
border-radius: 8rpx;
border: 1px solid #ECECEC;
margin-bottom: 24rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
position: relative;
.search-input{
flex: 1;
padding: 0 20rpx;
font-size: 28rpx;
color: #6C7282;
border: none;
outline: none;
background: transparent;
}
.search-container{
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #FFFFFF;
border: 2rpx solid #ECECEC;
border-top: 0;
z-index: 1;
max-height: 30vh;
overflow: hidden;
.list-item{
height: 80rpx
padding: 0rpx 24rpx;
display: flex;
align-items: center;
justify-items: flex-start;
border-top: 2rpx dashed #e3e3e3;
}
.list-item:hover{
background: #e5e5e5
}
}
}
.btn {
display: flex;
justify-content: space-between;

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="我的浏览" :show-bg-image="false" :use-scroll-view="false">
<template #headerleft>
<view class="btn">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -24,16 +24,14 @@
</view>
<scroll-view scroll-y class="main-scroll" @scrolltolower="getJobList('add')">
<view class="one-cards">
<view class="mian">
<renderJobs
:list="pageState.list"
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
<!-- <loadmore ref="loadmoreRef"></loadmore> -->
</view>
<renderJobViewRecord
:list="pageState.list"
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobViewRecord>
<empty v-else></empty>
<!-- <loadmore ref="loadmoreRef"></loadmore> -->
</view>
</scroll-view>
</view>
@@ -88,10 +86,6 @@ function toSelectDate() {
});
}
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
}
function searchCollection(e) {
const value = e.detail.value;
pageState.search.jobTitle = value;
@@ -152,12 +146,16 @@ function getPreviousDay(dateStr) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
@@ -166,9 +164,12 @@ image {
.collection-content
height: 100%
display: flex
flex-direction: column
flex-direction: column;
background: #f4f4f4
.collection-search
padding: 10rpx 20rpx;
background: #FFFFFF;
.search-content
position: relative
@@ -215,6 +216,6 @@ image {
.one-cards{
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
height: 100%
}
</style>
</style>

View File

@@ -50,10 +50,11 @@ function delCollectionCard(item) {
}
function getPremiumList() {
$api.createRequest('/app/company/card').then((resData) => {
const LoadCache = (resData) => {
const { rows, total } = resData;
list.value = rows;
});
};
$api.createRequestWithCache('/app/company/card', {}, 'GET', false, {}, LoadCache).then(LoadCache);
}
function seeDetail(item) {
@@ -88,6 +89,7 @@ function seeDetail(item) {
font-weight: 600;
font-size: 32rpx;
color: #333333;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
.card-text{
margin-top: 16rpx

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout :title="title" :show-bg-image="false" @onScrollBottom="getDataList('add')">
<template #headerleft>
<view class="btn">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -27,7 +27,7 @@
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanys>
<empty v-else pdTop="200"></empty>
<empty v-else is-position></empty>
</view>
</AppLayout>
</template>
@@ -101,12 +101,16 @@ function getDataList(type = 'add') {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
@@ -147,6 +151,8 @@ image {
}
.main-list{
background-color: #F4F4F4;
padding: 1rpx 28rpx 28rpx 28rpx
padding: 1rpx 28rpx 28rpx 28rpx;
min-height: calc(100% - 29rpx);
position: relative
}
</style>

View File

@@ -11,30 +11,40 @@
<view class="button-click" :class="{ active: type === 1 }" @click="changeType(1)">公司企业</view>
</view>
<view class="coll-main">
<swiper class="swiper" :current="type" @change="changeSwiperType">
<swiper-item class="list">
<swiper class="swiper" :disable-touch="disableTouch" :current="type" @change="changeSwiperType">
<swiper-item
class="list"
@touchstart.passive="handleTouchStart"
@touchmove.passive="handleTouchMove"
@touchend="disableTouch = false"
>
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<view class="mian">
<renderJobs
:list="pageState.list"
<renderJobCollectionRecord
v-if="pageState.list.length"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
></renderJobCollectionRecord>
<empty v-else></empty>
</view>
</scroll-view>
</swiper-item>
<swiper-item class="list">
<swiper-item
class="list"
@touchstart.passive="handleTouchStart"
@touchmove.passive="handleTouchMove"
@touchend="disableTouch = false"
>
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLowerCompany">
<view class="mian">
<renderCompanys
<renderCompanyCollectionRecord
:list="pageCompanyState.list"
v-if="pageCompanyState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanys>
<empty v-else pdTop="200"></empty>
></renderCompanyCollectionRecord>
<empty v-else></empty>
</view>
</scroll-view>
</swiper-item>
@@ -69,14 +79,79 @@ const pageCompanyState = reactive({
pageSize: 10,
});
const disableTouch = ref(false);
const startPointX = ref(0);
const totalPage = 2;
const THRESHOLD = 5;
onShow(() => {
getJobList();
getCompanyList();
});
function handleTouchStart(e) {
// 确保有触摸点
if (e.touches.length > 0) {
startPointX.value = e.touches[0].clientX;
disableTouch.value = false;
}
}
function handleTouchMove(e) {
if (e.touches.length === 0) return;
const currentX = e.touches[0].clientX;
const diffX = currentX - startPointX.value;
if (type.value === 0) {
if (diffX > THRESHOLD) {
disableTouch.value = true;
} else {
disableTouch.value = false;
}
return;
}
if (type.value === totalPage - 1) {
if (diffX < -THRESHOLD) {
disableTouch.value = true;
} else {
disableTouch.value = false;
}
return;
}
disableTouch.value = false;
}
function changeSwiperType(e) {
const newIndex = e.detail.current;
const lastIndex = type.value;
const isSwipingRight = newIndex < lastIndex;
const isSwipingLeft = newIndex > lastIndex;
if (lastIndex === 0 && isSwipingRight) {
disableTouch.value = true;
type.value = 0;
setTimeout(() => {
disableTouch.value = false;
}, 50);
return;
}
if (lastIndex === totalPage - 1 && isSwipingLeft) {
disableTouch.value = true;
type.value = lastIndex;
setTimeout(() => {
disableTouch.value = false;
}, 50);
return;
}
const current = e.detail.current;
type.value = current;
disableTouch.value = false;
}
function changeType(e) {
@@ -103,7 +178,8 @@ function getJobList(type = 'add') {
current: pageState.page,
pageSize: pageState.pageSize,
};
$api.createRequest('/app/user/collection/job', params).then((resData) => {
const LoadCache = (resData) => {
console.log(resData);
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
@@ -116,7 +192,8 @@ function getJobList(type = 'add') {
// pageState.list = resData.rows;
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
});
};
$api.createRequestWithCache('/app/user/collection/job', params, 'GET', false, {}, LoadCache).then(LoadCache);
}
function getCompanyList(type = 'add') {
@@ -131,7 +208,7 @@ function getCompanyList(type = 'add') {
current: pageCompanyState.page,
pageSize: pageCompanyState.pageSize,
};
$api.createRequest('/app/user/collection/company', params).then((resData) => {
const LoadCache = (resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageCompanyState.pageSize * (pageCompanyState.page - 1);
@@ -144,7 +221,8 @@ function getCompanyList(type = 'add') {
// pageCompanyState.list = resData.rows;
pageCompanyState.total = resData.total;
pageCompanyState.maxPage = Math.ceil(pageCompanyState.total / pageCompanyState.pageSize);
});
};
$api.createRequestWithCache('/app/user/collection/company', params, 'GET', false, {}, LoadCache).then(LoadCache);
}
</script>
@@ -187,6 +265,7 @@ function getCompanyList(type = 'add') {
.swiper{
height: 100%
.mian{
height: 100%
padding: 0 28rpx 28rpx 28rpx
}
}

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btn">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -11,15 +11,15 @@
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
<view class="companyinfo-right">
<view class="row1 line_2">{{ fairInfo?.name }}</view>
<view class="row1 line_2" @tap="$api.copyText(fairInfo.zphmc)">{{ fairInfo?.zphmc }}</view>
<view class="row2">
<text>{{ fairInfo.location }}</text>
<convert-distance
<text @tap="$api.copyText(fairInfo.jbf)">{{ fairInfo.jbf }}</text>
<!-- <convert-distance
:alat="fairInfo.latitude"
:along="fairInfo.longitude"
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance>
></convert-distance> -->
</view>
</view>
</view>
@@ -27,36 +27,38 @@
<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>
<text class="info-title line_1" @tap="$api.copyText(fairInfo.zphdz)">
{{ fairInfo.zphdz }}
</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-desirption">{{ fairInfo.zphjj }}</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 class="left-date">{{ parseDateTime(fairInfo.zphjbsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(fairInfo.zphjbsj).date }}</view>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(fairInfo.startTime, fairInfo.endTime).statusText }}
{{ getTimeStatus(fairInfo.zphjbsj, fairInfo.zphjzsj).statusText }}
</view>
<view class="center-dateDay">
{{ getHoursBetween(fairInfo.startTime, fairInfo.endTime) }}小时
{{ getHoursBetween(fairInfo.zphjbsj, fairInfo.zphjzsj) }}小时
</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 class="left-date">{{ parseDateTime(fairInfo.zphjzsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(fairInfo.zphjzsj).date }}</view>
</view>
</view>
</view>
@@ -69,18 +71,19 @@
src="@/static/icon/downs.png"
></image>
</view>
<scroll-view scroll-y class="Detailscroll-view">
<scroll-view scroll-y class="Detailscroll-view" @scrolltolower="getCompanyList('add')">
<view class="views">
<view class="Detail-title">
<text class="title">参会单位{{ companyList.length }}</text>
<text class="title">参会单位{{ pageState.total }}</text>
</view>
<renderCompanys
v-if="companyList.length"
:list="companyList"
<renderCompanysOutData
v-if="pageState.list.length"
:zphId="zphId"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanys>
<empty v-else pdTop="200"></empty>
></renderCompanysOutData>
<empty v-else is-position></empty>
</view>
</scroll-view>
</view>
@@ -110,28 +113,74 @@ const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const isExpanded = ref(false);
const fairInfo = ref({});
const companyList = ref([]);
const pageState = reactive({
current: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 10,
});
const hasnext = ref(true);
const zphId = ref('');
const pageOptions = ref({});
onLoad((options) => {
getCompanyInfo(options.jobFairId);
zphId.value = options.jobFairId;
pageOptions.value = options;
getJobFairInfo(options.jobFairId, options.jobFairName);
getCompanyList('refresh');
});
function getCompanyInfo(id) {
$api.createRequest(`/app/fair/${id}`).then((resData) => {
function getJobFairInfo(id, name) {
$api.createRequest(`/app/internal/jobFairThirdPart/${id}`, {}, 'GET', true).then((resData) => {
fairInfo.value = resData.data;
companyList.value = resData.data.companyList;
hasAppointment();
});
}
function getCompanyList(type = 'add') {
const { jobFairId, jobFairName } = pageOptions.value;
if (type === 'refresh') {
pageState.current = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.current < pageState.maxPage) {
pageState.current += 1;
}
let params = {
current: pageState.current,
pageSize: pageState.pageSize,
};
$api.createRequest(
`/app/internal/companyThirdPart/?zphID=${jobFairId}&zphmc=${jobFairName}`,
params,
'GET',
true
).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.current - 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);
});
}
const hasAppointment = () => {
const isTimePassed = (timeStr) => {
if (!timeStr) return false;
const targetTime = new Date(timeStr.replace(/-/g, '/')).getTime(); // 兼容格式
const now = Date.now();
return now < targetTime;
};
hasnext.value = isTimePassed(fairInfo.value.startTime);
hasnext.value = isTimePassed(fairInfo.value.zphjbsj);
};
function openMap(lat, lng, name = '位置') {
@@ -149,16 +198,16 @@ function expand() {
// 取消/收藏岗位
function applyExhibitors() {
const fairId = fairInfo.value.jobFairId;
const fairId = fairInfo.value.zphID;
if (fairInfo.value.isCollection) {
// $api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
// getCompanyInfo(fairId);
// $api.msg('取消预约成功');
// });
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
getJobFairInfo(fairId);
$api.msg('取消预约成功');
});
$api.msg('已预约成功');
} else {
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'POST').then((resData) => {
getCompanyInfo(fairId);
getJobFairInfo(fairId);
$api.msg('预约成功');
});
}
@@ -219,12 +268,16 @@ function getHoursBetween(startTimeStr, endTimeStr) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
@@ -251,6 +304,7 @@ image {
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;
@@ -382,6 +436,8 @@ image {
background: #F4F4F4;
.views{
padding: 28rpx
min-height: calc(100% - 56rpx);
position: relative
.Detail-title{
font-weight: 600;
font-size: 32rpx;

View File

@@ -145,10 +145,17 @@ function changeArea() {
function changeJobs() {
selectJobsModel.value?.open({
title: '添加岗位',
defaultId: fromValue.jobTitleId,
success: (ids, labels) => {
console.log(ids, labels);
fromValue.jobTitleId = ids;
state.jobsText = labels.split(',');
},
cancel: (ids, labels) => {
console.log(ids, labels);
// fromValue.jobTitleId = ids;
// state.jobsText = labels.split(',');
},
});
}
@@ -199,6 +206,9 @@ function getFormCompletionPercent(form) {
height: 80rpx;
border-bottom: 2rpx solid #EBEBEB
position: relative;
.triangle {
pointer-events: none;
}
.triangle::before
position: absolute;
right: 20rpx;
@@ -262,10 +272,9 @@ function getFormCompletionPercent(form) {
display: flex
flex-wrap: wrap
.nx-item
padding: 20rpx 28rpx
width: fit-content
margin: 12rpx 12rpx 0 0;
padding: 12rpx 25rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E8EAEE;
margin-right: 24rpx
margin-top: 24rpx
</style>

View File

@@ -0,0 +1,73 @@
<template>
<view class="collection-content">
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</template>
<script setup>
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, navBack, vacanciesTo } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import { usePagination } from '@/hook/usePagination';
import { jobMoreMap } from '@/utils/markdownParser';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const loadmoreRef = ref(null);
// 响应式搜索条件(可以被修改)
const searchParams = ref({});
const pageSize = ref(10);
const { list, loading, refresh, loadMore } = usePagination(
(params) => $api.createRequest('/app/job/list', params, 'GET', true),
null, // 转换函数
{
pageSize: pageSize,
search: searchParams,
autoWatchSearch: true,
onBeforeRequest: () => {
loadmoreRef.value?.change('loading');
},
onAfterRequest: () => {
loadmoreRef.value?.change('more');
},
}
);
onLoad((options) => {
let params = jobMoreMap.get(options.jobId);
if (params) {
uni.setStorageSync('jobMoreMap', params);
} else {
params = uni.getStorageSync('jobMoreMap');
}
const objs = removeNullProperties(params);
searchParams.value = objs;
refresh();
});
function removeNullProperties(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === null) {
delete obj[key]; // 删除值为 null 的属性
}
}
return obj;
}
onReachBottom(() => {
loadMore();
});
</script>
<style lang="stylus" scoped>
.collection-content{
padding: 1rpx 28rpx 20rpx 28rpx;
background: #F4F4F4;
height: 100%
min-height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
}
</style>

View File

@@ -1,75 +1,138 @@
<template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="mys-tops btn-feel">
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || '编辑用户名' }}</text>
<view class="edit-icon mar_le10">
<image
class="button-click"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/personalInfo/personalInfo')"
></image>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view
v-if="userInfo.resumeOcrStatus && showNotice"
class="notice-line"
:class="userInfo.resumeOcrStatus?.includes('成功') ? 'green' : 'blue'"
>
<image
v-if="userInfo.resumeOcrStatus?.includes('成功')"
class="icon"
src="@/static/icon/notice-green.png"
/>
<image v-else class="icon" src="@/static/icon/notice-blue.png" />
<view class="text">{{ userInfo.resumeOcrStatus }}</view>
<image
@click="closeNotice"
v-if="userInfo.resumeOcrStatus?.includes('成功')"
class="close"
src="@/static/icon/close-green.png"
/>
<image @click="closeNotice" v-else class="close" src="@/static/icon/close-blue.png" />
</view>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top" style="margin-top: 12rpx; padding: 0; background: none">
<view class="mys-tops">
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || '编辑用户名' }}</text>
<view class="edit-icon mar_le10">
<image
class="button-click"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/personalInfo/personalInfo')"
></image>
</view>
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label
class="mar_ri10"
dictType="affiliation"
:value="userInfo.politicalAffiliation"
></dict-Label>
</view>
<view class="subName">{{ userInfo.phone }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="right-sex">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy1.png"></image>
<image v-else src="@/static/icon/girl1.png"></image>
</view>
</view>
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label
class="mar_ri10"
dictType="affiliation"
:value="userInfo.politicalAffiliation"
></dict-Label>
<!-- 求职期望 -->
<view class="mys-line">
<view class="line"></view>
</view>
<view class="mys-info">
<view class="mys-h4">
<view>求职期望</view>
<image
class="icon"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/jobExpect/jobExpect')"
></image>
</view>
<view class="mys-text">
<text>期望薪资</text>
<text>{{ userInfo.salaryMin / 1000 }}k-{{ userInfo.salaryMax / 1000 }}k</text>
</view>
<view class="mys-text">
<text>期望工作地</text>
<text>青岛市-</text>
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
<view class="mys-list">
<view class="cards button-click" v-for="(title, index) in userInfo.jobTitle" :key="index">
{{ title }}
</view>
</view>
</view>
<view class="subName">{{ userInfo.phone }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="right-sex">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy1.png"></image>
<image v-else src="@/static/icon/girl1.png"></image>
<view class="card-top" style="margin-top: 24rpx">
<view class="mys-info" style="padding: 0">
<view class="mys-h4">
<text>工作经历</text>
<view class="mys-edit-icon btn-tada" @click="navTo('/packageA/pages/workExp/workExp')">
<image class="icon button-click btn-feel" src="@/static/icon/plus.png"></image>
<view class="txt">添加</view>
</view>
</view>
<view class="exp-item button-click" v-for="item in userInfo.workExp" :key="item.id">
<view class="fl_box fl_justbet mar_top15">
<view class="fs_16">{{ item.company }}</view>
<image
class="icon btn-feel"
src="@/static/icon/edit1.png"
@click="navTo(`/packageA/pages/workExp/workExp?id=${item.id}`)"
></image>
</view>
<view class="mys-text fl_box fl_justbet">
<text class="color_333333 fs_14">{{ item.position }}</text>
<text class="datetext">{{ item.startTime }}--{{ item.endTime || '至今' }}</text>
</view>
<view class="mys-text">
<text>{{ item.duty }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 求职期望 -->
<view class="mys-line"></view>
<view class="mys-info">
<view class="mys-h4">
<text>求职期望</text>
<view class="mys-edit-icon">
<image
class="button-click"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/jobExpect/jobExpect')"
></image>
</view>
<template #footer>
<view class="footer-container">
<view class="footer-button btn-feel" @click="chooseResume">上传简历</view>
</view>
<view class="mys-text">
<text>期望薪资</text>
<text>{{ userInfo.salaryMin / 1000 }}k-{{ userInfo.salaryMax / 1000 }}k</text>
</view>
<view class="mys-text">
<text>期望工资地</text>
<text>青岛市-</text>
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
<view class="mys-list">
<view class="cards button-click" v-for="(title, index) in userInfo.jobTitle" :key="index">
{{ title }}
</view>
</view>
</view>
</view>
</template>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
const { $api, navTo } = inject('globalFunction');
const { $api, navTo, navBack } = inject('globalFunction');
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
@@ -77,22 +140,156 @@ import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
import config from '@/config.js';
const showNotice = ref(true);
onLoad(() => {
getUserResume();
});
function closeNotice() {
showNotice.value = false;
}
function chooseResume() {
uni.chooseImage({
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
count: 1,
success: ({ tempFilePaths, tempFiles }) => {
uploadResume(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
getUserResume();
$api.msg('上传成功');
})
.catch((err) => {
$api.msg('上传失败');
});
},
fail: (error) => {},
});
}
function uploadResume(tempFilePath, loading) {
if (loading) {
uni.showLoading({
title: '请稍后',
mask: true,
});
}
let Authorization = '';
if (useUserStore().token) {
Authorization = `${useUserStore().token}`;
}
const header = {};
header['Authorization'] = encodeURIComponent(Authorization);
return new Promise((resolve, reject) => {
uni.uploadFile({
url: config.baseUrl + '/app/oss/uploadToObs',
filePath: tempFilePath,
name: 'file',
header,
success: (uploadFileRes) => {
if (uploadFileRes.statusCode === 200) {
resolve(uploadFileRes.data);
} else {
reject();
}
},
fail: (err) => {
reject(err);
},
complete: () => {
if (loading) {
uni.hideLoading();
}
},
});
});
}
</script>
<style lang="stylus" scoped>
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
.footer-container{
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
.footer-button{
width: 100%;
height: 90rpx;
background: #1677FF;
border-radius: 8rpx;
color: #FFFFFF;
line-height: 90rpx;
text-align: center;
}
}
.notice-line
width 100%;
height:60rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding:0 30rpx
margin-bottom: 20rpx
.icon
width:35rpx;
height:35rpx;
.text
flex: 1;
overflow hidden
padding:0 25rpx
.close
width:25rpx;
height:25rpx;
.notice-line.blue{
background: #E8F1FF
color: #1677ff
}
.notice-line.green{
background: #D4FFF1
color: #38bb8f
}
image{
width: 100%;
height: 100%
}
.mys-container{
padding-bottom:20rpx;
.card-top{
background: #FFFFFF;
margin: 0 28rpx;
border-radius: 8rpx;
padding: 24rpx
}
.mys-tops{
display: flex
justify-content: space-between
padding: 52rpx 48rpx
padding: 38rpx 44rpx
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) , rgba(255, 255, 255, 1));
border-radius: 8rpx 8rpx 0 0 ;
.tops-left{
.name{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
font-size: 44rpx;
font-size: 36rpx;
color: #333333;
display: flex
align-items: center
@@ -107,8 +304,8 @@ image{
.subName{
margin-top: 12rpx
font-weight: 400;
font-size: 32rpx;
color: #333333;
font-size: 26rpx;
color: #999999;
}
}
@@ -131,32 +328,55 @@ image{
}
}
.mys-line{
margin: 0 28rpx
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 2rpx dashed #000000;
opacity: 0.16;
background: #ffffff;
padding: 0 24rpx
.line{
border: 2rpx dashed #eeeeee;
}
}
.mys-info{
padding: 28rpx
background: #ffffff;
border-radius: 0 0 8rpx 8rpx ;
.mys-h4{
font-weight: 600;
font-size: 32rpx;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
color: #000000;
margin-bottom: 8rpx
display: flex;
justify-content: space-between
align-items: center
.mys-edit-icon{
display: inline-block
.icon{
width: 40rpx;
height: 40rpx
}
.mys-edit-icon{
display: flex;
align-items: center;
.txt{
font-size: 26rpx;
color: #444;
font-weight: 400;
}
.icon{
width: 28rpx;
height: 28rpx
margin-right: 5rpx;
margin-top: 2rpx
vertical-align: bottom;
}
}
}
.datetext{
font-weight: 400;
font-size: 26rpx;
color: #999999;
}
.mys-text{
font-weight: 400;
font-size: 28rpx;
color: #333333;
font-size: 26rpx;
color: #999999;
margin-top: 16rpx
}
.mys-list{
@@ -164,17 +384,27 @@ image{
align-items: center
flex-wrap: wrap;
.cards{
margin: 28rpx 28rpx 0 0
height: 80rpx;
padding: 0 38rpx;
margin: 12rpx 12rpx 0 0
padding: 12rpx 25rpx;
width: fit-content
display: flex
align-items: center
justify-content: center
border-radius: 12rpx 12rpx 12rpx 12rpx;
border-radius:10rpx;
border: 2rpx solid #E8EAEE;
}
}
.exp-item{
padding-bottom: 28rpx;
border-bottom: 2rpx dashed #EEEEEE;
.icon{
width 40rpx;
height 40rpx
}
}
.exp-item:nth-last-child(1){
border-bottom: none;
}
}
}
</style>

View File

@@ -24,12 +24,11 @@
<scroll-view class="scroll-view" scroll-y @scrolltolower="scrollBottom">
<view class="list">
<renderJobs
:list="pageState.list"
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
<empty v-else></empty>
</view>
</scroll-view>
</view>
@@ -140,6 +139,7 @@ function getList(type = 'add', loading = true) {
height: 100%
.list{
padding: 0 28rpx 28rpx 28rpx
height: calc(100% - 28rpx)
}
}
}

View File

@@ -1,257 +1,267 @@
<template>
<AppLayout
title="个人信息"
:sub-title="`完成度${percent}`"
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="confirm">确认</view>
</template>
<view class="content">
<view class="content-input">
<view class="input-titile">姓名</view>
<input class="input-con" v-model="fromValue.name" placeholder="请输入您的姓名" />
</view>
<view class="content-sex">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 0 }" @click="changeSex(0)">
</view>
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 1 }" @click="changeSex(1)">
</view>
</view>
</view>
<view class="content-input" @click="changeDateBirt">
<view class="input-titile">出生年月</view>
<input
class="input-con triangle"
v-model="fromValue.birthDate"
disabled
placeholder="请选择您的出生年月"
/>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con triangle" v-model="state.educationText" disabled placeholder="请选择您的学历" />
</view>
<view class="content-input" @click="changePoliticalAffiliation">
<view class="input-titile">政治面貌</view>
<input
class="input-con triangle"
v-model="state.politicalAffiliationText"
disabled
placeholder="请选择您的政治面貌"
/>
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
<input class="input-con" v-model="fromValue.phone" placeholder="请输入您的手机号码" />
</view>
<AppLayout title="个人信息" :sub-title="`完成度${percent}`" 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="confirm">确认</view>
</template>
<view class="content">
<view class="content-avatar">
<view class="avatar-title">编辑头像</view>
<view @click="selectAvatar">
<image class="avatar" v-if="fromValue.avatar" :src="fromValue.avatar" />
<image class="avatar" v-else-if="fromValue.sex == '0'" src="@/static/icon/boy.png" />
<image class="avatar" v-else="fromValue.sex == '1'" src="@/static/icon/girl.png" />
</view>
</AppLayout>
</view>
<view class="content-input">
<view class="input-titile">姓名</view>
<input class="input-con" v-model="fromValue.name" placeholder="请输入您的姓名" />
</view>
<view class="content-sex">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 0 }" @click="changeSex(0)"> </view>
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 1 }" @click="changeSex(1)"> </view>
</view>
</view>
<view class="content-input" @click="changeDateBirt">
<view class="input-titile">出生年月</view>
<input class="input-con triangle" v-model="fromValue.birthDate" disabled placeholder="请选择您的出生年月" />
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con triangle" v-model="state.educationText" disabled placeholder="请选择您的学历" />
</view>
<view class="content-input" @click="changePoliticalAffiliation">
<view class="input-titile">政治面貌</view>
<input class="input-con triangle" v-model="state.politicalAffiliationText" disabled placeholder="请选择您的政治面貌" />
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
<input class="input-con" v-model="fromValue.phone" placeholder="请输入您的手机号码" />
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, navBack, checkingPhoneRegExp } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
import { reactive, inject, watch, ref, onMounted } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
const { $api, navTo, navBack, checkingPhoneRegExp } = inject("globalFunction");
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 openSelectPopup = inject('openSelectPopup');
const openSelectPopup = inject("openSelectPopup");
const percent = ref('0%');
const percent = ref("0%");
const state = reactive({
educationText: '',
politicalAffiliationText: '',
educationText: "",
politicalAffiliationText: "",
});
const fromValue = reactive({
name: '',
sex: 0,
birthDate: '',
education: '',
politicalAffiliation: '',
name: "",
sex: 0,
birthDate: "",
education: "",
politicalAffiliation: "",
avatar: "",
});
onLoad(() => {
initLoad();
// setTimeout(() => {
// const { age, birthDate } = useUserStore().userInfo;
// const newAge = calculateAge(birthDate);
// // 计算年龄是否对等
// if (age != newAge) {
// completeResume();
// }
// }, 1000);
initLoad();
// setTimeout(() => {
// const { age, birthDate } = useUserStore().userInfo;
// const newAge = calculateAge(birthDate);
// // 计算年龄是否对等
// if (age != newAge) {
// completeResume();
// }
// }, 1000);
});
function initLoad() {
fromValue.name = userInfo.value.name;
fromValue.sex = Number(userInfo.value.sex);
fromValue.phone = userInfo.value.phone;
fromValue.birthDate = userInfo.value.birthDate;
fromValue.education = userInfo.value.education;
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
// 回显
state.educationText = dictLabel('education', userInfo.value.education);
state.politicalAffiliationText = dictLabel('affiliation', userInfo.value.politicalAffiliation);
const result = getFormCompletionPercent(fromValue);
percent.value = result;
fromValue.name = userInfo.value.name;
fromValue.sex = Number(userInfo.value.sex);
fromValue.phone = userInfo.value.phone;
fromValue.birthDate = userInfo.value.birthDate;
fromValue.education = userInfo.value.education;
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
fromValue.avatar = userInfo.value.avatar;
// 回显
state.educationText = dictLabel("education", userInfo.value.education);
state.politicalAffiliationText = dictLabel("affiliation", userInfo.value.politicalAffiliation);
const result = getFormCompletionPercent(fromValue);
percent.value = result;
}
const confirm = () => {
if (!fromValue.name) {
return $api.msg('请输入姓名');
}
if (!fromValue.birthDate) {
return $api.msg('请选择出生年月');
}
if (!fromValue.education) {
return $api.msg('请选择学历');
}
if (!fromValue.politicalAffiliation) {
return $api.msg('请选择政治面貌');
}
if (!checkingPhoneRegExp(fromValue.phone)) {
return $api.msg('请输入正确手机号');
}
const params = {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest('/app/user/resume', params, 'post').then((resData) => {
$api.msg('完成');
state.disbleDate = true;
getUserResume().then(() => {
navBack();
});
if (!fromValue.name) {
return $api.msg("请输入姓名");
}
if (!fromValue.birthDate) {
return $api.msg("请选择出生年月");
}
if (!fromValue.education) {
return $api.msg("请选择学历");
}
if (!fromValue.politicalAffiliation) {
return $api.msg("请选择政治面貌");
}
// if (!checkingPhoneRegExp(fromValue.phone)) {
// return $api.msg('请输入正确手机号');
// }
const params = {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest("/app/user/resume", params, "post").then((resData) => {
$api.msg("完成");
state.disbleDate = true;
getUserResume().then(() => {
navBack();
});
});
};
const changeDateBirt = () => {
const datearray = generateDatePickerArrays();
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
openSelectPopup({
title: '年龄段',
maskClick: true,
data: datearray,
defaultIndex,
success: (_, value) => {
const [year, month, day] = value;
const dateStr = `${year.value}-${month.value}-${day.value}`;
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg('没有这一天');
}
},
});
const datearray = generateDatePickerArrays();
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
openSelectPopup({
title: "年龄段",
maskClick: true,
data: datearray,
defaultIndex,
success: (_, value) => {
const [year, month, day] = value;
const dateStr = `${year.value}-${month.value}-${day.value}`;
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg("没有这一天");
}
},
});
};
const changeEducation = () => {
openSelectPopup({
title: '学历',
maskClick: true,
data: [oneDictData('education')],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
},
});
openSelectPopup({
title: "学历",
maskClick: true,
data: [oneDictData("education")],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
},
});
};
const changeSex = (sex) => {
fromValue.sex = sex;
fromValue.sex = sex;
};
const changePoliticalAffiliation = () => {
openSelectPopup({
title: '政治面貌',
maskClick: true,
data: [oneDictData('affiliation')],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
},
});
openSelectPopup({
title: "政治面貌",
maskClick: true,
data: [oneDictData("affiliation")],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
},
});
};
function generateDatePickerArrays(startYear = 1975, endYear = new Date().getFullYear()) {
const years = [];
const months = [];
const days = [];
const years = [];
const months = [];
const days = [];
for (let y = startYear; y <= endYear; y++) {
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, '0'));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, '0'));
}
for (let y = startYear; y <= endYear; y++) {
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, "0"));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, "0"));
}
return [years, months, days];
return [years, months, days];
}
function isValidDate(dateString) {
const [year, month, day] = dateString.split('-').map(Number);
const [year, month, day] = dateString.split("-").map(Number);
const date = new Date(year, month - 1, day); // 月份从0开始
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
const date = new Date(year, month - 1, day); // 月份从0开始
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
}
const calculateAge = (birthDate) => {
const birth = new Date(birthDate);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
const dayDiff = today.getDate() - birth.getDate();
const birth = new Date(birthDate);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
const dayDiff = today.getDate() - birth.getDate();
// 如果生日的月份还没到,或者刚到生日月份但当天还没过,则年龄减 1
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
// 如果生日的月份还没到,或者刚到生日月份但当天还没过,则年龄减 1
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
return age;
return age;
};
function getFormCompletionPercent(form) {
let total = Object.keys(form).length;
let filled = 0;
let total = Object.keys(form).length;
let filled = 0;
for (const key in form) {
const value = form[key];
if (value !== '' && value !== null && value !== undefined) {
if (typeof value === 'number') {
filled += 1;
} else if (typeof value === 'string' && value.trim() !== '') {
filled += 1;
}
}
for (const key in form) {
const value = form[key];
if (value !== "" && value !== null && value !== undefined) {
if (typeof value === "number") {
filled += 1;
} else if (typeof value === "string" && value.trim() !== "") {
filled += 1;
}
}
}
if (total === 0) return '0%';
const percent = (filled / total) * 100;
return percent.toFixed(0) + '%'; // 取整,不要小数点
if (total === 0) return "0%";
const percent = (filled / total) * 100;
return percent.toFixed(0) + "%"; // 取整,不要小数点
}
// 主函数
function getDatePickerIndexes(dateStr) {
const [year, month, day] = dateStr.split('-');
const [year, month, day] = dateStr.split("-");
const [years, months, days] = generateDatePickerArrays();
const [years, months, days] = generateDatePickerArrays();
const yearIndex = years.indexOf(year);
const monthIndex = months.indexOf(month);
const dayIndex = days.indexOf(day);
const yearIndex = years.indexOf(year);
const monthIndex = months.indexOf(month);
const dayIndex = days.indexOf(day);
return [yearIndex, monthIndex, dayIndex];
return [yearIndex, monthIndex, dayIndex];
}
function selectAvatar() {
uni.chooseImage({
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
count: 1,
success: ({ tempFilePaths, tempFiles }) => {
$api
.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg("上传失败");
});
},
fail: (error) => {},
});
}
</script>
@@ -267,6 +277,23 @@ function getDatePickerIndexes(dateStr) {
height: calc(100% - 120rpx)
}
.content-avatar{
margin-bottom: 52rpx;
padding-bottom: 28rpx
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2rpx solid #EBEBEB
.avatar-title{
font-size: 30rpx;
color #333;
}
.avatar{
width:110rpx;
height: 110rpx;
border-radius: 50%;
}
}
.content-input
margin-bottom: 52rpx
.input-titile
@@ -302,12 +329,12 @@ function getDatePickerIndexes(dateStr) {
background: #697279;
transform: rotate(45deg)
.content-sex
height: 110rpx;
display: flex
justify-content: space-between;
align-items: flex-start;
border-bottom: 2rpx solid #EBEBEB
margin-bottom: 52rpx
padding-bottom: 28rpx
.sex-titile
line-height: 80rpx;
.sext-ri

View File

@@ -28,9 +28,12 @@ watch(
() => props.value,
(newVal) => {
if (newVal && Object.keys(newVal).length > 0) {
const { skill, experience, education, salary, age, location } = newVal.radarChart;
const labels = ['学历', '年龄', '工作地', '技能', '工作经验', '期望薪资'];
const data = [education, age, location, skill, experience, salary].map((item) => item * 0.05);
// const { skill, experience, education, salary, age, location } = newVal.radarChart;
const { experience, education, salary, age, location } = newVal.radarChart;
// const labels = ['学历', '年龄', '工作地', '技能', '工作经验', '期望薪资'];
const labels = ['学历', '年龄', '工作地', '工作经验', '期望薪资'];
// const data = [education, age, location, skill, experience, salary].map((item) => item * 0.05);
const data = [education, age, location, experience, salary].map((item) => item * 0.05);
rawRadarChart(labels, data);
}
},
@@ -43,10 +46,8 @@ function rawRadarChart(labels, data) {
const height = 80;
const centerX = 150;
const centerY = 125;
// const data = [2, 3.5, 5, 3.5, 5, 3.5]; // 示例数据
// const labels = ['火烧', '泡水', '事故', '外观', '部件', '火烧'];
const colors = ['#F5F5F5', '#F5F5F5', '#F5F5F5', '#F5F5F5', '#F5F5F5'];
const maxScore = 5; // 数据最大值
const maxScore = 5;
const angleStep = (2 * Math.PI) / labels.length;
@@ -62,9 +63,8 @@ function rawRadarChart(labels, data) {
ctx.fill();
//多边形圈
// 多边形圈
for (let i = 5; i > 0; i--) {
ctx.setStrokeStyle(colors[i - 1]); // 设置边框颜色
ctx.setStrokeStyle(colors[i - 1]);
ctx.beginPath();
labels.forEach((label, index) => {
const x = centerX + (width / 5) * i * Math.cos(angleStep * index - Math.PI / 2);
@@ -76,19 +76,20 @@ function rawRadarChart(labels, data) {
}
});
ctx.closePath();
ctx.stroke(); // 只描边,不填充
ctx.stroke();
}
// //竖线
//竖线
labels.forEach((label, index) => {
ctx.setStrokeStyle('#F5F5F5');
ctx.setFillStyle('#F5F5F5');
ctx.beginPath();
const x1 = centerX + width * 0.6 * Math.sin(angleStep * index);
const y1 = centerY + height * 0.6 * Math.cos(angleStep * index);
const x = centerX + width * Math.sin(angleStep * index);
const y = centerY + height * Math.cos(angleStep * index);
// 修改坐标计算,使用与多边形圈相同的角度计算方式
const x1 = centerX + width * 0.6 * Math.cos(angleStep * index - Math.PI / 2);
const y1 = centerY + height * 0.6 * Math.sin(angleStep * index - Math.PI / 2);
const x = centerX + width * Math.cos(angleStep * index - Math.PI / 2);
const y = centerY + height * Math.sin(angleStep * index - Math.PI / 2);
ctx.moveTo(x1, y1);
ctx.lineTo(x, y);
@@ -102,11 +103,11 @@ function rawRadarChart(labels, data) {
ctx.setFillStyle('rgba(37,107,250, 0.24)');
ctx.setLineWidth(2);
ctx.beginPath();
const pointList = []; // 记录每个点的位置,等会画小圆点
const pointList = [];
data.forEach((score, index) => {
const x = centerX + width * (score / maxScore) * Math.cos(angleStep * index - Math.PI / 2);
const y = centerY + height * (score / maxScore) * Math.sin(angleStep * index - Math.PI / 2);
pointList.push({ x, y }); // 保存位置
pointList.push({ x, y });
if (index === 0) {
ctx.moveTo(x, y);
} else {
@@ -118,15 +119,16 @@ function rawRadarChart(labels, data) {
ctx.stroke();
// 绘制每个小圆点
ctx.setFillStyle('#256BFA'); // 小圆点颜色(你可以改)
ctx.setFillStyle('#256BFA');
pointList.forEach((point) => {
ctx.beginPath();
ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI); // 半径 4可以自己调大小
ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
ctx.fill();
});
// 绘制标签
// 绘制标签
ctx.setTextAlign('center');
ctx.setTextBaseline('middle');
labels.forEach((label, index) => {
const x = centerX + (width + 30) * Math.cos(angleStep * index - Math.PI / 2);
@@ -137,30 +139,9 @@ function rawRadarChart(labels, data) {
ctx.setFontSize(12);
ctx.font = 'bold 12px sans-serif';
ctx.fillText(label, x, y);
// ctx.setFillStyle('#A2A4A2');
// ctx.font = '12px sans-serif';
// ctx.setFontSize(12);
// ctx.fillText(data[index], x, y + 16);
});
ctx.draw();
//转图片
// uni.canvasToTempFilePath({
// x: 0,
// y: 0,
// width: 320,
// height: 320,
// destWidth: 840,
// destHeight: 840,
// canvasId: 'radarCanvas',
// success: (res) => {
// // 在H5平台下tempFilePath 为 base64
// src = res.tempFilePath;
// },
// });
}
</script>

View File

@@ -6,11 +6,11 @@
:style="{
left: position.x + 'px',
top: position.y + 'px',
width: isFullScreen ? '100%' : '300rpx',
height: isFullScreen ? '100vh' : '200rpx',
width: isFullScreen ? '100%' : videoWidth + 'rpx',
height: isFullScreen ? '100vh' : videoHeight + 'rpx',
}"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchstart.passive="handleTouchStart"
@touchmove.passive="handleTouchMove"
@touchend="handleTouchEnd"
@touchmove.stop.prevent
>
@@ -24,8 +24,10 @@
height: '100%',
}"
id="myVideo"
ref="videoRef"
@play="onPlay"
@pause="onPause"
@loadedmetadata="onLoadedMetadata"
></video>
<!-- 控制栏 -->
@@ -41,6 +43,8 @@
import { ref, reactive, onMounted } from 'vue';
import { nextTick } from 'vue';
const videoRef = ref(null);
const visible = ref(false);
const isPlaying = ref(false);
const isFullScreen = ref(false);
@@ -49,6 +53,8 @@ const position = reactive({ x: 20, y: 100 });
const videoContext = ref(null);
const startPos = reactive({ x: 0, y: 0 });
const moving = ref(false);
const videoWidth = ref(0);
const videoHeight = ref(0);
// 初始化视频上下文
onMounted(() => {
@@ -71,8 +77,8 @@ const handleTouchMove = (e) => {
const newY = e.touches[0].clientY - startPos.y;
// 边界检测
const maxX = window.innerWidth - 150; // 300rpx换算后的值
const maxY = 50 + window.innerHeight - 200;
const maxX = window.innerWidth - videoWidth.value / 2; // 300rpx换算后的值
const maxY = window.innerHeight - videoHeight.value / 2;
position.x = Math.max(0, Math.min(newX, maxX));
position.y = Math.max(0, Math.min(newY, maxY));
@@ -124,6 +130,32 @@ const open = (url) => {
});
};
const onLoadedMetadata = (e) => {
const video = e.detail;
const width = video.width;
const height = video.height;
const ratio = width / height;
// 设置宽度:横屏宽 300竖屏宽 180可自定义
if (ratio >= 1) {
videoWidth.value = 300; // 横屏
videoHeight.value = 300 * (height / width); // 保持比例
} else {
videoWidth.value = 180; // 竖屏
videoHeight.value = 180 * (height / width); // 保持比例
}
// console.log(`宽高: ${width}x${height}`);
// console.log(`比例: ${ratio.toFixed(2)} (${getRatioName(ratio)})`);
};
function getRatioName(ratio) {
const rounded = Math.round(ratio * 100) / 100;
if (Math.abs(rounded - 16 / 9) < 0.01) return '16:9';
if (Math.abs(rounded - 4 / 3) < 0.01) return '4:3';
if (Math.abs(rounded - 1) < 0.01) return '1:1';
return `${rounded.toFixed(2)}:1`;
}
// 暴露方法
defineExpose({ open });
</script>

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="" backGorundColor="#F4F4F4">
<template #headerleft>
<view class="btn">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -11,25 +11,54 @@
<image src="@/static/icon/collect2.png" v-else @click="jobCollection"></image>
</view>
</template>
<view class="content">
<!-- 根据 dataType 显示不同内容 -->
<view class="content" v-show="!isEmptyObject(jobInfo)">
<!-- 顶部信息区域 -->
<view class="content-top btn-feel">
<view class="top-salary">{{ jobInfo.minSalary }}-{{ jobInfo.maxSalary }}/</view>
<view class="top-name">{{ jobInfo.jobTitle }}</view>
<view class="top-salary" v-if="jobInfo.maxSalary">
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="top-salary" v-else>
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="top-name">{{ dataType === 2 ? jobInfo.gwmc : jobInfo.jobTitle }}</view>
<view class="top-info">
<view class="info-img"><image src="/static/icon/post12.png"></image></view>
<view class="info-text">
<!-- 第三方数据展示 -->
<view class="info-text" v-if="dataType === 2">
{{ jobInfo.xlyq == '不限' ? '学历不限' : jobInfo.xlyq }}
</view>
<!-- 原数据展示 -->
<view class="info-text" v-else>
<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">
<!-- 第三方数据展示 -->
<view class="info-text" v-if="dataType === 2">
{{ jobInfo.gwgzjy == '不限' ? '经验不限' : jobInfo.gwgzjy }}
</view>
<!-- 原数据展示 -->
<view class="info-text" v-else>
<dict-Label dictType="education" :value="jobInfo.education"></dict-Label>
</view>
</view>
<view class="position-source">
<text>来源&nbsp;</text>
{{ jobInfo.dataSource }}
{{ dataType === 2 ? '青岛人才网' : jobInfo.dataSource }}
</view>
</view>
<!-- AI讲解区域 -->
<view class="ai-explain" v-if="jobInfo.isExplain">
<view class="exbg">
<view class="explain-left btn-shaky">
@@ -39,38 +68,42 @@
<view class="explain-right button-click" @click="seeExplain">点击查看</view>
</view>
</view>
<!-- 职位描述区域 -->
<view class="content-card">
<view class="card-title">
<text class="title">职位描述</text>
</view>
<view class="description" :style="{ whiteSpace: 'pre-wrap' }">
{{ jobInfo.description }}
{{ dataType === 2 ? jobInfo.gwms : jobInfo.description }}
</view>
</view>
<!-- 公司信息区域 -->
<view class="content-card">
<view class="card-title">
<text class="title">公司信息</text>
<text
class="btntext button-click"
@click="navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${jobInfo.company.companyId}`)"
>
单位详情
</text>
<text class="btntext button-click" @click="handleCompanyDetail">单位详情</text>
</view>
<view class="company-info">
<view class="companyinfo-left">
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
<view class="companyinfo-right">
<view class="row1">{{ jobInfo.company?.name }}</view>
<view class="row1">{{ dataType === 2 ? jobInfo.gsmc : jobInfo.company?.name }}</view>
<view class="row2">
<dict-tree-Label
v-if="jobInfo.company?.industry"
v-if="dataType !== 2 && jobInfo.company?.industry"
dictType="industry"
:value="jobInfo.company?.industry"
></dict-tree-Label>
<span v-if="jobInfo.company?.industry">&nbsp;</span>
<dict-Label dictType="scale" :value="jobInfo.company?.scale"></dict-Label>
<span v-if="dataType !== 2 && jobInfo.company?.industry">&nbsp;</span>
<dict-Label
v-if="dataType !== 2"
dictType="scale"
:value="jobInfo.company?.scale"
></dict-Label>
<span v-if="dataType === 2">{{ jobInfo.qyxz }}</span>
</view>
<view class="row2">
<text>在招</text>
@@ -88,14 +121,16 @@
></map>
</view>
</view>
<view class="content-card">
<!-- 竞争力分析区域 -->
<view class="content-card" v-if="dataType !== 2 && raderData?.totalApplicants > 2">
<view class="card-title">
<text class="title">竞争力分析</text>
</view>
<view class="description">
三个月内共15位求职者申请你的简历匹配度为{{ raderData.matchScore }}排名位于第{{
raderData.rank
}}超过{{ raderData.percentile }}%的竞争者处在优秀位置
三个月内共{{ raderData.totalApplicants }}位求职者申请你的简历匹配度为{{
raderData.matchScore
}}排名位于第{{ raderData.rank }}超过{{ raderData.percentile }}%的竞争者处在优秀位置
</view>
<RadarMap :value="raderData"></RadarMap>
@@ -107,10 +142,7 @@
v-for="(item, index) in matchingDegree"
:key="index"
class="progress-item"
:class="{
active: index < currentStep - 1,
half: index < currentStep && currentStep < index + 1, // 半条
}"
:class="getClass(index)"
/>
</view>
</view>
@@ -119,11 +151,25 @@
</view>
</view>
</view>
<view style="height: 24px"></view>
</view>
<template #footer>
<view class="footer">
<view class="btn-wq button-click" @click="jobApply">立即前往</view>
<view
v-if="dataType == 2"
class="btn-wq button-click"
:class="{ 'btn-des': jobInfo.isApply }"
@click="jobApply"
>
<span v-if="jobInfo.isApply">已投递</span>
<span v-if="!jobInfo.isApply">立即投递</span>
</view>
<view v-else class="btn-wq button-click" @click="jobApply">
<span v-if="jobInfo.isApply">立即前往</span>
<span v-if="!jobInfo.isApply">立即投递</span>
</view>
</view>
</template>
<VideoPlayer ref="videoPalyerRef" />
@@ -134,10 +180,13 @@
import point from '@/static/icon/point.png';
import VideoPlayer from './component/videoPlayer.vue';
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
const { $api, navTo, getLenPx, parseQueryParams, navBack } = inject('globalFunction');
import RadarMap from './component/radarMap.vue';
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';
const matchingDegree = ref(['一般', '良好', '优秀', '极好']);
const currentStep = ref(1);
const companyCount = ref(0);
@@ -148,9 +197,11 @@ const jobIdRef = ref();
const raderData = ref({});
const videoPalyerRef = ref(null);
const explainUrlRef = ref('');
const dataType = ref(1); // 1: 原数据, 2: 第三方数据
onLoad((option) => {
if (option.jobId) {
dataType.value = option.dataType ? parseInt(option.dataType) : 1;
initLoad(option);
}
});
@@ -158,6 +209,7 @@ onLoad((option) => {
onShow(() => {
const option = parseQueryParams(); // 兼容微信内置浏览器
if (option.jobId) {
dataType.value = option.dataType ? parseInt(option.dataType) : 1;
initLoad(option);
}
});
@@ -167,105 +219,208 @@ function initLoad(option) {
if (jobId !== jobIdRef.value) {
jobIdRef.value = jobId;
getDetail(jobId);
getCompetivetuveness(jobId);
}
}
function seeExplain() {
if (jobInfo.value.explainUrl) {
videoPalyerRef.value?.open(jobInfo.value.explainUrl);
// console.log(jobInfo.value.explainUrl);
// explainUrlRef.value = jobInfo.value.explainUrl;
}
}
function getDetail(jobId) {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
getCompanyIsAJobs(companyId);
if (latitude && longitude) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName), // X 轴调整,负数向左
borderRadius: 5,
},
width: 34,
},
];
}
});
if (dataType.value === 2) {
// 第三方数据接口
return new Promise((reslove, reject) => {
$api.createRequest(`/app/internal/jobThirdPart/${jobId}`, {}, 'GET', true).then((resData) => {
const { gsID, gsmc, zphID } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(gsID, gsmc, zphID);
if (resData.data.latitude && resData.data.longitude) {
initMapCovers(resData.data.latitude, resData.data.longitude, resData.data.gsmc);
}
});
});
} else {
// 原数据接口
$api.createRequest(`/app/job/${jobId}`, {}, 'GET', true).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
getCompanyIsAJobs(companyId);
getCompetivetuveness(jobId);
if (latitude && longitude) {
initMapCovers(latitude, longitude, companyName);
}
});
}
}
function getCompanyIsAJobs(companyId) {
$api.createRequest(`/app/company/count/${companyId}`).then((resData) => {
companyCount.value = resData.data;
});
function initMapCovers(latitude, longitude, companyName) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName),
borderRadius: 5,
},
width: 34,
},
];
}
function getCompanyIsAJobs(...args) {
if (dataType.value === 2) {
// 第三方数据获取公司职位数量
const [gsID, gsmc, zphID] = args;
$api.createRequest(`/app/internal/jobThirdPart?gsID=${gsID}&gsmc=${gsmc}&zphID=${zphID}`).then((resData) => {
companyCount.value = resData.total;
});
} else {
// 原数据获取公司职位数量
const [companyId] = args;
$api.createRequest(`/app/company/count/${companyId}`).then((resData) => {
companyCount.value = resData.data;
});
}
}
function getTextWidth(text, size = 12) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${12}px Arial`;
return -(context.measureText(text).width / 2) - 20; // 计算文字中心点
return -(context.measureText(text).width / 2) - 20;
}
function getCompetivetuveness(jobId) {
$api.createRequest(`/app/job/competitiveness/${jobId}`, {}, 'GET').then((resData) => {
raderData.value = resData.data;
currentStep.value = resData.data.matchScore * 0.04;
});
if (dataType.value !== 2) {
$api.createRequest(`/app/job/competitiveness/${jobId}`, {}, 'GET').then((resData) => {
raderData.value = resData.data;
currentStep.value = resData.data.matchScore * 0.04;
});
}
}
// 申请岗位
function jobApply() {
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isApply) {
const jobUrl = jobInfo.value.jobUrl;
return window.open(jobUrl);
if (dataType.value === 2) {
// 第三方数据申请逻辑
const params = {
jobid: jobInfo.value.id,
jobname: jobInfo.value.gwmc,
};
if (jobInfo.value.isApply) {
$api.msg('已经投递过该岗位了~');
return;
} else {
$api.createRequest(`/app/internal/sendResume`, params, 'POST').then((resData) => {
$api.msg('投递成功');
getDetail(jobIdRef.value);
});
}
} else {
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
getDetail(jobId);
$api.msg('申请成功');
// 原数据申请逻辑
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isApply) {
const jobUrl = jobInfo.value.jobUrl;
return window.open(jobUrl);
});
} else {
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
getDetail(jobId);
$api.msg('申请成功');
const jobUrl = jobInfo.value.jobUrl;
return window.open(jobUrl);
});
}
}
}
// 取消/收藏岗位
function jobCollection() {
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'DELETE').then((resData) => {
getDetail(jobId);
$api.msg('取消收藏成功');
});
if (dataType.value === 2) {
// 第三方数据收藏逻辑
const id = jobInfo.value.id;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${id}/2`, {}, 'DELETE').then((resData) => {
getDetail(jobIdRef.value);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/job/collection/${id}/2`, {}, 'POST').then((resData) => {
getDetail(jobIdRef.value);
$api.msg('收藏成功');
});
}
} else {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'POST').then((resData) => {
getDetail(jobId);
$api.msg('收藏成功');
});
// 原数据收藏逻辑
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'DELETE').then((resData) => {
getDetail(jobId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'POST').then((resData) => {
getDetail(jobId);
$api.msg('收藏成功');
});
}
}
}
// 处理公司详情跳转
function handleCompanyDetail() {
if (dataType.value === 2) {
navTo(
`/packageA/pages/UnitDetails/UnitDetails?companyId=${jobInfo.value.gsID}&companyName=${jobInfo.value.gsmc}&zphId=${jobInfo.value.zphID}&dataType=2`
);
} else {
navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${jobInfo.value.company.companyId}`);
}
}
function getClass(index) {
const current = currentStep.value;
const floorIndex = Math.floor(current);
if (index < floorIndex) {
return 'active';
} else if (index === floorIndex) {
const decimal = current % 1;
const percent = Math.round(decimal * 100);
return `half${percent}`;
} else {
return '';
}
}
</script>
<style lang="stylus" scoped>
/* 样式保持不变与现在的post页面相同 */
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
width: 52rpx;
height: 52rpx;
}
.btnshare {
width: 48rpx;
height: 48rpx;
margin-right: 46rpx;
}
image {
height: 100%;
@@ -274,15 +429,14 @@ image {
.progress-container {
display: flex;
align-items: center;
gap: 8rpx; /* 间距 */
gap: 8rpx;
margin-top: 24rpx
}
.progress-text{
margin-top: 8rpx
display: flex;
align-items: center;
gap: 8rpx; /* 间距 */
gap: 8rpx;
justify-content: space-around
width: 100%
font-weight: 400;
@@ -301,51 +455,27 @@ image {
transition: background-color 0.3s;
}
/* 完整激活格子 */
.progress-item.active {
background: linear-gradient(to right, #256bfa, #8c68ff);
}
/* 当前进度进行中的格子 */
.progress-item.half::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%; /* 根据 currentStep 小数动态控制 */
// background: linear-gradient(to right, #256bfa, #8c68ff);
background: linear-gradient(to right, #256bfa 50%, #eaeaea 50%);
border-radius: 24rpx;
}
for i in 0..100
.progress-item.half{i}::before
content ''
position absolute
left 0
top 0
bottom 0
width 100%
background linear-gradient(to right, #256bfa (i)%, #eaeaea (i)%)
border-radius 24rpx
.card-footer{
.footer-title{
font-weight: 600;
font-size: 28rpx;
color: #000000;
}
.footer-content{
.content-line{
display: grid
grid-template-columns: repeat(4, 1fr)
background: linear-gradient( to left, #9E74FD 0%, #256BFA 100%);
border-radius: 10rpx
.line-pargrah{
height: 20rpx;
position: relative
}
.line-pargrah::after{
position: absolute;
content: '';
right: 10
top: 0
width: 6rpx
height: 20rpx
background: #FFFFFF
}
}
}
}
// ai
.ai-explain{
@@ -491,7 +621,6 @@ image {
margin-right: 24rpx
}
.companyinfo-right{
.row1{
font-weight: 500;
font-size: 32rpx;
@@ -528,5 +657,9 @@ image {
text-align: center;
line-height: 90rpx
}
.btn-des{
background: #6697FB;
box-shadow: 0rpx -4rpx 24rpx 0rpx rgba(11,44,112,0.12);
}
}
</style>

View File

@@ -125,6 +125,7 @@ onUnmounted(() => {
}
.time-block {
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
text-align: center;
font-weight: 500;
font-size: 28rpx;

View File

@@ -12,32 +12,41 @@
</view>
</view>
<view class="main">
<scroll-view scroll-y>
<view v-if="pageState.list.length">
<scroll-view class="height-100" scroll-y>
<view>
<view class="card" v-for="(item, index) in pageState.list" :key="index">
<view @click="navTo('/packageA/pages/exhibitors/exhibitors?jobFairId=' + item.jobFairId)">
<view
@click="
navTo(
'/packageA/pages/exhibitors/exhibitors?jobFairId=' +
item.zphID +
'&jobFairName=' +
item.zphmc
)
"
>
<view class="card-row">
<Countdown startTime="item.startTime" :endTime="item.endTime" />
<Countdown :startTime="item.zphjbsj" :endTime="item.zphjzsj" />
</view>
<view class="card-Title">{{ item.name }}</view>
<view class="card-Title">{{ item.zphmc }}</view>
<view class="card-row">
<view class="rowleft">{{ item.location }}</view>
<view class="rowright">
<convert-distance
<view class="rowleft">{{ item.zphdz }}</view>
<view class="rowright" style="white-space: nowrap">
<!-- <convert-distance
:alat="item.latitude"
:along="item.longitude"
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance>
></convert-distance> -->
</view>
</view>
</view>
<view class="footer" v-if="isTimePassed(item.startTime)">
<view class="footer" v-if="isTimePassed(item.zphjbsj)">
<view class="card_cancel" @click="updateCancel(item)">取消预约</view>
</view>
</view>
</view>
<empty v-else pdTop="200"></empty>
<empty v-if="!pageState.list.length"></empty>
</scroll-view>
</view>
</view>
@@ -72,6 +81,7 @@ const ranOptions = ref([
]);
function isTimePassed(timeStr) {
if (!timeStr) return false;
const targetTime = new Date(timeStr.replace(/-/g, '/')).getTime(); // 兼容格式
const now = Date.now();
return now < targetTime;
@@ -88,10 +98,19 @@ function chnageRanOption(item) {
}
function updateCancel(item) {
const fairId = item.jobFairId;
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
getList('refresh');
$api.msg('取消预约成功');
const fairId = item.zphID;
uni.showModal({
title: '提示',
content: '确定要取消预约吗?',
showCancel: true,
success: ({ confirm, cancel }) => {
if (confirm) {
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
getList('refresh');
$api.msg('取消预约成功');
});
}
},
});
}
@@ -108,7 +127,7 @@ function getList(type = 'add', loading = true) {
pageSize: pageState.pageSize,
type: ranItem.value.value,
};
$api.createRequest('/app/user/collection/fair', params).then((resData) => {
const LoadCache = (resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
@@ -121,7 +140,8 @@ function getList(type = 'add', loading = true) {
// pageState.list = resData.rows;
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
});
};
$api.createRequestWithCache('/app/user/collection/fair', params, 'GET', false, {}, LoadCache).then(LoadCache);
}
</script>
@@ -141,6 +161,7 @@ function getList(type = 'add', loading = true) {
color: #666D7F;
}
.active{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
color: #000000;
@@ -165,8 +186,10 @@ function getList(type = 'add', loading = true) {
display: flex
align-items: center
}
}
.card-Title{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
line-height: 70rpx

View File

@@ -26,6 +26,7 @@
<view
class="item button-click"
:class="{
optional: item.isThisMonth && hasZphInData(item),
noOptional: !item.isThisMonth,
active: current.date === item.date && item.isThisMonth,
}"
@@ -58,8 +59,10 @@ const pages = reactive({
year: 0,
month: 0,
});
const hasZphDateArray = ref([]);
onLoad((options) => {
updateDateArray();
if (options.date) {
current.value = {
date: options?.date || null,
@@ -77,8 +80,32 @@ onLoad((options) => {
addMonth();
});
}
if (options.entrance === 'careerfair') {
updateDateArray();
}
});
function hasZphInData(item) {
if (!item || typeof item.date !== 'string') {
return false;
}
const dateArray = Array.isArray(hasZphDateArray.value) ? hasZphDateArray.value : [];
return dateArray.some((date) => {
return typeof date === 'string' && date === item.date;
});
}
async function updateDateArray() {
const LoadCache = (resData) => {
if (resData.code === 200) {
hasZphDateArray.value = resData.data;
}
};
$api.createRequestWithCache('/app/internal/getDateList', {}, 'GET', false, {}, LoadCache).then(LoadCache);
}
function backParams() {
if (isValidDateString(current.value.date)) {
navBack({

View File

@@ -0,0 +1,136 @@
<template>
<div class="video-container">
<view class="back-box">
<view class="btn">
<uni-icons type="left" size="26" color="#FFFFFF" @click="navBack"></uni-icons>
</view>
</view>
<mTikTok :video-list="state.videoList" :pause-type="1" :controls="false" @loadMore="loadMore" @change="change">
<template v-slot="data">
<view class="video-layer">
<view class="title line_1">{{ currentItem.companyName }}</view>
<view class="discription">
<text class="line_1">
{{ currentItem.jobTitle }}
</text>
<view class="seedetail" @click="nextDetail">
查看详情
<uni-icons type="right" color="#FFFFFF" size="14"></uni-icons>
</view>
</view>
</view>
</template>
</mTikTok>
</div>
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navBack, navTo } = inject('globalFunction');
import mTikTok from '@/components/TikTok/TikTok.vue';
import useUserStore from '@/stores/useUserStore';
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const state = reactive({
videoList: [],
});
const currentItem = ref(null);
onLoad(() => {
const jobInfo = uni.getStorageSync(`job-Info`);
if (jobInfo) {
currentItem.value = jobInfo;
state.videoList.push(jobInfo);
}
getNextVideoSrc(2);
});
function nextDetail() {
const job = currentItem.value;
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
console.log(job.jobId);
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
function getNextVideoSrc(num) {
let params = {
uuid: useUserStore().seesionId,
count: num || 1,
};
$api.createRequest('/app/job/littleVideo/random', params).then((resData) => {
const { data, code } = resData;
state.videoList.push(...data);
});
}
const loadMore = () => {
// 触发加载更多
console.log('加载更多');
getNextVideoSrc();
};
const change = (e) => {
currentItem.value = e.detail;
console.log('🚀 ~ file: index.vue:53 ~ change ~ data:', e);
};
</script>
<style lang="stylus" scoped>
.video-container{
width: 100%;
height: 100vh;
position: relative
.back-box{
position: absolute;
left: 20rpx;
top: 20rpx
color: #FFFFFF
z-index: 2
}
}
.video-layer {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: 30rpx;
color: #fff;
.title{
font-weight: 500;
font-size: 30rpx;
line-height: 100%;
letter-spacing: 0%;
}
.discription{
font-weight: 400;
font-size: 28rpx;
letter-spacing: 0%;
margin-top: 20rpx
display: flex
align-items: center
.seedetail{
font-weight: 500;
font-size: 32rpx;
line-height: 100%;
letter-spacing: 0%;
white-space: nowrap
padding-left: 20rpx
}
}
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top btn-feel">
<view class="info">
<view class="avatar">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="info-right">
<view class="name">{{ userInfo.name || "编辑用户名" }}</view>
<view class="des">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">|</text>
<text class="mar_ri10">{{ userInfo.age }}</text>
<text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<!-- <text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="affiliation" :value="userInfo.politicalAffiliation"></dict-Label> -->
</view>
<view class="phone">
<image class="call-icon" src="@/static/icon/call.png" />
<view>{{ userInfo.phone }}</view>
</view>
</view>
</view>
<view class="info-bottom">
<!-- <view>到岗2025-11-02</view> -->
<view></view>
<view>地点青岛市-<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label></view>
</view>
<view class="des-card" style="margin-top: 24rpx">
<view class="fl_box fl_justbet">
<view style="white-space:nowrap">求职意向岗位</view>
<view class="line_1" style="padding-left:40rpx" >{{ userInfo.jobIntention || userInfo.jobTitle?.join(',') || '-' }}</view>
</view>
<view class="fl_box fl_justbet">
<view>毕业学校</view>
<view>{{ userInfo.graduationSchool || "-" }}</view>
</view>
<!-- <view class="fl_box fl_justbet">
<view>当前状态</view>
<view>在职 看工作机会</view>
</view> -->
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">个人技能</view>
</view>
<view class="skill-box">
<view class="skill-item" v-for="item in userInfo?.skillList" :key="item.id">{{ item.skill }}</view>
</view>
<view v-if="!userInfo?.skillList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无个人技能</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">关键经历</view>
</view>
<view class="exp-box">
<view class="exp-item" v-for="(item, index) in userInfo?.workExp" :key="item.id">{{ index + 1 + "." }}{{ item.duty }}</view>
</view>
<view v-if="!userInfo?.workExp?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无关键经历</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">荣誉及证书情况</view>
</view>
<ul class="certificate-box">
<li class="certificate-item" v-for="(item, index) in userInfo?.certificateList" :key="item.id">{{ item.name }}</li>
</ul>
<view v-if="!userInfo?.certificateList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无荣誉证书</view>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, computed } from "vue";
const { $api, navTo, navBack } = inject("globalFunction");
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
</script>
<style lang="scss" scoped>
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
.card-top {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.9));
}
.card {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: #fff;
padding: 24rpx;
margin-top: 24rpx;
.title {
position: relative;
.bg {
width: 108rpx;
height: 16rpx;
position: absolute;
left: 0;
bottom: 0;
}
.text {
margin-left: 20rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
}
}
.skill-box {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 24rpx;
.skill-item {
padding: 8rpx 20rpx;
font-size: 24rpx;
color: #333;
background: #e7f1ff;
border-radius: 8rpx;
}
}
.exp-box {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
margin-top: 24rpx;
.exp-item {
font-size: 26rpx;
color: #333;
}
}
.certificate-box {
margin-top: 24rpx;
padding-inline-start: 40rpx !important;
.certificate-item {
font-size: 26rpx;
color: #333;
margin-bottom: 24rpx;
}
}
image {
width: 100%;
height: 100%;
}
.mys-container {
padding: 28rpx;
.info {
display: flex;
align-items: center;
padding: 24rpx;
.avatar {
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;
image{
border-radius: 50%;
}
}
.info-right {
height: 160rpx;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.name {
font-size: 40rpx;
font-weight: bold;
}
.des {
font-size: 26rpx;
color: #999999;
}
.phone {
display: flex;
align-items: center;
font-size: 26rpx;
color: #999999;
.call-icon {
width: 24rpx;
height: 24rpx;
margin-right: 5rpx;
}
}
}
}
.info-bottom {
background: linear-gradient(to right, #91b6ff, #87afff, #b7adff);
border-radius: 0 0 8rpx 8rpx;
padding: 12rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
color: #333;
}
.des-card {
padding: 24rpx;
font-size: 26rpx;
color: #666;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.empty-box {
padding: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.img {
width: 100%;
}
.content {
margin-top: 24rpx;
font-size: 30rpx;
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,245 @@
<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 blue" @click="confirm">确认</view>
</template>
<view class="content">
<view class="content-input">
<view class="input-titile">公司</view>
<input class="input-con" v-model="fromValue.company" placeholder="请输入公司名称" />
</view>
<view class="content-input">
<view class="input-titile">岗位</view>
<input class="input-con" v-model="fromValue.position" placeholder="请输入岗位" />
</view>
<view class="content-input">
<view class="input-titile">时间</view>
<view class="flex-box">
<view class="input-box btn-feel" @click="changestartTime">
<input v-model="fromValue.startTime" class="input-con triangle" disabled placeholder="开始时间" />
<image class="icon" src="@/static/icon/arrow-down.png" />
</view>
<view class="gap">-</view>
<view class="input-box btn-feel" @click="changeendTime">
<input v-model="fromValue.endTime" class="input-con triangle" disabled placeholder="至今" />
<image class="icon" src="@/static/icon/arrow-down.png" />
</view>
</view>
</view>
<view class="content-input">
<view class="input-titile">工作内容</view>
<textarea class="text-area" placeholder="请输入工作内容" v-model="fromValue.duty"></textarea>
</view>
</view>
<!-- 时间选择器组件 -->
<DatePicker ref="datePicker" />
<template #footer v-if="fromValue.id">
<view class="footer-container">
<view class="footer-button btn-feel" @click="delCurrent">删除该工作经历</view>
</view>
</template>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, computed } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
const { $api, navTo, navBack } = inject("globalFunction");
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
import DatePicker from "@/components/DatePicker/DatePicker.vue";
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
// 初始化数据
const fromValue = reactive({
position: "",
company: "",
startTime: "",
endTime: "",
duty: "",
id: undefined,
});
// 获取时间选择器组件的引用
const datePicker = ref();
onLoad((e) => {
initLoad(e?.id);
});
const confirm = async () => {
// 验证必填字段
if (!fromValue.company) {
return $api.msg("请输入公司名称");
}
if (!fromValue.position) {
return $api.msg("请输入岗位");
}
if (!fromValue.startTime) {
return $api.msg("请选择开始时间");
}
// 验证时间逻辑:结束时间不能早于开始时间
if (fromValue.endTime && new Date(fromValue.endTime) < new Date(fromValue.startTime)) {
return $api.msg("结束时间不能早于开始时间");
}
let res;
try {
if (fromValue.id) {
res = await $api.createRequest("/app/user/experience/edit", fromValue, "post");
} else {
res = await $api.createRequest("/app/user/experience/save", fromValue, "post");
}
$api.msg("保存成功");
getUserResume().then(() => {
navBack();
});
} catch (error) {
$api.msg("保存失败");
}
};
function delCurrent() {
uni.showModal({
title: "提示",
content: "确认要删除此条工作经历吗?",
showCancel: true,
success: async ({ confirm, cancel }) => {
if (confirm) {
await $api.createRequest("/app/user/experience/delete", { id: fromValue.id }, "post");
$api.msg("删除成功");
getUserResume().then(() => {
navBack();
});
}
},
});
}
function initLoad(id) {
if (!id) return;
$api
.createRequest(`/app/user/experience/getSingle/${id}`, {}, "get")
.then((res) => {
Object.assign(fromValue, res.data);
})
.catch((err) => {
console.error("获取工作经历失败:", err);
});
}
// 选择开始时间
const changestartTime = () => {
console.log(1);
datePicker.value.open({
title: "选择开始时间",
defaultDate: fromValue.startTime,
success: (selectedDate) => {
fromValue.startTime = selectedDate;
},
});
};
// 选择结束时间
const changeendTime = () => {
datePicker.value.open({
title: "选择结束时间",
defaultDate: fromValue.endTime,
success: (selectedDate) => {
fromValue.endTime = selectedDate;
// 如果结束时间早于新的开始时间,清空结束时间
if (fromValue.startTime && new Date(fromValue.startTime) > new Date(selectedDate)) {
fromValue.endTime = "";
$api.msg("结束时间不能小于开始时间!");
}
},
});
};
</script>
<style lang="scss" scoped>
.btn.blue {
color: #1677ff;
}
.content {
padding: 28rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(100% - 120rpx);
}
.flex-box {
display: flex;
align-items: center;
.gap {
font-size: 40rpx;
font-weight: bold;
flex: 0.25;
text-align: center;
}
.icon {
width: 75rpx;
height: 50rpx;
}
.input-box {
flex: 0.375;
display: flex;
align-items: center;
}
}
.content-input {
margin-bottom: 48rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #ebebeb;
&:nth-last-of-type(1) {
border-bottom: none;
}
.input-titile {
font-weight: 400;
font-size: 28rpx;
color: #6a6a6a;
margin-bottom: 10rpx;
}
.input-con {
font-weight: 400;
font-size: 32rpx;
color: #333333;
line-height: 80rpx;
height: 80rpx;
position: relative;
}
.triangle {
pointer-events: none;
}
.text-area {
width: 100%;
height: 700rpx;
background: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
}
.footer-container {
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;
.footer-button {
width: 100%;
height: 90rpx;
background: #f93a4a;
border-radius: 8rpx;
color: #ffffff;
line-height: 90rpx;
text-align: center;
}
}
</style>

View File

@@ -1,12 +1,13 @@
{
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
"pages": [
//pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "青岛智慧就业平台",
// #ifdef H5
// #ifdef H5
"navigationStyle": "custom"
// #endif
// #endif
}
},
{
@@ -66,7 +67,6 @@
"navigationStyle": "custom"
}
}
],
"subpackages": [{
"root": "packageA",
@@ -78,7 +78,8 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
}, {
},
{
"path": "pages/post/post",
"style": {
"navigationBarTitleText": "职位详情",
@@ -86,7 +87,8 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
}, {
},
{
"path": "pages/UnitDetails/UnitDetails",
"style": {
"navigationBarTitleText": "单位详情",
@@ -94,7 +96,8 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
}, {
},
{
"path": "pages/exhibitors/exhibitors",
"style": {
"navigationBarTitleText": "参展单位",
@@ -102,19 +105,31 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
}, {
},
{
"path": "pages/myResume/myResume",
"style": {
"navigationBarTitleText": "我的简历",
"navigationBarBackgroundColor": "#FFFFFF"
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
}, {
},
{
"path": "pages/vCard/vCard",
"style": {
"navigationBarTitleText": "电子名片",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
"path": "pages/Intendedposition/Intendedposition",
"style": {
"navigationBarTitleText": "投递记录",
"navigationBarBackgroundColor": "#FFFFFF"
}
}, {
},
{
"path": "pages/collection/collection",
"style": {
"navigationBarTitleText": "我的收藏",
@@ -158,6 +173,13 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/workExp/workExp",
"style": {
"navigationBarTitleText": "工作经历",
"navigationStyle": "custom"
}
},
{
"path": "pages/reservation/reservation",
"style": {
@@ -186,10 +208,27 @@
"navigationBarTitleText": "系统通知",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
{
"path": "pages/tiktok/tiktok",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
"path": "pages/moreJobs/moreJobs",
"style": {
"navigationBarTitleText": "更多岗位",
"navigationBarBackgroundColor": "#FFFFFF"
}
}
]
}],
"tabBar": {
// "custom": true,
// "display": "none",
"color": "#5E5F60",
"selectedColor": "#256BFA",
"borderStyle": "black",

BIN
pages/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,544 @@
<template>
<view class="app-custom-root">
<view class="app-container">
<!-- 顶部头部区域 -->
<view class="container-header">
<view class="header-top">
<view class="header-btnLf button-click" @click="seemsg(0)" :class="{ active: state.current === 0 }">
现场招聘
</view>
<view class="header-btnLf button-click" @click="seemsg(1)" :class="{ active: state.current === 1 }">
VR虚拟招聘会
</view>
</view>
<view class="header-input btn-feel">
<uni-icons class="iconsearch" color="#666666" type="search" size="18"></uni-icons>
<input class="input" placeholder="招聘会" placeholder-class="inputplace" />
</view>
<view class="header-date">
<view class="data-week">
<view
class="weel-days button-click"
:class="{ active: currentDay.fullDate === item.fullDate }"
v-for="(item, index) in weekList"
:key="index"
@click="selectDate(item)"
>
<view class="label">{{ item.day }}</view>
<view class="day">{{ item.date }}</view>
</view>
</view>
<view class="data-all">
<image class="allimg button-click" @click="toSelectDate" src="/static/icon/date1.png"></image>
</view>
</view>
</view>
<!-- 主体内容区域 -->
<view class="container-main">
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<view class="cards">
<view
class="card press-button"
v-for="(item, index) in fairList"
:key="index"
@click="navTo('/packageA/pages/exhibitors/exhibitors?jobFairId=' + item.jobFairId)"
>
<view class="card-title">{{ item.zphmc }}</view>
<view class="card-row">
<text class="">{{ item.zphdz }}</text>
<text class="">
<convert-distance
:alat="item.latitude"
:along="item.longitude"
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance>
</text>
</view>
<view class="card-times">
<view class="time-left">
<view class="left-date">{{ parseDateTime(item.zphjbsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjbsj).date }}</view>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(item.zphjbsj, item.zphjzsj).statusText }}
</view>
<view class="center-dateDay">
{{ getHoursBetween(item.zphjbsj, item.zphjzsj) }}小时
</view>
</view>
<view class="line"></view>
<view class="time-right">
<view class="left-date">{{ parseDateTime(item.zphjzsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjzsj).date }}</view>
</view>
</view>
<view class="recommend-card-line"></view>
<view class="card-footer">内容简介{{ item.zphjj }}</view>
</view>
</view>
<empty v-if="!fairList.length" pdTop="200"></empty>
</scroll-view>
</view>
<Tabbar :currentpage="1"></Tabbar>
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navTo, cloneDeep } = inject('globalFunction');
const weekList = ref([]);
const fairList = ref([]);
const currentDay = ref({});
const state = reactive({
current: 0,
all: [{}],
});
const pageState = reactive({
page: 0,
total: 0,
maxPage: 2,
pageSize: 10,
search: {},
});
onLoad(() => {
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 currentDate = `${year}-${month}-${day}`;
const result = getNextDates({
startDate: currentDate,
});
weekList.value = result;
currentDay.value.fullDate = result[0].fullDate;
getFair('refresh');
});
function toSelectDate() {
navTo('/packageA/pages/selectDate/selectDate', {
query: {
date: currentDay.value.fullDate,
},
onBack: (res) => {
console.log(res);
const result = getNextDates({
startDate: res.date,
});
const formattedDate = res.date.slice(5); // MM-DD
const dateFull = {
date: res.date.slice(5),
day: '周' + res.week,
fullDate: res.date,
};
currentDay.value = dateFull;
weekList.value = result;
getFair('refresh');
},
});
}
// 查看消息类型
function changeSwiperMsgType(e) {
const currented = e.detail.current;
state.current = currented;
}
function seemsg(index) {
if (index === 1) {
return $api.msg('功能确定中');
}
state.current = index;
}
const handleScrollToLower = () => {
return;
getFair();
console.log('触底');
};
function getFair(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
let params = {
...pageState.search,
// current: pageState.page,
// pageSize: pageState.pageSize,
};
// if (currentDay.value?.fullDate) {
// params.queryDate = currentDay.value.fullDate;
// }
if (currentDay.value?.fullDate) {
params.zphjbsj = currentDay.value.fullDate.replace(/-/g, '');
}
$api.createRequest('/app/internal/jobFairThirdPart', params).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
// const str = pageState.pageSize * (pageState.page - 1);
// const end = fairList.value.length;
// const reslist = rows;
// fairList.value.splice(str, end, ...reslist);
fairList.value = rows;
} else {
fairList.value = rows;
}
// pageState.list = resData.rows;
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
});
}
function parseDateTime(datetimeStr) {
if (!datetimeStr) return { time: '', date: '' };
const dateObj = new Date(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 = new Date(startTimeStr);
const endTime = new Date(endTimeStr);
// 判断状态0 开始中1 过期2 待开始
let status = 0;
let statusText = '开始中';
if (now < startTime) {
status = 2; // 待开始
statusText = '待开始';
} else if (now > endTime) {
status = 1; // 已过期
statusText = '已过期';
} else {
status = 0; // 进行中
statusText = '进行中';
}
return {
status, // 0: 进行中1: 已过期2: 待开始
statusText,
};
}
function getHoursBetween(startTimeStr, endTimeStr) {
const start = new Date(startTimeStr);
const end = new Date(endTimeStr);
const diffMs = end - start;
const diffHours = diffMs / (1000 * 60 * 60);
return +diffHours.toFixed(2); // 保留 2 位小数
}
const selectDate = (item) => {
if (currentDay.value?.fullDate === item.fullDate) {
currentDay.value = {};
getFair('refresh');
return;
}
currentDay.value = item;
getFair('refresh');
};
function getNextDates({ startDate = '', count = 6 }) {
const baseDate = startDate ? new Date(startDate) : new Date(); // 指定起点或今天
const dates = [];
const dayNames = ['日', '一', '二', '三', '四', '五', '六'];
for (let i = 0; i < count; i++) {
const date = new Date(baseDate);
date.setDate(baseDate.getDate() + i);
const fullDate = date.toISOString().slice(0, 10); // YYYY-MM-DD
const formattedDate = fullDate.slice(5); // MM-DD
const dayOfWeek = dayNames[date.getDay()];
dates.push({
date: formattedDate,
fullDate,
day: '周' + dayOfWeek,
});
}
// 可选设置默认选中项
// currentDay.value = dates[0];
return dates;
}
</script>
<style lang="stylus" scoped>
.app-custom-root {
position: fixed;
z-index: 10;
width: 100vw;
height: calc(100% - var(--window-bottom));
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
.container-header {
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 400rpx;
.header-top{
display: flex;
line-height: calc(88rpx - 14rpx);
padding: 16rpx 44rpx 14rpx 44rpx;
.header-btnLf {
display: flex;
width: fit-content;
white-space: nowrap
justify-content: flex-start;
align-items: center;
width: calc(60rpx * 3);
font-weight: 500;
font-size: 40rpx;
color: #696969;
margin-right: 44rpx;
position: relative;
.btns-wd{
position: absolute
top: 2rpx;
right: 2rpx
width: 16rpx;
height: 16rpx;
background: #F73636;
border-radius: 50%;
border: 4rpx solid #EEEEFF;
}
}
.active {
font-weight: 600;
font-size: 40rpx;
color: #000000;
}
}
.header-input{
padding: 0 24rpx
width: calc(100% - 48rpx);
position: relative
.iconsearch{
position: absolute
left: 50rpx;
top: 50%
transform: translate(0, -50%)
}
.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;
}
}
.header-date{
padding: 28rpx
display: flex
justify-content: space-between
align-items: center
.data-week{
flex: 1
display: flex
justify-content: space-between
flex-wrap: nowrap
overflow: hidden
.weel-days{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
display: flex
justify-content: center
flex-direction: column
text-align: center
font-weight: 400;
font-size: 24rpx;
color: #333333;
width: 96rpx;
height: 88rpx;
.label{}
.day{
font-weight: 500;
}
}
.active{
background: rgba(37,107,250,0.1);
border-radius: 12rpx 12rpx 12rpx 12rpx;
color: #256BFA;
}
}
.data-all{
width: 66rpx;
height: 66rpx;
margin-left: 18rpx
.allimg{
width: 100%;
height: 100%
}
}
}
}
}
.container-main {
flex: 1;
overflow: hidden;
background-color: #f4f4f4;
}
.main-scroll {
width: 100%
height: 100%;
}
.cards{
padding: 28rpx 28rpx 28rpx 28rpx;
.card{
margin-top: 28rpx
padding: 32rpx;
background: #FFFFFF
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
.card-title{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.card-row{
display: flex
justify-content: space-between
font-weight: 400;
font-size: 28rpx;
color: #495265;
margin-top: 4rpx
}
.card-times{
display: flex;
justify-content: space-between
align-items: center
margin-top: 24rpx
.time-left,
.time-right{
text-align: center
.left-date{
font-weight: 500;
font-size: 48rpx;
color: #333333;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
.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;
}
}
}
.recommend-card-line{
width: calc(100%);
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 2rpx dashed rgba(0,0,0,0.14);
margin-top: 32rpx
position: relative
}
.recommend-card-line::before{
position: absolute
content: ''
left: 0
top: 0
transform: translate(-50% - 110rpx, -50%)
width: 28rpx;
height: 28rpx;
background: #F4F4F4;
border-radius: 50%;
}
.recommend-card-line::after{
position: absolute
content: ''
right: 0
top: 0
transform: translate(50% + 100rpx, -50%)
width: 28rpx;
height: 28rpx;
background: #F4F4F4;
border-radius: 50%;
}
.card-footer{
margin-top: 32rpx
min-height: 50rpx;
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
}
}
.card:first-child{
margin-top: 0
}
}
</style>

View File

@@ -13,7 +13,14 @@
</view>
<view class="header-input btn-feel">
<uni-icons class="iconsearch" color="#666666" type="search" size="18"></uni-icons>
<input class="input" placeholder="招聘会" placeholder-class="inputplace" />
<input
v-model="pageState.zphmc"
confirm-type="search"
@confirm="getFair"
class="input"
placeholder="招聘会"
placeholder-class="inputplace"
/>
</view>
<view class="header-date">
<view class="data-week">
@@ -37,52 +44,60 @@
<!-- 主体内容区域 -->
<view class="container-main">
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<view class="cards" v-if="fairList.length">
<view class="cards">
<view
class="card btn-incline"
class="card press-button"
v-for="(item, index) in fairList"
:key="index"
@click="navTo('/packageA/pages/exhibitors/exhibitors?jobFairId=' + item.jobFairId)"
@click="
navTo(
'/packageA/pages/exhibitors/exhibitors?jobFairId=' +
item.zphID +
'&jobFairName=' +
item.zphmc
)
"
>
<view class="card-title">{{ item.name }}</view>
<view class="card-title">{{ item.zphmc }}</view>
<view class="card-row">
<text class="">{{ item.location }}</text>
<text class="">{{ item.jbf }}</text>
<text class="">
<convert-distance
<!-- <convert-distance
:alat="item.latitude"
:along="item.longitude"
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance>
></convert-distance> -->
</text>
</view>
<view class="card-times">
<view class="time-left">
<view class="left-date">{{ parseDateTime(item.startTime).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.startTime).date }}</view>
<view class="left-date">{{ parseDateTime(item.zphjbsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjbsj).date }}</view>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(item.startTime, item.endTime).statusText }}
{{ getTimeStatus(item.zphjbsj, item.zphjzsj).statusText }}
</view>
<view class="center-dateDay">
{{ getHoursBetween(item.startTime, item.endTime) }}小时
{{ getHoursBetween(item.zphjbsj, item.zphjzsj) }}小时
</view>
</view>
<view class="line"></view>
<view class="time-right">
<view class="left-date">{{ parseDateTime(item.endTime).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.endTime).date }}</view>
<view class="left-date">{{ parseDateTime(item.zphjzsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjzsj).date }}</view>
</view>
</view>
<view class="recommend-card-line"></view>
<view class="card-footer">内容简介{{ item.description }}</view>
<view class="card-footer">内容简介{{ item.zphjj }}</view>
</view>
</view>
<empty v-else pdTop="200"></empty>
<empty v-if="!fairList.length"></empty>
</scroll-view>
</view>
<!-- <Tabbar :currentpage="1"></Tabbar> -->
</view>
</view>
</template>
@@ -90,10 +105,11 @@
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navTo, cloneDeep } = inject('globalFunction');
const { $api, navTo, cloneDeep, debounce } = inject('globalFunction');
const weekList = ref([]);
const fairList = ref([]);
const currentDay = ref({});
@@ -106,7 +122,7 @@ const pageState = reactive({
total: 0,
maxPage: 2,
pageSize: 10,
search: {},
zphmc: '',
});
onLoad(() => {
@@ -120,6 +136,7 @@ onLoad(() => {
startDate: currentDate,
});
weekList.value = result;
currentDay.value.fullDate = result[0].fullDate;
getFair('refresh');
});
@@ -127,6 +144,7 @@ function toSelectDate() {
navTo('/packageA/pages/selectDate/selectDate', {
query: {
date: currentDay.value.fullDate,
entrance: 'careerfair',
},
onBack: (res) => {
console.log(res);
@@ -159,6 +177,7 @@ function seemsg(index) {
}
const handleScrollToLower = () => {
return;
getFair();
console.log('触底');
};
@@ -172,21 +191,24 @@ function getFair(type = 'add') {
pageState.page += 1;
}
let params = {
...pageState.search,
current: pageState.page,
pageSize: pageState.pageSize,
zphmc: pageState.zphmc,
// current: pageState.page,
// pageSize: pageState.pageSize,
};
// if (currentDay.value?.fullDate) {
// params.queryDate = currentDay.value.fullDate;
// }
if (currentDay.value?.fullDate) {
params.queryDate = currentDay.value.fullDate;
params.zphjbsj = currentDay.value.fullDate.replace(/-/g, '');
}
$api.createRequest('/app/fair', params).then((resData) => {
$api.createRequest('/app/internal/jobFairThirdPart', params, 'GET', true).then((resData) => {
const { rows, total } = resData;
console.log(rows);
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = fairList.value.length;
const reslist = rows;
fairList.value.splice(str, end, ...reslist);
// const str = pageState.pageSize * (pageState.page - 1);
// const end = fairList.value.length;
// const reslist = rows;
// fairList.value.splice(str, end, ...reslist);
fairList.value = rows;
} else {
fairList.value = rows;
}
@@ -251,8 +273,8 @@ function getHoursBetween(startTimeStr, endTimeStr) {
const selectDate = (item) => {
if (currentDay.value?.fullDate === item.fullDate) {
currentDay.value = {};
getFair('refresh');
// currentDay.value = {};
// getFair('refresh');
return;
}
currentDay.value = item;
@@ -371,6 +393,7 @@ function getNextDates({ startDate = '', count = 6 }) {
flex-wrap: nowrap
overflow: hidden
.weel-days{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
display: flex
justify-content: center
flex-direction: column
@@ -424,6 +447,7 @@ function getNextDates({ startDate = '', count = 6 }) {
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
.card-title{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
color: #333333;
@@ -448,6 +472,7 @@ function getNextDates({ startDate = '', count = 6 }) {
font-weight: 500;
font-size: 48rpx;
color: #333333;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
.left-dateDay{
font-weight: 400;

View File

@@ -62,10 +62,11 @@
<view class="chatmain-warpper">
<ai-paging ref="paging"></ai-paging>
</view>
<!-- 自定义tabbar -->
<!-- <view class="chatmain-footer" v-show="!isDrawerOpen">
<Tabbar :currentpage="2"></Tabbar>
</view> -->
</view>
<!-- 自定义tabbar -->
<!-- <tabbar-custom :currentpage="2"></tabbar-custom> -->
</view>
</template>
@@ -73,6 +74,7 @@
import { ref, inject, nextTick, computed } from 'vue';
const { $api, navTo, insertSortData } = inject('globalFunction');
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import useUserStore from '@/stores/useUserStore';
import aiPaging from './components/ai-paging.vue';
@@ -88,6 +90,7 @@ const paging = ref(null);
// 实时过滤
const filteredList = computed(() => {
// console.log(tabeList.value);
if (!searchText.value) return tabeList.value;
const list = tabeList.value.filter((item) => !item.isTitle && item.title.includes(searchText.value));
const [result, lastData] = $api.insertSortData(list);
@@ -108,16 +111,16 @@ onHide(() => {
paging.value?.handleTouchCancel();
if (isDrawerOpen.value) {
isDrawerOpen.value = false;
uni.showTabBar();
// uni.showTabBar();
}
});
const toggleDrawer = () => {
isDrawerOpen.value = !isDrawerOpen.value;
if (isDrawerOpen.value) {
uni.hideTabBar();
// uni.hideTabBar();
} else {
uni.showTabBar();
// uni.showTabBar();
}
};
@@ -144,6 +147,7 @@ function updateSetting() {
<style lang="stylus" scoped>
header-height = 88rpx
footer-height = 98rpx
/* 页面容器 */
.container {
@@ -209,27 +213,48 @@ header-height = 88rpx
background: #FFFFFF;
display: flex
flex-direction: column
.drawer-user
border-top: 1rpx solid rgba(0,0,0,.1);
padding: 20rpx 28rpx
display: flex
.drawer-user {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
background-color: #ffffff;
color: #333333;
font-weight: 500;
align-items: center
position: relative
margin-bottom: calc( 32rpx + var(--window-bottom)); /*兼容 IOS<11.2*/
margin-bottom: calc( 32rpx +var(--window-bottom)); /*兼容 IOS>11.2*/
color: #000000
.drawer-user-img
width: 57.2rpx;
height: 57.2rpx
margin-right: 20rpx
.drawer-user-setting
width: 48rpx
height: 48rpx
position: absolute
top: 50%
right: 28rpx
transform: translate(0,-50%)
font-size: 28rpx;
&:active {
background-color: #f9f9f9;
}
.drawer-user-img {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
margin-right: 24rpx;
background-color: #eee;
flex-shrink: 0;
}
.user-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 30rpx;
}
.drawer-user-setting {
width: 48rpx;
height: 48rpx;
margin-left: auto;
opacity: 0.8;
}
}
.drawer-title
height: header-height;
line-height: header-height;
@@ -277,6 +302,8 @@ header-height = 88rpx
transition: margin-left 0.3s ease-in-out;
position: relative
background: #FFFFFF
display: flex
flex-direction: column
.head
display: block;
box-sizing: border-box;
@@ -304,15 +331,20 @@ header-height = 88rpx
height: 37rpx;
.chatmain-warpper
height: 'calc(100% - %s)' % header-height
height: 'calc(100% - %s)' %( header-height + footer-height)
position: relative;
display: block;
box-sizing: border-box;
width: 100%;
border-top: 2rpx solid #F4F4F4;
flex: 1
/* 页面被挤压时向右移动 */
.main-content.shift {
margin-left: 500rpx;
}
</style>
.chatmain-footer{
height: footer-height;
}
</style>

View File

@@ -113,7 +113,8 @@
</view>
</view>
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
<!-- <view class="message">{{ recognizedText }} {{ lastFinalText }}</view> -->
<view class="message">{{ recognizedText }}</view>
</view>
<view v-if="isTyping" class="self">
<text class="message msg-loading">
@@ -147,7 +148,7 @@
<view
class="input_vio"
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove"
@touchmove.passive="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
:catchtouchstart="true"
@@ -249,8 +250,6 @@ import {
ref,
inject,
nextTick,
defineProps,
defineEmits,
onMounted,
onUnmounted,
toRaw,
@@ -268,14 +267,18 @@ import AudioWave from './AudioWave.vue';
import WaveDisplay from './WaveDisplay.vue';
import FileIcon from './fileIcon.vue';
import FileText from './fileText.vue';
// 系统功能hook和阿里云hook
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
import successIcon from '@/static/icon/success.png';
// hook
// 语音识别
const {
isRecording,
startRecording,
@@ -285,9 +288,9 @@ const {
volumeLevel,
recognizedText,
lastFinalText,
} = useAudioRecorder(config.vioceBaseURl);
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio, audioUrl } = useTTSPlayer(config.speechSynthesis);
} = useAudioRecorder();
// 语音合成
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
// state
const queries = ref([]);
@@ -443,7 +446,7 @@ const scrollToBottom = throttle(function () {
}, 500);
function getGuess() {
$api.chatRequest('/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
$api.chatRequest('/app/chat/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
guessList.value = res.data;
showGuess.value = true;
nextTick(() => {
@@ -629,6 +632,7 @@ function readMarkdown(value, index) {
if (isPaused.value) {
resume();
} else {
// console.log(value, speechIndex.value, index, isPaused.value)
speak(value);
}
}

View File

@@ -37,7 +37,7 @@
</template>
<script setup>
import { ref, inject, defineEmits } from 'vue';
import { ref, inject } from 'vue';
const emit = defineEmits(['onSend']);
const { $api } = inject('globalFunction');
const popup = ref(null);

View File

@@ -0,0 +1,371 @@
<template>
<view class="container" id="pixi-box" ref="pixiContainerRef"></view>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick } from 'vue';
const emit = defineEmits(['tag-click']);
// DOM Ref
const pixiContainerRef = ref(null);
// PIXI 变量
let app = null;
let tagsContainer = null;
let activeTagInstances = [];
// 配置数据
const mockTags = [
{ name: '医生', bgColor: 0x0069fe, fontColor: 0xffffff, size: 17, opacity: 1.0, angle: 0, radius: 0 },
{
name: '工程师',
bgColor: 0x87e2ec,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: -Math.PI / 2,
radius: 68,
tailRotation: Math.PI / 2,
},
{
name: '建筑师',
bgColor: 0xffebeb,
tailColor: 0xffe1e1,
fontColor: 0xff6969,
size: 11.5,
opacity: 1,
angle: -Math.PI / 4.2,
radius: 125,
tailRotation: (3 * Math.PI) / 4,
},
{
name: '律师',
bgColor: 0x21ea85,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: -Math.PI / 10,
radius: 130,
tailRotation: (3 * Math.PI) / 4,
},
{
name: '记者',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 12,
opacity: 1,
angle: Math.PI / 120,
radius: 130,
tailRotation: (3 * Math.PI) / 3.4,
},
{
name: '程序员',
bgColor: 0xffd4b6,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: Math.PI / 7,
radius: 120,
tailRotation: (5 * Math.PI) / 4,
},
{
name: '摄影师',
bgColor: 0xd8e5fe,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 11,
opacity: 1,
angle: Math.PI / 3,
radius: 79,
tailRotation: (3 * Math.PI) / 2,
},
{
name: '设计师',
bgColor: 0xff9400,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: (2 * Math.PI) / 3,
radius: 92,
tailRotation: (7 * Math.PI) / 4,
},
{
name: '心理咨询师',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 10.5,
opacity: 1,
angle: (5.4 * Math.PI) / 6,
radius: 110,
tailRotation:(3 * Math.PI) /1.78,
},
{
name: '护士',
bgColor: 0xff6969,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: (6.3 * Math.PI) / 5.9,
radius: 110,
tailRotation: Math.PI / 4,
},
{
name: '会计',
bgColor: 0xfce9c9,
fontColor: 0xfbc55f,
size: 13,
opacity: 1,
angle: (7.2 * Math.PI) / 5.9,
radius: 120,
tailRotation: Math.PI / 4,
},
];
onMounted(async () => {
await nextTick();
setTimeout(() => {
initPixi();
}, 100);
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (app) {
app.destroy(true, { children: true, texture: true, baseTexture: true });
app = null;
}
});
const getContainerDOM = () => {
const refVal = pixiContainerRef.value;
if (!refVal) return document.getElementById('pixi-box');
if (refVal.$el) return refVal.$el;
return refVal;
};
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const initPixi = () => {
const container = getContainerDOM();
if (!container) return;
const width = container.clientWidth || 300;
const height = container.clientHeight || 300;
if (app) return;
app = new PIXI.Application({
width: width,
height: height,
backgroundAlpha: 0,
backgroundColor: 0xf5f7fa,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
});
app.view.style.touchAction = 'auto';
container.appendChild(app.view);
tagsContainer = new PIXI.Container();
app.stage.addChild(tagsContainer);
renderScene(width, height);
};
const renderScene = (sw, sh) => {
tagsContainer.removeChildren();
activeTagInstances = [];
const baseSize = 375;
const scaleFactor = (Math.min(sw, sh) / baseSize) * 0.9;
mockTags.forEach((data, index) => {
const scaledRadius = data.radius * (scaleFactor < 1 ? 1 : scaleFactor * 0.8);
let x = sw / 2 + scaledRadius * Math.cos(data.angle);
let y = sh / 2 + scaledRadius * Math.sin(data.angle);
const tag = createTag(data, index);
tagsContainer.addChild(tag);
const safeW = tag.width / 2 + 10;
const safeH = tag.height / 2 + 10;
// 强制修正 x 和 y使其不超出屏幕
x = clamp(x, safeW, sw - safeW);
y = clamp(y, safeH, sh - safeH);
tag.x = x;
tag.y = y;
// 4. 保存元数据
tag.userData = {
originalX: x,
originalY: y,
angle: data.angle,
radius: scaledRadius,
floatOffset: Math.random() * Math.PI * 2,
floatSpeed: 0.01 + Math.random() * 0.02,
floatRange: 2 + Math.random() * 2,
safeH: safeH,
};
if (data.radius > 0) {
const tail = createCometTail( data.tailColor || data.bgColor, data.tailRotation, tag.width);
tag.addChildAt(tail, 0);
tag.updateTail = () => tail.updateAnim();
}
activeTagInstances.push(tag);
});
// 动画循环
app.ticker.add(() => {
const screenH = app.screen.height;
activeTagInstances.forEach((tag) => {
const meta = tag.userData;
if (meta) {
// 计算新的浮动位置
meta.floatOffset += meta.floatSpeed;
let nextY = meta.originalY + Math.sin(meta.floatOffset) * meta.floatRange;
// 再次进行边界检查
if (nextY < meta.safeH) nextY = meta.safeH;
if (nextY > screenH - meta.safeH) nextY = screenH - meta.safeH;
tag.y = nextY;
if (tag.updateTail) tag.updateTail();
}
});
});
};
const createTag = (tagData, index) => {
const tagGroup = new PIXI.Container();
tagGroup.eventMode = 'static';
tagGroup.cursor = 'pointer';
tagGroup.on('pointertap', () => emit('tag-click', tagData));
const text = new PIXI.Text(tagData.name, {
fontFamily: ['PingFang SC', 'Microsoft YaHei', 'Arial'],
fontSize: tagData.size,
fill: tagData.fontColor,
padding: 4,
resolution: 2,
});
text.anchor.set(0.5);
const paddingH = 26;
const paddingV = 10;
let bgWidth = text.width + paddingH;
let bgHeight = text.height + paddingV;
if (index === 0) bgWidth = Math.max(bgWidth, tagData.size * 4.5);
const bg = new PIXI.Graphics();
bg.beginFill(tagData.bgColor, tagData.opacity ?? 1);
bg.drawRoundedRect(-bgWidth / 2, -bgHeight / 2, bgWidth, bgHeight, bgHeight / 2);
bg.endFill();
tagGroup.addChild(bg);
tagGroup.addChild(text);
return tagGroup;
};
const createCometTail = (bgColor, tailRotation, parentWidth) => {
const tailGroup = new PIXI.Container();
const graphics = new PIXI.Graphics();
tailGroup.addChild(graphics);
const baseLength = 45;
const startWidth = parentWidth * 0.6;
const endWidth = 20;
let breathPhase = Math.random() * Math.PI * 2;
const breathSpeed = 0.04;
tailGroup.updateAnim = () => {
breathPhase += breathSpeed;
const breathScale = 0.85 + 0.15 * Math.sin(breathPhase);
graphics.clear();
const currentLength = baseLength * breathScale;
const cos = Math.cos(tailRotation);
const sin = Math.sin(tailRotation);
const perpX = -sin;
const perpY = cos;
const p1 = { x: perpX * (startWidth / 2), y: perpY * (startWidth / 2) };
const p2 = { x: -perpX * (startWidth / 2), y: -perpY * (startWidth / 2) };
const endCX = cos * currentLength;
const endCY = sin * currentLength;
const p3 = { x: endCX - perpX * (endWidth / 2), y: endCY - perpY * (endWidth / 2) };
const p4 = { x: endCX + perpX * (endWidth / 2), y: endCY + perpY * (endWidth / 2) };
const segments = 8;
for (let i = 0; i < segments; i++) {
const t1 = i / segments;
const t2 = (i + 1) / segments;
const alpha = 0.4 * (1 - t1);
const sp1 = { x: p1.x + (p4.x - p1.x) * t1, y: p1.y + (p4.y - p1.y) * t1 };
const sp2 = { x: p2.x + (p3.x - p2.x) * t1, y: p2.y + (p3.y - p2.y) * t1 };
const ep1 = { x: p1.x + (p4.x - p1.x) * t2, y: p1.y + (p4.y - p1.y) * t2 };
const ep2 = { x: p2.x + (p3.x - p2.x) * t2, y: p2.y + (p3.y - p2.y) * t2 };
graphics.beginFill(bgColor, alpha);
graphics.moveTo(sp1.x, sp1.y);
graphics.lineTo(sp2.x, sp2.y);
graphics.lineTo(ep2.x, ep2.y);
graphics.lineTo(ep1.x, ep1.y);
graphics.endFill();
}
};
tailGroup.updateAnim();
return tailGroup;
};
const handleResize = () => {
const container = getContainerDOM();
if (!app || !container) return;
const w = container.clientWidth || 300;
const h = container.clientHeight || 300;
app.renderer.resize(w, h);
activeTagInstances.forEach((tag) => {
const meta = tag.userData;
if (!meta) return;
let newX = w / 2 + meta.radius * Math.cos(meta.angle);
let newY = h / 2 + meta.radius * Math.sin(meta.angle);
const safeW = tag.width / 2 + 10;
const safeH = tag.height / 2 + 10;
meta.originalX = clamp(newX, safeW, w - safeW);
meta.originalY = clamp(newY, safeH, h - safeH);
meta.safeH = safeH; // 更新安全高度
tag.x = meta.originalX;
});
};
</script>
<style scoped>
.container {
width: 100%;
height: 500rpx;
position: relative;
overflow: hidden;
color: #b9d3ff;
}
</style>

View File

@@ -0,0 +1,911 @@
<template>
<view class="app-container">
<view class="nav-hidden hidden-animation" :class="{ 'hidden-height': isScrollingDown }">
<view class="container-search">
<view class="search-input button-click" @click="navTo('/pages/search/search')">
<uni-icons class="iconsearch" color="#666666" type="search" size="18"></uni-icons>
<text class="inpute">职位名称薪资要求等</text>
</view>
<!-- <view class="chart button-click">职业图谱</view> -->
</view>
<view class="cards">
<view class="card press-button" @click="navTo('/pages/nearby/nearby')">
<view class="card-title">附近工作</view>
<view class="card-text">好岗职等你来</view>
</view>
<view class="card press-button" @click="navTo('/packageA/pages/choiceness/choiceness')">
<view class="card-title">精选企业</view>
<view class="card-text">优选职得信赖</view>
</view>
</view>
</view>
<view class="nav-filter">
<view class="filter-top" @touchmove.stop.prevent>
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<text>添加</text>
</view>
</view>
<view class="filter-bottom">
<view class="btm-left">
<view
class="button-click filterbtm"
:class="{ active: pageState.search.order === item.value }"
v-for="item in rangeOptions"
@click="handelHostestSearch(item)"
:key="item.value"
>
{{ item.text }}
</view>
</view>
<view class="btm-right button-click" @click="openFilter">
筛选
<image class="right-sx" :class="{ active: showFilter }" src="@/static/icon/shaixun.png"></image>
</view>
</view>
</view>
<view class="table-list">
<scroll-view :scroll-y="true" class="falls-scroll" @scroll="handleScroll" @scrolltolower="scrollBottom">
<view class="falls">
<custom-waterfalls-flow
:column="columnCount"
:columnSpace="columnSpace"
ref="waterfallsFlowRef"
:value="list"
>
<template v-slot:default="job">
<view class="item btn-feel" v-if="!job.recommend">
<view class="falls-card" @click="nextDetail(job)">
<view class="falls-card-pay">
<view class="pay-text">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="falls-card-title">{{ job.jobTitle }}</view>
<view class="fl_box fl_warp">
<view class="falls-card-education mar_ri10" v-if="job.education">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
<view class="falls-card-experience" v-if="job.experience">
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</view>
</view>
<view class="falls-card-company">
青岛
<dict-Label dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
<view class="falls-card-pepleNumber">
<view>
<image class="point2" src="/static/icon/pintDate.png"></image>
<view class="fl_1">
{{ job.postingDate || '发布日期' }}
</view>
</view>
<view>
<image class="point3" src="/static/icon/pointpeople.png"></image>
<view class="fl_1">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
</view>
<view class="falls-card-company2">
<image class="point3" src="/static/icon/point3.png"></image>
<view class="fl_1">
{{ job.companyName }}
</view>
</view>
<!-- <view class="falls-card-matchingrate">
<view class=""><matchingDegree :job="job"></matchingDegree></view>
<uni-icons type="star" size="30"></uni-icons>
</view> -->
</view>
</view>
<view class="item" :class="{ isBut: job.isBut }" v-else>
<view class="recommend-card">
<view class="card-content">
<view class="recommend-card-title">在找{{ job.jobCategory }}工作吗</view>
<view class="recommend-card-tip">{{ job.tip }}</view>
<view class="recommend-card-line"></view>
<view class="recommend-card-controll">
<view class="controll-no" @click="clearfindJob(job)">不是</view>
<view class="controll-yes" @click="findJob(job)">是的</view>
</view>
</view>
</view>
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
</view>
<empty v-if="!list.length"></empty>
</scroll-view>
</view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
<view class="maskFristEntry" v-if="maskFristEntry">
<view class="entry-content">
<text class="text1">左滑查看视频</text>
<text class="text2">左滑查看视频</text>
<view class="goExperience">去体验</view>
<view class="maskFristEntry-Close" @click="closeFristEntry">1</view>
</view>
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
import img from '@/static/icon/filter.png';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
const { $api, navTo, vacanciesTo, formatTotal } = inject('globalFunction');
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren, oneDictData } = useDictStore();
import useLocationStore from '@/stores/useLocationStore';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
import { useRecommedIndexedDBStore, jobRecommender } from '@/stores/useRecommedIndexedDBStore.js';
import { useScrollDirection } from '@/hook/useScrollDirection';
import { useColumnCount } from '@/hook/useColumnCount';
const { isScrollingDown, handleScroll } = useScrollDirection();
const recommedIndexDb = useRecommedIndexedDBStore();
const emits = defineEmits(['onShowTabbar']);
const waterfallsFlowRef = ref(null);
const loadmoreRef = ref(null);
const conditionSearch = ref({});
const waterfallcolumn = ref(2);
const maskFristEntry = ref(true);
const state = reactive({
tabIndex: 'all',
});
const list = ref([]);
const pageState = reactive({
page: 0,
total: 0,
maxPage: 2,
pageSize: 10,
search: {
order: 0,
},
});
const inputText = ref('');
const showFilter = ref(false);
const selectFilterModel = ref(null);
const showModel = ref(false);
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
]);
const isLoaded = ref(false);
const { columnCount, columnSpace } = useColumnCount(() => {
pageState.pageSize = 10 * (columnCount.value - 1);
getJobRecommend('refresh');
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
});
});
async function loadData() {
try {
if (isLoaded.value) return;
isLoaded.value = true;
} catch (err) {
isLoaded.value = false; // 重置状态允许重试
throw err;
}
}
function scrollBottom() {
loadmoreRef.value.change('loading');
if (state.tabIndex === 'all') {
getJobRecommend();
} else {
getJobList();
}
}
function findJob(job) {
if (job.isBut) {
$api.msg('已确认');
} else {
list.value = list.value.map((item) => {
if (item.recommend && item.jobCategory === job.jobCategory) {
return {
...item,
isBut: true,
};
}
return item;
});
const jobstr = job.jobCategory;
const jobsObj = {
地区: 'area',
岗位: 'jobTitle',
经验: 'experience',
};
const [name, value] = jobstr.split(':');
const nameAttr = jobsObj[name];
if (name === '岗位') {
conditionSearch.value[nameAttr] = value;
} else {
const valueAttr = oneDictData(nameAttr).filter((item) => item.label === value);
if (valueAttr.length) {
const val = valueAttr[0].value;
conditionSearch.value[nameAttr] = val;
}
}
}
}
function clearfindJob(job) {
if (job.isBut) {
$api.msg('已确认');
} else {
list.value = list.value.map((item) => {
if (item.recommend && item.jobCategory === job.jobCategory) {
return {
...item,
isBut: true,
};
}
return item;
});
recommedIndexDb.deleteRecords(job);
}
}
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
function openFilter() {
showFilter.value = true;
emits('onShowTabbar', false);
uni.hideTabBar();
selectFilterModel.value?.open({
title: '筛选',
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
getJobList('refresh');
uni.showTabBar();
},
cancel: () => {
showFilter.value = false;
emits('onShowTabbar', true);
uni.showTabBar();
},
});
}
function handleFilterConfirm(e) {
console.log(e);
}
function choosePosition(index) {
state.tabIndex = index;
list.value = [];
if (index === 'all') {
pageState.search = {
order: pageState.search.order,
};
inputText.value = '';
getJobRecommend('refresh');
} else {
// const id = useUserStore().userInfo.jobTitleId.split(',')[index];
pageState.search.jobTitle = userInfo.value.jobTitle[index];
inputText.value = '';
getJobList('refresh');
}
}
function handelHostestSearch(val) {
pageState.search.order = val.value;
if (state.tabIndex === 'all') {
getJobRecommend('refresh');
} else {
getJobList('refresh');
}
}
function getJobRecommend(type = 'add') {
if (type === 'refresh') {
list.value = [];
if (waterfallsFlowRef.value) waterfallsFlowRef.value.refresh();
}
let params = {
pageSize: pageState.pageSize,
sessionId: useUserStore().seesionId,
...pageState.search,
...conditionSearch.value,
};
let comd = {
recommend: true,
jobCategory: '',
tip: '确认你的兴趣,为您推荐更多合适的岗位',
};
$api.createRequest('/app/job/recommend', params).then((resData) => {
const { data, total } = resData;
pageState.total = 0;
if (type === 'add') {
// 记录系统
recommedIndexDb.getRecord().then((res) => {
if (res.length) {
// 数据分析系统
const resultData = recommedIndexDb.analyzer(res);
const { sort, result } = resultData;
// 岗位询问系统
const conditionCounts = Object.fromEntries(
sort.filter((item) => item[1] > 1) // 过滤掉次数为 1 的项
);
jobRecommender.updateConditions(conditionCounts);
const question = jobRecommender.getNextQuestion();
if (question) {
comd.jobCategory = question;
// 生成随机插入位置,排除前两个和最后两个位置
let insertIndex;
if (data.length <= 4) {
// 如果数据长度小于等于4直接插入到中间位置
insertIndex = Math.floor(data.length / 2);
} else {
// 生成2到data.length-2之间的随机位置
insertIndex = Math.floor(Math.random() * (data.length - 4)) + 2;
}
data.splice(insertIndex, 0, comd);
}
}
const reslist = dataToImg(data);
list.value.push(...reslist);
});
} else {
list.value = dataToImg(data);
}
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (data.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
// 当没有岗位刷新sessionId重新啦
if (!data.length) {
useUserStore().initSeesionId();
}
});
}
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === 'refresh') {
list.value = [];
pageState.page = 1;
pageState.maxPage = 2;
// waterfallsFlowRef.value.refresh();
if (waterfallsFlowRef.value) waterfallsFlowRef.value.refresh();
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
...pageState.search,
// ...conditionSearch.value,
};
$api.createRequest('/app/job/list', params).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const reslist = dataToImg(rows);
list.value.splice(str, end, ...reslist);
} else {
list.value = dataToImg(rows);
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value?.change('noMore');
} else {
loadmoreRef.value?.change('more');
}
}
});
}
function dataToImg(data) {
return data.map((item) => ({
...item,
image: img,
hide: true,
}));
}
defineExpose({ loadData });
</script>
<style lang="stylus" scoped>
// .maskFristEntry
// position: fixed;
// // right: 20rpx;
// // bottom: calc(50% - 200rpx);
// height: 100vh
// width: 100vw
// background: rgba(0,0,0,0.3)
// .entry-content
// display: flex;
// align-items: center
// position: absolute
// left: 50%
// top: 40%
// transform: translate(-50%, -50%)
// flex-direction: column
// background: url('@/static/imgs/fristEntry.png') 0 0 no-repeat;
// background-size: 100% 100%;
// width: 480rpx
// height: 584rpx
// // padding-left: 80rpx
// .text1
// margin-top: 370rpx
// font-size: 36rpx
// background: linear-gradient(273.34deg, #356CFA 3.58%, #A47FFD 85.84%);
// -webkit-background-clip: text;
// -webkit-text-fill-color: transparent;
// background-clip: text; /* 有些浏览器兼容用 */
// text-fill-color: transparent;
// padding-left: 28rpx
// .text2
// padding-left: 28rpx
// margin-top: 8rpx
// font-size: 20rpx;
// color: #666666;
// text-align: center;
// .indicateArrow
// height: 76rpx
// width: 68rpx
// .indicatefristEntry
// width: 244rpx
// height: 244rpx
// .goExperience
// margin-left: 28rpx
// margin-top: 28rpx
// width: 160rpx;
// height: 60rpx;
// background: linear-gradient( 180deg, #9974FD 0%, #286BFA 100%);
// border-radius: 12rpx 12rpx 12rpx 12rpx;
// font-size: 28rpx;
// color: #FFFFFF;
// text-align: center;
// line-height: 60rpx
// .maskFristEntry-Close
// position: absolute;
// left: calc(50% - 10rpx);
// bottom: -130rpx
// width: 42rpx
// height: 42rpx
// background: linear-gradient(273.34deg, #356CFA 3.58%, #A47FFD 85.84%);
// border-radius: 50%;
// .maskFristEntry-Close::before
// position: absolute;
// left: calc( 50% - 2rpx)
// top: calc( 50% - 10rpx)
// transform: rotate(45deg);
// content: ''
// background: #FFFFFF
// width: 4rpx
// height: 20rpx
// .maskFristEntry-Close::after
// position: absolute;
// left: calc( 50% - 2rpx)
// top: calc( 50% - 10rpx)
// transform: rotate(-45deg);
// content: ''
// background: #FFFFFF
// width: 4rpx
// height: 20rpx
.app-container
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
background-color: #FFFFFF;
display: flex;
flex-direction: column
.hidden-animation
max-height: 1000px;
transition: all 0.3s ease;
overflow: hidden;
.hidden-height
max-height: 0;
padding-top: 0;
padding-bottom: 0;
.container-search
padding: 16rpx 24rpx
display: flex
justify-content: space-between
.search-input
display: flex
align-items: center;
width: 100%
height: 80rpx;
line-height: 80rpx
// margin-right: 24rpx
background: #FFFFFF;
border-radius: 75rpx 75rpx 75rpx 75rpx;
.iconsearch
padding-left: 36rpx
.inpute
margin-left: 20rpx
font-weight: 400;
font-size: 28rpx;
color: #B5B5B5;
width: 100%
.chart
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
width: 170rpx;
background: radial-gradient( 0% 56% at 87% 61%, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0.47) 100%);
box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(210,210,210,0.14);
border-radius: 80rpx 80rpx 80rpx 80rpx;
border: 2rpx solid #FFFFFF;
text-align: center
font-weight: 500;
font-size: 28rpx;
height: 36rpx;
color: #000000;
padding: 20rpx 30rpx
.cards
padding: 10rpx 28rpx
display: grid
grid-gap: 38rpx;
grid-template-columns: 1fr 1fr;
.card
height: calc(158rpx - 40rpx);
padding: 22rpx 26rpx
box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(210,210,210,0.14);
border-radius: 16rpx 16rpx 16rpx 16rpx;
border: 2rpx solid #FFFFFF;
.card-title
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
font-size: 32rpx;
color: #000000;
.card-text
font-weight: 400;
font-size: 24rpx;
color: #9E9E9E;
margin-top: 4rpx
.card:first-child
background: radial-gradient( 0% 56% at 87% 61%, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0.47) 100%),
url('@/static/icon/fujin.png');
background-size: 100%, 100%
.card:last-child
background: radial-gradient( 0% 56% at 87% 61%, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0.47) 100%),
url('@/static/icon/jinxuan.png');
background-size: 100%, 100%
background-size: cover;
background-position: center;
.nav-filter
padding: 16rpx 28rpx 0 28rpx
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
-webkit-mask-image: linear-gradient(to right, black 60%, transparent);
mask-image: linear-gradient(to right, black 60%, transparent);
.jobs-left
display: flex
flex-wrap: nowrap
.job
font-weight: 400;
font-size: 36rpx;
color: #666D7F;
margin-right: 32rpx;
white-space: nowrap
.active
font-weight: 500;
font-size: 36rpx;
color: #000000;
.jobs-add
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
display: flex
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
line-height: 38rpx;
.filter-bottom
display: flex
justify-content: space-between
padding: 24rpx 0
.btm-left
display: flex
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
.filterbtm
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
margin-right: 24rpx
padding: 0rpx 16rpx
.active
font-weight: 500;
font-size: 32rpx;
color: #256BFA;
.btm-right
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
.table-list
background: #F4F4F4
flex: 1
overflow: hidden
.falls-scroll
width: 100%
height: 100%
.falls
padding: 28rpx 28rpx;
.item
position: relative;
// background: linear-gradient( 180deg, rgba(19, 197, 124, 0.4) 0%, rgba(255, 255, 255, 0) 30%), rgba(255, 255, 255, 0);
.falls-card
padding: 30rpx;
.falls-card-title
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
color: #606060;
text-align: left;
word-break:break-all
font-weight: 500;
font-size: 30rpx;
color: #333333;
margin-top: 10rpx
.falls-card-pay
// height: 50rpx;
word-break:break-all
color: #002979;
text-align: left;
display: flex;
align-items: end;
position: relative
.pay-text
font-family: DIN-Medium;
color: #4C6EFB;
padding-right: 10rpx
font-weight: 500;
font-size: 28rpx;
color: #4C6EFB;
line-height: 45rpx;
text-align: left;
.flame
position: absolute
bottom: 0
right: -10rpx
transform: translate(0, -30%)
width: 24rpx
height: 31rpx
.falls-card-education,.falls-card-experience
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: 20rpx;
white-space: nowrap
.falls-card-company,.falls-card-pepleNumber
margin-top: 20rpx;
font-size: 24rpx;
color: #999999;
line-height: 25rpx;
text-align: left;
.falls-card-pepleNumber
display: flex;
justify-content: space-between;
flex-wrap: wrap
margin-top: 10rpx;
font-weight: 400;
font-size: 24rpx;
color: #999999;
line-height: 46rpx
view
display:flex
align-items: center
white-space: nowrap;
.point2
margin: 0rpx 6rpx 0 2rpx
height: 22rpx
width: 22rpx
.point3
margin: 0rpx 4rpx 0 0
height: 28rpx
width: 28rpx
.falls-card-matchingrate
margin-top: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 21rpx;
color: #4778EC;
text-align: left;
.falls-card-company2
margin-top: 4rpx;
font-size: 24rpx;
color: #999999;
text-align: left;
display: flex
.point3
margin: 4rpx 4rpx 0 0
height: 26rpx
width: 26rpx
// 推荐卡片
.recommend-card::before
position: absolute
left: 0
top: 0
content: ''
height: 60rpx
width: 100%
height: 8rpx;
background: linear-gradient( to left, #9E74FD 0%, #256BFA 100%);
box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(0,54,170,0.15);
.recommend-card::after
content ''
position absolute
z-index 0
left 50%
top 40%
transform: translate(-50%, -50%)
width 250rpx
height 250rpx
background url('@/static/icon/backAI.png') no-repeat center center
opacity 0.6
background-size contain
pointer-events none
filter: blur(3rpx)
.recommend-card
padding 36rpx 24rpx
background: linear-gradient( 360deg, #DFE9FF 0%, #FFFFFF 52%, #FFFFFF 100%);
border-radius: 20rpx 20rpx 20rpx 20rpx;
position relative
box-shadow 0rpx 4rpx 8rpx 0rpx rgba(72, 89, 123, 0.3)
.card-content
position: relative;
z-index: 2;
.recommend-card-title
font-weight: 500;
font-size: 28rpx;
color: #333333;
.recommend-card-tip
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
margin-top: 28rpx
.recommend-card-line
width: calc(100%);
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 2rpx dashed rgba(0,0,0,0.14);
margin-top: 50rpx
position: relative
// .recommend-card-line::before
// position: absolute
// content: ''
// left: 0
// top: 0
// transform: translate(-50% - 90rpx, -50%)
// width: 28rpx;
// height: 28rpx;
// background: #F4F4F4;
// border-radius: 50%;
// .recommend-card-line::after
// position: absolute
// content: ''
// right: 0
// top: 0
// transform: translate(50% + 90rpx, -50%)
// width: 28rpx;
// height: 28rpx;
// background: #F4F4F4;
// border-radius: 50%;
.recommend-card-controll
display: flex
align-items: center
justify-content: space-between
margin-top: 40rpx
padding: 0 6rpx;
.controll-yes
width: 124rpx;
height: 60rpx;
background: rgba(37,107,250,0.1);
border-radius: 12rpx 12rpx 12rpx 12rpx;
text-align: center;
line-height: 60rpx
color: #256BFA
.controll-no
width: 124rpx;
height: 56rpx;
line-height: 56rpx
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #DEDEDE;
font-weight: 400;
font-size: 28rpx;
color: #333333;
text-align: center;
.controll-yes:active, .controll-no:active
width: 120rpx;
height: 66rpx;
line-height: 66rpx
background: #e8e8e8
border: 2rpx solid #e8e8e8
.isBut{
filter: grayscale(100%);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
<template>
<view class="app-container">
<view class="nav-filter">
<view class="filter-top" @touchmove.stop.prevent>
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/pages/search/search')">
<uni-icons class="iconsearch" color="#666D7F" type="search" size="18"></uni-icons>
<text>搜索</text>
</view>
</view>
</view>
<view class="cards">
<scroll-view :scroll-y="true" class="tab-scroll" @scrolltolower="scrollBottom">
<view class="scroll-content">
<custom-waterfalls-flow
ref="waterfallsFlowRef"
:column="columnCount"
:columnSpace="columnSpace"
:value="list"
>
<template v-slot:default="job">
<view class="slot-item">
<view class="job-image btn-feel" @click="nextVideo(job)">
<image class="cover-image" :src="job.cover" mode="aspectFill"></image>
<view class="cover-triangle"></view>
</view>
<view class="job-info" @click="nextDetail(job)">
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
:is-month="true"
></Salary-Expectation>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="title">{{ job.jobTitle }}</view>
<view class="desc">
<image class="point3" src="/static/icon/point3.png"></image>
<!-- <uni-icons type="location" size="14"></uni-icons> -->
<view class="descText">{{ job.companyName }}</view>
</view>
</view>
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
import { usePagination } from '@/hook/usePagination';
const { $api, navTo } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import img from '@/static/icon/filter.png';
import useLocationStore from '@/stores/useLocationStore';
import { useColumnCount } from '@/hook/useColumnCount';
import { useRecommedIndexedDBStore, jobRecommender } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
// status
const { userInfo } = storeToRefs(useUserStore());
const isLoaded = ref(false);
const waterfallsFlowRef = ref(null);
const loadmoreRef = ref(null);
const state = reactive({
tabIndex: 'all',
});
// 响应式搜索条件(可以被修改)
const searchParams = ref({});
const pageSize = ref(10);
const { list, loading, refresh, loadMore,finished } = usePagination(
(params) => $api.createRequest('/app/job/littleVideo', params),
dataToImg, // 转换函数
{
pageSize: pageSize,
search: searchParams,
dataKey: 'data',
onBeforeRequest: () => {
loadmoreRef.value?.change('loading');
},
}
);
watch(()=>finished.value, (newVal) => {
if (newVal) {
// 确保瀑布流组件知道数据已加载完成
loadmoreRef.value?.change('noMore')
}else{
loadmoreRef.value?.change('more')
}
})
// function imageloaded() {
// nextTick(() => {
// console.log('触发',finished.value)
// if (finished.value) {
// loadmoreRef.value?.change('noMore')
// } else {
// loadmoreRef.value?.change('more')
// }
// })
// }
const { columnCount, columnSpace } = useColumnCount(() => {
pageSize.value = 10 * (columnCount.value - 1);
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
});
});
async function loadData() {
try {
if (isLoaded.value) return;
isLoaded.value = true;
refresh();
} catch (err) {
isLoaded.value = false; // 重置状态允许重试
throw err;
}
}
async function choosePosition(index) {
state.tabIndex = index;
if (index === 'all') {
searchParams.value.jobTitle = '';
} else {
searchParams.value.jobTitle = userInfo.value.jobTitle[index];
}
console.log(searchParams.value);
refresh('refresh');
waterfallsFlowRef.value.refresh();
}
function scrollBottom() {
loadMore();
}
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
function nextVideo(job) {
uni.setStorageSync(`job-Info`, job);
navTo(`/packageA/pages/tiktok/tiktok`);
}
function dataToImg(data) {
return data.map((item) => ({
...item,
// image: item.cover,
image: img,
hide: true,
}));
}
defineExpose({ loadData });
</script>
<style lang="stylus" scoped>
.app-container
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
// background-color: #FFFFFF;
display: flex;
flex-direction: column
.cards
flex: 1;
overflow: hidden;
.tab-scroll
height: 100%;
white-space: nowrap;
text-overflow: clip;
.scroll-content{
padding: 24rpx
}
.nav-filter
padding: 16rpx 28rpx 30rpx 28rpx
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
-webkit-mask-image: linear-gradient(to right, black 60%, transparent);
mask-image: linear-gradient(to right, black 60%, transparent);
.jobs-left
display: flex
flex-wrap: nowrap
align-items: center
.job
font-weight: 400;
font-size: 28rpx;
color: #666D7F;
margin-right: 32rpx;
white-space: nowrap
.active
font-weight: 500;
font-size: 36rpx;
color: #000000;
.jobs-add
display: flex
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 28rpx;
color: #666D7F;
line-height: 38rpx;
.iconsearch
margin-right: 6rpx
.filter-bottom
display: flex
justify-content: space-between
padding: 24rpx 0
.btm-left
display: flex
.filterbtm
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
margin-right: 24rpx
padding: 0rpx 16rpx
.active
font-weight: 500;
font-size: 32rpx;
color: #256BFA;
.btm-right
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
.slot-item
background: #f4f4f4;
// background: #f6f8fa;
.job-image{
width: 100%;
height: 280rpx;
position: relative;
.cover-image{
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.cover-triangle{
position: absolute;
right: 20rpx;
top: 20rpx
width: 36rpx
height: 36rpx
border-radius: 50%
background: rgba(0,0,0,0.3)
}
.cover-triangle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-40%, -50%) rotate(90deg);
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-bottom: 12rpx solid #fff;
}
}
.job-info{
padding: 10rpx 10rpx 24rpx 24rpx
}
.salary
color: #4C6EFB;
font-size: 28rpx
display: flex
align-items: flex-start
justify-content: space-between
font-family: DIN-Medium;
.flame
margin-top: 4rpx
margin-right: 4rpx
width: 24rpx
height: 31rpx
.title
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 32rpx;
color: #333333;
margin-top: 6rpx;
white-space: pre-wrap
.desc
font-weight: 400;
font-size: 24rpx;
color: #6C7282;
margin-top: 6rpx;
display: flex
align-items: flex-start
.descText{
flex: 1
white-space: pre-wrap
}
.point3{
margin: 4rpx 4rpx 0 0
height: 26rpx
width: 26rpx
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<tabcontrolVue :current="tabCurrent">
<template v-slot:tab0>
<view class="login-content">
<image class="logo" src="../../static/logo.png"></image>
<image class="logo" src="@/static/logo.png"></image>
<view class="logo-title">就业</view>
</view>
<view class="btns">
@@ -109,6 +109,9 @@
</template>
</tabcontrolVue>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<view class="backdoor" @click="loginbackdoor">
<uni-icons type="gift-filled" size="30"></uni-icons>
</view>
</AppLayout>
</template>
@@ -125,7 +128,7 @@ const { getDictSelectOption, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup');
// status
const selectJobsModel = ref();
const tabCurrent = ref(0);
const tabCurrent = ref(1);
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
const state = reactive({
station: [],
@@ -238,13 +241,43 @@ function nextStep() {
// 获取职位
function getTreeselect() {
$api.createRequest('/app/common/jobTitle/treeselect', {}, 'GET').then((resData) => {
const LoadCache = (resData) => {
state.station = resData.data;
};
$api.createRequestWithCache('/app/common/jobTitle/treeselect', {}, 'GET', false, LoadCache).then(LoadCache);
}
function loginbackdoor() {
$api.createRequest('/app/mock/login', {}, 'post').then((resData) => {
$api.msg('模拟帐号密码测试登录成功');
loginSetToken(resData.token).then((resume) => {
if (resume.data.jobTitleId) {
// 设置推荐列表,每次退出登录都需要更新
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
nextStep();
}
});
});
}
// 登录
function loginTest() {
// uni.share({
// provider: 'weixin',
// scene: 'WXSceneSession',
// type: 2,
// imageUrl: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni@2x.png',
// success: function (res) {
// console.log('success:' + JSON.stringify(res));
// },
// fail: function (err) {
// console.log('fail:' + JSON.stringify(err));
// },
// });
const params = {
username: 'test',
password: 'test',
@@ -277,6 +310,11 @@ function complete() {
</script>
<style lang="stylus" scoped>
.backdoor{
position: fixed;
left: 0;
bottom: 200rpx;
}
.input-nx
position: relative
border-bottom: 2rpx solid #EBEBEB
@@ -284,12 +322,10 @@ function complete() {
display: flex
flex-wrap: wrap
.nx-item
padding: 20rpx 28rpx
width: fit-content
margin: 12rpx 12rpx 0 0;
padding: 12rpx 25rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E8EAEE;
margin-right: 24rpx
margin-top: 24rpx
.nx-item::before
position: absolute;
right: 20rpx;
@@ -315,7 +351,8 @@ function complete() {
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
position: fixed;
background: url('@/static/icon/background2.png') 0 0 no-repeat;
// background: linear-gradient( 180deg, #1677FF 0%, rgba(22,119,255,0) 54%, rgba(22,119,255,0) 100%);
// background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
display: flex;
flex-direction: column
@@ -390,6 +427,7 @@ function complete() {
font-size: 28rpx;
color: #6A6A6A;
.input-con
pointer-events: none;
font-weight: 400;
font-size: 32rpx;
color: #333333;

View File

@@ -2,7 +2,8 @@
<AppLayout title="我的" back-gorund-color="#F4F4F4">
<view class="mine-userinfo btn-feel" @click="navTo('/packageA/pages/myResume/myResume')">
<view class="userindo-head">
<image class="userindo-head-img" v-if="userInfo.sex === '0'" src="/static/icon/boy.png"></image>
<image class="userindo-head-img" v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image class="userindo-head-img" v-else-if="userInfo.sex === '0'" src="/static/icon/boy.png"></image>
<image class="userindo-head-img" v-else src="/static/icon/girl.png"></image>
</view>
<view class="userinfo-ls">
@@ -32,7 +33,7 @@
</view>
</view>
<view class="mini-cards">
<view class="card-top btn-feel">
<view class="card-top btn-feel" @click="navTo('/packageA/pages/vCard/vCard')">
<view class="top-title line_1">
<text>{{ userInfo.name || '暂无用户名' }}</text>
&nbsp;|&nbsp;
@@ -44,13 +45,11 @@
<text v-if="userInfo.jobTitle.length - 1 !== index">|</text>
</text>
</view>
<view class="top-btn button-click" @click="navTo('/packageA/pages/personalInfo/personalInfo')">
修改简历
</view>
<view class="top-btn button-click">电子名片</view>
</view>
<view class="card-main">
<view class="main-title">服务专区</view>
<view class="main-row btn-feel">
<view class="main-row btn-feel" @click="selectFile">
<view class="row-left">
<image class="left-img" src="@/static/icon/server1.png"></image>
<text class="left-text">实名认证</text>
@@ -96,23 +95,28 @@
></uni-popup-dialog>
</uni-popup>
</view>
<!-- <template #footer>
<Tabbar :currentpage="4"></Tabbar>
</template> -->
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import Tabbar from '@/components/tabbar/midell-box.vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import FileUploader from '@/utils/FileUploader.js';
const { $api, navTo } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
const popup = ref(null);
const { userInfo, Completion } = storeToRefs(useUserStore());
const counts = ref({});
const { userInfo, Completion, counts } = storeToRefs(useUserStore());
function logOut() {
popup.value.open();
}
onShow(() => {
getUserstatistics();
useUserStore().getUserstatistics();
});
function close() {
@@ -125,12 +129,14 @@ function confirm() {
const isAbove90 = (percent) => parseFloat(percent) < 90;
function getUserstatistics() {
$api.createRequest('/app/user/statistics').then((resData) => {
console.log(resData);
counts.value = resData.data;
});
function selectFile() {
// FileUploader.showMenuAndUpload({
// success: function (res) {
// alert('上传成功: ' + JSON.stringify(res));
// },
// });
}
function chooseFileUploadTest(pam) {}
</script>
<style lang="stylus" scoped>
@@ -153,6 +159,7 @@ function getUserstatistics() {
padding: 36rpx 36rpx 64rpx 36rpx
border-radius: 20rpx 20rpx 0rpx 0rpx;
position: relative
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
.top-title{
font-weight: 500;
font-size: 32rpx;
@@ -170,7 +177,7 @@ function getUserstatistics() {
width: auto;
max-width: 60%;
white-space: nowrap
overflow:hidden;
overflow:hidden;
text-overflow: ellipsis;
}
.top-btn{
@@ -214,6 +221,7 @@ function getUserstatistics() {
margin: 32rpx 16rpx 32rpx 10rpx
}
.left-text{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 28rpx;
color: #333333;
@@ -248,8 +256,9 @@ function getUserstatistics() {
justify-content: center
align-items: center
.mini-num{
font-family: DIN-Medium;
font-weight: 500;
font-size: 44rpx;
font-size: 46rpx;
color: #333333;
}
.mini-text{
@@ -279,6 +288,7 @@ function getUserstatistics() {
flex-direction: column;
align-items: flex-start;
.userinfo-ls-name
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
font-size: 40rpx;
color: #333333;
@@ -307,4 +317,4 @@ function getUserstatistics() {
border-radius: 2rpx
background: #A2A2A2;
transform: rotate(45deg)
</style>
</style>

View File

@@ -15,8 +15,20 @@
<!-- 主体内容区域 -->
<view class="container-main">
<swiper class="swiper" :current="state.current" @change="changeSwiperType">
<swiper-item class="swiper-item" v-for="(_, index) in 2" :key="index">
<swiper
class="swiper"
:disable-touch="disableTouch"
:current="state.current"
@change="changeSwiperType"
>
<swiper-item
class="swiper-item"
@touchstart.passive="handleTouchStart"
@touchmove.passive="handleTouchMove"
@touchend="disableTouch = false"
v-for="(_, index) in 2"
:key="index"
>
<!-- #ifndef MP-WEIXIN -->
<component :is="components[index]" :ref="(el) => handelComponentsRef(el, index)" />
<!-- #endif -->
@@ -27,6 +39,8 @@
</swiper-item>
</swiper>
</view>
<!-- <Tabbar :currentpage="3"></Tabbar> -->
</view>
</view>
</template>
@@ -34,6 +48,7 @@
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import ReadComponent from './read.vue';
import UnreadComponent from './unread.vue';
const loadedMap = reactive([false, false]);
@@ -43,6 +58,11 @@ import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
const { unreadCount } = storeToRefs(useReadMsg());
const disableTouch = ref(false);
const startPointX = ref(0);
const totalPage = 2;
const THRESHOLD = 5;
onShow(() => {
// 获取消息列表
useReadMsg().fetchMessages();
@@ -56,6 +76,40 @@ onMounted(() => {
handleTabChange(state.current);
});
function handleTouchStart(e) {
// 确保有触摸点
if (e.touches.length > 0) {
startPointX.value = e.touches[0].clientX;
disableTouch.value = false;
}
}
function handleTouchMove(e) {
if (e.touches.length === 0) return;
const currentX = e.touches[0].clientX;
const diffX = currentX - startPointX.value;
if (state.current === 0) {
if (diffX > THRESHOLD) {
disableTouch.value = true;
} else {
disableTouch.value = false;
}
return;
}
if (state.current === totalPage - 1) {
if (diffX < -THRESHOLD) {
disableTouch.value = true;
} else {
disableTouch.value = false;
}
return;
}
disableTouch.value = false;
}
const handelComponentsRef = (el, index) => {
if (el) {
swiperRefs[index].value = el;
@@ -63,9 +117,35 @@ const handelComponentsRef = (el, index) => {
};
// 查看消息类型
function changeSwiperType(e) {
const newIndex = e.detail.current;
const lastIndex = state.current;
const isSwipingRight = newIndex < lastIndex;
const isSwipingLeft = newIndex > lastIndex;
if (lastIndex === 0 && isSwipingRight) {
disableTouch.value = true;
state.current = 0;
setTimeout(() => {
disableTouch.value = false;
}, 50);
return;
}
if (lastIndex === totalPage - 1 && isSwipingLeft) {
disableTouch.value = true;
state.current = lastIndex;
setTimeout(() => {
disableTouch.value = false;
}, 50);
return;
}
const index = e.detail.current;
state.current = index;
handleTabChange(index);
disableTouch.value = false;
}
function changeType(index) {
state.current = index;
@@ -115,6 +195,7 @@ function changeSwiperMsgType(e) {
font-weight: bold;
}
.header-btnLf {
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: flex-start;
align-items: center;

View File

@@ -1,8 +1,8 @@
<template>
<scroll-view scroll-y class="main-scroll">
<view class="scrollmain">
<view v-if="msgList.length" class="scrollmain">
<view
class="list-card btn-feel"
class="list-card press-button"
v-for="(item, index) in msgList"
:key="index"
@click="seeDetail(item, index)"
@@ -35,7 +35,9 @@
<view class="info-text line_2">{{ item.subTitle || '消息' }}</view>
</view>
</view>
<empty v-if="!msgList.length"></empty>
</view>
<empty v-else pdTop="200" content="暂无消息~"></empty>
</scroll-view>
</template>
@@ -83,6 +85,7 @@ defineExpose({ loadData });
}
.scrollmain{
padding: 28rpx
height: calc(100% - 56rpx)
}
.read{
@@ -133,6 +136,8 @@ defineExpose({ loadData });
display: flex;
justify-content: space-between;
width: 100%
text
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
.card-time
font-weight: 400;
font-size: 28rpx;

View File

@@ -1,8 +1,8 @@
<template>
<scroll-view scroll-y class="main-scroll">
<view class="scrollmain">
<view v-if="unreadMsgList.length" class="scrollmain">
<view
class="list-card btn-feel"
class="list-card press-button"
v-for="(item, index) in unreadMsgList"
:key="index"
@click="seeDetail(item)"
@@ -33,7 +33,9 @@
<view class="info-text line_2">{{ item.subTitle || '消息' }}</view>
</view>
</view>
<empty v-if="!unreadMsgList.length"></empty>
</view>
<empty v-else pdTop="200" content="暂无消息~"></empty>
</scroll-view>
</template>
@@ -69,6 +71,7 @@ defineExpose({ loadData });
}
.scrollmain{
padding: 28rpx
height: calc(100% - 56rpx)
}
.read{
@@ -119,6 +122,8 @@ defineExpose({ loadData });
display: flex;
justify-content: space-between;
width: 100%
text
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
.card-time
font-weight: 400;
font-size: 28rpx;

View File

@@ -1,15 +1,26 @@
<template>
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom">
<view class="two-head">
<view
class="head-item"
:class="{ active: state.comId === item.commercialAreaId }"
v-for="(item, index) in state.comlist"
:key="item.commercialAreaName"
@click="clickCommercialArea(item)"
>
{{ item.commercialAreaName }}
<view class="head-all">
<text>热门商圈</text>
<text class="color_333333 button-click" @click="handleOpenBusinessDistrict">
更多
<uni-icons type="forward" color="#333333" size="14"></uni-icons>
</text>
</view>
<scroll-view class="scroll-head" :scroll-x="true" :scroll-into-view="activeTab" :show-scrollbar="false">
<view class="head-item-content">
<view
class="head-item"
:class="{ active: state.comId === item.commercialAreaId }"
v-for="(item, index) in comlistPuted"
:key="item.commercialAreaName"
@click="clickCommercialArea(item)"
>
{{ item.commercialAreaName }}
</view>
</view>
</scroll-view>
</view>
<view class="nearby-list">
<view class="nav-filter" @touchmove.stop.prevent>
@@ -58,23 +69,19 @@
</view>
</view>
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<empty v-if="!list.length"></empty>
<loadmore v-show="list.length > pageState.pageSize" ref="loadmoreRef"></loadmore>
</view>
</view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
<select-filter2-col ref="selectFilter2ColModel"></select-filter2-col>
</scroll-view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
import { storeToRefs } from 'pinia';
@@ -87,6 +94,7 @@ const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import point2 from '@/static/icon/point2.png';
import LocationPng from '@/static/icon/Location.png';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
import selectFilter2Col from '@/components/selectFilter/selectFilter2Col.vue';
const emit = defineEmits(['onFilter']);
const state = reactive({
@@ -96,12 +104,15 @@ const state = reactive({
comId: 0,
areaInfo: {},
});
const commercialAreaList = ref([]);
const isLoaded = ref(false);
const showFilter = ref(false);
const selectFilterModel = ref();
const selectFilter2ColModel = ref();
const fromValue = reactive({
area: 0,
});
const activeTab = ref('');
const loadmoreRef = ref(null);
const pageState = reactive({
page: 0,
@@ -114,6 +125,18 @@ const pageState = reactive({
});
const list = ref([]);
const comlistPuted = computed(() => {
// const commercialArea = state.comlist.find((item) => item.commercialAreaId === state.comId);
// if (commercialArea) {
// const otherItems = state.comlist.filter((item) => item.commercialAreaId !== state.comId);
// return [commercialArea, ...otherItems];
// } else {
// return [state.areaInfo, ...state.comlist];
// }
// activeTab.value = state.areaInfo.commercialAreaId;
return state.comlist;
});
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
@@ -149,7 +172,6 @@ function openFilter() {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
console.log(pageState.search);
getJobList('refresh');
},
cancel: () => {
@@ -201,7 +223,7 @@ function changeArea(area, item) {
}
function getBusinessDistrict() {
$api.createRequest(`/app/common/commercialArea`).then((resData) => {
$api.createRequest(`/app/common/commercialArea/getAllData`).then((resData) => {
if (resData.data.length) {
state.comlist = resData.data;
state.areaInfo = resData.data[0];
@@ -241,10 +263,12 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
@@ -264,29 +288,83 @@ function handleFilterConfirm(val) {
getJobList('refresh');
}
function handleOpenBusinessDistrict() {
if (commercialAreaList.value.length) {
openFilter2Col();
} else {
getBusinessDistrictList();
}
}
function getBusinessDistrictList() {
$api.createRequest(`/app/common/commercialArea`).then((resData) => {
if (resData.data.length) {
commercialAreaList.value = resData.data;
openFilter2Col();
}
});
}
function openFilter2Col() {
selectFilter2ColModel.value?.open({
data: commercialAreaList.value,
title: '商圈',
currentValue: state.comId,
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
latitude: values.latitude,
longitude: values.longitude,
};
state.areaInfo = values;
state.comId = values.value;
getJobList('refresh');
},
});
}
defineExpose({ loadData, handleFilterConfirm });
</script>
<style lang="stylus" scoped>
.scroll-head
width: 100%;
overflow: hidden;
.tabchecked
color: #4778EC !important
.nearby-scroll
overflow: hidden;
height: 100%;
background: #f4f4f4;
.two-head
margin: 22rpx;
padding: 22rpx;
display: flex;
flex-wrap: wrap
flex-direction: column
flex-wrap: no-wrap
// grid-template-columns: repeat(4, 1fr);
// grid-column-gap: 10rpx;
// grid-row-gap: 24rpx;
border-radius: 17rpx 17rpx 17rpx 17rpx;
background: #FFFFFF
// border-radius: 17rpx 17rpx 17rpx 17rpx;
.head-all{
display: flex;
justify-content: space-between;
align-items: center
margin-bottom: 16rpx
}
.head-item-content{
display: flex
flex-wrap: nowrap
}
.head-item
padding: 0 10rpx
margin: 10rpx
white-space: nowrap
min-width: 156rpx
// min-width: 156rpx
line-height: 64rpx
text-align: center;
width: fit-content;
// width: fit-content;
font-size: 21rpx;
font-weight: 400;
font-size: 28rpx;
@@ -294,20 +372,27 @@ defineExpose({ loadData, handleFilterConfirm });
background: #F6F6F6;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
color: #256BFA;
background: #E9F0FF;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.nearby-list
border-top: 2rpx solid #EBEBEB;
height: 100%
min-height: calc(100% - 140rpx)
background: #f4f4f4
display: flex;
flex-direction: column;
.one-cards{
height: 100%
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
flex: 1
}
.nav-filter
padding: 16rpx 28rpx 0 28rpx
background: #ffffff
.filter-top
display: flex
justify-content: space-between;
@@ -330,6 +415,7 @@ defineExpose({ loadData, handleFilterConfirm });
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;

View File

@@ -74,14 +74,9 @@
</view>
</view>
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<empty v-if="!list.length"></empty>
<loadmore v-show="list.length > pageState.pageSize" ref="loadmoreRef"></loadmore>
</view>
</view>
<!-- 筛选 -->
@@ -220,7 +215,7 @@ function handleControl(e) {
}
onMounted(() => {
$api.msg('使用模拟定位');
// $api.msg('使用模拟定位');
getInit();
});
@@ -292,10 +287,12 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
@@ -362,20 +359,28 @@ defineExpose({ loadData, handleFilterConfirm });
}
.nearby-scroll
overflow: hidden;
height: 100%;
background: #f4f4f4;
.nearby-map
height: 767rpx;
background: #e8e8e8;
overflow: hidden
.nearby-list
min-height: calc(100% - 384rpx)
background: #f4f4f4
display: flex;
flex-direction: column;
.one-cards{
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
height: 100%
flex: 1
}
.nav-filter
padding: 16rpx 28rpx 0 28rpx
background: #ffffff
.filter-top
display: flex
justify-content: space-between;
@@ -398,6 +403,7 @@ defineExpose({ loadData, handleFilterConfirm });
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;

View File

@@ -33,7 +33,14 @@
donted: index === state.dont,
}"
></view>
<view class="item-text">{{ item.stationName }}</view>
<view
class="item-text"
:class="{
textActive: index === state.dont,
}"
>
{{ item.stationName }}
</view>
</view>
</view>
</view>
@@ -88,14 +95,9 @@
</view>
</view>
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<empty v-if="!list.length"></empty>
<loadmore v-show="list.length > pageState.pageSize" ref="loadmoreRef"></loadmore>
</view>
</view>
<!-- 筛选 -->
@@ -185,7 +187,6 @@ function openFilter() {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
console.log(pageState.search);
getJobList('refresh');
},
cancel: () => {
@@ -311,10 +312,12 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
@@ -351,9 +354,12 @@ defineExpose({ loadData, handleFilterConfirm });
color: #4778EC !important;
.nearby-scroll
overflow: hidden;
background: #f4f4f4;
height: 100%
.three-head
margin: 24rpx 0 0 0;
// margin: 24rpx 0 0 0;
padding: 26rpx 0 0 0;
background: #FFFFFF;
border-radius: 17rpx 17rpx 17rpx 17rpx;
.one-picker
height: 100%
@@ -408,44 +414,43 @@ defineExpose({ loadData, handleFilterConfirm });
border-radius: 50%;
position: relative;
margin-bottom: 20rpx;
.donted::after
.item-dont::before
position: absolute;
content: '';
color: #FFFFFF;
font-size: 20rpx;
text-align: center;
left: 0;
top: -5rpx;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
width: 27rpx;
height: 27rpx;
line-height: 28rpx;
background: blue !important;
background: #F7B000;
border-radius: 50%;
.dontstart::after
.item-dont::after
position: absolute;
content: '始';
color: #FFFFFF;
// content: '始';
content: '';
font-size: 20rpx;
text-align: center;
left: 0;
top: -5rpx;
width: 27rpx;
height: 27rpx;
line-height: 28rpx;
background: #666666;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
width: 14rpx;
height: 14rpx;
background: #ffffff;
border-radius: 50%;
// .dontend::after
// .donted::after
// position: absolute;
// content: '';
// color: #FFFFFF;
// content: '';
// font-size: 20rpx;
// text-align: center;
// left: 0;
// top: -5rpx;
// width: 27rpx;
// height: 27rpx;
// line-height: 28rpx;
// background: #666666;
// left: 50%;
// top: 50%;
// transform: translate(-50%, -50%)
// width: 14rpx;
// height: 14rpx;
// background: #F7B000 !important;
// border-radius: 50%;
.item-text
position: absolute
@@ -458,6 +463,8 @@ defineExpose({ loadData, handleFilterConfirm });
text-align: center;
white-space: nowrap
transform: translate(-50% + 8rpx, 0)
.textActive
color: #F7B000
.three-item:nth-child(2n)
.item-text
margin-top: -90rpx;
@@ -468,19 +475,26 @@ defineExpose({ loadData, handleFilterConfirm });
top: -17rpx;
width: 100%;
height: 17rpx;
background: #FFCB47;
background: #F7B000;
border-radius: 17rpx 17rpx 17rpx 17rpx;
z-index: 1;
.nearby-list
border-top: 2rpx solid #EBEBEB;
min-height: calc(100% - 222rpx)
background: #f4f4f4
display: flex;
flex-direction: column;
.one-cards{
height: 100%
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
flex: 1
}
.nav-filter
padding: 16rpx 28rpx 0 28rpx
background: #ffffff
.filter-top
display: flex
justify-content: space-between;
@@ -503,6 +517,7 @@ defineExpose({ loadData, handleFilterConfirm });
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;

View File

@@ -65,14 +65,9 @@
</view>
</view>
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<empty v-if="!list.length"></empty>
<loadmore v-show="list.length > pageState.pageSize" ref="loadmoreRef"></loadmore>
</view>
</view>
<!-- 筛选 -->
@@ -222,10 +217,12 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
@@ -253,10 +250,13 @@ defineExpose({ loadData, handleFilterConfirm });
color: #4778EC !important
.nearby-scroll
overflow: hidden;
height: 100%;
background: #f4f4f4;
.two-head
margin: 22rpx;
padding: 22rpx;
display: flex;
flex-wrap: wrap
background: #FFFFFF;
// grid-template-columns: repeat(4, 1fr);
// grid-column-gap: 10rpx;
// grid-row-gap: 24rpx;
@@ -276,19 +276,27 @@ defineExpose({ loadData, handleFilterConfirm });
background: #F6F6F6;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
color: #256BFA;
background: #E9F0FF;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.nearby-list
border-top: 2rpx solid #EBEBEB;
min-height: calc(100% - 252rpx)
background: #f4f4f4
display: flex;
flex-direction: column;
.one-cards{
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
height: 100%
flex: 1
}
.nav-filter
padding: 16rpx 28rpx 0 28rpx
background: #ffffff
.filter-top
display: flex
justify-content: space-between;
@@ -311,6 +319,7 @@ defineExpose({ loadData, handleFilterConfirm });
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="附近" :use-scroll-view="false">
<AppLayout title="附近" :use-scroll-view="false" :show-bg-image="false">
<template #headerleft>
<view class="btn">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -13,8 +13,20 @@
<view class="head-item" :class="{ actived: state.current === 3 }" @click="changeType(3)">商圈附近</view>
</view>
<view class="nearby-content">
<swiper class="swiper" :current="state.current" @change="changeSwiperType">
<swiper-item class="swiper-item" v-for="(_, index) in 4" :key="index">
<swiper
class="swiper"
:disable-touch="disableTouch"
:current="state.current"
@change="changeSwiperType"
>
<swiper-item
@touchstart.passive="handleTouchStart"
@touchmove.passive="handleTouchMove"
@touchend="disableTouch = false"
class="swiper-item"
v-for="(_, index) in 4"
:key="index"
>
<!-- #ifndef MP-WEIXIN -->
<component :is="components[index]" :ref="(el) => handelComponentsRef(el, index)" />
<!-- #endif -->
@@ -49,6 +61,11 @@ const showFilter1 = ref(false);
const showFilter2 = ref(false);
const showFilter3 = ref(false);
const disableTouch = ref(false);
const startPointX = ref(0);
const totalPage = 4;
const THRESHOLD = 5;
const state = reactive({
current: 0,
all: [{}],
@@ -63,11 +80,72 @@ const handelComponentsRef = (el, index) => {
swiperRefs[index].value = el;
}
};
function handleTouchStart(e) {
// 确保有触摸点
if (e.touches.length > 0) {
startPointX.value = e.touches[0].clientX;
disableTouch.value = false;
}
}
function handleTouchMove(e) {
if (e.touches.length === 0) return;
const currentX = e.touches[0].clientX;
const diffX = currentX - startPointX.value;
if (state.current === 0) {
if (diffX > THRESHOLD) {
disableTouch.value = true;
} else {
disableTouch.value = false;
}
return;
}
if (state.current === totalPage - 1) {
if (diffX < -THRESHOLD) {
disableTouch.value = true;
} else {
disableTouch.value = false;
}
return;
}
disableTouch.value = false;
}
// 查看消息类型
function changeSwiperType(e) {
const newIndex = e.detail.current;
const lastIndex = state.current;
const isSwipingRight = newIndex < lastIndex;
const isSwipingLeft = newIndex > lastIndex;
if (lastIndex === 0 && isSwipingRight) {
disableTouch.value = true;
state.current = 0;
setTimeout(() => {
disableTouch.value = false;
}, 50);
return;
}
if (lastIndex === totalPage - 1 && isSwipingLeft) {
disableTouch.value = true;
state.current = lastIndex;
setTimeout(() => {
disableTouch.value = false;
}, 50);
return;
}
const index = e.detail.current;
state.current = index;
handleTabChange(index);
disableTouch.value = false;
}
function changeType(index) {
state.current = index;
@@ -83,12 +161,16 @@ function handleTabChange(index) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;

View File

@@ -1,33 +1,70 @@
<template>
<view class="container">
<view class="top">
<image class="btnback button-click" src="@/static/icon/back.png" @click="navBack"></image>
<view class="search-box">
<uni-icons
class="iconsearch"
color="#666666"
type="search"
size="18"
@confirm="searchCollection"
></uni-icons>
<input
class="inputed"
type="text"
focus
v-model="searchValue"
placeholder="搜索职位名称"
placeholder-class="placeholder"
@confirm="searchBtn"
/>
<view>
<view class="top">
<image class="btnback button-click" src="@/static/icon/back.png" @click="navBack"></image>
<view class="search-box">
<uni-icons
class="iconsearch"
color="#666666"
type="search"
size="18"
@confirm="searchCollection"
></uni-icons>
<input
class="inputed"
type="text"
focus
v-model="searchValue"
placeholder="搜索职位名称"
placeholder-class="placeholder"
@confirm="searchBtn"
/>
</view>
<view class="search-btn button-click" @click="searchBtn">搜索</view>
</view>
<view class="view-top" v-show="listCom.length || list.length">
<view class="top-item" @click="changeType(0)" :class="{ active: currentTab === 0 }">综合</view>
<view class="top-item" @click="changeType(1)" :class="{ active: currentTab === 1 }">视频</view>
</view>
<view class="search-btn button-click" @click="searchBtn">搜索</view>
</view>
<scroll-view scroll-y class="Detailscroll-view" v-show="list.length" @scrolltolower="getJobList('add')">
<view class="cards-box">
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<scroll-view scroll-y class="Detailscroll-view" v-show="listCom.length" @scrolltolower="choosePosition">
<view class="cards-box" v-show="currentTab === 0">
<renderJobs :list="listCom" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
</view>
<view class="cards-box" style="padding-top: 24rpx" v-show="currentTab === 1">
<custom-waterfalls-flow
ref="waterfallsFlowRef"
:column="columnCount"
:columnSpace="columnSpace"
@loaded="imageloaded"
:value="list"
>
<template v-slot:default="job">
<view class="slot-item">
<view class="job-image btn-feel" @click="nextVideo(job)">
<image class="cover-image" :src="job.cover" mode="aspectFill"></image>
<view class="cover-triangle"></view>
</view>
<view class="job-info" @click="nextDetail(job)">
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
:is-month="true"
></Salary-Expectation>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="title">{{ job.jobTitle }}</view>
<view class="desc">{{ job.companyName }}</view>
</view>
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</scroll-view>
<view class="main-content" v-show="!list.length">
<view class="main-content" v-show="!listCom.length">
<view class="content-top">
<view class="top-left">历史搜索</view>
<view class="top-right button-click" @click="remove">
@@ -44,16 +81,19 @@
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import { inject, ref, reactive, nextTick } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
const { $api, navBack } = inject('globalFunction');
const { $api, navBack, navTo } = inject('globalFunction');
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
import { useColumnCount } from '@/hook/useColumnCount';
import { usePagination } from '@/hook/usePagination';
import img from '@/static/icon/filter.png';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const searchValue = ref('');
const historyList = ref([]);
const list = ref([]);
const listCom = ref([]);
const pageState = reactive({
page: 0,
total: 0,
@@ -63,6 +103,50 @@ const pageState = reactive({
order: 0,
},
});
const isLoaded = ref(false);
const waterfallsFlowRef = ref(null);
const loadmoreRef = ref(null);
const currentTab = ref(0);
// 响应式搜索条件(可以被修改)
const searchParams = ref({});
const pageSize = ref(10);
const { list, loading, refresh, loadMore } = usePagination(
(params) => $api.createRequest('/app/job/littleVideo', params, 'GET', true),
dataToImg, // 转换函数
{
pageSize: pageSize,
search: searchParams,
dataKey: 'data',
autoWatchSearch: true,
onBeforeRequest: () => {
loadmoreRef.value?.change('loading');
},
onAfterRequest: () => {
loadmoreRef.value?.change('more');
},
}
);
async function choosePosition(index) {
if (currentTab.value === 0) {
getJobList('add');
} else {
loadMore();
}
}
function imageloaded() {
loadmoreRef.value?.change('more');
}
const { columnCount, columnSpace } = useColumnCount(() => {
pageSize.value = 10 * (columnCount.value - 1);
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
});
});
onLoad(() => {
let arr = uni.getStorageSync('searchList');
@@ -71,6 +155,20 @@ onLoad(() => {
}
});
function changeType(type) {
if (currentTab.value === type) return;
switch (type) {
case 0:
currentTab.value = 0;
getJobList('refresh');
break;
case 1:
currentTab.value = 1;
refresh();
waterfallsFlowRef.value?.refresh?.();
break;
}
}
function searchFn(item) {
searchValue.value = item;
searchBtn();
@@ -83,7 +181,15 @@ function searchBtn() {
historyList.value.unshift(searchValue.value);
historyList.value = unique(historyList.value);
uni.setStorageSync('searchList', historyList.value);
getJobList('refresh');
searchParams.value = {
jobTitle: searchValue,
};
if (currentTab.value === 0) {
getJobList('refresh');
} else {
refresh();
waterfallsFlowRef.value?.refresh?.();
}
}
function searchCollection(e) {
@@ -112,12 +218,25 @@ function remove() {
historyList.value = [];
}
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
function nextVideo(job) {
uni.setStorageSync(`job-Info`, job);
navTo(`/packageA/pages/tiktok/tiktok`);
}
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === 'refresh') {
list.value = [];
pageState.page = 1;
pageState.maxPage = 2;
}
@@ -128,15 +247,15 @@ function getJobList(type = 'add') {
jobTitle: searchValue.value,
};
$api.createRequest('/app/job/list', params).then((resData) => {
$api.createRequest('/app/job/list', params, 'GET', true).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const end = listCom.value.length;
const reslist = rows;
list.value.splice(str, end, ...reslist);
listCom.value.splice(str, end, ...reslist);
} else {
list.value = rows;
listCom.value = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
@@ -147,6 +266,15 @@ function getJobList(type = 'add') {
}
});
}
function dataToImg(data) {
return data.map((item) => ({
...item,
// image: item.cover,
image: img,
hide: true,
}));
}
</script>
<style lang="stylus" scoped>
@@ -156,12 +284,35 @@ function getJobList(type = 'add') {
.Detailscroll-view{
flex: 1
overflow: hidden
}
.container{
display: flex
flex-direction: column
background: #F4f4f4
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
.view-top{
display: flex;
justify-content: space-around
background: #FFFFFF;
.top-item{
padding: 6rpx 0 18rpx 0
}
.active{
color: #256BFA;
font-weight: 500;
position: relative;
}
.active::after{
position: absolute;
content: ''
left: calc(50% - 12rpx)
bottom: 10rpx
width: 24rpx
height: 6rpx
background: #256BFA
}
}
.main-content{
background: #FFFFFF
height: 100%
@@ -244,4 +395,67 @@ function getJobList(type = 'add') {
}
}
}
.slot-item
// background: #f4f4f4;
background: #FFFFFF;
.job-info{
padding: 10rpx 24rpx 24rpx 24rpx
}
.job-image{
width: 100%;
height: 280rpx;
position: relative;
.cover-image{
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.cover-triangle{
position: absolute;
right: 20rpx;
top: 20rpx
width: 36rpx
height: 36rpx
border-radius: 50%
background: rgba(0,0,0,0.3)
}
.cover-triangle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-40%, -50%) rotate(90deg);
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-bottom: 12rpx solid #fff;
}
}
.salary
color: #4C6EFB;
font-size: 28rpx
display: flex
align-items: flex-start
justify-content: space-between
.flame
margin-top: 4rpx
margin-right: 4rpx
width: 24rpx
height: 31rpx
.title
font-weight: 500;
font-size: 32rpx;
color: #333333;
margin-top: 6rpx;
white-space: pre-wrap
.desc
font-weight: 400;
font-size: 24rpx;
color: #6C7282;
margin-top: 6rpx;
</style>

BIN
static/.DS_Store vendored

Binary file not shown.

BIN
static/font/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/gif/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
static/icon/.DS_Store vendored

Binary file not shown.

BIN
static/icon/add-circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/icon/ai-card-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/icon/arrow-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
static/icon/back-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 34 KiB

BIN
static/icon/background3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

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