feat(ui):优化视频生成布局
This commit is contained in:
1 parent
135f8fdb02
commit
70d057ce23
3 files changed
+143
-83
No files matched your search
@@ -257,6 +257,48 @@ body.modal-open {
|
||||
@apply mt-1 text-xs text-red-500;
|
||||
}
|
||||
|
||||
/* ============ Playground 输入容器 ============ */
|
||||
/* 把附件片、输入框、操作按钮收进同一个圆角盒子,聚焦时整块高亮。
|
||||
对话、视频两个 Tab 共用,故放在全局而非某个组件的 scoped 里。 */
|
||||
.input-shell {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 1rem;
|
||||
background: #fff;
|
||||
padding: 0.625rem 0.75rem 0.5rem;
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
/* 聚焦与拖拽时整块高亮,而不是只高亮里面的 textarea */
|
||||
.input-shell:focus-within {
|
||||
border-color: rgba(20, 184, 166, 0.55);
|
||||
box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.09);
|
||||
}
|
||||
.input-shell-dragging {
|
||||
border-color: #2dd4bf;
|
||||
border-style: dashed;
|
||||
background: rgba(240, 253, 250, 0.6);
|
||||
}
|
||||
|
||||
/* 输入区内的小工具按钮(附件 / 清空 等) */
|
||||
.input-tool {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
transition: background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
.input-tool:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
color: #0d9488;
|
||||
}
|
||||
.input-tool:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
/* ============ 液态玻璃效果 ============ */
|
||||
/* 大面积磨砂(header、sticky 栏) */
|
||||
.glass {
|
||||
|
||||
@@ -751,45 +751,8 @@ function clearChat() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── 输入容器:把附件片、输入框、操作按钮收进同一个圆角盒子 ── */
|
||||
.input-shell {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 1rem;
|
||||
background: #fff;
|
||||
padding: 0.625rem 0.75rem 0.5rem;
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
/* 聚焦与拖拽时整块高亮,而不是只高亮里面的 textarea */
|
||||
.input-shell:focus-within {
|
||||
border-color: rgba(20, 184, 166, 0.55);
|
||||
box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.09);
|
||||
}
|
||||
.input-shell-dragging {
|
||||
border-color: #2dd4bf;
|
||||
border-style: dashed;
|
||||
background: rgba(240, 253, 250, 0.6);
|
||||
}
|
||||
|
||||
/* ── 输入区内的小工具按钮 ── */
|
||||
.input-tool {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
transition: background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
.input-tool:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
color: #0d9488;
|
||||
}
|
||||
.input-tool:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #d1d5db;
|
||||
}
|
||||
/* 输入容器(.input-shell / .input-tool)已提到 styles/main.css,
|
||||
与视频 Tab 共用一份定义 */
|
||||
|
||||
/* ── Temperature 滑块:替换浏览器默认的紫色原生样式 ── */
|
||||
.range-slider {
|
||||
|
||||
@@ -8,7 +8,7 @@ import GenerationProgressCard from './GenerationProgressCard.vue'
|
||||
import BaseSelect from '../../../components/common/BaseSelect.vue'
|
||||
import AttachmentBar from './AttachmentBar.vue'
|
||||
import PromptInput from './PromptInput.vue'
|
||||
import { useAttachments } from './useAttachments'
|
||||
import { useAttachments, formatBytes } from './useAttachments'
|
||||
import { compileVideoPrompt } from './attachmentParts'
|
||||
|
||||
interface ModelItem {
|
||||
@@ -39,13 +39,19 @@ const duration = ref(5)
|
||||
// 能随请求发出:/v1/videos 的 input_reference 仅承载一张图,其余素材待后端扩协议。
|
||||
const {
|
||||
list: attachmentList,
|
||||
totalBytes: attachmentBytes,
|
||||
accept: acceptTypes,
|
||||
max: attachmentMax,
|
||||
addFiles: addAttachments,
|
||||
remove: removeAttachment,
|
||||
clear: clearAttachments,
|
||||
} = useAttachments({ kinds: ['image', 'video', 'audio'], max: 6 })
|
||||
|
||||
const promptInput = ref<InstanceType<typeof PromptInput> | null>(null)
|
||||
// bare 变体的附件片不自带上传入口,文件选择器由输入区的「素材」按钮通过 ref 触发
|
||||
const attachmentBar = ref<InstanceType<typeof AttachmentBar> | null>(null)
|
||||
// 输入容器的拖拽高亮状态(与对话 Tab 一致,拖放由 input-shell 承接)
|
||||
const dragging = ref(false)
|
||||
// 提交时未能随请求发出的素材名,用于结果区提示
|
||||
const deferredNames = ref<string[]>([])
|
||||
|
||||
@@ -53,6 +59,20 @@ function insertMention(name: string) {
|
||||
promptInput.value?.insertMention(name)
|
||||
}
|
||||
|
||||
function openFilePicker() {
|
||||
attachmentBar.value?.pick()
|
||||
}
|
||||
|
||||
function clearAllAttachments() {
|
||||
clearAttachments()
|
||||
}
|
||||
|
||||
// 输入容器承接拖放:与粘贴共用同一条上传链路
|
||||
function onDropFiles(e: DragEvent) {
|
||||
dragging.value = false
|
||||
if (e.dataTransfer?.files?.length) addAttachments(Array.from(e.dataTransfer.files))
|
||||
}
|
||||
|
||||
const modelOptions = computed(() => props.models.map(m => ({ value: m.model_id, label: m.model_name || m.model_id })))
|
||||
|
||||
// 空态与任务卡里展示的模型名(列表由父组件异步加载,可能暂时查不到)
|
||||
@@ -289,7 +309,8 @@ function downloadVideo() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 与对话 Tab 同一套等高布局:左参数栏内部滚动,底部主操作常驻,右结果区撑满 -->
|
||||
<!-- 与对话 Tab 同一套等高布局:左参数栏内部滚动,右侧结果区撑满、
|
||||
提示词与素材入口收在结果卡底部,主操作按钮落在输入区操作行 -->
|
||||
<div class="flex flex-col lg:h-[calc(100vh-15rem)] lg:overflow-hidden">
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-[22rem_minmax(0,1fr)] lg:grid-rows-[minmax(0,1fr)] lg:min-h-0 lg:flex-1">
|
||||
<!-- Left: 参数 -->
|
||||
@@ -310,32 +331,6 @@ function downloadVideo() {
|
||||
<BaseSelect v-model="selectedModel" :options="modelOptions" />
|
||||
</div>
|
||||
|
||||
<!-- 输入:参考素材 + 提示词 -->
|
||||
<div class="space-y-4 border-t border-gray-100 pt-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-xs font-semibold text-gray-500">参考素材</label>
|
||||
<AttachmentBar
|
||||
:attachments="attachmentList"
|
||||
:accept="acceptTypes"
|
||||
:max="attachmentMax"
|
||||
@add="addAttachments"
|
||||
@remove="removeAttachment"
|
||||
@insert="insertMention"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-xs font-semibold text-gray-500">提示词</label>
|
||||
<PromptInput
|
||||
ref="promptInput"
|
||||
v-model="prompt"
|
||||
:attachments="attachmentList"
|
||||
:min-rows="4"
|
||||
placeholder="描述你想生成的视频,输入 @ 引用已上传的素材..."
|
||||
@paste="addAttachments"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出规格 -->
|
||||
<div class="space-y-4 border-t border-gray-100 pt-4">
|
||||
<div>
|
||||
@@ -371,24 +366,12 @@ function downloadVideo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主操作常驻卡片底部:参数再长也不用来回滚动找按钮 -->
|
||||
<div class="shrink-0 space-y-2 border-t border-gray-100 px-4 py-3">
|
||||
<button class="btn btn-primary w-full" :disabled="submitting || polling || !prompt.trim()" @click="submitTask">
|
||||
<Icon name="play" size="sm" />
|
||||
{{ submitting ? '提交中...' : polling ? '生成中...' : '生成视频' }}
|
||||
</button>
|
||||
<button v-if="currentTask" class="btn btn-secondary w-full" @click="resetTask">
|
||||
<Icon name="refresh" size="sm" />
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: 结果 -->
|
||||
<div class="lg:min-h-0">
|
||||
<div class="card flex min-h-[420px] flex-col overflow-hidden lg:h-full">
|
||||
<!-- Right: 结果 + 输入(提示词与素材入口贴卡片底部,与对话 Tab 同构) -->
|
||||
<div class="h-[62vh] min-h-[420px] lg:h-auto lg:min-h-0">
|
||||
<div class="card flex h-full flex-col overflow-hidden">
|
||||
<div class="flex shrink-0 items-center justify-between gap-3 border-b border-gray-100 px-5 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="photo" size="sm" class="text-primary-500" />
|
||||
@@ -408,7 +391,7 @@ function downloadVideo() {
|
||||
<h3 class="text-lg font-semibold text-gray-900">等待生成</h3>
|
||||
<p class="mt-1.5 max-w-sm text-sm text-gray-500">
|
||||
<template v-if="selectedModel">
|
||||
当前模型 <span class="font-medium text-gray-700">{{ selectedModelName }}</span>,填写提示词后点击「生成视频」
|
||||
当前模型 <span class="font-medium text-gray-700">{{ selectedModelName }}</span>,在下方输入提示词后点击「生成视频」
|
||||
</template>
|
||||
<template v-else>请先在左侧选择一个视频模型</template>
|
||||
</p>
|
||||
@@ -476,6 +459,78 @@ function downloadVideo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区:参考素材片 + 提示词 + 操作行整合进同一个圆角盒子(与对话 Tab 同构) -->
|
||||
<div class="shrink-0 border-t border-gray-100 p-3 lg:p-4">
|
||||
<div
|
||||
class="input-shell"
|
||||
:class="{ 'input-shell-dragging': dragging }"
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave="dragging = false"
|
||||
@drop.prevent="onDropFiles"
|
||||
>
|
||||
<!-- 素材片(bare:无边框无上传按钮,融入本容器);
|
||||
文件选择器仍挂载于此,由下方「素材」按钮通过 ref 触发 -->
|
||||
<AttachmentBar
|
||||
ref="attachmentBar"
|
||||
variant="bare"
|
||||
:class="attachmentList.length ? 'mb-2' : ''"
|
||||
:attachments="attachmentList"
|
||||
:accept="acceptTypes"
|
||||
:max="attachmentMax"
|
||||
@add="addAttachments"
|
||||
@remove="removeAttachment"
|
||||
@insert="insertMention"
|
||||
/>
|
||||
|
||||
<PromptInput
|
||||
ref="promptInput"
|
||||
v-model="prompt"
|
||||
bare
|
||||
:attachments="attachmentList"
|
||||
:max-rows="6"
|
||||
placeholder="描述你想生成的视频,输入 @ 引用已上传的素材..."
|
||||
@submit="submitTask"
|
||||
@paste="addAttachments"
|
||||
/>
|
||||
|
||||
<!-- 操作行 -->
|
||||
<div class="mt-1.5 flex items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<button
|
||||
class="input-tool"
|
||||
title="添加参考素材(图片/视频/音频,也可拖拽到此处或直接粘贴)"
|
||||
@click="openFilePicker"
|
||||
>
|
||||
<Icon name="paperclip" size="xs" />
|
||||
素材
|
||||
</button>
|
||||
<button
|
||||
v-if="attachmentList.length"
|
||||
class="input-tool"
|
||||
title="清空已选素材"
|
||||
@click="clearAllAttachments"
|
||||
>
|
||||
<Icon name="x" size="xs" />
|
||||
清空素材
|
||||
</button>
|
||||
<span class="ml-1 hidden truncate text-[11px] text-gray-400 sm:inline">
|
||||
Enter 生成 · Shift + Enter 换行<template v-if="attachmentList.length"> · 素材 {{ formatBytes(attachmentBytes) }}</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<button v-if="currentTask" class="btn btn-ghost btn-sm text-gray-500" :disabled="submitting || polling" @click="resetTask">
|
||||
<Icon name="refresh" size="xs" />
|
||||
重新生成
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm px-4" :disabled="submitting || polling || !prompt.trim()" @click="submitTask">
|
||||
<Icon name="play" size="xs" />
|
||||
{{ submitting ? '提交中...' : polling ? '生成中...' : '生成视频' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user