mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-09 10:43:44 +08:00
merge v4.1.1 and fixed conflicts
This commit is contained in:
@@ -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
|
||||
|
||||
116
web/src/assets/css/index.styl
Normal file
116
web/src/assets/css/index.styl
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
88
web/src/assets/css/song.styl
Normal file
88
web/src/assets/css/song.styl
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
369
web/src/assets/css/suno.styl
Normal file
369
web/src/assets/css/suno.styl
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
Binary file not shown.
Binary file not shown.
47
web/src/assets/img/suno-create-bg.svg
Normal file
47
web/src/assets/img/suno-create-bg.svg
Normal 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 |
77
web/src/components/BackTop.vue
Normal file
77
web/src/components/BackTop.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
282
web/src/components/MusicPlayer.vue
Normal file
282
web/src/components/MusicPlayer.vue
Normal 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>
|
||||
@@ -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 => {
|
||||
|
||||
106
web/src/components/ui/BlackDialog.vue
Normal file
106
web/src/components/ui/BlackDialog.vue
Normal 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>
|
||||
79
web/src/components/ui/BlackInput.vue
Normal file
79
web/src/components/ui/BlackInput.vue
Normal 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>
|
||||
43
web/src/components/ui/BlackSelect.vue
Normal file
43
web/src/components/ui/BlackSelect.vue
Normal 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>
|
||||
24
web/src/components/ui/BlackSwitch.vue
Normal file
24
web/src/components/ui/BlackSwitch.vue
Normal 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>
|
||||
97
web/src/components/ui/Generating.vue
Normal file
97
web/src/components/ui/Generating.vue
Normal 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>
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('')
|
||||
|
||||
|
||||
@@ -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 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 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 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 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()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
95
web/src/views/Song.vue
Normal 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
576
web/src/views/Suno.vue
Normal 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>
|
||||
@@ -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 URL,如:https://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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user