fix: 对接资产库和优化布局
This commit is contained in:
parent
089ffca4f5
commit
aec9dc0152
|
|
@ -1,7 +1,99 @@
|
|||
<template>
|
||||
<div class="vg-compose-card">
|
||||
<!-- 左侧面板 - 根据模式动态渲染 -->
|
||||
<div class="vg-compose-left" v-if="!isTextToVideo">
|
||||
<div class="vg-compose-root">
|
||||
<div
|
||||
class="vg-compose-card"
|
||||
:class="{
|
||||
'vg-compose-card--reference': isReference,
|
||||
'vg-compose-card--split': !isReference && !isTextToVideo,
|
||||
'vg-compose-card--solo': isTextToVideo
|
||||
}">
|
||||
<!-- 参考图模式:四列 — 参考图 / 资产 / 选择区域 / 富文本 -->
|
||||
<template v-if="isReference">
|
||||
<div class="vg-compose-mod vg-compose-mod--ref">
|
||||
<div class="vg-mod-body">
|
||||
<div
|
||||
v-if="mediaList.length === 0"
|
||||
class="vg-compose-empty vg-compose-empty--compact"
|
||||
@click="onReferenceEmptyAreaClick">
|
||||
<div class="vg-compose-empty-icon" aria-hidden="true">+</div>
|
||||
<div class="vg-compose-empty-text">上传或从右侧选择素材</div>
|
||||
</div>
|
||||
<div v-else class="vg-compose-media-scroll vg-compose-media-scroll--vertical">
|
||||
<div
|
||||
v-for="item in mediaList"
|
||||
:key="item.id || item.url"
|
||||
class="vg-compose-media-item vg-compose-media-item--tile"
|
||||
:title="item.name || ''">
|
||||
<div class="vg-compose-media-preview">
|
||||
<img v-if="item.mediaType === 'image'" :src="item.url" alt="" />
|
||||
<video
|
||||
v-else-if="item.mediaType === 'video'"
|
||||
:src="item.url"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata" />
|
||||
<div v-else-if="item.mediaType === 'audio'" class="vg-audio-tile">
|
||||
<span class="vg-audio-tile-icon">♪</span>
|
||||
<span class="vg-audio-tile-text">音频</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="vg-compose-remove-btn" @click.stop="removeItem(item)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vg-mod-foot muted">{{ mediaList.length }}/{{ maxMediaCount }}</div>
|
||||
</div>
|
||||
|
||||
<div class="vg-compose-mod vg-compose-mod--asset">
|
||||
<div class="vg-mod-body vg-mod-body--asset">
|
||||
<a-select
|
||||
v-model="assetGroupId"
|
||||
class="vg-asset-group-input"
|
||||
placeholder="请选择素材组"
|
||||
allow-clear
|
||||
:loading="groupLoading || groupLoadingMore"
|
||||
:disabled="groupLoading && assetGroups.length === 0"
|
||||
@dropdown-reach-bottom="onAssetGroupDropdownReachBottom">
|
||||
<a-option v-for="g in assetGroups" :key="g.value" :value="g.value">
|
||||
{{ g.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<span
|
||||
class="vg-asset-btn-wrap"
|
||||
:title="!hasAssetGroupId ? '请先选择素材组' : ''">
|
||||
<mf-button
|
||||
class="vg-compose-left-upload vg-mod-btn"
|
||||
size="small"
|
||||
:disabled="!hasAssetGroupId"
|
||||
:loading="assetLoading"
|
||||
@click="loadAssetsByGroup">
|
||||
选择资产
|
||||
</mf-button>
|
||||
</span>
|
||||
<span
|
||||
class="vg-asset-btn-wrap"
|
||||
:title="!hasAssetGroupId ? '请先选择素材组' : ''">
|
||||
<mf-button
|
||||
class="vg-compose-left-upload vg-mod-btn"
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="!hasAssetGroupId"
|
||||
@click="openFilePicker">
|
||||
上传资产
|
||||
</mf-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vg-compose-mod vg-compose-mod--pick">
|
||||
<div class="vg-mod-body vg-mod-body--params">
|
||||
<slot name="reference-params"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 首帧 / 首尾帧:保留左侧素材列 -->
|
||||
<div class="vg-compose-left" v-else-if="!isTextToVideo">
|
||||
<!-- 首帧模式 -->
|
||||
<div v-if="isFirstFrame" class="vg-first-frame-panel">
|
||||
<div class="vg-panel-title">首帧图</div>
|
||||
|
|
@ -49,134 +141,54 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参考图模式 -->
|
||||
<div v-else-if="isReference" class="vg-reference-panel">
|
||||
<div class="vg-compose-left-head">
|
||||
<div class="vg-compose-left-title">
|
||||
参考素材({{ mediaList.length }}/{{ maxMediaCount }})
|
||||
</div>
|
||||
</div>
|
||||
<div class="vg-asset-controls">
|
||||
<select v-model="assetGroupId" class="vg-asset-group-input">
|
||||
<option value="">请选择素材组</option>
|
||||
<option v-for="g in assetGroups" :key="g.Id || g.id" :value="g.Id || g.id">
|
||||
{{ g.Name || g.name || g.Id || g.id }}
|
||||
</option>
|
||||
</select>
|
||||
<mf-button class="vg-compose-left-upload" size="small" @click="loadAssetGroups" :loading="groupLoading">
|
||||
刷新分组
|
||||
</mf-button>
|
||||
<mf-button class="vg-compose-left-upload" size="small" @click="loadAssetsByGroup" :loading="assetLoading">
|
||||
查询资产
|
||||
</mf-button>
|
||||
<mf-button class="vg-compose-left-upload" size="small" type="primary" @click="openFilePicker">
|
||||
上传资产
|
||||
</mf-button>
|
||||
</div>
|
||||
|
||||
<div v-if="mediaList.length === 0" class="vg-compose-empty" @click="openFilePicker">
|
||||
<div class="vg-compose-empty-icon" aria-hidden="true">+</div>
|
||||
<div class="vg-compose-empty-text">点击上传资产或按 GroupId 查询资产</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="vg-compose-media-scroll">
|
||||
<div
|
||||
v-for="(item, index) in mediaList"
|
||||
:key="item.id || item.url"
|
||||
class="vg-compose-media-item"
|
||||
:title="item.name || ''">
|
||||
<div class="vg-compose-media-preview">
|
||||
<img v-if="item.mediaType === 'image'" :src="item.url" alt="" />
|
||||
<video
|
||||
v-else-if="item.mediaType === 'video'"
|
||||
:src="item.url"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata" />
|
||||
<div v-else-if="item.mediaType === 'audio'" class="vg-audio-tile">
|
||||
<span class="vg-audio-tile-icon">♪</span>
|
||||
<span class="vg-audio-tile-text">音频</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-compose-remove-btn"
|
||||
@click.stop="removeItem(item)">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文生视频模式 - 左侧不显示素材 -->
|
||||
<div v-else class="vg-compose-left hidden">
|
||||
<!-- 文生视频无需参考素材 -->
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="assetPickerVisible"
|
||||
title="选择素材"
|
||||
:ok-button-props="{ disabled: !assetPickerSelectedKeys.length }"
|
||||
@ok="confirmPickAssets">
|
||||
<div class="vg-asset-picker">
|
||||
<div v-if="assetQueryResults.length === 0" class="vg-asset-picker-empty">当前分组暂无可用素材</div>
|
||||
<label v-for="it in assetQueryResults" :key="it.assetId || it.id" class="vg-asset-picker-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="String(it.assetId || it.id)"
|
||||
v-model="assetPickerSelectedKeys"
|
||||
/>
|
||||
<div class="vg-asset-picker-preview">
|
||||
<img v-if="it.mediaType === 'image'" :src="it.url" alt="" />
|
||||
<video v-else-if="it.mediaType === 'video'" :src="it.url" muted playsinline preload="metadata"></video>
|
||||
<div v-else class="vg-audio-tile">♪ 音频</div>
|
||||
</div>
|
||||
<div class="vg-asset-picker-name">{{ it.name || it.assetId }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<div class="vg-compose-right">
|
||||
<div class="vg-compose-right-head">
|
||||
<div class="vg-compose-right" :class="{ 'vg-compose-right--reference': isReference }">
|
||||
<div v-if="!isReference" class="vg-compose-right-head">
|
||||
<div class="vg-compose-right-title">描述画面与动态</div>
|
||||
<mf-button size="small" type="text" class="vg-compose-clear" @click="clearAll">
|
||||
清空
|
||||
</mf-button>
|
||||
</div>
|
||||
|
||||
<div class="vg-rich-editor-wrap">
|
||||
<div
|
||||
ref="editorRef"
|
||||
class="vg-compose-textarea vg-rich-editor"
|
||||
contenteditable="true"
|
||||
:data-placeholder="placeholder"
|
||||
@input="onEditorInput"
|
||||
@keydown="onEditorKeydown"
|
||||
@keyup="onEditorKeyup"
|
||||
@click="onEditorClick"></div>
|
||||
<div v-if="mentionVisible" class="vg-mention-panel">
|
||||
<div :class="['vg-compose-right-body', { 'vg-reference-editor-row': isReference }]">
|
||||
<div class="vg-rich-editor-wrap">
|
||||
<div
|
||||
v-for="(item, idx) in mentionCandidates"
|
||||
:key="item.key"
|
||||
:class="['vg-mention-item', { active: idx === mentionActiveIndex }]"
|
||||
@mousedown.prevent="selectMentionItem(item)">
|
||||
<img v-if="item.mediaType === 'image'" :src="item.url" alt="" class="vg-mention-thumb" />
|
||||
<video
|
||||
v-else-if="item.mediaType === 'video'"
|
||||
:src="item.url"
|
||||
class="vg-mention-thumb"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata" />
|
||||
<span v-else class="vg-mention-audio">{{ item.name || '音频' }}</span>
|
||||
ref="editorRef"
|
||||
class="vg-compose-textarea vg-rich-editor"
|
||||
contenteditable="true"
|
||||
:data-placeholder="placeholder"
|
||||
@input="onEditorInput"
|
||||
@keydown="onEditorKeydown"
|
||||
@keyup="onEditorKeyup"
|
||||
@click="onEditorClick"></div>
|
||||
<div v-if="mentionVisible" class="vg-mention-panel">
|
||||
<div
|
||||
v-for="(item, idx) in mentionCandidates"
|
||||
:key="item.key"
|
||||
:class="['vg-mention-item', { active: idx === mentionActiveIndex }]"
|
||||
@mousedown.prevent="selectMentionItem(item)">
|
||||
<img v-if="item.mediaType === 'image'" :src="item.url" alt="" class="vg-mention-thumb" />
|
||||
<video
|
||||
v-else-if="item.mediaType === 'video'"
|
||||
:src="item.url"
|
||||
class="vg-mention-thumb"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata" />
|
||||
<span v-else class="vg-mention-audio">{{ item.name || '音频' }}</span>
|
||||
</div>
|
||||
<div v-if="mentionCandidates.length === 0" class="vg-mention-empty">暂无可引用素材</div>
|
||||
</div>
|
||||
<div v-if="mentionCandidates.length === 0" class="vg-mention-empty">暂无可引用素材</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择区域和提交按钮将从父组件传入 -->
|
||||
<slot name="toolbar"></slot>
|
||||
<div v-if="isReference" class="vg-reference-actions-col">
|
||||
<slot name="toolbar"></slot>
|
||||
</div>
|
||||
<template v-else>
|
||||
<slot name="toolbar"></slot>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
|
|
@ -187,12 +199,56 @@
|
|||
:multiple="isReference"
|
||||
@change="handleSelectFiles" />
|
||||
</div>
|
||||
<a-modal
|
||||
v-model:visible="assetPickerVisible"
|
||||
title="选择素材"
|
||||
:ok-button-props="{ disabled: !assetPickerSelectedKeys.length }"
|
||||
@ok="confirmPickAssets">
|
||||
<div class="vg-asset-picker">
|
||||
<div v-if="assetQueryResults.length === 0" class="vg-asset-picker-empty">当前分组暂无可用素材</div>
|
||||
<label v-for="it in assetQueryResults" :key="it.assetId || it.id" class="vg-asset-picker-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="String(it.assetId || it.id)"
|
||||
v-model="assetPickerSelectedKeys"
|
||||
/>
|
||||
<div class="vg-asset-picker-preview">
|
||||
<img v-if="it.mediaType === 'image'" :src="it.url" alt="" />
|
||||
<video v-else-if="it.mediaType === 'video'" :src="it.url" muted playsinline preload="metadata"></video>
|
||||
<div v-else class="vg-audio-tile">♪ 音频</div>
|
||||
</div>
|
||||
<div class="vg-asset-picker-name">{{ it.name || it.assetId }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||
import request from '@/utils/request'
|
||||
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||
|
||||
const ASSET_GROUP_LIST_PAGE_SIZE = 20
|
||||
|
||||
const buildAssetGroupListBody = (pageNumber) => ({
|
||||
filter: { groupType: 'AIGC' },
|
||||
pageNumber,
|
||||
pageSize: ASSET_GROUP_LIST_PAGE_SIZE,
|
||||
sortBy: 'CreateTime',
|
||||
sortOrder: 'Desc'
|
||||
})
|
||||
|
||||
const normalizeAssetGroupOption = (g) => {
|
||||
const value = String(g?.Id ?? g?.id ?? '').trim()
|
||||
const name = String(g?.Name ?? g?.name ?? '').trim()
|
||||
return {
|
||||
value,
|
||||
label: name || '-'
|
||||
}
|
||||
}
|
||||
|
||||
/** 参考素材:@ 引用同一类(图/视频/音频)最多 9 种不同素材;左侧列表最多可插入 12 条(由 maxMediaCount 控制) */
|
||||
const MAX_REFERENCE_UNIQUE_KIND = 9
|
||||
|
|
@ -228,7 +284,6 @@ const emit = defineEmits(['update:modelValue', 'update:mediaList'])
|
|||
|
||||
const fileInputRef = ref(null)
|
||||
const editorRef = ref(null)
|
||||
const { proxy } = getCurrentInstance() || {}
|
||||
const localPrompt = ref(props.modelValue || '')
|
||||
const internalMediaList = ref(Array.isArray(props.mediaList) ? [...props.mediaList] : [])
|
||||
const currentUploadIndex = ref(-1) // for first/last frame mode
|
||||
|
|
@ -238,6 +293,9 @@ const mentionActiveIndex = ref(-1)
|
|||
const assetGroupId = ref('')
|
||||
const assetLoading = ref(false)
|
||||
const groupLoading = ref(false)
|
||||
const groupLoadingMore = ref(false)
|
||||
const assetGroupPageNumber = ref(0)
|
||||
const assetGroupHasMore = ref(false)
|
||||
const assetGroups = ref([])
|
||||
const assetPickerVisible = ref(false)
|
||||
const assetQueryResults = ref([])
|
||||
|
|
@ -263,25 +321,10 @@ watch(
|
|||
}
|
||||
)
|
||||
|
||||
// 参考图模式加载本地存储
|
||||
watch(() => props.videoMode, (newMode) => {
|
||||
mentionVisible.value = false
|
||||
if (newMode === 'image-reference') {
|
||||
const saved = loadReferenceFromStorage()
|
||||
if (saved.length > 0 && (!internalMediaList.value || internalMediaList.value.length === 0)) {
|
||||
internalMediaList.value = saved
|
||||
emit('update:mediaList', saved)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if (editorRef.value && localPrompt.value) {
|
||||
editorRef.value.innerText = localPrompt.value
|
||||
}
|
||||
if (isReference.value) {
|
||||
loadAssetGroups()
|
||||
}
|
||||
})
|
||||
|
||||
const mediaList = computed(() => internalMediaList.value)
|
||||
|
|
@ -308,6 +351,9 @@ const isFirstFrame = computed(() => props.videoMode === 'image-first-frame')
|
|||
const isFirstLastFrame = computed(() => props.videoMode === 'image-first-last-frame')
|
||||
const isReference = computed(() => props.videoMode === 'image-reference')
|
||||
|
||||
/** 参考图模式:已选素材组 ID(与素材管理页一致,非空才可选择/上传资产) */
|
||||
const hasAssetGroupId = computed(() => !!String(assetGroupId.value || '').trim())
|
||||
|
||||
const setPrompt = (next) => {
|
||||
localPrompt.value = next
|
||||
emit('update:modelValue', next)
|
||||
|
|
@ -318,7 +364,36 @@ const setMediaList = (next) => {
|
|||
emit('update:mediaList', next)
|
||||
}
|
||||
|
||||
/** 与 AssetManage.buildListPayload 结构一致,用于当前素材组下列出素材 */
|
||||
const buildReferenceListAssetsPayload = () => {
|
||||
const gid = String(assetGroupId.value || '').trim()
|
||||
const filter = {
|
||||
groupType: 'AIGC',
|
||||
groupIds: [gid],
|
||||
statuses: ['Active']
|
||||
}
|
||||
return {
|
||||
filter,
|
||||
pageNumber: 1,
|
||||
pageSize: 100,
|
||||
sortBy: 'CreateTime',
|
||||
sortOrder: 'Desc'
|
||||
}
|
||||
}
|
||||
|
||||
const onReferenceEmptyAreaClick = () => {
|
||||
if (!hasAssetGroupId.value) {
|
||||
Message.warning('请先选择素材组')
|
||||
return
|
||||
}
|
||||
openFilePicker()
|
||||
}
|
||||
|
||||
const openFilePicker = () => {
|
||||
if (isReference.value && !hasAssetGroupId.value) {
|
||||
Message.warning('请先选择素材组')
|
||||
return
|
||||
}
|
||||
currentUploadIndex.value = -1
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
|
@ -406,61 +481,90 @@ const mergeAssetsToMediaList = (assets) => {
|
|||
setMediaList(next.slice(0, props.maxMediaCount))
|
||||
}
|
||||
|
||||
const loadAssetGroups = async () => {
|
||||
if (!proxy?.$axios) return
|
||||
groupLoading.value = true
|
||||
const loadAssetGroups = async (reset = true) => {
|
||||
if (reset) {
|
||||
groupLoading.value = true
|
||||
assetGroupPageNumber.value = 0
|
||||
assetGroups.value = []
|
||||
assetGroupHasMore.value = false
|
||||
} else {
|
||||
if (!assetGroupHasMore.value || groupLoadingMore.value || groupLoading.value) return
|
||||
groupLoadingMore.value = true
|
||||
}
|
||||
const nextPage = reset ? 1 : assetGroupPageNumber.value + 1
|
||||
try {
|
||||
const res = await proxy.$axios({
|
||||
const res = await request({
|
||||
url: 'api/byteAssetGroup/listAssetGroups',
|
||||
method: 'POST',
|
||||
data: {
|
||||
Filter: { GroupType: 'AIGC' },
|
||||
PageNumber: 1,
|
||||
PageSize: 100,
|
||||
SortBy: 'CreateTime',
|
||||
SortOrder: 'Desc'
|
||||
}
|
||||
data: buildAssetGroupListBody(nextPage)
|
||||
})
|
||||
const rows = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
||||
assetGroups.value = rows
|
||||
if (!assetGroupId.value && rows.length) {
|
||||
assetGroupId.value = rows[0]?.Id || rows[0]?.id || ''
|
||||
const payload = res?.data
|
||||
const rawRows = byteApiItems(payload)
|
||||
const rows = rawRows.map(normalizeAssetGroupOption).filter((x) => x.value)
|
||||
const total = byteApiTotalCount(payload)
|
||||
if (reset) {
|
||||
assetGroups.value = rows
|
||||
} else {
|
||||
const seen = new Set(assetGroups.value.map((x) => x.value))
|
||||
for (const r of rows) {
|
||||
if (!seen.has(r.value)) {
|
||||
assetGroups.value.push(r)
|
||||
seen.add(r.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
assetGroupPageNumber.value = nextPage
|
||||
assetGroupHasMore.value = assetGroups.value.length < total && rows.length > 0
|
||||
if (!assetGroupId.value && assetGroups.value.length) {
|
||||
assetGroupId.value = assetGroups.value[0].value
|
||||
}
|
||||
} catch (err) {
|
||||
Message.error(err?.message || '加载素材组失败')
|
||||
} finally {
|
||||
groupLoading.value = false
|
||||
groupLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 须在 loadReferenceFromStorage / loadAssetGroups 定义之后注册:immediate 会同步执行回调,否则 const 未初始化会抛错,导致从不请求 listAssetGroups
|
||||
watch(
|
||||
() => props.videoMode,
|
||||
(newMode) => {
|
||||
mentionVisible.value = false
|
||||
if (newMode === 'image-reference') {
|
||||
const saved = loadReferenceFromStorage()
|
||||
if (saved.length > 0 && (!internalMediaList.value || internalMediaList.value.length === 0)) {
|
||||
internalMediaList.value = saved
|
||||
emit('update:mediaList', saved)
|
||||
}
|
||||
loadAssetGroups(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const onAssetGroupDropdownReachBottom = () => {
|
||||
loadAssetGroups(false)
|
||||
}
|
||||
|
||||
const loadAssetsByGroup = async () => {
|
||||
const gid = String(assetGroupId.value || '').trim()
|
||||
if (!gid) {
|
||||
Message.warning('请先填写 GroupId 再查询')
|
||||
return
|
||||
}
|
||||
if (!proxy?.$axios) {
|
||||
Message.error('查询失败:axios 未就绪')
|
||||
Message.warning('请先选择素材组')
|
||||
return
|
||||
}
|
||||
assetLoading.value = true
|
||||
try {
|
||||
const res = await proxy.$axios({
|
||||
const res = await request({
|
||||
url: 'api/byteAsset/listAssets',
|
||||
method: 'POST',
|
||||
data: {
|
||||
Filter: {
|
||||
GroupIds: [gid],
|
||||
GroupType: 'AIGC',
|
||||
Statuses: ['Active']
|
||||
},
|
||||
PageNumber: 1,
|
||||
PageSize: 100,
|
||||
SortBy: 'CreateTime',
|
||||
SortOrder: 'Desc'
|
||||
}
|
||||
data: buildReferenceListAssetsPayload()
|
||||
})
|
||||
const rows = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
||||
if (res.code !== 200) {
|
||||
Message.error(res.msg || '查询素材失败')
|
||||
return
|
||||
}
|
||||
const rows = byteApiItems(res?.data)
|
||||
const assets = rows
|
||||
.map((row) => {
|
||||
const at = String(row?.AssetType || row?.assetType || 'Image').toLowerCase()
|
||||
|
|
@ -504,7 +608,7 @@ const confirmPickAssets = () => {
|
|||
|
||||
const isReferenceMode = isReference.value
|
||||
if (isReferenceMode && !String(assetGroupId.value || '').trim()) {
|
||||
Message.warning('参考图模式请先填写 GroupId')
|
||||
Message.warning('请先选择素材组')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
|
@ -581,8 +685,7 @@ const confirmPickAssets = () => {
|
|||
let assetId = entry.assetId || ''
|
||||
if (isReferenceMode) {
|
||||
const gid = String(assetGroupId.value || '').trim()
|
||||
if (!gid) throw new Error('请先填写 GroupId')
|
||||
if (!proxy?.$axios) throw new Error('上传资产失败:axios 未就绪')
|
||||
if (!gid) throw new Error('请先选择素材组')
|
||||
const mt = entry.mediaType || 'image'
|
||||
const assetType =
|
||||
mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image'
|
||||
|
|
@ -591,11 +694,14 @@ const confirmPickAssets = () => {
|
|||
fd.append('groupId', gid)
|
||||
fd.append('assetType', assetType)
|
||||
fd.append('name', entry?.name || '')
|
||||
const createRes = await proxy.$axios({
|
||||
const createRes = await request({
|
||||
url: 'api/byteAsset/createAsset',
|
||||
method: 'POST',
|
||||
data: fd
|
||||
})
|
||||
if (createRes.code !== 200) {
|
||||
throw new Error(createRes?.msg || '创建素材失败')
|
||||
}
|
||||
assetId = createRes?.data?.Id || createRes?.data?.id || ''
|
||||
if (!assetId) throw new Error(createRes?.msg || '创建素材失败:未返回资产ID')
|
||||
}
|
||||
|
|
@ -1125,6 +1231,7 @@ const selectMentionItem = (item) => {
|
|||
defineExpose({
|
||||
getEditorPlainText,
|
||||
getImageReferenceContentItems,
|
||||
clearAll,
|
||||
clearPromptOnly: () => {
|
||||
setPrompt('')
|
||||
if (editorRef.value) editorRef.value.innerHTML = ''
|
||||
|
|
@ -1135,7 +1242,16 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.vg-compose-root {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vg-compose-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
|
|
@ -1144,7 +1260,138 @@ defineExpose({
|
|||
border-radius: 18px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
min-height: 200px;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 参考图:参考图 | 资产(组+操作) | 选择区域(生成参数) | 富文本 */
|
||||
.vg-compose-card--reference {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 0.85fr) minmax(72px, 0.75fr) minmax(168px, 1.35fr) minmax(120px, 2.95fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
column-gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 避免网格子项按内容撑开高度,导致第四列富文本溢出卡片 */
|
||||
.vg-compose-card--reference > * {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-card--split {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.vg-compose-card--solo {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.vg-compose-card--solo .vg-compose-right {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vg-compose-mod {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vg-mod-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vg-mod-body--asset {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vg-asset-btn-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vg-mod-body--params {
|
||||
gap: 6px;
|
||||
justify-content: flex-start;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.vg-mod-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vg-mod-btn:deep(.arco-btn) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vg-mod-foot {
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
.vg-mod-foot.muted {
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
.vg-compose-mod--ref .vg-mod-body {
|
||||
min-height: 72px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.vg-compose-empty--compact {
|
||||
min-height: 72px;
|
||||
flex: 1;
|
||||
padding: 10px 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vg-compose-empty--compact .vg-compose-empty-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vg-compose-empty--compact .vg-compose-empty-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
padding: 0 4px;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vg-compose-media-scroll--vertical {
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
padding: 2px;
|
||||
scroll-snap-type: y proximity;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.vg-compose-media-item--tile {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.vg-compose-left {
|
||||
|
|
@ -1270,20 +1517,25 @@ defineExpose({
|
|||
}
|
||||
|
||||
.vg-asset-group-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 0 8px;
|
||||
outline: none;
|
||||
}
|
||||
font-size: 11px;
|
||||
|
||||
.vg-asset-group-input:focus {
|
||||
border-color: rgba(0, 202, 224, 0.55);
|
||||
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.12);
|
||||
:deep(.arco-select-view-single) {
|
||||
background: rgba(0, 0, 0, 0.25) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16) !important;
|
||||
border-radius: 8px !important;
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
min-height: 30px !important;
|
||||
}
|
||||
:deep(.arco-select-view-value) {
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
font-size: 11px;
|
||||
}
|
||||
:deep(.arco-select-view-single.arco-select-view-disabled) {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.vg-asset-picker {
|
||||
|
|
@ -1441,6 +1693,61 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-right--reference {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px 10px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vg-compose-right-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vg-reference-editor-row {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vg-reference-actions-col {
|
||||
flex-shrink: 0;
|
||||
width: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 14px;
|
||||
padding: 4px 0 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vg-compose-card--reference .vg-rich-editor-wrap {
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vg-compose-card--reference .vg-compose-textarea.vg-rich-editor {
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.vg-compose-toolbar {
|
||||
|
|
@ -1650,20 +1957,76 @@ defineExpose({
|
|||
box-shadow: 0 0 0 3px rgba(0, 202, 224, 0.15);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.vg-compose-card {
|
||||
/* 小分辨率:参考图四块改为自上而下竖版(顺序:参考图 → 资产 → 参数 → 富文本) */
|
||||
@media (max-width: 768px) {
|
||||
.vg-compose-card--split {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-card--reference {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-mod--ref {
|
||||
max-height: 220px;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-mod--asset,
|
||||
.vg-compose-mod--pick {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vg-compose-right--reference {
|
||||
flex: 1;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.vg-compose-left {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小于 1920 宽:参考素材与富文本上下排列,避免横向过窄时布局错乱 */
|
||||
/* 中等宽度:四列改为「参考条 + 右侧两行」,富文本独占一行(平板横屏等) */
|
||||
@media (max-width: 1200px) and (min-width: 769px) {
|
||||
.vg-compose-card--reference {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 32%) 1fr;
|
||||
grid-template-rows: auto auto minmax(100px, 1fr);
|
||||
column-gap: 10px;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.vg-compose-mod--ref {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.vg-compose-mod--asset {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.vg-compose-mod--pick {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.vg-compose-right--reference {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 3;
|
||||
}
|
||||
}
|
||||
|
||||
/* 非参考模式:窄屏上下排 */
|
||||
@media (max-width: 1919px) {
|
||||
.vg-compose-card {
|
||||
.vg-compose-card--split {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-height: 0;
|
||||
|
|
@ -1673,14 +2036,6 @@ defineExpose({
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.vg-reference-panel {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-media-scroll {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.vg-compose-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* 火山素材相关接口 data 层字段兼容:
|
||||
* Spring/Jackson 序列化为 camelCase(items、totalCount),
|
||||
* 旧前端曾按 PascalCase(Items、TotalCount)取值,此处统一解析。
|
||||
*/
|
||||
export function byteApiItems(data) {
|
||||
if (!data) return []
|
||||
if (Array.isArray(data.items)) return data.items
|
||||
if (Array.isArray(data.Items)) return data.Items
|
||||
return []
|
||||
}
|
||||
|
||||
export function byteApiTotalCount(data) {
|
||||
if (!data) return 0
|
||||
const raw = data.totalCount ?? data.TotalCount
|
||||
const n = Number(raw)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
|
@ -44,6 +44,16 @@
|
|||
<div class="ag-total">总数:{{ totalCount }}</div>
|
||||
<div class="ag-table-wrap">
|
||||
<table class="ag-table">
|
||||
<colgroup>
|
||||
<col class="ag-col-id" />
|
||||
<col class="ag-col-name" />
|
||||
<col class="ag-col-desc" />
|
||||
<col class="ag-col-type" />
|
||||
<col class="ag-col-project" />
|
||||
<col class="ag-col-time" />
|
||||
<col class="ag-col-time" />
|
||||
<col class="ag-col-action" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
|
@ -88,7 +98,9 @@
|
|||
:page-size="filters.pageSize"
|
||||
show-total
|
||||
show-jumper
|
||||
@change="search"
|
||||
show-page-size
|
||||
:page-size-options="listPageSizeOptions"
|
||||
@change="onGroupPageChange"
|
||||
@page-size-change="onPageSizeChange" />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
|
@ -100,27 +112,37 @@
|
|||
<a-modal
|
||||
v-model:visible="createVisible"
|
||||
title="新增资源组"
|
||||
width="520px"
|
||||
:confirm-loading="createLoading"
|
||||
@ok="createGroup">
|
||||
<div class="ag-create-form">
|
||||
<div class="ag-field">
|
||||
<label>名称</label>
|
||||
<a-input v-model="createForm.name" placeholder="请输入资源组名称(<=64字符)" />
|
||||
</div>
|
||||
<div class="ag-field">
|
||||
<label>描述</label>
|
||||
<!-- 弹层 teleported 到 body,使用 FormItem 保证标签与 dark 样式生效 -->
|
||||
<a-form :model="createForm" layout="horizontal" auto-label-width class="ag-create-modal-form">
|
||||
<a-form-item label="名称" required>
|
||||
<a-input
|
||||
v-model="createForm.name"
|
||||
placeholder="请输入资源组名称(≤64 字符)"
|
||||
:max-length="64"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea
|
||||
v-model="createForm.description"
|
||||
:max-length="300"
|
||||
show-word-limit
|
||||
placeholder="请输入描述(<=300字符)" />
|
||||
</div>
|
||||
</div>
|
||||
:auto-size="{ minRows: 3, maxRows: 8 }"
|
||||
placeholder="请输入描述(≤300 字符)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||
|
||||
const LIST_PAGE_NUMBER_MAX = 100
|
||||
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||
|
||||
export default {
|
||||
name: 'AssetGroupManage',
|
||||
data() {
|
||||
|
|
@ -146,7 +168,8 @@ export default {
|
|||
sortOrder: 'Desc'
|
||||
},
|
||||
totalCount: 0,
|
||||
items: []
|
||||
items: [],
|
||||
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -158,6 +181,19 @@ export default {
|
|||
this.search(1)
|
||||
},
|
||||
methods: {
|
||||
clampGroupPage(n) {
|
||||
const page = Number(n) || 1
|
||||
const size = Number(this.filters.pageSize) || 10
|
||||
let maxPage = LIST_PAGE_NUMBER_MAX
|
||||
if (this.totalCount > 0) {
|
||||
const totalPages = Math.ceil(this.totalCount / size)
|
||||
maxPage = Math.min(LIST_PAGE_NUMBER_MAX, Math.max(1, totalPages))
|
||||
}
|
||||
return Math.min(Math.max(1, page), maxPage)
|
||||
},
|
||||
onGroupPageChange(page) {
|
||||
this.search(this.clampGroupPage(page))
|
||||
},
|
||||
openCreateDialog() {
|
||||
this.createVisible = true
|
||||
},
|
||||
|
|
@ -179,10 +215,8 @@ export default {
|
|||
url: 'api/byteAssetGroup/createAssetGroup',
|
||||
method: 'POST',
|
||||
data: {
|
||||
Name: name,
|
||||
Description: String(this.createForm.description || '').trim(),
|
||||
GroupType: 'AIGC',
|
||||
ProjectName: 'default'
|
||||
name,
|
||||
description: String(this.createForm.description || '').trim()
|
||||
}
|
||||
})
|
||||
if (res.code === 200) {
|
||||
|
|
@ -201,22 +235,22 @@ export default {
|
|||
}
|
||||
},
|
||||
async search(page = this.filters.pageNumber) {
|
||||
this.filters.pageNumber = Number(page) || 1
|
||||
this.filters.pageNumber = this.clampGroupPage(page)
|
||||
this.listLoading = true
|
||||
try {
|
||||
const payload = {
|
||||
Filter: {
|
||||
GroupType: this.filters.groupType || 'AIGC'
|
||||
filter: {
|
||||
groupType: this.filters.groupType || 'AIGC'
|
||||
},
|
||||
PageNumber: this.filters.pageNumber,
|
||||
PageSize: this.filters.pageSize,
|
||||
SortBy: this.filters.sortBy,
|
||||
SortOrder: this.filters.sortOrder
|
||||
pageNumber: this.filters.pageNumber,
|
||||
pageSize: this.filters.pageSize,
|
||||
sortBy: this.filters.sortBy,
|
||||
sortOrder: this.filters.sortOrder
|
||||
}
|
||||
const name = String(this.filters.name || '').trim()
|
||||
if (name) payload.Filter.name = name
|
||||
if (name) payload.filter.name = name
|
||||
const ids = this.buildGroupIds()
|
||||
if (ids.length) payload.Filter.GroupIds = ids
|
||||
if (ids.length) payload.filter.groupIds = ids
|
||||
|
||||
const res = await this.$axios({
|
||||
url: 'api/byteAssetGroup/listAssetGroups',
|
||||
|
|
@ -224,8 +258,8 @@ export default {
|
|||
data: payload
|
||||
})
|
||||
const data = res.data || {}
|
||||
this.totalCount = Number(data.TotalCount || 0)
|
||||
this.items = Array.isArray(data.Items) ? data.Items : []
|
||||
this.totalCount = byteApiTotalCount(data)
|
||||
this.items = byteApiItems(data)
|
||||
if (res.code !== 200) {
|
||||
this.$message.error(res.msg || '查询失败')
|
||||
}
|
||||
|
|
@ -236,7 +270,8 @@ export default {
|
|||
}
|
||||
},
|
||||
onPageSizeChange(size) {
|
||||
this.filters.pageSize = Number(size) || 10
|
||||
const s = Number(size) || 10
|
||||
this.filters.pageSize = LIST_PAGE_SIZE_OPTIONS.includes(s) ? s : 10
|
||||
this.search(1)
|
||||
},
|
||||
resetFilters() {
|
||||
|
|
@ -313,12 +348,6 @@ export default {
|
|||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.ag-create-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ag-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -343,23 +372,53 @@ export default {
|
|||
}
|
||||
|
||||
.ag-table-wrap {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ag-table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 列宽按百分比分配,随容器(100% 宽)随分辨率伸缩 */
|
||||
.ag-col-id {
|
||||
width: 10%;
|
||||
}
|
||||
.ag-col-name {
|
||||
width: 12%;
|
||||
}
|
||||
.ag-col-desc {
|
||||
width: 24%;
|
||||
}
|
||||
.ag-col-type {
|
||||
width: 8%;
|
||||
}
|
||||
.ag-col-project {
|
||||
width: 12%;
|
||||
}
|
||||
.ag-col-time {
|
||||
width: 11%;
|
||||
}
|
||||
.ag-col-action {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.ag-table th,
|
||||
.ag-table td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ag-table th {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@
|
|||
<section class="asset-left">
|
||||
<div class="asset-left-head">
|
||||
<div class="panel-title">素材组树</div>
|
||||
<div class="field actions">
|
||||
<a-button size="mini" type="outline" :loading="groupLoading" @click="loadGroups">刷新分组</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-tree">
|
||||
<div
|
||||
|
|
@ -34,8 +31,17 @@
|
|||
<div class="panel-title mtop">查询素材</div>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>GroupId</label>
|
||||
<a-input v-model="filters.groupId" placeholder="按组过滤(默认选中左侧)" />
|
||||
<label>素材组</label>
|
||||
<a-select
|
||||
v-model="filters.groupId"
|
||||
placeholder="请选择素材组"
|
||||
allow-clear
|
||||
:loading="groupLoading"
|
||||
@change="onFilterGroupChange">
|
||||
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
||||
{{ (g.Name || g.name || g.Id || g.id) + ' · ' + (g.Id || g.id) }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Name</label>
|
||||
|
|
@ -89,9 +95,45 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="it in items" :key="it.Id || it.id">
|
||||
<td>{{ it.Id || it.id }}</td>
|
||||
<td class="asset-td-id">{{ it.Id || it.id }}</td>
|
||||
<td>{{ it.Name || it.name || '-' }}</td>
|
||||
<td class="url-cell">{{ it.URL || it.url || '-' }}</td>
|
||||
<td class="asset-url-cell">
|
||||
<template v-if="assetUrlPreviewKind(it) === 'image' && !thumbError[it.Id || it.id]">
|
||||
<a
|
||||
:href="assetUrl(it)"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="asset-thumb-link">
|
||||
<img
|
||||
class="asset-url-thumb"
|
||||
:src="assetUrl(it)"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="onAssetThumbError(it)" />
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="assetUrlPreviewKind(it) === 'video'">
|
||||
<video
|
||||
class="asset-url-video"
|
||||
:src="assetUrl(it)"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata"
|
||||
controls />
|
||||
</template>
|
||||
<template v-else-if="assetUrl(it)">
|
||||
<a :href="assetUrl(it)" target="_blank" rel="noreferrer" class="asset-url-plain">{{
|
||||
assetUrl(it)
|
||||
}}</a>
|
||||
</template>
|
||||
<span v-else class="asset-url-empty">-</span>
|
||||
<div
|
||||
v-if="assetUrlCaptionVisible(it)"
|
||||
class="asset-url-caption"
|
||||
:title="assetUrl(it)">
|
||||
{{ assetUrl(it) }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ it.GroupId || it.groupId || '-' }}</td>
|
||||
<td>{{ it.AssetType || it.assetType || '-' }}</td>
|
||||
<td>{{ it.Status || it.status || '-' }}</td>
|
||||
|
|
@ -114,7 +156,9 @@
|
|||
:page-size="filters.pageSize"
|
||||
show-total
|
||||
show-jumper
|
||||
@change="searchAssets"
|
||||
show-page-size
|
||||
:page-size-options="listPageSizeOptions"
|
||||
@change="onAssetPageChange"
|
||||
@page-size-change="onPageSizeChange" />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
|
@ -126,46 +170,58 @@
|
|||
<a-modal
|
||||
v-model:visible="createAssetVisible"
|
||||
title="新增素材"
|
||||
width="520px"
|
||||
:confirm-loading="createLoading"
|
||||
@ok="createAsset">
|
||||
<div class="create-form-vertical">
|
||||
<div class="field">
|
||||
<label>素材组</label>
|
||||
<a-select v-model="createForm.groupId" placeholder="请选择素材组">
|
||||
<a-form :model="createForm" layout="horizontal" auto-label-width class="asset-create-modal-form">
|
||||
<a-form-item label="素材组" required>
|
||||
<a-select v-model="createForm.groupId" placeholder="请选择素材组" allow-clear>
|
||||
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
||||
{{ g.Name || g.name || (g.Id || g.id) }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>上传素材</label>
|
||||
<input type="file" @change="onFileChange" />
|
||||
<div class="group-id">{{ createForm.fileName || '未选择文件' }}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>素材名称</label>
|
||||
<a-input v-model="createForm.name" placeholder="素材名称(可选)" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
</a-form-item>
|
||||
<a-form-item label="上传文件" required>
|
||||
<input class="asset-file-input" type="file" @change="onFileChange" />
|
||||
<div class="asset-file-hint">{{ createForm.fileName || '未选择文件' }}</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="素材名称">
|
||||
<a-input v-model="createForm.name" placeholder="素材名称(可选)" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型">
|
||||
<a-select v-model="createForm.assetType">
|
||||
<a-option value="Image">图片</a-option>
|
||||
<a-option value="Video">视频</a-option>
|
||||
<a-option value="Audio">音频</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||
|
||||
/** 素材树与筛选区素材组列表统一请求体 */
|
||||
const GROUP_LIST_REQUEST_BODY = {
|
||||
filter: { groupType: 'AIGC' },
|
||||
pageNumber: 1,
|
||||
pageSize: 100,
|
||||
sortBy: 'CreateTime',
|
||||
sortOrder: 'Desc'
|
||||
}
|
||||
|
||||
const GROUP_LIST_API = 'api/byteAssetGroup/listAssetGroups'
|
||||
const ASSET_CREATE_API = 'api/byteAsset/createAsset'
|
||||
const ASSET_LIST_API = 'api/byteAsset/listAssets'
|
||||
const ASSET_GET_API = 'api/byteAsset/getAsset'
|
||||
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
|
||||
|
||||
/** 列表页码上限(与接口分页一致);每页条数可选项 */
|
||||
const LIST_PAGE_NUMBER_MAX = 100
|
||||
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||
|
||||
export default {
|
||||
name: 'AssetManage',
|
||||
data() {
|
||||
|
|
@ -195,7 +251,9 @@ export default {
|
|||
totalCount: 0,
|
||||
items: [],
|
||||
detailVisible: false,
|
||||
detailData: null
|
||||
detailData: null,
|
||||
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS,
|
||||
thumbError: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -207,6 +265,46 @@ export default {
|
|||
this.loadGroups()
|
||||
},
|
||||
methods: {
|
||||
assetUrl(it) {
|
||||
return String(it?.URL || it?.url || '').trim()
|
||||
},
|
||||
/** image | video | link */
|
||||
assetUrlPreviewKind(it) {
|
||||
const u = this.assetUrl(it)
|
||||
if (!u || !/^https?:\/\//i.test(u)) return 'link'
|
||||
const t = String(it?.AssetType || it?.assetType || '')
|
||||
if (/image/i.test(t)) return 'image'
|
||||
if (/video/i.test(t)) return 'video'
|
||||
if (/audio/i.test(t)) return 'link'
|
||||
if (/\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(u)) return 'image'
|
||||
if (/\.(mp4|webm|mov|m4v|ogg)(\?|#|$)/i.test(u)) return 'video'
|
||||
return 'link'
|
||||
},
|
||||
onAssetThumbError(it) {
|
||||
const id = it?.Id || it?.id
|
||||
if (id) this.thumbError = { ...this.thumbError, [id]: true }
|
||||
},
|
||||
assetUrlCaptionVisible(it) {
|
||||
const u = this.assetUrl(it)
|
||||
if (!u) return false
|
||||
const k = this.assetUrlPreviewKind(it)
|
||||
if (k === 'video') return true
|
||||
if (k === 'image' && !this.thumbError[it?.Id || it?.id]) return true
|
||||
return false
|
||||
},
|
||||
clampAssetPage(n) {
|
||||
const page = Number(n) || 1
|
||||
const size = Number(this.filters.pageSize) || 10
|
||||
let maxPage = LIST_PAGE_NUMBER_MAX
|
||||
if (this.totalCount > 0) {
|
||||
const totalPages = Math.ceil(this.totalCount / size)
|
||||
maxPage = Math.min(LIST_PAGE_NUMBER_MAX, Math.max(1, totalPages))
|
||||
}
|
||||
return Math.min(Math.max(1, page), maxPage)
|
||||
},
|
||||
onAssetPageChange(page) {
|
||||
this.searchAssets(this.clampAssetPage(page))
|
||||
},
|
||||
async openCreateAssetDialog() {
|
||||
await this.loadGroups()
|
||||
this.createForm.groupId = this.selectedGroupId || this.createForm.groupId || ''
|
||||
|
|
@ -222,17 +320,11 @@ export default {
|
|||
const res = await this.$axios({
|
||||
url: GROUP_LIST_API,
|
||||
method: 'POST',
|
||||
data: {
|
||||
Filter: { GroupType: 'AIGC' },
|
||||
PageNumber: 1,
|
||||
PageSize: 100,
|
||||
SortBy: 'CreateTime',
|
||||
SortOrder: 'Desc'
|
||||
}
|
||||
data: { ...GROUP_LIST_REQUEST_BODY }
|
||||
})
|
||||
this.groups = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
||||
this.groups = byteApiItems(res?.data)
|
||||
if (!this.selectedGroupId && this.groups.length) {
|
||||
const gid = this.groups[0].Id || this.groups[0].id
|
||||
const gid = String(this.groups[0].Id || this.groups[0].id || '')
|
||||
this.selectedGroupId = gid
|
||||
this.createForm.groupId = gid
|
||||
this.filters.groupId = gid
|
||||
|
|
@ -245,12 +337,18 @@ export default {
|
|||
}
|
||||
},
|
||||
selectGroup(g) {
|
||||
const gid = g?.Id || g?.id
|
||||
const gid = String(g?.Id || g?.id || '')
|
||||
this.selectedGroupId = gid
|
||||
this.createForm.groupId = gid
|
||||
this.filters.groupId = gid
|
||||
this.searchAssets(1)
|
||||
},
|
||||
onFilterGroupChange(val) {
|
||||
const gid = val != null && val !== '' ? String(val) : ''
|
||||
this.selectedGroupId = gid
|
||||
this.createForm.groupId = gid
|
||||
this.searchAssets(1)
|
||||
},
|
||||
onFileChange(e) {
|
||||
const file = e?.target?.files?.[0]
|
||||
this.createForm.file = file || null
|
||||
|
|
@ -290,32 +388,33 @@ export default {
|
|||
},
|
||||
buildListPayload() {
|
||||
const filter = {
|
||||
GroupType: 'AIGC'
|
||||
groupType: 'AIGC'
|
||||
}
|
||||
const gid = String(this.filters.groupId || '').trim()
|
||||
if (gid) filter.GroupIds = [gid]
|
||||
if (gid) filter.groupIds = [gid]
|
||||
const name = String(this.filters.name || '').trim()
|
||||
if (name) filter.Name = name
|
||||
if (this.filters.status) filter.Statuses = [this.filters.status]
|
||||
if (name) filter.name = name
|
||||
if (this.filters.status) filter.statuses = [this.filters.status]
|
||||
return {
|
||||
Filter: filter,
|
||||
PageNumber: this.filters.pageNumber,
|
||||
PageSize: this.filters.pageSize,
|
||||
SortBy: this.filters.sortBy,
|
||||
SortOrder: this.filters.sortOrder
|
||||
filter,
|
||||
pageNumber: this.filters.pageNumber,
|
||||
pageSize: this.filters.pageSize,
|
||||
sortBy: this.filters.sortBy,
|
||||
sortOrder: this.filters.sortOrder
|
||||
}
|
||||
},
|
||||
async searchAssets(page = this.filters.pageNumber) {
|
||||
this.filters.pageNumber = Number(page) || 1
|
||||
this.filters.pageNumber = this.clampAssetPage(page)
|
||||
this.listLoading = true
|
||||
this.thumbError = {}
|
||||
try {
|
||||
const res = await this.$axios({
|
||||
url: ASSET_LIST_API,
|
||||
method: 'POST',
|
||||
data: this.buildListPayload()
|
||||
})
|
||||
this.totalCount = Number(res?.data?.TotalCount || 0)
|
||||
this.items = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
||||
this.totalCount = byteApiTotalCount(res?.data)
|
||||
this.items = byteApiItems(res?.data)
|
||||
if (res.code !== 200) {
|
||||
this.$message.error(res.msg || '查询素材失败')
|
||||
}
|
||||
|
|
@ -326,12 +425,18 @@ export default {
|
|||
}
|
||||
},
|
||||
onPageSizeChange(size) {
|
||||
this.filters.pageSize = Number(size) || 10
|
||||
const s = Number(size) || 10
|
||||
this.filters.pageSize = LIST_PAGE_SIZE_OPTIONS.includes(s) ? s : 10
|
||||
this.searchAssets(1)
|
||||
},
|
||||
resetFilters() {
|
||||
const gid = this.groups.length
|
||||
? String(this.groups[0].Id || this.groups[0].id || '')
|
||||
: String(this.selectedGroupId || '')
|
||||
this.selectedGroupId = gid
|
||||
this.createForm.groupId = gid
|
||||
this.filters = {
|
||||
groupId: this.selectedGroupId || '',
|
||||
groupId: gid,
|
||||
name: '',
|
||||
status: '',
|
||||
pageNumber: 1,
|
||||
|
|
@ -406,6 +511,10 @@ export default {
|
|||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.asset-right {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
|
@ -472,11 +581,6 @@ export default {
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.create-form-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.mtop {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
|
@ -487,14 +591,53 @@ export default {
|
|||
}
|
||||
.table-wrap {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.asset-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.asset-table th:nth-child(1),
|
||||
.asset-table td:nth-child(1) {
|
||||
width: 88px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.asset-table th:nth-child(2),
|
||||
.asset-table td:nth-child(2) {
|
||||
width: 14%;
|
||||
word-break: break-word;
|
||||
}
|
||||
.asset-table th:nth-child(3),
|
||||
.asset-table td:nth-child(3) {
|
||||
width: 30%;
|
||||
min-width: 160px;
|
||||
}
|
||||
.asset-table th:nth-child(4),
|
||||
.asset-table td:nth-child(4) {
|
||||
width: 10%;
|
||||
word-break: break-all;
|
||||
}
|
||||
.asset-table th:nth-child(5),
|
||||
.asset-table td:nth-child(5) {
|
||||
width: 9%;
|
||||
}
|
||||
.asset-table th:nth-child(6),
|
||||
.asset-table td:nth-child(6) {
|
||||
width: 9%;
|
||||
}
|
||||
.asset-table th:nth-child(7),
|
||||
.asset-table td:nth-child(7) {
|
||||
width: 14%;
|
||||
word-break: break-word;
|
||||
}
|
||||
.asset-table th:nth-child(8),
|
||||
.asset-table td:nth-child(8) {
|
||||
width: 16%;
|
||||
}
|
||||
.asset-table th,
|
||||
.asset-table td {
|
||||
padding: 10px;
|
||||
|
|
@ -506,10 +649,51 @@ export default {
|
|||
color: rgba(255, 255, 255, 0.72);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
.url-cell {
|
||||
max-width: 280px;
|
||||
.asset-url-cell {
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.asset-thumb-link {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
.asset-url-thumb {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 120px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.asset-url-video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 160px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: #000;
|
||||
}
|
||||
.asset-url-plain {
|
||||
color: rgba(0, 202, 224, 0.95);
|
||||
word-break: break-all;
|
||||
}
|
||||
.asset-url-empty {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.asset-url-caption {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
|
|
@ -539,3 +723,28 @@ export default {
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* 弹层挂载到 body,与深色页搭配的标签可读性 */
|
||||
.asset-create-modal-form.arco-form .arco-form-item-label-col .arco-form-item-label,
|
||||
.ag-create-modal-form.arco-form .arco-form-item-label-col .arco-form-item-label {
|
||||
color: var(--color-text-2, rgba(255, 255, 255, 0.78));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.asset-file-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.asset-file-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -16,89 +16,102 @@
|
|||
<div
|
||||
v-for="row in sortedTaskRows"
|
||||
:key="row.id"
|
||||
class="vg-chat-block">
|
||||
class="vg-chat-block"
|
||||
:class="{ 'vg-chat-block--compact': !isChatRowSuccessWithMedia(row) }">
|
||||
<!-- 用户输入部分 -->
|
||||
<div class="vg-chat-user-section">
|
||||
<div class="vg-chat-user-text">
|
||||
<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
|
||||
<span v-if="seg.type === 'text'">{{ seg.text }}</span>
|
||||
<img
|
||||
v-else-if="seg.type === 'image'"
|
||||
class="vg-chat-inline-ref-image"
|
||||
:src="seg.url"
|
||||
:alt="seg.token || ''" />
|
||||
<div
|
||||
class="vg-chat-user-section"
|
||||
:class="{ 'vg-chat-user-section--compact': !isChatRowSuccessWithMedia(row) }">
|
||||
<div v-if="isChatRowSuccessWithMedia(row)" class="vg-chat-user-block">
|
||||
<div class="vg-chat-user-text">
|
||||
<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
|
||||
<span v-if="seg.type === 'text'">{{ seg.text }}</span>
|
||||
<img
|
||||
v-else-if="seg.type === 'image'"
|
||||
class="vg-chat-inline-ref-image"
|
||||
:src="seg.url"
|
||||
:alt="seg.token || ''" />
|
||||
<video
|
||||
v-else-if="seg.type === 'video'"
|
||||
class="vg-chat-inline-ref-video"
|
||||
:src="seg.url"
|
||||
controls
|
||||
muted
|
||||
preload="metadata" />
|
||||
<audio
|
||||
v-else-if="seg.type === 'audio'"
|
||||
class="vg-chat-inline-audio"
|
||||
:src="seg.url"
|
||||
controls
|
||||
preload="metadata" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||
</div>
|
||||
<div v-else class="vg-chat-user-row">
|
||||
<div class="vg-chat-user-col-main">
|
||||
<div class="vg-chat-user-text">
|
||||
<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
|
||||
<span v-if="seg.type === 'text'">{{ seg.text }}</span>
|
||||
<img
|
||||
v-else-if="seg.type === 'image'"
|
||||
class="vg-chat-inline-ref-image"
|
||||
:src="seg.url"
|
||||
:alt="seg.token || ''" />
|
||||
<video
|
||||
v-else-if="seg.type === 'video'"
|
||||
class="vg-chat-inline-ref-video"
|
||||
:src="seg.url"
|
||||
controls
|
||||
muted
|
||||
preload="metadata" />
|
||||
<audio
|
||||
v-else-if="seg.type === 'audio'"
|
||||
class="vg-chat-inline-audio"
|
||||
:src="seg.url"
|
||||
controls
|
||||
preload="metadata" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||
</div>
|
||||
<div class="vg-chat-user-col-status">
|
||||
<span class="vg-chat-inline-status" :class="chatRowInlineStatusClass(row)">
|
||||
{{ taskStatusText(row) }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link vg-chat-inline-cancel"
|
||||
v-if="row.result && row.status === 0"
|
||||
@click="cancelRowTask(row)">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 仅「已完成且可播放/下载」保留下方结果区 -->
|
||||
<template v-if="isChatRowSuccessWithMedia(row)">
|
||||
<div class="vg-chat-divider"></div>
|
||||
<div class="vg-chat-ai-section">
|
||||
<div class="vg-chat-ai-top">
|
||||
<div class="vg-chat-ai-status">
|
||||
{{ taskStatusText(row) }}
|
||||
</div>
|
||||
<div class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
|
||||
</div>
|
||||
<div class="vg-chat-result">
|
||||
<video
|
||||
v-else-if="seg.type === 'video'"
|
||||
class="vg-chat-inline-ref-video"
|
||||
:src="seg.url"
|
||||
controls
|
||||
muted
|
||||
preload="metadata" />
|
||||
<audio
|
||||
v-else-if="seg.type === 'audio'"
|
||||
class="vg-chat-inline-audio"
|
||||
:src="seg.url"
|
||||
v-if="isVideoUrl(row.result)"
|
||||
:src="row.result"
|
||||
controls
|
||||
preload="metadata" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="vg-chat-divider"></div>
|
||||
|
||||
<!-- AI响应部分(视频结果) -->
|
||||
<div class="vg-chat-ai-section">
|
||||
<div class="vg-chat-ai-top">
|
||||
<div class="vg-chat-ai-status">
|
||||
{{ taskStatusText(row) }}
|
||||
</div>
|
||||
<div class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="row.status === 0" class="vg-chat-loading">
|
||||
生成中,请稍后…
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="row.status === 1 && row.result && isHttpOrHttpsUrl(row.result)"
|
||||
class="vg-chat-result">
|
||||
<video
|
||||
v-if="isVideoUrl(row.result)"
|
||||
:src="row.result"
|
||||
controls
|
||||
preload="metadata" />
|
||||
<div v-else class="vg-chat-result-link">
|
||||
<a :href="row.result" target="_blank" rel="noreferrer">查看结果</a>
|
||||
<div v-else class="vg-chat-result-link">
|
||||
<a :href="row.result" target="_blank" rel="noreferrer">查看结果</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="row.status === 1 && row.result && !isHttpOrHttpsUrl(row.result)"
|
||||
class="vg-chat-loading">
|
||||
执行任务中,请稍后…
|
||||
</div>
|
||||
|
||||
<div v-else-if="row.status === 1 && !row.result" class="vg-chat-loading">
|
||||
执行任务中,请稍后…
|
||||
</div>
|
||||
|
||||
<div v-else class="vg-chat-failed">
|
||||
生成失败或已取消
|
||||
</div>
|
||||
|
||||
<div class="vg-chat-ai-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link"
|
||||
v-if="row.result && row.status === 0"
|
||||
@click="cancelRowTask(row)">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="chatLoadingMore" class="vg-chat-loadmore">
|
||||
|
|
@ -122,11 +135,8 @@
|
|||
'描述画面与动态,例如:阳光下的女孩在海边起舞…'
|
||||
"
|
||||
:video-mode="videoMode">
|
||||
<template #toolbar>
|
||||
<div class="vg-toolbar">
|
||||
|
||||
<div class="vg-toolbar-settings">
|
||||
<div class="vg-params-row">
|
||||
<template #reference-params>
|
||||
<div class="vg-params-row vg-params-row--reference-col">
|
||||
<div class="vg-param">
|
||||
<span class="vg-param-label">生成模式</span>
|
||||
<a-select
|
||||
|
|
@ -174,21 +184,83 @@
|
|||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vg-toolbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="vg-submit-circle"
|
||||
:disabled="generateLoading"
|
||||
@click="generateVideo"
|
||||
:title="price ? $t('common.createVideo', { price }) : $t('common.generateVideo')">
|
||||
<span class="vg-submit-circle-inner">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M12.002 3c.424 0 .806.177 1.079.46l5.98 5.98.103.114a1.5 1.5 0 0 1-2.225 2.006l-3.437-3.436V19.5l-.008.153a1.5 1.5 0 0 1-2.985 0l-.007-.153V8.122l-3.44 3.438a1.5 1.5 0 0 1-2.225-2.006l.103-.115 6-5.999.025-.025.059-.052.044-.037c.029-.023.06-.044.09-.065l.014-.01a1.43 1.43 0 0 1 .101-.062l.03-.017c.209-.11.447-.172.699-.172Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<div class="vg-toolbar" :class="{ 'vg-toolbar--reference-submit': videoMode === 'image-reference' }">
|
||||
<div v-if="videoMode !== 'image-reference'" class="vg-toolbar-settings">
|
||||
<div class="vg-params-row">
|
||||
<div class="vg-param">
|
||||
<span class="vg-param-label">生成模式</span>
|
||||
<a-select
|
||||
v-model="videoMode"
|
||||
class="vg-param-select"
|
||||
placeholder="请选择模式"
|
||||
@change="pickVideoMode">
|
||||
<a-option v-for="opt in videoModeOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="vg-param">
|
||||
<span class="vg-param-label">模型</span>
|
||||
<a-select
|
||||
v-model="selectedModel"
|
||||
class="vg-param-select"
|
||||
placeholder="请选择模型"
|
||||
allow-clear>
|
||||
<a-option v-for="opt in modelOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="vg-param">
|
||||
<span class="vg-param-label">比例</span>
|
||||
<a-select v-model="selectedRatio" class="vg-param-select" placeholder="画幅比例" allow-clear>
|
||||
<a-option v-for="r in ratioOptions" :key="r" :value="r">{{ r }}</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="vg-param">
|
||||
<span class="vg-param-label">时长</span>
|
||||
<a-select v-model="selectedDuration" class="vg-param-select" placeholder="秒" allow-clear>
|
||||
<a-option v-for="d in durationOptions" :key="d" :value="d">{{ d }} 秒</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="vg-param">
|
||||
<span class="vg-param-label">分辨率</span>
|
||||
<a-select
|
||||
v-model="selectedResolution"
|
||||
class="vg-param-select"
|
||||
placeholder="分辨率"
|
||||
allow-clear>
|
||||
<a-option v-for="r in resolutionOptions" :key="r" :value="r">{{ r }}</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="vg-toolbar-actions"
|
||||
:class="{ 'vg-toolbar-actions--reference': videoMode === 'image-reference' }">
|
||||
<mf-button
|
||||
v-if="videoMode === 'image-reference'"
|
||||
size="small"
|
||||
type="text"
|
||||
class="vg-toolbar-clear"
|
||||
@click="clearReferenceCompose">
|
||||
清空
|
||||
</mf-button>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-submit-circle"
|
||||
:disabled="generateLoading"
|
||||
@click="generateVideo"
|
||||
:title="price ? $t('common.createVideo', { price }) : $t('common.generateVideo')">
|
||||
<span class="vg-submit-circle-inner">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M12.002 3c.424 0 .806.177 1.079.46l5.98 5.98.103.114a1.5 1.5 0 0 1-2.225 2.006l-3.437-3.436V19.5l-.008.153a1.5 1.5 0 0 1-2.985 0l-.007-.153V8.122l-3.44 3.438a1.5 1.5 0 0 1-2.225-2.006l.103-.115 6-5.999.025-.025.059-.052.044-.037c.029-.023.06-.044.09-.065l.014-.01a1.43 1.43 0 0 1 .101-.062l.03-.017c.209-.11.447-.172.699-.172Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VideoComposeCard>
|
||||
|
|
@ -513,6 +585,16 @@ export default {
|
|||
return '进行中'
|
||||
},
|
||||
|
||||
/** 是否展示完整「结果区」(视频 / 链接);其余状态仅紧凑展示在用户行右侧 */
|
||||
isChatRowSuccessWithMedia(row) {
|
||||
return row.status === 1 && row.result && this.isHttpOrHttpsUrl(row.result)
|
||||
},
|
||||
|
||||
chatRowInlineStatusClass(row) {
|
||||
if (row.status === 2) return 'vg-chat-inline-status--failed'
|
||||
return 'vg-chat-inline-status--running'
|
||||
},
|
||||
|
||||
parseDateTimeToMs(value) {
|
||||
if (!value) return 0
|
||||
const ms = new Date(value).getTime()
|
||||
|
|
@ -719,6 +801,10 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
clearReferenceCompose() {
|
||||
this.$refs.videoComposeRef?.clearAll?.()
|
||||
},
|
||||
|
||||
async generateVideo() {
|
||||
if (this.generateLoading) return
|
||||
|
||||
|
|
@ -998,13 +1084,13 @@ export default {
|
|||
radial-gradient(80% 50% at 100% 30%, rgba(33, 151, 255, 0.08), transparent 45%), var(--vg-ink);
|
||||
}
|
||||
|
||||
/* —— Body —— */
|
||||
/* —— Body:与对话区约 1:3,整体约 25% / 75% —— */
|
||||
.vg-body {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex: 1 1 0%;
|
||||
order: 1;
|
||||
min-height: 0;
|
||||
max-height: min(48vh, 560px);
|
||||
max-height: none;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
|
@ -1069,15 +1155,14 @@ export default {
|
|||
min-width: 280px;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vg-generator-inner {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
|
@ -1176,6 +1261,13 @@ export default {
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.vg-compose-root) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-editor-container {
|
||||
|
|
@ -1253,9 +1345,74 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
/* 参考图:工具条在富文本右侧窄列内,清空与提交纵向排列 */
|
||||
.vg-toolbar--reference-submit {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-toolbar-actions {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vg-toolbar-actions--reference {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.vg-toolbar-clear:deep(.arco-btn) {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.vg-toolbar-clear:deep(.arco-btn:hover) {
|
||||
color: #7eeaf2;
|
||||
background: rgba(0, 202, 224, 0.1);
|
||||
}
|
||||
|
||||
/* 参考图模式:生成参数两列网格 */
|
||||
.vg-params-row--reference-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: start;
|
||||
gap: 8px 10px;
|
||||
padding: 6px 8px;
|
||||
min-height: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.vg-params-row--reference-col .vg-param {
|
||||
flex: none;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vg-params-row--reference-col .vg-param-label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.vg-params-row--reference-col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.vg-submit-circle {
|
||||
|
|
@ -1388,10 +1545,12 @@ export default {
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
.vg-body {
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
max-height: none;
|
||||
min-height: min(36vh, 400px);
|
||||
}
|
||||
.vg-left-rail {
|
||||
flex: none;
|
||||
|
|
@ -1418,6 +1577,12 @@ export default {
|
|||
min-height: 260px;
|
||||
max-height: min(55vh, 480px);
|
||||
}
|
||||
|
||||
/* 对话内视频小屏占满宽,避免横向截断 */
|
||||
.vg-chat-result video {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.vg-task-empty {
|
||||
|
|
@ -1428,13 +1593,13 @@ export default {
|
|||
|
||||
.vg-chat-section {
|
||||
order: 0;
|
||||
flex: 1 1 auto;
|
||||
flex: 3 1 0%;
|
||||
margin-top: 0;
|
||||
padding: 14px 18px;
|
||||
background: var(--vg-panel);
|
||||
border: 1px solid var(--vg-border);
|
||||
border-radius: 16px;
|
||||
min-height: min(42vh, 480px);
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -1477,12 +1642,29 @@ export default {
|
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.vg-chat-block--compact {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.vg-chat-divider {
|
||||
height: 1px;
|
||||
margin: 4px 0 0;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 用户输入区域 */
|
||||
.vg-chat-user-section {
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.vg-chat-user-section--compact {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vg-chat-ai-section {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
|
@ -1530,6 +1712,53 @@ export default {
|
|||
color: var(--vg-muted);
|
||||
}
|
||||
|
||||
.vg-chat-user-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vg-chat-user-col-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vg-chat-user-col-main .vg-chat-time {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vg-chat-user-col-status {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
max-width: 42%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.vg-chat-inline-status {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vg-chat-inline-status--running {
|
||||
color: var(--vg-cyan);
|
||||
}
|
||||
|
||||
.vg-chat-inline-status--failed {
|
||||
color: rgba(255, 107, 107, 0.95);
|
||||
}
|
||||
|
||||
.vg-chat-inline-cancel {
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vg-chat-ai-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1690,7 +1919,7 @@ export default {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 小于 1920×1080 常见宽度:底部参数区纵向排布,避免挤压富文本 */
|
||||
/* 小于 1920×1080:工具条参数纵向排布;整体仍保持约 75%/25% 弹性分配 */
|
||||
@media (max-width: 1919px) {
|
||||
.video-gen {
|
||||
padding: 14px 12px 12px;
|
||||
|
|
@ -1698,14 +1927,9 @@ export default {
|
|||
}
|
||||
|
||||
.vg-chat-section {
|
||||
min-height: min(40vh, 440px);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.vg-body {
|
||||
max-height: min(50vh, 540px);
|
||||
}
|
||||
|
||||
.vg-generator-inner {
|
||||
padding: 10px 12px;
|
||||
gap: 8px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue