Compare commits
1 Commits
ee6813f767
...
shihezi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49af03f4bb |
62
App.vue
62
App.vue
@@ -1,38 +1,30 @@
|
||||
<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';
|
||||
import { GlobalInactivityManager } from '@/utils/GlobalInactivityManager';
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
const {
|
||||
$api,
|
||||
navTo,
|
||||
appendScriptTagElement,
|
||||
aes_Decrypt,
|
||||
sm2_Decrypt,
|
||||
safeReLaunch,
|
||||
isY9MachineType,
|
||||
isAsdMachineType,
|
||||
} = inject('globalFunction');
|
||||
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;
|
||||
|
||||
usePageAnimation();
|
||||
onLaunch((options) => {
|
||||
useDictStore().getDictData();
|
||||
if (isAsdMachineType()) {
|
||||
if (lightAppJssdk.user) {
|
||||
console.warn('爱山东环境');
|
||||
getUserInfo();
|
||||
useUserStore().changMiniProgramAppStatus(false);
|
||||
useUserStore().changMachineEnv(false);
|
||||
useLocationStore().getLocationLoop(); //循环获取定位
|
||||
useLocationStore().getLocationLoop()//循环获取定位
|
||||
return;
|
||||
}
|
||||
if (isY9MachineType()) {
|
||||
@@ -42,26 +34,25 @@ onLaunch((options) => {
|
||||
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);
|
||||
});
|
||||
})();
|
||||
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(); //循环获取定位
|
||||
useLocationStore().getLocationLoop()//循环获取定位
|
||||
useUserStore().changMiniProgramAppStatus(true);
|
||||
useUserStore().changMachineEnv(false);
|
||||
useUserStore().initSeesionId(); //更新
|
||||
@@ -79,6 +70,7 @@ onLaunch((options) => {
|
||||
|
||||
onMounted(() => {});
|
||||
|
||||
|
||||
onShow(() => {
|
||||
console.log('App Show');
|
||||
});
|
||||
@@ -88,7 +80,7 @@ onHide(() => {
|
||||
});
|
||||
|
||||
function handleInactivity() {
|
||||
console.warn('60秒无操作,执行安全逻辑');
|
||||
console.log('【全局】60秒无操作,执行安全逻辑');
|
||||
if (inactivityModalTimer) {
|
||||
clearTimeout(inactivityModalTimer);
|
||||
inactivityModalTimer = null;
|
||||
@@ -96,7 +88,6 @@ function handleInactivity() {
|
||||
|
||||
if (useUserStore().hasLogin) {
|
||||
// 1. 正常弹出确认框
|
||||
playTextDirectly('长时间无操作,是否继续使用?');
|
||||
$confirm({
|
||||
title: '会话即将过期',
|
||||
content: '长时间无操作,是否继续使用?',
|
||||
@@ -120,7 +111,7 @@ function handleInactivity() {
|
||||
},
|
||||
});
|
||||
|
||||
// 2. 启动 20 秒倒计时
|
||||
// 2. 启动 10 秒倒计时
|
||||
inactivityModalTimer = setTimeout(() => {
|
||||
inactivityModalTimer = null;
|
||||
console.log('【自动登出】10秒无响应,强制清理状态');
|
||||
@@ -129,7 +120,7 @@ function handleInactivity() {
|
||||
uni.$emit('hide-global-popup');
|
||||
|
||||
performLogout();
|
||||
}, 20000);
|
||||
}, 10000);
|
||||
} else {
|
||||
inactivityManager?.resume();
|
||||
}
|
||||
@@ -142,6 +133,13 @@ function performLogout() {
|
||||
inactivityManager?.resume(); // 恢复监听
|
||||
}
|
||||
|
||||
// 一体机环境判断
|
||||
function isY9MachineType() {
|
||||
const ua = navigator.userAgent;
|
||||
const isY9Machine = /Y9-ZYYH/i.test(ua); // 匹配机器型号
|
||||
return isY9Machine;
|
||||
}
|
||||
|
||||
// 爱山东环境登录
|
||||
function getUserInfo() {
|
||||
lightAppJssdk.user.getUserInfoWithEncryptedParamByAppId({
|
||||
|
||||
@@ -1,53 +1,35 @@
|
||||
import {
|
||||
$api,
|
||||
safeReLaunch
|
||||
$api
|
||||
} from "./globalFunction";
|
||||
import baseDB from '@/utils/db.js';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import {
|
||||
playTextDirectly
|
||||
} from '@/hook/useTTSPlayer-all-in-one'
|
||||
|
||||
|
||||
export class IncreaseRevie {
|
||||
constructor(arg) {
|
||||
this.myEventCallback = this.myCallback.bind(this);
|
||||
|
||||
this._debounceTimer = null; // 防抖计时器
|
||||
this._initTimer = null; // 启动延时计时器
|
||||
this._debounceTimer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
||||
start() {
|
||||
init() {
|
||||
this.close();
|
||||
|
||||
this._initTimer = setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
if (window.hh?.on) {
|
||||
console.log('开始监听 QR 扫码事件');
|
||||
window.hh.on('initQRListener', this.myEventCallback);
|
||||
}
|
||||
this._initTimer = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
if (this._initTimer) {
|
||||
clearTimeout(this._initTimer);
|
||||
this._initTimer = null;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -59,170 +41,25 @@ export class IncreaseRevie {
|
||||
}, 300);
|
||||
}
|
||||
|
||||
|
||||
async handleDebouncedCallback(res) {
|
||||
if (res.data) {
|
||||
const code = res.data.qrCode;
|
||||
if (/^\d{6}$/.test(String(code))) {
|
||||
$api.createRequest(`/app/qrcodeLogin/${code}`, {}, 'get').then((resData) => {
|
||||
useUserStore()
|
||||
.loginSetToken(resData.token)
|
||||
.then((resume) => {
|
||||
playTextDirectly('登录成功');
|
||||
// 根据是否有一体机岗位ID判断跳转路径
|
||||
if (resume.data.jobTitleId) {
|
||||
useUserStore().initSeesionId();
|
||||
safeReLaunch('/pages/index/index');
|
||||
} else {
|
||||
safeReLaunch('/pages/login/login');
|
||||
}
|
||||
// 关闭监听,避免重复扫码
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 格式不对不做处理,或者提示
|
||||
console.log('QR Code format mismatch');
|
||||
}
|
||||
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('识别失败');
|
||||
playTextDirectly('识别失败');
|
||||
$api.msg('识别失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class FaceLoginService {
|
||||
constructor() {
|
||||
this.isInitialized = false;
|
||||
this.defaultScope = "auth_user,yingpin"; // 默认聚合授权参数
|
||||
|
||||
this._retryTimer = null;
|
||||
}
|
||||
|
||||
start(scope = null) {
|
||||
// 启动前先清理可能存在的旧状态
|
||||
this.close();
|
||||
console.log("[FaceLogin] 服务启动...");
|
||||
// 开始执行初始化逻辑
|
||||
this.init(scope);
|
||||
}
|
||||
|
||||
close() {
|
||||
// 1. 清除正在等待执行的重试定时器
|
||||
if (this._retryTimer) {
|
||||
clearTimeout(this._retryTimer);
|
||||
this._retryTimer = null;
|
||||
console.log("[FaceLogin] 已取消挂起的初始化重试");
|
||||
}
|
||||
|
||||
// 2. 重置初始化状态
|
||||
this.isInitialized = false;
|
||||
|
||||
console.log("[FaceLogin] 服务已关闭");
|
||||
}
|
||||
|
||||
async init(scope = null, retryCount = 1) {
|
||||
const params = {
|
||||
action: "initFace",
|
||||
event: "open",
|
||||
taskId: this._generateTaskId(),
|
||||
params: {
|
||||
serviceId: "",
|
||||
query: "",
|
||||
scope: scope || this.defaultScope
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await this._bridgeCall(params);
|
||||
this.isInitialized = true;
|
||||
console.log("[FaceLogin] 初始化成功");
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`[FaceLogin] 初始化失败: ${err.message}`);
|
||||
if (retryCount > 0) {
|
||||
console.log("[FaceLogin] 3秒后尝试重新初始化...");
|
||||
this._retryTimer = setTimeout(() => {
|
||||
this._retryTimer = null; // 执行时清空引用
|
||||
this.init(scope, retryCount - 1); // 递归重试
|
||||
}, 3000);
|
||||
} else {
|
||||
// throw err; // 视业务逻辑决定是否阻断
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 唤起 1:N 刷脸并获取 AuthCode
|
||||
*/
|
||||
async startFaceLogin() {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("[FaceLogin] 服务未初始化,尝试自动补救初始化...");
|
||||
try {
|
||||
await this.init(null, 0);
|
||||
if (!this.isInitialized) throw new Error("初始化未完成");
|
||||
} catch (e) {
|
||||
throw new Error("刷脸服务初始化失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
action: "getFaceInfo",
|
||||
event: "open",
|
||||
taskId: this._generateTaskId()
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await this._bridgeCall(params);
|
||||
const {
|
||||
data
|
||||
} = res;
|
||||
// if (!data || !data.extInfo) {
|
||||
// throw new Error("返回数据缺少 extInfo");
|
||||
// }
|
||||
|
||||
// let extInfoObj;
|
||||
// try {
|
||||
// extInfoObj = JSON.parse(data.extInfo);
|
||||
// } catch (e) {
|
||||
// throw new Error("extInfo JSON解析失败");
|
||||
// }
|
||||
|
||||
// if (!extInfoObj.authCode) {
|
||||
// throw new Error("未获取到 authCode");
|
||||
// }
|
||||
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
const errMsg = error.message || error.subMessage || "刷脸失败";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
_bridgeCall(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof hh === 'undefined' || !hh.call) {
|
||||
return reject(new Error("Bridge环境未就绪 (hh未定义)"));
|
||||
}
|
||||
|
||||
hh.call("ampeHHCommunication", params, (res) => {
|
||||
res = JSON.parse(res)
|
||||
if (res && res.success) {
|
||||
resolve(res);
|
||||
} else {
|
||||
reject(res || {
|
||||
message: "未知错误",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_generateTaskId() {
|
||||
return 'task_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,27 +42,3 @@ uni-modal .uni-modal__ft{
|
||||
font-size: 36rpx !important;
|
||||
line-height: 80rpx !important;
|
||||
}
|
||||
|
||||
.uni-popup-dialog{
|
||||
width: 420rpx !important;
|
||||
}
|
||||
.uni-dialog-title{
|
||||
padding-top: 40rpx !important;
|
||||
}
|
||||
.uni-dialog-title-text{
|
||||
font-size: 34rpx !important;
|
||||
}
|
||||
.uni-dialog-content{
|
||||
padding: 25rpx !important;
|
||||
}
|
||||
.uni-dialog-content-text{
|
||||
font-size: 30rpx !important;
|
||||
}
|
||||
.uni-dialog-button{
|
||||
height: 80rpx !important;
|
||||
}
|
||||
.uni-dialog-button-text{
|
||||
font-size: 36rpx !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
>
|
||||
<!-- 顶部头部区域 -->
|
||||
<view
|
||||
v-if="showHeader || defaultShow"
|
||||
class="container-header"
|
||||
:style="border ? { borderBottom: `2rpx solid ${borderColor}` } : { borderBottom: 'none' }"
|
||||
>
|
||||
@@ -21,7 +20,6 @@
|
||||
<slot name="headerright"></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 主体不可滚动 headContent -->
|
||||
<view class="container-headContent">
|
||||
@@ -45,10 +43,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue"
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { isMiniProgram } = storeToRefs(useUserStore());
|
||||
import img from '@/static/icon/background2.png';
|
||||
const emit = defineEmits(['onScrollBottom']);
|
||||
defineProps({
|
||||
@@ -88,16 +82,8 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultShow = computed(()=>{
|
||||
return isMiniProgram.value
|
||||
})
|
||||
|
||||
const handleScrollToLower = () => {
|
||||
emit('onScrollBottom');
|
||||
};
|
||||
|
||||
@@ -33,14 +33,10 @@ onMounted(() => {
|
||||
state.resolve = options.resolve;
|
||||
popup.value.open();
|
||||
});
|
||||
uni.$on('hide-global-popup',()=>{
|
||||
popup.value.close()
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('show-global-popup');
|
||||
uni.$off('hide-global-popup');
|
||||
});
|
||||
|
||||
const onConfirm = () => {
|
||||
|
||||
@@ -66,11 +66,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -41,11 +41,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -50,11 +50,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -93,11 +93,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -93,11 +93,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -93,11 +93,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -56,11 +56,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -56,11 +56,11 @@ const props = defineProps({
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.366085,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.086656,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useReadMsg } from '@/stores/useReadMsg';
|
||||
import useScreenStore from '@/stores/useScreenStore'
|
||||
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { isMachineEnv, hasLogin } = storeToRefs(useUserStore());
|
||||
const { isMachineEnv } = storeToRefs(useUserStore());
|
||||
|
||||
const screenStore = useScreenStore()
|
||||
const {isWideScreen} = screenStore
|
||||
@@ -95,7 +95,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const changeItem = (item) => {
|
||||
if(isMachineEnv.value && item.needLogin && !hasLogin.value){
|
||||
if(isMachineEnv.value && item.needLogin){
|
||||
useUserStore().logOut()
|
||||
}else{
|
||||
uni.switchTab({
|
||||
|
||||
13
config.js
13
config.js
@@ -1,14 +1,15 @@
|
||||
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/app/asr/connect', // 测试
|
||||
vioceBaseURl: 'wss://fw.rc.qingdao.gov.cn/rgpp-api/api/app/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', // 测试
|
||||
speechSynthesis: 'wss://fw.rc.qingdao.gov.cn/rgpp-api/api/app/tts/connect', // 正式
|
||||
// speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
|
||||
// speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/', //直接替换即可
|
||||
speechSynthesis2: 'http://39.98.44.136:19527', //直接替换即可
|
||||
// indexedDB
|
||||
DBversion: 3,
|
||||
// 只使用本地缓寸的数据
|
||||
|
||||
@@ -364,7 +364,7 @@ class PiperTTS {
|
||||
this.isRecording = true;
|
||||
this.onStart();
|
||||
|
||||
const wsUrl = this.baseUrl.replace(/^http/, 'ws');
|
||||
const wsUrl = this.baseUrl.replace(/^http/, 'ws') + '/ws/synthesize';
|
||||
this.worker.postMessage({
|
||||
type: 'connect',
|
||||
data: {
|
||||
|
||||
216
hook/piper-sdk.js
Normal file
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;
|
||||
}
|
||||
}
|
||||
@@ -1,749 +0,0 @@
|
||||
// useAudioSpeak.js
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* TTS语音合成Hook
|
||||
* @param {Object} config - TTS配置
|
||||
* @param {string} config.apiUrl - 语音合成API地址
|
||||
* @param {number} config.maxSegmentLength - 最大分段长度
|
||||
* @returns {Object} TTS相关方法和状态
|
||||
*/
|
||||
export const useAudioSpeak = (config = {}) => {
|
||||
const {
|
||||
apiUrl = 'http://39.98.44.136:19527/synthesize',
|
||||
maxSegmentLength = 30
|
||||
} = config
|
||||
|
||||
// 状态
|
||||
const isSpeaking = ref(false)
|
||||
const isPaused = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 播放状态
|
||||
const currentText = ref('')
|
||||
const currentSegmentIndex = ref(0)
|
||||
const totalSegments = ref(0)
|
||||
const progress = ref(0)
|
||||
|
||||
// 音频相关
|
||||
let audioContext = null
|
||||
let audioSource = null
|
||||
let audioQueue = [] // 播放队列 [{blob, segmentIndex, text, isMerged?}, ...]
|
||||
let isPlayingQueue = false
|
||||
let isCancelled = false
|
||||
let segments = []
|
||||
let allRequestsCompleted = false
|
||||
let pendingMergeSegments = [] // 存储所有片段的原始数据 [{arrayBuffer, segmentIndex}]
|
||||
let firstSegmentHeader = null
|
||||
let lastPlayedIndex = -1
|
||||
let currentPlayingIndex = -1 // 当前正在播放的片段索引
|
||||
let isInterrupted = false // 是否被中断
|
||||
|
||||
// 按标点分割文本
|
||||
const splitTextByPunctuation = (text) => {
|
||||
const segments = []
|
||||
const punctuation = /[,。!?;!?;\n]/g
|
||||
let lastIndex = 0
|
||||
|
||||
while (true) {
|
||||
const match = punctuation.exec(text)
|
||||
if (!match) break
|
||||
|
||||
const isChinesePunctuation = /[,。!?;]/.test(match[0])
|
||||
const endIndex = isChinesePunctuation ? match.index + 1 : match.index
|
||||
|
||||
const segment = text.substring(lastIndex, endIndex)
|
||||
if (segment.trim()) {
|
||||
segments.push(segment.trim())
|
||||
}
|
||||
lastIndex = endIndex
|
||||
}
|
||||
|
||||
const lastSegment = text.substring(lastIndex)
|
||||
if (lastSegment.trim()) {
|
||||
segments.push(lastSegment.trim())
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
// 预处理文本
|
||||
const preprocessText = (text) => {
|
||||
if (!text || typeof text !== 'string') return []
|
||||
|
||||
const cleanText = text.replace(/\s+/g, ' ').trim()
|
||||
let segments = splitTextByPunctuation(cleanText)
|
||||
|
||||
const finalSegments = []
|
||||
segments.forEach(segment => {
|
||||
if (segment.length <= maxSegmentLength) {
|
||||
finalSegments.push(segment)
|
||||
} else {
|
||||
for (let i = 0; i < segment.length; i += maxSegmentLength) {
|
||||
finalSegments.push(segment.substring(i, i + maxSegmentLength))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return finalSegments.filter(seg => seg && seg.trim())
|
||||
}
|
||||
|
||||
// 检测WAV头部大小
|
||||
const detectWavHeaderSize = (arrayBuffer) => {
|
||||
try {
|
||||
const header = new Uint8Array(arrayBuffer.slice(0, 100))
|
||||
|
||||
if (header[0] === 0x52 && header[1] === 0x49 && header[2] === 0x46 && header[3] === 0x46) {
|
||||
for (let i = 36; i < 60; i++) {
|
||||
if (header[i] === 0x64 && header[i+1] === 0x61 && header[i+2] === 0x74 && header[i+3] === 0x61) {
|
||||
return i + 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 44
|
||||
} catch (error) {
|
||||
console.error('检测WAV头部大小失败:', error)
|
||||
return 44
|
||||
}
|
||||
}
|
||||
|
||||
// 请求音频片段
|
||||
const fetchAudioSegment = async (text, index) => {
|
||||
try {
|
||||
console.log(`📶正在请求第${index + 1}段音频: "${text}"`)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
speed: 1.0,
|
||||
volume: 1.0,
|
||||
pitch: 1.0,
|
||||
voice_type: 1
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误! 状态码: ${response.status}`)
|
||||
}
|
||||
|
||||
const audioBlob = await response.blob()
|
||||
|
||||
if (!audioBlob || audioBlob.size < 100) {
|
||||
throw new Error('音频数据太小或无效')
|
||||
}
|
||||
|
||||
console.log(`第${index + 1}段音频获取成功,大小: ${audioBlob.size} 字节`)
|
||||
|
||||
const arrayBuffer = await audioBlob.arrayBuffer()
|
||||
|
||||
// 保存原始数据用于可能的合并
|
||||
pendingMergeSegments.push({
|
||||
arrayBuffer: arrayBuffer,
|
||||
segmentIndex: index
|
||||
})
|
||||
|
||||
// 如果是第一个片段,保存其WAV头部用于合并
|
||||
if (index === 0) {
|
||||
const headerSize = detectWavHeaderSize(arrayBuffer)
|
||||
firstSegmentHeader = new Uint8Array(arrayBuffer.slice(0, headerSize))
|
||||
}
|
||||
|
||||
return {
|
||||
blob: audioBlob,
|
||||
segmentIndex: index,
|
||||
text: text,
|
||||
arrayBuffer: arrayBuffer
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`获取第${index + 1}段音频失败:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化音频上下文
|
||||
const initAudioContext = () => {
|
||||
if (!audioContext || audioContext.state === 'closed') {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)()
|
||||
console.log('音频上下文已初始化')
|
||||
}
|
||||
return audioContext
|
||||
}
|
||||
|
||||
// 解码并播放音频Blob
|
||||
const decodeAndPlayBlob = async (audioBlob, segmentIndex) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isCancelled || !audioContext) {
|
||||
console.log('播放已取消或音频上下文不存在,跳过播放')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置当前正在播放的片段索引
|
||||
currentPlayingIndex = segmentIndex
|
||||
console.log(`设置当前播放索引为: ${currentPlayingIndex}`)
|
||||
|
||||
const fileReader = new FileReader()
|
||||
|
||||
fileReader.onload = async (e) => {
|
||||
try {
|
||||
const arrayBuffer = e.target.result
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
|
||||
|
||||
// 如果在此期间被取消,直接返回
|
||||
if (isCancelled) {
|
||||
console.log('播放过程中被取消')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
audioSource = audioContext.createBufferSource()
|
||||
audioSource.buffer = audioBuffer
|
||||
audioSource.connect(audioContext.destination)
|
||||
|
||||
audioSource.onended = () => {
|
||||
console.log(`第${segmentIndex + 1}个片段播放完成`)
|
||||
audioSource = null
|
||||
lastPlayedIndex = segmentIndex
|
||||
currentPlayingIndex = -1
|
||||
resolve()
|
||||
}
|
||||
|
||||
audioSource.onerror = (error) => {
|
||||
console.error('音频播放错误:', error)
|
||||
currentPlayingIndex = -1
|
||||
reject(error)
|
||||
}
|
||||
|
||||
console.log(`▶️开始播放第${segmentIndex + 1}个片段`)
|
||||
|
||||
// 如果音频上下文被暂停,先恢复
|
||||
if (audioContext.state === 'suspended') {
|
||||
await audioContext.resume()
|
||||
}
|
||||
|
||||
audioSource.start(0)
|
||||
|
||||
} catch (error) {
|
||||
console.error('解码或播放音频失败:', error)
|
||||
currentPlayingIndex = -1
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
fileReader.onerror = (error) => {
|
||||
console.error('读取音频文件失败:', error)
|
||||
currentPlayingIndex = -1
|
||||
reject(error)
|
||||
}
|
||||
|
||||
fileReader.readAsArrayBuffer(audioBlob)
|
||||
})
|
||||
}
|
||||
|
||||
// 合并剩余音频片段
|
||||
const mergeRemainingSegments = (segmentsToMerge) => {
|
||||
if (segmentsToMerge.length === 0 || !firstSegmentHeader) {
|
||||
console.log('没有待合并的片段或缺少头部信息')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// 按segmentIndex排序
|
||||
segmentsToMerge.sort((a, b) => a.segmentIndex - b.segmentIndex)
|
||||
|
||||
console.log(`开始合并${segmentsToMerge.length}个剩余片段`)
|
||||
|
||||
// 计算总数据大小
|
||||
let totalAudioDataSize = 0
|
||||
for (const segment of segmentsToMerge) {
|
||||
const headerSize = detectWavHeaderSize(segment.arrayBuffer)
|
||||
totalAudioDataSize += segment.arrayBuffer.byteLength - headerSize
|
||||
}
|
||||
|
||||
console.log(`剩余音频数据总大小: ${totalAudioDataSize}字节`)
|
||||
|
||||
// 创建合并后的数组
|
||||
const headerSize = firstSegmentHeader.length
|
||||
const totalSize = headerSize + totalAudioDataSize
|
||||
const mergedArray = new Uint8Array(totalSize)
|
||||
|
||||
// 设置头部
|
||||
mergedArray.set(firstSegmentHeader, 0)
|
||||
|
||||
// 更新头部中的data大小
|
||||
const view = new DataView(mergedArray.buffer)
|
||||
view.setUint32(40, totalAudioDataSize, true)
|
||||
view.setUint32(4, 36 + totalAudioDataSize, true)
|
||||
|
||||
// 合并所有音频数据
|
||||
let offset = headerSize
|
||||
for (const segment of segmentsToMerge) {
|
||||
const segmentHeaderSize = detectWavHeaderSize(segment.arrayBuffer)
|
||||
const segmentData = new Uint8Array(segment.arrayBuffer.slice(segmentHeaderSize))
|
||||
mergedArray.set(segmentData, offset)
|
||||
offset += segmentData.length
|
||||
}
|
||||
|
||||
console.log(`音频合并完成,总大小: ${mergedArray.length}字节`)
|
||||
|
||||
// 创建Blob
|
||||
return new Blob([mergedArray], { type: 'audio/wav' })
|
||||
|
||||
} catch (error) {
|
||||
console.error('合并音频片段失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 从队列播放音频
|
||||
const playFromQueue = async () => {
|
||||
if (isPlayingQueue || audioQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
isPlayingQueue = true
|
||||
|
||||
try {
|
||||
while (audioQueue.length > 0 && !isCancelled && !isInterrupted) {
|
||||
const audioItem = audioQueue[0]
|
||||
currentSegmentIndex.value = audioItem.segmentIndex
|
||||
|
||||
// 播放第一个音频
|
||||
console.log(`准备播放第${audioItem.segmentIndex + 1}个片段: "${audioItem.text}"`)
|
||||
await decodeAndPlayBlob(audioItem.blob, audioItem.segmentIndex)
|
||||
|
||||
if (isCancelled || isInterrupted) {
|
||||
console.log('播放被中断,退出播放队列')
|
||||
break
|
||||
}
|
||||
|
||||
// 播放完成后移除
|
||||
audioQueue.shift()
|
||||
console.log(`片段${audioItem.segmentIndex + 1}播放完成,队列剩余: ${audioQueue.length}`)
|
||||
|
||||
// 更新进度
|
||||
progress.value = Math.floor(((audioItem.segmentIndex + 1) / totalSegments.value) * 100)
|
||||
|
||||
// 短暂延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
}
|
||||
|
||||
// 检查是否所有片段都播放完成
|
||||
if (audioQueue.length === 0 && lastPlayedIndex === totalSegments.value - 1) {
|
||||
console.log('所有音频片段播放完成')
|
||||
progress.value = 100
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('播放队列出错:', error)
|
||||
} finally {
|
||||
isPlayingQueue = false
|
||||
}
|
||||
}
|
||||
|
||||
// 向队列添加音频
|
||||
const addToQueue = (audioItem) => {
|
||||
// 如果被取消或中断,不添加到队列
|
||||
if (isCancelled || isInterrupted) {
|
||||
console.log('播放已中断,不添加到队列')
|
||||
return
|
||||
}
|
||||
|
||||
// 按segmentIndex插入到正确位置
|
||||
let insertIndex = 0
|
||||
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
||||
if (audioQueue[i].segmentIndex < audioItem.segmentIndex) {
|
||||
insertIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
audioQueue.splice(insertIndex, 0, audioItem)
|
||||
console.log(`音频片段${audioItem.segmentIndex + 1}已添加到队列,队列长度: ${audioQueue.length}`)
|
||||
|
||||
// 如果队列中有音频且当前没有在播放,开始播放
|
||||
if (!isPlayingQueue && audioQueue.length === 1) {
|
||||
playFromQueue()
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试合并剩余片段
|
||||
const tryMergeRemainingSegments = () => {
|
||||
if (!allRequestsCompleted) {
|
||||
console.log('合并检查: 请求未完成,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取真正未播放的片段(不包括已经播放的和正在播放的)
|
||||
const trulyUnplayedSegments = audioQueue.filter(item => {
|
||||
// 排除已经播放完成的
|
||||
if (item.segmentIndex <= lastPlayedIndex) {
|
||||
return false
|
||||
}
|
||||
// 排除当前正在播放的
|
||||
if (currentPlayingIndex !== -1 && item.segmentIndex === currentPlayingIndex) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const shouldMerge = trulyUnplayedSegments.length > 1
|
||||
|
||||
console.log(`🔀合并检查: 已播放到=${lastPlayedIndex}, 正在播放=${currentPlayingIndex}, 真正未播放片段=${trulyUnplayedSegments.length}, 应该合并=${shouldMerge}`)
|
||||
|
||||
if (!shouldMerge) {
|
||||
console.log('不符合合并条件(真正未播放片段数量 <= 1)')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('✔️符合合并条件,开始合并剩余片段')
|
||||
|
||||
// 获取这些片段的原始数据
|
||||
const segmentsToMergeData = []
|
||||
for (const item of trulyUnplayedSegments) {
|
||||
const segmentData = pendingMergeSegments.find(s => s.segmentIndex === item.segmentIndex)
|
||||
if (segmentData) {
|
||||
segmentsToMergeData.push(segmentData)
|
||||
}
|
||||
}
|
||||
|
||||
if (segmentsToMergeData.length === 0) {
|
||||
console.log('没有找到待合并的原始数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 合并这些片段
|
||||
const mergedBlob = mergeRemainingSegments(segmentsToMergeData)
|
||||
|
||||
if (mergedBlob) {
|
||||
// 从audioQueue中移除这些将被合并的片段
|
||||
const segmentIndicesToRemove = trulyUnplayedSegments.map(s => s.segmentIndex)
|
||||
|
||||
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
||||
if (segmentIndicesToRemove.includes(audioQueue[i].segmentIndex)) {
|
||||
audioQueue.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 将合并后的音频添加到队列的合适位置
|
||||
const firstSegmentIndex = Math.min(...segmentIndicesToRemove)
|
||||
const mergedText = trulyUnplayedSegments.map(s => s.text).join(' ')
|
||||
|
||||
const mergedAudioItem = {
|
||||
blob: mergedBlob,
|
||||
segmentIndex: firstSegmentIndex,
|
||||
text: mergedText,
|
||||
isMerged: true
|
||||
}
|
||||
|
||||
// 插入到正确位置(按segmentIndex)
|
||||
let insertIndex = 0
|
||||
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
||||
if (audioQueue[i].segmentIndex < firstSegmentIndex) {
|
||||
insertIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
audioQueue.splice(insertIndex, 0, mergedAudioItem)
|
||||
console.log(`合并后的音频已添加到队列位置${insertIndex},包含${trulyUnplayedSegments.length}个原始片段,队列长度: ${audioQueue.length}`)
|
||||
} else {
|
||||
console.log('合并失败,保持原始片段')
|
||||
}
|
||||
}
|
||||
|
||||
// 文本提取工具函数
|
||||
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) => {
|
||||
// 处理薪资格式
|
||||
let salaryText = job.salary;
|
||||
if (salaryText) {
|
||||
// 匹配 "XXXXX-XXXXX元/月" 格式
|
||||
const rangeMatch = salaryText.match(/(\d+)-(\d+)元\/月/);
|
||||
if (rangeMatch) {
|
||||
const minSalary = parseInt(rangeMatch[1], 10);
|
||||
const maxSalary = parseInt(rangeMatch[2], 10);
|
||||
|
||||
// 转换为千位单位
|
||||
const minK = Math.round(minSalary / 1000);
|
||||
const maxK = Math.round(maxSalary / 1000);
|
||||
|
||||
salaryText = `${minK}千到${maxK}千每月`;
|
||||
}
|
||||
// 如果不是 "XXXXX-XXXXX元/月" 格式,保持原样
|
||||
}
|
||||
|
||||
return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${salaryText},地点:${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');
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
const cleanup = () => {
|
||||
console.log('开始清理资源')
|
||||
isCancelled = true
|
||||
isInterrupted = true
|
||||
|
||||
if (audioSource) {
|
||||
try {
|
||||
audioSource.stop()
|
||||
console.log('音频源已停止')
|
||||
} catch (e) {
|
||||
console.warn('停止音频源失败:', e)
|
||||
}
|
||||
audioSource = null
|
||||
}
|
||||
|
||||
if (audioContext && audioContext.state !== 'closed') {
|
||||
audioContext.close()
|
||||
console.log('音频上下文已关闭')
|
||||
}
|
||||
|
||||
audioContext = null
|
||||
audioQueue = []
|
||||
segments = []
|
||||
isPlayingQueue = false
|
||||
allRequestsCompleted = false
|
||||
pendingMergeSegments = []
|
||||
firstSegmentHeader = null
|
||||
lastPlayedIndex = -1
|
||||
currentPlayingIndex = -1
|
||||
|
||||
isSpeaking.value = false
|
||||
isPaused.value = false
|
||||
isLoading.value = false
|
||||
progress.value = 0
|
||||
console.log('资源清理完成')
|
||||
}
|
||||
|
||||
// 停止播放
|
||||
const stopAudio = () => {
|
||||
console.log('停止音频播放')
|
||||
isCancelled = true
|
||||
isInterrupted = true
|
||||
|
||||
if (audioSource) {
|
||||
try {
|
||||
audioSource.stop()
|
||||
console.log('音频源已停止')
|
||||
} catch (e) {
|
||||
console.warn('停止音频源失败:', e)
|
||||
}
|
||||
audioSource = null
|
||||
}
|
||||
|
||||
audioQueue = []
|
||||
isPlayingQueue = false
|
||||
currentPlayingIndex = -1
|
||||
isSpeaking.value = false
|
||||
isPaused.value = false
|
||||
isLoading.value = false
|
||||
|
||||
// 恢复中断标志,为下一次播放准备
|
||||
setTimeout(() => {
|
||||
isCancelled = false
|
||||
isInterrupted = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 主speak方法
|
||||
const speak = async (text) => {
|
||||
console.log('开始新的语音播报')
|
||||
|
||||
// 先停止当前播放
|
||||
if (isSpeaking.value || audioQueue.length > 0) {
|
||||
console.log('检测到正在播放,先停止')
|
||||
stopAudio()
|
||||
// 等待一小段时间确保资源清理完成
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
}
|
||||
|
||||
text = extractSpeechText(text)
|
||||
console.log('开始语音播报:', text)
|
||||
|
||||
// 重置状态
|
||||
isCancelled = false
|
||||
isInterrupted = false
|
||||
currentText.value = text
|
||||
isLoading.value = true
|
||||
isSpeaking.value = true
|
||||
progress.value = 0
|
||||
audioQueue = []
|
||||
allRequestsCompleted = false
|
||||
pendingMergeSegments = []
|
||||
firstSegmentHeader = null
|
||||
lastPlayedIndex = -1
|
||||
currentPlayingIndex = -1
|
||||
|
||||
// 预处理文本
|
||||
segments = preprocessText(text)
|
||||
console.log('文本分段结果:', segments)
|
||||
|
||||
if (segments.length === 0) {
|
||||
console.warn('没有有效的文本可以播报')
|
||||
isLoading.value = false
|
||||
isSpeaking.value = false
|
||||
return
|
||||
}
|
||||
|
||||
totalSegments.value = segments.length
|
||||
|
||||
try {
|
||||
// 初始化音频上下文
|
||||
initAudioContext()
|
||||
|
||||
// 1. 串行请求所有音频片段
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (isCancelled || isInterrupted) {
|
||||
console.log('播放被取消或中断,停止请求')
|
||||
break
|
||||
}
|
||||
|
||||
console.log(`串行请求第${i + 1}/${segments.length}个片段`)
|
||||
|
||||
// 更新进度(请求进度)
|
||||
progress.value = Math.floor(((i + 1) / segments.length) * 50)
|
||||
|
||||
// 请求音频片段
|
||||
const audioItem = await fetchAudioSegment(segments[i], i)
|
||||
|
||||
if (isCancelled || isInterrupted) {
|
||||
console.log('播放被取消或中断,停止添加队列')
|
||||
break
|
||||
}
|
||||
|
||||
// 添加到播放队列
|
||||
addToQueue({
|
||||
blob: audioItem.blob,
|
||||
segmentIndex: i,
|
||||
text: segments[i]
|
||||
})
|
||||
|
||||
// 如果是第一个片段,取消loading状态
|
||||
if (i === 0 && isLoading.value) {
|
||||
console.log('第一个音频片段已就绪,开始播放')
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (isCancelled || isInterrupted) {
|
||||
console.log('播放被取消或中断,退出播放')
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 所有请求完成
|
||||
console.log('所有音频片段请求完成')
|
||||
allRequestsCompleted = true
|
||||
|
||||
// 3. 立即检查是否可以合并剩余片段
|
||||
tryMergeRemainingSegments()
|
||||
|
||||
// 4. 等待所有音频播放完成
|
||||
while (audioQueue.length > 0 && !isCancelled && !isInterrupted) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
console.log('音频播放完成')
|
||||
|
||||
} catch (error) {
|
||||
console.error('语音播报失败:', error)
|
||||
} finally {
|
||||
// 最终清理
|
||||
if (isCancelled || isInterrupted) {
|
||||
console.log('播放被取消或中断,进行清理')
|
||||
cleanup()
|
||||
} else {
|
||||
console.log('播放正常完成')
|
||||
isSpeaking.value = false
|
||||
isPaused.value = false
|
||||
isLoading.value = false
|
||||
progress.value = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停播放
|
||||
const pause = () => {
|
||||
if (audioContext && isSpeaking.value && !isPaused.value) {
|
||||
audioContext.suspend().then(() => {
|
||||
isPaused.value = true
|
||||
console.log('播放已暂停')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复播放
|
||||
const resume = () => {
|
||||
if (audioContext && isSpeaking.value && isPaused.value) {
|
||||
audioContext.resume().then(() => {
|
||||
isPaused.value = false
|
||||
console.log('播放已恢复')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 取消音频
|
||||
const cancelAudio = () => {
|
||||
console.log('取消音频播放')
|
||||
stopAudio()
|
||||
cleanup()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isSpeaking,
|
||||
isPaused,
|
||||
isLoading,
|
||||
currentText,
|
||||
currentSegmentIndex,
|
||||
totalSegments,
|
||||
progress,
|
||||
|
||||
// 方法
|
||||
speak,
|
||||
pause,
|
||||
resume,
|
||||
cancelAudio,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
@@ -56,16 +56,12 @@ export function useColumnCount(onChange = () => {}) {
|
||||
// if (process.client) {
|
||||
window.addEventListener('resize', calcColumn)
|
||||
// }
|
||||
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
// if (process.client) {
|
||||
// if (process.client) {
|
||||
window.removeEventListener('resize', calcColumn)
|
||||
// }
|
||||
|
||||
|
||||
})
|
||||
|
||||
// 列数变化时执行回调
|
||||
|
||||
@@ -4,9 +4,10 @@ import {
|
||||
} from 'vue'
|
||||
import {
|
||||
$api
|
||||
} from '../common/globalFunction';
|
||||
} from '../common/globalFunction'; // 你的请求封装
|
||||
import config from '@/config'
|
||||
|
||||
// 开源
|
||||
export function useAudioRecorder() {
|
||||
// --- 状态定义 ---
|
||||
const isRecording = ref(false)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {
|
||||
useTTSPlayer as useWebTTS
|
||||
} from '@/hook/useTTSPlayer-web.js'
|
||||
import {
|
||||
useTTSPlayer as useHardwareTTS
|
||||
} from '@/hook/useTTSPlayer-all-in-one.js'
|
||||
import {
|
||||
isY9MachineType
|
||||
} from '../common/globalFunction';
|
||||
|
||||
/**
|
||||
* 智能 TTS 适配器 Hook
|
||||
* 自动判断环境并返回对应的播放器实现
|
||||
*/
|
||||
export function useTTSPlayer() {
|
||||
if (isY9MachineType()) {
|
||||
return useHardwareTTS()
|
||||
} else {
|
||||
return useWebTTS()
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
import {
|
||||
ref,
|
||||
onUnmounted
|
||||
} from 'vue'
|
||||
import {
|
||||
onHide,
|
||||
onUnload
|
||||
} from '@dcloudio/uni-app'
|
||||
import {
|
||||
isY9MachineType
|
||||
} from '../common/globalFunction';
|
||||
|
||||
/**
|
||||
* 封装 hh.call 通用调用
|
||||
*/
|
||||
const callBridge = (params) => {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof window !== 'undefined' && window.hh && window.hh.call) {
|
||||
// 打印日志方便调试
|
||||
console.log('[TTS Bridge Send]:', params)
|
||||
window.hh.call("ampeHHCommunication", params, (res) => {
|
||||
console.log('[TTS Bridge Res]:', res)
|
||||
resolve(res)
|
||||
})
|
||||
} else {
|
||||
console.warn('当前环境不支持 hh.call,模拟成功')
|
||||
resolve({
|
||||
success: true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机 TaskId
|
||||
*/
|
||||
const generateTaskId = () => {
|
||||
return 'task_' + Date.now() + '_' + Math.floor(Math.random() * 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接播放文本 (静态方法,无需实例化 Hook)
|
||||
* @param {string} text - 需要朗读的文本
|
||||
*/
|
||||
export async function playTextDirectly(text) {
|
||||
if (!text) return
|
||||
|
||||
if (!isY9MachineType()) return
|
||||
|
||||
const processedText = extractSpeechText(text)
|
||||
if (!processedText) return
|
||||
|
||||
try {
|
||||
// 构造播放参数
|
||||
const params = {
|
||||
"action": "speech",
|
||||
"event": "open",
|
||||
"taskId": generateTaskId(),
|
||||
"params": {
|
||||
"text": processedText,
|
||||
"pitch": 1.0,
|
||||
"rate": 1.0,
|
||||
"speechQueue": 1 // 1表示打断当前播放,立即播放新的
|
||||
}
|
||||
}
|
||||
// 直接调用
|
||||
await callBridge(params)
|
||||
} catch (e) {
|
||||
console.error('Direct Play Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一体机 TTS 播放器 Hook (修复版)
|
||||
*/
|
||||
export function useTTSPlayer() {
|
||||
// UI 状态
|
||||
const isSpeaking = ref(false) // 是否正在交互
|
||||
const isPaused = ref(false) // 是否处于暂停状态
|
||||
const isLoading = ref(false) // 是否正在加载
|
||||
|
||||
// 记录最后一次播放的文本
|
||||
const lastText = ref('')
|
||||
|
||||
/**
|
||||
* 停止 (中断) - 内部使用
|
||||
*/
|
||||
const executeStop = async () => {
|
||||
const params = {
|
||||
"action": "speechStop",
|
||||
"event": "open",
|
||||
"taskId": generateTaskId()
|
||||
}
|
||||
await callBridge(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心朗读方法
|
||||
*/
|
||||
const speak = async (text) => {
|
||||
if (!text) return
|
||||
|
||||
const processedText = extractSpeechText(text)
|
||||
if (!processedText) return
|
||||
|
||||
// 1. 立即更新记忆文本 (确保即使后续失败,resume也能读到新的)
|
||||
lastText.value = processedText
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 2.【关键修复】先强制停止上一次播报
|
||||
// 虽然 speechQueue:1 理论上可以打断,但显式停止更稳健
|
||||
await executeStop()
|
||||
|
||||
// 3. 构造播放参数
|
||||
const params = {
|
||||
"action": "speech",
|
||||
"event": "open",
|
||||
"taskId": generateTaskId(),
|
||||
"params": {
|
||||
"text": processedText,
|
||||
"pitch": 1.0,
|
||||
"rate": 1.0,
|
||||
"speechQueue": 1 // 1表示打断
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 发送播放指令
|
||||
await callBridge(params)
|
||||
|
||||
// 5. 更新状态
|
||||
isSpeaking.value = true
|
||||
isPaused.value = false
|
||||
} catch (e) {
|
||||
console.error('TTS Speak Error:', e)
|
||||
resetState()
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停
|
||||
*/
|
||||
const pause = async () => {
|
||||
if (!isSpeaking.value || isPaused.value) return
|
||||
try {
|
||||
await executeStop()
|
||||
isPaused.value = true
|
||||
// 注意:不要设置 isSpeaking = false,因为逻辑上只是暂停
|
||||
} catch (e) {
|
||||
console.error("Pause failed:", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复 (由于硬件限制,实际上是从头播放当前文本)
|
||||
*/
|
||||
const resume = async () => {
|
||||
if (!isPaused.value) return
|
||||
|
||||
// 如果有缓存的文本,重新触发 speak
|
||||
if (lastText.value) {
|
||||
console.log('[TTS Resume] Re-speaking text:', lastText.value.substring(0, 20) + '...')
|
||||
// 调用 speak 会自动处理 stop 和状态更新
|
||||
await speak(lastText.value)
|
||||
} else {
|
||||
isPaused.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 播放/暂停
|
||||
* 注意:如果是切换了新文章,请直接调用 speak(newText),不要调用 togglePlay
|
||||
*/
|
||||
const togglePlay = () => {
|
||||
// 如果当前是暂停状态,则恢复
|
||||
if (isPaused.value) {
|
||||
resume()
|
||||
}
|
||||
// 如果当前正在播放,则暂停
|
||||
else if (isSpeaking.value) {
|
||||
pause()
|
||||
}
|
||||
// 如果既没播放也没暂停(比如刚进页面),需要业务层调用 speak 启动,这里无法自动推断文本
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止并重置
|
||||
*/
|
||||
const stop = async () => {
|
||||
await executeStop()
|
||||
resetState()
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
isSpeaking.value = false
|
||||
isPaused.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// === 生命周期管理 ===
|
||||
onUnmounted(() => stop())
|
||||
if (typeof onHide === 'function') onHide(() => stop())
|
||||
if (typeof onUnload === 'function') onUnload(() => stop())
|
||||
|
||||
return {
|
||||
speak,
|
||||
pause,
|
||||
resume,
|
||||
togglePlay,
|
||||
stop,
|
||||
cancelAudio: stop,
|
||||
isSpeaking,
|
||||
isPaused,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本提取工具函数 (保持不变)
|
||||
*/
|
||||
function extractSpeechText(markdown) {
|
||||
if (!markdown || markdown.indexOf('job-json') === -1) {
|
||||
return markdown;
|
||||
}
|
||||
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
|
||||
const jobs = [];
|
||||
let match;
|
||||
let lastJobEndIndex = 0;
|
||||
let firstJobStartIndex = -1;
|
||||
|
||||
while ((match = jobRegex.exec(markdown)) !== null) {
|
||||
const jobStr = match[1];
|
||||
try {
|
||||
const job = JSON.parse(jobStr);
|
||||
jobs.push(job);
|
||||
if (firstJobStartIndex === -1) {
|
||||
firstJobStartIndex = match.index;
|
||||
}
|
||||
lastJobEndIndex = jobRegex.lastIndex;
|
||||
} catch (e) {
|
||||
console.warn('JSON 解析失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
const guideText = firstJobStartIndex > 0 ?
|
||||
markdown.slice(0, firstJobStartIndex).trim() : '';
|
||||
const endingText = lastJobEndIndex < markdown.length ?
|
||||
markdown.slice(lastJobEndIndex).trim() : '';
|
||||
|
||||
const jobTexts = jobs.map((job, index) => {
|
||||
// 处理薪资格式
|
||||
let salaryText = job.salary;
|
||||
if (salaryText) {
|
||||
// 匹配 "XXXXX-XXXXX元/月" 格式
|
||||
const rangeMatch = salaryText.match(/(\d+)-(\d+)元\/月/);
|
||||
if (rangeMatch) {
|
||||
const minSalary = parseInt(rangeMatch[1], 10);
|
||||
const maxSalary = parseInt(rangeMatch[2], 10);
|
||||
|
||||
// 转换为千位单位
|
||||
const minK = Math.round(minSalary / 1000);
|
||||
const maxK = Math.round(maxSalary / 1000);
|
||||
|
||||
salaryText = `${minK}千到${maxK}千每月`;
|
||||
}
|
||||
// 如果不是 "XXXXX-XXXXX元/月" 格式,保持原样
|
||||
}
|
||||
|
||||
return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${salaryText},地点:${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
|
||||
// import { useTTSPlayer } from '@/hook/useTTSPlayer-all-in-one'
|
||||
|
||||
// const { speak, stop, isSpeaking } = useTTSPlayer()
|
||||
|
||||
// // 调用
|
||||
// speak('你好,这是一段测试文本')
|
||||
|
||||
// 使用方法 2
|
||||
// import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one'
|
||||
|
||||
// // 直接调用即可,无需实例化
|
||||
// playTextDirectly('直接朗读这段话,不需要处理暂停和UI状态')
|
||||
@@ -18,28 +18,12 @@ export function useTTSPlayer() {
|
||||
// 单例 Piper 实例
|
||||
let piper = null
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 地址 (含 Token)
|
||||
*/
|
||||
const getWsUrl = async () => {
|
||||
let wsUrl = config.speechSynthesis
|
||||
|
||||
// 拼接 Token
|
||||
const token = uni.getStorageSync('token') || '';
|
||||
if (token) {
|
||||
const separator = wsUrl.includes('?') ? '&' : '?';
|
||||
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
return wsUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建 SDK 实例
|
||||
*/
|
||||
const getPiperInstance = async () => {
|
||||
const getPiperInstance = () => {
|
||||
if (!piper) {
|
||||
|
||||
let baseUrl = await getWsUrl()
|
||||
let baseUrl = config.speechSynthesis2 || ''
|
||||
baseUrl = baseUrl.replace(/\/$/, '')
|
||||
|
||||
piper = new PiperTTS({
|
||||
@@ -77,7 +61,7 @@ export function useTTSPlayer() {
|
||||
const processedText = extractSpeechText(text)
|
||||
if (!processedText) return
|
||||
|
||||
const instance = await getPiperInstance()
|
||||
const instance = getPiperInstance()
|
||||
|
||||
// 重置状态
|
||||
isLoading.value = true
|
||||
@@ -209,59 +193,36 @@ export function useTTSPlayer() {
|
||||
*/
|
||||
function extractSpeechText(markdown) {
|
||||
if (!markdown || markdown.indexOf('job-json') === -1) {
|
||||
return markdown;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
lastJobEndIndex = jobRegex.lastIndex;
|
||||
} catch (e) {
|
||||
console.warn('JSON 解析失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
const guideText = firstJobStartIndex > 0 ?
|
||||
markdown.slice(0, firstJobStartIndex).trim() : '';
|
||||
markdown.slice(0, firstJobStartIndex).trim() : '';
|
||||
const endingText = lastJobEndIndex < markdown.length ?
|
||||
markdown.slice(lastJobEndIndex).trim() : '';
|
||||
|
||||
markdown.slice(lastJobEndIndex).trim() : '';
|
||||
const jobTexts = jobs.map((job, index) => {
|
||||
// 处理薪资格式
|
||||
let salaryText = job.salary;
|
||||
if (salaryText) {
|
||||
// 匹配 "XXXXX-XXXXX元/月" 格式
|
||||
const rangeMatch = salaryText.match(/(\d+)-(\d+)元\/月/);
|
||||
if (rangeMatch) {
|
||||
const minSalary = parseInt(rangeMatch[1], 10);
|
||||
const maxSalary = parseInt(rangeMatch[2], 10);
|
||||
|
||||
// 转换为千位单位
|
||||
const minK = Math.round(minSalary / 1000);
|
||||
const maxK = Math.round(maxSalary / 1000);
|
||||
|
||||
salaryText = `${minK}千到${maxK}千每月`;
|
||||
}
|
||||
// 如果不是 "XXXXX-XXXXX元/月" 格式,保持原样
|
||||
}
|
||||
|
||||
return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${salaryText},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}。`;
|
||||
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');
|
||||
}
|
||||
5
main.js
5
main.js
@@ -26,12 +26,9 @@ import renderCompanyCollectionRecord from '@/components/renderCompanyCollectionR
|
||||
import renderJobViewRecord from '@/components/renderJobViewRecord/renderJobViewRecord.vue';
|
||||
import MyIcons from '@/components/My-icons/my-icons.vue';
|
||||
import GlobalPopup from '@/components/GlobalPopup/GlobalPopup.vue'
|
||||
import FileIcon from '@/components/FileIcon/fileIcon.vue'
|
||||
import FileText from '@/components/FileText/fileText.vue'
|
||||
// import Tabbar from '@/components/tabbar/midell-box.vue'
|
||||
// 自动导入 directives 目录下所有指令
|
||||
console.log(lightAppJssdk)
|
||||
|
||||
const directives = import.meta.glob('./directives/*.js', {
|
||||
eager: true
|
||||
});
|
||||
@@ -61,8 +58,6 @@ export function createApp() {
|
||||
app.component('renderJobViewRecord', renderJobViewRecord) //渲染岗位浏览记录
|
||||
app.component('MyIcons', MyIcons)
|
||||
app.component('global-popup', GlobalPopup)
|
||||
app.component('FileIcon', FileIcon)
|
||||
app.component('FileText', FileText)
|
||||
// app.component('tabbar-custom', Tabbar)
|
||||
|
||||
for (const path in directives) {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wxdbdcc6a10153c99b",
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false,
|
||||
"es6" : true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppLayout title="" :use-scroll-view="false" showHeader>
|
||||
<AppLayout title="" :use-scroll-view="false">
|
||||
<template #headerleft v-if="isMiniProgram">
|
||||
<view class="btnback">
|
||||
<image src="@/static/icon/back.png" @click="navBack"></image>
|
||||
@@ -75,13 +75,11 @@ 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';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { isMiniProgram } = storeToRefs(useUserStore());
|
||||
const { checkAuth } = useUserStore();
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import useLocationStore from '@/stores/useLocationStore';
|
||||
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
|
||||
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const pageState = reactive({
|
||||
@@ -112,9 +110,6 @@ onLoad((options) => {
|
||||
});
|
||||
|
||||
function companyCollection() {
|
||||
if (!checkAuth()) {
|
||||
return
|
||||
}
|
||||
if (dataType.value === 2) {
|
||||
// 第三方数据收藏逻辑
|
||||
const id = companyInfo.value.id;
|
||||
@@ -124,13 +119,11 @@ function companyCollection() {
|
||||
$api.createRequest(`/app/company/collection/${id}/2`, {}, 'DELETE').then((resData) => {
|
||||
getCompanyInfo(companyId, zphId);
|
||||
$api.msg('取消收藏成功');
|
||||
playTextDirectly('取消收藏成功');
|
||||
});
|
||||
} else {
|
||||
$api.createRequest(`/app/company/collection/${id}/2`, {}, 'POST').then((resData) => {
|
||||
getCompanyInfo(companyId, zphId);
|
||||
$api.msg('收藏成功');
|
||||
playTextDirectly('收藏成功');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -140,13 +133,11 @@ function companyCollection() {
|
||||
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'DELETE').then((resData) => {
|
||||
getCompanyInfo(companyId);
|
||||
$api.msg('取消收藏成功');
|
||||
playTextDirectly('取消收藏成功');
|
||||
});
|
||||
} else {
|
||||
$api.createRequest(`/app/company/collection/${companyId}/1`, {}, 'POST').then((resData) => {
|
||||
getCompanyInfo(companyId);
|
||||
$api.msg('收藏成功');
|
||||
playTextDirectly('收藏成功');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -269,7 +260,8 @@ image {
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
.content-top{
|
||||
padding:28rpx;
|
||||
padding: 28rpx
|
||||
padding-top: 50rpx
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: nowrap
|
||||
|
||||
@@ -76,9 +76,6 @@ import { storeToRefs } from 'pinia';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { getUserResume } = useUserStore();
|
||||
const { userInfo, isMiniProgram ,hasLogin,isMachineEnv} = storeToRefs(useUserStore());
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
|
||||
|
||||
const popup = ref(null);
|
||||
const selectJobsModel = ref(null);
|
||||
|
||||
@@ -115,17 +112,12 @@ function handleFocus() {
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if(!hasLogin.value){
|
||||
useUserStore().logOut()
|
||||
return
|
||||
}
|
||||
const { id } = dataItem.value;
|
||||
let ids = userInfo.value.jobTitleId + `,${id}`;
|
||||
const result = dedupeAndCheck(ids);
|
||||
if (result.hasDuplicate) {
|
||||
popup.value.close();
|
||||
$api.msg('期望岗位已重复');
|
||||
playTextDirectly('期望岗位已重复')
|
||||
return;
|
||||
}
|
||||
complete({ jobTitleId: result.deduplicated });
|
||||
@@ -179,17 +171,14 @@ function handelChangeInpute(e) {
|
||||
function handelClickItem(item) {
|
||||
dataItem.value = item;
|
||||
popup.value.open();
|
||||
playTextDirectly('确添加该期望岗位吗?')
|
||||
}
|
||||
|
||||
function complete(values) {
|
||||
if (!values.jobTitleId.length) {
|
||||
playTextDirectly('至少添加一份期望岗位?')
|
||||
return $api.msg('至少添加一份期望岗位');
|
||||
}
|
||||
$api.createRequest('/app/user/resume', values, 'post').then((resData) => {
|
||||
$api.msg('操作成功');
|
||||
playTextDirectly('操作成功')
|
||||
$api.msg('完成');
|
||||
getUserResume();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,9 +41,6 @@ import useLocationStore from '@/stores/useLocationStore';
|
||||
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { isMiniProgram } = storeToRefs(useUserStore());
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
|
||||
|
||||
// state
|
||||
const title = ref('事业单位');
|
||||
const cardInfo = ref({});
|
||||
@@ -69,7 +66,6 @@ onLoad(() => {
|
||||
|
||||
// search
|
||||
function searchCollection(e) {
|
||||
playTextDirectly('正在为您查找…')
|
||||
const value = e.detail.value;
|
||||
pageState.search.companyName = value;
|
||||
getDataList('refresh');
|
||||
|
||||
@@ -112,8 +112,6 @@ import { storeToRefs } from 'pinia';
|
||||
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { isMiniProgram } = storeToRefs(useUserStore());
|
||||
const { checkAuth } = useUserStore();
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const fairInfo = ref({});
|
||||
@@ -203,24 +201,19 @@ function expand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
}
|
||||
|
||||
// 取消/预约招聘会
|
||||
// 取消/收藏岗位
|
||||
function applyExhibitors() {
|
||||
if (!checkAuth()) {
|
||||
return
|
||||
}
|
||||
|
||||
const fairId = fairInfo.value.zphID;
|
||||
if (fairInfo.value.isCollection) {
|
||||
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'DELETE').then((resData) => {
|
||||
getJobFairInfo(fairId);
|
||||
$api.msg('取消预约成功');
|
||||
playTextDirectly('取消预约成功')
|
||||
});
|
||||
$api.msg('已预约成功');
|
||||
} else {
|
||||
$api.createRequest(`/app/fair/collection/${fairId}`, {}, 'POST').then((resData) => {
|
||||
getJobFairInfo(fairId);
|
||||
$api.msg('预约成功');
|
||||
playTextDirectly('预约成功')
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -300,7 +293,8 @@ image {
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
.content-top{
|
||||
padding:28rpx;
|
||||
padding: 28rpx
|
||||
padding-top: 50rpx
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: nowrap
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
<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 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 fileList = ref([]);
|
||||
const deleting = ref(false);
|
||||
|
||||
const fileCount = computed(() => {
|
||||
return fileList.value.length ?? 0;
|
||||
});
|
||||
|
||||
// 计算加载文本
|
||||
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('文件地址丢失');
|
||||
}
|
||||
}
|
||||
async function delFile(file, idx) {
|
||||
deleting.value = true;
|
||||
try {
|
||||
await $api.createRequest(`/app/kiosk/remove?sessionId=${uuid.value}&ids=${file.id}`, {}, 'post', true);
|
||||
} catch (error) {
|
||||
$api.msg(error);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
fileList.value.splice(idx, 1);
|
||||
if(fileList.value.length == 0){
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
fileList.value = [];
|
||||
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}&accept='.pdf,.png,.jpg'`;
|
||||
|
||||
// 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 || deleting.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;
|
||||
}
|
||||
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>
|
||||
@@ -36,7 +36,7 @@
|
||||
<image
|
||||
class="button-click"
|
||||
src="@/static/icon/edit1.png"
|
||||
@click="handleNavTo('/packageA/pages/personalInfo/personalInfo')"
|
||||
@click="navTo('/packageA/pages/personalInfo/personalInfo')"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
@@ -74,7 +74,7 @@
|
||||
<image
|
||||
class="icon"
|
||||
src="@/static/icon/edit1.png"
|
||||
@click="handleNavTo('/packageA/pages/jobExpect/jobExpect')"
|
||||
@click="navTo('/packageA/pages/jobExpect/jobExpect')"
|
||||
></image>
|
||||
</view>
|
||||
<view class="mys-text">
|
||||
@@ -97,7 +97,7 @@
|
||||
<view class="mys-info" style="padding: 0">
|
||||
<view class="mys-h4">
|
||||
<text>工作经历</text>
|
||||
<view class="mys-edit-icon btn-tada" @click="handleNavTo('/packageA/pages/workExp/workExp')">
|
||||
<view class="mys-edit-icon btn-tada" @click="navTo('/packageA/pages/workExp/workExp')">
|
||||
<image class="icon button-click btn-feel" src="@/static/icon/plus.png"></image>
|
||||
<view class="txt">添加</view>
|
||||
</view>
|
||||
@@ -108,7 +108,7 @@
|
||||
<image
|
||||
class="icon btn-feel"
|
||||
src="@/static/icon/edit1.png"
|
||||
@click="handleNavTo(`/packageA/pages/workExp/workExp?id=${item.id}`)"
|
||||
@click="navTo(`/packageA/pages/workExp/workExp?id=${item.id}`)"
|
||||
></image>
|
||||
</view>
|
||||
<view class="mys-text fl_box fl_justbet">
|
||||
@@ -127,7 +127,6 @@
|
||||
<view class="footer-button btn-feel" @click="chooseResume">上传简历</view>
|
||||
</view>
|
||||
</template>
|
||||
<UploadQrcode ref="qrcodeRef" @onSend="handleFileSend" :leaveFileCount="1" ></UploadQrcode>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -138,35 +137,21 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import useDictStore from '@/stores/useDictStore';
|
||||
const { userInfo, isMiniProgram,isMachineEnv } = storeToRefs(useUserStore());
|
||||
const { getUserResume, checkAuth } = useUserStore();
|
||||
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
|
||||
const { getUserResume } = useUserStore();
|
||||
const { getDictData, oneDictData } = useDictStore();
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
import config from '@/config.js';
|
||||
import UploadQrcode from './components/uploadQrcode.vue';
|
||||
|
||||
const qrcodeRef = ref(null);
|
||||
const showNotice = ref(true);
|
||||
onLoad(() => {
|
||||
getUserResume();
|
||||
});
|
||||
|
||||
function handleNavTo(url) {
|
||||
if(!checkAuth()) {
|
||||
return
|
||||
}
|
||||
navTo(url)
|
||||
}
|
||||
|
||||
function closeNotice() {
|
||||
showNotice.value = false;
|
||||
}
|
||||
|
||||
function chooseResume() {
|
||||
if(isMachineEnv.value){
|
||||
qrcodeRef.value?.open()
|
||||
return
|
||||
}
|
||||
uni.chooseImage({
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
@@ -177,11 +162,9 @@ function chooseResume() {
|
||||
res = JSON.parse(res);
|
||||
getUserResume();
|
||||
$api.msg('上传成功');
|
||||
playTextDirectly('上传成功')
|
||||
})
|
||||
.catch((err) => {
|
||||
$api.msg('上传失败');
|
||||
playTextDirectly('上传失败')
|
||||
});
|
||||
},
|
||||
fail: (error) => {},
|
||||
@@ -226,22 +209,6 @@ function uploadResume(tempFilePath, loading) {
|
||||
});
|
||||
});
|
||||
}
|
||||
const handleFileSend = (rows)=>{
|
||||
const file = {
|
||||
url: rows[0].fileUrl,
|
||||
type: rows[0].fileSuffix,
|
||||
name: rows[0].originalName,
|
||||
}
|
||||
$api.createRequest(`/app/oss/uploadToObs?userId=${userInfo.value.userId}&filePath=${encodeURI(file.url)}`, { }, 'POST',true,).then((res) => {
|
||||
getUserResume();
|
||||
$api.msg('上传成功');
|
||||
playTextDirectly('上传失败')
|
||||
}).catch(()=>{
|
||||
$api.msg('上传失败');
|
||||
playTextDirectly('上传失败')
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppLayout title="" backGorundColor="#F4F4F4" showHeader>
|
||||
<AppLayout title="" backGorundColor="#F4F4F4">
|
||||
<template #headerleft v-if="isMiniProgram">
|
||||
<view class="btnback">
|
||||
<image src="@/static/icon/back.png" @click="navBack"></image>
|
||||
@@ -185,9 +185,7 @@ import dictLabel from '@/components/dict-Label/dict-Label.vue';
|
||||
import RadarMap from './component/radarMap.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
const { isMiniProgram, hasLogin } = storeToRefs(useUserStore());
|
||||
const { checkAuth } = useUserStore();
|
||||
|
||||
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
|
||||
import config from '@/config.js';
|
||||
@@ -323,26 +321,20 @@ function getCompetivetuveness(jobId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 申请岗位 第三方需要认证
|
||||
// 申请岗位
|
||||
function jobApply() {
|
||||
if (dataType.value === 2) {
|
||||
// 第三方数据申请逻辑
|
||||
|
||||
if (!checkAuth()) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
jobid: jobInfo.value.id,
|
||||
jobname: jobInfo.value.gwmc,
|
||||
};
|
||||
if (jobInfo.value.isApply) {
|
||||
$api.msg('已经投递过该岗位了~');
|
||||
playTextDirectly('已经投递过该岗位了');
|
||||
return;
|
||||
} else {
|
||||
$api.createRequest(`/app/internal/sendResume`, params, 'POST').then((resData) => {
|
||||
$api.msg('投递成功');
|
||||
playTextDirectly('投递成功');
|
||||
getDetail(jobIdRef.value);
|
||||
});
|
||||
}
|
||||
@@ -356,7 +348,6 @@ function jobApply() {
|
||||
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
|
||||
getDetail(jobId);
|
||||
$api.msg('申请成功');
|
||||
playTextDirectly('申请成功');
|
||||
const jobUrl = jobInfo.value.jobUrl;
|
||||
return window.open(jobUrl);
|
||||
});
|
||||
@@ -364,11 +355,8 @@ function jobApply() {
|
||||
}
|
||||
}
|
||||
|
||||
// 取消/收藏岗位 都需要认证
|
||||
// 取消/收藏岗位
|
||||
function jobCollection() {
|
||||
if (!checkAuth()) {
|
||||
return
|
||||
}
|
||||
if (dataType.value === 2) {
|
||||
// 第三方数据收藏逻辑
|
||||
const id = jobInfo.value.id;
|
||||
@@ -376,13 +364,11 @@ function jobCollection() {
|
||||
$api.createRequest(`/app/job/collection/${id}/2`, {}, 'DELETE').then((resData) => {
|
||||
getDetail(jobIdRef.value);
|
||||
$api.msg('取消收藏成功');
|
||||
playTextDirectly('取消收藏成功');
|
||||
});
|
||||
} else {
|
||||
$api.createRequest(`/app/job/collection/${id}/2`, {}, 'POST').then((resData) => {
|
||||
getDetail(jobIdRef.value);
|
||||
$api.msg('收藏成功');
|
||||
playTextDirectly('收藏成功');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -392,13 +378,11 @@ function jobCollection() {
|
||||
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'DELETE').then((resData) => {
|
||||
getDetail(jobId);
|
||||
$api.msg('取消收藏成功');
|
||||
playTextDirectly('取消收藏成功');
|
||||
});
|
||||
} else {
|
||||
$api.createRequest(`/app/job/collection/${jobId}/1`, {}, 'POST').then((resData) => {
|
||||
getDetail(jobId);
|
||||
$api.msg('收藏成功');
|
||||
playTextDirectly('收藏成功');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,7 @@
|
||||
{
|
||||
"path": "pages/search/search",
|
||||
"style": {
|
||||
"navigationBarTitleText": "搜索",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/auth",
|
||||
"style": {
|
||||
"navigationBarTitleText": "实名认证",
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,643 +0,0 @@
|
||||
<template>
|
||||
<AppLayout title="">
|
||||
<template #headerleft v-if="isMiniProgram">
|
||||
<view >
|
||||
<image class="btnback" src="@/static/icon/back.png" @click="navBack"></image>
|
||||
</view>
|
||||
</template>
|
||||
<view class="auth-container">
|
||||
<view class="auth-header">
|
||||
<view class="auth-title">身份认证</view>
|
||||
<view class="auth-subtitle">请填写您的身份信息进行验证</view>
|
||||
</view>
|
||||
<view class="auth-form">
|
||||
<view class="form-item" >
|
||||
<view class="form-label">
|
||||
身份证号
|
||||
<text v-if="idCardError" class="error-text">{{ idCardError }}</text>
|
||||
</view>
|
||||
<view class="form-input-wrapper" :class="{ 'error': idCardError }">
|
||||
<input
|
||||
class="form-input"
|
||||
type="idcard"
|
||||
v-model="formData.idCard"
|
||||
placeholder="请输入18位身份证号码"
|
||||
maxlength="18"
|
||||
@input="onIdCardInput"
|
||||
@blur="validateIdCard"
|
||||
/>
|
||||
<view class="input-clear" v-if="formData.idCard" @click="clearField('idCard')">
|
||||
<my-icons type="close" size="36" color="#ccc"></my-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view class="form-item" >
|
||||
<view class="form-label">
|
||||
手机号
|
||||
<text v-if="phoneError" class="error-text">{{ phoneError }}</text>
|
||||
</view>
|
||||
<view class="form-input-wrapper" :class="{ 'error': phoneError }">
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
v-model="formData.phone"
|
||||
placeholder="请输入11位手机号码"
|
||||
maxlength="11"
|
||||
@input="onPhoneInput"
|
||||
@blur="validatePhone"
|
||||
/>
|
||||
<view class="input-clear" v-if="formData.phone" @click="clearField('phone')">
|
||||
<my-icons type="close" size="36" color="#ccc"></my-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<view class="form-item" >
|
||||
<view class="form-label">
|
||||
验证码
|
||||
<text v-if="codeError" class="error-text">{{ codeError }}</text>
|
||||
</view>
|
||||
<view class="form-input-wrapper" :class="{ 'error': codeError }">
|
||||
<input
|
||||
class="form-input code-input"
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
@input="onCodeInput"
|
||||
@blur="validateCode"
|
||||
/>
|
||||
<view class="send-code-btn btn-feel"
|
||||
:class="{ 'disabled': !canSendCode }"
|
||||
@click="sendCode">
|
||||
{{ codeBtnText }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 认证按钮 -->
|
||||
<view class="auth-btn-container">
|
||||
<button class="auth-btn btn-feel" :class="{ 'disabled': !canSubmit }" @click="submitAuth">
|
||||
确认认证
|
||||
</button>
|
||||
<view class="auth-tips">
|
||||
认证信息仅用于身份验证,我们将严格保护您的隐私
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed, onMounted ,onUnmounted ,inject} from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
const { $api ,navBack} = inject('globalFunction');
|
||||
const { isMiniProgram } = storeToRefs(useUserStore());
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
idCard: '',
|
||||
phone: '',
|
||||
code: ''
|
||||
});
|
||||
|
||||
// 错误提示
|
||||
const idCardError = ref('');
|
||||
const phoneError = ref('');
|
||||
const codeError = ref('');
|
||||
|
||||
// 验证码倒计时
|
||||
const codeCountdown = ref(0);
|
||||
const codeTimer = ref(null);
|
||||
const codeBtnText = ref('发送验证码');
|
||||
|
||||
|
||||
|
||||
|
||||
const canSendCode = computed(() => {
|
||||
return codeCountdown.value === 0 && formData.value.phone.length === 11 && !phoneError.value;
|
||||
});
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return formData.value.idCard && formData.value.phone && formData.value.code &&
|
||||
!idCardError.value && !phoneError.value && !codeError.value;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 身份证输入处理
|
||||
const onIdCardInput = (e) => {
|
||||
formData.value.idCard = e.detail.value.toUpperCase();
|
||||
idCardError.value = '';
|
||||
};
|
||||
|
||||
// 手机号输入处理
|
||||
const onPhoneInput = (e) => {
|
||||
formData.value.phone = e.detail.value.replace(/[^\d]/g, '');
|
||||
phoneError.value = '';
|
||||
// 如果手机号变化且验证码已发送,清空验证码
|
||||
if (formData.value.code) {
|
||||
formData.value.code = '';
|
||||
codeError.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 验证码输入处理
|
||||
const onCodeInput = (e) => {
|
||||
// formData.value.code = e.detail.value.replace(/[^\d]/g, '');
|
||||
codeError.value = '';
|
||||
};
|
||||
|
||||
// 清空字段
|
||||
const clearField = (field) => {
|
||||
formData.value[field] = '';
|
||||
switch(field) {
|
||||
case 'idCard':
|
||||
idCardError.value = '';
|
||||
break;
|
||||
case 'phone':
|
||||
phoneError.value = '';
|
||||
break;
|
||||
case 'code':
|
||||
codeError.value = '';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 验证身份证号
|
||||
const validateIdCard = () => {
|
||||
if (!formData.value.idCard) {
|
||||
idCardError.value = '请输入身份证号码';
|
||||
return false;
|
||||
}
|
||||
|
||||
const idCard = formData.value.idCard.trim();
|
||||
if (idCard.length !== 18) {
|
||||
idCardError.value = '身份证号码必须为18位';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 身份证号格式校验
|
||||
const idCardPattern = /^\d{17}[\dXx]$/;
|
||||
if (!idCardPattern.test(idCard)) {
|
||||
idCardError.value = '身份证号码格式不正确';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验码验证
|
||||
if (!validateIdCardCheckCode(idCard)) {
|
||||
idCardError.value = '身份证号码校验失败';
|
||||
return false;
|
||||
}
|
||||
|
||||
idCardError.value = '';
|
||||
return true;
|
||||
};
|
||||
|
||||
// 身份证校验码验证
|
||||
const validateIdCardCheckCode = (idCard) => {
|
||||
if (idCard.length !== 18) return false;
|
||||
|
||||
const weight = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += parseInt(idCard[i]) * weight[i];
|
||||
}
|
||||
|
||||
const checkCode = checkCodes[sum % 11];
|
||||
return checkCode === idCard[17].toUpperCase();
|
||||
};
|
||||
|
||||
// 验证手机号
|
||||
const validatePhone = () => {
|
||||
if (!formData.value.phone) {
|
||||
phoneError.value = '请输入手机号码';
|
||||
return false;
|
||||
}
|
||||
|
||||
const phone = formData.value.phone.trim();
|
||||
if (phone.length !== 11) {
|
||||
phoneError.value = '手机号码必须为11位';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 手机号格式校验
|
||||
const phonePattern = /^1[3-9]\d{9}$/;
|
||||
if (!phonePattern.test(phone)) {
|
||||
phoneError.value = '手机号码格式不正确';
|
||||
return false;
|
||||
}
|
||||
|
||||
phoneError.value = '';
|
||||
return true;
|
||||
};
|
||||
|
||||
// 验证验证码
|
||||
const validateCode = () => {
|
||||
if (!formData.value.code) {
|
||||
codeError.value = '请输入验证码';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.value.code.length < 4) {
|
||||
codeError.value = '请输入正确的验证码';
|
||||
return false;
|
||||
}
|
||||
|
||||
codeError.value = '';
|
||||
return true;
|
||||
};
|
||||
|
||||
// 发送验证码
|
||||
const sendCode = async () => {
|
||||
if (!canSendCode.value) {
|
||||
if (!formData.value.phone) {
|
||||
phoneError.value = '请输入手机号码';
|
||||
} else if (formData.value.phone.length !== 11) {
|
||||
phoneError.value = '手机号码必须为11位';
|
||||
} else if (phoneError.value) {
|
||||
// 已有错误提示
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
if (!validatePhone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
codeCountdown.value = 60;
|
||||
updateCodeBtnText();
|
||||
|
||||
codeTimer.value = setInterval(() => {
|
||||
codeCountdown.value--;
|
||||
updateCodeBtnText();
|
||||
if (codeCountdown.value <= 0) {
|
||||
clearInterval(codeTimer.value);
|
||||
codeTimer.value = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
// 调用发送短信验证码接口
|
||||
await $api.createRequest(`/app/sendCaptchaMessage/${formData.value.phone}`, {}, 'get');
|
||||
$api.msg('验证码已发送')
|
||||
playTextDirectly('验证码已发送');
|
||||
} catch (error) {
|
||||
// 发送失败,重置倒计时
|
||||
codeCountdown.value = 0;
|
||||
clearInterval(codeTimer.value);
|
||||
codeTimer.value = null;
|
||||
updateCodeBtnText();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新验证码按钮文字
|
||||
const updateCodeBtnText = () => {
|
||||
if (codeCountdown.value > 0) {
|
||||
codeBtnText.value = `${codeCountdown.value}s后重新发送`;
|
||||
} else {
|
||||
codeBtnText.value = '发送验证码';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 提交认证
|
||||
const submitAuth = async () => {
|
||||
if (!canSubmit.value) {
|
||||
// 触发表单验证
|
||||
validateIdCard();
|
||||
validatePhone();
|
||||
validateCode();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 调用身份认证接口
|
||||
const params = {
|
||||
idNumber: formData.value.idCard.toUpperCase(),
|
||||
phone: formData.value.phone,
|
||||
captchaStr: formData.value.code
|
||||
};
|
||||
|
||||
const result = await $api.createRequest('/app/user/cert', params, 'post');
|
||||
|
||||
// 认证成功
|
||||
$api.msg('身份认证成功')
|
||||
playTextDirectly('身份认证成功');
|
||||
|
||||
// 保存认证信息到store
|
||||
useUserStore().getUserResume().then(()=>{
|
||||
setTimeout(() => {
|
||||
navBack()
|
||||
}, 500);
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (codeTimer.value) {
|
||||
clearInterval(codeTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20rpx 40rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.auth-form {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 48rpx;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
animation-fill-mode: both;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0.1s; }
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.3s; }
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 24rpx;
|
||||
color: #ff4d4f;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.form-input-wrapper {
|
||||
position: relative;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e8e8e8;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2rpx rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.code-input {
|
||||
padding-right: 220rpx;
|
||||
}
|
||||
|
||||
.input-clear {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 64rpx;
|
||||
min-width: 120rpx;
|
||||
padding: 0 20rpx;
|
||||
background: #1677ff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: #0958d9;
|
||||
transform: translateY(-50%) scale(0.98);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #d9d9d9;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.auth-btn-container {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
background: #1677ff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: #0958d9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #d9d9d9;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-tips {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-top: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
.auth-status {
|
||||
margin-top: 40rpx;
|
||||
padding: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
|
||||
&.success {
|
||||
background: #f6ffed;
|
||||
border: 1rpx solid #b7eb8f;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #fff2f0;
|
||||
border: 1rpx solid #ffccc7;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
background: #f0f5ff;
|
||||
border: 1rpx solid #adc6ff;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.btnback{
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 40rpx;
|
||||
top: 40rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
.code-input-container {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 200rpx;
|
||||
top: 20rpx;
|
||||
bottom: 20rpx;
|
||||
width: 1rpx;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.form-input-wrapper.error {
|
||||
border-color: #ff4d4f;
|
||||
animation: shake 0.5s;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(10rpx); }
|
||||
}
|
||||
</style>
|
||||
@@ -78,12 +78,6 @@
|
||||
src="/static/icon/stop.png"
|
||||
@click="stopMarkdown(msg.displayText, index)"
|
||||
></image>
|
||||
<image
|
||||
v-else-if="isLoading && speechIndex === index"
|
||||
class="controll-icon mar_le10 btn-light"
|
||||
src="/static/icon/audio-fetching.png"
|
||||
@click="stopMarkdown(msg.displayText, index)"
|
||||
></image>
|
||||
<image
|
||||
class="controll-icon mar_le10 btn-light"
|
||||
src="/static/icon/broadcast.png"
|
||||
@@ -273,10 +267,13 @@ 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/useRealtimeRecorder.js';
|
||||
import { useAudioSpeak } from '@/hook/useAudioSpeak.js';
|
||||
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
||||
// 全局
|
||||
const { $api, navTo, throttle } = inject('globalFunction');
|
||||
const emit = defineEmits(['onConfirm']);
|
||||
@@ -299,7 +296,7 @@ const {
|
||||
lastFinalText,
|
||||
} = useAudioRecorder();
|
||||
// 语音合成
|
||||
const { speak, pause, resume, isSpeaking, isPaused, isLoading, cancelAudio,cleanup } = useAudioSpeak();
|
||||
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
|
||||
|
||||
// state
|
||||
const queries = ref([]);
|
||||
@@ -351,14 +348,8 @@ const leaveFileCount = computed(()=>{ //还剩多少文件可以上传 给扫码
|
||||
onMounted(async () => {
|
||||
changeQueries();
|
||||
scrollToBottom();
|
||||
isAudioPermission.value = await requestMicPermission();
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
console.log('清理TTS资源')
|
||||
cleanup()
|
||||
})
|
||||
|
||||
isAudioPermission.value = await requestMicPermission();
|
||||
});
|
||||
|
||||
const requestMicPermission = async () => {
|
||||
try {
|
||||
@@ -732,8 +723,8 @@ function colseFeeBack() {
|
||||
}
|
||||
|
||||
function readMarkdown(value, index) {
|
||||
speechIndex.value = index;
|
||||
if (speechIndex.value !== index) {
|
||||
speechIndex.value = index;
|
||||
speak(value);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
|
||||
<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';
|
||||
@@ -64,12 +66,9 @@ const pollingTimer = ref(null);
|
||||
const isPolling = ref(false);
|
||||
const isVisible = ref(false);
|
||||
const uuid = ref(null);
|
||||
const fileCount = ref(0);
|
||||
const fileList = ref([]);
|
||||
const deleting = ref(false);
|
||||
|
||||
const fileCount = computed(() => {
|
||||
return fileList.value.length ?? 0;
|
||||
});
|
||||
const delFiles = ref([]); //本地记录删除的文件
|
||||
|
||||
// 计算加载文本
|
||||
const loadingText = computed(() => ({
|
||||
@@ -114,19 +113,10 @@ function preViewImage(file) {
|
||||
$api.msg('文件地址丢失');
|
||||
}
|
||||
}
|
||||
async function delFile(file, idx) {
|
||||
deleting.value = true;
|
||||
try {
|
||||
await $api.createRequest(`/app/kiosk/remove?sessionId=${uuid.value}&ids=${file.id}`, {}, 'post', true);
|
||||
} catch (error) {
|
||||
$api.msg(error);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
function delFile(file, idx) {
|
||||
fileList.value.splice(idx, 1);
|
||||
if(fileList.value.length == 0){
|
||||
open()
|
||||
}
|
||||
fileCount.value = fileList.value.length
|
||||
delFiles.value.push(file.fileUrl);
|
||||
}
|
||||
|
||||
function open() {
|
||||
@@ -159,7 +149,9 @@ function handleConfirm() {
|
||||
|
||||
// 重置所有状态
|
||||
function resetState() {
|
||||
delFiles.value = []
|
||||
fileList.value = [];
|
||||
fileCount.value = 0;
|
||||
loading.value = false;
|
||||
stopPolling();
|
||||
}
|
||||
@@ -187,23 +179,23 @@ async function initQrCode() {
|
||||
}
|
||||
|
||||
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 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'
|
||||
@@ -212,7 +204,7 @@ function makeQrcode() {
|
||||
// config.baseUrl + '/app/kiosk/upload'
|
||||
// }`;
|
||||
console.log(htmlPath);
|
||||
console.log('剩余可上传文件数量:', props.leaveFileCount);
|
||||
console.log('剩余可上传文件数量:',props.leaveFileCount)
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
uQRCode.make({
|
||||
@@ -245,16 +237,20 @@ function startPolling() {
|
||||
|
||||
// 轮询检查上传状态
|
||||
const poll = async () => {
|
||||
if (!isPolling.value || !isVisible.value || deleting.value) return;
|
||||
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;
|
||||
// 上传完成,触发事件
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ const renderScene = (sw, sh) => {
|
||||
radius: scaledRadius,
|
||||
floatOffset: Math.random() * Math.PI * 2,
|
||||
floatSpeed: 0.01 + Math.random() * 0.02,
|
||||
floatRange: 2 * ratio + Math.random() * 2,
|
||||
floatRange: 2 + Math.random() * 2,
|
||||
safeH: safeH,
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<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="title">简历匹配职位</view>
|
||||
<view class="match-item-container">
|
||||
<AIMatch :tags="matchTags" :loading="matchLoading" @tag-click="handleTagClick"></AIMatch>
|
||||
</view>
|
||||
@@ -534,75 +534,6 @@ function findJob(job) {
|
||||
conditionSearch.value[nameAttr] = val;
|
||||
}
|
||||
}
|
||||
if (state.tabIndex === 'all') {
|
||||
// 调用推荐接口
|
||||
getJobRecommendWithNewSearch(job.index);
|
||||
} else {
|
||||
// 调用普通列表接口
|
||||
getJobListWithNewSearch(job.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 立即查询新的数据替换现有数据
|
||||
function getJobRecommendWithNewSearch(index) {
|
||||
const params = {
|
||||
pageSize: pageState.pageSize,
|
||||
sessionId: useUserStore().seesionId,
|
||||
...pageState.search,
|
||||
...conditionSearch.value,
|
||||
};
|
||||
|
||||
$api.createRequest('/app/job/recommend', params).then((resData) => {
|
||||
const currentHeight = waterfallsFlowRef.value.$el.offsetHeight; // 获取当前高度
|
||||
waterfallsFlowRef.value.$el.style.minHeight = `${currentHeight}px`;
|
||||
const { data } = resData;
|
||||
const newData = dataToImg(data);
|
||||
const sliceIndex = index + 1;
|
||||
list.value.splice(sliceIndex, list.value.length - sliceIndex, ...newData);
|
||||
nextTick(() => {
|
||||
waterfallsFlowRef.value.refresh();
|
||||
setTimeout(() => {
|
||||
waterfallsFlowRef.value.$el.style.minHeight = '';
|
||||
}, 500);
|
||||
});
|
||||
// 更新加载状态
|
||||
updateLoadmoreStatus(newData);
|
||||
});
|
||||
}
|
||||
|
||||
// 立即查询新的数据替换现有数据
|
||||
function getJobListWithNewSearch(index) {
|
||||
// 重置分页参数
|
||||
pageState.page = 1;
|
||||
pageState.maxPage = 2;
|
||||
|
||||
const params = {
|
||||
current: pageState.page,
|
||||
pageSize: pageState.pageSize,
|
||||
...pageState.search,
|
||||
...conditionSearch.value,
|
||||
};
|
||||
|
||||
$api.createRequest('/app/job/list', params).then((resData) => {
|
||||
const { rows, total } = resData;
|
||||
// 将新数据转换为图片格式
|
||||
const newData = dataToImg(rows);
|
||||
//替换点击项以后的数据
|
||||
list.value.splice(index, list.value.length - index, ...newData);
|
||||
// 更新分页和加载状态
|
||||
pageState.total = total;
|
||||
pageState.maxPage = Math.ceil(total / pageState.pageSize);
|
||||
updateLoadmoreStatus(newData);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLoadmoreStatus(newData) {
|
||||
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
|
||||
if (newData.length < pageState.pageSize) {
|
||||
loadmoreRef.value.change('noMore');
|
||||
} else {
|
||||
loadmoreRef.value.change('more');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
v-for="(_, index) in 2"
|
||||
:key="index"
|
||||
>
|
||||
<component
|
||||
<component
|
||||
:is="components[index]"
|
||||
@onShowTabbar="changeShowTabbar"
|
||||
:ref="(el) => handelComponentsRef(el, index)"
|
||||
@@ -87,20 +87,12 @@ onLoad(() => {
|
||||
}
|
||||
// 预加载较重页面
|
||||
setTimeout(() => {
|
||||
uni.preloadPage({ url: '/packageA/pages/post/post' });
|
||||
uni.preloadPage({ url: '/pages/nearby/nearby' });
|
||||
uni.preloadPage({ url: '/pages/chat/chat' });
|
||||
uni.preloadPage({ url: '/pages/careerfair/careerfair' });
|
||||
uni.preloadPage({ url: '/pages/msglog/msglog' });
|
||||
uni.preloadPage({ url: '/pages/mine/mine' });
|
||||
uni.preloadPage({ url: '/pages/auth/auth' });
|
||||
uni.preloadPage({ url: '/pages/login/login' });
|
||||
uni.preloadPage({ url: '/pages/search/search' });
|
||||
uni.preloadPage({ url: '/packageA/pages/choiceness/choiceness' });
|
||||
uni.preloadPage({ url: '/packageA/pages/reservation/reservation' });
|
||||
uni.preloadPage({ url: '/packageA/pages/Intendedposition/Intendedposition' });
|
||||
uni.preloadPage({ url: '/packageA/pages/post/post' });
|
||||
uni.preloadPage({ url: '/packageA/pages/exhibitors/exhibitors' });
|
||||
uni.preloadPage({ url: '/packageA/pages/UnitDetails/UnitDetails' });
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
<template>
|
||||
<AppLayout :title="pageTitle">
|
||||
<view v-if="isMachineEnv && !hasLogin" class="alipay-login-container">
|
||||
<AppLayout title="就业服务程序">
|
||||
<view v-if="isMachineEnv" class="alipay-login-container">
|
||||
<!-- 切换 -->
|
||||
<view class="login-method-switch">
|
||||
<view
|
||||
class="method-item"
|
||||
:class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="switchLoginMethod('qrcode')"
|
||||
>
|
||||
扫码登录
|
||||
</view>
|
||||
<view
|
||||
class="method-item"
|
||||
:class="{ active: loginMethod === 'face' }"
|
||||
@@ -17,6 +10,13 @@
|
||||
>
|
||||
扫脸登录
|
||||
</view>
|
||||
<view
|
||||
class="method-item"
|
||||
:class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="switchLoginMethod('qrcode')"
|
||||
>
|
||||
扫码登录
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-scan-area">
|
||||
@@ -64,8 +64,7 @@
|
||||
</view>
|
||||
|
||||
<view class="countdown-container">
|
||||
<!-- 刷脸不限时间 -->
|
||||
<view class="countdown-wrapper" v-if="loginMethod === 'qrcode'">
|
||||
<view class="countdown-wrapper">
|
||||
<text class="countdown-number">{{ countdown }}</text>
|
||||
<text class="countdown-text">秒后自动返回</text>
|
||||
</view>
|
||||
@@ -107,7 +106,7 @@
|
||||
/>
|
||||
</view>
|
||||
<view class="content-sex">
|
||||
<view class="sex-titile">求职者性别</view>
|
||||
<view class="sex-titile">求职区域</view>
|
||||
<view class="sext-ri">
|
||||
<view
|
||||
class="sext-box"
|
||||
@@ -194,18 +193,14 @@ import { storeToRefs } from 'pinia';
|
||||
import tabcontrolVue from './components/tabcontrol.vue';
|
||||
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
|
||||
import { reactive, inject, watch, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import useDictStore from '@/stores/useDictStore';
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
import { IncreaseRevie, FaceLoginService } from '@/common/all-in-one-listen.js';
|
||||
const { $api, navTo } = inject('globalFunction');
|
||||
const { loginSetToken, getUserResume } = useUserStore();
|
||||
const { isMachineEnv, hasLogin } = storeToRefs(useUserStore());
|
||||
const { isMachineEnv } = storeToRefs(useUserStore());
|
||||
const { getDictSelectOption, oneDictData } = useDictStore();
|
||||
const openSelectPopup = inject('openSelectPopup');
|
||||
const qrHandler = new IncreaseRevie();
|
||||
const faceService = new FaceLoginService();
|
||||
// status
|
||||
const selectJobsModel = ref();
|
||||
const tabCurrent = ref(1);
|
||||
@@ -236,36 +231,19 @@ const scanLineTop = ref(0);
|
||||
let scanInterval = null;
|
||||
const countdown = ref(60);
|
||||
let countdownTimer = null;
|
||||
const loginMethod = ref('qrcode'); // 'qrcode' / 'face'
|
||||
const pageTitle = ref('就享家服务程序');
|
||||
const loginMethod = ref('face'); // 'qrcode' / 'face'
|
||||
|
||||
onLoad((parmas) => {
|
||||
getTreeselect();
|
||||
if (!isMachineEnv.value) {
|
||||
$api.msg('请完善微简历');
|
||||
}
|
||||
if (!isMachineEnv.value) $api.msg('请完善微简历');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (isMachineEnv) {
|
||||
startCountdown();
|
||||
startScanAnimation();
|
||||
faceService.start(); // 自动开始初始化流程
|
||||
if (loginMethod.value === 'face') {
|
||||
playTextDirectly('开始刷脸登录');
|
||||
} else {
|
||||
playTextDirectly('请进行登录');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (loginMethod.value === 'face') {
|
||||
handleFaceLogin();
|
||||
}
|
||||
if (loginMethod.value === 'qrcode') {
|
||||
qrHandler.start();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopScanAnimation();
|
||||
stopCountdown();
|
||||
@@ -276,7 +254,7 @@ const startCountdown = () => {
|
||||
countdown.value = 60;
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0 && loginMethod.value === 'qrcode') {
|
||||
if (countdown.value <= 0) {
|
||||
returnToHome();
|
||||
}
|
||||
}, 1000);
|
||||
@@ -308,63 +286,12 @@ const cancelLogin = () => {
|
||||
|
||||
// 切换登录方式
|
||||
const switchLoginMethod = (method) => {
|
||||
if (!isMachineEnv) {
|
||||
return;
|
||||
}
|
||||
if (loginMethod.value !== method) {
|
||||
loginMethod.value = method;
|
||||
switch (method) {
|
||||
case 'qrcode':
|
||||
faceService.close();
|
||||
qrHandler.start();
|
||||
playTextDirectly('扫码登录');
|
||||
resetCountdown();
|
||||
break;
|
||||
case 'face':
|
||||
qrHandler.close();
|
||||
handleFaceLogin();
|
||||
playTextDirectly('扫脸登录');
|
||||
break;
|
||||
}
|
||||
resetCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
async function handleFaceLogin() {
|
||||
try {
|
||||
const authCode = await faceService.startFaceLogin();
|
||||
console.log('authCode获取:', authCode);
|
||||
if (authCode.name) {
|
||||
pageTitle.value = `就享家服务程序(${authCode.name})`;
|
||||
}
|
||||
$api.createRequest('/app/alipay/scanLogin', authCode, 'POST').then((resData) => {
|
||||
loginSetToken(resData.token).then((resume) => {
|
||||
if (resume.data.jobTitleId) {
|
||||
useUserStore().initSeesionId();
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index',
|
||||
});
|
||||
} else {
|
||||
if (resume.data.sex) {
|
||||
pageTitle.value = `就享家服务程序(${name})`;
|
||||
fromValue.sex = resume.data.sex === '男' ? 0 : 1;
|
||||
}
|
||||
playTextDirectly('登录成功,请完善简历信息');
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
this.$api.msg(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
qrHandler.close();
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
qrHandler.close();
|
||||
});
|
||||
|
||||
// 开始动画
|
||||
const startScanAnimation = () => {
|
||||
clearInterval(scanInterval);
|
||||
@@ -404,9 +331,8 @@ function changeEducation() {
|
||||
maskClick: true,
|
||||
data: [oneDictData('education')],
|
||||
success: (_, [value]) => {
|
||||
fromValue.education = value.value;
|
||||
fromValue.area = value.value;
|
||||
state.educationText = value.label;
|
||||
console.log();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -459,7 +385,6 @@ function changeJobs() {
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (!state.experienceText) return $api.msg('请选择工作经验');
|
||||
tabCurrent.value += 1;
|
||||
}
|
||||
|
||||
@@ -528,9 +453,6 @@ function loginTest() {
|
||||
}
|
||||
|
||||
function complete() {
|
||||
if (!state.areaText) return $api.msg('请选择求职区域');
|
||||
if (!state.jobsText.length) return $api.msg('请选择求职岗位');
|
||||
if (!state.salayText) return $api.msg('请选择期望薪资');
|
||||
$api.createRequest('/app/user/resume', fromValue, 'post').then((resData) => {
|
||||
$api.msg('完成');
|
||||
getUserResume();
|
||||
@@ -1006,7 +928,6 @@ function complete() {
|
||||
margin-bottom: 52rpx
|
||||
.sex-titile
|
||||
line-height: 80rpx;
|
||||
color: #6A6A6A;
|
||||
.sext-ri
|
||||
display: flex
|
||||
align-items: center;
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
</view>
|
||||
<view class="card-main">
|
||||
<view class="main-title">服务专区</view>
|
||||
<view class="main-row btn-feel" @click="goAuth">
|
||||
<view class="main-row btn-feel" @click="selectFile">
|
||||
<view class="row-left">
|
||||
<image class="left-img" src="@/static/icon/server1.png"></image>
|
||||
<text class="left-text">实名认证</text>
|
||||
</view>
|
||||
<view class="row-right">{{isAuth ? '已认证' : '未认证'}}</view>
|
||||
<view class="row-right">已认证</view>
|
||||
</view>
|
||||
<view v-if="!isMachineEnv" class="main-row btn-feel" @click="handleItemClick('素质测评')">
|
||||
<view class="row-left">
|
||||
@@ -112,7 +112,7 @@ const { $api, navTo } = inject('globalFunction');
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
|
||||
const popup = ref(null);
|
||||
const { userInfo, Completion, counts, isMachineEnv, isAuth } = storeToRefs(useUserStore());
|
||||
const { userInfo, Completion, counts, isMachineEnv } = storeToRefs(useUserStore());
|
||||
|
||||
import useScreenStore from '@/stores/useScreenStore';
|
||||
const screenStore = useScreenStore();
|
||||
@@ -144,10 +144,12 @@ function confirm() {
|
||||
|
||||
const isAbove90 = (percent) => parseFloat(percent) < 90;
|
||||
|
||||
function goAuth() {
|
||||
if(!isAuth.value){
|
||||
navTo('/pages/auth/auth')
|
||||
}
|
||||
function selectFile() {
|
||||
// FileUploader.showMenuAndUpload({
|
||||
// success: function (res) {
|
||||
// alert('上传成功: ' + JSON.stringify(res));
|
||||
// },
|
||||
// });
|
||||
}
|
||||
function chooseFileUploadTest(pam) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view v-show="searchFocus" class="search-mask"></view>
|
||||
<view>
|
||||
<view class="top">
|
||||
<image
|
||||
@@ -10,50 +9,22 @@
|
||||
@click="navBack"
|
||||
></image>
|
||||
<view class="search-box">
|
||||
<!-- 修改为左右布局的切换按钮 -->
|
||||
<view class="search-type-tabs">
|
||||
<view
|
||||
class="type-tab button-click"
|
||||
:class="{ active: searchType === 'job' }"
|
||||
@click="setSearchType('job')"
|
||||
>
|
||||
职位
|
||||
</view>
|
||||
<view
|
||||
class="type-tab button-click"
|
||||
:class="{ active: searchType === 'major' }"
|
||||
@click="setSearchType('major')"
|
||||
>
|
||||
专业
|
||||
</view>
|
||||
</view>
|
||||
<my-icons
|
||||
class="iconsearch"
|
||||
color="#666666"
|
||||
type="search"
|
||||
size="36"
|
||||
@confirm="searchCollection"
|
||||
></my-icons>
|
||||
<input
|
||||
class="inputed"
|
||||
type="text"
|
||||
:focus="autoFocus"
|
||||
focus
|
||||
v-model="searchValue"
|
||||
:placeholder="searchType === 'job' ? '搜索职位名称' : '搜索专业名称'"
|
||||
placeholder="搜索职位名称"
|
||||
placeholder-class="placeholder"
|
||||
@input="handleInputChange"
|
||||
@blur="handleInputBlur"
|
||||
@focus="handleInputFocus"
|
||||
@confirm="searchBtn"
|
||||
/>
|
||||
<!-- 联想搜索下拉列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="search-suggestions"
|
||||
v-show="showSuggestions && filteredSuggestions.length"
|
||||
>
|
||||
<view
|
||||
class="suggestion-item"
|
||||
v-for="(item, index) in filteredSuggestions"
|
||||
:key="index"
|
||||
@click="selectSuggestion(item)"
|
||||
>
|
||||
<view class="item-txt line_1">{{ item }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="search-btn button-click" @click="searchBtn">搜索</view>
|
||||
</view>
|
||||
@@ -115,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref, reactive, nextTick, computed } from 'vue';
|
||||
import { inject, ref, reactive, nextTick } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
|
||||
const { $api, navBack, navTo } = inject('globalFunction');
|
||||
@@ -127,34 +98,9 @@ import img from '@/static/icon/filter.png';
|
||||
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const { isMiniProgram } = storeToRefs(useUserStore());
|
||||
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
|
||||
|
||||
// 搜索类型:job-职位,major-专业
|
||||
const searchType = ref('job');
|
||||
const searchValue = ref('');
|
||||
const historyList = ref([]);
|
||||
const listCom = ref([]);
|
||||
const showSuggestions = ref(false);
|
||||
const searchFocus = ref(false);
|
||||
const autoFocus = ref(false)
|
||||
|
||||
// 专业数据源(示例数据,可以替换为实际数据)
|
||||
const majorDataSource = ref([]);
|
||||
|
||||
// 计算属性:过滤后的联想列表
|
||||
const filteredSuggestions = computed(() => {
|
||||
if (!searchValue.value || searchType.value !== 'major') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchText = searchValue.value.toLowerCase();
|
||||
try {
|
||||
return majorDataSource.value.filter((item) => item.toLowerCase().includes(searchText));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const pageState = reactive({
|
||||
page: 0,
|
||||
total: 0,
|
||||
@@ -168,7 +114,7 @@ const isLoaded = ref(false);
|
||||
const waterfallsFlowRef = ref(null);
|
||||
const loadmoreRef = ref(null);
|
||||
const currentTab = ref(0);
|
||||
|
||||
// 响应式搜索条件(可以被修改)
|
||||
const searchParams = ref({});
|
||||
const pageSize = ref(10);
|
||||
|
||||
@@ -209,76 +155,16 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.keyWord) {
|
||||
searchValue.value = decodeURIComponent(options.keyWord);
|
||||
searchBtn();
|
||||
}else{
|
||||
//从标签点进来不自动聚焦
|
||||
autoFocus.value = true
|
||||
if(options.keyWord){
|
||||
searchValue.value = decodeURIComponent(options.keyWord)
|
||||
searchBtn()
|
||||
}
|
||||
let arr = uni.getStorageSync('searchList');
|
||||
if (arr) {
|
||||
historyList.value = uni.getStorageSync('searchList');
|
||||
}
|
||||
|
||||
getMajorDataSource();
|
||||
});
|
||||
|
||||
function getMajorDataSource() {
|
||||
const LoadCache = (resData) => {
|
||||
if (resData.code === 200) {
|
||||
majorDataSource.value = resData.data;
|
||||
console.log(majorDataSource.value);
|
||||
}
|
||||
};
|
||||
$api.createRequestWithCache('/app/common/majorManagement/getMajorList', {}, 'GET', false, LoadCache).then(
|
||||
LoadCache
|
||||
);
|
||||
}
|
||||
|
||||
// 设置搜索类型并触发搜索
|
||||
function setSearchType(type) {
|
||||
if (searchType.value === type) return;
|
||||
searchType.value = type;
|
||||
|
||||
// 如果输入框有值且当前是专业搜索,显示联想列表
|
||||
if (searchValue.value && searchType.value === 'major' && searchFocus.value) {
|
||||
showSuggestions.value = true;
|
||||
} else {
|
||||
showSuggestions.value = false;
|
||||
}
|
||||
|
||||
// 如果有搜索值,触发搜索
|
||||
if (searchValue.value) {
|
||||
// 重置页码
|
||||
pageState.page = 0;
|
||||
|
||||
// 触发搜索
|
||||
playTextDirectly('正在为您查找岗位');
|
||||
|
||||
// 根据搜索类型设置不同的参数
|
||||
if (searchType.value === 'job') {
|
||||
searchParams.value = {
|
||||
jobTitle: searchValue.value,
|
||||
};
|
||||
} else {
|
||||
searchParams.value = {
|
||||
majorTitle: searchValue.value,
|
||||
};
|
||||
}
|
||||
|
||||
if (currentTab.value === 0) {
|
||||
getJobList('refresh');
|
||||
} else {
|
||||
refresh();
|
||||
waterfallsFlowRef.value?.refresh?.();
|
||||
}
|
||||
|
||||
// 隐藏联想列表
|
||||
showSuggestions.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function changeType(type) {
|
||||
if (currentTab.value === type) return;
|
||||
switch (type) {
|
||||
@@ -293,84 +179,27 @@ function changeType(type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function searchFn(item) {
|
||||
searchValue.value = item;
|
||||
searchBtn();
|
||||
}
|
||||
|
||||
function handleInputChange(e) {
|
||||
const val = e.detail.value;
|
||||
searchValue.value = val;
|
||||
|
||||
// 如果是专业搜索且输入框有值,显示联想列表
|
||||
if (searchType.value === 'major' && val && searchFocus.value) {
|
||||
showSuggestions.value = true;
|
||||
} else {
|
||||
showSuggestions.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputFocus() {
|
||||
searchFocus.value = true;
|
||||
// 如果是专业搜索且输入框有值,显示联想列表
|
||||
if (searchType.value === 'major' && searchValue.value) {
|
||||
showSuggestions.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputBlur() {
|
||||
searchFocus.value = false;
|
||||
// 延迟隐藏联想列表,以便点击项能触发
|
||||
setTimeout(() => {
|
||||
showSuggestions.value = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 选择联想项
|
||||
function selectSuggestion(item) {
|
||||
searchValue.value = item;
|
||||
showSuggestions.value = false;
|
||||
// 直接触发搜索
|
||||
searchBtn();
|
||||
}
|
||||
|
||||
function searchBtn() {
|
||||
if (!searchValue.value) {
|
||||
return;
|
||||
}
|
||||
playTextDirectly('正在为您查找岗位');
|
||||
|
||||
// 保存到历史记录(仅当搜索成功时保存)
|
||||
historyList.value.unshift(searchValue.value);
|
||||
historyList.value = unique(historyList.value);
|
||||
uni.setStorageSync('searchList', historyList.value);
|
||||
|
||||
// 根据搜索类型设置不同的参数
|
||||
if (searchType.value === 'job') {
|
||||
// 职位搜索
|
||||
searchParams.value = {
|
||||
jobTitle: searchValue.value,
|
||||
};
|
||||
} else {
|
||||
// 专业搜索
|
||||
searchParams.value = {
|
||||
majorTitle: searchValue.value,
|
||||
};
|
||||
}
|
||||
|
||||
// 重置页码
|
||||
pageState.page = 0;
|
||||
|
||||
searchParams.value = {
|
||||
jobTitle: searchValue,
|
||||
};
|
||||
if (currentTab.value === 0) {
|
||||
getJobList('refresh');
|
||||
} else {
|
||||
refresh();
|
||||
waterfallsFlowRef.value?.refresh?.();
|
||||
}
|
||||
|
||||
// 隐藏联想列表
|
||||
showSuggestions.value = false;
|
||||
}
|
||||
|
||||
function searchCollection(e) {
|
||||
@@ -421,25 +250,13 @@ function getJobList(type = 'add') {
|
||||
pageState.page = 1;
|
||||
pageState.maxPage = 2;
|
||||
}
|
||||
|
||||
// 根据搜索类型构建参数
|
||||
let params = {
|
||||
current: pageState.page,
|
||||
pageSize: pageState.pageSize,
|
||||
...pageState.search,
|
||||
jobTitle: searchValue.value,
|
||||
};
|
||||
|
||||
// 移除之前的搜索参数,根据当前搜索类型添加
|
||||
if (searchType.value === 'job') {
|
||||
params.jobTitle = searchValue.value;
|
||||
// 确保没有majorTitle参数
|
||||
delete params.majorTitle;
|
||||
} else {
|
||||
params.majorTitle = searchValue.value;
|
||||
// 确保没有jobTitle参数
|
||||
delete params.jobTitle;
|
||||
}
|
||||
|
||||
$api.createRequest('/app/job/list', params, 'GET', true).then((resData) => {
|
||||
const { rows, total } = resData;
|
||||
if (type === 'add') {
|
||||
@@ -450,16 +267,6 @@ function getJobList(type = 'add') {
|
||||
listCom.value = [...listCom.value, ...reslist];
|
||||
} else {
|
||||
listCom.value = [...rows];
|
||||
// 一体机语音提示
|
||||
if (searchType.value === 'major') {
|
||||
if (rows.length) {
|
||||
$api.msg('为您找到相关专业的职位');
|
||||
playTextDirectly(`为您找到相关专业的职位`);
|
||||
} else {
|
||||
$api.msg('未找到相关专业的职位,请尝试其他关键词');
|
||||
playTextDirectly(`未找到相关专业的职位,请尝试其他关键词`);
|
||||
}
|
||||
}
|
||||
}
|
||||
pageState.total = resData.total;
|
||||
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
|
||||
@@ -488,6 +295,7 @@ function dataToImg(data) {
|
||||
.Detailscroll-view{
|
||||
flex: 1
|
||||
overflow: hidden
|
||||
|
||||
}
|
||||
.container{
|
||||
display: flex
|
||||
@@ -555,6 +363,8 @@ function dataToImg(data) {
|
||||
justify-content: space-between
|
||||
background-color: #fff;
|
||||
padding: 20rpx 20rpx;
|
||||
position: sticky;
|
||||
top: 0
|
||||
.btnback{
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
@@ -562,90 +372,27 @@ function dataToImg(data) {
|
||||
.search-box{
|
||||
flex: 1;
|
||||
padding: 0 24rpx 0 6rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
.search-type-tabs {
|
||||
background: #FFFFFF;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 21rpx;
|
||||
transform: translate(0, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx;
|
||||
z-index: 2;
|
||||
border: 1rpx solid #E0E0E0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
border-radius: 60rpx
|
||||
|
||||
.type-tab {
|
||||
padding: 10rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
border-radius: 30rpx;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&.active {
|
||||
background: #256BFA;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.type-tab:first-child {
|
||||
margin-right: 1rpx;
|
||||
}
|
||||
}
|
||||
position: relative
|
||||
.inputed {
|
||||
width: 100%;
|
||||
background: #F8F8F8;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
line-height: 36rpx;
|
||||
color: #666666;
|
||||
padding: 0 30rpx 0 230rpx;
|
||||
box-sizing: border-box;
|
||||
padding-left: 30rpx
|
||||
width: 100%;
|
||||
background: #F8F8F8;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
line-height: 36rpx;
|
||||
color: #666666;
|
||||
padding: 0 30rpx 0 80rpx;
|
||||
box-sizing: border-box;
|
||||
height: 80rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 73rpx;
|
||||
border-radius: 75rpx 75rpx 75rpx 75rpx;
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
position: absolute;
|
||||
top: 105%;
|
||||
width:calc(100% - 24rpx - 6rpx);
|
||||
background: #FFFFFF;
|
||||
border: 2rpx solid #ECECEC;
|
||||
border-top: 0;
|
||||
z-index: 10;
|
||||
border-radius: 30rpx
|
||||
max-height: 40vh;
|
||||
overflow hidden;
|
||||
.suggestion-item {
|
||||
height: 80rpx;
|
||||
padding: 0rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: flex-start;
|
||||
border-top: 2rpx dashed #e3e3e3;
|
||||
|
||||
.item-txt {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
.suggestion-item:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.iconsearch{
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 36rpx
|
||||
transform: translate(0, -50%)
|
||||
}
|
||||
}
|
||||
.search-btn {
|
||||
@@ -722,13 +469,4 @@ function dataToImg(data) {
|
||||
font-size: 24rpx;
|
||||
color: #6C7282;
|
||||
margin-top: 6rpx;
|
||||
.search-mask{
|
||||
position fixed;
|
||||
width 100vw;
|
||||
height:100vh;
|
||||
top:0;
|
||||
left:0
|
||||
z-index:9;
|
||||
background: #33333355;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
@@ -615,7 +615,8 @@
|
||||
<div class="upload-icon">📁</div>
|
||||
<div class="upload-text" id="uploadText">点击选择文件</div>
|
||||
<div class="upload-hint" id="uploadHint">支持图片、文档、文本等格式</div>
|
||||
<input type="file" id="fileInput" class="file-input" multiple />
|
||||
<input type="file" id="fileInput" class="file-input" multiple
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.ppt,.pptx,.txt,.md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -662,9 +663,6 @@
|
||||
const sessionId = urlParams.get('sessionId');
|
||||
const uploadApi = urlParams.get('uploadApi');
|
||||
const fileCountParam = urlParams.get('fileCount');
|
||||
const acceptParam = urlParams.get('accept');
|
||||
const DEFAULT_ACCEPT = 'image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.ppt,.pptx,.txt,.md';
|
||||
const ACCEPT_TYPES = acceptParam ? acceptParam : DEFAULT_ACCEPT;
|
||||
|
||||
// 配置常量
|
||||
const MAX_FILE_COUNT = fileCountParam ? parseInt(fileCountParam) : 2; // 从URL参数获取,默认为2
|
||||
@@ -673,7 +671,6 @@
|
||||
console.log('Session ID:', sessionId);
|
||||
console.log('Upload API:', uploadApi);
|
||||
console.log('Max file count:', MAX_FILE_COUNT);
|
||||
console.log('允许上传类型:', ACCEPT_TYPES);
|
||||
|
||||
// DOM元素
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
@@ -695,10 +692,6 @@
|
||||
const limitWarningInfo = document.getElementById('limitWarningInfo');
|
||||
const limitWarningText = document.getElementById('limitWarningText');
|
||||
|
||||
|
||||
//设置文件类型
|
||||
fileInput.accept = ACCEPT_TYPES;
|
||||
|
||||
// 状态变量
|
||||
let selectedFiles = [];
|
||||
let isUploading = false;
|
||||
@@ -1086,7 +1079,7 @@
|
||||
if (MAX_FILE_COUNT <= 0) {
|
||||
// 如果没有设置文件数量限制,始终可以上传
|
||||
uploadText.textContent = '点击选择文件';
|
||||
uploadHint.innerHTML = `支持格式: ${ACCEPT_TYPES}<br />文件大小不超过10MB`;
|
||||
uploadHint.innerHTML = '支持图片、文档、文本等格式<br />文件大小不超过10MB';
|
||||
fileInput.disabled = false;
|
||||
} else if (selectedFiles.length >= MAX_FILE_COUNT) {
|
||||
uploadText.textContent = `已达到最大文件数量(${MAX_FILE_COUNT}个)`;
|
||||
@@ -1094,7 +1087,8 @@
|
||||
fileInput.disabled = true;
|
||||
} else {
|
||||
uploadText.textContent = '点击选择文件';
|
||||
uploadHint.innerHTML = `支持格式: ${ACCEPT_TYPES}<br />最多${MAX_FILE_COUNT}个文件,每个不超过10MB`;
|
||||
uploadHint.innerHTML =
|
||||
`支持图片、文档、文本等格式<br />最多${MAX_FILE_COUNT}个文件,每个不超过10MB<br />已上传: ${uploadedCount}/${MAX_FILE_COUNT}`;
|
||||
fileInput.disabled = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,21 +10,18 @@ import {
|
||||
import config from '../config';
|
||||
|
||||
const defalutLongLat = {
|
||||
longitude: 120.366085,
|
||||
latitude: 36.086656,
|
||||
longitude: 120.382665,
|
||||
latitude: 36.066938,
|
||||
}
|
||||
|
||||
const useLocationStore = defineStore("location", () => {
|
||||
// 定义状态
|
||||
const longitudeVal = ref(120.366085) // 经度
|
||||
const latitudeVal = ref(36.086656) //纬度
|
||||
const longitudeVal = ref(null) // 经度
|
||||
const latitudeVal = ref(null) //纬度
|
||||
const timer = ref(null)
|
||||
const count = ref(0)
|
||||
|
||||
function getLocation() { // 获取经纬度两个平台
|
||||
// #ifndef H5
|
||||
const lightAppJssdk = {}
|
||||
// #endif
|
||||
return new Promise((resole, reject) => {
|
||||
try {
|
||||
if (lightAppJssdk.map) {
|
||||
@@ -37,7 +34,7 @@ const useLocationStore = defineStore("location", () => {
|
||||
fail: function(data) {
|
||||
longitudeVal.value = defalutLongLat.longitude
|
||||
latitudeVal.value = defalutLongLat.latitude
|
||||
reject()
|
||||
resole(defalutLongLat)
|
||||
msg('用户位置获取失败')
|
||||
console.log('失败3', data)
|
||||
}
|
||||
@@ -56,7 +53,7 @@ const useLocationStore = defineStore("location", () => {
|
||||
fail: function(data) {
|
||||
longitudeVal.value = defalutLongLat.longitude
|
||||
latitudeVal.value = defalutLongLat.latitude
|
||||
reject()
|
||||
resole(defalutLongLat)
|
||||
msg('用户位置获取失败')
|
||||
console.log('失败2', data)
|
||||
}
|
||||
@@ -65,7 +62,7 @@ const useLocationStore = defineStore("location", () => {
|
||||
} catch (e) {
|
||||
longitudeVal.value = defalutLongLat.longitude
|
||||
latitudeVal.value = defalutLongLat.latitude
|
||||
reject()
|
||||
resole(defalutLongLat)
|
||||
msg('测试环境,使用模拟定位')
|
||||
console.log('失败1', e)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export const useRecommedIndexedDBStore = defineStore("indexedDB", () => {
|
||||
async function addRecord(payload) {
|
||||
const totalRecords = await baseDB.db.getRecordCount(tableName.value);
|
||||
if (totalRecords >= total.value) {
|
||||
console.log(`数据超过 ${total.value} 条,删除最早的一条...`);
|
||||
console.log(`⚠数据超过 ${total.value} 条,删除最早的一条...`);
|
||||
await baseDB.db.deleteOldestRecord(tableName.value);
|
||||
}
|
||||
if (!baseDB.isDBReady) await baseDB.initDB();
|
||||
|
||||
@@ -5,11 +5,8 @@ import {
|
||||
ref,
|
||||
computed
|
||||
} from 'vue';
|
||||
|
||||
|
||||
import wideScreenStyles from '../common/wide-screen.css?inline';
|
||||
|
||||
|
||||
// 屏幕检测管理器类
|
||||
class ScreenDetectionManager {
|
||||
constructor() {
|
||||
@@ -52,25 +49,17 @@ class ScreenDetectionManager {
|
||||
|
||||
// 检测折叠屏
|
||||
checkVisualViewport() {
|
||||
try {
|
||||
if (window.visualViewport?.segments?.length > 1) {
|
||||
return {
|
||||
foldable: true,
|
||||
count: window.visualViewport.segments.length - 1
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
foldable: false,
|
||||
count: 1
|
||||
}
|
||||
if (window.visualViewport?.segments?.length > 1) {
|
||||
return {
|
||||
foldable: true,
|
||||
count: window.visualViewport.segments.length - 1
|
||||
}
|
||||
} catch (error) {
|
||||
} else {
|
||||
return {
|
||||
foldable: false,
|
||||
count: 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 动态加载 CSS
|
||||
@@ -85,11 +74,11 @@ class ScreenDetectionManager {
|
||||
|
||||
// 添加加载成功/失败监听
|
||||
this.cssLink.onload = () => {
|
||||
console.log('宽屏 CSS 文件加载成功');
|
||||
console.log('🎨 宽屏 CSS 文件加载成功');
|
||||
};
|
||||
|
||||
this.cssLink.onerror = () => {
|
||||
console.error('宽屏 CSS 文件加载失败');
|
||||
console.error('❌ 宽屏 CSS 文件加载失败');
|
||||
this.cssLink = null;
|
||||
};
|
||||
|
||||
@@ -108,7 +97,7 @@ class ScreenDetectionManager {
|
||||
try {
|
||||
this.cssLink.parentNode.removeChild(this.cssLink);
|
||||
this.cssLink = null;
|
||||
console.log('宽屏 CSS 文件已移除');
|
||||
console.log('🗑️ 宽屏 CSS 文件已移除');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('移除 CSS 失败:', error);
|
||||
|
||||
@@ -2,8 +2,7 @@ import {
|
||||
defineStore
|
||||
} from 'pinia';
|
||||
import {
|
||||
ref,
|
||||
inject
|
||||
ref
|
||||
} from 'vue'
|
||||
import {
|
||||
createRequest
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
import {
|
||||
msg,
|
||||
$api,
|
||||
navTo
|
||||
} from '../common/globalFunction';
|
||||
import baseDB from '@/utils/db.js';
|
||||
|
||||
@@ -63,7 +61,6 @@ const useUserStore = defineStore("user", () => {
|
||||
const counts = ref({})
|
||||
const isMiniProgram = ref(false)
|
||||
const isMachineEnv = ref(false)
|
||||
const isAuth = ref(false) //是否认证(身份证+手机号)
|
||||
|
||||
const login = (value) => {
|
||||
hasLogin.value = true;
|
||||
@@ -138,19 +135,6 @@ const useUserStore = defineStore("user", () => {
|
||||
userInfo.value = values.data;
|
||||
// role.value = values.role;
|
||||
hasLogin.value = true;
|
||||
isAuth.value = values.data?.isCert == 0 ? true : false //是否认证 0已认证
|
||||
}
|
||||
|
||||
const checkAuth = () => {
|
||||
if (!hasLogin.value) {
|
||||
logOut()
|
||||
return false //验证失败
|
||||
}
|
||||
if (hasLogin.value && !isAuth.value) {
|
||||
navTo('/pages/auth/auth')
|
||||
return false //验证失败
|
||||
}
|
||||
return true // 验证通过
|
||||
}
|
||||
|
||||
|
||||
@@ -202,9 +186,7 @@ const useUserStore = defineStore("user", () => {
|
||||
isMiniProgram,
|
||||
changMiniProgramAppStatus,
|
||||
changMachineEnv,
|
||||
isMachineEnv,
|
||||
isAuth,
|
||||
checkAuth
|
||||
isMachineEnv
|
||||
}
|
||||
}, {
|
||||
unistorage: true,
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
// 使用示例:
|
||||
// import IDCardParser from '@/utils/IDCardParser';
|
||||
|
||||
// const handleCheck = (idStr) => {
|
||||
// const parser = new IDCardParser(idStr);
|
||||
// const result = parser.getInfo();
|
||||
// if(result.valid) {
|
||||
// // 填充表单
|
||||
// console.log(result.age, result.gender);
|
||||
// } else {
|
||||
// alert(result.message);
|
||||
// }
|
||||
// }
|
||||
|
||||
class IDCardParser {
|
||||
constructor(idNumber) {
|
||||
this.idNumber = idNumber ? idNumber.trim().toUpperCase() : "";
|
||||
this.provinceMap = {
|
||||
11: "北京市",
|
||||
12: "天津市",
|
||||
13: "河北省",
|
||||
14: "山西省",
|
||||
15: "内蒙古自治区",
|
||||
21: "辽宁省",
|
||||
22: "吉林省",
|
||||
23: "黑龙江省",
|
||||
31: "上海市",
|
||||
32: "江苏省",
|
||||
33: "浙江省",
|
||||
34: "安徽省",
|
||||
35: "福建省",
|
||||
36: "江西省",
|
||||
37: "山东省",
|
||||
41: "河南省",
|
||||
42: "湖北省",
|
||||
43: "湖南省",
|
||||
44: "广东省",
|
||||
45: "广西壮族自治区",
|
||||
46: "海南省",
|
||||
50: "重庆市",
|
||||
51: "四川省",
|
||||
52: "贵州省",
|
||||
53: "云南省",
|
||||
54: "西藏自治区",
|
||||
61: "陕西省",
|
||||
62: "甘肃省",
|
||||
63: "青海省",
|
||||
64: "宁夏回族自治区",
|
||||
65: "新疆维吾尔自治区",
|
||||
71: "台湾省",
|
||||
81: "香港特别行政区",
|
||||
82: "澳门特别行政区"
|
||||
};
|
||||
|
||||
// 初始化验证结果
|
||||
const validation = this._validate();
|
||||
this.isValid = validation.isValid;
|
||||
this.errorMsg = validation.msg;
|
||||
|
||||
// 如果合法,预先解析数据
|
||||
if (this.isValid) {
|
||||
this._parseData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心校验逻辑
|
||||
*/
|
||||
_validate() {
|
||||
// 1. 基础正则 (18位,末位数字或X)
|
||||
if (!/^\d{17}[\d|X]$/.test(this.idNumber)) {
|
||||
return {
|
||||
isValid: false,
|
||||
msg: "格式错误:必须是18位,末位为数字或X"
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 省份校验
|
||||
const provinceCode = parseInt(this.idNumber.substring(0, 2));
|
||||
if (!this.provinceMap[provinceCode]) {
|
||||
return {
|
||||
isValid: false,
|
||||
msg: "省份代码错误"
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 日期合法性严格校验
|
||||
const year = parseInt(this.idNumber.substring(6, 10));
|
||||
const month = parseInt(this.idNumber.substring(10, 12));
|
||||
const day = parseInt(this.idNumber.substring(12, 14));
|
||||
|
||||
const date = new Date(year, month - 1, day);
|
||||
// JS Date会自动纠错(例如2月30会变成3月),所以必须反向比对
|
||||
if (
|
||||
date.getFullYear() !== year ||
|
||||
date.getMonth() + 1 !== month ||
|
||||
date.getDate() !== day
|
||||
) {
|
||||
return {
|
||||
isValid: false,
|
||||
msg: "出生日期无效"
|
||||
};
|
||||
}
|
||||
|
||||
// 检查年份范围(可选,例如限制在1900年以后)
|
||||
if (year < 1900 || date > new Date()) {
|
||||
return {
|
||||
isValid: false,
|
||||
msg: "出生日期不在合理范围内"
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 校验码计算 (ISO 7064:1983.MOD 11-2)
|
||||
if (!this._checkSum()) {
|
||||
return {
|
||||
isValid: false,
|
||||
msg: "校验码错误(身份证可能无效)"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
msg: "验证通过"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验码算法
|
||||
*/
|
||||
_checkSum() {
|
||||
const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += parseInt(this.idNumber[i]) * factors[i];
|
||||
}
|
||||
|
||||
const mod = sum % 11;
|
||||
return checkCodes[mod] === this.idNumber[17];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析内部数据
|
||||
*/
|
||||
_parseData() {
|
||||
const year = parseInt(this.idNumber.substring(6, 10));
|
||||
const month = parseInt(this.idNumber.substring(10, 12));
|
||||
const day = parseInt(this.idNumber.substring(12, 14));
|
||||
|
||||
this.birthDate = new Date(year, month - 1, day);
|
||||
this.province = this.provinceMap[parseInt(this.idNumber.substring(0, 2))];
|
||||
|
||||
// 性别:第17位,奇数男,偶数女
|
||||
const genderCode = parseInt(this.idNumber.substring(16, 17));
|
||||
this.gender = genderCode % 2 !== 0 ? "男" : "女";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有信息
|
||||
*/
|
||||
getInfo() {
|
||||
if (!this.isValid) {
|
||||
return {
|
||||
valid: false,
|
||||
message: this.errorMsg
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
idNumber: this.idNumber,
|
||||
province: this.province,
|
||||
birthday: this._formatDate(this.birthDate),
|
||||
gender: this.gender,
|
||||
age: this._calculateAge(),
|
||||
zodiac: this._getZodiac(), // 生肖
|
||||
constellation: this._getConstellation() // 星座
|
||||
};
|
||||
}
|
||||
|
||||
_formatDate(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
_calculateAge() {
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - this.birthDate.getFullYear();
|
||||
const m = today.getMonth() - this.birthDate.getMonth();
|
||||
|
||||
// 如果没过生日,年龄减1
|
||||
if (m < 0 || (m === 0 && today.getDate() < this.birthDate.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
_getZodiac() {
|
||||
// 猴鸡狗猪鼠牛虎兔龙蛇马羊 (对应余数 0-11)
|
||||
const zodiacs = "猴鸡狗猪鼠牛虎兔龙蛇马羊";
|
||||
return zodiacs.charAt(this.birthDate.getFullYear() % 12);
|
||||
}
|
||||
|
||||
_getConstellation() {
|
||||
const month = this.birthDate.getMonth() + 1;
|
||||
const day = this.birthDate.getDate();
|
||||
|
||||
// 星座分割日
|
||||
const dates = [20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22];
|
||||
const constellations = [
|
||||
"摩羯座", "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座",
|
||||
"巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座"
|
||||
];
|
||||
|
||||
if (day < dates[month - 1]) {
|
||||
return constellations[month - 1];
|
||||
} else {
|
||||
return constellations[month];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default IDCardParser;
|
||||
@@ -66,14 +66,7 @@ class BaseStore {
|
||||
}
|
||||
|
||||
initDB() {
|
||||
// #ifdef H5
|
||||
this.db = new IndexedDBHelper(this.dbName, config.DBversion);
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
this.db = new UniStorageHelper(this.dbName, config.DBversion);
|
||||
// #endif
|
||||
|
||||
return this.db.openDB([{
|
||||
name: 'record',
|
||||
keyPath: "id",
|
||||
|
||||
@@ -18,9 +18,7 @@ const needToEncrypt = [
|
||||
["post", "/app/user/experience/edit"],
|
||||
["post", "/app/user/experience/delete"],
|
||||
["get", "/app/user/experience/getSingle/{value}"],
|
||||
["get", "/app/user/experience/list"],
|
||||
["post", "/app/alipay/scanLogin"],
|
||||
["post", "/app/user/cert"],
|
||||
["get", "/app/user/experience/list"]
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -139,7 +137,6 @@ export function createRequest(url, data = {}, method = 'GET', loading = false, h
|
||||
} = resData.data
|
||||
if (code === 200) {
|
||||
resolve(resData.data)
|
||||
console.log(resData.data.data,'接口解密')
|
||||
return
|
||||
}
|
||||
if (msg) {
|
||||
|
||||
Reference in New Issue
Block a user