hookehuyr

feat(积分): 实现积分列表接口对接及页面展示

- 新增积分相关API接口文件及获取积分列表方法
- 修改积分页面,移除模拟数据,对接真实接口
- 添加积分余额显示及列表数据动态加载功能
- 实现分页加载、下拉刷新和不同积分类型筛选
1 +/*
2 + * @Date: 2025-12-24 12:26:27
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-12-24 12:37:23
5 + * @FilePath: /mlaj/src/api/points.js
6 + * @Description: 积分相关接口
7 + */
8 +import { fn, fetch } from './fn';
9 +
10 +const Api = {
11 + POINTS_LIST: '/srv/?a=points&t=list',
12 +}
13 +
14 +/**
15 + * @description 获取积分列表
16 + * @param {*} direction add=已获得,subtract=已消耗
17 + * @param {*} begin_date 开始日期
18 + * @param {*} end_date 结束日期
19 + * @param {*} keyword 搜索关键词
20 + * @param {*} page 页码
21 + * @param {*} limit 每页数量
22 + * @returns data: {
23 + * balance: 积分余额
24 + * point_list: [{ id, event_title 标题, event_time 时间, change 积分变化, event_type_desc 事件类型 }]
25 + * }
26 + */
27 +export const getPointsListAPI = (params) => fn(fetch.get(Api.POINTS_LIST, params));
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 <div class="header relative w-full h-48 bg-cover bg-center" :style="{ backgroundImage: `url(${headerBg})` }"> 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"> 5 <div class="absolute top-12 left-6">
6 <div class="text-white text-sm opacity-90 mb-1">当前星球币</div> 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> 7 + <div class="text-[#FFDD01] text-4xl font-bold tracking-wider">{{ balance }}</div>
8 </div> 8 </div>
9 9
10 <!-- 右侧瓶子 --> 10 <!-- 右侧瓶子 -->
...@@ -72,15 +72,16 @@ ...@@ -72,15 +72,16 @@
72 <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> 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"> 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"> 74 <div class="text-gray-900 text-sm font-medium leading-relaxed mb-3 line-clamp-2">
75 - {{ item.title }} 75 + {{ item.event_title }}
76 </div> 76 </div>
77 <div class="flex justify-between items-center"> 77 <div class="flex justify-between items-center">
78 <div class="flex flex-col"> 78 <div class="flex flex-col">
79 - <span class="text-gray-400 text-xs mb-1">{{ item.date }}</span> 79 + <span class="text-gray-400 text-xs mb-1">{{ item.event_time }}</span>
80 - <span class="text-gray-400 text-xs">{{ item.type }}</span> 80 + <span class="text-gray-400 text-xs">{{ item.event_type_desc }}</span>
81 </div> 81 </div>
82 - <div class="text-lg font-bold" :class="item.isIncome ? 'text-[#2E85FF]' : 'text-[#FF4D4F]'"> 82 + <div class="text-lg font-bold"
83 - {{ item.isIncome ? '+' : '' }}{{ item.amount }} 83 + :class="String(item.change).includes('-') ? 'text-[#FF4D4F]' : 'text-[#2E85FF]'">
84 + {{ item.change }}
84 </div> 85 </div>
85 </div> 86 </div>
86 <!-- 分割线 --> 87 <!-- 分割线 -->
...@@ -104,11 +105,12 @@ ...@@ -104,11 +105,12 @@
104 </template> 105 </template>
105 106
106 <script setup> 107 <script setup>
107 -import { ref, computed } from 'vue' 108 +import { ref } from 'vue'
108 import { useRoute, useRouter } from 'vue-router' 109 import { useRoute, useRouter } from 'vue-router'
109 import { useTitle } from '@vueuse/core' 110 import { useTitle } from '@vueuse/core'
110 import { showToast } from 'vant' 111 import { showToast } from 'vant'
111 import dayjs from 'dayjs' 112 import dayjs from 'dayjs'
113 +import { getPointsListAPI } from '@/api/points'
112 114
113 // 导入图片 115 // 导入图片
114 const headerBg = 'https://cdn.ipadbiz.cn/mlaj/recall/img/962@2x.png' 116 const headerBg = 'https://cdn.ipadbiz.cn/mlaj/recall/img/962@2x.png'
...@@ -139,6 +141,9 @@ const list = ref([]) ...@@ -139,6 +141,9 @@ const list = ref([])
139 const loading = ref(false) 141 const loading = ref(false)
140 const finished = ref(false) 142 const finished = ref(false)
141 const refreshing = ref(false) 143 const refreshing = ref(false)
144 +const page = ref(0)
145 +const limit = ref(10)
146 +const balance = ref('0')
142 147
143 // 切换Tab 148 // 切换Tab
144 const handleTabChange = (index) => { 149 const handleTabChange = (index) => {
...@@ -169,74 +174,61 @@ const onConfirmEndDate = ({ selectedValues }) => { ...@@ -169,74 +174,61 @@ const onConfirmEndDate = ({ selectedValues }) => {
169 onRefresh() 174 onRefresh()
170 } 175 }
171 176
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 // 加载列表 177 // 加载列表
217 -const onLoad = () => { 178 +const onLoad = async () => {
218 - setTimeout(() => { 179 + const nextPage = page.value + 1
180 +
181 + let direction = ''
182 + if (activeTab.value === 1) direction = 'add'
183 + if (activeTab.value === 2) direction = 'subtract'
184 +
185 + try {
186 + const res = await getPointsListAPI({
187 + direction,
188 + begin_date: startDate.value,
189 + end_date: endDate.value,
190 + keyword: searchKeyword.value,
191 + page: nextPage,
192 + limit: limit.value
193 + })
194 +
219 if (refreshing.value) { 195 if (refreshing.value) {
220 list.value = [] 196 list.value = []
221 refreshing.value = false 197 refreshing.value = false
222 } 198 }
223 199
224 - // 模拟数据加载 200 + if (res && res.data) {
225 - const newData = mockData.map(item => ({ ...item, id: item.id + list.value.length })) 201 + if (res.data.balance !== undefined) {
226 - list.value.push(...newData) 202 + balance.value = res.data.balance
203 + }
204 +
205 + const newItems = res.data.point_list || []
206 + list.value.push(...newItems)
227 207
228 - loading.value = false 208 + page.value = nextPage
229 209
230 - // 模拟数据加载完毕 210 + if (newItems.length < limit.value) {
231 - if (list.value.length >= 20) {
232 finished.value = true 211 finished.value = true
233 } 212 }
234 - }, 1000) 213 + } else {
214 + finished.value = true
215 + }
216 + } catch (error) {
217 + console.error('Failed to load points:', error)
218 + finished.value = true
219 + if (refreshing.value) {
220 + refreshing.value = false
221 + }
222 + } finally {
223 + loading.value = false
224 + }
235 } 225 }
236 226
237 const onRefresh = () => { 227 const onRefresh = () => {
238 finished.value = false 228 finished.value = false
239 loading.value = true 229 loading.value = true
230 + page.value = 0
231 + refreshing.value = true
240 onLoad() 232 onLoad()
241 } 233 }
242 </script> 234 </script>
......