会员开通续费

This commit is contained in:
Flow 2025-06-23 10:48:50 +08:00
parent 06391db10a
commit e71b032180
4 changed files with 1183 additions and 95 deletions

View File

@ -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 })
}
}

View File

@ -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>

View 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>

View File

@ -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(() => {