feat: 同步精简接口并统一表单校验
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import AppForm from '../../../components/ui/AppForm.vue'
|
||||
import { NFormItem } from 'naive-ui'
|
||||
import { stringArrayJsonRule } from '../../../lib/form-rules'
|
||||
import DetailDisclosure from '../../../components/ui/DetailDisclosure.vue'
|
||||
import { NButton, NCheckbox, NInput, NTag } from 'naive-ui'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
@@ -18,16 +21,6 @@ const form = reactive({
|
||||
})
|
||||
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.visualStyle,
|
||||
value => {
|
||||
@@ -49,7 +42,7 @@ watch(dirty, value => emit('dirty', value), { immediate: true })
|
||||
|
||||
/** 显式校验 JSON 后保存结构化约束;不对错误结构做静默转换。 */
|
||||
function save() {
|
||||
if (props.disabled || constraintsError.value) return
|
||||
if (props.disabled) return
|
||||
emit('save', {
|
||||
name: form.name.trim(),
|
||||
prompt: form.prompt,
|
||||
@@ -60,21 +53,28 @@ function save() {
|
||||
isLocked: form.isLocked
|
||||
})
|
||||
}
|
||||
const rules = { constraints: stringArrayJsonRule }
|
||||
const constraintsDetails = ref<InstanceType<typeof DetailDisclosure> | null>(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="panel p-5 sm:p-6" @submit.prevent="save">
|
||||
<AppForm
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:disabled="disabled"
|
||||
class="panel p-5 sm:p-6"
|
||||
@invalid="constraintsDetails?.expand()"
|
||||
@submit="save"
|
||||
>
|
||||
<fieldset :disabled="disabled" class="space-y-5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 class="font-medium">风格说明</h3>
|
||||
<NTag v-if="dirty" size="small" :bordered="false">有未保存修改</NTag>
|
||||
</div>
|
||||
<label class="block"
|
||||
><span class="field-label">风格名称</span
|
||||
<NFormItem class="block" path="name" label="风格名称" :label-props="{ for: 'style-name' }"
|
||||
><NInput :disabled="disabled" :input-props="{ id: 'style-name' }" v-model:value="form.name"></NInput
|
||||
></label>
|
||||
<label class="block"
|
||||
><span class="field-label">整体视觉语言</span
|
||||
></NFormItem>
|
||||
<NFormItem class="block" path="prompt" label="整体视觉语言" :label-props="{ for: 'style-prompt' }"
|
||||
><NInput
|
||||
:disabled="disabled"
|
||||
placeholder="媒介、真实感、色彩和整体美术方向"
|
||||
@@ -84,12 +84,12 @@ function save() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<DetailDisclosure title="分类风格与硬约束"
|
||||
</NFormItem>
|
||||
<DetailDisclosure ref="constraintsDetails" display-directive="show" title="分类风格与硬约束"
|
||||
><div class="grid gap-5 lg:grid-cols-3">
|
||||
<label
|
||||
><span class="field-label">人物风格与选角背景</span
|
||||
<NFormItem path="characterPrompt" label="人物风格与选角背景"
|
||||
><NInput
|
||||
:input-props="{ 'aria-label': '人物风格与选角背景' }"
|
||||
:disabled="disabled"
|
||||
placeholder="人物质感、妆造与默认选角背景;留空时 AI 默认采用中国人物基线"
|
||||
class="min-h-32"
|
||||
@@ -97,30 +97,33 @@ function save() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<label
|
||||
><span class="field-label">场景风格补充</span
|
||||
</NFormItem>
|
||||
<NFormItem path="scenePrompt" label="场景风格补充"
|
||||
><NInput
|
||||
:input-props="{ 'aria-label': '场景风格补充' }"
|
||||
:disabled="disabled"
|
||||
class="min-h-32"
|
||||
v-model:value="form.scenePrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
<label
|
||||
><span class="field-label">道具风格补充</span
|
||||
</NFormItem>
|
||||
<NFormItem path="propPrompt" label="道具风格补充"
|
||||
><NInput
|
||||
:input-props="{ 'aria-label': '道具风格补充' }"
|
||||
:disabled="disabled"
|
||||
class="min-h-32"
|
||||
v-model:value="form.propPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<label class="mt-4 block"
|
||||
><span class="field-label">硬约束 · JSON 字符串数组</span
|
||||
<NFormItem
|
||||
class="mt-4 block"
|
||||
path="constraints"
|
||||
label="硬约束 · JSON 字符串数组"
|
||||
:label-props="{ for: 'style-constraints' }"
|
||||
><NInput
|
||||
:disabled="disabled"
|
||||
spellcheck="false"
|
||||
@@ -130,9 +133,8 @@ function save() {
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||
></NInput>
|
||||
</label>
|
||||
</NFormItem>
|
||||
</DetailDisclosure>
|
||||
<p v-if="constraintsError" class="text-xs text-danger" role="alert">{{ constraintsError }}</p>
|
||||
<NCheckbox
|
||||
:disabled="disabled"
|
||||
id="style-lock"
|
||||
@@ -144,10 +146,8 @@ function save() {
|
||||
锁定后仍可人工编辑并保存。取消勾选并保存后,才能再次使用 AI 重生成。
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<NButton :disabled="disabled || !!constraintsError" type="primary" attr-type="submit">
|
||||
保存视觉风格
|
||||
</NButton>
|
||||
<NButton :disabled="disabled" type="primary" attr-type="submit"> 保存视觉风格 </NButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</AppForm>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCheckbox, NCollapse, NCollapseItem, NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import AppForm from '../../../components/ui/AppForm.vue'
|
||||
import { NFormItem } from 'naive-ui'
|
||||
import { fieldRule, imageUrlRule } from '../../../lib/form-rules'
|
||||
import { NButton, NCheckbox, NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { AssetImage } from '../../../components/ui'
|
||||
import { referenceImageUrl } from '../../../lib/assets'
|
||||
import type { AddStyleImageInput, StyleCategory, VisualStyleImage } from '../types'
|
||||
@@ -15,11 +18,10 @@ const emit = defineEmits<{
|
||||
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
|
||||
if (props.disabled) return
|
||||
emit('add', { ...form, imageUrl: form.imageUrl.trim(), source: 'upload' })
|
||||
}
|
||||
|
||||
@@ -29,6 +31,7 @@ function remove(image: VisualStyleImage) {
|
||||
confirmingId.value = ''
|
||||
emit('remove', image)
|
||||
}
|
||||
const rules = { imageUrl: imageUrlRule, sortOrder: fieldRule(Number.isSafeInteger, '排序须为整数') }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,19 +43,18 @@ function remove(image: VisualStyleImage) {
|
||||
登记已有 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
|
||||
<AppForm :model="form" :rules="rules" :disabled="disabled" class="mt-4" @submit="add">
|
||||
<fieldset :disabled="disabled" class="form-controls flex flex-wrap gap-3">
|
||||
<NFormItem class="min-w-48 flex-1" path="imageUrl" label="图片地址"
|
||||
><NInput
|
||||
placeholder="https://… 或 /storage/…"
|
||||
:input-props="{ 'aria-label': '风格图片地址' }"
|
||||
v-model:value="form.imageUrl"
|
||||
></NInput
|
||||
></label>
|
||||
<label
|
||||
><span class="field-label">分类</span
|
||||
></NFormItem>
|
||||
<NFormItem path="category" label="分类" :label-props="{ for: 'style-image-category' }"
|
||||
><NSelect
|
||||
id="style-image-category"
|
||||
v-model:value="form.category"
|
||||
:options="[
|
||||
...Object.entries(categories).map(([value, label]) => ({
|
||||
@@ -62,23 +64,19 @@ function remove(image: VisualStyleImage) {
|
||||
]"
|
||||
class="select-control"
|
||||
></NSelect
|
||||
></label>
|
||||
<label class="w-24"
|
||||
><span class="field-label">排序</span
|
||||
></NFormItem>
|
||||
<NFormItem class="w-24" path="sortOrder" label="排序"
|
||||
><NInputNumber
|
||||
:input-props="{ 'aria-label': '风格图片排序' }"
|
||||
:value="typeof form.sortOrder === 'number' ? form.sortOrder : null"
|
||||
@update:value="form.sortOrder = $event ?? 0"
|
||||
:step="1"
|
||||
></NInputNumber
|
||||
></label>
|
||||
<NCheckbox v-model:checked="form.enabled" class="flex gap-2 pb-2 text-xs">启用</NCheckbox>
|
||||
<NButton :disabled="disabled || !valid" attr-type="submit">登记参考图</NButton>
|
||||
></NFormItem>
|
||||
<NCheckbox v-model:checked="form.enabled" class="control-row-checkbox text-xs">启用</NCheckbox>
|
||||
<NButton :disabled="disabled" attr-type="submit">登记参考图</NButton>
|
||||
</fieldset>
|
||||
</form>
|
||||
<p v-if="form.imageUrl && !valid" class="mt-2 text-xs text-danger" role="alert">
|
||||
请填写有效图片地址和整数排序。
|
||||
</p>
|
||||
</AppForm>
|
||||
<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="surface-inset min-w-0 p-3">
|
||||
<AssetImage
|
||||
@@ -94,12 +92,6 @@ function remove(image: VisualStyleImage) {
|
||||
<p v-if="image.provider || image.model" class="mt-2 break-words text-[11px] text-muted">
|
||||
{{ image.provider }} {{ image.model }}
|
||||
</p>
|
||||
<NCollapse v-if="image.prompt" class="mt-2 text-xs"
|
||||
><NCollapseItem name="details"
|
||||
><template #header>实际提示词</template>
|
||||
<p class="mt-2 whitespace-pre-wrap">{{ image.prompt }}</p></NCollapseItem
|
||||
></NCollapse
|
||||
>
|
||||
<div class="mt-3 flex flex-wrap gap-3">
|
||||
<a
|
||||
v-if="referenceImageUrl(image.imageUrl)"
|
||||
|
||||
@@ -31,7 +31,6 @@ export function styleImageFixture(overrides: Partial<VisualStyleImage> = {}): Vi
|
||||
sortOrder: 0,
|
||||
provider: null,
|
||||
model: null,
|
||||
prompt: null,
|
||||
createdAt: '2026-08-28T00:00:00Z',
|
||||
updatedAt: '2026-08-28T00:00:00Z',
|
||||
...overrides
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface VisualStyleImage {
|
||||
sortOrder: number
|
||||
provider: string | null
|
||||
model: string | null
|
||||
prompt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user