feat: 添加更新日志功能;优化组件和状态管理;修复部分逻辑错误

This commit is contained in:
2025-04-08 16:11:00 +08:00
parent 0195e7b01a
commit 364d38ddc0
19 changed files with 536 additions and 312 deletions

3
default.d.ts vendored
View File

@@ -1,5 +1,5 @@
import type { LoadingBarProviderInst, MessageProviderInst, ModalProviderInst, NotificationProviderInst } from 'naive-ui'
import type { DialogProviderInst, LoadingBarProviderInst, MessageProviderInst, ModalProviderInst, NotificationProviderInst } from 'naive-ui'
import type { useRoute } from 'vue-router'
declare module 'vue3-aplayer' {
@@ -23,5 +23,6 @@ declare global {
$modal: ModalProviderInst
$mitt: Emitter<MittType>
$notification: NotificationProviderInst
$dialog: DialogProviderInst
}
}

View File

@@ -992,7 +992,7 @@
<NIcon :component="BarChartOutline" /> {{ webfetcher.sessionEventCount?.toLocaleString() ?? 0 }}
</NStatistic>
<NStatistic label="已发送">
{{ ((webfetcher.bytesSentSession ?? 0) / 1024).toFixed(2) }} KB <!-- Assuming exposed -->
{{ ((webfetcher.bytesSentSession ?? 0) / 1024 / 1024).toFixed(2) }} Mb <!-- Assuming exposed -->
</NStatistic>
</NFlex>
</NCard>

View File

@@ -10,6 +10,10 @@
const webfetcher = useWebFetcher();
const coverRef = ref();
const isHover = ref(false);
const roomCover = computed(() => {
return isHover.value ? roomInfo.value?.keyframe : roomInfo.value?.user_cover;
});
const { width, height } = useElementSize(coverRef);
function logout() {
@@ -90,8 +94,8 @@
<template #header>
<NSpace align="center">
直播状态
<NTag :type="!accountInfo.streamerInfo?.isStreaming ? 'error' : 'success'">
{{ !accountInfo.streamerInfo?.isStreaming ? '直播' : '直播' }}
<NTag :type="roomInfo?.live_status === 1 ? 'success' : 'error'">
{{ roomInfo?.live_status === 1 ? '直播' : '直播' }}
</NTag>
</NSpace>
<NText
@@ -102,13 +106,13 @@
</NText>
</template>
<div
v-if="roomInfo?.user_cover"
v-if="roomCover"
style="position: relative"
>
<div style="position: relative; width: 100%; max-width: 500px;">
<NImage
ref="coverRef"
:src="roomInfo?.user_cover"
:src="roomCover"
style="width: 100%; opacity: 0.5; border-radius: 8px;"
referrerpolicy="no-referrer"
:img-props="{ referrerpolicy: 'no-referrer', style: { width: '100%' } }"
@@ -118,6 +122,8 @@
style="position: absolute; z-index: 1; top: 0; width: 100%; background: linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0.2), transparent)"
:style="{ height: `${height}px`, maxWidth: '500px', cursor: 'pointer' }"
@click="openUrl(`https://live.bilibili.com/${accountInfo.biliRoomId}`)"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
/>
<NText style="position: absolute; bottom: 12px; left: 16px; z-index: 2; color: white; font-size: 18px">
{{ roomInfo?.title }}

View File

@@ -117,169 +117,160 @@ import { isTauri } from '@/data/constants';
</script>
<template>
<NAlert
v-if="!isTauri"
type="error"
title="错误"
<WindowBar />
<div
v-if="!isLoggedIn"
class="login-container"
>
此应用在 Tauri 环境外运行无法使用
</NAlert>
<template v-else>
<WindowBar />
<div
v-if="!isLoggedIn"
class="login-container"
<NCard
v-if="!isLoadingAccount"
:bordered="false"
size="large"
class="login-card"
>
<NCard
v-if="!isLoadingAccount"
:bordered="false"
size="large"
class="login-card"
>
<template #header>
<div class="login-header">
<div class="login-title">
登陆
</div>
<div class="login-subtitle">
输入你的 VTsuru Token
</div>
</div>
</template>
<NSpace
vertical
size="large"
>
<NSpace vertical>
<div class="token-label-container">
<span class="token-label">Token</span>
<NTooltip placement="top">
<template #trigger>
<NA
class="token-get-link"
@click="openUrl('https://vtsuru.suki.club/manage')"
>
前往获取
</NA>
</template>
登录后在管理面板主页的个人信息下方
</NTooltip>
</div>
<NInput
v-model:value="token"
type="password"
show-password-on="click"
placeholder="请输入Token"
@keyup.enter="login"
/>
</NSpace>
<NButton
block
type="primary"
:loading="isLoadingAccount"
:disabled="isLoadingAccount"
@click="login"
>
<template #header>
<div class="login-header">
<div class="login-title">
登陆
</NButton>
</NSpace>
</NCard>
</div>
<div class="login-subtitle">
输入你的 VTsuru Token
</div>
</div>
</template>
<NSpin
v-else
<NSpace
vertical
size="large"
/>
</div>
<NLayout
v-else
has-sider
class="main-layout"
@vue:mounted="initAll(true)"
>
<NLayoutSider
width="200"
bordered
class="main-layout-sider"
>
<div class="sider-content">
<div class="sider-header">
<NText
tag="div"
class="app-title"
>
<span>VTsuru.Client</span>
</NText>
<NTooltip trigger="hover">
<NSpace vertical>
<div class="token-label-container">
<span class="token-label">Token</span>
<NTooltip placement="top">
<template #trigger>
<NButton
quaternary
class="fetcher-status-button"
:type="webfetcher.state === 'connected' ? 'success' : 'error'"
<NA
class="token-get-link"
@click="openUrl('https://vtsuru.suki.club/manage')"
>
<CheckmarkCircle
v-if="webfetcher.state === 'connected'"
class="fetcher-status-icon connected"
/>
<CloseCircle
v-else
class="fetcher-status-icon disconnected"
/>
</NButton>
前往获取
</NA>
</template>
<div>
<div>EventFetcher 状态</div>
<div v-if="webfetcher.state === 'connected'">
运行中
</div>
<div v-else>
未运行 / 连接断开
</div>
</div>
登录后在管理面板主页的个人信息下方
</NTooltip>
</div>
<NMenu
:options="menuOptions"
:default-value="'go-back-home'"
class="sider-menu"
<NInput
v-model:value="token"
type="password"
show-password-on="click"
placeholder="请输入Token"
@keyup.enter="login"
/>
</div>
</NLayoutSider>
</NSpace>
<NLayoutContent
class="main-layout-content"
:native-scrollbar="false"
:scrollbar-props="{
trigger: 'none'
}"
>
<div style="padding: 12px; padding-right: 15px;">
<RouterView v-slot="{ Component }">
<Transition
name="fade-slide"
mode="out-in"
:appear="true"
>
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback>
<div class="suspense-fallback">
加载中...
</div>
</template>
</Suspense>
</KeepAlive>
</Transition>
</RouterView>
<NButton
block
type="primary"
:loading="isLoadingAccount"
:disabled="isLoadingAccount"
@click="login"
>
登陆
</NButton>
</NSpace>
</NCard>
<NSpin
v-else
size="large"
/>
</div>
<NLayout
v-else
has-sider
class="main-layout"
@vue:mounted="initAll(true)"
>
<NLayoutSider
width="200"
bordered
class="main-layout-sider"
>
<div class="sider-content">
<div class="sider-header">
<NText
tag="div"
class="app-title"
>
<span>VTsuru.Client</span>
</NText>
<NTooltip trigger="hover">
<template #trigger>
<NButton
quaternary
class="fetcher-status-button"
:type="webfetcher.state === 'connected' ? 'success' : 'error'"
>
<CheckmarkCircle
v-if="webfetcher.state === 'connected'"
class="fetcher-status-icon connected"
/>
<CloseCircle
v-else
class="fetcher-status-icon disconnected"
/>
</NButton>
</template>
<div>
<div>EventFetcher 状态</div>
<div v-if="webfetcher.state === 'connected'">
运行中
</div>
<div v-else>
未运行 / 连接断开
</div>
</div>
</NTooltip>
</div>
</NLayoutContent>
</NLayout>
</template>
<NMenu
:options="menuOptions"
:default-value="'go-back-home'"
class="sider-menu"
/>
</div>
</NLayoutSider>
<NLayoutContent
class="main-layout-content"
:native-scrollbar="false"
:scrollbar-props="{
trigger: 'none'
}"
>
<div style="padding: 12px; padding-right: 15px;">
<RouterView v-slot="{ Component }">
<Transition
name="fade-slide"
mode="out-in"
:appear="true"
>
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback>
<div class="suspense-fallback">
加载中...
</div>
</template>
</Suspense>
</KeepAlive>
</Transition>
</RouterView>
</div>
</NLayoutContent>
</NLayout>
</template>
<style scoped>

View File

@@ -333,6 +333,9 @@ watch(minimizeOnStart, (newValue) => {
Tauri 客户端
</NButton>
</p>
<p>
反馈: 🐧 873260337
</p>
<!-- Add more about info -->
</NCard>
</template>

View File

@@ -4,6 +4,7 @@ import { info, error } from '@tauri-apps/plugin-log';
import { QueryBiliAPI } from './utils'; // 假设 Bili API 工具路径
import { BiliRoomInfo, BiliStreamingInfo, FetcherStatisticData } from './models'; // 假设模型路径
import { useTauriStore } from '../store/useTauriStore';
import { useAccount } from '@/api/account';
// import { useAccount } from '@/api/account'; // 如果需要账户信息
// const accountInfo = useAccount(); // 如果需要
@@ -185,10 +186,8 @@ export async function getHistoricalStatistics(days: number = 7): Promise<Fetcher
* 更新房间和直播流信息
*/
async function updateRoomAndStreamingInfo() {
// 需要一个房间ID来查询这个ID可能来自设置、登录信息或固定配置
// const roomId = accountInfo.value?.roomid ?? settings.value.roomId; // 示例获取房间ID
const roomId = 21484828; // !!! 示例这里需要替换成实际获取房间ID的逻辑 !!!
if (!roomId) {
const account = useAccount();
if (!account.value.biliRoomId) {
// error("无法获取房间ID以更新直播信息");
return;
}
@@ -196,7 +195,7 @@ async function updateRoomAndStreamingInfo() {
try {
// 查询房间基本信息
const roomRes = await QueryBiliAPI(
`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`
`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${account.value.biliRoomId}`
);
const json = await roomRes.json();
if (json.code === 0) {

View File

@@ -4,6 +4,18 @@ import { isPermissionGranted, onAction, sendNotification } from "@tauri-apps/plu
import { openUrl } from "@tauri-apps/plugin-opener";
import { CN_HOST } from "@/data/constants";
export function onReceivedNotification(type: string, data: any) {
switch (type) {
case 'question-box':
onReceivedQuestion(data);
break;
default:
console.warn(`Unhandled notification type: ${type}`);
}
}
export async function onReceivedQuestion(question: QAInfo) {
const setting = useSettings();
if (setting.settings.notificationSettings.enableTypes.includes("question-box")) {

1
src/components.d.ts vendored
View File

@@ -57,6 +57,7 @@ declare module 'vue' {
SongPlayer: typeof import('./components/SongPlayer.vue')['default']
TempComponent: typeof import('./components/TempComponent.vue')['default']
TurnstileVerify: typeof import('./components/TurnstileVerify.vue')['default']
UpdateNoteContainer: typeof import('./components/UpdateNoteContainer.vue')['default']
UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default']
VEditor: typeof import('./components/VEditor.vue')['default']
VideoCollectInfoCard: typeof import('./components/VideoCollectInfoCard.vue')['default']

View File

@@ -139,7 +139,7 @@ const showContent = ref(!isViolation)
<br>
</template>
<NText :style="{ filter: showContent ? '' : 'blur(3.7px)', cursor: showContent ? '' : 'pointer' }">
<NText :style="{ filter: showContent ? '' : 'blur(3.7px)', cursor: showContent ? '' : 'pointer', whiteSpace: 'pre-wrap' }">
<NButton
v-if="isViolation"
size="small"

View File

@@ -16,6 +16,7 @@ onMounted(() => {
window.$route = useRoute()
window.$modal = useModal()
window.$notification = useNotification()
window.$dialog = useDialog()
const providerStore = useLoadingBarStore()
providerStore.setLoadingBar(window.$loadingBar)
})

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import { updateNoteItemContentType, updateNotes } from '@/data/UpdateNote';
import { NDivider, NGrid } from 'naive-ui';
import { VNode } from 'vue';
const currentVer = '1'
const savedVer = useStorage('UpdateNoteVer', 0)
function renderContent(content: updateNoteItemContentType): VNode | string | undefined {
if (Array.isArray(content)) {
return h('div', { style: { whiteSpace: 'pre-wrap' } }, content.map(item => renderContent(item)))
}
if (typeof content === 'string') {
return content
}
if (typeof content === 'function') {
return content()
}
return undefined;
}
</script>
<template>
<NScrollbar
style="max-height: 80vh;"
trigger="none"
>
<NFlex vertical>
<div
v-for="item in updateNotes"
:key="item.ver"
>
<NDivider title-placement="left">
{{ item.date }}
</NDivider>
<NGrid
x-gap="10"
>
<template
v-for="note in item.items"
:key="note.title"
>
<NGridItem span="6">
<div style="">
<NTag
v-if="note.type === 'fix'"
type="info"
round
:bordered="false"
>
错误修复
</NTag>
<NTag
v-else-if="note.type === 'new'"
type="success"
round
:bordered="false"
>
功能添加
</NTag>
<NTag
v-else-if="note.type === 'optimize'"
:color="{ textColor: '#000', color: '#f0ad4e', borderColor: '#f0ad4e' }"
round
:bordered="false"
>
功能优化
</NTag>
<NTag
v-else-if="note.type === 'other'"
type="error"
round
:bordered="false"
>
其他
</NTag>
</div>
</NGridItem>
<NGridItem span="18">
<NFlex vertical>
<template
v-for="content in note.content"
:key="content"
>
<component :is="renderContent(content)" />
</template>
</NFlex>
</NGridItem>
</template>
</NGrid>
</div>
</NFlex>
</NScrollbar>
</template>

155
src/data/Initializer.ts Normal file
View File

@@ -0,0 +1,155 @@
import { GetSelfAccount, useAccount, UpdateAccountLoop } from "@/api/account";
import { QueryGetAPI } from "@/api/query";
import { useAuthStore } from "@/store/useAuthStore";
import { useNotificationStore } from "@/store/useNotificationStore";
import { createDiscreteApi, NText, NFlex, NButton } from "naive-ui";
import { BASE_API_URL, isTauri, apiFail } from "./constants";
import { GetNotifactions } from "./notifactions";
import HyperDX from '@hyperdx/browser'
import EasySpeech from "easy-speech";
import { checkUpdateNote } from "./UpdateNote";
let currentVersion: string
let isHaveNewVersion = false
const { notification } = createDiscreteApi(['notification'])
export function InitVTsuru() {
QueryGetAPI<string>(`${BASE_API_URL}vtsuru/version`)
.then((version) => {
if (version.code == 200) {
currentVersion = version.data
const savedVersion = localStorage.getItem('Version')
localStorage.setItem('Version', currentVersion)
if (currentVersion && savedVersion && savedVersion !== currentVersion) {
setTimeout(() => {
location.reload()
}, 1000)
// alert('发现新的版本更新, 请按 Ctrl+F5 强制刷新页面')
notification.info({
title: '发现新的版本更新',
content: '将自动刷新页面',
duration: 5000,
meta: () => h(NText, { depth: 3 }, () => currentVersion),
})
}
else {
InitVersionCheck();
}
}
InitOther();
})
.catch(() => {
apiFail.value = true
console.log('默认API调用失败, 切换至故障转移节点')
})
.finally(async () => {
})
}
async function InitOther() {
if (process.env.NODE_ENV !== 'development') {
HyperDX.init({
apiKey: '7d1eb66c-24b8-445e-a406-dc2329fa9423',
service: 'vtsuru.live',
tracePropagationTargets: [/vtsuru.suki.club/i], // Set to link traces from frontend to backend requests
consoleCapture: true, // Capture console logs (default false)
advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false)
})
}
// 加载其他数据
InitTTS()
await GetSelfAccount()
const account = useAccount()
const useAuth = useAuthStore()
if (account.value.id) {
if (account.value.biliUserAuthInfo && !useAuth.currentToken) {
useAuth.currentToken = account.value.biliUserAuthInfo.token
}
HyperDX.setGlobalAttributes({
userId: account.value.id.toString(),
userName: account.value.name,
})
}
useAuth.getAuthInfo()
GetNotifactions()
UpdateAccountLoop()
useNotificationStore().init()
}
function InitVersionCheck() {
setInterval(() => {
if (isHaveNewVersion) {
return
}
QueryGetAPI<string>(`${BASE_API_URL}vtsuru/version`).then(
(keepCheckData) => {
if (
keepCheckData.code == 200
&& keepCheckData.data != currentVersion
) {
isHaveNewVersion = true
currentVersion = keepCheckData.data
localStorage.setItem('Version', currentVersion)
console.log(`[vtsuru] 发现新版本: ${currentVersion}`)
const url = new URL(window.location.href)
const path = url.pathname
if (!path.startsWith('/obs')) {
if (isTauri) {
location.reload();
}
else {
const n = notification.info({
title: '发现新的版本更新',
content: '是否现在刷新?',
meta: () => h(NText, { depth: 3 }, () => currentVersion),
action: () =>
h(NFlex, null, () => [
h(
NButton,
{
text: true,
type: 'primary',
onClick: () => location.reload(),
size: 'small',
},
{ default: () => '刷新' },
),
h(
NButton,
{
text: true,
onClick: () => n.destroy(),
size: 'small',
},
{ default: () => '稍后' },
),
]),
})
}
}
}
},
)
}, 60 * 1000)
}
function InitTTS() {
try {
const result = EasySpeech.detect()
if (result.speechSynthesis) {
EasySpeech.init({ maxTimeout: 5000, interval: 250 })
.then(() => console.log('[SpeechSynthesis] 已加载tts服务'))
.catch(e => console.error(e))
}
else {
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
}
}
catch (e) {
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
}
}

91
src/data/UpdateNote.ts Normal file
View File

@@ -0,0 +1,91 @@
import UpdateNoteContainer from "@/components/UpdateNoteContainer.vue";
import { NButton, NImage, NTag } from "naive-ui";
import { VNode } from "vue";
import { FETCH_API } from "./constants";
export const updateNotes: updateNoteType[] = [
{
ver: 2,
date: '2025.4.8',
items: [
{
type: 'new',
title: 'EventFetcher Tauri 客户端开始测试',
content: [
['比当前所有 EventFetcher 部署方法都更要简单且支持扫码登录的客户端开始测试力, 支持Windows, Linux, MacOS (后两个没测试过'],
[
'如果对此感兴趣的话可以使用 ',
() => h(NButton, {
text: true, tag: 'a', href: FETCH_API + 'https://github.com/Megghy/vtsuru-fetcher-client/releases/download/app-v0.1.0/vtsuru-fetcher-client_0.1.0_x64-setup.exe', target: '_blank', type: 'info'
}, () => '这个链接'),
' 下载Windows客户端, 其他平台请在下面的客户端 Repo 中的 Release 下载',
],
[
'当前可能存在一些问题, 可以加入秋秋群 873260337 进行反馈, 有功能需求也可以提出'
],
[],
[
'源码: ',
() => h(NButton, {
text: true, tag: 'a', href: 'https://github.com/Megghy/vtsuru-fetvher-client', target: '_blank', type: 'info'
}, () => ' 客户端 Repo '),
' | ',
() => h(NButton, {
text: true, tag: 'a', href: 'https://github.com/Megghy/vtsuru.live/tree/master/src/client', target: '_blank', type: 'info'
}, () => ' UI/逻辑 '),
],
[
() => h(NImage, { src: 'https://pan.suki.club/d/vtsuru/imgur/01295402D7FBBF192FE5608179A4A7A6.png', width: 200 }),
]
]
}
],
},
{
ver: 1,
date: '2025.3.18',
items: [
{
title: '歌单',
type: 'optimize',
content: [
[
'新增一个歌单样式: 列表',
() => h(NImage, { src: 'https://pan.suki.club/d/vtsuru/imgur/QQ20250408-134631.png', width: 300, height: 200 }),
]
],
},
],
},
];
export const currentUpdateNoteVer = updateNotes.sort((a, b) => b.ver - a.ver)[0].ver;
export const currentUpdateNote = updateNotes.sort((a, b) => b.ver - a.ver)[0].items;
export const savedUpdateNoteVer = useStorage('UpdateNoteVer', 0);
export function checkUpdateNote() {
if (savedUpdateNoteVer.value < currentUpdateNoteVer) {
window.$dialog.create({
title: '更新日志',
content: () => h(UpdateNoteContainer),
negativeText: '确定',
positiveText: '下次更新前不再提示',
onPositiveClick: () => {
savedUpdateNoteVer.value = currentUpdateNoteVer;
}
});
}
}
export type updateType = 'fix' | 'new' | 'optimize' | 'other';
export type updateNoteType = {
ver: number;
date: string;
items: updateNoteItemType[];
};
export type updateNoteItemType = {
title?: string | (() => VNode);
type: updateType;
content: updateNoteItemContentType[];
};
export type updateNoteItemContentType = (() => VNode) | string | updateNoteItemContentType[];

View File

@@ -1,158 +1,18 @@
import { QueryGetAPI } from '@/api/query'
import { apiFail, BASE_API_URL, isTauri } from '@/data/constants'
import HyperDX from '@hyperdx/browser'
import EasySpeech from 'easy-speech'
import { createDiscreteApi, NButton, NFlex, NText } from 'naive-ui'
import { createPinia } from 'pinia'
import { createApp, h } from 'vue'
import { GetSelfAccount, UpdateAccountLoop, useAccount } from './api/account'
import App from './App.vue'
import { GetNotifactions } from './data/notifactions'
import emitter from './mitt'
import router from './router'
import { useAuthStore } from './store/useAuthStore'
import { useNotificationStore } from './store/useNotificationStore'
import EasySpeech from 'easy-speech';
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
import { InitVTsuru } from './data/Initializer';
import emitter from './mitt';
import router from './router';
const pinia = createPinia()
export const getPinia = () => pinia
QueryGetAPI<string>(`${BASE_API_URL}vtsuru/version`)
.then((version) => {
if (version.code == 200) {
currentVersion = version.data
const savedVersion = localStorage.getItem('Version')
localStorage.setItem('Version', currentVersion)
if (currentVersion && savedVersion && savedVersion !== currentVersion) {
setTimeout(() => {
location.reload()
}, 1000)
// alert('发现新的版本更新, 请按 Ctrl+F5 强制刷新页面')
notification.info({
title: '发现新的版本更新',
content: '将自动刷新页面',
duration: 5000,
meta: () => h(NText, { depth: 3 }, () => currentVersion),
})
}
else {
setInterval(() => {
if (isHaveNewVersion) {
return
}
QueryGetAPI<string>(`${BASE_API_URL}vtsuru/version`).then(
(keepCheckData) => {
if (
keepCheckData.code == 200
&& keepCheckData.data != currentVersion
) {
isHaveNewVersion = true
currentVersion = keepCheckData.data
localStorage.setItem('Version', currentVersion)
console.log(`[vtsuru] 发现新版本: ${currentVersion}`)
const url = new URL(window.location.href)
const path = url.pathname
if (!path.startsWith('/obs')) {
if (isTauri) {
location.reload();
}
else {
const n = notification.info({
title: '发现新的版本更新',
content: '是否现在刷新?',
meta: () => h(NText, { depth: 3 }, () => currentVersion),
action: () =>
h(NFlex, null, () => [
h(
NButton,
{
text: true,
type: 'primary',
onClick: () => location.reload(),
size: 'small',
},
{ default: () => '刷新' },
),
h(
NButton,
{
text: true,
onClick: () => n.destroy(),
size: 'small',
},
{ default: () => '稍后' },
),
]),
})
}
}
}
},
)
}, 60 * 1000)
}
}
})
.catch(() => {
apiFail.value = true
console.log('默认API调用失败, 切换至故障转移节点')
})
.finally(async () => {
if (process.env.NODE_ENV !== 'development') {
HyperDX.init({
apiKey: '7d1eb66c-24b8-445e-a406-dc2329fa9423',
service: 'vtsuru.live',
tracePropagationTargets: [/vtsuru.suki.club/i], // Set to link traces from frontend to backend requests
consoleCapture: true, // Capture console logs (default false)
advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false)
})
}
// 加载其他数据
InitTTS()
await GetSelfAccount()
const account = useAccount()
const useAuth = useAuthStore()
if (account.value.id) {
if (account.value.biliUserAuthInfo && !useAuth.currentToken) {
useAuth.currentToken = account.value.biliUserAuthInfo.token
}
HyperDX.setGlobalAttributes({
userId: account.value.id.toString(),
userName: account.value.name,
})
}
useAuth.getAuthInfo()
GetNotifactions()
UpdateAccountLoop()
})
const app = createApp(App)
app.use(router).use(pinia).mount('#app')
let currentVersion: string
let isHaveNewVersion = false
const { notification } = createDiscreteApi(['notification'])
useNotificationStore().init()
InitVTsuru();
window.$mitt = emitter
function InitTTS() {
try {
const result = EasySpeech.detect()
if (result.speechSynthesis) {
EasySpeech.init({ maxTimeout: 5000, interval: 250 })
.then(() => console.log('[SpeechSynthesis] 已加载tts服务'))
.catch(e => console.error(e))
}
else {
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
}
}
catch (e) {
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
}
}

View File

@@ -16,6 +16,7 @@ import { ZstdCodec, ZstdInit } from '@oneidentity/zstd-js/wasm';
import { encode } from "@msgpack/msgpack";
import { getVersion } from '@tauri-apps/api/app';
import { onReceivedNotification } from '@/client/data/notification';
export const useWebFetcher = defineStore('WebFetcher', () => {
const route = useRoute();
@@ -275,7 +276,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
Stop(); // 服务器要求断开,调用 Stop 清理所有资源
});
connection.on('Request', async (url: string, method: string, body: string, useCookie: boolean) => onRequest(url, method, body, useCookie));
connection.on('Notification', (type: string, data: any) => { onReceivedNotification(type, data); });
// --- 尝试启动连接 ---
try {
await connection.start();

View File

@@ -4,6 +4,7 @@ import { cookie, isLoadingAccount, useAccount } from '@/api/account'
import { ThemeType } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
import { checkUpdateNote } from '@/data/UpdateNote';
import { ACCOUNT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore'
import { useMusicRequestProvider } from '@/store/useMusicRequest'
@@ -485,6 +486,9 @@ onMounted(() => {
canResendEmail.value = true
}
}
// 当进入管理页时检查更新日志
checkUpdateNote();
})
</script>

View File

@@ -163,7 +163,7 @@ function saveShareImage() {
// 生成的ba64图片
canvas.toBlob(
(data) => {
saveAs(data, `vtsuru-提问箱-${accountInfo.value?.name}.png`)
saveAs(data!, `vtsuru-提问箱-${accountInfo.value?.name}.png`)
},
'image/png',
1,

View File

@@ -183,6 +183,7 @@ onUnmounted(() => {
.question-display-text {
min-height: 50px;
white-space: pre-wrap;
}
.question-display-image {

View File

@@ -54,6 +54,11 @@ export default defineConfig({
Components({
resolvers: [NaiveUiResolver()],
dts: 'src/components.d.ts',
// allow auto load markdown components under `./src/components/`
extensions: ['vue', 'md'],
// allow auto import and register components used in markdown
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
}),
monacoEditorPlugin({ languageWorkers: ['css'] }),
oxlintPlugin(),