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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject, computed } from 'vue';
|
import { inject, computed } from "vue";
|
||||||
import useDictStore from '../../stores/useDictStore';
|
import useDictStore from "../../stores/useDictStore";
|
||||||
const { minSalary, maxSalary, isMonth } = defineProps(['minSalary', 'maxSalary', 'isMonth']);
|
const { minSalary, maxSalary } = defineProps(["minSalary", "maxSalary"]);
|
||||||
|
|
||||||
const salaryText = computed(() => {
|
|
||||||
if (!minSalary || !maxSalary) return '面议';
|
|
||||||
if (isMonth) {
|
|
||||||
return `${minSalary}-${maxSalary}/月`;
|
|
||||||
}
|
|
||||||
return `${minSalary / 1000}k-${maxSalary / 1000}k`;
|
|
||||||
});
|
|
||||||
</script>
|
</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://192.168.3.29:8081',
|
||||||
// baseUrl: 'http://10.213.6.207:19010/api',
|
// baseUrl: 'http://10.213.6.207:19010/api',
|
||||||
// 语音转文字
|
// 语音转文字
|
||||||
// vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
|
// vioceBaseURl: 'ws://39.98.44.136:8080/speech-recognition',
|
||||||
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/system/asr/connect', // 自定义
|
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
|
||||||
// 语音合成
|
// 语音合成
|
||||||
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
|
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
|
||||||
speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/',
|
|
||||||
// indexedDB
|
// indexedDB
|
||||||
DBversion: 2,
|
DBversion: 2,
|
||||||
// 只使用本地缓寸的数据
|
// 只使用本地缓寸的数据
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ export function usePagination(
|
|||||||
const res = await requestFn(params)
|
const res = await requestFn(params)
|
||||||
|
|
||||||
const rawData = res[dataKey]
|
const rawData = res[dataKey]
|
||||||
const total = res[totalKey] || 99999999
|
|
||||||
console.log(total, rawData)
|
|
||||||
const data = typeof transformFn === 'function' ? transformFn(rawData) : rawData
|
const data = typeof transformFn === 'function' ? transformFn(rawData) : rawData
|
||||||
|
|
||||||
if (type === 'refresh') {
|
if (type === 'refresh') {
|
||||||
@@ -96,9 +95,9 @@ export function usePagination(
|
|||||||
list.value.push(...data)
|
list.value.push(...data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const total = res[totalKey] || list.value?.length
|
||||||
pageState.total = total
|
pageState.total = total
|
||||||
pageState.maxPage = Math.ceil(total / pageState.pageSize)
|
pageState.maxPage = Math.ceil(total / pageState.pageSize)
|
||||||
|
|
||||||
finished.value = list.value.length >= total
|
finished.value = list.value.length >= total
|
||||||
empty.value = list.value.length === 0
|
empty.value = list.value.length === 0
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
|
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
|
||||||
// Alibaba Cloud
|
|
||||||
export function useAudioRecorder() {
|
export function useAudioRecorder() {
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
const isStopping = 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
|
onUnload
|
||||||
} from '@dcloudio/uni-app'
|
} from '@dcloudio/uni-app'
|
||||||
import WavDecoder from '@/lib/wav-decoder@1.3.0.js'
|
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 isSpeaking = ref(false)
|
||||||
const isPaused = ref(false)
|
const isPaused = ref(false)
|
||||||
const isComplete = ref(false)
|
const isComplete = ref(false)
|
||||||
@@ -90,13 +89,12 @@ export function useTTSPlayer() {
|
|||||||
|
|
||||||
const initWebSocket = () => {
|
const initWebSocket = () => {
|
||||||
const thisPlayId = currentPlayId
|
const thisPlayId = currentPlayId
|
||||||
socket = new WebSocket(config.speechSynthesis)
|
socket = new WebSocket(wsUrl)
|
||||||
socket.binaryType = 'arraybuffer'
|
socket.binaryType = 'arraybuffer'
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
if (pendingText && thisPlayId === activePlayId) {
|
if (pendingText && thisPlayId === activePlayId) {
|
||||||
const seepdText = extractSpeechText(pendingText)
|
const seepdText = extractSpeechText(pendingText)
|
||||||
console.log(seepdText)
|
|
||||||
socket.send(seepdText)
|
socket.send(seepdText)
|
||||||
pendingText = null
|
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>
|
<script>
|
||||||
eruda.init();
|
eruda.init();
|
||||||
</script> -->
|
</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>
|
<script>
|
||||||
// VConsole 默认会挂载到 `window.VConsole` 上
|
// VConsole 默认会挂载到 `window.VConsole` 上
|
||||||
var vConsole = new window.VConsole();
|
var vConsole = new window.VConsole();
|
||||||
</script>
|
</script> -->
|
||||||
<!-- 爱山东jssdk 本sdk存在性能问题 -->
|
<!-- 爱山东jssdk 本sdk存在性能问题 -->
|
||||||
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
|
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
|
||||||
<!-- 只在内网有效 -->
|
<!-- 只在内网有效 -->
|
||||||
<script type="text/javascript" src="./static/js/SM.js"></script>
|
<script type="text/javascript" src="./static/js/SM.js"></script>
|
||||||
|
<script type="text/javascript" src="./static/js/pixi.min.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"><!--app-html--></div>
|
<div id="app"><!--app-html--></div>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
"locale": "zh-Hans",
|
"locale": "zh-Hans",
|
||||||
"h5": {
|
"h5": {
|
||||||
"router": {
|
"router": {
|
||||||
"base": "./",
|
"base": "/app/",
|
||||||
"mode": "hash"
|
"mode": "hash"
|
||||||
},
|
},
|
||||||
"title": "青岛智慧就业服务",
|
"title": "青岛智慧就业服务",
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const pageState = reactive({
|
|||||||
maxPage: 1,
|
maxPage: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const hasnext = ref(false);
|
const hasnext = ref(true);
|
||||||
|
|
||||||
const zphId = ref('');
|
const zphId = ref('');
|
||||||
const pageOptions = ref({});
|
const pageOptions = ref({});
|
||||||
|
|||||||
@@ -186,8 +186,7 @@ function uploadResume(tempFilePath, loading) {
|
|||||||
header['Authorization'] = encodeURIComponent(Authorization);
|
header['Authorization'] = encodeURIComponent(Authorization);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.uploadFile({
|
uni.uploadFile({
|
||||||
url: config.baseUrl + '/app/user/resume/recognition',
|
url: config.baseUrl + '/app/oss/uploadToObs',
|
||||||
// url: config.baseUrl + '/app/oss/uploadToObs',
|
|
||||||
filePath: tempFilePath,
|
filePath: tempFilePath,
|
||||||
name: 'file',
|
name: 'file',
|
||||||
header,
|
header,
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ const pages = reactive({
|
|||||||
year: 0,
|
year: 0,
|
||||||
month: 0,
|
month: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasZphDateArray = ref([]);
|
const hasZphDateArray = ref([]);
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
updateDateArray();
|
||||||
if (options.date) {
|
if (options.date) {
|
||||||
current.value = {
|
current.value = {
|
||||||
date: options?.date || null,
|
date: options?.date || null,
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</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>
|
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -268,9 +268,9 @@ import WaveDisplay from './WaveDisplay.vue';
|
|||||||
import FileIcon from './fileIcon.vue';
|
import FileIcon from './fileIcon.vue';
|
||||||
import FileText from './fileText.vue';
|
import FileText from './fileText.vue';
|
||||||
// 系统功能hook和阿里云hook
|
// 系统功能hook和阿里云hook
|
||||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder2.js';
|
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||||
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.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';
|
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
|
||||||
// 全局
|
// 全局
|
||||||
const { $api, navTo, throttle } = inject('globalFunction');
|
const { $api, navTo, throttle } = inject('globalFunction');
|
||||||
@@ -612,23 +612,17 @@ function userGoodFeedback(msg) {
|
|||||||
// $api.msg('该功能正在开发中,敬请期待后续更新!');
|
// $api.msg('该功能正在开发中,敬请期待后续更新!');
|
||||||
feeback.value?.open();
|
feeback.value?.open();
|
||||||
feebackData.value = msg;
|
feebackData.value = msg;
|
||||||
uni.hideTabBar()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmFeeBack(value) {
|
function confirmFeeBack(value) {
|
||||||
useChatGroupDBStore()
|
useChatGroupDBStore()
|
||||||
.badFeedback(feebackData.value, value)
|
.badFeedback(feebackData.value, value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
uni.showTabBar()
|
|
||||||
feeback.value?.close();
|
feeback.value?.close();
|
||||||
feeBackTips.value?.open();
|
feeBackTips.value?.open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function colseFeeBack() {
|
|
||||||
uni.showTabBar()
|
|
||||||
}
|
|
||||||
|
|
||||||
function readMarkdown(value, index) {
|
function readMarkdown(value, index) {
|
||||||
speechIndex.value = index;
|
speechIndex.value = index;
|
||||||
if (speechIndex.value !== index) {
|
if (speechIndex.value !== index) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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="feeback">
|
||||||
<view class="titile">反馈</view>
|
<view class="titile">反馈</view>
|
||||||
<view class="pop-h3">针对问题</view>
|
<view class="pop-h3">针对问题</view>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject } from 'vue';
|
import { ref, inject } from 'vue';
|
||||||
const emit = defineEmits(['onSend', 'onClose']);
|
const emit = defineEmits(['onSend']);
|
||||||
const { $api } = inject('globalFunction');
|
const { $api } = inject('globalFunction');
|
||||||
const popup = ref(null);
|
const popup = ref(null);
|
||||||
const inputText = ref('');
|
const inputText = ref('');
|
||||||
@@ -66,13 +66,6 @@ function close() {
|
|||||||
popup.value.close();
|
popup.value.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePopup(e) {
|
|
||||||
if (e.show) {
|
|
||||||
} else {
|
|
||||||
emit('onClose');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
const text = getLabel();
|
const text = getLabel();
|
||||||
if (text) {
|
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>
|
<select-filter ref="selectFilterModel"></select-filter>
|
||||||
|
|
||||||
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
|
<view class="maskFristEntry" v-if="maskFristEntry">
|
||||||
<view class="entry-content">
|
<view class="entry-content">
|
||||||
<text class="text1">左滑查看视频</text>
|
<text class="text1">左滑查看视频</text>
|
||||||
<text class="text2">左滑查看视频</text>
|
<text class="text2">左滑查看视频</text>
|
||||||
<view class="goExperience">去体验</view>
|
<view class="goExperience">去体验</view>
|
||||||
<view class="maskFristEntry-Close" @click="closeFristEntry">1</view>
|
<view class="maskFristEntry-Close" @click="closeFristEntry">1</view>
|
||||||
</view>
|
</view>
|
||||||
</view> -->
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ const waterfallsFlowRef = ref(null);
|
|||||||
const loadmoreRef = ref(null);
|
const loadmoreRef = ref(null);
|
||||||
const conditionSearch = ref({});
|
const conditionSearch = ref({});
|
||||||
const waterfallcolumn = ref(2);
|
const waterfallcolumn = ref(2);
|
||||||
const maskFristEntry = ref(false);
|
const maskFristEntry = ref(true);
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tabIndex: 'all',
|
tabIndex: 'all',
|
||||||
});
|
});
|
||||||
|
|||||||
1386
pages/index/components/index-refactor.vue
Normal file
@@ -35,7 +35,6 @@
|
|||||||
ref="waterfallsFlowRef"
|
ref="waterfallsFlowRef"
|
||||||
:column="columnCount"
|
:column="columnCount"
|
||||||
:columnSpace="columnSpace"
|
:columnSpace="columnSpace"
|
||||||
@loaded="imageloaded"
|
|
||||||
:value="list"
|
:value="list"
|
||||||
>
|
>
|
||||||
<template v-slot:default="job">
|
<template v-slot:default="job">
|
||||||
@@ -63,8 +62,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</custom-waterfalls-flow>
|
</custom-waterfalls-flow>
|
||||||
<empty v-if="!list.length"></empty>
|
<loadmore ref="loadmoreRef"></loadmore>
|
||||||
<loadmore v-if="list.length >= pageSize" ref="loadmoreRef"></loadmore>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
@@ -94,7 +92,7 @@ const state = reactive({
|
|||||||
// 响应式搜索条件(可以被修改)
|
// 响应式搜索条件(可以被修改)
|
||||||
const searchParams = ref({});
|
const searchParams = ref({});
|
||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
const { list, loading, refresh, loadMore } = usePagination(
|
const { list, loading, refresh, loadMore,finished } = usePagination(
|
||||||
(params) => $api.createRequest('/app/job/littleVideo', params),
|
(params) => $api.createRequest('/app/job/littleVideo', params),
|
||||||
dataToImg, // 转换函数
|
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() {
|
// function imageloaded() {
|
||||||
loadmoreRef.value?.change('more');
|
// nextTick(() => {
|
||||||
}
|
// console.log('触发',finished.value)
|
||||||
|
// if (finished.value) {
|
||||||
|
// loadmoreRef.value?.change('noMore')
|
||||||
|
// } else {
|
||||||
|
// loadmoreRef.value?.change('more')
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
const { columnCount, columnSpace } = useColumnCount(() => {
|
const { columnCount, columnSpace } = useColumnCount(() => {
|
||||||
pageSize.value = 10 * (columnCount.value - 1);
|
pageSize.value = 10 * (columnCount.value - 1);
|
||||||
@@ -190,7 +203,6 @@ defineExpose({ loadData });
|
|||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
.scroll-content{
|
.scroll-content{
|
||||||
padding: 24rpx
|
padding: 24rpx
|
||||||
height: calc(100% - 48rpx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-filter
|
.nav-filter
|
||||||
|
|||||||
@@ -60,11 +60,13 @@
|
|||||||
import { reactive, inject, watch, ref, onMounted } from 'vue';
|
import { reactive, inject, watch, ref, onMounted } from 'vue';
|
||||||
import Tabbar from '@/components/tabbar/midell-box.vue';
|
import Tabbar from '@/components/tabbar/midell-box.vue';
|
||||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
|
import IndexRefactor from './components/index-refactor.vue';
|
||||||
import IndexOne from './components/index-one.vue';
|
import IndexOne from './components/index-one.vue';
|
||||||
import IndexTwo from './components/index-two.vue';
|
import IndexTwo from './components/index-two.vue';
|
||||||
const loadedMap = reactive([false, false]);
|
const loadedMap = reactive([false, false]);
|
||||||
const swiperRefs = [ref(null), ref(null)];
|
const swiperRefs = [ref(null), ref(null)];
|
||||||
const components = [IndexOne, IndexTwo];
|
// const components = [IndexOne, IndexTwo];
|
||||||
|
const components = [IndexRefactor, IndexTwo];
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useReadMsg } from '@/stores/useReadMsg';
|
import { useReadMsg } from '@/stores/useReadMsg';
|
||||||
const { unreadCount } = storeToRefs(useReadMsg());
|
const { unreadCount } = storeToRefs(useReadMsg());
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ function complete() {
|
|||||||
.backdoor{
|
.backdoor{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 300rpx;
|
bottom: 200rpx;
|
||||||
}
|
}
|
||||||
.input-nx
|
.input-nx
|
||||||
position: relative
|
position: relative
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<scroll-view scroll-y class="main-scroll">
|
<scroll-view scroll-y class="main-scroll">
|
||||||
<view class="scrollmain">
|
<view v-if="msgList.length" class="scrollmain">
|
||||||
<view
|
<view
|
||||||
class="list-card press-button"
|
class="list-card press-button"
|
||||||
v-for="(item, index) in msgList"
|
v-for="(item, index) in msgList"
|
||||||
@@ -15,15 +15,14 @@
|
|||||||
></image>
|
></image>
|
||||||
<image
|
<image
|
||||||
class="card-img-flame"
|
class="card-img-flame"
|
||||||
v-else-if="item.title === '职位上新'"
|
v-if="item.title === '职位上新'"
|
||||||
src="/static/icon/msgtype2.png"
|
src="/static/icon/msgtype2.png"
|
||||||
></image>
|
></image>
|
||||||
<image
|
<image
|
||||||
class="card-img-flame"
|
class="card-img-flame"
|
||||||
v-else-if="item.title === '系统通知'"
|
v-if="item.title === '系统通知'"
|
||||||
src="/static/icon/msgtype3.png"
|
src="/static/icon/msgtype3.png"
|
||||||
></image>
|
></image>
|
||||||
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
|
|
||||||
<view class="subscript" v-if="item.notReadCount || !item.isRead">
|
<view class="subscript" v-if="item.notReadCount || !item.isRead">
|
||||||
{{ item.notReadCount || '' }}
|
{{ item.notReadCount || '' }}
|
||||||
</view>
|
</view>
|
||||||
@@ -38,6 +37,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<empty v-if="!msgList.length"></empty>
|
<empty v-if="!msgList.length"></empty>
|
||||||
</view>
|
</view>
|
||||||
|
<empty v-else pdTop="200" content="暂无消息~"></empty>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -72,9 +72,6 @@ function seeDetail(item, index) {
|
|||||||
case '系统通知':
|
case '系统通知':
|
||||||
navTo('/packageA/pages/systemNotification/systemNotification');
|
navTo('/packageA/pages/systemNotification/systemNotification');
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
useReadMsg().markAsRead(item, index);
|
|
||||||
navTo('/packageA/pages/newJobPosition/newJobPosition');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<scroll-view scroll-y class="main-scroll">
|
<scroll-view scroll-y class="main-scroll">
|
||||||
<view class="scrollmain">
|
<view v-if="unreadMsgList.length" class="scrollmain">
|
||||||
<view
|
<view
|
||||||
class="list-card press-button"
|
class="list-card press-button"
|
||||||
v-for="(item, index) in unreadMsgList"
|
v-for="(item, index) in unreadMsgList"
|
||||||
@@ -15,15 +15,14 @@
|
|||||||
></image>
|
></image>
|
||||||
<image
|
<image
|
||||||
class="card-img-flame"
|
class="card-img-flame"
|
||||||
v-else-if="item.title === '职位上新'"
|
v-if="item.title === '职位上新'"
|
||||||
src="/static/icon/msgtype2.png"
|
src="/static/icon/msgtype2.png"
|
||||||
></image>
|
></image>
|
||||||
<image
|
<image
|
||||||
class="card-img-flame"
|
class="card-img-flame"
|
||||||
v-else-if="item.title === '系统通知'"
|
v-if="item.title === '系统通知'"
|
||||||
src="/static/icon/msgtype3.png"
|
src="/static/icon/msgtype3.png"
|
||||||
></image>
|
></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 class="subscript" v-if="item.notReadCount">{{ item.notReadCount }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="card-info">
|
<view class="card-info">
|
||||||
@@ -36,6 +35,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<empty v-if="!unreadMsgList.length"></empty>
|
<empty v-if="!unreadMsgList.length"></empty>
|
||||||
</view>
|
</view>
|
||||||
|
<empty v-else pdTop="200" content="暂无消息~"></empty>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -59,19 +59,6 @@ async function loadData() {
|
|||||||
|
|
||||||
function seeDetail(item) {
|
function seeDetail(item) {
|
||||||
console.log(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 });
|
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
|
<view
|
||||||
v-for="(item, index) in data.column"
|
v-for="(item, index) in data.column"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="waterfalls-flow-column"
|
class="waterfalls-flow-column "
|
||||||
:id="`waterfalls_flow_column_${index + 1}`"
|
:id="`waterfalls_flow_column_${index + 1}`"
|
||||||
:msg="msg"
|
:msg="msg"
|
||||||
:style="{ width: w, 'margin-left': index == 0 ? 0 : m }"
|
:style="{ width: w, 'margin-left': index == 0 ? 0 : m }"
|
||||||
|
|||||||