hookehuyr

feat(积分): 实现积分详情页与API对接

添加积分列表API接口并实现积分详情页数据展示
移除EditFamily页面的模拟数据,使用真实数据
在Profile页面根据用户角色动态显示菜单项
...@@ -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 }
......