feat: 收口移动端侧栏与创作拆解页布局

This commit is contained in:
GJ
2026-09-04 19:41:17 +08:00
parent c483b1912e
commit 542ec296c7
12 changed files with 250 additions and 49 deletions
+45 -26
View File
@@ -38,10 +38,12 @@ const NARROW_NAV = '(max-width: 800px)'
const SIDER_COLLAPSED_WIDTH = 64 const SIDER_COLLAPSED_WIDTH = 64
/** 展开侧栏宽度,窄屏裁切动画以内层保持该宽度。 */ /** 展开侧栏宽度,窄屏裁切动画以内层保持该宽度。 */
const SIDER_EXPANDED_WIDTH = 208 const SIDER_EXPANDED_WIDTH = 208
/** 窄屏折叠侧栏宽度,给正文多留空间,仍覆盖 44px 点击区。 */
const SIDER_COLLAPSED_WIDTH_NARROW = 48
/** 窄屏展开侧栏宽度。 */
const SIDER_EXPANDED_WIDTH_NARROW = 168
/** 菜单图标尺寸,折叠/展开共用,避免切换时缩放。 */ /** 菜单图标尺寸,折叠/展开共用,避免切换时缩放。 */
const NAV_ICON_SIZE = 20 const NAV_ICON_SIZE = 20
/** 折叠与展开共用的图标左偏移,避免切换时图标跳动。 */
const NAV_ICON_INDENT = SIDER_COLLAPSED_WIDTH / 2 - NAV_ICON_SIZE / 2
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */ /** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
const route = useRoute() const route = useRoute()
@@ -51,6 +53,12 @@ const collapsed = ref(narrow.value)
const settingsOpen = ref(false) const settingsOpen = ref(false)
/** 窄屏折叠只裁切文字,不把菜单收成图标,避免推挤动画时错位。 */ /** 窄屏折叠只裁切文字,不把菜单收成图标,避免推挤动画时错位。 */
const menuCollapsed = computed(() => !narrow.value && collapsed.value) const menuCollapsed = computed(() => !narrow.value && collapsed.value)
/** 当前视口下的折叠侧栏宽度。 */
const siderCollapsedWidth = computed(() => (narrow.value ? SIDER_COLLAPSED_WIDTH_NARROW : SIDER_COLLAPSED_WIDTH))
/** 当前视口下的展开侧栏宽度。 */
const siderExpandedWidth = computed(() => (narrow.value ? SIDER_EXPANDED_WIDTH_NARROW : SIDER_EXPANDED_WIDTH))
/** 折叠与展开共用的图标左偏移,避免切换时图标跳动。 */
const navIconIndent = computed(() => siderCollapsedWidth.value / 2 - NAV_ICON_SIZE / 2)
/** 视口跨过断点时:进入窄屏收回侧栏。 */ /** 视口跨过断点时:进入窄屏收回侧栏。 */
function syncNarrow(event: MediaQueryListEvent) { function syncNarrow(event: MediaQueryListEvent) {
@@ -144,19 +152,25 @@ const menuOptions = computed<MenuOption[]>(() => [
<div <div
class="admin-layout" class="admin-layout"
:style="{ :style="{
'--admin-sider-collapsed': SIDER_COLLAPSED_WIDTH + 'px', '--admin-sider-collapsed': siderCollapsedWidth + 'px',
'--admin-sider-expanded': SIDER_EXPANDED_WIDTH + 'px' '--admin-sider-expanded': siderExpandedWidth + 'px'
}" }"
> >
<!-- 以下是主导航侧栏宽屏与窄屏共用同一份 --> <!-- 以下是主导航侧栏宽屏与窄屏共用同一份 -->
<aside <aside
class="admin-sider" class="admin-sider"
:class="{ 'is-collapsed': collapsed }" :class="{ 'is-collapsed': collapsed }"
:style="{ width: (collapsed ? SIDER_COLLAPSED_WIDTH : SIDER_EXPANDED_WIDTH) + 'px' }" :style="{ width: (collapsed ? siderCollapsedWidth : siderExpandedWidth) + 'px' }"
aria-label="主导航" aria-label="主导航"
> >
<RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页"> <!-- 以下是侧栏品牌折叠仅图标展开图标加文字 -->
<Clapperboard :size="24" /><span v-if="!menuCollapsed" <RouterLink
to="/projects"
class="admin-brand"
:class="{ 'is-collapsed': collapsed }"
aria-label="短剧工作台首页"
>
<Clapperboard :size="narrow ? NAV_ICON_SIZE : 24" /><span v-if="!collapsed"
>短剧工作台<small>SHORT DRAMA STUDIO</small></span >短剧工作台<small>SHORT DRAMA STUDIO</small></span
> >
</RouterLink> </RouterLink>
@@ -165,13 +179,14 @@ const menuOptions = computed<MenuOption[]>(() => [
:value="route.path" :value="route.path"
:options="menuOptions" :options="menuOptions"
:collapsed="menuCollapsed" :collapsed="menuCollapsed"
:collapsed-width="SIDER_COLLAPSED_WIDTH" :collapsed-width="siderCollapsedWidth"
:icon-size="NAV_ICON_SIZE" :icon-size="NAV_ICON_SIZE"
:collapsed-icon-size="NAV_ICON_SIZE" :collapsed-icon-size="NAV_ICON_SIZE"
:indent="NAV_ICON_INDENT" :indent="navIconIndent"
:root-indent="NAV_ICON_INDENT" :root-indent="navIconIndent"
/> />
</NScrollbar> </NScrollbar>
<!-- 以下是侧栏底部后端连接折叠仅图标展开图标加文字 -->
<footer class="admin-sider-footer"> <footer class="admin-sider-footer">
<NTooltip placement="right" :disabled="!collapsed"> <NTooltip placement="right" :disabled="!collapsed">
<template #trigger> <template #trigger>
@@ -179,12 +194,12 @@ const menuOptions = computed<MenuOption[]>(() => [
quaternary quaternary
block block
class="admin-settings-button" class="admin-settings-button"
:class="{ 'is-collapsed': menuCollapsed }" :class="{ 'is-collapsed': collapsed }"
aria-label="后端连接" aria-label="后端连接"
@click="openSettings" @click="openSettings"
> >
<template #icon><Settings2 :size="18" /></template> <template #icon><Settings2 :size="18" /></template>
<span v-if="!menuCollapsed">后端连接</span> <span v-if="!collapsed">后端连接</span>
</NButton> </NButton>
</template> </template>
后端连接 后端连接
@@ -265,11 +280,14 @@ const menuOptions = computed<MenuOption[]>(() => [
@apply flex-1 min-h-0; @apply flex-1 min-h-0;
} }
.admin-sider-footer { .admin-sider-footer {
@apply shrink-0; @apply w-full shrink-0 overflow-hidden;
padding-block: 8px max(8px, env(safe-area-inset-bottom)); padding-block: 8px max(8px, env(safe-area-inset-bottom));
} }
.n-button.admin-settings-button { .n-button.admin-settings-button {
@apply justify-start h-11 px-5; @apply justify-start h-11 w-full px-5;
}
.n-button.admin-settings-button.is-collapsed {
@apply justify-center px-0;
} }
.admin-sider .n-menu .n-menu-item-content::before { .admin-sider .n-menu .n-menu-item-content::before {
@apply left-0 right-0 rounded-none; @apply left-0 right-0 rounded-none;
@@ -277,6 +295,9 @@ const menuOptions = computed<MenuOption[]>(() => [
.admin-brand { .admin-brand {
@apply flex shrink-0 items-center gap-3 h-[74px] py-0 px-5 text-ink whitespace-nowrap; @apply flex shrink-0 items-center gap-3 h-[74px] py-0 px-5 text-ink whitespace-nowrap;
} }
.admin-brand.is-collapsed {
@apply justify-center px-0;
}
.admin-brand > svg { .admin-brand > svg {
@apply shrink-0; @apply shrink-0;
} }
@@ -307,20 +328,20 @@ const menuOptions = computed<MenuOption[]>(() => [
min-width: calc(100% - var(--admin-sider-collapsed)); min-width: calc(100% - var(--admin-sider-collapsed));
max-width: calc(100% - var(--admin-sider-collapsed)); max-width: calc(100% - var(--admin-sider-collapsed));
} }
.admin-brand, .admin-nav-scroll.n-scrollbar {
.admin-nav-scroll.n-scrollbar,
.admin-sider-footer {
min-width: var(--admin-sider-expanded); min-width: var(--admin-sider-expanded);
} }
.admin-brand span, .admin-brand {
.admin-sider .n-menu-item-content-header, @apply h-12 w-full overflow-hidden;
.admin-settings-button span { }
.admin-sider:not(.is-collapsed) .admin-brand {
@apply justify-start px-3.5;
}
.admin-sider .n-menu-item-content-header {
opacity: 1; opacity: 1;
transition: opacity 0.2s ease; 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 .n-menu-item-content-header,
.admin-sider.is-collapsed .admin-settings-button span {
opacity: 0; opacity: 0;
} }
.admin-topbar { .admin-topbar {
@@ -329,9 +350,7 @@ const menuOptions = computed<MenuOption[]>(() => [
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.admin-sider, .admin-sider,
.admin-brand span, .admin-sider .n-menu-item-content-header {
.admin-sider .n-menu-item-content-header,
.admin-settings-button span {
transition: none; transition: none;
} }
} }
+3 -1
View File
@@ -11,12 +11,14 @@ const open = defineModel<boolean>('open', { default: false })
<NButton <NButton
class="workspace-tools-trigger" class="workspace-tools-trigger"
data-workspace-tools data-workspace-tools
:aria-label="label"
aria-haspopup="dialog" aria-haspopup="dialog"
:aria-expanded="open" :aria-expanded="open"
@click="open = true" @click="open = true"
> >
<template #icon><Settings2 :size="15" /></template> <template #icon><Settings2 :size="15" /></template>
{{ label }}<span v-if="hasReceipt" class="text-xs text-muted"> · 有回执</span> <span class="workspace-tools-label">{{ label }}</span
><span v-if="hasReceipt" class="text-xs text-muted"> · 有回执</span>
</NButton> </NButton>
<NDrawer <NDrawer
v-model:show="open" v-model:show="open"
+74 -6
View File
@@ -137,6 +137,7 @@ function exportResult() {
<template> <template>
<WorkspacePage split compact class="breakdown-workspace-page"> <WorkspacePage split compact class="breakdown-workspace-page">
<template #header> <template #header>
<!-- 以下是拆解状态与设置 -->
<div class="workspace-toolbar"> <div class="workspace-toolbar">
<h2 class="font-semibold">剧本拆解</h2> <h2 class="font-semibold">剧本拆解</h2>
<div v-if="snapshot" class="toolbar-summary"> <div v-if="snapshot" class="toolbar-summary">
@@ -144,7 +145,10 @@ function exportResult() {
>抽取 {{ summary.completed }} / {{ summary.total }} · 失败 {{ summary.failed }}</span >抽取 {{ summary.completed }} / {{ summary.total }} · 失败 {{ summary.failed }}</span
> >
<span>主体 {{ subjects.length }} · 分镜剧集 {{ shots.length }} / {{ plans.length }}</span> <span>主体 {{ subjects.length }} · 分镜剧集 {{ shots.length }} / {{ plans.length }}</span>
</div>
<div v-if="snapshot || project?.episodes.length" class="toolbar-actions">
<StatusBadge <StatusBadge
v-if="snapshot"
:status="execution?.status || 'unknown'" :status="execution?.status || 'unknown'"
:label=" :label="
execution?.status === 'completed' execution?.status === 'completed'
@@ -154,9 +158,12 @@ function exportResult() {
: '执行状态待确认' : '执行状态待确认'
" "
/> />
</div> <WorkspaceTools
<div v-if="project?.episodes.length" class="toolbar-actions"> v-if="project?.episodes.length"
<WorkspaceTools v-model:open="toolsOpen" title="拆解设置与恢复" label="拆解设置与恢复"> v-model:open="toolsOpen"
title="拆解设置与恢复"
label="拆解设置与恢复"
>
<section class="panel breakdown-settings-panel p-5 lg:p-6"> <section class="panel breakdown-settings-panel p-5 lg:p-6">
<div class="mb-5 flex flex-wrap items-start justify-between gap-3"> <div class="mb-5 flex flex-wrap items-start justify-between gap-3">
<div> <div>
@@ -491,6 +498,7 @@ function exportResult() {
:project-id="project?.id" :project-id="project?.id"
:checkpoints="checkpoints" :checkpoints="checkpoints"
workflow="breakdown" workflow="breakdown"
@close="showHistory = false"
/> />
</div> </div>
</section> </section>
@@ -583,9 +591,69 @@ function exportResult() {
.breakdown-results-section > .content-with-history.history-hidden { .breakdown-results-section > .content-with-history.history-hidden {
@apply grid-cols-[minmax(0,_1fr)] grid-rows-[minmax(0,_1fr)]; @apply grid-cols-[minmax(0,_1fr)] grid-rows-[minmax(0,_1fr)];
} }
@media (max-width: 760px) { @media (max-width: 800px) {
.breakdown-results-section > .content-with-history { .breakdown-workspace-page .workspace-heading {
@apply grid-rows-[minmax(0,_1fr)_min(25%,_130px)]; @apply py-2 px-3;
}
.breakdown-workspace-page .workspace-toolbar {
@apply min-h-0 flex-nowrap gap-2;
}
.breakdown-workspace-page .workspace-toolbar h2 {
@apply hidden;
}
.breakdown-workspace-page .toolbar-summary {
@apply min-w-0 flex-1 flex-nowrap overflow-x-auto overflow-y-hidden overscroll-x-contain;
scrollbar-width: none;
mask-image: linear-gradient(to right, #000 calc(100% - 12px), transparent);
}
.breakdown-workspace-page .toolbar-summary::-webkit-scrollbar {
display: none;
}
.breakdown-workspace-page .toolbar-summary > * {
@apply shrink-0;
}
.breakdown-workspace-page .toolbar-actions {
@apply ml-0 shrink-0;
}
.breakdown-workspace-page .toolbar-actions > * {
@apply shrink-0;
}
.breakdown-workspace-page .toolbar-actions .n-tag {
@apply hidden;
}
.breakdown-workspace-page .workspace-tools-label {
@apply hidden;
}
.breakdown-workspace-page .workspace-tools-trigger {
@apply h-8 w-8 p-0;
min-width: 32px;
}
.breakdown-workspace-page .workspace-tools-trigger .n-button__icon {
@apply m-0;
}
.breakdown-results-section .tabs-toolbar {
container-type: normal;
}
.breakdown-results-section .workspace-tabs > .n-tabs-nav {
@apply flex-nowrap;
}
.breakdown-results-section .workspace-tabs .n-tabs-nav-scroll-wrapper {
@apply min-w-0 flex-1;
flex-basis: auto;
}
.breakdown-results-section .workspace-tabs .n-tabs-nav__suffix {
@apply w-auto pl-2;
}
.breakdown-results-section .tabs-toolbar-actions {
@apply min-h-0 flex-nowrap;
}
.breakdown-results-section .content-with-history:not(.history-hidden) {
@apply relative;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
}
.breakdown-results-section .content-with-history:not(.history-hidden) .history-panel {
@apply absolute inset-0 z-30;
} }
} }
</style> </style>
@@ -24,7 +24,8 @@ const filtered = computed(() =>
</script> </script>
<template> <template>
<div class="p-5 lg:p-7"> <div class="subject-list">
<!-- 以下是主体搜索 -->
<div class="subject-list-toolbar"> <div class="subject-list-toolbar">
<NInput <NInput
class="subject-list-search" class="subject-list-search"
@@ -139,6 +140,14 @@ const filtered = computed(() =>
.subject-details[open] summary svg { .subject-details[open] summary svg {
@apply rotate-180; @apply rotate-180;
} }
.subject-list {
@apply p-5;
}
@media (min-width: 1024px) {
.subject-list {
@apply p-7;
}
}
.subject-list-toolbar { .subject-list-toolbar {
@apply flex flex-wrap items-center justify-start gap-y-2.5 gap-x-3 mb-5; @apply flex flex-wrap items-center justify-start gap-y-2.5 gap-x-3 mb-5;
} }
@@ -148,4 +157,15 @@ const filtered = computed(() =>
.subject-list-count { .subject-list-count {
@apply shrink-0 text-muted text-xs whitespace-nowrap; @apply shrink-0 text-muted text-xs whitespace-nowrap;
} }
@media (max-width: 800px) {
.subject-list {
@apply p-3;
}
.subject-list-toolbar {
@apply flex-nowrap gap-2 mb-3;
}
.subject-list-search.n-input {
@apply flex-1 max-w-none;
}
}
</style> </style>
+13 -1
View File
@@ -137,7 +137,7 @@ function exportScript() {
/> />
<RouterLink <RouterLink
v-if="complete" v-if="complete"
class="text-button text-accent" class="text-button text-accent script-next-link"
:to="`/projects/${project?.id}/breakdown`" :to="`/projects/${project?.id}/breakdown`"
>进入拆解<ArrowRight :size="14" >进入拆解<ArrowRight :size="14"
/></RouterLink> /></RouterLink>
@@ -248,6 +248,7 @@ function exportScript() {
:project-id="project?.id" :project-id="project?.id"
:checkpoints="checkpoints" :checkpoints="checkpoints"
workflow="create-drama" workflow="create-drama"
@close="showHistory = false"
/> />
</div></div </div></div
></WorkspacePage> ></WorkspacePage>
@@ -308,6 +309,17 @@ function exportScript() {
.script-section .tabs-toolbar-actions { .script-section .tabs-toolbar-actions {
@apply min-h-0 flex-nowrap; @apply min-h-0 flex-nowrap;
} }
.script-section .tabs-toolbar-actions .script-next-link {
@apply hidden;
}
.script-section .content-with-history:not(.history-hidden) {
@apply relative;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
}
.script-section .content-with-history:not(.history-hidden) .history-panel {
@apply absolute inset-0 z-30;
}
} }
.script-section { .script-section {
@apply flex-1 flex flex-col min-h-0; @apply flex-1 flex flex-col min-h-0;
+16 -4
View File
@@ -254,15 +254,27 @@ onBeforeUnmount(() => observer?.disconnect())
@apply grid-cols-[180px_minmax(0,_1fr)]; @apply grid-cols-[180px_minmax(0,_1fr)];
} }
} }
@media (max-width: 600px) { @media (max-width: 800px) {
.episode-reader { .episode-reader {
@apply grid-cols-[112px_minmax(0,_1fr)]; @apply grid-cols-[72px_minmax(0,_1fr)];
} }
.reader-directory-heading { .reader-directory-heading {
@apply pt-4 px-3 pb-3 flex-wrap; @apply flex-col items-start gap-0 pt-2.5 px-1.5 pb-2;
}
.reader-directory-heading h3 {
@apply hidden;
} }
.n-button.reader-episode-link { .n-button.reader-episode-link {
@apply py-3 px-2.5; @apply min-h-11 py-2 px-1;
}
.n-button.reader-episode-link .n-button__content {
@apply items-center text-center;
}
.reader-episode-title {
@apply hidden;
}
.reader-episode-label {
@apply text-[11px];
} }
.reader-content { .reader-content {
@apply px-[18px]; @apply px-[18px];
+19 -3
View File
@@ -9,6 +9,10 @@ import WorkflowDiagnosticsDialog from './WorkflowDiagnosticsDialog.vue'
/** 时间线只说明 checkpoint 已保存,不把每条记录误标为工作流成功。 */ /** 时间线只说明 checkpoint 已保存,不把每条记录误标为工作流成功。 */
const props = defineProps<{ checkpoints: Checkpoint[]; workflow: string; projectId?: string }>() const props = defineProps<{ checkpoints: Checkpoint[]; workflow: string; projectId?: string }>()
const emit = defineEmits<{
/** 关闭覆盖层中的执行记录 */
close: []
}>()
const diagnosticsOpen = ref(false) const diagnosticsOpen = ref(false)
const history = computed(() => workflowCheckpoints(props.checkpoints, props.workflow).slice(-18).toReversed()) const history = computed(() => workflowCheckpoints(props.checkpoints, props.workflow).slice(-18).toReversed())
</script> </script>
@@ -16,7 +20,10 @@ const history = computed(() => workflowCheckpoints(props.checkpoints, props.work
<template> <template>
<aside class="history-panel"> <aside class="history-panel">
<NScrollbar class="panel-scroll" content-class="history-content"> <NScrollbar class="panel-scroll" content-class="history-content">
<h3 class="mb-1 flex items-center gap-2 text-sm font-semibold"><Clock3 :size="15" />执行记录</h3> <div class="history-heading">
<h3 class="mb-1 flex items-center gap-2 text-sm font-semibold"><Clock3 :size="15" />执行记录</h3>
<NButton class="history-close" text size="small" @click="emit('close')">关闭</NButton>
</div>
<p class="mb-6 text-xs leading-5 text-muted">最近 18 checkpoint · 自动刷新</p> <p class="mb-6 text-xs leading-5 text-muted">最近 18 checkpoint · 自动刷新</p>
<NButton v-if="projectId" text size="small" class="mb-4" @click="diagnosticsOpen = true" <NButton v-if="projectId" text size="small" class="mb-4" @click="diagnosticsOpen = true"
>查看完整运行诊断</NButton >查看完整运行诊断</NButton
@@ -72,9 +79,18 @@ const history = computed(() => workflowCheckpoints(props.checkpoints, props.work
.history-content { .history-content {
@apply py-[23px] px-[21px]; @apply py-[23px] px-[21px];
} }
@media (max-width: 760px) { .history-heading {
@apply flex items-start justify-between gap-3;
}
.history-close {
@apply hidden;
}
@media (max-width: 800px) {
.history-panel { .history-panel {
@apply overflow-hidden min-h-0; @apply overflow-hidden min-h-0 bg-(--app-subtle);
}
.history-close {
@apply inline-flex shrink-0;
} }
} }
</style> </style>
+6
View File
@@ -238,6 +238,12 @@ body {
height: 32px; height: 32px;
white-space: nowrap; white-space: nowrap;
} }
.tabs-toolbar-actions .n-button.icon-button {
width: 32px;
min-width: 32px;
height: 32px;
padding: 0;
}
/* 窄内容区允许 suffix 独占一行;Tabs 本身继续使用组件内置横向滚动。 */ /* 窄内容区允许 suffix 独占一行;Tabs 本身继续使用组件内置横向滚动。 */
@container tabs-toolbar (max-width: 600px) { @container tabs-toolbar (max-width: 600px) {
.workspace-tabs > .n-tabs-nav { .workspace-tabs > .n-tabs-nav {
+31 -4
View File
@@ -31,6 +31,9 @@ describe('管理后台组件边界', () => {
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;/)
expect(css).toMatch(
/\.tabs-toolbar-actions \.n-button\.icon-button\s*\{[^}]*width:\s*32px;[^}]*min-width:\s*32px;[^}]*height:\s*32px;[^}]*padding:\s*0/
)
expect(css).toContain('@container tabs-toolbar (max-width: 600px)') expect(css).toContain('@container tabs-toolbar (max-width: 600px)')
expect(css).not.toContain('.result-toolbar > .n-button') expect(css).not.toContain('.result-toolbar > .n-button')
}) })
@@ -82,7 +85,9 @@ describe('管理后台组件边界', () => {
expect(css).toMatch( expect(css).toMatch(
/\.breakdown-results-section > \.content-with-history\s*\{[^}]*flex:\s*1;[^}]*height:\s*auto;[^}]*min-height:\s*0;[^}]*grid-template-rows:\s*minmax\(0, 1fr\)/ /\.breakdown-results-section > \.content-with-history\s*\{[^}]*flex:\s*1;[^}]*height:\s*auto;[^}]*min-height:\s*0;[^}]*grid-template-rows:\s*minmax\(0, 1fr\)/
) )
expect(css).toContain('grid-template-rows: minmax(0, 1fr) min(25%, 130px)') expect(css).toMatch(
/@media \(max-width: 800px\)[\s\S]*?\.breakdown-results-section \.content-with-history:not\(\.history-hidden\) \.history-panel\s*\{[^}]*inset:\s*0/
)
expect(css).toMatch(/\.panel-scroll\.n-scrollbar\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0/) expect(css).toMatch(/\.panel-scroll\.n-scrollbar\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0/)
}) })
it('标题与内容使用各自滚动容器,不随正文一起滚动', () => { it('标题与内容使用各自滚动容器,不随正文一起滚动', () => {
@@ -173,6 +178,9 @@ describe('管理后台组件边界', () => {
expect(css).toMatch(/\.admin-sider\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*height:\s*100%/) expect(css).toMatch(/\.admin-sider\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*height:\s*100%/)
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(/\.n-button\.admin-settings-button\.is-collapsed\s*\{[^}]*justify-content:\s*center/)
expect(css).toMatch(/\.admin-brand\.is-collapsed\s*\{[^}]*justify-content:\s*center/)
expect(css).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.admin-brand\s*\{[^}]*height:\s*48px/)
expect(css).toContain('@media (max-width: 800px)') expect(css).toContain('@media (max-width: 800px)')
expect(css).toMatch(/transition:\s*width\s+0\.32s/) expect(css).toMatch(/transition:\s*width\s+0\.32s/)
expect(css).toContain('calc(100% - var(--admin-sider-collapsed))') expect(css).toContain('calc(100% - var(--admin-sider-collapsed))')
@@ -219,22 +227,41 @@ describe('管理后台组件边界', () => {
await flushPromises() await flushPromises()
expect(wrapper.findAll('.admin-sider')).toHaveLength(1) expect(wrapper.findAll('.admin-sider')).toHaveLength(1)
expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed') expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed')
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 64px') expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 48px')
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false) expect(wrapper.find('.admin-sider-mask').exists()).toBe(false)
expect(wrapper.find('.admin-nav-drawer').exists()).toBe(false) expect(wrapper.find('.admin-nav-drawer').exists()).toBe(false)
expect(wrapper.getComponent(NMenu).props()).toMatchObject({
collapsedWidth: 48,
indent: 14,
rootIndent: 14
})
const collapsedBrand = wrapper.get('.admin-brand')
expect(collapsedBrand.classes()).toContain('is-collapsed')
expect(collapsedBrand.text()).toBe('')
const collapsedSettings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
expect(collapsedSettings.text()).toBe('')
expect(collapsedSettings.classes()).toContain('is-collapsed')
await wrapper.get('[aria-label="展开侧栏"]').trigger('click') await wrapper.get('[aria-label="展开侧栏"]').trigger('click')
await flushPromises() await flushPromises()
expect(wrapper.findAll('.admin-sider')).toHaveLength(1) 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-collapsed')
expect(wrapper.get('.admin-sider').classes()).not.toContain('is-overlay') expect(wrapper.get('.admin-sider').classes()).not.toContain('is-overlay')
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 208px') expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 168px')
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false) 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('.admin-sider').findAll('.n-menu .n-menu-item')).toHaveLength(8)
expect(wrapper.get('[aria-label="折叠侧栏"]').attributes('aria-expanded')).toBe('true') expect(wrapper.get('[aria-label="折叠侧栏"]').attributes('aria-expanded')).toBe('true')
const expandedBrand = wrapper.get('.admin-brand')
expect(expandedBrand.classes()).not.toContain('is-collapsed')
expect(expandedBrand.text()).toContain('短剧工作台')
const expandedSettings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
expect(expandedSettings.text()).toBe('后端连接')
expect(expandedSettings.classes()).not.toContain('is-collapsed')
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click') await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
await flushPromises() await flushPromises()
expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed') expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed')
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 64px') expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 48px')
expect(wrapper.get('.admin-brand').text()).toBe('')
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('')
}) })
it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => { it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => {
+12 -2
View File
@@ -222,8 +222,8 @@ describe('拆解页内容滚动', () => {
expect(wrapper!.findAll('.subject-record-list > article')).toHaveLength(30) expect(wrapper!.findAll('.subject-record-list > article')).toHaveLength(30)
}) })
it('主体搜索与统计靠左居中,窄内容区可换行,输入框不再使用外置图标容器', () => { it('主体搜索与统计靠左居中,窄屏搜索拉满同一行,输入框不再使用外置图标容器', () => {
// DOM 环境不计算布局,保护输入框宽度、主题统计色与窄屏换行的样式契约。 // DOM 环境不计算布局,保护输入框宽度、主题统计色与窄屏单行搜索的样式契约。
const css = readAllStyles() 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;/
@@ -232,9 +232,19 @@ 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).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.subject-list-search\.n-input\s*\{[^}]*flex:\s*1/)
expect(css + readFileSync('src/styles/styles.css', 'utf8')).not.toContain('.search-field') expect(css + readFileSync('src/styles/styles.css', 'utf8')).not.toContain('.search-field')
}) })
it('窄屏收口拆解顶栏并隐藏重复标题,设置按钮保留图标', () => {
const css = readAllStyles()
expect(css).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.workspace-toolbar h2\s*\{[^}]*display:\s*none/)
expect(css).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.workspace-tools-label\s*\{[^}]*display:\s*none/)
expect(css).toMatch(
/@media \(max-width: 800px\)[\s\S]*?\.workspace-tools-trigger\s*\{[^}]*height:\s*32px;[^}]*width:\s*32px/
)
})
it.each([ it.each([
['人物', 'character'], ['人物', 'character'],
['场景', 'scene'], ['场景', 'scene'],
@@ -71,7 +71,8 @@ describe('连续剧本阅读器', () => {
expect(content).toContain('padding: 32px') expect(content).toContain('padding: 32px')
expect(content).toContain('text-align: left') expect(content).toContain('text-align: left')
expect(content).not.toContain('auto') expect(content).not.toContain('auto')
expect(css).toMatch(/@media \(max-width: 600px\)[\s\S]*?\.reader-content\s*\{\s*padding-inline:\s*18px/) expect(css).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.reader-content\s*\{\s*padding-inline:\s*18px/)
expect(css).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.episode-reader\s*\{[^}]*72px/)
}) })
it('按编号连续渲染全部剧集、元数据和空正文,标题不截断,模型内容只按文本显示', () => { it('按编号连续渲染全部剧集、元数据和空正文,标题不截断,模型内容只按文本显示', () => {
+8
View File
@@ -20,6 +20,7 @@ import {
} from '@/features/storyboard/testing/fixtures' } from '@/features/storyboard/testing/fixtures'
import { getStoryboardSession } from '@/features/storyboard/model' import { getStoryboardSession } from '@/features/storyboard/model'
import { getOperation } from '@/features/workflows/operations' import { getOperation } from '@/features/workflows/operations'
import { readAllStyles } from '@/testing/styles'
import SubjectImagesPage from '@/features/subject-images/SubjectImagesPage.vue' import SubjectImagesPage from '@/features/subject-images/SubjectImagesPage.vue'
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures' import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
import { getImageSession } from '@/features/subject-images/model' import { getImageSession } from '@/features/subject-images/model'
@@ -1432,6 +1433,13 @@ describe('工作台页面交互', () => {
expect(provided.refresh).toHaveBeenCalledOnce() expect(provided.refresh).toHaveBeenCalledOnce()
}) })
it('窄屏隐藏进入拆解,选择器需压过工具栏文字按钮', () => {
const css = readAllStyles()
expect(css).toMatch(
/@media \(max-width: 800px\)[\s\S]*?\.tabs-toolbar-actions \.script-next-link\s*\{[^}]*display:\s*none/
)
})
it('切换剧本标签保留阅读容器和位置,执行记录按需展开', async () => { it('切换剧本标签保留阅读容器和位置,执行记录按需展开', async () => {
wrapper = mount(CreateDramaPage, { wrapper = mount(CreateDramaPage, {
attachTo: document.body, attachTo: document.body,