feat: 收口移动端侧栏与创作拆解页布局
This commit is contained in:
+45
-26
@@ -38,10 +38,12 @@ const NARROW_NAV = '(max-width: 800px)'
|
||||
const SIDER_COLLAPSED_WIDTH = 64
|
||||
/** 展开侧栏宽度,窄屏裁切动画以内层保持该宽度。 */
|
||||
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_INDENT = SIDER_COLLAPSED_WIDTH / 2 - NAV_ICON_SIZE / 2
|
||||
|
||||
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
|
||||
const route = useRoute()
|
||||
@@ -51,6 +53,12 @@ const collapsed = ref(narrow.value)
|
||||
const settingsOpen = ref(false)
|
||||
/** 窄屏折叠只裁切文字,不把菜单收成图标,避免推挤动画时错位。 */
|
||||
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) {
|
||||
@@ -144,19 +152,25 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
<div
|
||||
class="admin-layout"
|
||||
:style="{
|
||||
'--admin-sider-collapsed': SIDER_COLLAPSED_WIDTH + 'px',
|
||||
'--admin-sider-expanded': SIDER_EXPANDED_WIDTH + 'px'
|
||||
'--admin-sider-collapsed': siderCollapsedWidth + 'px',
|
||||
'--admin-sider-expanded': siderExpandedWidth + 'px'
|
||||
}"
|
||||
>
|
||||
<!-- 以下是主导航侧栏:宽屏与窄屏共用同一份 -->
|
||||
<aside
|
||||
class="admin-sider"
|
||||
:class="{ 'is-collapsed': collapsed }"
|
||||
:style="{ width: (collapsed ? SIDER_COLLAPSED_WIDTH : SIDER_EXPANDED_WIDTH) + 'px' }"
|
||||
:style="{ width: (collapsed ? siderCollapsedWidth : siderExpandedWidth) + 'px' }"
|
||||
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
|
||||
>
|
||||
</RouterLink>
|
||||
@@ -165,13 +179,14 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
:value="route.path"
|
||||
:options="menuOptions"
|
||||
:collapsed="menuCollapsed"
|
||||
:collapsed-width="SIDER_COLLAPSED_WIDTH"
|
||||
:collapsed-width="siderCollapsedWidth"
|
||||
:icon-size="NAV_ICON_SIZE"
|
||||
:collapsed-icon-size="NAV_ICON_SIZE"
|
||||
:indent="NAV_ICON_INDENT"
|
||||
:root-indent="NAV_ICON_INDENT"
|
||||
:indent="navIconIndent"
|
||||
:root-indent="navIconIndent"
|
||||
/>
|
||||
</NScrollbar>
|
||||
<!-- 以下是侧栏底部后端连接:折叠仅图标,展开图标加文字 -->
|
||||
<footer class="admin-sider-footer">
|
||||
<NTooltip placement="right" :disabled="!collapsed">
|
||||
<template #trigger>
|
||||
@@ -179,12 +194,12 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
quaternary
|
||||
block
|
||||
class="admin-settings-button"
|
||||
:class="{ 'is-collapsed': menuCollapsed }"
|
||||
:class="{ 'is-collapsed': collapsed }"
|
||||
aria-label="后端连接"
|
||||
@click="openSettings"
|
||||
>
|
||||
<template #icon><Settings2 :size="18" /></template>
|
||||
<span v-if="!menuCollapsed">后端连接</span>
|
||||
<span v-if="!collapsed">后端连接</span>
|
||||
</NButton>
|
||||
</template>
|
||||
后端连接
|
||||
@@ -265,11 +280,14 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
@apply flex-1 min-h-0;
|
||||
}
|
||||
.admin-sider-footer {
|
||||
@apply shrink-0;
|
||||
@apply w-full shrink-0 overflow-hidden;
|
||||
padding-block: 8px max(8px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.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 {
|
||||
@apply left-0 right-0 rounded-none;
|
||||
@@ -277,6 +295,9 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
.admin-brand {
|
||||
@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 {
|
||||
@apply shrink-0;
|
||||
}
|
||||
@@ -307,20 +328,20 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
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 {
|
||||
.admin-nav-scroll.n-scrollbar {
|
||||
min-width: var(--admin-sider-expanded);
|
||||
}
|
||||
.admin-brand span,
|
||||
.admin-sider .n-menu-item-content-header,
|
||||
.admin-settings-button span {
|
||||
.admin-brand {
|
||||
@apply h-12 w-full overflow-hidden;
|
||||
}
|
||||
.admin-sider:not(.is-collapsed) .admin-brand {
|
||||
@apply justify-start px-3.5;
|
||||
}
|
||||
.admin-sider .n-menu-item-content-header {
|
||||
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 {
|
||||
.admin-sider.is-collapsed .n-menu-item-content-header {
|
||||
opacity: 0;
|
||||
}
|
||||
.admin-topbar {
|
||||
@@ -329,9 +350,7 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.admin-sider,
|
||||
.admin-brand span,
|
||||
.admin-sider .n-menu-item-content-header,
|
||||
.admin-settings-button span {
|
||||
.admin-sider .n-menu-item-content-header {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@ const open = defineModel<boolean>('open', { default: false })
|
||||
<NButton
|
||||
class="workspace-tools-trigger"
|
||||
data-workspace-tools
|
||||
:aria-label="label"
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="open"
|
||||
@click="open = true"
|
||||
>
|
||||
<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>
|
||||
<NDrawer
|
||||
v-model:show="open"
|
||||
|
||||
@@ -137,6 +137,7 @@ function exportResult() {
|
||||
<template>
|
||||
<WorkspacePage split compact class="breakdown-workspace-page">
|
||||
<template #header>
|
||||
<!-- 以下是拆解状态与设置 -->
|
||||
<div class="workspace-toolbar">
|
||||
<h2 class="font-semibold">剧本拆解</h2>
|
||||
<div v-if="snapshot" class="toolbar-summary">
|
||||
@@ -144,7 +145,10 @@ function exportResult() {
|
||||
>抽取 {{ summary.completed }} / {{ summary.total }} · 失败 {{ summary.failed }}</span
|
||||
>
|
||||
<span>主体 {{ subjects.length }} · 分镜剧集 {{ shots.length }} / {{ plans.length }}</span>
|
||||
</div>
|
||||
<div v-if="snapshot || project?.episodes.length" class="toolbar-actions">
|
||||
<StatusBadge
|
||||
v-if="snapshot"
|
||||
:status="execution?.status || 'unknown'"
|
||||
:label="
|
||||
execution?.status === 'completed'
|
||||
@@ -154,9 +158,12 @@ function exportResult() {
|
||||
: '执行状态待确认'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="project?.episodes.length" class="toolbar-actions">
|
||||
<WorkspaceTools v-model:open="toolsOpen" title="拆解设置与恢复" label="拆解设置与恢复">
|
||||
<WorkspaceTools
|
||||
v-if="project?.episodes.length"
|
||||
v-model:open="toolsOpen"
|
||||
title="拆解设置与恢复"
|
||||
label="拆解设置与恢复"
|
||||
>
|
||||
<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>
|
||||
@@ -491,6 +498,7 @@ function exportResult() {
|
||||
:project-id="project?.id"
|
||||
:checkpoints="checkpoints"
|
||||
workflow="breakdown"
|
||||
@close="showHistory = false"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -583,9 +591,69 @@ function exportResult() {
|
||||
.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)];
|
||||
@media (max-width: 800px) {
|
||||
.breakdown-workspace-page .workspace-heading {
|
||||
@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>
|
||||
|
||||
@@ -24,7 +24,8 @@ const filtered = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5 lg:p-7">
|
||||
<div class="subject-list">
|
||||
<!-- 以下是主体搜索 -->
|
||||
<div class="subject-list-toolbar">
|
||||
<NInput
|
||||
class="subject-list-search"
|
||||
@@ -139,6 +140,14 @@ const filtered = computed(() =>
|
||||
.subject-details[open] summary svg {
|
||||
@apply rotate-180;
|
||||
}
|
||||
.subject-list {
|
||||
@apply p-5;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.subject-list {
|
||||
@apply p-7;
|
||||
}
|
||||
}
|
||||
.subject-list-toolbar {
|
||||
@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 {
|
||||
@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>
|
||||
|
||||
@@ -137,7 +137,7 @@ function exportScript() {
|
||||
/>
|
||||
<RouterLink
|
||||
v-if="complete"
|
||||
class="text-button text-accent"
|
||||
class="text-button text-accent script-next-link"
|
||||
:to="`/projects/${project?.id}/breakdown`"
|
||||
>进入拆解<ArrowRight :size="14"
|
||||
/></RouterLink>
|
||||
@@ -248,6 +248,7 @@ function exportScript() {
|
||||
:project-id="project?.id"
|
||||
:checkpoints="checkpoints"
|
||||
workflow="create-drama"
|
||||
@close="showHistory = false"
|
||||
/>
|
||||
</div></div
|
||||
></WorkspacePage>
|
||||
@@ -308,6 +309,17 @@ function exportScript() {
|
||||
.script-section .tabs-toolbar-actions {
|
||||
@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 {
|
||||
@apply flex-1 flex flex-col min-h-0;
|
||||
|
||||
@@ -254,15 +254,27 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
@apply grid-cols-[180px_minmax(0,_1fr)];
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 800px) {
|
||||
.episode-reader {
|
||||
@apply grid-cols-[112px_minmax(0,_1fr)];
|
||||
@apply grid-cols-[72px_minmax(0,_1fr)];
|
||||
}
|
||||
.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 {
|
||||
@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 {
|
||||
@apply px-[18px];
|
||||
|
||||
@@ -9,6 +9,10 @@ import WorkflowDiagnosticsDialog from './WorkflowDiagnosticsDialog.vue'
|
||||
|
||||
/** 时间线只说明 checkpoint 已保存,不把每条记录误标为工作流成功。 */
|
||||
const props = defineProps<{ checkpoints: Checkpoint[]; workflow: string; projectId?: string }>()
|
||||
const emit = defineEmits<{
|
||||
/** 关闭覆盖层中的执行记录 */
|
||||
close: []
|
||||
}>()
|
||||
const diagnosticsOpen = ref(false)
|
||||
const history = computed(() => workflowCheckpoints(props.checkpoints, props.workflow).slice(-18).toReversed())
|
||||
</script>
|
||||
@@ -16,7 +20,10 @@ const history = computed(() => workflowCheckpoints(props.checkpoints, props.work
|
||||
<template>
|
||||
<aside class="history-panel">
|
||||
<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>
|
||||
<NButton v-if="projectId" text size="small" class="mb-4" @click="diagnosticsOpen = true"
|
||||
>查看完整运行诊断</NButton
|
||||
@@ -72,9 +79,18 @@ const history = computed(() => workflowCheckpoints(props.checkpoints, props.work
|
||||
.history-content {
|
||||
@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 {
|
||||
@apply overflow-hidden min-h-0;
|
||||
@apply overflow-hidden min-h-0 bg-(--app-subtle);
|
||||
}
|
||||
.history-close {
|
||||
@apply inline-flex shrink-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -238,6 +238,12 @@ body {
|
||||
height: 32px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tabs-toolbar-actions .n-button.icon-button {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
/* 窄内容区允许 suffix 独占一行;Tabs 本身继续使用组件内置横向滚动。 */
|
||||
@container tabs-toolbar (max-width: 600px) {
|
||||
.workspace-tabs > .n-tabs-nav {
|
||||
|
||||
@@ -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__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 \.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).not.toContain('.result-toolbar > .n-button')
|
||||
})
|
||||
@@ -82,7 +85,9 @@ describe('管理后台组件边界', () => {
|
||||
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\)/
|
||||
)
|
||||
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/)
|
||||
})
|
||||
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-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(/\.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).toMatch(/transition:\s*width\s+0\.32s/)
|
||||
expect(css).toContain('calc(100% - var(--admin-sider-collapsed))')
|
||||
@@ -219,22 +227,41 @@ describe('管理后台组件边界', () => {
|
||||
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.get('.admin-sider').attributes('style')).toContain('width: 48px')
|
||||
expect(wrapper.find('.admin-sider-mask').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 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.get('.admin-sider').attributes('style')).toContain('width: 168px')
|
||||
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')
|
||||
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 flushPromises()
|
||||
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 () => {
|
||||
|
||||
@@ -222,8 +222,8 @@ describe('拆解页内容滚动', () => {
|
||||
expect(wrapper!.findAll('.subject-record-list > article')).toHaveLength(30)
|
||||
})
|
||||
|
||||
it('主体搜索与统计靠左居中,窄内容区可换行,输入框不再使用外置图标容器', () => {
|
||||
// DOM 环境不计算布局,保护输入框宽度、主题统计色与窄屏换行的样式契约。
|
||||
it('主体搜索与统计靠左居中,窄屏搜索拉满同一行,输入框不再使用外置图标容器', () => {
|
||||
// DOM 环境不计算布局,保护输入框宽度、主题统计色与窄屏单行搜索的样式契约。
|
||||
const css = readAllStyles()
|
||||
expect(css).toMatch(
|
||||
/\.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%;/
|
||||
)
|
||||
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')
|
||||
})
|
||||
|
||||
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([
|
||||
['人物', 'character'],
|
||||
['场景', 'scene'],
|
||||
|
||||
@@ -71,7 +71,8 @@ describe('连续剧本阅读器', () => {
|
||||
expect(content).toContain('padding: 32px')
|
||||
expect(content).toContain('text-align: left')
|
||||
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('按编号连续渲染全部剧集、元数据和空正文,标题不截断,模型内容只按文本显示', () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@/features/storyboard/testing/fixtures'
|
||||
import { getStoryboardSession } from '@/features/storyboard/model'
|
||||
import { getOperation } from '@/features/workflows/operations'
|
||||
import { readAllStyles } from '@/testing/styles'
|
||||
import SubjectImagesPage from '@/features/subject-images/SubjectImagesPage.vue'
|
||||
import { formFixture, imageFixture } from '@/features/subject-images/testing/fixtures'
|
||||
import { getImageSession } from '@/features/subject-images/model'
|
||||
@@ -1432,6 +1433,13 @@ describe('工作台页面交互', () => {
|
||||
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 () => {
|
||||
wrapper = mount(CreateDramaPage, {
|
||||
attachTo: document.body,
|
||||
|
||||
Reference in New Issue
Block a user