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

@@ -6,6 +6,6 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=GeekAI_DEV_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.7
VUE_APP_VERSION=v4.1.8
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

View File

@@ -2,6 +2,6 @@ VUE_APP_API_HOST=
VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=GeekAI_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.7
VUE_APP_VERSION=v4.1.8
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

19499
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,9 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@element-plus/icons-vue": "^2.3.1",
"@openai/realtime-api-beta": "github:openai/openai-realtime-api-beta",
"animate.css": "^4.1.1",
"axios": "^0.27.2",
"clipboard": "^2.0.11",
"compressorjs": "^1.2.1",
@@ -21,6 +22,7 @@
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-emoji": "^2.0.0",
"markdown-it-mathjax3": "^4.3.2",
"markmap-common": "^0.16.0",
"markmap-lib": "^0.16.1",
@@ -44,10 +46,13 @@
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"autoprefixer": "^10.4.20",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"postcss": "^8.4.49",
"stylus": "^0.58.1",
"stylus-loader": "^7.0.0",
"tailwindcss": "^3.4.17",
"webpack": "^5.90.3"
},
"eslintConfig": {

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,18 +1,19 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<html lang="zh-cn" data-theme="light">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,user-scalable=no"
/>
<title>Geek-AI 创作助手</title>
</head>
</head>
<body>
<noscript>
<strong>请开启JavaScript支持</strong>
</noscript>
<div id="app"></div>
</body>
</html>
<body>
<noscript>
<strong>请开启JavaScript支持</strong>
</noscript>
<div id="app"></div>
</body>
</html>

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>

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