import { ref, onBeforeUnmount, onMounted } from 'vue' import { onHide, onUnload } from '@dcloudio/uni-app' export function useSpeechReader() { const isSpeaking = ref(false) const isPaused = ref(false) let utterance = null const cleanMarkdown = (text) => { return formatTextForSpeech(text) } const speak = (text, options = { lang: 'zh-CN', rate: 0.9, pitch: 1.2 }) => { cancelAudio() // 重置之前的 // const voices = speechSynthesis.getVoices() // const chineseVoices = voices.filter(v => v.lang.includes('zh')) const speechText = extractSpeechText(text); utterance = new SpeechSynthesisUtterance(speechText) // utterance.lang = options.lang || 'zh' utterance.rate = options.rate || 1 utterance.pitch = options.pitch || 1.1 // 音调(0 - 2,偏高比较柔和) utterance.onend = () => { isSpeaking.value = false isPaused.value = false } speechSynthesis.speak(utterance) isSpeaking.value = true isPaused.value = false } const pause = () => { if (isSpeaking.value && !isPaused.value) { speechSynthesis.pause() isPaused.value = true } } const resume = () => { if (isSpeaking.value && isPaused.value) { speechSynthesis.resume() isPaused.value = false } } const cancelAudio = () => { speechSynthesis.cancel() isSpeaking.value = false isPaused.value = false } // 页面刷新/关闭时 onMounted(() => { if (typeof window !== 'undefined') { window.addEventListener('beforeunload', cancelAudio) } }) onBeforeUnmount(() => { cancelAudio() if (typeof window !== 'undefined') { window.removeEventListener('beforeunload', cancelAudio) } }) onHide(cancelAudio) onUnload(cancelAudio) return { speak, pause, resume, cancelAudio, isSpeaking, isPaused, } } function extractSpeechText(markdown) { const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g; const jobs = []; let match; let lastJobEndIndex = 0; let firstJobStartIndex = -1; // 提取岗位 json 数据及前后位置 while ((match = jobRegex.exec(markdown)) !== null) { const jobStr = match[1]; try { const job = JSON.parse(jobStr); jobs.push(job); if (firstJobStartIndex === -1) { firstJobStartIndex = match.index; } lastJobEndIndex = jobRegex.lastIndex; } catch (e) { console.warn('JSON 解析失败', e); } } // 提取引导语(第一个 job-json 之前的文字) const guideText = firstJobStartIndex > 0 ? markdown.slice(0, firstJobStartIndex).trim() : ''; // 提取结束语(最后一个 job-json 之后的文字) const endingText = lastJobEndIndex < markdown.length ? markdown.slice(lastJobEndIndex).trim() : ''; // 岗位信息格式化为语音文本 const jobTexts = jobs.map((job, index) => { return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}。`; }); // 拼接总语音内容 const finalTextParts = []; if (guideText) finalTextParts.push(guideText); finalTextParts.push(...jobTexts); if (endingText) finalTextParts.push(endingText); return finalTextParts.join('\n'); }