mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
Compare commits
3 Commits
febfa132c8
...
aa2d63a33c
| Author | SHA1 | Date | |
|---|---|---|---|
| aa2d63a33c | |||
| f9417870ce | |||
| 94a315a906 |
@@ -90,14 +90,6 @@ export function useFollowThank(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关注事件 - 旧方式实现,用于兼容现有代码
|
||||
*/
|
||||
function onFollow(event: EventModel) {
|
||||
// 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
|
||||
console.log('关注事件处理已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理计时器
|
||||
*/
|
||||
@@ -109,7 +101,6 @@ export function useFollowThank(
|
||||
}
|
||||
|
||||
return {
|
||||
onFollow,
|
||||
processFollow,
|
||||
clearTimer
|
||||
};
|
||||
|
||||
@@ -155,17 +155,8 @@ export function useGuardPm(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上舰事件 - 旧方式实现,用于兼容现有代码
|
||||
*/
|
||||
function onGuard(event: EventModel) {
|
||||
// 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
|
||||
console.log('舰长事件处理已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
onGuard,
|
||||
processGuard
|
||||
};
|
||||
}
|
||||
@@ -73,20 +73,6 @@ export function useScheduledDanmaku(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时弹幕 (旧方式)
|
||||
*/
|
||||
function startScheduledDanmaku() {
|
||||
console.log('定时弹幕已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时弹幕 (旧方式)
|
||||
*/
|
||||
function stopScheduledDanmaku() {
|
||||
console.log('定时弹幕已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化剩余时间为分:秒格式
|
||||
*/
|
||||
@@ -111,8 +97,6 @@ export function useScheduledDanmaku(
|
||||
}
|
||||
|
||||
return {
|
||||
startScheduledDanmaku,
|
||||
stopScheduledDanmaku,
|
||||
processScheduledActions,
|
||||
clearTimer,
|
||||
remainingSeconds,
|
||||
|
||||
@@ -55,14 +55,12 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||
);
|
||||
|
||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
||||
const guardPm = useGuardPm(
|
||||
isLive,
|
||||
roomId,
|
||||
(userId: number, message: string) => biliFunc.sendPrivateMessage(userId, message)
|
||||
);
|
||||
|
||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
||||
const followThank = useFollowThank(
|
||||
isLive,
|
||||
roomId,
|
||||
@@ -70,7 +68,6 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||
);
|
||||
|
||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
||||
const entryWelcome = useEntryWelcome(
|
||||
isLive,
|
||||
roomId,
|
||||
@@ -78,14 +75,12 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||
);
|
||||
|
||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
||||
const autoReply = useAutoReply(
|
||||
isLive,
|
||||
roomId,
|
||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||
);
|
||||
|
||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
||||
const scheduledDanmaku = useScheduledDanmaku(
|
||||
isLive,
|
||||
roomId,
|
||||
@@ -168,11 +163,11 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
break;
|
||||
|
||||
case TriggerType.GUARD:
|
||||
guardPm.onGuard(event);
|
||||
guardPm.processGuard(event, autoActions.value, runtimeState.value);
|
||||
break;
|
||||
|
||||
case TriggerType.FOLLOW:
|
||||
followThank.onFollow(event);
|
||||
followThank.processFollow(event, autoActions.value, runtimeState.value);
|
||||
break;
|
||||
|
||||
case TriggerType.ENTER:
|
||||
@@ -366,7 +361,7 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
if (!roomId.value) return;
|
||||
|
||||
// 使用专用模块处理定时发送
|
||||
scheduledDanmaku.startScheduledDanmaku();
|
||||
scheduledDanmaku.processScheduledActions(autoActions.value, runtimeState.value);
|
||||
|
||||
// 同时处理自定义的定时任务
|
||||
const scheduledActions = autoActions.value.filter(
|
||||
@@ -382,37 +377,33 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
|
||||
|
||||
const timerFunc = () => {
|
||||
if (!isLive.value && action.triggerConfig.onlyDuringLive) {
|
||||
// 如果设置了仅直播时发送,且当前未直播,则跳过
|
||||
return;
|
||||
// 仅在检查时判断直播状态,不停止定时器
|
||||
const shouldExecute =
|
||||
!action.triggerConfig.onlyDuringLive || isLive.value;
|
||||
|
||||
if (shouldExecute && !(action.triggerConfig.ignoreTianXuan && isTianXuanActive.value)) {
|
||||
// 创建执行上下文
|
||||
const context: ExecutionContext = {
|
||||
roomId: roomId.value,
|
||||
variables: {
|
||||
date: {
|
||||
formatted: new Date().toLocaleString(),
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
day: new Date().getDate(),
|
||||
hour: new Date().getHours(),
|
||||
minute: new Date().getMinutes(),
|
||||
second: new Date().getSeconds(),
|
||||
}
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 执行定时操作
|
||||
executeAction(action, context);
|
||||
}
|
||||
|
||||
if (action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) {
|
||||
// 如果设置了天选时刻不发送,且当前有天选,则跳过
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建执行上下文
|
||||
const context: ExecutionContext = {
|
||||
roomId: roomId.value,
|
||||
variables: {
|
||||
date: {
|
||||
formatted: new Date().toLocaleString(),
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
day: new Date().getDate(),
|
||||
hour: new Date().getHours(),
|
||||
minute: new Date().getMinutes(),
|
||||
second: new Date().getSeconds(),
|
||||
}
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 执行定时操作
|
||||
executeAction(action, context);
|
||||
|
||||
// 设置下一次执行
|
||||
// 无论是否执行,都设置下一次定时
|
||||
runtimeState.value.scheduledTimers[action.id] = setTimeout(timerFunc, intervalSeconds * 1000);
|
||||
};
|
||||
|
||||
@@ -465,8 +456,48 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
// 如果是定时操作,重新配置定时器
|
||||
if (action.triggerType === TriggerType.SCHEDULED) {
|
||||
if (enabled) {
|
||||
// 启用时重新启动定时器
|
||||
startScheduledActions();
|
||||
// 如果已有定时器,先清理
|
||||
if (runtimeState.value.scheduledTimers[id]) {
|
||||
clearTimeout(runtimeState.value.scheduledTimers[id]!);
|
||||
runtimeState.value.scheduledTimers[id] = null;
|
||||
}
|
||||
|
||||
// 启用时单独启动这个定时器
|
||||
const intervalSeconds = action.triggerConfig.intervalSeconds || 300;
|
||||
|
||||
const timerFunc = () => {
|
||||
// 仅在检查时判断直播状态,不停止定时器
|
||||
const shouldExecute =
|
||||
!action.triggerConfig.onlyDuringLive || isLive.value;
|
||||
|
||||
if (shouldExecute && !(action.triggerConfig.ignoreTianXuan && isTianXuanActive.value)) {
|
||||
// 创建执行上下文
|
||||
const context: ExecutionContext = {
|
||||
roomId: roomId.value,
|
||||
variables: {
|
||||
date: {
|
||||
formatted: new Date().toLocaleString(),
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
day: new Date().getDate(),
|
||||
hour: new Date().getHours(),
|
||||
minute: new Date().getMinutes(),
|
||||
second: new Date().getSeconds(),
|
||||
}
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 执行定时操作
|
||||
executeAction(action, context);
|
||||
}
|
||||
|
||||
// 无论是否执行,都设置下一次定时
|
||||
runtimeState.value.scheduledTimers[id] = setTimeout(timerFunc, intervalSeconds * 1000);
|
||||
};
|
||||
|
||||
// 启动定时器
|
||||
runtimeState.value.scheduledTimers[id] = setTimeout(timerFunc, intervalSeconds * 1000);
|
||||
} else if (runtimeState.value.scheduledTimers[id]) {
|
||||
// 禁用时清理定时器
|
||||
clearTimeout(runtimeState.value.scheduledTimers[id]!);
|
||||
@@ -484,14 +515,14 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
// 启动所有定时发送任务
|
||||
startScheduledActions();
|
||||
|
||||
// 监听直播状态变化,自动启停定时任务
|
||||
watch(isLive, (newIsLive) => {
|
||||
// 不再根据直播状态停止定时任务,只在回调中判断
|
||||
/*watch(isLive, (newIsLive) => {
|
||||
if (newIsLive) {
|
||||
startScheduledActions();
|
||||
} else {
|
||||
stopAllScheduledActions();
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
// 安全地订阅事件
|
||||
try {
|
||||
@@ -510,115 +541,6 @@ export const useAutoAction = defineStore('autoAction', () => {
|
||||
clearAllTimers();
|
||||
});
|
||||
}
|
||||
|
||||
// 迁移旧的配置
|
||||
migrateAutoReplyConfig();
|
||||
migrateGiftThankConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移旧的自动回复配置到新的AutoActionItem格式
|
||||
*/
|
||||
function migrateAutoReplyConfig() {
|
||||
try {
|
||||
// 尝试从localStorage获取旧配置
|
||||
const oldConfigStr = localStorage.getItem('autoAction.autoReplyConfig');
|
||||
if (!oldConfigStr) return;
|
||||
|
||||
const oldConfig = JSON.parse(oldConfigStr);
|
||||
if (!oldConfig.enabled || !oldConfig.rules || !Array.isArray(oldConfig.rules)) return;
|
||||
|
||||
// 检查是否已经迁移过(防止重复迁移)
|
||||
const migratedKey = 'autoAction.autoReplyMigrated';
|
||||
if (localStorage.getItem(migratedKey) === 'true') return;
|
||||
|
||||
// 将旧规则转换为新的AutoActionItem
|
||||
const newItems = oldConfig.rules.map((rule: any) => {
|
||||
const item = createDefaultAutoAction(TriggerType.DANMAKU);
|
||||
item.name = `弹幕回复: ${rule.keywords.join(',')}`;
|
||||
item.enabled = oldConfig.enabled;
|
||||
item.templates = rule.replies || ['感谢您的弹幕'];
|
||||
item.triggerConfig = {
|
||||
...item.triggerConfig,
|
||||
keywords: rule.keywords || [],
|
||||
blockwords: rule.blockwords || [],
|
||||
onlyDuringLive: oldConfig.onlyDuringLive,
|
||||
userFilterEnabled: oldConfig.userFilterEnabled,
|
||||
requireMedal: oldConfig.requireMedal,
|
||||
requireCaptain: oldConfig.requireCaptain
|
||||
};
|
||||
item.actionConfig = {
|
||||
...item.actionConfig,
|
||||
cooldownSeconds: oldConfig.cooldownSeconds || 5
|
||||
};
|
||||
return item;
|
||||
});
|
||||
|
||||
// 添加到现有的autoActions中
|
||||
autoActions.value = [...autoActions.value, ...newItems];
|
||||
|
||||
// 标记为已迁移
|
||||
localStorage.setItem(migratedKey, 'true');
|
||||
console.log(`成功迁移 ${newItems.length} 条自动回复规则`);
|
||||
} catch (error) {
|
||||
console.error('迁移自动回复配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移旧的礼物感谢配置到新的AutoActionItem格式
|
||||
*/
|
||||
function migrateGiftThankConfig() {
|
||||
try {
|
||||
// 尝试从localStorage获取旧配置
|
||||
const oldConfigStr = localStorage.getItem('autoAction.giftThankConfig');
|
||||
if (!oldConfigStr) return;
|
||||
|
||||
const oldConfig = JSON.parse(oldConfigStr);
|
||||
if (!oldConfig.enabled || !oldConfig.templates || !Array.isArray(oldConfig.templates)) return;
|
||||
|
||||
// 检查是否已经迁移过(防止重复迁移)
|
||||
const migratedKey = 'autoAction.giftThankMigrated';
|
||||
if (localStorage.getItem(migratedKey) === 'true') return;
|
||||
|
||||
// 创建新的礼物感谢项
|
||||
const item = createDefaultAutoAction(TriggerType.GIFT);
|
||||
item.name = '礼物感谢';
|
||||
item.enabled = oldConfig.enabled;
|
||||
item.templates = oldConfig.templates;
|
||||
|
||||
// 设置触发配置
|
||||
item.triggerConfig = {
|
||||
...item.triggerConfig,
|
||||
onlyDuringLive: oldConfig.onlyDuringLive ?? true,
|
||||
ignoreTianXuan: oldConfig.ignoreTianXuan ?? true,
|
||||
userFilterEnabled: oldConfig.userFilterEnabled ?? false,
|
||||
requireMedal: oldConfig.requireMedal ?? false,
|
||||
requireCaptain: oldConfig.requireCaptain ?? false,
|
||||
filterMode: oldConfig.filterModes?.useWhitelist ? 'whitelist' :
|
||||
oldConfig.filterModes?.useBlacklist ? 'blacklist' : undefined,
|
||||
filterGiftNames: oldConfig.filterGiftNames || [],
|
||||
minValue: oldConfig.minValue || 0
|
||||
};
|
||||
|
||||
// 设置操作配置
|
||||
item.actionConfig = {
|
||||
...item.actionConfig,
|
||||
delaySeconds: oldConfig.delaySeconds || 0,
|
||||
cooldownSeconds: 5,
|
||||
maxUsersPerMsg: oldConfig.maxUsersPerMsg || 3,
|
||||
maxItemsPerUser: oldConfig.maxGiftsPerUser || 3
|
||||
};
|
||||
|
||||
// 添加到现有的autoActions中
|
||||
autoActions.value.push(item);
|
||||
|
||||
// 标记为已迁移
|
||||
localStorage.setItem(migratedKey, 'true');
|
||||
console.log('成功迁移礼物感谢配置');
|
||||
} catch (error) {
|
||||
console.error('迁移礼物感谢配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载时清理
|
||||
|
||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -18,6 +18,7 @@ declare module 'vue' {
|
||||
LabelItem: typeof import('./components/LabelItem.vue')['default']
|
||||
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
||||
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
song: SongsInfo | undefined
|
||||
isLrcLoading?: string
|
||||
}>()
|
||||
const emits = defineEmits(['update:isLrcLoading'])
|
||||
const emits = defineEmits(['update:isLrcLoading', 'update:close', 'update:volume'])
|
||||
|
||||
const aplayerMusic = ref({
|
||||
title: '',
|
||||
|
||||
@@ -99,7 +99,7 @@ function InitVersionCheck() {
|
||||
const path = url.pathname
|
||||
|
||||
if (!path.startsWith('/obs')) {
|
||||
if (isTauri) {
|
||||
if (isTauri()) {
|
||||
location.reload();
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -88,6 +88,9 @@ export function checkUpdateNote() {
|
||||
positiveText: '下次更新前不再提示',
|
||||
onPositiveClick: () => {
|
||||
savedUpdateNoteVer.value = currentUpdateNoteVer;
|
||||
},
|
||||
onClose: () => {
|
||||
savedUpdateNoteVer.value = currentUpdateNoteVer;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const failoverAPI = `https://failover-api.vtsuru.suki.club/`;
|
||||
export const isBackendUsable = ref(true);
|
||||
export const isDev = import.meta.env.MODE === 'development';
|
||||
// @ts-ignore
|
||||
export const isTauri = window.__TAURI__ !== undefined || window.__TAURI_INTERNAL__ !== undefined;
|
||||
export const isTauri = () => window.__TAURI__ !== undefined || window.__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window;
|
||||
|
||||
export const AVATAR_URL = 'https://workers.vrp.moe/api/bilibili/avatar/';
|
||||
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club';
|
||||
|
||||
@@ -196,7 +196,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
danmakuClientState.value = 'connected'; // 明确设置状态
|
||||
danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址
|
||||
// 启动事件发送定时器 (如果之前没有启动)
|
||||
timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件
|
||||
timer ??= setInterval(sendEvents, 2000); // 每 2 秒尝试发送一次事件
|
||||
return { success: true, message: '弹幕客户端已启动' };
|
||||
} else {
|
||||
console.error(prefix.value + '弹幕客户端启动失败');
|
||||
@@ -301,7 +301,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
Data: string;
|
||||
};
|
||||
async function onRequest(url: string, method: string, body: string, useCookie: boolean) {
|
||||
if (!isTauri) {
|
||||
if (!isTauri()) {
|
||||
console.error(prefix.value + '非Tauri环境下无法处理请求: ' + url);
|
||||
return {
|
||||
Message: '非Tauri环境',
|
||||
@@ -386,7 +386,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
}
|
||||
|
||||
// 批量处理事件,每次最多发送20条
|
||||
const batchSize = 20;
|
||||
const batchSize = 30;
|
||||
const batch = events.slice(0, batchSize);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTimelineItem } from 'naive-ui'
|
||||
import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTimelineItem, NTag, NIcon } from 'naive-ui'
|
||||
import { h } from 'vue'
|
||||
import { CodeOutline, ServerOutline, HeartOutline, LogoGithub } from '@vicons/ionicons5'
|
||||
</script>
|
||||
<template>
|
||||
<NLayoutContent style="height: 100vh">
|
||||
<NLayoutContent style="height: 100vh; padding: 20px 0;">
|
||||
<NSpace
|
||||
style="margin-top: 50px"
|
||||
style="margin-top: 30px"
|
||||
justify="center"
|
||||
align="center"
|
||||
vertical
|
||||
>
|
||||
<NCard
|
||||
style="max-width: 80vw; width: 700px"
|
||||
style="max-width: 80vw; width: 700px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);"
|
||||
embedded
|
||||
>
|
||||
<template #header>
|
||||
关于
|
||||
<div style="font-size: 22px; font-weight: bold; padding: 8px 0;">
|
||||
关于
|
||||
</div>
|
||||
</template>
|
||||
<NText>
|
||||
一个兴趣使然的网站
|
||||
@@ -52,23 +56,174 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
||||
<NSpace
|
||||
vertical
|
||||
align="center"
|
||||
style="margin-bottom: 16px;"
|
||||
>
|
||||
<span style="color: gray">
|
||||
MADE WITH ❤️ BY
|
||||
<span style="color: #666; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
||||
MADE WITH <NIcon
|
||||
size="18"
|
||||
color="#f56c6c"
|
||||
><HeartOutline /></NIcon> BY
|
||||
<NButton
|
||||
type="primary"
|
||||
tag="a"
|
||||
href="https://space.bilibili.com/10021741"
|
||||
target="_blank"
|
||||
text
|
||||
style=""
|
||||
style="font-weight: bold;"
|
||||
>
|
||||
Megghy
|
||||
</NButton>
|
||||
</span>
|
||||
<div style="margin-top: 8px; display: flex; align-items: center; gap: 8px;">
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://github.com/Megghy/vtsuru.live"
|
||||
target="_blank"
|
||||
text
|
||||
style="display: flex; align-items: center; gap: 4px; color: #666;"
|
||||
>
|
||||
<NIcon size="16">
|
||||
<LogoGithub />
|
||||
</NIcon>
|
||||
<span>源代码仓库</span>
|
||||
</NButton>
|
||||
</div>
|
||||
</NSpace>
|
||||
<NDivider
|
||||
title-placement="left"
|
||||
style="margin-top: 12px;"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span>技术栈</span>
|
||||
</div>
|
||||
</NDivider>
|
||||
<NSpace
|
||||
vertical
|
||||
style="padding: 0 12px; margin-bottom: 16px;"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<NIcon
|
||||
size="20"
|
||||
color="#709e7e"
|
||||
>
|
||||
<CodeOutline />
|
||||
</NIcon>
|
||||
<NText style="font-weight: 500;">
|
||||
前端:
|
||||
</NText>
|
||||
<NTag
|
||||
type="success"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://vuejs.org/"
|
||||
target="_blank"
|
||||
text
|
||||
style="padding: 0; color: inherit;"
|
||||
>
|
||||
Vue.js
|
||||
</NButton>
|
||||
</NTag>
|
||||
<NTag
|
||||
type="info"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://www.typescriptlang.org/"
|
||||
target="_blank"
|
||||
text
|
||||
style="padding: 0; color: inherit;"
|
||||
>
|
||||
TypeScript
|
||||
</NButton>
|
||||
</NTag>
|
||||
<NTag
|
||||
type="warning"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://www.naiveui.com/"
|
||||
target="_blank"
|
||||
text
|
||||
style="padding: 0; color: inherit;"
|
||||
>
|
||||
Naive UI
|
||||
</NButton>
|
||||
</NTag>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<NIcon
|
||||
size="20"
|
||||
color="#7c7ca0"
|
||||
>
|
||||
<ServerOutline />
|
||||
</NIcon>
|
||||
<NText style="font-weight: 500;">
|
||||
后端:
|
||||
</NText>
|
||||
<NTag
|
||||
type="primary"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://dotnet.microsoft.com/"
|
||||
target="_blank"
|
||||
text
|
||||
style="padding: 0; color: inherit;"
|
||||
>
|
||||
C# .NET 10
|
||||
</NButton>
|
||||
</NTag>
|
||||
<NTag
|
||||
type="error"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://www.postgresql.org/"
|
||||
target="_blank"
|
||||
text
|
||||
style="padding: 0; color: inherit;"
|
||||
>
|
||||
PostgreSQL
|
||||
</NButton>
|
||||
</NTag>
|
||||
<NTag
|
||||
type="default"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
<NButton
|
||||
tag="a"
|
||||
href="https://keydb.dev/"
|
||||
target="_blank"
|
||||
text
|
||||
style="padding: 0; color: inherit;"
|
||||
>
|
||||
KeyDB
|
||||
</NButton>
|
||||
</NTag>
|
||||
</div>
|
||||
</NSpace>
|
||||
<NDivider title-placement="left">
|
||||
赞助我
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span>赞助我</span>
|
||||
</div>
|
||||
</NDivider>
|
||||
<iframe
|
||||
id="afdian_leaflet_vtsuru"
|
||||
@@ -77,10 +232,15 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
||||
scrolling="no"
|
||||
height="200"
|
||||
frameborder="0"
|
||||
style="border-radius: 8px;"
|
||||
/>
|
||||
<NDivider title-placement="left">
|
||||
更新日志
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span>更新日志</span>
|
||||
</div>
|
||||
</NDivider>
|
||||
<UpdateNoteContainer />
|
||||
<NDivider />
|
||||
<NTimeline>
|
||||
<NTimelineItem
|
||||
type="info"
|
||||
@@ -241,9 +401,46 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
||||
</NTimeline>
|
||||
</template>
|
||||
</NCard>
|
||||
<NButton @click="$router.push({ name: 'manage-index' })">
|
||||
<NButton
|
||||
type="primary"
|
||||
style="margin-top: 20px; padding: 8px 24px; font-size: 15px; border-radius: 8px;"
|
||||
@click="$router.push({ name: 'manage-index' })"
|
||||
>
|
||||
回到控制台
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NLayoutContent>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-timeline) {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
:deep(.n-timeline-item-content) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-card-header) {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:deep(.n-divider.n-divider--title-position-left .n-divider__title) {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
:deep(.n-timeline-item-content__title) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.n-timeline-item-content__content) {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
:deep(.n-timeline-item-content__time) {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TabletSpeaker24Filled,
|
||||
VehicleShip24Filled,
|
||||
VideoAdd20Filled,
|
||||
Mail24Filled,
|
||||
} from '@vicons/fluent'
|
||||
import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye } from '@vicons/ionicons5'
|
||||
import { useElementSize, useStorage } from '@vueuse/core'
|
||||
@@ -561,37 +562,90 @@ onMounted(() => {
|
||||
</RouterView>
|
||||
<!-- 未验证邮箱的提示 -->
|
||||
<template v-else>
|
||||
<NAlert type="info">
|
||||
请进行邮箱验证
|
||||
<br><br>
|
||||
<NSpace>
|
||||
<NButton
|
||||
size="small"
|
||||
type="info"
|
||||
:disabled="!canResendEmail"
|
||||
@click="resendEmail"
|
||||
<NCard>
|
||||
<NSpace
|
||||
vertical
|
||||
size="large"
|
||||
align="center"
|
||||
>
|
||||
<NFlex
|
||||
justify="center"
|
||||
align="center"
|
||||
vertical
|
||||
>
|
||||
重新发送验证邮件
|
||||
</NButton>
|
||||
<NCountdown
|
||||
v-if="!canResendEmail"
|
||||
:duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
|
||||
@finish="canResendEmail = true"
|
||||
/>
|
||||
<NIcon
|
||||
size="48"
|
||||
color="#2080f0"
|
||||
>
|
||||
<Mail24Filled />
|
||||
</NIcon>
|
||||
<NText style="font-size: 20px; margin-top: 16px; font-weight: 500;">
|
||||
请验证您的邮箱
|
||||
</NText>
|
||||
<NText
|
||||
depth="3"
|
||||
style="text-align: center; margin-top: 8px;"
|
||||
>
|
||||
我们已向您的邮箱发送了验证链接,请查收并点击链接完成验证
|
||||
</NText>
|
||||
</NFlex>
|
||||
|
||||
<NPopconfirm
|
||||
size="small"
|
||||
@positive-click="logout"
|
||||
<NAlert
|
||||
type="warning"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<Info24Filled />
|
||||
</NIcon>
|
||||
</template>
|
||||
如果长时间未收到邮件,请检查垃圾邮件文件夹,或点击下方按钮重新发送
|
||||
</NAlert>
|
||||
|
||||
<NSpace>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="!canResendEmail"
|
||||
style="min-width: 140px;"
|
||||
@click="resendEmail"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<Mail24Filled />
|
||||
</NIcon>
|
||||
</template>
|
||||
重新发送验证邮件
|
||||
</NButton>
|
||||
<NTag
|
||||
v-if="!canResendEmail"
|
||||
type="warning"
|
||||
round
|
||||
>
|
||||
<NCountdown
|
||||
:duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
|
||||
@finish="canResendEmail = true"
|
||||
/>
|
||||
后可重新发送
|
||||
</NTag>
|
||||
</NSpace>
|
||||
|
||||
<NDivider style="width: 80%; min-width: 250px;" />
|
||||
|
||||
<NPopconfirm @positive-click="logout">
|
||||
<template #trigger>
|
||||
<NButton type="error">
|
||||
登出
|
||||
<NButton secondary>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<PersonFeedback24Filled />
|
||||
</NIcon>
|
||||
</template>
|
||||
切换账号
|
||||
</NButton>
|
||||
</template>
|
||||
确定登出?
|
||||
确定要登出当前账号吗?
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</NAlert>
|
||||
</NCard>
|
||||
</template>
|
||||
<NBackTop />
|
||||
</NElement>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { onMounted, onUnmounted, ref } from 'vue'
|
||||
const timer = ref<any>()
|
||||
const visible = ref(true)
|
||||
const active = ref(true)
|
||||
|
||||
const originalBackgroundColor = ref('')
|
||||
onMounted(() => {
|
||||
timer.value = setInterval(() => {
|
||||
if (!visible.value || !active.value) return
|
||||
@@ -22,9 +24,21 @@ onMounted(() => {
|
||||
active.value = a
|
||||
}
|
||||
}
|
||||
// 使 .n-layout-content 背景透明
|
||||
const layoutContent = document.querySelector('.n-layout-content');
|
||||
if (layoutContent instanceof HTMLElement) {
|
||||
originalBackgroundColor.value = layoutContent.style.backgroundColor
|
||||
layoutContent.style.setProperty('background-color', 'transparent');
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer.value)
|
||||
// 还原 .n-layout-content 背景颜色
|
||||
const layoutContent = document.querySelector('.n-layout-content');
|
||||
if (layoutContent instanceof HTMLElement) {
|
||||
layoutContent.style.setProperty('background-color', originalBackgroundColor.value);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -46,9 +60,3 @@ onUnmounted(() => {
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.body,html,.n-element,.n-layout-content {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { isDarkMode } from '@/Utils'
|
||||
import { ThemeType } from '@/api/api-models'
|
||||
import { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient';
|
||||
import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent'
|
||||
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5'
|
||||
import { useElementSize, useStorage } from '@vueuse/core'
|
||||
import { isDarkMode } from '@/Utils' // 引入暗黑模式判断工具
|
||||
import { ThemeType } from '@/api/api-models' // 引入主题类型枚举
|
||||
import { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient' // 引入开放平台认证信息类型
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'; // 引入弹幕客户端状态管理
|
||||
import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent' // 引入 Fluent UI 图标
|
||||
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5' // 引入 Ionicons 图标
|
||||
import { useElementSize, useStorage } from '@vueuse/core' // 引入 VueUse 组合式函数
|
||||
import {
|
||||
NAlert,
|
||||
NAvatar,
|
||||
@@ -27,21 +27,28 @@ import {
|
||||
NTag,
|
||||
NText,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { h, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
} from 'naive-ui' // 引入 Naive UI 组件
|
||||
import { h, onMounted, onUnmounted, ref, computed } from 'vue' // 引入 Vue 相关 API
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router' // 引入 Vue Router 相关 API
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
||||
// -- 基本状态和工具 --
|
||||
const route = useRoute() // 获取当前路由信息
|
||||
const router = useRouter() // 获取路由实例
|
||||
const message = useMessage() // 获取 Naive UI 消息提示 API
|
||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto) // 使用 useStorage 持久化主题设置 (默认自动)
|
||||
const danmakuClient = useDanmakuClient(); // 获取弹幕客户端实例
|
||||
|
||||
const sider = ref()
|
||||
const { width } = useElementSize(sider)
|
||||
// -- 侧边栏状态 --
|
||||
const sider = ref<HTMLElement | null>(null) // 侧边栏 DOM 引用
|
||||
const { width: siderWidth } = useElementSize(sider) // 实时获取侧边栏宽度
|
||||
|
||||
const authInfo = ref<AuthInfo>()
|
||||
const danmakuClient = await useDanmakuClient().initOpenlive();
|
||||
// -- 认证与连接状态 --
|
||||
const authInfo = ref<AuthInfo>() // 存储从路由查询参数获取的认证信息
|
||||
const danmakuClientError = ref<string>() // 存储弹幕客户端初始化错误信息
|
||||
|
||||
const menuOptions = [
|
||||
// -- 菜单配置 --
|
||||
// 定义菜单项, 使用 h 函数渲染 RouterLink 以实现路由跳转
|
||||
const menuOptions = computed(() => [ // 改为 computed 以便将来可能动态修改
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
@@ -49,10 +56,10 @@ const menuOptions = [
|
||||
{
|
||||
to: {
|
||||
name: 'open-live-lottery',
|
||||
query: route.query,
|
||||
query: route.query, // 保留查询参数
|
||||
},
|
||||
},
|
||||
{ default: () => '抽奖' },
|
||||
{ default: () => '弹幕抽奖' },
|
||||
),
|
||||
key: 'open-live-lottery',
|
||||
icon: renderIcon(Lottery24Filled),
|
||||
@@ -67,7 +74,7 @@ const menuOptions = [
|
||||
query: route.query,
|
||||
},
|
||||
},
|
||||
{ default: () => '点歌' },
|
||||
{ default: () => '弹幕点歌' }, // 优化名称
|
||||
),
|
||||
key: 'open-live-live-request',
|
||||
icon: renderIcon(MusicalNote),
|
||||
@@ -82,7 +89,7 @@ const menuOptions = [
|
||||
query: route.query,
|
||||
},
|
||||
},
|
||||
{ default: () => '排队' },
|
||||
{ default: () => '弹幕排队' }, // 优化名称
|
||||
),
|
||||
key: 'open-live-queue',
|
||||
icon: renderIcon(PeopleQueue24Filled),
|
||||
@@ -97,37 +104,74 @@ const menuOptions = [
|
||||
query: route.query,
|
||||
},
|
||||
},
|
||||
{ default: () => '读弹幕' },
|
||||
{ default: () => '弹幕朗读' }, // 优化名称
|
||||
),
|
||||
key: 'open-live-speech',
|
||||
icon: renderIcon(TabletSpeaker24Filled),
|
||||
},
|
||||
]
|
||||
])
|
||||
|
||||
// -- 工具函数 --
|
||||
/**
|
||||
* 渲染 Naive UI 图标的辅助函数
|
||||
* @param icon 图标组件
|
||||
*/
|
||||
function renderIcon(icon: unknown) {
|
||||
return () => h(NIcon, null, { default: () => h(icon as any) })
|
||||
}
|
||||
const danmakuClientError = ref<string>()
|
||||
|
||||
// -- 主题切换逻辑 --
|
||||
const isDarkValue = computed({
|
||||
get: () => themeType.value === ThemeType.Dark || (themeType.value === ThemeType.Auto && isDarkMode.value),
|
||||
set: (value) => {
|
||||
themeType.value = value ? ThemeType.Dark : ThemeType.Light;
|
||||
}
|
||||
});
|
||||
|
||||
// -- 生命周期钩子 --
|
||||
onMounted(async () => {
|
||||
// 1. 从路由查询参数解析认证信息
|
||||
authInfo.value = route.query as unknown as AuthInfo
|
||||
|
||||
// 2. 检查是否存在必要的 Code 参数
|
||||
if (authInfo.value?.Code) {
|
||||
danmakuClient.initOpenlive(authInfo.value)
|
||||
try {
|
||||
// 3. 初始化开放平台弹幕客户端
|
||||
await danmakuClient.initOpenlive(authInfo.value) // 改为 await 处理可能的异步初始化
|
||||
// 可选: 初始化成功提示
|
||||
// message.success('弹幕客户端连接中...')
|
||||
} catch (error: any) {
|
||||
// 4. 处理初始化错误
|
||||
console.error("Danmaku client initialization failed:", error);
|
||||
danmakuClientError.value = `弹幕客户端初始化失败: ${error.message || '未知错误'}`;
|
||||
message.error(danmakuClientError.value);
|
||||
}
|
||||
} else {
|
||||
message.error('你不是从幻星平台访问此页面, 或未提供对应参数, 无法使用此功能')
|
||||
// 5. 如果缺少 Code, 显示错误信息
|
||||
message.error('无效访问: 缺少必要的认证参数 (Code)。请通过幻星平台获取链接。')
|
||||
// authInfo 清空, 触发 v-if 显示错误页
|
||||
authInfo.value = undefined;
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
// onUnmounted 清理 (如果需要)
|
||||
// onUnmounted(() => {
|
||||
// danmakuClient.dispose(); // 示例: 如果有清理逻辑
|
||||
// })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 情况一: 缺少认证信息, 显示错误提示页 -->
|
||||
<NLayoutContent
|
||||
v-if="!authInfo?.Code"
|
||||
style="height: 100vh"
|
||||
style="height: 100vh; display: flex; align-items: center; justify-content: center;"
|
||||
content-style="padding: 24px;"
|
||||
>
|
||||
<NResult
|
||||
status="error"
|
||||
title="无效访问"
|
||||
description="请确保您是通过正确的幻星平台 H5 插件链接访问此页面。"
|
||||
>
|
||||
<template #footer>
|
||||
请前往
|
||||
@@ -140,60 +184,91 @@ onUnmounted(() => {
|
||||
>
|
||||
幻星平台 | VTsuru
|
||||
</NButton>
|
||||
并点击 获取 , 再点击 获取 H5 插件链接来获取可用链接
|
||||
<br>
|
||||
或者直接在那个页面用也可以, 虽然并不推荐
|
||||
获取 H5 插件链接。
|
||||
</template>
|
||||
</NResult>
|
||||
</NLayoutContent>
|
||||
|
||||
<!-- 情况二: 存在认证信息, 显示主布局 -->
|
||||
<NLayout
|
||||
v-else
|
||||
style="height: 100vh"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<!-- 顶部导航栏 -->
|
||||
<NLayoutHeader
|
||||
style="height: 45px; padding: 5px 15px 5px 15px"
|
||||
style="height: 60px; display: flex; align-items: center; padding: 0 20px;"
|
||||
bordered
|
||||
>
|
||||
<NPageHeader :subtitle="($route.meta.title as string) ?? ''">
|
||||
<template #extra>
|
||||
<NSpace align="center">
|
||||
<NTag :type="danmakuClient.connected ? 'success' : 'warning'">
|
||||
{{ danmakuClient.connected ? `已连接 | ${danmakuClient.authInfo?.anchor_info?.uname}` : '未连接' }}
|
||||
</NTag>
|
||||
<NSwitch
|
||||
:default-value="!isDarkMode"
|
||||
@update:value="(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark)
|
||||
"
|
||||
<!-- 使用 NPageHeader 增强语义和结构 -->
|
||||
<NPageHeader style="width: 100%;">
|
||||
<!-- 标题区域 -->
|
||||
<template #title>
|
||||
<NButton
|
||||
text
|
||||
style="text-decoration: none;"
|
||||
@click="router.push({ name: 'open-live-index', query: route.query })"
|
||||
>
|
||||
<NText
|
||||
strong
|
||||
style="font-size: 1.5rem; line-height: 1;"
|
||||
type="primary"
|
||||
>
|
||||
<!-- 网站/应用 Logo 或名称 -->
|
||||
<img
|
||||
src="/favicon.ico"
|
||||
alt="VTsuru Logo"
|
||||
style="height: 24px; vertical-align: middle; margin-right: 8px;"
|
||||
> <!-- 可选: 添加 Logo -->
|
||||
VTsuru 开放平台
|
||||
</NText>
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<!-- 副标题/当前页面信息 -->
|
||||
<template #subtitle>
|
||||
<NText depth="3">
|
||||
{{ $route.meta.title as string ?? '功能模块' }}
|
||||
</NText>
|
||||
</template>
|
||||
|
||||
<!-- 右侧额外操作区域 -->
|
||||
<template #extra>
|
||||
<NSpace
|
||||
align="center"
|
||||
:size="20"
|
||||
>
|
||||
<!-- 连接状态指示 -->
|
||||
<NTag
|
||||
:type="danmakuClient.connected ? 'success' : 'warning'"
|
||||
round
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="danmakuClient.connected ? Sunny : Moon" /> <!-- 示例图标 -->
|
||||
</template>
|
||||
{{ danmakuClient.connected ? `已连接: ${danmakuClient.authInfo?.anchor_info?.uname ?? '主播'}` : '连接中...' }}
|
||||
</NTag>
|
||||
<!-- 主题切换开关 -->
|
||||
<NSwitch v-model:value="isDarkValue">
|
||||
<template #checked>
|
||||
<NIcon :component="Sunny" />
|
||||
<NIcon :component="Moon" />
|
||||
</template>
|
||||
<template #unchecked>
|
||||
<NIcon :component="Moon" />
|
||||
<NIcon :component="Sunny" />
|
||||
</template>
|
||||
</NSwitch>
|
||||
</NSpace>
|
||||
</template>
|
||||
<template #title>
|
||||
<NButton
|
||||
text
|
||||
tag="a"
|
||||
@click="$router.push({ name: 'open-live-index', query: $route.query })"
|
||||
>
|
||||
<NText
|
||||
strong
|
||||
style="font-size: 1.4rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-justify: auto"
|
||||
>
|
||||
VTSURU | 开放平台
|
||||
</NText>
|
||||
</NButton>
|
||||
</template>
|
||||
</NPageHeader>
|
||||
</NLayoutHeader>
|
||||
|
||||
<!-- 主体内容区域 (包含侧边栏和内容) -->
|
||||
<NLayout
|
||||
has-sider
|
||||
style="height: calc(100vh - 45px - 30px)"
|
||||
style="height: calc(100vh - 60px - 40px)"
|
||||
>
|
||||
<!-- 左侧导航栏 -->
|
||||
<NLayoutSider
|
||||
ref="sider"
|
||||
bordered
|
||||
@@ -225,7 +300,7 @@ onUnmounted(() => {
|
||||
}"
|
||||
/>
|
||||
<NEllipsis
|
||||
v-if="width > 100"
|
||||
v-if="siderWidth > 100"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
<NText strong>
|
||||
@@ -243,7 +318,7 @@ onUnmounted(() => {
|
||||
/>
|
||||
<NSpace justify="center">
|
||||
<NText
|
||||
v-if="width > 150"
|
||||
v-if="siderWidth > 150"
|
||||
depth="3"
|
||||
>
|
||||
有更多功能建议请
|
||||
@@ -257,60 +332,118 @@ onUnmounted(() => {
|
||||
</NText>
|
||||
</NSpace>
|
||||
</NLayoutSider>
|
||||
|
||||
<!-- 右侧主内容区域 -->
|
||||
<NLayoutContent
|
||||
style="height: 100%; padding: 10px"
|
||||
style="height: 100%;"
|
||||
content-style="padding: 15px; height: 100%;"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<!-- 弹幕客户端错误提示 -->
|
||||
<NAlert
|
||||
v-if="danmakuClientError"
|
||||
type="error"
|
||||
title="无法启动弹幕客户端"
|
||||
title="弹幕客户端错误"
|
||||
closable
|
||||
style="margin-bottom: 15px;"
|
||||
@close="danmakuClientError = undefined"
|
||||
>
|
||||
{{ danmakuClientError }}
|
||||
</NAlert>
|
||||
<RouterView
|
||||
v-if="danmakuClient.authInfo"
|
||||
v-slot="{ Component }"
|
||||
>
|
||||
<KeepAlive>
|
||||
|
||||
<!-- 路由视图: 根据认证状态显示不同内容 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<!-- 情况一: 认证信息加载中或连接中 -->
|
||||
<div
|
||||
v-if="!danmakuClient.authInfo && !danmakuClientError"
|
||||
style="display: flex; justify-content: center; align-items: center; height: 80%;"
|
||||
>
|
||||
<NSpin size="large">
|
||||
<template #description>
|
||||
正在加载主播信息并连接服务...
|
||||
</template>
|
||||
</NSpin>
|
||||
</div>
|
||||
<!-- 情况二: 加载/连接成功, 渲染对应页面 -->
|
||||
<KeepAlive v-else-if="Component && danmakuClient.authInfo">
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
:room-info="danmakuClient.authInfo"
|
||||
:code="authInfo.Code"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<!-- 情况三: 组件无法渲染或其他错误 (理论上不应发生, 但作为后备) -->
|
||||
<NResult
|
||||
v-else-if="!danmakuClientError"
|
||||
status="warning"
|
||||
title="页面加载失败"
|
||||
description="无法加载当前功能模块,请尝试刷新或联系开发者。"
|
||||
/>
|
||||
</RouterView>
|
||||
<template v-else>
|
||||
{{ }}
|
||||
<NAlert
|
||||
type="info"
|
||||
title="正在请求弹幕客户端认证信息..."
|
||||
>
|
||||
<NSpin show />
|
||||
</NAlert>
|
||||
</template>
|
||||
<NBackTop />
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<NBackTop
|
||||
:right="40"
|
||||
:bottom="60"
|
||||
/>
|
||||
</NLayoutContent>
|
||||
</NLayout>
|
||||
|
||||
<!-- 底部信息栏 -->
|
||||
<NLayoutFooter
|
||||
style="height: 30px"
|
||||
style="height: 40px; display: flex; align-items: center; justify-content: center; padding: 0 20px;"
|
||||
bordered
|
||||
>
|
||||
<NSpace
|
||||
justify="center"
|
||||
align="center"
|
||||
style="height: 100%"
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
© {{ new Date().getFullYear() }} <!-- 动态年份 -->
|
||||
<NButton
|
||||
text
|
||||
tag="a"
|
||||
href="/"
|
||||
href="https://vtsuru.live"
|
||||
target="_blank"
|
||||
type="info"
|
||||
type="primary"
|
||||
style="margin-left: 5px;"
|
||||
>
|
||||
vtsuru.live
|
||||
</NButton>
|
||||
</NSpace>
|
||||
- 由 VTsuru 提供支持
|
||||
</NText>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 添加过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.n-pageheader-wrapper {
|
||||
width: 100% !important;
|
||||
}
|
||||
/* 优化 NPageHeader 在窄屏幕下的表现 (可选) */
|
||||
@media (max-width: 768px) {
|
||||
.n-page-header-wrapper {
|
||||
padding: 0 10px !important; /* 减少内边距 */
|
||||
}
|
||||
.n-page-header__title {
|
||||
font-size: 1.2rem !important; /* 缩小标题字号 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保 NLayoutContent 的内边距生效 */
|
||||
:deep(.n-layout-scroll-container) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
VideoAdd20Filled,
|
||||
WindowWrench20Filled,
|
||||
} from '@vicons/fluent';
|
||||
import { Chatbox, Home, Moon, MusicalNote, Sunny } from '@vicons/ionicons5';
|
||||
import { BrowsersOutline, Chatbox, Home, Moon, MusicalNote, Sunny } from '@vicons/ionicons5';
|
||||
import { useElementSize, useStorage } from '@vueuse/core';
|
||||
import {
|
||||
MenuOption,
|
||||
@@ -491,6 +491,36 @@
|
||||
:auto-focus="false"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<NAlert
|
||||
type="info"
|
||||
style="border-radius: 8px;"
|
||||
>
|
||||
<NFlex
|
||||
vertical
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<div style="text-align: center;">
|
||||
如果你不是主播且不发送棉花糖(提问)的话则不需要注册登录
|
||||
</div>
|
||||
<NFlex
|
||||
justify="center"
|
||||
style="width: 100%; margin-top: 8px;"
|
||||
>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="$router.push({ name: 'bili-user'})"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="BrowsersOutline" />
|
||||
</template>
|
||||
前往 Bilibili 认证用户主页
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NAlert>
|
||||
<br>
|
||||
<!-- 异步加载注册登录组件,优化初始加载性能 -->
|
||||
<RegisterAndLogin @close="registerAndLoginModalVisiable = false" />
|
||||
</NModal>
|
||||
|
||||
@@ -34,26 +34,10 @@ import {
|
||||
NTime,
|
||||
NUl,
|
||||
useMessage,
|
||||
NInfiniteScroll,
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// 事件类型枚举
|
||||
enum EventType {
|
||||
Guard,
|
||||
SC,
|
||||
}
|
||||
|
||||
// 事件数据模型接口
|
||||
interface EventModel {
|
||||
type: EventType
|
||||
name: string
|
||||
uid: number
|
||||
msg: string
|
||||
time: number
|
||||
num: number
|
||||
price: number
|
||||
uface: string
|
||||
}
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { EventDataTypes, EventModel } from '@/api/api-models'
|
||||
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
@@ -76,45 +60,92 @@ const rangeShortcuts = {
|
||||
|
||||
// 响应式状态
|
||||
const selectedDate = ref<[number, number]>([rangeShortcuts.本月()[0], rangeShortcuts.本月()[1]])
|
||||
const selectedType = ref(EventType.Guard)
|
||||
const events = ref<EventModel[]>(await get())
|
||||
const isLoading = ref(false)
|
||||
const selectedType = ref(EventDataTypes.Guard)
|
||||
const events = ref<EventModel[]>([]) // 初始为空数组
|
||||
const isLoading = ref(false) // 用于初始加载
|
||||
const isLoadingMore = ref(false) // 用于无限滚动加载
|
||||
const displayMode = ref<'grid' | 'column'>('grid')
|
||||
const exportType = ref<'json' | 'csv'>('csv') // 移除了未实现的xml选项
|
||||
const exportType = ref<'json' | 'csv'>('csv')
|
||||
const offset = ref(0) // 当前偏移量
|
||||
const limit = ref(20) // 每次加载数量
|
||||
const hasMore = ref(true) // 是否还有更多数据
|
||||
|
||||
// 根据类型过滤事件
|
||||
// 根据类型过滤事件 - 这个计算属性现在可能只显示当前已加载的事件
|
||||
// 如果需要导出 *所有* 选定日期/类型的数据,导出逻辑需要调整
|
||||
const selectedEvents = computed(() => {
|
||||
return events.value.filter((e) => e.type == selectedType.value)
|
||||
})
|
||||
|
||||
// 获取事件数据
|
||||
async function onDateChange() {
|
||||
isLoading.value = true
|
||||
const data = await get()
|
||||
events.value = data
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// API请求获取数据
|
||||
async function get() {
|
||||
// API请求获取数据 - 修改为支持分页
|
||||
async function get(currentOffset: number, currentLimit: number) {
|
||||
try {
|
||||
const data = await QueryGetAPI<EventModel[]>(EVENT_API_URL + 'get', {
|
||||
start: selectedDate.value[0],
|
||||
end: selectedDate.value[1],
|
||||
offset: currentOffset, // 添加 offset 参数
|
||||
limit: currentLimit, // 添加 limit 参数
|
||||
})
|
||||
if (data.code == 200) {
|
||||
message.success('已获取数据')
|
||||
return new List(data.data).OrderByDescending((d) => d.time).ToArray()
|
||||
message.success(`成功获取 ${data.data.length} 条数据`) // 调整提示
|
||||
return data.data // 直接返回数据数组
|
||||
} else {
|
||||
message.error('获取数据失败: ' + data.message)
|
||||
return []
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取数据失败')
|
||||
message.error('获取数据失败: ' + (err as Error).message) // 提供更详细的错误信息
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 封装的数据获取函数
|
||||
async function fetchData(isInitialLoad = false) {
|
||||
if (isLoading.value || isLoadingMore.value) return // 防止重复加载
|
||||
|
||||
if (isInitialLoad) {
|
||||
isLoading.value = true
|
||||
offset.value = 0 // 重置偏移量
|
||||
events.value = [] // 清空现有事件
|
||||
hasMore.value = true // 假设有更多数据
|
||||
} else {
|
||||
isLoadingMore.value = true
|
||||
}
|
||||
|
||||
const currentOffset = offset.value
|
||||
const fetchedData = await get(currentOffset, limit.value)
|
||||
|
||||
if (fetchedData.length > 0) {
|
||||
// 使用 Linqts 进行排序后追加或设置
|
||||
const sortedData = new List(fetchedData).OrderByDescending((d) => d.time).ToArray()
|
||||
events.value = isInitialLoad ? sortedData : [...events.value, ...sortedData]
|
||||
offset.value += fetchedData.length // 更新偏移量
|
||||
hasMore.value = fetchedData.length === limit.value // 如果返回的数量等于请求的数量,则可能还有更多
|
||||
} else {
|
||||
hasMore.value = false // 没有获取到数据,说明没有更多了
|
||||
}
|
||||
|
||||
if (isInitialLoad) {
|
||||
isLoading.value = false
|
||||
} else {
|
||||
isLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 日期或类型变化时重新加载
|
||||
async function onFilterChange() {
|
||||
await fetchData(true) // 初始加载
|
||||
}
|
||||
|
||||
// 无限滚动加载更多
|
||||
async function loadMore() {
|
||||
if (!hasMore.value || isLoadingMore.value || isLoading.value) return
|
||||
console.log('Loading more...') // 调试信息
|
||||
await fetchData(false)
|
||||
}
|
||||
|
||||
// 监视日期和类型变化
|
||||
watch([selectedDate, selectedType], onFilterChange, { immediate: true }) // 初始加载数据
|
||||
|
||||
// 获取SC颜色
|
||||
function GetSCColor(price: number): string {
|
||||
if (price === 0) return `#2a60b2`
|
||||
@@ -139,8 +170,11 @@ function GetGuardColor(price: number | null | undefined): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 导出数据功能
|
||||
// 导出数据功能 - 注意:这现在只导出已加载的数据
|
||||
function exportData() {
|
||||
if(hasMore.value) {
|
||||
message.warning('当前导出的是已加载的部分数据,并非所有数据。')
|
||||
}
|
||||
let text = ''
|
||||
const fileName = generateExportFileName()
|
||||
|
||||
@@ -154,7 +188,7 @@ function exportData() {
|
||||
selectedEvents.value.map((v) => ({
|
||||
type: v.type,
|
||||
time: format(v.time, 'yyyy-MM-dd HH:mm:ss'),
|
||||
name: v.name,
|
||||
name: v.uname,
|
||||
uId: v.uid,
|
||||
num: v.num,
|
||||
price: v.price,
|
||||
@@ -202,7 +236,7 @@ function objectsToCSV(arr: any[]) {
|
||||
<NCard
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
>
|
||||
>
|
||||
<template v-if="accountInfo?.isBiliVerified">
|
||||
<!-- 日期选择和类型选择区域 -->
|
||||
<NSpace
|
||||
@@ -216,23 +250,19 @@ function objectsToCSV(arr: any[]) {
|
||||
:shortcuts="rangeShortcuts"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
@update:value="onDateChange"
|
||||
:disabled="isLoading || isLoadingMore"
|
||||
/>
|
||||
<NRadioGroup v-model:value="selectedType">
|
||||
<NRadioButton :value="EventType.Guard">
|
||||
<NRadioGroup
|
||||
v-model:value="selectedType"
|
||||
:disabled="isLoading || isLoadingMore"
|
||||
>
|
||||
<NRadioButton :value="EventDataTypes.Guard">
|
||||
舰长
|
||||
</NRadioButton>
|
||||
<NRadioButton :value="EventType.SC">
|
||||
<NRadioButton :value="EventDataTypes.SC">
|
||||
Superchat
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="isLoading"
|
||||
@click="onDateChange"
|
||||
>
|
||||
刷新
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<br>
|
||||
|
||||
@@ -255,18 +285,28 @@ function objectsToCSV(arr: any[]) {
|
||||
</NRadioGroup>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="selectedEvents.length === 0"
|
||||
:disabled="selectedEvents.length === 0 || isLoading || isLoadingMore"
|
||||
@click="exportData"
|
||||
>
|
||||
导出
|
||||
导出{{ hasMore ? ' (已加载部分)' : ' (全部)' }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NText
|
||||
v-if="hasMore && selectedEvents.length > 0"
|
||||
type="warning"
|
||||
style="font-size: smaller; display: block; margin-top: 5px;"
|
||||
>
|
||||
当前仅显示已加载的部分数据,滚动到底部可加载更多。导出功能也仅导出已加载数据。
|
||||
</NText>
|
||||
</NCard>
|
||||
|
||||
<NDivider> 共 {{ selectedEvents.length }} 条 </NDivider>
|
||||
<NDivider>
|
||||
共加载 {{ selectedEvents.length }} 条 {{ hasMore ? '(滚动加载更多...)' : '' }}
|
||||
</NDivider>
|
||||
|
||||
<!-- 数据展示区域 -->
|
||||
<NSpin :show="isLoading">
|
||||
<!-- 主 Spinner 只在初始加载时显示 -->
|
||||
<!-- 显示模式切换 -->
|
||||
<NRadioGroup
|
||||
v-model:value="displayMode"
|
||||
@@ -290,85 +330,122 @@ function objectsToCSV(arr: any[]) {
|
||||
>
|
||||
<!-- 网格视图 -->
|
||||
<div v-if="displayMode == 'grid'">
|
||||
<NGrid
|
||||
cols="1 500:2 800:3 1000:4 1200:5"
|
||||
:x-gap="12"
|
||||
:y-gap="8"
|
||||
<NInfiniteScroll
|
||||
:distance="100"
|
||||
:disabled="isLoadingMore || !hasMore || isLoading"
|
||||
@load="loadMore"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="item in selectedEvents"
|
||||
:key="item.time"
|
||||
<NGrid
|
||||
cols="1 500:2 800:3 1000:4 1200:5"
|
||||
:x-gap="12"
|
||||
:y-gap="8"
|
||||
style="min-height: 200px;"
|
||||
>
|
||||
<NCard
|
||||
size="small"
|
||||
:style="`height: ${selectedType == EventType.Guard ? '175px' : '220px'}`"
|
||||
embedded
|
||||
hoverable
|
||||
<NGridItem
|
||||
v-for="item in selectedEvents"
|
||||
:key="item.time + '_' + item.uid + '_' + item.price"
|
||||
>
|
||||
<NCard
|
||||
size="small"
|
||||
:style="`height: ${selectedType == EventDataTypes.Guard ? '175px' : '220px'}`"
|
||||
embedded
|
||||
hoverable
|
||||
>
|
||||
<NSpace
|
||||
align="center"
|
||||
vertical
|
||||
:size="5"
|
||||
>
|
||||
<NAvatar
|
||||
round
|
||||
lazy
|
||||
borderd
|
||||
:size="64"
|
||||
:src="item.uid ? AVATAR_URL + item.uid : item.uface"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)"
|
||||
/>
|
||||
<NSpace>
|
||||
<NTag
|
||||
v-if="selectedType == EventType.Guard"
|
||||
size="tiny"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ item.msg }}
|
||||
</NTag>
|
||||
<NTag
|
||||
size="tiny"
|
||||
round
|
||||
:color="{
|
||||
color: selectedType == EventType.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
||||
textColor: 'white',
|
||||
borderColor: isDarkMode ? 'white' : '#00000000',
|
||||
}"
|
||||
>
|
||||
{{ item.price }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
<NText>
|
||||
{{ item.name }}
|
||||
</NText>
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: small"
|
||||
<NSpace
|
||||
align="center"
|
||||
vertical
|
||||
:size="5"
|
||||
>
|
||||
<NTime :time="item.time" />
|
||||
</NText>
|
||||
<NEllipsis v-if="selectedType == EventType.SC">
|
||||
{{ item.msg }}
|
||||
</NEllipsis>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
<NAvatar
|
||||
round
|
||||
lazy
|
||||
borderd
|
||||
:size="64"
|
||||
:src="item.uid ? AVATAR_URL + item.uid : item.uface"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)"
|
||||
/>
|
||||
<NSpace>
|
||||
<NTag
|
||||
v-if="selectedType == EventDataTypes.Guard"
|
||||
size="tiny"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ item.msg }}
|
||||
</NTag>
|
||||
<NTag
|
||||
size="tiny"
|
||||
round
|
||||
:color="{
|
||||
color: selectedType == EventDataTypes.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
||||
textColor: 'white',
|
||||
borderColor: isDarkMode ? 'white' : '#00000000',
|
||||
}"
|
||||
>
|
||||
{{ item.price }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
<NText
|
||||
:depth="1"
|
||||
style="font-weight: 500;"
|
||||
>
|
||||
<!-- 用户名加粗一点 -->
|
||||
{{ item.uname }}
|
||||
</NText>
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: small"
|
||||
>
|
||||
<NTime :time="item.time" />
|
||||
</NText>
|
||||
<NEllipsis
|
||||
v-if="selectedType == EventDataTypes.SC"
|
||||
:line-clamp="3"
|
||||
>
|
||||
<!-- SC 消息限制行数 -->
|
||||
{{ item.msg }}
|
||||
</NEllipsis>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
<!-- 加载更多指示器 -->
|
||||
<div
|
||||
v-if="isLoadingMore"
|
||||
style="text-align: center; padding: 10px;"
|
||||
>
|
||||
<NSpin size="small" />
|
||||
<NText
|
||||
depth="3"
|
||||
style="margin-left: 5px;"
|
||||
>
|
||||
加载中...
|
||||
</NText>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hasMore && !isLoading && selectedEvents.length > 0"
|
||||
style="text-align: center; padding: 10px;"
|
||||
>
|
||||
<NText depth="3">
|
||||
没有更多数据了
|
||||
</NText>
|
||||
</div>
|
||||
</NInfiniteScroll>
|
||||
</div>
|
||||
|
||||
<!-- 表格视图 -->
|
||||
<NTable v-else>
|
||||
<!-- 表格视图 (未应用无限滚动) -->
|
||||
<NTable v-else-if="!isLoading && selectedEvents.length > 0">
|
||||
<!-- 添加 v-else-if 避免初始加载时显示空表格 -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>UId</th>
|
||||
<th>时间</th>
|
||||
<th v-if="selectedType == EventType.Guard">
|
||||
<th v-if="selectedType == EventDataTypes.Guard">
|
||||
类型
|
||||
</th>
|
||||
<th>价格</th>
|
||||
<th v-if="selectedType == EventType.SC">
|
||||
<th v-if="selectedType == EventDataTypes.SC">
|
||||
内容
|
||||
</th>
|
||||
</tr>
|
||||
@@ -376,20 +453,24 @@ function objectsToCSV(arr: any[]) {
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in selectedEvents"
|
||||
:key="item.time"
|
||||
:key="item.time + '_' + item.uid + '_' + item.price"
|
||||
>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.uname }}</td>
|
||||
<td>{{ item.uid }}</td>
|
||||
<td>
|
||||
<NTime :time="item.time" />
|
||||
<NTime
|
||||
:time="item.time"
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
/> <!-- 指定格式 -->
|
||||
</td>
|
||||
<td v-if="selectedType == EventType.Guard">
|
||||
<td v-if="selectedType == EventDataTypes.Guard">
|
||||
{{ item.msg }}
|
||||
</td>
|
||||
<td>
|
||||
<NTag
|
||||
size="small"
|
||||
:color="{
|
||||
color: selectedType == EventType.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
||||
color: selectedType == EventDataTypes.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
||||
textColor: 'white',
|
||||
borderColor: isDarkMode ? 'white' : '#00000000',
|
||||
}"
|
||||
@@ -397,7 +478,7 @@ function objectsToCSV(arr: any[]) {
|
||||
{{ item.price }}
|
||||
</NTag>
|
||||
</td>
|
||||
<td v-if="selectedType == EventType.SC">
|
||||
<td v-if="selectedType == EventDataTypes.SC">
|
||||
<NEllipsis style="max-width: 300px">
|
||||
{{ item.msg }}
|
||||
</NEllipsis>
|
||||
@@ -405,6 +486,14 @@ function objectsToCSV(arr: any[]) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</NTable>
|
||||
<!-- 初始加载时或无数据时的提示 -->
|
||||
<NAlert
|
||||
v-else-if="!isLoading && selectedEvents.length === 0"
|
||||
title="无数据"
|
||||
type="info"
|
||||
>
|
||||
在选定的时间范围和类型内没有找到数据。
|
||||
</NAlert>
|
||||
</Transition>
|
||||
</NSpin>
|
||||
</template>
|
||||
@@ -474,4 +563,9 @@ function objectsToCSV(arr: any[]) {
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 网格卡片样式微调 */
|
||||
.n-card {
|
||||
transition: box-shadow 0.3s ease; /* 平滑阴影过渡 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,13 +13,17 @@ const accountInfo = useAccount()
|
||||
|
||||
<template>
|
||||
<NDivider> 功能 </NDivider>
|
||||
<NSpace justify="center">
|
||||
<NSpace
|
||||
justify="center"
|
||||
:size="[20, 20]"
|
||||
item-style="display: flex;"
|
||||
>
|
||||
<NCard
|
||||
hoverable
|
||||
embedded
|
||||
bordered
|
||||
size="small"
|
||||
title="弹幕抽奖"
|
||||
style="width: 300px"
|
||||
style="width: 300px; flex-grow: 1;"
|
||||
>
|
||||
通过弹幕或者礼物收集用户, 并进行抽取, 允许设置多种条件
|
||||
<template #footer>
|
||||
@@ -33,10 +37,10 @@ const accountInfo = useAccount()
|
||||
</NCard>
|
||||
<NCard
|
||||
hoverable
|
||||
embedded
|
||||
bordered
|
||||
size="small"
|
||||
title="弹幕点播"
|
||||
style="width: 300px"
|
||||
style="width: 300px; flex-grow: 1;"
|
||||
>
|
||||
通过弹幕或者SC进行点歌或者其他的点播(比如跳舞或者点播视频之类的), 注册后可以保存和导出
|
||||
<template #footer>
|
||||
@@ -50,10 +54,10 @@ const accountInfo = useAccount()
|
||||
</NCard>
|
||||
<NCard
|
||||
hoverable
|
||||
embedded
|
||||
bordered
|
||||
size="small"
|
||||
title="弹幕排队"
|
||||
style="width: 300px"
|
||||
style="width: 300px; flex-grow: 1;"
|
||||
>
|
||||
通过发送弹幕或者礼物进行排队, 允许设置多种条件
|
||||
<template #footer>
|
||||
@@ -68,10 +72,10 @@ const accountInfo = useAccount()
|
||||
|
||||
<NCard
|
||||
hoverable
|
||||
embedded
|
||||
bordered
|
||||
size="small"
|
||||
title="读弹幕"
|
||||
style="width: 300px"
|
||||
style="width: 300px; flex-grow: 1;"
|
||||
>
|
||||
通过浏览器自带的tts服务读弹幕 (此功能需要 Chrome, Edge 等现代浏览器!)
|
||||
<template #footer>
|
||||
@@ -85,42 +89,50 @@ const accountInfo = useAccount()
|
||||
</NCard>
|
||||
</NSpace>
|
||||
<br>
|
||||
<NAlert
|
||||
<br>
|
||||
<NSpace
|
||||
v-if="accountInfo?.eventFetcherState?.online != true"
|
||||
type="warning"
|
||||
title="可用性警告"
|
||||
style="max-width: 600px; margin: 0 auto"
|
||||
justify="center"
|
||||
>
|
||||
当浏览器在后台运行时, 定时器和 Websocket 连接将受到严格限制, 这会导致弹幕接收功能无法正常工作 (详见
|
||||
<NButton
|
||||
text
|
||||
tag="a"
|
||||
href="https://developer.chrome.com/blog/background_tabs/"
|
||||
target="_blank"
|
||||
type="info"
|
||||
<NAlert
|
||||
type="warning"
|
||||
title="可用性警告"
|
||||
style="max-width: 600px; margin: 0 auto"
|
||||
>
|
||||
此文章
|
||||
</NButton>), 虽然本站已经针对此问题做出了处理, 一般情况下即使掉线了也会重连, 不过还是有可能会遗漏事件
|
||||
<br>
|
||||
为避免这种情况, 建议注册本站账后使用
|
||||
<NButton
|
||||
type="primary"
|
||||
text
|
||||
size="tiny"
|
||||
tag="a"
|
||||
href="https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs"
|
||||
target="_blank"
|
||||
>
|
||||
VtsuruEventFetcher
|
||||
</NButton>, 否则请在使用功能时尽量保持网页在前台运行
|
||||
</NAlert>
|
||||
当浏览器在后台运行时, 定时器和 Websocket 连接将受到严格限制, 这会导致弹幕接收功能无法正常工作 (详见
|
||||
<NButton
|
||||
text
|
||||
tag="a"
|
||||
href="https://developer.chrome.com/blog/background_tabs/"
|
||||
target="_blank"
|
||||
type="info"
|
||||
>
|
||||
此文章
|
||||
</NButton>), 虽然本站已经针对此问题做出了处理, 一般情况下即使掉线了也会重连, 不过还是有可能会遗漏事件
|
||||
<br>
|
||||
为避免这种情况, 建议注册本站账后使用
|
||||
<NButton
|
||||
type="primary"
|
||||
text
|
||||
size="tiny"
|
||||
tag="a"
|
||||
href="https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs"
|
||||
target="_blank"
|
||||
>
|
||||
VtsuruEventFetcher
|
||||
</NButton>, 否则请在使用功能时尽量保持网页在前台运行
|
||||
</NAlert>
|
||||
</NSpace>
|
||||
<br v-if="accountInfo?.eventFetcherState?.online != true">
|
||||
<NDivider> 还有更多 </NDivider>
|
||||
<NSpace
|
||||
justify="center"
|
||||
align="center"
|
||||
vertical
|
||||
>
|
||||
舰长积分、动态抽奖、视频征集、歌单、棉花糖、日程表...
|
||||
<p style="font-size: 1.1em; color: var(--n-text-color-2)">
|
||||
舰长积分、动态抽奖、视频征集、歌单、棉花糖、日程表...
|
||||
</p>
|
||||
<p>
|
||||
详见
|
||||
<NButton
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
useMessage,
|
||||
useNotification,
|
||||
} from 'naive-ui';
|
||||
import { computed, h, onActivated, onDeactivated, onMounted, onUnmounted, ref, VNodeChild } from 'vue';
|
||||
import { computed, h, onActivated, onDeactivated, onMounted, onUnmounted, ref, VNodeChild, CSSProperties } from 'vue';
|
||||
// import { useRoute } from 'vue-router' // 未使用
|
||||
import QueueOBS from '../obs/QueueOBS.vue';
|
||||
import { copyToClipboard } from '@/Utils';
|
||||
@@ -971,6 +971,46 @@
|
||||
client.offEvent('gift', onGetGift);
|
||||
dispose();
|
||||
});
|
||||
|
||||
// --- 辅助函数 ---
|
||||
function getIndexStyle(status: QueueStatus): CSSProperties {
|
||||
// 基础颜色定义 - 扁平化风格
|
||||
let backgroundColor;
|
||||
|
||||
// 根据状态设置不同的颜色
|
||||
switch (status) {
|
||||
case QueueStatus.Progressing:
|
||||
backgroundColor = '#18a058'; // 处理中 - 绿色
|
||||
break;
|
||||
case QueueStatus.Waiting:
|
||||
backgroundColor = '#2080f0'; // 等待中 - 蓝色
|
||||
break;
|
||||
case QueueStatus.Finish:
|
||||
backgroundColor = '#86909c'; // 已完成 - 灰色
|
||||
break;
|
||||
case QueueStatus.Cancel:
|
||||
backgroundColor = '#d03050'; // 已取消 - 红色
|
||||
break;
|
||||
default:
|
||||
backgroundColor = '#2080f0'; // 默认 - 蓝色
|
||||
}
|
||||
|
||||
const style: CSSProperties = {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold',
|
||||
width: '24px', // 确保宽高一致以形成完美圆形
|
||||
height: '24px', // 保持一致的宽高
|
||||
borderRadius: '50%', // 圆形
|
||||
color: 'white',
|
||||
fontSize: '13px', // 适当调整字体大小
|
||||
backgroundColor, // 扁平化的纯色背景
|
||||
transition: 'opacity 0.2s', // 仅保留简单的过渡效果
|
||||
};
|
||||
|
||||
return style;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1067,8 +1107,9 @@
|
||||
>
|
||||
<NSpace
|
||||
align="center"
|
||||
justify="start"
|
||||
justify="space-between"
|
||||
wrap
|
||||
:item-style="{ marginBottom: '8px' }"
|
||||
>
|
||||
<!-- 队列统计信息 -->
|
||||
<NSpace align="center">
|
||||
@@ -1170,7 +1211,6 @@
|
||||
|
||||
<!-- 队列列表 -->
|
||||
<NSpin :show="isLoading && originQueue.length === 0">
|
||||
<!-- 初始加载时显示 Spin -->
|
||||
<NList
|
||||
v-if="queue.length > 0"
|
||||
hoverable
|
||||
@@ -1195,18 +1235,21 @@
|
||||
:wrap="false"
|
||||
>
|
||||
<!-- 左侧信息 -->
|
||||
<NSpace align="center">
|
||||
<NTag
|
||||
round
|
||||
size="small"
|
||||
:type="queueData.status == QueueStatus.Progressing ? 'success' : 'default'"
|
||||
style="min-width: 30px; text-align: center;"
|
||||
<NSpace
|
||||
align="center"
|
||||
:size="8"
|
||||
:wrap="false"
|
||||
>
|
||||
<span
|
||||
:style="getIndexStyle(queueData.status)"
|
||||
class="queue-index"
|
||||
:class="{ 'queue-index-processing': queueData.status === QueueStatus.Progressing }"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</NTag>
|
||||
</span>
|
||||
<NText
|
||||
strong
|
||||
style="font-size: 16px; margin-right: 5px;"
|
||||
style="font-size: 16px;"
|
||||
>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
@@ -1262,7 +1305,7 @@
|
||||
<NIcon
|
||||
:component="Info24Filled"
|
||||
size="16"
|
||||
style="cursor: help; color: #aaa; margin-left: 5px;"
|
||||
style="cursor: help; color: #aaa;"
|
||||
/>
|
||||
</template>
|
||||
<NCard
|
||||
@@ -1303,8 +1346,9 @@
|
||||
<NSpace
|
||||
justify="end"
|
||||
align="center"
|
||||
:size="5"
|
||||
:size="6"
|
||||
:wrap="false"
|
||||
style="flex-shrink: 0;"
|
||||
>
|
||||
<!-- 开始/暂停处理 -->
|
||||
<NTooltip>
|
||||
@@ -1418,18 +1462,23 @@
|
||||
:bordered="false"
|
||||
style="margin-bottom: 10px;"
|
||||
>
|
||||
<NSpace align="center">
|
||||
<NInputGroup style="width: 300px">
|
||||
<NInputGroupLabel> 筛选用户 </NInputGroupLabel>
|
||||
<NInput
|
||||
v-model:value="filterName"
|
||||
clearable
|
||||
placeholder="输入用户名"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NCheckbox v-model:checked="filterNameContains">
|
||||
模糊匹配
|
||||
</NCheckbox>
|
||||
<NSpace
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<NSpace align="center">
|
||||
<NInputGroup style="width: 300px">
|
||||
<NInputGroupLabel> 筛选用户 </NInputGroupLabel>
|
||||
<NInput
|
||||
v-model:value="filterName"
|
||||
clearable
|
||||
placeholder="输入用户名"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NCheckbox v-model:checked="filterNameContains">
|
||||
模糊匹配
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
<NButton
|
||||
size="small"
|
||||
type="error"
|
||||
@@ -1461,13 +1510,21 @@
|
||||
:disabled="!configCanEdit"
|
||||
>
|
||||
<NSpin :show="isLoading">
|
||||
<NCollapse>
|
||||
<!-- 规则设置 -->
|
||||
<NCollapseItem
|
||||
<NSpace
|
||||
vertical
|
||||
:size="20"
|
||||
style="padding-top: 10px;"
|
||||
>
|
||||
<!-- 加入规则 -->
|
||||
<NCard
|
||||
size="small"
|
||||
title="加入规则"
|
||||
name="rules"
|
||||
:bordered="false"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NSpace
|
||||
vertical
|
||||
:size="12"
|
||||
>
|
||||
<NSpace align="center">
|
||||
<NInputGroup style="width: 350px">
|
||||
<NInputGroupLabel> 弹幕关键词 </NInputGroupLabel>
|
||||
@@ -1525,6 +1582,7 @@
|
||||
<NSpace
|
||||
v-if="!settings.allowAllDanmaku"
|
||||
vertical
|
||||
:size="10"
|
||||
style="margin-left: 20px;"
|
||||
>
|
||||
<NInputGroup style="width: 270px">
|
||||
@@ -1555,14 +1613,20 @@
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCollapseItem>
|
||||
</NCard>
|
||||
|
||||
<!-- 礼物设置 -->
|
||||
<NCollapseItem
|
||||
<NDivider />
|
||||
|
||||
<!-- 礼物规则 -->
|
||||
<NCard
|
||||
size="small"
|
||||
title="礼物规则"
|
||||
name="gift"
|
||||
:bordered="false"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NSpace
|
||||
vertical
|
||||
:size="12"
|
||||
>
|
||||
<NCheckbox
|
||||
v-model:checked="settings.allowGift"
|
||||
@update:checked="updateSettings"
|
||||
@@ -1572,6 +1636,7 @@
|
||||
<NSpace
|
||||
v-if="settings.allowGift"
|
||||
vertical
|
||||
:size="10"
|
||||
style="margin-left: 20px;"
|
||||
>
|
||||
<NInputGroup style="width: 250px">
|
||||
@@ -1651,12 +1716,15 @@
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCollapseItem>
|
||||
</NCard>
|
||||
|
||||
<!-- 冷却设置 -->
|
||||
<NCollapseItem
|
||||
<NDivider />
|
||||
|
||||
<!-- 冷却时间 (CD) -->
|
||||
<NCard
|
||||
size="small"
|
||||
title="冷却时间 (CD)"
|
||||
name="cooldown"
|
||||
:bordered="false"
|
||||
>
|
||||
<NCheckbox
|
||||
v-model:checked="settings.enableCooldown"
|
||||
@@ -1667,6 +1735,7 @@
|
||||
<NSpace
|
||||
v-if="settings.enableCooldown"
|
||||
vertical
|
||||
:size="10"
|
||||
style="margin-left: 20px; margin-top: 10px;"
|
||||
>
|
||||
<NInputGroup style="width: 280px">
|
||||
@@ -1702,14 +1771,20 @@
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
</NCollapseItem>
|
||||
</NCard>
|
||||
|
||||
<!-- 显示设置 -->
|
||||
<NCollapseItem
|
||||
<NDivider />
|
||||
|
||||
<!-- 显示与界面 -->
|
||||
<NCard
|
||||
size="small"
|
||||
title="显示与界面"
|
||||
name="display"
|
||||
:bordered="false"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NSpace
|
||||
vertical
|
||||
:size="12"
|
||||
>
|
||||
<NDivider
|
||||
title-placement="left"
|
||||
style="margin: 5px 0;"
|
||||
@@ -1741,11 +1816,11 @@
|
||||
其他界面设置
|
||||
</NDivider>
|
||||
<NCheckbox v-model:checked="isWarnMessageAutoClose">
|
||||
自动关闭“加入队列失败”的通知消息 (默认3秒)
|
||||
自动关闭"加入队列失败"的通知消息 (默认3秒)
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</NSpin>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
@@ -1802,14 +1877,17 @@
|
||||
style="padding-top: 100px;"
|
||||
/>
|
||||
</div>
|
||||
<NCollapse style="margin-top: 15px;">
|
||||
<NCollapse
|
||||
style="margin-top: 15px;"
|
||||
accordion
|
||||
>
|
||||
<NCollapseItem title="详细说明">
|
||||
<NUl>
|
||||
<NLi>在 OBS 中添加一个新的“浏览器”来源。</NLi>
|
||||
<NLi>将上方 URL 粘贴到“URL”栏中。</NLi>
|
||||
<NLi>在 OBS 中添加一个新的"浏览器"来源。</NLi>
|
||||
<NLi>将上方 URL 粘贴到"URL"栏中。</NLi>
|
||||
<NLi>推荐宽度设置为 280-350px,高度根据需要调整 (例如 500-700px)。</NLi>
|
||||
<NLi>可在“设置”标签页中调整 OBS 组件的显示内容。</NLi>
|
||||
<NLi>如果需要自定义样式,可以在 OBS 的“自定义 CSS”中添加覆盖样式。</NLi>
|
||||
<NLi>可在"设置"标签页中调整 OBS 组件的显示内容。</NLi>
|
||||
<NLi>如果需要自定义样式,可以在 OBS 的"自定义 CSS"中添加覆盖样式。</NLi>
|
||||
</NUl>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
@@ -1817,20 +1895,14 @@
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 移除全局动画,仅在需要的地方应用 */
|
||||
/* @keyframes loading { ... } */
|
||||
|
||||
/* 处理中状态的边框动画 */
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(103, 194, 58, 0.7);
|
||||
/* 对应 success 颜色 */
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 5px rgba(103, 194, 58, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0px rgba(103, 194, 58, 0);
|
||||
}
|
||||
@@ -1838,11 +1910,7 @@
|
||||
|
||||
/* 处理中状态的卡片左边框或标签动画 */
|
||||
.n-card[style*="border-left: 4px solid #63e2b7;"],
|
||||
/* 处理中卡片的左边框 */
|
||||
.n-tag--success[style*="animation: animated-border"]
|
||||
|
||||
/* 处理中标签 (如果应用了动画) */
|
||||
{
|
||||
.n-tag--success[style*="animation: animated-border"] {
|
||||
animation: animated-border 1.5s infinite;
|
||||
}
|
||||
|
||||
@@ -1853,9 +1921,41 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 如果需要特定列换行,可以单独设置 */
|
||||
/* .n-data-table-td[data-col-key="user.name"] { white-space: normal; } */
|
||||
/* 序号悬停效果 - 扁平化风格 */
|
||||
.queue-index:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* 移除旧的 round 动画 */
|
||||
/* @keyframes animated-border-round { ... } */
|
||||
/* 处理中状态的序号动画 - 扁平化风格 */
|
||||
.queue-index-processing {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.queue-index-processing::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #18a058;
|
||||
opacity: 0;
|
||||
animation: flat-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes flat-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user