ui: 默认展开内容折叠区

This commit is contained in:
GouJ
2026-09-22 18:42:56 +08:00
parent b9db84c261
commit 8e8fab307f
19 changed files with 90 additions and 126 deletions
+2 -2
View File
@@ -2,9 +2,9 @@
import { ref } from 'vue' import { ref } from 'vue'
import { NButton, NCollapse, NCollapseItem } from 'naive-ui' import { NButton, NCollapse, NCollapseItem } from 'naive-ui'
/** 保留说明与诊断的可发现入口,默认不占据主内容区域;阻塞错误不应放入此组件。 */ /** 说明与诊断默认直接展示,同时保留手动收起能力;阻塞错误不应放入此组件。 */
withDefaults(defineProps<{ title?: string }>(), { title: '使用说明' }) withDefaults(defineProps<{ title?: string }>(), { title: '使用说明' })
const expanded = ref<string[]>([]) const expanded = ref<string[]>(['details'])
/** 原生按钮保留 Enter/Space 键盘激活;文字点击不再向外重复触发折叠。 */ /** 原生按钮保留 Enter/Space 键盘激活;文字点击不再向外重复触发折叠。 */
function toggle() { function toggle() {
expanded.value = expanded.value.length ? [] : ['details'] expanded.value = expanded.value.length ? [] : ['details']
@@ -79,7 +79,7 @@ const filtered = computed(() =>
class="text-button mt-3" class="text-button mt-3"
>查看形态图片生图</RouterLink >查看形态图片生图</RouterLink
> >
<NCollapse class="subject-details mt-4" <NCollapse class="subject-details mt-4" :default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header ><template #header
>外观描述与形态 >外观描述与形态
@@ -198,7 +198,10 @@ function exportScript() {
<dd>{{ project.world[field.key] || '未提供' }}</dd></template <dd>{{ project.world[field.key] || '未提供' }}</dd></template
> >
</dl> </dl>
<NCollapse v-if="project.world.rules" class="mt-6 text-sm" <NCollapse
v-if="project.world.rules"
class="mt-6 text-sm"
:default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header>世界规则</template> ><template #header>世界规则</template>
<NScrollbar class="code-scroll mt-3" x-scrollable> <NScrollbar class="code-scroll mt-3" x-scrollable>
@@ -219,7 +222,10 @@ function exportScript() {
v-else v-else
title="暂无可用审核结果" title="暂无可用审核结果"
description="项目详情不再提供审核记录;可在执行记录中查看工作流状态。" description="项目详情不再提供审核记录;可在执行记录中查看工作流状态。"
/><NCollapse v-if="state?.rewriteSuggestion" class="mt-5 text-sm" /><NCollapse
v-if="state?.rewriteSuggestion"
class="mt-5 text-sm"
:default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header>查看改写建议原文</template> ><template #header>查看改写建议原文</template>
<NScrollbar class="code-scroll mt-3" x-scrollable> <NScrollbar class="code-scroll mt-3" x-scrollable>
@@ -167,7 +167,7 @@ onScopeDispose(() => controller?.abort())
</script> </script>
<template> <template>
<NCollapse class="production-tool-section"> <NCollapse class="production-tool-section" :default-expanded-names="['creative-profile']">
<NCollapseItem name="creative-profile" title="项目画布与生成模型"> <NCollapseItem name="creative-profile" title="项目画布与生成模型">
<NSpin :show="loading"> <NSpin :show="loading">
<NAlert v-if="error" type="error" :show-icon="false" class="mb-3">{{ error }}</NAlert> <NAlert v-if="error" type="error" :show-icon="false" class="mb-3">{{ error }}</NAlert>
@@ -179,61 +179,40 @@ onScopeDispose(() => controller?.abort())
> >
后端尚未启用完整的图片和视频模型暂时无法保存项目生成配置 后端尚未启用完整的图片和视频模型暂时无法保存项目生成配置
</NAlert> </NAlert>
<div v-if="profile" class="creative-profile-layout"> <div v-if="profile" class="creative-profile-grid">
<!-- 以下是项目画布设置模块 --> <NFormItem label="作品宽高比">
<section class="canvas-settings" aria-labelledby="canvas-settings-title"> <NSelect
<div class="setting-heading"> :value="profile.aspectRatio"
<h3 id="canvas-settings-title">项目画布</h3> :options="aspectRatios"
<p>统一图片和视频的画面比例</p> :disabled="saving"
</div> @update:value="profile.aspectRatio = $event as CreativeProfileAspectRatio"
<NFormItem label="作品宽高比" :show-feedback="false" class="canvas-ratio-field"> />
</NFormItem>
<div></div>
<section class="model-section">
<NFormItem label="图片模型">
<NSelect <NSelect
:value="profile.aspectRatio" :value="imageSelection"
:options="aspectRatios" :options="modelOptions(enabledImages)"
:disabled="saving" :disabled="saving"
@update:value="profile.aspectRatio = $event as CreativeProfileAspectRatio" @update:value="selectModel('image', $event)"
/> />
</NFormItem> </NFormItem>
</section>
<!-- 以下是图片生成模型设置模块 -->
<section class="model-section" aria-labelledby="image-model-title">
<div class="model-section-heading">
<div class="setting-heading">
<h3 id="image-model-title">图片生成</h3>
<p>选择首帧图片模型并配置生成参数</p>
</div>
<NFormItem label="图片模型" :show-feedback="false" class="model-picker">
<NSelect
:value="imageSelection"
:options="modelOptions(enabledImages)"
:disabled="saving"
@update:value="selectModel('image', $event)"
/>
</NFormItem>
</div>
<GenerationOptionFields <GenerationOptionFields
v-model="profile.imageOptions" v-model="profile.imageOptions"
:schemas="imageModel?.generationOptions || []" :schemas="imageModel?.generationOptions || []"
:disabled="saving" :disabled="saving"
/> />
</section> </section>
<!-- 以下是视频生成模型设置模块 --> <section class="model-section">
<section class="model-section" aria-labelledby="video-model-title"> <NFormItem label="视频模型">
<div class="model-section-heading"> <NSelect
<div class="setting-heading"> :value="videoSelection"
<h3 id="video-model-title">视频生成</h3> :options="modelOptions(enabledVideos)"
<p>选择成片模型并配置时长画质等参数</p> :disabled="saving"
</div> @update:value="selectModel('video', $event)"
<NFormItem label="视频模型" :show-feedback="false" class="model-picker"> />
<NSelect </NFormItem>
:value="videoSelection"
:options="modelOptions(enabledVideos)"
:disabled="saving"
@update:value="selectModel('video', $event)"
/>
</NFormItem>
</div>
<GenerationOptionFields <GenerationOptionFields
v-model="profile.videoOptions" v-model="profile.videoOptions"
:schemas="videoModel?.generationOptions || []" :schemas="videoModel?.generationOptions || []"
@@ -241,8 +220,7 @@ onScopeDispose(() => controller?.abort())
/> />
</section> </section>
</div> </div>
<!-- 以下是配置保存操作模块 --> <div class="mt-3 flex items-center gap-3">
<div class="profile-save-actions">
<NButton <NButton
type="primary" type="primary"
:loading="saving" :loading="saving"
@@ -259,43 +237,18 @@ onScopeDispose(() => controller?.abort())
<style scoped> <style scoped>
@reference "../../../styles/styles.css"; @reference "../../../styles/styles.css";
.creative-profile-layout { .creative-profile-grid {
@apply grid gap-4; @apply grid grid-cols-[repeat(2,_minmax(0,_1fr))] gap-x-5;
}
.canvas-settings,
.model-section {
@apply min-w-0 bg-(--app-subtle);
}
.canvas-settings {
@apply grid grid-cols-[minmax(0,_1fr)_220px] items-center gap-6 px-5 py-4;
}
.setting-heading h3 {
@apply text-[13px] font-semibold text-(--app-text);
}
.setting-heading p {
@apply mt-1 text-xs leading-5 text-muted;
}
.canvas-ratio-field,
.model-picker {
@apply mb-0;
} }
.model-section { .model-section {
@apply px-5 pt-4 pb-5; @apply min-w-0 p-4 bg-(--app-subtle);
}
.model-section-heading {
@apply grid grid-cols-[minmax(0,_1fr)_minmax(260px,_360px)] items-center gap-6 pb-5 mb-5 border-b border-(--app-border);
}
.profile-save-actions {
@apply mt-4 flex flex-wrap items-center gap-x-3 gap-y-2;
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.canvas-settings, .creative-profile-grid {
.model-section-heading { @apply grid-cols-[minmax(0,_1fr)];
@apply grid-cols-[minmax(0,_1fr)] gap-3;
} }
.canvas-ratio-field, .creative-profile-grid > div:empty {
.model-picker { @apply hidden;
@apply max-w-none;
} }
} }
</style> </style>
@@ -6,10 +6,10 @@ import { downloadText, formatDate } from '../../../lib/format'
import { useAdvancedProduction } from '../useAdvancedProduction' import { useAdvancedProduction } from '../useAdvancedProduction'
import ProductionPlanPanel from './ProductionPlanPanel.vue' import ProductionPlanPanel from './ProductionPlanPanel.vue'
/** 高级入口明确真实流程边界,默认折叠且不在挂载时触发预检或生成。 */ /** 高级入口默认展示真实流程边界,但不会在挂载时触发预检或生成。 */
const { id, operation, session, checking, error, check, blocked, preflight, start } = useAdvancedProduction() const { id, operation, session, checking, error, check, blocked, preflight, start } = useAdvancedProduction()
const receipt = computed(() => session.value.pipelineReceipt) const receipt = computed(() => session.value.pipelineReceipt)
const expanded = ref<string[]>([]) const expanded = ref<string[]>(['advanced'])
watch( watch(
receipt, receipt,
value => { value => {
@@ -60,7 +60,7 @@ onScopeDispose(() => controller?.abort())
</script> </script>
<template> <template>
<NCollapse class="production-tool-section"> <NCollapse class="production-tool-section" :default-expanded-names="['episode-assembly']">
<NCollapseItem name="episode-assembly" title="单集成片拼接"> <NCollapseItem name="episode-assembly" title="单集成片拼接">
<NSpin :show="loading"> <NSpin :show="loading">
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
@@ -83,7 +83,7 @@ const problems = computed(() => [
{{ plan.manualBlocked }} {{ plan.manualBlocked }}
项需人工处理同一镜头可能重复计数可先生成就绪的主体参考图成功结果会保留遇到阻塞时后续阶段可能暂停 项需人工处理同一镜头可能重复计数可先生成就绪的主体参考图成功结果会保留遇到阻塞时后续阶段可能暂停
</NAlert> </NAlert>
<NCollapse v-if="problems.length" class="mt-3"> <NCollapse v-if="problems.length" class="mt-3" :default-expanded-names="['issues']">
<NCollapseItem name="issues" title="查看前置条件与阻塞原因"> <NCollapseItem name="issues" title="查看前置条件与阻塞原因">
<ul class="space-y-3 text-xs"> <ul class="space-y-3 text-xs">
<li v-for="item in problems" :key="item.key"> <li v-for="item in problems" :key="item.key">
@@ -125,7 +125,7 @@ const rules = computed(() => ({
limit: integerRule('本批镜头上限'), limit: integerRule('本批镜头上限'),
...sizeRules(() => formModel.value) ...sizeRules(() => formModel.value)
})) }))
const optionsExpanded = ref<string[]>([]) const optionsExpanded = ref<string[]>(['options'])
</script> </script>
<template> <template>
@@ -277,7 +277,7 @@ const optionsExpanded = ref<string[]>([])
session.error session.error
}}</NAlert> }}</NAlert>
<QualityResult v-if="session.receipt" :receipt="session.receipt" @locate="emit('locate', $event)" /> <QualityResult v-if="session.receipt" :receipt="session.receipt" @locate="emit('locate', $event)" />
<NCollapse v-else-if="saved" class="mt-4" <NCollapse v-else-if="saved" class="mt-4" :default-expanded-names="['saved']"
><NCollapseItem name="saved" :title="`本会话最近校验 · ${saved.passed ? '通过' : '未通过'}`" ><NCollapseItem name="saved" :title="`本会话最近校验 · ${saved.passed ? '通过' : '未通过'}`"
><p>{{ saved.summary }}</p> ><p>{{ saved.summary }}</p>
<p v-for="(issue, index) in saved.issues" :key="index" class="mt-2 text-danger"> <p v-for="(issue, index) in saved.issues" :key="index" class="mt-2 text-danger">
@@ -4,7 +4,7 @@ import { NAlert, NButton, NCollapse, NCollapseItem } from 'naive-ui'
import { downloadText } from '../../../lib/format' import { downloadText } from '../../../lib/format'
import type { QualityReceipt } from '../quality.types' import type { QualityReceipt } from '../quality.types'
/** 摘要失败直接显示,完整逐轮信息按需展开,不用完成请求冒充质量通过。 */ /** 摘要失败完整逐轮信息默认直接显示,不用完成请求冒充质量通过。 */
const props = defineProps<{ receipt: QualityReceipt }>() const props = defineProps<{ receipt: QualityReceipt }>()
const emit = defineEmits<{ locate: [shotId: string] }>() const emit = defineEmits<{ locate: [shotId: string] }>()
const passed = computed(() => const passed = computed(() =>
@@ -113,7 +113,7 @@ function exportResult() {
> >
{{ issue.frameLabel ? `${issue.frameLabel} · ` : '' }}{{ issue.description }} {{ issue.frameLabel ? `${issue.frameLabel} · ` : '' }}{{ issue.description }}
</NAlert> </NAlert>
<NCollapse class="mt-4"> <NCollapse class="mt-4" :default-expanded-names="['details']">
<NCollapseItem name="details" title="主体检查与执行详情"> <NCollapseItem name="details" title="主体检查与执行详情">
<template v-if="receipt.kind === 'keyframe' || receipt.kind === 'video'"> <template v-if="receipt.kind === 'keyframe' || receipt.kind === 'video'">
<p class="text-xs"> <p class="text-xs">
@@ -4,7 +4,7 @@ import { NAlert, NButton, NCollapse, NCollapseItem, NTag } from 'naive-ui'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { ExternalLink, ImagePlus, RefreshCw } from '@lucide/vue' import { ExternalLink, ImagePlus, RefreshCw } from '@lucide/vue'
import { AssetImage, StatusBadge } from '../../../components/ui' import { AssetImage, StatusBadge } from '../../../components/ui'
import { useQuery } from '../../../composables/useQuery' import { usePolling } from '../../../composables/usePolling'
import { mediaAssetUrl } from '../../../lib/assets' import { mediaAssetUrl } from '../../../lib/assets'
import { errorMessage } from '../../../lib/http' import { errorMessage } from '../../../lib/http'
import { formatDate } from '../../../lib/format' import { formatDate } from '../../../lib/format'
@@ -41,16 +41,20 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ changed: [] }>() const emit = defineEmits<{ changed: [] }>()
const key = computed(() => props.shot.shotId) const key = computed(() => props.shot.shotId)
/** 单镜头资产仅在进入、切换镜头或显式刷新时读取。 */ /** 单镜头资产已有刷新入口,不再定时轮询。 */
const query = useQuery(key, async (shotId, signal) => { const query = usePolling(
const [keyframes, videos] = await Promise.all([ key,
productionApi.listKeyframes(shotId, signal), async (shotId, signal) => {
productionApi.listVideos(shotId, signal) const [keyframes, videos] = await Promise.all([
]) productionApi.listKeyframes(shotId, signal),
if (keyframes.some(item => item.shotId !== shotId) || videos.some(item => item.shotId !== shotId)) productionApi.listVideos(shotId, signal)
throw new Error('资产记录与当前镜头不匹配,请刷新后重试。') ])
return { keyframes, videos } if (keyframes.some(item => item.shotId !== shotId) || videos.some(item => item.shotId !== shotId))
}) throw new Error('资产记录与当前镜头不匹配,请刷新后重试。')
return { keyframes, videos }
},
false
)
const keyframes = computed(() => query.data.value?.keyframes ?? []) const keyframes = computed(() => query.data.value?.keyframes ?? [])
const videos = computed(() => query.data.value?.videos ?? []) const videos = computed(() => query.data.value?.videos ?? [])
const currentKeyframe = computed(() => primaryKeyframe(keyframes.value)) const currentKeyframe = computed(() => primaryKeyframe(keyframes.value))
@@ -63,7 +67,7 @@ const qualityOpen = ref(false)
const qualityTarget = ref<QualityTarget | null>(null) const qualityTarget = ref<QualityTarget | null>(null)
const specsOpen = ref(false) const specsOpen = ref(false)
/** 从实际候选打开质量面板,保留单镜头主操作,不默认展开全部校验参数。 */ /** 从实际候选打开质量面板,保留单镜头主操作。 */
function openQuality(kind: 'keyframe' | 'video', assetId: string) { function openQuality(kind: 'keyframe' | 'video', assetId: string) {
qualityTarget.value = { qualityTarget.value = {
kind, kind,
+1 -1
View File
@@ -298,7 +298,7 @@ const rules = { concurrency: integerRule('批量并发'), maxRepairAttempts: int
修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于 修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于
VisualState VisualState
</p> </p>
<NCollapse class="mt-3 pt-3" <NCollapse class="mt-3 pt-3" :default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header>视频提示词批量操作</template> ><template #header>视频提示词批量操作</template>
<p class="my-3 text-xs leading-6 text-muted"> <p class="my-3 text-xs leading-6 text-muted">
@@ -99,7 +99,7 @@ const filtered = computed(() =>
) )
) )
/** 深链接仅在目标 ID 变化时选择,不因目录刷新把用户切回原主体。 */ /** 深链接仅在目标 ID 变化时选择,不因目录轮询把用户切回原主体。 */
const linkedSubjectId = computed( const linkedSubjectId = computed(
() => subjects.value.find(item => item.id === route.query.subjectId || item.ref === route.query.subjectRef)?.id () => subjects.value.find(item => item.id === route.query.subjectId || item.ref === route.query.subjectRef)?.id
) )
@@ -343,7 +343,7 @@ const rules = { concurrency: integerRule('并发数'), candidateLimit: integerRu
/></NButton> /></NButton>
</div> </div>
</section> </section>
<NCollapse class="panel mb-5 p-5" <NCollapse class="panel mb-5 p-5" :default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header>高级全部主体身份文本</template> ><template #header>高级全部主体身份文本</template>
<p class="mt-3 text-xs leading-6 text-muted"> <p class="mt-3 text-xs leading-6 text-muted">
@@ -355,7 +355,7 @@ const rules = { concurrency: integerRule('批量并发'), limit: integerRule('
<p class="mt-2 text-[11px] leading-6 text-muted"> <p class="mt-2 text-[11px] leading-6 text-muted">
图库不自动刷新可点击刷新图库读取最新记录主图供分镜引用候选图和历史失败记录均保留过期主图不会自动删除或替换 图库不自动刷新可点击刷新图库读取最新记录主图供分镜引用候选图和历史失败记录均保留过期主图不会自动删除或替换
</p> </p>
<NCollapse class="mt-4 pt-4" <NCollapse class="mt-4 pt-4" :default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header>项目批量生图</template> ><template #header>项目批量生图</template>
<AppForm :model="formModel" :rules="rules" :disabled="operation.pending" validate-on-change> <AppForm :model="formModel" :rules="rules" :disabled="operation.pending" validate-on-change>
@@ -82,7 +82,10 @@ const rules = { ...sizeRules(() => formModel.value) }
:autosize="{ minRows: 3, maxRows: 12 }" :autosize="{ minRows: 3, maxRows: 12 }"
></NInput> ></NInput>
</NFormItem> </NFormItem>
<NCollapse v-if="form?.generationPrompt || form?.appearancePrompt" class="text-xs" <NCollapse
v-if="form?.generationPrompt || form?.appearancePrompt"
class="text-xs"
:default-expanded-names="['details']"
><NCollapseItem name="details" ><NCollapseItem name="details"
><template #header>{{ ><template #header>{{
form?.generationPrompt ? '查看正式生图提示词' : '查看原始外观素材(不是最终提示词)' form?.generationPrompt ? '查看正式生图提示词' : '查看原始外观素材(不是最终提示词)'
@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
import { NAlert, NButton, NCollapse, NCollapseItem, NInput, NSelect, NTab, NTabs } from 'naive-ui' import { NAlert, NButton, NCollapse, NCollapseItem, NInput, NSelect, NTab, NTabs } from 'naive-ui'
import { Download, RefreshCw, Search } from '@lucide/vue' import { Download, RefreshCw, Search } from '@lucide/vue'
import { AppDialog, EmptyState } from '../../components/ui' import { AppDialog, EmptyState } from '../../components/ui'
import { useQuery } from '../../composables/useQuery' import { usePolling } from '../../composables/usePolling'
import { downloadText, formatDate, nodeLabel } from '../../lib/format' import { downloadText, formatDate, nodeLabel } from '../../lib/format'
import { loadWorkflowDiagnostics } from './diagnostics' import { loadWorkflowDiagnostics } from './diagnostics'
import type { Checkpoint } from './types' import type { Checkpoint } from './types'
@@ -13,7 +13,7 @@ const props = defineProps<{ projectId: string; checkpoints: Checkpoint[] }>()
const open = defineModel<boolean>('open', { default: false }) const open = defineModel<boolean>('open', { default: false })
const key = computed(() => (open.value ? props.projectId : '')) const key = computed(() => (open.value ? props.projectId : ''))
/** 诊断弹窗已有刷新按钮,打开后只读取一次。 */ /** 诊断弹窗已有刷新按钮,打开后只读取一次。 */
const query = useQuery(key, (id, signal) => (id ? loadWorkflowDiagnostics(id, signal) : Promise.resolve(null))) const query = usePolling(key, (id, signal) => (id ? loadWorkflowDiagnostics(id, signal) : Promise.resolve(null)), false)
const tab = ref('timeline') const tab = ref('timeline')
const workflow = ref('all') const workflow = ref('all')
const search = ref('') const search = ref('')
@@ -136,7 +136,7 @@ function exportReport() {
<p class="mt-2 break-all font-mono text-[11px] text-muted">{{ node.checkpointId }}</p> <p class="mt-2 break-all font-mono text-[11px] text-muted">{{ node.checkpointId }}</p>
</article> </article>
</div> </div>
<NCollapse v-else class="mt-4" <NCollapse v-else class="mt-4" :default-expanded-names="groups.map(group => group.phase)"
><NCollapseItem ><NCollapseItem
v-for="group in groups" v-for="group in groups"
:key="group.phase" :key="group.phase"
+5 -4
View File
@@ -37,8 +37,11 @@ describe('精简入口保留功能', () => {
expect(wrapper.emitted('select')).toHaveLength(1) expect(wrapper.emitted('select')).toHaveLength(1)
}) })
it('补充说明默认展开,键盘可聚焦的标题可查看原文', async () => { it('补充说明默认展开,键盘可聚焦的标题仍可手动收起和展开', async () => {
wrapper = mount(DetailDisclosure, { props: { title: '规则说明' }, slots: { default: '完整规则仍然保留' } }) wrapper = mount(DetailDisclosure, { props: { title: '规则说明' }, slots: { default: '完整规则仍然保留' } })
expect(wrapper.text()).toContain('完整规则仍然保留')
await wrapper.get('.n-collapse-item__header-main').trigger('click')
await flushPromises()
expect(wrapper.text()).not.toContain('完整规则仍然保留') expect(wrapper.text()).not.toContain('完整规则仍然保留')
await wrapper.get('.n-collapse-item__header-main').trigger('click') await wrapper.get('.n-collapse-item__header-main').trigger('click')
await flushPromises() await flushPromises()
@@ -47,9 +50,7 @@ describe('精简入口保留功能', () => {
it('风格高级字段收起不丢失保存值,展开后仍可修改', async () => { it('风格高级字段收起不丢失保存值,展开后仍可修改', async () => {
wrapper = mount(StyleEditor, { props: { visualStyle: null, disabled: false } }) wrapper = mount(StyleEditor, { props: { visualStyle: null, disabled: false } })
expect(wrapper.find('#style-constraints').exists()).toBe(false) expect(wrapper.find('#style-constraints').exists()).toBe(true)
await wrapper.get('.n-collapse-item__header-main').trigger('click')
await flushPromises()
await wrapper.get('#style-constraints').setValue('["保留硬约束"]') await wrapper.get('#style-constraints').setValue('["保留硬约束"]')
const fields = wrapper.findAllComponents(NInput) const fields = wrapper.findAllComponents(NInput)
fields.find(item => item.props('placeholder')?.includes('人物质感'))!.vm.$emit('update:value', '人物风格') fields.find(item => item.props('placeholder')?.includes('人物质感'))!.vm.$emit('update:value', '人物风格')
+1 -2
View File
@@ -93,8 +93,7 @@ describe('统一的 Naive UI 表单校验', () => {
it('折叠后的非法 JSON 仍拦截保存并展开错误,修正后保留草稿正常提交', async () => { it('折叠后的非法 JSON 仍拦截保存并展开错误,修正后保留草稿正常提交', async () => {
wrapper = mount(StyleEditor, { attachTo: document.body, props: { visualStyle: null, disabled: false } }) wrapper = mount(StyleEditor, { attachTo: document.body, props: { visualStyle: null, disabled: false } })
await flushPromises() await flushPromises()
button('分类风格与硬约束').click() expect(button('分类风格与硬约束').getAttribute('aria-expanded')).toBe('true')
await flushPromises()
await input('#style-name', '手工风格') await input('#style-name', '手工风格')
await input('#style-constraints', '{"invalid":true}') await input('#style-constraints', '{"invalid":true}')
button('分类风格与硬约束').click() button('分类风格与硬约束').click()
@@ -99,7 +99,7 @@ function checkpoint(): Checkpoint {
} }
} }
/** 提供响应式 checkpoint,模拟手动刷新但不连接后端或启动模型任务。 */ /** 提供响应式 checkpoint,模拟轮询更新但不连接后端或启动模型任务。 */
function mountPage(records = [checkpoint()], episodes = 1): ReturnType<typeof useProjectData> { function mountPage(records = [checkpoint()], episodes = 1): ReturnType<typeof useProjectData> {
const project: ProjectDetail = { const project: ProjectDetail = {
id: 'breakdown-scroll-test', id: 'breakdown-scroll-test',
@@ -248,7 +248,7 @@ describe('拆解页内容滚动', () => {
['人物', 'character'], ['人物', 'character'],
['场景', 'scene'], ['场景', 'scene'],
['道具', 'prop'] ['道具', 'prop']
] as const)('%s 的奇偶主体展开后均使用独立形态块,搜索重排不影响形态归属', async (label, module) => { ] as const)('%s 的奇偶主体默认展示独立形态块,搜索重排不影响形态归属', async (label, module) => {
const record = checkpoint() const record = checkpoint()
// 前两个主体各准备两种形态,覆盖奇偶条纹和默认标签,不调用模型接口。 // 前两个主体各准备两种形态,覆盖奇偶条纹和默认标签,不调用模型接口。
record.state.breakdownResult!.subjectForms = [0, 1].flatMap(subjectIndex => record.state.breakdownResult!.subjectForms = [0, 1].flatMap(subjectIndex =>
@@ -267,8 +267,6 @@ describe('拆解页内容滚动', () => {
const articles = wrapper!.findAll('.subject-record-list > article') const articles = wrapper!.findAll('.subject-record-list > article')
for (const index of [0, 1]) { for (const index of [0, 1]) {
const article = articles[index]! const article = articles[index]!
await article.get('.n-collapse-item__header-main').trigger('click')
await flushPromises()
const cards = article.findAll('.subject-form-card') const cards = article.findAll('.subject-form-card')
expect(cards).toHaveLength(2) expect(cards).toHaveLength(2)
expect(cards[0]!.get('.n-tag').text()).toBe('默认') expect(cards[0]!.get('.n-tag').text()).toBe('默认')
@@ -341,7 +339,7 @@ describe('拆解页内容滚动', () => {
expect(toolbar.text()).toContain('第二集') expect(toolbar.text()).toContain('第二集')
expect(toolbar.get('.breakdown-storyboard-summary').text()).toContain('2 个节拍 · 2 个镜头') expect(toolbar.get('.breakdown-storyboard-summary').text()).toContain('2 个节拍 · 2 个镜头')
expect(wrapper!.findAll('.beat-section')).toHaveLength(2) expect(wrapper!.findAll('.beat-section')).toHaveLength(2)
// 刷新删除当前选项时,输入框与正文一起回到仍存在的第一集。 // 轮询删除当前选项时,输入框与正文一起回到仍存在的第一集。
provided.data.value!.checkpoints = [checkpoint()] provided.data.value!.checkpoints = [checkpoint()]
await flushPromises() await flushPromises()
expect(select.props('value')).toBe(1) expect(select.props('value')).toBe(1)
+2 -2
View File
@@ -374,7 +374,7 @@ describe('视觉质量契约与付费边界', () => {
}) })
describe('质量面板渐进展示', () => { describe('质量面板渐进展示', () => {
it('默认仅校验,未确认时不可提交,打开或更改参数不产生费用', async () => { it('默认展示校验参数,未确认时不可提交,打开或更改参数不产生费用', async () => {
const data = server() const data = server()
wrapper = mount(QualityDialog, { wrapper = mount(QualityDialog, {
attachTo: document.body, attachTo: document.body,
@@ -388,7 +388,7 @@ describe('质量面板渐进展示', () => {
await flushPromises() await flushPromises()
expect(validationButton().disabled).toBe(true) expect(validationButton().disabled).toBe(true)
expect(document.body.textContent).toContain('更多参数与模型限制') expect(document.body.textContent).toContain('更多参数与模型限制')
expect(document.querySelector('[aria-label="额外允许的画面文字"]')).toBeNull() expect(document.querySelector('[aria-label="额外允许的画面文字"]')).not.toBeNull()
expect(data.fetcher).not.toHaveBeenCalled() expect(data.fetcher).not.toHaveBeenCalled()
wrapper.findAllComponents(NCheckbox).at(-1)!.vm.$emit('update:checked', true) wrapper.findAllComponents(NCheckbox).at(-1)!.vm.$emit('update:checked', true)
await flushPromises() await flushPromises()