diff --git a/src/api/api-models.ts b/src/api/api-models.ts index ff83c88..1289d71 100644 --- a/src/api/api-models.ts +++ b/src/api/api-models.ts @@ -550,7 +550,7 @@ export enum QueueStatus { } export interface EventModel { type: EventDataTypes - name: string + uname: string uface: string uid: number open_id: string diff --git a/src/client/ClientAutoAction.vue b/src/client/ClientAutoAction.vue new file mode 100644 index 0000000..d100f82 --- /dev/null +++ b/src/client/ClientAutoAction.vue @@ -0,0 +1,93 @@ + + + + + + 施工中 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/ClientDanmakuWindow.vue b/src/client/ClientDanmakuWindow.vue index 8948114..2f3b161 100644 --- a/src/client/ClientDanmakuWindow.vue +++ b/src/client/ClientDanmakuWindow.vue @@ -5,11 +5,13 @@ import { nanoid } from 'nanoid'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import ClientDanmakuItem from './ClientDanmakuItem.vue'; + import { TransitionGroup } from 'vue'; // 添加TransitionGroup导入 type TempDanmakuType = EventModel & { randomId: string; isNew?: boolean; // 添加:标记是否为新弹幕 disappearAt?: number; // 消失时间戳 + timestamp?: number; // 添加:记录插入时间戳 }; let bc: BroadcastChannel | undefined = undefined; @@ -17,6 +19,8 @@ const danmakuList = ref([]); const maxItems = computed(() => setting.value?.maxDanmakuCount || 50); const hasItems = computed(() => danmakuList.value.length > 0); + const isInBatchUpdate = ref(false); // 添加批量更新状态标志 + const pendingRemovalItems = ref([]); // 待移除的弹幕ID // 动态设置CSS变量 function updateCssVariables() { @@ -67,18 +71,51 @@ disappearAt = Date.now() + setting.value.autoDisappearTime * 1000; } + // 判断短时间内是否有大量弹幕插入 + const isRapidInsertion = danmakuList.value.filter(item => + item.isNew && Date.now() - (item.timestamp || 0) < 500).length > 5; + + if (isRapidInsertion && !isInBatchUpdate.value) { + isInBatchUpdate.value = true; + // 在大量插入时简化动画,300ms后恢复 + setTimeout(() => { + isInBatchUpdate.value = false; + }, 300); + } + // 为传入的弹幕对象添加一个随机ID和isNew标记 const dataWithId = { ...data, randomId: nanoid(), // 生成一个随机ID disappearAt, // 添加消失时间 isNew: true, // 标记为新弹幕,用于动画 + timestamp: Date.now(), // 添加时间戳记录插入时间 }; danmakuList.value.unshift(dataWithId); - // Limit the list size AFTER adding the new item - while (danmakuList.value.length > maxItems.value) { - danmakuList.value.pop(); + + // 优化超出长度的弹幕处理 - 改为标记并动画方式移除 + if (danmakuList.value.length > maxItems.value) { + // 找到要移除的项目 + const itemsToRemove = danmakuList.value.slice(maxItems.value); + itemsToRemove.forEach(item => { + if (!pendingRemovalItems.value.includes(item.randomId)) { + pendingRemovalItems.value.push(item.randomId); + } + }); + + // 延迟移除,给动画足够时间 + setTimeout(() => { + danmakuList.value = danmakuList.value.filter(item => + !pendingRemovalItems.value.includes(item.randomId) || + item.timestamp && Date.now() - item.timestamp < setting.value!.animationDuration + ); + + // 更新待移除列表 + pendingRemovalItems.value = pendingRemovalItems.value.filter(id => + danmakuList.value.some(item => item.randomId === id) + ); + }, setting.value.animationDuration || 300); } // 设置一个定时器,在动画完成后移除isNew标记 @@ -97,11 +134,20 @@ if (!setting.value || setting.value.autoDisappearTime <= 0) return; const now = Date.now(); - // 让弹幕有足够时间完成消失动画后再从列表中移除 - danmakuList.value = danmakuList.value.filter(item => { - // 如果设置了消失时间,则在消失时间+动画时长后才真正移除 - const animationDuration = setting.value?.animationDuration || 300; - return !item.disappearAt || (item.disappearAt + animationDuration) > now; + const animationDuration = setting.value.animationDuration || 300; + + // 先标记将要消失的弹幕 + danmakuList.value.forEach(item => { + if (item.disappearAt && item.disappearAt <= now && !pendingRemovalItems.value.includes(item.randomId)) { + // 标记为待移除,但还不实际移除 + pendingRemovalItems.value.push(item.randomId); + + // 延迟删除,让动画有时间完成 + setTimeout(() => { + danmakuList.value = danmakuList.value.filter(d => d.randomId !== item.randomId); + pendingRemovalItems.value = pendingRemovalItems.value.filter(id => id !== item.randomId); + }, animationDuration); + } }); } @@ -174,27 +220,34 @@ - - + + - + @@ -257,6 +310,7 @@ gap: var(--dw-item-spacing); padding-bottom: 8px; /* 添加底部内边距以防止项目溢出 */ box-sizing: border-box; /* 确保padding不会增加元素的实际尺寸 */ + position: relative; /* 为TransitionGroup添加相对定位 */ } .danmaku-list.reverse { @@ -276,35 +330,101 @@ border-radius: 4px; } - /* 弹幕进入动画 */ + /* 弹幕项样式 */ .danmaku-item { transform-origin: center left; transition: all var(--dw-animation-duration) ease; + /* 添加硬件加速,防止文字模糊 */ + transform: translateZ(0); + will-change: transform, opacity; + backface-visibility: hidden; + -webkit-font-smoothing: subpixel-antialiased; } - .danmaku-item-new { - animation: danmaku-in var(--dw-animation-duration) ease-out forwards; + /* 正在离开的弹幕项样式 */ + .danmaku-item-leaving { + animation: danmaku-leave var(--dw-animation-duration) cubic-bezier(0.4, 0, 0.2, 1) forwards; + opacity: 0.8; /* 轻微降低不透明度,提高视觉层次感 */ + z-index: -1; /* 确保离开的项在其他项下方 */ } - @keyframes danmaku-in { - from { - opacity: 0; - transform: translateX(-20px); - } - to { + /* 批量更新模式下的优化 */ + .batch-update .danmaku-list-move { + transition-duration: 100ms !important; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + .batch-item { + transition-duration: 100ms !important; + } + + /* TransitionGroup动画效果 */ + .danmaku-list-enter-active, + .danmaku-list-leave-active, + .danmaku-list-move { + transition: all var(--dw-animation-duration) cubic-bezier(0.55, 0, 0.1, 1); + /* 确保动画过程中文字不模糊 */ + transform: translateZ(0); + will-change: transform, opacity; + backface-visibility: hidden; + } + + .danmaku-list-leave-active { + position: absolute; + pointer-events: none; + z-index: -1; + width: 100%; + } + + .danmaku-list-enter-from { + opacity: 0; + transform: scaleY(0.5) translateX(-30px) translateZ(0); + } + + .danmaku-list-enter-to { + opacity: 1; + transform: scaleY(1) translateX(0) translateZ(0); + } + + .danmaku-list-leave-to { + opacity: 0; + transform: scaleY(0.5) translateX(30px) translateZ(0); + } + + /* 处理已有弹幕的移动动画 */ + .danmaku-list-move { + transition: transform var(--dw-animation-duration) cubic-bezier(0.55, 0, 0.1, 1); + /* 确保移动动画时文字不模糊 */ + backface-visibility: hidden; + transform: translateZ(0); + } + + /* 根据弹幕类型提供不同的动画特性 */ + [data-type="3"] { /* 普通弹幕 */ + --transition-delay: 0.02s; + } + + [data-type="2"] { /* 礼物 */ + --transition-delay: 0.04s; + animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* 小弹跳效果 */ + } + + [data-type="1"] { /* SC */ + --transition-delay: 0.05s; + animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); /* 特殊强调效果 */ + } + + /* 添加弹幕消失动画 */ + @keyframes danmaku-leave { + 0% { opacity: 1; - transform: translateX(0); + transform: translateX(0) translateZ(0); + filter: blur(0px); } - } - - @keyframes danmaku-out { - from { - opacity: 1; - transform: translateX(0); - } - to { + 100% { opacity: 0; - transform: translateX(20px); + transform: translateX(30px) translateZ(0); + filter: blur(1px); } } \ No newline at end of file diff --git a/src/client/ClientFetcher.vue b/src/client/ClientFetcher.vue index faba986..d8764e5 100644 --- a/src/client/ClientFetcher.vue +++ b/src/client/ClientFetcher.vue @@ -542,6 +542,89 @@ + + + + + + + + {{ connectionStatusText }} + + + + + {{ formattedStartedAt }} + + + {{ uptime }} + + + + + {{ signalRStateText }} + + + {{ webfetcher.signalRId ?? 'N/A' }} + + + + + + + {{ danmakuClientStateText }} + + + {{ webfetcher.danmakuServerUrl ?? 'N/A' }} + + + + + + + + + + {{ networkStatus === 'online' ? '在线' : '离线' }} + + + + + + - - - - - - - - {{ connectionStatusText }} - - - - - {{ formattedStartedAt }} - - - {{ uptime }} - - - - - {{ signalRStateText }} - - - {{ webfetcher.signalRId ?? 'N/A' }} - - - - - - - {{ danmakuClientStateText }} - - - {{ webfetcher.danmakuServerUrl ?? 'N/A' }} - - - - - - - - - - {{ networkStatus === 'online' ? '在线' : '离线' }} - - - - - - - h(RouterLink, { to: { name: 'client-danmaku-window-manage' } }, () => '弹幕机管理'), + h(RouterLink, { to: { name: 'client-danmaku-window-manage' } }, () => '弹幕机'), key: 'danmaku-window-manage', - icon: () => h(Settings24Filled), + icon: () => h(Chat24Filled), show: danmakuWindow.danmakuWindow != undefined }, + { + label: () => + h(RouterLink, { to: { name: 'client-auto-action-manage' } }, () => '自动操作'), + key: 'danmaku-auto-action-manage', + icon: () => h(FlashAuto24Filled), + }, { label: () => h(RouterLink, { to: { name: 'client-settings' } }, () => '设置'), diff --git a/src/client/components/autoaction/AutoReplyConfig.vue b/src/client/components/autoaction/AutoReplyConfig.vue new file mode 100644 index 0000000..b5f358e --- /dev/null +++ b/src/client/components/autoaction/AutoReplyConfig.vue @@ -0,0 +1,313 @@ + + + + + + + + 自动回复设置 + + + + + 冷却时间 (秒): + + + + + + + + + + 触发关键词: + + + + {{ keyword }} + + + + + + + 回复内容: + + + + {{ reply }} + + + + + + + 屏蔽词: + + + + {{ blockword }} + + + + + + + + 删除规则 + + + 确定要删除此规则吗? + + + + + + + + + + + 触发关键词: + + + + + 添加 + + + + + {{ keyword }} + + + + + + + 回复内容: (可以使用 {{ '\{\{ user.name \}\}' }} 作为用户名变量) + + + + + 添加 + + + + + {{ reply }} + + + + + + + 屏蔽词: (可选,当弹幕中包含屏蔽词时不触发) + + + + + 添加 + + + + + {{ blockword }} + + + + + + 保存规则 + + + + + + + + diff --git a/src/client/components/autoaction/CommonConfigItems.vue b/src/client/components/autoaction/CommonConfigItems.vue new file mode 100644 index 0000000..c118f7f --- /dev/null +++ b/src/client/components/autoaction/CommonConfigItems.vue @@ -0,0 +1,120 @@ + + + + + + + 启用功能: + + + + + 仅直播中开启: + + + + + 延迟时间 (秒): + + + + + 屏蔽天选时刻: + + + + + + 用户过滤设置 + + + + 启用用户过滤: + + + + + 要求本房间勋章: + + + + + 要求任意舰长: + + + + + + + + diff --git a/src/client/components/autoaction/EntryWelcomeConfig.vue b/src/client/components/autoaction/EntryWelcomeConfig.vue new file mode 100644 index 0000000..7369197 --- /dev/null +++ b/src/client/components/autoaction/EntryWelcomeConfig.vue @@ -0,0 +1,59 @@ + + + + + + + + 入场欢迎设置 + + + + + 每次欢迎最大用户数: + + + + + + + diff --git a/src/client/components/autoaction/FollowThankConfig.vue b/src/client/components/autoaction/FollowThankConfig.vue new file mode 100644 index 0000000..d511791 --- /dev/null +++ b/src/client/components/autoaction/FollowThankConfig.vue @@ -0,0 +1,59 @@ + + + + + + + + 关注感谢设置 + + + + + 每次感谢最大用户数: + + + + + + + diff --git a/src/client/components/autoaction/GiftThankConfig.vue b/src/client/components/autoaction/GiftThankConfig.vue new file mode 100644 index 0000000..4ffa205 --- /dev/null +++ b/src/client/components/autoaction/GiftThankConfig.vue @@ -0,0 +1,161 @@ + + + + + + + + 礼物过滤设置 + + + + + 过滤模式: + + + + + 最低价值 (元): + + + + + + + + 感谢设置 + + + + + 感谢模式: + + + + {{ option.label }} + + + + + + + 每次感谢最大用户数: + + + + + 每用户最大礼物数: + + + + + 包含礼物数量: + + + + + + + diff --git a/src/client/components/autoaction/GuardPmConfig.vue b/src/client/components/autoaction/GuardPmConfig.vue new file mode 100644 index 0000000..a6909ec --- /dev/null +++ b/src/client/components/autoaction/GuardPmConfig.vue @@ -0,0 +1,223 @@ + + + + + + + + 私信设置 + + + + + 私信模板: + + + + + + ? + + + + + {{ ph.name }}: {{ ph.description }} + + + + + + + + + 发送弹幕确认: + + + + + 弹幕确认模板: + + + + + 防止重复发送: + + + + + + 礼品码模式 + + + + + 启用礼品码模式: + + + + + + + + + + + 添加 + + + + + + + + + {{ code }} + + 删除 + + + + + + + + + + diff --git a/src/client/components/autoaction/ScheduledDanmakuConfig.vue b/src/client/components/autoaction/ScheduledDanmakuConfig.vue new file mode 100644 index 0000000..6d08036 --- /dev/null +++ b/src/client/components/autoaction/ScheduledDanmakuConfig.vue @@ -0,0 +1,78 @@ + + + + + + + + 定时弹幕设置 + + + + + 发送间隔 (秒): + + + + + 发送模式: + + + + {{ option.label }} + + + + + + + + + diff --git a/src/client/components/autoaction/TemplateEditor.vue b/src/client/components/autoaction/TemplateEditor.vue new file mode 100644 index 0000000..6e1daf9 --- /dev/null +++ b/src/client/components/autoaction/TemplateEditor.vue @@ -0,0 +1,167 @@ + + + + + + + + + 变量说明 + + + + + {{ ph.name }}: {{ ph.description }} + + + 默认变量 + + + {{ ph.name }}: {{ ph.description }} + + + + + + + {{ description }} + + + + + + {{ template }} + + + + 删除 + + + 确定要删除此模板吗? + + + + + + + + + + + 添加模板 + + + + + + diff --git a/src/client/components/danmaku/BaseDanmakuItem.vue b/src/client/components/danmaku/BaseDanmakuItem.vue index 657badf..1d3f6aa 100644 --- a/src/client/components/danmaku/BaseDanmakuItem.vue +++ b/src/client/components/danmaku/BaseDanmakuItem.vue @@ -136,7 +136,7 @@ const priceText = computed(() => { // 获取用户名显示 const displayName = computed(() => { - return props.item.name || '匿名用户'; + return props.item.uname || '匿名用户'; }); // 获取消息显示内容 diff --git a/src/client/components/danmaku/CardStyleDanmakuItem.vue b/src/client/components/danmaku/CardStyleDanmakuItem.vue index 2ca0030..d9ce0e5 100644 --- a/src/client/components/danmaku/CardStyleDanmakuItem.vue +++ b/src/client/components/danmaku/CardStyleDanmakuItem.vue @@ -19,7 +19,8 @@ import { VehicleShip24Filled } from '@vicons/fluent'; showAvatar, guardColor, scColorClass, - parsedMessage + parsedMessage, + medalColor } = danmakuUtils; @@ -44,7 +45,7 @@ import { VehicleShip24Filled } from '@vicons/fluent'; class="username" :style="{ color: item.type === EventDataTypes.SC ? '#222' : '#fff' }" > - {{ item?.name || '匿名用户' }} + {{ item?.uname || '匿名用户' }} @@ -101,7 +102,7 @@ import { VehicleShip24Filled } from '@vicons/fluent'; class="username" :style="{ color: '#fff' }" > - {{ item?.name || '匿名用户' }} + {{ item?.uname || '匿名用户' }} + + + {{ item.fans_medal_name }} + {{ item.fans_medal_level }} + 进入了直播间 @@ -264,7 +277,6 @@ import { VehicleShip24Filled } from '@vicons/fluent'; .card-header { display: flex; align-items: center; - gap: 6px; margin-bottom: 4px; min-height: var(--dw-avatar-size); } @@ -388,6 +400,34 @@ import { VehicleShip24Filled } from '@vicons/fluent'; flex-shrink: 0; } + /* 粉丝勋章样式 */ + .fans-medal { + display: flex; + align-items: center; + border-radius: 4px; + margin-left: 4px; + font-size: 0.75em; + height: 16px; + overflow: hidden; + flex-shrink: 0; + } + + .medal-name { + padding: 0 3px; + color: #fff; + max-width: 40px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background-color: rgba(255, 255, 255, 0.2); + } + + .medal-level { + padding: 0 3px; + color: #fff; + font-weight: bold; + } + .enter-badge { color: #67C23A; font-size: 0.85em; diff --git a/src/client/components/danmaku/danmakuUtils.ts b/src/client/components/danmaku/danmakuUtils.ts index 97a02da..67d9e6d 100644 --- a/src/client/components/danmaku/danmakuUtils.ts +++ b/src/client/components/danmaku/danmakuUtils.ts @@ -3,6 +3,31 @@ import { DanmakuWindowSettings } from '../../store/useDanmakuWindow'; import { computed, ComputedRef } from 'vue'; import { GetGuardColor } from '@/Utils'; +// 粉丝勋章等级对应的颜色 +export const MEDAL_LEVEL_COLORS: { [key: number]: string } = { + 1: '#68a49a', 2: '#5b9a8f', 3: '#539288', 4: '#4e8a80', + 5: '#607ea0', 6: '#54708f', 7: '#4e6887', 8: '#49617e', + 9: '#8d7a9b', 10: '#816d8f', 11: '#776385', 12: '#6e5a7c', + 13: '#c06d80', 14: '#b66174', 15: '#ac586a', 16: '#a34f61', + 17: '#caa44a', 18: '#bf973e', 19: '#b68c35', 20: '#ae812f', + 21: '#347368', 22: '#2e685e', 23: '#285e55', 24: '#25564e', + 25: '#354b86', 26: '#2e4179', 27: '#293a6f', 28: '#243466', + 29: '#624180', 30: '#573873', 31: '#4f3168', 32: '#482b5f', + 33: '#a23e54', 34: '#92364a', 35: '#843042', 36: '#772a3b', + 37: '#f38b3c', 38: '#e87b2e', 39: '#de6e23', 40: '#d5621a' +}; + +// 获取粉丝勋章颜色 +export function getMedalColor(level: number): string { + // 处理超过40级的情况,颜色循环使用 + if (level > 40) { + level = 40; + } + + // 如果找不到对应等级颜色或者等级小于1,返回默认颜色 + return MEDAL_LEVEL_COLORS[level] || '#999999'; +} + export interface BaseDanmakuItemProps { item: EventModel & { randomId: string; isNew?: boolean; disappearAt?: number; }; setting: DanmakuWindowSettings; @@ -129,7 +154,7 @@ export function useDanmakuUtils( // 获取用户名显示 const displayName = computed(() => { - return props.item.name || '匿名用户'; + return props.item.uname || '匿名用户'; }); // 获取消息显示内容 @@ -164,6 +189,14 @@ export function useDanmakuUtils( return undefined; // 普通消息使用默认颜色 }); + // 计算粉丝勋章颜色 + const medalColor = computed(() => { + if (props.item.fans_medal_level && props.item.fans_medal_level > 0) { + return getMedalColor(props.item.fans_medal_level); + } + return '#999999'; // 默认颜色 + }); + return { scColorClass, typeClass, @@ -175,7 +208,8 @@ export function useDanmakuUtils( priceText, displayName, displayContent, - textModeColor + textModeColor, + medalColor // 添加粉丝勋章颜色计算属性 }; } diff --git a/src/client/data/initialize.ts b/src/client/data/initialize.ts index ef05274..f3a519e 100644 --- a/src/client/data/initialize.ts +++ b/src/client/data/initialize.ts @@ -128,7 +128,7 @@ export async function initAll(isOnBoot: boolean) { await useWindow.getEmojiData(); } if (await w.isVisible()) { - //useWindow.isDanmakuWindowOpen = true; + useWindow.isDanmakuWindowOpen = true; console.log('弹幕窗口已打开'); } diff --git a/src/client/store/useAutoAction.ts b/src/client/store/useAutoAction.ts new file mode 100644 index 0000000..de4fe1e --- /dev/null +++ b/src/client/store/useAutoAction.ts @@ -0,0 +1,642 @@ +import { ref, reactive, watch, computed, onUnmounted } from 'vue'; +import { defineStore, acceptHMRUpdate } from 'pinia'; +import { EventModel, EventDataTypes, GuardLevel } from '@/api/api-models'; +import { useDanmakuClient } from '@/store/useDanmakuClient'; +import { useBiliFunction } from './useBiliFunction'; +import { useAccount } from '@/api/account'; +import { useStorage } from '@vueuse/core' + +// --- 配置类型定义 --- +export interface GiftThankConfig { + enabled: boolean; + delaySeconds: number; // 延迟感谢秒数 (0表示立即) + templates: string[]; // 感谢弹幕模板 + filterMode: 'none' | 'blacklist' | 'whitelist' | 'value' | 'free'; // 过滤模式 + filterGiftNames: string[]; // 黑/白名单礼物名称 + minValue: number; // 最低价值 (用于 value 模式) + ignoreTianXuan: boolean; // 屏蔽天选时刻礼物 + thankMode: 'singleGift' | 'singleUserMultiGift' | 'multiUserMultiGift'; // 感谢模式 + maxUsersPerMsg: number; // 每次感谢最大用户数 + maxGiftsPerUser: number; // 每用户最大礼物数 (用于 singleUserMultiGift) + includeQuantity: boolean; // 是否包含礼物数量 + userFilterEnabled: boolean; // 是否启用用户过滤 + requireMedal: boolean; // 要求本房间勋章 + requireCaptain: boolean; // 要求任意舰长 + onlyDuringLive: boolean; // 仅直播中开启 +} + +export interface GuardPmConfig { + enabled: boolean; + template: string; // 私信模板 + sendDanmakuConfirm: boolean; // 是否发送弹幕确认 + danmakuTemplate: string; // 弹幕确认模板 + preventRepeat: boolean; // 防止重复发送 (需要本地存储) + giftCodeMode: boolean; // 礼品码模式 + giftCodes: { level: GuardLevel; codes: string[]; }[]; // 分等级礼品码 + onlyDuringLive: boolean; // 仅直播中开启 +} + +export interface FollowThankConfig { + enabled: boolean; + delaySeconds: number; + templates: string[]; + maxUsersPerMsg: number; + ignoreTianXuan: boolean; + onlyDuringLive: boolean; +} + +export interface EntryWelcomeConfig { + enabled: boolean; + delaySeconds: number; + templates: string[]; + maxUsersPerMsg: number; + ignoreTianXuan: boolean; + userFilterEnabled: boolean; + requireMedal: boolean; + requireCaptain: boolean; + onlyDuringLive: boolean; +} + +export interface ScheduledDanmakuConfig { + enabled: boolean; + intervalSeconds: number; + messages: string[]; + mode: 'random' | 'sequential'; + onlyDuringLive: boolean; +} + +export interface AutoReplyConfig { + enabled: boolean; + cooldownSeconds: number; + rules: { keywords: string[]; replies: string[]; blockwords: string[]; }[]; + userFilterEnabled: boolean; + requireMedal: boolean; + requireCaptain: boolean; + onlyDuringLive: boolean; +} + +// --- 聚合数据结构 --- +interface AggregatedGift { + uid: number; + name: string; // 用户名 + gifts: { [giftName: string]: { count: number; price: number; }; }; // 礼物名 -> {数量, 单价} + totalPrice: number; + timestamp: number; +} + +interface AggregatedUser { + uid: number; + name: string; + timestamp: number; +} + + +export const useAutoAction = defineStore('autoAction', () => { + const danmakuClient = useDanmakuClient(); + const biliFunc = useBiliFunction(); + const account = useAccount(); // 用于获取房间ID和直播状态 + + // --- 状态定义 --- + const giftThankConfig = useStorage( + 'autoAction.giftThankConfig', + { + enabled: false, + delaySeconds: 5, + templates: ['感谢 {{user.name}} 赠送的 {{gift.summary}}!'], + filterMode: 'none', + filterGiftNames: [], + minValue: 0, + ignoreTianXuan: true, + thankMode: 'singleUserMultiGift', + maxUsersPerMsg: 3, + maxGiftsPerUser: 3, + includeQuantity: true, + userFilterEnabled: false, + requireMedal: false, + requireCaptain: false, + onlyDuringLive: true + } + ) + + const guardPmConfig = useStorage( + 'autoAction.guardPmConfig', + { + enabled: false, + template: '感谢 {{user.name}} 成为 {{guard.levelName}}!欢迎加入!', + sendDanmakuConfirm: false, + danmakuTemplate: '已私信 {{user.name}} 舰长福利!', + preventRepeat: true, + giftCodeMode: false, + giftCodes: [], + onlyDuringLive: true + } + ) + + const followThankConfig = useStorage( + 'autoAction.followThankConfig', + { + enabled: false, + delaySeconds: 10, + templates: ['感谢 {{user.name}} 的关注!'], + maxUsersPerMsg: 5, + ignoreTianXuan: true, + onlyDuringLive: true + } + ) + + const entryWelcomeConfig = useStorage( + 'autoAction.entryWelcomeConfig', + { + enabled: false, + delaySeconds: 15, + templates: ['欢迎 {{user.name}} 进入直播间!'], + maxUsersPerMsg: 5, + ignoreTianXuan: true, + userFilterEnabled: false, + requireMedal: false, + requireCaptain: false, + onlyDuringLive: true + } + ) + + const scheduledDanmakuConfig = useStorage( + 'autoAction.scheduledDanmakuConfig', + { + enabled: false, + intervalSeconds: 300, + messages: ['点点关注不迷路~'], + mode: 'random', + onlyDuringLive: true + } + ) + + const autoReplyConfig = useStorage( + 'autoAction.autoReplyConfig', + { + enabled: false, + cooldownSeconds: 5, + rules: [], + userFilterEnabled: false, + requireMedal: false, + requireCaptain: false, + onlyDuringLive: true + } + ) + + // --- 运行时数据 --- + const aggregatedGifts = ref([]); // 聚合的礼物信息 + const aggregatedFollows = ref([]); // 聚合的关注用户 + const aggregatedEntries = ref([]); // 聚合的入场用户 + const sentGuardPms = useStorage>('autoAction.sentGuardPms', new Set()); // 已发送私信的舰长UID + const giftThankTimer = ref(null); + const followThankTimer = ref(null); + const entryWelcomeTimer = ref(null); + const scheduledDanmakuTimer = ref(null); + const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({}); // 自动回复冷却计时 + const currentScheduledIndex = ref(0); // 定时弹幕顺序模式索引 + const isTianXuanActive = ref(false); // 天选时刻状态 + + // --- Helper Functions --- + const isLive = computed(() => account.value.streamerInfo?.isStreaming ?? false); // 获取直播状态 + const roomId = computed(() => account.value.streamerInfo?.roomId); // 获取房间ID + + // 检查是否应处理事件 (直播状态过滤) + function shouldProcess(config: { enabled: boolean; onlyDuringLive: boolean; }): boolean { + if (!config.enabled) return false; + return !config.onlyDuringLive || isLive.value; + } + + // 检查用户过滤 + function checkUserFilter(config: { userFilterEnabled: boolean; requireMedal: boolean; requireCaptain: boolean; }, event: EventModel): boolean { + if (!config.userFilterEnabled) return true; + if (config.requireMedal && !event.fans_medal_wearing_status) return false; + if (config.requireCaptain && event.guard_level === GuardLevel.None) return false; + return true; + } + + // 获取随机模板 + function getRandomTemplate(templates: string[]): string { + if (!templates || templates.length === 0) return ''; + return templates[Math.floor(Math.random() * templates.length)]; + } + + // Helper to get nested property value + function getNestedValue(obj: Record, path: string): any { + return path.split('.').reduce((o, k) => (o && typeof o === 'object' && k in o) ? o[k] : undefined, obj); + } + + // 格式化消息 (支持 {{object.property}} ) + function formatMessage(template: string, params: Record): string { + return template.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (match, path) => { + const value = getNestedValue(params, path); + return value !== undefined ? String(value) : match; + }); + } + + // 检查是否处于天选时刻 + function checkTianXuanStatus() { + if (!roomId.value) return; + // 这里可以调用API检查天选时刻状态 + // 示例实现,实际应该调用B站API + biliFunc.checkRoomTianXuanStatus(roomId.value).then(active => { + isTianXuanActive.value = active; + }); + } + + // 每5分钟更新一次天选状态 + const tianXuanTimer = setInterval(checkTianXuanStatus, 5 * 60 * 1000); + + // 清理所有计时器 + function clearAllTimers() { + [giftThankTimer, followThankTimer, entryWelcomeTimer, scheduledDanmakuTimer].forEach(timer => { + if (timer.value) clearTimeout(timer.value); + }); + clearInterval(tianXuanTimer); + } + + // --- 事件处理 --- + + // 处理礼物事件 + function onGift(event: EventModel) { + if (!shouldProcess(giftThankConfig.value) || !roomId.value) return; + if (giftThankConfig.value.ignoreTianXuan && isTianXuanActive.value) return; + if (!checkUserFilter(giftThankConfig.value, event)) return; + + // 礼物过滤逻辑 + const giftName = event.uname; + const giftPrice = event.price / 1000; // B站价格单位通常是 1/1000 元 + const giftCount = event.num; + + switch (giftThankConfig.value.filterMode) { + case 'blacklist': + if (giftThankConfig.value.filterGiftNames.includes(giftName)) return; + break; + case 'whitelist': + if (!giftThankConfig.value.filterGiftNames.includes(giftName)) return; + break; + case 'value': + if (giftPrice < giftThankConfig.value.minValue) return; + break; + case 'free': + if (giftPrice === 0) return; // 免费礼物价格为0 + break; + } + + // 添加到聚合列表 + let userGift = aggregatedGifts.value.find(g => g.uid === event.uid); + if (!userGift) { + userGift = { uid: event.uid, name: event.uname, gifts: {}, totalPrice: 0, timestamp: Date.now() }; + aggregatedGifts.value.push(userGift); + } + if (!userGift.gifts[giftName]) { + userGift.gifts[giftName] = { count: 0, price: giftPrice }; + } + userGift.gifts[giftName].count += giftCount; + userGift.totalPrice += giftPrice * giftCount; + userGift.timestamp = Date.now(); // 更新时间戳 + + // 重置或启动延迟计时器 + if (giftThankTimer.value) clearTimeout(giftThankTimer.value); + if (giftThankConfig.value.delaySeconds > 0) { + giftThankTimer.value = setTimeout(sendGiftThankYou, giftThankConfig.value.delaySeconds * 1000); + } else { + sendGiftThankYou(); // 立即发送 + } + } + + // 发送礼物感谢 + function sendGiftThankYou() { + if (!roomId.value || aggregatedGifts.value.length === 0) return; + + const usersToThank = aggregatedGifts.value.slice(0, giftThankConfig.value.maxUsersPerMsg); + aggregatedGifts.value = aggregatedGifts.value.slice(giftThankConfig.value.maxUsersPerMsg); // 移除已处理的用户 + + // 根据感谢模式构建弹幕内容 + let messages: string[] = []; + const template = getRandomTemplate(giftThankConfig.value.templates); + if (!template) return; + + usersToThank.forEach(user => { + const topGifts = Object.entries(user.gifts) + .sort(([, a], [, b]) => b.price * b.count - a.price * a.count) // 按总价值排序 + .slice(0, giftThankConfig.value.maxGiftsPerUser); + + const giftStrings = topGifts.map(([name, data]) => + giftThankConfig.value.includeQuantity ? `${name}x${data.count}` : name + ); + + if (giftStrings.length > 0) { + // 准备模板参数 + const params = { + user: { name: user.name }, + gift: { + summary: giftStrings.join(', '), + totalPrice: user.totalPrice.toFixed(2) + } + }; + messages.push(formatMessage(template, params)); + } + }); + + // 发送弹幕 + messages.forEach(msg => { + if (msg) biliFunc.sendLiveDanmaku(roomId.value!, msg); + }); + + // 如果还有未感谢的礼物,继续设置计时器 + if (aggregatedGifts.value.length > 0) { + if (giftThankTimer.value) clearTimeout(giftThankTimer.value); + giftThankTimer.value = setTimeout(sendGiftThankYou, giftThankConfig.value.delaySeconds * 1000); + } else { + giftThankTimer.value = null; + } + } + + // 处理上舰事件 (Guard) + function onGuard(event: EventModel) { + if (!shouldProcess(guardPmConfig.value) || !roomId.value) return; + + const userId = event.uid; + const userName = event.uname; + const guardLevel = event.guard_level; + + if (guardLevel === GuardLevel.None) return; // 不是上舰事件 + + // 防止重复发送 + if (guardPmConfig.value.preventRepeat) { + if (sentGuardPms.value.has(userId)) { + console.log(`用户 ${userName} (${userId}) 已发送过上舰私信,跳过。`); + return; + } + } + + // 查找礼品码 + let giftCode = ''; + if (guardPmConfig.value.giftCodeMode) { + const levelCodes = guardPmConfig.value.giftCodes.find(gc => gc.level === guardLevel)?.codes; + if (levelCodes && levelCodes.length > 0) { + giftCode = levelCodes.shift() || ''; + // 更新储存的礼品码 + saveGuardConfig(); + } else { + // 尝试查找通用码 (level 0) + const commonCodes = guardPmConfig.value.giftCodes.find(gc => gc.level === GuardLevel.None)?.codes; + if (commonCodes && commonCodes.length > 0) { + giftCode = commonCodes.shift() || ''; + saveGuardConfig(); + } else { + console.warn(`等级 ${guardLevel} 或通用礼品码已用完,无法发送给 ${userName}`); + } + } + } + + // 格式化私信内容 + const guardLevelName = { [GuardLevel.Zongdu]: '总督', [GuardLevel.Tidu]: '提督', [GuardLevel.Jianzhang]: '舰长' }[guardLevel] || '舰长'; + const pmParams = { + user: { name: userName }, + guard: { + levelName: guardLevelName, + giftCode: giftCode + } + }; + const pmContent = formatMessage(guardPmConfig.value.template, pmParams); + + // 发送私信 + biliFunc.sendPrivateMessage(userId, pmContent).then(success => { + if (success) { + console.log(`成功发送上舰私信给 ${userName} (${userId})`); + if (guardPmConfig.value.preventRepeat) { + sentGuardPms.value.add(userId); + } + // 发送弹幕确认 + if (guardPmConfig.value.sendDanmakuConfirm && guardPmConfig.value.danmakuTemplate) { + const confirmParams = { user: { name: userName } }; + const confirmMsg = formatMessage(guardPmConfig.value.danmakuTemplate, confirmParams); + biliFunc.sendLiveDanmaku(roomId.value!, confirmMsg); + } + } else { + console.error(`发送上舰私信给 ${userName} (${userId}) 失败`); + // 失败时归还礼品码 + if (giftCode && guardPmConfig.value.giftCodeMode) { + returnGiftCode(guardLevel, giftCode); + } + } + }); + } + + // 归还礼品码到列表 + function returnGiftCode(level: GuardLevel, code: string) { + const levelCodes = guardPmConfig.value.giftCodes.find(gc => gc.level === level); + if (levelCodes) { + levelCodes.codes.push(code); + } else { + guardPmConfig.value.giftCodes.push({ level, codes: [code] }); + } + saveGuardConfig(); + } + + // 保存舰长配置到本地 + function saveGuardConfig() { + // useStorage会自动保存,无需额外操作 + } + + // 处理关注事件 + function onFollow(event: EventModel) { + if (!shouldProcess(followThankConfig.value) || !roomId.value) return; + if (followThankConfig.value.ignoreTianXuan && isTianXuanActive.value) return; + + aggregatedFollows.value.push({ uid: event.uid, name: event.uname, timestamp: Date.now() }); + + if (followThankTimer.value) clearTimeout(followThankTimer.value); + if (followThankConfig.value.delaySeconds > 0) { + followThankTimer.value = setTimeout(sendFollowThankYou, followThankConfig.value.delaySeconds * 1000); + } else { + sendFollowThankYou(); + } + } + + // 发送关注感谢 + function sendFollowThankYou() { + if (!roomId.value || aggregatedFollows.value.length === 0) return; + const usersToThank = aggregatedFollows.value.slice(0, followThankConfig.value.maxUsersPerMsg); + aggregatedFollows.value = aggregatedFollows.value.slice(followThankConfig.value.maxUsersPerMsg); + + const template = getRandomTemplate(followThankConfig.value.templates); + if (!template) return; + + const names = usersToThank.map(u => u.name).join('、'); + const params = { user: { name: names } }; + const message = formatMessage(template, params); + + if (message) biliFunc.sendLiveDanmaku(roomId.value!, message); + + if (aggregatedFollows.value.length > 0) { + if (followThankTimer.value) clearTimeout(followThankTimer.value); + followThankTimer.value = setTimeout(sendFollowThankYou, followThankConfig.value.delaySeconds * 1000); + } else { + followThankTimer.value = null; + } + } + + // 处理入场事件 (Enter) + function onEnter(event: EventModel) { + if (!shouldProcess(entryWelcomeConfig.value) || !roomId.value) return; + if (entryWelcomeConfig.value.ignoreTianXuan && isTianXuanActive.value) return; + if (!checkUserFilter(entryWelcomeConfig.value, event)) return; + + aggregatedEntries.value.push({ uid: event.uid, name: event.uname, timestamp: Date.now() }); + + if (entryWelcomeTimer.value) clearTimeout(entryWelcomeTimer.value); + if (entryWelcomeConfig.value.delaySeconds > 0) { + entryWelcomeTimer.value = setTimeout(sendEntryWelcome, entryWelcomeConfig.value.delaySeconds * 1000); + } else { + sendEntryWelcome(); + } + } + + // 发送入场欢迎 + function sendEntryWelcome() { + if (!roomId.value || aggregatedEntries.value.length === 0) return; + const usersToWelcome = aggregatedEntries.value.slice(0, entryWelcomeConfig.value.maxUsersPerMsg); + aggregatedEntries.value = aggregatedEntries.value.slice(entryWelcomeConfig.value.maxUsersPerMsg); + + const template = getRandomTemplate(entryWelcomeConfig.value.templates); + if (!template) return; + + const names = usersToWelcome.map(u => u.name).join('、'); + const params = { user: { name: names } }; + const message = formatMessage(template, params); + + if (message) biliFunc.sendLiveDanmaku(roomId.value!, message); + + if (aggregatedEntries.value.length > 0) { + if (entryWelcomeTimer.value) clearTimeout(entryWelcomeTimer.value); + entryWelcomeTimer.value = setTimeout(sendEntryWelcome, entryWelcomeConfig.value.delaySeconds * 1000); + } else { + entryWelcomeTimer.value = null; + } + } + + // 处理弹幕事件 (用于自动回复) + function onDanmaku(event: EventModel) { + if (!shouldProcess(autoReplyConfig.value) || !roomId.value) return; + if (!checkUserFilter(autoReplyConfig.value, event)) return; + + const message = event.msg; + const userId = event.uid; + const now = Date.now(); + + for (const rule of autoReplyConfig.value.rules) { + const keywordMatch = rule.keywords.some(kw => message.includes(kw)); + if (!keywordMatch) continue; + + const blockwordMatch = rule.blockwords.some(bw => message.includes(bw)); + if (blockwordMatch) continue; // 包含屏蔽词,不回复 + + // 检查冷却 + const ruleKey = rule.keywords.join('|'); + const lastReplyTime = lastReplyTimestamps.value[ruleKey] || 0; + if (now - lastReplyTime < autoReplyConfig.value.cooldownSeconds * 1000) { + continue; // 仍在冷却中 + } + + // 选择回复并发送 + const reply = getRandomTemplate(rule.replies); + if (reply) { + const params = { user: { name: event.uname } }; + const formattedReply = formatMessage(reply, params); + biliFunc.sendLiveDanmaku(roomId.value!, formattedReply); + lastReplyTimestamps.value[ruleKey] = now; // 更新冷却时间 + break; // 匹配到一个规则就停止 + } + } + } + + // 发送定时弹幕 + function sendScheduledDanmaku() { + if (!shouldProcess(scheduledDanmakuConfig.value) || !roomId.value || scheduledDanmakuConfig.value.messages.length === 0) { + stopScheduledDanmaku(); // 停止计时器如果条件不满足 + return; + } + + let message = ''; + if (scheduledDanmakuConfig.value.mode === 'random') { + message = getRandomTemplate(scheduledDanmakuConfig.value.messages); + } else { + message = scheduledDanmakuConfig.value.messages[currentScheduledIndex.value]; + currentScheduledIndex.value = (currentScheduledIndex.value + 1) % scheduledDanmakuConfig.value.messages.length; + } + + if (message) { + biliFunc.sendLiveDanmaku(roomId.value!, message); + } + + // 设置下一次定时 + if (scheduledDanmakuTimer.value) clearTimeout(scheduledDanmakuTimer.value); + scheduledDanmakuTimer.value = setTimeout(sendScheduledDanmaku, scheduledDanmakuConfig.value.intervalSeconds * 1000); + } + + // 启动定时弹幕 + function startScheduledDanmaku() { + if (scheduledDanmakuTimer.value) clearTimeout(scheduledDanmakuTimer.value); // 清除旧的 + if (shouldProcess(scheduledDanmakuConfig.value) && scheduledDanmakuConfig.value.intervalSeconds > 0) { + scheduledDanmakuTimer.value = setTimeout(sendScheduledDanmaku, scheduledDanmakuConfig.value.intervalSeconds * 1000); + } + } + + // 停止定时弹幕 + function stopScheduledDanmaku() { + if (scheduledDanmakuTimer.value) { + clearTimeout(scheduledDanmakuTimer.value); + scheduledDanmakuTimer.value = null; + } + } + + // 监听配置变化以启动/停止定时弹幕 + watch(() => [scheduledDanmakuConfig.value.enabled, scheduledDanmakuConfig.value.onlyDuringLive, isLive.value, scheduledDanmakuConfig.value.intervalSeconds], () => { + if (scheduledDanmakuConfig.value.enabled && (!scheduledDanmakuConfig.value.onlyDuringLive || isLive.value)) { + startScheduledDanmaku(); + } else { + stopScheduledDanmaku(); + } + }, { immediate: true }); // 立即执行一次检查 + + // 当组件卸载时清理所有计时器 + onUnmounted(() => { + clearAllTimers(); + }); + + // 初始化,订阅事件 + function init() { + danmakuClient.onEvent('danmaku', (data) => onDanmaku(data as EventModel)); + danmakuClient.onEvent('gift', (data) => onGift(data as EventModel)); + danmakuClient.onEvent('guard', (data) => onGuard(data as EventModel)); + danmakuClient.onEvent('follow', (data) => onFollow(data as EventModel)); + danmakuClient.onEvent('enter', (data) => onEnter(data as EventModel)); + + // 初始检查天选状态 + checkTianXuanStatus(); + + // 启动定时弹幕(如果初始状态满足条件) + startScheduledDanmaku(); + + console.log('自动操作模块已初始化'); + } + + return { + init, + // --- 配置 --- + giftThankConfig, + guardPmConfig, + followThankConfig, + entryWelcomeConfig, + scheduledDanmakuConfig, + autoReplyConfig, + }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useAutoAction, import.meta.hot)); +} + +export { GuardLevel }; diff --git a/src/client/store/useBiliCookie.ts b/src/client/store/useBiliCookie.ts index 598103c..8309d07 100644 --- a/src/client/store/useBiliCookie.ts +++ b/src/client/store/useBiliCookie.ts @@ -6,6 +6,7 @@ import { QueryBiliAPI } from '../data/utils'; import { BiliUserProfile } from '../data/models'; import { defineStore, acceptHMRUpdate } from 'pinia'; import { ref, computed, shallowRef } from 'vue'; +import { StorageSerializers } from '@vueuse/core'; // --- 常量定义 --- // Tauri Store 存储键名 @@ -72,7 +73,11 @@ type CookieCloudState = 'unset' | 'valid' | 'invalid' | 'syncing'; export const useBiliCookie = defineStore('biliCookie', () => { // --- 依赖和持久化存储实例 --- // 使用 useTauriStore 获取持久化存储目标 - const biliCookieStore = useTauriStore().getTarget(BILI_COOKIE_KEY); + const biliCookieStore = useStorage(BILI_COOKIE_KEY, { + cookie: '', + refreshToken: undefined, // 可选,未使用 + lastRefresh: new Date(0), // 默认值 + }); // 为保持响应性 const cookieCloudStore = useTauriStore().getTarget(COOKIE_CLOUD_KEY); const userInfoCacheStore = useTauriStore().getTarget(USER_INFO_CACHE_KEY); @@ -328,7 +333,7 @@ export const useBiliCookie = defineStore('biliCookie', () => { // 1. 加载持久化数据 const [storedCookieData, storedCloudConfig, storedUserInfo] = await Promise.all([ - biliCookieStore.get(), + biliCookieStore.value, cookieCloudStore.get(), userInfoCacheStore.get(), ]); @@ -411,7 +416,7 @@ export const useBiliCookie = defineStore('biliCookie', () => { // 如果没有尝试云同步,或者云同步失败,则检查本地 Cookie if (!cloudSyncAttempted || !cloudSyncSuccess) { debug('[BiliCookie] 检查本地存储的 Cookie 有效性...'); - const storedCookie = (await biliCookieStore.get())?.cookie; + const storedCookie = biliCookieStore.value?.cookie; if (storedCookie) { const { valid } = await _checkCookieValidity(storedCookie); // 只有在云同步未成功时才更新状态,避免覆盖云同步设置的状态 @@ -450,7 +455,7 @@ export const useBiliCookie = defineStore('biliCookie', () => { lastRefresh: new Date() // 更新刷新时间戳 }; try { - await biliCookieStore.set(dataToStore); + biliCookieStore.value = dataToStore; // 使用响应式存储 info('[BiliCookie] 新 Bilibili Cookie 已验证并保存'); _updateCookieState(true, true); // 更新状态为存在且有效 } catch (err) { @@ -473,7 +478,7 @@ export const useBiliCookie = defineStore('biliCookie', () => { * @returns Promise Cookie 字符串或 undefined */ const getBiliCookie = async (): Promise => { - const data = await biliCookieStore.get(); + const data = biliCookieStore.value; return data?.cookie; }; @@ -489,11 +494,7 @@ export const useBiliCookie = defineStore('biliCookie', () => { debug('[BiliCookie] 定时检查已停止'); } // 清除 Cookie 存储 - try { - await biliCookieStore.delete(); - } catch (err) { - error('[BiliCookie] 清除 Bilibili Cookie 存储失败: ' + String(err)); - } + biliCookieStore.value = undefined; // 清除持久化存储 // 清除用户信息缓存 await _clearUserInfoCache(); // 重置状态变量 @@ -563,6 +564,8 @@ export const useBiliCookie = defineStore('biliCookie', () => { uId: computed(() => uId.value), // 只读 ref userInfo, // computed 属性本身就是只读的 + cookie: computed(() => biliCookieStore.value?.cookie), // 只读 ref + // 方法 init, check, // 暴露 check 方法,允许手动触发检查 (例如,应用从后台恢复) diff --git a/src/client/store/useBiliFunction.ts b/src/client/store/useBiliFunction.ts new file mode 100644 index 0000000..dafda76 --- /dev/null +++ b/src/client/store/useBiliFunction.ts @@ -0,0 +1,212 @@ +import { useAccount } from "@/api/account"; +import { useBiliCookie } from "./useBiliCookie"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; // 引入 Body +import { defineStore, acceptHMRUpdate } from 'pinia'; +import { computed } from 'vue'; + +export const useBiliFunction = defineStore('biliFunction', () => { + const biliCookieStore = useBiliCookie(); + const account = useAccount(); + const cookie = computed(() => biliCookieStore.cookie); + const uid = computed(() => account.value.biliId); + + const csrf = computed(() => { + if (!cookie.value) return null; + const match = cookie.value.match(/bili_jct=([^;]+)/); + return match ? match[1] : null; + }); + + /** + * 发送直播弹幕 + * @param roomId 直播间 ID + * @param message 弹幕内容 + * @param color 弹幕颜色 (十六进制, 如 FFFFFF) + * @param fontsize 字体大小 (默认 25) + * @param mode 弹幕模式 (1: 滚动, 4: 底部, 5: 顶部) + * @returns Promise 是否发送成功 (基于API响应码) + */ + async function sendLiveDanmaku(roomId: number, message: string, color: string = 'ffffff', fontsize: number = 25, mode: number = 1): Promise { + if (!csrf.value || !cookie.value) { + console.error("发送弹幕失败:缺少 cookie 或 csrf token"); + return false; + } + if (!message || message.trim().length === 0) { + console.warn("尝试发送空弹幕,已阻止。"); + return false; + } + const url = "https://api.live.bilibili.com/msg/send"; + const rnd = Math.floor(Date.now() / 1000); + const data = { + bubble: '0', + msg: message, + color: parseInt(color, 16).toString(), + fontsize: fontsize.toString(), + mode: mode.toString(), + roomid: roomId.toString(), + rnd: rnd.toString(), + csrf: csrf.value, + csrf_token: csrf.value, + }; + + try { + // 注意: B站网页版发送弹幕是用 application/x-www-form-urlencoded + const response = await tauriFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookie.value, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36", + "Referer": `https://live.bilibili.com/${roomId}` + }, + body: JSON.stringify(data), // 发送 JSON 数据 + }); + + if (!response.ok) { + console.error("发送弹幕网络失败:", response.status, await response.text()); + return false; + } + const json = await response.json(); + // B站成功码通常是 0 + if (json.code !== 0) { + console.error("发送弹幕API失败:", json.code, json.message || json.msg); + return false; + } + + console.log("发送弹幕成功:", message); + return true; + } catch (error) { + console.error("发送弹幕时发生错误:", error); + return false; + } + } + + /** + * 封禁直播间用户 (需要主播或房管权限) + * @param roomId 直播间 ID + * @param userId 要封禁的用户 UID + * @param hours 封禁时长 (小时, 1-720) + */ + async function banLiveUser(roomId: number, userId: number, hours: number = 1) { + // 使用 csrf.value + if (!csrf.value || !cookie.value) { + console.error("封禁用户失败:缺少 cookie 或 csrf token"); + return; + } + // 确保 hours 在 1 到 720 之间 + const validHours = Math.max(1, Math.min(hours, 720)); + const url = "https://api.live.bilibili.com/banned_service/v2/Silent/add_user"; + const data = { + room_id: roomId.toString(), + block_uid: userId.toString(), + hour: validHours.toString(), + csrf: csrf.value, // 使用计算属性的值 + csrf_token: csrf.value, // 使用计算属性的值 + visit_id: "", // 通常可以为空 + }; + + try { + const response = await tauriFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookie.value, // 使用计算属性的值 + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36", + "Referer": `https://live.bilibili.com/p/html/live-room-setting/#/room-manager/black-list?room_id=${roomId}` // 模拟来源 + }, + body: JSON.stringify(data), // 发送 JSON 数据 + }); + if (!response.ok) { + console.error("封禁用户失败:", response.status, await response.text()); + return response.statusText; + } + const json = await response.json(); + if (json.code !== 0) { + console.error("封禁用户API失败:", json.code, json.message || json.msg); + return json.data; + } + console.log("封禁用户成功:", json.data); + return json.data; + } catch (error) { + console.error("封禁用户时发生错误:", error); + } + } + + /** + * 发送私信 + * @param receiverId 接收者 UID + * @param message 私信内容 + * @returns Promise 是否发送成功 (基于API响应码) + */ + async function sendPrivateMessage(receiverId: number, message: string): Promise { + if (!csrf.value || !cookie.value || !uid.value) { + console.error("发送私信失败:缺少 cookie, csrf token 或 uid"); + return false; + } + if (!message || message.trim().length === 0) { + console.warn("尝试发送空私信,已阻止。"); + return false; + } + const url = "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg"; + const timestamp = Math.floor(Date.now() / 1000); + const content = JSON.stringify({ content: message }); + const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16).toUpperCase(); + }); + const data = { + 'msg[sender_uid]': uid.value.toString(), + 'msg[receiver_id]': receiverId.toString(), + 'msg[receiver_type]': '1', + 'msg[msg_type]': '1', + 'msg[msg_status]': '0', + 'msg[content]': content, + 'msg[timestamp]': timestamp.toString(), + 'msg[new_face_version]': '0', + 'msg[dev_id]': dev_id, + 'build': '0', + 'mobi_app': 'web', + 'csrf': csrf.value, + 'csrf_token': csrf.value, + }; + + try { + const response = await tauriFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookie.value, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36", + "Referer": `https://message.bilibili.com/`, + }, + body: JSON.stringify(data), // 发送 JSON 数据 + }); + + if (!response.ok) { + console.error("发送私信网络失败:", response.status, await response.text()); + return false; + } + // 私信成功码也是 0 + if (response.data.code !== 0) { + console.error("发送私信API失败:", response.data.code, response.data.message); + return false; + } + console.log(`发送私信给 ${receiverId} 成功`); + return true; + } catch (error) { + console.error("发送私信时发生错误:", error); + return false; + } + } + + return { + sendLiveDanmaku, + banLiveUser, + sendPrivateMessage, + csrf, + uid, + }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useBiliFunction, import.meta.hot)); +} \ No newline at end of file diff --git a/src/client/store/useDanmakuWindow.ts b/src/client/store/useDanmakuWindow.ts index 3ac73da..cbab707 100644 --- a/src/client/store/useDanmakuWindow.ts +++ b/src/client/store/useDanmakuWindow.ts @@ -66,16 +66,23 @@ function generateTestDanmaku(): EventModel { const randomTime = Date.now(); const randomOuid = `oid_${randomUid}`; + // 扩展粉丝勋章相关的随机数据 + const hasMedal = Math.random() > 0.3; // 70% 概率拥有粉丝勋章 + const isWearingMedal = hasMedal && Math.random() > 0.2; // 佩戴粉丝勋章的概率 + const medalNames = ['鸽子团', '鲨鱼牌', '椰奶', '饼干', '猫猫头', '南极', '狗妈', '可爱', '团子', '喵']; + const randomMedalName = medalNames[Math.floor(Math.random() * medalNames.length)]; + const randomMedalLevel = isWearingMedal ? Math.floor(Math.random() * 40) + 1 : 0; + const baseEvent: Partial = { - name: randomName, + uname: randomName, uface: `https://i0.hdslb.com/bfs/face/member/noface.jpg`, // Placeholder for user avatar uid: randomUid, open_id: randomOuid, // Assuming open_id is same as ouid for test time: randomTime, guard_level: Math.floor(Math.random() * 4) as GuardLevel, - fans_medal_level: Math.floor(Math.random() * 41), - fans_medal_name: '测试牌', - fans_medal_wearing_status: Math.random() > 0.5, + fans_medal_level: randomMedalLevel, + fans_medal_name: randomMedalName, + fans_medal_wearing_status: isWearingMedal, ouid: randomOuid, }; @@ -243,8 +250,6 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => { danmakuWindowSetting.value.y = position.y; }); - isWindowOpened.value = true; - bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL); bc.onmessage = (event: MessageEvent) => { if (event.data.type === 'window-ready') { @@ -350,7 +355,7 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => { // 新增:发送测试弹幕函数 function sendTestDanmaku() { if (!isWindowOpened.value || !bc) { - console.warn('[danmaku-window] 窗口未打开或 BC 未初始化,无法发送测试弹幕'); + console.warn('[danmaku-window] 窗口未打开或 BroadcastChannel 未初始化,无法发送测试弹幕'); return; } const testData = generateTestDanmaku(); diff --git a/src/components.d.ts b/src/components.d.ts index e2e7cf3..aaa7844 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -39,6 +39,7 @@ declare module 'vue' { NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NScrollbar: typeof import('naive-ui')['NScrollbar'] NSpace: typeof import('naive-ui')['NSpace'] + NSSwitch: typeof import('naive-ui')['NSSwitch'] NTab: typeof import('naive-ui')['NTab'] NTag: typeof import('naive-ui')['NTag'] NText: typeof import('naive-ui')['NText'] diff --git a/src/data/DanmakuClients/DirectClient.ts b/src/data/DanmakuClients/DirectClient.ts index 3ba6ef4..52e85a2 100644 --- a/src/data/DanmakuClients/DirectClient.ts +++ b/src/data/DanmakuClients/DirectClient.ts @@ -59,16 +59,16 @@ export default class DirectClient extends BaseDanmakuClient { d( { type: EventDataTypes.Message, - name: info[2][1], + uname: info[2][1], uid: info[2][0], msg: info[1], price: 0, num: 1, time: Date.now(), guard_level: info[7], - fans_medal_level: info[0][15].medal?.level, - fans_medal_name: info[0][15].medal?.name, - fans_medal_wearing_status: info[0][15].medal?.is_light === 1, + fans_medal_level: info[0][15].user.medal?.level, + fans_medal_name: info[0][15].user.medal?.name, + fans_medal_wearing_status: info[0][15].user.medal?.is_light === 1, emoji: info[0]?.[13]?.url?.replace("http://", "https://") || '', uface: info[0][15].user.base.face.replace("http://", "https://"), open_id: '', @@ -85,7 +85,7 @@ export default class DirectClient extends BaseDanmakuClient { d( { type: EventDataTypes.Gift, - name: data.uname, + uname: data.uname, uid: data.uid, msg: data.giftName, price: data.price / 1000, @@ -110,7 +110,7 @@ export default class DirectClient extends BaseDanmakuClient { d( { type: EventDataTypes.SC, - name: data.user_info.uname, + uname: data.user_info.uname, uid: data.uid, msg: data.message, price: data.price, @@ -135,7 +135,7 @@ export default class DirectClient extends BaseDanmakuClient { d( { type: EventDataTypes.Guard, - name: data.username, + uname: data.username, uid: data.uid, msg: data.gift_name, price: data.price / 1000, @@ -160,7 +160,7 @@ export default class DirectClient extends BaseDanmakuClient { d( { type: EventDataTypes.Enter, - name: data.uname, + uname: data.uname, uid: data.uid, msg: '', price: 0, @@ -185,7 +185,7 @@ export default class DirectClient extends BaseDanmakuClient { d( { type: EventDataTypes.SCDel, - name: '', + uname: '', uid: 0, msg: JSON.stringify(data.ids), price: 0, diff --git a/src/data/DanmakuClients/OpenLiveClient.ts b/src/data/DanmakuClients/OpenLiveClient.ts index 7d25106..e3fec5c 100644 --- a/src/data/DanmakuClients/OpenLiveClient.ts +++ b/src/data/DanmakuClients/OpenLiveClient.ts @@ -125,7 +125,7 @@ export default class OpenLiveClient extends BaseDanmakuClient { d( { type: EventDataTypes.Message, - name: data.uname, + uname: data.uname, uid: data.uid, msg: data.msg, price: 0, @@ -154,7 +154,7 @@ export default class OpenLiveClient extends BaseDanmakuClient { d( { type: EventDataTypes.Gift, - name: data.uname, + uname: data.uname, uid: data.uid, msg: data.gift_name, price: data.paid ? price : -price, @@ -181,7 +181,7 @@ export default class OpenLiveClient extends BaseDanmakuClient { d( { type: EventDataTypes.SC, - name: data.uname, + uname: data.uname, uid: data.uid, msg: data.message, price: data.rmb, @@ -208,7 +208,7 @@ export default class OpenLiveClient extends BaseDanmakuClient { d( { type: EventDataTypes.Guard, - name: data.user_info.uname, + uname: data.user_info.uname, uid: data.user_info.uid, msg: data.guard_level == 1 @@ -243,7 +243,7 @@ export default class OpenLiveClient extends BaseDanmakuClient { d( { type: EventDataTypes.Enter, - name: data.uname, + uname: data.uname, msg: '', price: 0, num: 0, @@ -270,7 +270,7 @@ export default class OpenLiveClient extends BaseDanmakuClient { d( { type: EventDataTypes.Enter, - name: '', + uname: '', msg: JSON.stringify(data.message_ids), price: 0, num: 0, diff --git a/src/router/client.ts b/src/router/client.ts index 280a6f1..5484951 100644 --- a/src/router/client.ts +++ b/src/router/client.ts @@ -34,6 +34,14 @@ export default { title: '弹幕窗口管理', } }, + { + path: 'auto-action', + name: 'client-auto-action-manage', + component: () => import('@/client/ClientAutoAction.vue'), + meta: { + title: '自动操作管理', + } + }, { path: 'danmaku-window', name: 'client-danmaku-window-redirect', diff --git a/src/views/open_live/LiveRequest.vue b/src/views/open_live/LiveRequest.vue index bf309e0..4109207 100644 --- a/src/views/open_live/LiveRequest.vue +++ b/src/views/open_live/LiveRequest.vue @@ -253,11 +253,11 @@ async function getAllSong() { } async function addSong(danmaku: EventModel) { console.log( - `[OPEN-LIVE-LIVE-REQUEST] 收到 [${danmaku.name}] 的点播${danmaku.type == EventDataTypes.SC ? 'SC' : '弹幕'}: ${danmaku.msg}`, + `[OPEN-LIVE-LIVE-REQUEST] 收到 [${danmaku.uname}] 的点播${danmaku.type == EventDataTypes.SC ? 'SC' : '弹幕'}: ${danmaku.msg}`, ) if (settings.value.enableOnStreaming && accountInfo.value?.streamerInfo?.isStreaming != true) { notice.info({ - title: `${danmaku.name} 点播失败`, + title: `${danmaku.uname} 点播失败`, description: '当前未在直播中, 无法添加点播请求. 或者关闭设置中的仅允许直播时加入', meta: () => h(NTime, { type: 'relative', time: Date.now(), key: updateKey.value }), }) @@ -266,18 +266,18 @@ async function addSong(danmaku: EventModel) { if (accountInfo.value) { await QueryPostAPI(SONG_REQUEST_API_URL + 'try-add', danmaku).then((data) => { if (data.code == 200) { - message.success(`[${danmaku.name}] 添加曲目: ${data.data.songName}`) + message.success(`[${danmaku.uname}] 添加曲目: ${data.data.songName}`) if (data.message != 'EventFetcher') originSongs.value.unshift(data.data) } else { //message.error(`[${danmaku.name}] 添加曲目失败: ${data.message}`) const time = Date.now() notice.warning({ - title: danmaku.name + ' 点播失败', + title: danmaku.uname + ' 点播失败', description: data.message, duration: isWarnMessageAutoClose.value ? 3000 : 0, meta: () => h(NTime, { type: 'relative', time: time, key: updateKey.value }), }) - console.log(`[OPEN-LIVE-LIVE-REQUEST] [${danmaku.name}] 添加曲目失败: ${data.message}`) + console.log(`[OPEN-LIVE-LIVE-REQUEST] [${danmaku.uname}] 添加曲目失败: ${data.message}`) } }) } else { @@ -288,7 +288,7 @@ async function addSong(danmaku: EventModel) { from: danmaku.type == EventDataTypes.Message ? SongRequestFrom.Danmaku : SongRequestFrom.SC, scPrice: danmaku.type == EventDataTypes.SC ? danmaku.price : 0, user: { - name: danmaku.name, + name: danmaku.uname, uid: danmaku.uid, oid: danmaku.open_id, face: danmaku.uface, @@ -302,7 +302,7 @@ async function addSong(danmaku: EventModel) { id: songs.value.length == 0 ? 1 : new List(songs.value).Max((s) => s.id) + 1, } as SongRequestInfo localActiveSongs.value.unshift(songData) - message.success(`[${danmaku.name}] 添加: ${songData.songName}`) + message.success(`[${danmaku.uname}] 添加: ${songData.songName}`) } } async function addSongManual() { diff --git a/src/views/open_live/MusicRequest.vue b/src/views/open_live/MusicRequest.vue index d5c24d0..54881ee 100644 --- a/src/views/open_live/MusicRequest.vue +++ b/src/views/open_live/MusicRequest.vue @@ -281,7 +281,7 @@ async function onGetEvent(data: EventModel) { const lastRequest = cooldown.value[data.uid] if (Date.now() - lastRequest < settings.value.orderCooldown * 1000) { message.info( - `[${data.name}] 冷却中,距离下次点歌还有 ${((settings.value.orderCooldown * 1000 - (Date.now() - lastRequest)) / 1000).toFixed(1)} 秒`, + `[${data.uname}] 冷却中,距离下次点歌还有 ${((settings.value.orderCooldown * 1000 - (Date.now() - lastRequest)) / 1000).toFixed(1)} 秒`, ) return } @@ -290,13 +290,13 @@ async function onGetEvent(data: EventModel) { const result = await searchMusic(name) if (result) { if (settings.value.blacklist.includes(result.name)) { - message.warning(`[${data.name}] 点歌失败,因为 ${result.name} 在黑名单中`) + message.warning(`[${data.uname}] 点歌失败,因为 ${result.name} 在黑名单中`) return } cooldown.value[data.uid] = Date.now() const music = { from: { - name: data.name, + name: data.uname, uid: data.uid, guard_level: data.guard_level, fans_medal_level: data.fans_medal_level, diff --git a/src/views/open_live/OpenQueue.vue b/src/views/open_live/OpenQueue.vue index 423dc73..9d93833 100644 --- a/src/views/open_live/OpenQueue.vue +++ b/src/views/open_live/OpenQueue.vue @@ -251,7 +251,7 @@ if (!checkMessage(danmaku)) { return; } - console.log(`[OPEN-LIVE-QUEUE] 收到 [${danmaku.name}] 的排队请求`); + console.log(`[OPEN-LIVE-QUEUE] 收到 [${danmaku.uname}] 的排队请求`); // 检查是否仅直播时允许加入 if (settings.value.enableOnStreaming && accountInfo.value?.streamerInfo?.isStreaming != true) { message.info('当前未在直播中, 无法添加排队请求. 或者关闭设置中的仅允许直播时加入'); @@ -275,21 +275,21 @@ originQueue.value.splice(existingIndex, 1, data.data); // 替换现有条目 } else { // 新用户加入 originQueue.value.push(data.data); // 添加到末尾 (排序由 computed 处理) - message.success(`[${danmaku.name}] 添加至队列`); + message.success(`[${danmaku.uname}] 添加至队列`); } } } else { // 添加失败 const time = Date.now(); notice.warning({ - title: danmaku.name + ' 排队失败', + title: danmaku.uname + ' 排队失败', description: data.message, duration: isWarnMessageAutoClose.value ? 3000 : 0, meta: () => h(NTime, { type: 'relative', time: time, key: updateKey.value }), // 使用 updateKey 强制更新时间显示 }); - console.log(`[OPEN-LIVE-QUEUE] [${danmaku.name}] 排队失败: ${data.message}`); + console.log(`[OPEN-LIVE-QUEUE] [${danmaku.uname}] 排队失败: ${data.message}`); } } catch (err: any) { - message.error(`[${danmaku.name}] 添加队列时出错: ${err.message || err}`); + message.error(`[${danmaku.uname}] 添加队列时出错: ${err.message || err}`); console.error(`[OPEN-LIVE-QUEUE] 添加队列出错:`, err); } } else { // 未登录,操作本地队列 @@ -298,7 +298,7 @@ from: danmaku.type == EventDataTypes.Message ? QueueFrom.Danmaku : QueueFrom.Gift, giftPrice: danmaku.type == EventDataTypes.SC ? danmaku.price : undefined, user: { - name: danmaku.name, + name: danmaku.uname, uid: danmaku.uid, oid: danmaku.open_id, fans_medal_level: danmaku.fans_medal_level, @@ -311,7 +311,7 @@ id: localQueues.value.length == 0 ? 1 : new List(localQueues.value).Max((s) => s.id) + 1, // 本地 ID } as ResponseQueueModel; localQueues.value.unshift(songData); // 添加到本地队列开头 - message.success(`[${danmaku.name}] 添加至本地队列`); + message.success(`[${danmaku.uname}] 添加至本地队列`); } } @@ -410,7 +410,7 @@ function checkMessage(eventData: EventModel): boolean { // 未登录时,如果用户已在本地队列,则不允许重复添加 (简单检查) if (!configCanEdit.value && localQueues.value.some((q) => q.user?.uid == eventData.uid && q.status < QueueStatus.Finish)) { - console.log(`[OPEN-LIVE-QUEUE] 本地队列已存在用户 [${eventData.name}],跳过`); + console.log(`[OPEN-LIVE-QUEUE] 本地队列已存在用户 [${eventData.uname}],跳过`); return false; } diff --git a/src/views/open_live/ReadDanmaku.vue b/src/views/open_live/ReadDanmaku.vue index 6551a12..1958124 100644 --- a/src/views/open_live/ReadDanmaku.vue +++ b/src/views/open_live/ReadDanmaku.vue @@ -357,7 +357,7 @@ function onGetEvent(data: EventModel) { exist.combineCount ??= 0 exist.combineCount += data.num console.log( - `[TTS] ${data.name} 增加已存在礼物数量: ${data.msg} [${exist.data.num - data.num} => ${exist.data.num}]`, + `[TTS] ${data.uname} 增加已存在礼物数量: ${data.msg} [${exist.data.num - data.num} => ${exist.data.num}]`, ) return } @@ -401,7 +401,7 @@ function getTextFromDanmaku(data: EventModel | undefined) { text = text .replace( templateConstants.name.regex, - settings.value.voiceType == 'api' && settings.value.splitText ? `'${data.name}'` : data.name, + settings.value.voiceType == 'api' && settings.value.splitText ? `'${data.uname}'` : data.uname, ) .replace(templateConstants.count.regex, data.num.toString()) .replace(templateConstants.price.regex, data.price.toString()) @@ -483,7 +483,7 @@ function test(type: EventDataTypes) { case EventDataTypes.Message: forceSpeak({ type: EventDataTypes.Message, - name: accountInfo.value?.name ?? '测试用户', + uname: accountInfo.value?.name ?? '测试用户', uid: accountInfo.value?.biliId ?? 0, msg: '测试弹幕', price: 0, @@ -502,7 +502,7 @@ function test(type: EventDataTypes) { case EventDataTypes.SC: forceSpeak({ type: EventDataTypes.SC, - name: accountInfo.value?.name ?? '测试用户', + uname: accountInfo.value?.name ?? '测试用户', uid: accountInfo.value?.biliId ?? 0, msg: '测试留言', price: 30, @@ -521,7 +521,7 @@ function test(type: EventDataTypes) { case EventDataTypes.Guard: forceSpeak({ type: EventDataTypes.Guard, - name: accountInfo.value?.name ?? '测试用户', + uname: accountInfo.value?.name ?? '测试用户', uid: accountInfo.value?.biliId ?? 0, msg: '舰长', price: 0, @@ -540,7 +540,7 @@ function test(type: EventDataTypes) { case EventDataTypes.Gift: forceSpeak({ type: EventDataTypes.Gift, - name: accountInfo.value?.name ?? '测试用户', + uname: accountInfo.value?.name ?? '测试用户', uid: accountInfo.value?.biliId ?? 0, msg: '测试礼物', price: 5, @@ -790,7 +790,7 @@ onUnmounted(() => { > SC - {{ item.data.name }} + {{ item.data.uname }} {{ getTextFromDanmaku(item.data) }}
+ {{ description }} +