123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- 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
- };
- };
|