邹方成 6f7207da2d feat: 优化UI设计并重构样式系统
refactor(components): 重构ElCard、FlipGrid、YifanSelector和PaymentPopup组件样式
refactor(pages): 优化地址管理、商品详情、订单列表、积分记录和活动页面UI
style: 更新uni.scss全局样式变量和设计系统
docs: 添加说明文档记录UI优化进度
2025-12-17 14:32:55 +08:00

339 lines
7.7 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="page-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.change || item.amount || 0) > 0 }">
{{ (item.change || item.amount || 0) > 0 ? '↓' : '↑' }}
</view>
<view class="record-content">
<view class="record-main">
<view class="record-title">{{ item.title || item.reason || '积分变更' }}</view>
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
</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 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}`
}
async function fetchRecords(append = false) {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!user_id || !token || !phoneBound) {
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.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-x: hidden;
}
.page-bg-decoration {
position: absolute;
top: -200rpx;
right: -200rpx;
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.15), transparent 70%);
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.header-area {
padding: $spacing-xl $spacing-lg;
position: relative;
z-index: 1;
}
.page-title {
font-size: 48rpx;
font-weight: 900;
color: $text-main;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.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>