fix: 生成页面优化布局
This commit is contained in:
parent
eb5211b9e4
commit
0d4b1d18be
|
|
@ -51,12 +51,12 @@ export default {
|
|||
this.$router.replace('/403')
|
||||
},
|
||||
ok() {
|
||||
// 满18+:关闭弹窗并直接进入视频生成
|
||||
// 满18+:关闭弹窗并进入首页
|
||||
Promise.resolve(this.$store.dispatch('main/setForbidden', false)).finally(() => {
|
||||
// 默认语言:繁体中文(zh_HK)
|
||||
this.$store.dispatch('main/setLanguage', 'zh_HK')
|
||||
i18n.global.locale = 'zh_HK'
|
||||
this.$router.push({ name: 'video-gen' })
|
||||
this.$router.push({ name: 'index' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,27 @@
|
|||
class="vg-compose-card"
|
||||
:class="{
|
||||
'vg-compose-card--reference': isReference,
|
||||
'vg-compose-card--split': !isReference && !isTextToVideo,
|
||||
'vg-compose-card--solo': isTextToVideo
|
||||
'vg-compose-card--split': !isReference
|
||||
}">
|
||||
<!-- 参考图模式:四列 — 参考图 / 资产 / 选择区域 / 富文本 -->
|
||||
<template v-if="isReference">
|
||||
<div class="vg-compose-mod vg-compose-mod--ref">
|
||||
<div
|
||||
class="vg-compose-mod vg-compose-mod--ref"
|
||||
:class="{ 'vg-compose-mod--dragover': referenceDragDepth > 0 }"
|
||||
@dragenter.prevent="onReferenceDragEnter"
|
||||
@dragleave.prevent="onReferenceDragLeave"
|
||||
@dragover.prevent="onReferenceDragOver"
|
||||
@drop.prevent="onReferenceDrop"
|
||||
@paste="onReferencePaste">
|
||||
<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 class="vg-compose-empty-text">
|
||||
点击或拖拽/粘贴添加参考(仅云存储,不入库);「上传资产」会写入素材库并加入参考
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="vg-compose-media-scroll vg-compose-media-scroll--vertical">
|
||||
<div
|
||||
|
|
@ -78,7 +86,7 @@
|
|||
size="small"
|
||||
type="primary"
|
||||
:disabled="!hasAssetGroupId"
|
||||
@click="openFilePicker">
|
||||
@click="openFilePickerForAssetUpload">
|
||||
上传资产
|
||||
</mf-button>
|
||||
</span>
|
||||
|
|
@ -92,8 +100,16 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 首帧 / 首尾帧:保留左侧素材列 -->
|
||||
<div class="vg-compose-left" v-else-if="!isTextToVideo">
|
||||
<!-- 文生视频:不展示参考图区域块,仅展示参数选择 -->
|
||||
<div class="vg-compose-left" v-else-if="isTextToVideo">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="vg-compose-left vg-compose-left--two-col" v-else-if="!isTextToVideo">
|
||||
<!-- 首帧模式 -->
|
||||
<div v-if="isFirstFrame" class="vg-first-frame-panel">
|
||||
<div class="vg-panel-title">首帧图</div>
|
||||
|
|
@ -141,17 +157,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数选择列:已由 VideoGen.vue 统一通过 reference-params slot 渲染 -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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-compose-right-body', { 'vg-reference-editor-row': isReference }]">
|
||||
<div
|
||||
class="vg-compose-right"
|
||||
:class="{ 'vg-compose-right--reference': isReference || isTextToVideo || isFirstFrame || isFirstLastFrame }">
|
||||
<div class="vg-compose-right-body vg-reference-editor-row">
|
||||
<div class="vg-rich-editor-wrap">
|
||||
<div
|
||||
ref="editorRef"
|
||||
|
|
@ -182,12 +200,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isReference" class="vg-reference-actions-col">
|
||||
<div class="vg-reference-actions-col">
|
||||
<slot name="toolbar"></slot>
|
||||
</div>
|
||||
<template v-else>
|
||||
<slot name="toolbar"></slot>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -300,6 +315,10 @@ const assetGroups = ref([])
|
|||
const assetPickerVisible = ref(false)
|
||||
const assetQueryResults = ref([])
|
||||
const assetPickerSelectedKeys = ref([])
|
||||
/** 参考图区拖拽高亮(嵌套 dragenter/leave 计数) */
|
||||
const referenceDragDepth = ref(0)
|
||||
/** 仅「上传资产」按钮为 true;左侧直接上传/拖拽/粘贴为 false */
|
||||
const createAssetIntent = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
|
|
@ -382,18 +401,34 @@ const buildReferenceListAssetsPayload = () => {
|
|||
}
|
||||
|
||||
const onReferenceEmptyAreaClick = () => {
|
||||
openReferenceDirectUpload()
|
||||
}
|
||||
|
||||
/** 参考图区直接上传(仅 COS,不调 createAsset) */
|
||||
const openReferenceDirectUpload = () => {
|
||||
if (!isReference.value) return
|
||||
createAssetIntent.value = false
|
||||
currentUploadIndex.value = -1
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
/** 仅「上传资产」:COS + createAsset 并写入素材组 */
|
||||
const openFilePickerForAssetUpload = () => {
|
||||
if (!hasAssetGroupId.value) {
|
||||
Message.warning('请先选择素材组')
|
||||
return
|
||||
}
|
||||
openFilePicker()
|
||||
createAssetIntent.value = true
|
||||
currentUploadIndex.value = -1
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const openFilePicker = () => {
|
||||
if (isReference.value && !hasAssetGroupId.value) {
|
||||
Message.warning('请先选择素材组')
|
||||
if (isReference.value) {
|
||||
openReferenceDirectUpload()
|
||||
return
|
||||
}
|
||||
createAssetIntent.value = false
|
||||
currentUploadIndex.value = -1
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
|
@ -601,20 +636,17 @@ const confirmPickAssets = () => {
|
|||
assetPickerSelectedKeys.value = []
|
||||
}
|
||||
|
||||
const handleSelectFiles = async (event) => {
|
||||
const input = event.target
|
||||
const files = Array.from(input.files || [])
|
||||
if (!files.length) return
|
||||
const processFilesList = async (files, options = {}) => {
|
||||
if (!files || !files.length) return
|
||||
|
||||
const isReferenceMode = isReference.value
|
||||
if (isReferenceMode && !String(assetGroupId.value || '').trim()) {
|
||||
const wantCreateAsset = isReferenceMode && options.createAsset === true
|
||||
if (wantCreateAsset && !String(assetGroupId.value || '').trim()) {
|
||||
Message.warning('请先选择素材组')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
let targetList = [...mediaList.value]
|
||||
|
||||
// 首尾帧模式特殊处理索引
|
||||
if (isFirstLastFrame.value && currentUploadIndex.value >= 0) {
|
||||
const idx = currentUploadIndex.value
|
||||
if (files.length > 0) {
|
||||
|
|
@ -622,20 +654,25 @@ const confirmPickAssets = () => {
|
|||
const mediaType = detectMediaType(file)
|
||||
if (mediaType !== 'image') {
|
||||
Message.warning('首尾帧仅支持图片')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
const id = `frame_${Date.now()}`
|
||||
const localPreview = URL.createObjectURL(file)
|
||||
const entry = { id, url: localPreview, mediaType: 'image', name: file.name, _fileRef: file, isUploading: true, label: idx === 0 ? '[首帧]' : '[尾帧]' }
|
||||
const entry = {
|
||||
id,
|
||||
url: localPreview,
|
||||
mediaType: 'image',
|
||||
name: file.name,
|
||||
_fileRef: file,
|
||||
isUploading: true,
|
||||
label: idx === 0 ? '[首帧]' : '[尾帧]'
|
||||
}
|
||||
targetList[idx] = entry
|
||||
}
|
||||
} else {
|
||||
// 其他模式正常处理
|
||||
const remain = props.maxMediaCount - mediaList.value.length
|
||||
if (remain <= 0 && !isReferenceMode) {
|
||||
Message.warning(`最多添加 ${props.maxMediaCount} 个参考素材`)
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -671,8 +708,7 @@ const confirmPickAssets = () => {
|
|||
setMediaList(targetList)
|
||||
await nextTick()
|
||||
|
||||
// 上传处理
|
||||
const toUpload = targetList.filter(item => item.isUploading)
|
||||
const toUpload = targetList.filter((item) => item.isUploading)
|
||||
for (const entry of toUpload) {
|
||||
try {
|
||||
const res = await uploadFile({
|
||||
|
|
@ -683,7 +719,7 @@ const confirmPickAssets = () => {
|
|||
const url = extractUploadUrlFromResponse(res)
|
||||
if (!url) throw new Error(res?.msg || '未返回文件地址')
|
||||
let assetId = entry.assetId || ''
|
||||
if (isReferenceMode) {
|
||||
if (wantCreateAsset) {
|
||||
const gid = String(assetGroupId.value || '').trim()
|
||||
if (!gid) throw new Error('请先选择素材组')
|
||||
const mt = entry.mediaType || 'image'
|
||||
|
|
@ -715,7 +751,7 @@ const confirmPickAssets = () => {
|
|||
)
|
||||
)
|
||||
if (isReferenceMode) {
|
||||
Message.success('已上传完成')
|
||||
Message.success(wantCreateAsset ? '已上传并写入素材库' : '已上传完成')
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -726,11 +762,57 @@ const confirmPickAssets = () => {
|
|||
Message.error('上传失败,请重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.value = ''
|
||||
const handleSelectFiles = async (event) => {
|
||||
const input = event.target
|
||||
const files = Array.from(input.files || [])
|
||||
const createAsset =
|
||||
isReference.value && createAssetIntent.value === true
|
||||
try {
|
||||
await processFilesList(files, { createAsset })
|
||||
} finally {
|
||||
createAssetIntent.value = false
|
||||
}
|
||||
if (input) input.value = ''
|
||||
currentUploadIndex.value = -1
|
||||
}
|
||||
|
||||
const onReferenceDragEnter = () => {
|
||||
if (!isReference.value) return
|
||||
referenceDragDepth.value++
|
||||
}
|
||||
const onReferenceDragLeave = () => {
|
||||
if (!isReference.value) return
|
||||
referenceDragDepth.value = Math.max(0, referenceDragDepth.value - 1)
|
||||
}
|
||||
const onReferenceDragOver = () => {
|
||||
if (!isReference.value) return
|
||||
referenceDragDepth.value = Math.max(1, referenceDragDepth.value)
|
||||
}
|
||||
const onReferenceDrop = async (e) => {
|
||||
if (!isReference.value) return
|
||||
referenceDragDepth.value = 0
|
||||
const files = Array.from(e.dataTransfer?.files || [])
|
||||
if (!files.length) return
|
||||
await processFilesList(files, { createAsset: false })
|
||||
}
|
||||
|
||||
const onReferencePaste = async (e) => {
|
||||
if (!isReference.value) return
|
||||
const items = e.clipboardData?.items
|
||||
if (!items || !items.length) return
|
||||
const files = []
|
||||
for (const item of items) {
|
||||
if (item.kind !== 'file') continue
|
||||
const f = item.getAsFile()
|
||||
if (f && f.type.startsWith('image/')) files.push(f)
|
||||
}
|
||||
if (!files.length) return
|
||||
e.preventDefault()
|
||||
await processFilesList(files, { createAsset: false })
|
||||
}
|
||||
|
||||
const onPromptInput = (e) => {
|
||||
setPrompt(e.target.value)
|
||||
}
|
||||
|
|
@ -1159,33 +1241,13 @@ const onEditorKeyup = (e) => {
|
|||
}
|
||||
}
|
||||
|
||||
const selectMentionItem = (item) => {
|
||||
if (!item?.url || !editorRef.value) return
|
||||
const buildReferenceHolderElement = (item) => {
|
||||
const kind = item.mediaType || 'image'
|
||||
const key = `${kind}:${(item.assetId || '').trim() || String(item.url || '').trim()}`
|
||||
const keys = getUniqueRefKeysInDocForKind(kind)
|
||||
if (!keys.has(key) && keys.size >= MAX_REFERENCE_UNIQUE_KIND) {
|
||||
const label = kind === 'video' ? '视频' : kind === 'audio' ? '音频' : '图片'
|
||||
Message.warning(`同类参考${label}最多 ${MAX_REFERENCE_UNIQUE_KIND} 种,无法再插入新素材`)
|
||||
mentionVisible.value = false
|
||||
return
|
||||
}
|
||||
editorRef.value.focus()
|
||||
restoreSelection()
|
||||
removeMentionTrigger()
|
||||
saveSelection()
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!editorRef.value.contains(range.commonAncestorContainer)) return
|
||||
|
||||
const token = '[?]'
|
||||
const holder = document.createElement('span')
|
||||
holder.className = 'vg-inline-ref'
|
||||
holder.setAttribute('data-mention-reference', '1')
|
||||
holder.setAttribute('data-token', token)
|
||||
holder.setAttribute('data-reference-url', item.url)
|
||||
holder.setAttribute('data-token', '[?]')
|
||||
holder.setAttribute('data-reference-url', String(item.url || '').trim())
|
||||
holder.setAttribute('data-reference-asset-id', item.assetId || '')
|
||||
holder.setAttribute('data-reference-kind', kind)
|
||||
holder.setAttribute('contenteditable', 'false')
|
||||
|
|
@ -1214,6 +1276,157 @@ const selectMentionItem = (item) => {
|
|||
img.className = 'vg-inline-ref-image'
|
||||
holder.appendChild(img)
|
||||
}
|
||||
return holder
|
||||
}
|
||||
|
||||
const extractRefsFromContent = (content) => {
|
||||
const images = []
|
||||
const videos = []
|
||||
const audios = []
|
||||
if (!Array.isArray(content)) return { images, videos, audios }
|
||||
for (const item of content) {
|
||||
if (item?.type === 'image_url' && (!item.role || item.role === 'reference_image')) {
|
||||
const url = item?.image_url?.url
|
||||
if (url) images.push(url)
|
||||
}
|
||||
if (item?.type === 'video_url' && item?.role === 'reference_video') {
|
||||
const url = item?.video_url?.url
|
||||
if (url) videos.push(url)
|
||||
}
|
||||
if (item?.type === 'audio_url' && item?.role === 'reference_audio') {
|
||||
const url = item?.audio_url?.url
|
||||
if (url) audios.push(url)
|
||||
}
|
||||
}
|
||||
return { images, videos, audios }
|
||||
}
|
||||
|
||||
const parseContentItemToMedia = (it, idx) => {
|
||||
const id = `hist_${idx}_${Date.now().toString(36)}`
|
||||
if (it?.type === 'image_url' && (!it.role || it.role === 'reference_image')) {
|
||||
const raw = String(it.image_url?.url || '').trim()
|
||||
if (!raw) return null
|
||||
const assetId = raw.startsWith('asset://') ? raw.slice(9) : ''
|
||||
return { id, url: raw, assetId, mediaType: 'image', name: '' }
|
||||
}
|
||||
if (it?.type === 'video_url' && it.role === 'reference_video') {
|
||||
const raw = String(it.video_url?.url || '').trim()
|
||||
if (!raw) return null
|
||||
const assetId = raw.startsWith('asset://') ? raw.slice(9) : ''
|
||||
return { id, url: raw, assetId, mediaType: 'video', name: '' }
|
||||
}
|
||||
if (it?.type === 'audio_url' && it.role === 'reference_audio') {
|
||||
const raw = String(it.audio_url?.url || '').trim()
|
||||
if (!raw) return null
|
||||
const assetId = raw.startsWith('asset://') ? raw.slice(9) : ''
|
||||
return { id, url: raw, assetId, mediaType: 'audio', name: '' }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const normRefKey = (u) => String(u || '').trim()
|
||||
|
||||
const findMediaItemForRefUrl = (items, url, kind) => {
|
||||
const u = normRefKey(url)
|
||||
if (!u) return null
|
||||
return (
|
||||
items.find((x) => {
|
||||
if ((x.mediaType || 'image') !== kind) return false
|
||||
const xu = normRefKey(x.url)
|
||||
const xa = normRefKey(x.assetId)
|
||||
if (u === xu) return true
|
||||
if (u.startsWith('asset://') && u.slice(9) === xa) return true
|
||||
if (xa && `asset://${xa}` === u) return true
|
||||
return false
|
||||
}) || null
|
||||
)
|
||||
}
|
||||
|
||||
const loadReferenceFromTaskRow = async (row) => {
|
||||
if (!isReference.value || !editorRef.value) return
|
||||
let vp = row?.videoParams
|
||||
try {
|
||||
vp = typeof vp === 'string' ? JSON.parse(vp) : vp
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const content = vp?.content
|
||||
if (!Array.isArray(content) || !content.length) return
|
||||
const head = content[0]
|
||||
if (head?.type !== 'text') return
|
||||
|
||||
const mediaItems = []
|
||||
for (let i = 1; i < content.length; i++) {
|
||||
const parsed = parseContentItemToMedia(content[i], i)
|
||||
if (parsed) mediaItems.push(parsed)
|
||||
}
|
||||
setMediaList(mediaItems.slice(0, props.maxMediaCount))
|
||||
await nextTick()
|
||||
|
||||
const refs = extractRefsFromContent(content)
|
||||
const text = head.text || ''
|
||||
editorRef.value.innerHTML = ''
|
||||
const tokenReg = /(\[图片(\d+)\]|\[视频(\d+)\]|\[音频(\d+)\]|\[图(\d+)\])/g
|
||||
let last = 0
|
||||
let m
|
||||
while ((m = tokenReg.exec(text)) !== null) {
|
||||
if (m.index > last) {
|
||||
editorRef.value.appendChild(document.createTextNode(text.slice(last, m.index)))
|
||||
}
|
||||
let url = ''
|
||||
let kind = 'image'
|
||||
if (m[2] != null) {
|
||||
url = refs.images[Number(m[2]) - 1] || ''
|
||||
kind = 'image'
|
||||
} else if (m[3] != null) {
|
||||
url = refs.videos[Number(m[3]) - 1] || ''
|
||||
kind = 'video'
|
||||
} else if (m[4] != null) {
|
||||
url = refs.audios[Number(m[4]) - 1] || ''
|
||||
kind = 'audio'
|
||||
} else if (m[5] != null) {
|
||||
url = refs.images[Number(m[5]) - 1] || ''
|
||||
kind = 'image'
|
||||
}
|
||||
const mediaItem = findMediaItemForRefUrl(mediaItems, url, kind)
|
||||
if (mediaItem) {
|
||||
const holder = buildReferenceHolderElement(mediaItem)
|
||||
editorRef.value.appendChild(holder)
|
||||
} else {
|
||||
editorRef.value.appendChild(document.createTextNode(m[0]))
|
||||
}
|
||||
last = m.index + m[0].length
|
||||
}
|
||||
if (last < text.length) {
|
||||
editorRef.value.appendChild(document.createTextNode(text.slice(last)))
|
||||
}
|
||||
renumberAllReferenceMentions()
|
||||
setPrompt(getEditorPlainText())
|
||||
saveReferenceToStorage(internalMediaList.value)
|
||||
}
|
||||
|
||||
const selectMentionItem = (item) => {
|
||||
if (!item?.url || !editorRef.value) return
|
||||
const kind = item.mediaType || 'image'
|
||||
const key = `${kind}:${(item.assetId || '').trim() || String(item.url || '').trim()}`
|
||||
const keys = getUniqueRefKeysInDocForKind(kind)
|
||||
if (!keys.has(key) && keys.size >= MAX_REFERENCE_UNIQUE_KIND) {
|
||||
const label = kind === 'video' ? '视频' : kind === 'audio' ? '音频' : '图片'
|
||||
Message.warning(`同类参考${label}最多 ${MAX_REFERENCE_UNIQUE_KIND} 种,无法再插入新素材`)
|
||||
mentionVisible.value = false
|
||||
return
|
||||
}
|
||||
editorRef.value.focus()
|
||||
restoreSelection()
|
||||
removeMentionTrigger()
|
||||
saveSelection()
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!editorRef.value.contains(range.commonAncestorContainer)) return
|
||||
|
||||
const holder = buildReferenceHolderElement({ ...item, mediaType: kind })
|
||||
|
||||
range.insertNode(holder)
|
||||
range.setStartAfter(holder)
|
||||
|
|
@ -1232,6 +1445,7 @@ defineExpose({
|
|||
getEditorPlainText,
|
||||
getImageReferenceContentItems,
|
||||
clearAll,
|
||||
loadReferenceFromTaskRow,
|
||||
clearPromptOnly: () => {
|
||||
setPrompt('')
|
||||
if (editorRef.value) editorRef.value.innerHTML = ''
|
||||
|
|
@ -1267,6 +1481,8 @@ defineExpose({
|
|||
/* 参考图:参考图 | 资产(组+操作) | 选择区域(生成参数) | 富文本 */
|
||||
.vg-compose-card--reference {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
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;
|
||||
|
|
@ -1346,10 +1562,25 @@ defineExpose({
|
|||
color: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
/* 参考图列:限制在网格单元高度内,多图时在列表内滚动 */
|
||||
.vg-compose-mod--ref {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-mod--ref .vg-mod-body {
|
||||
min-height: 72px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vg-compose-mod--ref.vg-compose-mod--dragover {
|
||||
outline: 2px dashed rgba(0, 202, 224, 0.65);
|
||||
outline-offset: 2px;
|
||||
border-radius: 12px;
|
||||
background: rgba(0, 202, 224, 0.06);
|
||||
}
|
||||
|
||||
.vg-compose-empty--compact {
|
||||
|
|
@ -1377,14 +1608,19 @@ defineExpose({
|
|||
}
|
||||
|
||||
.vg-compose-media-scroll--vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
padding: 2px;
|
||||
padding: 2px 4px 2px 2px;
|
||||
scroll-snap-type: y proximity;
|
||||
touch-action: pan-y;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.vg-compose-media-item--tile {
|
||||
|
|
@ -1400,6 +1636,28 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 首帧/首尾帧:左侧(两列布局)增加参数后,让右侧富文本框宽度略收窄 */
|
||||
.vg-compose-card--split .vg-compose-left--two-col {
|
||||
width: 40%;
|
||||
overflow-y: hidden; /* 让滚动优先发生在参数模块内部 */
|
||||
}
|
||||
|
||||
.vg-compose-left--two-col {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
|
||||
gap: 6px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.vg-compose-left--two-col > * {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-compose-left.hidden {
|
||||
|
|
@ -1419,6 +1677,60 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.vg-first-frame-panel,
|
||||
.vg-first-last-panel {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
/* 首帧/首尾帧:上传区不参与 flex 收缩,避免参数模块挤压导致上传区裁剪 */
|
||||
.vg-compose-card--split .vg-first-frame-panel,
|
||||
.vg-compose-card--split .vg-first-last-panel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 首帧/首尾帧:参数模块允许在内部滚动 */
|
||||
.vg-compose-card--split .vg-compose-mod--pick {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 首帧/首尾帧:缩小左侧上传区,避免高度不足导致滚动 */
|
||||
.vg-first-frame-panel,
|
||||
.vg-first-last-panel {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vg-first-frame-panel .vg-upload-placeholder,
|
||||
.vg-first-last-panel .vg-upload-placeholder {
|
||||
padding: 16px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.vg-first-frame-panel .vg-upload-icon,
|
||||
.vg-first-last-panel .vg-upload-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vg-first-frame-panel .vg-upload-text,
|
||||
.vg-first-last-panel .vg-upload-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.vg-first-frame-panel .vg-compose-media-item,
|
||||
.vg-first-last-panel .vg-compose-media-item {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.vg-upload-single {
|
||||
|
|
@ -1711,6 +2023,7 @@ defineExpose({
|
|||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vg-reference-editor-row {
|
||||
|
|
@ -1991,6 +2304,12 @@ defineExpose({
|
|||
.vg-compose-left {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vg-compose-left--two-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等宽度:四列改为「参考条 + 右侧两行」,富文本独占一行(平板横屏等) */
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ const state = {
|
|||
showMessage: false,
|
||||
messageData: {},
|
||||
messageCount: 0,
|
||||
// 是否显示18禁弹窗
|
||||
showForbidden: true,
|
||||
// 是否显示18禁弹窗:默认不拦截,直接进入首页等主页面
|
||||
showForbidden: false,
|
||||
// 阻止页面切换
|
||||
showPrevent: false,
|
||||
showLogin: false
|
||||
|
|
|
|||
|
|
@ -11,27 +11,27 @@
|
|||
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
||||
</div>
|
||||
<div class="ag-field">
|
||||
<label>GroupIds</label>
|
||||
<a-input v-model="filters.groupIdsText" placeholder="多个ID用英文逗号分隔" />
|
||||
<label>资源组编号</label>
|
||||
<a-input v-model="filters.groupIdsText" placeholder="多个编号用英文逗号分隔" />
|
||||
</div>
|
||||
<div class="ag-field">
|
||||
<label>GroupType</label>
|
||||
<label>资源组类型</label>
|
||||
<a-select v-model="filters.groupType">
|
||||
<a-option value="AIGC">AIGC</a-option>
|
||||
<a-option value="AIGC">AIGC(生成类)</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="ag-field">
|
||||
<label>SortBy</label>
|
||||
<label>排序字段</label>
|
||||
<a-select v-model="filters.sortBy">
|
||||
<a-option value="CreateTime">CreateTime</a-option>
|
||||
<a-option value="UpdateTime">UpdateTime</a-option>
|
||||
<a-option value="CreateTime">创建时间</a-option>
|
||||
<a-option value="UpdateTime">更新时间</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="ag-field">
|
||||
<label>SortOrder</label>
|
||||
<label>排序方向</label>
|
||||
<a-select v-model="filters.sortOrder">
|
||||
<a-option value="Desc">Desc</a-option>
|
||||
<a-option value="Asc">Asc</a-option>
|
||||
<a-option value="Desc">从新到旧</a-option>
|
||||
<a-option value="Asc">从旧到新</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="ag-actions">
|
||||
|
|
@ -56,13 +56,13 @@
|
|||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>GroupType</th>
|
||||
<th>ProjectName</th>
|
||||
<th>CreateTime</th>
|
||||
<th>UpdateTime</th>
|
||||
<th>编号</th>
|
||||
<th>名称</th>
|
||||
<th>描述</th>
|
||||
<th>类型</th>
|
||||
<th>项目名称</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<td>{{ item.Id || item.id }}</td>
|
||||
<td>{{ item.Name || item.name }}</td>
|
||||
<td>{{ item.Description || item.description || '-' }}</td>
|
||||
<td>{{ item.GroupType || item.groupType || '-' }}</td>
|
||||
<td>{{ formatGroupTypeLabel(item) }}</td>
|
||||
<td>{{ item.ProjectName || item.projectName || '-' }}</td>
|
||||
<td>{{ item.CreateTime || item.createTime || '-' }}</td>
|
||||
<td>{{ item.UpdateTime || item.updateTime || '-' }}</td>
|
||||
|
|
@ -181,6 +181,12 @@ export default {
|
|||
this.search(1)
|
||||
},
|
||||
methods: {
|
||||
formatGroupTypeLabel(item) {
|
||||
const t = String(item?.GroupType || item?.groupType || '').trim()
|
||||
if (!t) return '-'
|
||||
if (t === 'AIGC') return 'AIGC(生成类)'
|
||||
return t
|
||||
},
|
||||
clampGroupPage(n) {
|
||||
const page = Number(n) || 1
|
||||
const size = Number(this.filters.pageSize) || 10
|
||||
|
|
@ -294,7 +300,7 @@ export default {
|
|||
const res = await this.$axios({
|
||||
url: 'api/byteAssetGroup/getAssetGroup',
|
||||
method: 'POST',
|
||||
data: { Id: id }
|
||||
data: { id: id }
|
||||
})
|
||||
if (res.code === 200) {
|
||||
this.detailData = res.data || {}
|
||||
|
|
|
|||
|
|
@ -44,31 +44,31 @@
|
|||
</a-select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Name</label>
|
||||
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
||||
<label>素材名称</label>
|
||||
<a-input v-model="filters.name" placeholder="按名称筛选" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Status</label>
|
||||
<a-select v-model="filters.status">
|
||||
<label>状态</label>
|
||||
<a-select v-model="filters.status" placeholder="全部状态">
|
||||
<a-option value="">全部</a-option>
|
||||
<a-option value="Active">Active</a-option>
|
||||
<a-option value="Processing">Processing</a-option>
|
||||
<a-option value="Failed">Failed</a-option>
|
||||
<a-option value="Active">可用</a-option>
|
||||
<a-option value="Processing">处理中</a-option>
|
||||
<a-option value="Failed">失败</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>SortBy</label>
|
||||
<label>排序字段</label>
|
||||
<a-select v-model="filters.sortBy">
|
||||
<a-option value="CreateTime">CreateTime</a-option>
|
||||
<a-option value="UpdateTime">UpdateTime</a-option>
|
||||
<a-option value="GroupId">GroupId</a-option>
|
||||
<a-option value="CreateTime">创建时间</a-option>
|
||||
<a-option value="UpdateTime">更新时间</a-option>
|
||||
<a-option value="GroupId">素材组</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>SortOrder</label>
|
||||
<label>排序方向</label>
|
||||
<a-select v-model="filters.sortOrder">
|
||||
<a-option value="Desc">Desc</a-option>
|
||||
<a-option value="Asc">Asc</a-option>
|
||||
<a-option value="Desc">从新到旧</a-option>
|
||||
<a-option value="Asc">从旧到新</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="field actions">
|
||||
|
|
@ -83,13 +83,13 @@
|
|||
<table class="asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>URL</th>
|
||||
<th>GroupId</th>
|
||||
<th>AssetType</th>
|
||||
<th>Status</th>
|
||||
<th>CreateTime</th>
|
||||
<th>素材编号</th>
|
||||
<th>名称</th>
|
||||
<th>访问地址</th>
|
||||
<th>所属素材组</th>
|
||||
<th>类型</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -135,8 +135,8 @@
|
|||
</div>
|
||||
</td>
|
||||
<td>{{ it.GroupId || it.groupId || '-' }}</td>
|
||||
<td>{{ it.AssetType || it.assetType || '-' }}</td>
|
||||
<td>{{ it.Status || it.status || '-' }}</td>
|
||||
<td>{{ formatAssetTypeLabel(it) }}</td>
|
||||
<td>{{ formatAssetStatusLabel(it) }}</td>
|
||||
<td>{{ it.CreateTime || it.createTime || '-' }}</td>
|
||||
<td>
|
||||
<a-button size="mini" type="outline" @click="getAsset(it)">详情</a-button>
|
||||
|
|
@ -265,6 +265,26 @@ export default {
|
|||
this.loadGroups()
|
||||
},
|
||||
methods: {
|
||||
formatAssetTypeLabel(it) {
|
||||
const t = String(it?.AssetType || it?.assetType || '').trim()
|
||||
if (!t) return '-'
|
||||
const map = {
|
||||
Image: '图片',
|
||||
Video: '视频',
|
||||
Audio: '音频'
|
||||
}
|
||||
return map[t] || t
|
||||
},
|
||||
formatAssetStatusLabel(it) {
|
||||
const s = String(it?.Status || it?.status || '').trim()
|
||||
if (!s) return '-'
|
||||
const map = {
|
||||
Active: '可用',
|
||||
Processing: '处理中',
|
||||
Failed: '失败'
|
||||
}
|
||||
return map[s] || s
|
||||
},
|
||||
assetUrl(it) {
|
||||
return String(it?.URL || it?.url || '').trim()
|
||||
},
|
||||
|
|
@ -356,7 +376,7 @@ export default {
|
|||
},
|
||||
async createAsset() {
|
||||
const groupId = String(this.createForm.groupId || '').trim()
|
||||
if (!groupId) return this.$message.error('请填写 GroupId')
|
||||
if (!groupId) return this.$message.error('请选择素材组')
|
||||
if (!this.createForm.file) return this.$message.error('请选择上传文件')
|
||||
this.createLoading = true
|
||||
try {
|
||||
|
|
@ -476,7 +496,7 @@ export default {
|
|||
const res = await this.$axios({
|
||||
url: ASSET_DELETE_API,
|
||||
method: 'POST',
|
||||
data: { Id: id }
|
||||
data: { id: id }
|
||||
})
|
||||
if (res.code === 200) {
|
||||
this.$message.success('删除成功')
|
||||
|
|
|
|||
|
|
@ -46,6 +46,29 @@
|
|||
preload="metadata" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="vg-chat-params" v-if="rowChatParamsVisible(row)">
|
||||
<span class="vg-chat-param-chip">{{ rowChatParams(row).modeLabel }}</span>
|
||||
<span class="vg-chat-param-chip">模型 {{ rowChatParams(row).modelLabel }}</span>
|
||||
<span class="vg-chat-param-chip">比例 {{ rowChatParams(row).ratio }}</span>
|
||||
<span class="vg-chat-param-chip">时长 {{ rowChatParams(row).duration }}</span>
|
||||
<span class="vg-chat-param-chip">分辨率 {{ rowChatParams(row).resolution }}</span>
|
||||
</div>
|
||||
<div class="vg-chat-task-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link vg-chat-action-btn"
|
||||
:disabled="generateLoading"
|
||||
@click="editFromTaskRow(row)">
|
||||
重新编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link vg-chat-action-btn"
|
||||
:disabled="generateLoading"
|
||||
@click="regenerateFromTaskRow(row)">
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||
</div>
|
||||
<div v-else class="vg-chat-user-row">
|
||||
|
|
@ -73,6 +96,29 @@
|
|||
preload="metadata" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="vg-chat-params" v-if="rowChatParamsVisible(row)">
|
||||
<span class="vg-chat-param-chip">{{ rowChatParams(row).modeLabel }}</span>
|
||||
<span class="vg-chat-param-chip">模型 {{ rowChatParams(row).modelLabel }}</span>
|
||||
<span class="vg-chat-param-chip">比例 {{ rowChatParams(row).ratio }}</span>
|
||||
<span class="vg-chat-param-chip">时长 {{ rowChatParams(row).duration }}</span>
|
||||
<span class="vg-chat-param-chip">分辨率 {{ rowChatParams(row).resolution }}</span>
|
||||
</div>
|
||||
<div class="vg-chat-task-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link vg-chat-action-btn"
|
||||
:disabled="generateLoading"
|
||||
@click="editFromTaskRow(row)">
|
||||
重新编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link vg-chat-action-btn"
|
||||
:disabled="generateLoading"
|
||||
@click="regenerateFromTaskRow(row)">
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
<div class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||
</div>
|
||||
<div class="vg-chat-user-col-status">
|
||||
|
|
@ -98,6 +144,13 @@
|
|||
<div class="vg-chat-ai-status">
|
||||
{{ taskStatusText(row) }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-chat-download-btn"
|
||||
@click="downloadChatVideo(row)"
|
||||
title="下载到本地">
|
||||
下载视频
|
||||
</button>
|
||||
<div class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
|
||||
</div>
|
||||
<div class="vg-chat-result">
|
||||
|
|
@ -186,62 +239,11 @@
|
|||
</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 vg-toolbar--reference-submit">
|
||||
<div
|
||||
class="vg-toolbar-actions"
|
||||
:class="{ 'vg-toolbar-actions--reference': videoMode === 'image-reference' }">
|
||||
:class="{ 'vg-toolbar-actions--reference': true }">
|
||||
<mf-button
|
||||
v-if="videoMode === 'image-reference'"
|
||||
size="small"
|
||||
type="text"
|
||||
class="vg-toolbar-clear"
|
||||
|
|
@ -749,6 +751,182 @@ export default {
|
|||
await this.refreshChatFirstPage()
|
||||
},
|
||||
|
||||
parseVideoParams(row) {
|
||||
if (!row?.videoParams) return null
|
||||
try {
|
||||
return typeof row.videoParams === 'string' ? JSON.parse(row.videoParams) : row.videoParams
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
videoModeLabel(mode) {
|
||||
const m = String(mode || '').trim()
|
||||
if (!m) return '—'
|
||||
const hit = this.videoModeOptions.find((o) => o.value === m)
|
||||
return hit ? hit.label : m
|
||||
},
|
||||
|
||||
modelLabelForValue(val) {
|
||||
if (!val) return ''
|
||||
const hit = this.modelOptions.find((o) => o.value === val)
|
||||
return hit ? hit.label : ''
|
||||
},
|
||||
|
||||
rowChatParams(row) {
|
||||
const vp = this.parseVideoParams(row)
|
||||
const modelVal = row?.model ?? vp?.model ?? ''
|
||||
const ratio = row?.ratio ?? vp?.ratio ?? ''
|
||||
const durRaw = row?.duration != null ? row.duration : vp?.duration
|
||||
const resolution = row?.resolution ?? vp?.resolution ?? ''
|
||||
const mode = row?.mode || vp?.generationMode || ''
|
||||
return {
|
||||
modeLabel: this.videoModeLabel(mode),
|
||||
modelLabel: this.modelLabelForValue(modelVal) || modelVal || '—',
|
||||
ratio: ratio || '—',
|
||||
duration: durRaw != null && durRaw !== '' ? `${durRaw} 秒` : '—',
|
||||
resolution: resolution || '—'
|
||||
}
|
||||
},
|
||||
|
||||
rowChatParamsVisible(row) {
|
||||
return !!(
|
||||
row &&
|
||||
(row.mode ||
|
||||
row.model ||
|
||||
row.ratio ||
|
||||
row.duration != null ||
|
||||
row.resolution ||
|
||||
row.videoParams)
|
||||
)
|
||||
},
|
||||
|
||||
getRowPromptTextForRestore(row) {
|
||||
if (row?.text) return row.text
|
||||
const vp = this.parseVideoParams(row)
|
||||
const c = vp?.content?.[0]
|
||||
if (c?.type === 'text' && c.text) return c.text
|
||||
return ''
|
||||
},
|
||||
|
||||
buildMediaListForTextToVideo(row) {
|
||||
const out = []
|
||||
try {
|
||||
const vp = this.parseVideoParams(row)
|
||||
const content = vp?.content
|
||||
if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (item?.type === 'image_url' && item.image_url?.url) {
|
||||
out.push({
|
||||
id: `hist_${out.length}_${Date.now()}`,
|
||||
url: item.image_url.url,
|
||||
mediaType: 'image',
|
||||
name: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!out.length) {
|
||||
return this.getRowAttachmentList(row).map((x, i) => ({
|
||||
id: `att_${row.id || ''}_${i}`,
|
||||
url: x.url,
|
||||
mediaType: x.mediaType || 'image',
|
||||
name: ''
|
||||
}))
|
||||
}
|
||||
return out
|
||||
},
|
||||
|
||||
async applyTaskRowToForm(row) {
|
||||
if (!row) return
|
||||
const mode = row.mode || 'text-to-video'
|
||||
if (row.model && this.modelOptions.some((o) => o.value === row.model)) {
|
||||
this.selectedModel = row.model
|
||||
}
|
||||
if (row.ratio && this.ratioOptions.includes(row.ratio)) {
|
||||
this.selectedRatio = row.ratio
|
||||
}
|
||||
const dur = row.duration != null ? Number(row.duration) : null
|
||||
if (dur != null && this.durationOptions.includes(dur)) {
|
||||
this.selectedDuration = dur
|
||||
}
|
||||
if (row.resolution && this.resolutionOptions.includes(row.resolution)) {
|
||||
this.selectedResolution = row.resolution
|
||||
}
|
||||
if (mode === 'image-reference') {
|
||||
try {
|
||||
localStorage.removeItem('video_reference_media')
|
||||
} catch (_) {}
|
||||
}
|
||||
this.videoMode = mode
|
||||
const prompt = this.getRowPromptTextForRestore(row)
|
||||
if (mode !== 'image-reference') {
|
||||
this.promptText = prompt
|
||||
} else {
|
||||
this.promptText = ''
|
||||
}
|
||||
await this.$nextTick()
|
||||
if (mode === 'text-to-video') {
|
||||
this.mediaList = this.buildMediaListForTextToVideo(row)
|
||||
} else if (mode === 'image-first-frame') {
|
||||
this.mediaList = row.img1
|
||||
? [{ id: 'r1', url: row.img1, mediaType: 'image', name: '' }]
|
||||
: []
|
||||
} else if (mode === 'image-first-last-frame') {
|
||||
const list = []
|
||||
if (row.img1) list.push({ id: 'r1', url: row.img1, mediaType: 'image', name: '' })
|
||||
if (row.img2) list.push({ id: 'r2', url: row.img2, mediaType: 'image', name: '' })
|
||||
this.mediaList = list
|
||||
} else if (mode === 'image-reference') {
|
||||
this.mediaList = []
|
||||
await this.$nextTick()
|
||||
await this.$refs.videoComposeRef?.loadReferenceFromTaskRow?.(row)
|
||||
}
|
||||
},
|
||||
|
||||
async editFromTaskRow(row) {
|
||||
try {
|
||||
await this.applyTaskRowToForm(row)
|
||||
this.$message.success('已载入该条任务到编辑区,可修改后再次生成')
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.videoComposeRef?.$el
|
||||
if (el && typeof el.scrollIntoView === 'function') {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}
|
||||
})
|
||||
} catch (_) {
|
||||
this.$message.error('载入失败,请重试')
|
||||
}
|
||||
},
|
||||
|
||||
regenerateFromTaskRow(row) {
|
||||
this.$confirm({
|
||||
title: '重新生成',
|
||||
content: '将按该条记录的提示词与参数再次提交任务,是否继续?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
await this.applyTaskRowToForm(row)
|
||||
await this.$nextTick()
|
||||
await this.generateVideo()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
downloadChatVideo(row) {
|
||||
const url = row?.result
|
||||
if (!url || !this.isHttpOrHttpsUrl(url)) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `video_${row.id || Date.now()}.mp4`
|
||||
a.target = '_blank'
|
||||
a.rel = 'noreferrer'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
},
|
||||
|
||||
async loadPriceInfo() {
|
||||
this.$axios({
|
||||
url: 'api/manager/selectInfo',
|
||||
|
|
@ -1422,6 +1600,8 @@ export default {
|
|||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
/* 稍微下移:让按钮对齐参数区视觉中线 */
|
||||
margin-top: 6px;
|
||||
background: linear-gradient(145deg, #00d4e8, #0090a8);
|
||||
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.35), 0 8px 24px rgba(0, 150, 180, 0.35);
|
||||
color: #fff;
|
||||
|
|
@ -1759,6 +1939,42 @@ export default {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.vg-chat-params {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.vg-chat-param-chip {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.vg-chat-task-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vg-chat-action-btn {
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vg-chat-action-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.vg-chat-ai-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1768,6 +1984,22 @@ export default {
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vg-chat-download-btn {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 202, 224, 0.45);
|
||||
background: rgba(0, 202, 224, 0.12);
|
||||
color: var(--vg-cyan);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vg-chat-download-btn:hover {
|
||||
background: rgba(0, 202, 224, 0.2);
|
||||
}
|
||||
|
||||
.vg-chat-ai-status {
|
||||
font-size: 13px;
|
||||
color: var(--vg-cyan);
|
||||
|
|
|
|||
Loading…
Reference in New Issue