Compare commits
26 Commits
qingdaoV0.
...
0d5e3024bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d5e3024bc | ||
|
|
268648868f | ||
| 7ce14fa7e2 | |||
| 9a5bffae85 | |||
| 51e67a8c8f | |||
| 531681b74e | |||
| 7e0ec650b1 | |||
| 3a5d8ccb1a | |||
| 34bad16bf4 | |||
| f5099e9cc0 | |||
| 4295199887 | |||
|
|
7322e0854e | ||
|
|
d260e24265 | ||
|
|
c75653b7a3 | ||
|
|
805b384958 | ||
|
|
77f97892bc | ||
|
|
20191c9454 | ||
| fc2d0f90ec | |||
| 6b20c045a9 | |||
| 183c71da3c | |||
| 5e2f8ac169 | |||
| bca67b7f25 | |||
| 83a1078a4d | |||
| e4d100242b | |||
| 3eca164bde | |||
| a7d6b8709c |
@@ -1,17 +1,37 @@
|
||||
<template>
|
||||
<view>{{ salaryText }}</view>
|
||||
<view>
|
||||
<view v-if="!minSalary || !maxSalary">面议</view>
|
||||
<view v-else class="texts">
|
||||
<text class="num">{{ minSalary / 1000 }}</text>
|
||||
<text class="unit">k</text>
|
||||
<text class="gap">~</text>
|
||||
<text class="num">{{ maxSalary / 1000 }}</text>
|
||||
<text class="unit">k</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed } from 'vue';
|
||||
import useDictStore from '../../stores/useDictStore';
|
||||
const { minSalary, maxSalary, isMonth } = defineProps(['minSalary', 'maxSalary', 'isMonth']);
|
||||
|
||||
const salaryText = computed(() => {
|
||||
if (!minSalary || !maxSalary) return '面议';
|
||||
if (isMonth) {
|
||||
return `${minSalary}-${maxSalary}/月`;
|
||||
}
|
||||
return `${minSalary / 1000}k-${maxSalary / 1000}k`;
|
||||
});
|
||||
import { inject, computed } from "vue";
|
||||
import useDictStore from "../../stores/useDictStore";
|
||||
const { minSalary, maxSalary } = defineProps(["minSalary", "maxSalary"]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.texts{
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.num{
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.unit{
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.gap{
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
margin-left: 5rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,11 +4,10 @@ export default {
|
||||
// baseUrl: 'http://192.168.3.29:8081',
|
||||
// baseUrl: 'http://10.213.6.207:19010/api',
|
||||
// 语音转文字
|
||||
// vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
|
||||
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/system/asr/connect', // 自定义
|
||||
// vioceBaseURl: 'ws://39.98.44.136:8080/speech-recognition',
|
||||
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
|
||||
// 语音合成
|
||||
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
|
||||
speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/',
|
||||
// indexedDB
|
||||
DBversion: 2,
|
||||
// 只使用本地缓寸的数据
|
||||
|
||||
@@ -86,8 +86,7 @@ export function usePagination(
|
||||
const res = await requestFn(params)
|
||||
|
||||
const rawData = res[dataKey]
|
||||
const total = res[totalKey] || 99999999
|
||||
console.log(total, rawData)
|
||||
|
||||
const data = typeof transformFn === 'function' ? transformFn(rawData) : rawData
|
||||
|
||||
if (type === 'refresh') {
|
||||
@@ -96,9 +95,9 @@ export function usePagination(
|
||||
list.value.push(...data)
|
||||
}
|
||||
|
||||
const total = res[totalKey] || list.value?.length
|
||||
pageState.total = total
|
||||
pageState.maxPage = Math.ceil(total / pageState.pageSize)
|
||||
|
||||
finished.value = list.value.length >= total
|
||||
empty.value = list.value.length === 0
|
||||
} catch (err) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
|
||||
import config from '@/config'
|
||||
|
||||
// Alibaba Cloud
|
||||
export function useAudioRecorder() {
|
||||
const isRecording = ref(false)
|
||||
const isStopping = ref(false)
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
import {
|
||||
ref,
|
||||
onUnmounted
|
||||
} from 'vue'
|
||||
import {
|
||||
$api
|
||||
} from '../common/globalFunction'; // 你的请求封装
|
||||
import config from '@/config'
|
||||
|
||||
// 开源
|
||||
export function useAudioRecorder() {
|
||||
// --- 状态定义 ---
|
||||
const isRecording = ref(false)
|
||||
const isSocketConnected = ref(false)
|
||||
const recordingDuration = ref(0)
|
||||
const volumeLevel = ref(0) // 0-100
|
||||
const recognizedText = ref('')
|
||||
|
||||
// --- 内部变量 ---
|
||||
let socketTask = null
|
||||
let durationTimer = null
|
||||
|
||||
// --- APP/小程序 变量 ---
|
||||
let recorderManager = null;
|
||||
|
||||
// --- H5 变量 ---
|
||||
let audioContext = null;
|
||||
let scriptProcessor = null;
|
||||
let mediaStreamSource = null;
|
||||
let h5Stream = null;
|
||||
|
||||
// --- 配置项 ---
|
||||
const RECORD_CONFIG = {
|
||||
duration: 600000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
format: 'pcm',
|
||||
frameSize: 4096
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 地址 (含 Token)
|
||||
*/
|
||||
const getWsUrl = async () => {
|
||||
let wsUrl = config.vioceBaseURl
|
||||
|
||||
// 拼接 Token
|
||||
const token = uni.getStorageSync('token') || '';
|
||||
if (token) {
|
||||
const separator = wsUrl.includes('?') ? '&' : '?';
|
||||
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
return wsUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始录音 (入口)
|
||||
*/
|
||||
const startRecording = async () => {
|
||||
if (isRecording.value) return
|
||||
|
||||
try {
|
||||
recognizedText.value = ''
|
||||
volumeLevel.value = 0
|
||||
|
||||
// #ifdef H5
|
||||
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||
uni.showToast({
|
||||
title: 'H5录音需要HTTPS环境',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
const url = await getWsUrl()
|
||||
console.log('正在连接 ASR:', url)
|
||||
|
||||
await connectSocket(url);
|
||||
|
||||
} catch (err) {
|
||||
console.error('启动失败:', err);
|
||||
uni.showToast({
|
||||
title: '启动失败: ' + (err.message || ''),
|
||||
icon: 'none'
|
||||
});
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 WebSocket
|
||||
*/
|
||||
const connectSocket = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
socketTask = uni.connectSocket({
|
||||
url: url,
|
||||
success: () => console.log('Socket 连接请求发送'),
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
|
||||
socketTask.onOpen((res) => {
|
||||
console.log('WebSocket 已连接');
|
||||
isSocketConnected.value = true;
|
||||
|
||||
// #ifdef H5
|
||||
startH5Recording().then(() => resolve()).catch(err => {
|
||||
socketTask.close();
|
||||
reject(err);
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
startAppRecording();
|
||||
resolve();
|
||||
// #endif
|
||||
});
|
||||
|
||||
socketTask.onMessage((res) => {
|
||||
// 接收文本结果
|
||||
if (res.data) {
|
||||
recognizedText.value = res.data;
|
||||
}
|
||||
});
|
||||
|
||||
socketTask.onError((err) => {
|
||||
console.error('Socket 错误:', err);
|
||||
isSocketConnected.value = false;
|
||||
stopRecording();
|
||||
});
|
||||
|
||||
socketTask.onClose(() => {
|
||||
isSocketConnected.value = false;
|
||||
console.log('Socket 已关闭');
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const startH5Recording = async () => {
|
||||
try {
|
||||
// 1. 获取麦克风流
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true
|
||||
});
|
||||
h5Stream = stream;
|
||||
|
||||
// 2. 创建 AudioContext
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
audioContext = new AudioContext({
|
||||
sampleRate: 16000
|
||||
});
|
||||
|
||||
mediaStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
|
||||
scriptProcessor.onaudioprocess = (event) => {
|
||||
if (!isSocketConnected.value || !socketTask) return;
|
||||
|
||||
const inputData = event.inputBuffer.getChannelData(0);
|
||||
|
||||
calculateVolume(inputData, true);
|
||||
|
||||
const buffer = new ArrayBuffer(inputData.length * 2);
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < inputData.length; i++) {
|
||||
let s = Math.max(-1, Math.min(1, inputData[i]));
|
||||
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
|
||||
socketTask.send({
|
||||
data: buffer,
|
||||
fail: (e) => console.error('发送音频失败', e)
|
||||
});
|
||||
};
|
||||
|
||||
mediaStreamSource.connect(scriptProcessor);
|
||||
scriptProcessor.connect(audioContext.destination);
|
||||
|
||||
isRecording.value = true;
|
||||
recordingDuration.value = 0;
|
||||
durationTimer = setInterval(() => recordingDuration.value++, 1000);
|
||||
|
||||
console.log('H5 录音已启动');
|
||||
|
||||
} catch (err) {
|
||||
console.error('H5 录音启动失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const stopH5Resources = () => {
|
||||
if (scriptProcessor) scriptProcessor.disconnect();
|
||||
if (mediaStreamSource) mediaStreamSource.disconnect();
|
||||
if (audioContext) audioContext.close();
|
||||
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
|
||||
|
||||
scriptProcessor = null;
|
||||
mediaStreamSource = null;
|
||||
audioContext = null;
|
||||
h5Stream = null;
|
||||
}
|
||||
|
||||
const startAppRecording = () => {
|
||||
recorderManager = uni.getRecorderManager();
|
||||
|
||||
recorderManager.onFrameRecorded((res) => {
|
||||
const {
|
||||
frameBuffer
|
||||
} = res;
|
||||
|
||||
calculateVolume(frameBuffer, false);
|
||||
|
||||
if (isSocketConnected.value && socketTask) {
|
||||
socketTask.send({
|
||||
data: frameBuffer
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
recorderManager.onStart(() => {
|
||||
console.log('APP 录音已开始');
|
||||
isRecording.value = true;
|
||||
recordingDuration.value = 0;
|
||||
durationTimer = setInterval(() => recordingDuration.value++, 1000);
|
||||
});
|
||||
|
||||
recorderManager.onError((err) => {
|
||||
console.error('APP 录音报错:', err);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
recorderManager.start(RECORD_CONFIG);
|
||||
}
|
||||
const stopHardwareResource = () => {
|
||||
// APP/小程序停止
|
||||
if (recorderManager) {
|
||||
recorderManager.stop();
|
||||
}
|
||||
|
||||
// H5停止
|
||||
// #ifdef H5
|
||||
if (scriptProcessor) scriptProcessor.disconnect();
|
||||
if (mediaStreamSource) mediaStreamSource.disconnect();
|
||||
if (audioContext) audioContext.close();
|
||||
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
|
||||
|
||||
scriptProcessor = null;
|
||||
mediaStreamSource = null;
|
||||
audioContext = null;
|
||||
h5Stream = null;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止录音 (通用)
|
||||
*/
|
||||
const stopRecording = () => {
|
||||
// 停止 APP 录音
|
||||
if (recorderManager) {
|
||||
recorderManager.stop();
|
||||
}
|
||||
|
||||
// 停止 H5 录音资源
|
||||
// #ifdef H5
|
||||
stopH5Resources();
|
||||
// #endif
|
||||
|
||||
// 关闭 Socket
|
||||
if (socketTask) {
|
||||
socketTask.close();
|
||||
}
|
||||
|
||||
cleanup();
|
||||
}
|
||||
|
||||
const cancelRecording = () => {
|
||||
if (!isRecording.value) return;
|
||||
|
||||
console.log('取消录音 - 丢弃结果');
|
||||
|
||||
// 1. 停止硬件录音
|
||||
stopHardwareResource();
|
||||
|
||||
// 2. 强制关闭 Socket
|
||||
if (socketTask) {
|
||||
socketTask.close();
|
||||
}
|
||||
|
||||
// 3. 关键:清空已识别的文本
|
||||
recognizedText.value = '';
|
||||
|
||||
// 4. 清理资源
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理状态
|
||||
*/
|
||||
const cleanup = () => {
|
||||
clearInterval(durationTimer);
|
||||
isRecording.value = false;
|
||||
isSocketConnected.value = false;
|
||||
socketTask = null;
|
||||
recorderManager = null;
|
||||
volumeLevel.value = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算音量 (兼容 Float32 和 Int16/ArrayBuffer)
|
||||
*/
|
||||
const calculateVolume = (data, isFloat32) => {
|
||||
let sum = 0;
|
||||
let length = 0;
|
||||
|
||||
if (isFloat32) {
|
||||
length = data.length;
|
||||
for (let i = 0; i < length; i += 10) {
|
||||
sum += Math.abs(data[i]);
|
||||
}
|
||||
volumeLevel.value = Math.min(100, Math.floor((sum / (length / 10)) * 100 * 3));
|
||||
} else {
|
||||
const int16Data = new Int16Array(data);
|
||||
length = int16Data.length;
|
||||
for (let i = 0; i < length; i += 10) {
|
||||
sum += Math.abs(int16Data[i]);
|
||||
}
|
||||
const avg = sum / (length / 10);
|
||||
volumeLevel.value = Math.min(100, Math.floor((avg / 10000) * 100));
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (isRecording.value) {
|
||||
stopRecording();
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
isRecording,
|
||||
isSocketConnected,
|
||||
recordingDuration,
|
||||
volumeLevel,
|
||||
recognizedText,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
cancelRecording
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,8 @@ import {
|
||||
onUnload
|
||||
} from '@dcloudio/uni-app'
|
||||
import WavDecoder from '@/lib/wav-decoder@1.3.0.js'
|
||||
import config from '@/config'
|
||||
|
||||
export function useTTSPlayer() {
|
||||
export function useTTSPlayer(wsUrl) {
|
||||
const isSpeaking = ref(false)
|
||||
const isPaused = ref(false)
|
||||
const isComplete = ref(false)
|
||||
@@ -90,13 +89,12 @@ export function useTTSPlayer() {
|
||||
|
||||
const initWebSocket = () => {
|
||||
const thisPlayId = currentPlayId
|
||||
socket = new WebSocket(config.speechSynthesis)
|
||||
socket = new WebSocket(wsUrl)
|
||||
socket.binaryType = 'arraybuffer'
|
||||
|
||||
socket.onopen = () => {
|
||||
if (pendingText && thisPlayId === activePlayId) {
|
||||
const seepdText = extractSpeechText(pendingText)
|
||||
console.log(seepdText)
|
||||
socket.send(seepdText)
|
||||
pendingText = null
|
||||
}
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
import {
|
||||
ref,
|
||||
onUnmounted,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
// 如果是 uni-app 环境,保留这些导入;如果是纯 Web Vue3,可以移除
|
||||
import {
|
||||
onHide,
|
||||
onUnload
|
||||
} from '@dcloudio/uni-app'
|
||||
import config from '@/config'
|
||||
|
||||
/**
|
||||
* Piper TTS 播放钩子 (WebSocket MSE 流式版 - 含 cancelAudio)
|
||||
* 依赖: 后端必须去除 MP3 ID3 标签 (-map_metadata -1)
|
||||
*/
|
||||
export function useTTSPlayer() {
|
||||
// 状态管理
|
||||
const isSpeaking = ref(false)
|
||||
const isPaused = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 核心对象
|
||||
let audio = null
|
||||
let mediaSource = null
|
||||
let sourceBuffer = null
|
||||
let ws = null
|
||||
|
||||
// 缓冲队列管理
|
||||
let bufferQueue = []
|
||||
let isAppending = false
|
||||
let isStreamEnded = false
|
||||
|
||||
// 初始化 Audio 监听器 (只运行一次)
|
||||
const initAudioElement = () => {
|
||||
if (!audio && typeof window !== 'undefined') {
|
||||
audio = new Audio()
|
||||
|
||||
// 错误监听
|
||||
audio.addEventListener('error', (e) => {
|
||||
// 如果是手动停止导致的 error (src 被置空),忽略
|
||||
if (!audio.src) return
|
||||
console.error('Audio Player Error:', e)
|
||||
resetState()
|
||||
})
|
||||
|
||||
// 播放结束监听
|
||||
audio.addEventListener('ended', () => {
|
||||
resetState()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心朗读方法 (WebSocket)
|
||||
* @param {string} text - 要朗读的文本
|
||||
*/
|
||||
const speak = async (text) => {
|
||||
if (!text) return
|
||||
|
||||
// 1. 提取文本
|
||||
const processedText = extractSpeechText(text)
|
||||
if (!processedText) return
|
||||
|
||||
// 2. 彻底清理旧状态
|
||||
cancelAudio()
|
||||
initAudioElement()
|
||||
|
||||
isLoading.value = true
|
||||
isSpeaking.value = true
|
||||
isPaused.value = false
|
||||
isStreamEnded = false
|
||||
|
||||
// 3. 检查环境
|
||||
if (!window.MediaSource || !window.WebSocket) {
|
||||
console.error('当前环境不支持 MediaSource 或 WebSocket')
|
||||
resetState()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 4. 初始化 MSE
|
||||
mediaSource = new MediaSource()
|
||||
// 绑定 MSE 到 Audio
|
||||
audio.src = URL.createObjectURL(mediaSource)
|
||||
|
||||
// 监听 MSE 打开事件
|
||||
mediaSource.addEventListener('sourceopen', () => {
|
||||
// 防止多次触发
|
||||
if (mediaSource.sourceBuffers.length > 0) return
|
||||
startWebSocketStream(processedText)
|
||||
})
|
||||
|
||||
// 尝试播放 (处理浏览器自动播放策略)
|
||||
const playPromise = audio.play()
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch(e => {
|
||||
console.warn('自动播放被拦截 (需用户交互):', e)
|
||||
// 保持 isSpeaking 为 true,UI 显示播放按钮,用户点击后调用 resume() 即可
|
||||
})
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('TTS Initialization Failed:', err)
|
||||
cancelAudio()
|
||||
}
|
||||
}
|
||||
|
||||
// 启动 WebSocket 流程
|
||||
const startWebSocketStream = (text) => {
|
||||
const mime = 'audio/mpeg'
|
||||
|
||||
// 4.1 创建 SourceBuffer
|
||||
try {
|
||||
sourceBuffer = mediaSource.addSourceBuffer(mime)
|
||||
sourceBuffer.addEventListener('updateend', () => {
|
||||
isAppending = false
|
||||
processQueue()
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('SourceBuffer Create Failed:', e)
|
||||
return
|
||||
}
|
||||
|
||||
// 4.2 计算 WebSocket 地址
|
||||
let baseUrl = config.speechSynthesis2 || ''
|
||||
baseUrl = baseUrl.replace(/\/$/, '')
|
||||
const wsUrl = baseUrl.replace(/^http/, 'ws') + '/ws/synthesize'
|
||||
|
||||
// 4.3 建立连接
|
||||
ws = new WebSocket(wsUrl)
|
||||
ws.binaryType = 'arraybuffer' // 关键
|
||||
|
||||
ws.onopen = () => {
|
||||
// console.log('WS Open')
|
||||
ws.send(JSON.stringify({
|
||||
text: text,
|
||||
speaker_id: 0,
|
||||
length_scale: 1.0,
|
||||
noise_scale: 0.667
|
||||
}))
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
bufferQueue.push(event.data)
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (e) => {
|
||||
console.error('WS Error:', e)
|
||||
cancelAudio()
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
// console.log('WS Closed')
|
||||
isStreamEnded = true
|
||||
// 检查是否需要结束 MSE 流
|
||||
checkEndOfStream()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理缓冲队列
|
||||
const processQueue = () => {
|
||||
if (!sourceBuffer || sourceBuffer.updating || bufferQueue.length === 0) {
|
||||
// 如果队列空了,且流已结束,尝试结束 MSE
|
||||
if (bufferQueue.length === 0 && isStreamEnded && !sourceBuffer.updating) {
|
||||
checkEndOfStream()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
isAppending = true
|
||||
const chunk = bufferQueue.shift()
|
||||
|
||||
try {
|
||||
sourceBuffer.appendBuffer(chunk)
|
||||
} catch (e) {
|
||||
// console.error('AppendBuffer Error:', e)
|
||||
isAppending = false
|
||||
}
|
||||
}
|
||||
|
||||
// 结束 MSE 流
|
||||
const checkEndOfStream = () => {
|
||||
if (mediaSource && mediaSource.readyState === 'open' && bufferQueue.length === 0 && !sourceBuffer
|
||||
?.updating) {
|
||||
try {
|
||||
mediaSource.endOfStream()
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (audio && !audio.paused) {
|
||||
audio.pause()
|
||||
isPaused.value = true
|
||||
isSpeaking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resume = () => {
|
||||
if (audio && audio.paused) {
|
||||
audio.play()
|
||||
isPaused.value = false
|
||||
isSpeaking.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// === 新增/核心方法:取消并停止 ===
|
||||
const cancelAudio = () => {
|
||||
// 1. 断开 WebSocket (停止数据接收)
|
||||
if (ws) {
|
||||
// 移除监听器防止报错
|
||||
ws.onclose = null
|
||||
ws.onerror = null
|
||||
ws.onmessage = null
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
|
||||
// 2. 停止音频播放
|
||||
if (audio) {
|
||||
audio.pause()
|
||||
// 释放 Blob URL 内存
|
||||
if (audio.src) {
|
||||
URL.revokeObjectURL(audio.src)
|
||||
audio.removeAttribute('src')
|
||||
}
|
||||
audio.currentTime = 0
|
||||
}
|
||||
|
||||
// 3. 清理 MSE 对象
|
||||
if (mediaSource) {
|
||||
try {
|
||||
if (mediaSource.readyState === 'open') {
|
||||
mediaSource.endOfStream()
|
||||
}
|
||||
} catch (e) {}
|
||||
mediaSource = null
|
||||
}
|
||||
|
||||
sourceBuffer = null
|
||||
bufferQueue = []
|
||||
isAppending = false
|
||||
isStreamEnded = false
|
||||
|
||||
// 4. 重置 UI 状态
|
||||
resetState()
|
||||
}
|
||||
|
||||
// 只是重置 UI 变量的辅助函数
|
||||
const resetState = () => {
|
||||
isSpeaking.value = false
|
||||
isPaused.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// 别名 stop -> cancelAudio (保持兼容性)
|
||||
const stop = cancelAudio
|
||||
|
||||
// === 生命周期 ===
|
||||
onMounted(() => {
|
||||
initAudioElement()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAudio()
|
||||
audio = null
|
||||
})
|
||||
|
||||
if (typeof onHide === 'function') onHide(cancelAudio)
|
||||
if (typeof onUnload === 'function') onUnload(cancelAudio)
|
||||
|
||||
return {
|
||||
speak,
|
||||
pause,
|
||||
resume,
|
||||
stop,
|
||||
cancelAudio, // 新增导出
|
||||
isSpeaking,
|
||||
isPaused,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取文本逻辑
|
||||
*/
|
||||
function extractSpeechText(markdown) {
|
||||
if (!markdown || markdown.indexOf('job-json') === -1) {
|
||||
return markdown;
|
||||
}
|
||||
|
||||
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
|
||||
const jobs = [];
|
||||
let match;
|
||||
let lastJobEndIndex = 0;
|
||||
let firstJobStartIndex = -1;
|
||||
|
||||
while ((match = jobRegex.exec(markdown)) !== null) {
|
||||
const jobStr = match[1];
|
||||
try {
|
||||
const job = JSON.parse(jobStr);
|
||||
jobs.push(job);
|
||||
if (firstJobStartIndex === -1) {
|
||||
firstJobStartIndex = match.index;
|
||||
}
|
||||
lastJobEndIndex = jobRegex.lastIndex;
|
||||
} catch (e) {
|
||||
console.warn('JSON 解析失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
const guideText = firstJobStartIndex > 0 ?
|
||||
markdown.slice(0, firstJobStartIndex).trim() : '';
|
||||
|
||||
const endingText = lastJobEndIndex < markdown.length ?
|
||||
markdown.slice(lastJobEndIndex).trim() : '';
|
||||
|
||||
const jobTexts = jobs.map((job, index) => {
|
||||
return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}。`;
|
||||
});
|
||||
|
||||
const finalTextParts = [];
|
||||
if (guideText) finalTextParts.push(guideText);
|
||||
finalTextParts.push(...jobTexts);
|
||||
if (endingText) finalTextParts.push(endingText);
|
||||
|
||||
return finalTextParts.join('\n');
|
||||
}
|
||||
@@ -22,15 +22,17 @@
|
||||
<script>
|
||||
eruda.init();
|
||||
</script> -->
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
// VConsole 默认会挂载到 `window.VConsole` 上
|
||||
var vConsole = new window.VConsole();
|
||||
</script>
|
||||
</script> -->
|
||||
<!-- 爱山东jssdk 本sdk存在性能问题 -->
|
||||
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
|
||||
<!-- 只在内网有效 -->
|
||||
<script type="text/javascript" src="./static/js/SM.js"></script>
|
||||
<script type="text/javascript" src="./static/js/pixi.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"locale": "zh-Hans",
|
||||
"h5": {
|
||||
"router": {
|
||||
"base": "./",
|
||||
"base": "/app/",
|
||||
"mode": "hash"
|
||||
},
|
||||
"title": "青岛智慧就业服务",
|
||||
|
||||
@@ -121,7 +121,7 @@ const pageState = reactive({
|
||||
maxPage: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const hasnext = ref(false);
|
||||
const hasnext = ref(true);
|
||||
|
||||
const zphId = ref('');
|
||||
const pageOptions = ref({});
|
||||
|
||||
@@ -186,8 +186,7 @@ function uploadResume(tempFilePath, loading) {
|
||||
header['Authorization'] = encodeURIComponent(Authorization);
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: config.baseUrl + '/app/user/resume/recognition',
|
||||
// url: config.baseUrl + '/app/oss/uploadToObs',
|
||||
url: config.baseUrl + '/app/oss/uploadToObs',
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
header,
|
||||
|
||||
@@ -59,10 +59,10 @@ const pages = reactive({
|
||||
year: 0,
|
||||
month: 0,
|
||||
});
|
||||
|
||||
const hasZphDateArray = ref([]);
|
||||
|
||||
onLoad((options) => {
|
||||
updateDateArray();
|
||||
if (options.date) {
|
||||
current.value = {
|
||||
date: options?.date || null,
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<PopupFeeBack ref="feeback" @onClose="colseFeeBack" @onSend="confirmFeeBack"></PopupFeeBack>
|
||||
<PopupFeeBack ref="feeback" @onSend="confirmFeeBack"></PopupFeeBack>
|
||||
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
|
||||
</view>
|
||||
</template>
|
||||
@@ -268,9 +268,9 @@ import WaveDisplay from './WaveDisplay.vue';
|
||||
import FileIcon from './fileIcon.vue';
|
||||
import FileText from './fileText.vue';
|
||||
// 系统功能hook和阿里云hook
|
||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder2.js';
|
||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
|
||||
import { useTTSPlayer } from '@/hook/useTTSPlayer2.js';
|
||||
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
||||
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
|
||||
// 全局
|
||||
const { $api, navTo, throttle } = inject('globalFunction');
|
||||
@@ -612,23 +612,17 @@ function userGoodFeedback(msg) {
|
||||
// $api.msg('该功能正在开发中,敬请期待后续更新!');
|
||||
feeback.value?.open();
|
||||
feebackData.value = msg;
|
||||
uni.hideTabBar()
|
||||
}
|
||||
|
||||
function confirmFeeBack(value) {
|
||||
useChatGroupDBStore()
|
||||
.badFeedback(feebackData.value, value)
|
||||
.then(() => {
|
||||
uni.showTabBar()
|
||||
feeback.value?.close();
|
||||
feeBackTips.value?.open();
|
||||
});
|
||||
}
|
||||
|
||||
function colseFeeBack() {
|
||||
uni.showTabBar()
|
||||
}
|
||||
|
||||
function readMarkdown(value, index) {
|
||||
speechIndex.value = index;
|
||||
if (speechIndex.value !== index) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" @change="changePopup" background-color="#F6F6F6">
|
||||
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" background-color="#F6F6F6">
|
||||
<view class="feeback">
|
||||
<view class="titile">反馈</view>
|
||||
<view class="pop-h3">针对问题</view>
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
const emit = defineEmits(['onSend', 'onClose']);
|
||||
const emit = defineEmits(['onSend']);
|
||||
const { $api } = inject('globalFunction');
|
||||
const popup = ref(null);
|
||||
const inputText = ref('');
|
||||
@@ -66,13 +66,6 @@ function close() {
|
||||
popup.value.close();
|
||||
}
|
||||
|
||||
function changePopup(e) {
|
||||
if (e.show) {
|
||||
} else {
|
||||
emit('onClose');
|
||||
}
|
||||
}
|
||||
|
||||
function send() {
|
||||
const text = getLabel();
|
||||
if (text) {
|
||||
|
||||
371
pages/index/components/AIMatch.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<view class="container" id="pixi-box" ref="pixiContainerRef"></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref, nextTick } from 'vue';
|
||||
const emit = defineEmits(['tag-click']);
|
||||
|
||||
// DOM Ref
|
||||
const pixiContainerRef = ref(null);
|
||||
|
||||
// PIXI 变量
|
||||
let app = null;
|
||||
let tagsContainer = null;
|
||||
let activeTagInstances = [];
|
||||
|
||||
// 配置数据
|
||||
const mockTags = [
|
||||
{ name: '医生', bgColor: 0x0069fe, fontColor: 0xffffff, size: 17, opacity: 1.0, angle: 0, radius: 0 },
|
||||
{
|
||||
name: '工程师',
|
||||
bgColor: 0x87e2ec,
|
||||
fontColor: 0xffffff,
|
||||
size: 14,
|
||||
opacity: 1,
|
||||
angle: -Math.PI / 2,
|
||||
radius: 68,
|
||||
tailRotation: Math.PI / 2,
|
||||
},
|
||||
{
|
||||
name: '建筑师',
|
||||
bgColor: 0xffebeb,
|
||||
tailColor: 0xffe1e1,
|
||||
fontColor: 0xff6969,
|
||||
size: 11.5,
|
||||
opacity: 1,
|
||||
angle: -Math.PI / 4.2,
|
||||
radius: 125,
|
||||
tailRotation: (3 * Math.PI) / 4,
|
||||
},
|
||||
{
|
||||
name: '律师',
|
||||
bgColor: 0x21ea85,
|
||||
fontColor: 0xffffff,
|
||||
size: 15,
|
||||
opacity: 1,
|
||||
angle: -Math.PI / 10,
|
||||
radius: 130,
|
||||
tailRotation: (3 * Math.PI) / 4,
|
||||
},
|
||||
{
|
||||
name: '记者',
|
||||
bgColor: 0xebf3ff,
|
||||
tailColor: 0xb9d3ff,
|
||||
fontColor: 0x1d71ef,
|
||||
size: 12,
|
||||
opacity: 1,
|
||||
angle: Math.PI / 120,
|
||||
radius: 130,
|
||||
tailRotation: (3 * Math.PI) / 3.4,
|
||||
},
|
||||
{
|
||||
name: '程序员',
|
||||
bgColor: 0xffd4b6,
|
||||
fontColor: 0xffffff,
|
||||
size: 14,
|
||||
opacity: 1,
|
||||
angle: Math.PI / 7,
|
||||
radius: 120,
|
||||
tailRotation: (5 * Math.PI) / 4,
|
||||
},
|
||||
{
|
||||
name: '摄影师',
|
||||
bgColor: 0xd8e5fe,
|
||||
tailColor: 0xb9d3ff,
|
||||
fontColor: 0x1d71ef,
|
||||
size: 11,
|
||||
opacity: 1,
|
||||
angle: Math.PI / 3,
|
||||
radius: 79,
|
||||
tailRotation: (3 * Math.PI) / 2,
|
||||
},
|
||||
{
|
||||
name: '设计师',
|
||||
bgColor: 0xff9400,
|
||||
fontColor: 0xffffff,
|
||||
size: 14,
|
||||
opacity: 1,
|
||||
angle: (2 * Math.PI) / 3,
|
||||
radius: 92,
|
||||
tailRotation: (7 * Math.PI) / 4,
|
||||
},
|
||||
{
|
||||
name: '心理咨询师',
|
||||
bgColor: 0xebf3ff,
|
||||
tailColor: 0xb9d3ff,
|
||||
fontColor: 0x1d71ef,
|
||||
size: 10.5,
|
||||
opacity: 1,
|
||||
angle: (5.4 * Math.PI) / 6,
|
||||
radius: 110,
|
||||
tailRotation:(3 * Math.PI) /1.78,
|
||||
},
|
||||
{
|
||||
name: '护士',
|
||||
bgColor: 0xff6969,
|
||||
fontColor: 0xffffff,
|
||||
size: 15,
|
||||
opacity: 1,
|
||||
angle: (6.3 * Math.PI) / 5.9,
|
||||
radius: 110,
|
||||
tailRotation: Math.PI / 4,
|
||||
},
|
||||
{
|
||||
name: '会计',
|
||||
bgColor: 0xfce9c9,
|
||||
fontColor: 0xfbc55f,
|
||||
size: 13,
|
||||
opacity: 1,
|
||||
angle: (7.2 * Math.PI) / 5.9,
|
||||
radius: 120,
|
||||
tailRotation: Math.PI / 4,
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
initPixi();
|
||||
}, 100);
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (app) {
|
||||
app.destroy(true, { children: true, texture: true, baseTexture: true });
|
||||
app = null;
|
||||
}
|
||||
});
|
||||
|
||||
const getContainerDOM = () => {
|
||||
const refVal = pixiContainerRef.value;
|
||||
if (!refVal) return document.getElementById('pixi-box');
|
||||
if (refVal.$el) return refVal.$el;
|
||||
return refVal;
|
||||
};
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
const initPixi = () => {
|
||||
const container = getContainerDOM();
|
||||
if (!container) return;
|
||||
|
||||
const width = container.clientWidth || 300;
|
||||
const height = container.clientHeight || 300;
|
||||
|
||||
if (app) return;
|
||||
|
||||
app = new PIXI.Application({
|
||||
width: width,
|
||||
height: height,
|
||||
backgroundAlpha: 0,
|
||||
backgroundColor: 0xf5f7fa,
|
||||
antialias: true,
|
||||
resolution: window.devicePixelRatio || 1,
|
||||
autoDensity: true,
|
||||
});
|
||||
app.view.style.touchAction = 'auto';
|
||||
|
||||
container.appendChild(app.view);
|
||||
|
||||
tagsContainer = new PIXI.Container();
|
||||
app.stage.addChild(tagsContainer);
|
||||
|
||||
renderScene(width, height);
|
||||
};
|
||||
|
||||
const renderScene = (sw, sh) => {
|
||||
tagsContainer.removeChildren();
|
||||
activeTagInstances = [];
|
||||
|
||||
const baseSize = 375;
|
||||
const scaleFactor = (Math.min(sw, sh) / baseSize) * 0.9;
|
||||
|
||||
mockTags.forEach((data, index) => {
|
||||
const scaledRadius = data.radius * (scaleFactor < 1 ? 1 : scaleFactor * 0.8);
|
||||
|
||||
let x = sw / 2 + scaledRadius * Math.cos(data.angle);
|
||||
let y = sh / 2 + scaledRadius * Math.sin(data.angle);
|
||||
|
||||
const tag = createTag(data, index);
|
||||
|
||||
tagsContainer.addChild(tag);
|
||||
|
||||
const safeW = tag.width / 2 + 10;
|
||||
const safeH = tag.height / 2 + 10;
|
||||
|
||||
// 强制修正 x 和 y,使其不超出屏幕
|
||||
x = clamp(x, safeW, sw - safeW);
|
||||
y = clamp(y, safeH, sh - safeH);
|
||||
|
||||
tag.x = x;
|
||||
tag.y = y;
|
||||
|
||||
// 4. 保存元数据
|
||||
tag.userData = {
|
||||
originalX: x,
|
||||
originalY: y,
|
||||
angle: data.angle,
|
||||
radius: scaledRadius,
|
||||
floatOffset: Math.random() * Math.PI * 2,
|
||||
floatSpeed: 0.01 + Math.random() * 0.02,
|
||||
floatRange: 2 + Math.random() * 2,
|
||||
safeH: safeH,
|
||||
};
|
||||
|
||||
if (data.radius > 0) {
|
||||
const tail = createCometTail( data.tailColor || data.bgColor, data.tailRotation, tag.width);
|
||||
tag.addChildAt(tail, 0);
|
||||
tag.updateTail = () => tail.updateAnim();
|
||||
}
|
||||
|
||||
activeTagInstances.push(tag);
|
||||
});
|
||||
|
||||
// 动画循环
|
||||
app.ticker.add(() => {
|
||||
const screenH = app.screen.height;
|
||||
|
||||
activeTagInstances.forEach((tag) => {
|
||||
const meta = tag.userData;
|
||||
if (meta) {
|
||||
// 计算新的浮动位置
|
||||
meta.floatOffset += meta.floatSpeed;
|
||||
let nextY = meta.originalY + Math.sin(meta.floatOffset) * meta.floatRange;
|
||||
|
||||
// 再次进行边界检查
|
||||
if (nextY < meta.safeH) nextY = meta.safeH;
|
||||
if (nextY > screenH - meta.safeH) nextY = screenH - meta.safeH;
|
||||
|
||||
tag.y = nextY;
|
||||
|
||||
if (tag.updateTail) tag.updateTail();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createTag = (tagData, index) => {
|
||||
const tagGroup = new PIXI.Container();
|
||||
tagGroup.eventMode = 'static';
|
||||
tagGroup.cursor = 'pointer';
|
||||
|
||||
tagGroup.on('pointertap', () => emit('tag-click', tagData));
|
||||
|
||||
const text = new PIXI.Text(tagData.name, {
|
||||
fontFamily: ['PingFang SC', 'Microsoft YaHei', 'Arial'],
|
||||
fontSize: tagData.size,
|
||||
fill: tagData.fontColor,
|
||||
padding: 4,
|
||||
resolution: 2,
|
||||
});
|
||||
text.anchor.set(0.5);
|
||||
|
||||
const paddingH = 26;
|
||||
const paddingV = 10;
|
||||
let bgWidth = text.width + paddingH;
|
||||
let bgHeight = text.height + paddingV;
|
||||
|
||||
if (index === 0) bgWidth = Math.max(bgWidth, tagData.size * 4.5);
|
||||
|
||||
const bg = new PIXI.Graphics();
|
||||
bg.beginFill(tagData.bgColor, tagData.opacity ?? 1);
|
||||
bg.drawRoundedRect(-bgWidth / 2, -bgHeight / 2, bgWidth, bgHeight, bgHeight / 2);
|
||||
bg.endFill();
|
||||
|
||||
tagGroup.addChild(bg);
|
||||
tagGroup.addChild(text);
|
||||
|
||||
return tagGroup;
|
||||
};
|
||||
|
||||
const createCometTail = (bgColor, tailRotation, parentWidth) => {
|
||||
const tailGroup = new PIXI.Container();
|
||||
const graphics = new PIXI.Graphics();
|
||||
tailGroup.addChild(graphics);
|
||||
|
||||
const baseLength = 45;
|
||||
const startWidth = parentWidth * 0.6;
|
||||
const endWidth = 20;
|
||||
|
||||
let breathPhase = Math.random() * Math.PI * 2;
|
||||
const breathSpeed = 0.04;
|
||||
|
||||
tailGroup.updateAnim = () => {
|
||||
breathPhase += breathSpeed;
|
||||
const breathScale = 0.85 + 0.15 * Math.sin(breathPhase);
|
||||
graphics.clear();
|
||||
const currentLength = baseLength * breathScale;
|
||||
|
||||
const cos = Math.cos(tailRotation);
|
||||
const sin = Math.sin(tailRotation);
|
||||
const perpX = -sin;
|
||||
const perpY = cos;
|
||||
|
||||
const p1 = { x: perpX * (startWidth / 2), y: perpY * (startWidth / 2) };
|
||||
const p2 = { x: -perpX * (startWidth / 2), y: -perpY * (startWidth / 2) };
|
||||
const endCX = cos * currentLength;
|
||||
const endCY = sin * currentLength;
|
||||
const p3 = { x: endCX - perpX * (endWidth / 2), y: endCY - perpY * (endWidth / 2) };
|
||||
const p4 = { x: endCX + perpX * (endWidth / 2), y: endCY + perpY * (endWidth / 2) };
|
||||
|
||||
const segments = 8;
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const t1 = i / segments;
|
||||
const t2 = (i + 1) / segments;
|
||||
const alpha = 0.4 * (1 - t1);
|
||||
const sp1 = { x: p1.x + (p4.x - p1.x) * t1, y: p1.y + (p4.y - p1.y) * t1 };
|
||||
const sp2 = { x: p2.x + (p3.x - p2.x) * t1, y: p2.y + (p3.y - p2.y) * t1 };
|
||||
const ep1 = { x: p1.x + (p4.x - p1.x) * t2, y: p1.y + (p4.y - p1.y) * t2 };
|
||||
const ep2 = { x: p2.x + (p3.x - p2.x) * t2, y: p2.y + (p3.y - p2.y) * t2 };
|
||||
graphics.beginFill(bgColor, alpha);
|
||||
graphics.moveTo(sp1.x, sp1.y);
|
||||
graphics.lineTo(sp2.x, sp2.y);
|
||||
graphics.lineTo(ep2.x, ep2.y);
|
||||
graphics.lineTo(ep1.x, ep1.y);
|
||||
graphics.endFill();
|
||||
}
|
||||
};
|
||||
tailGroup.updateAnim();
|
||||
return tailGroup;
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
const container = getContainerDOM();
|
||||
if (!app || !container) return;
|
||||
|
||||
const w = container.clientWidth || 300;
|
||||
const h = container.clientHeight || 300;
|
||||
|
||||
app.renderer.resize(w, h);
|
||||
|
||||
activeTagInstances.forEach((tag) => {
|
||||
const meta = tag.userData;
|
||||
if (!meta) return;
|
||||
|
||||
let newX = w / 2 + meta.radius * Math.cos(meta.angle);
|
||||
let newY = h / 2 + meta.radius * Math.sin(meta.angle);
|
||||
|
||||
const safeW = tag.width / 2 + 10;
|
||||
const safeH = tag.height / 2 + 10;
|
||||
|
||||
meta.originalX = clamp(newX, safeW, w - safeW);
|
||||
meta.originalY = clamp(newY, safeH, h - safeH);
|
||||
meta.safeH = safeH; // 更新安全高度
|
||||
|
||||
tag.x = meta.originalX;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: #b9d3ff;
|
||||
}
|
||||
</style>
|
||||
@@ -148,14 +148,14 @@
|
||||
<!-- 筛选 -->
|
||||
<select-filter ref="selectFilterModel"></select-filter>
|
||||
|
||||
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
|
||||
<view class="maskFristEntry" v-if="maskFristEntry">
|
||||
<view class="entry-content">
|
||||
<text class="text1">左滑查看视频</text>
|
||||
<text class="text2">左滑查看视频</text>
|
||||
<view class="goExperience">去体验</view>
|
||||
<view class="maskFristEntry-Close" @click="closeFristEntry">1</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -183,7 +183,7 @@ const waterfallsFlowRef = ref(null);
|
||||
const loadmoreRef = ref(null);
|
||||
const conditionSearch = ref({});
|
||||
const waterfallcolumn = ref(2);
|
||||
const maskFristEntry = ref(false);
|
||||
const maskFristEntry = ref(true);
|
||||
const state = reactive({
|
||||
tabIndex: 'all',
|
||||
});
|
||||
|
||||
1386
pages/index/components/index-refactor.vue
Normal file
@@ -35,7 +35,6 @@
|
||||
ref="waterfallsFlowRef"
|
||||
:column="columnCount"
|
||||
:columnSpace="columnSpace"
|
||||
@loaded="imageloaded"
|
||||
:value="list"
|
||||
>
|
||||
<template v-slot:default="job">
|
||||
@@ -63,8 +62,7 @@
|
||||
</view>
|
||||
</template>
|
||||
</custom-waterfalls-flow>
|
||||
<empty v-if="!list.length"></empty>
|
||||
<loadmore v-if="list.length >= pageSize" ref="loadmoreRef"></loadmore>
|
||||
<loadmore ref="loadmoreRef"></loadmore>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -94,7 +92,7 @@ const state = reactive({
|
||||
// 响应式搜索条件(可以被修改)
|
||||
const searchParams = ref({});
|
||||
const pageSize = ref(10);
|
||||
const { list, loading, refresh, loadMore } = usePagination(
|
||||
const { list, loading, refresh, loadMore,finished } = usePagination(
|
||||
(params) => $api.createRequest('/app/job/littleVideo', params),
|
||||
dataToImg, // 转换函数
|
||||
{
|
||||
@@ -106,10 +104,25 @@ const { list, loading, refresh, loadMore } = usePagination(
|
||||
},
|
||||
}
|
||||
);
|
||||
watch(()=>finished.value, (newVal) => {
|
||||
if (newVal) {
|
||||
// 确保瀑布流组件知道数据已加载完成
|
||||
loadmoreRef.value?.change('noMore')
|
||||
}else{
|
||||
loadmoreRef.value?.change('more')
|
||||
}
|
||||
})
|
||||
|
||||
function imageloaded() {
|
||||
loadmoreRef.value?.change('more');
|
||||
}
|
||||
// function imageloaded() {
|
||||
// nextTick(() => {
|
||||
// console.log('触发',finished.value)
|
||||
// if (finished.value) {
|
||||
// loadmoreRef.value?.change('noMore')
|
||||
// } else {
|
||||
// loadmoreRef.value?.change('more')
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
const { columnCount, columnSpace } = useColumnCount(() => {
|
||||
pageSize.value = 10 * (columnCount.value - 1);
|
||||
@@ -190,7 +203,6 @@ defineExpose({ loadData });
|
||||
text-overflow: clip;
|
||||
.scroll-content{
|
||||
padding: 24rpx
|
||||
height: calc(100% - 48rpx)
|
||||
}
|
||||
|
||||
.nav-filter
|
||||
|
||||
@@ -60,11 +60,13 @@
|
||||
import { reactive, inject, watch, ref, onMounted } from 'vue';
|
||||
import Tabbar from '@/components/tabbar/midell-box.vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import IndexRefactor from './components/index-refactor.vue';
|
||||
import IndexOne from './components/index-one.vue';
|
||||
import IndexTwo from './components/index-two.vue';
|
||||
const loadedMap = reactive([false, false]);
|
||||
const swiperRefs = [ref(null), ref(null)];
|
||||
const components = [IndexOne, IndexTwo];
|
||||
// const components = [IndexOne, IndexTwo];
|
||||
const components = [IndexRefactor, IndexTwo];
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useReadMsg } from '@/stores/useReadMsg';
|
||||
const { unreadCount } = storeToRefs(useReadMsg());
|
||||
|
||||
@@ -313,7 +313,7 @@ function complete() {
|
||||
.backdoor{
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 300rpx;
|
||||
bottom: 200rpx;
|
||||
}
|
||||
.input-nx
|
||||
position: relative
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="main-scroll">
|
||||
<view class="scrollmain">
|
||||
<view v-if="msgList.length" class="scrollmain">
|
||||
<view
|
||||
class="list-card press-button"
|
||||
v-for="(item, index) in msgList"
|
||||
@@ -15,15 +15,14 @@
|
||||
></image>
|
||||
<image
|
||||
class="card-img-flame"
|
||||
v-else-if="item.title === '职位上新'"
|
||||
v-if="item.title === '职位上新'"
|
||||
src="/static/icon/msgtype2.png"
|
||||
></image>
|
||||
<image
|
||||
class="card-img-flame"
|
||||
v-else-if="item.title === '系统通知'"
|
||||
v-if="item.title === '系统通知'"
|
||||
src="/static/icon/msgtype3.png"
|
||||
></image>
|
||||
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
|
||||
<view class="subscript" v-if="item.notReadCount || !item.isRead">
|
||||
{{ item.notReadCount || '' }}
|
||||
</view>
|
||||
@@ -38,6 +37,7 @@
|
||||
</view>
|
||||
<empty v-if="!msgList.length"></empty>
|
||||
</view>
|
||||
<empty v-else pdTop="200" content="暂无消息~"></empty>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
@@ -72,9 +72,6 @@ function seeDetail(item, index) {
|
||||
case '系统通知':
|
||||
navTo('/packageA/pages/systemNotification/systemNotification');
|
||||
break;
|
||||
default:
|
||||
useReadMsg().markAsRead(item, index);
|
||||
navTo('/packageA/pages/newJobPosition/newJobPosition');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="main-scroll">
|
||||
<view class="scrollmain">
|
||||
<view v-if="unreadMsgList.length" class="scrollmain">
|
||||
<view
|
||||
class="list-card press-button"
|
||||
v-for="(item, index) in unreadMsgList"
|
||||
@@ -15,15 +15,14 @@
|
||||
></image>
|
||||
<image
|
||||
class="card-img-flame"
|
||||
v-else-if="item.title === '职位上新'"
|
||||
v-if="item.title === '职位上新'"
|
||||
src="/static/icon/msgtype2.png"
|
||||
></image>
|
||||
<image
|
||||
class="card-img-flame"
|
||||
v-else-if="item.title === '系统通知'"
|
||||
v-if="item.title === '系统通知'"
|
||||
src="/static/icon/msgtype3.png"
|
||||
></image>
|
||||
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
|
||||
<view class="subscript" v-if="item.notReadCount">{{ item.notReadCount }}</view>
|
||||
</view>
|
||||
<view class="card-info">
|
||||
@@ -36,6 +35,7 @@
|
||||
</view>
|
||||
<empty v-if="!unreadMsgList.length"></empty>
|
||||
</view>
|
||||
<empty v-else pdTop="200" content="暂无消息~"></empty>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
@@ -59,19 +59,6 @@ async function loadData() {
|
||||
|
||||
function seeDetail(item) {
|
||||
console.log(item);
|
||||
switch (item.title) {
|
||||
case '职位上新':
|
||||
useReadMsg().markAsRead(item, index);
|
||||
navTo('/packageA/pages/newJobPosition/newJobPosition');
|
||||
break;
|
||||
case '招聘会预约提醒':
|
||||
useReadMsg().markAsRead(item, index);
|
||||
navTo('/packageA/pages/reservation/reservation');
|
||||
break;
|
||||
case '系统通知':
|
||||
navTo('/packageA/pages/systemNotification/systemNotification');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ loadData });
|
||||
|
||||
BIN
static/icon/add-circle.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/icon/ai-card-bg.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/icon/bottom-card-bg.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/icon/flame3.png
Normal file
|
After Width: | Height: | Size: 987 B |
BIN
static/icon/index-robot.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
static/icon/index-search.png
Normal file
|
After Width: | Height: | Size: 683 B |
BIN
static/icon/index-text-bg.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/icon/index-text-bg2.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/icon/item-bg-img1.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/icon/item-bg-img2.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/icon/item-bg-img3.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
static/icon/item-bg-text.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/icon/leart-gold.png
Normal file
|
After Width: | Height: | Size: 778 B |
BIN
static/icon/match-card-bg.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
static/icon/pintDate2.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
static/icon/polygon-down.png
Normal file
|
After Width: | Height: | Size: 295 B |
BIN
static/icon/position-icon.png
Normal file
|
After Width: | Height: | Size: 555 B |
BIN
static/icon/top-card-bg.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/icon/video-mask.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
static/icon/work-img1.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
static/icon/work-img2.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1108
static/js/pixi.min.js
vendored
Normal file
@@ -3,7 +3,7 @@
|
||||
<view
|
||||
v-for="(item, index) in data.column"
|
||||
:key="index"
|
||||
class="waterfalls-flow-column"
|
||||
class="waterfalls-flow-column "
|
||||
:id="`waterfalls_flow_column_${index + 1}`"
|
||||
:msg="msg"
|
||||
:style="{ width: w, 'margin-left': index == 0 ? 0 : m }"
|
||||
|
||||