hookehuyr

feat(微信运动): 添加微信运动授权组件和功能集成

添加 WeRunAuth 组件用于微信运动数据授权和步数获取
在 Dashboard 页面集成微信运动数据展示和授权流程
更新 app.config 添加微信运动权限配置
......@@ -27,5 +27,6 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TabBar: typeof import('./src/components/TabBar.vue')['default']
WeRunAuth: typeof import('./src/components/WeRunAuth.vue')['default']
}
}
......
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-29 10:52:42
* @LastEditTime: 2025-08-29 15:56:45
* @FilePath: /lls_program/src/app.config.js
* @Description: 文件描述
*/
......@@ -9,9 +9,9 @@ export default {
pages: [
// 'pages/index/index',
// 'pages/auth/index',
'pages/Welcome/index',
'pages/Dashboard/index',
'pages/MyFamily/index',
'pages/Welcome/index',
'pages/CreateFamily/index',
'pages/JoinFamily/index',
'pages/Activities/index',
......@@ -47,6 +47,9 @@ export default {
permission: {
'scope.userLocation': {
desc: '您的位置信息将用于活动打卡和位置相关服务'
},
'scope.werun': {
desc: '您的微信运动数据将用于记录步数和健康统计'
}
}
}
......
<template>
<view class="werun-auth-container">
<!-- 未授权状态 -->
<view v-if="!isAuthorized" class="auth-prompt">
<view class="px-5 py-6 bg-white rounded-xl shadow-md mx-4 mt-4">
<view class="flex flex-col items-center justify-center py-8">
<view class="mb-4">
<image
src="https://placehold.co/100x100/e2f3ff/0369a1?text=步数&font=roboto"
class="w-20 h-20 rounded-full"
/>
</view>
<text class="text-lg font-medium mb-2">获取微信运动数据</text>
<text class="text-gray-500 text-center mb-6 px-4">
授权后可查看您的步数信息,与家人一起记录健康生活
</text>
<view
class="bg-blue-500 text-white px-8 py-3 rounded-full text-sm"
@click="requestAuth"
>
立即授权
</view>
</view>
</view>
</view>
<!-- 已授权状态 - 显示步数相关内容 -->
<view v-else>
<slot :steps="todaySteps" :isLoading="isLoading" />
</view>
</view>
</template>
<script setup>
import { ref, onMounted, defineEmits } from 'vue'
import Taro from '@tarojs/taro'
// 定义事件
const emit = defineEmits(['auth-change', 'steps-update'])
// 响应式数据
const isAuthorized = ref(false)
const todaySteps = ref(0)
const isLoading = ref(false)
/**
* @description 检查微信运动授权状态
*/
const checkAuthStatus = async () => {
try {
const authSetting = await Taro.getSetting()
const hasWeRunAuth = authSetting.authSetting['scope.werun']
isAuthorized.value = hasWeRunAuth === true
// 通知父组件授权状态变化
emit('auth-change', isAuthorized.value)
// 如果已授权,获取步数数据
if (isAuthorized.value) {
await getWeRunData()
}
} catch (error) {
console.error('检查授权状态失败:', error)
isAuthorized.value = false
emit('auth-change', false)
}
}
/**
* @description 请求微信运动授权
*/
const requestAuth = async () => {
try {
isLoading.value = true
// 请求微信运动授权
await Taro.authorize({
scope: 'scope.werun'
})
isAuthorized.value = true
emit('auth-change', true)
// 授权成功后获取步数数据
await getWeRunData()
Taro.showToast({
title: '授权成功',
icon: 'success'
})
} catch (error) {
console.error('授权失败:', error)
// 用户拒绝授权,引导到设置页面
Taro.showModal({
title: '授权提示',
content: '需要获取微信运动数据权限才能使用步数功能,请在设置中开启',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
Taro.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.werun']) {
checkAuthStatus()
}
}
})
}
}
})
} finally {
isLoading.value = false
}
}
/**
* @description 获取微信运动数据
*/
const getWeRunData = async () => {
try {
isLoading.value = true
// 先调用 wx.login 获取 code
const loginRes = await Taro.login()
// 获取微信运动数据
const weRunRes = await Taro.getWeRunData()
// 这里需要将加密数据发送到后端解密
// 暂时使用模拟数据
const mockSteps = Math.floor(Math.random() * 10000) + 1000
todaySteps.value = mockSteps
// 通知父组件步数更新
emit('steps-update', todaySteps.value)
console.log('微信运动数据获取成功:', {
encryptedData: weRunRes.encryptedData,
iv: weRunRes.iv,
code: loginRes.code
})
// TODO: 实际项目中需要将 encryptedData, iv, code 发送到后端解密
// const decryptedData = await api.decryptWeRunData({
// encryptedData: weRunRes.encryptedData,
// iv: weRunRes.iv,
// code: loginRes.code
// })
// todaySteps.value = decryptedData.todaySteps
} catch (error) {
console.error('获取微信运动数据失败:', error)
// 如果获取失败,使用默认值
todaySteps.value = 0
emit('steps-update', 0)
Taro.showToast({
title: '获取步数失败',
icon: 'none'
})
} finally {
isLoading.value = false
}
}
/**
* @description 刷新步数数据
*/
const refreshSteps = async () => {
if (isAuthorized.value) {
await getWeRunData()
}
}
// 暴露方法给父组件
defineExpose({
refreshSteps,
checkAuthStatus
})
// 组件挂载时检查授权状态
onMounted(() => {
checkAuthStatus()
})
</script>
<style scoped>
.werun-auth-container {
width: 100%;
}
.auth-prompt {
text-align: center;
}
</style>
\ No newline at end of file
......@@ -13,56 +13,65 @@
</view>
</view>
<!-- Today's steps section -->
<view class="px-5 py-6 bg-white rounded-xl shadow-md mx-4 mt-4">
<view class="flex justify-between items-center mb-1">
<span class="text-gray-500">今日</span>
</view>
<view class="flex justify-between items-center">
<view class="flex items-baseline">
<span class="text-4xl font-bold">
{{ todaySteps.toLocaleString() }}
</span>
<span class="ml-1 text-gray-500">步</span>
</view>
<view class="bg-blue-500 text-white px-4 py-2 rounded-full text-sm" @click="handleCollectAll">
一键收取
<!-- 微信步数授权组件 -->
<WeRunAuth
ref="weRunAuthRef"
@auth-change="handleAuthChange"
@steps-update="handleStepsUpdate"
>
<template #default="{ steps, isLoading }">
<!-- Today's steps section -->
<view class="px-5 py-6 bg-white rounded-xl shadow-md mx-4 mt-4">
<view class="flex justify-between items-center mb-1">
<span class="text-gray-500">今日</span>
</view>
<view class="flex justify-between items-center">
<view class="flex items-baseline">
<span class="text-4xl font-bold">
{{ isLoading ? '...' : steps.toLocaleString() }}
</span>
<span class="ml-1 text-gray-500">步</span>
</view>
<view class="bg-blue-500 text-white px-4 py-2 rounded-full text-sm" @click="handleCollectAll">
一键收取
</view>
</view>
</view>
</view>
</view>
<!-- Points circles -->
<view class="flex justify-between px-5 py-6 my-4 bg-white rounded-xl shadow-md mx-4">
<PointsCollector ref="pointsCollectorRef" height="30vh" />
</view>
<!-- Points circles -->
<view class="flex justify-between px-5 py-6 my-4 bg-white rounded-xl shadow-md mx-4">
<PointsCollector ref="pointsCollectorRef" height="30vh" />
</view>
<!-- Photo button -->
<view class="px-5 mb-4">
<view @tap="openCamera" class="w-full bg-blue-500 text-white py-3 rounded-lg flex flex-col items-center justify-center">
<view class="flex items-center justify-center">
<Photograph size="20" class="mr-2" />
拍照留念,奖励积分
<!-- Photo button -->
<view class="px-5 mb-4">
<view @tap="openCamera" class="w-full bg-blue-500 text-white py-3 rounded-lg flex flex-col items-center justify-center">
<view class="flex items-center justify-center">
<Photograph size="20" class="mr-2" />
拍照留念,奖励积分
</view>
</view>
</view>
</view>
</view>
<!-- Family step ranking -->
<view class="p-5 bg-white rounded-xl shadow-md mx-4">
<view class="flex justify-between items-center mb-4">
<h2 class="font-medium text-lg">今日家庭步数排行</h2>
<span class="text-sm text-gray-500">
总计 {{ totalSteps.toLocaleString() }} 步
</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" />
<span class="text-sm text-gray-700">
{{ member.steps.toLocaleString() }}步
</span>
<!-- Family step ranking -->
<view class="p-5 bg-white rounded-xl shadow-md mx-4">
<view class="flex justify-between items-center mb-4">
<h2 class="font-medium text-lg">今日家庭步数排行</h2>
<span class="text-sm text-gray-500">
总计 {{ getTotalSteps(steps).toLocaleString() }} 步
</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" />
<span class="text-sm text-gray-700">
{{ member.steps.toLocaleString() }}步
</span>
</view>
</view>
</view>
</view>
</view>
</template>
</WeRunAuth>
<!-- Family album -->
<view class="p-5 mt-4 mb-6 bg-white rounded-xl shadow-md mx-4">
......@@ -93,9 +102,12 @@ import Taro from '@tarojs/taro';
import { Setting, Photograph, Right } from '@nutui/icons-vue-taro';
import BottomNav from '../../components/BottomNav.vue';
import PointsCollector from '@/components/PointsCollector.vue'
import WeRunAuth from '@/components/WeRunAuth.vue'
const todaySteps = ref(2000);
const todaySteps = ref(0);
const isWeRunAuthorized = ref(false);
const pointsCollectorRef = ref(null)
const weRunAuthRef = ref(null)
/**
* 触发积分收集组件的一键收取
......@@ -106,6 +118,33 @@ const handleCollectAll = () => {
}
}
/**
* 处理微信步数授权状态变化
* @param {boolean} authorized - 授权状态
*/
const handleAuthChange = (authorized) => {
isWeRunAuthorized.value = authorized
console.log('微信步数授权状态:', authorized)
}
/**
* 处理步数数据更新
* @param {number} steps - 步数
*/
const handleStepsUpdate = (steps) => {
todaySteps.value = steps
console.log('步数更新:', steps)
}
/**
* 计算总步数(包含用户步数和家庭成员步数)
* @param {number} userSteps - 用户步数
* @returns {number} 总步数
*/
const getTotalSteps = (userSteps) => {
return familyMembers.value.reduce((sum, member) => sum + member.steps, 0) + userSteps
}
// Mock data for family members
const familyMembers = ref([
{
......@@ -134,10 +173,7 @@ const familyMembers = ref([
}
]);
// Calculate total family steps
const totalSteps = computed(() =>
familyMembers.value.reduce((sum, member) => sum + member.steps, 0) + todaySteps.value
);
// 注意:totalSteps 计算逻辑已移至 getTotalSteps 方法中
const handleSyncSteps = () => {
// In a real app, this would sync with a health API
......@@ -162,10 +198,10 @@ const openCamera = () => {
onMounted(() => {
// TODO: 等待真实接口获取用户是否加入家庭
const hasJoinedFamily = false; // Change to true to simulate having a family
// const hasJoinedFamily = false; // Change to true to simulate having a family
if (!hasJoinedFamily) {
Taro.redirectTo({ url: '/pages/Welcome/index' });
}
// if (!hasJoinedFamily) {
// Taro.redirectTo({ url: '/pages/Welcome/index' });
// }
})
</script>
......