mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-12 04:03:42 +08:00
restore new ui files
This commit is contained in:
201
new-ui/projects/web/src/App.vue
Normal file
201
new-ui/projects/web/src/App.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view/>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ElConfigProvider} from 'element-plus';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="stylus">
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.el-overlay-dialog {
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
overflow hidden
|
||||
|
||||
.el-dialog {
|
||||
margin 0;
|
||||
|
||||
.el-dialog__body {
|
||||
max-height 90vh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 省略显示 */
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sl {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sl3 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sl4 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 居中布局 */
|
||||
.auto_center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.h_center {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
-webkit-transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.w_center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
-webkit-transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* flex布局 */
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.justify-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.items-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.self-start {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.self-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.self-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.self-baseline {
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.self-stretch {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.flex-auto {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.grow-0 {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.shrink-1 {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
21
new-ui/projects/web/src/action/session.js
Normal file
21
new-ui/projects/web/src/action/session.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import {httpGet} from "@/utils/http";
|
||||
|
||||
export function checkSession() {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/user/session').then(res => {
|
||||
resolve(res.data)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function checkAdminSession() {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/admin/session').then(res => {
|
||||
resolve(res)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
10
new-ui/projects/web/src/assets/css/admin/form.css
Normal file
10
new-ui/projects/web/src/assets/css/admin/form.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.el-form-item__content .tip-input {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.el-form-item__content .tip-input .input {
|
||||
width: 100%;
|
||||
}
|
||||
.el-form-item__content .tip-input .info {
|
||||
margin-left: 6px;
|
||||
}
|
||||
15
new-ui/projects/web/src/assets/css/admin/form.styl
Normal file
15
new-ui/projects/web/src/assets/css/admin/form.styl
Normal file
@@ -0,0 +1,15 @@
|
||||
.el-form-item__content {
|
||||
.tip-input {
|
||||
display flex
|
||||
width 100%
|
||||
|
||||
.input {
|
||||
width 100%
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left 6px
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
56
new-ui/projects/web/src/assets/css/chat-app.css
Normal file
56
new-ui/projects/web/src/assets/css/chat-app.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.page-apps {
|
||||
background-color: #282c34;
|
||||
height: 100vh;
|
||||
}
|
||||
.page-apps .title {
|
||||
text-align: center;
|
||||
background-color: #25272d;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
}
|
||||
.page-apps .inner {
|
||||
display: flex;
|
||||
color: #fff;
|
||||
padding: 15px;
|
||||
overflow-y: visible;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item {
|
||||
border: 1px solid #666;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
}
|
||||
.page-apps .inner .list-box .app-item .el-image {
|
||||
padding: 6px;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item .el-image .el-image__inner {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item .title {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item .title .name {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #47fff1;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item .title .opt {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item .hello-msg {
|
||||
height: 60px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
.page-apps .inner .list-box .app-item:hover {
|
||||
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
69
new-ui/projects/web/src/assets/css/chat-app.styl
Normal file
69
new-ui/projects/web/src/assets/css/chat-app.styl
Normal file
@@ -0,0 +1,69 @@
|
||||
.page-apps {
|
||||
background-color: #282c34;
|
||||
height 100vh
|
||||
|
||||
.title {
|
||||
text-align center
|
||||
background-color #25272d
|
||||
font-size 24px
|
||||
color #ffffff
|
||||
padding 10px
|
||||
border-bottom 1px solid #3c3c3c
|
||||
}
|
||||
|
||||
.inner {
|
||||
display flex
|
||||
color #ffffff
|
||||
padding 15px;
|
||||
overflow-y visible
|
||||
overflow-x hidden
|
||||
|
||||
.list-box {
|
||||
.app-item {
|
||||
border 1px solid #666666
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
|
||||
.el-image {
|
||||
padding 6px
|
||||
|
||||
.el-image__inner {
|
||||
border-radius 10px
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display flex
|
||||
padding 10px
|
||||
|
||||
.name {
|
||||
width 100%
|
||||
text-align left
|
||||
font-size 16px
|
||||
font-weight bold
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
.opt {
|
||||
position: relative;
|
||||
top -5px
|
||||
}
|
||||
}
|
||||
|
||||
.hello-msg {
|
||||
height 60px
|
||||
padding 10px
|
||||
font-size 14px
|
||||
color #999999
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
312
new-ui/projects/web/src/assets/css/chat-plus.css
Normal file
312
new-ui/projects/web/src/assets/css/chat-plus.css
Normal file
@@ -0,0 +1,312 @@
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
#app .common-layout {
|
||||
height: 100%;
|
||||
}
|
||||
#app .common-layout .el-aside {
|
||||
background-color: #252526;
|
||||
}
|
||||
#app .common-layout .el-aside .title-box {
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
#app .common-layout .el-aside .title-box span {
|
||||
padding-top: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: #28292a;
|
||||
border-top: 1px solid #2f3032;
|
||||
border-right: 1px solid #2f3032;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .search-box {
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .search-box .el-input__wrapper {
|
||||
background-color: #363535;
|
||||
box-shadow: none;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item:hover {
|
||||
background-color: #343540;
|
||||
}
|
||||
#app .common-layout .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 {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 190px;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title {
|
||||
color: #c1c1c1;
|
||||
padding: 5px 10px;
|
||||
max-width: 220px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .btn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
#app .common-layout .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 {
|
||||
background-color: #343540;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item.active .btn {
|
||||
display: inline;
|
||||
}
|
||||
#app .common-layout .el-aside .tool-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 0 20px 10px 20px;
|
||||
border-top: 1px solid #3c3c3c;
|
||||
}
|
||||
#app .common-layout .el-aside .tool-box .user-info {
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
#app .common-layout .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 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#app .common-layout .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 {
|
||||
color: #ccc;
|
||||
line-height: 24px;
|
||||
}
|
||||
#app .common-layout .el-main {
|
||||
overflow: hidden;
|
||||
--el-main-padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: #28292a;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .chat-config {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .chat-config .role-select-label {
|
||||
color: #fff;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .chat-config .el-select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .chat-config .role-select {
|
||||
max-width: 130px;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .chat-config .el-button .el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .iconfont {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .is-circle {
|
||||
margin-left: 5px;
|
||||
}
|
||||
#app .common-layout .el-main .chat-head .is-circle .iconfont {
|
||||
margin-right: 0;
|
||||
}
|
||||
#app .common-layout .el-main .right-box {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-left: 1px solid #4f4f4f;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .chat-box {
|
||||
overflow-y: scroll;
|
||||
--content-font-size: 16px;
|
||||
--content-color: #c1c1c1;
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
padding: 0 0 50px 0;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .chat-box .chat-line {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .re-generate {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .re-generate .btn-box {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .re-generate .btn-box .el-button .el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .input-box {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||
padding: 0 15px;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .input-box .input-container {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .input-box .input-container .el-textarea .el-textarea__inner::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .input-box .input-container .select-file {
|
||||
position: absolute;
|
||||
right: 48px;
|
||||
top: 20px;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .input-box .input-container .send-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 20px;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container .input-box .input-container .send-btn .el-button {
|
||||
padding: 8px 5px;
|
||||
border-radius: 6px;
|
||||
background: #19c37d;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
#app .common-layout .el-main .right-box #container::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
#app .el-message-box {
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
}
|
||||
#app .el-message {
|
||||
min-width: 100px;
|
||||
max-width: 600px;
|
||||
}
|
||||
.el-select-dropdown__wrap .el-select-dropdown__item .role-option {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.el-select-dropdown__wrap .el-select-dropdown__item .role-option .el-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.el-select-dropdown__wrap .el-select-dropdown__item .role-option span {
|
||||
margin-left: 5px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.account {
|
||||
display: flex;
|
||||
background-color: #90ffc2;
|
||||
color: #000;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.account .vip-logo .el-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
.account .vip-info {
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
.account .vip-info h4,
|
||||
.account .vip-info p {
|
||||
margin: 0;
|
||||
}
|
||||
.account .vip-info h4 {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
.account .vip-info p {
|
||||
color: #333;
|
||||
}
|
||||
.account .pay-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .notice {
|
||||
padding: 0 20px 0 20px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .notice .el-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
.dialog-service {
|
||||
text-align: center;
|
||||
}
|
||||
.dialog-service .el-image {
|
||||
width: 360px;
|
||||
}
|
||||
417
new-ui/projects/web/src/assets/css/chat-plus.styl
Normal file
417
new-ui/projects/web/src/assets/css/chat-plus.styl
Normal file
@@ -0,0 +1,417 @@
|
||||
$sideBgColor = #252526;
|
||||
$borderColor = #4676d0;
|
||||
#app {
|
||||
|
||||
height: 100%;
|
||||
|
||||
.common-layout {
|
||||
height: 100%;
|
||||
|
||||
// left side
|
||||
|
||||
.el-aside {
|
||||
background-color: $sideBgColor;
|
||||
|
||||
.title-box {
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
|
||||
span {
|
||||
padding-top: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
display: flex
|
||||
flex-flow: column
|
||||
background-color: #28292A
|
||||
border-top: 1px solid #2F3032
|
||||
border-right: 1px solid #2F3032
|
||||
|
||||
.search-box {
|
||||
flex-wrap: wrap
|
||||
padding: 10px 15px;
|
||||
//background-color #343540
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: #363535;
|
||||
box-shadow: none
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
//display flex
|
||||
//flex-wrap: wrap;
|
||||
//flex-direction column
|
||||
width: 100%
|
||||
overflow-y: scroll
|
||||
|
||||
.chat-list-item {
|
||||
display: flex
|
||||
width: 100%
|
||||
justify-content: flex-start
|
||||
padding: 8px 12px
|
||||
//border-bottom: 1px solid #3c3c3c
|
||||
cursor: pointer
|
||||
|
||||
&:hover {
|
||||
background-color #343540
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.chat-title-input {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
color: #c1c1c1
|
||||
padding: 5px 10px;
|
||||
max-width 220px;
|
||||
font-size 14px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display none
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 16px;
|
||||
color #ffffff
|
||||
|
||||
.el-icon {
|
||||
margin-right 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list-item.active {
|
||||
background-color: #343540;
|
||||
|
||||
.btn {
|
||||
display inline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tool-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding 0 20px 10px 20px;
|
||||
border-top 1px solid #3c3c3c;
|
||||
|
||||
.user-info {
|
||||
width 100%
|
||||
padding-top 10px;
|
||||
|
||||
.el-dropdown-link {
|
||||
width 100%;
|
||||
cursor: pointer
|
||||
display flex
|
||||
|
||||
.el-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.username {
|
||||
display flex
|
||||
line-height 22px;
|
||||
width 230px;
|
||||
padding-left 10px;
|
||||
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: #cccccc;
|
||||
line-height 24px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-main {
|
||||
overflow: hidden;
|
||||
--el-main-padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.chat-head {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: #28292A
|
||||
|
||||
.chat-config {
|
||||
display flex
|
||||
flex-direction row
|
||||
align-items: center;
|
||||
justify-content center;
|
||||
padding-top 10px;
|
||||
|
||||
.role-select-label {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
.el-select {
|
||||
//max-width 150px;
|
||||
margin-right 10px;
|
||||
}
|
||||
|
||||
.role-select {
|
||||
max-width 130px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
margin-right 5px;
|
||||
}
|
||||
|
||||
.is-circle {
|
||||
margin-left 5px
|
||||
.iconfont {
|
||||
margin-right 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
background-color: #ffffff
|
||||
border-left: 1px solid #4f4f4f
|
||||
|
||||
#container {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
overflow-y: scroll;
|
||||
//border-bottom: 1px solid #4f4f4f
|
||||
|
||||
// 变量定义
|
||||
--content-font-size: 16px;
|
||||
--content-color: #c1c1c1;
|
||||
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
padding: 0 0 50px 0;
|
||||
|
||||
.chat-line {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.re-generate {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.btn-box {
|
||||
position: absolute
|
||||
bottom: 10px;
|
||||
|
||||
.el-button {
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-box {
|
||||
background-color: #ffffff
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||
padding 0 15px;
|
||||
|
||||
.input-container {
|
||||
width 100%
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 10px 0;
|
||||
display flex
|
||||
justify-content center
|
||||
position relative
|
||||
|
||||
.el-textarea {
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.select-file {
|
||||
position absolute;
|
||||
right 48px;
|
||||
top 20px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
position absolute;
|
||||
right 12px;
|
||||
top 20px;
|
||||
|
||||
.el-button {
|
||||
padding 8px 5px;
|
||||
border-radius 6px;
|
||||
background: rgb(25, 195, 125)
|
||||
color #ffffff;
|
||||
font-size 20px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#container::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
min-width: 100px;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select-dropdown__wrap {
|
||||
.el-select-dropdown__item {
|
||||
.role-option {
|
||||
display flex
|
||||
flex-flow row
|
||||
margin-top 8px;
|
||||
|
||||
.el-image {
|
||||
width 20px
|
||||
height 20px
|
||||
border-radius 50%
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left 5px;
|
||||
height 20px;
|
||||
line-height 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
display flex
|
||||
background-color #90FFC2
|
||||
color #000000
|
||||
width 100%
|
||||
border-radius 10px
|
||||
padding 10px
|
||||
|
||||
.vip-logo {
|
||||
.el-image {
|
||||
width 40px
|
||||
height 40px
|
||||
border-radius 100%
|
||||
background-color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
.vip-info {
|
||||
padding: 0 10px 0 10px
|
||||
|
||||
h4, p {
|
||||
margin 0
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight bold
|
||||
font-size 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
color #333333
|
||||
}
|
||||
}
|
||||
|
||||
.pay-btn {
|
||||
width 100%
|
||||
display flex
|
||||
justify-content right
|
||||
align-items center
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.el-overlay-dialog {
|
||||
.el-dialog {
|
||||
.el-dialog__body {
|
||||
.notice {
|
||||
padding 0 20px 0 20px
|
||||
line-height 1.8
|
||||
|
||||
.el-text {
|
||||
font-size 16px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-service {
|
||||
text-align center
|
||||
|
||||
.el-image {
|
||||
width 360px;
|
||||
}
|
||||
}
|
||||
19
new-ui/projects/web/src/assets/css/color-dark.css
Normal file
19
new-ui/projects/web/src/assets/css/color-dark.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.admin-home .login-wrap {
|
||||
background: #324157;
|
||||
}
|
||||
.admin-home .plugins-tips {
|
||||
background: #eef1f6;
|
||||
}
|
||||
.admin-home .plugins-tips a {
|
||||
color: #20a0ff;
|
||||
}
|
||||
.admin-home .tags-li.active {
|
||||
border: 1px solid #409eff;
|
||||
background-color: #409eff;
|
||||
}
|
||||
.admin-home .message-title {
|
||||
color: #20a0ff;
|
||||
}
|
||||
.admin-home .collapse-btn:hover {
|
||||
background: #283446;
|
||||
}
|
||||
30
new-ui/projects/web/src/assets/css/color-dark.styl
Normal file
30
new-ui/projects/web/src/assets/css/color-dark.styl
Normal file
@@ -0,0 +1,30 @@
|
||||
.admin-home {
|
||||
.header {
|
||||
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
background: #324157;
|
||||
}
|
||||
|
||||
.plugins-tips {
|
||||
background: #eef1f6;
|
||||
}
|
||||
|
||||
.plugins-tips a {
|
||||
color: #20a0ff;
|
||||
}
|
||||
|
||||
.tags-li.active {
|
||||
border: 1px solid #409EFF;
|
||||
background-color: #409EFF;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
color: #20a0ff;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background: rgb(40, 52, 70);
|
||||
}
|
||||
}
|
||||
13
new-ui/projects/web/src/assets/css/custom-scroll.css
Normal file
13
new-ui/projects/web/src/assets/css/custom-scroll.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.custom-scroll ::-webkit-scrollbar {
|
||||
width: 8px; /* 滚动条宽度 */
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-track {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
26
new-ui/projects/web/src/assets/css/custom-scroll.styl
Normal file
26
new-ui/projects/web/src/assets/css/custom-scroll.styl
Normal file
@@ -0,0 +1,26 @@
|
||||
.custom-scroll {
|
||||
/* 修改滚动条的颜色 */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
/* 修改滚动条轨道的背景颜色 */
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #282C34;
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444444;
|
||||
border-radius 8px
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块的悬停颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666666;
|
||||
}
|
||||
}
|
||||
403
new-ui/projects/web/src/assets/css/image-mj.css
Normal file
403
new-ui/projects/web/src/assets/css/image-mj.css
Normal file
@@ -0,0 +1,403 @@
|
||||
.page-mj {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.page-mj .inner {
|
||||
display: flex;
|
||||
}
|
||||
.page-mj .inner .mj-box {
|
||||
margin: 10px;
|
||||
background-color: #262626;
|
||||
border: 1px solid #454545;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.page-mj .inner .mj-box h2 {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .mj-box ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params {
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .el-icon {
|
||||
position: relative;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content {
|
||||
background-color: #383838;
|
||||
border-radius: 5px;
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content:hover {
|
||||
background-color: #585858;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #c4c4c4;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size9-16 {
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size16-9 {
|
||||
height: 9px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 7px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size3-4 {
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size4-3 {
|
||||
height: 12px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 4px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size2-3 {
|
||||
width: 11px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size3-2 {
|
||||
height: 11px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 5px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size1-2 {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size2-1 {
|
||||
height: 8px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content.active {
|
||||
color: #47fff1;
|
||||
background-color: #585858;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content.active .shape {
|
||||
border: 1px solid #47fff1;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .model {
|
||||
background-color: #383838;
|
||||
border: 1px solid #454545;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .model:hover {
|
||||
background-color: #585858;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .model .el-image {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .model .text {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .model.active {
|
||||
color: #47fff1;
|
||||
background-color: #585858;
|
||||
border: 1px solid #47fff1;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-select {
|
||||
--el-select-input-focus-border-color: #47fff1;
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-input__wrapper {
|
||||
background: #383838;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-input__inner {
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line.pt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-mj .inner .el-form .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .el-form .el-input,
|
||||
.page-mj .inner .el-form .el-slider {
|
||||
width: 180px;
|
||||
}
|
||||
.page-mj .inner .task-list-box {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
|
||||
color: #47fff1;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
|
||||
background-color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-textarea {
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline {
|
||||
display: flex;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-list-box {
|
||||
display: flex;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-list-box .img-item {
|
||||
width: 120px;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn .el-button {
|
||||
width: 200px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn .text-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
background-color: #555;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress span {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #666;
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
position: relative;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a {
|
||||
padding: 3px 0;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
|
||||
background-color: #6d6f78;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item:hover .remove {
|
||||
display: block;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover {
|
||||
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image img {
|
||||
height: 240px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image .image-slot {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
color: #fff;
|
||||
height: 240px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image .image-slot .iconfont {
|
||||
font-size: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image.upscale img {
|
||||
height: 310px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image.upscale .image-slot {
|
||||
height: 310px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image.upscale .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.mj-list-item-prompt .el-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
240
new-ui/projects/web/src/assets/css/image-mj.styl
Normal file
240
new-ui/projects/web/src/assets/css/image-mj.styl
Normal file
@@ -0,0 +1,240 @@
|
||||
.page-mj {
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
|
||||
.mj-box {
|
||||
margin 10px
|
||||
background-color #262626
|
||||
border 1px solid #454545
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 10px
|
||||
border-radius 10px
|
||||
color #ffffff;
|
||||
font-size 14px
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size 20px
|
||||
text-align center
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mj-params {
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
|
||||
|
||||
.param-line {
|
||||
padding 0 10px
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
background-color #383838
|
||||
border-radius 5px
|
||||
padding 8px 14px
|
||||
display flex
|
||||
cursor pointer
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color #585858
|
||||
}
|
||||
|
||||
.icon {
|
||||
width 20px
|
||||
height 20px
|
||||
margin-bottom 5px
|
||||
}
|
||||
|
||||
.shape {
|
||||
width 16px
|
||||
height 16px
|
||||
margin-bottom 5px
|
||||
border 1px solid #C4C4C4
|
||||
border-radius 3px
|
||||
}
|
||||
|
||||
.shape.size9-16 {
|
||||
width 9px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size16-9 {
|
||||
height 9px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 7px
|
||||
}
|
||||
|
||||
.shape.size3-4 {
|
||||
width 12px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size4-3 {
|
||||
height 12px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 4px
|
||||
}
|
||||
|
||||
.shape.size2-3 {
|
||||
width 11px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size3-2 {
|
||||
height 11px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 5px
|
||||
}
|
||||
|
||||
.shape.size1-2 {
|
||||
width 8px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size2-1 {
|
||||
height 8px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 8px
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.grid-content.active {
|
||||
color #47fff1
|
||||
background-color #585858
|
||||
|
||||
.shape {
|
||||
border 1px solid #47fff1
|
||||
}
|
||||
}
|
||||
|
||||
.model {
|
||||
background-color #383838
|
||||
border 1px solid #454545
|
||||
border-radius 5px
|
||||
padding 10px
|
||||
margin-bottom 10px
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
cursor pointer
|
||||
|
||||
&:hover {
|
||||
background-color #585858
|
||||
}
|
||||
|
||||
.el-image {
|
||||
height 60px
|
||||
width 100%
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top 6px
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.model.active {
|
||||
color #47fff1
|
||||
background-color #585858
|
||||
border 1px solid #47fff1
|
||||
}
|
||||
|
||||
.form-item-inner {
|
||||
display flex
|
||||
align-items: center
|
||||
|
||||
.el-select {
|
||||
--el-select-input-focus-border-color: #47FFF1;
|
||||
--el-input-focus-border-color: #47FFF1;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background: #383838;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
}
|
||||
}
|
||||
|
||||
.img-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width 100%
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.uploader-icon {
|
||||
font-size: 28px
|
||||
color: #8c939d
|
||||
width 100%
|
||||
height: 120px
|
||||
text-align: center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
display: flex
|
||||
align-items: center
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
.el-input, .el-slider {
|
||||
width 180px
|
||||
}
|
||||
}
|
||||
|
||||
@import "task-list.styl"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.mj-list-item-prompt {
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
cursor pointer
|
||||
position relative
|
||||
}
|
||||
}
|
||||
361
new-ui/projects/web/src/assets/css/image-sd.css
Normal file
361
new-ui/projects/web/src/assets/css/image-sd.css
Normal file
@@ -0,0 +1,361 @@
|
||||
.page-sd {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.page-sd .inner {
|
||||
display: flex;
|
||||
}
|
||||
.page-sd .inner .sd-box {
|
||||
margin: 10px;
|
||||
background-color: #262626;
|
||||
border: 1px solid #454545;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.page-sd .inner .sd-box h2 {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #47fff1;
|
||||
}
|
||||
.page-sd .inner .sd-box ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params {
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params .param-line {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params .param-line .el-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params .param-line .el-input__suffix-inner .el-icon {
|
||||
top: 0;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params .param-line .grid-content,
|
||||
.page-sd .inner .sd-box .sd-params .param-line .form-item-inner {
|
||||
display: flex;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params .param-line .grid-content .el-icon,
|
||||
.page-sd .inner .sd-box .sd-params .param-line .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.page-sd .inner .sd-box .sd-params .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-sd .inner .sd-box .submit-btn {
|
||||
padding: 10px 15px 0 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-sd .inner .sd-box .submit-btn .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
.page-sd .inner .sd-box .submit-btn .el-button span {
|
||||
color: #2d3a4b;
|
||||
}
|
||||
.page-sd .inner .el-form .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
|
||||
color: #47fff1;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
|
||||
background-color: #47fff1;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-textarea {
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline {
|
||||
display: flex;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-list-box {
|
||||
display: flex;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-list-box .img-item {
|
||||
width: 120px;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn .el-button {
|
||||
width: 200px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn .text-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
background-color: #555;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress span {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #666;
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
position: relative;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a {
|
||||
padding: 3px 0;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
|
||||
background-color: #6d6f78;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item:hover .remove {
|
||||
display: block;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover {
|
||||
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image img {
|
||||
height: 240px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image .image-slot {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
color: #fff;
|
||||
height: 240px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image .image-slot .iconfont {
|
||||
font-size: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image.upscale img {
|
||||
height: 310px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image.upscale .image-slot {
|
||||
height: 310px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image.upscale .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog {
|
||||
background-color: #1a1b1e;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__header .el-dialog__title {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0 0 0 15px !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row {
|
||||
width: 100%;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container .image-slot {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container .image-slot .el-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info {
|
||||
background-color: #25262b;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line {
|
||||
width: 100%;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt {
|
||||
background-color: #35363b;
|
||||
padding: 10px;
|
||||
color: #999;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
min-height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt .el-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper label {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
color: #a5a5a5;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper .item-value {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: #35363b;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params {
|
||||
padding: 20px 0 10px 0;
|
||||
}
|
||||
.page-sd .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
.page-sd .mj-list-item-prompt .el-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
106
new-ui/projects/web/src/assets/css/image-sd.styl
Normal file
106
new-ui/projects/web/src/assets/css/image-sd.styl
Normal file
@@ -0,0 +1,106 @@
|
||||
.page-sd {
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
|
||||
.sd-box {
|
||||
margin 10px
|
||||
background-color #262626
|
||||
border 1px solid #454545
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 10px
|
||||
border-radius 10px
|
||||
color #ffffff;
|
||||
font-size 14px
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size 20px
|
||||
text-align center
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sd-params {
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
|
||||
|
||||
.param-line {
|
||||
padding 0 10px
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 3px
|
||||
}
|
||||
|
||||
.el-input__suffix-inner {
|
||||
.el-icon {
|
||||
top 0
|
||||
}
|
||||
}
|
||||
|
||||
.grid-content
|
||||
.form-item-inner {
|
||||
display flex
|
||||
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
margin-top 2px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding 10px 15px 0 15px
|
||||
text-align center
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
|
||||
span {
|
||||
color #2D3A4B
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
@import "task-list.styl"
|
||||
}
|
||||
|
||||
@import "sd-task-dialog.styl"
|
||||
|
||||
|
||||
.mj-list-item-prompt {
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
cursor pointer
|
||||
position relative
|
||||
top 2px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
192
new-ui/projects/web/src/assets/css/images-wall.css
Normal file
192
new-ui/projects/web/src/assets/css/images-wall.css
Normal file
@@ -0,0 +1,192 @@
|
||||
.page-images-wall {
|
||||
display: flex;
|
||||
background-color: #282c34;
|
||||
}
|
||||
.page-images-wall .inner {
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-images-wall .inner .header {
|
||||
display: flex;
|
||||
padding: 0 40px;
|
||||
}
|
||||
.page-images-wall .inner .header h2 {
|
||||
width: 300px;
|
||||
}
|
||||
.page-images-wall .inner .header .settings {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
.page-images-wall .inner .header .settings .el-radio-group {
|
||||
font-size: 16px;
|
||||
}
|
||||
.page-images-wall .inner .header .settings .el-radio-group .el-radio {
|
||||
color: #fff;
|
||||
}
|
||||
.page-images-wall .inner .waterfall {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .image {
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .image .el-image {
|
||||
width: 100%;
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .opt {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
line-height: 1.2;
|
||||
border-top-right-radius: 10px;
|
||||
background-color: rgba(10,10,10,0.7);
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .opt span {
|
||||
word-break: break-all;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .opt .el-icon,
|
||||
.page-images-wall .inner .waterfall .list-item .opt .iconfont {
|
||||
top: 2px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 5px;
|
||||
padding: 2px;
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .opt .el-icon:hover,
|
||||
.page-images-wall .inner .waterfall .list-item .opt .iconfont:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item:hover .opt {
|
||||
display: block;
|
||||
animation: expandUp 0.3s ease-in-out forwards;
|
||||
transform-origin: bottom center;
|
||||
transform: scaleY(0); /* 初始状态,元素高度为0 */
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item:hover .image .el-image {
|
||||
transform: scale(1.2); /* 放大图像到1.2倍大小 */
|
||||
}
|
||||
.page-images-wall .inner .footer {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-images-wall .inner .footer .iconfont {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog {
|
||||
background-color: #1a1b1e;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__header .el-dialog__title {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0 0 0 15px !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row {
|
||||
width: 100%;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container .image-slot {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container .image-slot .el-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info {
|
||||
background-color: #25262b;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line {
|
||||
width: 100%;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt {
|
||||
background-color: #35363b;
|
||||
padding: 10px;
|
||||
color: #999;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
min-height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt .el-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper label {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
color: #a5a5a5;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper .item-value {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: #35363b;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params {
|
||||
padding: 20px 0 10px 0;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
@-moz-keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@-o-keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
124
new-ui/projects/web/src/assets/css/images-wall.styl
Normal file
124
new-ui/projects/web/src/assets/css/images-wall.styl
Normal file
@@ -0,0 +1,124 @@
|
||||
@keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.page-images-wall {
|
||||
display: flex;
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
width 100%
|
||||
color #ffffff
|
||||
overflow hidden
|
||||
|
||||
.header {
|
||||
display flex
|
||||
padding 0 40px
|
||||
|
||||
h2 {
|
||||
width 300px
|
||||
}
|
||||
|
||||
.settings {
|
||||
width 100%
|
||||
display flex
|
||||
justify-content right
|
||||
|
||||
.el-radio-group {
|
||||
font-size 16px
|
||||
|
||||
.el-radio {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
overflow-y auto
|
||||
overflow-x hidden
|
||||
|
||||
.list-item {
|
||||
|
||||
.image {
|
||||
overflow hidden
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
transition: transform 0.3s;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
display none
|
||||
position absolute
|
||||
width 100%
|
||||
bottom 0
|
||||
left 0
|
||||
color #ffffff
|
||||
padding 8px 10px
|
||||
line-height 1.2
|
||||
border-top-right-radius 10px
|
||||
background-color rgba(10, 10, 10, 0.7)
|
||||
|
||||
span {
|
||||
word-break break-all
|
||||
}
|
||||
|
||||
.el-icon, .iconfont {
|
||||
top 2px
|
||||
cursor pointer
|
||||
border 1px solid #ffffff
|
||||
border-radius 5px
|
||||
padding 2px
|
||||
font-size 16px;
|
||||
margin-right 10px
|
||||
|
||||
&:hover {
|
||||
background-color #444444
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.opt {
|
||||
display block
|
||||
animation: expandUp 0.3s ease-in-out forwards;
|
||||
transform-origin: bottom center;
|
||||
transform: scaleY(0); /* 初始状态,元素高度为0 */
|
||||
}
|
||||
|
||||
.image {
|
||||
.el-image {
|
||||
transform: scale(1.2); /* 放大图像到1.2倍大小 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.footer {
|
||||
display flex
|
||||
padding 20px
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.iconfont {
|
||||
margin-left 6px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import "sd-task-dialog.styl"
|
||||
}
|
||||
138
new-ui/projects/web/src/assets/css/main.css
Normal file
138
new-ui/projects/web/src/assets/css/main.css
Normal file
@@ -0,0 +1,138 @@
|
||||
html,
|
||||
body,
|
||||
#app,
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.admin-home a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin-home .content-box {
|
||||
position: absolute;
|
||||
left: 250px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/*padding-bottom: 30px;*/
|
||||
-webkit-transition: left 0.3s ease-in-out;
|
||||
transition: left 0.3s ease-in-out;
|
||||
background: #f0f0f0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content {
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
/*BaseForm*/
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .container {
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .container .handle-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .crumbs {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-table th {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .pagination {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .plugins-tips {
|
||||
padding: 20px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-button + .el-tooltip {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-table tr:hover {
|
||||
background: #f6faff;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .mgb20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .move-enter-active,
|
||||
.admin-home .content-box .content .move-leave-active {
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .move-enter-from,
|
||||
.admin-home .content-box .content .move-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .form-box {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .form-box .line {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-time-panel__content::after,
|
||||
.admin-home .content-box .content .el-time-panel__content::before {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content [class*=" el-icon-"],
|
||||
.admin-home .content-box .content [class^=el-icon-] {
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-sub-menu [class^=el-icon-] {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.admin-home .content-collapse {
|
||||
left: 65px;
|
||||
}
|
||||
151
new-ui/projects/web/src/assets/css/main.styl
Normal file
151
new-ui/projects/web/src/assets/css/main.styl
Normal file
@@ -0,0 +1,151 @@
|
||||
//* {
|
||||
// margin: 0;
|
||||
// padding: 0;
|
||||
//}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app,
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.admin-home {
|
||||
a {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.content-box {
|
||||
position: absolute;
|
||||
left: 250px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding-bottom: 30px;
|
||||
-webkit-transition: left .3s ease-in-out;
|
||||
transition: left .3s ease-in-out;
|
||||
background: #f0f0f0;
|
||||
|
||||
.content {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
|
||||
.container {
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
|
||||
.handle-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.el-table th {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plugins-tips {
|
||||
padding: 20px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-button + .el-tooltip {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.el-table tr:hover {
|
||||
background: #f6faff;
|
||||
}
|
||||
|
||||
.mgb20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.move-enter-active,
|
||||
.move-leave-active {
|
||||
transition: opacity .1s ease;
|
||||
}
|
||||
|
||||
.move-enter-from,
|
||||
.move-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*BaseForm*/
|
||||
|
||||
.form-box {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.form-box .line {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-time-panel__content::after,
|
||||
.el-time-panel__content::before {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
[class*=" el-icon-"], [class^=el-icon-] {
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.el-sub-menu [class^=el-icon-] {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-collapse {
|
||||
left: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
153
new-ui/projects/web/src/assets/css/member.css
Normal file
153
new-ui/projects/web/src/assets/css/member.css
Normal file
@@ -0,0 +1,153 @@
|
||||
.member {
|
||||
background-color: #282c34;
|
||||
height: 100vh;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .amount {
|
||||
text-align: center;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .amount span {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .count-down {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .pay-qrcode {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .pay-qrcode .el-image {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .tip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .tip .el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .tip .text {
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.member .el-dialog .el-dialog__body .pay-container .tip.success {
|
||||
color: #07c160;
|
||||
}
|
||||
.member .title {
|
||||
text-align: center;
|
||||
background-color: #25272d;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
}
|
||||
.member .inner {
|
||||
color: #fff;
|
||||
padding: 15px 0 15px 15px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.member .inner .user-profile {
|
||||
padding: 10px 20px 20px 20px;
|
||||
background-color: #393f4a;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
height: 100vh;
|
||||
}
|
||||
.member .inner .user-profile .el-form-item__label {
|
||||
color: #fff;
|
||||
justify-content: start;
|
||||
}
|
||||
.member .inner .user-profile .user-opt .el-col {
|
||||
padding: 10px;
|
||||
}
|
||||
.member .inner .user-profile .user-opt .el-col .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
.member .inner .product-box .info {
|
||||
padding: 10px 20px 20px 0;
|
||||
}
|
||||
.member .inner .product-box .info .el-alert__description {
|
||||
font-size: 14px !important;
|
||||
margin: 0;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item {
|
||||
border: 1px solid #666;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .image-container .el-image {
|
||||
padding: 6px;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .image-container .el-image .el-image__inner {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-title {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-title .name {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #47fff1;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line .label {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line .price,
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line .expire,
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line calls {
|
||||
display: flex;
|
||||
width: 90px;
|
||||
justify-content: right;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line .price {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line .expire {
|
||||
color: #409eff;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .info-line .calls {
|
||||
color: #f2cb51;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .pay-way {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item .product-info .pay-way .iconfont {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.member .inner .product-box .list-box .product-item:hover {
|
||||
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
.member .inner .product-box .headline {
|
||||
padding: 0 20px 20px 0;
|
||||
}
|
||||
.member .inner .product-box .user-order {
|
||||
padding: 0 20px 20px 0;
|
||||
}
|
||||
199
new-ui/projects/web/src/assets/css/member.styl
Normal file
199
new-ui/projects/web/src/assets/css/member.styl
Normal file
@@ -0,0 +1,199 @@
|
||||
.member {
|
||||
background-color: #282c34;
|
||||
height 100vh
|
||||
|
||||
.el-dialog {
|
||||
.el-dialog__body {
|
||||
padding-top 10px
|
||||
|
||||
.pay-container {
|
||||
.amount {
|
||||
text-align center
|
||||
|
||||
span {
|
||||
color #f56c6c
|
||||
}
|
||||
}
|
||||
|
||||
.count-down {
|
||||
display flex
|
||||
justify-content center
|
||||
}
|
||||
|
||||
.pay-qrcode {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.el-image {
|
||||
width 360px;
|
||||
height 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.el-icon {
|
||||
font-size 24px
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 16px
|
||||
margin-left 10px
|
||||
}
|
||||
}
|
||||
|
||||
.tip.success {
|
||||
color #07c160
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align center
|
||||
background-color #25272d
|
||||
font-size 24px
|
||||
color #ffffff
|
||||
padding 10px
|
||||
border-bottom 1px solid #3c3c3c
|
||||
}
|
||||
|
||||
.inner {
|
||||
color #ffffff
|
||||
padding 15px 0 15px 15px;
|
||||
overflow-x hidden
|
||||
overflow-y visible
|
||||
|
||||
.user-profile {
|
||||
padding 10px 20px 20px 20px
|
||||
background-color #393F4A
|
||||
color #ffffff
|
||||
border-radius 10px
|
||||
height 100vh
|
||||
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
justify-content start
|
||||
}
|
||||
|
||||
.user-opt {
|
||||
.el-col {
|
||||
padding 10px
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.product-box {
|
||||
.info {
|
||||
.el-alert__description {
|
||||
font-size 14px !important
|
||||
margin 0
|
||||
}
|
||||
padding 10px 20px 20px 0
|
||||
}
|
||||
|
||||
.list-box {
|
||||
.product-item {
|
||||
border 1px solid #666666
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
cursor pointer
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
|
||||
.image-container {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.el-image {
|
||||
padding 6px
|
||||
|
||||
.el-image__inner {
|
||||
border-radius 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-title {
|
||||
display flex
|
||||
padding 10px
|
||||
|
||||
.name {
|
||||
width 100%
|
||||
text-align center
|
||||
font-size 16px
|
||||
font-weight bold
|
||||
color #47fff1
|
||||
}
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding 10px 20px
|
||||
font-size 14px
|
||||
color #999999
|
||||
|
||||
.info-line {
|
||||
display flex
|
||||
width 100%
|
||||
padding 5px 0
|
||||
|
||||
.label {
|
||||
display flex
|
||||
width 100%
|
||||
}
|
||||
|
||||
.price, .expire, calls {
|
||||
display flex
|
||||
width 90px
|
||||
justify-content right
|
||||
}
|
||||
|
||||
.price {
|
||||
color #f56c6c
|
||||
}
|
||||
|
||||
.expire {
|
||||
color #409eff
|
||||
}
|
||||
|
||||
.calls {
|
||||
color #F2CB51
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pay-way {
|
||||
padding 10px 0
|
||||
display flex
|
||||
justify-content: space-between
|
||||
|
||||
.iconfont {
|
||||
margin-right 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.headline {
|
||||
padding 0 20px 20px 0
|
||||
}
|
||||
|
||||
.user-order {
|
||||
padding 0 20px 20px 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
new-ui/projects/web/src/assets/css/mobile/chat-list.css
Normal file
29
new-ui/projects/web/src/assets/css/mobile/chat-list.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.mobile-chat-list .content .van-cell__value .chat-list-item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mobile-chat-list .content .van-cell__value .chat-list-item .van-image {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.mobile-chat-list .content .van-cell__value .chat-list-item .van-ellipsis {
|
||||
margin-top: 5px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mobile-chat-list .van-nav-bar .van-nav-bar__right .van-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
.van-popup .picker-option {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.van-popup .picker-option .van-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
47
new-ui/projects/web/src/assets/css/mobile/chat-list.styl
Normal file
47
new-ui/projects/web/src/assets/css/mobile/chat-list.styl
Normal file
@@ -0,0 +1,47 @@
|
||||
.mobile-chat-list {
|
||||
.content {
|
||||
.van-cell__value {
|
||||
.chat-list-item {
|
||||
display flex
|
||||
font-size 14px
|
||||
|
||||
.van-image {
|
||||
min-width 32px
|
||||
width 32px
|
||||
height 32px
|
||||
}
|
||||
|
||||
.van-ellipsis {
|
||||
margin-top 5px;
|
||||
margin-left 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.van-nav-bar {
|
||||
.van-nav-bar__right {
|
||||
.van-icon {
|
||||
font-size 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.van-popup {
|
||||
.picker-option {
|
||||
display flex
|
||||
width 100%
|
||||
padding 0 10px
|
||||
overflow hidden
|
||||
height 20px
|
||||
text-overflow ellipsis
|
||||
|
||||
.van-image {
|
||||
width 20px;
|
||||
height 20px;
|
||||
margin-right 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
46
new-ui/projects/web/src/assets/css/mobile/chat-session.css
Normal file
46
new-ui/projects/web/src/assets/css/mobile/chat-session.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.mobile-chat .message-list-box {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 10px;
|
||||
overflow-x: auto;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.mobile-chat .message-list-box .van-cell {
|
||||
background: none;
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
.mobile-chat .chat-box-wrapper .van-sticky .van-cell-group--inset {
|
||||
margin: 0;
|
||||
}
|
||||
.mobile-chat .chat-box-wrapper .van-sticky .van-cell-group--inset .van-cell {
|
||||
padding: 10px;
|
||||
}
|
||||
.mobile-chat .chat-box-wrapper .van-sticky .van-cell-group--inset .van-cell .icon-box .van-icon {
|
||||
font-size: 24px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mobile-chat .chat-box-wrapper .van-sticky .van-cell-group--inset .van-cell .button-voice {
|
||||
padding: 0 2px;
|
||||
height: 30px;
|
||||
}
|
||||
.mobile-chat .chat-box-wrapper .van-sticky .van-cell-group--inset .van-cell .button-voice .el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
.mobile-chat .van-nav-bar__title .van-dropdown-menu__title {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.mobile-chat .van-nav-bar__title .van-cell__title {
|
||||
text-align: left;
|
||||
}
|
||||
.mobile-chat .van-nav-bar__right .van-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
.van-overlay .mic-wrapper {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
}
|
||||
.van-theme-dark .mobile-chat .message-list-box {
|
||||
background: #232425;
|
||||
}
|
||||
75
new-ui/projects/web/src/assets/css/mobile/chat-session.styl
Normal file
75
new-ui/projects/web/src/assets/css/mobile/chat-session.styl
Normal file
@@ -0,0 +1,75 @@
|
||||
.mobile-chat {
|
||||
.message-list-box {
|
||||
padding-top 50px
|
||||
padding-bottom 10px
|
||||
overflow-x auto
|
||||
background #F5F5F5;
|
||||
|
||||
.van-cell {
|
||||
background none
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-box-wrapper {
|
||||
.van-sticky {
|
||||
.van-cell-group--inset {
|
||||
margin 0
|
||||
|
||||
.van-cell {
|
||||
padding 10px
|
||||
|
||||
.icon-box {
|
||||
.van-icon {
|
||||
font-size 24px
|
||||
margin-left 10px
|
||||
}
|
||||
}
|
||||
|
||||
.button-voice {
|
||||
padding 0 2px
|
||||
|
||||
.el-icon {
|
||||
font-size 24px
|
||||
}
|
||||
height 30px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.van-nav-bar__title {
|
||||
.van-dropdown-menu__title {
|
||||
margin-right 10px
|
||||
}
|
||||
|
||||
.van-cell__title {
|
||||
text-align left
|
||||
}
|
||||
}
|
||||
|
||||
.van-nav-bar__right {
|
||||
.van-icon {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.van-overlay {
|
||||
.mic-wrapper {
|
||||
display flex
|
||||
height 100vh
|
||||
justify-content center
|
||||
align-items center
|
||||
flex-flow column
|
||||
}
|
||||
}
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-chat {
|
||||
.message-list-box {
|
||||
background #232425;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
new-ui/projects/web/src/assets/css/mobile/image-mj.css
Normal file
133
new-ui/projects/web/src/assets/css/mobile/image-mj.css
Normal file
@@ -0,0 +1,133 @@
|
||||
.mobile-mj .content .text-line {
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .rate {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
padding: 5px 10px;
|
||||
margin: 5px 0;
|
||||
border-radius: 5px;
|
||||
flex-flow: column;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .rate .icon {
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .rate .icon .van-image {
|
||||
max-width: 20px;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .rate .text {
|
||||
text-align: center;
|
||||
color: #555;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .model {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
padding: 6px;
|
||||
margin: 5px 0;
|
||||
border-radius: 5px;
|
||||
flex-flow: column;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .model .icon {
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .model .icon .van-image {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .model .text {
|
||||
text-align: center;
|
||||
color: #555;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-row .van-col .active {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-button {
|
||||
position: relative;
|
||||
}
|
||||
.mobile-mj .content .text-line .van-button .van-tag {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
.mobile-mj .content .text-line .align-right {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .van-image,
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
|
||||
min-height: 100px;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(50,50,50,0.5);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress .van-circle__text {
|
||||
color: #fff;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
color: #c1c1c1;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon {
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon .iconfont {
|
||||
font-size: 24px;
|
||||
}
|
||||
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .text {
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content {
|
||||
padding: 0;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item {
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .opt .opt-btn {
|
||||
padding: 3px 10px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
margin: 3px 0;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .van-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .upscale {
|
||||
height: 260px;
|
||||
width: 100%;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove .el-button {
|
||||
margin-left: 5px;
|
||||
height: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
187
new-ui/projects/web/src/assets/css/mobile/image-mj.styl
Normal file
187
new-ui/projects/web/src/assets/css/mobile/image-mj.styl
Normal file
@@ -0,0 +1,187 @@
|
||||
.mobile-mj {
|
||||
.content {
|
||||
.text-line {
|
||||
padding 6px
|
||||
font-size 14px
|
||||
|
||||
.van-row {
|
||||
.van-col {
|
||||
.rate {
|
||||
display: flex;
|
||||
justify-content center
|
||||
background-color #f5f5f5
|
||||
padding 5px 10px
|
||||
margin 5px 0
|
||||
border-radius 5px
|
||||
flex-flow column
|
||||
|
||||
.icon {
|
||||
text-align center
|
||||
|
||||
.van-image {
|
||||
max-width 20px
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align center
|
||||
color #555555
|
||||
}
|
||||
}
|
||||
|
||||
.model {
|
||||
display: flex;
|
||||
justify-content center
|
||||
background-color #f5f5f5
|
||||
padding 6px
|
||||
margin 5px 0
|
||||
border-radius 5px
|
||||
flex-flow column
|
||||
|
||||
.icon {
|
||||
text-align center
|
||||
|
||||
.van-image {
|
||||
width 100%
|
||||
height 50px
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align center
|
||||
color #555555
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color #e5e5e5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.van-button {
|
||||
position relative
|
||||
|
||||
.van-tag {
|
||||
position absolute
|
||||
right 20px
|
||||
}
|
||||
}
|
||||
|
||||
.align-right {
|
||||
display flex
|
||||
justify-content right
|
||||
}
|
||||
}
|
||||
|
||||
.running-job-list {
|
||||
.van-grid {
|
||||
.van-grid-item {
|
||||
.van-grid-item__content {
|
||||
padding 0
|
||||
position relative
|
||||
|
||||
.van-image, .task-in-queue {
|
||||
min-height 100px
|
||||
}
|
||||
|
||||
.progress {
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(50, 50, 50, 0.5)
|
||||
position absolute
|
||||
left 0
|
||||
top 0
|
||||
|
||||
.van-circle__text {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
// end progress
|
||||
|
||||
.task-in-queue {
|
||||
display flex
|
||||
flex-flow column
|
||||
justify-content center
|
||||
color #c1c1c1
|
||||
|
||||
.icon {
|
||||
text-align center
|
||||
|
||||
.iconfont {
|
||||
font-size 24px
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size 14px
|
||||
margin-top 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//end running jobs
|
||||
|
||||
.finish-job-list {
|
||||
.van-grid {
|
||||
.van-grid-item {
|
||||
.van-grid-item__content {
|
||||
padding 0
|
||||
|
||||
.job-item {
|
||||
overflow hidden
|
||||
border-radius 6px
|
||||
position relative
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
.opt {
|
||||
.opt-btn {
|
||||
padding 3px 10px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
margin 3px 0
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
.van-image {
|
||||
width 100%
|
||||
height 200px
|
||||
}
|
||||
|
||||
.upscale {
|
||||
height 260px
|
||||
width 100%
|
||||
}
|
||||
|
||||
.remove {
|
||||
position absolute
|
||||
right 5px
|
||||
top 5px
|
||||
|
||||
.el-button {
|
||||
margin-left 5px
|
||||
height auto
|
||||
padding 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
new-ui/projects/web/src/assets/css/sd-task-dialog.css
Normal file
63
new-ui/projects/web/src/assets/css/sd-task-dialog.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.el-overlay-dialog .el-dialog {
|
||||
background-color: #1a1b1e;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__header .el-dialog__title {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0 0 0 15px !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row {
|
||||
width: 100%;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info {
|
||||
background-color: #25262b;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line {
|
||||
width: 100%;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt {
|
||||
background-color: #35363b;
|
||||
padding: 10px;
|
||||
color: #999;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
min-height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt .el-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper label {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
color: #a5a5a5;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper .item-value {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: #35363b;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params {
|
||||
padding: 20px 0 10px 0;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
96
new-ui/projects/web/src/assets/css/sd-task-dialog.styl
Normal file
96
new-ui/projects/web/src/assets/css/sd-task-dialog.styl
Normal file
@@ -0,0 +1,96 @@
|
||||
.el-overlay-dialog {
|
||||
.el-dialog {
|
||||
background-color #1a1b1e
|
||||
|
||||
.el-dialog__header {
|
||||
.el-dialog__title {
|
||||
color #F5F5F5
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding 0 0 0 15px !important
|
||||
display flex
|
||||
height 100%
|
||||
|
||||
.el-row {
|
||||
width 100%
|
||||
|
||||
.img-container {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.image-slot {
|
||||
display flex
|
||||
height 100vh
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.el-icon {
|
||||
font-size 60px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-info {
|
||||
background-color #25262b
|
||||
padding 1rem 1.5rem
|
||||
|
||||
.info-line {
|
||||
width 100%
|
||||
|
||||
.prompt {
|
||||
background-color #35363b
|
||||
padding 10px
|
||||
color #999999
|
||||
overflow auto
|
||||
max-height 100px
|
||||
min-height 50px
|
||||
|
||||
position relative
|
||||
|
||||
.el-icon {
|
||||
position absolute
|
||||
right 10px
|
||||
bottom 10px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-top 10px
|
||||
display flex
|
||||
|
||||
label {
|
||||
display flex
|
||||
width 100px
|
||||
color #a5a5a5
|
||||
}
|
||||
|
||||
.item-value {
|
||||
display flex
|
||||
width 100%
|
||||
background-color #35363b
|
||||
padding 2px 5px
|
||||
border-radius 5px
|
||||
color #F5F5F5
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.copy-params {
|
||||
padding 20px 0 10px 0
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end el-row
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
215
new-ui/projects/web/src/assets/css/task-list.css
Normal file
215
new-ui/projects/web/src/assets/css/task-list.css
Normal file
@@ -0,0 +1,215 @@
|
||||
.task-list-box {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
.task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
|
||||
color: #47fff1;
|
||||
font-size: 18px;
|
||||
}
|
||||
.task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
|
||||
background-color: #47fff1;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-textarea {
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
.task-list-box .task-list-inner .text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
.task-list-box .task-list-inner .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.task-list-box .task-list-inner .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.task-list-box .task-list-inner .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline {
|
||||
display: flex;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-list-box {
|
||||
display: flex;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-list-box .img-item {
|
||||
width: 120px;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.task-list-box .task-list-inner .submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.task-list-box .task-list-inner .submit-btn .el-button {
|
||||
width: 200px;
|
||||
}
|
||||
.task-list-box .task-list-inner .submit-btn .text-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
background-color: #555;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress span {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #666;
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
position: relative;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a {
|
||||
padding: 3px 0;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
|
||||
background-color: #6d6f78;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item:hover .remove {
|
||||
display: block;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover {
|
||||
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image img {
|
||||
height: 240px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image .image-slot {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
color: #fff;
|
||||
height: 240px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image .image-slot .iconfont {
|
||||
font-size: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image.upscale {
|
||||
max-height: 310px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image.upscale img {
|
||||
height: 310px;
|
||||
}
|
||||
.task-list-box .task-list-inner .job-list-box .el-image.upscale .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
295
new-ui/projects/web/src/assets/css/task-list.styl
Normal file
295
new-ui/projects/web/src/assets/css/task-list.styl
Normal file
@@ -0,0 +1,295 @@
|
||||
.task-list-box {
|
||||
width 100%
|
||||
padding 10px
|
||||
color #ffffff
|
||||
overflow-x hidden
|
||||
|
||||
.task-list-inner {
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title-tabs .el-tabs__item.is-active {
|
||||
color: #47FFF1;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title-tabs .el-tabs__active-bar {
|
||||
background-color: #47FFF1;
|
||||
}
|
||||
|
||||
.el-textarea {
|
||||
--el-input-focus-border-color: #47FFF1;
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background: transparent;
|
||||
padding 5px
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
|
||||
.form-item-inner {
|
||||
display flex
|
||||
align-items: center
|
||||
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
// 图片上传样式
|
||||
|
||||
.img-inline {
|
||||
display flex
|
||||
|
||||
.img-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width 120px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.uploader-icon {
|
||||
font-size: 28px
|
||||
color: #8c939d
|
||||
width 100%
|
||||
height: 120px
|
||||
text-align: center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img-list-box {
|
||||
display flex
|
||||
|
||||
.img-item {
|
||||
width 120px
|
||||
position relative
|
||||
margin-right 10px
|
||||
|
||||
.el-image {
|
||||
width 120px
|
||||
height 120px
|
||||
border-radius 5px
|
||||
}
|
||||
|
||||
.el-button {
|
||||
position absolute
|
||||
right 5px
|
||||
top 5px
|
||||
width 20px
|
||||
height 20px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
|
||||
.submit-btn {
|
||||
display flex
|
||||
margin: 20px 0
|
||||
|
||||
.el-button {
|
||||
width 200px
|
||||
}
|
||||
|
||||
.text-info {
|
||||
width 100%
|
||||
display flex
|
||||
justify-content right
|
||||
align-items center
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 任务列表
|
||||
|
||||
.job-list-box {
|
||||
.running-job-list {
|
||||
.job-item {
|
||||
//border: 1px solid #454545;
|
||||
width: 100%;
|
||||
padding 2px
|
||||
background-color #555555
|
||||
|
||||
.job-item-inner {
|
||||
position relative
|
||||
height 100%
|
||||
overflow hidden
|
||||
|
||||
.progress {
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 0
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
span {
|
||||
font-size 20px
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.finish-job-list {
|
||||
.job-item {
|
||||
width 100%
|
||||
height 100%
|
||||
border 1px solid #666666
|
||||
padding 6px
|
||||
overflow hidden
|
||||
border-radius 6px
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
position relative
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
|
||||
li {
|
||||
margin-right 6px
|
||||
|
||||
a {
|
||||
padding 3px 0
|
||||
width 40px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-prompt {
|
||||
font-size 20px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.remove {
|
||||
display none
|
||||
position absolute
|
||||
right 10px
|
||||
top 10px
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.remove {
|
||||
display block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animate {
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
height 100%
|
||||
overflow visible
|
||||
|
||||
img {
|
||||
height 240px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display flex
|
||||
flex-flow column
|
||||
justify-content center
|
||||
align-items center
|
||||
min-height 200px
|
||||
color #ffffff
|
||||
height 240px
|
||||
|
||||
.iconfont {
|
||||
font-size 50px
|
||||
margin-bottom 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-image.upscale {
|
||||
img {
|
||||
height 310px
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
height 310px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
227
new-ui/projects/web/src/assets/iconfont/iconfont.css
Normal file
227
new-ui/projects/web/src/assets/iconfont/iconfont.css
Normal file
@@ -0,0 +1,227 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1708054962140') format('woff2'),
|
||||
url('iconfont.woff?t=1708054962140') format('woff'),
|
||||
url('iconfont.ttf?t=1708054962140') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-prompt:before {
|
||||
content: "\e6ce";
|
||||
}
|
||||
|
||||
.icon-share-bold:before {
|
||||
content: "\e626";
|
||||
}
|
||||
|
||||
.icon-cancel-share:before {
|
||||
content: "\e682";
|
||||
}
|
||||
|
||||
.icon-xls:before {
|
||||
content: "\e678";
|
||||
}
|
||||
|
||||
.icon-file:before {
|
||||
content: "\e62f";
|
||||
}
|
||||
|
||||
.icon-doc:before {
|
||||
content: "\e6c2";
|
||||
}
|
||||
|
||||
.icon-pdf:before {
|
||||
content: "\e6c3";
|
||||
}
|
||||
|
||||
.icon-ppt:before {
|
||||
content: "\e642";
|
||||
}
|
||||
|
||||
.icon-txt:before {
|
||||
content: "\e644";
|
||||
}
|
||||
|
||||
.icon-sql:before {
|
||||
content: "\e65b";
|
||||
}
|
||||
|
||||
.icon-md:before {
|
||||
content: "\e63a";
|
||||
}
|
||||
|
||||
.icon-loading:before {
|
||||
content: "\e627";
|
||||
}
|
||||
|
||||
.icon-alipay:before {
|
||||
content: "\e634";
|
||||
}
|
||||
|
||||
.icon-face:before {
|
||||
content: "\e64b";
|
||||
}
|
||||
|
||||
.icon-book:before {
|
||||
content: "\e622";
|
||||
}
|
||||
|
||||
.icon-vip-user:before {
|
||||
content: "\e605";
|
||||
}
|
||||
|
||||
.icon-vip:before {
|
||||
content: "\e688";
|
||||
}
|
||||
|
||||
.icon-cart:before {
|
||||
content: "\e603";
|
||||
}
|
||||
|
||||
.icon-image-list:before {
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.icon-share:before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.icon-palette:before {
|
||||
content: "\e6da";
|
||||
}
|
||||
|
||||
.icon-palette-pen:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
|
||||
.icon-image:before {
|
||||
content: "\e7de";
|
||||
}
|
||||
|
||||
.icon-order:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
||||
.icon-service:before {
|
||||
content: "\e62d";
|
||||
}
|
||||
|
||||
.icon-like:before {
|
||||
content: "\e640";
|
||||
}
|
||||
|
||||
.icon-recharge:before {
|
||||
content: "\e637";
|
||||
}
|
||||
|
||||
.icon-model:before {
|
||||
content: "\e867";
|
||||
}
|
||||
|
||||
.icon-plugin:before {
|
||||
content: "\e69d";
|
||||
}
|
||||
|
||||
.icon-quick-start:before {
|
||||
content: "\e677";
|
||||
}
|
||||
|
||||
.icon-control:before {
|
||||
content: "\e69e";
|
||||
}
|
||||
|
||||
.icon-bug:before {
|
||||
content: "\e645";
|
||||
}
|
||||
|
||||
.icon-export:before {
|
||||
content: "\e791";
|
||||
}
|
||||
|
||||
.icon-sub-menu:before {
|
||||
content: "\e86b";
|
||||
}
|
||||
|
||||
.icon-wechat-pay:before {
|
||||
content: "\e639";
|
||||
}
|
||||
|
||||
.icon-donate:before {
|
||||
content: "\e7ee";
|
||||
}
|
||||
|
||||
.icon-reward:before {
|
||||
content: "\e6c7";
|
||||
}
|
||||
|
||||
.icon-bell:before {
|
||||
content: "\e887";
|
||||
}
|
||||
|
||||
.icon-home:before {
|
||||
content: "\e6cb";
|
||||
}
|
||||
|
||||
.icon-menu:before {
|
||||
content: "\e6a6";
|
||||
}
|
||||
|
||||
.icon-log:before {
|
||||
content: "\e61c";
|
||||
}
|
||||
|
||||
.icon-user-fill:before {
|
||||
content: "\e7d5";
|
||||
}
|
||||
|
||||
.icon-user-circle:before {
|
||||
content: "\e860";
|
||||
}
|
||||
|
||||
.icon-chart:before {
|
||||
content: "\e631";
|
||||
}
|
||||
|
||||
.icon-wechat:before {
|
||||
content: "\e621";
|
||||
}
|
||||
|
||||
.icon-config:before {
|
||||
content: "\e612";
|
||||
}
|
||||
|
||||
.icon-api-key:before {
|
||||
content: "\e7d8";
|
||||
}
|
||||
|
||||
.icon-role:before {
|
||||
content: "\e60a";
|
||||
}
|
||||
|
||||
.icon-dashboard:before {
|
||||
content: "\e602";
|
||||
}
|
||||
|
||||
.icon-password:before {
|
||||
content: "\e62a";
|
||||
}
|
||||
|
||||
.icon-send:before {
|
||||
content: "\e604";
|
||||
}
|
||||
|
||||
.icon-logout:before {
|
||||
content: "\e62e";
|
||||
}
|
||||
|
||||
.icon-github:before {
|
||||
content: "\e66f";
|
||||
}
|
||||
|
||||
1
new-ui/projects/web/src/assets/iconfont/iconfont.js
Normal file
1
new-ui/projects/web/src/assets/iconfont/iconfont.js
Normal file
File diff suppressed because one or more lines are too long
380
new-ui/projects/web/src/assets/iconfont/iconfont.json
Normal file
380
new-ui/projects/web/src/assets/iconfont/iconfont.json
Normal file
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"id": "4125778",
|
||||
"name": "chatgpt",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "8017627",
|
||||
"name": "prompt",
|
||||
"font_class": "prompt",
|
||||
"unicode": "e6ce",
|
||||
"unicode_decimal": 59086
|
||||
},
|
||||
{
|
||||
"icon_id": "1132455",
|
||||
"name": "share-bold",
|
||||
"font_class": "share-bold",
|
||||
"unicode": "e626",
|
||||
"unicode_decimal": 58918
|
||||
},
|
||||
{
|
||||
"icon_id": "7567359",
|
||||
"name": "cancel-share",
|
||||
"font_class": "cancel-share",
|
||||
"unicode": "e682",
|
||||
"unicode_decimal": 59010
|
||||
},
|
||||
{
|
||||
"icon_id": "12600976",
|
||||
"name": "xls",
|
||||
"font_class": "xls",
|
||||
"unicode": "e678",
|
||||
"unicode_decimal": 59000
|
||||
},
|
||||
{
|
||||
"icon_id": "3750429",
|
||||
"name": "file",
|
||||
"font_class": "file",
|
||||
"unicode": "e62f",
|
||||
"unicode_decimal": 58927
|
||||
},
|
||||
{
|
||||
"icon_id": "4318810",
|
||||
"name": "doc",
|
||||
"font_class": "doc",
|
||||
"unicode": "e6c2",
|
||||
"unicode_decimal": 59074
|
||||
},
|
||||
{
|
||||
"icon_id": "4318811",
|
||||
"name": "pdf",
|
||||
"font_class": "pdf",
|
||||
"unicode": "e6c3",
|
||||
"unicode_decimal": 59075
|
||||
},
|
||||
{
|
||||
"icon_id": "4628503",
|
||||
"name": "ppt",
|
||||
"font_class": "ppt",
|
||||
"unicode": "e642",
|
||||
"unicode_decimal": 58946
|
||||
},
|
||||
{
|
||||
"icon_id": "6233095",
|
||||
"name": "txt",
|
||||
"font_class": "txt",
|
||||
"unicode": "e644",
|
||||
"unicode_decimal": 58948
|
||||
},
|
||||
{
|
||||
"icon_id": "12600910",
|
||||
"name": "sql",
|
||||
"font_class": "sql",
|
||||
"unicode": "e65b",
|
||||
"unicode_decimal": 58971
|
||||
},
|
||||
{
|
||||
"icon_id": "31260974",
|
||||
"name": "md",
|
||||
"font_class": "md",
|
||||
"unicode": "e63a",
|
||||
"unicode_decimal": 58938
|
||||
},
|
||||
{
|
||||
"icon_id": "1278349",
|
||||
"name": "loading",
|
||||
"font_class": "loading",
|
||||
"unicode": "e627",
|
||||
"unicode_decimal": 58919
|
||||
},
|
||||
{
|
||||
"icon_id": "1486848",
|
||||
"name": "支付宝支付",
|
||||
"font_class": "alipay",
|
||||
"unicode": "e634",
|
||||
"unicode_decimal": 58932
|
||||
},
|
||||
{
|
||||
"icon_id": "845789",
|
||||
"name": "笑脸",
|
||||
"font_class": "face",
|
||||
"unicode": "e64b",
|
||||
"unicode_decimal": 58955
|
||||
},
|
||||
{
|
||||
"icon_id": "11836501",
|
||||
"name": "知识库",
|
||||
"font_class": "book",
|
||||
"unicode": "e622",
|
||||
"unicode_decimal": 58914
|
||||
},
|
||||
{
|
||||
"icon_id": "1105",
|
||||
"name": "认证用户",
|
||||
"font_class": "vip-user",
|
||||
"unicode": "e605",
|
||||
"unicode_decimal": 58885
|
||||
},
|
||||
{
|
||||
"icon_id": "1473442",
|
||||
"name": "VIP",
|
||||
"font_class": "vip",
|
||||
"unicode": "e688",
|
||||
"unicode_decimal": 59016
|
||||
},
|
||||
{
|
||||
"icon_id": "1306",
|
||||
"name": "购物车空",
|
||||
"font_class": "cart",
|
||||
"unicode": "e603",
|
||||
"unicode_decimal": 58883
|
||||
},
|
||||
{
|
||||
"icon_id": "1210",
|
||||
"name": "多素材",
|
||||
"font_class": "image-list",
|
||||
"unicode": "e601",
|
||||
"unicode_decimal": 58881
|
||||
},
|
||||
{
|
||||
"icon_id": "6438267",
|
||||
"name": "分享",
|
||||
"font_class": "share",
|
||||
"unicode": "e63e",
|
||||
"unicode_decimal": 58942
|
||||
},
|
||||
{
|
||||
"icon_id": "8361893",
|
||||
"name": "绘画",
|
||||
"font_class": "palette",
|
||||
"unicode": "e6da",
|
||||
"unicode_decimal": 59098
|
||||
},
|
||||
{
|
||||
"icon_id": "17832791",
|
||||
"name": "绘图、调色盘",
|
||||
"font_class": "palette-pen",
|
||||
"unicode": "e60c",
|
||||
"unicode_decimal": 58892
|
||||
},
|
||||
{
|
||||
"icon_id": "4766917",
|
||||
"name": "image",
|
||||
"font_class": "image",
|
||||
"unicode": "e7de",
|
||||
"unicode_decimal": 59358
|
||||
},
|
||||
{
|
||||
"icon_id": "1375",
|
||||
"name": "订单",
|
||||
"font_class": "order",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "6562297",
|
||||
"name": "客服",
|
||||
"font_class": "service",
|
||||
"unicode": "e62d",
|
||||
"unicode_decimal": 58925
|
||||
},
|
||||
{
|
||||
"icon_id": "21598358",
|
||||
"name": "喜欢",
|
||||
"font_class": "like",
|
||||
"unicode": "e640",
|
||||
"unicode_decimal": 58944
|
||||
},
|
||||
{
|
||||
"icon_id": "936873",
|
||||
"name": "充值1",
|
||||
"font_class": "recharge",
|
||||
"unicode": "e637",
|
||||
"unicode_decimal": 58935
|
||||
},
|
||||
{
|
||||
"icon_id": "18991679",
|
||||
"name": "model",
|
||||
"font_class": "model",
|
||||
"unicode": "e867",
|
||||
"unicode_decimal": 59495
|
||||
},
|
||||
{
|
||||
"icon_id": "5244045",
|
||||
"name": "插件",
|
||||
"font_class": "plugin",
|
||||
"unicode": "e69d",
|
||||
"unicode_decimal": 59037
|
||||
},
|
||||
{
|
||||
"icon_id": "8893244",
|
||||
"name": "高效率 copy",
|
||||
"font_class": "quick-start",
|
||||
"unicode": "e677",
|
||||
"unicode_decimal": 58999
|
||||
},
|
||||
{
|
||||
"icon_id": "16480872",
|
||||
"name": "插件功能",
|
||||
"font_class": "control",
|
||||
"unicode": "e69e",
|
||||
"unicode_decimal": 59038
|
||||
},
|
||||
{
|
||||
"icon_id": "22187612",
|
||||
"name": "缺陷管理",
|
||||
"font_class": "bug",
|
||||
"unicode": "e645",
|
||||
"unicode_decimal": 58949
|
||||
},
|
||||
{
|
||||
"icon_id": "4765958",
|
||||
"name": "export",
|
||||
"font_class": "export",
|
||||
"unicode": "e791",
|
||||
"unicode_decimal": 59281
|
||||
},
|
||||
{
|
||||
"icon_id": "6343824",
|
||||
"name": "menu",
|
||||
"font_class": "sub-menu",
|
||||
"unicode": "e86b",
|
||||
"unicode_decimal": 59499
|
||||
},
|
||||
{
|
||||
"icon_id": "1487626",
|
||||
"name": "微信支付",
|
||||
"font_class": "wechat-pay",
|
||||
"unicode": "e639",
|
||||
"unicode_decimal": 58937
|
||||
},
|
||||
{
|
||||
"icon_id": "7791019",
|
||||
"name": "捐赠",
|
||||
"font_class": "donate",
|
||||
"unicode": "e7ee",
|
||||
"unicode_decimal": 59374
|
||||
},
|
||||
{
|
||||
"icon_id": "14593731",
|
||||
"name": "打赏",
|
||||
"font_class": "reward",
|
||||
"unicode": "e6c7",
|
||||
"unicode_decimal": 59079
|
||||
},
|
||||
{
|
||||
"icon_id": "8623603",
|
||||
"name": "bell",
|
||||
"font_class": "bell",
|
||||
"unicode": "e887",
|
||||
"unicode_decimal": 59527
|
||||
},
|
||||
{
|
||||
"icon_id": "673799",
|
||||
"name": "首页",
|
||||
"font_class": "home",
|
||||
"unicode": "e6cb",
|
||||
"unicode_decimal": 59083
|
||||
},
|
||||
{
|
||||
"icon_id": "11808058",
|
||||
"name": "menu",
|
||||
"font_class": "menu",
|
||||
"unicode": "e6a6",
|
||||
"unicode_decimal": 59046
|
||||
},
|
||||
{
|
||||
"icon_id": "1166053",
|
||||
"name": "日志",
|
||||
"font_class": "log",
|
||||
"unicode": "e61c",
|
||||
"unicode_decimal": 58908
|
||||
},
|
||||
{
|
||||
"icon_id": "6151151",
|
||||
"name": "user-fill",
|
||||
"font_class": "user-fill",
|
||||
"unicode": "e7d5",
|
||||
"unicode_decimal": 59349
|
||||
},
|
||||
{
|
||||
"icon_id": "6172778",
|
||||
"name": "user-circle",
|
||||
"font_class": "user-circle",
|
||||
"unicode": "e860",
|
||||
"unicode_decimal": 59488
|
||||
},
|
||||
{
|
||||
"icon_id": "7821146",
|
||||
"name": "chart",
|
||||
"font_class": "chart",
|
||||
"unicode": "e631",
|
||||
"unicode_decimal": 58929
|
||||
},
|
||||
{
|
||||
"icon_id": "8430216",
|
||||
"name": "wechat",
|
||||
"font_class": "wechat",
|
||||
"unicode": "e621",
|
||||
"unicode_decimal": 58913
|
||||
},
|
||||
{
|
||||
"icon_id": "9769247",
|
||||
"name": "config",
|
||||
"font_class": "config",
|
||||
"unicode": "e612",
|
||||
"unicode_decimal": 58898
|
||||
},
|
||||
{
|
||||
"icon_id": "10055632",
|
||||
"name": "API密钥",
|
||||
"font_class": "api-key",
|
||||
"unicode": "e7d8",
|
||||
"unicode_decimal": 59352
|
||||
},
|
||||
{
|
||||
"icon_id": "11466716",
|
||||
"name": "mg-role",
|
||||
"font_class": "role",
|
||||
"unicode": "e60a",
|
||||
"unicode_decimal": 58890
|
||||
},
|
||||
{
|
||||
"icon_id": "4194141",
|
||||
"name": "首页dashboard",
|
||||
"font_class": "dashboard",
|
||||
"unicode": "e602",
|
||||
"unicode_decimal": 58882
|
||||
},
|
||||
{
|
||||
"icon_id": "611345",
|
||||
"name": "密码",
|
||||
"font_class": "password",
|
||||
"unicode": "e62a",
|
||||
"unicode_decimal": 58922
|
||||
},
|
||||
{
|
||||
"icon_id": "1418205",
|
||||
"name": "发送",
|
||||
"font_class": "send",
|
||||
"unicode": "e604",
|
||||
"unicode_decimal": 58884
|
||||
},
|
||||
{
|
||||
"icon_id": "1048893",
|
||||
"name": "注销",
|
||||
"font_class": "logout",
|
||||
"unicode": "e62e",
|
||||
"unicode_decimal": 58926
|
||||
},
|
||||
{
|
||||
"icon_id": "14401714",
|
||||
"name": "github",
|
||||
"font_class": "github",
|
||||
"unicode": "e66f",
|
||||
"unicode_decimal": 58991
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
new-ui/projects/web/src/assets/iconfont/iconfont.ttf
Normal file
BIN
new-ui/projects/web/src/assets/iconfont/iconfont.ttf
Normal file
Binary file not shown.
BIN
new-ui/projects/web/src/assets/iconfont/iconfont.woff
Normal file
BIN
new-ui/projects/web/src/assets/iconfont/iconfont.woff
Normal file
Binary file not shown.
BIN
new-ui/projects/web/src/assets/iconfont/iconfont.woff2
Normal file
BIN
new-ui/projects/web/src/assets/iconfont/iconfont.woff2
Normal file
Binary file not shown.
BIN
new-ui/projects/web/src/assets/img/login-bg.jpg
Normal file
BIN
new-ui/projects/web/src/assets/img/login-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
new-ui/projects/web/src/assets/img/reg-bg.jpg
Normal file
BIN
new-ui/projects/web/src/assets/img/reg-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
352
new-ui/projects/web/src/components/CaptchaPlus.vue
Normal file
352
new-ui/projects/web/src/components/CaptchaPlus.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
|
||||
<div class="wg-cap-wrap" :style="{width: width}">
|
||||
<div class="wg-cap-wrap__header">
|
||||
<span>请在下图<em>依次</em>点击:</span>
|
||||
<img class="wg-cap-wrap__thumb" v-if="thumbBase64Code" :src="thumbBase64Code" alt=" ">
|
||||
</div>
|
||||
<div class="wg-cap-wrap__body">
|
||||
<img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" "
|
||||
@click="handleClickPos($event)">
|
||||
<img class="wg-cap-wrap__loading"
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+"
|
||||
alt="正在加载中...">
|
||||
<div v-for="(dot, key) in dots" :key="key" class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`">
|
||||
<span>{{ dot.index }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wg-cap-wrap__footer">
|
||||
<div class="wg-cap-wrap__ico">
|
||||
<img @click="handleCloseEvent"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg=="
|
||||
alt="关闭">
|
||||
<img @click="handleRefreshEvent"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4="
|
||||
alt="刷新">
|
||||
</div>
|
||||
<div class="wg-cap-wrap__btn">
|
||||
<el-button type="primary" @click="handleConfirmEvent">确认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CaptchaPlus',
|
||||
mounted() {
|
||||
this.$emit('refresh')
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
width: {
|
||||
type: String,
|
||||
default: '300px'
|
||||
},
|
||||
calcPosType: {
|
||||
type: String,
|
||||
default: 'dom',
|
||||
validator: value => ['dom', 'screen'].includes(value)
|
||||
},
|
||||
maxDot: {
|
||||
type: Number,
|
||||
default: 5
|
||||
// validator: value => value > 10
|
||||
},
|
||||
imageBase64: String,
|
||||
thumbBase64: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dots: [],
|
||||
imageBase64Code: '',
|
||||
thumbBase64Code: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.dots = []
|
||||
this.imageBase64Code = ''
|
||||
this.thumbBase64Code = ''
|
||||
},
|
||||
imageBase64(val) {
|
||||
this.dots = []
|
||||
this.imageBase64Code = val
|
||||
},
|
||||
thumbBase64(val) {
|
||||
this.dots = []
|
||||
this.thumbBase64Code = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @Description: 处理关闭事件
|
||||
*/
|
||||
handleCloseEvent() {
|
||||
this.$emit('close')
|
||||
// this.dots = []
|
||||
// this.imageBase64Code = ''
|
||||
// this.thumbBase64Code = ''
|
||||
},
|
||||
/**
|
||||
* @Description: 处理刷新事件
|
||||
*/
|
||||
handleRefreshEvent() {
|
||||
this.dots = []
|
||||
this.$emit('refresh')
|
||||
},
|
||||
/**
|
||||
* @Description: 处理确认事件
|
||||
*/
|
||||
handleConfirmEvent() {
|
||||
this.$emit('confirm', this.dots)
|
||||
},
|
||||
/**
|
||||
* @Description: 处理dot
|
||||
* @param ev
|
||||
*/
|
||||
handleClickPos(ev) {
|
||||
if (this.dots.length >= this.maxDot) {
|
||||
return
|
||||
}
|
||||
const e = ev || window.event
|
||||
e.preventDefault()
|
||||
const dom = e.currentTarget
|
||||
|
||||
const {domX, domY} = this.getDomXY(dom)
|
||||
// ===============================================
|
||||
// @notice 如 getDomXY 不准确可尝试使用 calcLocationLeft 或 calcLocationTop
|
||||
// const domX = this.calcLocationLeft(dom)
|
||||
// const domY = this.calcLocationTop(dom)
|
||||
// ===============================================
|
||||
let mouseX = (navigator.vendor === 'Netscape') ? e.pageX : e.x + document.body.offsetTop
|
||||
let mouseY = (navigator.vendor === 'Netscape') ? e.pageY : e.y + document.body.offsetTop
|
||||
// 兼容移动触摸事件
|
||||
if (e.touches && e.touches.length > 0) {
|
||||
mouseX = e.touches[0].clientX
|
||||
mouseY = e.touches[0].clientY
|
||||
} else {
|
||||
mouseX = e.clientX
|
||||
mouseY = e.clientY
|
||||
}
|
||||
|
||||
// 计算点击的相对位置
|
||||
const xPos = mouseX - domX
|
||||
const yPos = mouseY - domY
|
||||
|
||||
// 转整形
|
||||
const xp = parseInt(xPos.toString())
|
||||
const yp = parseInt(yPos.toString())
|
||||
|
||||
// 减去点的一半
|
||||
this.dots.push({
|
||||
x: xp - 11,
|
||||
y: yp - 11,
|
||||
index: this.dots.length + 1
|
||||
})
|
||||
return false
|
||||
},
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param el
|
||||
*/
|
||||
calcLocationLeft(el) {
|
||||
let tmp = el.offsetLeft
|
||||
let val = el.offsetParent
|
||||
while (val != null) {
|
||||
tmp += val.offsetLeft
|
||||
val = val.offsetParent
|
||||
}
|
||||
return tmp
|
||||
},
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param el
|
||||
*/
|
||||
calcLocationTop(el) {
|
||||
let tmp = el.offsetTop
|
||||
let val = el.offsetParent
|
||||
while (val != null) {
|
||||
tmp += val.offsetTop
|
||||
val = val.offsetParent
|
||||
}
|
||||
return tmp
|
||||
},
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param dom
|
||||
*/
|
||||
getDomXY(dom) {
|
||||
let x = 0
|
||||
let y = 0
|
||||
if (dom.getBoundingClientRect) {
|
||||
let box = dom.getBoundingClientRect();
|
||||
let D = document.documentElement;
|
||||
x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
|
||||
y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop
|
||||
} else {
|
||||
while (dom !== document.body) {
|
||||
x += dom.offsetLeft
|
||||
y += dom.offsetTop
|
||||
dom = dom.offsetParent
|
||||
}
|
||||
}
|
||||
return {
|
||||
domX: x,
|
||||
domY: y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.wg-cap-wrap {
|
||||
background: #ffffff;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.wg-cap-wrap__header {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
padding-right: 5px;
|
||||
|
||||
em {
|
||||
padding: 0 3px;
|
||||
font-weight: bold;
|
||||
color: #3e7cff;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-cap-wrap__image {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__thumb {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__thumb.wg-cap-wrap__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.wg-cap-wrap__body {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
background: #34383e;
|
||||
margin: auto;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.wg-cap-wrap__picture {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__picture.wg-cap-wrap__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__loading {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
margin-left: -34px;
|
||||
margin-top: -34px;
|
||||
line-height: 68px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__dot {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #cedffe;
|
||||
background: #3e7cff;
|
||||
border: 2px solid #f7f9fb;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
-webkit-border-radius: 22px;
|
||||
-moz-border-radius: 22px;
|
||||
border-radius: 22px;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-cap-wrap__footer {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: #34383e;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding-top: 15px;
|
||||
|
||||
.wg-cap-wrap__ico {
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #34383e;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-cap-wrap__btn {
|
||||
display flex
|
||||
width: 120px;
|
||||
justify-content right
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
244
new-ui/projects/web/src/components/ChatMidJourney.vue
Normal file
244
new-ui/projects/web/src/components/ChatMidJourney.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-mj" v-loading="loading">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="User"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content">
|
||||
<div class="text" v-html="data.html"></div>
|
||||
<div class="images" v-if="data.image?.url !== ''">
|
||||
<el-image :src="data.image?.url"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[data.image?.url]"
|
||||
fit="cover"
|
||||
:initial-index="0" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot"
|
||||
:style="{height: height+'px', lineHeight:height+'px'}">
|
||||
正在加载图片<span class="dot">...</span></div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="upscale(1)">U1</a></li>
|
||||
<li><a @click="upscale(2)">U2</a></li>
|
||||
<li><a @click="upscale(3)">U3</a></li>
|
||||
<li><a @click="upscale(4)">U4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="variation(1)">V1</a></li>
|
||||
<li><a @click="variation(2)">V2</a></li>
|
||||
<li><a @click="variation(3)">V3</a></li>
|
||||
<li><a @click="variation(4)">V4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bar" v-if="createdAt !== ''">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<span class="bar-item">tokens: {{ tokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {Clock, Picture} from "@element-plus/icons-vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {getSessionId} from "@/store/session";
|
||||
|
||||
const props = defineProps({
|
||||
content: Object,
|
||||
icon: String,
|
||||
chatId: String,
|
||||
roleId: Number,
|
||||
createdAt: String
|
||||
});
|
||||
|
||||
const data = ref(props.content)
|
||||
const tokens = ref(0)
|
||||
const cacheKey = "img_placeholder_height"
|
||||
const item = localStorage.getItem(cacheKey);
|
||||
const loading = ref(false)
|
||||
const height = ref(0)
|
||||
if (item) {
|
||||
height.value = parseInt(item)
|
||||
}
|
||||
if (data.value["image"]?.width > 0) {
|
||||
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
|
||||
localStorage.setItem(cacheKey, height.value)
|
||||
}
|
||||
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
|
||||
// console.log(data.value)
|
||||
|
||||
watch(() => props.content, (newVal) => {
|
||||
data.value = newVal;
|
||||
});
|
||||
const emits = defineEmits(['disable-input', 'disable-input']);
|
||||
const upscale = (index) => {
|
||||
send('/api/mj/upscale', index)
|
||||
}
|
||||
|
||||
const variation = (index) => {
|
||||
send('/api/mj/variation', index)
|
||||
}
|
||||
|
||||
const send = (url, index) => {
|
||||
loading.value = true
|
||||
emits('disable-input')
|
||||
httpPost(url, {
|
||||
index: index,
|
||||
src: "chat",
|
||||
message_id: data.value?.["message_id"],
|
||||
message_hash: data.value?.["image"]?.hash,
|
||||
session_id: getSessionId(),
|
||||
prompt: data.value?.["prompt"],
|
||||
chat_id: props.chatId,
|
||||
role_id: props.roleId,
|
||||
icon: props.icon,
|
||||
}).then(() => {
|
||||
ElMessage.success("任务推送成功,请耐心等待任务执行...")
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + e.message)
|
||||
emits('disable-input')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-mj {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
.text {
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
max-width 350px;
|
||||
|
||||
.el-image {
|
||||
border-radius 10px;
|
||||
|
||||
.image-slot {
|
||||
color #c1c1c1
|
||||
width 350px
|
||||
text-align center
|
||||
border-radius 10px;
|
||||
border 1px solid #e1e1e1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
padding-left 10px
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 6px 0
|
||||
width 64px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
150
new-ui/projects/web/src/components/ChatPrompt.vue
Normal file
150
new-ui/projects/web/src/components/ChatPrompt.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-prompt">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {Clock} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.finalTokens) {
|
||||
httpPost("/api/chat/tokens", {text: this.content, model: this.model}).then(res => {
|
||||
this.finalTokens = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</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;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
248
new-ui/projects/web/src/components/ChatReply.vue
Normal file
248
new-ui/projects/web/src/components/ChatReply.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-reply">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="ChatGPT">
|
||||
</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: {{ tokens }}</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent">
|
||||
<el-icon>
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {Clock, DocumentCopy, Position} from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatReply',
|
||||
components: {Position, Clock, DocumentCopy},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
orgContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tokens: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'images/gpt-icon.png',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
finalTokens: this.tokens
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.common-layout {
|
||||
.chat-line-reply {
|
||||
justify-content: center;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
.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 50px
|
||||
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;
|
||||
|
||||
.bar-item {
|
||||
background-color #e7e7e8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.tool-box {
|
||||
font-size 16px;
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
92
new-ui/projects/web/src/components/ConfigDialog.vue
Normal file
92
new-ui/projects/web/src/components/ConfigDialog.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="config-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:before-close="close"
|
||||
style="max-width: 600px"
|
||||
title="账户信息"
|
||||
>
|
||||
<div class="user-info" id="user-info">
|
||||
<el-form v-if="user.id" :model="user" label-width="150px">
|
||||
<el-form-item label="账户">
|
||||
<span>{{ user.username }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余算力">
|
||||
<el-tag>{{ user['power'] }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
|
||||
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import Compressor from "compressorjs";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
user: Object,
|
||||
models: Array,
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const user = ref({
|
||||
username: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
calls: 0,
|
||||
tokens: 0,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 获取最新用户信息
|
||||
httpGet('/api/user/profile').then(res => {
|
||||
user.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取用户信息失败:" + e.message)
|
||||
});
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['hide']);
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.config-dialog {
|
||||
.el-dialog {
|
||||
--el-dialog-width 90%;
|
||||
max-width 800px;
|
||||
|
||||
.el-dialog__body {
|
||||
overflow-y auto;
|
||||
|
||||
.user-info {
|
||||
position relative;
|
||||
|
||||
.el-message {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
color #c1c1c1
|
||||
font-size 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
new-ui/projects/web/src/components/CountDown.vue
Normal file
100
new-ui/projects/web/src/components/CountDown.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<!-- 倒计时组件 -->
|
||||
<div class="countdown">
|
||||
<el-tag size="large" :type="type">{{ timerStr }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
second: Number,
|
||||
type: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['timeout']);
|
||||
const counter = ref(props.second)
|
||||
const timerStr = ref("")
|
||||
const handler = ref(null)
|
||||
|
||||
watch(() => props.second, (newVal) => {
|
||||
counter.value = newVal
|
||||
resetTimer()
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
resetTimer()
|
||||
})
|
||||
|
||||
const resetTimer = () => {
|
||||
if (handler.value) {
|
||||
clearInterval(handler.value)
|
||||
}
|
||||
|
||||
counter.value = props.second
|
||||
formatTimer(counter.value)
|
||||
handler.value = setInterval(() => {
|
||||
formatTimer(counter.value)
|
||||
if (counter.value === 0) {
|
||||
clearInterval(handler.value)
|
||||
emits("timeout")
|
||||
}
|
||||
counter.value--
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const formatTimer = (secs) => {
|
||||
const timer = []
|
||||
let hour, min
|
||||
// 计算小时
|
||||
if (secs > 3600) {
|
||||
hour = Math.floor(secs / 3600)
|
||||
if (hour < 10) {
|
||||
hour = "0" + hour
|
||||
}
|
||||
secs = secs % 3600
|
||||
timer.push(hour + " 时 ")
|
||||
} else {
|
||||
timer.push("00 时 ")
|
||||
}
|
||||
// 计算分钟
|
||||
if (secs > 60) {
|
||||
min = Math.floor(secs / 60)
|
||||
if (min < 10) {
|
||||
min = "0" + min
|
||||
}
|
||||
secs = secs % 60
|
||||
timer.push(min + " 分 ")
|
||||
} else {
|
||||
timer.push("00 分 ")
|
||||
}
|
||||
// 计算秒数
|
||||
if (secs < 10) {
|
||||
secs = "0" + secs
|
||||
}
|
||||
timer.push(secs + " 秒")
|
||||
timerStr.value = timer.join("")
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
defineExpose({resetTimer})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.countdown {
|
||||
display flex
|
||||
|
||||
.el-tag--large {
|
||||
.el-tag__content {
|
||||
font-size 14px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
199
new-ui/projects/web/src/components/FileSelect.vue
Normal file
199
new-ui/projects/web/src/components/FileSelect.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<el-container class="file-list-box">
|
||||
<el-tooltip class="box-item" effect="dark" content="打开文件管理中心">
|
||||
<el-button class="file-upload-img" @click="fetchFiles">
|
||||
<el-icon>
|
||||
<PictureFilled/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
:width="800"
|
||||
title="文件管理"
|
||||
>
|
||||
|
||||
<div class="file-list">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="3">
|
||||
<div class="grid-content">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="3" v-for="file in fileList" :key="file.url">
|
||||
<div class="grid-content">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
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-tooltip>
|
||||
|
||||
<div class="opt">
|
||||
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {Delete, PictureFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem} from "@/utils/libs";
|
||||
|
||||
const props = defineProps({
|
||||
userId: Number,
|
||||
});
|
||||
const emits = defineEmits(['selected']);
|
||||
const show = ref(false)
|
||||
const fileList = ref([])
|
||||
|
||||
const fetchFiles = () => {
|
||||
show.value = true
|
||||
httpGet("/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();
|
||||
formData.append('file', file.file, file.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
fileList.value.unshift(res.data)
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
};
|
||||
|
||||
const removeFile = (file) => {
|
||||
httpGet('/api/upload/remove?id=' + file.id).then(() => {
|
||||
fileList.value = removeArrayItem(fileList.value, file, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
ElMessage.success("文件删除成功!")
|
||||
}).catch((e) => {
|
||||
ElMessage.error('文件删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const insertURL = (url) => {
|
||||
show.value = false
|
||||
emits('selected', url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.file-list-box {
|
||||
.file-upload-img {
|
||||
padding: 8px 5px;
|
||||
border-radius: 6px;
|
||||
background: #19c37d;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
|
||||
.el-dialog__body {
|
||||
//padding 0
|
||||
|
||||
.file-list {
|
||||
|
||||
.grid-content {
|
||||
margin-bottom 10px
|
||||
position relative
|
||||
|
||||
.avatar-uploader {
|
||||
width 100%
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border 1px dashed #e1e1e1
|
||||
border-radius 6px
|
||||
|
||||
.el-upload {
|
||||
width 100%
|
||||
height 80px
|
||||
}
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
height 80px
|
||||
border 1px solid #ffffff
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
|
||||
&:hover {
|
||||
border 1px solid #20a0ff
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color #20a0ff
|
||||
font-size 40px
|
||||
}
|
||||
|
||||
.opt {
|
||||
display none
|
||||
position absolute
|
||||
top 5px
|
||||
right 5px
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.opt {
|
||||
display block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
new-ui/projects/web/src/components/FooterBar.vue
Normal file
36
new-ui/projects/web/src/components/FooterBar.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="footer">
|
||||
Powered by {{ author }} @
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">{{ title }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
const author = ref('极客学长')
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display flex;
|
||||
justify-content center
|
||||
|
||||
.footer {
|
||||
max-width 400px;
|
||||
text-align center;
|
||||
font-size 14px;
|
||||
padding 20px;
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
94
new-ui/projects/web/src/components/InviteList.vue
Normal file
94
new-ui/projects/web/src/components/InviteList.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="invite-list" v-loading="loading">
|
||||
<el-row v-if="items.length > 0">
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
|
||||
style="--el-table-border-color:#373C47;
|
||||
--el-table-tr-bg-color:#2D323B;
|
||||
--el-table-row-hover-bg-color:#373C47;
|
||||
--el-table-header-bg-color:#474E5C;
|
||||
--el-table-text-color:#d1d1d1">
|
||||
<el-table-column prop="username" label="用户"/>
|
||||
<el-table-column prop="invite_code" label="邀请码"/>
|
||||
<el-table-column prop="remark" label="邀请奖励"/>
|
||||
|
||||
<el-table-column label="注册时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData()"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {DocumentCopy} from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
const items = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
const clipboard = new Clipboard('.copy-order-no');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpPost('/api/invite/list', {page: page.value, page_size: pageSize.value}).then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.invite-list {
|
||||
.pagination {
|
||||
margin: 20px 0 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.copy-order-no {
|
||||
cursor pointer
|
||||
position relative
|
||||
left 6px
|
||||
top 2px
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
new-ui/projects/web/src/components/ItemList.vue
Normal file
81
new-ui/projects/web/src/components/ItemList.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="list-box" ref="container">
|
||||
<div class="list-inner">
|
||||
<div
|
||||
class="list-item"
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:style="{width:itemWidth + 'px'}"
|
||||
>
|
||||
<div class="item-inner" :style="{padding: gap/2+'px'}">
|
||||
<div class="item-wrapper">
|
||||
<slot :item="item" :index="index" :width="itemWidth"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 列表组件
|
||||
import {onMounted, ref} from "vue";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
gap: {
|
||||
type: Number,
|
||||
default: 12
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 240
|
||||
}
|
||||
});
|
||||
|
||||
const container = ref(null)
|
||||
const itemWidth = ref(props.width)
|
||||
|
||||
onMounted(() => {
|
||||
computeSize()
|
||||
})
|
||||
|
||||
const computeSize = () => {
|
||||
const w = container.value.offsetWidth - 10 // 减去滚动条的宽度
|
||||
let cols = Math.floor(w / props.width)
|
||||
itemWidth.value = Math.floor(w / cols)
|
||||
}
|
||||
|
||||
window.onresize = () => {
|
||||
computeSize()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
|
||||
.list-box {
|
||||
|
||||
.list-inner {
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
|
||||
.list-item {
|
||||
.item-inner {
|
||||
display flex
|
||||
|
||||
.item-wrapper {
|
||||
height 100%
|
||||
width 100%
|
||||
display flex
|
||||
justify-content center
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
116
new-ui/projects/web/src/components/LoginDialog.vue
Normal file
116
new-ui/projects/web/src/components/LoginDialog.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="login-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
:before-close="close"
|
||||
:width="400"
|
||||
title="用户登录"
|
||||
>
|
||||
<div class="form">
|
||||
<el-form label-width="75px">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label">
|
||||
<el-icon>
|
||||
<User/>
|
||||
</el-icon>
|
||||
<span>账号</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-input v-model="username" size="large" placeholder="手机号码"/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label">
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
<span>密码</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-input v-model="password" type="password" size="large" placeholder="密码"/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<div class="login-btn">
|
||||
<el-button type="primary" @click="submit" size="large" round>登录</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue"
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {setUserToken} from "@/store/session";
|
||||
import {validateMobile} from "@/utils/validate";
|
||||
import {Lock, User} from "@element-plus/icons-vue";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const username = ref("")
|
||||
const password = ref("")
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['hide']);
|
||||
const submit = function () {
|
||||
if (!validateMobile(username.value)) {
|
||||
return ElMessage.error('请输入合法的手机号');
|
||||
}
|
||||
if (password.value.trim() === '') {
|
||||
return ElMessage.error('请输入密码');
|
||||
}
|
||||
|
||||
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
||||
setUserToken(res.data)
|
||||
ElMessage.success("登录成功!")
|
||||
emits("hide")
|
||||
}).catch((e) => {
|
||||
ElMessage.error('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.login-dialog {
|
||||
border-radius 20px
|
||||
|
||||
.label {
|
||||
padding-top 3px
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
font-size 20px
|
||||
margin-right 6px
|
||||
top 4px
|
||||
}
|
||||
|
||||
span {
|
||||
font-size 16px
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
text-align center
|
||||
padding-top 10px
|
||||
|
||||
.el-button {
|
||||
width 50%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
new-ui/projects/web/src/components/PasswordDialog.vue
Normal file
94
new-ui/projects/web/src/components/PasswordDialog.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="password-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
style="max-width: 600px"
|
||||
:before-close="close"
|
||||
title="修改密码"
|
||||
>
|
||||
<div class="form" id="password-form">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="原始密码">
|
||||
<el-input v-model="form['old_pass']" type="password"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="form['password']" type="password"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input v-model="form['repass']" type="password"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
<el-button type="primary" @click="save">
|
||||
保存
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue"
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const form = ref({})
|
||||
const emits = defineEmits(['hide', 'logout']);
|
||||
const save = function () {
|
||||
if (!form.value['password'] || form.value['password'].length < 8) {
|
||||
return ElMessage.error({message: "密码的长度为8-16个字符", appendTo: "#password-form"});
|
||||
}
|
||||
if (form.value['repass'] !== form.value['password']) {
|
||||
return ElMessage.error({message: '两次输入密码不一致', appendTo: '#password-form'});
|
||||
}
|
||||
httpPost('/api/user/password', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '更新成功',
|
||||
appendTo: '#password-form',
|
||||
duration: 1000,
|
||||
onClose: () => emits('logout', false)
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error({
|
||||
message: '更新失败,' + e.message,
|
||||
appendTo: '#password-form'
|
||||
})
|
||||
})
|
||||
}
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.password-dialog {
|
||||
.el-dialog {
|
||||
--el-dialog-width 90%;
|
||||
max-width 650px;
|
||||
|
||||
.el-dialog__body {
|
||||
overflow-y auto;
|
||||
|
||||
.form {
|
||||
position relative;
|
||||
|
||||
.el-message {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
new-ui/projects/web/src/components/ResetAccount.vue
Normal file
97
new-ui/projects/web/src/components/ResetAccount.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
style="max-width: 600px"
|
||||
:before-close="close"
|
||||
:title="title"
|
||||
>
|
||||
<div class="form" id="bind-mobile-form">
|
||||
<el-alert v-if="username !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
|
||||
<p>当前绑定账号:{{ username }},只允许使绑定有效的手机号或者邮箱地址作为登录账号。</p>
|
||||
</el-alert>
|
||||
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="新账号">
|
||||
<el-input v-model="form.username"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-input v-model="form.code" maxlength="6"/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<send-msg size="" :receiver="form.username"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="save">
|
||||
提交绑定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
username: String
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
|
||||
const title = ref('重置登录账号')
|
||||
const form = ref({
|
||||
username: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const save = () => {
|
||||
if (!validateMobile(form.value.username) && !validateEmail(form.value.username)) {
|
||||
return ElMessage.error("请输入合法的手机号/邮箱地址")
|
||||
}
|
||||
if (form.value.code === '') {
|
||||
return ElMessage.error("请输入验证码");
|
||||
}
|
||||
|
||||
httpPost('/api/user/bind/username', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '绑定成功',
|
||||
duration: 1000,
|
||||
onClose: () => emits('hide', false)
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error("绑定失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
#bind-mobile-form {
|
||||
.el-form-item__content {
|
||||
.el-row {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
new-ui/projects/web/src/components/ResetPass.vue
Normal file
108
new-ui/projects/web/src/components/ResetPass.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="reset-pass">
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
width="540px"
|
||||
:before-close="close"
|
||||
:title="title"
|
||||
>
|
||||
<div class="form">
|
||||
|
||||
<el-form :model="form" label-width="120px" label-position="left">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" placeholder="手机号/邮箱地址"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-input v-model="form.code" maxlength="6"/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<send-msg size="" :receiver="form.username"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="form.password" type="password"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="重复密码">
|
||||
<el-input v-model="form.repass" type="password"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="save" round>
|
||||
重置密码
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
mobile: String
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
|
||||
const title = ref('重置密码')
|
||||
const form = ref({
|
||||
username: '',
|
||||
code: '',
|
||||
password: '',
|
||||
repass: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const save = () => {
|
||||
if (!validateMobile(form.value.username) && !validateEmail(form.value.username)) {
|
||||
return ElMessage.error("请输入正确的手机号码/邮箱地址");
|
||||
}
|
||||
if (form.value.code === '') {
|
||||
return ElMessage.error("请输入验证码");
|
||||
}
|
||||
if (form.value.repass !== form.value.password) {
|
||||
return ElMessage.error("两次输入密码不一致");
|
||||
}
|
||||
|
||||
httpPost('/api/user/resetPass', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '重置密码成功', duration: 1000, onClose: () => emits('hide', false)
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error("重置密码失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.reset-pass {
|
||||
.form {
|
||||
padding 10px 40px
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
text-align center
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
86
new-ui/projects/web/src/components/RewardVerify.vue
Normal file
86
new-ui/projects/web/src/components/RewardVerify.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="mobile !== ''"
|
||||
:before-close="close"
|
||||
:width="450"
|
||||
:title="title"
|
||||
>
|
||||
<div class="form" id="bind-mobile-form">
|
||||
<el-alert v-if="mobile !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
|
||||
<p>请输入您参与众筹的 <strong style="color:#F56C6C">微信支付转账单号</strong> 兑换相应的对话次数。</p>
|
||||
</el-alert>
|
||||
|
||||
<el-form :model="form">
|
||||
<el-form-item label="转账单号">
|
||||
<el-input v-model="form.tx_id"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form :model="form">
|
||||
<el-form-item label="兑换类别">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio label="chat" border>对话聊天</el-radio>
|
||||
<el-radio label="img" border>AI绘图</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="save">
|
||||
确认核销
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
mobile: String
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
|
||||
const title = ref('众筹码核销')
|
||||
const form = ref({
|
||||
tx_id: '',
|
||||
type: 'chat'
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const save = () => {
|
||||
if (form.value.tx_id === '') {
|
||||
return ElMessage.error({message: "请输入微信支付转账单号"});
|
||||
}
|
||||
|
||||
httpPost('/api/reward/verify', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '核销成功',
|
||||
duration: 1000,
|
||||
onClose: () => location.reload()
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error({message: "核销失败:" + e.message});
|
||||
})
|
||||
}
|
||||
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
138
new-ui/projects/web/src/components/SendMsg.vue
Normal file
138
new-ui/projects/web/src/components/SendMsg.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<el-container class="captcha-box">
|
||||
<el-button type="primary" class="send-btn" :size="props.size" :disabled="!canSend" @click="loadCaptcha" plain>
|
||||
{{ btnText }}
|
||||
</el-button>
|
||||
|
||||
<el-dialog
|
||||
v-model="showCaptcha"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="false"
|
||||
style="width:90%;max-width: 360px;"
|
||||
>
|
||||
<captcha-plus
|
||||
:max-dot="maxDot"
|
||||
:image-base64="imageBase64"
|
||||
:thumb-base64="thumbBase64"
|
||||
width="300"
|
||||
@close="showCaptcha = false"
|
||||
@refresh="handleRequestCaptCode"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 发送短信验证码组件
|
||||
import {ref} from "vue";
|
||||
import lodash from 'lodash'
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import CaptchaPlus from "@/components/CaptchaPlus.vue";
|
||||
|
||||
const props = defineProps({
|
||||
receiver: String,
|
||||
size: String,
|
||||
});
|
||||
const btnText = ref('发送验证码')
|
||||
const canSend = ref(true)
|
||||
const showCaptcha = ref(false)
|
||||
const maxDot = ref(5)
|
||||
const imageBase64 = ref('')
|
||||
const thumbBase64 = ref('')
|
||||
const captKey = ref('')
|
||||
const dots = ref(null)
|
||||
|
||||
const handleRequestCaptCode = () => {
|
||||
|
||||
httpGet('/api/captcha/get').then(res => {
|
||||
const data = res.data
|
||||
imageBase64.value = data.image
|
||||
thumbBase64.value = data.thumb
|
||||
captKey.value = data.key
|
||||
}).catch(e => {
|
||||
ElMessage.error('获取人机验证数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirm = (dots) => {
|
||||
if (lodash.size(dots) <= 0) {
|
||||
return ElMessage.error('请进行人机验证再操作')
|
||||
}
|
||||
|
||||
let dotArr = []
|
||||
lodash.forEach(dots, (dot) => {
|
||||
dotArr.push(dot.x, dot.y)
|
||||
})
|
||||
dots.value = dotArr.join(',')
|
||||
httpPost('/api/captcha/check', {
|
||||
dots: dots.value,
|
||||
key: captKey.value
|
||||
}).then(() => {
|
||||
// ElMessage.success('人机验证成功')
|
||||
showCaptcha.value = false
|
||||
sendMsg()
|
||||
}).catch(() => {
|
||||
ElMessage.error('人机验证失败')
|
||||
handleRequestCaptCode()
|
||||
})
|
||||
}
|
||||
|
||||
const loadCaptcha = () => {
|
||||
if (!validateMobile(props.receiver) && !validateEmail(props.receiver)) {
|
||||
return ElMessage.error("请输入合法的手机号/邮箱地址")
|
||||
}
|
||||
|
||||
showCaptcha.value = true
|
||||
handleRequestCaptCode() // 每次点开都刷新验证码
|
||||
}
|
||||
|
||||
const sendMsg = () => {
|
||||
if (!canSend.value) {
|
||||
return
|
||||
}
|
||||
|
||||
canSend.value = false
|
||||
httpPost('/api/sms/code', {receiver: props.receiver, key: captKey.value, dots: dots.value}).then(() => {
|
||||
ElMessage.success('验证码发送成功')
|
||||
let time = 120
|
||||
btnText.value = time
|
||||
const handler = setInterval(() => {
|
||||
time = time - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(handler)
|
||||
btnText.value = '重新发送'
|
||||
canSend.value = true
|
||||
} else {
|
||||
btnText.value = time
|
||||
}
|
||||
}, 1000)
|
||||
}).catch(e => {
|
||||
canSend.value = true
|
||||
ElMessage.error('验证码发送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.captcha-box {
|
||||
.send-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
.el-dialog__header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
//padding 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
new-ui/projects/web/src/components/UserOrder.vue
Normal file
107
new-ui/projects/web/src/components/UserOrder.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="user-bill" v-loading="loading">
|
||||
<el-row v-if="items.length > 0">
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
|
||||
style="--el-table-border-color:#373C47;
|
||||
--el-table-tr-bg-color:#2D323B;
|
||||
--el-table-row-hover-bg-color:#373C47;
|
||||
--el-table-header-bg-color:#474E5C;
|
||||
--el-table-text-color:#d1d1d1">
|
||||
<el-table-column prop="order_no" label="订单号">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.order_no }}</span>
|
||||
<el-icon class="copy-order-no" :data-clipboard-text="scope.row.order_no">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="subject" label="产品名称"/>
|
||||
<el-table-column prop="amount" label="订单金额"/>
|
||||
<el-table-column label="订单算力">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.remark?.power }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="支付时间">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>
|
||||
<el-tag v-else>未支付</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData()"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {DocumentCopy} from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
const items = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
const clipboard = new Clipboard('.copy-order-no');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpPost('/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
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.user-bill {
|
||||
.pagination {
|
||||
margin: 20px 0 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.copy-order-no {
|
||||
cursor pointer
|
||||
position relative
|
||||
left 6px
|
||||
top 2px
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
new-ui/projects/web/src/components/UserProfile.vue
Normal file
125
new-ui/projects/web/src/components/UserProfile.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="user-info" id="user-info">
|
||||
<el-form v-if="user.id" :model="user" label-width="150px">
|
||||
<el-row>
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
accept=".png,.jpg,.jpeg,.bmp"
|
||||
>
|
||||
<el-avatar v-if="user.avatar" :src="user.avatar" shape="circle" :size="100"/>
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-row>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="user['nickname']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="账号">
|
||||
<span>{{ user.username }}</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="您已经是 VIP 会员"
|
||||
placement="right"
|
||||
>
|
||||
<el-image v-if="user.vip" :src="vipImg" style="height: 25px;margin-left: 10px"/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余算力">
|
||||
<el-tag>{{ user['power'] }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
|
||||
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-row class="opt-line">
|
||||
<el-button color="#47fff1" :dark="false" round @click="save">保存</el-button>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import Compressor from "compressorjs";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {checkSession} from "@/action/session";
|
||||
|
||||
const user = ref({
|
||||
vip: false,
|
||||
username: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
mobile: '',
|
||||
power: 0,
|
||||
})
|
||||
const vipImg = ref("/images/vip.png")
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
// 获取最新用户信息
|
||||
httpGet('/api/user/profile').then(res => {
|
||||
user.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取用户信息失败:" + e.message)
|
||||
});
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
})
|
||||
})
|
||||
|
||||
const afterRead = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
user.value.avatar = res.data.url
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
httpPost('/api/user/profile/update', user.value).then(() => {
|
||||
ElMessage.success({message: '更新成功', duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('更新失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.user-info {
|
||||
padding 20px
|
||||
|
||||
.el-row {
|
||||
justify-content center
|
||||
margin-bottom 10px
|
||||
}
|
||||
|
||||
.opt-line {
|
||||
padding-top 20px
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
167
new-ui/projects/web/src/components/Welcome.vue
Normal file
167
new-ui/projects/web/src/components/Welcome.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">
|
||||
<div class="item-title">
|
||||
<div><i class="iconfont icon-quick-start"></i></div>
|
||||
<div>小试牛刀</div>
|
||||
</div>
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in samples" :key="item"><a @click="send(item)">{{ item }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">
|
||||
<div class="item-title">
|
||||
<div><i class="iconfont icon-plugin"></i></div>
|
||||
<div>插件增强</div>
|
||||
</div>
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in plugins" :key="item.value"><a @click="send(item.value)">{{ item.text }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">
|
||||
<div class="item-title">
|
||||
<div><i class="iconfont icon-control"></i></div>
|
||||
<div>能力扩展</div>
|
||||
</div>
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in capabilities" :key="item">
|
||||
<span v-if="item.value === ''">{{ item.text }}</span>
|
||||
<a @click="send(item.value)" v-else>{{ item.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
|
||||
const samples = ref([
|
||||
"用小学生都能听懂的术语解释什么是量子纠缠",
|
||||
"能给一位6岁男孩的生日会提供一些创造性的建议吗?",
|
||||
"如何用 Go 语言实现支持代理 Http client 请求?"
|
||||
])
|
||||
|
||||
const plugins = ref([
|
||||
{
|
||||
value: "今日早报",
|
||||
text: "今日早报:获取当天全球的热门新闻事件列表"
|
||||
},
|
||||
{
|
||||
value: "微博热搜",
|
||||
text: "微博热搜:新浪微博热搜榜,微博当日热搜榜单"
|
||||
},
|
||||
{
|
||||
value: "今日头条",
|
||||
text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
|
||||
}
|
||||
])
|
||||
|
||||
const capabilities = ref([
|
||||
{
|
||||
text: "轻松扮演翻译专家,程序员,AI 女友,文案高手...",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
text: "国产大语言模型支持,百度文心,科大讯飞,ChatGLM...",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
text: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2",
|
||||
value: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2"
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const emits = defineEmits(['send']);
|
||||
const send = (text) => {
|
||||
emits('send', text)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.welcome {
|
||||
text-align center
|
||||
display flex
|
||||
justify-content center
|
||||
margin-top 8vh
|
||||
|
||||
.container {
|
||||
max-width 768px;
|
||||
width 100%
|
||||
|
||||
.title {
|
||||
font-size: 2.25rem
|
||||
line-height: 2.5rem
|
||||
font-weight 600
|
||||
margin-bottom: 4rem
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
.item-title {
|
||||
div {
|
||||
padding 6px 10px;
|
||||
|
||||
.iconfont {
|
||||
font-size 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-box {
|
||||
ul {
|
||||
padding 10px;
|
||||
|
||||
li {
|
||||
font-size 14px;
|
||||
padding .75rem
|
||||
border-radius 5px;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
|
||||
line-height 1.5
|
||||
color #666666
|
||||
|
||||
a {
|
||||
cursor pointer
|
||||
display block
|
||||
width 100%
|
||||
}
|
||||
margin-top 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
279
new-ui/projects/web/src/components/admin/AdminHeader.vue
Normal file
279
new-ui/projects/web/src/components/admin/AdminHeader.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="header admin-header">
|
||||
<!-- 折叠按钮 -->
|
||||
<div class="collapse-btn" @click="collapseChange">
|
||||
<el-icon v-if="sidebar.collapse">
|
||||
<Expand/>
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Fold/>
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb :separator-icon="ArrowRight">
|
||||
<el-breadcrumb-item v-for="item in breadcrumb">{{ item.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-user-con">
|
||||
<!-- 消息中心 -->
|
||||
<div class="btn-bell">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="message ? `有${message}条未读消息` : `消息中心`"
|
||||
placement="bottom"
|
||||
>
|
||||
<i class="iconfont icon-bell"></i>
|
||||
</el-tooltip>
|
||||
<span class="btn-bell-badge" v-if="message"></span>
|
||||
</div>
|
||||
<!-- 用户名下拉菜单 -->
|
||||
<el-dropdown class="user-name" :hide-on-click="true" trigger="click">
|
||||
<span class="el-dropdown-link">
|
||||
<el-avatar class="user-avatar" :size="30" :src="avatar"/>
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down/>
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span>{{ sysTitle }}</span>
|
||||
</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item @click="showDialog = true">
|
||||
<i class="iconfont icon-reward"></i>
|
||||
<span>打赏作者</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="logout">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
<span>退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:show-close="true"
|
||||
class="donate-dialog"
|
||||
width="400px"
|
||||
title="请作者喝杯咖啡"
|
||||
>
|
||||
<el-alert type="info" :closable="false">
|
||||
<p>如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~</p>
|
||||
</el-alert>
|
||||
<p>
|
||||
<el-image :src="donateImg"/>
|
||||
</p>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {getMenuItems, useSidebarStore} from '@/store/sidebar';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {ArrowDown, ArrowRight, Expand, Fold} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const message = ref(5);
|
||||
const sysTitle = ref(process.env.VUE_APP_TITLE)
|
||||
const avatar = ref('/images/user-info.jpg')
|
||||
const donateImg = ref('/images/wechat-pay.png')
|
||||
const showDialog = ref(false)
|
||||
const sidebar = useSidebarStore();
|
||||
const router = useRouter();
|
||||
const breadcrumb = ref([])
|
||||
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
initBreadCrumb(to.path)
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initBreadCrumb(router.currentRoute.value.path)
|
||||
})
|
||||
|
||||
// 初始化面包屑导航
|
||||
const initBreadCrumb = (path) => {
|
||||
breadcrumb.value = [{title: "首页"}]
|
||||
const items = getMenuItems()
|
||||
if (items) {
|
||||
let bk = false
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].index === path) {
|
||||
breadcrumb.value.push({
|
||||
title: items[i].title,
|
||||
path: items[i].index
|
||||
})
|
||||
break
|
||||
}
|
||||
if (bk) {
|
||||
break
|
||||
}
|
||||
|
||||
if (items[i]['subs']) {
|
||||
const subs = items[i]['subs']
|
||||
for (let j = 0; j < subs.length; j++) {
|
||||
if (subs[j].index === path) {
|
||||
breadcrumb.value.push({
|
||||
title: items[i].title,
|
||||
path: items[i].index
|
||||
})
|
||||
breadcrumb.value.push({
|
||||
title: subs[j].title,
|
||||
path: subs[j].index
|
||||
})
|
||||
bk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏折叠
|
||||
const collapseChange = () => {
|
||||
sidebar.handleCollapse();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (document.body.clientWidth < 1024) {
|
||||
collapseChange();
|
||||
}
|
||||
});
|
||||
|
||||
const logout = function () {
|
||||
httpGet("/api/admin/logout").then(() => {
|
||||
router.replace('/admin/login')
|
||||
}).catch((e) => {
|
||||
ElMessage.error("注销失败: " + e.message);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.header {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow hidden
|
||||
height: 50px;
|
||||
font-size: 22px;
|
||||
color: #303133;
|
||||
background-color #ffffff
|
||||
border-bottom 1px solid #eaecef
|
||||
|
||||
.collapse-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
float: left;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color #eaecef
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
float left
|
||||
display flex
|
||||
align-items center
|
||||
height 50px
|
||||
}
|
||||
|
||||
.header-right {
|
||||
float: right;
|
||||
padding-right: 20px;
|
||||
|
||||
.header-user-con {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
align-items: center;
|
||||
|
||||
.btn-bell {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn-bell-badge {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: #f56c6c;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.icon-bell {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-left: 10px;
|
||||
|
||||
.el-icon {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
text-align: center;
|
||||
|
||||
.icon-reward {
|
||||
font-size 18px;
|
||||
font-weight bold;
|
||||
color #F56C6C
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="stylus">
|
||||
.donate-dialog {
|
||||
.el-dialog__body {
|
||||
text-align center;
|
||||
|
||||
.el-alert__description {
|
||||
text-align left
|
||||
font-size 14px;
|
||||
line-height 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
226
new-ui/projects/web/src/components/admin/AdminSidebar.vue
Normal file
226
new-ui/projects/web/src/components/admin/AdminSidebar.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<el-image :src="logo"/>
|
||||
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
class="sidebar-el-menu"
|
||||
:default-active="onRoutes"
|
||||
:collapse="sidebar.collapse"
|
||||
background-color="#324157"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#20a0ff"
|
||||
unique-opened
|
||||
router
|
||||
>
|
||||
<template v-for="item in items">
|
||||
<template v-if="item.subs">
|
||||
<el-sub-menu :index="item.index" :key="item.index">
|
||||
<template #title>
|
||||
<i :class="'iconfont icon-'+item.icon"></i>
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
<template v-for="subItem in item.subs">
|
||||
<el-sub-menu
|
||||
v-if="subItem.subs"
|
||||
:index="subItem.index"
|
||||
:key="subItem.index"
|
||||
>
|
||||
<template #title>{{ subItem.title }}</template>
|
||||
<el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">
|
||||
{{ threeItem.title }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="subItem.index">
|
||||
<i v-if="subItem.icon" :class="'iconfont icon-'+subItem.icon"></i>
|
||||
{{ subItem.title }}
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-menu-item :index="item.index" :key="item.index">
|
||||
<i :class="'iconfont icon-'+item.icon"></i>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from 'vue';
|
||||
import {setMenuItems, useSidebarStore} from '@/store/sidebar';
|
||||
import {useRoute} from 'vue-router';
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const title = ref('Chat-Plus-Admin')
|
||||
const logo = ref('/images/logo.png')
|
||||
|
||||
// 加载系统配置
|
||||
httpGet('/api/admin/config/get?key=system').then(res => {
|
||||
title.value = res.data['admin_title'];
|
||||
}).catch(e => {
|
||||
ElMessage.error("加载系统配置失败: " + e.message)
|
||||
})
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: 'home',
|
||||
index: '/admin/dashboard',
|
||||
title: '仪表盘',
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'user-fill',
|
||||
index: '/admin/user',
|
||||
title: '用户管理',
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'role',
|
||||
index: '/admin/role',
|
||||
title: '角色管理',
|
||||
},
|
||||
{
|
||||
icon: 'api-key',
|
||||
index: '/admin/apikey',
|
||||
title: 'API-KEY',
|
||||
},
|
||||
{
|
||||
icon: 'model',
|
||||
index: '/admin/chat/model',
|
||||
title: '语言模型',
|
||||
},
|
||||
{
|
||||
icon: 'recharge',
|
||||
index: '/admin/product',
|
||||
title: '充值产品',
|
||||
},
|
||||
{
|
||||
icon: 'order',
|
||||
index: '/admin/order',
|
||||
title: '充值订单',
|
||||
},
|
||||
{
|
||||
icon: 'reward',
|
||||
index: '/admin/reward',
|
||||
title: '众筹管理',
|
||||
},
|
||||
{
|
||||
icon: 'control',
|
||||
index: '/admin/functions',
|
||||
title: '函数管理',
|
||||
},
|
||||
{
|
||||
icon: 'prompt',
|
||||
index: '/admin/chats',
|
||||
title: '对话管理',
|
||||
},
|
||||
{
|
||||
icon: 'config',
|
||||
index: '/admin/system',
|
||||
title: '系统设置',
|
||||
},
|
||||
{
|
||||
icon: 'log',
|
||||
index: '/admin/loginLog',
|
||||
title: '用户登录日志',
|
||||
},
|
||||
// {
|
||||
// icon: 'menu',
|
||||
// index: '1',
|
||||
// title: '常用模板页面',
|
||||
// subs: [
|
||||
// {
|
||||
// index: '/admin/demo/form',
|
||||
// title: '表单页面',
|
||||
// },
|
||||
// {
|
||||
// index: '/admin/demo/table',
|
||||
// title: '常用表格',
|
||||
// },
|
||||
// {
|
||||
// index: '/admin/demo/import',
|
||||
// title: '导入Excel',
|
||||
// },
|
||||
// {
|
||||
// index: '/admin/demo/editor',
|
||||
// title: '富文本编辑器',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
const onRoutes = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
const sidebar = useSidebarStore();
|
||||
setMenuItems(items)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.sidebar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow-y: scroll;
|
||||
|
||||
.logo {
|
||||
display flex
|
||||
width 219px
|
||||
background-color #324157
|
||||
padding 6px 15px;
|
||||
|
||||
.el-image {
|
||||
width 30px;
|
||||
height 30px;
|
||||
padding-top 8px;
|
||||
border-radius 100%
|
||||
|
||||
.el-image__inner {
|
||||
height 40px
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color #ffffff
|
||||
font-weight bold
|
||||
padding 12px 0 12px 10px;
|
||||
transition: width 2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
height: 100%;
|
||||
|
||||
.el-menu-item, .el-sub-menu {
|
||||
.iconfont {
|
||||
font-size 16px;
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color rgb(40, 52, 70)
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-el-menu:not(.el-menu--collapse) {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
181
new-ui/projects/web/src/components/admin/AdminTags.vue
Normal file
181
new-ui/projects/web/src/components/admin/AdminTags.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="tags" v-if="tags.show">
|
||||
<ul>
|
||||
<li
|
||||
class="tags-li"
|
||||
v-for="(item, index) in tags.list"
|
||||
:class="{ active: isActive(item.path) }"
|
||||
:key="index"
|
||||
>
|
||||
<router-link :to="item.path" class="tags-li-title">{{ item.title }}</router-link>
|
||||
<el-icon @click="closeTags(index)">
|
||||
<Close/>
|
||||
</el-icon>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tags-close-box">
|
||||
<el-dropdown @command="handleTags">
|
||||
<el-button size="small" type="info">
|
||||
标签选项
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu size="small">
|
||||
<el-dropdown-item command="other">关闭其他</el-dropdown-item>
|
||||
<el-dropdown-item command="all">关闭所有</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useTagsStore} from '@/store/tags';
|
||||
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
|
||||
import {ArrowDown, Close} from "@element-plus/icons-vue";
|
||||
import {checkAdminSession} from "@/action/session";
|
||||
import {ElMessageBox} from "element-plus";
|
||||
|
||||
const router = useRouter();
|
||||
checkAdminSession().catch(() => {
|
||||
ElMessageBox({
|
||||
title: '提示',
|
||||
message: "当前会话已经失效,请重新登录",
|
||||
confirmButtonText: 'OK',
|
||||
callback: () => router.replace('/admin/login')
|
||||
});
|
||||
})
|
||||
const isActive = (path) => {
|
||||
return path === route.fullPath;
|
||||
};
|
||||
|
||||
const tags = useTagsStore();
|
||||
const route = useRoute();
|
||||
// 关闭单个标签
|
||||
const closeTags = (index) => {
|
||||
const delItem = tags.list[index];
|
||||
tags.delTagsItem(index);
|
||||
const item = tags.list[index] ? tags.list[index] : tags.list[index - 1];
|
||||
if (item) {
|
||||
delItem.path === route.fullPath && router.push(item.path);
|
||||
} else {
|
||||
router.push('/admin');
|
||||
}
|
||||
};
|
||||
|
||||
// 设置标签
|
||||
const setTags = (route) => {
|
||||
const isExist = tags.list.some(item => {
|
||||
return item.path === route.fullPath;
|
||||
});
|
||||
if (!isExist) {
|
||||
if (tags.list.length >= 8) tags.delTagsItem(0);
|
||||
tags.setTagsItem({
|
||||
name: route.name,
|
||||
title: route.meta.title,
|
||||
path: route.fullPath
|
||||
});
|
||||
}
|
||||
};
|
||||
setTags(route);
|
||||
onBeforeRouteUpdate(to => {
|
||||
setTags(to);
|
||||
});
|
||||
|
||||
// 关闭全部标签
|
||||
const closeAll = () => {
|
||||
tags.clearTags();
|
||||
router.push('/admin');
|
||||
};
|
||||
// 关闭其他标签
|
||||
const closeOther = () => {
|
||||
const curItem = tags.list.filter(item => {
|
||||
return item.path === route.fullPath;
|
||||
});
|
||||
tags.closeTagsOther(curItem);
|
||||
};
|
||||
const handleTags = (command) => {
|
||||
command === 'other' ? closeOther() : closeAll();
|
||||
};
|
||||
|
||||
// 关闭当前页面的标签页
|
||||
// tags.closeCurrentTag({
|
||||
// $router: router,
|
||||
// $route: route
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tags {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
padding-right: 120px;
|
||||
-webkit-box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
|
||||
}
|
||||
|
||||
.tags ul {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tags-li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
float: left;
|
||||
margin: 3px 5px 2px 3px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
height: 23px;
|
||||
border: 1px solid #e9eaec;
|
||||
background: #fff;
|
||||
padding: 0 5px 0 12px;
|
||||
color: #666;
|
||||
-webkit-transition: all 0.3s ease-in;
|
||||
-moz-transition: all 0.3s ease-in;
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.tags-li:not(.active):hover {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.tags-li.active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tags-li-title {
|
||||
float: left;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tags-li.active .tags-li-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tags-close-box {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
box-sizing: border-box;
|
||||
padding-top: 1px;
|
||||
text-align: center;
|
||||
width: 110px;
|
||||
height: 30px;
|
||||
background: #fff;
|
||||
//box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1); z-index: 10;
|
||||
}
|
||||
</style>
|
||||
271
new-ui/projects/web/src/components/mobile/ChatMidJourney.vue
Normal file
271
new-ui/projects/web/src/components/mobile/ChatMidJourney.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="mobile-message-mj">
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="triangle"></div>
|
||||
<div class="content-box">
|
||||
<div class="content">
|
||||
<div class="content-inner">
|
||||
<div class="text" v-html="data.html"></div>
|
||||
<div class="images" v-if="data.image?.url !== ''">
|
||||
<el-image :src="data.image?.url"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[data.image?.url]"
|
||||
fit="cover"
|
||||
:initial-index="0" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot"
|
||||
:style="{height: height+'px', lineHeight:height+'px'}">
|
||||
正在加载图片<span class="dot">...</span></div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="upscale(1)">U1</a></li>
|
||||
<li><a @click="upscale(2)">U2</a></li>
|
||||
<li><a @click="upscale(3)">U3</a></li>
|
||||
<li><a @click="upscale(4)">U4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="variation(1)">V1</a></li>
|
||||
<li><a @click="variation(2)">V2</a></li>
|
||||
<li><a @click="variation(3)">V3</a></li>
|
||||
<li><a @click="variation(4)">V4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {Picture} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {showNotify} from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: Object,
|
||||
icon: String,
|
||||
chatId: String,
|
||||
roleId: Number,
|
||||
createdAt: String
|
||||
});
|
||||
|
||||
const data = ref(props.content)
|
||||
const cacheKey = "img_placeholder_height"
|
||||
const item = localStorage.getItem(cacheKey);
|
||||
const loading = ref(false)
|
||||
const height = ref(0)
|
||||
if (item) {
|
||||
height.value = parseInt(item)
|
||||
}
|
||||
if (data.value["image"]?.width > 0) {
|
||||
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
|
||||
localStorage.setItem(cacheKey, height.value)
|
||||
}
|
||||
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
|
||||
// console.log(data.value)
|
||||
|
||||
watch(() => props.content, (newVal) => {
|
||||
data.value = newVal;
|
||||
});
|
||||
const emits = defineEmits(['disable-input', 'disable-input']);
|
||||
const upscale = (index) => {
|
||||
send('/api/mj/upscale', index)
|
||||
}
|
||||
|
||||
const variation = (index) => {
|
||||
send('/api/mj/variation', index)
|
||||
}
|
||||
|
||||
const send = (url, index) => {
|
||||
loading.value = true
|
||||
emits('disable-input')
|
||||
httpPost(url, {
|
||||
index: index,
|
||||
src: "chat",
|
||||
message_id: data.value?.["message_id"],
|
||||
message_hash: data.value?.["image"]?.hash,
|
||||
session_id: getSessionId(),
|
||||
key: data.value?.["key"],
|
||||
prompt: data.value?.["prompt"],
|
||||
chat_id: props.chatId,
|
||||
role_id: props.roleId,
|
||||
icon: props.icon,
|
||||
}).then(() => {
|
||||
showNotify({type: "success", message: "任务推送成功,请耐心等待任务执行..."})
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "任务推送失败:" + e.message})
|
||||
emits('disable-input')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.mobile-message-mj {
|
||||
display flex
|
||||
justify-content: flex-start;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 5px
|
||||
|
||||
.van-image {
|
||||
width 32px
|
||||
|
||||
img {
|
||||
border-radius 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 5px solid #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
|
||||
display flex
|
||||
flex-direction row
|
||||
|
||||
.content {
|
||||
text-align left
|
||||
width 100%
|
||||
overflow-x auto
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 5px 10px;
|
||||
color #444444
|
||||
background-color: #ffffff;
|
||||
font-size: 16px
|
||||
border-radius: 5px;
|
||||
|
||||
.content-inner {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
.text {
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
max-width 350px;
|
||||
|
||||
.el-image {
|
||||
border-radius 10px;
|
||||
|
||||
.image-slot {
|
||||
color #c1c1c1
|
||||
width 350px
|
||||
text-align center
|
||||
border-radius 10px;
|
||||
border 1px solid #e1e1e1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
padding-left 10px
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 3px 0
|
||||
width 50px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-message-reply {
|
||||
.chat-item {
|
||||
.triangle {
|
||||
border-right: 5px solid #404042;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
.content {
|
||||
color #c1c1c1
|
||||
background-color: #404042;
|
||||
|
||||
p > code {
|
||||
color #c1c1c1
|
||||
background-color #2b2b2b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
102
new-ui/projects/web/src/components/mobile/ChatPrompt.vue
Normal file
102
new-ui/projects/web/src/components/mobile/ChatPrompt.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="mobile-message-prompt">
|
||||
<div class="chat-item">
|
||||
<div ref="contentRef" :data-clipboard-text="content" class="content" v-html="content"></div>
|
||||
<div class="triangle"></div>
|
||||
</div>
|
||||
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {showNotify} from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '/images/user-icon.png',
|
||||
}
|
||||
});
|
||||
const contentRef = ref(null)
|
||||
onMounted(() => {
|
||||
const clipboard = new Clipboard(contentRef.value);
|
||||
clipboard.on('success', () => {
|
||||
showNotify({type: 'success', message: '复制成功', duration: 1000})
|
||||
})
|
||||
clipboard.on('error', () => {
|
||||
showNotify({type: 'danger', message: '复制失败', duration: 2000})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.mobile-message-prompt {
|
||||
display flex
|
||||
justify-content: flex-end
|
||||
|
||||
.chat-icon {
|
||||
margin-left 5px
|
||||
|
||||
.van-image {
|
||||
width 32px
|
||||
|
||||
img {
|
||||
border-radius 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 5px solid #98E165;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
text-align left
|
||||
padding: 5px 10px;
|
||||
background-color: #98E165;
|
||||
color #444444
|
||||
font-size: 14px
|
||||
border-radius: 5px
|
||||
line-height 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-message-prompt {
|
||||
.chat-item {
|
||||
|
||||
.triangle {
|
||||
border-left: 5px solid #223A34
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #223A34
|
||||
color #c1c1c1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
new-ui/projects/web/src/components/mobile/ChatReply.vue
Normal file
224
new-ui/projects/web/src/components/mobile/ChatReply.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="mobile-message-reply">
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="triangle"></div>
|
||||
<div class="content-box" ref="contentRef">
|
||||
<div :data-clipboard-text="orgContent" class="content content-mobile" v-html="content"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, ref} from "vue"
|
||||
|
||||
import {showImagePreview} from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
orgContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '/images/gpt-icon.png',
|
||||
}
|
||||
});
|
||||
|
||||
const contentRef = ref(null)
|
||||
onMounted(() => {
|
||||
const imgs = contentRef.value.querySelectorAll('img')
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (!imgs[i].src) {
|
||||
continue
|
||||
}
|
||||
imgs[i].addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
showImagePreview([imgs[i].src]);
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.mobile-message-reply {
|
||||
display flex
|
||||
justify-content: flex-start;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 5px
|
||||
|
||||
.van-image {
|
||||
width 32px
|
||||
|
||||
img {
|
||||
border-radius 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 5px solid #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
|
||||
display flex
|
||||
flex-direction row
|
||||
|
||||
.content {
|
||||
text-align left
|
||||
width 100%
|
||||
overflow-x auto
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 5px 10px;
|
||||
color #444444
|
||||
background-color: #ffffff;
|
||||
font-size: 14px
|
||||
border-radius: 5px;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
|
||||
p {
|
||||
code {
|
||||
color #2b2b2b
|
||||
background-color #c1c1c1
|
||||
padding 2px 5px
|
||||
border-radius 5px
|
||||
}
|
||||
|
||||
img {
|
||||
max-width 100%
|
||||
}
|
||||
}
|
||||
|
||||
.code-container {
|
||||
position relative
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
.copy-code-mobile {
|
||||
position: absolute;
|
||||
right 10px
|
||||
top 10px
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
color #c1c1c1
|
||||
|
||||
&:hover {
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
display none
|
||||
position absolute;
|
||||
right 10px
|
||||
bottom 50px
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-message-reply {
|
||||
.chat-item {
|
||||
.triangle {
|
||||
border-right: 5px solid #404042;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
.content {
|
||||
color #c1c1c1
|
||||
background-color: #404042;
|
||||
|
||||
p > code {
|
||||
color #c1c1c1
|
||||
background-color #2b2b2b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
102
new-ui/projects/web/src/main.js
Normal file
102
new-ui/projects/web/src/main.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import {createApp} from 'vue'
|
||||
import ElementPlus from "element-plus"
|
||||
import "element-plus/dist/index.css"
|
||||
import 'vant/lib/index.css';
|
||||
import App from './App.vue'
|
||||
import {createPinia} from "pinia";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Cell,
|
||||
CellGroup,
|
||||
Circle,
|
||||
Col,
|
||||
Collapse,
|
||||
CollapseItem,
|
||||
ConfigProvider,
|
||||
Dialog,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
Empty,
|
||||
Field,
|
||||
Form,
|
||||
Grid,
|
||||
GridItem,
|
||||
Icon,
|
||||
Image,
|
||||
ImagePreview,
|
||||
Lazyload,
|
||||
List,
|
||||
Loading,
|
||||
NavBar,
|
||||
Notify,
|
||||
Overlay,
|
||||
Picker,
|
||||
Popup,
|
||||
Row,
|
||||
Search,
|
||||
ShareSheet,
|
||||
Slider,
|
||||
Sticky,
|
||||
SwipeCell,
|
||||
Switch,
|
||||
Tab,
|
||||
Tabbar,
|
||||
TabbarItem,
|
||||
Tabs,
|
||||
Tag,
|
||||
TextEllipsis,
|
||||
Uploader
|
||||
} from "vant";
|
||||
import {router} from "@/router";
|
||||
import 'v3-waterfall/dist/style.css'
|
||||
import V3waterfall from "v3-waterfall";
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(ConfigProvider);
|
||||
app.use(Tabbar);
|
||||
app.use(TabbarItem);
|
||||
app.use(NavBar);
|
||||
app.use(Search);
|
||||
app.use(Cell)
|
||||
app.use(Image)
|
||||
app.use(TextEllipsis)
|
||||
app.use(Notify)
|
||||
app.use(Picker)
|
||||
app.use(Popup)
|
||||
app.use(List);
|
||||
app.use(Form);
|
||||
app.use(Field);
|
||||
app.use(CellGroup);
|
||||
app.use(Button);
|
||||
app.use(DropdownMenu);
|
||||
app.use(Icon);
|
||||
app.use(DropdownItem);
|
||||
app.use(Sticky);
|
||||
app.use(SwipeCell);
|
||||
app.use(Dialog);
|
||||
app.use(ShareSheet);
|
||||
app.use(Switch);
|
||||
app.use(Uploader);
|
||||
app.use(Tag);
|
||||
app.use(V3waterfall)
|
||||
app.use(Overlay)
|
||||
app.use(Col)
|
||||
app.use(Row)
|
||||
app.use(Slider)
|
||||
app.use(Badge)
|
||||
app.use(Collapse);
|
||||
app.use(CollapseItem);
|
||||
app.use(Grid);
|
||||
app.use(GridItem);
|
||||
app.use(Empty);
|
||||
app.use(Circle);
|
||||
app.use(Loading);
|
||||
app.use(Lazyload);
|
||||
app.use(ImagePreview);
|
||||
app.use(Tab);
|
||||
app.use(Tabs);
|
||||
app.use(router).use(ElementPlus).mount('#app')
|
||||
|
||||
|
||||
151
new-ui/projects/web/src/router.js
Normal file
151
new-ui/projects/web/src/router.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: "home",
|
||||
path: "/",
|
||||
redirect: "/chat",
|
||||
meta: { title: "首页" },
|
||||
component: () => import("@/views/Home.vue"),
|
||||
children: [
|
||||
{
|
||||
name: "chat",
|
||||
path: "/chat",
|
||||
meta: { title: "创作中心" },
|
||||
component: () => import("@/views/ChatPlus.vue"),
|
||||
},
|
||||
{
|
||||
name: "image-mj",
|
||||
path: "/mj",
|
||||
meta: { title: "MidJourney 绘画中心" },
|
||||
component: () => import("@/views/ImageMj.vue"),
|
||||
},
|
||||
{
|
||||
name: "image-sd",
|
||||
path: "/sd",
|
||||
meta: { title: "stable diffusion 绘画中心" },
|
||||
component: () => import("@/views/ImageSd.vue"),
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
path: "/member",
|
||||
meta: { title: "会员充值中心" },
|
||||
component: () => import("@/views/Member.vue"),
|
||||
},
|
||||
{
|
||||
name: "chat-role",
|
||||
path: "/apps",
|
||||
meta: { title: "应用中心" },
|
||||
component: () => import("@/views/ChatApps.vue"),
|
||||
},
|
||||
{
|
||||
name: "images",
|
||||
path: "/images-wall",
|
||||
meta: { title: "作品展示" },
|
||||
component: () => import("@/views/ImagesWall.vue"),
|
||||
},
|
||||
{
|
||||
name: "user-invitation",
|
||||
path: "/invite",
|
||||
meta: { title: "推广计划" },
|
||||
component: () => import("@/views/Invitation.vue"),
|
||||
},
|
||||
{
|
||||
name: "knowledge",
|
||||
path: "/knowledge",
|
||||
meta: { title: "我的知识库" },
|
||||
component: () => import("@/views/Knowledge.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "chat-export",
|
||||
path: "/chat/export",
|
||||
meta: { title: "导出会话记录" },
|
||||
component: () => import("@/views/ChatExport.vue"),
|
||||
},
|
||||
{
|
||||
name: "login",
|
||||
path: "/login",
|
||||
meta: { title: "用户登录" },
|
||||
component: () => import("@/views/Login.vue"),
|
||||
},
|
||||
{
|
||||
name: "register",
|
||||
path: "/register",
|
||||
|
||||
meta: { title: "用户注册" },
|
||||
component: () => import("@/views/Register.vue"),
|
||||
},
|
||||
|
||||
{
|
||||
name: "mobile",
|
||||
path: "/mobile",
|
||||
meta: { title: "ChatPlus-智能助手V3" },
|
||||
component: () => import("@/views/mobile/Home.vue"),
|
||||
redirect: "/mobile/chat",
|
||||
children: [
|
||||
{
|
||||
path: "/mobile/chat",
|
||||
name: "mobile-chat",
|
||||
component: () => import("@/views/mobile/ChatList.vue"),
|
||||
},
|
||||
{
|
||||
path: "/mobile/mj",
|
||||
name: "mobile-mj",
|
||||
component: () => import("@/views/mobile/ImageMj.vue"),
|
||||
},
|
||||
{
|
||||
path: "/mobile/profile",
|
||||
name: "mobile-profile",
|
||||
component: () => import("@/views/mobile/Profile.vue"),
|
||||
},
|
||||
{
|
||||
path: "/mobile/img-wall",
|
||||
name: "mobile-img-wall",
|
||||
component: () => import("@/views/mobile/ImgWall.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/mobile/chat/session",
|
||||
name: "mobile-chat-session",
|
||||
component: () => import("@/views/mobile/ChatSession.vue"),
|
||||
},
|
||||
{
|
||||
path: "/mobile/chat/export",
|
||||
name: "mobile-chat-export",
|
||||
component: () => import("@/views/mobile/ChatExport.vue"),
|
||||
},
|
||||
|
||||
{
|
||||
name: "test",
|
||||
path: "/test",
|
||||
meta: { title: "测试页面" },
|
||||
component: () => import("@/views/Test.vue"),
|
||||
},
|
||||
{
|
||||
name: "NotFound",
|
||||
path: "/:all(.*)",
|
||||
meta: { title: "页面没有找到" },
|
||||
component: () => import("@/views/404.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
// console.log(MY_VARIABLE)
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
let prevRoute = null;
|
||||
// dynamic change the title when router change
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} | ${process.env.VUE_APP_TITLE}`;
|
||||
}
|
||||
prevRoute = from;
|
||||
next();
|
||||
});
|
||||
|
||||
export { router, prevRoute };
|
||||
11
new-ui/projects/web/src/store/chat.js
Normal file
11
new-ui/projects/web/src/store/chat.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Storage from 'good-storage'
|
||||
|
||||
const CHAT_CONFIG_KEY = process.env.VUE_APP_KEY_PREFIX + "chat_config"
|
||||
|
||||
export function getChatConfig() {
|
||||
return Storage.get(CHAT_CONFIG_KEY)
|
||||
}
|
||||
|
||||
export function setChatConfig(chatConfig) {
|
||||
Storage.set(CHAT_CONFIG_KEY, chatConfig)
|
||||
}
|
||||
51
new-ui/projects/web/src/store/session.js
Normal file
51
new-ui/projects/web/src/store/session.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {randString} from "@/utils/libs";
|
||||
import Storage from "good-storage";
|
||||
|
||||
/**
|
||||
* storage handler
|
||||
*/
|
||||
|
||||
const SessionIDKey = process.env.VUE_APP_KEY_PREFIX + 'SESSION_ID';
|
||||
const UserTokenKey = process.env.VUE_APP_KEY_PREFIX + "Authorization";
|
||||
const AdminTokenKey = process.env.VUE_APP_KEY_PREFIX + "Admin-Authorization"
|
||||
|
||||
export function getSessionId() {
|
||||
let sessionId = Storage.get(SessionIDKey)
|
||||
if (!sessionId) {
|
||||
sessionId = randString(42)
|
||||
setSessionId(sessionId)
|
||||
}
|
||||
return sessionId
|
||||
}
|
||||
|
||||
export function removeSessionId() {
|
||||
Storage.remove(SessionIDKey)
|
||||
}
|
||||
|
||||
export function setSessionId(sessionId) {
|
||||
Storage.set(SessionIDKey, sessionId)
|
||||
}
|
||||
|
||||
export function getUserToken() {
|
||||
return Storage.get(UserTokenKey) ?? ""
|
||||
}
|
||||
|
||||
export function setUserToken(token) {
|
||||
Storage.set(UserTokenKey, token)
|
||||
}
|
||||
|
||||
export function removeUserToken() {
|
||||
Storage.remove(UserTokenKey)
|
||||
}
|
||||
|
||||
export function getAdminToken() {
|
||||
return Storage.get(AdminTokenKey)
|
||||
}
|
||||
|
||||
export function setAdminToken(token) {
|
||||
Storage.set(AdminTokenKey, token)
|
||||
}
|
||||
|
||||
export function removeAdminToken() {
|
||||
Storage.remove(AdminTokenKey)
|
||||
}
|
||||
26
new-ui/projects/web/src/store/sidebar.js
Normal file
26
new-ui/projects/web/src/store/sidebar.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import Storage from "good-storage";
|
||||
|
||||
export const useSidebarStore = defineStore('sidebar', {
|
||||
state: () => {
|
||||
return {
|
||||
collapse: false
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
handleCollapse() {
|
||||
this.collapse = !this.collapse;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const MENU_STORE_KEY = "admin_menu_items"
|
||||
|
||||
export function getMenuItems() {
|
||||
return Storage.get(MENU_STORE_KEY)
|
||||
}
|
||||
|
||||
export function setMenuItems(items) {
|
||||
return Storage.set(MENU_STORE_KEY, items)
|
||||
}
|
||||
11
new-ui/projects/web/src/store/system.js
Normal file
11
new-ui/projects/web/src/store/system.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Storage from "good-storage";
|
||||
|
||||
const MOBILE_THEME = process.env.VUE_APP_KEY_PREFIX + "MOBILE_THEME"
|
||||
|
||||
export function getMobileTheme() {
|
||||
return Storage.get(MOBILE_THEME) ? Storage.get(MOBILE_THEME) : 'light'
|
||||
}
|
||||
|
||||
export function setMobileTheme(theme) {
|
||||
Storage.set(MOBILE_THEME, theme)
|
||||
}
|
||||
47
new-ui/projects/web/src/store/tags.js
Normal file
47
new-ui/projects/web/src/store/tags.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import {defineStore} from 'pinia';
|
||||
|
||||
export const useTagsStore = defineStore('tags', {
|
||||
state: () => {
|
||||
return {
|
||||
list: []
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
show: state => {
|
||||
return state.list.length > 0;
|
||||
},
|
||||
nameList: state => {
|
||||
return state.list.map(item => item.name);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
delTagsItem(index) {
|
||||
this.list.splice(index, 1);
|
||||
},
|
||||
setTagsItem(data) {
|
||||
this.list.push(data);
|
||||
},
|
||||
clearTags() {
|
||||
this.list = [];
|
||||
},
|
||||
closeTagsOther(data) {
|
||||
this.list = data;
|
||||
},
|
||||
closeCurrentTag(data) {
|
||||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||||
const item = this.list[i];
|
||||
if (item.path === data.$route.fullPath) {
|
||||
if (i < len - 1) {
|
||||
data.$router.push(this.list[i + 1].path);
|
||||
} else if (i > 0) {
|
||||
data.$router.push(this.list[i - 1].path);
|
||||
} else {
|
||||
data.$router.push('/admin');
|
||||
}
|
||||
this.list.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
56
new-ui/projects/web/src/utils/http.js
Normal file
56
new-ui/projects/web/src/utils/http.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import axios from 'axios'
|
||||
import {getAdminToken, getSessionId, getUserToken} from "@/store/session";
|
||||
|
||||
axios.defaults.timeout = 30000
|
||||
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
|
||||
axios.defaults.withCredentials = true;
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
|
||||
// HTTP拦截器
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
// set token
|
||||
config.headers['Chat-Token'] = getSessionId();
|
||||
config.headers['Authorization'] = getUserToken();
|
||||
config.headers['Admin-Authorization'] = getAdminToken();
|
||||
return config
|
||||
}, error => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
let data = response.data;
|
||||
if (data.code === 0) {
|
||||
return response
|
||||
} else {
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
}, error => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
|
||||
// send a http get request
|
||||
export function httpGet(url, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(url, {
|
||||
params: params
|
||||
}).then(response => {
|
||||
resolve(response.data)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// send a http post request
|
||||
export function httpPost(url, data = {}, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(url, data, options).then(response => {
|
||||
resolve(response.data)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
205
new-ui/projects/web/src/utils/libs.js
Normal file
205
new-ui/projects/web/src/utils/libs.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Util lib functions
|
||||
*/
|
||||
|
||||
// generate a random string
|
||||
export function randString(length) {
|
||||
const str = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
const size = str.length
|
||||
let buf = []
|
||||
for (let i = 0; i < length; i++) {
|
||||
const rand = Math.random() * size
|
||||
buf.push(str.charAt(rand))
|
||||
}
|
||||
return buf.join("")
|
||||
}
|
||||
|
||||
export function UUID() {
|
||||
let d = new Date().getTime();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// 判断是否是移动设备
|
||||
export function isMobile() {
|
||||
const userAgent = navigator.userAgent;
|
||||
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
|
||||
return mobileRegex.test(userAgent);
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
export function dateFormat(timestamp, format) {
|
||||
if (!timestamp) {
|
||||
return '';
|
||||
} else if (timestamp < 9680917502) {
|
||||
timestamp = timestamp * 1000;
|
||||
}
|
||||
let year, month, day, HH, mm, ss;
|
||||
let time = new Date(timestamp);
|
||||
let timeDate;
|
||||
year = time.getFullYear(); // 年
|
||||
month = time.getMonth() + 1; // 月
|
||||
day = time.getDate(); // 日
|
||||
HH = time.getHours(); // 时
|
||||
mm = time.getMinutes(); // 分
|
||||
ss = time.getSeconds(); // 秒
|
||||
|
||||
month = month < 10 ? '0' + month : month;
|
||||
day = day < 10 ? '0' + day : day;
|
||||
HH = HH < 10 ? '0' + HH : HH; // 时
|
||||
mm = mm < 10 ? '0' + mm : mm; // 分
|
||||
ss = ss < 10 ? '0' + ss : ss; // 秒
|
||||
|
||||
switch (format) {
|
||||
case 'yyyy':
|
||||
timeDate = String(year);
|
||||
break;
|
||||
case 'yyyy-MM':
|
||||
timeDate = year + '-' + month;
|
||||
break;
|
||||
case 'yyyy-MM-dd':
|
||||
timeDate = year + '-' + month + '-' + day;
|
||||
break;
|
||||
case 'yyyy/MM/dd':
|
||||
timeDate = year + '/' + month + '/' + day;
|
||||
break;
|
||||
case 'yyyy-MM-dd HH:mm:ss':
|
||||
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
|
||||
break;
|
||||
case 'HH:mm:ss':
|
||||
timeDate = HH + ':' + mm + ':' + ss;
|
||||
break;
|
||||
case 'MM':
|
||||
timeDate = String(month);
|
||||
break;
|
||||
default:
|
||||
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
|
||||
break;
|
||||
}
|
||||
return timeDate;
|
||||
}
|
||||
|
||||
// 判断数组中是否包含某个元素
|
||||
export function arrayContains(array, value, compare) {
|
||||
if (!array) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof compare !== 'function') {
|
||||
compare = function (v1, v2) {
|
||||
return v1 === v2;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (compare(array[i], value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除数组中指定的元素
|
||||
export function removeArrayItem(array, value, compare) {
|
||||
if (typeof compare !== 'function') {
|
||||
compare = function (v1, v2) {
|
||||
return v1 === v2;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (compare(array[i], value)) {
|
||||
array.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
// 渲染输入的换行符
|
||||
export function renderInputText(text) {
|
||||
const replaceRegex = /(\n\r|\r\n|\r|\n)/g;
|
||||
text = text || '';
|
||||
return text.replace(replaceRegex, "<br/>");
|
||||
}
|
||||
|
||||
// 拷贝对象
|
||||
export function copyObj(origin) {
|
||||
return JSON.parse(JSON.stringify(origin));
|
||||
}
|
||||
|
||||
export function disabledDate(time) {
|
||||
return time.getTime() < Date.now()
|
||||
}
|
||||
|
||||
// 字符串截取
|
||||
export function substr(str, length) {
|
||||
let result = ''
|
||||
let count = 0
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charAt(i)
|
||||
const charCode = str.charCodeAt(i);
|
||||
|
||||
// 判断字符是否为中文字符
|
||||
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
|
||||
// 中文字符算两个字符
|
||||
count += 2
|
||||
} else {
|
||||
count++
|
||||
}
|
||||
|
||||
if (count <= length) {
|
||||
result += char
|
||||
} else {
|
||||
result += " ..."
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function isImage(url) {
|
||||
const expr = /\.(jpg|jpeg|png|gif|bmp|svg)$/i;
|
||||
return expr.test(url);
|
||||
}
|
||||
|
||||
export function processContent(content) {
|
||||
//process img url
|
||||
const linkRegex = /(https?:\/\/\S+)/g;
|
||||
const links = content.match(linkRegex);
|
||||
if (links) {
|
||||
for (let link of links) {
|
||||
if (isImage(link)) {
|
||||
const index = content.indexOf(link)
|
||||
if (content.substring(index - 1, 2) !== "]") {
|
||||
content = content.replace(link, "\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理引用块
|
||||
if (content.indexOf("\n") === -1) {
|
||||
return content
|
||||
}
|
||||
|
||||
const texts = content.split("\n")
|
||||
const lines = []
|
||||
for (let txt of texts) {
|
||||
lines.push(txt)
|
||||
if (txt.startsWith(">")) {
|
||||
lines.push("\n")
|
||||
}
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export function escapeHTML(html) {
|
||||
return html.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
11
new-ui/projects/web/src/utils/validate.js
Normal file
11
new-ui/projects/web/src/utils/validate.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// 正则校验工具函数
|
||||
|
||||
export function validateEmail(email) {
|
||||
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return regex.test(email);
|
||||
}
|
||||
|
||||
export function validateMobile(mobile) {
|
||||
const regex = /^1[3456789]\d{9}$/;
|
||||
return regex.test(mobile);
|
||||
}
|
||||
40
new-ui/projects/web/src/views/404.vue
Normal file
40
new-ui/projects/web/src/views/404.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="page-404" :style="{ height: winHeight + 'px' }">
|
||||
<div class="inner">
|
||||
<h1>404</h1>
|
||||
<h2>饶了地球一圈,还是没有找到您要的页面</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue"
|
||||
|
||||
const winHeight = ref(window.innerHeight)
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.page-404 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
text-align center
|
||||
|
||||
h1 {
|
||||
color: #202020;
|
||||
font-size: 120px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
116
new-ui/projects/web/src/views/ChatApps.vue
Normal file
116
new-ui/projects/web/src/views/ChatApps.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="page-apps custom-scroll">
|
||||
<div class="title">
|
||||
AI 助手应用中心
|
||||
</div>
|
||||
<div class="inner" :style="{height: listBoxHeight + 'px'}">
|
||||
<ItemList :items="list" v-if="list.length > 0" :gap="20" :width="250">
|
||||
<template #default="scope">
|
||||
<div class="app-item" :style="{width: scope.width+'px'}">
|
||||
<el-image :src="scope.item.icon" fit="cover" :style="{height: scope.width+'px'}"/>
|
||||
<div class="title">
|
||||
<span class="name">{{ scope.item.name }}</span>
|
||||
<div class="opt">
|
||||
|
||||
<el-button v-if="hasRole(scope.item.key)" size="small" type="danger"
|
||||
@click="updateRole(scope.item,'remove')">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
<span>移除应用</span>
|
||||
</el-button>
|
||||
<el-button v-else size="small"
|
||||
style="--el-color-primary:#009999"
|
||||
@click="updateRole(scope.item, 'add')">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
<span>添加应用</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hello-msg" ref="elements">{{ scope.item.intro }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
</div>
|
||||
|
||||
<login-dialog :show="showLoginDialog" @hide="getRoles"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import {Delete, Plus} from "@element-plus/icons-vue";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {arrayContains, removeArrayItem, substr} from "@/utils/libs";
|
||||
|
||||
const listBoxHeight = window.innerHeight - 97
|
||||
const list = ref([])
|
||||
const showLoginDialog = ref(false)
|
||||
const roles = ref([])
|
||||
const elements = ref(null)
|
||||
onMounted(() => {
|
||||
httpGet("/api/role/list?all=true").then((res) => {
|
||||
const items = res.data
|
||||
// 处理 hello message
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].intro = substr(items[i].hello_msg, 80)
|
||||
}
|
||||
list.value = items
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取应用失败:" + e.message)
|
||||
})
|
||||
|
||||
getRoles()
|
||||
})
|
||||
|
||||
const getRoles = () => {
|
||||
showLoginDialog.value = false
|
||||
checkSession().then(user => {
|
||||
roles.value = user.chat_roles
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateRole = (row, opt) => {
|
||||
checkSession().then(() => {
|
||||
const title = ref("")
|
||||
if (opt === "add") {
|
||||
title.value = "添加应用"
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (exists) {
|
||||
return
|
||||
}
|
||||
roles.value.push(row.key)
|
||||
} else {
|
||||
title.value = "移除应用"
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (!exists) {
|
||||
return
|
||||
}
|
||||
roles.value = removeArrayItem(roles.value, row.key)
|
||||
}
|
||||
httpPost("/api/role/update", {keys: roles.value}).then(() => {
|
||||
ElMessage.success({message: title.value + "成功!", duration: 1000})
|
||||
}).catch(e => {
|
||||
ElMessage.error(title.value + "失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
showLoginDialog.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const hasRole = (roleKey) => {
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/chat-app.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
173
new-ui/projects/web/src/views/ChatExport.vue
Normal file
173
new-ui/projects/web/src/views/ChatExport.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="chat-export" v-loading="loading">
|
||||
<div class="chat-box" id="chat-box">
|
||||
<div class="title">
|
||||
<h2>{{ chatTitle }}</h2>
|
||||
<el-button type="success" @click="exportChat" :icon="Promotion">
|
||||
导出 PDF 文档
|
||||
</el-button>
|
||||
</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']"
|
||||
:content="item.content"/>
|
||||
<chat-reply v-else-if="item.type==='reply'"
|
||||
:icon="item.icon"
|
||||
:org-content="item.orgContent"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:tokens="item['tokens']"
|
||||
:content="item.content"/>
|
||||
<chat-mid-journey v-else-if="item.type==='mj'"
|
||||
:content="item.content"
|
||||
:icon="item.icon"
|
||||
:created-at="dateFormat(item['created_at'])"/>
|
||||
</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, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import hl from "highlight.js";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Promotion} from "@element-plus/icons-vue";
|
||||
import ChatMidJourney from "@/components/ChatMidJourney.vue";
|
||||
|
||||
const chatData = ref([])
|
||||
const router = useRouter()
|
||||
const chatId = router.currentRoute.value.query['chat_id']
|
||||
const loading = ref(true)
|
||||
const chatTitle = ref('')
|
||||
|
||||
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
|
||||
const data = res.data
|
||||
if (!data) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const md = require('markdown-it')({breaks: true});
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === "prompt") {
|
||||
chatData.value.push(data[i]);
|
||||
continue;
|
||||
} else if (data[i].type === "mj") {
|
||||
data[i].content = JSON.parse(data[i].content)
|
||||
data[i].content.content = md.render(data[i].content?.content)
|
||||
chatData.value.push(data[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
data[i].orgContent = data[i].content;
|
||||
data[i].content = md.render(data[i].content);
|
||||
chatData.value.push(data[i]);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
hl.configure({ignoreUnescapedHTML: true})
|
||||
const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
})
|
||||
})
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error('加载聊天记录失败:' + e.message);
|
||||
})
|
||||
|
||||
httpGet('/api/chat/detail?chat_id=' + chatId).then(res => {
|
||||
chatTitle.value = res.data.title
|
||||
}).catch(e => {
|
||||
ElMessage.error("加载会失败: " + e.message)
|
||||
})
|
||||
|
||||
const exportChat = () => {
|
||||
window.print()
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
.chat-export {
|
||||
display flex
|
||||
justify-content center
|
||||
padding 0 20px
|
||||
|
||||
.chat-box {
|
||||
width 800px;
|
||||
// 变量定义
|
||||
--content-font-size: 16px;
|
||||
--content-color: #c1c1c1;
|
||||
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
padding: 0 0 50px 0;
|
||||
|
||||
.title {
|
||||
text-align center
|
||||
}
|
||||
|
||||
|
||||
.chat-line {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.chat-line-inner {
|
||||
.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
|
||||
|
||||
.copy-reply {
|
||||
display none
|
||||
}
|
||||
|
||||
.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%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
888
new-ui/projects/web/src/views/ChatPlus.vue
Normal file
888
new-ui/projects/web/src/views/ChatPlus.vue
Normal file
@@ -0,0 +1,888 @@
|
||||
<template>
|
||||
<div class="common-layout theme-white">
|
||||
<el-container>
|
||||
<el-aside>
|
||||
<div class="title-box">
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<div class="chat-list">
|
||||
<div class="search-box">
|
||||
<el-input v-model="chatName" class="w-50 m-2" size="small" placeholder="搜索会话" @keyup="searchChat">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<Search/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<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)">
|
||||
<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)"
|
||||
placeholder="请输入会话标题"/>
|
||||
</span>
|
||||
<span v-else class="chat-title">{{ chat.title }}</span>
|
||||
<span class="btn btn-check" v-if="chat.edit || chat.removing">
|
||||
<el-icon @click="confirm($event, chat)"><Check/></el-icon>
|
||||
<el-icon @click="cancel($event, chat)"><Close/></el-icon>
|
||||
</span>
|
||||
<span class="btn" v-else>
|
||||
<el-icon title="编辑" @click="editChatTitle($event, chat)"><Edit/></el-icon>
|
||||
<el-icon title="删除会话" @click="removeChat($event, chat)"><Delete/></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-box">
|
||||
<el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="isLogin">
|
||||
<span class="el-dropdown-link">
|
||||
<el-image :src="loginUser.avatar"/>
|
||||
<span class="username">{{ loginUser.nickname }}</span>
|
||||
<el-icon><ArrowDown/></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu style="width: 296px;">
|
||||
<el-dropdown-item @click="showConfig">
|
||||
<el-icon>
|
||||
<Tools/>
|
||||
</el-icon>
|
||||
<span>账户信息</span>
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item @click="clearAllChats">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
<span>清除所有会话</span>
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item @click="logout">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
<span>注销</span>
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span>
|
||||
powered by
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">chatgpt-plus-v3</el-link>
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-aside>
|
||||
<el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
|
||||
<div class="chat-head">
|
||||
<div class="chat-config">
|
||||
<span class="role-select-label">聊天角色:</span>
|
||||
<el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="newChat">
|
||||
<el-option
|
||||
v-for="item in roles"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="role-option">
|
||||
<el-image :src="item.icon"></el-image>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="modelID" placeholder="模型" @change="newChat">
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="newChat">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
新建对话
|
||||
</el-button>
|
||||
|
||||
<el-button type="success" @click="exportChat" plain>
|
||||
<i class="iconfont icon-export"></i>
|
||||
<span>导出会话</span>
|
||||
</el-button>
|
||||
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
content="部署文档"
|
||||
placement="bottom">
|
||||
<a href="https://ai.r9it.com/docs/install/" target="_blank">
|
||||
<el-button type="primary" circle>
|
||||
<i class="iconfont icon-book"></i>
|
||||
</el-button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
content="项目源码"
|
||||
placement="bottom">
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-button type="success" circle>
|
||||
<i class="iconfont icon-github"></i>
|
||||
</el-button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-box" :style="{height: mainWinHeight+'px'}">
|
||||
<div>
|
||||
<div id="container">
|
||||
<div class="chat-box" id="chat-box" :style="{height: chatBoxHeight+'px'}">
|
||||
<div v-if="showHello">
|
||||
<welcome @send="autofillPrompt"/>
|
||||
</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'"
|
||||
:icon="item.icon"
|
||||
:org-content="item.orgContent"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:tokens="item['tokens']"
|
||||
:content="item.content"/>
|
||||
<chat-mid-journey v-else-if="item.type==='mj'"
|
||||
:content="item.content"
|
||||
:role-id="item.role_id"
|
||||
:chat-id="item.chat_id"
|
||||
:icon="item.icon"
|
||||
@disable-input="disableInput(true)"
|
||||
@enable-input="enableInput"
|
||||
:created-at="dateFormat(item['created_at'])"/>
|
||||
</div>
|
||||
</div><!-- end chat box -->
|
||||
|
||||
<div class="re-generate">
|
||||
<div class="btn-box">
|
||||
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
|
||||
<el-icon>
|
||||
<VideoPause/>
|
||||
</el-icon>
|
||||
停止生成
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" v-if="showReGenerate" @click="reGenerate" plain>
|
||||
<el-icon>
|
||||
<RefreshRight/>
|
||||
</el-icon>
|
||||
重新生成
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
ref="textInput"
|
||||
v-model="prompt"
|
||||
v-on:keydown="inputKeyDown"
|
||||
autofocus
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
|
||||
/>
|
||||
<span class="select-file">
|
||||
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertURL"/>
|
||||
</span>
|
||||
<span class="send-btn">
|
||||
<el-button @click="sendMessage">
|
||||
<el-icon><Promotion/></el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div><!-- end input box -->
|
||||
|
||||
</div><!-- end container -->
|
||||
</div><!-- end loading -->
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
<el-dialog
|
||||
v-model="showNotice"
|
||||
:show-close="true"
|
||||
custom-class="notice-dialog"
|
||||
title="网站公告"
|
||||
>
|
||||
<div class="notice">
|
||||
<el-text type="primary">
|
||||
<div v-html="notice"></div>
|
||||
</el-text>
|
||||
|
||||
<p style="text-align: right">
|
||||
<el-button @click="notShow" type="success" plain>我知道了,不再显示</el-button>
|
||||
</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<config-dialog v-if="isLogin" :show="showConfigDialog" :models="models" @hide="showConfigDialog = false"/>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import {nextTick, onMounted, ref} from 'vue'
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
import {
|
||||
ArrowDown,
|
||||
Check,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
Promotion,
|
||||
RefreshRight,
|
||||
Search,
|
||||
Tools,
|
||||
VideoPause
|
||||
} from '@element-plus/icons-vue'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import {dateFormat, escapeHTML, isMobile, processContent, randString, removeArrayItem, UUID} from "@/utils/libs";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import hl from "highlight.js";
|
||||
import {getSessionId, getUserToken, removeUserToken} from "@/store/session";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {useRouter} from "vue-router";
|
||||
import Clipboard from "clipboard";
|
||||
import ConfigDialog from "@/components/ConfigDialog.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import Welcome from "@/components/Welcome.vue";
|
||||
import ChatMidJourney from "@/components/ChatMidJourney.vue";
|
||||
import FileSelect from "@/components/FileSelect.vue";
|
||||
|
||||
const title = ref('ChatGPT-智能助手');
|
||||
const models = ref([])
|
||||
const modelID = ref(0)
|
||||
const chatData = ref([]);
|
||||
const allChats = ref([]); // 会话列表
|
||||
const chatList = ref(allChats.value);
|
||||
const activeChat = ref({});
|
||||
const mainWinHeight = ref(0); // 主窗口高度
|
||||
const chatBoxHeight = ref(0); // 聊天内容框高度
|
||||
const leftBoxHeight = ref(0);
|
||||
const loading = ref(true);
|
||||
const loginUser = ref(null);
|
||||
const roles = ref([]);
|
||||
const roleId = ref(0)
|
||||
const newChatItem = ref(null);
|
||||
const router = useRouter();
|
||||
const showConfigDialog = ref(false);
|
||||
const isLogin = ref(false)
|
||||
const showHello = ref(true)
|
||||
const textInput = ref(null)
|
||||
const showNotice = ref(false)
|
||||
const notice = ref("")
|
||||
const noticeKey = ref("SYSTEM_NOTICE")
|
||||
|
||||
if (isMobile()) {
|
||||
router.replace("/mobile")
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
resizeElement();
|
||||
checkSession().then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
// 获取会话列表
|
||||
httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
|
||||
if (res.data) {
|
||||
chatList.value = res.data;
|
||||
allChats.value = res.data;
|
||||
}
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1').then(res => {
|
||||
models.value = res.data
|
||||
modelID.value = models.value[0].id
|
||||
|
||||
// 加载角色列表
|
||||
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
|
||||
roles.value = res.data;
|
||||
roleId.value = roles.value[0]['id'];
|
||||
const chatId = localStorage.getItem("chat_id")
|
||||
const chat = getChatById(chatId)
|
||||
if (chat === null) {
|
||||
// 创建新的对话
|
||||
newChat();
|
||||
} else {
|
||||
// 加载对话
|
||||
loadChat(chat)
|
||||
}
|
||||
}).catch((e) => {
|
||||
ElMessage.error('获取聊天角色失败: ' + e.messages)
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error("加载模型失败: " + e.message)
|
||||
})
|
||||
|
||||
}).catch(() => {
|
||||
// TODO: 增加重试按钮
|
||||
ElMessage.error("加载会话列表失败!")
|
||||
})
|
||||
|
||||
// 获取系统配置
|
||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
|
||||
// 获取系统公告
|
||||
httpGet("/api/admin/config/get?key=notice").then(res => {
|
||||
notice.value = md.render(res.data['content'])
|
||||
const oldNotice = localStorage.getItem(noticeKey.value);
|
||||
// 如果公告有更新,则显示公告
|
||||
if (oldNotice !== notice.value && notice.value.length > 10) {
|
||||
showNotice.value = true
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
router.push('/login')
|
||||
});
|
||||
|
||||
const clipboard = new Clipboard('.copy-reply, .copy-code-btn');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功!');
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
window.onresize = () => resizeElement();
|
||||
});
|
||||
|
||||
const getRoleById = function (rid) {
|
||||
for (let i = 0; i < roles.value.length; i++) {
|
||||
if (roles.value[i]['id'] === rid) {
|
||||
return roles.value[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const resizeElement = function () {
|
||||
chatBoxHeight.value = window.innerHeight - 51 - 82 - 38;
|
||||
mainWinHeight.value = window.innerHeight - 51;
|
||||
leftBoxHeight.value = window.innerHeight - 43 - 47 - 45;
|
||||
};
|
||||
|
||||
// 新建会话
|
||||
const newChat = function () {
|
||||
// 已有新开的会话
|
||||
if (newChatItem.value !== null && newChatItem.value['role_id'] === roles.value[0]['role_id']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前聊天角色图标
|
||||
let icon = '';
|
||||
roles.value.forEach(item => {
|
||||
if (item['id'] === roleId.value) {
|
||||
icon = item['icon']
|
||||
}
|
||||
})
|
||||
newChatItem.value = {
|
||||
chat_id: "",
|
||||
icon: icon,
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
title: '',
|
||||
edit: false,
|
||||
removing: false,
|
||||
};
|
||||
activeChat.value = {} //取消激活的会话高亮
|
||||
showStopGenerate.value = false;
|
||||
showReGenerate.value = false;
|
||||
connect(null, roleId.value)
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
const changeChat = (chat) => {
|
||||
localStorage.setItem("chat_id", chat.chat_id)
|
||||
loadChat(chat)
|
||||
}
|
||||
|
||||
const loadChat = function (chat) {
|
||||
if (activeChat.value['chat_id'] === chat.chat_id) {
|
||||
return;
|
||||
}
|
||||
activeChat.value = chat
|
||||
newChatItem.value = null;
|
||||
roleId.value = chat.role_id;
|
||||
modelID.value = chat.model_id;
|
||||
showStopGenerate.value = false;
|
||||
showReGenerate.value = false;
|
||||
connect(chat.chat_id, chat.role_id)
|
||||
}
|
||||
|
||||
// 编辑会话标题
|
||||
const curOpt = ref('')
|
||||
const tmpChatTitle = ref('');
|
||||
const editChatTitle = function (event, chat) {
|
||||
event.stopPropagation();
|
||||
chat.edit = true;
|
||||
curOpt.value = 'edit';
|
||||
tmpChatTitle.value = chat.title;
|
||||
};
|
||||
|
||||
|
||||
const titleKeydown = (e, chat) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.stopPropagation();
|
||||
confirm(e, chat)
|
||||
}
|
||||
}
|
||||
// 确认修改
|
||||
const confirm = function (event, chat) {
|
||||
event.stopPropagation();
|
||||
if (curOpt.value === 'edit') {
|
||||
if (tmpChatTitle.value === '') {
|
||||
return ElMessage.error("请输入会话标题!");
|
||||
}
|
||||
if (!chat.chat_id) {
|
||||
return ElMessage.error("对话 ID 为空,请刷新页面再试!");
|
||||
}
|
||||
httpPost('/api/chat/update', {chat_id: chat.chat_id, title: tmpChatTitle.value}).then(() => {
|
||||
chat.title = tmpChatTitle.value;
|
||||
chat.edit = false;
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
})
|
||||
} else if (curOpt.value === 'remove') {
|
||||
httpGet('/api/chat/remove?chat_id=' + chat.chat_id).then(() => {
|
||||
chatList.value = removeArrayItem(chatList.value, chat, function (e1, e2) {
|
||||
return e1.id === e2.id
|
||||
})
|
||||
// 重置会话
|
||||
newChat();
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// 取消修改
|
||||
const cancel = function (event, chat) {
|
||||
event.stopPropagation();
|
||||
chat.edit = false;
|
||||
chat.removing = false;
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
const removeChat = function (event, chat) {
|
||||
event.stopPropagation();
|
||||
chat.removing = true;
|
||||
curOpt.value = 'remove';
|
||||
}
|
||||
|
||||
const latexPlugin = require('markdown-it-latex2img')
|
||||
const mathjaxPlugin = require('markdown-it-mathjax')
|
||||
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(latexPlugin)
|
||||
md.use(mathjaxPlugin)
|
||||
|
||||
// 创建 socket 连接
|
||||
const prompt = ref('');
|
||||
const showStopGenerate = ref(false); // 停止生成
|
||||
const showReGenerate = ref(false); // 重新生成
|
||||
const previousText = ref(''); // 上一次提问
|
||||
const lineBuffer = ref(''); // 输出缓冲行
|
||||
const socket = ref(null);
|
||||
const activelyClose = ref(false); // 主动关闭
|
||||
const canSend = ref(true);
|
||||
const heartbeatHandle = ref(null)
|
||||
const connect = function (chat_id, role_id) {
|
||||
let isNewChat = false;
|
||||
if (!chat_id) {
|
||||
isNewChat = true;
|
||||
chat_id = UUID();
|
||||
}
|
||||
// 先关闭已有连接
|
||||
if (socket.value !== null) {
|
||||
activelyClose.value = true;
|
||||
socket.value.close();
|
||||
}
|
||||
|
||||
const _role = getRoleById(role_id);
|
||||
// 初始化 WebSocket 对象
|
||||
const _sessionId = getSessionId();
|
||||
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/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
chatData.value = []; // 初始化聊天数据
|
||||
previousText.value = '';
|
||||
enableInput()
|
||||
activelyClose.value = false;
|
||||
|
||||
if (isNewChat) { // 加载打招呼信息
|
||||
loading.value = false;
|
||||
chatData.value.push({
|
||||
chat_id: chat_id,
|
||||
role_id: role_id,
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: _role['hello_msg'],
|
||||
orgContent: _role['hello_msg'],
|
||||
})
|
||||
ElMessage.success({message: "对话连接成功!", duration: 1000})
|
||||
} else { // 加载聊天记录
|
||||
loadChatHistory(chat_id);
|
||||
}
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8");
|
||||
reader.onload = () => {
|
||||
const data = JSON.parse(String(reader.result));
|
||||
if (data.type === 'start') {
|
||||
chatData.value.push({
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: ""
|
||||
});
|
||||
} else if (data.type === 'end') { // 消息接收完毕
|
||||
// 追加当前会话到会话列表
|
||||
if (isNewChat && newChatItem.value !== null) {
|
||||
newChatItem.value['title'] = previousText.value;
|
||||
newChatItem.value['chat_id'] = chat_id;
|
||||
chatList.value.unshift(newChatItem.value);
|
||||
activeChat.value = newChatItem.value;
|
||||
newChatItem.value = null; // 只追加一次
|
||||
}
|
||||
|
||||
enableInput()
|
||||
lineBuffer.value = ''; // 清空缓冲
|
||||
|
||||
// 获取 token
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
httpPost("/api/chat/tokens", {text: "", model: getModelValue(modelID.value), chat_id: chat_id}).then(res => {
|
||||
reply['created_at'] = new Date().getTime();
|
||||
reply['tokens'] = res.data;
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
lineBuffer.value += data.content;
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
reply['orgContent'] = lineBuffer.value;
|
||||
reply['content'] = md.render(processContent(lineBuffer.value));
|
||||
}
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
localStorage.setItem("chat_id", chat_id)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
if (activelyClose.value) { // 忽略主动关闭
|
||||
return;
|
||||
}
|
||||
// 停止发送消息
|
||||
disableInput(true)
|
||||
socket.value = null;
|
||||
loading.value = true;
|
||||
checkSession().then(() => {
|
||||
connect(chat_id, role_id)
|
||||
}).catch(() => {
|
||||
loading.value = true
|
||||
setTimeout(() => connect(chat_id, role_id), 3000)
|
||||
});
|
||||
});
|
||||
|
||||
socket.value = _socket;
|
||||
}
|
||||
|
||||
const disableInput = (force) => {
|
||||
canSend.value = false;
|
||||
showReGenerate.value = false;
|
||||
showStopGenerate.value = !force;
|
||||
}
|
||||
|
||||
const enableInput = () => {
|
||||
canSend.value = true;
|
||||
showReGenerate.value = previousText.value !== "";
|
||||
showStopGenerate.value = false;
|
||||
}
|
||||
|
||||
// 登录输入框输入事件处理
|
||||
const inputKeyDown = function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (e.ctrlKey) { // Ctrl + Enter 换行
|
||||
prompt.value += "\n";
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 自动填充 prompt
|
||||
const autofillPrompt = (text) => {
|
||||
prompt.value = text
|
||||
textInput.value.focus()
|
||||
// sendMessage()
|
||||
}
|
||||
// 发送消息
|
||||
const sendMessage = function () {
|
||||
if (canSend.value === false) {
|
||||
ElMessage.warning("AI 正在作答中,请稍后...");
|
||||
return
|
||||
}
|
||||
|
||||
if (prompt.value.trim().length === 0 || canSend.value === false) {
|
||||
return false;
|
||||
}
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: "prompt",
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: md.render(escapeHTML(processContent(prompt.value))),
|
||||
created_at: new Date().getTime(),
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
})
|
||||
|
||||
showHello.value = false
|
||||
disableInput(false)
|
||||
socket.value.send(JSON.stringify({type: "chat", content: prompt.value}));
|
||||
previousText.value = prompt.value;
|
||||
prompt.value = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
const showConfig = function () {
|
||||
showConfigDialog.value = true;
|
||||
}
|
||||
|
||||
const clearAllChats = function () {
|
||||
ElMessageBox.confirm(
|
||||
'确认要清空所有的会话历史记录吗?<br/>此操作不可以撤销!',
|
||||
'操作提示:',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: true,
|
||||
showClose: true,
|
||||
closeOnClickModal: false,
|
||||
center: true,
|
||||
}
|
||||
).then(() => {
|
||||
httpGet("/api/chat/clear").then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
chatData.value = [];
|
||||
chatList.value = [];
|
||||
newChat();
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const logout = function () {
|
||||
activelyClose.value = true;
|
||||
httpGet('/api/user/logout').then(() => {
|
||||
removeUserToken();
|
||||
router.push('/login');
|
||||
}).catch(() => {
|
||||
ElMessage.error('注销失败!');
|
||||
})
|
||||
}
|
||||
|
||||
const loadChatHistory = function (chatId) {
|
||||
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
|
||||
const data = res.data
|
||||
if (!data) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
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))
|
||||
chatData.value.push(data[i]);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
})
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
// TODO: 显示重新加载按钮
|
||||
ElMessage.error('加载聊天记录失败:' + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const stopGenerate = function () {
|
||||
showStopGenerate.value = false;
|
||||
httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
|
||||
enableInput()
|
||||
})
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = function () {
|
||||
disableInput(false)
|
||||
const text = '重新生成上述问题的答案:' + previousText.value;
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: "prompt",
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: md.render(text)
|
||||
});
|
||||
socket.value.send(JSON.stringify({type: "chat", content: previousText.value}));
|
||||
}
|
||||
|
||||
const chatName = ref('')
|
||||
// 搜索会话
|
||||
const searchChat = function () {
|
||||
if (chatName.value === '') {
|
||||
chatList.value = allChats.value
|
||||
return
|
||||
}
|
||||
const items = [];
|
||||
for (let i = 0; i < allChats.value.length; i++) {
|
||||
if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
|
||||
items.push(allChats.value[i]);
|
||||
}
|
||||
}
|
||||
chatList.value = items;
|
||||
}
|
||||
|
||||
// 导出会话
|
||||
const exportChat = () => {
|
||||
if (!activeChat.value['chat_id']) {
|
||||
return ElMessage.error("请先选中一个会话")
|
||||
}
|
||||
|
||||
const url = location.protocol + '//' + location.host + '/chat/export?chat_id=' + activeChat.value['chat_id']
|
||||
// console.log(url)
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
const getChatById = (chatId) => {
|
||||
for (let index in chatList.value) {
|
||||
if (chatList.value[index].chat_id === chatId) {
|
||||
return chatList.value[index]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getModelValue = (model_id) => {
|
||||
for (let i = 0; i < models.value.length; i++) {
|
||||
if (models.value[i].id === model_id) {
|
||||
return models.value[i].value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
const notShow = () => {
|
||||
localStorage.setItem(noticeKey.value, notice.value)
|
||||
showNotice.value = false
|
||||
}
|
||||
|
||||
// 插入文件路径
|
||||
const insertURL = (url) => {
|
||||
prompt.value += " " + url + " "
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "@/assets/css/chat-plus.styl"
|
||||
</style>
|
||||
|
||||
<style lang="stylus">
|
||||
.notice-dialog {
|
||||
.el-dialog__body {
|
||||
padding 0 20px
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
new-ui/projects/web/src/views/Home.vue
Normal file
139
new-ui/projects/web/src/views/Home.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="navigator">
|
||||
<div class="logo">
|
||||
<el-image :src="logo"/>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<ul class="nav-items">
|
||||
<li v-for="item in navs" :key="item.path">
|
||||
<!-- <el-tooltip effect="light" :content="item.title" placement="right">-->
|
||||
<!-- -->
|
||||
<!-- </el-tooltip>-->
|
||||
<a @click="changeNav(item)" :class="item.path === curPath ? 'active' : ''">
|
||||
<el-image :src="item.icon_path" :width="20" v-if="item.icon_path"/>
|
||||
<i :class="'iconfont icon-' + item.icon" v-else></i>
|
||||
</a>
|
||||
<div :class="item.path === curPath ? 'title active' : 'title'">{{ item.title }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="move" mode="out-in">
|
||||
<component :is="Component"></component>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {useRouter} from "vue-router";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {isMobile} from "@/utils/libs";
|
||||
import {ref} from "vue";
|
||||
|
||||
const router = useRouter();
|
||||
const logo = '/images/logo.png';
|
||||
const navs = ref([
|
||||
{path: "/chat", icon_path: "/images/chat.png", title: "对话聊天"},
|
||||
{path: "/mj", icon_path: "/images/mj.png", title: "MJ 绘画"},
|
||||
{path: "/sd", icon_path: "/images/sd.png", title: "SD 绘画"},
|
||||
{path: "/apps", icon: "menu", title: "应用中心"},
|
||||
{path: "/images-wall", icon: "image-list", title: "作品展示"},
|
||||
{path: "/knowledge", icon: "book", title: "知识库"},
|
||||
{path: "/member", icon: "vip-user", title: "会员计划"},
|
||||
{path: "/invite", icon: "share", title: "推广计划"},
|
||||
])
|
||||
const curPath = ref(router.currentRoute.value.path)
|
||||
|
||||
const changeNav = (item) => {
|
||||
curPath.value = item.path
|
||||
router.push(item.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '@/assets/iconfont/iconfont.css';
|
||||
.home {
|
||||
display: flex;
|
||||
background-color: #25272D;
|
||||
height 100vh
|
||||
width 100%
|
||||
|
||||
.navigator {
|
||||
display flex
|
||||
flex-flow column
|
||||
width 70px
|
||||
padding 10px 6px
|
||||
border-right: 1px solid #3c3c3c
|
||||
|
||||
.logo {
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
|
||||
|
||||
.divider {
|
||||
border-bottom 1px solid #4A4A4A
|
||||
width 80%
|
||||
height 10px
|
||||
}
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
margin-top: 20px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
li {
|
||||
margin-bottom 15px
|
||||
|
||||
a {
|
||||
color #DADBDC
|
||||
background-color #40444A
|
||||
border-radius 10px
|
||||
width 48px
|
||||
height 48px
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
cursor pointer
|
||||
|
||||
.el-image {
|
||||
border-radius 10px
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
|
||||
a:hover, a.active {
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px
|
||||
padding-top: 5px
|
||||
color: #e5e7eb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.active {
|
||||
color #47fff1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
896
new-ui/projects/web/src/views/ImageMj.vue
Normal file
896
new-ui/projects/web/src/views/ImageMj.vue
Normal file
@@ -0,0 +1,896 @@
|
||||
<template>
|
||||
<div class="page-mj">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="mj-box">
|
||||
<h2>MidJourney 创作中心</h2>
|
||||
|
||||
<div class="mj-params" :style="{ height: mjBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line pt">
|
||||
<span>图片比例:</span>
|
||||
<el-tooltip effect="light" content="生成图片的尺寸比例" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="item in rates" :key="item.value">
|
||||
<div class="flex-col items-center"
|
||||
:class="item.value === params.rate ? 'grid-content active' : 'grid-content'"
|
||||
@click="changeRate(item)">
|
||||
<!-- <div :class="'shape ' + item.css"></div>-->
|
||||
<el-image class="icon" :src="item.img" fit="cover"></el-image>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="图片画质">
|
||||
<template #default>
|
||||
<div class="form-item-inner flex-row items-center">
|
||||
<el-select v-model="params.quality" placeholder="请选择">
|
||||
<el-option v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-tooltip effect="light" content="生成的图片质量,质量越好出图越慢" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span>模型选择:</span>
|
||||
<el-tooltip effect="light" content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型" raw-content
|
||||
placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="item in models" :key="item.value">
|
||||
<div :class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)">
|
||||
<el-image :src="item.img" fit="cover"></el-image>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="重复平铺">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.tile" inactive-color="#464649" active-color="#47fff1"/>
|
||||
<el-tooltip effect="light"
|
||||
content="重复:--tile,参数释义:生成可用作重复平铺的图像,以创建无缝图案。" raw-content
|
||||
placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="原始模式">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.raw" inactive-color="#464649" active-color="#47fff1"/>
|
||||
<el-tooltip effect="light"
|
||||
content="启用新的RAW模式,呈现的人物写实感更加逼真,人物细节、光源、流畅度也更加接近原始作品。<br/> 同时也意味着您需要添加更长的提示。"
|
||||
raw-content
|
||||
placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="创意度">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-slider v-model.number="params.chaos" :max="100" :step="1"
|
||||
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
|
||||
<el-tooltip effect="light"
|
||||
content="参数用法:--chaos 或--c,取值范围: 0-100 <br/> 取值越高结果越发散,反之则稳定收敛<br /> 默认值0最为精准稳定"
|
||||
raw-content placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="风格化">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-slider v-model.number="params.stylize" :min="0" :max="1000" :step="1"
|
||||
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
|
||||
<el-tooltip effect="light"
|
||||
content="风格化:--stylize 或 --s,范围 1-1000,默认值100 <br/>高取值会产生非常艺术化但与提示关联性较低的图像"
|
||||
raw-content placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="随机种子">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.seed" style="--el-input-focus-border-color:#47fff1"/>
|
||||
<el-tooltip effect="light"
|
||||
content="随机种子:--seed,默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像"
|
||||
raw-content
|
||||
placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-box" @scrollend="handleScrollEnd">
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="extra-params">
|
||||
<el-form>
|
||||
<el-tabs v-model="activeName" class="title-tabs" @tabChange="tabChange">
|
||||
<el-tab-pane label="文生图(可选)" name="image">
|
||||
<div class="text">图生图:以某张图片为底稿参考来创作绘画,生成类似风格或类型图像,支持 PNG 和 JPG 格式图片;
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-form-item label="">
|
||||
<template #default>
|
||||
<div class="form-item-inner flex-row items-center">
|
||||
<el-input v-model="params.img" size="small" placeholder="请输入图片地址或者上传图片"
|
||||
style="width: 300px;"/>
|
||||
<el-icon @click="params.img = ''" title="清空图片">
|
||||
<DeleteFilled/>
|
||||
</el-icon>
|
||||
<el-tooltip effect="light"
|
||||
content="垫图:以某张图片为底稿参考来创作绘画 <br/> 支持 PNG 和 JPG 格式图片"
|
||||
raw-content placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<div class="img-inline">
|
||||
<div class="img-list-box">
|
||||
<div class="img-item" v-for="imgURL in imgList">
|
||||
<el-image :src="imgURL" fit="cover"/>
|
||||
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false"
|
||||
:http-request="uploadImg" style="--el-color-primary:#47fff1">
|
||||
<el-icon class="uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="图像权重:">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-slider v-model.number="params.weight" :max="1" :step="0.01"
|
||||
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
|
||||
<el-tooltip effect="light"
|
||||
content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响"
|
||||
raw-content placement="right">
|
||||
<el-icon style="margin-top: 9px">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="prompt-box">
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>提示词:</span>
|
||||
<el-tooltip effect="light" content="输入你想要的内容,用逗号分割" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="translatePrompt(false)" :disabled="translating">
|
||||
<el-icon style="margin-right: 6px;font-size: 18px;">
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
翻译
|
||||
</el-button>
|
||||
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
raw-content
|
||||
content="使用 AI 翻译并重写提示词,<br/>增加更多细节,风格等描述"
|
||||
placement="top-end"
|
||||
>
|
||||
<el-button type="success" @click="rewritePrompt" :disabled="translating">
|
||||
<el-icon style="margin-right: 6px;font-size: 18px;">
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
翻译并重写
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<el-input v-model="params.prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
|
||||
ref="promptRef"
|
||||
placeholder="这里输入你的英文咒语,例如:A chinese girl walking in the middle of a cobblestone street"/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>不希望出现的内容:(可选)</span>
|
||||
<el-tooltip effect="light" content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-button type="primary" @click="translatePrompt(true)" :disabled="translating">
|
||||
<el-icon style="margin-right: 6px;font-size: 18px;">
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
翻译
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
|
||||
ref="promptRef"
|
||||
placeholder="这里输入你不希望出现在图片上的内容,元素"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="融图(可选)" name="blend">
|
||||
<div class="text">请上传两张以上的图片,最多不超过五张,超过五张图片请使用文生图功能</div>
|
||||
<div class="img-inline">
|
||||
<div class="img-list-box">
|
||||
<div class="img-item" v-for="imgURL in imgList">
|
||||
<el-image :src="imgURL" fit="cover"/>
|
||||
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false"
|
||||
:http-request="uploadImg" style="--el-color-primary:#47fff1">
|
||||
<el-icon class="uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="换脸(可选)" name="swapFace">
|
||||
<div class="text">请上传两张有脸部的图片,用右边图片的脸替换左边图片的脸</div>
|
||||
<div class="img-inline">
|
||||
<div class="img-list-box">
|
||||
<div class="img-item" v-for="imgURL in imgList">
|
||||
<el-image :src="imgURL" fit="cover"/>
|
||||
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false"
|
||||
:http-request="uploadImg" style="--el-color-primary:#47fff1">
|
||||
<el-icon class="uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="submit-btn">
|
||||
<el-button color="#47fff1" :dark="false" @click="generate" round>立即生成</el-button>
|
||||
<div class="text-info">
|
||||
<el-tag type="success">绘图可用额度:{{ imgCalls }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<ItemList :items="runningJobs" v-if="runningJobs.length > 0">
|
||||
<template #default="scope">
|
||||
<div class="job-item">
|
||||
<div v-if="scope.item.progress > 0" class="job-item-inner">
|
||||
<el-image :src="scope.item['img_url']" :zoom-rate="1.2"
|
||||
:preview-src-list="[scope.item['img_url']]" fit="cover" :initial-index="0"
|
||||
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="scope.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>
|
||||
</template>
|
||||
</ItemList>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.7)">
|
||||
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" :width="240" :gap="16">
|
||||
<template #default="scope">
|
||||
<div class="job-item">
|
||||
<el-image
|
||||
:src="scope.item['thumb_url']"
|
||||
:class="scope.item['can_opt'] ? '' : 'upscale'" :zoom-rate="1.2"
|
||||
:preview-src-list="[scope.item['img_url']]" fit="cover" :initial-index="scope.index"
|
||||
loading="lazy" v-if="scope.item.progress > 0">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot" v-if="scope.item['img_url'] === ''">
|
||||
<i class="iconfont icon-loading"></i>
|
||||
<span>正在下载图片</span>
|
||||
</div>
|
||||
<div class="image-slot" v-else>
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="opt" v-if="scope.item['can_opt']">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="upscale(1, scope.item)">U1</a></li>
|
||||
<li><a @click="upscale(2, scope.item)">U2</a></li>
|
||||
<li><a @click="upscale(3, scope.item)">U3</a></li>
|
||||
<li><a @click="upscale(4, scope.item)">U4</a></li>
|
||||
<li class="show-prompt">
|
||||
|
||||
<el-popover placement="left" title="提示词" :width="240" trigger="hover">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<ChromeFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="mj-list-item-prompt">
|
||||
<span>{{ scope.item.prompt }}</span>
|
||||
<el-icon class="copy-prompt-mj"
|
||||
:data-clipboard-text="scope.item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="variation(1, scope.item)">V1</a></li>
|
||||
<li><a @click="variation(2, scope.item)">V2</a></li>
|
||||
<li><a @click="variation(3, scope.item)">V3</a></li>
|
||||
<li><a @click="variation(4, scope.item)">V4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(scope.item)" circle/>
|
||||
<el-button type="warning" v-if="scope.item.publish" @click="publishImage(scope.item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(scope.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, onUnmounted, ref} from "vue"
|
||||
import {
|
||||
ChromeFilled,
|
||||
Delete,
|
||||
DeleteFilled,
|
||||
DocumentCopy,
|
||||
InfoFilled,
|
||||
Picture,
|
||||
Plus,
|
||||
Refresh
|
||||
} from "@element-plus/icons-vue";
|
||||
import Compressor from "compressorjs";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useRouter} from "vue-router";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {isMobile, removeArrayItem} from "@/utils/libs";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const rates = [
|
||||
{css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png"},
|
||||
{css: "size1-2", value: "1:2", text: "1:2", img: "/images/mj/rate_1_2.png"},
|
||||
{css: "size2-1", value: "2:1", text: "2:1", img: "/images/mj/rate_2_1.png"},
|
||||
{css: "size2-3", value: "2:3", text: "2:3", img: "/images/mj/rate_3_4.png"},
|
||||
{css: "size3-2", value: "3:2", text: "3:2", img: "/images/mj/rate_4_3.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: "优质模式MJ-5.1", value: " --v 5.1", img: "/images/mj/mj-v5.1.jpg"},
|
||||
{text: "虚幻模式MJ-5", value: " --v 5", img: "/images/mj/mj-v5.jpg"},
|
||||
{text: "真实模式MJ-4", value: " --v 4", img: "/images/mj/mj-v4.jpg"},
|
||||
{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: "动漫风niji5 表现力", value: " --niji 5 --style expressive", img: "/images/mj/nj3.jpg"},
|
||||
{text: "动漫风niji4", value: " --niji 4", img: "/images/mj/nj4.jpg"},
|
||||
]
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 0,
|
||||
label: '默认'
|
||||
},
|
||||
{
|
||||
value: 0.25,
|
||||
label: '普通'
|
||||
},
|
||||
{
|
||||
value: 0.5,
|
||||
label: '清晰'
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: '高清'
|
||||
},
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
const params = ref({
|
||||
task_type: "image",
|
||||
rate: rates[0].value,
|
||||
model: models[0].value,
|
||||
chaos: 0,
|
||||
stylize: 0,
|
||||
seed: 0,
|
||||
img_arr: [],
|
||||
raw: false,
|
||||
weight: 0.25,
|
||||
prompt: router.currentRoute.value.params["prompt"] ?? "",
|
||||
neg_prompt: "",
|
||||
tile: false,
|
||||
quality: 0
|
||||
})
|
||||
|
||||
const imgList = ref([])
|
||||
|
||||
const activeName = ref('image')
|
||||
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
|
||||
const socket = ref(null)
|
||||
const imgCalls = ref(0)
|
||||
const translating = ref(false)
|
||||
const userId = ref(0)
|
||||
|
||||
if (isMobile()) {
|
||||
router.replace("/mobile/mj")
|
||||
}
|
||||
|
||||
const rewritePrompt = () => {
|
||||
translating.value = true
|
||||
httpPost("/api/prompt/rewrite", {"prompt": params.value.prompt}).then(res => {
|
||||
params.value.prompt = res.data
|
||||
translating.value = false
|
||||
}).catch(e => {
|
||||
translating.value = false
|
||||
ElMessage.error("翻译失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const translatePrompt = (negative) => {
|
||||
translating.value = true
|
||||
let prompt = params.value.prompt
|
||||
if (negative) {
|
||||
prompt = params.value.neg_prompt
|
||||
}
|
||||
httpPost("/api/prompt/translate", {"prompt": prompt}).then(res => {
|
||||
if (negative) {
|
||||
params.value.neg_prompt = res.data
|
||||
} else {
|
||||
params.value.prompt = res.data
|
||||
}
|
||||
translating.value = false
|
||||
}).catch(e => {
|
||||
translating.value = false
|
||||
ElMessage.error("翻译失败:" + 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', () => {
|
||||
connect()
|
||||
});
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
checkSession().then(user => {
|
||||
imgCalls.value = user['img_calls']
|
||||
userId.value = user.id
|
||||
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
connect()
|
||||
|
||||
}).catch(() => {
|
||||
router.push('/login')
|
||||
});
|
||||
|
||||
clipboard.value = new Clipboard('.copy-prompt-mj');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
// 获取运行中的任务
|
||||
const fetchRunningJobs = () => {
|
||||
httpGet(`/api/mj/jobs?status=0`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
ElNotification({
|
||||
title: '任务执行失败',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `任务ID:${jobs[i]['task_id']}<br />原因:${jobs[i]['err_msg']}`,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
})
|
||||
imgCalls.value += 1
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
runningJobs.value = _jobs
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const handleScrollEnd = () => {
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
};
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const isOver = ref(false)
|
||||
const loading = ref(false)
|
||||
const fetchFinishJobs = (page) => {
|
||||
if (isOver.value === true) {
|
||||
ElMessage.info("全部数据加载完毕!")
|
||||
return
|
||||
}
|
||||
|
||||
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]['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) {
|
||||
isOver.value = true
|
||||
}
|
||||
if (page === 1) {
|
||||
finishedJobs.value = jobs
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(jobs)
|
||||
}
|
||||
nextTick(() => loading.value = false)
|
||||
}).catch(e => {
|
||||
loading.value = false
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 切换图片比例
|
||||
const changeRate = (item) => {
|
||||
params.value.rate = item.value
|
||||
}
|
||||
// 切换模型
|
||||
const changeModel = (item) => {
|
||||
params.value.model = item.value
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
imgList.value.push(res.data.url)
|
||||
ElMessage.success('上传成功')
|
||||
}).catch((e) => {
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === '' && params.value.task_type === "image") {
|
||||
promptRef.value.focus()
|
||||
return ElMessage.error("请输入绘画提示词!")
|
||||
}
|
||||
if (params.value.model.indexOf("niji") !== -1 && params.value.raw) {
|
||||
return ElMessage.error("动漫模型不允许启用原始模式")
|
||||
}
|
||||
if (imgList.value.length !== 2 && params.value.task_type === "swapFace") {
|
||||
return ElMessage.error("换脸操作需要上传两张图片")
|
||||
}
|
||||
params.value.session_id = getSessionId()
|
||||
params.value.img_arr = imgList.value
|
||||
httpPost("/api/mj/image", params.value).then(() => {
|
||||
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
|
||||
imgCalls.value -= 1
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 图片放大任务
|
||||
const upscale = (index, item) => {
|
||||
send('/api/mj/upscale', index, item)
|
||||
}
|
||||
|
||||
// 图片变换任务
|
||||
const variation = (index, item) => {
|
||||
send('/api/mj/variation', index, item)
|
||||
}
|
||||
|
||||
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(() => {
|
||||
ElMessage.success("任务推送成功,请耐心等待任务执行...")
|
||||
imgCalls.value -= 1
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (item) => {
|
||||
ElMessageBox.confirm(
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
'删除提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (item, action) => {
|
||||
let text = "图片发布"
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
ElMessage.error(text + "失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 切换菜单
|
||||
const tabChange = (tab) => {
|
||||
params.value.task_type = tab
|
||||
}
|
||||
|
||||
// 删除已上传图片
|
||||
const removeUploadImage = (url) => {
|
||||
imgList.value = removeArrayItem(imgList.value, url)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-mj.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
784
new-ui/projects/web/src/views/ImageSd.vue
Normal file
784
new-ui/projects/web/src/views/ImageSd.vue
Normal file
@@ -0,0 +1,784 @@
|
||||
<template>
|
||||
<div class="page-sd">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="sd-box">
|
||||
<h2>Stable Diffusion 创作中心</h2>
|
||||
|
||||
<div class="sd-params" :style="{ height: mjBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="采样方法">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.sampler" size="small">
|
||||
<el-option v-for="item in samplers" :label="item" :value="item" :key="item"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="出图效果比较好的一般是 Euler 和 DPM 系列算法"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片尺寸">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-input v-model.number="params.width" size="small" placeholder="图片宽度"/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input v-model.number="params.height" size="small" placeholder="图片高度"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="迭代步数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.steps" size="small"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="值越大则代表细节越多,同时也意味着出图速度越慢"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="引导系数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.cfg_scale" size="small"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="随机因子">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.seed" size="small"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="使用随机数"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon @click="params.seed = -1">
|
||||
<Orange/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="面部修复">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.face_fix" style="--el-switch-on-color: #47fff1;"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="仅对绘制人物图像有效果。"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="高清修复">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-show="params.hd_fix">
|
||||
<div class="param-line">
|
||||
<el-form-item label="重绘幅度">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1"
|
||||
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="放大算法">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.hd_scale_alg" size="small">
|
||||
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="高清修复放大算法,主流算法有Latent和ESRGAN_4x"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="放大倍数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_scale" size="small"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="迭代步数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_steps" size="small"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="重绘迭代步数,如果设置为0,则设置跟原图相同的迭代步数"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-line" v-loading="translating" element-loading-background="rgba(122, 122, 122, 0.8)">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
ref="promptRef"
|
||||
placeholder="正向提示词,例如:A chinese girl walking in the middle of a cobblestone street"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<el-button type="primary" @click="translatePrompt" size="small">
|
||||
<el-icon style="margin-right: 6px;font-size: 18px;">
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
翻译
|
||||
</el-button>
|
||||
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
raw-content
|
||||
content="使用 AI 翻译并重写提示词,<br/>增加更多细节,风格等描述"
|
||||
placement="top-end"
|
||||
>
|
||||
<el-button type="success" @click="rewritePrompt" size="small">
|
||||
<el-icon style="margin-right: 6px;font-size: 18px;">
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
翻译并重写
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span>反向提示词:</span>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="不希望出现的元素,下面给了默认的起手式"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="params.negative_prompt"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
placeholder="反向提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line" style="padding: 10px">
|
||||
<el-tag type="success">绘图可用额度:{{ imgCalls }}</el-tag>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="submit-btn">
|
||||
<el-button color="#47fff1" :dark="false" round @click="generate">立即生成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-box" @scrollend="handleScrollEnd">
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<ItemList :items="runningJobs" v-if="runningJobs.length > 0" :width="240">
|
||||
<template #default="scope">
|
||||
<div class="job-item">
|
||||
<div v-if="scope.item.progress > 0" class="job-item-inner">
|
||||
<el-image :src="scope.item['img_url']"
|
||||
fit="cover"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon v-if="scope.item['img_url'] !== ''">
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="progress">
|
||||
<el-progress type="circle" :percentage="scope.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>
|
||||
</template>
|
||||
</ItemList>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.7)">
|
||||
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" :width="240" :gap="16">
|
||||
<template #default="scope">
|
||||
<div class="job-item animate" @click="showTask(scope.item)">
|
||||
<el-image
|
||||
:src="scope.item['img_url']+'?imageView2/1/w/240/h/240/q/75'"
|
||||
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="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event,scope.item)" circle/>
|
||||
<el-button type="warning" v-if="scope.item.publish"
|
||||
@click="publishImage($event,scope.item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event,scope.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
<!-- 任务详情弹框 -->
|
||||
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<div class="img-container" :style="{maxHeight: fullImgHeight+'px'}">
|
||||
<el-image :src="item['img_url']" fit="contain"/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="task-info">
|
||||
<div class="info-line">
|
||||
<el-divider>
|
||||
正向提示词
|
||||
</el-divider>
|
||||
<div class="prompt">
|
||||
<span>{{ item.prompt }}</span>
|
||||
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<el-divider>
|
||||
反向提示词
|
||||
</el-divider>
|
||||
<div class="prompt">
|
||||
<span>{{ item.params.negative_prompt }}</span>
|
||||
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.negative_prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>采样方法:</label>
|
||||
<div class="item-value">{{ item.params.sampler }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>图片尺寸:</label>
|
||||
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>迭代步数:</label>
|
||||
<div class="item-value">{{ item.params.steps }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>引导系数:</label>
|
||||
<div class="item-value">{{ item.params.cfg_scale }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>随机因子:</label>
|
||||
<div class="item-value">{{ item.params.seed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.params.hd_fix">
|
||||
<el-divider>
|
||||
高清修复
|
||||
</el-divider>
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>重绘幅度:</label>
|
||||
<div class="item-value">{{ item.params.hd_redraw_rate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>放大算法:</label>
|
||||
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>放大倍数:</label>
|
||||
<div class="item-value">{{ item.params.hd_scale }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>迭代步数:</label>
|
||||
<div class="item-value">{{ item.params.hd_steps }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copy-params">
|
||||
<el-button type="primary" round @click="copyParams(item)">画一张同款的</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue"
|
||||
import {Delete, DocumentCopy, InfoFilled, Orange, Picture, Refresh} from "@element-plus/icons-vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useRouter} from "vue-router";
|
||||
import {getSessionId} from "@/store/session";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const fullImgHeight = ref(window.innerHeight - 60)
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
const translating = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const samplers = ["Euler a", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM++ 2M SDE Karras"]
|
||||
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
|
||||
const params = ref({
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
sampler: samplers[0],
|
||||
seed: -1,
|
||||
steps: 30,
|
||||
cfg_scale: 7,
|
||||
face_fix: false,
|
||||
hd_fix: false,
|
||||
hd_redraw_rate: 0.5,
|
||||
hd_scale: 2,
|
||||
hd_scale_alg: scaleAlg[0],
|
||||
hd_steps: 15,
|
||||
prompt: "",
|
||||
negative_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 imgCalls = ref(0)
|
||||
|
||||
const rewritePrompt = () => {
|
||||
translating.value = true
|
||||
httpPost("/api/prompt/rewrite", {"prompt": params.value.prompt}).then(res => {
|
||||
params.value.prompt = res.data
|
||||
translating.value = false
|
||||
}).catch(e => {
|
||||
translating.value = false
|
||||
ElMessage.error("翻译失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const translatePrompt = () => {
|
||||
translating.value = true
|
||||
httpPost("/api/prompt/translate", {"prompt": params.value.prompt}).then(res => {
|
||||
params.value.prompt = res.data
|
||||
translating.value = false
|
||||
}).catch(e => {
|
||||
translating.value = false
|
||||
ElMessage.error("翻译失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
connect()
|
||||
});
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
checkSession().then(user => {
|
||||
imgCalls.value = user['img_calls']
|
||||
userId.value = user.id
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs()
|
||||
connect()
|
||||
}).catch(() => {
|
||||
router.push('/login')
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-prompt-sd');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const fetchRunningJobs = (userId) => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/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) {
|
||||
ElNotification({
|
||||
title: '任务执行失败',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `任务ID:${jobs[i]['task_id']}<br />原因:${jobs[i]['err_msg']}`,
|
||||
type: 'error',
|
||||
})
|
||||
imgCalls.value += 1
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
runningJobs.value = _jobs
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const handleScrollEnd = () => {
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const isOver = ref(false)
|
||||
const loading = ref(false)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
if (isOver.value === true) {
|
||||
ElMessage.info("全部数据加载完毕!")
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
isOver.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
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return ElMessage.error("请输入绘画提示词!")
|
||||
}
|
||||
if (params.value.seed === '') {
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost("/api/sd/image", params.value).then(() => {
|
||||
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
|
||||
imgCalls.value -= 1
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + 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()
|
||||
ElMessageBox.confirm(
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
'删除提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (event, item, action) => {
|
||||
event.stopPropagation()
|
||||
let text = "图片发布"
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
ElMessage.error(text + "失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-sd.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
364
new-ui/projects/web/src/views/ImagesWall.vue
Normal file
364
new-ui/projects/web/src/views/ImagesWall.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<div class="page-images-wall">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="header">
|
||||
<h2>AI 绘画作品墙</h2>
|
||||
<div class="settings">
|
||||
<el-radio-group v-model="imgType" @change="changeImgType">
|
||||
<el-radio label="mj" size="large">MidJourney</el-radio>
|
||||
<el-radio label="sd" size="large">Stable Diffusion</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="waterfall" :style="{ height:listBoxHeight + 'px' }" id="waterfall-box">
|
||||
<v3-waterfall v-if="imgType === 'mj'"
|
||||
id="waterfall"
|
||||
:list="data['mj']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="false"
|
||||
@scrollReachBottom="getNext">
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image :src="slotProp.item['img_thumb']"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[slotProp.item['img_url']]"
|
||||
:preview-teleported="true"
|
||||
:initial-index="10"
|
||||
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>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="画同款"
|
||||
placement="top"
|
||||
>
|
||||
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<v3-waterfall v-else
|
||||
id="waterfall"
|
||||
:list="data['sd']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="false"
|
||||
@scrollReachBottom="getNext">
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image :src="slotProp.item['img_thumb']" loading="lazy"
|
||||
@click="showTask(slotProp.item)">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<div class="footer" v-if="isOver">
|
||||
<span>没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 任务详情弹框 -->
|
||||
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<div class="img-container" :style="{maxHeight: fullImgHeight+'px'}">
|
||||
<el-image :src="item['img_url']" fit="contain">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="task-info">
|
||||
<div class="info-line">
|
||||
<el-divider>
|
||||
正向提示词
|
||||
</el-divider>
|
||||
<div class="prompt">
|
||||
<span>{{ item.prompt }}</span>
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<el-divider>
|
||||
反向提示词
|
||||
</el-divider>
|
||||
<div class="prompt">
|
||||
<span>{{ item.params.negative_prompt }}</span>
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.params.negative_prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>采样方法:</label>
|
||||
<div class="item-value">{{ item.params.sampler }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>图片尺寸:</label>
|
||||
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>迭代步数:</label>
|
||||
<div class="item-value">{{ item.params.steps }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>引导系数:</label>
|
||||
<div class="item-value">{{ item.params.cfg_scale }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>随机因子:</label>
|
||||
<div class="item-value">{{ item.params.seed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.params.hd_fix">
|
||||
<el-divider>
|
||||
高清修复
|
||||
</el-divider>
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>重绘幅度:</label>
|
||||
<div class="item-value">{{ item.params.hd_redraw_rate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>放大算法:</label>
|
||||
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>放大倍数:</label>
|
||||
<div class="item-value">{{ item.params.hd_scale }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>迭代步数:</label>
|
||||
<div class="item-value">{{ item.params.hd_steps }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copy-params">
|
||||
<el-button type="primary" round @click="drawSameSd(item)">画一张同款的</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, onUnmounted, ref} from "vue"
|
||||
import {DocumentCopy, Picture} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const data = ref({
|
||||
"mj": [],
|
||||
"sd": []
|
||||
})
|
||||
const loading = ref(true)
|
||||
const isOver = ref(false)
|
||||
const imgType = ref("mj") // 图片类别
|
||||
const listBoxHeight = window.innerHeight - 74
|
||||
const colWidth = ref(240)
|
||||
const fullImgHeight = ref(window.innerHeight - 60)
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
|
||||
// 计算瀑布流列宽度
|
||||
const calcColWidth = () => {
|
||||
const listBoxWidth = window.innerWidth - 60 - 80
|
||||
const rows = Math.floor(listBoxWidth / colWidth.value)
|
||||
colWidth.value = Math.floor((listBoxWidth - (rows - 1) * 12) / rows)
|
||||
}
|
||||
calcColWidth()
|
||||
window.onresize = () => {
|
||||
calcColWidth()
|
||||
}
|
||||
|
||||
const page = ref(0)
|
||||
const pageSize = ref(15)
|
||||
// 获取下一页数据
|
||||
const getNext = () => {
|
||||
if (isOver.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
const url = imgType.value === "mj" ? "/api/mj/imgWall" : "/api/sd/imgWall"
|
||||
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
loading.value = false
|
||||
if (res.data.length === 0) {
|
||||
isOver.value = 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 (data.value[imgType.value].length === 0) {
|
||||
data.value[imgType.value] = imageList
|
||||
return
|
||||
}
|
||||
|
||||
if (imageList.length < pageSize.value) {
|
||||
isOver.value = true
|
||||
}
|
||||
data.value[imgType.value] = data.value[imgType.value].concat(imageList)
|
||||
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取图片失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
getNext()
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard('.copy-prompt-wall');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const changeImgType = () => {
|
||||
document.getElementById('waterfall-box').scrollTo(0, 0)
|
||||
page.value = 0
|
||||
data.value = {
|
||||
"mj": [],
|
||||
"sd": []
|
||||
}
|
||||
loading.value = true
|
||||
isOver.value = false
|
||||
nextTick(() => getNext())
|
||||
}
|
||||
|
||||
const showTask = (row) => {
|
||||
item.value = row
|
||||
showTaskDialog.value = true
|
||||
}
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const drawSameSd = (row) => {
|
||||
router.push({name: "image-sd", params: {copyParams: JSON.stringify(row.params)}})
|
||||
}
|
||||
|
||||
const drawSameMj = (row) => {
|
||||
router.push({name: "image-mj", params: {prompt: row.prompt}})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/images-wall.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
262
new-ui/projects/web/src/views/Invitation.vue
Normal file
262
new-ui/projects/web/src/views/Invitation.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="custom-scroll">
|
||||
<div class="page-invitation">
|
||||
<div class="inner">
|
||||
<h2>会员推广计划</h2>
|
||||
<div class="share-box">
|
||||
<div class="info">
|
||||
我们非常欢迎您把此应用分享给您身边的朋友,分享成功注册后您将获得 <strong>{{ inviteChatCalls }}</strong>
|
||||
次对话额度以及
|
||||
<strong>{{ inviteImgCalls }}</strong> 次AI绘画额度作为奖励。
|
||||
你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友。
|
||||
</div>
|
||||
|
||||
<div class="invite-qrcode">
|
||||
<el-image :src="qrImg"/>
|
||||
</div>
|
||||
|
||||
<div class="invite-url">
|
||||
<span>{{ inviteURL }}</span>
|
||||
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL">复制链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="invite-stats">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="item-box yellow">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10">
|
||||
<div class="item-icon">
|
||||
<i class="iconfont icon-role"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="item-info">
|
||||
<div class="num">{{ hits }}</div>
|
||||
<div class="text">点击量</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="item-box blue">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10">
|
||||
<div class="item-icon">
|
||||
<i class="iconfont icon-order"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="item-info">
|
||||
<div class="num">{{ regNum }}</div>
|
||||
<div class="text">注册量</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="item-box green">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10">
|
||||
<div class="item-icon">
|
||||
<i class="iconfont icon-chart"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="item-info">
|
||||
<div class="num">{{ rate }}%</div>
|
||||
<div class="text">转化率</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<h2>您推荐用户</h2>
|
||||
|
||||
<div class="invite-logs">
|
||||
<invite-list v-if="isLogin"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import QRCode from "qrcode";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import InviteList from "@/components/InviteList.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const inviteURL = ref("")
|
||||
const qrImg = ref("")
|
||||
const inviteChatCalls = ref(0)
|
||||
const inviteImgCalls = ref(0)
|
||||
const hits = ref(0)
|
||||
const regNum = ref(0)
|
||||
const rate = ref(0)
|
||||
const router = useRouter()
|
||||
const isLogin = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
isLogin.value = true
|
||||
httpGet("/api/invite/code").then(res => {
|
||||
const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}`
|
||||
hits.value = res.data["hits"]
|
||||
regNum.value = res.data["reg_num"]
|
||||
if (hits.value > 0) {
|
||||
rate.value = ((regNum.value / hits.value) * 100).toFixed(2)
|
||||
}
|
||||
QRCode.toDataURL(text, {width: 400, height: 400, margin: 2}, (error, url) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
} else {
|
||||
qrImg.value = url;
|
||||
}
|
||||
});
|
||||
inviteURL.value = text
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取邀请码失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||
inviteChatCalls.value = res.data["invite_chat_calls"]
|
||||
inviteImgCalls.value = res.data["invite_img_calls"]
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
router.push('/login')
|
||||
});
|
||||
|
||||
// 复制链接
|
||||
const clipboard = new Clipboard('.copy-link');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功!');
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
.page-invitation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #282c34;
|
||||
height 100vh
|
||||
overflow-x hidden
|
||||
overflow-y visible
|
||||
|
||||
.inner {
|
||||
display flex
|
||||
flex-flow column
|
||||
max-width 1000px
|
||||
width 100%
|
||||
color #e1e1e1
|
||||
|
||||
h2 {
|
||||
color #ffffff;
|
||||
}
|
||||
|
||||
.share-box {
|
||||
.info {
|
||||
line-height 1.5
|
||||
border 1px solid #444444
|
||||
border-radius 10px
|
||||
padding 10px
|
||||
|
||||
strong {
|
||||
color #f56c6c
|
||||
}
|
||||
}
|
||||
|
||||
.invite-qrcode {
|
||||
padding 20px
|
||||
text-align center
|
||||
}
|
||||
|
||||
.invite-url {
|
||||
padding 15px
|
||||
display flex
|
||||
justify-content space-between
|
||||
border 1px solid #444444
|
||||
border-radius 10px
|
||||
|
||||
span {
|
||||
position relative
|
||||
font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif
|
||||
top 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-stats {
|
||||
padding 30px 10px
|
||||
|
||||
.item-box {
|
||||
border-radius 10px
|
||||
padding 0 10px
|
||||
|
||||
.el-col {
|
||||
height 140px
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.iconfont {
|
||||
font-size 60px
|
||||
}
|
||||
|
||||
.item-info {
|
||||
font-size 18px
|
||||
|
||||
.text, .num {
|
||||
padding 3px 0
|
||||
text-align center
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size 40px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.yellow {
|
||||
background-color #ffeecc
|
||||
color #D68F00
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color #D6E4FF
|
||||
color #1062FE
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color #E7F8EB
|
||||
color #2D9F46
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.invite-logs {
|
||||
padding-bottom 20px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
41
new-ui/projects/web/src/views/Knowledge.vue
Normal file
41
new-ui/projects/web/src/views/Knowledge.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="page-knowledge" :style="{ height: winHeight + 'px' }">
|
||||
<div class="inner">
|
||||
<h1>会员知识库搜索</h1>
|
||||
<h2>页面正在紧锣密鼓开发中,敬请期待!</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue"
|
||||
|
||||
const winHeight = ref(window.innerHeight)
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.page-knowledge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items center
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
text-align center
|
||||
|
||||
h1 {
|
||||
color: #202020;
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
204
new-ui/projects/web/src/views/Login.vue
Normal file
204
new-ui/projects/web/src/views/Login.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg"></div>
|
||||
<div class="main">
|
||||
<div class="contain">
|
||||
<div class="logo">
|
||||
<el-image src="images/logo.png" fit="cover"/>
|
||||
</div>
|
||||
<div class="header">{{ title }}</div>
|
||||
<div class="content">
|
||||
<div class="block">
|
||||
<el-input placeholder="手机号/邮箱地址" size="large" v-model="username" autocomplete="off" autofocus
|
||||
@keyup="handleKeyup">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码" size="large" v-model="password" show-password autocomplete="off"
|
||||
@keyup="handleKeyup">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-row class="btn-row">
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-row class="text-line" gutter="20">
|
||||
<el-button type="primary" @click="router.push('/register')" size="small" plain>注册新账号</el-button>
|
||||
<el-button type="success" @click="showResetPass = true" size="small" plain>重置密码</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<reset-pass @hide="showResetPass = false" :show="showResetPass"/>
|
||||
|
||||
<footer class="footer">
|
||||
<footer-bar/>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {useRouter} from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
import {isMobile} from "@/utils/libs";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {setUserToken} from "@/store/session";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
import {prevRoute} from "@/router";
|
||||
import ResetPass from "@/components/ResetPass.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('ChatPlus 用户登录');
|
||||
const username = ref(process.env.VUE_APP_USER);
|
||||
const password = ref(process.env.VUE_APP_PASS);
|
||||
const showResetPass = ref(false)
|
||||
|
||||
checkSession().then(() => {
|
||||
if (isMobile()) {
|
||||
router.push('/mobile')
|
||||
} else {
|
||||
router.push('/chat')
|
||||
}
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
login();
|
||||
}
|
||||
};
|
||||
|
||||
const login = function () {
|
||||
if (username.value.trim() === '') {
|
||||
return ElMessage.error("请输入用户民")
|
||||
}
|
||||
if (password.value.trim() === '') {
|
||||
return ElMessage.error('请输入密码');
|
||||
}
|
||||
|
||||
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
||||
setUserToken(res.data)
|
||||
if (prevRoute.path === '' || prevRoute.path === '/register') {
|
||||
if (isMobile()) {
|
||||
router.push('/mobile')
|
||||
} else {
|
||||
router.push('/chat')
|
||||
}
|
||||
} else {
|
||||
router.push(prevRoute.path)
|
||||
}
|
||||
|
||||
}).catch((e) => {
|
||||
ElMessage.error('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 24px
|
||||
font-size 24px
|
||||
color $white_v1
|
||||
letter-space 2px
|
||||
text-align center
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
color #ffffff;
|
||||
|
||||
.container {
|
||||
padding 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
366
new-ui/projects/web/src/views/Member.vue
Normal file
366
new-ui/projects/web/src/views/Member.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="member custom-scroll">
|
||||
<div class="title">
|
||||
会员充值中心
|
||||
</div>
|
||||
<div class="inner" :style="{height: listBoxHeight + 'px'}">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="7">
|
||||
<div class="user-profile">
|
||||
<user-profile/>
|
||||
|
||||
<el-row class="user-opt" :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="showPasswordDialog = true">修改密码</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="showBindMobileDialog = true">更改账号</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" v-if="enableReward" @click="showRewardDialog = true">加入众筹</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" v-if="enableReward" @click="showRewardVerifyDialog = true">众筹核销
|
||||
</el-button>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" style="padding-top: 30px">
|
||||
<el-button type="danger" round @click="logout">退出登录</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="17">
|
||||
<div class="product-box">
|
||||
<div class="info" v-if="orderPayInfoText !== ''">
|
||||
<el-alert type="info" show-icon :closable="false" effect="dark">
|
||||
<strong>说明:</strong> {{ orderPayInfoText }}
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<ItemList :items="list" v-if="list.length > 0" :gap="30" :width="240">
|
||||
<template #default="scope">
|
||||
<div class="product-item" :style="{width: scope.width+'px'}">
|
||||
<div class="image-container">
|
||||
<el-image :src="vipImg" fit="cover"/>
|
||||
</div>
|
||||
<div class="product-title">
|
||||
<span class="name">{{ scope.item.name }}</span>
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<div class="info-line">
|
||||
<span class="label">商品原价:</span>
|
||||
<span class="price">¥{{ scope.item.price }}</span>
|
||||
</div>
|
||||
<div class="info-line">
|
||||
<span class="label">促销立减:</span>
|
||||
<span class="price">¥{{ scope.item.discount }}</span>
|
||||
</div>
|
||||
<div class="info-line">
|
||||
<span class="label">有效期:</span>
|
||||
<span class="expire" v-if="scope.item.days > 0">{{ scope.item.days }}天</span>
|
||||
<span class="expire" v-else>长期有效</span>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<span class="label">对话次数:</span>
|
||||
<span class="calls" v-if="scope.item.calls > 0">{{ scope.item.calls }}</span>
|
||||
<span class="calls" v-else>{{ vipMonthCalls }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<span class="label">绘图次数:</span>
|
||||
<span class="calls" v-if="scope.item.img_calls > 0">{{ scope.item.img_calls }}</span>
|
||||
<span class="calls" v-else>{{ vipMonthImgCalls }}</span>
|
||||
</div>
|
||||
|
||||
<div class="pay-way">
|
||||
<el-button type="primary" @click="alipay(scope.item)" size="small" v-if="payWays['alipay']">
|
||||
<i class="iconfont icon-alipay"></i> 支付宝
|
||||
</el-button>
|
||||
<el-button type="success" @click="huPiPay(scope.item)" size="small" v-if="payWays['hupi']">
|
||||
<span v-if="payWays['hupi']['name'] === 'wechat'"><i class="iconfont icon-wechat-pay"></i> 微信</span>
|
||||
<span v-else><i class="iconfont icon-alipay"></i> 支付宝</span>
|
||||
</el-button>
|
||||
|
||||
<el-button type="success" @click="PayJs(scope.item)" size="small" v-if="payWays['payjs']">
|
||||
<span><i class="iconfont icon-wechat-pay"></i> 微信</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
|
||||
<h2 class="headline">消费账单</h2>
|
||||
|
||||
<div class="user-order">
|
||||
<user-order v-if="isLogin"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
|
||||
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false"/>
|
||||
|
||||
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"
|
||||
@logout="logout"/>
|
||||
|
||||
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :username="user.username"
|
||||
@hide="showBindMobileDialog = false"/>
|
||||
|
||||
<reward-verify v-if="isLogin" :show="showRewardVerifyDialog" @hide="showRewardVerifyDialog = false"/>
|
||||
|
||||
<el-dialog
|
||||
v-model="showRewardDialog"
|
||||
:show-close="true"
|
||||
width="400px"
|
||||
title="参与众筹"
|
||||
>
|
||||
<el-alert type="info" :closable="false">
|
||||
<div style="font-size: 14px">您好,众筹 9.9元,就可以兑换 100 次对话,以此来覆盖我们的 OpenAI
|
||||
账单和服务器的费用。<strong
|
||||
style="color: #f56c6c">由于本人没有开通微信支付,付款后请凭借转账单号,点击【众筹核销】按钮手动核销。</strong>
|
||||
</div>
|
||||
</el-alert>
|
||||
<div style="text-align: center;padding-top: 10px;">
|
||||
<el-image v-if="enableReward" :src="rewardImg"/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="showPayDialog"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="true"
|
||||
:width="400"
|
||||
@close="closeOrder"
|
||||
title="充值订单支付">
|
||||
<div class="pay-container">
|
||||
<h3 class="amount">实付金额:<span>¥{{ amount }}</span></h3>
|
||||
<div class="count-down">
|
||||
<count-down :second="orderTimeout" @timeout="refreshPayCode" ref="countDownRef"/>
|
||||
</div>
|
||||
|
||||
<div class="pay-qrcode" v-loading="loading">
|
||||
<el-image :src="qrcode"/>
|
||||
</div>
|
||||
|
||||
<div class="tip success" v-if="text !== ''">
|
||||
<el-icon>
|
||||
<SuccessFilled/>
|
||||
</el-icon>
|
||||
<span class="text">{{ text }}</span>
|
||||
</div>
|
||||
<div class="tip" v-else>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
<span class="text">请打开手机{{ payName }}扫码支付</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import {InfoFilled, SuccessFilled} from "@element-plus/icons-vue";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import UserProfile from "@/components/UserProfile.vue";
|
||||
import PasswordDialog from "@/components/PasswordDialog.vue";
|
||||
import BindMobile from "@/components/ResetAccount.vue";
|
||||
import RewardVerify from "@/components/RewardVerify.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {removeUserToken} from "@/store/session";
|
||||
import UserOrder from "@/components/UserOrder.vue";
|
||||
import CountDown from "@/components/CountDown.vue";
|
||||
|
||||
const listBoxHeight = window.innerHeight - 97
|
||||
const list = ref([])
|
||||
const showLoginDialog = ref(false)
|
||||
const showPayDialog = ref(false)
|
||||
const vipImg = ref("/images/vip.png")
|
||||
const enableReward = ref(false) // 是否启用众筹功能
|
||||
const rewardImg = ref('/images/reward.png')
|
||||
const qrcode = ref("")
|
||||
const showPasswordDialog = ref(false);
|
||||
const showBindMobileDialog = ref(false);
|
||||
const showRewardDialog = ref(false);
|
||||
const showRewardVerifyDialog = ref(false);
|
||||
const text = ref("")
|
||||
const user = ref(null)
|
||||
const isLogin = ref(false)
|
||||
const router = useRouter()
|
||||
const curPayProduct = ref(null)
|
||||
const activeOrderNo = ref("")
|
||||
const countDownRef = ref(null)
|
||||
const orderTimeout = ref(1800)
|
||||
const loading = ref(true)
|
||||
const orderPayInfoText = ref("")
|
||||
const vipMonthCalls = ref(0)
|
||||
const vipMonthImgCalls = ref(0)
|
||||
|
||||
const payWays = ref({})
|
||||
const amount = ref(0)
|
||||
const payName = ref("支付宝")
|
||||
const curPay = ref("alipay") // 当前支付方式
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(_user => {
|
||||
user.value = _user
|
||||
isLogin.value = true
|
||||
httpGet("/api/product/list").then((res) => {
|
||||
list.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取产品套餐失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
router.push("/login")
|
||||
})
|
||||
|
||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||
rewardImg.value = res.data['reward_img']
|
||||
enableReward.value = res.data['enabled_reward']
|
||||
orderPayInfoText.value = res.data['order_pay_info_text']
|
||||
if (res.data['order_pay_timeout'] > 0) {
|
||||
orderTimeout.value = res.data['order_pay_timeout']
|
||||
}
|
||||
vipMonthCalls.value = res.data['vip_month_calls']
|
||||
vipMonthImgCalls.value = res.data['vip_month_img_calls']
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/payment/payWays").then(res => {
|
||||
payWays.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取支付方式失败:" + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
// refresh payment qrcode
|
||||
const refreshPayCode = () => {
|
||||
if (curPay.value === 'alipay') {
|
||||
alipay()
|
||||
} else if (curPay.value === 'hupi') {
|
||||
huPiPay()
|
||||
}
|
||||
}
|
||||
|
||||
const genPayQrcode = () => {
|
||||
loading.value = true
|
||||
text.value = ""
|
||||
httpPost("/api/payment/qrcode", {
|
||||
pay_way: curPay.value,
|
||||
product_id: curPayProduct.value.id,
|
||||
user_id: user.value.id
|
||||
}).then(res => {
|
||||
showPayDialog.value = true
|
||||
qrcode.value = res.data['image']
|
||||
activeOrderNo.value = res.data['order_no']
|
||||
queryOrder(activeOrderNo.value)
|
||||
loading.value = false
|
||||
// 重置计数器
|
||||
if (countDownRef.value) {
|
||||
countDownRef.value.resetTimer()
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error("生成支付订单失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const alipay = (row) => {
|
||||
payName.value = "支付宝"
|
||||
curPay.value = "alipay"
|
||||
amount.value = (row.price - row.discount).toFixed(2)
|
||||
if (!user.value.id) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
curPayProduct.value = row
|
||||
}
|
||||
genPayQrcode()
|
||||
}
|
||||
|
||||
// 虎皮椒支付
|
||||
const huPiPay = (row) => {
|
||||
payName.value = payWays.value["hupi"]["name"] === "wechat" ? '微信' : '支付宝'
|
||||
curPay.value = "hupi"
|
||||
amount.value = (row.price - row.discount).toFixed(2)
|
||||
if (!user.value.id) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
curPayProduct.value = row
|
||||
}
|
||||
genPayQrcode()
|
||||
}
|
||||
|
||||
// PayJS 支付
|
||||
const PayJs = (row) => {
|
||||
payName.value = '微信'
|
||||
curPay.value = "payjs"
|
||||
amount.value = (row.price - row.discount).toFixed(2)
|
||||
if (!user.value.id) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
curPayProduct.value = row
|
||||
}
|
||||
genPayQrcode()
|
||||
}
|
||||
|
||||
const queryOrder = (orderNo) => {
|
||||
httpPost("/api/payment/query", {order_no: orderNo}).then(res => {
|
||||
if (res.data.status === 1) {
|
||||
text.value = "扫码成功,请在手机上进行支付!"
|
||||
queryOrder(orderNo)
|
||||
} else if (res.data.status === 2) {
|
||||
text.value = "支付成功,正在刷新页面"
|
||||
if (curPay.value === "payjs") {
|
||||
setTimeout(() => location.reload(), 3000)
|
||||
} else {
|
||||
setTimeout(() => location.reload(), 500)
|
||||
}
|
||||
} else {
|
||||
// 如果当前订单没有过期,继续等待订单的下一个状态
|
||||
if (activeOrderNo.value === orderNo) {
|
||||
queryOrder(orderNo)
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error("查询支付状态失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const logout = function () {
|
||||
httpGet('/api/user/logout').then(() => {
|
||||
removeUserToken();
|
||||
router.push('/login');
|
||||
}).catch(() => {
|
||||
ElMessage.error('注销失败!');
|
||||
})
|
||||
}
|
||||
|
||||
const closeOrder = () => {
|
||||
activeOrderNo.value = ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import "@/assets/css/member.styl"
|
||||
</style>
|
||||
328
new-ui/projects/web/src/views/Register.vue
Normal file
328
new-ui/projects/web/src/views/Register.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg"></div>
|
||||
<div class="register-page">
|
||||
<div class="page-inner">
|
||||
<div class="contain" v-if="enableRegister">
|
||||
<div class="logo">
|
||||
<el-image src="images/logo.png" fit="cover"/>
|
||||
</div>
|
||||
|
||||
<div class="header">{{ title }}</div>
|
||||
<div class="content">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef">
|
||||
<div class="block">
|
||||
<el-input :placeholder="placeholder"
|
||||
size="large"
|
||||
v-model="formData.username"
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Iphone/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码(8-16位)"
|
||||
maxlength="16" size="large"
|
||||
v-model="formData.password" show-password
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="重复密码(8-16位)"
|
||||
size="large" maxlength="16" v-model="formData.repass" show-password
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block" v-if="enableMobile || enableEmail">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-input placeholder="验证码"
|
||||
size="large" maxlength="30"
|
||||
v-model="formData.code"
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Checked/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<send-msg size="large" :receiver="formData.username"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="邀请码"
|
||||
size="large"
|
||||
v-model="formData.invite_code"
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-row class="btn-row">
|
||||
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-row class="text-line">
|
||||
已经有账号?
|
||||
<el-link type="primary" @click="router.push('/login')">登录</el-link>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip-result" v-else>
|
||||
<el-result icon="error" title="注册功能已关闭">
|
||||
<template #sub-title>
|
||||
<p>抱歉,系统已关闭注册功能,请联系管理员添加账号!</p>
|
||||
<div class="wechat-card">
|
||||
<el-image :src="wxImg"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<footer-bar/>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
import {Checked, Iphone, Lock, Message} from "@element-plus/icons-vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {useRouter} from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {arrayContains} from "@/utils/libs";
|
||||
import {setUserToken} from "@/store/session";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('ChatPlus 用户注册');
|
||||
const formData = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
repass: '',
|
||||
invite_code: router.currentRoute.value.query['invite_code'],
|
||||
})
|
||||
const formRef = ref(null)
|
||||
const enableMobile = ref(false)
|
||||
const enableEmail = ref(false)
|
||||
const enableRegister = ref(true)
|
||||
const wxImg = ref("/images/wx.png")
|
||||
const ways = []
|
||||
const placeholder = ref("用户名:")
|
||||
|
||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||
if (res.data) {
|
||||
const registerWays = res.data['register_ways']
|
||||
if (arrayContains(registerWays, "mobile")) {
|
||||
enableMobile.value = true
|
||||
ways.push("手机号")
|
||||
}
|
||||
if (arrayContains(registerWays, "email")) {
|
||||
enableEmail.value = true
|
||||
ways.push("邮箱地址")
|
||||
}
|
||||
placeholder.value += ways.join("/")
|
||||
// 是否启用注册
|
||||
enableRegister.value = res.data['enabled_register']
|
||||
// 覆盖微信二维码
|
||||
if (res.data['wechat_card_url'] !== '') {
|
||||
wxImg.value = res.data['wechat_card_url']
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/invite/hits", {code: formData.value.invite_code}).then(() => {
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
|
||||
const register = function () {
|
||||
if (formData.value.username === '') {
|
||||
return ElMessage.error('请输入用户名');
|
||||
}
|
||||
|
||||
if (formData.value.password.length < 8) {
|
||||
return ElMessage.error('密码的长度为8-16个字符');
|
||||
}
|
||||
if (formData.value.repass !== formData.value.password) {
|
||||
return ElMessage.error('两次输入密码不一致');
|
||||
}
|
||||
|
||||
if ((enableEmail.value || enableMobile.value) && formData.value.code === '') {
|
||||
return ElMessage.error('请输入验证码');
|
||||
}
|
||||
httpPost('/api/user/register', formData.value).then((res) => {
|
||||
setUserToken(res.data)
|
||||
ElMessage.success({
|
||||
"message": "注册成功,即将跳转到对话主界面...",
|
||||
onClose: () => router.push("/chat"),
|
||||
duration: 1000
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('注册失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.bg {
|
||||
position fixed
|
||||
left 0
|
||||
right 0
|
||||
top 0
|
||||
bottom 0
|
||||
background-color #091519
|
||||
background-image url("~@/assets/img/reg-bg.jpg")
|
||||
background-size cover
|
||||
background-position center
|
||||
background-repeat no-repeat
|
||||
//filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
|
||||
}
|
||||
|
||||
.register-page {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.page-inner {
|
||||
max-width 450px
|
||||
min-width 360px
|
||||
height 100vh
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.contain {
|
||||
padding 0 40px 20px 40px;
|
||||
width 100%
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
z-index 10
|
||||
background-color rgba(255, 255, 255, 0.3)
|
||||
|
||||
.logo {
|
||||
text-align center
|
||||
|
||||
.el-image {
|
||||
width 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 24px
|
||||
font-size 24px
|
||||
color $white_v1
|
||||
letter-space 2px
|
||||
text-align center
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tip-result {
|
||||
z-index 10
|
||||
|
||||
.wechat-card {
|
||||
padding 20px
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
color #ffffff;
|
||||
|
||||
.container {
|
||||
padding 20px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="stylus">
|
||||
.register-page {
|
||||
.el-result {
|
||||
|
||||
border-radius 10px;
|
||||
background-color rgba(14, 25, 30, 0.6)
|
||||
border 1px solid #666
|
||||
|
||||
.el-result__title p {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
.el-result__subtitle p {
|
||||
color #c1c1c1
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
new-ui/projects/web/src/views/Test.vue
Normal file
16
new-ui/projects/web/src/views/Test.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
|
||||
const title = ref('Test Page')
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
||||
301
new-ui/projects/web/src/views/admin/ApiKey.vue
Normal file
301
new-ui/projects/web/src/views/admin/ApiKey.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
<a href="https://gpt.bemore.lol" target="_blank" style="margin-left: 10px">
|
||||
<el-button type="success" :icon="ShoppingCart" @click="add" plain>购买API-KEY</el-button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="platform" label="所属平台"/>
|
||||
<el-table-column prop="name" label="名称"/>
|
||||
<el-table-column prop="value" label="KEY">
|
||||
<template #default="scope">
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
:content="scope.row.api_url"
|
||||
placement="top">{{ scope.row.value }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="用途">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.type === 'chat'">聊天</el-tag>
|
||||
<el-tag v-else-if="scope.row.type === 'img'" type="success">绘图</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="use_proxy" label="使用代理">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row['use_proxy']" @change="set('use_proxy',scope.row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="最后使用时间">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row['last_used_at']">{{ scope.row['last_used_at'] }}</span>
|
||||
<el-tag v-else>未使用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enabled" label="启用状态">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row['enabled']" @change="set('enabled',scope.row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="false"
|
||||
:title="title"
|
||||
>
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 10px; font-size:14px;">
|
||||
<p><b>注意:</b>如果是百度文心一言平台,API-KEY 为 APIKey|SecretKey,中间用竖线(|)连接</p>
|
||||
<p><b>注意:</b>如果是讯飞星火大模型,API-KEY 为 AppId|APIKey|APISecret,中间用竖线(|)连接</p>
|
||||
</el-alert>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="所属平台:" prop="platform">
|
||||
<el-select v-model="item.platform" placeholder="请选择平台" @change="changePlatform">
|
||||
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
|
||||
item.name
|
||||
}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称:" prop="name">
|
||||
<el-input v-model="item.name" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用途:" prop="type">
|
||||
<el-select v-model="item.type" placeholder="请选择用途" @change="changePlatform">
|
||||
<el-option v-for="item in types" :value="item.value" :label="item.name" :key="item.value">{{
|
||||
item.name
|
||||
}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API KEY:" prop="value">
|
||||
<el-input v-model="item.value" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="API URL:" prop="api_url">
|
||||
<el-input v-model="item.api_url" autocomplete="off"
|
||||
placeholder="如果你用了第三方的 API 中转,这里填写中转地址"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="使用代理:" prop="use_proxy">
|
||||
<el-switch v-model="item.use_proxy"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="是否使用代理访问 API URL,OpenAI 官方API需要开启代理访问"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态:" prop="enable">
|
||||
<el-switch v-model="item.enabled"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">提交</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs";
|
||||
import {InfoFilled, Plus, ShoppingCart} from "@element-plus/icons-vue";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const item = ref({})
|
||||
const showDialog = ref(false)
|
||||
const rules = reactive({
|
||||
platform: [{required: true, message: '请选择平台', trigger: 'change',}],
|
||||
name: [{required: true, message: '请输入名称', trigger: 'change',}],
|
||||
type: [{required: true, message: '请选择用途', trigger: 'change',}],
|
||||
value: [{required: true, message: '请输入 API KEY 值', trigger: 'change',}]
|
||||
})
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const title = ref("")
|
||||
const platforms = ref([
|
||||
{
|
||||
name: "【OpenAI】ChatGPT",
|
||||
value: "OpenAI",
|
||||
api_url: "https://gpt.bemore.lol/v1/chat/completions",
|
||||
img_url: "https://gpt.bemore.lol/v1/images/generations"
|
||||
},
|
||||
{
|
||||
name: "【讯飞】星火大模型",
|
||||
value: "XunFei",
|
||||
api_url: "wss://spark-api.xf-yun.com/{version}/chat"
|
||||
},
|
||||
{
|
||||
name: "【清华智普】ChatGLM",
|
||||
value: "ChatGLM",
|
||||
api_url: "https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke"
|
||||
},
|
||||
{
|
||||
name: "【百度】文心一言",
|
||||
value: "Baidu",
|
||||
api_url: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}"
|
||||
},
|
||||
{
|
||||
name: "【微软】Azure",
|
||||
value: "Azure",
|
||||
api_url: "https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15"
|
||||
},
|
||||
{
|
||||
name: "【阿里】千义通问",
|
||||
value: "QWen",
|
||||
api_url: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
|
||||
},
|
||||
])
|
||||
const types = ref([
|
||||
{name: "聊天", value: "chat"},
|
||||
{name: "绘画", value: "img"},
|
||||
])
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/apikey/list').then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
|
||||
const add = function () {
|
||||
showDialog.value = true
|
||||
title.value = "新增 API KEY"
|
||||
item.value = {}
|
||||
}
|
||||
|
||||
const edit = function (row) {
|
||||
showDialog.value = true
|
||||
title.value = "修改 API KEY"
|
||||
item.value = row
|
||||
}
|
||||
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/apikey/save', item.value).then((res) => {
|
||||
ElMessage.success('操作成功!')
|
||||
if (!item.value['id']) {
|
||||
const newItem = res.data
|
||||
newItem.last_used_at = dateFormat(newItem.last_used_at)
|
||||
items.value.push(newItem)
|
||||
}
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet('/api/admin/apikey/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const set = (filed, row) => {
|
||||
httpPost('/api/admin/apikey/set', {id: row.id, filed: filed, value: row[filed]}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const changePlatform = () => {
|
||||
let platform = null
|
||||
for (let v of platforms.value) {
|
||||
if (v.value === item.value.platform) {
|
||||
platform = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if (platform !== null) {
|
||||
if (item.value.type === "img" && platform.img_url) {
|
||||
item.value.api_url = platform.img_url
|
||||
} else {
|
||||
item.value.api_url = platform.api_url
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__content {
|
||||
|
||||
.el-icon {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
388
new-ui/projects/web/src/views/admin/ChatList.vue
Normal file
388
new-ui/projects/web/src/views/admin/ChatList.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="container chat-list">
|
||||
<el-tabs v-model="activeName" @tab-change="handleChange">
|
||||
<el-tab-pane label="对话列表" name="chat" v-loading="data.chat.loading">
|
||||
<div class="handle-box">
|
||||
<el-input v-model.number="data.chat.query.user_id" placeholder="账户ID" class="handle-input mr10"
|
||||
@keyup="searchChat($event)"></el-input>
|
||||
<el-input v-model="data.chat.query.title" placeholder="对话标题" class="handle-input mr10"
|
||||
@keyup="searchChat($event)"></el-input>
|
||||
<el-input v-model="data.chat.query.model" placeholder="模型" class="handle-input mr10"
|
||||
@keyup="searchChat($event)"></el-input>
|
||||
<el-date-picker
|
||||
v-model="data.chat.query.created_at"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="fetchChatData">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="data.chat.items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="user_id" label="账户ID"/>
|
||||
<el-table-column prop="username" label="账户"/>
|
||||
<el-table-column prop="title" label="标题"/>
|
||||
<el-table-column prop="model" label="模型"/>
|
||||
<el-table-column prop="msg_num" label="消息数量"/>
|
||||
<el-table-column prop="token" label="消耗算力"/>
|
||||
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="showMessages(scope.row)">查看</el-button>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeChat(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="data.chat.total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="data.chat.page"
|
||||
v-model:page-size="data.chat.pageSize"
|
||||
@current-change="fetchChatData()"
|
||||
:total="data.chat.total"/>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="消息记录" name="message">
|
||||
<div class="handle-box">
|
||||
<el-input v-model.number="data.message.query.user_id" placeholder="账户ID" class="handle-input mr10"
|
||||
@keyup="searchMessage($event)"></el-input>
|
||||
<el-input v-model="data.message.query.content" placeholder="消息内容" class="handle-input mr10"
|
||||
@keyup="searchMessage($event)"></el-input>
|
||||
<el-input v-model="data.message.query.model" placeholder="模型" class="handle-input mr10"
|
||||
@keyup="searchMessage($event)"></el-input>
|
||||
<el-date-picker
|
||||
v-model="data.message.query.created_at"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="fetchMessageData">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="data.message.items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="user_id" label="账户ID"/>
|
||||
<el-table-column prop="username" label="账户"/>
|
||||
<el-table-column label="角色">
|
||||
<template #default="scope">
|
||||
<el-avatar :size="30" :src="scope.row.icon"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="model" label="模型"/>
|
||||
|
||||
<el-table-column label="消息内容">
|
||||
<template #default="scope">
|
||||
<el-text style="width: 200px" truncated @click="showContent(scope.row.content)">
|
||||
{{ scope.row.content }}
|
||||
</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="token" label="消耗算力"/>
|
||||
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="showContent(scope.row.content)">查看</el-button>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeMessage(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="data.message.total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="data.message.page"
|
||||
v-model:page-size="data.message.pageSize"
|
||||
@current-change="fetchMessageData()"
|
||||
:total="data.message.total"/>
|
||||
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
|
||||
<el-dialog
|
||||
v-model="showContentDialog"
|
||||
title="消息详情"
|
||||
>
|
||||
<div v-html="dialogContent" style="overflow: auto; max-height: 300px"></div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="showChatItemDialog"
|
||||
title="对话详情"
|
||||
>
|
||||
<div class="chat-box common-layout">
|
||||
<div v-for="item in messages" :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'"
|
||||
:icon="item.icon"
|
||||
:org-content="item.orgContent"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:tokens="item['tokens']"
|
||||
:content="item.content"/>
|
||||
</div>
|
||||
</div><!-- end chat box -->
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, processContent, removeArrayItem} from "@/utils/libs";
|
||||
import {Search} from "@element-plus/icons-vue";
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import hl from "highlight.js";
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
|
||||
// 变量定义
|
||||
const data = ref({
|
||||
"chat": {
|
||||
items: [],
|
||||
query: {title: "", created_at: [], page: 1, page_size: 15},
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
loading: true
|
||||
},
|
||||
"message": {
|
||||
items: [],
|
||||
query: {title: "", created_at: [], page: 1, page_size: 15},
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
loading: true
|
||||
}
|
||||
})
|
||||
const items = ref([])
|
||||
const query = ref({title: "", created_at: []})
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const loading = ref(true)
|
||||
const activeName = ref("chat")
|
||||
|
||||
onMounted(() => {
|
||||
fetchChatData()
|
||||
})
|
||||
|
||||
const handleChange = (tab) => {
|
||||
if (tab === "chat") {
|
||||
fetchChatData()
|
||||
} else if (tab === "message") {
|
||||
fetchMessageData()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索对话
|
||||
const searchChat = (evt) => {
|
||||
if (evt.keyCode === 13) {
|
||||
fetchChatData()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索消息
|
||||
const searchMessage = (evt) => {
|
||||
if (evt.keyCode === 13) {
|
||||
fetchMessageData()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchChatData = () => {
|
||||
const d = data.value.chat
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/chat/list', d.query).then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const fetchMessageData = () => {
|
||||
const d = data.value.message
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/chat/message', d.query).then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const removeChat = function (row) {
|
||||
httpGet('/api/admin/chat/remove?chat_id=' + row.chat_id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
fetchChatData()
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeMessage = function (row) {
|
||||
httpGet('/api/admin/chat/message/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
fetchMessageData()
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const latexPlugin = require('markdown-it-latex2img')
|
||||
const mathjaxPlugin = require('markdown-it-mathjax')
|
||||
const md = require('markdown-it')({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
|
||||
}
|
||||
|
||||
// 处理代码高亮
|
||||
const preCode = md.utils.escapeHtml(str)
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
|
||||
}
|
||||
});
|
||||
md.use(latexPlugin)
|
||||
md.use(mathjaxPlugin)
|
||||
|
||||
const showContentDialog = ref(false)
|
||||
const dialogContent = ref("")
|
||||
const showContent = (content) => {
|
||||
showContentDialog.value = true
|
||||
dialogContent.value = md.render(processContent(content))
|
||||
}
|
||||
|
||||
const showChatItemDialog = ref(false)
|
||||
const messages = ref([])
|
||||
const showMessages = (row) => {
|
||||
showChatItemDialog.value = true
|
||||
messages.value = []
|
||||
httpGet('/api/admin/chat/history?chat_id=' + row.chat_id).then(res => {
|
||||
const data = res.data
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i].orgContent = data[i].content;
|
||||
data[i].content = md.render(processContent(data[i].content))
|
||||
messages.value.push(data[i]);
|
||||
}
|
||||
}).catch(e => {
|
||||
// TODO: 显示重新加载按钮
|
||||
ElMessage.error('加载聊天记录失败:' + e.message);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.chat-list {
|
||||
|
||||
.handle-box {
|
||||
.handle-input {
|
||||
max-width 150px;
|
||||
margin-right 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
overflow-y: auto;
|
||||
overflow-x hidden
|
||||
|
||||
// 变量定义
|
||||
--content-font-size: 16px;
|
||||
--content-color: #c1c1c1;
|
||||
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
height 90vh
|
||||
|
||||
.chat-line {
|
||||
// 隐藏滚动条
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
256
new-ui/projects/web/src/views/admin/ChatModel.vue
Normal file
256
new-ui/projects/web/src/views/admin/ChatModel.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="platform" label="所属平台">
|
||||
<template #default="scope">
|
||||
<span class="sort" :data-id="scope.row.id">{{ scope.row.platform }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="模型名称"/>
|
||||
<el-table-column prop="value" label="模型值"/>
|
||||
<el-table-column prop="weight" label="对话权重"/>
|
||||
<el-table-column prop="enabled" label="启用状态">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row['enabled']" @change="modelSet('enabled',scope.row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enabled" label="开放状态">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row['open']" @change="modelSet('open',scope.row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="title"
|
||||
:close-on-click-modal="false"
|
||||
style="width: 90%; max-width: 600px;"
|
||||
>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="所属平台:" prop="platform">
|
||||
<el-select v-model="item.platform" placeholder="请选择平台">
|
||||
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
|
||||
item.name
|
||||
}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型名称:" prop="name">
|
||||
<el-input v-model="item.name" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型值:" prop="value">
|
||||
<el-input v-model="item.value" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="对话权重:" prop="weight">
|
||||
|
||||
<template #default>
|
||||
<div class="tip-input">
|
||||
<el-input-number :min="1" v-model="item.weight" autocomplete="off"/>
|
||||
<div class="info">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="对话权重,每次对话扣减多少次对话额度"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态:" prop="enable">
|
||||
<el-switch v-model="item.enabled"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开放状态:" prop="open">
|
||||
<el-switch v-model="item.open"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">提交</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||
import {InfoFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {Sortable} from "sortablejs";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const item = ref({})
|
||||
const showDialog = ref(false)
|
||||
const title = ref("")
|
||||
const rules = reactive({
|
||||
platform: [{required: true, message: '请选择平台', trigger: 'change',}],
|
||||
name: [{required: true, message: '请输入模型名称', trigger: 'change',}],
|
||||
value: [{required: true, message: '请输入模型值', trigger: 'change',}]
|
||||
})
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const platforms = ref([
|
||||
{name: "【OpenAI】ChatGPT", value: "OpenAI"},
|
||||
{name: "【讯飞】星火大模型", value: "XunFei"},
|
||||
{name: "【清华智普】ChatGLM", value: "ChatGLM"},
|
||||
{name: "【百度】文心一言", value: "Baidu"},
|
||||
{name: "【微软】Azure", value: "Azure"},
|
||||
{name: "【阿里】通义千问", value: "QWen"},
|
||||
|
||||
])
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/model/list').then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
|
||||
// 初始化拖动排序插件
|
||||
Sortable.create(drawBodyWrapper, {
|
||||
sort: true,
|
||||
animation: 500,
|
||||
onEnd({newIndex, oldIndex, from}) {
|
||||
if (oldIndex === newIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
|
||||
const ids = []
|
||||
const sorts = []
|
||||
sortedData.forEach((id, index) => {
|
||||
ids.push(parseInt(id))
|
||||
sorts.push(index)
|
||||
items.value[index].sort_num = index
|
||||
})
|
||||
|
||||
httpPost("/api/admin/model/sort", {ids: ids, sorts: sorts}).then(() => {
|
||||
}).catch(e => {
|
||||
ElMessage.error("排序失败:" + e.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const add = function () {
|
||||
title.value = "新增模型"
|
||||
showDialog.value = true
|
||||
item.value = {enabled: true, weight: 1}
|
||||
}
|
||||
|
||||
const edit = function (row) {
|
||||
title.value = "修改模型"
|
||||
showDialog.value = true
|
||||
item.value = row
|
||||
}
|
||||
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/model/save', item.value).then((res) => {
|
||||
ElMessage.success('操作成功!')
|
||||
if (!item.value['id']) {
|
||||
const newItem = res.data
|
||||
items.value.push(newItem)
|
||||
}
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const modelSet = (filed, row) => {
|
||||
httpPost('/api/admin/model/set', {id: row.id, filed: filed, value: row[filed]}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet('/api/admin/model/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/admin/form.styl";
|
||||
.list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
140
new-ui/projects/web/src/views/admin/Dashboard.vue
Normal file
140
new-ui/projects/web/src/views/admin/Dashboard.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="dashboard" v-loading="loading">
|
||||
<el-row class="mgb20" :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
||||
<div class="grid-content grid-con-1">
|
||||
<el-icon class="grid-con-icon">
|
||||
<User/>
|
||||
</el-icon>
|
||||
<div class="grid-cont-right">
|
||||
<div class="grid-num">{{ stats.users }}</div>
|
||||
<div>今日新增用户</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
||||
<div class="grid-content grid-con-2">
|
||||
<el-icon class="grid-con-icon">
|
||||
<ChatDotRound/>
|
||||
</el-icon>
|
||||
<div class="grid-cont-right">
|
||||
<div class="grid-num">{{ stats.chats }}</div>
|
||||
<div>今日新增对话</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
||||
<div class="grid-content grid-con-3">
|
||||
<el-icon class="grid-con-icon">
|
||||
<TrendCharts/>
|
||||
</el-icon>
|
||||
<div class="grid-cont-right">
|
||||
<div class="grid-num">{{ stats.tokens }}</div>
|
||||
<div>今日消耗 Tokens</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
||||
<div class="grid-content grid-con-3">
|
||||
<el-icon class="grid-con-icon">
|
||||
<i class="iconfont icon-reward"></i>
|
||||
</el-icon>
|
||||
<div class="grid-cont-right">
|
||||
<div class="grid-num">¥{{ stats.income }}</div>
|
||||
<div>今日入账</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {ChatDotRound, TrendCharts, User} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const stats = ref({users: 0, chats: 0, tokens: 0, rewards: 0})
|
||||
const loading = ref(true)
|
||||
|
||||
httpGet('/api/admin/dashboard/stats').then((res) => {
|
||||
stats.value.users = res.data.users
|
||||
stats.value.chats = res.data.chats
|
||||
stats.value.tokens = res.data.tokens
|
||||
stats.value.income = res.data.income
|
||||
loading.value = false
|
||||
}).catch((e) => {
|
||||
ElMessage.error("获取统计数据失败:" + e.message)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.dashboard {
|
||||
.grid-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.grid-cont-right {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.grid-num {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.grid-con-icon {
|
||||
font-size: 50px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
color: #fff;
|
||||
|
||||
.iconfont {
|
||||
font-size: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-con-1 .grid-con-icon {
|
||||
background: rgb(45, 140, 240);
|
||||
}
|
||||
|
||||
.grid-con-1 .grid-num {
|
||||
color: rgb(45, 140, 240);
|
||||
}
|
||||
|
||||
.grid-con-2 .grid-con-icon {
|
||||
background: rgb(100, 213, 114);
|
||||
}
|
||||
|
||||
.grid-con-2 .grid-num {
|
||||
color: rgb(100, 213, 114);
|
||||
}
|
||||
|
||||
.grid-con-3 .grid-con-icon {
|
||||
background: rgb(242, 94, 67);
|
||||
}
|
||||
|
||||
.grid-con-3 .grid-num {
|
||||
color: rgb(242, 94, 67);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
338
new-ui/projects/web/src/views/admin/Functions.vue
Normal file
338
new-ui/projects/web/src/views/admin/Functions.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="container role-list" v-loading="loading">
|
||||
<div class="handle-box">
|
||||
<el-button type="primary" :icon="Plus" @click="addRow">新增</el-button>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-table :data="tableData" :border="parentBorder" style="width: 100%">
|
||||
<el-table-column label="函数名称" prop="name">
|
||||
<template #default="scope">
|
||||
<span class="sort" :data-id="scope.row.id">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="函数别名" prop="label"/>
|
||||
<el-table-column label="功能描述" prop="description"/>
|
||||
<el-table-column label="启用状态">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.enabled" @change="functionSet('enabled',scope.row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="150" align="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button>
|
||||
<el-popconfirm title="确定要删除当前函数吗?" @confirm="remove(scope.row)" :width="200">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="title"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" label-position="left" :rules="rules">
|
||||
<el-form-item label="函数名称:" prop="name">
|
||||
<el-input
|
||||
v-model="item.name"
|
||||
placeholder="函数名称最好为英文"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="函数标签:" prop="label">
|
||||
<el-input
|
||||
v-model="item.label"
|
||||
placeholder="函数的中文名称"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="功能描述:" prop="description">
|
||||
<el-input
|
||||
v-model="item.description"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="函数参数:" prop="parameters">
|
||||
<template #default>
|
||||
<el-table :data="params" :border="childBorder" size="small">
|
||||
<el-table-column label="参数名称" width="120">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.type" placeholder="参数类型">
|
||||
<el-option v-for="pt in paramsType" :value="pt" :key="pt">{{ pt }}</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数描述">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.desc"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="必填参数" width="80">
|
||||
<template #default="scope">
|
||||
<div class="param-opt">
|
||||
<el-checkbox v-model="scope.row.required"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="scope">
|
||||
<div class="param-opt">
|
||||
<el-button type="danger" :icon="Delete" circle @click="removeParam(scope.$index)" size="small"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="param-line">
|
||||
<el-button type="primary" @click="addParam" size="small">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
增加参数
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="API 地址:" prop="action">
|
||||
<el-input
|
||||
v-model="item.action"
|
||||
autocomplete="off"
|
||||
placeholder="该函数实现的API地址,可以是第三方服务API"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="API Token:" prop="token">
|
||||
<el-input
|
||||
v-model="item.token"
|
||||
autocomplete="off"
|
||||
placeholder="API授权Token"
|
||||
>
|
||||
<template #append>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="只有本地服务才可以使用自动生成Token<br/>第三方服务请填写第三方服务API Token"
|
||||
placement="top-end"
|
||||
raw-content
|
||||
>
|
||||
<el-button @click="generateToken">生成Token</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="item.enabled"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {Delete, Plus} from "@element-plus/icons-vue";
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {arrayContains, copyObj} from "@/utils/libs";
|
||||
|
||||
const showDialog = ref(false)
|
||||
const parentBorder = ref(true)
|
||||
const childBorder = ref(true)
|
||||
const tableData = ref([])
|
||||
const item = ref({})
|
||||
const params = ref([])
|
||||
const formRef = ref(null)
|
||||
const loading = ref(true)
|
||||
const title = ref("新增函数")
|
||||
|
||||
const rules = reactive({
|
||||
name: [{required: true, message: '请输入函数名称', trigger: 'blur',}],
|
||||
label: [{required: true, message: '请输入函数标签', trigger: 'blur',}],
|
||||
description: [{required: true, message: '请输入函数功能描述', trigger: 'blur',}],
|
||||
})
|
||||
const paramsType = ref(["string", "number"])
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
fetch()
|
||||
})
|
||||
|
||||
const fetch = () => {
|
||||
httpGet('/api/admin/function/list').then((res) => {
|
||||
if (res.data) {
|
||||
tableData.value = res.data
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const curIndex = ref(0)
|
||||
const rowEdit = function (index, row) {
|
||||
title.value = "编辑函数"
|
||||
curIndex.value = index
|
||||
item.value = copyObj(row)
|
||||
// initialize parameters
|
||||
const props = item.value?.parameters?.properties
|
||||
const required = item.value?.parameters?.required
|
||||
const _params = []
|
||||
for (let key in props) {
|
||||
_params.push({
|
||||
name: key,
|
||||
type: props[key].type,
|
||||
desc: props[key].description,
|
||||
required: arrayContains(required, key)
|
||||
})
|
||||
}
|
||||
params.value = _params
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const addRow = function () {
|
||||
item.value = {enabled: true}
|
||||
params.value = []
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
const properties = {}
|
||||
const required = []
|
||||
// process params
|
||||
for (let i = 0; i < params.value.length; i++) {
|
||||
properties[params.value[i].name] = {"type": params.value[i].type, "description": params.value[i].desc}
|
||||
if (params.value[i].required) {
|
||||
required.push(params.value[i].name)
|
||||
}
|
||||
}
|
||||
item.value.parameters = {type: "object", "properties": properties, required: required}
|
||||
httpPost('/api/admin/function/save', item.value).then((res) => {
|
||||
ElMessage.success('操作成功')
|
||||
console.log(res.data)
|
||||
if (item.value.id > 0) {
|
||||
tableData.value[curIndex.value] = item.value
|
||||
} else {
|
||||
tableData.value.push(res.data)
|
||||
}
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet('/api/admin/function/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
fetch()
|
||||
}).catch(() => {
|
||||
ElMessage.error("删除失败!")
|
||||
})
|
||||
}
|
||||
|
||||
const addParam = function () {
|
||||
if (!params.value) {
|
||||
item.value = []
|
||||
}
|
||||
params.value.push({name: "", type: "string", desc: "", required: false})
|
||||
}
|
||||
|
||||
const removeParam = function (index) {
|
||||
params.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const functionSet = (filed, row) => {
|
||||
httpPost('/api/admin/function/set', {id: row.id, filed: filed, value: row[filed]}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const generateToken = () => {
|
||||
httpGet('/api/admin/function/token').then(res => {
|
||||
ElMessage.success("生成 Token 成功")
|
||||
item.value.token = res.data
|
||||
}).catch(() => {
|
||||
ElMessage.error("生成 Token 失败")
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.role-list {
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.param-line {
|
||||
padding 5px 0
|
||||
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.param-opt {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
margin-top 5px;
|
||||
margin-left 5px;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.el-input--small {
|
||||
width 30px;
|
||||
|
||||
.el-input__inner {
|
||||
text-align center
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
new-ui/projects/web/src/views/admin/Home.vue
Normal file
46
new-ui/projects/web/src/views/admin/Home.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="admin-home" v-if="isLogin">
|
||||
<admin-sidebar/>
|
||||
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
|
||||
<admin-header/>
|
||||
<admin-tags/>
|
||||
<div class="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="move" mode="out-in">
|
||||
<keep-alive :include="tags.nameList">
|
||||
<component :is="Component"></component>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {useSidebarStore} from '@/store/sidebar';
|
||||
import {useTagsStore} from '@/store/tags';
|
||||
import AdminHeader from "@/components/admin/AdminHeader.vue";
|
||||
import AdminSidebar from "@/components/admin/AdminSidebar.vue";
|
||||
import AdminTags from "@/components/admin/AdminTags.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {checkAdminSession} from "@/action/session";
|
||||
import {ref} from "vue";
|
||||
|
||||
const sidebar = useSidebarStore();
|
||||
const tags = useTagsStore();
|
||||
const isLogin = ref(false)
|
||||
|
||||
// 获取会话信息
|
||||
const router = useRouter();
|
||||
checkAdminSession().then(() => {
|
||||
isLogin.value = true
|
||||
}).catch(() => {
|
||||
router.replace('/admin/login')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import '@/assets/css/main.css';
|
||||
@import '@/assets/css/color-dark.css';
|
||||
@import '@/assets/iconfont/iconfont.css';
|
||||
</style>
|
||||
163
new-ui/projects/web/src/views/admin/Login.vue
Normal file
163
new-ui/projects/web/src/views/admin/Login.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="admin-login">
|
||||
<div class="main">
|
||||
<div class="contain">
|
||||
<div class="header">{{ title }}</div>
|
||||
<div class="content">
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入用户名" size="large" v-model="username" autocomplete="off" autofocus
|
||||
@keyup="keyupHandle">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码" size="large" v-model="password" show-password autocomplete="off"
|
||||
@keyup="keyupHandle">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-row class="btn-row">
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<footer-bar/>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {useRouter} from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
import {setAdminToken} from "@/store/session";
|
||||
import {checkAdminSession} from "@/action/session";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('ChatGPT Plus Admin');
|
||||
const username = ref(process.env.VUE_APP_ADMIN_USER);
|
||||
const password = ref(process.env.VUE_APP_ADMIN_PASS);
|
||||
|
||||
checkAdminSession().then(() => {
|
||||
router.push("/admin")
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
const keyupHandle = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
login();
|
||||
}
|
||||
}
|
||||
|
||||
const login = function () {
|
||||
if (username.value === '') {
|
||||
return ElMessage.error('请输入用户名');
|
||||
}
|
||||
if (password.value === '') {
|
||||
return ElMessage.error('请输入密码');
|
||||
}
|
||||
|
||||
httpPost('/api/admin/login', {username: username.value.trim(), password: password.value.trim()}).then(res => {
|
||||
setAdminToken(res.data.token)
|
||||
router.push("/admin")
|
||||
}).catch((e) => {
|
||||
ElMessage.error('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.admin-login {
|
||||
display flex
|
||||
justify-content center
|
||||
width: 100%
|
||||
background #2D3A4B
|
||||
|
||||
.main {
|
||||
width 400px;
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
height 100vh
|
||||
|
||||
.contain {
|
||||
width 100%
|
||||
padding 20px 40px;
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
background rgba(255, 255, 255, 0.3)
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 20px
|
||||
font-size 24px
|
||||
text-align center
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.footer {
|
||||
color #ffffff;
|
||||
|
||||
.container {
|
||||
padding 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
81
new-ui/projects/web/src/views/admin/LoginLog.vue
Normal file
81
new-ui/projects/web/src/views/admin/LoginLog.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
<el-row>
|
||||
<el-table :data="items" border :row-key="row => row.id">
|
||||
<el-table-column label="用户名" prop="username"/>
|
||||
<el-table-column label="登录IP" prop="login_ip"/>
|
||||
<el-table-column label="登录地址" prop="login_address"/>
|
||||
<el-table-column label="登录时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchList(page, pageSize)"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
|
||||
// 用户登录日志
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
const total = ref(0)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
fetchList(1, 15)
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchList = function (_page, _pageSize) {
|
||||
httpGet(`/api/admin/user/loginLog?page=${_page}&page_size=${_pageSize}`).then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display flex;
|
||||
justify-content flex-start
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
153
new-ui/projects/web/src/views/admin/Order.vue
Normal file
153
new-ui/projects/web/src/views/admin/Order.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="container order" v-loading="loading">
|
||||
<div class="handle-box">
|
||||
<el-input v-model="query.order_no" placeholder="订单号" class="handle-input mr10"></el-input>
|
||||
<el-select v-model="query.status" placeholder="订单状态" style="width: 100px">
|
||||
<el-option
|
||||
v-for="item in orderStatus"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="query.pay_time"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="order_no" label="订单号"/>
|
||||
<el-table-column prop="username" label="下单用户"/>
|
||||
<el-table-column prop="subject" label="产品名称"/>
|
||||
<el-table-column prop="amount" label="订单金额"/>
|
||||
<el-table-column label="调用次数">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.remark?.calls }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="下单时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="支付时间">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>
|
||||
<el-tag v-else>未支付</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pay_way" label="支付方式"/>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData()"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||
import {Search} from "@element-plus/icons-vue";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const query = ref({order_no: "", pay_time: [], status: -1})
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const loading = ref(true)
|
||||
const orderStatus = ref([
|
||||
{value: -1, label: "全部"},
|
||||
{value: 0, label: "未支付"},
|
||||
{value: 2, label: "已支付"},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
query.value.page = page.value
|
||||
query.value.page_size = pageSize.value
|
||||
httpPost('/api/admin/order/list', query.value).then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet('/api/admin/order/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.order {
|
||||
|
||||
.handle-box {
|
||||
.handle-input {
|
||||
max-width 150px;
|
||||
margin-right 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
232
new-ui/projects/web/src/views/admin/Product.vue
Normal file
232
new-ui/projects/web/src/views/admin/Product.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="name" label="产品名称">
|
||||
<template #default="scope">
|
||||
<span class="sort" :data-id="scope.row.id">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="price" label="产品价格"/>
|
||||
<el-table-column prop="discount" label="优惠金额"/>
|
||||
<el-table-column prop="days" label="有效期(天)">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.days === 0">长期有效</el-tag>
|
||||
<span v-else>{{ scope.row.days }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="calls" label="对话次数"/>
|
||||
<el-table-column prop="img_calls" label="绘图次数"/>
|
||||
<el-table-column prop="sales" label="销量"/>
|
||||
<el-table-column prop="enabled" label="启用状态">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row['enabled']" @change="enable(scope.row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="更新时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['updated_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="title"
|
||||
:close-on-click-modal="false"
|
||||
style="width: 90%; max-width: 600px;"
|
||||
>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="产品名称:" prop="name">
|
||||
<el-input v-model="item.name" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="产品价格:" prop="price">
|
||||
<el-input v-model="item.price" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优惠金额:" prop="discount">
|
||||
<el-input v-model="item.discount" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期:" prop="days">
|
||||
<el-input v-model.number="item.days" autocomplete="off" placeholder="会员有效期(天)"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="对话次数:" prop="calls">
|
||||
<el-input v-model.number="item.calls" autocomplete="off" placeholder="增加对话次数"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="绘图次数:" prop="img_calls">
|
||||
<el-input v-model.number="item.img_calls" autocomplete="off" placeholder="增加绘图次数"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态:" prop="enable">
|
||||
<el-switch v-model="item.enabled"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">提交</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import {Sortable} from "sortablejs";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const item = ref({})
|
||||
const showDialog = ref(false)
|
||||
const title = ref("")
|
||||
const rules = reactive({
|
||||
name: [{required: true, message: '请输入产品名称', trigger: 'change',}],
|
||||
price: [{required: true, message: '请输产品价格', trigger: 'change',}],
|
||||
discount: [{required: true, message: '请输优惠金额', trigger: 'change',}],
|
||||
days: [{required: true, message: '请输入有效期', trigger: 'change',}],
|
||||
})
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/product/list').then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
|
||||
// 初始化拖动排序插件
|
||||
Sortable.create(drawBodyWrapper, {
|
||||
sort: true,
|
||||
animation: 500,
|
||||
onEnd({newIndex, oldIndex, from}) {
|
||||
if (oldIndex === newIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
|
||||
const ids = []
|
||||
const sorts = []
|
||||
sortedData.forEach((id, index) => {
|
||||
ids.push(parseInt(id))
|
||||
sorts.push(index)
|
||||
})
|
||||
|
||||
httpPost("/api/admin/product/sort", {ids: ids, sorts: sorts}).catch(e => {
|
||||
ElMessage.error("排序失败:" + e.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const add = function () {
|
||||
title.value = "新增模型"
|
||||
showDialog.value = true
|
||||
item.value = {}
|
||||
}
|
||||
|
||||
const edit = function (row) {
|
||||
title.value = "修改模型"
|
||||
showDialog.value = true
|
||||
item.value = row
|
||||
}
|
||||
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
item.value['price'] = parseFloat(item.value['price'])
|
||||
item.value['discount'] = parseFloat(item.value['discount'])
|
||||
httpPost('/api/admin/product/save', item.value).then((res) => {
|
||||
ElMessage.success('操作成功!')
|
||||
if (!item.value['id']) {
|
||||
const newItem = res.data
|
||||
items.value.push(newItem)
|
||||
}
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const enable = (row) => {
|
||||
httpPost('/api/admin/product/enable', {id: row.id, enabled: row.enabled}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet('/api/admin/product/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
104
new-ui/projects/web/src/views/admin/Reward.vue
Normal file
104
new-ui/projects/web/src/views/admin/Reward.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id">
|
||||
<el-table-column prop="username" label="用户"/>
|
||||
<el-table-column prop="tx_id" label="转账单号"/>
|
||||
<el-table-column prop="amount" label="转账金额"/>
|
||||
<el-table-column prop="remark" label="备注"/>
|
||||
|
||||
<el-table-column label="转账时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="核销时间">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row['status']">{{ dateFormat(scope.row['updated_at']) }}</span>
|
||||
<el-tag v-else>未核销</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="兑换详情">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row['exchange']['calls'] > 0">聊天{{ scope.row['exchange']['calls'] }}次</el-tag>
|
||||
<el-tag v-else-if="scope.row['exchange']['img_calls'] > 0" type="success">
|
||||
绘图{{ scope.row['exchange']['img_calls'] }}次
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/reward/list').then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet('/api/admin/reward/remove?id=' + row.id).then(() => {
|
||||
ElMessage.success("删除成功!")
|
||||
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
display flex;
|
||||
justify-content flex-end
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user