feat(积分页面): 重构积分页面并添加下拉刷新功能
添加新的图片资源并重构积分页面,实现完整的UI设计和交互功能 - 新增积分列表展示、筛选和搜索功能 - 实现日期选择器和Tab切换功能 - 添加下拉刷新和滚动加载更多功能 - 使用Vant组件优化用户体验
Showing
7 changed files
with
258 additions
and
20 deletions
src/assets/images/recall/962@2x.png
0 → 100644
155 KB
src/assets/images/recall/jx@2x.png
0 → 100644
1.82 KB
src/assets/images/recall/pz02@2x.png
0 → 100644
51.9 KB
src/assets/images/recall/xian@2x.png
0 → 100644
793 Bytes
| ... | @@ -78,6 +78,7 @@ declare module 'vue' { | ... | @@ -78,6 +78,7 @@ declare module 'vue' { |
| 78 | VanPickerGroup: typeof import('vant/es')['PickerGroup'] | 78 | VanPickerGroup: typeof import('vant/es')['PickerGroup'] |
| 79 | VanPopup: typeof import('vant/es')['Popup'] | 79 | VanPopup: typeof import('vant/es')['Popup'] |
| 80 | VanProgress: typeof import('vant/es')['Progress'] | 80 | VanProgress: typeof import('vant/es')['Progress'] |
| 81 | + VanPullRefresh: typeof import('vant/es')['PullRefresh'] | ||
| 81 | VanRate: typeof import('vant/es')['Rate'] | 82 | VanRate: typeof import('vant/es')['Rate'] |
| 82 | VanRow: typeof import('vant/es')['Row'] | 83 | VanRow: typeof import('vant/es')['Row'] |
| 83 | VanSearch: typeof import('vant/es')['Search'] | 84 | VanSearch: typeof import('vant/es')['Search'] | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-03-20 20:36:36 | 2 | * @Date: 2025-03-20 20:36:36 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-24 09:36:29 | 4 | + * @LastEditTime: 2025-12-24 10:58:25 |
| 5 | * @FilePath: /mlaj/src/router/routes.js | 5 | * @FilePath: /mlaj/src/router/routes.js |
| 6 | * @Description: 路由地址映射配置 | 6 | * @Description: 路由地址映射配置 |
| 7 | */ | 7 | */ |
| ... | @@ -97,12 +97,6 @@ export const routes = [ | ... | @@ -97,12 +97,6 @@ export const routes = [ |
| 97 | meta: { title: '我的活动' }, | 97 | meta: { title: '我的活动' }, |
| 98 | }, | 98 | }, |
| 99 | { | 99 | { |
| 100 | - path: '/profile/points', | ||
| 101 | - name: 'Points', | ||
| 102 | - component: () => import('../views/profile/pointsPage.vue'), | ||
| 103 | - meta: { title: '我的积分' }, | ||
| 104 | - }, | ||
| 105 | - { | ||
| 106 | path: '/recall/login', | 100 | path: '/recall/login', |
| 107 | name: 'RecallLogin', | 101 | name: 'RecallLogin', |
| 108 | component: () => import('../views/recall/login.vue'), | 102 | component: () => import('../views/recall/login.vue'), |
| ... | @@ -242,6 +236,12 @@ export const routes = [ | ... | @@ -242,6 +236,12 @@ export const routes = [ |
| 242 | meta: { title: '学习记录' }, | 236 | meta: { title: '学习记录' }, |
| 243 | }, | 237 | }, |
| 244 | { | 238 | { |
| 239 | + path: '/profile/points', | ||
| 240 | + name: 'Points', | ||
| 241 | + component: () => import('../views/profile/pointsPage.vue'), | ||
| 242 | + meta: { title: '我的积分' }, | ||
| 243 | + }, | ||
| 244 | + { | ||
| 245 | path: '/test', | 245 | path: '/test', |
| 246 | name: 'test', | 246 | name: 'test', |
| 247 | component: () => import('../views/test.vue'), | 247 | component: () => import('../views/test.vue'), | ... | ... |
| 1 | -<!-- | ||
| 2 | - * @Date: 2025-12-23 17:53:26 | ||
| 3 | - * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | - * @LastEditTime: 2025-12-23 17:53:56 | ||
| 5 | - * @FilePath: /mlaj/src/views/profile/pointsPage.vue | ||
| 6 | - * @Description: 我的积分页面 | ||
| 7 | ---> | ||
| 8 | <template> | 1 | <template> |
| 9 | - <div class="points-page"> | 2 | + <div class="points-page min-h-screen bg-gray-50 flex flex-col"> |
| 10 | - 我的积分 | 3 | + <!-- 头部区域 --> |
| 4 | + <div class="header relative w-full h-48 bg-cover bg-center" :style="{ backgroundImage: `url(${headerBg})` }"> | ||
| 5 | + <div class="absolute top-12 left-6"> | ||
| 6 | + <div class="text-white text-sm opacity-90 mb-1">当前星球币</div> | ||
| 7 | + <div class="text-[#FFDD01] text-4xl font-bold tracking-wider">15,800</div> | ||
| 8 | + </div> | ||
| 9 | + | ||
| 10 | + <!-- 右侧瓶子 --> | ||
| 11 | + <img :src="bottleImg" class="absolute right-4 top-4 w-32 object-contain animate-float" alt="bottle" /> | ||
| 12 | + | ||
| 13 | + <!-- 底部提示条 --> | ||
| 14 | + <div class="absolute bottom-0 left-0 w-full h-8 bg-white/20 flex items-center px-4"> | ||
| 15 | + <span class="text-white text-xs scale-90 origin-left">可用于兑换活动优惠券或实物奖励,敬请期待!</span> | ||
| 16 | + </div> | ||
| 17 | + </div> | ||
| 18 | + | ||
| 19 | + <!-- Tab 标签页 --> | ||
| 20 | + <div class="bg-white pt-2 sticky top-0 z-10 shadow-sm"> | ||
| 21 | + <div class="grid grid-cols-3 items-center border-gray-100 pb-0 relative"> | ||
| 22 | + <div v-for="(tab, index) in tabs" :key="index" | ||
| 23 | + class="relative py-3 flex flex-col items-center cursor-pointer z-10" @click="handleTabChange(index)"> | ||
| 24 | + <span class="text-base font-medium transition-colors duration-300" | ||
| 25 | + :class="activeTab === index ? 'text-gray-900 font-bold' : 'text-gray-400'"> | ||
| 26 | + {{ tab }} | ||
| 27 | + </span> | ||
| 28 | + </div> | ||
| 29 | + <!-- 移动指示器 --> | ||
| 30 | + <div class="absolute bottom-0 h-1.5 w-1/3 transition-transform duration-300 ease-in-out z-10" | ||
| 31 | + :style="{ transform: `translateX(${activeTab * 100}%)` }"> | ||
| 32 | + <div class="w-full h-full flex justify-center"> | ||
| 33 | + <img :src="indicatorImg" class="w-8 h-1.5" alt="indicator" /> | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + </div> | ||
| 37 | + | ||
| 38 | + <!-- 筛选区域 --> | ||
| 39 | + <div class="p-3 bg-white space-y-3"> | ||
| 40 | + <!-- 日期选择 --> | ||
| 41 | + <div class="flex items-center justify-between space-x-2"> | ||
| 42 | + <div class="flex-1 bg-white border border-gray-200 rounded-lg h-9 flex items-center px-3 cursor-pointer" | ||
| 43 | + @click="showStartDatePicker = true"> | ||
| 44 | + <van-icon name="calendar-o" class="text-gray-400 mr-2" /> | ||
| 45 | + <span :class="startDate ? 'text-gray-800' : 'text-gray-400'" class="text-sm truncate"> | ||
| 46 | + {{ startDate || '开始日期' }} | ||
| 47 | + </span> | ||
| 48 | + </div> | ||
| 49 | + <span class="text-gray-400 text-sm">至</span> | ||
| 50 | + <div class="flex-1 bg-white border border-gray-200 rounded-lg h-9 flex items-center px-3 cursor-pointer" | ||
| 51 | + @click="showEndDatePicker = true"> | ||
| 52 | + <van-icon name="calendar-o" class="text-gray-400 mr-2" /> | ||
| 53 | + <span :class="endDate ? 'text-gray-800' : 'text-gray-400'" class="text-sm truncate"> | ||
| 54 | + {{ endDate || '结束日期' }} | ||
| 55 | + </span> | ||
| 56 | + </div> | ||
| 57 | + </div> | ||
| 58 | + | ||
| 59 | + <!-- 搜索框 --> | ||
| 60 | + <div class="bg-white border border-gray-200 rounded-lg h-9 flex items-center px-3"> | ||
| 61 | + <van-icon name="search" class="text-gray-400 mr-2" size="16" /> | ||
| 62 | + <input v-model="searchKeyword" type="text" placeholder="搜索活动或课程名称" | ||
| 63 | + class="flex-1 bg-transparent text-sm text-gray-800 placeholder-gray-400 outline-none" | ||
| 64 | + @keyup.enter="onRefresh" /> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | + </div> | ||
| 68 | + | ||
| 69 | + <!-- 列表区域 --> | ||
| 70 | + <div class="flex-1 overflow-y-auto p-3"> | ||
| 71 | + <van-pull-refresh v-model="refreshing" @refresh="onRefresh"> | ||
| 72 | + <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> | ||
| 73 | + <div v-for="item in list" :key="item.id" class="bg-white rounded-xl p-4 mb-3 shadow-sm"> | ||
| 74 | + <div class="text-gray-900 text-sm font-medium leading-relaxed mb-3 line-clamp-2"> | ||
| 75 | + {{ item.title }} | ||
| 76 | + </div> | ||
| 77 | + <div class="flex justify-between items-center"> | ||
| 78 | + <div class="flex flex-col"> | ||
| 79 | + <span class="text-gray-400 text-xs mb-1">{{ item.date }}</span> | ||
| 80 | + <span class="text-gray-400 text-xs">{{ item.type }}</span> | ||
| 81 | + </div> | ||
| 82 | + <div class="text-lg font-bold" :class="item.isIncome ? 'text-[#2E85FF]' : 'text-[#FF4D4F]'"> | ||
| 83 | + {{ item.isIncome ? '+' : '' }}{{ item.amount }} | ||
| 84 | + </div> | ||
| 85 | + </div> | ||
| 86 | + <!-- 分割线 --> | ||
| 87 | + <div v-if="item.id !== list[list.length - 1].id" class="mt-3 border-t border-gray-50 border-dashed"></div> | ||
| 88 | + </div> | ||
| 89 | + </van-list> | ||
| 90 | + </van-pull-refresh> | ||
| 91 | + </div> | ||
| 92 | + | ||
| 93 | + <!-- 日期选择弹窗 --> | ||
| 94 | + <van-popup v-model:show="showStartDatePicker" position="bottom"> | ||
| 95 | + <van-date-picker v-model="currentStartDate" title="选择开始日期" :min-date="minDate" :max-date="maxDate" | ||
| 96 | + @confirm="onConfirmStartDate" @cancel="showStartDatePicker = false" /> | ||
| 97 | + </van-popup> | ||
| 98 | + | ||
| 99 | + <van-popup v-model:show="showEndDatePicker" position="bottom"> | ||
| 100 | + <van-date-picker v-model="currentEndDate" title="选择结束日期" :min-date="minDate" :max-date="maxDate" | ||
| 101 | + @confirm="onConfirmEndDate" @cancel="showEndDatePicker = false" /> | ||
| 102 | + </van-popup> | ||
| 11 | </div> | 103 | </div> |
| 12 | </template> | 104 | </template> |
| 13 | 105 | ||
| 14 | <script setup> | 106 | <script setup> |
| 15 | -import { ref } from 'vue' | 107 | +import { ref, computed } from 'vue' |
| 16 | import { useRoute, useRouter } from 'vue-router' | 108 | import { useRoute, useRouter } from 'vue-router' |
| 17 | import { useTitle } from '@vueuse/core' | 109 | import { useTitle } from '@vueuse/core' |
| 110 | +import { showToast } from 'vant' | ||
| 111 | +import dayjs from 'dayjs' | ||
| 112 | + | ||
| 113 | +// 导入图片 | ||
| 114 | +import headerBg from '@/assets/images/recall/962@2x.png' | ||
| 115 | +import bottleImg from '@/assets/images/recall/pz02@2x.png' | ||
| 116 | +import indicatorImg from '@/assets/images/recall/xian@2x.png' | ||
| 117 | + | ||
| 118 | +useTitle('我的积分') | ||
| 119 | +const route = useRoute() | ||
| 120 | +const router = useRouter() | ||
| 121 | + | ||
| 122 | +// 状态 | ||
| 123 | +const tabs = ['全部', '已获得', '已消耗'] | ||
| 124 | +const activeTab = ref(0) | ||
| 125 | +const searchKeyword = ref('') | ||
| 126 | +const startDate = ref('') | ||
| 127 | +const endDate = ref('') | ||
| 128 | +const showStartDatePicker = ref(false) | ||
| 129 | +const showEndDatePicker = ref(false) | ||
| 130 | + | ||
| 131 | +const now = new Date() | ||
| 132 | +const minDate = new Date(2020, 0, 1) | ||
| 133 | +const maxDate = new Date(2030, 11, 31) | ||
| 134 | +const currentStartDate = ref([now.getFullYear().toString(), (now.getMonth() + 1).toString().padStart(2, '0'), now.getDate().toString().padStart(2, '0')]) | ||
| 135 | +const currentEndDate = ref([now.getFullYear().toString(), (now.getMonth() + 1).toString().padStart(2, '0'), now.getDate().toString().padStart(2, '0')]) | ||
| 18 | 136 | ||
| 19 | -const $route = useRoute(); | 137 | +// 列表相关 |
| 20 | -const $router = useRouter(); | 138 | +const list = ref([]) |
| 21 | -useTitle($route.meta.title); | 139 | +const loading = ref(false) |
| 140 | +const finished = ref(false) | ||
| 141 | +const refreshing = ref(false) | ||
| 22 | 142 | ||
| 143 | +// 切换Tab | ||
| 144 | +const handleTabChange = (index) => { | ||
| 145 | + activeTab.value = index | ||
| 146 | + onRefresh() | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +// 日期确认 | ||
| 150 | +const onConfirmStartDate = ({ selectedValues }) => { | ||
| 151 | + const dateStr = selectedValues.join('-') | ||
| 152 | + if (endDate.value && dayjs(dateStr).isAfter(dayjs(endDate.value))) { | ||
| 153 | + showToast('开始日期不能晚于结束日期') | ||
| 154 | + return | ||
| 155 | + } | ||
| 156 | + startDate.value = dateStr | ||
| 157 | + showStartDatePicker.value = false | ||
| 158 | + onRefresh() | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +const onConfirmEndDate = ({ selectedValues }) => { | ||
| 162 | + const dateStr = selectedValues.join('-') | ||
| 163 | + if (startDate.value && dayjs(dateStr).isBefore(dayjs(startDate.value))) { | ||
| 164 | + showToast('结束日期不能早于开始日期') | ||
| 165 | + return | ||
| 166 | + } | ||
| 167 | + endDate.value = dateStr | ||
| 168 | + showEndDatePicker.value = false | ||
| 169 | + onRefresh() | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +// 模拟数据 | ||
| 173 | +const mockData = [ | ||
| 174 | + { | ||
| 175 | + id: 1, | ||
| 176 | + title: '2025.11月3日-10日江苏东台养生营,邀您一起进入童话世界!', | ||
| 177 | + date: '2025-11-03', | ||
| 178 | + type: '活动参与奖励', | ||
| 179 | + amount: 6400, | ||
| 180 | + isIncome: true | ||
| 181 | + }, | ||
| 182 | + { | ||
| 183 | + id: 2, | ||
| 184 | + title: '【自然的恩典】青少年成长营-贵阳百花湖3(小学初中专场)', | ||
| 185 | + date: '2025-10-03', | ||
| 186 | + type: '活动参与奖励', | ||
| 187 | + amount: 7998, | ||
| 188 | + isIncome: true | ||
| 189 | + }, | ||
| 190 | + { | ||
| 191 | + id: 3, | ||
| 192 | + title: '2024年4月22-25日浙江义乌【中华智慧商业应用论坛】', | ||
| 193 | + date: '2024-04-20', | ||
| 194 | + type: '活动参与奖励', | ||
| 195 | + amount: 3200, | ||
| 196 | + isIncome: true | ||
| 197 | + }, | ||
| 198 | + { | ||
| 199 | + id: 4, | ||
| 200 | + title: '2023.7.6-7.11【自然的恩典】“爱我中华”优秀传统文化夏令营-天津场', | ||
| 201 | + date: '2023-07-01', | ||
| 202 | + type: '活动参与奖励', | ||
| 203 | + amount: 3990, | ||
| 204 | + isIncome: true | ||
| 205 | + }, | ||
| 206 | + { | ||
| 207 | + id: 5, | ||
| 208 | + title: '兑换活动优惠券', | ||
| 209 | + date: '2023-07-01', | ||
| 210 | + type: '兑换奖励', | ||
| 211 | + amount: 200, | ||
| 212 | + isIncome: false | ||
| 213 | + } | ||
| 214 | +] | ||
| 215 | + | ||
| 216 | +// 加载列表 | ||
| 217 | +const onLoad = () => { | ||
| 218 | + setTimeout(() => { | ||
| 219 | + if (refreshing.value) { | ||
| 220 | + list.value = [] | ||
| 221 | + refreshing.value = false | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + // 模拟数据加载 | ||
| 225 | + const newData = mockData.map(item => ({ ...item, id: item.id + list.value.length })) | ||
| 226 | + list.value.push(...newData) | ||
| 227 | + | ||
| 228 | + loading.value = false | ||
| 229 | + | ||
| 230 | + // 模拟数据加载完毕 | ||
| 231 | + if (list.value.length >= 20) { | ||
| 232 | + finished.value = true | ||
| 233 | + } | ||
| 234 | + }, 1000) | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +const onRefresh = () => { | ||
| 238 | + finished.value = false | ||
| 239 | + loading.value = true | ||
| 240 | + onLoad() | ||
| 241 | +} | ||
| 23 | </script> | 242 | </script> |
| 24 | 243 | ||
| 25 | <style lang="less" scoped> | 244 | <style lang="less" scoped> |
| 245 | +.animate-float { | ||
| 246 | + animation: float 3s ease-in-out infinite; | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +@keyframes float { | ||
| 250 | + | ||
| 251 | + 0%, | ||
| 252 | + 100% { | ||
| 253 | + transform: translateY(0); | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + 50% { | ||
| 257 | + transform: translateY(-5px); | ||
| 258 | + } | ||
| 259 | +} | ||
| 26 | 260 | ||
| 261 | +:deep(.van-picker__toolbar) { | ||
| 262 | + border-bottom: 1px solid #f5f5f5; | ||
| 263 | +} | ||
| 27 | </style> | 264 | </style> | ... | ... |
-
Please register or login to post a comment