diff --git a/bun.lockb b/bun.lockb index 219734b..9f11ac5 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs index 70a4b80..617605f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,28 +1,75 @@ -import oxlint from 'eslint-plugin-oxlint'; -import vue from 'eslint-plugin-vue'; -import ts from 'typescript-eslint'; - -// `VueVine()` 返回一个 ESLint flat config +import antfu from '@antfu/eslint-config' import VueVine from '@vue-vine/eslint-config' -export default [ +export default antfu( { - languageOptions: { - ecmaVersion: 'latest', + // 项目类型: app (默认) 或 lib + type: 'app', + + // 启用 TypeScript 支持 (自动检测) + typescript: { + tsconfigPath: 'tsconfig.json', }, + + // 启用 Vue 支持 (自动检测) + vue: true, + + // 启用格式化规则 + stylistic: { + indent: 2, + quotes: 'single', + semi: false, + }, + + // 禁用某些文件类型的支持 + jsonc: true, + yaml: true, + markdown: true, + + // 忽略的文件 + ignores: [ + '**/node_modules', + '**/dist', + '**/output', + '**/.vitepress/cache', + '**/.nuxt', + '**/.next', + '**/.vercel', + '**/.changeset', + '**/.idea', + '**/.cache', + '**/.output', + '**/.vite-inspect', + '**/CHANGELOG*.md', + '**/*.min.*', + '**/LICENSE*', + '**/__snapshots__', + '**/auto-import?(s).d.ts', + '**/components.d.ts', + ], }, - ...vue.configs['flat/recommended'], { - // files: ['*.vue', '**/*.vue'], - languageOptions: { - parserOptions: { - parser: ts.parser, - }, - }, + // 自定义规则 rules: { - "vue/no-mutating-props": "off", + // Vue 相关规则 + 'vue/multi-word-component-names': 'off', + 'vue/no-mutating-props': 'off', + 'vue/no-v-html': 'off', + 'vue/require-default-prop': 'off', + + // TypeScript 相关规则 + 'ts/no-explicit-any': 'off', + 'ts/ban-ts-comment': 'off', + + // 通用规则 + 'no-console': 'off', + 'unused-imports/no-unused-vars': 'warn', + + // 关闭一些过于严格的规则 + 'antfu/if-newline': 'off', + 'style/brace-style': ['error', '1tbs'], }, }, + // 集成 VueVine 配置 ...VueVine(), - ...oxlint.configs['flat/recommended'], // oxlint should be the last one -] +) diff --git a/package.json b/package.json index 778f728..35e3f74 100644 --- a/package.json +++ b/package.json @@ -13,84 +13,82 @@ "@guolao/vue-monaco-editor": "^1.5.5", "@hyperdx/browser": "^0.21.2", "@hyperdx/cli": "^0.1.0", - "@microsoft/signalr": "^8.0.7", - "@microsoft/signalr-protocol-msgpack": "^8.0.7", + "@microsoft/signalr": "^9.0.6", + "@microsoft/signalr-protocol-msgpack": "^9.0.6", "@mixer/postmessage-rpc": "^1.1.4", "@oneidentity/zstd-js": "^1.0.3", - "@tauri-apps/api": "^2.5.0", - "@tauri-apps/plugin-autostart": "^2.3.0", - "@tauri-apps/plugin-http": "^2.4.4", - "@tauri-apps/plugin-log": "^2.4.0", - "@tauri-apps/plugin-notification": "^2.2.2", - "@tauri-apps/plugin-opener": "^2.2.7", - "@tauri-apps/plugin-os": "^2.2.1", - "@tauri-apps/plugin-process": "^2.2.1", - "@tauri-apps/plugin-store": "^2.2.0", - "@tauri-apps/plugin-updater": "^2.7.1", + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-autostart": "^2.5.0", + "@tauri-apps/plugin-http": "^2.5.2", + "@tauri-apps/plugin-log": "^2.7.0", + "@tauri-apps/plugin-notification": "^2.3.1", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-os": "^2.3.1", + "@tauri-apps/plugin-process": "^2.3.0", + "@tauri-apps/plugin-store": "^2.4.0", + "@tauri-apps/plugin-updater": "^2.9.0", "@types/crypto-js": "^4.2.2", "@types/md5": "^2.3.5", "@types/vue-cropperjs": "^4.1.6", "@vicons/fluent": "^0.13.0", - "@vitejs/plugin-vue": "^5.2.4", - "@vueuse/core": "^13.3.0", - "@vueuse/integrations": "^13.3.0", - "@vueuse/router": "^13.3.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vueuse/core": "^13.9.0", + "@vueuse/integrations": "^13.9.0", + "@vueuse/router": "^13.9.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", "bilibili-live-ws": "^6.3.1", - "cropperjs": "^2.0.0", + "cropperjs": "^2.0.1", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "easy-speech": "^2.4.0", - "echarts": "^5.6.0", - "eslint-plugin-oxlint": "^0.16.12", + "echarts": "^6.0.0", "fast-xml-parser": "^5.2.5", "file-saver": "^2.0.5", "grapheme-splitter": "^1.0.4", "html2canvas": "^1.4.1", - "jszip": "^3.10.1", "idb-keyval": "^6.2.2", - "linqts": "^2.0.0", + "jszip": "^3.10.1", + "linqts": "^3.2.0", "lodash": "^4.17.21", "md5": "^2.3.0", "mitt": "^3.0.1", - "monaco-editor": "^0.52.2", - "naive-ui": "^2.41.1", - "nanoid": "^5.1.5", + "monaco-editor": "^0.53.0", + "naive-ui": "^2.43.1", + "nanoid": "^5.1.6", "peerjs": "^1.5.5", "pinia": "^3.0.3", "qrcode.vue": "^3.6.0", - "unplugin-auto-import": "^19.3.0", - "unplugin-vue-components": "^28.7.0", - "unplugin-vue-markdown": "^28.3.1", - "uuid": "^11.1.0", - "vite": "6.3.4", - "vite-plugin-oxlint": "^1.3.3", + "unplugin-auto-import": "^20.2.0", + "unplugin-vue-components": "^29.1.0", + "unplugin-vue-markdown": "^29.2.0", + "uuid": "^13.0.0", + "vite": "7.1.7", "vite-svg-loader": "^5.1.0", - "vue": "3.5.13", + "vue": "3.5.22", "vue-cropperjs": "^5.0.0", - "vue-echarts": "^7.0.3", + "vue-echarts": "^8.0.0", "vue-request": "^2.0.4", "vue-router": "^4.5.1", "vue-turnstile": "^1.0.11", "vue3-aplayer": "^1.7.3", "vue3-marquee": "^4.2.2", - "vueuc": "^0.4.64", - "worker-timers": "^8.0.22", + "vueuc": "^0.4.65", + "worker-timers": "^8.0.25", "xlsx": "^0.18.5" }, "devDependencies": { - "@types/bun": "^1.2.16", + "@antfu/eslint-config": "^5.4.1", + "@types/bun": "^1.2.23", "@types/file-saver": "^2.0.7", "@types/jszip": "^3.4.1", - "@types/uuid": "^10.0.0", + "@types/uuid": "^11.0.0", "@vicons/ionicons5": "^0.13.0", - "@vitejs/plugin-vue-jsx": "^4.2.0", - "@vue-vine/eslint-config": "^0.2.20", - "eslint": "^9.29.0", - "eslint-plugin-vue": "^10.2.0", + "@vitejs/plugin-vue-jsx": "^5.1.1", + "@vue-vine/eslint-config": "^1.1.9", + "eslint": "^9.36.0", "stylus": "^0.64.0", - "typescript": "^5.9.0-dev.20250614", - "vue-vine": "^0.4.4" + "typescript": "^5.9.2", + "vue-vine": "^1.7.6" } } diff --git a/src/api/api-models.ts b/src/api/api-models.ts index 17da721..91952c0 100644 --- a/src/api/api-models.ts +++ b/src/api/api-models.ts @@ -413,13 +413,22 @@ export interface LotteryUserCardInfo { export interface ScheduleWeekInfo { year: number week: number - days: ScheduleDayInfo[] + days: ScheduleDayInfo[][] } export interface ScheduleDayInfo { title: string | null tag: string | null tagColor: string | null time: string | null + id: string | null +} + +export interface BatchScheduleRequest { + startYear: number + startWeek: number + count: number + dayOfWeek: number + schedule: ScheduleDayInfo } export enum ThemeType { Auto = 'auto', diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 253a6b9..9ff7970 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -115,6 +115,7 @@ declare global { const getActivePinia: typeof import('pinia')['getActivePinia'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] + const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] const getDate: typeof import('date-fns')['getDate'] const getDay: typeof import('date-fns')['getDay'] const getDayOfYear: typeof import('date-fns')['getDayOfYear'] @@ -179,6 +180,7 @@ declare global { const isSameWeek: typeof import('date-fns')['isSameWeek'] const isSameYear: typeof import('date-fns')['isSameYear'] const isSaturday: typeof import('date-fns')['isSaturday'] + const isShallow: typeof import('vue')['isShallow'] const isSunday: typeof import('date-fns')['isSunday'] const isThisHour: typeof import('date-fns')['isThisHour'] const isThisISOWeek: typeof import('date-fns')['isThisISOWeek'] @@ -507,6 +509,7 @@ declare global { const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] + const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl'] const useTimeout: typeof import('@vueuse/core')['useTimeout'] const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] @@ -553,6 +556,6 @@ declare global { // for type re-export declare global { // @ts-ignore - export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' import('vue') } diff --git a/src/components.d.ts b/src/components.d.ts index 32b6e3c..2860afd 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -19,27 +19,14 @@ declare module 'vue' { LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default'] MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default'] NAlert: typeof import('naive-ui')['NAlert'] - NAvatar: typeof import('naive-ui')['NAvatar'] - NButton: typeof import('naive-ui')['NButton'] NCard: typeof import('naive-ui')['NCard'] - NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem'] - NEllipsis: typeof import('naive-ui')['NEllipsis'] - NEmpty: typeof import('naive-ui')['NEmpty'] NFlex: typeof import('naive-ui')['NFlex'] NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NGridItem: typeof import('naive-ui')['NGridItem'] NIcon: typeof import('naive-ui')['NIcon'] - NImage: typeof import('naive-ui')['NImage'] - NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] - NModal: typeof import('naive-ui')['NModal'] - NPopconfirm: typeof import('naive-ui')['NPopconfirm'] NScrollbar: typeof import('naive-ui')['NScrollbar'] - NSpace: typeof import('naive-ui')['NSpace'] - NSwitch: typeof import('naive-ui')['NSwitch'] NTag: typeof import('naive-ui')['NTag'] NText: typeof import('naive-ui')['NText'] - NTime: typeof import('naive-ui')['NTime'] PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default'] PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default'] PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default'] @@ -55,7 +42,6 @@ declare module 'vue' { SongList: typeof import('./components/SongList.vue')['default'] SongPlayer: typeof import('./components/SongPlayer.vue')['default'] TempComponent: typeof import('./components/TempComponent.vue')['default'] - ToolDynamicNineGrid: typeof import('./components/manage/tools/ToolDynamicNineGrid.vue')['default'] TurnstileVerify: typeof import('./components/TurnstileVerify.vue')['default'] UpdateNoteContainer: typeof import('./components/UpdateNoteContainer.vue')['default'] UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default'] diff --git a/src/components/ScheduleList.vue b/src/components/ScheduleList.vue index cf784de..67da69e 100644 --- a/src/components/ScheduleList.vue +++ b/src/components/ScheduleList.vue @@ -1,9 +1,12 @@ @@ -78,67 +83,199 @@ const emit = defineEmits<{ - - - - - - - - {{ day.tag }} - - - - - - - 休息 - - - - + + + + {{ weekdays[index] }} + + + + - - {{ weekdays[index] }} - {{ day.time }} + 休息 - - - - - {{ day.title }} - - - - + + + + + + + + + + + {{ schedule.tag }} + + + + + + + {{ schedule.time }} + + + + + + × + + + + + + + + {{ schedule.title }} + + + + + + + + × + + + + + + + + diff --git a/src/views/ManageLayout.vue b/src/views/ManageLayout.vue index 95fdae9..691ac13 100644 --- a/src/views/ManageLayout.vue +++ b/src/views/ManageLayout.vue @@ -23,7 +23,7 @@ import { VideoAdd20Filled, Mail24Filled, } from '@vicons/fluent' -import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye, PlayForward, PlayBack, Play, Pause, VolumeHigh, ChevronUp, ChevronDown, TrashBin } from '@vicons/ionicons5' +import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye, PlayForward, PlayBack, Play, Pause, VolumeHigh, ChevronUp, ChevronDown, TrashBin, Bookmark, BookmarkOutline } from '@vicons/ionicons5' import { useElementSize, useStorage } from '@vueuse/core' import { NAlert, @@ -68,6 +68,50 @@ const themeType = useStorage('Settings.Theme', ThemeType.Auto) // 收藏功能相关 const favoriteMenuItems = useStorage('Settings.FavoriteMenuItems', []) +const isFavorite = (key: string) => favoriteMenuItems.value?.includes(key) +const toggleFavorite = (key: string) => { + const list = favoriteMenuItems.value ?? [] + const idx = list.indexOf(key) + if (idx === -1) list.unshift(key) + else list.splice(idx, 1) + favoriteMenuItems.value = [...list] +} +const renderFavoriteExtra = (key: string) => () => + h( + 'span', + { class: ['menu-fav', isFavorite(key) ? 'active' : ''] }, + [ + h( + NTooltip, + { placement: 'right' }, + { + trigger: () => + h( + NButton, + { + text: true, + size: 'tiny', + circle: true, + onClick: (e: MouseEvent) => { + e.stopPropagation() + toggleFavorite(key) + }, + style: 'padding: 0; height: 18px; width: 18px;' + }, + { + icon: () => + h(NIcon, { + component: isFavorite(key) ? Bookmark : BookmarkOutline, + size: 16, + color: isFavorite(key) ? '#f5c451' : undefined, + }), + }, + ), + default: () => (isFavorite(key) ? '取消收藏' : '收藏'), + }, + ), + ], + ) // 侧边栏和布局相关 const sider = ref() @@ -130,259 +174,323 @@ const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified) // 图标渲染函数 - 用于菜单项 const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) }) -// 菜单配置 +// 菜单配置(支持分组与收藏置顶) const menuOptions = computed(() => { - return [ - { + // 通用的菜单项工厂,自动挂载收藏按钮到叶子节点 + const withFavoriteExtra = (item: any): any => { + if (item?.children?.length) { + return { + ...item, + children: item.children.map(withFavoriteExtra), + } + } + return { + ...item, + extra: width.value >= 180 ? renderFavoriteExtra(item.key) : undefined, + } + } + + const commonItems = [ + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-history' } }, { default: () => '历史' }), key: 'manage-history', disabled: accountInfo.value?.isEmailVerified === false, icon: renderIcon(AnalyticsSharp), - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-live' } }, { default: () => '直播记录' }), key: 'manage-live', disabled: accountInfo.value?.isEmailVerified === false, icon: renderIcon(Live24Filled), - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-analyze' } }, { default: () => '直播数据' }), key: 'manage-analyze', disabled: accountInfo.value?.isEmailVerified === false, icon: renderIcon(Eye), - }, - { + }), + ] + + const dataItems = [ + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-event' } }, { default: () => '舰长和SC' }), key: 'manage-event', disabled: accountInfo.value?.isEmailVerified === false, icon: renderIcon(VehicleShip24Filled), - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-point' } }, { default: () => '积分和礼物' }), key: 'manage-point', disabled: accountInfo.value?.isEmailVerified === false, icon: renderIcon(BookCoins20Filled), - }, - { + }), + ] + + const toolsItems = [ + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-schedule' } }, { default: () => '日程' }), key: 'manage-schedule', icon: renderIcon(CalendarClock24Filled), disabled: accountInfo.value?.isEmailVerified === false, - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-songList' } }, { default: () => '歌单' }), key: 'manage-songList', icon: renderIcon(MusicalNote), disabled: accountInfo.value?.isEmailVerified === false, - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-questionBox' } }, { default: () => '棉花糖 (提问箱' }), key: 'manage-questionBox', icon: renderIcon(Chatbox), disabled: accountInfo.value?.isEmailVerified === false, - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-videoCollect' } }, { default: () => '视频征集' }), key: 'manage-videoCollect', icon: renderIcon(VideoAdd20Filled), disabled: accountInfo.value?.isEmailVerified === false, - }, - { + }), + withFavoriteExtra({ label: () => h(RouterLink, { to: { name: 'manage-lottery' } }, { default: () => '动态抽奖' }), key: 'manage-lottery', icon: renderIcon(Lottery24Filled), - }, - { - label: () => h( - NTooltip, - {}, - { - trigger: () => h( - NText, - () => [ - '弹幕相关', - h( - NTooltip, - { style: 'padding: 0;' }, - { - trigger: () => h(NIcon, { component: Info24Filled }), - default: () => h( - NAlert, - { - type: 'warning', - size: 'small', - title: '可用性警告', - style: 'max-width: 600px;', - }, - () => h('div', {}, [ - ' 当浏览器在后台运行时, 定时器和 Websocket 连接将受到严格限制, 这会导致弹幕接收功能无法正常工作 (详见', - h( - NButton, - { - text: true, - tag: 'a', - href: 'https://developer.chrome.com/blog/background_tabs/', - target: '_blank', - type: 'info', - }, - () => '此文章', - ), - '), 虽然本站已经针对此问题做出了处理, 一般情况下即使掉线了也会重连, 不过还是有可能会遗漏事件', - h('br'), - '为避免这种情况, 建议注册本站账后使用', - h( - NButton, - { - type: 'primary', - text: true, - size: 'small', - tag: 'a', - href: 'https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs', - target: '_blank', - }, - () => 'VtsuruEventFetcher', - ), - ', 否则请在使用功能时尽量保持网页在前台运行, 同时关闭浏览器的 页面休眠/内存节省 功能', - h('br'), - 'Chrome: ', - h( - NButton, - { - type: 'info', - text: true, - size: 'small', - tag: 'a', - href: 'https://support.google.com/chrome/answer/12929150?hl=zh-Hans#zippy=%2C%E5%BC%80%E5%90%AF%E6%88%96%E5%85%B3%E9%97%AD%E7%9C%81%E5%86%85%E5%AD%98%E6%A8%A1%E5%BC%8F%2C%E8%AE%A9%E7%89%B9%E5%AE%9A%E7%BD%91%E7%AB%99%E4%BF%9D%E6%8C%81%E6%B4%BB%E5%8A%A8%E7%8A%B6%E6%80%81', - target: '_blank', - }, - () => '让特定网站保持活动状态', - ), - ', Edge: ', - h( - NButton, - { - type: 'info', - text: true, - size: 'small', - tag: 'a', - href: 'https://support.microsoft.com/zh-cn/topic/%E4%BA%86%E8%A7%A3-microsoft-edge-%E4%B8%AD%E7%9A%84%E6%80%A7%E8%83%BD%E5%8A%9F%E8%83%BD-7b36f363-2119-448a-8de6-375cfd88ab25', - target: '_blank', - }, - () => '永远不想进入睡眠状态的网站', - ), - ]), - ), - }, - ), - ] - ), - default: () => isBiliVerified.value - ? '需要使用直播弹幕的功能' - : '你尚未进行 Bilibili 认证, 请前往面板进行绑定', - }, - ), - key: 'manage-danmaku', - icon: renderIcon(Chat24Filled), - disabled: accountInfo.value?.isEmailVerified === false || !isBiliVerified.value, - children: [ - { - label: () => !isBiliVerified.value ? '弹幕机' : h( - NBadge, - { value: '新', offset: [15, 12], type: 'info' }, - () => h( - NTooltip, - {}, - { - trigger: () => h( - RouterLink, - { to: { name: 'manage-danmuji' } }, - { default: () => '弹幕机' }, - ), - default: () => '兼容 blivechat 样式 (其实就是直接用的 blivechat 组件', - } - ) - ), - key: 'manage-danmuji', - disabled: !isBiliVerified.value, - icon: renderIcon(Lottery24Filled), - }, - { - label: () => !isBiliVerified.value ? '抽奖' : h( - RouterLink, - { to: { name: 'manage-liveLottery' } }, - { default: () => '抽奖' }, - ), - key: 'manage-liveLottery', - icon: renderIcon(Lottery24Filled), - disabled: !isBiliVerified.value, - }, - { - label: () => !isBiliVerified.value ? '点播' : h( - NTooltip, - {}, - { - trigger: () => h( - RouterLink, - { to: { name: 'manage-liveRequest' } }, - { default: () => '点播' }, - ), - default: () => '歌势之类用的, 可以用来点歌或者跳舞什么的', - }, - ), - key: 'manage-liveRequest', - icon: renderIcon(MusicalNote), - disabled: !isBiliVerified.value, - }, - { - label: () => !isBiliVerified.value ? '点歌' : h( - NTooltip, - {}, - { - trigger: () => h( - RouterLink, - { to: { name: 'manage-musicRequest' } }, - { default: () => '点歌' }, - ), - default: () => '就是传统的点歌机, 发弹幕后播放指定的歌曲', - }, - ), - key: 'manage-musicRequest', - icon: renderIcon(MusicalNote), - disabled: !isBiliVerified.value, - }, - { - label: () => !isBiliVerified.value ? '排队' : h( - RouterLink, - { to: { name: 'manage-liveQueue' } }, - { default: () => '排队' }, - ), - key: 'manage-liveQueue', - icon: renderIcon(PeopleQueue24Filled), - disabled: !isBiliVerified.value, - }, - { - label: () => !isBiliVerified.value ? '读弹幕' : h( - RouterLink, - { to: { name: 'manage-speech' } }, - { default: () => '读弹幕' }, - ), - key: 'manage-speech', - icon: renderIcon(TabletSpeaker24Filled), - disabled: !isBiliVerified.value, - }, - /*{ - label: () => !isBiliVerified.value ? '弹幕投票' : h( - RouterLink, - { to: { name: 'manage-danmakuVote' } }, - { default: () => '弹幕投票' }, - ), - key: 'manage-danmakuVote', - icon: renderIcon(Chat24Filled), - disabled: !isBiliVerified.value, - },*/ - ], - }, + }), ] + + const danmakuItem = { + label: () => h( + NTooltip, + {}, + { + trigger: () => h( + NText, + () => [ + '弹幕相关', + h( + NTooltip, + { style: 'padding: 0;' }, + { + trigger: () => h(NIcon, { component: Info24Filled }), + default: () => h( + NAlert, + { + type: 'warning', + size: 'small', + title: '可用性警告', + style: 'max-width: 600px;', + }, + () => h('div', {}, [ + ' 当浏览器在后台运行时, 定时器和 Websocket 连接将受到严格限制, 这会导致弹幕接收功能无法正常工作 (详见', + h( + NButton, + { + text: true, + tag: 'a', + href: 'https://developer.chrome.com/blog/background_tabs/', + target: '_blank', + type: 'info', + }, + () => '此文章', + ), + '), 虽然本站已经针对此问题做出了处理, 一般情况下即使掉线了也会重连, 不过还是有可能会遗漏事件', + h('br'), + '为避免这种情况, 建议注册本站账后使用', + h( + NButton, + { + type: 'primary', + text: true, + size: 'small', + tag: 'a', + href: 'https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs', + target: '_blank', + }, + () => 'VtsuruEventFetcher', + ), + ', 否则请在使用功能时尽量保持网页在前台运行, 同时关闭浏览器的 页面休眠/内存节省 功能', + h('br'), + 'Chrome: ', + h( + NButton, + { + type: 'info', + text: true, + size: 'small', + tag: 'a', + href: 'https://support.google.com/chrome/answer/12929150?hl=zh-Hans#zippy=%2C%E5%BC%80%E5%90%AF%E6%88%96%E5%85%B3%E9%97%AD%E7%9C%81%E5%86%85%E5%AD%98%E6%A8%A1%E5%BC%8F%2C%E8%AE%A9%E7%89%B9%E5%AE%9A%E7%BD%91%E7%AB%99%E4%BF%9D%E6%8C%81%E6%B4%BB%E5%8A%A8%E7%8A%B6%E6%80%81', + target: '_blank', + }, + () => '让特定网站保持活动状态', + ), + ', Edge: ', + h( + NButton, + { + type: 'info', + text: true, + size: 'small', + tag: 'a', + href: 'https://support.microsoft.com/zh-cn/topic/%E4%BA%86%E8%A7%A3-microsoft-edge-%E4%B8%AD%E7%9A%84%E6%80%A7%E8%83%BD%E5%8A%9F%E8%83%BD-7b36f363-2119-448a-8de6-375cfd88ab25', + target: '_blank', + }, + () => '永远不想进入睡眠状态的网站', + ), + ]), + ), + }, + ), + ] + ), + default: () => (isBiliVerified.value + ? '需要使用直播弹幕的功能' + : '你尚未进行 Bilibili 认证, 请前往面板进行绑定'), + }, + ), + key: 'manage-danmaku', + icon: renderIcon(Chat24Filled), + disabled: accountInfo.value?.isEmailVerified === false || !isBiliVerified.value, + children: [ + withFavoriteExtra({ + label: () => !isBiliVerified.value ? '弹幕机' : h(NTooltip, + {}, + { + trigger: () => h( + RouterLink, + { to: { name: 'manage-danmuji' } }, + { default: () => '弹幕机' }, + ), + default: () => '兼容 blivechat 样式 (其实就是直接用的 blivechat 组件', + }), + key: 'manage-danmuji', + disabled: !isBiliVerified.value, + icon: renderIcon(Lottery24Filled), + }), + withFavoriteExtra({ + label: () => !isBiliVerified.value ? '抽奖' : h( + RouterLink, + { to: { name: 'manage-liveLottery' } }, + { default: () => '抽奖' }, + ), + key: 'manage-liveLottery', + icon: renderIcon(Lottery24Filled), + disabled: !isBiliVerified.value, + }), + withFavoriteExtra({ + label: () => !isBiliVerified.value ? '点播' : h( + NTooltip, + {}, + { + trigger: () => h( + RouterLink, + { to: { name: 'manage-liveRequest' } }, + { default: () => '点播' }, + ), + default: () => '歌势之类用的, 可以用来点歌或者跳舞什么的', + }, + ), + key: 'manage-liveRequest', + icon: renderIcon(MusicalNote), + disabled: !isBiliVerified.value, + }), + withFavoriteExtra({ + label: () => !isBiliVerified.value ? '点歌' : h( + NTooltip, + {}, + { + trigger: () => h( + RouterLink, + { to: { name: 'manage-musicRequest' } }, + { default: () => '点歌' }, + ), + default: () => '就是传统的点歌机, 发弹幕后播放指定的歌曲', + }, + ), + key: 'manage-musicRequest', + icon: renderIcon(MusicalNote), + disabled: !isBiliVerified.value, + }), + withFavoriteExtra({ + label: () => !isBiliVerified.value ? '排队' : h( + RouterLink, + { to: { name: 'manage-liveQueue' } }, + { default: () => '排队' }, + ), + key: 'manage-liveQueue', + icon: renderIcon(PeopleQueue24Filled), + disabled: !isBiliVerified.value, + }), + withFavoriteExtra({ + label: () => !isBiliVerified.value ? '读弹幕' : h( + RouterLink, + { to: { name: 'manage-speech' } }, + { default: () => '读弹幕' }, + ), + key: 'manage-speech', + icon: renderIcon(TabletSpeaker24Filled), + disabled: !isBiliVerified.value, + }), + /*withFavoriteExtra({ + label: () => !isBiliVerified.value ? '弹幕投票' : h( + RouterLink, + { to: { name: 'manage-danmakuVote' } }, + { default: () => '弹幕投票' }, + ), + key: 'manage-danmakuVote', + icon: renderIcon(Chat24Filled), + disabled: !isBiliVerified.value, + }),*/ + ], + } + + // 扁平化叶子项用于收藏置顶 + const flattenLeaf = (items: any[]): any[] => { + const result: any[] = [] + for (const it of items) { + if (it.children?.length) { + result.push(...flattenLeaf(it.children)) + } else { + result.push(it) + } + } + return result + } + + const allLeaf = [ + ...flattenLeaf(commonItems), + ...flattenLeaf(dataItems), + ...flattenLeaf(toolsItems), + ...flattenLeaf(danmakuItem.children ?? []), + ] + const leafMap = new Map(allLeaf.map(i => [i.key, i])) + + const favorites = (favoriteMenuItems.value ?? []) + .map(k => leafMap.get(k)) + .filter(Boolean) as any[] + + const notFav = (i: any) => !isFavorite(i.key) + const danmakuChildren = (danmakuItem.children ?? []).filter(notFav) + const danmakuForGroup = danmakuChildren.length > 0 ? { ...danmakuItem, children: danmakuChildren } : null + + const groups: any[] = [] + if (favorites.length > 0) { + groups.push({ type: 'group', key: 'group-favorites', label: '我的收藏', children: favorites }) + } + if (commonItems.filter(notFav).length > 0) { + groups.push({ type: 'group', key: 'group-common', label: '常用', children: commonItems.filter(notFav) }) + } + if (dataItems.filter(notFav).length > 0) { + groups.push({ type: 'group', key: 'group-data', label: '数据', children: dataItems.filter(notFav) }) + } + const toolsGroupChildren = [ + ...(danmakuForGroup ? [danmakuForGroup] : []), + ...toolsItems.filter(notFav), + ] + if (toolsGroupChildren.length > 0) { + groups.push({ type: 'group', key: 'group-tools', label: '互动与工具', children: toolsGroupChildren }) + } + + return groups }) // 重发验证邮件 @@ -590,11 +698,15 @@ onMounted(() => { @@ -1337,4 +1449,40 @@ onMounted(() => { .music-player-card .n-tag:hover { transform: scale(1.05); } + +/* 侧边栏菜单收藏按钮与紧凑样式 */ +:deep(.manage-sider-menu .menu-fav) { + opacity: 0; + width: 0; + margin-left: 0; + overflow: hidden; + transition: opacity 0.15s ease, width 0.15s ease, margin-left 0.15s ease; + pointer-events: none; /* 不阻挡文字区域点击 */ + display: inline-flex; + align-items: center; + justify-content: center; +} + +:deep(.manage-sider-menu .n-menu-item:hover .menu-fav), +:deep(.manage-sider-menu .menu-fav.active) { + opacity: 1; + width: 18px; + margin-left: 6px; + pointer-events: auto; +} + +:deep(.manage-sider-menu .menu-fav .n-button) { + padding: 0; + height: 18px; + width: 18px; +} + +/* 略微收紧图标与文本的间距,提升有效可读宽度 */ +:deep(.manage-sider-menu .n-menu-item .n-menu-item-content .n-menu-item-content__icon) { + margin-right: 6px; +} + +:deep(.manage-sider-menu .n-menu-item .n-menu-item-content) { + padding-right: 6px; +} diff --git a/src/views/manage/ScheduleManageView.vue b/src/views/manage/ScheduleManageView.vue index 1806362..28c5fd0 100644 --- a/src/views/manage/ScheduleManageView.vue +++ b/src/views/manage/ScheduleManageView.vue @@ -1,21 +1,22 @@