Files
2025-09-29 21:38:32 +08:00

473 lines
12 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="vip-page">
<view class="vip-hero">
<image class="hero-icon" src="/static/icons/icons8-vip-48 (1).png" mode="aspectFit" />
<view class="hero-text">
<text class="hero-title">{{ isVip ? 'VIP会员' : '升级 VIP 会员' }}</text>
<text class="hero-subtitle">{{ isVip ? '尊享完整数据与高效体验' : '开通后可查看全部历史数据并解锁高级功能' }}</text>
</view>
<view class="status-pill" :class="{ active: isVip }">
<text>{{ isVip ? '已开通' : '普通用户' }}</text>
</view>
</view>
<view class="vip-summary" v-if="isVip">
<view class="summary-item">
<text class="summary-label">会员状态</text>
<text class="summary-value success">已激活</text>
</view>
<view class="summary-item">
<text class="summary-label">有效期至</text>
<text class="summary-value">{{ expireDisplay }}</text>
</view>
</view>
<view class="vip-summary" v-else>
<view class="summary-item">
<text class="summary-label">当前身份</text>
<text class="summary-value">普通用户</text>
</view>
<view class="summary-item">
<text class="summary-label">会员价格</text>
<text class="summary-value highlight">¥{{ priceDisplay }}/</text>
</view>
</view>
<view class="benefit-section">
<view class="section-header">
<text class="section-title">会员特权</text>
<text class="section-subtitle">聚焦数据留存与专业形象让经营更有底气</text>
</view>
<view class="benefit-grid">
<view v-for="item in benefits" :key="item.key" class="benefit-card">
<image v-if="item.icon" :src="item.icon" class="benefit-icon" mode="aspectFit" />
<text class="benefit-title">{{ item.title }}</text>
<text class="benefit-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<!-- 已是VIP展示申请普通管理员入口 -->
<view v-if="isVip" class="apply-card">
<view class="apply-text">
<text class="apply-title">申请成为普通管理员</text>
<text class="apply-desc">在普通管理端参与配件审核</text>
</view>
<view role="button" :class="['apply-btn', { disabled: applyDisabled }]" @click="onApplyNormalAdmin">
<text>{{ applyBtnText }}</text>
</view>
</view>
<view v-if="!isVip" class="purchase-card">
<view class="purchase-text">
<text class="purchase-title">立即升级 VIP</text>
<text class="purchase-desc">不限历史数据专属标识助您高效管账</text>
</view>
<button class="purchase-btn" @click="onPay">
<text>立即开通</text>
</button>
</view>
</view>
</template>
<script>
import { get, post } from '../../common/http.js'
export default {
data(){
return {
isVip: false,
expire: '',
price: 0,
benefits: [],
normalAdmin: { isNormalAdmin: false, applicationStatus: 'none' }
}
},
onShow(){
this.loadVip()
this.loadNormalAdminStatus()
this.composeBenefits()
},
computed: {
expireDisplay(){
const v = this.expire
if (v === null || v === undefined) return ''
if (typeof v === 'number') {
const d = new Date(v)
if (!isNaN(d.getTime())) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${dd}`
}
return ''
}
const s = String(v)
const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
if (m) return m[1]
const idx = s.search(/[ T]/)
if (idx > 0) {
const head = s.slice(0, idx)
if (head) return head
}
const d2 = new Date(s)
if (!isNaN(d2.getTime())) {
const y = d2.getFullYear()
const m2 = String(d2.getMonth() + 1).padStart(2, '0')
const dd2 = String(d2.getDate()).padStart(2, '0')
return `${y}-${m2}-${dd2}`
}
return s
},
priceDisplay(){
const n = Number(this.price)
return Number.isFinite(n) && n > 0 ? n.toFixed(2) : '0.00'
},
applyDisabled(){
const s = String(this.normalAdmin?.applicationStatus || 'none')
return !!(this.normalAdmin?.isNormalAdmin || s === 'approved' || s === 'pending')
},
applyBtnText(){
if (this.normalAdmin?.isNormalAdmin || this.normalAdmin?.applicationStatus === 'approved') return '已通过'
if (this.normalAdmin?.applicationStatus === 'pending') return '审核中'
if (!this.isVip) return '仅限VIP'
return '提交申请'
}
},
methods: {
composeBenefits(){
this.benefits = [
{ key: 'history', title: '完整历史留存', desc: '无限期保留交易、库存与客户数据', icon: '/static/icons/icons8-graph-report-50.png' },
{ key: 'analysis', title: '高级统计面板', desc: '秒级汇总销售毛利,掌握生意节奏', icon: '/static/icons/icons8-profit-50.png' },
{ key: 'priority', title: '优先客服支持', desc: '遇到问题优先处理,响应更迅速', icon: '/static/icons/icons8-account-male-100.png' }
]
},
async loadVip(){
try {
const data = await get('/api/vip/status')
this.isVip = !!data?.isVip
this.expire = data?.expireAt || ''
if (typeof data?.price === 'number') this.price = data.price
} catch(e) {
// 保底不回退到硬编码价格仅展示0并提示可开通
this.isVip = false
}
},
async loadNormalAdminStatus(){
try {
const data = await get('/api/normal-admin/application/status')
this.normalAdmin = {
isNormalAdmin: !!data?.isNormalAdmin,
applicationStatus: String(data?.applicationStatus || 'none')
}
} catch(e) {
this.normalAdmin = { isNormalAdmin: false, applicationStatus: 'none' }
}
},
async onPay(){
try {
await post('/api/vip/pay', {})
uni.showToast({ title: '已开通VIP', icon: 'success' })
await this.loadVip()
} catch(e) {
uni.showToast({ title: String(e.message || '开通失败'), icon: 'none' })
}
},
async onApplyNormalAdmin(){
if (this.applyDisabled) {
const msg = this.normalAdmin?.isNormalAdmin || this.normalAdmin?.applicationStatus === 'approved' ? '已通过,无需重复申请' : (this.normalAdmin?.applicationStatus === 'pending' ? '审核中,请耐心等待' : '不可申请')
return uni.showToast({ title: msg, icon: 'none' })
}
try {
await post('/api/normal-admin/apply', { remark: '从我的-会员发起申请' })
uni.showToast({ title: '申请已提交', icon: 'success' })
await this.loadNormalAdminStatus()
} catch(e) {
uni.showToast({ title: String(e.message || '申请失败'), icon: 'none' })
}
}
}
}
</script>
<style lang="scss">
page {
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%) !important;
}
.vip-page {
min-height: 100vh;
padding: 32rpx 24rpx 120rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 28rpx;
}
.vip-hero {
display: flex;
align-items: center;
gap: 20rpx;
padding: 26rpx 28rpx;
border-radius: 24rpx;
background: rgba(255,255,255,0.98);
border: 2rpx solid #edf2f9;
box-shadow: 0 10rpx 30rpx rgba(76,141,255,0.12);
}
.hero-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: #f0f6ff;
padding: 12rpx;
}
.hero-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.hero-title {
font-size: 36rpx;
font-weight: 800;
color: $uni-color-primary;
letter-spacing: 1rpx;
}
.hero-subtitle {
font-size: 26rpx;
color: #5175b5;
line-height: 36rpx;
}
.status-pill {
flex: 0 0 auto;
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: #e6edfb;
color: #4463a6;
font-size: 24rpx;
font-weight: 700;
border: 2rpx solid rgba(76,141,255,0.2);
}
.status-pill.active {
background: #4c8dff;
color: #fff;
border-color: #4c8dff;
}
/* 指定 hero 内激活态徽标文本为黑色 */
.vip-hero .status-pill.active text { color: #000 !important; }
.vip-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0,1fr));
gap: 16rpx;
background: rgba(255,255,255,0.98);
padding: 24rpx;
border-radius: 24rpx;
border: 2rpx solid #eef3fb;
box-shadow: 0 8rpx 24rpx rgba(99,132,191,0.10);
}
.summary-item {
background: #f6f9ff;
border-radius: 18rpx;
padding: 22rpx 24rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
border: 2rpx solid rgba(76,141,255,0.12);
}
.summary-label {
font-size: 24rpx;
color: #5f7394;
}
.summary-value {
font-size: 30rpx;
font-weight: 700;
color: #1f2c3d;
}
.summary-value.success {
color: #1ead91;
}
.summary-value.highlight {
color: #2f58d1;
}
.benefit-section {
background: rgba(255,255,255,0.98);
border-radius: 24rpx;
padding: 28rpx;
border: 2rpx solid #edf2f9;
box-shadow: 0 12rpx 28rpx rgba(32,75,143,0.10);
display: flex;
flex-direction: column;
gap: 24rpx;
}
.section-header {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.section-title {
font-size: 34rpx;
font-weight: 800;
color: $uni-text-color;
}
.section-subtitle {
font-size: 24rpx;
color: #5f7394;
line-height: 34rpx;
}
.benefit-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20rpx;
}
.benefit-card {
background: #f7faff;
border-radius: 20rpx;
padding: 24rpx 20rpx;
border: 2rpx solid rgba(76,141,255,0.12);
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.04);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14rpx;
}
.benefit-icon {
width: 48rpx;
height: 48rpx;
}
.benefit-title {
font-size: 28rpx;
font-weight: 700;
color: $uni-text-color;
}
.benefit-desc {
font-size: 24rpx;
line-height: 34rpx;
color: #5f7394;
}
.purchase-card {
margin-top: auto;
background: linear-gradient(135deg, rgba(76,141,255,0.14) 0%, rgba(76,141,255,0.06) 100%);
border-radius: 28rpx;
padding: 30rpx 28rpx;
display: flex;
align-items: center;
gap: 24rpx;
border: 2rpx solid rgba(76,141,255,0.18);
box-shadow: 0 10rpx 24rpx rgba(76,141,255,0.15);
}
.purchase-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.purchase-title {
font-size: 32rpx;
font-weight: 800;
color: $uni-color-primary;
}
.purchase-desc {
font-size: 24rpx;
color: #4463a6;
line-height: 34rpx;
}
.purchase-btn {
flex: 0 0 auto;
padding: 20rpx 36rpx;
border-radius: 999rpx;
border: none;
background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%);
color: #fff;
font-size: 28rpx;
font-weight: 700;
box-shadow: 0 10rpx 22rpx rgba(45,107,230,0.20);
}
.purchase-btn:active {
opacity: 0.88;
}
.apply-card {
margin-top: 0;
background: linear-gradient(135deg, rgba(30,173,145,0.14) 0%, rgba(30,173,145,0.06) 100%);
border-radius: 28rpx;
padding: 30rpx 28rpx;
display: flex;
align-items: center;
gap: 24rpx;
border: 2rpx solid rgba(30,173,145,0.18);
box-shadow: 0 10rpx 24rpx rgba(30,173,145,0.15);
}
.apply-text { flex: 1; display:flex; flex-direction: column; gap: 10rpx; }
.apply-title { font-size: 32rpx; font-weight: 800; color: #1ead91; }
.apply-desc { font-size: 24rpx; color: #247a66; line-height: 34rpx; }
.apply-btn {
flex: 0 0 auto;
padding: 20rpx 36rpx;
border-radius: 999rpx;
border: none;
background-color: transparent;
background: linear-gradient(135deg, #1ead91 0%, #159b7e 100%);
color: #fff;
font-size: 28rpx;
font-weight: 700;
box-shadow: 0 10rpx 22rpx rgba(21,155,126,0.20);
}
.apply-btn::after { border: none; }
.apply-btn:active { opacity: .9; }
.apply-btn.disabled {
opacity: .5;
background: #c7e8df;
color: #fff;
box-shadow: none;
pointer-events: none;
}
@media (max-width: 375px) {
.vip-summary {
grid-template-columns: 1fr;
}
.benefit-grid {
grid-template-columns: 1fr;
}
.purchase-card {
flex-direction: column;
align-items: stretch;
}
.status-pill {
display: none;
}
}
</style>