merge code for v4.1.8

This commit is contained in:
GeekMaster
2025-04-17 10:07:04 +08:00
180 changed files with 19568 additions and 25200 deletions

View File

@@ -1,29 +1,29 @@
<template>
<el-config-provider>
<router-view/>
<router-view />
</el-config-provider>
</template>
<script setup>
import {ElConfigProvider} from 'element-plus';
import {onMounted, ref, watch} from "vue";
import {checkSession, getClientId, getSystemInfo} from "@/store/cache";
import {isChrome, isMobile} from "@/utils/libs";
import {showMessageInfo} from "@/utils/dialog";
import {useSharedStore} from "@/store/sharedata";
import {getUserToken} from "@/store/session";
import { ElConfigProvider } from "element-plus";
import { onMounted, ref, watch } from "vue";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { isChrome, isMobile } from "@/utils/libs";
import { showMessageInfo } from "@/utils/dialog";
import { useSharedStore } from "@/store/sharedata";
import { getUserToken } from "@/store/session";
const debounce = (fn, delay) => {
let timer
let timer;
return (...args) => {
if (timer) {
clearTimeout(timer)
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
fn(...args);
}, delay);
};
};
const _ResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
@@ -31,63 +31,69 @@ window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
callback = debounce(callback, 200);
super(callback);
}
}
};
const store = useSharedStore()
const store = useSharedStore();
onMounted(() => {
// 获取系统参数
getSystemInfo().then((res) => {
const link = document.createElement('link')
link.rel = 'shortcut icon'
link.href = res.data.logo
document.head.appendChild(link)
})
const link = document.createElement("link");
link.rel = "shortcut icon";
link.href = res.data.logo;
document.head.appendChild(link);
});
if (!isChrome() && !isMobile()) {
showMessageInfo("建议使用 Chrome 浏览器以获得最佳体验。")
showMessageInfo("建议使用 Chrome 浏览器以获得最佳体验。");
}
checkSession().then(() => {
store.setIsLogin(true)
}).catch(()=>{})
})
checkSession()
.then(() => {
store.setIsLogin(true);
})
.catch(() => {});
watch(() => store.isLogin, (val) => {
if (val) {
connect()
}
})
// 设置主题
document.documentElement.setAttribute("data-theme", store.theme);
});
const handler = ref(0)
// 初始化 websocket 连接
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;
watch(
() => store.isLogin,
(val) => {
if (val) {
connect();
}
}
const clientId = getClientId()
const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`,["token",getUserToken()]);
_socket.addEventListener('open', () => {
console.log('WebSocket 连接')
);
const handler = ref(0);
// 初始化 websocket 连接
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 clientId = getClientId();
const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`, ["token", getUserToken()]);
_socket.addEventListener("open", () => {
console.log("WebSocket 已连接");
handler.value = setInterval(() => {
if (_socket.readyState === WebSocket.OPEN) {
_socket.send(JSON.stringify({"type":"ping"}))
_socket.send(JSON.stringify({ type: "ping" }));
}
},5000)
})
_socket.addEventListener('close', () => {
clearInterval(handler.value)
connect()
}, 5000);
});
store.setSocket(_socket)
}
_socket.addEventListener("close", () => {
clearInterval(handler.value);
connect();
});
store.setSocket(_socket);
};
</script>
<style lang="stylus">
html, body {
margin: 0;
@@ -102,6 +108,14 @@ html, body {
text-rendering: optimizeLegibility;
--primary-color: #21aa93
h1 { font-size: 2em; } /* 通常是 2em */
h2 { font-size: 1.5em; } /* 通常是 1.5em */
h3 { font-size: 1.17em; } /* 通常是 1.17em */
h4 { font-size: 1em; } /* 通常是 1em */
h5 { font-size: 0.83em; } /* 通常是 0.83em */
h6 { font-size: 0.67em; } /* 通常是 0.67em */
}
.el-overlay-dialog {
@@ -137,4 +151,5 @@ html, body {
color #07C160
}
@import '@/assets/iconfont/iconfont.css'
</style>

View File

@@ -1,14 +1,74 @@
.el-form-item__content {
.tip-input {
display flex
width 100%
.form {
.el-form-item__label {
.label-title {
display flex
align-items center
.el-input, .el-select, .el-switch {
margin-right 10px
}
.info {
margin-top 2px
.el-icon {
margin-left 5px
cursor pointer
}
}
}
}
.el-form-item__content {
width 100%
.uploader-icon {
font-size 24px
position relative
top 3px
}
.tip-input-line {
.tip {
margin-top 10px
color #c1c1c1
font-size 12px;
line-height 1.5;
}
}
}
.el-input {
width 100%
}
.text {
font-size 14px
}
.active-info {
line-height 1.5
padding 10px 0 30px 0
}
.el-descriptions {
margin-bottom 20px
.el-icon {
font-size 18px
}
.selected {
color #0bc15f
}
.closed {
color #da0d54
}
.text {
margin-left 10px
font-size 12px
color #999999
position: relative;
top -5px
}
}
.el-alert {
margin-bottom 15px;
}
}

View File

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

View File

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

View File

@@ -1,37 +1,33 @@
$sideBgColor = #252526;
$borderColor = #4676d0;
#app {
height: 100%;
.chat-page {
height: 100%;
:deep (.el-message-box__message){
font-size: 18px !important
}
.newChat{
margin-bottom: 10px
}
// left side
.el-container{
height: 100%;
}
.el-aside {
//background-color: $sideBgColor;
padding 10px
width var(--el-aside-width, 320px)
.media-page {
display: flex
flex-flow: column
//background-color: $sideBgColor
border-radius 10px
padding 10px 0
.search-box {
flex-wrap: wrap
padding: 10px 0;
.search-input {
--el-input-bg-color: #363535
--el-input-border-color: #464545
--el-input-focus-border-color: #47fff1
--el-input-hover-border-color: #2DA39A
box-shadow: none
}
margin-bottom: 10px
}
//
@@ -51,14 +47,14 @@ $borderColor = #4676d0;
width: 100%
justify-content: flex-start
padding: 8px 12px
//border-bottom: 1px solid #3c3c3c
//border: 1px solid #3c3c3c
cursor: pointer
border: 1px solid #3c3c3c
border: 1px solid var(--theme-bg-color)
margin-bottom 6px
border-radius 5px
&:hover {
background-color #343540
border: 1px solid var(--border-active);
}
.avatar {
@@ -78,7 +74,7 @@ $borderColor = #4676d0;
}
.chat-title {
color: #c1c1c1
color: var(--el-text-color-regular);
padding: 5px 10px;
max-width 220px;
font-size 14px;
@@ -92,10 +88,11 @@ $borderColor = #4676d0;
position: absolute;
right: 2px;
top: 16px;
color #ffffff
color var(--text-fb)
.el-dropdown-link {
color #ffffff
color var(--text-fb)
}
.el-icon {
@@ -105,8 +102,9 @@ $borderColor = #4676d0;
}
.chat-list-item.active {
background-color: #343540;
border-color #21aa93
background-color :var(--theme-bg);
box-shadow: 0 3px 9px rgba(112, 144, 176, 0.12);
border: 1px solid var(--border-active);
}
}
}
@@ -116,7 +114,7 @@ $borderColor = #4676d0;
display: flex;
justify-content: center;
padding-top 12px
border-top 1px solid #3c3c3c;
// border-top 0.5px solid var(--el-border-color);
.iconfont {
margin-right 5px
@@ -133,15 +131,15 @@ $borderColor = #4676d0;
min-width: 0;
flex: 1;
background-color: var(--el-bg-color)
color var(--el-text-color-primary)
color var(--text-fb)
.chat-config {
height 30px
height 50px
padding 10px 30px
display flex
justify-content center
justify-items center
border-bottom 1px solid #d9d9e3
// border-bottom 1px solid var(--el-border-color);
.role-select-label {
color #ffffff
@@ -157,18 +155,22 @@ $borderColor = #4676d0;
}
.setting {
padding 5px
// padding 5px
border-radius 5px
cursor pointer
background-color #f2f2f2
margin-right 10px
width: 26px;
height: 26px;
text-align: center;
line-height: 26px;
// background-color #f2f2f2
// margin-right 10px
.iconfont {
font-size 18px
color #19c37d
font-size 16px
color var(--el-color-primary)
}
&:hover {
background-color #D5FAD3
background-color var(--text--hover)
}
}
@@ -183,7 +185,8 @@ $borderColor = #4676d0;
overflow: hidden;
width: 100%;
position relative
background: var(--chat-bg)
::-webkit-scrollbar {
width: 12px /* */
background #F1F1F1
@@ -205,6 +208,8 @@ $borderColor = #4676d0;
.chat-box {
overflow-y: auto;
//border-bottom: 1px solid #4f4f4f
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IEEdge */
//
--content-font-size: 16px;
@@ -217,8 +222,12 @@ $borderColor = #4676d0;
font-size: 14px;
display: flex;
align-items: flex-start;
}
::-webkit-scrollbar {
display: none; /* Webkit */
}
}
.input-box {
@@ -228,10 +237,11 @@ $borderColor = #4676d0;
.input-box-inner {
display flex
background-color: #ffffff
background-color:var(--chat-bg);
justify-content: center;
align-items: center;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
// box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding 0 15px;
.tool-item {
@@ -243,7 +253,7 @@ $borderColor = #4676d0;
justify-items center
padding 6px
cursor pointer
background #F2F2F2
// background #F2F2F2
&:hover {
background #D5FAD3
@@ -274,13 +284,17 @@ $borderColor = #4676d0;
}
.input-border {
display flex
// display flex
width 100%
overflow hidden
border: 2px solid #21AA93
border: 2px solid var( --theme-border-primary)
border-radius 10px
padding 10px
background-color #F4F4F4
// background-color #F4F4F4
&:hover{
border-color var(--theme-border-hover)
}
.input-inner {
display flex
@@ -296,6 +310,7 @@ $borderColor = #4676d0;
}
.prompt-input {
min-height: 58px;
width 100%
line-height: 24px
border none
@@ -312,12 +327,35 @@ $borderColor = #4676d0;
.send-btn {
width 32px
margin-left 10px
.el-button {
padding 8px 5px;
border-radius 6px;
font-size 20px;
}
}
.little-btns{
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
.iconfont{
font-size: 19px;
cursor pointer
background-color: var(--chat-content-bg);
padding: 5px;
border-radius: 6px;
}
}
.add-new{
.el-icon{
font-size: 20px;
color: #754ff6;
}
cursor:pointer
}
}
}
@@ -380,7 +418,8 @@ $borderColor = #4676d0;
width 40px
height 40px
border-radius 100%
background-color #ffffff
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
}
}
@@ -417,9 +456,13 @@ $borderColor = #4676d0;
line-height 1.8
font-size 16px
overflow auto
height 100%
height: 70vh
}
}
.dialog-footer{
margin-right: 22px;
}
}
}
@@ -436,4 +479,5 @@ $borderColor = #4676d0;
.el-icon {
margin-left 5px;
}
}
}

View File

@@ -0,0 +1,153 @@
:root{
--sm-txt:rgba(163, 174, 208, 1);
--text-secondary: #8a939d;
--el-color-primary: rgb(107, 80, 225);
--van-primary-color:rgb(107, 80, 225);
--theme-textcolor-normal:#b0a0f8;
--el-border-radius-base: 5px;
--el-color-primary-light-5:rgb(107, 85, 255);
--el-color-primary-light-3:rgb(78, 51, 254);
--theme-btn-color:rgba(117, 81, 255, 1)
--common-text-color:#6e4ef9;
--el-component-size: 36px;
--el-color-primary-dark-2:rgb(169 152 247);
--el-button-active-border-color:rgb(169 152 247);
--el-color-success-light-9:#EAFFFC;
--el-color-success-light-8:#A7F0D9;
--el-message-text-color:#0ECD8B;
--el-color-success:#0ECD8B;
--text-fff:#fff
--theme-border-primary: rgba(86, 86, 95, .322); //
--theme-border-hover: rgb(107, 85, 255);//hover
--text--hover:rgba(215, 211, 240, 0.581) //hover
--el-input-focus-border-color: #b0a0f8;
--little-btn-bg:#e9d3f6;
--gray-btn-bg:#ededf591;
// --a-link-color: #3561ff
--a-link-color: #6e8eff
--shadow-color:rgba(223,71,255,0.6)
--sm-btn-bg:#6052ed;
--theme-text-tertiary: #595959;
--theme-btn-fill-tertiary: #f0ebff;
--theme-text-btn-tertiary: #6841ea;
// #e7e7e8
}
.el-dialog{
//--el-border-radius-base: calc(var(--el-component-size) / 2);
--el-dialog-border-radius: 10px
}
.login-box{
--el-component-size: 48px;
}
.btn-go{
background: var(--btnColor);
color: #fff;
border-radius: 5px;
padding: 5px 10px;
&:hover{
color: #fff;
}
}
.btn-normal{
background: var(--theme-btn-color);
color: #fff;
border-radius: 5px;
padding: 5px 10px;
&:hover{
color: #fff;
}
}
.flex{
display: flex;
align-items: center;
}
.flex-center{
display: flex;
align-items: center;
justify-content: center;
}
.flex-between{
display: flex;
align-items: center;
justify-content: space-between;
}
.theme-color-primary{
color: var(--el-color-primary);
}
.text-color-primary{
color:var(--text-color-primary)
}
.w100{
width: 100%;
}
.el-input__wrapper{
background: var( --card-bg)
}
.el-dialog__title{
font-weight: bold;
line-height: 28px;
}
.el-button--primary{
border-radius: 5px;
}
.el-button {
height auto
}
/* */
::-webkit-scrollbar {
width: 12px; /* */
height: 12px; /* */
}
/* */
::-webkit-scrollbar-track {
background: #f1f1f1 !important; /* */
border-radius: 6px; /* */
}
/* */
::-webkit-scrollbar-thumb {
background: #888 !important; /* */
border-radius: 6px; /* */
}
/* */
::-webkit-scrollbar-thumb:hover {
background: #555; /* */
}
//.el-message-box
.el-message-box{
--el-messagebox-border-radius: 10px
}
.el-message-box__container{
//border-top: 1px solid #dbd3f4;
padding-top: 7px;
.el-message-box__message{
--text-color:var(--theme-text-color-primary)
font-size: 16px
}
}
.sm-btn-theme{
background-color: var(--theme-btn-fill-tertiary) !important;
color: var(--theme-text-btn-tertiary) !important;
border: none;
}
.el-tag, .el-tag.el-tag--primary{
--el-tag-bg-color:#f0ebff
}
.box-card{
padding: 20px;
background-color: var(--chat-bg);
border-radius: 8px;
}
.el-table th.el-table__cell {
background-color: var(--chat-bg)
}

View File

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

View File

@@ -23,4 +23,15 @@
::-webkit-scrollbar-thumb:hover {
background-color: #666666;
}
overflow: auto; /* */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IEEdge */
::-webkit-scrollbar {
display: none; /* Webkit */
}
&.showScrollbar {
::-webkit-scrollbar {
display: none; /* Webkit */
}
}
}

View File

@@ -0,0 +1,24 @@
@font-face {
font-family: "OPlusSans3-Regular";
src: url("../fonts/OPlusSans3-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "OPlusSans3-Medium";
src: url("../fonts/OPlusSans3-Medium.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
$font-regular = "OPlusSans3-Regular", "PingFangSC-Regular", "Roboto", "sans-serif";
$font-medium = "OPlusSans3-Medium", "PingFangSC-Medium", "Roboto", "sans-serif";
.font-regular {
font-family: $font-regular;
font-weight: normal;
}
.font-medium {
font-family: $font-medium;
font-weight: 500;
}

View File

@@ -1,166 +1,240 @@
.home {
.layout{
display: flex;
height 100vh
width 100%
flex-flow column
.header {
display flex
justify-content space-between
height 50px
line-height 50px
background-color #1E1F22
padding-right 20px
.banner {
display flex
.logo {
display flex
padding 5px
cursor pointer
.el-image {
width 48px
height 48px
border-radius 50%
}
}
.title {
display: flex;
color: #ffffff;
font-size: 20px;
padding 0 10px
}
}
.navbar {
display flex
flex-flow row
.link-button {
margin-right 15px
color #e1e1e1
padding 0 10px
&:hover {
background-color #414141
}
.iconfont {
font-size 24px
}
}
.user-info {
width 100%
padding 5px 0;
.el-dropdown-link {
width 100%;
cursor: pointer
display flex
.el-image {
width: 36px;
height: 36px;
border-radius: 50%
}
.el-icon {
color: #cccccc;
line-height 24px;
}
}
}
position: relative;
height: 100vh;
.big-top-title{
padding-top: 10px;
}
.top-collapse{
padding-top: 10px
img{
width 24px !important
height: 24px !important
}
}
.main {
width 100%
display flex
flex-flow row
.navigator {
display flex
flex-flow column
width 60px
padding 10px 1px
border-right: 1px solid #3c3c3c
background-color: #1E1F22
.nav-items {
margin-top: 10px;
padding 0 5px
li {
margin-bottom 15px
display flex
flex-flow column
a {
color #DADBDC
border-radius 10px
width 48px
height 48px
display flex
justify-content center
align-items center
cursor pointer
background-color #414348
.el-image {
border-radius 10px
}
.iconfont {
font-size 20px
}
}
a:hover, a.active {
color #47fff1
background-color #0F7A71
}
.title {
font-size: 12px
padding-top: 6px
color: #e5e7eb;
text-align: center;
white-space: nowrap; /* */
overflow: hidden; /* */
text-overflow: unset; /* 使 */
}
.active {
color #47fff1
}
}
}
.tab-box{
align-items: center
background-color: var(--card-bg)
// height: 100%
// position: fixed;
height: 100vh;
.title{
font-size: 28px
height: 40px
width 120px
text-align: center;
word-wrap break-all;
overflow hidden
font-weight: 700
color:var(--text-theme-color)
}
.content {
width: 100%
overflow auto
box-sizing: border-box
background-color #282c34
img{
height: 44px
object-fit: cover
border-radius: 50%
border: 2px solid #754ff6;
background: #fff
}
.marr{
margin-right: 4px;
}
}
}
.flex-center-col{
display flex
align-items center
flex-direction column
.iconfont {
font-size 22px
}
.icon-expand {
font-size 24px
margin-bottom 10px
cursor pointer
color var(--text-color)
}
.icon-colspan {
font-size 18px
margin-left 3px
cursor pointer
color var(--text-color)
}
}
.menu-list-collapse{
.flex-center-col{
flex-direction: row;
}
.menu-list-item{
height: 38px;
line-height: 38px;
.iconfont {
font-size 16px
}
}
.menu-list-item:hover,
.active{
background: rgba(79, 89, 102, .122);
border-radius: 8px;
.el-icon{
background: transparent !important;
}
}
.menu-title{
font-size: 15px !important;
margin-bottom: 0 !important;
}
}
.openicon{
font-size: 40px;
color: #754ff6;
}
.menuIcon{
.openicon{
font-size: 28px;
color: #754ff6;
}
}
.menu-list{
margin-top: 20px;
.svg-icon{
svg{
width: 30px;
height: 30px;
}
}
.menu-list-item{
// margin-bottom: 10px;
margin: 0 8px 8px;
cursor: pointer;
font-weight: 550;
&:hover{
.el-icon{
background: rgba(79, 89, 102, .122);
}
}
.el-icon{
width: 24px;
height: 24px;
padding: 4px;
overflow: hidden;
border-radius: 50%;
font-size: 20px;
// img{
// width: 24px;
// height: 24px;
// }
}
&.active{
color: var(--text-color);
font-weight: 700;
filter: none !important;
.el-icon{
background: rgba(79, 89, 102, .122);
filter: invert(100%);
}
}
}
.bot{
position: absolute;
bottom: 6px;
}
.bot-line{
width : 100%;
height: 1px;
background: var(--line-box)
margin: 20px 0 10px 0;
}
.menu-title{
font-size: 12px;
margin-bottom: 6px;
}
.icon-house,
.icon-github{
font-size: 20px;
color: #754ff6;
cursor pointer
}
.menu-bot-item{
display: flex;
align-items: center;
justify-content: space-around;
align-items: center;
a{
// margin-right: 46px;
}
}
}
::v-deep(.theme-box){
position: relative !important;
right: initial;
bottom: initial;
width: 20px;
height: 20px;
line-height: 20px;
.iconfont{
font-size: 15px !important;}
}
.right-main{
height: 100%;
// background: #f5f7fd;
background: var(--theme-bg-all);
// background-image: linear-gradient(180deg, rgba(247, 232, 255, .54), rgba(191, 223, 255, .35));
width: 100%;
.loginMask{
position: absolute;
top: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.topheader{
display: flex;
position: fixed;
right: 8px;
z-index : 999;
top:0;
// width 100%;
align-items: center;
justify-content: flex-end;
}
.btn-go{
background: #754ff6;
margin: 10px 10px 0;
}
}
.el-popper {
.more-menus {
li {
padding 10px 15px
cursor pointer
border-radius 5px
margin 5px 0
padding: 0px 15px;
cursor: pointer;
border-radius: 5px;
margin: 5px 0;
height: 38px;
line-height: 38px;
.el-image {
position: relative
@@ -169,26 +243,49 @@
}
&:hover {
background-color #f1f1f1
background: rgba(79, 89, 102, 0.1);
}
}
li.active {
background-color #f1f1f1
background: rgba(79, 89, 102, 0.1);
}
}
.user-info-menu {
li {
a {
width 100%
justify-content left
&:hover {
text-decoration none !important
color var(--el-primary-text-color)
}
}
.setting-menus{
.title{
color: #222226;
}
.el-icon,
.iconfont{
font-size: 18px
margin-right: 6px
}
color: #222226;
}
.username{
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 1;
}
}
.rightHeightMax{
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
.rightHeight{
height: calc(100vh - 42px);
max-height: calc(100vh - 42px);
overflow: hidden;
.content{
padding-top: 42px;
}
}
.content{
height: 100%;
overflow: scroll;
}

View File

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

View File

@@ -365,7 +365,7 @@
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); /* 添加阴影效果 */
box-shadow: 0 0 10px rgba(223,71,255,0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */
}
.page-mj .inner .task-list-box .task-list-inner .job-list-box .el-image {

View File

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

View File

@@ -250,7 +250,7 @@
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); /* 添加阴影效果 */
box-shadow: 0 0 10px rgba(223,71,255,0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */
}
.page-sd .inner .task-list-box .task-list-inner .job-list-box .el-image {

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
.index-page {
margin: 0
overflow hidden
color #ffffff
color var(--text-color)
display flex
justify-content center
align-items baseline
padding-top 150px
align-items center
background: var(--theme-bg) !important
flex-flow column
height: 100vh
.color-bg {
position absolute
@@ -30,33 +33,47 @@
}
.menu-box {
position absolute
top 0
width 100%
display flex
height 80px
align-items center
.el-menu {
padding 0 30px
width 100%
display flex
justify-content space-between
align-items center
background none
border none
.menu-item {
display flex
padding 20px 0
// padding 20px 0
height 40px
align-items center
color #ffffff
color var(--text-color);
.iconfont{
color var(--text-color);
font-size: 28px;
}
.icon-book{
margin-right: 6px;
}
.title {
font-size 24px
color var(--text-color);
font-size: 24px;
padding 10px 10px 0 10px
font-weight: 700;
}
.logo {
height 50px
height 60px
border-radius 50%
background: #fff
border: 2px solid #754ff6
}
.el-button {
@@ -112,6 +129,34 @@
}
}
}
.nav-item-box{
width: 100%;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.645,0.045,0.355,1);
aspect-ratio: 1.1028 / 1;
background: var( --card-bg)
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 160px
// min-height: 190px;
i{
display: inline-block
min-width: 48px;
width: 48px;
height: 48px;
font-size: 38px
border-radius: 24px;
color: var(--normal-color)
}
&:hover{
box-shadow: 0 4px 14px 0 rgba(17, 13, 83, .18);
transform: translateY(-8px);}
}
}
}
@@ -121,4 +166,49 @@
}
}
}
.cursor-ani {
position: relative;
}
.cursor-ani::after {
content: '';
position: absolute;
width: 1px;
height: 28px;
background: #333;
transform: translateX(3px) translateY(3px);
animation: cursor-blinks 0.8s infinite forwards;
}
@keyframes cursor-blinks {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.cursor-ani {
display: inline-block;
min-height: 34px;
font-size: 1.5em;
}
.msg-text span {
transition: color 0.3s ease; /* */
font-weight: bold;
}
.logo-box{
width: 60px
height: 60px
background: #fff
border-radius: 50%
img{
width: 100%;
object-fit: cover;
height: 100%;
}
}

View File

@@ -1,117 +1,62 @@
.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); /* */
.loginPage{
background: var(--card-bg) !important
background-color: var(---card-bg) !important
.form-title{
color:var( --text-theme-color)
}
}
.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;
.left{
width: 50%;
.login-box{
width: 410px;
margin: 0 auto;
min-height: calc(100vh - 48px);
}
.wechatLog{
width: 410px;
height: 50px;
line-height: 50px;
text-align: center
background: var( --sign-bg)
a{
color: var(--text-theme-color)
.logo {
text-align center
.el-image {
width 120px;
cursor pointer
border-radius 50%
}
}
.header {
width 100%
margin-bottom 24px
font-size 24px
color $white_v1
letter-space 2px
text-align center
padding-top 10px
}
.content {
width 100%
height: auto
border-radius 3px
.block {
margin-bottom 16px
.el-input__inner {
border 1px solid $gray-v6 !important
.el-icon-user, .el-icon-lock {
font-size 20px
}
}
}
.btn-row {
padding-top 10px;
.login-btn {
width 100%
font-size 16px
letter-spacing 2px
}
}
.text-line {
justify-content center
padding-top 10px;
font-size 14px;
}
.opt {
padding 15px
.el-col {
text-align center
}
}
.divider {
border-top: 2px solid #c1c1c1;
}
.clogin {
padding 15px
display flex
justify-content center
.iconfont {
font-size 20px
background: #E9F1F6;
padding: 8px;
border-radius: 50%
cursor pointer
}
.iconfont.icon-wechat {
color #0bc15f
}
}
font-size: 14px;
margin-bottom: 26px
border-radius: 16px;
.icon-wechat{
color: #0bc15f
margin-right: 9px
font-size: 20px;
}
}
.footer {
color #ffffff;
.container {
padding 20px;
}
.text-color-primary{
cursor :pointer
}
}
.login-btn {
width :100%
height: 40px;
border-radius: 16px;
}
.code-input{
width: 306px;
margin-right: 9px;
}
:deep(.el-tabs__item.is-active), :deep(.el-tabs__item:hover) {
color: var(--common-text-color) !important
}
:deep(.el-tabs__item) {
color: var(--text-theme-color)
}

View File

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

View File

@@ -58,7 +58,8 @@ body {
.container {
padding: 15px 20px 30px 20px;
background: #ffffff;
margin-bottom 80px
margin-bottom 20px
max-width 100%
}
.crumbs {
@@ -170,24 +171,68 @@ body {
.content-collapse {
left: 65px;
}
.el-table {
width: 100%;
.el-table__body-header {
height 40px
}
}
}
.w-100 {
width 100%
}
.mr-1 {
margin-right 5px
margin-right 0.5rem
}
.mr-2 {
margin-right 10px
margin-right 1rem
}
.ml-1 {
margin-left 5px
margin-left 0.5rem
}
.ml-2 {
margin-left 10px
margin-left 1rem
}
.d-flex {
display flex !important
}
.justify-center {
justify-content center
}
.justify-between {
justify-content space-between
}
.justify-end {
justify-content flex-end
}
.align-center {
align-items center
}
.p-1 {
padding 0.5rem
}
.p-2 {
padding 1rem
}
.m-1 {
margin 0.5rem
}
.m-2 {
margin 1rem
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,12 +79,12 @@
}
}
.van-theme-dark {
.mobile-chat {
.chat-list-wrapper {
background #232425;
}
}
}
// .van-theme-dark {
// .mobile-chat {
// .chat-list-wrapper {
// background #232425;
// }
// }
// }
@import "model-select.styl"

View File

@@ -183,6 +183,7 @@
display flex
flex-flow column
justify-content center
height 200px
.title {
margin-bottom 20px

View File

@@ -4,15 +4,20 @@
width 100%
display flex
flex-flow row
.image-slot {
color var(--theme-text-color-primary)
}
}
.job-item {
margin-right 10px
width 200px
height 200px
overflow hidden
padding 2px
background-color #555555
background-color var( --gray-btn-bg)
.job-item-inner {
position relative
@@ -31,7 +36,7 @@
span {
font-size 20px
color #ffffff
color var(--theme-text-color-primary)
}
}
}

View File

@@ -1,6 +1,6 @@
.el-overlay-dialog {
.el-dialog {
background-color #1a1b1e
// background-color #1a1b1e
.el-dialog__header {
.el-dialog__title {
@@ -33,14 +33,15 @@
}
.task-info {
background-color #25262b
// background-color #25262b
padding 1rem 1.5rem
.info-line {
width 100%
.prompt {
background-color #35363b
// background-color #35363b
padding 10px
color #999999
overflow auto
@@ -64,16 +65,16 @@
label {
display flex
width 100px
color #a5a5a5
color :var(--text-fb)
}
.item-value {
display flex
width 100%
background-color #35363b
// background-color #35363b
padding 2px 5px
border-radius 5px
color #F5F5F5
color: var(--text-theme-color);
}
}

View File

@@ -1,9 +1,16 @@
.page-suno {
display flex
height 100%
background-color #0E0808
// background-color #0E0808
overflow auto
.item-group{
scrollbar-width: auto !important; /* Firefox */
-ms-overflow-style: auto !important; /* IEEdge */
::-webkit-scrollbar {
display: block !important;
}
}
.left-bar {
max-width 340px
min-width 340px
@@ -13,6 +20,7 @@
display flex
flex-flow row
justify-content: space-between;
align-items center
.upload-music {
.iconfont {
@@ -24,7 +32,7 @@
.params {
padding 20px 0
color rgb(250 247 245)
color: var(--text-theme-color);
position relative
.pure-music {
@@ -77,6 +85,7 @@
opacity: 0.9;
}
}
.song {
display flex
@@ -137,6 +146,8 @@
bottom 10px
font-size 12px
padding 2px 5px
background-color var(--sm-btn-bg)
color: #fff
}
}
@@ -144,8 +155,14 @@
position relative
overflow-x auto
overflow-y hidden
scrollbar-width: auto !important; /* Firefox */
-ms-overflow-style: auto !important; /* IEEdge */
width 100%
::-webkit-scrollbar {
display: block !important;
}
.inner {
display flex
flex-flow row
@@ -154,12 +171,16 @@
.tag {
margin-right 10px
word-break keep-all
background-color #312C2C
color #e1e1e1
border-radius 5px
background: var(--card-bg);
color:var(--theme-text-color-primary);
opacity 0.7
border-radius 8px
padding 3px 6px
cursor pointer
font-size 13px
&:hover{
color:var( --el-color-primary)
}
}
}
}
@@ -169,9 +190,11 @@
width 100%
color rgb(250 247 245)
overflow auto
background: var(--chat-bg)
.list-box {
padding 0 0 0 20px
padding 20px
.item {
display flex
flex-flow row
@@ -180,7 +203,7 @@
margin-bottom 10px
&:hover {
background-color #2A2525
background: rgba(188,149,236,0.08)
}
.left {
@@ -247,18 +270,18 @@
font-weight 700
a {
color rgb(250 247 245)
color var( --a-link-color)
&:hover {
text-decoration underline
}
}
.model {
color #E2E8F0
background-color #1C1616
border 1px solid #8f8f8f
color #8f8f8f
// background-color #1C1616
// border 1px solid #8f8f8f
font-weight normal
font-size 14px
font-size 12px
padding 1px 3px
border-radius 5px
margin-left 10px
@@ -271,7 +294,7 @@
.tags {
font-size 14px
color #d1d1d1
color var(--text-fb)
padding 3px 0
}
}
@@ -279,11 +302,13 @@
.right {
min-width 350px;
font-size 14px
padding 0 15px
padding 0 0 0 15px
display flex
justify-content right
.tools {
display flex
justify-content left
justify-content right
align-items center
flex-flow row
height 90px
@@ -291,20 +316,21 @@
.btn-publish {
padding 2px 10px
.text {
margin-right 10px
}
// .text {
// margin-right 10px
// }
}
.btn-icon {
background none
padding 6px
transition background 0.6s ease 0s
color #726E6C
color #919191
&:hover {
background #5f5958
color #e1e1e1
// background #5f5958
// color #e1e1e1
color:var(--el-color-primary)
}
.downloading {
@@ -356,7 +382,7 @@
}
.pagination {
padding 10px 20px
margin-top 20px
display flex
justify-content center
}
@@ -372,14 +398,26 @@
.btn {
margin-right 10px
background-color #363030
color: var((--theme-text-color-primary))
border none
border-radius 5px
padding 5px 10px
cursor pointer
background: var(--btn-bg)
&:hover {
background-color #5F5958
opacity :0.8
}
}
}
.submit-btn {
display flex
align-items: center
margin: 20px 0
justify-content: center;
.el-button {
width 200px
}
}

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,88 @@
@import 'font.styl'
:root[data-theme="dark"]{
--text-fb:#fff;
--text-color: rgba(255, 255, 255, 1) !important; //
--normal-color: rgba(163, 174, 208, 1); //
--el-text-color-primary: #fff;
p, h1, h2, h3, h4, h5, h6, article {
// color: var(--text-color) !important;
font-family: $font-regular;
}
html,
body,
#app,
.wrapper {
background: rgb(13, 20, 53)
background-color: rgb(13, 20, 53)
font-family: $font-regular;
}
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active:rgba(255, 255, 255, 0.1);
--card-bg:#252d58;
--chat-bg:#1f243f
--chat-wel-bg:#2d2f38;
--card-bg-table: rgba(17, 28, 68, 1);
--theme-bg:rgb(13, 20, 53);
--theme-bg-color: rgb(13, 20, 53);
--theme-bg-all:rgb(13, 20, 53);
--sign-bg: rgba(27, 37, 75, 1);
--text-theme-color: #fff;
--text-color-primary: #d1c7ff;
--theme-text-color-secondary: #a3aed0;
--theme-text-color-primary: #fff;
--theme-text-primary: #f3f3f3;
--line-box:rgba(255, 255, 255, 0.1);
--el-bg-color:#141a36;
--el-fill-color-blank: rgba(17, 28, 68, 1);
--el-fill-color-light: rgba(86, 86, 95, .2);
--el-color-primary-light-9:rgba(86, 86, 95, .2);
--el-text-color-regular: rgba(163, 174, 208, 1)
--el-border-color:rgb(79, 80, 85);//
--el-bg-color-overlay: rgba(17, 28, 68, 1);
--el-border-color-light: rgba(255, 255, 255, 0.2);
--chat-content-bg:rgba(86, 86, 95, .2);
--chat-content-bg-list:rgba(86, 86, 95, .2);
--hover-deep-color:#30323c;
//layout
.more-menus li.moreTitle,
.twoTittle .title,
.setting-menus span.title,
.setting-menus li .el-icon,
.setting-menus li .iconfont,
.layout .tab-box .menu-list-item{
filter: invert(100%);
}
.more-menus span.title{
color:#000;
}
//
--btn-bg: rgba(86, 86, 95, .5);
.el-table {
//
--el-fill-color-darker: rgba(100, 100, 100, .5);
--el-border-color-darker: #73767a;
--el-table-border-color: rgba(100, 100, 100, .5);
--el-table-row-hover-bg-color: rgba(16, 21, 43, .8);
--el-table-current-row-bg-color: rgba(16, 21, 43, .8);
}
//
--el-mask-color: rgba(255, 255, 255, 0.5);
--van-toast-background: rgba(255, 255, 255, 0.3);
--code-bg-color: #424242;
--code-text-color: #fff;
// vant
--van-cell-background: #141a36;
--van-cell-background-light: #242a46;
--van-button-default-background: #141a36;
--van-background: #141a36;
--van-tabbar-background: #141a36;
--van-nav-bar-background: #1B244A;
--van-dropdown-menu-background: #141a36;
}

View File

@@ -0,0 +1,53 @@
@import 'font.styl'
:root[data-theme="light"] {
--text-fb:#000;
--text-color: #5b62ce; //
--normal-color: rgba(43, 54, 116, 1); //
p, h1, h2, h3, h4, h5, h6, article {
font-family: $font-regular;
}
html,
body,
#app,
.wrapper {
font-family: $font-regular;
}
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active:rgba(134, 140, 255, 1);
--code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--card-bg:#fff;
--chat-bg:#fff;
--theme-bg:linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff);
--theme-bg-all:#f5f7fd;
--theme-bg-color: #f5f7fd;
--sign-bg: rgba(244, 247, 254, 1);
--text-theme-color: rgba(43, 54, 116, 1)
--text-color-primary: rgba(67, 24, 255, 1);
--line-box:rgba(79, 89, 102, 0.122);
--theme-text-color-primary: #000;
--theme-text-primary: #000;
--theme-text-color-secondary: #666;
--chat-content-bg:#f5f7fc;
--chat-list-bg: #0302020a;
--chat-content-bg-list:#fff;
--chat-wel-bg:rgba(247, 247, 248, 1);
--hover-deep-color:#fff;
--el-bg-color-overlay: #fff;
--el-bg-color:#fff;
--el-fill-color-blank: #fff;
--el-pagination-button-bg-color: rgba(86,86,95,0.2);
//
--btn-bg: rgba(100, 100, 100, .1);
//
--el-mask-color: rgba(100, 100, 100, 0.2);
// code
--code-bg-color: #ececec;
--code-text-color: var(--el-color-primary);
}

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1731289567907') format('woff2'),
url('iconfont.woff?t=1731289567907') format('woff'),
url('iconfont.ttf?t=1731289567907') format('truetype');
src: url('iconfont.woff2?t=1734934068681') format('woff2'),
url('iconfont.woff?t=1734934068681') format('woff'),
url('iconfont.ttf?t=1734934068681') format('truetype');
}
.iconfont {
@@ -13,6 +13,154 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-redeem:before {
content: "\e61a";
}
.icon-login:before {
content: "\e636";
}
.icon-present:before {
content: "\e648";
}
.icon-icon-warning:before {
content: "\e671";
}
.icon-help:before {
content: "\e64a";
}
.icon-success:before {
content: "\e61e";
}
.icon-error:before {
content: "\e64e";
}
.icon-house:before {
content: "\e619";
}
.icon-vip4:before {
content: "\e684";
}
.icon-vip1:before {
content: "\f90b";
}
.icon-vip2:before {
content: "\fabb";
}
.icon-vip3:before {
content: "\10135";
}
.icon-conversation:before {
content: "\e617";
}
.icon-arrow-down:before {
content: "\e615";
}
.icon-arrow-up:before {
content: "\e616";
}
.icon-refresh:before {
content: "\e90c";
}
.icon-refresh-bold:before {
content: "\e614";
}
.icon-copy:before {
content: "\e720";
}
.icon-new-chat:before {
content: "\e613";
}
.icon-expand:before {
content: "\e7a0";
}
.icon-colspan:before {
content: "\e79e";
}
.icon-question:before {
content: "\e8e9";
}
.icon-AIduihua_jihuo:before {
content: "\e6bb";
}
.icon-MidJourney:before {
content: "\e60e";
}
.icon-stable-diffusion:before {
content: "\e60f";
}
.icon-info:before {
content: "\e6a0";
}
.icon-more-horizontal:before {
content: "\e60d";
}
.icon-xinghao:before {
content: "\e8d6";
}
.icon-plus:before {
content: "\e61f";
}
.icon-plus-circle:before {
content: "\e822";
}
.icon-taiyang:before {
content: "\e60b";
}
.icon-yueliang:before {
content: "\e679";
}
.icon-prev-page:before {
content: "\e8ef";
}
.icon-next-page:before {
content: "\e8f0";
}
.icon-search:before {
content: "\e618";
}
.icon-sub_menu:before {
content: "\e75e";
}
.icon-google:before {
content: "\ea0c";
}
.icon-linggan:before {
content: "\e641";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,265 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "3624396",
"name": "兑换码",
"font_class": "redeem",
"unicode": "e61a",
"unicode_decimal": 58906
},
{
"icon_id": "8760110",
"name": "login",
"font_class": "login",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "29565277",
"name": "礼物",
"font_class": "present",
"unicode": "e648",
"unicode_decimal": 58952
},
{
"icon_id": "13519527",
"name": "警告",
"font_class": "icon-warning",
"unicode": "e671",
"unicode_decimal": 58993
},
{
"icon_id": "145466",
"name": "帮助",
"font_class": "help",
"unicode": "e64a",
"unicode_decimal": 58954
},
{
"icon_id": "1951950",
"name": "成功",
"font_class": "success",
"unicode": "e61e",
"unicode_decimal": 58910
},
{
"icon_id": "6204756",
"name": "失败",
"font_class": "error",
"unicode": "e64e",
"unicode_decimal": 58958
},
{
"icon_id": "3916695",
"name": "首页",
"font_class": "house",
"unicode": "e619",
"unicode_decimal": 58905
},
{
"icon_id": "10583159",
"name": "会员",
"font_class": "vip4",
"unicode": "e684",
"unicode_decimal": 59012
},
{
"icon_id": "23942994",
"name": "会员",
"font_class": "vip1",
"unicode": "f90b",
"unicode_decimal": 63755
},
{
"icon_id": "24111538",
"name": "会员",
"font_class": "vip2",
"unicode": "fabb",
"unicode_decimal": 64187
},
{
"icon_id": "37305926",
"name": "会员VIP",
"font_class": "vip3",
"unicode": "10135",
"unicode_decimal": 65845
},
{
"icon_id": "41134022",
"name": "新会话",
"font_class": "conversation",
"unicode": "e617",
"unicode_decimal": 58903
},
{
"icon_id": "17411805",
"name": "Arrow Down",
"font_class": "arrow-down",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "17411857",
"name": "Arrow Up",
"font_class": "arrow-up",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "7736305",
"name": "refresh",
"font_class": "refresh",
"unicode": "e90c",
"unicode_decimal": 59660
},
{
"icon_id": "1391302",
"name": "Refresh",
"font_class": "refresh-bold",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "19418384",
"name": "copy",
"font_class": "copy",
"unicode": "e720",
"unicode_decimal": 59168
},
{
"icon_id": "20584689",
"name": "New Chat",
"font_class": "new-chat",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "23995596",
"name": "收起展开-展开",
"font_class": "expand",
"unicode": "e7a0",
"unicode_decimal": 59296
},
{
"icon_id": "23995626",
"name": "收起展开-收起",
"font_class": "colspan",
"unicode": "e79e",
"unicode_decimal": 59294
},
{
"icon_id": "1727527",
"name": "306问号-线性圆框",
"font_class": "question",
"unicode": "e8e9",
"unicode_decimal": 59625
},
{
"icon_id": "35446270",
"name": "AI对话_激活",
"font_class": "AIduihua_jihuo",
"unicode": "e6bb",
"unicode_decimal": 59067
},
{
"icon_id": "39584617",
"name": "MidJourney-copy",
"font_class": "MidJourney",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "42109955",
"name": "stable-diffusion",
"font_class": "stable-diffusion",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "1227734",
"name": "info",
"font_class": "info",
"unicode": "e6a0",
"unicode_decimal": 59040
},
{
"icon_id": "159969",
"name": "more",
"font_class": "more-horizontal",
"unicode": "e60d",
"unicode_decimal": 58893
},
{
"icon_id": "8434022",
"name": "星号",
"font_class": "xinghao",
"unicode": "e8d6",
"unicode_decimal": 59606
},
{
"icon_id": "831577",
"name": "plus",
"font_class": "plus",
"unicode": "e61f",
"unicode_decimal": 58911
},
{
"icon_id": "6151285",
"name": "plus-circle",
"font_class": "plus-circle",
"unicode": "e822",
"unicode_decimal": 59426
},
{
"icon_id": "15056491",
"name": "太阳",
"font_class": "taiyang",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "40094190",
"name": "月亮-copy",
"font_class": "yueliang",
"unicode": "e679",
"unicode_decimal": 59001
},
{
"icon_id": "1727538",
"name": "上一页",
"font_class": "prev-page",
"unicode": "e8ef",
"unicode_decimal": 59631
},
{
"icon_id": "1727540",
"name": "下一页",
"font_class": "next-page",
"unicode": "e8f0",
"unicode_decimal": 59632
},
{
"icon_id": "2488134",
"name": "搜索",
"font_class": "search",
"unicode": "e618",
"unicode_decimal": 58904
},
{
"icon_id": "9845558",
"name": "sub_menu",
"font_class": "sub_menu",
"unicode": "e75e",
"unicode_decimal": 59230
},
{
"icon_id": "11983544",
"name": "google",
"font_class": "google",
"unicode": "ea0c",
"unicode_decimal": 59916
},
{
"icon_id": "15330210",
"name": "创意灵感",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

BIN
web/src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,60 @@
<template>
<div class="right flex-center">
<div class="logo">
<img src="@/assets/img/logo.png" alt="" />
</div>
<div>welcome</div>
<footer-bar />
</div>
</template>
<script setup>
import FooterBar from "@/components/FooterBar.vue";
</script>
<style lang="stylus" scoped>
.right{
font-size: 40px
font-weight: bold
color:#fff
flex-direction: column
background-image url("~@/assets/img/login-bg.png")
background-size cover
background-position center
width: 50%;
min-height: 100vh
max-height: 100vh
background-repeat: no-repeat;
position: relative;
overflow: hidden;
z-index: 1;
:deep(.foot-container){
position: absolute;
bottom: 20px;
width: 100%;
background: none;
color: var(--sm-txt);
font-size: 12px;
text-align: center;
.footer{
a,
span{
color: var(--text-fff)
}
}
}
}
.logo{
margin-bottom: 26px;
width: 200px
height: 200px
background: #fff
border-radius: 50%
img{
width: 100%;
object-fit: cover;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div>
<ThemeChange />
<div
@click="goBack"
class="flex back animate__animated animate__pulse animate__infinite"
>
<el-icon><ArrowLeftBold /></el-icon
>{{ title === "注册" ? "首页" : "返回" }}
</div>
<div class="title">{{ title }}</div>
<div class="smTitle" v-if="title !== '重置密码'">
{{ title === "登录" ? "没有账号" : "已有账号"
}}<span @click="goPageFun" class="text-color-primary sign"
>赶紧{{ title === "登录" ? "注册" : "登录" }}</span
>
</div>
<slot></slot>
<div class="flex orline" v-if="title !== '重置密码'">
<div class="lineor"></div>
<span></span>
<div class="lineor"></div>
</div>
</div>
</template>
<script setup>
import { ArrowLeftBold } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue";
import { defineProps } from "vue";
import { useRouter } from "vue-router";
const props = defineProps({
title: {
type: String,
default: "登录"
},
smTitle: { type: String, default: "没有账号?" },
goPage: {
type: String,
default: "/register"
}
});
const router = useRouter();
const goBack = () => {
if (props.title === "注册") {
router.push("/");
} else {
router.go(-1);
}
};
const goPageFun = () => {
if (props.title === "登录") {
router.push("/register");
} else {
router.push("/login");
}
};
</script>
<style lang="stylus" scoped>
.back{
color:var(--sm-txt)
font-size: 14px;
margin-bottom: 140px
margin-top: 18px
cursor: pointer
.el-icon{
margin-right: 6px
}
}
.title{
font-size: 36px
margin-bottom: 16px
color: var(--text-color)
}
.smTitle{
color: var(--text-color)
font-size: 14px;
margin-bottom: 36px
}
.sign{
text-decoration: underline;
cursor :pointer
}
.orline{
color:var(--text-secondary)
span{
font-size: 14px;
margin: 0 10px
}
.lineor{
width: 182px; height: 1px;
background: var(--text-secondary)
}
}
</style>

View File

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

View File

@@ -19,7 +19,7 @@
<el-input v-model="form.code" maxlength="6"/>
</el-col>
<el-col :span="8" style="padding-left: 10px">
<send-msg :receiver="form.mobile" type="mobile"/>
<send-msg :receiver="form.mobile" size="default" type="mobile"/>
</el-col>
</el-row>
</el-form-item>

View File

@@ -1,130 +1,138 @@
<template>
<el-container class="captcha-box">
<el-dialog
v-model="show"
:close-on-click-modal="true"
:show-close="false"
style="width: 360px;"
>
<el-dialog v-model="show" :close-on-click-modal="true" :show-close="isMobileInternal" style="width: 360px; --el-dialog-padding-primary: 5px 15px 15px 15px">
<template #title>
<div class="text-center p-3" style="color: var(--el-text-color-primary)" v-if="isMobileInternal">
<span>人机验证</span>
</div>
</template>
<slide-captcha
v-if="isMobile()"
:bg-img="bgImg"
:bk-img="bkImg"
:result="result"
@refresh="getSlideCaptcha"
@confirm="handleSlideConfirm"
@hide="show = false"/>
v-if="isMobileInternal"
:bg-img="bgImg"
:bk-img="bkImg"
:result="result"
@refresh="getSlideCaptcha"
@confirm="handleSlideConfirm"
@hide="show = false"
/>
<captcha-plus
v-else
:max-dot="maxDot"
:image-base64="imageBase64"
:thumb-base64="thumbBase64"
width="300"
@close="show = false"
@refresh="handleRequestCaptCode"
@confirm="handleConfirm"
v-else
:max-dot="maxDot"
:image-base64="imageBase64"
:thumb-base64="thumbBase64"
width="300"
@close="show = 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 {httpGet, httpPost} from "@/utils/http";
import { ref } from "vue";
import lodash from "lodash";
import { validateEmail, validateMobile } from "@/utils/validate";
import { httpGet, httpPost } from "@/utils/http";
import CaptchaPlus from "@/components/CaptchaPlus.vue";
import SlideCaptcha from "@/components/SlideCaptcha.vue";
import {isMobile} from "@/utils/libs";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import { isMobile } from "@/utils/libs";
import { showMessageError, showMessageOK } from "@/utils/dialog";
const show = ref(false)
const maxDot = ref(5)
const imageBase64 = ref('')
const thumbBase64 = ref('')
const captKey = ref('')
const dots = ref(null)
const show = ref(false);
const maxDot = ref(5);
const imageBase64 = ref("");
const thumbBase64 = ref("");
const captKey = ref("");
const dots = ref(null);
const isMobileInternal = isMobile();
const emits = defineEmits(['success']);
const emits = defineEmits(["success"]);
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 => {
showMessageError('获取人机验证数据失败:' + e.message)
})
}
httpGet("/api/captcha/get")
.then((res) => {
const data = res.data;
imageBase64.value = data.image;
thumbBase64.value = data.thumb;
captKey.value = data.key;
})
.catch((e) => {
showMessageError("获取人机验证数据失败:" + e.message);
});
};
const handleConfirm = (dts) => {
if (lodash.size(dts) <= 0) {
return showMessageError('请进行人机验证再操作')
return showMessageError("请进行人机验证再操作");
}
let dotArr = []
let dotArr = [];
lodash.forEach(dts, (dot) => {
dotArr.push(dot.x, dot.y)
})
dots.value = dotArr.join(',')
httpPost('/api/captcha/check', {
dotArr.push(dot.x, dot.y);
});
dots.value = dotArr.join(",");
httpPost("/api/captcha/check", {
dots: dots.value,
key: captKey.value
}).then(() => {
// ElMessage.success('人机验证成功')
show.value = false
emits('success', {key:captKey.value, dots:dots.value})
}).catch(() => {
showMessageError('人机验证失败')
handleRequestCaptCode()
key: captKey.value,
})
}
.then(() => {
// ElMessage.success('人机验证成功')
show.value = false;
emits("success", { key: captKey.value, dots: dots.value });
})
.catch(() => {
showMessageError("人机验证失败");
handleRequestCaptCode();
});
};
const loadCaptcha = () => {
show.value = true
show.value = true;
// 手机用滑动验证码
if (isMobile()) {
getSlideCaptcha()
getSlideCaptcha();
} else {
handleRequestCaptCode()
handleRequestCaptCode();
}
}
};
// 滑动验证码
const bgImg = ref('')
const bkImg = ref('')
const result = ref(0)
const bgImg = ref("");
const bkImg = ref("");
const result = ref(0);
const getSlideCaptcha = () => {
result.value = 0
httpGet("/api/captcha/slide/get").then(res => {
bkImg.value = res.data.bkImg
bgImg.value = res.data.bgImg
captKey.value = res.data.key
}).catch(e => {
showMessageError('获取人机验证数据失败:' + e.message)
})
}
result.value = 0;
httpGet("/api/captcha/slide/get")
.then((res) => {
bkImg.value = res.data.bkImg;
bgImg.value = res.data.bgImg;
captKey.value = res.data.key;
})
.catch((e) => {
showMessageError("获取人机验证数据失败:" + e.message);
});
};
const handleSlideConfirm = (x) => {
httpPost("/api/captcha/slide/check", {
key: captKey.value,
x: x
}).then(() => {
result.value = 1
show.value = false
emits('success',{key:captKey.value, x:x})
}).catch(() => {
result.value = 2
x: x,
})
}
.then(() => {
result.value = 1;
show.value = false;
emits("success", { key: captKey.value, x: x });
})
.catch(() => {
result.value = 2;
});
};
// 导出方法以便父组件调用
defineExpose({
loadCaptcha
loadCaptcha,
});
</script>
@@ -137,8 +145,9 @@ defineExpose({
}
.el-dialog__body {
padding 0
padding-bottom: 5px;
overflow: hidden;
}
}
}
</style>
</style>

View File

@@ -1,90 +1,93 @@
<template>
<div class="wg-cap-wrap" :style="{width: width}">
<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=" ">
<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="正在加载中...">
<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 class="wg-cap-wrap__ico flex">
<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',
name: "CaptchaPlus",
mounted() {
this.$emit('refresh')
this.$emit("refresh");
},
props: {
value: Boolean,
width: {
type: String,
default: '300px'
default: "300px",
},
calcPosType: {
type: String,
default: 'dom',
validator: value => ['dom', 'screen'].includes(value)
default: "dom",
validator: (value) => ["dom", "screen"].includes(value),
},
maxDot: {
type: Number,
default: 5
default: 5,
// validator: value => value > 10
},
imageBase64: String,
thumbBase64: String
thumbBase64: String,
},
data() {
return {
dots: [],
imageBase64Code: '',
thumbBase64Code: ''
}
imageBase64Code: "",
thumbBase64Code: "",
};
},
watch: {
value() {
this.dots = []
this.imageBase64Code = ''
this.thumbBase64Code = ''
this.dots = [];
this.imageBase64Code = "";
this.thumbBase64Code = "";
},
imageBase64(val) {
this.dots = []
this.imageBase64Code = val
this.dots = [];
this.imageBase64Code = val;
},
thumbBase64(val) {
this.dots = []
this.thumbBase64Code = val
}
this.dots = [];
this.thumbBase64Code = val;
},
},
methods: {
/**
* @Description: 处理关闭事件
*/
handleCloseEvent() {
this.$emit('close')
this.$emit("close");
// this.dots = []
// this.imageBase64Code = ''
// this.thumbBase64Code = ''
@@ -93,14 +96,14 @@ export default {
* @Description: 处理刷新事件
*/
handleRefreshEvent() {
this.dots = []
this.$emit('refresh')
this.dots = [];
this.$emit("refresh");
},
/**
* @Description: 处理确认事件
*/
handleConfirmEvent() {
this.$emit('confirm', this.dots)
this.$emit("confirm", this.dots);
},
/**
* @Description: 处理dot
@@ -108,102 +111,102 @@ export default {
*/
handleClickPos(ev) {
if (this.dots.length >= this.maxDot) {
return
return;
}
const e = ev || window.event
e.preventDefault()
const dom = e.currentTarget
const e = ev || window.event;
e.preventDefault();
const dom = e.currentTarget;
const {domX, domY} = this.getDomXY(dom)
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
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
mouseX = e.touches[0].clientX;
mouseY = e.touches[0].clientY;
} else {
mouseX = e.clientX
mouseY = e.clientY
mouseX = e.clientX;
mouseY = e.clientY;
}
// 计算点击的相对位置
const xPos = mouseX - domX
const yPos = mouseY - domY
const xPos = mouseX - domX;
const yPos = mouseY - domY;
// 转整形
const xp = parseInt(xPos.toString())
const yp = parseInt(yPos.toString())
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
index: this.dots.length + 1,
});
return false;
},
/**
* @Description: 找到元素的屏幕位置
* @param el
*/
calcLocationLeft(el) {
let tmp = el.offsetLeft
let val = el.offsetParent
let tmp = el.offsetLeft;
let val = el.offsetParent;
while (val != null) {
tmp += val.offsetLeft
val = val.offsetParent
tmp += val.offsetLeft;
val = val.offsetParent;
}
return tmp
return tmp;
},
/**
* @Description: 找到元素的屏幕位置
* @param el
*/
calcLocationTop(el) {
let tmp = el.offsetTop
let val = el.offsetParent
let tmp = el.offsetTop;
let val = el.offsetParent;
while (val != null) {
tmp += val.offsetTop
val = val.offsetParent
tmp += val.offsetTop;
val = val.offsetParent;
}
return tmp
return tmp;
},
/**
* @Description: 找到元素的屏幕位置
* @param dom
*/
getDomXY(dom) {
let x = 0
let y = 0
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
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
x += dom.offsetLeft;
y += dom.offsetTop;
dom = dom.offsetParent;
}
}
return {
domX: x,
domY: y
}
}
}
}
domY: y,
};
},
},
};
</script>
<style scoped lang="stylus">
.wg-cap-wrap {
background: #ffffff;
background: var(--el-bg-color);
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
@@ -219,14 +222,7 @@ export default {
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 {

View File

@@ -1,65 +1,66 @@
<template>
<div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="User"/>
</div>
<div class="chat-icon">
<img :src="data.icon" alt="User" />
</div>
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/>
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover" />
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ file.name }}</el-link>
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
</div>
<div class="info">
<span>{{GetFileType(file.ext)}}</span>
<span>{{FormatFileSize(file.size)}}</span>
</div>
<div class="info">
<span>{{ GetFileType(file.ext) }}</span>
<span>{{ FormatFileSize(file.size) }}</span>
</div>
</div>
</div>
</div>
<div class="content" v-html="content"></div>
<div class="bar" v-if="data.created_at > 0">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">tokens: {{ finalTokens }}</span>
</div>
</div>
<div class="content" v-html="content"></div>
<div class="bar" v-if="data.created_at > 0">
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ finalTokens }}</span>
</div>
</div>
</div>
</div>
<div class="chat-line chat-line-prompt-chat" v-else>
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="User"/>
<img :src="data.icon" alt="User" />
</div>
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/>
<el-image :src="file.url" fit="cover" />
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ file.name }}</el-link>
</div>
<div class="info">
<span>{{GetFileType(file.ext)}}</span>
<span>{{FormatFileSize(file.size)}}</span>
<span>{{ GetFileType(file.ext) }}</span>
<span>{{ FormatFileSize(file.size) }}</span>
</div>
</div>
</div>
@@ -69,8 +70,10 @@
<div class="content" v-html="content"></div>
</div>
<div class="bar" v-if="data.created_at > 0">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
</div>
</div>
</div>
@@ -78,66 +81,72 @@
</template>
<script setup>
import {onMounted, ref} from "vue"
import {Clock} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import { onMounted, ref } from "vue";
import { Clock } from "@element-plus/icons-vue";
import { httpPost } from "@/utils/http";
import hl from "highlight.js";
import {dateFormat, isImage, processPrompt} from "@/utils/libs";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
import { dateFormat, isImage, processPrompt } from "@/utils/libs";
import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
const md = new MarkdownIt({
breaks: true,
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
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, '&lt;/textarea>')}</textarea>`
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`
const langHtml = `<span class="lang-name">${lang}</span>`;
// 处理代码高亮
const preCode = hl.highlight(lang, str, true).value
const preCode = hl.highlight(lang, str, true).value;
// 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
}
// 处理代码高亮
const preCode = md.utils.escapeHtml(str)
const preCode = md.utils.escapeHtml(str);
// 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
},
});
md.use(mathjaxPlugin)
md.use(mathjaxPlugin);
md.use(emoji);
const props = defineProps({
data: {
type: Object,
default: {
content: '',
created_at: '',
content: "",
created_at: "",
tokens: 0,
model: '',
icon: '',
model: "",
icon: "",
},
},
listStyle: {
type: String,
default: 'list',
default: "list",
},
})
const finalTokens = ref(props.data.tokens)
const content =ref(processPrompt(props.data.content))
const files = ref([])
});
const finalTokens = ref(props.data.tokens);
const content = ref(processPrompt(props.data.content));
const files = ref([]);
onMounted(() => {
processFiles()
})
processFiles();
});
const processFiles = () => {
if (!props.data.content) {
return
return;
}
const linkRegex = /(https?:\/\/\S+)/g;
@@ -165,7 +174,7 @@ const processFiles = () => {
.catch(() => {});
for (let link of links) {
content.value = content.value.replace(link, "");
content.value = content.value.replace(link,"")
}
}
@@ -180,12 +189,14 @@ const isExternalImg = (link, files) => {
@import '@/assets/css/markdown/vue.css';
.chat-page,.chat-export {
.chat-line-prompt-list {
background-color #ffffff;
background-color:var( --chat-content-bg-list);
color:var(--theme-text-color-primary);
justify-content: center;
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3;
border-bottom: 0.5px solid var(--el-border-color);
.chat-line-inner {
display flex;
@@ -199,7 +210,7 @@ const isExternalImg = (link, files) => {
img {
width: 36px;
height: 36px;
border-radius: 10px;
border-radius: 50%;
padding: 1px;
}
}
@@ -228,8 +239,9 @@ const isExternalImg = (link, files) => {
display flex
flex-flow row
border-radius 10px
background-color #ffffff
background-color:var(--chat-content-bg);
border 1px solid #e3e3e3
color:var(--theme-text-color-primary);
padding 6px
margin-bottom 10px
@@ -261,7 +273,7 @@ const isExternalImg = (link, files) => {
.content {
word-break break-word;
padding: 0;
color #374151;
color:var(--theme-text-color-primary);
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
@@ -289,7 +301,7 @@ const isExternalImg = (link, files) => {
padding 10px 10px 10px 0;
.bar-item {
background-color #f7f7f8;
// background-color #f7f7f8;
color #888
padding 3px 5px;
margin-right 10px;
@@ -308,7 +320,7 @@ const isExternalImg = (link, files) => {
}
.chat-line-prompt-chat {
background-color #ffffff;
background: var(--chat-bg);
justify-content: center;
width 100%
padding-bottom: 1.5rem;
@@ -355,7 +367,8 @@ const isExternalImg = (link, files) => {
display flex
flex-flow row
border-radius 10px
background-color #ffffff
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
border 1px solid #e3e3e3
padding 6px
margin-bottom 10px
@@ -392,10 +405,10 @@ const isExternalImg = (link, files) => {
.content {
word-break break-word;
padding: 1rem
color #222222;
color var(--theme-text-primary);
font-size: var(--content-font-size);
overflow: auto;
background-color #98e165
background-color :var(--chat-content-bg);
border-radius: 10px 0 10px 10px;
img {

View File

@@ -2,48 +2,35 @@
<div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT">
<img :src="data.icon" alt="ChatGPT" />
</div>
<div class="chat-item">
<div class="content" v-html="md.render(processContent(data.content))"></div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">tokens: {{ data.tokens }}</span>
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ data.tokens }}</span>
<span class="bar-item">
<el-tooltip
class="box-item"
effect="dark"
content="复制回答"
placement="bottom"
>
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy/>
</el-icon>
</el-tooltip>
</span>
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly">
<span class="bar-item" @click="reGenerate(data.prompt)">
<el-tooltip
class="box-item"
effect="dark"
content="重新生成"
placement="bottom"
>
<el-icon><Refresh/></el-icon>
</el-tooltip>
</span>
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
</el-tooltip>
</span>
<span class="bar-item" @click="synthesis(data.content)">
<el-tooltip
class="box-item"
effect="dark"
content="生成语音朗读"
placement="bottom"
>
<i class="iconfont icon-speaker"></i>
</el-tooltip>
</span>
<span class="bar-item" @click="synthesis(data.content)">
<el-tooltip class="box-item" effect="dark" content="生成语音朗读" placement="bottom">
<i class="iconfont icon-speaker"></i>
</el-tooltip>
</span>
</span>
<!-- <span class="bar-item">-->
<!-- <el-dropdown trigger="click">-->
@@ -65,49 +52,36 @@
<div class="chat-line chat-line-reply-chat" v-else>
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT">
<img :src="data.icon" alt="ChatGPT" />
</div>
<div class="chat-item">
<div class="content-wrapper">
<div class="content" v-html="md.render(processContent(data.content))"></div>
</div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg">
<el-tooltip
class="box-item"
effect="dark"
content="复制回答"
placement="bottom"
>
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy/>
</el-icon>
</el-tooltip>
</span>
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly">
<span class="bar-item bg" @click="reGenerate(data.prompt)">
<el-tooltip
class="box-item"
effect="dark"
content="重新生成"
placement="bottom"
>
<el-icon><Refresh/></el-icon>
</el-tooltip>
</span>
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
</el-tooltip>
</span>
<span class="bar-item bg" @click="synthesis(data.content)">
<el-tooltip
class="box-item"
effect="dark"
content="生成语音朗读"
placement="bottom"
>
<i class="iconfont icon-speaker"></i>
</el-tooltip>
</span>
<span class="bar-item bg" @click="synthesis(data.content)">
<el-tooltip class="box-item" effect="dark" content="生成语音朗读" placement="bottom">
<i class="iconfont icon-speaker"></i>
</el-tooltip>
</span>
</span>
</div>
</div>
@@ -116,10 +90,14 @@
</template>
<script setup>
import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus";
import {dateFormat, processContent} from "@/utils/libs";
import { Clock, DocumentCopy, Refresh } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { dateFormat, processContent } from "@/utils/libs";
import hl from "highlight.js";
import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
// eslint-disable-next-line no-undef,no-unused-vars
const props = defineProps({
data: {
@@ -133,69 +111,75 @@ const props = defineProps({
},
readOnly: {
type: Boolean,
default: false
default: false,
},
listStyle: {
type: String,
default: 'list',
default: "list",
},
})
});
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
const md = new MarkdownIt({
breaks: true,
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
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, '&lt;/textarea>')}</textarea>`
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`
const langHtml = `<span class="lang-name">${lang}</span>`;
// 处理代码高亮
const preCode = hl.highlight(lang, str, true).value
const preCode = hl.highlight(str, { language: lang }).value;
// 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
}
// 处理代码高亮
const preCode = md.utils.escapeHtml(str)
const preCode = md.utils.escapeHtml(str);
// 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
},
});
md.use(mathjaxPlugin)
const emits = defineEmits(['regen']);
md.use(mathjaxPlugin);
md.use(emoji);
const emits = defineEmits(["regen"]);
if (!props.data.icon) {
props.data.icon = "images/gpt-icon.png"
props.data.icon = "images/gpt-icon.png";
}
const synthesis = (text) => {
console.log(text)
ElMessage.info("语音合成功能暂不可用")
}
console.log(text);
ElMessage.info("语音合成功能暂不可用");
};
// 重新生成
const reGenerate = (prompt) => {
console.log(prompt)
emits('regen', prompt)
}
console.log(prompt);
emits("regen", prompt);
};
</script>
<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
.chat-page,.chat-export {
--font-family: Menlo,"微软雅黑","Roboto Mono","Courier New",Courier,monospace,"Inter",sans-serif;
font-family: var(--font-family);
.chat-line-reply-list {
justify-content: center;
background-color: rgba(247, 247, 248, 1);
background-color: var(--chat-list-bg);
color:var(--theme-text-color-primary);
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3;
border-bottom: 0.5px solid var(--el-border-color);
.chat-line-inner {
display flex;
@@ -209,7 +193,7 @@ const reGenerate = (prompt) => {
img {
width: 36px;
height: 36px;
border-radius: 10px;
border-radius: 50%;
padding: 1px;
}
}
@@ -224,7 +208,7 @@ const reGenerate = (prompt) => {
min-height 20px;
word-break break-word;
padding: 0
color #374151;
color:var(--theme-text-color-primary);
font-size: var(--content-font-size);
border-radius: 5px;
overflow auto;
@@ -238,10 +222,13 @@ const reGenerate = (prompt) => {
line-height 1.5
code {
color #374151
background-color #e7e7e8
padding 0 3px;
border-radius 5px;
color:var(--theme-text-color-primary);
font-weight 600
// color:#fff
// background-color var(--el-color-primary-light-3)
// background-color: var(--el-color-primary);
// padding 3px 5px;
// border-radius 5px;
}
}
@@ -296,7 +283,8 @@ const reGenerate = (prompt) => {
color #212529
border-collapse collapse;
border 1px solid #dee2e6;
background-color #ffffff
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
thead {
th {
@@ -330,8 +318,6 @@ const reGenerate = (prompt) => {
padding 10px 10px 10px 0;
.bar-item {
background-color #e7e7e8;
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
@@ -396,10 +382,13 @@ const reGenerate = (prompt) => {
min-height 20px;
word-break break-word;
padding: 1rem
color #374151;
color var(--theme-text-primary);
font-size: var(--content-font-size);
overflow auto;
background-color #F5F5F5
// background-color #F5F5F5
background-color :var(--chat-content-bg);
border-radius: 0 10px 10px 10px;
img {
@@ -411,10 +400,12 @@ const reGenerate = (prompt) => {
line-height 1.5
code {
color #374151
background-color #e7e7e8
padding 0 3px;
border-radius 5px;
color:var(--code-text-color);
font-weight bold
font-family: var(--font-family);
background-color: var(--code-bg-color);
border-radius: 4px;
padding: .2rem .4rem;
}
}
@@ -469,7 +460,8 @@ const reGenerate = (prompt) => {
color #212529
border-collapse collapse;
border 1px solid #dee2e6;
background-color #ffffff
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
thead {
th {
@@ -504,7 +496,6 @@ const reGenerate = (prompt) => {
padding 10px 10px 10px 0;
.bar-item {
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
@@ -517,7 +508,7 @@ const reGenerate = (prompt) => {
}
.bar-item.bg {
background-color #e7e7e8
// background-color var( --gray-btn-bg)
cursor pointer
}
@@ -541,5 +532,4 @@ const reGenerate = (prompt) => {
}
}
</style>

View File

@@ -2,22 +2,22 @@
<el-container class="chat-file-list">
<div v-for="file in fileList" :key="file.url">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/>
<el-image :src="file.url" fit="cover" />
<div class="action">
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
</div>
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{substr(file.name, 30)}}</el-link>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ substr(file.name, 30) }}</el-link>
</div>
<div class="info">
<span>{{GetFileType(file.ext)}}</span>
<span>{{FormatFileSize(file.size)}}</span>
<span>{{ GetFileType(file.ext) }}</span>
<span>{{ FormatFileSize(file.size) }}</span>
</div>
</div>
<div class="action">
@@ -29,26 +29,24 @@
</template>
<script setup>
import {ref} from "vue";
import {CircleCloseFilled} from "@element-plus/icons-vue";
import {isImage, removeArrayItem, substr} from "@/utils/libs";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
import { ref } from "vue";
import { CircleCloseFilled } from "@element-plus/icons-vue";
import { isImage, removeArrayItem, substr } from "@/utils/libs";
import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
const props = defineProps({
files: {
type: Array,
default:[],
}
})
const emits = defineEmits(['removeFile']);
const fileList = ref(props.files)
default: [],
},
});
const emits = defineEmits(["removeFile"]);
const fileList = ref(props.files);
const removeFile = (file) => {
fileList.value = removeArrayItem(fileList.value, file, (v1,v2) => v1.url===v2.url)
emits('removeFile', file)
}
fileList.value = removeArrayItem(fileList.value, file, (v1, v2) => v1.url === v2.url);
emits("removeFile", file);
};
</script>
<style scoped lang="stylus">
@@ -75,7 +73,8 @@ const removeFile = (file) => {
display flex
flex-flow row
border-radius 10px
background-color #ffffff
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
border 1px solid #e3e3e3
padding 6px
margin-right 10px
@@ -110,7 +109,10 @@ const removeFile = (file) => {
color #da0d54
cursor pointer
font-size 20px
.el-icon {
background-color #fff
border-radius 50%
}
}
}
</style>
</style>

View File

@@ -3,145 +3,146 @@
<a class="file-upload-img" @click="fetchFiles(1)">
<i class="iconfont icon-attachment-st"></i>
</a>
<el-dialog
class="file-list-dialog"
v-model="show"
:close-on-click-modal="true"
:show-close="true"
:width="800"
title="文件管理"
>
<el-scrollbar ref="scrollbarRef" max-height="80vh" style="height: 100%;" @scroll="onScroll">
<el-dialog class="file-list-dialog" v-model="show" :close-on-click-modal="true" :show-close="true" :width="800" title="文件管理">
<el-scrollbar ref="scrollbarRef" max-height="80vh" style="height: 100%" @scroll="onScroll">
<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"
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="afterRead"
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
>
<el-icon class="avatar-uploader-icon">
<Plus/>
<Plus />
</el-icon>
</el-upload>
</div>
</el-col>
<el-col :span="3" v-for="file in fileData.items" :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)"/>
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
<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)" />
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)" />
</el-tooltip>
<div class="opt">
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle />
</div>
</div>
</el-col>
</el-row>
<el-row justify="center" v-if="!fileData.isLastPage" @click="fetchFiles(fileData.page)">
<el-link>加载更多</el-link>
</el-row>
</div>
</div>
</el-scrollbar>
</el-dialog>
</el-container>
</template>
<script setup>
import {reactive, ref} from "vue";
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {Delete, Plus} from "@element-plus/icons-vue";
import {isImage, removeArrayItem} from "@/utils/libs";
import {GetFileIcon} from "@/store/system";
import { reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import { httpGet, httpPost } from "@/utils/http";
import { Delete, Plus } from "@element-plus/icons-vue";
import { isImage, removeArrayItem } from "@/utils/libs";
import { GetFileIcon } from "@/store/system";
import { checkSession } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata";
const props = defineProps({
userId: Number,
});
const emits = defineEmits(['selected']);
const show = ref(false)
const scrollbarRef = ref(null)
const emits = defineEmits(["selected"]);
const show = ref(false);
const scrollbarRef = ref(null);
const fileData = reactive({
items:[],
items: [],
page: 1,
isLastPage: true,
})
});
const store = useSharedStore();
const fetchFiles = (pageNo) => {
if(pageNo === 1) show.value = true
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 }).then(res => {
const { items, page, total_page } = res.data
checkSession()
.then(() => {
show.value = true;
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 })
.then((res) => {
const { items, page, total_page } = res.data;
if(page === 1){
fileData.items = items
}else{
fileData.items = [...fileData.items, ...items]
}
if (page === 1) {
fileData.items = items;
} else {
fileData.items = [...fileData.items, ...items];
}
fileData.isLastPage = (page === total_page)
fileData.isLastPage = page === total_page;
if(!fileData.isLastPage){
fileData.page = page + 1
}
}).catch(() => {
})
}
if (!fileData.isLastPage) {
fileData.page = page + 1;
}
})
.catch((e) => {
showMessageError("获取文件列表失败:" + e.message);
});
})
.catch(() => {
store.setShowLoginDialog(true);
});
};
// el-scrollbar 滚动回调
const onScroll = (options) => {
const wrapRef = scrollbarRef.value.wrapRef
scrollbarRef.value.moveY = wrapRef.scrollTop * 100 / wrapRef.clientHeight
scrollbarRef.value.moveX = wrapRef.scrollLeft * 100 / wrapRef.clientWidth
const poor = wrapRef.scrollHeight - wrapRef.clientHeight
const wrapRef = scrollbarRef.value.wrapRef;
scrollbarRef.value.moveY = (wrapRef.scrollTop * 100) / wrapRef.clientHeight;
scrollbarRef.value.moveX = (wrapRef.scrollLeft * 100) / wrapRef.clientWidth;
const poor = wrapRef.scrollHeight - wrapRef.clientHeight;
// 判断滚动到底部 自动加载数据
if (options.scrollTop + 2 >= poor && !fileData.isLastPage) {
fetchFiles(fileData.page)
fetchFiles(fileData.page);
}
}
};
const afterRead = (file) => {
const formData = new FormData();
formData.append('file', file.file, file.name);
formData.append("file", file.file, file.name);
// 执行上传操作
httpPost('/api/upload', formData).then((res) => {
fileData.items.unshift(res.data)
ElMessage.success({message: "上传成功", duration: 500})
}).catch((e) => {
ElMessage.error('图片上传失败:' + e.message)
})
httpPost("/api/upload", formData)
.then((res) => {
fileData.items.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(() => {
fileData.items = removeArrayItem(fileData.items, file, (v1, v2) => {
return v1.id === v2.id
httpGet("/api/upload/remove?id=" + file.id)
.then(() => {
fileData.items = removeArrayItem(fileData.items, file, (v1, v2) => {
return v1.id === v2.id;
});
ElMessage.success("文件删除成功!");
fetchFiles(1);
})
ElMessage.success("文件删除成功!")
fetchFiles(1)
}).catch((e) => {
ElMessage.error('文件删除失败:' + e.message)
})
}
.catch((e) => {
ElMessage.error("文件删除失败:" + e.message);
});
};
const insertURL = (file) => {
show.value = false
show.value = false;
// 如果是相对路径,处理成绝对路径
if (file.url.indexOf("http") === -1) {
file.url = location.protocol + "//" + location.host + file.url
file.url = location.protocol + "//" + location.host + file.url;
}
emits('selected', file)
}
emits("selected", file);
};
</script>
<style lang="stylus">
@@ -149,7 +150,7 @@ const insertURL = (file) => {
.file-select-box {
.file-upload-img {
.iconfont {
font-size: 24px;
font-size: 19px;
}
}
@@ -215,4 +216,4 @@ const insertURL = (file) => {
}
}
}
</style>
</style>

View File

@@ -1,9 +1,11 @@
<template>
<div class="foot-container">
<div class="footer">
<div><span :style="{color:textColor}">{{copyRight}}</span></div>
<div>
<span>{{ copyRight }}</span>
</div>
<div v-if="!license.de_copy">
<a :href="gitURL" target="_blank" :style="{color:textColor}">
<a :href="gitURL" target="_blank">
{{ title }} -
{{ version }}
</a>
@@ -12,37 +14,45 @@
</div>
</template>
<script setup>
import { ref } from "vue";
import { httpGet } from "@/utils/http";
import { showMessageError } from "@/utils/dialog";
import { getLicenseInfo, getSystemInfo } from "@/store/cache";
import {ref} from "vue";
import {httpGet} from "@/utils/http";
import {showMessageError} from "@/utils/dialog";
import {getLicenseInfo, getSystemInfo} from "@/store/cache";
const title = ref("")
const version = ref(process.env.VUE_APP_VERSION)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
const copyRight = ref('')
const license = ref({})
const title = ref("");
const version = ref(process.env.VUE_APP_VERSION);
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const copyRight = ref("");
const license = ref({});
const props = defineProps({
textColor: {
type: String,
default: '#ffffff'
},
default: "#ffffff"
}
});
// 获取系统配置
getSystemInfo().then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE
copyRight.value = res.data.copyright?res.data.copyright:'极客学长 © 2023 - '+new Date().getFullYear()+' All rights reserved.'
}).catch(e => {
showMessageError("获取系统配置失败:" + e.message)
})
getSystemInfo()
.then((res) => {
title.value = res.data.title ?? process.env.VUE_APP_TITLE;
copyRight.value =
res.data.copyright.length > 1
? res.data.copyright
: "极客学长 © 2023 - " +
new Date().getFullYear() +
" All rights reserved.";
})
.catch((e) => {
showMessageError("获取系统配置失败:" + e.message);
});
getLicenseInfo().then(res => {
license.value = res.data
}).catch(e => {
showMessageError("获取 License 失败:" + e.message)
})
getLicenseInfo()
.then((res) => {
license.value = res.data;
})
.catch((e) => {
showMessageError("获取 License 失败:" + e.message);
});
</script>
<style scoped lang="stylus">
@@ -53,6 +63,8 @@ getLicenseInfo().then(res => {
width: 100%;
display flex;
justify-content center
background: var(--theme-bg);
margin-top -4px
.footer {
max-width 400px;
@@ -62,11 +74,15 @@ getLicenseInfo().then(res => {
width 100%
a {
color:var(--text-color)
&:hover {
text-decoration underline
}
}
span{
color:var(--text-color)
}
}
}
</style>
</style>

View File

@@ -1,76 +1,83 @@
<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
:data="items"
:row-key="(row) => row.id"
table-layout="auto"
border
>
<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>
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
</template>
</el-table-column>
</el-table>
</el-row>
<el-empty :image-size="100" v-else/>
<el-empty :image-size="100" :image="nodata" description="暂无数据" 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"/>
<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"
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
@current-change="fetchData()"
: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";
import nodata from "@/assets/img/no-data.png";
import { onMounted, ref } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/libs";
import Clipboard from "clipboard";
const items = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const loading = ref(true)
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', () => {
fetchData();
const clipboard = new Clipboard(".copy-order-no");
clipboard.on("success", () => {
ElMessage.success("复制成功");
})
});
clipboard.on('error', () => {
ElMessage.error('复制失败');
})
})
clipboard.on("error", () => {
ElMessage.error("复制失败");
});
});
// 获取数据
const fetchData = () => {
httpGet('/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);
})
}
httpGet("/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">
@@ -90,4 +97,4 @@ const fetchData = () => {
color #20a0ff
}
}
</style>
</style>

View File

@@ -1,46 +1,22 @@
<template>
<el-dialog
class="login-dialog"
v-model="showDialog"
:close-on-click-modal="true"
:show-close="false"
:before-close="close"
>
<template #header="{titleId, titleClass }">
<div class="header">
<div class="title" v-if="login">用户登录</div>
<div class="title" v-else>用户注册</div>
<div class="close-icon">
<el-icon @click="close">
<Close/>
</el-icon>
</div>
</div>
</template>
<div class="login-dialog w-full">
<div class="login-box" v-if="login">
<el-form :model="data" label-width="120px" class="form">
<el-form :model="data" class="form">
<div class="block">
<el-input placeholder="账号"
size="large"
v-model="data.username"
autocomplete="off">
<el-input placeholder="账号" size="large" v-model="data.username" autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
<Iphone />
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="请输入密码(8-16位)"
maxlength="16" size="large"
v-model="data.password" show-password
autocomplete="off">
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
<Lock />
</el-icon>
</template>
</el-input>
@@ -48,45 +24,45 @@
<el-row class="btn-row" :gutter="20">
<el-col :span="24">
<el-button class="login-btn" type="primary" size="large" @click="submitLogin">登录</el-button>
<el-button class="login-btn" type="primary" size="large" @click="submitLogin"> </el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<div class="reg">
还没有账号
<el-button type="primary" class="forget" size="small" @click="login = false">注册</el-button>
<div class="w-full">
<div class="text flex justify-center items-center pt-3 text-sm">
还没有账号
<el-button size="small" @click="login = false">注册</el-button>
<el-button type="info" class="forget" size="small" @click="showResetPass = true">忘记密码</el-button>
</div>
</el-col>
<el-col :span="12">
<div class="c-login" v-if="wechatLoginURL !== ''">
<div class="text">其他登录方式</div>
<div class="login-type">
<a class="wechat-login" :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"><i class="iconfont icon-wechat"></i></a>
<el-button type="info" class="forget" size="small" @click="showResetPass = true">忘记密码</el-button>
</div>
<div v-if="wechatLoginURL !== ''">
<el-divider>
<div class="text-center">其他登录方式</div>
</el-divider>
<div class="c-login flex justify-center">
<!-- <div class="login-type mr-2">
<a class="wechat-login" :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"><i class="iconfont icon-wechat"></i></a>
</div> -->
<div class="p-2 w-full">
<el-button type="success" class="w-full" size="large" :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"
><i class="iconfont icon-wechat mr-2"></i> 微信登录
</el-button>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</el-form>
</div>
<div class="register-box" v-else>
<div class="register-box w-full" v-else>
<el-form :model="data" class="form" v-if="enableRegister">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
<div class="block">
<el-input placeholder="手机号码"
size="large"
v-model="data.mobile"
maxlength="11"
autocomplete="off">
<el-input placeholder="手机号码" size="large" v-model="data.mobile" maxlength="11" autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
<Iphone />
</el-icon>
</template>
</el-input>
@@ -94,32 +70,26 @@
<div class="block">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="验证码"
size="large" maxlength="30"
v-model="data.code"
autocomplete="off">
<el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
<Checked />
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :receiver="data.mobile" type="mobile"/>
<send-msg size="large" :receiver="data.mobile" type="mobile" />
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="邮箱注册" name="email" v-if="enableEmail">
<div class="block">
<el-input placeholder="邮箱地址"
size="large"
v-model="data.email"
autocomplete="off">
<el-input placeholder="邮箱地址" size="large" v-model="data.email" autocomplete="off">
<template #prefix>
<el-icon>
<Message/>
<Message />
</el-icon>
</template>
</el-input>
@@ -127,32 +97,26 @@
<div class="block">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="验证码"
size="large" maxlength="30"
v-model="data.code"
autocomplete="off">
<el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
<Checked />
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :receiver="data.email" type="email"/>
<send-msg size="large" :receiver="data.email" type="email" />
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="用户名注册" name="username" v-if="enableUser">
<div class="block">
<el-input placeholder="用户名"
size="large"
v-model="data.username"
autocomplete="off">
<el-input placeholder="用户名" size="large" v-model="data.username" autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
<Iphone />
</el-icon>
</template>
</el-input>
@@ -161,55 +125,43 @@
</el-tabs>
<div class="block">
<el-input placeholder="请输入密码(8-16位)"
maxlength="16" size="large"
v-model="data.password" show-password
autocomplete="off">
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
<Lock />
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="重复密码(8-16位)"
size="large" maxlength="16" v-model="data.repass" show-password
autocomplete="off">
<el-input placeholder="重复密码(8-16位)" size="large" maxlength="16" v-model="data.repass" show-password autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
<Lock />
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="邀请码(可选)"
size="large"
v-model="data.invite_code"
autocomplete="off">
<el-input placeholder="邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off">
<template #prefix>
<el-icon>
<Message/>
<Message />
</el-icon>
</template>
</el-input>
</div>
<el-row class="btn-row" :gutter="20">
<el-col :span="12">
<el-button class="login-btn" type="primary" size="large" @click="submitRegister">注册</el-button>
</el-col>
<el-col :span="12">
<div class="text">
已有账号
<el-tag @click="login = true">登录</el-tag>
</div>
</el-col>
<div class="w-full">
<el-button class="login-btn w-full" type="primary" size="large" @click="submitRegister"> </el-button>
</div>
</el-row>
<div class="text text-sm flex justify-center items-center w-full pt-3">
已有账号
<el-button size="small" @click="login = true">登录</el-button>
</div>
</el-form>
<div class="tip-result" v-else>
@@ -224,45 +176,47 @@
<el-col :span="12">
<div class="wechat-card">
<el-image :src="wxImg"/>
<el-image :src="wxImg" />
</div>
</el-col>
</el-row>
</div>
</div>
<captcha v-if="enableVerify" @success="submit" ref="captchaRef" />
<captcha v-if="enableVerify" @success="submit" ref="captchaRef"/>
<reset-pass @hide="showResetPass = false" :show="showResetPass"/>
</el-dialog>
<reset-pass @hide="showResetPass = false" :show="showResetPass" />
</div>
</template>
<script setup>
import {onMounted, ref, watch} from "vue"
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {setUserToken} from "@/store/session";
import {validateEmail, validateMobile} from "@/utils/validate";
import {Checked, Close, Iphone, Lock, Message} from "@element-plus/icons-vue";
import { onMounted, ref, watch } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { setUserToken } from "@/store/session";
import { validateEmail, validateMobile } from "@/utils/validate";
import { Checked, Close, Iphone, Lock, Message } from "@element-plus/icons-vue";
import SendMsg from "@/components/SendMsg.vue";
import {arrayContains} from "@/utils/libs";
import {getSystemInfo} from "@/store/cache";
import { arrayContains } from "@/utils/libs";
import { getSystemInfo } from "@/store/cache";
import Captcha from "@/components/Captcha.vue";
import ResetPass from "@/components/ResetPass.vue";
import {setRoute} from "@/store/system";
import {useRouter} from "vue-router";
import {useSharedStore} from "@/store/sharedata";
import { setRoute } from "@/store/system";
import { useRouter } from "vue-router";
import { useSharedStore } from "@/store/sharedata";
// eslint-disable-next-line no-undef
const props = defineProps({
show: Boolean,
});
const showDialog = ref(false)
watch(() => props.show, (newValue) => {
showDialog.value = newValue
})
const showDialog = ref(false);
watch(
() => props.show,
(newValue) => {
showDialog.value = newValue;
}
);
const login = ref(true)
const login = ref(true);
const data = ref({
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS,
@@ -270,193 +224,171 @@ const data = ref({
email: "",
repass: "",
code: "",
invite_code: ""
})
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(true)
const wechatLoginURL = ref('')
const activeName = ref("")
const wxImg = ref("/images/wx.png")
const captchaRef = ref(null)
invite_code: "",
});
const enableMobile = ref(false);
const enableEmail = ref(false);
const enableUser = ref(false);
const enableRegister = ref(true);
const wechatLoginURL = ref("");
const activeName = ref("");
const wxImg = ref("/images/wx.png");
const captchaRef = ref(null);
// eslint-disable-next-line no-undef
const emits = defineEmits(['hide', 'success']);
const action = ref("login")
const enableVerify = ref(false)
const showResetPass = ref(false)
const router = useRouter()
const store = useSharedStore()
// 是否需要验证码,输入一次密码错之后就要验证码
const needVerify = ref(false)
const emits = defineEmits(["hide", "success"]);
const action = ref("login");
const enableVerify = ref(false);
const showResetPass = ref(false);
const router = useRouter();
const store = useSharedStore();
onMounted(() => {
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
httpGet("/api/user/clogin?return_url="+returnURL).then(res => {
wechatLoginURL.value = res.data.url
}).catch(e => {
console.log(e.message)
})
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`;
httpGet("/api/user/clogin?return_url=" + returnURL)
.then((res) => {
wechatLoginURL.value = res.data.url;
})
.catch((e) => {
console.log(e.message);
});
getSystemInfo().then(res => {
if (res.data) {
const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "username")) {
enableUser.value = true
activeName.value = 'username'
getSystemInfo()
.then((res) => {
if (res.data) {
const registerWays = res.data["register_ways"];
if (arrayContains(registerWays, "username")) {
enableUser.value = true;
activeName.value = "username";
}
if (arrayContains(registerWays, "email")) {
enableEmail.value = true;
activeName.value = "email";
}
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true;
activeName.value = "mobile";
}
// 是否启用注册
enableRegister.value = res.data["enabled_register"];
// 使用后台上传的客服微信二维码
if (res.data["wechat_card_url"] !== "") {
wxImg.value = res.data["wechat_card_url"];
}
enableVerify.value = res.data["enabled_verify"];
}
if (arrayContains(registerWays, "email")) {
enableEmail.value = true
activeName.value = 'email'
}
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true
activeName.value = 'mobile'
}
// 是否启用注册
enableRegister.value = res.data['enabled_register']
// 使用后台上传的客服微信二维码
if (res.data['wechat_card_url'] !== '') {
wxImg.value = res.data['wechat_card_url']
}
enableVerify.value = res.data['enabled_verify']
}
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
})
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
});
const submit = (verifyData) => {
if (action.value === "login") {
doLogin(verifyData)
doLogin(verifyData);
} else if (action.value === "register") {
doRegister(verifyData)
doRegister(verifyData);
}
}
};
// 登录操作
const submitLogin = () => {
if (data.value.username === '') {
return ElMessage.error('请输入用户名');
if (!data.value.username) {
return ElMessage.error("请输入用户名");
}
if (data.value.password === '') {
return ElMessage.error('请输入密码');
if (!data.value.password) {
return ElMessage.error("请输入密码");
}
if (enableVerify.value && needVerify.value) {
captchaRef.value.loadCaptcha()
action.value = "login"
if (enableVerify.value) {
captchaRef.value.loadCaptcha();
action.value = "login";
} else {
doLogin({})
doLogin({});
}
}
};
const doLogin = (verifyData) => {
data.value.key = verifyData.key
data.value.dots = verifyData.dots
data.value.x = verifyData.x
httpPost('/api/user/login', data.value).then((res) => {
setUserToken(res.data.token)
store.setIsLogin(true)
ElMessage.success("登录成功!")
emits("hide")
emits('success')
needVerify.value = false
}).catch((e) => {
ElMessage.error('登录失败,' + e.message)
needVerify.value = true
})
}
data.value.key = verifyData.key;
data.value.dots = verifyData.dots;
data.value.x = verifyData.x;
httpPost("/api/user/login", data.value)
.then((res) => {
setUserToken(res.data.token);
store.setIsLogin(true);
ElMessage.success("登录成功!");
emits("hide");
emits("success");
})
.catch((e) => {
ElMessage.error("登录失败," + e.message);
});
};
// 注册操作
const submitRegister = () => {
if (activeName.value === 'username' && data.value.username === '') {
return ElMessage.error('请输入用户名');
if (activeName.value === "username" && data.value.username === "") {
return ElMessage.error("请输入用户名");
}
if (activeName.value === 'mobile' && !validateMobile(data.value.mobile)) {
return ElMessage.error('请输入合法的手机号');
if (activeName.value === "mobile" && !validateMobile(data.value.mobile)) {
return ElMessage.error("请输入合法的手机号");
}
if (activeName.value === 'email' && !validateEmail(data.value.email)) {
return ElMessage.error('请输入合法的邮箱地址');
if (activeName.value === "email" && !validateEmail(data.value.email)) {
return ElMessage.error("请输入合法的邮箱地址");
}
if (data.value.password.length < 8) {
return ElMessage.error('密码的长度为8-16个字符');
return ElMessage.error("密码的长度为8-16个字符");
}
if (data.value.repass !== data.value.password) {
return ElMessage.error('两次输入密码不一致');
return ElMessage.error("两次输入密码不一致");
}
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return ElMessage.error('请输入验证码');
if ((activeName.value === "mobile" || activeName.value === "email") && data.value.code === "") {
return ElMessage.error("请输入验证码");
}
if (enableVerify.value && activeName.value === 'username') {
captchaRef.value.loadCaptcha()
action.value = "register"
if (enableVerify.value && activeName.value === "username") {
captchaRef.value.loadCaptcha();
action.value = "register";
} else {
doRegister({})
doRegister({});
}
}
};
const doRegister = (verifyData) => {
data.value.key = verifyData.key
data.value.dots = verifyData.dots
data.value.x = verifyData.x
data.value.reg_way = activeName.value
httpPost('/api/user/register', data.value).then((res) => {
setUserToken(res.data.token)
ElMessage.success({
"message": "注册成功!",
onClose: () => {
emits("hide")
emits('success')
},
duration: 1000
data.value.key = verifyData.key;
data.value.dots = verifyData.dots;
data.value.x = verifyData.x;
data.value.reg_way = activeName.value;
httpPost("/api/user/register", data.value)
.then((res) => {
setUserToken(res.data.token);
ElMessage.success({
message: "注册成功!",
onClose: () => {
emits("hide");
emits("success");
},
duration: 1000,
});
})
}).catch((e) => {
ElMessage.error('注册失败,' + e.message)
})
}
const close = function () {
emits('hide', false)
login.value = true
}
.catch((e) => {
ElMessage.error("注册失败," + e.message);
});
};
</script>
<style lang="stylus">
.login-dialog {
border-radius 10px
max-width 600px
.header {
position relative
.title {
padding 0
font-size 18px
}
.close-icon {
cursor pointer
position absolute
right 0
top 0
font-weight normal
font-size 20px
&:hover {
color #20a0ff
}
}
.el-tabs__nav {
display flex
width 100%
justify-content space-between
}
.el-dialog__body {
padding 10px 20px 20px 20px
}
.form {
.block {
margin-bottom 10px
@@ -466,6 +398,7 @@ const close = function () {
display flex
.login-btn {
font-size 16px
width 100%
}
@@ -491,7 +424,6 @@ const close = function () {
align-items: center;
}
.login-type {
padding 15px
display flex
justify-content center
@@ -507,14 +439,8 @@ const close = function () {
}
}
.reg {
height 50px
display flex
align-items center
.el-button {
margin-left 10px
}
.text {
color var(--el-text-color-primary)
}
}
@@ -525,4 +451,4 @@ const close = function () {
}
}
</style>
</style>

View File

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

View File

@@ -8,7 +8,7 @@
>
<div class="form" id="bind-mobile-form">
<el-form :model="form">
<el-form-item label="兑换码">
<el-form-item>
<el-input v-model="form.code"/>
</el-form-item>
</el-form>

View File

@@ -1,63 +1,45 @@
<template>
<div class="reset-pass">
<el-dialog
v-model="showDialog"
:close-on-click-modal="true"
width="540px"
:before-close="close"
:title="title"
class="reset-pass-dialog"
>
<el-dialog v-model="showDialog" :close-on-click-modal="true" width="500px" :before-close="close" :title="title" class="reset-pass-dialog">
<div class="form">
<el-form :model="form" label-width="80px" label-position="left">
<el-tabs v-model="form.type" class="demo-tabs">
<el-tab-pane label="手机号验证" name="mobile">
<el-form-item label="手机号">
<el-input v-model="form.mobile" placeholder="请输入手机号"/>
<el-input v-model="form.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="验证码">
<el-row class="code-row">
<el-col :span="16">
<el-input v-model="form.code" maxlength="6"/>
</el-col>
<el-col :span="8" class="send-button">
<send-msg size="" :receiver="form.mobile" type="mobile"/>
</el-col>
</el-row>
<div class="flex">
<el-input v-model="form.code" maxlength="6" class="mr-2 w-1/2" />
<send-msg size="" :receiver="form.mobile" type="mobile" />
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="邮箱验证" name="email">
<el-form-item label="邮箱地址">
<el-input v-model="form.email" placeholder="请输入邮箱地址"/>
<el-input v-model="form.email" placeholder="请输入邮箱地址" />
</el-form-item>
<el-form-item label="验证码">
<el-row class="code-row">
<el-col :span="16">
<el-input v-model="form.code" maxlength="6"/>
</el-col>
<el-col :span="8" class="send-button">
<send-msg size="" :receiver="form.email" type="email"/>
</el-col>
</el-row>
<div class="flex">
<el-input v-model="form.code" maxlength="6" class="mr-2 w-1/2" />
<send-msg size="" :receiver="form.email" type="email" />
</div>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item label="新密码">
<el-input v-model="form.password" type="password"/>
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="重复密码">
<el-input v-model="form.repass" type="password"/>
<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>
<el-button type="primary" @click="save" round> 重置密码 </el-button>
</div>
</template>
</el-dialog>
@@ -65,35 +47,35 @@
</template>
<script setup>
import {computed, ref} from "vue";
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";
import { ElMessage } from "element-plus";
import { httpPost } from "@/utils/http";
import { validateEmail, validateMobile } from "@/utils/validate";
const props = defineProps({
show: Boolean,
mobile: String
mobile: String,
});
const showDialog = computed(() => {
return props.show
})
return props.show;
});
const title = ref('重置密码')
const title = ref("重置密码");
const form = ref({
mobile: '',
email: '',
type: 'mobile',
code: '',
password: '',
repass: ''
})
mobile: "",
email: "",
type: "mobile",
code: "",
password: "",
repass: "",
});
const emits = defineEmits(['hide']);
const emits = defineEmits(["hide"]);
const save = () => {
if (form.value.code === '') {
if (form.value.code === "") {
return ElMessage.error("请输入验证码");
}
if (form.value.password.length < 8) {
@@ -103,18 +85,22 @@ const save = () => {
return ElMessage.error("两次输入密码不一致");
}
httpPost('/api/user/resetPass', form.value).then(() => {
ElMessage.success({
message: '重置密码成功', duration: 1000, onClose: () => emits('hide', false)
httpPost("/api/user/resetPass", form.value)
.then(() => {
ElMessage.success({
message: "重置密码成功",
duration: 1000,
onClose: () => emits("hide", false),
});
})
}).catch(e => {
ElMessage.error("重置密码失败:" + e.message);
})
}
.catch((e) => {
ElMessage.error("重置密码失败:" + e.message);
});
};
const close = function () {
emits('hide', false);
}
emits("hide", false);
};
</script>
<style lang="stylus">
@@ -140,5 +126,4 @@ const close = function () {
}
}
}
</style>
</style>

View File

@@ -0,0 +1,172 @@
<template>
<el-dialog v-model="show" :fullscreen="true" @close="close" style="--el-dialog-border-radius: 0px">
<template #header>
<div class="header">
<h3 style="color: var(--text-theme-color)">绘画任务详情</h3>
</div>
</template>
<el-row :gutter="20">
<el-col :span="16">
<div class="img-container">
<el-image :src="item['img_url']" fit="contain">
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<i class="iconfont icon-image"></i>
</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">
<i class="iconfont icon-copy"></i>
</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">
<i class="iconfont icon-copy"></i>
</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="drawSame(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import Clipboard from "clipboard";
import { showMessageOK, showMessageError } from "@/utils/dialog";
const props = defineProps({
modelValue: Boolean,
data: Object,
});
const item = ref(props.data);
const show = ref(props.modelValue);
const emit = defineEmits(["drawSame", "close"]);
const clipboard = ref(null);
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on("success", () => {
showMessageOK("复制成功!");
});
clipboard.value.on("error", () => {
showMessageError("复制失败!");
});
});
watch(
() => props.modelValue,
(newValue) => {
show.value = newValue;
}
);
watch(
() => props.data,
(newValue) => {
item.value = newValue;
}
);
const drawSame = (item) => {
emit("drawSame", item);
};
const close = () => {
emit("close");
};
</script>
<style lang="stylus" scoped></style>

View File

@@ -1,21 +1,21 @@
<template>
<el-container class="send-verify-code">
<el-button type="primary" class="send-btn" :size="props.size" :disabled="!canSend" @click="sendMsg" plain>
<el-button type="success" :size="props.size" :disabled="!canSend" @click="sendMsg">
{{ btnText }}
</el-button>
<captcha @success="doSendMsg" ref="captchaRef"/>
<captcha @success="doSendMsg" ref="captchaRef" />
</el-container>
</template>
<script setup>
// 发送短信验证码组件
import {ref} from "vue";
import {validateEmail, validateMobile} from "@/utils/validate";
import {httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import { ref } from "vue";
import { validateEmail, validateMobile } from "@/utils/validate";
import { httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import Captcha from "@/components/Captcha.vue";
import {getSystemInfo} from "@/store/cache";
import { getSystemInfo } from "@/store/cache";
// eslint-disable-next-line no-undef
const props = defineProps({
@@ -23,58 +23,65 @@ const props = defineProps({
size: String,
type: {
type: String,
default: 'mobile'
}
default: "mobile",
},
});
const btnText = ref('发送验证码')
const canSend = ref(true)
const captchaRef = ref(null)
const enableVerify = ref(false)
const btnText = ref("发送验证码");
const canSend = ref(true);
const captchaRef = ref(null);
const enableVerify = ref(false);
getSystemInfo().then(res => {
enableVerify.value = res.data['enabled_verify']
})
getSystemInfo().then((res) => {
enableVerify.value = res.data["enabled_verify"];
});
const sendMsg = () => {
if (!validateMobile(props.receiver) && props.type === 'mobile') {
return showMessageError("请输入合法的手机号")
if (!validateMobile(props.receiver) && props.type === "mobile") {
return ElMessage.error("请输入合法的手机号");
}
if (!validateEmail(props.receiver) && props.type === 'email') {
return showMessageError("请输入合法的邮箱地址")
if (!validateEmail(props.receiver) && props.type === "email") {
return ElMessage.error("请输入合法的邮箱地址");
}
if (enableVerify.value) {
captchaRef.value.loadCaptcha()
captchaRef.value.loadCaptcha();
} else {
doSendMsg({})
doSendMsg({});
}
}
};
const doSendMsg = (data) => {
if (!canSend.value) {
return
return;
}
canSend.value = false
httpPost('/api/sms/code', {receiver: props.receiver, key: data.key, dots: data.dots, x:data.x}).then(() => {
showMessageOK('验证码发送成功')
let time = 60
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
showMessageError('验证码发送失败:' + e.message)
canSend.value = false;
httpPost("/api/sms/code", {
receiver: props.receiver,
key: data.key,
dots: data.dots,
x: data.x,
})
}
.then(() => {
ElMessage.success("验证码发送成功");
let time = 60;
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" scoped>

View File

@@ -1,169 +1,177 @@
<template>
<div class="slide-captcha">
<div class="bg-img">
<el-image :src="backgroundImg" />
<div :class="verifyMsgClass" v-if="checked !== 0">
<span v-if="checked ===1">{{time}}s</span>
{{verifyMsg}}
<div class="flex justify-center items-center">
<div class="slide-captcha">
<div class="bg-img">
<el-image :src="backgroundImg" />
<div :class="verifyMsgClass" v-if="checked !== 0">
<span v-if="checked === 1">{{ time }}s</span>
{{ verifyMsg }}
</div>
<div class="refresh" @click="emits('refresh')">
<el-icon><Refresh /></el-icon>
</div>
<span class="block">
<el-image :src="blockImg" :style="{ left: blockLeft + 'px' }" />
</span>
</div>
<div class="refresh" @click="emits('refresh')">
<el-icon><Refresh /></el-icon>
</div>
<span class="block">
<el-image :src="blockImg" :style="{left: blockLeft+'px'}" />
</span>
</div>
<div class="verify">
<div class="verify-bar-area">
<span class="verify-msg">{{verifyText}}</span>
<div class="verify">
<div class="verify-bar-area">
<span class="verify-msg">{{ verifyText }}</span>
<div :class="leftBarClass" :style="{width: leftBarWidth+'px'}">
<div :class="blockClass" id="dragBlock"
:style="{left: blockLeft+'px'}">
<el-icon v-if="checked === 0"><ArrowRightBold /></el-icon>
<el-icon v-if="checked === 1"><CircleCheckFilled /></el-icon>
<el-icon v-if="checked === 2"><CircleCloseFilled /></el-icon>
<div :class="leftBarClass" :style="{ width: leftBarWidth + 'px' }">
<div :class="blockClass" id="dragBlock" :style="{ left: blockLeft + 'px' }">
<el-icon v-if="checked === 0"><ArrowRightBold /></el-icon>
<el-icon v-if="checked === 1"><CircleCheckFilled /></el-icon>
<el-icon v-if="checked === 2"><CircleCloseFilled /></el-icon>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// eslint-disable-next-line no-undef
import {onMounted, ref, watch} from "vue";
import {ArrowRightBold, CircleCheckFilled, CircleCloseFilled, Refresh} from "@element-plus/icons-vue";
import { onMounted, ref, watch } from "vue";
import { ArrowRightBold, CircleCheckFilled, CircleCloseFilled, Refresh } from "@element-plus/icons-vue";
// eslint-disable-next-line no-undef
const props = defineProps({
bgImg: String,
bkImg: String,
result: Number,
})
const verifyText = ref('向右滑动完成验证')
const verifyMsg = ref('')
const verifyMsgClass = ref("verify-text success")
const blockClass = ref('verify-move-block')
const leftBarClass = ref('verify-left-bar')
const backgroundImg = ref('')
const blockImg = ref('')
const leftBarWidth = ref(0)
const blockLeft = ref(0)
const checked = ref(0)
const time = ref('')
watch(() => props.bgImg, (newVal) => {
backgroundImg.value = newVal;
});
watch(() => props.bkImg, (newVal) => {
blockImg.value = newVal;
});
watch(() => props.result, (newVal) => {
checked.value = newVal;
if (newVal === 1) {
verifyMsgClass.value = "verify-text success"
blockClass.value = 'verify-move-block success'
leftBarClass.value = 'verify-left-bar success'
verifyMsg.value = '验证成功'
setTimeout(() => emits('hide'), 1000)
} else if (newVal ===2) {
verifyMsgClass.value = "verify-text error"
blockClass.value = 'verify-move-block error'
leftBarClass.value = 'verify-left-bar error'
verifyMsg.value = '验证失败'
setTimeout(() => {
reset()
emits('refresh')
}, 1000)
} else {
reset()
const verifyText = ref("向右滑动完成验证");
const verifyMsg = ref("");
const verifyMsgClass = ref("verify-text success");
const blockClass = ref("verify-move-block");
const leftBarClass = ref("verify-left-bar");
const backgroundImg = ref("");
const blockImg = ref("");
const leftBarWidth = ref(0);
const blockLeft = ref(0);
const checked = ref(0);
const time = ref("");
watch(
() => props.bgImg,
(newVal) => {
backgroundImg.value = newVal;
}
});
);
watch(
() => props.bkImg,
(newVal) => {
blockImg.value = newVal;
}
);
watch(
() => props.result,
(newVal) => {
checked.value = newVal;
if (newVal === 1) {
verifyMsgClass.value = "verify-text success";
blockClass.value = "verify-move-block success";
leftBarClass.value = "verify-left-bar success";
verifyMsg.value = "验证成功";
setTimeout(() => emits("hide"), 1000);
} else if (newVal === 2) {
verifyMsgClass.value = "verify-text error";
blockClass.value = "verify-move-block error";
leftBarClass.value = "verify-left-bar error";
verifyMsg.value = "验证失败";
setTimeout(() => {
reset();
emits("refresh");
}, 1000);
} else {
reset();
}
}
);
// eslint-disable-next-line no-undef
const emits = defineEmits(['confirm','refresh','hide']);
const emits = defineEmits(["confirm", "refresh", "hide"]);
let offsetX = 0, isDragging = false
let start = 0
let offsetX = 0,
isDragging = false;
let start = 0;
onMounted(() => {
const dragBlock = document.getElementById('dragBlock');
dragBlock.addEventListener('mousedown', (evt) => {
blockClass.value = 'verify-move-block active'
leftBarClass.value = 'verify-left-bar active'
leftBarWidth.value = 32
isDragging = true
verifyText.value = ""
offsetX = evt.clientX
start = new Date().getTime()
const dragBlock = document.getElementById("dragBlock");
dragBlock.addEventListener("mousedown", (evt) => {
blockClass.value = "verify-move-block active";
leftBarClass.value = "verify-left-bar active";
leftBarWidth.value = 32;
isDragging = true;
verifyText.value = "";
offsetX = evt.clientX;
start = new Date().getTime();
evt.preventDefault();
})
});
document.body.addEventListener('mousemove',(evt) => {
document.body.addEventListener("mousemove", (evt) => {
if (!isDragging) {
return
return;
}
const x = Math.max(evt.clientX - offsetX, 0)
const x = Math.max(evt.clientX - offsetX, 0);
blockLeft.value = x;
leftBarWidth.value = x + 32
})
leftBarWidth.value = x + 32;
});
document.body.addEventListener('mouseup', () => {
document.body.addEventListener("mouseup", () => {
if (!isDragging) {
return
return;
}
time.value = ((new Date().getTime() - start)/1000).toFixed(2)
isDragging = false
emits('confirm', Math.floor(blockLeft.value))
})
time.value = ((new Date().getTime() - start) / 1000).toFixed(2);
isDragging = false;
emits("confirm", Math.floor(blockLeft.value));
});
// 触摸事件
dragBlock.addEventListener('touchstart', function (e) {
dragBlock.addEventListener("touchstart", function (e) {
isDragging = true;
blockClass.value = 'verify-move-block active'
leftBarClass.value = 'verify-left-bar active'
leftBarWidth.value = 32
isDragging = true
verifyText.value = ""
blockClass.value = "verify-move-block active";
leftBarClass.value = "verify-left-bar active";
leftBarWidth.value = 32;
isDragging = true;
verifyText.value = "";
offsetX = e.touches[0].clientX - dragBlock.getBoundingClientRect().left;
start = new Date().getTime()
start = new Date().getTime();
e.preventDefault();
});
document.addEventListener('touchmove', function (e) {
document.addEventListener("touchmove", function (e) {
if (!isDragging) {
return
return;
}
e.preventDefault();
const x = Math.max(e.touches[0].clientX - offsetX, 0)
const x = Math.max(e.touches[0].clientX - offsetX, 0);
blockLeft.value = x;
leftBarWidth.value = x + 32
leftBarWidth.value = x + 32;
});
document.addEventListener('touchend', function () {
document.addEventListener("touchend", function () {
if (!isDragging) {
return
return;
}
time.value = ((new Date().getTime() - start)/1000).toFixed(2)
isDragging = false
emits('confirm', Math.floor(blockLeft.value))
time.value = ((new Date().getTime() - start) / 1000).toFixed(2);
isDragging = false;
emits("confirm", Math.floor(blockLeft.value));
});
})
});
// 重置验证码
const reset = () => {
blockClass.value = 'verify-move-block'
leftBarClass.value = 'verify-left-bar'
leftBarWidth.value = 0
blockLeft.value = 0
checked.value = 0
verifyText.value = "向右滑动完成验证"
}
blockClass.value = "verify-move-block";
leftBarClass.value = "verify-left-bar";
leftBarWidth.value = 0;
blockLeft.value = 0;
checked.value = 0;
verifyText.value = "向右滑动完成验证";
};
</script>
<style scoped lang="stylus">
@@ -177,6 +185,7 @@ const reset = () => {
}
.slide-captcha {
width 310px
* {
margin 0
padding 0
@@ -294,4 +303,4 @@ const reset = () => {
}
}
}
</style>
</style>

View File

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

View File

@@ -0,0 +1,52 @@
<template>
<div class="theme-box" @click="toggleTheme">
<span class="iconfont icon-yueliang">{{ themePage === "light" ? "&#xe679;" : "&#xe60b;" }}</span>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useSharedStore } from "@/store/sharedata";
// 定义主题状态,初始值从 localStorage 获取
const store = useSharedStore();
const themePage = ref(store.theme || "light");
// 切换主题函数
const toggleTheme = () => {
themePage.value = themePage.value === "light" ? "dark" : "light";
store.setTheme(themePage.value); // 保存主题
};
</script>
<style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css'
.theme-box{
z-index :111
position: fixed;
right: 40px;
bottom: 150px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 50%;
width 35px;
height: 35px;
line-height: 35px;
text-align: center;
// background-color: rgb(146, 147, 148);
background: linear-gradient(135deg, rgba(134, 140, 255, 1) 0%, rgba(67, 24, 255, 1) 100%);
transition: all 0.3s ease;
&:hover{
transform: scale(1.1);
}
&:active{
transform: scale(0.9);
}
.iconfont{
font-size: 20px;
color: yellow;
transition: transform 0.3s ease;
}
}
</style>

View File

@@ -1,66 +1,29 @@
<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>
<el-dialog class="config-dialog" v-model="showDialog" :close-on-click-modal="true" :before-close="close" style="max-width: 400px" title="账户信息">
<div class="flex-center-col p-4 pt-0" id="user-info">
<user-profile @hide="close" />
</div>
</el-dialog>
</template>
<script setup>
import {computed, onMounted, ref} from "vue"
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat} from "@/utils/libs";
import { computed } from "vue";
import UserProfile from "@/components/UserProfile.vue";
// 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)
});
})
return props.show;
});
// eslint-disable-next-line no-undef
const emits = defineEmits(['hide']);
const emits = defineEmits(["hide"]);
const close = function () {
emits('hide', false);
}
emits("hide", false);
};
</script>
<style lang="stylus">
@@ -68,23 +31,6 @@ const close = function () {
.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>
</style>

View File

@@ -1,95 +1,99 @@
<template>
<div class="user-bill" v-loading="loading" element-loading-background="rgba(255,255,255,.3)">
<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 :data="items" :row-key="(row) => row.id" table-layout="auto" border>
<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/>
<DocumentCopy />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="subject" label="产品名称"/>
<el-table-column prop="amount" label="订单金额"/>
<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 prop="pay_method" label="支付渠道"/>
<el-table-column prop="pay_name" label="支付名称"/>
<el-table-column prop="pay_method" label="支付渠道" />
<el-table-column prop="pay_name" label="支付名称" />
<el-table-column label="支付时间">
<template #default="scope">
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row["pay_time"]) }}</span>
<el-tag v-else>未支付</el-tag>
</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"/>
<el-empty :image-size="100" v-else :image="nodata" description="暂无数据" />
<div class="pagination pb-5">
<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()"
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
: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";
import {DocumentCopy} from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png";
import { onMounted, ref } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/libs";
import { DocumentCopy } from "@element-plus/icons-vue";
import Clipboard from "clipboard";
const items = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(12)
const loading = ref(true)
const items = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(12);
const loading = ref(true);
onMounted(() => {
fetchData()
const clipboard = new Clipboard('.copy-order-no');
clipboard.on('success', () => {
fetchData();
const clipboard = new Clipboard(".copy-order-no");
clipboard.on("success", () => {
ElMessage.success("复制成功");
})
});
clipboard.on('error', () => {
ElMessage.error('复制失败');
})
})
clipboard.on("error", () => {
ElMessage.error("复制失败");
});
});
// 获取数据
const fetchData = () => {
httpGet('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => {
if (res.data) {
items.value = res.data.items
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.page_size
}
loading.value = false
}).catch(e => {
ElMessage.error("获取数据失败" + e.message);
})
}
httpGet("/api/order/list", { page: page.value, page_size: pageSize.value })
.then((res) => {
if (res.data) {
items.value = res.data.items;
total.value = res.data.total;
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 {
background-color: var(--chat-bg);
.pagination {
margin: 20px 0 0 0;
display: flex;
@@ -105,4 +109,4 @@ const fetchData = () => {
color #20a0ff
}
}
</style>
</style>

View File

@@ -1,79 +1,79 @@
<template>
<div class="user-info" id="user-info">
<el-form :model="user" label-width="100px">
<div class="user-info flex-center-col" id="user-info">
<el-form :model="user" label-width="80px" label-position="left">
<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-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="afterRead" accept=".png,.jpg,.jpeg,.bmp">
<el-tooltip content="点击上传头像" placement="top" v-if="user.avatar">
<el-avatar :src="user.avatar" shape="circle" :size="100" />
</el-tooltip>
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
<Plus />
</el-icon>
</el-upload>
</el-row>
<el-form-item label="昵称">
<el-input v-model="user['nickname']"/>
<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"
>
<span class="vip-icon"><el-image v-if="user.vip" :src="vipImg" style="height: 25px;margin-left: 10px"/></span>
</el-tooltip>
<div class="flex">
<span>{{ user.username }}</span>
<el-tooltip class="box-item" content="您已经是 VIP 会员" placement="right">
<span class="vip-icon"><el-image v-if="user.vip" :src="vipImg" class="rounded-full ml-1 size-5" /></span>
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="剩余算力">
<el-tag>{{ user['power'] }}</el-tag>
<el-text type="warning">{{ user["power"] }}</el-text>
<el-tag type="info" size="small" class="ml-2 cursor-pointer" @click="gotoLog">算力日志</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 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" @click="save">保存</el-button>
<el-button :dark="false" type="primary" @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 { 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 "@/store/cache";
import { dateFormat } from "@/utils/libs";
import { checkSession } from "@/store/cache";
import { useRouter } from "vue-router";
const user = ref({
vip: false,
username: '演示数据',
nickname: '演示数据',
avatar: '/images/vip.png',
mobile: '演示数据',
username: "演示数据",
nickname: "演示数据",
avatar: "/images/menu/member.png",
mobile: "演示数据",
power: 99999,
})
const vipImg = ref("/images/vip.png")
});
const vipImg = ref("/images/menu/member.png");
const router = useRouter();
const emits = defineEmits(["hide"]);
onMounted(() => {
checkSession().then(() => {
// 获取最新用户信息
httpGet('/api/user/profile').then(res => {
user.value = res.data
}).catch(e => {
ElMessage.error("获取用户信息失败:" + e.message)
checkSession()
.then(() => {
// 获取最新用户信息
httpGet("/api/user/profile")
.then((res) => {
user.value = res.data;
})
.catch((e) => {
ElMessage.error("获取用户信息失败:" + e.message);
});
})
.catch((e) => {
console.log(e);
});
}).catch(e => {
console.log(e)
})
})
});
const afterRead = (file) => {
// 压缩图片并上传
@@ -81,14 +81,16 @@ const afterRead = (file) => {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append('file', result, result.name);
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)
})
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);
@@ -97,17 +99,23 @@ const afterRead = (file) => {
};
const save = () => {
httpPost('/api/user/profile/update', user.value).then(() => {
ElMessage.success({message: '更新成功', duration: 500})
}).catch((e) => {
ElMessage.error('更新失败:' + e.message)
})
}
httpPost("/api/user/profile/update", user.value)
.then(() => {
ElMessage.success({ message: "更新成功", duration: 500 });
})
.catch((e) => {
ElMessage.error("更新失败:" + e.message);
});
};
const gotoLog = () => {
router.push("/powerLog");
emits("hide", false);
};
</script>
<style lang="stylus" scoped>
.user-info {
padding 20px 0
.el-row {
justify-content center
@@ -120,11 +128,10 @@ const save = () => {
}
.opt-line {
padding-top 20px
.el-button {
width 100%
}
}
}
</style>
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="welcome">
<div class="container">
<h1 class="title">{{ title }}-{{ version }}</h1>
<h2 class="title">{{ title }}-{{ version }}</h2>
<el-row :gutter="20">
<el-col :span="8">
@@ -13,7 +13,9 @@
<div class="list-box">
<ul>
<li v-for="item in samples" :key="item"><a @click="send(item)">{{ item }}</a></li>
<li v-for="item in samples" :key="item">
<a @click="send(item)">{{ item }}</a>
</li>
</ul>
</div>
</div>
@@ -27,7 +29,9 @@
<div class="list-box">
<ul>
<li v-for="item in plugins" :key="item.value"><a @click="send(item.value)">{{ item.text }}</a></li>
<li v-for="item in plugins" :key="item.value">
<a @click="send(item.value)">{{ item.text }}</a>
</li>
</ul>
</div>
</div>
@@ -54,19 +58,18 @@
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { getSystemInfo } from "@/store/cache";
import {onMounted, ref} from "vue";
import {ElMessage} from "element-plus";
import {getSystemInfo} from "@/store/cache";
const title = ref(process.env.VUE_APP_TITLE)
const version = ref(process.env.VUE_APP_VERSION)
const title = ref(process.env.VUE_APP_TITLE);
const version = ref(process.env.VUE_APP_VERSION);
const samples = ref([
"用小学生都能听懂的术语解释什么是量子纠缠",
"能给一位6岁男孩的生日会提供一些创造性的建议吗",
"如何用 Go 语言实现支持代理 Http client 请求?"
])
]);
const plugins = ref([
{
@@ -81,7 +84,7 @@ const plugins = ref([
value: "今日头条",
text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
}
])
]);
const capabilities = ref([
{
@@ -96,20 +99,22 @@ const capabilities = ref([
text: "绘画马斯克开拖拉机20世纪中国农村。3:2",
value: "绘画马斯克开拖拉机20世纪中国农村。3:2"
}
])
]);
onMounted(() => {
getSystemInfo().then(res => {
title.value = res.data.title
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
})
getSystemInfo()
.then((res) => {
title.value = res.data.title;
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
});
const emits = defineEmits(['send']);
const emits = defineEmits(["send"]);
const send = (text) => {
emits('send', text)
}
emits("send", text);
};
</script>
<style scoped lang="stylus">
.welcome {
@@ -123,10 +128,11 @@ const send = (text) => {
width 100%
.title {
font-size: 2.25rem
// font-size: 2.25rem
line-height: 2.5rem
font-weight 600
margin-bottom: 4rem
color var( --theme-textcolor-normal)
}
.grid-content {
@@ -148,10 +154,9 @@ const send = (text) => {
font-size 14px;
padding .75rem
border-radius 5px;
background-color: rgba(247, 247, 248, 1);
background-color: var(--chat-wel-bg);
color:var( --theme-text-color-secondary);
line-height 1.5
color #666666
a {
cursor pointer
@@ -165,4 +170,4 @@ const send = (text) => {
}
}
}
</style>
</style>

View File

@@ -1,44 +1,36 @@
<template>
<div :class="'admin-header '+theme">
<div :class="'admin-header ' + theme">
<!-- 折叠按钮 -->
<div class="collapse-btn" @click="collapseChange">
<el-icon v-if="sidebar.collapse">
<Expand/>
<Expand />
</el-icon>
<el-icon v-else>
<Fold/>
<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-item v-for="item in breadcrumb" :key="item.title">{{ item.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<div class="header-user-con">
<!-- 切换主题 -->
<el-switch
style="margin-right: 10px"
v-model="dark"
inline-prompt
:active-action-icon="Moon"
:inactive-action-icon="Sunny"
@change="changeTheme"
/>
<el-switch style="margin-right: 10px" v-model="dark" inline-prompt :active-action-icon="Moon"
:inactive-action-icon="Sunny" @change="changeTheme"/>
<!-- 用户名下拉菜单 -->
<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>
<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>
<el-dropdown-item>
<i class="iconfont icon-version"></i> 当前版本{{ version }}
</el-dropdown-item>
<el-dropdown-item><i class="iconfont icon-version"></i> 当前版本{{ version }}</el-dropdown-item>
<el-dropdown-item divided @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
@@ -48,12 +40,11 @@
</el-dropdown>
</div>
</div>
</div>
</template>
<script setup>
import {onMounted, ref, watch} from 'vue';
import {getMenuItems, useSidebarStore} from '@/store/sidebar';
import {onMounted, ref, watch} from "vue";
import {getMenuItems, useSidebarStore} from "@/store/sidebar";
import {useRouter} from "vue-router";
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
@@ -61,69 +52,72 @@ import {ElMessage} from "element-plus";
import {removeAdminToken} from "@/store/session";
import {useSharedStore} from "@/store/sharedata";
const version = ref(process.env.VUE_APP_VERSION)
const avatar = ref('/images/user-info.jpg')
const version = ref(process.env.VUE_APP_VERSION);
const avatar = ref("/images/user-info.jpg");
const sidebar = useSidebarStore();
const router = useRouter();
const breadcrumb = ref([])
const breadcrumb = ref([]);
const store = useSharedStore()
const dark = ref(store.adminTheme === 'dark')
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
theme.value = val
})
const store = useSharedStore();
const dark = ref(store.theme === "dark");
const theme = ref(store.theme);
watch(
() => store.theme,
(val) => {
theme.value = val;
}
);
const changeTheme = () => {
store.setAdminTheme(dark.value ? 'dark' : 'light')
}
store.setTheme(dark.value ? "dark" : "light");
};
router.afterEach((to) => {
initBreadCrumb(to.path)
initBreadCrumb(to.path);
});
onMounted(() => {
initBreadCrumb(router.currentRoute.value.path)
})
initBreadCrumb(router.currentRoute.value.path);
});
// 初始化面包屑导航
const initBreadCrumb = (path) => {
breadcrumb.value = [{title: "首页"}]
const items = getMenuItems()
breadcrumb.value = [{ title: "首页" }];
const items = getMenuItems();
if (items) {
let bk = false
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
path: items[i].index,
});
break;
}
if (bk) {
break
break;
}
if (items[i]['subs']) {
const subs = items[i]['subs']
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
})
path: items[i].index,
});
breadcrumb.value.push({
title: subs[j].title,
path: subs[j].index
})
bk = true
break
path: subs[j].index,
});
bk = true;
break;
}
}
}
}
}
}
};
// 侧边栏折叠
const collapseChange = () => {
@@ -137,13 +131,15 @@ onMounted(() => {
});
const logout = function () {
httpGet("/api/admin/logout").then(() => {
removeAdminToken()
router.replace('/admin/login')
}).catch((e) => {
ElMessage.error("注销失败: " + e.message);
})
}
httpGet("/api/admin/logout")
.then(() => {
removeAdminToken();
router.replace("/admin/login");
})
.catch((e) => {
ElMessage.error("注销失败: " + e.message);
});
};
</script>
<style scoped lang="stylus">
.admin-header {
@@ -152,8 +148,8 @@ const logout = function () {
overflow hidden
height: 50px;
font-size: 22px;
color: #303133;
background-color #ffffff
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
.collapse-btn {
display: flex;
@@ -260,5 +256,4 @@ const logout = function () {
.admin-header {
}
</style>

View File

@@ -1,40 +1,36 @@
<template>
<div :class="'sidebar '+theme">
<a class="logo" href="/" target="_blank">
<el-image :src="logo"/>
<div :class="'sidebar ' + theme">
<a class="logo w-full flex items-center" href="/" target="_blank">
<img :src="logo" />
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
</a>
<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
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>
<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"
>
<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" :key="subItem.index">
<i v-if="subItem.icon" :class="'iconfont icon-'+subItem.icon"></i>
<i v-if="subItem.icon" :class="'iconfont icon-' + subItem.icon"></i>
{{ subItem.title }}
</el-menu-item>
</template>
@@ -42,7 +38,7 @@
</template>
<template v-else>
<el-menu-item :index="item.index" :key="item.index">
<i :class="'iconfont icon-'+item.icon"></i>
<i :class="'iconfont icon-' + item.icon"></i>
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
@@ -52,120 +48,125 @@
</template>
<script setup>
import {computed, ref, watch} from 'vue';
import {setMenuItems, useSidebarStore} from '@/store/sidebar';
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRoute} from "vue-router";
import {useSharedStore} from "@/store/sharedata";
import { computed, ref, watch } from "vue";
import { setMenuItems, useSidebarStore } from "@/store/sidebar";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { useRoute } from "vue-router";
import { useSharedStore } from "@/store/sharedata";
const title = ref('')
const logo = ref('')
const title = ref("");
const logo = ref("");
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {
title.value = res.data.admin_title
logo.value = res.data.logo
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
const store = useSharedStore()
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
theme.value = val
})
httpGet("/api/admin/config/get?key=system")
.then((res) => {
title.value = res.data.admin_title;
logo.value = res.data.logo;
})
.catch((e) => {
ElMessage.error("加载系统配置失败: " + e.message);
});
const store = useSharedStore();
const theme = ref(store.theme);
watch(
() => store.theme,
(val) => {
theme.value = val;
}
);
const items = [
{
icon: 'home',
index: '/admin/dashboard',
title: '仪表盘',
icon: "home",
index: "/admin/dashboard",
title: "仪表盘",
},
{
icon: 'user-fill',
index: '/admin/user',
title: '用户管理',
icon: "user-fill",
index: "/admin/user",
title: "用户管理",
},
{
icon: 'menu',
index: '1',
title: '应用管理',
icon: "menu",
index: "1",
title: "应用管理",
subs: [
{
index: '/admin/app',
title: '应用列表',
index: "/admin/app",
title: "应用列表",
},
{
index: '/admin/app/type',
title: '应用分类',
index: "/admin/app/type",
title: "应用分类",
},
],
},
{
icon: 'api-key',
index: '/admin/apikey',
title: 'API-KEY',
icon: "api-key",
index: "/admin/apikey",
title: "API-KEY",
},
{
icon: 'model',
index: '/admin/chat/model',
title: '语言模型',
icon: "model",
index: "/admin/chat/model",
title: "模型管理",
},
{
icon: 'recharge',
index: '/admin/product',
title: '充值产品',
icon: "recharge",
index: "/admin/product",
title: "充值产品",
},
{
icon: 'order',
index: '/admin/order',
title: '充值订单',
icon: "order",
index: "/admin/order",
title: "充值订单",
},
{
icon: 'reward',
index: '/admin/redeem',
title: '兑换码',
icon: "reward",
index: "/admin/redeem",
title: "兑换码",
},
{
icon: 'control',
index: '/admin/functions',
title: '函数管理',
icon: "control",
index: "/admin/functions",
title: "函数管理",
},
{
icon: 'prompt',
index: '/admin/chats',
title: '对话管理',
icon: "prompt",
index: "/admin/chats",
title: "对话管理",
},
{
icon: 'image',
index: '/admin/images',
title: '绘图管理',
icon: "image",
index: "/admin/images",
title: "绘图管理",
},
{
icon: 'mp3',
index: '/admin/medias',
title: '音视频管理',
icon: "mp3",
index: "/admin/medias",
title: "音视频管理",
},
{
icon: 'role',
index: '/admin/manger',
title: '管理员',
icon: "role",
index: "/admin/manger",
title: "管理员",
},
{
icon: 'config',
index: '/admin/system',
title: '系统设置',
icon: "config",
index: "/admin/system",
title: "系统设置",
},
{
icon: 'log',
index: '/admin/powerLog',
title: '用户算力日志',
icon: "log",
index: "/admin/powerLog",
title: "用户算力日志",
},
{
icon: 'log',
index: '/admin/loginLog',
title: '用户登录日志',
icon: "log",
index: "/admin/loginLog",
title: "用户登录日志",
},
// {
// icon: 'menu',
@@ -198,7 +199,7 @@ const onRoutes = computed(() => {
});
const sidebar = useSidebarStore();
setMenuItems(items)
setMenuItems(items);
</script>
<style scoped lang="stylus">
@@ -212,20 +213,17 @@ setMenuItems(items)
.logo {
display flex
width 219px
background-color #324157
padding 6px 15px;
cursor pointer
background-color: #324157
.el-image {
width 36px;
img {
height 36px;
padding-top 5px;
border-radius 100%
.el-image__inner {
height 40px
}
background #fff
border 2px solid #754ff6
padding 2px
}
.text {
@@ -292,5 +290,4 @@ setMenuItems(items)
border-color var(--el-border-color)
}
}
</style>

View File

@@ -42,8 +42,8 @@ import {useSharedStore} from "@/store/sharedata";
import {ref, watch} from "vue";
const store = useSharedStore()
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
const theme = ref(store.theme)
watch(() => store.theme, (val) => {
theme.value = val
})
const router = useRouter();

View File

@@ -1,7 +1,7 @@
<template>
<div class="mobile-message-mj">
<div class="chat-icon">
<van-image :src="icon"/>
<van-image :src="icon" />
</div>
<div class="chat-item">
@@ -11,21 +11,30 @@
<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">
<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>
<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/>
<Picture />
</el-icon>
</div>
</template>
@@ -33,7 +42,7 @@
</div>
</div>
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
<div class="opt" v-if="data.showOpt && data.image?.hash !== ''">
<div class="opt-line">
<ul>
<li><a @click="upscale(1)">U1</a></li>
@@ -54,17 +63,16 @@
</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";
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,
@@ -74,36 +82,40 @@ const props = defineProps({
createdAt: String
});
const data = ref(props.content)
const cacheKey = "img_placeholder_height"
const data = ref(props.content);
const cacheKey = "img_placeholder_height";
const item = localStorage.getItem(cacheKey);
const loading = ref(false)
const height = ref(0)
const loading = ref(false);
const height = ref(0);
if (item) {
height.value = parseInt(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)
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']);
watch(
() => props.content,
(newVal) => {
data.value = newVal;
}
);
const emits = defineEmits(["disable-input", "disable-input"]);
const upscale = (index) => {
send('/api/mj/upscale', index)
}
send("/api/mj/upscale", index);
};
const variation = (index) => {
send('/api/mj/variation', index)
}
send("/api/mj/variation", index);
};
const send = (url, index) => {
loading.value = true
emits('disable-input')
loading.value = true;
emits("disable-input");
httpPost(url, {
index: index,
src: "chat",
@@ -114,15 +126,20 @@ const send = (url, index) => {
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')
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">
@@ -268,4 +285,4 @@ const send = (url, index) => {
}
}
</style>
</style>

View File

@@ -1,29 +1,25 @@
<template>
<div class="black-dialog">
<el-dialog
v-model="showDialog"
style="--el-dialog-bg-color:#414141;
--el-text-color-primary:#f1f1f1;
--el-border-color:#414141;
--el-color-primary:#21aa93;
--el-color-primary-dark-2:#41555d;
--el-color-white: #e1e1e1;
--el-color-primary-light-3:#549688;
--el-fill-color-blank:#616161;
--el-color-primary-light-7:#717171;
--el-color-primary-light-9:#717171;
--el-text-color-regular:#e1e1e1"
:title="title"
:width="width"
:before-close="cancel"
v-model="showDialog"
:title="title"
:width="width"
:before-close="cancel"
>
<div class="dialog-body">
<slot></slot>
</div>
<template #footer v-if="!hideFooter">
<div class="dialog-footer">
<el-button @click="cancel">{{cancelText}}</el-button>
<el-button type="primary" @click="$emit('confirm')" v-if="!hideConfirm">{{confirmText}}</el-button>
<el-button @click="cancel" style="--el-border-radius-base: 8px">{{
cancelText
}}</el-button>
<el-button
type="primary"
@click="$emit('confirm')"
v-if="!hideConfirm"
>{{ confirmText }}</el-button
>
</div>
</template>
</el-dialog>
@@ -31,45 +27,47 @@
</template>
<script setup>
import {ref, watch} from "vue";
import { ref, watch } from "vue";
const props = defineProps({
show : Boolean,
show: Boolean,
title: {
type: String,
default: 'Tips',
default: "Tips"
},
width: {
type: String,
default: 'auto',
default: "auto"
},
hideFooter:{
hideFooter: {
type: Boolean,
default: false
},
hideConfirm:{
hideConfirm: {
type: Boolean,
default: false
},
confirmText: {
type: String,
default: '确定',
default: "确定"
},
cancelText: {
type: String,
default: '取消',
},
default: "取消"
}
});
const emits = defineEmits(['confirm','cancal']);
const showDialog = ref(props.show)
const emits = defineEmits(["confirm", "cancal"]);
const showDialog = ref(props.show);
watch(() => props.show, (newValue) => {
showDialog.value = newValue
})
watch(
() => props.show,
(newValue) => {
showDialog.value = newValue;
}
);
const cancel = () => {
showDialog.value = false
emits('cancal')
}
showDialog.value = false;
emits("cancal");
};
</script>
<style lang="stylus">
@@ -110,5 +108,4 @@ const cancel = () => {
}
}
}
</style>

View File

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

View File

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

View File

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

View File

@@ -5,63 +5,76 @@
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import {createApp} from 'vue'
import ElementPlus from "element-plus"
import "element-plus/dist/index.css"
import '@/assets/iconfont/iconfont.css';
import 'vant/lib/index.css';
import App from './App.vue'
import {createPinia} from "pinia";
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "@/assets/iconfont/iconfont.css";
import "vant/lib/index.css";
import App from "./App.vue";
import { useThemeStore } from "@/store/theme";
import { createPinia } from "pinia";
import "animate.css/animate.min.css";
import "@/assets/css/tailwind.css";
import {
ActionSheet,
Badge,
Button,
Cell,
CellGroup,
Circle,
Col,
Collapse,
CollapseItem,
ConfigProvider,
Dialog,
Divider,
DropdownItem,
DropdownMenu,
Empty,
Field,
Form,
Grid,
GridItem,
Icon,
Image,
ImagePreview,
Lazyload,
List,
Loading,
NavBar,
NoticeBar,
Notify,
Overlay,
Picker,
Popup,
Row,
Search,
ShareSheet,
Slider,
Sticky,
SwipeCell,
Switch,
Tab,
Tabbar,
TabbarItem,
Tabs,
Tag,
TextEllipsis,
Uploader
ActionSheet,
Badge,
Button,
Cell,
CellGroup,
Circle,
Col,
Collapse,
CollapseItem,
ConfigProvider,
Dialog,
Divider,
DropdownItem,
DropdownMenu,
Empty,
Field,
Form,
Grid,
GridItem,
Icon,
Image,
ImagePreview,
Lazyload,
List,
Loading,
NavBar,
NoticeBar,
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 { router } from "@/router";
import "v3-waterfall/dist/style.css";
import V3waterfall from "v3-waterfall";
import "@/assets/css/theme-dark.styl";
import "@/assets/css/theme-light.styl";
import "@/assets/css/common.styl";
const pinia = createPinia();
const themeStore = useThemeStore(pinia); // 使用 theme store
// 设置初始主题
document.documentElement.setAttribute("data-theme", themeStore.theme);
const app = createApp(App);
app.use(createPinia());
@@ -70,12 +83,12 @@ 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(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);
@@ -91,12 +104,12 @@ 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(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);
@@ -111,6 +124,4 @@ app.use(Tabs);
app.use(Divider);
app.use(NoticeBar);
app.use(ActionSheet);
app.use(router).use(ElementPlus).mount('#app')
app.use(router).use(ElementPlus).mount("#app");

View File

@@ -8,349 +8,348 @@
import {createRouter, createWebHistory} from "vue-router";
const routes = [
{
name: 'Index',
path: '/',
meta: {title: "首页"},
component: () => import('@/views/Index.vue'),
},
{
name: 'home',
path: '/home',
redirect: '/chat',
component: () => import('@/views/Home.vue'),
children: [
{
name: 'chat',
path: '/chat',
meta: {title: '创作中心'},
component: () => import('@/views/ChatPlus.vue'),
},
{
name: 'chat-id',
path: '/chat/:id',
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-app',
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: 'powerLog',
path: '/powerLog',
meta: {title: '消费日志'},
component: () => import('@/views/PowerLog.vue'),
},
{
name: 'xmind',
path: '/xmind',
meta: {title: '思维导图'},
component: () => import('@/views/MarkMap.vue'),
},
{
name: 'dalle',
path: '/dalle',
meta: {title: 'DALLE-3'},
component: () => import('@/views/Dalle.vue'),
},
{
name: 'suno',
path: '/suno',
meta: {title: 'Suno音乐创作'},
component: () => import('@/views/Suno.vue'),
},
{
name: 'ExternalLink',
path: '/external',
component: () => import('@/views/ExternalPage.vue'),
},
{
name: 'song',
path: '/song/:id',
meta: {title: 'Suno音乐播放'},
component: () => import('@/views/Song.vue'),
},
{
name: 'luma',
path: '/luma',
meta: {title: 'Luma视频创作'},
component: () => import('@/views/Luma.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: 'login-callback',
path: '/login/callback',
meta: {title: '用户登录'},
component: () => import('@/views/LoginCallback.vue'),
},
{
name: 'register',
path: '/register',
{
name: "Index",
path: "/",
meta: { title: "首页" },
component: () => import("@/views/Index.vue"),
},
{
name: "home",
path: "/home",
redirect: "/chat",
component: () => import("@/views/Home.vue"),
children: [
{
name: "chat",
path: "/chat",
meta: { title: "创作中心" },
component: () => import("@/views/ChatPlus.vue"),
},
{
name: "chat-id",
path: "/chat/:id",
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-app",
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: "powerLog",
path: "/powerLog",
meta: { title: "消费日志" },
component: () => import("@/views/PowerLog.vue"),
},
{
name: "xmind",
path: "/xmind",
meta: { title: "思维导图" },
component: () => import("@/views/MarkMap.vue"),
},
{
name: "dalle",
path: "/dalle",
meta: { title: "DALLE-3" },
component: () => import("@/views/Dalle.vue"),
},
{
name: "suno",
path: "/suno",
meta: { title: "Suno音乐创作" },
component: () => import("@/views/Suno.vue"),
},
{
name: "ExternalLink",
path: "/external",
component: () => import("@/views/ExternalPage.vue"),
},
{
name: "song",
path: "/song/:id",
meta: { title: "Suno音乐播放" },
component: () => import("@/views/Song.vue"),
},
{
name: "luma",
path: "/luma",
meta: { title: "Luma视频创作" },
component: () => import("@/views/Luma.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"),
},
meta: {title: '用户注册'},
component: () => import('@/views/Register.vue'),
},
{
path: '/admin/login',
name: 'admin-login',
meta: {title: '控制台登录'},
component: () => import('@/views/admin/Login.vue'),
},
{
path: '/payReturn',
name: 'pay-return',
meta: {title: '支付回调'},
component: () => import('@/views/PayReturn.vue'),
},
{
name: 'admin',
path: '/admin',
redirect: '/admin/dashboard',
component: () => import("@/views/admin/Home.vue"),
meta: {title: 'Geek-AI 控制台'},
children: [
{
path: '/admin/dashboard',
name: 'admin-dashboard',
meta: {title: '仪表盘'},
component: () => import('@/views/admin/Dashboard.vue'),
},
{
path: '/admin/system',
name: 'admin-system',
meta: {title: '系统设置'},
component: () => import('@/views/admin/SysConfig.vue'),
},
{
path: '/admin/user',
name: 'admin-user',
meta: {title: '用户管理'},
component: () => import('@/views/admin/Users.vue'),
},
{
path: '/admin/app',
name: 'admin-app',
meta: {title: '应用列表'},
component: () => import('@/views/admin/Apps.vue'),
},
{
path: '/admin/app/type',
name: 'admin-app-type',
meta: {title: '应用分类'},
component: () => import('@/views/admin/AppType.vue'),
},
{
path: '/admin/apikey',
name: 'admin-apikey',
meta: {title: 'API-KEY 管理'},
component: () => import('@/views/admin/ApiKey.vue'),
},
{
path: '/admin/chat/model',
name: 'admin-chat-model',
meta: {title: '语言模型'},
component: () => import('@/views/admin/ChatModel.vue'),
},
{
path: '/admin/product',
name: 'admin-product',
meta: {title: '充值产品'},
component: () => import('@/views/admin/Product.vue'),
},
{
path: '/admin/order',
name: 'admin-order',
meta: {title: '充值订单'},
component: () => import('@/views/admin/Order.vue'),
},
{
path: '/admin/redeem',
name: 'admin-redeem',
meta: {title: '兑换码管理'},
component: () => import('@/views/admin/Redeem.vue'),
},
{
path: '/admin/loginLog',
name: 'admin-loginLog',
meta: {title: '登录日志'},
component: () => import('@/views/admin/LoginLog.vue'),
},
{
path: '/admin/functions',
name: 'admin-functions',
meta: {title: '函数管理'},
component: () => import('@/views/admin/Functions.vue'),
},
{
path: '/admin/chats',
name: 'admin-chats',
meta: {title: '对话管理'},
component: () => import('@/views/admin/ChatList.vue'),
},
{
path: '/admin/images',
name: 'admin-images',
meta: {title: '绘图管理'},
component: () => import('@/views/admin/ImageList.vue'),
},
{
path: '/admin/medias',
name: 'admin-medias',
meta: {title: '音视频管理'},
component: () => import('@/views/admin/Medias.vue'),
},
{
path: '/admin/powerLog',
name: 'admin-power-log',
meta: {title: '算力日志'},
component: () => import('@/views/admin/PowerLog.vue'),
},
{
path: '/admin/manger',
name: 'admin-manger',
meta: {title: '管理员'},
component: () => import('@/views/admin/Manager.vue'),
}
]
},
{
name: "login-callback",
path: "/login/callback",
meta: { title: "用户登录" },
component: () => import("@/views/LoginCallback.vue"),
},
{
name: "register",
path: "/register",
meta: { title: "用户注册" },
component: () => import("@/views/Register.vue"),
},
{
name: "resetpassword",
path: "/resetpassword",
meta: { title: "重置密码" },
component: () => import("@/views/Resetpassword.vue"),
},
{
path: "/admin/login",
name: "admin-login",
meta: { title: "控制台登录" },
component: () => import("@/views/admin/Login.vue"),
},
{
path: "/payReturn",
name: "pay-return",
meta: { title: "支付回调" },
component: () => import("@/views/PayReturn.vue"),
},
{
name: "admin",
path: "/admin",
redirect: "/admin/dashboard",
component: () => import("@/views/admin/Home.vue"),
meta: { title: "Geek-AI 控制台" },
children: [
{
path: "/admin/dashboard",
name: "admin-dashboard",
meta: { title: "仪表盘" },
component: () => import("@/views/admin/Dashboard.vue"),
},
{
path: "/admin/system",
name: "admin-system",
meta: { title: "系统设置" },
component: () => import("@/views/admin/SysConfig.vue"),
},
{
path: "/admin/user",
name: "admin-user",
meta: { title: "用户管理" },
component: () => import("@/views/admin/Users.vue"),
},
{
path: "/admin/app",
name: "admin-app",
meta: { title: "应用列表" },
component: () => import("@/views/admin/Apps.vue"),
},
{
path: "/admin/app/type",
name: "admin-app-type",
meta: { title: "应用分类" },
component: () => import("@/views/admin/AppType.vue"),
},
{
path: "/admin/apikey",
name: "admin-apikey",
meta: { title: "API-KEY 管理" },
component: () => import("@/views/admin/ApiKey.vue"),
},
{
path: "/admin/chat/model",
name: "admin-chat-model",
meta: { title: "语言模型" },
component: () => import("@/views/admin/ChatModel.vue"),
},
{
path: "/admin/product",
name: "admin-product",
meta: { title: "充值产品" },
component: () => import("@/views/admin/Product.vue"),
},
{
path: "/admin/order",
name: "admin-order",
meta: { title: "充值订单" },
component: () => import("@/views/admin/Order.vue"),
},
{
path: "/admin/redeem",
name: "admin-redeem",
meta: { title: "兑换码管理" },
component: () => import("@/views/admin/Redeem.vue"),
},
{
path: "/admin/loginLog",
name: "admin-loginLog",
meta: { title: "登录日志" },
component: () => import("@/views/admin/LoginLog.vue"),
},
{
path: "/admin/functions",
name: "admin-functions",
meta: { title: "函数管理" },
component: () => import("@/views/admin/Functions.vue"),
},
{
path: "/admin/chats",
name: "admin-chats",
meta: { title: "对话管理" },
component: () => import("@/views/admin/ChatList.vue"),
},
{
path: "/admin/images",
name: "admin-images",
meta: { title: "绘图管理" },
component: () => import("@/views/admin/ImageList.vue"),
},
{
path: "/admin/medias",
name: "admin-medias",
meta: { title: "音视频管理" },
component: () => import("@/views/admin/Medias.vue"),
},
{
path: "/admin/powerLog",
name: "admin-power-log",
meta: { title: "算力日志" },
component: () => import("@/views/admin/PowerLog.vue"),
},
{
path: "/admin/manger",
name: "admin-manger",
meta: { title: "管理员" },
component: () => import("@/views/admin/Manager.vue"),
},
],
},
{
name: 'mobile-login',
path: '/mobile/login',
meta: {title: '用户登录'},
component: () => import('@/views/Login.vue'),
},
{
name: 'mobile-register',
path: '/mobile/register',
meta: {title: '用户注册'},
component: () => import('@/views/Register.vue'),
},
{
name: 'mobile',
path: '/mobile',
meta: {title: '首页'},
component: () => import('@/views/mobile/Home.vue'),
redirect: '/mobile/index',
children: [
{
path: '/mobile/index',
name: 'mobile-index',
component: () => import('@/views/mobile/Index.vue'),
},
{
path: '/mobile/chat',
name: 'mobile-chat',
component: () => import('@/views/mobile/ChatList.vue'),
},
{
path: '/mobile/image',
name: 'mobile-image',
component: () => import('@/views/mobile/Image.vue'),
},
{
path: '/mobile/profile',
name: 'mobile-profile',
component: () => import('@/views/mobile/Profile.vue'),
},
{
path: '/mobile/imgWall',
name: 'mobile-img-wall',
component: () => import('@/views/mobile/pages/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: "mobile-login",
path: "/mobile/login",
meta: { title: "用户登录" },
component: () => import("@/views/mobile/Login.vue"),
},
{
name: "mobile",
path: "/mobile",
meta: { title: "首页" },
component: () => import("@/views/mobile/Home.vue"),
redirect: "/mobile/index",
children: [
{
path: "/mobile/index",
name: "mobile-index",
component: () => import("@/views/mobile/Index.vue"),
},
{
path: "/mobile/chat",
name: "mobile-chat",
component: () => import("@/views/mobile/ChatList.vue"),
},
{
path: "/mobile/image",
name: "mobile-image",
component: () => import("@/views/mobile/Image.vue"),
},
{
path: "/mobile/profile",
name: "mobile-profile",
component: () => import("@/views/mobile/Profile.vue"),
},
{
path: "/mobile/imgWall",
name: "mobile-img-wall",
component: () => import("@/views/mobile/pages/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: 'test2',
path: '/test2',
meta: {title: '测试页面'},
component: () => import('@/views/RealtimeTest.vue'),
},
{
name: 'NotFound',
path: '/:all(.*)',
meta: {title: '页面没有找到'},
component: () => import('@/views/404.vue'),
},
]
{
name: "test",
path: "/test",
meta: { title: "测试页面" },
component: () => import("@/views/Test.vue"),
},
{
name: "test2",
path: "/test2",
meta: { title: "测试页面" },
component: () => import("@/views/RealtimeTest.vue"),
},
{
name: "NotFound",
path: "/:all(.*)",
meta: { title: "页面没有找到" },
component: () => import("@/views/404.vue"),
},
];
// console.log(MY_VARIABLE)
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
history: createWebHistory(),
routes: routes,
});
let prevRoute = null
let prevRoute = null;
// dynamic change the title when router change
router.beforeEach((to, from, next) => {
document.title = to.meta.title
prevRoute = from
next()
})
document.title = to.meta.title;
prevRoute = from;
next();
});
export {router, prevRoute};
export { router, prevRoute };

View File

@@ -1,77 +1,73 @@
import {defineStore} from 'pinia';
import Storage from 'good-storage'
import { defineStore } from "pinia";
import Storage from "good-storage";
export const useSharedStore = defineStore('shared', {
state: () => ({
showLoginDialog: false,
chatListStyle: Storage.get("chat_list_style","chat"),
chatStream: Storage.get("chat_stream",true),
socket: {conn:null, handlers:{}},
mobileTheme: Storage.get("mobile_theme", "light"),
adminTheme: Storage.get("admin_theme", "light"),
isLogin: false
}),
getters: {},
actions: {
setShowLoginDialog(value) {
this.showLoginDialog = value;
},
setChatListStyle(value) {
this.chatListStyle = value;
Storage.set("chat_list_style", value);
},
setChatStream(value) {
this.chatStream = value;
Storage.set("chat_stream", value);
},
setSocket(value) {
for (const key in this.socket.handlers) {
this.setMessageHandler(value, this.socket.handlers[key])
}
this.socket.conn = value
},
addMessageHandler(key, callback) {
if (!this.socket.handlers[key]) {
this.socket.handlers[key] = callback;
}
this.setMessageHandler(this.socket.conn, callback)
},
setMessageHandler(conn, callback) {
if (!conn) {
return
}
conn.addEventListener('message', (event) => {
try {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
callback(JSON.parse(String(reader.result)))
}
}
} catch (e) {
console.warn(e)
}
})
},
removeMessageHandler(key) {
if (this.socket.conn && this.socket.conn.readyState === WebSocket.OPEN) {
this.socket.conn.removeEventListener('message', this.socket.handlers[key])
}
delete this.socket.handlers[key]
},
setMobileTheme(theme) {
this.mobileTheme = theme
Storage.set("mobile_theme", theme)
},
setAdminTheme(theme) {
this.adminTheme = theme
Storage.set("admin_theme", theme)
},
setIsLogin(value) {
this.isLogin = value
}
export const useSharedStore = defineStore("shared", {
state: () => ({
showLoginDialog: false,
chatListStyle: Storage.get("chat_list_style", "chat"),
chatStream: Storage.get("chat_stream", true),
socket: { conn: null, handlers: {} },
theme: Storage.get("theme", "light"),
isLogin: false,
}),
getters: {},
actions: {
setShowLoginDialog(value) {
this.showLoginDialog = value;
},
setChatListStyle(value) {
this.chatListStyle = value;
Storage.set("chat_list_style", value);
},
setChatStream(value) {
this.chatStream = value;
Storage.set("chat_stream", value);
},
setSocket(value) {
for (const key in this.socket.handlers) {
this.setMessageHandler(value, this.socket.handlers[key]);
}
this.socket.conn = value;
},
addMessageHandler(key, callback) {
if (!this.socket.handlers[key]) {
this.socket.handlers[key] = callback;
}
this.setMessageHandler(this.socket.conn, callback);
},
setMessageHandler(conn, callback) {
if (!conn) {
return;
}
conn.addEventListener("message", (event) => {
try {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
callback(JSON.parse(String(reader.result)));
};
}
} catch (e) {
console.warn(e);
}
});
},
removeMessageHandler(key) {
if (this.socket.conn && this.socket.conn.readyState === WebSocket.OPEN) {
this.socket.conn.removeEventListener("message", this.socket.handlers[key]);
}
delete this.socket.handlers[key];
},
setTheme(theme) {
this.theme = theme;
document.documentElement.setAttribute("data-theme", theme); // 设置 HTML 的 data-theme 属性
Storage.set("theme", theme);
},
setIsLogin(value) {
this.isLogin = value;
},
},
});

15
web/src/store/theme.js Normal file
View File

@@ -0,0 +1,15 @@
// src/store/index.js
import { defineStore } from "pinia";
export const useThemeStore = defineStore("theme", {
state: () => ({
theme: localStorage.getItem("theme") || "light" // 默认从 localStorage 获取主题
}),
actions: {
setTheme(theme) {
this.theme = theme;
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme); // 保存到 localStorage
}
}
});

View File

@@ -1,43 +1,51 @@
/**
* Util lib functions
*/
import {showConfirmDialog, showFailToast, showSuccessToast, showToast} from "vant";
import {closeToast, showConfirmDialog, showFailToast, showLoadingToast, showSuccessToast, showToast} from "vant";
import {isMobile} from "@/utils/libs";
import {ElMessage} from "element-plus";
export function showLoginDialog(router) {
showConfirmDialog({
title: '登录',
message:
'此操作需要登录才能进行,前往登录?',
}).then(() => {
router.push("/login")
}).catch(() => {
showConfirmDialog({
title: "登录",
message: "此操作需要登录才能进行,前往登录?",
})
.then(() => {
router.push("/login");
})
.catch(() => {
// on cancel
});
}
export function showMessageOK(message) {
if (isMobile()) {
showSuccessToast(message)
} else {
ElMessage.success(message)
}
if (isMobile()) {
showSuccessToast(message);
} else {
ElMessage.success(message);
}
}
export function showMessageInfo(message) {
if (isMobile()) {
showToast(message)
} else {
ElMessage.info(message)
}
if (isMobile()) {
showToast(message);
} else {
ElMessage.info(message);
}
}
export function showMessageError(message) {
if (isMobile()) {
showFailToast(message)
} else {
ElMessage.error(message)
}
if (isMobile()) {
showFailToast({message: message, duration: 0});
} else {
ElMessage.error(message);
}
}
export function showLoading(message = "正在处理...") {
showLoadingToast({message: message, forbidClick: true, duration: 0});
}
export function closeLoading() {
closeToast();
}

View File

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

View File

@@ -1,87 +1,90 @@
<template>
<div class="chat-export" v-loading="loading">
<div class="chat-box" id="chat-box">
<div class="title">
<div class="title pt-4">
<h2>{{ chatTitle }}</h2>
</div>
<div v-for="item in chatData" :key="item.id">
<chat-prompt v-if="item.type==='prompt'" :data="item" list-style="list"/>
<chat-reply v-else-if="item.type==='reply'" :data="item" :read-only="true" list-style="list"/>
<chat-prompt v-if="item.type === 'prompt'" :data="item" list-style="list" />
<chat-reply v-else-if="item.type === 'reply'" :data="item" :read-only="true" list-style="list" />
</div>
</div><!-- end chat box -->
</div>
<!-- end chat box -->
</div>
</template>
<script setup>
import ChatReply from "@/components/ChatReply.vue";
import ChatPrompt from "@/components/ChatPrompt.vue";
import {nextTick, onMounted, ref} from "vue";
import {useRouter} from "vue-router";
import {httpGet} from "@/utils/http";
import 'highlight.js/styles/a11y-dark.css'
import { nextTick, onMounted, 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 { ElMessage } from "element-plus";
import Clipboard from "clipboard";
const chatData = ref([])
const router = useRouter()
const chatId = router.currentRoute.value.query['chat_id']
const loading = ref(true)
const chatTitle = ref('')
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;
httpGet("/api/chat/history?chat_id=" + chatId)
.then((res) => {
const data = res.data;
if (!data) {
loading.value = false;
return;
}
data[i].orgContent = data[i].content;
data[i].content = md.render(data[i].content);
chatData.value.push(data[i]);
}
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 = data[i].content?.content;
chatData.value.push(data[i]);
continue;
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
data[i].orgContent = data[i].content;
data[i].content = 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;
})
loading.value = false
}).catch(e => {
ElMessage.error('加载聊天记录失败:' + e.message);
})
.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)
})
httpGet("/api/chat/detail?chat_id=" + chatId)
.then((res) => {
chatTitle.value = res.data.title;
})
.catch((e) => {
ElMessage.error("加载会失败: " + e.message);
});
onMounted(() => {
const clipboard = new Clipboard('.copy-reply');
clipboard.on('success', () => {
ElMessage.success('复制成功!');
})
const clipboard = new Clipboard(".copy-reply");
clipboard.on("success", () => {
ElMessage.success("复制成功!");
});
clipboard.on('error', () => {
ElMessage.error('复制失败!');
})
})
clipboard.on("error", () => {
ElMessage.error("复制失败!");
});
});
</script>
<style lang="stylus">
.chat-export {
@@ -113,4 +116,4 @@ onMounted(() => {
}
}
}
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,32 +1,37 @@
<template>
<div class="page-iframe" v-loading="loading"
style="--el-color-primary:#47fff1"
element-loading-text="页面正在加载中..."
element-loading-background="rgba(122, 122, 122, 0.8)">
<div
class="page-iframe"
v-loading="loading"
style="--el-color-primary: #47fff1"
element-loading-text="页面正在加载中..."
element-loading-background="rgba(122, 122, 122, 0.8)"
>
<iframe :src="externalUrl" class="iframe" @load="onIframeLoad"></iframe>
</div>
</template>
<script setup>
import {useRouter} from "vue-router";
import {computed, ref} from "vue";
import { useRouter } from "vue-router";
import { computed, ref, onMounted } from "vue";
const loading = ref(true)
const router = useRouter()
const loading = ref(true);
const router = useRouter();
const externalUrl = computed(() => {
loading.value = true
return router.currentRoute.value.query.url || 'about:blank'
})
loading.value = true;
return router.currentRoute.value.query.url || "about:blank";
});
// 设置标题
document.title = router.currentRoute.value.query.title;
const onIframeLoad = () => {
loading.value = false
}
loading.value = false;
};
</script>
<style scoped lang="stylus">
.page-iframe {
width 100%
height 100%
height 100vh
overflow hidden
.iframe {

View File

@@ -1,243 +1,296 @@
<template>
<div class="home">
<div class="header">
<div class="banner">
<div class="logo">
<el-image :src="logo" @click="router.push('/')"/>
<div class="layout">
<div class="tab-box">
<div class="flex-center-col big-top-title xxx">
<div class="flex-center-col" @click="isCollapse = !isCollapse">
<el-tooltip content="展开菜单" placement="right" v-if="isCollapse">
<i class="iconfont icon-expand"></i>
</el-tooltip>
</div>
<div class="title">
<span>{{ title }}</span>
</div>
</div>
<div class="navbar">
<el-tooltip
v-if="!license.de_copy"
class="box-item"
effect="light"
content="部署文档"
placement="bottom">
<a :href="docsURL" class="link-button" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip
v-if="!license.de_copy"
class="box-item"
effect="light"
content="项目源码"
placement="bottom">
<a href="https://github.com/yangjian102621/chatgpt-plus" class="link-button" target="_blank">
<i class="iconfont icon-github"></i>
</a>
</el-tooltip>
<el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="loginUser.id">
<span class="el-dropdown-link">
<el-image :src="loginUser.avatar"/>
</span>
<template #dropdown>
<el-dropdown-menu class="user-info-menu">
<el-dropdown-item @click="showConfigDialog = true">
<el-icon>
<UserFilled/>
</el-icon>
<span class="username">{{ loginUser.nickname }}</span>
</el-dropdown-item>
<div v-if="!license.de_copy">
<el-dropdown-item>
<i class="iconfont icon-book"></i>
<a :href="docsURL" target="_blank">
用户手册
</a>
</el-dropdown-item>
<el-dropdown-item>
<i class="iconfont icon-github"></i>
<a :href="gitURL" target="_blank">
GeekAI {{ version }}
</a>
</el-dropdown-item>
</div>
<el-divider style="margin: 2px 0"/>
<el-dropdown-item @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else>
<el-button size="small" color="#21aa93" @click="store.setShowLoginDialog(true)" round>登录</el-button>
</div>
</div>
</div>
<div class="main">
<div class="navigator">
<ul class="nav-items">
<li v-for="item in mainNavs" :key="item.url">
<el-tooltip
effect="light"
:content="item.name"
placement="right">
<a @click="changeNav(item)" :class="item.url === curPath ? 'active' : ''">
<el-image :src="item.icon" style="width: 30px;height: 30px"/>
</a>
<div class="flex" :class="{ 'top-collapse': !isCollapse }">
<div class="top-avatar flex">
<span class="title" v-if="!isCollapse">{{ title }}</span>
<el-tooltip :content="title" placement="right">
<img :src="logo" alt="" :class="{ marr: !isCollapse }" v-if="isCollapse" />
</el-tooltip>
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</div>
<div class="menuIcon xxx" @click="isCollapse = !isCollapse">
<el-tooltip content="关闭菜单" placement="right" v-if="!isCollapse">
<i class="iconfont icon-colspan"></i>
</el-tooltip>
</div>
</div>
</div>
<div class="menu-list" :style="{ width: isCollapse ? '65px' : '170px' }" :class="{ 'menu-list-collapse': !isCollapse }">
<ul>
<li
class="menu-list-item flex-center-col"
v-for="item in mainNavs"
:key="item.url"
@click="changeNav(item)"
:class="item.url === curPath ? 'active' : ''"
>
<span v-if="item.icon.startsWith('icon')" :class="{ 'mr-1 ml-2': !isCollapse }">
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" class="el-icon ml-1" v-else />
<div class="menu-title" :class="{ 'menu-title-collapse': !isCollapse }">
{{ item.name }}
</div>
</li>
<el-popover
v-if="moreNavs.length > 0"
placement="right-end"
trigger="hover"
>
<template #reference>
<li>
<a class="active">
<el-image src="/images/menu/more.png" style="width: 30px;height: 30px"/>
</a>
</li>
</template>
<template #default>
<ul class="more-menus">
<li v-for="item in moreNavs" :key="item.url" :class="item.url === curPath ? 'active' : ''">
<a @click="changeNav(item)">
<el-image :src="item.icon" style="width: 20px;height: 20px"/>
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</a>
<!-- 更多 -->
<div class="bot" :style="{ width: isCollapse ? '65px' : '170px' }">
<div class="bot-line"></div>
<el-popover v-if="moreNavs.length > 0" placement="right-end" trigger="hover">
<template #reference>
<li class="menu-list-item flex-center-col">
<el-icon><CirclePlus /></el-icon>
<div class="menu-title">更多</div>
</li>
</ul>
</template>
</el-popover>
</template>
<template #default>
<ul class="more-menus">
<li
v-for="(item, index) in moreNavs"
:key="item.url"
:class="{
active: item.url === curPath,
moreTitle: index !== 3 && index !== 4,
twoTittle: index === 3 || index === 4,
}"
>
<a @click="changeNav(item)">
<span v-if="item.icon.startsWith('icon')" class="mr-2">
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" style="width: 20px; height: 20px" v-else />
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</a>
</li>
</ul>
</template>
</el-popover>
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
<template #reference>
<li class="menu-list-item flex-center-col">
<el-icon><Setting /></el-icon>
<div v-if="!isCollapse">设置</div>
</li>
</template>
<template #default>
<ul class="more-menus setting-menus">
<li>
<div @click="showConfigDialog = true" class="flex">
<el-icon>
<UserFilled />
</el-icon>
<span class="username title">{{ loginUser.nickname }}</span>
</div>
</li>
<li>
<a @click="logout" class="flex">
<i class="iconfont icon-logout"></i>
<span class="title">退出登录</span>
</a>
</li>
</ul>
</template>
</el-popover>
<li class="menu-bot-item">
<a :href="gitURL" class="link-button" target="_blank" v-if="!license.de_copy && !isCollapse">
<i class="iconfont icon-github"></i>
</a>
<a @click="router.push('/')" class="link-button">
<i class="iconfont icon-house"></i>
</a>
<ThemeChange />
</li>
</div>
</ul>
</div>
<div class="content custom-scroll" :style="{height: mainWinHeight+'px'}">
</div>
<el-scrollbar class="right-main">
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button>
</div>
<div class="content custom-scroll">
<router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
</div>
</div>
<!-- </div> -->
</el-scrollbar>
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false" />
<login-dialog :show="show" @hide="store.setShowLoginDialog(false)" @success="loginCallback"/>
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false"/>
<el-dialog v-model="showLoginDialog" width="500px" @close="store.setShowLoginDialog(false)">
<template #header>
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">登录后解锁功能</div>
</template>
<div class="p-4 pt-2 pb-2">
<LoginDialog @success="loginSuccess" @hide="store.setShowLoginDialog(false)" />
</div>
</el-dialog>
</div>
</template>
<script setup>
import {useRouter} from "vue-router";
import {onMounted, ref, watch} from "vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {UserFilled} from "@element-plus/icons-vue";
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {removeUserToken} from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue";
import {useSharedStore} from "@/store/sharedata";
import { CirclePlus, Setting, UserFilled } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue";
import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import {showMessageError} from "@/utils/dialog";
import { showMessageError } from "@/utils/dialog";
import LoginDialog from "@/components/LoginDialog.vue";
import { substr } from "@/utils/libs";
const isCollapse = ref(true);
const router = useRouter();
const logo = ref('');
const mainNavs = ref([])
const moreNavs = ref([])
const curPath = ref(router.currentRoute.value.path)
const title = ref("")
const mainWinHeight = window.innerHeight - 50
const loginUser = ref({})
const version = ref(process.env.VUE_APP_VERSION)
const routerViewKey = ref(0)
const showConfigDialog = ref(false)
const license = ref({de_copy: true})
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
const logo = ref("");
const mainNavs = ref([]);
const moreNavs = ref([]);
const curPath = ref();
const title = ref("");
const avatarImg = ref("/images/avatar/default.jpg");
const store = useSharedStore();
const show = ref(false)
watch(() => store.showLoginDialog, (newValue) => {
show.value = newValue
});
const loginUser = ref({});
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const showLoginDialog = ref(false);
// 监听路由变化
/**
* 从路径名中提取第一个路径段
* @param pathname - URL 的路径名部分,例如 '/chat/12345'
* @returns 第一个路径段(不含斜杠),例如 'chat',如果不存在则返回 null
*/
const extractFirstSegment = (pathname) => {
const segments = pathname.split("/").filter((segment) => segment.length > 0);
return segments.length > 0 ? segments[0] : null;
};
const getFirstPathSegment = (url) => {
try {
// 尝试使用 URL 构造函数解析完整的 URL
const parsedUrl = new URL(url);
return extractFirstSegment(parsedUrl.pathname);
} catch (error) {
// 如果解析失败,假设是相对路径,使用当前窗口的位置作为基准
if (typeof window !== "undefined") {
const parsedUrl = new URL(url, window.location.origin);
return extractFirstSegment(parsedUrl.pathname);
}
// 如果无法解析,返回 null
return null;
}
};
watch(
() => store.showLoginDialog,
(newValue) => {
showLoginDialog.value = newValue;
}
);
// 监听路由变化;
router.beforeEach((to, from, next) => {
curPath.value = to.path
curPath.value = to.path;
next();
});
if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url
curPath.value = router.currentRoute.value.query.url;
}
const changeNav = (item) => {
curPath.value = item.url
if (item.url.indexOf("http") !== -1) { // 外部链接
router.push({name: 'ExternalLink', query: {url: item.url}})
curPath.value = item.url;
if (item.url.indexOf("http") !== -1) {
// 外部链接
router.push({ path: "/external", query: { url: item.url, title: item.name } });
} else {
router.push(item.url)
// 路由切换,确保路径变化
if (router.currentRoute.value.path !== item.url) {
router.push(item.url).then(() => {
// 刷新 `routerViewKey` 触发视图重新渲染
routerViewKey.value += 1;
});
}
}
}
};
onMounted(() => {
getSystemInfo().then(res => {
logo.value = res.data.logo
title.value = res.data.title
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
curPath.value = router.currentRoute.value.path;
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
// 获取菜单
httpGet("/api/menu/list").then(res => {
mainNavs.value = res.data
// 根据窗口的高度计算应该显示多少菜单
const rows = Math.floor((window.innerHeight - 100) / 90)
if (res.data.length > rows) {
mainNavs.value = res.data.slice(0, rows)
moreNavs.value = res.data.slice(rows)
}
}).catch(e => {
ElMessage.error("获取系统菜单失败:" + e.message)
})
httpGet("/api/menu/list")
.then((res) => {
mainNavs.value = res.data;
// 根据窗口的高度计算应该显示多少菜单
const rows = Math.floor((window.innerHeight - 100) / 90);
if (res.data.length > rows) {
mainNavs.value = res.data.slice(0, rows);
moreNavs.value = res.data.slice(rows);
}
})
.catch((e) => {
ElMessage.error("获取系统菜单失败:" + e.message);
});
getLicenseInfo().then(res => {
license.value = res.data
}).catch(e => {
license.value = {de_copy: false}
showMessageError("获取 License 配置:" + e.message)
})
init()
})
getLicenseInfo()
.then((res) => {
license.value = res.data;
})
.catch((e) => {
license.value = { de_copy: false };
showMessageError("获取 License 配置:" + e.message);
});
curPath.value = "/" + getFirstPathSegment(window.location.href);
init();
});
const init = () => {
checkSession().then(user => {
loginUser.value = user
}).catch(() => {
})
}
checkSession()
.then((user) => {
loginUser.value = user;
})
.catch(() => {});
};
const logout = function () {
httpGet('/api/user/logout').then(() => {
removeUserToken()
store.setShowLoginDialog(true)
store.setIsLogin(false)
loginUser.value = {}
// 刷新组件
routerViewKey.value += 1
}).catch(() => {
ElMessage.error('注销失败!');
})
}
httpGet("/api/user/logout")
.then(() => {
removeUserToken();
router.push("/login");
})
.catch(() => {
ElMessage.error("注销失败!");
});
};
const loginCallback = () => {
init()
const loginSuccess = () => {
init();
store.setShowLoginDialog(false);
// 刷新组件
routerViewKey.value += 1
}
routerViewKey.value += 1;
};
</script>
<style lang="stylus" scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -11,17 +11,12 @@
<el-form-item label="采样方法">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.sampler" style="width:176px">
<el-option v-for="item in samplers" :label="item" :value="item" :key="item"/>
<el-select v-model="params.sampler" style="width: 150px">
<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-tooltip content="出图效果比较好的一般是 Euler 和 DPM 系列算法" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -33,17 +28,12 @@
<el-form-item label="采样调度">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.scheduler" style="width:176px">
<el-option v-for="item in schedulers" :label="item" :value="item" :key="item"/>
<el-select v-model="params.scheduler" style="width: 150px">
<el-option v-for="item in schedulers" :label="item" :value="item" :key="item" />
</el-select>
<el-tooltip
effect="light"
content="推荐自动或者 Karras"
raw-content
placement="right"
>
<el-tooltip content="推荐自动或者 Karras" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -57,10 +47,10 @@
<div class="form-item-inner">
<el-row :gutter="20">
<el-col :span="12">
<el-input v-model.number="params.width" placeholder="图片宽度"/>
<el-input v-model.number="params.width" placeholder="图片宽度" />
</el-col>
<el-col :span="12">
<el-input v-model.number="params.height" placeholder="图片高度"/>
<el-input v-model.number="params.height" placeholder="图片高度" />
</el-col>
</el-row>
</div>
@@ -72,15 +62,10 @@
<el-form-item label="迭代步数">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.steps"/>
<el-tooltip
effect="light"
content="值越大则代表细节越多,同时也意味着出图速度越慢"
raw-content
placement="right"
>
<el-input v-model.number="params.steps" />
<el-tooltip content="值越大则代表细节越多,同时也意味着出图速度越慢" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -92,15 +77,10 @@
<el-form-item label="引导系数">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.cfg_scale"/>
<el-tooltip
effect="light"
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
raw-content
placement="right"
>
<el-input v-model.number="params.cfg_scale" />
<el-tooltip content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -112,26 +92,16 @@
<el-form-item label="随机因子">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.seed"/>
<el-tooltip
effect="light"
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-input v-model.number="params.seed" />
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
<el-tooltip
effect="light"
content="使用随机数"
raw-content
placement="right"
>
<el-tooltip content="使用随机数" raw-content placement="right">
<el-icon @click="params.seed = -1" class="info-icon">
<Orange/>
<Orange />
</el-icon>
</el-tooltip>
</div>
@@ -143,15 +113,10 @@
<el-form-item label="高清修复">
<template #default>
<div class="form-item-inner">
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;" size="large"/>
<el-tooltip
effect="light"
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
raw-content
placement="right"
>
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1" size="large" />
<el-tooltip content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节" raw-content placement="right">
<el-icon style="margin-left: 10px; top: 12px">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -164,16 +129,10 @@
<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-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" style="width: 180px; --el-slider-main-bg-color: #47fff1" />
<el-tooltip content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -185,17 +144,12 @@
<el-form-item label="放大算法">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.hd_scale_alg" style="width:176px">
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item"/>
<el-select v-model="params.hd_scale_alg" style="width: 176px">
<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-tooltip content="高清修复放大算法主流算法有Latent和ESRGAN_4x" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -207,15 +161,10 @@
<el-form-item label="放大倍数">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.hd_scale"/>
<el-tooltip
effect="light"
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-input v-model.number="params.hd_scale" />
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -227,15 +176,10 @@
<el-form-item label="迭代步数">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.hd_steps"/>
<el-tooltip
effect="light"
content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数"
raw-content
placement="right"
>
<el-input v-model.number="params.hd_steps" />
<el-tooltip content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
@@ -246,13 +190,12 @@
<div class="param-line">
<el-input
v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
v-loading="isGenerating"
style="--el-mask-color:rgba(100, 100, 100, 0.8)"
v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
v-loading="isGenerating"
/>
</div>
@@ -265,52 +208,43 @@
<div class="param-line pt">
<span>反向提示词</span>
<el-tooltip
effect="light"
content="不希望出现的元素,下面给了默认的起手式"
placement="right"
>
<el-tooltip content="不希望出现的元素,下面给了默认的起手式" placement="right">
<el-icon class="info-icon">
<InfoFilled/>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
<div class="param-line">
<el-input
v-model="params.neg_prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
placeholder="反向提示词"
/>
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea" placeholder="反向提示词" />
</div>
<div class="text-info">
<el-row :gutter="10">
<el-col :span="12">
<el-tag>单次绘图消耗{{ sdPower }}算力</el-tag>
</el-col>
<el-col :span="12">
<el-tag type="success">当前可用{{ power }}算力</el-tag>
</el-col>
<el-text type="primary"
>单次绘图消耗
<el-text type="warning">{{ sdPower }}算力</el-text>
</el-text>
<el-text type="primary"
>当前可用 <el-text type="warning"> {{ power }}算力</el-text></el-text
>
</el-row>
</div>
</el-form>
</div>
<div class="submit-btn">
<el-button color="#47fff1" :dark="false" round @click="generate">立即生成</el-button>
<el-button type="primary" :dark="false" round @click="generate">立即生成</el-button>
</div>
</div>
<div class="task-list-box">
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<div class="job-list-box">
<h2>任务列表</h2>
<h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" />
<h2>创作记录</h2>
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
<template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
id="waterfall"
:list="finishedJobs"
srcKey="img_thumb"
@@ -320,215 +254,105 @@
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="fetchFinishJobs()">
<template #default="slotProp">
<div class="job-item animate">
<el-image v-if="slotProp.item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
@scrollReachBottom="fetchFinishJobs()"
>
<template #default="slotProp">
<div class="job-item animate">
<el-image v-if="slotProp.item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
</template>
</el-image>
<div v-else>
<el-image :src="slotProp.item['img_thumb']" @click="showTask(slotProp.item)" fit="cover" loading="lazy" />
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
<el-button type="warning" v-if="slotProp.item.publish" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</div>
</template>
</el-image>
<div v-else>
<el-image
:src="slotProp.item['img_thumb']"
@click="showTask(slotProp.item)"
fit="cover"
loading="lazy"/>
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
<el-button type="warning" v-if="slotProp.item.publish"
@click="publishImage(slotProp.item, false)"
circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</div>
</div>
</div>
</template>
<template #footer>
<div class="no-more-data">
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</template>
</v3-waterfall>
</template>
<template #footer>
<div class="no-more-data">
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</template>
</v3-waterfall>
</div>
<el-empty :image-size="100" v-else :image="nodata" description="暂无记录" />
</div>
<el-empty :image-size="100" v-else/>
</div> <!-- end finish job list-->
</template>
<!-- end finish job list-->
</div>
</div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box -->
<back-top :right="30" :bottom="30" />
</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.neg_prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.neg_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>
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="copyParams" @close="showTaskDialog = false" />
</div>
</div>
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {Delete, DocumentCopy, InfoFilled, Orange} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox} from "element-plus";
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { Delete, DocumentCopy, InfoFilled, Orange } from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard";
import {checkSession, getClientId, getSystemInfo} from "@/store/cache";
import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session";
import {useSharedStore} from "@/store/sharedata";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
import {showMessageError} from "@/utils/dialog";
const listBoxHeight = ref(0)
import { showMessageError } from "@/utils/dialog";
import SdTaskView from "@/components/SdTaskView.vue";
const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0)
const fullImgHeight = ref(window.innerHeight - 60)
const showTaskDialog = ref(false)
const item = ref({})
const isLogin = ref(false)
const loading = ref(true)
const colWidth = ref(220)
const fullImgHeight = ref(window.innerHeight - 60);
const showTaskDialog = ref(false);
const item = ref({});
const isLogin = ref(false);
const loading = ref(true);
const colWidth = ref(220);
const store = useSharedStore();
const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 80
listBoxHeight.value = window.innerHeight - 80;
// paramBoxHeight.value = window.innerHeight - 200
};
resizeElement()
resizeElement();
window.onresize = () => {
resizeElement()
}
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"]
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"]
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
resizeElement();
};
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"];
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"];
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"];
const params = ref({
client_id: getClientId(),
width: 1024,
@@ -545,208 +369,216 @@ const params = ref({
hd_steps: 0,
prompt: "",
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
})
});
const runningJobs = ref([])
const finishedJobs = ref([])
const router = useRouter()
const runningJobs = ref([]);
const finishedJobs = ref([]);
const router = useRouter();
// 检查是否有画同款的参数
const _params = router.currentRoute.value.params["copyParams"]
const _params = router.currentRoute.value.params["copyParams"];
if (_params) {
params.value = JSON.parse(_params)
params.value = JSON.parse(_params);
}
const power = ref(0)
const sdPower = ref(0) // 画一张 SD 图片消耗算力
const power = ref(0);
const sdPower = ref(0); // 画一张 SD 图片消耗算力
const userId = ref(0)
const clipboard = ref(null)
const userId = ref(0);
const clipboard = ref(null);
onMounted(() => {
initData()
clipboard.value = new Clipboard('.copy-prompt-sd');
clipboard.value.on('success', () => {
initData();
clipboard.value = new Clipboard(".copy-prompt-sd");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
})
});
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
getSystemInfo().then(res => {
sdPower.value = res.data.sd_power
params.value.neg_prompt = res.data.sd_neg_prompt
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
getSystemInfo()
.then((res) => {
sdPower.value = res.data.sd_power;
params.value.neg_prompt = res.data.sd_neg_prompt;
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
store.addMessageHandler("sd",(data) => {
store.addMessageHandler("sd", (data) => {
// 丢弃无关消息
if (data.channel !== "sd" || data.clientId !== getClientId()) {
return
return;
}
if (data.body === "FINISH" || data.body === "FAIL") {
page.value = 0
isOver.value = false
fetchFinishJobs()
page.value = 0;
isOver.value = false;
fetchFinishJobs();
}
nextTick(() => fetchRunningJobs())
})
})
nextTick(() => fetchRunningJobs());
});
});
onUnmounted(() => {
clipboard.value.destroy()
store.removeMessageHandler("sd")
})
clipboard.value.destroy();
store.removeMessageHandler("sd");
});
const initData = () => {
checkSession().then(user => {
power.value = user['power']
userId.value = user.id
isLogin.value = true
page.value = 0
fetchRunningJobs()
fetchFinishJobs()
}).catch(() => {
});
}
checkSession()
.then((user) => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
})
.catch(() => {});
};
const fetchRunningJobs = () => {
if (!isLogin.value) {
return
return;
}
// 获取运行中的任务
httpGet(`/api/sd/jobs?finish=0`).then(res => {
runningJobs.value = res.data.items
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
}
httpGet(`/api/sd/jobs?finish=0`)
.then((res) => {
runningJobs.value = res.data.items;
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
const page = ref(0)
const pageSize = ref(20)
const isOver = ref(false)
const page = ref(0);
const pageSize = ref(20);
const isOver = ref(false);
// 获取已完成的任务
const fetchFinishJobs = () => {
if (!isLogin.value || isOver.value === true) {
return
return;
}
loading.value = true
page.value = page.value + 1
loading.value = true;
page.value = page.value + 1;
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
if (res.data.items.length < pageSize.value) {
isOver.value = true
}
const imageList = res.data.items
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 (page.value === 1) {
finishedJobs.value = imageList
} else {
finishedJobs.value = finishedJobs.value.concat(imageList)
}
loading.value = false
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
}
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
if (res.data.items.length < pageSize.value) {
isOver.value = true;
}
const imageList = res.data.items;
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 (page.value === 1) {
finishedJobs.value = imageList;
} else {
finishedJobs.value = finishedJobs.value.concat(imageList);
}
loading.value = false;
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
// 创建绘图任务
const promptRef = ref(null)
const promptRef = ref(null);
const generate = () => {
if (params.value.prompt === '') {
promptRef.value.focus()
return ElMessage.error("请输入绘画提示词!")
if (params.value.prompt === "") {
promptRef.value.focus();
return ElMessage.error("请输入绘画提示词!");
}
if (!isLogin.value) {
store.setShowLoginDialog(true)
return
store.setShowLoginDialog(true);
return;
}
if (!params.value.seed) {
params.value.seed = -1
params.value.seed = -1;
}
params.value.session_id = getSessionId()
httpPost("/api/sd/image", params.value).then(() => {
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
power.value -= sdPower.value
fetchRunningJobs()
}).catch(e => {
ElMessage.error("任务推送失败:" + e.message)
})
}
params.value.session_id = getSessionId();
httpPost("/api/sd/image", params.value)
.then(() => {
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...");
power.value -= sdPower.value;
fetchRunningJobs();
})
.catch((e) => {
ElMessage.error("任务推送失败:" + e.message);
});
};
const showTask = (row) => {
item.value = row
showTaskDialog.value = true
}
item.value = row;
showTaskDialog.value = true;
};
const copyParams = (row) => {
params.value = row.params
showTaskDialog.value = false
}
params.value = row.params;
showTaskDialog.value = false;
};
const removeImage = (item) => {
ElMessageBox.confirm(
'此操作将会删除任务和图片,继续操作码?',
'删除提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
httpGet("/api/sd/remove", {id: item.id}).then(() => {
ElMessage.success("任务删除成功")
page.value = 0
isOver.value = false
fetchFinishJobs()
}).catch(e => {
ElMessage.error("任务删除失败:" + e.message)
})
}).catch(() => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
})
}
.then(() => {
httpGet("/api/sd/remove", { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
page.value = 0;
isOver.value = false;
fetchFinishJobs();
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
})
.catch(() => {});
};
// 发布图片到作品墙
const publishImage = (item, action) => {
let text = "图片发布"
let text = "图片发布";
if (action === false) {
text = "取消发布"
text = "取消发布";
}
httpGet("/api/sd/publish", {id: item.id, action: action}).then(() => {
ElMessage.success(text + "成功")
item.publish = action
page.value = 0
isOver.value = false
item.publish = action
}).catch(e => {
ElMessage.error(text + "失败:" + e.message)
})
}
httpGet("/api/sd/publish", { id: item.id, action: action })
.then(() => {
ElMessage.success(text + "成功");
item.publish = action;
page.value = 0;
isOver.value = false;
item.publish = action;
})
.catch((e) => {
ElMessage.error(text + "失败:" + e.message);
});
};
const isGenerating = ref(false)
const isGenerating = ref(false);
const generatePrompt = () => {
if (params.value.prompt === "") {
return showMessageError("请输入原始提示词")
return showMessageError("请输入原始提示词");
}
isGenerating.value = true
httpPost("/api/prompt/image", {prompt: params.value.prompt}).then(res => {
params.value.prompt = res.data
isGenerating.value = false
}).catch(e => {
showMessageError("生成提示词失败:"+e.message)
isGenerating.value = false
})
}
isGenerating.value = true;
httpPost("/api/prompt/image", { prompt: params.value.prompt })
.then((res) => {
params.value.prompt = res.data;
isGenerating.value = false;
})
.catch((e) => {
showMessageError("生成提示词失败:" + e.message);
isGenerating.value = false;
});
};
</script>
<style lang="stylus">

View File

@@ -2,8 +2,8 @@
<div class="page-images-wall">
<div class="inner custom-scroll">
<div class="header">
<h2>AI 绘画作品墙</h2>
<div class="settings">
<h2 class="text-xl pt-4 pb-4">AI 绘画作品墙</h2>
<div class="settings pr-14">
<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>
@@ -11,60 +11,52 @@
</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="isOver"
@scrollReachBottom="getNext">
<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="isOver"
@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">
<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>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div class="opt">
<el-tooltip
class="box-item"
effect="light"
content="复制提示词"
placement="top"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy/>
<DocumentCopy />
</el-icon>
</el-tooltip>
<el-tooltip
class="box-item"
effect="light"
content="画同款"
placement="top"
>
<el-tooltip class="box-item" content="画同款" placement="top">
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i>
</el-tooltip>
</div>
@@ -72,50 +64,47 @@
</template>
</v3-waterfall>
<v3-waterfall v-else-if="imgType === 'dall'"
id="waterfall"
:list="data['dall']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="getNext">
<v3-waterfall
v-else-if="imgType === 'dall'"
id="waterfall"
:list="data['dall']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@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">
<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>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div class="opt">
<el-tooltip
class="box-item"
effect="light"
content="复制提示词"
placement="top"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy/>
<DocumentCopy />
</el-icon>
</el-tooltip>
</div>
@@ -123,32 +112,31 @@
</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="isOver"
@scrollReachBottom="getNext">
<v3-waterfall
v-else
id="waterfall"
:list="data['sd']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@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)">
<el-image :src="slotProp.item['img_thumb']" loading="lazy" @click="showTask(slotProp.item)">
<template #placeholder>
<div class="image-slot">
正在加载图片
</div>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
<Picture />
</el-icon>
</div>
</template>
@@ -159,273 +147,156 @@
</v3-waterfall>
<div class="footer" v-if="isOver">
<!-- <el-empty
:image-size="100"
:image="nodata"
description="没有更多数据了"
/> -->
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end of waterfall -->
<back-top :right="30" :bottom="30" />
</div>
<!-- end of waterfall -->
</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>
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="drawSameSd" @close="showTaskDialog = false" />
</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 nodata from "@/assets/img/no-data.png";
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";
import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue";
import SdTaskView from "@/components/SdTaskView.vue";
const data = ref({
"mj": [],
"sd": [],
"dall": [],
})
const loading = ref(true)
const isOver = ref(false)
const imgType = ref("mj") // 图片类别
const listBoxHeight = window.innerHeight - 124
const colWidth = ref(220)
const fullImgHeight = ref(window.innerHeight - 60)
const showTaskDialog = ref(false)
const item = ref({})
mj: [],
sd: [],
dall: [],
});
const loading = ref(true);
const isOver = ref(false);
const imgType = ref("mj"); // 图片类别
const listBoxHeight = window.innerHeight - 124;
const colWidth = ref(220);
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()
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()
}
calcColWidth();
};
const page = ref(0)
const pageSize = ref(15)
const page = ref(0);
const pageSize = ref(15);
// 获取下一页数据
const getNext = () => {
if (isOver.value) {
return
return;
}
loading.value = true
page.value = page.value + 1
let url = ""
loading.value = true;
page.value = page.value + 1;
let url = "";
switch (imgType.value) {
case "mj":
url = "/api/mj/imgWall"
break
url = "/api/mj/imgWall";
break;
case "sd":
url = "/api/sd/imgWall"
break
url = "/api/sd/imgWall";
break;
case "dall":
url = "/api/dall/imgWall"
break
url = "/api/dall/imgWall";
break;
}
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`).then(res => {
loading.value = false
if (!res.data.items || res.data.items.length === 0) {
isOver.value = true
return
}
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
loading.value = false;
if (!res.data.items || res.data.items.length === 0) {
isOver.value = true;
return;
}
// 生成缩略图
const imageList = res.data.items
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
}
// 生成缩略图
const imageList = res.data.items;
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)
if (imageList.length < pageSize.value) {
isOver.value = true;
}
data.value[imgType.value] = data.value[imgType.value].concat(imageList);
})
.catch((e) => {
ElMessage.error("获取图片失败:" + e.message);
});
};
}).catch(e => {
ElMessage.error("获取图片失败:" + e.message)
})
}
getNext();
getNext()
const clipboard = ref(null)
const clipboard = ref(null);
onMounted(() => {
clipboard.value = new Clipboard('.copy-prompt-wall');
clipboard.value.on('success', () => {
clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
})
});
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
})
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
});
onUnmounted(() => {
clipboard.value.destroy()
})
clipboard.value.destroy();
});
const changeImgType = () => {
console.log(imgType.value)
document.getElementById('waterfall-box').scrollTo(0, 0)
page.value = 0
console.log(imgType.value);
document.getElementById("waterfall-box").scrollTo(0, 0);
page.value = 0;
data.value = {
"mj": [],
"sd": [],
"dall": [],
}
loading.value = true
isOver.value = false
nextTick(() => getNext())
}
mj: [],
sd: [],
dall: [],
};
loading.value = true;
isOver.value = false;
nextTick(() => getNext());
};
const showTask = (row) => {
item.value = row
showTaskDialog.value = true
}
item.value = row;
showTaskDialog.value = true;
};
const router = useRouter()
const router = useRouter();
const drawSameSd = (row) => {
router.push({name: "image-sd", params: {copyParams: JSON.stringify(row.params)}})
}
router.push({
name: "image-sd",
params: { copyParams: JSON.stringify(row.params) },
});
};
const drawSameMj = (row) => {
router.push({name: "image-mj", params: {prompt: row.prompt}})
}
router.push({ name: "image-mj", params: { prompt: row.prompt } });
};
</script>
<style lang="stylus">

View File

@@ -1,181 +1,172 @@
<template>
<div class="index-page" :style="{height: winHeight+'px'}">
<div :class="theme.imageBg?'color-bg image-bg':'color-bg'" :style="{backgroundImage:'url('+bgStyle.backgroundImage+')', backgroundColor:bgStyle.backgroundColor}"></div>
<div class="index-page">
<!-- 主题切换 -->
<ThemeChange />
<div class="menu-box">
<el-menu
mode="horizontal"
:ellipsis="false"
>
<el-menu mode="horizontal" :ellipsis="false">
<div class="menu-item">
<el-image :src="logo" class="logo" alt="Geek-AI"/>
<div class="title" :style="{color:theme.textColor}">{{ title }}</div>
<img :src="logo" class="logo" alt="Geek-AI" />
</div>
<div class="menu-item">
<span v-if="!license.de_copy">
<a :href="docsURL" target="_blank">
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
<i class="iconfont icon-book"></i>
<span>文档</span>
</el-button>
</a>
<a :href="gitURL" target="_blank">
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
<i class="iconfont icon-github"></i>
<span>源码</span>
</el-button>
</a>
<el-tooltip v-if="!license.de_copy" class="box-item" content="部署文档" placement="bottom">
<a :href="docsURL" class="link-button mr-2" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip v-if="!license.de_copy" class="box-item" content="项目源码" placement="bottom">
<a :href="gitURL" class="link-button" target="_blank">
<i class="iconfont icon-github"></i>
</a>
</el-tooltip>
</span>
<span v-if="!isLogin">
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/login')" class="shadow" round>登录</el-button>
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/register')" class="shadow" round>注册</el-button>
<!-- <el-button @click="router.push('/login')" class="shadow" round
>登录</el-button
>
<el-button @click="router.push('/register')" class="shadow" round
>注册</el-button
> -->
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录/注册</el-button>
</span>
</div>
</el-menu>
</div>
<div class="content">
<h1 :style="{color:theme.textColor}">欢迎使用 {{ title }}</h1>
<p :style="{color:theme.textColor}">{{ slogan }}</p>
<div style="height: 158px"></div>
<h1 class="animate__animated animate__backInDown">
{{ title }}
</h1>
<div class="msg-text cursor-ani">
<span v-for="(char, index) in displayedChars" :key="index" :style="{ color: rainbowColor(index) }">
{{ char }}
</span>
</div>
<div class="navs">
<el-space wrap>
<div v-for="item in navs" :key="item.url" class="nav-item">
<el-button @click="router.push(item.url)" :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" :dark="false">
<i :class="'iconfont '+iconMap[item.url]"></i>
<span>{{item.name}}</span>
</el-button>
<div class="navs animate__animated animate__backInDown">
<el-space wrap :size="14">
<div v-for="item in navs" :key="item.url" class="nav-item-box" @click="router.push(item.url)">
<i :class="'iconfont ' + iconMap[item.url]"></i>
<div>{{ item.name }}</div>
</div>
</el-space>
</div>
</div>
<footer-bar :text-color="theme.textColor" />
<footer-bar />
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {useRouter} from "vue-router";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {isMobile} from "@/utils/libs";
import ThemeChange from "@/components/ThemeChange.vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { isMobile } from "@/utils/libs";
const router = useRouter()
const router = useRouter();
if (isMobile()) {
router.push("/mobile/index")
router.push("/mobile/index");
}
const title = ref("")
const logo = ref("")
const slogan = ref("")
const license = ref({de_copy: true})
const winHeight = window.innerHeight - 150
const isLogin = ref(false)
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
const navs = ref([])
const btnColors = ref([
{bgColor: "#fff143", textColor: "#50616D"},
{bgColor: "#eaff56", textColor: "#50616D"},
{bgColor: "#bddd22", textColor: "#50616D"},
{bgColor: "#1bd1a5", textColor: "#50616D"},
{bgColor: "#e0eee8", textColor: "#50616D"},
{bgColor: "#7bcfa6", textColor: "#50616D"},
{bgColor: "#bce672", textColor: "#50616D"},
{bgColor: "#44cef6", textColor: "#ffffff"},
{bgColor: "#70f3ff", textColor: "#50616D"},
{bgColor: "#fffbf0", textColor: "#50616D"},
{bgColor: "#d6ecf0", textColor: "#50616D"},
{bgColor: "#88ada6", textColor: "#50616D"},
{bgColor: "#30dff3", textColor: "#50616D"},
{bgColor: "#d3e0f3", textColor: "#50616D"},
{bgColor: "#e9e7ef", textColor: "#50616D"},
{bgColor: "#eacd76", textColor: "#50616D"},
{bgColor: "#f2be45", textColor: "#50616D"},
{bgColor: "#549688", textColor: "#ffffff"},
{bgColor: "#758a99", textColor: "#ffffff"},
{bgColor: "#41555d", textColor: "#ffffff"},
{bgColor: "#21aa93", textColor: "#ffffff"},
{bgColor: "#0aa344", textColor: "#ffffff"},
{bgColor: "#f05654", textColor: "#ffffff"},
{bgColor: "#db5a6b", textColor: "#ffffff"},
{bgColor: "#db5a6b", textColor: "#ffffff"},
{bgColor: "#8d4bbb", textColor: "#ffffff"},
{bgColor: "#426666", textColor: "#ffffff"},
{bgColor: "#177cb0", textColor: "#ffffff"},
{bgColor: "#395260", textColor: "#ffffff"},
{bgColor: "#519a73", textColor: "#ffffff"},
{bgColor: "#75878a", textColor: "#ffffff"},
])
const iconMap =ref(
{
"/chat": "icon-chat",
"/mj": "icon-mj",
"/sd": "icon-sd",
"/dalle": "icon-dalle",
"/images-wall": "icon-image",
"/suno": "icon-suno",
"/xmind": "icon-xmind",
"/apps": "icon-app",
"/member": "icon-vip-user",
"/invite": "icon-share",
"/luma": "icon-luma",
}
)
const bgStyle = {}
const color = btnColors.value[Math.floor(Math.random() * btnColors.value.length)]
const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true})
const title = ref("");
const logo = ref("");
const slogan = ref("");
const license = ref({ de_copy: true });
const isLogin = ref(false);
const docsURL = ref(process.env.VUE_APP_DOCS_URL);
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const navs = ref([]);
const iconMap = ref({
"/chat": "icon-chat",
"/mj": "icon-mj",
"/sd": "icon-sd",
"/dalle": "icon-dalle",
"/images-wall": "icon-image",
"/suno": "icon-suno",
"/xmind": "icon-xmind",
"/apps": "icon-app",
"/member": "icon-vip-user",
"/invite": "icon-share",
"/luma": "icon-luma",
});
const displayedChars = ref([]);
const initAnimation = ref("");
let timer = null; // 定时器句柄
// 初始化间隔时间和随机时间数组
const interTime = ref(50);
const interArr = [90, 100, 70, 88, 80, 110, 85, 400, 90, 99];
onMounted(() => {
getSystemInfo().then(res => {
title.value = res.data.title
logo.value = res.data.logo
if (res.data.index_bg_url === 'color') {
// 随机选取一种颜色
theme.value.bgColor = color.bgColor
theme.value.btnBgColor = color.bgColor
theme.value.textColor = color.textColor
theme.value.btnTextColor = color.textColor
// 设置背景颜色
bgStyle.backgroundColor = theme.value.bgColor
bgStyle.backgroundImage = "/images/transparent-bg.png"
theme.value.imageBg = false
} else if (res.data.index_bg_url) {
bgStyle.backgroundImage = res.data.index_bg_url
} else {
bgStyle.backgroundImage = "/images/index-bg.jpg"
}
getSystemInfo()
.then((res) => {
title.value = res.data.title;
logo.value = res.data.logo;
slogan.value = res.data.slogan;
if (timer) clearInterval(timer); // 清除定时器
timer = setInterval(setContent, interTime.value);
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
slogan.value = res.data.slogan
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
getLicenseInfo()
.then((res) => {
license.value = res.data;
})
.catch((e) => {
license.value = { de_copy: false };
ElMessage.error("获取 License 配置失败:" + e.message);
});
getLicenseInfo().then(res => {
license.value = res.data
}).catch(e => {
license.value = {de_copy: false}
ElMessage.error("获取 License 配置失败:" + e.message)
})
httpGet("/api/menu/list?index=1")
.then((res) => {
navs.value = res.data;
})
.catch((e) => {
ElMessage.error("获取导航菜单失败:" + e.message);
});
httpGet("/api/menu/list?index=1").then(res => {
navs.value = res.data
}).catch(e => {
ElMessage.error("获取导航菜单失败:" + e.message)
})
checkSession().then(() => {
isLogin.value = true
}).catch(()=>{})
})
checkSession()
.then(() => {
isLogin.value = true;
})
.catch(() => {});
});
// 打字机内容逐字符显示
const setContent = () => {
if (initAnimation.value.length >= slogan.value.length) {
// 文本已全部输出
initAnimation.value = "";
displayedChars.value = [];
if (timer) clearInterval(timer);
timer = setInterval(setContent, interTime.value);
return;
} else {
const nextChar = slogan.value.charAt(initAnimation.value.length);
initAnimation.value += slogan.value.charAt(initAnimation.value.length); // 逐字符追加
displayedChars.value.push(nextChar);
interTime.value = interArr[Math.floor(Math.random() * interArr.length)]; // 设置随机间隔
if (timer) clearInterval(timer);
timer = setInterval(setContent, interTime.value);
}
};
// 计算彩虹色
const rainbowColor = (index) => {
const hue = (index * 40) % 360; // 每个字符间隔40度形成彩虹色
return `hsl(${hue}, 90%, 50%)`; // 色调(hue),饱和度(70%),亮度(50%)
};
</script>
<style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css'
@import "@/assets/css/index.styl"
</style>

View File

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

View File

@@ -1,176 +1,156 @@
<template>
<div>
<div class="bg"></div>
<div class="main">
<div class="contain">
<div class="logo">
<el-image :src="logo" fit="cover" @click="router.push('/')"/>
</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="opt" :gutter="24">
<el-col :span="8"><el-link type="primary" @click="router.push('/register')">注册</el-link></el-col>
<el-col :span="8">
<el-link type="info" @click="showResetPass = true">重置密码</el-link>
</el-col>
<el-col :span="8">
<el-link type="info" @click="router.push('/')">首页</el-link>
</el-col>
</el-row>
<div v-if="wechatLoginURL !== ''">
<el-divider class="divider">其他登录方式</el-divider>
<div class="clogin">
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"><i class="iconfont icon-wechat"></i></a>
<div class="flex-center loginPage">
<div class="left">
<div class="login-box">
<AccountTop>
<template #default>
<div class="wechatLog flex-center" v-if="wechatLoginURL !== ''">
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"> <i class="iconfont icon-wechat"></i>使用微信登录 </a>
</div>
</div>
</template>
</AccountTop>
<div class="input-form">
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules">
<el-form-item label="" prop="username">
<div class="form-title">账号</div>
<el-input v-model="ruleForm.username" size="large" placeholder="请输入账号" @keyup="handleKeyup" />
</el-form-item>
<el-form-item label="" prop="password">
<div class="flex-between w100">
<div class="form-title">密码</div>
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">忘记密码</div>
</div>
<el-input size="large" v-model="ruleForm.password" placeholder="请输入密码" show-password autocomplete="off" @keyup="handleKeyup" />
</el-form-item>
<el-form-item>
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
<reset-pass @hide="showResetPass = false" :show="showResetPass"/>
<captcha v-if="enableVerify" @success="doLogin" ref="captchaRef"/>
<footer-bar/>
</div>
<account-bg />
<captcha v-if="enableVerify" @success="doLogin" ref="captchaRef" />
</div>
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { useRouter } from "vue-router";
import AccountBg from "@/components/AccountBg.vue";
import { isMobile } from "@/utils/libs";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { setUserToken } from "@/store/session";
import { showMessageError } from "@/utils/dialog";
import { setRoute } from "@/store/system";
import { useSharedStore } from "@/store/sharedata";
import {onMounted, ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
import {isMobile} from "@/utils/libs";
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {setUserToken} from "@/store/session";
import ResetPass from "@/components/ResetPass.vue";
import {showMessageError} from "@/utils/dialog";
import AccountTop from "@/components/AccountTop.vue";
import Captcha from "@/components/Captcha.vue";
import {setRoute} from "@/store/system";
import {useSharedStore} from "@/store/sharedata";
const router = useRouter();
const title = ref('Geek-AI');
const username = ref(process.env.VUE_APP_USER);
const password = ref(process.env.VUE_APP_PASS);
const showResetPass = ref(false)
const logo = ref("")
const licenseConfig = ref({})
const wechatLoginURL = ref('')
const enableVerify = ref(false)
const captchaRef = ref(null)
// 是否需要验证码,输入一次密码错之后就要验证码
const needVerify = ref(false)
const title = ref("Geek-AI");
const logo = ref("");
const licenseConfig = ref({});
const wechatLoginURL = ref("");
const enableVerify = ref(false);
const captchaRef = ref(null);
const ruleFormRef = ref(null);
const ruleForm = reactive({
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS,
});
const rules = {
username: [{ required: true, trigger: "blur", message: "请输入账号" }],
password: [{ required: true, trigger: "blur", message: "请输入密码" }],
};
onMounted(() => {
// 获取系统配置
getSystemInfo().then(res => {
logo.value = res.data.logo
title.value = res.data.title
enableVerify.value = res.data['enabled_verify']
}).catch(e => {
showMessageError("获取系统配置失败:" + e.message)
})
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
enableVerify.value = res.data["enabled_verify"];
})
.catch((e) => {
showMessageError("获取系统配置失败:" + e.message);
});
getLicenseInfo().then(res => {
licenseConfig.value = res.data
}).catch(e => {
showMessageError("获取 License 配置:" + e.message)
})
getLicenseInfo()
.then((res) => {
licenseConfig.value = res.data;
})
.catch((e) => {
showMessageError("获取 License 配置:" + e.message);
});
checkSession().then(() => {
if (isMobile()) {
router.push('/mobile')
} else {
router.push('/chat')
}
}).catch(() => {
})
checkSession()
.then(() => {
if (isMobile()) {
router.push("/mobile");
} else {
router.push("/chat");
}
})
.catch(() => {});
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
httpGet("/api/user/clogin?return_url="+returnURL).then(res => {
wechatLoginURL.value = res.data.url
}).catch(e => {
console.error(e)
})
})
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`;
httpGet("/api/user/clogin?return_url=" + returnURL)
.then((res) => {
wechatLoginURL.value = res.data.url;
})
.catch((e) => {
console.error(e);
});
});
const handleKeyup = (e) => {
if (e.key === 'Enter') {
if (e.key === "Enter") {
login();
}
};
const login = function () {
if (username.value.trim() === '') {
return showMessageError("请输入用户民")
}
if (password.value.trim() === '') {
return showMessageError('请输入密码');
}
const login = async function () {
await ruleFormRef.value.validate(async (valid) => {
if (valid) {
if (enableVerify.value) {
captchaRef.value.loadCaptcha();
} else {
doLogin({});
}
}
});
};
if (enableVerify.value && needVerify.value) {
captchaRef.value.loadCaptcha()
} else {
doLogin({})
}
}
const store = useSharedStore()
const store = useSharedStore();
const doLogin = (verifyData) => {
httpPost('/api/user/login', {
username: username.value.trim(),
password: password.value.trim(),
httpPost("/api/user/login", {
username: ruleForm.username,
password: ruleForm.password,
key: verifyData.key,
dots: verifyData.dots,
x: verifyData.x
}).then((res) => {
setUserToken(res.data.token)
store.setIsLogin(true)
needVerify.value = false
if (isMobile()) {
router.push('/mobile')
} else {
router.push('/chat')
}
}).catch((e) => {
showMessageError('登录失败,' + e.message)
needVerify.value = true
x: verifyData.x,
})
}
.then((res) => {
setUserToken(res.data.token);
store.setIsLogin(true);
if (isMobile()) {
router.push("/mobile");
} else {
router.push("/chat");
}
})
.catch((e) => {
showMessageError("登录失败," + e.message);
});
};
</script>
<style lang="stylus" scoped>
@import "@/assets/css/login.styl"
</style>
</style>

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<div class="member custom-scroll" v-loading="loading" element-loading-background="rgba(255,255,255,.3)" :element-loading-text="loadingText">
<div class="inner">
<div class="user-profile">
<user-profile :key="profileKey"/>
<user-profile :key="profileKey" />
<el-row class="user-opt" :gutter="20">
<el-col :span="12">
@@ -19,24 +19,21 @@
<el-button type="primary" @click="showPasswordDialog = true">修改密码</el-button>
</el-col>
<el-col :span="24">
<el-button type="primary" @click="showRedeemVerifyDialog = true">卡密兑换
</el-button>
<el-button type="primary" @click="showRedeemVerifyDialog = true">卡密兑换 </el-button>
</el-col>
</el-row>
</div>
<div class="product-box">
<div class="info" v-if="orderPayInfoText !== ''">
<el-alert type="success" show-icon :closable="false" effect="dark">
<strong>说明:</strong> {{ vipInfoText }}
</el-alert>
<el-alert type="success" show-icon :closable="false" effect="dark"> <strong>说明:</strong> {{ vipInfoText }} </el-alert>
</div>
<el-row v-if="list.length > 0" :gutter="20" class="list-box">
<el-col v-for="item in list" :key="item" :span="6">
<div class="product-item">
<div class="image-container">
<el-image :src="vipImg" fit="cover"/>
<el-image :src="vipImg" fit="cover" />
</div>
<div class="product-title">
<span class="name">{{ item.name }}</span>
@@ -44,7 +41,9 @@
<div class="product-info">
<div class="info-line">
<span class="label">商品原价</span>
<span class="price"><del>{{ item.price }}</del></span>
<span class="price"
><del>{{ item.price }}</del></span
>
</div>
<div class="info-line">
<span class="label">优惠价</span>
@@ -62,22 +61,21 @@
</div>
<div class="pay-way">
<span type="primary" v-for="payWay in payWays" @click="pay(item,payWay)" :key="payWay">
<el-button v-if="payWay.pay_type==='alipay'" color="#15A6E8" circle>
<i class="iconfont icon-alipay" ></i>
<span type="primary" v-for="payWay in payWays" @click="pay(item, payWay)" :key="payWay">
<el-button v-if="payWay.pay_type === 'alipay'" color="#15A6E8" circle>
<i class="iconfont icon-alipay"></i>
</el-button>
<el-button v-else-if="payWay.pay_type==='qqpay'" circle>
<el-button v-else-if="payWay.pay_type === 'qqpay'" circle>
<i class="iconfont icon-qq"></i>
</el-button>
<el-button v-else-if="payWay.pay_type==='paypal'" class="paypal" round>
<el-button v-else-if="payWay.pay_type === 'paypal'" class="paypal" round>
<i class="iconfont icon-paypal"></i>
</el-button>
<el-button v-else-if="payWay.pay_type==='jdpay'" color="#E1251B" circle>
<el-button v-else-if="payWay.pay_type === 'jdpay'" color="#E1251B" circle>
<i class="iconfont icon-jd-pay"></i>
</el-button>
<el-button v-else-if="payWay.pay_type==='douyin'" class="douyin" circle>
<i class="iconfont icon-douyin"></i>
<el-button v-else-if="payWay.pay_type === 'douyin'" class="douyin" circle>
<i class="iconfont icon-douyin"></i>
</el-button>
<el-button v-else circle class="wechat" color="#67C23A">
<i class="iconfont icon-wechat-pay"></i>
@@ -88,27 +86,29 @@
</div>
</el-col>
</el-row>
<el-empty description="暂无数据" v-else />
<el-empty description="暂无数据" v-else :image="nodata" />
<div class="box-card">
<h2 class="headline">消费账单</h2>
<h2 class="headline">消费账单</h2>
<div class="user-order">
<user-order v-if="isLogin" :key="userOrderKey"/>
<div class="user-order">
<user-order v-if="isLogin" :key="userOrderKey" />
</div>
</div>
</div>
</div>
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"/>
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false"/>
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false"/>
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false"/>
<redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback"/>
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false" />
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false" />
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false" />
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false" />
<redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback" />
</div>
<el-dialog v-model="showDialog" :show-close=false :close-on-click-modal="false" hide-footer width="auto" class="pay-dialog">
<el-dialog v-model="showDialog" :show-close="false" :close-on-click-modal="false" hide-footer width="auto" class="pay-dialog">
<div v-if="qrImg !== ''">
<div class="product-info">请使用微信扫码支付<span class="price">{{price}}</span></div>
<div class="product-info">
请使用微信扫码支付<span class="price">{{ price }}</span>
</div>
<el-image :src="qrImg" fit="cover" />
</div>
<div style="padding-bottom: 10px; text-align: center">
@@ -120,89 +120,97 @@
</template>
<script setup>
import {onMounted, ref} from "vue"
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {checkSession, getSystemInfo} from "@/store/cache";
import nodata from "@/assets/img/no-data.png";
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { httpGet, httpPost } from "@/utils/http";
import { checkSession, getSystemInfo } from "@/store/cache";
import UserProfile from "@/components/UserProfile.vue";
import PasswordDialog from "@/components/PasswordDialog.vue";
import BindMobile from "@/components/BindMobile.vue";
import RedeemVerify from "@/components/RedeemVerify.vue";
import UserOrder from "@/components/UserOrder.vue";
import {useSharedStore} from "@/store/sharedata";
import { useSharedStore } from "@/store/sharedata";
import BindEmail from "@/components/BindEmail.vue";
import ThirdLogin from "@/components/ThirdLogin.vue";
import QRCode from "qrcode";
const list = ref([])
const vipImg = ref("/images/vip.png")
const enableReward = ref(false) // 是否启用众筹功能
const rewardImg = ref('/images/reward.png')
const showPasswordDialog = ref(false)
const showBindMobileDialog = ref(false)
const showBindEmailDialog = ref(false)
const showRedeemVerifyDialog = ref(false)
const showThirdLoginDialog = ref(false)
const user = ref(null)
const isLogin = ref(false)
const orderTimeout = ref(1800)
const loading = ref(true)
const loadingText = ref("加载中...")
const orderPayInfoText = ref("")
const payWays = ref([])
const vipInfoText = ref("")
const store = useSharedStore()
const profileKey = ref(0)
const userOrderKey = ref(0)
const showDialog = ref(false)
const qrImg = ref("")
const price = ref(0)
const list = ref([]);
const vipImg = ref("/images/menu/member.png");
const enableReward = ref(false); // 是否启用众筹功能
const rewardImg = ref("/images/reward.png");
const showPasswordDialog = ref(false);
const showBindMobileDialog = ref(false);
const showBindEmailDialog = ref(false);
const showRedeemVerifyDialog = ref(false);
const showThirdLoginDialog = ref(false);
const user = ref(null);
const isLogin = ref(false);
const orderTimeout = ref(1800);
const loading = ref(true);
const loadingText = ref("加载中...");
const orderPayInfoText = ref("");
const payWays = ref([]);
const vipInfoText = ref("");
const store = useSharedStore();
const profileKey = ref(0);
const userOrderKey = ref(0);
const showDialog = ref(false);
const qrImg = ref("");
const price = ref(0);
onMounted(() => {
checkSession().then(_user => {
user.value = _user
isLogin.value = true
}).catch(() => {
store.setShowLoginDialog(true)
})
checkSession()
.then((_user) => {
user.value = _user;
isLogin.value = true;
})
.catch(() => {
store.setShowLoginDialog(true);
});
httpGet("/api/product/list").then((res) => {
list.value = res.data
loading.value = false
}).catch(e => {
ElMessage.error("获取产品套餐失败:" + e.message)
})
httpGet("/api/product/list")
.then((res) => {
list.value = res.data;
loading.value = false;
})
.catch((e) => {
ElMessage.error("获取产品套餐失败:" + e.message);
});
getSystemInfo().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']
}
vipInfoText.value = res.data['vip_info_text']
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
getSystemInfo()
.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"];
}
vipInfoText.value = res.data["vip_info_text"];
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
httpGet("/api/payment/payWays").then(res => {
payWays.value = res.data
}).catch(e => {
ElMessage.error("获取支付方式失败:" + e.message)
})
})
httpGet("/api/payment/payWays")
.then((res) => {
payWays.value = res.data;
})
.catch((e) => {
ElMessage.error("获取支付方式失败:" + e.message);
});
});
const pay = (product, payWay) => {
if (!isLogin.value) {
store.setShowLoginDialog(true)
return
store.setShowLoginDialog(true);
return;
}
loading.value = true
loadingText.value = "正在生成支付订单..."
let host = process.env.VUE_APP_API_HOST
if (host === '') {
loading.value = true;
loadingText.value = "正在生成支付订单...";
let host = process.env.VUE_APP_API_HOST;
if (host === "") {
host = `${location.protocol}//${location.host}`;
}
httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, {
@@ -211,46 +219,46 @@ const pay = (product, payWay) => {
pay_type: payWay.pay_type,
user_id: user.value.id,
host: host,
device: "jump"
}).then(res => {
showDialog.value = true
loading.value = false
if (payWay.pay_way === 'wechat') {
price.value = Number(product.discount)
QRCode.toDataURL(res.data, {width: 300, height: 300, margin: 2}, (error, url) => {
if (error) {
console.error(error)
} else {
qrImg.value = url;
}
})
} else {
window.open(res.data, '_blank');
}
}).catch(e => {
setTimeout(() => {
ElMessage.error("生成支付订单失败:" + e.message)
loading.value = false
}, 500)
device: "jump",
})
}
.then((res) => {
showDialog.value = true;
loading.value = false;
if (payWay.pay_way === "wechat") {
price.value = Number(product.discount);
QRCode.toDataURL(res.data, { width: 300, height: 300, margin: 2 }, (error, url) => {
if (error) {
console.error(error);
} else {
qrImg.value = url;
}
});
} else {
window.open(res.data, "_blank");
}
})
.catch((e) => {
setTimeout(() => {
ElMessage.error("生成支付订单失败:" + e.message);
loading.value = false;
}, 500);
});
};
const redeemCallback = (success) => {
showRedeemVerifyDialog.value = false
showRedeemVerifyDialog.value = false;
if (success) {
profileKey.value += 1
profileKey.value += 1;
}
}
};
const payCallback = (success) => {
showDialog.value = false
showDialog.value = false;
if (success) {
profileKey.value += 1
userOrderKey.value += 1
profileKey.value += 1;
userOrderKey.value += 1;
}
}
};
</script>
<style lang="stylus">

Some files were not shown because too many files have changed in this diff Show More