Files
jczxw-pc/app/pages/console/points.vue
2026-04-23 16:30:57 +08:00

275 lines
7.1 KiB
Vue

<template>
<div class="space-y-4">
<a-page-header title="积分明细" sub-title="查看积分收支记录" />
<!-- 积分概览 -->
<a-card :bordered="false" class="card points-banner">
<div class="points-row">
<div>
<div class="points-label">当前积分</div>
<div class="points-value">{{ currentPoints }}</div>
</div>
<StarOutlined class="points-icon" />
</div>
</a-card>
<!-- 筛选 -->
<a-card :bordered="false" class="card">
<div class="filter-row">
<a-select
v-model:value="filterScene"
placeholder="全部类型"
allow-clear
style="width: 140px"
@change="onFilterChange"
>
<a-select-option :value="1">充值赠送</a-select-option>
<a-select-option :value="2">消费扣除</a-select-option>
<a-select-option :value="3">签到奖励</a-select-option>
<a-select-option :value="4">活动奖励</a-select-option>
<a-select-option :value="5">积分兑换</a-select-option>
<a-select-option :value="9">系统调整</a-select-option>
</a-select>
<a-range-picker
v-model:value="filterDateRange"
value-format="YYYY-MM-DD"
:placeholder="['开始日期', '结束日期']"
style="width: 240px"
@change="onFilterChange"
/>
<a-button @click="resetFilter">重置</a-button>
</div>
<!-- 列表 -->
<a-spin :spinning="loading">
<div v-if="!list.length && !loading" class="empty-wrap">
<a-empty description="暂无积分记录" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
<div v-else class="log-list">
<div v-for="item in list" :key="item.logId" class="log-item">
<div class="log-icon" :class="getSceneClass(item.scene)">
<component :is="getSceneIcon(item.scene)" />
</div>
<div class="log-body">
<div class="log-desc">{{ item.describe || getSceneLabel(item.scene) }}</div>
<div class="log-time">{{ item.createTime }}</div>
</div>
<div class="log-points" :class="(item.points ?? 0) > 0 ? 'add' : 'sub'">
{{ (item.points ?? 0) > 0 ? '+' : '' }}{{ item.points }}
</div>
</div>
</div>
<a-pagination
v-if="total > pageSize"
v-model:current="page"
:total="total"
:page-size="pageSize"
style="margin-top: 16px; text-align: right"
@change="loadList"
/>
</a-spin>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, type Component } from 'vue'
import { Empty, message } from 'ant-design-vue'
import {
StarOutlined,
GiftOutlined,
ShoppingOutlined,
CheckCircleOutlined,
TrophyOutlined,
SwapOutlined,
SettingOutlined,
} from '@ant-design/icons-vue'
import { getUserInfo } from '@/api/layout'
import { pageUserPointLog } from '@/api/user/point-log'
import type { UserPointLog } from '@/api/user/point-log/model'
definePageMeta({ layout: 'console' })
useHead({ title: '积分明细 - 控制台' })
const currentPoints = ref(0)
const loading = ref(false)
const list = ref<UserPointLog[]>([])
const page = ref(1)
const total = ref(0)
const pageSize = 15
const filterScene = ref<number | undefined>(undefined)
const filterDateRange = ref<[string, string] | null>(null)
const SCENE_MAP: Record<number, { label: string; icon: Component; cls: string }> = {
1: { label: '充值赠送', icon: GiftOutlined, cls: 'gift' },
2: { label: '消费扣除', icon: ShoppingOutlined, cls: 'spend' },
3: { label: '签到奖励', icon: CheckCircleOutlined, cls: 'checkin' },
4: { label: '活动奖励', icon: TrophyOutlined, cls: 'activity' },
5: { label: '积分兑换', icon: SwapOutlined, cls: 'exchange' },
9: { label: '系统调整', icon: SettingOutlined, cls: 'system' },
}
function getSceneLabel(scene?: number) {
return scene !== undefined ? (SCENE_MAP[scene]?.label ?? '积分变动') : '积分变动'
}
function getSceneIcon(scene?: number): Component {
return scene !== undefined ? (SCENE_MAP[scene]?.icon ?? StarOutlined) : StarOutlined
}
function getSceneClass(scene?: number) {
return scene !== undefined ? (SCENE_MAP[scene]?.cls ?? 'default') : 'default'
}
async function loadBalance() {
try {
const u = await getUserInfo()
currentPoints.value = u?.points ?? 0
} catch { /* ignore */ }
}
async function loadList() {
loading.value = true
try {
const params: Record<string, unknown> = {
page: page.value,
limit: pageSize,
}
if (filterScene.value !== undefined) params.scene = filterScene.value
if (filterDateRange.value?.[0]) params.createTimeStart = filterDateRange.value[0]
if (filterDateRange.value?.[1]) params.createTimeEnd = filterDateRange.value[1]
const res = await pageUserPointLog(params)
list.value = res?.list ?? []
total.value = res?.total ?? 0
} catch (e) {
message.error(e instanceof Error ? e.message : '加载失败')
} finally {
loading.value = false
}
}
function onFilterChange() {
page.value = 1
loadList()
}
function resetFilter() {
filterScene.value = undefined
filterDateRange.value = null
page.value = 1
loadList()
}
loadBalance()
loadList()
</script>
<style scoped>
.card {
border-radius: 12px;
}
/* 积分概览 */
.points-banner {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
}
.points-row {
display: flex;
align-items: center;
justify-content: space-between;
color: white;
}
.points-label {
font-size: 14px;
opacity: 0.85;
margin-bottom: 6px;
}
.points-value {
font-size: 36px;
font-weight: 700;
}
.points-icon {
font-size: 44px;
opacity: 0.25;
}
:deep(.points-banner .ant-card-body) {
color: white;
}
/* 筛选 */
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
/* 记录列表 */
.empty-wrap {
padding: 32px 0;
}
.log-list {
display: flex;
flex-direction: column;
}
.log-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 0;
border-bottom: 1px solid #f5f5f5;
}
.log-item:last-child {
border-bottom: none;
}
.log-icon {
width: 38px;
height: 38px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.log-icon.gift { background: #fff7e6; color: #fa8c16; }
.log-icon.spend { background: #fff1f0; color: #f5222d; }
.log-icon.checkin { background: #f6ffed; color: #52c41a; }
.log-icon.activity { background: #fffbe6; color: #faad14; }
.log-icon.exchange { background: #f0f5ff; color: #597ef7; }
.log-icon.system { background: #f5f5f5; color: #8c8c8c; }
.log-icon.default { background: #e6f7ff; color: #1890ff; }
.log-body {
flex: 1;
min-width: 0;
}
.log-desc {
font-size: 14px;
color: rgba(0,0,0,0.85);
}
.log-time {
font-size: 12px;
color: rgba(0,0,0,0.35);
margin-top: 2px;
}
.log-points {
font-size: 16px;
font-weight: 700;
white-space: nowrap;
flex-shrink: 0;
}
.log-points.add {
color: #52c41a;
}
.log-points.sub {
color: #f5222d;
}
</style>