Merge branch 'seedance' of https://gitea.06zk.com/best_yunwei/ai_images into seedance
This commit is contained in:
commit
9c0f533aff
|
|
@ -51,12 +51,12 @@ export default {
|
||||||
this.$router.replace('/403')
|
this.$router.replace('/403')
|
||||||
},
|
},
|
||||||
ok() {
|
ok() {
|
||||||
// 满18+:关闭弹窗并直接进入视频生成
|
// 满18+:关闭弹窗并进入首页
|
||||||
Promise.resolve(this.$store.dispatch('main/setForbidden', false)).finally(() => {
|
Promise.resolve(this.$store.dispatch('main/setForbidden', false)).finally(() => {
|
||||||
// 默认语言:繁体中文(zh_HK)
|
// 默认语言:繁体中文(zh_HK)
|
||||||
this.$store.dispatch('main/setLanguage', 'zh_HK')
|
this.$store.dispatch('main/setLanguage', 'zh_HK')
|
||||||
i18n.global.locale = '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"
|
||||||
:class="{
|
:class="{
|
||||||
'vg-compose-card--reference': isReference,
|
'vg-compose-card--reference': isReference,
|
||||||
'vg-compose-card--split': !isReference && !isTextToVideo,
|
'vg-compose-card--split': !isReference
|
||||||
'vg-compose-card--solo': isTextToVideo
|
|
||||||
}">
|
}">
|
||||||
<!-- 参考图模式:四列 — 参考图 / 资产 / 选择区域 / 富文本 -->
|
<!-- 参考图模式:四列 — 参考图 / 资产 / 选择区域 / 富文本 -->
|
||||||
<template v-if="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 class="vg-mod-body">
|
||||||
<div
|
<div
|
||||||
v-if="mediaList.length === 0"
|
v-if="mediaList.length === 0"
|
||||||
class="vg-compose-empty vg-compose-empty--compact"
|
class="vg-compose-empty vg-compose-empty--compact"
|
||||||
@click="onReferenceEmptyAreaClick">
|
@click="onReferenceEmptyAreaClick">
|
||||||
<div class="vg-compose-empty-icon" aria-hidden="true">+</div>
|
<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>
|
||||||
<div v-else class="vg-compose-media-scroll vg-compose-media-scroll--vertical">
|
<div v-else class="vg-compose-media-scroll vg-compose-media-scroll--vertical">
|
||||||
<div
|
<div
|
||||||
|
|
@ -78,7 +86,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!hasAssetGroupId"
|
:disabled="!hasAssetGroupId"
|
||||||
@click="openFilePicker">
|
@click="openFilePickerForAssetUpload">
|
||||||
上传资产
|
上传资产
|
||||||
</mf-button>
|
</mf-button>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -92,8 +100,16 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 v-if="isFirstFrame" class="vg-first-frame-panel">
|
||||||
<div class="vg-panel-title">首帧图</div>
|
<div class="vg-panel-title">首帧图</div>
|
||||||
|
|
@ -141,17 +157,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="vg-compose-right" :class="{ 'vg-compose-right--reference': isReference }">
|
<div
|
||||||
<div v-if="!isReference" class="vg-compose-right-head">
|
class="vg-compose-right"
|
||||||
<div class="vg-compose-right-title">描述画面与动态</div>
|
:class="{ 'vg-compose-right--reference': isReference || isTextToVideo || isFirstFrame || isFirstLastFrame }">
|
||||||
<mf-button size="small" type="text" class="vg-compose-clear" @click="clearAll">
|
<div class="vg-compose-right-body vg-reference-editor-row">
|
||||||
清空
|
|
||||||
</mf-button>
|
|
||||||
</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"
|
||||||
|
|
@ -182,12 +200,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isReference" class="vg-reference-actions-col">
|
<div 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -300,6 +315,10 @@ const assetGroups = ref([])
|
||||||
const assetPickerVisible = ref(false)
|
const assetPickerVisible = ref(false)
|
||||||
const assetQueryResults = ref([])
|
const assetQueryResults = ref([])
|
||||||
const assetPickerSelectedKeys = ref([])
|
const assetPickerSelectedKeys = ref([])
|
||||||
|
/** 参考图区拖拽高亮(嵌套 dragenter/leave 计数) */
|
||||||
|
const referenceDragDepth = ref(0)
|
||||||
|
/** 仅「上传资产」按钮为 true;左侧直接上传/拖拽/粘贴为 false */
|
||||||
|
const createAssetIntent = ref(false)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
|
|
@ -382,18 +401,34 @@ const buildReferenceListAssetsPayload = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReferenceEmptyAreaClick = () => {
|
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) {
|
if (!hasAssetGroupId.value) {
|
||||||
Message.warning('请先选择素材组')
|
Message.warning('请先选择素材组')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
openFilePicker()
|
createAssetIntent.value = true
|
||||||
|
currentUploadIndex.value = -1
|
||||||
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFilePicker = () => {
|
const openFilePicker = () => {
|
||||||
if (isReference.value && !hasAssetGroupId.value) {
|
if (isReference.value) {
|
||||||
Message.warning('请先选择素材组')
|
openReferenceDirectUpload()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
createAssetIntent.value = false
|
||||||
currentUploadIndex.value = -1
|
currentUploadIndex.value = -1
|
||||||
fileInputRef.value?.click()
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
@ -601,135 +636,182 @@ const confirmPickAssets = () => {
|
||||||
assetPickerSelectedKeys.value = []
|
assetPickerSelectedKeys.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectFiles = async (event) => {
|
const processFilesList = async (files, options = {}) => {
|
||||||
const input = event.target
|
if (!files || !files.length) return
|
||||||
const files = Array.from(input.files || [])
|
|
||||||
if (!files.length) return
|
|
||||||
|
|
||||||
const isReferenceMode = isReference.value
|
const isReferenceMode = isReference.value
|
||||||
if (isReferenceMode && !String(assetGroupId.value || '').trim()) {
|
const wantCreateAsset = isReferenceMode && options.createAsset === true
|
||||||
Message.warning('请先选择素材组')
|
if (wantCreateAsset && !String(assetGroupId.value || '').trim()) {
|
||||||
input.value = ''
|
Message.warning('请先选择素材组')
|
||||||
return
|
|
||||||
}
|
|
||||||
let targetList = [...mediaList.value]
|
|
||||||
|
|
||||||
// 首尾帧模式特殊处理索引
|
|
||||||
if (isFirstLastFrame.value && currentUploadIndex.value >= 0) {
|
|
||||||
const idx = currentUploadIndex.value
|
|
||||||
if (files.length > 0) {
|
|
||||||
const file = files[0]
|
|
||||||
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 ? '[首帧]' : '[尾帧]' }
|
|
||||||
targetList[idx] = entry
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 其他模式正常处理
|
|
||||||
const remain = props.maxMediaCount - mediaList.value.length
|
|
||||||
if (remain <= 0 && !isReferenceMode) {
|
|
||||||
Message.warning(`最多添加 ${props.maxMediaCount} 个参考素材`)
|
|
||||||
input.value = ''
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let targetList = [...mediaList.value]
|
||||||
|
|
||||||
const selected = files.slice(0, remain || files.length)
|
if (isFirstLastFrame.value && currentUploadIndex.value >= 0) {
|
||||||
const uploadingEntries = []
|
const idx = currentUploadIndex.value
|
||||||
|
if (files.length > 0) {
|
||||||
for (const file of selected) {
|
const file = files[0]
|
||||||
const mediaType = detectMediaType(file)
|
const mediaType = detectMediaType(file)
|
||||||
if (!props.allowedMediaTypes.includes(mediaType)) {
|
if (mediaType !== 'image') {
|
||||||
Message.warning(`当前模式不支持该类型:${mediaType}`)
|
Message.warning('首尾帧仅支持图片')
|
||||||
continue
|
return
|
||||||
}
|
|
||||||
|
|
||||||
const id = `tmp_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
|
||||||
const localPreview = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
const entry = {
|
|
||||||
id,
|
|
||||||
url: localPreview,
|
|
||||||
mediaType,
|
|
||||||
name: file.name,
|
|
||||||
_fileRef: file,
|
|
||||||
isUploading: true
|
|
||||||
}
|
|
||||||
uploadingEntries.push(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadingEntries.length) {
|
|
||||||
targetList = [...targetList, ...uploadingEntries]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMediaList(targetList)
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// 上传处理
|
|
||||||
const toUpload = targetList.filter(item => item.isUploading)
|
|
||||||
for (const entry of toUpload) {
|
|
||||||
try {
|
|
||||||
const res = await uploadFile({
|
|
||||||
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
|
||||||
file: entry._fileRef,
|
|
||||||
name: 'file'
|
|
||||||
})
|
|
||||||
const url = extractUploadUrlFromResponse(res)
|
|
||||||
if (!url) throw new Error(res?.msg || '未返回文件地址')
|
|
||||||
let assetId = entry.assetId || ''
|
|
||||||
if (isReferenceMode) {
|
|
||||||
const gid = String(assetGroupId.value || '').trim()
|
|
||||||
if (!gid) throw new Error('请先选择素材组')
|
|
||||||
const mt = entry.mediaType || 'image'
|
|
||||||
const assetType =
|
|
||||||
mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image'
|
|
||||||
const fd = new FormData()
|
|
||||||
fd.append('file', entry._fileRef)
|
|
||||||
fd.append('groupId', gid)
|
|
||||||
fd.append('assetType', assetType)
|
|
||||||
fd.append('name', entry?.name || '')
|
|
||||||
const createRes = await request({
|
|
||||||
url: 'api/byteAsset/createAsset',
|
|
||||||
method: 'POST',
|
|
||||||
data: fd
|
|
||||||
})
|
|
||||||
if (createRes.code !== 200) {
|
|
||||||
throw new Error(createRes?.msg || '创建素材失败')
|
|
||||||
}
|
}
|
||||||
assetId = createRes?.data?.Id || createRes?.data?.id || ''
|
const id = `frame_${Date.now()}`
|
||||||
if (!assetId) throw new Error(createRes?.msg || '创建素材失败:未返回资产ID')
|
const localPreview = URL.createObjectURL(file)
|
||||||
|
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} 个参考素材`)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const localPreview = entry.url
|
const selected = files.slice(0, remain || files.length)
|
||||||
setMediaList(
|
const uploadingEntries = []
|
||||||
mediaList.value.map((x) =>
|
|
||||||
normalizeItemKey(x) === normalizeItemKey(entry)
|
for (const file of selected) {
|
||||||
? { ...x, url, assetId, isUploading: false }
|
const mediaType = detectMediaType(file)
|
||||||
: x
|
if (!props.allowedMediaTypes.includes(mediaType)) {
|
||||||
)
|
Message.warning(`当前模式不支持该类型:${mediaType}`)
|
||||||
)
|
continue
|
||||||
if (isReferenceMode) {
|
}
|
||||||
Message.success('已上传完成')
|
|
||||||
|
const id = `tmp_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||||
|
const localPreview = URL.createObjectURL(file)
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
id,
|
||||||
|
url: localPreview,
|
||||||
|
mediaType,
|
||||||
|
name: file.name,
|
||||||
|
_fileRef: file,
|
||||||
|
isUploading: true
|
||||||
|
}
|
||||||
|
uploadingEntries.push(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uploadingEntries.length) {
|
||||||
|
targetList = [...targetList, ...uploadingEntries]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMediaList(targetList)
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const toUpload = targetList.filter((item) => item.isUploading)
|
||||||
|
for (const entry of toUpload) {
|
||||||
try {
|
try {
|
||||||
URL.revokeObjectURL(localPreview)
|
const res = await uploadFile({
|
||||||
} catch (_) {}
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
} catch (err) {
|
file: entry._fileRef,
|
||||||
setMediaList(mediaList.value.filter((x) => normalizeItemKey(x) !== normalizeItemKey(entry)))
|
name: 'file'
|
||||||
Message.error('上传失败,请重试')
|
})
|
||||||
|
const url = extractUploadUrlFromResponse(res)
|
||||||
|
if (!url) throw new Error(res?.msg || '未返回文件地址')
|
||||||
|
let assetId = entry.assetId || ''
|
||||||
|
if (wantCreateAsset) {
|
||||||
|
const gid = String(assetGroupId.value || '').trim()
|
||||||
|
if (!gid) throw new Error('请先选择素材组')
|
||||||
|
const mt = entry.mediaType || 'image'
|
||||||
|
const assetType =
|
||||||
|
mt === 'video' ? 'Video' : mt === 'audio' ? 'Audio' : 'Image'
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', entry._fileRef)
|
||||||
|
fd.append('groupId', gid)
|
||||||
|
fd.append('assetType', assetType)
|
||||||
|
fd.append('name', entry?.name || '')
|
||||||
|
const createRes = await request({
|
||||||
|
url: 'api/byteAsset/createAsset',
|
||||||
|
method: 'POST',
|
||||||
|
data: fd
|
||||||
|
})
|
||||||
|
if (createRes.code !== 200) {
|
||||||
|
throw new Error(createRes?.msg || '创建素材失败')
|
||||||
|
}
|
||||||
|
assetId = createRes?.data?.Id || createRes?.data?.id || ''
|
||||||
|
if (!assetId) throw new Error(createRes?.msg || '创建素材失败:未返回资产ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
const localPreview = entry.url
|
||||||
|
setMediaList(
|
||||||
|
mediaList.value.map((x) =>
|
||||||
|
normalizeItemKey(x) === normalizeItemKey(entry)
|
||||||
|
? { ...x, url, assetId, isUploading: false }
|
||||||
|
: x
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (isReferenceMode) {
|
||||||
|
Message.success(wantCreateAsset ? '已上传并写入素材库' : '已上传完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(localPreview)
|
||||||
|
} catch (_) {}
|
||||||
|
} catch (err) {
|
||||||
|
setMediaList(mediaList.value.filter((x) => normalizeItemKey(x) !== normalizeItemKey(entry)))
|
||||||
|
Message.error('上传失败,请重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.value = ''
|
const handleSelectFiles = async (event) => {
|
||||||
currentUploadIndex.value = -1
|
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) => {
|
const onPromptInput = (e) => {
|
||||||
setPrompt(e.target.value)
|
setPrompt(e.target.value)
|
||||||
|
|
@ -1159,33 +1241,13 @@ const onEditorKeyup = (e) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectMentionItem = (item) => {
|
const buildReferenceHolderElement = (item) => {
|
||||||
if (!item?.url || !editorRef.value) return
|
|
||||||
const kind = item.mediaType || 'image'
|
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')
|
const holder = document.createElement('span')
|
||||||
holder.className = 'vg-inline-ref'
|
holder.className = 'vg-inline-ref'
|
||||||
holder.setAttribute('data-mention-reference', '1')
|
holder.setAttribute('data-mention-reference', '1')
|
||||||
holder.setAttribute('data-token', token)
|
holder.setAttribute('data-token', '[?]')
|
||||||
holder.setAttribute('data-reference-url', item.url)
|
holder.setAttribute('data-reference-url', String(item.url || '').trim())
|
||||||
holder.setAttribute('data-reference-asset-id', item.assetId || '')
|
holder.setAttribute('data-reference-asset-id', item.assetId || '')
|
||||||
holder.setAttribute('data-reference-kind', kind)
|
holder.setAttribute('data-reference-kind', kind)
|
||||||
holder.setAttribute('contenteditable', 'false')
|
holder.setAttribute('contenteditable', 'false')
|
||||||
|
|
@ -1214,6 +1276,157 @@ const selectMentionItem = (item) => {
|
||||||
img.className = 'vg-inline-ref-image'
|
img.className = 'vg-inline-ref-image'
|
||||||
holder.appendChild(img)
|
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.insertNode(holder)
|
||||||
range.setStartAfter(holder)
|
range.setStartAfter(holder)
|
||||||
|
|
@ -1232,6 +1445,7 @@ defineExpose({
|
||||||
getEditorPlainText,
|
getEditorPlainText,
|
||||||
getImageReferenceContentItems,
|
getImageReferenceContentItems,
|
||||||
clearAll,
|
clearAll,
|
||||||
|
loadReferenceFromTaskRow,
|
||||||
clearPromptOnly: () => {
|
clearPromptOnly: () => {
|
||||||
setPrompt('')
|
setPrompt('')
|
||||||
if (editorRef.value) editorRef.value.innerHTML = ''
|
if (editorRef.value) editorRef.value.innerHTML = ''
|
||||||
|
|
@ -1267,6 +1481,8 @@ defineExpose({
|
||||||
/* 参考图:参考图 | 资产(组+操作) | 选择区域(生成参数) | 富文本 */
|
/* 参考图:参考图 | 资产(组+操作) | 选择区域(生成参数) | 富文本 */
|
||||||
.vg-compose-card--reference {
|
.vg-compose-card--reference {
|
||||||
display: grid;
|
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-columns: minmax(72px, 0.85fr) minmax(72px, 0.75fr) minmax(168px, 1.35fr) minmax(120px, 2.95fr);
|
||||||
grid-template-rows: minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr);
|
||||||
column-gap: 10px;
|
column-gap: 10px;
|
||||||
|
|
@ -1346,10 +1562,25 @@ defineExpose({
|
||||||
color: rgba(255, 255, 255, 0.38);
|
color: rgba(255, 255, 255, 0.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 参考图列:限制在网格单元高度内,多图时在列表内滚动 */
|
||||||
|
.vg-compose-mod--ref {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.vg-compose-mod--ref .vg-mod-body {
|
.vg-compose-mod--ref .vg-mod-body {
|
||||||
min-height: 72px;
|
flex: 1 1 0;
|
||||||
overflow-x: hidden;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
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 {
|
.vg-compose-empty--compact {
|
||||||
|
|
@ -1377,14 +1608,19 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-compose-media-scroll--vertical {
|
.vg-compose-media-scroll--vertical {
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 2px;
|
padding: 2px 4px 2px 2px;
|
||||||
scroll-snap-type: y proximity;
|
scroll-snap-type: y proximity;
|
||||||
touch-action: pan-y;
|
touch-action: pan-y;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-compose-media-item--tile {
|
.vg-compose-media-item--tile {
|
||||||
|
|
@ -1400,6 +1636,28 @@ defineExpose({
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
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 {
|
.vg-compose-left.hidden {
|
||||||
|
|
@ -1419,6 +1677,60 @@ defineExpose({
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
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 {
|
.vg-upload-single {
|
||||||
|
|
@ -1711,6 +2023,7 @@ defineExpose({
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vg-reference-editor-row {
|
.vg-reference-editor-row {
|
||||||
|
|
@ -1991,6 +2304,12 @@ defineExpose({
|
||||||
.vg-compose-left {
|
.vg-compose-left {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vg-compose-left--two-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 中等宽度:四列改为「参考条 + 右侧两行」,富文本独占一行(平板横屏等) */
|
/* 中等宽度:四列改为「参考条 + 右侧两行」,富文本独占一行(平板横屏等) */
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ const state = {
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
messageData: {},
|
messageData: {},
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
// 是否显示18禁弹窗
|
// 是否显示18禁弹窗:默认不拦截,直接进入首页等主页面
|
||||||
showForbidden: true,
|
showForbidden: false,
|
||||||
// 阻止页面切换
|
// 阻止页面切换
|
||||||
showPrevent: false,
|
showPrevent: false,
|
||||||
showLogin: false
|
showLogin: false
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,27 @@
|
||||||
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ag-field">
|
<div class="ag-field">
|
||||||
<label>GroupIds</label>
|
<label>资源组编号</label>
|
||||||
<a-input v-model="filters.groupIdsText" placeholder="多个ID用英文逗号分隔" />
|
<a-input v-model="filters.groupIdsText" placeholder="多个编号用英文逗号分隔" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ag-field">
|
<div class="ag-field">
|
||||||
<label>GroupType</label>
|
<label>资源组类型</label>
|
||||||
<a-select v-model="filters.groupType">
|
<a-select v-model="filters.groupType">
|
||||||
<a-option value="AIGC">AIGC</a-option>
|
<a-option value="AIGC">AIGC(生成类)</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ag-field">
|
<div class="ag-field">
|
||||||
<label>SortBy</label>
|
<label>排序字段</label>
|
||||||
<a-select v-model="filters.sortBy">
|
<a-select v-model="filters.sortBy">
|
||||||
<a-option value="CreateTime">CreateTime</a-option>
|
<a-option value="CreateTime">创建时间</a-option>
|
||||||
<a-option value="UpdateTime">UpdateTime</a-option>
|
<a-option value="UpdateTime">更新时间</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ag-field">
|
<div class="ag-field">
|
||||||
<label>SortOrder</label>
|
<label>排序方向</label>
|
||||||
<a-select v-model="filters.sortOrder">
|
<a-select v-model="filters.sortOrder">
|
||||||
<a-option value="Desc">Desc</a-option>
|
<a-option value="Desc">从新到旧</a-option>
|
||||||
<a-option value="Asc">Asc</a-option>
|
<a-option value="Asc">从旧到新</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ag-actions">
|
<div class="ag-actions">
|
||||||
|
|
@ -56,13 +56,13 @@
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>编号</th>
|
||||||
<th>Name</th>
|
<th>名称</th>
|
||||||
<th>Description</th>
|
<th>描述</th>
|
||||||
<th>GroupType</th>
|
<th>类型</th>
|
||||||
<th>ProjectName</th>
|
<th>项目名称</th>
|
||||||
<th>CreateTime</th>
|
<th>创建时间</th>
|
||||||
<th>UpdateTime</th>
|
<th>更新时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
<td>{{ item.Id || item.id }}</td>
|
<td>{{ item.Id || item.id }}</td>
|
||||||
<td>{{ item.Name || item.name }}</td>
|
<td>{{ item.Name || item.name }}</td>
|
||||||
<td>{{ item.Description || item.description || '-' }}</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.ProjectName || item.projectName || '-' }}</td>
|
||||||
<td>{{ item.CreateTime || item.createTime || '-' }}</td>
|
<td>{{ item.CreateTime || item.createTime || '-' }}</td>
|
||||||
<td>{{ item.UpdateTime || item.updateTime || '-' }}</td>
|
<td>{{ item.UpdateTime || item.updateTime || '-' }}</td>
|
||||||
|
|
@ -181,6 +181,12 @@ export default {
|
||||||
this.search(1)
|
this.search(1)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatGroupTypeLabel(item) {
|
||||||
|
const t = String(item?.GroupType || item?.groupType || '').trim()
|
||||||
|
if (!t) return '-'
|
||||||
|
if (t === 'AIGC') return 'AIGC(生成类)'
|
||||||
|
return t
|
||||||
|
},
|
||||||
clampGroupPage(n) {
|
clampGroupPage(n) {
|
||||||
const page = Number(n) || 1
|
const page = Number(n) || 1
|
||||||
const size = Number(this.filters.pageSize) || 10
|
const size = Number(this.filters.pageSize) || 10
|
||||||
|
|
@ -294,7 +300,7 @@ export default {
|
||||||
const res = await this.$axios({
|
const res = await this.$axios({
|
||||||
url: 'api/byteAssetGroup/getAssetGroup',
|
url: 'api/byteAssetGroup/getAssetGroup',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { Id: id }
|
data: { id: id }
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
this.detailData = res.data || {}
|
this.detailData = res.data || {}
|
||||||
|
|
|
||||||
|
|
@ -44,31 +44,31 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Name</label>
|
<label>素材名称</label>
|
||||||
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
<a-input v-model="filters.name" placeholder="按名称筛选" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Status</label>
|
<label>状态</label>
|
||||||
<a-select v-model="filters.status">
|
<a-select v-model="filters.status" placeholder="全部状态">
|
||||||
<a-option value="">全部</a-option>
|
<a-option value="">全部</a-option>
|
||||||
<a-option value="Active">Active</a-option>
|
<a-option value="Active">可用</a-option>
|
||||||
<a-option value="Processing">Processing</a-option>
|
<a-option value="Processing">处理中</a-option>
|
||||||
<a-option value="Failed">Failed</a-option>
|
<a-option value="Failed">失败</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>SortBy</label>
|
<label>排序字段</label>
|
||||||
<a-select v-model="filters.sortBy">
|
<a-select v-model="filters.sortBy">
|
||||||
<a-option value="CreateTime">CreateTime</a-option>
|
<a-option value="CreateTime">创建时间</a-option>
|
||||||
<a-option value="UpdateTime">UpdateTime</a-option>
|
<a-option value="UpdateTime">更新时间</a-option>
|
||||||
<a-option value="GroupId">GroupId</a-option>
|
<a-option value="GroupId">素材组</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>SortOrder</label>
|
<label>排序方向</label>
|
||||||
<a-select v-model="filters.sortOrder">
|
<a-select v-model="filters.sortOrder">
|
||||||
<a-option value="Desc">Desc</a-option>
|
<a-option value="Desc">从新到旧</a-option>
|
||||||
<a-option value="Asc">Asc</a-option>
|
<a-option value="Asc">从旧到新</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field actions">
|
<div class="field actions">
|
||||||
|
|
@ -83,13 +83,13 @@
|
||||||
<table class="asset-table">
|
<table class="asset-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Id</th>
|
<th>素材编号</th>
|
||||||
<th>Name</th>
|
<th>名称</th>
|
||||||
<th>URL</th>
|
<th>访问地址</th>
|
||||||
<th>GroupId</th>
|
<th>所属素材组</th>
|
||||||
<th>AssetType</th>
|
<th>类型</th>
|
||||||
<th>Status</th>
|
<th>状态</th>
|
||||||
<th>CreateTime</th>
|
<th>创建时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -135,8 +135,8 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ it.GroupId || it.groupId || '-' }}</td>
|
<td>{{ it.GroupId || it.groupId || '-' }}</td>
|
||||||
<td>{{ it.AssetType || it.assetType || '-' }}</td>
|
<td>{{ formatAssetTypeLabel(it) }}</td>
|
||||||
<td>{{ it.Status || it.status || '-' }}</td>
|
<td>{{ formatAssetStatusLabel(it) }}</td>
|
||||||
<td>{{ it.CreateTime || it.createTime || '-' }}</td>
|
<td>{{ it.CreateTime || it.createTime || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-button size="mini" type="outline" @click="getAsset(it)">详情</a-button>
|
<a-button size="mini" type="outline" @click="getAsset(it)">详情</a-button>
|
||||||
|
|
@ -265,6 +265,26 @@ export default {
|
||||||
this.loadGroups()
|
this.loadGroups()
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
assetUrl(it) {
|
||||||
return String(it?.URL || it?.url || '').trim()
|
return String(it?.URL || it?.url || '').trim()
|
||||||
},
|
},
|
||||||
|
|
@ -356,7 +376,7 @@ export default {
|
||||||
},
|
},
|
||||||
async createAsset() {
|
async createAsset() {
|
||||||
const groupId = String(this.createForm.groupId || '').trim()
|
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('请选择上传文件')
|
if (!this.createForm.file) return this.$message.error('请选择上传文件')
|
||||||
this.createLoading = true
|
this.createLoading = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -476,7 +496,7 @@ export default {
|
||||||
const res = await this.$axios({
|
const res = await this.$axios({
|
||||||
url: ASSET_DELETE_API,
|
url: ASSET_DELETE_API,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { Id: id }
|
data: { id: id }
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
this.$message.success('删除成功')
|
this.$message.success('删除成功')
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,29 @@
|
||||||
preload="metadata" />
|
preload="metadata" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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 class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="vg-chat-user-row">
|
<div v-else class="vg-chat-user-row">
|
||||||
|
|
@ -73,6 +96,29 @@
|
||||||
preload="metadata" />
|
preload="metadata" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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 class="vg-chat-time">{{ formatCreateTime(row.createTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vg-chat-user-col-status">
|
<div class="vg-chat-user-col-status">
|
||||||
|
|
@ -98,6 +144,13 @@
|
||||||
<div class="vg-chat-ai-status">
|
<div class="vg-chat-ai-status">
|
||||||
{{ taskStatusText(row) }}
|
{{ taskStatusText(row) }}
|
||||||
</div>
|
</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 class="vg-chat-ai-time">{{ formatCreateTime(row.updateTime || row.createTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vg-chat-result">
|
<div class="vg-chat-result">
|
||||||
|
|
@ -186,62 +239,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="vg-toolbar" :class="{ 'vg-toolbar--reference-submit': videoMode === 'image-reference' }">
|
<div class="vg-toolbar vg-toolbar--reference-submit">
|
||||||
<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
|
<div
|
||||||
class="vg-toolbar-actions"
|
class="vg-toolbar-actions"
|
||||||
:class="{ 'vg-toolbar-actions--reference': videoMode === 'image-reference' }">
|
:class="{ 'vg-toolbar-actions--reference': true }">
|
||||||
<mf-button
|
<mf-button
|
||||||
v-if="videoMode === 'image-reference'"
|
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
class="vg-toolbar-clear"
|
class="vg-toolbar-clear"
|
||||||
|
|
@ -749,6 +751,182 @@ export default {
|
||||||
await this.refreshChatFirstPage()
|
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() {
|
async loadPriceInfo() {
|
||||||
this.$axios({
|
this.$axios({
|
||||||
url: 'api/manager/selectInfo',
|
url: 'api/manager/selectInfo',
|
||||||
|
|
@ -1422,6 +1600,8 @@ export default {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* 稍微下移:让按钮对齐参数区视觉中线 */
|
||||||
|
margin-top: 6px;
|
||||||
background: linear-gradient(145deg, #00d4e8, #0090a8);
|
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);
|
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.35), 0 8px 24px rgba(0, 150, 180, 0.35);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
@ -1759,6 +1939,42 @@ export default {
|
||||||
padding: 0;
|
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 {
|
.vg-chat-ai-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1768,6 +1984,22 @@ export default {
|
||||||
margin-bottom: 12px;
|
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 {
|
.vg-chat-ai-status {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--vg-cyan);
|
color: var(--vg-cyan);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue