hookehuyr

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

添加积分列表API接口并实现积分详情页数据展示
移除EditFamily页面的模拟数据,使用真实数据
在Profile页面根据用户角色动态显示菜单项
......@@ -10,6 +10,7 @@ import { fn, fetch } from './fn';
const Api = {
SYNC_WX_STEP: '/srv/?a=point&t=sync_wx_step',
COLLECT_POINT: '/srv/?a=point&t=collect',
POINT_LIST: '/srv/?f=walk&a=point&t=list',
}
/**
......@@ -31,3 +32,23 @@ export const syncWxStepAPI = (params) => fn(fetch.post(Api.SYNC_WX_STEP, params)
*/
export const collectPointAPI = (params) => fn(fetch.post(Api.COLLECT_POINT, params));
/**
* @description: 查询积分列表
* @param {Object} params - 请求参数
* @param {string} [params.page] - 页码,从0开始,默认为0
* @param {string} [params.limit] - 每页数量,默认为10
* @returns {Object} response - 响应对象
* @returns {number} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
* @returns {number} response.data.total_points - 我的积分
* @returns {Array} response.data.logs - 积分日志列表
* @returns {number} response.data.logs[].id - 积分日志ID
* @returns {string} response.data.logs[].points_change - 积分变化
* @returns {string} response.data.logs[].balance_after - 最新的家庭积分
* @returns {string} response.data.logs[].log_type - 日志类型
* @returns {string} response.data.logs[].source_type - 积分来源类型
* @returns {string} response.data.logs[].note - 备注
*/
export const getPointListAPI = (params) => fn(fetch.get(Api.POINT_LIST, params));
......
<!--
* @Date: 2025-08-27 17:44:53
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-03 11:23:08
* @LastEditTime: 2025-09-03 20:31:29
* @FilePath: /lls_program/src/pages/EditFamily/index.vue
* @Description: 文件描述
-->
......@@ -223,21 +223,13 @@ onMounted(async () => {
if (code) {
familyName.value = data.name;
familyIntro.value = data.note;
districtValue.value = [data.county];
selectedDistrict.value = data.county;
selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value).text;
familyMotto.value = data.passphrase.split(',');
districtValue.value = [+data.county];
selectedDistrict.value = +data.county;
selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value)?.text;
//
familyMotto.value = data.passphrase.split('');
familyAvatar.value = data.avatar_url;
}
// Mock data for current family information
familyName.value = '幸福一家';
familyIntro.value = '我们是相亲相爱的一家人';
districtValue.value = [310104]; // 模拟勾选项已选择徐汇区
// 需要提交的数据集
selectedDistrict.value = 310104; // 模拟已选择徐汇区
selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value).text;
familyMotto.value = ['家', '和', '万', '事'];
familyAvatar.value = 'https://img.yzcdn.cn/vant/cat.jpeg';
});
/**
......
......@@ -7,7 +7,7 @@
<view class="relative z-10 flex-1 pb-20">
<!-- Points display -->
<view class="pt-4 pb-6 flex flex-col items-center">
<h2 class="text-4xl font-bold text-white mb-1">2580</h2>
<h2 class="text-4xl font-bold text-white mb-1">{{ totalPoints }}</h2>
<p class="text-white text-opacity-80">当前可用积分</p>
</view>
<!-- Points strategy section -->
......@@ -71,15 +71,26 @@
</view>
<!-- Points history list -->
<view class="pt-4">
<view v-for="item in filteredPoints" :key="item.id" class="py-4 border-b border-gray-100 flex justify-between">
<view>
<h4 class="font-medium">{{ item.title }}</h4>
<p class="text-sm text-gray-500">{{ item.date }}</p>
<!-- Loading state -->
<view v-if="loading && pointsHistory.length === 0" class="py-8 text-center text-gray-500">
加载中...
</view>
<!-- Empty state -->
<view v-else-if="!loading && filteredPoints.length === 0" class="py-8 text-center text-gray-500">
暂无积分记录
</view>
<!-- Points list -->
<view v-else>
<view v-for="item in filteredPoints" :key="item.id" class="py-4 border-b border-gray-100 flex justify-between">
<view>
<h4 class="font-medium">{{ item.title }}</h4>
<p class="text-sm text-gray-500">{{ item.date }}</p>
</view>
<span :class="['font-medium', item.type === 'earned' ? 'text-green-500' : 'text-red-500']">
{{ item.type === 'earned' ? '+' : '-' }}
{{ item.points }}
</span>
</view>
<span :class="['font-medium', item.type === 'earned' ? 'text-green-500' : 'text-red-500']">
{{ item.type === 'earned' ? '+' : '-' }}
{{ item.points }}
</span>
</view>
</view>
</view>
......@@ -89,51 +100,95 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import Taro from '@tarojs/taro';
import { ref, computed, onMounted } from 'vue';
import Taro, { useDidShow } from '@tarojs/taro';
import AppHeader from '../../components/AppHeader.vue';
import BottomNav from '../../components/BottomNav.vue';
import { Right, My } from '@nutui/icons-vue-taro';
import { getPointListAPI } from '../../api/points';
const activeTab = ref('all');
const pointsHistory = ref([]);
const totalPoints = ref(0);
const loading = ref(false);
const page = ref(0);
const limit = ref(20);
const hasMore = ref(true);
/**
* 获取积分列表数据
*/
const fetchPointsList = async (isRefresh = false) => {
if (loading.value) return;
try {
loading.value = true;
if (isRefresh) {
page.value = 0;
pointsHistory.value = [];
hasMore.value = true;
}
const params = {
page: page.value.toString(),
limit: limit.value.toString()
};
const response = await getPointListAPI(params);
if (response.code && response.data) {
totalPoints.value = response.data.total_points || 0;
const pointsHistory = ref([
{
id: 1,
title: '每日步数奖励',
date: '2025-08-24 09:30',
points: 100,
type: 'earned'
},
{
id: 2,
title: '周末步数挑战完成',
date: '2025-08-24 08:15',
points: 200,
type: 'earned'
},
{
id: 3,
title: '兑换杏花楼85折券',
date: '2025-08-23 15:20',
points: 10,
type: 'spent'
},
{
id: 4,
title: '邀请家人奖励',
date: '2025-08-23 14:00',
points: 500,
type: 'earned'
},
{
id: 5,
title: '兑换老凤祥9折券',
date: '2025-08-22 16:45',
points: 1000,
type: 'spent'
const logs = response.data.logs || [];
// 转换API数据格式为页面需要的格式
const formattedLogs = logs.map(log => ({
id: log.id,
title: log.note || '积分变动',
date: formatDate(new Date()), // API没有返回时间,使用当前时间
points: Math.abs(parseInt(log.points_change) || 0),
type: parseInt(log.points_change) > 0 ? 'earned' : 'spent',
log_type: log.log_type,
source_type: log.source_type,
balance_after: log.balance_after
}));
if (isRefresh) {
pointsHistory.value = formattedLogs;
} else {
pointsHistory.value = [...pointsHistory.value, ...formattedLogs];
}
// 判断是否还有更多数据
hasMore.value = logs.length >= limit.value;
if (logs.length > 0) {
page.value += 1;
}
}
} catch (error) {
console.error('获取积分列表失败:', error);
Taro.showToast({
title: '获取数据失败',
icon: 'none'
});
} finally {
loading.value = false;
}
]);
};
/**
* 格式化日期
*/
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const filteredPoints = computed(() => {
if (activeTab.value === 'all') {
......@@ -146,5 +201,15 @@ const handleViewAll = () => {
Taro.navigateTo({
url: '/pages/PointsList/index'
})
}
};
// 页面显示时获取数据
useDidShow(() => {
fetchPointsList(true);
});
// 组件挂载时获取数据
onMounted(() => {
fetchPointsList(true);
});
</script>
......
<!--
* @Date: 2025-08-27 17:47:46
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-02 20:41:29
* @LastEditTime: 2025-09-03 20:02:11
* @FilePath: /lls_program/src/pages/Profile/index.vue
* @Description: 文件描述
-->
......@@ -53,13 +53,13 @@ const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
// 获取接口信息
import { getUserProfileAPI } from '@/api/user'
const menuItems = shallowRef([
const allMenuItems = [
{
id: 'family',
icon: My,
label: '我的家庭',
color: 'bg-blue-500',
onClick: () => Taro.navigateTo({ url: '/pages/MyFamily/index' })
onClick: () => Taro.navigateTo({ url: '/pages/MyFamily/index' }),
},
{
id: 'points',
......@@ -96,11 +96,14 @@ const menuItems = shallowRef([
color: 'bg-blue-500',
onClick: () => Taro.navigateTo({ url: '/pages/PrivacyPolicy/index' })
}
]);
];
const menuItems = shallowRef([]);
const userInfo = ref({
nickName: '',
avatarUrl: '',
is_creator: false,
});
const goToEditProfile = () => {
......@@ -114,6 +117,12 @@ const initPageData = async () => {
userInfo.value = {
nickName: data?.user?.nickname || '',
avatarUrl: data?.user?.avatar_url || '',
is_creator: data?.user?.is_creator || false
}
if (userInfo.value.is_creator) {
menuItems.value = allMenuItems;
} else {
menuItems.value = allMenuItems.filter(item => !['points', 'rewards'].includes(item.id));
}
}
}
......