feat: VIP模块
This commit is contained in:
154
subPackages/sub_clinic_admin/components/vip-member-card.vue
Normal file
154
subPackages/sub_clinic_admin/components/vip-member-card.vue
Normal 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>
|
||||
@@ -14,6 +14,8 @@
|
||||
<text v-if="storeName && storeName !== '-'" class="store-line">{{ storeName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 会员卡片:功能列表入口上方 -->
|
||||
<vip-member-card :vip="vipInfo" />
|
||||
<!-- 白色菜单卡:绑定微信 + 切换账号 -->
|
||||
<view class="setting-panel">
|
||||
<wx-bind-entry ref="wxBindEntry" />
|
||||
@@ -35,18 +37,22 @@
|
||||
<script>
|
||||
/**
|
||||
* 诊所/门店管理员「我的」:布局对齐医生(菜单卡 + 绿色退出)
|
||||
* 资料区与功能入口之间展示门店 VIP 会员卡片
|
||||
*/
|
||||
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 WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
|
||||
import { getClinicAdminMyInfo } from '@/api/clinicAdmin.js'
|
||||
import { clearLoginStorage } from '@/utils/loginSession.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, AccountSwitchEntry, WxBindEntry },
|
||||
components: { ClinicPageLayout, VipMemberCard, AccountSwitchEntry, WxBindEntry },
|
||||
data() {
|
||||
return {
|
||||
displayName: '',
|
||||
storeName: '',
|
||||
vipInfo: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -56,12 +62,45 @@ export default {
|
||||
},
|
||||
},
|
||||
onShow() {
|
||||
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) || '-'
|
||||
this.loadLocalUser()
|
||||
this.refreshVip()
|
||||
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
|
||||
},
|
||||
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() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
|
||||
Reference in New Issue
Block a user