mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-20 18:14:28 +08:00
646 lines
24 KiB
Vue
646 lines
24 KiB
Vue
<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>
|