Files
2026-09-29 15:17:13 +08:00

346 lines
8.3 KiB
Vue

<template>
<view class="points-page">
<view class="page-hero">
<AppNavbar title="我的积分" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>我的积分</text>
</view>
<text class="intro-text">查看积分余额和积分明细记录。</text>
</view>
</view>
<view class="card summary-card">
<view class="summary-item">
<view class="summary-value">{{ summary.currentPoints }}</view>
<view class="summary-label">当前积分</view>
</view>
<view class="summary-item">
<view class="summary-value">{{ summary.totalEarned }}</view>
<view class="summary-label">累计获得</view>
</view>
<!-- <view class="summary-item">-->
<!-- <view class="summary-value">{{ summary.totalUsed }}</view>-->
<!-- <view class="summary-label">累计消耗</view>-->
<!-- </view>-->
</view>
<view class="card block">
<view class="section-title">积分明细</view>
<view v-if="loading && !records.length" class="empty">加载中...</view>
<view v-else-if="!records.length" class="empty">暂无积分记录</view>
<view v-else class="record-list">
<view class="record-item" v-for="item in records" :key="item.id">
<view class="record-main">
<view class="record-title-row">
<view class="record-title">{{ item.title }}</view>
<view :class="['record-points', item.delta >= 0 ? 'record-points--plus' : 'record-points--minus']">
{{ item.deltaText }}
</view>
</view>
<view class="record-meta">{{ dayjs(item.date).format('YYYY-MM-DD HH:mm:ss') }}</view>
<view class="record-desc" v-if="item.remark">{{ item.remark }}</view>
</view>
</view>
<view class="list-status">{{ finished ? '没有更多了' : '上拉加载更多' }}</view>
</view>
</view>
</view>
</template>
<script>
import AppNavbar from "@/components/AppNavbar.vue";
import {userPointLogUserListReq} from "@/api/shop";
import {userInfoReq} from "@/api/user";
import dayjs from 'dayjs'
export default {
components: {AppNavbar},
data() {
return {
loading: false,
page: 1,
limit: 20,
finished: false,
records: [],
user: {},
summary: {
currentPoints: 0,
totalEarned: 0,
totalUsed: 0
}
}
},
onShow() {
this.refreshData()
},
onPullDownRefresh() {
this.refreshData().finally(() => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
if (this.loading || this.finished) return
this.fetchPointLogs()
},
methods: {
dayjs,
async refreshData() {
this.page = 1
this.finished = false
this.records = []
await Promise.all([
this.getUserData(),
this.fetchPointLogs(true)
])
},
async getUserData() {
try {
const {data} = await userInfoReq()
this.user = data || {}
this.summary.currentPoints = this.resolveSummaryPoints()
} catch (error) {
this.user = {}
}
},
async fetchPointLogs(refresh = false) {
this.loading = true
try {
const currentPage = refresh ? 1 : this.page
const {data} = await userPointLogUserListReq({
page: currentPage,
limit: this.limit
})
const rawList = Array.isArray(data)
? data
: (data && Array.isArray(data.list) ? data.list : [])
const normalizedList = rawList.map(this.normalizeRecord)
this.records = refresh ? normalizedList : this.records.concat(normalizedList)
this.page = currentPage + 1
this.finished = !rawList.length || rawList.length < this.limit
this.updateSummary(data)
} catch (error) {
if (refresh) {
this.records = []
}
uni.showToast({
title: '积分加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
updateSummary(payload) {
const totalEarned = this.records
.filter(item => item.delta > 0)
.reduce((sum, item) => sum + item.delta, 0)
const totalUsed = this.records
.filter(item => item.delta < 0)
.reduce((sum, item) => sum + Math.abs(item.delta), 0)
this.summary = {
currentPoints: this.resolveSummaryPoints(payload, this.records),
totalEarned,
totalUsed
}
},
resolveSummaryPoints(payload = {}, records = this.records) {
const candidates = [
payload.currentPoints,
payload.currentPoint,
payload.points,
payload.point,
payload.totalPoints,
payload.totalPoint,
this.user.currentPoints,
this.user.currentPoint,
this.user.points,
this.user.point,
this.user.integral,
this.user.score
]
for (let i = 0; i < candidates.length; i++) {
const value = Number(candidates[i])
if (Number.isFinite(value)) return value
}
return records.reduce((sum, item) => sum + item.delta, 0)
},
resolveDelta(item = {}) {
const candidates = [
item.changePoint,
item.changePoints,
item.changeNum,
item.delta,
item.num,
item.amount,
item.value,
item.point,
item.points,
item.integral,
item.score
]
for (let i = 0; i < candidates.length; i++) {
const value = Number(candidates[i])
if (Number.isFinite(value)) return value
}
return 0
},
normalizeRecord(item = {}) {
const delta = this.resolveDelta(item)
return {
id: item.id || item.logId || item.userPointLogId || `${item.createTime || item.createdAt || ''}-${delta}`,
title: item.title || item.typeName || item.bizTypeName || item.remark || '积分变动',
remark: item.description || item.content || item.memo || '',
date: item.createTime || item.createdAt || item.date || '-',
delta,
deltaText: `${delta > 0 ? '+' : ''}${delta}`
}
}
}
}
</script>
<style lang="scss" scoped>
.points-page {
min-height: 100vh;
background: #f5f5f5;
}
.page-hero {
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat top center / 100% auto;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.card {
margin: 18rpx 20rpx 0;
background: #ffffff;
border-radius: 20rpx;
padding: 20rpx 18rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.06);
}
.summary-card {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12rpx;
}
.summary-item {
padding: 18rpx 12rpx;
border-radius: 16rpx;
text-align: center;
background: linear-gradient(135deg, #f0fffb, #ffffff);
border: 1rpx solid #ccfbf1;
}
.summary-value {
font-size: 34rpx;
font-weight: 800;
color: #2fc6a7;
}
.summary-label {
margin-top: 8rpx;
font-size: 22rpx;
color: #6b7280;
}
.section-title {
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
margin-bottom: 14rpx;
}
.record-list {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.record-item {
padding: 18rpx 16rpx;
border-radius: 16rpx;
background: #f8fafc;
border: 1rpx solid #e5e7eb;
}
.record-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.record-title {
font-size: 28rpx;
font-weight: 600;
color: #1f2937;
}
.record-points {
font-size: 30rpx;
font-weight: 700;
}
.record-points--plus {
color: #16a34a;
}
.record-points--minus {
color: #ef4444;
}
.record-meta {
margin-top: 10rpx;
font-size: 22rpx;
color: #94a3b8;
}
.record-desc {
margin-top: 8rpx;
font-size: 24rpx;
color: #64748b;
line-height: 1.5;
}
.empty,
.list-status {
padding: 36rpx 0 18rpx;
text-align: center;
font-size: 24rpx;
color: #94a3b8;
}
</style>