merge v4.2.1

This commit is contained in:
RockYang
2025-11-26 20:09:48 +08:00
74 changed files with 3834 additions and 1210 deletions

View File

@@ -0,0 +1,363 @@
.page-keling
display flex
min-height 100vh
:deep(.el-form-item__label)
color var(--text-theme-color)
.grid-content
// background-color #383838
background var(--card-bg)
border-radius 8px
padding 8px 14px
display flex
cursor pointer
margin-bottom 10px
// border 1px solid #383838
border 1px solid var(--chat-bg)
&:hover
border 1px solid var(--theme-border-hover)
.icon
width 20px
height 20px
margin-bottom 5px
.texts
margin-left 5px
margin-top 2px
color var(--text-theme-color)
.param-line.pt
padding-top 5px
padding-bottom 5px
.grid-content.active
// color #47fff1
// background-color #585858
border 1px solid var(--theme-border-hover)
.h-20
height 4rem !important
.main-content
padding-right 1.5rem
padding-left 1.5rem
padding-bottom 1rem
flex 1
background var(--chat-bg)
// width: 100%;
// padding 0 10px 10px 10px
color var(--text-theme-color)
overflow-x hidden
.camera-control
padding 10px
border-radius 4px
background var(--card-bg)
:deep(.el-form-item:last-child)
margin-bottom 0 !important
.title-tabs
:deep(.el-tabs__item.is-active)
color var(--theme-textcolor-normal)
font-size 18px
:deep(.el-tabs__item)
color var(--text-theme-color)
font-size 18px
.el-tabs
--el-tabs-header-height 55px
.el-tabs__item
color var(--text-theme-color)
font-size 18px
.el-tabs__item.is-active, .title-tabs .el-tabs__item.is-active
.title-tabs .el-tabs__active-bar
background-color var(--theme-textcolor-normal)
:deep(.el-textarea)
--el-input-focus-border-color var(--el-color-primary)
:deep(.el-textarea__inner)
background transparent
color var(--text-theme-color)
.el-input__wrapper
background transparent
padding 5px
.text
margin-bottom 10px
color #6b778c
font-size 15px
.param-line.pt
padding-top 5px
padding-bottom 5px
.form-item-inner
display flex
align-items center
.el-icon
margin-left 10px
.el-form-item__label
color var(--text-theme-color)
//
.img-inline
display flex
gap 20px
align-items center
.img-uploader
text-align center
:deep(.el-upload)
border 1px dashed var(--el-border-color)
border-radius 6px
cursor pointer
position relative
overflow hidden
width 120px
height 120px
line-height 120px
transition var(--el-transition-duration-fast)
margin-bottom 20px
&:hover
border-color var(--el-color-primary)
.el-icon.uploader-icon
font-size 28px
color #8c939d
width 100%
height 120px
text-align center
.img-list-box
display flex
.img-item
width 120px
position relative
margin-right 10px
.el-image
width 120px
height 120px
border-radius 5px
.el-button
position absolute
right 5px
top 5px
width 20px
height 20px
.el-row.text-info
width 100%
padding 10px 0
.el-tag
margin-right 10px
//
.submit-btn
display flex
margin 20px 0
.el-button
width 200px
.video-list
.btn
margin-right 10px
border none
border-radius 5px
padding 5px 10px
cursor pointer
color var(--theme-text-color-primary)
background-color var(--btn-bg)
&:hover
opacity 0.7
.list-box
padding 0
.item
display flex
flex-flow row
align-items center
min-height 100px
padding 10px 15px
border-radius 10px
cursor pointer
margin-bottom 20px
background var(--chat-bg)
.left
.container
width 160px
position relative
max-height 120px
overflow hidden
display flex
justify-content center
align-items center
.video
width 160px
border-radius 5px
.el-image
width 160px
height 90px
border-radius 5px
.duration
position absolute
bottom 0
right 0
background-color rgba(14, 8, 8, 0.7)
padding 0 3px
font-family 'Input Sans'
font-size 14px
font-weight 700
border-radius 0.125rem
.play
position absolute
width 100%
height 100%
top 0
left 50%
border none
border-radius 5px
background rgba(100, 100, 100, 0.3)
cursor pointer
color var(--text-theme-color)
opacity 0
transform translate(-50%, 0px)
transition opacity 0.3s ease 0s
&:hover
.play
opacity 1
// display block
.center
width 100%
// border 1px solid saddlebrown
display flex
justify-content center
align-items flex-start
flex-flow column
padding 0 20px
.prompt, .failed
padding 0
font-size 16px
max-height 60px
line-height 28px
overflow hidden
text-overflow ellipsis
.prompt
color var(--text-fb)
cursor text
.failed
color #E4696B
.right
display flex
justify-content right
min-width 200px
font-size 14px
padding 0
.tools
display flex
justify-content left
align-items center
flex-flow row
height 90px
.btn-publish
padding 2px 10px
.text
margin-right 10px
.btn-icon
background none
padding 6px
transition background 0.6s ease 0s
color #919191
&:hover
// background #5f5958
// color #e1e1e1
color var(--el-color-primary)
.downloading
width 16px
.pagination
margin-top 20px
display flex
justify-content center
.inner
display flex
width 100%
.mj-box
margin 10px
// background-color #262626
// border 1px solid #454545
// height: calc(100vh - 50px)
// overflow: scroll
min-width 300px
max-width 300px
padding 20px
border-radius 10px
color var(--text-theme-color)
font-size 14px
overflow auto
h2
font-weight bold
font-size 20px
text-align center
color var(--theme-textcolor-normal)
//
::-webkit-scrollbar
width 0
height 0
background-color transparent
.mj-params
margin-top 10px
overflow auto
.param-line
padding 0 10px
.el-icon
position relative
.model
background var(--card-bg)
// border 1px solid #454545
border-radius 8px
padding 5px
margin-bottom 10px
display flex
flex-flow column
align-items center
cursor pointer
border 1px solid var(--chat-bg)
&:hover
border 1px solid var(--theme-border-hover)
.el-image
height 40px
width 100%
.text
margin-top 4px
font-size 12px
.model.active
// color #47fff1
// background-color #585858
border 1px solid var(--theme-border-hover)
.form-item-inner
display flex
align-items center
.el-select
--el-select-input-focus-border-color var(--el-color-primary)
--el-input-focus-border-color var(--el-color-primary)
.el-input__wrapper
background var(--chat-bg)
.el-input__inner
color var(--text-theme-color)
.el-icon
margin-left 10px
.img-uploader
.el-upload
border 1px dashed var(--el-border-color)
border-radius 6px
cursor pointer
position relative
overflow hidden
width 100%
transition var(--el-transition-duration-fast)
&:hover
border-color var(--el-color-primary)
.el-icon.uploader-icon
font-size 28px
color #8c939d
width 100%
height 120px
text-align center
.param-line.pt
display flex
align-items center
padding-top 5px
padding-bottom 5px
.el-form
.el-form-item__label
color var(--text-theme-color)
.el-input, .el-slider
width 180px
.uploader-icon
font-size 24px
position relative
top 3px
.no-more-data
text-align center
padding 30px
.generate-btn
.iconfont
margin-right 5px

View File

@@ -2,6 +2,7 @@
display: flex;
justify-content: center;
background-color: #0E0808;
height: 100vh;
.inner {
text-align left
@@ -62,7 +63,6 @@
.prompt {
width 100%
height 500px
background-color transparent
white-space pre-wrap
overflow-y auto

View File

@@ -90,7 +90,7 @@
.song {
display flex
padding 10px
background-color #252020
background-color var(--el-bg-color)
border-radius 10px
margin-bottom 10px
font-size 14px
@@ -109,6 +109,7 @@
display flex
margin-left 10px
align-items center
color var(--el-color-primary)
}
.el-button--info {
@@ -281,8 +282,8 @@
.model {
color #8f8f8f
// background-color #1C1616
// border 1px solid #8f8f8f
background-color var(--el-bg-color)
border 1px solid var(--el-border-color-light)
font-weight normal
font-size 12px
padding 1px 3px
@@ -347,7 +348,9 @@
.task {
height 100px
background-color #2A2525
background-color var(--el-bg-color)
border: 1px solid var(--el-border-color-light);
border-radius 5px
display flex
margin-bottom 10px
.left {
@@ -358,7 +361,7 @@
width 320px
.title {
font-size 14px
color #e1e1e1
color var(--el-text-color-primary)
white-space: nowrap; /* */
overflow: hidden; /* */
text-overflow: ellipsis; /* */

View File

@@ -45,6 +45,10 @@
--chat-content-bg:rgba(86, 86, 95, .2);
--chat-user-content-bg: #762AA4;
--hover-deep-color:#30323c;
--tab-title-bg:#525777;//tab
--tab-title-color:#fff;//tab
//
--bg-deep-color:rgba(255,255,255,0.8);
//layout
.more-menus li.moreTitle,
.twoTittle .title,

View File

@@ -39,6 +39,10 @@
--el-bg-color:#fff;
--el-fill-color-blank: #fff;
--el-pagination-button-bg-color: rgba(86,86,95,0.2);
--tab-title-bg:#fff;//tab
--tab-title-color:#595959;//tab
//
--btn-bg: rgba(100, 100, 100, .1);

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1736144380052') format('woff2'),
url('iconfont.woff?t=1736144380052') format('woff'),
url('iconfont.ttf?t=1736144380052') format('truetype');
src: url('iconfont.woff2?t=1740279975534') format('woff2'),
url('iconfont.woff?t=1740279975534') format('woff'),
url('iconfont.ttf?t=1740279975534') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-keling:before {
content: "\eab7";
}
.icon-gitee:before {
content: "\e6d0";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "42692844",
"name": "可灵大模型",
"font_class": "keling",
"unicode": "eab7",
"unicode_decimal": 60087
},
{
"icon_id": "6905420",
"name": "码云",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -2,25 +2,22 @@
<div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="User"/>
<img :src="data.icon" alt="User" />
</div>
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/>
<el-image :src="file.url" fit="cover" />
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover"/>
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{
file.name
}}
</el-link>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ file.name }} </el-link>
</div>
<div class="info">
<span>{{ GetFileType(file.ext) }}</span>
@@ -33,7 +30,7 @@
<div class="content" v-html="content"></div>
<div class="bar" v-if="data.created_at > 0">
<span class="bar-item"
><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ finalTokens }}</span>
</div>
@@ -44,25 +41,22 @@
<div class="chat-line chat-line-prompt-chat" v-else>
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="User"/>
<img :src="data.icon" alt="User" />
</div>
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/>
<el-image :src="file.url" fit="cover" />
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover"/>
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{
file.name
}}
</el-link>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ file.name }} </el-link>
</div>
<div class="info">
<span>{{ GetFileType(file.ext) }}</span>
@@ -77,7 +71,7 @@
</div>
<div class="bar" v-if="data.created_at > 0">
<span class="bar-item"
><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
</div>
@@ -87,12 +81,12 @@
</template>
<script setup>
import {onMounted, ref} from "vue";
import {Clock} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import { onMounted, ref } from "vue";
import { Clock } from "@element-plus/icons-vue";
import { httpPost } from "@/utils/http";
import hl from "highlight.js";
import {dateFormat, isImage, processPrompt} from "@/utils/libs";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
import { dateFormat, isImage, processPrompt } from "@/utils/libs";
import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
@@ -107,8 +101,8 @@ const md = new MarkdownIt({
// 显示复制代码按钮
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`;
@@ -157,19 +151,27 @@ const processFiles = () => {
const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex);
const urlPrefix = `${window.location.protocol}//${window.location.host}`;
if (links) {
httpPost("/api/upload/list", {urls: links})
.then((res) => {
files.value = res.data.items;
const _links = links.map((link) => {
if (link.startsWith(urlPrefix)) {
return link.replace(urlPrefix, "");
}
return link;
});
// 合并数组并去重
const urls = [...new Set([...links, ..._links])];
httpPost("/api/upload/list", { urls: urls })
.then((res) => {
files.value = res.data.items;
for (let link of links) {
if (isExternalImg(link, files.value)) {
files.value.push({url: link, ext: ".png"});
}
for (let link of links) {
if (isExternalImg(link, files.value)) {
files.value.push({ url: link, ext: ".png" });
}
})
.catch(() => {
});
}
})
.catch(() => {});
for (let link of links) {
content.value = content.value.replace(link, "");

View File

@@ -173,6 +173,11 @@ const reGenerate = (prompt) => {
font-family: var(--font-family);
.chat-line {
.boxed {
border: 1px solid var(--el-border-color);
border-radius: 5px;
padding: 0 5px;
}
.chat-item {
.content-wrapper {
img {

View File

@@ -53,6 +53,7 @@ import {isImage, removeArrayItem} from "@/utils/libs";
import {GetFileIcon} from "@/store/system";
import {checkSession} from "@/store/cache";
import {useSharedStore} from "@/store/sharedata";
import {closeLoading, showLoading} from "@/utils/dialog";
const props = defineProps({
userId: Number,
@@ -111,14 +112,17 @@ const onScroll = (options) => {
const afterRead = (file) => {
const formData = new FormData();
formData.append("file", file.file, file.name);
showLoading("文件上传中...");
// 执行上传操作
httpPost("/api/upload", formData)
.then((res) => {
fileData.items.unshift(res.data);
ElMessage.success({ message: "上传成功", duration: 500 });
closeLoading()
})
.catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
closeLoading()
});
};

View File

@@ -4,7 +4,7 @@
<div>
<span>{{ copyRight }}</span>
</div>
<div v-if="!license.de_copy">
<div v-if="!license?.de_copy">
<a :href="gitURL" target="_blank">
{{ title }} -
{{ version }}
@@ -30,15 +30,19 @@ const license = ref({});
const props = defineProps({
textColor: {
type: String,
default: "#ffffff",
},
default: "#ffffff"
}
});
// 获取系统配置
getSystemInfo()
.then((res) => {
title.value = res.data.title ?? process.env.VUE_APP_TITLE;
copyRight.value = (res.data.copyright ? res.data.copyright : "极客学长") + " © 2023 - " + new Date().getFullYear() + " All rights reserved";
copyRight.value =
(res.data.copyright ? res.data.copyright : "极客学长") +
" © 2023 - " +
new Date().getFullYear() +
" All rights reserved";
icp.value = res.data.icp;
})
.catch((e) => {

View File

@@ -63,7 +63,11 @@ const doSendMsg = (data) => {
x: data.x,
})
.then(() => {
ElMessage.success("验证码发送成功");
if (props.type === "mobile") {
ElMessage.success("验证码发送成功");
} else if (props.type === "email") {
ElMessage.success("验证码已发送至邮箱,如果长时间未收到,请检查是否在垃圾邮件中!");
}
let time = 60;
btnText.value = time;
const handler = setInterval(() => {

View File

@@ -109,6 +109,12 @@ const routes = [
meta: { title: "Luma视频创作" },
component: () => import("@/views/Luma.vue"),
},
{
name: "keling",
path: "/keling",
meta: { title: "KeLing视频创作" },
component: () => import("@/views/KeLing.vue"),
},
],
},
{

View File

@@ -1,84 +1,99 @@
import {httpGet} from "@/utils/http";
import { httpGet } from "@/utils/http";
import Storage from "good-storage";
import {randString} from "@/utils/libs";
import { randString } from "@/utils/libs";
const userDataKey = "USER_INFO_CACHE_KEY"
const adminDataKey = "ADMIN_INFO_CACHE_KEY"
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY"
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY"
const userDataKey = "USER_INFO_CACHE_KEY";
const adminDataKey = "ADMIN_INFO_CACHE_KEY";
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY";
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY";
export function checkSession() {
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
resolve(res.data)
}).catch(e => {
Storage.remove(userDataKey)
reject(e)
})
})
const item = Storage.get(userDataKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet("/api/user/session")
.then((res) => {
item.data = res.data;
item.expire = Date.now() + 1000 * 3;
Storage.set(userDataKey, item);
resolve(item.data);
})
.catch((e) => {
Storage.remove(userDataKey);
reject(e);
});
});
}
export function checkAdminSession() {
const item = Storage.get(adminDataKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/admin/session').then(res => {
item.data = res.data
item.expire = Date.now() + 1000 * 30
Storage.set(adminDataKey, item)
resolve(item.data)
}).catch(e => {
Storage.remove(adminDataKey)
reject(e)
})
})
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet("/api/admin/session")
.then((res) => {
item.data = res.data;
item.expire = Date.now() + 1000 * 30;
Storage.set(adminDataKey, item);
resolve(item.data);
})
.catch((e) => {
Storage.remove(adminDataKey);
reject(e);
});
});
}
export function removeAdminInfo() {
Storage.remove(adminDataKey)
Storage.remove(adminDataKey);
}
export function getSystemInfo() {
const item = Storage.get(systemInfoKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/config/get?key=system').then(res => {
item.data = res
item.expire = Date.now() + 1000 * 30
Storage.set(systemInfoKey, item)
resolve(item.data)
}).catch(err => {
reject(err)
})
})
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet("/api/config/get?key=system")
.then((res) => {
item.data = res;
item.expire = Date.now() + 1000 * 30;
Storage.set(systemInfoKey, item);
resolve(item.data);
})
.catch((err) => {
reject(err);
});
});
}
export function getLicenseInfo() {
const item = Storage.get(licenseInfoKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
const item = Storage.get(licenseInfoKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet('/api/config/license').then(res => {
item.data = res
item.expire = Date.now() + 1000 * 30
Storage.set(licenseInfoKey, item)
resolve(item.data)
}).catch(err => {
resolve(err)
})
})
return new Promise((resolve, reject) => {
httpGet("/api/config/license")
.then((res) => {
item.data = res;
item.expire = Date.now() + 1000 * 30;
Storage.set(licenseInfoKey, item);
resolve(item.data);
})
.catch((err) => {
resolve(err);
});
});
}
export function getClientId() {
let clientId = Storage.get('client_id')
if (clientId) {
return clientId
}
clientId = randString(42)
Storage.set('client_id', clientId)
return clientId
}
let clientId = Storage.get("client_id");
if (clientId) {
return clientId;
}
clientId = randString(42);
Storage.set("client_id", clientId);
return clientId;
}

View File

@@ -213,6 +213,10 @@ export function processContent(content) {
return "";
});
}
// 支持 \[ 公式标签
content = content.replace(/\\\[/g, "$$").replace(/\\\]/g, "$$");
content = content.replace(/\\\(\\boxed\{(\d+)\}\\\)/g, '<span class="boxed">$1</span>');
return content;
}
@@ -240,7 +244,7 @@ export function showLoginDialog(router) {
export const replaceImg = (img) => {
if (!img.startsWith("http")) {
img = `${location.protocol}//${location.host}/${img}`;
img = `${location.protocol}//${location.host}${img}`;
}
const devHost = process.env.VUE_APP_API_HOST;
const localhost = "http://localhost:5678";

View File

@@ -370,7 +370,6 @@ httpGet("/api/function/list")
showMessageError("获取工具函数失败:" + e.message);
});
// 创建 socket 连接
const prompt = ref("");
const showStopGenerate = ref(false); // 停止生成
const lineBuffer = ref(""); // 输出缓冲行

View File

@@ -66,6 +66,7 @@
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
maxlength="2000"
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
v-loading="isGenerating"
/>
@@ -230,7 +231,7 @@ import { Delete, InfoFilled, Picture } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
@@ -266,7 +267,6 @@ const styles = [
{ name: "自然", value: "natural" },
];
const params = ref({
client_id: getClientId(),
quality: "standard",
size: "1024x1024",
style: "vivid",
@@ -275,6 +275,8 @@ const params = ref({
const finishedJobs = ref([]);
const runningJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const power = ref(0);
const dallPower = ref(0); // 画一张 SD 图片消耗算力
const clipboard = ref(null);
@@ -300,20 +302,6 @@ onMounted(() => {
showMessageError("获取系统配置失败:" + e.message);
});
store.addMessageHandler("dall", (data) => {
// 丢弃无关消息
if (data.channel !== "dall" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 0;
isOver.value = false;
fetchFinishJobs();
}
nextTick(() => fetchRunningJobs());
});
// 获取模型列表
httpGet("/api/dall/models")
.then((res) => {
@@ -329,7 +317,9 @@ onMounted(() => {
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("dall");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
});
const initData = () => {
@@ -338,10 +328,16 @@ const initData = () => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
// 轮询运行中任务
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
}
}, 5000);
})
.catch(() => {});
};
@@ -353,7 +349,17 @@ const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/dall/jobs?finish=false`)
.then((res) => {
runningJobs.value = res.data.items;
// 如果任务有更新,则更新已完成任务列表
if (res.data.items && res.data.items.length !== runningJobs.value.length) {
page.value = 0;
fetchFinishJobs();
}
if (res.data.items.length > 0) {
runningJobs.value = res.data.items;
} else {
allowPulling.value = false;
runningJobs.value = [];
}
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
@@ -409,7 +415,12 @@ const generate = () => {
.then(() => {
ElMessage.success("任务执行成功!");
power.value -= dallPower.value;
fetchRunningJobs();
// 追加任务列表
runningJobs.value.push({
prompt: params.value.prompt,
progress: 0,
});
allowPulling.value = true;
})
.catch((e) => {
ElMessage.error("任务执行失败:" + e.message);

View File

@@ -175,6 +175,7 @@
<el-input
v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
maxlength="2000"
type="textarea"
ref="promptRef"
v-loading="isGenerating"
@@ -208,6 +209,7 @@
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
maxlength="2000"
placeholder="请在此输入你不希望出现在图片上的内容,系统会自动翻译中文提示词"
/>
</div>
@@ -654,14 +656,14 @@ import Compressor from "compressorjs";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox, ElNotification } from "element-plus";
import Clipboard from "clipboard";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { copyObj, removeArrayItem } from "@/utils/libs";
import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
import { showMessageError } from "@/utils/dialog";
import { closeLoading, showLoading, showMessageError } from "@/utils/dialog";
const listBoxHeight = ref(0);
const paramBoxHeight = ref(0);
@@ -744,7 +746,6 @@ const options = [
const router = useRouter();
const initParams = {
client_id: getClientId(),
task_type: "image",
rate: rates[0].value,
model: models[0].value,
@@ -770,6 +771,10 @@ const activeName = ref("txt2img");
const runningJobs = ref([]);
const finishedJobs = ref([]);
const taskPulling = ref(true); // 任务轮询
const tastPullHandler = ref(null);
const downloadPulling = ref(false); // 图片下载轮询
const downloadPullHandler = ref(null);
const power = ref(0);
const userId = ref(0);
@@ -786,25 +791,16 @@ onMounted(() => {
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
store.addMessageHandler("mj", (data) => {
// 丢弃无关消息
if (data.channel !== "mj" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 0;
isOver.value = false;
fetchFinishJobs();
}
nextTick(() => fetchRunningJobs());
});
});
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("mj");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
if (downloadPullHandler.value) {
clearInterval(downloadPullHandler.value);
}
});
// 初始化数据
@@ -815,8 +811,20 @@ const initData = () => {
userId.value = user.id;
isLogin.value = true;
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchRunningJobs();
}
}, 5000);
downloadPullHandler.value = setInterval(() => {
if (downloadPulling.value) {
page.value = 0;
fetchFinishJobs();
}
}, 5000);
})
.catch(() => {});
};
@@ -859,6 +867,14 @@ const fetchRunningJobs = () => {
}
_jobs.push(jobs[i]);
}
if (runningJobs.value.length !== _jobs.length) {
page.value = 0;
downloadPulling.value = true;
fetchFinishJobs();
}
if (_jobs.length === 0) {
taskPulling.value = false;
}
runningJobs.value = _jobs;
})
.catch((e) => {
@@ -880,6 +896,7 @@ const fetchFinishJobs = () => {
httpGet(`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items;
let hasDownload = false;
for (let i = 0; i < jobs.length; i++) {
if (jobs[i]["img_url"] !== "") {
if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") {
@@ -888,16 +905,29 @@ const fetchFinishJobs = () => {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
}
} else {
if (jobs[i].progress === 100) {
hasDownload = true;
}
jobs[i]["thumb_url"] = "/images/img-placeholder.jpg";
}
// 如果当前是第一页,则开启图片下载轮询
if (page.value === 1) {
downloadPulling.value = hasDownload;
}
if (jobs[i].type !== "upscale" && jobs[i].progress === 100) {
jobs[i]["can_opt"] = true;
}
}
if (jobs.length < pageSize.value) {
isOver.value = true;
}
// 对比一下jobs和finishedJobs如果相同则不进行更新
if (JSON.stringify(jobs) === JSON.stringify(finishedJobs.value)) {
return;
}
if (page.value === 1) {
finishedJobs.value = jobs;
} else {
@@ -937,6 +967,7 @@ const uploadImg = (file) => {
success(result) {
const formData = new FormData();
formData.append("file", result, result.name);
showLoading("图片上传中...");
// 执行上传操作
httpPost("/api/upload", formData)
.then((res) => {
@@ -948,9 +979,11 @@ const uploadImg = (file) => {
imgKey.value = "";
}
ElMessage.success("上传成功");
closeLoading();
})
.catch((e) => {
ElMessage.error("上传失败:" + e.message);
closeLoading();
});
},
error(err) {
@@ -983,7 +1016,10 @@ const generate = () => {
.then(() => {
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...");
power.value -= mjPower.value;
fetchRunningJobs();
taskPulling.value = true;
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
ElMessage.error("任务推送失败:" + e.message);
@@ -1003,7 +1039,6 @@ const variation = (index, item) => {
const send = (url, index, item) => {
httpPost(url, {
index: index,
client_id: getClientId(),
channel_id: item.channel_id,
message_id: item.message_id,
message_hash: item.hash,
@@ -1013,7 +1048,10 @@ const send = (url, index, item) => {
.then(() => {
ElMessage.success("任务推送成功,请耐心等待任务执行...");
power.value -= mjActionPower.value;
fetchRunningJobs();
taskPulling.value = true;
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
ElMessage.error("任务推送失败:" + e.message);

View File

@@ -194,6 +194,7 @@
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
maxlength="2000"
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
v-loading="isGenerating"
/>
@@ -324,7 +325,7 @@ import nodata from "@/assets/img/no-data.png";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
@@ -354,7 +355,6 @@ const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SD
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"];
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"];
const params = ref({
client_id: getClientId(),
width: 1024,
height: 1024,
sampler: samplers[0],
@@ -373,6 +373,8 @@ const params = ref({
const runningJobs = ref([]);
const finishedJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const router = useRouter();
// 检查是否有画同款的参数
const _params = router.currentRoute.value.params["copyParams"];
@@ -403,25 +405,13 @@ onMounted(() => {
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
store.addMessageHandler("sd", (data) => {
// 丢弃无关消息
if (data.channel !== "sd" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 0;
isOver.value = false;
fetchFinishJobs();
}
nextTick(() => fetchRunningJobs());
});
});
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("sd");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
});
const initData = () => {
@@ -433,6 +423,12 @@ const initData = () => {
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
}
}, 5000);
})
.catch(() => {});
};
@@ -445,6 +441,13 @@ const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/sd/jobs?finish=0`)
.then((res) => {
if (runningJobs.value.length !== res.data.items.length) {
page.value = 0;
fetchFinishJobs();
}
if (runningJobs.value.length === 0) {
allowPulling.value = false;
}
runningJobs.value = res.data.items;
})
.catch((e) => {
@@ -506,7 +509,10 @@ const generate = () => {
.then(() => {
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...");
power.value -= sdPower.value;
fetchRunningJobs();
allowPulling.value = true;
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
ElMessage.error("任务推送失败:" + e.message);

View File

@@ -8,18 +8,26 @@
<img :src="logo" class="logo" alt="Geek-AI" />
</div>
<div class="menu-item">
<span v-if="!license.de_copy">
<span v-if="!license?.de_copy">
<el-tooltip class="box-item" content="部署文档" placement="bottom">
<a :href="docsURL" class="link-button mr-3" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip class="box-item" content="Github 源码" placement="bottom">
<el-tooltip
class="box-item"
content="Github 源码"
placement="bottom"
>
<a :href="githubURL" class="link-button mr-3" target="_blank">
<i class="iconfont icon-github"></i>
</a>
</el-tooltip>
<el-tooltip class="box-item" content="Gitee 源码" placement="bottom">
<el-tooltip
class="box-item"
content="Gitee 源码"
placement="bottom"
>
<a :href="giteeURL" class="link-button" target="_blank">
<i class="iconfont icon-gitee"></i>
</a>
@@ -27,7 +35,12 @@
</span>
<span v-if="!isLogin">
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录/注册</el-button>
<el-button
@click="router.push('/login')"
class="btn-go animate__animated animate__pulse animate__infinite"
round
>登录/注册</el-button
>
</span>
</div>
</el-menu>
@@ -38,14 +51,23 @@
{{ title }}
</h1>
<div class="msg-text cursor-ani">
<span v-for="(char, index) in displayedChars" :key="index" :style="{ color: rainbowColor(index) }">
<span
v-for="(char, index) in displayedChars"
:key="index"
:style="{ color: rainbowColor(index) }"
>
{{ char }}
</span>
</div>
<div class="navs animate__animated animate__backInDown">
<el-space wrap :size="14">
<div v-for="item in navs" :key="item.url" class="nav-item-box" @click="router.push(item.url)">
<div
v-for="item in navs"
:key="item.url"
class="nav-item-box"
@click="router.push(item.url)"
>
<i :class="'iconfont ' + iconMap[item.url]"></i>
<div>{{ item.name }}</div>
</div>
@@ -58,14 +80,14 @@
</template>
<script setup>
import {onMounted, ref} from "vue";
import {useRouter} from "vue-router";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
import ThemeChange from "@/components/ThemeChange.vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {isMobile} from "@/utils/libs";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { isMobile } from "@/utils/libs";
const router = useRouter();
@@ -95,7 +117,7 @@ const iconMap = ref({
"/apps": "icon-app",
"/member": "icon-vip-user",
"/invite": "icon-share",
"/luma": "icon-luma",
"/luma": "icon-luma"
});
const displayedChars = ref([]);
@@ -163,7 +185,7 @@ const setContent = () => {
const rainbowColor = (index) => {
const hue = (index * 40) % 360; // 每个字符间隔40度形成彩虹色
return `hsl(${hue}, 90%, 50%)`; // 色调(hue),饱和度(70%),亮度(50%)
}
};
</script>
<style lang="stylus" scoped>

662
web/src/views/KeLing.vue Normal file
View File

@@ -0,0 +1,662 @@
<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
>&nbsp;&nbsp;
<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 failed from "@/assets/img/failed.png";
import TaskList from "@/components/TaskList.vue";
import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
import { Plus, Delete, InfoFilled, ChromeFilled, DocumentCopy, Download, WarnTriangleFilled, CircleCloseFilled } from "@element-plus/icons-vue";
import { httpGet, httpPost, httpDownload } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import { checkSession, getSystemInfo } from "@/store/cache";
import Clipboard from "clipboard";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
import { replaceImg, substr } from "@/utils/libs";
import Generating from "@/components/ui/Generating.vue";
import nodata from "@/assets/img/no-data.png";
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>

View File

@@ -92,11 +92,7 @@ onMounted(() => {
checkSession()
.then(() => {
if (isMobile()) {
router.push("/mobile");
} else {
router.push("/chat");
}
router.back();
})
.catch(() => {});
@@ -108,6 +104,7 @@ onMounted(() => {
.catch((e) => {
console.error(e);
});
});
const handleKeyup = (e) => {
@@ -140,11 +137,7 @@ const doLogin = (verifyData) => {
.then((res) => {
setUserToken(res.data.token);
store.setIsLogin(true);
if (isMobile()) {
router.push("/mobile");
} else {
router.push("/chat");
}
router.back();
})
.catch((e) => {
showMessageError("登录失败," + e.message);

View File

@@ -19,7 +19,7 @@
<i class="iconfont icon-image"></i>
</el-upload>
</div>
<textarea class="prompt-input" :rows="row" v-model="formData.prompt" placeholder="请输入提示词或者上传图片" autofocus> </textarea>
<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>
@@ -58,7 +58,7 @@
<img src="/images/play.svg" alt="" />
</button>
</div>
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" />
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress === 101" />
<generating message="正在生成视频" v-else />
</div>
</div>
@@ -68,10 +68,16 @@
</div>
<div class="right" v-if="item.progress === 100">
<div class="tools">
<button class="btn btn-publish">
<!-- <button class="btn btn-publish">
<span class="text">发布</span>
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
</button>
</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">
@@ -111,7 +117,7 @@
</div>
</el-container>
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto">
<video style="width: 100%; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
<video style="max-width: 90vw; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
您的浏览器不支持视频播放
</video>
</black-dialog>
@@ -124,22 +130,20 @@ import nodata from "@/assets/img/no-data.png";
import { onMounted, onUnmounted, reactive, ref } from "vue";
import { CircleCloseFilled } from "@element-plus/icons-vue";
import { httpDownload, httpPost, httpGet } from "@/utils/http";
import { checkSession, getClientId } from "@/store/cache";
import { checkSession } from "@/store/cache";
import { closeLoading, showLoading, 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";
import { useSharedStore } from "@/store/sharedata";
import Clipboard from "clipboard";
const showDialog = ref(false);
const currentVideoUrl = ref("");
const row = ref(1);
const images = ref([]);
const formData = reactive({
client_id: getClientId(),
prompt: "",
expand_prompt: false,
loop: false,
@@ -147,32 +151,43 @@ const formData = reactive({
end_frame_img: "",
});
const store = useSharedStore();
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);
});
store.addMessageHandler("luma", (data) => {
// 丢弃无关消息
if (data.channel !== "luma" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
fetchData(1);
}
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
});
});
onUnmounted(() => {
store.removeMessageHandler("luma");
clipboard.value.destroy();
if (pullHandler.value) {
clearInterval(pullHandler.value);
}
});
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;
@@ -231,14 +246,16 @@ const publishJob = (item) => {
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();
});
};
@@ -249,12 +266,7 @@ const remove = (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;
@@ -266,8 +278,19 @@ const fetchData = (_page) => {
})
.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;
list.value = res.data.items;
taskPulling.value = needPull;
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
list.value = items;
}
noData.value = list.value.length === 0;
})
.catch(() => {
@@ -276,19 +299,19 @@ const fetchData = (_page) => {
});
};
// 创建视频
const create = () => {
const len = images.value.length;
if (len) {
formData.first_frame_img = images.value[0];
formData.first_frame_img = replaceImg(images.value[0]);
if (len === 2) {
formData.end_frame_img = images.value[1];
formData.end_frame_img = replaceImg(images.value[1]);
}
}
httpPost("/api/video/luma/create", formData)
.then(() => {
fetchData(1);
taskPulling.value = true;
showMessageOK("创建任务成功");
})
.catch((e) => {

View File

@@ -1,5 +1,5 @@
<template>
<div class="page-song" :style="{ height: winHeight + 'px' }">
<div class="page-song">
<div class="inner">
<h2 class="title">{{ song.title }}</h2>
<div class="row tags" v-if="song.tags">
@@ -11,15 +11,10 @@
<el-avatar :size="32" :src="song.user?.avatar" />
</span>
<span class="nickname">{{ song.user?.nickname }}</span>
<button class="btn btn-icon" @click="play">
<i class="iconfont icon-play"></i> {{ song.play_times }}
</button>
<button class="btn btn-icon" @click="play"><i class="iconfont icon-play"></i> {{ song.play_times }}</button>
<el-tooltip content="复制歌曲链接" placement="top">
<button
class="btn btn-icon copy-link"
:data-clipboard-text="getShareURL(song)"
>
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)">
<i class="iconfont icon-share1"></i>
</button>
</el-tooltip>
@@ -31,18 +26,12 @@
</div>
<div class="row">
<textarea class="prompt" maxlength="2000" rows="18" readonly>{{
song.prompt
}}</textarea>
<textarea class="prompt" maxlength="2000" rows="18" readonly>{{ song.prompt }}</textarea>
</div>
</div>
<div class="music-player" v-if="playList.length > 0">
<music-player
:songs="playList"
ref="playerRef"
@play="song.play_times += 1"
/>
<music-player :songs="playList" ref="playerRef" @play="song.play_times += 1" />
</div>
</div>
</template>
@@ -67,8 +56,7 @@ httpGet("/api/suno/detail", { song_id: id })
.then((res) => {
song.value = res.data;
playList.value = [song.value];
document.title =
song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
document.title = song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
})
.catch((e) => {
showMessageError("获取歌曲详情失败:" + e.message);

View File

@@ -278,8 +278,8 @@ import BlackInput from "@/components/ui/BlackInput.vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import { compact } from "lodash";
import { httpDownload, httpGet, httpPost } from "@/utils/http";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { checkSession, getClientId } from "@/store/cache";
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
import { checkSession } from "@/store/cache";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatTime, replaceImg } from "@/utils/libs";
import Clipboard from "clipboard";
@@ -313,7 +313,6 @@ const tags = ref([
{ label: "嘻哈", value: "hip hop" },
]);
const data = ref({
client_id: getClientId(),
model: "chirp-v3-0",
tags: "",
lyrics: "",
@@ -330,6 +329,8 @@ const playList = ref([]);
const playerRef = ref(null);
const showPlayer = ref(false);
const list = ref([]);
const taskPulling = ref(true);
const tastPullHandler = ref(null);
const btnText = ref("开始创作");
const refSong = ref(null);
const showDialog = ref(false);
@@ -350,24 +351,20 @@ onMounted(() => {
checkSession()
.then(() => {
fetchData(1);
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchData(1);
}
}, 5000);
})
.catch(() => {});
store.addMessageHandler("suno", (data) => {
// 丢弃无关消息
if (data.channel !== "suno" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
fetchData(1);
}
});
});
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("suno");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
});
const page = ref(1);
@@ -381,15 +378,23 @@ const fetchData = (_page) => {
httpGet("/api/suno/list", { page: page.value, page_size: pageSize.value })
.then((res) => {
total.value = res.data.total;
let needPull = false;
const items = [];
for (let v of res.data.items) {
if (v.progress === 100) {
v.major_model_version = v["raw_data"]["major_model_version"];
}
if (v.progress === 0 || v.progress === 102) {
needPull = true;
}
items.push(v);
}
loading.value = false;
list.value = items;
taskPulling.value = needPull;
// 如果任务有变化,则刷新任务列表
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
list.value = items;
}
noData.value = list.value.length === 0;
})
.catch((e) => {
@@ -425,6 +430,7 @@ const create = () => {
httpPost("/api/suno/create", data.value)
.then(() => {
fetchData(1);
taskPulling.value = true;
showMessageOK("创建任务成功");
})
.catch((e) => {
@@ -437,6 +443,7 @@ const merge = (item) => {
httpPost("/api/suno/create", { song_id: item.song_id, type: 3 })
.then(() => {
fetchData(1);
taskPulling.value = true;
showMessageOK("创建任务成功");
})
.catch((e) => {
@@ -473,6 +480,7 @@ const download = (item) => {
const uploadAudio = (file) => {
const formData = new FormData();
formData.append("file", file.file, file.name);
showLoading("正在上传文件...");
// 执行上传操作
httpPost("/api/upload", formData)
.then((res) => {
@@ -484,9 +492,11 @@ const uploadAudio = (file) => {
.then(() => {
fetchData(1);
showMessageOK("歌曲上传成功");
closeLoading();
})
.catch((e) => {
showMessageError("歌曲上传失败:" + e.message);
closeLoading();
});
removeRefSong();
ElMessage.success({ message: "上传成功", duration: 500 });
@@ -597,14 +607,17 @@ const uploadCover = (file) => {
success(result) {
const formData = new FormData();
formData.append("file", result, result.name);
showLoading("图片上传中...");
// 执行上传操作
httpPost("/api/upload", formData)
.then((res) => {
editData.value.cover = res.data.url;
ElMessage.success({ message: "上传成功", duration: 500 });
closeLoading();
})
.catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
closeLoading();
});
},
error(err) {

View File

@@ -140,6 +140,7 @@ const types = ref([
{ label: "DALL-E", value: "dalle" },
{ label: "Suno文生歌", value: "suno" },
{ label: "Luma视频", value: "luma" },
{ label: "可灵视频", value: "keling" },
{ label: "Realtime API", value: "realtime" },
{ label: "其他", value: "other" },
]);

View File

@@ -1,12 +1,12 @@
<template>
<div class="container media-page">
<el-tabs v-model="activeName" @tab-change="handleChange">
<el-tab-pane label="Suno音乐" name="suno" v-loading="data.suno.loading">
<el-tab-pane v-for="media in mediaTypes" :key="media.name" :label="media.label" :name="media.name" v-loading="data[media.name].loading">
<div class="handle-box">
<el-input v-model="data.suno.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'suno')" clearable />
<el-input v-model="data.suno.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'suno')" clearable />
<el-input v-model="data[media.name].query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, media.name)" clearable />
<el-input v-model="data[media.name].query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, media.name)" clearable />
<el-date-picker
v-model="data.suno.query.created_at"
v-model="data[media.name].query.created_at"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
@@ -14,111 +14,27 @@
value-format="YYYY-MM-DD"
style="margin-right: 10px; width: 200px; position: relative; top: 3px"
/>
<el-button type="primary" :icon="Search" @click="fetchSunoData">搜索</el-button>
<el-button type="primary" :icon="Search" @click="media.fetchData">搜索</el-button>
</div>
<div v-if="data.suno.items.length > 0">
<div v-if="data[media.name].items.length > 0">
<el-row>
<el-table :data="data.suno.items" :row-key="(row) => row.id" table-layout="auto">
<el-table :data="data[media.name].items" :row-key="(row) => row.id" table-layout="auto">
<el-table-column prop="user_id" label="用户ID" />
<el-table-column label="歌曲预览">
<template #default="scope">
<el-table-column label="预览">
<template #default="scope" v-if="media.previewComponent === 'MusicPreview'">
<div class="container" v-if="scope.row.cover_url">
<el-image :src="scope.row.cover_url" fit="cover" />
<div class="duration">{{ formatTime(scope.row.duration) }}</div>
<div class="duration">
{{ formatTime(scope.row.duration) }}
</div>
<button class="play" @click="playMusic(scope.row)">
<img src="/images/play.svg" alt="" />
</button>
</div>
<el-image v-else src="/images/failed.jpg" style="height: 90px" fit="cover" />
</template>
</el-table-column>
<el-table-column prop="title" label="标题" />
<el-table-column prop="progress" label="任务进度">
<template #default="scope">
<span v-if="scope.row.progress <= 100">{{ scope.row.progress }}%</span>
<el-tag v-else type="danger">已失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="power" label="消耗算力" />
<el-table-column prop="tags" label="风格" />
<el-table-column prop="play_times" label="播放次数" />
<el-table-column label="歌词">
<template #default="scope">
<el-button size="small" type="primary" plain @click="showLyric(scope.row)">查看歌词</el-button>
</template>
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
<template #default="scope">
<el-popover
placement="top-start"
title="失败原因"
:width="300"
trigger="hover"
:content="scope.row.err_msg"
v-if="scope.row.progress === 101"
>
<template #reference>
<el-text type="danger">{{ substr(scope.row.err_msg, 20) }}</el-text>
</template>
</el-popover>
<span v-else>无</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, 'suno')">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-row>
<div class="pagination">
<el-pagination
v-if="data.suno.total > 0"
background
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="data.suno.page"
v-model:page-size="data.suno.pageSize"
@current-change="fetchSunoData()"
:total="data.suno.total"
/>
</div>
</div>
<el-empty v-else />
</el-tab-pane>
<el-tab-pane label="Luma视频" name="luma" v-loading="data.luma.loading">
<div class="handle-box">
<el-input v-model="data.luma.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'sd')" clearable />
<el-input v-model="data.luma.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'sd')" clearable />
<el-date-picker
v-model="data.luma.query.created_at"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="margin-right: 10px; width: 200px; position: relative; top: 3px"
/>
<el-button type="primary" :icon="Search" @click="fetchLumaData">搜索</el-button>
</div>
<div v-if="data.luma.items.length > 0">
<el-row>
<el-table :data="data.luma.items" :row-key="(row) => row.id" table-layout="auto">
<el-table-column prop="user_id" label="用户ID" />
<el-table-column label="视频预览">
<template #default="scope">
<template #default="scope" v-if="media.previewComponent === 'VideoPreview'">
<div class="container">
<div v-if="scope.row.progress === 100">
<video class="video" :src="replaceImg(scope.row.video_url)" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
@@ -138,7 +54,16 @@
</template>
</el-table-column>
<el-table-column prop="power" label="消耗算力" />
<el-table-column label="提示词">
<template v-if="media.previewComponent === 'MusicPreview'">
<el-table-column prop="tags" label="风格" />
<el-table-column prop="play_times" label="播放次数" />
<el-table-column label="歌词">
<template #default="scope">
<el-button size="small" type="primary" plain @click="showLyric(scope.row)">查看歌词</el-button>
</template>
</el-table-column>
</template>
<el-table-column label="提示词" v-if="media.previewComponent === 'VideoPreview'">
<template #default="scope">
<el-popover placement="top-start" title="提示词" :width="300" trigger="hover" :content="scope.row.prompt">
<template #reference>
@@ -155,12 +80,12 @@
<el-table-column label="失败原因">
<template #default="scope">
<el-popover
v-if="scope.row.progress === 101"
placement="top-start"
title="失败原因"
:width="300"
trigger="hover"
:content="scope.row.err_msg"
v-if="scope.row.progress === 101"
>
<template #reference>
<el-text type="danger">{{ substr(scope.row.err_msg, 20) }}</el-text>
@@ -171,7 +96,7 @@
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, 'luma')">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, media.name)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
@@ -183,21 +108,20 @@
<div class="pagination">
<el-pagination
v-if="data.luma.total > 0"
v-if="data[media.name].total > 0"
background
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="data.luma.page"
v-model:page-size="data.luma.pageSize"
@current-change="fetchLumaData()"
:total="data.luma.total"
v-model:current-page="data[media.name].page"
v-model:page-size="data[media.name].pageSize"
@current-change="media.fetchData"
:total="data[media.name].total"
/>
</div>
</div>
<el-empty v-else />
</el-tab-pane>
</el-tabs>
<el-dialog v-model="showVideoDialog" title="视频预览">
<video style="width: 100%; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted">
您的浏览器不支持视频播放
@@ -223,7 +147,6 @@ import { Search } from "@element-plus/icons-vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import Generating from "@/components/ui/Generating.vue";
// 变量定义
const data = ref({
suno: {
items: [],
@@ -241,7 +164,36 @@ const data = ref({
pageSize: 10,
loading: true,
},
keling: {
items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 10,
loading: true,
},
});
const mediaTypes = [
{
name: "suno",
label: "Suno音乐",
fetchData: () => fetchSunoData(),
previewComponent: "MusicPreview",
},
{
name: "luma",
label: "Luma视频",
fetchData: () => fetchLumaData(),
previewComponent: "VideoPreview",
},
{
name: "keling",
label: "可灵视频",
fetchData: () => fetchKelingData(),
previewComponent: "VideoPreview",
},
];
const activeName = ref("suno");
const playList = ref([]);
const playerRef = ref(null);
@@ -263,6 +215,9 @@ const handleChange = (tab) => {
case "luma":
fetchLumaData();
break;
case "keling":
fetchKelingData();
break;
}
};
@@ -278,7 +233,7 @@ const fetchSunoData = () => {
const d = data.value.suno;
d.query.page = d.page;
d.query.page_size = d.pageSize;
httpPost("/api/admin/media/list/suno", d.query)
httpPost("/api/admin/media/suno", d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
@@ -297,7 +252,27 @@ const fetchLumaData = () => {
const d = data.value.luma;
d.query.page = d.page;
d.query.page_size = d.pageSize;
httpPost("/api/admin/media/list/luma", d.query)
d.query.type = "luma";
httpPost("/api/admin/media/videos", d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
d.total = res.data.total;
d.page = res.data.page;
d.pageSize = res.data.page_size;
}
d.loading = false;
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
const fetchKelingData = () => {
const d = data.value.keling;
d.query.page = d.page;
d.query.page_size = d.pageSize;
d.query.type = "keling";
httpPost("/api/admin/media/videos", d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
@@ -320,6 +295,16 @@ const remove = function (row, tab) {
})
.catch((e) => {
ElMessage.error("删除失败" + e.message);
})
.finally(() => {
nextTick(() => {
// data.value[tab].page = 1;
// data.value[tab].pageSize = 10;
const mediaType = mediaTypes.find((type) => type.name === tab);
if (mediaType && mediaType.fetchData) {
mediaType.fetchData();
}
});
});
};

View File

@@ -179,6 +179,9 @@
<el-option v-for="item in mjModels" :value="item.value" :label="item.name" :key="item.value">{{ item.name }} </el-option>
</el-select>
</el-form-item>
<el-form-item label="上传文件限制" prop="max_file_size">
<el-input v-model.number="system['max_file_size']" placeholder="最大上传文件大小单位MB" />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="算力配置">
@@ -240,6 +243,25 @@
<el-form-item label="Luma 算力" prop="luma_power">
<el-input v-model.number="system['luma_power']" placeholder="使用 Luma 生成一段视频消耗算力" />
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
可灵算力
<el-tooltip effect="dark" content="可灵每个模型价格不一样具体请参考https://api.geekai.pro/models" raw-content placement="right">
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-row :gutter="20" v-if="system['keling_powers']">
<el-col :span="6" v-for="[key] in Object.entries(system['keling_powers'])" :key="key">
<el-form-item :label="key" label-position="left">
<el-input v-model.number="system['keling_powers'][key]" size="small" />
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
@@ -406,6 +428,20 @@ onMounted(() => {
httpGet("/api/admin/config/get?key=system")
.then((res) => {
system.value = res.data;
system.value.keling_powers = system.value.keling_powers || {
"kling-v1-6_std_5": 240,
"kling-v1-6_std_10": 480,
"kling-v1-6_pro_5": 420,
"kling-v1-6_pro_10": 840,
"kling-v1-5_std_5": 240,
"kling-v1-5_std_10": 480,
"kling-v1-5_pro_5": 420,
"kling-v1-5_pro_10": 840,
"kling-v1_std_5": 120,
"kling-v1_std_10": 240,
"kling-v1_pro_5": 420,
"kling-v1_pro_10": 840,
};
configBak.value = copyObj(system.value);
})
.catch((e) => {

View File

@@ -52,6 +52,7 @@
<van-picker
:columns="columns"
title="选择模型和角色"
@change="onChange"
@cancel="showPicker = false"
@confirm="newChat"
>
@@ -114,7 +115,8 @@ checkSession().then((user) => {
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg
helloMsg: items[i].hello_msg,
model_id: items[i].model_id
})
}
}
@@ -259,6 +261,19 @@ const removeChat = (item) => {
}
const onChange = (item) => {
const selectedValues = item.selectedOptions
if (selectedValues[0].model_id) {
for (let i = 0; i < columns.value[1].length; i++) {
columns.value[1][i].disabled = columns.value[1][i].value !== selectedValues[0].model_id;
}
} else {
for (let i = 0; i < columns.value[1].length; i++) {
columns.value[1][i].disabled = false;
}
}
}
</script>
<style lang="stylus" scoped>

View File

@@ -78,7 +78,10 @@
</div>
<van-popup v-model:show="showPicker" position="bottom" class="popup">
<van-picker :columns="columns" v-model="selectedValues" title="选择模型和角色" @cancel="showPicker = false" @confirm="newChat">
<van-picker :columns="columns" v-model="selectedValues"
title="选择模型和角色"
@change="onChange"
@cancel="showPicker = false" @confirm="newChat">
<template #option="item">
<div class="picker-option">
<van-image v-if="item.icon" :src="item.icon" fit="cover" round />
@@ -106,7 +109,6 @@ import { useSharedStore } from "@/store/sharedata";
import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
import FileList from "@/components/FileList.vue";
const winHeight = ref(0);
const navBarRef = ref(null);
const bottomBarRef = ref(null);
@@ -631,6 +633,19 @@ const getModelName = (model_id) => {
// showMic.value = false
// recognition.stop()
// }
const onChange = (item) => {
const selectedValues = item.selectedOptions
if (selectedValues[0].model_id) {
for (let i = 0; i < columns.value[1].length; i++) {
columns.value[1][i].disabled = columns.value[1][i].value !== selectedValues[0].model_id;
}
} else {
for (let i = 0; i < columns.value[1].length; i++) {
columns.value[1][i].disabled = false;
}
}
}
</script>
<style lang="stylus">

View File

@@ -1,6 +1,6 @@
<template>
<div class="login flex w-full flex-col place-content-center h-lvh">
<el-image src="/images/logo.png" class="w-1/2 mx-auto logo" />
<el-image :src="logo" class="w-1/2 mx-auto logo" />
<div class="title text-center text-3xl font-bold mt-8">{{ title }}</div>
<div class="w-full p-8">
<login-dialog @success="loginSuccess" />
@@ -16,6 +16,7 @@ import { ref, onMounted } from "vue";
const router = useRouter();
const title = ref("登录");
const logo = ref("");
const loginSuccess = () => {
router.back();
@@ -24,6 +25,7 @@ const loginSuccess = () => {
onMounted(() => {
getSystemInfo().then((res) => {
title.value = res.data.title;
logo.value = res.data.logo;
});
});
</script>

View File

@@ -33,6 +33,7 @@
v-model="params.prompt"
rows="3"
autosize
maxlength="2000"
type="textarea"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
/>
@@ -132,7 +133,7 @@ import { onMounted, onUnmounted, ref } from "vue";
import { Delete } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import Clipboard from "clipboard";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast } from "vant";
@@ -173,7 +174,6 @@ const styles = [
{ text: "自然", value: "natural" },
];
const params = ref({
client_id: getClientId(),
quality: qualities[0].value,
size: sizes[0].value,
style: styles[0].value,
@@ -190,6 +190,8 @@ const showModelPicker = ref(false);
const runningJobs = ref([]);
const finishedJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const router = useRouter();
const power = ref(0);
const dallPower = ref(0); // 画一张 DALL 图片消耗算力
@@ -219,17 +221,6 @@ onMounted(() => {
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
});
store.addMessageHandler("dall", (data) => {
if (data.channel !== "dall" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 1;
fetchFinishJobs(1);
}
fetchRunningJobs();
});
// 获取模型列表
httpGet("/api/dall/models")
.then((res) => {
@@ -246,7 +237,9 @@ onMounted(() => {
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("dall");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
});
const initData = () => {
@@ -256,6 +249,12 @@ const initData = () => {
isLogin.value = true;
fetchRunningJobs();
fetchFinishJobs(1);
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
}
}, 5000);
})
.catch(() => {
loading.value = false;
@@ -266,6 +265,12 @@ const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/dall/jobs?finish=0`)
.then((res) => {
if (runningJobs.value.length !== res.data.items.length) {
fetchFinishJobs(1);
}
if (res.data.items.length === 0) {
allowPulling.value = false;
}
runningJobs.value = res.data.items;
})
.catch((e) => {
@@ -332,7 +337,10 @@ const generate = () => {
.then(() => {
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...");
power.value -= dallPower.value;
fetchRunningJobs();
allowPulling.value = true;
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);

View File

@@ -59,6 +59,7 @@
<div class="text-line">
<van-field
v-model="params.prompt"
maxlength="2000"
rows="3"
autosize
type="textarea"
@@ -72,6 +73,7 @@
v-model="params.prompt"
rows="3"
autosize
maxlength="2000"
type="textarea"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
/>
@@ -135,7 +137,7 @@
<div class="text-line">
<van-collapse v-model="activeColspan">
<van-collapse-item title="反向提示词" name="neg_prompt">
<van-field v-model="params.neg_prompt" rows="3" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
<van-field v-model="params.neg_prompt" rows="3" maxlength="2000" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
</van-collapse-item>
</van-collapse>
</div>
@@ -253,7 +255,7 @@ import { showConfirmDialog, showFailToast, showImagePreview, showNotify, showSuc
import { httpGet, httpPost } from "@/utils/http";
import Compressor from "compressorjs";
import { getSessionId } from "@/store/session";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { Delete } from "@element-plus/icons-vue";
import { showLoginDialog } from "@/utils/libs";
@@ -280,7 +282,6 @@ const models = [
];
const imgList = ref([]);
const params = ref({
client_id: getClientId(),
task_type: "image",
rate: rates[0].value,
model: models[0].value,
@@ -308,6 +309,10 @@ const isLogin = ref(false);
const prompt = ref("");
const store = useSharedStore();
const clipboard = ref(null);
const taskPulling = ref(true);
const tastPullHandler = ref(null);
const downloadPulling = ref(false);
const downloadPullHandler = ref(null);
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt");
@@ -325,26 +330,33 @@ onMounted(() => {
isLogin.value = true;
fetchRunningJobs();
fetchFinishJobs(1);
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchRunningJobs();
}
}, 5000);
downloadPullHandler.value = setInterval(() => {
if (downloadPulling.value) {
page.value = 1;
fetchFinishJobs(1);
}
}, 5000);
})
.catch(() => {
// router.push('/login')
});
store.addMessageHandler("mj", (data) => {
if (data.channel !== "mj" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 1;
fetchFinishJobs(1);
}
fetchRunningJobs();
});
});
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("mj");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
if (downloadPullHandler.value) {
clearInterval(downloadPullHandler.value);
}
});
const mjPower = ref(1);
@@ -360,6 +372,10 @@ getSystemInfo()
// 获取运行中的任务
const fetchRunningJobs = (userId) => {
if (!isLogin.value) {
return;
}
httpGet(`/api/mj/jobs?finish=0&user_id=${userId}`)
.then((res) => {
const jobs = res.data.items;
@@ -379,6 +395,14 @@ const fetchRunningJobs = (userId) => {
}
_jobs.push(jobs[i]);
}
if (runningJobs.value.length !== _jobs.length) {
page.value = 1;
downloadPulling.value = true;
fetchFinishJobs(1);
}
if (_jobs.length === 0) {
taskPulling.value = false;
}
runningJobs.value = _jobs;
})
.catch((e) => {
@@ -392,11 +416,16 @@ const error = ref(false);
const page = ref(0);
const pageSize = ref(10);
const fetchFinishJobs = (page) => {
if (!isLogin.value) {
return;
}
loading.value = true;
// 获取已完成的任务
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items;
let hasDownload = false;
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
@@ -404,13 +433,23 @@ const fetchFinishJobs = (page) => {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
}
if (jobs[i]["img_url"] === "" && jobs[i].progress === 100) {
hasDownload = true;
}
if (jobs[i].type !== "upscale" && jobs[i].progress === 100) {
jobs[i]["can_opt"] = true;
}
}
if (page === 1) {
downloadPulling.value = hasDownload;
}
if (jobs.length < pageSize.value) {
finished.value = true;
}
if (page === 1) {
finishedJobs.value = jobs;
} else {
@@ -478,7 +517,6 @@ const uploadImg = (file) => {
const send = (url, index, item) => {
httpPost(url, {
client_id: getClientId(),
index: index,
channel_id: item.channel_id,
message_id: item.message_id,
@@ -489,7 +527,9 @@ const send = (url, index, item) => {
.then(() => {
showSuccessToast("任务推送成功,请耐心等待任务执行...");
power.value -= mjActionPower.value;
fetchRunningJobs();
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);
@@ -523,7 +563,10 @@ const generate = () => {
.then(() => {
showToast("绘画任务推送成功,请耐心等待任务执行");
power.value -= mjPower.value;
fetchRunningJobs();
taskPulling.value = true;
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);

View File

@@ -67,6 +67,7 @@
<van-field
v-model="params.prompt"
maxlength="2000"
rows="3"
autosize
type="textarea"
@@ -75,7 +76,7 @@
<van-collapse v-model="activeColspan">
<van-collapse-item title="反向提示词" name="neg_prompt">
<van-field v-model="params.neg_prompt" rows="3" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
<van-field v-model="params.neg_prompt" rows="3" maxlength="2000" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
</van-collapse-item>
</van-collapse>
@@ -174,7 +175,7 @@ import { onMounted, onUnmounted, ref } from "vue";
import { Delete } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import Clipboard from "clipboard";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast } from "vant";
@@ -210,7 +211,6 @@ const upscaleAlgArr = ref([
const showUpscalePicker = ref(false);
const params = ref({
client_id: getClientId(),
width: 1024,
height: 1024,
sampler: samplers.value[0].value,
@@ -228,6 +228,8 @@ const params = ref({
const runningJobs = ref([]);
const finishedJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const router = useRouter();
// 检查是否有画同款的参数
const _params = router.currentRoute.value.params["copyParams"];
@@ -259,22 +261,13 @@ onMounted(() => {
.catch((e) => {
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
});
store.addMessageHandler("sd", (data) => {
if (data.channel !== "sd" || data.clientId !== getClientId()) {
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 1;
fetchFinishJobs(1);
}
fetchRunningJobs();
});
});
onUnmounted(() => {
clipboard.value.destroy();
store.removeMessageHandler("sd");
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
});
const initData = () => {
@@ -285,6 +278,12 @@ const initData = () => {
isLogin.value = true;
fetchRunningJobs();
fetchFinishJobs(1);
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
}
}, 5000);
})
.catch(() => {
loading.value = false;
@@ -308,6 +307,14 @@ const fetchRunningJobs = () => {
}
_jobs.push(jobs[i]);
}
if (runningJobs.value.length !== _jobs.length) {
fetchFinishJobs(1);
}
if (runningJobs.value.length === 0) {
allowPulling.value = false;
}
runningJobs.value = _jobs;
})
.catch((e) => {
@@ -374,7 +381,10 @@ const generate = () => {
.then(() => {
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...");
power.value -= sdPower.value;
fetchRunningJobs();
allowPulling.value = true;
runningJobs.value.push({
progress: 0,
});
})
.catch((e) => {
showFailToast("任务推送失败:" + e.message);