feat: VIP模块

This commit is contained in:
李琦
2026-08-04 14:53:00 +08:00
parent fe841e772a
commit 1f8803a971
2 changed files with 197 additions and 4 deletions

View File

@@ -0,0 +1,154 @@
<template>
<!-- 会员卡片等级徽标 + 期限丝带叠加绑定整体放大 -->
<view class="vip-card">
<view class="vip-card-inner">
<view class="vip-badge-combo">
<image
v-if="levelBadge"
class="vip-level"
:src="levelBadge"
mode="aspectFit"
/>
<image
v-if="durationBadge"
class="vip-ribbon"
:src="durationBadge"
mode="aspectFit"
/>
<view v-if="!durationBadge && !levelBadge" class="vip-badge-placeholder">
<text class="vip-badge-placeholder-text">V</text>
</view>
</view>
<view class="vip-info">
<text class="vip-title">{{ levelName }}</text>
<text class="vip-sub">{{ durationLabel }} · {{ levelCode }}</text>
<text class="vip-expire">{{ expireText }}</text>
</view>
</view>
</view>
</template>
<script>
/**
* 诊所管理员「我的」页会员卡片
* 期限图为透明丝带,叠在等级徽标上绑定展示
*/
export default {
name: 'VipMemberCard',
props: {
vip: {
type: Object,
default: null,
},
},
computed: {
durationBadge() {
return (this.vip && this.vip.duration_badge_url) || ''
},
levelBadge() {
return (this.vip && this.vip.badge_url) || ''
},
durationLabel() {
return (this.vip && this.vip.duration_label) || '普通会员'
},
levelName() {
return (this.vip && this.vip.level_name) || '普通会员'
},
levelCode() {
return (this.vip && this.vip.level_code) || 'V0'
},
expireText() {
if (!this.vip) return '默认会员'
if (this.vip.is_lifetime) return '永久有效'
const expireAt = Number(this.vip.expire_at || 0)
if (!expireAt) {
if (this.levelCode === 'V0') return '默认会员'
return '永久有效'
}
if (typeof this.vip.expire_at === 'string' && this.vip.expire_at.indexOf('-') > -1) {
return '有效期至 ' + this.vip.expire_at
}
const d = new Date(expireAt * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return '有效期至 ' + y + '-' + m + '-' + day
},
},
}
</script>
<style lang="scss" scoped>
.vip-card {
margin: 0 24rpx 24rpx;
}
.vip-card-inner {
display: flex;
align-items: center;
padding: 36rpx 32rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #2c3e50 0%, #4a6741 55%, #6acdbb 100%);
box-shadow: 0 8rpx 24rpx rgba(44, 62, 80, 0.18);
}
/* 组合徽标容器:等级在下,丝带叠在中下部 */
.vip-badge-combo {
position: relative;
width: 160rpx;
height: 160rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.vip-level {
width: 140rpx;
height: 140rpx;
position: relative;
z-index: 1;
}
.vip-ribbon {
position: absolute;
left: 50%;
bottom: 10rpx;
transform: translateX(-50%);
width: 200rpx;
height: 72rpx;
z-index: 2;
}
.vip-badge-placeholder {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.vip-badge-placeholder-text {
color: #fff;
font-size: 52rpx;
font-weight: 700;
}
.vip-info {
margin-left: 24rpx;
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.vip-title {
color: #ffffff;
font-size: 34rpx;
font-weight: 700;
margin-bottom: 8rpx;
}
.vip-sub {
color: rgba(255, 255, 255, 0.88);
font-size: 24rpx;
margin-bottom: 8rpx;
}
.vip-expire {
color: rgba(255, 255, 255, 0.75);
font-size: 22rpx;
}
</style>

View File

@@ -14,6 +14,8 @@
<text v-if="storeName && storeName !== '-'" class="store-line">{{ storeName }}</text> <text v-if="storeName && storeName !== '-'" class="store-line">{{ storeName }}</text>
</view> </view>
</view> </view>
<!-- 会员卡片功能列表入口上方 -->
<vip-member-card :vip="vipInfo" />
<!-- 白色菜单卡绑定微信 + 切换账号 --> <!-- 白色菜单卡绑定微信 + 切换账号 -->
<view class="setting-panel"> <view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" /> <wx-bind-entry ref="wxBindEntry" />
@@ -35,18 +37,22 @@
<script> <script>
/** /**
* 诊所/门店管理员「我的」:布局对齐医生(菜单卡 + 绿色退出) * 诊所/门店管理员「我的」:布局对齐医生(菜单卡 + 绿色退出)
* 资料区与功能入口之间展示门店 VIP 会员卡片
*/ */
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue' import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import VipMemberCard from '@/subPackages/sub_clinic_admin/components/vip-member-card.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue' import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue' import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import { getClinicAdminMyInfo } from '@/api/clinicAdmin.js'
import { clearLoginStorage } from '@/utils/loginSession.js' import { clearLoginStorage } from '@/utils/loginSession.js'
export default { export default {
components: { ClinicPageLayout, AccountSwitchEntry, WxBindEntry }, components: { ClinicPageLayout, VipMemberCard, AccountSwitchEntry, WxBindEntry },
data() { data() {
return { return {
displayName: '', displayName: '',
storeName: '', storeName: '',
vipInfo: null,
} }
}, },
computed: { computed: {
@@ -56,12 +62,45 @@ export default {
}, },
}, },
onShow() { onShow() {
const user = uni.getStorageSync('clinic_admin_user') || {} this.loadLocalUser()
this.displayName = user.nick_name || user.name || user.account || '管理员' this.refreshVip()
this.storeName = user.store_name || (user.store && user.store.name) || '-'
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh() if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
}, },
methods: { methods: {
/** 先读本地缓存,保证首屏有基础信息 */
loadLocalUser() {
const user = uni.getStorageSync('clinic_admin_user') || {}
this.displayName = user.nick_name || user.name || user.account || '管理员'
this.storeName = user.store_name || (user.store && user.store.name) || '-'
if (user.vip) {
this.vipInfo = user.vip
}
},
/**
* 拉取 my-info刷新 VIP 并写回缓存
* 为什么单独请求:本地缓存可能滞后于后台开通/到期回落
*/
async refreshVip() {
try {
const { data, ok } = await getClinicAdminMyInfo()
if (!ok || !data) return
const info = data
if (info.nick_name || info.name) {
this.displayName = info.nick_name || info.name || this.displayName
}
if (info.store_name) {
this.storeName = info.store_name
}
this.vipInfo = info.vip || null
const cached = uni.getStorageSync('clinic_admin_user') || {}
uni.setStorageSync('clinic_admin_user', Object.assign({}, cached, info, {
vip: info.vip || null,
}))
} catch (e) {
// 静默失败:保留本地缓存展示
console.warn('刷新诊所管理员VIP失败', e)
}
},
logout() { logout() {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',