hookehuyr

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

添加 WeRunAuth 组件用于微信运动数据授权和步数获取
在 Dashboard 页面集成微信运动数据展示和授权流程
更新 app.config 添加微信运动权限配置
...@@ -27,5 +27,6 @@ declare module 'vue' { ...@@ -27,5 +27,6 @@ declare module 'vue' {
27 RouterLink: typeof import('vue-router')['RouterLink'] 27 RouterLink: typeof import('vue-router')['RouterLink']
28 RouterView: typeof import('vue-router')['RouterView'] 28 RouterView: typeof import('vue-router')['RouterView']
29 TabBar: typeof import('./src/components/TabBar.vue')['default'] 29 TabBar: typeof import('./src/components/TabBar.vue')['default']
30 + WeRunAuth: typeof import('./src/components/WeRunAuth.vue')['default']
30 } 31 }
31 } 32 }
......
1 /* 1 /*
2 * @Date: 2025-06-28 10:33:00 2 * @Date: 2025-06-28 10:33:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-08-29 10:52:42 4 + * @LastEditTime: 2025-08-29 15:56:45
5 * @FilePath: /lls_program/src/app.config.js 5 * @FilePath: /lls_program/src/app.config.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -9,9 +9,9 @@ export default { ...@@ -9,9 +9,9 @@ export default {
9 pages: [ 9 pages: [
10 // 'pages/index/index', 10 // 'pages/index/index',
11 // 'pages/auth/index', 11 // 'pages/auth/index',
12 + 'pages/Welcome/index',
12 'pages/Dashboard/index', 13 'pages/Dashboard/index',
13 'pages/MyFamily/index', 14 'pages/MyFamily/index',
14 - 'pages/Welcome/index',
15 'pages/CreateFamily/index', 15 'pages/CreateFamily/index',
16 'pages/JoinFamily/index', 16 'pages/JoinFamily/index',
17 'pages/Activities/index', 17 'pages/Activities/index',
...@@ -47,6 +47,9 @@ export default { ...@@ -47,6 +47,9 @@ export default {
47 permission: { 47 permission: {
48 'scope.userLocation': { 48 'scope.userLocation': {
49 desc: '您的位置信息将用于活动打卡和位置相关服务' 49 desc: '您的位置信息将用于活动打卡和位置相关服务'
50 + },
51 + 'scope.werun': {
52 + desc: '您的微信运动数据将用于记录步数和健康统计'
50 } 53 }
51 } 54 }
52 } 55 }
......
1 +<template>
2 + <view class="werun-auth-container">
3 + <!-- 未授权状态 -->
4 + <view v-if="!isAuthorized" class="auth-prompt">
5 + <view class="px-5 py-6 bg-white rounded-xl shadow-md mx-4 mt-4">
6 + <view class="flex flex-col items-center justify-center py-8">
7 + <view class="mb-4">
8 + <image
9 + src="https://placehold.co/100x100/e2f3ff/0369a1?text=步数&font=roboto"
10 + class="w-20 h-20 rounded-full"
11 + />
12 + </view>
13 + <text class="text-lg font-medium mb-2">获取微信运动数据</text>
14 + <text class="text-gray-500 text-center mb-6 px-4">
15 + 授权后可查看您的步数信息,与家人一起记录健康生活
16 + </text>
17 + <view
18 + class="bg-blue-500 text-white px-8 py-3 rounded-full text-sm"
19 + @click="requestAuth"
20 + >
21 + 立即授权
22 + </view>
23 + </view>
24 + </view>
25 + </view>
26 +
27 + <!-- 已授权状态 - 显示步数相关内容 -->
28 + <view v-else>
29 + <slot :steps="todaySteps" :isLoading="isLoading" />
30 + </view>
31 + </view>
32 +</template>
33 +
34 +<script setup>
35 +import { ref, onMounted, defineEmits } from 'vue'
36 +import Taro from '@tarojs/taro'
37 +
38 +// 定义事件
39 +const emit = defineEmits(['auth-change', 'steps-update'])
40 +
41 +// 响应式数据
42 +const isAuthorized = ref(false)
43 +const todaySteps = ref(0)
44 +const isLoading = ref(false)
45 +
46 +/**
47 + * @description 检查微信运动授权状态
48 + */
49 +const checkAuthStatus = async () => {
50 + try {
51 + const authSetting = await Taro.getSetting()
52 + const hasWeRunAuth = authSetting.authSetting['scope.werun']
53 +
54 + isAuthorized.value = hasWeRunAuth === true
55 +
56 + // 通知父组件授权状态变化
57 + emit('auth-change', isAuthorized.value)
58 +
59 + // 如果已授权,获取步数数据
60 + if (isAuthorized.value) {
61 + await getWeRunData()
62 + }
63 + } catch (error) {
64 + console.error('检查授权状态失败:', error)
65 + isAuthorized.value = false
66 + emit('auth-change', false)
67 + }
68 +}
69 +
70 +/**
71 + * @description 请求微信运动授权
72 + */
73 +const requestAuth = async () => {
74 + try {
75 + isLoading.value = true
76 +
77 + // 请求微信运动授权
78 + await Taro.authorize({
79 + scope: 'scope.werun'
80 + })
81 +
82 + isAuthorized.value = true
83 + emit('auth-change', true)
84 +
85 + // 授权成功后获取步数数据
86 + await getWeRunData()
87 +
88 + Taro.showToast({
89 + title: '授权成功',
90 + icon: 'success'
91 + })
92 + } catch (error) {
93 + console.error('授权失败:', error)
94 +
95 + // 用户拒绝授权,引导到设置页面
96 + Taro.showModal({
97 + title: '授权提示',
98 + content: '需要获取微信运动数据权限才能使用步数功能,请在设置中开启',
99 + confirmText: '去设置',
100 + success: (res) => {
101 + if (res.confirm) {
102 + Taro.openSetting({
103 + success: (settingRes) => {
104 + if (settingRes.authSetting['scope.werun']) {
105 + checkAuthStatus()
106 + }
107 + }
108 + })
109 + }
110 + }
111 + })
112 + } finally {
113 + isLoading.value = false
114 + }
115 +}
116 +
117 +/**
118 + * @description 获取微信运动数据
119 + */
120 +const getWeRunData = async () => {
121 + try {
122 + isLoading.value = true
123 +
124 + // 先调用 wx.login 获取 code
125 + const loginRes = await Taro.login()
126 +
127 + // 获取微信运动数据
128 + const weRunRes = await Taro.getWeRunData()
129 +
130 + // 这里需要将加密数据发送到后端解密
131 + // 暂时使用模拟数据
132 + const mockSteps = Math.floor(Math.random() * 10000) + 1000
133 + todaySteps.value = mockSteps
134 +
135 + // 通知父组件步数更新
136 + emit('steps-update', todaySteps.value)
137 +
138 + console.log('微信运动数据获取成功:', {
139 + encryptedData: weRunRes.encryptedData,
140 + iv: weRunRes.iv,
141 + code: loginRes.code
142 + })
143 +
144 + // TODO: 实际项目中需要将 encryptedData, iv, code 发送到后端解密
145 + // const decryptedData = await api.decryptWeRunData({
146 + // encryptedData: weRunRes.encryptedData,
147 + // iv: weRunRes.iv,
148 + // code: loginRes.code
149 + // })
150 + // todaySteps.value = decryptedData.todaySteps
151 +
152 + } catch (error) {
153 + console.error('获取微信运动数据失败:', error)
154 +
155 + // 如果获取失败,使用默认值
156 + todaySteps.value = 0
157 + emit('steps-update', 0)
158 +
159 + Taro.showToast({
160 + title: '获取步数失败',
161 + icon: 'none'
162 + })
163 + } finally {
164 + isLoading.value = false
165 + }
166 +}
167 +
168 +/**
169 + * @description 刷新步数数据
170 + */
171 +const refreshSteps = async () => {
172 + if (isAuthorized.value) {
173 + await getWeRunData()
174 + }
175 +}
176 +
177 +// 暴露方法给父组件
178 +defineExpose({
179 + refreshSteps,
180 + checkAuthStatus
181 +})
182 +
183 +// 组件挂载时检查授权状态
184 +onMounted(() => {
185 + checkAuthStatus()
186 +})
187 +</script>
188 +
189 +<style scoped>
190 +.werun-auth-container {
191 + width: 100%;
192 +}
193 +
194 +.auth-prompt {
195 + text-align: center;
196 +}
197 +</style>
...\ No newline at end of file ...\ No newline at end of file
...@@ -13,56 +13,65 @@ ...@@ -13,56 +13,65 @@
13 </view> 13 </view>
14 </view> 14 </view>
15 15
16 - <!-- Today's steps section --> 16 + <!-- 微信步数授权组件 -->
17 - <view class="px-5 py-6 bg-white rounded-xl shadow-md mx-4 mt-4"> 17 + <WeRunAuth
18 - <view class="flex justify-between items-center mb-1"> 18 + ref="weRunAuthRef"
19 - <span class="text-gray-500">今日</span> 19 + @auth-change="handleAuthChange"
20 - </view> 20 + @steps-update="handleStepsUpdate"
21 - <view class="flex justify-between items-center"> 21 + >
22 - <view class="flex items-baseline"> 22 + <template #default="{ steps, isLoading }">
23 - <span class="text-4xl font-bold"> 23 + <!-- Today's steps section -->
24 - {{ todaySteps.toLocaleString() }} 24 + <view class="px-5 py-6 bg-white rounded-xl shadow-md mx-4 mt-4">
25 - </span> 25 + <view class="flex justify-between items-center mb-1">
26 - <span class="ml-1 text-gray-500">步</span> 26 + <span class="text-gray-500">今日</span>
27 - </view> 27 + </view>
28 - <view class="bg-blue-500 text-white px-4 py-2 rounded-full text-sm" @click="handleCollectAll"> 28 + <view class="flex justify-between items-center">
29 - 一键收取 29 + <view class="flex items-baseline">
30 + <span class="text-4xl font-bold">
31 + {{ isLoading ? '...' : steps.toLocaleString() }}
32 + </span>
33 + <span class="ml-1 text-gray-500">步</span>
34 + </view>
35 + <view class="bg-blue-500 text-white px-4 py-2 rounded-full text-sm" @click="handleCollectAll">
36 + 一键收取
37 + </view>
38 + </view>
30 </view> 39 </view>
31 - </view>
32 - </view>
33 40
34 - <!-- Points circles --> 41 + <!-- Points circles -->
35 - <view class="flex justify-between px-5 py-6 my-4 bg-white rounded-xl shadow-md mx-4"> 42 + <view class="flex justify-between px-5 py-6 my-4 bg-white rounded-xl shadow-md mx-4">
36 - <PointsCollector ref="pointsCollectorRef" height="30vh" /> 43 + <PointsCollector ref="pointsCollectorRef" height="30vh" />
37 - </view> 44 + </view>
38 45
39 - <!-- Photo button --> 46 + <!-- Photo button -->
40 - <view class="px-5 mb-4"> 47 + <view class="px-5 mb-4">
41 - <view @tap="openCamera" class="w-full bg-blue-500 text-white py-3 rounded-lg flex flex-col items-center justify-center"> 48 + <view @tap="openCamera" class="w-full bg-blue-500 text-white py-3 rounded-lg flex flex-col items-center justify-center">
42 - <view class="flex items-center justify-center"> 49 + <view class="flex items-center justify-center">
43 - <Photograph size="20" class="mr-2" /> 50 + <Photograph size="20" class="mr-2" />
44 - 拍照留念,奖励积分 51 + 拍照留念,奖励积分
52 + </view>
53 + </view>
45 </view> 54 </view>
46 - </view>
47 - </view>
48 55
49 - <!-- Family step ranking --> 56 + <!-- Family step ranking -->
50 - <view class="p-5 bg-white rounded-xl shadow-md mx-4"> 57 + <view class="p-5 bg-white rounded-xl shadow-md mx-4">
51 - <view class="flex justify-between items-center mb-4"> 58 + <view class="flex justify-between items-center mb-4">
52 - <h2 class="font-medium text-lg">今日家庭步数排行</h2> 59 + <h2 class="font-medium text-lg">今日家庭步数排行</h2>
53 - <span class="text-sm text-gray-500"> 60 + <span class="text-sm text-gray-500">
54 - 总计 {{ totalSteps.toLocaleString() }} 步 61 + 总计 {{ getTotalSteps(steps).toLocaleString() }} 步
55 - </span> 62 + </span>
56 - </view> 63 + </view>
57 - <view class="grid grid-cols-4 gap-2"> 64 + <view class="grid grid-cols-4 gap-2">
58 - <view v-for="member in familyMembers" :key="member.id" class="flex flex-col items-center"> 65 + <view v-for="member in familyMembers" :key="member.id" class="flex flex-col items-center">
59 - <image :src="member.avatar" :alt="member.name" class="w-16 h-16 rounded-full mb-1" /> 66 + <image :src="member.avatar" :alt="member.name" class="w-16 h-16 rounded-full mb-1" />
60 - <span class="text-sm text-gray-700"> 67 + <span class="text-sm text-gray-700">
61 - {{ member.steps.toLocaleString() }}步 68 + {{ member.steps.toLocaleString() }}步
62 - </span> 69 + </span>
70 + </view>
71 + </view>
63 </view> 72 </view>
64 - </view> 73 + </template>
65 - </view> 74 + </WeRunAuth>
66 75
67 <!-- Family album --> 76 <!-- Family album -->
68 <view class="p-5 mt-4 mb-6 bg-white rounded-xl shadow-md mx-4"> 77 <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'; ...@@ -93,9 +102,12 @@ import Taro from '@tarojs/taro';
93 import { Setting, Photograph, Right } from '@nutui/icons-vue-taro'; 102 import { Setting, Photograph, Right } from '@nutui/icons-vue-taro';
94 import BottomNav from '../../components/BottomNav.vue'; 103 import BottomNav from '../../components/BottomNav.vue';
95 import PointsCollector from '@/components/PointsCollector.vue' 104 import PointsCollector from '@/components/PointsCollector.vue'
105 +import WeRunAuth from '@/components/WeRunAuth.vue'
96 106
97 -const todaySteps = ref(2000); 107 +const todaySteps = ref(0);
108 +const isWeRunAuthorized = ref(false);
98 const pointsCollectorRef = ref(null) 109 const pointsCollectorRef = ref(null)
110 +const weRunAuthRef = ref(null)
99 111
100 /** 112 /**
101 * 触发积分收集组件的一键收取 113 * 触发积分收集组件的一键收取
...@@ -106,6 +118,33 @@ const handleCollectAll = () => { ...@@ -106,6 +118,33 @@ const handleCollectAll = () => {
106 } 118 }
107 } 119 }
108 120
121 +/**
122 + * 处理微信步数授权状态变化
123 + * @param {boolean} authorized - 授权状态
124 + */
125 +const handleAuthChange = (authorized) => {
126 + isWeRunAuthorized.value = authorized
127 + console.log('微信步数授权状态:', authorized)
128 +}
129 +
130 +/**
131 + * 处理步数数据更新
132 + * @param {number} steps - 步数
133 + */
134 +const handleStepsUpdate = (steps) => {
135 + todaySteps.value = steps
136 + console.log('步数更新:', steps)
137 +}
138 +
139 +/**
140 + * 计算总步数(包含用户步数和家庭成员步数)
141 + * @param {number} userSteps - 用户步数
142 + * @returns {number} 总步数
143 + */
144 +const getTotalSteps = (userSteps) => {
145 + return familyMembers.value.reduce((sum, member) => sum + member.steps, 0) + userSteps
146 +}
147 +
109 // Mock data for family members 148 // Mock data for family members
110 const familyMembers = ref([ 149 const familyMembers = ref([
111 { 150 {
...@@ -134,10 +173,7 @@ const familyMembers = ref([ ...@@ -134,10 +173,7 @@ const familyMembers = ref([
134 } 173 }
135 ]); 174 ]);
136 175
137 -// Calculate total family steps 176 +// 注意:totalSteps 计算逻辑已移至 getTotalSteps 方法中
138 -const totalSteps = computed(() =>
139 - familyMembers.value.reduce((sum, member) => sum + member.steps, 0) + todaySteps.value
140 -);
141 177
142 const handleSyncSteps = () => { 178 const handleSyncSteps = () => {
143 // In a real app, this would sync with a health API 179 // In a real app, this would sync with a health API
...@@ -162,10 +198,10 @@ const openCamera = () => { ...@@ -162,10 +198,10 @@ const openCamera = () => {
162 198
163 onMounted(() => { 199 onMounted(() => {
164 // TODO: 等待真实接口获取用户是否加入家庭 200 // TODO: 等待真实接口获取用户是否加入家庭
165 - const hasJoinedFamily = false; // Change to true to simulate having a family 201 + // const hasJoinedFamily = false; // Change to true to simulate having a family
166 202
167 - if (!hasJoinedFamily) { 203 + // if (!hasJoinedFamily) {
168 - Taro.redirectTo({ url: '/pages/Welcome/index' }); 204 + // Taro.redirectTo({ url: '/pages/Welcome/index' });
169 - } 205 + // }
170 }) 206 })
171 </script> 207 </script>
......