UNPKG

@leelaa/vitepress-plugin-extended

Version:

VitePress 增强插件集合,提供多种高级功能和组件

258 lines (253 loc) 9.63 kB
import { defineComponent, ref, computed, watch, onUnmounted, createElementBlock, openBlock, renderSlot, createCommentVNode, createElementVNode, normalizeClass } from 'vue'; import { s as styleInject } from './style-inject.es-tgCJW-Cu.js'; const _hoisted_1 = { class: "text-to-speech" }; const _hoisted_2 = ["disabled"]; const _hoisted_3 = { key: 0 }; const _hoisted_4 = { key: 1 }; const _hoisted_5 = { key: 2 }; var script = /* @__PURE__ */ defineComponent({ __name: "index", props: { text: { type: String, required: true }, rate: { type: Number, required: false, default: 1 }, volume: { type: Number, required: false, default: 1 }, autoplay: { type: Boolean, required: false, default: false } }, setup(__props, { expose: __expose }) { const props = __props; const isPlaying = ref(false); const loading = ref(false); const currentTime = ref(0); const totalTime = ref(0); const rate = ref(props.rate); let currentUtterance = null; let startTime = 0; let pausedAt = 0; let timeInterval = null; const progress = computed(() => { if (totalTime.value === 0) return 0; return Math.min(currentTime.value / totalTime.value * 100, 100); }); const calculateTotalTime = (text, playbackRate) => { if (!text?.trim()) return 0; const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; const englishWords = (text.match(/[a-zA-Z]+/g) || []).length; const totalChars = chineseChars + englishWords; const baseCharsPerMinute = 200; const adjustedCharsPerMinute = baseCharsPerMinute * playbackRate; const estimatedMinutes = totalChars / adjustedCharsPerMinute; return Math.max(1, Math.ceil(estimatedMinutes * 60)); }; const play = async () => { if (!props.text?.trim()) return false; if (!speechSynthesis) { return; } try { loading.value = true; if (currentUtterance) { speechSynthesis.cancel(); } totalTime.value = calculateTotalTime(props.text, rate.value); currentUtterance = new SpeechSynthesisUtterance(props.text.trim()); currentUtterance.rate = rate.value; currentUtterance.volume = props.volume; currentUtterance.pitch = 1; const voices = speechSynthesis.getVoices(); const preferredVoice = voices.find( (voice) => voice.lang.includes("zh") || voice.lang.includes("cmn") ) || voices.find((voice) => voice.default); if (preferredVoice) { currentUtterance.voice = preferredVoice; } currentUtterance.onstart = () => { loading.value = false; isPlaying.value = true; startTime = Date.now() - pausedAt * 1e3; startTimeTracking(); }; currentUtterance.onend = () => { loading.value = false; isPlaying.value = false; currentTime.value = totalTime.value; stopTimeTracking(); pausedAt = 0; currentUtterance = null; }; currentUtterance.onerror = (event) => { loading.value = false; isPlaying.value = false; stopTimeTracking(); if (event.error !== "interrupted") { console.error("\u8BED\u97F3\u64AD\u653E\u9519\u8BEF:", event.error); } currentUtterance = null; }; speechSynthesis.speak(currentUtterance); return true; } catch (error) { loading.value = false; isPlaying.value = false; console.error("\u64AD\u653E\u5931\u8D25:", error); return false; } }; const pause = () => { if (!speechSynthesis) { return; } if (isPlaying.value && speechSynthesis.speaking) { speechSynthesis.pause(); isPlaying.value = false; pausedAt = currentTime.value; stopTimeTracking(); } }; const resume = () => { if (!speechSynthesis) { return; } if (!isPlaying.value && speechSynthesis.paused) { speechSynthesis.resume(); isPlaying.value = true; startTime = Date.now() - pausedAt * 1e3; startTimeTracking(); } }; const stop = () => { if (!speechSynthesis) { return; } speechSynthesis.cancel(); isPlaying.value = false; loading.value = false; currentTime.value = 0; pausedAt = 0; stopTimeTracking(); currentUtterance = null; }; const togglePlayPause = () => { if (!speechSynthesis) { return; } if (!props.text?.trim()) return; if (isPlaying.value) { pause(); } else if (speechSynthesis.paused && currentUtterance) { resume(); } else { play(); } }; const setRate = (newRate) => { if (newRate < 0.5 || newRate > 3) return; const wasPlaying = isPlaying.value; rate.value = newRate; totalTime.value = calculateTotalTime(props.text, newRate); if (wasPlaying) { stop(); setTimeout(() => { play(); }, 200); } }; const startTimeTracking = () => { stopTimeTracking(); timeInterval = window.setInterval(() => { if (isPlaying.value) { const elapsed = (Date.now() - startTime) / 1e3; currentTime.value = Math.min(elapsed, totalTime.value); } }, 100); }; const stopTimeTracking = () => { if (timeInterval) { clearInterval(timeInterval); timeInterval = null; } }; watch( () => props.text, (newText) => { stop(); if (newText?.trim()) { totalTime.value = calculateTotalTime(newText, rate.value); } else { totalTime.value = 0; } currentTime.value = 0; pausedAt = 0; }, { immediate: true } ); watch( () => props.rate, (newRate) => { if (newRate && newRate !== rate.value) { setRate(newRate); } }, { immediate: true } ); watch( () => props.autoplay, (shouldAutoplay) => { if (shouldAutoplay && props.text?.trim() && !isPlaying.value) { setTimeout(() => { play(); }, 500); } }, { immediate: true } ); onUnmounted(() => { stop(); }); __expose({ // 方法 play, pause, resume, stop, toggle: togglePlayPause, setRate, // 状态(响应式) isPlaying: computed(() => isPlaying.value), currentTime: computed(() => currentTime.value), totalTime: computed(() => totalTime.value), progress: computed(() => progress.value), rate: computed(() => rate.value), loading: computed(() => loading.value) }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", _hoisted_1, [ renderSlot(_ctx.$slots, "default", { isPlaying: isPlaying.value, currentTime: currentTime.value, totalTime: totalTime.value, progress: progress.value, play, pause, toggle: togglePlayPause, setRate, rate: rate.value, loading: loading.value }, () => [ createCommentVNode(" \u9ED8\u8BA4\u7684\u65B9\u5F62\u64AD\u653E\u6309\u94AE "), createElementVNode("button", { onClick: togglePlayPause, class: normalizeClass(["default-play-button", { playing: isPlaying.value }]), disabled: !_ctx.text || loading.value }, [ loading.value ? (openBlock(), createElementBlock("span", _hoisted_3, "\u23F3")) : !isPlaying.value ? (openBlock(), createElementBlock("span", _hoisted_4, "\u25B6\uFE0F")) : (openBlock(), createElementBlock("span", _hoisted_5, "\u23F8\uFE0F")) ], 10, _hoisted_2) ]) ]); }; } }); var css_248z = "\n.dark .content-area[data-v-d5229638] {\r\n background: #99999980 !important;\n}\n.dark .code-viewer[data-v-d5229638] {\r\n background: #99999980 !important;\n}\n.dark .control-button[data-v-d5229638]:hover {\r\n background: #cccccc80 !important;\n}\n.dark .toolbar[data-v-d5229638] {\r\n background: #1d1d1d !important;\n}\n.dark .control-button[data-v-d5229638]:hover {\r\n background: #cccccc80 !important;\n}\n.dark .control-group[data-v-d5229638] {\r\n background-color: #000 !important;\n}\n.text-to-speech[data-v-d5229638] {\r\n display: inline-block;\n}\n.default-play-button[data-v-d5229638] {\r\n width: 48px;\r\n height: 48px;\r\n border: 2px solid #e5e7eb;\r\n border-radius: 8px;\r\n background: #ffffff;\r\n color: #374151;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n font-size: 16px;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n.default-play-button[data-v-d5229638]:hover:not(:disabled) {\r\n background: #f9fafb;\r\n border-color: #d1d5db;\r\n transform: translateY(-1px);\r\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n.default-play-button[data-v-d5229638]:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n transform: none;\n}\n.default-play-button.playing[data-v-d5229638] {\r\n background: #3b82f6;\r\n border-color: #3b82f6;\r\n color: white;\n}\n.default-play-button.playing[data-v-d5229638]:hover {\r\n background: #2563eb;\r\n border-color: #2563eb;\n}\r\n"; styleInject(css_248z); script.__scopeId = "data-v-d5229638"; script.__file = "packages/ToSpeech/index.vue"; export { script as default };