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>
|
<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: '提示',
|
||||||
|
|||||||
Reference in New Issue
Block a user