refactor: 使用 Naive UI 重构后台布局与黑白双主题

This commit is contained in:
GouJ
2026-09-01 16:31:21 +08:00
parent 55516537bf
commit db8c176b55
51 changed files with 3277 additions and 2442 deletions
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NCheckbox, NInput, NInputNumber } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { AppDialog } from '../../../components/ui'
import { validImageSize } from '../../subject-images/model'
@@ -38,18 +39,20 @@ function submit() {
<template>
<AppDialog v-model:open="open" title="生成角色选角候选" :description="`${subjectName} · 候选不会自动成为身份母版`">
<form class="mt-5 space-y-4" @submit.prevent="submit">
<p class="alert text-xs">
<NAlert type="info" :show-icon="false" class="text-xs">
后端会依据角色 Identity项目视觉风格和默认选角约束独立生成一张 primary
候选不会复用当前身份母版生成完成后仍需人工点击确认选角
</p>
</NAlert>
<label class="block">
<span class="field-label">覆盖本次完整提示词可选</span>
<textarea
id="casting-candidate-prompt"
v-model="prompt"
class="input min-h-24"
<NInput
placeholder="建议留空,使用后端编译的稳定身份和选角约束;未明确人物背景时默认中国人物"
/>
:input-props="{ id: 'casting-candidate-prompt' }"
class="min-h-24"
v-model:value="prompt"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</label>
<p v-if="prompt.trim()" class="text-xs text-danger">
自定义文本会替换后端完整提示词请自行包含人物身份基础服装和中性参考图要求
@@ -57,27 +60,35 @@ function submit() {
<div class="grid grid-cols-2 gap-4">
<label
><span class="field-label">宽度px</span
><input v-model.number="width" class="input" type="number" min="1" placeholder="后端默认"
/></label>
><NInputNumber
placeholder="后端默认"
:value="typeof width === 'number' ? width : null"
@update:value="width = $event ?? ''"
:min="1"
></NInputNumber
></label>
<label
><span class="field-label">高度px</span
><input v-model.number="height" class="input" type="number" min="1" placeholder="后端默认"
/></label>
><NInputNumber
placeholder="后端默认"
:value="typeof height === 'number' ? height : null"
@update:value="height = $event ?? ''"
:min="1"
></NInputNumber
></label>
</div>
<p v-if="!valid" class="text-xs text-danger">宽高需要同时留空或同时填写正整数</p>
<label class="flex items-start gap-2 text-xs leading-6">
<input
id="casting-candidate-cost"
v-model="acknowledged"
type="checkbox"
class="mt-1.5 accent-accent"
/>我已确认调用 Seedream 可能产生费用且当前没有同项目生成任务
</label>
<NCheckbox
id="casting-candidate-cost"
v-model:checked="acknowledged"
class="flex items-start gap-2 text-xs leading-6"
>我已确认调用 Seedream 可能产生费用且当前没有同项目生成任务
</NCheckbox>
<div class="dialog-footer">
<button type="button" class="button button-secondary" @click="open = false">取消</button>
<button type="submit" class="button button-primary" :disabled="disabled || !valid || !acknowledged">
<NButton @click="open = false">取消</NButton>
<NButton :disabled="disabled || !valid || !acknowledged" type="primary" attr-type="submit">
确认生成候选
</button>
</NButton>
</div>
</form>
</AppDialog>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NButton, NCheckbox, NInput } from 'naive-ui'
import { computed, reactive, ref, watch } from 'vue'
import type { SaveIdentityInput, SubjectIdentity } from '../types'
@@ -34,21 +35,27 @@ function save() {
<fieldset :disabled="disabled" class="space-y-4">
<label class="block"
><span class="field-label">稳定身份描述</span
><textarea
id="identity-description"
v-model="form.description"
class="input min-h-28"
><NInput
:disabled="disabled"
placeholder="只描述跨形态不变的面部、结构、材质等特征"
/>
:input-props="{ id: 'identity-description' }"
class="min-h-28"
v-model:value="form.description"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</label>
<label class="block"
><span class="field-label">身份核心提示词稳定事实</span
><textarea
id="identity-prompt"
v-model="form.generationPrompt"
class="input min-h-36"
><NInput
:disabled="disabled"
placeholder="不固定某一形态的服装、伤势或临时状态"
/>
:input-props="{ id: 'identity-prompt' }"
class="min-h-36"
v-model:value="form.generationPrompt"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</label>
<p class="text-xs leading-6 text-muted">
{{
@@ -60,18 +67,22 @@ function save() {
}}
这里保存身份事实背景视角和参考图约束由后端另行编译不是最终整段生图 Prompt
</p>
<label class="flex items-start gap-2 text-xs leading-6"
><input id="identity-lock" v-model="form.isLocked" type="checkbox" class="mt-1.5 accent-accent" />{{
<NCheckbox
:disabled="disabled"
id="identity-lock"
v-model:checked="form.isLocked"
class="flex items-start gap-2 text-xs leading-6"
>{{
module === 'character'
? '确认并锁定演员身份。Character 必须同时具有身份母版并锁定 Identity,首帧才能进入就绪状态。'
: '确认并锁定身份文本。锁定后仍可人工保存、生成图片及切换母版。'
}}</label
}}</NCheckbox
>
<div class="flex flex-wrap items-center justify-between gap-3">
<span class="text-xs text-muted">{{
dirty ? '有未保存修改,自动刷新不会覆盖草稿。' : '图片以已保存的身份提示词为准。'
}}</span
><button class="button button-primary" type="submit" :disabled="disabled">保存主体身份</button>
><NButton :disabled="disabled" type="primary" attr-type="submit">保存主体身份</NButton>
</div>
</fieldset>
</form>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NCollapse, NCollapseItem, NTag } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { AssetImage, StatusBadge } from '../../../components/ui'
import { referenceImageUrl } from '../../../lib/assets'
@@ -49,14 +50,14 @@ function chooseAnchor() {
<section class="mt-5 border-t border-line pt-5" aria-label="身份参考图库">
<div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-medium">身份参考图 · {{ images.length }}</h3>
<span class="tag">{{ anchor ? '已选母版' : '尚无母版' }}</span>
<NTag size="small" :bordered="false">{{ anchor ? '已选母版' : '尚无母版' }}</NTag>
</div>
<p class="mt-2 text-xs leading-6 text-muted">
身份母版确定是谁形态主图确定该造型长什么样切换母版仅影响之后的生成不会替换已有形态图分镜参考图或提示词
</p>
<p v-if="character && anchor" class="alert mt-3 text-xs leading-6">
<NAlert v-if="character && anchor" type="info" :show-icon="false" class="mt-3 text-xs leading-6">
如果要更换演员身份请生成新的选角候选并确认选角正面三分之四全身等辅助身份图会继续引用当前母版只用于保持同一演员不会重新选择人物
</p>
</NAlert>
<template v-if="selected">
<AssetImage
:src="selected.status === 'completed' ? selected.imageUrl : null"
@@ -66,13 +67,13 @@ function chooseAnchor() {
/>
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap items-center gap-2">
<StatusBadge :status="selected.status" /><span class="tag">{{
<StatusBadge :status="selected.status" /><NTag size="small" :bordered="false">{{
selected.isAnchor
? '当前身份母版'
: selected.viewType === 'primary'
? '母版候选'
: identityViewLabels[selected.viewType]
}}</span
}}</NTag
><span class="text-xs text-muted"
>{{ selected.enabled ? '启用' : '停用' }} · {{ selected.width || '—' }} ×
{{ selected.height || '—' }}</span
@@ -87,15 +88,15 @@ function chooseAnchor() {
>打开原图</a
>
</div>
<p v-if="selected.error" class="alert alert-error mt-3" role="alert">{{ selected.error }}</p>
<NAlert v-if="selected.error" role="alert" type="error" :show-icon="false" class="mt-3">{{
selected.error
}}</NAlert>
<div class="mt-3 flex flex-wrap items-center gap-3">
<button
<NButton
v-if="!selected.isAnchor || (character && !identityLocked)"
class="button button-secondary"
:disabled="!canSelect"
@click="confirming = true"
>
{{ character ? '确认选角并锁定' : '设为身份母版' }}</button
>{{ character ? '确认选角并锁定' : '设为身份母版' }}</NButton
><span class="text-[11px] text-muted"
>{{ selected.provider }} · {{ selected.model }} · {{ formatDate(selected.createdAt) }}</span
>
@@ -103,8 +104,8 @@ function chooseAnchor() {
<p v-if="selected.viewType !== 'primary'" class="mt-2 text-xs text-muted">
辅助视图不能直接设为母版它会继承当前身份母版不适合用来更换演员
</p>
<div v-if="confirming" class="alert mt-3">
<p class="text-xs">
<NAlert v-if="confirming" type="info" :show-icon="false" class="mt-3"
><p class="text-xs">
{{
character
? '确认选择这张图片作为正式演员?后端会在同一事务中切换身份母版并锁定 Identity,完成后首帧才具备稳定人物身份。'
@@ -112,15 +113,18 @@ function chooseAnchor() {
}}
</p>
<div class="mt-3 flex gap-3">
<button class="button button-primary" :disabled="!canSelect" @click="chooseAnchor">
{{ character ? '确认演员选择' : '确认切换身份母版' }}</button
><button class="text-button" @click="confirming = false">取消</button>
</div>
</div>
<details v-if="selected.prompt" class="mt-4 text-xs">
<summary class="cursor-pointer text-muted">本次实际提示词</summary>
<p class="mt-3 whitespace-pre-wrap leading-6">{{ selected.prompt }}</p>
</details>
<NButton :disabled="!canSelect" @click="chooseAnchor" type="primary">{{
character ? '确认演员选择' : '确认切换身份母版'
}}</NButton
><NButton @click="confirming = false" text size="small">取消</NButton>
</div></NAlert
>
<NCollapse v-if="selected.prompt" class="mt-4 text-xs"
><NCollapseItem name="details"
><template #header>本次实际提示词</template>
<p class="mt-3 whitespace-pre-wrap leading-6">{{ selected.prompt }}</p></NCollapseItem
></NCollapse
>
<p
v-if="readImageProvenance(selected.rawJson).referenceImageId"
class="mt-3 break-all text-[11px] text-muted"
@@ -129,16 +133,15 @@ function chooseAnchor() {
</p>
<p class="mt-2 break-all font-mono text-[10px] text-muted">Identity image ID · {{ selected.id }}</p>
<div class="image-history mt-4" aria-label="身份图片历史">
<button
<NButton
v-for="image in images"
:key="image.id"
class="image-history-item"
:class="{ selected: image.id === selected.id }"
:aria-label="`查看身份图片 ${image.id}`"
:aria-pressed="image.id === selected.id"
@click="selectedId = image.id"
>
<AssetImage
class="image-history-item"
:class="{ selected: image.id === selected.id }"
><AssetImage
:src="image.status === 'completed' ? image.imageUrl : null"
:alt="identityViewLabels[image.viewType]"
:retryable="false"
@@ -149,8 +152,8 @@ function chooseAnchor() {
: image.viewType === 'primary'
? '母版候选'
: identityViewLabels[image.viewType]
}}</span>
</button>
}}</span></NButton
>
</div>
</template>
<p v-else class="py-8 text-sm text-muted">尚无身份参考图先保存身份提示词再生成第一张母版</p>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NCheckbox, NInput, NInputNumber, NSelect } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { AppDialog } from '../../../components/ui'
import { validImageSize } from '../../subject-images/model'
@@ -61,40 +62,50 @@ function submit() {
<template>
<AppDialog v-model:open="open" title="生成身份参考图" :description="subjectName">
<form class="mt-5 space-y-4" @submit.prevent="submit">
<p class="alert text-xs">
<NAlert type="info" :show-icon="false" class="text-xs">
调用 Seedream每次新增一张图片可能产生费用第一张成功的 primary 图自动成为母版已有母版时
primary 图仅作为停用候选保留
</p>
</NAlert>
<label class="block"
><span class="field-label">参考视角</span
><select id="identity-view" v-model="viewType" class="input">
<option v-for="[value, label] in allowedViews" :key="value" :value="value">
{{ label }}{{ value }}
</option>
</select></label
>
><NSelect
id="identity-view"
v-model:value="viewType"
:options="allowedViews.map(([value, label]) => ({ label: label + '' + value + '', value }))"
class="select-control"
></NSelect
></label>
<label class="block"
><span class="field-label">身份锚定来源</span
><select id="identity-reference" v-model="referenceImageId" class="input">
<option value="">
{{ anchor ? '自动引用当前身份母版' : '自动选择(当前无母版,将按文本生成)' }}
</option>
<option v-for="image in references" :key="image.id" :value="image.id">
{{ identityViewLabels[image.viewType] }} · {{ image.id }}
</option>
</select></label
>
><NSelect
id="identity-reference"
v-model:value="referenceImageId"
:options="[
{
label: String(anchor ? '自动引用当前身份母版' : '自动选择(当前无母版,将按文本生成)'),
value: ''
},
...references.map(image => ({
label: String(identityViewLabels[image.viewType] + ' · ' + image.id),
value: image.id
}))
]"
class="select-control"
></NSelect
></label>
<p class="text-xs leading-6 text-muted">
只能选择此主体已完成的图片后端需取得 Provider 可访问的远程地址本地预览可见不保证远程地址仍有效
</p>
<label class="block"
><span class="field-label">覆盖本次完整提示词可选</span
><textarea
id="identity-image-prompt"
v-model="prompt"
class="input min-h-24"
><NInput
placeholder="建议留空,使用后端编译的身份约束和视角要求"
/>
:input-props="{ id: 'identity-image-prompt' }"
class="min-h-24"
v-model:value="prompt"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</label>
<p v-if="prompt.trim()" class="text-xs text-danger">
自定义文本会替换后端编译的完整提示词请自行包含身份约束和视角要求
@@ -102,40 +113,38 @@ function submit() {
<div class="grid grid-cols-2 gap-4">
<label
><span class="field-label">宽度px</span
><input
id="identity-width"
v-model.number="width"
type="number"
min="1"
step="1"
class="input"
placeholder="后端默认" /></label
><NInputNumber
placeholder="后端默认"
:input-props="{ id: 'identity-width' }"
:value="typeof width === 'number' ? width : null"
@update:value="width = $event ?? ''"
:min="1"
:step="1"
></NInputNumber></label
><label
><span class="field-label">高度px</span
><input
id="identity-height"
v-model.number="height"
type="number"
min="1"
step="1"
class="input"
><NInputNumber
placeholder="后端默认"
/></label>
:input-props="{ id: 'identity-height' }"
:value="typeof height === 'number' ? height : null"
@update:value="height = $event ?? ''"
:min="1"
:step="1"
></NInputNumber
></label>
</div>
<p v-if="!valid" class="text-xs text-danger" role="alert">尺寸须成对填写正整数指定的参考图必须仍可用</p>
<label class="flex items-start gap-2 text-xs leading-6"
><input
id="identity-image-cost"
v-model="acknowledged"
type="checkbox"
class="mt-1.5 accent-accent"
/>我已确认后台没有同项目任务了解本次会调用模型并可能产生费用</label
<NCheckbox
id="identity-image-cost"
v-model:checked="acknowledged"
class="flex items-start gap-2 text-xs leading-6"
>我已确认后台没有同项目任务了解本次会调用模型并可能产生费用</NCheckbox
>
<div class="dialog-footer">
<button type="button" class="button button-secondary" @click="open = false">取消</button
><button class="button button-primary" type="submit" :disabled="disabled || !valid || !acknowledged">
<NButton @click="open = false">取消</NButton
><NButton :disabled="disabled || !valid || !acknowledged" type="primary" attr-type="submit">
确认生成身份图
</button>
</NButton>
</div>
</form>
</AppDialog>