mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-02-13 18:04:26 +08:00
merge code for v4.1.8
This commit is contained in:
60
web/src/components/AccountBg.vue
Normal file
60
web/src/components/AccountBg.vue
Normal 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>
|
||||
101
web/src/components/AccountTop.vue
Normal file
101
web/src/components/AccountTop.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, '</textarea>')}</textarea>`
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
const 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 {
|
||||
|
||||
@@ -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, '</textarea>')}</textarea>`
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
const 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
172
web/src/components/SdTaskView.vue
Normal file
172
web/src/components/SdTaskView.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
52
web/src/components/ThemeChange.vue
Normal file
52
web/src/components/ThemeChange.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="theme-box" @click="toggleTheme">
|
||||
<span class="iconfont icon-yueliang">{{ themePage === "light" ? "" : "" }}</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,57 +1,58 @@
|
||||
<template>
|
||||
<div class="black-input-wrapper">
|
||||
<el-input v-model="model" :type="type" :rows="rows"
|
||||
@input="onInput"
|
||||
style="--el-input-bg-color:#252020;
|
||||
--el-input-border-color:#414141;
|
||||
--el-input-focus-border-color:#414141;
|
||||
--el-text-color-regular: #f1f1f1;
|
||||
--el-input-border-radius: 10px;
|
||||
--el-border-color-hover:#616161"
|
||||
resize="none"
|
||||
:placeholder="placeholder" :maxlength="maxlength"/>
|
||||
<el-input
|
||||
v-model="model"
|
||||
:type="type"
|
||||
:rows="rows"
|
||||
@input="onInput"
|
||||
resize="none"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
/>
|
||||
<div class="word-stat" v-if="rows > 1">
|
||||
<span>{{value.length}}</span>/<span>{{maxlength}}</span>
|
||||
<span>{{ value.length }}</span
|
||||
>/<span>{{ maxlength }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {ref, watch} from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
value : {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'input',
|
||||
default: "input"
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
default: 5
|
||||
},
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: 1024
|
||||
}
|
||||
});
|
||||
watch(() => props.value, (newValue) => {
|
||||
model.value = newValue
|
||||
})
|
||||
const model = ref(props.value)
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
model.value = newValue;
|
||||
}
|
||||
);
|
||||
const model = ref(props.value);
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['update:value']);
|
||||
const emits = defineEmits(["update:value"]);
|
||||
const onInput = (value) => {
|
||||
emits('update:value',value)
|
||||
}
|
||||
emits("update:value", value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -77,4 +78,4 @@ const onInput = (value) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
<template>
|
||||
|
||||
<el-select v-model="model" :placeholder="placeholder"
|
||||
:value="value" @change="$emit('update:value', $event)"
|
||||
style="--el-fill-color-blank:#252020;
|
||||
--el-text-color-regular: #a1a1a1;
|
||||
--el-select-disabled-color:#0E0808;
|
||||
--el-color-primary-light-9:#0E0808;
|
||||
--el-border-radius-base:20px;
|
||||
--el-border-color:#0E0808;">
|
||||
<el-option v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
<el-select
|
||||
v-model="model"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@change="$emit('update:value', $event)"
|
||||
style="--el-border-radius-base: 20px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BlackSelect',
|
||||
name: "BlackSelect",
|
||||
props: {
|
||||
value : {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
default: "请选择"
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
@@ -37,7 +36,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
model: this.value
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<template>
|
||||
|
||||
<el-switch v-model="model" :size="size"
|
||||
@change="$emit('update:value', $event)"
|
||||
style="--el-switch-on-color:#555555;--el-color-white:#0E0808"/>
|
||||
|
||||
<el-switch
|
||||
v-model="model"
|
||||
:size="size"
|
||||
@change="$emit('update:value', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {ref, watch} from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
const props = defineProps({
|
||||
value : Boolean,
|
||||
value: Boolean,
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
default: "default"
|
||||
}
|
||||
});
|
||||
const model = ref(props.value)
|
||||
const model = ref(props.value);
|
||||
|
||||
watch(() => props.value, (newValue) => {
|
||||
model.value = newValue
|
||||
})
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
model.value = newValue;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user