import { onMounted, onUnmounted, reactive, toRefs } from 'vue'; /** * 朗读API * @param musicContent 阅读内容 * @returns */ export const useSpeak = (musicContent?: string) => { const _musicContent = musicContent ? '#' + musicContent : '#musicContent'; const state = reactive({ showDom: false, synth: null as any, // 选中的索引 selectOptions: { startIndex: 0, anchorOffset: 0, // 开始段落的偏移值 endIndex: 0, focusOffset: 0 // 结束段落的偏移值 }, isSpeak: false // 是否在播放 }); // 函数:递归处理节点 const processNode = (node: any) => { const result = document.createDocumentFragment(); node.childNodes?.forEach((child: any) => { if (child.nodeType === Node.TEXT_NODE) { // 按标点符号分割文本 const sentences = child.textContent.split(/(?<=[,,;;。])\s*/); sentences?.forEach((sentence: any) => { if (sentence.trim()) { const customTag = document.createElement('label'); customTag.textContent = sentence.trim(); customTag.classList.add('speak-label'); result.appendChild(customTag); } }); } else if (child.nodeType === Node.ELEMENT_NODE) { const element = document.createElement(child.nodeName.toLowerCase()); // 复制原有的属性 Array.from(child.attributes).forEach((attr: any) => { element.setAttribute(attr.name, attr.value); }); // 递归处理子节点 const processedChildren = processNode(child); element.appendChild(processedChildren); result.appendChild(element); } }); return result; }; // 选中的方向 const checkSelectionDirection = (selection: any) => { if (selection.rangeCount > 0) { const anchorNode = selection.anchorNode; const anchorOffset = selection.anchorOffset; const focusNode = selection.focusNode; const focusOffset = selection.focusOffset; // 检查是否在同一节点内选择 if (anchorNode === focusNode) { if (anchorOffset < focusOffset) { return 'up'; } else { return 'down'; } } else { // 检查不同节点的选择情况 const range = selection.getRangeAt(0); const startContainer = range.startContainer; const endContainer = range.endContainer; if (startContainer === anchorNode && endContainer === focusNode) { return 'up'; } else { return 'down'; } } } else { return 'up'; } }; const getSelectText = () => { const selection: any = window.getSelection(); const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; // console.log(selection, 'selection'); if (selection.toString().length > 0) { state.showDom = true; const textContainer: any = document.querySelector(_musicContent); const sentences: any = textContainer?.querySelectorAll('label.speak-label'); let startIndex = 0, anchorOffset = 0, endIndex = 0, focusOffset = 0; console.log(selection, 'selection'); const firstNode = checkSelectionDirection(selection) === 'up' ? selection.anchorNode.parentNode : selection.focusNode.parentNode; const endNode = checkSelectionDirection(selection) === 'down' ? selection.anchorNode.parentNode : selection.focusNode.parentNode; if (checkSelectionDirection(selection) === 'up') { anchorOffset = selection.anchorOffset; focusOffset = selection.focusOffset; } else { anchorOffset = selection.focusOffset; focusOffset = selection.anchorOffset; } sentences?.forEach((element: any, index: number) => { if (element === firstNode) { startIndex = index; anchorOffset = checkSelectionDirection(selection) === 'up' ? selection.anchorOffset : selection.focusOffset; } if (element === endNode) { endIndex = index; focusOffset = checkSelectionDirection(selection) === 'down' ? selection.anchorOffset : selection.focusOffset; // focusOffset = selection.focusOffset; } }); state.selectOptions.startIndex = startIndex; state.selectOptions.anchorOffset = anchorOffset; state.selectOptions.endIndex = endIndex; state.selectOptions.focusOffset = focusOffset; } else { state.showDom = false; } // 判断选中的类型 setTimeout(() => { if (selection.type !== 'Range') { state.showDom = false; } }, 200); if (range && !selection.isCollapsed) { // 获取选中文本的坐标 // const rect = range.getBoundingClientRect(); const rects = range.getClientRects(); if (rects.length > 0) { const firstRect = rects[0]; const x = firstRect.left; const y = firstRect.top; const bottom = firstRect.bottom; const fHeight = firstRect.height; const musicContent: any = document.querySelector(_musicContent); const parentRect: any = musicContent?.getBoundingClientRect(); const showDom: any = document.getElementById('selectionCouser'); const showDomRect = showDom.getBoundingClientRect(); // 判断 上边超出边界 // if (y - parentRect?.top > showDomRect.height + fHeight / 2) { // showDom.style.top = // ( // y - // parentRect?.top - // (showDomRect.height + fHeight / 2) + // musicContent?.scrollTop // ).toFixed(2) + 'px'; // } else { // console.log( // false, // parentRect?.bottom - // bottom + // (showDomRect.height + fHeight / 2) + // musicContent?.scrollTop // ); showDom.style.top = ( y - parentRect?.top + (showDomRect.height + fHeight / 2) + musicContent?.scrollTop ).toFixed(2) + 'px'; // } // console.log({ // parentRectWidth: parentRect?.width, // firstRectLeft: x, // parentRectLeft: parentRect?.left, // parentRectStatus: // parentRect?.width - (x - parentRect?.left) > showDomRect.width, // diff: parentRect?.width - (x - parentRect?.left), // showDomRect: showDomRect.width // }); if (parentRect?.width - (x - parentRect?.left) > showDomRect.width) { // 判断是否选择到最右边 超出边界 showDom.style.left = (x - parentRect?.left).toFixed(2) + 'px'; showDom.style.right = 'auto'; } else { showDom.style.right = '0px'; showDom.style.left = 'auto'; } } } }; /** 开始朗读 */ const onTextStart = () => { onCloseSpeak(); onSpeak({ startIndex: state.selectOptions.startIndex, anchorOffset: state.selectOptions.anchorOffset }); }; /** 只读这段 */ const onTextReadOnly = () => { onCloseSpeak(); onSpeak(state.selectOptions); }; function clearSelection() { if (window.getSelection) { window.getSelection()?.removeAllRanges(); // 清除选中区域 } else if ((document as any).selection) { (document as any).selection.empty(); // 清除选中区域(IE 8 或更早) } state.showDom = false; // state.selectOptions.startIndex = 0; // state.selectOptions.anchorOffset = 0; // state.selectOptions.endIndex = 0; // state.selectOptions.focusOffset = 0; } /** 关闭朗读 */ const onCloseSpeak = () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // responsiveVoice.cancel(); state.synth?.cancel(); state.isSpeak = false; const textContainer: any = document.querySelector(_musicContent); const sentences: any = textContainer?.querySelectorAll('label.speak-label'); sentences?.forEach((sentence: any, i: number) => { sentence.classList.toggle('highlight', i === -1); }); clearSelection(); }; /** 全文朗读 */ const onAllSpeak = () => { clearSelection(); try { onSpeak({ startIndex: 0 }); } catch (e: any) { console.log(e, '12'); } }; // 开始播放 const onSpeak = (options: { startIndex: number; endIndex?: number; anchorOffset?: number; focusOffset?: number; }) => { const textContainer: any = document.querySelector(_musicContent); const sentences: any = textContainer?.querySelectorAll('label.speak-label'); // console.log(options, '--endIndex'); let currentSentenceIndex = options.startIndex || 0; const end = options.endIndex === undefined ? sentences.length - 1 : options.endIndex; // 高亮显示 const highlightSentence = (index: number) => { sentences?.forEach((sentence: any, i: number) => { sentence.classList.toggle('highlight', i === index); }); // 滚动到高亮的部分 const highlightText = textContainer?.querySelector('.highlight'); // console.log(highlightText, 'highlight'); highlightText?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; // 开始播放 const speaker = () => { try { state.synth = window.speechSynthesis; // 获取可用的 voice 列表 // const voices = speechSynthesis.getVoices(); // 选择一个特定的 voice // const voice = voices.find(voice => voice.lang === 'zh-CN'); // console.log(voice, 'voice'); // 如果当前正在播放,先暂停 if (state.synth.speaking) { state.synth.cancel(); // 取消当前播放 } let sentence = sentences[currentSentenceIndex].textContent; if (sentence.length <= 0) { console.error('暂无播放内容'); return; } // 判断是否为选中的内容播放 if ( options.startIndex === options.endIndex && options.endIndex !== undefined ) { sentence = sentence.substr( options.anchorOffset, (options.focusOffset || 0) - (options.anchorOffset || 0) ); } else { if (options.startIndex === currentSentenceIndex) { sentence = sentence.substr(options.anchorOffset, sentence.length); } if (options.endIndex === currentSentenceIndex) { sentence = sentence.substr(0, options.focusOffset); } } const replaceText = ['长笛', '曲']; const afterReplaceText = ['尝笛', '取']; if (sentence) { replaceText.forEach((item: string, index: number) => { if (sentence.includes(item)) { const regex = new RegExp(item, 'g'); sentence = sentence.replace(regex, afterReplaceText[index]); } }); } console.log(sentence, currentSentenceIndex, end, '---------'); const utterance = new SpeechSynthesisUtterance(sentence); utterance.lang = 'zh-CN'; utterance.volume = 1; utterance.rate = 0.8; // 语速 0.1到10 utterance.pitch = 1.5; // 范围从0(最小)到2(最大) // utterance.text = sentence; if (utterance) { utterance.onstart = null; utterance.onend = null; utterance.onerror = null; } // console.log(sentence, utterance); utterance.onstart = () => { state.isSpeak = true; highlightSentence(currentSentenceIndex); }; utterance.onend = () => { console.log('朗读结束'); currentSentenceIndex++; if (currentSentenceIndex <= end && state.isSpeak) { speaker(); // 继续下一个句子 } else { currentSentenceIndex = 0; // 结束后重置索引 highlightSentence(-1); // 清除高亮 state.isSpeak = false; } }; utterance.onerror = () => { currentSentenceIndex++; if (currentSentenceIndex <= end && state.isSpeak) { speaker(); // 继续下一个句子 } else { state.isSpeak = false; } }; setTimeout(() => { state.synth.speak(utterance); }, 80); } catch (e) { console.log(e, 'e'); } // 事件监听(如果需要) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // responsiveVoice.speak(sentence, 'Chinese Male', { // onstart: () => { // console.log('开始朗读'); // state.isSpeak = true; // highlightSentence(currentSentenceIndex); // }, // onend: () => { // console.log('朗读结束'); // currentSentenceIndex++; // if (currentSentenceIndex <= end && state.isSpeak) { // speaker(); // 继续下一个句子 // } else { // currentSentenceIndex = 0; // 结束后重置索引 // highlightSentence(-1); // 清除高亮 // state.isSpeak = false; // } // }, // onerror: (e: any) => { // console.error('朗读错误:', e); // currentSentenceIndex++; // if (currentSentenceIndex <= end && state.isSpeak) { // speaker(); // 继续下一个句子 // } else { // state.isSpeak = false; // } // } // }); }; speaker(); }; onMounted(async () => { document.addEventListener('mouseup', getSelectText); document.addEventListener('touchend', getSelectText); }); onUnmounted(() => { document.removeEventListener('mouseup', getSelectText); document.addEventListener('touchend', getSelectText); onCloseSpeak(); }); return { ...toRefs(state), onAllSpeak, onTextStart, onCloseSpeak, onTextReadOnly, processNode }; };