652 lines
14 KiB
Plaintext
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>
|