82 Commits

Author SHA1 Message Date
Apcallover
2b1a87b65e flat: 未知 2025-12-10 11:20:05 +08:00
Apcallover
67f5dbbfb0 flat: 演示版 2025-12-09 11:13:32 +08:00
Apcallover
f24d95cedf flat: 缓动 2025-12-08 21:46:55 +08:00
Apcallover
c173fdd6a8 flat: 资源压缩 2025-12-08 11:16:43 +08:00
Apcallover
e0c4f18da0 flat: 语音合成、语音识别 2025-12-07 17:06:20 +08:00
Apcallover
a2ca068669 flat: 性能优化 2025-12-07 14:18:51 +08:00
Apcallover
d01f153c6a flat: 暂存 2025-12-07 12:56:02 +08:00
Apcallover
2a5681f8e0 flat: 优化+ 跳转 2025-12-06 15:19:40 +08:00
Apcallover
adc1c6ef06 flat: 暂存 2025-12-06 14:31:57 +08:00
Apcallover
09faeb3509 flat: 合并 2025-12-06 14:19:52 +08:00
Apcallover
0323a0f02e flat: update 2025-12-06 14:18:33 +08:00
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
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
d2e77e66fc flat: 暂存 2025-11-18 17:25:39 +08:00
Apcallover
ab3d9985c8 flat: 部署 2025-11-18 13:57:07 +08:00
161 changed files with 8863 additions and 3627 deletions

123
App.vue
View File

@@ -2,15 +2,23 @@
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, safeReLaunch } = inject('globalFunction');
import config from '@/config.js';
usePageAnimation();
const appword = 'aKd20dbGdFvmuwrt'; // 固定值
onLaunch((options) => {
useUserStore().initSeesionId(); //更新
// uni.hideTabBar();
useDictStore().getDictData();
uni.hideTabBar();
// 登录
try {
getUserInfo();
useUserStore().changMiniProgramAppStatus(false);
} catch {
console.log('不是爱山东平台,使用测试登陆');
useUserStore().changMiniProgramAppStatus(true);
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
@@ -19,9 +27,11 @@ onLaunch((options) => {
$api.msg('登录成功');
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
safeReLaunch('/pages/login/login');
// uni.redirectTo({
// url: '/pages/login/login',
// });
}
}
});
@@ -34,17 +44,108 @@ 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',
// });
safeReLaunch('/pages/index/index');
} else {
safeReLaunch('/pages/login/login');
// 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 {

View File

@@ -191,3 +191,37 @@
-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

@@ -33,12 +33,17 @@ html {
overflow-x: hidden;
}
li {
list-style: none;
}
/* 布局调整 */
/* 点击动效 */
/* 缩小 */
.button-click {
transition: transform 0.1s ease;
/* transition: transform 0.1s ease; */
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.button-click:active {
@@ -67,7 +72,8 @@ html {
}
.btn-feel {
transition: transform 0.15s ease;
transition: transform 0.5s ease;
/* transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); */
transform-style: preserve-3d;
}
@@ -84,7 +90,8 @@ html {
border: none;
border-radius: 6px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
/* transition: transform 0.4s ease, box-shadow 0.1s ease; */
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* box-shadow: 0 4px 0 #2C3E50; */
}
@@ -465,3 +472,166 @@ html {
text-overflow: ellipsis;
/* 使用省略号 */
}
.grayscale {
filter: grayscale(100%) opacity(0.6);
}
.height-100 {
height: 100%;
}
.pointEveNone {
pointer-events: none;
}
/*
*
* TransitionGroup 动画
* stagger 错峰
* fade-up-stagger --i 淡入上滑(经典错峰)
* pop-in-stagger --i 旋转缩放弹入(活泼)
* slide-left-stagger 从左侧滑入(消息流风格)
* blur-fade-stagger --i 渐显 + 模糊(毛玻璃感)
* bounce-up-stagger 从底部弹跳入场(物理感)
*
*/
/* 淡入上滑(经典错峰) */
.fade-up-stagger-enter-active {
transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28);
transition-delay: calc(var(--i) * 0.1s);
}
.fade-up-stagger-enter-from {
opacity: 0;
transform: translateY(20px);
}
.fade-up-stagger-leave-active {
transition: opacity 0.25s ease;
}
.fade-up-stagger-leave-to {
opacity: 0;
}
/* */
.pop-in-stagger-enter-active {
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
transition-delay: calc(var(--i) * 0.08s);
}
.pop-in-stagger-enter-from {
opacity: 0;
transform: scale(0.7) rotate(-10deg);
}
.pop-in-stagger-leave-active {
transition: all 0.2s ease;
}
.pop-in-stagger-leave-to {
opacity: 0;
transform: scale(0.9) rotate(5deg);
}
.slide-left-stagger-enter-active {
transition: all 0.35s ease-out;
transition-delay: calc(var(--i) * 0.07s);
}
.slide-left-stagger-enter-from {
opacity: 0;
transform: translateX(-100%);
}
.slide-left-stagger-leave-active {
transition: all 0.25s ease;
}
.slide-left-stagger-leave-to {
opacity: 0;
transform: translateX(50%);
}
/* 错峰动画 */
.stagger-enter-active {
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transition-delay: calc(var(--i) * 0.42s);
/* 关键120ms 间隔 */
}
.stagger-enter-from {
opacity: 0;
transform: translateY(30px) scale(0.95);
/* 更大位移 + 轻微缩放 */
}
.stagger-leave-active {
transition: all 0.25s ease;
}
.stagger-leave-to {
opacity: 0;
transform: translateX(20px);
}
.stagger-move {
transition: transform 0.4s ease;
}
.blur-fade-stagger-enter-active {
transition: all 0.5s ease;
transition-delay: calc(var(--i) * 0.09s);
}
.blur-fade-stagger-enter-from {
opacity: 0;
filter: blur(8px);
transform: scale(1.02);
}
.blur-fade-stagger-leave-active {
transition: all 0.2s ease;
}
.blur-fade-stagger-leave-to {
opacity: 0;
filter: blur(4px);
}
.bounce-up-stagger-enter-active {
animation: stagger-bounce 0.6s forwards;
animation-delay: calc(var(--i) * 0.12s);
}
.bounce-up-stagger-leave-active {
transition: all 0.25s ease;
}
.bounce-up-stagger-leave-to {
opacity: 0;
transform: translateY(30px);
}
@keyframes stagger-bounce {
0% {
opacity: 0;
transform: translateY(100px) scale(0.8);
}
60% {
transform: translateY(-10px) scale(1.05);
}
80% {
transform: translateY(5px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}

View File

@@ -1,5 +1,7 @@
import '@/lib/encryption/sm4.min.js'
import useUserStore from "../stores/useUserStore";
import {
createRequestWithCache,
createRequest,
uploadFile
} from "../utils/request";
@@ -7,9 +9,6 @@ import streamRequest, {
chatRequest
} from "../utils/streamRequest.js";
const sm4 = typeof window.sm4 !== 'undefined' ? window.sm4 :
(typeof window.smCrypto !== 'undefined' ? window.smCrypto.sm4 : null);
export const CloneDeep = (props) => {
if (typeof props !== 'object' || props === null) {
return props
@@ -52,6 +51,23 @@ const prePage = () => {
}
export function safeReLaunch(url) {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// 移除传入url开头的斜杠用于对比
const cleanUrl = url.startsWith('/') ? url.slice(1) : url;
if (currentPage && currentPage.route === cleanUrl) {
console.log('已在当前页');
return;
}
uni.reLaunch({
url
});
}
/**
@@ -286,7 +302,9 @@ function deg2rad(deg) {
}
function vacanciesTo(vacancies) {
if (vacancies >= 0) {
if (vacancies === null) {
return '-人'
} else if (vacancies >= 0) {
return vacancies + "人"
} else {
return '不限人数'
@@ -554,6 +572,23 @@ 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 {
@@ -570,7 +605,7 @@ export function sm4Decrypt(key, value, mode = "hex") {
return decrypted
} catch (e) {
console.log('解密失败')
console.log('解密失败', e)
}
}
@@ -607,7 +642,10 @@ export const $api = {
uploadFile,
formatFileSize,
sendingMiniProgramMessage,
copyText
copyText,
aes_Decrypt,
createRequestWithCache,
safeReLaunch
}
@@ -638,4 +676,8 @@ export default {
isInWechatMiniProgramWebview,
isEmptyObject,
sm4Decrypt,
aes_Decrypt,
sm2_Decrypt,
sm2_Encrypt,
safeReLaunch
}

View File

@@ -1,5 +1,9 @@
<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">
<!-- <view class="content_top btn-shaky"> -->
@@ -30,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: {},
};
@@ -52,19 +61,33 @@ image {
width: 100%;
height: 100%;
}
.empty {
width: 100%;
min-height: 100vh;
position: relative;
.ty_content {
.position-center {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, 0);
top: 50%;
transform: translate(-50%, -50%);
}
.empty {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// min-height: 100vh;
// height: 400rpx;
// position: relative;
.ty_content {
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

@@ -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

@@ -0,0 +1,182 @@
<template>
<transition-group name="blur-fade-stagger" tag="view">
<view v-for="(job, index) in dataSource" :key="job.id" :style="{ '--i': 2 }">
<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>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw, watch, ref, nextTick } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const dataSource = ref([]);
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 processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
let shouldReset = false;
if (dataSource.value.length > newList.length) {
shouldReset = true;
} else if (dataSource.value.length > 0 && newList.length > 0) {
// 注意:这里沿用你代码中使用的 item.id 作为唯一标识
const oldId = dataSource.value[0].id;
const newId = newList[0].id;
if (oldId !== newId) {
shouldReset = true;
}
}
if (shouldReset) {
dataSource.value = [];
processedIds.clear();
}
const newItems = newList.filter((item) => !processedIds.has(item.id));
if (newItems.length === 0) return;
newItems.forEach((item) => processedIds.add(item.id));
const delay = 50;
newItems.forEach((item, index) => {
setTimeout(() => {
dataSource.value.push(item);
}, index * delay);
});
},
{ immediate: true, deep: true }
);
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

@@ -1,13 +1,17 @@
<template>
<view v-for="job in listData" :key="job.id">
<transition-group name="blur-fade-stagger" tag="view">
<view v-for="job in dataSource" :key="job.id || job.jobId" :style="{ '--i': 2 }">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
<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>
@@ -36,13 +40,15 @@
{{ job.title }}
</view>
</view>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
import { inject, computed, toRaw, ref, watch } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const dataSource = ref([]);
const props = defineProps({
list: {
type: Array,
@@ -71,13 +77,58 @@ const listData = computed(() => {
return props.list;
});
const processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
// --- 新增逻辑开始 ---
// 判断是否需要重置数据 (例如点击了搜索、切换了Tab、或下拉刷新)
let shouldReset = false;
// 场景1: 新列表长度比当前渲染的列表短说明发生了重置如从20条变成了10条
if (dataSource.value.length > newList.length) {
shouldReset = true;
}
// 场景2: 列表不为空且第一条数据的ID发生了变化说明是全新的搜索结果
else if (dataSource.value.length > 0 && newList.length > 0) {
const oldId = dataSource.value[0].id || dataSource.value[0].jobId;
const newId = newList[0].id || newList[0].jobId;
if (oldId !== newId) {
shouldReset = true;
}
}
// 如果判定为重置则清空现有数据和ID记录
if (shouldReset) {
dataSource.value = [];
processedIds.clear();
}
// --- 新增逻辑结束 ---
const newItems = newList.filter((item) => !processedIds.has(item.id || item.jobId));
if (newItems.length === 0) return;
newItems.forEach((item) => processedIds.add(item.id || item.jobId));
const delay = 50;
newItems.forEach((item, index) => {
setTimeout(() => {
dataSource.value.push(item);
}, index * delay);
});
},
{ immediate: true, deep: true } // 建议加上 deep虽然这里监听的是数组引用变化
);
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
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>

View File

@@ -0,0 +1,191 @@
<template>
<transition-group name="blur-fade-stagger" tag="view">
<view v-for="job in dataSource" :key="job.id" :style="{ '--i': 2 }">
<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>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw, ref, watch } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const dataSource = ref([]);
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;
});
const processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
const newItems = newList.filter((item) => !processedIds.has(item.id));
if (newItems.length === 0) return;
newItems.forEach((item) => processedIds.add(item.id));
const delay = 50;
newItems.forEach((item, index) => {
setTimeout(() => {
dataSource.value.push(item);
}, index * delay);
});
},
{ immediate: true }
);
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

@@ -86,14 +86,13 @@ const open = (newConfig = {}) => {
} = newConfig;
reset();
serchforIt(defaultId);
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;
@@ -154,7 +153,19 @@ function serchforIt(defaultId) {
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;
@@ -163,7 +174,8 @@ function serchforIt(defaultId) {
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 = () => {

View File

@@ -1,28 +1,36 @@
export default {
// baseUrl: 'http://39.98.44.136:8080', // 测试
baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// baseUrl: 'http://10.133.17.161:8080/api', // 测试
// baseUrl: 'http://192.168.3.19:8080', // 测试
// baseUrl: 'http://39.98.44.136:6009', // 测试
// sseAI+
// StreamBaseURl: 'http://39.98.44.136:8000',
StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai',
// StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai/test',
baseUrl: 'https://fw.rc.qingdao.gov.cn/rgpp-api/api', // 内网
// baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// 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',
// vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/system/asr/connect', // 自定义
vioceBaseURl: 'wss://fw.rc.qingdao.gov.cn/rgpp-api/api/system/asr/connect', // 内网
// 语音合成
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/', //直接替换即可
// indexedDB
DBversion: 2,
DBversion: 3,
// 只使用本地缓寸的数据
OnlyUseCachedDB: false,
// 素质测评URL
Quality_assessment_URL: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/3511c4a8b32c468489ace780d40f6d92/zycpvhyjw/index.html#/pages/evaluation_record/evaluation_record?uuid=2f19e4d676df4650b6fb7edf87461571',
// 职业指导
Career_guidance: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/927b0032dfe3405ab0124fda9282ebdd/zyzd/index.html',
// ai 模拟面试
mock_interview: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/4a21b3b6efec4f8db2c3d3d5fa51edc9/szjx-rgzn-xnsc/#/pages/interview/schedule',
// VR虚拟招聘会
virtualJobFair: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/4a21b3b6efec4f8db2c3d3d5fa51edc9/szjx-rgzn-xnsc/#/pages/metaverse/job_fair',
// 使用模拟定位
UsingSimulatedPositioning: true,
// 应用信息
appInfo: {
// 应用名称
name: "青岛市就业服务",
// 爱山东应用标识
loveShandong: 'szjxrgznqzzp',
// 爱山东应用Key
sm2PrivateKey: '0d152c849f10e4469f2af8cedea62004e4f1db7be23c2f7270c1441d8050799d',
// 地区名
areaName: '青岛市',
// AI名称
@@ -78,5 +86,12 @@ export default {
mode: 'ECB', // default
iv: 'UISwD9fW6cFh9SNS', // default is null
cipherType: 'base64' // default is base64
}
},
animations: [ //动画
'fade-up-stagger',
'pop-in-stagger',
'slide-left-stagger',
'blur-fade-stagger',
'bounce-up-stagger'
]
}

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 */

214
hook/piper-sdk.js Normal file
View File

@@ -0,0 +1,214 @@
/**
* PiperTTS SDK - 兼容移动端的流式语音合成客户端
* 特性:
* 1. Web Audio API 实时调度,解决移动端不支持 MSE 的问题
* 2. 头部注入 (Header Injection) 技术,解决分片解码错误
* 3. 自动状态管理与事件回调
*/
export class PiperTTS {
constructor(config = {}) {
this.baseUrl = config.baseUrl || 'http://localhost:5001';
this.audioCtx = config.audioCtx || new(window.AudioContext || window.webkitAudioContext)();
this.onStatus = config.onStatus || ((msg, type) => console.log(`[Piper] ${msg}`));
this.onStart = config.onStart || (() => {});
this.onEnd = config.onEnd || (() => {});
// 内部状态
this.ws = null;
this.nextTime = 0; // 下一段音频的预定播放时间
this.audioHeader = null; // 保存WAV/MP3头部
this.chunkQueue = []; // 数据缓冲队列
this.queueSize = 0; // 当前缓冲区字节数
this.analyser = null; // 可视化分析器节点
// 配置参数
this.flushThreshold = 8 * 1024; // 8KB 阈值
}
/**
* [重要] 初始化音频引擎
* 必须在用户点击事件click/touch中调用一次否则手机上没声音
*/
async init() {
if (this.audioCtx.state === 'suspended') {
await this.audioCtx.resume();
this.onStatus('音频引擎已激活', 'success');
}
}
/**
* 绑定可视化分析器
* @param {AnalyserNode} analyserNode - Web Audio Analyser节点
*/
attachVisualizer(analyserNode) {
this.analyser = analyserNode;
}
/**
* 开始合成并播放
* @param {string} text - 要合成的文本
* @param {object} options - 可选参数 {speaker_id, noise_scale, etc.}
*/
speak(text, options = {}) {
if (!text) return;
this.stop(); // 清理上一次播放
this.onStatus('正在建立连接...', 'processing');
try {
const wsUrl = this.baseUrl.replace(/^http/, 'ws') + '/ws/synthesize';
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.onStatus('连接成功,请求生成...', 'processing');
// 初始化时间轴:当前时间 + 缓冲延迟
this.nextTime = this.audioCtx.currentTime + 0.1;
this.onStart();
this.ws.send(
JSON.stringify({
text: text,
speaker_id: options.speakerId || null,
length_scale: options.lengthScale || 1.0,
noise_scale: options.noiseScale || 0.667,
})
);
};
this.ws.onmessage = (event) => this._handleMessage(event);
this.ws.onclose = async () => {
// 处理剩余残余数据
if (this.chunkQueue.length > 0) {
await this._processQueue(true);
}
this.onStatus('播放结束', 'success');
this.onEnd();
};
this.ws.onerror = (err) => {
console.error(err);
this.onStatus('连接发生错误', 'error');
};
} catch (e) {
this.onStatus(`启动失败: ${e.message}`, 'error');
}
}
/**
* 停止播放并重置状态
*/
stop() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
// 重置缓冲
this.chunkQueue = [];
this.queueSize = 0;
this.audioHeader = null;
// 注意Web Audio API 很难"立即停止"已经在 flight 中的 node
// 除非我们追踪所有的 sourceNode 并调用 .stop()。
// 简单实现suspend 再 resume 或者关闭 context (不推荐频繁关闭)。
// 这里的 stop 主要停止数据接收。
}
// --- 内部私有方法 ---
async _handleMessage(event) {
if (!(event.data instanceof ArrayBuffer)) return;
const chunk = event.data;
// 1. 捕获头部 (Header Injection 核心)
if (!this.audioHeader) {
// 截取前100字节作为通用头
this.audioHeader = chunk.slice(0, 100);
}
// 2. 入队
this.chunkQueue.push(chunk);
this.queueSize += chunk.byteLength;
// 3. 达到阈值则解码播放
if (this.queueSize >= this.flushThreshold) {
await this._processQueue();
}
}
async _processQueue(isLast = false) {
if (this.chunkQueue.length === 0) return;
// 1. 合并 Buffer
const rawData = new Uint8Array(this.queueSize);
let offset = 0;
for (const chunk of this.chunkQueue) {
rawData.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
}
// 清空队列
this.chunkQueue = [];
this.queueSize = 0;
try {
// 2. 构造带头部的 Buffer
let decodeTarget;
// 简单的头部检测逻辑,如果没有头,就拼上去
if (this.audioHeader && !this._hasHeader(rawData)) {
const newBuffer = new Uint8Array(this.audioHeader.byteLength + rawData.byteLength);
newBuffer.set(new Uint8Array(this.audioHeader), 0);
newBuffer.set(rawData, this.audioHeader.byteLength);
decodeTarget = newBuffer.buffer;
} else {
decodeTarget = rawData.buffer;
}
// 3. 解码
const decodedBuffer = await this.audioCtx.decodeAudioData(decodeTarget);
// 4. 播放调度
this._scheduleBuffer(decodedBuffer);
} catch (err) {
// 解码失败处理:如果是中间数据,放回队列头部等待拼接
if (!isLast) {
this.chunkQueue.unshift(rawData);
this.queueSize += rawData.byteLength;
} else {
console.warn('最后一段数据解码失败,丢弃', err);
}
}
}
_scheduleBuffer(decodedBuffer) {
const source = this.audioCtx.createBufferSource();
source.buffer = decodedBuffer;
// 连接可视化
if (this.analyser) {
source.connect(this.analyser);
this.analyser.connect(this.audioCtx.destination);
} else {
source.connect(this.audioCtx.destination);
}
// 计算播放时间:如果发生卡顿,立即播放;否则无缝衔接
const scheduleTime = Math.max(this.audioCtx.currentTime, this.nextTime);
source.start(scheduleTime);
// 更新下一段的开始时间
this.nextTime = scheduleTime + decodedBuffer.duration;
}
_hasHeader(uint8Arr) {
if (uint8Arr.byteLength < 4) return false;
// Check "RIFF" (WAV)
if (uint8Arr[0] === 82 && uint8Arr[1] === 73 && uint8Arr[2] === 70) return true;
// Check "ID3" (MP3)
if (uint8Arr[0] === 73 && uint8Arr[1] === 68 && uint8Arr[2] === 51) return true;
// Check MP3 Sync Word (Simplify)
if (uint8Arr[0] === 0xff && (uint8Arr[1] & 0xe0) === 0xe0) return true;
return false;
}
}

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
}

View File

@@ -86,8 +86,7 @@ export function usePagination(
const res = await requestFn(params)
const rawData = res[dataKey]
const total = res[totalKey] || 99999999
console.log(total, rawData)
const data = typeof transformFn === 'function' ? transformFn(rawData) : rawData
if (type === 'refresh') {
@@ -96,9 +95,9 @@ export function usePagination(
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) {

View File

@@ -9,6 +9,7 @@ import {
import config from '@/config'
// Alibaba Cloud
export function useAudioRecorder() {
const isRecording = ref(false)
const isStopping = ref(false)

View File

@@ -0,0 +1,348 @@
import {
ref,
onUnmounted
} from 'vue'
import {
$api
} from '../common/globalFunction'; // 你的请求封装
import config from '@/config'
// 开源
export function useAudioRecorder() {
// --- 状态定义 ---
const isRecording = ref(false)
const isSocketConnected = ref(false)
const recordingDuration = ref(0)
const volumeLevel = ref(0) // 0-100
const recognizedText = ref('')
// --- 内部变量 ---
let socketTask = null
let durationTimer = null
// --- APP/小程序 变量 ---
let recorderManager = null;
// --- H5 变量 ---
let audioContext = null;
let scriptProcessor = null;
let mediaStreamSource = null;
let h5Stream = null;
// --- 配置项 ---
const RECORD_CONFIG = {
duration: 600000,
sampleRate: 16000,
numberOfChannels: 1,
format: 'pcm',
frameSize: 4096
}
/**
* 获取 WebSocket 地址 (含 Token)
*/
const getWsUrl = async () => {
let wsUrl = config.vioceBaseURl
// 拼接 Token
const token = uni.getStorageSync('token') || '';
if (token) {
const separator = wsUrl.includes('?') ? '&' : '?';
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
}
return wsUrl;
}
/**
* 开始录音 (入口)
*/
const startRecording = async () => {
if (isRecording.value) return
try {
recognizedText.value = ''
volumeLevel.value = 0
// #ifdef H5
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
uni.showToast({
title: 'H5录音需要HTTPS环境',
icon: 'none'
});
return;
}
// #endif
const url = await getWsUrl()
console.log('正在连接 ASR:', url)
await connectSocket(url);
} catch (err) {
console.error('启动失败:', err);
uni.showToast({
title: '启动失败: ' + (err.message || ''),
icon: 'none'
});
cleanup();
}
}
/**
* 连接 WebSocket
*/
const connectSocket = (url) => {
return new Promise((resolve, reject) => {
socketTask = uni.connectSocket({
url: url,
success: () => console.log('Socket 连接请求发送'),
fail: (err) => reject(err)
});
socketTask.onOpen((res) => {
console.log('WebSocket 已连接');
isSocketConnected.value = true;
// #ifdef H5
startH5Recording().then(() => resolve()).catch(err => {
socketTask.close();
reject(err);
});
// #endif
// #ifndef H5
startAppRecording();
resolve();
// #endif
});
socketTask.onMessage((res) => {
// 接收文本结果
if (res.data) {
recognizedText.value = res.data;
}
});
socketTask.onError((err) => {
console.error('Socket 错误:', err);
isSocketConnected.value = false;
stopRecording();
});
socketTask.onClose(() => {
isSocketConnected.value = false;
console.log('Socket 已关闭');
});
})
}
const startH5Recording = async () => {
try {
// 1. 获取麦克风流
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
h5Stream = stream;
// 2. 创建 AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext({
sampleRate: 16000
});
mediaStreamSource = audioContext.createMediaStreamSource(stream);
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
scriptProcessor.onaudioprocess = (event) => {
if (!isSocketConnected.value || !socketTask) return;
const inputData = event.inputBuffer.getChannelData(0);
calculateVolume(inputData, true);
const buffer = new ArrayBuffer(inputData.length * 2);
const view = new DataView(buffer);
for (let i = 0; i < inputData.length; i++) {
let s = Math.max(-1, Math.min(1, inputData[i]));
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
socketTask.send({
data: buffer,
fail: (e) => console.error('发送音频失败', e)
});
};
mediaStreamSource.connect(scriptProcessor);
scriptProcessor.connect(audioContext.destination);
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
console.log('H5 录音已启动');
} catch (err) {
console.error('H5 录音启动失败:', err);
throw err;
}
}
const stopH5Resources = () => {
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
scriptProcessor = null;
mediaStreamSource = null;
audioContext = null;
h5Stream = null;
}
const startAppRecording = () => {
recorderManager = uni.getRecorderManager();
recorderManager.onFrameRecorded((res) => {
const {
frameBuffer
} = res;
calculateVolume(frameBuffer, false);
if (isSocketConnected.value && socketTask) {
socketTask.send({
data: frameBuffer
});
}
});
recorderManager.onStart(() => {
console.log('APP 录音已开始');
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
});
recorderManager.onError((err) => {
console.error('APP 录音报错:', err);
cleanup();
});
recorderManager.start(RECORD_CONFIG);
}
const stopHardwareResource = () => {
// APP/小程序停止
if (recorderManager) {
recorderManager.stop();
}
// H5停止
// #ifdef H5
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
scriptProcessor = null;
mediaStreamSource = null;
audioContext = null;
h5Stream = null;
// #endif
}
/**
* 停止录音 (通用)
*/
const stopRecording = () => {
// 停止 APP 录音
if (recorderManager) {
recorderManager.stop();
}
// 停止 H5 录音资源
// #ifdef H5
stopH5Resources();
// #endif
// 关闭 Socket
if (socketTask) {
socketTask.close();
}
cleanup();
}
const cancelRecording = () => {
if (!isRecording.value) return;
console.log('取消录音 - 丢弃结果');
// 1. 停止硬件录音
stopHardwareResource();
// 2. 强制关闭 Socket
if (socketTask) {
socketTask.close();
}
// 3. 关键:清空已识别的文本
recognizedText.value = '';
// 4. 清理资源
cleanup();
}
/**
* 清理状态
*/
const cleanup = () => {
clearInterval(durationTimer);
isRecording.value = false;
isSocketConnected.value = false;
socketTask = null;
recorderManager = null;
volumeLevel.value = 0;
}
/**
* 计算音量 (兼容 Float32 和 Int16/ArrayBuffer)
*/
const calculateVolume = (data, isFloat32) => {
let sum = 0;
let length = 0;
if (isFloat32) {
length = data.length;
for (let i = 0; i < length; i += 10) {
sum += Math.abs(data[i]);
}
volumeLevel.value = Math.min(100, Math.floor((sum / (length / 10)) * 100 * 3));
} else {
const int16Data = new Int16Array(data);
length = int16Data.length;
for (let i = 0; i < length; i += 10) {
sum += Math.abs(int16Data[i]);
}
const avg = sum / (length / 10);
volumeLevel.value = Math.min(100, Math.floor((avg / 10000) * 100));
}
}
onUnmounted(() => {
if (isRecording.value) {
stopRecording();
}
})
return {
isRecording,
isSocketConnected,
recordingDuration,
volumeLevel,
recognizedText,
startRecording,
stopRecording,
cancelRecording
}
}

View File

@@ -49,6 +49,7 @@ export function useTTSPlayer() {
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) {

View File

@@ -9,8 +9,9 @@ import {
onUnload
} from '@dcloudio/uni-app'
import WavDecoder from '@/lib/wav-decoder@1.3.0.js'
import config from '@/config'
export function useTTSPlayer(wsUrl) {
export function useTTSPlayer() {
const isSpeaking = ref(false)
const isPaused = ref(false)
const isComplete = ref(false)
@@ -89,12 +90,13 @@ export function useTTSPlayer(wsUrl) {
const initWebSocket = () => {
const thisPlayId = currentPlayId
socket = new WebSocket(wsUrl)
socket = new WebSocket(config.speechSynthesis)
socket.binaryType = 'arraybuffer'
socket.onopen = () => {
if (pendingText && thisPlayId === activePlayId) {
const seepdText = extractSpeechText(pendingText)
console.log(seepdText)
socket.send(seepdText)
pendingText = null
}

216
hook/useTTSPlayer2.js Normal file
View File

@@ -0,0 +1,216 @@
import {
ref,
onUnmounted,
onMounted,
watch
} from 'vue'
import {
onHide,
onUnload
} from '@dcloudio/uni-app'
import config from '@/config'
// 请确保 piper-sdk.js 已经正确 export class PiperTTS
import {
PiperTTS
} from './piper-sdk.js'
export function useTTSPlayer() {
// UI 状态
const isSpeaking = ref(false)
const isPaused = ref(false)
const isLoading = ref(false)
// SDK 实例
let piper = null
/**
* 初始化 SDK 实例
* 每次 stop 后 piper 会被置空,这里会重新创建
*/
const initPiper = () => {
if (piper) return
let baseUrl = config.speechSynthesis2 || ''
baseUrl = baseUrl.replace(/\/$/, '')
piper = new PiperTTS({
baseUrl: baseUrl,
onStatus: (msg, type) => {
if (type === 'error') {
console.error('[TTS Error]', msg)
// 出错时不重置状态,交给用户手动处理或结束事件处理
resetState()
}
},
onStart: () => {
isLoading.value = false
isSpeaking.value = true
isPaused.value = false
},
onEnd: () => {
resetState()
}
})
}
/**
* 核心朗读方法
*/
const speak = async (text) => {
if (!text) return
const processedText = extractSpeechText(text)
if (!processedText) return
// 1. 【关键修改】先彻底停止并销毁旧实例
// 这会断开 socket 并且 close AudioContext确保上一个声音立即消失
await stop()
// 2. 初始化新实例 (因为 stop() 把 piper 设为了 null)
initPiper()
// 3. 更新 UI 为加载中
isLoading.value = true
isPaused.value = false
isSpeaking.value = true // 预先设为 true防止按钮闪烁
try {
// 4. 激活音频引擎 (移动端防静音关键)
await piper.init()
// 5. 发送请求
piper.speak(processedText, {
speakerId: 0,
noiseScale: 0.667,
lengthScale: 1.0
})
} catch (e) {
console.error('TTS Speak Error:', e)
resetState()
}
}
/**
* 暂停
*/
const pause = async () => {
if (piper && piper.audioCtx && piper.audioCtx.state === 'running') {
await piper.audioCtx.suspend()
isPaused.value = true
}
}
/**
* 恢复
*/
const resume = async () => {
if (piper && piper.audioCtx && piper.audioCtx.state === 'suspended') {
await piper.audioCtx.resume()
isPaused.value = false
isSpeaking.value = true
}
}
/**
* 停止并重置 (核打击模式)
*/
const stop = async () => {
if (piper) {
// 1. 断开 WebSocket
piper.stop()
// 2. 【关键】关闭 AudioContext
// Web Audio API 中,已经 schedule 的 buffer 很难单独取消
// 最直接的方法是关闭整个 Context
if (piper.audioCtx && piper.audioCtx.state !== 'closed') {
try {
await piper.audioCtx.close()
} catch (e) {
console.warn('AudioContext close failed', e)
}
}
// 3. 销毁实例引用
piper = null
}
resetState()
}
// UI 状态重置
const resetState = () => {
isSpeaking.value = false
isPaused.value = false
isLoading.value = false
}
// === 生命周期 ===
onMounted(() => {
// 预初始化可以不做,等到点击时再做,避免空闲占用 AudioContext 资源
// initPiper()
})
onUnmounted(() => {
stop()
})
// Uniapp 生命周期
if (typeof onHide === 'function') onHide(stop)
if (typeof onUnload === 'function') onUnload(stop)
return {
speak,
pause,
resume,
stop,
cancelAudio: stop,
isSpeaking,
isPaused,
isLoading
}
}
/**
* 提取文本逻辑 (保持不变)
*/
function extractSpeechText(markdown) {
if (!markdown || markdown.indexOf('job-json') === -1) {
return markdown;
}
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
const jobs = [];
let match;
let lastJobEndIndex = 0;
let firstJobStartIndex = -1;
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);
}
}
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() : '';
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

@@ -17,18 +17,24 @@
})();
</script>
<title></title>
<!-- vconsole -->
<!-- eruda -->
<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
eruda.init();
</script> -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
vConsole.destroy();
</script> -->
<!-- 爱山东jssdk -->
<!-- <script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></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>
<script type="module" src="./static/js/sm4.min.js"></script>
</head>
<!-- <body> -->
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>

7
lib/encryption/sm2.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

@@ -80,7 +80,7 @@
"locale": "zh-Hans",
"h5": {
"router": {
"base": "/app/",
"base": "./",
"mode": "hash"
},
"title": "青岛智慧就业服务",
@@ -97,6 +97,9 @@
"serviceHost": ""
}
}
},
"devServer": {
"https": false
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"pixi.js": "^7.4.3"
}
}

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>
@@ -21,6 +21,7 @@ import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const userStore = useUserStore();
const { isMiniProgram } = storeToRefs(useUserStore());
const state = reactive({});
const pageState = reactive({
page: 0,
@@ -37,14 +38,19 @@ onLoad(() => {
getJobList();
});
onMounted(() => {
// #ifdef H5
if (!isMiniProgram.value) {
const a = document.getElementsByClassName('uni-page-head-hd')[0];
a.style.display = 'none';
}
// #endif
});
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 +63,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 +76,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 +86,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,6 +1,6 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -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>
@@ -69,12 +75,15 @@ import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import { storeToRefs } from 'pinia';
const { isMiniProgram } = storeToRefs(useUserStore());
import useUserStore from '@/stores/useUserStore';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
const isExpanded = ref(false);
const pageState = reactive({
current: 0,
page: 0,
list: [],
total: 0,
@@ -82,35 +91,113 @@ const pageState = reactive({
pageSize: 10,
});
const companyInfo = ref({});
const pageOptions = ref({});
const dataType = ref(1); // 1: 原数据, 2: 第三方数据
onLoad((options) => {
console.log(options);
// 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() {
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}`, {}, 'DELETE').then((resData) => {
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'POST').then((resData) => {
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId);
$api.msg('收藏成功');
});
}
}
}
function getCompanyInfo(id) {
$api.createRequest(`/app/company/${id}`).then((resData) => {
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 {
// 原数据接口
const [companyId] = args;
$api.createRequest(`/app/company/${companyId}`, {}, 'GET', true).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 getOriginalJobsList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
@@ -118,23 +205,30 @@ 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;
handleJobsListResponse(type, rows, total, 'page');
});
}
function handleJobsListResponse(type, rows, total, pageKey) {
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const str = pageState.pageSize * (pageState[pageKey] - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
pageState.list = [...pageState.list, ...rows];
// pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}
pageState.total = resData.total;
pageState.total = total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
});
}
function expand() {
@@ -174,7 +268,6 @@ image {
margin-right: 24rpx
}
.companyinfo-right{
.row1{
font-weight: 500;
font-size: 32rpx;
@@ -210,7 +303,7 @@ image {
}
}
.expanded {
max-height: 1000rpx; // 足够显示完整内容
max-height: 1000rpx;
}
.expand{
display: flex
@@ -237,6 +330,8 @@ image {
background: #F4F4F4;
.views{
padding: 28rpx
min-height: calc(100% - 56rpx)
position: relative
.Detail-title{
font-weight: 600;
font-size: 32rpx;

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="添加岗位">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -17,6 +17,28 @@
</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 { userInfo, isMiniProgram } = 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,6 +1,6 @@
<template>
<AppLayout title="我的浏览" :show-bg-image="false" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -24,17 +24,15 @@
</view>
<scroll-view scroll-y class="main-scroll" @scrolltolower="getJobList('add')">
<view class="one-cards">
<view class="mian">
<renderJobs
<renderJobViewRecord
:list="pageState.list"
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
></renderJobViewRecord>
<empty v-else></empty>
<!-- <loadmore ref="loadmoreRef"></loadmore> -->
</view>
</view>
</scroll-view>
</view>
</AppLayout>
@@ -48,7 +46,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, navBack } = inject('globalFunction');
import useLocationStore from '@/stores/useLocationStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const userStore = useUserStore();
const browseDate = ref('');
@@ -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;
@@ -129,6 +123,7 @@ function getJobList(type = 'add', loading = true) {
reslist.shift();
}
}
// pageState.list = [...pageState.list, ...rows];
pageState.list.splice(str, end, ...reslist);
pageState.lastDate = lastDate;
} else {
@@ -170,9 +165,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
@@ -219,6 +217,6 @@ image {
.one-cards{
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
height: 100%
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="精选企业">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -28,7 +28,7 @@ const { $api, navBack, navTo } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { getUserResume } = useUserStore();
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const list = ref([]);
onLoad(() => {
@@ -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) {

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout :title="title" :show-bg-image="false" @onScrollBottom="getDataList('add')">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -27,7 +27,7 @@
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanys>
<empty v-else pdTop="200"></empty>
<empty v-else is-position></empty>
</view>
</AppLayout>
</template>
@@ -39,6 +39,8 @@ import { storeToRefs } from 'pinia';
const { $api, navTo, navBack } = inject('globalFunction');
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
// state
const title = ref('事业单位');
const cardInfo = ref({});
@@ -151,6 +153,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

@@ -1,6 +1,6 @@
<template>
<AppLayout title="我的收藏" :show-bg-image="false" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -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>
@@ -51,6 +61,8 @@ import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navBack } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const type = ref(0);
const pageState = reactive({
@@ -69,14 +81,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 +180,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 +194,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 +210,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 +223,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 +267,7 @@ function getCompanyList(type = 'add') {
.swiper{
height: 100%
.mian{
height: 100%
padding: 0 28rpx 28rpx 28rpx
}
}

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -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-if="!pageState.list.length" is-position></empty>
</view>
</scroll-view>
</view>
@@ -107,31 +110,80 @@ import useLocationStore from '@/stores/useLocationStore';
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const isExpanded = ref(false);
const fairInfo = ref({});
const companyList = ref([]);
const hasnext = ref(true);
const pageState = reactive({
current: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 10,
});
const hasnext = ref(false);
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 = [...pageState.list, ...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 +201,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('预约成功');
});
}
@@ -387,6 +439,8 @@ image {
background: #F4F4F4;
.views{
padding: 28rpx
min-height: calc(100% - 56rpx);
position: relative
.Detail-title{
font-weight: 600;
font-size: 32rpx;

View File

@@ -1,28 +1,56 @@
<template>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<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 btn-feel">
<view class="mys-tops">
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || "编辑用户名" }}</text>
<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>
<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>
<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>
<dict-Label
class="mar_ri10"
dictType="affiliation"
:value="userInfo.politicalAffiliation"
></dict-Label>
</view>
<view class="subName">{{ userInfo.phone }}</view>
<view class="subName">{{ userInfo.phone || '未知手机号' }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
@@ -40,10 +68,14 @@
<view class="mys-line">
<view class="line"></view>
</view>
<view class="mys-info btn-feel">
<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>
<image
class="icon"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/jobExpect/jobExpect')"
></image>
</view>
<view class="mys-text">
<text>期望薪资</text>
@@ -63,21 +95,25 @@
</view>
<view class="card-top" style="margin-top: 24rpx">
<view class="mys-info" style="padding: 0">
<view class="mys-h4 btn-feel">
<view class="mys-h4">
<text>工作经历</text>
<view class="mys-edit-icon btn-feel" @click="navTo('/packageA/pages/workExp/workExp')">
<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 btn-feel" v-for="item in userInfo.workExp" :key="item.id">
<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>
<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>
<text class="datetext">{{ item.startTime }}--{{ item.endTime || '至今' }}</text>
</view>
<view class="mys-text">
<text>{{ item.duty }}</text>
@@ -88,22 +124,91 @@
</view>
<template #footer>
<view class="footer-container">
<view class="footer-button btn-feel">上传简历</view>
<view class="footer-button btn-feel" @click="chooseResume">上传简历</view>
</view>
</template>
</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());
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, isMiniProgram } = 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/user/resume/recognition',
// 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>
@@ -133,6 +238,36 @@ const { getDictData, oneDictData } = useDictStore();
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%

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>
@@ -43,7 +42,7 @@ const { $api, navTo, debounce, customSystem } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const pageState = reactive({
@@ -59,6 +58,15 @@ const state = reactive({
tabIndex: 'all',
});
onMounted(() => {
// #ifdef H5
if (!isMiniProgram.value) {
const a = document.getElementsByClassName('uni-page-head-hd')[0];
a.style.display = 'none';
}
// #endif
});
onLoad(() => {
getList('refresh');
});
@@ -140,6 +148,7 @@ function getList(type = 'add', loading = true) {
height: 100%
.list{
padding: 0 28rpx 28rpx 28rpx
height: calc(100% - 28rpx)
}
}
}

View File

@@ -1,5 +1,11 @@
<template>
<AppLayout title="个人信息" :sub-title="`完成度${percent}`" border back-gorund-color="#ffffff" :show-bg-image="false">
<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>
@@ -22,21 +28,40 @@
<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 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="请选择您的出生年月" />
<input
class="input-con triangle pointEveNone"
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="请选择您的学历" />
<input
class="input-con triangle pointEveNone"
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="请选择您的政治面貌" />
<input
class="input-con triangle pointEveNone"
v-model="state.politicalAffiliationText"
disabled
placeholder="请选择您的政治面貌"
/>
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
@@ -47,29 +72,29 @@
</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: "",
name: '',
sex: 0,
birthDate: "",
education: "",
politicalAffiliation: "",
avatar: "",
birthDate: '',
education: '',
politicalAffiliation: '',
avatar: '',
});
onLoad(() => {
initLoad();
@@ -92,23 +117,23 @@ function initLoad() {
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
fromValue.avatar = userInfo.value.avatar;
// 回显
state.educationText = dictLabel("education", userInfo.value.education);
state.politicalAffiliationText = dictLabel("affiliation", userInfo.value.politicalAffiliation);
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("请输入姓名");
return $api.msg('请输入姓名');
}
if (!fromValue.birthDate) {
return $api.msg("请选择出生年月");
return $api.msg('请选择出生年月');
}
if (!fromValue.education) {
return $api.msg("请选择学历");
return $api.msg('请选择学历');
}
if (!fromValue.politicalAffiliation) {
return $api.msg("请选择政治面貌");
return $api.msg('请选择政治面貌');
}
// if (!checkingPhoneRegExp(fromValue.phone)) {
// return $api.msg('请输入正确手机号');
@@ -117,8 +142,8 @@ const confirm = () => {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest("/app/user/resume", params, "post").then((resData) => {
$api.msg("完成");
$api.createRequest('/app/user/resume', params, 'post').then((resData) => {
$api.msg('完成');
state.disbleDate = true;
getUserResume().then(() => {
navBack();
@@ -128,9 +153,13 @@ const confirm = () => {
const changeDateBirt = () => {
const datearray = generateDatePickerArrays();
let defaultIndex = [0, 0, 0];
if (fromValue.birthDate) {
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
}
console.log(defaultIndex);
openSelectPopup({
title: "年龄段",
title: '年龄段',
maskClick: true,
data: datearray,
defaultIndex,
@@ -140,16 +169,16 @@ const changeDateBirt = () => {
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg("没有这一天");
$api.msg('没有这一天');
}
},
});
};
const changeEducation = () => {
openSelectPopup({
title: "学历",
title: '学历',
maskClick: true,
data: [oneDictData("education")],
data: [oneDictData('education')],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
@@ -162,9 +191,9 @@ const changeSex = (sex) => {
const changePoliticalAffiliation = () => {
openSelectPopup({
title: "政治面貌",
title: '政治面貌',
maskClick: true,
data: [oneDictData("affiliation")],
data: [oneDictData('affiliation')],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
@@ -181,17 +210,17 @@ function generateDatePickerArrays(startYear = 1975, endYear = new Date().getFull
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, "0"));
months.push(m.toString().padStart(2, '0'));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, "0"));
days.push(d.toString().padStart(2, '0'));
}
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;
@@ -218,22 +247,22 @@ function getFormCompletionPercent(form) {
for (const key in form) {
const value = form[key];
if (value !== "" && value !== null && value !== undefined) {
if (typeof value === "number") {
if (value !== '' && value !== null && value !== undefined) {
if (typeof value === 'number') {
filled += 1;
} else if (typeof value === "string" && value.trim() !== "") {
} else if (typeof value === 'string' && value.trim() !== '') {
filled += 1;
}
}
}
if (total === 0) return "0%";
if (total === 0) return '0%';
const percent = (filled / total) * 100;
return percent.toFixed(0) + "%"; // 取整,不要小数点
return percent.toFixed(0) + '%'; // 取整,不要小数点
}
// 主函数
function getDatePickerIndexes(dateStr) {
const [year, month, day] = dateStr.split("-");
const [year, month, day] = dateStr.split('-');
const [years, months, days] = generateDatePickerArrays();
@@ -246,18 +275,17 @@ function getDatePickerIndexes(dateStr) {
function selectAvatar() {
uni.chooseImage({
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
count: 1,
success: ({ tempFilePaths, tempFiles }) => {
$api
.uploadFile(tempFilePaths[0], true)
$api.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg("上传失败");
$api.msg('上传失败');
});
},
fail: (error) => {},

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;
@@ -61,10 +62,9 @@ function rawRadarChart(labels, data) {
ctx.closePath();
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

@@ -9,8 +9,8 @@
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
>

View File

@@ -1,44 +1,64 @@
<template>
<AppLayout title="" backGorundColor="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<template #headerright>
<!-- <view class="btnshare">
<image src="@/static/icon/share.png" @click="shareJob"></image>
</view> -->
<view class="btn mar_ri10">
<image src="@/static/icon/collect3.png" v-if="!jobInfo.isCollection" @click="jobCollection"></image>
<image src="@/static/icon/collect2.png" v-else @click="jobCollection"></image>
</view>
</template>
<!-- 根据 dataType 显示不同内容 -->
<view class="content" v-show="!isEmptyObject(jobInfo)">
<!-- 顶部信息区域 -->
<view class="content-top btn-feel">
<view class="top-salary">
<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-name">{{ jobInfo.jobTitle }}</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">
@@ -48,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>
@@ -97,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>
@@ -125,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" />
@@ -143,8 +183,13 @@ import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import RadarMap from './component/radarMap.vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
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);
@@ -155,9 +200,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);
}
});
@@ -165,6 +212,7 @@ onLoad((option) => {
onShow(() => {
const option = parseQueryParams(); // 兼容微信内置浏览器
if (option.jobId) {
dataType.value = option.dataType ? parseInt(option.dataType) : 1;
initLoad(option);
}
});
@@ -180,20 +228,40 @@ function initLoad(option) {
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) {
if (dataType.value === 2) {
// 第三方数据接口
return new Promise((reslove, reject) => {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
$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 initMapCovers(latitude, longitude, companyName) {
mapCovers.value = [
{
latitude: latitude,
@@ -205,39 +273,65 @@ function getDetail(jobId) {
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName), // X 轴调整,负数向左
anchorX: getTextWidth(companyName),
borderRadius: 5,
},
width: 34,
},
];
}
});
});
}
function getCompanyIsAJobs(companyId) {
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) {
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() {
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 {
// 原数据申请逻辑
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isApply) {
const jobUrl = jobInfo.value.jobUrl;
@@ -251,22 +345,51 @@ function jobApply() {
});
}
}
}
// 取消/收藏岗位
function jobCollection() {
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 {
// 原数据收藏逻辑
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'DELETE').then((resData) => {
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'DELETE').then((resData) => {
getDetail(jobId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'POST').then((resData) => {
$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;
@@ -285,6 +408,7 @@ function getClass(index) {
</script>
<style lang="stylus" scoped>
/* 样式保持不变与现在的post页面相同 */
.btnback{
width: 64rpx;
height: 64rpx;
@@ -308,15 +432,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;
@@ -335,12 +458,10 @@ image {
transition: background-color 0.3s;
}
/* 完整激活格子 */
.progress-item.active {
background: linear-gradient(to right, #256bfa, #8c68ff);
}
/* 当前进度进行中的格子 */
for i in 0..100
.progress-item.half{i}::before
content ''
@@ -352,36 +473,12 @@ for i in 0..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{
@@ -527,7 +624,6 @@ for i in 0..100
margin-right: 24rpx
}
.companyinfo-right{
.row1{
font-weight: 500;
font-size: 32rpx;
@@ -564,5 +660,9 @@ for i in 0..100
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

@@ -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>
@@ -51,7 +60,7 @@ import Countdown from './component/countdown.vue';
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const pageState = reactive({
@@ -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;
@@ -82,18 +92,36 @@ onLoad(() => {
getList();
});
onMounted(() => {
// #ifdef H5
if (!isMiniProgram.value) {
const a = document.getElementsByClassName('uni-page-head-hd')[0];
a.style.display = 'none';
}
// #endif
});
function chnageRanOption(item) {
ranItem.value = item;
getList();
}
function updateCancel(item) {
const fairId = item.jobFairId;
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('取消预约成功');
});
}
},
});
}
function getList(type = 'add', loading = true) {
if (type === 'refresh') {
@@ -108,7 +136,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 +149,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>
@@ -166,6 +195,7 @@ 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;

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="选择日期" :use-scroll-view="false" back-gorund-color="#FAFAFA">
<template #headerleft>
<view class="btn">
<view class="btn" v-if="isMiniProgram">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -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,
}"
@@ -50,6 +51,9 @@ const { $api, navTo, navBack } = inject('globalFunction');
const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
const calendarData = ref([]);
const current = ref({});
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
import { Solar, Lunar } from '@/lib/lunar-javascript@1.7.2.js';
const isRecord = ref(false);
@@ -58,6 +62,7 @@ const pages = reactive({
year: 0,
month: 0,
});
const hasZphDateArray = ref([]);
onLoad((options) => {
if (options.date) {
@@ -77,8 +82,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

@@ -1,6 +1,6 @@
<template>
<div class="video-container">
<view class="back-box">
<view class="back-box" v-if="isMiniProgram">
<view class="btn">
<uni-icons type="left" size="26" color="#FFFFFF" @click="navBack"></uni-icons>
</view>
@@ -31,6 +31,8 @@ import mTikTok from '@/components/TikTok/TikTok.vue';
import useUserStore from '@/stores/useUserStore';
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
import { storeToRefs } from 'pinia';
const { isMiniProgram } = storeToRefs(useUserStore());
const state = reactive({
videoList: [],
});
@@ -95,7 +97,7 @@ const change = (e) => {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: 30rpx;
bottom: calc( var(--window-bottom));
color: #fff;
.title{
font-weight: 500;

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
@@ -15,11 +15,11 @@
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="info-right">
<view class="name">{{ userInfo.name || "编辑用户名" }}</view>
<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">{{ 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>
@@ -27,27 +27,33 @@
</view>
<view class="phone">
<image class="call-icon" src="@/static/icon/call.png" />
<view>{{ userInfo.phone }}</view>
<view>{{ userInfo.phone || '未知手机号' }}</view>
</view>
</view>
</view>
<view class="info-bottom">
<view>到岗2025-11-02</view>
<view>地点青岛市-<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label></view>
<!-- <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 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>{{ userInfo.graduationSchool || '--' }}</view>
</view>
<view class="fl_box fl_justbet">
<!-- <view class="fl_box fl_justbet">
<view>当前状态</view>
<view>在职 看工作机会</view>
</view>
</view> -->
</view>
</view>
@@ -70,7 +76,9 @@
<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 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>
@@ -84,7 +92,9 @@
</view>
<ul class="certificate-box">
<li class="certificate-item" v-for="(item, index) in userInfo?.certificateList" :key="item.id">{{ item.name }}</li>
<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>
@@ -96,13 +106,13 @@
</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());
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, isMiniProgram } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
</script>
@@ -193,6 +203,9 @@ image {
align-items: center;
padding: 24rpx;
.avatar {
background: #e8e8e8;
border-radius: 50%;
overflow: hidden;
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;

View File

@@ -19,7 +19,12 @@
<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="开始时间" />
<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>
@@ -45,24 +50,24 @@
</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";
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: "",
position: '',
company: '',
startTime: '',
endTime: '',
duty: '',
id: undefined,
});
@@ -76,43 +81,43 @@ onLoad((e) => {
const confirm = async () => {
// 验证必填字段
if (!fromValue.company) {
return $api.msg("请输入公司名称");
return $api.msg('请输入公司名称');
}
if (!fromValue.position) {
return $api.msg("请输入岗位");
return $api.msg('请输入岗位');
}
if (!fromValue.startTime) {
return $api.msg("请选择开始时间");
return $api.msg('请选择开始时间');
}
// 验证时间逻辑:结束时间不能早于开始时间
if (fromValue.endTime && new Date(fromValue.endTime) < new Date(fromValue.startTime)) {
return $api.msg("结束时间不能早于开始时间");
return $api.msg('结束时间不能早于开始时间');
}
let res;
try {
if (fromValue.id) {
res = await $api.createRequest("/app/user/experience/edit", fromValue, "post");
res = await $api.createRequest('/app/user/experience/edit', fromValue, 'post');
} else {
res = await $api.createRequest("/app/user/experience/save", fromValue, "post");
res = await $api.createRequest('/app/user/experience/save', fromValue, 'post');
}
$api.msg("保存成功");
$api.msg('保存成功');
getUserResume().then(() => {
navBack();
});
} catch (error) {
$api.msg("保存失败");
$api.msg('保存失败');
}
};
function delCurrent() {
uni.showModal({
title: "提示",
content: "确认要删除此条工作经历吗?",
title: '提示',
content: '确认要删除此条工作经历吗?',
showCancel: true,
success: async ({ confirm, cancel }) => {
if (confirm) {
await $api.createRequest("/app/user/experience/delete", { id: fromValue.id }, "post");
$api.msg("删除成功");
await $api.createRequest('/app/user/experience/delete', { id: fromValue.id }, 'post');
$api.msg('删除成功');
getUserResume().then(() => {
navBack();
});
@@ -123,13 +128,12 @@ function delCurrent() {
function initLoad(id) {
if (!id) return;
$api
.createRequest(`/app/user/experience/getSingle/${id}`, {}, "get")
$api.createRequest(`/app/user/experience/getSingle/${id}`, {}, 'get')
.then((res) => {
Object.assign(fromValue, res.data);
})
.catch((err) => {
console.error("获取工作经历失败:", err);
console.error('获取工作经历失败:', err);
});
}
@@ -137,7 +141,7 @@ function initLoad(id) {
const changestartTime = () => {
console.log(1);
datePicker.value.open({
title: "选择开始时间",
title: '选择开始时间',
defaultDate: fromValue.startTime,
success: (selectedDate) => {
fromValue.startTime = selectedDate;
@@ -148,14 +152,14 @@ const changestartTime = () => {
// 选择结束时间
const changeendTime = () => {
datePicker.value.open({
title: "选择结束时间",
title: '选择结束时间',
defaultDate: fromValue.endTime,
success: (selectedDate) => {
fromValue.endTime = selectedDate;
// 如果结束时间早于新的开始时间,清空结束时间
if (fromValue.startTime && new Date(fromValue.startTime) > new Date(selectedDate)) {
fromValue.endTime = "";
$api.msg("结束时间不能小于开始时间!");
fromValue.endTime = '';
$api.msg('结束时间不能小于开始时间!');
}
},
});

View File

@@ -117,7 +117,7 @@
{
"path": "pages/vCard/vCard",
"style": {
"navigationBarTitleText": "子名片",
"navigationBarTitleText": "子名片",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
@@ -227,8 +227,8 @@
]
}],
"tabBar": {
"custom": true,
"display": "none",
// "custom": true,
// "display": "none",
"color": "#5E5F60",
"selectedColor": "#256BFA",
"borderStyle": "black",

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,53 +44,63 @@
<!-- 主体内容区域 -->
<view class="container-main">
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<view class="cards" v-if="fairList.length">
<view class="cards">
<transition-group name="stagger" tag="view" :css="true">
<view
class="card press-button"
v-for="(item, index) in fairList"
:key="index"
@click="navTo('/packageA/pages/exhibitors/exhibitors?jobFairId=' + item.jobFairId)"
:key="item.zphID"
:style="{ '--i': index }"
@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>
</transition-group>
</view>
<empty v-else pdTop="200"></empty>
<empty v-if="!fairList.length" content="暂时没有结果,下一天也许就有惊喜"></empty>
</scroll-view>
</view>
<Tabbar :currentpage="1"></Tabbar>
<!-- <Tabbar :currentpage="1"></Tabbar> -->
</view>
</view>
</template>
@@ -94,8 +111,9 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
import config from '@/config';
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({});
@@ -108,7 +126,7 @@ const pageState = reactive({
total: 0,
maxPage: 2,
pageSize: 10,
search: {},
zphmc: '',
});
onLoad(() => {
@@ -122,13 +140,21 @@ onLoad(() => {
startDate: currentDate,
});
weekList.value = result;
currentDay.value.fullDate = result[0].fullDate;
getFair('refresh');
});
const handleItemClick = (item) => {
lightAppJssdk.navigation.hide({
url: config.virtualJobFair,
});
};
function toSelectDate() {
navTo('/packageA/pages/selectDate/selectDate', {
query: {
date: currentDay.value.fullDate,
entrance: 'careerfair',
},
onBack: (res) => {
console.log(res);
@@ -155,12 +181,15 @@ function changeSwiperMsgType(e) {
function seemsg(index) {
if (index === 1) {
return $api.msg('功能确定中');
handleItemClick();
// $api.msg('功能确定中');
return;
}
state.current = index;
}
const handleScrollToLower = () => {
return;
getFair();
console.log('触底');
};
@@ -174,21 +203,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', false).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;
}
@@ -253,8 +285,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;

View File

@@ -63,9 +63,9 @@
<ai-paging ref="paging"></ai-paging>
</view>
<!-- 自定义tabbar -->
<view class="chatmain-footer" v-show="!isDrawerOpen">
<!-- <view class="chatmain-footer" v-show="!isDrawerOpen">
<Tabbar :currentpage="2"></Tabbar>
</view>
</view> -->
</view>
</view>
</template>
@@ -111,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();
}
};
@@ -213,27 +213,48 @@ footer-height = 98rpx
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;

View File

@@ -148,7 +148,7 @@
<view
class="input_vio"
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove"
@touchmove.passive="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
:catchtouchstart="true"
@@ -240,7 +240,7 @@
</scroll-view>
</view>
</view>
<PopupFeeBack ref="feeback" @onSend="confirmFeeBack"></PopupFeeBack>
<PopupFeeBack ref="feeback" @onClose="colseFeeBack" @onSend="confirmFeeBack"></PopupFeeBack>
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
</view>
</template>
@@ -268,10 +268,10 @@ 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';
import { useAudioRecorder } from '@/hook/useRealtimeRecorder2.js';
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer2.js';
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
@@ -612,17 +612,23 @@ function userGoodFeedback(msg) {
// $api.msg('该功能正在开发中,敬请期待后续更新!');
feeback.value?.open();
feebackData.value = msg;
uni.hideTabBar()
}
function confirmFeeBack(value) {
useChatGroupDBStore()
.badFeedback(feebackData.value, value)
.then(() => {
uni.showTabBar()
feeback.value?.close();
feeBackTips.value?.open();
});
}
function colseFeeBack() {
uni.showTabBar()
}
function readMarkdown(value, index) {
speechIndex.value = index;
if (speechIndex.value !== index) {
@@ -632,7 +638,7 @@ function readMarkdown(value, index) {
if (isPaused.value) {
resume();
} else {
console.log(value, speechIndex.value, index, isPaused.value)
// console.log(value, speechIndex.value, index, isPaused.value)
speak(value);
}
}

View File

@@ -1,5 +1,5 @@
<template>
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" background-color="#F6F6F6">
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" @change="changePopup" background-color="#F6F6F6">
<view class="feeback">
<view class="titile">反馈</view>
<view class="pop-h3">针对问题</view>
@@ -38,7 +38,7 @@
<script setup>
import { ref, inject } from 'vue';
const emit = defineEmits(['onSend']);
const emit = defineEmits(['onSend', 'onClose']);
const { $api } = inject('globalFunction');
const popup = ref(null);
const inputText = ref('');
@@ -66,6 +66,13 @@ function close() {
popup.value.close();
}
function changePopup(e) {
if (e.show) {
} else {
emit('onClose');
}
}
function send() {
const text = getLabel();
if (text) {

View File

@@ -1,345 +1,371 @@
<template>
<view class="container">
<!-- 用于承载 PIXI Canvas -->
<!-- #ifdef H5 -->
<view id="matchCanvas" class="match-canvas"></view>
<!-- #endif -->
<!-- #ifndef H5 -->
<canvas type="webgl" id="matchCanvas" canvas-id="matchCanvas" class="match-canvas" />
<!-- #endif -->
</view>
<view class="container" id="pixi-box" ref="pixiContainerRef"></view>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import * as PIXI from "pixi.js";
import { onMounted, onUnmounted, ref, nextTick } from 'vue';
const emit = defineEmits(['tag-click']);
const appRef = ref(null); // 存储 PIXI 应用实例
// 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: 0x0069fe,
fontColor: 0xffffff,
size: 17,
opacity: 1.0,
angle: 0,
radius: 0,
},
{
name: "工程师",
name: '工程师',
bgColor: 0x87e2ec,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: -Math.PI / 2, // 12点方向
radius: 60,
tailRotation: Math.PI / 2, // 拖尾向下
angle: -Math.PI / 2,
radius: 68,
tailRotation: Math.PI / 2,
},
{
name: "建筑师",
name: '建筑师',
bgColor: 0xffebeb,
fontColor: 0xf86e6e,
tailColor: 0xffe1e1,
fontColor: 0xff6969,
size: 11.5,
opacity: 1,
angle: -Math.PI / 4, // 1点方向
radius: 115,
tailRotation: (3 * Math.PI) / 4, // 拖尾向左下
angle: -Math.PI / 4.2,
radius: 125,
tailRotation: (3 * Math.PI) / 4,
},
{
name: "律师",
name: '律师',
bgColor: 0x21ea85,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: -Math.PI / 10, // 2点方向
angle: -Math.PI / 10,
radius: 130,
tailRotation: (3 * Math.PI) / 4, // 拖尾向左下
tailRotation: (3 * Math.PI) / 4,
},
{
name: "记者",
name: '记者',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 12,
opacity: 1,
angle: Math.PI / 120, // 3点方向
angle: Math.PI / 120,
radius: 130,
tailRotation: Math.PI, // 拖尾向左
tailRotation: (3 * Math.PI) / 3.4,
},
{
name: "程序员",
bgColor: 0xff9d57,
name: '程序员',
bgColor: 0xffd4b6,
fontColor: 0xffffff,
size: 14.5,
opacity: 0.6,
angle: Math.PI / 9, // 4点方向
size: 14,
opacity: 1,
angle: Math.PI / 7,
radius: 120,
tailRotation: (5 * Math.PI) / 4, // 拖尾向左上
tailRotation: (5 * Math.PI) / 4,
},
{
name: "摄影师",
name: '摄影师',
bgColor: 0xd8e5fe,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 11,
opacity: 1,
angle: Math.PI / 3.2, // 5点方向
radius: 75,
tailRotation: (3 * Math.PI) / 2, // 拖尾向上
angle: Math.PI / 3,
radius: 79,
tailRotation: (3 * Math.PI) / 2,
},
{
name: "设计师",
name: '设计师',
bgColor: 0xff9400,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: (2 * Math.PI) / 3, // 7点方向
angle: (2 * Math.PI) / 3,
radius: 92,
tailRotation: (7 * Math.PI) / 4, // 拖尾向右上
tailRotation: (7 * Math.PI) / 4,
},
{
name: "心理咨询师",
name: '心理咨询师',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 10.5,
opacity: 1,
angle: (5.4 * Math.PI) / 6, // 8点方向
angle: (5.4 * Math.PI) / 6,
radius: 110,
tailRotation: 0, // 拖尾向右
tailRotation:(3 * Math.PI) /1.78,
},
{
name: "护士",
name: '护士',
bgColor: 0xff6969,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: (6.3 * Math.PI) / 6, // 10点方向
angle: (6.3 * Math.PI) / 5.9,
radius: 110,
tailRotation: Math.PI / 4, // 拖尾向右下
tailRotation: Math.PI / 4,
},
{
name: "会计",
name: '会计',
bgColor: 0xfce9c9,
fontColor: 0xfbc55f,
size: 13,
opacity: 1,
angle: (7.2 * Math.PI) / 6, // 11点方向
radius: 115,
tailRotation: Math.PI / 4, // 拖尾向右下
angle: (7.2 * Math.PI) / 5.9,
radius: 120,
tailRotation: Math.PI / 4,
},
];
onMounted(async () => {
if (appRef.value) return;
// 初始化 PIXI 应用
const canvas = document.getElementById("matchCanvas");
const sw = canvas.clientWidth;
const sh = canvas.clientHeight;
const app = new PIXI.Application({
backgroundAlpha: 0,
antialias: true,
autoDensity: true,
width: sw,
height: sh,
backgroundColor: 0xf5f7fa,
await nextTick();
setTimeout(() => {
initPixi();
}, 100);
window.addEventListener('resize', handleResize);
});
appRef.value = app;
canvas.appendChild(app.view);
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (app) {
app.destroy(true, { children: true, texture: true, baseTexture: true });
app = null;
}
});
// 标签容器(管理所有标签)
const tagsContainer = new PIXI.Container();
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);
// 存储已放置标签的信息(位置、尺寸、浮动动画参数)
const placedTags = [];
renderScene(width, height);
};
for (let i = 0; i < mockTags.length; i++) {
const { angle, radius, tailRotation, ...tagData } = mockTags[i];
const x = sw / 2 + radius * Math.cos(angle);
const y = sh / 2 + radius * Math.sin(angle);
const tag = createTag(tagData, x, y, placedTags, app, i);
const renderScene = (sw, sh) => {
tagsContainer.removeChildren();
activeTagInstances = [];
// 上下浮动动画
const originalY = tag.y;
let floatOffset = Math.random() * Math.PI * 2;
let floatSpeed = 0.01 + Math.random() * 0.02;
let floatRange = 2 + Math.random() * 2;
const baseSize = 375;
const scaleFactor = (Math.min(sw, sh) / baseSize) * 0.9;
// 为标签添加彗星拖尾效果
if (radius > 0) {
// 中心标签不需要拖尾
const tail = createCometTail(tagData.bgColor, tailRotation, tag);
mockTags.forEach((data, index) => {
const scaledRadius = data.radius * (scaleFactor < 1 ? 1 : scaleFactor * 0.8);
// 修正:使用 addChildAt 将拖尾添加到最底层
tag.addChildAt(tail, 0);
let x = sw / 2 + scaledRadius * Math.cos(data.angle);
let y = sh / 2 + scaledRadius * Math.sin(data.angle);
// 为拖尾添加单独的动画
app.ticker.add(() => {
if (tail.updateTail) {
tail.updateTail();
}
});
}
// 使用PIXI ticker进行动画
app.ticker.add(() => {
floatOffset += floatSpeed;
tag.y = originalY + Math.sin(floatOffset) * floatRange;
});
const tag = createTag(data, index);
tagsContainer.addChild(tag);
placedTags.push({ ...tagData, bounds: tag.getBounds() });
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);
});
// 销毁时清理资源
onUnmounted(() => {
if (appRef.value) {
appRef.value.destroy(true, true);
appRef.value = null;
// 动画循环
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();
}
});
// 创建彗星拖尾效果
function createCometTail(bgColor, tailRotation, tag) {
const tailGroup = new PIXI.Container();
// 拖尾直接放在标签中心位置
tailGroup.x = 0;
tailGroup.y = 0;
const tail = new PIXI.Graphics();
tailGroup.addChild(tail);
// 拖尾参数
const baseLength = 45; // 基础长度
const startWidth = tag.width * 0.9; // 起始宽度(长边)
const endWidth = 35; // 末端宽度(短边)
// 拖尾动画参数
let breathPhase = Math.random() * Math.PI * 2;
const breathSpeed = 0.04;
// 更新拖尾的呼吸动画
tailGroup.updateTail = () => {
breathPhase += breathSpeed;
const breathScale = 0.85 + 0.15 * Math.sin(breathPhase);
tail.clear();
// 绘制梯形拖尾
const currentLength = baseLength * breathScale;
// 计算拖尾的四个顶点
// 长边在标签中心,水平方向(与标签长边平行)
const startLeft = { x: -startWidth / 2, y: 0 };
const startRight = { x: startWidth / 2, y: 0 };
// 短边在拖尾方向,保持水平
const endCenter = {
x: Math.cos(tailRotation) * currentLength,
y: Math.sin(tailRotation) * currentLength,
});
};
const endLeft = {
x: endCenter.x - endWidth / 2,
y: endCenter.y,
};
const endRight = {
x: endCenter.x + endWidth / 2,
y: endCenter.y,
};
// 使用分段绘制实现渐变
const segments = 6;
for (let i = 0; i < segments; i++) {
const progress = i / segments;
const nextProgress = (i + 1) / segments;
// 计算分段的位置 - 保持长边水平
const segmentStartLeft = {
x: startLeft.x * (1 - progress) + endLeft.x * progress,
y: startLeft.y * (1 - progress) + endLeft.y * progress,
};
const segmentStartRight = {
x: startRight.x * (1 - progress) + endRight.x * progress,
y: startRight.y * (1 - progress) + endRight.y * progress,
};
const segmentEndLeft = {
x: startLeft.x * (1 - nextProgress) + endLeft.x * nextProgress,
y: startLeft.y * (1 - nextProgress) + endLeft.y * nextProgress,
};
const segmentEndRight = {
x: startRight.x * (1 - nextProgress) + endRight.x * nextProgress,
y: startRight.y * (1 - nextProgress) + endRight.y * nextProgress,
};
// 透明度从0.4渐变到0
const segmentAlpha = 0.4 * (1 - progress);
tail.beginFill(bgColor, segmentAlpha);
tail.moveTo(segmentStartLeft.x, segmentStartLeft.y);
tail.lineTo(segmentEndLeft.x, segmentEndLeft.y);
tail.lineTo(segmentEndRight.x, segmentEndRight.y);
tail.lineTo(segmentStartRight.x, segmentStartRight.y);
tail.lineTo(segmentStartLeft.x, segmentStartLeft.y);
tail.endFill();
}
};
// 初始绘制
tailGroup.updateTail();
return tailGroup;
}
// 创建单个标签(背景+文本)
function createTag(tagData, x, y, placedTags, app, index) {
const createTag = (tagData, index) => {
const tagGroup = new PIXI.Container();
tagGroup.x = x;
tagGroup.y = y;
tagGroup.eventMode = 'static';
tagGroup.cursor = 'pointer';
tagGroup.on('pointertap', () => emit('tag-click', tagData));
// 先创建文本以测量宽度
const text = new PIXI.Text(tagData.name, {
fontFamily: "Arial",
fontFamily: ['PingFang SC', 'Microsoft YaHei', 'Arial'],
fontSize: tagData.size,
fill: tagData.fontColor,
padding: 4,
resolution: 2,
});
text.anchor.set(0.5);
// 根据文字个数动态计算宽度
const padding = 10;
const charWidth = tagData.size * 1.5;
const charHeight = tagData.size * 1.3;
const textWidth = tagData.name.length * charWidth;
const paddingH = 26;
const paddingV = 10;
let bgWidth = text.width + paddingH;
let bgHeight = text.height + paddingV;
let width = textWidth + padding * 2;
if (index == 0) width = tagData.size * 4.5 + 10;
if (index === 0) bgWidth = Math.max(bgWidth, tagData.size * 4.5);
const height = charHeight + padding;
// 背景(圆角矩形)
const bg = new PIXI.Graphics();
bg.beginFill(tagData.bgColor, tagData.opacity ?? 1);
bg.drawRoundedRect(-width / 2, -height / 2, width, height, 20);
bg.drawRoundedRect(-bgWidth / 2, -bgHeight / 2, bgWidth, bgHeight, bgHeight / 2);
bg.endFill();
tagGroup.addChild(bg);
// 添加文本
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%;
}
.match-canvas {
width: 100%;
height: 350rpx; /* 可根据需求调整高度 */
height: 500rpx;
position: relative;
overflow: hidden;
color: #b9d3ff;
}
</style>

View File

@@ -66,7 +66,7 @@
</view>
<view class="table-list">
<scroll-view :scroll-y="true" class="falls-scroll" @scroll="handleScroll" @scrolltolower="scrollBottom">
<view class="falls" v-if="list.length">
<view class="falls">
<custom-waterfalls-flow
:column="columnCount"
:columnSpace="columnSpace"
@@ -142,7 +142,7 @@
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
</view>
<empty v-else pdTop="200"></empty>
<empty v-if="!list.length"></empty>
</scroll-view>
</view>
<!-- 筛选 -->
@@ -298,6 +298,7 @@ function nextDetail(job) {
function openFilter() {
showFilter.value = true;
emits('onShowTabbar', false);
uni.hideTabBar();
selectFilterModel.value?.open({
title: '筛选',
maskClick: true,
@@ -310,10 +311,12 @@ function openFilter() {
}
showFilter.value = false;
getJobList('refresh');
uni.showTabBar();
},
cancel: () => {
showFilter.value = false;
emits('onShowTabbar', true);
uni.showTabBar();
},
});
}
@@ -359,7 +362,11 @@ function getJobRecommend(type = 'add') {
...pageState.search,
...conditionSearch.value,
};
let comd = { recommend: true, jobCategory: '', tip: '确认你的兴趣,为您推荐更多合适的岗位' };
let comd = {
recommend: true,
jobCategory: '',
tip: '确认你的兴趣,为您推荐更多合适的岗位',
};
$api.createRequest('/app/job/recommend', params).then((resData) => {
const { data, total } = resData;
pageState.total = 0;
@@ -380,7 +387,16 @@ function getJobRecommend(type = 'add') {
if (question) {
comd.jobCategory = question;
data.unshift(comd);
// 生成随机插入位置,排除前两个和最后两个位置
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);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
<template>
<scroll-view :scroll-y="true" class="app-container" :scroll-top="scrollTop" @scroll="checkStickyStatus" @scrolltolower="scrollBottom">
<scroll-view
:scroll-y="true"
class="app-container"
:scroll-top="scrollTop"
@scroll="checkStickyStatus"
@scrolltolower="scrollBottom"
>
<view class="nav-hidden">
<view class="container-search">
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
@@ -21,7 +27,7 @@
<view class="title">人工智能专区</view>
<view class="item-box">
<view class="box-l">
<view class="item1 button-click">
<view class="item1 button-click" @click="handleItemClick('素质测评')">
<view class="title">AI素质测评</view>
<view class="des">提高个人素质</view>
<image class="bg-text" mode="widthFix" src="@/static/icon/item-bg-text.png"></image>
@@ -29,13 +35,13 @@
</view>
</view>
<view class="box-r">
<view class="item2 button-click">
<view class="item2 button-click" @click="handleItemClick('就业指导')">
<view class="title">就业指导</view>
<view class="des">根据个人建议</view>
<image class="bg" src="@/static/icon/top-card-bg.png" />
<image class="bg-img" src="@/static/icon/item-bg-img2.png"></image>
</view>
<view class="item3 button-click">
<view class="item3 button-click" @click="handleItemClick('模拟面试')">
<view class="title">AI模拟面试</view>
<view class="des">提高面试成功率</view>
<image class="bg" src="@/static/icon/bottom-card-bg.png" />
@@ -43,7 +49,7 @@
</view>
</view>
</view>
<view class="tip">
<view class="tip button-click" @click="navTo('/packageA/pages/myResume/myResume')">
<image class="icon" src="@/static/icon/leart-gold.png" />
<view class="text">使用人工智能需要先完成素质测评完善简历 ></view>
</view>
@@ -55,7 +61,7 @@
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
<view class="title">简历匹配职位</view>
<view class="match-item-container">
<AIMatch ></AIMatch>
<AIMatch @tag-click="handleTagClick"></AIMatch>
</view>
</view>
</view>
@@ -74,28 +80,54 @@
</view>
</view>
<view class="list-card">
<image v-if="showVideoTip" @click="showVideoTip = false" class="video-mask" src="@/static/icon/video-mask.png" />
<image v-if="maskFirstEntry" @click="closeVideoTip" class="video-mask" src="@/static/icon/video-mask.png" />
<view class="nav-filter" :class="{ stuck: isSticky }">
<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)">
<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>
<image @click="navTo('/packageA/pages/addPosition/addPosition')" class="add-icon button-click" src="@/static/icon/add-circle.png"></image>
<image
@click="navTo('/packageA/pages/addPosition/addPosition')"
class="add-icon button-click"
src="@/static/icon/add-circle.png"
></image>
</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">
<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/polygon-down.png"></image>
<image
class="right-sx"
:class="{ active: showFilter }"
src="@/static/icon/polygon-down.png"
></image>
</view>
</view>
</view>
@@ -122,7 +154,10 @@
<view v-if="job.isHot">
<view class="falls-card-pay">
<view class="pay-text">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
<image class="flame" src="/static/icon/flame3.png"></image>
</view>
@@ -132,7 +167,10 @@
<view class="falls-card-title">{{ job.jobTitle }}</view>
<view class="falls-card-pay" style="margin-top: 10rpx">
<view class="pay-text">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
</view>
@@ -153,7 +191,7 @@
<view>
<image class="point2" src="/static/icon/pintDate2.png"></image>
<view class="fl_1">
{{ job.postingDate || "发布日期" }}
{{ job.postingDate || '发布日期' }}
</view>
</view>
<view>
@@ -176,7 +214,9 @@
</view>
<view v-if="!job.education" class="recommend-card" :class="{ isBut: job.isBut }">
<view class="card-content">
<view class="recommend-card-title">在找岗位{{ job.jobCategory }}的工作吗</view>
<view class="recommend-card-title">
在找岗位{{ job.jobCategory }}的工作吗
</view>
<!-- <view class="recommend-card-tip">{{ job.tip }}</view> -->
<view class="recommend-card-tip">确认您的兴趣为您推荐更多合适的岗位</view>
<!-- <view class="recommend-card-line"></view> -->
@@ -193,60 +233,62 @@
<loadmore ref="loadmoreRef"></loadmore>
<view v-if="showScrollBottom" style="height: 200px"></view>
</view>
<empty v-else pdTop="200"></empty>
<empty v-else is-position></empty>
</view>
</view>
</view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
<!-- <view class="maskFirstEntry" v-if="maskFirstEntry">
<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 class="maskFirstEntry-Close" @click="closefirstEntry">1</view>
</view>
</view> -->
</scroll-view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from "vue";
import img from "@/static/icon/filter.png";
import dictLabel from "@/components/dict-Label/dict-Label.vue";
const { $api, navTo, vacanciesTo, formatTotal, throttle } = inject("globalFunction");
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue';
import img from '@/static/icon/filter.png';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
const { $api, navTo, vacanciesTo, formatTotal, throttle } = 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";
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";
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();
import AIMatch from "./AIMatch.vue"
import config from '@/config';
import AIMatch from './AIMatch.vue';
const { proxy } = getCurrentInstance();
const showVideoTip = ref(true);
const maskFirstEntry = ref(true);
const isSticky = ref(false);
const showScrollBottom = ref(false);
const scrollTop = ref(0);
const emits = defineEmits(["onShowTabbar"]);
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",
tabIndex: 'all',
});
const list = ref([]);
const pageState = reactive({
@@ -258,31 +300,73 @@ const pageState = reactive({
order: 0,
},
});
const inputText = ref("");
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: "最新发布" },
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
]);
const isLoaded = ref(false);
const occupations = [
'律师',
'工程师',
'医生',
'教师',
'设计师',
'程序员',
'会计师',
'建筑师',
'护士',
'销售',
'经理',
'顾问',
'分析师',
'研究员',
'编辑',
'记者',
'摄影师',
'厨师',
'司机',
'保安',
'客服',
'行政',
'人事',
'市场',
'运营',
'产品',
'测试',
'运维',
'前端',
'后端',
'全栈',
'数据',
'策划',
'导演',
'演员',
'歌手',
'作家',
'画家',
'翻译',
'导游',
];
const colors = ['#0069FE', '#FF9400', '#FF6969', '#21EA85', '#87E2EC'];
const occupations = ["律师", "工程师", "医生", "教师", "设计师", "程序员", "会计师", "建筑师", "护士", "销售", "经理", "顾问", "分析师", "研究员", "编辑", "记者", "摄影师", "厨师", "司机", "保安", "客服", "行政", "人事", "市场", "运营", "产品", "测试", "运维", "前端", "后端", "全栈", "数据", "策划", "导演", "演员", "歌手", "作家", "画家", "翻译", "导游"];
const colors = ["#0069FE", "#FF9400", "#FF6969", "#21EA85", "#87E2EC"];
onMounted(() => {
let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读
maskFirstEntry.value = firstEntry;
});
const checkStickyStatus = (e) => {
scrollTop.value = e.detail.scrollTop;
nextTick(() => {
const query = uni.createSelectorQuery().in(proxy);
query
.select(".nav-filter")
.select('.nav-filter')
.boundingClientRect()
.exec((res) => {
if (res[0]) {
@@ -296,6 +380,38 @@ const checkStickyStatus = (e) => {
});
};
function closeVideoTip() {
uni.setStorageSync('firstEntry', false);
maskFirstEntry.value = false;
}
const handleTagClick = (tagInfo) => {
console.log('点击的标签信息:', tagInfo);
$api.msg('暂未开放');
};
const handleItemClick = (item) => {
switch (item) {
case '素质测评':
lightAppJssdk.navigation.hide({
url: config.Quality_assessment_URL,
});
break;
case '就业指导':
lightAppJssdk.navigation.hide({
url: config.Career_guidance,
});
break;
case '模拟面试':
lightAppJssdk.navigation.hide({
url: config.mock_interview,
});
break;
default:
$api.msg('暂未开放');
}
};
const hexToRgba = (hex, opacity) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
@@ -308,12 +424,12 @@ const getTextColor = (hexColor) => {
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 180 ? "#1D71EF" : "#FFFFFF";
return brightness > 180 ? '#1D71EF' : '#FFFFFF';
};
const { columnCount, columnSpace } = useColumnCount(() => {
pageState.pageSize = 10 * (columnCount.value - 1);
getJobRecommend("refresh");
getJobRecommend('refresh');
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
@@ -331,14 +447,16 @@ async function loadData() {
}
const scrollBottom = () => {
if (loadmoreRef?.value?.status == "loading" || loadmoreRef?.value?.status == "noMore") return;
loadmoreRef.value.change("loading");
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
// if (loadmoreRef?.value?.status == 'loading' || loadmoreRef?.value?.status == 'noMore') return;
loadmoreRef.value.change('loading');
stopScroll();
if (state.tabIndex === "all") {
if (state.tabIndex === 'all') {
getJobRecommend();
} else {
getJobList();
}
}
};
function stopScroll() {
@@ -351,7 +469,7 @@ function stopScroll() {
function findJob(job) {
if (job.isBut) {
$api.msg("已确认");
$api.msg('已确认');
} else {
list.value = list.value.map((item) => {
if (item.recommend && item.jobCategory === job.jobCategory) {
@@ -364,13 +482,13 @@ function findJob(job) {
});
const jobstr = job.jobCategory;
const jobsObj = {
地区: "area",
岗位: "jobTitle",
经验: "experience",
地区: 'area',
岗位: 'jobTitle',
经验: 'experience',
};
const [name, value] = jobstr.split(":");
const [name, value] = jobstr.split(':');
const nameAttr = jobsObj[name];
if (name === "岗位") {
if (name === '岗位') {
conditionSearch.value[nameAttr] = value;
} else {
const valueAttr = oneDictData(nameAttr).filter((item) => item.label === value);
@@ -384,7 +502,7 @@ function findJob(job) {
function clearfindJob(job) {
if (job.isBut) {
$api.msg("已确认");
$api.msg('已确认');
} else {
list.value = list.value.map((item) => {
if (item.recommend && item.jobCategory === job.jobCategory) {
@@ -410,23 +528,23 @@ function nextDetail(job) {
function openFilter() {
showFilter.value = true;
emits("onShowTabbar", false);
emits('onShowTabbar', false);
selectFilterModel.value?.open({
title: "筛选",
title: '筛选',
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(",");
pageState.search[key] = value.join(',');
}
showFilter.value = false;
getJobList("refresh");
getJobList('refresh');
},
cancel: () => {
showFilter.value = false;
emits("onShowTabbar", true);
emits('onShowTabbar', true);
},
});
}
@@ -438,31 +556,31 @@ function handleFilterConfirm(e) {
function choosePosition(index) {
state.tabIndex = index;
list.value = [];
if (index === "all") {
if (index === 'all') {
pageState.search = {
order: pageState.search.order,
};
inputText.value = "";
getJobRecommend("refresh");
inputText.value = '';
getJobRecommend('refresh');
} else {
// const id = useUserStore().userInfo.jobTitleId.split(',')[index];
pageState.search.jobTitle = userInfo.value.jobTitle[index];
inputText.value = "";
getJobList("refresh");
inputText.value = '';
getJobList('refresh');
}
}
function handelHostestSearch(val) {
pageState.search.order = val.value;
if (state.tabIndex === "all") {
getJobRecommend("refresh");
if (state.tabIndex === 'all') {
getJobRecommend('refresh');
} else {
getJobList("refresh");
getJobList('refresh');
}
}
function getJobRecommend(type = "add") {
if (type === "refresh") {
function getJobRecommend(type = 'add') {
if (type === 'refresh') {
list.value = [];
if (waterfallsFlowRef.value) waterfallsFlowRef.value.refresh();
}
@@ -474,13 +592,13 @@ function getJobRecommend(type = "add") {
};
let comd = {
recommend: true,
jobCategory: "",
tip: "确认你的兴趣,为您推荐更多合适的岗位",
jobCategory: '',
tip: '确认你的兴趣,为您推荐更多合适的岗位',
};
$api.createRequest("/app/job/recommend", params).then((resData) => {
$api.createRequest('/app/job/recommend', params).then((resData) => {
const { data, total } = resData;
pageState.total = 0;
if (type === "add") {
if (type === 'add') {
// 记录系统
recommedIndexDb.getRecord().then((res) => {
if (res.length) {
@@ -497,7 +615,16 @@ function getJobRecommend(type = "add") {
if (question) {
comd.jobCategory = question;
data.unshift(comd);
// 生成随机插入位置,排除前两个和最后两个位置
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);
@@ -507,11 +634,11 @@ function getJobRecommend(type = "add") {
list.value = dataToImg(data);
}
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === "function") {
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (data.length < pageState.pageSize) {
loadmoreRef.value.change("noMore");
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change("more");
loadmoreRef.value.change('more');
}
}
// 当没有岗位刷新sessionId重新啦
@@ -521,11 +648,11 @@ function getJobRecommend(type = "add") {
});
}
function getJobList(type = "add") {
if (type === "add" && pageState.page < pageState.maxPage) {
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === "refresh") {
if (type === 'refresh') {
list.value = [];
pageState.page = 1;
pageState.maxPage = 2;
@@ -539,9 +666,9 @@ function getJobList(type = "add") {
// ...conditionSearch.value,
};
$api.createRequest("/app/job/list", params).then((resData) => {
$api.createRequest('/app/job/list', params).then((resData) => {
const { rows, total } = resData;
if (type === "add") {
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const reslist = dataToImg(rows);
@@ -552,11 +679,11 @@ function getJobList(type = "add") {
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === "function") {
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value?.change("noMore");
loadmoreRef.value?.change('noMore');
} else {
loadmoreRef.value?.change("more");
loadmoreRef.value?.change('more');
}
}
});
@@ -574,7 +701,7 @@ defineExpose({ loadData });
</script>
<style lang="stylus" scoped>
// .maskFristEntry
// .maskFirstEntry
// position: fixed;
// // right: 20rpx;
// // bottom: calc(50% - 200rpx);
@@ -589,7 +716,7 @@ defineExpose({ loadData });
// top: 40%
// transform: translate(-50%, -50%)
// flex-direction: column
// background: url('@/static/imgs/fristEntry.png') 0 0 no-repeat;
// background: url('@/static/imgs/firstEntry.png') 0 0 no-repeat;
// background-size: 100% 100%;
// width: 480rpx
// height: 584rpx
@@ -612,7 +739,7 @@ defineExpose({ loadData });
// .indicateArrow
// height: 76rpx
// width: 68rpx
// .indicatefristEntry
// .indicatefirstEntry
// width: 244rpx
// height: 244rpx
// .goExperience
@@ -626,7 +753,7 @@ defineExpose({ loadData });
// color: #FFFFFF;
// text-align: center;
// line-height: 60rpx
// .maskFristEntry-Close
// .maskFirstEntry-Close
// position: absolute;
// left: calc(50% - 10rpx);
// bottom: -130rpx
@@ -634,7 +761,7 @@ defineExpose({ loadData });
// height: 42rpx
// background: linear-gradient(273.34deg, #356CFA 3.58%, #A47FFD 85.84%);
// border-radius: 50%;
// .maskFristEntry-Close::before
// .maskFirstEntry-Close::before
// position: absolute;
// left: calc( 50% - 2rpx)
// top: calc( 50% - 10rpx)
@@ -643,7 +770,7 @@ defineExpose({ loadData });
// background: #FFFFFF
// width: 4rpx
// height: 20rpx
// .maskFristEntry-Close::after
// .maskFirstEntry-Close::after
// position: absolute;
// left: calc( 50% - 2rpx)
// top: calc( 50% - 10rpx)
@@ -887,6 +1014,8 @@ defineExpose({ loadData });
.match-item-container
width:100%;
margin-top:30rpx;
position :relative;
z-index:1
.match-item-row
display: flex;
justify-content: center;
@@ -992,7 +1121,7 @@ defineExpose({ loadData });
.video-mask{
position:fixed;
right:10rpx;
bottom:100rpx;
bottom:50rpx;
width:264rpx;
height:298rpx;
z-index:6
@@ -1089,6 +1218,8 @@ defineExpose({ loadData });
.falls-scroll
width: 100%
height: 100%
min-height: 100vh;
position: relative
.falls
padding: 28rpx 28rpx;
.item,.m-height{
@@ -1250,7 +1381,7 @@ defineExpose({ loadData });
text-align: center;
line-height: 56rpx
color: #fff
fontweight:500
font-weight:500
font-size: 24rpx;
.controll-no
width: 130rpx;
@@ -1259,7 +1390,7 @@ defineExpose({ loadData });
text-align: center;
line-height: 56rpx
color: #74AEFF
fontweight:500
font-weight :500
font-size: 24rpx;
border: 2rpx solid #74AEFF
box-sizing: border-box

View File

@@ -35,7 +35,6 @@
ref="waterfallsFlowRef"
:column="columnCount"
:columnSpace="columnSpace"
@loaded="imageloaded"
:value="list"
>
<template v-slot:default="job">
@@ -63,7 +62,8 @@
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
<empty v-if="!list.length"></empty>
<loadmore v-if="list.length >= pageSize" ref="loadmoreRef"></loadmore>
</view>
</scroll-view>
</view>
@@ -93,7 +93,7 @@ const state = reactive({
// 响应式搜索条件(可以被修改)
const searchParams = ref({});
const pageSize = ref(10);
const { list, loading, refresh, loadMore } = usePagination(
const { list, loading, refresh, loadMore,finished } = usePagination(
(params) => $api.createRequest('/app/job/littleVideo', params),
dataToImg, // 转换函数
{
@@ -105,10 +105,25 @@ const { list, loading, refresh, loadMore } = usePagination(
},
}
);
function imageloaded() {
loadmoreRef.value?.change('more');
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);
@@ -189,6 +204,7 @@ defineExpose({ loadData });
text-overflow: clip;
.scroll-content{
padding: 24rpx
height: calc(100% - 48rpx)
}
.nav-filter

View File

@@ -3,8 +3,21 @@
<view class="app-container">
<!-- 主体内容区域 -->
<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
@touchstart.passive="handleTouchStart"
@touchmove.passive="handleTouchMove"
@touchend="disableTouch = false"
class="swiper-item"
v-for="(_, index) in 2"
:key="index"
>
<!-- #ifndef MP-WEIXIN -->
<component
:is="components[index]"
@@ -28,7 +41,7 @@
</swiper>
</view>
<Tabbar v-show="showTabbar" :currentpage="0"></Tabbar>
<!-- <Tabbar v-show="showTabbar" :currentpage="0"></Tabbar> -->
<!-- maskFristEntry -->
<view class="maskFristEntry" v-if="maskFristEntry">
@@ -36,7 +49,7 @@
<text class="text1">左滑查看视频</text>
<text class="text2">快去体验吧</text>
<view class="goExperience" @click="goExperience">去体验</view>
<view class="maskFristEntry-Close" @click="closeFristEntry">1</view>
<view class="maskFristEntry-Close" @click="closeFristEntry"></view>
</view>
</view>
</view>
@@ -60,16 +73,32 @@ const { unreadCount } = storeToRefs(useReadMsg());
const showTabbar = ref(true);
const maskFristEntry = ref(false);
const disableTouch = ref(false);
const startPointX = ref(0);
const totalPage = 2;
const THRESHOLD = 5;
onLoad(() => {
// 判断浏览器是否有 fristEntry 第一次进入
let fristEntry = uni.getStorageSync('fristEntry') === false ? false : true; // 默认未读
maskFristEntry.value = fristEntry;
// maskFristEntry.value = true;
if (fristEntry) {
uni.hideTabBar();
}
// 预加载较重页面
setTimeout(() => {
uni.preloadPage({ url: '/packageA/pages/post/post' });
uni.preloadPage({ url: '/pages/nearby/nearby' });
uni.preloadPage({ url: '/pages/chat/chat' });
uni.preloadPage({ url: '/packageA/pages/choiceness/choiceness' });
uni.preloadPage({ url: '/packageA/pages/reservation/reservation' });
uni.preloadPage({ url: '/packageA/pages/Intendedposition/Intendedposition' });
}, 1000);
});
onShow(() => {
// 获取消息列表
useReadMsg().fetchMessages();
// useReadMsg().fetchMessages();
});
const state = reactive({
@@ -87,15 +116,69 @@ const handelComponentsRef = (el, index) => {
}
};
function handleTouchStart(e) {
startPointX.value = e.touches[0].clientX;
disableTouch.value = false;
}
function handleTouchMove(e) {
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 changeShowTabbar(val) {
showTabbar.value = val;
}
//1 查看消息类型
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;
@@ -117,10 +200,12 @@ function changeSwiperMsgType(e) {
function closeFristEntry() {
uni.setStorageSync('fristEntry', false);
maskFristEntry.value = false;
uni.showTabBar();
}
function goExperience() {
closeFristEntry();
uni.showTabBar();
state.current = 1;
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<AppLayout title="AI+就业服务程序">
<AppLayout title="就业服务程序">
<tabcontrolVue :current="tabCurrent">
<template v-slot:tab0>
<view class="login-content">
@@ -109,7 +109,10 @@
</template>
</tabcontrolVue>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<view class="backdoor" @click="loginbackdoor">后门</view>
<!-- 后门 -->
<!-- <view class="backdoor" @click="loginbackdoor">
<uni-icons type="gift-filled" size="30"></uni-icons>
</view> -->
</AppLayout>
</template>
@@ -126,7 +129,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: [],
@@ -150,7 +153,8 @@ const fromValue = reactive({
});
onLoad((parmas) => {
// getTreeselect();
getTreeselect();
$api.msg('请完善微简历');
});
onMounted(() => {});
@@ -239,9 +243,10 @@ 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() {
@@ -280,7 +285,7 @@ function loginTest() {
password: 'test',
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
$api.msg('模拟帐号密码测试登录成功');
$api.msg('模拟帐号密码测试登录成功 测试环境使用模拟定位');
loginSetToken(resData.token).then((resume) => {
if (resume.data.jobTitleId) {
// 设置推荐列表,每次退出登录都需要更新
@@ -309,9 +314,8 @@ function complete() {
<style lang="stylus" scoped>
.backdoor{
position: fixed;
left: 0;
top: 500rpx;
background: red
left: 24rpx;
top: 10rpx;
}
.input-nx
position: relative
@@ -425,6 +429,7 @@ function complete() {
font-size: 28rpx;
color: #6A6A6A;
.input-con
pointer-events: none;
font-weight: 400;
font-size: 32rpx;
color: #333333;

View File

@@ -49,14 +49,14 @@
</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>
</view>
<view class="row-right">已认证</view>
</view>
<view class="main-row btn-feel">
<view class="main-row btn-feel" @click="handleItemClick('素质测评')">
<view class="row-left">
<image class="left-img" src="@/static/icon/server2.png"></image>
<text class="left-text">素质测评</text>
@@ -65,7 +65,7 @@
<uni-icons color="#909090" type="right" size="14"></uni-icons>
</view>
</view>
<view class="main-row btn-feel">
<view class="main-row btn-feel" @click="handleItemClick('模拟面试')">
<view class="row-left">
<image class="left-img" src="@/static/icon/server3.png"></image>
<text class="left-text">AI面试</text>
@@ -82,7 +82,7 @@
<view class="row-right">已开启</view>
</view>
</view>
<view class="card-back button-click" @click="logOut">退出登录</view>
<!-- <view class="card-back button-click" @click="logOut">退出登录</view> -->
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
mode="base"
@@ -95,9 +95,9 @@
></uni-popup-dialog>
</uni-popup>
</view>
<template #footer>
<!-- <template #footer>
<Tabbar :currentpage="4"></Tabbar>
</template>
</template> -->
</AppLayout>
</template>
@@ -106,16 +106,18 @@ 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';
import config from '@/config';
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() {
@@ -128,11 +130,36 @@ function confirm() {
const isAbove90 = (percent) => parseFloat(percent) < 90;
function getUserstatistics() {
$api.createRequest('/app/user/statistics').then((resData) => {
counts.value = resData.data;
});
function selectFile() {
// FileUploader.showMenuAndUpload({
// success: function (res) {
// alert('上传成功: ' + JSON.stringify(res));
// },
// });
}
function chooseFileUploadTest(pam) {}
const handleItemClick = (item) => {
switch (item) {
case '素质测评':
lightAppJssdk.navigation.hide({
url: config.Quality_assessment_URL,
});
break;
case '就业指导':
lightAppJssdk.navigation.hide({
url: config.Career_guidance,
});
break;
case '模拟面试':
lightAppJssdk.navigation.hide({
url: config.mock_interview,
});
break;
default:
$api.msg('暂未开放');
}
};
</script>
<style lang="stylus" scoped>
@@ -276,6 +303,7 @@ function getUserstatistics() {
overflow: hidden
border-radius: 50%
margin-right: 22rpx;
background: #e8e8e8;
.userindo-head-img
width: 100%;
height: 100%;

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 -->
@@ -28,7 +40,7 @@
</swiper>
</view>
<Tabbar :currentpage="3"></Tabbar>
<!-- <Tabbar :currentpage="3"></Tabbar> -->
</view>
</view>
</template>
@@ -46,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();
@@ -59,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;
@@ -66,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;

View File

@@ -1,6 +1,6 @@
<template>
<scroll-view scroll-y class="main-scroll">
<view class="scrollmain">
<view v-if="msgList.length" class="scrollmain">
<view
class="list-card press-button"
v-for="(item, index) in msgList"
@@ -15,14 +15,15 @@
></image>
<image
class="card-img-flame"
v-if="item.title === '职位上新'"
v-else-if="item.title === '职位上新'"
src="/static/icon/msgtype2.png"
></image>
<image
class="card-img-flame"
v-if="item.title === '系统通知'"
v-else-if="item.title === '系统通知'"
src="/static/icon/msgtype3.png"
></image>
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
<view class="subscript" v-if="item.notReadCount || !item.isRead">
{{ item.notReadCount || '' }}
</view>
@@ -35,7 +36,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>
@@ -70,6 +73,9 @@ function seeDetail(item, index) {
case '系统通知':
navTo('/packageA/pages/systemNotification/systemNotification');
break;
default:
useReadMsg().markAsRead(item, index);
navTo('/packageA/pages/newJobPosition/newJobPosition');
}
}
@@ -83,6 +89,7 @@ defineExpose({ loadData });
}
.scrollmain{
padding: 28rpx
height: calc(100% - 56rpx)
}
.read{

View File

@@ -1,6 +1,6 @@
<template>
<scroll-view scroll-y class="main-scroll">
<view class="scrollmain">
<view v-if="unreadMsgList.length" class="scrollmain">
<view
class="list-card press-button"
v-for="(item, index) in unreadMsgList"
@@ -15,14 +15,15 @@
></image>
<image
class="card-img-flame"
v-if="item.title === '职位上新'"
v-else-if="item.title === '职位上新'"
src="/static/icon/msgtype2.png"
></image>
<image
class="card-img-flame"
v-if="item.title === '系统通知'"
v-else-if="item.title === '系统通知'"
src="/static/icon/msgtype3.png"
></image>
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
<view class="subscript" v-if="item.notReadCount">{{ item.notReadCount }}</view>
</view>
<view class="card-info">
@@ -33,7 +34,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>
@@ -57,6 +60,19 @@ async function loadData() {
function seeDetail(item) {
console.log(item);
switch (item.title) {
case '职位上新':
useReadMsg().markAsRead(item, index);
navTo('/packageA/pages/newJobPosition/newJobPosition');
break;
case '招聘会预约提醒':
useReadMsg().markAsRead(item, index);
navTo('/packageA/pages/reservation/reservation');
break;
case '系统通知':
navTo('/packageA/pages/systemNotification/systemNotification');
break;
}
}
defineExpose({ loadData });
@@ -69,6 +85,7 @@ defineExpose({ loadData });
}
.scrollmain{
padding: 28rpx
height: calc(100% - 56rpx)
}
.read{

View File

@@ -69,14 +69,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>
<!-- 筛选 -->
@@ -340,15 +335,18 @@ defineExpose({ loadData, handleFilterConfirm });
color: #4778EC !important
.nearby-scroll
overflow: hidden;
height: 100%;
background: #f4f4f4;
.two-head
margin: 22rpx;
padding: 22rpx;
display: flex;
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;
@@ -380,15 +378,21 @@ defineExpose({ loadData, handleFilterConfirm });
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;

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();
});
@@ -364,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;

View File

@@ -95,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>
<!-- 筛选 -->
@@ -359,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%
@@ -482,14 +480,21 @@ defineExpose({ loadData, handleFilterConfirm });
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;

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>
<!-- 筛选 -->
@@ -255,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;
@@ -284,14 +282,21 @@ defineExpose({ loadData, handleFilterConfirm });
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;

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="附近" :use-scroll-view="false" :show-bg-image="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -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 -->
@@ -38,6 +50,9 @@ import threeComponent from './components/three.vue';
import fourComponent from './components/four.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const { $api, debounce, throttle, navBack } = inject('globalFunction');
const loadedMap = reactive([false, false, false, false]);
const swiperRefs = [ref(null), ref(null), ref(null), ref(null)];
@@ -49,6 +64,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 +83,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;

View File

@@ -2,7 +2,12 @@
<view class="container">
<view>
<view class="top">
<image class="btnback button-click" src="@/static/icon/back.png" @click="navBack"></image>
<image
v-if="isMiniProgram"
class="btnback button-click"
src="@/static/icon/back.png"
@click="navBack"
></image>
<view class="search-box">
<uni-icons
class="iconsearch"
@@ -91,6 +96,8 @@ import { useColumnCount } from '@/hook/useColumnCount';
import { usePagination } from '@/hook/usePagination';
import img from '@/static/icon/filter.png';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const searchValue = ref('');
const historyList = ref([]);
const listCom = ref([]);
@@ -253,9 +260,10 @@ function getJobList(type = 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = listCom.value.length;
const reslist = rows;
listCom.value.splice(str, end, ...reslist);
// listCom.value.splice(str, end, ...reslist);
listCom.value = [...listCom.value, ...reslist];
} else {
listCom.value = rows;
listCom.value = [...rows];
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 217 B

BIN
static/icon/close-blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

BIN
static/icon/close-green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

After

Width:  |  Height:  |  Size: 730 B

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