refactor: 使用 Naive UI 重构后台布局与黑白双主题
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user