会员开通续费
This commit is contained in:
parent
06391db10a
commit
e71b032180
@ -98,5 +98,15 @@ export const PersonApi = {
|
||||
url: `/system/person/member-growth-data`,
|
||||
params: { orgid }
|
||||
})
|
||||
},
|
||||
|
||||
// 续费会员
|
||||
rechargePerson: async (userid: number, vipendtime: string) => {
|
||||
return await request.put({ url: `/system/person/recharge?userid=` + userid + `&vipendtime=` + vipendtime })
|
||||
},
|
||||
|
||||
// 开通会员
|
||||
becomeVip: async (data: any) => {
|
||||
return await request.put({ url: `/system/person/become-vip`, data })
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,93 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="会员续费" width="600px" @close="handleClose">
|
||||
<div class="package-grid">
|
||||
<div
|
||||
v-for="pkg in packages"
|
||||
:key="pkg.id"
|
||||
class="package-card"
|
||||
:class="{ active: selectedPackage === pkg }"
|
||||
@click="selectedPackage = pkg"
|
||||
>
|
||||
<div class="package-name">{{ pkg.name }}</div>
|
||||
<div class="package-price">
|
||||
<span class="currency">¥</span>
|
||||
<span class="amount">{{ pkg.price }}</span>
|
||||
<Dialog v-model="dialogVisible" title="会员续费" width="700px" @close="handleClose">
|
||||
<div class="recharge-content">
|
||||
<!-- 当前会员信息卡片 -->
|
||||
<div class="member-info-card">
|
||||
<div class="card-header">
|
||||
<div class="avatar">
|
||||
<Icon icon="ep:user" class="avatar-icon" />
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<div class="member-name">{{ props.member.name || '未知用户' }}</div>
|
||||
<div class="member-phone">{{ props.member.phone || '暂无手机号' }}</div>
|
||||
</div>
|
||||
<div class="vip-status" :class="{ 'vip-active': props.member.isvip === 1 }">
|
||||
<Icon :icon="props.member.isvip === 1 ? 'ep:medal' : 'ep:user'" />
|
||||
{{ props.member.isvip === 1 ? 'VIP会员' : '普通用户' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间设置区域 -->
|
||||
<div class="time-settings">
|
||||
<div class="section-title">
|
||||
<Icon icon="ep:clock" class="title-icon" />
|
||||
<span>时间设置</span>
|
||||
</div>
|
||||
|
||||
<div class="time-grid">
|
||||
<!-- 当前到期时间 -->
|
||||
<div class="time-card current-time">
|
||||
<div class="time-label">
|
||||
<Icon icon="ep:calendar" class="label-icon" />
|
||||
当前到期时间
|
||||
</div>
|
||||
<div class="time-value">{{ formatExpireTime }}</div>
|
||||
<div class="time-status" :class="{ 'expired': isExpired }">
|
||||
{{ getExpireStatus }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新的到期时间 -->
|
||||
<div class="time-card new-time">
|
||||
<div class="time-label">
|
||||
<Icon icon="ep:calendar-plus" class="label-icon" />
|
||||
新的到期时间
|
||||
</div>
|
||||
<el-date-picker
|
||||
v-model="newExpireDate"
|
||||
type="date"
|
||||
placeholder="请选择新的到期时间"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled-date="disabledDate"
|
||||
class="expire-picker"
|
||||
@change="calculateDuration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开通时长显示 -->
|
||||
<div class="duration-card" v-if="newExpireDate">
|
||||
<div class="duration-header">
|
||||
<Icon icon="ep:timer" class="duration-icon" />
|
||||
<span>开通时长</span>
|
||||
</div>
|
||||
<div class="duration-value">{{ calculatedDuration }}</div>
|
||||
<div class="duration-tip">从当前到期时间开始计算</div>
|
||||
</div>
|
||||
<div class="package-duration">{{ pkg.duration }}个月</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose" class="cancel-btn">
|
||||
<Icon icon="ep:close" class="btn-icon" />
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="loading" class="confirm-btn">
|
||||
<Icon icon="ep:check" class="btn-icon" />
|
||||
确认续费
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineEmits, defineProps, watch } from 'vue'
|
||||
import { ref, defineEmits, defineProps, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { PersonApi } from '@/api/person'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@ -41,17 +103,42 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const dialogVisible = ref(props.visible)
|
||||
const selectedPackage = ref(null)
|
||||
const newExpireDate = ref('')
|
||||
const loading = ref(false)
|
||||
const calculatedDuration = ref('')
|
||||
|
||||
const packages = [
|
||||
{ id: 1, name: '月度会员', price: 30, duration: 1 },
|
||||
{ id: 2, name: '季度会员', price: 80, duration: 3 },
|
||||
{ id: 3, name: '年度会员', price: 298, duration: 12 }
|
||||
]
|
||||
// 计算当前到期时间的格式化显示
|
||||
const formatExpireTime = computed(() => {
|
||||
if (!props.member.vipendtime) {
|
||||
return '未开通会员'
|
||||
}
|
||||
return new Date(props.member.vipendtime).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
})
|
||||
|
||||
// 判断是否已过期
|
||||
const isExpired = computed(() => {
|
||||
if (!props.member.vipendtime) return false
|
||||
return new Date(props.member.vipendtime) < new Date()
|
||||
})
|
||||
|
||||
// 获取到期状态
|
||||
const getExpireStatus = computed(() => {
|
||||
if (!props.member.vipendtime) return '未开通'
|
||||
if (isExpired.value) return '已过期'
|
||||
return '正常'
|
||||
})
|
||||
|
||||
// 监听visible属性变化
|
||||
watch(() => props.visible, (val) => {
|
||||
dialogVisible.value = val
|
||||
if (val) {
|
||||
// 弹窗打开时,设置默认时间为当前到期时间后一年
|
||||
setDefaultExpireDate()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听dialogVisible变化
|
||||
@ -59,79 +146,409 @@ watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
selectedPackage.value = null
|
||||
// 设置默认到期时间(当前到期时间后一年)
|
||||
const setDefaultExpireDate = () => {
|
||||
let baseDate
|
||||
if (props.member.vipendtime) {
|
||||
// 如果有到期时间,以到期时间为基准
|
||||
baseDate = new Date(props.member.vipendtime)
|
||||
} else {
|
||||
// 如果没有到期时间,以当前时间为基准
|
||||
baseDate = new Date()
|
||||
}
|
||||
|
||||
// 设置为一年后
|
||||
const defaultDate = new Date(baseDate)
|
||||
defaultDate.setFullYear(defaultDate.getFullYear() + 1)
|
||||
|
||||
// 只设置日期部分,时分秒设为23:59:59
|
||||
const year = defaultDate.getFullYear()
|
||||
const month = String(defaultDate.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(defaultDate.getDate()).padStart(2, '0')
|
||||
|
||||
newExpireDate.value = `${year}-${month}-${day}`
|
||||
|
||||
// 计算默认时长
|
||||
calculateDuration()
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedPackage.value) {
|
||||
ElMessage.warning('请选择会员套餐')
|
||||
// 计算开通时长
|
||||
const calculateDuration = () => {
|
||||
if (!newExpireDate.value) {
|
||||
calculatedDuration.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
let startDate
|
||||
if (props.member.vipendtime) {
|
||||
// 如果有当前到期时间,以当前到期时间为起始时间
|
||||
startDate = new Date(props.member.vipendtime)
|
||||
} else {
|
||||
// 如果没有到期时间,以当前时间为起始时间
|
||||
startDate = new Date()
|
||||
}
|
||||
|
||||
// 将选择的日期转换为当天的23:59:59
|
||||
const endDate = new Date(newExpireDate.value + ' 23:59:59')
|
||||
|
||||
// 计算时间差(毫秒)
|
||||
const timeDiff = endDate.getTime() - startDate.getTime()
|
||||
|
||||
if (timeDiff <= 0) {
|
||||
calculatedDuration.value = '无效时长'
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为天、小时、分钟
|
||||
const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
|
||||
// 格式化显示
|
||||
let durationText = ''
|
||||
if (days > 0) {
|
||||
durationText += `${days}天`
|
||||
}
|
||||
if (hours > 0) {
|
||||
durationText += `${hours}小时`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
durationText += `${minutes}分钟`
|
||||
}
|
||||
|
||||
calculatedDuration.value = durationText || '0分钟'
|
||||
}
|
||||
|
||||
// 禁用过去的日期
|
||||
const disabledDate = (time) => {
|
||||
return time.getTime() < Date.now()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
newExpireDate.value = ''
|
||||
calculatedDuration.value = ''
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!newExpireDate.value) {
|
||||
ElMessage.warning('请选择新的到期时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 计算新的到期时间
|
||||
const currentDate = props.member.vipExpireDate ? new Date(props.member.vipExpireDate) : new Date()
|
||||
const newExpireDate = new Date(currentDate.setMonth(currentDate.getMonth() + selectedPackage.value.duration))
|
||||
// 验证选择的时间是否合理
|
||||
const selectedDate = new Date(newExpireDate.value + ' 23:59:59')
|
||||
const now = new Date()
|
||||
|
||||
if (selectedDate <= now) {
|
||||
ElMessage.warning('新的到期时间不能早于当前时间')
|
||||
return
|
||||
}
|
||||
|
||||
emit('success', {
|
||||
memberId: props.member.id,
|
||||
packageId: selectedPackage.value.id,
|
||||
expireDate: newExpireDate.toISOString().split('T')[0]
|
||||
})
|
||||
handleClose()
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用接口更新会员到期时间
|
||||
const updateData = {
|
||||
userid: props.member.id,
|
||||
vipendtime: newExpireDate.value + ' 23:59:59'
|
||||
}
|
||||
|
||||
await PersonApi.rechargePerson(updateData.userid, updateData.vipendtime)
|
||||
|
||||
ElMessage.success('会员续费成功')
|
||||
|
||||
// 触发成功事件
|
||||
emit('success', {
|
||||
memberId: props.member.id,
|
||||
newExpireDate: updateData.vipendtime
|
||||
})
|
||||
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.error('续费失败:', error)
|
||||
ElMessage.error('续费失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.package-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
.recharge-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.package-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
/* 会员信息卡片 */
|
||||
.member-info-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 24px;
|
||||
color: white;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.package-card:hover {
|
||||
border-color: #409eff;
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.member-phone {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.vip-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.vip-status.vip-active {
|
||||
background: rgba(255, 215, 0, 0.3);
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* 时间设置区域 */
|
||||
.time-settings {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.time-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.time-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.package-card.active {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
.time-card.current-time {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.package-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
.time-card.new-time {
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.package-price {
|
||||
color: #f56c6c;
|
||||
margin-bottom: 10px;
|
||||
.time-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.package-price .currency {
|
||||
.label-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.package-price .amount {
|
||||
.time-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.time-status {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-status:not(.expired) {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.time-status.expired {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.expire-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 开通时长卡片 */
|
||||
.duration-card {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.duration-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.duration-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.duration-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.package-duration {
|
||||
color: #909399;
|
||||
.duration-tip {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.time-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vip-status {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
647
src/views/vip/components/becomvip.vue
Normal file
647
src/views/vip/components/becomvip.vue
Normal file
@ -0,0 +1,647 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="开通会员" width="700px" @close="handleClose">
|
||||
<div class="become-vip-content">
|
||||
<!-- 当前会员信息卡片 -->
|
||||
<div class="member-info-card">
|
||||
<div class="card-header">
|
||||
<div class="avatar">
|
||||
<Icon icon="ep:user" class="avatar-icon" />
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<div class="member-name">{{ props.member.name || '未知用户' }}</div>
|
||||
<div class="member-phone">{{ props.member.phone || '暂无手机号' }}</div>
|
||||
</div>
|
||||
<div class="vip-status">
|
||||
<Icon icon="ep:user" />
|
||||
普通用户
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会员时长选择区域 -->
|
||||
<div class="vip-duration-settings">
|
||||
<div class="section-title">
|
||||
<Icon icon="ep:clock" class="title-icon" />
|
||||
<span>选择会员时长</span>
|
||||
</div>
|
||||
|
||||
<div class="duration-grid">
|
||||
<!-- 快速选择按钮 -->
|
||||
<div class="quick-select">
|
||||
<div class="quick-select-title">
|
||||
<Icon icon="ep:lightning" class="quick-icon" />
|
||||
快速选择
|
||||
</div>
|
||||
<div class="quick-buttons">
|
||||
<el-button
|
||||
v-for="duration in quickDurations"
|
||||
:key="duration.value"
|
||||
:type="selectedDuration === duration.value ? 'primary' : 'default'"
|
||||
:class="{ 'selected': selectedDuration === duration.value }"
|
||||
@click="selectDuration(duration.value)"
|
||||
class="quick-btn"
|
||||
>
|
||||
{{ duration.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义时长 -->
|
||||
<div class="custom-duration">
|
||||
<div class="custom-title">
|
||||
<Icon icon="ep:calendar-plus" class="custom-icon" />
|
||||
自定义时长
|
||||
</div>
|
||||
<el-date-picker
|
||||
v-model="customExpireDate"
|
||||
type="date"
|
||||
placeholder="请选择到期时间"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled-date="disabledDate"
|
||||
class="custom-picker"
|
||||
@change="handleCustomDateChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时长预览 -->
|
||||
<div class="duration-preview" v-if="selectedDuration || customExpireDate">
|
||||
<div class="preview-header">
|
||||
<Icon icon="ep:timer" class="preview-icon" />
|
||||
<span>开通时长预览</span>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div class="preview-item">
|
||||
<span class="label">开通时间:</span>
|
||||
<span class="value">{{ formatStartTime }}</span>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="label">到期时间:</span>
|
||||
<span class="value">{{ formatEndTime }}</span>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="label">总时长:</span>
|
||||
<span class="value highlight">{{ calculatedDuration }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose" class="cancel-btn">
|
||||
<Icon icon="ep:close" class="btn-icon" />
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="loading" class="confirm-btn">
|
||||
<Icon icon="ep:check" class="btn-icon" />
|
||||
确认开通
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineEmits, defineProps, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { PersonApi } from '@/api/person'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const dialogVisible = ref(props.visible)
|
||||
const selectedDuration = ref('1year') // 默认选择一年
|
||||
const customExpireDate = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// 快速选择时长选项
|
||||
const quickDurations = [
|
||||
{ label: '1个月', value: '1month' },
|
||||
{ label: '3个月', value: '3months' },
|
||||
{ label: '6个月', value: '6months' },
|
||||
{ label: '1年', value: '1year' },
|
||||
{ label: '2年', value: '2years' },
|
||||
{ label: '3年', value: '3years' }
|
||||
]
|
||||
|
||||
// 监听visible属性变化
|
||||
watch(() => props.visible, (val) => {
|
||||
dialogVisible.value = val
|
||||
if (val) {
|
||||
// 弹窗打开时,设置默认选择一年
|
||||
selectedDuration.value = '1year'
|
||||
customExpireDate.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 监听dialogVisible变化
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val)
|
||||
})
|
||||
|
||||
// 选择时长
|
||||
const selectDuration = (duration) => {
|
||||
selectedDuration.value = duration
|
||||
customExpireDate.value = ''
|
||||
}
|
||||
|
||||
// 处理自定义日期变化
|
||||
const handleCustomDateChange = () => {
|
||||
if (customExpireDate.value) {
|
||||
selectedDuration.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化开始时间
|
||||
const formatStartTime = computed(() => {
|
||||
const now = new Date()
|
||||
return now.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
})
|
||||
|
||||
// 格式化结束时间
|
||||
const formatEndTime = computed(() => {
|
||||
if (customExpireDate.value) {
|
||||
return new Date(customExpireDate.value).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
if (selectedDuration.value) {
|
||||
const endDate = calculateEndDate()
|
||||
return endDate.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
return '-'
|
||||
})
|
||||
|
||||
// 计算结束日期
|
||||
const calculateEndDate = () => {
|
||||
const now = new Date()
|
||||
const endDate = new Date(now)
|
||||
|
||||
switch (selectedDuration.value) {
|
||||
case '1month':
|
||||
endDate.setMonth(endDate.getMonth() + 1)
|
||||
break
|
||||
case '3months':
|
||||
endDate.setMonth(endDate.getMonth() + 3)
|
||||
break
|
||||
case '6months':
|
||||
endDate.setMonth(endDate.getMonth() + 6)
|
||||
break
|
||||
case '1year':
|
||||
endDate.setFullYear(endDate.getFullYear() + 1)
|
||||
break
|
||||
case '2years':
|
||||
endDate.setFullYear(endDate.getFullYear() + 2)
|
||||
break
|
||||
case '3years':
|
||||
endDate.setFullYear(endDate.getFullYear() + 3)
|
||||
break
|
||||
default:
|
||||
return now
|
||||
}
|
||||
|
||||
return endDate
|
||||
}
|
||||
|
||||
// 计算总时长
|
||||
const calculatedDuration = computed(() => {
|
||||
if (customExpireDate.value) {
|
||||
const startDate = new Date()
|
||||
const endDate = new Date(customExpireDate.value + ' 23:59:59')
|
||||
return calculateTimeDifference(startDate, endDate)
|
||||
}
|
||||
|
||||
if (selectedDuration.value) {
|
||||
const startDate = new Date()
|
||||
const endDate = calculateEndDate()
|
||||
return calculateTimeDifference(startDate, endDate)
|
||||
}
|
||||
|
||||
return '-'
|
||||
})
|
||||
|
||||
// 计算时间差
|
||||
const calculateTimeDifference = (startDate, endDate) => {
|
||||
const timeDiff = endDate.getTime() - startDate.getTime()
|
||||
|
||||
if (timeDiff <= 0) {
|
||||
return '无效时长'
|
||||
}
|
||||
|
||||
const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
|
||||
const months = Math.floor(days / 30)
|
||||
const years = Math.floor(months / 12)
|
||||
|
||||
if (years > 0) {
|
||||
const remainingMonths = months % 12
|
||||
return remainingMonths > 0 ? `${years}年${remainingMonths}个月` : `${years}年`
|
||||
} else if (months > 0) {
|
||||
const remainingDays = days % 30
|
||||
return remainingDays > 0 ? `${months}个月${remainingDays}天` : `${months}个月`
|
||||
} else {
|
||||
return `${days}天`
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用过去的日期
|
||||
const disabledDate = (time) => {
|
||||
return time.getTime() < Date.now()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
selectedDuration.value = '1year'
|
||||
customExpireDate.value = ''
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedDuration.value && !customExpireDate.value) {
|
||||
ElMessage.warning('请选择会员时长')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
let expireDate
|
||||
if (customExpireDate.value) {
|
||||
expireDate = customExpireDate.value + ' 23:59:59'
|
||||
} else {
|
||||
const endDate = calculateEndDate()
|
||||
const year = endDate.getFullYear()
|
||||
const month = String(endDate.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(endDate.getDate()).padStart(2, '0')
|
||||
expireDate = `${year}-${month}-${day} 23:59:59`
|
||||
}
|
||||
|
||||
// 调用becomeVip接口开通会员
|
||||
const now = new Date()
|
||||
const data = {
|
||||
userid: props.member.id,
|
||||
status: 1,
|
||||
vipstarttime: now.getFullYear() + '-' +
|
||||
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(now.getDate()).padStart(2, '0') + ' ' +
|
||||
String(now.getHours()).padStart(2, '0') + ':' +
|
||||
String(now.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(now.getSeconds()).padStart(2, '0'),
|
||||
vipendtime: expireDate
|
||||
}
|
||||
await PersonApi.becomeVip(data)
|
||||
|
||||
ElMessage.success('会员开通成功')
|
||||
|
||||
// 触发成功事件
|
||||
emit('success', {
|
||||
memberId: props.member.id,
|
||||
newExpireDate: data.vipendtime
|
||||
})
|
||||
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.error('开通失败:', error)
|
||||
ElMessage.error('开通失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.become-vip-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 会员信息卡片 */
|
||||
.member-info-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
color: white;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.member-phone {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.vip-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 会员时长设置区域 */
|
||||
.vip-duration-settings {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.duration-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 快速选择区域 */
|
||||
.quick-select {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-select:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.quick-select-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
color: #f59e0b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.quick-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-btn.selected {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* 自定义时长区域 */
|
||||
.custom-duration {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-duration:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.custom-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
color: #10b981;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.custom-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 时长预览 */
|
||||
.duration-preview {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.preview-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.preview-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.preview-item .label {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.preview-item .value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.preview-item .value.highlight {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.duration-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.quick-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vip-status {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -70,7 +70,7 @@
|
||||
<div class="flex items-center justify-center">
|
||||
<el-button
|
||||
v-if="scope.row.isvip === 0"
|
||||
type="primary"
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="handleRecharge(scope.row)"
|
||||
@ -116,6 +116,13 @@
|
||||
@success="handleRechargeSuccess"
|
||||
/>
|
||||
|
||||
<!-- 开通会员弹窗 -->
|
||||
<BecomeVipDialog
|
||||
v-model:visible="becomeVipDialogVisible"
|
||||
:member="currentMember"
|
||||
@success="handleBecomeVipSuccess"
|
||||
/>
|
||||
|
||||
<!-- 密码验证弹窗 -->
|
||||
<PasswordVerifyDialog
|
||||
v-model:visible="passwordVerifyVisible"
|
||||
@ -128,6 +135,8 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import RechargeDialog from './components/RechargeDialog.vue'
|
||||
import BecomeVipDialog from './components/becomvip.vue'
|
||||
import PasswordVerifyDialog from './components/PasswordVerifyDialog.vue'
|
||||
import { PersonApi } from '@/api/person'
|
||||
import { getUserProfile } from '@/api/system/user/profile'
|
||||
|
||||
@ -146,6 +155,7 @@ const queryFormRef = ref()
|
||||
|
||||
// 对话框控制
|
||||
const rechargeDialogVisible = ref(false)
|
||||
const becomeVipDialogVisible = ref(false)
|
||||
const passwordVerifyVisible = ref(false)
|
||||
|
||||
// 当前选中的会员
|
||||
@ -193,7 +203,6 @@ const getList = async () => {
|
||||
if (res) {
|
||||
memberList.value = res.list
|
||||
total.value = res.total
|
||||
console.log(memberList.value)
|
||||
} else {
|
||||
memberList.value = []
|
||||
total.value = 0
|
||||
@ -223,18 +232,27 @@ const resetQuery = () => {
|
||||
// 续费/开通会员
|
||||
const handleRecharge = (member) => {
|
||||
currentMember.value = member
|
||||
rechargeDialogVisible.value = true
|
||||
if (member.isvip === 0) {
|
||||
// 未开通会员,打开开通会员弹窗
|
||||
becomeVipDialogVisible.value = true
|
||||
} else {
|
||||
// 已开通会员,打开续费弹窗
|
||||
rechargeDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 续费成功
|
||||
const handleRechargeSuccess = (data) => {
|
||||
const member = mockMembers.find(m => m.id === data.memberId)
|
||||
if (member) {
|
||||
member.vipStatus = true
|
||||
member.vipExpireDate = data.expireDate
|
||||
getList()
|
||||
ElMessage.success('操作成功')
|
||||
}
|
||||
// 刷新会员列表数据
|
||||
getList()
|
||||
ElMessage.success('续费成功')
|
||||
}
|
||||
|
||||
// 开通会员成功
|
||||
const handleBecomeVipSuccess = (data) => {
|
||||
// 刷新会员列表数据
|
||||
getList()
|
||||
ElMessage.success('会员开通成功')
|
||||
}
|
||||
|
||||
// 取消会员
|
||||
@ -244,25 +262,21 @@ const handleCancel = (member) => {
|
||||
}
|
||||
|
||||
// 密码验证成功
|
||||
const handlePasswordVerifySuccess = (password) => {
|
||||
// TODO: 这里需要调用后端API验证密码并取消会员
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
const index = mockMembers.findIndex(m => m.id === currentMember.value.id)
|
||||
if (index !== -1) {
|
||||
mockMembers[index] = {
|
||||
...currentMember.value,
|
||||
vipStatus: false,
|
||||
vipExpireDate: ''
|
||||
}
|
||||
getList()
|
||||
ElMessage.success('已取消会员资格')
|
||||
// 关闭密码验证弹窗
|
||||
passwordVerifyVisible.value = false
|
||||
// 清空当前选中的会员
|
||||
currentMember.value = null
|
||||
}
|
||||
}, 500)
|
||||
const handlePasswordVerifySuccess = async (password) => {
|
||||
try {
|
||||
// TODO: 这里需要调用后端API验证密码并取消会员
|
||||
// 暂时显示成功消息
|
||||
ElMessage.success('已取消会员资格')
|
||||
// 关闭密码验证弹窗
|
||||
passwordVerifyVisible.value = false
|
||||
// 清空当前选中的会员
|
||||
currentMember.value = null
|
||||
// 刷新列表
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('取消会员失败:', error)
|
||||
ElMessage.error('取消会员失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user