655 lines
14 KiB
Plaintext
655 lines
14 KiB
Plaintext
<template>
|
||
<scroll-view class="points-page" direction="vertical">
|
||
<view class="points-header">
|
||
<view class="points-info">
|
||
<text class="points-label">当前积分</text>
|
||
<text class="points-value">{{ totalPoints }}</text>
|
||
</view>
|
||
<view class="points-actions">
|
||
<button class="exchange-btn" @click="handleExchange">积分兑换</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quick-actions">
|
||
<view class="action-item" @click="goToSignin">
|
||
<view class="action-icon signin-icon">📅</view>
|
||
<text class="action-text">每日签到</text>
|
||
<view class="action-badge" v-if="!signedToday">
|
||
<text class="badge-text">+5</text>
|
||
</view>
|
||
<view class="signed-badge" v-else>
|
||
<text class="signed-text">已签</text>
|
||
</view>
|
||
</view>
|
||
<view class="action-item" @click="handleExchange">
|
||
<view class="action-icon exchange-icon">🎁</view>
|
||
<text class="action-text">积分兑换</text>
|
||
</view>
|
||
<view class="action-item" @click="goToMyReviews">
|
||
<view class="action-icon review-icon">⭐</view>
|
||
<text class="action-text">我的评价</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="signin-card" v-if="!signedToday">
|
||
<view class="signin-info">
|
||
<text class="signin-title">今日未签到</text>
|
||
<text class="signin-desc">连续签到可获得额外奖励</text>
|
||
</view>
|
||
<button class="signin-btn" @click="goToSignin">去签到</button>
|
||
</view>
|
||
|
||
<view class="signin-card signed" v-else>
|
||
<view class="signin-info">
|
||
<text class="signin-title">今日已签到</text>
|
||
<text class="signin-desc">已连续签到 {{ continuousDays }} 天</text>
|
||
</view>
|
||
<text class="signed-icon">✓</text>
|
||
</view>
|
||
|
||
<view class="expiring-card" v-if="expiringPoints > 0" @click="showExpiringDetails">
|
||
<view class="expiring-icon">⚠️</view>
|
||
<view class="expiring-info">
|
||
<text class="expiring-title">{{ expiringPoints }} 积分即将过期</text>
|
||
<text class="expiring-date">过期日期:{{ expiringDate }}</text>
|
||
</view>
|
||
<text class="expiring-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="records-section">
|
||
<text class="section-title">积分明细</text>
|
||
|
||
<view v-if="loading" class="loading-state">
|
||
<text>加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="records.length === 0" class="empty-state">
|
||
<text class="empty-text">暂无积分记录</text>
|
||
</view>
|
||
|
||
<view v-else class="record-list">
|
||
<view v-for="item in records" :key="item.id" class="record-item">
|
||
<view class="record-left">
|
||
<text class="record-title">{{ item.description ?? getTypeText(item.type) }}</text>
|
||
<text class="record-time">{{ formatTime(item.created_at) }}</text>
|
||
</view>
|
||
<view class="record-right">
|
||
<text class="record-amount" :class="{ positive: item.points > 0, negative: item.points < 0 }">
|
||
{{ item.points > 0 ? '+' : '' }}{{ item.points }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="expiring-popup" v-if="showExpiringPopup" @click="closeExpiringPopup">
|
||
<view class="popup-content" @click.stop>
|
||
<view class="popup-header">
|
||
<text class="popup-title">即将过期积分</text>
|
||
<text class="popup-close" @click="closeExpiringPopup">×</text>
|
||
</view>
|
||
<view class="popup-list">
|
||
<view class="popup-item" v-for="(detail, index) in expiringDetails" :key="index">
|
||
<view class="popup-item-info">
|
||
<text class="popup-item-points">+{{ detail.points }} 积分</text>
|
||
<text class="popup-item-desc">{{ detail.description ?? '积分获取' }}</text>
|
||
</view>
|
||
<view class="popup-item-expire">
|
||
<text class="popup-item-date">{{ formatDate(detail.expires_at) }}</text>
|
||
<text class="popup-item-label">过期</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="popup-tip">
|
||
<text class="tip-text">积分有效期为获取后365天,请及时使用避免过期</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||
|
||
type PointRecord = {
|
||
id: string
|
||
user_id: string
|
||
points: number
|
||
type: string
|
||
description: string
|
||
created_at: string
|
||
}
|
||
|
||
type ExpiringDetail = {
|
||
points: number
|
||
description: string | null
|
||
expires_at: string
|
||
created_at: string
|
||
}
|
||
|
||
const totalPoints = ref<number>(0)
|
||
const records = ref<PointRecord[]>([])
|
||
const loading = ref<boolean>(true)
|
||
const signedToday = ref<boolean>(false)
|
||
const continuousDays = ref<number>(0)
|
||
const expiringPoints = ref<number>(0)
|
||
const expiringDate = ref<string>('')
|
||
const expiringDetails = ref<ExpiringDetail[]>([])
|
||
const showExpiringPopup = ref<boolean>(false)
|
||
|
||
const loadPoints = async (): Promise<void> => {
|
||
try {
|
||
const points = await supabaseService.getUserPoints()
|
||
totalPoints.value = points
|
||
} catch (e) {
|
||
console.error('获取积分失败', e)
|
||
}
|
||
}
|
||
|
||
const loadRecords = async (): Promise<void> => {
|
||
try {
|
||
const list = await supabaseService.getPointRecords()
|
||
records.value = list as PointRecord[]
|
||
} catch (e) {
|
||
console.error('获取积分记录失败', e)
|
||
}
|
||
}
|
||
|
||
const loadSigninStatus = async (): Promise<void> => {
|
||
try {
|
||
const status = await supabaseService.getTodaySigninStatus()
|
||
signedToday.value = status.getBoolean('signed') ?? false
|
||
continuousDays.value = status.getNumber('continuous_days') ?? 0
|
||
} catch (e) {
|
||
console.error('获取签到状态失败', e)
|
||
}
|
||
}
|
||
|
||
const loadExpiringPoints = async (): Promise<void> => {
|
||
try {
|
||
const result = await supabaseService.getExpiringPoints()
|
||
expiringPoints.value = result.getNumber('expiring_points') ?? 0
|
||
expiringDate.value = result.getString('expiring_date') ?? ''
|
||
|
||
const detailsRaw = result.get('details')
|
||
if (detailsRaw != null && Array.isArray(detailsRaw)) {
|
||
const details: ExpiringDetail[] = []
|
||
const arr = detailsRaw as any[]
|
||
for (let i = 0; i < arr.length; i++) {
|
||
const item = arr[i]
|
||
let itemObj: UTSJSONObject
|
||
if (item instanceof UTSJSONObject) {
|
||
itemObj = item
|
||
} else {
|
||
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
}
|
||
details.push({
|
||
points: itemObj.getNumber('points') ?? 0,
|
||
description: itemObj.getString('description'),
|
||
expires_at: itemObj.getString('expires_at') ?? '',
|
||
created_at: itemObj.getString('created_at') ?? ''
|
||
})
|
||
}
|
||
expiringDetails.value = details
|
||
}
|
||
} catch (e) {
|
||
console.error('获取即将过期积分失败', e)
|
||
}
|
||
}
|
||
|
||
const loadData = async (): Promise<void> => {
|
||
loading.value = true
|
||
await Promise.all([
|
||
loadPoints(),
|
||
loadRecords(),
|
||
loadSigninStatus(),
|
||
loadExpiringPoints()
|
||
])
|
||
loading.value = false
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
|
||
const handleExchange = (): void => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/points/exchange'
|
||
})
|
||
}
|
||
|
||
const goToSignin = (): void => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/points/signin'
|
||
})
|
||
}
|
||
|
||
const goToMyReviews = (): void => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/my-reviews'
|
||
})
|
||
}
|
||
|
||
const showExpiringDetails = (): void => {
|
||
showExpiringPopup.value = true
|
||
}
|
||
|
||
const closeExpiringPopup = (): void => {
|
||
showExpiringPopup.value = false
|
||
}
|
||
|
||
const getTypeText = (type: string): string => {
|
||
if (type == 'signin') {
|
||
return '每日签到'
|
||
} else if (type == 'shopping') {
|
||
return '购物奖励'
|
||
} else if (type == 'redeem') {
|
||
return '积分兑换'
|
||
} else if (type == 'admin') {
|
||
return '系统调整'
|
||
} else if (type == 'register') {
|
||
return '注册赠送'
|
||
} else if (type == 'expire') {
|
||
return '积分过期'
|
||
} else {
|
||
return '积分变动'
|
||
}
|
||
}
|
||
|
||
const formatTime = (timeStr: string): string => {
|
||
if (timeStr == '') return ''
|
||
const date = new Date(timeStr)
|
||
const y = date.getFullYear()
|
||
const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
||
const d = date.getDate().toString().padStart(2, '0')
|
||
const hh = date.getHours().toString().padStart(2, '0')
|
||
const mm = date.getMinutes().toString().padStart(2, '0')
|
||
return `${y}-${m}-${d} ${hh}:${mm}`
|
||
}
|
||
|
||
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}`
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.points-page {
|
||
flex: 1;
|
||
height: 100%;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.points-header {
|
||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
|
||
padding: 30px 20px;
|
||
color: white;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.points-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.points-label {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.points-value {
|
||
font-size: 36px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.exchange-btn {
|
||
background-color: rgba(255,255,255,0.2);
|
||
color: white;
|
||
border: 1px solid rgba(255,255,255,0.4);
|
||
font-size: 14px;
|
||
border-radius: 20px;
|
||
padding: 0 15px;
|
||
height: 32px;
|
||
line-height: 32px;
|
||
}
|
||
|
||
.quick-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
background-color: white;
|
||
padding: 16px 0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.action-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
position: relative;
|
||
}
|
||
|
||
.action-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 22px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.signin-icon {
|
||
background-color: #fff5f0;
|
||
}
|
||
|
||
.exchange-icon {
|
||
background-color: #f0f5ff;
|
||
}
|
||
|
||
.review-icon {
|
||
background-color: #fff5f0;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.action-badge {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 20px;
|
||
background-color: #ff6b35;
|
||
border-radius: 8px;
|
||
padding: 2px 6px;
|
||
}
|
||
|
||
.badge-text {
|
||
font-size: 10px;
|
||
color: white;
|
||
}
|
||
|
||
.signed-badge {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 20px;
|
||
background-color: #52c41a;
|
||
border-radius: 8px;
|
||
padding: 2px 6px;
|
||
}
|
||
|
||
.signed-text {
|
||
font-size: 10px;
|
||
color: white;
|
||
}
|
||
|
||
.signin-card {
|
||
background-color: white;
|
||
margin: 0 12px 8px;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.signin-card.signed {
|
||
background: linear-gradient(135deg, #f6ffed 0%, #e6fffb 100%);
|
||
}
|
||
|
||
.signin-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.signin-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.signin-desc {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.signin-btn {
|
||
background-color: #ff6b35;
|
||
color: white;
|
||
font-size: 14px;
|
||
border-radius: 16px;
|
||
padding: 0 20px;
|
||
height: 32px;
|
||
line-height: 32px;
|
||
}
|
||
|
||
.signed-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
background-color: #52c41a;
|
||
border-radius: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
color: white;
|
||
}
|
||
|
||
.expiring-card {
|
||
background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
|
||
margin: 0 12px 8px;
|
||
border-radius: 12px;
|
||
padding: 14px 16px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.expiring-icon {
|
||
font-size: 24px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.expiring-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.expiring-title {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #d48806;
|
||
}
|
||
|
||
.expiring-date {
|
||
font-size: 12px;
|
||
color: #ad8b00;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.expiring-arrow {
|
||
font-size: 20px;
|
||
color: #d48806;
|
||
}
|
||
|
||
.records-section {
|
||
background-color: white;
|
||
padding: 0 16px;
|
||
min-height: 300px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
display: flex;
|
||
}
|
||
|
||
.record-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #f9f9f9;
|
||
}
|
||
|
||
.record-left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.record-title {
|
||
margin-bottom: 4px;
|
||
font-size: 15px;
|
||
color: #333;
|
||
}
|
||
|
||
.record-time {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.record-amount {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.record-amount.positive {
|
||
color: #ff6b35;
|
||
}
|
||
|
||
.record-amount.negative {
|
||
color: #333;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 40px 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.empty-text {
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.loading-state {
|
||
padding: 40px 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.expiring-popup {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.popup-content {
|
||
background-color: white;
|
||
border-radius: 16px 16px 0 0;
|
||
width: 100%;
|
||
max-height: 400px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.popup-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.popup-close {
|
||
font-size: 24px;
|
||
color: #999;
|
||
}
|
||
|
||
.popup-list {
|
||
max-height: 300px;
|
||
}
|
||
|
||
.popup-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.popup-item-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.popup-item-points {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #ff6b35;
|
||
}
|
||
|
||
.popup-item-desc {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.popup-item-expire {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.popup-item-date {
|
||
font-size: 12px;
|
||
color: #d48806;
|
||
}
|
||
|
||
.popup-item-label {
|
||
font-size: 10px;
|
||
color: #999;
|
||
}
|
||
|
||
.popup-tip {
|
||
margin-top: 16px;
|
||
padding: 12px;
|
||
background-color: #f9f9f9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.tip-text {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
</style>
|