hookehuyr

feat(积分收集): 重构积分收集组件以支持后端数据

- 移除模拟数据,使用接口返回的待收集积分数据
- 添加积分来源类型映射和显示
- 修改总步数计算逻辑以匹配新数据结构
- 添加对积分和待收集积分的监听
......@@ -16,13 +16,9 @@
:style="getItemStyle(item)"
@tap="collectItem(item)"
>
<view v-if="item.type === 'steps'" class="item-content">
<text class="item-value">{{ item.steps }}步</text>
<text class="item-type">{{ item.value }}分</text>
</view>
<view v-else class="item-content">
<text class="item-value">奖励</text>
<text class="item-type">{{ item.value }}分</text>
<view class="item-content">
<text class="item-value">{{ item.sourceLabel }}</text>
<text class="item-type">{{ item.points }}分</text>
</view>
</view>
......@@ -38,7 +34,7 @@
</template>
<script setup>
import { ref, onMounted, defineProps, defineExpose, defineEmits } from 'vue'
import { ref, onMounted, defineProps, defineExpose, defineEmits, watch } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
const emit = defineEmits(['collection-complete'])
......@@ -46,46 +42,64 @@ const props = defineProps({
height: {
type: String,
default: '30vh'
},
pendingPoints: {
type: Array,
default: () => []
},
totalPoints: {
type: Number,
default: 0
}
})
// 响应式数据
const totalPoints = ref(12500) // 总积分
const animatedTotalPoints = ref(12500) // 动画中的总积分
const animatedTotalPoints = ref(props.totalPoints) // 动画中的总积分
const floatingItems = ref([]) // 漂浮的积分项
const isCollecting = ref(false) // 是否正在收集,防止重复触发
// source_type 中英文映射
const sourceTypeMap = {
'WALKING': '步数积分',
'CHECK_IN': '活动打卡',
'CHECK_IN_COUNT': '完成活动',
'FAMILY_SIZE': '家庭成员',
'COMPANION_PHOTO': '陪伴拍照',
'WHEELCHAIR_COMPANION': '陪伴轮椅'
}
/**
* 生成模拟数据并分配随机位置
* 根据pending_points数据生成漂浮积分项并分配随机位置
*/
const generateMockData = () => {
const mockItems = [
{ id: 1, type: 'steps', value: 5000, steps: 5000 },
{ id: 2, type: 'points', value: 500 },
{ id: 3, type: 'steps', value: 8000, steps: 5000 },
{ id: 4, type: 'points', value: 30 },
{ id: 5, type: 'steps', value: 12000, steps: 12000 },
{ id: 6, type: 'points', value: 800 },
{ id: 7, type: 'points', value: 250 },
{ id: 8, type: 'steps', value: 6000, steps: 6000 },
{ id: 9, type: 'points', value: 1000 },
{ id: 10, type: 'steps', value: 10000, steps: 10000 },
].map(item => ({ ...item, collecting: false })); // 初始化collecting状态
const maxValue = Math.max(...mockItems.map(i => i.value));
const generatePointsData = () => {
if (!props.pendingPoints || props.pendingPoints.length === 0) {
return [];
}
const pointsItems = props.pendingPoints.map(item => ({
id: item.id,
points: parseInt(item.points),
sourceType: item.source_type,
sourceLabel: sourceTypeMap[item.source_type] || item.source_type,
title: item.title,
note: item.note,
collecting: false
}));
const maxValue = Math.max(...pointsItems.map(i => i.points), 1);
const baseSize = 80;
const { windowWidth, windowHeight } = Taro.getWindowInfo();
const positionedItems = [];
// 为每个项目分配一个不重叠的随机位置
return mockItems.map(item => {
return pointsItems.map(item => {
let x, y, hasCollision;
const maxAttempts = 100; // 限制尝试次数以避免无限循环
let attempts = 0;
const centerNoFlyZone = 25; // 中心圆形25%的禁飞区半径
// 计算项目大小和半径
const sizeRatio = item.value / maxValue;
const sizeRatio = item.points / maxValue;
const size = baseSize + (sizeRatio * 40); // rpx
const radiusXPercent = (size / 750) * 100 / 2 * (windowWidth / windowHeight) ; // 归一化为高度的百分比
......@@ -140,8 +154,8 @@ const generateMockData = () => {
*/
const getItemStyle = (item) => {
const baseSize = 80;
const maxValue = Math.max(...(floatingItems.value.map(i => i.value).length > 0 ? floatingItems.value.map(i => i.value) : [1]));
const sizeRatio = item.value / maxValue;
const maxValue = Math.max(...(floatingItems.value.map(i => i.points).length > 0 ? floatingItems.value.map(i => i.points) : [1]));
const sizeRatio = item.points / maxValue;
const size = baseSize + (sizeRatio * 40);
const style = {
......@@ -174,13 +188,13 @@ const collectItem = (item) => {
item.collecting = true;
setTimeout(() => {
const totalToAdd = item.type === 'steps' ? Math.floor(item.value / 10) : item.value;
animateNumber(totalPoints.value, totalPoints.value + totalToAdd);
totalPoints.value += totalToAdd;
const totalToAdd = item.points;
const newTotal = props.totalPoints + totalToAdd;
animateNumber(animatedTotalPoints.value, newTotal);
floatingItems.value = floatingItems.value.filter(i => i.id !== item.id);
if (floatingItems.value.length === 0) {
emit('collection-complete', totalPoints.value);
emit('collection-complete', newTotal);
}
}, 800); // 动画时长
}
......@@ -199,17 +213,17 @@ const collectAll = async () => {
setTimeout(() => {
item.collecting = true;
}, index * 80); // 依次触发动画
totalToAdd += item.value;
totalToAdd += item.points;
});
const totalAnimationTime = itemsToCollect.length * 80 + 800;
setTimeout(() => {
animateNumber(totalPoints.value, totalPoints.value + totalToAdd);
totalPoints.value += totalToAdd;
const newTotal = props.totalPoints + totalToAdd;
animateNumber(animatedTotalPoints.value, newTotal);
floatingItems.value = [];
isCollecting.value = false;
emit('collection-complete', totalPoints.value);
emit('collection-complete', newTotal);
}, totalAnimationTime);
}
......@@ -243,10 +257,16 @@ defineExpose({
* 初始化数据
*/
const initData = async () => {
floatingItems.value = generateMockData();
console.warn('初始化数据')
floatingItems.value = generatePointsData();
animatedTotalPoints.value = props.totalPoints;
console.warn('初始化积分数据', props.pendingPoints)
}
// 监听props变化
watch(() => [props.pendingPoints, props.totalPoints], () => {
initData();
}, { deep: true, immediate: true })
// 组件挂载时初始化数据
onMounted(() => {
initData();
......
......@@ -29,7 +29,7 @@
<view class="flex justify-between items-center">
<view class="flex items-baseline">
<span class="text-4xl font-bold">
{{ todaySteps }}
{{ todaySteps?.toLocaleString() }}
</span>
<span class="ml-1 text-gray-500">步</span>
</view>
......@@ -51,6 +51,7 @@
ref="pointsCollectorRef"
height="30vh"
:total-points="finalTotalPoints"
:pending-points="pendingPoints"
@collection-complete="handleCollectionComplete"
/>
</template>
......@@ -77,14 +78,14 @@
<view class="flex justify-between items-center mb-4">
<h2 class="font-medium text-lg">今日家庭步数排行</h2>
<span class="text-sm text-gray-500">
总计 {{ getTotalSteps(totalFamilySteps).toLocaleString() }} 步
总计 {{ getTotalSteps() }} 步
</span>
</view>
<view class="grid grid-cols-4 gap-2">
<view v-for="member in familyMembers" :key="member.id" class="flex flex-col items-center">
<image :src="member.avatar" :alt="member.name" class="w-16 h-16 rounded-full mb-1" />
<view v-for="member in familyMembers" :key="member.user_id" class="flex flex-col items-center">
<image :src="member.avatar_url || defaultAvatar" :alt="member.role" class="w-16 h-16 rounded-full mb-1" />
<span class="text-sm text-gray-700">
{{ member.steps.toLocaleString() }}步
{{ member?.today_step?.toLocaleString() }}步
</span>
</view>
</view>
......@@ -205,6 +206,7 @@ const pointsCollectorRef = ref(null)
const weRunAuthRef = ref(null)
const showTotalPointsOnly = ref(false)
const finalTotalPoints = ref(0)
const pendingPoints = ref([]) // 待收集的积分数据
const familyName = ref('')
const familySlogn = ref('')
......@@ -291,60 +293,14 @@ const handleStepsSynced = async (data) => {
/**
* 计算总步数(包含用户步数和家庭成员步数)
* @param {number} userSteps - 用户步数
* @returns {number} 总步数
* @returns {string} 格式化后的总步数
*/
const getTotalSteps = (userSteps) => {
return familyMembers.value.reduce((sum, member) => sum + member.steps, 0) + userSteps
const getTotalSteps = () => {
return familyMembers.value.reduce((sum, member) => sum + member.today_step, 0).toLocaleString()
}
// Mock data for family members
const familyMembers = ref([
{
id: 1,
name: '妈妈',
steps: 7000,
avatar: 'https://randomuser.me/api/portraits/women/44.jpg'
},
{
id: 2,
name: '爸爸',
steps: 6000,
avatar: 'https://randomuser.me/api/portraits/men/32.jpg'
},
{
id: 3,
name: '儿子',
steps: 5000,
avatar: 'https://randomuser.me/api/portraits/men/22.jpg'
},
{
id: 4,
name: '女儿',
steps: 4000,
avatar: 'https://randomuser.me/api/portraits/women/29.jpg'
},
{
id: 5,
name: '孙子',
steps: 3000,
avatar: 'https://randomuser.me/api/portraits/men/25.jpg'
},
{
id: 6,
name: '孙女',
steps: 20000,
avatar: 'https://randomuser.me/api/portraits/women/27.jpg'
},
]);
// 注意:totalSteps 计算逻辑已移至 getTotalSteps 方法中
const handleSyncSteps = () => {
// In a real app, this would sync with a health API
// For demo purposes, we'll just log and do nothing
console.log('Syncing steps...');
};
const familyMembers = ref([]);
const goToProfile = () => {
Taro.navigateTo({ url: '/pages/EditFamily/index' });
......@@ -376,10 +332,14 @@ const initPageData = async () => {
if (code) {
// 获取用户信息
console.warn(data);
// 真实情况下 需要重新获取积分列表, 测试随机积分获取状态, 接口有积分列表就显示出来, 没有就不显示
// 设置待收集的积分数据
pendingPoints.value = data.pending_points || [];
// 获取今日家庭总步数
familyMembers.value = data.step_ranking || [];
// 根据是否有待收集积分决定显示模式
showTotalPointsOnly.value = !data.pending_points.length;
// 总积分数量也要从接口获取传进去
finalTotalPoints.value = 10086;
// 总积分数量从接口获取
finalTotalPoints.value = data.family.total_points || 0;
// 获取用户信息
familyName.value = data.family.name;
familySlogn.value = data.family.note;
......