Files
geekai/web/src/views/Luma.vue
2024-09-09 18:07:10 +08:00

343 lines
11 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-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"
placeholder="请输入提示词或者上传图片"
autofocus>
</textarea>
<div class="send-icon" @click="create">
<i class="iconfont icon-send"></i>
</div>
</div>
<div class="params">
<div class="item-group">
<span class="label">循环参考图</span>
<el-switch v-model="formData.loop" size="small" style="--el-switch-on-color:#BF78BF;" />
</div>
<div class="item-group">
<span class="label">提示词优化</span>
<el-switch v-model="formData.expand_prompt" size="small" style="--el-switch-on-color:#BF78BF;" />
</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">你的作品</h2>
<!-- <el-row :gutter="20" class="videos" v-if="!noData">-->
<!-- <el-col :span="8" class="item" :key="item.id" v-for="item in videos">-->
<!-- <div class="video-box" @mouseover="item.playing = true" @mouseout="item.playing = false">-->
<!-- <img :src="item.cover" :alt="item.name" v-show="!item.playing"/>-->
<!-- <video :src="item.url" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="item.playing">-->
<!-- 您的浏览器不支持视频播放-->
<!-- </video>-->
<!-- </div>-->
<!-- <div class="video-name">{{item.name}}</div>-->
<!-- <div class="opts">-->
<!-- <button class="btn" @click="download(item)" :disabled="item.downloading">-->
<!-- <i class="iconfont icon-download" v-if="!item.downloading"></i>-->
<!-- <el-image src="/images/loading.gif" fit="cover" v-else />-->
<!-- <span>下载</span>-->
<!-- </button>-->
<!-- </div>-->
<!-- </el-col>-->
<!-- </el-row>-->
<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" @click="play(item)">
<img src="/images/play.svg" alt=""/>
</button>
</div>
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" />
<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 effect="light" 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" fit="cover" v-else />
</button>
</el-tooltip>
<el-tooltip effect="light" 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" description="没有任何作品,赶紧去创作吧!" v-else />
<div class="pagination">
<el-pagination v-if="total > pageSize" background
style="--el-pagination-button-bg-color:#414141;
--el-pagination-button-color:#d1d1d1;
--el-disabled-bg-color:#414141;
--el-color-primary:#666666;
--el-pagination-hover-color:#e1e1e1"
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="1000">
<video style="width: 100%;" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
您的浏览器不支持视频播放
</video>
</black-dialog>
</div>
</template>
<script setup>
import {onMounted, reactive, ref} from "vue";
import {CircleCloseFilled} from "@element-plus/icons-vue";
import {httpDownload, httpPost, httpGet} from "@/utils/http";
import {checkSession} from "@/store/cache";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import { replaceImg } from "@/utils/libs"
import {ElMessage, ElMessageBox} from "element-plus";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import Generating from "@/components/ui/Generating.vue";
import BlackDialog from "@/components/ui/BlackDialog.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 socket = ref(null)
const userId = ref(0)
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
if (location.protocol === 'https:') {
host = 'wss://' + location.host;
} else {
host = 'ws://' + location.host;
}
}
const _socket = new WebSocket(host + `/api/video/client?user_id=${userId.value}`);
_socket.addEventListener('open', () => {
socket.value = _socket;
});
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8")
reader.onload = () => {
const message = String(reader.result)
if (message === "FINISH" || message === "FAIL") {
fetchData()
}
}
}
});
_socket.addEventListener('close', () => {
if (socket.value !== null) {
connect()
}
});
}
onMounted(()=>{
checkSession().then(user => {
userId.value = user.id
connect()
})
fetchData(1)
})
const download = (item) => {
const url = replaceImg(item.video_url)
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`
// parse filename
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);
// 执行上传操作
httpPost('/api/upload', formData).then((res) => {
images.value.push(res.data.url)
ElMessage.success({message: "上传成功", duration: 500})
}).catch((e) => {
ElMessage.error('图片上传失败:' + e.message)
})
};
const remove = (img) => {
images.value = images.value.filter(item => item !== img)
}
const switchReverse = () => {
images.value = images.value.reverse()
}
const loading = ref(false)
const list = ref([])
const noData = ref(true)
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
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
loading.value = false
list.value = res.data.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 = images.value[0]
if(len === 2){
formData.end_frame_img = images.value[1]
}
}
httpPost("/api/video/luma/create", formData).then(() => {
fetchData(1)
showMessageOK("创建任务成功")
}).catch(e => {
showMessageError("创建任务失败:"+e.message)
})
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/luma.styl"
</style>