feat(积分): 实现积分详情页与API对接
添加积分列表API接口并实现积分详情页数据展示 移除EditFamily页面的模拟数据,使用真实数据 在Profile页面根据用户角色动态显示菜单项
Showing
4 changed files
with
145 additions
and
58 deletions
| ... | @@ -10,6 +10,7 @@ import { fn, fetch } from './fn'; | ... | @@ -10,6 +10,7 @@ import { fn, fetch } from './fn'; |
| 10 | const Api = { | 10 | const Api = { |
| 11 | SYNC_WX_STEP: '/srv/?a=point&t=sync_wx_step', | 11 | SYNC_WX_STEP: '/srv/?a=point&t=sync_wx_step', |
| 12 | COLLECT_POINT: '/srv/?a=point&t=collect', | 12 | COLLECT_POINT: '/srv/?a=point&t=collect', |
| 13 | + POINT_LIST: '/srv/?f=walk&a=point&t=list', | ||
| 13 | } | 14 | } |
| 14 | 15 | ||
| 15 | /** | 16 | /** |
| ... | @@ -31,3 +32,23 @@ export const syncWxStepAPI = (params) => fn(fetch.post(Api.SYNC_WX_STEP, params) | ... | @@ -31,3 +32,23 @@ export const syncWxStepAPI = (params) => fn(fetch.post(Api.SYNC_WX_STEP, params) |
| 31 | */ | 32 | */ |
| 32 | 33 | ||
| 33 | export const collectPointAPI = (params) => fn(fetch.post(Api.COLLECT_POINT, params)); | 34 | export const collectPointAPI = (params) => fn(fetch.post(Api.COLLECT_POINT, params)); |
| 35 | + | ||
| 36 | +/** | ||
| 37 | + * @description: 查询积分列表 | ||
| 38 | + * @param {Object} params - 请求参数 | ||
| 39 | + * @param {string} [params.page] - 页码,从0开始,默认为0 | ||
| 40 | + * @param {string} [params.limit] - 每页数量,默认为10 | ||
| 41 | + * @returns {Object} response - 响应对象 | ||
| 42 | + * @returns {number} response.code - 响应状态码 | ||
| 43 | + * @returns {string} response.msg - 响应消息 | ||
| 44 | + * @returns {Object} response.data - 响应数据 | ||
| 45 | + * @returns {number} response.data.total_points - 我的积分 | ||
| 46 | + * @returns {Array} response.data.logs - 积分日志列表 | ||
| 47 | + * @returns {number} response.data.logs[].id - 积分日志ID | ||
| 48 | + * @returns {string} response.data.logs[].points_change - 积分变化 | ||
| 49 | + * @returns {string} response.data.logs[].balance_after - 最新的家庭积分 | ||
| 50 | + * @returns {string} response.data.logs[].log_type - 日志类型 | ||
| 51 | + * @returns {string} response.data.logs[].source_type - 积分来源类型 | ||
| 52 | + * @returns {string} response.data.logs[].note - 备注 | ||
| 53 | + */ | ||
| 54 | +export const getPointListAPI = (params) => fn(fetch.get(Api.POINT_LIST, params)); | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-08-27 17:44:53 | 2 | * @Date: 2025-08-27 17:44:53 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-09-03 11:23:08 | 4 | + * @LastEditTime: 2025-09-03 20:31:29 |
| 5 | * @FilePath: /lls_program/src/pages/EditFamily/index.vue | 5 | * @FilePath: /lls_program/src/pages/EditFamily/index.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -223,21 +223,13 @@ onMounted(async () => { | ... | @@ -223,21 +223,13 @@ onMounted(async () => { |
| 223 | if (code) { | 223 | if (code) { |
| 224 | familyName.value = data.name; | 224 | familyName.value = data.name; |
| 225 | familyIntro.value = data.note; | 225 | familyIntro.value = data.note; |
| 226 | - districtValue.value = [data.county]; | 226 | + districtValue.value = [+data.county]; |
| 227 | - selectedDistrict.value = data.county; | 227 | + selectedDistrict.value = +data.county; |
| 228 | - selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value).text; | 228 | + selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value)?.text; |
| 229 | - familyMotto.value = data.passphrase.split(','); | 229 | + // |
| 230 | + familyMotto.value = data.passphrase.split(''); | ||
| 230 | familyAvatar.value = data.avatar_url; | 231 | familyAvatar.value = data.avatar_url; |
| 231 | } | 232 | } |
| 232 | - // Mock data for current family information | ||
| 233 | - familyName.value = '幸福一家'; | ||
| 234 | - familyIntro.value = '我们是相亲相爱的一家人'; | ||
| 235 | - districtValue.value = [310104]; // 模拟勾选项已选择徐汇区 | ||
| 236 | - // 需要提交的数据集 | ||
| 237 | - selectedDistrict.value = 310104; // 模拟已选择徐汇区 | ||
| 238 | - selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value).text; | ||
| 239 | - familyMotto.value = ['家', '和', '万', '事']; | ||
| 240 | - familyAvatar.value = 'https://img.yzcdn.cn/vant/cat.jpeg'; | ||
| 241 | }); | 233 | }); |
| 242 | 234 | ||
| 243 | /** | 235 | /** | ... | ... |
| ... | @@ -7,7 +7,7 @@ | ... | @@ -7,7 +7,7 @@ |
| 7 | <view class="relative z-10 flex-1 pb-20"> | 7 | <view class="relative z-10 flex-1 pb-20"> |
| 8 | <!-- Points display --> | 8 | <!-- Points display --> |
| 9 | <view class="pt-4 pb-6 flex flex-col items-center"> | 9 | <view class="pt-4 pb-6 flex flex-col items-center"> |
| 10 | - <h2 class="text-4xl font-bold text-white mb-1">2580</h2> | 10 | + <h2 class="text-4xl font-bold text-white mb-1">{{ totalPoints }}</h2> |
| 11 | <p class="text-white text-opacity-80">当前可用积分</p> | 11 | <p class="text-white text-opacity-80">当前可用积分</p> |
| 12 | </view> | 12 | </view> |
| 13 | <!-- Points strategy section --> | 13 | <!-- Points strategy section --> |
| ... | @@ -71,6 +71,16 @@ | ... | @@ -71,6 +71,16 @@ |
| 71 | </view> | 71 | </view> |
| 72 | <!-- Points history list --> | 72 | <!-- Points history list --> |
| 73 | <view class="pt-4"> | 73 | <view class="pt-4"> |
| 74 | + <!-- Loading state --> | ||
| 75 | + <view v-if="loading && pointsHistory.length === 0" class="py-8 text-center text-gray-500"> | ||
| 76 | + 加载中... | ||
| 77 | + </view> | ||
| 78 | + <!-- Empty state --> | ||
| 79 | + <view v-else-if="!loading && filteredPoints.length === 0" class="py-8 text-center text-gray-500"> | ||
| 80 | + 暂无积分记录 | ||
| 81 | + </view> | ||
| 82 | + <!-- Points list --> | ||
| 83 | + <view v-else> | ||
| 74 | <view v-for="item in filteredPoints" :key="item.id" class="py-4 border-b border-gray-100 flex justify-between"> | 84 | <view v-for="item in filteredPoints" :key="item.id" class="py-4 border-b border-gray-100 flex justify-between"> |
| 75 | <view> | 85 | <view> |
| 76 | <h4 class="font-medium">{{ item.title }}</h4> | 86 | <h4 class="font-medium">{{ item.title }}</h4> |
| ... | @@ -84,56 +94,101 @@ | ... | @@ -84,56 +94,101 @@ |
| 84 | </view> | 94 | </view> |
| 85 | </view> | 95 | </view> |
| 86 | </view> | 96 | </view> |
| 97 | + </view> | ||
| 87 | <!-- <BottomNav /> --> | 98 | <!-- <BottomNav /> --> |
| 88 | </view> | 99 | </view> |
| 89 | </template> | 100 | </template> |
| 90 | 101 | ||
| 91 | <script setup> | 102 | <script setup> |
| 92 | -import { ref, computed } from 'vue'; | 103 | +import { ref, computed, onMounted } from 'vue'; |
| 93 | -import Taro from '@tarojs/taro'; | 104 | +import Taro, { useDidShow } from '@tarojs/taro'; |
| 94 | import AppHeader from '../../components/AppHeader.vue'; | 105 | import AppHeader from '../../components/AppHeader.vue'; |
| 95 | import BottomNav from '../../components/BottomNav.vue'; | 106 | import BottomNav from '../../components/BottomNav.vue'; |
| 96 | import { Right, My } from '@nutui/icons-vue-taro'; | 107 | import { Right, My } from '@nutui/icons-vue-taro'; |
| 108 | +import { getPointListAPI } from '../../api/points'; | ||
| 97 | 109 | ||
| 98 | const activeTab = ref('all'); | 110 | const activeTab = ref('all'); |
| 111 | +const pointsHistory = ref([]); | ||
| 112 | +const totalPoints = ref(0); | ||
| 113 | +const loading = ref(false); | ||
| 114 | +const page = ref(0); | ||
| 115 | +const limit = ref(20); | ||
| 116 | +const hasMore = ref(true); | ||
| 117 | + | ||
| 118 | +/** | ||
| 119 | + * 获取积分列表数据 | ||
| 120 | + */ | ||
| 121 | +const fetchPointsList = async (isRefresh = false) => { | ||
| 122 | + if (loading.value) return; | ||
| 123 | + | ||
| 124 | + try { | ||
| 125 | + loading.value = true; | ||
| 126 | + | ||
| 127 | + if (isRefresh) { | ||
| 128 | + page.value = 0; | ||
| 129 | + pointsHistory.value = []; | ||
| 130 | + hasMore.value = true; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + const params = { | ||
| 134 | + page: page.value.toString(), | ||
| 135 | + limit: limit.value.toString() | ||
| 136 | + }; | ||
| 137 | + | ||
| 138 | + const response = await getPointListAPI(params); | ||
| 139 | + | ||
| 140 | + if (response.code && response.data) { | ||
| 141 | + totalPoints.value = response.data.total_points || 0; | ||
| 142 | + | ||
| 143 | + const logs = response.data.logs || []; | ||
| 99 | 144 | ||
| 100 | -const pointsHistory = ref([ | 145 | + // 转换API数据格式为页面需要的格式 |
| 101 | - { | 146 | + const formattedLogs = logs.map(log => ({ |
| 102 | - id: 1, | 147 | + id: log.id, |
| 103 | - title: '每日步数奖励', | 148 | + title: log.note || '积分变动', |
| 104 | - date: '2025-08-24 09:30', | 149 | + date: formatDate(new Date()), // API没有返回时间,使用当前时间 |
| 105 | - points: 100, | 150 | + points: Math.abs(parseInt(log.points_change) || 0), |
| 106 | - type: 'earned' | 151 | + type: parseInt(log.points_change) > 0 ? 'earned' : 'spent', |
| 107 | - }, | 152 | + log_type: log.log_type, |
| 108 | - { | 153 | + source_type: log.source_type, |
| 109 | - id: 2, | 154 | + balance_after: log.balance_after |
| 110 | - title: '周末步数挑战完成', | 155 | + })); |
| 111 | - date: '2025-08-24 08:15', | 156 | + |
| 112 | - points: 200, | 157 | + if (isRefresh) { |
| 113 | - type: 'earned' | 158 | + pointsHistory.value = formattedLogs; |
| 114 | - }, | 159 | + } else { |
| 115 | - { | 160 | + pointsHistory.value = [...pointsHistory.value, ...formattedLogs]; |
| 116 | - id: 3, | 161 | + } |
| 117 | - title: '兑换杏花楼85折券', | 162 | + |
| 118 | - date: '2025-08-23 15:20', | 163 | + // 判断是否还有更多数据 |
| 119 | - points: 10, | 164 | + hasMore.value = logs.length >= limit.value; |
| 120 | - type: 'spent' | 165 | + |
| 121 | - }, | 166 | + if (logs.length > 0) { |
| 122 | - { | 167 | + page.value += 1; |
| 123 | - id: 4, | 168 | + } |
| 124 | - title: '邀请家人奖励', | 169 | + } |
| 125 | - date: '2025-08-23 14:00', | 170 | + } catch (error) { |
| 126 | - points: 500, | 171 | + console.error('获取积分列表失败:', error); |
| 127 | - type: 'earned' | 172 | + Taro.showToast({ |
| 128 | - }, | 173 | + title: '获取数据失败', |
| 129 | - { | 174 | + icon: 'none' |
| 130 | - id: 5, | 175 | + }); |
| 131 | - title: '兑换老凤祥9折券', | 176 | + } finally { |
| 132 | - date: '2025-08-22 16:45', | 177 | + loading.value = false; |
| 133 | - points: 1000, | ||
| 134 | - type: 'spent' | ||
| 135 | } | 178 | } |
| 136 | -]); | 179 | +}; |
| 180 | + | ||
| 181 | +/** | ||
| 182 | + * 格式化日期 | ||
| 183 | + */ | ||
| 184 | +const formatDate = (date) => { | ||
| 185 | + const year = date.getFullYear(); | ||
| 186 | + const month = String(date.getMonth() + 1).padStart(2, '0'); | ||
| 187 | + const day = String(date.getDate()).padStart(2, '0'); | ||
| 188 | + const hours = String(date.getHours()).padStart(2, '0'); | ||
| 189 | + const minutes = String(date.getMinutes()).padStart(2, '0'); | ||
| 190 | + return `${year}-${month}-${day} ${hours}:${minutes}`; | ||
| 191 | +}; | ||
| 137 | 192 | ||
| 138 | const filteredPoints = computed(() => { | 193 | const filteredPoints = computed(() => { |
| 139 | if (activeTab.value === 'all') { | 194 | if (activeTab.value === 'all') { |
| ... | @@ -146,5 +201,15 @@ const handleViewAll = () => { | ... | @@ -146,5 +201,15 @@ const handleViewAll = () => { |
| 146 | Taro.navigateTo({ | 201 | Taro.navigateTo({ |
| 147 | url: '/pages/PointsList/index' | 202 | url: '/pages/PointsList/index' |
| 148 | }) | 203 | }) |
| 149 | -} | 204 | +}; |
| 205 | + | ||
| 206 | +// 页面显示时获取数据 | ||
| 207 | +useDidShow(() => { | ||
| 208 | + fetchPointsList(true); | ||
| 209 | +}); | ||
| 210 | + | ||
| 211 | +// 组件挂载时获取数据 | ||
| 212 | +onMounted(() => { | ||
| 213 | + fetchPointsList(true); | ||
| 214 | +}); | ||
| 150 | </script> | 215 | </script> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-08-27 17:47:46 | 2 | * @Date: 2025-08-27 17:47:46 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-09-02 20:41:29 | 4 | + * @LastEditTime: 2025-09-03 20:02:11 |
| 5 | * @FilePath: /lls_program/src/pages/Profile/index.vue | 5 | * @FilePath: /lls_program/src/pages/Profile/index.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -53,13 +53,13 @@ const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ... | @@ -53,13 +53,13 @@ const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' |
| 53 | // 获取接口信息 | 53 | // 获取接口信息 |
| 54 | import { getUserProfileAPI } from '@/api/user' | 54 | import { getUserProfileAPI } from '@/api/user' |
| 55 | 55 | ||
| 56 | -const menuItems = shallowRef([ | 56 | +const allMenuItems = [ |
| 57 | { | 57 | { |
| 58 | id: 'family', | 58 | id: 'family', |
| 59 | icon: My, | 59 | icon: My, |
| 60 | label: '我的家庭', | 60 | label: '我的家庭', |
| 61 | color: 'bg-blue-500', | 61 | color: 'bg-blue-500', |
| 62 | - onClick: () => Taro.navigateTo({ url: '/pages/MyFamily/index' }) | 62 | + onClick: () => Taro.navigateTo({ url: '/pages/MyFamily/index' }), |
| 63 | }, | 63 | }, |
| 64 | { | 64 | { |
| 65 | id: 'points', | 65 | id: 'points', |
| ... | @@ -96,11 +96,14 @@ const menuItems = shallowRef([ | ... | @@ -96,11 +96,14 @@ const menuItems = shallowRef([ |
| 96 | color: 'bg-blue-500', | 96 | color: 'bg-blue-500', |
| 97 | onClick: () => Taro.navigateTo({ url: '/pages/PrivacyPolicy/index' }) | 97 | onClick: () => Taro.navigateTo({ url: '/pages/PrivacyPolicy/index' }) |
| 98 | } | 98 | } |
| 99 | -]); | 99 | +]; |
| 100 | + | ||
| 101 | +const menuItems = shallowRef([]); | ||
| 100 | 102 | ||
| 101 | const userInfo = ref({ | 103 | const userInfo = ref({ |
| 102 | nickName: '', | 104 | nickName: '', |
| 103 | avatarUrl: '', | 105 | avatarUrl: '', |
| 106 | + is_creator: false, | ||
| 104 | }); | 107 | }); |
| 105 | 108 | ||
| 106 | const goToEditProfile = () => { | 109 | const goToEditProfile = () => { |
| ... | @@ -114,6 +117,12 @@ const initPageData = async () => { | ... | @@ -114,6 +117,12 @@ const initPageData = async () => { |
| 114 | userInfo.value = { | 117 | userInfo.value = { |
| 115 | nickName: data?.user?.nickname || '', | 118 | nickName: data?.user?.nickname || '', |
| 116 | avatarUrl: data?.user?.avatar_url || '', | 119 | avatarUrl: data?.user?.avatar_url || '', |
| 120 | + is_creator: data?.user?.is_creator || false | ||
| 121 | + } | ||
| 122 | + if (userInfo.value.is_creator) { | ||
| 123 | + menuItems.value = allMenuItems; | ||
| 124 | + } else { | ||
| 125 | + menuItems.value = allMenuItems.filter(item => !['points', 'rewards'].includes(item.id)); | ||
| 117 | } | 126 | } |
| 118 | } | 127 | } |
| 119 | } | 128 | } | ... | ... |
-
Please register or login to post a comment