hookehuyr

feat(积分页面): 重构积分页面并添加下拉刷新功能

添加新的图片资源并重构积分页面,实现完整的UI设计和交互功能
- 新增积分列表展示、筛选和搜索功能
- 实现日期选择器和Tab切换功能
- 添加下拉刷新和滚动加载更多功能
- 使用Vant组件优化用户体验
......@@ -78,6 +78,7 @@ declare module 'vue' {
VanPickerGroup: typeof import('vant/es')['PickerGroup']
VanPopup: typeof import('vant/es')['Popup']
VanProgress: typeof import('vant/es')['Progress']
VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanRate: typeof import('vant/es')['Rate']
VanRow: typeof import('vant/es')['Row']
VanSearch: typeof import('vant/es')['Search']
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-24 09:36:29
* @LastEditTime: 2025-12-24 10:58:25
* @FilePath: /mlaj/src/router/routes.js
* @Description: 路由地址映射配置
*/
......@@ -97,12 +97,6 @@ export const routes = [
meta: { title: '我的活动' },
},
{
path: '/profile/points',
name: 'Points',
component: () => import('../views/profile/pointsPage.vue'),
meta: { title: '我的积分' },
},
{
path: '/recall/login',
name: 'RecallLogin',
component: () => import('../views/recall/login.vue'),
......@@ -242,6 +236,12 @@ export const routes = [
meta: { title: '学习记录' },
},
{
path: '/profile/points',
name: 'Points',
component: () => import('../views/profile/pointsPage.vue'),
meta: { title: '我的积分' },
},
{
path: '/test',
name: 'test',
component: () => import('../views/test.vue'),
......
<!--
* @Date: 2025-12-23 17:53:26
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-23 17:53:56
* @FilePath: /mlaj/src/views/profile/pointsPage.vue
* @Description: 我的积分页面
-->
<template>
<div class="points-page">
我的积分
<div class="points-page min-h-screen bg-gray-50 flex flex-col">
<!-- 头部区域 -->
<div class="header relative w-full h-48 bg-cover bg-center" :style="{ backgroundImage: `url(${headerBg})` }">
<div class="absolute top-12 left-6">
<div class="text-white text-sm opacity-90 mb-1">当前星球币</div>
<div class="text-[#FFDD01] text-4xl font-bold tracking-wider">15,800</div>
</div>
<!-- 右侧瓶子 -->
<img :src="bottleImg" class="absolute right-4 top-4 w-32 object-contain animate-float" alt="bottle" />
<!-- 底部提示条 -->
<div class="absolute bottom-0 left-0 w-full h-8 bg-white/20 flex items-center px-4">
<span class="text-white text-xs scale-90 origin-left">可用于兑换活动优惠券或实物奖励,敬请期待!</span>
</div>
</div>
<!-- Tab 标签页 -->
<div class="bg-white pt-2 sticky top-0 z-10 shadow-sm">
<div class="grid grid-cols-3 items-center border-gray-100 pb-0 relative">
<div v-for="(tab, index) in tabs" :key="index"
class="relative py-3 flex flex-col items-center cursor-pointer z-10" @click="handleTabChange(index)">
<span class="text-base font-medium transition-colors duration-300"
:class="activeTab === index ? 'text-gray-900 font-bold' : 'text-gray-400'">
{{ tab }}
</span>
</div>
<!-- 移动指示器 -->
<div class="absolute bottom-0 h-1.5 w-1/3 transition-transform duration-300 ease-in-out z-10"
:style="{ transform: `translateX(${activeTab * 100}%)` }">
<div class="w-full h-full flex justify-center">
<img :src="indicatorImg" class="w-8 h-1.5" alt="indicator" />
</div>
</div>
</div>
<!-- 筛选区域 -->
<div class="p-3 bg-white space-y-3">
<!-- 日期选择 -->
<div class="flex items-center justify-between space-x-2">
<div class="flex-1 bg-white border border-gray-200 rounded-lg h-9 flex items-center px-3 cursor-pointer"
@click="showStartDatePicker = true">
<van-icon name="calendar-o" class="text-gray-400 mr-2" />
<span :class="startDate ? 'text-gray-800' : 'text-gray-400'" class="text-sm truncate">
{{ startDate || '开始日期' }}
</span>
</div>
<span class="text-gray-400 text-sm">至</span>
<div class="flex-1 bg-white border border-gray-200 rounded-lg h-9 flex items-center px-3 cursor-pointer"
@click="showEndDatePicker = true">
<van-icon name="calendar-o" class="text-gray-400 mr-2" />
<span :class="endDate ? 'text-gray-800' : 'text-gray-400'" class="text-sm truncate">
{{ endDate || '结束日期' }}
</span>
</div>
</div>
<!-- 搜索框 -->
<div class="bg-white border border-gray-200 rounded-lg h-9 flex items-center px-3">
<van-icon name="search" class="text-gray-400 mr-2" size="16" />
<input v-model="searchKeyword" type="text" placeholder="搜索活动或课程名称"
class="flex-1 bg-transparent text-sm text-gray-800 placeholder-gray-400 outline-none"
@keyup.enter="onRefresh" />
</div>
</div>
</div>
<!-- 列表区域 -->
<div class="flex-1 overflow-y-auto p-3">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="item in list" :key="item.id" class="bg-white rounded-xl p-4 mb-3 shadow-sm">
<div class="text-gray-900 text-sm font-medium leading-relaxed mb-3 line-clamp-2">
{{ item.title }}
</div>
<div class="flex justify-between items-center">
<div class="flex flex-col">
<span class="text-gray-400 text-xs mb-1">{{ item.date }}</span>
<span class="text-gray-400 text-xs">{{ item.type }}</span>
</div>
<div class="text-lg font-bold" :class="item.isIncome ? 'text-[#2E85FF]' : 'text-[#FF4D4F]'">
{{ item.isIncome ? '+' : '' }}{{ item.amount }}
</div>
</div>
<!-- 分割线 -->
<div v-if="item.id !== list[list.length - 1].id" class="mt-3 border-t border-gray-50 border-dashed"></div>
</div>
</van-list>
</van-pull-refresh>
</div>
<!-- 日期选择弹窗 -->
<van-popup v-model:show="showStartDatePicker" position="bottom">
<van-date-picker v-model="currentStartDate" title="选择开始日期" :min-date="minDate" :max-date="maxDate"
@confirm="onConfirmStartDate" @cancel="showStartDatePicker = false" />
</van-popup>
<van-popup v-model:show="showEndDatePicker" position="bottom">
<van-date-picker v-model="currentEndDate" title="选择结束日期" :min-date="minDate" :max-date="maxDate"
@confirm="onConfirmEndDate" @cancel="showEndDatePicker = false" />
</van-popup>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import { showToast } from 'vant'
import dayjs from 'dayjs'
// 导入图片
import headerBg from '@/assets/images/recall/962@2x.png'
import bottleImg from '@/assets/images/recall/pz02@2x.png'
import indicatorImg from '@/assets/images/recall/xian@2x.png'
useTitle('我的积分')
const route = useRoute()
const router = useRouter()
// 状态
const tabs = ['全部', '已获得', '已消耗']
const activeTab = ref(0)
const searchKeyword = ref('')
const startDate = ref('')
const endDate = ref('')
const showStartDatePicker = ref(false)
const showEndDatePicker = ref(false)
const now = new Date()
const minDate = new Date(2020, 0, 1)
const maxDate = new Date(2030, 11, 31)
const currentStartDate = ref([now.getFullYear().toString(), (now.getMonth() + 1).toString().padStart(2, '0'), now.getDate().toString().padStart(2, '0')])
const currentEndDate = ref([now.getFullYear().toString(), (now.getMonth() + 1).toString().padStart(2, '0'), now.getDate().toString().padStart(2, '0')])
// 列表相关
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);
// 切换Tab
const handleTabChange = (index) => {
activeTab.value = index
onRefresh()
}
// 日期确认
const onConfirmStartDate = ({ selectedValues }) => {
const dateStr = selectedValues.join('-')
if (endDate.value && dayjs(dateStr).isAfter(dayjs(endDate.value))) {
showToast('开始日期不能晚于结束日期')
return
}
startDate.value = dateStr
showStartDatePicker.value = false
onRefresh()
}
const onConfirmEndDate = ({ selectedValues }) => {
const dateStr = selectedValues.join('-')
if (startDate.value && dayjs(dateStr).isBefore(dayjs(startDate.value))) {
showToast('结束日期不能早于开始日期')
return
}
endDate.value = dateStr
showEndDatePicker.value = false
onRefresh()
}
// 模拟数据
const mockData = [
{
id: 1,
title: '2025.11月3日-10日江苏东台养生营,邀您一起进入童话世界!',
date: '2025-11-03',
type: '活动参与奖励',
amount: 6400,
isIncome: true
},
{
id: 2,
title: '【自然的恩典】青少年成长营-贵阳百花湖3(小学初中专场)',
date: '2025-10-03',
type: '活动参与奖励',
amount: 7998,
isIncome: true
},
{
id: 3,
title: '2024年4月22-25日浙江义乌【中华智慧商业应用论坛】',
date: '2024-04-20',
type: '活动参与奖励',
amount: 3200,
isIncome: true
},
{
id: 4,
title: '2023.7.6-7.11【自然的恩典】“爱我中华”优秀传统文化夏令营-天津场',
date: '2023-07-01',
type: '活动参与奖励',
amount: 3990,
isIncome: true
},
{
id: 5,
title: '兑换活动优惠券',
date: '2023-07-01',
type: '兑换奖励',
amount: 200,
isIncome: false
}
]
// 加载列表
const onLoad = () => {
setTimeout(() => {
if (refreshing.value) {
list.value = []
refreshing.value = false
}
// 模拟数据加载
const newData = mockData.map(item => ({ ...item, id: item.id + list.value.length }))
list.value.push(...newData)
loading.value = false
// 模拟数据加载完毕
if (list.value.length >= 20) {
finished.value = true
}
}, 1000)
}
const onRefresh = () => {
finished.value = false
loading.value = true
onLoad()
}
</script>
<style lang="less" scoped>
.animate-float {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
:deep(.van-picker__toolbar) {
border-bottom: 1px solid #f5f5f5;
}
</style>
......