续费弹窗
This commit is contained in:
parent
bb77d1635d
commit
74ec9fb972
@ -147,6 +147,14 @@
|
||||
@success="handleDeviceVipSuccess"
|
||||
/>
|
||||
|
||||
<RenewVipDialog
|
||||
v-model:visible="renewVipDialogVisible"
|
||||
:member="currentDevice"
|
||||
:combos="enabledCombos"
|
||||
:title="'续费会员'"
|
||||
@success="handleDeviceVipSuccess"
|
||||
/>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
@ -158,6 +166,7 @@ import download from '@/utils/download'
|
||||
import { DeviceApi, DeviceVO } from '@/api/device'
|
||||
import { getUserProfile } from '@/api/system/user/profile'
|
||||
import BecomeVipDialog from './becomevip.vue'
|
||||
import RenewVipDialog from './renewvip.vue'
|
||||
import { ComboApi } from '@/api/combo'
|
||||
import { getStrDictOptions, getDictLabel } from '@/utils/dict'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
@ -190,6 +199,7 @@ const queryParams = reactive({
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
const becomeVipDialogVisible = ref(false)
|
||||
const renewVipDialogVisible = ref(false)
|
||||
const currentDevice = ref<any>(null)
|
||||
const enabledCombos = ref<any[]>([])
|
||||
|
||||
@ -309,7 +319,7 @@ const handleDeviceOpenVip = async (row: DeviceVO) => {
|
||||
const handleDeviceRecharge = async (row: DeviceVO) => {
|
||||
await preloadCombos()
|
||||
currentDevice.value = { ...row, name: (row as any).devicename }
|
||||
becomeVipDialogVisible.value = true
|
||||
renewVipDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 取消会员:置 isvip=0 并清空起止时间
|
||||
@ -326,6 +336,7 @@ const handleDeviceCancel = async (row: DeviceVO) => {
|
||||
// 弹窗提交成功后刷新列表
|
||||
const handleDeviceVipSuccess = () => {
|
||||
becomeVipDialogVisible.value = false
|
||||
renewVipDialogVisible.value = false
|
||||
getList()
|
||||
}
|
||||
</script>
|
727
src/views/vip/VIP_device/renewvip.vue
Normal file
727
src/views/vip/VIP_device/renewvip.vue
Normal file
@ -0,0 +1,727 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="title" width="700px" @close="handleClose">
|
||||
<div class="become-vip-content">
|
||||
<!-- 会员套餐 -->
|
||||
<div class="my-combos" v-if="enabledCombos.length">
|
||||
<div class="combos-header">
|
||||
<Icon icon="ep:star" class="combos-icon" />
|
||||
<span>会员套餐</span>
|
||||
</div>
|
||||
<div class="combos-carousel">
|
||||
<el-button class="nav-btn" link @click="prevPage" :disabled="comboPageIndex === 0">
|
||||
<Icon icon="ep:arrow-left" />
|
||||
</el-button>
|
||||
<div class="combos-list">
|
||||
<div
|
||||
v-for="combo in visibleCombos"
|
||||
:key="combo.id"
|
||||
class="combo-card"
|
||||
:class="{ selected: selectedComboId === combo.id }"
|
||||
@click="selectCombo(combo)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="combo-name">{{ combo.comboname }}</div>
|
||||
<div class="combo-price">¥{{ combo.price }}</div>
|
||||
<div class="combo-period">{{ combo.period }}天</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="nav-btn" link @click="nextPage" :disabled="(comboPageIndex + 1) * pageSize >= enabledCombos.length">
|
||||
<Icon icon="ep:arrow-right" />
|
||||
</el-button>
|
||||
</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" :class="{ selected: !!customExpireDate }" @click.self="toggleCustomDuration">
|
||||
<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"
|
||||
:clearable="true"
|
||||
class="custom-picker"
|
||||
@change="handleCustomDateChange"
|
||||
/>
|
||||
</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, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { PersonApi } from '@/api/person'
|
||||
import { DeviceApi } from '@/api/device'
|
||||
import { ComboApi } from '@/api/combo'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
combos: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '开通会员'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const dialogVisible = ref(props.visible)
|
||||
const selectedDuration = ref('')
|
||||
const customExpireDate = ref('')
|
||||
const loading = ref(false)
|
||||
// 套餐相关
|
||||
const enabledCombos = ref([])
|
||||
const comboPageIndex = ref(0)
|
||||
const pageSize = 3
|
||||
const selectedComboId = ref(null)
|
||||
const visibleCombos = computed(() => {
|
||||
const start = comboPageIndex.value * pageSize
|
||||
return enabledCombos.value.slice(start, start + pageSize)
|
||||
})
|
||||
const prevPage = () => {
|
||||
if (comboPageIndex.value > 0) comboPageIndex.value -= 1
|
||||
}
|
||||
const nextPage = () => {
|
||||
if ((comboPageIndex.value + 1) * pageSize < enabledCombos.value.length) comboPageIndex.value += 1
|
||||
}
|
||||
const selectCombo = (combo) => {
|
||||
selectedComboId.value = selectedComboId.value === combo.id ? null : combo.id
|
||||
}
|
||||
|
||||
// 快速选择时长选项
|
||||
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) {
|
||||
// 弹窗打开时,不默认选择
|
||||
customExpireDate.value = ''
|
||||
// 打开时拉取套餐
|
||||
fetchCombos()
|
||||
selectedComboId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 监听dialogVisible变化
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val)
|
||||
})
|
||||
|
||||
// 选择时长
|
||||
const selectDuration = (duration) => {
|
||||
if (selectedDuration.value === duration) {
|
||||
selectedDuration.value = ''
|
||||
} else {
|
||||
selectedDuration.value = duration
|
||||
customExpireDate.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理自定义日期变化
|
||||
const handleCustomDateChange = () => {
|
||||
if (customExpireDate.value) {
|
||||
selectedDuration.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 点击自定义区域空白可取消选中
|
||||
const toggleCustomDuration = () => {
|
||||
if (customExpireDate.value) {
|
||||
customExpireDate.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 已移除时长预览相关计算属性
|
||||
|
||||
// 计算结束日期:基于传入的基准时间(用于续费叠加)
|
||||
const calculateEndDateFrom = (baseDate) => {
|
||||
const endDate = new Date(baseDate)
|
||||
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 baseDate
|
||||
}
|
||||
return endDate
|
||||
}
|
||||
|
||||
// 拉取启用状态的套餐(若父组件已传入,则直接使用)
|
||||
const fetchCombos = async () => {
|
||||
if (Array.isArray(props.combos) && props.combos.length) {
|
||||
enabledCombos.value = props.combos
|
||||
comboPageIndex.value = 0
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await ComboApi.getComboPage({ pageNo: 1, pageSize: 100, status: 1 })
|
||||
enabledCombos.value = Array.isArray(res?.list) ? res.list : []
|
||||
comboPageIndex.value = 0
|
||||
} catch (e) {
|
||||
enabledCombos.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 已移除时长预览相关计算函数
|
||||
|
||||
// 禁用过去的日期
|
||||
const disabledDate = (time) => {
|
||||
return time.getTime() < Date.now()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
selectedDuration.value = ''
|
||||
customExpireDate.value = ''
|
||||
loading.value = false
|
||||
selectedComboId.value = null
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 工具:日期计算
|
||||
const addDays = (base, days) => {
|
||||
const d = new Date(base.getTime())
|
||||
d.setDate(d.getDate() + days)
|
||||
return d
|
||||
}
|
||||
const endOfDay = (d) => {
|
||||
const x = new Date(d.getTime())
|
||||
x.setHours(23, 59, 59, 0)
|
||||
return x
|
||||
}
|
||||
|
||||
// 先查询当前设备,获取当前到期时间和开始时间
|
||||
const detail = await DeviceApi.getDeviceId(props.member.devicecode)
|
||||
const now = new Date()
|
||||
const currentVipEndMs = detail && detail.vipendtime ? Number(detail.vipendtime) : 0
|
||||
const currentVipStartMs = detail && detail.vipstarttime ? Number(detail.vipstarttime) : now.getTime()
|
||||
|
||||
// 基准时间:取 当前到期时间 与 当前时间 的较大者
|
||||
const baseDate = new Date(Math.max(currentVipEndMs || 0, now.getTime()))
|
||||
|
||||
// 计算新的到期时间
|
||||
let vipendtimeMs = 0
|
||||
if (selectedComboId.value) {
|
||||
const combo = enabledCombos.value.find(c => c.id === selectedComboId.value)
|
||||
if (!combo) {
|
||||
ElMessage.warning('未找到所选套餐')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
const end = endOfDay(addDays(baseDate, Number(combo.period) || 0))
|
||||
vipendtimeMs = end.getTime()
|
||||
} else if (customExpireDate.value) {
|
||||
const chosen = endOfDay(new Date(customExpireDate.value))
|
||||
if (chosen.getTime() <= baseDate.getTime()) {
|
||||
ElMessage.warning('选择的到期时间需晚于当前到期时间')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
vipendtimeMs = chosen.getTime()
|
||||
} else if (selectedDuration.value) {
|
||||
const endDate = calculateEndDateFrom(baseDate)
|
||||
const end = endOfDay(endDate)
|
||||
vipendtimeMs = end.getTime()
|
||||
} else {
|
||||
ElMessage.warning('请先选择套餐或设置会员时长')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
devicecode: props.member.devicecode,
|
||||
orgid: props.member.orgid,
|
||||
vipstarttime: currentVipStartMs,
|
||||
vipendtime: vipendtimeMs,
|
||||
isvip: 1
|
||||
}
|
||||
|
||||
await DeviceApi.updateDeviceVip(payload)
|
||||
|
||||
ElMessage.success('会员开通成功')
|
||||
|
||||
// 触发成功事件
|
||||
emit('success', {
|
||||
devicecode: props.member.devicecode,
|
||||
newExpireDate: vipendtimeMs
|
||||
})
|
||||
|
||||
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: 16px;
|
||||
margin-bottom: 16px;
|
||||
color: white;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
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: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.member-phone {
|
||||
font-size: 13px;
|
||||
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: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.duration-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 快速选择区域 */
|
||||
.quick-select {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
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: 12px;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
color: #f59e0b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.quick-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
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: 16px;
|
||||
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: 12px;
|
||||
}
|
||||
|
||||
.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: 16px;
|
||||
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: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.preview-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 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;
|
||||
}
|
||||
/* 按钮间距 */
|
||||
.el-button+.el-button{
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.preview-item .value.highlight {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 我的套餐 */
|
||||
.my-combos {
|
||||
margin-top: 16px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.combos-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.combos-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.combos-carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.nav-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.combos-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
.combo-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.combo-card:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.combo-name {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
.combo-price {
|
||||
color: #10b981;
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.combo-period {
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.combo-card.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@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>
|
Loading…
Reference in New Issue
Block a user