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
+27 -24
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import { NAlert, NButton, NTag } from 'naive-ui'
import { computed, ref } from 'vue'
import { usePolling } from '../../composables/usePolling'
import { runOperation } from '../workflows/operations'
@@ -87,18 +89,22 @@ function removeImage(image: VisualStyleImage) {
</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>
<WorkspacePage
><template #header
><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></template
>
<div class="my-5 flex flex-wrap items-center gap-3">
<span class="tag">{{ style ? (style.isLocked ? '已锁定' : '未锁定') : '尚未创建' }}</span>
<NTag size="small" :bordered="false">{{
style ? (style.isLocked ? '已锁定' : '未锁定') : '尚未创建'
}}</NTag>
<ConfirmAction
:label="style ? 'AI 重新生成风格' : 'AI 生成风格'"
:disabled="disabled || !!style?.isLocked || dirty"
@@ -106,26 +112,24 @@ function removeImage(image: VisualStyleImage) {
description="调用文本模型生成整体及分类风格。重新生成会覆盖已保存的文本与硬约束;旧身份、旧图片和已有提示词不会自动更新。"
@confirm="generate"
/>
<button class="text-button" :disabled="query.loading.value" @click="query.refresh">刷新风格</button>
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small">刷新风格</NButton>
</div>
<p v-if="query.error.value" class="alert alert-error mb-4" role="alert">
{{ query.error.value }} 请确认后端 dev 已更新并重启查询失败不等同于尚未创建
</p>
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mb-4"
>{{ query.error.value }} 请确认后端 dev 已更新并重启查询失败不等同于尚未创建
</NAlert>
<p v-if="dirty" class="mb-4 text-xs text-muted">
有未保存修改AI 重生成暂不可用自动刷新不会覆盖编辑内容离开页面不会自动保存
</p>
<p class="alert mb-5 text-xs">
<NAlert type="info" :show-icon="false" class="mb-5 text-xs">
AI
生成时若项目和世界观没有明确其它人物背景人物风格默认采用中国人物选角基线人工填写的人物背景和角色事实优先修改风格只影响之后的生成旧资产不会自动更新
</p>
<StyleEditor
生成时若项目和世界观没有明确其它人物背景人物风格默认采用中国人物选角基线人工填写的人物背景和角色事实优先修改风格只影响之后的生成旧资产不会自动更新 </NAlert
><StyleEditor
v-if="query.data.value"
:key="editorRevision"
:style="style"
:visual-style="style"
:disabled="disabled"
@save="save"
@dirty="dirty = $event"
/>
@dirty="dirty = $event" />
<p v-else-if="query.loading.value" class="py-8 text-sm text-muted" role="status">正在读取视觉风格</p>
<StyleImages
:key="imageRevision"
@@ -134,6 +138,5 @@ function removeImage(image: VisualStyleImage) {
@add="addImage"
@toggle="toggleImage"
@remove="removeImage"
/>
</section>
/></WorkspacePage>
</template>
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { NButton, NCheckbox, NInput, NTag } from 'naive-ui'
import { computed, reactive, ref, watch } from 'vue'
import type { SaveVisualStyleInput, VisualStyle } from '../types'
/** 编辑草稿独立于轮询结果,刷新不会覆盖尚未保存的输入。 */
const props = defineProps<{ style: VisualStyle | null; disabled: boolean }>()
const props = defineProps<{ visualStyle: VisualStyle | null; disabled: boolean }>()
const emit = defineEmits<{ save: [input: SaveVisualStyleInput]; dirty: [value: boolean] }>()
const form = reactive({
name: '',
@@ -27,7 +28,7 @@ const constraintsError = computed(() => {
}
})
watch(
() => props.style,
() => props.visualStyle,
value => {
if (baseline.value && dirty.value) return
Object.assign(form, {
@@ -65,63 +66,84 @@ function 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>
<NTag v-if="dirty" size="small" :bordered="false">有未保存修改</NTag>
</div>
<label class="block"
><span class="field-label">风格名称</span><input id="style-name" v-model="form.name" class="input"
/></label>
><span class="field-label">风格名称</span
><NInput :disabled="disabled" :input-props="{ id: 'style-name' }" v-model:value="form.name"></NInput
></label>
<label class="block"
><span class="field-label">整体视觉语言</span
><textarea
id="style-prompt"
v-model="form.prompt"
class="input min-h-32"
><NInput
:disabled="disabled"
placeholder="媒介、真实感、色彩和整体美术方向"
/>
:input-props="{ id: 'style-prompt' }"
class="min-h-32"
v-model:value="form.prompt"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</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"
><NInput
:disabled="disabled"
placeholder="人物质感、妆造与默认选角背景;留空时 AI 默认采用中国人物基线"
/>
class="min-h-32"
v-model:value="form.characterPrompt"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</label>
<label
><span class="field-label">场景风格补充</span
><textarea v-model="form.scenePrompt" class="input min-h-32" />
><NInput
: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
><textarea v-model="form.propPrompt" class="input min-h-32" />
><NInput
:disabled="disabled"
class="min-h-32"
v-model:value="form.propPrompt"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</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"
><NInput
:disabled="disabled"
spellcheck="false"
/>
:input-props="{ id: 'style-constraints' }"
class="min-h-24 font-mono text-xs"
v-model:value="form.constraints"
type="textarea"
:autosize="{ minRows: 3, maxRows: 12 }"
></NInput>
</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
<NCheckbox
:disabled="disabled"
id="style-lock"
v-model:checked="form.isLocked"
class="flex items-start gap-2 text-sm"
>确认并锁定风格阻止后续 AI 自动覆盖</NCheckbox
>
<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">
<NButton :disabled="disabled || !!constraintsError" type="primary" attr-type="submit">
保存视觉风格
</button>
</NButton>
</div>
</fieldset>
</form>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NButton, NCheckbox, NCollapse, NCollapseItem, NInput, NInputNumber, NSelect } from 'naive-ui'
import { computed, reactive, ref } from 'vue'
import { AssetImage } from '../../../components/ui'
import { referenceImageUrl } from '../../../lib/assets'
@@ -43,29 +44,36 @@ function remove(image: VisualStyleImage) {
<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"
><NInput
placeholder="https://… 或 /storage/…"
aria-label="风格图片地址"
/></label>
:input-props="{ 'aria-label': '风格图片地址' }"
v-model:value="form.imageUrl"
></NInput
></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
>
><NSelect
v-model:value="form.category"
:options="[
...Object.entries(categories).map(([value, label]) => ({
label: String(label),
value: value
}))
]"
class="select-control"
></NSelect
></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>
><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>
</fieldset>
</form>
<p v-if="form.imageUrl && !valid" class="mt-2 text-xs text-danger" role="alert">
@@ -85,10 +93,12 @@ 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>
<details v-if="image.prompt" class="mt-2 text-xs">
<summary>实际提示词</summary>
<p class="mt-2 whitespace-pre-wrap">{{ image.prompt }}</p>
</details>
<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)"
@@ -98,19 +108,25 @@ function remove(image: VisualStyleImage) {
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">
<NButton :disabled="disabled" @click="emit('toggle', image)" text size="small">{{
image.enabled ? '停用' : '启用'
}}</NButton>
<NButton
:disabled="disabled"
@click="confirmingId = image.id"
text
size="small"
class="text-danger"
>
移除
</button>
</NButton>
</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>
<NButton :disabled="disabled" @click="remove(image)" text size="small" class="text-danger">
确认移除</NButton
><NButton @click="confirmingId = ''" text size="small">取消</NButton>
</div>
</div>
</article>