feat: add API endpoint mapping and optimize danmaku handling

- Added API endpoint mapping functionality to support user-selected API endpoints in QueryAPIInternal
- Refactored danmaku container component to use shallow refs for better performance
- Implemented normalized danmaku list handling with unique ID generation
- Added separate tracking of base and dynamic danmakus for better state management
- Improved danmaku filtering and ranking logic with computed properties
- Optimized processing state
This commit is contained in:
2025-11-01 18:30:37 +08:00
parent e0f57bcaf5
commit b51257f861
9 changed files with 1034 additions and 222 deletions

View File

@@ -1,5 +1,5 @@
import type { APIRoot, PaginationResponse } from './api-models'
import { apiFail } from '@/data/constants'
import { apiFail, mapToCurrentAPI } from '@/data/constants'
import { cookie } from './account'
import { useBiliAuth } from '@/store/useBiliAuth'
@@ -76,7 +76,9 @@ async function QueryPostAPIWithParamsInternal<T>(
}
async function QueryAPIInternal<T>(url: URL, init: RequestInit) {
try {
const data = await fetch(url, init)
// 使用用户选择的API
const mappedUrl = mapToCurrentAPI(url.toString())
const data = await fetch(mappedUrl, init)
const result = (await data.json()) as T
return result
} catch (e) {

View File

@@ -2,9 +2,8 @@
<script setup lang="ts">
import type { DanmakuModel, ResponseLiveInfoModel } from '@/api/api-models'
import { Info12Filled, Money20Regular, Money24Regular, Search24Filled, Wrench24Filled } from '@vicons/fluent'
import { useDebounceFn, useLocalStorage, useWindowSize } from '@vueuse/core'
import { useDebounceFn, useLocalStorage } from '@vueuse/core'
import { saveAs } from 'file-saver'
import { List } from 'linqts'
import {
NAvatar,
NButton,
@@ -31,7 +30,7 @@ import {
NTooltip,
useMessage,
} from 'naive-ui'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { useAccount } from '@/api/account'
import { EventDataTypes } from '@/api/api-models'
import DanmakuItem from '@/components/DanmakuItem.vue'
@@ -44,6 +43,7 @@ enum RankType {
Danmaku,
Paid,
}
interface RankInfo {
ouId: string
uName: string
@@ -60,7 +60,6 @@ const {
itemHeight = 30,
showLiveInfo = true,
showLiver = false,
// to = ClickNameTo.UserHistory,
isInModal = false,
showName = true,
showBorder = true,
@@ -79,56 +78,13 @@ const emit = defineEmits<{
}>()
const accountInfo = useAccount()
const message = useMessage()
const debouncedFn = useDebounceFn(() => {
UpdateDanmakus()
}, 750)
const isLoading = ref(false)
const showModal = ref(false)
const showExportModal = ref(false)
const userDanmakus = ref<DanmakuModel[] | undefined>()
const hideAvatar = useLocalStorage('Setting.HideAvatar', false)
const { width } = useWindowSize()
interface Props {
currentLive: ResponseLiveInfoModel
currentDanmakus: DanmakuModel[]
defaultFilterSelected?: EventDataTypes[]
height?: number
itemHeight?: number
showLiver?: boolean
showLiveInfo?: boolean
showName?: boolean
// to?: ClickNameTo;
isInModal?: boolean
showRank?: boolean
showBorder?: boolean
showTools?: boolean
showStatistic?: boolean
animeNum?: boolean
bordered?: boolean
toolsVisiable?: boolean
itemRange?: number
to: 'userDanmakus' | 'space'
}
defineExpose({
InsertDanmakus,
})
const danmakus = ref<DanmakuModel[]>([])
watch(
() => showTools,
(newV, oldV) => {
innerShowTools.value = newV
},
)
const danmakuRef = computed(() => {
// 不知道为啥不能直接watch
return currentDanmakus
})
watch(danmakuRef, (newValue) => {
danmakus.value = GetFilteredDanmakus(newValue)
})
const keyword = ref('')
const enableRegex = ref(false)
@@ -141,25 +97,243 @@ const orderDecreasing = ref(false)
const orderByPrice = ref(false)
const processing = ref(false)
const isRanking = ref(false)
const rankInfo = ref<RankInfo[]>([])
const currentRankInfo = ref<RankInfo[]>([])
const rankType = ref(RankType.Danmaku)
const hideEmoji = ref(false)
const existEnterMessage = ref(false)
const exportType = ref<'json' | 'xml' | 'csv'>('json')
const onlyExportFilteredDanmakus = ref(false)
const isExporting = ref(false)
const message = useMessage()
let processingTimer: ReturnType<typeof setTimeout> | undefined
const baseDanmakus = shallowRef<DanmakuModel[]>([])
const dynamicDanmakus = shallowRef<DanmakuModel[]>([])
interface Props {
currentLive: ResponseLiveInfoModel
currentDanmakus: DanmakuModel[]
defaultFilterSelected?: EventDataTypes[]
height?: number
itemHeight?: number
showLiver?: boolean
showLiveInfo?: boolean
showName?: boolean
isInModal?: boolean
showRank?: boolean
showBorder?: boolean
showTools?: boolean
showStatistic?: boolean
animeNum?: boolean
bordered?: boolean
toolsVisiable?: boolean
itemRange?: number
to: 'userDanmakus' | 'space'
}
function createDanmakuSignature(danmaku: DanmakuModel, index: number) {
const segments = []
if (danmaku.ouId) segments.push(`ou_${danmaku.ouId}`)
if (danmaku.uId) segments.push(`u_${danmaku.uId}`)
if (danmaku.time) segments.push(`t_${danmaku.time}`)
if (segments.length === 0) segments.push(`idx_${index}`)
return segments.join('_')
}
function normalizeDanmakuList(source: DanmakuModel[] | undefined | null, existingIds?: Set<string>) {
if (!source?.length) return []
const usedIds = existingIds ?? new Set<string>()
const normalized: DanmakuModel[] = []
source.forEach((item, index) => {
const baseId = item.id ?? createDanmakuSignature(item, index)
let candidateId = baseId
let suffix = 1
while (usedIds.has(candidateId)) {
candidateId = `${baseId}_${suffix++}`
}
normalized.push({
...item,
id: candidateId,
})
usedIds.add(candidateId)
})
return normalized
}
function triggerProcessing(delay = 120) {
processing.value = true
if (processingTimer) {
clearTimeout(processingTimer)
}
processingTimer = setTimeout(() => {
processing.value = false
}, delay)
}
function UpdateDanmakus() {
triggerProcessing(80)
}
const debouncedFn = useDebounceFn(UpdateDanmakus, 750)
watch(
() => showTools,
(value) => {
innerShowTools.value = value
},
)
watch(
() => rankType.value,
() => {
triggerProcessing(40)
},
)
watch(
() => currentDanmakus,
(list) => {
const normalized = normalizeDanmakuList(list)
const baseIds = new Set(normalized.map(item => item.id))
const filteredDynamic = dynamicDanmakus.value.filter(item => !baseIds.has(item.id))
baseDanmakus.value = normalized
if (filteredDynamic.length !== dynamicDanmakus.value.length) {
dynamicDanmakus.value = filteredDynamic
}
triggerProcessing(normalized.length ? 80 : 0)
},
{ immediate: true, deep: true },
)
const combinedDanmakus = computed(() => {
if (!dynamicDanmakus.value.length) {
return baseDanmakus.value
}
if (!baseDanmakus.value.length) {
return dynamicDanmakus.value
}
return [...baseDanmakus.value, ...dynamicDanmakus.value]
})
const existEnterMessage = computed(() =>
combinedDanmakus.value.some(item => item.type === EventDataTypes.Enter),
)
const filteredDanmakus = computed(() => {
const source = combinedDanmakus.value
if (!source.length) return []
const selectedTypes = new Set(filterSelected.value)
let working = source.filter(item => selectedTypes.has(item.type))
if (hideEmoji.value) {
working = working.filter(item => item.type !== EventDataTypes.Message || !item.isEmoji)
}
const keywordValue = keyword.value.trim()
if (keywordValue !== '') {
let regex: RegExp | null = null
if (enableRegex.value) {
try {
regex = new RegExp(keywordValue)
} catch {
regex = null
}
}
const keywordLower = keywordValue.toLowerCase()
const matcher = (item: DanmakuModel) => {
if (item.uId != null && item.uId.toString() === keywordValue) return true
if (item.uName && item.uName === keywordValue) return true
const message = item.msg ?? ''
if (!message) return false
if (regex) {
return regex.test(message)
}
return message.toLowerCase().includes(keywordLower)
}
working = working.filter(item => (deselect.value ? !matcher(item) : matcher(item)))
}
if (price.value && price.value > 0) {
const minPrice = price.value
working = working.filter(item => (item.price ?? 0) >= (minPrice ?? 0))
}
if (orderByPrice.value) {
working = working
.filter(item => item.type !== EventDataTypes.Message)
.sort((a, b) => (a.price ?? 0) - (b.price ?? 0))
} else {
working = [...working].sort((a, b) => a.time - b.time)
}
if (orderDecreasing.value) {
working.reverse()
}
return working
})
const filteredDanmakuCount = computed(() => filteredDanmakus.value.length)
const totalDanmakuCount = computed(() => combinedDanmakus.value.length)
const totalFilteredPrice = computed(() =>
filteredDanmakus.value.reduce((sum, item) => sum + (item.price && item.price > 0 ? item.price : 0), 0),
)
const rankStats = computed(() => aggregateRankStats(combinedDanmakus.value))
const currentRankInfo = computed(() => buildRankedList(rankStats.value, rankType.value))
function aggregateRankStats(source: DanmakuModel[]): RankInfo[] {
if (!source.length) return []
const ranking = new Map<string, RankInfo>()
source.forEach((item, index) => {
const key = item.ouId || (item.uId != null ? `uid_${item.uId}` : `idx_${index}`)
let info = ranking.get(key)
if (!info) {
info = {
ouId: item.ouId || key,
uName: item.uName ?? '未命名用户',
Paid: 0,
Danmakus: 0,
Index: 0,
}
ranking.set(key, info)
} else if (item.uName) {
info.uName = item.uName
}
if (item.type === EventDataTypes.Message) {
info.Danmakus += 1
}
if (typeof item.price === 'number') {
info.Paid += item.price
}
})
return Array.from(ranking.values())
}
function buildRankedList(stats: RankInfo[], type: RankType) {
if (!stats.length) return []
const filtered = stats
.filter(info => (type === RankType.Paid ? info.Paid > 0 : true))
.map(info => ({ ...info }))
filtered.sort((a, b) =>
type === RankType.Danmaku ? b.Danmakus - a.Danmakus : b.Paid - a.Paid,
)
const limited = filtered.slice(0, 100)
limited.forEach((info, idx) => {
info.Index = idx + 1
})
return limited
}
function OnNameClick(uId: number, ouId: string) {
if (isInModal) {
emit('onClickName', uId, ouId)
return
}
const sourceDanmakus = combinedDanmakus.value
switch (to) {
case 'userDanmakus': {
userDanmakus.value = currentDanmakus.filter(d => (d.uId ? d.uId == uId : d.ouId == ouId))
userDanmakus.value = sourceDanmakus.filter(d => (d.uId ? d.uId === uId : d.ouId === ouId))
showModal.value = true
break
}
@@ -176,6 +350,7 @@ function OnNameClick(uId: number, ouId: string) {
}
}
}
function ToUserSpace(uId: number) {
showModal.value = false
nextTick(() => {
@@ -187,71 +362,56 @@ function ToUserSpace(uId: number) {
})
})
}
function ChangePrice(p: number) {
if (p == price.value) {
if (p === price.value) {
price.value = undefined
} else {
price.value = p
}
UpdateDanmakus()
}
function UpdateDanmakus() {
processing.value = true
nextTick(() => {
setTimeout(() => {
canInsert = false
danmakus.value = GetFilteredDanmakus()
setTimeout(() => {
canInsert = true
}, 1000) // 立马就能的话会莫名key重复
processing.value = false
}, 50)
})
}
function OnRank(isRank: boolean) {
if (isRank) OnRankDirect(rankType.value, rankInfo.value.length == 0 && danmakus.value.length > 0)
}
function OnRankDirect(type: RankType, refresh: boolean, orderByDescending = true) {
if (refresh) {
const rank = {} as {
[ouId: string]: RankInfo
}
currentDanmakus.forEach((danmaku) => {
if (danmaku.ouId in rank) {
if (danmaku.type == EventDataTypes.Message) rank[danmaku.ouId].Danmakus++
rank[danmaku.ouId].Paid += danmaku.price ?? 0
} else {
rank[danmaku.ouId] = {
// uId: danmaku.uId,
ouId: danmaku.ouId,
uName: danmaku.uName,
Paid: danmaku.price ?? 0,
Danmakus: danmaku.type == EventDataTypes.Message ? 1 : 0,
Index: 0,
}
}
})
rankInfo.value = new List(Object.entries(rank)).Select(([uId, user]) => user).ToArray()
if (isRank) {
triggerProcessing(60)
}
let ienum = {} as List<RankInfo>
switch (rankType.value) {
case RankType.Danmaku: {
ienum = new List(rankInfo.value).OrderByDescending(user => user.Danmakus)
break
}
case RankType.Paid: {
ienum = new List(rankInfo.value).Where(user => (user?.Paid ?? 0) > 0).OrderByDescending(user => user.Paid)
break
}
}
if (!orderByDescending) ienum = ienum.Reverse()
currentRankInfo.value = ienum.Take(100).ToArray()
let index = 1
currentRankInfo.value.forEach((info) => {
info.Index = index
index++
})
}
function InsertDanmakus(targetDanmakus: DanmakuModel[]) {
if (!Array.isArray(targetDanmakus) || targetDanmakus.length === 0) return
const existingIds = new Set<string>([
...baseDanmakus.value.map(item => item.id),
...dynamicDanmakus.value.map(item => item.id),
])
const normalized = normalizeDanmakuList(targetDanmakus, existingIds)
if (!normalized.length) return
dynamicDanmakus.value = orderDecreasing.value
? [...normalized, ...dynamicDanmakus.value]
: [...dynamicDanmakus.value, ...normalized]
triggerProcessing(40)
}
function Export() {
isExporting.value = true
const source = onlyExportFilteredDanmakus.value ? filteredDanmakus.value : combinedDanmakus.value
saveAs(
new Blob(
[
GetString(
accountInfo.value,
currentLive,
source,
exportType.value,
),
],
{ type: 'text/plain;charset=utf-8' },
),
`${Date.now()}_${currentLive.startAt}_${currentLive.title.replace('_', '-')}_${accountInfo.value?.name}.${exportType.value}`,
)
isExporting.value = false
}
function GetRankIndexColor(index: number) {
switch (index) {
case 1:
@@ -264,86 +424,22 @@ function GetRankIndexColor(index: number) {
return 'background:#afafaf;'
}
}
function Export() {
isExporting.value = true
saveAs(
new Blob(
[
GetString(
accountInfo.value,
currentLive,
onlyExportFilteredDanmakus.value ? danmakus.value : currentDanmakus,
exportType.value,
),
],
{ type: 'text/plain;charset=utf-8' },
),
`${Date.now()}_${currentLive.startAt}_${currentLive.title.replace('_', '-')}_${accountInfo.value?.name}.${exportType.value}`,
)
isExporting.value = false
}
let canInsert = false
function InsertDanmakus(targetDanmakus: DanmakuModel[]) {
if (processing.value || !canInsert) {
return
}
const data = GetFilteredDanmakus(targetDanmakus)
if (orderDecreasing.value) {
danmakus.value.unshift(...data)
currentDanmakus.unshift(...data)
} else {
danmakus.value.push(...data)
currentDanmakus.push(...data)
}
}
function GetFilteredDanmakus(targetDanmakus?: DanmakuModel[]) {
if (!targetDanmakus) targetDanmakus = currentDanmakus
let tempDanmakus = targetDanmakus.filter(d => filterSelected.value.includes(d.type))
if (hideEmoji.value) {
tempDanmakus = tempDanmakus.filter(d => d.type != EventDataTypes.Message || !d.isEmoji)
}
if (orderByPrice.value) {
tempDanmakus = tempDanmakus
.filter(d => d.type != EventDataTypes.Message)
.sort((a, b) => (a.price ?? 0) - (b.price ?? 0))
} else {
tempDanmakus = tempDanmakus.sort((a, b) => a.time - b.time)
}
if (keyword.value && keyword.value != '') {
tempDanmakus = tempDanmakus.filter(d => (deselect.value ? !CheckKeyword(d) : CheckKeyword(d)))
}
function CheckKeyword(d: DanmakuModel) {
if (d.uId.toString() == keyword.value || d.uName == keyword.value) {
return true
}
return enableRegex.value
? !!d.msg?.match(keyword.value)
: d.msg?.toLowerCase().includes(keyword.value.toLowerCase()) == true
}
if (price.value && price.value > 0) {
tempDanmakus = tempDanmakus.filter(d => (d.price ?? 0) >= (price.value ?? 0))
}
if (orderDecreasing.value) tempDanmakus = tempDanmakus.reverse()
let index = 0
tempDanmakus.forEach((d) => {
d.id = `${d.ouId}_${d.time}_${index}`
index++
})
return tempDanmakus
}
function RoundNumber(num: number) {
if (Number.isInteger(num)) {
return num
} else {
return num.toFixed(1)
}
return num.toFixed(1)
}
onMounted(() => {
danmakus.value = GetFilteredDanmakus()
existEnterMessage.value = danmakus.value.some(d => d.type == EventDataTypes.Message)
// defaultFilterSelected.push(EventDataTypes.Enter)
onBeforeUnmount(() => {
if (processingTimer) {
clearTimeout(processingTimer)
}
})
defineExpose({
InsertDanmakus,
})
</script>
@@ -681,8 +777,8 @@ onMounted(() => {
style="font-size: 12px"
size="small"
>
{{ danmakus.length }}
{{ danmakus.length != currentDanmakus.length ? `/ ${currentDanmakus.length}` : '' }}
{{ filteredDanmakuCount }}
{{ filteredDanmakuCount !== totalDanmakuCount ? `/ ${totalDanmakuCount}` : '' }}
</NTag>
<NDivider vertical />
@@ -693,14 +789,14 @@ onMounted(() => {
:bordered="false"
>
💰
{{ RoundNumber(danmakus.reduce((a, b) => a + (b.price && b.price > 0 ? b.price : 0), 0)) }}
{{ RoundNumber(totalFilteredPrice) }}
</NTag>
</NDivider>
</NCollapseTransition>
<div :style="isRanking ? 'display:none' : ''">
<SimpleVirtualList
v-if="danmakus.length > itemRange"
:items="danmakus"
v-if="filteredDanmakuCount > itemRange"
:items="filteredDanmakus"
:default-size="itemHeight"
:default-height="height ?? 1000"
>
@@ -718,7 +814,7 @@ onMounted(() => {
</template>
</SimpleVirtualList>
<p
v-for="item in danmakus"
v-for="item in filteredDanmakus"
v-else
:key="item.id"
:style="`min-height: ${itemHeight}px;width:97%;display:flex;align-items:center;`"
@@ -737,11 +833,6 @@ onMounted(() => {
<NRadioGroup
v-model:value="rankType"
size="small"
@update:value="
(type) => {
OnRankDirect(type, false)
}
"
>
<NRadioButton :value="RankType.Danmaku">
弹幕

View File

@@ -1,4 +1,5 @@
import { defineAsyncComponent, markRaw, ref } from 'vue'
import { useStorage } from '@vueuse/core'
const debugAPI
= import.meta.env.VITE_API == 'dev'
@@ -18,6 +19,42 @@ export const IMGUR_URL = `${FILE_BASE_URL}/imgur/`
export const THINGS_URL = `${FILE_BASE_URL}/things/`
export const apiFail = ref(false)
// API 配置
export interface APIConfig {
name: string
url: string
key: string
}
export const availableAPIs: APIConfig[] = [
{ name: '主API (国内)', url: releseAPI, key: 'main' },
{ name: '备用API (国外)', url: failoverAPI, key: 'failover' },
]
// 从 localStorage 读取用户选择的 API默认使用主 API
export const selectedAPIKey = useStorage<string>('Settings.SelectedAPI', 'main')
// 获取当前选择的 API URL
export function getCurrentAPIUrl(): string {
if (import.meta.env.NODE_ENV === 'development') {
return debugAPI
}
const selected = availableAPIs.find(api => api.key === selectedAPIKey.value)
return selected?.url || releseAPI
}
// 将URL映射到当前选择的API
export function mapToCurrentAPI(url: string): string {
if (import.meta.env.NODE_ENV === 'development') {
return url // 开发环境不替换
}
const currentAPI = getCurrentAPIUrl()
// 替换所有已知的API域名为当前选择的API
return url
.replace(releseAPI, currentAPI)
.replace(failoverAPI, currentAPI)
}
export const BASE_URL
= import.meta.env.NODE_ENV === 'development'
? debugAPI

View File

@@ -34,16 +34,18 @@ import {
NPageHeader,
NPopconfirm,
NScrollbar,
NSelect,
NSlider,
NSpace,
NSpin,
NSwitch,
NTag,
NText,
NTime,
NTooltip,
useMessage,
} from 'naive-ui'
import { computed, h, onMounted, ref, watch } from 'vue'
import { computed, h, onMounted, onUnmounted, ref, watch } from 'vue'
// @ts-ignore
import APlayer from 'vue3-aplayer'
import { RouterLink, useRoute } from 'vue-router'
@@ -51,7 +53,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 { ACCOUNT_API_URL } from '@/data/constants'
import { ACCOUNT_API_URL, availableAPIs, selectedAPIKey } from '@/data/constants'
import { useBiliAuth } from '@/store/useBiliAuth'
import { useMusicRequestProvider } from '@/store/useMusicRequest'
import { isDarkMode, NavigateToNewTab } from '@/Utils'
@@ -180,6 +182,45 @@ const currentPlayingInfo = computed(() => {
const canResendEmail = ref(false)
const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified)
// 加载超时检测
const loadingTimeout = ref(false)
const showAPISwitchDialog = ref(false)
let loadingTimer: number | null = null
// 监听加载状态设置3秒超时
watch(isLoadingAccount, (loading) => {
if (loading) {
loadingTimeout.value = false
showAPISwitchDialog.value = false
loadingTimer = window.setTimeout(() => {
if (isLoadingAccount.value) {
loadingTimeout.value = true
// 如果当前使用主API提示切换到备用API
if (selectedAPIKey.value === 'main') {
showAPISwitchDialog.value = true
}
}
}, 3000)
} else {
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
loadingTimeout.value = false
showAPISwitchDialog.value = false
}
}, { immediate: true })
// 切换API
function switchToBackupAPI() {
selectedAPIKey.value = 'failover'
message.info('已切换到备用API正在重新加载...')
showAPISwitchDialog.value = false
setTimeout(() => {
location.reload()
}, 500)
}
// 图标渲染函数 - 用于菜单项
const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) })
@@ -615,6 +656,13 @@ onMounted(() => {
}
}
})
onUnmounted(() => {
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
})
</script>
<template>
@@ -1124,6 +1172,24 @@ onMounted(() => {
<NSpin :loading="isLoadingAccount" size="large">
<NText>正在请求账户数据...</NText>
</NSpin>
<NAlert
v-if="showAPISwitchDialog"
type="warning"
style="margin-top: 20px; max-width: 400px;"
title="加载时间较长"
>
<NSpace vertical>
<NText>当前API响应较慢是否切换到备用API</NText>
<NFlex justify="end" :size="8">
<NButton size="small" @click="showAPISwitchDialog = false">
继续等待
</NButton>
<NButton type="primary" size="small" @click="switchToBackupAPI">
切换到备用API
</NButton>
</NFlex>
</NSpace>
</NAlert>
</NFlex>
</NCard>
</template>

View File

@@ -16,6 +16,7 @@ import {
NInputGroup,
NModal,
NPopconfirm,
NSelect,
NSpace,
NTabPane,
NTabs,
@@ -31,7 +32,7 @@ import { cookie, useAccount } from '@/api/account'
import { BiliAuthCodeStatusType } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
import { ACCOUNT_API_URL, CN_HOST, TURNSTILE_KEY } from '@/data/constants'
import { ACCOUNT_API_URL, availableAPIs, CN_HOST, selectedAPIKey, TURNSTILE_KEY } from '@/data/constants'
import { checkUpdateNote } from '@/data/UpdateNote'
import SettingPaymentView from './Setting_PaymentView.vue'
import SettingsManageView from './SettingsManageView.vue'
@@ -63,6 +64,20 @@ const biliCode = ref('')
const biliAuthText = ref('')
const isLoading = ref(false)
// API选择器选项
const apiOptions = availableAPIs.map(api => ({
label: api.name,
value: api.key,
}))
// 切换API
function handleAPIChange(value: string) {
message.info(`正在切换到${availableAPIs.find(api => api.key === value)?.name}...`)
setTimeout(() => {
location.reload()
}, 500)
}
function logout() {
cookie.value = undefined
window.location.reload()
@@ -546,6 +561,26 @@ onUnmounted(() => {
</NAlert>
</NSpace>
<NDivider />
<NAlert
type="info"
title="API 设置"
style="width: 100%; max-width: 800px; margin-bottom: 16px;"
>
<NFlex align="center" :wrap="false" style="gap: 12px;">
<NText>当前使用的API:</NText>
<NSelect
v-model:value="selectedAPIKey"
:options="apiOptions"
style="flex: 1; max-width: 200px;"
@update:value="handleAPIChange"
/>
<NText depth="3" style="font-size: 12px;">
如果访问速度较慢可以尝试切换API
</NText>
</NFlex>
</NAlert>
<NDivider />
<NSpace justify="center">
<NButton
type="info"

View File

@@ -59,32 +59,12 @@ async function loadInitialData() {
}
}
function eventModelToDanmakuModel(event: EventModel): DanmakuModel {
return {
ouId: event.ouid,
uId: event.uid ?? 0,
uName: event.uname,
msg: event.msg ?? '',
time: event.time,
type: event.type,
price: event.price,
num: event.num,
guardLevel: event.guard_level,
fansMedalLevel: event.fans_medal_level,
fansMedalName: event.fans_medal_name,
fansMedalWearingStatus: event.fans_medal_wearing_status,
isEmoji: !!event.emoji,
emoji: event.emoji,
id: `${event.ouid}_${event.time}_${Date.now()}`,
} as DanmakuModel
}
function onNewDanmaku(event: EventModel) {
function onNewDanmaku(event: DanmakuModel) {
if (!liveInfo.value) return
const danmaku = eventModelToDanmakuModel(event)
danmakuContainerRef.value?.InsertDanmakus([danmaku])
console.log('New Danmaku:', event)
danmakuContainerRef.value?.InsertDanmakus([event])
// 更新统计信息
if (event.price && event.price > 0) {
liveInfo.value.live.totalIncome += event.price