ui: 默认展开内容折叠区
This commit is contained in:
@@ -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,14 +179,8 @@ 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">
|
|
||||||
<div class="setting-heading">
|
|
||||||
<h3 id="canvas-settings-title">项目画布</h3>
|
|
||||||
<p>统一图片和视频的画面比例。</p>
|
|
||||||
</div>
|
|
||||||
<NFormItem label="作品宽高比" :show-feedback="false" class="canvas-ratio-field">
|
|
||||||
<NSelect
|
<NSelect
|
||||||
:value="profile.aspectRatio"
|
:value="profile.aspectRatio"
|
||||||
:options="aspectRatios"
|
:options="aspectRatios"
|
||||||
@@ -194,16 +188,9 @@ onScopeDispose(() => controller?.abort())
|
|||||||
@update:value="profile.aspectRatio = $event as CreativeProfileAspectRatio"
|
@update:value="profile.aspectRatio = $event as CreativeProfileAspectRatio"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</section>
|
<div></div>
|
||||||
|
<section class="model-section">
|
||||||
<!-- 以下是图片生成模型设置模块 -->
|
<NFormItem label="图片模型">
|
||||||
<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
|
<NSelect
|
||||||
:value="imageSelection"
|
:value="imageSelection"
|
||||||
:options="modelOptions(enabledImages)"
|
:options="modelOptions(enabledImages)"
|
||||||
@@ -211,21 +198,14 @@ onScopeDispose(() => controller?.abort())
|
|||||||
@update:value="selectModel('image', $event)"
|
@update:value="selectModel('image', $event)"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</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">
|
|
||||||
<div class="setting-heading">
|
|
||||||
<h3 id="video-model-title">视频生成</h3>
|
|
||||||
<p>选择成片模型并配置时长、画质等参数。</p>
|
|
||||||
</div>
|
|
||||||
<NFormItem label="视频模型" :show-feedback="false" class="model-picker">
|
|
||||||
<NSelect
|
<NSelect
|
||||||
:value="videoSelection"
|
:value="videoSelection"
|
||||||
:options="modelOptions(enabledVideos)"
|
:options="modelOptions(enabledVideos)"
|
||||||
@@ -233,7 +213,6 @@ onScopeDispose(() => controller?.abort())
|
|||||||
@update:value="selectModel('video', $event)"
|
@update:value="selectModel('video', $event)"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</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,8 +41,10 @@ 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(
|
||||||
|
key,
|
||||||
|
async (shotId, signal) => {
|
||||||
const [keyframes, videos] = await Promise.all([
|
const [keyframes, videos] = await Promise.all([
|
||||||
productionApi.listKeyframes(shotId, signal),
|
productionApi.listKeyframes(shotId, signal),
|
||||||
productionApi.listVideos(shotId, signal)
|
productionApi.listVideos(shotId, signal)
|
||||||
@@ -50,7 +52,9 @@ const query = useQuery(key, async (shotId, signal) => {
|
|||||||
if (keyframes.some(item => item.shotId !== shotId) || videos.some(item => item.shotId !== shotId))
|
if (keyframes.some(item => item.shotId !== shotId) || videos.some(item => item.shotId !== shotId))
|
||||||
throw new Error('资产记录与当前镜头不匹配,请刷新后重试。')
|
throw new Error('资产记录与当前镜头不匹配,请刷新后重试。')
|
||||||
return { keyframes, videos }
|
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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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', '人物风格')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user