增加推销模式
This commit is contained in:
640
pages/mall/consumer/member/index.uvue
Normal file
640
pages/mall/consumer/member/index.uvue
Normal file
@@ -0,0 +1,640 @@
|
||||
<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 || ('累计消费' + 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 || '系统升级' }}</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()
|
||||
|
||||
memberInfo.value = {
|
||||
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) {
|
||||
const nextLevelAny = nextLevelRaw as any
|
||||
if (typeof nextLevelAny._getValue === 'function') {
|
||||
memberInfo.value.next_level = {
|
||||
id: (nextLevelAny._getValue('id') as number) ?? 0,
|
||||
name: (nextLevelAny._getValue('name') as string) ?? '',
|
||||
min_amount: (nextLevelAny._getValue('min_amount') as number) ?? 0,
|
||||
discount: 1.0,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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]
|
||||
const itemAny = item as any
|
||||
|
||||
if (typeof itemAny._getValue === 'function') {
|
||||
parsed.push({
|
||||
id: (itemAny._getValue('id') as number) ?? 0,
|
||||
name: (itemAny._getValue('name') as string) ?? '',
|
||||
min_amount: (itemAny._getValue('min_amount') as number) ?? 0,
|
||||
discount: (itemAny._getValue('discount') as number) ?? 1.0,
|
||||
description: itemAny._getValue('description') as string | null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
const itemAny = item as any
|
||||
|
||||
if (typeof itemAny._getValue === 'function') {
|
||||
parsed.push({
|
||||
id: (itemAny._getValue('id') as string) ?? '',
|
||||
old_level: (itemAny._getValue('old_level') as number) ?? 0,
|
||||
new_level: (itemAny._getValue('new_level') as number) ?? 0,
|
||||
reason: itemAny._getValue('reason') as string | null,
|
||||
created_at: (itemAny._getValue('created_at') as string) ?? ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user