style:样式切换

This commit is contained in:
lqins 2024-12-19 16:57:57 +08:00
parent 710b008453
commit 357c77ef30
59 changed files with 4775 additions and 3420 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -51,6 +51,6 @@
color: #999; color: #999;
} }
.page-apps .inner .list-box .app-item:hover { .page-apps .inner .list-box .app-item:hover {
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */ box-shadow: 0 0 10px var(--shadow-color); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */ transform: translateY(-10px); /* 向上移动10像素 */
} }

View File

@ -1,30 +1,38 @@
.page-apps { .page-apps {
background-color: #282c34; // background-color: #282c34;
height 100% height 100%
.apps-type-nav{ .apps-type-nav{
height 43px height 50px
padding 8px 0; padding 8px 0;
margin-bottom 3px margin 10px auto
} }
.scrollbar-type-nav{ .scrollbar-type-nav{
display flex display flex
align-items center align-items center
height 43px
padding 0 5px padding 2px
background-color #f4f1f7
width fit-content
border 1px solid rgba(79,89,102,.078)
border-radius: 20px
margin: 0 auto
// background: var(--chat-bg);
// width 100%
li{ li{
flex-shrink 0 flex-shrink 0
display flex display flex
align-items center align-items center
justify-content center justify-content center
margin 0 10px margin 5px 8px
height 26px height 26px
border-radius 4px border-radius 4px
border 1px solid rgb(80,80,80) // border 1px solid rgb(80,80,80)
padding 2px 12px padding 2px 12px
background rgba(60,60,60 0.9) // background rgba(60,60,60 0.9)
color #fff color var(--theme-text-tertiary)
font-weight: bold
font-size 14px font-size 14px
cursor pointer cursor pointer
@ -34,9 +42,12 @@
overflow hidden overflow hidden
margin-right 5px margin-right 5px
border-radius 50% border-radius 50%
} }
&.active{ &.active{
background #21aa93; background #fff;
color: var(--el-color-primary);
border-radius 20px
} }
} }
} }
@ -53,16 +64,24 @@
.item { .item {
display flex display flex
flex-flow row flex-flow row
border 1px solid rgb(80,80,80) // border 1px solid rgb(80,80,80)
padding 10px padding 10px
background rgba(60,60,60 0.5) background: var(--chat-bg);
border-radius 8px
.image { .image {
width 80px width 80px
height 80px height 80px
min-width 80px min-width 80px
border-radius 5px border-radius 50%
overflow hidden overflow hidden
object-fit: contain
display: flex
align-items center
justify-content center
flex-shrink 0
border: 2px solid #f5f7fd
background: #fff
} }
.inner { .inner {
@ -75,7 +94,7 @@
text-align left text-align left
.info-title { .info-title {
color var(--el-text-color) color: var(--text-theme-color)
font-size 1.25rem font-size 1.25rem
line-height 1.75rem line-height 1.75rem
letter-spacing: .025em; letter-spacing: .025em;
@ -96,7 +115,8 @@
word-break: break-all; word-break: break-all;
height 34px height 34px
font-size: .875rem; font-size: .875rem;
color #999999 color var(--el-text-color-primary)
} }
} }

View File

@ -1,5 +1,4 @@
$sideBgColor = #252526;
$borderColor = #4676d0;
#app { #app {
height: 100%; height: 100%;
@ -33,7 +32,7 @@ $borderColor = #4676d0;
// .search-input { // .search-input {
// --el-input-bg-color: #363535 // --el-input-bg-color: #363535
// --el-input-border-color: #464545 // --el-input-border-color: #464545
// --el-input-focus-border-color: #47fff1 // --el-input-focus-border-color:#b0a0f8
// --el-input-hover-border-color: #2DA39A // --el-input-hover-border-color: #2DA39A
// box-shadow: none // box-shadow: none
// } // }

View File

@ -2,6 +2,7 @@
--sm-txt:rgba(163, 174, 208, 1); --sm-txt:rgba(163, 174, 208, 1);
--text-secondary: #8a939d; --text-secondary: #8a939d;
--el-color-primary: rgb(107, 80, 225); --el-color-primary: rgb(107, 80, 225);
--theme-textcolor-normal:#b0a0f8;
--el-border-radius-base: 8px; --el-border-radius-base: 8px;
--el-color-primary-light-5:rgb(107, 85, 255); --el-color-primary-light-5:rgb(107, 85, 255);
--el-color-primary-light-3:rgb(78, 51, 254); --el-color-primary-light-3:rgb(78, 51, 254);
@ -19,9 +20,18 @@
--theme-border-primary: rgba(86, 86, 95, .322); // --theme-border-primary: rgba(86, 86, 95, .322); //
--theme-border-hover: rgb(107, 85, 255);//hover --theme-border-hover: rgb(107, 85, 255);//hover
--text--hover:rgba(215, 211, 240, 0.581) //hover --text--hover:rgba(215, 211, 240, 0.581) //hover
--el-input-focus-border-color: #b0a0f8;
--little-btn-bg:#e9d3f6;
--gray-btn-bg:#ededf591;
--a-link-color: #3561ff
--shadow-color:rgba(223,71,255,0.6)
--sm-btn-bg:#6052ed;
--theme-text-tertiary: #595959;
--theme-btn-fill-tertiary: #f0ebff;
--theme-text-btn-tertiary: #6841ea;
// #e7e7e8
} }
.el-dialog{ .el-dialog{
--el-border-radius-base: 20px; --el-border-radius-base: 20px;
@ -90,13 +100,13 @@
/* */ /* */
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #f1f1f1; /* */ background: #f1f1f1 !important; /* */
border-radius: 6px; /* */ border-radius: 6px; /* */
} }
/* */ /* */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #888; /* */ background: #888 !important; /* */
border-radius: 6px; /* */ border-radius: 6px; /* */
} }
@ -115,4 +125,13 @@
--text-color:var(--theme-text-color-primary) --text-color:var(--theme-text-color-primary)
font-size: 16px font-size: 16px
} }
} }
.sm-btn-theme{
background-color: var(--theme-btn-fill-tertiary) !important;
color: var(--theme-text-btn-tertiary) !important;
border: none;
}
.el-tag, .el-tag.el-tag--primary{
--el-tag-bg-color:#f0ebff
}

View File

@ -1,13 +1,18 @@
.custom-scroll ::-webkit-scrollbar { .custom-scroll ::-webkit-scrollbar {
width: 8px; /* 滚动条宽度 */ width: 8px; /* 滚动条宽度 */
display:none;
} }
.custom-scroll ::-webkit-scrollbar-track { .custom-scroll ::-webkit-scrollbar-track {
background-color: #282c34; background-color: #282c34;
display:none;
} }
.custom-scroll ::-webkit-scrollbar-thumb { .custom-scroll ::-webkit-scrollbar-thumb {
background-color: #444; background-color: #444;
border-radius: 8px; border-radius: 8px;
display:none;
} }
.custom-scroll ::-webkit-scrollbar-thumb:hover { .custom-scroll ::-webkit-scrollbar-thumb:hover {
background-color: #666; background-color: #666;
display:none;
} }

View File

@ -15,7 +15,9 @@
.tab-box{ .tab-box{
align-items: center align-items: center
background-color: var(--card-bg) background-color: var(--card-bg)
height: 100% // height: 100%
// position: fixed;
height: 100vh;
.title{ .title{
font-size: 28px font-size: 28px
font-weight: 700 font-weight: 700
@ -173,6 +175,13 @@
background: var(--theme-bg-all); background: var(--theme-bg-all);
// background-image: linear-gradient(180deg, rgba(247, 232, 255, .54), rgba(191, 223, 255, .35)); // background-image: linear-gradient(180deg, rgba(247, 232, 255, .54), rgba(191, 223, 255, .35));
width: 100%; width: 100%;
.loginMask{
position: absolute;
top: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.topheader{ .topheader{
display: flex; display: flex;
position: fixed; position: fixed;

View File

@ -1,25 +1,25 @@
.page-dall { .page-dall {
background-color: #282c34; // background-color: #282c34;
.inner { .inner {
display: flex; display: flex;
.sd-box { .sd-box {
margin 10px margin 10px
background-color #262626 // background-color #262626
border 1px solid #454545 // border 1px solid #454545
min-width 300px min-width 300px
max-width 300px max-width 300px
padding 10px padding 10px
border-radius 10px border-radius 10px
color #ffffff; color var(--text-theme-color);
font-size 14px font-size 14px
h2 { h2 {
font-weight: bold; font-weight: bold;
font-size 20px font-size 20px
text-align center text-align center
color #47fff1 color#b0a0f8
} }
// //
@ -66,25 +66,24 @@
text-align center text-align center
.el-button { .el-button {
width 100% width 200px
span {
color #2D3A4B
}
} }
} }
} }
.el-form { .el-form {
.el-form-item__label { .el-form-item__label {
color #ffffff color var(--text-theme-color)
} }
} }
.task-list-box { .task-list-box {
background: var(--chat-bg);
width 100% width 100%
padding 10px padding 10px
color #ffffff color var(--text-theme-color)
overflow-x hidden overflow-x hidden
.task-list-inner { .task-list-inner {
@ -93,26 +92,26 @@
} }
.el-tabs__item { .el-tabs__item {
color: #fff; color: var(--text-theme-color);
font-size: 18px; font-size: 18px;
} }
.title-tabs .el-tabs__item.is-active { .title-tabs .el-tabs__item.is-active {
color: #47FFF1; color:#b0a0f8;
font-size: 18px; font-size: 18px;
} }
.title-tabs .el-tabs__active-bar { .title-tabs .el-tabs__active-bar {
background-color: #47FFF1; background-color:#b0a0f8;
} }
.el-textarea { .el-textarea {
--el-input-focus-border-color: #47FFF1; // --el-input-focus-border-color:#b0a0f8;
} }
.el-textarea__inner { .el-textarea__inner {
background: transparent; background: transparent;
color: #fff; color: var(--text-theme-color);
} }
.el-input__wrapper { .el-input__wrapper {
@ -141,7 +140,7 @@
} }
.el-form-item__label { .el-form-item__label {
color #ffffff color var(--text-theme-color)
} }
// //

View File

@ -365,7 +365,7 @@
display: block; display: block;
} }
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover { .page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover {
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */ box-shadow: 0 0 10px rgba(223,71,255,0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */ transform: translateY(-10px); /* 向上移动10像素 */
} }
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image { .page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image {

View File

@ -1,26 +1,30 @@
.page-mj { .page-mj {
background-color: #282c34; // background-color: #282c34;
height 100% height 100%
.inner { .inner {
display: flex; display: flex;
// height: 100%
.mj-box { .mj-box {
margin 10px margin 10px
background-color #262626 // background-color #262626
border 1px solid #454545 // border 1px solid #454545
// height: calc(100vh - 50px)
// overflow: scroll
min-width 300px min-width 300px
max-width 300px max-width 300px
padding 10px padding 10px
border-radius 10px border-radius 10px
color #ffffff; color var(--text-theme-color);
font-size 14px font-size 14px
overflow auto
h2 { h2 {
font-weight: bold; font-weight: bold;
font-size 20px font-size 20px
text-align center text-align center
color #47fff1 color var( --theme-textcolor-normal)
} }
// //
@ -44,16 +48,20 @@
} }
.grid-content { .grid-content {
background-color #383838 // background-color #383838
border-radius 5px background: var(--card-bg);
border-radius: 8px;
padding 8px 14px padding 8px 14px
display flex display flex
cursor pointer cursor pointer
margin-bottom: 10px; margin-bottom: 10px;
border 1px solid #383838 // border 1px solid #383838
border 1px solid var(--chat-bg)
&:hover { &:hover {
background-color #585858 border 1px solid var(--theme-border-hover)
} }
.icon { .icon {
@ -70,28 +78,30 @@
.grid-content.active { .grid-content.active {
color #47fff1 // color #47fff1
background-color #585858 // background-color #585858
border 1px solid #47fff1 border 1px solid var(--theme-border-hover)
} }
.model { .model {
background-color #383838 background: var(--card-bg);
border 1px solid #454545 // border 1px solid #454545
border-radius 5px border-radius 8px
padding 5px padding 5px
margin-bottom 10px margin-bottom 10px
display flex display flex
flex-flow column flex-flow column
align-items center align-items center
cursor pointer cursor pointer
border 1px solid var(--chat-bg)
&:hover { &:hover {
background-color #585858 border 1px solid var(--theme-border-hover)
} }
.el-image { .el-image {
height 30px height 40px
width 100% width 100%
} }
@ -103,9 +113,10 @@
} }
.model.active { .model.active {
color #47fff1 // color #47fff1
background-color #585858 // background-color #585858
border 1px solid #47fff1 border 1px solid var(--theme-border-hover)
} }
.form-item-inner { .form-item-inner {
@ -113,16 +124,16 @@
align-items: center align-items: center
.el-select { .el-select {
--el-select-input-focus-border-color: #47FFF1; --el-select-input-focus-border-color: var(--el-color-primary)
--el-input-focus-border-color: #47FFF1; --el-input-focus-border-color: var(--el-color-primary)
} }
.el-input__wrapper { .el-input__wrapper {
background: #383838; background: var(--chat-bg)
} }
.el-input__inner { .el-input__inner {
color: #fff color: var(--text-theme-color)
} }
.el-icon { .el-icon {
@ -167,7 +178,7 @@
.el-form { .el-form {
.el-form-item__label { .el-form-item__label {
color #ffffff color var(--text-theme-color)
} }
.el-input, .el-slider { .el-input, .el-slider {
@ -182,9 +193,10 @@
} }
.task-list-box { .task-list-box {
background: var(--chat-bg);
width 100% width 100%
padding 0 10px 10px 10px padding 0 10px 10px 10px
color #ffffff color var(--text-theme-color)
overflow-x hidden overflow-x hidden
.task-list-inner { .task-list-inner {
@ -193,26 +205,26 @@
} }
.el-tabs__item { .el-tabs__item {
color: #fff; color: var(--text-theme-color);
font-size: 18px; font-size: 18px;
} }
.title-tabs .el-tabs__item.is-active { .title-tabs .el-tabs__item.is-active {
color: #47FFF1; color: var( --theme-textcolor-normal);
font-size: 18px; font-size: 18px;
} }
.title-tabs .el-tabs__active-bar { .title-tabs .el-tabs__active-bar {
background-color: #47FFF1; background-color: var( --theme-textcolor-normal);
} }
.el-textarea { .el-textarea {
--el-input-focus-border-color: #47FFF1; --el-input-focus-border-color: var(--el-color-primary)
} }
.el-textarea__inner { .el-textarea__inner {
background: transparent; background: transparent;
color: #fff; color: var(--text-theme-color);
} }
.el-input__wrapper { .el-input__wrapper {
@ -241,7 +253,7 @@
} }
.el-form-item__label { .el-form-item__label {
color #ffffff color var(--text-theme-color)
} }
// //
@ -367,7 +379,7 @@
display block display block
cursor pointer cursor pointer
background-color #4E5058 background-color #4E5058
color #ffffff color var(--text-theme-color)
&:hover { &:hover {
background-color #6D6F78 background-color #6D6F78
@ -422,7 +434,7 @@
justify-content center justify-content center
align-items center align-items center
min-height 220px min-height 220px
color #ffffff color var(--text-theme-color)
overflow hidden overflow hidden
.err-msg-container { .err-msg-container {

View File

@ -250,7 +250,7 @@
display: block; display: block;
} }
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover { .page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover {
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */ box-shadow: 0 0 10px rgba(223,71,255,0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */ transform: translateY(-10px); /* 向上移动10像素 */
} }
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image { .page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image {

View File

@ -1,18 +1,18 @@
.page-sd { .page-sd {
background-color: #282c34; // background-color: #282c34;
.inner { .inner {
display: flex; display: flex;
.sd-box { .sd-box {
margin 10px margin 10px
background-color #262626 // background-color #262626
border 1px solid #454545 // border 1px solid #454545
min-width 300px min-width 300px
max-width 300px max-width 300px
padding 10px 10px 20px 10px padding 10px 10px 20px 10px
border-radius 10px border-radius 10px
color #ffffff; color var(--text-theme-color);
font-size 14px font-size 14px
overflow auto overflow auto
@ -20,7 +20,8 @@
font-weight: bold; font-weight: bold;
font-size 20px font-size 20px
text-align center text-align center
color #47fff1 color var( --theme-textcolor-normal)
} }
// //
@ -67,25 +68,24 @@
text-align center text-align center
.el-button { .el-button {
width 100% width 200px
span {
color #2D3A4B
}
} }
} }
} }
.el-form { .el-form {
.el-form-item__label { .el-form-item__label {
color #ffffff color: var(--text-theme-color)
} }
} }
.task-list-box { .task-list-box {
background: var(--chat-bg);
width 100% width 100%
padding 0 10px 10px 10px padding 0 10px 10px 10px
color #ffffff color: var(--text-theme-color)
overflow-x hidden overflow-x hidden
.task-list-inner { .task-list-inner {
@ -108,7 +108,7 @@
} }
.el-textarea { .el-textarea {
--el-input-focus-border-color: #47FFF1; // --el-input-focus-border-color: #47FFF1;
} }
.el-textarea__inner { .el-textarea__inner {
@ -142,7 +142,7 @@
} }
.el-form-item__label { .el-form-item__label {
color #ffffff color: var(--text-theme-color)
} }
// //

View File

@ -9,11 +9,11 @@
.page-images-wall { .page-images-wall {
display: flex; display: flex;
background-color: #282c34; // background-color: #282c34;
.inner { .inner {
width 100% width 100%
color #ffffff color var(--text-theme-color);
overflow hidden overflow hidden
.header { .header {
@ -33,7 +33,7 @@
font-size 16px font-size 16px
.el-radio { .el-radio {
color #ffffff color var(--text-theme-color);
} }
} }
@ -68,7 +68,7 @@
width 100% width 100%
bottom 0 bottom 0
left 0 left 0
color #ffffff color var(--text-theme-color);
padding 8px 10px padding 8px 10px
line-height 1.2 line-height 1.2
border-top-right-radius 10px border-top-right-radius 10px
@ -77,11 +77,15 @@
span { span {
word-break break-all word-break break-all
} }
.iconfont{
}
.el-icon, .iconfont { .el-icon, .iconfont {
top 2px top 2px
cursor pointer cursor pointer
border 1px solid #ffffff color: #fff !important;
border 1px solid #fff !important;
border-radius 5px border-radius 5px
padding 2px padding 2px
font-size 16px; font-size 16px;

View File

@ -1,12 +1,14 @@
.page-luma { .page-luma {
display flex display flex
height 100% height 100%
background-color #0E0808 // background-color #0E0808
// background: var(--chat-bg);
overflow auto overflow auto
//justify-content center //justify-content center
flex-flow column flex-flow column
align-items center align-items center
background: linear-gradient(180deg, rgba(75, 62, 53, 0.8), rgba(144, 50, 181, 0.3)); // background: linear-gradient(180deg, rgba(75, 62, 53, 0.8), rgba(144, 50, 181, 0.3));
.prompt-box { .prompt-box {
@ -50,7 +52,7 @@
.btn-swap { .btn-swap {
margin-right 10px margin-right 10px
.icon-exchange{ .icon-exchange{
color #ffffff color var(--text-theme-color)
cursor pointer cursor pointer
} }
} }
@ -60,18 +62,20 @@
.prompt-container { .prompt-container {
width: 100%; width: 100%;
.input-container { .input-container {
background: linear-gradient(90deg, rgba(75, 62, 53, 0.8), rgba(144, 50, 181, 0.3)); background: var(--chat-bg);
// background: linear-gradient(90deg, rgba(75, 62, 53, 0.8), rgba(144, 50, 181, 0.3));
border-radius: 28px; border-radius: 28px;
padding: 10px 20px; padding: 10px 20px;
display: flex; display: flex;
align-items: center; align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); margin-bottom: 16px;
// box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
.prompt-input { .prompt-input {
background: transparent; background: transparent;
border: none; border: none;
outline: none; outline: none;
color: white; color var(--text-theme-color);
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
padding: 10px; padding: 10px;
@ -82,16 +86,14 @@
overflow-wrap: break-word; overflow-wrap: break-word;
scrollbar-width: none; /* */ scrollbar-width: none; /* */
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
} }
.upload-icon, .send-icon { .upload-icon, .send-icon {
color #e1e1e1 color var( --el-color-primary)
.iconfont { .iconfont {
font-size 20px font-size 20px
cursor pointer cursor pointer
@ -104,7 +106,7 @@
.params { .params {
display flex display flex
justify-content right justify-content right
color #e1e1e1 color var(--text-theme-color);
font-size 14px font-size 14px
padding 10px 30px padding 10px 30px
@ -129,9 +131,9 @@
padding 0 40px padding 0 40px
.h-title { .h-title {
color #ffffff color var(--text-theme-color)
width 100% width 100%
font-size 36px // font-size 36px
text-align left text-align left
} }
@ -147,9 +149,7 @@
cursor pointer cursor pointer
margin-bottom 10px margin-bottom 10px
&:hover {
background-color #2A2525
}
.left { .left {
.container { .container {
@ -189,7 +189,8 @@
border-radius 5px border-radius 5px
background rgba(100, 100, 100, 0.3) background rgba(100, 100, 100, 0.3)
cursor pointer cursor pointer
color #ffffff color var(--text-theme-color)
opacity 0 opacity 0
transform: translate(-50%, 0px); transform: translate(-50%, 0px);
transition opacity 0.3s ease 0s transition opacity 0.3s ease 0s
@ -222,7 +223,8 @@
text-overflow ellipsis text-overflow ellipsis
} }
.prompt { .prompt {
color rgb(250 247 245) color var( --el-text-color-primary)
cursor: text
} }
.failed { .failed {
color #E4696B color #E4696B
@ -248,7 +250,7 @@
.text { .text {
margin-right 10px margin-right 10px
color #e1e1e1 color #000
} }
} }
@ -259,8 +261,9 @@
color #726E6C color #726E6C
&:hover { &:hover {
background #5f5958 // background #5f5958
color #e1e1e1 // color #e1e1e1
color:var(--el-color-primary)
} }
.downloading { .downloading {
@ -317,7 +320,7 @@
// border-radius 20px // border-radius 20px
// padding 3px 15px // padding 3px 15px
// cursor pointer // cursor pointer
// color #ffffff // color var(--text-theme-color)
// font-size 14px // font-size 14px
// //
// .iconfont { // .iconfont {
@ -344,14 +347,14 @@
.btn { .btn {
margin-right 10px margin-right 10px
background-color #363030
border none border none
border-radius 5px border-radius 5px
padding 5px 10px padding 5px 10px
cursor pointer cursor pointer
color #000
&:hover { &:hover {
background-color #5F5958 opacity: 0.7
} }
} }

View File

@ -1,5 +1,5 @@
.page-mark-map { .page-mark-map {
background-color: #282c34; // background-color: #282c34;
height 100% height 100%
.inner { .inner {
@ -7,20 +7,20 @@
.mark-map-box { .mark-map-box {
margin 10px margin 10px
background-color #262626 // background-color #262626
border 1px solid #454545 // border 1px solid #454545
min-width 300px min-width 300px
max-width 300px max-width 300px
padding 10px padding 10px
border-radius 10px border-radius 10px
color #ffffff; color var(--text-theme-color);
font-size 14px font-size 14px
h2 { h2 {
font-weight: bold; font-weight: bold;
font-size 20px font-size 20px
text-align center text-align center
color #47fff1 color var( --theme-textcolor-normal)
} }
// //
@ -43,7 +43,7 @@
width 100% width 100%
span { span {
color #2D3A4B color #fff
} }
} }
@ -61,13 +61,15 @@
.el-form { .el-form {
.el-form-item__label { .el-form-item__label {
color #ffffff color var(--text-theme-color)
} }
} }
.chat-box { .chat-box {
width 100% width 100%
background: var(--chat-bg);
.top-bar { .top-bar {
display flex display flex
@ -77,13 +79,13 @@
} }
.markdown { .markdown {
color #ffffff color var(--text-theme-color)
display flex display flex
justify-content center justify-content center
align-items center align-items center
h1 { h1 {
color: #47fff1; color: var( --theme-textcolor-normal);
} }
h2 { h2 {
@ -116,7 +118,7 @@
.markmap { .markmap {
width 100% width 100%
color #ffffff color var(--text-theme-color)
font-size 12px font-size 12px
.markmap-foreign { .markmap-foreign {
@ -139,7 +141,7 @@
.mm-toolbar-item { .mm-toolbar-item {
cursor pointer cursor pointer
color var(--el-color-white) color var( --el-text-color-primary)
} }
} }

View File

@ -4,10 +4,11 @@
list-style: normal; list-style: normal;
} }
a { a {
color: #42b983;
font-weight: 600; color :var(--a-link-color);
text-decoration: underline;
padding: 0 2px; padding: 0 2px;
text-decoration: none;
} }
h1, h1,

View File

@ -141,7 +141,7 @@
margin-right: 5px; margin-right: 5px;
} }
.member .inner .product-box .list-box .product-item:hover { .member .inner .product-box .list-box .product-item:hover {
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */ box-shadow: 0 0 10px var(--shadow-color); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */ transform: translateY(-10px); /* 向上移动10像素 */
} }
.member .inner .product-box .headline { .member .inner .product-box .headline {

View File

@ -1,18 +1,18 @@
.member { .member {
background-color: #282c34; // background-color: #282c34;
height 100% height 100%
.title { .title {
text-align center text-align center
background-color #25272d background-color #25272d
font-size 24px font-size 24px
color #ffffff color var(--text-theme-color)
padding 10px padding 10px
border-bottom 1px solid #3c3c3c border-bottom 1px solid #3c3c3c
} }
.inner { .inner {
color #ffffff color var(--text-theme-color)
padding 15px 0 15px 15px; padding 15px 0 15px 15px;
overflow-x hidden overflow-x hidden
overflow-y visible overflow-y visible
@ -22,13 +22,13 @@
.user-profile { .user-profile {
padding 10px 20px 20px 20px padding 10px 20px 20px 20px
width 300px width 300px
background-color #393F4A background-color var(--chat-bg)
color #ffffff color var(--text-theme-color)
border-radius 10px border-radius 10px
//height 100vh //height 100vh
.el-form-item__label { .el-form-item__label {
color #ffffff color var(--text-theme-color)
justify-content start justify-content start
} }
@ -58,7 +58,8 @@
.list-box { .list-box {
.product-item { .product-item {
border 1px solid #666666 // border 1px solid #666666
background-color var(--chat-bg)
border-radius 6px border-radius 6px
overflow hidden overflow hidden
cursor pointer cursor pointer
@ -87,7 +88,7 @@
text-align center text-align center
font-size 16px font-size 16px
font-weight bold font-weight bold
color #47fff1 color var( --el-color-primary)
} }
} }
@ -138,14 +139,14 @@
padding 0 padding 0
.icon-alipay,.icon-wechat-pay { .icon-alipay,.icon-wechat-pay {
color #ffffff color var(--text-theme-color)
} }
.icon-qq { .icon-qq {
color #15A6E8 color #15A6E8
font-size 24px font-size 24px
} }
.icon-jd-pay { .icon-jd-pay {
color #ffffff color var(--text-theme-color)
font-size 24px font-size 24px
} }
.icon-douyin { .icon-douyin {
@ -161,7 +162,7 @@
} }
&:hover { &:hover {
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* */ // box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* */
transform: translateY(-10px); /* 10 */ transform: translateY(-10px); /* 10 */
} }
} }

View File

@ -12,7 +12,7 @@
height 200px height 200px
overflow hidden overflow hidden
padding 2px padding 2px
background-color #555555 background-color var( --gray-btn-bg)
.job-item-inner { .job-item-inner {
position relative position relative

View File

@ -1,7 +1,7 @@
.page-suno { .page-suno {
display flex display flex
height 100% height 100%
background-color #0E0808 // background-color #0E0808
overflow auto overflow auto
.left-bar { .left-bar {
@ -24,7 +24,7 @@
.params { .params {
padding 20px 0 padding 20px 0
color rgb(250 247 245) color: var(--text-theme-color);
position relative position relative
.pure-music { .pure-music {
@ -77,6 +77,7 @@
opacity: 0.9; opacity: 0.9;
} }
} }
.song { .song {
display flex display flex
@ -137,6 +138,8 @@
bottom 10px bottom 10px
font-size 12px font-size 12px
padding 2px 5px padding 2px 5px
background-color var(--sm-btn-bg)
color: #fff
} }
} }
@ -154,12 +157,16 @@
.tag { .tag {
margin-right 10px margin-right 10px
word-break keep-all word-break keep-all
background-color #312C2C background: var(--card-bg);
color #e1e1e1 color:var(--theme-text-color-primary);
border-radius 5px opacity 0.7
border-radius 8px
padding 3px 6px padding 3px 6px
cursor pointer cursor pointer
font-size 13px font-size 13px
&:hover{
color:var( --el-color-primary)
}
} }
} }
} }
@ -169,6 +176,8 @@
width 100% width 100%
color rgb(250 247 245) color rgb(250 247 245)
overflow auto overflow auto
background: var(--chat-bg)
.list-box { .list-box {
padding 0 0 0 20px padding 0 0 0 20px
@ -180,7 +189,7 @@
margin-bottom 10px margin-bottom 10px
&:hover { &:hover {
background-color #2A2525 background: rgba(188,149,236,0.08)
} }
.left { .left {
@ -247,18 +256,18 @@
font-weight 700 font-weight 700
a { a {
color rgb(250 247 245) color vae( --a-link-color)
&:hover { &:hover {
text-decoration underline text-decoration underline
} }
} }
.model { .model {
color #E2E8F0 color #8f8f8f
background-color #1C1616 // background-color #1C1616
border 1px solid #8f8f8f // border 1px solid #8f8f8f
font-weight normal font-weight normal
font-size 14px font-size 12px
padding 1px 3px padding 1px 3px
border-radius 5px border-radius 5px
margin-left 10px margin-left 10px
@ -271,7 +280,7 @@
.tags { .tags {
font-size 14px font-size 14px
color #d1d1d1 color var(--el-text-color-primary)
padding 3px 0 padding 3px 0
} }
} }
@ -303,8 +312,9 @@
color #726E6C color #726E6C
&:hover { &:hover {
background #5f5958 // background #5f5958
color #e1e1e1 // color #e1e1e1
color:var(--el-color-primary)
} }
.downloading { .downloading {
@ -372,14 +382,25 @@
.btn { .btn {
margin-right 10px margin-right 10px
background-color #363030 color: #000
border none border none
border-radius 5px border-radius 5px
padding 5px 10px padding 5px 10px
cursor pointer cursor pointer
&:hover { &:hover {
background-color #5F5958 opacity :0.8
} }
} }
}
.submit-btn {
display flex
align-items: center
margin: 20px 0
justify-content: center;
.el-button {
width 200px
}
} }

View File

@ -20,7 +20,9 @@
} }
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active:rgba(255, 255, 255, 0.1); --border-active:rgba(255, 255, 255, 0.1);
--card-bg: rgba(17, 28, 68, 1); // --card-bg: rgba(17, 28, 68, 1);
--card-bg: #1f243f;
--card-bg-table: rgba(17, 28, 68, 1);
--theme-bg:rgb(13, 20, 53); --theme-bg:rgb(13, 20, 53);
--theme-bg-all:rgb(13, 20, 53); --theme-bg-all:rgb(13, 20, 53);
--sign-bg: rgba(27, 37, 75, 1); --sign-bg: rgba(27, 37, 75, 1);
@ -28,7 +30,7 @@
--text-color-primary: #d1c7ff; --text-color-primary: #d1c7ff;
--el-text-color-regular: rgba(163, 174, 208, 1) --el-text-color-regular: rgba(163, 174, 208, 1)
--el-border-color:rgb(79, 80, 85) --el-border-color:rgb(79, 80, 85)
--el-text-color-primary: #fff; --el-text-color-primary: #fff;//
--el-bg-color-overlay: rgba(17, 28, 68, 1); --el-bg-color-overlay: rgba(17, 28, 68, 1);
--el-border-color-light: rgba(255, 255, 255, 0.2); --el-border-color-light: rgba(255, 255, 255, 0.2);
--line-box:rgba(255, 255, 255, 0.1); --line-box:rgba(255, 255, 255, 0.1);
@ -39,6 +41,7 @@
--el-color-primary-light-9:rgba(86, 86, 95, .2); --el-color-primary-light-9:rgba(86, 86, 95, .2);
--chat-wel-bg:#2d2f388a; --chat-wel-bg:#2d2f388a;
--theme-text-color-secondary: #a3aed0; --theme-text-color-secondary: #a3aed0;
// --el-pagination-button-bg-color: rgba(86,86,95,0.2);
//layout //layout
.more-menus li.moreTitle, .more-menus li.moreTitle,
@ -52,9 +55,14 @@
.more-menus span.title{ .more-menus span.title{
color:#000; color:#000;
} }
.menu-list .menu-list-item.active .el-icon{
background: #0080006e;
}
--theme-text-color-primary: #fff; --theme-text-color-primary: #fff;
--theme-text-primary: #f3f3f3; --theme-text-primary: #f3f3f3;
--chat-content-bg:rgba(86, 86, 95, .2); --chat-content-bg:rgba(86, 86, 95, .2);
--chat-content-bg-list:rgba(86, 86, 95, .2); --chat-content-bg-list:rgba(86, 86, 95, .2);
// --theme-text-tertiary: #e1e1e1;
} }

View File

@ -45,7 +45,7 @@
--chat-list-bg: #0302020a; --chat-list-bg: #0302020a;
--chat-content-bg-list:#fff; --chat-content-bg-list:#fff;
--chat-wel-bg:rgba(247, 247, 248, 1); --chat-wel-bg:rgba(247, 247, 248, 1);
--el-pagination-button-bg-color: rgba(86,86,95,0.2);
} }

View File

@ -73,7 +73,7 @@
.animate { .animate {
&:hover { &:hover {
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* */ box-shadow: 0 0 10px var(--shadow-color); /* */
transform: translateY(-10px); /* 10 */ transform: translateY(-10px); /* 10 */
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -256,7 +256,7 @@ const reGenerate = (prompt) => {
code { code {
color:var(--theme-text-color-primary); color:var(--theme-text-color-primary);
background-color #e7e7e8 background-color var(--el-color-primary-light-3)
padding 0 3px; padding 0 3px;
border-radius 5px; border-radius 5px;
} }
@ -348,7 +348,7 @@ const reGenerate = (prompt) => {
padding 10px 10px 10px 0; padding 10px 10px 10px 0;
.bar-item { .bar-item {
background-color #e7e7e8; background-color var( --little-btn-bg);
color #888 color #888
padding 3px 5px; padding 3px 5px;
margin-right 10px; margin-right 10px;
@ -433,7 +433,7 @@ const reGenerate = (prompt) => {
code { code {
color:var(--theme-text-color-primary); color:var(--theme-text-color-primary);
background-color #e7e7e8 background-color var( --little-btn-bg)
padding 0 3px; padding 0 3px;
border-radius 5px; border-radius 5px;
} }
@ -539,7 +539,7 @@ const reGenerate = (prompt) => {
} }
.bar-item.bg { .bar-item.bg {
background-color #e7e7e8 background-color var( --gray-btn-bg)
cursor pointer cursor pointer
} }

View File

@ -1,76 +1,90 @@
<template> <template>
<div class="invite-list" v-loading="loading"> <div class="invite-list" v-loading="loading">
<el-row v-if="items.length > 0"> <el-row v-if="items.length > 0">
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border <el-table
style="--el-table-border-color:#373C47; :data="items"
--el-table-tr-bg-color:#2D323B; :row-key="(row) => row.id"
--el-table-row-hover-bg-color:#373C47; table-layout="auto"
--el-table-header-bg-color:#474E5C; border
--el-table-text-color:#d1d1d1"> style="
<el-table-column prop="username" label="用户"/> --el-table-border-color: #373c47;
<el-table-column prop="invite_code" label="邀请码"/> --el-table-tr-bg-color: #2d323b;
<el-table-column prop="remark" label="邀请奖励"/> --el-table-row-hover-bg-color: #373c47;
--el-table-header-bg-color: #474e5c;
--el-table-text-color: #d1d1d1;
"
>
<el-table-column prop="username" label="用户" />
<el-table-column prop="invite_code" label="邀请码" />
<el-table-column prop="remark" label="邀请奖励" />
<el-table-column label="注册时间"> <el-table-column label="注册时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span> <span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-row> </el-row>
<el-empty :image-size="100" v-else/> <el-empty :image-size="100" :image="nodata" description="暂无数据" v-else />
<div class="pagination"> <div class="pagination">
<el-pagination v-if="total > 0" background <el-pagination
layout="total,prev, pager, next" v-if="total > 0"
:hide-on-single-page="true" background
v-model:current-page="page" layout="total,prev, pager, next"
v-model:page-size="pageSize" :hide-on-single-page="true"
@current-change="fetchData()" v-model:current-page="page"
:total="total"/> v-model:page-size="pageSize"
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
@current-change="fetchData()"
:total="total"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import nodata from "@/assets/img/no-data.png";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import { onMounted, ref } from "vue";
import {dateFormat} from "@/utils/libs"; import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/libs";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
const items = ref([]) const items = ref([]);
const total = ref(0) const total = ref(0);
const page = ref(1) const page = ref(1);
const pageSize = ref(10) const pageSize = ref(10);
const loading = ref(true) const loading = ref(true);
onMounted(() => { onMounted(() => {
fetchData() fetchData();
const clipboard = new Clipboard('.copy-order-no'); const clipboard = new Clipboard(".copy-order-no");
clipboard.on('success', () => { clipboard.on("success", () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) });
clipboard.on('error', () => { clipboard.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
}) });
// //
const fetchData = () => { const fetchData = () => {
httpGet('/api/invite/list', {page: page.value, page_size: pageSize.value}).then((res) => { httpGet("/api/invite/list", { page: page.value, page_size: pageSize.value })
if (res.data) { .then((res) => {
items.value = res.data.items if (res.data) {
total.value = res.data.total items.value = res.data.items;
page.value = res.data.page total.value = res.data.total;
pageSize.value = res.data.page_size page.value = res.data.page;
} pageSize.value = res.data.page_size;
loading.value = false }
}).catch(e => { loading.value = false;
ElMessage.error("获取数据失败:" + e.message); })
}) .catch((e) => {
} ElMessage.error("获取数据失败:" + e.message);
});
};
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
@ -90,4 +104,4 @@ const fetchData = () => {
color #20a0ff color #20a0ff
} }
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-container class="realtime-conversation" :style="{height: height}"> <el-container class="realtime-conversation" :style="{ height: height }">
<!-- connection animation --> <!-- connection animation -->
<el-container class="connection-container" v-if="!isConnected"> <el-container class="connection-container" v-if="!isConnected">
<div class="phone-container"> <div class="phone-container">
@ -36,14 +36,18 @@
</div> </div>
</div> </div>
<div class="call-controls"> <div class="call-controls">
<el-tooltip content="长按发送语音" placement="top" effect="light"> <el-tooltip content="长按发送语音" placement="top">
<ripple-button> <ripple-button>
<button class="call-button answer" @mousedown="startRecording" @mouseup="stopRecording"> <button
class="call-button answer"
@mousedown="startRecording"
@mouseup="stopRecording"
>
<i class="iconfont icon-mic-bold"></i> <i class="iconfont icon-mic-bold"></i>
</button> </button>
</ripple-button> </ripple-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="结束通话" placement="top" effect="light"> <el-tooltip content="结束通话" placement="top">
<button class="call-button hangup" @click="hangUp"> <button class="call-button hangup" @click="hangUp">
<i class="iconfont icon-hung-up"></i> <i class="iconfont icon-hung-up"></i>
</button> </button>
@ -51,32 +55,31 @@
</div> </div>
</div> </div>
</el-container> </el-container>
</template> </template>
<script setup> <script setup>
import RippleButton from "@/components/ui/RippleButton.vue"; import RippleButton from "@/components/ui/RippleButton.vue";
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from "vue";
import { RealtimeClient } from '@openai/realtime-api-beta'; import { RealtimeClient } from "@openai/realtime-api-beta";
import { WavRecorder, WavStreamPlayer } from '@/lib/wavtools/index.js'; import { WavRecorder, WavStreamPlayer } from "@/lib/wavtools/index.js";
import { instructions } from '@/utils/conversation_config.js'; import { instructions } from "@/utils/conversation_config.js";
import { WavRenderer } from '@/utils/wav_renderer'; import { WavRenderer } from "@/utils/wav_renderer";
import {showMessageError} from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
import {getUserToken} from "@/store/session"; import { getUserToken } from "@/store/session";
// eslint-disable-next-line no-unused-vars,no-undef // eslint-disable-next-line no-unused-vars,no-undef
const props = defineProps({ const props = defineProps({
height: { height: {
type: String, type: String,
default: '100vh' default: "100vh"
} }
}) });
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const emits = defineEmits(['close']); const emits = defineEmits(["close"]);
/********************** connection animation code *************************/ /********************** connection animation code *************************/
const fullText = "正在接通中..."; const fullText = "正在接通中...";
const connectingText = ref("") const connectingText = ref("");
let index = 0; let index = 0;
const typeText = () => { const typeText = () => {
if (index < fullText.length) { if (index < fullText.length) {
@ -85,12 +88,12 @@ const typeText = () => {
setTimeout(typeText, 200); // 300 setTimeout(typeText, 200); // 300
} else { } else {
setTimeout(() => { setTimeout(() => {
connectingText.value = ''; connectingText.value = "";
index = 0; index = 0;
typeText(); typeText();
}, 1000); // 1 }, 1000); // 1
} }
} };
/*************************** end of code ****************************************/ /*************************** end of code ****************************************/
/********************** conversation process code ***************************/ /********************** conversation process code ***************************/
@ -102,31 +105,29 @@ const animateVoice = () => {
rightVoiceActive.value = Math.random() > 0.5; rightVoiceActive.value = Math.random() > 0.5;
}; };
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 })); const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }));
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 })); const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }));
let host = process.env.VUE_APP_WS_HOST let host = process.env.VUE_APP_WS_HOST;
if (host === '') { if (host === "") {
if (location.protocol === 'https:') { if (location.protocol === "https:") {
host = 'wss://' + location.host; host = "wss://" + location.host;
} else { } else {
host = 'ws://' + location.host; host = "ws://" + location.host;
} }
} }
const client = ref( const client = ref(
new RealtimeClient({ new RealtimeClient({
url: `${host}/api/realtime`, url: `${host}/api/realtime`,
apiKey: getUserToken(), apiKey: getUserToken(),
dangerouslyAllowAPIKeyInBrowser: true, dangerouslyAllowAPIKeyInBrowser: true
}) })
); );
// // Set up client instructions and transcription // // Set up client instructions and transcription
client.value.updateSession({ client.value.updateSession({
instructions: instructions, instructions: instructions,
turn_detection: null, turn_detection: null,
input_audio_transcription: { model: 'whisper-1' }, input_audio_transcription: { model: "whisper-1" },
voice: 'alloy', voice: "alloy"
}); });
// set voice wave canvas // set voice wave canvas
@ -137,62 +138,66 @@ const isRecording = ref(false);
const backgroundAudio = ref(null); const backgroundAudio = ref(null);
const hangUpAudio = ref(null); const hangUpAudio = ref(null);
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
const connect = async () => { const connect = async () => {
if (isConnected.value) { if (isConnected.value) {
return return;
} }
// //
if (backgroundAudio.value) { if (backgroundAudio.value) {
backgroundAudio.value.play().catch(error => { backgroundAudio.value.play().catch((error) => {
console.error('播放失败,可能是浏览器的自动播放策略导致的:', error); console.error("播放失败,可能是浏览器的自动播放策略导致的:", error);
}); });
} }
// //
await sleep(3000) await sleep(3000);
try { try {
await client.value.connect(); await client.value.connect();
await wavRecorder.value.begin(); await wavRecorder.value.begin();
await wavStreamPlayer.value.connect(); await wavStreamPlayer.value.connect();
console.log("对话连接成功!") console.log("对话连接成功!");
if (!client.value.isConnected()) { if (!client.value.isConnected()) {
return return;
} }
isConnected.value = true; isConnected.value = true;
backgroundAudio.value?.pause() backgroundAudio.value?.pause();
backgroundAudio.value.currentTime = 0 backgroundAudio.value.currentTime = 0;
client.value.sendUserMessageContent([ client.value.sendUserMessageContent([
{ {
type: 'input_text', type: "input_text",
text: '你好,我是极客学长!', text: "你好,我是极客学长!"
}, }
]); ]);
if (client.value.getTurnDetectionType() === 'server_vad') { if (client.value.getTurnDetectionType() === "server_vad") {
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono)); await wavRecorder.value.record((data) =>
client.value.appendInputAudio(data.mono)
);
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
}; };
// //
const startRecording = async () => { const startRecording = async () => {
if (isRecording.value) { if (isRecording.value) {
return return;
} }
isRecording.value = true; isRecording.value = true;
try { try {
const trackSampleOffset = await wavStreamPlayer.value.interrupt(); const trackSampleOffset = await wavStreamPlayer.value.interrupt();
if (trackSampleOffset?.trackId) { if (trackSampleOffset?.trackId) {
const { trackId, offset } = trackSampleOffset; const { trackId, offset } = trackSampleOffset;
client.value.cancelResponse(trackId, offset); client.value.cancelResponse(trackId, offset);
} }
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono)); await wavRecorder.value.record((data) =>
client.value.appendInputAudio(data.mono)
);
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
}; };
@ -203,7 +208,7 @@ const stopRecording = async () => {
await wavRecorder.value.pause(); await wavRecorder.value.pause();
client.value.createResponse(); client.value.createResponse();
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
}; };
@ -232,13 +237,13 @@ const initialize = async () => {
canvas.width = canvas.offsetWidth; canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight; canvas.height = canvas.offsetHeight;
} }
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const result = wavRecorder.value.recording const result = wavRecorder.value.recording
? wavRecorder.value.getFrequencies('voice') ? wavRecorder.value.getFrequencies("voice")
: { values: new Float32Array([0]) }; : { values: new Float32Array([0]) };
WavRenderer.drawBars(canvas, ctx, result.values, '#0099ff', 10, 0, 8); WavRenderer.drawBars(canvas, ctx, result.values, "#0099ff", 10, 0, 8);
} }
} }
if (serverCanvasRef.value) { if (serverCanvasRef.value) {
@ -247,13 +252,13 @@ const initialize = async () => {
canvas.width = canvas.offsetWidth; canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight; canvas.height = canvas.offsetHeight;
} }
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const result = wavStreamPlayer.value.analyser const result = wavStreamPlayer.value.analyser
? wavStreamPlayer.value.getFrequencies('voice') ? wavStreamPlayer.value.getFrequencies("voice")
: { values: new Float32Array([0]) }; : { values: new Float32Array([0]) };
WavRenderer.drawBars(canvas, ctx, result.values, '#009900', 10, 0, 8); WavRenderer.drawBars(canvas, ctx, result.values, "#009900", 10, 0, 8);
} }
} }
requestAnimationFrame(render); requestAnimationFrame(render);
@ -261,17 +266,17 @@ const initialize = async () => {
}; };
render(); render();
client.value.on('error', (event) => { client.value.on("error", (event) => {
showMessageError(event.error) showMessageError(event.error);
}); });
client.value.on('realtime.event', (re) => { client.value.on("realtime.event", (re) => {
if (re.event.type === 'error') { if (re.event.type === "error") {
showMessageError(re.event.error) showMessageError(re.event.error);
} }
}); });
client.value.on('conversation.interrupted', async () => { client.value.on("conversation.interrupted", async () => {
const trackSampleOffset = await wavStreamPlayer.value.interrupt(); const trackSampleOffset = await wavStreamPlayer.value.interrupt();
if (trackSampleOffset?.trackId) { if (trackSampleOffset?.trackId) {
const { trackId, offset } = trackSampleOffset; const { trackId, offset } = trackSampleOffset;
@ -279,21 +284,20 @@ const initialize = async () => {
} }
}); });
client.value.on('conversation.updated', async ({ item, delta }) => { client.value.on("conversation.updated", async ({ item, delta }) => {
// console.log('item updated', item, delta) // console.log('item updated', item, delta)
if (delta?.audio) { if (delta?.audio) {
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id); wavStreamPlayer.value.add16BitPCM(delta.audio, item.id);
} }
}); });
};
}
const voiceInterval = ref(null); const voiceInterval = ref(null);
onMounted(() => { onMounted(() => {
initialize() initialize();
// //
voiceInterval.value = setInterval(animateVoice, 200); voiceInterval.value = setInterval(animateVoice, 200);
typeText() typeText();
}); });
onUnmounted(() => { onUnmounted(() => {
@ -304,32 +308,31 @@ onUnmounted(() => {
// //
const hangUp = async () => { const hangUp = async () => {
try { try {
isConnected.value = false isConnected.value = false;
// //
if (backgroundAudio.value?.currentTime) { if (backgroundAudio.value?.currentTime) {
backgroundAudio.value?.pause() backgroundAudio.value?.pause();
backgroundAudio.value.currentTime = 0 backgroundAudio.value.currentTime = 0;
} }
// //
client.value.reset() client.value.reset();
// //
await wavRecorder.value.end() await wavRecorder.value.end();
await wavStreamPlayer.value.interrupt() await wavStreamPlayer.value.interrupt();
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} finally { } finally {
// //
hangUpAudio.value?.play() hangUpAudio.value?.play();
emits('close') emits("close");
} }
}; };
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
defineExpose({ connect,hangUp }); defineExpose({ connect, hangUp });
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
@import "@/assets/css/realtime.styl" @import "@/assets/css/realtime.styl"
</style>
</style>

View File

@ -3,12 +3,14 @@
<div class="running-job-box" v-if="list.length > 0"> <div class="running-job-box" v-if="list.length > 0">
<div class="job-item" v-for="item in list" :key="item.id"> <div class="job-item" v-for="item in list" :key="item.id">
<div v-if="item.progress > 0" class="job-item-inner"> <div v-if="item.progress > 0" class="job-item-inner">
<div class="progress" v-if="item.progress > 0">
<div class="progress" v-if="item.progress > 0"> <el-progress
<el-progress type="circle" :percentage="item.progress" :width="100" type="circle"
color="#47fff1"/> :percentage="item.progress"
</div> :width="100"
color="#47fff1"
/>
</div>
</div> </div>
<el-image fit="cover" v-else> <el-image fit="cover" v-else>
<template #error> <template #error>
@ -20,21 +22,22 @@
</el-image> </el-image>
</div> </div>
</div> </div>
<el-empty :image-size="100" v-else/> <el-empty :image-size="100" v-else :image="nodata" description="暂无任务" />
</div> </div>
</template> </template>
<script setup> <script setup>
import nodata from "@/assets/img/no-data.png";
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const props = defineProps({ const props = defineProps({
list: { list: {
type: Array, type: Array,
default:[], default: []
} }
}) });
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
@import "~@/assets/css/running-job-list.styl" @import "~@/assets/css/running-job-list.styl"
</style> </style>

View File

@ -1,95 +1,111 @@
<template> <template>
<div class="user-bill" v-loading="loading" element-loading-background="rgba(255,255,255,.3)"> <div
class="user-bill"
v-loading="loading"
element-loading-background="rgba(255,255,255,.3)"
>
<el-row v-if="items.length > 0"> <el-row v-if="items.length > 0">
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border <el-table
style="--el-table-border-color:#373C47; :data="items"
--el-table-tr-bg-color:#2D323B; :row-key="(row) => row.id"
--el-table-row-hover-bg-color:#373C47; table-layout="auto"
--el-table-header-bg-color:#474E5C; border
--el-table-text-color:#d1d1d1"> >
<el-table-column prop="order_no" label="订单号"> <el-table-column prop="order_no" label="订单号">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.order_no }}</span> <span>{{ scope.row.order_no }}</span>
<el-icon class="copy-order-no" :data-clipboard-text="scope.row.order_no"> <el-icon
<DocumentCopy/> class="copy-order-no"
:data-clipboard-text="scope.row.order_no"
>
<DocumentCopy />
</el-icon> </el-icon>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="subject" label="产品名称"/> <el-table-column prop="subject" label="产品名称" />
<el-table-column prop="amount" label="订单金额"/> <el-table-column prop="amount" label="订单金额" />
<el-table-column label="订单算力"> <el-table-column label="订单算力">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.remark?.power }}</span> <span>{{ scope.row.remark?.power }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="pay_method" label="支付渠道"/> <el-table-column prop="pay_method" label="支付渠道" />
<el-table-column prop="pay_name" label="支付名称"/> <el-table-column prop="pay_name" label="支付名称" />
<el-table-column label="支付时间"> <el-table-column label="支付时间">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span> <span v-if="scope.row['pay_time']">{{
dateFormat(scope.row["pay_time"])
}}</span>
<el-tag v-else>未支付</el-tag> <el-tag v-else>未支付</el-tag>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-row> </el-row>
<el-empty :image-size="100" v-else/> <el-empty :image-size="100" v-else />
<div class="pagination"> <div class="pagination">
<el-pagination v-if="total > 0" background <el-pagination
layout="total,prev, pager, next" v-if="total > 0"
:hide-on-single-page="true" background
v-model:current-page="page" layout="total,prev, pager, next"
v-model:page-size="pageSize" :hide-on-single-page="true"
@current-change="fetchData()" v-model:current-page="page"
:total="total"/> v-model:page-size="pageSize"
@current-change="fetchData()"
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
:total="total"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import { onMounted, ref } from "vue";
import {httpGet} from "@/utils/http"; import { httpGet } from "@/utils/http";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {dateFormat} from "@/utils/libs"; import { dateFormat } from "@/utils/libs";
import {DocumentCopy} from "@element-plus/icons-vue"; import { DocumentCopy } from "@element-plus/icons-vue";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
const items = ref([]) const items = ref([]);
const total = ref(0) const total = ref(0);
const page = ref(1) const page = ref(1);
const pageSize = ref(12) const pageSize = ref(12);
const loading = ref(true) const loading = ref(true);
onMounted(() => { onMounted(() => {
fetchData() fetchData();
const clipboard = new Clipboard('.copy-order-no'); const clipboard = new Clipboard(".copy-order-no");
clipboard.on('success', () => { clipboard.on("success", () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) });
clipboard.on('error', () => { clipboard.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
}) });
// //
const fetchData = () => { const fetchData = () => {
httpGet('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => { httpGet("/api/order/list", { page: page.value, page_size: pageSize.value })
if (res.data) { .then((res) => {
items.value = res.data.items if (res.data) {
total.value = res.data.total items.value = res.data.items;
page.value = res.data.page total.value = res.data.total;
pageSize.value = res.data.page_size page.value = res.data.page;
} pageSize.value = res.data.page_size;
loading.value = false }
}).catch(e => { loading.value = false;
ElMessage.error("获取数据失败:" + e.message); })
}) .catch((e) => {
} ElMessage.error("获取数据失败:" + e.message);
});
};
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.user-bill { .user-bill {
background-color: var(--chat-bg);
.pagination { .pagination {
margin: 20px 0 0 0; margin: 20px 0 0 0;
display: flex; display: flex;
@ -105,4 +121,4 @@ const fetchData = () => {
color #20a0ff color #20a0ff
} }
} }
</style> </style>

View File

@ -3,77 +3,92 @@
<el-form :model="user" label-width="100px"> <el-form :model="user" label-width="100px">
<el-row> <el-row>
<el-upload <el-upload
class="avatar-uploader" class="avatar-uploader"
:auto-upload="true" :auto-upload="true"
:show-file-list="false" :show-file-list="false"
:http-request="afterRead" :http-request="afterRead"
accept=".png,.jpg,.jpeg,.bmp" accept=".png,.jpg,.jpeg,.bmp"
> >
<el-avatar v-if="user.avatar" :src="user.avatar" shape="circle" :size="100"/> <el-avatar
v-if="user.avatar"
:src="user.avatar"
shape="circle"
:size="100"
/>
<el-icon v-else class="avatar-uploader-icon"> <el-icon v-else class="avatar-uploader-icon">
<Plus/> <Plus />
</el-icon> </el-icon>
</el-upload> </el-upload>
</el-row> </el-row>
<el-form-item label="昵称"> <el-form-item label="昵称">
<el-input v-model="user['nickname']"/> <el-input v-model="user['nickname']" />
</el-form-item> </el-form-item>
<el-form-item label="账号"> <el-form-item label="账号">
<span>{{ user.username }}</span> <div class="flex">
<el-tooltip <span>{{ user.username }}</span>
<el-tooltip
class="box-item" class="box-item"
effect="light"
content="您已经是 VIP 会员" content="您已经是 VIP 会员"
placement="right" placement="right"
> >
<span class="vip-icon"><el-image v-if="user.vip" :src="vipImg" style="height: 25px;margin-left: 10px"/></span> <span class="vip-icon"
</el-tooltip> ><el-image
v-if="user.vip"
:src="vipImg"
style="height: 25px; margin-left: 10px"
/></span>
</el-tooltip>
</div>
</el-form-item> </el-form-item>
<el-form-item label="剩余算力"> <el-form-item label="剩余算力">
<el-tag>{{ user['power'] }}</el-tag> <el-text type="warning">{{ user["power"] }}</el-text>
</el-form-item> </el-form-item>
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0"> <el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag> <el-tag type="danger">{{ dateFormat(user["expired_time"]) }}</el-tag>
</el-form-item> </el-form-item>
<el-row class="opt-line"> <el-row class="opt-line">
<el-button color="#47fff1" :dark="false" @click="save">保存</el-button> <el-button :dark="false" type="primary" @click="save">保存</el-button>
</el-row> </el-row>
</el-form> </el-form>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import { onMounted, ref } from "vue";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {Plus} from "@element-plus/icons-vue"; import { Plus } from "@element-plus/icons-vue";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import {dateFormat} from "@/utils/libs"; import { dateFormat } from "@/utils/libs";
import {checkSession} from "@/store/cache"; import { checkSession } from "@/store/cache";
const user = ref({ const user = ref({
vip: false, vip: false,
username: '演示数据', username: "演示数据",
nickname: '演示数据', nickname: "演示数据",
avatar: '/images/vip.png', avatar: "/images/menu/member.png",
mobile: '演示数据', mobile: "演示数据",
power: 99999, power: 99999
}) });
const vipImg = ref("/images/vip.png") const vipImg = ref("/images/menu/member.png");
onMounted(() => { onMounted(() => {
checkSession().then(() => { checkSession()
// .then(() => {
httpGet('/api/user/profile').then(res => { //
user.value = res.data httpGet("/api/user/profile")
}).catch(e => { .then((res) => {
ElMessage.error("获取用户信息失败:" + e.message) user.value = res.data;
})
.catch((e) => {
ElMessage.error("获取用户信息失败:" + e.message);
});
})
.catch((e) => {
console.log(e);
}); });
}).catch(e => { });
console.log(e)
})
})
const afterRead = (file) => { const afterRead = (file) => {
// //
@ -81,28 +96,32 @@ const afterRead = (file) => {
quality: 0.6, quality: 0.6,
success(result) { success(result) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', result, result.name); formData.append("file", result, result.name);
// //
httpPost('/api/upload', formData).then((res) => { httpPost("/api/upload", formData)
user.value.avatar = res.data.url .then((res) => {
ElMessage.success({message: "上传成功", duration: 500}) user.value.avatar = res.data.url;
}).catch((e) => { ElMessage.success({ message: "上传成功", duration: 500 });
ElMessage.error('图片上传失败:' + e.message) })
}) .catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
});
}, },
error(err) { error(err) {
console.log(err.message); console.log(err.message);
}, }
}); });
}; };
const save = () => { const save = () => {
httpPost('/api/user/profile/update', user.value).then(() => { httpPost("/api/user/profile/update", user.value)
ElMessage.success({message: '更新成功', duration: 500}) .then(() => {
}).catch((e) => { ElMessage.success({ message: "更新成功", duration: 500 });
ElMessage.error('更新失败:' + e.message) })
}) .catch((e) => {
} ElMessage.error("更新失败:" + e.message);
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@ -127,4 +146,4 @@ const save = () => {
} }
} }
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<div class="container"> <div class="container">
<h1 class="title">{{ title }}-{{ version }}</h1> <h2 class="title">{{ title }}-{{ version }}</h2>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
@ -128,10 +128,11 @@ const send = (text) => {
width 100% width 100%
.title { .title {
font-size: 2.25rem // font-size: 2.25rem
line-height: 2.5rem line-height: 2.5rem
font-weight 600 font-weight 600
margin-bottom: 4rem margin-bottom: 4rem
color var( --theme-textcolor-normal)
} }
.grid-content { .grid-content {

View File

@ -1,57 +1,58 @@
<template> <template>
<div class="black-input-wrapper"> <div class="black-input-wrapper">
<el-input v-model="model" :type="type" :rows="rows" <el-input
@input="onInput" v-model="model"
style="--el-input-bg-color:#252020; :type="type"
--el-input-border-color:#414141; :rows="rows"
--el-input-focus-border-color:#414141; @input="onInput"
--el-text-color-regular: #f1f1f1; resize="none"
--el-input-border-radius: 10px; :placeholder="placeholder"
--el-border-color-hover:#616161" :maxlength="maxlength"
resize="none" />
:placeholder="placeholder" :maxlength="maxlength"/>
<div class="word-stat" v-if="rows > 1"> <div class="word-stat" v-if="rows > 1">
<span>{{value.length}}</span>/<span>{{maxlength}}</span> <span>{{ value.length }}</span
>/<span>{{ maxlength }}</span>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch } from "vue";
import {ref, watch} from "vue";
const props = defineProps({ const props = defineProps({
value : { value: {
type: String, type: String,
default: '', default: ""
}, },
placeholder: { placeholder: {
type: String, type: String,
default: '', default: ""
}, },
type: { type: {
type: String, type: String,
default: 'input', default: "input"
}, },
rows: { rows: {
type: Number, type: Number,
default: 5, default: 5
}, },
maxlength: { maxlength: {
type: Number, type: Number,
default: 1024 default: 1024
} }
}); });
watch(() => props.value, (newValue) => { watch(
model.value = newValue () => props.value,
}) (newValue) => {
const model = ref(props.value) model.value = newValue;
}
);
const model = ref(props.value);
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const emits = defineEmits(['update:value']); const emits = defineEmits(["update:value"]);
const onInput = (value) => { const onInput = (value) => {
emits('update:value',value) emits("update:value", value);
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">
@ -77,4 +78,4 @@ const onInput = (value) => {
} }
} }
} }
</style> </style>

View File

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

View File

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

View File

@ -1,26 +1,32 @@
<template> <template>
<div> <div>
<div class="page-apps custom-scroll"> <div class="page-apps custom-scroll">
<div class="apps-type-nav"> <div class="apps-type-nav">
<el-scrollbar> <el-scrollbar>
<ul class="scrollbar-type-nav"> <ul class="scrollbar-type-nav">
<li :class="{active: typeId === ''}" @click="getAppList('')">全部分类</li> <li :class="{ active: typeId === '' }" @click="getAppList('')">
<li v-for="item in appTypes" :key="item.id" :class="{active: typeId === item.id}" @click="getAppList(item.id)"> 全部分类
</li>
<li
v-for="item in appTypes"
:key="item.id"
:class="{ active: typeId === item.id }"
@click="getAppList(item.id)"
>
<div class="image" v-if="item.icon"> <div class="image" v-if="item.icon">
<el-image :src="item.icon" fit="cover"/> <el-image :src="item.icon" fit="cover" />
</div> </div>
{{ item.name }} {{ item.name }}
</li> </li>
</ul> </ul>
</el-scrollbar> </el-scrollbar>
</div> </div>
<div class="app-list-container" :style="{height: listBoxHeight + 'px'}"> <div class="app-list-container" :style="{ height: listBoxHeight + 'px' }">
<ItemList :items="list" v-if="list.length > 0" :gap="15" :width="300"> <ItemList :items="list" v-if="list.length > 0" :gap="15" :width="300">
<template #default="scope"> <template #default="scope">
<div class="item"> <div class="item">
<div class="image"> <div class="image">
<el-image :src="scope.item.icon" fit="cover"/> <el-image :src="scope.item.icon" fit="cover" />
</div> </div>
<div class="inner"> <div class="inner">
@ -29,17 +35,34 @@
<div class="info-text">{{ scope.item.hello_msg }}</div> <div class="info-text">{{ scope.item.hello_msg }}</div>
</div> </div>
<div class="btn"> <div class="btn">
<el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button> <el-button
<el-tooltip effect="light" content="从工作区移除" placement="top" v-if="hasRole(scope.item.key)"> size="small"
<el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button> class="sm-btn-theme"
@click="useRole(scope.item)"
>使用</el-button
>
<el-tooltip
content="从工作区移除"
placement="top"
v-if="hasRole(scope.item.key)"
>
<el-button
size="small"
type="danger"
@click="updateRole(scope.item, 'remove')"
>移除</el-button
>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="添加到工作区" placement="top" v-else> <el-tooltip content="添加到工作区" placement="top" v-else>
<el-button size="small" style="--el-color-primary:#009999" @click="updateRole(scope.item, 'add')">添加</el-button> <el-button
size="small"
style="--el-color-primary: #009999"
@click="updateRole(scope.item, 'add')"
>添加</el-button
>
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="app-item">--> <!-- <div class="app-item">-->
<!-- <el-image :src="scope.item.icon" fit="cover"/>--> <!-- <el-image :src="scope.item.icon" fit="cover"/>-->
@ -73,95 +96,108 @@
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import { onMounted, ref } from "vue";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {checkSession} from "@/store/cache"; import { checkSession } from "@/store/cache";
import {arrayContains, removeArrayItem, substr} from "@/utils/libs"; import { arrayContains, removeArrayItem, substr } from "@/utils/libs";
import {useRouter} from "vue-router"; import { useRouter } from "vue-router";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
import ItemList from "@/components/ItemList.vue"; import ItemList from "@/components/ItemList.vue";
const listBoxHeight = window.innerHeight - 133 const listBoxHeight = window.innerHeight - 133;
const typeId = ref('') const typeId = ref("");
const appTypes = ref([]) const appTypes = ref([]);
const list = ref([]) const list = ref([]);
const roles = ref([]) const roles = ref([]);
const store = useSharedStore(); const store = useSharedStore();
onMounted(() => { onMounted(() => {
getAppType() getAppType();
getAppList() getAppList();
getRoles() getRoles();
}) });
const getRoles = () => { const getRoles = () => {
checkSession().then(user => { checkSession()
roles.value = user.chat_roles .then((user) => {
}).catch(e => { roles.value = user.chat_roles;
console.log(e.message) })
}) .catch((e) => {
} console.log(e.message);
});
};
const getAppType = () => { const getAppType = () => {
httpGet("/api/app/type/list").then((res) => { httpGet("/api/app/type/list")
appTypes.value = res.data .then((res) => {
}).catch(e => { appTypes.value = res.data;
ElMessage.error("获取分类失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("获取分类失败:" + e.message);
});
};
const getAppList = (tid = '') => { const getAppList = (tid = "") => {
typeId.value = tid; typeId.value = tid;
httpGet("/api/app/list", { tid }).then((res) => { httpGet("/api/app/list", { tid })
const items = res.data .then((res) => {
// hello message const items = res.data;
for (let i = 0; i < items.length; i++) { // hello message
items[i].intro = substr(items[i].hello_msg, 80) for (let i = 0; i < items.length; i++) {
} items[i].intro = substr(items[i].hello_msg, 80);
list.value = items }
}).catch(e => { list.value = items;
ElMessage.error("获取应用失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("获取应用失败:" + e.message);
});
};
const updateRole = (row, opt) => { const updateRole = (row, opt) => {
checkSession().then(() => { checkSession()
const title = ref("") .then(() => {
if (opt === "add") { const title = ref("");
title.value = "添加应用" if (opt === "add") {
const exists = arrayContains(roles.value, row.key) title.value = "添加应用";
if (exists) { const exists = arrayContains(roles.value, row.key);
return if (exists) {
return;
}
roles.value.push(row.key);
} else {
title.value = "移除应用";
const exists = arrayContains(roles.value, row.key);
if (!exists) {
return;
}
roles.value = removeArrayItem(roles.value, row.key);
} }
roles.value.push(row.key) httpPost("/api/app/update", { keys: roles.value })
} else { .then(() => {
title.value = "移除应用" ElMessage.success({
const exists = arrayContains(roles.value, row.key) message: title.value + "成功!",
if (!exists) { duration: 1000
return });
} })
roles.value = removeArrayItem(roles.value, row.key) .catch((e) => {
} ElMessage.error(title.value + "失败:" + e.message);
httpPost("/api/app/update", {keys: roles.value}).then(() => { });
ElMessage.success({message: title.value + "成功!", duration: 1000})
}).catch(e => {
ElMessage.error(title.value + "失败:" + e.message)
}) })
}).catch(() => { .catch(() => {
store.setShowLoginDialog(true) store.setShowLoginDialog(true);
}) });
} };
const hasRole = (roleKey) => { const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2) return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
} };
const router = useRouter() const router = useRouter();
const useRole = (role) => { const useRole = (role) => {
router.push(`/chat?role_id=${role.id}`) router.push(`/chat?role_id=${role.id}`);
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@ -880,6 +880,7 @@ const autofillPrompt = (text) => {
// //
const sendMessage = function () { const sendMessage = function () {
if (!isLogin.value) { if (!isLogin.value) {
console.log("未登录");
store.setShowLoginDialog(true); store.setShowLoginDialog(true);
return; return;
} }

View File

@ -11,8 +11,13 @@
<el-form-item label="图片质量"> <el-form-item label="图片质量">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.quality" style="width:176px"> <el-select v-model="params.quality" style="width: 176px">
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value"/> <el-option
v-for="v in qualities"
:label="v.name"
:value="v.value"
:key="v.value"
/>
</el-select> </el-select>
</div> </div>
</template> </template>
@ -23,8 +28,13 @@
<el-form-item label="图片尺寸"> <el-form-item label="图片尺寸">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.size" style="width:176px"> <el-select v-model="params.size" style="width: 176px">
<el-option v-for="v in sizes" :label="v" :value="v" :key="v"/> <el-option
v-for="v in sizes"
:label="v"
:value="v"
:key="v"
/>
</el-select> </el-select>
</div> </div>
</template> </template>
@ -35,17 +45,21 @@
<el-form-item label="图片样式"> <el-form-item label="图片样式">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.style" style="width:176px"> <el-select v-model="params.style" style="width: 176px">
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value"/> <el-option
v-for="v in styles"
:label="v.name"
:value="v.value"
:key="v.value"
/>
</el-select> </el-select>
<el-tooltip <el-tooltip
effect="light" content="生动使模型倾向于生成超真实和戏剧性的图像"
content="生动使模型倾向于生成超真实和戏剧性的图像" raw-content
raw-content placement="right"
placement="right"
> >
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled/> <InfoFilled />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</div> </div>
@ -55,52 +69,68 @@
<div class="param-line"> <div class="param-line">
<el-input <el-input
v-model="params.prompt" v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }" :autosize="{ minRows: 4, maxRows: 6 }"
type="textarea" type="textarea"
ref="promptRef" ref="promptRef"
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词" placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
v-loading="isGenerating" v-loading="isGenerating"
style="--el-mask-color:rgba(100, 100, 100, 0.8)" style="--el-mask-color: rgba(100, 100, 100, 0.8)"
/> />
</div> </div>
<el-row class="text-info"> <el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating"> <el-button
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i> class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i
class="iconfont icon-chuangzuo"
style="margin-right: 5px"
></i>
<span>生成专业绘画指令</span> <span>生成专业绘画指令</span>
</el-button> </el-button>
</el-row> </el-row>
<div class="text-info"> <div class="text-info">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12"> <el-text type="primary"
<el-tag>每次绘图消耗{{ dallPower }}算力</el-tag> >每次绘图消耗
</el-col> <el-text type="warning"
<el-col :span="12"> >{{ dallPower }}算力</el-text
<el-tag type="success">当前可用{{ power }}算力</el-tag> ></el-text
</el-col> >
&nbsp;&nbsp;
<el-text type="primary"
>当前可用
<el-text type="warning"> {{ power }}算力</el-text>
</el-text>
</el-row> </el-row>
</div> </div>
</el-form> </el-form>
</div> </div>
<div class="submit-btn"> <div class="submit-btn">
<el-button color="#47fff1" :dark="false" round @click="generate"> <el-button type="primary" :dark="false" round @click="generate">
立即生成 立即生成
</el-button> </el-button>
</div> </div>
</div> </div>
<div class="task-list-box"> <div class="task-list-box">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }"> <div
class="task-list-inner"
:style="{ height: listBoxHeight + 'px' }"
>
<div class="job-list-box"> <div class="job-list-box">
<h2>任务列表</h2> <h2>任务列表</h2>
<task-list :list="runningJobs" /> <task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2>创作记录</h2> <h2>创作记录</h2>
<div class="finish-job-list"> <div class="finish-job-list">
<div v-if="finishedJobs.length > 0"> <div v-if="finishedJobs.length > 0">
<v3-waterfall <v3-waterfall
id="waterfall" id="waterfall"
:list="finishedJobs" :list="finishedJobs"
srcKey="img_thumb" srcKey="img_thumb"
@ -110,331 +140,390 @@
:distanceToScroll="100" :distanceToScroll="100"
:isLoading="loading" :isLoading="loading"
:isOver="isOver" :isOver="isOver"
@scrollReachBottom="fetchFinishJobs()"> @scrollReachBottom="fetchFinishJobs()"
<template #default="slotProp"> >
<div class="job-item"> <template #default="slotProp">
<el-image <div class="job-item">
<el-image
v-if="slotProp.item.img_url !== ''" v-if="slotProp.item.img_url !== ''"
@click="previewImg(slotProp.item)" @click="previewImg(slotProp.item)"
:src="slotProp.item['img_thumb']" :src="slotProp.item['img_thumb']"
fit="cover" fit="cover"
loading="lazy"> loading="lazy"
<template #placeholder> >
<div class="image-slot"> <template #placeholder>
正在加载图片 <div class="image-slot">正在加载图片</div>
</div> </template>
</template>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<el-icon> <el-icon>
<Picture/> <Picture />
</el-icon> </el-icon>
</div> </div>
</template> </template>
</el-image> </el-image>
<el-image v-else-if="slotProp.item.progress === 101"> <el-image v-else-if="slotProp.item.progress === 101">
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<div class="err-msg-container"> <div class="err-msg-container">
<div class="title">任务失败</div> <div class="title">任务失败</div>
<div class="opt"> <div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top"> <el-popover
<template #reference> title="错误详情"
<el-button type="info">详情</el-button> trigger="click"
</template> :width="250"
</el-popover> :content="slotProp.item['err_msg']"
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button> 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>
</div> </div>
</div> </template>
</template> </el-image>
</el-image>
<el-image v-else> <el-image v-else>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<i class="iconfont icon-loading"></i> <i class="iconfont icon-loading"></i>
<span>正在下载图片</span> <span>正在下载图片</span>
</div> </div>
</template> </template>
</el-image> </el-image>
<div class="remove"> <div class="remove">
<el-tooltip content="删除" placement="top" effect="light"> <el-tooltip content="删除" placement="top">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/> <el-button
</el-tooltip> type="danger"
<el-tooltip content="取消分享" placement="top" effect="light" v-if="slotProp.item.publish"> :icon="Delete"
<el-button type="warning" @click="removeImage(slotProp.item)"
@click="publishImage(slotProp.item, false)" circle
circle> />
<i class="iconfont icon-cancel-share"></i> </el-tooltip>
</el-button> <el-tooltip
</el-tooltip> content="取消分享"
<el-tooltip content="分享" placement="top" effect="light" v-else> placement="top"
<el-button type="success" @click="publishImage(slotProp.item, true)" circle> v-if="slotProp.item.publish"
<i class="iconfont icon-share-bold"></i> >
</el-button> <el-button
</el-tooltip> type="warning"
@click="publishImage(slotProp.item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip 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 content="复制提示词" placement="top" effect="light"> <el-tooltip content="复制提示词" placement="top">
<el-button type="info" circle class="copy-prompt" <el-button
:data-clipboard-text="slotProp.item.prompt"> type="info"
<i class="iconfont icon-file"></i> circle
</el-button> class="copy-prompt"
</el-tooltip> :data-clipboard-text="slotProp.item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
</div>
</div> </div>
</div> </template>
</template>
<template #footer>
<div class="no-more-data">
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</template>
</v3-waterfall>
<template #footer>
<div class="no-more-data">
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</template>
</v3-waterfall>
</div>
<el-empty
:image-size="100"
:image="nodata"
description="暂无记录"
v-else
/>
</div> </div>
<el-empty :image-size="100" v-else/> </template>
</div> <!-- end finish job list-->
<!-- end finish job list-->
</div> </div>
</div> </div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/> <back-top :right="30" :bottom="30" />
</div><!-- end task list box --> </div>
<!-- end task list box -->
</div> </div>
</div> </div>
<el-image-viewer @close="() => { previewURL = '' }" v-if="previewURL !== ''" :url-list="[previewURL]"/> <el-image-viewer
@close="
() => {
previewURL = '';
}
"
v-if="previewURL !== ''"
:url-list="[previewURL]"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue" import nodata from "@/assets/img/no-data.png";
import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http"; import { nextTick, onMounted, onUnmounted, ref } from "vue";
import {ElMessage, ElMessageBox} from "element-plus"; import { Delete, InfoFilled, Picture } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {checkSession, getClientId, getSystemInfo} from "@/store/cache"; import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import {showMessageError} from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
const listBoxHeight = ref(0) const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0) // const paramBoxHeight = ref(0)
const isLogin = ref(false) const isLogin = ref(false);
const loading = ref(true) const loading = ref(true);
const colWidth = ref(220) const colWidth = ref(220);
const isOver = ref(false) const isOver = ref(false);
const previewURL = ref("") const previewURL = ref("");
const store = useSharedStore(); const store = useSharedStore();
const resizeElement = function () { const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 90 listBoxHeight.value = window.innerHeight - 90;
// paramBoxHeight.value = window.innerHeight - 110 // paramBoxHeight.value = window.innerHeight - 110
}; };
resizeElement() resizeElement();
window.onresize = () => { window.onresize = () => {
resizeElement() resizeElement();
} };
const qualities = [ const qualities = [
{name: "标准", value: "standard"}, { name: "标准", value: "standard" },
{name: "高清", value: "hd"}, { name: "高清", value: "hd" }
] ];
const sizes = ["1024x1024", "1792x1024", "1024x1792"] const sizes = ["1024x1024", "1792x1024", "1024x1792"];
const styles = [ const styles = [
{name: "生动", value: "vivid"}, { name: "生动", value: "vivid" },
{name: "自然", value: "natural"} { name: "自然", value: "natural" }
] ];
const params = ref({ const params = ref({
client_id: getClientId(), client_id: getClientId(),
quality: "standard", quality: "standard",
size: "1024x1024", size: "1024x1024",
style: "vivid", style: "vivid",
prompt: "" prompt: ""
}) });
const finishedJobs = ref([]) const finishedJobs = ref([]);
const runningJobs = ref([]) const runningJobs = ref([]);
const power = ref(0) const power = ref(0);
const dallPower = ref(0) // SD const dallPower = ref(0); // SD
const clipboard = ref(null) const clipboard = ref(null);
const userId = ref(0) const userId = ref(0);
onMounted(() => { onMounted(() => {
initData() initData();
clipboard.value = new Clipboard('.copy-prompt'); clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on('success', () => { clipboard.value.on("success", () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) });
clipboard.value.on('error', () => { clipboard.value.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
getSystemInfo().then(res => { getSystemInfo()
dallPower.value = res.data["dall_power"] .then((res) => {
}).catch(e => { dallPower.value = res.data["dall_power"];
ElMessage.error("获取系统配置失败:" + e.message) })
}) .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
store.addMessageHandler("dall",(data) => { store.addMessageHandler("dall", (data) => {
// //
if (data.channel !== "dall" || data.clientId !== getClientId()) { if (data.channel !== "dall" || data.clientId !== getClientId()) {
return return;
} }
if (data.body === "FINISH" || data.body === "FAIL") { if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 0 page.value = 0;
isOver.value = false isOver.value = false;
fetchFinishJobs() fetchFinishJobs();
} }
nextTick(() => fetchRunningJobs()) nextTick(() => fetchRunningJobs());
}) });
}) });
onUnmounted(() => { onUnmounted(() => {
clipboard.value.destroy() clipboard.value.destroy();
store.removeMessageHandler("dall") store.removeMessageHandler("dall");
}) });
const initData = () => { const initData = () => {
checkSession().then(user => { checkSession()
power.value = user['power'] .then((user) => {
userId.value = user.id power.value = user["power"];
isLogin.value = true userId.value = user.id;
isLogin.value = true;
page.value = 0 page.value = 0;
fetchRunningJobs() fetchRunningJobs();
fetchFinishJobs() fetchFinishJobs();
}).catch(() => { })
}); .catch(() => {});
} };
const fetchRunningJobs = () => { const fetchRunningJobs = () => {
if (!isLogin.value) { if (!isLogin.value) {
return return;
} }
// //
httpGet(`/api/dall/jobs?finish=false`).then(res => { httpGet(`/api/dall/jobs?finish=false`)
runningJobs.value = res.data.items .then((res) => {
}).catch(e => { runningJobs.value = res.data.items;
ElMessage.error("获取任务失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("获取任务失败:" + e.message);
});
};
const page = ref(1) const page = ref(1);
const pageSize = ref(15) const pageSize = ref(15);
// //
const fetchFinishJobs = () => { const fetchFinishJobs = () => {
if (!isLogin.value) { if (!isLogin.value) {
return return;
} }
loading.value = true loading.value = true;
page.value = page.value + 1 page.value = page.value + 1;
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => { httpGet(
if (res.data.items.length < pageSize.value) { `/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`
isOver.value = true )
} .then((res) => {
const imageList = res.data.items if (res.data.items.length < pageSize.value) {
for (let i = 0; i < imageList.length; i++) { isOver.value = true;
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75" }
} const imageList = res.data.items;
if (page.value === 1) { for (let i = 0; i < imageList.length; i++) {
finishedJobs.value = imageList imageList[i]["img_thumb"] =
} else { imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
finishedJobs.value = finishedJobs.value.concat(imageList) }
} if (page.value === 1) {
finishedJobs.value = imageList;
loading.value = false } else {
}).catch(e => { finishedJobs.value = finishedJobs.value.concat(imageList);
ElMessage.error("获取任务失败:" + e.message) }
})
}
loading.value = false;
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
// //
const promptRef = ref(null) const promptRef = ref(null);
const generate = () => { const generate = () => {
if (params.value.prompt === '') { if (params.value.prompt === "") {
promptRef.value.focus() promptRef.value.focus();
return ElMessage.error("请输入绘画提示词!") return ElMessage.error("请输入绘画提示词!");
} }
if (!isLogin.value) { if (!isLogin.value) {
store.setShowLoginDialog(true) store.setShowLoginDialog(true);
return return;
} }
httpPost("/api/dall/image", params.value).then(() => { httpPost("/api/dall/image", params.value)
ElMessage.success("任务执行成功!") .then(() => {
power.value -= dallPower.value ElMessage.success("任务执行成功!");
fetchRunningJobs() power.value -= dallPower.value;
}).catch(e => { fetchRunningJobs();
ElMessage.error("任务执行失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("任务执行失败:" + e.message);
});
};
const removeImage = (item) => { const removeImage = (item) => {
ElMessageBox.confirm( ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
'此操作将会删除任务和图片,继续操作码?', confirmButtonText: "确认",
'删除提示', cancelButtonText: "取消",
{ type: "warning"
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
httpGet("/api/dall/remove", {id: item.id}).then(() => {
ElMessage.success("任务删除成功")
page.value = 0
isOver.value = false
fetchFinishJobs()
}).catch(e => {
ElMessage.error("任务删除失败:" + e.message)
})
}).catch(() => {
}) })
} .then(() => {
httpGet("/api/dall/remove", { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
page.value = 0;
isOver.value = false;
fetchFinishJobs();
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
})
.catch(() => {});
};
const previewImg = (item) => { const previewImg = (item) => {
previewURL.value = item.img_url previewURL.value = item.img_url;
} };
// //
const publishImage = (item, action) => { const publishImage = (item, action) => {
let text = "图片发布" let text = "图片发布";
if (action === false) { if (action === false) {
text = "取消发布" text = "取消发布";
} }
httpGet("/api/dall/publish", {id: item.id, action: action}).then(() => { httpGet("/api/dall/publish", { id: item.id, action: action })
ElMessage.success(text + "成功") .then(() => {
item.publish = action ElMessage.success(text + "成功");
page.value = 0 item.publish = action;
isOver.value = false page.value = 0;
}).catch(e => { isOver.value = false;
ElMessage.error(text + "失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error(text + "失败:" + e.message);
});
};
const isGenerating = ref(false) const isGenerating = ref(false);
const generatePrompt = () => { const generatePrompt = () => {
if (params.value.prompt === "") { if (params.value.prompt === "") {
return showMessageError("请输入原始提示词") return showMessageError("请输入原始提示词");
} }
isGenerating.value = true isGenerating.value = true;
httpPost("/api/prompt/image", {prompt: params.value.prompt}).then(res => { httpPost("/api/prompt/image", { prompt: params.value.prompt })
params.value.prompt = res.data .then((res) => {
isGenerating.value = false params.value.prompt = res.data;
}).catch(e => { isGenerating.value = false;
showMessageError("生成提示词失败:"+e.message) })
isGenerating.value = false .catch((e) => {
}) showMessageError("生成提示词失败:" + e.message);
} isGenerating.value = false;
});
};
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@ -180,7 +180,14 @@
</ul> </ul>
</div> </div>
</div> </div>
<!-- :style="{ 'padding-left': isCollapse ? '65px' : '170px' }" -->
<div class="right-main"> <div class="right-main">
<div
v-if="loginUser.id === undefined || !loginUser.id"
class="loginMask"
:style="{ left: isCollapse ? '65px' : '170px' }"
@click="showNoticeLogin = true"
></div>
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id"> <div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button <el-button
@click="router.push('/login')" @click="router.push('/login')"
@ -189,6 +196,7 @@
>登录</el-button >登录</el-button
> >
</div> </div>
<!-- <div class="content custom-scroll"> -->
<div class="content custom-scroll"> <div class="content custom-scroll">
<router-view :key="routerViewKey" v-slot="{ Component }"> <router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in"> <transition name="move" mode="out-in">
@ -196,6 +204,7 @@
</transition> </transition>
</router-view> </router-view>
</div> </div>
<!-- </div> -->
</div> </div>
<config-dialog <config-dialog
v-if="loginUser.id" v-if="loginUser.id"
@ -203,12 +212,21 @@
@hide="showConfigDialog = false" @hide="showConfigDialog = false"
/> />
</div> </div>
<el-dialog v-model="showNoticeLogin">
<el-result icon="warning" title="未登录" sub-title="登录后解锁功能">
<template #extra>
<el-button type="primary" @click="router.push('/login')"
>登录</el-button
>
</template>
</el-result>
</el-dialog>
</template> </template>
<script setup> <script setup>
import { CirclePlus, Setting } from "@element-plus/icons-vue"; import { CirclePlus, Setting } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue"; import ThemeChange from "@/components/ThemeChange.vue";
import { avatarImg } from "@/assets/img/avatar.jpg"; import avatarImg from "@/assets/img/avatar.jpg";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http"; import { httpGet } from "@/utils/http";
@ -226,10 +244,37 @@ const router = useRouter();
const logo = ref(""); const logo = ref("");
const mainNavs = ref([]); const mainNavs = ref([]);
const moreNavs = ref([]); const moreNavs = ref([]);
const curPath = ref(router.currentRoute.value.path); // const curPath = ref(router.currentRoute.value.path);
const curPath = ref();
const title = ref(""); const title = ref("");
const showNoticeLogin = ref(false);
// const mainWinHeight = window.innerHeight - 50; // const mainWinHeight = window.innerHeight - 50;
/**
* 从路径名中提取第一个路径段
* @param pathname - URL 的路径名部分例如 '/chat/12345'
* @returns 第一个路径段不含斜杠例如 'chat'如果不存在则返回 null
*/
const extractFirstSegment = (pathname) => {
const segments = pathname.split("/").filter((segment) => segment.length > 0);
return segments.length > 0 ? segments[0] : null;
};
const getFirstPathSegment = (url) => {
try {
// 使 URL URL
const parsedUrl = new URL(url);
return extractFirstSegment(parsedUrl.pathname);
} catch (error) {
// 使
if (typeof window !== "undefined") {
const parsedUrl = new URL(url, window.location.origin);
return extractFirstSegment(parsedUrl.pathname);
}
// null
return null;
}
};
const loginUser = ref({}); const loginUser = ref({});
const mainWinHeight = loginUser.value.id const mainWinHeight = loginUser.value.id
? window.innerHeight ? window.innerHeight
@ -252,10 +297,10 @@ watch(
); );
// //
router.beforeEach((to, from, next) => { // router.beforeEach((to, from, next) => {
curPath.value = to.path; // curPath.value = to.path;
next(); // next();
}); // });
if (curPath.value === "/external") { if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url; curPath.value = router.currentRoute.value.query.url;
@ -302,7 +347,7 @@ onMounted(() => {
license.value = { de_copy: false }; license.value = { de_copy: false };
showMessageError("获取 License 配置:" + e.message); showMessageError("获取 License 配置:" + e.message);
}); });
curPath.value = "/" + getFirstPathSegment(window.location.href);
init(); init();
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,37 +11,43 @@
</el-radio-group> </el-radio-group>
</div> </div>
</div> </div>
<div class="waterfall" :style="{ height:listBoxHeight + 'px' }" id="waterfall-box"> <div
<v3-waterfall v-if="imgType === 'mj'" class="waterfall"
id="waterfall" :style="{ height: listBoxHeight + 'px' }"
:list="data['mj']" id="waterfall-box"
srcKey="img_thumb" >
:gap="12" <v3-waterfall
:bottomGap="-5" v-if="imgType === 'mj'"
:colWidth="colWidth" id="waterfall"
:distanceToScroll="100" :list="data['mj']"
:isLoading="loading" srcKey="img_thumb"
:isOver="isOver" :gap="12"
@scrollReachBottom="getNext"> :bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="getNext"
>
<template #default="slotProp"> <template #default="slotProp">
<div class="list-item"> <div class="list-item">
<div class="image"> <div class="image">
<el-image :src="slotProp.item['img_thumb']" <el-image
:zoom-rate="1.2" :src="slotProp.item['img_thumb']"
:preview-src-list="[slotProp.item['img_url']]" :zoom-rate="1.2"
:preview-teleported="true" :preview-src-list="[slotProp.item['img_url']]"
:initial-index="10" :preview-teleported="true"
loading="lazy"> :initial-index="10"
loading="lazy"
>
<template #placeholder> <template #placeholder>
<div class="image-slot"> <div class="image-slot">正在加载图片</div>
正在加载图片
</div>
</template> </template>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<el-icon> <el-icon>
<Picture/> <Picture />
</el-icon> </el-icon>
</div> </div>
</template> </template>
@ -49,59 +55,61 @@
</div> </div>
<div class="opt"> <div class="opt">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="light" content="复制提示词"
content="复制提示词" placement="top"
placement="top"
> >
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt"> <el-icon
<DocumentCopy/> class="copy-prompt-wall"
:data-clipboard-text="slotProp.item.prompt"
>
<DocumentCopy />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip class="box-item" content="画同款" placement="top">
class="box-item" <i
effect="light" class="iconfont icon-palette-pen"
content="画同款" @click="drawSameMj(slotProp.item)"
placement="top" ></i>
>
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i>
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
</template> </template>
</v3-waterfall> </v3-waterfall>
<v3-waterfall v-else-if="imgType === 'dall'" <v3-waterfall
id="waterfall" v-else-if="imgType === 'dall'"
:list="data['dall']" id="waterfall"
srcKey="img_thumb" :list="data['dall']"
:gap="12" srcKey="img_thumb"
:bottomGap="-5" :gap="12"
:colWidth="colWidth" :bottomGap="-5"
:distanceToScroll="100" :colWidth="colWidth"
:isLoading="loading" :distanceToScroll="100"
:isOver="isOver" :isLoading="loading"
@scrollReachBottom="getNext"> :isOver="isOver"
@scrollReachBottom="getNext"
>
<template #default="slotProp"> <template #default="slotProp">
<div class="list-item"> <div class="list-item">
<div class="image"> <div class="image">
<el-image :src="slotProp.item['img_thumb']" <el-image
:zoom-rate="1.2" :src="slotProp.item['img_thumb']"
:preview-src-list="[slotProp.item['img_url']]" :zoom-rate="1.2"
:preview-teleported="true" :preview-src-list="[slotProp.item['img_url']]"
:initial-index="10" :preview-teleported="true"
loading="lazy"> :initial-index="10"
loading="lazy"
>
<template #placeholder> <template #placeholder>
<div class="image-slot"> <div class="image-slot">正在加载图片</div>
正在加载图片
</div>
</template> </template>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<el-icon> <el-icon>
<Picture/> <Picture />
</el-icon> </el-icon>
</div> </div>
</template> </template>
@ -109,13 +117,15 @@
</div> </div>
<div class="opt"> <div class="opt">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="light" content="复制提示词"
content="复制提示词" placement="top"
placement="top"
> >
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt"> <el-icon
<DocumentCopy/> class="copy-prompt-wall"
:data-clipboard-text="slotProp.item.prompt"
>
<DocumentCopy />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</div> </div>
@ -123,32 +133,35 @@
</template> </template>
</v3-waterfall> </v3-waterfall>
<v3-waterfall v-else <v3-waterfall
id="waterfall" v-else
:list="data['sd']" id="waterfall"
srcKey="img_thumb" :list="data['sd']"
:gap="12" srcKey="img_thumb"
:bottomGap="-5" :gap="12"
:colWidth="colWidth" :bottomGap="-5"
:distanceToScroll="100" :colWidth="colWidth"
:isLoading="loading" :distanceToScroll="100"
:isOver="isOver" :isLoading="loading"
@scrollReachBottom="getNext"> :isOver="isOver"
@scrollReachBottom="getNext"
>
<template #default="slotProp"> <template #default="slotProp">
<div class="list-item"> <div class="list-item">
<div class="image"> <div class="image">
<el-image :src="slotProp.item['img_thumb']" loading="lazy" <el-image
@click="showTask(slotProp.item)"> :src="slotProp.item['img_thumb']"
loading="lazy"
@click="showTask(slotProp.item)"
>
<template #placeholder> <template #placeholder>
<div class="image-slot"> <div class="image-slot">正在加载图片</div>
正在加载图片
</div>
</template> </template>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<el-icon> <el-icon>
<Picture/> <Picture />
</el-icon> </el-icon>
</div> </div>
</template> </template>
@ -159,31 +172,36 @@
</v3-waterfall> </v3-waterfall>
<div class="footer" v-if="isOver"> <div class="footer" v-if="isOver">
<span>没有更多数据了</span> <el-empty
<i class="iconfont icon-face"></i> :image-size="100"
:image="nodata"
description="没有更多数据了"
/>
<!-- <span>没有更多数据了</span>
<i class="iconfont icon-face"></i> -->
</div> </div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/> <back-top :right="30" :bottom="30" />
</div>
</div><!-- end of waterfall --> <!-- end of waterfall -->
</div> </div>
<!-- 任务详情弹框 --> <!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true"> <el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="16"> <el-col :span="16">
<div class="img-container" :style="{maxHeight: fullImgHeight+'px'}"> <div
class="img-container"
:style="{ maxHeight: fullImgHeight + 'px' }"
>
<el-image :src="item['img_url']" fit="contain"> <el-image :src="item['img_url']" fit="contain">
<template #placeholder> <template #placeholder>
<div class="image-slot"> <div class="image-slot">正在加载图片</div>
正在加载图片
</div>
</template> </template>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<el-icon> <el-icon>
<Picture/> <Picture />
</el-icon> </el-icon>
</div> </div>
</template> </template>
@ -193,26 +211,27 @@
<el-col :span="8"> <el-col :span="8">
<div class="task-info"> <div class="task-info">
<div class="info-line"> <div class="info-line">
<el-divider> <el-divider> 正向提示词 </el-divider>
正向提示词
</el-divider>
<div class="prompt"> <div class="prompt">
<span>{{ item.prompt }}</span> <span>{{ item.prompt }}</span>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.prompt"> <el-icon
<DocumentCopy/> class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<DocumentCopy />
</el-icon> </el-icon>
</div> </div>
</div> </div>
<div class="info-line"> <div class="info-line">
<el-divider> <el-divider> 反向提示词 </el-divider>
反向提示词
</el-divider>
<div class="prompt"> <div class="prompt">
<span>{{ item.params.negative_prompt }}</span> <span>{{ item.params.negative_prompt }}</span>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.params.negative_prompt"> <el-icon
<DocumentCopy/> class="copy-prompt-wall"
:data-clipboard-text="item.params.negative_prompt"
>
<DocumentCopy />
</el-icon> </el-icon>
</div> </div>
</div> </div>
@ -227,7 +246,9 @@
<div class="info-line"> <div class="info-line">
<div class="wrapper"> <div class="wrapper">
<label>图片尺寸</label> <label>图片尺寸</label>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div> <div class="item-value">
{{ item.params.width }} x {{ item.params.height }}
</div>
</div> </div>
</div> </div>
@ -253,9 +274,7 @@
</div> </div>
<div v-if="item.params.hd_fix"> <div v-if="item.params.hd_fix">
<el-divider> <el-divider> 高清修复 </el-divider>
高清修复
</el-divider>
<div class="info-line"> <div class="info-line">
<div class="wrapper"> <div class="wrapper">
<label>重绘幅度</label> <label>重绘幅度</label>
@ -286,146 +305,151 @@
</div> </div>
<div class="copy-params"> <div class="copy-params">
<el-button type="primary" round @click="drawSameSd(item)">画一张同款的</el-button> <el-button type="primary" round @click="drawSameSd(item)"
>画一张同款的</el-button
>
</div> </div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue" import nodata from "@/assets/img/no-data.png";
import {DocumentCopy, Picture} from "@element-plus/icons-vue"; import { nextTick, onMounted, onUnmounted, ref } from "vue";
import {httpGet} from "@/utils/http"; import { DocumentCopy, Picture } from "@element-plus/icons-vue";
import {ElMessage} from "element-plus"; import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {useRouter} from "vue-router"; import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
const data = ref({ const data = ref({
"mj": [], mj: [],
"sd": [], sd: [],
"dall": [], dall: []
}) });
const loading = ref(true) const loading = ref(true);
const isOver = ref(false) const isOver = ref(false);
const imgType = ref("mj") // const imgType = ref("mj"); //
const listBoxHeight = window.innerHeight - 124 const listBoxHeight = window.innerHeight - 124;
const colWidth = ref(220) const colWidth = ref(220);
const fullImgHeight = ref(window.innerHeight - 60) const fullImgHeight = ref(window.innerHeight - 60);
const showTaskDialog = ref(false) const showTaskDialog = ref(false);
const item = ref({}) const item = ref({});
// //
const calcColWidth = () => { const calcColWidth = () => {
const listBoxWidth = window.innerWidth - 60 - 80 const listBoxWidth = window.innerWidth - 60 - 80;
const rows = Math.floor(listBoxWidth / colWidth.value) const rows = Math.floor(listBoxWidth / colWidth.value);
colWidth.value = Math.floor((listBoxWidth - (rows - 1) * 12) / rows) colWidth.value = Math.floor((listBoxWidth - (rows - 1) * 12) / rows);
} };
calcColWidth() calcColWidth();
window.onresize = () => { window.onresize = () => {
calcColWidth() calcColWidth();
} };
const page = ref(0) const page = ref(0);
const pageSize = ref(15) const pageSize = ref(15);
// //
const getNext = () => { const getNext = () => {
if (isOver.value) { if (isOver.value) {
return return;
} }
loading.value = true loading.value = true;
page.value = page.value + 1 page.value = page.value + 1;
let url = "" let url = "";
switch (imgType.value) { switch (imgType.value) {
case "mj": case "mj":
url = "/api/mj/imgWall" url = "/api/mj/imgWall";
break break;
case "sd": case "sd":
url = "/api/sd/imgWall" url = "/api/sd/imgWall";
break break;
case "dall": case "dall":
url = "/api/dall/imgWall" url = "/api/dall/imgWall";
break break;
} }
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`).then(res => { httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
loading.value = false .then((res) => {
if (!res.data.items || res.data.items.length === 0) { loading.value = false;
isOver.value = true if (!res.data.items || res.data.items.length === 0) {
return isOver.value = true;
} return;
}
// //
const imageList = res.data.items const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) { for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75" imageList[i]["img_thumb"] =
} imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
if (data.value[imgType.value].length === 0) { }
data.value[imgType.value] = imageList if (data.value[imgType.value].length === 0) {
return data.value[imgType.value] = imageList;
} return;
}
if (imageList.length < pageSize.value) { if (imageList.length < pageSize.value) {
isOver.value = true isOver.value = true;
} }
data.value[imgType.value] = data.value[imgType.value].concat(imageList) data.value[imgType.value] = data.value[imgType.value].concat(imageList);
})
.catch((e) => {
ElMessage.error("获取图片失败:" + e.message);
});
};
}).catch(e => { getNext();
ElMessage.error("获取图片失败:" + e.message)
})
}
getNext() const clipboard = ref(null);
const clipboard = ref(null)
onMounted(() => { onMounted(() => {
clipboard.value = new Clipboard('.copy-prompt-wall'); clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on('success', () => { clipboard.value.on("success", () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) });
clipboard.value.on('error', () => { clipboard.value.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
}) });
onUnmounted(() => { onUnmounted(() => {
clipboard.value.destroy() clipboard.value.destroy();
}) });
const changeImgType = () => { const changeImgType = () => {
console.log(imgType.value) console.log(imgType.value);
document.getElementById('waterfall-box').scrollTo(0, 0) document.getElementById("waterfall-box").scrollTo(0, 0);
page.value = 0 page.value = 0;
data.value = { data.value = {
"mj": [], mj: [],
"sd": [], sd: [],
"dall": [], dall: []
} };
loading.value = true loading.value = true;
isOver.value = false isOver.value = false;
nextTick(() => getNext()) nextTick(() => getNext());
} };
const showTask = (row) => { const showTask = (row) => {
item.value = row item.value = row;
showTaskDialog.value = true showTaskDialog.value = true;
} };
const router = useRouter();
const router = useRouter()
const drawSameSd = (row) => { const drawSameSd = (row) => {
router.push({name: "image-sd", params: {copyParams: JSON.stringify(row.params)}}) router.push({
} name: "image-sd",
params: { copyParams: JSON.stringify(row.params) }
});
};
const drawSameMj = (row) => { const drawSameMj = (row) => {
router.push({name: "image-mj", params: {prompt: row.prompt}}) router.push({ name: "image-mj", params: { prompt: row.prompt } });
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@ -15,7 +15,6 @@
<el-tooltip <el-tooltip
v-if="!license.de_copy" v-if="!license.de_copy"
class="box-item" class="box-item"
effect="light"
content="部署文档" content="部署文档"
placement="bottom" placement="bottom"
> >
@ -26,7 +25,6 @@
<el-tooltip <el-tooltip
v-if="!license.de_copy" v-if="!license.de_copy"
class="box-item" class="box-item"
effect="light"
content="项目源码" content="项目源码"
placement="bottom" placement="bottom"
> >

View File

@ -5,18 +5,25 @@
<h2>会员推广计划</h2> <h2>会员推广计划</h2>
<div class="share-box"> <div class="share-box">
<div class="info"> <div class="info">
我们非常欢迎您把此应用分享给您身边的朋友分享成功注册后您和被邀请人都将获得 <strong>{{ invitePower }}</strong> 我们非常欢迎您把此应用分享给您身边的朋友分享成功注册后您和被邀请人都将获得
<strong>{{ invitePower }}</strong>
算力额度作为奖励 算力额度作为奖励
你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友 你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友
</div> </div>
<div class="invite-qrcode"> <div class="invite-qrcode">
<el-image :src="qrImg"/> <el-image :src="qrImg" />
</div> </div>
<div class="invite-url"> <div class="invite-url">
<span>{{ inviteURL }}</span> <span>{{ inviteURL }}</span>
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL">复制链接</el-button> <el-button
type="primary"
plain
class="copy-link"
:data-clipboard-text="inviteURL"
>复制链接</el-button
>
</div> </div>
</div> </div>
@ -76,10 +83,10 @@
</el-row> </el-row>
</div> </div>
<h2>您推荐用户</h2> <h2>您推荐用户</h2>
<div class="invite-logs"> <div class="invite-logs">
<invite-list v-if="isLogin"/> <invite-list v-if="isLogin" />
</div> </div>
</div> </div>
</div> </div>
@ -87,69 +94,79 @@
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import { onMounted, ref } from "vue";
import QRCode from "qrcode"; import QRCode from "qrcode";
import {httpGet} from "@/utils/http"; import { httpGet } from "@/utils/http";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import InviteList from "@/components/InviteList.vue"; import InviteList from "@/components/InviteList.vue";
import {checkSession, getSystemInfo} from "@/store/cache"; import { checkSession, getSystemInfo } from "@/store/cache";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
const inviteURL = ref("") const inviteURL = ref("");
const qrImg = ref("/images/wx.png") const qrImg = ref("/images/wx.png");
const invitePower = ref(0) const invitePower = ref(0);
const hits = ref(0) const hits = ref(0);
const regNum = ref(0) const regNum = ref(0);
const rate = ref(0) const rate = ref(0);
const isLogin = ref(false) const isLogin = ref(false);
const store = useSharedStore() const store = useSharedStore();
onMounted(() => { onMounted(() => {
initData() initData();
// //
const clipboard = new Clipboard('.copy-link'); const clipboard = new Clipboard(".copy-link");
clipboard.on('success', () => { clipboard.on("success", () => {
ElMessage.success('复制成功!'); ElMessage.success("复制成功!");
}) });
clipboard.on('error', () => { clipboard.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
}) });
const initData = () => { const initData = () => {
checkSession().then(() => { checkSession()
isLogin.value = true .then(() => {
httpGet("/api/invite/code").then(res => { isLogin.value = true;
const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}` httpGet("/api/invite/code")
hits.value = res.data["hits"] .then((res) => {
regNum.value = res.data["reg_num"] const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}`;
if (hits.value > 0) { hits.value = res.data["hits"];
rate.value = ((regNum.value / hits.value) * 100).toFixed(2) regNum.value = res.data["reg_num"];
} if (hits.value > 0) {
QRCode.toDataURL(text, {width: 400, height: 400, margin: 2}, (error, url) => { rate.value = ((regNum.value / hits.value) * 100).toFixed(2);
if (error) { }
console.error(error) QRCode.toDataURL(
} else { text,
qrImg.value = url; { width: 400, height: 400, margin: 2 },
} (error, url) => {
}); if (error) {
inviteURL.value = text console.error(error);
}).catch(e => { } else {
ElMessage.error("获取邀请码失败:" + e.message) qrImg.value = url;
}) }
}
);
inviteURL.value = text;
})
.catch((e) => {
ElMessage.error("获取邀请码失败:" + e.message);
});
getSystemInfo().then(res => { getSystemInfo()
invitePower.value = res.data["invite_power"] .then((res) => {
}).catch(e => { invitePower.value = res.data["invite_power"];
ElMessage.error("获取系统配置失败:" + e.message) })
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
}) })
}).catch(() => { .catch(() => {
store.setShowLoginDialog(true) store.setShowLoginDialog(true);
}); });
} };
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@ -157,7 +174,7 @@ const initData = () => {
.page-invitation { .page-invitation {
display: flex; display: flex;
justify-content: center; justify-content: center;
background-color: #282c34; // background-color: #282c34;
height 100% height 100%
overflow-x hidden overflow-x hidden
overflow-y visible overflow-y visible
@ -167,17 +184,18 @@ const initData = () => {
flex-flow column flex-flow column
max-width 1000px max-width 1000px
width 100% width 100%
color #e1e1e1 color: var(--text-theme-color);
h2 { h2 {
color #ffffff; color: var(--theme-textcolor-normal);
text-align center text-align center
} }
.share-box { .share-box {
.info { .info {
line-height 1.5 line-height 1.5
border 1px solid #444444 // border 1px solid #444444
background:var(--chat-bg)
border-radius 10px border-radius 10px
padding 10px padding 10px

View File

@ -4,7 +4,7 @@
<div class="images"> <div class="images">
<template v-for="(img, index) in images" :key="img"> <template v-for="(img, index) in images" :key="img">
<div class="item"> <div class="item">
<el-image :src="replaceImg(img)" fit="cover"/> <el-image :src="replaceImg(img)" fit="cover" />
<el-icon @click="remove(img)"><CircleCloseFilled /></el-icon> <el-icon @click="remove(img)"><CircleCloseFilled /></el-icon>
</div> </div>
<div class="btn-swap" v-if="images.length === 2 && index === 0"> <div class="btn-swap" v-if="images.length === 2 && index === 0">
@ -16,22 +16,23 @@
<div class="input-container"> <div class="input-container">
<div class="upload-icon" v-if="images.length < 2"> <div class="upload-icon" v-if="images.length < 2">
<el-upload <el-upload
class="avatar-uploader" class="avatar-uploader"
:auto-upload="true" :auto-upload="true"
:show-file-list="false" :show-file-list="false"
:http-request="upload" :http-request="upload"
accept=".jpg,.png,.jpeg" accept=".jpg,.png,.jpeg"
> >
<i class="iconfont icon-image"></i> <i class="iconfont icon-image"></i>
</el-upload> </el-upload>
</div> </div>
<textarea <textarea
class="prompt-input" class="prompt-input"
:rows="row" :rows="row"
v-model="formData.prompt" v-model="formData.prompt"
placeholder="请输入提示词或者上传图片" placeholder="请输入提示词或者上传图片"
autofocus> autofocus
</textarea> >
</textarea>
<div class="send-icon" @click="create"> <div class="send-icon" @click="create">
<i class="iconfont icon-send"></i> <i class="iconfont icon-send"></i>
</div> </div>
@ -39,26 +40,34 @@
<div class="params"> <div class="params">
<div class="item-group"> <div class="item-group">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating"> <el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i> <i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
<span>生成AI视频提示词</span> <span>生成AI视频提示词</span>
</el-button> </el-button>
</div> </div>
<div class="item-group"> <div class="item-group">
<span class="label">循环参考图</span> <span class="label">循环参考图</span>
<el-switch v-model="formData.loop" size="small" style="--el-switch-on-color:#BF78BF;" /> <el-switch v-model="formData.loop" size="small" />
</div> </div>
<div class="item-group"> <div class="item-group">
<span class="label">提示词优化</span> <span class="label">提示词优化</span>
<el-switch v-model="formData.expand_prompt" size="small" style="--el-switch-on-color:#BF78BF;" /> <el-switch v-model="formData.expand_prompt" size="small" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<el-container
class="video-container"
<el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)"> v-loading="loading"
element-loading-background="rgba(100,100,100,0.3)"
>
<h2 class="h-title">你的作品</h2> <h2 class="h-title">你的作品</h2>
<div class="list-box" v-if="!noData"> <div class="list-box" v-if="!noData">
@ -67,36 +76,63 @@
<div class="left"> <div class="left">
<div class="container"> <div class="container">
<div v-if="item.progress === 100"> <div v-if="item.progress === 100">
<video class="video" :src="replaceImg(item.video_url)" preload="auto" loop="loop" muted="muted"> <video
class="video"
:src="replaceImg(item.video_url)"
preload="auto"
loop="loop"
muted="muted"
>
您的浏览器不支持视频播放 您的浏览器不支持视频播放
</video> </video>
<button class="play" @click="play(item)"> <button class="play" @click="play(item)">
<img src="/images/play.svg" alt=""/> <img src="/images/play.svg" alt="" />
</button> </button>
</div> </div>
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" /> <el-image
:src="item.cover_url"
fit="cover"
v-else-if="item.progress > 100"
/>
<generating message="正在生成视频" v-else /> <generating message="正在生成视频" v-else />
</div> </div>
</div> </div>
<div class="center"> <div class="center">
<div class="failed" v-if="item.progress === 101">任务执行失败{{item.err_msg}}任务提示词{{item.prompt}}</div> <div class="failed" v-if="item.progress === 101">
<div class="prompt" v-else>{{item.prompt}}</div> 任务执行失败{{ item.err_msg }}任务提示词{{ item.prompt }}
</div>
<div class="prompt" v-else>{{ item.prompt }}</div>
</div> </div>
<div class="right" v-if="item.progress === 100"> <div class="right" v-if="item.progress === 100">
<div class="tools"> <div class="tools">
<button class="btn btn-publish"> <button class="btn btn-publish">
<span class="text">发布</span> <span class="text">发布</span>
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" /> <black-switch
v-model:value="item.publish"
@change="publishJob(item)"
size="small"
/>
</button> </button>
<el-tooltip effect="light" content="下载视频" placement="top"> <el-tooltip content="下载视频" placement="top">
<button class="btn btn-icon" @click="download(item)" :disabled="item.downloading"> <button
<i class="iconfont icon-download" v-if="!item.downloading"></i> class="btn btn-icon"
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else /> @click="download(item)"
:disabled="item.downloading"
>
<i
class="iconfont icon-download"
v-if="!item.downloading"
></i>
<el-image
src="/images/loading.gif"
class="downloading"
fit="cover"
v-else
/>
</button> </button>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<button class="btn btn-icon" @click="removeJob(item)"> <button class="btn btn-icon" @click="removeJob(item)">
<i class="iconfont icon-remove"></i> <i class="iconfont icon-remove"></i>
</button> </button>
@ -111,210 +147,243 @@
</div> </div>
</div> </div>
</div> </div>
<el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else /> <el-empty
:image-size="100"
:image="nodata"
description="没有任何作品,赶紧去创作吧!"
v-else
/>
<div class="pagination"> <div class="pagination">
<el-pagination v-if="total > pageSize" background <el-pagination
style="--el-pagination-button-bg-color:#414141; v-if="total > pageSize"
--el-pagination-button-color:#d1d1d1; background
--el-disabled-bg-color:#414141; style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
--el-color-primary:#666666;
--el-pagination-hover-color:#e1e1e1"
layout="total,prev, pager, next" layout="total,prev, pager, next"
:hide-on-single-page="true" :hide-on-single-page="true"
v-model:current-page="page" v-model:current-page="page"
v-model:page-size="pageSize" v-model:page-size="pageSize"
@current-change="fetchData(page)" @current-change="fetchData(page)"
:total="total"/> :total="total"
/>
</div> </div>
</el-container> </el-container>
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto"> <black-dialog
<video style="width: 100%; max-height: 90vh;" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog"> v-model:show="showDialog"
title="预览视频"
hide-footer
@cancal="showDialog = false"
width="auto"
>
<video
style="width: 100%; max-height: 90vh"
:src="currentVideoUrl"
preload="auto"
:autoplay="true"
loop="loop"
muted="muted"
v-show="showDialog"
>
您的浏览器不支持视频播放 您的浏览器不支持视频播放
</video> </video>
</black-dialog> </black-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, onUnmounted, reactive, ref} from "vue"; import nodata from "@/assets/img/no-data.png";
import {CircleCloseFilled} from "@element-plus/icons-vue";
import {httpDownload, httpPost, httpGet} from "@/utils/http"; import { onMounted, onUnmounted, reactive, ref } from "vue";
import {checkSession, getClientId} from "@/store/cache"; import { CircleCloseFilled } from "@element-plus/icons-vue";
import {showMessageError, showMessageOK} from "@/utils/dialog"; import { httpDownload, httpPost, httpGet } from "@/utils/http";
import { replaceImg } from "@/utils/libs" import { checkSession, getClientId } from "@/store/cache";
import {ElMessage, ElMessageBox} from "element-plus"; import { showMessageError, showMessageOK } from "@/utils/dialog";
import { replaceImg } from "@/utils/libs";
import { ElMessage, ElMessageBox } from "element-plus";
import BlackSwitch from "@/components/ui/BlackSwitch.vue"; import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import Generating from "@/components/ui/Generating.vue"; import Generating from "@/components/ui/Generating.vue";
import BlackDialog from "@/components/ui/BlackDialog.vue"; import BlackDialog from "@/components/ui/BlackDialog.vue";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
const showDialog = ref(false) const showDialog = ref(false);
const currentVideoUrl = ref('') const currentVideoUrl = ref("");
const row = ref(1) const row = ref(1);
const images = ref([]) const images = ref([]);
const formData = reactive({ const formData = reactive({
client_id: getClientId(), client_id: getClientId(),
prompt: '', prompt: "",
expand_prompt: false, expand_prompt: false,
loop: false, loop: false,
first_frame_img: '', first_frame_img: "",
end_frame_img: '' end_frame_img: ""
}) });
const store = useSharedStore() const store = useSharedStore();
onMounted(()=>{ onMounted(() => {
checkSession().then(() => { checkSession().then(() => {
fetchData(1) fetchData(1);
}) });
store.addMessageHandler("luma",(data) => { store.addMessageHandler("luma", (data) => {
// //
if (data.channel !== "luma" || data.clientId !== getClientId()) { if (data.channel !== "luma" || data.clientId !== getClientId()) {
return return;
} }
if (data.body === "FINISH" || data.body === "FAIL") { if (data.body === "FINISH" || data.body === "FAIL") {
fetchData(1) fetchData(1);
} }
}) });
}) });
onUnmounted(() => { onUnmounted(() => {
store.removeMessageHandler("luma") store.removeMessageHandler("luma");
}) });
const download = (item) => { const download = (item) => {
const url = replaceImg(item.video_url) const url = replaceImg(item.video_url);
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}` const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`;
// parse filename // parse filename
const urlObj = new URL(url); const urlObj = new URL(url);
const fileName = urlObj.pathname.split('/').pop(); const fileName = urlObj.pathname.split("/").pop();
item.downloading = true item.downloading = true;
httpDownload(downloadURL).then(response => { httpDownload(downloadURL)
const blob = new Blob([response.data]); .then((response) => {
const link = document.createElement('a'); const blob = new Blob([response.data]);
link.href = URL.createObjectURL(blob); const link = document.createElement("a");
link.download = fileName; link.href = URL.createObjectURL(blob);
document.body.appendChild(link); link.download = fileName;
link.click(); document.body.appendChild(link);
document.body.removeChild(link); link.click();
URL.revokeObjectURL(link.href); document.body.removeChild(link);
item.downloading = false URL.revokeObjectURL(link.href);
}).catch(() => { item.downloading = false;
showMessageError("下载失败") })
item.downloading = false .catch(() => {
}) showMessageError("下载失败");
} item.downloading = false;
});
};
const play = (item) => { const play = (item) => {
currentVideoUrl.value = replaceImg(item.video_url) currentVideoUrl.value = replaceImg(item.video_url);
showDialog.value = true showDialog.value = true;
} };
const removeJob = (item) => { const removeJob = (item) => {
ElMessageBox.confirm( ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
'此操作将会删除任务相关文件,继续操作码?', confirmButtonText: "确认",
'删除提示', cancelButtonText: "取消",
{ type: "warning"
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
httpGet("/api/video/remove", {id: item.id}).then(() => {
ElMessage.success("任务删除成功")
fetchData()
}).catch(e => {
ElMessage.error("任务删除失败:" + e.message)
})
}).catch(() => {
}) })
} .then(() => {
httpGet("/api/video/remove", { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
fetchData();
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
})
.catch(() => {});
};
const publishJob = (item) => { const publishJob = (item) => {
httpGet("/api/video/publish", {id: item.id, publish:item.publish}).then(() => { httpGet("/api/video/publish", { id: item.id, publish: item.publish })
ElMessage.success("操作成功") .then(() => {
}).catch(e => { ElMessage.success("操作成功");
ElMessage.error("操作失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("操作失败:" + e.message);
});
};
const upload = (file) => { const upload = (file) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file.file, file.name); formData.append("file", file.file, file.name);
// //
httpPost('/api/upload', formData).then((res) => { httpPost("/api/upload", formData)
images.value.push(res.data.url) .then((res) => {
ElMessage.success({message: "上传成功", duration: 500}) images.value.push(res.data.url);
}).catch((e) => { ElMessage.success({ message: "上传成功", duration: 500 });
ElMessage.error('图片上传失败:' + e.message) })
}) .catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
});
}; };
const remove = (img) => { const remove = (img) => {
images.value = images.value.filter(item => item !== img) images.value = images.value.filter((item) => item !== img);
} };
const switchReverse = () => { const switchReverse = () => {
images.value = images.value.reverse() images.value = images.value.reverse();
} };
const loading = ref(false) const loading = ref(false);
const list = ref([]) const list = ref([]);
const noData = ref(true) const noData = ref(true);
const page = ref(1) const page = ref(1);
const pageSize = ref(10) const pageSize = ref(10);
const total = ref(0) const total = ref(0);
const fetchData = (_page) => { const fetchData = (_page) => {
if (_page) { if (_page) {
page.value = _page page.value = _page;
} }
httpGet("/api/video/list",{page:page.value, page_size:pageSize.value, type: 'luma'}).then(res => { httpGet("/api/video/list", {
total.value = res.data.total page: page.value,
loading.value = false page_size: pageSize.value,
list.value = res.data.items type: "luma"
noData.value = list.value.length === 0
}).catch(() => {
loading.value = false
noData.value = true
}) })
} .then((res) => {
total.value = res.data.total;
loading.value = false;
list.value = res.data.items;
noData.value = list.value.length === 0;
})
.catch(() => {
loading.value = false;
noData.value = true;
});
};
// //
const create = () => { const create = () => {
const len = images.value.length;
const len = images.value.length; if (len) {
if(len){ formData.first_frame_img = images.value[0];
formData.first_frame_img = images.value[0] if (len === 2) {
if(len === 2){ formData.end_frame_img = images.value[1];
formData.end_frame_img = images.value[1]
} }
} }
httpPost("/api/video/luma/create", formData).then(() => { httpPost("/api/video/luma/create", formData)
fetchData(1) .then(() => {
showMessageOK("创建任务成功") fetchData(1);
}).catch(e => { showMessageOK("创建任务成功");
showMessageError("创建任务失败:"+e.message) })
}) .catch((e) => {
} showMessageError("创建任务失败:" + e.message);
});
};
const isGenerating = ref(false) const isGenerating = ref(false);
const generatePrompt = () => { const generatePrompt = () => {
if (formData.prompt === "") { if (formData.prompt === "") {
return showMessageError("请输入原始提示词") return showMessageError("请输入原始提示词");
} }
isGenerating.value = true isGenerating.value = true;
httpPost("/api/prompt/image", {prompt: formData.prompt}).then(res => { httpPost("/api/prompt/image", { prompt: formData.prompt })
formData.prompt = res.data .then((res) => {
isGenerating.value = false formData.prompt = res.data;
}).catch(e => { isGenerating.value = false;
showMessageError("生成提示词失败:"+e.message) })
isGenerating.value = false .catch((e) => {
}) showMessageError("生成提示词失败:" + e.message);
} isGenerating.value = false;
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@ -7,64 +7,81 @@
<div class="mark-map-params"> <div class="mark-map-params">
<el-form label-width="80px" label-position="left"> <el-form label-width="80px" label-position="left">
<div class="param-line"> <div class="param-line">你的需求</div>
你的需求
</div>
<div class="param-line"> <div class="param-line">
<el-input <el-input
v-model="prompt" v-model="prompt"
:autosize="{ minRows: 4, maxRows: 6 }" :autosize="{ minRows: 4, maxRows: 6 }"
type="textarea" type="textarea"
placeholder="请给AI输入提示词让AI帮你完善" placeholder="请给AI输入提示词让AI帮你完善"
/> />
</div> </div>
<div class="param-line">请选择生成思维导图的AI模型</div>
<div class="param-line"> <div class="param-line">
请选择生成思维导图的AI模型 <el-select
</div> v-model="modelID"
<div class="param-line"> placeholder="请选择模型"
<el-select v-model="modelID" placeholder="请选择模型" style="width:100%"> style="width: 100%"
>
<el-option <el-option
v-for="item in models" v-for="item in models"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
> >
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
<el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{ <el-tag
item.power style="margin-left: 5px; position: relative; top: -2px"
}}算力 type="info"
size="small"
>{{ item.power }}算力
</el-tag> </el-tag>
</el-option> </el-option>
</el-select> </el-select>
</div> </div>
<div class="text-info"> <div class="text-info">
<el-tag type="success">当前可用算力{{ loginUser.power }}</el-tag> <el-text type="primary"
>当前可用算力<el-text type="warning">{{
loginUser.power
}}</el-text></el-text
>
</div> </div>
<div class="param-line"> <div class="submit-btn flex-center">
<el-button color="#47fff1" :dark="false" round @click="generateAI" :loading="loading"> <el-button
style="width: 200px"
type="primary"
:dark="false"
round
@click="generateAI"
:loading="loading"
>
生成思维导图 生成思维导图
</el-button> </el-button>
</div> </div>
<div class="param-line"> <div class="param-line">使用已有内容生成</div>
使用已有内容生成
</div>
<div class="param-line"> <div class="param-line">
<el-input <el-input
v-model="content" v-model="content"
:autosize="{ minRows: 4, maxRows: 6 }" :autosize="{ minRows: 4, maxRows: 6 }"
type="textarea" type="textarea"
placeholder="请用markdown语法输入您想要生成思维导图的内容" placeholder="请用markdown语法输入您想要生成思维导图的内容"
/> />
</div> </div>
<div class="param-line"> <div class="param-line flex-center">
<el-button color="#C5F9AE" :dark="false" round @click="generate">直接生成免费</el-button> <el-button
color="rgb(78, 51, 254)"
style="width: 200px"
:dark="false"
round
@click="generate"
>直接生成免费</el-button
>
</div> </div>
</el-form> </el-form>
</div> </div>
</div> </div>
@ -73,188 +90,196 @@
<div class="top-bar"> <div class="top-bar">
<el-button @click="downloadImage" type="primary"> <el-button @click="downloadImage" type="primary">
<el-icon> <el-icon>
<Download/> <Download />
</el-icon> </el-icon>
<span>下载图片</span> <span>下载图片</span>
</el-button> </el-button>
</div> </div>
<div class="body" id="markmap"> <div class="body" id="markmap">
<svg ref="svgRef" :style="{ height: rightBoxHeight + 'px' }"/> <svg ref="svgRef" :style="{ height: rightBoxHeight + 'px' }" />
<div id="toolbar"></div> <div id="toolbar"></div>
</div> </div>
</div><!-- end task list box --> </div>
<!-- end task list box -->
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {nextTick, ref} from 'vue'; import { nextTick, ref } from "vue";
import {Markmap} from 'markmap-view'; import { Markmap } from "markmap-view";
import {Transformer} from 'markmap-lib'; import { Transformer } from "markmap-lib";
import {checkSession, getSystemInfo} from "@/store/cache"; import { checkSession, getSystemInfo } from "@/store/cache";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {Download} from "@element-plus/icons-vue"; import { Download } from "@element-plus/icons-vue";
import {Toolbar} from 'markmap-toolbar'; import { Toolbar } from "markmap-toolbar";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
const leftBoxHeight = ref(window.innerHeight - 105) const leftBoxHeight = ref(window.innerHeight - 105);
const rightBoxHeight = ref(window.innerHeight - 115) const rightBoxHeight = ref(window.innerHeight - 115);
const prompt = ref("") const prompt = ref("");
const text = ref("") const text = ref("");
const content = ref(text.value) const content = ref(text.value);
const html = ref("") const html = ref("");
const isLogin = ref(false) const isLogin = ref(false);
const loginUser = ref({power: 0}) const loginUser = ref({ power: 0 });
const transformer = new Transformer(); const transformer = new Transformer();
const store = useSharedStore(); const store = useSharedStore();
const loading = ref(false) const loading = ref(false);
const svgRef = ref(null) const svgRef = ref(null);
const markMap = ref(null) const markMap = ref(null);
const models = ref([]) const models = ref([]);
const modelID = ref(0) const modelID = ref(0);
getSystemInfo().then(res => { getSystemInfo()
text.value = res.data['mark_map_text'] .then((res) => {
content.value = text.value text.value = res.data["mark_map_text"];
initData() content.value = text.value;
nextTick(() => { initData();
try { nextTick(() => {
markMap.value = Markmap.create(svgRef.value) try {
const {el} = Toolbar.create(markMap.value); markMap.value = Markmap.create(svgRef.value);
document.getElementById('toolbar').append(el); const { el } = Toolbar.create(markMap.value);
update() document.getElementById("toolbar").append(el);
} catch (e) { update();
console.error(e) } catch (e) {
} console.error(e);
}
});
}) })
}).catch(e => { .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message);
}) });
const initData = () => { const initData = () => {
httpGet("/api/model/list").then(res => { httpGet("/api/model/list")
for (let v of res.data) { .then((res) => {
models.value.push(v) for (let v of res.data) {
} models.value.push(v);
modelID.value = models.value[0].id }
}).catch(e => { modelID.value = models.value[0].id;
ElMessage.error("获取模型失败:" + e.message) })
}) .catch((e) => {
ElMessage.error("获取模型失败:" + e.message);
checkSession().then(user => { });
loginUser.value = user
isLogin.value = true checkSession()
}).catch(() => { .then((user) => {
}); loginUser.value = user;
} isLogin.value = true;
})
.catch(() => {});
};
const update = () => { const update = () => {
try { try {
const {root} = transformer.transform(processContent(text.value)) const { root } = transformer.transform(processContent(text.value));
markMap.value.setData(root) markMap.value.setData(root);
markMap.value.fit() markMap.value.fit();
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
} };
const processContent = (text) => { const processContent = (text) => {
if (!text) { if (!text) {
return text return text;
} }
const arr = [] const arr = [];
const lines = text.split("\n") const lines = text.split("\n");
for (let line of lines) { for (let line of lines) {
if (line.indexOf("```") !== -1) { if (line.indexOf("```") !== -1) {
continue continue;
} }
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '') line = line.replace(/([*_~`>])|(\d+\.)\s/g, "");
arr.push(line) arr.push(line);
} }
return arr.join("\n") return arr.join("\n");
} };
window.onresize = () => { window.onresize = () => {
leftBoxHeight.value = window.innerHeight - 145 leftBoxHeight.value = window.innerHeight - 145;
rightBoxHeight.value = window.innerHeight - 85 rightBoxHeight.value = window.innerHeight - 85;
} };
const generate = () => { const generate = () => {
text.value = content.value text.value = content.value;
update() update();
} };
// 使 AI // 使 AI
const generateAI = () => { const generateAI = () => {
html.value = '' html.value = "";
text.value = '' text.value = "";
if (prompt.value === '') { if (prompt.value === "") {
return ElMessage.error("请输入你的需求") return ElMessage.error("请输入你的需求");
} }
if (!isLogin.value) { if (!isLogin.value) {
store.setShowLoginDialog(true) store.setShowLoginDialog(true);
return return;
} }
loading.value = true loading.value = true;
httpPost("/api/markMap/gen", { httpPost("/api/markMap/gen", {
prompt:prompt.value, prompt: prompt.value,
model_id: modelID.value model_id: modelID.value
}).then(res => {
text.value = res.data
content.value = processContent(text.value)
const model = getModelById(modelID.value)
loginUser.value.power -= model.power
nextTick(() => update())
loading.value = false
}).catch(e => {
ElMessage.error("生成思维导图失败:" + e.message)
loading.value = false
}) })
} .then((res) => {
text.value = res.data;
content.value = processContent(text.value);
const model = getModelById(modelID.value);
loginUser.value.power -= model.power;
nextTick(() => update());
loading.value = false;
})
.catch((e) => {
ElMessage.error("生成思维导图失败:" + e.message);
loading.value = false;
});
};
const getModelById = (modelId) => { const getModelById = (modelId) => {
for (let m of models.value) { for (let m of models.value) {
if (m.id === modelId) { if (m.id === modelId) {
return m return m;
} }
} }
} };
// download SVG to png file // download SVG to png file
const downloadImage = () => { const downloadImage = () => {
const svgElement = document.getElementById("markmap"); const svgElement = document.getElementById("markmap");
// SVG // SVG
const serializer = new XMLSerializer() const serializer = new XMLSerializer();
const source = '<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(svgRef.value) const source =
const image = new Image() '<?xml version="1.0" standalone="no"?>\r\n' +
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source) serializer.serializeToString(svgRef.value);
const image = new Image();
image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
// //
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas");
canvas.width = svgElement.offsetWidth canvas.width = svgElement.offsetWidth;
canvas.height = svgElement.offsetHeight canvas.height = svgElement.offsetHeight;
let context = canvas.getContext('2d') let context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height); context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'white'; context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height); context.fillRect(0, 0, canvas.width, canvas.height);
image.onload = function () { image.onload = function () {
context.drawImage(image, 0, 0) context.drawImage(image, 0, 0);
const a = document.createElement('a') const a = document.createElement("a");
a.download = "geek-ai-xmind.png" a.download = "geek-ai-xmind.png";
a.href = canvas.toDataURL(`image/png`) a.href = canvas.toDataURL(`image/png`);
a.click() a.click();
} };
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@ -1,25 +1,39 @@
<template> <template>
<div> <div>
<div class="member custom-scroll" v-loading="loading" element-loading-background="rgba(255,255,255,.3)" :element-loading-text="loadingText"> <div
class="member custom-scroll"
v-loading="loading"
element-loading-background="rgba(255,255,255,.3)"
:element-loading-text="loadingText"
>
<div class="inner"> <div class="inner">
<div class="user-profile"> <div class="user-profile">
<user-profile :key="profileKey"/> <user-profile :key="profileKey" />
<el-row class="user-opt" :gutter="20"> <el-row class="user-opt" :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-button type="primary" @click="showBindEmailDialog = true">绑定邮箱</el-button> <el-button type="primary" @click="showBindEmailDialog = true"
>绑定邮箱</el-button
>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-button type="primary" @click="showBindMobileDialog = true">绑定手机</el-button> <el-button type="primary" @click="showBindMobileDialog = true"
>绑定手机</el-button
>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-button type="primary" @click="showThirdLoginDialog = true">第三方登录</el-button> <el-button type="primary" @click="showThirdLoginDialog = true"
>第三方登录</el-button
>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-button type="primary" @click="showPasswordDialog = true">修改密码</el-button> <el-button type="primary" @click="showPasswordDialog = true"
>修改密码</el-button
>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-button type="primary" @click="showRedeemVerifyDialog = true">卡密兑换 <el-button type="primary" @click="showRedeemVerifyDialog = true"
>卡密兑换
</el-button> </el-button>
</el-col> </el-col>
</el-row> </el-row>
@ -36,7 +50,7 @@
<el-col v-for="item in list" :key="item" :span="6"> <el-col v-for="item in list" :key="item" :span="6">
<div class="product-item"> <div class="product-item">
<div class="image-container"> <div class="image-container">
<el-image :src="vipImg" fit="cover"/> <el-image :src="vipImg" fit="cover" />
</div> </div>
<div class="product-title"> <div class="product-title">
<span class="name">{{ item.name }}</span> <span class="name">{{ item.name }}</span>
@ -44,7 +58,9 @@
<div class="product-info"> <div class="product-info">
<div class="info-line"> <div class="info-line">
<span class="label">商品原价</span> <span class="label">商品原价</span>
<span class="price"><del>{{ item.price }}</del></span> <span class="price"
><del>{{ item.price }}</del></span
>
</div> </div>
<div class="info-line"> <div class="info-line">
<span class="label">优惠价</span> <span class="label">优惠价</span>
@ -52,7 +68,9 @@
</div> </div>
<div class="info-line"> <div class="info-line">
<span class="label">有效期</span> <span class="label">有效期</span>
<span class="expire" v-if="item.days > 0">{{ item.days }}</span> <span class="expire" v-if="item.days > 0"
>{{ item.days }}</span
>
<span class="expire" v-else>长期有效</span> <span class="expire" v-else>长期有效</span>
</div> </div>
@ -62,22 +80,42 @@
</div> </div>
<div class="pay-way"> <div class="pay-way">
<span
<span type="primary" v-for="payWay in payWays" @click="pay(item,payWay)" :key="payWay"> type="primary"
<el-button v-if="payWay.pay_type==='alipay'" color="#15A6E8" circle> v-for="payWay in payWays"
<i class="iconfont icon-alipay" ></i> @click="pay(item, payWay)"
:key="payWay"
>
<el-button
v-if="payWay.pay_type === 'alipay'"
color="#15A6E8"
circle
>
<i class="iconfont icon-alipay"></i>
</el-button> </el-button>
<el-button v-else-if="payWay.pay_type==='qqpay'" circle> <el-button v-else-if="payWay.pay_type === 'qqpay'" circle>
<i class="iconfont icon-qq"></i> <i class="iconfont icon-qq"></i>
</el-button> </el-button>
<el-button v-else-if="payWay.pay_type==='paypal'" class="paypal" round> <el-button
v-else-if="payWay.pay_type === 'paypal'"
class="paypal"
round
>
<i class="iconfont icon-paypal"></i> <i class="iconfont icon-paypal"></i>
</el-button> </el-button>
<el-button v-else-if="payWay.pay_type==='jdpay'" color="#E1251B" circle> <el-button
v-else-if="payWay.pay_type === 'jdpay'"
color="#E1251B"
circle
>
<i class="iconfont icon-jd-pay"></i> <i class="iconfont icon-jd-pay"></i>
</el-button> </el-button>
<el-button v-else-if="payWay.pay_type==='douyin'" class="douyin" circle> <el-button
<i class="iconfont icon-douyin"></i> v-else-if="payWay.pay_type === 'douyin'"
class="douyin"
circle
>
<i class="iconfont icon-douyin"></i>
</el-button> </el-button>
<el-button v-else circle class="wechat" color="#67C23A"> <el-button v-else circle class="wechat" color="#67C23A">
<i class="iconfont icon-wechat-pay"></i> <i class="iconfont icon-wechat-pay"></i>
@ -93,116 +131,155 @@
<h2 class="headline">消费账单</h2> <h2 class="headline">消费账单</h2>
<div class="user-order"> <div class="user-order">
<user-order v-if="isLogin" :key="userOrderKey"/> <user-order v-if="isLogin" :key="userOrderKey" />
</div> </div>
</div> </div>
</div> </div>
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"/> <password-dialog
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false"/> v-if="isLogin"
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false"/> :show="showPasswordDialog"
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false"/> @hide="showPasswordDialog = false"
<redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback"/> />
<bind-mobile
v-if="isLogin"
:show="showBindMobileDialog"
@hide="showBindMobileDialog = false"
/>
<bind-email
v-if="isLogin"
:show="showBindEmailDialog"
@hide="showBindEmailDialog = false"
/>
<third-login
v-if="isLogin"
:show="showThirdLoginDialog"
@hide="showThirdLoginDialog = false"
/>
<redeem-verify
v-if="isLogin"
:show="showRedeemVerifyDialog"
@hide="redeemCallback"
/>
</div> </div>
<el-dialog v-model="showDialog" :show-close=false :close-on-click-modal="false" hide-footer width="auto" class="pay-dialog"> <el-dialog
v-model="showDialog"
:show-close="false"
:close-on-click-modal="false"
hide-footer
width="auto"
class="pay-dialog"
>
<div v-if="qrImg !== ''"> <div v-if="qrImg !== ''">
<div class="product-info">请使用微信扫码支付<span class="price">{{price}}</span></div> <div class="product-info">
请使用微信扫码支付<span class="price">{{ price }}</span>
</div>
<el-image :src="qrImg" fit="cover" /> <el-image :src="qrImg" fit="cover" />
</div> </div>
<div style="padding-bottom: 10px; text-align: center"> <div style="padding-bottom: 10px; text-align: center">
<el-button type="success" @click="payCallback(true)">支付成功</el-button> <el-button type="success" @click="payCallback(true)"
<el-button type="danger" @click="payCallback(false)">支付失败</el-button> >支付成功</el-button
>
<el-button type="danger" @click="payCallback(false)"
>支付失败</el-button
>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import { onMounted, ref } from "vue";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {checkSession, getSystemInfo} from "@/store/cache"; import { checkSession, getSystemInfo } from "@/store/cache";
import UserProfile from "@/components/UserProfile.vue"; import UserProfile from "@/components/UserProfile.vue";
import PasswordDialog from "@/components/PasswordDialog.vue"; import PasswordDialog from "@/components/PasswordDialog.vue";
import BindMobile from "@/components/BindMobile.vue"; import BindMobile from "@/components/BindMobile.vue";
import RedeemVerify from "@/components/RedeemVerify.vue"; import RedeemVerify from "@/components/RedeemVerify.vue";
import UserOrder from "@/components/UserOrder.vue"; import UserOrder from "@/components/UserOrder.vue";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
import BindEmail from "@/components/BindEmail.vue"; import BindEmail from "@/components/BindEmail.vue";
import ThirdLogin from "@/components/ThirdLogin.vue"; import ThirdLogin from "@/components/ThirdLogin.vue";
import QRCode from "qrcode"; import QRCode from "qrcode";
const list = ref([]) const list = ref([]);
const vipImg = ref("/images/vip.png") const vipImg = ref("/images/menu/member.png");
const enableReward = ref(false) // const enableReward = ref(false); //
const rewardImg = ref('/images/reward.png') const rewardImg = ref("/images/reward.png");
const showPasswordDialog = ref(false) const showPasswordDialog = ref(false);
const showBindMobileDialog = ref(false) const showBindMobileDialog = ref(false);
const showBindEmailDialog = ref(false) const showBindEmailDialog = ref(false);
const showRedeemVerifyDialog = ref(false) const showRedeemVerifyDialog = ref(false);
const showThirdLoginDialog = ref(false) const showThirdLoginDialog = ref(false);
const user = ref(null) const user = ref(null);
const isLogin = ref(false) const isLogin = ref(false);
const orderTimeout = ref(1800) const orderTimeout = ref(1800);
const loading = ref(true) const loading = ref(true);
const loadingText = ref("加载中...") const loadingText = ref("加载中...");
const orderPayInfoText = ref("") const orderPayInfoText = ref("");
const payWays = ref([])
const vipInfoText = ref("")
const store = useSharedStore()
const profileKey = ref(0)
const userOrderKey = ref(0)
const showDialog = ref(false)
const qrImg = ref("")
const price = ref(0)
const payWays = ref([]);
const vipInfoText = ref("");
const store = useSharedStore();
const profileKey = ref(0);
const userOrderKey = ref(0);
const showDialog = ref(false);
const qrImg = ref("");
const price = ref(0);
onMounted(() => { onMounted(() => {
checkSession().then(_user => { checkSession()
user.value = _user .then((_user) => {
isLogin.value = true user.value = _user;
}).catch(() => { isLogin.value = true;
store.setShowLoginDialog(true) })
}) .catch(() => {
store.setShowLoginDialog(true);
});
httpGet("/api/product/list").then((res) => { httpGet("/api/product/list")
list.value = res.data .then((res) => {
loading.value = false list.value = res.data;
}).catch(e => { loading.value = false;
ElMessage.error("获取产品套餐失败:" + e.message) })
}) .catch((e) => {
ElMessage.error("获取产品套餐失败:" + e.message);
});
getSystemInfo().then(res => { getSystemInfo()
rewardImg.value = res.data['reward_img'] .then((res) => {
enableReward.value = res.data['enabled_reward'] rewardImg.value = res.data["reward_img"];
orderPayInfoText.value = res.data['order_pay_info_text'] enableReward.value = res.data["enabled_reward"];
if (res.data['order_pay_timeout'] > 0) { orderPayInfoText.value = res.data["order_pay_info_text"];
orderTimeout.value = res.data['order_pay_timeout'] if (res.data["order_pay_timeout"] > 0) {
} orderTimeout.value = res.data["order_pay_timeout"];
vipInfoText.value = res.data['vip_info_text'] }
}).catch(e => { vipInfoText.value = res.data["vip_info_text"];
ElMessage.error("获取系统配置失败:" + e.message) })
}) .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
httpGet("/api/payment/payWays").then(res => { httpGet("/api/payment/payWays")
payWays.value = res.data .then((res) => {
}).catch(e => { payWays.value = res.data;
ElMessage.error("获取支付方式失败:" + e.message) })
}) .catch((e) => {
}) ElMessage.error("获取支付方式失败:" + e.message);
});
});
const pay = (product, payWay) => { const pay = (product, payWay) => {
if (!isLogin.value) { if (!isLogin.value) {
store.setShowLoginDialog(true) store.setShowLoginDialog(true);
return return;
} }
loading.value = true loading.value = true;
loadingText.value = "正在生成支付订单..." loadingText.value = "正在生成支付订单...";
let host = process.env.VUE_APP_API_HOST let host = process.env.VUE_APP_API_HOST;
if (host === '') { if (host === "") {
host = `${location.protocol}//${location.host}`; host = `${location.protocol}//${location.host}`;
} }
httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, { httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, {
@ -212,45 +289,49 @@ const pay = (product, payWay) => {
user_id: user.value.id, user_id: user.value.id,
host: host, host: host,
device: "jump" device: "jump"
}).then(res => {
showDialog.value = true
loading.value = false
if (payWay.pay_way === 'wechat') {
price.value = Number(product.discount)
QRCode.toDataURL(res.data, {width: 300, height: 300, margin: 2}, (error, url) => {
if (error) {
console.error(error)
} else {
qrImg.value = url;
}
})
} else {
window.open(res.data, '_blank');
}
}).catch(e => {
setTimeout(() => {
ElMessage.error("生成支付订单失败:" + e.message)
loading.value = false
}, 500)
}) })
} .then((res) => {
showDialog.value = true;
loading.value = false;
if (payWay.pay_way === "wechat") {
price.value = Number(product.discount);
QRCode.toDataURL(
res.data,
{ width: 300, height: 300, margin: 2 },
(error, url) => {
if (error) {
console.error(error);
} else {
qrImg.value = url;
}
}
);
} else {
window.open(res.data, "_blank");
}
})
.catch((e) => {
setTimeout(() => {
ElMessage.error("生成支付订单失败:" + e.message);
loading.value = false;
}, 500);
});
};
const redeemCallback = (success) => { const redeemCallback = (success) => {
showRedeemVerifyDialog.value = false showRedeemVerifyDialog.value = false;
if (success) { if (success) {
profileKey.value += 1 profileKey.value += 1;
} }
} };
const payCallback = (success) => { const payCallback = (success) => {
showDialog.value = false showDialog.value = false;
if (success) { if (success) {
profileKey.value += 1 profileKey.value += 1;
userOrderKey.value += 1 userOrderKey.value += 1;
} }
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@ -1,127 +1,160 @@
<template> <template>
<div class="power-log custom-scroll" v-loading="loading"> <div class="power-log custom-scroll" v-loading="loading">
<div class="inner" :style="{height: listBoxHeight + 'px'}"> <!-- :style="{ height: listBoxHeight + 'px' }" -->
<div class="inner">
<div class="list-box"> <div class="list-box">
<div class="handle-box"> <div class="handle-box">
<el-input v-model="query.model" placeholder="模型" class="handle-input mr10" clearable></el-input> <el-input
v-model="query.model"
placeholder="模型"
class="handle-input mr10"
clearable
></el-input>
<el-date-picker <el-date-picker
v-model="query.date" v-model="query.date"
type="daterange" type="daterange"
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="margin: 0 10px;width: 200px;" style="margin: 0 10px; width: 200px"
/> />
<el-button color="#21aa93" :icon="Search" @click="fetchData">搜索</el-button> <el-button type="primary" :icon="Search" @click="fetchData"
>搜索</el-button
>
</div> </div>
<el-row v-if="items.length > 0"> <el-row v-if="items.length > 0">
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border <el-table
style="--el-table-border-color:#373C47; :data="items"
--el-table-tr-bg-color:#2D323B; :row-key="(row) => row.id"
--el-table-row-hover-bg-color:#373C47; table-layout="auto"
--el-table-header-bg-color:#474E5C; border
--el-table-text-color:#d1d1d1"> >
<el-table-column prop="username" label="用户" width="130px"/> <el-table-column prop="username" label="用户" width="130px" />
<el-table-column prop="model" label="模型" width="130px"/> <el-table-column prop="model" label="模型" width="130px" />
<el-table-column prop="type" label="类型"> <el-table-column prop="type" label="类型">
<template #default="scope"> <template #default="scope">
<el-tag size="small" :type="tagColors[scope.row.type]">{{ scope.row.type_str }}</el-tag> <el-tag size="small" :type="tagColors[scope.row.type]">{{
scope.row.type_str
}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="数额"> <el-table-column label="数额">
<template #default="scope"> <template #default="scope">
<div> <div>
<el-text type="success" v-if="scope.row.mark === 1">+{{ scope.row.amount }}</el-text> <el-text type="success" v-if="scope.row.mark === 1"
<el-text type="danger" v-if="scope.row.mark === 0">-{{ scope.row.amount }}</el-text> >+{{ scope.row.amount }}</el-text
>
<el-text type="danger" v-if="scope.row.mark === 0"
>-{{ scope.row.amount }}</el-text
>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="balance" label="余额"/> <el-table-column prop="balance" label="余额" />
<el-table-column label="发生时间" width="160px"> <el-table-column label="发生时间" width="160px">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span> <span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注"/> <el-table-column prop="remark" label="备注" />
</el-table> </el-table>
<div class="pagination"> <div class="pagination">
<el-pagination v-if="total > 0" background <el-pagination
layout="total,prev, pager, next" v-if="total > 0"
:hide-on-single-page="true" background
v-model:current-page="page" layout="total,prev, pager, next"
v-model:page-size="pageSize" style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
@current-change="fetchData()" :hide-on-single-page="true"
:total="total"/> v-model:current-page="page"
v-model:page-size="pageSize"
@current-change="fetchData()"
:total="total"
/>
</div> </div>
</el-row> </el-row>
<el-empty :image-size="100" v-else/> <el-empty
:image-size="100"
v-else
:image="nodata"
description="暂无数据"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import nodata from "@/assets/img/no-data.png";
import {dateFormat} from "@/utils/libs";
import {Search} from "@element-plus/icons-vue";
import Clipboard from "clipboard";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {checkSession} from "@/store/cache";
const items = ref([]) import { onMounted, ref } from "vue";
const total = ref(0) import { dateFormat } from "@/utils/libs";
const page = ref(1) import { Search } from "@element-plus/icons-vue";
const pageSize = ref(20) import Clipboard from "clipboard";
const loading = ref(false) import { ElMessage } from "element-plus";
const listBoxHeight = window.innerHeight - 87 import { httpPost } from "@/utils/http";
import { checkSession } from "@/store/cache";
const items = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const loading = ref(false);
const listBoxHeight = window.innerHeight - 87;
const query = ref({ const query = ref({
model: "", model: "",
date: [] date: []
}) });
const tagColors = ref(["primary", "success", "primary", "danger", "info", "warning"]) const tagColors = ref([
"primary",
"success",
"primary",
"danger",
"info",
"warning"
]);
onMounted(() => { onMounted(() => {
checkSession().then(() => { checkSession()
fetchData() .then(() => {
}).catch(() => { fetchData();
}) })
const clipboard = new Clipboard('.copy-order-no'); .catch(() => {});
clipboard.on('success', () => { const clipboard = new Clipboard(".copy-order-no");
clipboard.on("success", () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) });
clipboard.on('error', () => { clipboard.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
}) });
// //
const fetchData = () => { const fetchData = () => {
loading.value = true loading.value = true;
httpPost('/api/powerLog/list', { httpPost("/api/powerLog/list", {
model: query.value.model, model: query.value.model,
date: query.value.date, date: query.value.date,
page: page.value, page: page.value,
page_size: pageSize.value page_size: pageSize.value
}).then((res) => {
if (res.data) {
items.value = res.data.items
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.page_size
}
loading.value = false
}).catch(e => {
loading.value = false
ElMessage.error("获取数据失败:" + e.message);
}) })
} .then((res) => {
if (res.data) {
items.value = res.data.items;
total.value = res.data.total;
page.value = res.data.page;
pageSize.value = res.data.page_size;
}
loading.value = false;
})
.catch((e) => {
loading.value = false;
ElMessage.error("获取数据失败:" + e.message);
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@ -135,7 +168,10 @@ const fetchData = () => {
.list-box { .list-box {
overflow-x hidden overflow-x hidden
//overflow-y auto //overflow-y auto
background: var(--chat-bg);
padding: 20px;
margin-top: 20px;
border-radius: 10px;
.handle-box { .handle-box {
padding 20px 0 padding 20px 0

View File

@ -1,93 +1,104 @@
<template> <template>
<div class="page-song" :style="{ height: winHeight + 'px' }"> <div class="page-song" :style="{ height: winHeight + 'px' }">
<div class="inner"> <div class="inner">
<h2 class="title">{{song.title}}</h2> <h2 class="title">{{ song.title }}</h2>
<div class="row tags" v-if="song.tags"> <div class="row tags" v-if="song.tags">
<span>{{song.tags}}</span> <span>{{ song.tags }}</span>
</div> </div>
<div class="row author"> <div class="row author">
<span> <span>
<el-avatar :size="32" :src="song.user?.avatar" /> <el-avatar :size="32" :src="song.user?.avatar" />
</span> </span>
<span class="nickname">{{song.user?.nickname}}</span> <span class="nickname">{{ song.user?.nickname }}</span>
<button class="btn btn-icon" @click="play"> <button class="btn btn-icon" @click="play">
<i class="iconfont icon-play"></i> {{song.play_times}} <i class="iconfont icon-play"></i> {{ song.play_times }}
</button> </button>
<el-tooltip effect="light" content="复制歌曲链接" placement="top"> <el-tooltip content="复制歌曲链接" placement="top">
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)" > <button
class="btn btn-icon copy-link"
:data-clipboard-text="getShareURL(song)"
>
<i class="iconfont icon-share1"></i> <i class="iconfont icon-share1"></i>
</button> </button>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="row date"> <div class="row date">
<span>{{dateFormat(song.created_at)}}</span> <span>{{ dateFormat(song.created_at) }}</span>
<span class="version">{{song.raw_data?.major_model_version}}</span> <span class="version">{{ song.raw_data?.major_model_version }}</span>
</div> </div>
<div class="row"> <div class="row">
<textarea class="prompt" maxlength="2000" rows="18" readonly>{{song.prompt}}</textarea> <textarea class="prompt" maxlength="2000" rows="18" readonly>{{
song.prompt
}}</textarea>
</div> </div>
</div> </div>
<div class="music-player" v-if="playList.length > 0"> <div class="music-player" v-if="playList.length > 0">
<music-player :songs="playList" ref="playerRef" @play="song.play_times += 1"/> <music-player
:songs="playList"
ref="playerRef"
@play="song.play_times += 1"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, onUnmounted, ref} from "vue" import { onMounted, onUnmounted, ref } from "vue";
import {useRouter} from "vue-router"; import { useRouter } from "vue-router";
import {httpGet} from "@/utils/http"; import { httpGet } from "@/utils/http";
import {showMessageError} from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
import {dateFormat} from "@/utils/libs"; import { dateFormat } from "@/utils/libs";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import MusicPlayer from "@/components/MusicPlayer.vue"; import MusicPlayer from "@/components/MusicPlayer.vue";
const router = useRouter() const router = useRouter();
const id = router.currentRoute.value.params.id const id = router.currentRoute.value.params.id;
const song = ref({title:""}) const song = ref({ title: "" });
const playList = ref([]) const playList = ref([]);
const playerRef = ref(null) const playerRef = ref(null);
httpGet("/api/suno/detail",{song_id:id}).then(res => { httpGet("/api/suno/detail", { song_id: id })
song.value = res.data .then((res) => {
playList.value = [song.value] song.value = res.data;
document.title = song.value?.title+ " | By "+song.value?.user.nickname+" | Suno音乐" playList.value = [song.value];
}).catch(e => { document.title =
showMessageError("获取歌曲详情失败:"+e.message) song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
}) })
.catch((e) => {
showMessageError("获取歌曲详情失败:" + e.message);
});
const clipboard = ref(null) const clipboard = ref(null);
onMounted(() => { onMounted(() => {
clipboard.value = new Clipboard('.copy-link'); clipboard.value = new Clipboard(".copy-link");
clipboard.value.on('success', () => { clipboard.value.on("success", () => {
ElMessage.success("复制歌曲链接成功!"); ElMessage.success("复制歌曲链接成功!");
}) });
clipboard.value.on('error', () => { clipboard.value.on("error", () => {
ElMessage.error('复制失败!'); ElMessage.error("复制失败!");
}) });
}) });
onUnmounted(() => { onUnmounted(() => {
clipboard.value.destroy() clipboard.value.destroy();
}) });
// //
const play = () => { const play = () => {
playerRef.value.play() playerRef.value.play();
} };
const winHeight = ref(window.innerHeight - 50);
const winHeight = ref(window.innerHeight-50)
const getShareURL = (item) => { const getShareURL = (item) => {
return `${location.protocol}//${location.host}/song/${item.id}` return `${location.protocol}//${location.host}/song/${item.id}`;
} };
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

File diff suppressed because it is too large Load Diff

View File

@ -9,8 +9,8 @@
<template #default="props"> <template #default="props">
<div> <div>
<el-table :data="props.row.context" :border="childBorder"> <el-table :data="props.row.context" :border="childBorder">
<el-table-column label="对话应用" prop="role" width="120"/> <el-table-column label="对话应用" prop="role" width="120" />
<el-table-column label="对话内容" prop="content"/> <el-table-column label="对话内容" prop="content" />
</el-table> </el-table>
</div> </div>
</template> </template>
@ -23,24 +23,39 @@
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="应用类型" prop="type_name"/> <el-table-column label="应用类型" prop="type_name" />
<el-table-column label="应用标识" prop="key"/> <el-table-column label="应用标识" prop="key" />
<el-table-column label="绑定模型" prop="model_name"/> <el-table-column label="绑定模型" prop="model_name" />
<el-table-column label="启用状态"> <el-table-column label="启用状态">
<template #default="scope"> <template #default="scope">
<el-switch v-model="scope.row['enable']" @change="roleSet('enable',scope.row)"/> <el-switch
v-model="scope.row['enable']"
@change="roleSet('enable', scope.row)"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="应用图标" prop="icon"> <el-table-column label="应用图标" prop="icon">
<template #default="scope"> <template #default="scope">
<el-image :src="scope.row.icon" style="width: 45px; height: 45px; border-radius: 50%"/> <el-image
:src="scope.row.icon"
style="width: 45px; height: 45px; border-radius: 50%"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="打招呼信息" prop="hello_msg"/> <el-table-column label="打招呼信息" prop="hello_msg" />
<el-table-column label="操作" width="150"> <el-table-column label="操作" width="150">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button> <el-button
<el-popconfirm title="确定要删除当前应用吗?" @confirm="removeRole(scope.row)" :width="200"> size="small"
type="primary"
@click="rowEdit(scope.$index, scope.row)"
>编辑</el-button
>
<el-popconfirm
title="确定要删除当前应用吗?"
@confirm="removeRole(scope.row)"
:width="200"
>
<template #reference> <template #reference>
<el-button size="small" type="danger">删除</el-button> <el-button size="small" type="danger">删除</el-button>
</template> </template>
@ -51,48 +66,48 @@
</el-row> </el-row>
<el-dialog <el-dialog
v-model="showDialog" v-model="showDialog"
:title="optTitle" :title="optTitle"
:close-on-click-modal="false" :close-on-click-modal="false"
width="50%" width="50%"
> >
<el-form :model="role" label-width="120px" ref="formRef" label-position="left" :rules="rules"> <el-form
:model="role"
label-width="120px"
ref="formRef"
label-position="left"
:rules="rules"
>
<el-form-item label="应用名称:" prop="name"> <el-form-item label="应用名称:" prop="name">
<el-input <el-input v-model="role.name" autocomplete="off" />
v-model="role.name"
autocomplete="off"
/>
</el-form-item> </el-form-item>
<el-form-item label="应用分类:" prop="tid"> <el-form-item label="应用分类:" prop="tid">
<el-select <el-select
v-model="role.tid" v-model="role.tid"
filterable filterable
placeholder="请选择分类" placeholder="请选择分类"
clearable clearable
> >
<el-option <el-option
v-for="item in appTypes" v-for="item in appTypes"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="应用标志:" prop="key"> <el-form-item label="应用标志:" prop="key">
<el-input <el-input v-model="role.key" autocomplete="off" />
v-model="role.key"
autocomplete="off"
/>
</el-form-item> </el-form-item>
<el-form-item label="应用图标:" prop="icon"> <el-form-item label="应用图标:" prop="icon">
<el-input v-model="role.icon"> <el-input v-model="role.icon">
<template #append> <template #append>
<el-upload <el-upload
:auto-upload="true" :auto-upload="true"
:show-file-list="false" :show-file-list="false"
:http-request="uploadImg" :http-request="uploadImg"
> >
上传 上传
</el-upload> </el-upload>
@ -102,25 +117,22 @@
<el-form-item label="绑定模型:" prop="model_id"> <el-form-item label="绑定模型:" prop="model_id">
<el-select <el-select
v-model="role.model_id" v-model="role.model_id"
filterable filterable
placeholder="请选择模型" placeholder="请选择模型"
clearable clearable
> >
<el-option <el-option
v-for="item in models" v-for="item in models"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="打招呼信息:" prop="hello_msg"> <el-form-item label="打招呼信息:" prop="hello_msg">
<el-input <el-input v-model="role.hello_msg" autocomplete="off" />
v-model="role.hello_msg"
autocomplete="off"
/>
</el-form-item> </el-form-item>
<el-form-item label="上下文信息:" prop="context"> <el-form-item label="上下文信息:" prop="context">
@ -130,10 +142,10 @@
<template #default="scope"> <template #default="scope">
<el-select v-model="scope.row.role" placeholder="Role"> <el-select v-model="scope.row.role" placeholder="Role">
<el-option <el-option
v-for="value in messageRoles" v-for="value in messageRoles"
:key="value" :key="value"
:label="value" :label="value"
:value="value" :value="value"
/> />
</el-select> </el-select>
</template> </template>
@ -143,12 +155,16 @@
<div class="context-msg-key"> <div class="context-msg-key">
<span>对话内容</span> <span>对话内容</span>
<span class="fr"> <span class="fr">
<el-button type="primary" @click="addContext" size="small"> <el-button
<el-icon> type="primary"
<Plus/> @click="addContext"
</el-icon> size="small"
增加一行 >
</el-button> <el-icon>
<Plus />
</el-icon>
增加一行
</el-button>
</span> </span>
</div> </div>
</template> </template>
@ -156,35 +172,58 @@
<template #default="scope"> <template #default="scope">
<div class="context-msg-content"> <div class="context-msg-content">
<el-input <el-input
type="textarea" type="textarea"
:rows="3" :rows="3"
v-model="scope.row.content" v-model="scope.row.content"
autocomplete="off" autocomplete="off"
v-loading="isGenerating" v-loading="isGenerating"
/> />
<span class="remove-item"> <span class="remove-item">
<el-tooltip effect="dark" content="删除当前行" placement="right"> <el-tooltip
effect="dark"
content="删除当前行"
placement="right"
>
<el-button circle type="danger" size="small"> <el-button circle type="danger" size="small">
<el-icon @click="removeContext(scope.$index)"><Delete /></el-icon> <el-icon @click="removeContext(scope.$index)"
><Delete
/></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-popover placement="right" :width="400" trigger="click"> <el-popover
placement="right"
:width="400"
trigger="click"
>
<template #reference> <template #reference>
<el-button type="primary" circle size="small" class="icon-btn"> <el-button
type="primary"
circle
size="small"
class="icon-btn"
>
<i class="iconfont icon-linggan"></i> <i class="iconfont icon-linggan"></i>
</el-button> </el-button>
</template> </template>
<el-input <el-input
type="textarea" type="textarea"
:rows="3" :rows="3"
v-model="metaPrompt" v-model="metaPrompt"
autocomplete="off" autocomplete="off"
placeholder="请您输入要 AI实现的目标任务或者需要AI扮演的角色" placeholder="请您输入要 AI实现的目标任务或者需要AI扮演的角色"
/> />
<el-row class="text-line"> <el-row class="text-line">
<el-text class="mx-1" type="info" size="small">使用 AI 生成 System 预设指令</el-text> <el-text type="info" size="small"
<el-button class="generate-btn" size="small" @click="generatePrompt(scope.row)" color="#5865f2" :disabled="isGenerating"> >使用 AI 生成 System 预设指令</el-text
>
<el-button
class="generate-btn"
size="small"
@click="generatePrompt(scope.row)"
color="#5865f2"
:disabled="isGenerating"
>
<i class="iconfont icon-chuangzuo"></i> <i class="iconfont icon-chuangzuo"></i>
<span>立即生成</span> <span>立即生成</span>
</el-button> </el-button>
@ -199,175 +238,195 @@
</el-form-item> </el-form-item>
<el-form-item label="启用状态"> <el-form-item label="启用状态">
<el-switch v-model="role.enable"/> <el-switch v-model="role.enable" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button> <el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button> <el-button type="primary" @click="save">保存</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Delete, Plus } from "@element-plus/icons-vue";
import {Delete, Plus} from "@element-plus/icons-vue"; import { onMounted, reactive, ref } from "vue";
import {onMounted, reactive, ref} from "vue"; import { httpGet, httpPost } from "@/utils/http";
import {httpGet, httpPost} from "@/utils/http"; import { ElMessage } from "element-plus";
import {ElMessage} from "element-plus"; import { copyObj, removeArrayItem } from "@/utils/libs";
import {copyObj, removeArrayItem} from "@/utils/libs"; import { Sortable } from "sortablejs";
import {Sortable} from "sortablejs"
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import {showMessageError} from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
const showDialog = ref(false) const showDialog = ref(false);
const parentBorder = ref(true) const parentBorder = ref(true);
const childBorder = ref(true) const childBorder = ref(true);
const tableData = ref([]) const tableData = ref([]);
const sortedTableData = ref([]) const sortedTableData = ref([]);
const role = ref({context: []}) const role = ref({ context: [] });
const formRef = ref(null) const formRef = ref(null);
const optTitle = ref("") const optTitle = ref("");
const loading = ref(true) const loading = ref(true);
const rules = reactive({ const rules = reactive({
name: [{required: true, message: '请输入用户名', trigger: 'blur',}], name: [{ required: true, message: "请输入用户名", trigger: "blur" }],
key: [{required: true, message: '请输入应用标识', trigger: 'blur',}], key: [{ required: true, message: "请输入应用标识", trigger: "blur" }],
icon: [{required: true, message: '请输入应用图标', trigger: 'blur',}], icon: [{ required: true, message: "请输入应用图标", trigger: "blur" }],
sort: [ sort: [
{required: true, message: '请输入排序数字', trigger: 'blur'}, { required: true, message: "请输入排序数字", trigger: "blur" },
{type: 'number', message: '请输入有效数字'}, { type: "number", message: "请输入有效数字" }
], ],
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}] hello_msg: [
}) { required: true, message: "请输入打招呼信息", trigger: "change" }
]
});
const appTypes = ref([]) const appTypes = ref([]);
const models = ref([]) const models = ref([]);
const messageRoles = ref(["system", "user", "assistant"]) const messageRoles = ref(["system", "user", "assistant"]);
onMounted(() => { onMounted(() => {
fetchData() fetchData();
// get chat models // get chat models
httpGet('/api/admin/model/list?enable=1').then((res) => { httpGet("/api/admin/model/list?enable=1")
models.value = res.data .then((res) => {
}).catch(() => { models.value = res.data;
ElMessage.error("获取AI模型数据失败"); })
}) .catch(() => {
ElMessage.error("获取AI模型数据失败");
});
// get app type // get app type
httpGet('/api/admin/app/type/list?enable=1').then((res) => { httpGet("/api/admin/app/type/list?enable=1")
appTypes.value = res.data .then((res) => {
}).catch(() => { appTypes.value = res.data;
ElMessage.error("获取应用分类数据失败"); })
}) .catch(() => {
ElMessage.error("获取应用分类数据失败");
}) });
});
const fetchData = () => { const fetchData = () => {
// //
httpGet('/api/admin/role/list').then((res) => { httpGet("/api/admin/role/list")
// .then((res) => {
// const arr = res.data; //
// for (let i = 0; i < arr.length; i++) { // const arr = res.data;
// if(arr[i].model_id == 0){ // for (let i = 0; i < arr.length; i++) {
// arr[i].model_id = '' // if(arr[i].model_id == 0){
// } // arr[i].model_id = ''
// } // }
tableData.value = res.data // }
sortedTableData.value = copyObj(tableData.value) tableData.value = res.data;
loading.value = false sortedTableData.value = copyObj(tableData.value);
}).catch(() => { loading.value = false;
ElMessage.error("获取聊天应用失败"); })
}) .catch(() => {
ElMessage.error("获取聊天应用失败");
});
const drawBodyWrapper = document.querySelector('.el-table__body tbody') const drawBodyWrapper = document.querySelector(".el-table__body tbody");
// //
Sortable.create(drawBodyWrapper, { Sortable.create(drawBodyWrapper, {
sort: true, sort: true,
animation: 500, animation: 500,
onEnd({newIndex, oldIndex, from}) { onEnd({ newIndex, oldIndex, from }) {
if (oldIndex === newIndex) { if (oldIndex === newIndex) {
return return;
} }
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id')); const sortedData = Array.from(from.children).map((row) =>
const ids = [] row.querySelector(".sort").getAttribute("data-id")
const sorts = [] );
const ids = [];
const sorts = [];
sortedData.forEach((id, index) => { sortedData.forEach((id, index) => {
ids.push(parseInt(id)) ids.push(parseInt(id));
sorts.push(index+1) sorts.push(index + 1);
tableData.value[index].sort_num = index + 1 tableData.value[index].sort_num = index + 1;
}) });
httpPost("/api/admin/role/sort", {ids: ids, sorts: sorts}).catch(e => { httpPost("/api/admin/role/sort", { ids: ids, sorts: sorts }).catch(
ElMessage.error("排序失败:" + e.message) (e) => {
}) ElMessage.error("排序失败:" + e.message);
}
);
} }
}) });
} };
const roleSet = (filed, row) => { const roleSet = (filed, row) => {
httpPost('/api/admin/role/set', {id: row.id, filed: filed, value: row[filed]}).then(() => { httpPost("/api/admin/role/set", {
ElMessage.success("操作成功!") id: row.id,
}).catch(e => { filed: filed,
ElMessage.error("操作失败:" + e.message) value: row[filed]
}) })
} .then(() => {
ElMessage.success("操作成功!");
})
.catch((e) => {
ElMessage.error("操作失败:" + e.message);
});
};
// //
const curIndex = ref(0) const curIndex = ref(0);
const rowEdit = function (index, row) { const rowEdit = function (index, row) {
optTitle.value = "修改应用" optTitle.value = "修改应用";
curIndex.value = index curIndex.value = index;
role.value = copyObj(row) role.value = copyObj(row);
showDialog.value = true showDialog.value = true;
} };
const addRole = function () { const addRole = function () {
optTitle.value = "添加新应用" optTitle.value = "添加新应用";
role.value = {context: []} role.value = { context: [] };
showDialog.value = true showDialog.value = true;
} };
const save = function () { const save = function () {
formRef.value.validate((valid) => { formRef.value.validate((valid) => {
if (valid) { if (valid) {
showDialog.value = false showDialog.value = false;
httpPost('/api/admin/role/save', role.value).then(() => { httpPost("/api/admin/role/save", role.value)
ElMessage.success('操作成功') .then(() => {
fetchData() ElMessage.success("操作成功");
}).catch((e) => { fetchData();
ElMessage.error('操作失败,' + e.message) })
}) .catch((e) => {
ElMessage.error("操作失败," + e.message);
});
} }
}) });
} };
const removeRole = function (row) { const removeRole = function (row) {
httpGet('/api/admin/role/remove?id=' + row.id).then(() => { httpGet("/api/admin/role/remove?id=" + row.id)
ElMessage.success("删除成功!") .then(() => {
tableData.value = removeArrayItem(tableData.value, row, (v1, v2) => { ElMessage.success("删除成功!");
return v1.id === v2.id tableData.value = removeArrayItem(tableData.value, row, (v1, v2) => {
return v1.id === v2.id;
});
}) })
}).catch(() => { .catch(() => {
ElMessage.error("删除失败!") ElMessage.error("删除失败!");
}) });
} };
const addContext = function () { const addContext = function () {
if (!role.value.context) { if (!role.value.context) {
role.value.context = [] role.value.context = [];
} }
role.value.context.push({role: '', content: ''}) role.value.context.push({ role: "", content: "" });
} };
const removeContext = function (index) { const removeContext = function (index) {
role.value.context.splice(index, 1); role.value.context.splice(index, 1);
} };
// //
const uploadImg = (file) => { const uploadImg = (file) => {
@ -376,36 +435,40 @@ const uploadImg = (file) => {
quality: 0.6, quality: 0.6,
success(result) { success(result) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', result, result.name); formData.append("file", result, result.name);
// //
httpPost('/api/admin/upload', formData).then((res) => { httpPost("/api/admin/upload", formData)
role.value.icon = res.data.url .then((res) => {
ElMessage.success('上传成功') role.value.icon = res.data.url;
}).catch((e) => { ElMessage.success("上传成功");
ElMessage.error('上传失败:' + e.message) })
}) .catch((e) => {
ElMessage.error("上传失败:" + e.message);
});
}, },
error(e) { error(e) {
ElMessage.error('上传失败:' + e.message) ElMessage.error("上传失败:" + e.message);
}, }
}); });
} };
const isGenerating = ref(false) const isGenerating = ref(false);
const metaPrompt = ref("") const metaPrompt = ref("");
const generatePrompt = (row) => { const generatePrompt = (row) => {
if (metaPrompt.value === "") { if (metaPrompt.value === "") {
return showMessageError("请输入元提示词") return showMessageError("请输入元提示词");
} }
isGenerating.value = true isGenerating.value = true;
httpPost("/api/prompt/meta", {prompt: metaPrompt.value}).then(res => { httpPost("/api/prompt/meta", { prompt: metaPrompt.value })
row.content = res.data .then((res) => {
isGenerating.value = false row.content = res.data;
}).catch(e => { isGenerating.value = false;
showMessageError("生成失败:"+e.message) })
isGenerating.value = false .catch((e) => {
}) showMessageError("生成失败:" + e.message);
} isGenerating.value = false;
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@ -488,4 +551,4 @@ const generatePrompt = (row) => {
font-size 14px font-size 14px
} }
} }
</style> </style>

View File

@ -3,29 +3,48 @@
<el-tabs v-model="activeName" @tab-change="handleChange"> <el-tabs v-model="activeName" @tab-change="handleChange">
<el-tab-pane label="对话列表" name="chat" v-loading="data.chat.loading"> <el-tab-pane label="对话列表" name="chat" v-loading="data.chat.loading">
<div class="handle-box"> <div class="handle-box">
<el-input v-model.number="data.chat.query.user_id" placeholder="账户ID" class="handle-input mr10" <el-input
@keyup="searchChat($event)"></el-input> v-model.number="data.chat.query.user_id"
<el-input v-model="data.chat.query.title" placeholder="对话标题" class="handle-input mr10" placeholder="账户ID"
@keyup="searchChat($event)"></el-input> class="handle-input mr10"
@keyup="searchChat($event)"
></el-input>
<el-input
v-model="data.chat.query.title"
placeholder="对话标题"
class="handle-input mr10"
@keyup="searchChat($event)"
></el-input>
<el-date-picker <el-date-picker
v-model="data.chat.query.created_at" v-model="data.chat.query.created_at"
type="daterange" type="daterange"
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="margin-right: 10px;width: 200px; position: relative;top:3px;" style="
margin-right: 10px;
width: 200px;
position: relative;
top: 3px;
"
/> />
<el-button type="primary" :icon="Search" @click="fetchChatData">搜索</el-button> <el-button type="primary" :icon="Search" @click="fetchChatData"
>搜索</el-button
>
</div> </div>
<el-row> <el-row>
<el-table :data="data.chat.items" :row-key="row => row.id" table-layout="auto"> <el-table
<el-table-column prop="user_id" label="账户ID"/> :data="data.chat.items"
<el-table-column prop="username" label="账户"/> :row-key="(row) => row.id"
table-layout="auto"
>
<el-table-column prop="user_id" label="账户ID" />
<el-table-column prop="username" label="账户" />
<el-table-column label="图标"> <el-table-column label="图标">
<template #default="scope"> <template #default="scope">
<el-avatar :size="30" :src="scope.row.role.icon"/> <el-avatar :size="30" :src="scope.row.role.icon" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="角色"> <el-table-column label="角色">
@ -33,21 +52,29 @@
<span>{{ scope.row.role.name }}</span> <span>{{ scope.row.role.name }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="model" label="模型"/> <el-table-column prop="model" label="模型" />
<el-table-column prop="title" label="标题"/> <el-table-column prop="title" label="标题" />
<el-table-column prop="msg_num" label="消息数量"/> <el-table-column prop="msg_num" label="消息数量" />
<el-table-column prop="token" label="消耗算力"/> <el-table-column prop="token" label="消耗算力" />
<el-table-column label="创建时间"> <el-table-column label="创建时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span> <span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="180"> <el-table-column label="操作" width="180">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="primary" @click="showMessages(scope.row)">查看</el-button> <el-button
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeChat(scope.row)"> size="small"
type="primary"
@click="showMessages(scope.row)"
>查看</el-button
>
<el-popconfirm
title="确定要删除当前记录吗?"
@confirm="removeChat(scope.row)"
>
<template #reference> <template #reference>
<el-button size="small" type="danger">删除</el-button> <el-button size="small" type="danger">删除</el-button>
</template> </template>
@ -58,67 +85,104 @@
</el-row> </el-row>
<div class="pagination"> <div class="pagination">
<el-pagination v-if="data.chat.total > 0" background <el-pagination
layout="total,prev, pager, next" v-if="data.chat.total > 0"
:hide-on-single-page="true" background
v-model:current-page="data.chat.page" layout="total,prev, pager, next"
v-model:page-size="data.chat.pageSize" :hide-on-single-page="true"
@current-change="fetchChatData()" v-model:current-page="data.chat.page"
:total="data.chat.total"/> v-model:page-size="data.chat.pageSize"
@current-change="fetchChatData()"
:total="data.chat.total"
/>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="消息记录" name="message"> <el-tab-pane label="消息记录" name="message">
<div class="handle-box"> <div class="handle-box">
<el-input v-model.number="data.message.query.user_id" placeholder="账户ID" class="handle-input mr10" <el-input
@keyup="searchMessage($event)"></el-input> v-model.number="data.message.query.user_id"
<el-input v-model="data.message.query.content" placeholder="消息内容" class="handle-input mr10" placeholder="账户ID"
@keyup="searchMessage($event)"></el-input> class="handle-input mr10"
<el-input v-model="data.message.query.model" placeholder="模型" class="handle-input mr10" @keyup="searchMessage($event)"
@keyup="searchMessage($event)"></el-input> ></el-input>
<el-input
v-model="data.message.query.content"
placeholder="消息内容"
class="handle-input mr10"
@keyup="searchMessage($event)"
></el-input>
<el-input
v-model="data.message.query.model"
placeholder="模型"
class="handle-input mr10"
@keyup="searchMessage($event)"
></el-input>
<el-date-picker <el-date-picker
v-model="data.message.query.created_at" v-model="data.message.query.created_at"
type="daterange" type="daterange"
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="margin-right: 10px;width: 200px; position: relative;top:3px;" style="
margin-right: 10px;
width: 200px;
position: relative;
top: 3px;
"
/> />
<el-button type="primary" :icon="Search" @click="fetchMessageData">搜索</el-button> <el-button type="primary" :icon="Search" @click="fetchMessageData"
>搜索</el-button
>
</div> </div>
<el-row> <el-row>
<el-table :data="data.message.items" :row-key="row => row.id" table-layout="auto"> <el-table
<el-table-column prop="user_id" label="账户ID"/> :data="data.message.items"
<el-table-column prop="username" label="账户"/> :row-key="(row) => row.id"
table-layout="auto"
>
<el-table-column prop="user_id" label="账户ID" />
<el-table-column prop="username" label="账户" />
<el-table-column label="角色"> <el-table-column label="角色">
<template #default="scope"> <template #default="scope">
<el-avatar :size="30" :src="scope.row.icon"/> <el-avatar :size="30" :src="scope.row.icon" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="model" label="模型"/> <el-table-column prop="model" label="模型" />
<el-table-column label="消息内容"> <el-table-column label="消息内容">
<template #default="scope"> <template #default="scope">
<el-text style="width: 200px" truncated @click="showContent(scope.row.content)"> <el-text
style="width: 200px"
truncated
@click="showContent(scope.row.content)"
>
{{ scope.row.content }} {{ scope.row.content }}
</el-text> </el-text>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="token" label="算力"/> <el-table-column prop="token" label="算力" />
<el-table-column label="创建时间"> <el-table-column label="创建时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span> <span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="180"> <el-table-column label="操作" width="180">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="primary" @click="showContent(scope.row.content)">查看</el-button> <el-button
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeMessage(scope.row)"> size="small"
type="primary"
@click="showContent(scope.row.content)"
>查看</el-button
>
<el-popconfirm
title="确定要删除当前记录吗?"
@confirm="removeMessage(scope.row)"
>
<template #reference> <template #reference>
<el-button size="small" type="danger">删除</el-button> <el-button size="small" type="danger">删除</el-button>
</template> </template>
@ -129,24 +193,25 @@
</el-row> </el-row>
<div class="pagination"> <div class="pagination">
<el-pagination v-if="data.message.total > 0" background <el-pagination
layout="total,prev, pager, next" v-if="data.message.total > 0"
:hide-on-single-page="true" background
v-model:current-page="data.message.page" layout="total,prev, pager, next"
v-model:page-size="data.message.pageSize" :hide-on-single-page="true"
@current-change="fetchMessageData()" v-model:current-page="data.message.page"
:total="data.message.total"/> v-model:page-size="data.message.pageSize"
@current-change="fetchMessageData()"
:total="data.message.total"
/>
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-dialog <el-dialog
v-model="showContentDialog" v-model="showContentDialog"
title="消息详情" title="消息详情"
class="chat-dialog" class="chat-dialog"
style="--el-dialog-width:60%" style="--el-dialog-width: 60%"
> >
<div class="chat-detail"> <div class="chat-detail">
<div class="chat-line" v-html="dialogContent"></div> <div class="chat-line" v-html="dialogContent"></div>
@ -154,138 +219,147 @@
</el-dialog> </el-dialog>
<el-dialog <el-dialog
v-model="showChatItemDialog" v-model="showChatItemDialog"
title="对话详情" title="对话详情"
class="chat-dialog" class="chat-dialog"
style="--el-dialog-width:60%" style="--el-dialog-width: 60%"
> >
<div class="chat-box chat-page"> <div class="chat-box chat-page">
<div v-for="item in messages" :key="item.id"> <div v-for="item in messages" :key="item.id">
<chat-prompt <chat-prompt v-if="item.type === 'prompt'" :data="item" />
v-if="item.type==='prompt'" <chat-reply
:data="item"/> v-else-if="item.type === 'reply'"
<chat-reply v-else-if="item.type==='reply'" :read-only="true"
:read-only="true" :data="item"
:data="item"/> />
</div> </div>
</div><!-- end chat box --> </div>
<!-- end chat box -->
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import { onMounted, ref } from "vue";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {dateFormat, processContent} from "@/utils/libs"; import { dateFormat, processContent } from "@/utils/libs";
import {Search} from "@element-plus/icons-vue"; import { Search } from "@element-plus/icons-vue";
import 'highlight.js/styles/a11y-dark.css' import "highlight.js/styles/a11y-dark.css";
import hl from "highlight.js"; import hl from "highlight.js";
import ChatPrompt from "@/components/ChatPrompt.vue"; import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue"; import ChatReply from "@/components/ChatReply.vue";
// //
const data = ref({ const data = ref({
"chat": { chat: {
items: [], items: [],
query: {title: "", created_at: [], page: 1, page_size: 15}, query: { title: "", created_at: [], page: 1, page_size: 15 },
total: 0, total: 0,
page: 1, page: 1,
pageSize: 15, pageSize: 15,
loading: true loading: true
}, },
"message": { message: {
items: [], items: [],
query: {title: "", created_at: [], page: 1, page_size: 15}, query: { title: "", created_at: [], page: 1, page_size: 15 },
total: 0, total: 0,
page: 1, page: 1,
pageSize: 15, pageSize: 15,
loading: true loading: true
} }
}) });
const activeName = ref("chat") const activeName = ref("chat");
onMounted(() => { onMounted(() => {
fetchChatData() fetchChatData();
}) });
const handleChange = (tab) => { const handleChange = (tab) => {
if (tab === "chat") { if (tab === "chat") {
fetchChatData() fetchChatData();
} else if (tab === "message") { } else if (tab === "message") {
fetchMessageData() fetchMessageData();
} }
} };
// //
const searchChat = (evt) => { const searchChat = (evt) => {
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
fetchChatData() fetchChatData();
} }
} };
// //
const searchMessage = (evt) => { const searchMessage = (evt) => {
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
fetchMessageData() fetchMessageData();
} }
} };
// //
const fetchChatData = () => { const fetchChatData = () => {
const d = data.value.chat const d = data.value.chat;
d.query.page = d.page d.query.page = d.page;
d.query.page_size = d.pageSize d.query.page_size = d.pageSize;
httpPost('/api/admin/chat/list', d.query).then((res) => { httpPost("/api/admin/chat/list", d.query)
if (res.data) { .then((res) => {
d.items = res.data.items if (res.data) {
d.total = res.data.total d.items = res.data.items;
d.page = res.data.page d.total = res.data.total;
d.pageSize = res.data.page_size d.page = res.data.page;
} d.pageSize = res.data.page_size;
d.loading = false }
}).catch(e => { d.loading = false;
ElMessage.error("获取数据失败:" + e.message); })
}) .catch((e) => {
} ElMessage.error("获取数据失败:" + e.message);
});
};
const fetchMessageData = () => { const fetchMessageData = () => {
const d = data.value.message const d = data.value.message;
d.query.page = d.page d.query.page = d.page;
d.query.page_size = d.pageSize d.query.page_size = d.pageSize;
httpPost('/api/admin/chat/message', d.query).then((res) => { httpPost("/api/admin/chat/message", d.query)
if (res.data) { .then((res) => {
d.items = res.data.items if (res.data) {
d.total = res.data.total d.items = res.data.items;
d.page = res.data.page d.total = res.data.total;
d.pageSize = res.data.page_size d.page = res.data.page;
} d.pageSize = res.data.page_size;
d.loading = false }
}).catch(e => { d.loading = false;
ElMessage.error("获取数据失败:" + e.message); })
}) .catch((e) => {
} ElMessage.error("获取数据失败:" + e.message);
});
};
const removeChat = function (row) { const removeChat = function (row) {
httpGet('/api/admin/chat/remove?chat_id=' + row.chat_id).then(() => { httpGet("/api/admin/chat/remove?chat_id=" + row.chat_id)
ElMessage.success("删除成功!") .then(() => {
fetchChatData() ElMessage.success("删除成功!");
}).catch((e) => { fetchChatData();
ElMessage.error("删除失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("删除失败:" + e.message);
});
};
const removeMessage = function (row) { const removeMessage = function (row) {
httpGet('/api/admin/chat/message/remove?id=' + row.id).then(() => { httpGet("/api/admin/chat/message/remove?id=" + row.id)
ElMessage.success("删除成功!") .then(() => {
fetchMessageData() ElMessage.success("删除成功!");
}).catch((e) => { fetchMessageData();
ElMessage.error("删除失败:" + e.message) })
}) .catch((e) => {
} ElMessage.error("删除失败:" + e.message);
});
};
const mathjaxPlugin = require('markdown-it-mathjax3') const mathjaxPlugin = require("markdown-it-mathjax3");
const md = require('markdown-it')({ const md = require("markdown-it")({
breaks: true, breaks: true,
html: true, html: true,
linkify: true, linkify: true,
@ -293,41 +367,43 @@ const md = require('markdown-it')({
highlight: function (str, lang) { highlight: function (str, lang) {
if (lang && hl.getLanguage(lang)) { if (lang && hl.getLanguage(lang)) {
// //
const preCode = hl.highlight(lang, str, true).value const preCode = hl.highlight(lang, str, true).value;
// pre // pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>` return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`;
} }
// //
const preCode = md.utils.escapeHtml(str) const preCode = md.utils.escapeHtml(str);
// pre // pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>` return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`;
} }
}); });
md.use(mathjaxPlugin) md.use(mathjaxPlugin);
const showContentDialog = ref(false) const showContentDialog = ref(false);
const dialogContent = ref("") const dialogContent = ref("");
const showContent = (content) => { const showContent = (content) => {
showContentDialog.value = true showContentDialog.value = true;
dialogContent.value = md.render(processContent(content)) dialogContent.value = md.render(processContent(content));
} };
const showChatItemDialog = ref(false) const showChatItemDialog = ref(false);
const messages = ref([]) const messages = ref([]);
const showMessages = (row) => { const showMessages = (row) => {
showChatItemDialog.value = true showChatItemDialog.value = true;
messages.value = [] messages.value = [];
httpGet('/api/admin/chat/history?chat_id=' + row.chat_id).then(res => { httpGet("/api/admin/chat/history?chat_id=" + row.chat_id)
const data = res.data .then((res) => {
for (let i = 0; i < data.length; i++) { const data = res.data;
messages.value.push(data[i]); for (let i = 0; i < data.length; i++) {
} messages.value.push(data[i]);
}).catch(e => { }
// TODO: })
ElMessage.error('加载聊天记录失败:' + e.message); .catch((e) => {
}) // TODO:
} ElMessage.error("加载聊天记录失败:" + e.message);
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@ -93,6 +93,7 @@
<el-pagination v-if="data.mj.total > 0" background <el-pagination v-if="data.mj.total > 0" background
layout="total,prev, pager, next" layout="total,prev, pager, next"
:hide-on-single-page="true" :hide-on-single-page="true"
v-model:current-page="data.mj.page" v-model:current-page="data.mj.page"
v-model:page-size="data.mj.pageSize" v-model:page-size="data.mj.pageSize"
@current-change="fetchMjData()" @current-change="fetchMjData()"

View File

@ -1,27 +1,47 @@
<template> <template>
<div class="container user-list" v-loading="loading"> <div class="container user-list" v-loading="loading">
<div class="handle-box"> <div class="handle-box">
<el-input v-model="query.username" placeholder="账号" class="handle-input mr10"></el-input> <el-input
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button> v-model="query.username"
<el-button type="success" :icon="Plus" @click="addUser">新增用户</el-button> placeholder="账号"
<el-button type="danger" :icon="Delete" @click="multipleDelete">删除</el-button> class="handle-input mr10"
></el-input>
<el-button type="primary" :icon="Search" @click="handleSearch"
>搜索</el-button
>
<el-button type="success" :icon="Plus" @click="addUser"
>新增用户</el-button
>
<el-button type="danger" :icon="Delete" @click="multipleDelete"
>删除</el-button
>
</div> </div>
<el-row> <el-row>
<el-table :data="users.items" border class="table" :row-key="row => row.id" <el-table
@selection-change="handleSelectionChange" table-layout="auto"> :data="users.items"
border
class="table"
:row-key="(row) => row.id"
@selection-change="handleSelectionChange"
table-layout="auto"
>
<el-table-column type="selection" width="38"></el-table-column> <el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="id" label="ID"/> <el-table-column prop="id" label="ID" />
<el-table-column label="账号"> <el-table-column label="账号">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.username }}</span> <span>{{ scope.row.username }}</span>
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px;position: relative; top:5px; left: 5px"/> <el-image
v-if="scope.row.vip"
:src="vipImg"
style="height: 20px; position: relative; top: 5px; left: 5px"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="mobile" label="手机"/> <el-table-column prop="mobile" label="手机" />
<el-table-column prop="email" label="邮箱"/> <el-table-column prop="email" label="邮箱" />
<el-table-column prop="nickname" label="昵称"/> <el-table-column prop="nickname" label="昵称" />
<el-table-column prop="power" label="剩余算力"/> <el-table-column prop="power" label="剩余算力" />
<el-table-column label="状态" width="80"> <el-table-column label="状态" width="80">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status" type="success">正常</el-tag> <el-tag v-if="scope.row.status" type="success">正常</el-tag>
@ -30,337 +50,384 @@
</el-table-column> </el-table-column>
<el-table-column label="过期时间"> <el-table-column label="过期时间">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row['expired_time']">{{ scope.row['expired_time'] }}</span> <span v-if="scope.row['expired_time']">{{
scope.row["expired_time"]
}}</span>
<el-tag v-else>长期有效</el-tag> <el-tag v-else>长期有效</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="注册时间"> <el-table-column label="注册时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span> <span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column fixed="right" label="操作" width="200"> <el-table-column fixed="right" label="操作" width="200">
<template #default="scope"> <template #default="scope">
<el-button-group class="ml-4"> <el-button-group class="ml-4">
<el-button size="small" type="primary" @click="userEdit(scope.row)">编辑</el-button> <el-button
<el-button size="small" type="danger" @click="removeUser(scope.row)">删除</el-button> size="small"
<el-button size="small" type="success" @click="resetPass(scope.row)">重置密码</el-button> type="primary"
@click="userEdit(scope.row)"
>编辑</el-button
>
<el-button
size="small"
type="danger"
@click="removeUser(scope.row)"
>删除</el-button
>
<el-button
size="small"
type="success"
@click="resetPass(scope.row)"
>重置密码</el-button
>
</el-button-group> </el-button-group>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="pagination"> <div class="pagination">
<el-pagination v-if="users.total > 0" <el-pagination
background v-if="users.total > 0"
layout="total, prev, pager, next" background
v-model:current-page="users.page" layout="total, prev, pager, next"
v-model:page-size="users.page_size" v-model:current-page="users.page"
:total="users.total" v-model:page-size="users.page_size"
@current-change="fetchUserList(users.page, users.page_size)" :total="users.total"
@current-change="fetchUserList(users.page, users.page_size)"
/> />
</div> </div>
</el-row> </el-row>
<el-dialog <el-dialog
v-model="showUserEditDialog" v-model="showUserEditDialog"
:title="title" :title="title"
:close-on-click-modal="false" :close-on-click-modal="false"
width="50%" width="50%"
> >
<el-form :model="user" label-width="100px" ref="userEditFormRef" :rules="rules"> <el-form
:model="user"
label-width="100px"
ref="userEditFormRef"
:rules="rules"
>
<el-form-item label="账号:" prop="username"> <el-form-item label="账号:" prop="username">
<el-input v-model="user.username" autocomplete="off"/> <el-input v-model="user.username" autocomplete="off" />
</el-form-item> </el-form-item>
<el-form-item label="手机:" prop="mobile"> <el-form-item label="手机:" prop="mobile">
<el-input v-model="user.mobile" autocomplete="off"/> <el-input v-model="user.mobile" autocomplete="off" />
</el-form-item> </el-form-item>
<el-form-item label="邮箱:" prop="email"> <el-form-item label="邮箱:" prop="email">
<el-input v-model="user.email" autocomplete="off"/> <el-input v-model="user.email" autocomplete="off" />
</el-form-item> </el-form-item>
<el-form-item v-if="add" label="密码:" prop="password"> <el-form-item v-if="add" label="密码:" prop="password">
<el-input v-model="user.password" autocomplete="off" placeholder="8-16位"/> <el-input
v-model="user.password"
autocomplete="off"
placeholder="8-16位"
/>
</el-form-item> </el-form-item>
<el-form-item label="剩余算力:" prop="power"> <el-form-item label="剩余算力:" prop="power">
<el-input v-model.number="user.power" autocomplete="off" placeholder="0"/> <el-input
v-model.number="user.power"
autocomplete="off"
placeholder="0"
/>
</el-form-item> </el-form-item>
<el-form-item label="有效期:" prop="expired_time"> <el-form-item label="有效期:" prop="expired_time">
<el-date-picker <el-date-picker
v-model="user.expired_time" v-model="user.expired_time"
type="datetime" type="datetime"
placeholder="选择日期" placeholder="选择日期"
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledDate" :disabled-date="disabledDate"
/> />
</el-form-item> </el-form-item>
<el-form-item label="聊天角色" prop="chat_roles"> <el-form-item label="聊天角色" prop="chat_roles">
<el-select <el-select
v-model="user.chat_roles" v-model="user.chat_roles"
multiple multiple
:filterable="true" :filterable="true"
placeholder="选择聊天角色,多选" placeholder="选择聊天角色,多选"
> >
<el-option <el-option
v-for="item in roles" v-for="item in roles"
:key="item.key" :key="item.key"
:label="item.name" :label="item.name"
:value="item.key" :value="item.key"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="模型权限" prop="chat_models"> <el-form-item label="模型权限" prop="chat_models">
<el-select <el-select
v-model="user.chat_models" v-model="user.chat_models"
multiple multiple
:filterable="true" :filterable="true"
placeholder="选择AI模型多选" placeholder="选择AI模型多选"
> >
<el-option <el-option
v-for="item in models" v-for="item in models"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="启用状态"> <el-form-item label="启用状态">
<el-switch v-model="user.status"/> <el-switch v-model="user.status" />
</el-form-item> </el-form-item>
<el-form-item label="开通VIP"> <el-form-item label="开通VIP">
<el-switch v-model="user.vip"/> <el-switch v-model="user.vip" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showUserEditDialog = false">取消</el-button> <el-button @click="showUserEditDialog = false">取消</el-button>
<el-button type="primary" @click="saveUser">提交</el-button> <el-button type="primary" @click="saveUser">提交</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog <el-dialog v-model="showResetPassDialog" title="重置密码" width="50%">
v-model="showResetPassDialog"
title="重置密码"
width="50%"
>
<el-form label-width="100px" ref="userEditFormRef"> <el-form label-width="100px" ref="userEditFormRef">
<el-form-item label="账户:"> <el-form-item label="账户:">
<el-input v-model="pass.username" autocomplete="off" readonly disabled/> <el-input
v-model="pass.username"
autocomplete="off"
readonly
disabled
/>
</el-form-item> </el-form-item>
<el-form-item label="新密码:"> <el-form-item label="新密码:">
<el-input v-model="pass.password" autocomplete="off"/> <el-input v-model="pass.password" autocomplete="off" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="doResetPass">提交</el-button> <el-button type="primary" @click="doResetPass">提交</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, reactive, ref} from "vue"; import { onMounted, reactive, ref } from "vue";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {ElMessage, ElMessageBox} from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import {dateFormat, disabledDate} from "@/utils/libs"; import { dateFormat, disabledDate } from "@/utils/libs";
import {Delete, Plus, Search} from "@element-plus/icons-vue"; import { Delete, Plus, Search } from "@element-plus/icons-vue";
// //
const users = ref({page: 1, page_size: 15, items: []}) const users = ref({ page: 1, page_size: 15, items: [] });
const query = ref({username: '', page: 1, page_size: 15}) const query = ref({ username: "", page: 1, page_size: 15 });
const title = ref('添加用户') const title = ref("添加用户");
const vipImg = ref("/images/vip.png") const vipImg = ref("/images/menu/member.png");
const add = ref(true) const add = ref(true);
const user = ref({chat_roles: [], chat_models: []}) const user = ref({ chat_roles: [], chat_models: [] });
const pass = ref({username: '', password: '', id: 0}) const pass = ref({ username: "", password: "", id: 0 });
const roles = ref([]) const roles = ref([]);
const models = ref([]) const models = ref([]);
const showUserEditDialog = ref(false) const showUserEditDialog = ref(false);
const showResetPassDialog = ref(false) const showResetPassDialog = ref(false);
const rules = reactive({ const rules = reactive({
username: [{required: true, message: '请输入账号', trigger: 'blur',}], username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [ password: [
{ {
required: true, required: true,
validator: (rule, value) => { validator: (rule, value) => {
return !(value.length > 16 || value.length < 8); return !(value.length > 16 || value.length < 8);
},
}, message: '密码必须为8-16', message: "密码必须为8-16",
trigger: 'blur' trigger: "blur"
} }
], ],
calls: [ calls: [
{required: true, message: '请输入提问次数'}, { required: true, message: "请输入提问次数" },
{type: 'number', message: '请输入有效数字'}, { type: "number", message: "请输入有效数字" }
], ],
chat_roles: [{required: true, message: '请选择聊天角色', trigger: 'change'}], chat_roles: [
chat_models: [{required: true, message: '请选择AI模型', trigger: 'change'}], { required: true, message: "请选择聊天角色", trigger: "change" }
}) ],
const loading = ref(true) chat_models: [{ required: true, message: "请选择AI模型", trigger: "change" }]
});
const loading = ref(true);
const userEditFormRef = ref(null) const userEditFormRef = ref(null);
onMounted(() => { onMounted(() => {
fetchUserList(users.value.page, users.value.page_size) fetchUserList(users.value.page, users.value.page_size);
// //
httpGet('/api/admin/role/list').then((res) => { httpGet("/api/admin/role/list")
roles.value = res.data; .then((res) => {
}).catch(() => { roles.value = res.data;
ElMessage.error("获取聊天角色失败"); })
}) .catch(() => {
ElMessage.error("获取聊天角色失败");
});
httpGet('/api/admin/model/list').then(res => { httpGet("/api/admin/model/list")
models.value = res.data .then((res) => {
}).catch(e => { models.value = res.data;
ElMessage.error("获取模型失败:" + e.message) })
}) .catch((e) => {
}) ElMessage.error("获取模型失败:" + e.message);
});
});
const fetchUserList = function (page, pageSize) { const fetchUserList = function (page, pageSize) {
query.value.page = page query.value.page = page;
query.value.page_size = pageSize query.value.page_size = pageSize;
httpGet('/api/admin/user/list', query.value).then((res) => { httpGet("/api/admin/user/list", query.value)
if (res.data) { .then((res) => {
// if (res.data) {
const arr = res.data.items; //
for (let i = 0; i < arr.length; i++) { const arr = res.data.items;
arr[i].expired_time = dateFormat(arr[i].expired_time) for (let i = 0; i < arr.length; i++) {
arr[i].expired_time = dateFormat(arr[i].expired_time);
}
users.value.items = arr;
users.value.total = res.data.total;
users.value.page = res.data.page;
user.value.page_size = res.data.page_size;
} }
users.value.items = arr loading.value = false;
users.value.total = res.data.total })
users.value.page = res.data.page .catch(() => {
user.value.page_size = res.data.page_size ElMessage.error("加载用户列表失败");
} });
loading.value = false };
}).catch(() => {
ElMessage.error('加载用户列表失败')
})
}
const handleSearch = () => { const handleSearch = () => {
fetchUserList(users.value.page, users.value.page_size) fetchUserList(users.value.page, users.value.page_size);
} };
// //
const removeUser = function (user) { const removeUser = function (user) {
ElMessageBox.confirm( ElMessageBox.confirm(
'此操作将会永久删除用户信息和聊天记录,确认操作吗?', "此操作将会永久删除用户信息和聊天记录,确认操作吗?",
'警告', "警告",
{ {
confirmButtonText: '确定', confirmButtonText: "确定",
cancelButtonText: '取消', cancelButtonText: "取消",
type: 'warning', type: "warning"
} }
).then(() => { )
httpGet('/api/admin/user/remove', {id: user.id}).then(() => { .then(() => {
ElMessage.success('操作成功!') httpGet("/api/admin/user/remove", { id: user.id })
fetchUserList(users.value.page, users.value.page_size) .then(() => {
}).catch((e) => { ElMessage.success("操作成功!");
ElMessage.error('操作失败,' + e.message) fetchUserList(users.value.page, users.value.page_size);
})
.catch((e) => {
ElMessage.error("操作失败," + e.message);
});
}) })
}).catch(() => { .catch(() => {
ElMessage.info('操作被取消') ElMessage.info("操作被取消");
}) });
};
}
const userEdit = function (row) { const userEdit = function (row) {
user.value = row user.value = row;
title.value = '编辑用户' title.value = "编辑用户";
showUserEditDialog.value = true showUserEditDialog.value = true;
add.value = false add.value = false;
} };
const addUser = () => { const addUser = () => {
user.value = {chat_id: 0, chat_roles: [], chat_models: []} user.value = { chat_id: 0, chat_roles: [], chat_models: [] };
title.value = '添加用户' title.value = "添加用户";
showUserEditDialog.value = true showUserEditDialog.value = true;
add.value = true add.value = true;
} };
const saveUser = function () { const saveUser = function () {
userEditFormRef.value.validate((valid) => { userEditFormRef.value.validate((valid) => {
if (valid) { if (valid) {
showUserEditDialog.value = false showUserEditDialog.value = false;
console.log(user.value) console.log(user.value);
httpPost('/api/admin/user/save', user.value).then((res) => { httpPost("/api/admin/user/save", user.value)
ElMessage.success('操作成功!') .then((res) => {
if (add.value) { ElMessage.success("操作成功!");
users.value.items.push(res.data) if (add.value) {
} users.value.items.push(res.data);
}).catch((e) => { }
ElMessage.error('操作失败,' + e.message) })
}) .catch((e) => {
ElMessage.error("操作失败," + e.message);
});
} else { } else {
return false return false;
} }
}) });
} };
const userIds = ref([]) const userIds = ref([]);
const handleSelectionChange = function (rows) { const handleSelectionChange = function (rows) {
userIds.value = [] userIds.value = [];
rows.forEach((row) => { rows.forEach((row) => {
userIds.value.push(row.id) userIds.value.push(row.id);
}) });
} };
const multipleDelete = function () { const multipleDelete = function () {
ElMessageBox.confirm( ElMessageBox.confirm(
'此操作将会永久删除用户信息和聊天记录,确认操作吗?', "此操作将会永久删除用户信息和聊天记录,确认操作吗?",
'警告', "警告",
{ {
confirmButtonText: '确定', confirmButtonText: "确定",
cancelButtonText: '取消', cancelButtonText: "取消",
type: 'warning', type: "warning"
} }
).then(() => { )
loading.value = true .then(() => {
httpGet('/api/admin/user/remove', {ids: userIds.value}).then(() => { loading.value = true;
ElMessage.success('操作成功!') httpGet("/api/admin/user/remove", { ids: userIds.value })
fetchUserList(users.value.page, users.value.page_size) .then(() => {
loading.value = false ElMessage.success("操作成功!");
}).catch((e) => { fetchUserList(users.value.page, users.value.page_size);
ElMessage.error('操作失败,' + e.message) loading.value = false;
loading.value = false })
.catch((e) => {
ElMessage.error("操作失败," + e.message);
loading.value = false;
});
}) })
}).catch(() => { .catch(() => {
ElMessage.info('操作被取消') ElMessage.info("操作被取消");
}) });
} };
const resetPass = (row) => { const resetPass = (row) => {
showResetPassDialog.value = true showResetPassDialog.value = true;
pass.value.id = row.id pass.value.id = row.id;
pass.value.username = row.username pass.value.username = row.username;
} };
const doResetPass = () => { const doResetPass = () => {
httpPost('/api/admin/user/resetPass', pass.value).then(() => { httpPost("/api/admin/user/resetPass", pass.value)
ElMessage.success('操作成功!') .then(() => {
showResetPassDialog.value = false ElMessage.success("操作成功!");
}).catch((e) => { showResetPassDialog.value = false;
ElMessage.error('操作失败,' + e.message) })
}) .catch((e) => {
} ElMessage.error("操作失败," + e.message);
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@ -398,4 +465,4 @@ const doResetPass = () => {
} }
} }
</style> </style>