stylus 语法换成 saas 语法

This commit is contained in:
GeekMaster
2025-08-01 17:32:06 +08:00
parent 068b5ddeef
commit 54f8494b5c
145 changed files with 8445 additions and 8446 deletions

View File

@@ -1,7 +1,12 @@
<template>
<div class="right flex-center">
<div class="logo">
<el-image :src="logo" alt="" style="max-width: 300px; max-height: 300px" class="rounded-full" />
<el-image
:src="logo"
alt=""
style="max-width: 300px; max-height: 300px"
class="rounded-full"
/>
</div>
<div>welcome</div>
<footer-bar />
@@ -9,42 +14,43 @@
</template>
<script setup>
import FooterBar from "@/components/FooterBar.vue";
import { getSystemInfo } from "@/store/cache";
import { ref } from "vue";
import FooterBar from '@/components/FooterBar.vue'
import { getSystemInfo } from '@/store/cache'
import { ref } from 'vue'
const logo = ref("");
const title = ref("");
const logo = ref('')
const title = ref('')
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
logo.value = res.data.logo
title.value = res.data.title
})
.catch((err) => {
console.log(err);
logo.value = "/images/logo.png";
title.value = "Geek-AI";
});
console.log(err)
logo.value = '/images/logo.png'
title.value = 'Geek-AI'
})
</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
<style lang="scss" 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
min-height: 100vh;
max-height: 100vh;
background-repeat: no-repeat;
position: relative;
overflow: hidden;
z-index: 1;
:deep(.foot-container){
:deep(.foot-container) {
position: absolute;
bottom: 20px;
width: 100%;
@@ -53,21 +59,23 @@ getSystemInfo()
font-size: 12px;
text-align: center;
.footer{
.footer {
a,
span{
color: var(--text-fff)
span {
color: var(--text-fff);
}
}
}
}
.logo{
.logo {
margin-bottom: 26px;
width: 200px
height: 200px
background: #fff
border-radius: 50%
img{
width: 200px;
height: 200px;
background: #fff;
border-radius: 50%;
img {
width: 100%;
object-fit: cover;
height: 100%;

View File

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

View File

@@ -6,7 +6,7 @@
:style="{
bottom: bottom + 'px',
right: right + 'px',
backgroundColor: bgColor
backgroundColor: bgColor,
}"
>
<el-icon><ArrowUpBold /></el-icon>
@@ -14,56 +14,56 @@
</template>
<script>
import { ArrowUpBold } from "@element-plus/icons-vue";
import { ArrowUpBold } from '@element-plus/icons-vue'
export default {
name: "BackTop",
name: 'BackTop',
components: { ArrowUpBold },
props: {
bottom: {
type: Number,
default: 155
default: 155,
},
right: {
type: Number,
default: 30
default: 30,
},
bgColor: {
type: String,
default: "#b6aaf9"
}
default: '#b6aaf9',
},
},
data() {
return {
showButton: false
};
showButton: false,
}
},
mounted() {
this.checkScroll();
window.addEventListener("resize", this.checkScroll);
this.$el.parentElement.addEventListener("scroll", this.checkScroll);
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;
const container = this.$el.parentElement
container.scrollTo({
top: 0,
behavior: "smooth"
});
behavior: 'smooth',
})
},
checkScroll() {
const container = this.$el.parentElement;
this.showButton = container.scrollTop > 50;
}
}
};
const container = this.$el.parentElement
this.showButton = container.scrollTop > 50
},
},
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.scroll-to-top {
position: fixed;
color: white;
@@ -72,12 +72,12 @@ export default {
cursor: pointer;
outline: none;
transition: opacity 0.3s;
width 30px
height 30px
display flex
justify-content center
align-items center
font-size 18px
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
&:hover {
opacity: 0.6;

View File

@@ -1,25 +1,25 @@
<template>
<el-dialog
v-model="showDialog"
:close-on-click-modal="true"
style="max-width: 400px"
@close="close"
:title="title"
v-model="showDialog"
:close-on-click-modal="true"
style="max-width: 400px"
@close="close"
:title="title"
>
<div class="form">
<div class="text-center" v-if="email !== ''">当前已绑定邮箱{{ email }}</div>
<el-form label-position="top">
<el-form-item label="邮箱地址">
<el-input v-model="form.email"/>
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="验证码">
<el-row :gutter="0">
<el-col :span="16">
<el-input v-model="form.code" maxlength="6"/>
<el-input v-model="form.code" maxlength="6" />
</el-col>
<el-col :span="8" style="padding-left: 10px">
<send-msg :receiver="form.email" type="email"/>
<send-msg :receiver="form.email" type="email" />
</el-col>
</el-row>
</el-form-item>
@@ -28,24 +28,22 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="save">
提交绑定
</el-button>
<el-button type="primary" @click="save"> 提交绑定 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref, watch} from "vue";
import SendMsg from "@/components/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {checkSession} from "@/store/cache";
import SendMsg from '@/components/SendMsg.vue'
import { checkSession } from '@/store/cache'
import { httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
const props = defineProps({
show: Boolean,
});
})
const showDialog = computed(() => {
return props.show
@@ -55,53 +53,55 @@ const title = ref('绑定邮箱')
const email = ref('')
const form = ref({
email: '',
code: ''
code: '',
})
watch(showDialog, (val) => {
if (val) {
form.value.code = ''
form.value.email = ''
checkSession().then(user => {
checkSession().then((user) => {
email.value = user.email
})
}
})
const emits = defineEmits(['hide']);
const emits = defineEmits(['hide'])
const save = () => {
if (form.value.code === '') {
return ElMessage.error("请输入验证码");
return ElMessage.error('请输入验证码')
}
httpPost('/api/user/bind/email', form.value).then(() => {
ElMessage.success("绑定成功")
emits('hide')
}).catch(e => {
ElMessage.error("绑定失败:" + e.message);
})
httpPost('/api/user/bind/email', form.value)
.then(() => {
ElMessage.success('绑定成功')
emits('hide')
})
.catch((e) => {
ElMessage.error('绑定失败:' + e.message)
})
}
const close = function () {
emits('hide');
emits('hide')
}
</script>
<style lang="stylus" scoped>
<style lang="scss" scoped>
.form {
.text-center {
text-align center
padding-bottom 15px
font-size 14px
color #a1a1a1
font-weight 700
text-align: center;
padding-bottom: 15px;
font-size: 14px;
color: #a1a1a1;
font-weight: 700;
}
.el-form-item__content {
.el-row {
width 100%
width: 100%;
}
}
}
</style>
</style>

View File

@@ -1,25 +1,25 @@
<template>
<el-dialog
v-model="showDialog"
:close-on-click-modal="true"
style="max-width: 400px"
@close="close"
:title="title"
v-model="showDialog"
:close-on-click-modal="true"
style="max-width: 400px"
@close="close"
:title="title"
>
<div class="form">
<div class="text-center" v-if="mobile !== ''">当前已绑手机号{{ mobile }}</div>
<el-form label-position="top">
<el-form-item label="手机号">
<el-input v-model="form.mobile"/>
<el-input v-model="form.mobile" />
</el-form-item>
<el-form-item label="验证码">
<el-row :gutter="0">
<el-col :span="16">
<el-input v-model="form.code" maxlength="6"/>
<el-input v-model="form.code" maxlength="6" />
</el-col>
<el-col :span="8" style="padding-left: 10px">
<send-msg :receiver="form.mobile" size="default" type="mobile"/>
<send-msg :receiver="form.mobile" size="default" type="mobile" />
</el-col>
</el-row>
</el-form-item>
@@ -28,24 +28,22 @@
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="save">
提交绑定
</el-button>
<el-button type="primary" @click="save"> 提交绑定 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref, watch} from "vue";
import SendMsg from "@/components/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {checkSession} from "@/store/cache";
import SendMsg from '@/components/SendMsg.vue'
import { checkSession } from '@/store/cache'
import { httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
const props = defineProps({
show: Boolean,
});
})
const showDialog = computed(() => {
return props.show
@@ -55,56 +53,58 @@ const title = ref('绑定手机')
const mobile = ref('')
const form = ref({
mobile: '',
code: ''
code: '',
})
watch(showDialog, (val) => {
if (val) {
form.value = {
mobile: '',
code: ''
code: '',
}
checkSession().then(user => {
checkSession().then((user) => {
mobile.value = user.mobile
})
}
})
const emits = defineEmits(['hide']);
const emits = defineEmits(['hide'])
const save = () => {
if (form.value.code === '') {
return ElMessage.error("请输入验证码");
return ElMessage.error('请输入验证码')
}
httpPost('/api/user/bind/mobile', form.value).then(() => {
ElMessage.success("绑定成功")
emits('hide')
}).catch(e => {
ElMessage.error("绑定失败:" + e.message);
})
httpPost('/api/user/bind/mobile', form.value)
.then(() => {
ElMessage.success('绑定成功')
emits('hide')
})
.catch((e) => {
ElMessage.error('绑定失败:' + e.message)
})
}
const close = function () {
emits('hide');
emits('hide')
}
</script>
<style lang="stylus" scoped>
<style lang="scss" scoped>
.form {
.text-center {
text-align center
padding-bottom 15px
font-size 14px
color #a1a1a1
font-weight 700
text-align: center;
padding-bottom: 15px;
font-size: 14px;
color: #a1a1a1;
font-weight: 700;
}
.el-form-item__content {
.el-row {
width 100%
width: 100%;
}
}
}
</style>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<!--拨号组件-->
<el-container class="calling-container" :style="{height: height}">
<el-container class="calling-container" :style="{ height: height }">
<div class="phone-container">
<div class="signal"></div>
<div class="signal"></div>
@@ -12,40 +12,38 @@
</template>
<script setup>
import {onMounted, ref} from "vue";
import { onMounted, ref } from 'vue'
const fullText = "正在接通中...";
const text = ref("")
let index = 0;
const fullText = '正在接通中...'
const text = ref('')
let index = 0
const props = defineProps({
height: {
type: String,
default: '100vh'
}
default: '100vh',
},
})
function typeText() {
if (index < fullText.length) {
text.value += fullText[index];
index++;
setTimeout(typeText, 300); // 每300毫秒显示一个字
text.value += fullText[index]
index++
setTimeout(typeText, 300) // 每300毫秒显示一个字
} else {
setTimeout(() => {
text.value = '';
index = 0;
typeText();
}, 1000); // 等待1秒后重新开始
text.value = ''
index = 0
typeText()
}, 1000) // 等待1秒后重新开始
}
}
onMounted(() => {
typeText()
})
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.calling-container {
background-color: #000;
display: flex;
@@ -55,7 +53,7 @@ onMounted(() => {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
width 100vw
width: 100vw;
.phone-container {
position: relative;
@@ -71,9 +69,11 @@ onMounted(() => {
width: 60px;
height: 60px;
background-color: #00ffcc;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 15.5c-1.25 0-2.45-.2-3.57-.57a1.02 1.02 0 0 0-1.02.24l-2.2 2.2a15.074 15.074 0 0 1-6.59-6.59l2.2-2.2c.27-.27.35-.68.24-1.02a11.36 11.36 0 0 1-.57-3.57c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1zM5.03 5h1.5c.07.89.22 1.76.46 2.59l-1.2 1.2c-.41-1.2-.67-2.47-.76-3.79zM19 18.97c-1.32-.09-2.59-.35-3.8-.75l1.2-1.2c.85.24 1.72.39 2.6.45v1.5z'/%3E%3C/svg%3E") no-repeat 50% 50%;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 15.5c-1.25 0-2.45-.2-3.57-.57a1.02 1.02 0 0 0-1.02.24l-2.2 2.2a15.074 15.074 0 0 1-6.59-6.59l2.2-2.2c.27-.27.35-.68.24-1.02a11.36 11.36 0 0 1-.57-3.57c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1zM5.03 5h1.5c.07.89.22 1.76.46 2.59l-1.2 1.2c-.41-1.2-.67-2.47-.76-3.79zM19 18.97c-1.32-.09-2.59-.35-3.8-.75l1.2-1.2c.85.24 1.72.39 2.6.45v1.5z'/%3E%3C/svg%3E")
no-repeat 50% 50%;
mask-size: cover;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 15.5c-1.25 0-2.45-.2-3.57-.57a1.02 1.02 0 0 0-1.02.24l-2.2 2.2a15.074 15.074 0 0 1-6.59-6.59l2.2-2.2c.27-.27.35-.68.24-1.02a11.36 11.36 0 0 1-.57-3.57c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1zM5.03 5h1.5c.07.89.22 1.76.46 2.59l-1.2 1.2c-.41-1.2-.67-2.47-.76-3.79zM19 18.97c-1.32-.09-2.59-.35-3.8-.75l1.2-1.2c.85.24 1.72.39 2.6.45v1.5z'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 15.5c-1.25 0-2.45-.2-3.57-.57a1.02 1.02 0 0 0-1.02.24l-2.2 2.2a15.074 15.074 0 0 1-6.59-6.59l2.2-2.2c.27-.27.35-.68.24-1.02a11.36 11.36 0 0 1-.57-3.57c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1zM5.03 5h1.5c.07.89.22 1.76.46 2.59l-1.2 1.2c-.41-1.2-.67-2.47-.76-3.79zM19 18.97c-1.32-.09-2.59-.35-3.8-.75l1.2-1.2c.85.24 1.72.39 2.6.45v1.5z'/%3E%3C/svg%3E")
no-repeat 50% 50%;
-webkit-mask-size: cover;
animation: shake 0.5s ease-in-out infinite;
}
@@ -108,9 +108,16 @@ onMounted(() => {
}
@keyframes shake {
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
25% { transform: translate(-52%, -48%) rotate(-5deg); }
75% { transform: translate(-48%, -52%) rotate(5deg); }
0%,
100% {
transform: translate(-50%, -50%) rotate(0deg);
}
25% {
transform: translate(-52%, -48%) rotate(-5deg);
}
75% {
transform: translate(-48%, -52%) rotate(5deg);
}
}
@keyframes signal {
@@ -126,5 +133,4 @@ onMounted(() => {
}
}
}
</style>
</style>

View File

@@ -1,8 +1,17 @@
<template>
<el-container class="captcha-box">
<el-dialog v-model="show" :close-on-click-modal="true" :show-close="isMobileInternal" style="width: 360px; --el-dialog-padding-primary: 5px 15px 15px 15px">
<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 #header>
<div class="text-center p-3" style="color: var(--el-text-color-primary)" v-if="isMobileInternal">
<div
class="text-center p-3"
style="color: var(--el-text-color-primary)"
v-if="isMobileInternal"
>
<span>人机验证</span>
</div>
</template>
@@ -31,113 +40,111 @@
</template>
<script setup>
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 CaptchaPlus from '@/components/CaptchaPlus.vue'
import SlideCaptcha from '@/components/SlideCaptcha.vue'
import { showMessageError } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { isMobile } from '@/utils/libs'
import lodash from 'lodash'
import { ref } from 'vue'
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 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")
httpGet('/api/captcha/get')
.then((res) => {
const data = res.data;
imageBase64.value = data.image;
thumbBase64.value = data.thumb;
captKey.value = data.key;
const data = res.data
imageBase64.value = data.image
thumbBase64.value = data.thumb
captKey.value = data.key
})
.catch((e) => {
showMessageError("获取人机验证数据失败:" + e.message);
});
};
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 });
show.value = false
emits('success', { key: captKey.value, dots: dots.value })
})
.catch(() => {
showMessageError("人机验证失败");
handleRequestCaptCode();
});
};
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")
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;
bkImg.value = res.data.bkImg
bgImg.value = res.data.bgImg
captKey.value = res.data.key
})
.catch((e) => {
showMessageError("获取人机验证数据失败:" + e.message);
});
};
showMessageError('获取人机验证数据失败:' + e.message)
})
}
const handleSlideConfirm = (x) => {
httpPost("/api/captcha/slide/check", {
httpPost('/api/captcha/slide/check', {
key: captKey.value,
x: x,
})
.then(() => {
result.value = 1;
show.value = false;
emits("success", { key: captKey.value, x: x });
result.value = 1
show.value = false
emits('success', { key: captKey.value, x: x })
})
.catch(() => {
result.value = 2;
});
};
result.value = 2
})
}
// 导出方法以便父组件调用
defineExpose({
loadCaptcha,
});
})
</script>
<style lang="stylus">
<style lang="scss">
.captcha-box {
.el-dialog {
.el-dialog__header {

View File

@@ -5,13 +5,24 @@
<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__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;`">
<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>
@@ -37,20 +48,20 @@
<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,
@@ -63,23 +74,23 @@ export default {
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: {
@@ -87,7 +98,7 @@ export default {
* @Description: 处理关闭事件
*/
handleCloseEvent() {
this.$emit("close");
this.$emit('close')
// this.dots = []
// this.imageBase64Code = ''
// this.thumbBase64Code = ''
@@ -96,14 +107,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
@@ -111,107 +122,105 @@ 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;
})
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;
let box = dom.getBoundingClientRect()
let D = document.documentElement
x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft
y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop
} else {
while (dom !== document.body) {
x += dom.offsetLeft;
y += dom.offsetTop;
dom = dom.offsetParent;
x += dom.offsetLeft
y += dom.offsetTop
dom = dom.offsetParent
}
}
return {
domX: x,
domY: y,
};
}
},
},
};
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.wg-cap-wrap {
background: var(--el-bg-color);
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
@@ -227,7 +236,6 @@ export default {
span {
padding-right: 5px;
em {
padding: 0 3px;
font-weight: bold;
@@ -255,7 +263,6 @@ export default {
.wg-cap-wrap__thumb.wg-cap-wrap__hidden {
display: none;
}
}
.wg-cap-wrap__body {
@@ -328,7 +335,6 @@ export default {
.wg-cap-wrap__ico {
flex: 1;
img {
width: 24px;
height: 24px;
@@ -339,9 +345,9 @@ export default {
}
.wg-cap-wrap__btn {
display flex
display: flex;
width: 120px;
justify-content right
justify-content: right;
}
}
}

View File

@@ -169,27 +169,27 @@ const isExternalImg = (link, files) => {
}
</script>
<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
.chat-page, .chat-export {
<style lang="scss">
@use '@/assets/css/markdown/vue.css' as *;
.chat-page,
.chat-export {
.chat-line-prompt-list {
background-color: var(--chat-content-bg-list);
color: var(--theme-text-color-primary);
justify-content: center;
width 100%
width: 100%;
padding-bottom: 1.5rem;
padding-top: 1.5rem;
// border-bottom: 0.5px solid var(--el-border-color);
.chat-line-inner {
display flex;
width 100%;
max-width 900px;
padding-left 10px;
display: flex;
width: 100%;
max-width: 900px;
padding-left: 10px;
.chat-icon {
margin-right 20px;
margin-right: 20px;
img {
width: 36px;
@@ -200,61 +200,61 @@ const isExternalImg = (link, files) => {
}
.chat-item {
width 100%
width: 100%;
padding: 0 5px 0 0;
overflow: hidden;
.file-list-box {
display flex
flex-flow column
display: flex;
flex-flow: column;
.image {
display flex
flex-flow row
margin-right 10px
position relative
display: flex;
flex-flow: row;
margin-right: 10px;
position: relative;
.el-image {
border 1px solid #e3e3e3
border-radius 10px
margin-bottom 10px
max-width 150px
max-height 150px
border: 1px solid #e3e3e3;
border-radius: 10px;
margin-bottom: 10px;
max-width: 150px;
max-height: 150px;
}
}
.item {
display flex
flex-flow row
border-radius 10px
display: flex;
flex-flow: row;
border-radius: 10px;
background-color: var(--chat-content-bg);
border 1px solid #e3e3e3
border: 1px solid #e3e3e3;
color: var(--theme-text-color-primary);
padding 6px
margin-bottom 10px
padding: 6px;
margin-bottom: 10px;
.icon {
.el-image {
width 40px
height 40px
width: 40px;
height: 40px;
}
}
.body {
margin-left 8px
font-size 14px
margin-left: 8px;
font-size: 14px;
.title {
font-weight bold
line-height 24px
color #0D0D0D
font-weight: bold;
line-height: 24px;
color: #0d0d0d;
}
.info {
color #B4B4B4
color: #b4b4b4;
span {
margin-right 10px
margin-right: 10px;
}
}
}
@@ -262,7 +262,7 @@ const isExternalImg = (link, files) => {
}
.content {
word-break break-word;
word-break: break-word;
padding: 0;
color: var(--theme-text-color-primary);
font-size: var(--content-font-size);
@@ -272,59 +272,57 @@ const isExternalImg = (link, files) => {
img {
max-width: 600px;
border-radius: 10px;
margin 10px 0
margin: 10px 0;
}
p {
line-height 1.5
line-height: 1.5;
}
p:last-child {
margin-bottom: 0
margin-bottom: 0;
}
p:first-child {
margin-top 0
margin-top: 0;
}
}
.bar {
padding 10px 10px 10px 0;
padding: 10px 10px 10px 0;
.bar-item {
// background-color #f7f7f8;
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
color: #888;
padding: 3px 5px;
margin-right: 10px;
border-radius: 5px;
.el-icon {
position relative
top 2px;
position: relative;
top: 2px;
}
}
}
}
}
}
.chat-line-prompt-chat {
background: var(--chat-bg);
justify-content: center;
width 100%
width: 100%;
padding-bottom: 1.5rem;
padding-top: 1.5rem;
.chat-line-inner {
display flex;
width 100%;
padding 0 25px;
flex-flow row-reverse
display: flex;
width: 100%;
padding: 0 25px;
flex-flow: row-reverse;
.chat-icon {
margin-left 20px;
margin-left: 20px;
img {
width: 36px;
@@ -337,74 +335,73 @@ const isExternalImg = (link, files) => {
.chat-item {
padding: 0;
overflow: hidden;
max-width calc(100% - 110px);
max-width: calc(100% - 110px);
.file-list-box {
display flex
flex-flow column
display: flex;
flex-flow: column;
.image {
display flex
flex-flow row
margin-right 10px
position relative
display: flex;
flex-flow: row;
margin-right: 10px;
position: relative;
.el-image {
border 1px solid #e3e3e3
border-radius 10px
margin-bottom 10px
max-width 150px
max-height 150px
border: 1px solid #e3e3e3;
border-radius: 10px;
margin-bottom: 10px;
max-width: 150px;
max-height: 150px;
}
}
.item {
display flex
flex-flow row
border-radius 10px
display: flex;
flex-flow: row;
border-radius: 10px;
background-color: var(--chat-content-bg);
color: var(--theme-text-color-primary);
border 1px solid #e3e3e3
padding 6px
margin-bottom 10px
border: 1px solid #e3e3e3;
padding: 6px;
margin-bottom: 10px;
.icon {
.el-image {
width 40px
height 40px
width: 40px;
height: 40px;
}
}
.body {
margin-left 8px
font-size 14px
margin-left: 8px;
font-size: 14px;
.title {
font-weight bold
line-height 24px
color #0D0D0D
font-weight: bold;
line-height: 24px;
color: #0d0d0d;
}
.info {
color #B4B4B4
color: #b4b4b4;
span {
margin-right 10px
margin-right: 10px;
}
}
}
}
}
.content-wrapper {
display flex
flex-flow row-reverse
display: flex;
flex-flow: row-reverse;
.content {
word-break break-word;
padding: 1rem
color var(--theme-text-primary);
word-break: break-word;
padding: 1rem;
color: var(--theme-text-primary);
font-size: var(--content-font-size);
overflow: auto;
background-color: var(--chat-user-content-bg);
@@ -413,79 +410,40 @@ const isExternalImg = (link, files) => {
img {
max-width: 600px;
border-radius: 10px;
margin 10px 0
margin: 10px 0;
}
p {
line-height 1.5
line-height: 1.5;
}
p:last-child {
margin-bottom: 0
margin-bottom: 0;
}
p:first-child {
margin-top 0
margin-top: 0;
}
}
}
.bar {
padding 10px 10px 10px 0;
padding: 10px 10px 10px 0;
.bar-item {
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
color: #888;
padding: 3px 5px;
margin-right: 10px;
border-radius: 5px;
.el-icon {
position relative
top 2px;
position: relative;
top: 2px;
}
}
}
}
}
}
}
.operations
display none
position absolute
right 5px
top 5px
.text-box
&:hover
.operations
display flex
gap 5px
.op-edit
cursor pointer
color #409eff
font-size 16px
&:hover
color darken(#409eff, 10%)
.position-relative {
position: relative;
}
.action-buttons {
position: absolute;
top: 10px;
right: 10px;
display: none;
}
.content:hover .action-buttons {
display: block;
}
</style>

View File

@@ -311,11 +311,13 @@ const handleExpandClick = (e) => {
}
</script>
<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
<style lang="scss">
@use '@/assets/css/markdown/vue.css' as *;
.chat-page,.chat-export {
--font-family: Menlo,"微软雅黑","Roboto Mono","Courier New",Courier,monospace,"Inter",sans-serif;
.chat-page,
.chat-export {
--font-family: Menlo, '微软雅黑', 'Roboto Mono', 'Courier New', Courier, monospace, 'Inter',
sans-serif;
font-family: var(--font-family);
.chat-line {
@@ -327,130 +329,129 @@ const handleExpandClick = (e) => {
.chat-item {
.content-wrapper {
img {
max-width: 600px;
max-width: 600px;
border-radius: 10px;
}
p {
line-height: 1.5;
code {
color: var(--theme-text-color-primary);
font-weight: 600;
}
}
p:last-child {
margin-bottom: 0;
}
p:first-child {
margin-top: 0;
}
.code-container {
background-color: #2b2b2b;
border-radius: 10px;
position: relative;
.hljs {
border-radius: 10px;
width: 100%;
}
p {
line-height 1.5
code {
color:var(--theme-text-color-primary);
font-weight 600
}
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
.code-container {
background-color #2b2b2b
border-radius 10px
position relative
.hljs {
border-radius 10px
width 100%
}
.copy-code-btn {
cursor pointer
font-size 12px
color #c1c1c1
&:hover {
color #20a0ff
}
}
}
// 添加代码块展开/收起样式
.code-collapsed {
.hljs {
max-height 200px
overflow hidden
position relative
transition max-height 0.3s ease
&::after {
content ''
position absolute
bottom 0
left 0
right 0
height 30px
background linear-gradient(transparent, #2b2b2b)
pointer-events none
}
}
}
.code-expanded {
.hljs {
max-height none
overflow auto
transition max-height 0.3s ease
&::after {
display none
}
}
}
.expand-btn {
transition color 0.2s ease
.copy-code-btn {
cursor: pointer;
font-size: 12px;
color: #c1c1c1;
&:hover {
color #20a0ff !important
color: #20a0ff;
}
}
}
// 添加代码块展开/收起样式
.code-collapsed {
.hljs {
max-height: 200px;
overflow: hidden;
position: relative;
transition: max-height 0.3s ease;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30px;
background: linear-gradient(transparent, #2b2b2b);
pointer-events: none;
}
}
}
.code-expanded {
.hljs {
max-height: none;
overflow: auto;
transition: max-height 0.3s ease;
&::after {
display: none;
}
}
}
.expand-btn {
transition: color 0.2s ease;
&:hover {
color: #20a0ff !important;
}
}
.lang-name {
color: #00e0e0;
}
// 设置表格边框
table {
width: 100%;
margin-bottom: 1rem;
border-collapse: collapse;
border: 1px solid #dee2e6;
background-color: var(--chat-content-bg);
color: var(--theme-text-color-primary);
thead {
th {
border: 1px solid #dee2e6;
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
padding: 10px;
}
}
.lang-name {
color #00e0e0
td {
border: 1px solid #dee2e6;
padding: 10px;
}
}
// 设置表格边框
// 代码快
table {
width 100%
margin-bottom 1rem
border-collapse collapse;
border 1px solid #dee2e6;
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
thead {
th {
border 1px solid #dee2e6
vertical-align: bottom
border-bottom: 2px solid #dee2e6
padding 10px
}
}
td {
border 1px solid #dee2e6
padding 10px
}
}
// 代码快
blockquote {
margin 0 0 0.8rem 0
background-color: var(--quote-bg-color);
padding: 0.8rem 1.5rem;
color: var(--quote-text-color);
border-left: 0.4rem solid #6b50e1; /* 紫色边框 */
font-size: 16px;
line-height: 1.6;
}
blockquote {
margin: 0 0 0.8rem 0;
background-color: var(--quote-bg-color);
padding: 0.8rem 1.5rem;
color: var(--quote-text-color);
border-left: 0.4rem solid #6b50e1; /* 紫色边框 */
font-size: 16px;
line-height: 1.6;
}
}
}
}
@@ -458,21 +459,21 @@ const handleExpandClick = (e) => {
.chat-line-reply-list {
justify-content: center;
background-color: var(--chat-content-bg);
color:var(--theme-text-color-primary);
width 100%
color: var(--theme-text-color-primary);
width: 100%;
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border: 1px solid var(--el-border-color);
border-radius: 10px;
.chat-line-inner {
display flex;
width 100%;
max-width 900px;
padding-left 10px;
display: flex;
width: 100%;
max-width: 900px;
padding-left: 10px;
.chat-icon {
margin-right 20px;
margin-right: 20px;
img {
width: 36px;
@@ -483,82 +484,79 @@ const handleExpandClick = (e) => {
}
.chat-item {
width 100%
width: 100%;
position: relative;
padding: 0;
overflow: hidden;
.content-wrapper {
min-height 20px;
word-break break-word;
padding: 0
color:var(--theme-text-color-primary);
min-height: 20px;
word-break: break-word;
padding: 0;
color: var(--theme-text-color-primary);
font-size: var(--content-font-size);
border-radius: 5px;
overflow auto;
overflow: auto;
}
.bar {
padding 10px 10px 10px 0;
padding: 10px 10px 10px 0;
.bar-item {
margin-right 10px;
border-radius 5px;
cursor pointer
display flex
align-items center
justify-content center
height 26px
margin-right: 10px;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 26px;
.voice-icon {
width 20px
height 20px
width: 20px;
height: 20px;
}
.el-icon {
position relative
top 2px;
cursor pointer
position: relative;
top: 2px;
cursor: pointer;
}
}
.el-button {
height 20px
padding 5px 2px;
height: 20px;
padding: 5px 2px;
}
}
}
.tool-box {
font-size 16px;
font-size: 16px;
.el-button {
height 20px
padding 5px 2px;
height: 20px;
padding: 5px 2px;
}
}
}
}
.chat-line-reply-chat {
justify-content: center;
padding 1.5rem;
padding: 1.5rem;
.chat-line-inner {
display flex;
width 100%
flex-flow row
display: flex;
width: 100%;
flex-flow: row;
.chat-icon {
margin-right 20px;
margin-right: 20px;
img {
width: 36px;
height: 36px;
border-radius: 50%
border-radius: 50%;
padding: 1px;
}
}
@@ -567,75 +565,71 @@ const handleExpandClick = (e) => {
position: relative;
padding: 0;
overflow: hidden;
width 100%
max-width calc(100% - 110px)
width: 100%;
max-width: calc(100% - 110px);
.content-wrapper {
display flex
display: flex;
.content {
min-height 20px;
word-break break-word;
padding: 1rem
color var(--theme-text-primary);
min-height: 20px;
word-break: break-word;
padding: 1rem;
color: var(--theme-text-primary);
font-size: var(--content-font-size);
overflow auto;
overflow: auto;
// background-color #F5F5F5
background-color :var(--chat-content-bg);
background-color: var(--chat-content-bg);
border-radius: 0 10px 10px 10px;
width 100%
width: 100%;
}
}
.bar {
padding 10px 10px 10px 0;
display flex
padding: 10px 10px 10px 0;
display: flex;
.bar-item {
margin-right 10px;
border-radius 5px;
display flex
align-items center
justify-content center
height 26px
margin-right: 10px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
height: 26px;
.voice-icon {
width 20px
height 20px
width: 20px;
height: 20px;
}
.el-icon {
position relative
top 2px;
cursor pointer
position: relative;
top: 2px;
cursor: pointer;
}
}
.bar-item.bg {
// background-color var( --gray-btn-bg)
cursor pointer
cursor: pointer;
}
.el-button {
height 20px
padding 5px 2px;
height: 20px;
padding: 5px 2px;
}
}
}
.tool-box {
font-size 16px;
font-size: 16px;
.el-button {
height 20px
padding 5px 2px;
height: 20px;
padding: 5px 2px;
}
}
}
}
}
</style>

View File

@@ -45,9 +45,9 @@
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
import { useSharedStore } from '@/store/sharedata'
import { httpGet } from '@/utils/http'
import { computed, onMounted, ref } from 'vue'
const store = useSharedStore()
const data = ref({
@@ -60,7 +60,7 @@ const props = defineProps({
show: Boolean,
})
const showDialog = ref(props.show)
const showDialog = computed(() => props.show)
const emits = defineEmits(['hide'])
const close = function () {
emits('hide', false)
@@ -81,8 +81,4 @@ const changeTTSModel = (item) => {
}
</script>
<style lang="stylus" scoped>
.chat-setting {
}
</style>
<style lang="scss" scoped></style>

View File

@@ -5,28 +5,30 @@
</div>
</template>
<script setup>
import {onMounted, ref, watch} from "vue";
import { onMounted, ref, watch } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
second: Number,
type: {
type: String,
default: ""
}
});
default: '',
},
})
// eslint-disable-next-line no-undef
const emits = defineEmits(['timeout']);
const emits = defineEmits(['timeout'])
const counter = ref(props.second)
const timerStr = ref("")
const timerStr = ref('')
const handler = ref(null)
watch(() => props.second, (newVal) => {
counter.value = newVal
resetTimer()
});
watch(
() => props.second,
(newVal) => {
counter.value = newVal
resetTimer()
}
)
onMounted(() => {
resetTimer()
@@ -43,7 +45,7 @@ const resetTimer = () => {
formatTimer(counter.value)
if (counter.value === 0) {
clearInterval(handler.value)
emits("timeout")
emits('timeout')
}
counter.value--
}, 1000)
@@ -56,45 +58,44 @@ const formatTimer = (secs) => {
if (secs > 3600) {
hour = Math.floor(secs / 3600)
if (hour < 10) {
hour = "0" + hour
hour = '0' + hour
}
secs = secs % 3600
timer.push(hour + "")
timer.push(hour + '')
} else {
timer.push("00 时 ")
timer.push('00 时 ')
}
// 计算分钟
if (secs > 60) {
min = Math.floor(secs / 60)
if (min < 10) {
min = "0" + min
min = '0' + min
}
secs = secs % 60
timer.push(min + "")
timer.push(min + '')
} else {
timer.push("00 分 ")
timer.push('00 分 ')
}
// 计算秒数
if (secs < 10) {
secs = "0" + secs
secs = '0' + secs
}
timer.push(secs + "")
timerStr.value = timer.join("")
timer.push(secs + '')
timerStr.value = timer.join('')
}
// eslint-disable-next-line no-undef
defineExpose({resetTimer})
defineExpose({ resetTimer })
</script>
<style lang="stylus">
<style lang="scss">
.countdown {
display flex
display: flex;
.el-tag--large {
.el-tag__content {
font-size 14px
font-size: 14px;
}
}
}
</style>
</style>

View File

@@ -13,7 +13,9 @@
</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>
@@ -29,89 +31,94 @@
</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 { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
import { isImage, removeArrayItem, substr } from '@/utils/libs'
import { CircleCloseFilled } from '@element-plus/icons-vue'
import { ref } from 'vue'
const props = defineProps({
files: {
type: Array,
default: [],
},
});
const emits = defineEmits(["removeFile"]);
const fileList = ref(props.files);
})
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">
<style scoped lang="scss">
.chat-file-list {
display flex
flex-flow row
display: flex;
flex-flow: row;
.image {
display flex
flex-flow row
margin-right 10px
max-width 600px
position relative
display: flex;
flex-flow: row;
margin-right: 10px;
max-width: 600px;
position: relative;
.el-image {
height 56px
width 56px
border 1px solid #e3e3e3
border-radius 10px
height: 56px;
width: 56px;
border: 1px solid #e3e3e3;
border-radius: 10px;
}
}
.item {
position relative
display flex
flex-flow row
border-radius 10px
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
border 1px solid #e3e3e3
padding 6px
margin-right 10px
position: relative;
display: flex;
flex-flow: row;
border-radius: 10px;
background-color: var(--chat-content-bg);
color: var(--theme-text-color-primary);
border: 1px solid #e3e3e3;
padding: 6px;
margin-right: 10px;
.icon {
.el-image {
width 40px
height 40px
width: 40px;
height: 40px;
}
}
.body {
margin-left 5px
font-size 14px
margin-left: 5px;
font-size: 14px;
.title {
line-height 24px
color #0D0D0D
line-height: 24px;
color: #0d0d0d;
}
.info {
color #B4B4B4
color: #b4b4b4;
span {
margin-right 10px
margin-right: 10px;
}
}
}
}
.action {
position absolute
top -8px
right -8px
color #da0d54
cursor pointer
font-size 20px
position: absolute;
top: -8px;
right: -8px;
color: #da0d54;
cursor: pointer;
font-size: 20px;
.el-icon {
background-color #fff
border-radius 50%
background-color: #fff;
border-radius: 50%;
}
}
}

View File

@@ -3,7 +3,14 @@
<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-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">
@@ -25,12 +32,28 @@
<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-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>
@@ -45,180 +68,176 @@
</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 {checkSession} from "@/store/cache";
import {useSharedStore} from "@/store/sharedata";
import {closeLoading, showLoading} from "@/utils/dialog";
import { checkSession } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import { GetFileIcon } from '@/store/system'
import { closeLoading, showLoading } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { isImage, removeArrayItem } from '@/utils/libs'
import { Delete, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
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: [],
page: 1,
isLastPage: true,
});
const store = useSharedStore();
})
const store = useSharedStore()
const fetchFiles = (pageNo) => {
checkSession()
.then(() => {
show.value = true;
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 })
show.value = true
httpPost('/api/upload/list', { page: pageNo || 1, page_size: 30 })
.then((res) => {
const { items, page, total_page } = res.data;
const { items, page, total_page } = res.data
if (page === 1) {
fileData.items = items;
fileData.items = items
} else {
fileData.items = [...fileData.items, ...items];
fileData.items = [...fileData.items, ...items]
}
fileData.isLastPage = page === total_page;
fileData.isLastPage = page === total_page
if (!fileData.isLastPage) {
fileData.page = page + 1;
fileData.page = page + 1
}
})
.catch((e) => {
showMessageError("获取文件列表失败:" + e.message);
});
showMessageError('获取文件列表失败:' + e.message)
})
})
.catch(() => {
store.setShowLoginDialog(true);
});
};
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);
showLoading("文件上传中...");
const formData = new FormData()
formData.append('file', file.file, file.name)
showLoading('文件上传中...')
// 执行上传操作
httpPost("/api/upload", formData)
httpPost('/api/upload', formData)
.then((res) => {
fileData.items.unshift(res.data);
ElMessage.success({ message: "上传成功", duration: 500 });
fileData.items.unshift(res.data)
ElMessage.success({ message: '上传成功', duration: 500 })
closeLoading()
})
.catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
ElMessage.error('图片上传失败:' + e.message)
closeLoading()
});
};
})
}
const removeFile = (file) => {
httpGet("/api/upload/remove?id=" + file.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);
return v1.id === v2.id
})
ElMessage.success('文件删除成功!')
fetchFiles(1)
})
.catch((e) => {
ElMessage.error("文件删除失败:" + e.message);
});
};
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;
if (file.url.indexOf('http') === -1) {
file.url = location.protocol + '//' + location.host + file.url
}
emits("selected", file);
};
emits('selected', file)
}
</script>
<style lang="stylus">
<style lang="scss">
.file-select-box {
.file-upload-img {
.iconfont {
font-size: 19px;
cursor pointer;
cursor: pointer;
}
}
.el-dialog {
.el-dialog__body {
//padding 0
overflow hidden
overflow: hidden;
.file-list {
margin-right 10px
margin-right: 10px;
.grid-content {
margin-bottom 10px
position relative
margin-bottom: 10px;
position: relative;
.avatar-uploader {
width 100%
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border 1px dashed #e1e1e1
border-radius 6px
border: 1px dashed #e1e1e1;
border-radius: 6px;
.el-upload {
width 100%
height 80px
width: 100%;
height: 80px;
}
}
.el-image {
width 100%
height 80px
border 1px solid #ffffff
border-radius 6px
cursor pointer
width: 100%;
height: 80px;
border: 1px solid #ffffff;
border-radius: 6px;
cursor: pointer;
&:hover {
border 1px solid #20a0ff
border: 1px solid #20a0ff;
}
}
.iconfont {
color #20a0ff
font-size 40px
color: #20a0ff;
font-size: 40px;
}
.opt {
display none
position absolute
top 5px
right 5px
display: none;
position: absolute;
top: 5px;
right: 5px;
}
&:hover {
.opt {
display block
display: block;
}
}
}
}
}
}
}

View File

@@ -58,33 +58,33 @@ getLicenseInfo()
})
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.foot-container {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display flex;
justify-content center
display: flex;
justify-content: center;
background: var(--theme-bg);
margin-top -4px
margin-top: -4px;
.footer {
max-width 400px;
text-align center;
font-size 14px;
padding 20px;
width 100%
max-width: 400px;
text-align: center;
font-size: 14px;
padding: 20px;
width: 100%;
a {
color:var(--text-color)
color: var(--text-color);
&:hover {
text-decoration underline
text-decoration: underline;
}
}
span{
color:var(--text-color)
span {
color: var(--text-color);
}
}
}

View File

@@ -221,7 +221,7 @@ const removeImage = (index) => {
}
</script>
<style lang="stylus">
<style lang="scss">
.image-upload {
width: 100%;
}
@@ -297,6 +297,7 @@ const removeImage = (index) => {
justify-content: center;
}
}
.upload-placeholder {
display: flex;
flex-direction: column;
@@ -311,6 +312,7 @@ const removeImage = (index) => {
.el-upload-dragger {
width: 100%;
}
.uploader {
width: 100%;
}

View File

@@ -1,19 +1,14 @@
<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
>
<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>
@@ -36,51 +31,51 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
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";
import { httpGet } from '@/utils/http'
import { dateFormat } from '@/utils/libs'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
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", () => {
ElMessage.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 })
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;
items.value = res.data.items
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.page_size
}
loading.value = false;
loading.value = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
ElMessage.error('获取数据失败' + e.message)
})
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.invite-list {
.pagination {
margin: 20px 0 0 0;
@@ -90,11 +85,11 @@ const fetchData = () => {
}
.copy-order-no {
cursor pointer
position relative
left 6px
top 2px
color #20a0ff
cursor: pointer;
position: relative;
left: 6px;
top: 2px;
color: #20a0ff;
}
}
</style>

View File

@@ -1,7 +1,12 @@
<template>
<div class="item__list-box" ref="containerRef">
<el-row :gutter="gap">
<el-col v-for="item in items" :key="item.id" :span="span" :style="{marginBottom:gap+'px'} ">
<el-col
v-for="item in items"
:key="item.id"
:span="span"
:style="{ marginBottom: gap + 'px' }"
>
<slot :item="item"></slot>
</el-col>
</el-row>
@@ -10,23 +15,23 @@
<script setup>
// 列表组件
import {onMounted, ref} from "vue";
import { onMounted, ref } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
items: {
type: Array,
required: true
required: true,
},
gap: {
type: Number,
default: 10
default: 10,
},
width: {
type: Number,
default: 240
}
});
default: 240,
},
})
const containerRef = ref(null)
const span = ref(12)
@@ -54,10 +59,8 @@ const calcSpan = () => {
window.onresize = () => calcSpan()
</script>
<style lang="stylus">
<style lang="scss">
.item__list-box {
width 100%
width: 100%;
}
</style>
</style>

View File

@@ -436,77 +436,75 @@ const doRegister = (verifyData) => {
}
</script>
<style lang="stylus">
<style lang="scss">
.login-dialog {
border-radius 10px
border-radius: 10px;
.el-tabs__nav {
display flex
width 100%
justify-content space-between
display: flex;
width: 100%;
justify-content: space-between;
}
.form {
.block {
margin-bottom 10px
margin-bottom: 10px;
}
.btn-row {
display flex
display: flex;
.login-btn {
font-size 16px
width 100%
font-size: 16px;
width: 100%;
}
.text {
line-height 40px
line-height: 40px;
.el-tag {
cursor pointer
cursor: pointer;
}
}
.forget {
margin-left 10px
margin-left: 10px;
}
}
.c-login {
display flex
display: flex;
.text {
font-size 16px
color #a1a1a1
font-size: 16px;
color: #a1a1a1;
display: flex;
align-items: center;
}
.login-type {
display flex
justify-content center
display: flex;
justify-content: center;
.iconfont {
font-size 18px
background: #E9F1F6;
font-size: 18px;
background: #e9f1f6;
padding: 8px;
border-radius: 50%;
}
.iconfont.icon-wechat {
color #0bc15f
color: #0bc15f;
}
}
}
.text {
color var(--el-text-color-primary)
color: var(--el-text-color-primary);
}
}
.register-box {
.wechat-card {
text-align center
text-align: center;
}
}
}
</style>

View File

@@ -1,263 +1,273 @@
<template>
<div class="player">
<div class="container">
<div class="cover">
<el-image :src="cover" fit="cover" />
</div>
<div class="info">
<div class="title">{{title}}</div>
<div class="style">
<span class="tags">{{ tags }}</span>
<span class="text-lightGray"> | </span>
<span class="time">{{ formatTime(currentTime) }}<span class="split">/</span>{{ formatTime(duration) }}</span>
</div>
</div>
<div class="player">
<div class="container">
<div class="cover">
<el-image :src="cover" fit="cover" />
</div>
<div class="info">
<div class="title">{{ title }}</div>
<div class="style">
<span class="tags">{{ tags }}</span>
<span class="text-lightGray"> | </span>
<span class="time"
>{{ formatTime(currentTime) }}<span class="split">/</span
>{{ formatTime(duration) }}</span
>
</div>
</div>
<div class="controls-container">
<div class="controls">
<button @click="prevSong" class="control-btn">
<i class="iconfont icon-prev"></i>
</button>
<button @click="togglePlay" class="control-btn">
<i class="iconfont icon-play" v-if="!isPlaying"></i>
<i class="iconfont icon-pause" v-else></i>
</button>
<button @click="nextSong" class="control-btn">
<i class="iconfont icon-next"></i>
</button>
</div>
</div>
<div class="controls-container">
<div class="controls">
<button @click="prevSong" class="control-btn">
<i class="iconfont icon-prev"></i>
</button>
<button @click="togglePlay" class="control-btn">
<i class="iconfont icon-play" v-if="!isPlaying"></i>
<i class="iconfont icon-pause" v-else></i>
</button>
<button @click="nextSong" class="control-btn">
<i class="iconfont icon-next"></i>
</button>
</div>
</div>
<div class="progress-bar" @click="setProgress" ref="progressBarRef">
<div class="progress" :style="{ width: `${progressPercent}%` }"></div>
</div>
<audio ref="audio" @timeupdate="updateProgress" @ended="nextSong"></audio>
<div class="progress-bar" @click="setProgress" ref="progressBarRef">
<div class="progress" :style="{ width: `${progressPercent}%` }"></div>
</div>
<audio ref="audio" @timeupdate="updateProgress" @ended="nextSong"></audio>
<el-button v-if="showClose" class="close" type="info" :icon="Close" circle size="small" @click="emits('close')" />
</div>
</div>
<el-button
v-if="showClose"
class="close"
type="info"
:icon="Close"
circle
size="small"
@click="emits('close')"
/>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, watch} from 'vue';
import {showMessageError} from "@/utils/dialog";
import {Close} from "@element-plus/icons-vue";
import {formatTime} from "@/utils/libs";
import {httpGet} from "@/utils/http"
import { showMessageError } from '@/utils/dialog'
import { httpGet } from '@/utils/http'
import { formatTime } from '@/utils/libs'
import { Close } from '@element-plus/icons-vue'
import { onMounted, ref, watch } from 'vue'
const audio = ref(null);
const isPlaying = ref(false);
const songIndex = ref(0);
const currentTime = ref(0);
const duration = ref(100);
const progressPercent = ref(0);
const audio = ref(null)
const isPlaying = ref(false)
const songIndex = ref(0)
const currentTime = ref(0)
const duration = ref(100)
const progressPercent = ref(0)
const progressBarRef = ref(null)
const title = ref("")
const tags = ref("")
const cover = ref("")
const title = ref('')
const tags = ref('')
const cover = ref('')
// eslint-disable-next-line no-undef
const props = defineProps({
songs: {
type: Array,
required: true,
default: () => []
default: () => [],
},
showClose: {
type: Boolean,
default: false
}
});
default: false,
},
})
// eslint-disable-next-line no-undef
const emits = defineEmits(['close','play']);
watch(() => props.songs, (newVal) => {
loadSong(newVal[songIndex.value]);
});
const emits = defineEmits(['close', 'play'])
watch(
() => props.songs,
(newVal) => {
loadSong(newVal[songIndex.value])
}
)
const loadSong = (song) => {
if (!song) {
showMessageError("歌曲加载失败")
showMessageError('歌曲加载失败')
return
}
title.value = song.title
tags.value = song.tags
cover.value = song.cover_url
audio.value.src = song.audio_url;
audio.value.load();
audio.value.src = song.audio_url
audio.value.load()
isPlaying.value = false
audio.value.onloadedmetadata = () => {
duration.value = audio.value.duration;
};
};
duration.value = audio.value.duration
}
}
const togglePlay = () => {
if (isPlaying.value) {
audio.value.pause();
audio.value.pause()
isPlaying.value = false
} else {
play()
}
};
}
const play = () => {
if (isPlaying.value) {
return
}
audio.value.play();
audio.value.play()
isPlaying.value = true
if (audio.value.currentTime === 0) {
emits("play")
emits('play')
// 增加播放数量
httpGet("/api/suno/play",{song_id:props.songs[songIndex.value].song_id}).then().catch()
httpGet('/api/suno/play', { song_id: props.songs[songIndex.value].song_id }).then().catch()
}
}
const prevSong = () => {
songIndex.value = (songIndex.value - 1 + props.songs.length) % props.songs.length;
loadSong(props.songs[songIndex.value]);
audio.value.play();
isPlaying.value = true;
};
songIndex.value = (songIndex.value - 1 + props.songs.length) % props.songs.length
loadSong(props.songs[songIndex.value])
audio.value.play()
isPlaying.value = true
}
const nextSong = () => {
songIndex.value = (songIndex.value + 1) % props.songs.length;
loadSong(props.songs[songIndex.value]);
audio.value.play();
isPlaying.value = true;
};
songIndex.value = (songIndex.value + 1) % props.songs.length
loadSong(props.songs[songIndex.value])
audio.value.play()
isPlaying.value = true
}
const updateProgress = () => {
try {
currentTime.value = audio.value.currentTime;
progressPercent.value = (currentTime.value / duration.value) * 100;
currentTime.value = audio.value.currentTime
progressPercent.value = (currentTime.value / duration.value) * 100
} catch (e) {
console.error(e.message)
}
};
}
const setProgress = (event) => {
const totalWidth = progressBarRef.value.offsetWidth;
const clickX = event.offsetX;
const audioDuration = audio.value.duration;
audio.value.currentTime = (clickX / totalWidth) * audioDuration;
};
const totalWidth = progressBarRef.value.offsetWidth
const clickX = event.offsetX
const audioDuration = audio.value.duration
audio.value.currentTime = (clickX / totalWidth) * audioDuration
}
// eslint-disable-next-line no-undef
defineExpose({
play
});
play,
})
onMounted(() => {
loadSong(props.songs[songIndex.value]);
});
loadSong(props.songs[songIndex.value])
})
</script>
<style lang="stylus" scoped>
<style lang="scss" scoped>
.player {
display flex
justify-content center
width 100%
display: flex;
justify-content: center;
width: 100%;
.container {
display flex
display: flex;
background-color: #363030;
border-radius: 10px;
border 1px solid #544F4F;
border: 1px solid #544f4f;
padding: 5px;
width: 80%
width: 80%;
text-align: center;
position relative
overflow hidden
position: relative;
overflow: hidden;
.cover {
.el-image {
border-radius: 50%;
width 50px
width: 50px;
}
}
.info {
padding 0 10px
min-width 300px
display flex
justify-content center
align-items flex-start
flex-flow column
line-height 1.5
padding: 0 10px;
min-width: 300px;
display: flex;
justify-content: center;
align-items: flex-start;
flex-flow: column;
line-height: 1.5;
.title {
font-weight 700
font-size 16px
color #ffffff
font-weight: 700;
font-size: 16px;
color: #ffffff;
}
.style {
font-size 14px
display flex
color #e1e1e1
font-size: 14px;
display: flex;
color: #e1e1e1;
.tags {
font-weight 600
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 隐藏溢出的文本 */
text-overflow: ellipsis; /* 使用省略号表示溢出的文本 */
max-width 200px
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.text-lightGray {
color: rgb(114 110 108);
padding 0 3px
color: rgb(114, 110, 108);
padding: 0 3px;
}
.time {
font-family 'Input Sans'
font-weight 700
font-family: 'Input Sans';
font-weight: 700;
.split {
font-size 12px
position relative
top -2px
margin 0 1px 0 3px
font-size: 12px;
position: relative;
top: -2px;
margin: 0 1px 0 3px;
}
}
}
}
.controls-container {
width 100%
display flex
flex-flow column
justify-content center
width: 100%;
display: flex;
flex-flow: column;
justify-content: center;
.controls {
display: flex;
justify-content: space-around;
margin-bottom 10px
margin-bottom: 10px;
.control-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
background-color #363030
border-radius 5px
padding 6px
background-color: #363030;
border-radius: 5px;
padding: 6px;
.iconfont {
font-size 20px
font-size: 20px;
}
&:hover {
background-color #5F5958
background-color: #5f5958;
}
}
}
}
.progress-bar {
position absolute
width 100%
left 0
bottom 0
position: absolute;
width: 100%;
left: 0;
bottom: 0;
height: 8px;
background-color: #555;
cursor: pointer;
@@ -268,15 +278,13 @@ onMounted(() => {
border-radius: 5px;
width: 0;
}
}
.close {
position absolute
right 10px
top 15px
position: absolute;
right: 10px;
top: 15px;
}
}
}
</style>

View File

@@ -1,23 +1,23 @@
<template>
<el-dialog
class="password-dialog"
v-model="showDialog"
:close-on-click-modal="true"
:show-close="true"
style="max-width: 600px"
:before-close="close"
title="修改密码"
class="password-dialog"
v-model="showDialog"
:close-on-click-modal="true"
:show-close="true"
style="max-width: 600px"
:before-close="close"
title="修改密码"
>
<div class="form" id="password-form">
<el-form :model="form" label-width="120px">
<el-form-item label="原始密码">
<el-input v-model="form['old_pass']" type="password"/>
<el-input v-model="form['old_pass']" type="password" />
</el-form-item>
<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>
@@ -25,64 +25,64 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="close">关闭</el-button>
<el-button type="primary" @click="save">
保存
</el-button>
<el-button type="primary" @click="save"> 保存 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref} from "vue"
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import { httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
const props = defineProps({
show: Boolean,
});
})
const showDialog = computed(() => {
return props.show
})
const form = ref({})
const emits = defineEmits(['hide', 'logout']);
const emits = defineEmits(['hide', 'logout'])
const save = function () {
if (!form.value['password'] || form.value['password'].length < 8) {
return ElMessage.error({message: "密码的长度为8-16个字符", appendTo: "#password-form"});
return ElMessage.error({ message: '密码的长度为8-16个字符', appendTo: '#password-form' })
}
if (form.value['repass'] !== form.value['password']) {
return ElMessage.error({message: '两次输入密码不一致', appendTo: '#password-form'});
return ElMessage.error({ message: '两次输入密码不一致', appendTo: '#password-form' })
}
httpPost('/api/user/password', form.value).then(() => {
ElMessage.success({
message: '更新成功',
appendTo: '#password-form',
duration: 1000,
onClose: () => emits('logout', false)
httpPost('/api/user/password', form.value)
.then(() => {
ElMessage.success({
message: '更新成功',
appendTo: '#password-form',
duration: 1000,
onClose: () => emits('logout', false),
})
})
}).catch((e) => {
ElMessage.error({
message: '更新失败,' + e.message,
appendTo: '#password-form'
.catch((e) => {
ElMessage.error({
message: '更新失败,' + e.message,
appendTo: '#password-form',
})
})
})
}
const close = function () {
emits('hide', false);
emits('hide', false)
}
</script>
<style lang="stylus">
<style lang="scss">
.password-dialog {
.el-dialog {
--el-dialog-width 90%;
max-width 650px;
--el-dialog-width: 90%;
max-width: 650px;
.el-dialog__body {
overflow-y auto;
overflow-y: auto;
.form {
position relative;
position: relative;
.el-message {
position: absolute;
@@ -91,4 +91,4 @@ const close = function () {
}
}
}
</style>
</style>

View File

@@ -324,6 +324,6 @@ const hangUp = async () => {
defineExpose({ connect, hangUp })
</script>
<style lang="stylus" scoped>
@import "../assets/css/realtime.styl"
<style lang="scss" scoped>
@use '../assets/css/realtime.scss' as *;
</style>

View File

@@ -1,38 +1,36 @@
<template>
<el-dialog
v-model="showDialog"
:close-on-click-modal="true"
:before-close="close"
:width="450"
:title="title"
v-model="showDialog"
:close-on-click-modal="true"
:before-close="close"
:width="450"
:title="title"
>
<div class="form" id="bind-mobile-form">
<el-form :model="form">
<el-form-item>
<el-input v-model="form.code"/>
<el-input v-model="form.code" />
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="save">
立即兑换
</el-button>
<el-button type="primary" @click="save"> 立即兑换 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref} from "vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
const props = defineProps({
show: Boolean,
});
})
const showDialog = computed(() => {
return props.show
@@ -43,26 +41,26 @@ const form = ref({
code: '',
})
const emits = defineEmits(['hide']);
const emits = defineEmits(['hide'])
const save = () => {
if (form.value.code === '') {
return ElMessage.error({message: "请输入兑换码"});
return ElMessage.error({ message: '请输入兑换码' })
}
httpPost('/api/redeem/verify', form.value).then(() => {
showMessageOK("兑换成功!")
emits('hide', true)
}).catch(e => {
showMessageError("兑换失败:" + e.message)
})
httpPost('/api/redeem/verify', form.value)
.then(() => {
showMessageOK('兑换成功!')
emits('hide', true)
})
.catch((e) => {
showMessageError('兑换失败:' + e.message)
})
}
const close = function () {
emits('hide', false);
emits('hide', false)
}
</script>
<style scoped>
</style>
<style scoped lang="scss"></style>

View File

@@ -1,6 +1,13 @@
<template>
<div class="reset-pass">
<el-dialog v-model="showDialog" :close-on-click-modal="true" width="500px" :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">
@@ -47,82 +54,81 @@
</template>
<script setup>
import { computed, ref } from "vue";
import SendMsg from "@/components/SendMsg.vue";
import { ElMessage } from "element-plus";
import { httpPost } from "@/utils/http";
import { validateEmail, validateMobile } from "@/utils/validate";
import SendMsg from '@/components/SendMsg.vue'
import { httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
const props = defineProps({
show: Boolean,
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 === "") {
return ElMessage.error("请输入验证码");
if (form.value.code === '') {
return ElMessage.error('请输入验证码')
}
if (form.value.password.length < 8) {
return ElMessage.error("密码长度必须大于8位");
return ElMessage.error('密码长度必须大于8位')
}
if (form.value.repass !== form.value.password) {
return ElMessage.error("两次输入密码不一致");
return ElMessage.error('两次输入密码不一致')
}
httpPost("/api/user/resetPass", form.value)
httpPost('/api/user/resetPass', form.value)
.then(() => {
ElMessage.success({
message: "重置密码成功",
message: '重置密码成功',
duration: 1000,
onClose: () => emits("hide", false),
});
onClose: () => emits('hide', false),
})
})
.catch((e) => {
ElMessage.error("重置密码失败:" + e.message);
});
};
ElMessage.error('重置密码失败:' + e.message)
})
}
const close = function () {
emits("hide", false);
};
emits('hide', false)
}
</script>
<style lang="stylus">
<style lang="scss">
.reset-pass {
.form {
padding 0 20px
padding: 0 20px;
}
.code-row {
width 100%
width: 100%;
.send-button {
padding-left 10px
padding-left: 10px;
}
}
.reset-pass-dialog {
.el-dialog__footer {
text-align center
padding-top 0
text-align: center;
padding-top: 0;
}
.el-dialog__body {
padding 0
padding: 0;
}
}
}

View File

@@ -1,5 +1,10 @@
<template>
<el-dialog v-model="show" :fullscreen="true" @close="close" style="--el-dialog-border-radius: 0px">
<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>
@@ -121,52 +126,52 @@
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import Clipboard from "clipboard";
import { showMessageOK, showMessageError } from "@/utils/dialog";
import { showMessageError, showMessageOK } from '@/utils/dialog'
import Clipboard from 'clipboard'
import { onMounted, ref, watch } from 'vue'
const props = defineProps({
modelValue: Boolean,
data: Object,
});
})
const item = ref(props.data);
const show = ref(props.modelValue);
const emit = defineEmits(["drawSame", "close"]);
const item = ref(props.data)
const show = ref(props.modelValue)
const emit = defineEmits(['drawSame', 'close'])
const clipboard = ref(null);
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on("success", () => {
showMessageOK("复制成功!");
});
clipboard.value = new Clipboard('.copy-prompt-wall')
clipboard.value.on('success', () => {
showMessageOK('复制成功!')
})
clipboard.value.on("error", () => {
showMessageError("复制失败!");
});
});
clipboard.value.on('error', () => {
showMessageError('复制失败!')
})
})
watch(
() => props.modelValue,
(newValue) => {
show.value = newValue;
show.value = newValue
}
);
)
watch(
() => props.data,
(newValue) => {
item.value = newValue;
item.value = newValue
}
);
)
const drawSame = (item) => {
emit("drawSame", item);
};
emit('drawSame', item)
}
const close = () => {
emit("close");
};
emit('close')
}
</script>
<style lang="stylus" scoped></style>
<style lang="scss" scoped></style>

View File

@@ -10,12 +10,12 @@
<script setup>
// 发送短信验证码组件
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 Captcha from '@/components/Captcha.vue'
import { getSystemInfo } from '@/store/cache'
import { httpPost } from '@/utils/http'
import { validateEmail, validateMobile } from '@/utils/validate'
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
@@ -23,73 +23,72 @@ 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"];
});
enableVerify.value = res.data['enabled_verify']
})
const sendMsg = () => {
if (!validateMobile(props.receiver) && props.type === "mobile") {
return ElMessage.error("请输入合法的手机号");
if (!validateMobile(props.receiver) && props.type === 'mobile') {
return ElMessage.error('请输入合法的手机号')
}
if (!validateEmail(props.receiver) && props.type === "email") {
return ElMessage.error("请输入合法的邮箱地址");
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", {
canSend.value = false
httpPost('/api/sms/code', {
receiver: props.receiver,
key: data.key,
dots: data.dots,
x: data.x,
})
.then(() => {
if (props.type === "mobile") {
ElMessage.success("验证码发送成功");
} else if (props.type === "email") {
ElMessage.success("验证码已发送至邮箱,如果长时间未收到,请检查是否在垃圾邮件中!");
if (props.type === 'mobile') {
ElMessage.success('验证码发送成功')
} else if (props.type === 'email') {
ElMessage.success('验证码已发送至邮箱,如果长时间未收到,请检查是否在垃圾邮件中!')
}
let time = 60;
btnText.value = time;
let time = 60
btnText.value = time
const handler = setInterval(() => {
time = time - 1;
time = time - 1
if (time <= 0) {
clearInterval(handler);
btnText.value = "重新发送";
canSend.value = true;
clearInterval(handler)
btnText.value = '重新发送'
canSend.value = true
} else {
btnText.value = time;
btnText.value = time
}
}, 1000);
}, 1000)
})
.catch((e) => {
canSend.value = true;
ElMessage.error("验证码发送失败:" + e.message);
});
};
canSend.value = true
ElMessage.error('验证码发送失败:' + e.message)
})
}
</script>
<style lang="stylus" scoped>
<style lang="scss" scoped>
.send-verify-code {
.send-btn {
width: 100%;

View File

@@ -34,147 +34,152 @@
<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 {
ArrowRightBold,
CircleCheckFilled,
CircleCloseFilled,
Refresh,
} from '@element-plus/icons-vue'
import { onMounted, ref, watch } from '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("");
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;
backgroundImg.value = newVal
}
);
)
watch(
() => props.bkImg,
(newVal) => {
blockImg.value = newVal;
blockImg.value = newVal
}
);
)
watch(
() => props.result,
(newVal) => {
checked.value = 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);
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 = "验证失败";
verifyMsgClass.value = 'verify-text error'
blockClass.value = 'verify-move-block error'
leftBarClass.value = 'verify-left-bar error'
verifyMsg.value = '验证失败'
setTimeout(() => {
reset();
emits("refresh");
}, 1000);
reset()
emits('refresh')
}, 1000)
} else {
reset();
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;
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();
evt.preventDefault();
});
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);
blockLeft.value = x;
leftBarWidth.value = x + 32;
});
const x = Math.max(evt.clientX - offsetX, 0)
blockLeft.value = x
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) {
isDragging = true;
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();
e.preventDefault();
});
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 = ''
offsetX = e.touches[0].clientX - dragBlock.getBoundingClientRect().left
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);
blockLeft.value = x;
leftBarWidth.value = x + 32;
});
e.preventDefault()
const x = Math.max(e.touches[0].clientX - offsetX, 0)
blockLeft.value = x
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">
<style scoped lang="scss">
@keyframes expandUp {
0% {
transform: scaleY(0);
@@ -185,21 +190,21 @@ const reset = () => {
}
.slide-captcha {
width 310px
width: 310px;
* {
margin 0
padding 0
margin: 0;
padding: 0;
}
.bg-img {
position relative
width 310px
position: relative;
width: 310px;
.verify-text {
position absolute
bottom 3px
padding 5px 10px
width 290px
color #ffffff
position: absolute;
bottom: 3px;
padding: 5px 10px;
width: 290px;
color: #ffffff;
animation: expandUp 0.3s ease-in-out forwards;
transform-origin: bottom center;
@@ -207,98 +212,96 @@ const reset = () => {
}
.verify-text.success {
background-color rgba(92,184,92, 0.5)
background-color: rgba(92, 184, 92, 0.5);
}
.verify-text.error {
background-color rgba(184,92,92, 0.5)
background-color: rgba(184, 92, 92, 0.5);
}
.refresh {
position absolute
right 5px
top 5px
font-size 20px
cursor pointer
color #ffffff
position: absolute;
right: 5px;
top: 5px;
font-size: 20px;
cursor: pointer;
color: #ffffff;
}
.block {
position absolute
top 0
left 0
position: absolute;
top: 0;
left: 0;
}
}
.verify {
.verify-bar-area {
position relative
border: 1px solid #dddddd
overflow hidden
height 34px
position: relative;
border: 1px solid #dddddd;
overflow: hidden;
height: 34px;
.verify-msg {
display flex
line-height 32px
width 100%
justify-content center
display: flex;
line-height: 32px;
width: 100%;
justify-content: center;
}
.verify-left-bar {
position absolute
left 0
top 0
height 32px;
position: absolute;
left: 0;
top: 0;
height: 32px;
.verify-move-block {
position absolute
position: absolute;
width: 32px;
height: 32px;
background-color: rgb(255, 255, 255);
border-top 1px solid #ffffff
border-bottom 1px solid #ffffff
border-right 1px solid #dddddd
border-top: 1px solid #ffffff;
border-bottom: 1px solid #ffffff;
border-right: 1px solid #dddddd;
display flex
justify-content center
align-items center
display: flex;
justify-content: center;
align-items: center;
.el-icon {
font-size 20px
cursor pointer
font-size: 20px;
cursor: pointer;
}
}
.verify-move-block.active {
background #409eff
color #ffffff
border 1px solid #409eff
background: #409eff;
color: #ffffff;
border: 1px solid #409eff;
}
.verify-move-block.success {
background #57AD57
color #ffffff
border 1px solid #57AD57
background: #57ad57;
color: #ffffff;
border: 1px solid #57ad57;
}
.verify-move-block.error {
background #D9534F
color #ffffff
border 1px solid #D9534F
background: #d9534f;
color: #ffffff;
border: 1px solid #d9534f;
}
}
.verify-left-bar.active {
background-color #F0FFF0
border 1px solid #409eff
background-color: #f0fff0;
border: 1px solid #409eff;
}
.verify-left-bar.success {
background-color #F0FFF0
border 1px solid #57AD57
background-color: #f0fff0;
border: 1px solid #57ad57;
}
.verify-left-bar.error {
background-color #F0FFF0
border 1px solid #D9534F
background-color: #f0fff0;
border: 1px solid #d9534f;
}
}
}

View File

@@ -33,6 +33,6 @@ const props = defineProps({
})
</script>
<style lang="stylus" scoped>
@import "../assets/css/running-job-list.styl"
<style lang="scss" scoped>
@use '../assets/css/running-job-list.scss' as *;
</style>

View File

@@ -5,51 +5,53 @@
</template>
<script setup>
import { ref } from "vue";
import { useSharedStore } from "@/store/sharedata";
import { useSharedStore } from '@/store/sharedata'
import { ref } from 'vue'
const props = defineProps({
size: {
type: String,
default: "",
default: '',
},
});
})
// 定义主题状态,初始值从 localStorage 获取
const store = useSharedStore();
const themePage = ref(store.theme || "light");
const store = useSharedStore()
const themePage = ref(store.theme || 'light')
// 切换主题函数
const toggleTheme = () => {
themePage.value = themePage.value === "light" ? "dark" : "light";
store.setTheme(themePage.value); // 保存主题
};
themePage.value = themePage.value === 'light' ? 'dark' : 'light'
store.setTheme(themePage.value) // 保存主题
}
</script>
<style lang="stylus" scoped>
.theme-box{
z-index :111
<style lang="scss" scoped>
.theme-box {
z-index: 111;
position: fixed;
right: 40px;
bottom: 150px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 50%;
width 35px;
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{
&:hover {
transform: scale(1.1);
}
&:active{
&:active {
transform: scale(0.9);
}
.iconfont{
.iconfont {
font-size: 20px;
color: yellow;
transition: transform 0.3s ease;

View File

@@ -1,10 +1,10 @@
<template>
<el-dialog
v-model="showDialog"
:close-on-click-modal="true"
style="max-width: 400px"
@close="close"
:title="title"
v-model="showDialog"
:close-on-click-modal="true"
style="max-width: 400px"
@close="close"
:title="title"
>
<div class="third-login" v-loading="loading">
<div class="item" v-if="wechatBindURL !== ''">
@@ -17,21 +17,20 @@
</template>
<script setup>
import {computed, ref, watch} from "vue";
import {httpGet} from "@/utils/http";
import {checkSession} from "@/store/cache";
import {showMessageError} from "@/utils/dialog";
import { checkSession } from '@/store/cache'
import { showMessageError } from '@/utils/dialog'
import { httpGet } from '@/utils/http'
import { computed, ref, watch } from 'vue'
const props = defineProps({
show: Boolean,
});
const emits = defineEmits(['hide']);
})
const emits = defineEmits(['hide'])
const showDialog = computed(() => {
return props.show
})
const title = ref('绑定第三方登录')
const openid = ref('')
const wechatBindURL = ref('')
@@ -39,55 +38,57 @@ const loading = ref(true)
watch(showDialog, (val) => {
if (val) {
checkSession().then(user => {
checkSession().then((user) => {
openid.value = user.openid
})
const returnURL = `${location.protocol}//${location.host}/login/callback?action=bind`
httpGet("/api/user/clogin?return_url="+returnURL).then(res => {
wechatBindURL.value = res.data.url
loading.value = false
}).catch(e => {
showMessageError(e.message)
})
httpGet('/api/user/clogin?return_url=' + returnURL)
.then((res) => {
wechatBindURL.value = res.data.url
loading.value = false
})
.catch((e) => {
showMessageError(e.message)
})
}
})
const close = function () {
emits('hide');
emits('hide')
}
</script>
<style lang="stylus" scoped>
<style lang="scss" scoped>
.third-login {
display flex
justify-content center
min-height 100px
display: flex;
justify-content: center;
min-height: 100px;
.item {
display flex
flex-flow column
align-items center
display: flex;
flex-flow: column;
align-items: center;
.link {
display flex
display: flex;
.iconfont {
font-size 30px
cursor pointer
background #e9f1f6
padding 10px
border-radius 50%
font-size: 30px;
cursor: pointer;
background: #e9f1f6;
padding: 10px;
border-radius: 50%;
}
margin-bottom 10px
margin-bottom: 10px;
}
.text {
font-size 14px
font-size: 14px;
}
.icon-wechat,.ok {
.icon-wechat,
.ok {
color: #0bc15f;
}
}
}
</style>
</style>

View File

@@ -1,5 +1,12 @@
<template>
<el-dialog class="config-dialog" v-model="showDialog" :close-on-click-modal="true" :before-close="close" style="max-width: 400px" title="账户信息">
<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 pl-4 pr-4" id="user-info">
<user-profile @hide="close" />
</div>
@@ -7,30 +14,30 @@
</template>
<script setup>
import { computed } from "vue";
import UserProfile from "@/components/UserProfile.vue";
import UserProfile from '@/components/UserProfile.vue'
import { computed } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
show: Boolean,
});
})
const showDialog = computed(() => {
return props.show;
});
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">
<style lang="scss">
.config-dialog {
.el-dialog {
--el-dialog-width 90%;
max-width 800px;
--el-dialog-width: 90%;
max-width: 800px;
}
}
</style>

View File

@@ -21,7 +21,7 @@
<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>
@@ -45,52 +45,52 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
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";
import { httpGet } from '@/utils/http'
import { dateFormat } from '@/utils/libs'
import { DocumentCopy } from '@element-plus/icons-vue'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
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", () => {
ElMessage.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 })
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;
items.value = res.data.items
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.page_size
}
loading.value = false;
loading.value = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
ElMessage.error('获取数据失败' + e.message)
})
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.user-bill {
background-color: var(--chat-bg);
@@ -102,11 +102,11 @@ const fetchData = () => {
}
.copy-order-no {
cursor pointer
position relative
left 6px
top 2px
color #20a0ff
cursor: pointer;
position: relative;
left: 6px;
top: 2px;
color: #20a0ff;
}
}
</style>

View File

@@ -2,7 +2,13 @@
<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-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>
@@ -18,19 +24,27 @@
<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>
<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-text type="warning">{{ user["power"] }}</el-text>
<el-tag type="info" size="small" class="ml-2 cursor-pointer" @click="gotoLog">算力日志</el-tag>
<el-tooltip :content="`每日签到可获得 ${systemConfig.daily_power} 算力`" placement="top" v-if="systemConfig.daily_power > 0">
<el-text type="warning">{{ user['power'] }}</el-text>
<el-tag type="info" size="small" class="ml-2 cursor-pointer" @click="gotoLog"
>算力日志</el-tag
>
<el-tooltip
:content="`每日签到可获得 ${systemConfig.daily_power} 算力`"
placement="top"
v-if="systemConfig.daily_power > 0"
>
<el-button type="primary" size="small" @click="signIn" class="ml-2">签到</el-button>
</el-tooltip>
</el-form-item>
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
<el-tag type="danger">{{ dateFormat(user["expired_time"]) }}</el-tag>
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
</el-form-item>
<el-row class="opt-line">
@@ -41,116 +55,114 @@
</template>
<script setup>
import { onMounted, ref } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import Compressor from "compressorjs";
import { dateFormat } from "@/utils/libs";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { checkSession, getSystemInfo } from '@/store/cache'
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { dateFormat } from '@/utils/libs'
import { Plus } from '@element-plus/icons-vue'
import Compressor from 'compressorjs'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const user = ref({
vip: false,
username: "演示数据",
nickname: "演示数据",
avatar: "/images/menu/member.png",
mobile: "演示数据",
username: '演示数据',
nickname: '演示数据',
avatar: '/images/menu/member.png',
mobile: '演示数据',
power: 99999,
});
})
const vipImg = ref("/images/menu/member.png");
const systemConfig = ref({});
const router = useRouter();
const emits = defineEmits(["hide"]);
const vipImg = ref('/images/menu/member.png')
const systemConfig = ref({})
const router = useRouter()
const emits = defineEmits(['hide'])
onMounted(() => {
checkSession()
.then(() => {
// 获取最新用户信息
httpGet("/api/user/profile")
httpGet('/api/user/profile')
.then((res) => {
user.value = res.data;
user.value = res.data
})
.catch((e) => {
ElMessage.error("获取用户信息失败:" + e.message);
});
ElMessage.error('获取用户信息失败:' + e.message)
})
})
.catch((e) => {
console.log(e);
});
console.log(e)
})
getSystemInfo().then((res) => {
systemConfig.value = res.data;
});
});
systemConfig.value = res.data
})
})
const afterRead = (file) => {
// 压缩图片并上传
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append("file", result, result.name);
const formData = new FormData()
formData.append('file', result, result.name)
// 执行上传操作
httpPost("/api/upload", formData)
httpPost('/api/upload', formData)
.then((res) => {
user.value.avatar = res.data.url;
ElMessage.success({ message: "上传成功", duration: 500 });
user.value.avatar = res.data.url
ElMessage.success({ message: '上传成功', duration: 500 })
})
.catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
});
ElMessage.error('图片上传失败:' + e.message)
})
},
error(err) {
console.log(err.message);
console.log(err.message)
},
});
};
})
}
const save = () => {
httpPost("/api/user/profile/update", user.value)
httpPost('/api/user/profile/update', user.value)
.then(() => {
ElMessage.success({ message: "更新成功", duration: 500 });
ElMessage.success({ message: '更新成功', duration: 500 })
})
.catch((e) => {
ElMessage.error("更新失败:" + e.message);
});
};
ElMessage.error('更新失败:' + e.message)
})
}
const gotoLog = () => {
router.push("/powerLog");
emits("hide", false);
};
router.push('/powerLog')
emits('hide', false)
}
const signIn = () => {
httpGet("/api/user/signin")
httpGet('/api/user/signin')
.then(() => {
showMessageOK("签到成功");
user.value.power += systemConfig.value.daily_power;
showMessageOK('签到成功')
user.value.power += systemConfig.value.daily_power
})
.catch((e) => {
showMessageError(e.message);
});
};
showMessageError(e.message)
})
}
</script>
<style lang="stylus" scoped>
<style lang="scss" scoped>
.user-info {
.el-row {
justify-content center
margin-bottom 10px
justify-content: center;
margin-bottom: 10px;
}
.vip-icon {
position relative
top 5px
position: relative;
top: 5px;
}
.opt-line {
.el-button {
width 100%
width: 100%;
}
}
}

View File

@@ -116,54 +116,54 @@ const send = (text) => {
emits('send', text)
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.welcome {
text-align center
display flex
justify-content center
margin-top 8vh
text-align: center;
display: flex;
justify-content: center;
margin-top: 8vh;
.container {
max-width 768px;
width 100%
max-width: 768px;
width: 100%;
.title {
// font-size: 2.25rem
line-height: 2.5rem
font-weight 600
margin-bottom: 4rem
color var(--text-color)
line-height: 2.5rem;
font-weight: 600;
margin-bottom: 4rem;
color: var(--text-color);
}
.grid-content {
.item-title {
div {
padding 6px 10px;
padding: 6px 10px;
.iconfont {
font-size 24px;
font-size: 24px;
}
}
}
.list-box {
ul {
padding 10px;
padding: 10px;
li {
font-size 14px;
padding .75rem
border-radius 5px;
font-size: 14px;
padding: 0.75rem;
border-radius: 5px;
background-color: var(--chat-wel-bg);
color:var( --theme-text-color-secondary);
line-height 1.5
color: var(--theme-text-color-secondary);
line-height: 1.5;
a {
cursor pointer
display block
width 100%
cursor: pointer;
display: block;
width: 100%;
}
margin-top 10px;
margin-top: 10px;
}
}
}

View File

@@ -151,15 +151,15 @@ const logout = function () {
})
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.admin-header {
position: relative;
box-sizing: border-box;
overflow hidden
overflow: hidden;
height: 50px;
font-size: 22px;
background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
background-color: var(--chat-content-bg);
color: var(--theme-text-color-primary);
.collapse-btn {
display: flex;
@@ -171,15 +171,15 @@ const logout = function () {
cursor: pointer;
&:hover {
background-color #eaecef
background-color: #eaecef;
}
}
.breadcrumb {
float left
display flex
align-items center
height 50px
float: left;
display: flex;
align-items: center;
height: 50px;
}
.header-right {
@@ -226,7 +226,6 @@ const logout = function () {
}
.user-avatar {
}
}
}
@@ -243,27 +242,26 @@ const logout = function () {
text-align: center;
.icon-reward {
font-size 18px;
font-weight bold;
color #F56C6C
font-size: 18px;
font-weight: bold;
color: #f56c6c;
}
}
</style>
<style lang="stylus">
<style lang="scss">
.donate-dialog {
.el-dialog__body {
text-align center;
text-align: center;
.el-alert__description {
text-align left
font-size 14px;
line-height 1.5
text-align: left;
font-size: 14px;
line-height: 1.5;
}
}
}
.admin-header {
}
</style>

View File

@@ -234,7 +234,7 @@ const sidebar = useSidebarStore()
setMenuItems(items)
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.sidebar {
display: block;
position: absolute;
@@ -244,24 +244,24 @@ setMenuItems(items)
overflow-y: scroll;
.logo {
display flex
padding 6px 15px;
cursor pointer
background-color: #324157
display: flex;
padding: 6px 15px;
cursor: pointer;
background-color: #324157;
img {
height 36px;
padding-top 5px;
border-radius 100%
background #fff
border 2px solid #754ff6
padding 2px
height: 36px;
padding-top: 5px;
border-radius: 100%;
background: #fff;
border: 2px solid #754ff6;
padding: 2px;
}
.text {
color #ffffff
font-weight bold
padding 12px 0 12px 10px;
color: #ffffff;
font-weight: bold;
padding: 12px 0 12px 10px;
transition: width 2s ease;
}
}
@@ -269,15 +269,16 @@ setMenuItems(items)
ul {
height: 100%;
.el-menu-item, .el-sub-menu {
.el-menu-item,
.el-sub-menu {
.iconfont {
font-size 16px;
margin-right 5px;
font-size: 16px;
margin-right: 5px;
}
}
.el-menu-item.is-active {
background-color rgb(40, 52, 70)
background-color: rgb(40, 52, 70);
}
}
@@ -291,26 +292,26 @@ setMenuItems(items)
}
.sidebar.dark {
border-right 1px solid var(--el-border-color-dark)
border-right: 1px solid var(--el-border-color-dark);
.logo {
background var(--el-bg-color)
border-right 1px solid var(--el-border-color)
background: var(--el-bg-color);
border-right: 1px solid var(--el-border-color);
.text {
color var(--el-text-color-regular)
color: var(--el-text-color-regular);
}
}
ul {
background var(--el-bg-color)
background: var(--el-bg-color);
.el-menu-item.is-active {
background-color var(--el-menu-bg-color-dark)
background-color: var(--el-menu-bg-color-dark);
}
.el-menu-item:hover {
background-color var(--el-menu-bg-color-darker)
background-color: var(--el-menu-bg-color-darker);
}
}
@@ -319,7 +320,7 @@ setMenuItems(items)
}
.el-menu {
border-color var(--el-border-color)
border-color: var(--el-border-color);
}
}
</style>

View File

@@ -1,15 +1,15 @@
<template>
<div :class="'tags '+theme" v-if="tags.show">
<div :class="'tags ' + theme" v-if="tags.show">
<ul>
<li
class="tags-li"
v-for="(item, index) in tags.list"
:class="{ active: isActive(item.path) }"
:key="index"
class="tags-li"
v-for="(item, index) in tags.list"
:class="{ active: isActive(item.path) }"
:key="index"
>
<router-link :to="item.path" class="tags-li-title">{{ item.title }}</router-link>
<el-icon @click="closeTags(index)">
<Close/>
<Close />
</el-icon>
</li>
</ul>
@@ -18,7 +18,7 @@
<el-button size="small" type="info">
标签选项
<el-icon class="el-icon--right">
<arrow-down/>
<arrow-down />
</el-icon>
</el-button>
<template #dropdown>
@@ -33,80 +33,83 @@
</template>
<script setup>
import {useTagsStore} from '@/store/tags';
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {ArrowDown, Close} from "@element-plus/icons-vue";
import {checkAdminSession} from "@/store/cache";
import {ElMessageBox} from "element-plus";
import {useSharedStore} from "@/store/sharedata";
import {ref, watch} from "vue";
import { checkAdminSession } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import { useTagsStore } from '@/store/tags'
import { ArrowDown, Close } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import { ref, watch } from 'vue'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
const store = useSharedStore()
const theme = ref(store.theme)
watch(() => store.theme, (val) => {
theme.value = val
})
const router = useRouter();
watch(
() => store.theme,
(val) => {
theme.value = val
}
)
const router = useRouter()
checkAdminSession().catch(() => {
ElMessageBox({
title: '提示',
message: "当前会话已经失效,请重新登录",
message: '当前会话已经失效,请重新登录',
confirmButtonText: 'OK',
callback: () => router.replace('/admin/login')
});
callback: () => router.replace('/admin/login'),
})
})
const isActive = (path) => {
return path === route.fullPath;
};
return path === route.fullPath
}
const tags = useTagsStore();
const route = useRoute();
const tags = useTagsStore()
const route = useRoute()
// 关闭单个标签
const closeTags = (index) => {
const delItem = tags.list[index];
tags.delTagsItem(index);
const item = tags.list[index] ? tags.list[index] : tags.list[index - 1];
const delItem = tags.list[index]
tags.delTagsItem(index)
const item = tags.list[index] ? tags.list[index] : tags.list[index - 1]
if (item) {
delItem.path === route.fullPath && router.push(item.path);
delItem.path === route.fullPath && router.push(item.path)
} else {
router.push('/admin');
router.push('/admin')
}
};
}
// 设置标签
const setTags = (route) => {
const isExist = tags.list.some(item => {
return item.path === route.fullPath;
});
const isExist = tags.list.some((item) => {
return item.path === route.fullPath
})
if (!isExist) {
if (tags.list.length >= 8) tags.delTagsItem(0);
if (tags.list.length >= 8) tags.delTagsItem(0)
tags.setTagsItem({
name: route.name,
title: route.meta.title,
path: route.fullPath
});
path: route.fullPath,
})
}
};
setTags(route);
onBeforeRouteUpdate(to => {
setTags(to);
});
}
setTags(route)
onBeforeRouteUpdate((to) => {
setTags(to)
})
// 关闭全部标签
const closeAll = () => {
tags.clearTags();
router.push('/admin');
};
tags.clearTags()
router.push('/admin')
}
// 关闭其他标签
const closeOther = () => {
const curItem = tags.list.filter(item => {
return item.path === route.fullPath;
});
tags.closeTagsOther(curItem);
};
const curItem = tags.list.filter((item) => {
return item.path === route.fullPath
})
tags.closeTagsOther(curItem)
}
const handleTags = (command) => {
command === 'other' ? closeOther() : closeAll();
};
command === 'other' ? closeOther() : closeAll()
}
// 关闭当前页面的标签页
// tags.closeCurrentTag({
@@ -115,15 +118,15 @@ const handleTags = (command) => {
// });
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.tags {
position: relative;
height: 30px;
overflow: hidden;
background: #fff;
padding 5px 120px 5px 10px
-webkit-box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
padding: 5px 120px 5px 10px;
-webkit-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
ul {
box-sizing: border-box;
@@ -164,7 +167,7 @@ const handleTags = (command) => {
}
.tags-li.active .tags-li-title {
color: var(--el-color-primary)
color: var(--el-color-primary);
}
}
@@ -183,7 +186,6 @@ const handleTags = (command) => {
}
.tags.dark {
border-bottom 1px solid var(--el-border-color)
border-bottom: 1px solid var(--el-border-color);
}
</style>

View File

@@ -24,7 +24,7 @@
class="image-slot"
:style="{
height: height + 'px',
lineHeight: height + 'px'
lineHeight: height + 'px',
}"
>
正在加载图片<span class="dot">...</span>
@@ -68,93 +68,92 @@
</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 { getSessionId } from '@/store/session'
import { httpPost } from '@/utils/http'
import { Picture } from '@element-plus/icons-vue'
import { showNotify } from 'vant'
import { ref, watch } from 'vue'
const props = defineProps({
content: Object,
icon: String,
chatId: String,
roleId: Number,
createdAt: String
});
createdAt: String,
})
const data = ref(props.content);
const cacheKey = "img_placeholder_height";
const item = localStorage.getItem(cacheKey);
const loading = ref(false);
const height = ref(0);
const data = ref(props.content)
const cacheKey = 'img_placeholder_height'
const item = localStorage.getItem(cacheKey)
const loading = ref(false)
const height = ref(0)
if (item) {
height.value = parseInt(item);
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);
if (data.value['image']?.width > 0) {
height.value = (350 * data.value['image']?.height) / data.value['image']?.width
localStorage.setItem(cacheKey, height.value)
}
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
data.value['showOpt'] = data.value['content']?.indexOf('- Image #') === -1
// console.log(data.value)
watch(
() => props.content,
(newVal) => {
data.value = newVal;
data.value = newVal
}
);
const emits = defineEmits(["disable-input", "disable-input"]);
)
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",
message_id: data.value?.["message_id"],
message_hash: data.value?.["image"]?.hash,
src: 'chat',
message_id: data.value?.['message_id'],
message_hash: data.value?.['image']?.hash,
session_id: getSessionId(),
key: data.value?.["key"],
prompt: data.value?.["prompt"],
key: data.value?.['key'],
prompt: data.value?.['prompt'],
chat_id: props.chatId,
role_id: props.roleId,
icon: props.icon
icon: props.icon,
})
.then(() => {
showNotify({
type: "success",
message: "任务推送成功,请耐心等待任务执行..."
});
loading.value = false;
type: 'success',
message: '任务推送成功,请耐心等待任务执行...',
})
loading.value = false
})
.catch((e) => {
showNotify({ type: "danger", message: "任务推送失败:" + e.message });
emits("disable-input");
});
};
showNotify({ type: 'danger', message: '任务推送失败:' + e.message })
emits('disable-input')
})
}
</script>
<style lang="stylus">
<style lang="scss">
.mobile-message-mj {
display flex
display: flex;
justify-content: flex-start;
.chat-icon {
margin-right 5px
margin-right: 5px;
.van-image {
width 32px
width: 32px;
img {
border-radius 5px
border-radius: 5px;
}
}
}
@@ -177,48 +176,47 @@ const send = (url, index) => {
}
.content-box {
display flex
flex-direction row
display: flex;
flex-direction: row;
.content {
text-align left
width 100%
overflow-x auto
min-height 20px;
word-break break-word;
text-align: left;
width: 100%;
overflow-x: auto;
min-height: 20px;
word-break: break-word;
padding: 5px 10px;
color #444444
color: #444444;
background-color: #ffffff;
font-size: 16px
font-size: 16px;
border-radius: 5px;
.content-inner {
word-break break-word;
word-break: break-word;
padding: 6px 10px;
color #374151;
color: #374151;
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
.text {
p:first-child {
margin-top 0
margin-top: 0;
}
}
.images {
max-width 350px;
max-width: 350px;
.el-image {
border-radius 10px;
border-radius: 10px;
.image-slot {
color #c1c1c1
width 350px
text-align center
border-radius 10px;
border 1px solid #e1e1e1
color: #c1c1c1;
width: 350px;
text-align: center;
border-radius: 10px;
border: 1px solid #e1e1e1;
}
}
}
@@ -226,42 +224,39 @@ const send = (url, index) => {
.opt {
.opt-line {
margin 6px 0
margin: 6px 0;
ul {
display flex
flex-flow row
padding-left 10px
display: flex;
flex-flow: row;
padding-left: 10px;
li {
margin-right 10px
margin-right: 10px;
a {
padding 3px 0
width 50px
text-align center
border-radius 5px
display block
cursor pointer
background-color #4E5058
color #ffffff
padding: 3px 0;
width: 50px;
text-align: center;
border-radius: 5px;
display: block;
cursor: pointer;
background-color: #4e5058;
color: #ffffff;
&:hover {
background-color #6D6F78
background-color: #6d6f78;
}
}
}
}
}
}
}
}
}
}
.van-theme-dark {
.mobile-message-reply {
.chat-item {
@@ -271,18 +266,16 @@ const send = (url, index) => {
.content-box {
.content {
color #c1c1c1
color: #c1c1c1;
background-color: #404042;
p > code {
color #c1c1c1
background-color #2b2b2b
color: #c1c1c1;
background-color: #2b2b2b;
}
}
}
}
}
}
</style>

View File

@@ -45,19 +45,19 @@ onMounted(() => {
})
</script>
<style lang="stylus">
<style lang="scss">
.mobile-message-prompt {
display flex
justify-content: flex-end
display: flex;
justify-content: flex-end;
.chat-icon {
margin-left 5px
margin-left: 5px;
.van-image {
width 32px
width: 32px;
img {
border-radius 5px
border-radius: 5px;
}
}
}
@@ -72,21 +72,21 @@ onMounted(() => {
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #98E165;
border-left: 5px solid #98e165;
position: absolute;
right: 0;
top: 10px;
}
.content {
word-break break-word;
text-align left
word-break: break-word;
text-align: left;
padding: 5px 10px;
background-color: #98E165;
color #444444
font-size: 14px
border-radius: 5px
line-height 1.5
background-color: #98e165;
color: #444444;
font-size: 14px;
border-radius: 5px;
line-height: 1.5;
}
}
}
@@ -94,14 +94,13 @@ onMounted(() => {
.van-theme-dark {
.mobile-message-prompt {
.chat-item {
.triangle {
border-left: 5px solid #223A34
border-left: 5px solid #223a34;
}
.content {
background-color: #223A34
color #c1c1c1
background-color: #223a34;
color: #c1c1c1;
}
}
}

View File

@@ -62,19 +62,19 @@ onMounted(() => {
})
</script>
<style lang="stylus">
<style lang="scss">
.mobile-message-reply {
display flex
display: flex;
justify-content: flex-start;
.chat-icon {
margin-right 5px
margin-right: 5px;
.van-image {
width 32px
width: 32px;
img {
border-radius 5px
border-radius: 5px;
}
}
}
@@ -97,107 +97,102 @@ onMounted(() => {
}
.content-box {
display flex
flex-direction row
display: flex;
flex-direction: row;
.content {
text-align left
width 100%
overflow-x auto
min-height 20px;
word-break break-word;
text-align: left;
width: 100%;
overflow-x: auto;
min-height: 20px;
word-break: break-word;
padding: 5px 10px;
color #444444
color: #444444;
background-color: #ffffff;
font-size: 14px
font-size: 14px;
border-radius: 5px;
p:last-child {
margin-bottom: 0
margin-bottom: 0;
}
p:first-child {
margin-top 0
margin-top: 0;
}
p {
code {
color #2b2b2b
background-color #c1c1c1
padding 2px 5px
border-radius 5px
color: #2b2b2b;
background-color: #c1c1c1;
padding: 2px 5px;
border-radius: 5px;
}
img {
max-width 100%
max-width: 100%;
}
}
.code-container {
position relative
position: relative;
.hljs {
border-radius 10px
line-height 1.5
border-radius: 10px;
line-height: 1.5;
}
.copy-code-mobile {
position: absolute;
right 10px
top 10px
cursor pointer
font-size 12px
color #c1c1c1
right: 10px;
top: 10px;
cursor: pointer;
font-size: 12px;
color: #c1c1c1;
&:hover {
color #20a0ff
color: #20a0ff;
}
}
}
.lang-name {
display none
position absolute;
right 10px
bottom 50px
padding 2px 6px 4px 6px
background-color #444444
border-radius 10px
color #00e0e0
display: none;
position: absolute;
right: 10px;
bottom: 50px;
padding: 2px 6px 4px 6px;
background-color: #444444;
border-radius: 10px;
color: #00e0e0;
}
// 设置表格边框
table {
width 100%
margin-bottom 1rem
color #212529
border-collapse collapse;
border 1px solid #dee2e6;
background-color #ffffff
width: 100%;
margin-bottom: 1rem;
color: #212529;
border-collapse: collapse;
border: 1px solid #dee2e6;
background-color: #ffffff;
thead {
th {
border 1px solid #dee2e6
vertical-align: bottom
border-bottom: 2px solid #dee2e6
padding 10px
border: 1px solid #dee2e6;
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
padding: 10px;
}
}
td {
border 1px solid #dee2e6
padding 10px
border: 1px solid #dee2e6;
padding: 10px;
}
}
// 代码快
blockquote {
margin 0
margin: 0;
background-color: #ebfffe;
padding: 0.8rem 1.5rem;
border-left: 0.5rem solid;
@@ -206,11 +201,9 @@ onMounted(() => {
}
}
}
}
}
.van-theme-dark {
.mobile-message-reply {
.chat-item {
@@ -220,18 +213,16 @@ onMounted(() => {
.content-box {
.content {
color #c1c1c1
color: #c1c1c1;
background-color: #404042;
p > code {
color #c1c1c1
background-color #2b2b2b
color: #c1c1c1;
background-color: #2b2b2b;
}
}
}
}
}
}
</style>

View File

@@ -1,11 +1,6 @@
<template>
<div class="black-dialog">
<el-dialog
v-model="showDialog"
:title="title"
:width="width"
:before-close="cancel"
>
<el-dialog v-model="showDialog" :title="title" :width="width" :before-close="cancel">
<div class="dialog-body">
<slot></slot>
</div>
@@ -14,12 +9,9 @@
<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
>
<el-button type="primary" @click="$emit('confirm')" v-if="!hideConfirm">{{
confirmText
}}</el-button>
</div>
</template>
</el-dialog>
@@ -27,81 +19,81 @@
</template>
<script setup>
import { ref, watch } from "vue";
import { ref, watch } from 'vue'
const props = defineProps({
show: Boolean,
title: {
type: String,
default: "Tips"
default: 'Tips',
},
width: {
type: String,
default: "auto"
default: 'auto',
},
hideFooter: {
type: Boolean,
default: false
default: false,
},
hideConfirm: {
type: Boolean,
default: false
default: false,
},
confirmText: {
type: String,
default: "确定"
default: '确定',
},
cancelText: {
type: String,
default: "取消"
}
});
const emits = defineEmits(["confirm", "cancal"]);
const showDialog = ref(props.show);
default: '取消',
},
})
const emits = defineEmits(['confirm', 'cancal'])
const showDialog = ref(props.show)
watch(
() => props.show,
(newValue) => {
showDialog.value = newValue;
showDialog.value = newValue
}
);
)
const cancel = () => {
showDialog.value = false;
emits("cancal");
};
showDialog.value = false
emits('cancal')
}
</script>
<style lang="stylus">
<style lang="scss">
.black-dialog {
.dialog-body {
.form {
.form-item {
display flex
flex-flow column
font-family: "Neue Montreal";
padding 10px 0
display: flex;
flex-flow: column;
font-family: 'Neue Montreal';
padding: 10px 0;
.label {
margin-bottom 0.6rem
margin-inline-end 0.75rem
color #ffffff
font-size 1rem
font-weight 500
margin-bottom: 0.6rem;
margin-inline-end: 0.75rem;
color: #ffffff;
font-size: 1rem;
font-weight: 500;
}
.input {
display flex
padding 10px
text-align left
font-size 1rem
background none
border-radius 0.375rem
border 1px solid #8f8f8f
display: flex;
padding: 10px;
text-align: left;
font-size: 1rem;
background: none;
border-radius: 0.375rem;
border: 1px solid #8f8f8f;
outline: none;
transition: border-color 0.5s ease, box-shadow 0.5s ease;
&:focus {
border-color: #0F7A71;
box-shadow: 0 0 5px #0F7A71;
border-color: #0f7a71;
box-shadow: 0 0 5px #0f7a71;
}
}
}

View File

@@ -17,47 +17,47 @@
</template>
<script setup>
import { ref, watch } from "vue";
import { ref, watch } from 'vue'
const props = defineProps({
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
}
});
default: 1024,
},
})
watch(
() => props.value,
(newValue) => {
model.value = newValue;
model.value = newValue
}
);
const model = ref(props.value);
)
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">
<style lang="scss">
.black-input-wrapper {
position relative
position: relative;
.el-textarea__inner {
padding: 20px;
@@ -66,15 +66,16 @@ const onInput = (value) => {
.word-stat {
position: absolute;
bottom 10px
right 10px
color rgb(209 203 199)
font-family: Neue Montreal, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-size .875rem
line-height 1.25rem
bottom: 10px;
right: 10px;
color: rgb(209, 203, 199);
font-family: Neue Montreal, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji,
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-size: 0.875rem;
line-height: 1.25rem;
span {
margin 0 1px
margin: 0 1px;
}
}
}

View File

@@ -13,7 +13,7 @@
<div class="bar"></div>
</div>
<div class="text">
<slot>{{message}}</slot>
<slot>{{ message }}</slot>
</div>
</div>
</template>
@@ -25,13 +25,13 @@ defineProps({
type: String,
default: '任务正在执行',
},
});
})
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.container {
display: flex;
flex-flow column
flex-flow: column;
justify-content: center;
align-items: center;
margin: 0;
@@ -43,7 +43,7 @@ defineProps({
justify-content: center;
align-items: flex-end;
height: 30px;
margin-bottom: 5px
margin-bottom: 5px;
.bar {
width: 8px;
@@ -95,8 +95,9 @@ defineProps({
}
@keyframes wave {
0%, 100% {
height: 10px;
0%,
100% {
height: 10px;
}
50% {
height: 30px;
@@ -106,4 +107,4 @@ defineProps({
.text {
font-size: 14px;
}
</style>
</style>

View File

@@ -2,48 +2,48 @@
<!-- 多项目输入组件 -->
<div class="items-input-box">
<el-tag
v-for="tag in tags"
:key="tag"
closable
:disable-transitions="false"
@close="handleClose(tag)"
v-for="tag in tags"
:key="tag"
closable
:disable-transitions="false"
@close="handleClose(tag)"
>
{{ tag }}
</el-tag>
<el-input
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else class="button-new-tag" size="small" @click="showInput">
+ 新增
</el-button>
<el-button v-else class="button-new-tag" size="small" @click="showInput"> + 新增 </el-button>
</div>
</template>
<script setup>
import {nextTick, ref, watch} from "vue";
import { nextTick, ref, watch } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
value : {
value: {
type: Array,
default: () => []
default: () => [],
},
});
})
// eslint-disable-next-line no-undef
const emits = defineEmits(['update:value']);
const emits = defineEmits(['update:value'])
const tags = ref(props.value)
const inputValue = ref('')
const inputVisible = ref(false)
const InputRef = ref(null)
watch(() => props.value, (newValue) => {
tags.value = newValue
})
watch(
() => props.value,
(newValue) => {
tags.value = newValue
}
)
const handleClose = (tag) => {
tags.value.splice(tags.value.indexOf(tag), 1)
@@ -66,14 +66,13 @@ const handleInputConfirm = () => {
}
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.items-input-box {
display flex
display: flex;
.el-tag {
display flex
margin-right 6px
display: flex;
margin-right: 6px;
}
}
</style>
</style>

View File

@@ -1,35 +1,35 @@
<template>
<button
class="ripple-button"
@mousedown="startRipples"
@mouseup="stopRipples"
@mouseleave="stopRipples"
class="ripple-button"
@mousedown="startRipples"
@mouseup="stopRipples"
@mouseleave="stopRipples"
>
<slot></slot>
<span
v-for="ripple in ripples"
:key="ripple.id"
class="ripple"
:style="getRippleStyle(ripple)"
v-for="ripple in ripples"
:key="ripple.id"
class="ripple"
:style="getRippleStyle(ripple)"
></span>
</button>
</template>
<script setup>
import { ref } from 'vue';
import { ref } from 'vue'
const ripples = ref([]);
let rippleCount = 0;
let animationId;
const ripples = ref([])
let rippleCount = 0
let animationId
const startRipples = (event) => {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const button = event.currentTarget
const rect = button.getBoundingClientRect()
const size = Math.max(rect.width, rect.height)
// const x = event.clientX - rect.left;
// const y = event.clientY - rect.top;
const x = rect.right - rect.left - size/2;
const y = rect.bottom - rect.top - size/2;
const x = rect.right - rect.left - size / 2
const y = rect.bottom - rect.top - size / 2
const createRipple = () => {
ripples.value.push({
@@ -37,59 +37,59 @@ const startRipples = (event) => {
x,
y,
size: 0,
opacity: 0.5
});
opacity: 0.5,
})
if (ripples.value.length > 3) {
ripples.value.shift();
ripples.value.shift()
}
};
}
const animate = () => {
ripples.value.forEach(ripple => {
ripple.size += 2;
ripple.opacity -= 0.01;
});
ripples.value.forEach((ripple) => {
ripple.size += 2
ripple.opacity -= 0.01
})
ripples.value = ripples.value.filter(ripple => ripple.opacity > 0);
ripples.value = ripples.value.filter((ripple) => ripple.opacity > 0)
if (ripples.value.length < 3) {
createRipple();
createRipple()
}
animationId = requestAnimationFrame(animate);
};
animationId = requestAnimationFrame(animate)
}
createRipple();
animate();
};
createRipple()
animate()
}
const stopRipples = () => {
cancelAnimationFrame(animationId);
ripples.value = [];
};
cancelAnimationFrame(animationId)
ripples.value = []
}
const getRippleStyle = (ripple) => ({
left: `${ripple.x}px`,
top: `${ripple.y}px`,
width: `${ripple.size}px`,
height: `${ripple.size}px`,
opacity: ripple.opacity
});
opacity: ripple.opacity,
})
</script>
<style scoped lang="stylus">
<style scoped lang="scss">
.ripple-button {
position: relative;
overflow: hidden;
border: none;
background none;
background: none;
color: white;
cursor: pointer;
border-radius: 50%;
outline: none;
margin 0
padding 0
margin: 0;
padding: 0;
}
.ripple {
@@ -99,4 +99,4 @@ const getRippleStyle = (ripple) => ({
transform: translate(-50%, -50%);
pointer-events: none;
}
</style>
</style>