Compare commits

71 Commits

Author SHA1 Message Date
Apcallover
49af03f4bb flat: 暂存 2025-12-19 17:43:23 +08:00
4ae11e31f4 地图优化 2025-12-19 16:45:21 +08:00
bca0d997c6 style 2025-12-19 16:33:15 +08:00
Apcallover
b43eb98a1c flat: ok 2025-12-19 16:13:09 +08:00
Apcallover
44c297aac2 flat: ok 2025-12-19 16:10:39 +08:00
f64c9e5dae bug fix 2025-12-19 16:05:16 +08:00
975835baa5 修改上传html 加入文件校验 2025-12-19 15:48:24 +08:00
1ac524e1f1 fix 2025-12-19 14:47:08 +08:00
7e8bef0cb9 feat : 一体机定位直到成功,其他环境循环2分钟定位, 上传加入文件校验 2025-12-19 14:46:37 +08:00
Apcallover
ce597b182d flat 文件校验 2025-12-19 12:06:35 +08:00
Apcallover
fdd5577c85 flat: 合并代码 2025-12-19 11:35:00 +08:00
Apcallover
4dfc7bdfd8 flat:代码合并 2025-12-19 10:37:41 +08:00
Apcallover
4befbb05cc flat: 添加语音识别sdk+ 文件检测 2025-12-19 10:25:10 +08:00
74dc6debcd bug fix 2025-12-19 09:59:04 +08:00
534dfd8126 style 2025-12-19 09:39:10 +08:00
4c29882f36 合并 2025-12-18 18:55:00 +08:00
4cf75922da Merge branch 'main-ALL-IN-ONE' of http://124.243.245.42:3000/sdz/qingdao-employment-service into main-ALL-IN-ONE 2025-12-18 18:54:23 +08:00
bb67e9ba12 feat : 优化文件上传 优化一体机体验 2025-12-18 18:53:16 +08:00
Apcallover
7479176eff flat:暂存 2025-12-18 11:16:47 +08:00
Apcallover
3fe0a1b9e1 flat: 兼容写法 2025-12-18 11:16:20 +08:00
284d696b25 fix 2025-12-18 10:47:39 +08:00
85a24d3346 fix : 统一文件类型 2025-12-18 10:39:37 +08:00
854bc9c197 fix : 图片类型判断, 一体机modal样式 2025-12-18 10:31:54 +08:00
Apcallover
606b6104bb flat: bug修复2 2025-12-17 21:12:26 +08:00
Apcallover
a10fd29440 flat: bug修复 2025-12-17 19:48:45 +08:00
Apcallover
ab3201b015 flat:合并、扫码上传文件等 2025-12-17 18:31:17 +08:00
Apcallover
8420d265e1 flat: 注入全局弹窗 2025-12-17 18:27:59 +08:00
a30a302e90 新增一体机附件上传 2025-12-17 18:10:06 +08:00
Apcallover
3d826aec86 flat: 一体机兼容,添加全局事件扫码回调等 2025-12-17 15:16:48 +08:00
8b9f7890af style 扫码扫脸登录 2025-12-17 15:08:58 +08:00
32e147e601 隐藏一体机下部分模块 修改扫码登录页 2025-12-17 13:42:05 +08:00
8ccdcbf93b style : 一体机下隐藏部分模块 2025-12-17 11:32:13 +08:00
01d817f99c delete : 去除无用文件 2025-12-17 10:25:27 +08:00
137542a92f Merge branch 'main-ALL-IN-ONE' of http://124.243.245.42:3000/sdz/qingdao-employment-service into main-ALL-IN-ONE 2025-12-17 10:15:15 +08:00
cd9c3acfc0 首页-人工只能专区,招聘会-VR,我的-AI面试, 一体机模式下提示暂未开放 2025-12-17 10:15:13 +08:00
Apcallover
80213b18e9 flat: 微调 2025-12-16 20:24:03 +08:00
4a03b4d4cb feat : 新增一体机支付宝扫码登录页 2025-12-16 16:51:56 +08:00
Apcallover
77dfab84f1 flat: 暂存,新增一体机sdk,和一体机环境 2025-12-16 15:36:42 +08:00
33f1d0a8ab 修改宽屏判断阈值 2025-12-16 10:34:40 +08:00
92d87347d0 Merge branch 'main-ALL-IN-ONE' of http://124.243.245.42:3000/sdz/qingdao-employment-service into main-ALL-IN-ONE 2025-12-16 09:31:56 +08:00
add2cfeac4 静态资源压缩 2025-12-16 09:31:54 +08:00
Apcallover
8e316cf2a1 flat: 2025-12-16 09:30:45 +08:00
Apcallover
d6bf2648de Merge branch 'main-ALL-IN-ONE' of http://124.243.245.42:3000/sdz/qingdao-employment-service into main-ALL-IN-ONE 2025-12-16 09:25:54 +08:00
0a2d523a40 fix 2025-12-16 09:25:02 +08:00
8a138bed99 fix 文案 2025-12-16 09:23:18 +08:00
Apcallover
d9f0356491 flat:修改 2025-12-16 09:22:57 +08:00
ff941848b1 fix 2025-12-16 09:20:42 +08:00
e19673b99e delete 2025-12-15 17:58:12 +08:00
7260043942 style 2025-12-15 17:42:53 +08:00
c351bfa54f style 2025-12-15 17:39:08 +08:00
dd81468f47 feat : 对接简历匹配职位接口 修改样式 2025-12-15 17:37:10 +08:00
79207ee66a fix 2025-12-15 16:09:22 +08:00
215ec11fe7 Merge branch 'main-ALL-IN-ONE' of http://124.243.245.42:3000/sdz/qingdao-employment-service into main-ALL-IN-ONE 2025-12-15 16:08:46 +08:00
74f13d26a8 fix 下拉加载 2025-12-15 16:04:08 +08:00
Apcallover
e6cb12659d flat: 修改 2025-12-15 15:56:47 +08:00
4571744f2e fix 2025-12-15 15:46:06 +08:00
3ce8e65abc fix 2025-12-15 15:43:12 +08:00
159efd166a fix 适配 2025-12-15 15:40:16 +08:00
817fa27136 fix 聊天页tabbar显示隐藏 2025-12-15 15:23:30 +08:00
cb9f626788 fix 1080 2025-12-15 15:20:44 +08:00
330eec226f feat: 一体机适配 2025-12-15 15:11:11 +08:00
4983d9cbc7 暂时提交 2025-12-15 10:54:30 +08:00
fa1132ebe9 临时style适配 2025-12-12 16:35:02 +08:00
30764cec57 fix 传参 2025-12-12 10:50:45 +08:00
cb1052346e feat : 足迹日期选择新增有记录的日期项; request改为proxy + header url 2025-12-11 14:04:17 +08:00
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
167 changed files with 11089 additions and 3814 deletions

147
App.vue
View File

@@ -1,39 +1,76 @@
<script setup>
import { reactive, inject, onMounted } from 'vue';
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
import { IncreaseRevie } from '@/common/all-in-one-listen.js';
import useUserStore from './stores/useUserStore';
import usePageAnimation from './hook/usePageAnimation';
import useDictStore from './stores/useDictStore';
const { $api, navTo, appendScriptTagElement, aes_Decrypt, sm2_Decrypt } = inject('globalFunction');
import { GlobalInactivityManager } from '@/utils/GlobalInactivityManager';
const { $api, navTo, appendScriptTagElement, aes_Decrypt, sm2_Decrypt, safeReLaunch } = inject('globalFunction');
import config from '@/config.js';
import baseDB from '@/utils/db.js';
import { $confirm } from '@/utils/modal.js';
import useLocationStore from '@/stores/useLocationStore';
usePageAnimation();
const appword = 'aKd20dbGdFvmuwrt'; // 固定值
let uQRListen = null;
let inactivityManager = null;
let inactivityModalTimer = null;
onLaunch((options) => {
// uni.hideTabBar();
useDictStore().getDictData();
try {
if (lightAppJssdk.user) {
console.warn('爱山东环境');
getUserInfo();
} catch {
console.log('不是爱山东平台,使用测试登陆');
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
} else {
uni.redirectTo({
url: '/pages/login/login',
useUserStore().changMiniProgramAppStatus(false);
useUserStore().changMachineEnv(false);
useLocationStore().getLocationLoop()//循环获取定位
return;
}
if (isY9MachineType()) {
console.warn('求职一体机环境');
baseDB.resetAndReinit(); // 清空indexdb
useUserStore().logOutApp();
useUserStore().changMiniProgramAppStatus(true);
useUserStore().changMachineEnv(true);
(function loop() {
console.log('📍一体机尝试获取定位')
useLocationStore().getLocation().then(({longitude,latitude})=>{
console.log(`✅一体机获取定位成功:lng:${longitude},lat${latitude}`)
})
.catch(err=>{
console.log('❌一体机获取定位失败,30s后尝试重新获取')
setTimeout(() => {
loop()
}, 3000);
})
})()
uQRListen = new IncreaseRevie();
inactivityManager = new GlobalInactivityManager(handleInactivity, 60 * 1000);
inactivityManager.start();
return;
}
// 正式上线去除此方法
console.warn('浏览器环境');
useLocationStore().getLocationLoop()//循环获取定位
useUserStore().changMiniProgramAppStatus(true);
useUserStore().changMachineEnv(false);
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || '';
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
}
} else {
safeReLaunch('/pages/login/login');
}
});
onMounted(() => {});
onShow(() => {
console.log('App Show');
});
@@ -42,6 +79,68 @@ onHide(() => {
console.log('App Hide');
});
function handleInactivity() {
console.log('【全局】60秒无操作执行安全逻辑');
if (inactivityModalTimer) {
clearTimeout(inactivityModalTimer);
inactivityModalTimer = null;
}
if (useUserStore().hasLogin) {
// 1. 正常弹出确认框
$confirm({
title: '会话即将过期',
content: '长时间无操作,是否继续使用?',
success: (res) => {
if (inactivityModalTimer) {
clearTimeout(inactivityModalTimer);
inactivityModalTimer = null;
}
if (res.confirm) {
inactivityManager?.resume();
} else {
performLogout();
}
},
fail: () => {
if (inactivityModalTimer) {
clearTimeout(inactivityModalTimer);
inactivityModalTimer = null;
}
performLogout();
},
});
// 2. 启动 10 秒倒计时
inactivityModalTimer = setTimeout(() => {
inactivityModalTimer = null;
console.log('【自动登出】10秒无响应强制清理状态');
// 【关键改进】:通知全局组件强制关闭弹窗,防止用户点击陈旧弹窗
uni.$emit('hide-global-popup');
performLogout();
}, 10000);
} else {
inactivityManager?.resume();
}
}
function performLogout() {
uni.clearStorageSync();
baseDB.resetAndReinit();
useUserStore().logOutApp();
inactivityManager?.resume(); // 恢复监听
}
// 一体机环境判断
function isY9MachineType() {
const ua = navigator.userAgent;
const isY9Machine = /Y9-ZYYH/i.test(ua); // 匹配机器型号
return isY9Machine;
}
// 爱山东环境登录
function getUserInfo() {
lightAppJssdk.user.getUserInfoWithEncryptedParamByAppId({
appId: config.appInfo.loveShandong, // 接入方在成功创建应用后自动生成
@@ -108,13 +207,9 @@ function loginCallback(userInfo) {
.then((resume) => {
if (resume.data.jobTitleId) {
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
safeReLaunch('/pages/index/index');
} else {
uni.redirectTo({
url: '/pages/login/login',
});
safeReLaunch('/pages/login/login');
}
});
});
@@ -166,19 +261,19 @@ uni-modal,
@font-face {
font-family: PingFangSC-Regular;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Regular.woff2') format('woff2');
src: url('/static/font/PingFangSC-Regular.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: PingFangSC-Medium;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Medium.woff2') format('woff2');
src: url('/static/font/PingFangSC-Medium.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: DIN-Medium;
src: url('https://qd.zhaopinzao8dian.com/file/csn/DIN-Medium.woff2') format('woff2');
src: url('/static/font/DIN-Medium.woff2') format('woff2');
font-display: swap;
}

View File

@@ -0,0 +1,65 @@
import {
$api
} from "./globalFunction";
import baseDB from '@/utils/db.js';
import useUserStore from '@/stores/useUserStore';
export class IncreaseRevie {
constructor(arg) {
this.myEventCallback = this.myCallback.bind(this);
this._debounceTimer = null;
this.init();
}
init() {
this.close();
setTimeout(() => {
if (window.hh?.on) {
window.hh.on('initQRListener', this.myEventCallback);
}
}, 1000);
}
close() {
if (window.hh?.off) {
window.hh.off('initQRListener', this.myEventCallback);
}
if (this._debounceTimer) {
clearTimeout(this._debounceTimer);
this._debounceTimer = null;
}
}
myCallback(res) {
if (this._debounceTimer) {
clearTimeout(this._debounceTimer);
}
this._debounceTimer = setTimeout(() => {
this.handleDebouncedCallback(res);
this._debounceTimer = null;
}, 300);
}
async handleDebouncedCallback(res) {
if (res.data) {
const code = res.data.qrQode
console.log('二维码code', code);
// 把code给到后端后端拿code兑换用户信息给前端返回token进行登录
// 一体机用户需要清空indexDB
// useUserStore()
// .loginSetToken(resData.token)
// .then((resume) => {
// if (resume.data.jobTitleId) {
// useUserStore().initSeesionId();
// safeReLaunch('/pages/index/index');
// } else {
// safeReLaunch('/pages/login/login');
// }
// });
// baseDB.resetAndReinit(); // 清空indexdb
} else {
$api.msg('识别失败')
}
}
}

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; */
}
@@ -477,4 +484,154 @@ html {
.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

@@ -51,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
});
}
/**
@@ -613,6 +630,10 @@ export function sm4Encrypt(key, value, mode = "hex") {
}
}
export function reloadBrowser() {
window.location.reload()
}
export const $api = {
msg,
@@ -627,7 +648,8 @@ export const $api = {
sendingMiniProgramMessage,
copyText,
aes_Decrypt,
createRequestWithCache
createRequestWithCache,
safeReLaunch
}
@@ -660,5 +682,7 @@ export default {
sm4Decrypt,
aes_Decrypt,
sm2_Decrypt,
sm2_Encrypt
sm2_Encrypt,
safeReLaunch,
reloadBrowser
}

44
common/wide-screen.css Normal file
View File

@@ -0,0 +1,44 @@
/* toast大小改为rpx */
uni-toast .uni-sample-toast {
width: 300rpx !important;
}
uni-toast .uni-simple-toast__text {
font-size: 30rpx !important;
padding: 15rpx 25rpx !important;
border-radius: 15rpx !important;
}
uni-toast .uni-toast {
width: 160rpx !important;
padding: 15rpx 25rpx !important;
border-radius: 15rpx !important;
}
uni-toast .uni-toast__icon {
width: 45rpx !important;
height: 45rpx !important;
}
uni-toast .uni-toast__content {
font-size: 30rpx !important;
}
.uni-load-more{
height: 40rpx !important;
}
.uni-load-more svg{
height: 30rpx !important;
width: 30rpx !important;
}
.uni-load-more__text{
font-size: 30rpx !important;
}
uni-modal .uni-modal{
max-width: 450rpx !important;
}
uni-modal .uni-modal__bd{
font-size: 34rpx !important;
min-height: 100rpx !important;
}
uni-modal .uni-modal__ft{
font-size: 36rpx !important;
line-height: 80rpx !important;
}

View File

@@ -27,7 +27,7 @@
</view>
<!-- 主体内容区域 -->
<view class="container-main">
<view class="container-main" :style="{ backgroundColor: mainbackGroundColor }">
<scroll-view v-if="useScrollView" scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<slot></slot>
</scroll-view>
@@ -70,6 +70,10 @@ defineProps({
type: String,
default: '#ffffff',
},
mainbackGroundColor: {
type: String,
default: '',
},
useScrollView: {
type: Boolean,
default: true,

View File

@@ -0,0 +1,115 @@
<template>
<uni-popup ref="popup" type="dialog" mask-background-color="rgba(0,0,0,0.5)" @change="onPopupChange">
<view class="global-confirm-wrapper">
<uni-popup-dialog
mode="base"
:type="state.type"
:title="state.title"
:content="state.content"
:before-close="true"
@confirm="onConfirm"
@close="onClose"
></uni-popup-dialog>
</view>
</uni-popup>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
const popup = ref(null);
const state = reactive({
title: '',
content: '',
type: 'info',
resolve: null,
});
onMounted(() => {
uni.$on('show-global-popup', (options) => {
state.title = options.title || '提示';
state.content = options.content || '';
state.type = options.type || 'info';
state.resolve = options.resolve;
popup.value.open();
});
});
onUnmounted(() => {
uni.$off('show-global-popup');
});
const onConfirm = () => {
if (state.resolve) state.resolve(true);
popup.value.close();
};
const onClose = () => {
if (state.resolve) state.resolve(false);
popup.value.close();
};
const onPopupChange = (e) => {
if (!e.show && state.resolve) {
state.resolve(false);
state.resolve = null;
}
};
</script>
<style lang="scss" scoped>
.global-confirm-wrapper {
/* 整个弹窗容器宽度自适应 */
width: 600rpx;
/* 深度覆盖 uni-popup-dialog 内置样式 */
:deep(.uni-popup-dialog) {
width: 600rpx !important;
background-color: #fff;
border-radius: 24rpx;
/* 标题部分 */
.uni-dialog-title {
padding: 40rpx 0 20rpx;
.uni-dialog-title-text {
font-size: 36rpx;
font-weight: bold;
}
}
/* 内容部分 */
.uni-dialog-content {
padding: 20rpx 40rpx 40rpx;
.uni-dialog-content-text {
font-size: 30rpx;
color: #666;
line-height: 1.5;
}
}
/* 底部按钮组 */
.uni-dialog-button-group {
height: 100rpx;
border-top: 1rpx solid #f0f0f0;
.uni-dialog-button {
height: 100%;
/* 每个按钮的样式 */
.uni-dialog-button-text {
font-size: 32rpx;
}
/* 确定按钮颜色(通常是蓝色或主题色) */
.uni-button-color {
color: #007aff;
}
}
/* 按钮中间的分割线 */
.uni-border-left {
border-left: 1rpx solid #f0f0f0;
}
}
}
}
</style>

View File

@@ -0,0 +1,664 @@
.uniui-cart-filled:before {
content: "\e6d0";
}
.uniui-gift-filled:before {
content: "\e6c4";
}
.uniui-color:before {
content: "\e6cf";
}
.uniui-wallet:before {
content: "\e6b1";
}
.uniui-settings-filled:before {
content: "\e6ce";
}
.uniui-auth-filled:before {
content: "\e6cc";
}
.uniui-shop-filled:before {
content: "\e6cd";
}
.uniui-staff-filled:before {
content: "\e6cb";
}
.uniui-vip-filled:before {
content: "\e6c6";
}
.uniui-plus-filled:before {
content: "\e6c7";
}
.uniui-folder-add-filled:before {
content: "\e6c8";
}
.uniui-color-filled:before {
content: "\e6c9";
}
.uniui-tune-filled:before {
content: "\e6ca";
}
.uniui-calendar-filled:before {
content: "\e6c0";
}
.uniui-notification-filled:before {
content: "\e6c1";
}
.uniui-wallet-filled:before {
content: "\e6c2";
}
.uniui-medal-filled:before {
content: "\e6c3";
}
.uniui-fire-filled:before {
content: "\e6c5";
}
.uniui-refreshempty:before {
content: "\e6bf";
}
.uniui-location-filled:before {
content: "\e6af";
}
.uniui-person-filled:before {
content: "\e69d";
}
.uniui-personadd-filled:before {
content: "\e698";
}
.uniui-arrowthinleft:before {
content: "\e6d2";
}
.uniui-arrowthinup:before {
content: "\e6d3";
}
.uniui-arrowthindown:before {
content: "\e6d4";
}
.uniui-back:before {
content: "\e6b9";
}
.uniui-forward:before {
content: "\e6ba";
}
.uniui-arrow-right:before {
content: "\e6bb";
}
.uniui-arrow-left:before {
content: "\e6bc";
}
.uniui-arrow-up:before {
content: "\e6bd";
}
.uniui-arrow-down:before {
content: "\e6be";
}
.uniui-arrowthinright:before {
content: "\e6d1";
}
.uniui-down:before {
content: "\e6b8";
}
.uniui-bottom:before {
content: "\e6b8";
}
.uniui-arrowright:before {
content: "\e6d5";
}
.uniui-right:before {
content: "\e6b5";
}
.uniui-up:before {
content: "\e6b6";
}
.uniui-top:before {
content: "\e6b6";
}
.uniui-left:before {
content: "\e6b7";
}
.uniui-arrowup:before {
content: "\e6d6";
}
.uniui-eye:before {
content: "\e651";
}
.uniui-eye-filled:before {
content: "\e66a";
}
.uniui-eye-slash:before {
content: "\e6b3";
}
.uniui-eye-slash-filled:before {
content: "\e6b4";
}
.uniui-info-filled:before {
content: "\e649";
}
.uniui-reload:before {
content: "\e6b2";
}
.uniui-micoff-filled:before {
content: "\e6b0";
}
.uniui-map-pin-ellipse:before {
content: "\e6ac";
}
.uniui-map-pin:before {
content: "\e6ad";
}
.uniui-location:before {
content: "\e6ae";
}
.uniui-starhalf:before {
content: "\e683";
}
.uniui-star:before {
content: "\e688";
}
.uniui-star-filled:before {
content: "\e68f";
}
.uniui-calendar:before {
content: "\e6a0";
}
.uniui-fire:before {
content: "\e6a1";
}
.uniui-medal:before {
content: "\e6a2";
}
.uniui-font:before {
content: "\e6a3";
}
.uniui-gift:before {
content: "\e6a4";
}
.uniui-link:before {
content: "\e6a5";
}
.uniui-notification:before {
content: "\e6a6";
}
.uniui-staff:before {
content: "\e6a7";
}
.uniui-vip:before {
content: "\e6a8";
}
.uniui-folder-add:before {
content: "\e6a9";
}
.uniui-tune:before {
content: "\e6aa";
}
.uniui-auth:before {
content: "\e6ab";
}
.uniui-person:before {
content: "\e699";
}
.uniui-email-filled:before {
content: "\e69a";
}
.uniui-phone-filled:before {
content: "\e69b";
}
.uniui-phone:before {
content: "\e69c";
}
.uniui-email:before {
content: "\e69e";
}
.uniui-personadd:before {
content: "\e69f";
}
.uniui-chatboxes-filled:before {
content: "\e692";
}
.uniui-contact:before {
content: "\e693";
}
.uniui-chatbubble-filled:before {
content: "\e694";
}
.uniui-contact-filled:before {
content: "\e695";
}
.uniui-chatboxes:before {
content: "\e696";
}
.uniui-chatbubble:before {
content: "\e697";
}
.uniui-upload-filled:before {
content: "\e68e";
}
.uniui-upload:before {
content: "\e690";
}
.uniui-weixin:before {
content: "\e691";
}
.uniui-compose:before {
content: "\e67f";
}
.uniui-qq:before {
content: "\e680";
}
.uniui-download-filled:before {
content: "\e681";
}
.uniui-pyq:before {
content: "\e682";
}
.uniui-sound:before {
content: "\e684";
}
.uniui-trash-filled:before {
content: "\e685";
}
.uniui-sound-filled:before {
content: "\e686";
}
.uniui-trash:before {
content: "\e687";
}
.uniui-videocam-filled:before {
content: "\e689";
}
.uniui-spinner-cycle:before {
content: "\e68a";
}
.uniui-weibo:before {
content: "\e68b";
}
.uniui-videocam:before {
content: "\e68c";
}
.uniui-download:before {
content: "\e68d";
}
.uniui-help:before {
content: "\e679";
}
.uniui-navigate-filled:before {
content: "\e67a";
}
.uniui-plusempty:before {
content: "\e67b";
}
.uniui-smallcircle:before {
content: "\e67c";
}
.uniui-minus-filled:before {
content: "\e67d";
}
.uniui-micoff:before {
content: "\e67e";
}
.uniui-closeempty:before {
content: "\e66c";
}
.uniui-clear:before {
content: "\e66d";
}
.uniui-navigate:before {
content: "\e66e";
}
.uniui-minus:before {
content: "\e66f";
}
.uniui-image:before {
content: "\e670";
}
.uniui-mic:before {
content: "\e671";
}
.uniui-paperplane:before {
content: "\e672";
}
.uniui-close:before {
content: "\e673";
}
.uniui-help-filled:before {
content: "\e674";
}
.uniui-paperplane-filled:before {
content: "\e675";
}
.uniui-plus:before {
content: "\e676";
}
.uniui-mic-filled:before {
content: "\e677";
}
.uniui-image-filled:before {
content: "\e678";
}
.uniui-locked-filled:before {
content: "\e668";
}
.uniui-info:before {
content: "\e669";
}
.uniui-locked:before {
content: "\e66b";
}
.uniui-camera-filled:before {
content: "\e658";
}
.uniui-chat-filled:before {
content: "\e659";
}
.uniui-camera:before {
content: "\e65a";
}
.uniui-circle:before {
content: "\e65b";
}
.uniui-checkmarkempty:before {
content: "\e65c";
}
.uniui-chat:before {
content: "\e65d";
}
.uniui-circle-filled:before {
content: "\e65e";
}
.uniui-flag:before {
content: "\e65f";
}
.uniui-flag-filled:before {
content: "\e660";
}
.uniui-gear-filled:before {
content: "\e661";
}
.uniui-home:before {
content: "\e662";
}
.uniui-home-filled:before {
content: "\e663";
}
.uniui-gear:before {
content: "\e664";
}
.uniui-smallcircle-filled:before {
content: "\e665";
}
.uniui-map-filled:before {
content: "\e666";
}
.uniui-map:before {
content: "\e667";
}
.uniui-refresh-filled:before {
content: "\e656";
}
.uniui-refresh:before {
content: "\e657";
}
.uniui-cloud-upload:before {
content: "\e645";
}
.uniui-cloud-download-filled:before {
content: "\e646";
}
.uniui-cloud-download:before {
content: "\e647";
}
.uniui-cloud-upload-filled:before {
content: "\e648";
}
.uniui-redo:before {
content: "\e64a";
}
.uniui-images-filled:before {
content: "\e64b";
}
.uniui-undo-filled:before {
content: "\e64c";
}
.uniui-more:before {
content: "\e64d";
}
.uniui-more-filled:before {
content: "\e64e";
}
.uniui-undo:before {
content: "\e64f";
}
.uniui-images:before {
content: "\e650";
}
.uniui-paperclip:before {
content: "\e652";
}
.uniui-settings:before {
content: "\e653";
}
.uniui-search:before {
content: "\e654";
}
.uniui-redo-filled:before {
content: "\e655";
}
.uniui-list:before {
content: "\e644";
}
.uniui-mail-open-filled:before {
content: "\e63a";
}
.uniui-hand-down-filled:before {
content: "\e63c";
}
.uniui-hand-down:before {
content: "\e63d";
}
.uniui-hand-up-filled:before {
content: "\e63e";
}
.uniui-hand-up:before {
content: "\e63f";
}
.uniui-heart-filled:before {
content: "\e641";
}
.uniui-mail-open:before {
content: "\e643";
}
.uniui-heart:before {
content: "\e639";
}
.uniui-loop:before {
content: "\e633";
}
.uniui-pulldown:before {
content: "\e632";
}
.uniui-scan:before {
content: "\e62a";
}
.uniui-bars:before {
content: "\e627";
}
.uniui-checkbox:before {
content: "\e62b";
}
.uniui-checkbox-filled:before {
content: "\e62c";
}
.uniui-shop:before {
content: "\e62f";
}
.uniui-headphones:before {
content: "\e630";
}
.uniui-cart:before {
content: "\e631";
}

View File

@@ -0,0 +1,664 @@
export type IconsData = {
id : string
name : string
font_family : string
css_prefix_text : string
description : string
glyphs : Array<IconsDataItem>
}
export type IconsDataItem = {
font_class : string
unicode : string
}
export const fontData = [
{
"font_class": "arrow-down",
"unicode": "\ue6be"
},
{
"font_class": "arrow-left",
"unicode": "\ue6bc"
},
{
"font_class": "arrow-right",
"unicode": "\ue6bb"
},
{
"font_class": "arrow-up",
"unicode": "\ue6bd"
},
{
"font_class": "auth",
"unicode": "\ue6ab"
},
{
"font_class": "auth-filled",
"unicode": "\ue6cc"
},
{
"font_class": "back",
"unicode": "\ue6b9"
},
{
"font_class": "bars",
"unicode": "\ue627"
},
{
"font_class": "calendar",
"unicode": "\ue6a0"
},
{
"font_class": "calendar-filled",
"unicode": "\ue6c0"
},
{
"font_class": "camera",
"unicode": "\ue65a"
},
{
"font_class": "camera-filled",
"unicode": "\ue658"
},
{
"font_class": "cart",
"unicode": "\ue631"
},
{
"font_class": "cart-filled",
"unicode": "\ue6d0"
},
{
"font_class": "chat",
"unicode": "\ue65d"
},
{
"font_class": "chat-filled",
"unicode": "\ue659"
},
{
"font_class": "chatboxes",
"unicode": "\ue696"
},
{
"font_class": "chatboxes-filled",
"unicode": "\ue692"
},
{
"font_class": "chatbubble",
"unicode": "\ue697"
},
{
"font_class": "chatbubble-filled",
"unicode": "\ue694"
},
{
"font_class": "checkbox",
"unicode": "\ue62b"
},
{
"font_class": "checkbox-filled",
"unicode": "\ue62c"
},
{
"font_class": "checkmarkempty",
"unicode": "\ue65c"
},
{
"font_class": "circle",
"unicode": "\ue65b"
},
{
"font_class": "circle-filled",
"unicode": "\ue65e"
},
{
"font_class": "clear",
"unicode": "\ue66d"
},
{
"font_class": "close",
"unicode": "\ue673"
},
{
"font_class": "closeempty",
"unicode": "\ue66c"
},
{
"font_class": "cloud-download",
"unicode": "\ue647"
},
{
"font_class": "cloud-download-filled",
"unicode": "\ue646"
},
{
"font_class": "cloud-upload",
"unicode": "\ue645"
},
{
"font_class": "cloud-upload-filled",
"unicode": "\ue648"
},
{
"font_class": "color",
"unicode": "\ue6cf"
},
{
"font_class": "color-filled",
"unicode": "\ue6c9"
},
{
"font_class": "compose",
"unicode": "\ue67f"
},
{
"font_class": "contact",
"unicode": "\ue693"
},
{
"font_class": "contact-filled",
"unicode": "\ue695"
},
{
"font_class": "down",
"unicode": "\ue6b8"
},
{
"font_class": "bottom",
"unicode": "\ue6b8"
},
{
"font_class": "download",
"unicode": "\ue68d"
},
{
"font_class": "download-filled",
"unicode": "\ue681"
},
{
"font_class": "email",
"unicode": "\ue69e"
},
{
"font_class": "email-filled",
"unicode": "\ue69a"
},
{
"font_class": "eye",
"unicode": "\ue651"
},
{
"font_class": "eye-filled",
"unicode": "\ue66a"
},
{
"font_class": "eye-slash",
"unicode": "\ue6b3"
},
{
"font_class": "eye-slash-filled",
"unicode": "\ue6b4"
},
{
"font_class": "fire",
"unicode": "\ue6a1"
},
{
"font_class": "fire-filled",
"unicode": "\ue6c5"
},
{
"font_class": "flag",
"unicode": "\ue65f"
},
{
"font_class": "flag-filled",
"unicode": "\ue660"
},
{
"font_class": "folder-add",
"unicode": "\ue6a9"
},
{
"font_class": "folder-add-filled",
"unicode": "\ue6c8"
},
{
"font_class": "font",
"unicode": "\ue6a3"
},
{
"font_class": "forward",
"unicode": "\ue6ba"
},
{
"font_class": "gear",
"unicode": "\ue664"
},
{
"font_class": "gear-filled",
"unicode": "\ue661"
},
{
"font_class": "gift",
"unicode": "\ue6a4"
},
{
"font_class": "gift-filled",
"unicode": "\ue6c4"
},
{
"font_class": "hand-down",
"unicode": "\ue63d"
},
{
"font_class": "hand-down-filled",
"unicode": "\ue63c"
},
{
"font_class": "hand-up",
"unicode": "\ue63f"
},
{
"font_class": "hand-up-filled",
"unicode": "\ue63e"
},
{
"font_class": "headphones",
"unicode": "\ue630"
},
{
"font_class": "heart",
"unicode": "\ue639"
},
{
"font_class": "heart-filled",
"unicode": "\ue641"
},
{
"font_class": "help",
"unicode": "\ue679"
},
{
"font_class": "help-filled",
"unicode": "\ue674"
},
{
"font_class": "home",
"unicode": "\ue662"
},
{
"font_class": "home-filled",
"unicode": "\ue663"
},
{
"font_class": "image",
"unicode": "\ue670"
},
{
"font_class": "image-filled",
"unicode": "\ue678"
},
{
"font_class": "images",
"unicode": "\ue650"
},
{
"font_class": "images-filled",
"unicode": "\ue64b"
},
{
"font_class": "info",
"unicode": "\ue669"
},
{
"font_class": "info-filled",
"unicode": "\ue649"
},
{
"font_class": "left",
"unicode": "\ue6b7"
},
{
"font_class": "link",
"unicode": "\ue6a5"
},
{
"font_class": "list",
"unicode": "\ue644"
},
{
"font_class": "location",
"unicode": "\ue6ae"
},
{
"font_class": "location-filled",
"unicode": "\ue6af"
},
{
"font_class": "locked",
"unicode": "\ue66b"
},
{
"font_class": "locked-filled",
"unicode": "\ue668"
},
{
"font_class": "loop",
"unicode": "\ue633"
},
{
"font_class": "mail-open",
"unicode": "\ue643"
},
{
"font_class": "mail-open-filled",
"unicode": "\ue63a"
},
{
"font_class": "map",
"unicode": "\ue667"
},
{
"font_class": "map-filled",
"unicode": "\ue666"
},
{
"font_class": "map-pin",
"unicode": "\ue6ad"
},
{
"font_class": "map-pin-ellipse",
"unicode": "\ue6ac"
},
{
"font_class": "medal",
"unicode": "\ue6a2"
},
{
"font_class": "medal-filled",
"unicode": "\ue6c3"
},
{
"font_class": "mic",
"unicode": "\ue671"
},
{
"font_class": "mic-filled",
"unicode": "\ue677"
},
{
"font_class": "micoff",
"unicode": "\ue67e"
},
{
"font_class": "micoff-filled",
"unicode": "\ue6b0"
},
{
"font_class": "minus",
"unicode": "\ue66f"
},
{
"font_class": "minus-filled",
"unicode": "\ue67d"
},
{
"font_class": "more",
"unicode": "\ue64d"
},
{
"font_class": "more-filled",
"unicode": "\ue64e"
},
{
"font_class": "navigate",
"unicode": "\ue66e"
},
{
"font_class": "navigate-filled",
"unicode": "\ue67a"
},
{
"font_class": "notification",
"unicode": "\ue6a6"
},
{
"font_class": "notification-filled",
"unicode": "\ue6c1"
},
{
"font_class": "paperclip",
"unicode": "\ue652"
},
{
"font_class": "paperplane",
"unicode": "\ue672"
},
{
"font_class": "paperplane-filled",
"unicode": "\ue675"
},
{
"font_class": "person",
"unicode": "\ue699"
},
{
"font_class": "person-filled",
"unicode": "\ue69d"
},
{
"font_class": "personadd",
"unicode": "\ue69f"
},
{
"font_class": "personadd-filled",
"unicode": "\ue698"
},
{
"font_class": "personadd-filled-copy",
"unicode": "\ue6d1"
},
{
"font_class": "phone",
"unicode": "\ue69c"
},
{
"font_class": "phone-filled",
"unicode": "\ue69b"
},
{
"font_class": "plus",
"unicode": "\ue676"
},
{
"font_class": "plus-filled",
"unicode": "\ue6c7"
},
{
"font_class": "plusempty",
"unicode": "\ue67b"
},
{
"font_class": "pulldown",
"unicode": "\ue632"
},
{
"font_class": "pyq",
"unicode": "\ue682"
},
{
"font_class": "qq",
"unicode": "\ue680"
},
{
"font_class": "redo",
"unicode": "\ue64a"
},
{
"font_class": "redo-filled",
"unicode": "\ue655"
},
{
"font_class": "refresh",
"unicode": "\ue657"
},
{
"font_class": "refresh-filled",
"unicode": "\ue656"
},
{
"font_class": "refreshempty",
"unicode": "\ue6bf"
},
{
"font_class": "reload",
"unicode": "\ue6b2"
},
{
"font_class": "right",
"unicode": "\ue6b5"
},
{
"font_class": "scan",
"unicode": "\ue62a"
},
{
"font_class": "search",
"unicode": "\ue654"
},
{
"font_class": "settings",
"unicode": "\ue653"
},
{
"font_class": "settings-filled",
"unicode": "\ue6ce"
},
{
"font_class": "shop",
"unicode": "\ue62f"
},
{
"font_class": "shop-filled",
"unicode": "\ue6cd"
},
{
"font_class": "smallcircle",
"unicode": "\ue67c"
},
{
"font_class": "smallcircle-filled",
"unicode": "\ue665"
},
{
"font_class": "sound",
"unicode": "\ue684"
},
{
"font_class": "sound-filled",
"unicode": "\ue686"
},
{
"font_class": "spinner-cycle",
"unicode": "\ue68a"
},
{
"font_class": "staff",
"unicode": "\ue6a7"
},
{
"font_class": "staff-filled",
"unicode": "\ue6cb"
},
{
"font_class": "star",
"unicode": "\ue688"
},
{
"font_class": "star-filled",
"unicode": "\ue68f"
},
{
"font_class": "starhalf",
"unicode": "\ue683"
},
{
"font_class": "trash",
"unicode": "\ue687"
},
{
"font_class": "trash-filled",
"unicode": "\ue685"
},
{
"font_class": "tune",
"unicode": "\ue6aa"
},
{
"font_class": "tune-filled",
"unicode": "\ue6ca"
},
{
"font_class": "undo",
"unicode": "\ue64f"
},
{
"font_class": "undo-filled",
"unicode": "\ue64c"
},
{
"font_class": "up",
"unicode": "\ue6b6"
},
{
"font_class": "top",
"unicode": "\ue6b6"
},
{
"font_class": "upload",
"unicode": "\ue690"
},
{
"font_class": "upload-filled",
"unicode": "\ue68e"
},
{
"font_class": "videocam",
"unicode": "\ue68c"
},
{
"font_class": "videocam-filled",
"unicode": "\ue689"
},
{
"font_class": "vip",
"unicode": "\ue6a8"
},
{
"font_class": "vip-filled",
"unicode": "\ue6c6"
},
{
"font_class": "wallet",
"unicode": "\ue6b1"
},
{
"font_class": "wallet-filled",
"unicode": "\ue6c2"
},
{
"font_class": "weibo",
"unicode": "\ue68b"
},
{
"font_class": "weixin",
"unicode": "\ue691"
}
] as IconsDataItem[]
// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)

Binary file not shown.

View File

@@ -0,0 +1,110 @@
<template>
<!-- #ifdef APP-NVUE -->
<text :style="styleObj" class="my-icons" @click="_onClick">{{unicode}}</text>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<text :style="styleObj" class="my-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick">
<slot></slot>
</text>
<!-- #endif -->
</template>
<script>
import { fontData } from './my-icons_file_vue.js';
const getVal = (val) => {
const reg = /^[0-9]*$/g
return (typeof val === 'number' || reg.test(val)) ? val + 'rpx' : val;
}
// #ifdef APP-NVUE
var domModule = weex.requireModule('dom');
import iconUrl from './my-icons.ttf'
domModule.addRule('fontFace', {
'fontFamily': "my-icons",
'src': "url('" + iconUrl + "')"
});
// #endif
/**
* Icons 图标
* @description 用于展示 icons 图标
* @tutorial https://ext.dcloud.net.cn/plugin?id=28
* @property {Number} size 图标大小
* @property {String} type 图标图案,参考示例
* @property {String} color 图标颜色
* @property {String} customPrefix 自定义图标
* @event {Function} click 点击 Icon 触发事件
*/
export default {
name: 'my-icons',
emits: ['click'],
props: {
type: {
type: String,
default: ''
},
color: {
type: String,
default: '#333333'
},
size: {
type: [Number, String],
default: 16
},
customPrefix: {
type: String,
default: ''
},
fontFamily: {
type: String,
default: ''
}
},
data() {
return {
icons: fontData
}
},
computed: {
unicode() {
let code = this.icons.find(v => v.font_class === this.type)
if (code) {
return code.unicode
}
return ''
},
iconSize() {
return getVal(this.size)
},
styleObj() {
if (this.fontFamily !== '') {
return `color: ${this.color}; font-size: ${this.iconSize}; font-family: ${this.fontFamily};`
}
return `color: ${this.color}; font-size: ${this.iconSize};`
}
},
methods: {
_onClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss">
/* #ifndef APP-NVUE */
@import './my-icons.css';
@font-face {
font-family: my-icons;
src: url('./my-icons.ttf');
}
/* #endif */
.my-icons {
font-family: my-icons;
text-decoration: none;
text-align: center;
}
</style>

View File

@@ -0,0 +1,649 @@
export const fontData = [
{
"font_class": "arrow-down",
"unicode": "\ue6be"
},
{
"font_class": "arrow-left",
"unicode": "\ue6bc"
},
{
"font_class": "arrow-right",
"unicode": "\ue6bb"
},
{
"font_class": "arrow-up",
"unicode": "\ue6bd"
},
{
"font_class": "auth",
"unicode": "\ue6ab"
},
{
"font_class": "auth-filled",
"unicode": "\ue6cc"
},
{
"font_class": "back",
"unicode": "\ue6b9"
},
{
"font_class": "bars",
"unicode": "\ue627"
},
{
"font_class": "calendar",
"unicode": "\ue6a0"
},
{
"font_class": "calendar-filled",
"unicode": "\ue6c0"
},
{
"font_class": "camera",
"unicode": "\ue65a"
},
{
"font_class": "camera-filled",
"unicode": "\ue658"
},
{
"font_class": "cart",
"unicode": "\ue631"
},
{
"font_class": "cart-filled",
"unicode": "\ue6d0"
},
{
"font_class": "chat",
"unicode": "\ue65d"
},
{
"font_class": "chat-filled",
"unicode": "\ue659"
},
{
"font_class": "chatboxes",
"unicode": "\ue696"
},
{
"font_class": "chatboxes-filled",
"unicode": "\ue692"
},
{
"font_class": "chatbubble",
"unicode": "\ue697"
},
{
"font_class": "chatbubble-filled",
"unicode": "\ue694"
},
{
"font_class": "checkbox",
"unicode": "\ue62b"
},
{
"font_class": "checkbox-filled",
"unicode": "\ue62c"
},
{
"font_class": "checkmarkempty",
"unicode": "\ue65c"
},
{
"font_class": "circle",
"unicode": "\ue65b"
},
{
"font_class": "circle-filled",
"unicode": "\ue65e"
},
{
"font_class": "clear",
"unicode": "\ue66d"
},
{
"font_class": "close",
"unicode": "\ue673"
},
{
"font_class": "closeempty",
"unicode": "\ue66c"
},
{
"font_class": "cloud-download",
"unicode": "\ue647"
},
{
"font_class": "cloud-download-filled",
"unicode": "\ue646"
},
{
"font_class": "cloud-upload",
"unicode": "\ue645"
},
{
"font_class": "cloud-upload-filled",
"unicode": "\ue648"
},
{
"font_class": "color",
"unicode": "\ue6cf"
},
{
"font_class": "color-filled",
"unicode": "\ue6c9"
},
{
"font_class": "compose",
"unicode": "\ue67f"
},
{
"font_class": "contact",
"unicode": "\ue693"
},
{
"font_class": "contact-filled",
"unicode": "\ue695"
},
{
"font_class": "down",
"unicode": "\ue6b8"
},
{
"font_class": "bottom",
"unicode": "\ue6b8"
},
{
"font_class": "download",
"unicode": "\ue68d"
},
{
"font_class": "download-filled",
"unicode": "\ue681"
},
{
"font_class": "email",
"unicode": "\ue69e"
},
{
"font_class": "email-filled",
"unicode": "\ue69a"
},
{
"font_class": "eye",
"unicode": "\ue651"
},
{
"font_class": "eye-filled",
"unicode": "\ue66a"
},
{
"font_class": "eye-slash",
"unicode": "\ue6b3"
},
{
"font_class": "eye-slash-filled",
"unicode": "\ue6b4"
},
{
"font_class": "fire",
"unicode": "\ue6a1"
},
{
"font_class": "fire-filled",
"unicode": "\ue6c5"
},
{
"font_class": "flag",
"unicode": "\ue65f"
},
{
"font_class": "flag-filled",
"unicode": "\ue660"
},
{
"font_class": "folder-add",
"unicode": "\ue6a9"
},
{
"font_class": "folder-add-filled",
"unicode": "\ue6c8"
},
{
"font_class": "font",
"unicode": "\ue6a3"
},
{
"font_class": "forward",
"unicode": "\ue6ba"
},
{
"font_class": "gear",
"unicode": "\ue664"
},
{
"font_class": "gear-filled",
"unicode": "\ue661"
},
{
"font_class": "gift",
"unicode": "\ue6a4"
},
{
"font_class": "gift-filled",
"unicode": "\ue6c4"
},
{
"font_class": "hand-down",
"unicode": "\ue63d"
},
{
"font_class": "hand-down-filled",
"unicode": "\ue63c"
},
{
"font_class": "hand-up",
"unicode": "\ue63f"
},
{
"font_class": "hand-up-filled",
"unicode": "\ue63e"
},
{
"font_class": "headphones",
"unicode": "\ue630"
},
{
"font_class": "heart",
"unicode": "\ue639"
},
{
"font_class": "heart-filled",
"unicode": "\ue641"
},
{
"font_class": "help",
"unicode": "\ue679"
},
{
"font_class": "help-filled",
"unicode": "\ue674"
},
{
"font_class": "home",
"unicode": "\ue662"
},
{
"font_class": "home-filled",
"unicode": "\ue663"
},
{
"font_class": "image",
"unicode": "\ue670"
},
{
"font_class": "image-filled",
"unicode": "\ue678"
},
{
"font_class": "images",
"unicode": "\ue650"
},
{
"font_class": "images-filled",
"unicode": "\ue64b"
},
{
"font_class": "info",
"unicode": "\ue669"
},
{
"font_class": "info-filled",
"unicode": "\ue649"
},
{
"font_class": "left",
"unicode": "\ue6b7"
},
{
"font_class": "link",
"unicode": "\ue6a5"
},
{
"font_class": "list",
"unicode": "\ue644"
},
{
"font_class": "location",
"unicode": "\ue6ae"
},
{
"font_class": "location-filled",
"unicode": "\ue6af"
},
{
"font_class": "locked",
"unicode": "\ue66b"
},
{
"font_class": "locked-filled",
"unicode": "\ue668"
},
{
"font_class": "loop",
"unicode": "\ue633"
},
{
"font_class": "mail-open",
"unicode": "\ue643"
},
{
"font_class": "mail-open-filled",
"unicode": "\ue63a"
},
{
"font_class": "map",
"unicode": "\ue667"
},
{
"font_class": "map-filled",
"unicode": "\ue666"
},
{
"font_class": "map-pin",
"unicode": "\ue6ad"
},
{
"font_class": "map-pin-ellipse",
"unicode": "\ue6ac"
},
{
"font_class": "medal",
"unicode": "\ue6a2"
},
{
"font_class": "medal-filled",
"unicode": "\ue6c3"
},
{
"font_class": "mic",
"unicode": "\ue671"
},
{
"font_class": "mic-filled",
"unicode": "\ue677"
},
{
"font_class": "micoff",
"unicode": "\ue67e"
},
{
"font_class": "micoff-filled",
"unicode": "\ue6b0"
},
{
"font_class": "minus",
"unicode": "\ue66f"
},
{
"font_class": "minus-filled",
"unicode": "\ue67d"
},
{
"font_class": "more",
"unicode": "\ue64d"
},
{
"font_class": "more-filled",
"unicode": "\ue64e"
},
{
"font_class": "navigate",
"unicode": "\ue66e"
},
{
"font_class": "navigate-filled",
"unicode": "\ue67a"
},
{
"font_class": "notification",
"unicode": "\ue6a6"
},
{
"font_class": "notification-filled",
"unicode": "\ue6c1"
},
{
"font_class": "paperclip",
"unicode": "\ue652"
},
{
"font_class": "paperplane",
"unicode": "\ue672"
},
{
"font_class": "paperplane-filled",
"unicode": "\ue675"
},
{
"font_class": "person",
"unicode": "\ue699"
},
{
"font_class": "person-filled",
"unicode": "\ue69d"
},
{
"font_class": "personadd",
"unicode": "\ue69f"
},
{
"font_class": "personadd-filled",
"unicode": "\ue698"
},
{
"font_class": "personadd-filled-copy",
"unicode": "\ue6d1"
},
{
"font_class": "phone",
"unicode": "\ue69c"
},
{
"font_class": "phone-filled",
"unicode": "\ue69b"
},
{
"font_class": "plus",
"unicode": "\ue676"
},
{
"font_class": "plus-filled",
"unicode": "\ue6c7"
},
{
"font_class": "plusempty",
"unicode": "\ue67b"
},
{
"font_class": "pulldown",
"unicode": "\ue632"
},
{
"font_class": "pyq",
"unicode": "\ue682"
},
{
"font_class": "qq",
"unicode": "\ue680"
},
{
"font_class": "redo",
"unicode": "\ue64a"
},
{
"font_class": "redo-filled",
"unicode": "\ue655"
},
{
"font_class": "refresh",
"unicode": "\ue657"
},
{
"font_class": "refresh-filled",
"unicode": "\ue656"
},
{
"font_class": "refreshempty",
"unicode": "\ue6bf"
},
{
"font_class": "reload",
"unicode": "\ue6b2"
},
{
"font_class": "right",
"unicode": "\ue6b5"
},
{
"font_class": "scan",
"unicode": "\ue62a"
},
{
"font_class": "search",
"unicode": "\ue654"
},
{
"font_class": "settings",
"unicode": "\ue653"
},
{
"font_class": "settings-filled",
"unicode": "\ue6ce"
},
{
"font_class": "shop",
"unicode": "\ue62f"
},
{
"font_class": "shop-filled",
"unicode": "\ue6cd"
},
{
"font_class": "smallcircle",
"unicode": "\ue67c"
},
{
"font_class": "smallcircle-filled",
"unicode": "\ue665"
},
{
"font_class": "sound",
"unicode": "\ue684"
},
{
"font_class": "sound-filled",
"unicode": "\ue686"
},
{
"font_class": "spinner-cycle",
"unicode": "\ue68a"
},
{
"font_class": "staff",
"unicode": "\ue6a7"
},
{
"font_class": "staff-filled",
"unicode": "\ue6cb"
},
{
"font_class": "star",
"unicode": "\ue688"
},
{
"font_class": "star-filled",
"unicode": "\ue68f"
},
{
"font_class": "starhalf",
"unicode": "\ue683"
},
{
"font_class": "trash",
"unicode": "\ue687"
},
{
"font_class": "trash-filled",
"unicode": "\ue685"
},
{
"font_class": "tune",
"unicode": "\ue6aa"
},
{
"font_class": "tune-filled",
"unicode": "\ue6ca"
},
{
"font_class": "undo",
"unicode": "\ue64f"
},
{
"font_class": "undo-filled",
"unicode": "\ue64c"
},
{
"font_class": "up",
"unicode": "\ue6b6"
},
{
"font_class": "top",
"unicode": "\ue6b6"
},
{
"font_class": "upload",
"unicode": "\ue690"
},
{
"font_class": "upload-filled",
"unicode": "\ue68e"
},
{
"font_class": "videocam",
"unicode": "\ue68c"
},
{
"font_class": "videocam-filled",
"unicode": "\ue689"
},
{
"font_class": "vip",
"unicode": "\ue6a8"
},
{
"font_class": "vip-filled",
"unicode": "\ue6c6"
},
{
"font_class": "wallet",
"unicode": "\ue6b1"
},
{
"font_class": "wallet-filled",
"unicode": "\ue6c2"
},
{
"font_class": "weibo",
"unicode": "\ue68b"
},
{
"font_class": "weixin",
"unicode": "\ue691"
}
]
// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)

View File

@@ -1,7 +1,7 @@
<template>
<view class="expected-station">
<view class="sex-search" v-if="search">
<uni-icons class="iconsearch" type="search" size="20"></uni-icons>
<my-icons class="iconsearch" type="search" size="40"></my-icons>
<input class="uni-input searchinput" confirm-type="search" />
</view>
<view class="sex-content">

View File

@@ -1,48 +1,52 @@
<template>
<view v-for="job in listData" :key="job.id">
<view class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company line_1">{{ job.gsmc }}</text>
</view>
<view class="card-bottom" >
<view class="fl_box fs_14" >
<!-- <dict-tree-Label class="mar_ri10" dictType="industry" :value="job.industry"></dict-tree-Label>
<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>{{ job.gsxy }}</view>
</view>
<view class="ris">
<text class="fs_14">
在招职位·
<text class="color_256BFA">{{ job.zzgwsl || '-' }}</text>
</text>
</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 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>
</view>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
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: '标题',
default: () => [],
},
longitude: {
type: Number,
@@ -61,13 +65,49 @@ const props = defineProps({
default: '',
},
});
const processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
const listData = computed(() => {
return props.list;
});
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`);
navTo(
`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.gsID}&companyName=${company.gsmc}&zphId=${props.zphId}&dataType=2`
);
}
</script>

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.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 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,6 +77,51 @@ 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) {

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.gwmc }}</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" :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>
<view class="card-companyName">{{ job.gsmc }}</view>
<view class="card-tags">
<view class="tag">
{{job.xlyq == '不限' ? '学历不限' : job.xlyq}}
<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="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
<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>
<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,6 +77,24 @@ const listData = computed(() => {
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) {

View File

@@ -4,7 +4,7 @@
<!-- 头部 -->
<view class="modal-header">
<text class="back-btn" @click="handleClose">
<uni-icons type="left" size="24"></uni-icons>
<my-icons type="left" size="48"></my-icons>
</text>
<text class="modal-title">{{ title }}</text>
<view class="back-btn"></view>

View File

@@ -19,8 +19,17 @@
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { ref, onMounted, computed } from 'vue';
import { useReadMsg } from '@/stores/useReadMsg';
import useScreenStore from '@/stores/useScreenStore'
import useUserStore from '@/stores/useUserStore';
const { isMachineEnv } = storeToRefs(useUserStore());
const screenStore = useScreenStore()
const {isWideScreen} = screenStore
const props = defineProps({
currentpage: {
type: Number,
@@ -66,6 +75,7 @@ const tabbarList = computed(() => [
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3].count,
needLogin:true,
},
{
id: 4,
@@ -75,18 +85,24 @@ const tabbarList = computed(() => [
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4].count,
needLogin:true,
},
]);
onMounted(() => {
uni.hideTabBar();
if(isWideScreen)uni.hideTabBar();
currentItem.value = props.currentpage;
});
const changeItem = (item) => {
uni.switchTab({
url: item.path,
});
if(isMachineEnv.value && item.needLogin){
useUserStore().logOut()
}else{
uni.switchTab({
url: item.path,
});
}
};
</script>

View File

@@ -1,21 +1,27 @@
export default {
// baseUrl: 'https://fw.rc.qingdao.gov.cn/rgpp-api/api', // 内网
baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
baseUrl: 'http://36.105.163.21:30081/rgpp/api', // 内网
// baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// baseUrl: 'http://192.168.3.29:8081',
// baseUrl: 'http://10.213.6.207:19010/api',
// 语音转文字
// vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/system/asr/connect', // 自定义
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/app/asr/connect', // 自定义
// vioceBaseURl: 'wss://fw.rc.qingdao.gov.cn/rgpp-api/api/app/asr/connect', // 内网
// 语音合成
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/',
// speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
// speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/', //直接替换即可
speechSynthesis2: 'http://39.98.44.136:19527', //直接替换即可
// indexedDB
DBversion: 2,
DBversion: 3,
// 只使用本地缓寸的数据
OnlyUseCachedDB: false,
// 素质测评URL
Quality_assessment_URL: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/49ee8533b31b46238906b31c27c5dfc9/zycpvhyjw/index.html#/pages/evaluation_record/evaluation_record?uuid=2',
Career_guidance: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/2626f6e3c899445db8639a873d172d73/zyzd/index.html',
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,
// 应用信息
@@ -53,7 +59,7 @@ export default {
allowedFileTypes: [
"text/plain", // .txt
"text/markdown", // .md
"text/html", // .html
// "text/html", // .html
"application/msword", // .doc
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
"application/pdf", // .pdf
@@ -81,5 +87,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'
]
}

459
hook/piper-bundle.js Normal file
View File

@@ -0,0 +1,459 @@
/**
* PiperTTS Bundle (SDK + Worker + PCMPlayer)
* Fix: Smart End Detection that supports Pause/Resume
*/
class PCMPlayer {
constructor(options) {
this.init(options);
}
init(options) {
this.option = Object.assign({}, {
inputCodec: 'Int16',
channels: 1,
sampleRate: 16000,
flushTime: 50,
fftSize: 2048,
}, options);
this.samples = new Float32Array();
this.interval = setInterval(this.flush.bind(this), this.option.flushTime);
this.convertValue = this.getConvertValue();
this.typedArray = this.getTypedArray();
this.initAudioContext();
this.bindAudioContextEvent();
}
getConvertValue() {
const map = {
Int8: 128,
Int16: 32768,
Int32: 2147483648,
Float32: 1
};
if (!map[this.option.inputCodec]) throw new Error('Codec Error');
return map[this.option.inputCodec];
}
getTypedArray() {
const map = {
Int8: Int8Array,
Int16: Int16Array,
Int32: Int32Array,
Float32: Float32Array
};
if (!map[this.option.inputCodec]) throw new Error('Codec Error');
return map[this.option.inputCodec];
}
initAudioContext() {
this.audioCtx = new(window.AudioContext || window.webkitAudioContext)();
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value = 1.0;
this.gainNode.connect(this.audioCtx.destination);
this.startTime = this.audioCtx.currentTime;
this.analyserNode = this.audioCtx.createAnalyser();
this.analyserNode.fftSize = this.option.fftSize;
}
static isTypedArray(data) {
return (data.byteLength && data.buffer && data.buffer.constructor == ArrayBuffer) || data.constructor ==
ArrayBuffer;
}
isSupported(data) {
if (!PCMPlayer.isTypedArray(data)) throw new Error('Data must be ArrayBuffer or TypedArray');
return true;
}
feed(data) {
this.isSupported(data);
data = this.getFormattedValue(data);
const tmp = new Float32Array(this.samples.length + data.length);
tmp.set(this.samples, 0);
tmp.set(data, this.samples.length);
this.samples = tmp;
}
getFormattedValue(data) {
data = data.constructor == ArrayBuffer ? new this.typedArray(data) : new this.typedArray(data.buffer);
let float32 = new Float32Array(data.length);
for (let i = 0; i < data.length; i++) {
float32[i] = data[i] / this.convertValue;
}
return float32;
}
volume(val) {
this.gainNode.gain.value = val;
}
destroy() {
if (this.interval) clearInterval(this.interval);
this.samples = null;
if (this.audioCtx) {
this.audioCtx.close();
this.audioCtx = null;
}
}
flush() {
if (!this.samples.length) return;
const bufferSource = this.audioCtx.createBufferSource();
if (typeof this.option.onended === 'function') {
bufferSource.onended = (e) => this.option.onended(this, e);
}
const length = this.samples.length / this.option.channels;
const audioBuffer = this.audioCtx.createBuffer(this.option.channels, length, this.option.sampleRate);
for (let channel = 0; channel < this.option.channels; channel++) {
const audioData = audioBuffer.getChannelData(channel);
let offset = channel;
let decrement = 50;
for (let i = 0; i < length; i++) {
audioData[i] = this.samples[offset];
if (i < 50) audioData[i] = (audioData[i] * i) / 50;
if (i >= length - 51) audioData[i] = (audioData[i] * decrement--) / 50;
offset += this.option.channels;
}
}
if (this.startTime < this.audioCtx.currentTime) {
this.startTime = this.audioCtx.currentTime;
}
bufferSource.buffer = audioBuffer;
bufferSource.connect(this.gainNode);
bufferSource.connect(this.analyserNode);
bufferSource.start(this.startTime);
this.startTime += audioBuffer.duration;
this.samples = new Float32Array();
}
async pause() {
await this.audioCtx.suspend();
}
async continue () {
await this.audioCtx.resume();
}
bindAudioContextEvent() {
if (typeof this.option.onstatechange === 'function') {
this.audioCtx.onstatechange = (e) => {
this.option.onstatechange(this, e, this.audioCtx.state);
};
}
}
}
// ==========================================
// Worker 源码
// ==========================================
const WORKER_SOURCE = `
let globalWs = null;
self.onmessage = function (e) {
const { type, data } = e.data;
switch (type) {
case 'connect': connectWebSocket(data); break;
case 'stop': closeWs(); break;
}
};
function closeWs() {
if (globalWs) {
globalWs.onerror = null;
globalWs.onclose = null;
globalWs.onmessage = null;
try { globalWs.close(1000, 'User stopped'); } catch (e) {}
globalWs = null;
}
}
function connectWebSocket(config) {
closeWs();
const { url, text, options } = config;
self.postMessage({ type: 'status', data: 'ws_connecting' });
try {
const currentWs = new WebSocket(url);
currentWs.binaryType = 'arraybuffer';
globalWs = currentWs;
currentWs.onopen = () => {
if (globalWs !== currentWs) return;
self.postMessage({ type: 'status', data: 'ws_connected' });
currentWs.send(JSON.stringify({
text: text,
speaker_id: options.speakerId || 0,
length_scale: options.lengthScale || 1.0,
noise_scale: options.noiseScale || 0.667,
}));
self.postMessage({ type: 'status', data: 'generating' });
};
currentWs.onmessage = (event) => {
if (globalWs !== currentWs) return;
if (typeof event.data === 'string' && event.data === 'END') {
const wsToClose = currentWs;
globalWs = null;
wsToClose.onmessage = null;
wsToClose.onerror = null;
wsToClose.onclose = null;
try { wsToClose.close(1000, 'Done'); } catch(e) {}
self.postMessage({ type: 'end' });
} else {
self.postMessage({ type: 'audio-data', buffer: event.data }, [event.data]);
}
};
currentWs.onclose = (e) => {
if (globalWs === currentWs) {
self.postMessage({ type: 'end' });
globalWs = null;
}
};
currentWs.onerror = () => {
if (globalWs === currentWs) {
self.postMessage({ type: 'error', data: 'WebSocket error' });
}
};
} catch (e) {
self.postMessage({ type: 'error', data: e.message });
}
}
`;
// ==========================================
// PiperTTS SDK
// ==========================================
class PiperTTS {
constructor(config = {}) {
this.baseUrl = config.baseUrl || 'http://localhost:5001';
this.onStatus = config.onStatus || console.log;
this.onStart = config.onStart || (() => {});
this.onEnd = config.onEnd || (() => {});
this.sampleRate = config.sampleRate || 16000;
this.player = null;
this.worker = null;
this.recordedChunks = [];
this.isRecording = false;
// 新增:检测音频结束的定时器 ID
this.endCheckInterval = null;
this._initWorker();
}
_initWorker() {
const blob = new Blob([WORKER_SOURCE], {
type: 'application/javascript'
});
this.worker = new Worker(URL.createObjectURL(blob));
this.worker.onmessage = (e) => {
const {
type,
data,
buffer
} = e.data;
switch (type) {
case 'status':
const map = {
ws_connecting: '正在连接...',
ws_connected: '已连接',
generating: '流式接收中...'
};
this.onStatus(map[data] || data, 'processing');
break;
case 'error':
if (this.recordedChunks.length > 0) {
this.onStatus('数据接收完毕', 'success');
this._triggerEndWithDelay();
} else {
this.onStatus(`错误: ${data}`, 'error');
this.stop();
}
break;
case 'audio-data':
this._handleAudio(buffer);
break;
case 'end':
this.onStatus('数据接收完毕', 'success');
this._triggerEndWithDelay();
break;
}
};
}
/**
* 【核心修改】智能轮询检测
* 只有当 AudioContext 处于 running 状态且时间走完时,才触发 onEnd
*/
_triggerEndWithDelay() {
// 先清除可能存在的旧定时器
if (this.endCheckInterval) clearInterval(this.endCheckInterval);
// 每 200ms 检查一次
this.endCheckInterval = setInterval(() => {
// 1. 如果播放器没了,直接结束
if (!this.player || !this.player.audioCtx) {
this._finishEndCheck();
return;
}
// 2. 如果处于暂停状态 (suspended),什么都不做,继续等
if (this.player.audioCtx.state === 'suspended') {
return;
}
// 3. 计算剩余时间
// startTime 是缓冲区结束的绝对时间currentTime 是当前时间
const remainingTime = this.player.startTime - this.player.audioCtx.currentTime;
// 4. 如果剩余时间小于 0.1秒(留点冗余),说明播完了
if (remainingTime <= 0.1) {
this._finishEndCheck();
}
}, 200);
}
_finishEndCheck() {
if (this.endCheckInterval) {
clearInterval(this.endCheckInterval);
this.endCheckInterval = null;
}
this.onEnd();
}
_initPlayer() {
if (this.player) {
this.player.destroy();
}
this.player = new PCMPlayer({
inputCodec: 'Int16',
channels: 1,
sampleRate: this.sampleRate,
flushTime: 50,
});
}
async speak(text, options = {}) {
if (!text) return;
this.stop();
this._initPlayer();
if (this.player) {
await this.player.continue();
}
this.recordedChunks = [];
this.isRecording = true;
this.onStart();
const wsUrl = this.baseUrl.replace(/^http/, 'ws') + '/ws/synthesize';
this.worker.postMessage({
type: 'connect',
data: {
url: wsUrl,
text,
options
},
});
}
stop() {
// 停止时必须清除轮询检测
if (this.endCheckInterval) {
clearInterval(this.endCheckInterval);
this.endCheckInterval = null;
}
this.worker.postMessage({
type: 'stop'
});
if (this.player) {
this.player.destroy();
this.player = null;
}
this.onStatus('已停止', 'default');
}
_handleAudio(arrayBuffer) {
if (this.isRecording) {
this.recordedChunks.push(arrayBuffer);
}
if (this.player) {
this.player.feed(arrayBuffer);
}
}
getAnalyserNode() {
return this.player ? this.player.analyserNode : null;
}
downloadAudio(filename = 'tts_output.wav') {
if (this.recordedChunks.length === 0) return;
let totalLen = 0;
for (let chunk of this.recordedChunks) totalLen += chunk.byteLength;
const tmp = new Uint8Array(totalLen);
let offset = 0;
for (let chunk of this.recordedChunks) {
tmp.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
}
const wavBuffer = this._encodeWAV(new Int16Array(tmp.buffer), this.sampleRate);
const blob = new Blob([wavBuffer], {
type: 'audio/wav'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style = 'display: none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}
_encodeWAV(samples, sampleRate) {
const buffer = new ArrayBuffer(44 + samples.length * 2);
const view = new DataView(buffer);
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) view.setUint8(offset + i, string.charCodeAt(i));
};
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + samples.length * 2, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, samples.length * 2, true);
let offset = 44;
for (let i = 0; i < samples.length; i++) {
view.setInt16(offset, samples[i], true);
offset += 2;
}
return view;
}
}
export default PiperTTS;

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

@@ -0,0 +1,216 @@
/**
* PiperTTS SDK - 兼容移动端的流式语音合成客户端
* 特性:
* 1. Web Audio API 实时调度,解决移动端不支持 MSE 的问题
* 2. 头部注入 (Header Injection) 技术,解决分片解码错误
* 3. 自动状态管理与事件回调
*/
export class PiperTTS {
constructor(config = {}) {
this.baseUrl = config.baseUrl || 'http://localhost:5001';
this.wsUrl = config.wsUrl || '/ws/synthesize'
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') + this.wsUrl;
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;
}
}

View File

@@ -6,6 +6,10 @@ import {
watch
} from 'vue'
import useScreenStore from '@/stores/useScreenStore'
const screenStore = useScreenStore()
export function useColumnCount(onChange = () => {}) {
const columnCount = ref(0)
const columnSpace = ref(2)
@@ -20,18 +24,21 @@ export function useColumnCount(onChange = () => {}) {
// }
// }
const calcColumn = () => {
const width = uni.getSystemInfoSync().windowWidth
// const width = uni.getSystemInfoSync().windowWidth
const {foldFeature,foldCount} = screenStore
let count = 2
if (width >= 1000) {
count = 5
} else if (width >= 750) {
count = 4
} else if (width >= 500) {
count = 3
} else {
count = 2
}
//折叠屏2*屏幕数量
if(foldFeature) count = foldCount * 2
else count = 2
// if (width >= 1000) {
// count = 5
// } else if (width >= 750) {
// count = 4
// } else if (width >= 500) {
// count = 3
// } else {
// count = 2
// }
if (count !== columnCount.value) {
columnCount.value = count

View File

@@ -3,255 +3,344 @@ import {
onUnmounted
} from 'vue'
import {
$api,
} from '../common/globalFunction';
$api
} from '../common/globalFunction'; // 你的请求封装
import config from '@/config'
// Alibaba Cloud
// 开源
export function useAudioRecorder() {
// --- 状态定义 ---
const isRecording = ref(false)
const isStopping = ref(false)
const isSocketConnected = ref(false)
const recordingDuration = ref(0)
const audioDataForDisplay = ref(new Array(16).fill(0))
const volumeLevel = ref(0)
const volumeLevel = ref(0) // 0-100
const recognizedText = ref('')
const lastFinalText = ref('')
let audioStream = null
let audioContext = null
let audioInput = null
let scriptProcessor = null
let websocket = null
// --- 内部变量 ---
let socketTask = null
let durationTimer = null
const generateUUID = () => {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11)
.replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
).replace(/-/g, '')
// --- 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
}
const fetchWsUrl = async () => {
const res = await $api.createRequest('/app/speech/getToken')
if (res.code !== 200) throw new Error('无法获取语音识别 wsUrl')
const wsUrl = res.msg
return wsUrl
/**
* 获取 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;
}
function extractWsParams(wsUrl) {
const url = new URL(wsUrl)
const appkey = url.searchParams.get('appkey')
const token = url.searchParams.get('token')
return {
appkey,
token
/**
* 开始录音 (入口)
*/
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();
}
}
const connectWebSocket = async () => {
const wsUrl = await fetchWsUrl()
const {
appkey,
token
} = extractWsParams(wsUrl)
/**
* 连接 WebSocket
*/
const connectSocket = (url) => {
return new Promise((resolve, reject) => {
websocket = new WebSocket(wsUrl)
websocket.binaryType = 'arraybuffer'
socketTask = uni.connectSocket({
url: url,
success: () => console.log('Socket 连接请求发送'),
fail: (err) => reject(err)
});
websocket.onopen = () => {
isSocketConnected.value = true
socketTask.onOpen((res) => {
console.log('WebSocket 已连接');
isSocketConnected.value = true;
// 发送 StartTranscription 消息(参考 demo.html
const startTranscriptionMessage = {
header: {
appkey: appkey, // 不影响使用,可留空或由 wsUrl 带入
namespace: 'SpeechTranscriber',
name: 'StartTranscription',
task_id: generateUUID(),
message_id: generateUUID()
},
payload: {
format: 'pcm',
sample_rate: 16000,
enable_intermediate_result: true,
enable_punctuation_prediction: true,
enable_inverse_text_normalization: true
}
// #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;
}
websocket.send(JSON.stringify(startTranscriptionMessage))
resolve()
}
});
websocket.onerror = (e) => {
isSocketConnected.value = false
reject(e)
}
socketTask.onError((err) => {
console.error('Socket 错误:', err);
isSocketConnected.value = false;
stopRecording();
});
websocket.onclose = () => {
isSocketConnected.value = false
}
websocket.onmessage = (e) => {
const msg = JSON.parse(e.data)
const name = msg?.header?.name
const payload = msg?.payload
switch (name) {
case 'TranscriptionResultChanged': {
// 中间识别文本(可选:使用 stash_result.unfixedText 更精确)
const text = payload?.unfixed_result || payload?.result || ''
lastFinalText.value = text
break
}
case 'SentenceBegin': {
// 可选:开始新的一句,重置状态
// console.log('开始新的句子识别')
break
}
case 'SentenceEnd': {
const text = payload?.result || ''
const confidence = payload?.confidence || 0
if (text && confidence > 0.5) {
recognizedText.value += text
lastFinalText.value = ''
// console.log('识别完成:', {
// text,
// confidence
// })
}
break
}
case 'TranscriptionStarted': {
// console.log('识别任务已开始')
break
}
case 'TranscriptionCompleted': {
lastFinalText.value = ''
// console.log('识别全部完成')
break
}
case 'TaskFailed': {
console.error('识别失败:', msg?.header?.status_text)
break
}
default:
console.log('未知消息类型:', name, msg)
break
}
}
socketTask.onClose(() => {
isSocketConnected.value = false;
console.log('Socket 已关闭');
});
})
}
const startRecording = async () => {
if (isRecording.value) return
const startH5Recording = async () => {
try {
recognizedText.value = ''
lastFinalText.value = ''
await connectWebSocket()
audioStream = await navigator.mediaDevices.getUserMedia({
// 1. 获取麦克风流
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
})
audioContext = new(window.AudioContext || window.webkitAudioContext)({
});
h5Stream = stream;
// 2. 创建 AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext({
sampleRate: 16000
})
audioInput = audioContext.createMediaStreamSource(audioStream)
scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1)
});
mediaStreamSource = audioContext.createMediaStreamSource(stream);
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
scriptProcessor.onaudioprocess = (event) => {
const input = event.inputBuffer.getChannelData(0)
const pcm = new Int16Array(input.length)
let sum = 0
for (let i = 0; i < input.length; ++i) {
const s = Math.max(-1, Math.min(1, input[i]))
pcm[i] = s * 0x7FFF
sum += s * s
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);
}
volumeLevel.value = Math.sqrt(sum / input.length)
audioDataForDisplay.value = Array(16).fill(volumeLevel.value)
socketTask.send({
data: buffer,
fail: (e) => console.error('发送音频失败', e)
});
};
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(pcm.buffer)
}
}
mediaStreamSource.connect(scriptProcessor);
scriptProcessor.connect(audioContext.destination);
audioInput.connect(scriptProcessor)
scriptProcessor.connect(audioContext.destination)
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
console.log('H5 录音已启动');
isRecording.value = true
recordingDuration.value = 0
durationTimer = setInterval(() => recordingDuration.value++, 1000)
} catch (err) {
console.error('启动失败:', err)
cleanup()
console.error('H5 录音启动失败:', err);
throw err;
}
}
const stopRecording = () => {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
const stopH5Resources = () => {
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify({
header: {
namespace: 'SpeechTranscriber',
name: 'StopTranscription',
message_id: generateUUID()
}
}))
websocket.close()
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();
}
cleanup()
isStopping.value = false
// 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 || isStopping.value) return
isStopping.value = true
websocket?.close()
cleanup()
isStopping.value = false
if (!isRecording.value) return;
console.log('取消录音 - 丢弃结果');
// 1. 停止硬件录音
stopHardwareResource();
// 2. 强制关闭 Socket
if (socketTask) {
socketTask.close();
}
// 3. 关键:清空已识别的文本
recognizedText.value = '';
// 4. 清理资源
cleanup();
}
/**
* 清理状态
*/
const cleanup = () => {
clearInterval(durationTimer)
clearInterval(durationTimer);
isRecording.value = false;
isSocketConnected.value = false;
socketTask = null;
recorderManager = null;
volumeLevel.value = 0;
}
scriptProcessor?.disconnect()
audioInput?.disconnect()
audioStream?.getTracks().forEach(track => track.stop())
audioContext?.close()
/**
* 计算音量 (兼容 Float32 和 Int16/ArrayBuffer)
*/
const calculateVolume = (data, isFloat32) => {
let sum = 0;
let length = 0;
audioStream = null
audioContext = null
audioInput = null
scriptProcessor = null
websocket = null
isRecording.value = false
isSocketConnected.value = false
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()
if (isRecording.value) {
stopRecording();
}
})
return {
isRecording,
isStopping,
isSocketConnected,
recordingDuration,
audioDataForDisplay,
volumeLevel,
recognizedText,
lastFinalText,
startRecording,
stopRecording,
cancelRecording

View File

@@ -1,348 +0,0 @@
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

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

View File

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

View File

@@ -1,217 +1,205 @@
import {
ref,
onUnmounted,
onBeforeUnmount,
onMounted
onUnmounted
} from 'vue'
import {
onHide,
onUnload
} from '@dcloudio/uni-app'
import WavDecoder from '@/lib/wav-decoder@1.3.0.js'
import config from '@/config'
import PiperTTS from './piper-bundle.js'
export function useTTSPlayer() {
const isSpeaking = ref(false)
const isPaused = ref(false)
const isComplete = ref(false)
// UI 状态
const isSpeaking = ref(false) // 是否正在交互(含播放、暂停、加载)
const isPaused = ref(false) // 是否处于暂停状态
const isLoading = ref(false) // 是否正在加载/连接
const audioContext = new(window.AudioContext || window.webkitAudioContext)()
let playTime = audioContext.currentTime
let sourceNodes = []
let socket = null
let sampleRate = 16000
let numChannels = 1
let isHeaderDecoded = false
let pendingText = null
// 单例 Piper 实例
let piper = null
let currentPlayId = 0
let activePlayId = 0
/**
* 获取或创建 SDK 实例
*/
const getPiperInstance = () => {
if (!piper) {
let baseUrl = config.speechSynthesis2 || ''
baseUrl = baseUrl.replace(/\/$/, '')
const speak = (text) => {
currentPlayId++
const myPlayId = currentPlayId
reset()
pendingText = text
activePlayId = myPlayId
}
const pause = () => {
if (audioContext.state === 'running') {
audioContext.suspend()
isPaused.value = true
isSpeaking.value = false
}
}
const resume = () => {
if (audioContext.state === 'suspended') {
audioContext.resume()
isPaused.value = false
isSpeaking.value = true
}
}
const cancelAudio = () => {
stop()
}
const stop = () => {
isSpeaking.value = false
isPaused.value = false
isComplete.value = false
playTime = audioContext.currentTime
sourceNodes.forEach(node => {
try {
node.stop()
node.disconnect()
} catch (e) {}
})
sourceNodes = []
if (socket) {
socket.close()
socket = null
}
isHeaderDecoded = false
pendingText = null
}
const reset = () => {
stop()
isSpeaking.value = false
isPaused.value = false
isComplete.value = false
playTime = audioContext.currentTime
initWebSocket()
}
const initWebSocket = () => {
const thisPlayId = currentPlayId
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
}
}
socket.onmessage = async (e) => {
if (thisPlayId !== activePlayId) return // 忽略旧播放的消息
if (typeof e.data === 'string') {
try {
const msg = JSON.parse(e.data)
if (msg.status === 'complete') {
isComplete.value = true
setTimeout(() => {
if (thisPlayId === activePlayId) {
isSpeaking.value = false
}
}, (playTime - audioContext.currentTime) * 1000)
piper = new PiperTTS({
baseUrl: baseUrl,
sampleRate: 16000,
onStatus: (msg, type) => {
if (type === 'error') {
console.error('[TTS Error]', msg)
resetState()
}
} catch (e) {
console.log('[TTSPlayer] 文本消息:', e.data)
},
onStart: () => {
isLoading.value = false
isSpeaking.value = true
isPaused.value = false
},
onEnd: () => {
// 只有非暂停状态下的结束,才重置所有状态
// 如果是用户手动暂停导致的中断,不应视为自然播放结束
isSpeaking.value = false
isLoading.value = false
isPaused.value = false
}
} else if (e.data instanceof ArrayBuffer) {
if (!isHeaderDecoded) {
try {
const decoded = await WavDecoder.decode(e.data)
sampleRate = decoded.sampleRate
numChannels = decoded.channelData.length
decoded.channelData.forEach((channel, i) => {
const audioBuffer = audioContext.createBuffer(1, channel.length,
sampleRate)
audioBuffer.copyToChannel(channel, 0)
playBuffer(audioBuffer)
})
isHeaderDecoded = true
} catch (err) {
console.error('WAV 解码失败:', err)
}
} else {
const pcm = new Int16Array(e.data)
const audioBuffer = pcmToAudioBuffer(pcm, sampleRate, numChannels)
playBuffer(audioBuffer)
}
}
})
}
return piper
}
const pcmToAudioBuffer = (pcm, sampleRate, numChannels) => {
const length = pcm.length / numChannels
const audioBuffer = audioContext.createBuffer(numChannels, length, sampleRate)
for (let ch = 0; ch < numChannels; ch++) {
const channelData = audioBuffer.getChannelData(ch)
for (let i = 0; i < length; i++) {
const sample = pcm[i * numChannels + ch]
channelData[i] = sample / 32768
}
}
return audioBuffer
}
/**
* 核心朗读方法
*/
const speak = async (text) => {
if (!text) return
const playBuffer = (audioBuffer) => {
if (!isSpeaking.value) {
playTime = audioContext.currentTime
}
const source = audioContext.createBufferSource()
source.buffer = audioBuffer
source.connect(audioContext.destination)
source.start(playTime)
sourceNodes.push(source)
playTime += audioBuffer.duration
const processedText = extractSpeechText(text)
if (!processedText) return
const instance = getPiperInstance()
// 重置状态
isLoading.value = true
isPaused.value = false
isSpeaking.value = true
try {
// 直接调用 speakSDK 内部会自动处理 init 和 stop
await instance.speak(processedText, {
speakerId: 0,
noiseScale: 0.667,
lengthScale: 1.0
})
} catch (e) {
console.error('TTS Speak Error:', e)
resetState()
}
}
onUnmounted(() => {
stop()
})
/**
* 暂停
*/
const pause = async () => {
// 1. 只有正在播放且未暂停时,才执行暂停
if (!isSpeaking.value || isPaused.value) return
// 页面刷新/关闭时
onMounted(() => {
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', cancelAudio)
// 2. 检查播放器实例是否存在
if (piper && piper.player) {
try {
// 执行音频挂起
await piper.player.pause()
// 3. 成功后更新 UI
isPaused.value = true
} catch (e) {
console.error("Pause failed:", e)
// 即使报错,如果不是致命错误,也可以尝试强制更新 UI
// isPaused.value = true
}
}
})
}
onBeforeUnmount(() => {
cancelAudio()
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', cancelAudio)
/**
* 恢复 (继续播放)
*/
const resume = async () => {
// 1. 只有处于暂停状态时,才执行恢复
if (!isPaused.value) return
if (piper && piper.player) {
try {
await piper.player.continue()
// 2. 成功后更新 UI
isPaused.value = false
isSpeaking.value = true
} catch (e) {
console.error("Resume failed:", e)
}
}
})
}
onHide(cancelAudio)
onUnload(cancelAudio)
/**
* 切换 播放/暂停 (方便按钮绑定)
*/
const togglePlay = () => {
if (isPaused.value) {
resume()
} else {
pause()
}
}
initWebSocket()
/**
* 停止 (中断)
*/
const stop = () => {
if (piper) {
piper.stop()
}
resetState()
}
/**
* 彻底销毁
*/
const destroy = () => {
if (piper) {
piper.stop()
piper = null
}
resetState()
}
const resetState = () => {
isSpeaking.value = false
isPaused.value = false
isLoading.value = false
}
// === 生命周期管理 ===
onUnmounted(destroy)
if (typeof onHide === 'function') {
onHide(() => {
togglePlay()
// stop()
})
}
if (typeof onUnload === 'function') {
onUnload(destroy)
}
return {
speak,
pause,
resume,
cancelAudio,
togglePlay, // 新增:单按钮切换功能
stop,
cancelAudio: stop,
isSpeaking,
isPaused,
isComplete
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;
// 提取岗位 json 数据及前后位置
while ((match = jobRegex.exec(markdown)) !== null) {
const jobStr = match[1];
try {
@@ -225,27 +213,16 @@ function extractSpeechText(markdown) {
console.warn('JSON 解析失败', e);
}
}
// 提取引导语(第一个 job-json 之前的文字)
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() :
'';
// 提取结束语(最后一个 job-json 之后的文字)
markdown.slice(0, firstJobStartIndex).trim() : '';
const endingText = lastJobEndIndex < markdown.length ?
markdown.slice(lastJobEndIndex).trim() :
'';
// 岗位信息格式化为语音文本
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

@@ -1,333 +0,0 @@
import {
ref,
onUnmounted,
onMounted
} from 'vue'
// 如果是 uni-app 环境,保留这些导入;如果是纯 Web Vue3可以移除
import {
onHide,
onUnload
} from '@dcloudio/uni-app'
import config from '@/config'
/**
* Piper TTS 播放钩子 (WebSocket MSE 流式版 - 含 cancelAudio)
* 依赖: 后端必须去除 MP3 ID3 标签 (-map_metadata -1)
*/
export function useTTSPlayer() {
// 状态管理
const isSpeaking = ref(false)
const isPaused = ref(false)
const isLoading = ref(false)
// 核心对象
let audio = null
let mediaSource = null
let sourceBuffer = null
let ws = null
// 缓冲队列管理
let bufferQueue = []
let isAppending = false
let isStreamEnded = false
// 初始化 Audio 监听器 (只运行一次)
const initAudioElement = () => {
if (!audio && typeof window !== 'undefined') {
audio = new Audio()
// 错误监听
audio.addEventListener('error', (e) => {
// 如果是手动停止导致的 error (src 被置空),忽略
if (!audio.src) return
console.error('Audio Player Error:', e)
resetState()
})
// 播放结束监听
audio.addEventListener('ended', () => {
resetState()
})
}
}
/**
* 核心朗读方法 (WebSocket)
* @param {string} text - 要朗读的文本
*/
const speak = async (text) => {
if (!text) return
// 1. 提取文本
const processedText = extractSpeechText(text)
if (!processedText) return
// 2. 彻底清理旧状态
cancelAudio()
initAudioElement()
isLoading.value = true
isSpeaking.value = true
isPaused.value = false
isStreamEnded = false
// 3. 检查环境
if (!window.MediaSource || !window.WebSocket) {
console.error('当前环境不支持 MediaSource 或 WebSocket')
resetState()
return
}
try {
// 4. 初始化 MSE
mediaSource = new MediaSource()
// 绑定 MSE 到 Audio
audio.src = URL.createObjectURL(mediaSource)
// 监听 MSE 打开事件
mediaSource.addEventListener('sourceopen', () => {
// 防止多次触发
if (mediaSource.sourceBuffers.length > 0) return
startWebSocketStream(processedText)
})
// 尝试播放 (处理浏览器自动播放策略)
const playPromise = audio.play()
if (playPromise !== undefined) {
playPromise.catch(e => {
console.warn('自动播放被拦截 (需用户交互):', e)
// 保持 isSpeaking 为 trueUI 显示播放按钮,用户点击后调用 resume() 即可
})
}
} catch (err) {
console.error('TTS Initialization Failed:', err)
cancelAudio()
}
}
// 启动 WebSocket 流程
const startWebSocketStream = (text) => {
const mime = 'audio/mpeg'
// 4.1 创建 SourceBuffer
try {
sourceBuffer = mediaSource.addSourceBuffer(mime)
sourceBuffer.addEventListener('updateend', () => {
isAppending = false
processQueue()
})
} catch (e) {
console.error('SourceBuffer Create Failed:', e)
return
}
// 4.2 计算 WebSocket 地址
let baseUrl = config.speechSynthesis2 || ''
baseUrl = baseUrl.replace(/\/$/, '')
const wsUrl = baseUrl.replace(/^http/, 'ws') + '/ws/synthesize'
// 4.3 建立连接
ws = new WebSocket(wsUrl)
ws.binaryType = 'arraybuffer' // 关键
ws.onopen = () => {
// console.log('WS Open')
ws.send(JSON.stringify({
text: text,
speaker_id: 0,
length_scale: 1.0,
noise_scale: 0.667
}))
isLoading.value = false
}
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
bufferQueue.push(event.data)
processQueue()
}
}
ws.onerror = (e) => {
console.error('WS Error:', e)
cancelAudio()
}
ws.onclose = () => {
// console.log('WS Closed')
isStreamEnded = true
// 检查是否需要结束 MSE 流
checkEndOfStream()
}
}
// 处理缓冲队列
const processQueue = () => {
if (!sourceBuffer || sourceBuffer.updating || bufferQueue.length === 0) {
// 如果队列空了,且流已结束,尝试结束 MSE
if (bufferQueue.length === 0 && isStreamEnded && !sourceBuffer.updating) {
checkEndOfStream()
}
return
}
isAppending = true
const chunk = bufferQueue.shift()
try {
sourceBuffer.appendBuffer(chunk)
} catch (e) {
// console.error('AppendBuffer Error:', e)
isAppending = false
}
}
// 结束 MSE 流
const checkEndOfStream = () => {
if (mediaSource && mediaSource.readyState === 'open' && bufferQueue.length === 0 && !sourceBuffer
?.updating) {
try {
mediaSource.endOfStream()
} catch (e) {}
}
}
const pause = () => {
if (audio && !audio.paused) {
audio.pause()
isPaused.value = true
isSpeaking.value = false
}
}
const resume = () => {
if (audio && audio.paused) {
audio.play()
isPaused.value = false
isSpeaking.value = true
}
}
// === 新增/核心方法:取消并停止 ===
const cancelAudio = () => {
// 1. 断开 WebSocket (停止数据接收)
if (ws) {
// 移除监听器防止报错
ws.onclose = null
ws.onerror = null
ws.onmessage = null
ws.close()
ws = null
}
// 2. 停止音频播放
if (audio) {
audio.pause()
// 释放 Blob URL 内存
if (audio.src) {
URL.revokeObjectURL(audio.src)
audio.removeAttribute('src')
}
audio.currentTime = 0
}
// 3. 清理 MSE 对象
if (mediaSource) {
try {
if (mediaSource.readyState === 'open') {
mediaSource.endOfStream()
}
} catch (e) {}
mediaSource = null
}
sourceBuffer = null
bufferQueue = []
isAppending = false
isStreamEnded = false
// 4. 重置 UI 状态
resetState()
}
// 只是重置 UI 变量的辅助函数
const resetState = () => {
isSpeaking.value = false
isPaused.value = false
isLoading.value = false
}
// 别名 stop -> cancelAudio (保持兼容性)
const stop = cancelAudio
// === 生命周期 ===
onMounted(() => {
initAudioElement()
})
onUnmounted(() => {
cancelAudio()
audio = null
})
if (typeof onHide === 'function') onHide(cancelAudio)
if (typeof onUnload === 'function') onUnload(cancelAudio)
return {
speak,
pause,
resume,
stop,
cancelAudio, // 新增导出
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

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

13
main.js
View File

@@ -6,6 +6,8 @@ import {
import globalFunction from '@/common/globalFunction'
import '@/lib/string-similarity.min.js'
import similarityJobs from '@/utils/similarity_Job.js';
import useScreenStore from './stores/useScreenStore'
// 组件
import AppLayout from './components/AppLayout/AppLayout.vue';
@@ -22,6 +24,8 @@ import renderDeliveryRecord from '@/components/renderDeliveryRecord/renderDelive
import renderJobCollectionRecord from '@/components/renderJobCollectionRecord/renderJobCollectionRecord.vue';
import renderCompanyCollectionRecord from '@/components/renderCompanyCollectionRecord/renderCompanyCollectionRecord.vue';
import renderJobViewRecord from '@/components/renderJobViewRecord/renderJobViewRecord.vue';
import MyIcons from '@/components/My-icons/my-icons.vue';
import GlobalPopup from '@/components/GlobalPopup/GlobalPopup.vue'
// import Tabbar from '@/components/tabbar/midell-box.vue'
// 自动导入 directives 目录下所有指令
console.log(lightAppJssdk)
@@ -33,8 +37,7 @@ import {
createSSRApp,
} from 'vue'
const foldFeature = window.visualViewport && 'segments' in window.visualViewport
console.log('是否支持多段屏幕:', foldFeature)
// 全局组件
export function createApp() {
@@ -53,6 +56,8 @@ export function createApp() {
app.component('renderJobCollectionRecord', renderJobCollectionRecord) //渲染岗位收藏记录
app.component('renderCompanyCollectionRecord', renderCompanyCollectionRecord) //渲染公司收藏记录
app.component('renderJobViewRecord', renderJobViewRecord) //渲染岗位浏览记录
app.component('MyIcons', MyIcons)
app.component('global-popup', GlobalPopup)
// app.component('tabbar-custom', Tabbar)
for (const path in directives) {
@@ -66,6 +71,7 @@ export function createApp() {
...globalFunction,
similarityJobs
});
app.provide('deviceInfo', globalFunction.getdeviceInfo());
app.use(SelectPopupPlugin);
@@ -74,6 +80,9 @@ export function createApp() {
store.use(createUnistorage());
app.use(store);
// 初始化屏幕检测
const screenStore = useScreenStore()
screenStore.initScreenDetection()
return {
app,
Pinia

View File

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

970
package-lock.json generated Normal file
View File

@@ -0,0 +1,970 @@
{
"name": "qingdao-employment-service",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"unplugin-vue-components": "^0.26.0"
}
},
"node_modules/@antfu/utils": {
"version": "0.7.10",
"resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
"integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/compiler-core": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.25",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.5.25",
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.25",
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-ssr": "3.5.25",
"@vue/shared": "3.5.25",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.25.tgz",
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.25",
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.25",
"@vue/runtime-core": "3.5.25",
"@vue/shared": "3.5.25",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.25",
"@vue/shared": "3.5.25"
},
"peerDependencies": {
"vue": "3.5.25"
}
},
"node_modules/@vue/shared": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.25.tgz",
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/local-pkg": {
"version": "0.4.3",
"resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz",
"integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC",
"peer": true
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/unplugin": {
"version": "1.16.1",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz",
"integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/unplugin-vue-components": {
"version": "0.26.0",
"resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz",
"integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@antfu/utils": "^0.7.6",
"@rollup/pluginutils": "^5.0.4",
"chokidar": "^3.5.3",
"debug": "^4.3.4",
"fast-glob": "^3.3.1",
"local-pkg": "^0.4.3",
"magic-string": "^0.30.3",
"minimatch": "^9.0.3",
"resolve": "^1.22.4",
"unplugin": "^1.4.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@babel/parser": "^7.15.8",
"@nuxt/kit": "^3.2.2",
"vue": "2 || 3"
},
"peerDependenciesMeta": {
"@babel/parser": {
"optional": true
},
"@nuxt/kit": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",
"@vue/runtime-dom": "3.5.25",
"@vue/server-renderer": "3.5.25",
"@vue/shared": "3.5.25"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"devDependencies": {
"unplugin-vue-components": "^0.26.0"
}
}

View File

@@ -1,14 +1,27 @@
<template>
<view class="collection-content">
<renderDeliveryRecord
v-if="pageState.list.length"
seeDate="applyTime"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderDeliveryRecord>
<empty v-else :is-position="true"></empty>
</view>
<AppLayout
title="投递记录"
:use-scroll-view="true"
mainback-ground-color="#f4f4f4"
:show-bg-image="false"
@onScrollBottom="getJobList()"
>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view class="collection-content">
<renderDeliveryRecord
v-if="pageState.list.length"
seeDate="applyTime"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderDeliveryRecord>
<empty v-else :is-position="true"></empty>
</view>
</AppLayout>
</template>
<script setup>
@@ -16,11 +29,12 @@ import dictLabel from '@/components/dict-Label/dict-Label.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, vacanciesTo } = inject('globalFunction');
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
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,10 +51,6 @@ onLoad(() => {
getJobList();
});
onReachBottom(() => {
getJobList();
});
function getJobList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
@@ -73,11 +83,18 @@ function getJobList(type = 'add') {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
image {
height: 100%;
width: 100%;
}
.collection-content{
padding: 1rpx 28rpx 20rpx 28rpx;
background: #F4F4F4;
height: 100%;
min-height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
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>
@@ -75,6 +75,8 @@ 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');
@@ -182,10 +184,13 @@ function getThirdPartyJobsList(type = 'add') {
let params = {
current: pageState.current,
pageSize: pageState.pageSize,
gsID:companyId,
gsmc:companyName,
zphID:zphId
};
$api.createRequest(
`/app/internal/jobThirdPart?gsID=${companyId}&gsmc=${companyName}&zphID=${zphId}`,
`/app/internal/jobThirdPart`,
params,
'GET',
true
@@ -220,7 +225,8 @@ function handleJobsListResponse(type, rows, total, pageKey) {
const str = pageState.pageSize * (pageState[pageKey] - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
pageState.list = [...pageState.list, ...rows];
// pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}

View File

@@ -1,10 +1,11 @@
<template>
<AppLayout title="添加岗位">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view v-show="searchFocus" class="search-mask" ></view>
<view class="main">
<view class="content-title">
<view class="title-lf">
@@ -18,7 +19,7 @@
</view>
<view class="content-list">
<view class="list-search">
<uni-icons type="search" color="#333333" size="24"></uni-icons>
<my-icons type="search" color="#333333" size="48"></my-icons>
<input
class="search-input"
v-model="inputVal"
@@ -74,7 +75,7 @@ const { $api, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { getUserResume } = useUserStore();
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram ,hasLogin,isMachineEnv} = storeToRefs(useUserStore());
const popup = ref(null);
const selectJobsModel = ref(null);
@@ -83,6 +84,7 @@ const dataSource = ref([]);
const filterList = ref([]);
const dataItem = ref(null);
const inputVal = ref('');
const searchFocus = ref(false);
onLoad(() => {
getTree();
@@ -93,12 +95,14 @@ function close() {
}
function handleBlur() {
searchFocus.value = false
setTimeout(() => {
filterList.value = [];
}, 100);
}
function handleFocus() {
searchFocus.value = true
const val = inputVal.value.toLowerCase();
if (val && dataSource.value) {
filterList.value = dataSource.value.filter((item) => item.lable.toLowerCase().search(val) !== -1);
@@ -141,6 +145,10 @@ function deleteItem(item, index) {
}
function changeJobs() {
if(isMachineEnv.value && !hasLogin.value){
useUserStore().logOut()
return
}
selectJobsModel.value?.open({
title: '添加岗位',
maskClick: true,
@@ -217,6 +225,7 @@ function flattenTree(treeData, parentPath = '') {
align-items: center;
padding: 0 24rpx;
position: relative;
z-index:10;
.search-input{
flex: 1;
padding: 0 20rpx;
@@ -261,6 +270,15 @@ function flattenTree(treeData, parentPath = '') {
width: 100%;
}
}
.search-mask{
position fixed;
width 100vw;
height:100vh;
top:0;
left:0
z-index:9;
background: #33333355;
}
.main{
padding: 54rpx 28rpx 28rpx 28rpx
.content-list{

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>
@@ -9,7 +9,7 @@
<view class="collection-search">
<view class="search-content">
<view class="header-input button-click">
<uni-icons class="iconsearch" color="#6A6A6A" type="search" size="22"></uni-icons>
<my-icons class="iconsearch" color="#6A6A6A" type="search" size="44"></my-icons>
<input
class="input"
@confirm="searchCollection"
@@ -46,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('');
@@ -123,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 {

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(() => {

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>
@@ -9,7 +9,7 @@
<view class="collection-search">
<view class="search-content">
<view class="header-input button-click">
<uni-icons class="iconsearch" color="#6A6A6A" type="search" size="22"></uni-icons>
<my-icons class="iconsearch" color="#6A6A6A" type="search" size="44"></my-icons>
<input
class="input"
@confirm="searchCollection"
@@ -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({});

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>
@@ -61,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({

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>
@@ -83,7 +83,7 @@
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanysOutData>
<empty v-else is-position></empty>
<empty v-if="!pageState.list.length" is-position></empty>
</view>
</scroll-view>
</view>
@@ -110,6 +110,8 @@ 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({});
@@ -151,9 +153,11 @@ function getCompanyList(type = 'add') {
let params = {
current: pageState.current,
pageSize: pageState.pageSize,
zphID:jobFairId,
zphmc:jobFairName
};
$api.createRequest(
`/app/internal/companyThirdPart/?zphID=${jobFairId}&zphmc=${jobFairName}`,
`/app/internal/companyThirdPart`,
params,
'GET',
true
@@ -163,7 +167,8 @@ function getCompanyList(type = 'add') {
const str = pageState.pageSize * (pageState.current - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
pageState.list = [...pageState.list, ...rows];
// pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}

View File

@@ -1,8 +1,15 @@
<template>
<view class="collection-content">
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<loadmore ref="loadmoreRef"></loadmore>
</view>
<AppLayout title="更多岗位" :use-scroll-view="true" :show-bg-image="false" @onScrollBottom="loadMore">
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view class="collection-content">
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</AppLayout>
</template>
<script setup>
@@ -16,6 +23,7 @@ import useLocationStore from '@/stores/useLocationStore';
import { usePagination } from '@/hook/usePagination';
import { jobMoreMap } from '@/utils/markdownParser';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { isMiniProgram } = storeToRefs(useUserStore());
const loadmoreRef = ref(null);
// 响应式搜索条件(可以被修改)
@@ -57,17 +65,20 @@ function removeNullProperties(obj) {
}
return obj;
}
onReachBottom(() => {
loadMore();
});
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
image {
height: 100%;
width: 100%;
}
.collection-content{
padding: 1rpx 28rpx 20rpx 28rpx;
background: #F4F4F4;
height: 100%
min-height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
height: 100%;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
@@ -137,7 +137,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
import config from '@/config.js';

View File

@@ -1,48 +1,55 @@
<template>
<view class="reser-content">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="content-top">
<view
class="top-item button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="changeOption('all')"
>
全部
</view>
<view
class="top-item button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="changeOption(index)"
>
{{ item }}
</view>
<AppLayout title="新职位推荐" :use-scroll-view="false" :show-bg-image="false">
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</scroll-view>
<view class="main">
<scroll-view class="scroll-view" scroll-y @scrolltolower="scrollBottom">
<view class="list">
<renderJobs
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else></empty>
</template>
<view class="reser-content">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="content-top">
<view
class="top-item button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="changeOption('all')"
>
全部
</view>
<view
class="top-item button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="changeOption(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="main">
<scroll-view class="scroll-view" scroll-y @scrolltolower="scrollBottom">
<view class="list">
<renderJobs
v-if="pageState.list.length"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else></empty>
</view>
</scroll-view>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
const { $api, navTo, debounce, customSystem, navBack } = 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({
@@ -107,9 +114,17 @@ function getList(type = 'add', loading = true) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
image {
height: 100%;
width: 100%;
}
.reser-content{
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
height: 100%;
display: flex;
flex-direction: column;
.content-top{

View File

@@ -82,6 +82,7 @@ const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup');
import { FileValidator } from '@/utils/fileValidator.js'; //文件校验
const percent = ref('0%');
const state = reactive({
@@ -278,15 +279,26 @@ function selectAvatar() {
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('上传失败');
});
success: async (res) => {
const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0];
const imageValidator = new FileValidator();
try {
await imageValidator.validate(file);
$api.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg('上传失败');
});
} catch (error) {
$api.msg(error);
}
},
fail: (error) => {},
});
@@ -392,4 +404,4 @@ function selectAvatar() {
color: #FFFFFF;
text-align: center;
line-height: 90rpx
</style>
</style>

View File

@@ -1,6 +1,6 @@
<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>
@@ -168,7 +168,7 @@
</view>
<view v-else class="btn-wq button-click" @click="jobApply">
<span v-if="jobInfo.isApply">立即前往</span>
<span v-if="!jobInfo.isApply">立即投递</span>
<span v-if="!jobInfo.isApply">立即前往</span>
</view>
</view>
</template>
@@ -183,6 +183,9 @@ 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, hasLogin } = storeToRefs(useUserStore());
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';
@@ -249,7 +252,10 @@ function getDetail(jobId) {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
getCompanyIsAJobs(companyId);
getCompetivetuveness(jobId);
if (hasLogin.value) {
getCompetivetuveness(jobId);
}
if (latitude && longitude) {
initMapCovers(latitude, longitude, companyName);
@@ -282,7 +288,12 @@ 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) => {
const params = {
gsID,
gsmc,
zphID,
};
$api.createRequest(`/app/internal/jobThirdPart`, params).then((resData) => {
companyCount.value = resData.total;
});
} else {

View File

@@ -1,5 +1,11 @@
<template>
<view class="reser-content">
<AppLayout title="我的预约" :use-scroll-view="false" :show-bg-image="false">
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view class="reser-content">
<view class="content-top">
<view
class="top-item"
@@ -12,7 +18,7 @@
</view>
</view>
<view class="main">
<scroll-view class="height-100" scroll-y>
<scroll-view class="height-100" scroll-y @scrolltolower="getList('add')">
<view>
<view class="card" v-for="(item, index) in pageState.list" :key="index">
<view
@@ -38,29 +44,30 @@
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance> -->
</view>
</view>
</view>
</view>
<view class="footer" v-if="isTimePassed(item.zphjbsj)">
<view class="card_cancel" @click="updateCancel(item)">取消预约</view>
<view class="footer" v-if="isTimePassed(item.zphjbsj)">
<view class="card_cancel" @click="updateCancel(item)">取消预约</view>
</view>
</view>
</view>
</view>
<empty v-if="!pageState.list.length"></empty>
</scroll-view>
<empty v-if="!pageState.list.length"></empty>
</scroll-view>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
const { $api, navTo, debounce, customSystem, navBack } = inject('globalFunction');
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({
@@ -94,7 +101,7 @@ onLoad(() => {
function chnageRanOption(item) {
ranItem.value = item;
getList();
getList('refresh');
}
function updateCancel(item) {
@@ -146,9 +153,17 @@ function getList(type = 'add', loading = true) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
image {
height: 100%;
width: 100%;
}
.reser-content{
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
height:100%;
display: flex;
flex-direction: column;
.content-top{

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,7 +26,7 @@
<view
class="item button-click"
:class="{
optional: item.isThisMonth && hasZphInData(item),
optional: (item.isThisMonth && hasZphInData(item)) || hasViewRecordInData(item),
noOptional: !item.isThisMonth,
active: current.date === item.date && item.isThisMonth,
}"
@@ -51,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);
@@ -59,10 +62,10 @@ const pages = reactive({
year: 0,
month: 0,
});
const hasViewRecordDateArray = ref([]);
const hasZphDateArray = ref([]);
onLoad((options) => {
updateDateArray();
if (options.date) {
current.value = {
date: options?.date || null,
@@ -74,6 +77,7 @@ onLoad((options) => {
new Array(recordNum.value + 1).fill(null).map(() => {
addMonth();
});
updateViewRecordDateArray()
} else {
initPagesDate();
new Array(recordNum.value).fill(null).map(() => {
@@ -81,7 +85,7 @@ onLoad((options) => {
});
}
if (options.entrance === 'careerfair') {
updateDateArray();
updateCareerFairDateArray();
}
});
@@ -90,6 +94,17 @@ function hasZphInData(item) {
return false;
}
const dateArray = Array.isArray(hasViewRecordDateArray.value) ? hasViewRecordDateArray.value : [];
return dateArray.some((date) => {
return typeof date === 'string' && date === item.date;
});
}
function hasViewRecordInData(item) {
if (!item || typeof item.date !== 'string') {
return false;
}
const dateArray = Array.isArray(hasZphDateArray.value) ? hasZphDateArray.value : [];
return dateArray.some((date) => {
@@ -97,7 +112,7 @@ function hasZphInData(item) {
});
}
async function updateDateArray() {
async function updateCareerFairDateArray() {
const LoadCache = (resData) => {
if (resData.code === 200) {
hasZphDateArray.value = resData.data;
@@ -105,6 +120,14 @@ async function updateDateArray() {
};
$api.createRequestWithCache('/app/internal/getDateList', {}, 'GET', false, {}, LoadCache).then(LoadCache);
}
async function updateViewRecordDateArray() {
const LoadCache = (resData) => {
if (resData.code === 200) {
hasViewRecordDateArray.value = resData.data;
}
};
$api.createRequestWithCache('/app/user/getJobReviewDate', {}, 'GET', false, {}, LoadCache).then(LoadCache);
}
function backParams() {
if (isValidDateString(current.value.date)) {
@@ -305,6 +328,7 @@ function getMonthCalendarData({ year, month, selectableDates = [] }) {
justify-content: center
border-radius: 20rpx 20rpx 20rpx 20rpx;
padding: 16rpx 0
margin: 0 2rpx;
margin-bottom: 20rpx
.item-top{
font-weight: 600;

View File

@@ -8,7 +8,7 @@
<view class="card-subTitle">{{ item.subTitle }}</view>
<view class="card-btns" v-if="item.noticeType === '2'" @click="seeBussiness(item)">
<view class="btns-text">立即查看</view>
<view class="btns-text"><uni-icons type="forward" color="#256bfa" size="18"></uni-icons></view>
<view class="btns-text"><my-icons type="forward" color="#256bfa" size="32"></my-icons></view>
</view>
</view>
</view>

View File

@@ -1,8 +1,8 @@
<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>
<my-icons type="left" size="52" color="#FFFFFF" @click="navBack"></my-icons>
</view>
</view>
<mTikTok :video-list="state.videoList" :pause-type="1" :controls="false" @loadMore="loadMore" @change="change">
@@ -15,7 +15,7 @@
</text>
<view class="seedetail" @click="nextDetail">
查看详情
<uni-icons type="right" color="#FFFFFF" size="14"></uni-icons>
<my-icons type="right" color="#FFFFFF" size="28"></my-icons>
</view>
</view>
</view>
@@ -31,6 +31,8 @@ import mTikTok from '@/components/TikTok/TikTok.vue';
import useUserStore from '@/stores/useUserStore';
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
import { storeToRefs } from 'pinia';
const { isMiniProgram } = storeToRefs(useUserStore());
const state = reactive({
videoList: [],
});
@@ -95,7 +97,7 @@ const change = (e) => {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: 30rpx;
bottom: calc( var(--window-bottom));
color: #fff;
.title{
font-weight: 500;

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
@@ -112,7 +112,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
</script>

View File

@@ -1,245 +1,250 @@
<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;
font-size: 28rpx;
}
}
.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

@@ -126,7 +126,8 @@
"path": "pages/Intendedposition/Intendedposition",
"style": {
"navigationBarTitleText": "投递记录",
"navigationBarBackgroundColor": "#FFFFFF"
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
@@ -184,7 +185,8 @@
"path": "pages/reservation/reservation",
"style": {
"navigationBarTitleText": "我的预约",
"navigationBarBackgroundColor": "#FFFFFF"
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
@@ -199,7 +201,8 @@
"path": "pages/newJobPosition/newJobPosition",
"style": {
"navigationBarTitleText": "新职位推荐",
"navigationBarBackgroundColor": "#FFFFFF"
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
@@ -221,7 +224,8 @@
"path": "pages/moreJobs/moreJobs",
"style": {
"navigationBarTitleText": "更多岗位",
"navigationBarBackgroundColor": "#FFFFFF"
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
}
]
@@ -277,7 +281,7 @@
// "enablePullDownRefresh": false,
// "navigationStyle": "custom",
"rpxCalcBaseDeviceWidth": 375,
"rpxCalcMaxDeviceWidth": 750,
"rpxCalcMaxDeviceWidth": 960,
"rpxCalcIncludeWidth": 750
},
"uniIdRouter": {}

View File

@@ -1,544 +0,0 @@
<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

@@ -7,12 +7,12 @@
<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 }">
<view v-if="!isMachineEnv" 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>
<my-icons class="iconsearch" color="#666666" type="search" size="36"></my-icons>
<input
v-model="pageState.zphmc"
confirm-type="search"
@@ -45,59 +45,62 @@
<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.zphID +
'&jobFairName=' +
item.zphmc
)
"
>
<view class="card-title">{{ item.zphmc }}</view>
<view class="card-row">
<text class="">{{ item.jbf }}</text>
<text class="">
<!-- <convert-distance
<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.zphjbsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjbsj).date }}</view>
</text>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(item.zphjbsj, item.zphjzsj).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.zphjbsj, item.zphjzsj) }}小时
<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.zphjzsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjzsj).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.zphjj }}</view>
</view>
</transition-group>
</view>
<empty v-if="!fairList.length" content="暂时没有结果,下一天也许就有惊喜"></empty>
</scroll-view>
</view>
<!-- <Tabbar :currentpage="1"></Tabbar> -->
<Tabbar v-show="showTabbar" :currentpage="1"></Tabbar>
</view>
</view>
</template>
@@ -108,8 +111,24 @@ 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());
import useUserStore from '@/stores/useUserStore';
const { isMachineEnv } = storeToRefs(useUserStore());
const { $api, navTo, cloneDeep, debounce } = inject('globalFunction');
import useScreenStore from '@/stores/useScreenStore'
const screenStore = useScreenStore()
const showTabbar = ref(false)
watch(
() => screenStore.isWideScreen,
(newVal) => {
showTabbar.value = newVal
},
{ immediate: true }
)
const weekList = ref([]);
const fairList = ref([]);
const currentDay = ref({});
@@ -140,6 +159,12 @@ onLoad(() => {
getFair('refresh');
});
const handleItemClick = (item) => {
lightAppJssdk.navigation.hide({
url: config.virtualJobFair,
});
};
function toSelectDate() {
navTo('/packageA/pages/selectDate/selectDate', {
query: {
@@ -171,7 +196,8 @@ function changeSwiperMsgType(e) {
function seemsg(index) {
if (index === 1) {
return $api.msg('功能确定中');
handleItemClick();
return;
}
state.current = index;
}
@@ -201,7 +227,7 @@ function getFair(type = 'add') {
if (currentDay.value?.fullDate) {
params.zphjbsj = currentDay.value.fullDate.replace(/-/g, '');
}
$api.createRequest('/app/internal/jobFairThirdPart', params, 'GET', true).then((resData) => {
$api.createRequest('/app/internal/jobFairThirdPart', params, 'GET', false).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
// const str = pageState.pageSize * (pageState.page - 1);

View File

@@ -15,7 +15,7 @@
placeholder-class="input-placeholder"
placeholder="请输入搜索历史对话"
/>
<uni-icons class="input-search" type="search" size="20"></uni-icons>
<my-icons class="input-search" type="search" size="40"></my-icons>
</view>
<scroll-view scroll-y :show-scrollbar="false" class="chat-scroll">
<view
@@ -63,15 +63,15 @@
<ai-paging ref="paging"></ai-paging>
</view>
<!-- 自定义tabbar -->
<!-- <view class="chatmain-footer" v-show="!isDrawerOpen">
<view class="chatmain-footer" v-show="!isDrawerOpen && showTabbar">
<Tabbar :currentpage="2"></Tabbar>
</view> -->
</view>
</view>
</view>
</template>
<script setup>
import { ref, inject, nextTick, computed } from 'vue';
import { ref, inject, nextTick, computed, watch } from 'vue';
const { $api, navTo, insertSortData } = inject('globalFunction');
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
@@ -88,6 +88,19 @@ import config from '@/config';
const searchText = ref('');
const paging = ref(null);
import useScreenStore from '@/stores/useScreenStore';
const screenStore = useScreenStore();
const showTabbar = ref(false);
watch(
() => screenStore.isWideScreen,
(newVal) => {
showTabbar.value = newVal;
},
{ immediate: true }
);
// 实时过滤
const filteredList = computed(() => {
// console.log(tabeList.value);
@@ -111,16 +124,16 @@ onHide(() => {
paging.value?.handleTouchCancel();
if (isDrawerOpen.value) {
isDrawerOpen.value = false;
// uni.showTabBar();
if (!screenStore.isWideScreen) uni.showTabBar();
}
});
const toggleDrawer = () => {
isDrawerOpen.value = !isDrawerOpen.value;
if (isDrawerOpen.value) {
// uni.hideTabBar();
uni.hideTabBar();
} else {
// uni.showTabBar();
if (!screenStore.isWideScreen) uni.showTabBar();
}
};

View File

@@ -44,7 +44,7 @@
class="msg-files btn-light"
v-for="(file, vInex) in msg.files"
:key="vInex"
@click="jumpUrl(file)"
@click="handleMsgFileClick(file)"
>
<image class="msg-file-icon" src="/static/icon/Vector2.png"></image>
<text class="msg-file-text">{{ file.name || '附件' }}</text>
@@ -185,7 +185,7 @@
<!-- btn -->
<CollapseTransition :show="showfile">
<view class="area-tips">
<uni-icons type="info-filled" color="#ADADAD" size="15"></uni-icons>
<my-icons type="info-filled" color="#ADADAD" size="30"></my-icons>
上传后自动解析简历内容
</view>
<view class="area-file">
@@ -210,15 +210,15 @@
<view class="uploadfiles-list">
<view
class="file-uploadsend"
:class="{ 'file-border': isImage(file.type) }"
v-for="(file, index) in filesList"
:key="index"
>
<image
class="file-iconImg"
@click="jumpUrl(file)"
@click="preViewImage(file)"
v-if="isImage(file.type)"
:src="file.url"
mode="heightFix"
></image>
<view class="file-doc" @click="jumpUrl(file)" v-else>
<FileIcon class="doc-icon" :type="file.type"></FileIcon>
@@ -233,7 +233,7 @@
<!-- <image class="file-icon" @click="jumpUrl(file)" :src=""></image> -->
</view>
<view class="file-del" catchtouchmove="true" @click="delfile(file)">
<uni-icons type="closeempty" color="#FFFFFF" size="10"></uni-icons>
<my-icons type="closeempty" color="#FFFFFF" size="20"></my-icons>
</view>
</view>
</view>
@@ -241,6 +241,7 @@
</view>
</view>
<PopupFeeBack ref="feeback" @onClose="colseFeeBack" @onSend="confirmFeeBack"></PopupFeeBack>
<UploadQrcode ref="qrcodeRef" @onSend="handleFileSend" :leaveFileCount="leaveFileCount" :sessionId="chatSessionID"></UploadQrcode>
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
</view>
</template>
@@ -263,20 +264,25 @@ import useChatGroupDBStore from '@/stores/userChatGroupStore';
import MdRender from '@/components/md-render/md-render.vue';
import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue';
import PopupFeeBack from './popupbadFeeback.vue';
import UploadQrcode from './uploadQrcode.vue';
import AudioWave from './AudioWave.vue';
import WaveDisplay from './WaveDisplay.vue';
import FileIcon from './fileIcon.vue';
import FileText from './fileText.vue';
import useScreenStore from '@/stores/useScreenStore'
const screenStore = useScreenStore();
// 系统功能hook和阿里云hook
import { useAudioRecorder } from '@/hook/useRealtimeRecorder2.js';
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer2.js';
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
import successIcon from '@/static/icon/success.png';
import useUserStore from '@/stores/useUserStore';
const { isMachineEnv } = storeToRefs(useUserStore());
import { FileValidator } from '@/utils/fileValidator.js'; //文件校验
// hook
// 语音识别
const {
@@ -314,6 +320,7 @@ const feeBackTips = ref(null);
const state = reactive({
uploadFileTips: '请根据以上附件,帮我推荐岗位。',
});
const qrcodeRef = ref(null);
const statusText = computed(() => {
switch (status.value) {
@@ -334,6 +341,10 @@ const audiowaveStyle = computed(() => {
: '#f1f1f1';
});
const leaveFileCount = computed(()=>{ //还剩多少文件可以上传 给扫码传参使用
return config.allowedFileNumber - filesList.value.length
})
onMounted(async () => {
changeQueries();
scrollToBottom();
@@ -446,7 +457,7 @@ const scrollToBottom = throttle(function () {
}, 500);
function getGuess() {
$api.chatRequest('/app/chat/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
$api.createRequest('/app/chat/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
guessList.value = res.data;
showGuess.value = true;
nextTick(() => {
@@ -456,7 +467,15 @@ function getGuess() {
}
function isImage(type) {
return new RegExp('image').test(type);
if (!type || typeof type !== 'string') return false;
const imageTypes = [
'jpg', 'jpeg', 'png', 'gif', 'bmp',
'webp', 'svg', 'tiff', 'tif', 'ico',
'apng', 'avif', 'heic', 'heif', 'jfif'
];
const lowerType = type.toLowerCase();
return imageTypes.some(item=>lowerType.includes(item))
}
function isFile(type) {
@@ -469,11 +488,40 @@ function isFile(type) {
function jumpUrl(file) {
if (file.url) {
window.open(file.url);
if(!isMachineEnv.value)window.open(file.url);
else $api.msg('该文件无法预览');
} else {
$api.msg('文件地址丢失');
}
}
function preViewImage(file) {
if (file.url) {
uni.previewImage({
urls: [file.url]
})
} else {
$api.msg('文件地址丢失');
}
}
function handleMsgFileClick(file) {
if(isImage(file.type)){
if (file.url) {
uni.previewImage({
urls: [file.url]
})
} else {
$api.msg('文件地址丢失');
}
}else{
if (file.url) {
if(!isMachineEnv.value)window.open(file.url);
else $api.msg('该文件无法预览');
} else {
$api.msg('文件地址丢失');
}
}
}
function VerifyNumberFiles(num) {
if (filesList.value.length >= config.allowedFileNumber) {
@@ -490,21 +538,29 @@ function uploadCamera(type = 'camera') {
count: 1, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: [type], //从相册选择
success: function (res) {
success: async (res)=> {
const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0];
// 继续上传
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
resData = JSON.parse(resData);
if (isImage(file.type)) {
filesList.value.push({
url: resData.msg,
type: file.type,
name: file.name,
});
textInput.value = state.uploadFileTips;
}
});
const imageValidator = new FileValidator()
try {
await imageValidator.validate(file)
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
resData = JSON.parse(resData);
console.log(file.type,'++')
if (isImage(file.type)) {
filesList.value.push({
url: resData.msg,
type: file.type,
name: file.name,
});
textInput.value = state.uploadFileTips;
}
});
} catch (error) {
$api.msg(error)
}
},
});
}
@@ -513,29 +569,58 @@ function getUploadFile(type = 'camera') {
if (VerifyNumberFiles()) return;
uni.chooseFile({
count: 1,
success: (res) => {
success: async(res) => {
const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0];
const allowedTypes = config.allowedFileTypes || [];
const size = $api.formatFileSize(file.size);
if (!allowedTypes.includes(file.type)) {
return $api.msg('仅支持 txt md html word pdf ppt csv excel 格式类型');
}
// 继续上传
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
resData = JSON.parse(resData);
filesList.value.push({
url: resData.msg,
type: file.type,
name: file.name,
size: size,
const imageValidator = new FileValidator({allowedExtensions:config.allowedFileTypes})
try{
await imageValidator.validate(file)
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
resData = JSON.parse(resData);
filesList.value.push({
url: resData.msg,
type: file.type,
name: file.name,
size: size,
});
textInput.value = state.uploadFileTips;
});
textInput.value = state.uploadFileTips;
});
}catch(error){
$api.msg(error)
}
},
});
}
const handleFileSend = (rows)=>{
try {
rows.map(item=>{
if(isImage(item.fileSuffix)){
filesList.value.push({
url: item.fileUrl,
type: item.fileSuffix,
name: item.originalName,
});
}else{
filesList.value.push({
url: item.fileUrl,
type: item.fileSuffix,
name: item.originalName,
});
}
})
textInput.value = state.uploadFileTips;
} catch (error) {
console.log(error)
}
}
const tipsPermisson = () => {
uni.showToast({
title: '需要授权麦克风权限才能使用语音功能',
@@ -597,6 +682,14 @@ function changeVoice() {
}
function changeShowFile() {
if(isMachineEnv.value){
if (filesList.value.length >= config.allowedFileNumber){
return $api.msg(`最大上传文件数量 ${config.allowedFileNumber}`);
}else{
qrcodeRef.value?.open()
return
}
}
showfile.value = !showfile.value;
}
@@ -619,14 +712,14 @@ function confirmFeeBack(value) {
useChatGroupDBStore()
.badFeedback(feebackData.value, value)
.then(() => {
uni.showTabBar()
if (!screenStore.isWideScreen) uni.showTabBar()
feeback.value?.close();
feeBackTips.value?.open();
});
}
function colseFeeBack() {
uni.showTabBar()
if (!screenStore.isWideScreen) uni.showTabBar()
}
function readMarkdown(value, index) {
@@ -898,6 +991,7 @@ image-margin-top = 40rpx
position: relative
background: #F5F5F5;
border-radius: 60rpx 60rpx 60rpx 60rpx;
font-size: 28rpx;
}
.input_vio
flex: 1;
@@ -1009,6 +1103,7 @@ image-margin-top = 40rpx
left: 0
padding: 10rpx 0 10rpx 30rpx
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
z-index:1
.uploadfiles-scroll
height: 100%
.uploadfiles-list
@@ -1023,22 +1118,23 @@ image-margin-top = 40rpx
padding: 16rpx 20rpx 18rpx 20rpx
height: calc(100% - 40rpx)
.doc-icon
width: 60rpx
height: 76rpx
margin-right: 20rpx
width: 60rpx;
height: 76rpx;
margin-right: 20rpx;
.doc-con
flex: 1
width: 0
max-width:320rpx;
overflow :hidden;
padding-right:40rpx;
box-sizing:border-box;
.file-uploadsend
margin: 10rpx 18rpx 0 0;
height: 100%
font-size: 24rpx
position: relative
min-width: 460rpx;
height: 160rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E2E2E2;
overflow: hidden
flex-shrink: 0;
.file-del
position: absolute
right: 25rpx
@@ -1072,7 +1168,7 @@ image-margin-top = 40rpx
max-width: 100%
.file-iconImg
height: 100%
width: 100%
// width: 100%
.filerow
display: flex
align-items: center
@@ -1082,8 +1178,6 @@ image-margin-top = 40rpx
height: 20rpx
width: 2rpx
background: rgba(226, 226, 226, .9)
.file-border
width: 160rpx !important;
@keyframes ai-circle {
0% {

View File

@@ -1,9 +1,11 @@
<template>
<image v-if="type === 'application/pdf'" :src="pdfIcon" class="file-icon" />
<image v-if="type === 'application/pdf' || type === 'pdf'" :src="pdfIcon" class="file-icon" />
<image
v-else-if="
type === 'application/msword' ||
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
type === 'doc' ||
type === 'docx'
"
:src="docIcon"
class="file-icon"
@@ -11,18 +13,22 @@
<image
v-else-if="
type === 'application/vnd.ms-powerpoint' ||
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation'||
type === 'ppt' ||
type === 'pptx'
"
:src="pptIcon"
class="file-icon"
/>
<image v-else-if="type === 'text/markdown'" :src="mdIcon" class="file-icon" />
<image v-else-if="type === 'text/plain'" :src="txtIcon" class="file-icon" />
<image v-else-if="type === 'text/html'" :src="htmlIcon" class="file-icon" />
<image v-else-if="type === 'text/markdown' || type === 'md'" :src="mdIcon" class="file-icon" />
<image v-else-if="type === 'text/plain' || type=== 'txt'" :src="txtIcon" class="file-icon" />
<image v-else-if="type === 'text/html' || type === 'html'" :src="htmlIcon" class="file-icon" />
<image
v-else-if="
type === 'application/vnd.ms-excel' ||
type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'||
type === 'xls' ||
type === 'xlsx'
"
:src="excelIcon"
class="file-icon"

View File

@@ -23,6 +23,25 @@ const fileAbbreviation = computed(() => {
'text/markdown': 'MD',
'text/plain': 'TXT',
'text/html': 'HTML',
'pdf':'PDF',
'doc':'DOC',
'docx':'DOCX',
'ppt':'PPT',
'pptx':'PPTX',
'xls': 'XLS',
'xlsx': 'XLSX',
'md':'MD',
'txt':'TXT',
'html':'HTML',
'jpg':'JPG',
'img':'IMG',
'png':'PNG',
'jpeg':'JPEG',
'gif':'GIF',
'webp':'WEBP',
'svg':'SVG',
'tiff':'TIFF',
};
return typeMap[props.type] || 'OTHER';
});

View File

@@ -0,0 +1,560 @@
<template>
<uni-popup ref="popup" type="center" borderRadius="12px 12px 0 0" @change="changePopup">
<view class="popup-inner">
<view v-show="!fileCount" class="title">请扫码上传附件</view>
<view v-show="!fileCount" class="img-box">
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<uni-load-more status="loading" :icon-size="24" :content-text="loadingText" />
</view>
<canvas canvas-id="qrcode" id="qrcode" />
</view>
<view class="tips" v-if="!loading">
已上传
<span class="num">{{ fileCount }}</span>
个文件
</view>
<view v-show="!fileCount" class="tips" v-if="!loading">请使用手机扫描二维码上传文件</view>
<view v-show="fileCount" class="file-list">
<view v-for="(file, index) in fileList" class="file-item">
<image
class="file-icon"
@click="preViewImage(file)"
v-if="isImage(file.fileSuffix)"
:src="file.fileUrl"
mode="scaleToFill"
></image>
<FileIcon v-else class="file-icon" :type="file.fileSuffix"></FileIcon>
<view class="right">
<view class="file-name">{{ file.originalName }}</view>
<FileText :type="file.fileSuffix"></FileText>
</view>
<view class="remove-btn" @click="delFile(file, index)">×</view>
</view>
</view>
<view v-show="fileCount" class="confirm-btn btn-feel" @click="handleConfirm">确认</view>
<view class="close-btn" @click="close"></view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, inject, onUnmounted, watch, computed } from 'vue';
import FileIcon from './fileIcon.vue';
import FileText from './fileText.vue';
import uQRCode from '@/static/js/qrcode';
import config from '@/config';
import { onShow, onHide } from '@dcloudio/uni-app';
import { UUID } from '../../../lib/uuid-min';
const props = defineProps({
sessionId: {
type: [Number, String],
default: '',
},
leaveFileCount: {
type: [Number, String],
default: 2,
},
});
const emit = defineEmits(['onSend', 'onClose']);
const { $api } = inject('globalFunction');
const popup = ref(null);
const loading = ref(false);
const pollingTimer = ref(null);
const isPolling = ref(false);
const isVisible = ref(false);
const uuid = ref(null);
const fileCount = ref(0);
const fileList = ref([]);
const delFiles = ref([]); //本地记录删除的文件
// 计算加载文本
const loadingText = computed(() => ({
contentdown: '二维码生成中',
contentrefresh: '二维码生成中',
}));
onUnmounted(() => {
stopPolling();
clearResources();
});
function isImage(type) {
if (!type || typeof type !== 'string') return false;
const imageTypes = [
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
'svg',
'tiff',
'tif',
'ico',
'apng',
'avif',
'heic',
'heif',
'jfif',
];
const lowerType = type.toLowerCase();
return imageTypes.some((item) => lowerType.includes(item));
}
function preViewImage(file) {
if (file.fileUrl) {
uni.previewImage({
urls: [file.fileUrl],
});
} else {
$api.msg('文件地址丢失');
}
}
function delFile(file, idx) {
fileList.value.splice(idx, 1);
fileCount.value = fileList.value.length
delFiles.value.push(file.fileUrl);
}
function open() {
uuid.value = UUID.generate();
resetState();
isVisible.value = true;
popup.value.open();
initQrCode();
}
function close() {
isVisible.value = false;
stopPolling();
popup.value.close();
resetState();
}
function changePopup(e) {
if (e.show) {
} else {
stopPolling();
emit('onClose');
}
}
function handleConfirm() {
emit('onSend', fileList.value);
close();
}
// 重置所有状态
function resetState() {
delFiles.value = []
fileList.value = [];
fileCount.value = 0;
loading.value = false;
stopPolling();
}
async function initQrCode() {
try {
loading.value = true;
// 清除之前的二维码
clearCanvas();
await makeQrcode();
// 二维码生成成功后开始轮询
if (isVisible.value) {
startPolling();
}
} catch (error) {
console.error('生成二维码失败:', error);
uni.showToast({
title: '生成二维码失败,请重试',
icon: 'none',
});
close();
} finally {
loading.value = false;
}
}
function makeQrcode() {
const protocol = window.location.protocol;
const host = window.location.host;
const isLocal = host.includes('localhost') || host.includes('127.0.0.1');
// const pathPrefix = isLocal ? '' : '/rgpp-api/all-in-one';
let pathPrefix = '';
if (host.includes('localhost') || host.includes('127.0.0.1')) {
pathPrefix = '';
} else if (host.includes('qd.zhaopinzao8dian.com')) {
// 外网测试环境
pathPrefix = '/app';
} else if (host.includes('fw.rc.qingdao.gov.cn')) {
// 青岛政务网环境
pathPrefix = '/rgpp-api/all-in-one';
} else {
pathPrefix = '';
}
const htmlPath = `${protocol}//${host}${pathPrefix}/static/upload.html?sessionId=${uuid.value}&uploadApi=${config.baseUrl}/app/kiosk/upload&fileCount=${props.leaveFileCount}`;
// const htmlPath = `${window.location.host}/static/upload.html?sessionId=${uuid.value}&uploadApi=${
// config.baseUrl + '/app/kiosk/upload'
// }`;
// const htmlPath = `${window.location.host}/static/upload.html?sessionId=${props.sessionId}&uploadApi=${
// config.baseUrl + '/app/kiosk/upload'
// }`;
console.log(htmlPath);
console.log('剩余可上传文件数量:',props.leaveFileCount)
return new Promise((resolve, reject) => {
setTimeout(() => {
uQRCode.make({
canvasId: 'qrcode',
text: htmlPath,
size: uni.upx2px(300),
margin: 0,
backgroundColor: '#ffffff',
foregroundColor: '#1677ff',
fileType: 'png',
correctLevel: uQRCode.defaults.correctLevel,
});
resolve();
}, 100);
});
}
// 清除画布
function clearCanvas() {
const ctx = uni.createCanvasContext('qrcode');
ctx.clearRect(0, 0, uni.upx2px(300), uni.upx2px(300));
ctx.draw();
}
function startPolling() {
if (isPolling.value) return;
isPolling.value = true;
console.log('开始轮询');
// 轮询检查上传状态
const poll = async () => {
if (!isPolling.value || !isVisible.value) return;
const { data } = await $api.createRequest('/app/kiosk/list', { sessionId: uuid.value });
// const { data } = await $api.createRequest('/app/kiosk/list',{sessionId:props.sessionId});
if (data && data.length) {
// 上传完成,触发事件
fileList.value = data.filter((item) => !delFiles.value.includes(item.fileUrl))
fileCount.value = fileList.value.length;
// emit('onSend', data);
}
if (isPolling.value && isVisible.value) {
pollingTimer.value = setTimeout(poll, 2000); // 每2秒轮询一次
}
};
poll();
}
function stopPolling() {
isPolling.value = false;
if (pollingTimer.value) {
clearTimeout(pollingTimer.value);
pollingTimer.value = null;
}
console.log('停止轮询');
}
function clearResources() {
stopPolling();
clearCanvas();
}
defineExpose({ open, close });
</script>
<style lang="scss" scoped>
.popup-inner {
padding: 40rpx 30rpx 50rpx;
width: 520rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
border-radius: 20rpx;
background: linear-gradient(135deg, #fafafa 0%, #f0f7ff 100%);
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
}
.title {
font-size: 32rpx;
color: #1a1a1a;
margin-bottom: 30rpx;
font-weight: 600;
text-align: center;
position: relative;
padding-bottom: 16rpx;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: linear-gradient(90deg, #4191fe 0%, #256bfa 100%);
border-radius: 2rpx;
}
}
.img-box {
margin: 0 auto 30rpx;
width: 300rpx;
height: 300rpx;
background: linear-gradient(145deg, #ffffff 0%, #f8faff 100%);
border-radius: 16rpx;
overflow: hidden;
position: relative;
box-shadow: 0 8rpx 32rpx rgba(65, 145, 254, 0.2);
padding: 20rpx;
border: 2rpx solid rgba(65, 145, 254, 0.1);
canvas {
width: 100%;
height: 100%;
border-radius: 8rpx;
transition: all 0.3s ease;
animation: canvasFadeIn 0.5s ease;
}
}
@keyframes canvasFadeIn {
0% {
opacity: 0;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20rpx);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.file-list {
margin-top: 30rpx;
width: 470rpx;
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
padding-right: 10rpx;
.file-item {
width: 100%;
padding: 25rpx;
padding-right: 15rpx;
box-sizing: border-box;
border: 1px solid #bbc5d1;
display: flex;
align-items: center;
animation: fadeIn 0.5s ease;
background: linear-gradient(135deg, #ffffff 0%, #f0f8ff 100%);
border-radius: 16rpx;
margin-bottom: 20rpx;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 6rpx;
height: 100%;
background: linear-gradient(180deg, #4191fe 0%, #256bfa 100%);
border-radius: 3rpx 0 0 3rpx;
}
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 8rpx 24rpx rgba(65, 145, 254, 0.15);
}
.file-icon {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease;
&:active {
transform: scale(0.95);
}
}
.right {
flex: 1;
overflow: hidden;
min-width: 0;
.file-name {
font-size: 30rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 8rpx;
transition: color 0.3s ease;
}
.file-size {
font-size: 24rpx;
color: #838383;
}
}
.remove-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 52rpx;
cursor: pointer;
margin-left: 20rpx;
flex-shrink: 0;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
margin-top: -15rpx;
&:active {
background-color: rgba(255, 107, 107, 0.1);
transform: scale(0.9);
}
}
}
}
.confirm-btn {
font-weight: 500;
font-size: 32rpx;
color: #ffffff;
text-align: center;
width: 350rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(135deg, #4191fe 0%, #256bfa 100%);
border-radius: 16rpx;
margin-top: 30rpx;
transition: all 0.3s ease;
box-shadow: 0 8rpx 24rpx rgba(37, 107, 250, 0.3);
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s ease;
}
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 16rpx rgba(37, 107, 250, 0.4);
&::after {
left: 100%;
}
}
}
.loading-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #ffffff 0%, #f8faff 100%);
border-radius: 16rpx;
}
.tips {
font-size: 26rpx;
color: #666;
text-align: center;
line-height: 1.6;
padding: 0 20rpx;
margin-top: 10rpx;
animation: fadeIn 0.5s ease;
.num {
color: #4191fe;
font-size: 28rpx;
font-weight: 600;
margin: 0 8rpx;
position: relative;
}
}
.close-btn {
position: absolute;
right: 20rpx;
top: 20rpx;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
z-index: 10;
&:active {
transform: scale(0.9) rotate(90deg);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
&::before,
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 24rpx;
height: 2rpx;
background: #5a5a68;
border-radius: 1rpx;
transition: all 0.3s ease;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
</style>

View File

@@ -1,9 +1,25 @@
<template>
<view class="container" id="pixi-box" ref="pixiContainerRef"></view>
<view class="out">
<view v-if="loading" class="loading">
<view class="semicircle-loader"></view>
</view>
<view v-else class="container" id="pixi-box" ref="pixiContainerRef"></view>
</view>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick } from 'vue';
import { onMounted, onUnmounted, ref, nextTick, watch } from 'vue';
const props = defineProps({
tags: {
type: Array,
default: [],
},
loading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['tag-click']);
// DOM Ref
@@ -15,10 +31,9 @@ let tagsContainer = null;
let activeTagInstances = [];
// 配置数据
const mockTags = [
{ name: '医生', bgColor: 0x0069fe, fontColor: 0xffffff, size: 17, opacity: 1.0, angle: 0, radius: 0 },
const tagsConfig = ref([
{ bgColor: 0x0069fe, fontColor: 0xffffff, size: 16, opacity: 1.0, angle: 0, radius: 0 },
{
name: '工程师',
bgColor: 0x87e2ec,
fontColor: 0xffffff,
size: 14,
@@ -28,7 +43,6 @@ const mockTags = [
tailRotation: Math.PI / 2,
},
{
name: '建筑师',
bgColor: 0xffebeb,
tailColor: 0xffe1e1,
fontColor: 0xff6969,
@@ -36,62 +50,47 @@ const mockTags = [
opacity: 1,
angle: -Math.PI / 4.2,
radius: 125,
tailRotation: (3 * Math.PI) / 4,
tailRotation: (3 * Math.PI) / 4.5,
},
{
name: '律师',
bgColor: 0x21ea85,
fontColor: 0xffffff,
size: 15,
opacity: 1,
angle: -Math.PI / 10,
radius: 130,
tailRotation: (3 * Math.PI) / 4,
tailRotation: (3 * Math.PI) / 4.2,
},
{
name: '记者',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
size: 12,
opacity: 1,
angle: Math.PI / 120,
radius: 130,
tailRotation: (3 * Math.PI) / 3.4,
angle: Math.PI / 18,
radius: 135,
tailRotation: (3 * Math.PI) / 4.3,
},
{
name: '程序员',
bgColor: 0xffd4b6,
fontColor: 0xffffff,
size: 14,
opacity: 1,
angle: Math.PI / 7,
radius: 120,
tailRotation: (5 * Math.PI) / 4,
angle: Math.PI / 4.3,
radius: 100,
tailRotation: -(3 * Math.PI) / 4.5,
},
{
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,
tailRotation: -Math.PI / 2.4,
},
{
name: '心理咨询师',
bgColor: 0xebf3ff,
tailColor: 0xb9d3ff,
fontColor: 0x1d71ef,
@@ -99,35 +98,45 @@ const mockTags = [
opacity: 1,
angle: (5.4 * Math.PI) / 6,
radius: 110,
tailRotation:(3 * Math.PI) /1.78,
tailRotation: (3 * Math.PI) / 1.79,
},
{
name: '护士',
bgColor: 0xff6969,
fontColor: 0xffffff,
size: 15,
size: 14,
opacity: 1,
angle: (6.3 * Math.PI) / 5.9,
radius: 110,
tailRotation: Math.PI / 4,
angle: (6.3 * Math.PI) / 5.8,
radius: 120,
tailRotation: Math.PI / 2.9,
},
{
name: '会计',
bgColor: 0xfce9c9,
fontColor: 0xfbc55f,
size: 13,
opacity: 1,
angle: (7.2 * Math.PI) / 5.9,
radius: 120,
tailRotation: Math.PI / 4,
tailRotation: Math.PI / 3,
},
];
]);
watch(
() => props.tags,
(val) => {
if (val && val.length) {
tagsConfig.value.map((tag, index) => {
// console.log(val[index])
tag.name = val[index]?.job_name;
});
setTimeout(() => {
initPixi();
}, 100);
}
},
{ deep: true }
);
onMounted(async () => {
await nextTick();
setTimeout(() => {
initPixi();
}, 100);
window.addEventListener('resize', handleResize);
});
@@ -177,19 +186,18 @@ const initPixi = () => {
};
const renderScene = (sw, sh) => {
let ratio = window.innerWidth / 400;
if (ratio < 1) ratio = 1;
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);
tagsConfig.value.forEach((data, index) => {
const scaledRadius = data.radius * ratio;
let x = sw / 2 + scaledRadius * Math.cos(data.angle);
let y = sh / 2 + scaledRadius * Math.sin(data.angle);
const tag = createTag(data, index);
const tag = createTag(data, index, ratio);
tagsContainer.addChild(tag);
@@ -216,7 +224,13 @@ const renderScene = (sw, sh) => {
};
if (data.radius > 0) {
const tail = createCometTail( data.tailColor || data.bgColor, data.tailRotation, tag.width);
const tail = createCometTail(
data.tailColor || data.bgColor,
data.tailRotation,
tag.width,
tag.height,
ratio
);
tag.addChildAt(tail, 0);
tag.updateTail = () => tail.updateAnim();
}
@@ -227,7 +241,6 @@ const renderScene = (sw, sh) => {
// 动画循环
app.ticker.add(() => {
const screenH = app.screen.height;
activeTagInstances.forEach((tag) => {
const meta = tag.userData;
if (meta) {
@@ -247,7 +260,8 @@ const renderScene = (sw, sh) => {
});
};
const createTag = (tagData, index) => {
const createTag = (tagData, index, ratio) => {
if (ratio > 1) ratio = ratio * 0.9;
const tagGroup = new PIXI.Container();
tagGroup.eventMode = 'static';
tagGroup.cursor = 'pointer';
@@ -256,15 +270,15 @@ const createTag = (tagData, index) => {
const text = new PIXI.Text(tagData.name, {
fontFamily: ['PingFang SC', 'Microsoft YaHei', 'Arial'],
fontSize: tagData.size,
fontSize: tagData.size * ratio,
fill: tagData.fontColor,
padding: 4,
padding: 4 * ratio,
resolution: 2,
});
text.anchor.set(0.5);
const paddingH = 26;
const paddingV = 10;
const paddingH = 26 * ratio;
const paddingV = 10 * ratio;
let bgWidth = text.width + paddingH;
let bgHeight = text.height + paddingV;
@@ -281,14 +295,15 @@ const createTag = (tagData, index) => {
return tagGroup;
};
const createCometTail = (bgColor, tailRotation, parentWidth) => {
const createCometTail = (bgColor, tailRotation, parentWidth, parentHeight, ratio) => {
if (ratio > 1) ratio = ratio;
const tailGroup = new PIXI.Container();
const graphics = new PIXI.Graphics();
tailGroup.addChild(graphics);
const baseLength = 45;
const startWidth = parentWidth * 0.6;
const endWidth = 20;
const baseLength = 45 * ratio;
const startWidth = parentHeight + 20 * ratio;
const endWidth = parentHeight * 0.4;
let breathPhase = Math.random() * Math.PI * 2;
const breathSpeed = 0.04;
@@ -335,35 +350,44 @@ const createCometTail = (bgColor, tailRotation, parentWidth) => {
const handleResize = () => {
const container = getContainerDOM();
if (!app || !container) return;
const w = container.clientWidth || 300;
const h = container.clientHeight || 300;
app.renderer.resize(w, h);
activeTagInstances.forEach((tag) => {
const meta = tag.userData;
if (!meta) return;
let newX = w / 2 + meta.radius * Math.cos(meta.angle);
let newY = h / 2 + meta.radius * Math.sin(meta.angle);
const safeW = tag.width / 2 + 10;
const safeH = tag.height / 2 + 10;
meta.originalX = clamp(newX, safeW, w - safeW);
meta.originalY = clamp(newY, safeH, h - safeH);
meta.safeH = safeH; // 更新安全高度
tag.x = meta.originalX;
});
app.destroy(true, { children: true, texture: true, baseTexture: true });
app = null;
initPixi();
};
</script>
<style scoped>
.out {
width: 100%;
height: 100%;
}
.loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.semicircle-loader {
width: 150rpx;
height: 150rpx;
border: 12rpx solid #f3f3f3;
border-top: 12rpx solid #3498db;
border-radius: 50%;
animation: spin 1.2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.container {
width: 100%;
height: 500rpx;
height: 100%;
position: relative;
overflow: hidden;
color: #b9d3ff;

View File

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

View File

@@ -11,7 +11,12 @@
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
<view class="search-inner">
<view class="inner-left">
<image class="bg-text2" mode="widthFix" src="@/static/icon/index-text-bg2.png"></image>
<image
class="bg-text2"
mode="widthFix"
@click="reloadBrowser()"
src="@/static/icon/index-text-bg2.png"
></image>
<view class="search-input button-click" @click="navTo('/pages/search/search')">
<image class="icon" src="@/static/icon/index-search.png"></image>
<text class="inpute">请告诉我想找什么工作</text>
@@ -20,7 +25,7 @@
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
</view>
</view>
<view class="ai-card-out">
<view v-if="!isMachineEnv" class="ai-card-out">
<view class="ai-card">
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
<view class="ai-card-inner">
@@ -49,24 +54,23 @@
</view>
</view>
</view>
<view class="tip" @click="navTo('/packageA/pages/myResume/myResume')">
<view class="tip button-click" @click="navTo('/packageA/pages/myResume/myResume')">
<image class="icon" src="@/static/icon/leart-gold.png" />
<view class="text">使用人工智能需要先完成素质测评完善简历 ></view>
</view>
</view>
</view>
</view>
<view class="match-card-out">
<view v-if="hasLogin" :class="{ 'match-move-top': isMachineEnv }" class="match-card-out">
<view class="match-card">
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
<view class="title">简历匹配职位</view>
<view class="match-item-container">
<AIMatch @tag-click="handleTagClick"></AIMatch>
<AIMatch :tags="matchTags" :loading="matchLoading" @tag-click="handleTagClick"></AIMatch>
</view>
</view>
</view>
<view class="cards">
<view :class="{ 'cards-move-top': isMachineEnv && !hasLogin }" class="cards">
<view class="card card1 press-button" @click="navTo('/pages/nearby/nearby')">
<view class="card-title">附近工作</view>
<view class="card-text">好岗职等你来</view>
@@ -209,7 +213,7 @@
</view>
<!-- <view class="falls-card-matchingrate">
<view class=""><matchingDegree :job="job"></matchingDegree></view>
<uni-icons type="star" size="30"></uni-icons>
<my-icons type="star" size="60"></my-icons>
</view> -->
</view>
<view v-if="!job.education" class="recommend-card" :class="{ isBut: job.isBut }">
@@ -233,7 +237,7 @@
<loadmore ref="loadmoreRef"></loadmore>
<view v-if="showScrollBottom" style="height: 200px"></view>
</view>
<empty v-else pdTop="200"></empty>
<empty v-else is-position></empty>
</view>
</view>
</view>
@@ -255,11 +259,11 @@
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue';
import img from '@/static/icon/filter.png';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
const { $api, navTo, vacanciesTo, formatTotal, throttle } = inject('globalFunction');
const { $api, navTo, vacanciesTo, formatTotal, throttle, reloadBrowser } = inject('globalFunction');
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, hasLogin, isMachineEnv } = storeToRefs(useUserStore());
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren, oneDictData } = useDictStore();
import useLocationStore from '@/stores/useLocationStore';
@@ -280,6 +284,9 @@ const isSticky = ref(false);
const showScrollBottom = ref(false);
const scrollTop = ref(0);
const matchTags = ref([]);
const matchLoading = ref(false);
const emits = defineEmits(['onShowTabbar']);
const waterfallsFlowRef = ref(null);
@@ -311,6 +318,8 @@ const rangeOptions = ref([
]);
const isLoaded = ref(false);
const colors = ['#0069FE', '#FF9400', '#FF6969', '#21EA85', '#87E2EC'];
const occupations = [
'律师',
'工程师',
@@ -354,13 +363,42 @@ const occupations = [
'导游',
];
const colors = ['#0069FE', '#FF9400', '#FF6969', '#21EA85', '#87E2EC'];
onMounted(() => {
let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读
maskFirstEntry.value = firstEntry;
getMatchTags();
// console.log(isMachineEnv.value, '+++++++++');
});
async function getMatchTags() {
if (hasLogin.value) {
try {
matchLoading.value = true;
const { data } = await $api.createRequest('/app/user/getJobAdviceByResume');
matchTags.value = data;
matchLoading.value = false;
} catch (err) {
console.error('简历匹配职位获取失败');
}
} else {
const vals = getRandomStrings(occupations).map((text) => ({
job_name: text,
score: 0,
}));
matchTags.value = vals;
}
}
function getRandomStrings(arr, count = 10) {
const size = Math.min(arr.length, count);
const shuffled = [...arr];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled.slice(0, size);
}
const checkStickyStatus = (e) => {
scrollTop.value = e.detail.scrollTop;
nextTick(() => {
@@ -387,7 +425,7 @@ function closeVideoTip() {
const handleTagClick = (tagInfo) => {
console.log('点击的标签信息:', tagInfo);
$api.msg('暂未开放');
navTo(`/pages/search/search?keyWord=${encodeURIComponent(tagInfo.name)}`);
};
const handleItemClick = (item) => {
@@ -402,7 +440,11 @@ const handleItemClick = (item) => {
url: config.Career_guidance,
});
break;
case '模拟面试':
lightAppJssdk.navigation.hide({
url: config.mock_interview,
});
break;
default:
$api.msg('暂未开放');
}
@@ -428,7 +470,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
getJobRecommend('refresh');
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
});
});
@@ -537,6 +578,7 @@ function openFilter() {
}
showFilter.value = false;
getJobList('refresh');
emits('onShowTabbar', true);
},
cancel: () => {
showFilter.value = false;
@@ -852,6 +894,12 @@ defineExpose({ loadData });
height: 36rpx;
color: #000000;
padding: 20rpx 30rpx
.match-move-top
margin-top:-20rpx !important;
.cards-move-top
margin-top:-60rpx !important;
position:relative;
z-index:1
.ai-card-out
width:100%;
box-sizing:border-box
@@ -1009,6 +1057,7 @@ defineExpose({ loadData });
top:0;
.match-item-container
width:100%;
height:400rpx;
margin-top:30rpx;
position :relative;
z-index:1
@@ -1214,6 +1263,8 @@ defineExpose({ loadData });
.falls-scroll
width: 100%
height: 100%
min-height: 100vh;
position: relative
.falls
padding: 28rpx 28rpx;
.item,.m-height{

View File

@@ -23,7 +23,7 @@
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/pages/search/search')">
<uni-icons class="iconsearch" color="#666D7F" type="search" size="18"></uni-icons>
<my-icons class="iconsearch" color="#666D7F" type="search" size="36"></my-icons>
<text>搜索</text>
</view>
</view>
@@ -55,7 +55,7 @@
<view class="title">{{ job.jobTitle }}</view>
<view class="desc">
<image class="point3" src="/static/icon/point3.png"></image>
<!-- <uni-icons type="location" size="14"></uni-icons> -->
<!-- <my-icons type="location" size="28"></my-icons> -->
<view class="descText">{{ job.companyName }}</view>
</view>
</view>
@@ -129,7 +129,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
pageSize.value = 10 * (columnCount.value - 1);
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
});
});

View File

@@ -18,30 +18,16 @@
v-for="(_, index) in 2"
:key="index"
>
<!-- #ifndef MP-WEIXIN -->
<component
:is="components[index]"
@onShowTabbar="changeShowTabbar"
:ref="(el) => handelComponentsRef(el, index)"
/>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<IndexOne
v-show="currentIndex === 0"
@onShowTabbar="changeShowTabbar"
:ref="(el) => handelComponentsRef(el, index)"
/>
<IndexTwo
v-show="currentIndex === 1"
@onShowTabbar="changeShowTabbar"
:ref="(el) => handelComponentsRef(el, index)"
/>
<!-- #endif -->
</swiper-item>
</swiper>
</view>
<!-- <Tabbar v-show="showTabbar" :currentpage="0"></Tabbar> -->
<Tabbar v-show="showTabbar" :currentpage="0"></Tabbar>
<!-- maskFristEntry -->
<view class="maskFristEntry" v-if="maskFristEntry">
@@ -61,16 +47,19 @@ import { reactive, inject, watch, ref, onMounted } from 'vue';
import Tabbar from '@/components/tabbar/midell-box.vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import IndexRefactor from './components/index-refactor.vue';
import IndexOne from './components/index-one.vue';
import IndexTwo from './components/index-two.vue';
import useScreenStore from '@/stores/useScreenStore';
const screenStore = useScreenStore();
const { isWideScreen } = screenStore;
const loadedMap = reactive([false, false]);
const swiperRefs = [ref(null), ref(null)];
// const components = [IndexOne, IndexTwo];
const components = [IndexRefactor, IndexTwo];
import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
const { unreadCount } = storeToRefs(useReadMsg());
const showTabbar = ref(true);
const showTabbar = ref(false);
const maskFristEntry = ref(false);
const disableTouch = ref(false);
@@ -78,12 +67,23 @@ const startPointX = ref(0);
const totalPage = 2;
const THRESHOLD = 5;
watch(
() => screenStore.isWideScreen,
(newVal) => {
showTabbar.value = newVal;
},
{ immediate: true }
);
onLoad(() => {
// 判断浏览器是否有 fristEntry 第一次进入
let fristEntry = uni.getStorageSync('fristEntry') === false ? false : true; // 默认未读
maskFristEntry.value = fristEntry;
if (fristEntry) {
uni.hideTabBar();
if (!isWideScreen) uni.hideTabBar();
else showTabbar.value = false;
} else {
if (isWideScreen) showTabbar.value = true;
}
// 预加载较重页面
setTimeout(() => {
@@ -91,7 +91,9 @@ onLoad(() => {
uni.preloadPage({ url: '/pages/nearby/nearby' });
uni.preloadPage({ url: '/pages/chat/chat' });
uni.preloadPage({ url: '/packageA/pages/choiceness/choiceness' });
}, 3000);
uni.preloadPage({ url: '/packageA/pages/reservation/reservation' });
uni.preloadPage({ url: '/packageA/pages/Intendedposition/Intendedposition' });
}, 1000);
});
onShow(() => {
@@ -143,7 +145,12 @@ function handleTouchMove(e) {
}
function changeShowTabbar(val) {
showTabbar.value = val;
if (isWideScreen) {
showTabbar.value = val;
} else {
if (val) uni.showTabBar();
else uni.hideTabBar();
}
}
//1 查看消息类型
@@ -198,12 +205,14 @@ function changeSwiperMsgType(e) {
function closeFristEntry() {
uni.setStorageSync('fristEntry', false);
maskFristEntry.value = false;
uni.showTabBar();
if (!isWideScreen) uni.showTabBar();
else showTabbar.value = true;
}
function goExperience() {
closeFristEntry();
uni.showTabBar();
if (!isWideScreen) uni.showTabBar();
else showTabbar.value = true;
state.current = 1;
}
</script>
@@ -366,4 +375,4 @@ function goExperience() {
background: #FFFFFF
width: 4rpx
height: 20rpx
</style>
</style>

View File

@@ -1,6 +1,78 @@
<template>
<AppLayout title="AI+就业服务程序">
<tabcontrolVue :current="tabCurrent">
<AppLayout title="就业服务程序">
<view v-if="isMachineEnv" class="alipay-login-container">
<!-- 切换 -->
<view class="login-method-switch">
<view
class="method-item"
:class="{ active: loginMethod === 'face' }"
@click="switchLoginMethod('face')"
>
扫脸登录
</view>
<view
class="method-item"
:class="{ active: loginMethod === 'qrcode' }"
@click="switchLoginMethod('qrcode')"
>
扫码登录
</view>
</view>
<view class="login-scan-area">
<view class="login-title">{{ loginMethod === 'qrcode' ? '扫码登录' : '扫脸登录' }}</view>
<view class="qrcode-tips">
<text class="tips-text">
{{ loginMethod === 'qrcode' ? '请将二维码对准机器下方' : '请将面部对准摄像头区域' }}
</text>
</view>
</view>
<!-- 扫码登录 -->
<view v-if="loginMethod === 'qrcode'" class="qrcode-container">
<view class="qrcode-wrapper">
<view class="qrcode-border">
<view class="qrcode-content">
<view class="qrcode-pattern">
<image class="qrcode-img" src="@/static/icon/qrcode.png" />
<view class="qrcode-corner top-left"></view>
<view class="qrcode-corner top-right"></view>
<view class="qrcode-corner bottom-left"></view>
<view class="qrcode-corner bottom-right"></view>
<view class="scan-line" :style="{ top: scanLineTop + 'rpx' }"></view>
</view>
</view>
</view>
</view>
</view>
<!-- 扫脸登录 -->
<view v-else class="face-container">
<view class="face-wrapper">
<view class="face-border">
<view class="face-content">
<view class="face-pattern">
<image class="face-img" src="@/static/icon/face-icon.png" />
<view class="face-scan-arc arc-1"></view>
<view class="face-scan-arc arc-2"></view>
<view class="face-scan-arc arc-3"></view>
<view class="scan-line" :style="{ top: scanLineTop + 'rpx' }"></view>
</view>
</view>
</view>
</view>
</view>
<view class="countdown-container">
<view class="countdown-wrapper">
<text class="countdown-number">{{ countdown }}</text>
<text class="countdown-text">秒后自动返回</text>
</view>
<view class="cancel-btn" @click="cancelLogin">取消登录</view>
</view>
</view>
<!-- 正常登录-->
<tabcontrolVue v-else :current="tabCurrent">
<template v-slot:tab0>
<view class="login-content">
<image class="logo" src="@/static/logo.png"></image>
@@ -109,21 +181,24 @@
</template>
</tabcontrolVue>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<view class="backdoor" @click="loginbackdoor">
<uni-icons type="gift-filled" size="30"></uni-icons>
<!-- 后门 -->
<view class="backdoor" v-if="!isMachineEnv" @click="loginbackdoor">
<my-icons type="gift-filled" size="60"></my-icons>
</view>
</AppLayout>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import tabcontrolVue from './components/tabcontrol.vue';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { $api, navTo } = inject('globalFunction');
const { loginSetToken, getUserResume } = useUserStore();
const { isMachineEnv } = storeToRefs(useUserStore());
const { getDictSelectOption, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup');
// status
@@ -151,11 +226,85 @@ const fromValue = reactive({
experience: '1',
});
// 扫码扫脸登录
const scanLineTop = ref(0);
let scanInterval = null;
const countdown = ref(60);
let countdownTimer = null;
const loginMethod = ref('face'); // 'qrcode' / 'face'
onLoad((parmas) => {
getTreeselect();
if (!isMachineEnv.value) $api.msg('请完善微简历');
});
onMounted(() => {});
onMounted(() => {
if (isMachineEnv) {
startCountdown();
startScanAnimation();
}
});
onUnmounted(() => {
stopScanAnimation();
stopCountdown();
});
const startCountdown = () => {
stopCountdown();
countdown.value = 60;
countdownTimer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
returnToHome();
}
}, 1000);
};
const stopCountdown = () => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
};
const resetCountdown = () => {
stopCountdown();
startCountdown();
};
// 返回首页
const returnToHome = () => {
stopCountdown();
stopScanAnimation();
useUserStore().logOutApp();
};
// 取消登录
const cancelLogin = () => {
returnToHome();
};
// 切换登录方式
const switchLoginMethod = (method) => {
if (loginMethod.value !== method) {
loginMethod.value = method;
resetCountdown();
}
};
// 开始动画
const startScanAnimation = () => {
clearInterval(scanInterval);
scanInterval = setInterval(() => {
scanLineTop.value = (scanLineTop.value + 2) % 300;
}, 30);
};
// 停止动画
const stopScanAnimation = () => {
clearInterval(scanInterval);
};
function changeSex(sex) {
fromValue.sex = sex;
@@ -248,6 +397,11 @@ function getTreeselect() {
}
function loginbackdoor() {
if (isMachineEnv.value) {
useUserStore().logOutApp();
$api.msg('返回首页');
return;
}
$api.createRequest('/app/mock/login', {}, 'post').then((resData) => {
$api.msg('模拟帐号密码测试登录成功');
loginSetToken(resData.token).then((resume) => {
@@ -283,7 +437,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,6 +463,316 @@ function complete() {
}
</script>
<style lang="scss" scoped>
.alipay-login-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
box-sizing: border-box;
}
/* 登录方式切换 */
.login-method-switch {
display: flex;
width: 80%;
margin-bottom: 40rpx;
background: #f5f5f5;
border-radius: 50rpx;
padding: 6rpx;
}
.method-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
border-radius: 50rpx;
transition: all 0.3s;
&.active {
background: #1677ff;
color: #fff;
font-weight: 500;
}
}
/* 扫码登录区域样式 */
.login-scan-area {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
}
.login-title {
font-size: 36rpx;
font-weight: 600;
color: #1677ff;
margin-bottom: 20rpx;
text-align: center;
}
.qrcode-tips {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.tips-text {
font-size: 28rpx;
color: #333;
}
/* 二维码容器 */
.qrcode-container {
width: 100%;
display: flex;
justify-content: center;
}
.qrcode-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.qrcode-border {
width: 400rpx;
height: 400rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 20rpx;
box-shadow: 0 8rpx 30rpx rgba(22, 119, 255, 0.1);
position: relative;
}
.qrcode-content {
width: 100%;
height: 100%;
background-color: #f8f8f8;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.qrcode-pattern {
width: 300rpx;
height: 300rpx;
background-color: #fff;
position: relative;
padding: 30rpx;
}
.qrcode-img {
width: 100%;
height: 100%;
}
.qrcode-corner {
position: absolute;
width: 60rpx;
height: 60rpx;
border: 6rpx solid #1677ff;
&.top-left {
top: 0;
left: 0;
border-right: none;
border-bottom: none;
border-radius: 10rpx 0 0 0;
}
&.top-right {
top: 0;
right: 0;
border-left: none;
border-bottom: none;
border-radius: 0 10rpx 0 0;
}
&.bottom-left {
bottom: 0;
left: 0;
border-right: none;
border-top: none;
border-radius: 0 0 0 10rpx;
}
&.bottom-right {
bottom: 0;
right: 0;
border-left: none;
border-top: none;
border-radius: 0 0 10rpx 0;
}
}
.scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6rpx;
background: linear-gradient(90deg, transparent, #217bf9, transparent);
box-shadow: 0 -6rpx 4rpx #ffffff;
z-index: 10;
}
/* 扫脸登录样式 */
.face-container {
width: 100%;
display: flex;
justify-content: center;
}
.face-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.face-border {
width: 400rpx;
height: 400rpx;
background-color: #fff;
border-radius: 50%;
padding: 20rpx;
box-shadow: 0 8rpx 30rpx rgba(22, 119, 255, 0.1);
position: relative;
}
.face-content {
width: 100%;
height: 100%;
background-color: #f8f8f8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.face-pattern {
width: 300rpx;
height: 300rpx;
position: relative;
}
.face-img {
width: 100%;
height: 100%;
}
.face-scan-arc {
position: absolute;
border: 4rpx solid transparent;
border-top-color: #1677ff;
border-radius: 50%;
width: 300rpx;
height: 300rpx;
top: 0rpx;
left: 0rpx;
&.arc-1 {
animation: faceScanRotate1 3s linear infinite;
}
&.arc-2 {
transform: rotate(120deg);
animation: faceScanRotate2 3s linear infinite;
}
&.arc-3 {
transform: rotate(240deg);
animation: faceScanRotate3 3s linear infinite;
}
}
@keyframes faceScanRotate1 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes faceScanRotate2 {
0% {
transform: rotate(120deg);
}
100% {
transform: rotate(480deg);
}
}
@keyframes faceScanRotate3 {
0% {
transform: rotate(240deg);
}
100% {
transform: rotate(600deg);
}
}
/* 底部操作区域 */
.bottom-action-area {
width: 100%;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 30rpx;
}
.countdown-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.countdown-number {
font-size: 40rpx;
font-weight: 600;
color: #1677ff;
min-width: 60rpx;
text-align: center;
}
.countdown-text {
font-size: 28rpx;
color: #666;
}
.cancel-btn {
margin-top: 20rpx;
width: 300rpx;
height: 80rpx;
background: transparent;
border: 2rpx solid #1677ff;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #1677ff;
font-weight: 500;
transition: all 0.3s;
&:active {
background: rgba(22, 119, 255, 0.1);
}
}
</style>
<style lang="stylus" scoped>
.backdoor{
position: fixed;

View File

@@ -56,22 +56,22 @@
</view>
<view class="row-right">已认证</view>
</view>
<view class="main-row btn-feel">
<view v-if="!isMachineEnv" 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>
</view>
<view class="row-right">
<uni-icons color="#909090" type="right" size="14"></uni-icons>
<my-icons color="#909090" type="right" size="28"></my-icons>
</view>
</view>
<view class="main-row btn-feel">
<view v-if="!isMachineEnv" 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>
</view>
<view class="row-right">
<uni-icons color="#909090" type="right" size="14"></uni-icons>
<my-icons color="#909090" type="right" size="28"></my-icons>
</view>
</view>
<view class="main-row btn-feel">
@@ -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" v-if="isMachineEnv" @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>
<Tabbar :currentpage="4"></Tabbar>
</template> -->
<template #footer>
<Tabbar v-show="showTabbar" :currentpage="4"></Tabbar>
</template>
</AppLayout>
</template>
@@ -107,10 +107,25 @@ 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, counts } = storeToRefs(useUserStore());
const { userInfo, Completion, counts, isMachineEnv } = storeToRefs(useUserStore());
import useScreenStore from '@/stores/useScreenStore';
const screenStore = useScreenStore();
const showTabbar = ref(false);
watch(
() => screenStore.isWideScreen,
(newVal) => {
showTabbar.value = newVal;
},
{ immediate: true }
);
function logOut() {
popup.value.open();
@@ -137,6 +152,28 @@ function selectFile() {
// });
}
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>
@@ -318,4 +355,4 @@ function chooseFileUploadTest(pam) {}
border-radius: 2rpx
background: #A2A2A2;
transform: rotate(45deg)
</style>
</style>

View File

@@ -40,7 +40,7 @@
</swiper>
</view>
<!-- <Tabbar :currentpage="3"></Tabbar> -->
<Tabbar v-show="showTabbar" :currentpage="3"></Tabbar>
</view>
</view>
</template>
@@ -58,6 +58,19 @@ import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
const { unreadCount } = storeToRefs(useReadMsg());
import useScreenStore from '@/stores/useScreenStore'
const screenStore = useScreenStore()
const showTabbar = ref(false)
watch(
() => screenStore.isWideScreen,
(newVal) => {
showTabbar.value = newVal
},
{ immediate: true }
)
const disableTouch = ref(false);
const startPointX = ref(0);
const totalPage = 2;

View File

@@ -5,7 +5,7 @@
<text>热门商圈</text>
<text class="color_333333 button-click" @click="handleOpenBusinessDistrict">
更多
<uni-icons type="forward" color="#333333" size="14"></uni-icons>
<my-icons type="forward" color="#333333" size="28"></my-icons>
</text>
</view>
<scroll-view class="scroll-head" :scroll-x="true" :scroll-into-view="activeTab" :show-scrollbar="false">
@@ -46,7 +46,7 @@
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<my-icons class="iconsearch" color="#666D7F" type="plusempty" size="36"></my-icons>
<text>添加</text>
</view>
</view>

View File

@@ -2,13 +2,14 @@
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom">
<view class="nearby-map" @touchmove.stop.prevent>
<map
style="width: 100%; height: 400px"
style="width: 100%; height: 690rpx"
:latitude="latitudeVal"
:longitude="longitudeVal"
:markers="mapCovers"
:circles="mapCircles"
:controls="mapControls"
@controltap="handleControl"
:scale="mapScale"
></map>
<view class="nearby-select">
<view class="select-view" @click="changeRangeShow">
@@ -16,7 +17,7 @@
<image class="view-sx" :class="{ active: rangeShow }" src="@/static/icon/shaixun.png"></image>
</view>
<transition name="fade-slide">
<view class="select-list" v-if="rangeShow">
<view class="select-list" v-show="rangeShow">
<view class="list-item button-click" v-for="(item, index) in range" @click="changeRadius(item)">
{{ item }}km
</view>
@@ -51,7 +52,7 @@
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<my-icons class="iconsearch" color="#666D7F" type="plusempty" size="36"></my-icons>
<text>添加</text>
</view>
</view>
@@ -106,16 +107,18 @@ const tMap = ref();
const progress = ref();
const mapCovers = ref([]);
const mapCircles = ref([]);
const mapScale = ref(14.5)
const mapControls = ref([
{
id: 1,
position: {
// 控件位置
left: customSystem.systemInfo.screenWidth - 48 - 14,
top: 320,
width: 48,
height: 48,
left: customSystem.systemInfo.screenWidth - uni.upx2px(75 + 30),
top: uni.upx2px(655 - 75 - 30),
width: uni.upx2px(75),
height: uni.upx2px(75),
},
width:100,
iconPath: LocationPng, // 控件图标
},
]);
@@ -147,6 +150,9 @@ function changeRangeShow() {
}
function changeRadius(item) {
console.log(item);
if(item > 1) mapScale.value = 14.5 - item * 0.3
else mapScale.value = 14.5
pageState.search.radius = item;
rangeShow.value = false;
progressChange(item);
@@ -220,31 +226,27 @@ onMounted(() => {
});
function getInit() {
useLocationStore()
.getLocation()
.then((res) => {
mapCovers.value = [
{
latitude: res.latitude,
longitude: res.longitude,
iconPath: point2,
},
];
mapCircles.value = [
{
latitude: res.latitude,
longitude: res.longitude,
radius: 1000,
fillColor: '#1c52fa25',
color: '#256BFA',
},
];
getJobList('refresh');
});
mapCovers.value = [
{
latitude: latitudeVal.value,
longitude: longitudeVal.value,
iconPath: point2,
},
];
mapCircles.value = [
{
latitude: latitudeVal.value,
longitude:longitudeVal.value,
radius: 1000,
fillColor: '#1c52fa25',
color: '#256BFA',
},
];
getJobList('refresh');
}
function progressChange(value) {
const range = 1 + value;
const range = value < 1 ? 1 : value;
pageState.search.radius = range;
mapCircles.value = [
{
@@ -362,11 +364,11 @@ defineExpose({ loadData, handleFilterConfirm });
height: 100%;
background: #f4f4f4;
.nearby-map
height: 767rpx;
height: 655rpx;
background: #e8e8e8;
overflow: hidden
.nearby-list
min-height: calc(100% - 384rpx)
min-height: calc(100% - 400rpx)
background: #f4f4f4
display: flex;
flex-direction: column;

View File

@@ -72,7 +72,7 @@
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<my-icons class="iconsearch" color="#666D7F" type="plusempty" size="36"></my-icons>
<text>添加</text>
</view>
</view>

View File

@@ -42,7 +42,7 @@
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<my-icons class="iconsearch" color="#666D7F" type="plusempty" size="36"></my-icons>
<text>添加</text>
</view>
</view>

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>
@@ -50,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)];

View File

@@ -2,15 +2,20 @@
<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
<my-icons
class="iconsearch"
color="#666666"
type="search"
size="18"
size="36"
@confirm="searchCollection"
></uni-icons>
></my-icons>
<input
class="inputed"
type="text"
@@ -68,7 +73,7 @@
<view class="content-top">
<view class="top-left">历史搜索</view>
<view class="top-right button-click" @click="remove">
<uni-icons type="trash" color="#C1C1C1" size="20"></uni-icons>
<my-icons type="trash" color="#C1C1C1" size="40"></my-icons>
</view>
</view>
<view class="content-history">
@@ -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([]);
@@ -144,11 +151,14 @@ const { columnCount, columnSpace } = useColumnCount(() => {
pageSize.value = 10 * (columnCount.value - 1);
nextTick(() => {
waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
});
});
onLoad(() => {
onLoad((options) => {
if(options.keyWord){
searchValue.value = decodeURIComponent(options.keyWord)
searchBtn()
}
let arr = uni.getStorageSync('searchList');
if (arr) {
historyList.value = uni.getStorageSync('searchList');
@@ -253,9 +263,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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.5 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: 4.5 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: 3.2 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

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.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

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