会员开通续费
This commit is contained in:
parent
06391db10a
commit
e71b032180
@ -98,5 +98,15 @@ export const PersonApi = {
|
|||||||
url: `/system/person/member-growth-data`,
|
url: `/system/person/member-growth-data`,
|
||||||
params: { orgid }
|
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>
|
<template>
|
||||||
<Dialog v-model="dialogVisible" title="会员续费" width="600px" @close="handleClose">
|
<Dialog v-model="dialogVisible" title="会员续费" width="700px" @close="handleClose">
|
||||||
<div class="package-grid">
|
<div class="recharge-content">
|
||||||
<div
|
<!-- 当前会员信息卡片 -->
|
||||||
v-for="pkg in packages"
|
<div class="member-info-card">
|
||||||
:key="pkg.id"
|
<div class="card-header">
|
||||||
class="package-card"
|
<div class="avatar">
|
||||||
:class="{ active: selectedPackage === pkg }"
|
<Icon icon="ep:user" class="avatar-icon" />
|
||||||
@click="selectedPackage = pkg"
|
|
||||||
>
|
|
||||||
<div class="package-name">{{ pkg.name }}</div>
|
|
||||||
<div class="package-price">
|
|
||||||
<span class="currency">¥</span>
|
|
||||||
<span class="amount">{{ pkg.price }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="package-duration">{{ pkg.duration }}个月</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">取 消</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
<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>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, defineProps, watch } from 'vue'
|
import { ref, defineEmits, defineProps, watch, computed } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { PersonApi } from '@/api/person'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@ -41,17 +103,42 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update:visible', 'success'])
|
const emit = defineEmits(['update:visible', 'success'])
|
||||||
|
|
||||||
const dialogVisible = ref(props.visible)
|
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 },
|
const formatExpireTime = computed(() => {
|
||||||
{ id: 2, name: '季度会员', price: 80, duration: 3 },
|
if (!props.member.vipendtime) {
|
||||||
{ id: 3, name: '年度会员', price: 298, duration: 12 }
|
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属性变化
|
// 监听visible属性变化
|
||||||
watch(() => props.visible, (val) => {
|
watch(() => props.visible, (val) => {
|
||||||
dialogVisible.value = val
|
dialogVisible.value = val
|
||||||
|
if (val) {
|
||||||
|
// 弹窗打开时,设置默认时间为当前到期时间后一年
|
||||||
|
setDefaultExpireDate()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听dialogVisible变化
|
// 监听dialogVisible变化
|
||||||
@ -59,79 +146,409 @@ watch(dialogVisible, (val) => {
|
|||||||
emit('update:visible', val)
|
emit('update:visible', val)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
// 设置默认到期时间(当前到期时间后一年)
|
||||||
dialogVisible.value = false
|
const setDefaultExpireDate = () => {
|
||||||
selectedPackage.value = null
|
let baseDate
|
||||||
|
if (props.member.vipendtime) {
|
||||||
|
// 如果有到期时间,以到期时间为基准
|
||||||
|
baseDate = new Date(props.member.vipendtime)
|
||||||
|
} else {
|
||||||
|
// 如果没有到期时间,以当前时间为基准
|
||||||
|
baseDate = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
// 设置为一年后
|
||||||
if (!selectedPackage.value) {
|
const defaultDate = new Date(baseDate)
|
||||||
ElMessage.warning('请选择会员套餐')
|
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 calculateDuration = () => {
|
||||||
|
if (!newExpireDate.value) {
|
||||||
|
calculatedDuration.value = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算新的到期时间
|
let startDate
|
||||||
const currentDate = props.member.vipExpireDate ? new Date(props.member.vipExpireDate) : new Date()
|
if (props.member.vipendtime) {
|
||||||
const newExpireDate = new Date(currentDate.setMonth(currentDate.getMonth() + selectedPackage.value.duration))
|
// 如果有当前到期时间,以当前到期时间为起始时间
|
||||||
|
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 selectedDate = new Date(newExpireDate.value + ' 23:59:59')
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
if (selectedDate <= now) {
|
||||||
|
ElMessage.warning('新的到期时间不能早于当前时间')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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', {
|
emit('success', {
|
||||||
memberId: props.member.id,
|
memberId: props.member.id,
|
||||||
packageId: selectedPackage.value.id,
|
newExpireDate: updateData.vipendtime
|
||||||
expireDate: newExpireDate.toISOString().split('T')[0]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
handleClose()
|
handleClose()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('续费失败:', error)
|
||||||
|
ElMessage.error('续费失败,请重试')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.package-grid {
|
.recharge-content {
|
||||||
display: grid;
|
padding: 0;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card {
|
/* 会员信息卡片 */
|
||||||
border: 1px solid #e4e7ed;
|
.member-info-card {
|
||||||
border-radius: 8px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
margin-bottom: 24px;
|
||||||
cursor: pointer;
|
color: white;
|
||||||
transition: all 0.3s;
|
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card:hover {
|
.card-header {
|
||||||
border-color: #409eff;
|
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);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card.active {
|
.time-card.current-time {
|
||||||
border-color: #409eff;
|
border-left: 4px solid #10b981;
|
||||||
background-color: #ecf5ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-name {
|
.time-card.new-time {
|
||||||
font-size: 16px;
|
border-left: 4px solid #3b82f6;
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-price {
|
.time-label {
|
||||||
color: #f56c6c;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
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;
|
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-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-duration {
|
.duration-tip {
|
||||||
color: #909399;
|
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;
|
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>
|
</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">
|
<div class="flex items-center justify-center">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="scope.row.isvip === 0"
|
v-if="scope.row.isvip === 0"
|
||||||
type="primary"
|
type="success"
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleRecharge(scope.row)"
|
@click="handleRecharge(scope.row)"
|
||||||
@ -116,6 +116,13 @@
|
|||||||
@success="handleRechargeSuccess"
|
@success="handleRechargeSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 开通会员弹窗 -->
|
||||||
|
<BecomeVipDialog
|
||||||
|
v-model:visible="becomeVipDialogVisible"
|
||||||
|
:member="currentMember"
|
||||||
|
@success="handleBecomeVipSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 密码验证弹窗 -->
|
<!-- 密码验证弹窗 -->
|
||||||
<PasswordVerifyDialog
|
<PasswordVerifyDialog
|
||||||
v-model:visible="passwordVerifyVisible"
|
v-model:visible="passwordVerifyVisible"
|
||||||
@ -128,6 +135,8 @@
|
|||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import RechargeDialog from './components/RechargeDialog.vue'
|
import RechargeDialog from './components/RechargeDialog.vue'
|
||||||
|
import BecomeVipDialog from './components/becomvip.vue'
|
||||||
|
import PasswordVerifyDialog from './components/PasswordVerifyDialog.vue'
|
||||||
import { PersonApi } from '@/api/person'
|
import { PersonApi } from '@/api/person'
|
||||||
import { getUserProfile } from '@/api/system/user/profile'
|
import { getUserProfile } from '@/api/system/user/profile'
|
||||||
|
|
||||||
@ -146,6 +155,7 @@ const queryFormRef = ref()
|
|||||||
|
|
||||||
// 对话框控制
|
// 对话框控制
|
||||||
const rechargeDialogVisible = ref(false)
|
const rechargeDialogVisible = ref(false)
|
||||||
|
const becomeVipDialogVisible = ref(false)
|
||||||
const passwordVerifyVisible = ref(false)
|
const passwordVerifyVisible = ref(false)
|
||||||
|
|
||||||
// 当前选中的会员
|
// 当前选中的会员
|
||||||
@ -193,7 +203,6 @@ const getList = async () => {
|
|||||||
if (res) {
|
if (res) {
|
||||||
memberList.value = res.list
|
memberList.value = res.list
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
console.log(memberList.value)
|
|
||||||
} else {
|
} else {
|
||||||
memberList.value = []
|
memberList.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
@ -223,18 +232,27 @@ const resetQuery = () => {
|
|||||||
// 续费/开通会员
|
// 续费/开通会员
|
||||||
const handleRecharge = (member) => {
|
const handleRecharge = (member) => {
|
||||||
currentMember.value = member
|
currentMember.value = member
|
||||||
|
if (member.isvip === 0) {
|
||||||
|
// 未开通会员,打开开通会员弹窗
|
||||||
|
becomeVipDialogVisible.value = true
|
||||||
|
} else {
|
||||||
|
// 已开通会员,打开续费弹窗
|
||||||
rechargeDialogVisible.value = true
|
rechargeDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 续费成功
|
// 续费成功
|
||||||
const handleRechargeSuccess = (data) => {
|
const handleRechargeSuccess = (data) => {
|
||||||
const member = mockMembers.find(m => m.id === data.memberId)
|
// 刷新会员列表数据
|
||||||
if (member) {
|
|
||||||
member.vipStatus = true
|
|
||||||
member.vipExpireDate = data.expireDate
|
|
||||||
getList()
|
getList()
|
||||||
ElMessage.success('操作成功')
|
ElMessage.success('续费成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开通会员成功
|
||||||
|
const handleBecomeVipSuccess = (data) => {
|
||||||
|
// 刷新会员列表数据
|
||||||
|
getList()
|
||||||
|
ElMessage.success('会员开通成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消会员
|
// 取消会员
|
||||||
@ -244,25 +262,21 @@ const handleCancel = (member) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 密码验证成功
|
// 密码验证成功
|
||||||
const handlePasswordVerifySuccess = (password) => {
|
const handlePasswordVerifySuccess = async (password) => {
|
||||||
|
try {
|
||||||
// TODO: 这里需要调用后端API验证密码并取消会员
|
// 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('已取消会员资格')
|
ElMessage.success('已取消会员资格')
|
||||||
// 关闭密码验证弹窗
|
// 关闭密码验证弹窗
|
||||||
passwordVerifyVisible.value = false
|
passwordVerifyVisible.value = false
|
||||||
// 清空当前选中的会员
|
// 清空当前选中的会员
|
||||||
currentMember.value = null
|
currentMember.value = null
|
||||||
|
// 刷新列表
|
||||||
|
getList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消会员失败:', error)
|
||||||
|
ElMessage.error('取消会员失败,请重试')
|
||||||
}
|
}
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
Loading…
Reference in New Issue
Block a user