mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-09 18:53:43 +08:00
merge v4.1.0 and fixed conflicts
This commit is contained in:
@@ -3,14 +3,12 @@
|
||||
display flex
|
||||
width 100%
|
||||
|
||||
.el-input {
|
||||
width 50%
|
||||
.el-input,.el-select,.el-switch {
|
||||
margin-right 10px
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left 6px
|
||||
margin-top 2px
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,27 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app .common-layout {
|
||||
#app .chat-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside {
|
||||
#app .chat-page .el-aside {
|
||||
background-color: #252526;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .title-box {
|
||||
#app .chat-page .el-aside .title-box {
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .title-box span {
|
||||
#app .chat-page .el-aside .title-box span {
|
||||
padding-top: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list {
|
||||
#app .chat-page .el-aside .chat-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: #28292a;
|
||||
@@ -30,28 +30,28 @@
|
||||
border-right: 1px solid #2f3032;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .search-box {
|
||||
#app .chat-page .el-aside .chat-list .search-box {
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .search-box .el-input__wrapper {
|
||||
#app .chat-page .el-aside .chat-list .search-box .el-input__wrapper {
|
||||
background-color: #363535;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list ::-webkit-scrollbar {
|
||||
#app .chat-page .el-aside .chat-list ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content {
|
||||
#app .chat-page .el-aside .chat-list .content {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
@@ -59,17 +59,17 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item:hover {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item:hover {
|
||||
background-color: #343540;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .avatar {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title-input {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-title-input {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
margin-left: 10px;
|
||||
@@ -79,7 +79,7 @@
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-title {
|
||||
color: #c1c1c1;
|
||||
padding: 5px 10px;
|
||||
max-width: 220px;
|
||||
@@ -89,7 +89,7 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .btn {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .btn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
@@ -97,19 +97,19 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .btn .el-icon {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .btn .el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item.active {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item.active {
|
||||
background-color: #343540;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item.active .btn {
|
||||
#app .chat-page .el-aside .chat-list .content .chat-list-item.active .btn {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .tool-box {
|
||||
#app .chat-page .el-aside .tool-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
@@ -117,48 +117,48 @@
|
||||
border-top: 1px solid #3c3c3c;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .tool-box .user-info {
|
||||
#app .chat-page .el-aside .tool-box .user-info {
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link {
|
||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .el-image {
|
||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link .el-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .username {
|
||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link .username {
|
||||
display: flex;
|
||||
line-height: 22px;
|
||||
width: 230px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .el-icon {
|
||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link .el-icon {
|
||||
color: #ccc;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main {
|
||||
#app .chat-page .el-main {
|
||||
overflow: hidden;
|
||||
--el-main-padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head {
|
||||
#app .chat-page .el-main .chat-head {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: #28292a;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .chat-config {
|
||||
#app .chat-page .el-main .chat-head .chat-config {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -166,54 +166,54 @@
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .chat-config .role-select-label {
|
||||
#app .chat-page .el-main .chat-head .chat-config .role-select-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .chat-config .el-select {
|
||||
#app .chat-page .el-main .chat-head .chat-config .el-select {
|
||||
max-width: 150px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .chat-config .role-select {
|
||||
#app .chat-page .el-main .chat-head .chat-config .role-select {
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .chat-config .el-button .el-icon {
|
||||
#app .chat-page .el-main .chat-head .chat-config .el-button .el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .iconfont {
|
||||
#app .chat-page .el-main .chat-head .iconfont {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .is-circle {
|
||||
#app .chat-page .el-main .chat-head .is-circle {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-head .is-circle .iconfont {
|
||||
#app .chat-page .el-main .chat-head .is-circle .iconfont {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box {
|
||||
#app .chat-page .el-main .chat-box {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-left: 1px solid #4f4f4f;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container {
|
||||
#app .chat-page .el-main .chat-box #container {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container ::-webkit-scrollbar {
|
||||
#app .chat-page .el-main .chat-box #container ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .chat-box {
|
||||
#app .chat-page .el-main .chat-box #container .chat-box {
|
||||
overflow-y: scroll;
|
||||
--content-font-size: 16px;
|
||||
--content-color: #c1c1c1;
|
||||
@@ -221,28 +221,28 @@
|
||||
padding: 0 0 50px 0;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .chat-box .chat-line {
|
||||
#app .chat-page .el-main .chat-box #container .chat-box .chat-line {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .re-generate {
|
||||
#app .chat-page .el-main .chat-box #container .re-generate {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .re-generate .btn-box {
|
||||
#app .chat-page .el-main .chat-box #container .re-generate .btn-box {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .re-generate .btn-box .el-button .el-icon {
|
||||
#app .chat-page .el-main .chat-box #container .re-generate .btn-box .el-button .el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .input-box {
|
||||
#app .chat-page .el-main .chat-box #container .input-box {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -251,7 +251,7 @@
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .input-box .input-container {
|
||||
#app .chat-page .el-main .chat-box #container .input-box .input-container {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: none;
|
||||
@@ -261,24 +261,24 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .el-textarea .el-textarea__inner::-webkit-scrollbar {
|
||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .el-textarea .el-textarea__inner::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .select-file {
|
||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .select-file {
|
||||
position: absolute;
|
||||
right: 48px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .send-btn {
|
||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .send-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .send-btn .el-button {
|
||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .send-btn .el-button {
|
||||
padding: 8px 5px;
|
||||
border-radius: 6px;
|
||||
background: #19c37d;
|
||||
@@ -286,7 +286,7 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#app .common-layout .el-main .chat-box #container::-webkit-scrollbar {
|
||||
#app .chat-page .el-main .chat-box #container::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ $borderColor = #4676d0;
|
||||
|
||||
height: 100%;
|
||||
|
||||
.common-layout {
|
||||
.chat-page {
|
||||
height: 100%;
|
||||
|
||||
// left side
|
||||
@@ -156,6 +156,20 @@ $borderColor = #4676d0;
|
||||
max-width 130px;
|
||||
}
|
||||
|
||||
.setting {
|
||||
padding 5px
|
||||
border-radius 5px
|
||||
cursor pointer
|
||||
.iconfont {
|
||||
font-size 18px
|
||||
color #19c37d
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background #D5FAD3
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
@@ -169,13 +183,25 @@ $borderColor = #4676d0;
|
||||
position relative
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
width: 12px /* 滚动条宽度 */
|
||||
background #F1F1F1
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #c1c1c1;
|
||||
border-radius 12px
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #A8A8A8;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
//border-bottom: 1px solid #4f4f4f
|
||||
|
||||
// 变量定义
|
||||
@@ -252,25 +278,35 @@ $borderColor = #4676d0;
|
||||
border: 2px solid #21AA93
|
||||
border-radius 10px
|
||||
padding 10px
|
||||
background-color #F4F4F4
|
||||
|
||||
|
||||
.prompt-input::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
.input-inner {
|
||||
display flex
|
||||
flex-flow column
|
||||
width 100%
|
||||
line-height: 24px
|
||||
border none
|
||||
font-size 14px
|
||||
background none
|
||||
resize: none
|
||||
white-space: pre-wrap; /* 保持文本换行 */
|
||||
word-wrap: break-word; /* 允许单词换行 */
|
||||
overflow-wrap: break-word; /* 允许长单词换行,适用于现代浏览器 */
|
||||
|
||||
.file-list {
|
||||
padding-bottom 10px
|
||||
}
|
||||
.prompt-input::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
width 100%
|
||||
line-height: 24px
|
||||
border none
|
||||
font-size 14px
|
||||
background none
|
||||
resize: none
|
||||
white-space: pre-wrap; /* 保持文本换行 */
|
||||
word-wrap: break-word; /* 允许单词换行 */
|
||||
overflow-wrap: break-word; /* 允许长单词换行,适用于现代浏览器 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.send-btn {
|
||||
width 32px
|
||||
margin-left 10px
|
||||
|
||||
115
web/src/assets/css/login.styl
Normal file
115
web/src/assets/css/login.styl
Normal file
@@ -0,0 +1,115 @@
|
||||
.bg {
|
||||
position fixed
|
||||
left 0
|
||||
right 0
|
||||
top 0
|
||||
bottom 0
|
||||
background-color #313237
|
||||
background-image url("~@/assets/img/login-bg.jpg")
|
||||
background-size cover
|
||||
background-position center
|
||||
background-repeat repeat-y
|
||||
//filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
|
||||
}
|
||||
|
||||
.main {
|
||||
.contain {
|
||||
position fixed
|
||||
left 50%
|
||||
top 40%
|
||||
width 90%
|
||||
max-width 400px;
|
||||
transform translate(-50%, -50%)
|
||||
padding 20px 10px;
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
|
||||
.logo {
|
||||
text-align center
|
||||
|
||||
.el-image {
|
||||
width 120px;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 24px
|
||||
font-size 24px
|
||||
color $white_v1
|
||||
letter-space 2px
|
||||
text-align center
|
||||
padding-top 10px
|
||||
}
|
||||
|
||||
.content {
|
||||
width 100%
|
||||
height: auto
|
||||
border-radius 3px
|
||||
|
||||
.block {
|
||||
margin-bottom 16px
|
||||
|
||||
.el-input__inner {
|
||||
border 1px solid $gray-v6 !important
|
||||
|
||||
.el-icon-user, .el-icon-lock {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
padding-top 10px;
|
||||
|
||||
.login-btn {
|
||||
width 100%
|
||||
font-size 16px
|
||||
letter-spacing 2px
|
||||
}
|
||||
}
|
||||
|
||||
.text-line {
|
||||
justify-content center
|
||||
padding-top 10px;
|
||||
font-size 14px;
|
||||
}
|
||||
|
||||
.opt {
|
||||
padding 15px
|
||||
.el-col {
|
||||
text-align center
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 2px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.clogin {
|
||||
padding 15px
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.iconfont {
|
||||
font-size 20px
|
||||
background: #E9F1F6;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.iconfont.icon-wechat {
|
||||
color #0bc15f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
color #ffffff;
|
||||
|
||||
.container {
|
||||
padding 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
237
web/src/assets/css/markdown/vue.css
Normal file
237
web/src/assets/css/markdown/vue.css
Normal file
@@ -0,0 +1,237 @@
|
||||
.chat-line {
|
||||
ol, ul {
|
||||
margin: 0.8em 0;
|
||||
list-style: normal;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
font-weight: 600;
|
||||
padding: 0 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
h1:hover a.anchor,
|
||||
h2:hover a.anchor,
|
||||
h3:hover a.anchor,
|
||||
h4:hover a.anchor,
|
||||
h5:hover a.anchor,
|
||||
h6:hover a.anchor {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 tt,
|
||||
h1 code {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
h2 tt,
|
||||
h2 code {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
h3 tt,
|
||||
h3 code {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
h4 tt,
|
||||
h4 code {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
h5 tt,
|
||||
h5 code {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
h6 tt,
|
||||
h6 code {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
h2 a,
|
||||
h3 a {
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: .4rem;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
line-height: 1.225;
|
||||
margin: 35px 0 15px;
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.43;
|
||||
margin: 20px 0 7px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
p,
|
||||
blockquote,
|
||||
ul,
|
||||
ol,
|
||||
dl,
|
||||
table {
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
li > ol,
|
||||
li > ul {
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 2px;
|
||||
padding: 0;
|
||||
margin: 16px 0;
|
||||
background-color: #e7e7e7;
|
||||
border: 0 none;
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
body > h2:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
body > h1:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
body > h1:first-child + h2 {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
body > h3:first-child,
|
||||
body > h4:first-child,
|
||||
body > h5:first-child,
|
||||
body > h6:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
a:first-child h1,
|
||||
a:first-child h2,
|
||||
a:first-child h3,
|
||||
a:first-child h4,
|
||||
a:first-child h5,
|
||||
a:first-child h6 {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
h1 p,
|
||||
h2 p,
|
||||
h3 p,
|
||||
h4 p,
|
||||
h5 p,
|
||||
h6 p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
li p.first {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
ul:first-child,
|
||||
ol:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul:last-child,
|
||||
ol:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #42b983;
|
||||
padding: 10px 15px;
|
||||
color: #777;
|
||||
background-color: rgba(66, 185, 131, .1);
|
||||
}
|
||||
|
||||
table {
|
||||
padding: 0;
|
||||
word-break: initial;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-top: 1px solid #dfe2e5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table tr:nth-child(2n),
|
||||
thead {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
table tr th {
|
||||
font-weight: bold;
|
||||
border: 1px solid #dfe2e5;
|
||||
border-bottom: 0;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 6px 13px;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
border: 1px solid #dfe2e5;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 6px 13px;
|
||||
}
|
||||
|
||||
table tr th:first-child,
|
||||
table tr td:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
table tr th:last-child,
|
||||
table tr td:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 388 KiB |
@@ -1,152 +1,420 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-prompt">
|
||||
<div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="User"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover"/>
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{GetFileType(file.ext)}}</span>
|
||||
<span>{{FormatFileSize(file.size)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<span class="bar-item">tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-line chat-line-prompt-chat" v-else>
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="User"/>
|
||||
<img :src="data.icon" alt="User"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="createdAt">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<!-- <span class="bar-item">Tokens: {{ finalTokens }}</span>-->
|
||||
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover"/>
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{GetFileType(file.ext)}}</span>
|
||||
<span>{{FormatFileSize(file.size)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="content"></div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<span class="bar-item">tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {Clock} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import hl from "highlight.js";
|
||||
import {dateFormat, isImage, processPrompt} from "@/utils/libs";
|
||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatPrompt',
|
||||
components: {Clock},
|
||||
methods: {},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'images/user-icon.png',
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tokens: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
finalTokens: this.tokens
|
||||
const mathjaxPlugin = require('markdown-it-mathjax3')
|
||||
const md = require('markdown-it')({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
highlight: function (str, lang) {
|
||||
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
|
||||
// 显示复制代码按钮
|
||||
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '</textarea>')}</textarea>`
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
|
||||
}
|
||||
|
||||
// 处理代码高亮
|
||||
const preCode = md.utils.escapeHtml(str)
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
|
||||
}
|
||||
});
|
||||
md.use(mathjaxPlugin)
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {
|
||||
content: '',
|
||||
created_at: '',
|
||||
tokens: 0,
|
||||
model: '',
|
||||
icon: '',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.finalTokens) {
|
||||
httpPost("/api/chat/tokens", {text: this.content, model: this.model}).then(res => {
|
||||
this.finalTokens = res.data;
|
||||
}).catch(() => {
|
||||
})
|
||||
listStyle: {
|
||||
type: String,
|
||||
default: 'list',
|
||||
},
|
||||
})
|
||||
const finalTokens = ref(props.data.tokens)
|
||||
const content =ref(processPrompt(props.data.content))
|
||||
const files = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
if (!finalTokens.value) {
|
||||
httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
|
||||
finalTokens.value = res.data;
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const linkRegex = /(https?:\/\/\S+)/g;
|
||||
const links = props.data.content.match(linkRegex);
|
||||
if (links) {
|
||||
httpPost("/api/upload/list", {urls: links}).then(res => {
|
||||
files.value = res.data
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
for (let link of links) {
|
||||
content.value = content.value.replace(link,"")
|
||||
}
|
||||
}
|
||||
content.value = md.render(content.value.trim())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-prompt {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
@import '@/assets/css/markdown/vue.css';
|
||||
.chat-page,.chat-export {
|
||||
.chat-line-prompt-list {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
width 100%
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
margin 10px 0
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color #20a0ff
|
||||
}
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
.chat-item {
|
||||
width 100%
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
.file-list-box {
|
||||
display flex
|
||||
flex-flow column
|
||||
.image {
|
||||
display flex
|
||||
flex-flow row
|
||||
margin-right 10px
|
||||
position relative
|
||||
top 2px;
|
||||
|
||||
.el-image {
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
}
|
||||
}
|
||||
.item {
|
||||
display flex
|
||||
flex-flow row
|
||||
border-radius 10px
|
||||
background-color #ffffff
|
||||
border 1px solid #e3e3e3
|
||||
padding 6px
|
||||
margin-bottom 10px
|
||||
|
||||
.icon {
|
||||
.el-image {
|
||||
width 40px
|
||||
height 40px
|
||||
}
|
||||
}
|
||||
.body {
|
||||
margin-left 8px
|
||||
font-size 14px
|
||||
.title {
|
||||
font-weight bold
|
||||
line-height 24px
|
||||
color #0D0D0D
|
||||
}
|
||||
.info {
|
||||
color #B4B4B4
|
||||
|
||||
span {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 0;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
margin 10px 0
|
||||
}
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px 10px 10px 0;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.chat-line-prompt-chat {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
padding 0 25px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
max-width 60%
|
||||
|
||||
.file-list-box {
|
||||
display flex
|
||||
flex-flow column
|
||||
.image {
|
||||
display flex
|
||||
flex-flow row
|
||||
margin-right 10px
|
||||
position relative
|
||||
|
||||
.el-image {
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
}
|
||||
}
|
||||
.item {
|
||||
display flex
|
||||
flex-flow row
|
||||
border-radius 10px
|
||||
background-color #ffffff
|
||||
border 1px solid #e3e3e3
|
||||
padding 6px
|
||||
margin-bottom 10px
|
||||
|
||||
.icon {
|
||||
.el-image {
|
||||
width 40px
|
||||
height 40px
|
||||
}
|
||||
}
|
||||
.body {
|
||||
margin-left 8px
|
||||
font-size 14px
|
||||
.title {
|
||||
font-weight bold
|
||||
line-height 24px
|
||||
color #0D0D0D
|
||||
}
|
||||
.info {
|
||||
color #B4B4B4
|
||||
|
||||
span {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.content-wrapper {
|
||||
display flex
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 1rem
|
||||
color #222222;
|
||||
font-size: var(--content-font-size);
|
||||
overflow: auto;
|
||||
background-color #98e165
|
||||
border-radius: 0 10px 10px 10px;
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
margin 10px 0
|
||||
}
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.bar {
|
||||
padding 10px 10px 10px 0;
|
||||
|
||||
.bar-item {
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-reply">
|
||||
<div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="ChatGPT">
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="content" v-html="data.content"></div>
|
||||
<div class="bar" v-if="data.created_at">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<!-- <span class="bar-item">Tokens: {{ tokens }}</span>-->
|
||||
<span class="bar-item">tokens: {{ data.tokens }}</span>
|
||||
<span class="bar-item">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@@ -61,6 +61,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-line chat-line-reply-chat" v-else>
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="ChatGPT">
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="data.content"></div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<span class="bar-item">tokens: {{ data.tokens }}</span>
|
||||
<span class="bar-item bg">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.orgContent">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly">
|
||||
<span class="bar-item bg" @click="reGenerate(data.prompt)">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="重新生成"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon><Refresh/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="bar-item bg" @click="synthesis(data.orgContent)">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="生成语音朗读"
|
||||
placement="bottom"
|
||||
>
|
||||
<i class="iconfont icon-speaker"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -71,12 +124,22 @@ import {dateFormat} from "@/utils/libs";
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
default: {
|
||||
icon: "",
|
||||
content: "",
|
||||
created_at: "",
|
||||
tokens: 0,
|
||||
orgContent: ""
|
||||
},
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
listStyle: {
|
||||
type: String,
|
||||
default: 'list',
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['regen']);
|
||||
@@ -92,13 +155,15 @@ const synthesis = (text) => {
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = (prompt) => {
|
||||
console.log(prompt)
|
||||
emits('regen', prompt)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.common-layout {
|
||||
.chat-line-reply {
|
||||
@import '@/assets/css/markdown/vue.css';
|
||||
.chat-page,.chat-export {
|
||||
.chat-line-reply-list {
|
||||
justify-content: center;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
width 100%
|
||||
@@ -126,24 +191,18 @@ const reGenerate = (prompt) => {
|
||||
.chat-item {
|
||||
width 100%
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
padding: 0
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow auto;
|
||||
|
||||
a {
|
||||
color #20a0ff
|
||||
}
|
||||
|
||||
// control the image size in content
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
@@ -170,10 +229,11 @@ const reGenerate = (prompt) => {
|
||||
|
||||
.code-container {
|
||||
position relative
|
||||
display flex
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
line-height 1.5
|
||||
width 100%
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
@@ -194,7 +254,7 @@ const reGenerate = (prompt) => {
|
||||
.lang-name {
|
||||
position absolute;
|
||||
right 10px
|
||||
bottom 50px
|
||||
bottom 20px
|
||||
padding 2px 6px 4px 6px
|
||||
background-color #444444
|
||||
border-radius 10px
|
||||
@@ -241,7 +301,7 @@ const reGenerate = (prompt) => {
|
||||
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
padding 10px 10px 10px 0;
|
||||
|
||||
.bar-item {
|
||||
background-color #e7e7e8;
|
||||
@@ -277,6 +337,187 @@ const reGenerate = (prompt) => {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-line-reply-chat {
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
padding 0 25px;
|
||||
width 100%
|
||||
flex-flow row-reverse
|
||||
|
||||
.chat-icon {
|
||||
margin-left 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
max-width 60%
|
||||
|
||||
.content-wrapper {
|
||||
display flex
|
||||
flex-flow row-reverse
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 1rem
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
overflow auto;
|
||||
background-color #F5F5F5
|
||||
border-radius: 10px 0 10px 10px;
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
|
||||
code {
|
||||
color #374151
|
||||
background-color #e7e7e8
|
||||
padding 0 3px;
|
||||
border-radius 5px;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
|
||||
.code-container {
|
||||
position relative
|
||||
display flex
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
width 100%
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
right 10px
|
||||
top 10px
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
color #c1c1c1
|
||||
|
||||
&:hover {
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
position absolute;
|
||||
right 10px
|
||||
bottom 20px
|
||||
padding 2px 6px 4px 6px
|
||||
background-color #444444
|
||||
border-radius 10px
|
||||
color #00e0e0
|
||||
}
|
||||
|
||||
|
||||
// 设置表格边框
|
||||
|
||||
table {
|
||||
width 100%
|
||||
margin-bottom 1rem
|
||||
color #212529
|
||||
border-collapse collapse;
|
||||
border 1px solid #dee2e6;
|
||||
background-color #ffffff
|
||||
|
||||
thead {
|
||||
th {
|
||||
border 1px solid #dee2e6
|
||||
vertical-align: bottom
|
||||
border-bottom: 2px solid #dee2e6
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border 1px solid #dee2e6
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
// 代码快
|
||||
|
||||
blockquote {
|
||||
margin 0
|
||||
background-color: #ebfffe;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-left: 0.5rem solid;
|
||||
border-color: #026863;
|
||||
color: #2c3e50;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px 10px 10px 0;
|
||||
|
||||
.bar-item {
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.bar-item.bg {
|
||||
background-color #e7e7e8
|
||||
cursor pointer
|
||||
}
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.tool-box {
|
||||
font-size 16px;
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
50
web/src/components/ChatSetting.vue
Normal file
50
web/src/components/ChatSetting.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="config-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:before-close="close"
|
||||
style="max-width: 600px"
|
||||
title="聊天配置"
|
||||
>
|
||||
<div class="chat-setting">
|
||||
<el-form :model="data" label-width="100px" label-position="left">
|
||||
<el-form-item label="聊天样式:">
|
||||
<el-radio-group v-model="data.style" @change="(val) => {store.setChatListStyle(val)}">
|
||||
<el-radio value="list">列表样式</el-radio>
|
||||
<el-radio value="chat">对话样式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue"
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
const store = useSharedStore();
|
||||
|
||||
const data = ref({
|
||||
style: store.chatListStyle,
|
||||
})
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const emits = defineEmits(['hide']);
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.chat-setting {
|
||||
|
||||
}
|
||||
</style>
|
||||
115
web/src/components/FileList.vue
Normal file
115
web/src/components/FileList.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<el-container class="chat-file-list">
|
||||
<div v-for="file in fileList">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover"/>
|
||||
<div class="action">
|
||||
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{substr(file.name, 30)}}</el-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{GetFileType(file.ext)}}</span>
|
||||
<span>{{FormatFileSize(file.size)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {CircleCloseFilled} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
|
||||
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default:[],
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['removeFile']);
|
||||
const fileList = ref(props.files)
|
||||
|
||||
|
||||
const removeFile = (file) => {
|
||||
fileList.value = removeArrayItem(fileList.value, file, (v1,v2) => v1.url===v2.url)
|
||||
emits('removeFile', file)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
|
||||
.chat-file-list {
|
||||
display flex
|
||||
flex-flow row
|
||||
.image {
|
||||
display flex
|
||||
flex-flow row
|
||||
margin-right 10px
|
||||
position relative
|
||||
|
||||
.el-image {
|
||||
height 56px
|
||||
width 56px
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
}
|
||||
}
|
||||
.item {
|
||||
position relative
|
||||
display flex
|
||||
flex-flow row
|
||||
border-radius 10px
|
||||
background-color #ffffff
|
||||
border 1px solid #e3e3e3
|
||||
padding 6px
|
||||
margin-right 10px
|
||||
|
||||
.icon {
|
||||
.el-image {
|
||||
width 40px
|
||||
height 40px
|
||||
}
|
||||
}
|
||||
.body {
|
||||
margin-left 5px
|
||||
font-size 14px
|
||||
.title {
|
||||
line-height 24px
|
||||
color #0D0D0D
|
||||
}
|
||||
.info {
|
||||
color #B4B4B4
|
||||
|
||||
span {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
position absolute
|
||||
top -8px
|
||||
right -8px
|
||||
color #da0d54
|
||||
cursor pointer
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-container class="file-list-box">
|
||||
<el-container class="file-select-box">
|
||||
<a class="file-upload-img" @click="fetchFiles">
|
||||
<i class="iconfont icon-attachment-st"></i>
|
||||
</a>
|
||||
@@ -20,6 +20,7 @@
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
@@ -34,8 +35,8 @@
|
||||
effect="dark"
|
||||
:content="file.name"
|
||||
placement="top">
|
||||
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file.url)"/>
|
||||
<el-image :src="getFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file.url)"/>
|
||||
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/>
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
|
||||
</el-tooltip>
|
||||
|
||||
<div class="opt">
|
||||
@@ -55,6 +56,7 @@ import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {Delete, Plus} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem} from "@/utils/libs";
|
||||
import {GetFileIcon} from "@/store/system";
|
||||
|
||||
const props = defineProps({
|
||||
userId: Number,
|
||||
@@ -65,30 +67,12 @@ const fileList = ref([])
|
||||
|
||||
const fetchFiles = () => {
|
||||
show.value = true
|
||||
httpGet("/api/upload/list").then(res => {
|
||||
httpPost("/api/upload/list").then(res => {
|
||||
fileList.value = res.data
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const getFileIcon = (ext) => {
|
||||
const files = {
|
||||
".docx": "doc.png",
|
||||
".doc": "doc.png",
|
||||
".xls": "xls.png",
|
||||
".xlsx": "xls.png",
|
||||
".ppt": "ppt.png",
|
||||
".pptx": "ppt.png",
|
||||
".md": "md.png",
|
||||
".pdf": "pdf.png",
|
||||
".sql": "sql.png"
|
||||
}
|
||||
if (files[ext]) {
|
||||
return '/images/ext/' + files[ext]
|
||||
}
|
||||
|
||||
return '/images/ext/file.png'
|
||||
}
|
||||
|
||||
const afterRead = (file) => {
|
||||
const formData = new FormData();
|
||||
@@ -113,19 +97,19 @@ const removeFile = (file) => {
|
||||
})
|
||||
}
|
||||
|
||||
const insertURL = (url) => {
|
||||
const insertURL = (file) => {
|
||||
show.value = false
|
||||
// 如果是相对路径,处理成绝对路径
|
||||
if (url.indexOf("http") === -1) {
|
||||
url = location.protocol + "//" + location.host + url
|
||||
if (file.url.indexOf("http") === -1) {
|
||||
file.url = location.protocol + "//" + location.host + file.url
|
||||
}
|
||||
emits('selected', url)
|
||||
emits('selected', file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.file-list-box {
|
||||
.file-select-box {
|
||||
.file-upload-img {
|
||||
.iconfont {
|
||||
font-size: 24px;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="foot-container">
|
||||
<div class="footer">
|
||||
Powered by {{ author }} @
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank" style="--el-link-text-color:#ffffff">
|
||||
<el-link type="primary" :href="gitURL" target="_blank" style="--el-link-text-color:#ffffff">
|
||||
{{ title }} -
|
||||
{{ version }}
|
||||
</el-link>
|
||||
@@ -15,6 +15,7 @@ import {ref} from "vue";
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
|
||||
const author = ref('极客学长')
|
||||
</script>
|
||||
|
||||
@@ -33,6 +34,10 @@ const author = ref('极客学长')
|
||||
font-size 14px;
|
||||
padding 20px;
|
||||
width 100%
|
||||
|
||||
.el-link {
|
||||
color #409eff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ const submitLogin = () => {
|
||||
}
|
||||
|
||||
httpPost('/api/user/login', data.value).then((res) => {
|
||||
setUserToken(res.data)
|
||||
setUserToken(res.data.token)
|
||||
ElMessage.success("登录成功!")
|
||||
emits("hide")
|
||||
emits('success')
|
||||
|
||||
58
web/src/components/TaskList.vue
Normal file
58
web/src/components/TaskList.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="running-job-list">
|
||||
<div class="running-job-box" v-if="list.length > 0">
|
||||
<div class="job-item" v-for="item in list">
|
||||
<div v-if="item.progress > 0" class="job-item-inner">
|
||||
<el-image v-if="item.img_url" :src="item['img_url']" fit="cover" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="progress">
|
||||
<el-progress type="circle" :percentage="item.progress" :width="100"
|
||||
color="#47fff1"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-image fit="cover" v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {CircleCloseFilled, Picture} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default:[],
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "~@/assets/css/running-job-list.styl"
|
||||
</style>
|
||||
@@ -46,8 +46,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {DocumentCopy} from "@element-plus/icons-vue";
|
||||
@@ -73,7 +73,7 @@ onMounted(() => {
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpPost('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => {
|
||||
httpGet('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
|
||||
@@ -101,6 +101,12 @@ const routes = [
|
||||
meta: {title: '用户登录'},
|
||||
component: () => import('@/views/Login.vue'),
|
||||
},
|
||||
{
|
||||
name: 'login-callback',
|
||||
path: '/login/callback',
|
||||
meta: {title: '用户登录'},
|
||||
component: () => import('@/views/LoginCallback.vue'),
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
path: '/register',
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import Storage from 'good-storage'
|
||||
|
||||
export const useSharedStore = defineStore('shared', {
|
||||
state: () => ({
|
||||
showLoginDialog: false
|
||||
showLoginDialog: false,
|
||||
chatListStyle: Storage.get("chat_list_style","chat")
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
setShowLoginDialog(value) {
|
||||
this.showLoginDialog = value;
|
||||
},
|
||||
setChatListStyle(value) {
|
||||
this.chatListStyle = value;
|
||||
Storage.set("chat_list_style", value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,4 +24,38 @@ export function getAdminTheme() {
|
||||
|
||||
export function setAdminTheme(theme) {
|
||||
Storage.set(ADMIN_THEME, theme)
|
||||
}
|
||||
|
||||
export function GetFileIcon(ext) {
|
||||
const files = {
|
||||
".docx": "doc.png",
|
||||
".doc": "doc.png",
|
||||
".xls": "xls.png",
|
||||
".xlsx": "xls.png",
|
||||
".csv": "xls.png",
|
||||
".ppt": "ppt.png",
|
||||
".pptx": "ppt.png",
|
||||
".md": "md.png",
|
||||
".pdf": "pdf.png",
|
||||
".sql": "sql.png"
|
||||
}
|
||||
if (files[ext]) {
|
||||
return '/images/ext/' + files[ext]
|
||||
}
|
||||
|
||||
return '/images/ext/file.png'
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
export function GetFileType (ext) {
|
||||
return ext.replace(".", "").toUpperCase()
|
||||
}
|
||||
|
||||
// 将文件大小转成字符
|
||||
export function FormatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
@@ -26,18 +26,16 @@ axios.interceptors.request.use(
|
||||
})
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
let data = response.data;
|
||||
if (data.code === 0) {
|
||||
return response
|
||||
} else if (data.code === 400) {
|
||||
if (response.request.responseURL.indexOf("/api/admin") !== -1) {
|
||||
return response
|
||||
}, error => {
|
||||
if (error.response.status === 401 || error.response.status === 400) {
|
||||
if (error.response.request.responseURL.indexOf("/api/admin") !== -1) {
|
||||
removeAdminToken()
|
||||
} else {
|
||||
removeUserToken()
|
||||
}
|
||||
return Promise.reject(error.response.data)
|
||||
}
|
||||
return Promise.reject(response.data)
|
||||
}, error => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
|
||||
@@ -188,58 +188,13 @@ export function processContent(content) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lines = content.split("\n")
|
||||
if (lines.length <= 1) {
|
||||
return content
|
||||
}
|
||||
const texts = []
|
||||
// 定义匹配数学公式的正则表达式
|
||||
const formulaRegex = /^\s*[a-z|A-Z]+[^=]+\s*=\s*[^=]+$/;
|
||||
let hasCode = false
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
// 处理引用块换行
|
||||
if (lines[i].startsWith(">")) {
|
||||
texts.push(lines[i])
|
||||
texts.push("\n")
|
||||
continue
|
||||
}
|
||||
// 如果包含代码块则跳过公式检测
|
||||
if (lines[i].indexOf("```") !== -1) {
|
||||
texts.push(lines[i])
|
||||
hasCode = true
|
||||
continue
|
||||
}
|
||||
// 识别并处理数学公式,需要排除那些已经被识别出来的公式
|
||||
if (i > 0 && formulaRegex.test(lines[i]) && lines[i - 1].indexOf("$$") === -1 && !hasCode) {
|
||||
texts.push("$$")
|
||||
texts.push(lines[i])
|
||||
texts.push("$$")
|
||||
continue
|
||||
}
|
||||
texts.push(lines[i])
|
||||
}
|
||||
return texts.join("\n")
|
||||
return content
|
||||
}
|
||||
|
||||
export function processPrompt(prompt) {
|
||||
prompt = prompt.replace(/&/g, "&")
|
||||
return prompt.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
const linkRegex = /(https?:\/\/\S+)/g;
|
||||
const links = prompt.match(linkRegex);
|
||||
if (links) {
|
||||
for (let link of links) {
|
||||
if (isImage(link)) {
|
||||
const index = prompt.indexOf(link)
|
||||
if (prompt.substring(index - 1, 2) !== "]") {
|
||||
prompt = prompt.replace(link, "\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
|
||||
// 判断是否为微信浏览器
|
||||
@@ -258,3 +213,4 @@ export function showLoginDialog(router) {
|
||||
// on cancel
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,22 +6,14 @@
|
||||
</div>
|
||||
|
||||
<div v-for="item in chatData" :key="item.id">
|
||||
<chat-prompt
|
||||
v-if="item.type==='prompt'"
|
||||
:icon="item.icon"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:tokens="item['tokens']"
|
||||
:model="item['model']"
|
||||
:content="item.content"/>
|
||||
<chat-reply v-else-if="item.type==='reply'"
|
||||
:data="item" :read-only="true"/>
|
||||
<chat-prompt v-if="item.type==='prompt'" :data="item" list-style="list"/>
|
||||
<chat-reply v-else-if="item.type==='reply'" :data="item" :read-only="true" list-style="list"/>
|
||||
</div>
|
||||
</div><!-- end chat box -->
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import {nextTick, onMounted, ref} from "vue";
|
||||
@@ -98,7 +90,7 @@ onMounted(() => {
|
||||
padding 0 20px
|
||||
|
||||
.chat-box {
|
||||
width 800px;
|
||||
width 100%;
|
||||
// 变量定义
|
||||
--content-font-size: 16px;
|
||||
--content-color: #c1c1c1;
|
||||
@@ -110,57 +102,13 @@ onMounted(() => {
|
||||
text-align center
|
||||
}
|
||||
|
||||
|
||||
.chat-line-prompt {
|
||||
.chat-line {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.chat-line-inner {
|
||||
.chat-icon {
|
||||
margin-right: 0
|
||||
}
|
||||
.content {
|
||||
padding-top: 0
|
||||
font-size 16px;
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-line-reply {
|
||||
padding-top: 1.5rem;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex
|
||||
|
||||
.bar-item {
|
||||
background-color: #f7f7f8;
|
||||
color: #888;
|
||||
padding: 3px 5px;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
margin-right: 20px
|
||||
|
||||
img {
|
||||
width 30px
|
||||
height 30px
|
||||
border-radius: 10px;
|
||||
padding: 1px
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
img {
|
||||
max-width 90%
|
||||
}
|
||||
}
|
||||
max-width 800px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<div class="chat-page">
|
||||
<el-container>
|
||||
<el-aside>
|
||||
<div class="chat-list">
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="content" :style="{height: leftBoxHeight+'px'}">
|
||||
<el-row v-for="chat in chatList" :key="chat.chat_id">
|
||||
<div :class="chat.chat_id === activeChat.chat_id?'chat-list-item active':'chat-list-item'"
|
||||
@click="changeChat(chat)">
|
||||
@click="loadChat(chat)">
|
||||
<el-image :src="chat.icon" class="avatar"/>
|
||||
<span class="chat-title-input" v-if="chat.edit">
|
||||
<el-input v-model="tmpChatTitle" size="small" @keydown="titleKeydown($event, chat)"
|
||||
@@ -99,6 +99,12 @@
|
||||
</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<span class="setting" @click="showChatSetting = true">
|
||||
<el-tooltip class="box-item" effect="dark" content="对话设置">
|
||||
<i class="iconfont icon-config"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -109,13 +115,8 @@
|
||||
</div>
|
||||
<div v-for="item in chatData" :key="item.id" v-else>
|
||||
<chat-prompt
|
||||
v-if="item.type==='prompt'"
|
||||
:icon="item.icon"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:tokens="item['tokens']"
|
||||
:model="getModelValue(modelID)"
|
||||
:content="item.content"/>
|
||||
<chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false"/>
|
||||
v-if="item.type==='prompt'" :data="item" :list-style="listStyle"/>
|
||||
<chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle"/>
|
||||
</div>
|
||||
</div><!-- end chat box -->
|
||||
|
||||
@@ -129,14 +130,18 @@
|
||||
|
||||
<span class="tool-item" v-if="isLogin">
|
||||
<el-tooltip class="box-item" effect="dark" content="上传附件">
|
||||
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertURL"/>
|
||||
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertFile"/>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<div class="input-body">
|
||||
<div ref="textHeightRef" class="hide-div">{{prompt}}</div>
|
||||
<div class="input-border">
|
||||
<textarea
|
||||
<div class="input-inner">
|
||||
<div class="file-list" v-if="files.length > 0">
|
||||
<file-list :files="files" @remove-file="removeFile" />
|
||||
</div>
|
||||
<textarea
|
||||
ref="inputRef"
|
||||
class="prompt-input"
|
||||
:rows="row"
|
||||
@@ -146,6 +151,8 @@
|
||||
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
|
||||
autofocus>
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<span class="send-btn">
|
||||
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
|
||||
<el-icon>
|
||||
@@ -181,21 +188,21 @@
|
||||
</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<ChatSetting :show="showChatSetting" @hide="showChatSetting = false"/>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import {nextTick, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
import {Delete, Edit, More, Plus, Promotion, Search, Share, VideoPause} from '@element-plus/icons-vue'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import {
|
||||
dateFormat,
|
||||
isMobile,
|
||||
processContent,
|
||||
processPrompt,
|
||||
randString,
|
||||
removeArrayItem,
|
||||
UUID
|
||||
@@ -210,6 +217,8 @@ import {checkSession} from "@/action/session";
|
||||
import Welcome from "@/components/Welcome.vue";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import FileSelect from "@/components/FileSelect.vue";
|
||||
import FileList from "@/components/FileList.vue";
|
||||
import ChatSetting from "@/components/ChatSetting.vue";
|
||||
|
||||
const title = ref('ChatGPT-智能助手');
|
||||
const models = ref([])
|
||||
@@ -236,6 +245,12 @@ const notice = ref("")
|
||||
const noticeKey = ref("SYSTEM_NOTICE")
|
||||
const store = useSharedStore();
|
||||
const row = ref(1)
|
||||
const showChatSetting = ref(false)
|
||||
const listStyle = ref(store.chatListStyle)
|
||||
watch(() => store.chatListStyle, (newValue) => {
|
||||
listStyle.value = newValue
|
||||
});
|
||||
|
||||
|
||||
if (isMobile()) {
|
||||
router.replace("/mobile/chat")
|
||||
@@ -417,12 +432,8 @@ const newChat = () => {
|
||||
connect(null, roleId.value)
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
const changeChat = (chat) => {
|
||||
localStorage.setItem("chat_id", chat.chat_id)
|
||||
loadChat(chat)
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
const loadChat = function (chat) {
|
||||
if (!isLogin.value) {
|
||||
store.setShowLoginDialog(true)
|
||||
@@ -546,6 +557,7 @@ const socket = ref(null);
|
||||
const activelyClose = ref(false); // 主动关闭
|
||||
const canSend = ref(true);
|
||||
const heartbeatHandle = ref(null)
|
||||
const sessionId = ref("")
|
||||
const connect = function (chat_id, role_id) {
|
||||
let isNewChat = false;
|
||||
if (!chat_id) {
|
||||
@@ -560,7 +572,7 @@ const connect = function (chat_id, role_id) {
|
||||
|
||||
const _role = getRoleById(role_id);
|
||||
// 初始化 WebSocket 对象
|
||||
const _sessionId = getSessionId();
|
||||
sessionId.value = getSessionId();
|
||||
let host = process.env.VUE_APP_WS_HOST
|
||||
if (host === '') {
|
||||
if (location.protocol === 'https:') {
|
||||
@@ -582,7 +594,7 @@ const connect = function (chat_id, role_id) {
|
||||
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
|
||||
});
|
||||
}
|
||||
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
|
||||
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
chatData.value = []; // 初始化聊天数据
|
||||
enableInput()
|
||||
@@ -615,10 +627,12 @@ const connect = function (chat_id, role_id) {
|
||||
reader.onload = () => {
|
||||
const data = JSON.parse(String(reader.result));
|
||||
if (data.type === 'start') {
|
||||
const prePrompt = chatData.value[chatData.value.length-1].content
|
||||
chatData.value.push({
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
prompt:prePrompt,
|
||||
content: ""
|
||||
});
|
||||
} else if (data.type === 'end') { // 消息接收完毕
|
||||
@@ -743,12 +757,18 @@ const sendMessage = function () {
|
||||
if (prompt.value.trim().length === 0 || canSend.value === false) {
|
||||
return false;
|
||||
}
|
||||
// 如果携带了文件,则串上文件地址
|
||||
let content = prompt.value
|
||||
if (files.value.length > 0) {
|
||||
content += files.value.map(file => file.url).join(" ")
|
||||
}
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: "prompt",
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: md.render(processPrompt(prompt.value)),
|
||||
content: content,
|
||||
model: getModelValue(modelID.value),
|
||||
created_at: new Date().getTime() / 1000,
|
||||
});
|
||||
|
||||
@@ -758,9 +778,11 @@ const sendMessage = function () {
|
||||
|
||||
showHello.value = false
|
||||
disableInput(false)
|
||||
socket.value.send(JSON.stringify({type: "chat", content: prompt.value}));
|
||||
tmpChatTitle.value = prompt.value
|
||||
prompt.value = '';
|
||||
socket.value.send(JSON.stringify({type: "chat", content: content}));
|
||||
tmpChatTitle.value = content
|
||||
prompt.value = ''
|
||||
files.value = []
|
||||
row.value = 1
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -810,9 +832,11 @@ const loadChatHistory = function (chatId) {
|
||||
showHello.value = false
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i].orgContent = data[i].content;
|
||||
data[i].content = md.render(processContent(data[i].content))
|
||||
if (i > 0 && data[i].type === 'reply') {
|
||||
data[i].prompt = data[i - 1].orgContent
|
||||
if (data[i].type === 'reply') {
|
||||
data[i].content = md.render(processContent(data[i].content))
|
||||
if (i > 0) {
|
||||
data[i].prompt = data[i - 1].orgContent
|
||||
}
|
||||
}
|
||||
chatData.value.push(data[i]);
|
||||
}
|
||||
@@ -829,7 +853,7 @@ const loadChatHistory = function (chatId) {
|
||||
|
||||
const stopGenerate = function () {
|
||||
showStopGenerate.value = false;
|
||||
httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
|
||||
httpGet("/api/chat/stop?session_id=" + sessionId.value).then(() => {
|
||||
enableInput()
|
||||
})
|
||||
}
|
||||
@@ -843,7 +867,7 @@ const reGenerate = function (prompt) {
|
||||
type: "prompt",
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: md.render(text)
|
||||
content: text
|
||||
});
|
||||
socket.value.send(JSON.stringify({type: "chat", content: prompt}));
|
||||
}
|
||||
@@ -893,9 +917,13 @@ const notShow = () => {
|
||||
showNotice.value = false
|
||||
}
|
||||
|
||||
// 插入文件路径
|
||||
const insertURL = (url) => {
|
||||
prompt.value += " " + url + " "
|
||||
const files = ref([])
|
||||
// 插入文件
|
||||
const insertFile = (file) => {
|
||||
files.value.push(file)
|
||||
}
|
||||
const removeFile = (file) => {
|
||||
files.value = removeArrayItem(files.value, file, (v1,v2) => v1.url===v2.url)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -86,43 +86,7 @@
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<div class="running-job-box" v-if="runningJobs.length > 0">
|
||||
<div class="job-item" v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0" class="job-item-inner">
|
||||
<el-image :src="item['img_url']" fit="cover" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="progress">
|
||||
<el-progress type="circle" :percentage="item.progress" :width="100"
|
||||
color="#47fff1"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-image fit="cover" v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
<task-list :list="runningJobs" />
|
||||
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list">
|
||||
@@ -228,6 +192,7 @@ import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
@@ -369,7 +334,7 @@ const fetchRunningJobs = () => {
|
||||
return
|
||||
}
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/dall/jobs?status=0`).then(res => {
|
||||
httpGet(`/api/dall/jobs?finish=false`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
@@ -402,7 +367,7 @@ const fetchFinishJobs = () => {
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
|
||||
httpGet(`/api/dall/jobs?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
isOver.value = true
|
||||
}
|
||||
@@ -454,7 +419,7 @@ const removeImage = (event, item) => {
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
httpGet("/api/dall/remove", {id: item.id, user_id: item.user}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
@@ -477,7 +442,7 @@ const publishImage = (event, item, action) => {
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
|
||||
httpGet("/api/dall/publish", {id: item.id, action: action,user_id:item.user_id}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
|
||||
@@ -49,15 +49,15 @@
|
||||
<div v-if="!licenseConfig.de_copy">
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-book"></i>
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<a :href="docsURL" target="_blank">
|
||||
用户手册
|
||||
</a>
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<a href="https://ai.r9it.com/docs/" target="_blank">
|
||||
Geek-AI {{ version }}
|
||||
<a :href="gitURL" target="_blank">
|
||||
GeekAI {{ version }}
|
||||
</a>
|
||||
</el-dropdown-item>
|
||||
</div>
|
||||
@@ -142,7 +142,7 @@ import {checkSession} from "@/action/session";
|
||||
import {removeUserToken} from "@/store/session";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import ConfigDialog from "@/components/ConfigDialog.vue";
|
||||
import ConfigDialog from "@/components/UserInfoDialog.vue";
|
||||
import {showMessageError} from "@/utils/dialog";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -157,6 +157,8 @@ const version = ref(process.env.VUE_APP_VERSION)
|
||||
const routerViewKey = ref(0)
|
||||
const showConfigDialog = ref(false)
|
||||
const licenseConfig = ref({})
|
||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
|
||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
|
||||
|
||||
const store = useSharedStore();
|
||||
const show = ref(false)
|
||||
@@ -215,10 +217,11 @@ const init = () => {
|
||||
const logout = function () {
|
||||
httpGet('/api/user/logout').then(() => {
|
||||
removeUserToken()
|
||||
store.setShowLoginDialog(true)
|
||||
loginUser.value = {}
|
||||
// 刷新组件
|
||||
routerViewKey.value += 1
|
||||
router.push("/login")
|
||||
// store.setShowLoginDialog(true)
|
||||
// loginUser.value = {}
|
||||
// // 刷新组件
|
||||
// routerViewKey.value += 1
|
||||
}).catch(() => {
|
||||
ElMessage.error('注销失败!');
|
||||
})
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-box" @scrollend="handleScrollEnd">
|
||||
<div class="task-list-box">
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="extra-params">
|
||||
<el-form>
|
||||
@@ -450,43 +450,7 @@
|
||||
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<div class="running-job-box" v-if="runningJobs.length > 0">
|
||||
<div class="job-item" v-for="item in runningJobs">
|
||||
<div v-if="item.progress > 0" class="job-item-inner">
|
||||
<el-image :src="item['img_url']" fit="cover" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="progress">
|
||||
<el-progress type="circle" :percentage="item.progress" :width="100"
|
||||
color="#47fff1"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-image fit="cover" v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
<task-list :list="runningJobs" />
|
||||
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list">
|
||||
@@ -617,6 +581,7 @@ import {useRouter} from "vue-router";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {copyObj, removeArrayItem} from "@/utils/libs";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
const paramBoxHeight = ref(0)
|
||||
@@ -817,7 +782,7 @@ const fetchRunningJobs = () => {
|
||||
return
|
||||
}
|
||||
|
||||
httpGet(`/api/mj/jobs?status=0`).then(res => {
|
||||
httpGet(`/api/mj/jobs?finish=false`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
@@ -855,7 +820,7 @@ const fetchFinishJobs = () => {
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
// 获取已完成的任务
|
||||
httpGet(`/api/mj/jobs?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
httpGet(`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
const jobs = res.data
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i]['img_url'] !== "") {
|
||||
@@ -996,7 +961,7 @@ const removeImage = (item) => {
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
httpGet("/api/mj/remove", {id: item.id, user_id: item.user_id}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
@@ -1014,7 +979,7 @@ const publishImage = (item, action) => {
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
|
||||
httpGet("/api/mj/publish", {id: item.id, action: action,user_id: item.user_id}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="采样调度器">
|
||||
<el-form-item label="采样调度">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.scheduler" style="width:176px">
|
||||
@@ -296,39 +296,8 @@
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<div class="running-job-box" v-if="runningJobs.length > 0">
|
||||
<div class="job-item" v-for="item in runningJobs">
|
||||
<div v-if="item.progress > 0" class="job-item-inner">
|
||||
<el-image :src="item['img_url']" fit="cover" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="progress">
|
||||
<el-progress type="circle" :percentage="item.progress" :width="100"
|
||||
color="#47fff1"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-image fit="cover" v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
<task-list :list="runningJobs" />
|
||||
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list">
|
||||
<div v-if="finishedJobs.length > 0">
|
||||
@@ -506,6 +475,7 @@ import {checkSession} from "@/action/session";
|
||||
import {useRouter} from "vue-router";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
@@ -661,7 +631,7 @@ const fetchRunningJobs = () => {
|
||||
}
|
||||
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/jobs?status=0`).then(res => {
|
||||
httpGet(`/api/sd/jobs?finish=0`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
@@ -694,7 +664,7 @@ const fetchFinishJobs = () => {
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
|
||||
httpGet(`/api/sd/jobs?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
isOver.value = true
|
||||
}
|
||||
@@ -761,7 +731,7 @@ const removeImage = (event, item) => {
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
httpGet("/api/sd/remove", {id: item.id, user_id: item.user}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
@@ -780,7 +750,7 @@ const publishImage = (event, item, action) => {
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
|
||||
httpGet("/api/sd/publish", {id: item.id, action: action, user_id: item.user}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="index-page" :style="{height: winHeight+'px'}">
|
||||
<div :class="bgClass"></div>
|
||||
<div class="index-bg" :style="{backgroundImage: 'url('+bgImgUrl+')'}"></div>
|
||||
<div class="menu-box">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
@@ -12,17 +12,17 @@
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<span v-if="!licenseConfig.de_copy">
|
||||
<a href="https://ai.r9it.com/docs/install/" target="_blank">
|
||||
<a :href="docsURL" target="_blank">
|
||||
<el-button type="primary" round>
|
||||
<i class="iconfont icon-book"></i>
|
||||
<span>部署文档</span>
|
||||
<span>文档</span>
|
||||
</el-button>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<a :href="gitURL" target="_blank">
|
||||
<el-button type="success" round>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span>项目源码</span>
|
||||
<span>源码</span>
|
||||
</el-button>
|
||||
</a>
|
||||
</span>
|
||||
@@ -65,7 +65,6 @@
|
||||
|
||||
<script setup>
|
||||
|
||||
// import * as THREE from 'three';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
@@ -84,17 +83,20 @@ const title = ref("Geek-AI 创作系统")
|
||||
const logo = ref("/images/logo.png")
|
||||
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
|
||||
const licenseConfig = ref({})
|
||||
// const size = Math.max(window.innerWidth * 0.5, window.innerHeight * 0.8)
|
||||
const winHeight = window.innerHeight - 150
|
||||
const bgClass = ref('fixed-bg')
|
||||
const bgImgUrl = ref('')
|
||||
const isLogin = ref(false)
|
||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
|
||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
logo.value = res.data.logo
|
||||
if (res.data.rand_bg) {
|
||||
bgClass.value = "rand-bg"
|
||||
if (res.data.index_bg_url) {
|
||||
bgImgUrl.value = res.data.index_bg_url
|
||||
} else {
|
||||
bgImgUrl.value = "/images/index-bg.jpg"
|
||||
}
|
||||
if (res.data.slogan) {
|
||||
slogan.value = res.data.slogan
|
||||
@@ -126,24 +128,12 @@ onMounted(() => {
|
||||
align-items baseline
|
||||
padding-top 150px
|
||||
|
||||
.fixed-bg {
|
||||
.index-bg {
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100vw
|
||||
height 100vh
|
||||
background-image url("~@/assets/img/ai-bg.jpg")
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.rand-bg {
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100vw
|
||||
height 100vh
|
||||
background-image url("https://api.dujin.org/bing/1920.php")
|
||||
filter: blur(8px);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-row class="opt" :gutter="20">
|
||||
<el-row class="opt" :gutter="24">
|
||||
<el-col :span="8"><el-link type="primary" @click="router.push('/register')">注册</el-link></el-col>
|
||||
<el-col :span="8">
|
||||
<el-link type="info" @click="showResetPass = true">重置密码</el-link>
|
||||
@@ -43,6 +43,14 @@
|
||||
<el-link type="info" @click="router.push('/')">首页</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div v-if="wechatLoginURL !== ''">
|
||||
<el-divider class="divider">其他登录方式</el-divider>
|
||||
|
||||
<div class="clogin">
|
||||
<a class="wechat-login" :href="wechatLoginURL"><i class="iconfont icon-wechat"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +65,7 @@
|
||||
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {useRouter} from "vue-router";
|
||||
@@ -75,28 +83,38 @@ const password = ref(process.env.VUE_APP_PASS);
|
||||
const showResetPass = ref(false)
|
||||
const logo = ref("/images/logo.png")
|
||||
const licenseConfig = ref({})
|
||||
const wechatLoginURL = ref('')
|
||||
|
||||
// 获取系统配置
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
logo.value = res.data.logo
|
||||
title.value = res.data.title
|
||||
}).catch(e => {
|
||||
showMessageError("获取系统配置失败:" + e.message)
|
||||
})
|
||||
onMounted(() => {
|
||||
// 获取系统配置
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
logo.value = res.data.logo
|
||||
title.value = res.data.title
|
||||
}).catch(e => {
|
||||
showMessageError("获取系统配置失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/config/license").then(res => {
|
||||
licenseConfig.value = res.data
|
||||
}).catch(e => {
|
||||
showMessageError("获取 License 配置:" + e.message)
|
||||
})
|
||||
httpGet("/api/config/license").then(res => {
|
||||
licenseConfig.value = res.data
|
||||
}).catch(e => {
|
||||
showMessageError("获取 License 配置:" + e.message)
|
||||
})
|
||||
|
||||
checkSession().then(() => {
|
||||
if (isMobile()) {
|
||||
router.push('/mobile')
|
||||
} else {
|
||||
router.push('/chat')
|
||||
}
|
||||
}).catch(() => {
|
||||
checkSession().then(() => {
|
||||
if (isMobile()) {
|
||||
router.push('/mobile')
|
||||
} else {
|
||||
router.push('/chat')
|
||||
}
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
const returnURL = `${location.protocol}//${location.host}/login/callback`
|
||||
httpGet("/api/user/clogin?return_url="+returnURL).then(res => {
|
||||
wechatLoginURL.value = res.data.url
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
@@ -114,7 +132,7 @@ const login = function () {
|
||||
}
|
||||
|
||||
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
||||
setUserToken(res.data)
|
||||
setUserToken(res.data.token)
|
||||
if (isMobile()) {
|
||||
router.push('/mobile')
|
||||
} else {
|
||||
@@ -129,99 +147,5 @@ const login = function () {
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.bg {
|
||||
position fixed
|
||||
left 0
|
||||
right 0
|
||||
top 0
|
||||
bottom 0
|
||||
background-color #313237
|
||||
background-image url("~@/assets/img/login-bg.jpg")
|
||||
background-size cover
|
||||
background-position center
|
||||
background-repeat repeat-y
|
||||
//filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
|
||||
}
|
||||
|
||||
.main {
|
||||
.contain {
|
||||
position fixed
|
||||
left 50%
|
||||
top 40%
|
||||
width 90%
|
||||
max-width 400px;
|
||||
transform translate(-50%, -50%)
|
||||
padding 20px 10px;
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
|
||||
.logo {
|
||||
text-align center
|
||||
|
||||
.el-image {
|
||||
width 120px;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 24px
|
||||
font-size 24px
|
||||
color $white_v1
|
||||
letter-space 2px
|
||||
text-align center
|
||||
padding-top 10px
|
||||
}
|
||||
|
||||
.content {
|
||||
width 100%
|
||||
height: auto
|
||||
border-radius 3px
|
||||
|
||||
.block {
|
||||
margin-bottom 16px
|
||||
|
||||
.el-input__inner {
|
||||
border 1px solid $gray-v6 !important
|
||||
|
||||
.el-icon-user, .el-icon-lock {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
padding-top 10px;
|
||||
|
||||
.login-btn {
|
||||
width 100%
|
||||
font-size 16px
|
||||
letter-spacing 2px
|
||||
}
|
||||
}
|
||||
|
||||
.text-line {
|
||||
justify-content center
|
||||
padding-top 10px;
|
||||
font-size 14px;
|
||||
}
|
||||
|
||||
.opt {
|
||||
padding 15px
|
||||
.el-col {
|
||||
text-align center
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
color #ffffff;
|
||||
|
||||
.container {
|
||||
padding 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "@/assets/css/login.styl"
|
||||
</style>
|
||||
105
web/src/views/LoginCallback.vue
Normal file
105
web/src/views/LoginCallback.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="login-callback"
|
||||
v-loading="loading"
|
||||
element-loading-text="正在同步登录信息..."
|
||||
:style="{ height: winHeight + 'px' }">
|
||||
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
style="width: 360px;"
|
||||
>
|
||||
<el-result
|
||||
icon="success"
|
||||
title="登录成功"
|
||||
style="--el-result-padding:10px"
|
||||
>
|
||||
<template #sub-title>
|
||||
<div class="user-info">
|
||||
<div class="line">您的初始账户信息如下:</div>
|
||||
<div class="line"><span>用户名:</span>{{username}}</div>
|
||||
<div class="line"><span>密码:</span>{{password}}</div>
|
||||
<div class="line">您后期也可以通过此账号和密码登录</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="finishLogin">我知道了</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue"
|
||||
import {useRouter} from "vue-router"
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {setUserToken} from "@/store/session";
|
||||
import {isMobile} from "@/utils/libs";
|
||||
|
||||
const winHeight = ref(window.innerHeight)
|
||||
const loading = ref(true)
|
||||
const router = useRouter()
|
||||
const show = ref(false)
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
|
||||
|
||||
const code = router.currentRoute.value.query.code
|
||||
if (code === "") {
|
||||
ElMessage.error({message: "登录失败:code 参数不能为空",duration: 2000, onClose: () => router.push("/")})
|
||||
} else {
|
||||
// 发送请求获取用户信息
|
||||
httpGet("/api/user/clogin/callback",{login_type: "wx",code: code}).then(res => {
|
||||
setUserToken(res.data.token)
|
||||
if (res.data.username) {
|
||||
username.value = res.data.username
|
||||
password.value = res.data.password
|
||||
show.value = true
|
||||
loading.value = false
|
||||
} else {
|
||||
finishLogin()
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessageBox.alert(e.message, {
|
||||
confirmButtonText: '重新登录',
|
||||
type:"error",
|
||||
title:"登录失败",
|
||||
callback: () => {
|
||||
router.push("/login")
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
const finishLogin = () => {
|
||||
if (isMobile()) {
|
||||
router.push('/mobile')
|
||||
} else {
|
||||
router.push('/chat')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.login-callback {
|
||||
.user-info {
|
||||
display flex
|
||||
flex-direction column
|
||||
padding 10px
|
||||
border 1px dashed #e1e1e1
|
||||
border-radius 10px
|
||||
.line {
|
||||
text-align left
|
||||
font-size 14px
|
||||
line-height 1.5
|
||||
|
||||
span{
|
||||
font-weight bold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -333,7 +333,7 @@ const wechatPay = (row) => {
|
||||
}
|
||||
|
||||
const queryOrder = (orderNo) => {
|
||||
httpPost("/api/payment/query", {order_no: orderNo}).then(res => {
|
||||
httpGet("/api/order/query", {order_no: orderNo}).then(res => {
|
||||
if (res.data.status === 1) {
|
||||
text.value = "扫码成功,请在手机上进行支付!"
|
||||
queryOrder(orderNo)
|
||||
|
||||
@@ -178,7 +178,7 @@ const tableData = ref([])
|
||||
const sortedTableData = ref([])
|
||||
const role = ref({context: []})
|
||||
const formRef = ref(null)
|
||||
const optTitle = ref({})
|
||||
const optTitle = ref("")
|
||||
const loading = ref(true)
|
||||
|
||||
const rules = reactive({
|
||||
@@ -231,7 +231,7 @@ const fetchData = () => {
|
||||
sortedData.forEach((id, index) => {
|
||||
ids.push(parseInt(id))
|
||||
sorts.push(index+1)
|
||||
items.value[index].sort_num = index + 1
|
||||
tableData.value[index].sort_num = index + 1
|
||||
})
|
||||
|
||||
httpPost("/api/admin/role/sort", {ids: ids, sorts: sorts}).catch(e => {
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
v-model="showChatItemDialog"
|
||||
title="对话详情"
|
||||
>
|
||||
<div class="chat-box common-layout">
|
||||
<div class="chat-box chat-page">
|
||||
<div v-for="item in messages" :key="item.id">
|
||||
<chat-prompt
|
||||
v-if="item.type==='prompt'"
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="绑定API-KEY:" prop="apikey">
|
||||
<el-select v-model="item.key_id" placeholder="请选择 API KEY" clearable>
|
||||
<el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
|
||||
<el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
|
||||
{{ v.name }}
|
||||
<el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
|
||||
@@ -229,7 +229,7 @@ const platforms = ref([])
|
||||
|
||||
// 获取 API KEY
|
||||
const apiKeys = ref([])
|
||||
httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
|
||||
httpGet('/api/admin/apikey/list?type=chat').then(res => {
|
||||
apiKeys.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取 API KEY 失败:" + e.message)
|
||||
|
||||
@@ -33,32 +33,42 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="开放注册" prop="enabled_register">
|
||||
<el-switch v-model="system['enabled_register']"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="关闭注册之后只能通过管理后台添加用户"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-form-item label="首页背景图" prop="logo">
|
||||
<div class="tip-input">
|
||||
<el-input v-model="system['index_bg_url']" placeholder="网站首页背景图片">
|
||||
<template #append>
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
@click="beforeUpload('index_bg_url')"
|
||||
:http-request="uploadImg"
|
||||
>
|
||||
<el-icon class="uploader-icon">
|
||||
<UploadFilled/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="system.index_bg_url = 'https://api.dujin.org/bing/1920.php'">使用动态背景</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="动态背景">
|
||||
<el-switch v-model="system['rand_bg']"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="打开之后前端首页将使用随机壁纸作为背景图"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-form-item label="开放注册" prop="enabled_register">
|
||||
<div class="tip-input">
|
||||
<el-switch v-model="system['enabled_register']"/>
|
||||
<div class="info">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="关闭注册之后只能通过管理后台添加用户"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="注册方式" prop="register_ways">
|
||||
@@ -211,17 +221,21 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="众筹支付">
|
||||
<el-form-item label="启用众筹功能" prop="enabled_reward">
|
||||
<el-switch v-model="system['enabled_reward']"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="如果关闭次功能将不在用户菜单显示众筹二维码"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<div class="tip-input">
|
||||
<el-switch v-model="system['enabled_reward']"/>
|
||||
<div class="info">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="如果关闭次功能将不在用户菜单显示众筹二维码"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="system['enabled_reward']">
|
||||
@@ -534,7 +548,6 @@ const onUploadImg = (files, callback) => {
|
||||
|
||||
.el-icon {
|
||||
font-size 16px
|
||||
margin-left 10px
|
||||
cursor pointer
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ const editChat = (row) => {
|
||||
tmpChatTitle.value = row.title
|
||||
}
|
||||
const saveTitle = () => {
|
||||
httpPost('/api/chat/update', {id: item.value.id, title: tmpChatTitle.value}).then(() => {
|
||||
httpPost('/api/chat/update', {chat_id: item.value.chat_id, title: tmpChatTitle.value}).then(() => {
|
||||
showSuccessToast("操作成功!");
|
||||
item.value.title = tmpChatTitle.value;
|
||||
}).catch(e => {
|
||||
|
||||
@@ -1,637 +0,0 @@
|
||||
<template>
|
||||
<div class="mobile-mj">
|
||||
<van-form @submit="generate">
|
||||
<div class="text-line">图片比例</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="4" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.rate ? 'rate active' : 'rate'"
|
||||
@click="changeRate(item)">
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">模型选择</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8" v-for="item in models" :key="item.value">
|
||||
<div :class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)">
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">
|
||||
<van-text-ellipsis :content="item.text"/>
|
||||
</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="创意度">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.chaos" :max="100" :step="1"
|
||||
@update:model-value="showToast('当前值:' + params.chaos)"/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="风格化">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.stylize" :max="1000" :step="1"
|
||||
@update:model-value="showToast('当前值:' + params.stylize)"/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="原始模式">
|
||||
<template #input>
|
||||
<van-switch v-model="params.raw"/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-tabs v-model:active="activeName" @change="tabChange" animated>
|
||||
<van-tab title="文生图" name="txt2img">
|
||||
<div class="text-line">
|
||||
<van-field v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="图生图" name="img2img">
|
||||
<div class="text-line">
|
||||
<van-field v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg"/>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="垫图权重">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.iw" :max="1" :step="0.01"
|
||||
@update:model-value="showToast('当前值:' + params.iw)"/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="tip-text">提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。</div>
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.cref"
|
||||
center
|
||||
clearable
|
||||
label="角色一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus"/>
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.sref"
|
||||
center
|
||||
clearable
|
||||
label="风格一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus"/>
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="一致性权重">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.cw" :max="100" :step="1"
|
||||
@update:model-value="showToast('当前值:' + params.cw)"/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="融图" name="blend">
|
||||
<div class="tip-text">请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg"/>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="换脸" name="swapFace">
|
||||
<div class="tip-text">请上传两张有脸部的图片,用左边图片的脸替换右边图片的脸。</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg"/>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<el-tag>绘图消耗{{ mjPower }}算力,U/V 操作消耗{{ mjActionPower }}算力,当前算力:{{ power }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-button round block type="primary" native-type="submit">
|
||||
立即生成
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
<h3>任务列表</h3>
|
||||
<div class="running-job-list">
|
||||
<van-empty v-if="runningJobs.length ===0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image :src="item['img_url']">
|
||||
<template v-slot:error>加载失败</template>
|
||||
</van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress+'%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<h3>创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty v-if="finishedJobs.length ===0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs">
|
||||
<div class="job-item">
|
||||
<van-image
|
||||
:src="item['thumb_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover">
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20"/>
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="opt" v-if="item['can_opt']">
|
||||
|
||||
<van-grid :gutter="3" :column-num="4">
|
||||
<van-grid-item><a @click="upscale(1, item)" class="opt-btn">U1</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle/>
|
||||
<el-button type="warning" v-if="item.publish" @click="publishImage(item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, onUnmounted, ref} from "vue";
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showFailToast,
|
||||
showNotify,
|
||||
showToast,
|
||||
showDialog,
|
||||
showImagePreview,
|
||||
showSuccessToast
|
||||
} from "vant";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import Compressor from "compressorjs";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useRouter} from "vue-router";
|
||||
import {Delete} from "@element-plus/icons-vue";
|
||||
|
||||
const activeColspan = ref([""])
|
||||
|
||||
const rates = [
|
||||
{css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png"},
|
||||
{css: "size2-3", value: "2:3", text: "2:3", img: "/images/mj/rate_3_4.png"},
|
||||
{css: "size3-4", value: "3:4", text: "3:4", img: "/images/mj/rate_3_4.png"},
|
||||
{css: "size4-3", value: "4:3", text: "4:3", img: "/images/mj/rate_4_3.png"},
|
||||
{css: "size16-9", value: "16:9", text: "16:9", img: "/images/mj/rate_16_9.png"},
|
||||
{css: "size9-16", value: "9:16", text: "9:16", img: "/images/mj/rate_9_16.png"},
|
||||
]
|
||||
const models = [
|
||||
{text: "MJ-6.0", value: " --v 6", img: "/images/mj/mj-v6.png"},
|
||||
{text: "MJ-5.2", value: " --v 5.2", img: "/images/mj/mj-v5.2.png"},
|
||||
{text: "Niji5", value: " --niji 5", img: "/images/mj/mj-niji.png"},
|
||||
{text: "Niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg"},
|
||||
{text: "Niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg"},
|
||||
{text: "Niji6", value: " --niji 6", img: "/images/mj/nj3.jpg"},
|
||||
]
|
||||
const imgList = ref([])
|
||||
const params = ref({
|
||||
task_type: "image",
|
||||
rate: rates[0].value,
|
||||
model: models[0].value,
|
||||
chaos: 0,
|
||||
stylize: 0,
|
||||
seed: 0,
|
||||
img_arr: [],
|
||||
raw: false,
|
||||
iw: 0,
|
||||
prompt: "",
|
||||
neg_prompt: "",
|
||||
tile: false,
|
||||
quality: 0,
|
||||
cref: "",
|
||||
sref: "",
|
||||
cw: 0,
|
||||
})
|
||||
const userId = ref(0)
|
||||
const router = useRouter()
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const socket = ref(null)
|
||||
const power = ref(0)
|
||||
const activeName = ref("txt2img")
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(user => {
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
connect()
|
||||
|
||||
}).catch(() => {
|
||||
router.push('/login')
|
||||
});
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socket.value = null
|
||||
})
|
||||
|
||||
const mjPower = ref(1)
|
||||
const mjActionPower = ref(1)
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
mjPower.value = res.data["mj_power"]
|
||||
mjActionPower.value = res.data["mj_action_power"]
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
|
||||
})
|
||||
|
||||
const heartbeatHandle = ref(null)
|
||||
const connect = () => {
|
||||
let host = process.env.VUE_APP_WS_HOST
|
||||
if (host === '') {
|
||||
if (location.protocol === 'https:') {
|
||||
host = 'wss://' + location.host;
|
||||
} else {
|
||||
host = 'ws://' + location.host;
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳函数
|
||||
const sendHeartbeat = () => {
|
||||
clearTimeout(heartbeatHandle.value)
|
||||
new Promise((resolve, reject) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
|
||||
}
|
||||
resolve("success")
|
||||
}).then(() => {
|
||||
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
|
||||
});
|
||||
}
|
||||
|
||||
const _socket = new WebSocket(host + `/api/mj/client?user_id=${userId.value}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
socket.value = _socket;
|
||||
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取运行中的任务
|
||||
const fetchRunningJobs = (userId) => {
|
||||
httpGet(`/api/mj/jobs?status=0&user_id=${userId}`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
showNotify({
|
||||
message: `任务执行失败:${jobs[i]['err_msg']}`,
|
||||
type: 'danger',
|
||||
})
|
||||
if (jobs[i].type === 'image') {
|
||||
power.value += mjPower.value
|
||||
} else {
|
||||
power.value += mjActionPower.value
|
||||
}
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
runningJobs.value = _jobs
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "获取任务失败:" + e.message})
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref(false)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true
|
||||
// 获取已完成的任务
|
||||
httpGet(`/api/mj/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
const jobs = res.data
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
showNotify({
|
||||
message: `任务ID:${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
|
||||
type: 'danger',
|
||||
})
|
||||
if (jobs[i].type === 'image') {
|
||||
power.value += mjPower.value
|
||||
} else {
|
||||
power.value += mjActionPower.value
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (jobs[i]['use_proxy']) {
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?x-oss-process=image/quality,q_60&format=webp'
|
||||
} else {
|
||||
if (jobs[i].type === 'upscale' || jobs[i].type === 'swapFace') {
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
|
||||
} else {
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/480/q/75'
|
||||
}
|
||||
}
|
||||
|
||||
if (jobs[i].type === 'image' || jobs[i].type === 'variation') {
|
||||
jobs[i]['can_opt'] = true
|
||||
}
|
||||
}
|
||||
if (jobs.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
if (page === 1) {
|
||||
finishedJobs.value = jobs
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(jobs)
|
||||
}
|
||||
nextTick(() => loading.value = false)
|
||||
}).catch(e => {
|
||||
loading.value = false
|
||||
error.value = true
|
||||
showFailToast("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
};
|
||||
|
||||
// 切换图片比例
|
||||
const changeRate = (item) => {
|
||||
params.value.rate = item.value
|
||||
}
|
||||
// 切换模型
|
||||
const changeModel = (item) => {
|
||||
params.value.model = item.value
|
||||
}
|
||||
|
||||
const imgKey = ref("")
|
||||
const beforeUpload = (key) => {
|
||||
imgKey.value = key
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
file.status = "uploading"
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then(res => {
|
||||
file.url = res.data.url
|
||||
if (imgKey.value !== "") { // 单张图片上传
|
||||
params.value[imgKey.value] = res.data.url
|
||||
imgKey.value = ''
|
||||
}
|
||||
file.status = "done"
|
||||
}).catch(e => {
|
||||
file.status = 'failed'
|
||||
file.message = '上传失败'
|
||||
showFailToast("图片上传失败:" + e.message)
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const send = (url, index, item) => {
|
||||
httpPost(url, {
|
||||
index: index,
|
||||
channel_id: item.channel_id,
|
||||
message_id: item.message_id,
|
||||
message_hash: item.hash,
|
||||
session_id: getSessionId(),
|
||||
prompt: item.prompt,
|
||||
}).then(() => {
|
||||
showSuccessToast("任务推送成功,请耐心等待任务执行...")
|
||||
power.value -= mjActionPower.value
|
||||
}).catch(e => {
|
||||
showFailToast("任务推送失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 图片放大任务
|
||||
const upscale = (index, item) => {
|
||||
send('/api/mj/upscale', index, item)
|
||||
}
|
||||
|
||||
// 图片变换任务
|
||||
const variation = (index, item) => {
|
||||
send('/api/mj/variation', index, item)
|
||||
}
|
||||
|
||||
const generate = () => {
|
||||
if (params.value.prompt === '' && params.value.task_type === "image") {
|
||||
return showFailToast("请输入绘画提示词!")
|
||||
}
|
||||
if (params.value.model.indexOf("niji") !== -1 && params.value.raw) {
|
||||
return showFailToast("动漫模型不允许启用原始模式")
|
||||
}
|
||||
params.value.session_id = getSessionId()
|
||||
params.value.img_arr = imgList.value.map(img => img.url)
|
||||
httpPost("/api/mj/image", params.value).then(() => {
|
||||
showToast("绘画任务推送成功,请耐心等待任务执行")
|
||||
power.value -= mjPower.value
|
||||
}).catch(e => {
|
||||
showFailToast("任务推送失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (item) => {
|
||||
showConfirmDialog({
|
||||
title: '标题',
|
||||
message:
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
}).then(() => {
|
||||
httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
showSuccessToast("任务删除成功")
|
||||
}).catch(e => {
|
||||
showFailToast("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
showToast("您取消了操作")
|
||||
});
|
||||
}
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (item, action) => {
|
||||
let text = "图片发布"
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
|
||||
showSuccessToast(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
showFailToast(text + "失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showPrompt = (item) => {
|
||||
showDialog({
|
||||
title: "绘画提示词",
|
||||
message: item.prompt,
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
}
|
||||
|
||||
const imageView = (item) => {
|
||||
showImagePreview([item['img_url']]);
|
||||
}
|
||||
|
||||
// 切换菜单
|
||||
const tabChange = (tab) => {
|
||||
if (tab === "txt2img" || tab === "img2img") {
|
||||
params.value.task_type = "image"
|
||||
} else {
|
||||
params.value.task_type = tab
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mobile/image-mj.styl"
|
||||
</style>
|
||||
@@ -1,523 +0,0 @@
|
||||
<template>
|
||||
<div class="mobile-sd">
|
||||
<van-form @submit="generate">
|
||||
<van-cell-group inset>
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.sampler"
|
||||
is-link
|
||||
readonly
|
||||
label="采样方法"
|
||||
placeholder="选择采样方法"
|
||||
@click="showSamplerPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="samplers"
|
||||
@cancel="showSamplerPicker = false"
|
||||
@confirm="samplerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field label="图片尺寸">
|
||||
<template #input>
|
||||
<van-row gutter="20">
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.width" size="small" placeholder="宽"/>
|
||||
</van-col>
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.height" size="small" placeholder="高"/>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field v-model.number="params.steps" label="迭代步数"
|
||||
placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o"
|
||||
@click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o"
|
||||
@click="showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')"/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o"
|
||||
@click="showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')"/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field label="高清修复">
|
||||
<template #input>
|
||||
<van-switch v-model="params.hd_fix"/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<div v-if="params.hd_fix">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.hd_scale_alg"
|
||||
is-link
|
||||
readonly
|
||||
label="放大算法"
|
||||
placeholder="选择放大算法"
|
||||
@click="showUpscalePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="upscaleAlgArr"
|
||||
@cancel="showUpscalePicker = false"
|
||||
@confirm="upscaleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field v-model.number="params.hd_scale" label="放大倍数"/>
|
||||
<van-field v-model.number="params.hd_steps" label="迭代步数"/>
|
||||
|
||||
<van-field label="重绘幅度">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1"
|
||||
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"/>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o"
|
||||
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
|
||||
<div class="text-line pt-6">
|
||||
<el-tag>绘图消耗{{ sdPower }}算力,当前算力:{{ power }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-button round block type="primary" native-type="submit">
|
||||
立即生成
|
||||
</van-button>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3>任务列表</h3>
|
||||
<div class="running-job-list">
|
||||
<van-empty v-if="runningJobs.length ===0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image :src="item['img_url']">
|
||||
<template v-slot:error>加载失败</template>
|
||||
</van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress+'%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<h3>创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty v-if="finishedJobs.length ===0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs">
|
||||
<div class="job-item">
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover">
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20"/>
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle/>
|
||||
<el-button type="warning" v-if="item.publish" @click="publishImage($event,item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showTask(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue"
|
||||
import {Delete} from "@element-plus/icons-vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useRouter} from "vue-router";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {
|
||||
showConfirmDialog, showDialog,
|
||||
showFailToast,
|
||||
showImagePreview,
|
||||
showNotify,
|
||||
showSuccessToast,
|
||||
showToast
|
||||
} from "vant";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
const activeColspan = ref([""])
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const samplers = ref([
|
||||
{text: "Euler a", value: "Euler a"},
|
||||
{text: "DPM++ 2S a Karras", value: "DPM++ 2S a Karras"},
|
||||
{text: "DPM++ 2M Karras", value: "DPM++ 2M Karras"},
|
||||
{text: "DPM++ 2M SDE Karras", value: "DPM++ 2M SDE Karras"},
|
||||
{text: "DPM++ 2M Karras", value: "DPM++ 2M Karras"},
|
||||
{text: "DPM++ 3M SDE Karras", value: "DPM++ 3M SDE Karras"},
|
||||
])
|
||||
const showSamplerPicker = ref(false)
|
||||
|
||||
const upscaleAlgArr = ref([
|
||||
{text: "Latent", value: "Latent"},
|
||||
{text: "ESRGAN_4x", value: "ESRGAN_4x"},
|
||||
{text: "ESRGAN 4x+", value: "ESRGAN 4x+"},
|
||||
{text: "SwinIR_4x", value: "SwinIR_4x"},
|
||||
{text: "LDSR", value: "LDSR"},
|
||||
])
|
||||
const showUpscalePicker = ref(false)
|
||||
|
||||
const params = ref({
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
sampler: samplers.value[0].value,
|
||||
seed: -1,
|
||||
steps: 20,
|
||||
cfg_scale: 7,
|
||||
hd_fix: false,
|
||||
hd_redraw_rate: 0.7,
|
||||
hd_scale: 2,
|
||||
hd_scale_alg: upscaleAlgArr.value[0].value,
|
||||
hd_steps: 0,
|
||||
prompt: "",
|
||||
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
|
||||
})
|
||||
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const router = useRouter()
|
||||
// 检查是否有画同款的参数
|
||||
const _params = router.currentRoute.value.params["copyParams"]
|
||||
if (_params) {
|
||||
params.value = JSON.parse(_params)
|
||||
}
|
||||
const power = ref(0)
|
||||
const sdPower = ref(0) // 画一张 SD 图片消耗算力
|
||||
|
||||
const socket = ref(null)
|
||||
const userId = ref(0)
|
||||
const heartbeatHandle = ref(null)
|
||||
const connect = () => {
|
||||
let host = process.env.VUE_APP_WS_HOST
|
||||
if (host === '') {
|
||||
if (location.protocol === 'https:') {
|
||||
host = 'wss://' + location.host;
|
||||
} else {
|
||||
host = 'ws://' + location.host;
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳函数
|
||||
const sendHeartbeat = () => {
|
||||
clearTimeout(heartbeatHandle.value)
|
||||
new Promise((resolve, reject) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
|
||||
}
|
||||
resolve("success")
|
||||
}).then(() => {
|
||||
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
|
||||
});
|
||||
}
|
||||
|
||||
const _socket = new WebSocket(host + `/api/sd/client?user_id=${userId.value}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
socket.value = _socket;
|
||||
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
finished.value = false
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt-sd');
|
||||
clipboard.value.on('success', () => {
|
||||
showNotify({type: "success", message: "复制成功!"});
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
showNotify({type: "danger", message: '复制失败!'});
|
||||
})
|
||||
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
sdPower.value = res.data["sd_power"]
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
socket.value = null
|
||||
})
|
||||
|
||||
|
||||
const initData = () => {
|
||||
checkSession().then(user => {
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
connect()
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
});
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/jobs?status=0`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
showNotify({
|
||||
message: `任务ID:${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
|
||||
type: 'danger',
|
||||
})
|
||||
power.value += sdPower.value
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
runningJobs.value = _jobs
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "获取任务失败:" + e.message})
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref(false)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(10)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true
|
||||
httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
if (page === 1) {
|
||||
finishedJobs.value = res.data
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(res.data)
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
loading.value = false
|
||||
showNotify({type: "danger", message: "获取任务失败:" + e.message})
|
||||
})
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return showToast("请输入绘画提示词!")
|
||||
}
|
||||
|
||||
if (!isLogin.value) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (params.value.seed === '') {
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost("/api/sd/image", params.value).then(() => {
|
||||
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...")
|
||||
power.value -= sdPower.value
|
||||
}).catch(e => {
|
||||
showFailToast("任务推送失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showTask = (row) => {
|
||||
item.value = row
|
||||
showTaskDialog.value = true
|
||||
}
|
||||
|
||||
const copyParams = (row) => {
|
||||
params.value = row.params
|
||||
showTaskDialog.value = false
|
||||
}
|
||||
|
||||
const removeImage = (event, item) => {
|
||||
event.stopPropagation()
|
||||
showConfirmDialog({
|
||||
title: '标题',
|
||||
message:
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
}).then(() => {
|
||||
httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
showSuccessToast("任务删除成功")
|
||||
}).catch(e => {
|
||||
showFailToast("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
showToast("您取消了操作")
|
||||
});
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (event, item, action) => {
|
||||
event.stopPropagation()
|
||||
let text = "图片发布"
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
|
||||
showSuccessToast(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
showFailToast(text + "失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const imageView = (item) => {
|
||||
showImagePreview([item['img_url']]);
|
||||
}
|
||||
|
||||
|
||||
const samplerConfirm = (item) => {
|
||||
params.value.sampler = item.selectedOptions[0].text;
|
||||
showSamplerPicker.value = false
|
||||
}
|
||||
|
||||
const upscaleConfirm = (item) => {
|
||||
params.value.hd_scale_alg = item.selectedOptions[0].text;
|
||||
showUpscalePicker.value = false
|
||||
}
|
||||
|
||||
const showInfo = (message) => {
|
||||
showDialog({
|
||||
title: "参数说明",
|
||||
message: message,
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mobile/image-sd.styl"
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div class="img-wall container">
|
||||
<van-nav-bar :title="title"/>
|
||||
|
||||
<div class="content">
|
||||
<van-tabs v-model:active="activeName">
|
||||
<van-tab title="MidJourney" name="mj">
|
||||
<van-list
|
||||
v-model:error="data['mj'].error"
|
||||
v-model:loading="data['mj'].loading"
|
||||
:finished="data['mj'].finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
style="height: 100%;width: 100%;"
|
||||
>
|
||||
<van-cell v-for="item in data['mj'].data" :key="item.id">
|
||||
<van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</van-tab>
|
||||
<van-tab title="StableDiffusion" name="sd">
|
||||
<van-list
|
||||
v-model:error="data['sd'].error"
|
||||
v-model:loading="data['sd'].loading"
|
||||
:finished="data['sd'].finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-cell v-for="item in data['sd'].data" :key="item.id">
|
||||
<van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</van-tab>
|
||||
<van-tab title="DALLE3" name="dalle3">
|
||||
<van-empty description="功能正在开发中"/>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {showDialog, showFailToast, showSuccessToast} from "vant";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const title = ref('图片创作广场')
|
||||
const activeName = ref("mj")
|
||||
const data = ref({
|
||||
"mj": {
|
||||
loading: false,
|
||||
finished: false,
|
||||
error: false,
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
url: "/api/mj/jobs",
|
||||
data: []
|
||||
},
|
||||
"sd": {
|
||||
loading: false,
|
||||
finished: false,
|
||||
error: false,
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
url: "/api/sd/jobs",
|
||||
data: []
|
||||
},
|
||||
"dalle3": {
|
||||
loading: false,
|
||||
finished: false,
|
||||
error: false,
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
url: "/api/dalle3/jobs",
|
||||
data: []
|
||||
}
|
||||
})
|
||||
|
||||
const onLoad = () => {
|
||||
const d = data.value[activeName.value]
|
||||
httpGet(`${d.url}?status=1&page=${d.page}&page_size=${d.pageSize}&publish=true`).then(res => {
|
||||
d.loading = false
|
||||
if (res.data.length === 0) {
|
||||
d.finished = true
|
||||
return
|
||||
}
|
||||
|
||||
// 生成缩略图
|
||||
const imageList = res.data
|
||||
for (let i = 0; i < imageList.length; i++) {
|
||||
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"
|
||||
}
|
||||
if (imageList.length < d.pageSize) {
|
||||
d.finished = true
|
||||
}
|
||||
if (d.data.length === 0) {
|
||||
d.data = imageList
|
||||
} else {
|
||||
d.data = d.data.concat(imageList)
|
||||
}
|
||||
d.page += 1
|
||||
}).catch(() => {
|
||||
d.error = true
|
||||
showFailToast("加载图片数据失败")
|
||||
})
|
||||
};
|
||||
|
||||
const showPrompt = (item) => {
|
||||
showDialog({
|
||||
title: "绘画提示词",
|
||||
message: item.prompt,
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.img-wall {
|
||||
.content {
|
||||
padding-top 60px
|
||||
|
||||
.van-cell__value {
|
||||
.van-image {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -302,7 +302,7 @@ const pay = (payWay, item) => {
|
||||
if (isWeChatBrowser() && payWay === 'wechat') {
|
||||
showFailToast("请在系统自带浏览器打开支付页面,或者在 PC 端进行扫码支付")
|
||||
} else {
|
||||
location.href = res.data
|
||||
location.href = res.data.url
|
||||
}
|
||||
}).catch(e => {
|
||||
showFailToast("生成支付订单失败:" + e.message)
|
||||
|
||||
@@ -317,7 +317,7 @@ const initData = () => {
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/dall/jobs?status=0`).then(res => {
|
||||
httpGet(`/api/dall/jobs?finish=0`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
@@ -345,7 +345,7 @@ const pageSize = ref(10)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true
|
||||
httpGet(`/api/dall/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
httpGet(`/api/dall/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
@@ -410,7 +410,7 @@ const removeImage = (event, item) => {
|
||||
message:
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
}).then(() => {
|
||||
httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
httpGet("/api/dall/remove", {id: item.id, user_id: item.user_id}).then(() => {
|
||||
showSuccessToast("任务删除成功")
|
||||
}).catch(e => {
|
||||
showFailToast("任务删除失败:" + e.message)
|
||||
@@ -427,7 +427,7 @@ const publishImage = (event, item, action) => {
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
|
||||
httpGet("/api/dall/publish", {id: item.id, action: action, user_id: item.user_id}).then(() => {
|
||||
showSuccessToast(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
|
||||
@@ -421,7 +421,7 @@ const connect = () => {
|
||||
|
||||
// 获取运行中的任务
|
||||
const fetchRunningJobs = (userId) => {
|
||||
httpGet(`/api/mj/jobs?status=0&user_id=${userId}`).then(res => {
|
||||
httpGet(`/api/mj/jobs?finish=0&user_id=${userId}`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
@@ -453,7 +453,7 @@ const pageSize = ref(10)
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true
|
||||
// 获取已完成的任务
|
||||
httpGet(`/api/mj/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
const jobs = res.data
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
@@ -600,7 +600,7 @@ const removeImage = (item) => {
|
||||
message:
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
}).then(() => {
|
||||
httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
httpGet("/api/mj/remove", {id: item.id, user_id: item.user_id}).then(() => {
|
||||
showSuccessToast("任务删除成功")
|
||||
}).catch(e => {
|
||||
showFailToast("任务删除失败:" + e.message)
|
||||
@@ -615,7 +615,7 @@ const publishImage = (item, action) => {
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
|
||||
httpGet("/api/mj/publish", {id: item.id, action: action,user_id: item.user_id}).then(() => {
|
||||
showSuccessToast(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
|
||||
@@ -381,7 +381,7 @@ const initData = () => {
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/jobs?status=0`).then(res => {
|
||||
httpGet(`/api/sd/jobs?finish=0`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
@@ -409,7 +409,7 @@ const pageSize = ref(10)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true
|
||||
httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
httpGet(`/api/sd/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
@@ -474,7 +474,7 @@ const removeImage = (event, item) => {
|
||||
message:
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
}).then(() => {
|
||||
httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
httpGet("/api/sd/remove", {id: item.id, user_id: item.user}).then(() => {
|
||||
showSuccessToast("任务删除成功")
|
||||
}).catch(e => {
|
||||
showFailToast("任务删除失败:" + e.message)
|
||||
@@ -491,7 +491,7 @@ const publishImage = (event, item, action) => {
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
|
||||
httpGet("/api/sd/publish", {id: item.id, action: action, user_id: item.user}).then(() => {
|
||||
showSuccessToast(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
|
||||
Reference in New Issue
Block a user