mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 更新组件声明和优化自动操作逻辑
- 移除了旧的关注和舰长事件处理逻辑,简化了代码结构。 - 优化了定时弹幕和自动回复的处理逻辑 - 更新了数据获取逻辑,支持分页加载和无限滚动
This commit is contained in:
@@ -90,14 +90,6 @@ export function useFollowThank(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理关注事件 - 旧方式实现,用于兼容现有代码
|
|
||||||
*/
|
|
||||||
function onFollow(event: EventModel) {
|
|
||||||
// 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
|
|
||||||
console.log('关注事件处理已迁移到新的AutoActionItem结构');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理计时器
|
* 清理计时器
|
||||||
*/
|
*/
|
||||||
@@ -109,7 +101,6 @@ export function useFollowThank(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onFollow,
|
|
||||||
processFollow,
|
processFollow,
|
||||||
clearTimer
|
clearTimer
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -155,17 +155,8 @@ export function useGuardPm(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理上舰事件 - 旧方式实现,用于兼容现有代码
|
|
||||||
*/
|
|
||||||
function onGuard(event: EventModel) {
|
|
||||||
// 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
|
|
||||||
console.log('舰长事件处理已迁移到新的AutoActionItem结构');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
onGuard,
|
|
||||||
processGuard
|
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 {
|
return {
|
||||||
startScheduledDanmaku,
|
|
||||||
stopScheduledDanmaku,
|
|
||||||
processScheduledActions,
|
processScheduledActions,
|
||||||
clearTimer,
|
clearTimer,
|
||||||
remainingSeconds,
|
remainingSeconds,
|
||||||
|
|||||||
@@ -55,14 +55,12 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
|
||||||
const guardPm = useGuardPm(
|
const guardPm = useGuardPm(
|
||||||
isLive,
|
isLive,
|
||||||
roomId,
|
roomId,
|
||||||
(userId: number, message: string) => biliFunc.sendPrivateMessage(userId, message)
|
(userId: number, message: string) => biliFunc.sendPrivateMessage(userId, message)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
|
||||||
const followThank = useFollowThank(
|
const followThank = useFollowThank(
|
||||||
isLive,
|
isLive,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -70,7 +68,6 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
|
||||||
const entryWelcome = useEntryWelcome(
|
const entryWelcome = useEntryWelcome(
|
||||||
isLive,
|
isLive,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -78,14 +75,12 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
|
||||||
const autoReply = useAutoReply(
|
const autoReply = useAutoReply(
|
||||||
isLive,
|
isLive,
|
||||||
roomId,
|
roomId,
|
||||||
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
(roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore - 忽略类型错误以保持功能正常
|
|
||||||
const scheduledDanmaku = useScheduledDanmaku(
|
const scheduledDanmaku = useScheduledDanmaku(
|
||||||
isLive,
|
isLive,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -168,11 +163,11 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case TriggerType.GUARD:
|
case TriggerType.GUARD:
|
||||||
guardPm.onGuard(event);
|
guardPm.processGuard(event, autoActions.value, runtimeState.value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TriggerType.FOLLOW:
|
case TriggerType.FOLLOW:
|
||||||
followThank.onFollow(event);
|
followThank.processFollow(event, autoActions.value, runtimeState.value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TriggerType.ENTER:
|
case TriggerType.ENTER:
|
||||||
@@ -366,7 +361,7 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
if (!roomId.value) return;
|
if (!roomId.value) return;
|
||||||
|
|
||||||
// 使用专用模块处理定时发送
|
// 使用专用模块处理定时发送
|
||||||
scheduledDanmaku.startScheduledDanmaku();
|
scheduledDanmaku.processScheduledActions(autoActions.value, runtimeState.value);
|
||||||
|
|
||||||
// 同时处理自定义的定时任务
|
// 同时处理自定义的定时任务
|
||||||
const scheduledActions = autoActions.value.filter(
|
const scheduledActions = autoActions.value.filter(
|
||||||
@@ -382,37 +377,33 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
|
const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
|
||||||
|
|
||||||
const timerFunc = () => {
|
const timerFunc = () => {
|
||||||
if (!isLive.value && action.triggerConfig.onlyDuringLive) {
|
// 仅在检查时判断直播状态,不停止定时器
|
||||||
// 如果设置了仅直播时发送,且当前未直播,则跳过
|
const shouldExecute =
|
||||||
return;
|
!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);
|
runtimeState.value.scheduledTimers[action.id] = setTimeout(timerFunc, intervalSeconds * 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -465,8 +456,48 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
// 如果是定时操作,重新配置定时器
|
// 如果是定时操作,重新配置定时器
|
||||||
if (action.triggerType === TriggerType.SCHEDULED) {
|
if (action.triggerType === TriggerType.SCHEDULED) {
|
||||||
if (enabled) {
|
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]) {
|
} else if (runtimeState.value.scheduledTimers[id]) {
|
||||||
// 禁用时清理定时器
|
// 禁用时清理定时器
|
||||||
clearTimeout(runtimeState.value.scheduledTimers[id]!);
|
clearTimeout(runtimeState.value.scheduledTimers[id]!);
|
||||||
@@ -484,14 +515,14 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
// 启动所有定时发送任务
|
// 启动所有定时发送任务
|
||||||
startScheduledActions();
|
startScheduledActions();
|
||||||
|
|
||||||
// 监听直播状态变化,自动启停定时任务
|
// 不再根据直播状态停止定时任务,只在回调中判断
|
||||||
watch(isLive, (newIsLive) => {
|
/*watch(isLive, (newIsLive) => {
|
||||||
if (newIsLive) {
|
if (newIsLive) {
|
||||||
startScheduledActions();
|
startScheduledActions();
|
||||||
} else {
|
} else {
|
||||||
stopAllScheduledActions();
|
stopAllScheduledActions();
|
||||||
}
|
}
|
||||||
});
|
});*/
|
||||||
|
|
||||||
// 安全地订阅事件
|
// 安全地订阅事件
|
||||||
try {
|
try {
|
||||||
@@ -510,115 +541,6 @@ export const useAutoAction = defineStore('autoAction', () => {
|
|||||||
clearAllTimers();
|
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']
|
LabelItem: typeof import('./components/LabelItem.vue')['default']
|
||||||
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
||||||
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
||||||
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
NBadge: typeof import('naive-ui')['NBadge']
|
NBadge: typeof import('naive-ui')['NBadge']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ function InitVersionCheck() {
|
|||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
|
|
||||||
if (!path.startsWith('/obs')) {
|
if (!path.startsWith('/obs')) {
|
||||||
if (isTauri) {
|
if (isTauri()) {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ export function checkUpdateNote() {
|
|||||||
positiveText: '下次更新前不再提示',
|
positiveText: '下次更新前不再提示',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: () => {
|
||||||
savedUpdateNoteVer.value = currentUpdateNoteVer;
|
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 isBackendUsable = ref(true);
|
||||||
export const isDev = import.meta.env.MODE === 'development';
|
export const isDev = import.meta.env.MODE === 'development';
|
||||||
// @ts-ignore
|
// @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 AVATAR_URL = 'https://workers.vrp.moe/api/bilibili/avatar/';
|
||||||
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club';
|
export const FILE_BASE_URL = 'https://files.vtsuru.suki.club';
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
danmakuClientState.value = 'connected'; // 明确设置状态
|
danmakuClientState.value = 'connected'; // 明确设置状态
|
||||||
danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址
|
danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址
|
||||||
// 启动事件发送定时器 (如果之前没有启动)
|
// 启动事件发送定时器 (如果之前没有启动)
|
||||||
timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件
|
timer ??= setInterval(sendEvents, 2000); // 每 2 秒尝试发送一次事件
|
||||||
return { success: true, message: '弹幕客户端已启动' };
|
return { success: true, message: '弹幕客户端已启动' };
|
||||||
} else {
|
} else {
|
||||||
console.error(prefix.value + '弹幕客户端启动失败');
|
console.error(prefix.value + '弹幕客户端启动失败');
|
||||||
@@ -301,7 +301,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
Data: string;
|
Data: string;
|
||||||
};
|
};
|
||||||
async function onRequest(url: string, method: string, body: string, useCookie: boolean) {
|
async function onRequest(url: string, method: string, body: string, useCookie: boolean) {
|
||||||
if (!isTauri) {
|
if (!isTauri()) {
|
||||||
console.error(prefix.value + '非Tauri环境下无法处理请求: ' + url);
|
console.error(prefix.value + '非Tauri环境下无法处理请求: ' + url);
|
||||||
return {
|
return {
|
||||||
Message: '非Tauri环境',
|
Message: '非Tauri环境',
|
||||||
@@ -386,7 +386,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 批量处理事件,每次最多发送20条
|
// 批量处理事件,每次最多发送20条
|
||||||
const batchSize = 20;
|
const batchSize = 30;
|
||||||
const batch = events.slice(0, batchSize);
|
const batch = events.slice(0, batchSize);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NLayoutContent style="height: 100vh">
|
<NLayoutContent style="height: 100vh; padding: 20px 0;">
|
||||||
<NSpace
|
<NSpace
|
||||||
style="margin-top: 50px"
|
style="margin-top: 30px"
|
||||||
justify="center"
|
justify="center"
|
||||||
align="center"
|
align="center"
|
||||||
vertical
|
vertical
|
||||||
>
|
>
|
||||||
<NCard
|
<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
|
embedded
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
关于
|
<div style="font-size: 22px; font-weight: bold; padding: 8px 0;">
|
||||||
|
关于
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<NText>
|
<NText>
|
||||||
一个兴趣使然的网站
|
一个兴趣使然的网站
|
||||||
@@ -52,23 +56,174 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
|||||||
<NSpace
|
<NSpace
|
||||||
vertical
|
vertical
|
||||||
align="center"
|
align="center"
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
>
|
>
|
||||||
<span style="color: gray">
|
<span style="color: #666; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
||||||
MADE WITH ❤️ BY
|
MADE WITH <NIcon
|
||||||
|
size="18"
|
||||||
|
color="#f56c6c"
|
||||||
|
><HeartOutline /></NIcon> BY
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
tag="a"
|
tag="a"
|
||||||
href="https://space.bilibili.com/10021741"
|
href="https://space.bilibili.com/10021741"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
text
|
text
|
||||||
style=""
|
style="font-weight: bold;"
|
||||||
>
|
>
|
||||||
Megghy
|
Megghy
|
||||||
</NButton>
|
</NButton>
|
||||||
</span>
|
</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>
|
</NSpace>
|
||||||
<NDivider title-placement="left">
|
<NDivider title-placement="left">
|
||||||
赞助我
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
|
<span>赞助我</span>
|
||||||
|
</div>
|
||||||
</NDivider>
|
</NDivider>
|
||||||
<iframe
|
<iframe
|
||||||
id="afdian_leaflet_vtsuru"
|
id="afdian_leaflet_vtsuru"
|
||||||
@@ -77,10 +232,15 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
|||||||
scrolling="no"
|
scrolling="no"
|
||||||
height="200"
|
height="200"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
style="border-radius: 8px;"
|
||||||
/>
|
/>
|
||||||
<NDivider title-placement="left">
|
<NDivider title-placement="left">
|
||||||
更新日志
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
|
<span>更新日志</span>
|
||||||
|
</div>
|
||||||
</NDivider>
|
</NDivider>
|
||||||
|
<UpdateNoteContainer />
|
||||||
|
<NDivider />
|
||||||
<NTimeline>
|
<NTimeline>
|
||||||
<NTimelineItem
|
<NTimelineItem
|
||||||
type="info"
|
type="info"
|
||||||
@@ -241,9 +401,46 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
|||||||
</NTimeline>
|
</NTimeline>
|
||||||
</template>
|
</template>
|
||||||
</NCard>
|
</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>
|
</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NLayoutContent>
|
</NLayoutContent>
|
||||||
</template>
|
</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,
|
TabletSpeaker24Filled,
|
||||||
VehicleShip24Filled,
|
VehicleShip24Filled,
|
||||||
VideoAdd20Filled,
|
VideoAdd20Filled,
|
||||||
|
Mail24Filled,
|
||||||
} from '@vicons/fluent'
|
} from '@vicons/fluent'
|
||||||
import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye } from '@vicons/ionicons5'
|
import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye } from '@vicons/ionicons5'
|
||||||
import { useElementSize, useStorage } from '@vueuse/core'
|
import { useElementSize, useStorage } from '@vueuse/core'
|
||||||
@@ -561,37 +562,90 @@ onMounted(() => {
|
|||||||
</RouterView>
|
</RouterView>
|
||||||
<!-- 未验证邮箱的提示 -->
|
<!-- 未验证邮箱的提示 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NAlert type="info">
|
<NCard>
|
||||||
请进行邮箱验证
|
<NSpace
|
||||||
<br><br>
|
vertical
|
||||||
<NSpace>
|
size="large"
|
||||||
<NButton
|
align="center"
|
||||||
size="small"
|
>
|
||||||
type="info"
|
<NFlex
|
||||||
:disabled="!canResendEmail"
|
justify="center"
|
||||||
@click="resendEmail"
|
align="center"
|
||||||
|
vertical
|
||||||
>
|
>
|
||||||
重新发送验证邮件
|
<NIcon
|
||||||
</NButton>
|
size="48"
|
||||||
<NCountdown
|
color="#2080f0"
|
||||||
v-if="!canResendEmail"
|
>
|
||||||
:duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()"
|
<Mail24Filled />
|
||||||
@finish="canResendEmail = true"
|
</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
|
<NAlert
|
||||||
size="small"
|
type="warning"
|
||||||
@positive-click="logout"
|
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>
|
<template #trigger>
|
||||||
<NButton type="error">
|
<NButton secondary>
|
||||||
登出
|
<template #icon>
|
||||||
|
<NIcon>
|
||||||
|
<PersonFeedback24Filled />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
切换账号
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
确定登出?
|
确定要登出当前账号吗?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NAlert>
|
</NCard>
|
||||||
</template>
|
</template>
|
||||||
<NBackTop />
|
<NBackTop />
|
||||||
</NElement>
|
</NElement>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
VideoAdd20Filled,
|
VideoAdd20Filled,
|
||||||
WindowWrench20Filled,
|
WindowWrench20Filled,
|
||||||
} from '@vicons/fluent';
|
} 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 { useElementSize, useStorage } from '@vueuse/core';
|
||||||
import {
|
import {
|
||||||
MenuOption,
|
MenuOption,
|
||||||
@@ -491,6 +491,36 @@
|
|||||||
:auto-focus="false"
|
:auto-focus="false"
|
||||||
:mask-closable="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" />
|
<RegisterAndLogin @close="registerAndLoginModalVisiable = false" />
|
||||||
</NModal>
|
</NModal>
|
||||||
|
|||||||
@@ -34,26 +34,10 @@ import {
|
|||||||
NTime,
|
NTime,
|
||||||
NUl,
|
NUl,
|
||||||
useMessage,
|
useMessage,
|
||||||
|
NInfiniteScroll,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { EventDataTypes, EventModel } from '@/api/api-models'
|
||||||
// 事件类型枚举
|
|
||||||
enum EventType {
|
|
||||||
Guard,
|
|
||||||
SC,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件数据模型接口
|
|
||||||
interface EventModel {
|
|
||||||
type: EventType
|
|
||||||
name: string
|
|
||||||
uid: number
|
|
||||||
msg: string
|
|
||||||
time: number
|
|
||||||
num: number
|
|
||||||
price: number
|
|
||||||
uface: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -76,45 +60,92 @@ const rangeShortcuts = {
|
|||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const selectedDate = ref<[number, number]>([rangeShortcuts.本月()[0], rangeShortcuts.本月()[1]])
|
const selectedDate = ref<[number, number]>([rangeShortcuts.本月()[0], rangeShortcuts.本月()[1]])
|
||||||
const selectedType = ref(EventType.Guard)
|
const selectedType = ref(EventDataTypes.Guard)
|
||||||
const events = ref<EventModel[]>(await get())
|
const events = ref<EventModel[]>([]) // 初始为空数组
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false) // 用于初始加载
|
||||||
|
const isLoadingMore = ref(false) // 用于无限滚动加载
|
||||||
const displayMode = ref<'grid' | 'column'>('grid')
|
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(() => {
|
const selectedEvents = computed(() => {
|
||||||
return events.value.filter((e) => e.type == selectedType.value)
|
return events.value.filter((e) => e.type == selectedType.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取事件数据
|
// API请求获取数据 - 修改为支持分页
|
||||||
async function onDateChange() {
|
async function get(currentOffset: number, currentLimit: number) {
|
||||||
isLoading.value = true
|
|
||||||
const data = await get()
|
|
||||||
events.value = data
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// API请求获取数据
|
|
||||||
async function get() {
|
|
||||||
try {
|
try {
|
||||||
const data = await QueryGetAPI<EventModel[]>(EVENT_API_URL + 'get', {
|
const data = await QueryGetAPI<EventModel[]>(EVENT_API_URL + 'get', {
|
||||||
start: selectedDate.value[0],
|
start: selectedDate.value[0],
|
||||||
end: selectedDate.value[1],
|
end: selectedDate.value[1],
|
||||||
|
offset: currentOffset, // 添加 offset 参数
|
||||||
|
limit: currentLimit, // 添加 limit 参数
|
||||||
})
|
})
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
message.success('已获取数据')
|
message.success(`成功获取 ${data.data.length} 条数据`) // 调整提示
|
||||||
return new List(data.data).OrderByDescending((d) => d.time).ToArray()
|
return data.data // 直接返回数据数组
|
||||||
} else {
|
} else {
|
||||||
message.error('获取数据失败: ' + data.message)
|
message.error('获取数据失败: ' + data.message)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('获取数据失败')
|
message.error('获取数据失败: ' + (err as Error).message) // 提供更详细的错误信息
|
||||||
return []
|
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颜色
|
// 获取SC颜色
|
||||||
function GetSCColor(price: number): string {
|
function GetSCColor(price: number): string {
|
||||||
if (price === 0) return `#2a60b2`
|
if (price === 0) return `#2a60b2`
|
||||||
@@ -139,8 +170,11 @@ function GetGuardColor(price: number | null | undefined): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出数据功能
|
// 导出数据功能 - 注意:这现在只导出已加载的数据
|
||||||
function exportData() {
|
function exportData() {
|
||||||
|
if(hasMore.value) {
|
||||||
|
message.warning('当前导出的是已加载的部分数据,并非所有数据。')
|
||||||
|
}
|
||||||
let text = ''
|
let text = ''
|
||||||
const fileName = generateExportFileName()
|
const fileName = generateExportFileName()
|
||||||
|
|
||||||
@@ -154,7 +188,7 @@ function exportData() {
|
|||||||
selectedEvents.value.map((v) => ({
|
selectedEvents.value.map((v) => ({
|
||||||
type: v.type,
|
type: v.type,
|
||||||
time: format(v.time, 'yyyy-MM-dd HH:mm:ss'),
|
time: format(v.time, 'yyyy-MM-dd HH:mm:ss'),
|
||||||
name: v.name,
|
name: v.uname,
|
||||||
uId: v.uid,
|
uId: v.uid,
|
||||||
num: v.num,
|
num: v.num,
|
||||||
price: v.price,
|
price: v.price,
|
||||||
@@ -202,7 +236,7 @@ function objectsToCSV(arr: any[]) {
|
|||||||
<NCard
|
<NCard
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<template v-if="accountInfo?.isBiliVerified">
|
<template v-if="accountInfo?.isBiliVerified">
|
||||||
<!-- 日期选择和类型选择区域 -->
|
<!-- 日期选择和类型选择区域 -->
|
||||||
<NSpace
|
<NSpace
|
||||||
@@ -216,23 +250,19 @@ function objectsToCSV(arr: any[]) {
|
|||||||
:shortcuts="rangeShortcuts"
|
:shortcuts="rangeShortcuts"
|
||||||
start-placeholder="开始时间"
|
start-placeholder="开始时间"
|
||||||
end-placeholder="结束时间"
|
end-placeholder="结束时间"
|
||||||
@update:value="onDateChange"
|
:disabled="isLoading || isLoadingMore"
|
||||||
/>
|
/>
|
||||||
<NRadioGroup v-model:value="selectedType">
|
<NRadioGroup
|
||||||
<NRadioButton :value="EventType.Guard">
|
v-model:value="selectedType"
|
||||||
|
:disabled="isLoading || isLoadingMore"
|
||||||
|
>
|
||||||
|
<NRadioButton :value="EventDataTypes.Guard">
|
||||||
舰长
|
舰长
|
||||||
</NRadioButton>
|
</NRadioButton>
|
||||||
<NRadioButton :value="EventType.SC">
|
<NRadioButton :value="EventDataTypes.SC">
|
||||||
Superchat
|
Superchat
|
||||||
</NRadioButton>
|
</NRadioButton>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
<NButton
|
|
||||||
type="primary"
|
|
||||||
:loading="isLoading"
|
|
||||||
@click="onDateChange"
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</NButton>
|
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -255,18 +285,28 @@ function objectsToCSV(arr: any[]) {
|
|||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="selectedEvents.length === 0"
|
:disabled="selectedEvents.length === 0 || isLoading || isLoadingMore"
|
||||||
@click="exportData"
|
@click="exportData"
|
||||||
>
|
>
|
||||||
导出
|
导出{{ hasMore ? ' (已加载部分)' : ' (全部)' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
|
<NText
|
||||||
|
v-if="hasMore && selectedEvents.length > 0"
|
||||||
|
type="warning"
|
||||||
|
style="font-size: smaller; display: block; margin-top: 5px;"
|
||||||
|
>
|
||||||
|
当前仅显示已加载的部分数据,滚动到底部可加载更多。导出功能也仅导出已加载数据。
|
||||||
|
</NText>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<NDivider> 共 {{ selectedEvents.length }} 条 </NDivider>
|
<NDivider>
|
||||||
|
共加载 {{ selectedEvents.length }} 条 {{ hasMore ? '(滚动加载更多...)' : '' }}
|
||||||
|
</NDivider>
|
||||||
|
|
||||||
<!-- 数据展示区域 -->
|
<!-- 数据展示区域 -->
|
||||||
<NSpin :show="isLoading">
|
<NSpin :show="isLoading">
|
||||||
|
<!-- 主 Spinner 只在初始加载时显示 -->
|
||||||
<!-- 显示模式切换 -->
|
<!-- 显示模式切换 -->
|
||||||
<NRadioGroup
|
<NRadioGroup
|
||||||
v-model:value="displayMode"
|
v-model:value="displayMode"
|
||||||
@@ -290,85 +330,122 @@ function objectsToCSV(arr: any[]) {
|
|||||||
>
|
>
|
||||||
<!-- 网格视图 -->
|
<!-- 网格视图 -->
|
||||||
<div v-if="displayMode == 'grid'">
|
<div v-if="displayMode == 'grid'">
|
||||||
<NGrid
|
<NInfiniteScroll
|
||||||
cols="1 500:2 800:3 1000:4 1200:5"
|
:distance="100"
|
||||||
:x-gap="12"
|
:disabled="isLoadingMore || !hasMore || isLoading"
|
||||||
:y-gap="8"
|
@load="loadMore"
|
||||||
>
|
>
|
||||||
<NGridItem
|
<NGrid
|
||||||
v-for="item in selectedEvents"
|
cols="1 500:2 800:3 1000:4 1200:5"
|
||||||
:key="item.time"
|
:x-gap="12"
|
||||||
|
:y-gap="8"
|
||||||
|
style="min-height: 200px;"
|
||||||
>
|
>
|
||||||
<NCard
|
<NGridItem
|
||||||
size="small"
|
v-for="item in selectedEvents"
|
||||||
:style="`height: ${selectedType == EventType.Guard ? '175px' : '220px'}`"
|
:key="item.time + '_' + item.uid + '_' + item.price"
|
||||||
embedded
|
>
|
||||||
hoverable
|
<NCard
|
||||||
|
size="small"
|
||||||
|
:style="`height: ${selectedType == EventDataTypes.Guard ? '175px' : '220px'}`"
|
||||||
|
embedded
|
||||||
|
hoverable
|
||||||
>
|
>
|
||||||
<NSpace
|
<NSpace
|
||||||
align="center"
|
align="center"
|
||||||
vertical
|
vertical
|
||||||
:size="5"
|
: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"
|
|
||||||
>
|
>
|
||||||
<NTime :time="item.time" />
|
<NAvatar
|
||||||
</NText>
|
round
|
||||||
<NEllipsis v-if="selectedType == EventType.SC">
|
lazy
|
||||||
{{ item.msg }}
|
borderd
|
||||||
</NEllipsis>
|
:size="64"
|
||||||
</NSpace>
|
:src="item.uid ? AVATAR_URL + item.uid : item.uface"
|
||||||
</NCard>
|
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||||
</NGridItem>
|
style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)"
|
||||||
</NGrid>
|
/>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 表格视图 -->
|
<!-- 表格视图 (未应用无限滚动) -->
|
||||||
<NTable v-else>
|
<NTable v-else-if="!isLoading && selectedEvents.length > 0">
|
||||||
|
<!-- 添加 v-else-if 避免初始加载时显示空表格 -->
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>用户名</th>
|
<th>用户名</th>
|
||||||
<th>UId</th>
|
<th>UId</th>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th v-if="selectedType == EventType.Guard">
|
<th v-if="selectedType == EventDataTypes.Guard">
|
||||||
类型
|
类型
|
||||||
</th>
|
</th>
|
||||||
<th>价格</th>
|
<th>价格</th>
|
||||||
<th v-if="selectedType == EventType.SC">
|
<th v-if="selectedType == EventDataTypes.SC">
|
||||||
内容
|
内容
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -376,20 +453,24 @@ function objectsToCSV(arr: any[]) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="item in selectedEvents"
|
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>{{ item.uid }}</td>
|
||||||
<td>
|
<td>
|
||||||
<NTime :time="item.time" />
|
<NTime
|
||||||
|
:time="item.time"
|
||||||
|
format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
/> <!-- 指定格式 -->
|
||||||
</td>
|
</td>
|
||||||
<td v-if="selectedType == EventType.Guard">
|
<td v-if="selectedType == EventDataTypes.Guard">
|
||||||
{{ item.msg }}
|
{{ item.msg }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NTag
|
<NTag
|
||||||
|
size="small"
|
||||||
:color="{
|
:color="{
|
||||||
color: selectedType == EventType.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
color: selectedType == EventDataTypes.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
||||||
textColor: 'white',
|
textColor: 'white',
|
||||||
borderColor: isDarkMode ? 'white' : '#00000000',
|
borderColor: isDarkMode ? 'white' : '#00000000',
|
||||||
}"
|
}"
|
||||||
@@ -397,7 +478,7 @@ function objectsToCSV(arr: any[]) {
|
|||||||
{{ item.price }}
|
{{ item.price }}
|
||||||
</NTag>
|
</NTag>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="selectedType == EventType.SC">
|
<td v-if="selectedType == EventDataTypes.SC">
|
||||||
<NEllipsis style="max-width: 300px">
|
<NEllipsis style="max-width: 300px">
|
||||||
{{ item.msg }}
|
{{ item.msg }}
|
||||||
</NEllipsis>
|
</NEllipsis>
|
||||||
@@ -405,6 +486,14 @@ function objectsToCSV(arr: any[]) {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</NTable>
|
</NTable>
|
||||||
|
<!-- 初始加载时或无数据时的提示 -->
|
||||||
|
<NAlert
|
||||||
|
v-else-if="!isLoading && selectedEvents.length === 0"
|
||||||
|
title="无数据"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
在选定的时间范围和类型内没有找到数据。
|
||||||
|
</NAlert>
|
||||||
</Transition>
|
</Transition>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</template>
|
</template>
|
||||||
@@ -474,4 +563,9 @@ function objectsToCSV(arr: any[]) {
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 网格卡片样式微调 */
|
||||||
|
.n-card {
|
||||||
|
transition: box-shadow 0.3s ease; /* 平滑阴影过渡 */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user