feat: 同步视觉风格与主体身份母版工作区

对齐后端 dev 059e597,新增项目风格编辑锁定、身份文本生成、身份参考图与母版切换。
整理六个工作区入口,接通人物及场景母版继承说明、形态图片来源追溯。
补充草稿保护、正式 ID 校验、费用确认及部分失败回执,67 项测试和静态检查、构建通过。
未修改后端,未调用真实模型;本环境未完成浏览器视觉验收。
This commit is contained in:
GouJ
2026-08-28 19:28:27 +08:00
parent 47443c0efe
commit 669f4d9df0
34 changed files with 2273 additions and 22 deletions
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { usePolling } from '../../composables/usePolling'
import { runOperation } from '../workflows/operations'
import { useProjectMutationGuard } from '../workflows/useProjectMutationGuard'
import ConfirmAction from '../workflows/ConfirmAction.vue'
import { visualStyleApi } from './api'
import type { AddStyleImageInput, SaveVisualStyleInput, VisualStyle, VisualStyleImage } from './types'
import StyleEditor from './components/StyleEditor.vue'
import StyleImages from './components/StyleImages.vue'
/** 项目视觉风格工作区:文本编辑、锁定和风格图记录分开管理。 */
const { projectId, blocked } = useProjectMutationGuard()
const query = usePolling(projectId, async (id, signal) => {
const style = await visualStyleApi.get(id, signal)
assertProject(style, id)
return { style }
})
const style = computed(() => query.data.value?.style ?? null)
const disabled = computed(() => blocked.value || !!query.error.value || !query.data.value)
const editorRevision = ref(0)
const imageRevision = ref(0)
const dirty = ref(false)
/** 防止错误项目的数据进入编辑器;空风格是正常的首次使用状态。 */
function assertProject(value: VisualStyle | null, id: string) {
if (value && (value.projectId !== id || value.images.some(image => image.visualStyleId !== value.id)))
throw new Error('视觉风格与当前项目不匹配,请刷新后重试。')
}
/** 操作成功后重建草稿,失败则保留用户输入;不因查询失败重发写请求。 */
async function writeStyle(label: string, action: (id: string) => Promise<VisualStyle>) {
if (disabled.value) return
const id = projectId.value
const ok = await runOperation(id, label, async () => {
const result = await action(id)
if (!result) throw new Error('后端未返回已保存的视觉风格,请刷新核对。')
assertProject(result, id)
if (projectId.value === id) {
query.data.value = { style: result }
editorRevision.value++
dirty.value = false
}
})
if (ok) await query.refresh()
}
/** 人工保存允许修改锁定风格。 */
function save(input: SaveVisualStyleInput) {
void writeStyle('保存项目视觉风格', id => visualStyleApi.save(id, input))
}
/** AI 覆盖必须先保存或放弃草稿,并在已锁定时禁止提交。 */
function generate() {
if (style.value?.isLocked || dirty.value) return
const force = !!style.value
void writeStyle('AI 生成项目视觉风格', id => visualStyleApi.generate(id, force))
}
/** 图片写操作固定项目与风格归属,保存后再读取列表。 */
async function writeImage(label: string, action: (id: string) => Promise<VisualStyleImage>, reset = false) {
if (disabled.value || !style.value) return
const id = projectId.value
const styleId = style.value.id
const ok = await runOperation(id, label, async () => {
const result = await action(id)
if (!result || result.visualStyleId !== styleId) throw new Error('后端未确认风格图片变更,请刷新核对。')
})
if (ok && reset && projectId.value === id) imageRevision.value++
await query.refresh()
}
/** 登记已存在的图片地址,不上传文件或调用模型。 */
function addImage(input: AddStyleImageInput) {
void writeImage('登记风格参考图', id => visualStyleApi.addImage(id, input), true)
}
/** 风格图启停不受文字锁定影响。 */
function toggleImage(image: VisualStyleImage) {
void writeImage('修改风格参考图启用状态', id => visualStyleApi.setImageEnabled(id, image.id, !image.enabled))
}
/** 删除已由子组件二次确认,不删除实际存储文件。 */
function removeImage(image: VisualStyleImage) {
void writeImage('移除风格参考图记录', id => visualStyleApi.removeImage(id, image.id))
}
</script>
<template>
<section class="mt-7">
<div class="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 class="text-lg font-semibold">项目视觉风格</h2>
<p class="mt-2 text-sm text-muted">确定项目统一的视觉语言再为人物场景和道具建立稳定身份</p>
</div>
<RouterLink :to="`/projects/${projectId}/subject-identity`" class="text-button"
>下一步主体身份 </RouterLink
>
</div>
<div class="my-5 flex flex-wrap items-center gap-3">
<span class="tag">{{ style ? (style.isLocked ? '已锁定' : '未锁定') : '尚未创建' }}</span>
<ConfirmAction
:label="style ? 'AI 重新生成风格' : 'AI 生成风格'"
:disabled="disabled || !!style?.isLocked || dirty"
acknowledgement
description="调用文本模型生成整体及分类风格。重新生成会覆盖已保存的文本与硬约束;旧身份、旧图片和已有提示词不会自动更新。"
@confirm="generate"
/>
<button class="text-button" :disabled="query.loading.value" @click="query.refresh">刷新风格</button>
</div>
<p v-if="query.error.value" class="alert alert-error mb-4" role="alert">
{{ query.error.value }} 请确认后端 dev 已更新并重启查询失败不等同于尚未创建
</p>
<p v-if="dirty" class="mb-4 text-xs text-muted">
有未保存修改AI 重生成暂不可用自动刷新不会覆盖编辑内容离开页面不会自动保存
</p>
<p class="alert mb-5 text-xs">
修改风格只影响之后的相关生成已有主体身份形态图片和视频提示词不会自动更新请按需要逐步重生成
</p>
<StyleEditor
v-if="query.data.value"
:key="editorRevision"
:style="style"
:disabled="disabled"
@save="save"
@dirty="dirty = $event"
/>
<p v-else-if="query.loading.value" class="py-8 text-sm text-muted" role="status">正在读取视觉风格</p>
<StyleImages
:key="imageRevision"
:images="style?.images ?? []"
:disabled="disabled || !style"
@add="addImage"
@toggle="toggleImage"
@remove="removeImage"
/>
</section>
</template>