360 lines
8.5 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="wrap">
<!-- 顶部装饰背景 - 漂浮光球 -->
<view class="bg-decoration"></view>
<view class="header-area">
<view class="page-title">积分明细</view>
<view class="page-subtitle">Points Record</view>
</view>
<view class="content-area">
<view v-if="error" class="error-card">
<text class="error-icon"></text>
<text>{{ error }}</text>
</view>
<view v-if="records.length === 0 && !loading" class="empty-state">
<image class="empty-img" src="/static/empty-points.png" mode="widthFix" />
<text class="empty-text">暂无积分记录</text>
</view>
<view class="records-list" v-else>
<view
v-for="(item, index) in records"
:key="item.id || item.time || item.created_at"
class="record-item"
:style="{ animationDelay: `${index * 0.05}s` }"
>
<view class="record-icon" :class="{ 'is-add': (item.points || 0) > 0 }">
{{ (item.points || 0) > 0 ? '↓' : '↑' }}
</view>
<view class="record-content">
<view class="record-main">
<view class="record-title">{{ getActionText(item.action) || item.title || item.reason || '积分变更' }}</view>
<view class="record-amount" :class="{ inc: (item.points || 0) > 0, dec: (item.points || 0) < 0 }">
{{ (item.points ?? 0) > 0 ? '+' : '' }}{{ formatPoints(item.points) }}
</view>
</view>
<view class="record-footer">
<view class="record-time">{{ formatTime(item.time || item.created_at) }}</view>
<view class="record-status">交易成功</view>
</view>
</view>
</view>
</view>
<view v-if="loadingMore" class="loading-more">
<view class="spinner"></view>
<text>加载中...</text>
</view>
<view v-else-if="!hasMore && records.length > 0" class="no-more">- 到底啦 -</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getPointsRecords } from '../../api/appUser'
const records = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const error = ref('')
const page = ref(1)
const pageSize = ref(20)
const hasMore = ref(true)
function formatPoints(v) {
const n = Number(v) || 0
if (n === 0) return '0'
const f = n / 100
return Number.isInteger(f) ? String(f) : f.toFixed(2).replace(/\.?0+$/, '')
}
function formatTime(t) {
if (!t) return ''
const d = typeof t === 'string' ? new Date(t) : new Date(t)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}`
}
function getActionText(action) {
const map = {
'signin': '每日签到',
'register': '注册赠送',
'invite_reward': '邀请奖励',
'order_deduct': '下单抵扣',
'consume_order': '下单消费',
'refund_restore': '退款返还',
'refund_points': '积分退回',
'refund_amount': '金额退款奖励',
'manual_add': '管理手动增加',
'manual': '系统调整',
'redeem_coupon': '兑换优惠券',
'redeem_product': '兑换商品',
'redeem_reward': '奖品兑换积分',
'redeem_item_card': '兑换道具卡'
}
return map[action] || ''
}
async function fetchRecords(append = false) {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
// 使用统一的手机号绑定检查
const hasPhoneBound = uni.getStorageSync('login_method') === 'wechat_phone' || uni.getStorageSync('login_method') === 'sms' || uni.getStorageSync('phone_number')
if (!user_id || !token || !hasPhoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
if (append) {
if (!hasMore.value || loadingMore.value) return
loadingMore.value = true
page.value = page.value + 1
} else {
loading.value = true
page.value = 1
hasMore.value = true
}
error.value = ''
try {
const list = await getPointsRecords(user_id, page.value, pageSize.value)
const items = Array.isArray(list) ? list : (list && (list.list || list.items)) || []
const total = (list && list.total) || 0
if (append) {
records.value = records.value.concat(items)
} else {
records.value = items
}
if (total) {
hasMore.value = records.value.length < total
} else {
hasMore.value = items.length === pageSize.value
}
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取积分记录失败'
} finally {
if (append) {
loadingMore.value = false
} else {
loading.value = false
}
}
}
onLoad(() => {
fetchRecords(false)
})
onReachBottom(() => {
fetchRecords(true)
})
</script>
<style lang="scss" scoped>
.wrap {
min-height: 100vh;
background-color: $bg-page;
position: relative;
overflow: hidden;
}
/* 背景装饰 - 漂浮光球 (与个人中心统一) */
.header-area {
padding: $spacing-xl $spacing-lg;
padding-top: calc(env(safe-area-inset-top) + 20rpx);
position: relative;
z-index: 1;
}
.page-title {
font-size: 48rpx;
font-weight: 900;
color: $text-main;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.page-subtitle {
font-size: 24rpx;
color: $text-tertiary;
text-transform: uppercase;
letter-spacing: 2rpx;
font-weight: 600;
}
.content-area {
padding: 0 $spacing-lg $spacing-xl;
position: relative;
z-index: 1;
}
.records-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.record-item {
display: flex;
align-items: center;
background: rgba($bg-card, 0.8);
border-radius: $radius-lg;
padding: 30rpx;
box-shadow: $shadow-sm;
backdrop-filter: blur(10px);
border: 1rpx solid rgba(255,255,255,0.5);
animation: fadeInUp 0.5s ease-out backwards;
transition: all 0.2s;
&:active {
transform: scale(0.98);
background: rgba($bg-card, 0.95);
}
}
.record-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: $bg-secondary;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $text-secondary;
margin-right: 24rpx;
flex-shrink: 0;
font-weight: 800;
&.is-add {
background: rgba($uni-color-success, 0.1);
color: $uni-color-success;
}
}
.record-content {
flex: 1;
}
.record-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.record-title {
font-size: 30rpx;
font-weight: 700;
color: $text-main;
}
.record-amount {
font-size: 36rpx;
font-weight: 900;
font-family: 'DIN Alternate', sans-serif;
&.inc { color: $uni-color-success; }
&.dec { color: $text-main; }
}
.record-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.record-time {
font-size: 24rpx;
color: $text-tertiary;
}
.record-status {
font-size: 20rpx;
color: $text-tertiary;
background: $bg-secondary;
padding: 2rpx 10rpx;
border-radius: 6rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
.empty-img {
width: 240rpx;
margin-bottom: 30rpx;
opacity: 0.6;
filter: grayscale(100%);
}
.empty-text {
color: $text-tertiary;
font-size: 28rpx;
}
}
.error-card {
background: rgba($uni-color-error, 0.05);
border: 1rpx solid rgba($uni-color-error, 0.1);
color: $uni-color-error;
padding: 20rpx;
border-radius: $radius-md;
margin-bottom: 30rpx;
display: flex;
align-items: center;
font-size: 26rpx;
.error-icon {
margin-right: 12rpx;
}
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
color: $text-tertiary;
font-size: 24rpx;
gap: 12rpx;
}
.spinner {
width: 28rpx;
height: 28rpx;
border: 3rpx solid $bg-secondary;
border-top-color: $text-tertiary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.no-more {
text-align: center;
padding: 40rpx 0;
color: $text-tertiary;
font-size: 24rpx;
opacity: 0.6;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>