@leelaa/vitepress-plugin-extended
Version:
VitePress 增强插件集合,提供多种高级功能和组件
258 lines (253 loc) • 9.63 kB
JavaScript
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 };