mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-24 20:14:26 +08:00
merge v4.2.1
This commit is contained in:
363
web/src/assets/css/keling.styl
Normal file
363
web/src/assets/css/keling.styl
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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; /* 用省略号表示溢出的内容 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
Binary file not shown.
BIN
web/src/assets/img/failed.png
Normal file
BIN
web/src/assets/img/failed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -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,
|
||||
"</textarea>"
|
||||
/<\/textarea>/g,
|
||||
"</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, "");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -370,7 +370,6 @@ httpGet("/api/function/list")
|
||||
showMessageError("获取工具函数失败:" + e.message);
|
||||
});
|
||||
|
||||
// 创建 socket 连接
|
||||
const prompt = ref("");
|
||||
const showStopGenerate = ref(false); // 停止生成
|
||||
const lineBuffer = ref(""); // 输出缓冲行
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
662
web/src/views/KeLing.vue
Normal 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
|
||||
>
|
||||
<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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" },
|
||||
]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user