feat: 收口组件样式并调整移动端侧栏
This commit is contained in:
@@ -6,3 +6,4 @@ coverage
|
|||||||
.eslintcache
|
.eslintcache
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.worktree/
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ API_PROXY_TARGET=http://localhost:3412
|
|||||||
## 技术与规范
|
## 技术与规范
|
||||||
|
|
||||||
- Vite 8、Vue 3、TypeScript、Vue Router
|
- Vite 8、Vue 3、TypeScript、Vue Router
|
||||||
- Tailwind CSS 4,通过 `@tailwindcss/vite` 集成
|
- Tailwind CSS 4,通过 `@tailwindcss/vite` 集成;组件专属样式写在 SFC `<style>`,主题变量、base、Naive 覆盖和跨页原子 class 留在 `src/styles/styles.css` / `src/styles/admin.css`
|
||||||
- Naive UI:布局、菜单、表格、选择器、输入框、按钮、弹窗、标签、折叠面板、进度和时间线
|
- Naive UI:布局、菜单、表格、选择器、输入框、按钮、弹窗、标签、折叠面板、进度和时间线
|
||||||
- Oxlint:脚本质量检查;ESLint:补充 Vue 模板语义
|
- Oxlint:脚本质量检查;ESLint:补充 Vue 模板语义
|
||||||
- Oxfmt(不是 `oxformat`):沿用后端四空格、单引号、无分号、无尾逗号的风格
|
- Oxfmt(不是 `oxformat`):沿用后端四空格、单引号、无分号、无尾逗号的风格
|
||||||
- Vitest + Vue Test Utils + happy-dom:API 契约、组件交互和异步生命周期测试
|
- Vitest + Vue Test Utils + happy-dom:API 契约、组件交互和异步生命周期测试;`*.test.ts` 放在 `tests/`,按 `src` 镜像,不和组件放在一起
|
||||||
- Lefthook + Commitlint:提交前执行检查,提交信息使用 `feat:`、`fix:`、`refactor:` 等
|
- Lefthook + Commitlint:提交前执行检查,提交信息使用 `feat:`、`fix:`、`refactor:` 等
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -89,6 +89,8 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
|
|||||||
| `src/composables` | 具备取消和竞态保护的轮询、系统主题与偏好持久化 |
|
| `src/composables` | 具备取消和竞态保护的轮询、系统主题与偏好持久化 |
|
||||||
| `src/lib` | HTTP、错误格式、日期和导出工具 |
|
| `src/lib` | HTTP、错误格式、日期和导出工具 |
|
||||||
| `src/router` | 按 graph 拆分的懒加载路由 |
|
| `src/router` | 按 graph 拆分的懒加载路由 |
|
||||||
|
| `src/styles` | 全局主题、base、Naive 覆盖和跨页原子 class |
|
||||||
|
| `tests` | 与 `src` 镜像的 `*.test.ts`,不和组件源码放在一起 |
|
||||||
|
|
||||||
每个功能模块通过 `index.ts` 暴露公共入口。业务类型和行为函数均添加中文职责注释。不要把下一条 graph 的服务继续堆进 `App.vue`。
|
每个功能模块通过 `index.ts` 暴露公共入口。业务类型和行为函数均添加中文职责注释。不要把下一条 graph 的服务继续堆进 `App.vue`。
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
|
|||||||
- 拆解设置中“每组集数”和“抽取模块”使用相同标题间距;输入框、复选框与预览按钮对齐到 34px 控件行,模块说明独立放在下方。“集 / 组”保持单行,窄抽屉按实际内容宽度将操作和完整模块分组换行,校验与预览逻辑不变。
|
- 拆解设置中“每组集数”和“抽取模块”使用相同标题间距;输入框、复选框与预览按钮对齐到 34px 控件行,模块说明独立放在下方。“集 / 组”保持单行,窄抽屉按实际内容宽度将操作和完整模块分组换行,校验与预览逻辑不变。
|
||||||
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
||||||
|
|
||||||
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
布局样式集中在 `src/styles/admin.css`,业务排版保留在 `src/styles/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
||||||
|
|
||||||
本次 UI 重构保留前端 `dev@5551653` 的业务更新,包括过期形态图、主首帧和主视频的提示与更新入口。首帧默认只补当前剧集;存在过期主首帧时优先更新全项目过期项。非覆盖模式下的过期主视频重建完成后,会接替旧主视频;普通新增候选不自动替换主资产。
|
本次 UI 重构保留前端 `dev@5551653` 的业务更新,包括过期形态图、主首帧和主视频的提示与更新入口。首帧默认只补当前剧集;存在过期主首帧时优先更新全项目过期项。非覆盖模式下的过期主视频重建完成后,会接替旧主视频;普通新增候选不自动替换主资产。
|
||||||
|
|
||||||
|
|||||||
+184
-23
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, provide, ref, shallowRef } from 'vue'
|
import { computed, h, onScopeDispose, provide, ref, shallowRef, watch } from 'vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
NButton,
|
NButton,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
NGlobalStyle,
|
NGlobalStyle,
|
||||||
NLayout,
|
NLayout,
|
||||||
NLayoutHeader,
|
NLayoutHeader,
|
||||||
NLayoutSider,
|
|
||||||
NMenu,
|
NMenu,
|
||||||
NScrollbar,
|
NScrollbar,
|
||||||
NTooltip,
|
NTooltip,
|
||||||
@@ -33,10 +32,61 @@ import { useTheme } from './composables/useTheme'
|
|||||||
import ThemeToggle from './components/ui/ThemeToggle.vue'
|
import ThemeToggle from './components/ui/ThemeToggle.vue'
|
||||||
import { projectAccessKey, type ProjectAccess } from './features/projects/access'
|
import { projectAccessKey, type ProjectAccess } from './features/projects/access'
|
||||||
|
|
||||||
|
/** 窄屏与桌面共用同一侧栏;窄屏展开时主栏右移,宽度保持折叠时的尺寸。 */
|
||||||
|
const NARROW_NAV = '(max-width: 800px)'
|
||||||
|
/** 折叠侧栏宽度,与菜单 collapsed-width 对齐。 */
|
||||||
|
const SIDER_COLLAPSED_WIDTH = 64
|
||||||
|
/** 展开侧栏宽度,窄屏裁切动画以内层保持该宽度。 */
|
||||||
|
const SIDER_EXPANDED_WIDTH = 208
|
||||||
|
/** 菜单图标尺寸,折叠/展开共用,避免切换时缩放。 */
|
||||||
|
const NAV_ICON_SIZE = 20
|
||||||
|
/** 折叠与展开共用的图标左偏移,避免切换时图标跳动。 */
|
||||||
|
const NAV_ICON_INDENT = SIDER_COLLAPSED_WIDTH / 2 - NAV_ICON_SIZE / 2
|
||||||
|
|
||||||
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
|
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const collapsed = ref(window.matchMedia('(max-width: 800px)').matches)
|
const narrowMedia = window.matchMedia(NARROW_NAV)
|
||||||
|
const narrow = ref(narrowMedia.matches)
|
||||||
|
const collapsed = ref(narrow.value)
|
||||||
const settingsOpen = ref(false)
|
const settingsOpen = ref(false)
|
||||||
|
/** 窄屏折叠只裁切文字,不把菜单收成图标,避免推挤动画时错位。 */
|
||||||
|
const menuCollapsed = computed(() => !narrow.value && collapsed.value)
|
||||||
|
|
||||||
|
/** 视口跨过断点时:进入窄屏收回侧栏。 */
|
||||||
|
function syncNarrow(event: MediaQueryListEvent) {
|
||||||
|
narrow.value = event.matches
|
||||||
|
if (event.matches) collapsed.value = true
|
||||||
|
}
|
||||||
|
narrowMedia.addEventListener('change', syncNarrow)
|
||||||
|
onScopeDispose(() => narrowMedia.removeEventListener('change', syncNarrow))
|
||||||
|
|
||||||
|
/** 宽屏与窄屏都只切换这一份侧栏的展开态。 */
|
||||||
|
function toggleNav() {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开后端连接说明;窄屏先收回侧栏给弹窗腾出宽度。 */
|
||||||
|
function openSettings() {
|
||||||
|
if (narrow.value) collapsed.value = true
|
||||||
|
settingsOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
if (narrow.value) collapsed.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Esc 收回窄屏展开的侧栏。 */
|
||||||
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape' && narrow.value && !collapsed.value) collapsed.value = true
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
onScopeDispose(() => window.removeEventListener('keydown', onKeydown))
|
||||||
|
|
||||||
|
/** 顶栏切换按钮的可访问名称。 */
|
||||||
|
const navToggleLabel = computed(() => (collapsed.value ? '展开侧栏' : '折叠侧栏'))
|
||||||
const { preference, theme, overrides } = useTheme()
|
const { preference, theme, overrides } = useTheme()
|
||||||
const projectId = computed(() => String(route.params.projectId || ''))
|
const projectId = computed(() => String(route.params.projectId || ''))
|
||||||
const projectAccess = shallowRef<ProjectAccess | null>(null)
|
const projectAccess = shallowRef<ProjectAccess | null>(null)
|
||||||
@@ -91,18 +141,22 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
>
|
>
|
||||||
<NGlobalStyle />
|
<NGlobalStyle />
|
||||||
<a href="#main-content" class="skip-link">跳到主要内容</a>
|
<a href="#main-content" class="skip-link">跳到主要内容</a>
|
||||||
<NLayout has-sider class="admin-layout">
|
<div
|
||||||
<NLayoutSider
|
class="admin-layout"
|
||||||
collapse-mode="width"
|
:style="{
|
||||||
:collapsed="collapsed"
|
'--admin-sider-collapsed': SIDER_COLLAPSED_WIDTH + 'px',
|
||||||
:width="208"
|
'--admin-sider-expanded': SIDER_EXPANDED_WIDTH + 'px'
|
||||||
:collapsed-width="64"
|
}"
|
||||||
:native-scrollbar="false"
|
>
|
||||||
|
<!-- 以下是主导航侧栏:宽屏与窄屏共用同一份 -->
|
||||||
|
<aside
|
||||||
class="admin-sider"
|
class="admin-sider"
|
||||||
content-class="admin-sider-content"
|
:class="{ 'is-collapsed': collapsed }"
|
||||||
|
:style="{ width: (collapsed ? SIDER_COLLAPSED_WIDTH : SIDER_EXPANDED_WIDTH) + 'px' }"
|
||||||
|
aria-label="主导航"
|
||||||
>
|
>
|
||||||
<RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页">
|
<RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页">
|
||||||
<Clapperboard :size="24" /><span v-if="!collapsed"
|
<Clapperboard :size="24" /><span v-if="!menuCollapsed"
|
||||||
>短剧工作台<small>SHORT DRAMA STUDIO</small></span
|
>短剧工作台<small>SHORT DRAMA STUDIO</small></span
|
||||||
>
|
>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -110,9 +164,12 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
<NMenu
|
<NMenu
|
||||||
:value="route.path"
|
:value="route.path"
|
||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
:collapsed="collapsed"
|
:collapsed="menuCollapsed"
|
||||||
:collapsed-width="64"
|
:collapsed-width="SIDER_COLLAPSED_WIDTH"
|
||||||
:collapsed-icon-size="19"
|
:icon-size="NAV_ICON_SIZE"
|
||||||
|
:collapsed-icon-size="NAV_ICON_SIZE"
|
||||||
|
:indent="NAV_ICON_INDENT"
|
||||||
|
:root-indent="NAV_ICON_INDENT"
|
||||||
/>
|
/>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
<footer class="admin-sider-footer">
|
<footer class="admin-sider-footer">
|
||||||
@@ -122,27 +179,29 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
quaternary
|
quaternary
|
||||||
block
|
block
|
||||||
class="admin-settings-button"
|
class="admin-settings-button"
|
||||||
:class="{ 'is-collapsed': collapsed, 'icon-button': collapsed }"
|
:class="{ 'is-collapsed': menuCollapsed }"
|
||||||
aria-label="后端连接"
|
aria-label="后端连接"
|
||||||
@click="settingsOpen = true"
|
@click="openSettings"
|
||||||
>
|
>
|
||||||
<template #icon><Settings2 :size="18" /></template>
|
<template #icon><Settings2 :size="18" /></template>
|
||||||
<span v-if="!collapsed">后端连接</span>
|
<span v-if="!menuCollapsed">后端连接</span>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
后端连接
|
后端连接
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
</footer>
|
</footer>
|
||||||
</NLayoutSider>
|
</aside>
|
||||||
|
<!-- 以下是顶栏与主内容 -->
|
||||||
<NLayout class="admin-main">
|
<NLayout class="admin-main">
|
||||||
<NLayoutHeader class="admin-topbar">
|
<NLayoutHeader class="admin-topbar">
|
||||||
<div class="flex min-w-0 items-center gap-3">
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
<NButton
|
<NButton
|
||||||
quaternary
|
quaternary
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
:aria-label="collapsed ? '展开侧栏' : '折叠侧栏'"
|
:aria-label="navToggleLabel"
|
||||||
:title="collapsed ? '展开侧栏' : '折叠侧栏'"
|
:title="navToggleLabel"
|
||||||
@click="collapsed = !collapsed"
|
:aria-expanded="!collapsed"
|
||||||
|
@click="toggleNav"
|
||||||
>
|
>
|
||||||
<template #icon
|
<template #icon
|
||||||
><PanelLeftOpen v-if="collapsed" :size="18" /><PanelLeftClose v-else :size="18"
|
><PanelLeftOpen v-if="collapsed" :size="18" /><PanelLeftClose v-else :size="18"
|
||||||
@@ -154,7 +213,7 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
</NLayoutHeader>
|
</NLayoutHeader>
|
||||||
<main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main>
|
<main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main>
|
||||||
</NLayout>
|
</NLayout>
|
||||||
</NLayout>
|
</div>
|
||||||
<AppDialog
|
<AppDialog
|
||||||
v-model:open="settingsOpen"
|
v-model:open="settingsOpen"
|
||||||
title="后端连接"
|
title="后端连接"
|
||||||
@@ -175,3 +234,105 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
</AppDialog>
|
</AppDialog>
|
||||||
</NConfigProvider>
|
</NConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "styles/styles.css";
|
||||||
|
.skip-link {
|
||||||
|
@apply fixed z-[100] -top-[80px] left-[15px] py-2.5 px-[15px] bg-(--app-surface) border-[1px_solid_var(--color-accent)];
|
||||||
|
}
|
||||||
|
.skip-link:focus {
|
||||||
|
@apply top-2.5 z-[3000];
|
||||||
|
}
|
||||||
|
.admin-layout {
|
||||||
|
--admin-sider-collapsed: 64px;
|
||||||
|
--admin-sider-expanded: 208px;
|
||||||
|
@apply flex h-dvh overflow-hidden;
|
||||||
|
}
|
||||||
|
.admin-main {
|
||||||
|
@apply min-w-0 h-full flex-1;
|
||||||
|
}
|
||||||
|
.admin-main > .n-layout-scroll-container {
|
||||||
|
@apply h-full flex flex-col overflow-hidden;
|
||||||
|
}
|
||||||
|
.admin-sider {
|
||||||
|
@apply flex flex-col h-full w-[208px] shrink-0 overflow-hidden bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed {
|
||||||
|
@apply w-16;
|
||||||
|
}
|
||||||
|
.admin-nav-scroll.n-scrollbar {
|
||||||
|
@apply flex-1 min-h-0;
|
||||||
|
}
|
||||||
|
.admin-sider-footer {
|
||||||
|
@apply shrink-0;
|
||||||
|
padding-block: 8px max(8px, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.n-button.admin-settings-button {
|
||||||
|
@apply justify-start h-11 px-5;
|
||||||
|
}
|
||||||
|
.admin-sider .n-menu .n-menu-item-content::before {
|
||||||
|
@apply left-0 right-0 rounded-none;
|
||||||
|
}
|
||||||
|
.admin-brand {
|
||||||
|
@apply flex shrink-0 items-center gap-3 h-[74px] py-0 px-5 text-ink whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.admin-brand > svg {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.admin-brand span {
|
||||||
|
@apply text-[15px] font-semibold;
|
||||||
|
}
|
||||||
|
.admin-brand small {
|
||||||
|
@apply block text-[8px] tracking-[1px] font-normal text-muted mt-[3px];
|
||||||
|
}
|
||||||
|
.admin-topbar {
|
||||||
|
@apply relative z-10 flex items-center justify-between shrink-0 h-11 py-0 px-5 gap-3 bg-(--app-surface);
|
||||||
|
}
|
||||||
|
.admin-sider .n-menu-divider {
|
||||||
|
@apply h-3 bg-transparent;
|
||||||
|
}
|
||||||
|
.admin-content {
|
||||||
|
@apply flex-1 min-h-0 min-w-0 overflow-hidden outline-none;
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.admin-sider {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
/* 主栏宽度锁在折叠态剩余空间,侧栏变宽时把主栏推到右侧而不是压窄。 */
|
||||||
|
.admin-layout > .admin-main {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: calc(100% - var(--admin-sider-collapsed));
|
||||||
|
min-width: calc(100% - var(--admin-sider-collapsed));
|
||||||
|
max-width: calc(100% - var(--admin-sider-collapsed));
|
||||||
|
}
|
||||||
|
.admin-brand,
|
||||||
|
.admin-nav-scroll.n-scrollbar,
|
||||||
|
.admin-sider-footer {
|
||||||
|
min-width: var(--admin-sider-expanded);
|
||||||
|
}
|
||||||
|
.admin-brand span,
|
||||||
|
.admin-sider .n-menu-item-content-header,
|
||||||
|
.admin-settings-button span {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed .admin-brand span,
|
||||||
|
.admin-sider.is-collapsed .n-menu-item-content-header,
|
||||||
|
.admin-sider.is-collapsed .admin-settings-button span {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.admin-topbar {
|
||||||
|
@apply py-0 px-2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.admin-sider,
|
||||||
|
.admin-brand span,
|
||||||
|
.admin-sider .n-menu-item-content-header,
|
||||||
|
.admin-settings-button span {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
-1763
File diff suppressed because it is too large
Load Diff
@@ -9,3 +9,21 @@ import { EmptyState } from './ui'
|
|||||||
>
|
>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../styles/styles.css";
|
||||||
|
.page-container {
|
||||||
|
@apply max-w-[1540px] mx-auto pt-[37px] px-10 pb-14;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.page-container {
|
||||||
|
@apply px-[26px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.page-container {
|
||||||
|
@apply pt-[25px] px-[18px] pb-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -24,3 +24,24 @@ const open = defineModel<boolean>('open', { default: false })
|
|||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.app-dialog {
|
||||||
|
--app-field: var(--app-control);
|
||||||
|
--app-field-hover: var(--app-control-hover);
|
||||||
|
}
|
||||||
|
.app-dialog {
|
||||||
|
@apply max-w-[calc(100vw-32px)] max-h-[calc(100dvh-32px)];
|
||||||
|
}
|
||||||
|
.app-dialog .n-card-header {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.dialog-body-scroll {
|
||||||
|
@apply max-h-[calc(100dvh-154px)];
|
||||||
|
}
|
||||||
|
.app-dialog .n-card__content {
|
||||||
|
@apply min-h-0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -50,3 +50,26 @@ watch(
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.asset-image {
|
||||||
|
@apply grid place-items-center w-full aspect-[4/3] overflow-hidden bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.asset-image img {
|
||||||
|
@apply w-full h-full min-h-0;
|
||||||
|
}
|
||||||
|
.asset-image-empty {
|
||||||
|
@apply flex items-center justify-center flex-col gap-2.5 p-5 text-muted text-[11px] text-center;
|
||||||
|
}
|
||||||
|
.asset-image .n-image {
|
||||||
|
@apply w-full h-full min-h-0 min-w-0 flex justify-center;
|
||||||
|
}
|
||||||
|
.asset-image .n-image img {
|
||||||
|
@apply w-full h-full;
|
||||||
|
}
|
||||||
|
.asset-image.asset-image-preview {
|
||||||
|
@apply h-[min(42dvh,420px)] min-h-0 aspect-auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -21,3 +21,14 @@ function toggle() {
|
|||||||
<div class="detail-disclosure-content"><slot /></div></NCollapseItem
|
<div class="detail-disclosure-content"><slot /></div></NCollapseItem
|
||||||
></NCollapse>
|
></NCollapse>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.detail-disclosure.n-collapse {
|
||||||
|
@apply text-xs text-muted;
|
||||||
|
}
|
||||||
|
.detail-disclosure-content {
|
||||||
|
@apply py-3 px-3.5 bg-(--app-subtle) text-ink leading-[1.8];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -21,3 +21,46 @@ defineProps<{ active: boolean }>()
|
|||||||
</span>
|
</span>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.n-button.directory-item {
|
||||||
|
@apply flex shrink-0 w-full h-auto min-h-[88px] py-3 px-3.5 rounded-none whitespace-normal text-left;
|
||||||
|
}
|
||||||
|
.n-button.directory-item .n-button__content {
|
||||||
|
@apply block w-full min-w-0;
|
||||||
|
}
|
||||||
|
.directory-item-body {
|
||||||
|
@apply flex flex-col items-start gap-[7px] min-w-0;
|
||||||
|
}
|
||||||
|
.n-button.directory-item.has-leading .n-button__content {
|
||||||
|
@apply flex items-center gap-3;
|
||||||
|
}
|
||||||
|
.directory-item-leading {
|
||||||
|
@apply flex shrink-0;
|
||||||
|
}
|
||||||
|
.directory-item.has-leading .directory-item-body {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
.directory-item-eyebrow {
|
||||||
|
@apply font-mono text-[11px] leading-normal text-muted wrap-anywhere;
|
||||||
|
}
|
||||||
|
.directory-item-title {
|
||||||
|
@apply text-[13px] leading-[1.7] font-medium text-ink wrap-anywhere;
|
||||||
|
}
|
||||||
|
.directory-item-meta {
|
||||||
|
@apply flex items-center flex-wrap gap-y-1.5 gap-x-2 text-muted text-[11px] leading-[1.7] wrap-anywhere;
|
||||||
|
}
|
||||||
|
.n-button.directory-item.selected {
|
||||||
|
@apply bg-(--app-selected) shadow-[inset_3px_0_var(--app-accent)];
|
||||||
|
}
|
||||||
|
.n-button.directory-item:focus-visible {
|
||||||
|
@apply outline-[2px_solid_var(--app-accent)] outline-offset-[-2px];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.n-button.directory-item {
|
||||||
|
@apply w-[228px] min-h-[104px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,3 +13,14 @@ defineProps<{ title: string; description: string }>()
|
|||||||
></template>
|
></template>
|
||||||
</NEmpty>
|
</NEmpty>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.empty-state {
|
||||||
|
@apply flex flex-col items-center justify-center min-h-[300px] py-[52px] px-6 text-center;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
@apply min-h-[200px] py-10 px-5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -34,3 +34,11 @@ function selectTheme(key: string | number) {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</NDropdown>
|
</NDropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.n-button.theme-toggle {
|
||||||
|
@apply shrink-0 w-[34px] h-[34px];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -12,3 +12,72 @@ defineProps<{ split?: boolean; compact?: boolean }>()
|
|||||||
<NScrollbar v-else class="workspace-scroll" content-class="workspace-scroll-content"><slot /></NScrollbar>
|
<NScrollbar v-else class="workspace-scroll" content-class="workspace-scroll-content"><slot /></NScrollbar>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.workspace-page {
|
||||||
|
@apply relative h-full flex flex-col min-h-0 overflow-hidden;
|
||||||
|
}
|
||||||
|
.workspace-heading-scroll.n-scrollbar {
|
||||||
|
@apply shrink-0 h-auto max-h-[min(35dvh,260px)] overflow-hidden;
|
||||||
|
}
|
||||||
|
.workspace-heading {
|
||||||
|
@apply pt-4 px-6 pb-3;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-heading {
|
||||||
|
@apply py-2 px-5;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-split {
|
||||||
|
@apply pt-0 px-5 pb-3 gap-2;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-scroll-content {
|
||||||
|
@apply pt-0 px-5 pb-4;
|
||||||
|
}
|
||||||
|
.workspace-heading h2 {
|
||||||
|
@apply text-base;
|
||||||
|
}
|
||||||
|
.workspace-heading .text-sm {
|
||||||
|
@apply text-xs;
|
||||||
|
}
|
||||||
|
.workspace-heading .production-controls {
|
||||||
|
@apply gap-3;
|
||||||
|
}
|
||||||
|
.workspace-heading .n-input-number {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
.workspace-scroll {
|
||||||
|
@apply flex-1 min-h-0;
|
||||||
|
}
|
||||||
|
.workspace-scroll-content {
|
||||||
|
@apply pt-1 px-6 pb-6;
|
||||||
|
}
|
||||||
|
.workspace-scroll > .n-scrollbar-container {
|
||||||
|
@apply overscroll-contain;
|
||||||
|
}
|
||||||
|
.workspace-split {
|
||||||
|
@apply flex flex-col flex-1 min-h-0 overflow-hidden pt-1 px-6 pb-5 gap-3;
|
||||||
|
}
|
||||||
|
.workspace-split > .my-6 {
|
||||||
|
@apply m-0 shrink-0;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.workspace-heading {
|
||||||
|
@apply p-3;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-heading {
|
||||||
|
@apply py-2 px-3;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-split,
|
||||||
|
.content-first-workspace .workspace-scroll-content {
|
||||||
|
@apply pt-0 px-3 pb-3;
|
||||||
|
}
|
||||||
|
.workspace-scroll-content,
|
||||||
|
.workspace-split {
|
||||||
|
@apply pt-0 px-3 pb-3;
|
||||||
|
}
|
||||||
|
.workspace-heading .production-controls {
|
||||||
|
@apply grid-cols-[minmax(100px,_1fr)_80px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -46,3 +46,45 @@ const open = defineModel<boolean>('open', { default: false })
|
|||||||
</NDrawerContent>
|
</NDrawerContent>
|
||||||
</NDrawer>
|
</NDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.workspace-tools-drawer .n-drawer-header,
|
||||||
|
.workspace-tools-drawer .n-drawer-footer {
|
||||||
|
@apply bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.workspace-tools-drawer .n-drawer-body {
|
||||||
|
@apply bg-(--app-body);
|
||||||
|
}
|
||||||
|
.workspace-tools-drawer {
|
||||||
|
@apply max-w-full;
|
||||||
|
}
|
||||||
|
.workspace-tools-heading {
|
||||||
|
@apply flex items-center justify-between gap-4 w-full text-[15px];
|
||||||
|
}
|
||||||
|
.workspace-tools-body {
|
||||||
|
@apply p-5;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .storyboard-controls,
|
||||||
|
.workspace-tools-body .production-controls {
|
||||||
|
@apply grid grid-cols-[repeat(auto-fit,_minmax(160px,_1fr))] gap-4 my-5;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .storyboard-coverage {
|
||||||
|
@apply my-5;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .generation-row {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .generation-row > div + div {
|
||||||
|
@apply mt-3.5;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .production-pipeline {
|
||||||
|
@apply grid-cols-[repeat(auto-fit,_minmax(min(100%,_230px),_1fr))];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.workspace-tools-body {
|
||||||
|
@apply p-3.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -497,3 +497,95 @@ function exportResult() {
|
|||||||
</template>
|
</template>
|
||||||
</WorkspacePage>
|
</WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.group-preview {
|
||||||
|
@apply mt-6 pt-5;
|
||||||
|
}
|
||||||
|
.group-chip {
|
||||||
|
@apply flex items-center gap-2.5 bg-(--app-subtle) rounded-none py-2 px-[11px] text-[11px];
|
||||||
|
}
|
||||||
|
.recovery-strip {
|
||||||
|
@apply bg-(--app-subtle) flex justify-between flex-wrap items-center gap-[15px] p-[17px] rounded-none;
|
||||||
|
}
|
||||||
|
.breakdown-settings-panel {
|
||||||
|
container: breakdown-settings / inline-size;
|
||||||
|
}
|
||||||
|
.breakdown-config {
|
||||||
|
@apply grid grid-cols-[180px_minmax(0,_1fr)_auto] items-start gap-y-4 gap-x-6;
|
||||||
|
--breakdown-label-offset: 28px;
|
||||||
|
}
|
||||||
|
.breakdown-config .field-label {
|
||||||
|
@apply mt-0 mx-0 mb-2 p-0 leading-[20px];
|
||||||
|
}
|
||||||
|
.breakdown-group-field,
|
||||||
|
.breakdown-modules-field,
|
||||||
|
.breakdown-module-option {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
.breakdown-group-input {
|
||||||
|
@apply flex items-center gap-3;
|
||||||
|
}
|
||||||
|
.breakdown-group-number.n-input-number {
|
||||||
|
@apply flex-[0_0_120px] w-[120px];
|
||||||
|
}
|
||||||
|
.breakdown-group-unit {
|
||||||
|
@apply shrink-0 whitespace-nowrap text-muted text-sm;
|
||||||
|
}
|
||||||
|
.breakdown-modules-field {
|
||||||
|
@apply m-0 p-0 border-0;
|
||||||
|
}
|
||||||
|
.breakdown-module-options {
|
||||||
|
@apply grid grid-cols-[repeat(3,_minmax(0,_1fr))] gap-y-3 gap-x-4;
|
||||||
|
}
|
||||||
|
.breakdown-module-description {
|
||||||
|
@apply mt-1 mx-0 mb-0 pl-6 text-muted text-[11px] leading-[18px] wrap-anywhere;
|
||||||
|
}
|
||||||
|
.breakdown-preview-button.n-button {
|
||||||
|
@apply self-start mt-[var(--breakdown-label-offset)] whitespace-nowrap;
|
||||||
|
}
|
||||||
|
@container breakdown-settings (max-width: 780px) {
|
||||||
|
.breakdown-config {
|
||||||
|
@apply grid-cols-[180px_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.breakdown-preview-button.n-button {
|
||||||
|
@apply col-[2] justify-self-end mt-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container breakdown-settings (max-width: 560px) {
|
||||||
|
.breakdown-config {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.breakdown-module-options {
|
||||||
|
@apply grid-cols-[repeat(auto-fit,_minmax(min(100%,_160px),_1fr))];
|
||||||
|
}
|
||||||
|
.breakdown-preview-button.n-button {
|
||||||
|
@apply col-[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.result-toolbar .n-tabs-tab__label {
|
||||||
|
@apply inline-flex gap-1.5;
|
||||||
|
}
|
||||||
|
.breakdown-results-section {
|
||||||
|
@apply flex flex-col flex-1 min-h-0 min-w-0 gap-2;
|
||||||
|
}
|
||||||
|
.breakdown-results-section > .result-toolbar {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.breakdown-results-section > .content-with-history {
|
||||||
|
@apply flex-1 h-auto min-h-0 grid-rows-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.breakdown-results-scroll .n-scrollbar-content {
|
||||||
|
@apply wrap-anywhere;
|
||||||
|
}
|
||||||
|
.breakdown-results-section > .content-with-history.history-hidden {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.breakdown-results-section > .content-with-history {
|
||||||
|
@apply grid-rows-[minmax(0,_1fr)_min(25%,_130px)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -106,3 +106,52 @@ const purposeLabels: Record<string, string> = {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.beat-section {
|
||||||
|
@apply mt-[25px] pt-[23px];
|
||||||
|
}
|
||||||
|
.beat-heading {
|
||||||
|
@apply flex items-center gap-2.5;
|
||||||
|
}
|
||||||
|
.beat-number {
|
||||||
|
@apply text-muted font-mono text-[11px];
|
||||||
|
}
|
||||||
|
.shot-row {
|
||||||
|
@apply flex gap-[17px] p-[17px] rounded-none bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.shot-label {
|
||||||
|
@apply shrink-0 w-[47px] text-[10px] text-muted;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.shot-row {
|
||||||
|
@apply gap-3 p-[13px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.breakdown-storyboard {
|
||||||
|
@apply pt-4 px-5 pb-5;
|
||||||
|
}
|
||||||
|
.breakdown-storyboard-toolbar {
|
||||||
|
@apply flex items-center justify-between flex-wrap gap-y-3 gap-x-6 mb-4;
|
||||||
|
}
|
||||||
|
.breakdown-episode-picker {
|
||||||
|
@apply flex items-center gap-3 flex-[0_1_420px] min-w-0 text-sm;
|
||||||
|
}
|
||||||
|
.breakdown-episode-picker > span {
|
||||||
|
@apply shrink-0 whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.breakdown-episode-picker .n-select {
|
||||||
|
@apply flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
.breakdown-storyboard-summary {
|
||||||
|
@apply flex items-center flex-wrap gap-y-2 gap-x-5 min-h-[34px] ml-auto;
|
||||||
|
}
|
||||||
|
.breakdown-storyboard-count {
|
||||||
|
@apply text-muted text-xs whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.breakdown-storyboard-summary .text-button {
|
||||||
|
@apply min-h-[34px] whitespace-nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -117,3 +117,35 @@ const filtered = computed(() =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.subject-record-list > article:nth-child(odd) {
|
||||||
|
@apply bg-(--app-control);
|
||||||
|
}
|
||||||
|
.subject-form-card {
|
||||||
|
@apply mt-3 p-4 min-w-0 wrap-anywhere bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.subject-record-list > article:nth-child(even) .subject-form-card {
|
||||||
|
@apply bg-(--app-control);
|
||||||
|
}
|
||||||
|
.subject-details summary {
|
||||||
|
@apply inline-flex items-center gap-[5px] text-ink text-[11px] list-none;
|
||||||
|
}
|
||||||
|
.subject-details summary::-webkit-details-marker {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.subject-details[open] summary svg {
|
||||||
|
@apply rotate-180;
|
||||||
|
}
|
||||||
|
.subject-list-toolbar {
|
||||||
|
@apply flex flex-wrap items-center justify-start gap-y-2.5 gap-x-3 mb-5;
|
||||||
|
}
|
||||||
|
.subject-list-search.n-input {
|
||||||
|
@apply flex-[0_1_260px] min-w-0 max-w-full;
|
||||||
|
}
|
||||||
|
.subject-list-count {
|
||||||
|
@apply shrink-0 text-muted text-xs whitespace-nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -249,3 +249,49 @@ function exportScript() {
|
|||||||
</div></div
|
</div></div
|
||||||
></WorkspacePage>
|
></WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.stage-strip {
|
||||||
|
@apply flex flex-wrap items-center gap-3.5;
|
||||||
|
}
|
||||||
|
.stage-item {
|
||||||
|
@apply flex items-center gap-[7px] text-muted text-[11px];
|
||||||
|
}
|
||||||
|
.stage-item.done {
|
||||||
|
@apply text-ink;
|
||||||
|
}
|
||||||
|
.stage-arrow {
|
||||||
|
@apply ml-3 text-muted;
|
||||||
|
}
|
||||||
|
.json-view {
|
||||||
|
@apply whitespace-pre-wrap wrap-anywhere p-4 bg-(--app-subtle) rounded-none font-mono text-[11px] leading-[1.9];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.stage-strip {
|
||||||
|
@apply gap-[9px];
|
||||||
|
}
|
||||||
|
.stage-arrow {
|
||||||
|
@apply ml-[3px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.script-section {
|
||||||
|
@apply flex-1 flex flex-col min-h-0;
|
||||||
|
}
|
||||||
|
.script-section > :first-child {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.script-section .content-with-history {
|
||||||
|
@apply flex-1 h-auto min-h-0;
|
||||||
|
}
|
||||||
|
.script-workspace-page .workspace-split > :not(.script-section) {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.script-section .n-tabs-tab__label {
|
||||||
|
@apply inline-flex items-center gap-2;
|
||||||
|
}
|
||||||
|
.script-section .content-with-history.history-hidden {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -166,3 +166,106 @@ onBeforeUnmount(() => observer?.disconnect())
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.script-summary {
|
||||||
|
@apply text-xs text-muted leading-[1.9] pt-[17px] px-0 pb-[22px] mb-[23px];
|
||||||
|
}
|
||||||
|
.script-body {
|
||||||
|
@apply whitespace-pre-wrap wrap-anywhere text-sm leading-[2.15] text-ink;
|
||||||
|
}
|
||||||
|
.script-notes {
|
||||||
|
@apply mt-9 pt-5 text-xs;
|
||||||
|
}
|
||||||
|
.script-notes dt {
|
||||||
|
@apply text-ink font-medium mb-1.5;
|
||||||
|
}
|
||||||
|
.script-notes dd {
|
||||||
|
@apply text-muted leading-[1.8] mb-[15px];
|
||||||
|
}
|
||||||
|
.episode-reader {
|
||||||
|
@apply grid grid-cols-[clamp(190px,_22%,_240px)_minmax(0,_1fr)] h-full min-h-0 min-w-0 overflow-hidden;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
.reader-directory {
|
||||||
|
@apply flex flex-col min-h-0 min-w-0 bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.reader-directory-heading {
|
||||||
|
@apply flex justify-between items-center shrink-0 gap-2 pt-[22px] px-5 pb-3.5 text-[13px];
|
||||||
|
}
|
||||||
|
.reader-directory-heading h3 {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
.reader-directory-heading span {
|
||||||
|
@apply text-[11px] text-muted whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.reader-directory-scroll {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
.reader-directory-content {
|
||||||
|
@apply flex flex-col gap-1.5 pt-1 px-0 pb-[18px];
|
||||||
|
}
|
||||||
|
.n-button.reader-episode-link {
|
||||||
|
@apply shrink-0 w-full h-auto min-h-[64px] py-3 px-3.5 rounded-none text-muted whitespace-normal text-left;
|
||||||
|
}
|
||||||
|
.n-button.reader-episode-link .n-button__content {
|
||||||
|
@apply flex flex-col items-start gap-[5px] w-full min-w-0;
|
||||||
|
}
|
||||||
|
.reader-episode-label {
|
||||||
|
@apply text-[13px] font-semibold leading-normal;
|
||||||
|
}
|
||||||
|
.reader-episode-title {
|
||||||
|
@apply text-xs leading-[1.6] wrap-anywhere;
|
||||||
|
}
|
||||||
|
.n-button.reader-episode-link.active {
|
||||||
|
@apply bg-(--app-selected) text-accent shadow-[inset_3px_0_var(--app-accent)];
|
||||||
|
}
|
||||||
|
.reader-page {
|
||||||
|
@apply min-w-0 min-h-0 overflow-hidden bg-(--app-surface);
|
||||||
|
}
|
||||||
|
.reader-scroll .n-scrollbar-container:focus-visible {
|
||||||
|
@apply outline-[2px_solid_var(--app-accent)] outline-offset-[-3px];
|
||||||
|
}
|
||||||
|
.reader-content {
|
||||||
|
@apply mx-0 p-8 text-left;
|
||||||
|
/* 正文紧邻目录左对齐,保留阅读行宽,不在宽屏内容区居中。 */
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
.reader-chapter + .reader-chapter {
|
||||||
|
@apply mt-12 pt-9;
|
||||||
|
}
|
||||||
|
.reader-chapter:last-child {
|
||||||
|
/* 末集较短时仍可定位到阅读线,不必把倒数第二集误认为当前集。 */
|
||||||
|
min-height: max(160px, calc(var(--reader-height) - 64px));
|
||||||
|
}
|
||||||
|
.reader-chapter-heading {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
.reader-chapter-heading p {
|
||||||
|
@apply text-muted text-[13px] font-medium mb-2.5;
|
||||||
|
}
|
||||||
|
.reader-chapter-heading h2 {
|
||||||
|
@apply text-[clamp(18px,2cqw,24px)] font-semibold leading-[1.6] wrap-anywhere;
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.episode-reader {
|
||||||
|
@apply grid-cols-[180px_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.episode-reader {
|
||||||
|
@apply grid-cols-[112px_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.reader-directory-heading {
|
||||||
|
@apply pt-4 px-3 pb-3 flex-wrap;
|
||||||
|
}
|
||||||
|
.n-button.reader-episode-link {
|
||||||
|
@apply py-3 px-2.5;
|
||||||
|
}
|
||||||
|
.reader-content {
|
||||||
|
@apply px-[18px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -619,3 +619,99 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</WorkspacePage>
|
</WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.production-controls {
|
||||||
|
@apply grid grid-cols-[minmax(220px,_1.6fr)_minmax(110px,_0.5fr)_minmax(230px,_1fr)_auto] items-end gap-[18px];
|
||||||
|
}
|
||||||
|
.production-pipeline {
|
||||||
|
@apply grid grid-cols-[repeat(3,_minmax(0,_1fr))] gap-3.5;
|
||||||
|
}
|
||||||
|
.production-pipeline-card {
|
||||||
|
@apply p-5;
|
||||||
|
}
|
||||||
|
.production-stats {
|
||||||
|
@apply grid grid-cols-[repeat(3,_1fr)] gap-2;
|
||||||
|
}
|
||||||
|
.production-stats div,
|
||||||
|
.production-status-grid div {
|
||||||
|
@apply p-2.5 rounded-none bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.production-stats dt,
|
||||||
|
.production-status-grid dt {
|
||||||
|
@apply text-muted text-[10px];
|
||||||
|
}
|
||||||
|
.production-stats dd,
|
||||||
|
.production-status-grid dd {
|
||||||
|
@apply mt-[5px] text-[13px] font-mono;
|
||||||
|
}
|
||||||
|
.production-status-grid {
|
||||||
|
@apply grid grid-cols-[repeat(5,_minmax(0,_1fr))] gap-2.5;
|
||||||
|
}
|
||||||
|
.production-workspace {
|
||||||
|
@apply grid grid-cols-[220px_minmax(0,_1fr)] items-start overflow-hidden;
|
||||||
|
}
|
||||||
|
.production-shot-list {
|
||||||
|
@apply max-h-[760px] overflow-hidden bg-(--app-surface) py-2;
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.production-pipeline {
|
||||||
|
@apply grid-cols-[1fr];
|
||||||
|
}
|
||||||
|
.production-controls {
|
||||||
|
@apply grid-cols-[repeat(2,_minmax(0,_1fr))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.production-controls,
|
||||||
|
.production-workspace {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.production-shot-list {
|
||||||
|
@apply flex max-h-none overflow-hidden;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.production-status-grid {
|
||||||
|
@apply grid-cols-[repeat(2,_minmax(0,_1fr))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.workspace-inline-status {
|
||||||
|
@apply flex items-center flex-wrap justify-between gap-y-2 gap-x-4 py-[7px] px-3 shrink-0 rounded-none bg-(--app-subtle) text-xs;
|
||||||
|
}
|
||||||
|
.production-pipeline-card > .confirm-action {
|
||||||
|
@apply mt-4 mb-1;
|
||||||
|
}
|
||||||
|
.production-workspace {
|
||||||
|
@apply flex-1 min-h-0 items-stretch overflow-hidden;
|
||||||
|
}
|
||||||
|
.production-workspace {
|
||||||
|
@apply grid-cols-[clamp(240px,_22%,_280px)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.production-shot-list {
|
||||||
|
@apply max-h-none overflow-hidden min-h-0;
|
||||||
|
}
|
||||||
|
.production-workspace > article {
|
||||||
|
@apply overflow-hidden min-h-0 overscroll-contain;
|
||||||
|
}
|
||||||
|
.production-shot-list-content {
|
||||||
|
@apply gap-3 pt-0 px-0 pb-3;
|
||||||
|
}
|
||||||
|
.production-shot-list {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.production-controls {
|
||||||
|
@apply grid-cols-[minmax(130px,_1fr)_90px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.production-workspace {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[184px_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.production-pipeline {
|
||||||
|
@apply grid-cols-[1fr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -259,3 +259,11 @@ async function submit() {
|
|||||||
</p>
|
</p>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.quality-fields {
|
||||||
|
@apply grid grid-cols-[repeat(auto-fit,_minmax(min(100%,_180px),_1fr))] items-start gap-4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -140,3 +140,17 @@ function exportResult() {
|
|||||||
</NCollapse>
|
</NCollapse>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.quality-result {
|
||||||
|
@apply mt-6 p-4 bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.quality-result-row {
|
||||||
|
@apply flex flex-wrap items-center gap-y-2 gap-x-3 mt-2.5 p-3 bg-(--app-control) text-xs wrap-anywhere;
|
||||||
|
}
|
||||||
|
.quality-json {
|
||||||
|
@apply mt-4 whitespace-pre-wrap wrap-anywhere text-[11px] leading-[1.7];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -523,3 +523,46 @@ async function inspectSpecs() {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.production-stage {
|
||||||
|
@apply py-5 px-0;
|
||||||
|
}
|
||||||
|
.production-stage:first-of-type {
|
||||||
|
@apply border-t-0;
|
||||||
|
}
|
||||||
|
.production-stage-heading {
|
||||||
|
@apply flex items-start justify-between gap-4;
|
||||||
|
}
|
||||||
|
.keyframe-grid {
|
||||||
|
@apply grid grid-cols-[repeat(3,_minmax(0,_1fr))] gap-3;
|
||||||
|
}
|
||||||
|
.keyframe-card {
|
||||||
|
@apply overflow-hidden rounded-none bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.keyframe-card .asset-image {
|
||||||
|
@apply aspect-video rounded-none;
|
||||||
|
}
|
||||||
|
.video-record {
|
||||||
|
@apply flex overflow-hidden rounded-none bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.production-video {
|
||||||
|
@apply w-[min(44%,390px)] min-h-[170px] bg-ink object-contain;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.keyframe-grid {
|
||||||
|
@apply grid-cols-[repeat(2,_minmax(0,_1fr))];
|
||||||
|
}
|
||||||
|
.video-record {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
.production-video {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.production-video {
|
||||||
|
@apply bg-[#080808];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -80,3 +80,50 @@ onScopeDispose(() => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.project-frame {
|
||||||
|
@apply flex flex-col h-full min-h-0 overflow-hidden;
|
||||||
|
}
|
||||||
|
.project-header {
|
||||||
|
@apply shrink-0 py-[9px] px-5 bg-(--app-surface);
|
||||||
|
}
|
||||||
|
.project-header .n-page-header__main,
|
||||||
|
.project-header .n-page-header__title {
|
||||||
|
@apply min-w-0 overflow-hidden;
|
||||||
|
}
|
||||||
|
.project-title {
|
||||||
|
@apply max-w-full text-base font-semibold;
|
||||||
|
}
|
||||||
|
.project-notices.n-scrollbar {
|
||||||
|
@apply shrink-0 h-auto max-h-[100px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.project-notices-content {
|
||||||
|
@apply py-2 px-6 grid gap-[5px];
|
||||||
|
}
|
||||||
|
.project-notices .n-alert {
|
||||||
|
@apply py-[7px] px-3 text-xs;
|
||||||
|
}
|
||||||
|
.project-view {
|
||||||
|
@apply flex-1 min-h-0 overflow-hidden;
|
||||||
|
}
|
||||||
|
.project-header .n-page-header-wrapper {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
.project-access-gate {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
.workspace-loading {
|
||||||
|
@apply grid place-content-center h-full;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.project-header {
|
||||||
|
@apply py-2 px-3;
|
||||||
|
}
|
||||||
|
.project-title {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -140,3 +140,73 @@ function clearFilters() {
|
|||||||
</p>
|
</p>
|
||||||
</WorkspacePage>
|
</WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.page-heading {
|
||||||
|
@apply flex items-center justify-between gap-6 mb-7;
|
||||||
|
}
|
||||||
|
.page-heading h1 {
|
||||||
|
@apply mt-[9px] mx-0 mb-0 text-[27px] font-semibold tracking-[-0.035em] leading-[1.4];
|
||||||
|
}
|
||||||
|
.page-description {
|
||||||
|
@apply mt-[9px] text-muted text-xs leading-[1.7];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.page-heading {
|
||||||
|
@apply items-start flex-col gap-4;
|
||||||
|
}
|
||||||
|
.page-heading h1 {
|
||||||
|
@apply text-[23px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.project-table-area {
|
||||||
|
@apply flex-1 min-h-0;
|
||||||
|
}
|
||||||
|
.project-data-table {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
.project-data-table .n-data-table-base-table {
|
||||||
|
@apply flex-1 min-h-0;
|
||||||
|
}
|
||||||
|
.project-index-heading {
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
.project-index-heading h1 {
|
||||||
|
@apply text-[22px] font-semibold;
|
||||||
|
}
|
||||||
|
.project-index-toolbar {
|
||||||
|
@apply flex items-center justify-between flex-wrap gap-3 mb-1;
|
||||||
|
}
|
||||||
|
.project-search {
|
||||||
|
@apply w-[240px];
|
||||||
|
}
|
||||||
|
.project-status-filters.n-radio-group {
|
||||||
|
@apply flex flex-wrap h-auto min-h-(--n-height) gap-y-1 gap-x-px;
|
||||||
|
}
|
||||||
|
.project-status-filters .n-radio-group__splitor {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.project-status-filters .n-radio-button {
|
||||||
|
@apply min-w-[76px] px-[18px] text-center;
|
||||||
|
}
|
||||||
|
.project-name-cell {
|
||||||
|
@apply block min-w-0;
|
||||||
|
}
|
||||||
|
.project-name-cell strong,
|
||||||
|
.project-name-cell small {
|
||||||
|
@apply block overflow-hidden text-ellipsis whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.project-name-cell small {
|
||||||
|
@apply text-muted mt-1 text-[11px];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.project-search {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
.project-index-toolbar .n-radio-group {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -418,3 +418,73 @@ watch(
|
|||||||
></WorkspacePage
|
></WorkspacePage
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.storyboard-controls {
|
||||||
|
@apply grid grid-cols-[minmax(180px,_1fr)_100px_145px_auto] gap-[18px] items-end;
|
||||||
|
}
|
||||||
|
.generation-row {
|
||||||
|
@apply flex items-center justify-between gap-5 pt-5 mt-5;
|
||||||
|
}
|
||||||
|
.generation-row > div:last-child {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.storyboard-workspace {
|
||||||
|
@apply grid grid-cols-[220px_minmax(0,_1fr)] overflow-hidden;
|
||||||
|
}
|
||||||
|
.shot-list {
|
||||||
|
@apply max-h-[1000px] overflow-hidden bg-(--app-surface) pt-0 px-2 pb-5;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.generation-row {
|
||||||
|
@apply items-start flex-col gap-3;
|
||||||
|
}
|
||||||
|
.storyboard-controls {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.storyboard-controls {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)] gap-3;
|
||||||
|
}
|
||||||
|
.storyboard-workspace {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.shot-list {
|
||||||
|
@apply max-h-[240px];
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.storyboard-workspace-page .storyboard-workspace {
|
||||||
|
@apply flex-1 h-auto min-h-0 overflow-hidden;
|
||||||
|
}
|
||||||
|
.storyboard-coverage {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.storyboard-workspace {
|
||||||
|
@apply grid-cols-[clamp(240px,_22%,_280px)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.storyboard-workspace {
|
||||||
|
@apply h-[clamp(360px,65dvh,850px)] min-h-0;
|
||||||
|
}
|
||||||
|
.storyboard-workspace > article,
|
||||||
|
.storyboard-workspace .shot-list {
|
||||||
|
@apply overflow-hidden min-h-0 h-full max-h-none;
|
||||||
|
}
|
||||||
|
.shot-list-content {
|
||||||
|
@apply gap-3 pt-0 px-0 pb-3;
|
||||||
|
}
|
||||||
|
.shot-list {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.storyboard-workspace {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[184px_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.storyboard-controls {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -55,3 +55,35 @@ const groups = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.directory-group-heading {
|
||||||
|
@apply sticky top-0 z-[1] flex items-center justify-between gap-3 shrink-0 py-2.5 px-3.5 bg-(--app-control) font-mono font-semibold text-[11px] leading-[1.6] text-ink;
|
||||||
|
}
|
||||||
|
.directory-group-count {
|
||||||
|
@apply font-normal text-muted;
|
||||||
|
}
|
||||||
|
.beat-directory-group {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
.beat-directory-items {
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
.directory-mobile-beat {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.directory-group-heading {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.beat-directory-group,
|
||||||
|
.beat-directory-items {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.directory-mobile-beat {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -214,3 +214,20 @@ function exportResult(kind: 'spec' | 'prompt') {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.alert {
|
||||||
|
@apply py-3 px-[15px] rounded-none bg-(--app-subtle) text-ink text-xs leading-[1.8] wrap-anywhere;
|
||||||
|
}
|
||||||
|
.reference-grid {
|
||||||
|
@apply grid grid-cols-[repeat(auto-fill,_minmax(150px,_1fr))] gap-3.5;
|
||||||
|
}
|
||||||
|
.reference-card {
|
||||||
|
@apply bg-(--app-subtle) rounded-none overflow-hidden;
|
||||||
|
}
|
||||||
|
.storyboard-json {
|
||||||
|
@apply p-4 rounded-none bg-(--app-subtle) font-mono text-[11px] leading-[1.9];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -621,3 +621,117 @@ watch(
|
|||||||
@generate="generateCastingCandidate"
|
@generate="generateCastingCandidate"
|
||||||
/></WorkspacePage>
|
/></WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.identity-workspace {
|
||||||
|
@apply grid grid-cols-[240px_minmax(0,_1fr)] items-start gap-5;
|
||||||
|
}
|
||||||
|
.identity-subject-list {
|
||||||
|
@apply max-h-[560px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.casting-stats {
|
||||||
|
@apply grid grid-cols-[repeat(5,_minmax(0,_1fr))] gap-2.5;
|
||||||
|
}
|
||||||
|
.casting-stats div {
|
||||||
|
@apply py-2.5 px-3 rounded-none bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.casting-stats dt {
|
||||||
|
@apply text-muted text-[10px];
|
||||||
|
}
|
||||||
|
.casting-stats dd {
|
||||||
|
@apply mt-[5px] font-mono text-[13px];
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.identity-workspace {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.identity-subject-list {
|
||||||
|
@apply max-h-[220px];
|
||||||
|
}
|
||||||
|
.casting-stats {
|
||||||
|
@apply grid-cols-[repeat(2,_minmax(0,_1fr))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar-type-filter {
|
||||||
|
@apply w-[152px];
|
||||||
|
}
|
||||||
|
.identity-tools-intro {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
.identity-workspace {
|
||||||
|
@apply flex-1 min-h-0 items-stretch overflow-hidden;
|
||||||
|
}
|
||||||
|
.identity-workspace {
|
||||||
|
@apply grid-cols-[clamp(240px,_22%,_280px)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.identity-workspace > div {
|
||||||
|
@apply min-h-0 overflow-hidden overscroll-contain;
|
||||||
|
}
|
||||||
|
.identity-subject-list {
|
||||||
|
@apply max-h-none min-h-0 flex-1;
|
||||||
|
}
|
||||||
|
.directory-filters {
|
||||||
|
@apply grid gap-2.5 pt-0 px-3 pb-3 shrink-0;
|
||||||
|
}
|
||||||
|
.directory-filters > * {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
.identity-subject-list-content {
|
||||||
|
@apply gap-0.5 pt-0 px-0 pb-2;
|
||||||
|
}
|
||||||
|
.n-button.identity-subject-item {
|
||||||
|
@apply min-h-[86px] py-2.5 px-3.5;
|
||||||
|
}
|
||||||
|
.n-button.identity-subject-item:not(.selected) {
|
||||||
|
@apply bg-(--app-surface);
|
||||||
|
}
|
||||||
|
.n-button.identity-subject-item:not(.selected):hover {
|
||||||
|
@apply bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.identity-subject-item .directory-item-body {
|
||||||
|
@apply gap-[3px];
|
||||||
|
}
|
||||||
|
.identity-subject-item .directory-item-title {
|
||||||
|
@apply order-0 text-sm font-semibold;
|
||||||
|
}
|
||||||
|
.identity-subject-item .directory-item-eyebrow {
|
||||||
|
@apply order-1 text-[10px];
|
||||||
|
}
|
||||||
|
.identity-subject-item .directory-item-meta {
|
||||||
|
@apply order-2 gap-y-1 gap-x-1.5 text-[10px];
|
||||||
|
}
|
||||||
|
.casting-list {
|
||||||
|
@apply grid grid-cols-[repeat(auto-fit,_minmax(min(100%,_260px),_1fr))] gap-3 mt-6 pt-5;
|
||||||
|
}
|
||||||
|
.n-button.casting-item {
|
||||||
|
@apply w-full min-w-0 h-auto min-h-[76px] py-3 px-3.5 whitespace-normal text-left bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.n-button.casting-item.selected {
|
||||||
|
@apply bg-(--app-selected) shadow-[inset_3px_0_0_var(--app-accent)];
|
||||||
|
}
|
||||||
|
.n-button.casting-item .n-button__content {
|
||||||
|
@apply flex w-full min-w-0 items-center justify-between gap-4 text-left;
|
||||||
|
}
|
||||||
|
.casting-item-identity {
|
||||||
|
@apply flex flex-col gap-[5px] min-w-0;
|
||||||
|
}
|
||||||
|
.casting-item-name {
|
||||||
|
@apply text-sm font-medium leading-normal wrap-anywhere;
|
||||||
|
}
|
||||||
|
.casting-item-ref {
|
||||||
|
@apply text-muted font-mono text-[11px] leading-normal wrap-anywhere;
|
||||||
|
}
|
||||||
|
.casting-item-status {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.identity-workspace {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[250px_minmax(0,_1fr)] gap-2.5;
|
||||||
|
}
|
||||||
|
.directory-filters {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)_120px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -105,3 +105,18 @@ watch(
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../../styles/styles.css";
|
||||||
|
.identity-thumbnail {
|
||||||
|
@apply flex w-12 h-[64px] overflow-hidden bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.identity-thumbnail .n-image,
|
||||||
|
.identity-thumbnail .n-image img {
|
||||||
|
@apply w-full h-full;
|
||||||
|
}
|
||||||
|
.identity-thumbnail-placeholder {
|
||||||
|
@apply flex flex-col items-center justify-center w-full h-full gap-[5px] text-muted text-[10px];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -739,3 +739,118 @@ async function showStaleForms() {
|
|||||||
@changed="refreshAssets" /></template
|
@changed="refreshAssets" /></template
|
||||||
></WorkspacePage>
|
></WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.form-image-grid {
|
||||||
|
@apply grid grid-cols-[repeat(auto-fill,_minmax(min(260px,_100%),_1fr))] gap-5;
|
||||||
|
}
|
||||||
|
.form-image-card {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
.gallery-workspace-page .workspace-scroll-content {
|
||||||
|
@apply pt-2;
|
||||||
|
}
|
||||||
|
.gallery-workspace-page .gallery-sticky-controls {
|
||||||
|
@apply sticky top-0 z-10 bg-(--app-body) mt-2.5 mb-0;
|
||||||
|
}
|
||||||
|
.gallery-workspace-page .form-image-grid {
|
||||||
|
@apply pt-3;
|
||||||
|
}
|
||||||
|
.gallery-workspace-page .form-image-masonry {
|
||||||
|
@apply block columns-[260px] gap-x-5;
|
||||||
|
}
|
||||||
|
.form-image-masonry > .form-image-card {
|
||||||
|
@apply break-inside-avoid mb-5;
|
||||||
|
}
|
||||||
|
.form-image-masonry .asset-image {
|
||||||
|
@apply aspect-[var(--form-image-aspect,4/3)];
|
||||||
|
}
|
||||||
|
.gallery-sticky-controls > .form-image-filter-region {
|
||||||
|
@apply my-0;
|
||||||
|
}
|
||||||
|
.gallery-workspace-page .workspace-scroll > .n-scrollbar-container {
|
||||||
|
@apply [overflow-anchor:none];
|
||||||
|
}
|
||||||
|
.form-image-filter-region {
|
||||||
|
@apply my-4 py-3.5 px-4 bg-(--app-subtle);
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
.form-image-toolbar {
|
||||||
|
@apply grid grid-cols-[minmax(0,_1fr)] items-center gap-y-3 gap-x-4;
|
||||||
|
}
|
||||||
|
.form-image-toolbar.has-impact-picker {
|
||||||
|
@apply grid-cols-[minmax(220px,_340px)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.form-image-toolbar.has-impact-picker.has-impact-actions {
|
||||||
|
@apply grid-cols-[minmax(260px,_420px)_minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
.asset-impact-picker {
|
||||||
|
@apply flex items-center gap-2 min-w-0;
|
||||||
|
}
|
||||||
|
.asset-impact-picker .asset-impact-select {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
.form-image-toolbar > * {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
.form-image-filters {
|
||||||
|
@apply grid grid-cols-[minmax(200px,_360px)_152px_minmax(260px,_1fr)_auto] items-center gap-y-3 gap-x-4;
|
||||||
|
}
|
||||||
|
.form-image-filters > * {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
.filter-toggles {
|
||||||
|
@apply flex items-center flex-wrap gap-y-2.5 gap-x-4 text-xs;
|
||||||
|
}
|
||||||
|
.filter-toggles .n-checkbox__label {
|
||||||
|
@apply whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.filter-result-count {
|
||||||
|
@apply justify-self-end text-muted text-xs whitespace-nowrap;
|
||||||
|
}
|
||||||
|
.form-image-display-controls {
|
||||||
|
@apply flex items-center justify-self-end gap-3;
|
||||||
|
}
|
||||||
|
.gallery-layout-switch {
|
||||||
|
@apply flex items-center gap-1;
|
||||||
|
}
|
||||||
|
@container (max-width: 1200px) {
|
||||||
|
.form-image-toolbar.has-impact-picker,
|
||||||
|
.form-image-toolbar.has-impact-picker.has-impact-actions {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container (max-width: 900px) {
|
||||||
|
.form-image-filters {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)_152px];
|
||||||
|
}
|
||||||
|
.form-image-display-controls {
|
||||||
|
@apply col-[1/-1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container (max-width: 460px) {
|
||||||
|
.form-image-filters {
|
||||||
|
@apply grid-cols-[minmax(0,_1fr)_120px] gap-x-2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asset-impact-context {
|
||||||
|
@apply flex flex-col gap-2.5 py-[5px] px-4 bg-(--app-subtle) m-0 min-w-0;
|
||||||
|
}
|
||||||
|
.asset-impact-links-scroll.n-scrollbar {
|
||||||
|
@apply h-auto min-w-0 max-w-full;
|
||||||
|
}
|
||||||
|
.asset-impact-links {
|
||||||
|
@apply flex items-center gap-3 w-max whitespace-nowrap min-h-8 py-1.5;
|
||||||
|
}
|
||||||
|
.asset-impact-links > * {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
.asset-impact-select {
|
||||||
|
@apply min-w-0 w-full;
|
||||||
|
}
|
||||||
|
.form-image-card-focused {
|
||||||
|
@apply shadow-[inset_3px_0_0_var(--app-accent-text)];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -165,3 +165,26 @@ function removeImage(image: VisualStyleImage) {
|
|||||||
@remove="removeImage"
|
@remove="removeImage"
|
||||||
/></WorkspacePage>
|
/></WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.visual-style-toolbar {
|
||||||
|
@apply flex items-center justify-between flex-wrap gap-y-4 gap-x-6 my-5 py-3.5 px-4 bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
.visual-style-status {
|
||||||
|
@apply flex items-center gap-3 min-w-0;
|
||||||
|
}
|
||||||
|
.visual-style-status > svg {
|
||||||
|
@apply shrink-0 text-muted;
|
||||||
|
}
|
||||||
|
.visual-style-status strong {
|
||||||
|
@apply text-sm font-medium;
|
||||||
|
}
|
||||||
|
.visual-style-status p {
|
||||||
|
@apply mt-1 mx-0 mb-0 text-muted text-xs leading-[1.6];
|
||||||
|
}
|
||||||
|
.visual-style-actions {
|
||||||
|
@apply flex items-center flex-wrap gap-2.5 ml-auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -56,3 +56,11 @@ function confirm() {
|
|||||||
</AppDialog>
|
</AppDialog>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.confirm-action {
|
||||||
|
@apply inline-flex items-center max-w-full;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -54,3 +54,27 @@ const history = computed(() => workflowCheckpoints(props.checkpoints, props.work
|
|||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.history-panel {
|
||||||
|
@apply py-[23px] px-[21px] bg-(--app-subtle);
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.history-panel {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.history-panel {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
.history-content {
|
||||||
|
@apply py-[23px] px-[21px];
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.history-panel {
|
||||||
|
@apply overflow-hidden min-h-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -161,3 +161,20 @@ function exportReport() {
|
|||||||
/>
|
/>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||||
|
@reference "../../styles/styles.css";
|
||||||
|
.diagnostics-filters {
|
||||||
|
@apply flex flex-wrap items-center gap-3;
|
||||||
|
}
|
||||||
|
.diagnostics-filters > .n-input {
|
||||||
|
@apply flex-[1_1_240px] min-w-0;
|
||||||
|
}
|
||||||
|
.diagnostics-filters > .n-select {
|
||||||
|
@apply flex-[0_1_180px] min-w-0;
|
||||||
|
}
|
||||||
|
.diagnostics-records {
|
||||||
|
@apply grid gap-3 min-w-0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { router } from './router'
|
import { router } from './router'
|
||||||
import './styles.css'
|
import './styles/styles.css'
|
||||||
import './admin.css'
|
import './styles/admin.css'
|
||||||
|
|
||||||
/** 单页应用入口;不在浏览器保存模型密钥。 */
|
/** 单页应用入口;不在浏览器保存模型密钥。 */
|
||||||
createApp(App).use(router).mount('#app')
|
createApp(App).use(router).mount('#app')
|
||||||
|
|||||||
-781
@@ -1,781 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
|
|
||||||
/* 微信风格明暗主题:灰阶表面搭配绿色操作态,业务排版保持不变。 */
|
|
||||||
@theme {
|
|
||||||
--color-ink: var(--app-ink);
|
|
||||||
--color-muted: var(--app-muted);
|
|
||||||
--color-faint: var(--app-muted);
|
|
||||||
--color-line: var(--app-border);
|
|
||||||
--color-accent: var(--app-accent-text);
|
|
||||||
--color-danger: var(--app-danger);
|
|
||||||
--color-success: var(--app-success);
|
|
||||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: var(--app-body);
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
button,
|
|
||||||
a,
|
|
||||||
summary {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
button:not(:disabled),
|
|
||||||
summary,
|
|
||||||
select {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid var(--color-accent);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
input:focus-visible,
|
|
||||||
textarea:focus-visible,
|
|
||||||
select:focus-visible {
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
::selection {
|
|
||||||
background: var(--app-selected);
|
|
||||||
color: var(--app-ink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.identity-workspace {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 240px minmax(0, 1fr);
|
|
||||||
align-items: start;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
.identity-subject-list {
|
|
||||||
max-height: 560px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.casting-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.casting-stats div {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.casting-stats dt {
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.casting-stats dd {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.identity-workspace {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.identity-subject-list {
|
|
||||||
max-height: 220px;
|
|
||||||
}
|
|
||||||
.casting-stats {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
max-width: 1540px;
|
|
||||||
margin-inline: auto;
|
|
||||||
padding: 37px 40px 56px;
|
|
||||||
}
|
|
||||||
.page-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
.page-heading h1 {
|
|
||||||
margin: 9px 0 0;
|
|
||||||
font-size: 27px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.035em;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.eyebrow {
|
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 0.11em;
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.page-description {
|
|
||||||
margin-top: 9px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 9px 14px;
|
|
||||||
min-height: 36px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition:
|
|
||||||
background 0.15s,
|
|
||||||
border-color 0.15s;
|
|
||||||
}
|
|
||||||
.button-primary {
|
|
||||||
background: var(--app-button-primary);
|
|
||||||
color: var(--app-on-accent);
|
|
||||||
}
|
|
||||||
.button-primary:hover:not(:disabled) {
|
|
||||||
background: var(--app-button-hover);
|
|
||||||
}
|
|
||||||
.button-primary:active:not(:disabled) {
|
|
||||||
background: var(--app-button-pressed);
|
|
||||||
}
|
|
||||||
.button:disabled {
|
|
||||||
background: var(--app-disabled-bg);
|
|
||||||
color: var(--app-disabled-text);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.button-secondary {
|
|
||||||
background: var(--app-control);
|
|
||||||
color: var(--app-ink);
|
|
||||||
}
|
|
||||||
.button-secondary:hover:not(:disabled) {
|
|
||||||
background: var(--app-control-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
padding: 6px 0;
|
|
||||||
}
|
|
||||||
.text-button:hover:not(:disabled) {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
.panel {
|
|
||||||
--app-field: var(--app-control);
|
|
||||||
--app-field-hover: var(--app-control-hover);
|
|
||||||
background: var(--app-surface);
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 次级信息用低对比底色成组,避免用边框把工作台切成碎片。 */
|
|
||||||
.surface-inset {
|
|
||||||
--app-field: var(--app-surface);
|
|
||||||
--app-field-hover: var(--app-surface);
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
/* 首项与其它条目统一内边距;背景过渡仅用于主题切换,不随鼠标或焦点变化。 */
|
|
||||||
.record-list > article {
|
|
||||||
padding: 20px;
|
|
||||||
transition: background-color 160ms ease;
|
|
||||||
}
|
|
||||||
.record-list > article:nth-child(even) {
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
/* 主体奇数行常驻原悬停底色,与偶数行形成固定斑马纹;内部控件仍保留交互反馈。 */
|
|
||||||
.subject-record-list > article:nth-child(odd) {
|
|
||||||
background: var(--app-control);
|
|
||||||
}
|
|
||||||
/* 形态块与所在主体条纹使用相反灰阶,避免偶数行内外同色;不增加边框或悬停态。 */
|
|
||||||
.subject-form-card {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.subject-record-list > article:nth-child(even) .subject-form-card {
|
|
||||||
background: var(--app-control);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-scroll {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.project-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.project-table th {
|
|
||||||
padding: 13px 20px;
|
|
||||||
background: var(--app-control);
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.project-table td {
|
|
||||||
padding: 21px 20px;
|
|
||||||
}
|
|
||||||
.project-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.project-table tbody tr:nth-child(even) td {
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.project-table tbody tr:hover td {
|
|
||||||
background: var(--app-selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
.production-controls {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(220px, 1.6fr) minmax(110px, 0.5fr) minmax(230px, 1fr) auto;
|
|
||||||
align-items: end;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
.production-pipeline {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.production-pipeline-card {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.production-step {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
color: var(--app-ink);
|
|
||||||
}
|
|
||||||
.production-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.production-stats div,
|
|
||||||
.production-status-grid div {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.production-stats dt,
|
|
||||||
.production-status-grid dt {
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.production-stats dd,
|
|
||||||
.production-status-grid dd {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
}
|
|
||||||
.production-status-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.production-workspace {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 220px minmax(0, 1fr);
|
|
||||||
align-items: start;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.production-shot-list {
|
|
||||||
max-height: 760px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--app-surface);
|
|
||||||
padding-block: 8px;
|
|
||||||
}
|
|
||||||
.production-stage {
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
.production-stage:first-of-type {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
.production-stage-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.keyframe-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.keyframe-card {
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.keyframe-card .asset-image {
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.video-record {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.production-video {
|
|
||||||
width: min(44%, 390px);
|
|
||||||
min-height: 170px;
|
|
||||||
background: var(--app-ink);
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.production-pipeline {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.production-controls {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.production-controls,
|
|
||||||
.production-workspace {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.production-shot-list {
|
|
||||||
display: flex;
|
|
||||||
max-height: none;
|
|
||||||
overflow: hidden;
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
.production-status-grid,
|
|
||||||
.keyframe-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.video-record {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.production-video {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 300px;
|
|
||||||
padding: 52px 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
.field-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 9px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--app-ink);
|
|
||||||
}
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--app-field);
|
|
||||||
color: var(--color-ink);
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.input::placeholder {
|
|
||||||
color: var(--app-muted);
|
|
||||||
}
|
|
||||||
.input:disabled {
|
|
||||||
background: var(--app-subtle);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.alert {
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
color: var(--app-ink);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.8;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
.alert-error {
|
|
||||||
background: var(--app-subtle);
|
|
||||||
color: var(--app-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-strip {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.stage-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
color: var(--app-muted);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.stage-item.done {
|
|
||||||
color: var(--app-ink);
|
|
||||||
}
|
|
||||||
.stage-arrow {
|
|
||||||
margin-left: 12px;
|
|
||||||
color: var(--app-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-with-history {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 207px;
|
|
||||||
min-height: 480px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.history-panel {
|
|
||||||
padding: 23px 21px;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-image-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
.form-image-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.asset-image {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.asset-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.asset-image-empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.image-history {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.image-history-item .asset-image-empty {
|
|
||||||
font-size: 9px;
|
|
||||||
padding: 6px;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.image-history-item .asset-image-empty svg {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
.storyboard-controls {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(180px, 1fr) 100px 145px auto;
|
|
||||||
gap: 18px;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
.generation-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.generation-row > div:last-child {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.storyboard-workspace {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 220px minmax(0, 1fr);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.shot-list {
|
|
||||||
max-height: 1000px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--app-surface);
|
|
||||||
padding: 0 8px 20px;
|
|
||||||
}
|
|
||||||
.reference-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.reference-card {
|
|
||||||
background: var(--app-subtle);
|
|
||||||
border-radius: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.reference-placeholder {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.storyboard-json {
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
font:
|
|
||||||
11px/1.9 ui-monospace,
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
.script-summary {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
line-height: 1.9;
|
|
||||||
padding: 17px 0 22px;
|
|
||||||
margin-bottom: 23px;
|
|
||||||
}
|
|
||||||
.script-body {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 2.15;
|
|
||||||
color: var(--app-ink);
|
|
||||||
}
|
|
||||||
.script-notes {
|
|
||||||
margin-top: 36px;
|
|
||||||
padding-top: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.script-notes dt {
|
|
||||||
color: var(--app-ink);
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.script-notes dd {
|
|
||||||
color: var(--color-muted);
|
|
||||||
line-height: 1.8;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.detail-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 70px minmax(0, 1fr);
|
|
||||||
gap: 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.9;
|
|
||||||
}
|
|
||||||
.detail-grid dt {
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
.detail-grid dd {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
.json-view {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
border-radius: 0;
|
|
||||||
font:
|
|
||||||
11px/1.9 ui-monospace,
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-preview {
|
|
||||||
margin-top: 24px;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
.group-chip {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 8px 11px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.recovery-strip {
|
|
||||||
background: var(--app-subtle);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 17px;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.subject-ref {
|
|
||||||
display: inline-flex;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
padding: 3px 6px;
|
|
||||||
border-radius: 0;
|
|
||||||
color: var(--app-ink);
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.subject-details summary {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
color: var(--app-ink);
|
|
||||||
font-size: 11px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.subject-details summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.subject-details[open] summary svg {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
.beat-section {
|
|
||||||
margin-top: 25px;
|
|
||||||
padding-top: 23px;
|
|
||||||
}
|
|
||||||
.beat-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.beat-number {
|
|
||||||
color: var(--app-muted);
|
|
||||||
font:
|
|
||||||
11px ui-monospace,
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
.shot-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 17px;
|
|
||||||
padding: 17px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--app-subtle);
|
|
||||||
}
|
|
||||||
.shot-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 47px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--app-muted);
|
|
||||||
}
|
|
||||||
.skip-link {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 100;
|
|
||||||
top: -80px;
|
|
||||||
left: 15px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
background: var(--app-surface);
|
|
||||||
border: 1px solid var(--color-accent);
|
|
||||||
}
|
|
||||||
.skip-link:focus {
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 中等宽度保留正文空间,执行记录下移,移动端导航收为图标。 */
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.generation-row {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.storyboard-controls {
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.page-container {
|
|
||||||
padding-inline: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-with-history {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.history-panel {
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.storyboard-controls {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.storyboard-workspace {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.shot-list {
|
|
||||||
max-height: 240px;
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
padding: 25px 18px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-heading {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.page-heading h1 {
|
|
||||||
font-size: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-strip {
|
|
||||||
gap: 9px;
|
|
||||||
}
|
|
||||||
.stage-arrow {
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-table td,
|
|
||||||
.project-table th {
|
|
||||||
padding-inline: 14px;
|
|
||||||
}
|
|
||||||
.shot-row {
|
|
||||||
gap: 12px;
|
|
||||||
padding: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
/* 管理后台视口边界:document 不滚动,各个工作区和面板独立滚动。 */
|
||||||
|
:root {
|
||||||
|
--app-ink: #191919;
|
||||||
|
--app-muted: #707070;
|
||||||
|
--app-body: #ededed;
|
||||||
|
--app-surface: #ffffff;
|
||||||
|
--app-subtle: #f7f7f7;
|
||||||
|
--app-control: #f0f0f0;
|
||||||
|
--app-control-hover: #e5e5e5;
|
||||||
|
--app-field: #ffffff;
|
||||||
|
--app-field-hover: #ffffff;
|
||||||
|
--app-border: #e5e5e5;
|
||||||
|
--app-inverse: #ffffff;
|
||||||
|
--app-accent: #07c160;
|
||||||
|
--app-accent-hover: #2dcb79;
|
||||||
|
--app-accent-text: #087c42;
|
||||||
|
--app-on-accent: #ffffff;
|
||||||
|
--app-button-primary: #078640;
|
||||||
|
--app-button-hover: #087c42;
|
||||||
|
--app-button-pressed: #066b36;
|
||||||
|
--app-disabled-bg: #e4e4e4;
|
||||||
|
--app-disabled-text: #8c8c8c;
|
||||||
|
--app-selected: #e4f4e9;
|
||||||
|
--app-shadow: #00000018;
|
||||||
|
--app-danger: #a93232;
|
||||||
|
--app-success: #087c42;
|
||||||
|
}
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--app-ink: #d9d9d9;
|
||||||
|
--app-muted: #a3a3a3;
|
||||||
|
--app-body: #111111;
|
||||||
|
--app-surface: #191919;
|
||||||
|
--app-subtle: #232323;
|
||||||
|
--app-control: #2b2b2b;
|
||||||
|
--app-control-hover: #353535;
|
||||||
|
--app-field: #2b2b2b;
|
||||||
|
--app-field-hover: #353535;
|
||||||
|
--app-disabled-bg: #303030;
|
||||||
|
--app-disabled-text: #808080;
|
||||||
|
--app-border: #333333;
|
||||||
|
--app-inverse: #111111;
|
||||||
|
--app-accent-text: #5cd693;
|
||||||
|
--app-selected: #173527;
|
||||||
|
--app-shadow: #00000055;
|
||||||
|
--app-danger: #fa7373;
|
||||||
|
--app-success: #5cd693;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app,
|
||||||
|
.app-provider {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
background: var(--app-body);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
/* 外壳固定品牌与底部设置;只有菜单自身使用 NScrollbar 滚动。 */
|
||||||
|
/* Naive 菜单背景默认左右内缩 8px;只铺满导航行,不改图标与文字缩进。 */
|
||||||
|
/* 按钮保持直角,单图标按钮以 Naive 当前尺寸为边长,加载时也不改变宽度。 */
|
||||||
|
.n-button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.n-button.icon-button {
|
||||||
|
width: var(--n-height);
|
||||||
|
min-width: var(--n-height);
|
||||||
|
height: var(--n-height);
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.n-button.icon-button .n-button__icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.n-button:focus-visible {
|
||||||
|
outline: 2px solid var(--app-accent-text);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
/* 标题移除额外按钮后让名称使用剩余空间;未完成项目的引导仍留在内容区。 */
|
||||||
|
/* 常用工具固定在滚动区外;低频操作使用局部抽屉,不叠加占高面板。 */
|
||||||
|
.workspace-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
.toolbar-episode {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 1 340px;
|
||||||
|
min-width: 180px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.toolbar-episode .n-select {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.toolbar-metrics,
|
||||||
|
.toolbar-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
/* 身份说明与折叠入口分开,避免正文紧贴下一组操作。 */
|
||||||
|
/* 与 34px 中号输入框共用一行;不再通过 pb-2 / pb-3 手动垫高复选框。 */
|
||||||
|
.n-checkbox.control-row-checkbox {
|
||||||
|
align-self: end;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding-block: 0;
|
||||||
|
}
|
||||||
|
.control-row-checkbox .n-checkbox-box-wrapper {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
/* 拆解配置跟随抽屉实际宽度;标题 20px + 间距 8px,下方统一使用 34px 控件行。 */
|
||||||
|
/* 筛选与关联行整体吸顶,底部不留固定分隔带;列表自己的顶部间距随内容滚走。 */
|
||||||
|
/* 使用成熟的多列排版实现瀑布流;完整卡片不跨列,窄容器自动减少列数。 */
|
||||||
|
.workspace-feedback.n-scrollbar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: auto;
|
||||||
|
max-height: min(24dvh, 160px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.workspace-feedback .n-alert + .n-alert {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.overview-content {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
/* 目录统一为固定标题/筛选区和独立滚动列表;不再依赖按钮的默认行高与内边距。 */
|
||||||
|
.directory-panel {
|
||||||
|
--app-field: var(--app-surface);
|
||||||
|
--app-field-hover: var(--app-surface);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
.directory-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 18px 16px 14px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
.directory-heading h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
.directory-heading h3 span {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
.directory-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.directory-list-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0 16px;
|
||||||
|
}
|
||||||
|
/* 可选前导内容用于主体母版,不改变剧集与镜头目录的原有排版。 */
|
||||||
|
.directory-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--app-control);
|
||||||
|
}
|
||||||
|
.directory-status svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
/* 标签与操作由同一 NTabs 导航行管理,suffix 共享底线及垂直中心。 */
|
||||||
|
.tabs-toolbar {
|
||||||
|
container: tabs-toolbar / inline-size;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.workspace-tabs.n-tabs {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.workspace-tabs > .n-tabs-nav {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.workspace-tabs .n-tabs-tab {
|
||||||
|
min-height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.workspace-tabs .n-tabs-nav-scroll-wrapper {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.workspace-tabs .n-tabs-nav__suffix {
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.tabs-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 14px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
.tabs-toolbar-actions .n-button,
|
||||||
|
.tabs-toolbar-actions .text-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
/* 窄内容区允许 suffix 独占一行;Tabs 本身继续使用组件内置横向滚动。 */
|
||||||
|
@container tabs-toolbar (max-width: 600px) {
|
||||||
|
.workspace-tabs > .n-tabs-nav {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.workspace-tabs .n-tabs-nav-scroll-wrapper {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
.workspace-tabs .n-tabs-nav__suffix {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 筛选按内容区的实际宽度换行,避免侧栏展开后下拉框把其他控件挤到下一行。 */
|
||||||
|
/* 镜头选择和图库筛选共用一行,以控件中心对齐;窄屏保留完整筛选组,不挤压复选框。 */
|
||||||
|
.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 {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
/* 拆解结果使用剩余高度,不能再用视口比例定高后隐藏长列表。 */
|
||||||
|
/* 剧集选择、统计和下一步入口共用一行,避免空操作行与双层顶部留白。 */
|
||||||
|
/* 连续阅读区保留双栏,执行记录按需展开,不挤占默认阅读宽度。 */
|
||||||
|
/* 面板只负责尺寸与裁切;滚动和深浅主题滚动条统一交给 Naive UI。 */
|
||||||
|
.panel-scroll.n-scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.n-scrollbar > .n-scrollbar-container {
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
.production-detail,
|
||||||
|
.storyboard-detail,
|
||||||
|
.content-with-history > main {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.image-history.n-scrollbar,
|
||||||
|
.table-scroll.n-scrollbar {
|
||||||
|
height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.image-history.n-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.image-history-heading {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px 16px;
|
||||||
|
margin-block: 16px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.image-history-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 4px 4px 12px;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.code-scroll.n-scrollbar {
|
||||||
|
height: auto;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.code-scroll .json-view,
|
||||||
|
.code-scroll .storyboard-json {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
/* 项目筛选与搜索框同高,保留更宽的点击区;窄屏可自然换行。 */
|
||||||
|
/* 状态说明与操作各自成组,避免小标签夹在不同尺寸的按钮之间。 */
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
/* 预览尺寸不随原图分辨率增长,历史缩略图也不能被按钮默认宽度覆盖。 */
|
||||||
|
.n-button__content {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.n-button:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.n-button.image-history-item {
|
||||||
|
width: 128px;
|
||||||
|
min-width: 128px;
|
||||||
|
max-width: 128px;
|
||||||
|
flex: 0 0 128px;
|
||||||
|
height: auto;
|
||||||
|
padding: 6px;
|
||||||
|
white-space: normal;
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
.n-button.image-history-item.selected {
|
||||||
|
background: var(--app-selected);
|
||||||
|
box-shadow: inset 0 0 0 2px var(--app-accent);
|
||||||
|
}
|
||||||
|
.n-button.image-history-item .n-button__content {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.image-history-item .asset-image {
|
||||||
|
height: 88px;
|
||||||
|
min-height: 0;
|
||||||
|
aspect-ratio: auto;
|
||||||
|
}
|
||||||
|
.image-history-label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.image-history-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
/* 选角进度按抽屉实际宽度排列;按钮尺寸显式覆盖组件默认值。 */
|
||||||
|
/* 主体搜索靠左,统计紧随输入框;窄内容区自然换行,不占满整行或挤压控件。 */
|
||||||
|
/* 诊断筛选按实际空间换行,长 checkpoint ID 不撑开弹窗。 */
|
||||||
|
/* 镜头影响和素材定位使用固定底色;高亮只表示当前定位,不代表素材失效。 */
|
||||||
|
/* 关联素材始终单行横向浏览,数量和长名称不会撑高定位区或压缩图片区域。 */
|
||||||
|
/* 低频记录收起后归还正文宽度,不留下历史列的占位。 */
|
||||||
|
/* 补充说明只按需展开,继续使用平直背景层级,不增加边框或圆角。 */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.content-with-history {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.toolbar-episode {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.toolbar-actions {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.directory-panel {
|
||||||
|
height: 100%;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
.directory-heading {
|
||||||
|
padding: 12px 14px 8px;
|
||||||
|
}
|
||||||
|
.directory-list-content {
|
||||||
|
flex-direction: row;
|
||||||
|
width: max-content;
|
||||||
|
align-items: stretch;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.content-with-history {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr) 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
/* 微信风格明暗主题:灰阶表面搭配绿色操作态,业务排版保持不变。 */
|
||||||
|
@theme {
|
||||||
|
--color-ink: var(--app-ink);
|
||||||
|
--color-muted: var(--app-muted);
|
||||||
|
--color-faint: var(--app-muted);
|
||||||
|
--color-line: var(--app-border);
|
||||||
|
--color-accent: var(--app-accent-text);
|
||||||
|
--color-danger: var(--app-danger);
|
||||||
|
--color-success: var(--app-success);
|
||||||
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--app-body);
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
a,
|
||||||
|
summary {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
summary,
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
input:focus-visible,
|
||||||
|
textarea:focus-visible,
|
||||||
|
select:focus-visible {
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background: var(--app-selected);
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer components {
|
||||||
|
.eyebrow {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.11em;
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
border-color 0.15s;
|
||||||
|
}
|
||||||
|
.button-primary {
|
||||||
|
background: var(--app-button-primary);
|
||||||
|
color: var(--app-on-accent);
|
||||||
|
}
|
||||||
|
.button-primary:hover:not(:disabled) {
|
||||||
|
background: var(--app-button-hover);
|
||||||
|
}
|
||||||
|
.button-primary:active:not(:disabled) {
|
||||||
|
background: var(--app-button-pressed);
|
||||||
|
}
|
||||||
|
.button:disabled {
|
||||||
|
background: var(--app-disabled-bg);
|
||||||
|
color: var(--app-disabled-text);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.button-secondary {
|
||||||
|
background: var(--app-control);
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
.button-secondary:hover:not(:disabled) {
|
||||||
|
background: var(--app-control-hover);
|
||||||
|
}
|
||||||
|
.text-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
.text-button:hover:not(:disabled) {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
--app-field: var(--app-control);
|
||||||
|
--app-field-hover: var(--app-control-hover);
|
||||||
|
background: var(--app-surface);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
/* 次级信息用低对比底色成组,避免用边框把工作台切成碎片。 */
|
||||||
|
.surface-inset {
|
||||||
|
--app-field: var(--app-surface);
|
||||||
|
--app-field-hover: var(--app-surface);
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
/* 首项与其它条目统一内边距;背景过渡仅用于主题切换,不随鼠标或焦点变化。 */
|
||||||
|
.record-list > article {
|
||||||
|
padding: 20px;
|
||||||
|
transition: background-color 160ms ease;
|
||||||
|
}
|
||||||
|
.record-list > article:nth-child(even) {
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
/* 主体奇数行常驻原悬停底色,与偶数行形成固定斑马纹;内部控件仍保留交互反馈。 */
|
||||||
|
/* 形态块与所在主体条纹使用相反灰阶,避免偶数行内外同色;不增加边框或悬停态。 */
|
||||||
|
.table-scroll {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.project-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.project-table th {
|
||||||
|
padding: 13px 20px;
|
||||||
|
background: var(--app-control);
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.project-table td {
|
||||||
|
padding: 21px 20px;
|
||||||
|
}
|
||||||
|
.project-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.project-table tbody tr:nth-child(even) td {
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
.project-table tbody tr:hover td {
|
||||||
|
background: var(--app-selected);
|
||||||
|
}
|
||||||
|
.production-step {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--app-subtle);
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--app-field);
|
||||||
|
color: var(--color-ink);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 9px 11px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.input::placeholder {
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
.input:disabled {
|
||||||
|
background: var(--app-subtle);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.alert-error {
|
||||||
|
background: var(--app-subtle);
|
||||||
|
color: var(--app-danger);
|
||||||
|
}
|
||||||
|
.content-with-history {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 207px;
|
||||||
|
min-height: 480px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.image-history {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.image-history-item .asset-image-empty {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 6px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.image-history-item .asset-image-empty svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.reference-placeholder {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
background: var(--app-subtle);
|
||||||
|
}
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 70px minmax(0, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.9;
|
||||||
|
}
|
||||||
|
.detail-grid dt {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
.detail-grid dd {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.subject-ref {
|
||||||
|
display: inline-flex;
|
||||||
|
background: var(--app-subtle);
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 0;
|
||||||
|
color: var(--app-ink);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 中等宽度保留正文空间,执行记录下移,移动端导航收为图标。 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.content-with-history {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.project-table td,
|
||||||
|
.project-table th {
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { readFileSync, readdirSync } from 'node:fs'
|
||||||
|
|
||||||
|
const SPACE: Record<string, string> = {
|
||||||
|
'0': '0',
|
||||||
|
px: '1px',
|
||||||
|
'0.5': '2px',
|
||||||
|
'1': '4px',
|
||||||
|
'1.5': '6px',
|
||||||
|
'2': '8px',
|
||||||
|
'2.5': '10px',
|
||||||
|
'3': '12px',
|
||||||
|
'3.5': '14px',
|
||||||
|
'4': '16px',
|
||||||
|
'5': '20px',
|
||||||
|
'6': '24px',
|
||||||
|
'7': '28px',
|
||||||
|
'8': '32px',
|
||||||
|
'9': '36px',
|
||||||
|
'10': '40px',
|
||||||
|
'11': '44px',
|
||||||
|
'12': '48px',
|
||||||
|
'14': '56px'
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATIC: Record<string, string> = {
|
||||||
|
flex: 'display: flex;',
|
||||||
|
grid: 'display: grid;',
|
||||||
|
block: 'display: block;',
|
||||||
|
hidden: 'display: none;',
|
||||||
|
contents: 'display: contents;',
|
||||||
|
'inline-flex': 'display: inline-flex;',
|
||||||
|
'flex-col': 'flex-direction: column;',
|
||||||
|
'flex-wrap': 'flex-wrap: wrap;',
|
||||||
|
'items-center': 'align-items: center;',
|
||||||
|
'items-end': 'align-items: end;',
|
||||||
|
'items-start': 'align-items: flex-start;',
|
||||||
|
'items-stretch': 'align-items: stretch;',
|
||||||
|
'self-start': 'align-self: start;',
|
||||||
|
'justify-center': 'justify-content: center;',
|
||||||
|
'justify-start': 'justify-content: flex-start;',
|
||||||
|
'justify-between': 'justify-content: space-between;',
|
||||||
|
'justify-self-end': 'justify-self: end;',
|
||||||
|
'shrink-0': 'flex-shrink: 0;',
|
||||||
|
'flex-1': 'flex: 1;',
|
||||||
|
'overflow-hidden': 'overflow: hidden;',
|
||||||
|
'min-h-0': 'min-height: 0;',
|
||||||
|
'min-w-0': 'min-width: 0;',
|
||||||
|
'w-full': 'width: 100%;',
|
||||||
|
'w-max': 'width: max-content;',
|
||||||
|
'h-full': 'height: 100%;',
|
||||||
|
'h-auto': 'height: auto;',
|
||||||
|
'h-dvh': 'height: 100dvh;',
|
||||||
|
'max-w-full': 'max-width: 100%;',
|
||||||
|
'max-h-none': 'max-height: none;',
|
||||||
|
'whitespace-nowrap': 'white-space: nowrap;',
|
||||||
|
'whitespace-normal': 'white-space: normal;',
|
||||||
|
'whitespace-pre-wrap': 'white-space: pre-wrap;',
|
||||||
|
'text-center': 'text-align: center;',
|
||||||
|
'text-left': 'text-align: left;',
|
||||||
|
'wrap-anywhere': 'overflow-wrap: anywhere;',
|
||||||
|
'rounded-none': 'border-radius: 0;',
|
||||||
|
relative: 'position: relative;',
|
||||||
|
sticky: 'position: sticky;',
|
||||||
|
fixed: 'position: fixed;',
|
||||||
|
'top-0': 'top: 0;',
|
||||||
|
'left-0': 'left: 0;',
|
||||||
|
'right-0': 'right: 0;',
|
||||||
|
'bottom-0': 'bottom: 0;',
|
||||||
|
'inset-0': 'inset: 0;',
|
||||||
|
'list-none': 'list-style: none;',
|
||||||
|
'place-items-center': 'place-items: center;',
|
||||||
|
'place-content-center': 'place-content: center;',
|
||||||
|
'object-contain': 'object-fit: contain;',
|
||||||
|
'overscroll-contain': 'overscroll-behavior: contain;',
|
||||||
|
'break-inside-avoid': 'break-inside: avoid;',
|
||||||
|
'outline-none': 'outline: none;',
|
||||||
|
'text-ellipsis': 'text-overflow: ellipsis;',
|
||||||
|
'font-normal': 'font-weight: 400;',
|
||||||
|
'font-medium': 'font-weight: 500;',
|
||||||
|
'font-semibold': 'font-weight: 600;',
|
||||||
|
'font-mono': 'font-family: ui-monospace, monospace;',
|
||||||
|
'text-xs': 'font-size: 12px;',
|
||||||
|
'text-sm': 'font-size: 14px;',
|
||||||
|
'text-base': 'font-size: 16px;',
|
||||||
|
'leading-normal': 'line-height: 1.5;',
|
||||||
|
'm-0': 'margin: 0;',
|
||||||
|
'p-0': 'padding: 0;',
|
||||||
|
'border-0': 'border: 0;',
|
||||||
|
'border-t-0': 'border-top: none;',
|
||||||
|
'mx-auto': 'margin-inline: auto;',
|
||||||
|
'mx-0': 'margin-inline: 0;',
|
||||||
|
'my-0': 'margin-block: 0;',
|
||||||
|
'ml-auto': 'margin-left: auto;',
|
||||||
|
'px-0': 'padding-inline: 0;',
|
||||||
|
'bg-transparent': 'background: transparent;',
|
||||||
|
'order-0': 'order: 0;',
|
||||||
|
'order-1': 'order: 1;',
|
||||||
|
'order-2': 'order: 2;',
|
||||||
|
'aspect-auto': 'aspect-ratio: auto;',
|
||||||
|
'aspect-video': 'aspect-ratio: 16 / 9;',
|
||||||
|
'rotate-180': 'transform: rotate(180deg);',
|
||||||
|
'text-ink': 'color: var(--app-ink);',
|
||||||
|
'text-muted': 'color: var(--app-muted);',
|
||||||
|
'text-accent': 'color: var(--app-accent-text);',
|
||||||
|
'bg-ink': 'background: var(--app-ink);',
|
||||||
|
'z-10': 'z-index: 10;'
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX_PROP: Record<string, string> = {
|
||||||
|
'min-h': 'min-height',
|
||||||
|
'min-w': 'min-width',
|
||||||
|
'max-h': 'max-height',
|
||||||
|
'max-w': 'max-width',
|
||||||
|
'grid-cols': 'grid-template-columns',
|
||||||
|
'grid-rows': 'grid-template-rows',
|
||||||
|
col: 'grid-column',
|
||||||
|
flex: 'flex',
|
||||||
|
w: 'width',
|
||||||
|
h: 'height',
|
||||||
|
p: 'padding',
|
||||||
|
px: 'padding-inline',
|
||||||
|
py: 'padding-block',
|
||||||
|
pt: 'padding-top',
|
||||||
|
pr: 'padding-right',
|
||||||
|
pb: 'padding-bottom',
|
||||||
|
pl: 'padding-left',
|
||||||
|
m: 'margin',
|
||||||
|
mx: 'margin-inline',
|
||||||
|
my: 'margin-block',
|
||||||
|
mt: 'margin-top',
|
||||||
|
mr: 'margin-right',
|
||||||
|
mb: 'margin-bottom',
|
||||||
|
ml: 'margin-left',
|
||||||
|
gap: 'gap',
|
||||||
|
'gap-x': 'column-gap',
|
||||||
|
'gap-y': 'row-gap',
|
||||||
|
text: 'font-size',
|
||||||
|
leading: 'line-height',
|
||||||
|
tracking: 'letter-spacing',
|
||||||
|
top: 'top',
|
||||||
|
right: 'right',
|
||||||
|
bottom: 'bottom',
|
||||||
|
left: 'left',
|
||||||
|
inset: 'inset',
|
||||||
|
z: 'z-index',
|
||||||
|
shadow: 'box-shadow',
|
||||||
|
outline: 'outline',
|
||||||
|
'outline-offset': 'outline-offset',
|
||||||
|
bg: 'background',
|
||||||
|
columns: 'columns',
|
||||||
|
aspect: 'aspect-ratio'
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIXES = Object.keys(PREFIX_PROP).toSorted((a, b) => b.length - a.length)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把单个 Tailwind 工具类还原成 CSS 声明,供样式契约测试对照。
|
||||||
|
* @param util 工具类名
|
||||||
|
* @returns CSS 声明,无法还原时返回空字符串
|
||||||
|
*/
|
||||||
|
function expandUtil(util: string): string {
|
||||||
|
if (util.startsWith('[') && util.endsWith(']')) {
|
||||||
|
const inner = util.slice(1, -1)
|
||||||
|
const colon = inner.indexOf(':')
|
||||||
|
if (colon > 0) return `${inner.slice(0, colon)}: ${inner.slice(colon + 1)};`
|
||||||
|
}
|
||||||
|
if (STATIC[util]) return STATIC[util]!
|
||||||
|
const paren = util.match(/^([a-z-]+)-\((--[\w-]+)\)$/)
|
||||||
|
if (paren) {
|
||||||
|
const prop = PREFIX_PROP[paren[1]!] ?? paren[1]
|
||||||
|
const cssVar = paren[2]!
|
||||||
|
if (prop === 'background') return `background: var(${cssVar});`
|
||||||
|
if (paren[1] === 'text') return `color: var(${cssVar});`
|
||||||
|
return `${prop}: var(${cssVar});`
|
||||||
|
}
|
||||||
|
const arb = util.match(/^(-?[a-z-]+)-\[(.+)\]$/)
|
||||||
|
if (arb) {
|
||||||
|
const name = arb[1]!
|
||||||
|
const raw = arb[2]!.replace(/_/g, ' ').replace(/,(?=\S)/g, ', ')
|
||||||
|
const prop = PREFIX_PROP[name.replace(/^-/, '')]
|
||||||
|
if (!prop) return `${name}: ${raw};`
|
||||||
|
if (name.startsWith('-')) return `${prop}: -${raw};`
|
||||||
|
return `${prop}: ${raw};`
|
||||||
|
}
|
||||||
|
for (const prefix of PREFIXES) {
|
||||||
|
if (!util.startsWith(`${prefix}-`)) continue
|
||||||
|
const key = util.slice(prefix.length + 1)
|
||||||
|
const px = SPACE[key]
|
||||||
|
const prop = PREFIX_PROP[prefix]
|
||||||
|
if (px !== undefined && prop) return `${prop}: ${px};`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 @apply 行展开成 CSS 声明,并补回测试仍在匹配的 padding/margin 简写。
|
||||||
|
* @param css 含 @apply 的样式源码
|
||||||
|
* @returns 展开后的样式文本
|
||||||
|
*/
|
||||||
|
function expandApply(css: string): string {
|
||||||
|
return css.replace(/@apply\s+([^;]+);/g, (_all, utils: string) => {
|
||||||
|
const decls = utils.trim().split(/\s+/).map(expandUtil).filter(Boolean)
|
||||||
|
const valueOf = (prefix: string) =>
|
||||||
|
decls
|
||||||
|
.find(item => item.startsWith(prefix))
|
||||||
|
?.slice(prefix.length)
|
||||||
|
.replace(';', '')
|
||||||
|
.trim()
|
||||||
|
const paddingX = valueOf('padding-inline:')
|
||||||
|
const paddingY = valueOf('padding-block:')
|
||||||
|
const paddingT = valueOf('padding-top:')
|
||||||
|
const paddingB = valueOf('padding-bottom:')
|
||||||
|
if (paddingY && paddingX) decls.push(`padding: ${paddingY} ${paddingX};`)
|
||||||
|
else if (paddingT && paddingX && paddingB) decls.push(`padding: ${paddingT} ${paddingX} ${paddingB};`)
|
||||||
|
const marginX = valueOf('margin-inline:')
|
||||||
|
const marginT = valueOf('margin-top:')
|
||||||
|
const marginB = valueOf('margin-bottom:')
|
||||||
|
if (marginT && marginX && marginB) decls.push(`margin: ${marginT} ${marginX} ${marginB};`)
|
||||||
|
if (marginT && marginB) decls.push(`margin-block: ${marginT} ${marginB};`)
|
||||||
|
const gapY = valueOf('row-gap:')
|
||||||
|
const gapX = valueOf('column-gap:')
|
||||||
|
if (gapY && gapX) decls.push(`gap: ${gapY} ${gapX};`)
|
||||||
|
return decls.join(' ')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取全局 CSS 与全部组件内 style,供样式契约测试对照搬迁后的规则。
|
||||||
|
* @returns 拼接后的样式源码
|
||||||
|
*/
|
||||||
|
export function readAllStyles(): string {
|
||||||
|
const vueFiles = readdirSync('src', { recursive: true, encoding: 'utf8' }).filter(path =>
|
||||||
|
String(path).endsWith('.vue')
|
||||||
|
)
|
||||||
|
const raw = ['src/styles/styles.css', 'src/styles/admin.css', ...vueFiles.map(path => `src/${path}`)]
|
||||||
|
.map(path => readFileSync(path, 'utf8'))
|
||||||
|
.join('\n')
|
||||||
|
return expandApply(raw)
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NInput, NDropdown } from 'naive-ui'
|
import { NInput, NDropdown } from 'naive-ui'
|
||||||
import ActionMenu from './ActionMenu.vue'
|
import ActionMenu from '@/components/ui/ActionMenu.vue'
|
||||||
import DetailDisclosure from './DetailDisclosure.vue'
|
import DetailDisclosure from '@/components/ui/DetailDisclosure.vue'
|
||||||
import StyleEditor from '../../features/visual-style/components/StyleEditor.vue'
|
import StyleEditor from '@/features/visual-style/components/StyleEditor.vue'
|
||||||
import SubjectList from '../../features/breakdown/components/SubjectList.vue'
|
import SubjectList from '@/features/breakdown/components/SubjectList.vue'
|
||||||
import { selectMenu } from '../../testing/naive'
|
import { selectMenu } from '@/testing/naive'
|
||||||
import type { SubjectCandidate } from '../../features/breakdown/types'
|
import type { SubjectCandidate } from '@/features/breakdown/types'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { NImage } from 'naive-ui'
|
import { NImage } from 'naive-ui'
|
||||||
import AssetImage from './AssetImage.vue'
|
import AssetImage from '@/components/ui/AssetImage.vue'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
+8
-10
@@ -1,8 +1,8 @@
|
|||||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NButton } from 'naive-ui'
|
import { NButton } from 'naive-ui'
|
||||||
import { readFileSync } from 'node:fs'
|
import DirectoryItem from '@/components/ui/DirectoryItem.vue'
|
||||||
import DirectoryItem from './DirectoryItem.vue'
|
import { readAllStyles } from '@/testing/styles'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => wrapper?.unmount())
|
afterEach(() => wrapper?.unmount())
|
||||||
@@ -53,7 +53,7 @@ describe('统一目录条目', () => {
|
|||||||
|
|
||||||
it('显式保护最小高度、留白、换行和窄屏横向条目,避免旧按钮样式重新挤压目录', () => {
|
it('显式保护最小高度、留白、换行和窄屏横向条目,避免旧按钮样式重新挤压目录', () => {
|
||||||
// DOM 环境不计算视觉几何;这些约束不能替代浏览器验收。
|
// DOM 环境不计算视觉几何;这些约束不能替代浏览器验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.n-button\.directory-item\s*\{[^}]*min-height:\s*88px;[^}]*padding:\s*12px 14px/)
|
expect(css).toMatch(/\.n-button\.directory-item\s*\{[^}]*min-height:\s*88px;[^}]*padding:\s*12px 14px/)
|
||||||
expect(css).toMatch(/\.directory-item-body\s*\{[^}]*flex-direction:\s*column;[^}]*gap:\s*7px/)
|
expect(css).toMatch(/\.directory-item-body\s*\{[^}]*flex-direction:\s*column;[^}]*gap:\s*7px/)
|
||||||
expect(css).toMatch(/\.directory-item-title\s*\{[^}]*overflow-wrap:\s*anywhere/)
|
expect(css).toMatch(/\.directory-item-title\s*\{[^}]*overflow-wrap:\s*anywhere/)
|
||||||
@@ -68,7 +68,7 @@ describe('统一目录条目', () => {
|
|||||||
|
|
||||||
it('侧栏与主体、镜头、剧集目录统一直角满行,保留文字内边距和移动端滚动', () => {
|
it('侧栏与主体、镜头、剧集目录统一直角满行,保留文字内边距和移动端滚动', () => {
|
||||||
// happy-dom 无法计算伪元素几何,保护实际控制选中背景宽度的 CSS 规则。
|
// happy-dom 无法计算伪元素几何,保护实际控制选中背景宽度的 CSS 规则。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.admin-sider \.n-menu \.n-menu-item-content::before\s*\{[^}]*left:\s*0;[^}]*right:\s*0;[^}]*border-radius:\s*0/
|
/\.admin-sider \.n-menu \.n-menu-item-content::before\s*\{[^}]*left:\s*0;[^}]*right:\s*0;[^}]*border-radius:\s*0/
|
||||||
)
|
)
|
||||||
@@ -76,18 +76,16 @@ describe('统一目录条目', () => {
|
|||||||
expect(css).toMatch(/\.reader-directory-content\s*\{[^}]*padding:\s*4px 0 18px/)
|
expect(css).toMatch(/\.reader-directory-content\s*\{[^}]*padding:\s*4px 0 18px/)
|
||||||
expect(css).not.toMatch(/\.reader-directory-content\s*\{[^}]*padding-inline:\s*[1-9]/)
|
expect(css).not.toMatch(/\.reader-directory-content\s*\{[^}]*padding-inline:\s*[1-9]/)
|
||||||
for (const selector of ['directory-item', 'reader-episode-link']) {
|
for (const selector of ['directory-item', 'reader-episode-link']) {
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(new RegExp(`\\.n-button\\.${selector}\\s*\\{[^}]*width:\\s*100%`))
|
||||||
new RegExp(
|
expect(css).toMatch(new RegExp(`\\.n-button\\.${selector}\\s*\\{[^}]*padding:\\s*12px 14px`))
|
||||||
`\\.n-button\\.${selector}\\s*\\{[^}]*width:\\s*100%;[^}]*padding:\\s*12px 14px;[^}]*border-radius:\\s*0`
|
expect(css).toMatch(new RegExp(`\\.n-button\\.${selector}\\s*\\{[^}]*border-radius:\\s*0`))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
expect(css).toMatch(/\.directory-group-heading\s*\{[^}]*padding:\s*10px 14px/)
|
expect(css).toMatch(/\.directory-group-heading\s*\{[^}]*padding:\s*10px 14px/)
|
||||||
expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/)
|
expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Tabs 工具栏与筛选网格限制最小宽度,不让组件默认宽度挤压内容', () => {
|
it('Tabs 工具栏与筛选网格限制最小宽度,不让组件默认宽度挤压内容', () => {
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.workspace-tabs\.n-tabs\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0/)
|
expect(css).toMatch(/\.workspace-tabs\.n-tabs\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0/)
|
||||||
expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav-scroll-wrapper\s*\{[^}]*min-width:\s*0/)
|
expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav-scroll-wrapper\s*\{[^}]*min-width:\s*0/)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
NTabs
|
NTabs
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { readFileSync, readdirSync } from 'node:fs'
|
import { readFileSync, readdirSync } from 'node:fs'
|
||||||
import { useTheme } from '../../composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
import { readAllStyles } from '@/testing/styles'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -132,7 +133,7 @@ describe('全站平面主题', () => {
|
|||||||
|
|
||||||
it('普通按钮只在键盘焦点可见时显示外框,不用失焦或禁止聚焦隐藏点击反馈', () => {
|
it('普通按钮只在键盘焦点可见时显示外框,不用失焦或禁止聚焦隐藏点击反馈', () => {
|
||||||
// DOM 环境不模拟真实鼠标/键盘焦点判定,保护共享 CSS 与触发器契约。
|
// DOM 环境不模拟真实鼠标/键盘焦点判定,保护共享 CSS 与触发器契约。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.n-button:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--app-accent-text\);[^}]*outline-offset:\s*2px;/
|
/\.n-button:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--app-accent-text\);[^}]*outline-offset:\s*2px;/
|
||||||
)
|
)
|
||||||
@@ -143,7 +144,7 @@ describe('全站平面主题', () => {
|
|||||||
|
|
||||||
it('图库筛选吸顶,关联内容横向单行滚动,不给头部和图片区制造双重滚动边界', () => {
|
it('图库筛选吸顶,关联内容横向单行滚动,不给头部和图片区制造双重滚动边界', () => {
|
||||||
// 静态保护吸顶和横向尺寸规则;真实视口滚动与位置仍需浏览器验收。
|
// 静态保护吸顶和横向尺寸规则;真实视口滚动与位置仍需浏览器验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.gallery-workspace-page \.gallery-sticky-controls\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*z-index:\s*10;[^}]*background:\s*var\(--app-body\);/
|
/\.gallery-workspace-page \.gallery-sticky-controls\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*z-index:\s*10;[^}]*background:\s*var\(--app-body\);/
|
||||||
)
|
)
|
||||||
@@ -166,7 +167,7 @@ describe('全站平面主题', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('瀑布流自适应列宽,卡片保持完整并按封面尺寸预留高度', () => {
|
it('瀑布流自适应列宽,卡片保持完整并按封面尺寸预留高度', () => {
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.gallery-workspace-page \.form-image-masonry\s*\{[^}]*display:\s*block;[^}]*columns:\s*260px;[^}]*column-gap:\s*20px;/
|
/\.gallery-workspace-page \.form-image-masonry\s*\{[^}]*display:\s*block;[^}]*columns:\s*260px;[^}]*column-gap:\s*20px;/
|
||||||
)
|
)
|
||||||
@@ -174,15 +175,15 @@ describe('全站平面主题', () => {
|
|||||||
/\.form-image-masonry > \.form-image-card\s*\{[^}]*break-inside:\s*avoid;[^}]*margin-bottom:\s*20px;/
|
/\.form-image-masonry > \.form-image-card\s*\{[^}]*break-inside:\s*avoid;[^}]*margin-bottom:\s*20px;/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.form-image-masonry \.asset-image\s*\{[^}]*aspect-ratio:\s*var\(--form-image-aspect, 4 \/ 3\);/
|
/\.form-image-masonry \.asset-image\s*\{[^}]*aspect-ratio:\s*var\(--form-image-aspect,\s*4\s*\/\s*3\);/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('图库顶部统一镜头选择和筛选的背景与垂直对齐,窄屏整组换行', () => {
|
it('图库顶部统一镜头选择和筛选的背景与垂直对齐,窄屏整组换行', () => {
|
||||||
// DOM 环境不计算几何;保护容器宽度断点和居中契约,真实坐标仍需浏览器验收。
|
// DOM 环境不计算几何;保护容器宽度断点和居中契约,真实坐标仍需浏览器验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.form-image-filter-region\s*\{[^}]*padding:\s*14px 16px;[^}]*background:\s*var\(--app-subtle\);/
|
/\.form-image-filter-region\s*\{[^}]*padding(?:-block)?:\s*14px;[^}]*background:\s*var\(--app-subtle\);/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.form-image-toolbar\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*12px 16px;/)
|
expect(css).toMatch(/\.form-image-toolbar\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*12px 16px;/)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
@@ -192,12 +193,14 @@ describe('全站平面主题', () => {
|
|||||||
/@container \(max-width: 1200px\)\s*\{\s*\.form-image-toolbar\.has-impact-picker,\s*\.form-image-toolbar\.has-impact-picker\.has-impact-actions\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/
|
/@container \(max-width: 1200px\)\s*\{\s*\.form-image-toolbar\.has-impact-picker,\s*\.form-image-toolbar\.has-impact-picker\.has-impact-actions\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.asset-impact-picker\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*8px;/)
|
expect(css).toMatch(/\.asset-impact-picker\s*\{[^}]*align-items:\s*center;[^}]*gap:\s*8px;/)
|
||||||
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*gap:\s*10px;[^}]*padding:\s*5px 16px;[^}]*margin:\s*0;/)
|
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*gap:\s*10px/)
|
||||||
|
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*padding(?:-block)?:\s*5px/)
|
||||||
|
expect(css).toMatch(/\.asset-impact-context\s*\{[^}]*margin:\s*0/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => {
|
it('单图标按钮以当前控件高度为边长,尺寸与内容按钮互不影响', () => {
|
||||||
// happy-dom 不计算几何尺寸,此项约束共用样式和调用方,实际布局仍需浏览器验收。
|
// happy-dom 不计算几何尺寸,此项约束共用样式和调用方,实际布局仍需浏览器验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.n-button\.icon-button\s*\{[^}]*width:\s*var\(--n-height\);[^}]*min-width:\s*var\(--n-height\);[^}]*height:\s*var\(--n-height\);[^}]*padding:\s*0/
|
/\.n-button\.icon-button\s*\{[^}]*width:\s*var\(--n-height\);[^}]*min-width:\s*var\(--n-height\);[^}]*height:\s*var\(--n-height\);[^}]*padding:\s*0/
|
||||||
)
|
)
|
||||||
@@ -221,23 +224,22 @@ describe('全站平面主题', () => {
|
|||||||
|
|
||||||
it('业务面板、列表分组和标签以背景区分,保留键盘焦点与选中指示', () => {
|
it('业务面板、列表分组和标签以背景区分,保留键盘焦点与选中指示', () => {
|
||||||
// happy-dom 不计算布局;此项保护 CSS 契约,不能替代浏览器视觉验收。
|
// happy-dom 不计算布局;此项保护 CSS 契约,不能替代浏览器视觉验收。
|
||||||
const admin = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
const css = readFileSync('src/styles.css', 'utf8')
|
expect(css).not.toMatch(/border-radius:\s*[1-9]/)
|
||||||
expect(admin + css).not.toMatch(/border-radius:\s*[1-9]/)
|
expect(css).not.toMatch(/border(?:-[\w]+)?:\s*1px (?:solid|dashed) var\(--(?:app-border|color-line)\)/)
|
||||||
expect(admin + css).not.toMatch(/border(?:-[\w]+)?:\s*1px (?:solid|dashed) var\(--(?:app-border|color-line)\)/)
|
|
||||||
expect(css).toMatch(/\.surface-inset\s*\{[^}]*background:\s*var\(--app-subtle\)/)
|
expect(css).toMatch(/\.surface-inset\s*\{[^}]*background:\s*var\(--app-subtle\)/)
|
||||||
expect(css).toMatch(/\.record-list > article:nth-child\(even\)\s*\{\s*background:/)
|
expect(css).toMatch(/\.record-list > article:nth-child\(even\)\s*\{\s*background:/)
|
||||||
expect(admin).toMatch(/\.directory-group-heading\s*\{[^}]*background:\s*var\(--app-control\)/)
|
expect(css).toMatch(/\.directory-group-heading\s*\{[^}]*background:\s*var\(--app-control\)/)
|
||||||
expect(admin).toMatch(/\.directory-status\s*\{[^}]*background:\s*var\(--app-control\)/)
|
expect(css).toMatch(/\.directory-status\s*\{[^}]*background:\s*var\(--app-control\)/)
|
||||||
expect(admin).toMatch(/\.n-button\.directory-item:focus-visible\s*\{[^}]*outline:\s*2px/)
|
expect(css).toMatch(/\.n-button\.directory-item:focus-visible\s*\{[^}]*outline:\s*2px/)
|
||||||
expect(admin).toMatch(
|
expect(css).toMatch(
|
||||||
/\.n-button\.directory-item\.selected\s*\{[^}]*var\(--app-selected\)[^}]*var\(--app-accent\)/
|
/\.n-button\.directory-item\.selected\s*\{[^}]*var\(--app-selected\)[^}]*var\(--app-accent\)/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('斑马纹首项保留统一顶部留白,背景色平滑过渡并遵循减少动态效果设置', () => {
|
it('斑马纹首项保留统一顶部留白,背景色平滑过渡并遵循减少动态效果设置', () => {
|
||||||
// DOM 环境不计算实际内边距,检查共享规则及之前覆盖首项留白的工具类。
|
// DOM 环境不计算实际内边距,检查共享规则及之前覆盖首项留白的工具类。
|
||||||
const css = readFileSync('src/styles.css', 'utf8')
|
const css = readFileSync('src/styles/styles.css', 'utf8')
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.record-list > article\s*\{[^}]*padding:\s*20px;[^}]*transition:\s*background-color 160ms ease;/
|
/\.record-list > article\s*\{[^}]*padding:\s*20px;[^}]*transition:\s*background-color 160ms ease;/
|
||||||
)
|
)
|
||||||
@@ -2,17 +2,18 @@ import { h } from 'vue'
|
|||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NConfigProvider, NDrawer, NDrawerContent, NModal, NScrollbar, NTooltip } from 'naive-ui'
|
import { NConfigProvider, NDrawer, NDrawerContent, NMenu, NModal, NScrollbar, NTooltip } from 'naive-ui'
|
||||||
import App from '../../App.vue'
|
import App from '@/App.vue'
|
||||||
import WorkspacePage from './WorkspacePage.vue'
|
import WorkspacePage from '@/components/ui/WorkspacePage.vue'
|
||||||
import AppDialog from './AppDialog.vue'
|
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||||
import WorkspaceTools from './WorkspaceTools.vue'
|
import WorkspaceTools from '@/components/ui/WorkspaceTools.vue'
|
||||||
import ProjectLayout from '../../features/projects/ProjectLayout.vue'
|
import ProjectLayout from '@/features/projects/ProjectLayout.vue'
|
||||||
import ConfirmAction from '../../features/workflows/ConfirmAction.vue'
|
import ConfirmAction from '@/features/workflows/ConfirmAction.vue'
|
||||||
import ThemeToggle from './ThemeToggle.vue'
|
import ThemeToggle from '@/components/ui/ThemeToggle.vue'
|
||||||
import { projectsApi } from '../../features/projects/api'
|
import { projectsApi } from '@/features/projects/api'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { drawerPanel } from '../../testing/naive'
|
import { drawerPanel } from '@/testing/naive'
|
||||||
|
import { readAllStyles } from '@/testing/styles'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -26,7 +27,7 @@ afterEach(() => {
|
|||||||
describe('管理后台组件边界', () => {
|
describe('管理后台组件边界', () => {
|
||||||
it('Tabs suffix 与标签共用导航行,窄屏换行但不产生外层滚动', () => {
|
it('Tabs suffix 与标签共用导航行,窄屏换行但不产生外层滚动', () => {
|
||||||
// DOM 测试不计算坐标;保护对齐尺寸与容器断点,视觉验收仍需浏览器。
|
// DOM 测试不计算坐标;保护对齐尺寸与容器断点,视觉验收仍需浏览器。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.workspace-tabs > \.n-tabs-nav\s*\{[^}]*align-items:\s*stretch;/)
|
expect(css).toMatch(/\.workspace-tabs > \.n-tabs-nav\s*\{[^}]*align-items:\s*stretch;/)
|
||||||
expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav__suffix\s*\{[^}]*align-items:\s*center;/)
|
expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav__suffix\s*\{[^}]*align-items:\s*center;/)
|
||||||
expect(css).toMatch(/\.tabs-toolbar-actions\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*40px;/)
|
expect(css).toMatch(/\.tabs-toolbar-actions\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*40px;/)
|
||||||
@@ -36,31 +37,32 @@ describe('管理后台组件边界', () => {
|
|||||||
|
|
||||||
it('滚动边界样式包含 document 锁定和工作区收缩约束', () => {
|
it('滚动边界样式包含 document 锁定和工作区收缩约束', () => {
|
||||||
// happy-dom 不计算视口几何;这里只保护 CSS 契约,真实滚动仍需浏览器验收。
|
// happy-dom 不计算视口几何;这里只保护 CSS 契约,真实滚动仍需浏览器验收。
|
||||||
const adminCss = readFileSync('src/admin.css', 'utf8')
|
const adminCss = readAllStyles()
|
||||||
expect(adminCss).toMatch(/html,\s*body,\s*#app,\s*\.app-provider\s*\{[^}]*overflow:\s*hidden/)
|
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/)
|
expect(adminCss).toMatch(/\.workspace-page\s*\{[^}]*min-height:\s*0;[^}]*overflow:\s*hidden/)
|
||||||
})
|
})
|
||||||
it('业务样式不再直接开启浏览器原生滚动条', () => {
|
it('业务样式不再直接开启浏览器原生滚动条', () => {
|
||||||
const css = readFileSync('src/admin.css', 'utf8') + readFileSync('src/styles.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).not.toMatch(/overflow(?:-[xy])?\s*:\s*(?:auto|scroll)\b/)
|
expect(css).not.toMatch(/overflow(?:-[xy])?\s*:\s*(?:auto|scroll)\b/)
|
||||||
})
|
})
|
||||||
it('历史缩略图尺寸在非分层样式中覆盖 Naive 按钮,原图不能撑大预览或横向条目', () => {
|
it('历史缩略图尺寸在非分层样式中覆盖 Naive 按钮,原图不能撑大预览或横向条目', () => {
|
||||||
// happy-dom 不计算几何;专门保护此前失效的层叠和固定尺寸约束。
|
// happy-dom 不计算几何;专门保护此前失效的层叠和固定尺寸约束。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readFileSync('src/styles/admin.css', 'utf8')
|
||||||
expect(css).not.toContain('@layer')
|
expect(css).not.toContain('@layer')
|
||||||
expect(css).toMatch(
|
const all = readAllStyles()
|
||||||
|
expect(all).toMatch(
|
||||||
/\.n-button\.image-history-item\s*\{[^}]*width:\s*128px;[^}]*min-width:\s*128px;[^}]*max-width:\s*128px;[^}]*flex:\s*0 0 128px;[^}]*padding:\s*6px/
|
/\.n-button\.image-history-item\s*\{[^}]*width:\s*128px;[^}]*min-width:\s*128px;[^}]*max-width:\s*128px;[^}]*flex:\s*0 0 128px;[^}]*padding:\s*6px/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.image-history-item \.asset-image\s*\{[^}]*height:\s*88px;[^}]*min-height:\s*0/)
|
expect(all).toMatch(/\.image-history-item \.asset-image\s*\{[^}]*height:\s*88px;[^}]*min-height:\s*0/)
|
||||||
expect(css).toMatch(
|
expect(all).toMatch(
|
||||||
/\.asset-image\.asset-image-preview\s*\{[^}]*height:\s*min\(42dvh, 420px\);[^}]*aspect-ratio:\s*auto/
|
/\.asset-image\.asset-image-preview\s*\{[^}]*height:\s*min\(42dvh,\s*420px\);[^}]*aspect-ratio:\s*auto/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.image-history\.n-scrollbar\s*\{[^}]*max-width:\s*100%;[^}]*min-width:\s*0/)
|
expect(all).toMatch(/\.image-history\.n-scrollbar\s*\{[^}]*max-width:\s*100%;[^}]*min-width:\s*0/)
|
||||||
expect(css).toMatch(/\.image-history-content\s*\{[^}]*display:\s*flex;[^}]*width:\s*max-content/)
|
expect(all).toMatch(/\.image-history-content\s*\{[^}]*display:\s*flex;[^}]*width:\s*max-content/)
|
||||||
})
|
})
|
||||||
it('选角面板保留说明间距,卡片按可用宽度排列并覆盖按钮默认高度', () => {
|
it('选角面板保留说明间距,卡片按可用宽度排列并覆盖按钮默认高度', () => {
|
||||||
// 保护间距与换行契约,真实宽高和暗色效果仍需浏览器验收。
|
// 保护间距与换行契约,真实宽高和暗色效果仍需浏览器验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.identity-tools-intro\s*\{[^}]*margin-bottom:\s*24px/)
|
expect(css).toMatch(/\.identity-tools-intro\s*\{[^}]*margin-bottom:\s*24px/)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.casting-list\s*\{[^}]*repeat\(auto-fit, minmax\(min\(100%, 260px\), 1fr\)\);[^}]*gap:\s*12px;[^}]*margin-top:\s*24px/
|
/\.casting-list\s*\{[^}]*repeat\(auto-fit, minmax\(min\(100%, 260px\), 1fr\)\);[^}]*gap:\s*12px;[^}]*margin-top:\s*24px/
|
||||||
@@ -74,7 +76,7 @@ describe('管理后台组件边界', () => {
|
|||||||
})
|
})
|
||||||
it('拆解分栏跟随剩余高度收缩,移动端历史区不挤出结果区', () => {
|
it('拆解分栏跟随剩余高度收缩,移动端历史区不挤出结果区', () => {
|
||||||
// DOM 环境不计算几何,保护完整的 flex → grid → NScrollbar 高度链。
|
// DOM 环境不计算几何,保护完整的 flex → grid → NScrollbar 高度链。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.breakdown-results-section\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
expect(css).toMatch(/\.breakdown-results-section\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
||||||
expect(css).toMatch(/\.breakdown-results-section > \.result-toolbar\s*\{[^}]*flex-shrink:\s*0/)
|
expect(css).toMatch(/\.breakdown-results-section > \.result-toolbar\s*\{[^}]*flex-shrink:\s*0/)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
@@ -149,6 +151,13 @@ describe('管理后台组件边界', () => {
|
|||||||
expect(wrapper.find('.admin-topbar [aria-label="后端连接"]').exists()).toBe(false)
|
expect(wrapper.find('.admin-topbar [aria-label="后端连接"]').exists()).toBe(false)
|
||||||
expect(wrapper.find('.admin-nav-scroll [aria-label="后端连接"]').exists()).toBe(false)
|
expect(wrapper.find('.admin-nav-scroll [aria-label="后端连接"]').exists()).toBe(false)
|
||||||
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('后端连接')
|
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('后端连接')
|
||||||
|
expect(wrapper.getComponent(NMenu).props()).toMatchObject({
|
||||||
|
collapsedWidth: 64,
|
||||||
|
iconSize: 20,
|
||||||
|
collapsedIconSize: 20,
|
||||||
|
indent: 22,
|
||||||
|
rootIndent: 22
|
||||||
|
})
|
||||||
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
|
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
|
||||||
expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true)
|
expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true)
|
||||||
const settings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
|
const settings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
|
||||||
@@ -159,14 +168,17 @@ describe('管理后台组件边界', () => {
|
|||||||
expect(wrapper.findAll('.admin-nav-scroll .n-menu .n-menu-item')).toHaveLength(8)
|
expect(wrapper.findAll('.admin-nav-scroll .n-menu .n-menu-item')).toHaveLength(8)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('侧栏菜单独立滚动,页脚不参与滚动且窄屏不超出视口底部', () => {
|
it('侧栏菜单独立滚动,页脚不参与滚动且窄屏展开平移主栏', () => {
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(/\.admin-sider\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*height:\s*100%/)
|
||||||
/\.admin-sider-content\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0;[^}]*overflow:\s*hidden/
|
|
||||||
)
|
|
||||||
expect(css).toMatch(/\.admin-nav-scroll\.n-scrollbar\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
expect(css).toMatch(/\.admin-nav-scroll\.n-scrollbar\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
||||||
expect(css).toMatch(/\.admin-sider-footer\s*\{[^}]*flex-shrink:\s*0/)
|
expect(css).toMatch(/\.admin-sider-footer\s*\{[^}]*flex-shrink:\s*0/)
|
||||||
expect(css).toMatch(/\.admin-sider:not\([^)]*\)\s*\{[^}]*bottom:\s*0;[^}]*height:\s*auto/)
|
expect(css).toContain('@media (max-width: 800px)')
|
||||||
|
expect(css).toMatch(/transition:\s*width\s+0\.32s/)
|
||||||
|
expect(css).toContain('calc(100% - var(--admin-sider-collapsed))')
|
||||||
|
expect(css).toContain('@media (prefers-reduced-motion: reduce)')
|
||||||
|
expect(css).not.toContain('.admin-sider-mask')
|
||||||
|
expect(css).not.toContain('.admin-sider.is-overlay')
|
||||||
for (const path of ['storyboard/StoryboardPage.vue', 'production/ProductionPage.vue']) {
|
for (const path of ['storyboard/StoryboardPage.vue', 'production/ProductionPage.vue']) {
|
||||||
const source = readFileSync(`src/features/${path}`, 'utf8')
|
const source = readFileSync(`src/features/${path}`, 'utf8')
|
||||||
expect(source).not.toContain('<span>剧集</span')
|
expect(source).not.toContain('<span>剧集</span')
|
||||||
@@ -174,6 +186,57 @@ describe('管理后台组件边界', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('窄屏展开导航复用同一侧栏并右移主栏,不压缩主栏宽度', async () => {
|
||||||
|
const realMatchMedia = window.matchMedia.bind(window)
|
||||||
|
vi.stubGlobal('matchMedia', (query: string) => {
|
||||||
|
if (query.includes('max-width: 800px')) {
|
||||||
|
return {
|
||||||
|
matches: true,
|
||||||
|
media: query,
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
addListener() {},
|
||||||
|
removeListener() {},
|
||||||
|
dispatchEvent() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return realMatchMedia(query)
|
||||||
|
})
|
||||||
|
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('.admin-sider')).toHaveLength(1)
|
||||||
|
expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed')
|
||||||
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 64px')
|
||||||
|
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.admin-nav-drawer').exists()).toBe(false)
|
||||||
|
await wrapper.get('[aria-label="展开侧栏"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('.admin-sider')).toHaveLength(1)
|
||||||
|
expect(wrapper.get('.admin-sider').classes()).not.toContain('is-collapsed')
|
||||||
|
expect(wrapper.get('.admin-sider').classes()).not.toContain('is-overlay')
|
||||||
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 208px')
|
||||||
|
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false)
|
||||||
|
expect(wrapper.get('.admin-sider').findAll('.n-menu .n-menu-item')).toHaveLength(8)
|
||||||
|
expect(wrapper.get('[aria-label="折叠侧栏"]').attributes('aria-expanded')).toBe('true')
|
||||||
|
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed')
|
||||||
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 64px')
|
||||||
|
})
|
||||||
|
|
||||||
it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => {
|
it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => {
|
||||||
wrapper = mount(AppDialog, {
|
wrapper = mount(AppDialog, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
@@ -298,7 +361,7 @@ describe('管理后台组件边界', () => {
|
|||||||
expect(wrapper.classes()).toContain('confirm-action')
|
expect(wrapper.classes()).toContain('confirm-action')
|
||||||
expect(wrapper.classes()).toContain('mt-4')
|
expect(wrapper.classes()).toContain('mt-4')
|
||||||
expect(wrapper.get('button').text()).toBe('测试操作')
|
expect(wrapper.get('button').text()).toBe('测试操作')
|
||||||
expect(readFileSync('src/admin.css', 'utf8')).toMatch(
|
expect(readAllStyles()).toMatch(
|
||||||
/\.production-pipeline-card > \.confirm-action\s*\{[^}]*margin-block:\s*16px 4px/
|
/\.production-pipeline-card > \.confirm-action\s*\{[^}]*margin-block:\s*16px 4px/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { effectScope, nextTick, ref } from 'vue'
|
import { effectScope, nextTick, ref } from 'vue'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { usePolling } from './usePolling'
|
import { usePolling } from '@/composables/usePolling'
|
||||||
|
|
||||||
afterEach(() => vi.useRealTimers())
|
afterEach(() => vi.useRealTimers())
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { effectScope, nextTick, type EffectScope } from 'vue'
|
import { effectScope, nextTick, type EffectScope } from 'vue'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { useTheme } from './useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
let scope: EffectScope | undefined
|
let scope: EffectScope | undefined
|
||||||
@@ -99,7 +99,7 @@ describe('微信风格明暗主题', () => {
|
|||||||
it.each([false, true])('明暗模式 %s 的组件表面、语义色与业务 CSS 一致,主要文字保持可读', dark => {
|
it.each([false, true])('明暗模式 %s 的组件表面、语义色与业务 CSS 一致,主要文字保持可读', dark => {
|
||||||
const state = setup(dark)
|
const state = setup(dark)
|
||||||
const { common, Menu, Button, Layout, Input, Select } = state.overrides.value
|
const { common, Menu, Button, Layout, Input, Select } = state.overrides.value
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readFileSync('src/styles/admin.css', 'utf8')
|
||||||
const lightTokens = css.match(/:root\s*\{([^}]+)\}/)![1]!
|
const lightTokens = css.match(/:root\s*\{([^}]+)\}/)![1]!
|
||||||
const darkTokens = css.match(/:root\[data-theme='dark'\]\s*\{([^}]+)\}/)![1]!
|
const darkTokens = css.match(/:root\[data-theme='dark'\]\s*\{([^}]+)\}/)![1]!
|
||||||
const tokens = Object.fromEntries(
|
const tokens = Object.fromEntries(
|
||||||
+17
-15
@@ -3,12 +3,13 @@ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NSelect } from 'naive-ui'
|
import { NSelect } from 'naive-ui'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import BreakdownPage from './BreakdownPage.vue'
|
import BreakdownPage from '@/features/breakdown/BreakdownPage.vue'
|
||||||
import { projectContextKey, type useProjectData } from '../projects/context'
|
import { projectContextKey, type useProjectData } from '@/features/projects/context'
|
||||||
import type { ProjectDetail } from '../projects/types'
|
import type { ProjectDetail } from '@/features/projects/types'
|
||||||
import type { Checkpoint } from '../workflows/types'
|
import type { Checkpoint } from '@/features/workflows/types'
|
||||||
import type { BreakdownModule, EpisodePlan } from './types'
|
import type { BreakdownModule, EpisodePlan } from '@/features/breakdown/types'
|
||||||
import { drawerPanel, selectMenu } from '../../testing/naive'
|
import { drawerPanel, selectMenu } from '@/testing/naive'
|
||||||
|
import { readAllStyles } from '@/testing/styles'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -176,10 +177,11 @@ describe('拆解页内容滚动', () => {
|
|||||||
|
|
||||||
it('拆解配置按抽屉宽度换行,单位不收缩,三类控件对齐且说明位于控件下方', () => {
|
it('拆解配置按抽屉宽度换行,单位不收缩,三类控件对齐且说明位于控件下方', () => {
|
||||||
// DOM 环境不计算坐标,检查统一标题偏移、34px 控件行和容器断点契约。
|
// DOM 环境不计算坐标,检查统一标题偏移、34px 控件行和容器断点契约。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.breakdown-settings-panel\s*\{\s*container:\s*breakdown-settings \/ inline-size;/)
|
expect(css).toMatch(/\.breakdown-settings-panel\s*\{\s*container:\s*breakdown-settings \/ inline-size;/)
|
||||||
expect(css).toMatch(/\.breakdown-config\s*\{[^}]*--breakdown-label-offset:\s*28px;/)
|
expect(css).toMatch(/\.breakdown-config\s*\{[^}]*--breakdown-label-offset:\s*28px;/)
|
||||||
expect(css).toMatch(/\.breakdown-config \.field-label\s*\{[^}]*margin:\s*0 0 8px;[^}]*line-height:\s*20px;/)
|
expect(css).toMatch(/\.breakdown-config \.field-label\s*\{[^}]*margin(?:-bottom)?:\s*(?:0 0 )?8px/)
|
||||||
|
expect(css).toMatch(/\.breakdown-config \.field-label\s*\{[^}]*line-height:\s*20px/)
|
||||||
expect(css).toMatch(/\.breakdown-group-unit\s*\{[^}]*flex-shrink:\s*0;[^}]*white-space:\s*nowrap;/)
|
expect(css).toMatch(/\.breakdown-group-unit\s*\{[^}]*flex-shrink:\s*0;[^}]*white-space:\s*nowrap;/)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.n-checkbox\.control-row-checkbox\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*34px;/
|
/\.n-checkbox\.control-row-checkbox\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*34px;/
|
||||||
@@ -190,7 +192,7 @@ describe('拆解页内容滚动', () => {
|
|||||||
expect(css).toContain('@container breakdown-settings (max-width: 780px)')
|
expect(css).toContain('@container breakdown-settings (max-width: 780px)')
|
||||||
expect(css).toContain('@container breakdown-settings (max-width: 560px)')
|
expect(css).toContain('@container breakdown-settings (max-width: 560px)')
|
||||||
expect(css).toContain('repeat(auto-fit, minmax(min(100%, 160px), 1fr))')
|
expect(css).toContain('repeat(auto-fit, minmax(min(100%, 160px), 1fr))')
|
||||||
expect(readFileSync('src/styles.css', 'utf8')).not.toContain('.breakdown-config')
|
expect(readFileSync('src/styles/styles.css', 'utf8')).not.toContain('.breakdown-config')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each(['人物', '场景', '道具'])('%s 使用共用固定斑马纹样式,搜索后保持列表条目结构', async label => {
|
it.each(['人物', '场景', '道具'])('%s 使用共用固定斑马纹样式,搜索后保持列表条目结构', async label => {
|
||||||
@@ -222,7 +224,7 @@ describe('拆解页内容滚动', () => {
|
|||||||
|
|
||||||
it('主体搜索与统计靠左居中,窄内容区可换行,输入框不再使用外置图标容器', () => {
|
it('主体搜索与统计靠左居中,窄内容区可换行,输入框不再使用外置图标容器', () => {
|
||||||
// DOM 环境不计算布局,保护输入框宽度、主题统计色与窄屏换行的样式契约。
|
// DOM 环境不计算布局,保护输入框宽度、主题统计色与窄屏换行的样式契约。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.subject-list-toolbar\s*\{[^}]*display:\s*flex;[^}]*flex-wrap:\s*wrap;[^}]*align-items:\s*center;[^}]*justify-content:\s*flex-start;/
|
/\.subject-list-toolbar\s*\{[^}]*display:\s*flex;[^}]*flex-wrap:\s*wrap;[^}]*align-items:\s*center;[^}]*justify-content:\s*flex-start;/
|
||||||
)
|
)
|
||||||
@@ -230,7 +232,7 @@ describe('拆解页内容滚动', () => {
|
|||||||
/\.subject-list-search\.n-input\s*\{[^}]*flex:\s*0 1 260px;[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/
|
/\.subject-list-search\.n-input\s*\{[^}]*flex:\s*0 1 260px;[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.subject-list-count\s*\{[^}]*color:\s*var\(--app-muted\);/)
|
expect(css).toMatch(/\.subject-list-count\s*\{[^}]*color:\s*var\(--app-muted\);/)
|
||||||
expect(css + readFileSync('src/styles.css', 'utf8')).not.toContain('.search-field')
|
expect(css + readFileSync('src/styles/styles.css', 'utf8')).not.toContain('.search-field')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -277,7 +279,7 @@ describe('拆解页内容滚动', () => {
|
|||||||
|
|
||||||
it('形态块在两种主体条纹上采用相反灰阶,以间距分组且没有整块悬停态', () => {
|
it('形态块在两种主体条纹上采用相反灰阶,以间距分组且没有整块悬停态', () => {
|
||||||
// DOM 环境不提供真实主题绘制,检查奇偶行底色、内边距和状态选择器契约。
|
// DOM 环境不提供真实主题绘制,检查奇偶行底色、内边距和状态选择器契约。
|
||||||
const css = readFileSync('src/styles.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.subject-form-card\s*\{[^}]*margin-top:\s*12px;[^}]*padding:\s*16px;[^}]*background:\s*var\(--app-subtle\);/
|
/\.subject-form-card\s*\{[^}]*margin-top:\s*12px;[^}]*padding:\s*16px;[^}]*background:\s*var\(--app-subtle\);/
|
||||||
)
|
)
|
||||||
@@ -289,12 +291,12 @@ describe('拆解页内容滚动', () => {
|
|||||||
|
|
||||||
it('主体奇数行常驻原悬停底色,偶数行不变,整行不再随鼠标或焦点变色', () => {
|
it('主体奇数行常驻原悬停底色,偶数行不变,整行不再随鼠标或焦点变色', () => {
|
||||||
// DOM 环境无法模拟浏览器 :hover 命中;检查静态底色与无整行交互选择器的契约。
|
// DOM 环境无法模拟浏览器 :hover 命中;检查静态底色与无整行交互选择器的契约。
|
||||||
const css = readFileSync('src/styles.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.record-list > article:nth-child\(even\)\s*\{\s*background:\s*var\(--app-subtle\);/)
|
expect(css).toMatch(/\.record-list > article:nth-child\(even\)\s*\{\s*background:\s*var\(--app-subtle\);/)
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.subject-record-list > article:nth-child\(odd\)\s*\{\s*background:\s*var\(--app-control\);/
|
/\.subject-record-list > article:nth-child\(odd\)\s*\{\s*background:\s*var\(--app-control\);/
|
||||||
)
|
)
|
||||||
expect(css + readFileSync('src/admin.css', 'utf8')).not.toMatch(
|
expect(css + readFileSync('src/styles/admin.css', 'utf8')).not.toMatch(
|
||||||
/\.subject-record-list[^{}]*(?::hover|:focus-within)/
|
/\.subject-record-list[^{}]*(?::hover|:focus-within)/
|
||||||
)
|
)
|
||||||
// 仅移除整行变色,不影响内部链接、折叠等控件的键盘焦点提示。
|
// 仅移除整行变色,不影响内部链接、折叠等控件的键盘焦点提示。
|
||||||
@@ -352,7 +354,7 @@ describe('拆解页内容滚动', () => {
|
|||||||
|
|
||||||
it('分镜顶部无叠加操作行,选择标签保持单行且统计操作垂直居中', () => {
|
it('分镜顶部无叠加操作行,选择标签保持单行且统计操作垂直居中', () => {
|
||||||
// 仅保护样式契约,DOM 环境不提供真实布局坐标。
|
// 仅保护样式契约,DOM 环境不提供真实布局坐标。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(/\.breakdown-storyboard\s*\{[^}]*padding:\s*16px 20px 20px;/)
|
expect(css).toMatch(/\.breakdown-storyboard\s*\{[^}]*padding:\s*16px 20px 20px;/)
|
||||||
expect(css).toMatch(/\.breakdown-episode-picker > span\s*\{[^}]*flex-shrink:\s*0;[^}]*white-space:\s*nowrap;/)
|
expect(css).toMatch(/\.breakdown-episode-picker > span\s*\{[^}]*flex-shrink:\s*0;[^}]*white-space:\s*nowrap;/)
|
||||||
expect(css).toMatch(/\.breakdown-storyboard-summary\s*\{[^}]*align-items:\s*center;/)
|
expect(css).toMatch(/\.breakdown-storyboard-summary\s*\{[^}]*align-items:\s*center;/)
|
||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { readFileSync } from 'node:fs'
|
import EpisodeReader from '@/features/create-drama/EpisodeReader.vue'
|
||||||
import EpisodeReader from './EpisodeReader.vue'
|
import { readAllStyles } from '@/testing/styles'
|
||||||
import type { Episode } from '../projects/types'
|
import type { Episode } from '@/features/projects/types'
|
||||||
|
|
||||||
const episodes: Episode[] = [
|
const episodes: Episode[] = [
|
||||||
{ episode: 1, title: '来信', summary: '第一集摘要', content: '第一场:旧书店。', conflict: '信件失踪' },
|
{ episode: 1, title: '来信', summary: '第一集摘要', content: '第一场:旧书店。', conflict: '信件失踪' },
|
||||||
@@ -64,7 +64,7 @@ function currentEpisode() {
|
|||||||
describe('连续剧本阅读器', () => {
|
describe('连续剧本阅读器', () => {
|
||||||
it('正文容器靠左并保留目录间距,宽屏不再使用自动外边距居中', () => {
|
it('正文容器靠左并保留目录间距,宽屏不再使用自动外边距居中', () => {
|
||||||
// DOM 测试不计算布局;保护正文定位样式,目录联动仍由后续交互测试验证。
|
// DOM 测试不计算布局;保护正文定位样式,目录联动仍由后续交互测试验证。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
const content = css.match(/\.reader-content\s*\{([^}]+)\}/)![1]!
|
const content = css.match(/\.reader-content\s*\{([^}]+)\}/)![1]!
|
||||||
expect(content).toContain('max-width: 900px')
|
expect(content).toContain('max-width: 900px')
|
||||||
expect(content).toContain('margin-inline: 0')
|
expect(content).toContain('margin-inline: 0')
|
||||||
+10
-10
@@ -1,16 +1,16 @@
|
|||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { projectContextKey } from '../projects/context'
|
import { projectContextKey } from '@/features/projects/context'
|
||||||
import { testProjectContext } from '../../testing/project-context'
|
import { testProjectContext } from '@/testing/project-context'
|
||||||
import { getOperation } from '../workflows/operations'
|
import { getOperation } from '@/features/workflows/operations'
|
||||||
import { formFixture } from '../subject-images/testing/fixtures'
|
import { formFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
import { directionsResult } from '../storyboard/testing/fixtures'
|
import { directionsResult } from '@/features/storyboard/testing/fixtures'
|
||||||
import { checkPipeline, useAdvancedProduction } from './useAdvancedProduction'
|
import { checkPipeline, useAdvancedProduction } from '@/features/production/useAdvancedProduction'
|
||||||
import { useProduction } from './useProduction'
|
import { useProduction } from '@/features/production/useProduction'
|
||||||
import { getProductionSession } from './model'
|
import { getProductionSession } from '@/features/production/model'
|
||||||
import AdvancedProduction from './components/AdvancedProduction.vue'
|
import AdvancedProduction from '@/features/production/components/AdvancedProduction.vue'
|
||||||
import { expandSections } from '../../testing/naive'
|
import { expandSections } from '@/testing/naive'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
/** 从实际挂载的确认弹窗查找操作按钮。 */
|
/** 从实际挂载的确认弹窗查找操作按钮。 */
|
||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { mediaAssetUrl } from '../../lib/assets'
|
import { mediaAssetUrl } from '@/lib/assets'
|
||||||
import KeyframeDialog from './components/KeyframeDialog.vue'
|
import KeyframeDialog from '@/features/production/components/KeyframeDialog.vue'
|
||||||
import { productionApi } from './api'
|
import { productionApi } from '@/features/production/api'
|
||||||
import {
|
import {
|
||||||
isActiveVideo,
|
isActiveVideo,
|
||||||
issueLabel,
|
issueLabel,
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
primaryVideo,
|
primaryVideo,
|
||||||
productionStatusLabel,
|
productionStatusLabel,
|
||||||
validOptionalSize
|
validOptionalSize
|
||||||
} from './model'
|
} from '@/features/production/model'
|
||||||
import { keyframeFixture, videoFixture } from './testing/fixtures'
|
import { keyframeFixture, videoFixture } from '@/features/production/testing/fixtures'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
+11
-11
@@ -2,9 +2,9 @@ import { defineComponent, reactive, ref } from 'vue'
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NCheckbox, NInputNumber, NRadioGroup } from 'naive-ui'
|
import { NCheckbox, NInputNumber, NRadioGroup } from 'naive-ui'
|
||||||
import { testProjectContext } from '../../testing/project-context'
|
import { testProjectContext } from '@/testing/project-context'
|
||||||
import { productionApi } from './api'
|
import { productionApi } from '@/features/production/api'
|
||||||
import { qualityApi } from './quality-api'
|
import { qualityApi } from '@/features/production/quality-api'
|
||||||
import {
|
import {
|
||||||
allowedTextLines,
|
allowedTextLines,
|
||||||
qualityKey,
|
qualityKey,
|
||||||
@@ -13,14 +13,14 @@ import {
|
|||||||
savedVideoValidation,
|
savedVideoValidation,
|
||||||
videoRepairInfo,
|
videoRepairInfo,
|
||||||
validQualityInput
|
validQualityInput
|
||||||
} from './quality'
|
} from '@/features/production/quality'
|
||||||
import type { KeyframeReadiness } from './types'
|
import type { KeyframeReadiness } from '@/features/production/types'
|
||||||
import type { QualityInput, QualityTarget } from './quality.types'
|
import type { QualityInput, QualityTarget } from '@/features/production/quality.types'
|
||||||
import { useQuality } from './useQuality'
|
import { useQuality } from '@/features/production/useQuality'
|
||||||
import { keyframeFixture, videoFixture } from './testing/fixtures'
|
import { keyframeFixture, videoFixture } from '@/features/production/testing/fixtures'
|
||||||
import QualityDialog from './components/QualityDialog.vue'
|
import QualityDialog from '@/features/production/components/QualityDialog.vue'
|
||||||
import QualityResult from './components/QualityResult.vue'
|
import QualityResult from '@/features/production/components/QualityResult.vue'
|
||||||
import ModelCapabilities from './components/ModelCapabilities.vue'
|
import ModelCapabilities from '@/features/production/components/ModelCapabilities.vue'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
let sequence = 0
|
let sequence = 0
|
||||||
@@ -3,11 +3,12 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import App from '../../App.vue'
|
import App from '@/App.vue'
|
||||||
import ProjectLayout from './ProjectLayout.vue'
|
import ProjectLayout from '@/features/projects/ProjectLayout.vue'
|
||||||
import { isProjectComplete } from './access'
|
import { isProjectComplete } from '@/features/projects/access'
|
||||||
import { projectsApi } from './api'
|
import { projectsApi } from '@/features/projects/api'
|
||||||
import type { ProjectDetail, ProjectStatus } from './types'
|
import type { ProjectDetail, ProjectStatus } from '@/features/projects/types'
|
||||||
|
import { readAllStyles } from '@/testing/styles'
|
||||||
|
|
||||||
const downstream = ['breakdown', 'visual-style', 'subject-identity', 'subject-images', 'storyboard', 'production']
|
const downstream = ['breakdown', 'visual-style', 'subject-identity', 'subject-images', 'storyboard', 'production']
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
@@ -195,12 +196,14 @@ describe('剧本完成前的下游访问限制', () => {
|
|||||||
|
|
||||||
it('配置复选框居中对齐,输入表面随面板背景分层而非增加边框', () => {
|
it('配置复选框居中对齐,输入表面随面板背景分层而非增加边框', () => {
|
||||||
// 保护布局契约;实际像素对齐仍需浏览器视觉验收。
|
// 保护布局契约;实际像素对齐仍需浏览器视觉验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.n-checkbox\.control-row-checkbox\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*34px;[^}]*padding-block:\s*0/
|
/\.n-checkbox\.control-row-checkbox\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*34px;[^}]*padding-block:\s*0/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.app-dialog\s*\{[^}]*--app-field:\s*var\(--app-control\)/)
|
expect(css).toMatch(/\.app-dialog\s*\{[^}]*--app-field:\s*var\(--app-control\)/)
|
||||||
expect(readFileSync('src/styles.css', 'utf8')).toMatch(/\.panel\s*\{[^}]*--app-field:\s*var\(--app-control\)/)
|
expect(readFileSync('src/styles/styles.css', 'utf8')).toMatch(
|
||||||
|
/\.panel\s*\{[^}]*--app-field:\s*var\(--app-control\)/
|
||||||
|
)
|
||||||
for (const path of [
|
for (const path of [
|
||||||
'production/ProductionPage.vue',
|
'production/ProductionPage.vue',
|
||||||
'storyboard/StoryboardPage.vue',
|
'storyboard/StoryboardPage.vue',
|
||||||
+8
-7
@@ -1,8 +1,8 @@
|
|||||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { readFileSync } from 'node:fs'
|
import BeatShotDirectory from '@/features/storyboard/components/BeatShotDirectory.vue'
|
||||||
import BeatShotDirectory from './components/BeatShotDirectory.vue'
|
import { readAllStyles } from '@/testing/styles'
|
||||||
import { designedShot } from './testing/fixtures'
|
import { designedShot } from '@/features/storyboard/testing/fixtures'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => wrapper?.unmount())
|
afterEach(() => wrapper?.unmount())
|
||||||
@@ -50,13 +50,14 @@ describe('Beat 两级镜头目录', () => {
|
|||||||
|
|
||||||
it('组内吸顶与移动端归属提示分别保护,不在桌面重复展示 Beat', () => {
|
it('组内吸顶与移动端归属提示分别保护,不在桌面重复展示 Beat', () => {
|
||||||
// happy-dom 不计算吸顶位置,保护 CSS 边界,实际滚动仍需浏览器验收。
|
// happy-dom 不计算吸顶位置,保护 CSS 边界,实际滚动仍需浏览器验收。
|
||||||
const css = readFileSync('src/admin.css', 'utf8')
|
const css = readAllStyles()
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.directory-group-heading\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*background:\s*var\(--app-control\)/
|
/\.directory-group-heading\s*\{[^}]*position:\s*sticky;[^}]*top:\s*0;[^}]*background:\s*var\(--app-control\)/
|
||||||
)
|
)
|
||||||
expect(css).toMatch(/\.directory-mobile-beat\s*\{\s*display:\s*none/)
|
expect(css).toMatch(/\.directory-mobile-beat\s*\{\s*display:\s*none/)
|
||||||
const mobile = css.slice(css.lastIndexOf('@media (max-width: 760px)'))
|
expect(css).toMatch(/@media \(max-width: 760px\)[\s\S]*\.directory-mobile-beat\s*\{\s*display:\s*inline/)
|
||||||
expect(mobile).toMatch(/\.directory-mobile-beat\s*\{\s*display:\s*inline/)
|
expect(css).toMatch(
|
||||||
expect(mobile).toMatch(/\.beat-directory-group,\s*\.beat-directory-items\s*\{\s*display:\s*contents/)
|
/@media \(max-width: 760px\)[\s\S]*\.beat-directory-group,\s*\.beat-directory-items\s*\{\s*display:\s*contents/
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { directionLabel, mergeDesignedShots, referenceImageUrl, storyboardPrerequisites } from './model'
|
import {
|
||||||
import { directionsResult, episodeShots, storyboardCheckpoint, visualStatesResult } from './testing/fixtures'
|
directionLabel,
|
||||||
|
mergeDesignedShots,
|
||||||
|
referenceImageUrl,
|
||||||
|
storyboardPrerequisites
|
||||||
|
} from '@/features/storyboard/model'
|
||||||
|
import {
|
||||||
|
directionsResult,
|
||||||
|
episodeShots,
|
||||||
|
storyboardCheckpoint,
|
||||||
|
visualStatesResult
|
||||||
|
} from '@/features/storyboard/testing/fixtures'
|
||||||
|
|
||||||
afterEach(() => vi.unstubAllEnvs())
|
afterEach(() => vi.unstubAllEnvs())
|
||||||
|
|
||||||
+9
-9
@@ -1,11 +1,11 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NImage, NScrollbar } from 'naive-ui'
|
import { NImage, NScrollbar } from 'naive-ui'
|
||||||
import { AssetImage } from '../../components/ui'
|
import { AssetImage } from '@/components/ui'
|
||||||
import IdentityImageDialog from './components/IdentityImageDialog.vue'
|
import IdentityImageDialog from '@/features/subject-identity/components/IdentityImageDialog.vue'
|
||||||
import IdentityGallery from './components/IdentityGallery.vue'
|
import IdentityGallery from '@/features/subject-identity/components/IdentityGallery.vue'
|
||||||
import CastingCandidateDialog from './components/CastingCandidateDialog.vue'
|
import CastingCandidateDialog from '@/features/subject-identity/components/CastingCandidateDialog.vue'
|
||||||
import { subjectIdentityApi } from './api'
|
import { subjectIdentityApi } from '@/features/subject-identity/api'
|
||||||
import {
|
import {
|
||||||
canBeAnchor,
|
canBeAnchor,
|
||||||
castingStatusLabel,
|
castingStatusLabel,
|
||||||
@@ -13,10 +13,10 @@ import {
|
|||||||
groupIdentitySubjects,
|
groupIdentitySubjects,
|
||||||
mergeCastingSubjects,
|
mergeCastingSubjects,
|
||||||
readImageProvenance
|
readImageProvenance
|
||||||
} from './model'
|
} from '@/features/subject-identity/model'
|
||||||
import { identityImageFixture } from './testing/fixtures'
|
import { identityImageFixture } from '@/features/subject-identity/testing/fixtures'
|
||||||
import { formFixture } from '../subject-images/testing/fixtures'
|
import { formFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
import { expandSections, selectControl } from '../../testing/naive'
|
import { expandSections, selectControl } from '@/testing/naive'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NImage } from 'naive-ui'
|
import { NImage } from 'naive-ui'
|
||||||
import IdentityThumbnail from './components/IdentityThumbnail.vue'
|
import IdentityThumbnail from '@/features/subject-identity/components/IdentityThumbnail.vue'
|
||||||
import { identityFixture, identityImageFixture } from './testing/fixtures'
|
import { identityFixture, identityImageFixture } from '@/features/subject-identity/testing/fixtures'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
let observers: {
|
let observers: {
|
||||||
+10
-10
@@ -2,16 +2,16 @@ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NSelect } from 'naive-ui'
|
import { NSelect } from 'naive-ui'
|
||||||
import { testProjectContext } from '../../testing/project-context'
|
import { testProjectContext } from '@/testing/project-context'
|
||||||
import { projectContextKey } from '../projects/context'
|
import { projectContextKey } from '@/features/projects/context'
|
||||||
import { formFixture, imageFixture } from './testing/fixtures'
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
import { referenceLocation, referenceTargets } from '../production/asset-links'
|
import { referenceLocation, referenceTargets } from '@/features/production/asset-links'
|
||||||
import StaleKeyframeNotice from '../production/components/StaleKeyframeNotice.vue'
|
import StaleKeyframeNotice from '@/features/production/components/StaleKeyframeNotice.vue'
|
||||||
import SubjectImagesPage from './SubjectImagesPage.vue'
|
import SubjectImagesPage from '@/features/subject-images/SubjectImagesPage.vue'
|
||||||
import ProductionPage from '../production/ProductionPage.vue'
|
import ProductionPage from '@/features/production/ProductionPage.vue'
|
||||||
import { directionsResult, storyboardCheckpoint } from '../storyboard/testing/fixtures'
|
import { directionsResult, storyboardCheckpoint } from '@/features/storyboard/testing/fixtures'
|
||||||
import type { ProductionIssue } from '../production/types'
|
import type { ProductionIssue } from '@/features/production/types'
|
||||||
import type { ShotReferences } from '../storyboard/types'
|
import type { ShotReferences } from '@/features/storyboard/types'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { AssetImage } from '../../components/ui'
|
import { AssetImage } from '@/components/ui'
|
||||||
import { getOperation } from '../workflows/operations'
|
import { getOperation } from '@/features/workflows/operations'
|
||||||
import GenerateImageDialog from './components/GenerateImageDialog.vue'
|
import GenerateImageDialog from '@/features/subject-images/components/GenerateImageDialog.vue'
|
||||||
import ImageGalleryDialog from './components/ImageGalleryDialog.vue'
|
import ImageGalleryDialog from '@/features/subject-images/components/ImageGalleryDialog.vue'
|
||||||
import { coverImage, hasRunningImages, primaryImage, validImageSize } from './model'
|
import { coverImage, hasRunningImages, primaryImage, validImageSize } from '@/features/subject-images/model'
|
||||||
import { formFixture, imageFixture } from './testing/fixtures'
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
import { expandSections } from '../../testing/naive'
|
import { expandSections } from '@/testing/naive'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
/** Naive 将弹窗挂载到 body,需要从实际弹窗找到按钮。 */
|
/** Naive 将弹窗挂载到 body,需要从实际弹窗找到按钮。 */
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { formCoverAspectRatio } from './layout'
|
import { formCoverAspectRatio } from '@/features/subject-images/layout'
|
||||||
import { formFixture, imageFixture } from './testing/fixtures'
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
|
|
||||||
describe('图库瀑布流图片比例', () => {
|
describe('图库瀑布流图片比例', () => {
|
||||||
it.each([
|
it.each([
|
||||||
+8
-8
@@ -1,14 +1,14 @@
|
|||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { projectContextKey } from '../projects/context'
|
import { projectContextKey } from '@/features/projects/context'
|
||||||
import { testProjectContext } from '../../testing/project-context'
|
import { testProjectContext } from '@/testing/project-context'
|
||||||
import { getOperation } from '../workflows/operations'
|
import { getOperation } from '@/features/workflows/operations'
|
||||||
import { formFixture, imageFixture } from './testing/fixtures'
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
import { currentIdentityAnchorId, getImageSession, isPrimaryIdentityStale } from './model'
|
import { currentIdentityAnchorId, getImageSession, isPrimaryIdentityStale } from '@/features/subject-images/model'
|
||||||
import { useSubjectImages } from './useSubjectImages'
|
import { useSubjectImages } from '@/features/subject-images/useSubjectImages'
|
||||||
import FormPromptDialog from './components/FormPromptDialog.vue'
|
import FormPromptDialog from '@/features/subject-images/components/FormPromptDialog.vue'
|
||||||
import GenerateImageDialog from './components/GenerateImageDialog.vue'
|
import GenerateImageDialog from '@/features/subject-images/components/GenerateImageDialog.vue'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
const projectId = 'capability-test'
|
const projectId = 'capability-test'
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import WorkflowDiagnosticsDialog from './WorkflowDiagnosticsDialog.vue'
|
import WorkflowDiagnosticsDialog from '@/features/workflows/WorkflowDiagnosticsDialog.vue'
|
||||||
import { loadWorkflowDiagnostics, type WorkflowTimelineGroup } from './diagnostics'
|
import { loadWorkflowDiagnostics, type WorkflowTimelineGroup } from '@/features/workflows/diagnostics'
|
||||||
import type { Checkpoint } from './types'
|
import type { Checkpoint } from '@/features/workflows/types'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { getOperation, runOperation } from './operations'
|
import { getOperation, runOperation } from '@/features/workflows/operations'
|
||||||
|
|
||||||
describe('项目级长请求互斥', () => {
|
describe('项目级长请求互斥', () => {
|
||||||
it('同一项目不同时执行两个 graph,错误不伪装成功', async () => {
|
it('同一项目不同时执行两个 graph,错误不伪装成功', async () => {
|
||||||
@@ -3,34 +3,34 @@ import { NImage, NRadioGroup, NScrollbar } from 'naive-ui'
|
|||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import CreateProjectDialog from '../projects/components/CreateProjectDialog.vue'
|
import CreateProjectDialog from '@/features/projects/components/CreateProjectDialog.vue'
|
||||||
import BreakdownPage from '../breakdown/BreakdownPage.vue'
|
import BreakdownPage from '@/features/breakdown/BreakdownPage.vue'
|
||||||
import CreateDramaPage from '../create-drama/CreateDramaPage.vue'
|
import CreateDramaPage from '@/features/create-drama/CreateDramaPage.vue'
|
||||||
import ProjectsPage from '../projects/ProjectsPage.vue'
|
import ProjectsPage from '@/features/projects/ProjectsPage.vue'
|
||||||
import { projectContextKey, type useProjectData } from '../projects/context'
|
import { projectContextKey, type useProjectData } from '@/features/projects/context'
|
||||||
import type { ProjectDetail } from '../projects/types'
|
import type { ProjectDetail } from '@/features/projects/types'
|
||||||
import type { Checkpoint } from './types'
|
import type { Checkpoint } from '@/features/workflows/types'
|
||||||
import StoryboardPage from '../storyboard/StoryboardPage.vue'
|
import StoryboardPage from '@/features/storyboard/StoryboardPage.vue'
|
||||||
import ShotTools from '../storyboard/components/ShotTools.vue'
|
import ShotTools from '@/features/storyboard/components/ShotTools.vue'
|
||||||
import {
|
import {
|
||||||
designedShot,
|
designedShot,
|
||||||
directionsResult,
|
directionsResult,
|
||||||
storyboardCheckpoint,
|
storyboardCheckpoint,
|
||||||
visualStatesResult
|
visualStatesResult
|
||||||
} from '../storyboard/testing/fixtures'
|
} from '@/features/storyboard/testing/fixtures'
|
||||||
import { getStoryboardSession } from '../storyboard/model'
|
import { getStoryboardSession } from '@/features/storyboard/model'
|
||||||
import { getOperation } from './operations'
|
import { getOperation } from '@/features/workflows/operations'
|
||||||
import SubjectImagesPage from '../subject-images/SubjectImagesPage.vue'
|
import SubjectImagesPage from '@/features/subject-images/SubjectImagesPage.vue'
|
||||||
import { formFixture, imageFixture } from '../subject-images/testing/fixtures'
|
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||||
import { getImageSession } from '../subject-images/model'
|
import { getImageSession } from '@/features/subject-images/model'
|
||||||
import VisualStylePage from '../visual-style/VisualStylePage.vue'
|
import VisualStylePage from '@/features/visual-style/VisualStylePage.vue'
|
||||||
import SubjectIdentityPage from '../subject-identity/SubjectIdentityPage.vue'
|
import SubjectIdentityPage from '@/features/subject-identity/SubjectIdentityPage.vue'
|
||||||
import { styleFixture, styleImageFixture } from '../visual-style/testing/fixtures'
|
import { styleFixture, styleImageFixture } from '@/features/visual-style/testing/fixtures'
|
||||||
import { identityFixture, identityImageFixture } from '../subject-identity/testing/fixtures'
|
import { identityFixture, identityImageFixture } from '@/features/subject-identity/testing/fixtures'
|
||||||
import { getIdentitySession } from '../subject-identity/model'
|
import { getIdentitySession } from '@/features/subject-identity/model'
|
||||||
import { drawerPanel, expandSections, selectControl, selectMenu } from '../../testing/naive'
|
import { drawerPanel, expandSections, selectControl, selectMenu } from '@/testing/naive'
|
||||||
import ProductionPage from '../production/ProductionPage.vue'
|
import ProductionPage from '@/features/production/ProductionPage.vue'
|
||||||
import { getProductionSession } from '../production/model'
|
import { getProductionSession } from '@/features/production/model'
|
||||||
|
|
||||||
/** 新资产页面复用已有项目上下文和内存路由,不接入真实模型。 */
|
/** 新资产页面复用已有项目上下文和内存路由,不接入真实模型。 */
|
||||||
async function mountAssets(page: 'style' | 'identity', query = '') {
|
async function mountAssets(page: 'style' | 'identity', query = '') {
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { breakdownSnapshot, recoveryOptions, workflowCheckpoints } from './selectors'
|
import { breakdownSnapshot, recoveryOptions, workflowCheckpoints } from '@/features/workflows/selectors'
|
||||||
import type { Checkpoint } from './types'
|
import type { Checkpoint } from '@/features/workflows/types'
|
||||||
import type { BreakdownState } from '../breakdown/types'
|
import type { BreakdownState } from '@/features/breakdown/types'
|
||||||
|
|
||||||
/** 只构造测试所需的真实 checkpoint 字段,避免页面依赖虚构 DTO。 */
|
/** 只构造测试所需的真实 checkpoint 字段,避免页面依赖虚构 DTO。 */
|
||||||
function checkpoint(index: number, state: BreakdownState, workflowName = 'breakdown'): Checkpoint {
|
function checkpoint(index: number, state: BreakdownState, workflowName = 'breakdown'): Checkpoint {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { ApiError, optionalResource, request } from './http'
|
import { ApiError, optionalResource, request } from '@/lib/http'
|
||||||
import { projectsApi } from '../features/projects/api'
|
import { projectsApi } from '@/features/projects/api'
|
||||||
import { breakdownApi } from '../features/breakdown/api'
|
import { breakdownApi } from '@/features/breakdown/api'
|
||||||
import { storyboardApi } from '../features/storyboard/api'
|
import { storyboardApi } from '@/features/storyboard/api'
|
||||||
import { subjectImagesApi } from '../features/subject-images/api'
|
import { subjectImagesApi } from '@/features/subject-images/api'
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
+4
-1
@@ -13,7 +13,10 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
"types": ["vite/client", "node"]
|
"types": ["vite/client", "node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.vue", "*.config.ts"]
|
"include": ["src/**/*.ts", "src/**/*.vue", "tests/**/*.ts", "*.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
@@ -7,6 +8,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
return {
|
return {
|
||||||
plugins: [vue(), tailwindcss()],
|
plugins: [vue(), tailwindcss()],
|
||||||
|
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,10 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
/** 使用轻量 DOM 测试 API、异步状态和 Vue 页面行为。 */
|
/** 使用轻量 DOM 测试 API、异步状态和 Vue 页面行为。 */
|
||||||
export default defineConfig({ plugins: [vue()], test: { environment: 'happy-dom', restoreMocks: true } })
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
|
||||||
|
test: { environment: 'happy-dom', restoreMocks: true }
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user