Files

655 lines
14 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>