Files
geekai/web/src/views/Video.vue
2025-07-18 18:04:32 +08:00

646 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-video">
<!-- 左侧参数设置面板 -->
<div class="params-panel">
<!-- 视频类型切换标签页 -->
<el-tabs
v-model="store.activeVideoType"
@tab-change="store.switchVideoType"
class="video-type-tabs"
>
<!-- Luma 视频参数 -->
<el-tab-pane label="Luma视频" name="luma">
<div class="params-container">
<div class="param-line">
<el-input
v-model="store.lumaParams.prompt"
type="textarea"
maxlength="2000"
:autosize="{ minRows: 4, maxRows: 6 }"
placeholder="请在此输入视频提示词,用逗号分割,您也可以点击下面的提示词助手生成视频提示词"
/>
</div>
<!-- 提示词生成按钮 -->
<div class="param-line pt">
<el-button
class="generate-btn"
@click="store.generatePrompt"
:loading="store.isGenerating"
size="small"
color="#5865f2"
style="width: 100%"
>
<i class="iconfont icon-chuangzuo"></i>
生成AI视频提示词
</el-button>
</div>
<!-- 图片辅助生成开关 -->
<div class="param-line pt">
<div class="image-mode-toggle">
<span class="label">使用图片辅助生成</span>
<el-switch
v-model="store.lumaUseImageMode"
@change="store.toggleLumaImageMode"
size="small"
/>
</div>
</div>
<!-- 图片上传区域可折叠 -->
<div v-if="store.lumaUseImageMode" class="img-inline">
<div class="img-uploader video-img-box mr-2">
<el-icon
v-if="store.lumaParams.image"
@click="store.removeLumaImage('start')"
class="removeimg"
>
<CircleCloseFilled />
</el-icon>
<el-upload
class="uploader img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="store.uploadLumaStartImage"
accept=".jpg,.png,.jpeg"
>
<el-image
v-if="store.lumaParams.image"
:src="store.lumaParams.image"
fit="cover"
/>
<div class="flex flex-col" v-else>
<el-icon class="mb-1 text-base"><Plus /></el-icon>
<span>起始帧</span>
</div>
</el-upload>
</div>
<div
class="flex items-center h-[120px] cursor-pointer"
v-if="store.lumaParams.image && store.lumaParams.image_tail"
>
<el-tooltip content="交换图片" placement="top">
<i class="iconfont icon-exchange" @click="store.switchLumaImages"></i>
</el-tooltip>
</div>
<div class="img-uploader video-img-box ml-2">
<el-icon
v-if="store.lumaParams.image_tail"
@click="store.removeLumaImage('end')"
class="removeimg"
>
<CircleCloseFilled />
</el-icon>
<el-upload
class="uploader img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="store.uploadLumaEndImage"
accept=".jpg,.png,.jpeg"
>
<el-image
v-if="store.lumaParams.image_tail"
:src="store.lumaParams.image_tail"
fit="cover"
/>
<div class="flex flex-col" v-else>
<el-icon class="mb-1 text-base"><Plus /></el-icon>
<span>结束帧</span>
</div>
</el-upload>
</div>
</div>
<!-- Luma 特有参数设置 -->
<div class="item-group flex justify-between">
<span class="label">循环参考图</span>
<el-switch v-model="store.lumaParams.loop" size="small" />
</div>
<div class="item-group flex justify-between">
<span class="label">提示词优化</span>
<el-switch v-model="store.lumaParams.expand_prompt" size="small" />
</div>
<!-- 算力显示 -->
<el-row class="text-info">
<el-text type="primary"
>当前可用算力<el-text type="warning">{{ store.availablePower }}</el-text></el-text
>
</el-row>
<!-- 生成按钮 -->
<div class="submit-btn">
<el-button type="primary" :dark="false" @click="store.createLumaVideo" round>
立即生成 ({{ store.lumaPowerCost }}<i class="iconfont icon-vip2"></i>)
</el-button>
</div>
</div>
</el-tab-pane>
<!-- KeLing 视频参数 -->
<el-tab-pane label="可灵视频" name="keling">
<div class="params-container">
<el-form :model="store.kelingParams" label-width="80px" label-position="left">
<!-- 画面比例 -->
<div class="param-line">
<div class="param-line pt">
<span>画面比例</span>
<el-tooltip content="生成画面的尺寸比例" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</div>
<div class="param-line pt">
<el-row :gutter="10">
<el-col :span="8" v-for="item in store.rates" :key="item.value">
<div
class="flex-col items-center"
:class="
item.value === store.kelingParams.aspect_ratio
? 'grid-content active'
: 'grid-content'
"
@click="store.changeRate(item)"
>
<el-image class="icon proportion" :src="item.img" fit="cover"></el-image>
<div class="texts">{{ item.text }}</div>
</div>
</el-col>
</el-row>
</div>
</div>
<!-- 模型选择 -->
<div class="param-line">
<el-form-item label="模型选择">
<el-select
v-model="store.kelingParams.model"
placeholder="请选择模型"
@change="store.updateModelPower"
>
<el-option
v-for="item in store.models"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<!-- 视频时长 -->
<div class="param-line">
<el-form-item label="视频时长">
<el-select
v-model="store.kelingParams.duration"
placeholder="请选择时长"
@change="store.updateModelPower"
>
<el-option label="5秒" value="5" />
<el-option label="10秒" value="10" />
</el-select>
</el-form-item>
</div>
<!-- 生成模式 -->
<div class="param-line">
<el-form-item label="生成模式">
<el-select
v-model="store.kelingParams.mode"
placeholder="请选择模式"
@change="store.updateModelPower"
>
<el-option label="标准模式" value="std" />
<el-option label="专业模式" value="pro" />
</el-select>
</el-form-item>
</div>
<!-- 创意程度 -->
<div class="param-line">
<el-form-item label="创意程度">
<el-slider v-model="store.kelingParams.cfg_scale" :min="0" :max="1" :step="0.1" />
</el-form-item>
</div>
<!-- 运镜控制 -->
<div class="param-line" v-if="store.showCameraControl">
<div class="param-line pt">
<span>运镜控制</span>
<el-tooltip content="生成画面的运镜效果,仅 1.5的高级模式可用" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</div>
<div class="param-line">
<el-select
v-model="store.kelingParams.camera_control.type"
placeholder="请选择运镜类型"
>
<el-option label="请选择" value="" />
<el-option label="简单运镜" value="simple" />
<el-option label="下移拉远" value="down_back" />
<el-option label="推进上移" value="forward_up" />
<el-option label="右旋推进" value="right_turn_forward" />
<el-option label="左旋推进" value="left_turn_forward" />
</el-select>
</div>
<!-- 仅在simple模式下显示详细配置 -->
<div
class="camera-control mt-2"
v-if="store.kelingParams.camera_control.type === 'simple'"
>
<el-form-item label="水平移动">
<el-slider
v-model="store.kelingParams.camera_control.config.horizontal"
:min="-10"
:max="10"
/>
</el-form-item>
<el-form-item label="垂直移动">
<el-slider
v-model="store.kelingParams.camera_control.config.vertical"
:min="-10"
:max="10"
/>
</el-form-item>
<el-form-item label="左右旋转">
<el-slider
v-model="store.kelingParams.camera_control.config.pan"
:min="-10"
:max="10"
/>
</el-form-item>
<el-form-item label="上下旋转">
<el-slider
v-model="store.kelingParams.camera_control.config.tilt"
:min="-10"
:max="10"
/>
</el-form-item>
<el-form-item label="横向翻转">
<el-slider
v-model="store.kelingParams.camera_control.config.roll"
:min="-10"
:max="10"
/>
</el-form-item>
<el-form-item label="镜头缩放">
<el-slider
v-model="store.kelingParams.camera_control.config.zoom"
:min="-10"
:max="10"
/>
</el-form-item>
</div>
</div>
</el-form>
<!-- 图片辅助生成开关 -->
<div class="param-line pt">
<div class="image-mode-toggle">
<span class="label">使用图片辅助生成</span>
<el-switch
v-model="store.kelingUseImageMode"
@change="store.toggleKelingImageMode"
size="small"
/>
</div>
</div>
<!-- 图片上传区域可折叠 -->
<div v-if="store.kelingUseImageMode" class="img-inline">
<div class="img-uploader video-img-box mr-2">
<el-icon
v-if="store.kelingParams.image"
@click="store.removeKelingImage('start')"
class="removeimg"
>
<CircleCloseFilled />
</el-icon>
<el-upload
class="uploader img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="store.uploadKelingStartImage"
accept=".jpg,.png,.jpeg"
>
<el-image
v-if="store.kelingParams.image"
:src="store.kelingParams.image"
fit="cover"
/>
<div class="flex flex-col" v-else>
<el-icon class="mb-1 text-base"><Plus /></el-icon>
<span>起始帧</span>
</div>
</el-upload>
</div>
<div
class="flex items-center h-[120px] cursor-pointer"
v-if="store.kelingParams.image && store.kelingParams.image_tail"
>
<el-tooltip content="交换图片" placement="top">
<i class="iconfont icon-exchange" @click="store.switchKelingImages"></i>
</el-tooltip>
</div>
<div class="img-uploader video-img-box ml-2">
<el-icon
v-if="store.kelingParams.image_tail"
@click="store.removeKelingImage('end')"
class="removeimg"
>
<CircleCloseFilled />
</el-icon>
<el-upload
class="uploader img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="store.uploadKelingEndImage"
accept=".jpg,.png,.jpeg"
>
<el-image
v-if="store.kelingParams.image_tail"
:src="store.kelingParams.image_tail"
fit="cover"
/>
<div class="flex flex-col" v-else>
<el-icon class="mb-1 text-base"><Plus /></el-icon>
<span>结束帧</span>
</div>
</el-upload>
</div>
</div>
<!-- 提示词输入 -->
<div class="param-line pt">
<span>提示词</span>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</div>
<div class="param-line pt">
<el-input
v-model="store.kelingParams.prompt"
type="textarea"
maxlength="500"
:autosize="{ minRows: 4, maxRows: 6 }"
:placeholder="
store.kelingUseImageMode
? '描述视频画面细节'
: '请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词'
"
/>
</div>
<!-- 提示词生成按钮 -->
<div class="param-line pt">
<el-button
class="generate-btn"
@click="store.generatePrompt"
:loading="store.isGenerating"
size="small"
color="#5865f2"
style="width: 100%"
>
<i class="iconfont icon-chuangzuo"></i>
生成专业视频提示词
</el-button>
</div>
<!-- 排除内容 -->
<div class="param-line pt">
<span>不希望出现的内容可选</span>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</div>
<div class="param-line pt">
<el-input
v-model="store.kelingParams.negative_prompt"
type="textarea"
:autosize="{ minRows: 4, maxRows: 6 }"
placeholder="请在此输入你不希望出现在视频上的内容"
/>
</div>
<!-- 算力显示 -->
<el-row class="text-info">
<el-text type="primary"
>当前可用算力<el-text type="warning">{{ store.availablePower }}</el-text></el-text
>
</el-row>
<!-- 生成按钮 -->
<div class="submit-btn">
<el-button
type="primary"
:dark="false"
@click="store.createKelingVideo"
round
:loading="store.generating"
>
立即生成 ({{ store.kelingPowerCost }}<i class="iconfont icon-vip2"></i>)
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 右侧任务列表 -->
<div
class="main-content"
v-loading="store.loading"
element-loading-background="rgba(100,100,100,0.3)"
>
<div class="works-header">
<h2 class="h-title text-2xl">你的作品</h2>
<div class="filter-buttons">
<el-button-group>
<el-button
:type="store.taskFilter === 'all' ? 'primary' : 'default'"
@click="store.switchTaskFilter('all')"
size="small"
>
全部
</el-button>
<el-button
:type="store.taskFilter === 'luma' ? 'primary' : 'default'"
@click="store.switchTaskFilter('luma')"
size="small"
>
Luma
</el-button>
<el-button
:type="store.taskFilter === 'keling' ? 'primary' : 'default'"
@click="store.switchTaskFilter('keling')"
size="small"
>
可灵
</el-button>
</el-button-group>
</div>
</div>
<div class="video-list">
<div class="list-box" v-if="!store.noData">
<div v-for="item in store.currentList" :key="item.id">
<div class="item">
<div class="left">
<div class="container">
<div v-if="item.progress === 100">
<video
class="video"
:src="store.replaceImg(item.video_url)"
preload="auto"
loop="loop"
muted="muted"
>
您的浏览器不支持视频播放
</video>
<button
class="play flex justify-center items-center"
@click="store.playVideo(item)"
>
<img src="/images/play.svg" alt="" />
</button>
</div>
<el-image
:src="item.cover_url"
class="border rounded-lg"
fit="cover"
v-else-if="item.progress === 101"
/>
<generating message="正在生成视频" v-else />
</div>
</div>
<div class="center">
<div class="pb-2" v-if="item.raw_data">
<el-tag class="mr-1">{{
item.raw_data.task_type || store.activeVideoType
}}</el-tag>
<el-tag class="mr-1" v-if="item.raw_data.model">{{ item.raw_data.model }}</el-tag>
<el-tag class="mr-1" v-if="item.raw_data.duration"
>{{ item.raw_data.duration }}</el-tag
>
<el-tag class="mr-1" v-if="item.raw_data.mode">{{ item.raw_data.mode }}</el-tag>
</div>
<div class="failed" v-if="item.progress === 101">
任务执行失败{{ item.err_msg }}任务提示词{{ item.prompt }}
</div>
<div class="prompt" v-else>
{{ store.substr(item.prompt, 1000) }}
</div>
</div>
<div class="right" v-if="item.progress === 100">
<div class="tools">
<el-tooltip content="复制提示词" placement="top">
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
<i class="iconfont icon-copy"></i>
</button>
</el-tooltip>
<el-tooltip content="下载视频" placement="top">
<button
class="btn btn-icon"
@click="store.downloadVideo(item)"
:disabled="item.downloading"
>
<i class="iconfont icon-download" v-if="!item.downloading"></i>
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
</button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<button class="btn btn-icon" @click="store.removeJob(item)">
<i class="iconfont icon-remove"></i>
</button>
</el-tooltip>
</div>
</div>
<div class="right-error" v-else>
<el-button type="danger" @click="store.removeJob(item)" circle>
<i class="iconfont icon-remove"></i>
</el-button>
</div>
</div>
</div>
</div>
<el-empty
:image-size="100"
:image="store.nodata"
description="没有任何作品,赶紧去创作吧!"
v-else
/>
<div class="pagination">
<el-pagination
v-if="store.total > store.pageSize"
background
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
layout="total,prev, pager, next"
:hide-on-single-page="true"
:current-page="store.page"
:page-size="store.pageSize"
@current-change="store.fetchData"
:total="store.total"
/>
</div>
</div>
</div>
<!-- 视频预览对话框 -->
<black-dialog
:show="store.showDialog"
title="预览视频"
hide-footer
@cancal="store.showDialog = false"
@update:show="store.showDialog = $event"
width="auto"
>
<video
style="max-width: 90vw; max-height: 90vh"
:src="store.currentVideoUrl"
preload="auto"
:autoplay="true"
loop="loop"
muted="muted"
v-show="store.showDialog"
>
您的浏览器不支持视频播放
</video>
</black-dialog>
</div>
</template>
<script setup>
import BlackDialog from '@/components/ui/BlackDialog.vue'
import Generating from '@/components/ui/Generating.vue'
import { useVideoStore } from '@/store/video'
import { CircleCloseFilled, InfoFilled, Plus } from '@element-plus/icons-vue'
import { onMounted, onUnmounted } from 'vue'
const store = useVideoStore()
onMounted(() => {
store.init()
})
onUnmounted(() => {
store.cleanup()
})
</script>
<style lang="stylus" scoped>
@import "../assets/css/video.styl"
</style>