fix: 对接资产库和优化布局
This commit is contained in:
parent
089ffca4f5
commit
aec9dc0152
|
|
@ -1,7 +1,99 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="vg-compose-card">
|
<div class="vg-compose-root">
|
||||||
<!-- 左侧面板 - 根据模式动态渲染 -->
|
<div
|
||||||
<div class="vg-compose-left" v-if="!isTextToVideo">
|
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 v-if="isFirstFrame" class="vg-first-frame-panel">
|
||||||
<div class="vg-panel-title">首帧图</div>
|
<div class="vg-panel-title">首帧图</div>
|
||||||
|
|
@ -49,102 +141,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div v-if="mediaList.length === 0" class="vg-compose-empty" @click="openFilePicker">
|
<div class="vg-compose-right" :class="{ 'vg-compose-right--reference': isReference }">
|
||||||
<div class="vg-compose-empty-icon" aria-hidden="true">+</div>
|
<div v-if="!isReference" class="vg-compose-right-head">
|
||||||
<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-title">描述画面与动态</div>
|
<div class="vg-compose-right-title">描述画面与动态</div>
|
||||||
<mf-button size="small" type="text" class="vg-compose-clear" @click="clearAll">
|
<mf-button size="small" type="text" class="vg-compose-clear" @click="clearAll">
|
||||||
清空
|
清空
|
||||||
</mf-button>
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div :class="['vg-compose-right-body', { 'vg-reference-editor-row': isReference }]">
|
||||||
<div class="vg-rich-editor-wrap">
|
<div class="vg-rich-editor-wrap">
|
||||||
<div
|
<div
|
||||||
ref="editorRef"
|
ref="editorRef"
|
||||||
|
|
@ -175,9 +182,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选择区域和提交按钮将从父组件传入 -->
|
<div v-if="isReference" class="vg-reference-actions-col">
|
||||||
<slot name="toolbar"></slot>
|
<slot name="toolbar"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<slot name="toolbar"></slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref="fileInputRef"
|
ref="fileInputRef"
|
||||||
|
|
@ -187,12 +199,56 @@
|
||||||
:multiple="isReference"
|
:multiple="isReference"
|
||||||
@change="handleSelectFiles" />
|
@change="handleSelectFiles" />
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { Message } from '@arco-design/web-vue'
|
||||||
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
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 控制) */
|
/** 参考素材:@ 引用同一类(图/视频/音频)最多 9 种不同素材;左侧列表最多可插入 12 条(由 maxMediaCount 控制) */
|
||||||
const MAX_REFERENCE_UNIQUE_KIND = 9
|
const MAX_REFERENCE_UNIQUE_KIND = 9
|
||||||
|
|
@ -228,7 +284,6 @@ const emit = defineEmits(['update:modelValue', 'update:mediaList'])
|
||||||
|
|
||||||
const fileInputRef = ref(null)
|
const fileInputRef = ref(null)
|
||||||
const editorRef = ref(null)
|
const editorRef = ref(null)
|
||||||
const { proxy } = getCurrentInstance() || {}
|
|
||||||
const localPrompt = ref(props.modelValue || '')
|
const localPrompt = ref(props.modelValue || '')
|
||||||
const internalMediaList = ref(Array.isArray(props.mediaList) ? [...props.mediaList] : [])
|
const internalMediaList = ref(Array.isArray(props.mediaList) ? [...props.mediaList] : [])
|
||||||
const currentUploadIndex = ref(-1) // for first/last frame mode
|
const currentUploadIndex = ref(-1) // for first/last frame mode
|
||||||
|
|
@ -238,6 +293,9 @@ const mentionActiveIndex = ref(-1)
|
||||||
const assetGroupId = ref('')
|
const assetGroupId = ref('')
|
||||||
const assetLoading = ref(false)
|
const assetLoading = ref(false)
|
||||||
const groupLoading = ref(false)
|
const groupLoading = ref(false)
|
||||||
|
const groupLoadingMore = ref(false)
|
||||||
|
const assetGroupPageNumber = ref(0)
|
||||||
|
const assetGroupHasMore = ref(false)
|
||||||
const assetGroups = ref([])
|
const assetGroups = ref([])
|
||||||
const assetPickerVisible = ref(false)
|
const assetPickerVisible = ref(false)
|
||||||
const assetQueryResults = ref([])
|
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(() => {
|
onMounted(() => {
|
||||||
if (editorRef.value && localPrompt.value) {
|
if (editorRef.value && localPrompt.value) {
|
||||||
editorRef.value.innerText = localPrompt.value
|
editorRef.value.innerText = localPrompt.value
|
||||||
}
|
}
|
||||||
if (isReference.value) {
|
|
||||||
loadAssetGroups()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const mediaList = computed(() => internalMediaList.value)
|
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 isFirstLastFrame = computed(() => props.videoMode === 'image-first-last-frame')
|
||||||
const isReference = computed(() => props.videoMode === 'image-reference')
|
const isReference = computed(() => props.videoMode === 'image-reference')
|
||||||
|
|
||||||
|
/** 参考图模式:已选素材组 ID(与素材管理页一致,非空才可选择/上传资产) */
|
||||||
|
const hasAssetGroupId = computed(() => !!String(assetGroupId.value || '').trim())
|
||||||
|
|
||||||
const setPrompt = (next) => {
|
const setPrompt = (next) => {
|
||||||
localPrompt.value = next
|
localPrompt.value = next
|
||||||
emit('update:modelValue', next)
|
emit('update:modelValue', next)
|
||||||
|
|
@ -318,7 +364,36 @@ const setMediaList = (next) => {
|
||||||
emit('update:mediaList', 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 = () => {
|
const openFilePicker = () => {
|
||||||
|
if (isReference.value && !hasAssetGroupId.value) {
|
||||||
|
Message.warning('请先选择素材组')
|
||||||
|
return
|
||||||
|
}
|
||||||
currentUploadIndex.value = -1
|
currentUploadIndex.value = -1
|
||||||
fileInputRef.value?.click()
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
@ -406,61 +481,90 @@ const mergeAssetsToMediaList = (assets) => {
|
||||||
setMediaList(next.slice(0, props.maxMediaCount))
|
setMediaList(next.slice(0, props.maxMediaCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAssetGroups = async () => {
|
const loadAssetGroups = async (reset = true) => {
|
||||||
if (!proxy?.$axios) return
|
if (reset) {
|
||||||
groupLoading.value = true
|
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 {
|
try {
|
||||||
const res = await proxy.$axios({
|
const res = await request({
|
||||||
url: 'api/byteAssetGroup/listAssetGroups',
|
url: 'api/byteAssetGroup/listAssetGroups',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: buildAssetGroupListBody(nextPage)
|
||||||
Filter: { GroupType: 'AIGC' },
|
|
||||||
PageNumber: 1,
|
|
||||||
PageSize: 100,
|
|
||||||
SortBy: 'CreateTime',
|
|
||||||
SortOrder: 'Desc'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const rows = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
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
|
assetGroups.value = rows
|
||||||
if (!assetGroupId.value && rows.length) {
|
} else {
|
||||||
assetGroupId.value = rows[0]?.Id || rows[0]?.id || ''
|
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) {
|
} catch (err) {
|
||||||
Message.error(err?.message || '加载素材组失败')
|
Message.error(err?.message || '加载素材组失败')
|
||||||
} finally {
|
} finally {
|
||||||
groupLoading.value = false
|
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 loadAssetsByGroup = async () => {
|
||||||
const gid = String(assetGroupId.value || '').trim()
|
const gid = String(assetGroupId.value || '').trim()
|
||||||
if (!gid) {
|
if (!gid) {
|
||||||
Message.warning('请先填写 GroupId 再查询')
|
Message.warning('请先选择素材组')
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!proxy?.$axios) {
|
|
||||||
Message.error('查询失败:axios 未就绪')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assetLoading.value = true
|
assetLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await proxy.$axios({
|
const res = await request({
|
||||||
url: 'api/byteAsset/listAssets',
|
url: 'api/byteAsset/listAssets',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: buildReferenceListAssetsPayload()
|
||||||
Filter: {
|
|
||||||
GroupIds: [gid],
|
|
||||||
GroupType: 'AIGC',
|
|
||||||
Statuses: ['Active']
|
|
||||||
},
|
|
||||||
PageNumber: 1,
|
|
||||||
PageSize: 100,
|
|
||||||
SortBy: 'CreateTime',
|
|
||||||
SortOrder: 'Desc'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
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
|
const assets = rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const at = String(row?.AssetType || row?.assetType || 'Image').toLowerCase()
|
const at = String(row?.AssetType || row?.assetType || 'Image').toLowerCase()
|
||||||
|
|
@ -504,7 +608,7 @@ const confirmPickAssets = () => {
|
||||||
|
|
||||||
const isReferenceMode = isReference.value
|
const isReferenceMode = isReference.value
|
||||||
if (isReferenceMode && !String(assetGroupId.value || '').trim()) {
|
if (isReferenceMode && !String(assetGroupId.value || '').trim()) {
|
||||||
Message.warning('参考图模式请先填写 GroupId')
|
Message.warning('请先选择素材组')
|
||||||
input.value = ''
|
input.value = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -581,8 +685,7 @@ const confirmPickAssets = () => {
|
||||||
let assetId = entry.assetId || ''
|
let assetId = entry.assetId || ''
|
||||||
if (isReferenceMode) {
|
if (isReferenceMode) {
|
||||||
const gid = String(assetGroupId.value || '').trim()
|
const gid = String(assetGroupId.value || '').trim()
|
||||||
if (!gid) throw new Error('请先填写 GroupId')
|
if (!gid) throw new Error('请先选择素材组')
|
||||||
if (!proxy?.$axios) throw new Error('上传资产失败:axios 未就绪')
|
|
||||||
const mt = entry.mediaType || 'image'
|
const mt = entry.mediaType || 'image'
|
||||||
const assetType =
|
const assetType =
|
||||||
mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image'
|
mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image'
|
||||||
|
|
@ -591,11 +694,14 @@ const confirmPickAssets = () => {
|
||||||
fd.append('groupId', gid)
|
fd.append('groupId', gid)
|
||||||
fd.append('assetType', assetType)
|
fd.append('assetType', assetType)
|
||||||
fd.append('name', entry?.name || '')
|
fd.append('name', entry?.name || '')
|
||||||
const createRes = await proxy.$axios({
|
const createRes = await request({
|
||||||
url: 'api/byteAsset/createAsset',
|
url: 'api/byteAsset/createAsset',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: fd
|
data: fd
|
||||||
})
|
})
|
||||||
|
if (createRes.code !== 200) {
|
||||||
|
throw new Error(createRes?.msg || '创建素材失败')
|
||||||
|
}
|
||||||
assetId = createRes?.data?.Id || createRes?.data?.id || ''
|
assetId = createRes?.data?.Id || createRes?.data?.id || ''
|
||||||
if (!assetId) throw new Error(createRes?.msg || '创建素材失败:未返回资产ID')
|
if (!assetId) throw new Error(createRes?.msg || '创建素材失败:未返回资产ID')
|
||||||
}
|
}
|
||||||
|
|
@ -1125,6 +1231,7 @@ const selectMentionItem = (item) => {
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getEditorPlainText,
|
getEditorPlainText,
|
||||||
getImageReferenceContentItems,
|
getImageReferenceContentItems,
|
||||||
|
clearAll,
|
||||||
clearPromptOnly: () => {
|
clearPromptOnly: () => {
|
||||||
setPrompt('')
|
setPrompt('')
|
||||||
if (editorRef.value) editorRef.value.innerHTML = ''
|
if (editorRef.value) editorRef.value.innerHTML = ''
|
||||||
|
|
@ -1135,7 +1242,16 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
.vg-compose-root {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.vg-compose-card {
|
.vg-compose-card {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -1144,7 +1260,138 @@ defineExpose({
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
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 {
|
.vg-compose-left {
|
||||||
|
|
@ -1270,20 +1517,25 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-asset-group-input {
|
.vg-asset-group-input {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 30px;
|
font-size: 11px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vg-asset-group-input:focus {
|
:deep(.arco-select-view-single) {
|
||||||
border-color: rgba(0, 202, 224, 0.55);
|
background: rgba(0, 0, 0, 0.25) !important;
|
||||||
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.12);
|
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 {
|
.vg-asset-picker {
|
||||||
|
|
@ -1441,6 +1693,61 @@ defineExpose({
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
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 {
|
.vg-compose-toolbar {
|
||||||
|
|
@ -1650,20 +1957,76 @@ defineExpose({
|
||||||
box-shadow: 0 0 0 3px rgba(0, 202, 224, 0.15);
|
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;
|
flex-direction: column;
|
||||||
min-height: 0;
|
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 {
|
.vg-compose-left {
|
||||||
width: 100%;
|
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) {
|
@media (max-width: 1919px) {
|
||||||
.vg-compose-card {
|
.vg-compose-card--split {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
@ -1673,14 +2036,6 @@ defineExpose({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-reference-panel {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vg-compose-media-scroll {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vg-compose-right {
|
.vg-compose-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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-total">总数:{{ totalCount }}</div>
|
||||||
<div class="ag-table-wrap">
|
<div class="ag-table-wrap">
|
||||||
<table class="ag-table">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
|
@ -88,7 +98,9 @@
|
||||||
:page-size="filters.pageSize"
|
:page-size="filters.pageSize"
|
||||||
show-total
|
show-total
|
||||||
show-jumper
|
show-jumper
|
||||||
@change="search"
|
show-page-size
|
||||||
|
:page-size-options="listPageSizeOptions"
|
||||||
|
@change="onGroupPageChange"
|
||||||
@page-size-change="onPageSizeChange" />
|
@page-size-change="onPageSizeChange" />
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
@ -100,27 +112,37 @@
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="createVisible"
|
v-model:visible="createVisible"
|
||||||
title="新增资源组"
|
title="新增资源组"
|
||||||
|
width="520px"
|
||||||
:confirm-loading="createLoading"
|
:confirm-loading="createLoading"
|
||||||
@ok="createGroup">
|
@ok="createGroup">
|
||||||
<div class="ag-create-form">
|
<!-- 弹层 teleported 到 body,使用 FormItem 保证标签与 dark 样式生效 -->
|
||||||
<div class="ag-field">
|
<a-form :model="createForm" layout="horizontal" auto-label-width class="ag-create-modal-form">
|
||||||
<label>名称</label>
|
<a-form-item label="名称" required>
|
||||||
<a-input v-model="createForm.name" placeholder="请输入资源组名称(<=64字符)" />
|
<a-input
|
||||||
</div>
|
v-model="createForm.name"
|
||||||
<div class="ag-field">
|
placeholder="请输入资源组名称(≤64 字符)"
|
||||||
<label>描述</label>
|
:max-length="64"
|
||||||
|
allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="描述">
|
||||||
<a-textarea
|
<a-textarea
|
||||||
v-model="createForm.description"
|
v-model="createForm.description"
|
||||||
:max-length="300"
|
:max-length="300"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="请输入描述(<=300字符)" />
|
:auto-size="{ minRows: 3, maxRows: 8 }"
|
||||||
</div>
|
placeholder="请输入描述(≤300 字符)" />
|
||||||
</div>
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||||
|
|
||||||
|
const LIST_PAGE_NUMBER_MAX = 100
|
||||||
|
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AssetGroupManage',
|
name: 'AssetGroupManage',
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -146,7 +168,8 @@ export default {
|
||||||
sortOrder: 'Desc'
|
sortOrder: 'Desc'
|
||||||
},
|
},
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
items: []
|
items: [],
|
||||||
|
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -158,6 +181,19 @@ export default {
|
||||||
this.search(1)
|
this.search(1)
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
openCreateDialog() {
|
||||||
this.createVisible = true
|
this.createVisible = true
|
||||||
},
|
},
|
||||||
|
|
@ -179,10 +215,8 @@ export default {
|
||||||
url: 'api/byteAssetGroup/createAssetGroup',
|
url: 'api/byteAssetGroup/createAssetGroup',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
Name: name,
|
name,
|
||||||
Description: String(this.createForm.description || '').trim(),
|
description: String(this.createForm.description || '').trim()
|
||||||
GroupType: 'AIGC',
|
|
||||||
ProjectName: 'default'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
|
@ -201,22 +235,22 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async search(page = this.filters.pageNumber) {
|
async search(page = this.filters.pageNumber) {
|
||||||
this.filters.pageNumber = Number(page) || 1
|
this.filters.pageNumber = this.clampGroupPage(page)
|
||||||
this.listLoading = true
|
this.listLoading = true
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
Filter: {
|
filter: {
|
||||||
GroupType: this.filters.groupType || 'AIGC'
|
groupType: this.filters.groupType || 'AIGC'
|
||||||
},
|
},
|
||||||
PageNumber: this.filters.pageNumber,
|
pageNumber: this.filters.pageNumber,
|
||||||
PageSize: this.filters.pageSize,
|
pageSize: this.filters.pageSize,
|
||||||
SortBy: this.filters.sortBy,
|
sortBy: this.filters.sortBy,
|
||||||
SortOrder: this.filters.sortOrder
|
sortOrder: this.filters.sortOrder
|
||||||
}
|
}
|
||||||
const name = String(this.filters.name || '').trim()
|
const name = String(this.filters.name || '').trim()
|
||||||
if (name) payload.Filter.name = name
|
if (name) payload.filter.name = name
|
||||||
const ids = this.buildGroupIds()
|
const ids = this.buildGroupIds()
|
||||||
if (ids.length) payload.Filter.GroupIds = ids
|
if (ids.length) payload.filter.groupIds = ids
|
||||||
|
|
||||||
const res = await this.$axios({
|
const res = await this.$axios({
|
||||||
url: 'api/byteAssetGroup/listAssetGroups',
|
url: 'api/byteAssetGroup/listAssetGroups',
|
||||||
|
|
@ -224,8 +258,8 @@ export default {
|
||||||
data: payload
|
data: payload
|
||||||
})
|
})
|
||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
this.totalCount = Number(data.TotalCount || 0)
|
this.totalCount = byteApiTotalCount(data)
|
||||||
this.items = Array.isArray(data.Items) ? data.Items : []
|
this.items = byteApiItems(data)
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
this.$message.error(res.msg || '查询失败')
|
this.$message.error(res.msg || '查询失败')
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +270,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPageSizeChange(size) {
|
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)
|
this.search(1)
|
||||||
},
|
},
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
|
|
@ -313,12 +348,6 @@ export default {
|
||||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.ag-create-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-field {
|
.ag-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -343,23 +372,53 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-table-wrap {
|
.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: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-table {
|
.ag-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 12px;
|
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 th,
|
||||||
.ag-table td {
|
.ag-table td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-table th {
|
.ag-table th {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
<section class="asset-left">
|
<section class="asset-left">
|
||||||
<div class="asset-left-head">
|
<div class="asset-left-head">
|
||||||
<div class="panel-title">素材组树</div>
|
<div class="panel-title">素材组树</div>
|
||||||
<div class="field actions">
|
|
||||||
<a-button size="mini" type="outline" :loading="groupLoading" @click="loadGroups">刷新分组</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="group-tree">
|
<div class="group-tree">
|
||||||
<div
|
<div
|
||||||
|
|
@ -34,8 +31,17 @@
|
||||||
<div class="panel-title mtop">查询素材</div>
|
<div class="panel-title mtop">查询素材</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>GroupId</label>
|
<label>素材组</label>
|
||||||
<a-input v-model="filters.groupId" placeholder="按组过滤(默认选中左侧)" />
|
<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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
|
|
@ -89,9 +95,45 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="it in items" :key="it.Id || it.id">
|
<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>{{ 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.GroupId || it.groupId || '-' }}</td>
|
||||||
<td>{{ it.AssetType || it.assetType || '-' }}</td>
|
<td>{{ it.AssetType || it.assetType || '-' }}</td>
|
||||||
<td>{{ it.Status || it.status || '-' }}</td>
|
<td>{{ it.Status || it.status || '-' }}</td>
|
||||||
|
|
@ -114,7 +156,9 @@
|
||||||
:page-size="filters.pageSize"
|
:page-size="filters.pageSize"
|
||||||
show-total
|
show-total
|
||||||
show-jumper
|
show-jumper
|
||||||
@change="searchAssets"
|
show-page-size
|
||||||
|
:page-size-options="listPageSizeOptions"
|
||||||
|
@change="onAssetPageChange"
|
||||||
@page-size-change="onPageSizeChange" />
|
@page-size-change="onPageSizeChange" />
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
@ -126,46 +170,58 @@
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="createAssetVisible"
|
v-model:visible="createAssetVisible"
|
||||||
title="新增素材"
|
title="新增素材"
|
||||||
|
width="520px"
|
||||||
:confirm-loading="createLoading"
|
:confirm-loading="createLoading"
|
||||||
@ok="createAsset">
|
@ok="createAsset">
|
||||||
<div class="create-form-vertical">
|
<a-form :model="createForm" layout="horizontal" auto-label-width class="asset-create-modal-form">
|
||||||
<div class="field">
|
<a-form-item label="素材组" required>
|
||||||
<label>素材组</label>
|
<a-select v-model="createForm.groupId" placeholder="请选择素材组" allow-clear>
|
||||||
<a-select v-model="createForm.groupId" placeholder="请选择素材组">
|
|
||||||
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
<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.Name || g.name || (g.Id || g.id) }}
|
||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</a-form-item>
|
||||||
<div class="field">
|
<a-form-item label="上传文件" required>
|
||||||
<label>上传素材</label>
|
<input class="asset-file-input" type="file" @change="onFileChange" />
|
||||||
<input type="file" @change="onFileChange" />
|
<div class="asset-file-hint">{{ createForm.fileName || '未选择文件' }}</div>
|
||||||
<div class="group-id">{{ createForm.fileName || '未选择文件' }}</div>
|
</a-form-item>
|
||||||
</div>
|
<a-form-item label="素材名称">
|
||||||
<div class="field">
|
<a-input v-model="createForm.name" placeholder="素材名称(可选)" allow-clear />
|
||||||
<label>素材名称</label>
|
</a-form-item>
|
||||||
<a-input v-model="createForm.name" placeholder="素材名称(可选)" />
|
<a-form-item label="类型">
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>类型</label>
|
|
||||||
<a-select v-model="createForm.assetType">
|
<a-select v-model="createForm.assetType">
|
||||||
<a-option value="Image">图片</a-option>
|
<a-option value="Image">图片</a-option>
|
||||||
<a-option value="Video">视频</a-option>
|
<a-option value="Video">视频</a-option>
|
||||||
<a-option value="Audio">音频</a-option>
|
<a-option value="Audio">音频</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</a-form-item>
|
||||||
</div>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 GROUP_LIST_API = 'api/byteAssetGroup/listAssetGroups'
|
||||||
const ASSET_CREATE_API = 'api/byteAsset/createAsset'
|
const ASSET_CREATE_API = 'api/byteAsset/createAsset'
|
||||||
const ASSET_LIST_API = 'api/byteAsset/listAssets'
|
const ASSET_LIST_API = 'api/byteAsset/listAssets'
|
||||||
const ASSET_GET_API = 'api/byteAsset/getAsset'
|
const ASSET_GET_API = 'api/byteAsset/getAsset'
|
||||||
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
|
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
|
||||||
|
|
||||||
|
/** 列表页码上限(与接口分页一致);每页条数可选项 */
|
||||||
|
const LIST_PAGE_NUMBER_MAX = 100
|
||||||
|
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AssetManage',
|
name: 'AssetManage',
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -195,7 +251,9 @@ export default {
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
items: [],
|
items: [],
|
||||||
detailVisible: false,
|
detailVisible: false,
|
||||||
detailData: null
|
detailData: null,
|
||||||
|
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS,
|
||||||
|
thumbError: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -207,6 +265,46 @@ export default {
|
||||||
this.loadGroups()
|
this.loadGroups()
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
async openCreateAssetDialog() {
|
||||||
await this.loadGroups()
|
await this.loadGroups()
|
||||||
this.createForm.groupId = this.selectedGroupId || this.createForm.groupId || ''
|
this.createForm.groupId = this.selectedGroupId || this.createForm.groupId || ''
|
||||||
|
|
@ -222,17 +320,11 @@ export default {
|
||||||
const res = await this.$axios({
|
const res = await this.$axios({
|
||||||
url: GROUP_LIST_API,
|
url: GROUP_LIST_API,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: { ...GROUP_LIST_REQUEST_BODY }
|
||||||
Filter: { GroupType: 'AIGC' },
|
|
||||||
PageNumber: 1,
|
|
||||||
PageSize: 100,
|
|
||||||
SortBy: 'CreateTime',
|
|
||||||
SortOrder: 'Desc'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.groups = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
this.groups = byteApiItems(res?.data)
|
||||||
if (!this.selectedGroupId && this.groups.length) {
|
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.selectedGroupId = gid
|
||||||
this.createForm.groupId = gid
|
this.createForm.groupId = gid
|
||||||
this.filters.groupId = gid
|
this.filters.groupId = gid
|
||||||
|
|
@ -245,12 +337,18 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectGroup(g) {
|
selectGroup(g) {
|
||||||
const gid = g?.Id || g?.id
|
const gid = String(g?.Id || g?.id || '')
|
||||||
this.selectedGroupId = gid
|
this.selectedGroupId = gid
|
||||||
this.createForm.groupId = gid
|
this.createForm.groupId = gid
|
||||||
this.filters.groupId = gid
|
this.filters.groupId = gid
|
||||||
this.searchAssets(1)
|
this.searchAssets(1)
|
||||||
},
|
},
|
||||||
|
onFilterGroupChange(val) {
|
||||||
|
const gid = val != null && val !== '' ? String(val) : ''
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
onFileChange(e) {
|
onFileChange(e) {
|
||||||
const file = e?.target?.files?.[0]
|
const file = e?.target?.files?.[0]
|
||||||
this.createForm.file = file || null
|
this.createForm.file = file || null
|
||||||
|
|
@ -290,32 +388,33 @@ export default {
|
||||||
},
|
},
|
||||||
buildListPayload() {
|
buildListPayload() {
|
||||||
const filter = {
|
const filter = {
|
||||||
GroupType: 'AIGC'
|
groupType: 'AIGC'
|
||||||
}
|
}
|
||||||
const gid = String(this.filters.groupId || '').trim()
|
const gid = String(this.filters.groupId || '').trim()
|
||||||
if (gid) filter.GroupIds = [gid]
|
if (gid) filter.groupIds = [gid]
|
||||||
const name = String(this.filters.name || '').trim()
|
const name = String(this.filters.name || '').trim()
|
||||||
if (name) filter.Name = name
|
if (name) filter.name = name
|
||||||
if (this.filters.status) filter.Statuses = [this.filters.status]
|
if (this.filters.status) filter.statuses = [this.filters.status]
|
||||||
return {
|
return {
|
||||||
Filter: filter,
|
filter,
|
||||||
PageNumber: this.filters.pageNumber,
|
pageNumber: this.filters.pageNumber,
|
||||||
PageSize: this.filters.pageSize,
|
pageSize: this.filters.pageSize,
|
||||||
SortBy: this.filters.sortBy,
|
sortBy: this.filters.sortBy,
|
||||||
SortOrder: this.filters.sortOrder
|
sortOrder: this.filters.sortOrder
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async searchAssets(page = this.filters.pageNumber) {
|
async searchAssets(page = this.filters.pageNumber) {
|
||||||
this.filters.pageNumber = Number(page) || 1
|
this.filters.pageNumber = this.clampAssetPage(page)
|
||||||
this.listLoading = true
|
this.listLoading = true
|
||||||
|
this.thumbError = {}
|
||||||
try {
|
try {
|
||||||
const res = await this.$axios({
|
const res = await this.$axios({
|
||||||
url: ASSET_LIST_API,
|
url: ASSET_LIST_API,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: this.buildListPayload()
|
data: this.buildListPayload()
|
||||||
})
|
})
|
||||||
this.totalCount = Number(res?.data?.TotalCount || 0)
|
this.totalCount = byteApiTotalCount(res?.data)
|
||||||
this.items = Array.isArray(res?.data?.Items) ? res.data.Items : []
|
this.items = byteApiItems(res?.data)
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
this.$message.error(res.msg || '查询素材失败')
|
this.$message.error(res.msg || '查询素材失败')
|
||||||
}
|
}
|
||||||
|
|
@ -326,12 +425,18 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPageSizeChange(size) {
|
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)
|
this.searchAssets(1)
|
||||||
},
|
},
|
||||||
resetFilters() {
|
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 = {
|
this.filters = {
|
||||||
groupId: this.selectedGroupId || '',
|
groupId: gid,
|
||||||
name: '',
|
name: '',
|
||||||
status: '',
|
status: '',
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
|
|
@ -406,6 +511,10 @@ export default {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
.asset-right {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.panel-title {
|
.panel-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -472,11 +581,6 @@ export default {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.create-form-vertical {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.mtop {
|
.mtop {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
@ -487,14 +591,53 @@ export default {
|
||||||
}
|
}
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.asset-table {
|
.asset-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 12px;
|
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 th,
|
||||||
.asset-table td {
|
.asset-table td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
@ -506,10 +649,51 @@ export default {
|
||||||
color: rgba(255, 255, 255, 0.72);
|
color: rgba(255, 255, 255, 0.72);
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
}
|
}
|
||||||
.url-cell {
|
.asset-url-cell {
|
||||||
max-width: 280px;
|
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;
|
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 {
|
.pager {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -539,3 +723,28 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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,9 +16,13 @@
|
||||||
<div
|
<div
|
||||||
v-for="row in sortedTaskRows"
|
v-for="row in sortedTaskRows"
|
||||||
:key="row.id"
|
: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-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">
|
<div class="vg-chat-user-text">
|
||||||
<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
|
<template v-for="(seg, idx) in getRowPromptSegments(row)" :key="`${row.id || 'row'}_${idx}`">
|
||||||
<span v-if="seg.type === 'text'">{{ seg.text }}</span>
|
<span v-if="seg.type === 'text'">{{ seg.text }}</span>
|
||||||
|
|
@ -42,14 +46,53 @@
|
||||||
preload="metadata" />
|
preload="metadata" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||||
</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-divider"></div>
|
||||||
|
|
||||||
<!-- AI响应部分(视频结果) -->
|
|
||||||
<div class="vg-chat-ai-section">
|
<div class="vg-chat-ai-section">
|
||||||
<div class="vg-chat-ai-top">
|
<div class="vg-chat-ai-top">
|
||||||
<div class="vg-chat-ai-status">
|
<div class="vg-chat-ai-status">
|
||||||
|
|
@ -57,14 +100,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
|
<div class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="vg-chat-result">
|
||||||
<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
|
<video
|
||||||
v-if="isVideoUrl(row.result)"
|
v-if="isVideoUrl(row.result)"
|
||||||
:src="row.result"
|
:src="row.result"
|
||||||
|
|
@ -74,31 +110,8 @@
|
||||||
<a :href="row.result" target="_blank" rel="noreferrer">查看结果</a>
|
<a :href="row.result" target="_blank" rel="noreferrer">查看结果</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="chatLoadingMore" class="vg-chat-loadmore">
|
<div v-if="chatLoadingMore" class="vg-chat-loadmore">
|
||||||
|
|
@ -122,10 +135,59 @@
|
||||||
'描述画面与动态,例如:阳光下的女孩在海边起舞…'
|
'描述画面与动态,例如:阳光下的女孩在海边起舞…'
|
||||||
"
|
"
|
||||||
:video-mode="videoMode">
|
:video-mode="videoMode">
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="vg-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-toolbar-settings">
|
|
||||||
<div class="vg-params-row">
|
<div class="vg-params-row">
|
||||||
<div class="vg-param">
|
<div class="vg-param">
|
||||||
<span class="vg-param-label">生成模式</span>
|
<span class="vg-param-label">生成模式</span>
|
||||||
|
|
@ -175,7 +237,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vg-toolbar-actions">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="vg-submit-circle"
|
class="vg-submit-circle"
|
||||||
|
|
@ -513,6 +585,16 @@ export default {
|
||||||
return '进行中'
|
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) {
|
parseDateTimeToMs(value) {
|
||||||
if (!value) return 0
|
if (!value) return 0
|
||||||
const ms = new Date(value).getTime()
|
const ms = new Date(value).getTime()
|
||||||
|
|
@ -719,6 +801,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearReferenceCompose() {
|
||||||
|
this.$refs.videoComposeRef?.clearAll?.()
|
||||||
|
},
|
||||||
|
|
||||||
async generateVideo() {
|
async generateVideo() {
|
||||||
if (this.generateLoading) return
|
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);
|
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 {
|
.vg-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 1 auto;
|
flex: 1 1 0%;
|
||||||
order: 1;
|
order: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: min(48vh, 560px);
|
max-height: none;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
@ -1069,15 +1155,14 @@ export default {
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: visible;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-generator-inner {
|
.vg-generator-inner {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
@ -1176,6 +1261,13 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(.vg-compose-root) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-editor-container {
|
.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 {
|
.vg-toolbar-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: center;
|
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 {
|
.vg-submit-circle {
|
||||||
|
|
@ -1388,10 +1545,12 @@ export default {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 768px) {
|
||||||
.vg-body {
|
.vg-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
min-height: min(36vh, 400px);
|
||||||
}
|
}
|
||||||
.vg-left-rail {
|
.vg-left-rail {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|
@ -1418,6 +1577,12 @@ export default {
|
||||||
min-height: 260px;
|
min-height: 260px;
|
||||||
max-height: min(55vh, 480px);
|
max-height: min(55vh, 480px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 对话内视频小屏占满宽,避免横向截断 */
|
||||||
|
.vg-chat-result video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-task-empty {
|
.vg-task-empty {
|
||||||
|
|
@ -1428,13 +1593,13 @@ export default {
|
||||||
|
|
||||||
.vg-chat-section {
|
.vg-chat-section {
|
||||||
order: 0;
|
order: 0;
|
||||||
flex: 1 1 auto;
|
flex: 3 1 0%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
background: var(--vg-panel);
|
background: var(--vg-panel);
|
||||||
border: 1px solid var(--vg-border);
|
border: 1px solid var(--vg-border);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
min-height: min(42vh, 480px);
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
@ -1477,12 +1642,29 @@ export default {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
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 {
|
.vg-chat-user-section {
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
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 {
|
.vg-chat-ai-section {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
@ -1530,6 +1712,53 @@ export default {
|
||||||
color: var(--vg-muted);
|
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 {
|
.vg-chat-ai-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1690,7 +1919,7 @@ export default {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 小于 1920×1080 常见宽度:底部参数区纵向排布,避免挤压富文本 */
|
/* 小于 1920×1080:工具条参数纵向排布;整体仍保持约 75%/25% 弹性分配 */
|
||||||
@media (max-width: 1919px) {
|
@media (max-width: 1919px) {
|
||||||
.video-gen {
|
.video-gen {
|
||||||
padding: 14px 12px 12px;
|
padding: 14px 12px 12px;
|
||||||
|
|
@ -1698,14 +1927,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-chat-section {
|
.vg-chat-section {
|
||||||
min-height: min(40vh, 440px);
|
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-body {
|
|
||||||
max-height: min(50vh, 540px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vg-generator-inner {
|
.vg-generator-inner {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue