From db8c176b5564f60bbbdffe6a4cde8feb7bd666b0 Mon Sep 17 00:00:00 2001 From: GouJ Date: Tue, 1 Sep 2026 16:31:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8=20Naive=20UI=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=90=8E=E5=8F=B0=E5=B8=83=E5=B1=80=E4=B8=8E?= =?UTF-8?q?=E9=BB=91=E7=99=BD=E5=8F=8C=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- README.md | 21 +- index.html | 15 +- package.json | 30 +- pnpm-lock.yaml | 331 ++++----- src/App.vue | 248 +++---- src/admin.css | 574 ++++++++++++++++ src/components/ui/AppDialog.vue | 58 +- src/components/ui/AssetImage.vue | 13 +- src/components/ui/EmptyState.vue | 18 +- src/components/ui/StatusBadge.vue | 24 +- src/components/ui/WorkspacePage.vue | 12 + src/components/ui/layout.test.ts | 89 +++ src/composables/useTheme.test.ts | 91 +++ src/composables/useTheme.ts | 88 +++ src/features/breakdown/BreakdownPage.vue | 161 +++-- .../breakdown/components/StoryboardList.vue | 34 +- .../breakdown/components/SubjectList.vue | 53 +- src/features/create-drama/CreateDramaPage.vue | 120 ++-- src/features/production/ProductionPage.vue | 468 +++++++------ .../production/components/KeyframeDialog.vue | 49 +- .../components/ProductionReceipt.vue | 17 +- .../components/ShotProductionAssets.vue | 177 ++--- src/features/production/production.test.ts | 2 +- src/features/projects/ProjectLayout.vue | 125 ++-- src/features/projects/ProjectsPage.vue | 230 +++---- .../components/CreateProjectDialog.vue | 54 +- src/features/storyboard/StoryboardPage.vue | 417 ++++++------ .../components/GenerationReceipt.vue | 15 +- .../storyboard/components/ShotDesign.vue | 8 +- .../storyboard/components/ShotTools.vue | 40 +- .../subject-identity/SubjectIdentityPage.vue | 455 +++++++------ .../components/CastingCandidateDialog.vue | 55 +- .../components/IdentityEditor.vue | 39 +- .../components/IdentityGallery.vue | 59 +- .../components/IdentityImageDialog.vue | 105 +-- .../subject-identity/identity.test.ts | 18 +- .../subject-images/SubjectImagesPage.vue | 248 ++++--- .../components/GenerateImageDialog.vue | 87 +-- .../components/ImageGalleryDialog.vue | 86 +-- src/features/subject-images/images.test.ts | 6 +- src/features/visual-style/VisualStylePage.vue | 51 +- .../visual-style/components/StyleEditor.vue | 82 ++- .../visual-style/components/StyleImages.vue | 76 ++- src/features/workflows/ConfirmAction.vue | 32 +- src/features/workflows/HistoryPanel.vue | 15 +- src/features/workflows/pages.test.ts | 70 +- src/main.ts | 1 + src/router/index.ts | 3 +- src/styles.css | 626 +++--------------- src/testing/naive.ts | 21 + 51 files changed, 3277 insertions(+), 2442 deletions(-) create mode 100644 src/admin.css create mode 100644 src/components/ui/WorkspacePage.vue create mode 100644 src/components/ui/layout.test.ts create mode 100644 src/composables/useTheme.test.ts create mode 100644 src/composables/useTheme.ts create mode 100644 src/testing/naive.ts diff --git a/.gitignore b/.gitignore index 365ffaf..05d0c81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -node_modules +node_modules* dist coverage .env diff --git a/README.md b/README.md index 063ca09..43671ed 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ API_PROXY_TARGET=http://localhost:3412 - Vite 8、Vue 3、TypeScript、Vue Router - Tailwind CSS 4,通过 `@tailwindcss/vite` 集成 -- Reka UI:Dialog、Tabs、Checkbox、Progress 等无样式基础组件 +- Naive UI:布局、菜单、表格、选择器、输入框、按钮、弹窗、标签、折叠面板、进度和时间线 - Oxlint:脚本质量检查;ESLint:补充 Vue 模板语义 - Oxfmt(不是 `oxformat`):沿用后端四空格、单引号、无分号、无尾逗号的风格 - Vitest + Vue Test Utils + happy-dom:API 契约、组件交互和异步生命周期测试 @@ -76,13 +76,28 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S | `src/features/subject-identity` | 稳定身份 API、正式主体目录、文本编辑、生图配置、身份图库和母版切换 | | `src/features/production` | 视频提示词、镜头首帧、视频任务、就绪诊断、资产候选与批量回执 | | `src/features/workflows` | checkpoint 选择、执行记录、长请求互斥与操作确认 | -| `src/components/ui` | 跨业务使用的 Reka UI 封装与公共展示组件 | -| `src/composables` | 具备取消和竞态保护的轮询 | +| `src/components/ui` | Naive UI 公共封装、工作区滚动容器与图片展示 | +| `src/composables` | 具备取消和竞态保护的轮询、系统主题与偏好持久化 | | `src/lib` | HTTP、错误格式、日期和导出工具 | | `src/router` | 按 graph 拆分的懒加载路由 | 每个功能模块通过 `index.ts` 暴露公共入口。业务类型和行为函数均添加中文职责注释。不要把下一条 graph 的服务继续堆进 `App.vue`。 +## 后台布局与主题 + +- 外层应用固定为视口高度;侧栏、顶栏、项目标题和工作流导航不会随正文滚动。 +- 项目列表采用固定表头表格;剧本、分镜、身份与生产页的目录和详情独立滚动,其他工作区只滚动内容区。 +- 生成配置与批量操作放在可折叠区域,回执到达后自动展开;查询错误始终可见,不隐藏在折叠内容中。 +- 右上角提供浅色、暗黑、跟随系统三种模式,默认跟随系统,选择保存在本机浏览器。存储被禁用时仍可在当前会话切换。 +- 以黑白灰为主,仅错误、警告、成功等语义状态保留辅助色。Naive UI 和业务内容共用主题;加载前应用偏好,减少主题闪烁。 +- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。 + +布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`,避免通过 `body` 或 `window.scrollTo` 管理滚动。 + +本次 UI 重构保留前端 `dev@5551653` 的业务更新,包括过期形态图、主首帧和主视频的提示与更新入口。首帧默认只补当前剧集;存在过期主首帧时优先更新全项目过期项。非覆盖模式下的过期主视频重建完成后,会接替旧主视频;普通新增候选不自动替换主资产。 + +本地验收建议使用 1920×1080、1280×720 和窄屏视口:打开长剧本、长项目标题和多镜头项目,确认顶栏不移动、目录与正文能分别滚到底;切换暗黑模式后检查弹窗、下拉菜单、图片失败占位,刷新后主题应保持。 + ## 接口基线 对齐后端 `qianlanse/short-drama-agent` 的 `dev`: diff --git a/index.html b/index.html index e160c49..33b124d 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,20 @@ - + + 短剧工作台 diff --git a/package.json b/package.json index 122e590..e07c0f4 100644 --- a/package.json +++ b/package.json @@ -17,30 +17,30 @@ "prepare": "lefthook install" }, "dependencies": { + "@lucide/vue": "1.34.0", + "naive-ui": "2.45.3", "vue": "3.5.42", - "vue-router": "^4.6.0", - "reka-ui": "2.10.4", - "@lucide/vue": "1.34.0" + "vue-router": "^4.6.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^6.0.0", - "vite": "8.2.2", - "typescript": "^6.0.3", - "vue-tsc": "^3.2.0", - "tailwindcss": "^4.1.0", + "@commitlint/cli": "^21.2.2", + "@commitlint/config-conventional": "^21.2.2", "@tailwindcss/vite": "^4.1.0", "@types/node": "^24.0.0", - "oxlint": "^1.78.0", - "oxfmt": "^0.60.0", + "@typescript-eslint/parser": "^8.67.0", + "@vitejs/plugin-vue": "^6.0.0", + "@vue/test-utils": "^2.4.6", "eslint": "^10.0.0", "eslint-plugin-vue": "^10.0.0", - "@typescript-eslint/parser": "^8.67.0", - "vitest": "^4.0.15", - "@vue/test-utils": "^2.4.6", "happy-dom": "^20.0.0", "lefthook": "^2.1.10", - "@commitlint/cli": "^21.2.2", - "@commitlint/config-conventional": "^21.2.2" + "oxfmt": "^0.60.0", + "oxlint": "^1.78.0", + "tailwindcss": "^4.1.0", + "typescript": "^6.0.3", + "vite": "8.2.2", + "vitest": "^4.0.15", + "vue-tsc": "^3.2.0" }, "engines": { "node": ">=22.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b9c58d..bc501c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,9 @@ importers: '@lucide/vue': specifier: 1.34.0 version: 1.34.0(vue@3.5.42(typescript@6.0.3)) - reka-ui: - specifier: 2.10.4 - version: 2.10.4(vue@3.5.42(typescript@6.0.3)) + naive-ui: + specifier: 2.45.3 + version: 2.45.3(vue@3.5.42(typescript@6.0.3)) vue: specifier: 3.5.42 version: 3.5.42(typescript@6.0.3) @@ -184,6 +184,19 @@ packages: resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} engines: {node: '>=22'} + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==} + peerDependencies: + vue: ^3.0.11 + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -214,18 +227,6 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@floating-ui/core@1.8.0': - resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - - '@floating-ui/dom@1.8.0': - resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - - '@floating-ui/utils@0.2.12': - resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - - '@floating-ui/vue@1.1.11': - resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==} - '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -246,12 +247,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@internationalized/date@3.12.3': - resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} - - '@internationalized/number@3.6.7': - resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -272,6 +267,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@lucide/vue@1.34.0': resolution: {integrity: sha512-eB48kx1oe9O6mJAg0gFWgv66y4rhYJYuhfWF8wIz/YBFPDDHWHI7+kRJpkvz/8kePY6ddbwxXIGfP8E54wlGhA==} peerDependencies: @@ -641,9 +639,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.23': - resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -738,14 +733,6 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tanstack/virtual-core@3.17.8': - resolution: {integrity: sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==} - - '@tanstack/vue-virtual@3.13.36': - resolution: {integrity: sha512-gKpExv4RbB9luVG+SucTXoqPZv/gzu/Yvz6BNO+8kpNxJ2x+I/ulryzl5W9BRciahZGp5Tls3Dp5XP1ztVGbMw==} - peerDependencies: - vue: ^2.7.0 || ^3.0.0 - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -761,12 +748,15 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - '@types/web-bluetooth@0.0.21': - resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -912,19 +902,6 @@ packages: '@vue/server-renderer': optional: true - '@vueuse/core@14.4.0': - resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} - peerDependencies: - vue: ^3.5.0 - - '@vueuse/metadata@14.4.0': - resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} - - '@vueuse/shared@14.4.0': - resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} - peerDependencies: - vue: ^3.5.0 - abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -971,14 +948,13 @@ packages: resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} engines: {node: '>=22'} - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1063,14 +1039,28 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1083,9 +1073,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - defu@6.1.7: - resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1198,6 +1185,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -1275,6 +1265,10 @@ packages: resolution: {integrity: sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg==} engines: {node: '>=20.0.0'} + highlight.js@11.12.0: + resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==} + engines: {node: '>=12.0.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1573,6 +1567,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1597,6 +1597,12 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + naive-ui@2.45.3: + resolution: {integrity: sha512-D83gJ+6zUdkQ5DuUbmlfyaM1t8pV5WMPa6KmL9mByqQuiL12qBdA0n/iEIth4dQw1x/duNGpOS2anpt5FKO1kw==} + engines: {node: '>=20'} + peerDependencies: + vue: ^3.0.0 + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1617,9 +1623,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - ohash@2.0.12: - resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1713,11 +1716,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - reka-ui@2.10.4: - resolution: {integrity: sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw==} - peerDependencies: - vue: '>= 3.4.0' - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1735,6 +1733,9 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + seemly@0.3.10: + resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1815,15 +1816,15 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1842,6 +1843,11 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} + peerDependencies: + vue: ^3.0.11 + vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1926,23 +1932,17 @@ packages: jsdom: optional: true + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==} + peerDependencies: + vue: ^3.0.0 + vscode-uri@3.2.0: resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} vue-component-type-helpers@3.3.11: resolution: {integrity: sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==} - vue-demi@0.14.10: - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - vue-eslint-parser@10.4.1: resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1968,6 +1968,11 @@ packages: typescript: optional: true + vueuc@0.4.66: + resolution: {integrity: sha512-hHrZl+0AsrUvqYPBXSXQ2QaSrO3razjeUseLAhJWlOS/Hdfw/Y2MU05fOTP1qn1A3121gMxsHLaEz2V6CkWdTg==} + peerDependencies: + vue: ^3.0.11 + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -2170,6 +2175,16 @@ snapshots: '@conventional-changelog/template@1.4.0': {} + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.42(typescript@6.0.3))': + dependencies: + vue: 3.5.42(typescript@6.0.3) + + '@emotion/hash@0.8.0': {} + '@eslint-community/eslint-utils@4.10.1(eslint@10.9.1(jiti@2.7.0))': dependencies: eslint: 10.9.1(jiti@2.7.0) @@ -2200,26 +2215,6 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@floating-ui/core@1.8.0': - dependencies: - '@floating-ui/utils': 0.2.12 - - '@floating-ui/dom@1.8.0': - dependencies: - '@floating-ui/core': 1.8.0 - '@floating-ui/utils': 0.2.12 - - '@floating-ui/utils@0.2.12': {} - - '@floating-ui/vue@1.1.11(vue@3.5.42(typescript@6.0.3))': - dependencies: - '@floating-ui/dom': 1.8.0 - '@floating-ui/utils': 0.2.12 - vue-demi: 0.14.10(vue@3.5.42(typescript@6.0.3)) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -2236,14 +2231,6 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@internationalized/date@3.12.3': - dependencies: - '@swc/helpers': 0.5.23 - - '@internationalized/number@3.6.7': - dependencies: - '@swc/helpers': 0.5.23 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2272,6 +2259,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@juggle/resize-observer@3.4.0': {} + '@lucide/vue@1.34.0(vue@3.5.42(typescript@6.0.3))': dependencies: vue: 3.5.42(typescript@6.0.3) @@ -2452,10 +2441,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.23': - dependencies: - tslib: 2.8.1 - '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2524,13 +2509,6 @@ snapshots: tailwindcss: 4.3.3 vite: 8.2.2(@types/node@24.13.3)(jiti@2.7.0) - '@tanstack/virtual-core@3.17.8': {} - - '@tanstack/vue-virtual@3.13.36(vue@3.5.42(typescript@6.0.3))': - dependencies: - '@tanstack/virtual-core': 3.17.8 - vue: 3.5.42(typescript@6.0.3) - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2544,12 +2522,16 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 - '@types/web-bluetooth@0.0.21': {} - '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': @@ -2759,19 +2741,6 @@ snapshots: optionalDependencies: '@vue/server-renderer': 3.5.42 - '@vueuse/core@14.4.0(vue@3.5.42(typescript@6.0.3))': - dependencies: - '@types/web-bluetooth': 0.0.21 - '@vueuse/metadata': 14.4.0 - '@vueuse/shared': 14.4.0(vue@3.5.42(typescript@6.0.3)) - vue: 3.5.42(typescript@6.0.3) - - '@vueuse/metadata@14.4.0': {} - - '@vueuse/shared@14.4.0(vue@3.5.42(typescript@6.0.3))': - dependencies: - vue: 3.5.42(typescript@6.0.3) - abbrev@2.0.0: {} acorn-jsx@5.3.2(acorn@8.18.0): @@ -2810,12 +2779,10 @@ snapshots: argue-cli@3.1.0: {} - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - assertion-error@2.0.1: {} + async-validator@4.2.5: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -2894,18 +2861,29 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + cssesc@3.0.0: {} + csstype@3.0.11: {} + csstype@3.2.3: {} + date-fns-tz@3.2.0(date-fns@4.4.0): + dependencies: + date-fns: 4.4.0 + + date-fns@4.4.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 deep-is@0.1.4: {} - defu@6.1.7: {} - detect-libc@2.1.2: {} eastasianwidth@0.2.0: {} @@ -3029,6 +3007,8 @@ snapshots: esutils@2.0.3: {} + evtd@0.2.4: {} + expect-type@1.4.0: {} fast-deep-equal@3.1.3: {} @@ -3103,6 +3083,8 @@ snapshots: - bufferutil - utf-8-validate + highlight.js@11.12.0: {} + ignore@5.3.2: {} import-fresh@3.3.1: @@ -3322,6 +3304,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + + lodash@4.18.1: {} + lru-cache@10.4.3: {} magic-string@0.30.21: @@ -3342,6 +3328,28 @@ snapshots: muggle-string@0.4.1: {} + naive-ui@2.45.3(vue@3.5.42(typescript@6.0.3)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.42(typescript@6.0.3)) + '@types/lodash': 4.17.25 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.2.3 + date-fns: 4.4.0 + date-fns-tz: 3.2.0(date-fns@4.4.0) + evtd: 0.2.4 + highlight.js: 11.12.0 + lodash: 4.18.1 + lodash-es: 4.18.1 + seemly: 0.3.10 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.42(typescript@6.0.3)) + vooks: 0.2.12(vue@3.5.42(typescript@6.0.3)) + vue: 3.5.42(typescript@6.0.3) + vueuc: 0.4.66(vue@3.5.42(typescript@6.0.3)) + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -3356,8 +3364,6 @@ snapshots: obug@2.1.4: {} - ohash@2.0.12: {} - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3468,22 +3474,6 @@ snapshots: punycode@2.3.1: {} - reka-ui@2.10.4(vue@3.5.42(typescript@6.0.3)): - dependencies: - '@floating-ui/dom': 1.8.0 - '@floating-ui/vue': 1.1.11(vue@3.5.42(typescript@6.0.3)) - '@internationalized/date': 3.12.3 - '@internationalized/number': 3.6.7 - '@tanstack/vue-virtual': 3.13.36(vue@3.5.42(typescript@6.0.3)) - '@vueuse/core': 14.4.0(vue@3.5.42(typescript@6.0.3)) - '@vueuse/shared': 14.4.0(vue@3.5.42(typescript@6.0.3)) - aria-hidden: 1.2.6 - defu: 6.1.7 - ohash: 2.0.12 - vue: 3.5.42(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -3511,6 +3501,8 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.5 '@rolldown/binding-win32-x64-msvc': 1.2.5 + seemly@0.3.10: {} + semver@7.8.5: {} shebang-command@2.0.0: @@ -3577,12 +3569,12 @@ snapshots: tinyrainbow@3.1.1: {} + treemate@0.3.11: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 - tslib@2.8.1: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3597,6 +3589,11 @@ snapshots: util-deprecate@1.0.2: {} + vdirs@0.1.8(vue@3.5.42(typescript@6.0.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.42(typescript@6.0.3) + vite@8.2.2(@types/node@24.13.3)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 @@ -3637,14 +3634,15 @@ snapshots: transitivePeerDependencies: - msw + vooks@0.2.12(vue@3.5.42(typescript@6.0.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.42(typescript@6.0.3) + vscode-uri@3.2.0: {} vue-component-type-helpers@3.3.11: {} - vue-demi@0.14.10(vue@3.5.42(typescript@6.0.3)): - dependencies: - vue: 3.5.42(typescript@6.0.3) - vue-eslint-parser@10.4.1(eslint@10.9.1(jiti@2.7.0)): dependencies: debug: 4.4.3 @@ -3678,6 +3676,17 @@ snapshots: optionalDependencies: typescript: 6.0.3 + vueuc@0.4.66(vue@3.5.42(typescript@6.0.3)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.42(typescript@6.0.3)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.10 + vdirs: 0.1.8(vue@3.5.42(typescript@6.0.3)) + vooks: 0.2.12(vue@3.5.42(typescript@6.0.3)) + vue: 3.5.42(typescript@6.0.3) + whatwg-mimetype@3.0.0: {} which@2.0.2: diff --git a/src/App.vue b/src/App.vue index 475427c..229f3a5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,20 @@ diff --git a/src/admin.css b/src/admin.css new file mode 100644 index 0000000..9fda452 --- /dev/null +++ b/src/admin.css @@ -0,0 +1,574 @@ +/* 管理后台视口边界:document 不滚动,各个工作区和面板独立滚动。 */ +:root { + --app-ink: #202020; + --app-muted: #737373; + --app-surface: #ffffff; + --app-subtle: #f5f5f5; + --app-border: #e5e5e5; + --app-inverse: #ffffff; + --app-shadow: #00000018; + --app-danger: #b42318; + --app-success: #447454; +} +:root[data-theme='dark'] { + --app-ink: #ededed; + --app-muted: #a3a3a3; + --app-surface: #1c1c1c; + --app-subtle: #252525; + --app-border: #363636; + --app-inverse: #171717; + --app-shadow: #00000055; + --app-danger: #ee8b83; + --app-success: #91ba9b; +} +html, +body, +#app, +.app-provider { + height: 100%; + min-height: 0; + overflow: hidden; +} +html { + background: var(--app-surface); +} +body { + margin: 0; +} +.admin-layout { + height: 100dvh; + overflow: hidden; +} +.admin-main { + min-width: 0; + height: 100%; +} +.admin-main > .n-layout-scroll-container { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} +.admin-sider { + height: 100%; +} +.admin-brand { + display: flex; + align-items: center; + gap: 12px; + height: 74px; + padding: 0 20px; + color: var(--app-ink); + white-space: nowrap; +} +.admin-brand > svg { + flex-shrink: 0; +} +.admin-brand span { + font-size: 15px; + font-weight: 600; +} +.admin-brand small { + display: block; + font-size: 8px; + letter-spacing: 1px; + font-weight: 400; + color: var(--app-muted); + margin-top: 3px; +} +.admin-topbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + height: 56px; + padding: 0 20px; + gap: 12px; +} +.theme-select { + width: 120px; +} +.admin-content { + flex: 1; + min-height: 0; + min-width: 0; + overflow: hidden; + outline: none; +} +.project-frame { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; +} +.project-header { + flex-shrink: 0; + padding: 14px 24px 0; + background: var(--app-surface); + border-bottom: 1px solid var(--app-border); +} +.project-header .n-page-header__main, +.project-header .n-page-header__title { + min-width: 0; +} +.project-title { + max-width: min(58vw, 980px); + font-size: 16px; + font-weight: 600; +} +.project-tabs { + margin-top: 8px; +} +.project-tabs .n-tabs-nav { + border: 0; +} +.project-notices { + flex-shrink: 0; + max-height: 100px; + overflow: auto; + padding: 8px 24px; + display: grid; + gap: 5px; +} +.project-notices .n-alert { + padding: 7px 12px; + font-size: 12px; +} +.project-view { + flex: 1; + min-height: 0; + overflow: hidden; +} +.workspace-page { + height: 100%; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} +.workspace-heading { + flex-shrink: 0; + padding: 16px 24px 12px; + max-height: 35%; + overflow: auto; + overscroll-behavior: contain; +} +.workspace-heading h2 { + font-size: 16px; +} +.workspace-heading .text-sm { + font-size: 12px; +} +.workspace-heading .production-controls { + gap: 12px; +} +.workspace-heading .n-input-number { + width: 100%; +} +.workspace-scroll { + flex: 1; + min-height: 0; +} +.workspace-scroll-content { + padding: 4px 24px 24px; +} +.workspace-scroll > .n-scrollbar-container { + overscroll-behavior: contain; +} +.workspace-split { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; + padding: 4px 24px 20px; + gap: 12px; +} +.workspace-split > .my-6 { + margin: 0; + flex-shrink: 0; +} +.workspace-overview { + flex-shrink: 0; + max-height: 42%; + overflow: auto; + padding: 10px 14px; + background: var(--app-surface); + border: 1px solid var(--app-border); + border-radius: 6px; +} +.workspace-feedback { + flex-shrink: 0; + max-height: 24%; + overflow: auto; +} +.workspace-feedback .n-alert + .n-alert { + margin-top: 8px; +} +.workspace-split > .n-alert { + flex-shrink: 0; + max-height: 24%; + overflow: auto; +} +.script-section { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +.script-section > :first-child { + flex-shrink: 0; +} +.script-section > :first-child .n-tabs { + flex: 1; + min-width: 220px; + width: auto; +} +.script-section .content-with-history { + flex: 1; + height: auto; + min-height: 0; +} +.script-workspace-page .workspace-split > :not(.script-section) { + flex-shrink: 0; +} +.storyboard-workspace-page .storyboard-workspace { + flex: 1; + height: auto; + min-height: 0; + overflow: hidden; +} +.storyboard-coverage { + flex-shrink: 0; +} +.overview-content { + padding-bottom: 8px; +} +.workspace-loading { + display: grid; + place-content: center; + height: 100%; +} +.production-workspace, +.identity-workspace { + flex: 1; + min-height: 0; + align-items: stretch; + overflow: hidden; +} +.production-shot-list { + max-height: none; + overflow: auto; + min-height: 0; +} +.production-workspace > article { + overflow: auto; + min-height: 0; + overscroll-behavior: contain; +} +.identity-workspace > aside { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} +.identity-workspace { + grid-template-columns: 220px minmax(0, 1fr); +} +.identity-workspace > div { + min-height: 0; + overflow: auto; + overscroll-behavior: contain; +} +.identity-subject-list { + max-height: none; + min-height: 0; + flex: 1; +} +.storyboard-workspace { + height: clamp(360px, 65dvh, 850px); + min-height: 0; +} +.storyboard-workspace > article, +.storyboard-workspace .shot-list { + overflow: auto; + min-height: 0; + height: 100%; + max-height: none; +} +.content-with-history { + height: clamp(360px, 66dvh, 850px); + min-height: 0; + grid-template-columns: minmax(0, 1fr) 207px; +} +.content-with-history > main, +.content-with-history > aside, +.script-page { + min-height: 0; + overflow: auto; + overscroll-behavior: contain; +} +.script-workspace { + min-height: 0; +} +.episode-list { + max-height: none; + min-height: 0; +} +.project-table-area { + flex: 1; + min-height: 0; +} +.project-data-table { + height: 100%; +} +.project-data-table .n-data-table-base-table { + flex: 1; + min-height: 0; +} +.project-index-heading { + margin: 0; +} +.project-index-heading h1 { + font-size: 22px; + font-weight: 600; +} +.project-index-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 4px; +} +.project-search { + width: 240px; +} +.project-name-cell { + display: block; + min-width: 0; +} +.project-name-cell strong, +.project-name-cell small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.project-name-cell small { + color: var(--app-muted); + margin-top: 4px; + font-size: 11px; +} +.app-dialog { + max-width: calc(100vw - 32px); + max-height: calc(100dvh - 32px); +} +.app-dialog .n-card-header { + flex-shrink: 0; +} +.dialog-body-scroll { + max-height: calc(100dvh - 154px); +} +.app-dialog .n-card__content { + min-height: 0; +} +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; +} +.asset-image .n-image { + width: 100%; + height: 100%; + min-height: 0; + display: flex; + justify-content: center; +} +.asset-image .n-image img { + width: 100%; + height: 100%; + object-fit: contain; +} +.n-button__content { + gap: 6px; +} +.n-button:disabled { + opacity: 1; +} +.n-button.episode-link, +.n-button.shot-link, +.n-button.production-shot-link, +.n-button.identity-subject-item, +.n-button.casting-item, +.n-button.image-history-item { + height: auto; + white-space: normal; +} +.n-button.episode-link .n-button__content, +.n-button.shot-link .n-button__content { + width: 100%; + justify-content: flex-start; + text-align: left; +} +.n-button.production-shot-link .n-button__content, +.n-button.identity-subject-item .n-button__content, +.n-button.image-history-item .n-button__content { + display: block; + width: 100%; + text-align: left; + overflow: hidden; +} +.n-button.casting-item .n-button__content { + width: 100%; + justify-content: space-between; +} +.n-button.episode-link .n-button__border, +.n-button.shot-link .n-button__border, +.n-button.production-shot-link .n-button__border, +.n-button.identity-subject-item .n-button__border { + display: none; +} +.search-field { + padding: 0; + border: 0; + background: transparent; + min-width: 180px; +} +.search-field input { + width: 100%; + font-size: inherit; +} +.select-control { + min-width: 130px; +} +.n-checkbox { + align-items: flex-start; +} +.n-checkbox .n-checkbox__label { + white-space: normal; +} +.n-alert { + line-height: 1.8; +} +.n-collapse.panel { + padding: 16px; +} +.n-table.project-table th { + background: var(--app-subtle); +} +.empty-state { + min-height: 200px; + padding: 40px 20px; +} +.production-video { + background: #080808; +} +.button-primary { + color: var(--app-inverse); +} +.skip-link:focus { + z-index: 3000; +} +@media (max-width: 1000px) { + .project-title { + max-width: 44vw; + } + .production-controls { + grid-template-columns: minmax(130px, 1fr) 90px; + } + .content-with-history { + grid-template-columns: minmax(0, 1fr) 180px; + } +} +@media (max-width: 760px) { + .admin-topbar { + padding: 0 10px; + } + .admin-sider:not(.n-layout-sider--collapsed) { + position: absolute; + top: 56px; + bottom: 0; + z-index: 20; + box-shadow: 6px 0 24px var(--app-shadow); + } + .project-header { + padding: 10px 12px 0; + } + .project-title { + max-width: 37vw; + font-size: 14px; + } + .project-header .n-page-header__extra { + margin-left: 6px; + } + .project-header .n-tag { + display: none; + } + .workspace-heading { + padding: 12px; + } + .workspace-scroll-content, + .workspace-split { + padding: 0 12px 12px; + } + .workspace-heading .production-controls { + grid-template-columns: minmax(100px, 1fr) 80px; + } + .production-workspace { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 110px minmax(0, 1fr); + } + .production-shot-list { + display: flex; + overflow-x: auto; + height: 110px; + } + .production-shot-link { + flex-shrink: 0; + } + .identity-workspace { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 180px minmax(0, 1fr); + gap: 10px; + } + .storyboard-workspace { + grid-template-columns: 120px minmax(0, 1fr); + } + .storyboard-controls { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + .content-with-history { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) 130px; + } + .history-panel { + overflow: auto; + min-height: 0; + } + .script-workspace { + grid-template-columns: 105px minmax(0, 1fr); + } + .episode-list { + display: block; + padding: 0; + border-right: 1px solid var(--app-border); + } + .episode-link { + width: 100%; + } + .script-page { + padding: 18px; + } + .project-search { + width: 100%; + } + .project-index-toolbar .n-radio-group { + width: 100%; + } + .production-pipeline { + grid-template-columns: 1fr; + } +} diff --git a/src/components/ui/AppDialog.vue b/src/components/ui/AppDialog.vue index d62d0d2..ae7eee7 100644 --- a/src/components/ui/AppDialog.vue +++ b/src/components/ui/AppDialog.vue @@ -1,44 +1,26 @@ - diff --git a/src/components/ui/AssetImage.vue b/src/components/ui/AssetImage.vue index 2a95481..93e6206 100644 --- a/src/components/ui/AssetImage.vue +++ b/src/components/ui/AssetImage.vue @@ -1,4 +1,5 @@ - diff --git a/src/components/ui/StatusBadge.vue b/src/components/ui/StatusBadge.vue index 9fd0351..4024854 100644 --- a/src/components/ui/StatusBadge.vue +++ b/src/components/ui/StatusBadge.vue @@ -1,14 +1,22 @@ - diff --git a/src/components/ui/WorkspacePage.vue b/src/components/ui/WorkspacePage.vue new file mode 100644 index 0000000..b427875 --- /dev/null +++ b/src/components/ui/WorkspacePage.vue @@ -0,0 +1,12 @@ + + diff --git a/src/components/ui/layout.test.ts b/src/components/ui/layout.test.ts new file mode 100644 index 0000000..635e0eb --- /dev/null +++ b/src/components/ui/layout.test.ts @@ -0,0 +1,89 @@ +import { h } from 'vue' +import { createMemoryHistory, createRouter } from 'vue-router' +import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' +import { afterEach, describe, expect, it } from 'vitest' +import { NConfigProvider, NModal, NScrollbar } from 'naive-ui' +import App from '../../App.vue' +import WorkspacePage from './WorkspacePage.vue' +import AppDialog from './AppDialog.vue' +import { readFileSync } from 'node:fs' + +let wrapper: VueWrapper | undefined +afterEach(() => { + wrapper?.unmount() + wrapper = undefined + localStorage.clear() + document.body.innerHTML = '' +}) + +describe('管理后台组件边界', () => { + it('滚动边界样式包含 document 锁定和工作区收缩约束', () => { + // happy-dom 不计算视口几何;这里只保护 CSS 契约,真实滚动仍需浏览器验收。 + const adminCss = readFileSync('src/admin.css', 'utf8') + expect(adminCss).toMatch(/html,\s*body,\s*#app,\s*\.app-provider\s*\{[^}]*overflow:\s*hidden/) + expect(adminCss).toMatch(/\.workspace-page\s*\{[^}]*min-height:\s*0;[^}]*overflow:\s*hidden/) + }) + it('固定标题在滚动容器之外,普通工作区只有内容使用滚动组件', () => { + wrapper = mount(WorkspacePage, { + slots: { header: '

固定操作区

', default: '

正文

' } + }) + expect(wrapper.get('.workspace-heading').text()).toBe('固定操作区') + expect(wrapper.getComponent(NScrollbar).text()).toBe('正文') + expect(wrapper.getComponent(NScrollbar).find('h2').exists()).toBe(false) + }) + + it('分栏工作区不再包一层整页滚动', () => { + wrapper = mount(WorkspacePage, { + props: { split: true }, + slots: { default: '
正文
' } + }) + expect(wrapper.findComponent(NScrollbar).exists()).toBe(false) + expect(wrapper.get('.workspace-split').findAll(':scope > *')).toHaveLength(2) + }) + + it('真实主题选择器切换暗色,侧栏保持全部七个工作流入口', async () => { + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/projects', component: { render: () => h('p', '项目列表') } }, + { + path: '/projects/:projectId/:workspace', + component: { render: () => h(WorkspacePage, {}, () => '镜头详情') } + } + ] + }) + await router.push('/projects/test/production') + wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } }) + await flushPromises() + expect(wrapper.findAll('.n-menu a')).toHaveLength(8) + expect(wrapper.get('#main-content .workspace-page').text()).toBe('镜头详情') + await wrapper.get('.theme-select .n-base-selection').trigger('click') + await flushPromises() + const darkOption = [...document.querySelectorAll('.n-base-select-option')].find( + item => item.textContent?.trim() === '暗黑模式' + ) + expect(darkOption).toBeDefined() + darkOption!.click() + await flushPromises() + expect(document.documentElement.dataset.theme).toBe('dark') + expect(wrapper.getComponent(NConfigProvider).props('theme')?.name).toBe('dark') + expect(localStorage.getItem('drama-studio-theme')).toBe('dark') + await wrapper.get('[aria-label="折叠侧栏"]').trigger('click') + expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true) + }) + + it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => { + wrapper = mount(AppDialog, { + attachTo: document.body, + props: { open: true, title: '确认生图', description: '费用确认', busy: true }, + slots: { default: '

内容

' } + }) + await flushPromises() + expect(wrapper.getComponent(NModal).props()).toMatchObject({ + closable: false, + maskClosable: false, + closeOnEsc: false + }) + expect(document.querySelector('.dialog-body-scroll .n-scrollbar-container')).not.toBeNull() + }) +}) diff --git a/src/composables/useTheme.test.ts b/src/composables/useTheme.test.ts new file mode 100644 index 0000000..003aa61 --- /dev/null +++ b/src/composables/useTheme.test.ts @@ -0,0 +1,91 @@ +import { effectScope, nextTick, type EffectScope } from 'vue' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { useTheme } from './useTheme' + +let scope: EffectScope | undefined +afterEach(() => { + scope?.stop() + scope = undefined + localStorage.clear() + delete document.documentElement.dataset.theme + document.documentElement.style.colorScheme = '' + vi.unstubAllGlobals() + vi.restoreAllMocks() +}) + +/** 可控系统主题,验证跟随模式与事件清理,不依赖测试机器偏好。 */ +function setup(dark = false) { + const listeners = new Set<(event: MediaQueryListEvent) => void>() + vi.stubGlobal('matchMedia', () => ({ + matches: dark, + addEventListener: (_: string, listener: (event: MediaQueryListEvent) => void) => listeners.add(listener), + removeEventListener: (_: string, listener: (event: MediaQueryListEvent) => void) => listeners.delete(listener) + })) + scope = effectScope() + const state = scope.run(useTheme)! + return { + ...state, + listeners, + change: (matches: boolean) => listeners.forEach(listener => listener({ matches } as MediaQueryListEvent)) + } +} + +describe('黑白双主题', () => { + it('默认跟随系统并实时响应,销毁时移除监听', async () => { + const state = setup() + expect(state.preference.value).toBe('system') + expect(document.documentElement.dataset.theme).toBe('light') + state.change(true) + await nextTick() + expect(state.isDark.value).toBe(true) + expect(document.documentElement.style.colorScheme).toBe('dark') + expect(state.listeners.size).toBe(1) + scope!.stop() + expect(state.listeners.size).toBe(0) + }) + + it('手动选择优先于系统,并持久化到下一次挂载', async () => { + const state = setup(true) + state.preference.value = 'light' + await nextTick() + state.change(true) + await nextTick() + expect(state.isDark.value).toBe(false) + expect(localStorage.getItem('drama-studio-theme')).toBe('light') + scope!.stop() + const restored = setup(true) + expect(restored.preference.value).toBe('light') + expect(restored.theme.value).toBeNull() + }) + + it('深浅模式同时更新 Naive 主题和业务颜色标记', async () => { + const state = setup() + expect(state.overrides.value.common?.primaryColor).toBe('#202020') + state.preference.value = 'dark' + await nextTick() + expect(state.theme.value?.name).toBe('dark') + expect(state.overrides.value.common?.primaryColor).toBe('#f5f5f5') + expect(state.overrides.value.Button?.textColorPrimary).toBe('#171717') + expect(document.documentElement.dataset.theme).toBe('dark') + }) + + it('无效存储回退系统模式', () => { + localStorage.setItem('drama-studio-theme', 'unexpected') + const state = setup(true) + expect(state.preference.value).toBe('system') + expect(state.isDark.value).toBe(true) + }) + + it('禁用存储仍能切换当前会话的主题', async () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('storage blocked') + }) + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('storage blocked') + }) + const state = setup() + state.preference.value = 'dark' + await nextTick() + expect(document.documentElement.dataset.theme).toBe('dark') + }) +}) diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts new file mode 100644 index 0000000..b586cb4 --- /dev/null +++ b/src/composables/useTheme.ts @@ -0,0 +1,88 @@ +import { computed, onScopeDispose, ref, watchEffect } from 'vue' +import { darkTheme, type GlobalThemeOverrides } from 'naive-ui' + +/** 用户主题偏好;system 会跟随系统的实时深浅色变化。 */ +export type ThemePreference = 'light' | 'dark' | 'system' +const storageKey = 'drama-studio-theme' + +/** 读取主题时兼容隐私模式或禁用存储的浏览器。 */ +function readPreference(): ThemePreference { + try { + const value = localStorage.getItem(storageKey) + return value === 'light' || value === 'dark' ? value : 'system' + } catch { + return 'system' + } +} + +/** 主题只影响展示,不修改项目配置;卸载时移除系统主题监听。 */ +export function useTheme() { + const preference = ref(readPreference()) + const media = window.matchMedia('(prefers-color-scheme: dark)') + const systemDark = ref(media.matches) + const isDark = computed(() => preference.value === 'dark' || (preference.value === 'system' && systemDark.value)) + const theme = computed(() => (isDark.value ? darkTheme : null)) + const overrides = computed(() => { + const dark = isDark.value + const primary = dark ? '#f5f5f5' : '#202020' + const hover = dark ? '#d4d4d4' : '#444444' + return { + common: { + primaryColor: primary, + primaryColorHover: hover, + primaryColorPressed: hover, + primaryColorSuppl: primary, + infoColor: primary, + infoColorHover: hover, + infoColorPressed: hover, + infoColorSuppl: primary, + borderRadius: '6px', + bodyColor: dark ? '#141414' : '#f5f5f5', + cardColor: dark ? '#1c1c1c' : '#ffffff', + modalColor: dark ? '#202020' : '#ffffff', + popoverColor: dark ? '#262626' : '#ffffff', + tableColor: dark ? '#1c1c1c' : '#ffffff', + borderColor: dark ? '#363636' : '#e5e5e5', + textColorBase: dark ? '#ededed' : '#202020', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif" + }, + Button: { + textColorPrimary: dark ? '#171717' : '#ffffff', + textColorHoverPrimary: dark ? '#171717' : '#ffffff', + textColorPressedPrimary: dark ? '#171717' : '#ffffff' + }, + Layout: { + color: dark ? '#141414' : '#f5f5f5', + headerColor: dark ? '#1c1c1c' : '#ffffff', + siderColor: dark ? '#1c1c1c' : '#ffffff' + }, + Menu: { + itemColorActive: dark ? '#333333' : '#eeeeee', + itemColorActiveHover: dark ? '#383838' : '#e7e7e7', + itemTextColorActive: primary, + itemTextColorActiveHover: primary, + itemIconColorActive: primary, + itemIconColorActiveHover: primary + }, + Checkbox: { checkMarkColor: dark ? '#171717' : '#ffffff' }, + Progress: { fillColor: primary } + } + }) + /** 系统切换只更新 system 模式下的实际显示。 */ + function syncSystem(event: MediaQueryListEvent) { + systemDark.value = event.matches + } + media.addEventListener('change', syncSystem) + onScopeDispose(() => media.removeEventListener('change', syncSystem)) + watchEffect(() => { + document.documentElement.dataset.theme = isDark.value ? 'dark' : 'light' + document.documentElement.style.colorScheme = isDark.value ? 'dark' : 'light' + try { + localStorage.setItem(storageKey, preference.value) + } catch { + /* 存储不可用时保留当前会话偏好。 */ + } + }) + return { preference, isDark, theme, overrides } +} diff --git a/src/features/breakdown/BreakdownPage.vue b/src/features/breakdown/BreakdownPage.vue index c329830..7f8316d 100644 --- a/src/features/breakdown/BreakdownPage.vue +++ b/src/features/breakdown/BreakdownPage.vue @@ -1,16 +1,8 @@ - - + + /> + - - - + diff --git a/src/features/breakdown/components/StoryboardList.vue b/src/features/breakdown/components/StoryboardList.vue index 4d8975a..9401ab9 100644 --- a/src/features/breakdown/components/StoryboardList.vue +++ b/src/features/breakdown/components/StoryboardList.vue @@ -1,4 +1,5 @@ -
- 世界规则 -
{{ JSON.stringify(project.world.rules, null, 2) }}
-
+ +
{{ JSON.stringify(project.world.rules, null, 2) }}
+
- -
+ +
+
+
-
- 查看改写建议原文 -
{{ JSON.stringify(state.rewriteSuggestion, null, 2) }}
-
+ /> +
{{ JSON.stringify(state.rewriteSuggestion, null, 2) }}
+
+
-
- - + diff --git a/src/features/production/ProductionPage.vue b/src/features/production/ProductionPage.vue index a665d5b..5f05c4a 100644 --- a/src/features/production/ProductionPage.vue +++ b/src/features/production/ProductionPage.vue @@ -1,7 +1,8 @@ \ No newline at end of file +

+ diff --git a/src/features/production/components/KeyframeDialog.vue b/src/features/production/components/KeyframeDialog.vue index 61f98f0..e05deed 100644 --- a/src/features/production/components/KeyframeDialog.vue +++ b/src/features/production/components/KeyframeDialog.vue @@ -1,4 +1,5 @@ - diff --git a/src/features/projects/ProjectsPage.vue b/src/features/projects/ProjectsPage.vue index 0754225..a68e5a8 100644 --- a/src/features/projects/ProjectsPage.vue +++ b/src/features/projects/ProjectsPage.vue @@ -1,162 +1,134 @@ -
@@ -306,14 +348,15 @@ function exportEpisode() { 标题与剧情描述取自最近可用 Breakdown 快照;以下设计与状态取自数据库。

- 设计与状态参考图与生成规格 +
+ 设计与状态参考图与生成规格 +
+
+ +
+
前往拆解 - + > diff --git a/src/features/storyboard/components/GenerationReceipt.vue b/src/features/storyboard/components/GenerationReceipt.vue index 0ad2946..8ad59a6 100644 --- a/src/features/storyboard/components/GenerationReceipt.vue +++ b/src/features/storyboard/components/GenerationReceipt.vue @@ -1,4 +1,5 @@

- + - + {{ detail.error.value }} 查询失败时不允许生成或覆盖。 +

正在读取主体身份…

@@ -375,21 +425,18 @@ function chooseAnchor(imageId: string) { acknowledgement description="调用文本模型生成稳定身份描述和提示词。旧图片不会自动重生成;已锁定身份需先解锁并保存。" @confirm="generate" - />

请先保存身份修改,再使用 AI 或生成图片。

此主体的形态

- {{ form.name }}{{ form.isDefault ? ' · 默认' : '' }}{{ form.name }}{{ form.isDefault ? ' · 默认' : '' }}

@@ -467,8 +514,7 @@ function chooseAnchor(imageId: string) { :module="subject.module" :images="images" :disabled="!canGenerateImage" - @generate="generateImage" - /> + @generate="generateImage" /> - + /> diff --git a/src/features/subject-identity/components/CastingCandidateDialog.vue b/src/features/subject-identity/components/CastingCandidateDialog.vue index 3051420..6b5826e 100644 --- a/src/features/subject-identity/components/CastingCandidateDialog.vue +++ b/src/features/subject-identity/components/CastingCandidateDialog.vue @@ -1,4 +1,5 @@ ') expect(wrapper.find('script').exists()).toBe(false) expect(wrapper.emitted('anchor')).toBeUndefined() diff --git a/src/features/subject-images/SubjectImagesPage.vue b/src/features/subject-images/SubjectImagesPage.vue index 718f329..311671c 100644 --- a/src/features/subject-images/SubjectImagesPage.vue +++ b/src/features/subject-images/SubjectImagesPage.vue @@ -1,4 +1,6 @@ \ No newline at end of file + /> + diff --git a/src/features/subject-images/components/GenerateImageDialog.vue b/src/features/subject-images/components/GenerateImageDialog.vue index 504ef97..44c3848 100644 --- a/src/features/subject-images/components/GenerateImageDialog.vue +++ b/src/features/subject-images/components/GenerateImageDialog.vue @@ -1,4 +1,5 @@ ') expect(document.querySelector('[role="dialog"] script')).toBeNull() button('设为主参考图').click() diff --git a/src/features/visual-style/VisualStylePage.vue b/src/features/visual-style/VisualStylePage.vue index cabfa1b..afb9af8 100644 --- a/src/features/visual-style/VisualStylePage.vue +++ b/src/features/visual-style/VisualStylePage.vue @@ -1,4 +1,6 @@ diff --git a/src/features/visual-style/components/StyleEditor.vue b/src/features/visual-style/components/StyleEditor.vue index afe5c98..44c3c7e 100644 --- a/src/features/visual-style/components/StyleEditor.vue +++ b/src/features/visual-style/components/StyleEditor.vue @@ -1,9 +1,10 @@