mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-28 14:04:48 +08:00
视频页面整合完成
This commit is contained in:
@@ -1,745 +0,0 @@
|
||||
<template>
|
||||
<div class="page-keling">
|
||||
<div class="inner custom-scroll">
|
||||
<!-- 左侧参数设置面板 -->
|
||||
<el-scrollbar max-height="100vh">
|
||||
<div class="mj-box">
|
||||
<h2>视频参数设置</h2>
|
||||
<el-form :model="params" 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 rates" :key="item.value">
|
||||
<div
|
||||
class="flex-col items-center"
|
||||
:class="
|
||||
item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'
|
||||
"
|
||||
@click="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="params.model"
|
||||
placeholder="请选择模型"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in 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="params.duration"
|
||||
placeholder="请选择时长"
|
||||
@change="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="params.mode"
|
||||
placeholder="请选择模式"
|
||||
@change="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="params.cfg_scale" :min="0" :max="1" :step="0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 运镜控制 -->
|
||||
<div class="param-line" v-if="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="params.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="params.camera_control.type === 'simple'">
|
||||
<el-form-item label="水平移动">
|
||||
<el-slider
|
||||
v-model="params.camera_control.config.horizontal"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="垂直移动">
|
||||
<el-slider v-model="params.camera_control.config.vertical" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="左右旋转">
|
||||
<el-slider v-model="params.camera_control.config.pan" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上下旋转">
|
||||
<el-slider v-model="params.camera_control.config.tilt" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="横向翻转">
|
||||
<el-slider v-model="params.camera_control.config.roll" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="镜头缩放">
|
||||
<el-slider v-model="params.camera_control.config.zoom" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<div class="main-content task-list-inner">
|
||||
<!-- 任务类型选择 -->
|
||||
<div class="param-line">
|
||||
<el-tabs v-model="params.task_type" @tab-change="tabChange" class="title-tabs">
|
||||
<el-tab-pane label="文生视频" name="text2video">
|
||||
<div class="text">使用文字描述想要生成视频的内容</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="图生视频" name="image2video">
|
||||
<div class="text">
|
||||
以某张图片为底稿参考来创作视频,生成类似风格或类型视频,支持 PNG /JPG/JPEG
|
||||
格式图片;
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 生成操作区 -->
|
||||
<div class="generation-area">
|
||||
<div v-if="params.task_type === 'text2video'" class="text2video">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
type="textarea"
|
||||
maxlength="500"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词"
|
||||
/>
|
||||
<el-row class="text-info">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
@click="generatePrompt"
|
||||
:loading="isGenerating"
|
||||
size="small"
|
||||
color="#5865f2"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo"></i>
|
||||
生成专业视频提示词
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div v-else class="image2video">
|
||||
<div class="image-upload img-inline">
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
|
||||
<h4>起始帧</h4>
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadStartImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<img v-if="params.image" :src="params.image" class="preview" />
|
||||
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="btn-swap" v-if="params.image && params.image_tail">
|
||||
<i class="iconfont icon-exchange" @click="switchReverse"></i>
|
||||
</div>
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
<h4>结束帧</h4>
|
||||
<el-upload
|
||||
class="uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadEndImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<img v-if="params.image_tail" :src="params.image_tail" class="preview" />
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>提示词:</span>
|
||||
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="描述视频画面细节"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排除内容 -->
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>不希望出现的内容:(可选)</span>
|
||||
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="params.negative_prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入你不希望出现在视频上的内容"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<el-row class="text-info">
|
||||
<el-text type="primary"
|
||||
>每次生成视频消耗 <el-text type="warning">{{ powerCost }}算力;</el-text> </el-text
|
||||
>
|
||||
<el-text type="primary"
|
||||
>当前可用算力:<el-text type="warning">{{ availablePower }}</el-text></el-text
|
||||
>
|
||||
</el-row>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="submit-btn">
|
||||
<el-button type="primary" :dark="false" @click="generate" round>立即生成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表区域 -->
|
||||
<div class="video-list">
|
||||
<h2 class="text-xl p-3">你的作品</h2>
|
||||
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list" :key="item.id">
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video
|
||||
class="video"
|
||||
:src="item.video_url"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button
|
||||
class="play flex justify-center items-center"
|
||||
@click="previewVideo(item)"
|
||||
>
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image v-else-if="item.progress === 101" :src="item.cover_url" fit="cover" />
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="pb-2">
|
||||
<el-tag class="mr-1">{{ item.raw_data.task_type }}</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.model }}</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.duration }}秒</el-tag>
|
||||
<el-tag class="mr-1">{{ 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>
|
||||
{{ 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>
|
||||
|
||||
<!-- <button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button> -->
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button
|
||||
class="btn btn-icon"
|
||||
@click="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="removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-error" v-else>
|
||||
<el-button type="danger" @click="removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
background
|
||||
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<black-dialog
|
||||
v-model:show="previewVisible"
|
||||
title="视频预览"
|
||||
hide-footer
|
||||
@cancal="previewVisible = false"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
:src="currentVideo"
|
||||
controls
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
preload="auto"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr } from '@/utils/libs'
|
||||
import { CircleCloseFilled, InfoFilled, Plus } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
|
||||
const models = ref([
|
||||
{
|
||||
text: '可灵 1.6',
|
||||
value: 'kling-v1-6',
|
||||
},
|
||||
{
|
||||
text: '可灵 1.5',
|
||||
value: 'kling-v1-5',
|
||||
},
|
||||
{
|
||||
text: '可灵 1.0',
|
||||
value: 'kling-v1',
|
||||
},
|
||||
])
|
||||
// 参数设置
|
||||
const params = reactive({
|
||||
task_type: 'text2video',
|
||||
model: models.value[0].value,
|
||||
prompt: '',
|
||||
negative_prompt: '',
|
||||
cfg_scale: 0.7,
|
||||
mode: 'std',
|
||||
aspect_ratio: '16:9',
|
||||
duration: '5',
|
||||
camera_control: {
|
||||
type: '',
|
||||
config: {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
pan: 0,
|
||||
tilt: 0,
|
||||
roll: 0,
|
||||
zoom: 0,
|
||||
},
|
||||
},
|
||||
image: '',
|
||||
image_tail: '',
|
||||
})
|
||||
const rates = [
|
||||
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.png' },
|
||||
|
||||
{
|
||||
css: 'size16-9',
|
||||
value: '16:9',
|
||||
text: '16:9',
|
||||
img: '/images/mj/rate_16_9.png',
|
||||
},
|
||||
{
|
||||
css: 'size9-16',
|
||||
value: '9:16',
|
||||
text: '9:16',
|
||||
img: '/images/mj/rate_9_16.png',
|
||||
},
|
||||
]
|
||||
|
||||
// 切换图片比例
|
||||
const changeRate = (item) => {
|
||||
params.aspect_ratio = item.value
|
||||
}
|
||||
|
||||
const generating = ref(false)
|
||||
const isGenerating = ref(false)
|
||||
const powerCost = ref(10)
|
||||
const availablePower = ref(100)
|
||||
const taskFilter = ref('all')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const pullHandler = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
const currentVideo = ref('')
|
||||
const showCameraControl = ref(false)
|
||||
const keLingPowers = ref({})
|
||||
const isLogin = ref(false)
|
||||
// 动态更新模型消耗的算力
|
||||
const updateModelPower = () => {
|
||||
showCameraControl.value = params.model === 'kling-v1-5' && params.mode === 'pro'
|
||||
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {}
|
||||
}
|
||||
|
||||
// tab切换
|
||||
const tabChange = (tab) => {
|
||||
params.task_type = tab
|
||||
}
|
||||
|
||||
const uploadStartImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
try {
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
params.image = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
closeLoading()
|
||||
} catch (e) {
|
||||
showMessageError('上传失败: ' + e.message)
|
||||
closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
//移除图片
|
||||
const removeImage = (type) => {
|
||||
if (type === 'start') {
|
||||
params.image = ''
|
||||
} else if (type === 'end') {
|
||||
params.image_tail = ''
|
||||
}
|
||||
}
|
||||
|
||||
//图片交换方法
|
||||
const switchReverse = () => {
|
||||
;[params.image, params.image_tail] = [params.image_tail, params.image]
|
||||
}
|
||||
const uploadEndImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
try {
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
params.image_tail = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch (e) {
|
||||
showMessageError('上传失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const generatePrompt = async () => {
|
||||
if (isGenerating.value) return
|
||||
if (!params.prompt) {
|
||||
return showMessageError('请输入视频描述')
|
||||
}
|
||||
isGenerating.value = true
|
||||
try {
|
||||
const res = await httpPost('/api/prompt/video', { prompt: params.prompt })
|
||||
params.prompt = res.data
|
||||
} catch (e) {
|
||||
showMessageError('生成失败: ' + e.message)
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const generate = async () => {
|
||||
//增加防抖
|
||||
if (generating.value) return
|
||||
if (!params.prompt?.trim()) {
|
||||
return ElMessage.error('请输入视频描述')
|
||||
}
|
||||
// 提示词长度不能超过 500
|
||||
if (params.prompt.length > 500) {
|
||||
return ElMessage.error('视频描述不能超过 500 个字符')
|
||||
}
|
||||
if (params.task_type === 'image2video' && !params.image) {
|
||||
return ElMessage.error('请上传起始帧图片')
|
||||
}
|
||||
generating.value = true
|
||||
// 处理图片链接
|
||||
if (params.image) {
|
||||
params.image = replaceImg(params.image)
|
||||
}
|
||||
if (params.image_tail) {
|
||||
params.image_tail = replaceImg(params.image_tail)
|
||||
}
|
||||
try {
|
||||
await httpPost('/api/video/keling/create', params)
|
||||
showMessageOK('任务创建成功')
|
||||
// 新增重置
|
||||
page.value = 1
|
||||
list.value.unshift({
|
||||
progress: 0,
|
||||
prompt: params.prompt,
|
||||
raw_data: {
|
||||
task_type: params.task_type,
|
||||
model: params.model,
|
||||
duration: params.duration,
|
||||
mode: params.mode,
|
||||
},
|
||||
})
|
||||
taskPulling.value = true
|
||||
} catch (e) {
|
||||
showMessageError('创建失败: ' + e.message)
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page
|
||||
}
|
||||
|
||||
httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: 'keling',
|
||||
task_type: taskFilter.value === 'all' ? '' : taskFilter.value,
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true
|
||||
}
|
||||
items.push({
|
||||
...v,
|
||||
downloading: false,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const previewVideo = (task) => {
|
||||
currentVideo.value = task.video_url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
const downloadVideo = async (task) => {
|
||||
try {
|
||||
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`)
|
||||
const blob = new Blob([res.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = `video_${task.id}.mp4`
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (e) {
|
||||
showMessageError('下载失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet('/api/video/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData(page.value)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((u) => {
|
||||
isLogin.value = true
|
||||
availablePower.value = u.power
|
||||
fetchData(1)
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(page.value)
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
|
||||
getSystemInfo().then((res) => {
|
||||
keLingPowers.value = res.data.keling_powers
|
||||
updateModelPower()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '../assets/css/keling.styl'
|
||||
</style>
|
||||
@@ -1,387 +0,0 @@
|
||||
<template>
|
||||
<div class="page-luma">
|
||||
<div class="prompt-box">
|
||||
<div class="images">
|
||||
<template v-for="(img, index) in images" :key="img">
|
||||
<div class="item">
|
||||
<el-image :src="replaceImg(img)" fit="cover" />
|
||||
<el-icon @click="remove(img)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
<div class="btn-swap" v-if="images.length === 2 && index === 0">
|
||||
<i class="iconfont icon-exchange" @click="switchReverse"></i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="prompt-container">
|
||||
<div class="input-container">
|
||||
<div class="upload-icon" v-if="images.length < 2">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="upload"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<i class="iconfont icon-image"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
<textarea
|
||||
class="prompt-input"
|
||||
:rows="row"
|
||||
v-model="formData.prompt"
|
||||
maxlength="2000"
|
||||
placeholder="请输入提示词或者上传图片"
|
||||
autofocus
|
||||
>
|
||||
</textarea>
|
||||
<div class="send-icon" @click="create">
|
||||
<i class="iconfont icon-send"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="params">
|
||||
<div class="item-group">
|
||||
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2">
|
||||
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
|
||||
<span>生成AI视频提示词</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="item-group">
|
||||
<span class="label">循环参考图</span>
|
||||
<el-switch v-model="formData.loop" size="small" />
|
||||
</div>
|
||||
<div class="item-group">
|
||||
<span class="label">提示词优化</span>
|
||||
<el-switch v-model="formData.expand_prompt" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-container
|
||||
class="video-container"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(100,100,100,0.3)"
|
||||
>
|
||||
<h2 class="h-title text-2xl mb-5 mt-2">你的作品</h2>
|
||||
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list" :key="item.id">
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video
|
||||
class="video"
|
||||
:src="replaceImg(item.video_url)"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button class="play flex justify-center items-center" @click="play(item)">
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress === 101" />
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>{{ item.prompt }}</div>
|
||||
</div>
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
<div class="tools">
|
||||
<!-- <button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button> -->
|
||||
|
||||
<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="download(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="removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-error" v-else>
|
||||
<el-button type="danger" @click="removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
background
|
||||
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData(page)"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</el-container>
|
||||
<black-dialog
|
||||
v-model:show="showDialog"
|
||||
title="预览视频"
|
||||
hide-footer
|
||||
@cancal="showDialog = false"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:src="currentVideoUrl"
|
||||
preload="auto"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
v-show="showDialog"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
import { CircleCloseFilled } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
const row = ref(1)
|
||||
const images = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
prompt: '',
|
||||
expand_prompt: false,
|
||||
loop: false,
|
||||
first_frame_img: '',
|
||||
end_frame_img: '',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const clipboard = ref(null)
|
||||
const pullHandler = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
fetchData(1)
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(1)
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
})
|
||||
|
||||
const download = (item) => {
|
||||
const url = replaceImg(item.video_url)
|
||||
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
item.downloading = true
|
||||
httpDownload(downloadURL)
|
||||
.then((response) => {
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
})
|
||||
.catch(() => {
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
})
|
||||
}
|
||||
|
||||
const play = (item) => {
|
||||
currentVideoUrl.value = replaceImg(item.video_url)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet('/api/video/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const publishJob = (item) => {
|
||||
httpGet('/api/video/publish', { id: item.id, publish: item.publish })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const upload = (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file, file.name)
|
||||
showLoading('正在上传文件...')
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
images.value.push(res.data.url)
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const remove = (img) => {
|
||||
images.value = images.value.filter((item) => item !== img)
|
||||
}
|
||||
|
||||
const switchReverse = () => {
|
||||
images.value = images.value.reverse()
|
||||
}
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page
|
||||
}
|
||||
httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: 'luma',
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true
|
||||
}
|
||||
items.push(v)
|
||||
}
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const create = () => {
|
||||
const len = images.value.length
|
||||
if (len) {
|
||||
formData.first_frame_img = replaceImg(images.value[0])
|
||||
if (len === 2) {
|
||||
formData.end_frame_img = replaceImg(images.value[1])
|
||||
}
|
||||
}
|
||||
|
||||
httpPost('/api/video/luma/create', formData)
|
||||
.then(() => {
|
||||
fetchData(1)
|
||||
taskPulling.value = true
|
||||
showMessageOK('创建任务成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('创建任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const generatePrompt = () => {
|
||||
if (formData.prompt === '') {
|
||||
return showMessageError('请输入原始提示词')
|
||||
}
|
||||
showLoading('正在生成视频脚本...')
|
||||
httpPost('/api/prompt/video', { prompt: formData.prompt })
|
||||
.then((res) => {
|
||||
formData.prompt = res.data
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('生成提示词失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/luma.styl"
|
||||
</style>
|
||||
645
web/src/views/Video.vue
Normal file
645
web/src/views/Video.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<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">
|
||||
<span class="label">循环参考图</span>
|
||||
<el-switch v-model="store.lumaParams.loop" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<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>
|
||||
Reference in New Issue
Block a user