feat: 同步视觉风格与主体身份母版工作区
对齐后端 dev 059e597,新增项目风格编辑锁定、身份文本生成、身份参考图与母版切换。 整理六个工作区入口,接通人物及场景母版继承说明、形态图片来源追溯。 补充草稿保护、正式 ID 校验、费用确认及部分失败回执,67 项测试和静态检查、构建通过。 未修改后端,未调用真实模型;本环境未完成浏览器视觉验收。
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user