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

139
App.vue
View File

@@ -2,26 +2,36 @@
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();
// 登录
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
try {
getUserInfo();
useUserStore().changMiniProgramAppStatus(false);
} catch {
console.log('不是爱山东平台,使用测试登陆');
useUserStore().changMiniProgramAppStatus(true);
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
} else {
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

@@ -190,4 +190,38 @@
.btn-rubberBand:active {
-webkit-animation-name: tada;
animation-name: tada
}
@keyframes jelly {
0% {
transform: scale(1);
}
30% {
transform: scale(1.25, 0.75);
}
/* 压扁 */
40% {
transform: scale(0.75, 1.25);
}
/* 拉长 */
50% {
transform: scale(1.15, 0.85);
}
/* 稍微压扁 */
65% {
transform: scale(0.95, 1.05);
}
/* 稍微拉长 */
75% {
transform: scale(1.05, 0.95);
}
100% {
transform: scale(1);
}
}

View File

@@ -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; */
}
@@ -464,4 +471,167 @@ 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%;
}
.position-center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.empty {
width: 100%;
min-height: 100vh;
position: relative;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// min-height: 100vh;
// height: 400rpx;
// position: relative;
.ty_content {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, 0);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// position: absolute;
// left: 50%;
// top: 0;
// transform: translate(-50%, 0);
.content_top {
width: 450rpx;
height: 322rpx;

View File

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

@@ -130,4 +130,4 @@ function nextDetail(company) {
.ris{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
}
</style>
</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,48 +1,54 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
<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>
</view>
</view>
<view class="card-companyName">{{ job.gwmc }}</view>
<view class="card-tags">
<view class="tag">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
<view class="tag">
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</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 class="card-companyName">{{ job.companyName }}</view>
<view class="card-tags">
<view class="tag">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
<view class="tag">
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</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 class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
<view class="date-jobTitle" v-else>
{{ 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,19 +17,25 @@
})();
</script>
<title></title>
<!-- vconsole -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<!-- eruda -->
<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
var vConsole = new window.VConsole();
vConsole.destroy();
eruda.init();
</script> -->
<!-- 爱山东jssdk -->
<!-- <script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script> -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
</script> -->
<!-- 爱山东jssdk 本sdk存在性能问题 -->
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
<!-- 只在内网有效 -->
<script type="text/javascript" src="./static/js/SM.js"></script>
<script type="text/javascript" src="./static/js/pixi.min.js"></script>
<script type="module" src="./static/js/sm4.min.js"></script>
</head>
<!-- <body> -->
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

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);
getCompanyInfo(options.companyId || options.bussinessId);
// console.log(options);
dataType.value = options.dataType ? parseInt(options.dataType) : 1;
pageOptions.value = options;
if (dataType.value === 2) {
// 第三方数据
getCompanyInfo(options.companyId, options.zphId);
getJobsList('refresh');
} else {
// 原数据
getCompanyInfo(options.companyId || options.bussinessId);
}
});
function companyCollection() {
const companyId = companyInfo.value.companyId;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId);
$api.msg('取消收藏成功');
if (dataType.value === 2) {
// 第三方数据收藏逻辑
const id = companyInfo.value.id;
const companyId = companyInfo.value.gsID;
const zphId = companyInfo.value.zphID;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${id}/2`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId, zphId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/company/collection/${id}/2`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId, zphId);
$api.msg('收藏成功');
});
}
} else {
// 原数据收藏逻辑
const companyId = companyInfo.value.companyId;
if (companyInfo.value.isCollection) {
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'DELETE').then((resData) => {
getCompanyInfo(companyId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId);
$api.msg('收藏成功');
});
}
}
}
function getCompanyInfo(...args) {
if (dataType.value === 2) {
// 第三方数据接口
const [companyId, zphId] = args;
$api.createRequest(`/app/internal/companyThirdPart/${companyId}/${zphId}`, {}, 'GET', true).then((resData) => {
companyInfo.value = resData.data;
});
} else {
$api.createRequest(`/app/company/collection/${companyId}`, {}, 'POST').then((resData) => {
getCompanyInfo(companyId);
$api.msg('收藏成功');
// 原数据接口
const [companyId] = args;
$api.createRequest(`/app/company/${companyId}`, {}, 'GET', true).then((resData) => {
companyInfo.value = resData.data;
getJobsList();
});
}
}
function getCompanyInfo(id) {
$api.createRequest(`/app/company/${id}`).then((resData) => {
companyInfo.value = resData.data;
getJobsList();
function getJobsList(type = 'add') {
if (dataType.value === 2) {
// 第三方数据职位列表
getThirdPartyJobsList(type);
} else {
// 原数据职位列表
getOriginalJobsList(type);
}
}
function getThirdPartyJobsList(type = 'add') {
const { companyId, companyName, zphId } = pageOptions.value;
if (type === 'refresh') {
pageState.current = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.current < pageState.maxPage) {
pageState.current += 1;
}
let params = {
current: pageState.current,
pageSize: pageState.pageSize,
};
$api.createRequest(
`/app/internal/jobThirdPart?gsID=${companyId}&gsmc=${companyName}&zphID=${zphId}`,
params,
'GET',
true
).then((resData) => {
const { rows, total } = resData;
handleJobsListResponse(type, rows, total, 'current');
});
}
function getJobsList(type = 'add') {
function getOriginalJobsList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
@@ -118,25 +205,32 @@ function getJobsList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
};
$api.createRequest(`/app/company/job/${companyInfo.value.companyId}`, params).then((resData) => {
$api.createRequest(`/app/company/job/${companyInfo.value.companyId}`, params, 'GET', true).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
handleJobsListResponse(type, rows, total, 'page');
});
}
function handleJobsListResponse(type, rows, total, pageKey) {
if (type === 'add') {
const str = pageState.pageSize * (pageState[pageKey] - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list = [...pageState.list, ...rows];
// pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}
pageState.total = total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
}
function expand() {
isExpanded.value = !isExpanded.value;
}
@@ -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;
@@ -285,7 +380,7 @@ image {
}
.card-companyName{
font-weight: 400;
font-size: 28rpx;
font-size: 28rpx;
color: #6C7282;
}
.card-tags{

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>
@@ -12,11 +12,33 @@
<view class="lf-text">选择想找的工作我的将在首页为你推荐</view>
</view>
<view class="title-ri">
<!-- <text style="color: #256bfa">2</text>
<!-- <text style="color: #256bfa">2</text>
<text>/2</text> -->
</view>
</view>
<view class="content-list">
<view class="list-search">
<uni-icons type="search" color="#333333" size="24"></uni-icons>
<input
class="search-input"
v-model="inputVal"
placeholder="请输入岗位名称"
@input="handelChangeInpute"
@blur="handleBlur"
@focus="handleFocus"
/>
<view class="search-container" v-show="filterList.length">
<view
class="list-item"
v-for="(item, index) in filterList"
@click="handelClickItem(item)"
:key="item.id"
>
<view class="item-txt line_1">{{ item.lable }}</view>
</view>
</view>
</view>
<view class="list-row" v-for="(item, index) in userInfo.jobTitle" :key="index">
<text>{{ item }}</text>
<image
@@ -31,18 +53,85 @@
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
</AppLayout>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
mode="base"
title="确定添加该期望岗位吗"
type="info"
:duration="2000"
:before-close="true"
@confirm="confirm"
@close="close"
></uni-popup-dialog>
</uni-popup>
</template>
<script setup>
import { inject, ref, reactive } from 'vue';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
const { $api, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { getUserResume } = useUserStore();
const { userInfo } = storeToRefs(useUserStore());
const { 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,16 +24,14 @@
</view>
<scroll-view scroll-y class="main-scroll" @scrolltolower="getJobList('add')">
<view class="one-cards">
<view class="mian">
<renderJobs
:list="pageState.list"
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
<!-- <loadmore ref="loadmoreRef"></loadmore> -->
</view>
<renderJobViewRecord
:list="pageState.list"
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobViewRecord>
<empty v-else></empty>
<!-- <loadmore ref="loadmoreRef"></loadmore> -->
</view>
</scroll-view>
</view>
@@ -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>
</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,109 +1,214 @@
<template>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top" style="margin-top: 12rpx; padding: 0; background: none">
<view class="mys-tops btn-feel">
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || "编辑用户名" }}</text>
<view class="edit-icon mar_le10">
<image class="button-click" src="@/static/icon/edit1.png" @click="navTo('/packageA/pages/personalInfo/personalInfo')"></image>
</view>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label class="mar_ri10" dictType="affiliation" :value="userInfo.politicalAffiliation"></dict-Label>
</view>
<view class="subName">{{ userInfo.phone }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="right-sex">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy1.png"></image>
<image v-else src="@/static/icon/girl1.png"></image>
</view>
</view>
</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-line">
<view class="line"></view>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top" style="margin-top: 12rpx; padding: 0; background: none">
<view class="mys-tops">
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || '编辑用户名' }}</text>
<view class="edit-icon mar_le10">
<image
class="button-click"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/personalInfo/personalInfo')"
></image>
</view>
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age || '-' }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label
class="mar_ri10"
dictType="affiliation"
:value="userInfo.politicalAffiliation"
></dict-Label>
</view>
<view class="subName">{{ userInfo.phone || '未知手机号' }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="right-sex">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy1.png"></image>
<image v-else src="@/static/icon/girl1.png"></image>
</view>
</view>
</view>
<!-- 求职期望 -->
<view class="mys-line">
<view class="line"></view>
</view>
<view class="mys-info">
<view class="mys-h4">
<view>求职期望</view>
<image
class="icon"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/jobExpect/jobExpect')"
></image>
</view>
<view class="mys-text">
<text>期望薪资</text>
<text>{{ userInfo.salaryMin / 1000 }}k-{{ userInfo.salaryMax / 1000 }}k</text>
</view>
<view class="mys-text">
<text>期望工作地</text>
<text>青岛市-</text>
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
<view class="mys-list">
<view class="cards button-click" v-for="(title, index) in userInfo.jobTitle" :key="index">
{{ title }}
</view>
</view>
</view>
</view>
<view class="card-top" style="margin-top: 24rpx">
<view class="mys-info" style="padding: 0">
<view class="mys-h4">
<text>工作经历</text>
<view class="mys-edit-icon btn-tada" @click="navTo('/packageA/pages/workExp/workExp')">
<image class="icon button-click btn-feel" src="@/static/icon/plus.png"></image>
<view class="txt">添加</view>
</view>
</view>
<view class="exp-item button-click" v-for="item in userInfo.workExp" :key="item.id">
<view class="fl_box fl_justbet mar_top15">
<view class="fs_16">{{ item.company }}</view>
<image
class="icon btn-feel"
src="@/static/icon/edit1.png"
@click="navTo(`/packageA/pages/workExp/workExp?id=${item.id}`)"
></image>
</view>
<view class="mys-text fl_box fl_justbet">
<text class="color_333333 fs_14">{{ item.position }}</text>
<text class="datetext">{{ item.startTime }}--{{ item.endTime || '至今' }}</text>
</view>
<view class="mys-text">
<text>{{ item.duty }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="mys-info btn-feel">
<view class="mys-h4">
<view>求职期望</view>
<image class="icon" src="@/static/icon/edit1.png" @click="navTo('/packageA/pages/jobExpect/jobExpect')"></image>
</view>
<view class="mys-text">
<text>期望薪资</text>
<text>{{ userInfo.salaryMin / 1000 }}k-{{ userInfo.salaryMax / 1000 }}k</text>
</view>
<view class="mys-text">
<text>期望工作地</text>
<text>青岛市-</text>
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
<view class="mys-list">
<view class="cards button-click" v-for="(title, index) in userInfo.jobTitle" :key="index">
{{ title }}
<template #footer>
<view class="footer-container">
<view class="footer-button btn-feel" @click="chooseResume">上传简历</view>
</view>
</view>
</view>
</view>
<view class="card-top" style="margin-top: 24rpx">
<view class="mys-info" style="padding: 0">
<view class="mys-h4 btn-feel">
<text>工作经历</text>
<view class="mys-edit-icon btn-feel" @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="fl_box fl_justbet mar_top15">
<view class="fs_16">{{ item.company }}</view>
<image class="icon btn-feel" src="@/static/icon/edit1.png" @click="navTo(`/packageA/pages/workExp/workExp?id=${item.id}`)"></image>
</view>
<view class="mys-text fl_box fl_justbet">
<text class="color_333333 fs_14">{{ item.position }}</text>
<text class="datetext">{{ item.startTime }}--{{ item.endTime || "至今" }}</text>
</view>
<view class="mys-text">
<text>{{ item.duty }}</text>
</view>
</view>
</view>
</view>
</view>
<template #footer>
<view class="footer-container">
<view class="footer-button btn-feel">上传简历</view>
</view>
</template>
</AppLayout>
</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,267 +1,295 @@
<template>
<AppLayout title="个人信息" :sub-title="`完成度${percent}`" border back-gorund-color="#ffffff" :show-bg-image="false">
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click" @click="confirm">确认</view>
</template>
<view class="content">
<view class="content-avatar">
<view class="avatar-title">编辑头像</view>
<view @click="selectAvatar">
<image class="avatar" v-if="fromValue.avatar" :src="fromValue.avatar" />
<image class="avatar" v-else-if="fromValue.sex == '0'" src="@/static/icon/boy.png" />
<image class="avatar" v-else="fromValue.sex == '1'" src="@/static/icon/girl.png" />
<AppLayout
title="个人信息"
:sub-title="`完成度${percent}`"
border
back-gorund-color="#ffffff"
:show-bg-image="false"
>
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click" @click="confirm">确认</view>
</template>
<view class="content">
<view class="content-avatar">
<view class="avatar-title">编辑头像</view>
<view @click="selectAvatar">
<image class="avatar" v-if="fromValue.avatar" :src="fromValue.avatar" />
<image class="avatar" v-else-if="fromValue.sex == '0'" src="@/static/icon/boy.png" />
<image class="avatar" v-else="fromValue.sex == '1'" src="@/static/icon/girl.png" />
</view>
</view>
<view class="content-input">
<view class="input-titile">姓名</view>
<input class="input-con" v-model="fromValue.name" placeholder="请输入您的姓名" />
</view>
<view class="content-sex">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 0 }" @click="changeSex(0)">
</view>
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 1 }" @click="changeSex(1)">
</view>
</view>
</view>
<view class="content-input" @click="changeDateBirt">
<view class="input-titile">出生年月</view>
<input
class="input-con triangle pointEveNone"
v-model="fromValue.birthDate"
disabled
placeholder="请选择您的出生年月"
/>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<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 pointEveNone"
v-model="state.politicalAffiliationText"
disabled
placeholder="请选择您的政治面貌"
/>
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
<input class="input-con" v-model="fromValue.phone" placeholder="请输入您的手机号码" />
</view>
</view>
</view>
<view class="content-input">
<view class="input-titile">姓名</view>
<input class="input-con" v-model="fromValue.name" placeholder="请输入您的姓名" />
</view>
<view class="content-sex">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 0 }" @click="changeSex(0)"> </view>
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 1 }" @click="changeSex(1)"> </view>
</view>
</view>
<view class="content-input" @click="changeDateBirt">
<view class="input-titile">出生年月</view>
<input class="input-con triangle" v-model="fromValue.birthDate" disabled placeholder="请选择您的出生年月" />
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con triangle" v-model="state.educationText" disabled placeholder="请选择您的学历" />
</view>
<view class="content-input" @click="changePoliticalAffiliation">
<view class="input-titile">政治面貌</view>
<input class="input-con triangle" v-model="state.politicalAffiliationText" disabled placeholder="请选择您的政治面貌" />
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
<input class="input-con" v-model="fromValue.phone" placeholder="请输入您的手机号码" />
</view>
</view>
</AppLayout>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
const { $api, navTo, navBack, checkingPhoneRegExp } = inject("globalFunction");
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, navBack, checkingPhoneRegExp } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
const openSelectPopup = inject("openSelectPopup");
const openSelectPopup = inject('openSelectPopup');
const percent = ref("0%");
const percent = ref('0%');
const state = reactive({
educationText: "",
politicalAffiliationText: "",
educationText: '',
politicalAffiliationText: '',
});
const fromValue = reactive({
name: "",
sex: 0,
birthDate: "",
education: "",
politicalAffiliation: "",
avatar: "",
name: '',
sex: 0,
birthDate: '',
education: '',
politicalAffiliation: '',
avatar: '',
});
onLoad(() => {
initLoad();
// setTimeout(() => {
// const { age, birthDate } = useUserStore().userInfo;
// const newAge = calculateAge(birthDate);
// // 计算年龄是否对等
// if (age != newAge) {
// completeResume();
// }
// }, 1000);
initLoad();
// setTimeout(() => {
// const { age, birthDate } = useUserStore().userInfo;
// const newAge = calculateAge(birthDate);
// // 计算年龄是否对等
// if (age != newAge) {
// completeResume();
// }
// }, 1000);
});
function initLoad() {
fromValue.name = userInfo.value.name;
fromValue.sex = Number(userInfo.value.sex);
fromValue.phone = userInfo.value.phone;
fromValue.birthDate = userInfo.value.birthDate;
fromValue.education = userInfo.value.education;
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
fromValue.avatar = userInfo.value.avatar;
// 回显
state.educationText = dictLabel("education", userInfo.value.education);
state.politicalAffiliationText = dictLabel("affiliation", userInfo.value.politicalAffiliation);
const result = getFormCompletionPercent(fromValue);
percent.value = result;
fromValue.name = userInfo.value.name;
fromValue.sex = Number(userInfo.value.sex);
fromValue.phone = userInfo.value.phone;
fromValue.birthDate = userInfo.value.birthDate;
fromValue.education = userInfo.value.education;
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
fromValue.avatar = userInfo.value.avatar;
// 回显
state.educationText = dictLabel('education', userInfo.value.education);
state.politicalAffiliationText = dictLabel('affiliation', userInfo.value.politicalAffiliation);
const result = getFormCompletionPercent(fromValue);
percent.value = result;
}
const confirm = () => {
if (!fromValue.name) {
return $api.msg("请输入姓名");
}
if (!fromValue.birthDate) {
return $api.msg("请选择出生年月");
}
if (!fromValue.education) {
return $api.msg("请选择学历");
}
if (!fromValue.politicalAffiliation) {
return $api.msg("请选择政治面貌");
}
// if (!checkingPhoneRegExp(fromValue.phone)) {
// return $api.msg('请输入正确手机号');
// }
const params = {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest("/app/user/resume", params, "post").then((resData) => {
$api.msg("完成");
state.disbleDate = true;
getUserResume().then(() => {
navBack();
if (!fromValue.name) {
return $api.msg('请输入姓名');
}
if (!fromValue.birthDate) {
return $api.msg('请选择出生年月');
}
if (!fromValue.education) {
return $api.msg('请选择学历');
}
if (!fromValue.politicalAffiliation) {
return $api.msg('请选择政治面貌');
}
// if (!checkingPhoneRegExp(fromValue.phone)) {
// return $api.msg('请输入正确手机号');
// }
const params = {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest('/app/user/resume', params, 'post').then((resData) => {
$api.msg('完成');
state.disbleDate = true;
getUserResume().then(() => {
navBack();
});
});
});
};
const changeDateBirt = () => {
const datearray = generateDatePickerArrays();
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
openSelectPopup({
title: "年龄段",
maskClick: true,
data: datearray,
defaultIndex,
success: (_, value) => {
const [year, month, day] = value;
const dateStr = `${year.value}-${month.value}-${day.value}`;
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg("没有这一天");
}
},
});
const datearray = generateDatePickerArrays();
let defaultIndex = [0, 0, 0];
if (fromValue.birthDate) {
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
}
console.log(defaultIndex);
openSelectPopup({
title: '年龄段',
maskClick: true,
data: datearray,
defaultIndex,
success: (_, value) => {
const [year, month, day] = value;
const dateStr = `${year.value}-${month.value}-${day.value}`;
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg('没有这一天');
}
},
});
};
const changeEducation = () => {
openSelectPopup({
title: "学历",
maskClick: true,
data: [oneDictData("education")],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
},
});
openSelectPopup({
title: '学历',
maskClick: true,
data: [oneDictData('education')],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
},
});
};
const changeSex = (sex) => {
fromValue.sex = sex;
fromValue.sex = sex;
};
const changePoliticalAffiliation = () => {
openSelectPopup({
title: "政治面貌",
maskClick: true,
data: [oneDictData("affiliation")],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
},
});
openSelectPopup({
title: '政治面貌',
maskClick: true,
data: [oneDictData('affiliation')],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
},
});
};
function generateDatePickerArrays(startYear = 1975, endYear = new Date().getFullYear()) {
const years = [];
const months = [];
const days = [];
const years = [];
const months = [];
const days = [];
for (let y = startYear; y <= endYear; y++) {
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, "0"));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, "0"));
}
for (let y = startYear; y <= endYear; y++) {
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, '0'));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, '0'));
}
return [years, months, days];
return [years, months, days];
}
function isValidDate(dateString) {
const [year, month, day] = dateString.split("-").map(Number);
const [year, month, day] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day); // 月份从0开始
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
const date = new Date(year, month - 1, day); // 月份从0开始
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
}
const calculateAge = (birthDate) => {
const birth = new Date(birthDate);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
const dayDiff = today.getDate() - birth.getDate();
const birth = new Date(birthDate);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
const dayDiff = today.getDate() - birth.getDate();
// 如果生日的月份还没到,或者刚到生日月份但当天还没过,则年龄减 1
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
// 如果生日的月份还没到,或者刚到生日月份但当天还没过,则年龄减 1
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
return age;
return age;
};
function getFormCompletionPercent(form) {
let total = Object.keys(form).length;
let filled = 0;
let total = Object.keys(form).length;
let filled = 0;
for (const key in form) {
const value = form[key];
if (value !== "" && value !== null && value !== undefined) {
if (typeof value === "number") {
filled += 1;
} else if (typeof value === "string" && value.trim() !== "") {
filled += 1;
}
for (const key in form) {
const value = form[key];
if (value !== '' && value !== null && value !== undefined) {
if (typeof value === 'number') {
filled += 1;
} else if (typeof value === 'string' && value.trim() !== '') {
filled += 1;
}
}
}
}
if (total === 0) return "0%";
const percent = (filled / total) * 100;
return percent.toFixed(0) + "%"; // 取整,不要小数点
if (total === 0) return '0%';
const percent = (filled / total) * 100;
return percent.toFixed(0) + '%'; // 取整,不要小数点
}
// 主函数
function getDatePickerIndexes(dateStr) {
const [year, month, day] = dateStr.split("-");
const [year, month, day] = dateStr.split('-');
const [years, months, days] = generateDatePickerArrays();
const [years, months, days] = generateDatePickerArrays();
const yearIndex = years.indexOf(year);
const monthIndex = months.indexOf(month);
const dayIndex = days.indexOf(day);
const yearIndex = years.indexOf(year);
const monthIndex = months.indexOf(month);
const dayIndex = days.indexOf(day);
return [yearIndex, monthIndex, dayIndex];
return [yearIndex, monthIndex, dayIndex];
}
function selectAvatar() {
uni.chooseImage({
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
count: 1,
success: ({ tempFilePaths, tempFiles }) => {
$api
.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg("上传失败");
});
},
fail: (error) => {},
});
uni.chooseImage({
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
count: 1,
success: ({ tempFilePaths, tempFiles }) => {
$api.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg('上传失败');
});
},
fail: (error) => {},
});
}
</script>

View File

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

View File

@@ -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
>
@@ -187,4 +187,4 @@ defineExpose({ open });
background: rgba(255, 255, 255, 0.2);
border-radius: 4rpx;
}
</style>
</style>

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,91 +228,166 @@ 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) {
return new Promise((reslove, reject) => {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
if (dataType.value === 2) {
// 第三方数据接口
return new Promise((reslove, reject) => {
$api.createRequest(`/app/internal/jobThirdPart/${jobId}`, {}, 'GET', true).then((resData) => {
const { gsID, gsmc, zphID } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(gsID, gsmc, zphID);
if (resData.data.latitude && resData.data.longitude) {
initMapCovers(resData.data.latitude, resData.data.longitude, resData.data.gsmc);
}
});
});
} else {
// 原数据接口
$api.createRequest(`/app/job/${jobId}`, {}, 'GET', true).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(companyId);
getCompetivetuveness(jobId);
if (latitude && longitude) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName), // X 轴调整,负数向左
borderRadius: 5,
},
width: 34,
},
];
initMapCovers(latitude, longitude, companyName);
}
});
});
}
}
function getCompanyIsAJobs(companyId) {
$api.createRequest(`/app/company/count/${companyId}`).then((resData) => {
companyCount.value = resData.data;
});
function initMapCovers(latitude, longitude, companyName) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName),
borderRadius: 5,
},
width: 34,
},
];
}
function getCompanyIsAJobs(...args) {
if (dataType.value === 2) {
// 第三方数据获取公司职位数量
const [gsID, gsmc, zphID] = args;
$api.createRequest(`/app/internal/jobThirdPart?gsID=${gsID}&gsmc=${gsmc}&zphID=${zphID}`).then((resData) => {
companyCount.value = resData.total;
});
} else {
// 原数据获取公司职位数量
const [companyId] = args;
$api.createRequest(`/app/company/count/${companyId}`).then((resData) => {
companyCount.value = resData.data;
});
}
}
function getTextWidth(text, size = 12) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${12}px Arial`;
return -(context.measureText(text).width / 2) - 20; // 计算文字中心点
return -(context.measureText(text).width / 2) - 20;
}
function getCompetivetuveness(jobId) {
$api.createRequest(`/app/job/competitiveness/${jobId}`, {}, 'GET').then((resData) => {
raderData.value = resData.data;
currentStep.value = resData.data.matchScore * 0.04;
});
if (dataType.value !== 2) {
$api.createRequest(`/app/job/competitiveness/${jobId}`, {}, 'GET').then((resData) => {
raderData.value = resData.data;
currentStep.value = resData.data.matchScore * 0.04;
});
}
}
// 申请岗位
function jobApply() {
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isApply) {
const jobUrl = jobInfo.value.jobUrl;
return window.open(jobUrl);
if (dataType.value === 2) {
// 第三方数据申请逻辑
const params = {
jobid: jobInfo.value.id,
jobname: jobInfo.value.gwmc,
};
if (jobInfo.value.isApply) {
$api.msg('已经投递过该岗位了~');
return;
} else {
$api.createRequest(`/app/internal/sendResume`, params, 'POST').then((resData) => {
$api.msg('投递成功');
getDetail(jobIdRef.value);
});
}
} else {
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
getDetail(jobId);
$api.msg('申请成功');
// 原数据申请逻辑
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isApply) {
const jobUrl = jobInfo.value.jobUrl;
return window.open(jobUrl);
});
} else {
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
getDetail(jobId);
$api.msg('申请成功');
const jobUrl = jobInfo.value.jobUrl;
return window.open(jobUrl);
});
}
}
}
// 取消/收藏岗位
function jobCollection() {
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'DELETE').then((resData) => {
getDetail(jobId);
$api.msg('取消收藏成功');
});
if (dataType.value === 2) {
// 第三方数据收藏逻辑
const id = jobInfo.value.id;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${id}/2`, {}, 'DELETE').then((resData) => {
getDetail(jobIdRef.value);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/job/collection/${id}/2`, {}, 'POST').then((resData) => {
getDetail(jobIdRef.value);
$api.msg('收藏成功');
});
}
} else {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'POST').then((resData) => {
getDetail(jobId);
$api.msg('收藏成功');
});
// 原数据收藏逻辑
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'DELETE').then((resData) => {
getDetail(jobId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'POST').then((resData) => {
getDetail(jobId);
$api.msg('收藏成功');
});
}
}
}
// 处理公司详情跳转
function handleCompanyDetail() {
if (dataType.value === 2) {
navTo(
`/packageA/pages/UnitDetails/UnitDetails?companyId=${jobInfo.value.gsID}&companyName=${jobInfo.value.gsmc}&zphId=${jobInfo.value.zphID}&dataType=2`
);
} else {
navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${jobInfo.value.company.companyId}`);
}
}
@@ -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,16 +92,34 @@ 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;
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
getList('refresh');
$api.msg('取消预约成功');
const fairId = item.zphID;
uni.showModal({
title: '提示',
content: '确定要取消预约吗?',
showCancel: true,
success: ({ confirm, cancel }) => {
if (confirm) {
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
getList('refresh');
$api.msg('取消预约成功');
});
}
},
});
}
@@ -108,7 +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,265 +1,278 @@
<template>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top btn-feel">
<view class="info">
<view class="avatar">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="info-right">
<view class="name">{{ userInfo.name || "编辑用户名" }}</view>
<view class="des">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">|</text>
<text class="mar_ri10">{{ userInfo.age }}</text>
<text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<!-- <text class="mar_ri10">|</text>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top btn-feel">
<view class="info">
<view class="avatar">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="info-right">
<view class="name">{{ userInfo.name || '编辑用户名' }}</view>
<view class="des">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">|</text>
<text class="mar_ri10">{{ userInfo.age || '-' }}</text>
<text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<!-- <text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="affiliation" :value="userInfo.politicalAffiliation"></dict-Label> -->
</view>
<view class="phone">
<image class="call-icon" src="@/static/icon/call.png" />
<view>{{ userInfo.phone }}</view>
</view>
</view>
</view>
<view class="info-bottom">
<view>到岗2025-11-02</view>
<view>地点青岛市-<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label></view>
</view>
<view class="des-card" style="margin-top: 24rpx">
<view class="fl_box fl_justbet">
<view style="white-space:nowrap">求职意向岗位</view>
<view class="line_1" style="padding-left:40rpx" >{{ userInfo.jobIntention || userInfo.jobTitle?.join(',') || '-' }}</view>
</view>
<view class="fl_box fl_justbet">
<view>毕业学校</view>
<view>{{ userInfo.graduationSchool || "-" }}</view>
</view>
<view class="fl_box fl_justbet">
</view>
<view class="phone">
<image class="call-icon" src="@/static/icon/call.png" />
<view>{{ userInfo.phone || '未知手机号' }}</view>
</view>
</view>
</view>
<view class="info-bottom">
<!-- <view>到岗2025-11-02</view> -->
<view></view>
<view>
地点青岛市-
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
</view>
<view class="des-card" style="margin-top: 24rpx">
<view class="fl_box fl_justbet">
<view style="white-space: nowrap">求职意向岗位</view>
<view class="line_1" style="padding-left: 40rpx">
{{ userInfo.jobIntention || userInfo.jobTitle?.join(',') || '--' }}
</view>
</view>
<view class="fl_box fl_justbet">
<view>毕业学校</view>
<view>{{ userInfo.graduationSchool || '--' }}</view>
</view>
<!-- <view class="fl_box fl_justbet">
<view>当前状态</view>
<view>在职 看工作机会</view>
</view>
</view>
</view>
</view> -->
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">个人技能</view>
</view>
<view class="skill-box">
<view class="skill-item" v-for="item in userInfo?.skillList" :key="item.id">{{ item.skill }}</view>
</view>
<view v-if="!userInfo?.skillList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无个人技能</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">关键经历</view>
</view>
<view class="exp-box">
<view class="exp-item" v-for="(item, index) in userInfo?.workExp" :key="item.id">{{ index + 1 + "." }}{{ item.duty }}</view>
</view>
<view v-if="!userInfo?.workExp?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无关键经历</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">荣誉及证书情况</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">个人技能</view>
</view>
<view class="skill-box">
<view class="skill-item" v-for="item in userInfo?.skillList" :key="item.id">{{ item.skill }}</view>
</view>
<view v-if="!userInfo?.skillList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无个人技能</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">关键经历</view>
</view>
<view class="exp-box">
<view class="exp-item" v-for="(item, index) in userInfo?.workExp" :key="item.id">
{{ index + 1 + '.' }}{{ item.duty }}
</view>
</view>
<view v-if="!userInfo?.workExp?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无关键经历</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">荣誉及证书情况</view>
</view>
<ul class="certificate-box">
<li class="certificate-item" v-for="(item, index) in userInfo?.certificateList" :key="item.id">{{ item.name }}</li>
</ul>
<view v-if="!userInfo?.certificateList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无荣誉证书</view>
<ul class="certificate-box">
<li class="certificate-item" v-for="(item, index) in userInfo?.certificateList" :key="item.id">
{{ item.name }}
</li>
</ul>
<view v-if="!userInfo?.certificateList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无荣誉证书</view>
</view>
</view>
</view>
</view>
</view>
</AppLayout>
</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();
</script>
<style lang="scss" scoped>
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
.card-top {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.9));
}
.card {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: #fff;
padding: 24rpx;
margin-top: 24rpx;
.title {
position: relative;
.bg {
width: 108rpx;
height: 16rpx;
position: absolute;
left: 0;
bottom: 0;
}
.text {
margin-left: 20rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
}
}
.skill-box {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 24rpx;
.skill-item {
padding: 8rpx 20rpx;
font-size: 24rpx;
color: #333;
background: #e7f1ff;
border-radius: 8rpx;
}
}
.exp-box {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
margin-top: 24rpx;
.exp-item {
font-size: 26rpx;
color: #333;
}
}
.certificate-box {
margin-top: 24rpx;
padding-inline-start: 40rpx !important;
.certificate-item {
font-size: 26rpx;
color: #333;
margin-bottom: 24rpx;
}
}
image {
width: 100%;
height: 100%;
}
.mys-container {
padding: 28rpx;
.info {
display: flex;
align-items: center;
padding: 24rpx;
.avatar {
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;
image{
border-radius: 50%;
}
}
.info-right {
height: 160rpx;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.name {
font-size: 40rpx;
font-weight: bold;
}
.des {
font-size: 26rpx;
color: #999999;
}
.phone {
display: flex;
align-items: center;
font-size: 26rpx;
color: #999999;
.call-icon {
width: 24rpx;
height: 24rpx;
margin-right: 5rpx;
}
}
}
}
.info-bottom {
background: linear-gradient(to right, #91b6ff, #87afff, #b7adff);
border-radius: 0 0 8rpx 8rpx;
padding: 12rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
color: #333;
}
.des-card {
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
.card-top {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.9));
}
.card {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: #fff;
padding: 24rpx;
font-size: 26rpx;
color: #666;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.empty-box {
padding: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.img {
width: 100%;
margin-top: 24rpx;
.title {
position: relative;
.bg {
width: 108rpx;
height: 16rpx;
position: absolute;
left: 0;
bottom: 0;
}
.text {
margin-left: 20rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
}
.content {
margin-top: 24rpx;
font-size: 30rpx;
color: #666;
}
.skill-box {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 24rpx;
.skill-item {
padding: 8rpx 20rpx;
font-size: 24rpx;
color: #333;
background: #e7f1ff;
border-radius: 8rpx;
}
}
.exp-box {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
margin-top: 24rpx;
.exp-item {
font-size: 26rpx;
color: #333;
}
}
.certificate-box {
margin-top: 24rpx;
padding-inline-start: 40rpx !important;
.certificate-item {
font-size: 26rpx;
color: #333;
margin-bottom: 24rpx;
}
}
image {
width: 100%;
height: 100%;
}
.mys-container {
padding: 28rpx;
.info {
display: flex;
align-items: center;
padding: 24rpx;
.avatar {
background: #e8e8e8;
border-radius: 50%;
overflow: hidden;
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;
image {
border-radius: 50%;
}
}
.info-right {
height: 160rpx;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.name {
font-size: 40rpx;
font-weight: bold;
}
.des {
font-size: 26rpx;
color: #999999;
}
.phone {
display: flex;
align-items: center;
font-size: 26rpx;
color: #999999;
.call-icon {
width: 24rpx;
height: 24rpx;
margin-right: 5rpx;
}
}
}
}
.info-bottom {
background: linear-gradient(to right, #91b6ff, #87afff, #b7adff);
border-radius: 0 0 8rpx 8rpx;
padding: 12rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
color: #333;
}
.des-card {
padding: 24rpx;
font-size: 26rpx;
color: #666;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.empty-box {
padding: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.img {
width: 100%;
}
.content {
margin-top: 24rpx;
font-size: 30rpx;
color: #666;
}
}
}
}
</style>

View File

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

View File

@@ -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="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.name }}</view>
<view class="card-row">
<text class="">{{ item.location }}</text>
<text class="">
<convert-distance
<view class="cards">
<transition-group name="stagger" tag="view" :css="true">
<view
class="card press-button"
v-for="(item, index) in fairList"
:key="item.zphID"
:style="{ '--i': index }"
@click="
navTo(
'/packageA/pages/exhibitors/exhibitors?jobFairId=' +
item.zphID +
'&jobFairName=' +
item.zphmc
)
"
>
<view class="card-title">{{ item.zphmc }}</view>
<view class="card-row">
<text class="">{{ item.jbf }}</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.startTime).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.startTime).date }}</view>
></convert-distance> -->
</text>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(item.startTime, item.endTime).statusText }}
<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="center-dateDay">
{{ getHoursBetween(item.startTime, item.endTime) }}小时
<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="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>
<view class="recommend-card-line"></view>
<view class="card-footer">内容简介{{ item.zphjj }}</view>
</view>
<view class="recommend-card-line"></view>
<view class="card-footer">内容简介{{ item.description }}</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: 0x87e2ec,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: -Math.PI / 2, // 12点方向
radius: 60,
tailRotation: Math.PI / 2, // 拖尾向下
},
{
name: "建筑师",
bgColor: 0xffebeb,
fontColor: 0xf86e6e,
size: 11.5,
opacity: 1,
angle: -Math.PI / 4, // 1点方向
radius: 115,
tailRotation: (3 * Math.PI) / 4, // 拖尾向左下
},
{
name: "律师",
bgColor: 0x21ea85,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: -Math.PI / 10, // 2点方向
radius: 130,
tailRotation: (3 * Math.PI) / 4, // 拖尾向左下
},
{
name: "记者",
bgColor: 0xebf3ff,
fontColor: 0x1d71ef,
size: 12,
opacity: 1,
angle: Math.PI / 120, // 3点方向
radius: 130,
tailRotation: Math.PI, // 拖尾向左
},
{
name: "程序员",
bgColor: 0xff9d57,
fontColor: 0xffffff,
size: 14.5,
opacity: 0.6,
angle: Math.PI / 9, // 4点方向
radius: 120,
tailRotation: (5 * Math.PI) / 4, // 拖尾向左上
},
{
name: "摄影师",
bgColor: 0xd8e5fe,
fontColor: 0x1d71ef,
size: 11,
opacity: 1,
angle: Math.PI / 3.2, // 5点方向
radius: 75,
tailRotation: (3 * Math.PI) / 2, // 拖尾向上
},
{
name: "设计师",
bgColor: 0xff9400,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: (2 * Math.PI) / 3, // 7点方向
radius: 92,
tailRotation: (7 * Math.PI) / 4, // 拖尾向右上
},
{
name: "心理咨询师",
bgColor: 0xebf3ff,
fontColor: 0x1d71ef,
size: 10.5,
opacity: 1,
angle: (5.4 * Math.PI) / 6, // 8点方向
radius: 110,
tailRotation: 0, // 拖尾向右
},
{
name: "护士",
bgColor: 0xff6969,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: (6.3 * Math.PI) / 6, // 10点方向
radius: 110,
tailRotation: Math.PI / 4, // 拖尾向右下
},
{
name: "会计",
bgColor: 0xfce9c9,
fontColor: 0xfbc55f,
size: 13,
opacity: 1,
angle: (7.2 * Math.PI) / 6, // 11点方向
radius: 115,
tailRotation: Math.PI / 4, // 拖尾向右下
},
{ name: '医生', bgColor: 0x0069fe, fontColor: 0xffffff, size: 17, opacity: 1.0, angle: 0, radius: 0 },
{
name: '工程师',
bgColor: 0x87e2ec,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: -Math.PI / 2,
radius: 68,
tailRotation: Math.PI / 2,
},
{
name: '建筑师',
bgColor: 0xffebeb,
tailColor: 0xffe1e1,
fontColor: 0xff6969,
size: 11.5,
opacity: 1,
angle: -Math.PI / 4.2,
radius: 125,
tailRotation: (3 * Math.PI) / 4,
},
{
name: '律师',
bgColor: 0x21ea85,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: -Math.PI / 10,
radius: 130,
tailRotation: (3 * Math.PI) / 4,
},
{
name: '记者',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 12,
opacity: 1,
angle: Math.PI / 120,
radius: 130,
tailRotation: (3 * Math.PI) / 3.4,
},
{
name: '程序员',
bgColor: 0xffd4b6,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: Math.PI / 7,
radius: 120,
tailRotation: (5 * Math.PI) / 4,
},
{
name: '摄影师',
bgColor: 0xd8e5fe,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 11,
opacity: 1,
angle: Math.PI / 3,
radius: 79,
tailRotation: (3 * Math.PI) / 2,
},
{
name: '设计师',
bgColor: 0xff9400,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: (2 * Math.PI) / 3,
radius: 92,
tailRotation: (7 * Math.PI) / 4,
},
{
name: '心理咨询师',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 10.5,
opacity: 1,
angle: (5.4 * Math.PI) / 6,
radius: 110,
tailRotation:(3 * Math.PI) /1.78,
},
{
name: '护士',
bgColor: 0xff6969,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: (6.3 * Math.PI) / 5.9,
radius: 110,
tailRotation: Math.PI / 4,
},
{
name: '会计',
bgColor: 0xfce9c9,
fontColor: 0xfbc55f,
size: 13,
opacity: 1,
angle: (7.2 * Math.PI) / 5.9,
radius: 120,
tailRotation: Math.PI / 4,
},
];
onMounted(async () => {
if (appRef.value) return;
// 初始化 PIXI 应用
const canvas = document.getElementById("matchCanvas");
const sw = canvas.clientWidth;
const sh = canvas.clientHeight;
await nextTick();
setTimeout(() => {
initPixi();
}, 100);
window.addEventListener('resize', handleResize);
});
const app = new PIXI.Application({
backgroundAlpha: 0,
antialias: true,
autoDensity: true,
width: sw,
height: sh,
backgroundColor: 0xf5f7fa,
});
appRef.value = app;
canvas.appendChild(app.view);
// 标签容器(管理所有标签)
const tagsContainer = new PIXI.Container();
app.stage.addChild(tagsContainer);
// 存储已放置标签的信息(位置、尺寸、浮动动画参数)
const placedTags = [];
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 originalY = tag.y;
let floatOffset = Math.random() * Math.PI * 2;
let floatSpeed = 0.01 + Math.random() * 0.02;
let floatRange = 2 + Math.random() * 2;
// 为标签添加彗星拖尾效果
if (radius > 0) {
// 中心标签不需要拖尾
const tail = createCometTail(tagData.bgColor, tailRotation, tag);
// 修正:使用 addChildAt 将拖尾添加到最底层
tag.addChildAt(tail, 0);
// 为拖尾添加单独的动画
app.ticker.add(() => {
if (tail.updateTail) {
tail.updateTail();
}
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (app) {
app.destroy(true, { children: true, texture: true, baseTexture: true });
app = null;
}
});
// 使用PIXI ticker进行动画
app.ticker.add(() => {
floatOffset += floatSpeed;
tag.y = originalY + Math.sin(floatOffset) * floatRange;
const getContainerDOM = () => {
const refVal = pixiContainerRef.value;
if (!refVal) return document.getElementById('pixi-box');
if (refVal.$el) return refVal.$el;
return refVal;
};
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const initPixi = () => {
const container = getContainerDOM();
if (!container) return;
const width = container.clientWidth || 300;
const height = container.clientHeight || 300;
if (app) return;
app = new PIXI.Application({
width: width,
height: height,
backgroundAlpha: 0,
backgroundColor: 0xf5f7fa,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
});
app.view.style.touchAction = 'auto';
container.appendChild(app.view);
tagsContainer = new PIXI.Container();
app.stage.addChild(tagsContainer);
renderScene(width, height);
};
const renderScene = (sw, sh) => {
tagsContainer.removeChildren();
activeTagInstances = [];
const baseSize = 375;
const scaleFactor = (Math.min(sw, sh) / baseSize) * 0.9;
mockTags.forEach((data, index) => {
const scaledRadius = data.radius * (scaleFactor < 1 ? 1 : scaleFactor * 0.8);
let x = sw / 2 + scaledRadius * Math.cos(data.angle);
let y = sh / 2 + scaledRadius * Math.sin(data.angle);
const tag = createTag(data, index);
tagsContainer.addChild(tag);
const safeW = tag.width / 2 + 10;
const safeH = tag.height / 2 + 10;
// 强制修正 x 和 y使其不超出屏幕
x = clamp(x, safeW, sw - safeW);
y = clamp(y, safeH, sh - safeH);
tag.x = x;
tag.y = y;
// 4. 保存元数据
tag.userData = {
originalX: x,
originalY: y,
angle: data.angle,
radius: scaledRadius,
floatOffset: Math.random() * Math.PI * 2,
floatSpeed: 0.01 + Math.random() * 0.02,
floatRange: 2 + Math.random() * 2,
safeH: safeH,
};
if (data.radius > 0) {
const tail = createCometTail( data.tailColor || data.bgColor, data.tailRotation, tag.width);
tag.addChildAt(tail, 0);
tag.updateTail = () => tail.updateAnim();
}
activeTagInstances.push(tag);
});
tagsContainer.addChild(tag);
placedTags.push({ ...tagData, bounds: tag.getBounds() });
}
});
// 动画循环
app.ticker.add(() => {
const screenH = app.screen.height;
// 销毁时清理资源
onUnmounted(() => {
if (appRef.value) {
appRef.value.destroy(true, true);
appRef.value = null;
}
});
activeTagInstances.forEach((tag) => {
const meta = tag.userData;
if (meta) {
// 计算新的浮动位置
meta.floatOffset += meta.floatSpeed;
let nextY = meta.originalY + Math.sin(meta.floatOffset) * meta.floatRange;
// 创建彗星拖尾效果
function createCometTail(bgColor, tailRotation, tag) {
const tailGroup = new PIXI.Container();
// 再次进行边界检查
if (nextY < meta.safeH) nextY = meta.safeH;
if (nextY > screenH - meta.safeH) nextY = screenH - meta.safeH;
// 拖尾直接放在标签中心位置
tailGroup.x = 0;
tailGroup.y = 0;
tag.y = nextY;
const tail = new PIXI.Graphics();
tailGroup.addChild(tail);
if (tag.updateTail) tag.updateTail();
}
});
});
};
// 拖尾参数
const baseLength = 45; // 基础长度
const startWidth = tag.width * 0.9; // 起始宽度(长边)
const endWidth = 35; // 末端宽度(短边)
const createTag = (tagData, index) => {
const tagGroup = new PIXI.Container();
tagGroup.eventMode = 'static';
tagGroup.cursor = 'pointer';
// 拖尾动画参数
let breathPhase = Math.random() * Math.PI * 2;
const breathSpeed = 0.04;
tagGroup.on('pointertap', () => emit('tag-click', tagData));
// 更新拖尾的呼吸动画
tailGroup.updateTail = () => {
breathPhase += breathSpeed;
const breathScale = 0.85 + 0.15 * Math.sin(breathPhase);
tail.clear();
const text = new PIXI.Text(tagData.name, {
fontFamily: ['PingFang SC', 'Microsoft YaHei', 'Arial'],
fontSize: tagData.size,
fill: tagData.fontColor,
padding: 4,
resolution: 2,
});
text.anchor.set(0.5);
// 绘制梯形拖尾
const currentLength = baseLength * breathScale;
const paddingH = 26;
const paddingV = 10;
let bgWidth = text.width + paddingH;
let bgHeight = text.height + paddingV;
// 计算拖尾的四个顶点
// 长边在标签中心,水平方向(与标签长边平行)
const startLeft = { x: -startWidth / 2, y: 0 };
const startRight = { x: startWidth / 2, y: 0 };
if (index === 0) bgWidth = Math.max(bgWidth, tagData.size * 4.5);
// 短边在拖尾方向,保持水平
const endCenter = {
x: Math.cos(tailRotation) * currentLength,
y: Math.sin(tailRotation) * currentLength,
const bg = new PIXI.Graphics();
bg.beginFill(tagData.bgColor, tagData.opacity ?? 1);
bg.drawRoundedRect(-bgWidth / 2, -bgHeight / 2, bgWidth, bgHeight, bgHeight / 2);
bg.endFill();
tagGroup.addChild(bg);
tagGroup.addChild(text);
return tagGroup;
};
const createCometTail = (bgColor, tailRotation, parentWidth) => {
const tailGroup = new PIXI.Container();
const graphics = new PIXI.Graphics();
tailGroup.addChild(graphics);
const baseLength = 45;
const startWidth = parentWidth * 0.6;
const endWidth = 20;
let breathPhase = Math.random() * Math.PI * 2;
const breathSpeed = 0.04;
tailGroup.updateAnim = () => {
breathPhase += breathSpeed;
const breathScale = 0.85 + 0.15 * Math.sin(breathPhase);
graphics.clear();
const currentLength = baseLength * breathScale;
const cos = Math.cos(tailRotation);
const sin = Math.sin(tailRotation);
const perpX = -sin;
const perpY = cos;
const p1 = { x: perpX * (startWidth / 2), y: perpY * (startWidth / 2) };
const p2 = { x: -perpX * (startWidth / 2), y: -perpY * (startWidth / 2) };
const endCX = cos * currentLength;
const endCY = sin * currentLength;
const p3 = { x: endCX - perpX * (endWidth / 2), y: endCY - perpY * (endWidth / 2) };
const p4 = { x: endCX + perpX * (endWidth / 2), y: endCY + perpY * (endWidth / 2) };
const segments = 8;
for (let i = 0; i < segments; i++) {
const t1 = i / segments;
const t2 = (i + 1) / segments;
const alpha = 0.4 * (1 - t1);
const sp1 = { x: p1.x + (p4.x - p1.x) * t1, y: p1.y + (p4.y - p1.y) * t1 };
const sp2 = { x: p2.x + (p3.x - p2.x) * t1, y: p2.y + (p3.y - p2.y) * t1 };
const ep1 = { x: p1.x + (p4.x - p1.x) * t2, y: p1.y + (p4.y - p1.y) * t2 };
const ep2 = { x: p2.x + (p3.x - p2.x) * t2, y: p2.y + (p3.y - p2.y) * t2 };
graphics.beginFill(bgColor, alpha);
graphics.moveTo(sp1.x, sp1.y);
graphics.lineTo(sp2.x, sp2.y);
graphics.lineTo(ep2.x, ep2.y);
graphics.lineTo(ep1.x, ep1.y);
graphics.endFill();
}
};
tailGroup.updateAnim();
return tailGroup;
};
const endLeft = {
x: endCenter.x - endWidth / 2,
y: endCenter.y,
};
const handleResize = () => {
const container = getContainerDOM();
if (!app || !container) return;
const endRight = {
x: endCenter.x + endWidth / 2,
y: endCenter.y,
};
const w = container.clientWidth || 300;
const h = container.clientHeight || 300;
// 使用分段绘制实现渐变
const segments = 6;
for (let i = 0; i < segments; i++) {
const progress = i / segments;
const nextProgress = (i + 1) / segments;
app.renderer.resize(w, h);
// 计算分段的位置 - 保持长边水平
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,
};
activeTagInstances.forEach((tag) => {
const meta = tag.userData;
if (!meta) return;
// 透明度从0.4渐变到0
const segmentAlpha = 0.4 * (1 - progress);
let newX = w / 2 + meta.radius * Math.cos(meta.angle);
let newY = h / 2 + meta.radius * Math.sin(meta.angle);
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();
}
};
const safeW = tag.width / 2 + 10;
const safeH = tag.height / 2 + 10;
// 初始绘制
tailGroup.updateTail();
meta.originalX = clamp(newX, safeW, w - safeW);
meta.originalY = clamp(newY, safeH, h - safeH);
meta.safeH = safeH; // 更新安全高度
return tailGroup;
}
// 创建单个标签(背景+文本)
function createTag(tagData, x, y, placedTags, app, index) {
const tagGroup = new PIXI.Container();
tagGroup.x = x;
tagGroup.y = y;
// 先创建文本以测量宽度
const text = new PIXI.Text(tagData.name, {
fontFamily: "Arial",
fontSize: tagData.size,
fill: tagData.fontColor,
});
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;
let width = textWidth + padding * 2;
if (index == 0) width = tagData.size * 4.5 + 10;
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.endFill();
tagGroup.addChild(bg);
// 添加文本
tagGroup.addChild(text);
return tagGroup;
}
tag.x = meta.originalX;
});
};
</script>
<style scoped>
.container {
width: 100%;
}
.match-canvas {
width: 100%;
height: 350rpx; /* 可根据需求调整高度 */
width: 100%;
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

File diff suppressed because it is too large Load Diff

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(
},
}
);
watch(()=>finished.value, (newVal) => {
if (newVal) {
// 确保瀑布流组件知道数据已加载完成
loadmoreRef.value?.change('noMore')
}else{
loadmoreRef.value?.change('more')
}
})
function imageloaded() {
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;
@@ -447,4 +451,4 @@ defineExpose({ loadData, handleFilterConfirm });
height: 26rpx;
.active
transform: rotate(180deg)
</style>
</style>

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

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