cashier/pages/mine/score-records.vue

200 lines
4.6 KiB
Vue

<template>
<view class="score-records-page">
<view v-if="records.length" class="records-list">
<view v-for="(item, index) in records" :key="recordKey(item, index)" class="record-card">
<view class="record-header">
<text class="record-title">{{ item.remark || formatRecordType(item) }}</text>
<text class="record-score" :class="scoreClass(item)">{{ formatScoreChange(item) }}</text>
</view>
<view class="record-meta-row">
<text class="record-meta-label">时间</text>
<text class="record-meta-value">{{ item.create_time || '-' }}</text>
</view>
<view v-if="item.score !== undefined && item.score !== null && item.score !== ''" class="record-meta-row">
<text class="record-meta-label">余额</text>
<text class="record-meta-value">¥ {{ formatMoney(item.score) }}</text>
</view>
</view>
</view>
<view v-else-if="!loading" class="records-empty">
<wd-status-tip :image="emptyImage" tip="暂无积分流水" />
</view>
<view v-if="records.length && (loading || finished)" class="records-footer">
<wd-loadmore :state="loadMoreState" />
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { platformScoreRecord } from '@/api/cashier';
import type { ScoreRecordItem } from '@/api/cashier';
import { APP_CONFIG } from '@/config';
const PAGE_SIZE = 10;
const records = ref<ScoreRecordItem[]>([]);
const page = ref(1);
const loading = ref(false);
const finished = ref(false);
const emptyImage = `${APP_CONFIG.RESOURCE_URL}tip/message.png`;
const loadMoreState = computed(() => (loading.value ? 'loading' : 'finished'));
const formatMoney = (value?: string | number) => {
const amount = Number(value || 0);
if (!Number.isFinite(amount)) return '0.00';
return amount.toFixed(2);
};
const formatRecordType = (item: ScoreRecordItem) => {
const type = String(item.type ?? '');
const map: Record<string, string> = {
'1': '积分收入',
'2': '积分支出',
'3': '积分返还'
};
return map[type] || '积分变动';
};
const formatScoreChange = (item: ScoreRecordItem) => {
const change = item.change_score ?? item.score;
const amount = Number(change || 0);
if (!Number.isFinite(amount)) return '0.00';
const prefix = amount > 0 ? '+' : '';
return `${prefix}${amount.toFixed(2)}`;
};
const scoreClass = (item: ScoreRecordItem) => {
const change = Number(item.change_score ?? item.score ?? 0);
return change >= 0 ? 'score-plus' : 'score-minus';
};
const recordKey = (item: ScoreRecordItem, index: number) => String(item.id ?? `${item.create_time || 'record'}-${index}`);
const fetchRecords = async (reset = false) => {
if (loading.value) return;
if (!reset && finished.value) return;
loading.value = true;
const nextPage = reset ? 1 : page.value;
try {
const response = await platformScoreRecord({
page: nextPage,
limit: PAGE_SIZE
});
if (Number(response?.code) !== 200) {
throw new Error(response?.msg || '获取积分流水失败');
}
const list = Array.isArray(response?.data) ? response.data : [];
records.value = reset ? list : records.value.concat(list);
page.value = nextPage + 1;
finished.value = list.length < PAGE_SIZE;
} catch (error: any) {
uni.showToast({
title: error?.message || '获取积分流水失败',
icon: 'none'
});
} finally {
loading.value = false;
if (reset) {
uni.stopPullDownRefresh();
}
}
};
onLoad(() => {
fetchRecords(true);
});
onPullDownRefresh(() => {
finished.value = false;
fetchRecords(true);
});
onReachBottom(() => {
fetchRecords(false);
});
</script>
<style lang="scss" scoped>
.score-records-page {
min-height: 100vh;
padding: 24rpx;
background: #f4f6f8;
}
.records-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.record-card {
padding: 28rpx;
border-radius: 16rpx;
background: #ffffff;
box-shadow: 0 8rpx 22rpx rgba(17, 24, 39, 0.04);
}
.record-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.record-title {
flex: 1;
min-width: 0;
color: #111827;
font-size: 30rpx;
font-weight: 700;
}
.record-score {
flex-shrink: 0;
font-size: 34rpx;
font-weight: 800;
}
.score-plus {
color: #1f5bd8;
}
.score-minus {
color: #ef4444;
}
.record-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 18rpx;
}
.record-meta-label {
color: #8b929e;
font-size: 26rpx;
}
.record-meta-value {
color: #374151;
font-size: 26rpx;
}
.records-empty {
padding-top: 300rpx;
}
.records-footer {
padding: 12rpx 0 24rpx;
}
</style>