feat: 新增项目资产库与形态参考素材
This commit is contained in:
@@ -154,6 +154,7 @@ function removeImage(image: VisualStyleImage) {
|
||||
<p v-else-if="query.loading.value" class="py-8 text-sm text-muted" role="status">正在读取视觉风格…</p>
|
||||
<StyleImages
|
||||
:key="imageRevision"
|
||||
:project-id="projectId"
|
||||
:images="style?.images ?? []"
|
||||
:disabled="disabled || !style"
|
||||
@add="addImage"
|
||||
|
||||
@@ -4,12 +4,15 @@ 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 { AppDialog, AssetImage } from '../../../components/ui'
|
||||
import { referenceImageUrl } from '../../../lib/assets'
|
||||
import { errorMessage } from '../../../lib/http'
|
||||
import { projectAssetsApi } from '../../project-assets/api'
|
||||
import type { ProjectAsset } from '../../project-assets/types'
|
||||
import type { AddStyleImageInput, StyleCategory, VisualStyleImage } from '../types'
|
||||
|
||||
/** 风格图只登记已有地址;不伪造文件上传或风格生图功能。 */
|
||||
const props = defineProps<{ images: VisualStyleImage[]; disabled: boolean }>()
|
||||
/** 风格图可关联项目资产,也保留外部地址兼容入口。 */
|
||||
const props = defineProps<{ projectId: string; images: VisualStyleImage[]; disabled: boolean }>()
|
||||
const emit = defineEmits<{
|
||||
add: [input: AddStyleImageInput]
|
||||
toggle: [image: VisualStyleImage]
|
||||
@@ -17,6 +20,11 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
const form = reactive({ imageUrl: '', category: 'overall' as StyleCategory, sortOrder: 0, enabled: true })
|
||||
const confirmingId = ref('')
|
||||
const assetPickerOpen = ref(false)
|
||||
const assetLoading = ref(false)
|
||||
const assetError = ref('')
|
||||
const assets = ref<ProjectAsset[] | null>(null)
|
||||
const selectedAssetId = ref('')
|
||||
const categories = { overall: '整体', character: '人物', scene: '场景', prop: '道具' } as const
|
||||
|
||||
/** 仅通过校验后发出新增请求,输入框在失败时保留供修正。 */
|
||||
@@ -25,6 +33,36 @@ function add() {
|
||||
emit('add', { ...form, imageUrl: form.imageUrl.trim(), source: 'upload' })
|
||||
}
|
||||
|
||||
/** 素材仅在用户打开选择器时读取,避免风格页首屏增加重复请求。 */
|
||||
async function openAssetPicker() {
|
||||
if (props.disabled) return
|
||||
assetPickerOpen.value = true
|
||||
assetError.value = ''
|
||||
selectedAssetId.value = ''
|
||||
assetLoading.value = true
|
||||
try {
|
||||
const rows = await projectAssetsApi.list(props.projectId)
|
||||
if (rows.some(asset => asset.projectId !== props.projectId)) throw new Error('素材与当前项目不匹配。')
|
||||
assets.value = rows.filter(asset => asset.type === 'image')
|
||||
} catch (error) {
|
||||
assetError.value = errorMessage(error)
|
||||
} finally {
|
||||
assetLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function addAsset() {
|
||||
if (props.disabled || !selectedAssetId.value) return
|
||||
emit('add', {
|
||||
projectAssetId: selectedAssetId.value,
|
||||
category: form.category,
|
||||
source: 'upload',
|
||||
enabled: form.enabled,
|
||||
sortOrder: form.sortOrder
|
||||
})
|
||||
assetPickerOpen.value = false
|
||||
}
|
||||
|
||||
/** 删除只移除记录,需确认且不声称删除远程文件。 */
|
||||
function remove(image: VisualStyleImage) {
|
||||
if (props.disabled || confirmingId.value !== image.id) return
|
||||
@@ -40,8 +78,8 @@ const rules = { imageUrl: imageUrlRule, sortOrder: fieldRule(Number.isSafeIntege
|
||||
风格参考图 <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/
|
||||
图片地址。后端尚无文件上传接口;这些图片目前仅管理记录,现有身份/形态生图不会自动将风格图片传给模型。
|
||||
优先从项目资产库选择,也可登记已有 HTTP(S) 或 /storage/
|
||||
图片地址。启用的参考图会参与后续身份与形态生图,已有图片不会自动更新。
|
||||
</p>
|
||||
<AppForm :model="form" :rules="rules" :disabled="disabled" class="mt-4" @submit="add">
|
||||
<fieldset :disabled="disabled" class="form-controls style-image-controls">
|
||||
@@ -74,7 +112,10 @@ const rules = { imageUrl: imageUrlRule, sortOrder: fieldRule(Number.isSafeIntege
|
||||
></NInputNumber
|
||||
></NFormItem>
|
||||
<NCheckbox v-model:checked="form.enabled" class="control-row-checkbox text-xs">启用</NCheckbox>
|
||||
<NButton class="style-image-submit" :disabled="disabled" attr-type="submit">登记参考图</NButton>
|
||||
<div class="style-image-submit flex gap-2">
|
||||
<NButton :disabled="disabled" @click="openAssetPicker">从资产库选择</NButton>
|
||||
<NButton :disabled="disabled" attr-type="submit">登记参考图</NButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
</AppForm>
|
||||
<div v-if="images.length" class="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
@@ -125,10 +166,46 @@ const rules = { imageUrl: imageUrlRule, sortOrder: fieldRule(Number.isSafeIntege
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="mt-5 text-sm text-muted">尚无风格参考图。先保存视觉风格,再登记图片。</p>
|
||||
|
||||
<AppDialog
|
||||
v-model:open="assetPickerOpen"
|
||||
title="从项目资产库选择"
|
||||
description="选择一张项目图片作为当前分类的风格参考。这里只创建引用,不复制或删除原素材。"
|
||||
wide
|
||||
>
|
||||
<p v-if="assetError" class="mt-4 text-sm text-danger">{{ assetError }}</p>
|
||||
<p v-else-if="assetLoading" class="mt-4 text-sm text-muted" role="status">正在读取项目素材…</p>
|
||||
<div v-else-if="assets?.length" class="style-asset-picker mt-4">
|
||||
<button
|
||||
v-for="asset in assets"
|
||||
:key="asset.id"
|
||||
type="button"
|
||||
class="style-asset-option"
|
||||
:class="{ 'is-selected': selectedAssetId === asset.id }"
|
||||
:aria-pressed="selectedAssetId === asset.id"
|
||||
@click="selectedAssetId = asset.id"
|
||||
>
|
||||
<AssetImage :src="asset.publicUrl" :alt="asset.name" class="aspect-[4/3]" />
|
||||
<span class="block truncate p-3 text-left text-xs">{{ asset.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="assets" class="mt-5 text-sm text-muted">
|
||||
资产库暂无图片。<RouterLink :to="`/projects/${projectId}/assets`" class="text-button"
|
||||
>前往资产库上传</RouterLink
|
||||
>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<NButton @click="assetPickerOpen = false">取消</NButton>
|
||||
<NButton type="primary" :disabled="!selectedAssetId || disabled" @click="addAsset"
|
||||
>登记为{{ categories[form.category] }}参考图</NButton
|
||||
>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@reference "../../../styles/styles.css";
|
||||
/* 按面板实际宽度布局,侧栏或移动端压缩内容时也能保持地址输入可用。 */
|
||||
.style-images-panel {
|
||||
container-type: inline-size;
|
||||
@@ -139,6 +216,18 @@ const rules = { imageUrl: imageUrlRule, sortOrder: fieldRule(Number.isSafeIntege
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.style-asset-picker {
|
||||
@apply grid grid-cols-[repeat(auto-fill,_minmax(min(180px,_100%),_1fr))] gap-3;
|
||||
}
|
||||
.style-asset-option {
|
||||
@apply min-w-0 overflow-hidden text-left bg-(--app-subtle) border border-transparent;
|
||||
}
|
||||
.style-asset-option:hover {
|
||||
@apply bg-(--app-control-hover);
|
||||
}
|
||||
.style-asset-option.is-selected {
|
||||
@apply border-(--color-accent);
|
||||
}
|
||||
.style-image-controls .select-control {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
@@ -40,11 +40,12 @@ export type SaveVisualStyleInput = Partial<
|
||||
>
|
||||
>
|
||||
|
||||
/** 新增参考图使用已有地址,允许指定分类、启用状态及展示顺序。 */
|
||||
/** 新增参考图可使用资产库正式 ID 或外部地址,允许指定分类、启用状态及展示顺序。 */
|
||||
export interface AddStyleImageInput {
|
||||
category: StyleCategory
|
||||
source: 'upload'
|
||||
imageUrl: string
|
||||
imageUrl?: string
|
||||
projectAssetId?: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user