Files
medical-mall/pages/mall/consumer/member/index.uvue

652 lines
14 KiB
Plaintext

<template>
<scroll-view class="member-page" scroll-y>
<view class="member-header">
<view class="member-info">
<view class="level-badge" :class="'level-' + memberInfo.member_level">
<text class="level-name">{{ memberInfo.level_name }}</text>
</view>
<view class="discount-info">
<text class="discount-value">{{ getDiscountText(memberInfo.discount) }}</text>
<text class="discount-label">会员折扣</text>
</view>
</view>
</view>
<view class="progress-section" v-if="memberInfo.next_level != null">
<view class="progress-header">
<text class="progress-title">距离{{ getNextLevelName() }}还需</text>
<text class="progress-amount">{{ getRemainingAmount() }}元</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: memberInfo.progress_percent + '%' }"></view>
</view>
<view class="progress-footer">
<text class="current-amount">已消费 {{ memberInfo.total_spent }}元</text>
<text class="target-amount">目标 {{ getNextLevelMinAmount() }}元</text>
</view>
</view>
<view class="levels-section">
<view class="section-header">
<text class="section-title">会员等级</text>
</view>
<view class="level-list">
<view
class="level-item"
v-for="level in levels"
:key="level.id"
:class="{ current: level.id === memberInfo.member_level }"
>
<view class="level-left">
<view class="level-icon" :class="'level-bg-' + level.id">
<text class="icon-text">{{ level.name.charAt(0) }}</text>
</view>
<view class="level-detail">
<text class="level-title">{{ level.name }}</text>
<text class="level-condition">{{ level.description != null && level.description != '' ? level.description : ('累计消费' + level.min_amount + '元') }}</text>
</view>
</view>
<view class="level-right">
<text class="level-discount">{{ getDiscountText(level.discount) }}</text>
<view class="current-tag" v-if="level.id === memberInfo.member_level">
<text class="tag-text">当前</text>
</view>
</view>
</view>
</view>
</view>
<view class="benefits-section">
<view class="section-header">
<text class="section-title">会员权益</text>
</view>
<view class="benefit-list">
<view class="benefit-item">
<text class="benefit-icon">💰</text>
<text class="benefit-text">专属折扣价格</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">🎁</text>
<text class="benefit-text">生日专属优惠</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">🚀</text>
<text class="benefit-text">优先发货权益</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">📞</text>
<text class="benefit-text">专属客服通道</text>
</view>
</view>
</view>
<view class="logs-section">
<view class="section-header">
<text class="section-title">等级变更记录</text>
</view>
<view v-if="logsLoading" class="loading-state">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="logs.length === 0" class="empty-state">
<text class="empty-text">暂无变更记录</text>
</view>
<view v-else class="log-list">
<view class="log-item" v-for="log in logs" :key="log.id">
<view class="log-left">
<text class="log-change">{{ getLevelName(log.old_level) }} → {{ getLevelName(log.new_level) }}</text>
<text class="log-reason">{{ log.reason != null && log.reason != '' ? log.reason : '系统升级' }}</text>
</view>
<text class="log-time">{{ formatDate(log.created_at) }}</text>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type MemberLevel = {
id: number
name: string
min_amount: number
discount: number
description: string | null
}
type MemberInfo = {
member_level: number
level_name: string
discount: number
total_spent: number
next_level: MemberLevel | null
progress_percent: number
manual_level: boolean
}
type LevelLog = {
id: string
old_level: number
new_level: number
reason: string | null
created_at: string
}
const memberInfo = ref<MemberInfo>({
member_level: 0,
level_name: '普通会员',
discount: 1.0,
total_spent: 0,
next_level: null,
progress_percent: 0,
manual_level: false
})
const levels = ref<MemberLevel[]>([])
const logs = ref<LevelLog[]>([])
const logsLoading = ref<boolean>(false)
const loadMemberInfo = async (): Promise<void> => {
try {
const result = await supabaseService.getUserMemberInfo()
const info: MemberInfo = {
member_level: result.getNumber('member_level') ?? 0,
level_name: result.getString('level_name') ?? '普通会员',
discount: result.getNumber('discount') ?? 1.0,
total_spent: result.getNumber('total_spent') ?? 0,
next_level: null,
progress_percent: result.getNumber('progress_percent') ?? 0,
manual_level: result.getBoolean('manual_level') ?? false
}
const nextLevelRaw = result.get('next_level')
if (nextLevelRaw != null) {
let nextLevelObj: UTSJSONObject | null = null
if (nextLevelRaw instanceof UTSJSONObject) {
nextLevelObj = nextLevelRaw
} else {
nextLevelObj = JSON.parse(JSON.stringify(nextLevelRaw)) as UTSJSONObject
}
const nextLevel: MemberLevel = {
id: nextLevelObj.getNumber('id') ?? 0,
name: nextLevelObj.getString('name') ?? '',
min_amount: nextLevelObj.getNumber('min_amount') ?? 0,
discount: 1.0,
description: null
}
info.next_level = nextLevel
}
memberInfo.value = info
} catch (e) {
console.error('加载会员信息失败:', e)
}
}
const loadLevels = async (): Promise<void> => {
try {
const result = await supabaseService.getMemberLevels()
const parsed: MemberLevel[] = []
for (let i = 0; i < result.length; i++) {
const item = result[i]
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
parsed.push({
id: itemObj.getNumber('id') ?? 0,
name: itemObj.getString('name') ?? '',
min_amount: itemObj.getNumber('min_amount') ?? 0,
discount: itemObj.getNumber('discount') ?? 1.0,
description: itemObj.getString('description')
})
}
levels.value = parsed
} catch (e) {
console.error('加载会员等级失败:', e)
}
}
const loadLogs = async (): Promise<void> => {
logsLoading.value = true
try {
const result = await supabaseService.getMemberLevelLogs()
const parsed: LevelLog[] = []
for (let i = 0; i < result.length; i++) {
const item = result[i]
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
parsed.push({
id: itemObj.getString('id') ?? '',
old_level: itemObj.getNumber('old_level') ?? 0,
new_level: itemObj.getNumber('new_level') ?? 0,
reason: itemObj.getString('reason'),
created_at: itemObj.getString('created_at') ?? ''
})
}
logs.value = parsed
} catch (e) {
console.error('加载变更记录失败:', e)
} finally {
logsLoading.value = false
}
}
const getDiscountText = (discount: number): string => {
if (discount >= 1) return '无折扣'
return Math.round(discount * 100) / 10 + '折'
}
const getNextLevelName = (): string => {
if (memberInfo.value.next_level != null) {
return memberInfo.value.next_level.name
}
return ''
}
const getNextLevelMinAmount = (): number => {
if (memberInfo.value.next_level != null) {
return memberInfo.value.next_level.min_amount
}
return 0
}
const getRemainingAmount = (): number => {
if (memberInfo.value.next_level != null) {
return memberInfo.value.next_level.min_amount - memberInfo.value.total_spent
}
return 0
}
const getLevelName = (level: number): string => {
for (let i = 0; i < levels.value.length; i++) {
if (levels.value[i].id === level) {
return levels.value[i].name
}
}
return '普通会员'
}
const formatDate = (dateStr: string): string => {
if (dateStr === '') return ''
const date = new Date(dateStr)
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
return `${y}-${m}-${d}`
}
onMounted(() => {
loadMemberInfo()
loadLevels()
loadLogs()
})
</script>
<style>
.member-page {
flex: 1;
height: 100%;
background-color: #f5f5f5;
}
.member-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.member-info {
display: flex;
flex-direction: column;
align-items: center;
}
.level-badge {
padding: 8px 24px;
border-radius: 20px;
margin-bottom: 16px;
}
.level-badge.level-0 {
background-color: rgba(255, 255, 255, 0.3);
}
.level-badge.level-1 {
background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%);
}
.level-badge.level-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%);
}
.level-badge.level-3 {
background: linear-gradient(135deg, #ffd700 0%, #ffec8b 100%);
}
.level-badge.level-4 {
background: linear-gradient(135deg, #b9f2ff 0%, #89cff0 100%);
}
.level-badge.level-5 {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%);
}
.level-name {
font-size: 18px;
font-weight: bold;
color: white;
}
.discount-info {
display: flex;
flex-direction: column;
align-items: center;
}
.discount-value {
font-size: 36px;
font-weight: bold;
color: white;
}
.discount-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin-top: 4px;
}
.progress-section {
background-color: white;
padding: 16px;
margin: 12px;
border-radius: 12px;
}
.progress-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-title {
font-size: 14px;
color: #666;
}
.progress-amount {
font-size: 16px;
font-weight: bold;
color: #667eea;
}
.progress-bar {
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 4px;
}
.progress-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 8px;
}
.current-amount {
font-size: 12px;
color: #999;
}
.target-amount {
font-size: 12px;
color: #999;
}
.levels-section {
background-color: white;
margin: 12px;
border-radius: 12px;
overflow: hidden;
}
.section-header {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.level-list {
display: flex;
flex-direction: column;
}
.level-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f9f9f9;
}
.level-item.current {
background-color: #f8f5ff;
}
.level-left {
display: flex;
flex-direction: row;
align-items: center;
}
.level-icon {
width: 40px;
height: 40px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.level-bg-0 {
background-color: #f0f0f0;
}
.level-bg-1 {
background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%);
}
.level-bg-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%);
}
.level-bg-3 {
background: linear-gradient(135deg, #ffd700 0%, #ffec8b 100%);
}
.level-bg-4 {
background: linear-gradient(135deg, #b9f2ff 0%, #89cff0 100%);
}
.level-bg-5 {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%);
}
.icon-text {
font-size: 16px;
font-weight: bold;
color: white;
}
.level-detail {
display: flex;
flex-direction: column;
}
.level-title {
font-size: 15px;
font-weight: bold;
color: #333;
}
.level-condition {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.level-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.level-discount {
font-size: 14px;
font-weight: bold;
color: #667eea;
}
.current-tag {
background-color: #667eea;
padding: 2px 8px;
border-radius: 4px;
margin-top: 4px;
}
.tag-text {
font-size: 10px;
color: white;
}
.benefits-section {
background-color: white;
margin: 12px;
border-radius: 12px;
}
.benefit-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 8px;
}
.benefit-item {
width: 50%;
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 8px;
}
.benefit-icon {
font-size: 20px;
margin-right: 8px;
}
.benefit-text {
font-size: 13px;
color: #666;
}
.logs-section {
background-color: white;
margin: 12px;
border-radius: 12px;
}
.loading-state {
padding: 30px 0;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 14px;
color: #999;
}
.empty-state {
padding: 30px 0;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 14px;
color: #999;
}
.log-list {
display: flex;
flex-direction: column;
}
.log-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f9f9f9;
}
.log-left {
display: flex;
flex-direction: column;
}
.log-change {
font-size: 14px;
color: #333;
}
.log-reason {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.log-time {
font-size: 12px;
color: #999;
}
</style>