增加移动端登录页面

This commit is contained in:
RockYang 2024-12-26 16:52:20 +08:00
parent acee2d9d81
commit 8af0fec8ec
25 changed files with 831 additions and 1012 deletions

View File

@ -9,7 +9,8 @@
- Bug 修复:修复 Suno 已完成任务删除失败的 错误 - Bug 修复:修复 Suno 已完成任务删除失败的 错误
- 功能新增:支持 OpenAI 实时语音通话功能,目前已经支持按次收费,支持管理员设置每次实时语音通话的算力消耗 - 功能新增:支持 OpenAI 实时语音通话功能,目前已经支持按次收费,支持管理员设置每次实时语音通话的算力消耗
- 功能新增:生成提示词需要消耗算力,支持管理员设置每次生成提示词的算力消耗,防止被白嫖 - 功能新增:生成提示词需要消耗算力,支持管理员设置每次生成提示词的算力消耗,防止被白嫖
- 功能新增DALL-E-3 绘图支持 Gitee AI Flex 接口 - 功能新增DALL-E-3 绘图支持 Flux 绘图模型,支持在管理后添加 Flux,SD 等绘图模型
- 功能优化Markdown 支持解析 emoji 表情
## v4.1.7 ## v4.1.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -2,6 +2,7 @@
--sm-txt:rgba(163, 174, 208, 1); --sm-txt:rgba(163, 174, 208, 1);
--text-secondary: #8a939d; --text-secondary: #8a939d;
--el-color-primary: rgb(107, 80, 225); --el-color-primary: rgb(107, 80, 225);
--van-primary-color:rgb(107, 80, 225);
--theme-textcolor-normal:#b0a0f8; --theme-textcolor-normal:#b0a0f8;
--el-border-radius-base: 5px; --el-border-radius-base: 5px;
--el-color-primary-light-5:rgb(107, 85, 255); --el-color-primary-light-5:rgb(107, 85, 255);

View File

@ -20,8 +20,12 @@
height: 100vh; height: 100vh;
.title{ .title{
font-size: 28px font-size: 28px
height: 40px
width 120px
text-align: center;
word-wrap break-all;
overflow hidden
font-weight: 700 font-weight: 700
margin-right: 6px
color:var(--text-theme-color) color:var(--text-theme-color)
} }
img{ img{

View File

@ -52,6 +52,8 @@
.list-item { .list-item {
display flex
.image { .image {
overflow hidden overflow hidden

View File

@ -70,8 +70,10 @@
} }
.logo { .logo {
height 50px height 60px
width 60px
border-radius 50% border-radius 50%
background: var(--theme-bg)
} }
.el-button { .el-button {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

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

View File

@ -1,90 +1,93 @@
<template> <template>
<div class="wg-cap-wrap" :style="{ width: width }"> <div class="wg-cap-wrap" :style="{ width: width }">
<div class="wg-cap-wrap__header"> <div class="wg-cap-wrap__header">
<span>请在下图<em>依次</em>点击</span> <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>
<div class="wg-cap-wrap__body"> <div class="wg-cap-wrap__body">
<img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" " <img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" " @click="handleClickPos($event)" />
@click="handleClickPos($event)"> <img
<img class="wg-cap-wrap__loading" class="wg-cap-wrap__loading"
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+"
alt="正在加载中..."> alt="正在加载中..."
/>
<div v-for="(dot, key) in dots" :key="key" class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`"> <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> <span>{{ dot.index }}</span>
</div> </div>
</div> </div>
<div class="wg-cap-wrap__footer"> <div class="wg-cap-wrap__footer">
<div class="wg-cap-wrap__ico"> <div class="wg-cap-wrap__ico flex">
<img @click="handleCloseEvent" <img
@click="handleCloseEvent"
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg==" src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg=="
alt="关闭"> alt="关闭"
<img @click="handleRefreshEvent" />
<img
@click="handleRefreshEvent"
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4=" src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4="
alt="刷新"> alt="刷新"
/>
</div> </div>
<div class="wg-cap-wrap__btn"> <div class="wg-cap-wrap__btn">
<el-button type="primary" @click="handleConfirmEvent">确认</el-button> <el-button type="primary" @click="handleConfirmEvent">确认</el-button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'CaptchaPlus', name: "CaptchaPlus",
mounted() { mounted() {
this.$emit('refresh') this.$emit("refresh");
}, },
props: { props: {
value: Boolean, value: Boolean,
width: { width: {
type: String, type: String,
default: '300px' default: "300px",
}, },
calcPosType: { calcPosType: {
type: String, type: String,
default: 'dom', default: "dom",
validator: value => ['dom', 'screen'].includes(value) validator: (value) => ["dom", "screen"].includes(value),
}, },
maxDot: { maxDot: {
type: Number, type: Number,
default: 5 default: 5,
// validator: value => value > 10 // validator: value => value > 10
}, },
imageBase64: String, imageBase64: String,
thumbBase64: String thumbBase64: String,
}, },
data() { data() {
return { return {
dots: [], dots: [],
imageBase64Code: '', imageBase64Code: "",
thumbBase64Code: '' thumbBase64Code: "",
} };
}, },
watch: { watch: {
value() { value() {
this.dots = [] this.dots = [];
this.imageBase64Code = '' this.imageBase64Code = "";
this.thumbBase64Code = '' this.thumbBase64Code = "";
}, },
imageBase64(val) { imageBase64(val) {
this.dots = [] this.dots = [];
this.imageBase64Code = val this.imageBase64Code = val;
}, },
thumbBase64(val) { thumbBase64(val) {
this.dots = [] this.dots = [];
this.thumbBase64Code = val this.thumbBase64Code = val;
} },
}, },
methods: { methods: {
/** /**
* @Description: 处理关闭事件 * @Description: 处理关闭事件
*/ */
handleCloseEvent() { handleCloseEvent() {
this.$emit('close') this.$emit("close");
// this.dots = [] // this.dots = []
// this.imageBase64Code = '' // this.imageBase64Code = ''
// this.thumbBase64Code = '' // this.thumbBase64Code = ''
@ -93,14 +96,14 @@ export default {
* @Description: 处理刷新事件 * @Description: 处理刷新事件
*/ */
handleRefreshEvent() { handleRefreshEvent() {
this.dots = [] this.dots = [];
this.$emit('refresh') this.$emit("refresh");
}, },
/** /**
* @Description: 处理确认事件 * @Description: 处理确认事件
*/ */
handleConfirmEvent() { handleConfirmEvent() {
this.$emit('confirm', this.dots) this.$emit("confirm", this.dots);
}, },
/** /**
* @Description: 处理dot * @Description: 处理dot
@ -108,102 +111,102 @@ export default {
*/ */
handleClickPos(ev) { handleClickPos(ev) {
if (this.dots.length >= this.maxDot) { if (this.dots.length >= this.maxDot) {
return return;
} }
const e = ev || window.event const e = ev || window.event;
e.preventDefault() e.preventDefault();
const dom = e.currentTarget const dom = e.currentTarget;
const {domX, domY} = this.getDomXY(dom) const { domX, domY } = this.getDomXY(dom);
// =============================================== // ===============================================
// @notice getDomXY 使 calcLocationLeft calcLocationTop // @notice getDomXY 使 calcLocationLeft calcLocationTop
// const domX = this.calcLocationLeft(dom) // const domX = this.calcLocationLeft(dom)
// const domY = this.calcLocationTop(dom) // const domY = this.calcLocationTop(dom)
// =============================================== // ===============================================
let mouseX = (navigator.vendor === 'Netscape') ? e.pageX : e.x + 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 let mouseY = navigator.vendor === "Netscape" ? e.pageY : e.y + document.body.offsetTop;
// //
if (e.touches && e.touches.length > 0) { if (e.touches && e.touches.length > 0) {
mouseX = e.touches[0].clientX mouseX = e.touches[0].clientX;
mouseY = e.touches[0].clientY mouseY = e.touches[0].clientY;
} else { } else {
mouseX = e.clientX mouseX = e.clientX;
mouseY = e.clientY mouseY = e.clientY;
} }
// //
const xPos = mouseX - domX const xPos = mouseX - domX;
const yPos = mouseY - domY const yPos = mouseY - domY;
// //
const xp = parseInt(xPos.toString()) const xp = parseInt(xPos.toString());
const yp = parseInt(yPos.toString()) const yp = parseInt(yPos.toString());
// //
this.dots.push({ this.dots.push({
x: xp - 11, x: xp - 11,
y: yp - 11, y: yp - 11,
index: this.dots.length + 1 index: this.dots.length + 1,
}) });
return false return false;
}, },
/** /**
* @Description: 找到元素的屏幕位置 * @Description: 找到元素的屏幕位置
* @param el * @param el
*/ */
calcLocationLeft(el) { calcLocationLeft(el) {
let tmp = el.offsetLeft let tmp = el.offsetLeft;
let val = el.offsetParent let val = el.offsetParent;
while (val != null) { while (val != null) {
tmp += val.offsetLeft tmp += val.offsetLeft;
val = val.offsetParent val = val.offsetParent;
} }
return tmp return tmp;
}, },
/** /**
* @Description: 找到元素的屏幕位置 * @Description: 找到元素的屏幕位置
* @param el * @param el
*/ */
calcLocationTop(el) { calcLocationTop(el) {
let tmp = el.offsetTop let tmp = el.offsetTop;
let val = el.offsetParent let val = el.offsetParent;
while (val != null) { while (val != null) {
tmp += val.offsetTop tmp += val.offsetTop;
val = val.offsetParent val = val.offsetParent;
} }
return tmp return tmp;
}, },
/** /**
* @Description: 找到元素的屏幕位置 * @Description: 找到元素的屏幕位置
* @param dom * @param dom
*/ */
getDomXY(dom) { getDomXY(dom) {
let x = 0 let x = 0;
let y = 0 let y = 0;
if (dom.getBoundingClientRect) { if (dom.getBoundingClientRect) {
let box = dom.getBoundingClientRect(); let box = dom.getBoundingClientRect();
let D = document.documentElement; let D = document.documentElement;
x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; 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 { } else {
while (dom !== document.body) { while (dom !== document.body) {
x += dom.offsetLeft x += dom.offsetLeft;
y += dom.offsetTop y += dom.offsetTop;
dom = dom.offsetParent dom = dom.offsetParent;
} }
} }
return { return {
domX: x, domX: x,
domY: y domY: y,
} };
} },
} },
} };
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.wg-cap-wrap { .wg-cap-wrap {
background: #ffffff; background: var(--el-bg-color);
-webkit-border-radius: 10px; -webkit-border-radius: 10px;
-moz-border-radius: 10px; -moz-border-radius: 10px;
@ -219,14 +222,7 @@ export default {
height: 50px; height: 50px;
width: 100%; width: 100%;
font-size: 15px; font-size: 15px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex; display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center; align-items: center;
span { span {

View File

@ -1,32 +1,9 @@
<template> <template>
<el-dialog <div class="login-dialog w-full p-8">
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-box" v-if="login"> <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"> <div class="block">
<el-input <el-input placeholder="账号" size="large" v-model="data.username" autocomplete="off">
placeholder="账号"
size="large"
v-model="data.username"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Iphone /> <Iphone />
@ -36,14 +13,7 @@
</div> </div>
<div class="block"> <div class="block">
<el-input <el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off">
placeholder="请输入密码(8-16位)"
maxlength="16"
size="large"
v-model="data.password"
show-password
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Lock /> <Lock />
@ -54,67 +24,42 @@
<el-row class="btn-row" :gutter="20"> <el-row class="btn-row" :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-button <el-button class="login-btn" type="primary" size="large" @click="submitLogin"> </el-button>
class="login-btn"
type="primary"
size="large"
@click="submitLogin"
>登录</el-button
>
</el-col> </el-col>
</el-row> </el-row>
<el-row> <div class="w-full">
<el-col :span="12"> <div class="text flex justify-center items-center pt-3 text-sm">
<div class="reg">
还没有账号 还没有账号
<el-button <el-button size="small" @click="login = false">注册</el-button>
type="primary"
class="forget"
size="small"
@click="login = false"
>注册</el-button
>
<el-button <el-button type="info" class="forget" size="small" @click="showResetPass = true">忘记密码</el-button>
type="info" </div>
class="forget" <div v-if="wechatLoginURL !== ''">
size="small" <el-divider>
@click="showResetPass = true" <div class="text-center">其他登录方式</div>
>忘记密码</el-button </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> </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>
</div> </div>
</div> </div>
</el-col>
</el-row>
</el-form> </el-form>
</div> </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-form :model="data" class="form" v-if="enableRegister">
<el-tabs v-model="activeName" class="demo-tabs"> <el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile"> <el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
<div class="block"> <div class="block">
<el-input <el-input placeholder="手机号码" size="large" v-model="data.mobile" maxlength="11" autocomplete="off">
placeholder="手机号码"
size="large"
v-model="data.mobile"
maxlength="11"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Iphone /> <Iphone />
@ -125,13 +70,7 @@
<div class="block"> <div class="block">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12"> <el-col :span="12">
<el-input <el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off">
placeholder="验证码"
size="large"
maxlength="30"
v-model="data.code"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Checked /> <Checked />
@ -140,23 +79,14 @@
</el-input> </el-input>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<send-msg <send-msg size="large" :receiver="data.mobile" type="mobile" />
size="large"
:receiver="data.mobile"
type="mobile"
/>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="邮箱注册" name="email" v-if="enableEmail"> <el-tab-pane label="邮箱注册" name="email" v-if="enableEmail">
<div class="block"> <div class="block">
<el-input <el-input placeholder="邮箱地址" size="large" v-model="data.email" autocomplete="off">
placeholder="邮箱地址"
size="large"
v-model="data.email"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Message /> <Message />
@ -167,13 +97,7 @@
<div class="block"> <div class="block">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12"> <el-col :span="12">
<el-input <el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off">
placeholder="验证码"
size="large"
maxlength="30"
v-model="data.code"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Checked /> <Checked />
@ -189,12 +113,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="用户名注册" name="username" v-if="enableUser"> <el-tab-pane label="用户名注册" name="username" v-if="enableUser">
<div class="block"> <div class="block">
<el-input <el-input placeholder="用户名" size="large" v-model="data.username" autocomplete="off">
placeholder="用户名"
size="large"
v-model="data.username"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Iphone /> <Iphone />
@ -206,14 +125,7 @@
</el-tabs> </el-tabs>
<div class="block"> <div class="block">
<el-input <el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off">
placeholder="请输入密码(8-16位)"
maxlength="16"
size="large"
v-model="data.password"
show-password
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Lock /> <Lock />
@ -223,14 +135,7 @@
</div> </div>
<div class="block"> <div class="block">
<el-input <el-input placeholder="重复密码(8-16位)" size="large" maxlength="16" v-model="data.repass" show-password autocomplete="off">
placeholder="重复密码(8-16位)"
size="large"
maxlength="16"
v-model="data.repass"
show-password
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Lock /> <Lock />
@ -240,12 +145,7 @@
</div> </div>
<div class="block"> <div class="block">
<el-input <el-input placeholder="邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off">
placeholder="邀请码(可选)"
size="large"
v-model="data.invite_code"
autocomplete="off"
>
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Message /> <Message />
@ -254,23 +154,14 @@
</el-input> </el-input>
</div> </div>
<el-row class="btn-row" :gutter="20"> <div class="w-full">
<el-col :span="12"> <el-button class="login-btn w-full" type="primary" size="large" @click="submitRegister"> </el-button>
<el-button </div>
class="login-btn"
type="primary" <div class="text text-sm flex justify-center items-center w-full pt-3">
size="large" 已有账号
@click="submitRegister" <el-button size="small" @click="login = true">登录</el-button>
>注册</el-button
>
</el-col>
<el-col :span="12">
<div class="text">
已有账号
<el-tag @click="login = true">登录</el-tag>
</div> </div>
</el-col>
</el-row>
</el-form> </el-form>
<div class="tip-result" v-else> <div class="tip-result" v-else>
@ -291,11 +182,10 @@
</el-row> </el-row>
</div> </div>
</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" /> <reset-pass @hide="showResetPass = false" :show="showResetPass" />
</el-dialog> </div>
</template> </template>
<script setup> <script setup>
@ -316,7 +206,7 @@ import { useSharedStore } from "@/store/sharedata";
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const props = defineProps({ const props = defineProps({
show: Boolean show: Boolean,
}); });
const showDialog = ref(false); const showDialog = ref(false);
watch( watch(
@ -334,7 +224,7 @@ const data = ref({
email: "", email: "",
repass: "", repass: "",
code: "", code: "",
invite_code: "" invite_code: "",
}); });
const enableMobile = ref(false); const enableMobile = ref(false);
const enableEmail = ref(false); const enableEmail = ref(false);
@ -351,8 +241,6 @@ const enableVerify = ref(false);
const showResetPass = ref(false); const showResetPass = ref(false);
const router = useRouter(); const router = useRouter();
const store = useSharedStore(); const store = useSharedStore();
//
const needVerify = ref(false);
onMounted(() => { onMounted(() => {
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`; const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`;
@ -410,7 +298,7 @@ const submitLogin = () => {
if (data.value.password === "") { if (data.value.password === "") {
return ElMessage.error("请输入密码"); return ElMessage.error("请输入密码");
} }
if (enableVerify.value && needVerify.value) { if (enableVerify.value) {
captchaRef.value.loadCaptcha(); captchaRef.value.loadCaptcha();
action.value = "login"; action.value = "login";
} else { } else {
@ -429,11 +317,9 @@ const doLogin = (verifyData) => {
ElMessage.success("登录成功!"); ElMessage.success("登录成功!");
emits("hide"); emits("hide");
emits("success"); emits("success");
needVerify.value = false;
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("登录失败," + e.message); ElMessage.error("登录失败," + e.message);
needVerify.value = true;
}); });
}; };
@ -458,10 +344,7 @@ const submitRegister = () => {
return ElMessage.error("两次输入密码不一致"); return ElMessage.error("两次输入密码不一致");
} }
if ( if ((activeName.value === "mobile" || activeName.value === "email") && data.value.code === "") {
(activeName.value === "mobile" || activeName.value === "email") &&
data.value.code === ""
) {
return ElMessage.error("请输入验证码"); return ElMessage.error("请输入验证码");
} }
if (enableVerify.value && activeName.value === "username") { if (enableVerify.value && activeName.value === "username") {
@ -486,51 +369,25 @@ const doRegister = (verifyData) => {
emits("hide"); emits("hide");
emits("success"); emits("success");
}, },
duration: 1000 duration: 1000,
}); });
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("注册失败," + e.message); ElMessage.error("注册失败," + e.message);
}); });
}; };
const close = function () {
emits("hide", false);
login.value = true;
};
</script> </script>
<style lang="stylus"> <style lang="stylus">
.login-dialog { .login-dialog {
border-radius 10px border-radius 10px
max-width 600px
.header { .el-tabs__nav {
position relative display flex
width 100%
.title { justify-content space-between
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-dialog__body {
padding 10px 20px 20px 20px
}
.form { .form {
.block { .block {
@ -541,6 +398,7 @@ const close = function () {
display flex display flex
.login-btn { .login-btn {
font-size 16px
width 100% width 100%
} }
@ -566,7 +424,6 @@ const close = function () {
align-items: center; align-items: center;
} }
.login-type { .login-type {
padding 15px
display flex display flex
justify-content center justify-content center
@ -582,14 +439,8 @@ const close = function () {
} }
} }
.reg { .text {
height 50px color var(--el-text-color-primary)
display flex
align-items center
.el-button {
margin-left 10px
}
} }
} }

View File

@ -1,13 +1,6 @@
<template> <template>
<div class="reset-pass"> <div class="reset-pass">
<el-dialog <el-dialog v-model="showDialog" :close-on-click-modal="true" width="500px" :before-close="close" :title="title" class="reset-pass-dialog">
v-model="showDialog"
:close-on-click-modal="true"
width="540px"
:before-close="close"
:title="title"
class="reset-pass-dialog"
>
<div class="form"> <div class="form">
<el-form :model="form" label-width="80px" label-position="left"> <el-form :model="form" label-width="80px" label-position="left">
<el-tabs v-model="form.type" class="demo-tabs"> <el-tabs v-model="form.type" class="demo-tabs">
@ -16,14 +9,10 @@
<el-input v-model="form.mobile" placeholder="请输入手机号" /> <el-input v-model="form.mobile" placeholder="请输入手机号" />
</el-form-item> </el-form-item>
<el-form-item label="验证码"> <el-form-item label="验证码">
<el-row class="code-row"> <div class="flex">
<el-col :span="16"> <el-input v-model="form.code" maxlength="6" class="mr-2 w-1/2" />
<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" /> <send-msg size="" :receiver="form.mobile" type="mobile" />
</el-col> </div>
</el-row>
</el-form-item> </el-form-item>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="邮箱验证" name="email"> <el-tab-pane label="邮箱验证" name="email">
@ -31,14 +20,10 @@
<el-input v-model="form.email" placeholder="请输入邮箱地址" /> <el-input v-model="form.email" placeholder="请输入邮箱地址" />
</el-form-item> </el-form-item>
<el-form-item label="验证码"> <el-form-item label="验证码">
<el-row class="code-row"> <div class="flex">
<el-col :span="16"> <el-input v-model="form.code" maxlength="6" class="mr-2 w-1/2" />
<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" /> <send-msg size="" :receiver="form.email" type="email" />
</el-col> </div>
</el-row>
</el-form-item> </el-form-item>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@ -70,7 +55,7 @@ import { validateEmail, validateMobile } from "@/utils/validate";
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
mobile: String mobile: String,
}); });
const showDialog = computed(() => { const showDialog = computed(() => {
@ -84,7 +69,7 @@ const form = ref({
type: "mobile", type: "mobile",
code: "", code: "",
password: "", password: "",
repass: "" repass: "",
}); });
const emits = defineEmits(["hide"]); const emits = defineEmits(["hide"]);
@ -105,7 +90,7 @@ const save = () => {
ElMessage.success({ ElMessage.success({
message: "重置密码成功", message: "重置密码成功",
duration: 1000, duration: 1000,
onClose: () => emits("hide", false) onClose: () => emits("hide", false),
}); });
}) })
.catch((e) => { .catch((e) => {

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,11 +1,6 @@
<template> <template>
<el-container class="send-verify-code"> <el-container class="send-verify-code">
<el-button <el-button type="success" :size="props.size" :disabled="!canSend" @click="sendMsg">
type="success"
:size="props.size"
:disabled="!canSend"
@click="sendMsg"
>
{{ btnText }} {{ btnText }}
</el-button> </el-button>
@ -18,7 +13,7 @@
import { ref } from "vue"; import { ref } from "vue";
import { validateEmail, validateMobile } from "@/utils/validate"; import { validateEmail, validateMobile } from "@/utils/validate";
import { httpPost } from "@/utils/http"; import { httpPost } from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog"; import { ElMessage } from "element-plus";
import Captcha from "@/components/Captcha.vue"; import Captcha from "@/components/Captcha.vue";
import { getSystemInfo } from "@/store/cache"; import { getSystemInfo } from "@/store/cache";
@ -28,8 +23,8 @@ const props = defineProps({
size: String, size: String,
type: { type: {
type: String, type: String,
default: "mobile" default: "mobile",
} },
}); });
const btnText = ref("发送验证码"); const btnText = ref("发送验证码");
const canSend = ref(true); const canSend = ref(true);
@ -42,10 +37,10 @@ getSystemInfo().then((res) => {
const sendMsg = () => { const sendMsg = () => {
if (!validateMobile(props.receiver) && props.type === "mobile") { if (!validateMobile(props.receiver) && props.type === "mobile") {
return showMessageError("请输入合法的手机号"); return ElMessage.error("请输入合法的手机号");
} }
if (!validateEmail(props.receiver) && props.type === "email") { if (!validateEmail(props.receiver) && props.type === "email") {
return showMessageError("请输入合法的邮箱地址"); return ElMessage.error("请输入合法的邮箱地址");
} }
if (enableVerify.value) { if (enableVerify.value) {
@ -65,10 +60,10 @@ const doSendMsg = (data) => {
receiver: props.receiver, receiver: props.receiver,
key: data.key, key: data.key,
dots: data.dots, dots: data.dots,
x: data.x x: data.x,
}) })
.then(() => { .then(() => {
showMessageOK("验证码发送成功"); ElMessage.success("验证码发送成功");
let time = 60; let time = 60;
btnText.value = time; btnText.value = time;
const handler = setInterval(() => { const handler = setInterval(() => {
@ -84,7 +79,7 @@ const doSendMsg = (data) => {
}) })
.catch((e) => { .catch((e) => {
canSend.value = true; canSend.value = true;
showMessageError("验证码发送失败:" + e.message); ElMessage.error("验证码发送失败:" + e.message);
}); });
}; };
</script> </script>

View File

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

View File

@ -1,7 +1,7 @@
<template> <template>
<div :class="'sidebar ' + theme"> <div :class="'sidebar ' + theme">
<a class="logo w-full" href="/" target="_blank"> <a class="logo w-full flex items-center" href="/" target="_blank">
<el-image :src="logo" /> <img :src="logo" style="height: 36px" />
<span class="text" v-show="!sidebar.collapse">{{ title }}</span> <span class="text" v-show="!sidebar.collapse">{{ title }}</span>
</a> </a>

View File

@ -12,7 +12,7 @@ const routes = [
name: "Index", name: "Index",
path: "/", path: "/",
meta: { title: "首页" }, meta: { title: "首页" },
component: () => import("@/views/Index.vue") component: () => import("@/views/Index.vue"),
}, },
{ {
name: "home", name: "home",
@ -24,136 +24,136 @@ const routes = [
name: "chat", name: "chat",
path: "/chat", path: "/chat",
meta: { title: "创作中心" }, meta: { title: "创作中心" },
component: () => import("@/views/ChatPlus.vue") component: () => import("@/views/ChatPlus.vue"),
}, },
{ {
name: "chat-id", name: "chat-id",
path: "/chat/:id", path: "/chat/:id",
meta: { title: "创作中心" }, meta: { title: "创作中心" },
component: () => import("@/views/ChatPlus.vue") component: () => import("@/views/ChatPlus.vue"),
}, },
{ {
name: "image-mj", name: "image-mj",
path: "/mj", path: "/mj",
meta: { title: "MidJourney 绘画中心" }, meta: { title: "MidJourney 绘画中心" },
component: () => import("@/views/ImageMj.vue") component: () => import("@/views/ImageMj.vue"),
}, },
{ {
name: "image-sd", name: "image-sd",
path: "/sd", path: "/sd",
meta: { title: "stable diffusion 绘画中心" }, meta: { title: "stable diffusion 绘画中心" },
component: () => import("@/views/ImageSd.vue") component: () => import("@/views/ImageSd.vue"),
}, },
{ {
name: "member", name: "member",
path: "/member", path: "/member",
meta: { title: "会员充值中心" }, meta: { title: "会员充值中心" },
component: () => import("@/views/Member.vue") component: () => import("@/views/Member.vue"),
}, },
{ {
name: "chat-app", name: "chat-app",
path: "/apps", path: "/apps",
meta: { title: "应用中心" }, meta: { title: "应用中心" },
component: () => import("@/views/ChatApps.vue") component: () => import("@/views/ChatApps.vue"),
}, },
{ {
name: "images", name: "images",
path: "/images-wall", path: "/images-wall",
meta: { title: "作品展示" }, meta: { title: "作品展示" },
component: () => import("@/views/ImagesWall.vue") component: () => import("@/views/ImagesWall.vue"),
}, },
{ {
name: "user-invitation", name: "user-invitation",
path: "/invite", path: "/invite",
meta: { title: "推广计划" }, meta: { title: "推广计划" },
component: () => import("@/views/Invitation.vue") component: () => import("@/views/Invitation.vue"),
}, },
{ {
name: "powerLog", name: "powerLog",
path: "/powerLog", path: "/powerLog",
meta: { title: "消费日志" }, meta: { title: "消费日志" },
component: () => import("@/views/PowerLog.vue") component: () => import("@/views/PowerLog.vue"),
}, },
{ {
name: "xmind", name: "xmind",
path: "/xmind", path: "/xmind",
meta: { title: "思维导图" }, meta: { title: "思维导图" },
component: () => import("@/views/MarkMap.vue") component: () => import("@/views/MarkMap.vue"),
}, },
{ {
name: "dalle", name: "dalle",
path: "/dalle", path: "/dalle",
meta: { title: "DALLE-3" }, meta: { title: "DALLE-3" },
component: () => import("@/views/Dalle.vue") component: () => import("@/views/Dalle.vue"),
}, },
{ {
name: "suno", name: "suno",
path: "/suno", path: "/suno",
meta: { title: "Suno音乐创作" }, meta: { title: "Suno音乐创作" },
component: () => import("@/views/Suno.vue") component: () => import("@/views/Suno.vue"),
}, },
{ {
name: "ExternalLink", name: "ExternalLink",
path: "/external", path: "/external",
component: () => import("@/views/ExternalPage.vue") component: () => import("@/views/ExternalPage.vue"),
}, },
{ {
name: "song", name: "song",
path: "/song/:id", path: "/song/:id",
meta: { title: "Suno音乐播放" }, meta: { title: "Suno音乐播放" },
component: () => import("@/views/Song.vue") component: () => import("@/views/Song.vue"),
}, },
{ {
name: "luma", name: "luma",
path: "/luma", path: "/luma",
meta: { title: "Luma视频创作" }, meta: { title: "Luma视频创作" },
component: () => import("@/views/Luma.vue") component: () => import("@/views/Luma.vue"),
} },
] ],
}, },
{ {
name: "chat-export", name: "chat-export",
path: "/chat/export", path: "/chat/export",
meta: { title: "导出会话记录" }, meta: { title: "导出会话记录" },
component: () => import("@/views/ChatExport.vue") component: () => import("@/views/ChatExport.vue"),
}, },
{ {
name: "login", name: "login",
path: "/login", path: "/login",
meta: { title: "用户登录" }, meta: { title: "用户登录" },
component: () => import("@/views/Login.vue") component: () => import("@/views/Login.vue"),
}, },
{ {
name: "login-callback", name: "login-callback",
path: "/login/callback", path: "/login/callback",
meta: { title: "用户登录" }, meta: { title: "用户登录" },
component: () => import("@/views/LoginCallback.vue") component: () => import("@/views/LoginCallback.vue"),
}, },
{ {
name: "register", name: "register",
path: "/register", path: "/register",
meta: { title: "用户注册" }, meta: { title: "用户注册" },
component: () => import("@/views/Register.vue") component: () => import("@/views/Register.vue"),
}, },
{ {
name: "resetpassword", name: "resetpassword",
path: "/resetpassword", path: "/resetpassword",
meta: { title: "重置密码" }, meta: { title: "重置密码" },
component: () => import("@/views/Resetpassword.vue") component: () => import("@/views/Resetpassword.vue"),
}, },
{ {
path: "/admin/login", path: "/admin/login",
name: "admin-login", name: "admin-login",
meta: { title: "控制台登录" }, meta: { title: "控制台登录" },
component: () => import("@/views/admin/Login.vue") component: () => import("@/views/admin/Login.vue"),
}, },
{ {
path: "/payReturn", path: "/payReturn",
name: "pay-return", name: "pay-return",
meta: { title: "支付回调" }, meta: { title: "支付回调" },
component: () => import("@/views/PayReturn.vue") component: () => import("@/views/PayReturn.vue"),
}, },
{ {
name: "admin", name: "admin",
@ -166,118 +166,112 @@ const routes = [
path: "/admin/dashboard", path: "/admin/dashboard",
name: "admin-dashboard", name: "admin-dashboard",
meta: { title: "仪表盘" }, meta: { title: "仪表盘" },
component: () => import("@/views/admin/Dashboard.vue") component: () => import("@/views/admin/Dashboard.vue"),
}, },
{ {
path: "/admin/system", path: "/admin/system",
name: "admin-system", name: "admin-system",
meta: { title: "系统设置" }, meta: { title: "系统设置" },
component: () => import("@/views/admin/SysConfig.vue") component: () => import("@/views/admin/SysConfig.vue"),
}, },
{ {
path: "/admin/user", path: "/admin/user",
name: "admin-user", name: "admin-user",
meta: { title: "用户管理" }, meta: { title: "用户管理" },
component: () => import("@/views/admin/Users.vue") component: () => import("@/views/admin/Users.vue"),
}, },
{ {
path: "/admin/app", path: "/admin/app",
name: "admin-app", name: "admin-app",
meta: { title: "应用列表" }, meta: { title: "应用列表" },
component: () => import("@/views/admin/Apps.vue") component: () => import("@/views/admin/Apps.vue"),
}, },
{ {
path: "/admin/app/type", path: "/admin/app/type",
name: "admin-app-type", name: "admin-app-type",
meta: { title: "应用分类" }, meta: { title: "应用分类" },
component: () => import("@/views/admin/AppType.vue") component: () => import("@/views/admin/AppType.vue"),
}, },
{ {
path: "/admin/apikey", path: "/admin/apikey",
name: "admin-apikey", name: "admin-apikey",
meta: { title: "API-KEY 管理" }, meta: { title: "API-KEY 管理" },
component: () => import("@/views/admin/ApiKey.vue") component: () => import("@/views/admin/ApiKey.vue"),
}, },
{ {
path: "/admin/chat/model", path: "/admin/chat/model",
name: "admin-chat-model", name: "admin-chat-model",
meta: { title: "语言模型" }, meta: { title: "语言模型" },
component: () => import("@/views/admin/ChatModel.vue") component: () => import("@/views/admin/ChatModel.vue"),
}, },
{ {
path: "/admin/product", path: "/admin/product",
name: "admin-product", name: "admin-product",
meta: { title: "充值产品" }, meta: { title: "充值产品" },
component: () => import("@/views/admin/Product.vue") component: () => import("@/views/admin/Product.vue"),
}, },
{ {
path: "/admin/order", path: "/admin/order",
name: "admin-order", name: "admin-order",
meta: { title: "充值订单" }, meta: { title: "充值订单" },
component: () => import("@/views/admin/Order.vue") component: () => import("@/views/admin/Order.vue"),
}, },
{ {
path: "/admin/redeem", path: "/admin/redeem",
name: "admin-redeem", name: "admin-redeem",
meta: { title: "兑换码管理" }, meta: { title: "兑换码管理" },
component: () => import("@/views/admin/Redeem.vue") component: () => import("@/views/admin/Redeem.vue"),
}, },
{ {
path: "/admin/loginLog", path: "/admin/loginLog",
name: "admin-loginLog", name: "admin-loginLog",
meta: { title: "登录日志" }, meta: { title: "登录日志" },
component: () => import("@/views/admin/LoginLog.vue") component: () => import("@/views/admin/LoginLog.vue"),
}, },
{ {
path: "/admin/functions", path: "/admin/functions",
name: "admin-functions", name: "admin-functions",
meta: { title: "函数管理" }, meta: { title: "函数管理" },
component: () => import("@/views/admin/Functions.vue") component: () => import("@/views/admin/Functions.vue"),
}, },
{ {
path: "/admin/chats", path: "/admin/chats",
name: "admin-chats", name: "admin-chats",
meta: { title: "对话管理" }, meta: { title: "对话管理" },
component: () => import("@/views/admin/ChatList.vue") component: () => import("@/views/admin/ChatList.vue"),
}, },
{ {
path: "/admin/images", path: "/admin/images",
name: "admin-images", name: "admin-images",
meta: { title: "绘图管理" }, meta: { title: "绘图管理" },
component: () => import("@/views/admin/ImageList.vue") component: () => import("@/views/admin/ImageList.vue"),
}, },
{ {
path: "/admin/medias", path: "/admin/medias",
name: "admin-medias", name: "admin-medias",
meta: { title: "音视频管理" }, meta: { title: "音视频管理" },
component: () => import("@/views/admin/Medias.vue") component: () => import("@/views/admin/Medias.vue"),
}, },
{ {
path: "/admin/powerLog", path: "/admin/powerLog",
name: "admin-power-log", name: "admin-power-log",
meta: { title: "算力日志" }, meta: { title: "算力日志" },
component: () => import("@/views/admin/PowerLog.vue") component: () => import("@/views/admin/PowerLog.vue"),
}, },
{ {
path: "/admin/manger", path: "/admin/manger",
name: "admin-manger", name: "admin-manger",
meta: { title: "管理员" }, meta: { title: "管理员" },
component: () => import("@/views/admin/Manager.vue") component: () => import("@/views/admin/Manager.vue"),
} },
] ],
}, },
{ {
name: "mobile-login", name: "mobile-login",
path: "/mobile/login", path: "/mobile/login",
meta: { title: "用户登录" }, meta: { title: "用户登录" },
component: () => import("@/views/Login.vue") component: () => import("@/views/mobile/Login.vue"),
},
{
name: "mobile-register",
path: "/mobile/register",
meta: { title: "用户注册" },
component: () => import("@/views/Register.vue")
}, },
{ {
name: "mobile", name: "mobile",
@ -289,65 +283,65 @@ const routes = [
{ {
path: "/mobile/index", path: "/mobile/index",
name: "mobile-index", name: "mobile-index",
component: () => import("@/views/mobile/Index.vue") component: () => import("@/views/mobile/Index.vue"),
}, },
{ {
path: "/mobile/chat", path: "/mobile/chat",
name: "mobile-chat", name: "mobile-chat",
component: () => import("@/views/mobile/ChatList.vue") component: () => import("@/views/mobile/ChatList.vue"),
}, },
{ {
path: "/mobile/image", path: "/mobile/image",
name: "mobile-image", name: "mobile-image",
component: () => import("@/views/mobile/Image.vue") component: () => import("@/views/mobile/Image.vue"),
}, },
{ {
path: "/mobile/profile", path: "/mobile/profile",
name: "mobile-profile", name: "mobile-profile",
component: () => import("@/views/mobile/Profile.vue") component: () => import("@/views/mobile/Profile.vue"),
}, },
{ {
path: "/mobile/imgWall", path: "/mobile/imgWall",
name: "mobile-img-wall", name: "mobile-img-wall",
component: () => import("@/views/mobile/pages/ImgWall.vue") component: () => import("@/views/mobile/pages/ImgWall.vue"),
}, },
{ {
path: "/mobile/chat/session", path: "/mobile/chat/session",
name: "mobile-chat-session", name: "mobile-chat-session",
component: () => import("@/views/mobile/ChatSession.vue") component: () => import("@/views/mobile/ChatSession.vue"),
}, },
{ {
path: "/mobile/chat/export", path: "/mobile/chat/export",
name: "mobile-chat-export", name: "mobile-chat-export",
component: () => import("@/views/mobile/ChatExport.vue") component: () => import("@/views/mobile/ChatExport.vue"),
} },
] ],
}, },
{ {
name: "test", name: "test",
path: "/test", path: "/test",
meta: { title: "测试页面" }, meta: { title: "测试页面" },
component: () => import("@/views/Test.vue") component: () => import("@/views/Test.vue"),
}, },
{ {
name: "test2", name: "test2",
path: "/test2", path: "/test2",
meta: { title: "测试页面" }, meta: { title: "测试页面" },
component: () => import("@/views/RealtimeTest.vue") component: () => import("@/views/RealtimeTest.vue"),
}, },
{ {
name: "NotFound", name: "NotFound",
path: "/:all(.*)", path: "/:all(.*)",
meta: { title: "页面没有找到" }, meta: { title: "页面没有找到" },
component: () => import("@/views/404.vue") component: () => import("@/views/404.vue"),
} },
]; ];
// console.log(MY_VARIABLE) // console.log(MY_VARIABLE)
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: routes routes: routes,
}); });
let prevRoute = null; let prevRoute = null;

View File

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

View File

@ -9,8 +9,10 @@
</div> </div>
<div class="flex" :class="{ 'top-collapse': !isCollapse }"> <div class="flex" :class="{ 'top-collapse': !isCollapse }">
<div class="top-avatar flex"> <div class="top-avatar flex">
<span class="title" v-if="!isCollapse">GeekAI</span> <span class="title" v-if="!isCollapse">{{ title }}</span>
<img v-if="loginUser.id" :src="!!loginUser.avatar ? loginUser.avatar : avatarImg" alt="" :class="{ marr: !isCollapse }" /> <el-tooltip :content="title" placement="right">
<img :src="logo" alt="" :class="{ marr: !isCollapse }" v-if="isCollapse" />
</el-tooltip>
</div> </div>
<div class="menuIcon xxx" @click="isCollapse = !isCollapse"> <div class="menuIcon xxx" @click="isCollapse = !isCollapse">
<el-tooltip content="关闭菜单" placement="right" v-if="!isCollapse"> <el-tooltip content="关闭菜单" placement="right" v-if="!isCollapse">
@ -80,6 +82,9 @@
</template> </template>
<template #default> <template #default>
<ul class="more-menus setting-menus"> <ul class="more-menus setting-menus">
<li>
<img :src="loginUser.avatar ? loginUser.avatar : avatarImg" />
</li>
<li> <li>
<div @click="showConfigDialog = true" class="flex"> <div @click="showConfigDialog = true" class="flex">
<el-icon> <el-icon>
@ -113,12 +118,6 @@
</div> </div>
</div> </div>
<el-scrollbar class="right-main"> <el-scrollbar class="right-main">
<div
v-if="loginUser.id === undefined || !loginUser.id"
class="loginMask"
:style="{ left: isCollapse ? '65px' : '170px' }"
@click="showNoticeLogin = true"
></div>
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id"> <div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button> <el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button>
</div> </div>
@ -132,12 +131,14 @@
<!-- </div> --> <!-- </div> -->
</el-scrollbar> </el-scrollbar>
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false" /> <config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false" />
<el-dialog v-model="showNoticeLogin">
<el-result icon="warning" title="未登录" sub-title="登录后解锁功能"> <el-dialog v-model="showLoginDialog" width="500px" @close="store.setShowLoginDialog(false)">
<template #extra> <template #header>
<el-button type="primary" @click="router.push('/login')">登录</el-button> <div class="text-center text-xl" style="color: var(--theme-text-color-primary)">登录后解锁功能</div>
</template> </template>
</el-result> <div class="p-4 pt-2 pb-2">
<LoginDialog @success="loginSuccess" @hide="store.setShowLoginDialog(false)" />
</div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@ -145,7 +146,6 @@
<script setup> <script setup>
import { CirclePlus, Setting, UserFilled } from "@element-plus/icons-vue"; import { CirclePlus, Setting, UserFilled } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue"; import ThemeChange from "@/components/ThemeChange.vue";
import avatarImg from "@/assets/img/avatar.jpg";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http"; import { httpGet } from "@/utils/http";
@ -155,6 +155,8 @@ import { removeUserToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue"; import ConfigDialog from "@/components/UserInfoDialog.vue";
import { showMessageError } from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
import LoginDialog from "@/components/LoginDialog.vue";
import { substr } from "@/utils/libs";
const isCollapse = ref(true); const isCollapse = ref(true);
const router = useRouter(); const router = useRouter();
@ -164,7 +166,14 @@ const moreNavs = ref([]);
const curPath = ref(); const curPath = ref();
const title = ref(""); const title = ref("");
const showNoticeLogin = ref(false); const avatarImg = ref("/images/avatar/default.jpg");
const store = useSharedStore();
const loginUser = ref({});
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const showLoginDialog = ref(false);
/** /**
* 从路径名中提取第一个路径段 * 从路径名中提取第一个路径段
@ -190,18 +199,11 @@ const getFirstPathSegment = (url) => {
return null; return null;
} }
}; };
const loginUser = ref({});
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const store = useSharedStore();
const show = ref(false);
watch( watch(
() => store.showLoginDialog, () => store.showLoginDialog,
(newValue) => { (newValue) => {
show.value = newValue; showLoginDialog.value = newValue;
} }
); );
@ -216,7 +218,6 @@ if (curPath.value === "/external") {
} }
const changeNav = (item) => { const changeNav = (item) => {
curPath.value = item.url; curPath.value = item.url;
console.log(item.url);
if (item.url.indexOf("http") !== -1) { if (item.url.indexOf("http") !== -1) {
// //
router.push({ path: "/external", query: { url: item.url, title: item.name } }); router.push({ path: "/external", query: { url: item.url, title: item.name } });
@ -280,16 +281,19 @@ const logout = function () {
httpGet("/api/user/logout") httpGet("/api/user/logout")
.then(() => { .then(() => {
removeUserToken(); removeUserToken();
store.setShowLoginDialog(true); router.push("/login");
store.setIsLogin(false);
loginUser.value = {};
//
routerViewKey.value += 1;
}) })
.catch(() => { .catch(() => {
ElMessage.error("注销失败!"); ElMessage.error("注销失败!");
}); });
}; };
const loginSuccess = () => {
init();
store.setShowLoginDialog(false);
//
routerViewKey.value += 1;
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@ -311,110 +311,7 @@
</div> </div>
<!-- 任务详情弹框 --> <!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true"> <sd-task-view v-model="showTaskDialog" :data="item" @drawSame="copyParams" @close="showTaskDialog = false" />
<el-row :gutter="20">
<el-col :span="16">
<div class="img-container" :style="{ maxHeight: fullImgHeight + 'px' }">
<el-image :src="item['img_url']" fit="contain" />
</div>
</el-col>
<el-col :span="8">
<div class="task-info">
<div class="info-line">
<el-divider> 正向提示词 </el-divider>
<div class="prompt">
<span>{{ item.prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<el-divider> 反向提示词 </el-divider>
<div class="prompt">
<span>{{ item.params.neg_prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.neg_prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>采样方法</label>
<div class="item-value">{{ item.params.sampler }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>图片尺寸</label>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.steps }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>引导系数</label>
<div class="item-value">{{ item.params.cfg_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>随机因子</label>
<div class="item-value">{{ item.params.seed }}</div>
</div>
</div>
<div v-if="item.params.hd_fix">
<el-divider> 高清修复 </el-divider>
<div class="info-line">
<div class="wrapper">
<label>重绘幅度</label>
<div class="item-value">
{{ item.params.hd_redraw_rate }}
</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大算法</label>
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大倍数</label>
<div class="item-value">{{ item.params.hd_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.hd_steps }}</div>
</div>
</div>
</div>
<div class="copy-params">
<el-button type="primary" round @click="copyParams(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
</div> </div>
</div> </div>
</template> </template>
@ -434,7 +331,7 @@ import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import { showMessageError } from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
import SdTaskView from "@/components/SdTaskView.vue";
const listBoxHeight = ref(0); const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0) // const paramBoxHeight = ref(0)
const fullImgHeight = ref(window.innerHeight - 60); const fullImgHeight = ref(window.innerHeight - 60);

View File

@ -3,7 +3,7 @@
<div class="inner custom-scroll"> <div class="inner custom-scroll">
<div class="header"> <div class="header">
<h2 class="text-xl pt-4 pb-4">AI 绘画作品墙</h2> <h2 class="text-xl pt-4 pb-4">AI 绘画作品墙</h2>
<div class="settings"> <div class="settings pr-14">
<el-radio-group v-model="imgType" @change="changeImgType"> <el-radio-group v-model="imgType" @change="changeImgType">
<el-radio value="mj" size="large">MidJourney</el-radio> <el-radio value="mj" size="large">MidJourney</el-radio>
<el-radio value="sd" size="large">Stable Diffusion</el-radio> <el-radio value="sd" size="large">Stable Diffusion</el-radio>
@ -161,120 +161,7 @@
<!-- end of waterfall --> <!-- end of waterfall -->
</div> </div>
<!-- 任务详情弹框 --> <!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true"> <sd-task-view v-model="showTaskDialog" :data="item" @drawSame="drawSameSd" @close="showTaskDialog = false" />
<el-row :gutter="20">
<el-col :span="16">
<div class="img-container" :style="{ maxHeight: fullImgHeight + 'px' }">
<el-image :src="item['img_url']" fit="contain">
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
</el-col>
<el-col :span="8">
<div class="task-info">
<div class="info-line">
<el-divider> 正向提示词 </el-divider>
<div class="prompt">
<span>{{ item.prompt }}</span>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<el-divider> 反向提示词 </el-divider>
<div class="prompt">
<span>{{ item.params.negative_prompt }}</span>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.params.negative_prompt">
<DocumentCopy />
</el-icon>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>采样方法</label>
<div class="item-value">{{ item.params.sampler }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>图片尺寸</label>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.steps }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>引导系数</label>
<div class="item-value">{{ item.params.cfg_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>随机因子</label>
<div class="item-value">{{ item.params.seed }}</div>
</div>
</div>
<div v-if="item.params.hd_fix">
<el-divider> 高清修复 </el-divider>
<div class="info-line">
<div class="wrapper">
<label>重绘幅度</label>
<div class="item-value">{{ item.params.hd_redraw_rate }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大算法</label>
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>放大倍数</label>
<div class="item-value">{{ item.params.hd_scale }}</div>
</div>
</div>
<div class="info-line">
<div class="wrapper">
<label>迭代步数</label>
<div class="item-value">{{ item.params.hd_steps }}</div>
</div>
</div>
</div>
<div class="copy-params">
<el-button type="primary" round @click="drawSameSd(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
</div> </div>
</template> </template>
@ -287,7 +174,7 @@ import { ElMessage } from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import SdTaskView from "@/components/SdTaskView.vue";
const data = ref({ const data = ref({
mj: [], mj: [],
sd: [], sd: [],
@ -298,7 +185,6 @@ const isOver = ref(false);
const imgType = ref("mj"); // const imgType = ref("mj"); //
const listBoxHeight = window.innerHeight - 124; const listBoxHeight = window.innerHeight - 124;
const colWidth = ref(220); const colWidth = ref(220);
const fullImgHeight = ref(window.innerHeight - 60);
const showTaskDialog = ref(false); const showTaskDialog = ref(false);
const item = ref({}); const item = ref({});

View File

@ -6,9 +6,7 @@
<el-menu mode="horizontal" :ellipsis="false"> <el-menu mode="horizontal" :ellipsis="false">
<div class="menu-item"> <div class="menu-item">
<!-- <el-image :src="logo" class="logo" alt="Geek-AI" /> --> <!-- <el-image :src="logo" class="logo" alt="Geek-AI" /> -->
<div class="logo-box"> <img :src="logo" class="logo" alt="" />
<img src="@/assets/img/logo.png" alt="" />
</div>
</div> </div>
<div class="menu-item"> <div class="menu-item">
<span v-if="!license.de_copy"> <span v-if="!license.de_copy">

View File

@ -5,12 +5,7 @@
<AccountTop> <AccountTop>
<template #default> <template #default>
<div class="wechatLog flex-center" v-if="wechatLoginURL !== ''"> <div class="wechatLog flex-center" v-if="wechatLoginURL !== ''">
<a <a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"> <i class="iconfont icon-wechat"></i>使用微信登录 </a>
:href="wechatLoginURL"
@click="setRoute(router.currentRoute.value.path)"
>
<i class="iconfont icon-wechat"></i>使用微信登录
</a>
</div> </div>
</template> </template>
</AccountTop> </AccountTop>
@ -19,47 +14,26 @@
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules"> <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules">
<el-form-item label="" prop="username"> <el-form-item label="" prop="username">
<div class="form-title">账号</div> <div class="form-title">账号</div>
<el-input <el-input v-model="ruleForm.username" size="large" placeholder="请输入账号" @keyup="handleKeyup" />
v-model="ruleForm.username"
size="large"
placeholder="请输入账号"
@keyup="handleKeyup"
/>
</el-form-item> </el-form-item>
<el-form-item label="" prop="password"> <el-form-item label="" prop="password">
<div class="flex-between w100"> <div class="flex-between w100">
<div class="form-title">密码</div> <div class="form-title">密码</div>
<div <div class="form-forget text-color-primary" @click="router.push('/resetpassword')">忘记密码</div>
class="form-forget text-color-primary"
@click="router.push('/resetpassword')"
>
忘记密码
</div>
</div> </div>
<el-input <el-input size="large" v-model="ruleForm.password" placeholder="请输入密码" show-password autocomplete="off" @keyup="handleKeyup" />
size="large"
v-model="ruleForm.password"
placeholder="请输入密码"
show-password
autocomplete="off"
@keyup="handleKeyup"
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
class="login-btn"
size="large"
type="primary"
@click="login"
>登录</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</div> </div>
</div> </div>
<account-bg /> <account-bg />
<captcha v-if="enableVerify" @success="doLogin" ref="captchaRef" />
</div> </div>
</template> </template>
@ -76,6 +50,7 @@ import { setRoute } from "@/store/system";
import { useSharedStore } from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
import AccountTop from "@/components/AccountTop.vue"; import AccountTop from "@/components/AccountTop.vue";
import Captcha from "@/components/Captcha.vue";
const router = useRouter(); const router = useRouter();
const title = ref("Geek-AI"); const title = ref("Geek-AI");
@ -87,16 +62,14 @@ const licenseConfig = ref({});
const wechatLoginURL = ref(""); const wechatLoginURL = ref("");
const enableVerify = ref(false); const enableVerify = ref(false);
const captchaRef = ref(null); const captchaRef = ref(null);
//
const needVerify = ref(false);
const ruleFormRef = ref(null); const ruleFormRef = ref(null);
const ruleForm = reactive({ const ruleForm = reactive({
username: process.env.VUE_APP_USER, username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS password: process.env.VUE_APP_PASS,
}); });
const rules = { const rules = {
username: [{ required: true, trigger: "blur", message: "请输入账号" }], username: [{ required: true, trigger: "blur", message: "请输入账号" }],
password: [{ required: true, trigger: "blur", message: "请输入密码" }] password: [{ required: true, trigger: "blur", message: "请输入密码" }],
}; };
onMounted(() => { onMounted(() => {
// //
@ -147,7 +120,7 @@ const handleKeyup = (e) => {
const login = async function () { const login = async function () {
await ruleFormRef.value.validate(async (valid) => { await ruleFormRef.value.validate(async (valid) => {
if (valid) { if (valid) {
if (enableVerify.value && needVerify.value) { if (enableVerify.value) {
captchaRef.value.loadCaptcha(); captchaRef.value.loadCaptcha();
} else { } else {
doLogin({}); doLogin({});
@ -163,12 +136,11 @@ const doLogin = (verifyData) => {
password: password.value.trim(), password: password.value.trim(),
key: verifyData.key, key: verifyData.key,
dots: verifyData.dots, dots: verifyData.dots,
x: verifyData.x x: verifyData.x,
}) })
.then((res) => { .then((res) => {
setUserToken(res.data.token); setUserToken(res.data.token);
store.setIsLogin(true); store.setIsLogin(true);
needVerify.value = false;
if (isMobile()) { if (isMobile()) {
router.push("/mobile"); router.push("/mobile");
} else { } else {
@ -177,7 +149,6 @@ const doLogin = (verifyData) => {
}) })
.catch((e) => { .catch((e) => {
showMessageError("登录失败," + e.message); showMessageError("登录失败," + e.message);
needVerify.value = true;
}); });
}; };
</script> </script>

View File

@ -11,7 +11,6 @@
</template> </template>
<template #text> <template #text>
<div class="text">AI 对话</div> <div class="text">AI 对话</div>
</template> </template>
</van-grid-item> </van-grid-item>
@ -35,16 +34,11 @@
</van-grid> </van-grid>
<div class="app-list"> <div class="app-list">
<van-list <van-list v-model:loading="loading" :finished="true" finished-text="" @load="fetchApps">
v-model:loading="loading"
:finished="true"
finished-text=""
@load="fetchApps"
>
<van-cell v-for="item in apps" :key="item.id"> <van-cell v-for="item in apps" :key="item.id">
<div> <div>
<div class="item"> <div class="item">
<div class="image"> <div class="image flex justify-center items-center">
<van-image :src="item.icon" /> <van-image :src="item.icon" />
</div> </div>
<div class="info"> <div class="info">
@ -58,9 +52,7 @@
<van-button size="small" type="success" @click="useRole(item.id)">使用</van-button> <van-button size="small" type="success" @click="useRole(item.id)">使用</van-button>
<van-button size="small" type="danger" @click="updateRole(item, 'remove')">移除</van-button> <van-button size="small" type="danger" @click="updateRole(item, 'remove')">移除</van-button>
</div> </div>
<van-button v-else size="small" <van-button v-else size="small" style="--el-color-primary: #009999" @click="updateRole(item, 'add')">
style="--el-color-primary:#009999"
@click="updateRole(item, 'add')">
<van-icon name="add-o" /> <van-icon name="add-o" />
<span>添加应用</span> <span>添加应用</span>
</van-button> </van-button>
@ -82,83 +74,90 @@ import {arrayContains, removeArrayItem, showLoginDialog, substr} from "@/utils/l
import { showNotify } from "vant"; import { showNotify } from "vant";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
const title = ref(process.env.VUE_APP_TITLE) const title = ref(process.env.VUE_APP_TITLE);
const router = useRouter() const router = useRouter();
const isLogin = ref(false) const isLogin = ref(false);
const apps = ref([]) const apps = ref([]);
const loading = ref(false) const loading = ref(false);
const roles = ref([]) const roles = ref([]);
const slogan = ref('你有多大想象力AI就有多大创造力') const slogan = ref("你有多大想象力AI就有多大创造力");
onMounted(() => { onMounted(() => {
getSystemInfo().then(res => { getSystemInfo()
title.value = res.data.title .then((res) => {
title.value = res.data.title;
if (res.data.slogan) { if (res.data.slogan) {
slogan.value = res.data.slogan slogan.value = res.data.slogan;
} }
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
}) })
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
checkSession().then((user) => { checkSession()
isLogin.value = true .then((user) => {
roles.value = user.chat_roles isLogin.value = true;
}).catch(() => { roles.value = user.chat_roles;
})
fetchApps()
}) })
.catch(() => {});
fetchApps();
});
const fetchApps = () => { const fetchApps = () => {
httpGet("/api/app/list").then((res) => { httpGet("/api/app/list")
const items = res.data .then((res) => {
const items = res.data;
// hello message // hello message
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80) items[i].intro = substr(items[i].hello_msg, 80);
} }
apps.value = items apps.value = items;
}).catch(e => {
showNotify({type:"danger", message:"获取应用失败:" + e.message})
}) })
} .catch((e) => {
showNotify({ type: "danger", message: "获取应用失败:" + e.message });
});
};
const updateRole = (row, opt) => { const updateRole = (row, opt) => {
if (!isLogin.value) { if (!isLogin.value) {
return showLoginDialog(router) return showLoginDialog(router);
} }
const title = ref("") const title = ref("");
if (opt === "add") { if (opt === "add") {
title.value = "添加应用" title.value = "添加应用";
const exists = arrayContains(roles.value, row.key) const exists = arrayContains(roles.value, row.key);
if (exists) { if (exists) {
return return;
} }
roles.value.push(row.key) roles.value.push(row.key);
} else { } else {
title.value = "移除应用" title.value = "移除应用";
const exists = arrayContains(roles.value, row.key) const exists = arrayContains(roles.value, row.key);
if (!exists) { if (!exists) {
return return;
} }
roles.value = removeArrayItem(roles.value, row.key) roles.value = removeArrayItem(roles.value, row.key);
} }
httpPost("/api/role/update", {keys: roles.value}).then(() => { httpPost("/api/role/update", { keys: roles.value })
ElMessage.success({message: title.value + "成功!", duration: 1000}) .then(() => {
}).catch(e => { ElMessage.success({ message: title.value + "成功!", duration: 1000 });
ElMessage.error(title.value + "失败:" + e.message)
}) })
} .catch((e) => {
ElMessage.error(title.value + "失败:" + e.message);
});
};
const hasRole = (roleKey) => { const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2) return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
} };
const useRole = (roleId) => { const useRole = (roleId) => {
if (!isLogin.value) { if (!isLogin.value) {
return showLoginDialog(router) return showLoginDialog(router);
}
router.push(`/mobile/chat/session?role_id=${roleId}`)
} }
router.push(`/mobile/chat/session?role_id=${roleId}`);
};
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">

View File

@ -0,0 +1,43 @@
<template>
<div class="login flex w-full flex-col place-content-center h-lvh">
<el-image src="/images/logo.png" class="w-1/2 mx-auto logo" />
<div class="title text-center text-3xl font-bold mt-8">{{ title }}</div>
<login-dialog @success="loginSuccess" />
</div>
</template>
<script setup>
import LoginDialog from "@/components/LoginDialog.vue";
import { getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { ref, onMounted } from "vue";
const router = useRouter();
const title = ref("登录");
const loginSuccess = () => {
router.back();
};
onMounted(() => {
getSystemInfo().then((res) => {
title.value = res.data.title;
});
});
</script>
<style scoped lang="stylus">
.login {
background: var(--theme-bg);
transition: all 0.3s ease;
.logo {
background: #ffffff;
border-radius: 50%;
}
.title {
color: var(--text-theme-color);
}
}
</style>