merge v4.1.1 and fixed conflicts

This commit is contained in:
RockYang
2024-11-13 18:40:04 +08:00
110 changed files with 4820 additions and 1977 deletions

View File

@@ -79,7 +79,7 @@
background-color #383838
border 1px solid #454545
border-radius 5px
padding 10px
padding 5px
margin-bottom 10px
display flex
flex-flow column
@@ -91,12 +91,13 @@
}
.el-image {
height 60px
height 30px
width 100%
}
.text {
margin-top 6px
margin-top 4px
font-size 12px
}
}
@@ -420,9 +421,27 @@
flex-flow column
justify-content center
align-items center
min-height 200px
min-height 220px
color #ffffff
overflow hidden
.err-msg-container {
overflow hidden
word-break break-all
padding 15px
.title {
font-size 20px
text-align center
font-weight bold
color #f56c6c
margin-bottom 30px
}
.opt {
display flex
justify-content center
}
}
.iconfont {
font-size 50px
margin-bottom 10px

View File

@@ -0,0 +1,116 @@
.index-page {
margin: 0
overflow hidden
color #ffffff
display flex
justify-content center
align-items baseline
padding-top 150px
.color-bg {
position absolute
top 0
left 0
width 100vw
height 100vh
}
.image-bg {
filter: blur(8px);
background-size: cover;
background-position: center;
}
.shadow {
box-shadow rgba(0, 0, 0, 0.3) 0px 0px 3px
&:hover {
box-shadow rgba(0, 0, 0, 0.3) 0px 0px 8px
}
}
.menu-box {
position absolute
top 0
width 100%
display flex
.el-menu {
padding 0 30px
width 100%
display flex
justify-content space-between
background none
border none
.menu-item {
display flex
padding 20px 0
color #ffffff
.title {
font-size 24px
padding 10px 10px 0 10px
}
.el-image {
height 50px
}
.el-button {
margin-left 10px
span {
margin-left 5px
}
}
}
}
}
.content {
text-align: center;
position relative
h1 {
font-size: 5rem;
margin-bottom: 1rem;
}
p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
.navs {
display flex
max-width 900px
padding 20px
.nav-item {
width 200px
.el-button {
width 100%
padding: 25px 20px;
font-size: 1.3rem;
transition: all 0.3s ease;
.iconfont {
font-size 24px
margin-right 10px
position relative
top -2px
}
}
}
}
}
.footer {
.el-link__inner {
color #ffffff
}
}
}

View File

@@ -0,0 +1,88 @@
.page-song {
display: flex;
justify-content: center;
background-color: #0E0808;
.inner {
text-align left
color rgb(250 247 245)
padding 20px
max-width 600px
width 100%
font-family "Neue Montreal,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji"
.title {
font-size 40px
font-weight: 500
line-height 1rem
white-space nowrap
text-overflow ellipsis
}
.row {
padding 8px 0
}
.author {
display flex
align-items center
.nickname {
margin 0 10px
}
.btn {
margin-right 10px
background-color #363030
border none
border-radius 5px
padding 5px 10px
cursor pointer
&:hover {
background-color #5F5958
}
}
}
.date {
color #999999
display flex
align-items center
.version {
background-color #1C1616
border 1px solid #8f8f8f
font-weight normal
font-size 14px
padding 1px 3px
border-radius 5px
margin-left 10px
}
}
.prompt {
width 100%
height 500px
background-color transparent
white-space pre-wrap
overflow-y auto
resize none
position relative
outline 2px solid transparent
outline-offset 2px
border none
font-size 100%
line-height 2rem
}
}
.music-player {
width 100%
position: fixed;
bottom: 0;
left: 50px;
padding 20px 0
}
}

View File

@@ -0,0 +1,369 @@
.page-suno {
display flex
height 100%
background-color #0E0808
overflow auto
.left-bar {
max-width 340px
min-width 340px
padding 20px 30px
.bar-top {
display flex
flex-flow row
justify-content: space-between;
}
.params {
padding 20px 0
color rgb(250 247 245)
position relative
.pure-music {
position absolute
right 0
top 24px
display flex
.text {
margin-top 5px
margin-left 5px
}
}
.label {
padding 10px 0
.text {
margin-right 10px
}
.el-icon {
top 2px
}
}
.item {
margin-bottom: 20px
position relative
.create-btn {
margin 20px 0
background-image url("~@/assets/img/suno-create-bg.svg")
background-size: cover;
background-position: 50% 50%;
transition: background 1s ease-in-out;
overflow: hidden;
font-size 16px
width 100%
padding 16px
border-radius 25px
border none
cursor pointer
img {
position relative
top 3px
margin-right 5px
}
&:hover {
opacity: 0.9;
}
}
.song {
display flex
padding 10px
background-color #252020
border-radius 10px
margin-bottom 10px
font-size 14px
position relative
.el-image {
width 50px
height 50px
border-radius 10px
}
.title {
display flex
margin-left 10px
align-items center
}
.el-button--info {
position absolute
right 20px
top 20px
}
}
.extend-secs {
padding 10px 0
font-size 14px
input {
width 50px
text-align center
padding 8px 10px
font-size 14px
background none
border 1px solid #8f8f8f
margin 0 10px
border-radius 10px
outline: none;
transition: border-color 0.5s ease, box-shadow 0.5s ease;
&:focus {
border-color: #0F7A71;
box-shadow: 0 0 5px #0F7A71;
}
}
}
.btn-lyric {
position absolute
left 10px
bottom 10px
font-size 12px
padding 2px 5px
}
}
.tag-select {
position relative
overflow-x auto
overflow-y hidden
width 100%
.inner {
display flex
flex-flow row
padding-bottom 10px
.tag {
margin-right 10px
word-break keep-all
background-color #312C2C
color #e1e1e1
border-radius 5px
padding 3px 6px
cursor pointer
font-size 13px
}
}
}
}
}
.right-box {
width 100%
color rgb(250 247 245)
overflow auto
.list-box {
padding 0 0 0 20px
.item {
display flex
flex-flow row
padding 5px 0
cursor pointer
margin-bottom 10px
&:hover {
background-color #2A2525
}
.left {
.container {
width 60px
height 90px
position relative
.el-image {
height 90px
border-radius 5px
}
.duration {
position absolute
bottom 0
right 0
background-color rgba(14,8,8,.7)
padding 0 3px
font-family 'Input Sans'
font-size 14px
font-weight 700
border-radius .125rem
}
.play {
position absolute
width: 56px;
height 100%
top: 0;
left: 50%;
border none
border-radius 5px
background rgba(100, 100, 100, 0.3)
cursor pointer
color #ffffff
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
height 90px
padding 0 20px
.title {
padding 6px 0
font-size 16px
font-weight 700
a {
color rgb(250 247 245)
&:hover {
text-decoration underline
}
}
.model {
color #E2E8F0
background-color #1C1616
border 1px solid #8f8f8f
font-weight normal
font-size 14px
padding 1px 3px
border-radius 5px
margin-left 10px
.iconfont {
font-size 12px
}
}
}
.tags {
font-size 14px
color #d1d1d1
padding 3px 0
}
}
.right {
min-width 320px;
font-size 14px
padding 0 15px
.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 #726E6C
&:hover {
background #3C3737
}
}
}
}
}
.task {
height 100px
background-color #2A2525
display flex
margin-bottom 10px
.left {
display flex
justify-content left
align-items center
padding 20px
width 320px
.title {
font-size 14px
color #e1e1e1
white-space: nowrap; /* */
overflow: hidden; /* */
text-overflow: ellipsis; /* */
}
}
.center {
display flex
width 100%
justify-content center
.failed {
display flex
align-items center
color #E4696B
font-size 14px
}
}
.right {
display flex
width 100px
justify-content center
align-items center
}
}
}
.pagination {
padding 10px 20px
display flex
justify-content center
}
.music-player {
width 100%
position: fixed;
bottom: 0;
left: 50px;
padding 20px 0
}
}
.btn {
margin-right 10px
background-color #363030
border none
border-radius 5px
padding 5px 10px
cursor pointer
&:hover {
background-color #5F5958
}
}
}

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1715987806624') format('woff2'),
url('iconfont.woff?t=1715987806624') format('woff'),
url('iconfont.ttf?t=1715987806624') format('truetype');
src: url('iconfont.woff2?t=1721896403264') format('woff2'),
url('iconfont.woff?t=1721896403264') format('woff'),
url('iconfont.ttf?t=1721896403264') format('truetype');
}
.iconfont {
@@ -13,6 +13,62 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-link:before {
content: "\e6b4";
}
.icon-app:before {
content: "\e64f";
}
.icon-pause:before {
content: "\e693";
}
.icon-prev:before {
content: "\e6a5";
}
.icon-next:before {
content: "\e6a7";
}
.icon-play:before {
content: "\e6a8";
}
.icon-remove:before {
content: "\e82b";
}
.icon-edit:before {
content: "\e61d";
}
.icon-download:before {
content: "\e83a";
}
.icon-more-vertical:before {
content: "\e8cb";
}
.icon-share1:before {
content: "\e661";
}
.icon-suno:before {
content: "\e608";
}
.icon-mp:before {
content: "\e6c4";
}
.icon-mp1:before {
content: "\e647";
}
.icon-control-simple:before {
content: "\e624";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,104 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "880330",
"name": "link",
"font_class": "link",
"unicode": "e6b4",
"unicode_decimal": 59060
},
{
"icon_id": "1503777",
"name": "应用",
"font_class": "app",
"unicode": "e64f",
"unicode_decimal": 58959
},
{
"icon_id": "7156146",
"name": "暂停",
"font_class": "pause",
"unicode": "e693",
"unicode_decimal": 59027
},
{
"icon_id": "14929909",
"name": "多媒体控件Multimedia Controls (12)",
"font_class": "prev",
"unicode": "e6a5",
"unicode_decimal": 59045
},
{
"icon_id": "14929910",
"name": "多媒体控件Multimedia Controls (11)",
"font_class": "next",
"unicode": "e6a7",
"unicode_decimal": 59047
},
{
"icon_id": "14929913",
"name": "多媒体控件Multimedia Controls (13)",
"font_class": "play",
"unicode": "e6a8",
"unicode_decimal": 59048
},
{
"icon_id": "401063",
"name": "remove",
"font_class": "remove",
"unicode": "e82b",
"unicode_decimal": 59435
},
{
"icon_id": "968465",
"name": "编辑",
"font_class": "edit",
"unicode": "e61d",
"unicode_decimal": 58909
},
{
"icon_id": "6151351",
"name": "download",
"font_class": "download",
"unicode": "e83a",
"unicode_decimal": 59450
},
{
"icon_id": "18986714",
"name": "more",
"font_class": "more-vertical",
"unicode": "e8cb",
"unicode_decimal": 59595
},
{
"icon_id": "11903724",
"name": "share",
"font_class": "share1",
"unicode": "e661",
"unicode_decimal": 58977
},
{
"icon_id": "40001359",
"name": "suno",
"font_class": "suno",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "4318807",
"name": "mp3",
"font_class": "mp",
"unicode": "e6c4",
"unicode_decimal": 59076
},
{
"icon_id": "12600802",
"name": "mp4",
"font_class": "mp1",
"unicode": "e647",
"unicode_decimal": 58951
},
{
"icon_id": "12243734",
"name": "control",

Binary file not shown.

View File

@@ -0,0 +1,47 @@
<svg width="385" height="88" viewBox="0 0 385 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_137_12468)">
<rect width="385" height="88" fill="#F24018"/>
<rect width="385" height="88" fill="url(#paint0_radial_137_12468)" fill-opacity="0.25" style="mix-blend-mode:plus-lighter"/>
<g opacity="0.65" filter="url(#filter0_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M111.347 35.6229C151.009 57.6671 209.015 49.1739 230.965 88.9126C255.253 132.884 248.592 191.777 220.732 233.655C193.951 273.912 142.319 284.869 94.8085 293.578C52.1601 301.396 10.1351 297.096 -29.4684 279.522C-74.979 259.325 -135.081 238.424 -141.005 188.932C-146.904 139.646 -81.2632 116.551 -53.9401 75.0726C-29.8623 38.5207 -35.6231 -25.4633 6.91152 -35.6152C49.5563 -45.7935 73.0569 14.3417 111.347 35.6229Z" fill="#1000C0" style="mix-blend-mode:plus-lighter"/>
</g>
<g opacity="0.75" filter="url(#filter1_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M332.217 262.301C293.589 260.201 253.893 255.317 227.782 226.721C199.552 195.803 185.551 152.726 194.871 111.877C204.187 71.0419 233.315 31.0675 274.306 22.8078C310.373 15.5401 331.935 59.2082 363.663 77.865C389.371 92.9816 423.695 92.907 438.039 119.086C458.358 156.168 477.027 204.906 451.904 238.906C426.46 273.342 374.92 264.623 332.217 262.301Z" fill="#1000C0" style="mix-blend-mode:plus-lighter"/>
</g>
<g opacity="0.5" filter="url(#filter2_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.4648 -39.9253C-7.15838 -71.5039 -65.3436 -78.3935 -76.3165 -122.436C-88.4582 -171.17 -66.8828 -226.25 -29.217 -259.404C6.9909 -291.275 59.6564 -288.42 107.765 -284.468C150.95 -280.92 190.417 -265.845 224.132 -238.593C262.877 -207.277 315.527 -171.486 308.519 -122.202C301.54 -73.1229 232.229 -67.9118 195.184 -35.0033C162.539 -6.00341 151.647 57.2182 107.972 55.9516C64.1833 54.6817 56.9587 -9.43976 25.4648 -39.9253Z" fill="#E1B1F8" style="mix-blend-mode:color-dodge"/>
</g>
<g filter="url(#filter3_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M188.082 0.67854C155.276 -1.12779 121.563 -5.32785 99.3885 -29.9233C75.4135 -56.5155 63.5228 -93.5648 71.4379 -128.698C79.3502 -163.82 104.088 -198.201 138.899 -205.305C169.53 -211.556 187.842 -173.998 214.787 -157.952C236.62 -144.95 265.77 -145.014 277.953 -122.498C295.209 -90.6045 311.063 -48.6858 289.727 -19.4429C268.118 10.1745 224.347 2.6754 188.082 0.67854Z" fill="#F24018" style="mix-blend-mode:plus-lighter"/>
</g>
</g>
<defs>
<filter id="filter0_f_137_12468" x="-181.377" y="-76.7656" width="467.29" height="414.089" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="20" result="effect1_foregroundBlur_137_12468"/>
</filter>
<filter id="filter1_f_137_12468" x="136" y="-34" width="384" height="355" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="28" result="effect1_foregroundBlur_137_12468"/>
</filter>
<filter id="filter2_f_137_12468" x="-135.707" y="-342.825" width="500.858" height="454.795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="28" result="effect1_foregroundBlur_137_12468"/>
</filter>
<filter id="filter3_f_137_12468" x="-3" y="-278" width="375" height="353" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="36" result="effect1_foregroundBlur_137_12468"/>
</filter>
<radialGradient id="paint0_radial_137_12468" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(210.237 44) rotate(172.274) scale(178.996 234.083)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<clipPath id="clip0_137_12468">
<rect width="385" height="88" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,77 @@
<template>
<button v-if="showButton" @click="scrollToTop" class="scroll-to-top" :style="{bottom: bottom + 'px', right: right + 'px', backgroundColor: bgColor}">
<el-icon><ArrowUpBold /></el-icon>
</button>
</template>
<script>
import {ArrowUpBold} from "@element-plus/icons-vue";
export default {
name: 'BackTop',
components: {ArrowUpBold},
props: {
bottom: {
type: Number,
default: 30
},
right: {
type: Number,
default: 30
},
bgColor: {
type: String,
default: '#007bff'
}
},
data() {
return {
showButton: false
};
},
mounted() {
this.checkScroll();
window.addEventListener('resize', this.checkScroll);
this.$el.parentElement.addEventListener('scroll', this.checkScroll);
},
beforeUnmount() {
window.removeEventListener('resize', this.checkScroll);
this.$el.parentElement.removeEventListener('scroll', this.checkScroll);
},
methods: {
scrollToTop() {
const container = this.$el.parentElement;
container.scrollTo({
top: 0,
behavior: 'smooth'
});
},
checkScroll() {
const container = this.$el.parentElement;
this.showButton = container.scrollTop > 50;
}
}
}
</script>
<style scoped lang="stylus">
.scroll-to-top {
position: fixed;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
outline: none;
transition: opacity 0.3s;
width 40px
height 40px
display flex
justify-content center
align-items center
font-size 20px
&:hover {
opacity: 0.6;
}
}
</style>

View File

@@ -1,244 +0,0 @@
<template>
<div class="chat-line chat-line-mj" v-loading="loading">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="icon" alt="User"/>
</div>
<div class="chat-item">
<div class="content">
<div class="text" v-html="data.html"></div>
<div class="images" v-if="data.image?.url !== ''">
<el-image :src="data.image?.url"
:zoom-rate="1.2"
:preview-src-list="[data.image?.url]"
fit="cover"
:initial-index="0" loading="lazy">
<template #placeholder>
<div class="image-slot"
:style="{height: height+'px', lineHeight:height+'px'}">
正在加载图片<span class="dot">...</span></div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
</el-icon>
</div>
</template>
</el-image>
</div>
</div>
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
<div class="opt-line">
<ul>
<li><a @click="upscale(1)">U1</a></li>
<li><a @click="upscale(2)">U2</a></li>
<li><a @click="upscale(3)">U3</a></li>
<li><a @click="upscale(4)">U4</a></li>
</ul>
</div>
<div class="opt-line">
<ul>
<li><a @click="variation(1)">V1</a></li>
<li><a @click="variation(2)">V2</a></li>
<li><a @click="variation(3)">V3</a></li>
<li><a @click="variation(4)">V4</a></li>
</ul>
</div>
</div>
<div class="bar" v-if="createdAt !== ''">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">tokens: {{ tokens }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, watch} from "vue";
import {Clock, Picture} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {getSessionId} from "@/store/session";
const props = defineProps({
content: Object,
icon: String,
chatId: String,
roleId: Number,
createdAt: String
});
const data = ref(props.content)
const tokens = ref(0)
const cacheKey = "img_placeholder_height"
const item = localStorage.getItem(cacheKey);
const loading = ref(false)
const height = ref(0)
if (item) {
height.value = parseInt(item)
}
if (data.value["image"]?.width > 0) {
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
localStorage.setItem(cacheKey, height.value)
}
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
// console.log(data.value)
watch(() => props.content, (newVal) => {
data.value = newVal;
});
const emits = defineEmits(['disable-input', 'disable-input']);
const upscale = (index) => {
send('/api/mj/upscale', index)
}
const variation = (index) => {
send('/api/mj/variation', index)
}
const send = (url, index) => {
loading.value = true
emits('disable-input')
httpPost(url, {
index: index,
src: "chat",
message_id: data.value?.["message_id"],
message_hash: data.value?.["image"]?.hash,
session_id: getSessionId(),
prompt: data.value?.["prompt"],
chat_id: props.chatId,
role_id: props.roleId,
icon: props.icon,
}).then(() => {
ElMessage.success("任务推送成功,请耐心等待任务执行...")
loading.value = false
}).catch(e => {
ElMessage.error("任务推送失败:" + e.message)
emits('disable-input')
})
}
</script>
<style lang="stylus">
.chat-line-mj {
background-color #ffffff;
justify-content: center;
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3;
.chat-line-inner {
display flex;
width 100%;
max-width 900px;
padding-left 10px;
.chat-icon {
margin-right 20px;
img {
width: 36px;
height: 36px;
border-radius: 10px;
padding: 1px;
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.content {
word-break break-word;
padding: 6px 10px;
color #374151;
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
.text {
p:first-child {
margin-top 0
}
}
.images {
max-width 350px;
.el-image {
border-radius 10px;
.image-slot {
color #c1c1c1
width 350px
text-align center
border-radius 10px;
border 1px solid #e1e1e1
}
}
}
}
.opt {
.opt-line {
margin 6px 0
ul {
display flex
flex-flow row
padding-left 10px
li {
margin-right 10px
a {
padding 6px 0
width 64px
text-align center
border-radius 5px
display block
cursor pointer
background-color #4E5058
color #ffffff
&:hover {
background-color #6D6F78
}
}
}
}
}
}
.bar {
padding 10px;
.bar-item {
background-color #f7f7f8;
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
.el-icon {
position relative
top 2px;
}
}
}
}
}
}
</style>

View File

@@ -70,7 +70,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>
<span class="bar-item">tokens: {{ finalTokens }}</span>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
</div>
</div>
</div>
@@ -132,27 +132,38 @@ const content =ref(processPrompt(props.data.content))
const files = ref([])
onMounted(() => {
if (!finalTokens.value) {
httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
finalTokens.value = res.data;
}).catch(() => {
})
}
// if (!finalTokens.value) {
// httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
// finalTokens.value = res.data;
// }).catch(() => {
// })
// }
const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex);
if (links) {
httpPost("/api/upload/list", {urls: links}).then(res => {
files.value = res.data
for (let link of links) {
if (isExternalImg(link, files.value)) {
files.value.push({url:link, ext: ".png"})
}
}
}).catch(() => {
})
for (let link of links) {
content.value = content.value.replace(link,"")
}
}
content.value = md.render(content.value.trim())
})
const isExternalImg = (link, files) => {
return isImage(link) && !files.find(file => file.url === link)
}
</script>
<style lang="stylus">
@@ -297,9 +308,10 @@ onMounted(() => {
display flex;
width 100%;
padding 0 25px;
flex-flow row-reverse
.chat-icon {
margin-right 20px;
margin-left 20px;
img {
width: 36px;
@@ -366,6 +378,7 @@ onMounted(() => {
.content-wrapper {
display flex
flex-flow row-reverse
.content {
word-break break-word;
padding: 1rem
@@ -373,7 +386,7 @@ onMounted(() => {
font-size: var(--content-font-size);
overflow: auto;
background-color #98e165
border-radius: 0 10px 10px 10px;
border-radius: 10px 0 10px 10px;
img {
max-width: 600px;

View File

@@ -67,14 +67,13 @@
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT">
</div>
<div class="chat-item">
<div class="content-wrapper">
<div class="content" v-html="data.content"></div>
</div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">tokens: {{ data.tokens }}</span>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg">
<el-tooltip
class="box-item"
@@ -340,18 +339,15 @@ const reGenerate = (prompt) => {
.chat-line-reply-chat {
justify-content: center;
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
padding 1.5rem;
.chat-line-inner {
display flex;
padding 0 25px;
width 100%
flex-flow row-reverse
flex-flow row
.chat-icon {
margin-left 20px;
margin-right 20px;
img {
width: 36px;
@@ -365,11 +361,10 @@ const reGenerate = (prompt) => {
position: relative;
padding: 0;
overflow: hidden;
max-width 60%
max-width 70%
.content-wrapper {
display flex
flex-flow row-reverse
.content {
min-height 20px;
word-break break-word;
@@ -378,7 +373,7 @@ const reGenerate = (prompt) => {
font-size: var(--content-font-size);
overflow auto;
background-color #F5F5F5
border-radius: 10px 0 10px 10px;
border-radius: 0 10px 10px 10px;
img {
max-width: 600px;

View File

@@ -1,22 +1,47 @@
<template>
<div class="foot-container">
<div class="footer">
Powered by {{ author }} @
<el-link type="primary" :href="gitURL" target="_blank" style="--el-link-text-color:#ffffff">
{{ title }} -
{{ version }}
</el-link>
<div><span :style="{color:textColor}">{{copyRight}}</span></div>
<div v-if="!license.de_copy">
<a :href="gitURL" target="_blank" :style="{color:textColor}">
{{ title }} -
{{ version }}
</a>
</div>
</div>
</div>
</template>
<script setup>
import {ref} from "vue";
import {httpGet} from "@/utils/http";
import {showMessageError} from "@/utils/dialog";
const title = ref(process.env.VUE_APP_TITLE)
const title = ref("")
const version = ref(process.env.VUE_APP_VERSION)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
const author = ref('极客学长')
const copyRight = ref('')
const license = ref({})
const props = defineProps({
textColor: {
type: String,
default: '#ffffff'
},
});
// 获取系统配置
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE
copyRight.value = res.data.copyright.length>1?res.data.copyright:'极客学长 © 2023 - '+new Date().getFullYear()+' All rights reserved.'
}).catch(e => {
showMessageError("获取系统配置失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
license.value = res.data
}).catch(e => {
showMessageError("获取 License 失败:" + e.message)
})
</script>
<style scoped lang="stylus">
@@ -35,8 +60,10 @@ const author = ref('极客学长')
padding 20px;
width 100%
.el-link {
color #409eff
a {
&:hover {
text-decoration underline
}
}
}
}

View File

@@ -241,8 +241,8 @@ watch(() => props.show, (newValue) => {
const login = ref(true)
const data = ref({
username: "",
password: "",
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS,
repass: "",
code: "",
invite_code: ""
@@ -251,7 +251,7 @@ const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(false)
const activeName = ref("mobile")
const activeName = ref("")
const wxImg = ref("/images/wx.png")
// eslint-disable-next-line no-undef
const emits = defineEmits(['hide', 'success']);
@@ -261,12 +261,15 @@ httpGet("/api/config/get?key=system").then(res => {
const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true
activeName.value = activeName.value === "" ? "mobile" : activeName.value
}
if (arrayContains(registerWays, "email")) {
enableEmail.value = true
activeName.value = activeName.value === "" ? "email" : activeName.value
}
if (arrayContains(registerWays, "username")) {
enableUser.value = true
activeName.value = activeName.value === "" ? "username" : activeName.value
}
// 是否启用注册
enableRegister.value = res.data['enabled_register']

View File

@@ -0,0 +1,282 @@
<template>
<div class="player">
<div class="container">
<div class="cover">
<el-image :src="cover" fit="cover" />
</div>
<div class="info">
<div class="title">{{title}}</div>
<div class="style">
<span class="tags">{{ tags }}</span>
<span class="text-lightGray"> | </span>
<span class="time">{{ formatTime(currentTime) }}<span class="split">/</span>{{ formatTime(duration) }}</span>
</div>
</div>
<div class="controls-container">
<div class="controls">
<button @click="prevSong" class="control-btn">
<i class="iconfont icon-prev"></i>
</button>
<button @click="togglePlay" class="control-btn">
<i class="iconfont icon-play" v-if="!isPlaying"></i>
<i class="iconfont icon-pause" v-else></i>
</button>
<button @click="nextSong" class="control-btn">
<i class="iconfont icon-next"></i>
</button>
</div>
</div>
<div class="progress-bar" @click="setProgress" ref="progressBarRef">
<div class="progress" :style="{ width: `${progressPercent}%` }"></div>
</div>
<audio ref="audio" @timeupdate="updateProgress" @ended="nextSong"></audio>
<el-button v-if="showClose" class="close" type="info" :icon="Close" circle size="small" @click="emits('close')" />
</div>
</div>
</template>
<script setup>
import {ref, onMounted, watch} from 'vue';
import {showMessageError} from "@/utils/dialog";
import {Close} from "@element-plus/icons-vue";
import {formatTime} from "@/utils/libs";
import {httpGet} from "@/utils/http"
const audio = ref(null);
const isPlaying = ref(false);
const songIndex = ref(0);
const currentTime = ref(0);
const duration = ref(100);
const progressPercent = ref(0);
const progressBarRef = ref(null)
const title = ref("")
const tags = ref("")
const cover = ref("")
// eslint-disable-next-line no-undef
const props = defineProps({
songs: {
type: Array,
required: true,
default: () => []
},
showClose: {
type: Boolean,
default: false
}
});
// eslint-disable-next-line no-undef
const emits = defineEmits(['close','play']);
watch(() => props.songs, (newVal) => {
loadSong(newVal[songIndex.value]);
});
const loadSong = (song) => {
if (!song) {
showMessageError("歌曲加载失败")
return
}
title.value = song.title
tags.value = song.tags
cover.value = song.cover_url
audio.value.src = song.audio_url;
audio.value.load();
isPlaying.value = false
audio.value.onloadedmetadata = () => {
duration.value = audio.value.duration;
};
};
const togglePlay = () => {
if (isPlaying.value) {
audio.value.pause();
isPlaying.value = false
} else {
play()
}
};
const play = () => {
if (isPlaying.value) {
return
}
audio.value.play();
isPlaying.value = true
if (audio.value.currentTime === 0) {
emits("play")
// 增加播放数量
httpGet("/api/suno/play",{song_id:props.songs[songIndex.value].song_id}).then().catch()
}
}
const prevSong = () => {
songIndex.value = (songIndex.value - 1 + props.songs.length) % props.songs.length;
loadSong(props.songs[songIndex.value]);
audio.value.play();
isPlaying.value = true;
};
const nextSong = () => {
songIndex.value = (songIndex.value + 1) % props.songs.length;
loadSong(props.songs[songIndex.value]);
audio.value.play();
isPlaying.value = true;
};
const updateProgress = () => {
try {
currentTime.value = audio.value.currentTime;
progressPercent.value = (currentTime.value / duration.value) * 100;
} catch (e) {
console.error(e.message)
}
};
const setProgress = (event) => {
const totalWidth = progressBarRef.value.offsetWidth;
const clickX = event.offsetX;
const audioDuration = audio.value.duration;
audio.value.currentTime = (clickX / totalWidth) * audioDuration;
};
// eslint-disable-next-line no-undef
defineExpose({
play
});
onMounted(() => {
loadSong(props.songs[songIndex.value]);
});
</script>
<style lang="stylus" scoped>
.player {
display flex
justify-content center
width 100%
.container {
display flex
background-color: #363030;
border-radius: 10px;
border 1px solid #544F4F;
padding: 5px;
width: 80%
text-align: center;
position relative
overflow hidden
.cover {
.el-image {
border-radius: 50%;
width 50px
}
}
.info {
padding 0 10px
min-width 300px
display flex
justify-content center
align-items flex-start
flex-flow column
line-height 1.5
.title {
font-weight 700
font-size 16px
color #ffffff
}
.style {
font-size 14px
display flex
color #e1e1e1
.tags {
font-weight 600
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 隐藏溢出的文本 */
text-overflow: ellipsis; /* 使用省略号表示溢出的文本 */
max-width 200px
}
.text-lightGray {
color: rgb(114 110 108);
padding 0 3px
}
.time {
font-family 'Input Sans'
font-weight 700
.split {
font-size 12px
position relative
top -2px
margin 0 1px 0 3px
}
}
}
}
.controls-container {
width 100%
display flex
flex-flow column
justify-content center
.controls {
display: flex;
justify-content: space-around;
margin-bottom 10px
.control-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
background-color #363030
border-radius 5px
padding 6px
.iconfont {
font-size 20px
}
&:hover {
background-color #5F5958
}
}
}
}
.progress-bar {
position absolute
width 100%
left 0
bottom 0
height: 8px;
background-color: #555;
cursor: pointer;
.progress {
height: 100%;
background-color: #f50;
border-radius: 5px;
width: 0;
}
}
.close {
position absolute
right 10px
top 15px
}
}
}
</style>

View File

@@ -58,8 +58,8 @@ import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRoute} from "vue-router";
const title = ref('Chat-Plus-Admin')
const logo = ref('/images/logo.png')
const title = ref('')
const logo = ref('')
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {

View File

@@ -0,0 +1,106 @@
<template>
<div class="black-dialog">
<el-dialog
v-model="showDialog"
style="--el-dialog-bg-color:#414141;
--el-text-color-primary:#f1f1f1;
--el-border-color:#414141;
--el-color-primary:#21aa93;
--el-color-primary-dark-2:#41555d;
--el-color-white: #e1e1e1;
--el-color-primary-light-3:#549688;
--el-fill-color-blank:#616161;
--el-color-primary-light-7:#717171;
--el-color-primary-light-9:#717171;
--el-text-color-regular:#e1e1e1"
:title="title"
:width="width"
:before-close="cancel"
>
<div class="dialog-body">
<slot></slot>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">{{cancelText}}</el-button>
<el-button type="primary" @click="$emit('confirm')">{{confirmText}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref, watch} from "vue";
const props = defineProps({
show : Boolean,
title: {
type: String,
default: 'Tips',
},
width: {
type: Number,
default: 500,
},
confirmText: {
type: String,
default: '确定',
},
cancelText: {
type: String,
default: '取消',
},
});
const emits = defineEmits(['confirm','cancal']);
const showDialog = ref(props.show)
watch(() => props.show, (newValue) => {
showDialog.value = newValue
})
const cancel = () => {
showDialog.value = false
emits('cancal')
}
</script>
<style lang="stylus">
.black-dialog {
.dialog-body {
.form {
.form-item {
display flex
flex-flow column
font-family: "Neue Montreal";
padding 10px 0
.label {
margin-bottom 0.6rem
margin-inline-end 0.75rem
color #ffffff
font-size 1rem
font-weight 500
}
.input {
display flex
padding 10px
text-align left
font-size 1rem
background none
border-radius 0.375rem
border 1px solid #8f8f8f
outline: none;
transition: border-color 0.5s ease, box-shadow 0.5s ease;
&:focus {
border-color: #0F7A71;
box-shadow: 0 0 5px #0F7A71;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div class="black-input-wrapper">
<el-input v-model="model" :type="type" :rows="rows"
@input="onInput"
style="--el-input-bg-color:#252020;
--el-input-border-color:#414141;
--el-input-focus-border-color:#414141;
--el-text-color-regular: #f1f1f1;
--el-input-border-radius: 10px;
--el-border-color-hover:#616161"
resize="none"
:placeholder="placeholder" :maxlength="maxlength"/>
<div class="word-stat" v-if="rows > 1">
<span>{{value.length}}</span>/<span>{{maxlength}}</span>
</div>
</div>
</template>
<script setup>
import {ref, watch} from "vue";
const props = defineProps({
value : {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
type: {
type: String,
default: 'input',
},
rows: {
type: Number,
default: 5,
},
maxlength: {
type: Number,
default: 1024
}
});
watch(() => props.value, (newValue) => {
model.value = newValue
})
const model = ref(props.value)
const emits = defineEmits(['update:value']);
const onInput = (value) => {
emits('update:value',value)
}
</script>
<style lang="stylus">
.black-input-wrapper {
position relative
.el-textarea__inner {
padding: 20px;
font-size: 16px;
}
.word-stat {
position: absolute;
bottom 10px
right 10px
color rgb(209 203 199)
font-family: Neue Montreal, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-size .875rem
line-height 1.25rem
span {
margin 0 1px
}
}
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<el-select v-model="model" :placeholder="placeholder"
:value="value" @change="$emit('update:value', $event)"
style="--el-fill-color-blank:#252020;
--el-text-color-regular: #a1a1a1;
--el-select-disabled-color:#0E0808;
--el-color-primary-light-9:#0E0808;
--el-border-radius-base:20px;
--el-border-color:#0E0808;">
<el-option v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</template>
<script>
export default {
name: 'BlackSelect',
props: {
value : {
type: String,
default: '',
},
placeholder: {
type: String,
default: '请选择',
},
options: {
type: Array,
default: []
}
},
data() {
return {
model: this.value
}
}
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<el-switch v-model="model" :size="size"
@change="$emit('update:value', $event)"
style="--el-switch-on-color:#555555;--el-color-white:#0E0808"/>
</template>
<script setup>
import {ref, watch} from "vue";
const props = defineProps({
value : Boolean,
size: {
type: String,
default: 'default',
}
});
const model = ref(props.value)
watch(() => props.value, (newValue) => {
model.value = newValue
})
</script>

View File

@@ -0,0 +1,97 @@
<template>
<div class="container">
<div class="wave">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
<div class="text">正在生成歌曲</div>
</div>
</template>
<style scoped lang="stylus">
.container {
display: flex;
flex-flow column
justify-content: center;
align-items: center;
margin: 0;
font-family: 'Arial', sans-serif;
color: #e1e1e1;
.wave {
display: flex;
justify-content: center;
align-items: flex-end;
height: 30px;
margin-bottom: 5px
.bar {
width: 8px;
margin: 0 2px;
background-color: #919191;
animation: wave 1.5s infinite;
}
.bar:nth-child(1) {
animation-delay: 0s;
}
.bar:nth-child(2) {
animation-delay: 0.1s;
}
.bar:nth-child(3) {
animation-delay: 0.2s;
}
.bar:nth-child(4) {
animation-delay: 0.3s;
}
.bar:nth-child(5) {
animation-delay: 0.4s;
}
.bar:nth-child(6) {
animation-delay: 0.5s;
}
.bar:nth-child(7) {
animation-delay: 0.6s;
}
.bar:nth-child(8) {
animation-delay: 0.7s;
}
.bar:nth-child(9) {
animation-delay: 0.8s;
}
.bar:nth-child(10) {
animation-delay: 0.9s;
}
}
}
@keyframes wave {
0%, 100% {
height: 10px;
}
50% {
height: 30px;
}
}
.text {
font-size: 14px;
}
</style>

View File

@@ -82,11 +82,23 @@ const routes = [
meta: {title: 'DALLE-3'},
component: () => import('@/views/Dalle.vue'),
},
{
name: 'suno',
path: '/suno',
meta: {title: 'Suno音乐创作'},
component: () => import('@/views/Suno.vue'),
},
{
name: 'ExternalLink',
path: '/external',
component: () => import('@/views/ExternalPage.vue'),
},
{
name: 'song',
path: '/song/:id',
meta: {title: 'Suno音乐播放'},
component: () => import('@/views/Song.vue'),
},
]
},
{
@@ -280,23 +292,11 @@ const router = createRouter({
routes: routes,
})
const active = ref(false)
const title = ref('')
httpGet("/api/config/license").then(res => {
active.value = res.data.de_copy
}).catch(() => {})
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title
}).catch(()=>{})
let prevRoute = null
// dynamic change the title when router change
router.beforeEach((to, from, next) => {
if (!active.value) {
document.title = `${to.meta.title} | ${process.env.VUE_APP_TITLE}`
} else {
document.title = `${to.meta.title} | ${title.value}`
}
document.title = to.meta.title
prevRoute = from
next()
})

View File

@@ -37,7 +37,11 @@ export function GetFileIcon(ext) {
".pptx": "ppt.png",
".md": "md.png",
".pdf": "pdf.png",
".sql": "sql.png"
".sql": "sql.png",
".mp3": "mp3.png",
".wav": "mp3.png",
".mp4": "mp4.png",
".avi": "mp4.png",
}
if (files[ext]) {
return '/images/ext/' + files[ext]

View File

@@ -34,9 +34,15 @@ axios.interceptors.response.use(
} else {
removeUserToken()
}
console.log(error.response.data)
error.response.data.message = "请先登录"
return Promise.reject(error.response.data)
}
return Promise.reject(error)
if (error.response.status === 400) {
return Promise.reject(new Error(error.response.data.message))
} else {
return Promise.reject(error)
}
})

View File

@@ -90,6 +90,12 @@ export function dateFormat(timestamp, format) {
return timeDate;
}
export function formatTime(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
}
// 判断数组中是否包含某个元素
export function arrayContains(array, value, compare) {
if (!array) {

View File

@@ -15,18 +15,13 @@
<div class="info-text">{{ scope.item.hello_msg }}</div>
</div>
<div class="btn">
<div v-if="hasRole(scope.item.key)">
<el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button>
<el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button>
<el-tooltip effect="light" content="从工作区移除" placement="top" v-if="hasRole(scope.item.key)">
<el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>
</div>
<el-button v-else size="small"
style="--el-color-primary:#009999"
@click="updateRole(scope.item, 'add')">
<el-icon>
<Plus/>
</el-icon>
<span>添加应用</span>
</el-button>
</el-tooltip>
<el-tooltip effect="light" content="添加到工作区" placement="top" v-else>
<el-button size="small" style="--el-color-primary:#009999" @click="updateRole(scope.item, 'add')">添加</el-button>
</el-tooltip>
</div>
</div>
@@ -77,7 +72,7 @@ const roles = ref([])
const store = useSharedStore();
onMounted(() => {
httpGet("/api/role/list?all=true").then((res) => {
httpGet("/api/role/list").then((res) => {
const items = res.data
// 处理 hello message
for (let i = 0; i < items.length; i++) {

View File

@@ -118,6 +118,8 @@
v-if="item.type==='prompt'" :data="item" :list-style="listStyle"/>
<chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle"/>
</div>
<back-top :right="30" :bottom="100" bg-color="#19C27D"/>
</div><!-- end chat box -->
<div class="input-box">
@@ -219,6 +221,9 @@ import {useSharedStore} from "@/store/sharedata";
import FileSelect from "@/components/FileSelect.vue";
import FileList from "@/components/FileList.vue";
import ChatSetting from "@/components/ChatSetting.vue";
import axios from "axios";
import BackTop from "@/components/BackTop.vue";
import {showMessageError} from "@/utils/dialog";
const title = ref('ChatGPT-智能助手');
const models = ref([])
@@ -316,18 +321,17 @@ const initData = () => {
chatList.value = res.data;
allChats.value = res.data;
}
if (router.currentRoute.value.query.role_id) {
roleId.value = parseInt(router.currentRoute.value.query.role_id)
}
// 加载模型
httpGet('/api/model/list').then(res => {
models.value = res.data
modelID.value = models.value[0].id
// 加载角色列表
httpGet(`/api/role/list`).then((res) => {
httpGet(`/api/role/list`,{id:roleId.value}).then((res) => {
roles.value = res.data;
if (router.currentRoute.value.query.role_id) {
roleId.value = parseInt(router.currentRoute.value.query.role_id)
} else {
if (!roleId.value) {
roleId.value = roles.value[0]['id']
}
@@ -343,18 +347,8 @@ const initData = () => {
})
}).catch(() => {
loading.value = false
// 加载会话
httpGet("/api/chat/list").then((res) => {
if (res.data) {
chatList.value = res.data;
allChats.value = res.data;
}
}).catch(() => {
ElMessage.error("加载会话列表失败!")
})
// 加载模型
httpGet('/api/model/list').then(res => {
httpGet('/api/model/list',{id:roleId.value}).then(res => {
models.value = res.data
modelID.value = models.value[0].id
}).catch(e => {
@@ -369,6 +363,37 @@ const initData = () => {
ElMessage.error('获取聊天角色失败: ' + e.messages)
})
})
inputRef.value.addEventListener('paste', (event) => {
const items = (event.clipboardData || window.clipboardData).items;
let fileFound = false;
loading.value = true
for (let item of items) {
if (item.kind === 'file') {
const file = item.getAsFile();
fileFound = true;
const formData = new FormData();
formData.append('file', file);
// 执行上传操作
httpPost('/api/upload', formData).then((res) => {
files.value.push(res.data)
ElMessage.success({message: "上传成功", duration: 500})
loading.value = false
}).catch((e) => {
ElMessage.error('文件上传失败:' + e.message)
loading.value = false
})
break;
}
}
if (!fileFound) {
document.getElementById('status').innerText = 'No file found in paste data.';
}
});
}
const getRoleById = function (rid) {
@@ -582,18 +607,6 @@ const connect = function (chat_id, role_id) {
}
}
// 心跳函数
const sendHeartbeat = () => {
clearTimeout(heartbeatHandle.value)
new Promise((resolve, reject) => {
if (socket.value !== null) {
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
}
resolve("success")
}).then(() => {
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
});
}
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
_socket.addEventListener('open', () => {
chatData.value = []; // 初始化聊天数据
@@ -615,8 +628,6 @@ const connect = function (chat_id, role_id) {
} else { // 加载聊天记录
loadChatHistory(chat_id);
}
// 发送心跳消息
sendHeartbeat()
});
_socket.addEventListener('message', event => {
@@ -627,13 +638,14 @@ const connect = function (chat_id, role_id) {
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
const prePrompt = chatData.value[chatData.value.length-1].content
const prePrompt = chatData.value[chatData.value.length-1]?.content
chatData.value.push({
type: "reply",
id: randString(32),
icon: _role['icon'],
prompt:prePrompt,
content: ""
content: "",
orgContent: "",
});
} else if (data.type === 'end') { // 消息接收完毕
// 追加当前会话到会话列表
@@ -667,8 +679,10 @@ const connect = function (chat_id, role_id) {
} else {
lineBuffer.value += data.content;
const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(processContent(lineBuffer.value));
if (reply) {
reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(processContent(lineBuffer.value));
}
}
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
@@ -678,7 +692,7 @@ const connect = function (chat_id, role_id) {
};
}
} catch (e) {
console.error(e)
console.warn(e)
}
});
@@ -694,7 +708,7 @@ const connect = function (chat_id, role_id) {
connect(chat_id, role_id)
}).catch(() => {
loading.value = true
setTimeout(() => connect(chat_id, role_id), 3000)
showMessageError("会话已断开,刷新页面...")
});
});
@@ -740,7 +754,7 @@ const onInput = (e) => {
const autofillPrompt = (text) => {
prompt.value = text
inputRef.value.focus()
// sendMessage()
sendMessage()
}
// 发送消息
const sendMessage = function () {

View File

@@ -174,7 +174,7 @@
</div> <!-- end finish job list-->
</div>
</div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box -->
</div>
@@ -193,6 +193,7 @@ import Clipboard from "clipboard";
import {checkSession} from "@/action/session";
import {useSharedStore} from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
const listBoxHeight = ref(0)
// const paramBoxHeight = ref(0)

View File

@@ -12,18 +12,18 @@
<div class="navbar">
<el-tooltip
v-if="!licenseConfig.de_copy"
v-if="!license.de_copy"
class="box-item"
effect="light"
content="部署文档"
placement="bottom">
<a href="https://ai.r9it.com/docs/install/" class="link-button" target="_blank">
<a href="https://docs.geekai.me/install/" class="link-button" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip
v-if="!licenseConfig.de_copy"
v-if="!license.de_copy"
class="box-item"
effect="light"
content="项目源码"
@@ -46,7 +46,7 @@
<span class="username">{{ loginUser.nickname }}</span>
</el-dropdown-item>
<div v-if="!licenseConfig.de_copy">
<div v-if="!license.de_copy">
<el-dropdown-item>
<i class="iconfont icon-book"></i>
<a :href="docsURL" target="_blank">
@@ -146,7 +146,7 @@ import ConfigDialog from "@/components/UserInfoDialog.vue";
import {showMessageError} from "@/utils/dialog";
const router = useRouter();
const logo = ref('/images/logo.png');
const logo = ref('');
const mainNavs = ref([])
const moreNavs = ref([])
const curPath = ref(router.currentRoute.value.path)
@@ -156,7 +156,7 @@ const loginUser = ref({})
const version = ref(process.env.VUE_APP_VERSION)
const routerViewKey = ref(0)
const showConfigDialog = ref(false)
const licenseConfig = ref({})
const license = ref({de_copy: true})
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
@@ -166,6 +166,12 @@ watch(() => store.showLoginDialog, (newValue) => {
show.value = newValue
});
// 监听路由变化
router.beforeEach((to, from, next) => {
curPath.value = to.path
next();
});
if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url
}
@@ -199,8 +205,9 @@ onMounted(() => {
})
httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data
license.value = res.data
}).catch(e => {
license.value = {de_copy: false}
showMessageError("获取 License 配置:" + e.message)
})

View File

@@ -487,6 +487,23 @@
</div>
</template>
</el-image>
<el-image v-else-if="slotProp.item['err_msg'] !== ''">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
</template>
</el-image>
<el-image v-else>
<template #error>
<div class="image-slot">
@@ -536,16 +553,30 @@
</div>
</div>
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
<el-button type="warning" v-if="slotProp.item.publish"
@click="publishImage(slotProp.item, false)"
circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
<div class="remove" v-if="slotProp.item.progress === 100">
<el-tooltip effect="light" content="删除任务" placement="top">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
</el-tooltip>
<el-tooltip effect="light" content="取消发布" placement="top" v-if="slotProp.item.publish">
<el-button type="warning"
@click="publishImage(slotProp.item, false)"
circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip effect="light" content="发布图片" placement="top" v-else>
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip effect="light" content="复制提示词" placement="top">
<el-button type="success" class="copy-prompt-mj"
:data-clipboard-text="slotProp.item.prompt" circle>
<el-icon><DocumentCopy/></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</template>
@@ -562,6 +593,7 @@
</div> <!-- end finish job list-->
</div>
</div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box -->
</div>
@@ -582,6 +614,7 @@ 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";
const listBoxHeight = ref(0)
const paramBoxHeight = ref(0)
@@ -714,7 +747,7 @@ const connect = () => {
reader.readAsText(event.data, "UTF-8")
reader.onload = () => {
const message = String(reader.result)
if (message === "FINISH") {
if (message === "FINISH" || message === "FAIL") {
page.value = 0
isOver.value = false
fetchFinishJobs(page.value)
@@ -786,7 +819,7 @@ const fetchRunningJobs = () => {
const jobs = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
if (jobs[i].progress === 101) {
ElNotification({
title: '任务执行失败',
dangerouslyUseHTMLString: true,
@@ -799,7 +832,6 @@ const fetchRunningJobs = () => {
} else {
power.value += mjActionPower.value
}
continue
}
_jobs.push(jobs[i])
}
@@ -833,7 +865,7 @@ const fetchFinishJobs = () => {
jobs[i]['thumb_url'] = '/images/img-placeholder.jpg'
}
if (jobs[i].type === 'image' || jobs[i].type === 'variation') {
if ((jobs[i].type === 'image' || jobs[i].type === 'variation') && jobs[i].progress === 100) {
jobs[i]['can_opt'] = true
}
}
@@ -984,7 +1016,7 @@ const publishImage = (item, action) => {
item.publish = action
page.value = 0
isOver.value = false
fetchFinishJobs()
item.publish = action
}).catch(e => {
ElMessage.error(text + "失败:" + e.message)
})

View File

@@ -345,7 +345,7 @@
</div> <!-- end finish job list-->
</div>
</div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box -->
</div>
@@ -476,6 +476,7 @@ import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session";
import {useSharedStore} from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
const listBoxHeight = ref(0)
// const paramBoxHeight = ref(0)
@@ -755,7 +756,7 @@ const publishImage = (event, item, action) => {
item.publish = action
page.value = 0
isOver.value = false
fetchFinishJobs()
item.publish = action
}).catch(e => {
ElMessage.error(text + "失败:" + e.message)
})

View File

@@ -163,7 +163,10 @@
<i class="iconfont icon-face"></i>
</div>
</div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end of waterfall -->
</div>
<!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
@@ -301,6 +304,7 @@ import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import Clipboard from "clipboard";
import {useRouter} from "vue-router";
import BackTop from "@/components/BackTop.vue";
const data = ref({
"mj": [],

View File

@@ -1,6 +1,6 @@
<template>
<div class="index-page" :style="{height: winHeight+'px'}">
<div class="index-bg" :style="{backgroundImage: 'url('+bgImgUrl+')'}"></div>
<div :class="theme.imageBg?'color-bg image-bg':'color-bg'" :style="{backgroundImage:'url('+bgStyle.backgroundImage+')', backgroundColor:bgStyle.backgroundColor}"></div>
<div class="menu-box">
<el-menu
mode="horizontal"
@@ -8,19 +8,19 @@
>
<div class="menu-item">
<el-image :src="logo" alt="Geek-AI"/>
<div class="title">{{ title }}</div>
<div class="title" :style="{color:theme.textColor}">{{ title }}</div>
</div>
<div class="menu-item">
<span v-if="!licenseConfig.de_copy">
<span v-if="!license.de_copy">
<a :href="docsURL" target="_blank">
<el-button type="primary" round>
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
<i class="iconfont icon-book"></i>
<span>文档</span>
</el-button>
</a>
<a :href="gitURL" target="_blank">
<el-button type="success" round>
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
<i class="iconfont icon-github"></i>
<span>源码</span>
</el-button>
@@ -28,38 +28,29 @@
</span>
<span v-if="!isLogin">
<el-button @click="router.push('/login')" round>登录</el-button>
<el-button @click="router.push('/register')" round>注册</el-button>
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/login')" class="shadow" round>登录</el-button>
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/register')" class="shadow" round>注册</el-button>
</span>
</div>
</el-menu>
</div>
<div class="content">
<h1>欢迎使用 {{ title }}</h1>
<p>{{ slogan }}</p>
<el-button @click="router.push('/chat')" color="#ffffff" style="color:#007bff" :dark="false">
<i class="iconfont icon-chat"></i>
<span>AI 对话</span>
</el-button>
<el-button @click="router.push('/mj')" color="#C4CCFD" style="color:#424282" :dark="false">
<i class="iconfont icon-mj"></i>
<span>MJ 绘画</span>
</el-button>
<h1 :style="{color:theme.textColor}">欢迎使用 {{ title }}</h1>
<p :style="{color:theme.textColor}">{{ slogan }}</p>
<el-button @click="router.push('/sd')" color="#4AE6DF" style="color:#424282" :dark="false">
<i class="iconfont icon-sd"></i>
<span>SD 绘画</span>
</el-button>
<el-button @click="router.push('/xmind')" color="#FFFD55" style="color:#424282" :dark="false">
<i class="iconfont icon-xmind"></i>
<span>思维导图</span>
</el-button>
<!-- <div id="animation-container"></div>-->
<div class="navs">
<el-space wrap>
<div v-for="item in navs" class="nav-item">
<el-button @click="router.push(item.url)" :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" :dark="false">
<i :class="'iconfont '+iconMap[item.url]"></i>
<span>{{item.name}}</span>
</el-button>
</div>
</el-space>
</div>
</div>
<div class="footer" v-if="!licenseConfig.de_copy">
<footer-bar />
</div>
<footer-bar :text-color="theme.textColor" />
</div>
</template>
@@ -79,36 +70,102 @@ if (isMobile()) {
router.push("/mobile")
}
const title = ref("Geek-AI 创作系统")
const logo = ref("/images/logo.png")
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
const licenseConfig = ref({})
const title = ref("")
const logo = ref("")
const slogan = ref("")
const license = ref({de_copy: true})
const winHeight = window.innerHeight - 150
const bgImgUrl = ref('')
const isLogin = ref(false)
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
const navs = ref([])
const btnColors = ref([
{bgColor: "#fff143", textColor: "#50616D"},
{bgColor: "#eaff56", textColor: "#50616D"},
{bgColor: "#bddd22", textColor: "#50616D"},
{bgColor: "#1bd1a5", textColor: "#50616D"},
{bgColor: "#e0eee8", textColor: "#50616D"},
{bgColor: "#7bcfa6", textColor: "#50616D"},
{bgColor: "#bce672", textColor: "#50616D"},
{bgColor: "#44cef6", textColor: "#ffffff"},
{bgColor: "#70f3ff", textColor: "#50616D"},
{bgColor: "#fffbf0", textColor: "#50616D"},
{bgColor: "#d6ecf0", textColor: "#50616D"},
{bgColor: "#88ada6", textColor: "#50616D"},
{bgColor: "#30dff3", textColor: "#50616D"},
{bgColor: "#d3e0f3", textColor: "#50616D"},
{bgColor: "#e9e7ef", textColor: "#50616D"},
{bgColor: "#eacd76", textColor: "#50616D"},
{bgColor: "#f2be45", textColor: "#50616D"},
{bgColor: "#549688", textColor: "#ffffff"},
{bgColor: "#758a99", textColor: "#ffffff"},
{bgColor: "#41555d", textColor: "#ffffff"},
{bgColor: "#21aa93", textColor: "#ffffff"},
{bgColor: "#0aa344", textColor: "#ffffff"},
{bgColor: "#f05654", textColor: "#ffffff"},
{bgColor: "#db5a6b", textColor: "#ffffff"},
{bgColor: "#db5a6b", textColor: "#ffffff"},
{bgColor: "#8d4bbb", textColor: "#ffffff"},
{bgColor: "#426666", textColor: "#ffffff"},
{bgColor: "#177cb0", textColor: "#ffffff"},
{bgColor: "#395260", textColor: "#ffffff"},
{bgColor: "#519a73", textColor: "#ffffff"},
{bgColor: "#75878a", textColor: "#ffffff"},
])
const iconMap =ref(
{
"/chat": "icon-chat",
"/mj": "icon-mj",
"/sd": "icon-sd",
"/dalle": "icon-dalle",
"/images-wall": "icon-image",
"/suno": "icon-suno",
"/xmind": "icon-xmind",
"/apps": "icon-app",
"/member": "icon-vip-user",
"/invite": "icon-share",
}
)
const bgStyle = {}
const color = btnColors.value[Math.floor(Math.random() * btnColors.value.length)]
const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true})
onMounted(() => {
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title
logo.value = res.data.logo
if (res.data.index_bg_url) {
bgImgUrl.value = res.data.index_bg_url
if (res.data.index_bg_url === 'color') {
// 随机选取一种颜色
theme.value.bgColor = color.bgColor
theme.value.btnBgColor = color.bgColor
theme.value.textColor = color.textColor
theme.value.btnTextColor = color.textColor
// 设置背景颜色
bgStyle.backgroundColor = theme.value.bgColor
bgStyle.backgroundImage = "/images/transparent-bg.png"
theme.value.imageBg = false
} else if (res.data.index_bg_url) {
bgStyle.backgroundImage = res.data.index_bg_url
} else {
bgImgUrl.value = "/images/index-bg.jpg"
}
if (res.data.slogan) {
slogan.value = res.data.slogan
bgStyle.backgroundImage = "/images/index-bg.jpg"
}
slogan.value = res.data.slogan
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data
license.value = res.data
}).catch(e => {
ElMessage.error("获取 License 配置:" + e.message)
license.value = {de_copy: false}
ElMessage.error("获取 License 配置失败:" + e.message)
})
httpGet("/api/menu/list?index=1").then(res => {
navs.value = res.data
}).catch(e => {
ElMessage.error("获取导航菜单失败:" + e.message)
})
checkSession().then(() => {
@@ -119,107 +176,5 @@ onMounted(() => {
<style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css'
.index-page {
margin: 0
overflow hidden
color #ffffff
display flex
justify-content center
align-items baseline
padding-top 150px
.index-bg {
position absolute
top 0
left 0
width 100vw
height 100vh
filter: blur(8px);
background-size: cover;
background-position: center;
}
.menu-box {
position absolute
top 0
width 100%
display flex
.el-menu {
padding 0 30px
width 100%
display flex
justify-content space-between
background none
border none
.menu-item {
display flex
padding 20px 0
color #ffffff
.title {
font-size 24px
padding 10px 10px 0 10px
}
.el-image {
height 50px
}
.el-button {
margin-left 10px
span {
margin-left 5px
}
}
}
}
}
.content {
text-align: center;
position relative
h1 {
font-size: 5rem;
margin-bottom: 1rem;
}
p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
.el-button {
padding: 25px 20px;
font-size: 1.3rem;
transition: all 0.3s ease;
.iconfont {
font-size 1.6rem
margin-right 10px
}
}
#animation-container {
display flex
justify-content center
width 100%
height: 300px;
position: absolute;
top: 350px
}
}
.footer {
.el-link__inner {
color #ffffff
}
}
}
@import "@/assets/css/index.styl"
</style>

View File

@@ -55,10 +55,8 @@
</div>
<reset-pass @hide="showResetPass = false" :show="showResetPass"/>
<footer class="footer" v-if="!licenseConfig.de_copy">
<footer-bar/>
</footer>
<footer-bar/>
</div>
</div>
</template>
@@ -81,7 +79,7 @@ const title = ref('Geek-AI');
const username = ref(process.env.VUE_APP_USER);
const password = ref(process.env.VUE_APP_PASS);
const showResetPass = ref(false)
const logo = ref("/images/logo.png")
const logo = ref("")
const licenseConfig = ref({})
const wechatLoginURL = ref('')

View File

@@ -106,19 +106,10 @@ import {useSharedStore} from "@/store/sharedata";
const leftBoxHeight = ref(window.innerHeight - 105)
const rightBoxHeight = ref(window.innerHeight - 115)
const title = ref("")
const prompt = ref("")
const text = ref(`# Geek-AI 助手
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
- 基于 Websocket 实现,完美的打字机体验。
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。
`)
const text = ref("")
const md = require('markdown-it')({breaks: true});
const content = ref(text.value)
const html = ref("")
@@ -135,7 +126,20 @@ const models = ref([])
const modelID = ref(0)
const loading = ref(false)
onMounted(() => {
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE
text.value = `# ${title.value}
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
- 基于 Websocket 实现,完美的打字机体验。
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。
`
content.value = text.value
initData()
try {
markMap.value = Markmap.create(svgRef.value)
@@ -145,7 +149,9 @@ onMounted(() => {
} catch (e) {
console.error(e)
}
});
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
const initData = () => {
httpGet("/api/model/list").then(res => {
@@ -333,7 +339,6 @@ const downloadImage = () => {
a.download = "geek-ai-xmind.png"
a.href = canvas.toDataURL(`image/png`)
a.click()
}
}

View File

@@ -183,8 +183,8 @@ import {validateEmail, validateMobile} from "@/utils/validate";
import {showMessageError, showMessageOK} from "@/utils/dialog";
const router = useRouter();
const title = ref('Geek-AI 用户注册');
const logo = ref("/images/logo")
const title = ref('');
const logo = ref("")
const data = ref({
username: '',
password: '',
@@ -196,7 +196,7 @@ const data = ref({
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(false)
const enableRegister = ref(true)
const activeName = ref("mobile")
const wxImg = ref("/images/wx.png")
const licenseConfig = ref({})

95
web/src/views/Song.vue Normal file
View File

@@ -0,0 +1,95 @@
<template>
<div class="page-song" :style="{ height: winHeight + 'px' }">
<div class="inner">
<h2 class="title">{{song.title}}</h2>
<div class="row tags" v-if="song.tags">
<span>{{song.tags}}</span>
</div>
<div class="row author">
<span>
<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>
<el-tooltip effect="light" content="复制歌曲链接" placement="top">
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)" >
<i class="iconfont icon-share1"></i>
</button>
</el-tooltip>
</div>
<div class="row date">
<span>{{dateFormat(song.created_at)}}</span>
<span class="version">{{song.raw_data?.major_model_version}}</span>
</div>
<div class="row">
<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"/>
</div>
</div>
</template>
<script setup>
import {onMounted, onUnmounted, ref} from "vue"
import {useRouter} from "vue-router";
import {httpGet} from "@/utils/http";
import {showMessageError} from "@/utils/dialog";
import {dateFormat} from "@/utils/libs";
import Clipboard from "clipboard";
import {ElMessage} from "element-plus";
import MusicPlayer from "@/components/MusicPlayer.vue";
const router = useRouter()
const id = router.currentRoute.value.params.id
const song = ref({title:""})
const playList = ref([])
const playerRef = ref(null)
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音乐"
}).catch(e => {
showMessageError("获取歌曲详情失败:"+e.message)
})
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard('.copy-link');
clipboard.value.on('success', () => {
ElMessage.success("复制歌曲链接成功!");
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
})
onUnmounted(() => {
clipboard.value.destroy()
})
// 播放歌曲
const play = () => {
playerRef.value.play()
}
const winHeight = ref(window.innerHeight-50)
const getShareURL = (item) => {
return `${location.protocol}//${location.host}/song/${item.id}`
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/song.styl"
</style>

576
web/src/views/Suno.vue Normal file
View File

@@ -0,0 +1,576 @@
<template>
<div class="page-suno" :style="{ height: winHeight + 'px' }">
<div class="left-bar">
<div class="bar-top">
<el-tooltip effect="light" content="定义模式" placement="top">
<black-switch v-model:value="custom" size="large" />
</el-tooltip>
<black-select v-model:value="data.model" :options="models" placeholder="请选择模型" style="width: 100px" />
</div>
<div class="params">
<div class="pure-music">
<span class="switch"><black-switch v-model:value="data.instrumental" size="default" /></span>
<span class="text">纯音乐</span>
</div>
<div v-if="custom">
<div class="item-group" v-if="!data.instrumental">
<div class="label">
<span class="text">歌词</span>
<el-popover placement="right"
:width="200"
trigger="hover" content="自己写歌词或寻求 AI 的帮助。使用两节歌词8 行)可获得最佳效果。">
<template #reference>
<el-icon>
<InfoFilled/>
</el-icon>
</template>
</el-popover>
</div>
<div class="item"
v-loading="generating"
element-loading-text="正在生成歌词..."
element-loading-background="rgba(122, 122, 122, 0.8)">
<black-input v-model:value="data.lyrics" type="textarea" :rows="10" placeholder="请在这里输入你自己写的歌词..."/>
<button class="btn btn-lyric" @click="createLyric">生成歌词</button>
</div>
</div>
<div class="item-group">
<div class="label">
<span class="text">音乐风格</span>
<el-popover placement="right"
:width="200"
trigger="hover" content="描述您想要的音乐风格例如“原声流行音乐”。Sunos 模特无法识别艺术家的名字,但能够理解音乐流派和氛围。">
<template #reference>
<el-icon>
<InfoFilled/>
</el-icon>
</template>
</el-popover>
</div>
<div class="item">
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."/>
</div>
<div class="tag-select">
<div class="inner">
<span
class="tag"
@click="selectTag(tag)"
v-for="tag in tags"
:key="tag.value">{{ tag.label }}</span>
</div>
</div>
</div>
<div class="item-group">
<div class="label">
<span class="text">歌曲名称</span>
<el-popover placement="right"
:width="200"
trigger="hover" content="给你的歌曲起一个标题,以便于分享、发现和组织。">
<template #reference>
<el-icon>
<InfoFilled/>
</el-icon>
</template>
</el-popover>
</div>
<div class="item">
<black-input v-model:value="data.title" type="textarea" :rows="1" placeholder="请输入歌曲名称..."/>
</div>
</div>
</div>
<div v-else>
<div class="label">
<span class="text">歌曲描述</span>
<el-popover placement="right"
:width="200"
trigger="hover" content="描述您想要的音乐风格和主题例如关于假期的流行音乐。请使用流派和氛围而不是特定的艺术家和歌曲风格AI无法识别。">
<template #reference>
<el-icon>
<InfoFilled/>
</el-icon>
</template>
</el-popover>
</div>
<div class="item">
<black-input v-model:value="data.prompt" type="textarea" :rows="10" placeholder="例如:一首关于鸟人的摇滚歌曲..."/>
</div>
</div>
<div class="ref-song" v-if="refSong">
<div class="label">
<span class="text">续写</span>
<el-popover placement="right"
:width="200"
trigger="hover" content="输入额外的歌词,根据您之前的歌词来扩展歌曲。">
<template #reference>
<el-icon>
<InfoFilled/>
</el-icon>
</template>
</el-popover>
</div>
<div class="item">
<div class="song">
<el-image :src="refSong.cover_url" fit="cover" />
<span class="title">{{refSong.title}}</span>
<el-button type="info" @click="removeRefSong" size="small" :icon="Delete" circle />
</div>
<div class="extend-secs">
<input v-model="refSong.extend_secs" type="text"/> 秒开始续写
</div>
</div>
</div>
<div class="item">
<button class="create-btn" @click="create">
<img src="/images/create-new.svg" alt=""/>
<span>{{btnText}}</span>
</button>
</div>
</div>
</div>
<div class="right-box" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
<div class="list-box" v-if="!noData">
<div v-for="item in list">
<div class="item" v-if="item.progress === 100">
<div class="left">
<div class="container">
<el-image :src="item.cover_url" fit="cover" />
<div class="duration">{{formatTime(item.duration)}}</div>
<button class="play" @click="play(item)">
<img src="/images/play.svg" alt=""/>
</button>
</div>
</div>
<div class="center">
<div class="title">
<a :href="'/song/'+item.song_id" target="_blank">{{item.title}}</a>
<span class="model">{{item.major_model_version}}</span>
<span class="model" v-if="item.ref_song">
<i class="iconfont icon-link"></i>
{{item.ref_song.title}}
</span>
</div>
<div class="tags">{{item.tags}}</div>
</div>
<div class="right">
<div class="tools">
<el-tooltip effect="light" content="以当前歌曲为素材继续创作" placement="top">
<button class="btn" @click="extend(item)">续写</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 effect="light" content="下载歌曲" placement="top">
<a :href="item.audio_url" :download="item.title+'.mp3'" target="_blank">
<button class="btn btn-icon">
<i class="iconfont icon-download"></i>
</button>
</a>
</el-tooltip>
<el-tooltip effect="light" content="复制歌曲链接" placement="top">
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(item)" >
<i class="iconfont icon-share1"></i>
</button>
</el-tooltip>
<el-tooltip effect="light" content="编辑" placement="top">
<button class="btn btn-icon" @click="update(item)">
<i class="iconfont icon-edit"></i>
</button>
</el-tooltip>
<el-tooltip effect="light" content="删除" placement="top">
<button class="btn btn-icon" @click="removeJob(item)">
<i class="iconfont icon-remove"></i>
</button>
</el-tooltip>
</div>
</div>
</div>
<div class="task" v-else>
<div class="left">
<div class="title">
<span v-if="item.title">{{item.title}}</span>
<span v-else>{{item.prompt}}</span>
</div>
</div>
<div class="center">
<div class="failed" v-if="item.progress === 101">
{{item.err_msg}}
</div>
<generating v-else />
</div>
<div class="right">
<el-button type="info" @click="removeJob(item)" circle>
<i class="iconfont icon-remove"></i>
</el-button>
</div>
</div>
</div>
</div>
<el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else/>
<div class="pagination">
<el-pagination v-if="total > pageSize" background
style="--el-pagination-button-bg-color:#414141;
--el-pagination-button-color:#d1d1d1;
--el-disabled-bg-color:#414141;
--el-color-primary:#666666;
--el-pagination-hover-color:#e1e1e1"
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="page"
v-model:page-size="pageSize"
@current-change="fetchData(page)"
:total="total"/>
</div>
<div class="music-player" v-if="showPlayer">
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
</div>
</div>
<black-dialog v-model:show="showDialog" title="修改歌曲" @cancal="showDialog = false" @confirm="updateSong" :width="500">
<form class="form">
<div class="form-item">
<div class="label">歌曲名称</div>
<input class="input" v-model="editData.title" type="text" />
</div>
<div class="form-item">
<div class="label">封面图片</div>
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadCover"
accept=".png,.jpg,.jpeg,.bmp"
>
<el-avatar :src="editData.cover" shape="square" :size="100"/>
</el-upload>
</div>
</form>
</black-dialog>
</div>
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue"
import {Delete, InfoFilled} from "@element-plus/icons-vue";
import BlackSelect from "@/components/ui/BlackSelect.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import BlackInput from "@/components/ui/BlackInput.vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import {compact} from "lodash";
import {httpGet, httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import Generating from "@/components/ui/Generating.vue";
import {checkSession} from "@/action/session";
import {ElMessage, ElMessageBox} from "element-plus";
import {formatTime} from "@/utils/libs";
import Clipboard from "clipboard";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import Compressor from "compressorjs";
const winHeight = ref(window.innerHeight - 50)
const custom = ref(false)
const models = ref([
{label: "v3.0", value: "chirp-v3-0"},
{label: "v3.5", value:"chirp-v3-5"}
])
const tags = ref([
{label: "女声", value: "female vocals"},
{label: "男声", value: "male vocals"},
{label: "流行", value: "pop"},
{label: "摇滚", value: "rock"},
{label: "硬摇滚", value: "hard rock"},
{label: "电音", value: "electronic"},
{label: "金属", value: "metal"},
{label: "重金属", value: "heavy metal"},
{label: "节拍", value: "beat"},
{label: "弱拍", value: "upbeat"},
{label: "合成器", value: "synth"},
{label: "吉他", value: "guitar"},
{label: "钢琴", value: "piano"},
{label: "小提琴", value: "violin"},
{label: "贝斯", value: "bass"},
{label: "嘻哈", value: "hip hop"},
])
const data = ref({
model: "chirp-v3-0",
tags: "",
lyrics: "",
prompt: "",
title: "",
instrumental: false,
ref_task_id: "",
extend_secs: 0,
ref_song_id: "",
})
const loading = ref(true)
const noData = ref(false)
const playList = ref([])
const playerRef = ref(null)
const showPlayer = ref(false)
const list = ref([])
const btnText = ref("开始创作")
const refSong = ref(null)
const showDialog = ref(false)
const editData = ref({title:"",cover:"",id:0})
const socket = ref(null)
const userId = ref(0)
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
if (location.protocol === 'https:') {
host = 'wss://' + location.host;
} else {
host = 'ws://' + location.host;
}
}
const _socket = new WebSocket(host + `/api/suno/client?user_id=${userId.value}`);
_socket.addEventListener('open', () => {
socket.value = _socket;
});
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8")
reader.onload = () => {
const message = String(reader.result)
console.log(message)
if (message === "FINISH" || message === "FAIL") {
fetchData()
}
}
}
});
_socket.addEventListener('close', () => {
if (socket.value !== null) {
connect()
}
});
}
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard('.copy-link');
clipboard.value.on('success', () => {
ElMessage.success("复制歌曲链接成功!");
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
checkSession().then(user => {
userId.value = user.id
fetchData(1)
connect()
})
})
onUnmounted(() => {
clipboard.value.destroy()
})
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const fetchData = (_page) => {
if (_page) {
page.value = _page
}
httpGet("/api/suno/list",{page:page.value, page_size:pageSize.value}).then(res => {
total.value = res.data.total
const items = []
for (let v of res.data.items) {
if (v.progress === 100) {
v.major_model_version = v['raw_data']['major_model_version']
}
items.push(v)
}
loading.value = false
list.value = items
noData.value = list.value.length === 0
}).catch(e => {
showMessageError("获取作品列表失败:"+e.message)
})
}
// 创建新的歌曲
const create = () => {
data.value.type = custom.value ? 2 : 1
data.value.ref_task_id = refSong.value ? refSong.value.task_id : ""
data.value.ref_song_id = refSong.value ? refSong.value.song_id : ""
data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0
if (custom.value) {
if (data.value.lyrics === "") {
return showMessageError("请输入歌词")
}
if (data.value.title === "") {
return showMessageError("请输入歌曲标题")
}
} else {
if (data.value.prompt === "") {
return showMessageError("请输入歌曲描述")
}
}
if (refSong.value && data.value.extend_secs > refSong.value.duration) {
return showMessageError("续写开始时间不能超过原歌曲长度")
}
httpPost("/api/suno/create", data.value).then(() => {
fetchData(1)
showMessageOK("创建任务成功")
}).catch(e => {
showMessageError("创建任务失败:"+e.message)
})
}
// 续写歌曲
const extend = (item) => {
refSong.value = item
refSong.value.extend_secs = item.duration
data.value.title = item.title
custom.value = true
btnText.value = "续写歌曲"
}
// 更细歌曲
const update = (item) => {
showDialog.value = true
editData.value.title = item.title
editData.value.cover = item.cover_url
editData.value.id = item.id
}
const updateSong = () => {
if (editData.value.title === "" || editData.value.cover === "") {
return showMessageError("歌曲标题和封面不能为空")
}
httpPost("/api/suno/update", editData.value).then(() => {
showMessageOK("更新歌曲成功")
showDialog.value = false
fetchData()
}).catch(e => {
showMessageError("更新歌曲失败:"+e.message)
})
}
watch(() => custom.value, (newValue) => {
if (!newValue) {
removeRefSong()
}
})
const removeRefSong = () => {
refSong.value = null
btnText.value = "开始创作"
}
const play = (item) => {
playList.value = [item]
showPlayer.value = true
nextTick(()=> playerRef.value.play())
}
const selectTag = (tag) => {
if (data.value.tags.length + tag.value.length >= 119) {
return
}
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(",")
}
const removeJob = (item) => {
ElMessageBox.confirm(
'此操作将会删除任务相关文件,继续操作码?',
'删除提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
httpGet("/api/suno/remove", {id: item.id}).then(() => {
ElMessage.success("任务删除成功")
fetchData()
}).catch(e => {
ElMessage.error("任务删除失败:" + e.message)
})
}).catch(() => {
})
}
const publishJob = (item) => {
httpGet("/api/suno/publish", {id: item.id, publish:item.publish}).then(() => {
ElMessage.success("操作成功")
}).catch(e => {
ElMessage.error("操作失败:" + e.message)
})
}
const getShareURL = (item) => {
return `${location.protocol}//${location.host}/song/${item.id}`
}
const uploadCover = (file) => {
// 压缩图片并上传
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append('file', result, result.name);
// 执行上传操作
httpPost('/api/upload', formData).then((res) => {
editData.value.cover = res.data.url
ElMessage.success({message: "上传成功", duration: 500})
}).catch((e) => {
ElMessage.error('图片上传失败:' + e.message)
})
},
error(err) {
console.log(err.message);
},
});
}
const generating = ref(false)
const createLyric = () => {
if (data.value.lyrics === "") {
return showMessageError("请输入歌词描述")
}
generating.value = true
httpPost("/api/suno/lyric", {prompt: data.value.lyrics}).then(res => {
const lines = res.data.split('\n');
data.value.title = lines.shift().replace(/\*/g,"")
lines.shift()
data.value.lyrics = lines.join('\n');
generating.value = false
}).catch(e => {
showMessageError("歌词生成失败:"+e.message)
generating.value = false
})
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/suno.styl"
</style>

View File

@@ -2,19 +2,11 @@
<div class="container list" v-loading="loading">
<div class="handle-box">
<el-select v-model="query.platform" placeholder="平台" class="handle-input">
<el-option
v-for="item in platforms"
:key="item.value"
:label="item.name"
:value="item.value"
/>
</el-select>
<el-select v-model="query.type" placeholder="类型" class="handle-input">
<el-option
v-for="item in types"
:key="item.value"
:label="item.name"
:label="item.label"
:value="item.value"
/>
</el-select>
@@ -28,7 +20,6 @@
<el-row>
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="platform" label="所属平台"/>
<el-table-column prop="name" label="名称"/>
<el-table-column prop="value" label="API KEY">
<template #default="scope">
@@ -46,10 +37,9 @@
</el-icon>
</template>
</el-table-column>
<el-table-column prop="type" label="用途">
<el-table-column prop="type" label="类型">
<template #default="scope">
<el-tag v-if="scope.row.type === 'chat'">聊天</el-tag>
<el-tag v-else-if="scope.row.type === 'img'" type="success">绘图</el-tag>
{{getTypeName(scope.row.type)}}
</template>
</el-table-column>
<el-table-column prop="proxy_url" label="代理地址"/>
@@ -84,30 +74,14 @@
:close-on-click-modal="false"
:title="title"
>
<el-alert
type="warning"
:closable="false"
show-icon
style="margin-bottom: 10px; font-size:14px;">
<p><b>注意:</b>如果是百度文心一言平台API-KEY 为 APIKey|SecretKey中间用竖线|)连接</p>
<p><b>注意:</b>如果是讯飞星火大模型API-KEY 为 AppId|APIKey|APISecret中间用竖线|)连接</p>
</el-alert>
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="所属平台" prop="platform">
<el-select v-model="item.platform" placeholder="请选择平台" @change="changePlatform">
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
item.name
}}
</el-option>
</el-select>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="item.name" autocomplete="off"/>
</el-form-item>
<el-form-item label="用途" prop="type">
<el-select v-model="item.type" placeholder="请选择用途" @change="changeType">
<el-option v-for="item in types" :value="item.value" :label="item.name" :key="item.value">{{
item.name
<el-form-item label="类型" prop="type">
<el-select v-model="item.type" placeholder="请选择类型">
<el-option v-for="item in types" :value="item.value" :label="item.label" :key="item.value">{{
item.label
}}
</el-option>
</el-select>
@@ -117,12 +91,12 @@
</el-form-item>
<el-form-item label="API URL" prop="api_url">
<el-input v-model="item.api_url" autocomplete="off"
placeholder="必须填土完整的 Chat API URLhttps://api.openai.com/v1/chat/completions"/>
<div class="info">如果你使用了第三方中转这里就填写中转地址</div>
placeholder="只填 BASE URL 即可https://api.openai.com"/>
</el-form-item>
<el-form-item label="代理地址:" prop="proxy_url">
<el-input v-model="item.proxy_url" autocomplete="off"/>
<div class="info">如果想要通过代理来访问 API请填写代理地址http://127.0.0.1:7890</div>
</el-form-item>
<el-form-item label="启用状态:" prop="enable">
@@ -150,22 +124,24 @@ import ClipboardJS from "clipboard";
// 变量定义
const items = ref([])
const query = ref({type: '',platform:''})
const query = ref({type: ''})
const item = ref({})
const showDialog = ref(false)
const rules = reactive({
platform: [{required: true, message: '请选择平台', trigger: 'change',}],
name: [{required: true, message: '请输入名称', trigger: 'change',}],
type: [{required: true, message: '请选择用途', trigger: 'change',}],
value: [{required: true, message: '请输入 API KEY 值', trigger: 'change',}]
})
const loading = ref(true)
const formRef = ref(null)
const title = ref("")
const platforms = ref([])
const types = ref([
{name: "聊天", value: "chat"},
{name: "绘画", value: "img"},
{label: "对话", value:"chat"},
{label: "Midjourney", value:"mj"},
{label: "DALL-E", value:"dalle"},
{label: "Suno文生歌", value:"suno"},
{label: "Luma视频", value:"luma"},
])
@@ -180,12 +156,6 @@ onMounted(() => {
ElMessage.error('复制失败!');
})
httpGet("/api/admin/config/get/app").then(res => {
platforms.value = res.data.platforms
}).catch(e =>{
ElMessage.error("获取配置失败:"+e.message)
})
fetchData()
})
@@ -193,6 +163,15 @@ onUnmounted(() => {
clipboard.value.destroy()
})
const getTypeName = (type) => {
for (let v of types.value) {
if (v.value === type) {
return v.label
}
}
return ""
}
// 获取数据
const fetchData = () => {
@@ -262,26 +241,6 @@ const set = (filed, row) => {
})
}
const selectedPlatform = ref(null)
const changePlatform = (value) => {
console.log(value)
for (let v of platforms.value) {
if (v.value === value) {
selectedPlatform.value = v
item.value.api_url = v.chat_url
}
}
}
const changeType = (value) => {
if (selectedPlatform.value) {
if(value === 'img') {
item.value.api_url = selectedPlatform.value.img_url
} else {
item.value.api_url = selectedPlatform.value.chat_url
}
}
}
</script>
<style lang="stylus" scoped>

View File

@@ -2,14 +2,7 @@
<div class="container model-list" v-loading="loading">
<div class="handle-box">
<el-select v-model="query.platform" placeholder="平台" class="handle-input">
<el-option
v-for="item in platforms"
:key="item.value"
:label="item.name"
:value="item.value"
/>
</el-select>
<el-input v-model="query.name" placeholder="模型名称" class="handle-input" />
<el-button :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
@@ -17,11 +10,6 @@
<el-row>
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="platform" label="所属平台">
<template #default="scope">
<span class="sort" :data-id="scope.row.id">{{ scope.row.platform }}</span>
</template>
</el-table-column>
<el-table-column prop="name" label="模型名称"/>
<el-table-column prop="value" label="模型值">
<template #default="scope">
@@ -46,11 +34,6 @@
</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 prop="key_name" label="绑定API-KEY"/>
<el-table-column label="操作" width="180">
<template #default="scope">
@@ -72,15 +55,6 @@
style="width: 90%; max-width: 600px;"
>
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="所属平台" prop="platform">
<el-select v-model="item.platform" placeholder="请选择平台">
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
item.name
}}
</el-option>
</el-select>
</el-form-item>
<el-form-item label="模型名称" prop="name">
<el-input v-model="item.name" autocomplete="off"/>
</el-form-item>
@@ -89,7 +63,7 @@
<el-input v-model="item.value" autocomplete="off"/>
</el-form-item>
<el-form-item label="费率" prop="weight">
<el-form-item label="消耗算力" prop="weight">
<template #default>
<div class="tip-input">
<el-input-number :min="0" v-model="item.power" autocomplete="off"/>
@@ -121,18 +95,7 @@
class="box-item"
effect="dark"
raw-content
content="gpt-3.5-turbo:4096 <br/>
gpt-3.5-turbo-16k: 16384 <br/>
gpt-4: 8192 <br/>
gpt-4-32k: 32768 <br/>
chatglm_pro: 32768 <br/>
chatglm_std: 16384 <br/>
chatglm_lite: 4096 <br/>
qwen-turbo: 8192 <br/>
qwen-plus: 32768 <br/>
文心一言: 8192 <br/>
星火1.0: 4096 <br/>
星火2.0-星火3.5: 8192"
content="去各大模型的官方 API 文档查询模型支持的最大上下文长度"
placement="right"
>
<el-icon>
@@ -214,18 +177,16 @@ import ClipboardJS from "clipboard";
// 变量定义
const items = ref([])
const query = ref({platform:''})
const query = ref({name:''})
const item = ref({})
const showDialog = ref(false)
const title = ref("")
const rules = reactive({
platform: [{required: true, message: '请选择平台', trigger: 'change',}],
name: [{required: true, message: '请输入模型名称', trigger: 'change',}],
value: [{required: true, message: '请输入模型值', trigger: 'change',}]
})
const loading = ref(true)
const formRef = ref(null)
const platforms = ref([])
// 获取 API KEY
const apiKeys = ref([])
@@ -290,12 +251,6 @@ onMounted(() => {
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
httpGet("/api/admin/config/get/app").then(res => {
platforms.value = res.data.platforms
}).catch(e =>{
ElMessage.error("获取配置失败"+e.message)
})
})
onUnmounted(() => {
@@ -305,7 +260,7 @@ onUnmounted(() => {
const add = function () {
title.value = "新增模型"
showDialog.value = true
item.value = {enabled: true, weight: 1, open: true}
item.value = {enabled: true, power: 1, open: true,max_tokens: 1024,max_context: 8192, temperature: 0.9,}
}
const edit = function (row) {

View File

@@ -59,7 +59,7 @@ const router = useRouter();
const title = ref('Geek-AI Console');
const username = ref(process.env.VUE_APP_ADMIN_USER);
const password = ref(process.env.VUE_APP_ADMIN_PASS);
const logo = ref("/images/logo.png")
const logo = ref("")
checkAdminSession().then(() => {
router.push("/admin")

View File

@@ -50,9 +50,45 @@
</template>
</el-input>
<el-button type="primary" @click="system.index_bg_url = 'https://api.dujin.org/bing/1920.php'">使用动态背景</el-button>
<el-button @click="system.index_bg_url = 'color'">使用纯色背景</el-button>
</div>
</el-form-item>
<el-form-item label="首页导航菜单" prop="index_navs">
<div class="tip-input">
<el-select
v-model="system['index_navs']"
multiple
:filterable="true"
placeholder="请选择菜单,多选"
style="width: 100%"
>
<el-option
v-for="item in menus"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<div class="info">
<el-tooltip
class="box-item"
effect="dark"
content="被选中的菜单将会在首页导航栏显示"
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</el-form-item>
<el-form-item label="版权信息" prop="copyright">
<el-input v-model="system['copyright']" placeholder="更改此选项需要获取 License 授权"/>
</el-form-item>
<el-form-item label="开放注册" prop="enabled_register">
<div class="tip-input">
<el-switch v-model="system['enabled_register']"/>
@@ -218,6 +254,9 @@
<el-form-item label="DALL-E-3算力" prop="dall_power">
<el-input v-model.number="system['dall_power']" placeholder="使用DALL-E-3画一张图消耗算力"/>
</el-form-item>
<el-form-item label="Suno 算力" prop="suno_power">
<el-input v-model.number="system['suno_power']" placeholder="使用 Suno 生成一首音乐消耗算力"/>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="众筹支付">
<el-form-item label="启用众筹功能" prop="enabled_reward">
@@ -391,22 +430,25 @@ import {InfoFilled, UploadFilled,Select,CloseBold} from "@element-plus/icons-vue
import MdEditor from "md-editor-v3";
import 'md-editor-v3/lib/style.css';
import Menu from "@/views/admin/Menu.vue";
import {dateFormat} from "@/utils/libs";
import {copyObj, dateFormat} from "@/utils/libs";
import AIDrawing from "@/views/admin/AIDrawing.vue";
const activeName = ref('basic')
const system = ref({models: []})
const configBak = ref({})
const loading = ref(true)
const systemFormRef = ref(null)
const models = ref([])
const openAIModels = ref([])
const notice = ref("")
const license = ref({is_active: false})
const menus = ref([])
onMounted(() => {
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {
system.value = res.data
configBak.value = copyObj(system.value)
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
@@ -425,6 +467,12 @@ onMounted(() => {
ElMessage.error("获取模型失败:" + e.message)
})
httpGet('/api/admin/menu/list').then(res => {
menus.value = res.data
}).catch(e => {
ElMessage.error("获取模型失败:" + e.message)
})
fetchLicense()
})
@@ -447,7 +495,7 @@ const save = function (key) {
systemFormRef.value.validate((valid) => {
if (valid) {
system.value['power_price'] = parseFloat(system.value['power_price']) ?? 0
httpPost('/api/admin/config/update', {key: key, config: system.value}).then(() => {
httpPost('/api/admin/config/update', {key: key, config: system.value, config_bak: configBak.value}).then(() => {
ElMessage.success("操作成功!")
}).catch(e => {
ElMessage.error("操作失败:" + e.message)

View File

@@ -105,7 +105,7 @@ checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
// 加载角色列表
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
httpGet(`/api/role/list`).then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {

View File

@@ -109,7 +109,7 @@ onMounted(() => {
})
const fetchApps = () => {
httpGet("/api/role/list?all=true").then((res) => {
httpGet("/api/role/list").then((res) => {
const items = res.data
// 处理 hello message
for (let i = 0; i < items.length; i++) {

View File

@@ -3,10 +3,11 @@
<div class="content">
<van-form>
<div class="avatar">
<van-uploader v-model="fileList"
reupload max-count="1"
:deletable="false"
:after-read="afterRead"/>
<van-image :src="fileList[0].url" size="80" width="80" fit="cover" round />
<!-- <van-uploader v-model="fileList"-->
<!-- reupload max-count="1"-->
<!-- :deletable="false"-->
<!-- :after-read="afterRead"/>-->
</div>
<van-cell-group inset v-model="form">
<van-field
@@ -154,7 +155,7 @@
<script setup>
import {onMounted, ref} from "vue";
import {showFailToast, showNotify, showSuccessToast, showToast} from "vant";
import {showFailToast, showNotify, showSuccessToast} from "vant";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from 'compressorjs';
import {dateFormat, isWeChatBrowser, showLoginDialog} from "@/utils/libs";

View File

@@ -407,8 +407,16 @@ const connect = () => {
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
fetchRunningJobs()
fetchFinishJobs(1)
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8")
reader.onload = () => {
const message = String(reader.result)
if (message === "FINISH" || message === "FAIL") {
page.value = 1
fetchFinishJobs(1)
}
fetchRunningJobs()
}
}
});
@@ -456,7 +464,7 @@ const fetchFinishJobs = (page) => {
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
const jobs = res.data
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
if (jobs[i].progress === 101) {
showNotify({
message: `任务ID${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
type: 'danger',
@@ -479,7 +487,7 @@ const fetchFinishJobs = (page) => {
}
}
if (jobs[i].type === 'image' || jobs[i].type === 'variation') {
if ((jobs[i].type === 'image' || jobs[i].type === 'variation') && jobs[i].progress === 100){
jobs[i]['can_opt'] = true
}
}