fix: 参数选择改到参数文件

This commit is contained in:
old burden 2026-03-30 12:06:46 +08:00
parent 4bba35a426
commit 5ae8614b1d
1 changed files with 280 additions and 270 deletions

View File

@ -3,41 +3,27 @@
<header class="vg-hero"> <header class="vg-hero">
<div class="vg-hero-text"> <div class="vg-hero-text">
<h1 class="vg-hero-title">视频生成</h1> <h1 class="vg-hero-title">视频生成</h1>
<p class="vg-hero-desc">文生视频图生视频选择模型后生成成片</p> <p class="vg-hero-desc">左侧选择生成模式编辑描述与参考素材后生成成片</p>
</div>
<div class="vg-type-select" @click.stop>
<span class="vg-hero-mode-label">生成模式</span>
<button
type="button"
class="vg-type-trigger"
:class="{ 'is-open': typeMenuOpen }"
@click.stop="typeMenuOpen = !typeMenuOpen"
:aria-expanded="typeMenuOpen">
<span class="vg-type-label">{{ videoModeLabel }}</span>
<svg class="vg-type-chevron" width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.01 7.982A1.2 1.2 0 0 1 21 9.679l-8.156 8.06a1.2 1.2 0 0 1-1.688 0L3 9.68a1.2 1.2 0 0 1 1.687-1.707L12 15.199l7.313-7.227a1.2 1.2 0 0 1 1.697.01Z" fill="currentColor" />
</svg>
</button>
<transition name="vg-fade">
<div v-show="typeMenuOpen" class="vg-type-dropdown">
<div class="vg-type-dropdown-bg" />
<ul class="vg-type-options" role="listbox">
<li
v-for="opt in videoModeOptions"
:key="opt.value"
:class="['vg-type-option', { active: videoMode === opt.value }]"
role="option"
@click="pickVideoMode(opt.value)">
{{ opt.label }}
</li>
</ul>
</div>
</transition>
</div> </div>
</header> </header>
<div class="vg-body"> <div class="vg-body">
<!-- 左侧生成器 --> <!-- 最左生成模式 -->
<aside class="vg-left-rail">
<p class="vg-rail-title">生成模式</p>
<ul class="vg-mode-list" role="list">
<li
v-for="opt in videoModeOptions"
:key="opt.value"
:class="['vg-mode-item', { active: videoMode === opt.value }]"
role="listitem"
@click="pickVideoMode(opt.value)">
{{ opt.label }}
</li>
</ul>
</aside>
<!-- 中间生成器 -->
<div class="vg-generator"> <div class="vg-generator">
<div class="vg-generator-inner"> <div class="vg-generator-inner">
<!-- 参考上传区图生视频文生视频时显示轻量占位 --> <!-- 参考上传区图生视频文生视频时显示轻量占位 -->
@ -47,6 +33,15 @@
<div v-if="isImageVideoMode" class="vg-ref-content"> <div v-if="isImageVideoMode" class="vg-ref-content">
<div class="vg-ref-tilt"> <div class="vg-ref-tilt">
<div class="upload-inner"> <div class="upload-inner">
<template v-if="videoMode === 'image-reference'">
<div class="upload-title">
<div class="upload-title-left">参考图</div>
<div class="upload-title-tip">
请使用下方编辑器工具栏插入参考素材添加图片此处不再单独上传
</div>
</div>
</template>
<template v-else>
<div class="upload-title"> <div class="upload-title">
<div class="upload-title-left"> <div class="upload-title-left">
{{ imageUploadPrimaryLabel }} {{ imageUploadPrimaryLabel }}
@ -56,19 +51,11 @@
</div> </div>
</div> </div>
<mf-image-upload <mf-image-upload
v-if="videoMode !== 'image-reference'"
listType="picture-card" listType="picture-card"
uploadHeight="140px" uploadHeight="140px"
:show-file-list="false" :show-file-list="false"
:content="$t('common.uploadFirstPlaceholder') || '点击上传'" :content="$t('common.uploadFirstPlaceholder') || '点击上传'"
v-model="firstUrl" /> v-model="firstUrl" />
<mf-image-upload
v-else
listType="picture-card"
uploadHeight="140px"
:show-file-list="false"
:content="'上传参考图'"
v-model="referenceUrl" />
<div class="last-frame" v-if="videoMode === 'image-first-last-frame'"> <div class="last-frame" v-if="videoMode === 'image-first-last-frame'">
<div class="upload-title-left"> <div class="upload-title-left">
{{ $t('common.uploadLastPlaceholder') || '尾帧(可选)' }} {{ $t('common.uploadLastPlaceholder') || '尾帧(可选)' }}
@ -80,6 +67,7 @@
:content="$t('common.uploadLastPlaceholder') || '上传尾帧'" :content="$t('common.uploadLastPlaceholder') || '上传尾帧'"
v-model="lastUrl" /> v-model="lastUrl" />
</div> </div>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -96,32 +84,55 @@
<div class="vg-main-column"> <div class="vg-main-column">
<div class="rich-editor-container"> <div class="rich-editor-container">
<RichTextEditor <VideoRichEditor
v-model="editorContent" ref="videoRichEditor"
:show-toolbar="videoMode !== 'text-to-video'"
@text-change="bumpEditorVisualTick"
:placeholder=" :placeholder="
$t('common.textVideoPlaceholder') || $t('common.textVideoPlaceholder') ||
'描述画面与动态,例如:阳光下的女孩在海边起舞…' '描述画面与动态,例如:阳光下的女孩在海边起舞…'
" "
:uploaded-images="uploadedImages"
@text-change="handleTextChange"
@image-upload="handleImageUpload"
/> />
</div> </div>
<div class="vg-toolbar"> <div class="vg-toolbar">
<div class="vg-toolbar-settings"> <div class="vg-toolbar-settings">
<div class="vg-model-wrap"> <div class="vg-params-row">
<span class="vg-model-icon" aria-hidden="true"> <div class="vg-param">
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <span class="vg-param-label">模型</span>
<path fill="currentColor" d="M11.805 5.786c1.25-.926 2.193-1.373 2.471-1.096.489.488-1.261 3.03-3.909 5.677-4.33 4.331-6.715 8.968-5.326 10.358.29.29.723.416 1.264.394.421.017.92-.07 1.48-.249-2.117.9-3.859 1.005-4.76.104-1.874-1.874.61-7.402 5.553-12.353l.022-.02c.03-.032.062-.063.093-.094l.065-.064.11-.108c.97-.95 1.96-1.804 2.937-2.549Zm5.55 11.57c1.532-1.531 3.2-2.347 3.725-1.822.525.525-.29 2.192-1.822 3.724-1.532 1.531-3.2 2.347-3.725 1.822-.524-.525.291-2.192 1.822-3.724Z" /> <a-select
</svg> v-model="selectedModel"
</span> class="vg-param-select"
<a-select v-model="selectedModel" class="vg-model-select"> placeholder="请选择模型"
<a-option v-for="option in modelOptions" :key="option.value" :value="option.value"> allow-clear>
{{ option.label }} <a-option v-for="opt in modelOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-option> </a-option>
</a-select> </a-select>
</div> </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"> <div class="vg-toolbar-actions">
<button <button
@ -210,8 +221,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import VideoRichEditor from '@/components/VideoRichEditor.vue'
import RichTextEditor from '@/components/RichTextEditor.vue'
export default { export default {
name: 'VideoGen', name: 'VideoGen',
@ -227,8 +237,7 @@ export default {
], ],
firstUrl: '', firstUrl: '',
lastUrl: '', lastUrl: '',
referenceUrl: '', editorVisualTick: 0,
editorContent: '',
interval: null, interval: null,
videoUrl: null, videoUrl: null,
videoLoading: false, videoLoading: false,
@ -237,29 +246,25 @@ export default {
showResult: false, showResult: false,
price: null, price: null,
id: null, id: null,
modelOptions: [ modelOptions: [],
{ label: 'Seedance 2.0', value: 'ep-20260326165811-dlkth' }, ratioOptions: [],
{ label: 'Seedance 2.0 Fast', value: 'ep-20260326170056-dkj9m' } durationOptions: [],
], resolutionOptions: [],
selectedModel: 'ep-20260326165811-dlkth', selectedModel: '',
uploadedImages: [], selectedRatio: '',
selectedDuration: null,
selectedResolution: '',
maxPollAttempts: 40, maxPollAttempts: 40,
typeMenuOpen: false,
taskRows: [] taskRows: []
} }
}, },
components: { components: {
RichTextEditor VideoRichEditor
}, },
computed: { computed: {
...mapGetters(['lang']),
canCancel() { canCancel() {
return !!this.videoId && !this.videoUrl return !!this.videoId && !this.videoUrl
}, },
videoModeLabel() {
const o = this.videoModeOptions.find((x) => x.value === this.videoMode)
return o ? o.label : '文生视频'
},
isImageVideoMode() { isImageVideoMode() {
return this.videoMode !== 'text-to-video' return this.videoMode !== 'text-to-video'
}, },
@ -269,7 +274,12 @@ export default {
return this.$t('common.uploadFirstImage') || '上传首帧' return this.$t('common.uploadFirstImage') || '上传首帧'
}, },
posterUrl() { posterUrl() {
const f = this.videoMode === 'image-reference' ? this.referenceUrl : this.firstUrl void this.editorVisualTick
if (this.videoMode === 'image-reference') {
const url = this.firstReferenceImageUrlFromEditor()
return url || ''
}
const f = this.firstUrl
if (!f) return '' if (!f) return ''
return typeof f === 'object' ? (f.url || '') : f return typeof f === 'object' ? (f.url || '') : f
} }
@ -277,33 +287,39 @@ export default {
mounted() { mounted() {
this.loadPriceInfo() this.loadPriceInfo()
this.loadTaskList() this.loadTaskList()
document.addEventListener('click', this.onDocClick) this.loadVideoParams()
}, },
beforeUnmount() { beforeUnmount() {
this.destroyInterval() this.destroyInterval()
document.removeEventListener('click', this.onDocClick)
}, },
methods: { methods: {
onDocClick() { plainPromptText() {
this.typeMenuOpen = false return this.$refs.videoRichEditor?.getPlainText?.() || ''
}, },
bumpEditorVisualTick() {
this.editorVisualTick++
},
/** 编辑器内插入的参考图 URL与 getContentItems 一致) */
firstReferenceImageUrlFromEditor() {
const items = this.$refs.videoRichEditor?.getContentItems?.() || []
for (const i of items) {
if (i.type === 'image_url' && i.image_url?.url) {
if (!i.role || i.role === 'reference_image') return i.image_url.url
}
}
return ''
},
pickVideoMode(m) { pickVideoMode(m) {
this.videoMode = m this.videoMode = m
this.firstUrl = '' this.firstUrl = ''
this.lastUrl = '' this.lastUrl = ''
this.referenceUrl = '' this.$nextTick(() => {
this.editorContent = '' this.$refs.videoRichEditor?.clear?.()
this.uploadedImages = [] this.bumpEditorVisualTick()
this.typeMenuOpen = false })
},
plainPromptText() {
if (!this.editorContent || !String(this.editorContent).trim()) return ''
const s = String(this.editorContent)
if (s.indexOf('<') < 0) return s.trim()
const d = document.createElement('div')
d.innerHTML = s
return (d.textContent || d.innerText || '').trim()
}, },
async loadTaskList() { async loadTaskList() {
@ -343,27 +359,53 @@ export default {
}) })
}, },
handleTextChange(content) { async loadVideoParams() {
this.editorContent = content try {
}, const res = await this.$axios({
url: 'api/portal/video/options',
handleImageUpload(imageInfo) { method: 'GET'
if (imageInfo && imageInfo.url) {
this.uploadedImages.push({
url: imageInfo.url,
name: imageInfo.name || 'image'
}) })
if (res.code !== 200 || !res.data) return
const { defaults, models, ratios, durations, resolutions } = res.data
this.modelOptions = Array.isArray(models) ? models : []
this.ratioOptions = Array.isArray(ratios) ? ratios : []
this.durationOptions = Array.isArray(durations) ? durations.map((n) => Number(n)) : []
this.resolutionOptions = Array.isArray(resolutions) ? resolutions : []
const d = defaults || {}
if (d.model) {
this.selectedModel = d.model
} else if (this.modelOptions.length) {
this.selectedModel = this.modelOptions[0].value
}
if (d.ratio && this.ratioOptions.includes(d.ratio)) {
this.selectedRatio = d.ratio
} else if (this.ratioOptions.length) {
this.selectedRatio = this.ratioOptions[0]
}
const durNum = d.duration != null ? Number(d.duration) : null
if (durNum != null && this.durationOptions.includes(durNum)) {
this.selectedDuration = durNum
} else if (this.durationOptions.length) {
this.selectedDuration = this.durationOptions[0]
}
if (d.resolution && this.resolutionOptions.includes(d.resolution)) {
this.selectedResolution = d.resolution
} else if (this.resolutionOptions.length) {
this.selectedResolution = this.resolutionOptions[0]
}
} catch (_) {
this.$message?.warning?.('加载视频参数配置失败')
} }
}, },
generateVideo() { generateVideo() {
const text = this.plainPromptText() || '一个优雅的女孩在阳光下跳舞' const plain = this.plainPromptText()
const text = plain || '一个优雅的女孩在阳光下跳舞'
if (this.isImageVideoMode) { if (this.isImageVideoMode) {
if (this.videoMode === 'image-reference') { if (this.videoMode === 'image-reference') {
const ref = typeof this.referenceUrl === 'object' ? this.referenceUrl?.url : this.referenceUrl if (!this.firstReferenceImageUrlFromEditor()) {
if (!ref) { this.$message.error('请通过编辑器工具栏「插入参考素材」添加参考图')
this.$message.error('请上传参考图')
return return
} }
} else { } else {
@ -380,8 +422,16 @@ export default {
} }
} }
} }
} else if (!this.plainPromptText()) { } else {
this.$message.error('请输入视频描述文本') const contentItems = this.$refs.videoRichEditor?.getContentItems?.() || []
if (!plain && (!contentItems.length || !contentItems.some((i) => i.type === 'image_url'))) {
this.$message.error('请输入视频描述文本或插入参考素材')
return
}
}
if (!this.selectedModel || !this.selectedRatio || this.selectedDuration == null || !this.selectedResolution) {
this.$message.error('请选择模型、比例、时长与分辨率')
return return
} }
@ -390,7 +440,17 @@ export default {
const params = { const params = {
text, text,
functionType: '21', functionType: '21',
model: this.selectedModel model: this.selectedModel,
ratio: this.selectedRatio,
duration: this.selectedDuration,
resolution: this.selectedResolution
}
if (this.videoMode === 'text-to-video') {
const contentItems = this.$refs.videoRichEditor?.getContentItems?.() || []
if (contentItems.length) {
params.content = contentItems
}
} }
const urlMap = { const urlMap = {
@ -407,7 +467,7 @@ export default {
params.lastUrl = typeof this.lastUrl === 'object' ? this.lastUrl.url : this.lastUrl params.lastUrl = typeof this.lastUrl === 'object' ? this.lastUrl.url : this.lastUrl
} }
if (this.videoMode === 'image-reference') { if (this.videoMode === 'image-reference') {
params.referenceUrl = typeof this.referenceUrl === 'object' ? this.referenceUrl.url : this.referenceUrl params.referenceUrl = this.firstReferenceImageUrlFromEditor()
} }
this.$axios({ this.$axios({
@ -573,130 +633,71 @@ export default {
line-height: 1.5; line-height: 1.5;
} }
.vg-type-select {
position: relative;
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 12px;
}
.vg-hero-mode-label {
font-size: 13px;
color: var(--vg-muted);
white-space: nowrap;
}
.vg-type-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
margin: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.03));
border: 1px solid var(--vg-border);
border-radius: 12px;
color: #fff;
font-size: 15px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
box-shadow: 0 0 0 1px rgba(0, 202, 224, 0.15);
&:hover {
border-color: rgba(0, 202, 224, 0.45);
}
&.is-open .vg-type-chevron {
transform: rotate(180deg);
}
}
.vg-type-label {
background: linear-gradient(90deg, #fff, rgba(0, 202, 224, 0.95));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.vg-type-chevron {
width: 0.85em;
height: 0.85em;
opacity: 0.75;
transition: transform 0.2s cubic-bezier(0.15, 0.75, 0.3, 1);
}
.vg-type-dropdown {
position: absolute;
left: 0;
top: calc(100% + 8px);
min-width: 100%;
z-index: 50;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
}
.vg-type-dropdown-bg {
position: absolute;
inset: 0;
background: rgba(18, 20, 26, 0.98);
backdrop-filter: blur(12px);
border: 1px solid var(--vg-border);
border-radius: 14px;
}
.vg-type-options {
position: relative;
list-style: none;
margin: 0;
padding: 6px;
z-index: 1;
}
.vg-type-option {
padding: 12px 16px;
border-radius: 10px;
color: var(--vg-muted);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s;
&:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--vg-text);
}
&.active {
color: var(--vg-cyan);
background: rgba(0, 202, 224, 0.1);
}
}
.vg-fade-enter-active,
.vg-fade-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.vg-fade-enter-from,
.vg-fade-leave-to {
opacity: 0;
transform: translateY(-6px);
}
/* —— Body —— */ /* —— Body —— */
.vg-body { .vg-body {
display: flex; display: flex;
flex: 1; flex: 1;
gap: 20px; gap: 16px;
align-items: stretch; align-items: stretch;
min-height: 420px; min-height: 420px;
} }
.vg-left-rail {
flex: 0 0 200px;
min-width: 176px;
padding: 18px 14px;
background: var(--vg-panel);
border: 1px solid var(--vg-border);
border-radius: 20px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
align-self: stretch;
}
.vg-rail-title {
margin: 0 0 14px;
font-size: 12px;
color: var(--vg-muted);
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.vg-mode-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.vg-mode-item {
padding: 11px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
line-height: 1.35;
color: var(--vg-muted);
cursor: pointer;
border: 1px solid transparent;
background: rgba(0, 0, 0, 0.22);
transition: color 0.15s, border-color 0.15s, background 0.15s;
&:hover {
color: var(--vg-text);
background: rgba(255, 255, 255, 0.05);
}
&.active {
color: var(--vg-cyan);
border-color: rgba(0, 202, 224, 0.35);
background: rgba(0, 202, 224, 0.1);
}
}
.vg-generator { .vg-generator {
flex: 0 0 min(520px, 44vw); flex: 1;
min-width: 300px; min-width: 280px;
display: flex; display: flex;
} }
@ -809,21 +810,11 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.rich-editor-root) { :deep(.video-editor-root) {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} min-height: 280px;
:deep(.rich-editor) {
flex: 1;
min-height: 220px !important;
font-size: 15px;
line-height: 1.7;
padding: 16px 18px;
border-radius: 14px !important;
background: rgba(0, 0, 0, 0.25) !important;
border: 1px solid var(--vg-border) !important;
} }
} }
@ -842,29 +833,38 @@ export default {
min-width: 0; min-width: 0;
} }
.vg-model-wrap { .vg-params-row {
display: flex; display: flex;
align-items: center; flex-wrap: wrap;
gap: 10px; align-items: flex-end;
padding: 4px 12px; gap: 12px 14px;
padding: 10px 12px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--vg-border); border: 1px solid var(--vg-border);
} }
.vg-model-icon { .vg-param {
display: flex; display: flex;
color: var(--vg-cyan); flex-direction: column;
opacity: 0.85; gap: 6px;
flex: 1 1 140px;
min-width: 120px;
} }
.vg-model-select { .vg-param-label {
flex: 1; font-size: 12px;
min-width: 0; color: var(--vg-muted);
font-weight: 600;
}
.vg-param-select {
width: 100%;
:deep(.arco-select-view-single) { :deep(.arco-select-view-single) {
background: transparent !important; background: rgba(0, 0, 0, 0.2) !important;
border: none !important; border: 1px solid var(--vg-border) !important;
border-radius: 10px !important;
color: var(--vg-text) !important; color: var(--vg-text) !important;
} }
:deep(.arco-select-view-value) { :deep(.arco-select-view-value) {
@ -999,6 +999,20 @@ export default {
.vg-body { .vg-body {
flex-direction: column; flex-direction: column;
} }
.vg-left-rail {
flex: none;
width: 100%;
min-width: 0;
}
.vg-mode-list {
flex-direction: row;
flex-wrap: wrap;
}
.vg-mode-item {
flex: 1 1 calc(50% - 4px);
min-width: 140px;
text-align: center;
}
.vg-generator { .vg-generator {
flex: none; flex: none;
width: 100%; width: 100%;
@ -1007,10 +1021,6 @@ export default {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.vg-type-select {
flex-direction: column;
align-items: flex-start;
}
} }
.vg-task-empty { .vg-task-empty {