diff --git a/bun.lockb b/bun.lockb index 6ec8f3d..28847b1 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 28b1bbc..8064826 100644 --- a/package.json +++ b/package.json @@ -90,8 +90,10 @@ "eslint": "^9.36.0", "eslint-plugin-oxlint": "^1.19.0", "oxlint": "^1.19.0", + "rollup-plugin-visualizer": "^6.0.4", "stylus": "^0.64.0", "typescript": "^5.9.2", + "vite-plugin-cdn-import": "^1.0.1", "vue-vine": "^1.7.6" } } diff --git a/src/client/components/danmaku/danmakuUtils.ts b/src/client/components/danmaku/danmakuUtils.ts index dd1b0aa..e5520c3 100644 --- a/src/client/components/danmaku/danmakuUtils.ts +++ b/src/client/components/danmaku/danmakuUtils.ts @@ -1,4 +1,4 @@ -import type { ComputedRef } from 'vue' +import type { ComputedRef, Ref } from 'vue' import type { DanmakuWindowSettings } from '../../store/useDanmakuWindow' import type { EventModel } from '@/api/api-models' import { computed } from 'vue' @@ -67,7 +67,8 @@ export interface BaseDanmakuItemProps { export function useDanmakuUtils( props: BaseDanmakuItemProps, - emojiData: { data: { inline: { [key: string]: string }, plain: { [key: string]: string } } }, + emojiData: Ref<{ updateAt?: number, data: { inline: { [key: string]: string }, plain: { [key: string]: string } } }> + | { data: { inline: { [key: string]: string }, plain: { [key: string]: string } } }, ) { // 计算SC弹幕的颜色类 const scColorClass = computed(() => { @@ -124,7 +125,12 @@ export function useDanmakuUtils( let match try { - const availableEmojis = emojiData.data || {} // 确保 emojiData 已加载 + // 兼容 Ref 和 普通对象两种传参 + const store = (emojiData as any)?.value ?? emojiData + const availableEmojis = (store?.data ?? { inline: {}, plain: {} }) as { + inline?: { [key: string]: string } + plain?: { [key: string]: string } + } while ((match = regex.exec(props.item.msg)) !== null) { // 添加表情前的文本部分 @@ -133,7 +139,12 @@ export function useDanmakuUtils( } const emojiFullName = match[0] // 完整匹配,例如 "[哈哈]" - const emojiInfo = availableEmojis.inline[emojiFullName] || availableEmojis.plain[emojiFullName] + const emojiName = match[1] // 去除方括号后的名称,例如 "哈哈" + // 兼容键名为带/不带方括号的两种情况 + const emojiInfo = (availableEmojis.inline?.[emojiFullName] + ?? availableEmojis.inline?.[emojiName] + ?? availableEmojis.plain?.[emojiFullName] + ?? availableEmojis.plain?.[emojiName]) as string | undefined if (emojiInfo) { // 找到了表情 diff --git a/src/components.d.ts b/src/components.d.ts index 886add7..382f8ff 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -22,13 +22,19 @@ declare module 'vue' { NAvatar: typeof import('naive-ui')['NAvatar'] NButton: typeof import('naive-ui')['NButton'] NCard: typeof import('naive-ui')['NCard'] + 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'] NPopconfirm: typeof import('naive-ui')['NPopconfirm'] + NScrollbar: typeof import('naive-ui')['NScrollbar'] NSpace: typeof import('naive-ui')['NSpace'] 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'] diff --git a/src/router/index.ts b/src/router/index.ts index a8782c7..cb555a1 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -14,7 +14,7 @@ const routes: Array = [ { path: '/', name: 'index', - component: IndexView, + component: async () => import('@/views/IndexView.vue'), meta: { title: '你好', }, diff --git a/vite.config.mts b/vite.config.mts index b9701a4..8cee4b6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -9,6 +9,7 @@ import Markdown from 'unplugin-vue-markdown/vite' import { defineConfig } from 'vite' import svgLoader from 'vite-svg-loader' import { VineVitePlugin } from 'vue-vine/vite' +import { visualizer } from 'rollup-plugin-visualizer'; // 自定义SVGO插件,删除所有名称以sodipodi:和inkscape:开头的元素 const removeSodipodiInkscape = { @@ -87,6 +88,14 @@ export default defineConfig({ include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.vine$/], }), VineVitePlugin(), + visualizer({ + open: false, // 不自动打开浏览器,避免影响 CI/CD + gzipSize: true, // 显示 Gzip 压缩后的大小 + brotliSize: true, // 显示 Brotli 压缩后的大小 + filename: 'dist/stats.html', // 分析报告的输出路径 + template: 'treemap', // 使用树图模式展示 + sourcemap: true, // 使用 sourcemap 进行更精确的分析 + }), ], server: { port: 51000 }, resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, @@ -96,29 +105,161 @@ export default defineConfig({ '__BUILD_TIME__': JSON.stringify(new Date().toISOString()), }, optimizeDeps: { - include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router'], + include: [ + 'vue', + 'vue-router', + 'pinia', + '@vueuse/core', + 'naive-ui', + 'date-fns', + '@vicons/fluent', + '@vicons/ionicons5', + ], + exclude: ['@tauri-apps/api', '@tauri-apps/plugin-autostart', '@tauri-apps/plugin-http'], }, build: { sourcemap: true, target: 'esnext', minify: 'oxc', - chunkSizeWarningLimit: 1000, + chunkSizeWarningLimit: 1600, // 调整警告阈值,gamepad-assets 会比较大 + cssCodeSplit: true, + cssMinify: 'lightningcss', + // 报告压缩后的文件大小 + reportCompressedSize: true, + // 优化模块预加载策略 + modulePreload: { + polyfill: true, + }, + // 静态资源内联阈值(小于此大小的资源会被内联为 base64) + assetsInlineLimit: 4096, // 4KB,默认值,可根据需要调整 rollupOptions: { - output: { // @ts-ignore + output: { advancedChunks: { groups: [ { - name: 'vue-vendor', - test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/, - priority: -10, - }, - { - name: 'ui-vendor', - test: /[\\/]node_modules[\\/](naive-ui|@vueuse[\\/]core)[\\/]/, - priority: -10, + name: (id: string) => { + // 核心框架 + if (id.includes('node_modules/vue/') || id.includes('node_modules/@vue/')) { + return 'vue-core' + } + if (id.includes('node_modules/vue-router/')) { + return 'vue-router' + } + if (id.includes('node_modules/pinia/')) { + return 'pinia' + } + + // UI 框架及相关 + if (id.includes('node_modules/naive-ui/') || id.includes('node_modules/vueuc/')) { + return 'naive-ui' + } + + // VueUse 系列 + if (id.includes('node_modules/@vueuse/')) { + return 'vueuse' + } + + // 图标库 + if (id.includes('node_modules/@vicons/')) { + return 'icons' + } + + // Gamepad 配置和资源(体积大,单独分离) + if (id.includes('/gamepadConfigs') || id.includes('assets/controller/')) { + return 'gamepad-assets' + } + + // 字典文件(拼音、假名等,按需加载) + if (id.includes('/dictPinyin')) { + return 'dict-pinyin' + } + if (id.includes('/dictKana')) { + return 'dict-kana' + } + + // Monaco Editor (代码编辑器,体积大) + if (id.includes('node_modules/monaco-editor/') || id.includes('node_modules/@guolao/vue-monaco-editor/')) { + return 'monaco-editor' + } + + // ECharts (图表库,体积大) + if (id.includes('node_modules/echarts/') || id.includes('node_modules/vue-echarts/')) { + return 'echarts' + } + + // 富文本编辑器 + if (id.includes('node_modules/@wangeditor/')) { + return 'wangeditor' + } + + // SignalR 相关 + if (id.includes('node_modules/@microsoft/signalr')) { + return 'signalr' + } + + // Tauri 相关(客户端专用) + if (id.includes('node_modules/@tauri-apps/')) { + return 'tauri' + } + + // B站直播弹幕客户端 + if (id.includes('node_modules/bilibili-live-danmaku/')) { + return 'bili-danmaku' + } + + // 工具库 + if (id.includes('node_modules/lodash/')) { + return 'lodash' + } + if (id.includes('node_modules/date-fns/')) { + return 'date-fns' + } + + // Excel 相关 (体积大) + if (id.includes('node_modules/xlsx/')) { + return 'xlsx' + } + + // 压缩和解压 + if (id.includes('node_modules/jszip/') || id.includes('node_modules/@oneidentity/zstd-js/')) { + return 'compression' + } + + // 其他大型依赖 + if (id.includes('node_modules/html2canvas/')) { + return 'html2canvas' + } + if (id.includes('node_modules/cropperjs/') || id.includes('node_modules/vue-cropperjs/')) { + return 'cropper' + } + + // 通用 node_modules 分离 + if (id.includes('node_modules/')) { + return 'vendor' + } + + return null + }, }, ], }, + chunkFileNames: 'assets/js/[name]-[hash].js', + entryFileNames: 'assets/js/[name]-[hash].js', + assetFileNames: (assetInfo: any) => { + // 根据资源类型分类存放 + const info = assetInfo.name?.split('.') ?? [] + const ext = info[info.length - 1] + if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(ext)) { + return 'assets/images/[name]-[hash][extname]' + } + if (/woff2?|ttf|otf|eot/i.test(ext)) { + return 'assets/fonts/[name]-[hash][extname]' + } + if (/css/i.test(ext)) { + return 'assets/css/[name]-[hash][extname]' + } + return 'assets/[ext]/[name]-[hash][extname]' + }, }, }, },