使用 Math.round(score * 100) / 100 避免 JavaScript 浮点数计算误差 例如 0.1 × 46 现在正确显示为 4.60 而不是 4.600000000000005
273 lines
6.4 KiB
Vue
273 lines
6.4 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="reward-records-page">
|
||
<div class="container">
|
||
<!-- 页面标题 -->
|
||
<div class="page-header">
|
||
<a-button type="text" @click="router.back()" class="back-btn">
|
||
<Icon name="chevronLeft" size="md" color="#AFAFAF" />
|
||
</a-button>
|
||
<div class="header-info">
|
||
<h1 class="page-title">
|
||
<Icon name="clock" size="xl" color="#31f08b" style="margin-right: 12px;" />
|
||
奖励记录
|
||
</h1>
|
||
<p class="page-desc">查看你获得的所有奖励</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-row">
|
||
<div class="stat-card">
|
||
<div class="stat-value">{{ totalScore }}</div>
|
||
<div class="stat-label">累计获得积分</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{{ totalCount }}</div>
|
||
<div class="stat-label">获奖次数</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 记录列表 -->
|
||
<div class="records-section">
|
||
<a-table
|
||
:columns="columns"
|
||
:data-source="records"
|
||
:loading="loading"
|
||
:pagination="{
|
||
current: currentPage,
|
||
pageSize: pageSize,
|
||
total: total,
|
||
onChange: handlePageChange,
|
||
showSizeChanger: false
|
||
}"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'rewardType'">
|
||
<a-tag :color="getTypeColor(record.rewardType)">
|
||
{{ getTypeName(record.rewardType) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-if="column.key === 'rewardValue'">
|
||
<span class="reward-value">{{ parseRewardValue(record.rewardValue) }}</span>
|
||
</template>
|
||
<template v-if="column.key === 'createTime'">
|
||
{{ formatTime(record.createTime) }}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import dayjs from 'dayjs'
|
||
import {
|
||
getMyRewardRecords,
|
||
type RewardRecord
|
||
} from '@/api/rewardRule'
|
||
|
||
const router = useRouter()
|
||
|
||
const loading = ref(false)
|
||
const records = ref<RewardRecord[]>([])
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(20)
|
||
const total = ref(0)
|
||
|
||
const columns = [
|
||
{ title: '奖励规则', dataIndex: 'ruleName', key: 'ruleName' },
|
||
{ title: '奖励类型', dataIndex: 'rewardType', key: 'rewardType', width: 120 },
|
||
{ title: '奖励内容', dataIndex: 'rewardValue', key: 'rewardValue', width: 150 },
|
||
{ title: '获得时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||
]
|
||
|
||
const totalScore = computed(() => {
|
||
const total = records.value.reduce((sum, r) => {
|
||
try {
|
||
const obj = JSON.parse(r.rewardValue)
|
||
return sum + (obj.score || 0)
|
||
} catch {
|
||
return sum
|
||
}
|
||
}, 0)
|
||
return formatScore(total)
|
||
})
|
||
|
||
// 格式化积分(保留两位小数,修复浮点数精度问题)
|
||
const formatScore = (score: number | null | undefined): string => {
|
||
if (score === null || score === undefined) return '0.00'
|
||
// 先乘以100,四舍五入取整,再除以100,避免浮点数精度问题
|
||
return (Math.round(Number(score) * 100) / 100).toFixed(2)
|
||
}
|
||
|
||
const totalCount = computed(() => total.value)
|
||
|
||
const getTypeColor = (type: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
score: 'green',
|
||
badge: 'gold',
|
||
title: 'purple',
|
||
exp: 'blue'
|
||
}
|
||
return colorMap[type] || 'default'
|
||
}
|
||
|
||
const getTypeName = (type: string) => {
|
||
const nameMap: Record<string, string> = {
|
||
score: '积分',
|
||
badge: '徽章',
|
||
title: '称号',
|
||
exp: '经验值'
|
||
}
|
||
return nameMap[type] || type
|
||
}
|
||
|
||
const parseRewardValue = (value: string) => {
|
||
try {
|
||
const obj = JSON.parse(value)
|
||
if (obj.score) return `+${formatScore(obj.score)} 积分`
|
||
if (obj.badge) return `徽章: ${obj.badge}`
|
||
if (obj.title) return `称号: ${obj.title}`
|
||
return value
|
||
} catch {
|
||
return value
|
||
}
|
||
}
|
||
|
||
const formatTime = (time: string) => {
|
||
return dayjs(time).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page
|
||
loadData()
|
||
}
|
||
|
||
const loadData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const result = await getMyRewardRecords({
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value
|
||
})
|
||
records.value = result.records || []
|
||
total.value = result.total || 0
|
||
} catch (error) {
|
||
console.error('加载奖励记录失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "@/styles/variables.scss";
|
||
|
||
.reward-records-page {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.back-btn {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: $text-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-desc {
|
||
color: $text-secondary;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.stats-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
text-align: center;
|
||
|
||
.stat-value {
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: $text-secondary;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
|
||
.records-section {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.reward-value {
|
||
color: $brand-primary;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.ant-table) {
|
||
background: transparent;
|
||
|
||
.ant-table-thead > tr > th {
|
||
background: $bg-secondary;
|
||
border-bottom: 1px solid $border-primary;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.ant-table-tbody > tr > td {
|
||
border-bottom: 1px solid $border-primary;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.ant-table-tbody > tr:hover > td {
|
||
background: $bg-secondary;
|
||
}
|
||
}
|
||
</style>
|
||
|