Compare commits
71 Commits
d01f153c6a
...
shihezi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49af03f4bb | ||
| 4ae11e31f4 | |||
| bca0d997c6 | |||
|
|
b43eb98a1c | ||
|
|
44c297aac2 | ||
| f64c9e5dae | |||
| 975835baa5 | |||
| 1ac524e1f1 | |||
| 7e8bef0cb9 | |||
|
|
ce597b182d | ||
|
|
fdd5577c85 | ||
|
|
4dfc7bdfd8 | ||
|
|
4befbb05cc | ||
| 74dc6debcd | |||
| 534dfd8126 | |||
| 4c29882f36 | |||
| 4cf75922da | |||
| bb67e9ba12 | |||
|
|
7479176eff | ||
|
|
3fe0a1b9e1 | ||
| 284d696b25 | |||
| 85a24d3346 | |||
| 854bc9c197 | |||
|
|
606b6104bb | ||
|
|
a10fd29440 | ||
|
|
ab3201b015 | ||
|
|
8420d265e1 | ||
| a30a302e90 | |||
|
|
3d826aec86 | ||
| 8b9f7890af | |||
| 32e147e601 | |||
| 8ccdcbf93b | |||
| 01d817f99c | |||
| 137542a92f | |||
| cd9c3acfc0 | |||
|
|
80213b18e9 | ||
| 4a03b4d4cb | |||
|
|
77dfab84f1 | ||
| 33f1d0a8ab | |||
| 92d87347d0 | |||
| add2cfeac4 | |||
|
|
8e316cf2a1 | ||
|
|
d6bf2648de | ||
| 0a2d523a40 | |||
| 8a138bed99 | |||
|
|
d9f0356491 | ||
| ff941848b1 | |||
| e19673b99e | |||
| 7260043942 | |||
| c351bfa54f | |||
| dd81468f47 | |||
| 79207ee66a | |||
| 215ec11fe7 | |||
| 74f13d26a8 | |||
|
|
e6cb12659d | ||
| 4571744f2e | |||
| 3ce8e65abc | |||
| 159efd166a | |||
| 817fa27136 | |||
| cb9f626788 | |||
| 330eec226f | |||
| 4983d9cbc7 | |||
| fa1132ebe9 | |||
| 30764cec57 | |||
| cb1052346e | |||
|
|
2b1a87b65e | ||
|
|
67f5dbbfb0 | ||
|
|
f24d95cedf | ||
|
|
c173fdd6a8 | ||
|
|
e0c4f18da0 | ||
|
|
a2ca068669 |
147
App.vue
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
65
common/all-in-one-listen.js
Normal 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('识别失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
115
components/GlobalPopup/GlobalPopup.vue
Normal 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>
|
||||
664
components/My-icons/my-icons.css
Normal 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";
|
||||
}
|
||||
664
components/My-icons/my-icons.ts
Normal 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)
|
||||
BIN
components/My-icons/my-icons.ttf
Normal file
110
components/My-icons/my-icons.vue
Normal 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>
|
||||
649
components/My-icons/my-icons_file_vue.js
Normal 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)
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
35
config.js
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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, // 导出支持状态
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
// 直接调用 speak,SDK 内部会自动处理 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');
|
||||
}
|
||||
@@ -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 为 true,UI 显示播放按钮,用户点击后调用 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');
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
210
manifest.json
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"unplugin-vue-components": "^0.26.0"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
14
pages.json
@@ -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": {}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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% {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
});
|
||||
|
||||
560
pages/chat/components/uploadQrcode.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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{
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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);
|
||||
|
||||
BIN
static/font/DIN-Medium.woff2
Normal file
BIN
static/font/PingFangSC-Medium.woff2
Normal file
BIN
static/font/PingFangSC-Regular.woff2
Normal file
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 995 B After Width: | Height: | Size: 984 B |
|
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 414 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 515 B |
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 209 B |
|
Before Width: | Height: | Size: 543 B After Width: | Height: | Size: 533 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 187 B After Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 751 B After Width: | Height: | Size: 739 B |
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 843 B After Width: | Height: | Size: 834 B |
|
Before Width: | Height: | Size: 542 B After Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.8 KiB |