mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 更新配置和组件以支持构建时间显示功能, 修复读弹幕的问题
- 在vite.config.mts中添加__BUILD_TIME__常量以记录构建时间 - 在AboutView.vue中显示构建时间及其相对时间 - 在ReadDanmaku.vue中重构语音合成状态管理,优化礼物合并逻辑 - 更新相关类型定义,增强代码可读性和可维护性
This commit is contained in:
2
src/auto-imports.d.ts
vendored
2
src/auto-imports.d.ts
vendored
@@ -553,6 +553,6 @@ declare global {
|
|||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -1 +1 @@
|
|||||||
|
declare const __BUILD_TIME__: string;
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTimelineItem, NTag, NIcon } from 'naive-ui'
|
import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTimelineItem, NTag, NIcon } from 'naive-ui'
|
||||||
import { h } from 'vue'
|
import { h, computed } from 'vue'
|
||||||
import { CodeOutline, ServerOutline, HeartOutline, LogoGithub } from '@vicons/ionicons5'
|
import { CodeOutline, ServerOutline, HeartOutline, LogoGithub } from '@vicons/ionicons5'
|
||||||
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
import { zhCN } from 'date-fns/locale'
|
||||||
|
|
||||||
|
// 获取编译时间
|
||||||
|
const buildTime = computed(() => {
|
||||||
|
try {
|
||||||
|
const buildDate = new Date(__BUILD_TIME__)
|
||||||
|
return {
|
||||||
|
date: buildDate.toLocaleString('zh-CN'),
|
||||||
|
relative: formatDistanceToNow(buildDate, { addSuffix: true, locale: zhCN })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
date: '未知',
|
||||||
|
relative: '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NLayoutContent style="height: 100vh; padding: 20px 0;">
|
<NLayoutContent style="height: 100vh; padding: 20px 0;">
|
||||||
@@ -19,6 +37,9 @@ import { CodeOutline, ServerOutline, HeartOutline, LogoGithub } from '@vicons/io
|
|||||||
<div style="font-size: 22px; font-weight: bold; padding: 8px 0;">
|
<div style="font-size: 22px; font-weight: bold; padding: 8px 0;">
|
||||||
关于
|
关于
|
||||||
</div>
|
</div>
|
||||||
|
<div style="font-size: 13px; color: #666; margin-top: 4px; padding-bottom: 8px;">
|
||||||
|
构建时间: {{ buildTime.date }} ({{ buildTime.relative }})
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<NText>
|
<NText>
|
||||||
一个兴趣使然的网站
|
一个兴趣使然的网站
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
NUl,
|
NUl,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { clearInterval, setInterval } from 'worker-timers'
|
import { clearInterval, setInterval } from 'worker-timers'
|
||||||
|
|
||||||
@@ -57,12 +57,12 @@ type SpeechSettings = {
|
|||||||
enterTemplate: string
|
enterTemplate: string
|
||||||
voiceType: 'local' | 'api'
|
voiceType: 'local' | 'api'
|
||||||
voiceAPISchemeType: 'http' | 'https'
|
voiceAPISchemeType: 'http' | 'https'
|
||||||
voiceAPI?: string
|
voiceAPI: string
|
||||||
splitText: boolean
|
splitText: boolean
|
||||||
useAPIDirectly: boolean
|
useAPIDirectly: boolean
|
||||||
|
combineGiftDelay: number | undefined
|
||||||
combineGiftDelay?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpeechInfo = {
|
type SpeechInfo = {
|
||||||
volume: number
|
volume: number
|
||||||
pitch: number
|
pitch: number
|
||||||
@@ -70,6 +70,22 @@ type SpeechInfo = {
|
|||||||
voice: string
|
voice: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SpeechState = {
|
||||||
|
isSpeaking: boolean
|
||||||
|
speakingText: string
|
||||||
|
isApiAudioLoading: boolean
|
||||||
|
apiAudioSrc: string
|
||||||
|
canSpeech: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const speechState = reactive<SpeechState>({
|
||||||
|
isSpeaking: false,
|
||||||
|
speakingText: '',
|
||||||
|
isApiAudioLoading: false,
|
||||||
|
apiAudioSrc: '',
|
||||||
|
canSpeech: false,
|
||||||
|
})
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -91,7 +107,6 @@ const settings = useStorage<SpeechSettings>('Setting.Speech', {
|
|||||||
voiceAPI: 'voice.vtsuru.live/voice/bert-vits2?text={{text}}&id=1&format=mp3&streaming=true',
|
voiceAPI: 'voice.vtsuru.live/voice/bert-vits2?text={{text}}&id=1&format=mp3&streaming=true',
|
||||||
useAPIDirectly: false,
|
useAPIDirectly: false,
|
||||||
splitText: false,
|
splitText: false,
|
||||||
|
|
||||||
combineGiftDelay: 2,
|
combineGiftDelay: 2,
|
||||||
})
|
})
|
||||||
const speechSynthesisInfo = ref<{
|
const speechSynthesisInfo = ref<{
|
||||||
@@ -121,16 +136,15 @@ const voiceOptions = computed(() => {
|
|||||||
.DistinctBy((v) => v.value)
|
.DistinctBy((v) => v.value)
|
||||||
.ToArray()
|
.ToArray()
|
||||||
})
|
})
|
||||||
const isSpeaking = ref(false)
|
|
||||||
const speakingText = ref('')
|
|
||||||
const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([])
|
const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([])
|
||||||
|
const giftCombineMap = new Map<string, number>() // 用于快速查找礼物合并项
|
||||||
|
const MAX_QUEUE_SIZE = 50 // 最大队列长度限制
|
||||||
const isVtsuruVoiceAPI = computed(() => {
|
const isVtsuruVoiceAPI = computed(() => {
|
||||||
return (
|
return (
|
||||||
settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live')
|
settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const canSpeech = ref(false)
|
|
||||||
const readedDanmaku = ref(0)
|
const readedDanmaku = ref(0)
|
||||||
|
|
||||||
const templateConstants = {
|
const templateConstants = {
|
||||||
@@ -175,6 +189,22 @@ const templateConstants = {
|
|||||||
regex: /\{\s*gift_name\s*\}/gi,
|
regex: /\{\s*gift_name\s*\}/gi,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 根据舰长等级数字返回对应的中文名称
|
||||||
|
* @param guardLevel 舰长等级
|
||||||
|
*/
|
||||||
|
function getGuardLevelName(guardLevel: number): string {
|
||||||
|
switch (guardLevel) {
|
||||||
|
case 1: return '总督'
|
||||||
|
case 2: return '提督'
|
||||||
|
case 3: return '舰长'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制朗读指定事件, 会中断当前朗读并插队到最前面
|
||||||
|
* @param data 事件数据
|
||||||
|
*/
|
||||||
function forceSpeak(data: EventModel) {
|
function forceSpeak(data: EventModel) {
|
||||||
cancelSpeech()
|
cancelSpeech()
|
||||||
|
|
||||||
@@ -188,21 +218,55 @@ function forceSpeak(data: EventModel) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
async function speak() {
|
async function speak() {
|
||||||
if (isSpeaking.value || speakQueue.value.length == 0) {
|
if (speechState.isSpeaking || speakQueue.value.length == 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = speakQueue.value[0]
|
|
||||||
if (
|
// 寻找可以立即播放的事件(优先非等待合并的礼物)
|
||||||
data.data.type == EventDataTypes.Gift &&
|
let targetIndex = -1
|
||||||
data.updateAt > Date.now() - (settings.value.combineGiftDelay ?? 0) * 1000
|
const now = Date.now()
|
||||||
) {
|
const combineDelay = (settings.value.combineGiftDelay ?? 0) * 1000
|
||||||
|
|
||||||
|
for (let i = 0; i < speakQueue.value.length; i++) {
|
||||||
|
const item = speakQueue.value[i]
|
||||||
|
|
||||||
|
// 如果是礼物且还在等待合并期间,跳过
|
||||||
|
if (item.data.type == EventDataTypes.Gift &&
|
||||||
|
combineDelay > 0 &&
|
||||||
|
item.updateAt > now - combineDelay) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到第一个可以播放的事件
|
||||||
|
targetIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到可播放的事件,直接返回
|
||||||
|
if (targetIndex === -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let text = getTextFromDanmaku(speakQueue.value.shift()?.data)
|
|
||||||
|
// 获取要播放的事件并从队列中移除
|
||||||
|
const targetItem = speakQueue.value.splice(targetIndex, 1)[0]
|
||||||
|
|
||||||
|
// 如果移除的不是礼物事件,需要更新礼物合并映射的索引
|
||||||
|
if (targetItem.data.type !== EventDataTypes.Gift) {
|
||||||
|
// 重建礼物合并映射,因为索引可能发生变化
|
||||||
|
giftCombineMap.clear()
|
||||||
|
speakQueue.value.forEach((item, index) => {
|
||||||
|
if (item.data.type === EventDataTypes.Gift) {
|
||||||
|
const giftKey = `${item.data.uid}-${item.data.msg}`
|
||||||
|
giftCombineMap.set(giftKey, index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = getTextFromDanmaku(targetItem.data)
|
||||||
if (text) {
|
if (text) {
|
||||||
isSpeaking.value = true
|
speechState.isSpeaking = true
|
||||||
readedDanmaku.value++
|
readedDanmaku.value++
|
||||||
speakingText.value = text
|
speechState.speakingText = text
|
||||||
if (checkTimer) {
|
if (checkTimer) {
|
||||||
clearInterval(checkTimer)
|
clearInterval(checkTimer)
|
||||||
}
|
}
|
||||||
@@ -216,7 +280,7 @@ async function speak() {
|
|||||||
text = settings.value.splitText ? insertSpaces(text) : text
|
text = settings.value.splitText ? insertSpaces(text) : text
|
||||||
speakFromAPI(text)
|
speakFromAPI(text)
|
||||||
}
|
}
|
||||||
console.log(`[TTS] 正在朗读: ${text}`)
|
console.log(`[TTS] 正在朗读: ${text} ${targetIndex > 0 ? '(跳过等待合并的礼物)' : ''}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function insertSpaces(sentence: string) {
|
function insertSpaces(sentence: string) {
|
||||||
@@ -234,6 +298,10 @@ function insertSpaces(sentence: string) {
|
|||||||
return sentence
|
return sentence
|
||||||
}
|
}
|
||||||
let checkTimer: number | undefined
|
let checkTimer: number | undefined
|
||||||
|
/**
|
||||||
|
* 使用浏览器本地语音合成功能朗读
|
||||||
|
* @param text 要朗读的文本
|
||||||
|
*/
|
||||||
function speakDirect(text: string) {
|
function speakDirect(text: string) {
|
||||||
try {
|
try {
|
||||||
const synth = window.speechSynthesis
|
const synth = window.speechSynthesis
|
||||||
@@ -266,109 +334,163 @@ function speakDirect(text: string) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
// 如果本地语音合成失败,确保清理定时器
|
||||||
|
cancelSpeech()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const apiAudio = ref<HTMLAudioElement>()
|
const apiAudio = ref<HTMLAudioElement>()
|
||||||
const isApiAudioLoading = ref(false)
|
|
||||||
const apiAudioSrc = ref('')
|
|
||||||
const splitter = new GraphemeSplitter()
|
const splitter = new GraphemeSplitter()
|
||||||
function speakFromAPI(text: string) {
|
|
||||||
|
/**
|
||||||
|
* 构建API请求URL
|
||||||
|
* @param text 要朗读的文本
|
||||||
|
*/
|
||||||
|
function buildApiUrl(text: string): string | null {
|
||||||
if (!settings.value.voiceAPI) {
|
if (!settings.value.voiceAPI) {
|
||||||
message.error('未设置语音API')
|
message.error('未设置语音API')
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
isSpeaking.value = true
|
const scheme =
|
||||||
isApiAudioLoading.value = true
|
settings.value.voiceAPISchemeType === 'https'
|
||||||
let url = `${settings.value.voiceAPISchemeType == 'https' ? 'https' : (settings.value.useAPIDirectly ? '' : FETCH_API) + 'http'}://${settings.value.voiceAPI
|
? 'https://'
|
||||||
.trim()
|
: settings.value.useAPIDirectly
|
||||||
.replace(/^(?:https?:\/\/)/, '')
|
? 'http://'
|
||||||
.replace(/\{\{\s*text\s*\}\}/, encodeURIComponent(text))}`
|
: `${FETCH_API}http://`
|
||||||
let tempURL: URL
|
|
||||||
|
const url = `${scheme}${settings.value.voiceAPI.trim().replace(/^(?:https?:\/\/)/, '')}`.replace(
|
||||||
|
/\{\{\s*text\s*\}\}/,
|
||||||
|
encodeURIComponent(text),
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tempURL = new URL(url)
|
const tempURL = new URL(url)
|
||||||
|
if (isVtsuruVoiceAPI.value) {
|
||||||
|
tempURL.searchParams.set('vtsuruId', accountInfo.value?.id.toString() ?? '-1')
|
||||||
|
if (splitter.countGraphemes(tempURL.searchParams.get('text') ?? '') > 100) {
|
||||||
|
message.error(`本站提供的测试接口字数不允许超过 100 字. 内容: [${tempURL.searchParams.get('text')}]`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tempURL.toString()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
message.error('无效的API地址: ' + url)
|
message.error('无效的API地址: ' + url)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从API获取语音并播放
|
||||||
|
* @param text 要朗读的文本
|
||||||
|
*/
|
||||||
|
function speakFromAPI(text: string) {
|
||||||
|
const url = buildApiUrl(text)
|
||||||
|
if (!url) {
|
||||||
cancelSpeech()
|
cancelSpeech()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isVtsuruVoiceAPI.value) {
|
|
||||||
tempURL.searchParams.set('vtsuruId', accountInfo.value?.id.toString() ?? '-1')
|
speechState.isSpeaking = true
|
||||||
url = tempURL.toString()
|
speechState.isApiAudioLoading = true
|
||||||
}
|
speechState.apiAudioSrc = url
|
||||||
if (isVtsuruVoiceAPI.value && splitter.countGraphemes(tempURL.searchParams.get('text') ?? '') > 50) {
|
|
||||||
message.error('本站提供的测试接口字数不允许超过 100 字. 内容: [' + tempURL.searchParams.get('text') + ']')
|
|
||||||
cancelSpeech()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
apiAudioSrc.value = url
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
//apiAudio.value?.load()
|
if (apiAudio.value) {
|
||||||
apiAudio.value?.play().catch((err) => {
|
// 设置预加载
|
||||||
|
apiAudio.value.preload = 'auto'
|
||||||
|
|
||||||
|
// 播放音频
|
||||||
|
const playPromise = apiAudio.value.play()
|
||||||
|
if (playPromise) {
|
||||||
|
playPromise.catch((err) => {
|
||||||
if (err.toString().startsWith('AbortError')) {
|
if (err.toString().startsWith('AbortError')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log(err)
|
console.error('[speakFromAPI] 音频播放失败:', err)
|
||||||
console.log(err)
|
message.error('无法播放语音: ' + err.message)
|
||||||
message.error('无法播放语音:' + err)
|
|
||||||
cancelSpeech()
|
cancelSpeech()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function onAPIError(e: Event) {
|
function onAPIError(e: Event) {
|
||||||
if (!apiAudioSrc.value) return
|
if (!speechState.apiAudioSrc) return
|
||||||
|
message.error('音频加载失败, 请检查API是否可用以及网络连接')
|
||||||
cancelSpeech()
|
cancelSpeech()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 取消/停止当前所有朗读
|
||||||
|
*/
|
||||||
function cancelSpeech() {
|
function cancelSpeech() {
|
||||||
isSpeaking.value = false
|
speechState.isSpeaking = false
|
||||||
if (checkTimer) {
|
if (checkTimer) {
|
||||||
clearInterval(checkTimer)
|
clearInterval(checkTimer)
|
||||||
checkTimer = undefined
|
checkTimer = undefined
|
||||||
}
|
}
|
||||||
isApiAudioLoading.value = false
|
speechState.isApiAudioLoading = false
|
||||||
pauseAPI()
|
pauseAPI()
|
||||||
EasySpeech.cancel()
|
EasySpeech.cancel()
|
||||||
speakingText.value = ''
|
speechState.speakingText = ''
|
||||||
}
|
}
|
||||||
function pauseAPI() {
|
function pauseAPI() {
|
||||||
if (!apiAudio.value?.paused) {
|
if (apiAudio.value && !apiAudio.value.paused) {
|
||||||
apiAudio.value?.pause()
|
apiAudio.value.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 接收到事件, 添加到朗读队列
|
||||||
|
* @param data 事件数据
|
||||||
|
*/
|
||||||
function onGetEvent(data: EventModel) {
|
function onGetEvent(data: EventModel) {
|
||||||
if (!canSpeech.value) {
|
if (!speechState.canSpeech) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (data.type == EventDataTypes.Message && (data.emoji || /^(?:\[\w+\])+$/.test(data.msg))) {
|
if (data.type == EventDataTypes.Message && (data.emoji || /^(?:\[\w+\])+$/.test(data.msg))) {
|
||||||
// 不支持表情
|
// 不朗读纯表情/图片弹幕
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (data.type == EventDataTypes.Enter && !settings.value.enterTemplate) {
|
if (data.type == EventDataTypes.Enter && !settings.value.enterTemplate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (data.type == EventDataTypes.Gift) {
|
|
||||||
const exist = speakQueue.value.find(
|
// 礼物合并逻辑 - 使用Map优化性能
|
||||||
(v) =>
|
if (data.type == EventDataTypes.Gift && settings.value.combineGiftDelay) {
|
||||||
v.data.type == EventDataTypes.Gift &&
|
const giftKey = `${data.uid}-${data.msg}` // 用户ID和礼物名称组成的唯一键
|
||||||
v.data.uid == data.uid &&
|
const existIndex = giftCombineMap.get(giftKey)
|
||||||
v.data.msg == data.msg &&
|
|
||||||
v.updateAt > Date.now() - (settings.value.combineGiftDelay ?? 0) * 1000,
|
if (existIndex !== undefined && existIndex < speakQueue.value.length) {
|
||||||
)
|
const exist = speakQueue.value[existIndex]
|
||||||
if (exist) {
|
if (exist &&
|
||||||
|
exist.data.type == EventDataTypes.Gift &&
|
||||||
|
exist.updateAt > Date.now() - (settings.value.combineGiftDelay * 1000)) {
|
||||||
|
// 更新现有礼物数据
|
||||||
exist.updateAt = Date.now()
|
exist.updateAt = Date.now()
|
||||||
exist.data.num += data.num
|
exist.data.num += data.num
|
||||||
exist.data.price += data.price
|
exist.data.price += data.price
|
||||||
exist.combineCount ??= 0
|
exist.combineCount = (exist.combineCount ?? 0) + data.num
|
||||||
exist.combineCount += data.num
|
|
||||||
console.log(
|
console.log(
|
||||||
`[TTS] ${data.uname} 增加已存在礼物数量: ${data.msg} [${exist.data.num - data.num} => ${exist.data.num}]`,
|
`[TTS] ${data.uname} 增加已存在礼物数量: ${data.msg} [${exist.data.num - data.num} => ${exist.data.num}]`,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加新的礼物到队列
|
||||||
|
const newIndex = speakQueue.value.length
|
||||||
|
giftCombineMap.set(giftKey, newIndex)
|
||||||
|
|
||||||
|
// 清理过期的Map条目
|
||||||
|
setTimeout(() => {
|
||||||
|
if (giftCombineMap.get(giftKey) === newIndex) {
|
||||||
|
giftCombineMap.delete(giftKey)
|
||||||
|
}
|
||||||
|
}, (settings.value.combineGiftDelay + 1) * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
speakQueue.value.push({
|
speakQueue.value.push({
|
||||||
data,
|
data,
|
||||||
updateAt: data.type == EventDataTypes.Gift ? Date.now() : 0,
|
updateAt: data.type == EventDataTypes.Gift ? Date.now() : 0, // 礼物事件记录时间用于合并
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function getTextFromDanmaku(data: EventModel | undefined) {
|
function getTextFromDanmaku(data: EventModel | undefined) {
|
||||||
@@ -418,13 +540,13 @@ function getTextFromDanmaku(data: EventModel | undefined) {
|
|||||||
.replace(templateConstants.message.regex, data.msg)
|
.replace(templateConstants.message.regex, data.msg)
|
||||||
.replace(
|
.replace(
|
||||||
templateConstants.guard_level.regex,
|
templateConstants.guard_level.regex,
|
||||||
data.guard_level == 1 ? '总督' : data.guard_level == 2 ? '提督' : data.guard_level == 3 ? '舰长' : '',
|
getGuardLevelName(data.guard_level),
|
||||||
)
|
)
|
||||||
.replace(templateConstants.fans_medal_level.regex, data.fans_medal_level.toString())
|
.replace(templateConstants.fans_medal_level.regex, data.fans_medal_level.toString())
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
if (data.type === EventDataTypes.Message) {
|
if (data.type === EventDataTypes.Message) {
|
||||||
text = text.replace(/\[.*?\]/g, ' ') //删除表情
|
text = text.replace(/\[.*?\]/g, ' ') //删除 [表情], B站的表情是 [生气了] 这样的格式
|
||||||
} else if (data.type === EventDataTypes.Gift) {
|
} else if (data.type === EventDataTypes.Gift) {
|
||||||
text = text.replace(templateConstants.gift_name.regex, data.msg)
|
text = text.replace(templateConstants.gift_name.regex, data.msg)
|
||||||
} else if (data.type === EventDataTypes.Guard) {
|
} else if (data.type === EventDataTypes.Guard) {
|
||||||
@@ -432,7 +554,7 @@ function getTextFromDanmaku(data: EventModel | undefined) {
|
|||||||
}
|
}
|
||||||
text = fullWidthToHalfWidth(text)
|
text = fullWidthToHalfWidth(text)
|
||||||
.replace(/[^0-9a-zA-Z\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF ,.:'"\s]/gi, '')
|
.replace(/[^0-9a-zA-Z\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF ,.:'"\s]/gi, '')
|
||||||
.normalize('NFKC') //过滤无效字符, 全角转半角
|
.normalize('NFKC')
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
function fullWidthToHalfWidth(str: string) {
|
function fullWidthToHalfWidth(str: string) {
|
||||||
@@ -447,22 +569,28 @@ function fullWidthToHalfWidth(str: string) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
function startSpeech() {
|
function startSpeech() {
|
||||||
canSpeech.value = true
|
speechState.canSpeech = true
|
||||||
message.success('服务已启动')
|
message.success('服务已启动')
|
||||||
}
|
}
|
||||||
function stopSpeech() {
|
function stopSpeech() {
|
||||||
canSpeech.value = false
|
speechState.canSpeech = false
|
||||||
message.success('已停止监听')
|
message.success('已停止监听')
|
||||||
}
|
}
|
||||||
async function uploadConfig() {
|
async function uploadConfig() {
|
||||||
|
try {
|
||||||
const result = await UploadConfig('Speech', settings.value)
|
const result = await UploadConfig('Speech', settings.value)
|
||||||
if (result) {
|
if (result) {
|
||||||
message.success('已保存至服务器')
|
message.success('已保存至服务器')
|
||||||
} else {
|
} else {
|
||||||
message.error('保存失败')
|
message.error('保存失败')
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[uploadConfig] 上传配置失败:', error)
|
||||||
|
message.error('保存失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function downloadConfig() {
|
async function downloadConfig() {
|
||||||
|
try {
|
||||||
const result = await DownloadConfig<SpeechSettings>('Speech')
|
const result = await DownloadConfig<SpeechSettings>('Speech')
|
||||||
if (result.status === 'success' && result.data) {
|
if (result.status === 'success' && result.data) {
|
||||||
settings.value = result.data
|
settings.value = result.data
|
||||||
@@ -472,31 +600,20 @@ async function downloadConfig() {
|
|||||||
} else {
|
} else {
|
||||||
message.error('获取失败: ' + result.msg)
|
message.error('获取失败: ' + result.msg)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[downloadConfig] 下载配置失败:', error)
|
||||||
|
message.error('获取失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||||
}
|
}
|
||||||
function test(type: EventDataTypes) {
|
}
|
||||||
switch (type) {
|
|
||||||
case EventDataTypes.Message:
|
/**
|
||||||
forceSpeak({
|
* 创建测试事件数据
|
||||||
type: EventDataTypes.Message,
|
* @param type 事件类型
|
||||||
uname: accountInfo.value?.name ?? '测试用户',
|
* @param overrides 覆盖默认值的对象
|
||||||
uid: accountInfo.value?.biliId ?? 0,
|
*/
|
||||||
msg: '测试弹幕',
|
function createTestEventData(type: EventDataTypes, overrides: Partial<EventModel>): EventModel {
|
||||||
price: 0,
|
const baseData = {
|
||||||
num: 0,
|
type,
|
||||||
time: Date.now(),
|
|
||||||
guard_level: 0,
|
|
||||||
fans_medal_level: 1,
|
|
||||||
fans_medal_name: '',
|
|
||||||
fans_medal_wearing_status: false,
|
|
||||||
emoji: undefined,
|
|
||||||
uface: '',
|
|
||||||
open_id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
ouid: '00000000-0000-0000-0000-000000000000',
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case EventDataTypes.Enter:
|
|
||||||
forceSpeak({
|
|
||||||
type: EventDataTypes.Enter,
|
|
||||||
uname: accountInfo.value?.name ?? '测试用户',
|
uname: accountInfo.value?.name ?? '测试用户',
|
||||||
uid: accountInfo.value?.biliId ?? 0,
|
uid: accountInfo.value?.biliId ?? 0,
|
||||||
msg: '',
|
msg: '',
|
||||||
@@ -511,75 +628,122 @@ function test(type: EventDataTypes) {
|
|||||||
uface: '',
|
uface: '',
|
||||||
open_id: '00000000-0000-0000-0000-000000000000',
|
open_id: '00000000-0000-0000-0000-000000000000',
|
||||||
ouid: '00000000-0000-0000-0000-000000000000',
|
ouid: '00000000-0000-0000-0000-000000000000',
|
||||||
})
|
}
|
||||||
|
return { ...baseData, ...overrides }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试不同类型的事件
|
||||||
|
* @param type 事件类型
|
||||||
|
*/
|
||||||
|
function test(type: EventDataTypes) {
|
||||||
|
let testData: EventModel
|
||||||
|
switch (type) {
|
||||||
|
case EventDataTypes.Message:
|
||||||
|
testData = createTestEventData(EventDataTypes.Message, { msg: '测试弹幕' })
|
||||||
|
break
|
||||||
|
case EventDataTypes.Enter:
|
||||||
|
testData = createTestEventData(EventDataTypes.Enter, {})
|
||||||
break
|
break
|
||||||
case EventDataTypes.SC:
|
case EventDataTypes.SC:
|
||||||
forceSpeak({
|
testData = createTestEventData(EventDataTypes.SC, { msg: '测试留言', price: 30, num: 1 })
|
||||||
type: EventDataTypes.SC,
|
|
||||||
uname: accountInfo.value?.name ?? '测试用户',
|
|
||||||
uid: accountInfo.value?.biliId ?? 0,
|
|
||||||
msg: '测试留言',
|
|
||||||
price: 30,
|
|
||||||
num: 1,
|
|
||||||
time: Date.now(),
|
|
||||||
guard_level: 0,
|
|
||||||
fans_medal_level: 1,
|
|
||||||
fans_medal_name: '',
|
|
||||||
fans_medal_wearing_status: false,
|
|
||||||
emoji: undefined,
|
|
||||||
uface: '',
|
|
||||||
open_id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
ouid: '00000000-0000-0000-0000-000000000000',
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case EventDataTypes.Guard:
|
case EventDataTypes.Guard:
|
||||||
forceSpeak({
|
testData = createTestEventData(EventDataTypes.Guard, { msg: '舰长', num: 1, guard_level: 3 })
|
||||||
type: EventDataTypes.Guard,
|
|
||||||
uname: accountInfo.value?.name ?? '测试用户',
|
|
||||||
uid: accountInfo.value?.biliId ?? 0,
|
|
||||||
msg: '舰长',
|
|
||||||
price: 0,
|
|
||||||
num: 1,
|
|
||||||
time: Date.now(),
|
|
||||||
guard_level: 3,
|
|
||||||
fans_medal_level: 1,
|
|
||||||
fans_medal_name: '',
|
|
||||||
fans_medal_wearing_status: false,
|
|
||||||
emoji: undefined,
|
|
||||||
uface: '',
|
|
||||||
open_id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
ouid: '00000000-0000-0000-0000-000000000000',
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case EventDataTypes.Gift:
|
case EventDataTypes.Gift:
|
||||||
forceSpeak({
|
testData = createTestEventData(EventDataTypes.Gift, { msg: '测试礼物', price: 5, num: 5 })
|
||||||
type: EventDataTypes.Gift,
|
|
||||||
uname: accountInfo.value?.name ?? '测试用户',
|
|
||||||
uid: accountInfo.value?.biliId ?? 0,
|
|
||||||
msg: '测试礼物',
|
|
||||||
price: 5,
|
|
||||||
num: 5,
|
|
||||||
time: Date.now(),
|
|
||||||
guard_level: 0,
|
|
||||||
fans_medal_level: 1,
|
|
||||||
fans_medal_name: '',
|
|
||||||
fans_medal_wearing_status: false,
|
|
||||||
emoji: undefined,
|
|
||||||
uface: '',
|
|
||||||
open_id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
ouid: '00000000-0000-0000-0000-000000000000',
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在监听,加入队列;否则直接播放
|
||||||
|
if (speechState.canSpeech) {
|
||||||
|
onGetEvent(testData)
|
||||||
|
} else {
|
||||||
|
forceSpeak(testData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testAPI() {
|
function testAPI() {
|
||||||
|
// 直接测试API,不受监听状态影响
|
||||||
speakFromAPI('这是一条测试弹幕')
|
speakFromAPI('这是一条测试弹幕')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期或无效的队列项
|
||||||
|
*/
|
||||||
|
function cleanupQueue() {
|
||||||
|
const now = Date.now()
|
||||||
|
const validItems: typeof speakQueue.value = []
|
||||||
|
|
||||||
|
speakQueue.value.forEach((item, index) => {
|
||||||
|
// 保留非礼物事件或未过期的礼物事件
|
||||||
|
if (item.data.type !== EventDataTypes.Gift ||
|
||||||
|
item.updateAt > now - (settings.value.combineGiftDelay ?? 0) * 1000) {
|
||||||
|
validItems.push(item)
|
||||||
|
} else {
|
||||||
|
// 从Map中移除过期的礼物项
|
||||||
|
const giftKey = `${item.data.uid}-${item.data.msg}`
|
||||||
|
if (giftCombineMap.get(giftKey) === index) {
|
||||||
|
giftCombineMap.delete(giftKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果队列过长,保留最新的项目
|
||||||
|
if (validItems.length > MAX_QUEUE_SIZE) {
|
||||||
|
const removedItems = validItems.splice(0, validItems.length - MAX_QUEUE_SIZE)
|
||||||
|
console.warn(`[TTS] 队列过长,已移除 ${removedItems.length} 个旧项目`)
|
||||||
|
}
|
||||||
|
|
||||||
|
speakQueue.value = validItems
|
||||||
|
|
||||||
|
// 重建Map索引
|
||||||
|
giftCombineMap.clear()
|
||||||
|
speakQueue.value.forEach((item, index) => {
|
||||||
|
if (item.data.type === EventDataTypes.Gift) {
|
||||||
|
const giftKey = `${item.data.uid}-${item.data.msg}`
|
||||||
|
giftCombineMap.set(giftKey, index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let speechQueueTimer: number
|
let speechQueueTimer: number
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
EasySpeech.init({ maxTimeout: 5000, interval: 250 })
|
EasySpeech.init({ maxTimeout: 5000, interval: 250 })
|
||||||
speechSynthesisInfo.value = EasySpeech.detect()
|
speechSynthesisInfo.value = EasySpeech.detect()
|
||||||
|
|
||||||
|
// 自动选择默认语音
|
||||||
|
const checkAndSetDefaultVoice = () => {
|
||||||
|
const voices = EasySpeech.voices()
|
||||||
|
if (voices.length > 0 && !settings.value.speechInfo.voice) {
|
||||||
|
// 优先选择中文语音
|
||||||
|
const chineseVoice = voices.find(v => v.lang.startsWith('zh'))
|
||||||
|
settings.value.speechInfo.voice = chineseVoice?.name || voices[0].name
|
||||||
|
console.log(`[TTS] 自动选择默认语音: ${settings.value.speechInfo.voice}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即检查一次
|
||||||
|
checkAndSetDefaultVoice()
|
||||||
|
|
||||||
|
// 如果语音列表还没加载完成,等待加载完成后再检查
|
||||||
|
if (EasySpeech.voices().length === 0) {
|
||||||
|
const voiceCheckTimer = setInterval(() => {
|
||||||
|
if (EasySpeech.voices().length > 0) {
|
||||||
|
checkAndSetDefaultVoice()
|
||||||
|
clearInterval(voiceCheckTimer)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
// 10秒后停止检查避免无限循环
|
||||||
|
setTimeout(() => clearInterval(voiceCheckTimer), 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 语音播放队列处理
|
||||||
speechQueueTimer = setInterval(() => {
|
speechQueueTimer = setInterval(() => {
|
||||||
speak()
|
speak()
|
||||||
}, 250)
|
}, 250)
|
||||||
@@ -591,12 +755,29 @@ onMounted(() => {
|
|||||||
client.onEvent('enter', onGetEvent)
|
client.onEvent('enter', onGetEvent)
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
// 清理队列定时器
|
||||||
|
if (speechQueueTimer) {
|
||||||
clearInterval(speechQueueTimer)
|
clearInterval(speechQueueTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理语音超时定时器
|
||||||
|
if (checkTimer) {
|
||||||
|
clearInterval(checkTimer)
|
||||||
|
checkTimer = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止当前语音播放
|
||||||
|
cancelSpeech()
|
||||||
|
|
||||||
|
// 清理事件监听器
|
||||||
client.offEvent('danmaku', onGetEvent)
|
client.offEvent('danmaku', onGetEvent)
|
||||||
client.offEvent('sc', onGetEvent)
|
client.offEvent('sc', onGetEvent)
|
||||||
client.offEvent('guard', onGetEvent)
|
client.offEvent('guard', onGetEvent)
|
||||||
client.offEvent('gift', onGetEvent)
|
client.offEvent('gift', onGetEvent)
|
||||||
client.offEvent('enter', onGetEvent)
|
client.offEvent('enter', onGetEvent)
|
||||||
|
|
||||||
|
// 清理礼物合并映射
|
||||||
|
giftCombineMap.clear()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -662,13 +843,13 @@ onUnmounted(() => {
|
|||||||
<br>
|
<br>
|
||||||
<NSpace align="center">
|
<NSpace align="center">
|
||||||
<NButton
|
<NButton
|
||||||
:type="canSpeech ? 'error' : 'primary'"
|
:type="speechState.canSpeech ? 'error' : 'primary'"
|
||||||
data-umami-event="Use TTS"
|
data-umami-event="Use TTS"
|
||||||
:data-umami-event-uid="accountInfo?.id"
|
:data-umami-event-uid="accountInfo?.id"
|
||||||
size="large"
|
size="large"
|
||||||
@click="canSpeech ? stopSpeech() : startSpeech()"
|
@click="speechState.canSpeech ? stopSpeech() : startSpeech()"
|
||||||
>
|
>
|
||||||
{{ canSpeech ? '停止监听' : '开始监听' }}
|
{{ speechState.canSpeech ? '停止监听' : '开始监听' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -693,13 +874,13 @@ onUnmounted(() => {
|
|||||||
这将覆盖当前设置, 确定?
|
这将覆盖当前设置, 确定?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<template v-if="canSpeech">
|
<template v-if="speechState.canSpeech">
|
||||||
<NDivider> 状态 </NDivider>
|
<NDivider> 状态 </NDivider>
|
||||||
<NSpace
|
<NSpace
|
||||||
vertical
|
vertical
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<NTooltip v-if="settings.voiceType == 'api' && isApiAudioLoading">
|
<NTooltip v-if="settings.voiceType == 'api' && speechState.isApiAudioLoading">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton
|
<NButton
|
||||||
circle
|
circle
|
||||||
@@ -716,19 +897,19 @@ onUnmounted(() => {
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton
|
<NButton
|
||||||
circle
|
circle
|
||||||
:disabled="!isSpeaking"
|
:disabled="!speechState.isSpeaking"
|
||||||
:style="`animation: ${isSpeaking ? 'animated-border 2.5s infinite;' : ''}`"
|
:style="`animation: ${speechState.isSpeaking ? 'animated-border 2.5s infinite;' : ''}`"
|
||||||
@click="cancelSpeech"
|
@click="cancelSpeech"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon
|
<NIcon
|
||||||
:component="Mic24Filled"
|
:component="Mic24Filled"
|
||||||
:color="isSpeaking ? 'green' : 'gray'"
|
:color="speechState.isSpeaking ? 'green' : 'gray'"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
{{ isSpeaking ? '取消朗读' : '未朗读' }}
|
{{ speechState.isSpeaking ? `正在朗读: ${speechState.speakingText}` : '未朗读' }}
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
<NText depth="3">
|
<NText depth="3">
|
||||||
队列: {{ speakQueue.length }}
|
队列: {{ speakQueue.length }}
|
||||||
@@ -854,7 +1035,10 @@ onUnmounted(() => {
|
|||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="settings.speechInfo.voice"
|
v-model:value="settings.speechInfo.voice"
|
||||||
:options="voiceOptions"
|
:options="voiceOptions"
|
||||||
:fallback-option="() => ({ label: '未选择, 将使用默认语音', value: '' })"
|
:fallback-option="() => ({
|
||||||
|
label: settings.speechInfo.voice ? `已选择: ${settings.speechInfo.voice}` : '未选择, 将使用默认语音',
|
||||||
|
value: settings.speechInfo.voice || ''
|
||||||
|
})"
|
||||||
/>
|
/>
|
||||||
<span style="width: 100%">
|
<span style="width: 100%">
|
||||||
<NText> 音量 </NText>
|
<NText> 音量 </NText>
|
||||||
@@ -976,7 +1160,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
:loading="isApiAudioLoading"
|
:loading="speechState.isApiAudioLoading"
|
||||||
@click="testAPI"
|
@click="testAPI"
|
||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
@@ -1013,10 +1197,10 @@ onUnmounted(() => {
|
|||||||
</NSpace>
|
</NSpace>
|
||||||
<audio
|
<audio
|
||||||
ref="apiAudio"
|
ref="apiAudio"
|
||||||
:src="apiAudioSrc"
|
:src="speechState.apiAudioSrc"
|
||||||
:volume="settings.speechInfo.volume"
|
:volume="settings.speechInfo.volume"
|
||||||
@ended="cancelSpeech"
|
@ended="cancelSpeech"
|
||||||
@canplay="isApiAudioLoading = false"
|
@canplay="speechState.isApiAudioLoading = false"
|
||||||
@error="onAPIError"
|
@error="onAPIError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1052,7 +1236,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
:loading="isApiAudioLoading"
|
:loading="speechState.isApiAudioLoading"
|
||||||
@click="test(EventDataTypes.Message)"
|
@click="test(EventDataTypes.Message)"
|
||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
@@ -1066,7 +1250,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
:loading="isApiAudioLoading"
|
:loading="speechState.isApiAudioLoading"
|
||||||
@click="test(EventDataTypes.Gift)"
|
@click="test(EventDataTypes.Gift)"
|
||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
@@ -1080,7 +1264,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
:loading="isApiAudioLoading"
|
:loading="speechState.isApiAudioLoading"
|
||||||
@click="test(EventDataTypes.SC)"
|
@click="test(EventDataTypes.SC)"
|
||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
@@ -1094,7 +1278,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
:loading="isApiAudioLoading"
|
:loading="speechState.isApiAudioLoading"
|
||||||
@click="test(EventDataTypes.Guard)"
|
@click="test(EventDataTypes.Guard)"
|
||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
@@ -1108,7 +1292,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
:loading="isApiAudioLoading"
|
:loading="speechState.isApiAudioLoading"
|
||||||
@click="test(EventDataTypes.Enter)"
|
@click="test(EventDataTypes.Enter)"
|
||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: { port: 51000 },
|
server: { port: 51000 },
|
||||||
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
|
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
|
||||||
define: { 'process.env': {}, global: 'window' },
|
define: {
|
||||||
|
'process.env': {},
|
||||||
|
global: 'window',
|
||||||
|
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
|
||||||
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router']
|
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router']
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user