import { onMounted, onUnmounted, reactive, toRefs } from 'vue'; /** * 朗读API * @param musicContent 阅读内容 * @returns */ export const useSpeak = (musicContent?: string, selectionCouser?: string) => { const _musicContent = musicContent ? '#' + musicContent : '#musicContent'; const _selectionCouser = selectionCouser ? '#' + selectionCouser : '#selectionCouser'; 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; 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'); // 都为0的情况下判断为选中某一个段 if (selection.focusOffset === 0 && selection.anchorOffset === 0) { // 查找起始和结束节点的最近的元素父节点 const anchorNode = selection.anchorNode; const startParagraph = anchorNode.parentNode.parentElement; const firstNode = startParagraph.childNodes[0]; const endNode = startParagraph.childNodes[startParagraph.childNodes.length - 1]; sentences?.forEach((element: any, index: number) => { if (element === firstNode) { startIndex = index; anchorOffset = 0; } if (element === endNode) { endIndex = index; focusOffset = endNode.textContent.length; } }); } else { 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; } }); } 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) { // console.log(rects, 'rects'); const firstRect = rects[rects.length - 1]; const x = firstRect.right; 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.querySelector(_selectionCouser); const showDomRect = showDom?.getBoundingClientRect(); if (showDom) { // 判断 上边超出边界 showDom.style.top = ( y - parentRect?.top + (showDomRect.height + fHeight / 2) + musicContent?.scrollTop ).toFixed(2) + 'px'; if (parentRect?.right - x >= parentRect?.width - showDomRect.width) { showDom.style.right = (parentRect?.width - showDomRect.width).toFixed(2) + 'px'; showDom.style.left = 'auto'; } else { showDom.style.right = (parentRect?.right - x - 6).toFixed(2) + 'px'; showDom.style.left = 'auto'; } } // if (parentRect?.width - (x - parentRect?.left) > showDomRect.width) { // // 判断是否选择到最右边 超出边界 // // showDom.style.left = (x - parentRect?.right).toFixed(2) + 'px'; // // showDom.style.right = 'auto'; // showDom.style.right = (parentRect?.right - x - 6).toFixed(2) + 'px'; // showDom.style.left = 'auto'; // } else { // // showDom.style.right = '0px'; // // showDom.style.left = 'auto'; // showDom.style.right = 'auto'; // showDom.style.left = '0'; // } } } }; /** 开始朗读 */ 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; } // 清除所有高亮 function clearAllHighlights() { const textContainer: any = document.querySelector(_musicContent); const highlights = document.querySelectorAll('.showBgColor'); highlights.forEach(h => { if (h.classList.contains('speak-label')) { h.classList.toggle('showBgColor'); } else { const parent: any = h.parentNode; const textContent: any = h.textContent; parent.replaceChild(document.createTextNode(textContent), h); } }); // 合并相邻的文本节点 textContainer?.normalize(); } /** 关闭朗读 */ 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); }); clearAllHighlights(); 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'); // highlightText?.scrollIntoView({ // behavior: 'smooth', // block: 'center' // }); scrollToElement(highlightText); }; function getOffsetTopRelativeToParent(element: any, parent: any) { let offsetTop = 0; while (element && element !== parent) { offsetTop += element.offsetTop; element = element.offsetParent; } return offsetTop; } function scrollToElement(element: any) { const musicContent: any = document.querySelector(_musicContent); const musicRect = musicContent.getBoundingClientRect(); const diffTop = getOffsetTopRelativeToParent(element, musicContent); const musicHeight = musicRect.height / 2; let height = 0; // console.log(getOffsetTopRelativeToParent(element, musicContent), '12121'); if (diffTop - musicHeight >= 0) { height = diffTop - musicHeight; } else { height = 0; } document.querySelector(_musicContent)?.scrollTo({ top: height, behavior: 'smooth' }); } // 初始化高亮显示 const initLightText = () => { sentences?.forEach((sentence: any, i: number) => { if (i >= currentSentenceIndex && i <= end) { const text = sentence.textContent; if (currentSentenceIndex === end) { const range = document.createRange(); range.setStart(sentence.firstChild, options.anchorOffset || 0); range.setEnd( sentence.firstChild, options?.focusOffset || text.length ); const label = document.createElement('label'); label.classList.add('showBgColor'); range.surroundContents(label); } else { if (currentSentenceIndex === i) { const range = document.createRange(); range.setStart(sentence.firstChild, options.anchorOffset || 0); range.setEnd(sentence.firstChild, text.length); const label = document.createElement('label'); label.classList.add('showBgColor'); range.surroundContents(label); } else if (end === i) { const range = document.createRange(); range.setStart(sentence.firstChild, 0); range.setEnd( sentence.firstChild, options?.focusOffset || text.length ); const label = document.createElement('label'); label.classList.add('showBgColor'); range.surroundContents(label); } else { sentence.classList.add('showBgColor'); } } } // sentence.classList.toggle('highlight', i === index); }); }; initLightText(); // 开始播放 const speaker = () => { try { state.synth = window.speechSynthesis; // 如果当前正在播放,先暂停 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]); } }); } 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; } utterance.onstart = () => { state.isSpeak = true; highlightSentence(currentSentenceIndex); }; utterance.onend = () => { console.log('朗读结束'); currentSentenceIndex++; if (currentSentenceIndex <= end && state.isSpeak) { speaker(); // 继续下一个句子 } else { currentSentenceIndex = 0; // 结束后重置索引 highlightSentence(-1); // 清除高亮 clearAllHighlights(); 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'); } }; speaker(); }; const onDestory = () => { document.removeEventListener('mouseup', getSelectText); document.removeEventListener('touchend', getSelectText); onCloseSpeak(); }; onMounted(async () => { document.addEventListener('mouseup', getSelectText); document.addEventListener('touchend', getSelectText); }); onUnmounted(() => { onDestory(); }); return { ...toRefs(state), onAllSpeak, onTextStart, onDestory, onCloseSpeak, onTextReadOnly, processNode }; };