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>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { request } from '../../lib/http'
|
||||
import type { AddStyleImageInput, SaveVisualStyleInput, VisualStyle, VisualStyleImage } from './types'
|
||||
|
||||
/** 固定项目范围并编码正式 ID。 */
|
||||
function stylePath(projectId: string) {
|
||||
return `/projects/${encodeURIComponent(projectId)}/visual-style`
|
||||
}
|
||||
|
||||
/** 风格查询、编辑与图片登记;只有 generate 会调用文本模型。 */
|
||||
export const visualStyleApi = {
|
||||
get: (projectId: string, signal?: AbortSignal) => request<VisualStyle | null>(stylePath(projectId), { signal }),
|
||||
save: (projectId: string, input: SaveVisualStyleInput) =>
|
||||
request<VisualStyle>(stylePath(projectId), { method: 'PUT', body: input }),
|
||||
generate: (projectId: string, force: boolean) =>
|
||||
request<VisualStyle>(`${stylePath(projectId)}/generate`, { method: 'POST', body: { force }, timeoutMs: 0 }),
|
||||
addImage: (projectId: string, input: AddStyleImageInput) =>
|
||||
request<VisualStyleImage>(`${stylePath(projectId)}/images`, { method: 'POST', body: input }),
|
||||
setImageEnabled: (projectId: string, imageId: string, enabled: boolean) =>
|
||||
request<VisualStyleImage>(`${stylePath(projectId)}/images/${encodeURIComponent(imageId)}/enabled`, {
|
||||
method: 'PUT',
|
||||
body: { enabled }
|
||||
}),
|
||||
removeImage: (projectId: string, imageId: string) =>
|
||||
request<VisualStyleImage>(`${stylePath(projectId)}/images/${encodeURIComponent(imageId)}`, { method: 'DELETE' })
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import type { SaveVisualStyleInput, VisualStyle } from '../types'
|
||||
|
||||
/** 编辑草稿独立于轮询结果,刷新不会覆盖尚未保存的输入。 */
|
||||
const props = defineProps<{ style: VisualStyle | null; disabled: boolean }>()
|
||||
const emit = defineEmits<{ save: [input: SaveVisualStyleInput]; dirty: [value: boolean] }>()
|
||||
const form = reactive({
|
||||
name: '',
|
||||
prompt: '',
|
||||
characterPrompt: '',
|
||||
scenePrompt: '',
|
||||
propPrompt: '',
|
||||
constraints: '[]',
|
||||
isLocked: false
|
||||
})
|
||||
const baseline = ref('')
|
||||
const dirty = computed(() => JSON.stringify(form) !== baseline.value)
|
||||
const constraintsError = computed(() => {
|
||||
try {
|
||||
const value: unknown = JSON.parse(form.constraints)
|
||||
return Array.isArray(value) && value.every(item => typeof item === 'string')
|
||||
? ''
|
||||
: '硬约束须为字符串数组,例如 ["真人写实", "禁止二次元"]。'
|
||||
} catch {
|
||||
return '硬约束 JSON 格式不正确,请修正后保存。'
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.style,
|
||||
value => {
|
||||
if (baseline.value && dirty.value) return
|
||||
Object.assign(form, {
|
||||
name: value?.name ?? '默认视觉风格',
|
||||
prompt: value?.prompt ?? '',
|
||||
characterPrompt: value?.characterPrompt ?? '',
|
||||
scenePrompt: value?.scenePrompt ?? '',
|
||||
propPrompt: value?.propPrompt ?? '',
|
||||
constraints: JSON.stringify(value?.hardConstraints ?? [], null, 2),
|
||||
isLocked: value?.isLocked ?? false
|
||||
})
|
||||
baseline.value = JSON.stringify(form)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(dirty, value => emit('dirty', value), { immediate: true })
|
||||
|
||||
/** 显式校验 JSON 后保存结构化约束;不对错误结构做静默转换。 */
|
||||
function save() {
|
||||
if (props.disabled || constraintsError.value) return
|
||||
emit('save', {
|
||||
name: form.name.trim(),
|
||||
prompt: form.prompt,
|
||||
characterPrompt: form.characterPrompt,
|
||||
scenePrompt: form.scenePrompt,
|
||||
propPrompt: form.propPrompt,
|
||||
hardConstraints: JSON.parse(form.constraints) as string[],
|
||||
isLocked: form.isLocked
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="panel p-5 sm:p-6" @submit.prevent="save">
|
||||
<fieldset :disabled="disabled" class="space-y-5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 class="font-medium">风格说明</h3>
|
||||
<span v-if="dirty" class="tag">有未保存修改</span>
|
||||
</div>
|
||||
<label class="block"
|
||||
><span class="field-label">风格名称</span><input id="style-name" v-model="form.name" class="input"
|
||||
/></label>
|
||||
<label class="block"
|
||||
><span class="field-label">整体视觉语言</span
|
||||
><textarea
|
||||
id="style-prompt"
|
||||
v-model="form.prompt"
|
||||
class="input min-h-32"
|
||||
placeholder="媒介、真实感、色彩和整体美术方向"
|
||||
/>
|
||||
</label>
|
||||
<div class="grid gap-5 lg:grid-cols-3">
|
||||
<label
|
||||
><span class="field-label">人物风格补充</span
|
||||
><textarea v-model="form.characterPrompt" class="input min-h-32" />
|
||||
</label>
|
||||
<label
|
||||
><span class="field-label">场景风格补充</span
|
||||
><textarea v-model="form.scenePrompt" class="input min-h-32" />
|
||||
</label>
|
||||
<label
|
||||
><span class="field-label">道具风格补充</span
|
||||
><textarea v-model="form.propPrompt" class="input min-h-32" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="block"
|
||||
><span class="field-label">硬约束 · JSON 字符串数组</span
|
||||
><textarea
|
||||
id="style-constraints"
|
||||
v-model="form.constraints"
|
||||
class="input min-h-24 font-mono text-xs"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="constraintsError" class="text-xs text-danger" role="alert">{{ constraintsError }}</p>
|
||||
<label class="flex items-start gap-2 text-sm"
|
||||
><input
|
||||
id="style-lock"
|
||||
v-model="form.isLocked"
|
||||
type="checkbox"
|
||||
class="mt-1 accent-accent"
|
||||
/>确认并锁定风格,阻止后续 AI 自动覆盖</label
|
||||
>
|
||||
<p class="text-xs leading-6 text-muted">
|
||||
锁定后仍可人工编辑并保存。取消勾选并保存后,才能再次使用 AI 重生成。
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<button class="button button-primary" type="submit" :disabled="disabled || !!constraintsError">
|
||||
保存视觉风格
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { AssetImage } from '../../../components/ui'
|
||||
import { referenceImageUrl } from '../../../lib/assets'
|
||||
import type { AddStyleImageInput, StyleCategory, VisualStyleImage } from '../types'
|
||||
|
||||
/** 风格图只登记已有地址;不伪造文件上传或风格生图功能。 */
|
||||
const props = defineProps<{ images: VisualStyleImage[]; disabled: boolean }>()
|
||||
const emit = defineEmits<{
|
||||
add: [input: AddStyleImageInput]
|
||||
toggle: [image: VisualStyleImage]
|
||||
remove: [image: VisualStyleImage]
|
||||
}>()
|
||||
const form = reactive({ imageUrl: '', category: 'overall' as StyleCategory, sortOrder: 0, enabled: true })
|
||||
const confirmingId = ref('')
|
||||
const categories = { overall: '整体', character: '人物', scene: '场景', prop: '道具' } as const
|
||||
const valid = computed(() => !!referenceImageUrl(form.imageUrl) && Number.isSafeInteger(form.sortOrder))
|
||||
|
||||
/** 仅通过校验后发出新增请求,输入框在失败时保留供修正。 */
|
||||
function add() {
|
||||
if (props.disabled || !valid.value) return
|
||||
emit('add', { ...form, imageUrl: form.imageUrl.trim(), source: 'upload' })
|
||||
}
|
||||
|
||||
/** 删除只移除记录,需确认且不声称删除远程文件。 */
|
||||
function remove(image: VisualStyleImage) {
|
||||
if (props.disabled || confirmingId.value !== image.id) return
|
||||
confirmingId.value = ''
|
||||
emit('remove', image)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel mt-5 p-5 sm:p-6" aria-label="风格参考图">
|
||||
<h3 class="font-medium">
|
||||
风格参考图 <span class="ml-2 text-xs text-muted">{{ images.length }} 张</span>
|
||||
</h3>
|
||||
<p class="mt-2 text-xs leading-6 text-muted">
|
||||
登记已有 HTTP(S) 或 /storage/
|
||||
图片地址。后端尚无文件上传接口;这些图片目前仅管理记录,现有身份/形态生图不会自动将风格图片传给模型。
|
||||
</p>
|
||||
<form class="mt-4" @submit.prevent="add">
|
||||
<fieldset :disabled="disabled" class="flex flex-wrap items-end gap-3">
|
||||
<label class="min-w-48 flex-1"
|
||||
><span class="field-label">图片地址</span
|
||||
><input
|
||||
v-model="form.imageUrl"
|
||||
class="input"
|
||||
placeholder="https://… 或 /storage/…"
|
||||
aria-label="风格图片地址"
|
||||
/></label>
|
||||
<label
|
||||
><span class="field-label">分类</span
|
||||
><select v-model="form.category" class="input">
|
||||
<option v-for="(label, value) in categories" :key="value" :value="value">{{ label }}</option>
|
||||
</select></label
|
||||
>
|
||||
<label class="w-24"
|
||||
><span class="field-label">排序</span
|
||||
><input
|
||||
v-model.number="form.sortOrder"
|
||||
class="input"
|
||||
type="number"
|
||||
step="1"
|
||||
aria-label="风格图片排序"
|
||||
/></label>
|
||||
<label class="flex gap-2 pb-2 text-xs"><input v-model="form.enabled" type="checkbox" />启用</label>
|
||||
<button class="button button-secondary" type="submit" :disabled="disabled || !valid">登记参考图</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<p v-if="form.imageUrl && !valid" class="mt-2 text-xs text-danger" role="alert">
|
||||
请填写有效图片地址和整数排序。
|
||||
</p>
|
||||
<div v-if="images.length" class="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<article v-for="image in images" :key="image.id" class="min-w-0 rounded border border-line p-3">
|
||||
<AssetImage
|
||||
:src="image.imageUrl"
|
||||
:alt="`${categories[image.category]}风格参考图`"
|
||||
class="aspect-square"
|
||||
/>
|
||||
<p class="mt-3 text-xs">
|
||||
{{ categories[image.category] }} · {{ image.enabled ? '已启用' : '已停用' }} · 排序
|
||||
{{ image.sortOrder }}
|
||||
</p>
|
||||
<p v-if="image.provider || image.model" class="mt-2 break-words text-[11px] text-muted">
|
||||
{{ image.provider }} {{ image.model }}
|
||||
</p>
|
||||
<details v-if="image.prompt" class="mt-2 text-xs">
|
||||
<summary>实际提示词</summary>
|
||||
<p class="mt-2 whitespace-pre-wrap">{{ image.prompt }}</p>
|
||||
</details>
|
||||
<div class="mt-3 flex flex-wrap gap-3">
|
||||
<a
|
||||
v-if="referenceImageUrl(image.imageUrl)"
|
||||
:href="referenceImageUrl(image.imageUrl)!"
|
||||
class="text-button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>原图</a
|
||||
>
|
||||
<button class="text-button" :disabled="disabled" @click="emit('toggle', image)">
|
||||
{{ image.enabled ? '停用' : '启用' }}
|
||||
</button>
|
||||
<button class="text-button text-danger" :disabled="disabled" @click="confirmingId = image.id">
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="confirmingId === image.id" class="mt-3 text-xs">
|
||||
<p>仅移除参考图记录,不删除远程文件。</p>
|
||||
<div class="mt-2 flex gap-3">
|
||||
<button class="text-button text-danger" :disabled="disabled" @click="remove(image)">
|
||||
确认移除</button
|
||||
><button class="text-button" @click="confirmingId = ''">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="mt-5 text-sm text-muted">尚无风格参考图。先保存视觉风格,再登记图片。</p>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
/** 视觉风格模块公共入口。 */
|
||||
export { visualStyleApi } from './api'
|
||||
export type { VisualStyle, VisualStyleImage, StyleCategory, SaveVisualStyleInput } from './types'
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { VisualStyle, VisualStyleImage } from '../types'
|
||||
|
||||
/** 仅供测试使用的项目视觉风格,与后端持久化返回字段一致。 */
|
||||
export function styleFixture(overrides: Partial<VisualStyle> = {}): VisualStyle {
|
||||
return {
|
||||
id: 'style-db-1',
|
||||
projectId: 'page-test-project',
|
||||
name: '都市悬疑真人短剧',
|
||||
prompt: '真人写实,冷暖对比',
|
||||
characterPrompt: '真实皮肤质感',
|
||||
scenePrompt: '现代城市',
|
||||
propPrompt: '真实材质',
|
||||
hardConstraints: ['禁止二次元'],
|
||||
isLocked: false,
|
||||
images: [],
|
||||
createdAt: '2026-08-28T00:00:00Z',
|
||||
updatedAt: '2026-08-28T00:00:00Z',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/** 风格图记录不包含文件上传或模型调用。 */
|
||||
export function styleImageFixture(overrides: Partial<VisualStyleImage> = {}): VisualStyleImage {
|
||||
return {
|
||||
id: 'style-image-1',
|
||||
visualStyleId: 'style-db-1',
|
||||
category: 'overall',
|
||||
source: 'upload',
|
||||
imageUrl: '/storage/style.png',
|
||||
enabled: true,
|
||||
sortOrder: 0,
|
||||
provider: null,
|
||||
model: null,
|
||||
prompt: null,
|
||||
createdAt: '2026-08-28T00:00:00Z',
|
||||
updatedAt: '2026-08-28T00:00:00Z',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/** 风格参考图分类,与后端枚举保持一致。 */
|
||||
export type StyleCategory = 'overall' | 'character' | 'scene' | 'prop'
|
||||
|
||||
/** 已登记的风格参考图;这里只登记地址,不代表前端上传了文件。 */
|
||||
export interface VisualStyleImage {
|
||||
id: string
|
||||
visualStyleId: string
|
||||
category: StyleCategory
|
||||
source: 'upload' | 'generated'
|
||||
imageUrl: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
provider: string | null
|
||||
model: string | null
|
||||
prompt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** 项目统一视觉风格;hardConstraints 保留后端 JSON,避免静默丢弃旧结构。 */
|
||||
export interface VisualStyle {
|
||||
id: string
|
||||
projectId: string
|
||||
name: string
|
||||
prompt: string | null
|
||||
characterPrompt: string | null
|
||||
scenePrompt: string | null
|
||||
propPrompt: string | null
|
||||
hardConstraints: unknown
|
||||
isLocked: boolean
|
||||
images: VisualStyleImage[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** 人工保存只发送本次可编辑字段,锁定仍允许用户主动保存。 */
|
||||
export type SaveVisualStyleInput = Partial<
|
||||
Pick<
|
||||
VisualStyle,
|
||||
'name' | 'prompt' | 'characterPrompt' | 'scenePrompt' | 'propPrompt' | 'hardConstraints' | 'isLocked'
|
||||
>
|
||||
>
|
||||
|
||||
/** 新增参考图使用已有地址,允许指定分类、启用状态及展示顺序。 */
|
||||
export interface AddStyleImageInput {
|
||||
category: StyleCategory
|
||||
source: 'upload'
|
||||
imageUrl: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
}
|
||||
Reference in New Issue
Block a user