Authored by Bad

修复bug

@@ -32,10 +32,14 @@ SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn @@ -32,10 +32,14 @@ SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
32 ### SHOPRO_STATIC_URL = https://file.sheepjs.com 32 ### SHOPRO_STATIC_URL = https://file.sheepjs.com
33 33
34 # 前端 H5 访问域名 34 # 前端 H5 访问域名
35 -SHOPRO_H5_URL=http://127.0.0.1:3000 35 +SHOPRO_H5_URL=https://xunji.geaktec.com
36 36
37 # 是否开启直播 1 开启直播 | 0 关闭直播 37 # 是否开启直播 1 开启直播 | 0 关闭直播
38 SHOPRO_MPLIVE_ON=0 38 SHOPRO_MPLIVE_ON=0
39 39
40 # 租户ID 默认 1 40 # 租户ID 默认 1
41 SHOPRO_TENANT_ID=1 41 SHOPRO_TENANT_ID=1
  42 +
  43 +
  44 +# 默认头像
  45 +DEFAULT_AVATAR=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png
@@ -6,6 +6,8 @@ @@ -6,6 +6,8 @@
6 :placeholder="false" 6 :placeholder="false"
7 activeColor="#000" 7 activeColor="#000"
8 :fixed="true" 8 :fixed="true"
  9 + :bottom="0"
  10 + :safeAreaInsetBottom="false"
9 > 11 >
10 <up-tabbar-item 12 <up-tabbar-item
11 v-for="(item, index) in tabList" 13 v-for="(item, index) in tabList"
1 { 1 {
2 "name": "智能健身", 2 "name": "智能健身",
3 - "appid": "__UNI__7CAA18B", 3 + "appid": "__UNI__8F24C84",
4 "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。", 4 "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
5 "versionName": "2025.10", 5 "versionName": "2025.10",
6 "versionCode": "183", 6 "versionCode": "183",
@@ -100,7 +100,6 @@ @@ -100,7 +100,6 @@
100 "backgroundColor": "#1a1a1a" 100 "backgroundColor": "#1a1a1a"
101 } 101 }
102 }, 102 },
103 -  
104 { 103 {
105 "path": "pages/xunji/xunji-dongzuo-lianxi", 104 "path": "pages/xunji/xunji-dongzuo-lianxi",
106 "style": { 105 "style": {
@@ -113,14 +112,12 @@ @@ -113,14 +112,12 @@
113 "navigationBarTitleText": "新增超级组" 112 "navigationBarTitleText": "新增超级组"
114 } 113 }
115 }, 114 },
116 -  
117 { 115 {
118 "path": "pages/xunji/wode-jihua-paike", 116 "path": "pages/xunji/wode-jihua-paike",
119 "style": { 117 "style": {
120 "navigationBarTitleText": "我的计划详情的排课设置" 118 "navigationBarTitleText": "我的计划详情的排课设置"
121 } 119 }
122 }, 120 },
123 -  
124 { 121 {
125 "path": "pages/xunji/dongzuo-muluguanli", 122 "path": "pages/xunji/dongzuo-muluguanli",
126 "style": { 123 "style": {
@@ -131,7 +128,8 @@ @@ -131,7 +128,8 @@
131 { 128 {
132 "path": "pages/xunji/jihua-search", 129 "path": "pages/xunji/jihua-search",
133 "style": { 130 "style": {
134 - "navigationBarTitleText": "训记-计划-搜索" 131 + "navigationBarTitleText": "计划-搜索",
  132 + "navigationStyle": "default"
135 } 133 }
136 }, 134 },
137 { 135 {
@@ -185,7 +183,6 @@ @@ -185,7 +183,6 @@
185 "navigationBarTitleText": "我的常见问题" 183 "navigationBarTitleText": "我的常见问题"
186 } 184 }
187 }, 185 },
188 -  
189 { 186 {
190 "path": "pages/user/wode-guanyu-hongxing", 187 "path": "pages/user/wode-guanyu-hongxing",
191 "style": { 188 "style": {
@@ -11,9 +11,7 @@ @@ -11,9 +11,7 @@
11 <text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text> 11 <text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text>
12 </view> 12 </view>
13 </view> 13 </view>
14 - <view class="right">  
15 - <uni-icons type="right" size="16" color="#000" />  
16 - </view> 14 + <uni-icons type="right" size="16" color="#999" />
17 </view> 15 </view>
18 16
19 <!-- 未登录引导区 --> 17 <!-- 未登录引导区 -->
@@ -25,57 +23,8 @@ @@ -25,57 +23,8 @@
25 <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button> 23 <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button>
26 </view> 24 </view>
27 25
28 - <!-- -->  
29 - <!-- <view class="vip-banner" hover-class="opacity-hover" @click="goAddVip">  
30 - <view class="vip-info">  
31 - <uni-icons type="vip-filled" size="22" color="#f1c40f" />  
32 - <text class="vip-text">  
33 - {{ userInfo.deposit === 1 ? '鸿星·尊享会员 | 已开通' : '鸿星·会员 | 开通只需押金¥199' }}  
34 - </text>  
35 - </view>  
36 - <view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view>  
37 - </view> -->  
38 -  
39 - <!-- 课程状态快速入口 -->  
40 - <!-- <view class="section-card quick-entry">  
41 - <view v-for="entry in quickEntryConfig" :key="entry.type" class="entry-item" hover-class="opacity-hover"  
42 - @click="handleQuickEntry(entry.type)">  
43 - <text class="num">{{ userInfo[entry.key] || 0 }}</text>  
44 - <text class="label">{{ entry.label }}</text>  
45 - </view>  
46 - </view> -->  
47 -  
48 - <!-- 广告位 -->  
49 - <!-- <view class="banner-box" @click="goJiamen">  
50 - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png"  
51 - mode="aspectFill" class="banner-img" />  
52 - </view> -->  
53 -  
54 - <!-- 核心应用区 -->  
55 - <!-- <view class="section-card apply-section">  
56 - <view v-for="(item, index) in APPLY_CONFIG_LIST" :key="index" class="apply-item"  
57 - @click="authNavigateTo(item.url)">  
58 - <view class="icon-bg">  
59 - <image :src="item.icon" class="img" mode="aspectFit" />  
60 - </view>  
61 - <text class="text">{{ item.text }}</text>  
62 - </view>  
63 - </view> -->  
64 -  
65 - <!-- 资产账户网格区 -->  
66 - <!-- <view v-if="userStore.isLogin" class="section-card">  
67 - <view class="account-grid">  
68 - <view v-for="(acc, idx) in accountConfig" :key="idx" class="account-item" @click="authNavigateTo(acc.url)">  
69 - <text class="acc-lab">{{ acc.label }}</text>  
70 - <text class="acc-val">  
71 - {{ formatAccountValue(userInfo[acc.key], acc.isFloat) }}  
72 - <text v-if="acc.unit" class="unit">{{ acc.unit }}</text>  
73 - </text>  
74 - </view>  
75 - </view>  
76 - </view> -->  
77 26
78 - <!-- 功能矩阵九宫格 --> 27 + <!-- 功能 -->
79 <view class="section-card icon-grid-box"> 28 <view class="section-card icon-grid-box">
80 <view class="icon-grid"> 29 <view class="icon-grid">
81 <view v-for="(item, index) in FUNCTION_CONFIG_LIST" :key="index" class="icon-item" 30 <view v-for="(item, index) in FUNCTION_CONFIG_LIST" :key="index" class="icon-item"
@@ -95,15 +44,17 @@ @@ -95,15 +44,17 @@
95 </view> 44 </view>
96 45
97 <!-- 设置区 --> 46 <!-- 设置区 -->
98 - <view v-if="userStore.isLogin" class="section-card">  
99 - <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-shezhi')">  
100 - <text class="setting-text">个人设置</text> 47 + <view v-if="userStore.isLogin" class="section-card settings-section">
  48 + <view class="settings-title">个人设置</view>
  49 +
  50 + <view class="settings-item" @click="goToForgetPassword">
  51 + <text class="settings-label">修改密码</text>
101 <uni-icons type="right" size="14" color="#E0E0E0" /> 52 <uni-icons type="right" size="14" color="#E0E0E0" />
102 </view> 53 </view>
103 - <!-- <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')">  
104 - <text class="setting-text">隐私中心</text>  
105 - <uni-icons type="right" size="14" color="#E0E0E0" />  
106 - </view> --> 54 +
  55 + <view class="logout-wrap">
  56 + <button class="logout-btn" @click="handleLogout">退出登录</button>
  57 + </view>
107 </view> 58 </view>
108 59
109 <Tabbar /> 60 <Tabbar />
@@ -113,21 +64,23 @@ @@ -113,21 +64,23 @@
113 64
114 <script setup> 65 <script setup>
115 import { ref } from 'vue'; 66 import { ref } from 'vue';
  67 +import { onShow } from '@dcloudio/uni-app';
116 import UserApi from '@/sheep/api/member/user'; 68 import UserApi from '@/sheep/api/member/user';
117 import MemberApi from '@/sheep/api/member/member'; 69 import MemberApi from '@/sheep/api/member/member';
  70 +import AuthUtil from '@/sheep/api/member/auth';
118 import useUserStore from '@/sheep/store/user'; 71 import useUserStore from '@/sheep/store/user';
119 -import { onShow } from '@dcloudio/uni-app';  
120 -import TrainingFloating from '@/pages/TrainingFloating.vue'  
121 import { useTrainingStore } from '@/sheep/store/trainingStore'; 72 import { useTrainingStore } from '@/sheep/store/trainingStore';
  73 +import TrainingFloating from '@/pages/TrainingFloating.vue';
  74 +
122 const trainingStore = useTrainingStore(); 75 const trainingStore = useTrainingStore();
123 76
124 77
125 -// 响应式数据挂载 78 +// 状态
126 const userStore = useUserStore(); 79 const userStore = useUserStore();
127 const userInfo = ref({}); 80 const userInfo = ref({});
128 const memberLevelName = ref(''); 81 const memberLevelName = ref('');
129 82
130 -// 固定的 UI 配置 83 +// 配置
131 const APPLY_CONFIG_LIST = []; 84 const APPLY_CONFIG_LIST = [];
132 85
133 const FUNCTION_CONFIG_LIST = [ 86 const FUNCTION_CONFIG_LIST = [
@@ -155,31 +108,23 @@ const FUNCTION_CONFIG_LIST = [ @@ -155,31 +108,23 @@ const FUNCTION_CONFIG_LIST = [
155 ]; 108 ];
156 109
157 110
158 -// 课程计数状态配置映射  
159 const quickEntryConfig = [ 111 const quickEntryConfig = [
160 { type: 1, key: 'courseNum', label: '待上课' }, 112 { type: 1, key: 'courseNum', label: '待上课' },
161 { type: 2, key: 'courseWaitNum', label: '等待中' }, 113 { type: 2, key: 'courseWaitNum', label: '等待中' },
162 { type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' }, 114 { type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' },
163 ]; 115 ];
164 116
165 -// 资产账户字段清洗配置映射 (对应接口数据类型:number 与 integer) 117 +// 资产账户
  118 +
166 const accountConfig = []; 119 const accountConfig = [];
167 120
168 -/**  
169 - * JSDoc 核心资产数值洗涤函数  
170 - * @param {number|undefined} val 原始金钱/数量值  
171 - * @param {boolean} isFloat 是否需要保留两位小数  
172 - * @returns {string|number} 格式化后的安全渲染字符串  
173 - */  
174 const formatAccountValue = (val, isFloat) => { 121 const formatAccountValue = (val, isFloat) => {
175 if (val === undefined || val === null) return isFloat ? '0.00' : 0; 122 if (val === undefined || val === null) return isFloat ? '0.00' : 0;
176 return isFloat ? Number(val).toFixed(2) : Math.floor(val); 123 return isFloat ? Number(val).toFixed(2) : Math.floor(val);
177 }; 124 };
178 125
179 -/**  
180 - * 路由守卫拦截转发  
181 - * @param {string} url 目标绝对/相对地址  
182 - */ 126 +/** 路由守卫 */
  127 +
183 const authNavigateTo = (url) => { 128 const authNavigateTo = (url) => {
184 if (!userStore.isLogin) { 129 if (!userStore.isLogin) {
185 goLogin(); 130 goLogin();
@@ -191,51 +136,26 @@ const authNavigateTo = (url) => { @@ -191,51 +136,26 @@ const authNavigateTo = (url) => {
191 }); 136 });
192 }; 137 };
193 138
194 -/**  
195 - * 统一处理功能网格的点击分发  
196 - * @param {Object} item 节点配置项  
197 - */ 139 +/** 功能网格点击分发 */
  140 +
198 const handleGridItemClick = (item) => { 141 const handleGridItemClick = (item) => {
199 if (item.text === '联系客服') { 142 if (item.text === '联系客服') {
200 // #ifndef MP-WEIXIN 143 // #ifndef MP-WEIXIN
201 - // 兜底非微信小程序平台(如H5、App),可以正常走原来的普通客服页面路由  
202 authNavigateTo(item.url); 144 authNavigateTo(item.url);
203 // #endif 145 // #endif
204 return; 146 return;
205 } 147 }
206 -  
207 - // 其他正常功能,正常走路由拦截守卫  
208 authNavigateTo(item.url); 148 authNavigateTo(item.url);
209 }; 149 };
210 150
211 -/**  
212 - * 异步高内聚合并请求  
213 - * 解决因先后触发 setData 导致微信小程序底层 AppService 与 WebView 之间高频拥堵卡顿的问题  
214 - */ 151 +/** 拉取用户信息 */
  152 +
215 const fetchPageData = async () => { 153 const fetchPageData = async () => {
216 try { 154 try {
217 - // const [userRes, levelRes] = await Promise.all([  
218 - // UserApi.getUserInfo(),  
219 - // // MemberApi.getMemberLevel(),  
220 - // ]);  
221 -  
222 const userRes = await UserApi.getUserInfo(); 155 const userRes = await UserApi.getUserInfo();
223 -  
224 - console.log('后端返回个人信息:', userRes.data);  
225 - const rawUser = userRes.data || {};  
226 - userInfo.value = rawUser;  
227 -  
228 -  
229 - // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化  
230 - // const levels = levelRes.data?.detailList || [];  
231 - // if (rawUser.level !== undefined) {  
232 - // // const target = levels.find((item) => item.id === rawUser.level);  
233 - // // memberLevelName.value = target ? target.name : '';  
234 - // } else {  
235 - // // memberLevelName.value = '';  
236 - // } 156 + userInfo.value = userRes.data || {};
237 } catch (error) { 157 } catch (error) {
238 - console.error('[API Error] 拉取个人资产信息流失败:', error); 158 + console.error('[API Error] 拉取个人信息失败:', error);
239 } 159 }
240 }; 160 };
241 161
@@ -248,7 +168,33 @@ onShow(() => { @@ -248,7 +168,33 @@ onShow(() => {
248 } 168 }
249 }); 169 });
250 170
251 -// 路由跳转原子原子层 171 +// 设置
  172 +
  173 +const goToForgetPassword = () => {
  174 + uni.navigateTo({
  175 + url: '/pages7/pages/index/reset-password?isModify=true',
  176 + });
  177 +};
  178 +
  179 +const handleLogout = () => {
  180 + uni.showModal({
  181 + title: '提示',
  182 + content: '确定要退出登录吗?',
  183 + success: async (res) => {
  184 + if (!res.confirm) return;
  185 + try {
  186 + await AuthUtil.logout();
  187 + userStore.logout();
  188 + uni.reLaunch({ url: '/pages7/pages/index/login' });
  189 + } catch (err) {
  190 + console.error('[API Error] 退出登录失败:', err);
  191 + uni.showToast({ title: '退出失败', icon: 'none' });
  192 + }
  193 + },
  194 + });
  195 +};
  196 +
  197 +// 导航
252 const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' }); 198 const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' });
253 const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao'); 199 const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao');
254 const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan'); 200 const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan');
@@ -258,286 +204,215 @@ const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shang @@ -258,286 +204,215 @@ const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shang
258 </script> 204 </script>
259 205
260 <style scoped lang="scss"> 206 <style scoped lang="scss">
261 -$brand-color: #ff6b00;  
262 -$page-bg: #f8f8f8;  
263 - 207 +// 变量
  208 +// 主色
  209 +$color-primary: #ff6b00;
  210 +$color-danger: #ff4757;
  211 +$color-danger-active: #e63946;
  212 +$color-badge: #ff4d4f;
  213 +
  214 +// 文字
  215 +$color-text-primary: #333333;
  216 +$color-text-secondary: #555555;
  217 +$color-text-tertiary: #999999;
  218 +$color-text-white: #ffffff;
  219 +
  220 +// 背景
  221 +$color-bg-page: #f7f7f8;
  222 +$color-bg-card: #ffffff;
  223 +$color-bg-avatar: #f0f0f0;
  224 +$color-bg-hover: #fcfcfc;
  225 +
  226 +// 边框
  227 +$color-border: #f0f0f0;
  228 +
  229 +// 圆角
  230 +$radius-card: 24rpx;
  231 +$radius-button: 16rpx;
  232 +$radius-round: 50%;
  233 +$radius-tag: 8rpx;
  234 +$radius-badge: 16rpx;
  235 +
  236 +// 间距
  237 +$gap-section: 24rpx;
  238 +$gap-page-h: 28rpx;
  239 +$padding-card: 30rpx 28rpx;
  240 +
  241 +// 字号
  242 +$font-title: 32rpx;
  243 +$font-body: 28rpx;
  244 +$font-caption: 24rpx;
  245 +$font-small: 22rpx;
  246 +$font-mini: 18rpx;
  247 +
  248 +// 效果
  249 +$shadow-card: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
  250 +$transition-tap: background-color 0.2s ease, opacity 0.2s ease;
  251 +
  252 +// 页面
264 .my-page { 253 .my-page {
265 min-height: 100vh; 254 min-height: 100vh;
266 - background-color: $page-bg;  
267 - padding: 20rpx 28rpx calc(40rpx + env(safe-area-inset-bottom));  
268 - /* 解决部分 iOS 底部 Tabbar 高度塌陷 */ 255 + background-color: $color-bg-page;
  256 + padding: 20rpx $gap-page-h calc(40rpx + env(safe-area-inset-bottom));
269 box-sizing: border-box; 257 box-sizing: border-box;
270 258
  259 + // 卡片
271 .section-card { 260 .section-card {
272 - background: #ffffff;  
273 - border-radius: 24rpx;  
274 - padding: 30rpx 20rpx;  
275 - margin-bottom: 24rpx;  
276 - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03); 261 + background: $color-bg-card;
  262 + border-radius: $radius-card;
  263 + padding: $padding-card;
  264 + margin-bottom: $gap-section;
  265 + box-shadow: $shadow-card;
277 } 266 }
278 267
  268 + // 登录头部
279 .user-header { 269 .user-header {
280 display: flex; 270 display: flex;
281 align-items: center; 271 align-items: center;
282 - padding: 34rpx 30rpx; 272 + padding: 36rpx $gap-page-h;
283 273
284 .avatar { 274 .avatar {
285 width: 110rpx; 275 width: 110rpx;
286 height: 110rpx; 276 height: 110rpx;
287 - border-radius: 50%;  
288 - background: #f0f0f0;  
289 - border: 4rpx solid #fff; 277 + border-radius: $radius-round;
  278 + background: $color-bg-avatar;
  279 + border: 4rpx solid $color-bg-card;
  280 + flex-shrink: 0;
290 } 281 }
291 282
292 .info-content { 283 .info-content {
293 margin-left: 24rpx; 284 margin-left: 24rpx;
294 flex: 1; 285 flex: 1;
  286 + min-width: 0;
295 287
296 .name-row { 288 .name-row {
297 display: flex; 289 display: flex;
298 flex-direction: column; 290 flex-direction: column;
  291 + gap: 8rpx;
299 292
300 .nickname { 293 .nickname {
301 - font-size: 34rpx;  
302 - font-weight: bold;  
303 - color: #333; 294 + font-size: 36rpx;
  295 + font-weight: 600;
  296 + color: $color-text-primary;
304 } 297 }
305 298
306 .tag { 299 .tag {
307 - font-size: 20rpx;  
308 - color: $brand-color;  
309 - 300 + display: inline-block;
  301 + font-size: $font-mini;
  302 + color: $color-primary;
310 padding: 4rpx 12rpx; 303 padding: 4rpx 12rpx;
311 - border-radius: 8rpx;  
312 -  
313 - vertical-align: middle; 304 + border-radius: $radius-tag;
  305 + background: rgba($color-primary, 0.08);
314 } 306 }
315 } 307 }
316 } 308 }
317 309
318 - .right {  
319 - display: flex;  
320 - align-items: center;  
321 - gap: 60rpx;  
322 -  
323 - .img {  
324 - width: 60rpx;  
325 - height: 60rpx;  
326 - }  
327 - }  
328 } 310 }
329 311
  312 + // 未登录引导
330 .login-guide-box { 313 .login-guide-box {
331 display: flex; 314 display: flex;
332 justify-content: space-between; 315 justify-content: space-between;
333 align-items: center; 316 align-items: center;
  317 + padding: 36rpx $gap-page-h;
334 318
  319 + .guide-txt {
335 .title { 320 .title {
336 - font-size: 32rpx;  
337 - font-weight: bold;  
338 - color: #333; 321 + font-size: $font-title;
  322 + font-weight: 600;
  323 + color: $color-text-primary;
  324 + display: block;
339 } 325 }
340 326
341 .desc { 327 .desc {
342 - font-size: 22rpx;  
343 - color: #999;  
344 - margin-top: 4rpx; 328 + font-size: $font-small;
  329 + color: $color-text-tertiary;
  330 + margin-top: 6rpx;
345 display: block; 331 display: block;
346 } 332 }
  333 + }
347 334
348 .login-btn { 335 .login-btn {
349 margin: 0; 336 margin: 0;
350 - background: $brand-color;  
351 - color: #fff;  
352 - font-size: 24rpx; 337 + flex-shrink: 0;
  338 + background: $color-primary;
  339 + color: $color-text-white;
  340 + font-size: $font-caption;
353 height: 64rpx; 341 height: 64rpx;
354 line-height: 64rpx; 342 line-height: 64rpx;
355 border-radius: 32rpx; 343 border-radius: 32rpx;
356 - padding: 0 30rpx; 344 + padding: 0 32rpx;
  345 + transition: $transition-tap;
357 346
358 &::after { 347 &::after {
359 border: none; 348 border: none;
360 } 349 }
361 350
362 - /* 清理小程序 button 默认黑边线 */  
363 - }  
364 - }  
365 -  
366 - .vip-banner {  
367 - background: #2b2b2b;  
368 - border-radius: 20rpx;  
369 - padding: 24rpx 30rpx;  
370 - display: flex;  
371 - justify-content: space-between;  
372 - align-items: center;  
373 - margin-bottom: 24rpx;  
374 -  
375 - .vip-info {  
376 - display: flex;  
377 - align-items: center;  
378 - }  
379 -  
380 - .vip-text {  
381 - color: #f1c40f;  
382 - font-size: 24rpx;  
383 - margin-left: 14rpx;  
384 - }  
385 -  
386 - .vip-btn {  
387 - background: linear-gradient(90deg, #f1c40f, #f39c12);  
388 - color: #333;  
389 - font-size: 20rpx;  
390 - font-weight: bold;  
391 - padding: 8rpx 20rpx;  
392 - border-radius: 30rpx;  
393 - }  
394 - }  
395 -  
396 - .quick-entry {  
397 - display: flex;  
398 - justify-content: space-around;  
399 -  
400 - .entry-item {  
401 - text-align: center;  
402 -  
403 - .num {  
404 - font-size: 38rpx;  
405 - font-weight: bold;  
406 - color: #333;  
407 - }  
408 -  
409 - .label {  
410 - font-size: 22rpx;  
411 - color: #999;  
412 - margin-top: 4rpx;  
413 - display: block;  
414 - }  
415 - }  
416 - }  
417 -  
418 - .banner-box {  
419 - height: 160rpx;  
420 - margin-bottom: 24rpx;  
421 - border-radius: 20rpx;  
422 - overflow: hidden;  
423 -  
424 - .banner-img {  
425 - width: 100%;  
426 - height: 100%;  
427 - }  
428 - }  
429 -  
430 - .apply-section {  
431 - display: grid;  
432 - grid-template-columns: repeat(5, 1fr);  
433 -  
434 - .apply-item {  
435 - display: flex;  
436 - flex-direction: column;  
437 - align-items: center;  
438 -  
439 - .icon-bg {  
440 - width: 80rpx;  
441 - height: 80rpx;  
442 - background: #f9f9f9;  
443 - border-radius: 20rpx;  
444 - display: flex;  
445 - align-items: center;  
446 - justify-content: center;  
447 - margin-bottom: 12rpx;  
448 -  
449 - .img {  
450 - width: 44rpx;  
451 - height: 44rpx;  
452 - }  
453 - }  
454 -  
455 - .text {  
456 - font-size: 22rpx;  
457 - color: #666;  
458 - }  
459 - }  
460 - }  
461 -  
462 - .account-grid {  
463 - display: grid;  
464 - grid-template-columns: repeat(4, 1fr);  
465 - row-gap: 34rpx;  
466 -  
467 - .account-item {  
468 - display: flex;  
469 - flex-direction: column;  
470 - align-items: center;  
471 -  
472 - .acc-val {  
473 - font-size: 30rpx;  
474 - font-weight: bold;  
475 - color: #333;  
476 -  
477 - .unit {  
478 - font-size: 20rpx;  
479 - font-weight: normal;  
480 - color: #666;  
481 - margin-left: 2rpx;  
482 - }  
483 - }  
484 -  
485 - .acc-lab {  
486 - font-size: 22rpx;  
487 - color: #999;  
488 - margin-top: 4rpx; 351 + &:active {
  352 + opacity: 0.85;
489 } 353 }
490 } 354 }
491 } 355 }
492 356
  357 + // 功能区
493 .icon-grid { 358 .icon-grid {
494 display: grid; 359 display: grid;
495 grid-template-columns: repeat(4, 1fr); 360 grid-template-columns: repeat(4, 1fr);
496 - row-gap: 40rpx; 361 + row-gap: 36rpx;
497 362
498 .icon-item { 363 .icon-item {
499 display: flex; 364 display: flex;
500 flex-direction: column; 365 flex-direction: column;
501 align-items: center; 366 align-items: center;
502 position: relative; 367 position: relative;
  368 + transition: $transition-tap;
  369 +
  370 + &:active {
  371 + opacity: 0.7;
  372 + }
503 373
504 .icon-img-wrap { 374 .icon-img-wrap {
505 position: relative; 375 position: relative;
  376 + width: 90rpx;
  377 + height: 90rpx;
  378 + display: flex;
  379 + align-items: center;
  380 + justify-content: center;
506 margin-bottom: 12rpx; 381 margin-bottom: 12rpx;
  382 + background: #f5f6f8;
  383 + border-radius: 22rpx;
507 384
508 .icon-img { 385 .icon-img {
509 - width: 46rpx;  
510 - height: 46rpx; 386 + width: 44rpx;
  387 + height: 44rpx;
511 } 388 }
512 389
513 .badge { 390 .badge {
514 position: absolute; 391 position: absolute;
515 - top: -12rpx;  
516 - right: -42rpx;  
517 - /* 适当拓宽右移,容纳后端多字文本 */  
518 - background: #ff4d4f;  
519 - color: #fff;  
520 - font-size: 18rpx; 392 + top: -10rpx;
  393 + right: -36rpx;
  394 + background: $color-badge;
  395 + color: $color-text-white;
  396 + font-size: $font-mini;
521 padding: 2rpx 10rpx; 397 padding: 2rpx 10rpx;
522 - border-radius: 16rpx; 398 + border-radius: $radius-badge;
523 white-space: nowrap; 399 white-space: nowrap;
524 } 400 }
525 401
526 - /* 未读状态小红点 */  
527 .dot-badge { 402 .dot-badge {
528 position: absolute; 403 position: absolute;
529 - top: -4rpx;  
530 - right: -4rpx; 404 + top: -2rpx;
  405 + right: -2rpx;
531 width: 14rpx; 406 width: 14rpx;
532 height: 14rpx; 407 height: 14rpx;
533 - background: #ff4d4f;  
534 - border-radius: 50%; 408 + background: $color-badge;
  409 + border-radius: $radius-round;
535 } 410 }
536 } 411 }
537 412
538 .icon-text { 413 .icon-text {
539 - font-size: 24rpx;  
540 - color: #555; 414 + font-size: $font-caption;
  415 + color: $color-text-secondary;
541 } 416 }
542 417
543 .mp-contact-overlay-btn { 418 .mp-contact-overlay-btn {
@@ -547,12 +422,10 @@ $page-bg: #f8f8f8; @@ -547,12 +422,10 @@ $page-bg: #f8f8f8;
547 width: 100% !important; 422 width: 100% !important;
548 height: 100% !important; 423 height: 100% !important;
549 opacity: 0 !important; 424 opacity: 0 !important;
550 - /* 核心:完全透明 */  
551 border: none !important; 425 border: none !important;
552 padding: 0 !important; 426 padding: 0 !important;
553 margin: 0 !important; 427 margin: 0 !important;
554 z-index: 10; 428 z-index: 10;
555 - /* 确保盖在图表和文字的最上方 */  
556 429
557 &::after { 430 &::after {
558 border: none !important; 431 border: none !important;
@@ -561,29 +434,58 @@ $page-bg: #f8f8f8; @@ -561,29 +434,58 @@ $page-bg: #f8f8f8;
561 } 434 }
562 } 435 }
563 436
564 - .setting-item { 437 + // 设置区
  438 + .settings-section {
  439 + padding: 30rpx $gap-page-h;
  440 +
  441 + .settings-title {
  442 + font-size: 30rpx;
  443 + font-weight: 600;
  444 + color: $color-text-primary;
  445 + margin-bottom: 8rpx;
  446 + }
  447 + }
  448 +
  449 + .settings-item {
565 display: flex; 450 display: flex;
566 justify-content: space-between; 451 justify-content: space-between;
567 align-items: center; 452 align-items: center;
568 - padding: 24rpx 0;  
569 - border-bottom: 1rpx solid #f9f9f9; 453 + padding: 28rpx 0;
  454 + border-bottom: 1rpx solid $color-border;
570 455
571 - &:last-child {  
572 - border-bottom: none; 456 + .settings-label {
  457 + font-size: $font-body;
  458 + color: $color-text-primary;
573 } 459 }
574 -  
575 - .setting-text {  
576 - font-size: 28rpx;  
577 - color: #444;  
578 } 460 }
  461 +
  462 + .logout-wrap {
  463 + margin-top: 40rpx;
  464 +
  465 + .logout-btn {
  466 + width: 100%;
  467 + background-color: $color-danger;
  468 + color: $color-text-white;
  469 + border: none;
  470 + border-radius: $radius-button;
  471 + padding: 26rpx 0;
  472 + font-size: 30rpx;
  473 + font-weight: 500;
  474 + transition: $transition-tap;
  475 +
  476 + &::after {
  477 + border: none;
579 } 478 }
580 479
581 - .opacity-hover {  
582 - opacity: 0.7; 480 + &:active {
  481 + background-color: $color-danger-active;
  482 + }
  483 + }
583 } 484 }
584 485
  486 + // 触摸反馈
585 .card-hover { 487 .card-hover {
586 - background-color: #fcfcfc; 488 + background-color: $color-bg-hover;
587 } 489 }
588 } 490 }
589 </style> 491 </style>
1 <template> 1 <template>
2 <up-popup :show="actionShow" mode="bottom" minHeight="90vh" @close="actionShow = false" bgColor="#1a1a1a"> 2 <up-popup :show="actionShow" mode="bottom" minHeight="90vh" @close="actionShow = false" bgColor="#1a1a1a">
3 <scroll-view class="action-container" scroll-y> 3 <scroll-view class="action-container" scroll-y>
4 - <view class="header" v-if="type == 1"> 4 + <view class="header" v-if="type === 1">
5 <view class="explain"> 5 <view class="explain">
6 <!-- 3D 固定图片 --> 6 <!-- 3D 固定图片 -->
7 <image v-if="modeTab === 0" :src="actionDetail.url3dAnimation" class="media-content" mode="aspectFill" /> 7 <image v-if="modeTab === 0" :src="actionDetail.url3dAnimation" class="media-content" mode="aspectFill" />
@@ -32,15 +32,15 @@ @@ -32,15 +32,15 @@
32 </view> 32 </view>
33 33
34 <view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial"> 34 <view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial">
35 - <view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab == 0 }" 35 + <view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab === 0 }"
36 @click="switchModeTab(0)"> 36 @click="switchModeTab(0)">
37 3D 37 3D
38 </view> 38 </view>
39 - <view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab == 1 }" 39 + <view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab === 1 }"
40 @click="switchModeTab(1)"> 40 @click="switchModeTab(1)">
41 真人 41 真人
42 </view> 42 </view>
43 - <view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab == 2 }" 43 + <view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab === 2 }"
44 @click="switchModeTab(2)"> 44 @click="switchModeTab(2)">
45 讲解 45 讲解
46 </view> 46 </view>
@@ -56,17 +56,18 @@ @@ -56,17 +56,18 @@
56 <!-- 只有个人创建的动作,才有这个编辑按钮--> 56 <!-- 只有个人创建的动作,才有这个编辑按钮-->
57 <template v-if="actionDetail?.isSystem === 0"> 57 <template v-if="actionDetail?.isSystem === 0">
58 <view class="edit-icon" @click="edit"> 58 <view class="edit-icon" @click="edit">
59 - <uni-icons type="more" size="24" color="#fff"></uni-icons> 59 + <up-icon name="more-dot-fill" size="24" color="#fff" />
60 </view> 60 </view>
61 </template> 61 </template>
62 - 62 + <!-- #ifdef H5 -->
63 <button class="share-btn" open-type="share"> 63 <button class="share-btn" open-type="share">
64 - <uni-icons type="paperplane" size="24" color="#fff"></uni-icons> 64 + <up-icon name="share-fill" size="24" color="#fff" />
65 </button> 65 </button>
  66 + <!-- #endif -->
66 <up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24" 67 <up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24"
67 @click="toggleCollect"> 68 @click="toggleCollect">
68 </up-icon> 69 </up-icon>
69 - <!-- <FavoriteBtn :id="actionId" :type="type" /> --> 70 +
70 </view> 71 </view>
71 </view> 72 </view>
72 <!-- 要点,历史,平替动作标签 --> 73 <!-- 要点,历史,平替动作标签 -->
@@ -106,16 +107,16 @@ @@ -106,16 +107,16 @@
106 </view> 107 </view>
107 </view> 108 </view>
108 109
109 - <template v-if="type == 1"> 110 + <template v-if="type === 1">
110 <textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea> 111 <textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea>
111 </template> 112 </template>
112 113
113 - <template v-if="type == 2"> 114 + <template v-if="type === 2">
114 <textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea> 115 <textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea>
115 </template> 116 </template>
116 117
117 </view> 118 </view>
118 - <view class="memo-box" v-if="type == 1"> 119 + <view class="memo-box" v-if="type === 1">
119 <view class="memo-title"> 120 <view class="memo-title">
120 <view class="section-title">步骤</view> 121 <view class="section-title">步骤</view>
121 <view class="pencil-icon"> 122 <view class="pencil-icon">
@@ -168,18 +169,7 @@ @@ -168,18 +169,7 @@
168 </view> 169 </view>
169 </view> 170 </view>
170 </view> 171 </view>
171 - <!-- 2 历史 -->  
172 <view v-if="contentTab === 1" class="tab-pane slide-up"> 172 <view v-if="contentTab === 1" class="tab-pane slide-up">
173 - <!-- <view class="stat-card highlight">  
174 - <text class="label">最长时长</text>  
175 - <text class="value">01:02:03</text>  
176 - <text class="date">记录于 2026/03/04</text>  
177 - </view> -->  
178 -  
179 - <!-- <view class="chart-container" v-if="chartData && chartData.series">  
180 - <qiun-data-charts type="line" :opts="chartOpts" :chartData="chartData" />  
181 - </view> -->  
182 -  
183 <view class="history-list"> 173 <view class="history-list">
184 <template v-if="historyList && historyList.length > 0"> 174 <template v-if="historyList && historyList.length > 0">
185 <view class="history-item" v-for="item in historyList" :key="item.id"> 175 <view class="history-item" v-for="item in historyList" :key="item.id">
@@ -216,7 +206,7 @@ @@ -216,7 +206,7 @@
216 <view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up"> 206 <view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up">
217 <view class="substitute-list"> 207 <view class="substitute-list">
218 <view class="sub-item" v-for="item in alternativeActions" :key="item.id" @click="openActionItem(item)"> 208 <view class="sub-item" v-for="item in alternativeActions" :key="item.id" @click="openActionItem(item)">
219 - <image :src="item.url3dAnimation || lostImage" mode="aspectFill" class="img" /> 209 + <image :src="item.url3dAnimation" mode="aspectFill" class="img" />
220 <view class="sub-info"> 210 <view class="sub-info">
221 <text class="name">{{ item.name }}</text> 211 <text class="name">{{ item.name }}</text>
222 212
@@ -238,47 +228,35 @@ @@ -238,47 +228,35 @@
238 </template> 228 </template>
239 229
240 <script setup> 230 <script setup>
241 -import { onMounted, ref, nextTick } from 'vue'; 231 +import { ref, nextTick } from 'vue';
  232 +import dayjs from 'dayjs';
242 import ExercisesApi from '@/sheep/api/motion/exercises'; 233 import ExercisesApi from '@/sheep/api/motion/exercises';
243 import beizhu from '@/pages/xunji/components/beizhu.vue'; 234 import beizhu from '@/pages/xunji/components/beizhu.vue';
244 import SupersetsApi from '@/sheep/api/motion/supersets'; 235 import SupersetsApi from '@/sheep/api/motion/supersets';
245 import TrainingApi from '@/sheep/api/Training/traininghistory'; 236 import TrainingApi from '@/sheep/api/Training/traininghistory';
246 import { onShareAppMessage } from '@dcloudio/uni-app'; 237 import { onShareAppMessage } from '@dcloudio/uni-app';
247 -import { useTrainingStore } from '@/sheep/store/trainingStore' 238 +import { useTrainingStore } from '@/sheep/store/trainingStore';
248 239
  240 +const trainingStore = useTrainingStore();
  241 +const lostImage = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
249 242
250 -const trainingStore = useTrainingStore()  
251 -  
252 -// 静态配置  
253 -  
254 -const alternativeActions = ref([]); // 平替动作列表接口数据  
255 -const loadedHistory = ref(false)  
256 -const loadedAlternative = ref(false)  
257 -  
258 -// 响应式状态  
259 const actionShow = ref(false); 243 const actionShow = ref(false);
260 const modeTab = ref(0); 244 const modeTab = ref(0);
261 const contentTab = ref(0); 245 const contentTab = ref(0);
262 const isFavorite = ref(false); 246 const isFavorite = ref(false);
263 -  
264 -// 记录当前动作的详细数据  
265 const actionDetail = ref({}); 247 const actionDetail = ref({});
266 -// 记录当前动作的id  
267 const actionId = ref(0); 248 const actionId = ref(0);
268 -// 记录是超级组还是动作组 1=动作组,2=超级组  
269 const type = ref(0); 249 const type = ref(0);
270 const showBeizhuRef = ref(null); 250 const showBeizhuRef = ref(null);
  251 +const historyList = ref([]);
  252 +const alternativeActions = ref([]);
  253 +const loadedHistory = ref(false);
  254 +const loadedAlternative = ref(false);
271 255
272 -const historyList = ref([]); // 训练历史列表接口数据  
273 -const lostImage =  
274 - 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';  
275 -  
276 -// 切换图表模式  
277 const switchModeTab = (index) => { 256 const switchModeTab = (index) => {
278 modeTab.value = index; 257 modeTab.value = index;
279 }; 258 };
280 259
281 -// 判断链接是否为视频链接  
282 const isVideoUrl = (url) => { 260 const isVideoUrl = (url) => {
283 if (!url) return false; 261 if (!url) return false;
284 const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm']; 262 const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
@@ -286,13 +264,10 @@ const isVideoUrl = (url) => { @@ -286,13 +264,10 @@ const isVideoUrl = (url) => {
286 return videoExts.some(ext => lowerUrl.includes(ext)); 264 return videoExts.some(ext => lowerUrl.includes(ext));
287 }; 265 };
288 266
289 -// 跳转到视频播放的页面  
290 const playVideo = (url) => { 267 const playVideo = (url) => {
291 - uni.navigateTo({  
292 - url: '/pages4/pages/xunji/xunji-shiping?url=' + url,  
293 - }); 268 + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-shiping?url=' + url });
294 }; 269 };
295 -// 获取动作收藏状态 270 +
296 const checkExerciseFavorited = async () => { 271 const checkExerciseFavorited = async () => {
297 try { 272 try {
298 const res = await ExercisesApi.checkExerciseFavorited(actionId.value); 273 const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
@@ -308,21 +283,16 @@ const toggleCollect = async () => { @@ -308,21 +283,16 @@ const toggleCollect = async () => {
308 try { 283 try {
309 const status = isFavorite.value ? 0 : 1; 284 const status = isFavorite.value ? 0 : 1;
310 console.log('actionId.value=', actionId.value, 'status=', status); 285 console.log('actionId.value=', actionId.value, 'status=', status);
311 - if (type.value == 1) {  
312 - console.log('动作收藏传递的参数', actionId.value, status); 286 + if (type.value === 1) {
313 await ExercisesApi.toggleFavorite(actionId.value, status); 287 await ExercisesApi.toggleFavorite(actionId.value, status);
314 -  
315 } else { 288 } else {
316 - console.log('超级组收藏传递的参数', actionId.value, status);  
317 await SupersetsApi.toggleFavorite(actionId.value, status); 289 await SupersetsApi.toggleFavorite(actionId.value, status);
318 } 290 }
319 isFavorite.value = !isFavorite.value; 291 isFavorite.value = !isFavorite.value;
320 uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' }); 292 uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' });
321 } catch (err) { 293 } catch (err) {
322 console.log(err); 294 console.log(err);
323 - // 请求失败强制回滚UI  
324 - // isFavorite.value = originState;  
325 - uni.showToast({ title: '操作失败,请重试', icon: 'none' }); 295 +
326 } 296 }
327 }; 297 };
328 298
@@ -375,26 +345,16 @@ const open = async (id, typeData) => { @@ -375,26 +345,16 @@ const open = async (id, typeData) => {
375 contentTab.value = 0; 345 contentTab.value = 0;
376 console.log('动作详情页面接收id和类型:', actionId.value, type.value); 346 console.log('动作详情页面接收id和类型:', actionId.value, type.value);
377 // 如何判断是动作还是超级组 347 // 如何判断是动作还是超级组
378 - if (typeData == 1) { 348 + if (typeData === 1) {
379 await loadexercisedetail(actionId.value); 349 await loadexercisedetail(actionId.value);
380 - // await loadAlternativeActions(actionId.value);  
381 - console.log('进入动作收藏检查接口');  
382 await checkExerciseFavorited(); 350 await checkExerciseFavorited();
383 -  
384 -  
385 } else { 351 } else {
386 await loadsuperdetail(actionId.value); 352 await loadsuperdetail(actionId.value);
387 - console.log('进入超级组收藏检查接口');  
388 await checkSupersetFavorited(); 353 await checkSupersetFavorited();
389 } 354 }
390 355
391 - console.log('请求参数actionId/typeData', actionId.value, type.value);  
392 -  
393 - // 重置加载训练历史列表和平替动作列表标记  
394 - loadedHistory.value = false  
395 - loadedAlternative.value = false  
396 -  
397 - // loadTrainHistoryDetail(actionId.value, type.value); 356 + loadedHistory.value = false;
  357 + loadedAlternative.value = false;
398 actionShow.value = true; 358 actionShow.value = true;
399 }; 359 };
400 // 打开备注弹窗 360 // 打开备注弹窗
@@ -413,7 +373,7 @@ const handleNoteSave = async (content) => { @@ -413,7 +373,7 @@ const handleNoteSave = async (content) => {
413 console.log('type.value=', type.value); 373 console.log('type.value=', type.value);
414 374
415 try { 375 try {
416 - if (type.value == 2) { 376 + if (type.value === 2) {
417 await SupersetsApi.addNotes({ 377 await SupersetsApi.addNotes({
418 supersetsId: actionId.value, 378 supersetsId: actionId.value,
419 content: content, 379 content: content,
@@ -428,7 +388,7 @@ const handleNoteSave = async (content) => { @@ -428,7 +388,7 @@ const handleNoteSave = async (content) => {
428 } catch (e) { 388 } catch (e) {
429 console.log(e); 389 console.log(e);
430 } finally { 390 } finally {
431 - if (type.value == 1) { 391 + if (type.value === 1) {
432 await loadexercisedetail(actionId.value) 392 await loadexercisedetail(actionId.value)
433 } else { 393 } else {
434 await loadsuperdetail(actionId.value) 394 await loadsuperdetail(actionId.value)
@@ -451,7 +411,7 @@ onShareAppMessage((res) => { @@ -451,7 +411,7 @@ onShareAppMessage((res) => {
451 title: actionDetail.value.name || '健身动作分享', // 分享标题 411 title: actionDetail.value.name || '健身动作分享', // 分享标题
452 // path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径 412 // path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径
453 path: `/pages/xunji/xunji?currentTab=${3}`, 413 path: `/pages/xunji/xunji?currentTab=${3}`,
454 - imageUrl: actionDetail.value.urlImage || lostImage, // 分享图片 414 + imageUrl: actionDetail.value.urlImage, // 分享图片
455 }; 415 };
456 }); 416 });
457 417
@@ -517,53 +477,18 @@ const loadTrainHistoryDetail = async (id, type) => { @@ -517,53 +477,18 @@ const loadTrainHistoryDetail = async (id, type) => {
517 // 格式化时间 477 // 格式化时间
518 const formatDate = (dateArr) => { 478 const formatDate = (dateArr) => {
519 if (!Array.isArray(dateArr) || dateArr.length < 3) return ''; 479 if (!Array.isArray(dateArr) || dateArr.length < 3) return '';
520 - const [year, month, day] = dateArr;  
521 - const date = new Date(year, month - 1, day);  
522 - const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];  
523 - const week = weekArr[date.getDay()];  
524 - return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')} ${week}`; 480 + return dayjs(new Date(dateArr[0], dateArr[1] - 1, dateArr[2])).format('YYYY/MM/DD dd');
525 }; 481 };
526 482
527 -// 格式化每组数据:自动拼接 weight/reps/duration/distance  
528 const formatSetData = (set) => { 483 const formatSetData = (set) => {
529 const parts = []; 484 const parts = [];
530 -  
531 - // 重量  
532 - if (set.weight != null && set.weight !== '') {  
533 - parts.push(`${set.weight}kg`);  
534 - }  
535 -  
536 - // 次数  
537 - if (set.reps != null && set.reps !== '') {  
538 - parts.push(`${set.reps}次`);  
539 - }  
540 -  
541 - // 时长(自动转 时:分:秒)  
542 - if (set.duration != null && set.duration !== '') {  
543 - parts.push(formatTime(set.duration));  
544 - }  
545 -  
546 - // 距离  
547 - if (set.distance != null && set.distance !== '') {  
548 - parts.push(`${set.distance}m`);  
549 - }  
550 - 485 + if (set.weight != null && set.weight !== '') parts.push(`${set.weight}kg`);
  486 + if (set.reps != null && set.reps !== '') parts.push(`${set.reps}次`);
  487 + if (set.duration != null && set.duration !== '') parts.push(dayjs.duration(set.duration, 'seconds').format('HH:mm:ss'));
  488 + if (set.distance != null && set.distance !== '') parts.push(`${set.distance}m`);
551 return parts.length > 0 ? parts.join(' × ') : '无数据'; 489 return parts.length > 0 ? parts.join(' × ') : '无数据';
552 }; 490 };
553 491
554 -// 新增:秒数转 00:00:00 格式  
555 -const formatTime = (seconds) => {  
556 - const h = Math.floor(seconds / 3600);  
557 - const m = Math.floor((seconds % 3600) / 60);  
558 - const s = seconds % 60;  
559 -  
560 - const hh = h > 0 ? String(h).padStart(2, '0') + ':' : '';  
561 - const mm = String(m).padStart(2, '0') + ':';  
562 - const ss = String(s).padStart(2, '0');  
563 -  
564 - return hh + mm + ss;  
565 -};  
566 -  
567 const edit = () => { 492 const edit = () => {
568 uni.navigateTo({ 493 uni.navigateTo({
569 url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}` 494 url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}`
@@ -577,8 +502,6 @@ const openActionItem = (item) => { @@ -577,8 +502,6 @@ const openActionItem = (item) => {
577 }; 502 };
578 503
579 defineExpose({ open }); 504 defineExpose({ open });
580 -  
581 -onMounted(() => { });  
582 </script> 505 </script>
583 506
584 <style lang="scss" scoped> 507 <style lang="scss" scoped>
@@ -772,7 +695,6 @@ onMounted(() => { }); @@ -772,7 +695,6 @@ onMounted(() => { });
772 } 695 }
773 } 696 }
774 697
775 - // 要点板块样式  
776 .video-grid { 698 .video-grid {
777 display: grid; 699 display: grid;
778 grid-template-columns: 1fr 1fr; 700 grid-template-columns: 1fr 1fr;
@@ -816,7 +738,6 @@ onMounted(() => { }); @@ -816,7 +738,6 @@ onMounted(() => { });
816 color: #fff; 738 color: #fff;
817 height: 160rpx; 739 height: 160rpx;
818 border-radius: 16rpx; 740 border-radius: 16rpx;
819 - // padding: 20rpx;  
820 font-size: 30rpx; 741 font-size: 30rpx;
821 border: 0; 742 border: 0;
822 } 743 }
@@ -827,7 +748,6 @@ onMounted(() => { }); @@ -827,7 +748,6 @@ onMounted(() => { });
827 flex-direction: column; 748 flex-direction: column;
828 gap: 16rpx; 749 gap: 16rpx;
829 background: #464646; 750 background: #464646;
830 - // padding: 20rpx;  
831 box-sizing: border-box; 751 box-sizing: border-box;
832 border-radius: 16rpx; 752 border-radius: 16rpx;
833 753
@@ -877,7 +797,6 @@ onMounted(() => { }); @@ -877,7 +797,6 @@ onMounted(() => { });
877 color: #fff; 797 color: #fff;
878 } 798 }
879 799
880 - // 历史记录样式  
881 .stat-card { 800 .stat-card {
882 background: linear-gradient(135deg, #333, #222); 801 background: linear-gradient(135deg, #333, #222);
883 padding: 40rpx; 802 padding: 40rpx;
@@ -933,8 +852,6 @@ onMounted(() => { }); @@ -933,8 +852,6 @@ onMounted(() => { });
933 } 852 }
934 853
935 .tag { 854 .tag {
936 - // font-size: 20rpx;  
937 - // background: #444;  
938 margin: 5rpx 5rpx; 855 margin: 5rpx 5rpx;
939 border-radius: 4rpx; 856 border-radius: 4rpx;
940 } 857 }
@@ -948,7 +865,6 @@ onMounted(() => { }); @@ -948,7 +865,6 @@ onMounted(() => { });
948 margin-top: 20rpx; 865 margin-top: 20rpx;
949 866
950 .chip { 867 .chip {
951 - // background: #333;  
952 padding: 8rpx 20rpx; 868 padding: 8rpx 20rpx;
953 font-size: 24rpx; 869 font-size: 24rpx;
954 display: flex; 870 display: flex;
@@ -965,7 +881,6 @@ onMounted(() => { }); @@ -965,7 +881,6 @@ onMounted(() => { });
965 } 881 }
966 } 882 }
967 883
968 - // 平替动作样式  
969 .sub-item { 884 .sub-item {
970 display: flex; 885 display: flex;
971 align-items: center; 886 align-items: center;
@@ -1024,7 +939,6 @@ onMounted(() => { }); @@ -1024,7 +939,6 @@ onMounted(() => { });
1024 } 939 }
1025 } 940 }
1026 941
1027 -// 动画  
1028 .slide-up { 942 .slide-up {
1029 animation: slideUp 0.4s ease-out; 943 animation: slideUp 0.4s ease-out;
1030 } 944 }
@@ -24,138 +24,83 @@ @@ -24,138 +24,83 @@
24 </template> 24 </template>
25 25
26 <script setup> 26 <script setup>
  27 +import dayjs from 'dayjs';
27 import { ref, computed, watch } from 'vue'; 28 import { ref, computed, watch } from 'vue';
28 29
29 const emit = defineEmits(['dateTypeChange', 'dateRangeChange']); 30 const emit = defineEmits(['dateTypeChange', 'dateRangeChange']);
30 31
31 -// 当前日期类型: 'week' | 'month' 32 +// 当前日期类型与基准日期
32 const currentDateType = ref('week'); 33 const currentDateType = ref('week');
33 -// 基准日期,用于计算范围  
34 -const baseDate = ref(new Date());  
35 -  
36 -// 格式化日期辅助函数 YYYY/MM/DD  
37 -const formatDate = (date) => {  
38 - const y = date.getFullYear();  
39 - const m = String(date.getMonth() + 1).padStart(2, '0');  
40 - const d = String(date.getDate()).padStart(2, '0');  
41 - return `${y}/${m}/${d}`;  
42 -};  
43 -// 格式化日期辅助函数 YYYY-MM-DD  
44 -const formatDateYmd = (date) => {  
45 - const y = date.getFullYear();  
46 - const m = String(date.getMonth() + 1).padStart(2, '0');  
47 - const d = String(date.getDate()).padStart(2, '0');  
48 - return `${y}-${m}-${d}`;  
49 -}; 34 +const baseDate = ref(dayjs());
50 35
51 -const currentDateRange = computed(() => {  
52 - const current = new Date(baseDate.value);  
53 - let start, end; 36 +// ==================== 公共日期范围计算 ====================
54 37
  38 +/** 根据当前类型计算周期起止日期(dayjs 实例) */
  39 +const periodRange = computed(() => {
  40 + const d = baseDate.value;
55 if (currentDateType.value === 'week') { 41 if (currentDateType.value === 'week') {
56 - const day = current.getDay();  
57 - const diffToMonday = day === 0 ? -6 : 1 - day;  
58 - start = new Date(current);  
59 - start.setDate(current.getDate() + diffToMonday);  
60 - end = new Date(start);  
61 - end.setDate(start.getDate() + 6);  
62 - } else {  
63 - start = new Date(current.getFullYear(), current.getMonth(), 1);  
64 - end = new Date(current.getFullYear(), current.getMonth() + 1, 0); 42 + const dayOfWeek = d.day(); // 0=周日, 1=周一, ...
  43 + const start = d.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day');
  44 + const end = start.add(6, 'day');
  45 + return { start, end };
65 } 46 }
66 -  
67 - return {  
68 - start: formatDateYmd(start),  
69 - end: formatDateYmd(end)  
70 - }; 47 + // month
  48 + return { start: d.startOf('month'), end: d.endOf('month') };
71 }); 49 });
72 50
73 -// 计算显示的日期范围文本  
74 -const dateRangeText = computed(() => {  
75 - const current = new Date(baseDate.value);  
76 -  
77 - if (currentDateType.value === 'week') {  
78 - // 计算本周的周一和周日  
79 - const day = current.getDay();  
80 - const diffToMonday = day === 0 ? -6 : 1 - day; // 调整到周一  
81 -  
82 - const startOfWeek = new Date(current);  
83 - startOfWeek.setDate(current.getDate() + diffToMonday);  
84 -  
85 - const endOfWeek = new Date(startOfWeek);  
86 - endOfWeek.setDate(startOfWeek.getDate() + 6); 51 +// ==================== 计算属性 ====================
87 52
88 - return `${formatDate(startOfWeek)} - ${formatDate(endOfWeek)}`;  
89 - } else {  
90 - // 计算本月的一号和最后一天  
91 - const startOfMonth = new Date(current.getFullYear(), current.getMonth(), 1);  
92 - const endOfMonth = new Date(current.getFullYear(), current.getMonth() + 1, 0); 53 +/** 对外暴露的日期范围(YYYY-MM-DD) */
  54 +const currentDateRange = computed(() => ({
  55 + start: periodRange.value.start.format('YYYY-MM-DD'),
  56 + end: periodRange.value.end.format('YYYY-MM-DD'),
  57 +}));
93 58
94 - return `${formatDate(startOfMonth)} - ${formatDate(endOfMonth)}`;  
95 - } 59 +/** 显示的日期范围文本 "YYYY/MM/DD - YYYY/MM/DD" */
  60 +const dateRangeText = computed(() => {
  61 + const { start, end } = periodRange.value;
  62 + return `${start.format('YYYY/MM/DD')} - ${end.format('YYYY/MM/DD')}`;
96 }); 63 });
97 64
98 -// 新增: 计算下一个日期是否不可用(即是否为未来日期) 65 +/** 下一周期是否超过今天(超出则隐藏前进按钮) */
99 const isNextDisabled = computed(() => { 66 const isNextDisabled = computed(() => {
100 - const newDate = new Date(baseDate.value);  
101 - if (currentDateType.value === 'week') {  
102 - newDate.setDate(newDate.getDate() + 7);  
103 - } else {  
104 - newDate.setMonth(newDate.getMonth() + 1);  
105 - }  
106 -  
107 - // 重置时分秒进行比较  
108 - const now = new Date();  
109 - now.setHours(0, 0, 0, 0);  
110 - const checkDate = new Date(newDate);  
111 - checkDate.setHours(0, 0, 0, 0);  
112 -  
113 - return checkDate > now; 67 + const nextBase = currentDateType.value === 'week'
  68 + ? baseDate.value.add(7, 'day')
  69 + : baseDate.value.add(1, 'month');
  70 + return nextBase.startOf('day').isAfter(dayjs().startOf('day'));
114 }); 71 });
115 72
116 -// 切换周/月 73 +// ==================== 事件处理 ====================
  74 +
117 const switchDateType = (type) => { 75 const switchDateType = (type) => {
118 currentDateType.value = type; 76 currentDateType.value = type;
119 emit('dateTypeChange', type); 77 emit('dateTypeChange', type);
120 }; 78 };
121 79
122 -// 上一段时间  
123 const handlePrev = () => { 80 const handlePrev = () => {
124 - const newDate = new Date(baseDate.value);  
125 - if (currentDateType.value === 'week') {  
126 - newDate.setDate(newDate.getDate() - 7);  
127 - } else {  
128 - newDate.setMonth(newDate.getMonth() - 1);  
129 - }  
130 - baseDate.value = newDate; 81 + baseDate.value = currentDateType.value === 'week'
  82 + ? baseDate.value.subtract(7, 'day')
  83 + : baseDate.value.subtract(1, 'month');
131 }; 84 };
132 85
133 -// 下一段时间  
134 const handleNext = () => { 86 const handleNext = () => {
135 - // 虽然按钮已隐藏,但保留逻辑判断以防直接调用或其他边界情况  
136 - if (isNextDisabled.value) {  
137 - return;  
138 - }  
139 -  
140 - const newDate = new Date(baseDate.value);  
141 - if (currentDateType.value === 'week') {  
142 - newDate.setDate(newDate.getDate() + 7);  
143 - } else {  
144 - newDate.setMonth(newDate.getMonth() + 1);  
145 - }  
146 -  
147 - baseDate.value = newDate; 87 + if (isNextDisabled.value) return;
  88 + baseDate.value = currentDateType.value === 'week'
  89 + ? baseDate.value.add(7, 'day')
  90 + : baseDate.value.add(1, 'month');
148 }; 91 };
149 92
  93 +// ==================== 双向绑定 ====================
  94 +
150 watch( 95 watch(
151 [baseDate, currentDateType], 96 [baseDate, currentDateType],
152 () => { 97 () => {
153 emit('dateRangeChange', { 98 emit('dateRangeChange', {
154 startDate: currentDateRange.value.start, 99 startDate: currentDateRange.value.start,
155 - endDate: currentDateRange.value.end 100 + endDate: currentDateRange.value.end,
156 }); 101 });
157 }, 102 },
158 - { immediate: true } 103 + { immediate: true },
159 ); 104 );
160 </script> 105 </script>
161 106
@@ -180,10 +125,6 @@ watch( @@ -180,10 +125,6 @@ watch(
180 padding: 6rpx; 125 padding: 6rpx;
181 box-sizing: border-box; 126 box-sizing: border-box;
182 127
183 - // #ifdef H5  
184 - margin-top: 30px;  
185 - // #endif  
186 -  
187 .tab-item { 128 .tab-item {
188 display: flex; 129 display: flex;
189 align-items: center; 130 align-items: center;
1 <template> 1 <template>
2 - <view class="exercise-page" @click="closeAddMenu">  
3 - <!-- 搜索框 -->  
4 - <view class="search-bar">  
5 - <!-- <view class="search">  
6 - <uni-icons type="search" size="18"></uni-icons>  
7 - <input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" />  
8 - </view> -->  
9 - <view class="add-btn" @click.stop="addExercise">  
10 - <uni-icons type="plus" size="35" color="#333"></uni-icons>  
11 - <view class="floating-menu" v-show="addshow" @click.stop>  
12 - <view class="menu-item">  
13 - <uni-icons type="plus" size="24" color="#333"></uni-icons>  
14 - <text class="menu-text" @click="addnewmotion">新增动作</text> 2 + <view class="exercise-page">
  3 + <!-- 顶部操作栏:搜索 + 快捷新增 -->
  4 + <view class="top-bar">
  5 + <view class="search-wrapper">
  6 + <up-search v-model="searchKeyword" placeholder="搜索动作名称" :showAction="false" shape="round" bgColor="#f3f4f6"
  7 + placeholderColor="#9ca3af" searchIconColor="#6b7280" @clear="searchKeyword = ''" />
15 </view> 8 </view>
16 - <view class="menu-item">  
17 - <uni-icons type="link" size="24" color="#333"></uni-icons>  
18 - <text class="menu-text" @click="addnewsupersets">新增超级组</text> 9 +
  10 + <view class="add-btn-wrapper">
  11 + <view class="add-icon-btn" @click.stop="addShow = !addShow">
  12 + <up-icon name="plus" size="20" color="#111827" bold />
19 </view> 13 </view>
20 - <view class="menu-item">  
21 - <uni-icons type="list" size="24" color="#333"></uni-icons>  
22 - <text class="menu-text" @click="actionmanagement">动作目录管理</text> 14 +
  15 + <!-- 下拉浮动菜单 -->
  16 + <view class="floating-menu" v-if="addShow" @click.stop>
  17 + <view class="menu-item" @click="addnewmotion">
  18 + <up-icon name="plus-circle" size="18" color="#10b981" />
  19 + <text class="menu-text">新增动作</text>
23 </view> 20 </view>
  21 + <view class="menu-item" @click="addnewsupersets">
  22 + <up-icon name="attach" size="18" color="#3b82f6" />
  23 + <text class="menu-text">新增超级组</text>
24 </view> 24 </view>
  25 + <view class="menu-item" @click="actionmanagement">
  26 + <up-icon name="list-dot" size="18" color="#f59e0b" />
  27 + <text class="menu-text">动作目录管理</text>
25 </view> 28 </view>
26 </view> 29 </view>
27 30
28 - <!-- 浮动菜单 --> 31 + <view class="menu-mask" v-if="addShow" @click="addShow = false" />
  32 + </view>
  33 + </view>
29 34
30 - <!-- 左右布局 --> 35 + <!-- 主体左右双栏布局 -->
31 <view class="layout-container"> 36 <view class="layout-container">
32 - <scroll-view scroll-y class="left-nav" enable-flex>  
33 - <view class="nav-item" @click="handleCollectClick('collect')" :class="{ active: activeNav === 'collect' }">  
34 - 收藏 37 + <!-- 左侧一级分类导航 -->
  38 + <scroll-view scroll-y class="left-nav" :show-scrollbar="false">
  39 + <view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick">
  40 + <up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>
  41 + <text class="nav-text">收藏</text>
35 </view> 42 </view>
  43 +
36 <view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }" 44 <view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }"
37 @click="switchNav(nav.id)"> 45 @click="switchNav(nav.id)">
38 - {{ nav.name }} 46 + <text class="nav-text">{{ nav.name }}</text>
39 </view> 47 </view>
40 </scroll-view> 48 </scroll-view>
41 49
42 - <!-- 小动作列表 -->  
43 - <scroll-view scroll-y class="right-content" enable-flex>  
44 - <view class="tip" v-if="motionPart.length > 0">  
45 - <view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view>  
46 - <view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }"  
47 - @click="handlePartClick(item.id)"> 50 + <!-- 右侧二级筛选与动作网格内容 -->
  51 + <scroll-view scroll-y class="right-content" :show-scrollbar="false">
  52 + <!-- 二级子部位 Chips 筛选条 -->
  53 + <scroll-view scroll-x class="sub-part-scroll" v-if="motionPart.length > 0">
  54 + <view class="sub-part-wrapper">
  55 + <view class="part-chip" :class="{ active: activeMotionPart === '' }" @click="handlePartClick('')">
  56 + 全部
  57 + </view>
  58 + <view v-for="item in motionPart" :key="item.id" class="part-chip"
  59 + :class="{ active: activeMotionPart === item.id }" @click="handlePartClick(item.id)">
48 {{ item.name }} 60 {{ item.name }}
49 </view> 61 </view>
50 </view> 62 </view>
  63 + </scroll-view>
51 64
  65 + <!-- 动作与超级组网格卡片区 -->
52 <view class="exercise-grid"> 66 <view class="exercise-grid">
53 - <view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0">  
54 - <view class="equipment-list">  
55 - <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">  
56 - <view class="equipment-name"> {{ item.equipmentName }} </view>  
57 - <view class="action-list">  
58 - <view class="action-item" v-for="e in item.exercises" :key="e.id">  
59 - <image :src="e.url3dAnimation" mode="aspectFill" lazy-load class="action-img"  
60 - @click="goToDetail(e.id, 1)"></image>  
61 - <view class="action-name">{{ e.name }}</view>  
62 - <view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view> 67 + <template v-if="displayExercises.length > 0 || superGroupInfo.length > 0">
  68 + <!-- 器械分组列表 -->
  69 + <view class="equipment-group" v-for="item in displayExercises" :key="item.equipmentId">
  70 + <view class="equipment-title">
  71 + <view class="title-bar"></view>
  72 + <text>{{ item.equipmentName || '通用器械' }}</text>
63 </view> 73 </view>
  74 +
  75 + <view class="action-grid">
  76 + <view class="action-card" v-for="e in item.exercises" :key="e.id" @click="goToDetail(e.id, 1)">
  77 + <view class="image-wrapper">
  78 + <image :src="e.url3dAnimation" mode="aspectFil" lazy-load class="action-img"></image>
  79 + <view class="reps-badge" v-if="e.trainingReps">
  80 + {{ e.trainingReps }}次
64 </view> 81 </view>
65 </view> 82 </view>
66 -  
67 - <view class="supers" v-if="superGroupInfo.length > 0">  
68 - <view class="supers-name"> 超级组 </view>  
69 - <view class="super" v-for="item in superGroupInfo" :key="item.id" @click="goToDetail(item.id, 2)">  
70 - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"  
71 - mode="aspectFill" lazy-load class="super-img"></image>  
72 - <view class="right">  
73 - <view class="super-name">{{ item.name }}</view>  
74 - <view class="parts">{{ item.primaryMuscles.join() }}</view> 83 + <view class="action-info">
  84 + <text class="action-name">{{ e.name }}</text>
75 </view> 85 </view>
76 </view> 86 </view>
77 </view> 87 </view>
  88 + </view>
  89 +
  90 + <!-- 超级组展示模块 -->
  91 + <view class="supers-group" v-if="superGroupInfo.length > 0">
  92 + <view class="equipment-title">
  93 + <view class="title-bar blue"></view>
  94 + <text>超级组</text>
  95 + </view>
78 96
79 - <!-- <view class="add-super">  
80 - <view class="header">  
81 - <up-icon name="plus" color="#000" size="15" bold></up-icon>  
82 - <text>添加自定义动作</text> 97 + <view class="super-card" v-for="item in superGroupInfo" :key="item.id" @click="goToDetail(item.id, 2)">
  98 + <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
  99 + mode="aspectFill" lazy-load class="super-img"></image>
  100 + <view class="super-info">
  101 + <text class="super-name">{{ item.name }}</text>
  102 + <text class="super-muscles" v-if="item.primaryMuscles">
  103 + {{ Array.isArray(item.primaryMuscles) ? item.primaryMuscles.join(' · ') : item.primaryMuscles }}
  104 + </text>
83 </view> 105 </view>
84 - <view class="tip">添加官方库没有的动作</view>  
85 - </view> --> 106 + <up-icon name="arrow-right" size="14" color="#9ca3af"></up-icon>
86 </view> 107 </view>
87 </view> 108 </view>
  109 + </template>
88 110
89 - <!-- 空状态 -->  
90 - <view class="empty-state" v-else>  
91 - <text class="empty-text">暂无动作数据</text> 111 + <!-- 优雅的空状态 -->
  112 + <view class="empty-wrapper" v-else>
  113 + <up-empty mode="data" text="暂无相关动作数据"></up-empty>
92 </view> 114 </view>
93 </view> 115 </view>
  116 +
  117 +
94 </scroll-view> 118 </scroll-view>
95 </view> 119 </view>
96 120
97 - <!-- 详情弹窗 --> 121 + <!-- 动作详情弹窗组件 -->
98 <dongzuoXianqing ref="actionDetailRef" /> 122 <dongzuoXianqing ref="actionDetailRef" />
99 -  
100 - <!-- 底部安全区占位(关键!解决不同设备适配) -->  
101 - <view class="safe-area-bottom"></view>  
102 </view> 123 </view>
103 </template> 124 </template>
104 125
105 <script setup> 126 <script setup>
106 -import { ref, onMounted, nextTick } from 'vue'; 127 +import { ref, onMounted, computed } from 'vue';
107 import ExercisesApi from '@/sheep/api/motion/exercises'; 128 import ExercisesApi from '@/sheep/api/motion/exercises';
108 import SupersetsApi from '@/sheep/api/motion/supersets'; 129 import SupersetsApi from '@/sheep/api/motion/supersets';
109 import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue'; 130 import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue';
@@ -111,29 +132,35 @@ import { useActionStore } from '@/sheep/store/action'; @@ -111,29 +132,35 @@ import { useActionStore } from '@/sheep/store/action';
111 132
112 const actionStore = useActionStore(); 133 const actionStore = useActionStore();
113 134
114 -// 响应式数据  
115 const activeNav = ref(''); 135 const activeNav = ref('');
116 const navItems = ref([]); 136 const navItems = ref([]);
117 const exercises = ref([]); 137 const exercises = ref([]);
118 const superGroupInfo = ref([]); 138 const superGroupInfo = ref([]);
119 const motionPart = ref([]); 139 const motionPart = ref([]);
120 const activeMotionPart = ref(''); 140 const activeMotionPart = ref('');
121 -const currentCategoryId = ref('');  
122 -  
123 -const allExercises = ref([]);  
124 -  
125 -const addshow = ref(false);  
126 -  
127 -const superFavoriteList = ref([]);  
128 - 141 +const searchKeyword = ref('');
  142 +const addShow = ref(false);
129 const actionDetailRef = ref(null); 143 const actionDetailRef = ref(null);
130 144
131 -// 关闭菜单  
132 -const closeAddMenu = () => {  
133 - addshow.value = false;  
134 -}; 145 +const displayExercises = computed(() => {
  146 + if (!searchKeyword.value.trim()) {
  147 + return exercises.value;
  148 + }
  149 + const kw = searchKeyword.value.toLowerCase().trim();
  150 +
  151 + return exercises.value
  152 + .map((group) => {
  153 + const filtered = (group.exercises || []).filter((ex) =>
  154 + ex.name && ex.name.toLowerCase().includes(kw)
  155 + );
  156 + return {
  157 + ...group,
  158 + exercises: filtered,
  159 + };
  160 + })
  161 + .filter((group) => group.exercises && group.exercises.length > 0);
  162 +});
135 163
136 -// 部位筛选  
137 const handlePartClick = async (id) => { 164 const handlePartClick = async (id) => {
138 try { 165 try {
139 activeMotionPart.value = id; 166 activeMotionPart.value = id;
@@ -143,37 +170,40 @@ const handlePartClick = async (id) => { @@ -143,37 +170,40 @@ const handlePartClick = async (id) => {
143 }); 170 });
144 exercises.value = exerciseRes.data || []; 171 exercises.value = exerciseRes.data || [];
145 } catch (e) { 172 } catch (e) {
146 - console.error('加载动作失败:', e); 173 + console.error('加载子部位动作失败:', e);
147 } 174 }
148 }; 175 };
149 176
150 -// 收藏  
151 -const handleCollectClick = async (id) => {  
152 - activeNav.value = id; 177 +const handleCollectClick = () => {
  178 + activeNav.value = 'collect';
  179 + activeMotionPart.value = '';
  180 + motionPart.value = [];
153 loadCollectList(); 181 loadCollectList();
154 loadSuperFavoriteList(); 182 loadSuperFavoriteList();
155 }; 183 };
156 -// 动作收藏 184 +
  185 +// 加载动作收藏列表
157 const loadCollectList = async () => { 186 const loadCollectList = async () => {
158 try { 187 try {
159 const res = await ExercisesApi.getFavoriteExercises(); 188 const res = await ExercisesApi.getFavoriteExercises();
160 exercises.value = res.data || []; 189 exercises.value = res.data || [];
161 } catch (err) { 190 } catch (err) {
162 - console.error('加载动作收藏失败', err); 191 + console.error('加载动作收藏失败:', err);
163 } 192 }
164 }; 193 };
165 -// 超级组收藏 194 +
  195 +// 加载超级组收藏列表
166 const loadSuperFavoriteList = async () => { 196 const loadSuperFavoriteList = async () => {
167 try { 197 try {
168 const res = await SupersetsApi.getFavoriteSuperset(); 198 const res = await SupersetsApi.getFavoriteSuperset();
169 superGroupInfo.value = res.data || []; 199 superGroupInfo.value = res.data || [];
170 } catch (err) { 200 } catch (err) {
171 - superFavoriteList.value = [];  
172 - console.error('加载超级组收藏失败', err); 201 + superGroupInfo.value = [];
  202 + console.error('加载超级组收藏失败:', err);
173 } 203 }
174 }; 204 };
175 205
176 -// 加载 206 +// 加载
177 const loadCategories = async () => { 207 const loadCategories = async () => {
178 try { 208 try {
179 await actionStore.getloadCategories(); 209 await actionStore.getloadCategories();
@@ -187,26 +217,27 @@ const loadCategories = async () => { @@ -187,26 +217,27 @@ const loadCategories = async () => {
187 } 217 }
188 }; 218 };
189 219
190 -// 加载动作 220 +// 加载动作列表
191 const loadExercises = async (categoriesId) => { 221 const loadExercises = async (categoriesId) => {
192 try { 222 try {
193 const partRes = await ExercisesApi.getMotionPart(categoriesId); 223 const partRes = await ExercisesApi.getMotionPart(categoriesId);
194 motionPart.value = partRes.data || []; 224 motionPart.value = partRes.data || [];
195 - console.log(motionPart.value, 'motionPart.value');  
196 225
197 const exerciseRes = await ExercisesApi.getexercises({ categoriesId }); 226 const exerciseRes = await ExercisesApi.getexercises({ categoriesId });
198 - exercises.value = exerciseRes.data; 227 + exercises.value = exerciseRes.data || [];
199 } catch (error) { 228 } catch (error) {
200 console.error('加载动作失败:', error); 229 console.error('加载动作失败:', error);
201 } 230 }
202 }; 231 };
203 232
204 -// 切换导航 233 +// 切换一级导航
205 const switchNav = (id) => { 234 const switchNav = (id) => {
206 if (activeNav.value === id) return; 235 if (activeNav.value === id) return;
207 activeNav.value = id; 236 activeNav.value = id;
208 activeMotionPart.value = ''; 237 activeMotionPart.value = '';
209 - if (id == 'super') { 238 + searchKeyword.value = '';
  239 +
  240 + if (id === 'super') {
210 exercises.value = []; 241 exercises.value = [];
211 motionPart.value = []; 242 motionPart.value = [];
212 loadsupersetsinfo(); 243 loadsupersetsinfo();
@@ -216,21 +247,7 @@ const switchNav = (id) => { @@ -216,21 +247,7 @@ const switchNav = (id) => {
216 } 247 }
217 }; 248 };
218 249
219 -// 搜索  
220 -const onSearch = (e) => {  
221 - const searchText = e.detail.value.trim();  
222 - if (!currentCategoryId.value) return;  
223 -  
224 - searchText ? searchExercises(searchText) : loadExercises(currentCategoryId.value);  
225 -};  
226 -  
227 -const searchExercises = (keyword) => {  
228 - const key = keyword.toLowerCase().trim();  
229 - exercises.value = allExercises.value.filter((item) => {  
230 - return item.name && item.name.toLowerCase().includes(key);  
231 - });  
232 -};  
233 - 250 +// 加载超级组列表
234 const loadsupersetsinfo = async () => { 251 const loadsupersetsinfo = async () => {
235 try { 252 try {
236 const response = await SupersetsApi.getsupersets(); 253 const response = await SupersetsApi.getsupersets();
@@ -240,31 +257,22 @@ const loadsupersetsinfo = async () => { @@ -240,31 +257,22 @@ const loadsupersetsinfo = async () => {
240 } 257 }
241 }; 258 };
242 259
243 -// 页面跳转  
244 const goToDetail = (id, type) => { 260 const goToDetail = (id, type) => {
245 - console.log('type=', type);  
246 - nextTick(() => {  
247 - actionDetailRef.value.open(id, type);  
248 - });  
249 -};  
250 -  
251 -const addExercise = () => {  
252 - addshow.value = !addshow.value; 261 + actionDetailRef.value?.open(id, type);
253 }; 262 };
254 263
255 const addnewmotion = () => { 264 const addnewmotion = () => {
  265 + addShow.value = false;
256 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' }); 266 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });
257 }; 267 };
258 268
259 const addnewsupersets = () => { 269 const addnewsupersets = () => {
260 - uni.navigateTo({  
261 - url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(  
262 - JSON.stringify(navItems.value),  
263 - )}`,  
264 - }); 270 + addShow.value = false;
  271 + uni.navigateTo({ url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(JSON.stringify(navItems.value))}` });
265 }; 272 };
266 273
267 const actionmanagement = () => { 274 const actionmanagement = () => {
  275 + addShow.value = false;
268 uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' }); 276 uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' });
269 }; 277 };
270 278
@@ -272,352 +280,324 @@ onMounted(() => { @@ -272,352 +280,324 @@ onMounted(() => {
272 loadCategories(); 280 loadCategories();
273 }); 281 });
274 </script> 282 </script>
  283 +
275 <style lang="scss" scoped> 284 <style lang="scss" scoped>
276 .exercise-page { 285 .exercise-page {
277 - background-color: white;  
278 - box-sizing: border-box;  
279 - height: 100vh;  
280 - display: flex;  
281 - flex-direction: column;  
282 - box-sizing: border-box;  
283 -  
284 - .search-bar {  
285 - box-sizing: border-box;  
286 - width: 100%;  
287 - display: flex;  
288 - align-items: center;  
289 - justify-content: flex-end;  
290 - padding: 20rpx;  
291 - // background-color: #f5f5f5;  
292 - border-radius: 12rpx;  
293 - margin-top: 20rpx; 286 + height: 87vh;
  287 + // #ifdef H5
  288 + height: calc(100vh - 44px);
  289 + // #endif
294 290
295 - /* #ifdef H5 */  
296 - margin-top: 20px;  
297 - /* #endif */ 291 + background-color: #ffffff;
  292 + overflow: hidden;
298 293
299 - .search { 294 + /* 顶部搜索操作栏 */
  295 + .top-bar {
300 display: flex; 296 display: flex;
301 align-items: center; 297 align-items: center;
302 - padding: 10rpx 20rpx;  
303 - flex: 1;  
304 - border-radius: 30rpx;  
305 - background-color: #f5f5f5; 298 + padding: 16rpx 24rpx;
  299 + background-color: #ffffff;
  300 + border-bottom: 1rpx solid #f3f4f6;
  301 + z-index: 50;
306 302
307 - .search-icon {  
308 - width: 30rpx;  
309 - height: 30rpx;  
310 - }  
311 -  
312 - .search-input { 303 + .search-wrapper {
313 flex: 1; 304 flex: 1;
314 - padding-left: 15rpx;  
315 - font-size: 26rpx;  
316 - color: #333;  
317 - border: none;  
318 - outline: none;  
319 - background: transparent;  
320 - } 305 + margin-right: 20rpx;
321 } 306 }
322 307
323 - .add-btn {  
324 - width: 60rpx;  
325 - height: 60rpx;  
326 - background-color: transparent;  
327 - border: none;  
328 - margin-left: 10rpx; 308 + .add-btn-wrapper {
  309 + position: relative;
  310 +
  311 + .add-icon-btn {
  312 + width: 72rpx;
  313 + height: 72rpx;
  314 + border-radius: 50%;
  315 + background-color: #f3f4f6;
329 display: flex; 316 display: flex;
330 justify-content: center; 317 justify-content: center;
331 align-items: center; 318 align-items: center;
332 - z-index: 99;  
333 - position: relative; 319 + transition: background-color 0.2s;
334 320
335 - .plus-icon {  
336 - width: 40rpx;  
337 - height: 40rpx; 321 + &:active {
  322 + background-color: #e5e7eb;
  323 + }
338 } 324 }
339 325
340 .floating-menu { 326 .floating-menu {
341 position: absolute; 327 position: absolute;
342 - top: 30rpx;  
343 - right: 30rpx;  
344 - width: 300rpx;  
345 - background-color: #fff;  
346 - border-radius: 12rpx;  
347 - box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);  
348 - padding: 20rpx; 328 + top: 84rpx;
  329 + right: 0;
  330 + width: 320rpx;
  331 + background-color: #ffffff;
  332 + border-radius: 20rpx;
  333 + box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.12);
  334 + padding: 12rpx 0;
349 z-index: 100; 335 z-index: 100;
  336 + border: 1rpx solid #f3f4f6;
350 337
351 .menu-item { 338 .menu-item {
352 display: flex; 339 display: flex;
353 align-items: center; 340 align-items: center;
354 - padding: 20rpx 0;  
355 - border-bottom: 1rpx solid #eee; 341 + gap: 16rpx;
  342 + padding: 24rpx 28rpx;
  343 + transition: background-color 0.15s;
356 344
357 - .u-icon {  
358 - margin-right: 16rpx; 345 + &:active {
  346 + background-color: #f9fafb;
359 } 347 }
360 348
361 .menu-text { 349 .menu-text {
362 font-size: 28rpx; 350 font-size: 28rpx;
363 - color: #333; 351 + color: #1f2937;
  352 + font-weight: 500;
364 } 353 }
365 -  
366 - &:last-child {  
367 - border-bottom: none;  
368 } 354 }
369 } 355 }
  356 +
  357 + .menu-mask {
  358 + position: fixed;
  359 + top: 0;
  360 + left: 0;
  361 + right: 0;
  362 + bottom: 0;
  363 + z-index: 90;
  364 + background-color: transparent;
370 } 365 }
371 } 366 }
372 } 367 }
373 368
374 .layout-container { 369 .layout-container {
375 - box-sizing: border-box; 370 + height: 100%;
376 display: flex; 371 display: flex;
377 - height: calc(82vh + 25rpx);  
378 - background-color: #f5f5f5; 372 + overflow: hidden;
379 373
380 .left-nav { 374 .left-nav {
381 - width: 160rpx;  
382 - background-color: white;  
383 - border-right: 1px solid #eee;  
384 - // 添加底部 padding,避免最后一项被底部导航栏遮挡  
385 - padding-bottom: 20rpx;  
386 - // #ifdef MP-WEIXIN  
387 - // 微信小程序端可能需要更多底部空间  
388 - padding-bottom: 200rpx; 375 + width: 180rpx;
  376 + height: 100%;
  377 + background-color: #f9fafb;
  378 + border-right: 1rpx solid #f3f4f6;
389 379
390 - // #endif  
391 .nav-item { 380 .nav-item {
392 - padding: 24rpx 0;  
393 - text-align: center; 381 + display: flex;
  382 + align-items: center;
  383 + justify-content: center;
  384 + gap: 8rpx;
  385 + padding: 32rpx 16rpx;
  386 + position: relative;
  387 + transition: all 0.2s;
  388 +
  389 + .nav-text {
394 font-size: 26rpx; 390 font-size: 26rpx;
395 - color: #333; 391 + color: #4b5563;
  392 + font-weight: 400;
  393 + text-align: center;
  394 + line-height: 1.3;
396 } 395 }
397 396
398 - .active {  
399 - color: #16ad40;  
400 - background-color: #e1f6e9;  
401 - border-right: 3px solid #16ad40;  
402 - font-weight: bold; 397 + &.active {
  398 + background-color: #ffffff;
  399 +
  400 + .nav-text {
  401 + color: #10b981;
  402 + font-weight: 600;
  403 + }
  404 +
  405 + &::before {
  406 + content: '';
  407 + position: absolute;
  408 + left: 0;
  409 + top: 24rpx;
  410 + bottom: 24rpx;
  411 + width: 6rpx;
  412 + background-color: #10b981;
  413 + border-radius: 0 4rpx 4rpx 0;
  414 + }
  415 + }
403 } 416 }
404 } 417 }
405 418
  419 + /* 右侧内容滚动区 */
406 .right-content { 420 .right-content {
407 - box-sizing: border-box;  
408 - flex: 1;  
409 - padding: 20rpx; 421 + width: calc(100% - 180rpx);
410 height: 100%; 422 height: 100%;
411 - // 添加底部 padding,避免内容被底部导航栏遮挡  
412 - // 底部导航栏高度约 100rpx(50px),加上安全区域  
413 - padding-bottom: 20rpx;  
414 - // #ifdef MP-WEIXIN  
415 - // 微信小程序端可能需要更多底部空间  
416 - padding-bottom: 100rpx;  
417 -  
418 - // #endif  
419 - .tip {  
420 - display: flex;  
421 - padding-left: 20rpx;  
422 - align-items: center;  
423 - column-gap: 15rpx;  
424 - margin-bottom: 20rpx; 423 + background-color: #efefef;
  424 + padding: 20rpx 24rpx 200rpx;
  425 + box-sizing: border-box;
425 426
426 - .item {  
427 - margin: 0;  
428 - padding: 12rpx 20rpx;  
429 - font-size: 22rpx;  
430 - line-height: 1;  
431 - background-color: #fff;  
432 - border-radius: 20rpx; 427 + /* 二级子部位芯片滚动条 */
  428 + .sub-part-scroll {
  429 + width: 100%;
  430 + white-space: nowrap;
  431 + margin-bottom: 24rpx;
  432 +
  433 + .sub-part-wrapper {
  434 + display: inline-flex;
  435 + gap: 16rpx;
  436 + padding-bottom: 4rpx;
  437 +
  438 + .part-chip {
  439 + display: inline-block;
  440 + padding: 10rpx 24rpx;
  441 + font-size: 24rpx;
  442 + color: #4b5563;
  443 + background-color: #f3f4f6;
  444 + border-radius: 30rpx;
  445 + transition: all 0.2s;
433 446
434 &.active { 447 &.active {
435 - background-color: #d8ede0;  
436 - color: #43b05e; 448 + background-color: #ecfdf5;
  449 + color: #10b981;
  450 + font-weight: 600;
  451 + }
437 } 452 }
438 } 453 }
439 } 454 }
440 455
  456 + /* 动作网格区 */
441 .exercise-grid { 457 .exercise-grid {
442 display: flex; 458 display: flex;
443 flex-direction: column; 459 flex-direction: column;
444 - gap: 24rpx; 460 + gap: 32rpx;
445 461
446 - .title { 462 + .equipment-group {
  463 + .equipment-title {
  464 + display: flex;
  465 + align-items: center;
  466 + gap: 12rpx;
  467 + font-size: 28rpx;
447 font-weight: 600; 468 font-weight: 600;
448 - padding-left: 20rpx;  
449 - font-size: 30rpx;  
450 - }  
451 -  
452 - .content {  
453 - width: 100%;  
454 -  
455 - // display: grid;  
456 - // grid-template-columns: repeat(2, 1fr);  
457 - // /* 改为3列布局 */  
458 - // gap: 16rpx;  
459 -  
460 - // .exercise-item {  
461 - // display: flex;  
462 - // height: 150rpx;  
463 - // /* 减小项目高度 */  
464 - // flex-direction: column;  
465 - // align-items: center;  
466 - // justify-content: center;  
467 - // background-color: white;  
468 - // padding: 16rpx 8rpx;  
469 - // box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);  
470 -  
471 - // .exercise-img {  
472 - // width: 90%;  
473 - // height: 140rpx;  
474 - // /* 固定图片高度 */  
475 - // border-radius: 8rpx;  
476 - // margin-bottom: 12rpx;  
477 - // }  
478 -  
479 - // .exercise-title {  
480 - // font-size: 24rpx;  
481 - // color: #333;  
482 - // text-align: center;  
483 - // line-height: 1.3;  
484 - // max-width: 100%;  
485 -  
486 - // text-overflow: ellipsis;  
487 - // display: -webkit-box;  
488 - // -webkit-box-orient: vertical;  
489 - // }  
490 - // }  
491 - .equipment-item {  
492 - width: 100%;  
493 - // margin-bottom: 40rpx;  
494 -  
495 - .equipment-name {  
496 - color: #000;  
497 - font-size: 32rpx; 469 + color: #111827;
498 margin-bottom: 20rpx; 470 margin-bottom: 20rpx;
499 471
  472 + .title-bar {
  473 + width: 8rpx;
  474 + height: 28rpx;
  475 + background-color: #10b981;
  476 + border-radius: 4rpx;
500 477
501 - &:last-child {  
502 - margin-bottom: 50rpx; 478 + &.blue {
  479 + background-color: #3b82f6;
  480 + }
503 } 481 }
504 } 482 }
505 483
506 - .action-list { 484 + .action-grid {
507 display: grid; 485 display: grid;
508 grid-template-columns: repeat(2, 1fr); 486 grid-template-columns: repeat(2, 1fr);
509 - grid-gap: 10rpx;  
510 -  
511 - .action-item { 487 + gap: 15rpx;
  488 +
  489 + .action-card {
  490 + background-color: #ffffff;
  491 + border-radius: 16rpx;
  492 + border: 1rpx solid #f3f4f6;
  493 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
  494 + overflow: hidden;
512 display: flex; 495 display: flex;
513 flex-direction: column; 496 flex-direction: column;
514 - justify-content: center;  
515 - align-items: center;  
516 - gap: 10rpx;  
517 - background: #fff;  
518 - border-radius: 10rpx;  
519 - padding: 20rpx;  
520 - box-sizing: border-box; 497 + transition: transform 0.15s;
  498 +
  499 + &:active {
  500 + transform: scale(0.98);
  501 + }
  502 +
  503 + .image-wrapper {
521 position: relative; 504 position: relative;
  505 + width: 100%;
  506 + height: 200rpx;
  507 + background-color: #f9fafb;
522 508
523 .action-img { 509 .action-img {
524 width: 100%; 510 width: 100%;
525 - height: 250rpx;  
526 - border-radius: 10rpx; 511 + height: 200rpx;
527 } 512 }
528 513
529 - .trainingReps { 514 + .reps-badge {
530 position: absolute; 515 position: absolute;
531 - top: 20rpx;  
532 - right: 20rpx;  
533 - z-index: 1;  
534 - font-size: 22rpx; 516 + top: 12rpx;
  517 + right: 12rpx;
  518 + background: rgba(0, 0, 0, 0.6);
  519 + backdrop-filter: blur(8px);
  520 + color: #ffffff;
  521 + font-size: 20rpx;
  522 + padding: 4rpx 12rpx;
  523 + border-radius: 20rpx;
  524 + font-weight: 500;
535 } 525 }
536 -  
537 - &:last-child {  
538 - margin-bottom: 50rpx;  
539 - padding-bottom: env(safe-area-inset-bottom);  
540 } 526 }
541 527
  528 + .action-info {
  529 + padding: 16rpx 12rpx;
  530 + display: flex;
  531 + align-items: center;
  532 + justify-content: center;
  533 +
  534 + .action-name {
  535 + font-size: 24rpx;
  536 + font-weight: 500;
  537 + color: #1f2937;
  538 + text-align: center;
  539 + overflow: hidden;
  540 + white-space: nowrap;
  541 + text-overflow: ellipsis;
  542 + width: 100%;
542 } 543 }
543 } 544 }
544 } 545 }
545 } 546 }
546 -  
547 - .supers-name {  
548 - color: #000;  
549 - font-size: 32rpx;  
550 - margin-bottom: 20rpx;  
551 } 547 }
552 548
553 - .super { 549 + /* 超级组卡片样式 */
  550 + .supers-group {
  551 + .super-card {
554 display: flex; 552 display: flex;
555 align-items: center; 553 align-items: center;
556 - background: #fff; 554 + background-color: #ffffff;
  555 + border-radius: 16rpx;
  556 + border: 1rpx solid #f3f4f6;
557 padding: 20rpx; 557 padding: 20rpx;
558 - box-sizing: border-box;  
559 - border-radius: 10rpx; 558 + margin-bottom: 16rpx;
  559 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
560 gap: 20rpx; 560 gap: 20rpx;
561 - font-size: 26rpx;  
562 - margin-bottom: 20rpx;  
563 561
564 - .super-img {  
565 - width: 120rpx;  
566 - height: 120rpx;  
567 - border-radius: 10rpx; 562 + &:active {
  563 + background-color: #f9fafb;
568 } 564 }
569 565
570 - .super-name {  
571 - margin-bottom: 10rpx;  
572 - } 566 + .super-img {
  567 + width: 100rpx;
  568 + height: 100rpx;
  569 + border-radius: 12rpx;
  570 + background-color: #f3f4f6;
  571 + flex-shrink: 0;
573 } 572 }
574 573
575 - .add-super {  
576 - background: #fff;  
577 - padding: 20rpx;  
578 - box-sizing: border-box;  
579 - border-radius: 10rpx;  
580 -  
581 - .header { 574 + .super-info {
  575 + flex: 1;
582 display: flex; 576 display: flex;
583 - gap: 10rpx;  
584 - align-items: center;  
585 - margin-bottom: 20rpx; 577 + flex-direction: column;
  578 + gap: 8rpx;
  579 +
  580 + .super-name {
  581 + font-size: 28rpx;
  582 + font-weight: 600;
  583 + color: #111827;
586 } 584 }
587 585
588 - .tip {  
589 - margin: 0rpx;  
590 - padding: 0rpx; 586 + .super-muscles {
591 font-size: 22rpx; 587 font-size: 22rpx;
  588 + color: #6b7280;
592 } 589 }
593 } 590 }
594 } 591 }
595 -  
596 - .nav-item .category-group {  
597 - /* 超级组特殊样式 */  
598 - background-color: #f0f9ff;  
599 - font-weight: bold;  
600 - border-top: 1px solid #eee;  
601 } 592 }
602 593
603 - .empty-state {  
604 - display: flex;  
605 - justify-content: center;  
606 - align-items: center;  
607 -  
608 - .empty-text {  
609 - font-size: 28rpx;  
610 - color: #999; 594 + .empty-wrapper {
  595 + padding: 80rpx 0;
611 } 596 }
612 } 597 }
  598 +
  599 +
613 } 600 }
614 } 601 }
615 } 602 }
616 -  
617 -.safe-area-bottom {  
618 - height: env(safe-area-inset-bottom); // 适配iOS安全区  
619 - // 额外加上 Tabbar 的高度,你的 Tabbar 高度是 85px = 170rpx  
620 - padding-bottom: 170rpx;  
621 - box-sizing: border-box;  
622 -}  
623 </style> 603 </style>
1 -<!-- 第一层 模板大类 -->  
2 <template> 1 <template>
3 - <view class="template-page" @click="closeAllDropdowns">  
4 - <!-- 筛选按钮 -->  
5 - <view class="filter-section" @click.stop>  
6 - <!-- 部位筛选 --> 2 + <view class="template-page">
  3 + <!-- 顶部筛选操作栏(固定不缩放) -->
  4 + <view class="filter-section">
  5 + <!-- 部位筛选胶囊 -->
7 <view class="filter-wrapper"> 6 <view class="filter-wrapper">
8 - <view class="filter-btn" :class="{ active: showPartDropdown }" @click="togglePartDropdown"> 7 + <view class="filter-btn" :class="{ active: showPartDropdown, selected: activePartId !== '0' }"
  8 + @click.stop="togglePartDropdown">
9 <text class="text">{{ activePart }}</text> 9 <text class="text">{{ activePart }}</text>
10 - <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="13"></uni-icons> 10 + <up-icon :name="showPartDropdown ? 'arrow-up-fill' : 'arrow-down-fill'" size="10"
  11 + :color="showPartDropdown || activePartId !== '0' ? '#10b981' : '#6b7280'"></up-icon>
11 </view> 12 </view>
  13 +
12 <!-- 部位下拉菜单 --> 14 <!-- 部位下拉菜单 -->
13 - <view class="dropdown-menu" v-if="showPartDropdown">  
14 - <view v-for="(item, index) in partList" :key="index" class="dropdown-item"  
15 - :class="{ selected: activePart === item.title }" @click="selectPart(item)">  
16 - {{ item.title }} 15 + <view class="dropdown-menu" v-if="showPartDropdown" @click.stop>
  16 + <view v-for="item in partList" :key="item.id" class="dropdown-item"
  17 + :class="{ selected: activePartId === item.id }" @click="selectPart(item)">
  18 + <text>{{ item.title }}</text>
  19 + <up-icon v-if="activePartId === item.id" name="checkbox-mark" size="14" color="#10b981" />
17 </view> 20 </view>
18 </view> 21 </view>
19 </view> 22 </view>
20 23
21 - <!-- 场景筛选 --> 24 + <!-- 场景筛选胶囊 -->
22 <view class="filter-wrapper"> 25 <view class="filter-wrapper">
23 - <view class="filter-btn" :class="{ active: showSceneDropdown }" @click="toggleSceneDropdown"> 26 + <view class="filter-btn" :class="{ active: showSceneDropdown, selected: activeSceneId !== '0' }"
  27 + @click.stop="toggleSceneDropdown">
24 <text class="text">{{ activeScene }}</text> 28 <text class="text">{{ activeScene }}</text>
25 - <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="13"></uni-icons> 29 + <up-icon :name="showSceneDropdown ? 'arrow-up-fill' : 'arrow-down-fill'" size="10"
  30 + :color="showSceneDropdown || activeSceneId !== '0' ? '#10b981' : '#6b7280'"></up-icon>
26 </view> 31 </view>
  32 +
27 <!-- 场景下拉菜单 --> 33 <!-- 场景下拉菜单 -->
28 - <view class="dropdown-menu" v-if="showSceneDropdown">  
29 - <view v-for="(item, index) in sceneList" :key="index" class="dropdown-item" 34 + <view class="dropdown-menu" v-if="showSceneDropdown" @click.stop>
  35 + <view v-for="item in sceneList" :key="item.id" class="dropdown-item"
30 :class="{ selected: activeSceneId === item.id }" @click="selectScene(item)"> 36 :class="{ selected: activeSceneId === item.id }" @click="selectScene(item)">
31 - {{ item.title }} 37 + <text>{{ item.title }}</text>
  38 + <up-icon v-if="activeSceneId === item.id" name="checkbox-mark" size="14" color="#10b981" />
32 </view> 39 </view>
33 </view> 40 </view>
34 </view> 41 </view>
  42 +
  43 + <!-- 下拉全屏遮罩蒙层 -->
  44 + <view class="dropdown-mask" v-if="showPartDropdown || showSceneDropdown" @click="closeAllDropdowns"></view>
35 </view> 45 </view>
36 - <!-- 模板列表 (根据状态自动切换显示) -->  
37 - <scroll-view class="template-list" enable-flex scroll-y>  
38 - <!-- 用父容器包裹 v-if 分支,解决 key 冲突 -->  
39 - <view v-if="!isFiltering" class="sub-template-list">  
40 - <view v-for="(item, index) in templateList" :key="index" class="template-item">  
41 - <image :src="item.urlCover" mode="aspectFill" class="template-img"></image>  
42 - <view>  
43 - <view class="template-content" @click="gototemplate(item)">  
44 - <view class="template-title">{{ item.name }}</view>  
45 - <view class="template-count">{{ item.templatesCount }}个模板</view>  
46 - <view class="template-desc">{{ item.description }}</view> 46 +
  47 + <!-- 模板列表展示区(自适应滚动) -->
  48 + <view class="list-container">
  49 + <scroll-view class="template-scroll" scroll-y :show-scrollbar="false">
  50 + <view class="scroll-inner">
  51 + <!-- 模式一:未筛选状态,展示【模板大类列表】 -->
  52 + <template v-if="!isFiltering">
  53 + <view v-for="item in templateList" :key="item.id" class="template-card" @click="gototemplate(item)">
  54 + <image :src="item.urlCover" mode="aspectFill" class="cover-img" lazy-load />
  55 + <view class="card-content">
  56 + <view class="card-header">
  57 + <text class="title">{{ item.name }}</text>
  58 + <text class="count-badge">{{ item.templatesCount || 0 }}个模板</text>
47 </view> 59 </view>
  60 + <text class="desc" v-if="item.description">{{ item.description }}</text>
48 </view> 61 </view>
49 </view> 62 </view>
  63 + </template>
  64 +
  65 + <!-- 模式二:已筛选状态,展示【条件检索后的细分模板列表】 -->
  66 + <template v-else>
  67 + <view v-for="template in filteredTemplateList" :key="template.id" class="template-card"
  68 + @click="goToTemplateDetail(template)">
  69 + <image :src="template.urlCover" mode="aspectFill" class="cover-img" lazy-load />
  70 + <view class="card-content">
  71 + <text class="title">{{ template.name }}</text>
  72 +
  73 + <!-- 肌群说明 Chips -->
  74 + <view class="muscle-tags">
  75 + <view class="tag primary" v-if="template.primaryMuscleNames && template.primaryMuscleNames.length">
  76 + <text class="tag-label">主要:</text>
  77 + <text class="tag-val">{{ template.primaryMuscleNames.join(', ') }}</text>
50 </view> 78 </view>
51 79
52 - <!-- 用父容器包裹 v-else 分支,解决 key 冲突 -->  
53 - <view v-else>  
54 - <view v-for="(template, index) in filteredTemplateList" :key="index" class="template-item">  
55 - <image :src="template.urlCover" mode="aspectFill" class="template-img"></image>  
56 - <view>  
57 - <view class="template-content" @click="goToTemplateDetail(template)">  
58 - <view class="template-title">{{ template.name }}</view>  
59 - <!-- 空数据判断移到了这里 -->  
60 - <view class="template-count" v-if="template.primaryMuscles">  
61 - 主要训练部位:{{ template.primaryMuscleNames.join(', ') || '无' }}  
62 - </view>  
63 - <view class="template-desc" v-if="template.secondaryMuscles">  
64 - 次要训练部位:{{ template.secondaryMuscleNames.join(', ') || '无' }} 80 + <view class="tag secondary"
  81 + v-if="template.secondaryMuscleNames && template.secondaryMuscleNames.length">
  82 + <text class="tag-label">次要:</text>
  83 + <text class="tag-val">{{ template.secondaryMuscleNames.join(', ') }}</text>
65 </view> 84 </view>
66 </view> 85 </view>
67 </view> 86 </view>
68 </view> 87 </view>
69 88
70 - <!-- 空数据提示:移到 view-else 内部,且作为独立块 -->  
71 - <view v-if="filteredTemplateList.length === 0" class="empty-tip">  
72 - 暂无符合条件的训练模板 89 + <!-- 筛选无结果空状态 -->
  90 + <view v-if="filteredTemplateList.length === 0" class="empty-wrapper">
  91 + <up-empty mode="search" text="暂无符合条件的训练模板"></up-empty>
73 </view> 92 </view>
  93 + </template>
74 </view> 94 </view>
  95 +
  96 + <!-- 底部安全区垫高 -->
  97 + <view class="bottom-spacer"></view>
75 </scroll-view> 98 </scroll-view>
76 </view> 99 </view>
  100 + </view>
77 </template> 101 </template>
78 102
79 <script setup> 103 <script setup>
80 import { onMounted, ref } from 'vue'; 104 import { onMounted, ref } from 'vue';
81 import TemplatesApi from '@/sheep/api/Template/Templates'; 105 import TemplatesApi from '@/sheep/api/Template/Templates';
82 106
  107 +// 列表响应式数据
83 const templateList = ref([]); 108 const templateList = ref([]);
84 const filteredTemplateList = ref([]); 109 const filteredTemplateList = ref([]);
85 -// 2. 控制页面显示:false=显示大类,true=显示筛选后的模板 110 +
  111 +// 页面模式标记:false = 显示大类, true = 显示筛选后的细分模板
86 const isFiltering = ref(false); 112 const isFiltering = ref(false);
87 -const activeSceneId = ref('0');  
88 113
89 -// 下拉框部位筛选相关状态 114 +// 部位筛选状态
90 const partList = ref([]); 115 const partList = ref([]);
91 -const rawPartData = ref([]); // 存储接口返回的原始部位数据 116 +const rawPartData = ref([]);
92 const showPartDropdown = ref(false); 117 const showPartDropdown = ref(false);
93 const activePart = ref('部位'); 118 const activePart = ref('部位');
94 -const activePartId = ref('0'); // 新增:默认选中"不限",id=0  
95 -// 场景筛选相关状态 119 +const activePartId = ref('0'); // '0' 代表不限
  120 +
  121 +// 场景筛选状态
96 const sceneList = ref([ 122 const sceneList = ref([
97 { id: '0', title: '不限' }, 123 { id: '0', title: '不限' },
98 { id: '1', title: '健身房' }, 124 { id: '1', title: '健身房' },
99 { id: '2', title: '仅哑铃' }, 125 { id: '2', title: '仅哑铃' },
100 { id: '3', title: '仅哑铃+杠铃' }, 126 { id: '3', title: '仅哑铃+杠铃' },
101 - // { id: 4, title: '办公室' },  
102 ]); 127 ]);
103 const showSceneDropdown = ref(false); 128 const showSceneDropdown = ref(false);
104 const activeScene = ref('场景'); 129 const activeScene = ref('场景');
  130 +const activeSceneId = ref('0'); // '0' 代表不限
  131 +
  132 +// ====== API 数据加载 ======
105 133
106 -// 获取模板大类列表  
107 -const TemplatesList = async () => { 134 +/**
  135 + * 获取模板大类列表
  136 + */
  137 +const loadCategoryTemplates = async () => {
108 try { 138 try {
109 const response = await TemplatesApi.getTemplates(); 139 const response = await TemplatesApi.getTemplates();
110 - templateList.value = response.data;  
111 - console.log('模板大类接口返回数据', templateList.value) 140 + templateList.value = response.data || [];
112 } catch (error) { 141 } catch (error) {
113 - console.log('模板大类获取失败', error); 142 + console.error('模板大类获取失败:', error);
114 } 143 }
115 }; 144 };
116 -// 获取所有细分锻炼部位(接口) 145 +
  146 +/**
  147 + * 获取所有细分锻炼部位
  148 + */
117 const getPartCategories = async () => { 149 const getPartCategories = async () => {
118 try { 150 try {
119 const res = await TemplatesApi.getPartAllCategories(); 151 const res = await TemplatesApi.getPartAllCategories();
120 if (res.code === 0) { 152 if (res.code === 0) {
121 - rawPartData.value = res.data;  
122 - console.log('部位分类接口返回数据rawPartData.value:', rawPartData.value)  
123 - // 下拉列表赋值 153 + rawPartData.value = res.data || [];
124 partList.value = [ 154 partList.value = [
125 - { title: '不限', id: '0' }, // 给"不限"加id='0',方便后续筛选  
126 - ...res.data.map(item => ({  
127 - title: item.name, // 接口返回的name作为显示标题  
128 - id: String(item.id) // 保存接口的id,用于后续筛选传参  
129 - })) 155 + { title: '不限', id: '0' },
  156 + ...rawPartData.value.map((item) => ({
  157 + title: item.name,
  158 + id: String(item.id),
  159 + })),
130 ]; 160 ];
131 } 161 }
132 } catch (error) { 162 } catch (error) {
133 - console.log('部位列表获取失败', error); 163 + console.error('部位列表获取失败:', error);
134 } 164 }
135 }; 165 };
136 -// 切换部位下拉菜单 166 +
  167 +// ====== 交互与筛选逻辑 ======
  168 +
  169 +/**
  170 + * 切换部位下拉框
  171 + */
137 const togglePartDropdown = () => { 172 const togglePartDropdown = () => {
138 showPartDropdown.value = !showPartDropdown.value; 173 showPartDropdown.value = !showPartDropdown.value;
139 showSceneDropdown.value = false; 174 showSceneDropdown.value = false;
140 }; 175 };
141 176
142 -// 切换场景下拉菜单 177 +/**
  178 + * 切换场景下拉框
  179 + */
143 const toggleSceneDropdown = () => { 180 const toggleSceneDropdown = () => {
144 showSceneDropdown.value = !showSceneDropdown.value; 181 showSceneDropdown.value = !showSceneDropdown.value;
145 showPartDropdown.value = false; 182 showPartDropdown.value = false;
146 }; 183 };
147 184
148 -// 选择部位 185 +/**
  186 + * 选择部位
  187 + */
149 const selectPart = (item) => { 188 const selectPart = (item) => {
150 activePartId.value = item.id; 189 activePartId.value = item.id;
151 - // 判断是否是不限id=0  
152 - if (item.id === '0') {  
153 - activePart.value = '部位';  
154 - } else {  
155 - activePart.value = item.title;  
156 - } 190 + activePart.value = item.id === '0' ? '部位' : item.title;
157 showPartDropdown.value = false; 191 showPartDropdown.value = false;
158 doFilter(); 192 doFilter();
159 }; 193 };
160 194
161 -// 选择场景 195 +/**
  196 + * 选择场景
  197 + */
162 const selectScene = (item) => { 198 const selectScene = (item) => {
163 activeSceneId.value = item.id; 199 activeSceneId.value = item.id;
164 - // 判断是否是不限id=0  
165 - if (item.id === '0') {  
166 - activeScene.value = '场景';  
167 - } else {  
168 - activeScene.value = item.title;  
169 - } 200 + activeScene.value = item.id === '0' ? '场景' : item.title;
170 showSceneDropdown.value = false; 201 showSceneDropdown.value = false;
171 doFilter(); 202 doFilter();
172 }; 203 };
173 204
174 -// 点击页面其他地方关闭所有下拉菜单 205 +/**
  206 + * 关闭所有下拉菜单
  207 + */
175 const closeAllDropdowns = () => { 208 const closeAllDropdowns = () => {
176 showPartDropdown.value = false; 209 showPartDropdown.value = false;
177 showSceneDropdown.value = false; 210 showSceneDropdown.value = false;
178 }; 211 };
179 -// 核心:执行筛选逻辑 212 +
  213 +/**
  214 + * 执行筛选逻辑
  215 + */
180 const doFilter = async () => { 216 const doFilter = async () => {
181 - const musclesId = activePartId.value; // 对应接口 musclesId  
182 - const scene = activeSceneId.value; // 对应接口 scene  
183 - // 2. 判断:是否是【全部不限】 217 + const musclesId = activePartId.value;
  218 + const scene = activeSceneId.value;
  219 +
  220 + // 条件判断:若均为"不限",切回展示【大类列表】
184 if (musclesId === '0' && scene === '0') { 221 if (musclesId === '0' && scene === '0') {
185 - // 情况A:都不限 -> 恢复显示【模板大类】  
186 isFiltering.value = false; 222 isFiltering.value = false;
187 - // 重置按钮文字为默认  
188 activePart.value = '部位'; 223 activePart.value = '部位';
189 activeScene.value = '场景'; 224 activeScene.value = '场景';
190 - // 大类列表之前已经加载过了,直接显示  
191 return; 225 return;
192 } 226 }
193 - // 情况B:有筛选条件 -> 请求【筛选接口】 227 +
  228 + // 存在有效条件:发请求获取筛选列表
194 try { 229 try {
195 - isFiltering.value = true; // 切换成模板列表模式 230 + isFiltering.value = true;
196 const res = await TemplatesApi.queryTemplatesByCondition({ 231 const res = await TemplatesApi.queryTemplatesByCondition({
197 - musclesId: musclesId,  
198 - scene: scene 232 + musclesId,
  233 + scene,
199 }); 234 });
200 if (res.code === 0) { 235 if (res.code === 0) {
201 - // 把接口返回的模板数据存起来,页面会自动刷新  
202 - filteredTemplateList.value = res.data;  
203 - console.log('筛选接口返回数据filteredTemplateList.value:', filteredTemplateList.value) 236 + filteredTemplateList.value = res.data || [];
204 } 237 }
205 } catch (error) { 238 } catch (error) {
206 - console.log('筛选失败', error);  
207 - // 失败了也清空数据 239 + console.error('筛选模板失败:', error);
208 filteredTemplateList.value = []; 240 filteredTemplateList.value = [];
209 } 241 }
210 }; 242 };
211 -// 跳转到模板详情页,传入模板id  
212 -// 跳转地址是F:\hongxing-app\hongxing-new\pages4\pages\xunji\xunji-moban-xiangqing.vue  
213 -const goToTemplateDetail = (template) => { 243 +
  244 +// ====== 路由跳转 ======
  245 +
  246 +/**
  247 + * 跳转至大类包含的模板列表
  248 + */
  249 +const gototemplate = (item) => {
214 uni.navigateTo({ 250 uni.navigateTo({
215 - url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`, 251 + url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`,
216 }); 252 });
217 }; 253 };
218 -const gototemplate = (item) => {  
219 - // 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4 254 +
  255 +/**
  256 + * 跳转至具体模板详情页
  257 + */
  258 +const goToTemplateDetail = (template) => {
220 uni.navigateTo({ 259 uni.navigateTo({
221 - url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`, 260 + url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`,
222 }); 261 });
223 }; 262 };
224 263
225 onMounted(async () => { 264 onMounted(async () => {
226 - await TemplatesList();  
227 - await getPartCategories();  
228 - console.log('进入模板大类详情');  
229 - 265 + await Promise.all([loadCategoryTemplates(), getPartCategories()]);
230 }); 266 });
231 </script> 267 </script>
232 268
233 <style lang="scss" scoped> 269 <style lang="scss" scoped>
234 .template-page { 270 .template-page {
235 - width: 100vw;  
236 - height: 100vh;  
237 - box-sizing: border-box;  
238 -  
239 - .filter-section {  
240 - position: fixed;  
241 - // top: 80rpx; 271 + display: flex;
  272 + flex-direction: column;
  273 + height: calc(100vh - 44px);
242 // #ifdef MP-WEIXIN 274 // #ifdef MP-WEIXIN
243 - top: 234rpx; 275 + height: calc(100vh - 110px);
244 // #endif 276 // #endif
  277 + width: 100vw;
  278 + background-color: #f8fafc;
  279 + overflow: hidden;
245 280
246 - /* #ifdef H5 */  
247 - top: 77px;  
248 - /* #endif */  
249 -  
250 - left: 0;  
251 - right: 0;  
252 - z-index: 999;  
253 - 281 + /* 顶部筛选控制栏 */
  282 + .filter-section {
  283 + flex-shrink: 0;
  284 + position: relative;
  285 + z-index: 50;
254 display: flex; 286 display: flex;
255 - gap: 24rpx;  
256 - flex-wrap: wrap;  
257 - padding: 20rpx;  
258 - background-color: white; 287 + gap: 20rpx;
  288 + padding: 20rpx 30rpx;
  289 + background-color: #ffffff;
  290 + border-bottom: 1rpx solid #f1f5f9;
  291 +
  292 + .filter-wrapper {
  293 + position: relative;
259 294
260 .filter-btn { 295 .filter-btn {
261 display: flex; 296 display: flex;
262 align-items: center; 297 align-items: center;
263 - justify-content: center;  
264 - width: auto;  
265 - min-width: 70rpx;  
266 - height: 50rpx;  
267 - background: #fff;  
268 - color: #333;  
269 - font-size: 24rpx;  
270 - border: 2rpx solid #ddd;  
271 - border-radius: 25rpx;  
272 - padding: 0 20rpx;  
273 - white-space: nowrap; 298 + gap: 10rpx;
  299 + height: 58rpx;
  300 + padding: 0 24rpx;
  301 + background-color: #f8fafc;
  302 + border: 1rpx solid #e2e8f0;
  303 + border-radius: 30rpx;
  304 + transition: all 0.2s ease;
  305 +
  306 + .text {
  307 + font-size: 26rpx;
  308 + color: #334155;
  309 + font-weight: 500;
274 } 310 }
275 311
276 - .filter-btn .text {  
277 - margin-right: 8rpx;  
278 - } 312 + &.active,
  313 + &.selected {
  314 + background-color: #ecfdf5;
  315 + border-color: #10b981;
279 316
280 - .filter-btn.active {  
281 - border-color: #43b05e;  
282 - background: #e6f7f0;  
283 - color: #43b05e; 317 + .text {
  318 + color: #10b981;
  319 + font-weight: 600;
  320 + }
284 } 321 }
285 -  
286 - .filter-wrapper {  
287 - position: relative;  
288 - z-index: 10;  
289 } 322 }
290 323
291 - /* 下拉菜单样式 */ 324 + /* 下拉浮层 */
292 .dropdown-menu { 325 .dropdown-menu {
293 position: absolute; 326 position: absolute;
294 - top: calc(100% + 8rpx); 327 + top: 72rpx;
295 left: 0; 328 left: 0;
296 - background: #fff;  
297 - border-radius: 14rpx;  
298 - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);  
299 - padding: 12rpx 0;  
300 z-index: 100; 329 z-index: 100;
301 - min-width: 150rpx;  
302 - max-height: 320rpx; 330 + min-width: 220rpx;
  331 + max-height: 380rpx;
  332 + background-color: #ffffff;
  333 + border-radius: 16rpx;
  334 + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1);
  335 + border: 1rpx solid #f1f5f9;
  336 + padding: 8rpx 0;
303 overflow-y: auto; 337 overflow-y: auto;
304 - border: 1rpx solid #f0f0f0;  
305 - }  
306 338
307 - /* 下拉菜单项样式 */  
308 .dropdown-item { 339 .dropdown-item {
309 - padding: 16rpx 24rpx; 340 + display: flex;
  341 + align-items: center;
  342 + justify-content: space-between;
  343 + padding: 20rpx 28rpx;
310 font-size: 26rpx; 344 font-size: 26rpx;
311 - color: #333;  
312 - white-space: nowrap; 345 + color: #334155;
  346 + transition: background-color 0.15s;
  347 +
  348 + &:active {
  349 + background-color: #f8fafc;
313 } 350 }
314 351
315 - /* 下拉菜单项悬停样式 */  
316 - .dropdown-item:hover {  
317 - background-color: #43b05e; 352 + &.selected {
  353 + background-color: #f0fdf4;
  354 + color: #10b981;
  355 + font-weight: 600;
  356 + }
  357 + }
  358 + }
318 } 359 }
319 360
320 - /* 下拉菜单项选中样式 */  
321 - .dropdown-item.selected {  
322 - background-color: #f0f9f4;  
323 - color: #2e9d5a;  
324 - font-weight: 500; 361 + /* 遮罩蒙层 */
  362 + .dropdown-mask {
  363 + position: fixed;
  364 + top: 0;
  365 + left: 0;
  366 + right: 0;
  367 + bottom: 0;
  368 + z-index: 40;
  369 + background-color: rgba(0, 0, 0, 0.1);
325 } 370 }
326 } 371 }
327 372
328 - .template-list {  
329 - margin-top: 120rpx;  
330 - padding: 0 30rpx;  
331 - box-sizing: border-box; 373 + /* 列表自适应滚动容器 */
  374 + .list-container {
332 flex: 1; 375 flex: 1;
333 - display: flex;  
334 - flex-direction: column;  
335 -  
336 - // #ifdef MP-WEIXIN  
337 - padding-bottom: 200rpx;  
338 - height: calc(100vh - 274rpx);  
339 - // #endif 376 + height: 0;
  377 + /* 关键:接触 Flex 内容支撑限制 */
  378 + min-height: 0;
  379 + overflow: hidden;
340 380
341 - /* #ifdef H5 */  
342 - margin-top: 87px;  
343 - // 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px  
344 - height: calc(100vh - 77px - 87px);  
345 - /* #endif */ 381 + .template-scroll {
  382 + width: 100%;
  383 + height: 100%;
346 384
347 - .sub-template-list { 385 + .scroll-inner {
  386 + padding: 24rpx 30rpx;
348 display: flex; 387 display: flex;
349 flex-direction: column; 388 flex-direction: column;
350 - gap: 10rpx;  
351 - } 389 + gap: 20rpx;
352 390
353 - .template-item { 391 + /* 模板卡片组件样式 */
  392 + .template-card {
354 display: flex; 393 display: flex;
355 - background-color: white;  
356 - border-radius: 16rpx;  
357 - overflow: hidden;  
358 - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); 394 + align-items: center;
  395 + background-color: #ffffff;
  396 + border-radius: 20rpx;
359 padding: 20rpx; 397 padding: 20rpx;
  398 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
  399 + border: 1rpx solid #f1f5f9;
  400 + transition: transform 0.15s;
360 401
361 - &:first-child {  
362 - margin-top: 24rpx; 402 + &:active {
  403 + transform: scale(0.985);
  404 + background-color: #f8fafc;
363 } 405 }
364 406
365 -  
366 - .template-img {  
367 - width: 120rpx;  
368 - height: 120rpx;  
369 - border-radius: 12rpx; 407 + .cover-img {
  408 + width: 128rpx;
  409 + height: 128rpx;
  410 + border-radius: 16rpx;
370 margin-right: 20rpx; 411 margin-right: 20rpx;
  412 + background-color: #f1f5f9;
  413 + flex-shrink: 0;
371 } 414 }
372 415
373 - .template-content { 416 + .card-content {
374 flex: 1; 417 flex: 1;
  418 + display: flex;
  419 + flex-direction: column;
  420 + justify-content: center;
  421 + overflow: hidden;
  422 +
  423 + .card-header {
  424 + display: flex;
  425 + align-items: center;
  426 + justify-content: space-between;
  427 + margin-bottom: 8rpx;
  428 +
  429 + .title {
  430 + font-size: 30rpx;
  431 + font-weight: 600;
  432 + color: #0f172a;
  433 + overflow: hidden;
  434 + white-space: nowrap;
  435 + text-overflow: ellipsis;
  436 + flex: 1;
  437 + margin-right: 12rpx;
375 } 438 }
376 439
377 - .template-title {  
378 - font-size: 32rpx;  
379 - color: #333;  
380 - margin-bottom: 10rpx; 440 + .count-badge {
  441 + font-size: 22rpx;
  442 + color: #10b981;
  443 + background-color: #ecfdf5;
  444 + padding: 4rpx 12rpx;
  445 + border-radius: 20rpx;
  446 + font-weight: 500;
  447 + flex-shrink: 0;
  448 + }
381 } 449 }
382 450
383 - .template-count {  
384 - font-size: 24rpx;  
385 - color: #666;  
386 - margin-bottom: 10rpx; 451 + .title {
  452 + font-size: 30rpx;
  453 + font-weight: 600;
  454 + color: #0f172a;
  455 + margin-bottom: 8rpx;
387 } 456 }
388 457
389 - .template-desc { 458 + .desc {
390 font-size: 24rpx; 459 font-size: 24rpx;
391 - color: #666; 460 + color: #64748b;
392 line-height: 1.4; 461 line-height: 1.4;
  462 + display: -webkit-box;
  463 + -webkit-box-orient: vertical;
  464 + -webkit-line-clamp: 2;
  465 + overflow: hidden;
  466 + }
  467 +
  468 + /* 肌群 Tag 列表 */
  469 + .muscle-tags {
  470 + display: flex;
  471 + flex-direction: column;
  472 + gap: 6rpx;
  473 + margin-top: 4rpx;
  474 +
  475 + .tag {
  476 + display: flex;
  477 + align-items: center;
  478 + font-size: 22rpx;
  479 + line-height: 1.3;
  480 +
  481 + .tag-label {
  482 + color: #94a3b8;
  483 + flex-shrink: 0;
  484 + }
  485 +
  486 + .tag-val {
  487 + color: #475569;
  488 + overflow: hidden;
  489 + white-space: nowrap;
  490 + text-overflow: ellipsis;
393 } 491 }
394 492
395 - &:last-child {  
396 - margin-bottom: 5rpx; 493 + &.primary .tag-val {
  494 + color: #059669;
  495 + font-weight: 500;
  496 + }
  497 + }
  498 + }
  499 + }
  500 + }
  501 +
  502 + .empty-wrapper {
  503 + padding: 100rpx 0;
  504 + }
  505 + }
397 506
398 - /* #ifdef H5 */  
399 - margin-bottom: 30rpx;  
400 - /* #endif */ 507 + .bottom-spacer {
  508 + height: env(safe-area-inset-bottom);
  509 + padding-bottom: 40rpx;
401 } 510 }
402 } 511 }
403 } 512 }
1 <template> 1 <template>
2 <view class="calendar-page"> 2 <view class="calendar-page">
3 - <!-- 顶部栏:年月选择器和功能按钮 --> 3 + <!-- 顶部操作栏 -->
4 <view class="top-bar"> 4 <view class="top-bar">
5 - <view class="month-picker">  
6 - <uni-datetime-picker type="date" v-model="currentMonth" :clear-icon="false" />  
7 - <up-icon name="arrow-down" color="#333" size="15"></up-icon> 5 + <!-- 年月选择胶囊 -->
  6 + <picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange">
  7 + <view class="month-picker-pill">
  8 + <text class="month-text">{{ displayMonthText }}</text>
  9 + <up-icon name="arrow-down-fill" color="#333333" size="10"></up-icon>
8 </view> 10 </view>
  11 + </picker>
  12 +
  13 + <!-- 说明按钮 -->
9 <view class="action-buttons"> 14 <view class="action-buttons">
10 - <button class="action-btn" @click="ShowExplainPopup = true">说明</button> 15 + <up-button text="说明" type="info" plain shape="circle" size="mini"
  16 + customStyle="padding: 0 24rpx; height: 56rpx; font-size: 24rpx; border-color: #dcdfe6; color: #606266;"
  17 + @click="showExplainPopup = true" />
11 </view> 18 </view>
12 </view> 19 </view>
13 - <!-- 日历网格 -->  
14 - <view class="calendar-grid"> 20 +
  21 + <!-- 日历卡片容器 -->
  22 + <view class="calendar-card">
  23 + <!-- 星期表头 -->
15 <view class="week-header"> 24 <view class="week-header">
16 <text v-for="(day, index) in weekDays" :key="index" class="week-day"> 25 <text v-for="(day, index) in weekDays" :key="index" class="week-day">
17 {{ day }} 26 {{ day }}
18 </text> 27 </text>
19 </view> 28 </view>
  29 +
  30 + <!-- 日期网格 (动态 5行 或 6行) -->
20 <view class="date-grid"> 31 <view class="date-grid">
21 - <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{ today: date.today }"  
22 - @tap="selectDate(date)">  
23 - <text v-if="!date.today" class="date-number">{{ date.day }}</text>  
24 - <text v-else class="today-text">今</text> 32 + <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{
  33 +
  34 + 'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0)
  35 + }" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)">
  36 + <!-- 日期数字 / 今日徽章 -->
  37 + <view class="date-header">
  38 + <text v-if="date.today" class="today-badge">今</text>
  39 + <text v-else class="date-number">
  40 + {{ date.day }}
  41 + </text>
  42 + </view>
25 43
26 - <!-- 内容显示区域 --> 44 + <!-- 计划/容量数据列表 -->
27 <view class="plan-list" 45 <view class="plan-list"
28 v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)"> 46 v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)">
29 - <template v-for="(item, idx) in getVisibleItems(date).items" :key="idx">  
30 - <view class="plan-name" :class="{  
31 - 'weight-item': item.type === 'weight',  
32 - 'note-item': item.type === 'note'  
33 - }" :style="item.bg ? { backgroundColor: item.bg } : ''">  
34 - {{ item.text }} 47 + <template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx">
  48 + <view class="plan-tag" :class="`tag-type-${item.type}`"
  49 + :style="item.bg ? { backgroundColor: item.bg } : {}">
  50 + <text class="tag-text">{{ item.text }}</text>
35 </view> 51 </view>
36 </template> 52 </template>
37 53
38 - <!-- 统一只显示一个 + 剩余总数 -->  
39 - <text v-if="getVisibleItems(date).remaining > 0" class="plan-extra">  
40 - +{{ getVisibleItems(date).remaining }}  
41 - </text> 54 + <!-- 溢出剩余条数 -->
  55 + <view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">
  56 + +{{ getCellVisibleItems(date).remaining }}
  57 + </view>
42 </view> 58 </view>
43 </view> 59 </view>
44 </view> 60 </view>
45 </view> 61 </view>
46 62
47 - <!-- <view class="gridCellContent" v-if="ShowGridCellPopup && selectedDate">  
48 - <GridCellContentPopup :date="selectedDate" @close="ShowGridCellPopup = false" />  
49 - </view> -->  
50 - 63 + <!-- 单元格详情弹窗 -->
51 <GridCellContentPopup ref="gridPopupRef" :date="selectedDate" @refreshCalendar="handleRefreshCalendar" /> 64 <GridCellContentPopup ref="gridPopupRef" :date="selectedDate" @refreshCalendar="handleRefreshCalendar" />
52 65
53 - <!-- 日历说明弹窗 -->  
54 - <view class="explain-popup-overlay" v-if="ShowExplainPopup" @click.self="ShowExplainPopup = false">  
55 - <view class="explain-popup"> 66 + <!-- 日历图标说明弹窗 -->
  67 + <up-popup :show="showExplainPopup" mode="bottom" round="24rpx" :safeAreaInsetBottom="true"
  68 + @close="showExplainPopup = false">
  69 + <view class="explain-popup-content">
56 <view class="explain-header"> 70 <view class="explain-header">
57 - <text class="explain-title">日历说明</text>  
58 - <view class="close-btn" @click="ShowExplainPopup = false">  
59 - <text>×</text>  
60 - </view> 71 + <text class="explain-title">日历图标说明</text>
  72 + <up-icon name="close" color="#909399" size="18" @click="showExplainPopup = false" />
61 </view> 73 </view>
  74 +
62 <view class="explain-list"> 75 <view class="explain-list">
63 <view class="explain-item"> 76 <view class="explain-item">
64 <view class="color-block blue"></view> 77 <view class="color-block blue"></view>
65 - <text class="item-label">分钟</text>  
66 - <text class="item-desc">在场馆的运动时长</text> 78 + <view class="item-info">
  79 + <text class="item-label">时长</text>
  80 + <text class="item-desc">在场馆的实际运动时长</text>
  81 + </view>
67 </view> 82 </view>
  83 +
68 <view class="explain-item"> 84 <view class="explain-item">
69 <view class="color-block green"></view> 85 <view class="color-block green"></view>
  86 + <view class="item-info">
70 <text class="item-label">容量</text> 87 <text class="item-label">容量</text>
71 - <text class="item-desc">力量训练的总重量</text> 88 + <text class="item-desc">力量训练累计产生的总负重重量</text>
  89 + </view>
72 </view> 90 </view>
  91 +
73 <view class="explain-item"> 92 <view class="explain-item">
74 <view class="color-block yellow"></view> 93 <view class="color-block yellow"></view>
  94 + <view class="item-info">
75 <text class="item-label">课程</text> 95 <text class="item-label">课程</text>
76 - <text class="item-desc">训记或已结课的线下课程</text> 96 + <text class="item-desc">训记完成或已结课的线下课程</text>
77 </view> 97 </view>
  98 + </view>
  99 +
78 <view class="explain-item"> 100 <view class="explain-item">
79 <view class="color-block gray"></view> 101 <view class="color-block gray"></view>
80 - <text class="item-label">课程</text>  
81 - <text class="item-desc">训记排课或待上课的线下课程</text> 102 + <view class="item-info">
  103 + <text class="item-label">排课</text>
  104 + <text class="item-desc">训记排课计划或待上课的课程</text>
82 </view> 105 </view>
83 </view> 106 </view>
84 </view> 107 </view>
85 </view> 108 </view>
  109 + </up-popup>
86 </view> 110 </view>
87 </template> 111 </template>
88 112
89 <script setup> 113 <script setup>
90 import { ref, watch, computed, nextTick } from 'vue'; 114 import { ref, watch, computed, nextTick } from 'vue';
  115 +import dayjs from 'dayjs';
91 import DailyTemplateApi from '@/sheep/api/calendar/date'; 116 import DailyTemplateApi from '@/sheep/api/calendar/date';
92 -import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue' 117 +import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue';
93 118
94 -// ====== 响应式数据 ====== 119 +// 常量定义
  120 +const MAX_CELL_LINES = 3;
95 const weekDays = ref(['一', '二', '三', '四', '五', '六', '日']); 121 const weekDays = ref(['一', '二', '三', '四', '五', '六', '日']);
  122 +
  123 +// 响应式状态
96 const dates = ref([]); 124 const dates = ref([]);
97 -const today = new Date();  
98 -const ShowExplainPopup = ref(false) 125 +const showExplainPopup = ref(false);
99 const selectedDate = ref(''); 126 const selectedDate = ref('');
100 -// const ShowGridCellPopup = ref(false)  
101 const gridPopupRef = ref(null); 127 const gridPopupRef = ref(null);
102 128
103 -// 自动获取当前年月  
104 -const now = new Date();  
105 -const currentMonth = ref(  
106 - `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`  
107 -); 129 +const currentMonth = ref(dayjs().format('YYYY-MM-DD'));
108 130
109 -// ====== 工具函数 ======  
110 -const formatDateKey = (y, m, d) => {  
111 - return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;  
112 -}; 131 +// 顶部年月显示
  132 +const displayMonthText = computed(() => {
  133 + return dayjs(currentMonth.value).format('YYYY年M月');
  134 +});
113 135
114 -const arrayToDateStr = (arr) => {  
115 - if (!Array.isArray(arr) || arr.length !== 3) return null;  
116 - const [y, m, d] = arr;  
117 - return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; 136 +// 年月 Picker 触发
  137 +const onMonthChange = (e) => {
  138 + if (e.detail && e.detail.value) {
  139 + currentMonth.value = dayjs(e.detail.value).startOf('month').format('YYYY-MM-DD');
  140 + }
118 }; 141 };
119 142
120 -// ====== 生成日历数据 ======  
121 -const generateCalendar = (year, month) => {  
122 - const firstDay = new Date(year, month - 1, 1);  
123 - const lastDay = new Date(year, month, 0).getDate();  
124 - let startWeekDay = firstDay.getDay();  
125 - const adjustedStart = startWeekDay === 0 ? 6 : startWeekDay - 1; 143 +// ====== 计算每个日期格子的背景颜色 ======
  144 +const getCellBgColor = (date) => {
126 145
127 - const calendarDates = [];  
128 146
129 - // 上月填充  
130 - const prevMonthLastDay = new Date(year, month - 1, 0).getDate();  
131 - for (let i = adjustedStart; i > 0; i--) {  
132 - const day = prevMonthLastDay - i + 1;  
133 - const y = month === 1 ? year - 1 : year;  
134 - const m = month === 1 ? 12 : month - 1;  
135 - calendarDates.push({  
136 - day,  
137 - today: false,  
138 - dateKey: formatDateKey(y, m, day),  
139 - }); 147 + // 2. 如果存在训练计划/备注/重量,优先采用训练本身的背景色
  148 + if (date.planList && date.planList.length > 0 && date.planList[0].bg) {
  149 + return date.planList[0].bg;
  150 + }
  151 + if (date.noteList && date.noteList.length > 0 && date.noteList[0].backgroundColor) {
  152 + return date.noteList[0].backgroundColor;
140 } 153 }
141 154
142 - // 本月  
143 - for (let d = 1; d <= lastDay; d++) {  
144 - const isToday =  
145 - year === today.getFullYear() &&  
146 - month === today.getMonth() + 1 &&  
147 - d === today.getDate();  
148 - calendarDates.push({  
149 - day: d,  
150 - today: isToday,  
151 - dateKey: formatDateKey(year, month, d),  
152 - }); 155 + // 3. 有容量负重但无特定背景色:赋予柔和浅绿
  156 + if (date.weight && date.weight > 0) {
  157 + return '#f0fdf4';
153 } 158 }
154 159
155 - // 下月填充  
156 - const remaining = 42 - calendarDates.length;  
157 - for (let d = 1; d <= remaining; d++) {  
158 - const y = month === 12 ? year + 1 : year;  
159 - const m = month === 12 ? 1 : month + 1; 160 + // 4. 有计划但未指定背景色:赋予柔和浅黄
  161 + if (date.planList && date.planList.length > 0) {
  162 + return '#fefce8';
  163 + }
  164 +
  165 +
  166 +
  167 + // 6. 普通无训练的本月休息日:基础卡片白色/浅灰
  168 + return '#ededed';
  169 +};
  170 +
  171 +
  172 +const generateCalendar = (monthStr) => {
  173 + const current = dayjs(monthStr);
  174 + const startOfMonth = current.startOf('month');
  175 + const daysInMonth = current.daysInMonth();
  176 +
  177 + const dayOfWeek = startOfMonth.day(); // 0 表示周日
  178 + const startOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
  179 +
  180 + // 动态计算总单元格:35(5行)或 42(6行)
  181 + const totalCellsNeeded = startOffset + daysInMonth;
  182 + const totalGridSize = totalCellsNeeded > 35 ? 42 : 35;
  183 +
  184 + const gridStartDate = startOfMonth.subtract(startOffset, 'day');
  185 + const todayStr = dayjs().format('YYYY-MM-DD');
  186 +
  187 + const calendarDates = [];
  188 + for (let i = 0; i < totalGridSize; i++) {
  189 + const cellDate = gridStartDate.add(i, 'day');
  190 + const dateKey = cellDate.format('YYYY-MM-DD');
  191 +
160 calendarDates.push({ 192 calendarDates.push({
161 - day: d,  
162 - today: false,  
163 - dateKey: formatDateKey(y, m, d), 193 + day: cellDate.date(),
  194 + today: dateKey === todayStr,
  195 + isCurrentMonth: cellDate.month() === current.month(),
  196 + dateKey,
164 }); 197 });
165 } 198 }
166 199
@@ -168,26 +201,19 @@ const generateCalendar = (year, month) => { @@ -168,26 +201,19 @@ const generateCalendar = (year, month) => {
168 }; 201 };
169 202
170 // ====== 加载计划数据 ====== 203 // ====== 加载计划数据 ======
171 -const loadPlans = async (year, month) => { 204 +const loadPlans = async (monthStr) => {
172 try { 205 try {
173 - // 关键:获取 上个月、当前月、下个月 → 保证前后日期都有数据  
174 - const prevMonth = month - 1 < 1 ? 12 : month - 1;  
175 - const prevYear = month - 1 < 1 ? year - 1 : year;  
176 -  
177 - const nextMonth = month + 1 > 12 ? 1 : month + 1;  
178 - const nextYear = month + 1 > 12 ? year + 1 : year;  
179 -  
180 - // 并行请求三个月数据  
181 - // const [prevRes, currRes, nextRes] = await Promise.all([  
182 - // DailyTemplateApi.getCalendarPlans(prevYear, prevMonth),  
183 - // DailyTemplateApi.getCalendarPlans(year, month),  
184 - // DailyTemplateApi.getCalendarPlans(nextYear, nextMonth),  
185 - // ]);  
186 - const prevRes = await DailyTemplateApi.getCalendarPlans(prevYear, prevMonth);  
187 - const currRes = await DailyTemplateApi.getCalendarPlans(year, month);  
188 - const nextRes = await DailyTemplateApi.getCalendarPlans(nextYear, nextMonth);  
189 -  
190 - // 合并三个月数据 206 + const curr = dayjs(monthStr);
  207 + const prev = curr.subtract(1, 'month');
  208 + const next = curr.add(1, 'month');
  209 +
  210 + // 并行请求 3 个月数据
  211 + const [prevRes, currRes, nextRes] = await Promise.all([
  212 + DailyTemplateApi.getCalendarPlans(prev.year(), prev.month() + 1).catch(() => ({ data: [] })),
  213 + DailyTemplateApi.getCalendarPlans(curr.year(), curr.month() + 1).catch(() => ({ data: [] })),
  214 + DailyTemplateApi.getCalendarPlans(next.year(), next.month() + 1).catch(() => ({ data: [] })),
  215 + ]);
  216 +
191 const allPlans = [ 217 const allPlans = [
192 ...(prevRes.data || []), 218 ...(prevRes.data || []),
193 ...(currRes.data || []), 219 ...(currRes.data || []),
@@ -199,24 +225,25 @@ const loadPlans = async (year, month) => { @@ -199,24 +225,25 @@ const loadPlans = async (year, month) => {
199 const weightMap = {}; 225 const weightMap = {};
200 226
201 allPlans.forEach((plan) => { 227 allPlans.forEach((plan) => {
202 - let dateStr; 228 + let dateStr = null;
203 if (Array.isArray(plan.trainDate)) { 229 if (Array.isArray(plan.trainDate)) {
204 - dateStr = arrayToDateStr(plan.trainDate); 230 + const [y, m, d] = plan.trainDate;
  231 + dateStr = dayjs(`${y}-${m}-${d}`).format('YYYY-MM-DD');
205 } else if (typeof plan.trainDate === 'string') { 232 } else if (typeof plan.trainDate === 'string') {
206 - dateStr = plan.trainDate; 233 + dateStr = dayjs(plan.trainDate).format('YYYY-MM-DD');
207 } 234 }
208 235
209 - if (!dateStr) return; 236 + if (!dateStr || dateStr === 'Invalid Date') return;
210 237
211 - // 处理多条备注 238 + // 备注处理
212 if (Array.isArray(plan.notes) && plan.notes.length > 0) { 239 if (Array.isArray(plan.notes) && plan.notes.length > 0) {
213 - noteMap[dateStr] = plan.notes.map(note => ({ 240 + noteMap[dateStr] = plan.notes.map((note) => ({
214 content: note.content || '', 241 content: note.content || '',
215 - backgroundColor: note.backgroundColor || '#ccc' 242 + backgroundColor: note.backgroundColor || '#e2e8f0',
216 })); 243 }));
217 } 244 }
218 245
219 - // 处理计划和重量 246 + // 训练计划与容量处理
220 planMap[dateStr] = planMap[dateStr] || []; 247 planMap[dateStr] = planMap[dateStr] || [];
221 if (Array.isArray(plan.templates)) { 248 if (Array.isArray(plan.templates)) {
222 let totalWeightForDay = 0; 249 let totalWeightForDay = 0;
@@ -224,7 +251,7 @@ const loadPlans = async (year, month) => { @@ -224,7 +251,7 @@ const loadPlans = async (year, month) => {
224 if (template && template.name) { 251 if (template && template.name) {
225 planMap[dateStr].push({ 252 planMap[dateStr].push({
226 name: template.name, 253 name: template.name,
227 - bg: template.backgroundColor || '#ffdd44' 254 + bg: template.backgroundColor || '#fef08a',
228 }); 255 });
229 } 256 }
230 if (template && typeof template.totalWeight === 'number') { 257 if (template && typeof template.totalWeight === 'number') {
@@ -237,57 +264,37 @@ const loadPlans = async (year, month) => { @@ -237,57 +264,37 @@ const loadPlans = async (year, month) => {
237 } 264 }
238 }); 265 });
239 266
240 - const newDates = generateCalendar(year, month).map((date) => ({ 267 + dates.value = generateCalendar(monthStr).map((date) => ({
241 ...date, 268 ...date,
242 planList: planMap[date.dateKey] || [], 269 planList: planMap[date.dateKey] || [],
243 noteList: noteMap[date.dateKey] || [], 270 noteList: noteMap[date.dateKey] || [],
244 weight: weightMap[date.dateKey] || null, 271 weight: weightMap[date.dateKey] || null,
245 })); 272 }));
246 -  
247 - dates.value = newDates;  
248 } catch (err) { 273 } catch (err) {
249 - console.error('加载日历失败:', err);  
250 - const [y, m] = [  
251 - parseInt(currentMonth.value.split('-')[0]),  
252 - parseInt(currentMonth.value.split('-')[1]),  
253 - ];  
254 - dates.value = generateCalendar(y, m); 274 + console.error('加载日历数据失败:', err);
  275 + dates.value = generateCalendar(monthStr);
255 } 276 }
256 - console.log('日历主页面处理的完的是数据:dates', dates);  
257 -  
258 }; 277 };
259 -const selectedDateData = computed(() => {  
260 - return dates.value.find(item => item.dateKey === selectedDate.value) || null;  
261 -});  
262 278
263 -// ====================== 核心显示逻辑(已修复)======================  
264 -const maxLines = 3; // 一个格子最多显示3行  
265 -  
266 -const getVisibleItems = (date) => { 279 +// ====== 视图格子显示控制 ======
  280 +const getCellVisibleItems = (date) => {
267 const items = []; 281 const items = [];
268 let remaining = 0; 282 let remaining = 0;
269 283
270 - // 1. 重量优先显示  
271 if (date.weight && date.weight > 0) { 284 if (date.weight && date.weight > 0) {
272 items.push({ type: 'weight', text: `${date.weight}kg` }); 285 items.push({ type: 'weight', text: `${date.weight}kg` });
273 } 286 }
274 287
275 - // 2. 显示训练计划(已修改)  
276 for (const plan of date.planList) { 288 for (const plan of date.planList) {
277 - if (items.length < maxLines) {  
278 - items.push({  
279 - type: 'plan',  
280 - text: plan.name,  
281 - bg: plan.bg  
282 - }); 289 + if (items.length < MAX_CELL_LINES) {
  290 + items.push({ type: 'plan', text: plan.name, bg: plan.bg });
283 } else { 291 } else {
284 remaining++; 292 remaining++;
285 } 293 }
286 } 294 }
287 295
288 - // 3. 显示备注  
289 for (const note of date.noteList) { 296 for (const note of date.noteList) {
290 - if (items.length < maxLines) { 297 + if (items.length < MAX_CELL_LINES) {
291 items.push({ type: 'note', text: note.content, bg: note.backgroundColor }); 298 items.push({ type: 'note', text: note.content, bg: note.backgroundColor });
292 } else { 299 } else {
293 remaining++; 300 remaining++;
@@ -297,153 +304,79 @@ const getVisibleItems = (date) => { @@ -297,153 +304,79 @@ const getVisibleItems = (date) => {
297 return { items, remaining }; 304 return { items, remaining };
298 }; 305 };
299 306
300 -// 接收子组件通知,刷新整个日历数据  
301 const handleRefreshCalendar = () => { 307 const handleRefreshCalendar = () => {
302 - const [year, month] = currentMonth.value.split('-').map(Number);  
303 - loadPlans(year, month); 308 + loadPlans(currentMonth.value);
304 }; 309 };
305 -// ====================================================================  
306 310
307 -// ====== 监听月份变化 ======  
308 watch( 311 watch(
309 currentMonth, 312 currentMonth,
310 (newVal) => { 313 (newVal) => {
311 - const [year, month] = newVal.split('-').map(Number);  
312 - loadPlans(year, month); 314 + if (newVal) {
  315 + loadPlans(newVal);
  316 + }
313 }, 317 },
314 - { immediate: true }, 318 + { immediate: true }
315 ); 319 );
316 320
317 const selectDate = async (date) => { 321 const selectDate = async (date) => {
318 selectedDate.value = date.dateKey; 322 selectedDate.value = date.dateKey;
319 - // ShowGridCellPopup.value = true;  
320 - await nextTick() 323 + await nextTick();
321 gridPopupRef.value?.openPopup(); 324 gridPopupRef.value?.openPopup();
322 }; 325 };
323 -  
324 </script> 326 </script>
325 327
326 <style lang="scss" scoped> 328 <style lang="scss" scoped>
327 .calendar-page { 329 .calendar-page {
328 height: 100%; 330 height: 100%;
329 - padding: 40rpx;  
330 - background-color: white; 331 + padding: 30rpx 15rpx 20rpx;
  332 + background-color: #fff;
331 box-sizing: border-box; 333 box-sizing: border-box;
332 334
333 .top-bar { 335 .top-bar {
334 display: flex; 336 display: flex;
335 justify-content: space-between; 337 justify-content: space-between;
336 align-items: center; 338 align-items: center;
337 - margin-bottom: 20rpx;  
338 - margin-top: 35rpx; 339 + margin-bottom: 24rpx;
339 340
340 - .month-picker {  
341 - height: 40rpx;  
342 - border-radius: 50rpx;  
343 - overflow: hidden;  
344 - padding: 0 20rpx;  
345 - gap: 10rpx;  
346 -  
347 - :deep(.uni-date-x--border) {  
348 - border: none !important;  
349 - }  
350 -  
351 - :deep(.uni-date__x-input) {  
352 - height: 40rpx;  
353 - line-height: 40rpx;  
354 - padding: 0 10rpx;  
355 - }  
356 -  
357 - // 微信小程序端样式适配  
358 - // #ifdef MP-WEIXIN 341 + .month-picker-pill {
359 display: flex; 342 display: flex;
360 align-items: center; 343 align-items: center;
361 - justify-content: center;  
362 - min-width: 200rpx;  
363 - box-sizing: border-box;  
364 -  
365 - :deep(.uni-date-x) {  
366 - height: 35rpx;  
367 - // min-height: 40rpx;  
368 - border-radius: 50rpx;  
369 - background-color: transparent;  
370 - padding: 0;  
371 - margin: 0;  
372 - border: none !important;  
373 -  
374 - }  
375 -  
376 - :deep(.uni-date-editor--x) {  
377 - height: 100%;  
378 - width: 100%;  
379 - display: flex;  
380 - align-items: center;  
381 - justify-content: center;  
382 - border: none !important;  
383 - }  
384 -  
385 - :deep(.uni-date__x-input) {  
386 - // height: 40rpx;  
387 - // line-height: 40rpx;  
388 - font-size: 24rpx;  
389 - color: #333;  
390 - padding: 0 10rpx;  
391 - text-align: center;  
392 - flex: 1;  
393 - overflow: hidden;  
394 - text-overflow: ellipsis;  
395 - white-space: nowrap;  
396 - }  
397 -  
398 - :deep(.icon-calendar) {  
399 - display: none !important;  
400 - }  
401 -  
402 - // #endif 344 + gap: 12rpx;
  345 + background-color: #ececec;
  346 + padding: 12rpx 28rpx;
  347 + border-radius: 40rpx;
  348 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  349 + border: 1rpx solid #fff;
  350 +
  351 + .month-text {
  352 + font-size: 30rpx;
  353 + font-weight: 600;
  354 + color: #1a1a1a;
403 } 355 }
404 } 356 }
405 357
406 .action-buttons { 358 .action-buttons {
407 display: flex; 359 display: flex;
408 - justify-content: flex-start;  
409 - gap: 10rpx;  
410 -  
411 - // .action-btn {  
412 - // padding: 20rpx 20rpx;  
413 - // margin: 0;  
414 - // line-height: 1;  
415 - // border-radius: 20rpx;  
416 - // border: 1px solid #666;  
417 - // font-size: 20rpx;  
418 - // }  
419 -  
420 - :deep(uni-button),  
421 - :deep(wx-button) {  
422 - padding: 10rpx 20rpx;  
423 - margin: 0;  
424 - line-height: 1;  
425 - border-radius: 20rpx;  
426 - border: 1px solid #666;  
427 - font-size: 20rpx; 360 + align-items: center;
428 } 361 }
429 } 362 }
430 363
431 - .calendar-grid {  
432 - padding-bottom: 120rpx;  
433 - // #ifdef MP-WEIXIN  
434 - padding-bottom: 140rpx;  
435 - // #endif 364 + .calendar-card {
  365 + background-color: #ffffff;
  366 + border-radius: 24rpx;
  367 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
436 368
437 .week-header { 369 .week-header {
438 display: flex; 370 display: flex;
439 justify-content: space-between; 371 justify-content: space-between;
440 - margin-bottom: 10rpx; 372 + margin-bottom: 20rpx;
441 373
442 .week-day { 374 .week-day {
443 flex: 1; 375 flex: 1;
444 text-align: center; 376 text-align: center;
445 - font-size: 28rpx;  
446 - color: #666; 377 + font-size: 26rpx;
  378 + font-weight: 550;
  379 +
447 } 380 }
448 } 381 }
449 382
@@ -453,19 +386,55 @@ const selectDate = async (date) => { @@ -453,19 +386,55 @@ const selectDate = async (date) => {
453 gap: 10rpx; 386 gap: 10rpx;
454 } 387 }
455 388
  389 + /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
456 .date-cell { 390 .date-cell {
457 display: flex; 391 display: flex;
458 flex-direction: column; 392 flex-direction: column;
459 align-items: center; 393 align-items: center;
460 justify-content: flex-start; 394 justify-content: flex-start;
461 - border: 1px solid #eee;  
462 - font-size: 30rpx;  
463 - color: #333;  
464 - background-color: #f9fafc;  
465 - padding-top: 10rpx; 395 + border-radius: 12rpx;
466 height: 200rpx; 396 height: 200rpx;
467 - overflow: hidden; 397 + padding: 8rpx 4rpx;
468 box-sizing: border-box; 398 box-sizing: border-box;
  399 + transition: all 0.2s ease;
  400 +
  401 + &:active {
  402 + filter: brightness(0.96);
  403 + }
  404 +
  405 + &.other-month {
  406 + opacity: 0.5;
  407 + border-color: #f1f5f9;
  408 + }
  409 +
  410 + &.is-today {
  411 + border: 2rpx solid #3b82f6;
  412 + }
  413 +
  414 + &.has-data {
  415 + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);
  416 + }
  417 +
  418 + .date-header {
  419 + height: 36rpx;
  420 + display: flex;
  421 + align-items: center;
  422 + justify-content: center;
  423 + margin-bottom: 6rpx;
  424 +
  425 + .date-number {
  426 + font-size: 26rpx;
  427 + font-weight: 600;
  428 + color: #1f2937;
  429 +
  430 +
  431 + }
  432 +
  433 + .today-badge {
  434 +
  435 + font-weight: 600;
  436 +
  437 + }
469 } 438 }
470 439
471 .plan-list { 440 .plan-list {
@@ -473,159 +442,126 @@ const selectDate = async (date) => { @@ -473,159 +442,126 @@ const selectDate = async (date) => {
473 display: flex; 442 display: flex;
474 flex-direction: column; 443 flex-direction: column;
475 gap: 6rpx; 444 gap: 6rpx;
476 - margin-top: 10rpx;  
477 - padding: 0; 445 +
  446 + .plan-tag {
  447 + width: 100%;
  448 + border-radius: 6rpx;
  449 + padding: 2rpx 4rpx;
478 box-sizing: border-box; 450 box-sizing: border-box;
479 - } 451 + display: flex;
  452 + align-items: center;
  453 + justify-content: center;
  454 + background-color: rgba(255, 255, 255, 0.85);
  455 + /* 使 Tag 悬浮于彩色格子之上 */
480 456
481 - .weight,  
482 - .plan-extra,  
483 - .note-item,  
484 - .plan-name { 457 + .tag-text {
485 font-size: 18rpx; 458 font-size: 18rpx;
486 - background: #ffdd44;  
487 - padding: 4rpx 0;  
488 - text-align: center; 459 + line-height: 22rpx;
  460 + color: #111827;
489 overflow: hidden; 461 overflow: hidden;
490 white-space: nowrap; 462 white-space: nowrap;
491 text-overflow: ellipsis; 463 text-overflow: ellipsis;
  464 + text-align: center;
492 width: 100%; 465 width: 100%;
493 - box-sizing: border-box;  
494 } 466 }
495 467
496 - .weight,  
497 - .weight-item {  
498 - background: #b8ff66; 468 + &.tag-type-weight {
  469 + background-color: #bbf7d0 !important;
  470 +
  471 + .tag-text {
  472 + color: #14532d;
  473 + font-weight: 700;
499 } 474 }
500 } 475 }
501 -}  
502 476
503 -/* 日历说明弹窗样式 */  
504 -.explain-popup-overlay {  
505 - position: fixed;  
506 - top: 0;  
507 - left: 0;  
508 - right: 0;  
509 - bottom: 0;  
510 - background-color: rgba(0, 0, 0, 0.4);  
511 - display: flex;  
512 - justify-content: center;  
513 - align-items: flex-end;  
514 - z-index: 999; 477 + &.tag-type-note {
  478 + background-color: #ffffff;
515 479
516 - .explain-popup {  
517 - width: 100%; 480 + .tag-text {
  481 + color: #374151;
  482 + }
  483 + }
  484 + }
  485 +
  486 + .plan-extra {
  487 + font-size: 16rpx;
  488 + color: #4b5563;
  489 + text-align: center;
  490 + line-height: 20rpx;
  491 + font-weight: 600;
  492 + }
  493 + }
  494 + }
  495 + }
  496 +}
  497 +
  498 +.explain-popup-content {
  499 + padding: 36rpx 32rpx 48rpx 32rpx;
518 background-color: #ffffff; 500 background-color: #ffffff;
519 - border-radius: 24rpx 24rpx 0 0;  
520 - padding: 40rpx;  
521 - box-sizing: border-box;  
522 501
523 .explain-header { 502 .explain-header {
524 display: flex; 503 display: flex;
525 justify-content: space-between; 504 justify-content: space-between;
526 align-items: center; 505 align-items: center;
527 - margin-bottom: 40rpx; 506 + margin-bottom: 32rpx;
528 507
529 .explain-title { 508 .explain-title {
530 font-size: 32rpx; 509 font-size: 32rpx;
531 - font-weight: 60;  
532 - color: #333333;  
533 - }  
534 -  
535 - .close-btn {  
536 - width: 48rpx;  
537 - height: 48rpx;  
538 - border-radius: 50%;  
539 - background-color: #f5f5f5;  
540 - display: flex;  
541 - justify-content: center;  
542 - align-items: center;  
543 -  
544 - text {  
545 - font-size: 36rpx;  
546 - color: #666666;  
547 - line-height: 1;  
548 - } 510 + font-weight: 600;
  511 + color: #111827;
549 } 512 }
550 } 513 }
551 514
552 .explain-list { 515 .explain-list {
  516 + display: flex;
  517 + flex-direction: column;
  518 + gap: 28rpx;
  519 +
553 .explain-item { 520 .explain-item {
554 display: flex; 521 display: flex;
555 align-items: center; 522 align-items: center;
556 - margin-bottom: 32rpx;  
557 523
558 .color-block { 524 .color-block {
559 - width: 40rpx;  
560 - height: 40rpx; 525 + width: 36rpx;
  526 + height: 36rpx;
561 border-radius: 8rpx; 527 border-radius: 8rpx;
562 - margin-right: 20rpx; 528 + margin-right: 24rpx;
563 529
564 &.blue { 530 &.blue {
565 - background-color: #a8d8ff; 531 + background-color: #bae6fd;
566 } 532 }
567 533
568 &.green { 534 &.green {
569 - background-color: #b8ff66; 535 + background-color: #bbf7d0;
570 } 536 }
571 537
572 &.yellow { 538 &.yellow {
573 - background-color: #ffdd44; 539 + background-color: #fef08a;
574 } 540 }
575 541
576 &.gray { 542 &.gray {
577 - background-color: #f0f0f0;  
578 - border: 1rpx solid #eee; 543 + background-color: #e5e7eb;
579 } 544 }
580 } 545 }
581 546
  547 + .item-info {
  548 + display: flex;
  549 + align-items: center;
  550 + gap: 16rpx;
  551 +
582 .item-label { 552 .item-label {
583 font-size: 28rpx; 553 font-size: 28rpx;
584 - color: #333333;  
585 - width: 80rpx;  
586 - margin-right: 20rpx; 554 + font-weight: 600;
  555 + color: #1f2937;
  556 + width: 72rpx;
587 } 557 }
588 558
589 .item-desc { 559 .item-desc {
590 - font-size: 28rpx;  
591 - color: #666666;  
592 - flex: 1;  
593 - } 560 + font-size: 26rpx;
  561 + color: #6b7280;
594 } 562 }
595 } 563 }
596 } 564 }
597 -}  
598 -  
599 -// .gridCellContent {  
600 -// position: fixed;  
601 -// top: 0;  
602 -// left: 0;  
603 -// right: 0;  
604 -// bottom: 0;  
605 -// background: rgba(0, 0, 0, 0.5);  
606 -// z-index: 9999;  
607 -// display: flex;  
608 -// align-items: flex-end;  
609 -// padding: 0 !important;  
610 -// margin: 0 !important;  
611 -// width: 100vw !important;  
612 -// box-sizing: border-box;  
613 -// }  
614 -</style>  
615 -  
616 -<!-- #ifdef MP-WEIXIN -->  
617 -<style lang="scss">  
618 -.month-picker {  
619 -  
620 - .uni-date-editor--x.uni-date-x--border,  
621 - .uni-date-x--border {  
622 - border: none !important;  
623 - }  
624 -  
625 - .uni-date-x,  
626 - .uni-date-editor--x {  
627 - border: none !important;  
628 } 565 }
629 } 566 }
630 </style> 567 </style>
631 -<!-- #endif -->  
1 <template> 1 <template>
2 <view class="training-data-page"> 2 <view class="training-data-page">
  3 + <!-- 顶部时间范围选择器 -->
  4 + <view class="top-time-picker">
3 <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> 5 <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />
  6 + </view>
4 7
5 - <!-- 过渡动画 --> 8 + <!-- 主体内容可滚动区域 -->
  9 + <view class="main-content">
  10 + <scroll-view scroll-y class="scroll-area" :show-scrollbar="false">
6 <up-transition :show="true" mode="slide-right" duration="300"> 11 <up-transition :show="true" mode="slide-right" duration="300">
7 - <view class="content-scroll">  
8 - <!-- 肌肉部位数据区域 -->  
9 - <view class="section">  
10 - <view class="muscle-data">  
11 - <view class="title">主训练肌肉数据</view>  
12 - <view class="tabs">  
13 - <view class="tab" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">容量</view>  
14 - <view class="tab" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">组数</view> 12 + <view class="scroll-inner">
  13 + <!-- 1. 肌肉部位数据区域 -->
  14 + <view class="card-section">
  15 + <view class="section-header">
  16 + <text class="title">主训练肌肉数据</text>
15 </view> 17 </view>
16 - <view class="tip">{{ tabTip[partTabIndex] }}</view>  
17 18
18 - <!-- 网格布局:一行4格 + 正方形 + 总计自适应 -->  
19 - <view class="detail-part" :style="`--remain-span: ${remainSpan}`">  
20 - <view class="part-item" v-for="item in detailPartOptions" :key="item.id"  
21 - @click="openMuscleDetail(item.id)">  
22 - <text class=" txt">{{ item.name }}</text>  
23 - <text class="num">{{ item.value }}</text> 19 + <!-- 容量 / 组数 切换标签 -->
  20 + <view class="tab-group">
  21 + <view class="tab-item" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">
  22 + 容量
  23 + </view>
  24 + <view class="tab-item" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">
  25 + 组数
24 </view> 26 </view>
25 - <view class="part-item total-item">  
26 - <text class="txt">总计</text>  
27 - <text class="num">{{ totalPartValue }}</text>  
28 </view> 27 </view>
  28 + <text class="tip-text">{{ TREND_TIPS[partTabIndex] }}</text>
  29 +
  30 + <!-- 肌肉部位网格(1行4格 + 正方形 + 总计自适应占格) -->
  31 + <view class="muscle-grid">
  32 + <view class="grid-item" v-for="item in detailPartOptions" :key="item.id"
  33 + @click="openMuscleDetail(item.id)">
  34 + <text class="item-name">{{ item.name }}</text>
  35 + <text class="item-value">{{ item.value }}</text>
  36 + </view>
  37 +
  38 + <!-- 总计格子 -->
  39 + <view class="grid-item total-item" :style="{ gridColumn: `span ${remainSpan}` }">
  40 + <text class="item-name">总计</text>
  41 + <text class="item-value highlight">{{ totalPartValue }}</text>
29 </view> 42 </view>
30 </view> 43 </view>
31 </view> 44 </view>
32 45
33 - <!-- 趋势图表区域 -->  
34 - <view class="section">  
35 - <view class="header">  
36 - <view class="title">趋势</view>  
37 - <view class="mean">  
38 - <up-select v-model:current="partId" :label="partOptions.find(item => item.id === partId)?.name || '全部'"  
39 - :options="partOptions" @select="onPartChange"></up-select> 46 + <!-- 2. 趋势图表区域 -->
  47 + <view class="card-section">
  48 + <view class="section-header space-between">
  49 + <text class="title">趋势</text>
  50 + <view class="part-select-wrapper">
  51 + <up-select v-model:current="partId"
  52 + :label="partOptions.find((item) => item.id === partId)?.name || '全部'" :options="partOptions"
  53 + @select="onPartChange"></up-select>
40 </view> 54 </view>
41 </view> 55 </view>
42 - <view class="tabs">  
43 - <view class="tab" v-for="item in trendTabs" :key="item.id" :class="{ active: NumberTabIndex === item.id }"  
44 - @click="NumberTabIndex = item.id"> 56 +
  57 + <!-- 趋势维度切换 Tabs -->
  58 + <view class="tab-group flex-wrap">
  59 + <view class="tab-item" v-for="item in trendTabs" :key="item.id"
  60 + :class="{ active: NumberTabIndex === item.id }" @click="NumberTabIndex = item.id">
45 {{ item.name }} 61 {{ item.name }}
46 </view> 62 </view>
47 </view> 63 </view>
48 - <view class="tip">{{ tabTip[NumberTabIndex] }}</view> 64 + <text class="tip-text">{{ TREND_TIPS[NumberTabIndex] }}</text>
49 65
50 - <view class="statistical"> 66 + <!-- 统计图表(根据 showLastPeriod 切换 柱状图 / 折线图) -->
  67 + <view class="chart-container">
51 <template v-if="!showLastPeriod"> 68 <template v-if="!showLastPeriod">
52 <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries" 69 <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
53 chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" /> 70 chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
@@ -58,15 +75,18 @@ @@ -58,15 +75,18 @@
58 </template> 75 </template>
59 </view> 76 </view>
60 77
61 - <view class="chose">  
62 - <checkbox :checked="showLastPeriod" @click="toggleLastWeek" /> 78 + <!-- 显示上周/上月数据勾选框 -->
  79 + <view class="toggle-checkbox" @click="toggleLastWeek">
  80 + <checkbox :checked="showLastPeriod" color="#10b981" style="transform: scale(0.8);" />
  81 + <text class="toggle-label">
63 {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }} 82 {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
  83 + </text>
64 </view> 84 </view>
65 85
66 - <!-- 数据对比 -->  
67 - <view class="compare">  
68 - <view class="header-row">  
69 - <text class="title">对比</text> 86 + <!-- 3. 数据对比区域 -->
  87 + <view class="compare-section">
  88 + <view class="compare-header">
  89 + <text class="sub-title">对比</text>
70 <view class="legend-group"> 90 <view class="legend-group">
71 <view class="legend-item"> 91 <view class="legend-item">
72 <view class="dot blue"></view> 92 <view class="dot blue"></view>
@@ -81,13 +101,15 @@ @@ -81,13 +101,15 @@
81 </view> 101 </view>
82 </view> 102 </view>
83 </view> 103 </view>
  104 +
  105 + <!-- 对比数据卡片 -->
84 <view class="compare-card"> 106 <view class="compare-card">
85 <view class="data-list"> 107 <view class="data-list">
86 <view v-for="(item, index) in listData" :key="index" class="data-row"> 108 <view v-for="(item, index) in listData" :key="index" class="data-row">
87 <view class="label">{{ item.label }}</view> 109 <view class="label">{{ item.label }}</view>
88 - <view class="value">{{ item.lastPeriod }}</view>  
89 - <view class="value">{{ item.currPeriod }}</view>  
90 - <view class="value diff" :class="{ negative: item.diff < 0 }"> 110 + <view class="value text-secondary">{{ item.lastPeriod }}</view>
  111 + <view class="value text-main">{{ item.currPeriod }}</view>
  112 + <view class="value diff" :class="{ positive: item.diff > 0, negative: item.diff < 0 }">
91 {{ item.diff > 0 ? '+' + item.diff : item.diff }} 113 {{ item.diff > 0 ? '+' + item.diff : item.diff }}
92 </view> 114 </view>
93 </view> 115 </view>
@@ -98,22 +120,17 @@ @@ -98,22 +120,17 @@
98 </view> 120 </view>
99 </up-transition> 121 </up-transition>
100 122
101 - <!-- 肌肉详情弹窗 -->  
102 - <!-- <up-popup :show="muscleShow" mode="top" @close="muscleShow = false" safeAreaInsetTop>  
103 - <view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }">  
104 - <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />  
105 - <view class="statistical">  
106 - <LineChart v-if="muscleShow" @close="closeMuscleDetail" :categories="muscleChartCategories" :series="[  
107 - { name: '容量', data: muscleVolumeArr },  
108 - { name: '组数', data: muscleSetsArr }  
109 - ]" chartType="column" />  
110 - </view> 123 + <!-- 底部安全区域间距 -->
  124 + <view class="bottom-spacer"></view>
  125 + </scroll-view>
111 </view> 126 </view>
112 - </up-popup> -->  
113 - <up-popup :show="muscleShow" mode="top" @close="closeMuscleDetail" safeAreaInsetTop> 127 +
  128 + <!-- 肌肉详情弹窗 -->
  129 + <up-popup :show="muscleShow" mode="top" round="24rpx" @close="closeMuscleDetail" safeAreaInsetTop>
114 <view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }"> 130 <view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }">
115 <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> 131 <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />
116 132
  133 + <view class="popup-body">
117 <!-- 肌肉名称 --> 134 <!-- 肌肉名称 -->
118 <view class="muscle-name"> 135 <view class="muscle-name">
119 {{ currentMuscleName }} 136 {{ currentMuscleName }}
@@ -121,27 +138,30 @@ @@ -121,27 +138,30 @@
121 138
122 <!-- 容量/组数 切换标签 --> 139 <!-- 容量/组数 切换标签 -->
123 <view class="muscle-tabs"> 140 <view class="muscle-tabs">
124 - <view class="tab" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0">容量</view>  
125 - <view class="tab" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1">组数</view> 141 + <view class="tab-item" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0">
  142 + 容量
  143 + </view>
  144 + <view class="tab-item" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1">
  145 + 组数
  146 + </view>
126 </view> 147 </view>
127 - <view class="tip">{{ tabTip[muscleTabIndex] }}</view> 148 + <view class="tip">{{ TREND_TIPS[muscleTabIndex] }}</view>
128 149
129 <!-- 图表区域 --> 150 <!-- 图表区域 -->
130 <view class="statistical"> 151 <view class="statistical">
131 - <!-- <LineChart v-if="muscleShow" :categories="muscleChartCategories" :series="muscleChartSeries"  
132 - chartType="column" /> -->  
133 <LineChart v-if="muscleShow && muscleChartCategories.length" 152 <LineChart v-if="muscleShow && muscleChartCategories.length"
134 :key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`" 153 :key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`"
135 :categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true" 154 :categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true"
136 :extra="{ column: { width: muscleChartColumnWidth } }" /> 155 :extra="{ column: { width: muscleChartColumnWidth } }" />
137 </view> 156 </view>
138 </view> 157 </view>
  158 + </view>
139 </up-popup> 159 </up-popup>
140 -  
141 </view> 160 </view>
142 </template> 161 </template>
143 162
144 <script setup> 163 <script setup>
  164 +import dayjs from 'dayjs';
145 import LineChart from './LineChart.vue'; 165 import LineChart from './LineChart.vue';
146 import { ref, computed, onMounted } from 'vue'; 166 import { ref, computed, onMounted } from 'vue';
147 import timeSelect from './time-select.vue'; 167 import timeSelect from './time-select.vue';
@@ -149,303 +169,263 @@ import { getMenuButtonHeight } from '@/utils/safeArea'; @@ -149,303 +169,263 @@ import { getMenuButtonHeight } from '@/utils/safeArea';
149 import TrainingDataApi from '@/sheep/api/TrainingData/Data.js'; 169 import TrainingDataApi from '@/sheep/api/TrainingData/Data.js';
150 import LeftmotionApi from '@/sheep/api/motion/equipments'; 170 import LeftmotionApi from '@/sheep/api/motion/equipments';
151 171
152 -const statusBarHeight = getMenuButtonHeight();  
153 -  
154 -// 时间范围  
155 -const startDate = ref('')  
156 -const endDate = ref('')  
157 -  
158 -// 肌肉弹窗  
159 -// 弹窗的容量/组数切换索引  
160 -const muscleTabIndex = ref(0)  
161 -const muscleShow = ref(false);  
162 -const currentMuscleName = ref('')  
163 -const selectLabel = ref('全部')  
164 -const partId = ref('');  
165 -const partOptions = ref([]);  
166 -  
167 -const weeklyData = ref(null); 172 +// ==================== 常量 ====================
168 173
169 -// 加载锁  
170 -const weeklyLoading = ref(false);  
171 -const trainingLoading = ref(false);  
172 -const muscleListLoading = ref(false); 174 +const WEEKDAY_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
173 175
174 -// 时间类型 week / month  
175 -const MainCurrentDateType = ref('week'); 176 +/** 趋势维度文案 */
  177 +const TREND_DATA_KEYS = ['volume', 'setCount', 'duration', 'distance', 'count'];
176 178
177 -// 底部提示文案  
178 -const tabTip = ref([ 179 +const TREND_TIPS = [
179 '重量X组数X次数,用来评估训练的整体强度(kg)', 180 '重量X组数X次数,用来评估训练的整体强度(kg)',
180 '该时间段内训练组数趋势(组)', 181 '该时间段内训练组数趋势(组)',
181 '累计计时类动作的时长(min)', 182 '累计计时类动作的时长(min)',
182 '累计距离类动作的距离(km)', 183 '累计距离类动作的距离(km)',
183 '累计次数类动作的次数(次)', 184 '累计次数类动作的次数(次)',
184 -]); 185 +];
185 186
186 -// 趋势标签  
187 -const NumberTabIndex = ref(0) 187 +// ==================== 日期工具函数 ====================
  188 +
  189 +/** 格式化日期为 YYYY-MM-DD */
  190 +const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD');
  191 +
  192 +/** 获取包含指定日期的完整周日期列表(周一~周日),返回 dayjs 实例数组 */
  193 +const getWeekFullDates = (baseDateStr) => {
  194 + const d = dayjs(baseDateStr);
  195 + const dayOfWeek = d.day(); // 0=周日, 1=周一, ...
  196 + const monday = d.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day').startOf('day');
  197 + return Array.from({ length: 7 }, (_, i) => monday.add(i, 'day'));
  198 +};
  199 +
  200 +/** 获取指定日期所在月份的全部日期列表,返回 dayjs 实例数组 */
  201 +const getMonthFullDates = (baseDateStr) => {
  202 + const d = dayjs(baseDateStr);
  203 + const daysInMonth = d.daysInMonth();
  204 + return Array.from({ length: daysInMonth }, (_, i) =>
  205 + d.date(i + 1).startOf('day')
  206 + );
  207 +};
  208 +
  209 +/** 解析 API 返回的日期数组 [year, month, day] 为 dayjs 实例 */
  210 +const parseApiDate = (apiDateArr) => {
  211 + if (!Array.isArray(apiDateArr) || apiDateArr.length !== 3) return null;
  212 + const [year, month, day] = apiDateArr;
  213 + return dayjs(`${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
  214 +};
  215 +
  216 +/** 趋势推荐维度按周/月数据加载 */
  217 +const loadCategoryData = (date) => {
  218 + const loader = MainCurrentDateType.value === 'week'
  219 + ? loadWeeklyByCategory
  220 + : loadMonthlyByCategory;
  221 + return loader(date);
  222 +};
  223 +
  224 +/** 肌肉详情按周/月数据加载 */
  225 +const loadMuscleData = (muscleId, date) => {
  226 + const isWeekly = MainCurrentDateType.value === 'week';
  227 + return loadMuscleStats(muscleId, date, isWeekly);
  228 +};
  229 +
  230 +// ==================== 响应式状态 ====================
  231 +
  232 +const statusBarHeight = getMenuButtonHeight();
  233 +
  234 +// --- 时间范围 ---
  235 +const startDate = ref('');
  236 +const endDate = ref('');
  237 +const MainCurrentDateType = ref('week'); // 'week' | 'month'
  238 +
  239 +// --- 筛选状态 ---
  240 +const partId = ref('');
  241 +const partOptions = ref([]);
  242 +const showLastPeriod = ref(false);
  243 +
  244 +// --- 趋势 Tab 状态 ---
  245 +const NumberTabIndex = ref(0);
188 const trendTabs = ref([ 246 const trendTabs = ref([
189 { id: 0, name: '容量' }, 247 { id: 0, name: '容量' },
190 { id: 1, name: '组数' }, 248 { id: 1, name: '组数' },
191 { id: 2, name: '时长' }, 249 { id: 2, name: '时长' },
192 { id: 3, name: '距离' }, 250 { id: 3, name: '距离' },
193 { id: 4, name: '次数' }, 251 { id: 4, name: '次数' },
194 -]) 252 +]);
195 253
196 -// 肌肉部位标签 254 +// --- 肌肉部位 Tab ---
197 const partTabIndex = ref(0); 255 const partTabIndex = ref(0);
198 256
199 -// 核心:格式化肌肉部位数据,支持容量/组数切换  
200 -const detailPartOptions = computed(() => {  
201 - // 没有肌肉数据时返回空数组  
202 - if (!subCategorieList.value.length) return [] 257 +// --- 加载锁 ---
  258 +const weeklyLoading = ref(false);
  259 +const trainingLoading = ref(false);
  260 +const muscleListLoading = ref(false);
203 261
204 - // 根据当前选中的标签,决定取哪个数据  
205 - const isVolume = partTabIndex.value === 0 // 0是容量,1是组数  
206 - const dataMap = isVolume ? trainingData.value.totalVolumeMap : trainingData.value.setCountMap 262 +// --- 数据 ---
  263 +const weeklyData = ref(null);
  264 +const subCategorieList = ref([]);
  265 +const trainingData = ref({ setCountMap: {}, totalVolumeMap: {} });
207 266
208 - // 把 subCategorieList 里的每一项,加上对应的数据  
209 - return subCategorieList.value.map(item => {  
210 - return {  
211 - id: item.id,  
212 - name: item.name,  
213 - // 从 dataMap 里取数据,没有的话默认0  
214 - value: dataMap[item.id] ?? 0  
215 - }  
216 - })  
217 -})  
218 -  
219 -const totalPartValue = computed(() => {  
220 - return detailPartOptions.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0)  
221 -})  
222 -  
223 -// 弹窗用的完整日期列表(周/月模式下的所有日期)  
224 -const muscleFullDates = ref([])  
225 -const muscleProcessedData = ref({  
226 - volumeArr: [],  
227 - setsArr: []  
228 -})  
229 -  
230 -// 核心:计算最后一行剩余格子数,用于总计自适应占格  
231 -const itemTotal = computed(() => detailPartOptions.value.length)  
232 -const remainSpan = computed(() => {  
233 - const mod = itemTotal.value % 4  
234 - return mod === 0 ? 4 : 4 - mod  
235 -}) 267 +// --- 肌肉弹窗 ---
  268 +const muscleShow = ref(false);
  269 +const muscleTabIndex = ref(0);
  270 +const currentMuscleId = ref(null);
  271 +const currentMuscleName = ref('');
  272 +const muscleDetailData = ref({ muscleId: 0, dailyStatsList: [] });
  273 +const muscleFullDates = ref([]);
  274 +const muscleProcessedData = ref({ volumeArr: [], setsArr: [] });
236 275
237 -const showLastPeriod = ref(false); 276 +// ==================== 计算属性 ====================
238 277
239 -// 周/月 图表类目 & 数据  
240 -const weekCategories = ref(['周一', '周二', '周三', '周四', '周五', '周六', '周日']); 278 +/** 肌肉部位数据列表(支持容量/组数切换) */
  279 +const detailPartOptions = computed(() => {
  280 + if (!subCategorieList.value.length) return [];
241 281
242 -const lastMonthData = ref(Array(31).fill(0)); 282 + const isVolume = partTabIndex.value === 0;
  283 + const dataMap = isVolume
  284 + ? trainingData.value.totalVolumeMap
  285 + : trainingData.value.setCountMap;
243 286
244 -// ✅ 修复版:强制生成完整的周/月日期,就算没数据也要显示横轴  
245 -const chartCategories = computed(() => {  
246 - if (!weeklyData.value) return [] 287 + return subCategorieList.value.map((item) => ({
  288 + id: item.id,
  289 + name: item.name,
  290 + value: dataMap[item.id] ?? 0,
  291 + }));
  292 +});
247 293
248 - if (MainCurrentDateType.value === 'week') {  
249 - // 周模式:生成周一到周日的完整日期(月/日格式)  
250 - const weekDates = getWeekFullDates(startDate.value).map(d => {  
251 - const month = d.getMonth() + 1  
252 - const day = d.getDate()  
253 - return `${month}/${day}`  
254 - })  
255 - return weekDates  
256 - } else {  
257 - // 月模式:生成当月1日到月底的完整日期(纯日期格式)  
258 - const monthDates = getMonthFullDates(startDate.value).map(d => {  
259 - return `${d.getDate()}`  
260 - })  
261 - return monthDates  
262 - }  
263 -}) 294 +/** 肌肉部位总计 */
  295 +const totalPartValue = computed(() =>
  296 + detailPartOptions.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0)
  297 +);
264 298
265 -// 趋势图表动态柱子宽度:周20,月3  
266 -const trendChartColumnWidth = computed(() => {  
267 - return MainCurrentDateType.value === 'week' ? 15 : 3  
268 -}) 299 +/** 总计格子剩余 span(补齐最后一行) */
  300 +const remainSpan = computed(() => {
  301 + const mod = detailPartOptions.value.length % 4;
  302 + return mod === 0 ? 4 : 4 - mod;
  303 +});
269 304
  305 +/** 图表 X 轴日期标签 */
  306 +const chartCategories = computed(() => {
  307 + if (!weeklyData.value) return [];
270 308
  309 + const fullDates = MainCurrentDateType.value === 'week'
  310 + ? getWeekFullDates(startDate.value)
  311 + : getMonthFullDates(startDate.value);
271 312
272 -// 动态控制柱状图宽度:周15,月5  
273 -const muscleChartColumnWidth = computed(() => {  
274 - return MainCurrentDateType.value === 'week' ? 15 : 3  
275 -}) 313 + return MainCurrentDateType.value === 'week'
  314 + ? fullDates.map((d) => d.format('M/D'))
  315 + : fullDates.map((d) => d.format('D'));
  316 +});
276 317
277 -const trendTabIndex = ref(0); 318 +/** 柱体宽度 */
  319 +const trendChartColumnWidth = computed(() =>
  320 + MainCurrentDateType.value === 'week' ? 15 : 3
  321 +);
278 322
279 -// ✅ 修复版:对比表数据,直接从接口取上周/上月总数据  
280 -const listData = computed(() => {  
281 - if (!weeklyData.value) return []  
282 -  
283 - const curr = weeklyData.value  
284 - // 上周/上月的总数据,接口里的字段名是 lastWeekTotalXXX  
285 - const last = weeklyData.value  
286 -  
287 - return [  
288 - {  
289 - label: '容量',  
290 - lastPeriod: last.lastWeekTotalVolume ?? 0,  
291 - currPeriod: curr.totalVolume ?? 0,  
292 - diff: (curr.totalVolume ?? 0) - (last.lastWeekTotalVolume ?? 0)  
293 - },  
294 - {  
295 - label: '组数',  
296 - lastPeriod: last.lastWeekTotalSets ?? 0,  
297 - currPeriod: curr.totalSets ?? 0,  
298 - diff: (curr.totalSets ?? 0) - (last.lastWeekTotalSets ?? 0)  
299 - },  
300 - {  
301 - label: '时长',  
302 - lastPeriod: last.lastWeekTotalDuration ?? 0,  
303 - currPeriod: curr.totalDuration ?? 0,  
304 - diff: (curr.totalDuration ?? 0) - (last.lastWeekTotalDuration ?? 0)  
305 - },  
306 - {  
307 - label: '距离',  
308 - lastPeriod: last.lastWeekTotalDistance ?? 0,  
309 - currPeriod: curr.totalDistance ?? 0,  
310 - diff: (curr.totalDistance ?? 0) - (last.lastWeekTotalDistance ?? 0)  
311 - },  
312 - {  
313 - label: '次数',  
314 - lastPeriod: last.lastWeekTotalCount ?? 0,  
315 - currPeriod: curr.totalCount ?? 0,  
316 - diff: (curr.totalCount ?? 0) - (last.lastWeekTotalCount ?? 0)  
317 - }  
318 - ]  
319 -}) 323 +const muscleChartColumnWidth = computed(() =>
  324 + MainCurrentDateType.value === 'week' ? 15 : 3
  325 +);
320 326
  327 +/** 趋势图 Series 数据 */
321 const chartSeries = computed(() => { 328 const chartSeries = computed(() => {
322 - if (!weeklyData.value) return [] 329 + if (!weeklyData.value) return [];
323 330
324 - // 1. 获取当前周/月完整日期列表  
325 - let fullDateList = MainCurrentDateType.value === 'week' 331 + const isWeekly = MainCurrentDateType.value === 'week';
  332 + const fullDateList = isWeekly
326 ? getWeekFullDates(startDate.value) 333 ? getWeekFullDates(startDate.value)
327 - : getMonthFullDates(startDate.value)  
328 -  
329 - // 转成 YYYY-MM-DD  
330 - const fullDateKeys = fullDateList.map(d => {  
331 - const y = d.getFullYear()  
332 - const m = String(d.getMonth() + 1).padStart(2, '0')  
333 - const day = String(d.getDate()).padStart(2, '0')  
334 - return `${y}-${m}-${day}`  
335 - })  
336 - // weeklyData是周/月返回的数据  
337 - const currDataMap = MainCurrentDateType.value === 'week' 334 + : getMonthFullDates(startDate.value);
  335 +
  336 + const fullDateKeys = fullDateList.map((d) => formatDateKey(d));
  337 +
  338 + const currDataMap = isWeekly
338 ? weeklyData.value.weeklyDataMap || {} 339 ? weeklyData.value.weeklyDataMap || {}
339 : weeklyData.value.monthlyDataMap || {}; 340 : weeklyData.value.monthlyDataMap || {};
340 341
341 - const lastDataMap = MainCurrentDateType.value === 'week' 342 + const lastDataMap = isWeekly
342 ? weeklyData.value.lastWeekDataMap || {} 343 ? weeklyData.value.lastWeekDataMap || {}
343 : weeklyData.value.lastMonthDataMap || {}; 344 : weeklyData.value.lastMonthDataMap || {};
344 - const keyMap = ['volume', 'setCount', 'duration', 'distance', 'count']  
345 - const dataKey = keyMap[NumberTabIndex.value] || 'volume'  
346 345
347 - // 本周/本月数据  
348 - const currData = fullDateKeys.map(dateStr => currDataMap[dateStr]?.[dataKey] ?? 0) 346 + const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
349 347
350 - // ✅ 加这行,看控制台打印的 currData 数组  
351 - console.log('【月数据数组 currData】', currData);  
352 - console.log('【当前选中的 dataKey】', dataKey);  
353 - console.log('【接口返回的 monthlyDataMap】', weeklyData.value.monthlyDataMap); 348 + // 当前周期数据
  349 + const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0);
354 350
355 - // ==========================================  
356 - // 核心:周减7天 / 月减1个月(自动适配大小月)  
357 - // ==========================================  
358 - const lastData = fullDateKeys.map(dateStr => {  
359 - const now = new Date(dateStr)  
360 - const target = new Date(now)  
361 -  
362 - if (MainCurrentDateType.value === 'week') {  
363 - target.setDate(target.getDate() - 7)  
364 - } else {  
365 - target.setMonth(target.getMonth() - 1) // ✅ JS自动处理大小月  
366 - }  
367 -  
368 - const y = target.getFullYear()  
369 - const m = String(target.getMonth() + 1).padStart(2, '0')  
370 - const day = String(target.getDate()).padStart(2, '0')  
371 - const key = `${y}-${m}-${day}`  
372 -  
373 - return lastDataMap[key]?.[dataKey] ?? 0  
374 - }) 351 + // 上一周期数据(按日期偏移匹配)
  352 + const lastData = fullDateKeys.map((dateStr) => {
  353 + const targetKey = isWeekly
  354 + ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD')
  355 + : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD');
  356 + return lastDataMap[targetKey]?.[dataKey] ?? 0;
  357 + });
375 358
376 - const currName = MainCurrentDateType.value === 'week' ? '本周' : '本月'  
377 - const lastName = MainCurrentDateType.value === 'week' ? '上周' : '上月' 359 + const currName = isWeekly ? '本周' : '本月';
  360 + const lastName = isWeekly ? '上周' : '上月';
378 361
379 return showLastPeriod.value 362 return showLastPeriod.value
380 ? [ 363 ? [
381 { name: lastName, data: lastData, color: '#5b8ff9' }, 364 { name: lastName, data: lastData, color: '#5b8ff9' },
382 - { name: currName, data: currData, color: '#ffc53d' } 365 + { name: currName, data: currData, color: '#ffc53d' },
383 ] 366 ]
384 - : [{ name: currName, data: currData, color: '#ffc53d' }]  
385 -}) 367 + : [{ name: currName, data: currData, color: '#ffc53d' }];
  368 +});
386 369
387 -// 处理肌肉弹窗的图标坐标数据  
388 -// 1. 根据当前日期,生成周模式的完整日期列表(周一到周日)  
389 -const getWeekFullDates = (baseDateStr) => {  
390 - const date = new Date(baseDateStr)  
391 - const day = date.getDay() // 0=周日, 1=周一...  
392 - const monday = new Date(date)  
393 - monday.setDate(date.getDate() - (day === 0 ? 6 : day - 1))  
394 -  
395 - const dates = []  
396 - for (let i = 0; i < 7; i++) {  
397 - const d = new Date(monday)  
398 - d.setDate(monday.getDate() + i)  
399 - dates.push(d)  
400 - }  
401 - return dates  
402 -} 370 +/** 周期对比数据 */
  371 +const listData = computed(() => {
  372 + if (!weeklyData.value) return [];
  373 +
  374 + const d = weeklyData.value;
  375 + const fields = [
  376 + { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },
  377 + { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },
  378 + { label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration },
  379 + { label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance },
  380 + { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount },
  381 + ];
  382 +
  383 + return fields.map(({ label, last, curr }) => {
  384 + const lastVal = last ?? 0;
  385 + const currVal = curr ?? 0;
  386 + return {
  387 + label,
  388 + lastPeriod: lastVal,
  389 + currPeriod: currVal,
  390 + diff: currVal - lastVal,
  391 + };
  392 + });
  393 +});
403 394
404 -// 2. 根据当前日期,生成月模式的完整日期列表(当月1日到月底)  
405 -const getMonthFullDates = (baseDateStr) => {  
406 - const date = new Date(baseDateStr)  
407 - const year = date.getFullYear()  
408 - const month = date.getMonth()  
409 - const daysInMonth = new Date(year, month + 1, 0).getDate()  
410 -  
411 - const dates = []  
412 - for (let i = 1; i <= daysInMonth; i++) {  
413 - dates.push(new Date(year, month, i)) 395 +/** 肌肉弹窗图表 X 轴 */
  396 +const muscleChartCategories = computed(() => {
  397 + if (MainCurrentDateType.value === 'week') {
  398 + return WEEKDAY_LABELS;
414 } 399 }
415 - return dates  
416 -} 400 + if (!muscleFullDates.value.length) return [];
  401 + return muscleFullDates.value.map((d) => dayjs(d).format('M/D'));
  402 +});
417 403
418 -// 3. 接口返回的 date 数组 [年,月,日] 转成 Date 对象  
419 -const parseApiDate = (apiDateArr) => {  
420 - // apiDateArr 格式: [2026, 6, 1]  
421 - if (!Array.isArray(apiDateArr) || apiDateArr.length !== 3) return null  
422 - const [year, month, day] = apiDateArr  
423 - return new Date(year, month - 1, day) // 注意: 月份是0-11  
424 -} 404 +const muscleVolumeArr = computed(() => muscleProcessedData.value.volumeArr || []);
  405 +const muscleSetsArr = computed(() => muscleProcessedData.value.setsArr || []);
  406 +
  407 +/** 肌肉弹窗图表 Series */
  408 +const muscleChartSeries = computed(() => {
  409 + const data = muscleTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value;
  410 + const name = muscleTabIndex.value === 0 ? '容量' : '组数';
  411 + const color = muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9';
425 412
426 -// 接口请求方法  
427 -// 根据动作分类获取指定日期所在周训练数据 413 + return [{ name, data: data || [], color }];
  414 +});
  415 +
  416 +// ==================== API 请求方法 ====================
  417 +
  418 +/** 加载周分类趋势数据 */
428 const loadWeeklyByCategory = async (date) => { 419 const loadWeeklyByCategory = async (date) => {
429 - if (weeklyLoading.value) return;  
430 - if (!date) return; 420 + if (weeklyLoading.value || !date) return;
431 weeklyLoading.value = true; 421 weeklyLoading.value = true;
432 try { 422 try {
433 - let categoryId = '0'  
434 - if (partId.value && partId.value !== '') {  
435 - categoryId = partId.value  
436 - }  
437 - const res = await TrainingDataApi.weeklyByCategory(date, categoryId, showLastPeriod.value ? 'true' : 'false');  
438 - console.log('===== 周数据接口完整返回 =====');  
439 - console.log(res.data);  
440 - console.log('本周weeklyDataMap:', res.data.weeklyDataMap);  
441 - console.log('上周lastWeekDataMap:', res.data.lastWeekDataMap);  
442 - console.log('上周总数据(lastWeekTotalXXX):', {  
443 - volume: res.data.lastWeekTotalVolume,  
444 - sets: res.data.lastWeekTotalSets,  
445 - duration: res.data.lastWeekTotalDuration,  
446 - distance: res.data.lastWeekTotalDistance,  
447 - count: res.data.lastWeekTotalCount  
448 - }); 423 + const categoryId = (partId.value && partId.value !== '') ? partId.value : '0';
  424 + const res = await TrainingDataApi.weeklyByCategory(
  425 + date,
  426 + categoryId,
  427 + showLastPeriod.value ? 'true' : 'false',
  428 + );
449 weeklyData.value = res.data; 429 weeklyData.value = res.data;
450 } catch (err) { 430 } catch (err) {
451 console.error('动作分类周数据加载失败', err); 431 console.error('动作分类周数据加载失败', err);
@@ -454,19 +434,17 @@ const loadWeeklyByCategory = async (date) => { @@ -454,19 +434,17 @@ const loadWeeklyByCategory = async (date) => {
454 } 434 }
455 }; 435 };
456 436
457 -// 根据动作分类获取指定日期所在月训练数据(可选择包含上月数据) 437 +/** 加载月分类趋势数据 */
458 const loadMonthlyByCategory = async (date) => { 438 const loadMonthlyByCategory = async (date) => {
459 - if (weeklyLoading.value) return;  
460 - if (!date) return; 439 + if (weeklyLoading.value || !date) return;
461 weeklyLoading.value = true; 440 weeklyLoading.value = true;
462 try { 441 try {
463 - let categoryId = '0'  
464 - if (partId.value && partId.value !== '') {  
465 - categoryId = partId.value  
466 - }  
467 - console.log('是否返回上个的数据:', showLastPeriod.value);  
468 - const res = await TrainingDataApi.monthlyByCategory(date, categoryId, showLastPeriod.value ? 'true' : 'false');  
469 - console.log('【动作分类月数据接口返回】', res.data); 442 + const categoryId = (partId.value && partId.value !== '') ? partId.value : '0';
  443 + const res = await TrainingDataApi.monthlyByCategory(
  444 + date,
  445 + categoryId,
  446 + showLastPeriod.value ? 'true' : 'false',
  447 + );
470 weeklyData.value = res.data; 448 weeklyData.value = res.data;
471 } catch (err) { 449 } catch (err) {
472 console.error('动作分类月数据加载失败', err); 450 console.error('动作分类月数据加载失败', err);
@@ -475,56 +453,41 @@ const loadMonthlyByCategory = async (date) => { @@ -475,56 +453,41 @@ const loadMonthlyByCategory = async (date) => {
475 } 453 }
476 }; 454 };
477 455
478 -// 加载部位/动作分类  
479 -const loadbodyPartOptions = async () => {  
480 - const res = await LeftmotionApi.getAllCategories();  
481 - partOptions.value = [  
482 - { id: '0', name: '全部' },  
483 - ...res.data.map(item => {  
484 - return {  
485 - id: String(item.id),  
486 - name: item.name  
487 - }  
488 - })  
489 - ]  
490 - console.log('部位列表:', partOptions.value);  
491 -};  
492 -const currentMuscleId = ref(null) // 当前点击的肌肉ID  
493 -// 加载肌肉数据  
494 -const subCategorieList = ref([])  
495 -// 训练数据(来自 loadTrainingData 接口)  
496 -const trainingData = ref({  
497 - setCountMap: {}, // 组数数据 { id: count }  
498 - totalVolumeMap: {} // 容量数据 { id: volume }  
499 -}) 456 +/** 加载肌肉部位分类列表 */
500 const loadGetSubCategorieList = async () => { 457 const loadGetSubCategorieList = async () => {
501 if (muscleListLoading.value) return; 458 if (muscleListLoading.value) return;
502 muscleListLoading.value = true; 459 muscleListLoading.value = true;
503 try { 460 try {
504 const res = await TrainingDataApi.GetSubCategorieList(); 461 const res = await TrainingDataApi.GetSubCategorieList();
505 - subCategorieList.value = res.data;  
506 - console.log('肌肉数据:', subCategorieList.value); 462 + subCategorieList.value = res.data || [];
507 } catch (err) { 463 } catch (err) {
508 console.error('加载失败:', err); 464 console.error('加载失败:', err);
509 } finally { 465 } finally {
510 muscleListLoading.value = false; 466 muscleListLoading.value = false;
511 } 467 }
512 }; 468 };
513 -// 加载肌肉表格数据表格 469 +
  470 +/** 加载部位选项列表 */
  471 +const loadbodyPartOptions = async () => {
  472 + const res = await LeftmotionApi.getAllCategories();
  473 + partOptions.value = [
  474 + { id: '0', name: '全部' },
  475 + ...res.data.map((item) => ({ id: String(item.id), name: item.name })),
  476 + ];
  477 +};
  478 +
  479 +/** 加载肌肉部位训练数据(容量/组数分布) */
514 const loadTrainingData = async (sDate, eDate) => { 480 const loadTrainingData = async (sDate, eDate) => {
515 if (trainingLoading.value) return; 481 if (trainingLoading.value) return;
516 trainingLoading.value = true; 482 trainingLoading.value = true;
517 - const s = String(sDate)  
518 - const e = String(eDate)  
519 - if (!s || !e || s === 'undefined' || e === 'undefined') return 483 + const s = String(sDate);
  484 + const e = String(eDate);
  485 + if (!s || !e || s === 'undefined' || e === 'undefined') return;
520 try { 486 try {
521 const res = await TrainingDataApi.mainDataAnalysis(s, e); 487 const res = await TrainingDataApi.mainDataAnalysis(s, e);
522 - console.log('训练数据:', res.data);  
523 if (res.data) { 488 if (res.data) {
524 - trainingData.value.setCountMap = res.data.setCountMap || {}  
525 - trainingData.value.totalVolumeMap = res.data.totalVolumeMap || {}  
526 - console.log('trainingData.value', trainingData.value);  
527 - 489 + trainingData.value.setCountMap = res.data.setCountMap || {};
  490 + trainingData.value.totalVolumeMap = res.data.totalVolumeMap || {};
528 } 491 }
529 } catch (err) { 492 } catch (err) {
530 console.error('加载失败:', err); 493 console.error('加载失败:', err);
@@ -532,395 +495,343 @@ const loadTrainingData = async (sDate, eDate) => { @@ -532,395 +495,343 @@ const loadTrainingData = async (sDate, eDate) => {
532 trainingLoading.value = false; 495 trainingLoading.value = false;
533 } 496 }
534 }; 497 };
535 -// 打开肌肉弹窗需要的数据  
536 -const openMuscleDetail = async (muscleId) => {  
537 - console.log('点击了肌肉ID:', muscleId)  
538 - currentMuscleId.value = muscleId  
539 - muscleShow.value = true  
540 - // 默认选中容量  
541 - muscleTabIndex.value = 0  
542 -  
543 - // 从 subCategorieList 里找到肌肉名称  
544 - const muscle = subCategorieList.value.find(item => item.id === muscleId)  
545 - currentMuscleName.value = muscle?.name || ''  
546 498
547 - // 根据当前是周/月模式,加载对应接口  
548 - if (MainCurrentDateType.value === 'week') {  
549 - await loadWeeklyMuscleStats(muscleId, startDate.value)  
550 - } else {  
551 - await loadMonthlyMuscleStats(muscleId, startDate.value)  
552 - }  
553 -} 499 +/** 加载单肌肉统计数据(周/月通用) */
  500 +const loadMuscleStats = async (muscleId, date, isWeekly) => {
  501 + try {
  502 + const res = isWeekly
  503 + ? await TrainingDataApi.weeklyMuscleStats(muscleId, date)
  504 + : await TrainingDataApi.monthlyMuscleStats(muscleId, date);
554 505
555 -const muscleDetailData = ref({  
556 - muscleId: 0,  
557 - dailyStatsList: []  
558 -}) 506 + muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] };
559 507
560 -// 加载 单肌肉-周统计  
561 -const loadWeeklyMuscleStats = async (muscleId, date) => {  
562 - try {  
563 - const res = await TrainingDataApi.weeklyMuscleStats(muscleId, date)  
564 - console.log('单肌肉周数据:', res.data)  
565 - muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] }  
566 -  
567 - // --- 核心:数据对齐处理 ---  
568 - // 1. 生成本周完整日期列表  
569 - const fullDates = getWeekFullDates(date)  
570 - muscleFullDates.value = fullDates  
571 -  
572 - // 2. 把接口数据按日期存成映射表  
573 - const dateMap = {}  
574 - muscleDetailData.value.dailyStatsList.forEach(item => {  
575 - const d = parseApiDate(item.date)  
576 - if (d) {  
577 - // 转成 YYYY-MM-DD 字符串当key  
578 - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`  
579 - dateMap[key] = {  
580 - volume: item.volume,  
581 - sets: item.sets  
582 - }  
583 - }  
584 - })  
585 -  
586 - // 3. 按完整日期列表填充数据,没有数据填0  
587 - const volumeArr = []  
588 - const setsArr = []  
589 - fullDates.forEach(d => {  
590 - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`  
591 - const data = dateMap[key] || { volume: 0, sets: 0 }  
592 - volumeArr.push(data.volume)  
593 - setsArr.push(data.sets)  
594 - })  
595 -  
596 - muscleProcessedData.value = { volumeArr, setsArr }  
597 - } catch (err) {  
598 - console.error('单肌肉周数据失败', err)  
599 - }  
600 -} 508 + const fullDates = isWeekly ? getWeekFullDates(date) : getMonthFullDates(date);
  509 + muscleFullDates.value = fullDates;
601 510
602 -// 加载 单肌肉-月统计  
603 -const loadMonthlyMuscleStats = async (muscleId, date) => {  
604 - try {  
605 - const res = await TrainingDataApi.monthlyMuscleStats(muscleId, date)  
606 - console.log('单肌肉月数据:', res.data)  
607 - muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] }  
608 -  
609 - // --- 核心:数据对齐处理 ---  
610 - // 1. 生成本月完整日期列表  
611 - const fullDates = getMonthFullDates(date)  
612 - muscleFullDates.value = fullDates  
613 -  
614 - // 2. 把接口数据按日期存成映射表  
615 - const dateMap = {}  
616 - muscleDetailData.value.dailyStatsList.forEach(item => {  
617 - const d = parseApiDate(item.date) 511 + // 将 API 返回的 dailyStatsList 按日期建立索引
  512 + const dateMap = {};
  513 + muscleDetailData.value.dailyStatsList.forEach((item) => {
  514 + const d = parseApiDate(item.date);
618 if (d) { 515 if (d) {
619 - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`  
620 - dateMap[key] = {  
621 - volume: item.volume,  
622 - sets: item.sets  
623 - } 516 + dateMap[formatDateKey(d)] = { volume: item.volume, sets: item.sets };
624 } 517 }
625 - })  
626 -  
627 - // 3. 按完整日期列表填充数据,没有数据填0  
628 - const volumeArr = []  
629 - const setsArr = []  
630 - fullDates.forEach(d => {  
631 - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`  
632 - const data = dateMap[key] || { volume: 0, sets: 0 }  
633 - volumeArr.push(data.volume)  
634 - setsArr.push(data.sets)  
635 - })  
636 -  
637 - muscleProcessedData.value = { volumeArr, setsArr }  
638 - } catch (err) {  
639 - console.error('单肌肉月数据失败', err)  
640 - }  
641 -}  
642 -  
643 -const closeMuscleDetail = () => {  
644 -  
645 - muscleShow.value = false  
646 - // currentMuscleId.value = null  
647 - // currentMuscleName.value = ''  
648 - // muscleTabIndex.value = 0  
649 - // muscleDetailData.value = { muscleId: 0, dailyStatsList: [] }  
650 - // muscleFullDates.value = []  
651 - // muscleProcessedData.value = { volumeArr: [], setsArr: [] }  
652 - setTimeout(() => {  
653 - currentMuscleId.value = null;  
654 - currentMuscleName.value = '';  
655 - muscleDetailData.value = { muscleId: 0, dailyStatsList: [] };  
656 - muscleFullDates.value = [];  
657 - muscleProcessedData.value = { volumeArr: [], setsArr: [] };  
658 - }, 150);  
659 -}  
660 -  
661 -// 处理好的容量数组(已对齐完整日期)  
662 -const muscleVolumeArr = computed(() => {  
663 - return muscleProcessedData.value.volumeArr || []  
664 -})  
665 -  
666 -// 处理好的组数数组(已对齐完整日期)  
667 -const muscleSetsArr = computed(() => {  
668 - return muscleProcessedData.value.setsArr || []  
669 -}) 518 + });
670 519
671 -// 弹窗图表的类目(周:周一到周日 / 月:具体日期)  
672 -const muscleChartCategories = computed(() => {  
673 - if (MainCurrentDateType.value === 'week') {  
674 - return ['周一', '周二', '周三', '周四', '周五', '周六', '周日']  
675 - } else {  
676 - // 月模式:如果日期列表为空,返回空数组避免报错  
677 - if (!muscleFullDates.value.length) return []  
678 - return muscleFullDates.value.map(d => `${d.getMonth() + 1}/${d.getDate()}`)  
679 - }  
680 -}) 520 + // 按完整日期列表填充数据,缺失的补 0
  521 + const volumeArr = [];
  522 + const setsArr = [];
  523 + fullDates.forEach((d) => {
  524 + const data = dateMap[formatDateKey(d)] || { volume: 0, sets: 0 };
  525 + volumeArr.push(data.volume);
  526 + setsArr.push(data.sets);
  527 + });
681 528
682 -// 弹窗图表数据:根据 muscleTabIndex 只显示容量或组数  
683 -const muscleChartSeries = computed(() => {  
684 - const data = muscleTabIndex.value === 0  
685 - ? muscleVolumeArr.value  
686 - : muscleSetsArr.value  
687 -  
688 - // 兜底:如果数据为空,返回空数组  
689 - if (!data || !data.length) {  
690 - return [{  
691 - name: muscleTabIndex.value === 0 ? '容量' : '组数',  
692 - data: [],  
693 - color: muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9'  
694 - }] 529 + muscleProcessedData.value = { volumeArr, setsArr };
  530 + } catch (err) {
  531 + console.error('单肌肉数据加载失败', err);
695 } 532 }
  533 +};
696 534
697 - return [{  
698 - name: muscleTabIndex.value === 0 ? '容量' : '组数',  
699 - data: data,  
700 - color: muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9'  
701 - }]  
702 -})  
703 - 535 +// ==================== 页面事件 ====================
704 536
705 -// 事件方法 537 +/** 筛选部位切换 */
706 const onPartChange = (item) => { 538 const onPartChange = (item) => {
707 - console.log('选中的项:', item);  
708 partId.value = item.id; 539 partId.value = item.id;
709 - if (MainCurrentDateType.value === 'week') {  
710 - loadWeeklyByCategory(startDate.value);  
711 - } else {  
712 - loadMonthlyByCategory(startDate.value);  
713 - } 540 + loadCategoryData(startDate.value);
714 }; 541 };
715 542
716 -// 当前的日期类型周/月 543 +/** 时间类型切换(周/月) */
717 const handleTypeChange = async (type) => { 544 const handleTypeChange = async (type) => {
718 MainCurrentDateType.value = type; 545 MainCurrentDateType.value = type;
719 showLastPeriod.value = false; 546 showLastPeriod.value = false;
720 - loadTrainingData(startDate.value, endDate.value) 547 + loadTrainingData(startDate.value, endDate.value);
721 548
722 if (startDate.value) { 549 if (startDate.value) {
723 - if (type === 'week') {  
724 - await loadWeeklyByCategory(startDate.value)  
725 - } else {  
726 - await loadMonthlyByCategory(startDate.value)  
727 - } 550 + await loadCategoryData(startDate.value);
728 } 551 }
729 552
730 - // 如果弹窗打开,自动刷新肌肉数据 553 + // 如果肌肉弹窗打开,同步刷新
731 if (muscleShow.value && currentMuscleId.value) { 554 if (muscleShow.value && currentMuscleId.value) {
732 - if (type === 'week') {  
733 - await loadWeeklyMuscleStats(currentMuscleId.value, startDate.value)  
734 - } else {  
735 - await loadMonthlyMuscleStats(currentMuscleId.value, startDate.value)  
736 - } 555 + await loadMuscleData(currentMuscleId.value, startDate.value);
737 } 556 }
738 }; 557 };
739 -// 处理日期改变 558 +
  559 +/** 日期范围变更 */
740 const handleDateChange = async (data) => { 560 const handleDateChange = async (data) => {
741 - console.log(data.startDate, data.endDate);  
742 startDate.value = data.startDate; 561 startDate.value = data.startDate;
743 endDate.value = data.endDate; 562 endDate.value = data.endDate;
744 - await loadTrainingData(startDate.value, endDate.value)  
745 - if (MainCurrentDateType.value === 'week') {  
746 - loadWeeklyByCategory(startDate.value);  
747 - } else {  
748 - loadMonthlyByCategory(startDate.value);  
749 - }  
750 - // 弹窗打开 → 刷新肌肉详情 563 + await loadTrainingData(startDate.value, endDate.value);
  564 + loadCategoryData(startDate.value);
  565 +
751 if (muscleShow.value && currentMuscleId.value) { 566 if (muscleShow.value && currentMuscleId.value) {
752 - if (MainCurrentDateType.value === 'week') {  
753 - await loadWeeklyMuscleStats(currentMuscleId.value, startDate.value)  
754 - } else {  
755 - await loadMonthlyMuscleStats(currentMuscleId.value, startDate.value)  
756 - } 567 + await loadMuscleData(currentMuscleId.value, startDate.value);
757 } 568 }
758 }; 569 };
759 -// 处理是否返回上周/月 570 +
  571 +/** 打开肌肉详情弹窗 */
  572 +const openMuscleDetail = async (muscleId) => {
  573 + currentMuscleId.value = muscleId;
  574 + muscleShow.value = true;
  575 + muscleTabIndex.value = 0;
  576 +
  577 + const muscle = subCategorieList.value.find((item) => item.id === muscleId);
  578 + currentMuscleName.value = muscle?.name || '';
  579 +
  580 + await loadMuscleData(muscleId, startDate.value);
  581 +};
  582 +
  583 +/** 关闭肌肉详情弹窗 */
  584 +const closeMuscleDetail = () => {
  585 + muscleShow.value = false;
  586 + setTimeout(() => {
  587 + currentMuscleId.value = null;
  588 + currentMuscleName.value = '';
  589 + muscleDetailData.value = { muscleId: 0, dailyStatsList: [] };
  590 + muscleFullDates.value = [];
  591 + muscleProcessedData.value = { volumeArr: [], setsArr: [] };
  592 + }, 150);
  593 +};
  594 +
  595 +/** 切换显示上一周期数据 */
760 const toggleLastWeek = () => { 596 const toggleLastWeek = () => {
761 - showLastPeriod.value = !showLastPeriod.value 597 + showLastPeriod.value = !showLastPeriod.value;
762 if (startDate.value) { 598 if (startDate.value) {
763 - if (MainCurrentDateType.value === 'week') {  
764 - loadWeeklyByCategory(startDate.value)  
765 - } else {  
766 - loadMonthlyByCategory(startDate.value)  
767 - } 599 + loadCategoryData(startDate.value);
768 } 600 }
769 -} 601 +};
  602 +
  603 +// ==================== 生命周期 ====================
770 604
771 onMounted(async () => { 605 onMounted(async () => {
772 await loadbodyPartOptions(); 606 await loadbodyPartOptions();
773 await loadGetSubCategorieList(); 607 await loadGetSubCategorieList();
774 if (startDate.value && endDate.value) { 608 if (startDate.value && endDate.value) {
775 - await loadTrainingData(startDate.value, endDate.value)  
776 - if (MainCurrentDateType.value === 'week') {  
777 - await loadWeeklyByCategory(startDate.value)  
778 - } else {  
779 - await loadMonthlyByCategory(startDate.value)  
780 - } 609 + await loadTrainingData(startDate.value, endDate.value);
  610 + await loadCategoryData(startDate.value);
781 } 611 }
782 -  
783 }); 612 });
784 </script> 613 </script>
785 614
786 <style lang="scss" scoped> 615 <style lang="scss" scoped>
787 -.training-data-page {  
788 - min-height: 100vh; 616 +// ==================== CSS 变量 ====================
  617 +$color-primary: #202932;
  618 +$color-bg: #f8fafc;
  619 +$color-card: #ffffff;
  620 +$color-border: #f1f5f9;
  621 +$color-text: #0f172a;
  622 +$color-sub: #475569;
  623 +$color-muted: #94a3b8;
  624 +$color-green: #10b981;
  625 +$color-green-bg: #ecfdf5;
  626 +$color-green-border: #a7f3d0;
  627 +$color-blue: #5b8ff9;
  628 +$color-yellow: #ffc53d;
  629 +$color-red: #ff7875;
  630 +$radius-card: 24rpx;
  631 +$radius-tab: 50rpx;
  632 +$radius-item: 12rpx;
  633 +
  634 +// ==================== 公共 Tab 样式 ====================
  635 +.tab-group {
789 display: flex; 636 display: flex;
790 - flex-direction: column;  
791 - padding: 30rpx 24rpx 0;  
792 - box-sizing: border-box;  
793 - overflow: auto; 637 + gap: 16rpx;
  638 + margin-bottom: 12rpx;
794 639
795 - .content-scroll {  
796 - height: calc(100vh - 400rpx);  
797 - // #ifdef H5  
798 - height: calc(100vh - 185px);  
799 - // #endif  
800 - width: 100%;  
801 - box-sizing: border-box;  
802 - overflow: auto; 640 + &.flex-wrap {
  641 + flex-wrap: wrap;
803 } 642 }
  643 +}
804 644
805 - .section {  
806 - padding: 30rpx 20rpx;  
807 - box-sizing: border-box;  
808 - background-color: #fff;  
809 - border-radius: 15rpx;  
810 - margin-top: 20rpx; 645 +.tab-item {
  646 + border: 1rpx solid #e2e8f0;
  647 + border-radius: $radius-tab;
  648 + background-color: $color-card;
  649 + padding: 10rpx 24rpx;
  650 + font-size: 24rpx;
  651 + color: $color-sub;
  652 + transition: all 0.2s ease;
  653 + cursor: pointer;
811 654
812 - &:last-child {  
813 - padding-bottom: env(safe-area-inset-bottom);  
814 - margin-bottom: 110rpx; 655 + &.active {
  656 + background-color: $color-primary;
  657 + color: $color-card;
  658 + border-color: $color-primary;
  659 + font-weight: 600;
815 } 660 }
816 661
817 - .title {  
818 - font-size: 30rpx;  
819 - font-weight: bold;  
820 - margin-bottom: 20rpx; 662 + &:active {
  663 + opacity: 0.8;
821 } 664 }
  665 +}
822 666
823 - .tip { 667 +.tip-text {
824 font-size: 22rpx; 668 font-size: 22rpx;
  669 + color: $color-muted;
825 margin-bottom: 20rpx; 670 margin-bottom: 20rpx;
  671 + display: block;
  672 +}
  673 +
  674 +// ==================== 页面主容器 ====================
  675 +.training-data-page {
  676 + display: flex;
  677 + flex-direction: column;
  678 +
  679 + width: 100vw;
  680 + background-color: $color-bg;
  681 + overflow: hidden;
  682 +
  683 + // 顶部固定时间选择栏
  684 + .top-time-picker {
  685 + padding: 20rpx 24rpx;
  686 + box-sizing: border-box;
  687 + background-color: $color-card;
  688 + border-bottom: 1rpx solid $color-border;
  689 + z-index: 10;
826 } 690 }
827 691
828 - .img { 692 + // 主体可滚动区域
  693 + .main-content {
  694 + flex: 1;
  695 + height: 0;
  696 + min-height: 0;
  697 + overflow: hidden;
  698 +
  699 + .scroll-area {
829 width: 100%; 700 width: 100%;
830 - height: 400rpx;  
831 - } 701 + height: 100%;
832 702
833 - .tabs { 703 + .scroll-inner {
  704 + padding: 24rpx;
834 display: flex; 705 display: flex;
835 - gap: 20rpx; 706 + flex-direction: column;
  707 + gap: 24rpx;
  708 + }
  709 + }
  710 + }
836 711
837 - .tab {  
838 - border: 1rpx solid #eee;  
839 - border-radius: 50rpx;  
840 - background-color: #fff;  
841 - padding: 10rpx 20rpx;  
842 - font-size: 24rpx; 712 + // 卡片通用容器
  713 + .card-section {
  714 + background-color: $color-card;
  715 + border-radius: $radius-card;
  716 + padding: 28rpx 24rpx;
  717 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
  718 + border: 1rpx solid $color-border;
  719 +
  720 + .section-header {
  721 + display: flex;
  722 + align-items: center;
843 margin-bottom: 20rpx; 723 margin-bottom: 20rpx;
844 724
845 - &.active {  
846 - background-color: #202932;  
847 - color: #fff;  
848 - border: none; 725 + &.space-between {
  726 + justify-content: space-between;
  727 + }
  728 +
  729 + .title {
  730 + font-size: 30rpx;
  731 + font-weight: 700;
  732 + color: $color-text;
849 } 733 }
  734 +
  735 + .part-select-wrapper {
  736 + min-width: 140rpx;
850 } 737 }
851 } 738 }
852 739
853 - /* ========== 核心网格布局 已修复 ========== */  
854 - .detail-part { 740 + // 肌肉部位网格
  741 + .muscle-grid {
855 display: grid; 742 display: grid;
856 grid-template-columns: repeat(4, 1fr); 743 grid-template-columns: repeat(4, 1fr);
857 - gap: 5rpx; 744 + gap: 10rpx;
858 745
859 - .part-item { 746 + .grid-item {
860 width: 100%; 747 width: 100%;
861 height: 140rpx; 748 height: 140rpx;
862 - // aspect-ratio: 1 / 1;  
863 - /* 强制正方形 */  
864 - background-color: #eee; 749 + background-color: $color-bg;
  750 + border: 1rpx solid $color-border;
  751 + border-radius: $radius-item;
865 display: flex; 752 display: flex;
  753 + flex-direction: column;
866 align-items: center; 754 align-items: center;
867 justify-content: center; 755 justify-content: center;
868 - flex-direction: column;  
869 - font-size: 24rpx;  
870 gap: 10rpx; 756 gap: 10rpx;
871 - border-radius: 5rpx; 757 + transition: transform 0.15s, background-color 0.15s;
872 758
873 - .num {  
874 - font-weight: bold;  
875 - color: #000; 759 + &:active {
  760 + transform: scale(0.96);
  761 + background-color: #f1f5f9;
876 } 762 }
  763 +
  764 + .item-name {
  765 + font-size: 24rpx;
  766 + color: $color-sub;
877 } 767 }
878 768
879 - /* 总计自适应占格 */  
880 - .total-item {  
881 - grid-column: span var(--remain-span);  
882 - height: 140rpx; 769 + .item-value {
  770 + font-size: 26rpx;
  771 + font-weight: 700;
  772 + color: #000000;
  773 +
  774 + &.highlight {
  775 + color: $color-green;
883 } 776 }
884 } 777 }
885 778
886 - .header {  
887 - display: flex;  
888 - justify-content: space-between;  
889 - align-items: center; 779 + &.total-item {
  780 + background-color: $color-green-bg;
  781 + border-color: $color-green-border;
  782 +
  783 + .item-name {
  784 + color: #047857;
  785 + font-weight: 600;
  786 + }
  787 + }
  788 + }
890 } 789 }
891 790
892 - .statistical { 791 + // 图表容器
  792 + .chart-container {
893 width: 100%; 793 width: 100%;
894 height: 400rpx; 794 height: 400rpx;
895 } 795 }
896 796
897 - .chose { 797 + // 显示上期数据勾选
  798 + .toggle-checkbox {
  799 + display: flex;
  800 + align-items: center;
  801 + margin-top: 20rpx;
898 font-size: 24rpx; 802 font-size: 24rpx;
  803 + color: #333333;
  804 +
  805 + .toggle-label {
  806 + margin-left: 8rpx;
  807 + }
899 } 808 }
900 809
901 - .compare { 810 + // 周期对比区域
  811 + .compare-section {
902 margin-top: 30rpx; 812 margin-top: 30rpx;
903 813
904 - .header-row { 814 + .compare-header {
905 display: flex; 815 display: flex;
906 justify-content: space-between; 816 justify-content: space-between;
907 align-items: center; 817 align-items: center;
  818 + margin-bottom: 20rpx;
908 819
909 - .title { 820 + .sub-title {
910 font-size: 36rpx; 821 font-size: 36rpx;
911 - font-weight: bold;  
912 - color: #333; 822 + font-weight: 700;
  823 + color: #333333;
913 } 824 }
914 825
915 .legend-group { 826 .legend-group {
916 display: flex; 827 display: flex;
917 - gap: 100rpx; 828 + gap: 32rpx;
918 829
919 .legend-item { 830 .legend-item {
920 display: flex; 831 display: flex;
921 align-items: center; 832 align-items: center;
922 font-size: 24rpx; 833 font-size: 24rpx;
923 - color: #666; 834 + color: #666666;
924 835
925 .dot { 836 .dot {
926 width: 20rpx; 837 width: 20rpx;
@@ -928,22 +839,18 @@ onMounted(async () => { @@ -928,22 +839,18 @@ onMounted(async () => {
928 border-radius: 4rpx; 839 border-radius: 4rpx;
929 margin-right: 8rpx; 840 margin-right: 8rpx;
930 841
931 - &.blue {  
932 - background-color: #5b8ff9;  
933 - }  
934 -  
935 - &.yellow {  
936 - background-color: #ffc53d;  
937 - } 842 + &.blue { background-color: $color-blue; }
  843 + &.yellow { background-color: $color-yellow; }
938 } 844 }
939 } 845 }
940 } 846 }
941 } 847 }
942 848
943 .compare-card { 849 .compare-card {
944 - background-color: #ffffff; 850 + background-color: $color-card;
945 border-radius: 20rpx; 851 border-radius: 20rpx;
946 border: 5rpx solid #efefef; 852 border: 5rpx solid #efefef;
  853 + overflow: hidden;
947 854
948 .data-list { 855 .data-list {
949 .data-row { 856 .data-row {
@@ -953,31 +860,25 @@ onMounted(async () => { @@ -953,31 +860,25 @@ onMounted(async () => {
953 border-bottom: 1rpx solid #f0f0f0; 860 border-bottom: 1rpx solid #f0f0f0;
954 font-size: 26rpx; 861 font-size: 26rpx;
955 862
956 - &:last-child {  
957 - border-bottom: none;  
958 - }  
959 -  
960 - &:nth-child(odd) {  
961 - background-color: #f9fafc;  
962 - } 863 + &:last-child { border-bottom: none; }
  864 + &:nth-child(odd) { background-color: #f9fafc; }
963 865
964 .label { 866 .label {
965 flex: 1.5; 867 flex: 1.5;
966 - color: #333; 868 + color: #333333;
967 text-align: center; 869 text-align: center;
968 } 870 }
969 871
970 .value { 872 .value {
971 flex: 1; 873 flex: 1;
972 text-align: center; 874 text-align: center;
973 - color: #333; 875 + color: #333333;
974 876
975 &.diff { 877 &.diff {
976 - color: #666;  
977 - } 878 + font-weight: 600;
978 879
979 - &.negative {  
980 - color: #ff7875; 880 + &.positive { color: $color-green; }
  881 + &.negative { color: $color-red; }
981 } 882 }
982 } 883 }
983 } 884 }
@@ -986,7 +887,16 @@ onMounted(async () => { @@ -986,7 +887,16 @@ onMounted(async () => {
986 } 887 }
987 } 888 }
988 889
989 - .muscleShow-popup { 890 + // 底部安全间距
  891 + .bottom-spacer {
  892 + height: env(safe-area-inset-bottom);
  893 + padding-bottom: 110rpx;
  894 + }
  895 +}
  896 +
  897 +// ==================== 肌肉弹窗样式 ====================
  898 +.muscleShow-popup {
  899 + padding-bottom: 30rpx;
990 900
991 /* #ifdef MP-WEIXIN */ 901 /* #ifdef MP-WEIXIN */
992 min-height: 40vh; 902 min-height: 40vh;
@@ -996,6 +906,7 @@ onMounted(async () => { @@ -996,6 +906,7 @@ onMounted(async () => {
996 min-height: 55vh; 906 min-height: 55vh;
997 /* #endif */ 907 /* #endif */
998 908
  909 + .popup-body {
999 .muscle-name { 910 .muscle-name {
1000 font-size: 32rpx; 911 font-size: 32rpx;
1001 font-weight: bold; 912 font-weight: bold;
@@ -1006,25 +917,12 @@ onMounted(async () => { @@ -1006,25 +917,12 @@ onMounted(async () => {
1006 display: flex; 917 display: flex;
1007 gap: 20rpx; 918 gap: 20rpx;
1008 padding: 20rpx; 919 padding: 20rpx;
1009 -  
1010 - .tab {  
1011 - border: 1rpx solid #eee;  
1012 - border-radius: 50rpx;  
1013 - background-color: #fff;  
1014 - padding: 10rpx 20rpx;  
1015 - font-size: 24rpx;  
1016 -  
1017 - &.active {  
1018 - background-color: #202932;  
1019 - color: #fff;  
1020 - border: none;  
1021 - }  
1022 - }  
1023 } 920 }
1024 921
1025 .tip { 922 .tip {
1026 font-size: 22rpx; 923 font-size: 22rpx;
1027 padding: 0 20rpx 20rpx; 924 padding: 0 20rpx 20rpx;
  925 + color: $color-muted;
1028 } 926 }
1029 927
1030 .statistical { 928 .statistical {
1 <template> 1 <template>
2 <view class="plan-page"> 2 <view class="plan-page">
3 - <!-- 顶部横幅 -->  
4 - <view class="banner-wrapper">  
5 - <swiper class="swiper">  
6 - <swiper-item class="swiper-item">  
7 - <image class="img"  
8 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png"  
9 - mode="aspectFill" />  
10 - </swiper-item>  
11 - <swiper-item class="swiper-item">  
12 - <image class="img"  
13 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/38_1773628032712.png"  
14 - mode="aspectFill" />  
15 - </swiper-item>  
16 - </swiper>  
17 - </view>  
18 3
19 - <!-- 筛选条 -->  
20 - <!-- 后端计划分类的计划列表只返回了难度字段,所有只有这个字段的筛选生效了 -->  
21 <view class="filter-bar"> 4 <view class="filter-bar">
22 - <view v-for="(item, index) in filterList" :key="index" class="filter-item"  
23 - :class="{ active: activeFilter === index }" @click="  
24 - activeFilter = index;  
25 - toggleDrawer(index);  
26 - "> 5 + <view v-for="(item, index) in filterList" :key="item" class="filter-item"
  6 + :class="{ active: activeFilter === index }" @click="toggleDrawer(index)">
27 <text class="text">{{ 7 <text class="text">{{
28 selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}` 8 selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}`
29 }}</text> 9 }}</text>
@@ -33,20 +13,17 @@ @@ -33,20 +13,17 @@
33 <uni-icons type="search" size="20" color="#333" 13 <uni-icons type="search" size="20" color="#333"
34 style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx" @click="navigateToSearch" /> 14 style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx" @click="navigateToSearch" />
35 </view> 15 </view>
36 - </view>  
37 16
38 - <!-- 下拉抽屉组件 -->  
39 <view class="filter-drawer" v-show="drawerShow" @click.stop> 17 <view class="filter-drawer" v-show="drawerShow" @click.stop>
40 <view class="drawer-content"> 18 <view class="drawer-content">
41 - <view v-for="(item, idx) in filterDrawerData" :key="idx" class="drawer-item" @click="selectDrawerItem(item)"> 19 + <view v-for="item in filterDrawerData" :key="item" class="drawer-item" @click="selectDrawerItem(item)">
42 <text class="drawer-item-text">{{ item }}</text> 20 <text class="drawer-item-text">{{ item }}</text>
43 </view> 21 </view>
44 </view> 22 </view>
45 </view> 23 </view>
  24 + </view>
46 25
47 - <!-- 主体内容:左侧分类 + 右侧计划列表 -->  
48 <view class="content-layout"> 26 <view class="content-layout">
49 - <!-- 左侧分类 -->  
50 <scroll-view class="sidebar" scroll-y enable-flex> 27 <scroll-view class="sidebar" scroll-y enable-flex>
51 <view v-for="(item, index) in categoryList" :key="item.id" class="category-item" 28 <view v-for="(item, index) in categoryList" :key="item.id" class="category-item"
52 :class="{ active: activeCategory === index }" @click="switchCategory(index)"> 29 :class="{ active: activeCategory === index }" @click="switchCategory(index)">
@@ -54,7 +31,6 @@ @@ -54,7 +31,6 @@
54 </view> 31 </view>
55 </scroll-view> 32 </scroll-view>
56 33
57 - <!-- 右侧计划列表 -->  
58 <scroll-view class="plan-list-wrap" scroll-y enable-flex> 34 <scroll-view class="plan-list-wrap" scroll-y enable-flex>
59 <view v-for="plan in currentPlanList" :key="plan.id" class="plan-card" @tap="navigateToDetail(plan)"> 35 <view v-for="plan in currentPlanList" :key="plan.id" class="plan-card" @tap="navigateToDetail(plan)">
60 <image class="plan-cover" :src="plan.cover" mode="aspectFill" /> 36 <image class="plan-cover" :src="plan.cover" mode="aspectFill" />
@@ -70,7 +46,6 @@ @@ -70,7 +46,6 @@
70 </view> 46 </view>
71 </view> 47 </view>
72 </view> 48 </view>
73 - <!-- 空状态 -->  
74 <view v-if="currentPlanList.length === 0" class="empty-tip"> 49 <view v-if="currentPlanList.length === 0" class="empty-tip">
75 <text>该分类暂无训练计划</text> 50 <text>该分类暂无训练计划</text>
76 </view> 51 </view>
@@ -81,11 +56,39 @@ @@ -81,11 +56,39 @@
81 56
82 <script setup> 57 <script setup>
83 import { computed, onMounted, ref } from 'vue'; 58 import { computed, onMounted, ref } from 'vue';
84 -// ✅ getCurrentPages 是全局 API,无需 import  
85 import QueryPlanApi from '@/sheep/api/plan/queryplan'; 59 import QueryPlanApi from '@/sheep/api/plan/queryplan';
  60 +import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea';
  61 +const menuButtonHeight = ref(getMenuButtonHeight());
  62 +const topSafeArea = ref(getTopSafeArea());
  63 +const PageHeight = ref(topSafeArea.value);
  64 +// #ifdef MP-WEIXIN
  65 +PageHeight.value = menuButtonHeight.value + topSafeArea.value;
  66 +// #endif
  67 +const filterList = ['频率', '难度', '场景', '人群'];
  68 +const filterOptionsMap = {
  69 + 频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'],
  70 + 难度: ['不限', '初阶', '中阶', '高阶'],
  71 + 场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'],
  72 + 人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'],
  73 +};
  74 +const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };
  75 +const filterFieldMap = { 频率: 'frequency', 难度: 'difficultyLevel', 场景: 'scene', 人群: 'population' };
  76 +const filterMap = {
  77 + 频率: {
  78 + 不限: 0,
  79 + '1练/周': 1,
  80 + '2练/周': 2,
  81 + '3练/周': 3,
  82 + '4练/周': 4,
  83 + '5练/周': 5,
  84 + '6练/周': 6,
  85 + '7练/周': 7,
  86 + },
  87 + 难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 },
  88 + 场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 },
  89 + 人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 },
  90 +};
86 91
87 -// ====== 筛选相关(保持原样)======  
88 -const filterList = ref(['频率', '难度', '场景', '人群']);  
89 const drawerShow = ref(false); 92 const drawerShow = ref(false);
90 const activeFilter = ref(null); 93 const activeFilter = ref(null);
91 const selectedFilters = ref({ 94 const selectedFilters = ref({
@@ -94,15 +97,9 @@ const selectedFilters = ref({ @@ -94,15 +97,9 @@ const selectedFilters = ref({
94 场景: '不限', 97 场景: '不限',
95 人群: '不限', 98 人群: '不限',
96 }); 99 });
97 -const filterOptionsMap = {  
98 - 频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'],  
99 - 难度: ['不限', '初阶', '中阶', '高阶'],  
100 - 场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'],  
101 - 人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'],  
102 -};  
103 100
104 const filterDrawerData = computed(() => { 101 const filterDrawerData = computed(() => {
105 - const currentFilterName = filterList.value[activeFilter.value]; 102 + const currentFilterName = filterList[activeFilter.value];
106 return filterOptionsMap[currentFilterName] || []; 103 return filterOptionsMap[currentFilterName] || [];
107 }); 104 });
108 105
@@ -116,72 +113,35 @@ const toggleDrawer = (index) => { @@ -116,72 +113,35 @@ const toggleDrawer = (index) => {
116 }; 113 };
117 114
118 const selectDrawerItem = (item) => { 115 const selectDrawerItem = (item) => {
119 - const currentFilterName = filterList.value[activeFilter.value]; 116 + const currentFilterName = filterList[activeFilter.value];
120 selectedFilters.value[currentFilterName] = item; 117 selectedFilters.value[currentFilterName] = item;
121 drawerShow.value = false; 118 drawerShow.value = false;
122 console.log('筛选条件更新:', selectedFilters.value); 119 console.log('筛选条件更新:', selectedFilters.value);
123 }; 120 };
124 121
125 -// ====== 分类与计划 ======  
126 const categoryList = ref([]); 122 const categoryList = ref([]);
127 const activeCategory = ref(0); 123 const activeCategory = ref(0);
128 -const planData = ref([]); // 存储所有已加载的计划(带 _categoryId)  
129 -const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };  
130 -// 完全匹配你后台的数字,复制这一段!  
131 -const filterMap = {  
132 - 频率: {  
133 - 不限: 0,  
134 - '1练/周': 1,  
135 - '2练/周': 2,  
136 - '3练/周': 3,  
137 - '4练/周': 4,  
138 - '5练/周': 5,  
139 - '6练/周': 6,  
140 - '7练/周': 7,  
141 - },  
142 - 难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 },  
143 - 场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 },  
144 - 人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 },  
145 -}; 124 +const planData = ref([]);
146 const currentPlanList = computed(() => { 125 const currentPlanList = computed(() => {
147 const categoryId = categoryList.value[activeCategory.value]?.id; 126 const categoryId = categoryList.value[activeCategory.value]?.id;
148 if (categoryId == null) return []; 127 if (categoryId == null) return [];
149 128
150 let list = planData.value.filter((p) => p._categoryId === categoryId); 129 let list = planData.value.filter((p) => p._categoryId === categoryId);
151 130
152 - // 频率  
153 - if (selectedFilters.value.频率 !== '不限') {  
154 - const val = filterMap.频率[selectedFilters.value.频率];  
155 - list = list.filter((item) => item.frequency === val);  
156 - }  
157 -  
158 - // 难度  
159 - if (selectedFilters.value.难度 !== '不限') {  
160 - const val = filterMap.难度[selectedFilters.value.难度];  
161 - list = list.filter((item) => item.difficultyLevel === val);  
162 - }  
163 -  
164 - // 场景  
165 - if (selectedFilters.value.场景 !== '不限') {  
166 - const val = filterMap.场景[selectedFilters.value.场景];  
167 - list = list.filter((item) => item.scene === val);  
168 - }  
169 -  
170 - // 人群  
171 - if (selectedFilters.value.人群 !== '不限') {  
172 - const val = filterMap.人群[selectedFilters.value.人群];  
173 - list = list.filter((item) => item.population === val); 131 + Object.keys(filterMap).forEach((key) => {
  132 + if (selectedFilters.value[key] !== '不限') {
  133 + const val = filterMap[key][selectedFilters.value[key]];
  134 + list = list.filter((item) => item[filterFieldMap[key]] === val);
174 } 135 }
  136 + });
175 137
176 return list; 138 return list;
177 }); 139 });
178 140
179 -// ✅ 切换分类时加载数据  
180 const switchCategory = async (index) => { 141 const switchCategory = async (index) => {
181 activeCategory.value = index; 142 activeCategory.value = index;
182 const categoryId = categoryList.value[index]?.id; 143 const categoryId = categoryList.value[index]?.id;
183 if (categoryId != null) { 144 if (categoryId != null) {
184 - // 检查是否已加载过该分类  
185 const isLoaded = planData.value.some((p) => p._categoryId === categoryId); 145 const isLoaded = planData.value.some((p) => p._categoryId === categoryId);
186 if (!isLoaded) { 146 if (!isLoaded) {
187 await loadPlansByCategory(categoryId); 147 await loadPlansByCategory(categoryId);
@@ -189,51 +149,43 @@ const switchCategory = async (index) => { @@ -189,51 +149,43 @@ const switchCategory = async (index) => {
189 } 149 }
190 }; 150 };
191 151
192 -// ====== 数据加载 ======  
193 const loadCategories = async () => { 152 const loadCategories = async () => {
194 try { 153 try {
195 - // 获得计划分类,左侧导航栏显示用  
196 const res = await QueryPlanApi.getCategories(); 154 const res = await QueryPlanApi.getCategories();
197 categoryList.value = res.data || []; 155 categoryList.value = res.data || [];
198 - console.log('加载计划列表categoryList.value:', categoryList.value); 156 +
199 if (categoryList.value.length > 0) { 157 if (categoryList.value.length > 0) {
200 - await switchCategory(0); // 加载第一个分类 158 + await switchCategory(0);
201 } 159 }
202 } catch (err) { 160 } catch (err) {
203 console.error('加载分类失败:', err); 161 console.error('加载分类失败:', err);
204 - uni.showToast({ title: '加载分类失败', icon: 'none' });  
205 } 162 }
206 }; 163 };
207 164
208 -// ✅ 核心:加载计划,并手动附加 _categoryId  
209 const loadPlansByCategory = async (categoryId) => { 165 const loadPlansByCategory = async (categoryId) => {
210 try { 166 try {
211 - // 根据计划分类ID获得计划列表,右侧计划列表显示用  
212 const res = await QueryPlanApi.getPlanList(categoryId); 167 const res = await QueryPlanApi.getPlanList(categoryId);
213 - console.log('加载计划列表res.data:', res.data); 168 +
214 const plans = (res.data || []).map((item) => ({ 169 const plans = (res.data || []).map((item) => ({
215 id: item.id, 170 id: item.id,
216 title: item.name, 171 title: item.name,
217 meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`, 172 meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`,
218 tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '', 173 tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '',
219 - cover: item.urlCover || '默认图', 174 + cover: item.urlCover,
220 _categoryId: categoryId, 175 _categoryId: categoryId,
221 - frequency: item.frequencyPerWeek, // 新增  
222 - difficultyLevel: item.difficultyLevel, // 新增  
223 - scene: item.trainingScene, // 新增  
224 - population: item.targetPeople, // 新增 176 + frequency: item.frequencyPerWeek,
  177 + difficultyLevel: item.difficultyLevel,
  178 + scene: item.trainingScene,
  179 + population: item.targetPeople,
225 })); 180 }));
226 - // 合并数据(避免重复)  
227 planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans]; 181 planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans];
228 - // 打印planData  
229 - console.log('加载计划列表planData.value:', planData.value); 182 +
230 } catch (err) { 183 } catch (err) {
231 console.error('加载计划失败:', err); 184 console.error('加载计划失败:', err);
232 - uni.showToast({ title: '加载计划失败', icon: 'none' }); 185 +
233 } 186 }
234 }; 187 };
235 188
236 -// 跳转到搜索页面  
237 const navigateToSearch = () => { 189 const navigateToSearch = () => {
238 const app = getApp(); 190 const app = getApp();
239 app.globalData.allPlansForSearch = planData.value; 191 app.globalData.allPlansForSearch = planData.value;
@@ -241,88 +193,35 @@ const navigateToSearch = () => { @@ -241,88 +193,35 @@ const navigateToSearch = () => {
241 url: '/pages4/pages/xunji/jihua-search', 193 url: '/pages4/pages/xunji/jihua-search',
242 }); 194 });
243 }; 195 };
244 -// 跳转到计划详情页  
245 const navigateToDetail = (plan) => { 196 const navigateToDetail = (plan) => {
246 uni.navigateTo({ 197 uni.navigateTo({
247 url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`, 198 url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`,
248 }); 199 });
249 }; 200 };
250 201
251 -// ====== 返回按钮 ======  
252 -const back = () => {  
253 - const pages = getCurrentPages();  
254 - if (pages.length > 1) {  
255 - uni.navigateBack();  
256 - } else {  
257 - uni.switchTab({ url: '/pages/index/index' }); // 请替换为你的首页路径  
258 - }  
259 -};  
260 -  
261 onMounted(() => { 202 onMounted(() => {
262 loadCategories(); 203 loadCategories();
263 }); 204 });
264 </script> 205 </script>
265 206
266 <style lang="scss" scoped> 207 <style lang="scss" scoped>
267 -/* ========== 新增:页面头部样式 ========== */  
268 -// .page-header {  
269 -// display: flex;  
270 -// align-items: center;  
271 -// padding: 30rpx;  
272 -// background: #fff;  
273 -// box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);  
274 -// position: sticky;  
275 -// top: 0;  
276 -// z-index: 999;  
277 -// }  
278 -  
279 -// .page-title {  
280 -// font-size: 32rpx;  
281 -// font-weight: bold;  
282 -// color: #333;  
283 -// flex: 1;  
284 -// text-align: center;  
285 -// }  
286 -  
287 -/* ========== 以下是你原有的全部样式(完全保留) ========== */  
288 .plan-page { 208 .plan-page {
289 width: 100%; 209 width: 100%;
290 - height: 100vh;  
291 background-color: #f5f5f5; 210 background-color: #f5f5f5;
292 box-sizing: border-box; 211 box-sizing: border-box;
293 - overflow: hidden;  
294 - display: flex;  
295 - flex-direction: column;  
296 -}  
297 -  
298 -/* 顶部横幅 */  
299 -.banner-wrapper {  
300 - height: 250rpx;  
301 - width: 100%;  
302 - margin: 20rpx 0;  
303 -  
304 - .swiper {  
305 - width: 100%;  
306 - height: 100%;  
307 -  
308 - .swiper-item {  
309 - width: 100%;  
310 - height: 100%;  
311 - padding: 20rpx;  
312 - box-sizing: border-box;  
313 -  
314 - .img {  
315 - width: 100%;  
316 - height: 220rpx;  
317 - border-radius: 20rpx;  
318 - }  
319 - }  
320 - } 212 + height: calc(100vh - 50px);
  213 + // #ifdef MP-WEIXIN
  214 + height: calc(100vh - 138px);
  215 + // #endif
321 } 216 }
322 217
323 /* 筛选条 */ 218 /* 筛选条 */
324 .filter-bar { 219 .filter-bar {
325 - margin: 0 20rpx 20rpx; 220 + position: relative;
  221 + margin: 20rpx 20rpx;
  222 + // #ifdef MP-WEIXIN
  223 + margin-top: 40rpx;
  224 + // #endif
326 padding: 12rpx 18rpx; 225 padding: 12rpx 18rpx;
327 border-radius: 40rpx; 226 border-radius: 40rpx;
328 display: flex; 227 display: flex;
@@ -369,14 +268,9 @@ onMounted(() => { @@ -369,14 +268,9 @@ onMounted(() => {
369 //下拉抽屉 268 //下拉抽屉
370 .filter-drawer { 269 .filter-drawer {
371 position: absolute; 270 position: absolute;
372 - top: 560rpx;  
373 -  
374 - /* #ifdef H5 */  
375 - top: 247px;  
376 - /* #endif */  
377 -  
378 - left: 20rpx;  
379 - right: 20rpx; 271 + top: 100%;
  272 + left: -18rpx;
  273 + right: -18rpx;
380 z-index: 999; 274 z-index: 999;
381 border-radius: 0 0 40rpx 40rpx; 275 border-radius: 0 0 40rpx 40rpx;
382 background-color: #fff; 276 background-color: #fff;
@@ -406,11 +300,10 @@ onMounted(() => { @@ -406,11 +300,10 @@ onMounted(() => {
406 300
407 /* 主体布局 */ 301 /* 主体布局 */
408 .content-layout { 302 .content-layout {
409 - flex: 1; 303 + height: 100%;
410 display: flex; 304 display: flex;
411 box-sizing: border-box; 305 box-sizing: border-box;
412 - overflow: hidden;  
413 - min-height: 0; 306 +
414 } 307 }
415 308
416 /* 左侧分类 */ 309 /* 左侧分类 */
@@ -418,10 +311,10 @@ onMounted(() => { @@ -418,10 +311,10 @@ onMounted(() => {
418 width: 180rpx; 311 width: 180rpx;
419 background-color: #ffffff; 312 background-color: #ffffff;
420 border-radius: 16rpx; 313 border-radius: 16rpx;
421 - height: 72%;  
422 - /* #ifdef H5 */ 314 + height: 88%;
  315 + // #ifdef MP-WEIXIN
423 height: 86%; 316 height: 86%;
424 - /* #endif */ 317 + // #endif
425 box-sizing: border-box; 318 box-sizing: border-box;
426 overflow: hidden; 319 overflow: hidden;
427 320
@@ -455,10 +348,10 @@ onMounted(() => { @@ -455,10 +348,10 @@ onMounted(() => {
455 flex: 1; 348 flex: 1;
456 padding: 0 20rpx; 349 padding: 0 20rpx;
457 box-sizing: border-box; 350 box-sizing: border-box;
458 - height: 72%;  
459 - /* #ifdef H5 */ 351 + height: 88%;
  352 + // #ifdef MP-WEIXIN
460 height: 86%; 353 height: 86%;
461 - /* #endif */ 354 + // #endif
462 overflow: hidden; 355 overflow: hidden;
463 } 356 }
464 357
1 <template> 1 <template>
2 - <view class="home-page" v-if="userStore.isLogin">  
3 - <view class="tab-bar" :style="{ paddingTop: menuButtonHeight + topSafeArea + 'px' }">  
4 - <!-- <view class="tab-item" :class="{ active: currentTab === 0 }" @click="handleTabClick(0)">训记</view> -->  
5 - <view class="tab-item" :class="{ active: currentTab === 1 }" @click="handleTabClick(1)">计划</view>  
6 - <view class="tab-item" :class="{ active: currentTab === 2 }" @click="handleTabClick(2)">日历</view>  
7 - <view class="tab-item" :class="{ active: currentTab === 3 }" @click="handleTabClick(3)">动作</view>  
8 - <view class="tab-item" :class="{ active: currentTab === 4 }" @click="handleTabClick(4)">模板</view>  
9 - <view class="tab-item" :class="{ active: currentTab === 5 }" @click="handleTabClick(5)">数据</view>  
10 - <view class="menu-btn" @click="openDrawer">  
11 - <uni-icons type="bars" size="20"></uni-icons> 2 + <view class="home-page">
  3 + <view class="tab-bar" :style="{ paddingTop: PageHeight + 'px' }">
  4 + <view class="tab-item" :class="{ active: currentTab == 1 }" @click="handleTabClick(1)">计划</view>
  5 + <view class="tab-item" :class="{ active: currentTab == 2 }" @click="handleTabClick(2)">日历</view>
  6 + <view class="tab-item" :class="{ active: currentTab == 3 }" @click="handleTabClick(3)">动作</view>
  7 + <view class="tab-item" :class="{ active: currentTab == 4 }" @click="handleTabClick(4)">模板</view>
  8 + <view class="tab-item" :class="{ active: currentTab == 5 }" @click="handleTabClick(5)">数据</view>
  9 + <view class="menu-btn" @click="drawerShow = true">
  10 + <up-icon name="list" size="20" />
12 </view> 11 </view>
13 </view> 12 </view>
14 13
15 <view class="content"> 14 <view class="content">
16 - <!-- <xunjiXunji v-if="currentTab === 0" /> -->  
17 <xunjiXunlianjihua v-if="currentTab === 1" /> 15 <xunjiXunlianjihua v-if="currentTab === 1" />
18 <xunjiRili v-if="currentTab === 2" /> 16 <xunjiRili v-if="currentTab === 2" />
19 <xunjiDongzuo v-if="currentTab === 3" /> 17 <xunjiDongzuo v-if="currentTab === 3" />
@@ -21,116 +19,69 @@ @@ -21,116 +19,69 @@
21 <xunjiShuju v-if="currentTab === 5" /> 19 <xunjiShuju v-if="currentTab === 5" />
22 </view> 20 </view>
23 21
24 - <Tabbar />  
25 22
26 - <uni-drawer ref="drawer" mode="right" :mask="true" :width="280">  
27 - <view class="drawer-content" :style="{ paddingTop: menuButtonHeight + topSafeArea + 'px' }"> 23 +
  24 + <up-popup :show="drawerShow" mode="right" :closeable="true" @close="drawerShow = false">
  25 + <view class="drawer-content" :style="{ paddingTop: PageHeight + 'px' }">
28 <view class="user-header"> 26 <view class="user-header">
29 - <image class="avatar" :src="userInfo.avatar ||  
30 - 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'  
31 - " mode="aspectFill"></image> 27 + <image
  28 + class="avatar"
  29 + :src="userInfo.avatar || defaultAvatar "
  30 + mode="aspectFill"
  31 + />
32 <view class="user-info"> 32 <view class="user-info">
33 - <text class="nickname">{{ userInfo.nickname || '未登录' }}</text>  
34 - </view>  
35 - </view>  
36 -  
37 - <image class="ad-banner"  
38 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png"  
39 - mode="aspectFill"></image>  
40 -  
41 - <view class="menu-list">  
42 - <view class="menu-item" @click="navigateTo('/pages4/pages/xunji/xunji-wode-zhuye')">  
43 - <uni-icons type="person" size="18"></uni-icons>  
44 - <text class="menu-text">我的主页</text>  
45 - <uni-icons type="right" size="14"></uni-icons>  
46 - </view>  
47 - <view class="menu-item" @click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')">  
48 - <uni-icons type="wallet" size="18"></uni-icons>  
49 - <text class="menu-text">我的模板</text>  
50 - <uni-icons type="right" size="14"></uni-icons>  
51 - </view>  
52 - <view class="menu-item" @click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')">  
53 - <uni-icons type="list" size="18"></uni-icons>  
54 - <text class="menu-text">我的计划</text>  
55 - <uni-icons type="right" size="14"></uni-icons>  
56 - </view>  
57 - <view class="menu-item" @click="goWidgetLib">  
58 - <uni-icons type="more" size="18"></uni-icons>  
59 - <text class="menu-text">小组件库</text>  
60 - <uni-icons type="right" size="14"></uni-icons>  
61 - </view>  
62 - <view class="menu-item" @click="goFeedback">  
63 - <uni-icons type="chat" size="18"></uni-icons>  
64 - <text class="menu-text">训记反馈建议</text>  
65 - <uni-icons type="right" size="14"></uni-icons> 33 + <text class="nickname">{{ userInfo.nickname || '-' }}</text>
66 </view> 34 </view>
67 - <view class="menu-item" @click="goUserGroup">  
68 - <uni-icons type="chatboxes" size="18"></uni-icons>  
69 - <text class="menu-text">训记内测用户群</text>  
70 - <uni-icons type="right" size="14"></uni-icons>  
71 </view> 35 </view>
72 - <view class="menu-item" @click="goTutorial">  
73 - <uni-icons type="help" size="18"></uni-icons>  
74 - <text class="menu-text">训记使用教程</text>  
75 - <uni-icons type="right" size="14"></uni-icons> 36 + <up-cell-group :border="false">
  37 + <up-cell title="我的主页" icon="account" :isLink="true"
  38 + @click="navigateTo('/pages4/pages/xunji/xunji-wode-zhuye')" />
  39 + <up-cell title="我的模板" icon="file-text" :isLink="true"
  40 + @click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')" />
  41 + <up-cell title="我的计划" icon="order" :isLink="true"
  42 + @click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')" />
  43 + <up-cell title="训记使用教程" icon="question-circle" :isLink="true" @click="goTutorial" />
  44 + </up-cell-group>
76 </view> 45 </view>
77 - </view>  
78 - </view>  
79 - </uni-drawer> 46 + </up-popup>
80 47
81 <TrainingFloating /> 48 <TrainingFloating />
  49 + <Tabbar />
82 </view> 50 </view>
83 -  
84 </template> 51 </template>
85 52
86 <script setup> 53 <script setup>
87 -import { ref, shallowRef, computed, onMounted } from 'vue';  
88 -import { onLoad, onHide, onShow } from '@dcloudio/uni-app'; 54 +import { ref, computed } from 'vue';
  55 +import { onLoad, onShow } from '@dcloudio/uni-app';
89 import useUserStore from '@/sheep/store/user'; 56 import useUserStore from '@/sheep/store/user';
90 import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea'; 57 import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea';
91 -  
92 -// 组件导入 58 +import { defaultAvatar } from '@/sheep/config/index';
93 import xunjiRili from '@/pages/xunji/components/xunji-rili.vue'; 59 import xunjiRili from '@/pages/xunji/components/xunji-rili.vue';
94 import xunjiDongzuo from '@/pages/xunji/components/xunji-dongzuo.vue'; 60 import xunjiDongzuo from '@/pages/xunji/components/xunji-dongzuo.vue';
95 import xunjiMoban from '@/pages/xunji/components/xunji-moban.vue'; 61 import xunjiMoban from '@/pages/xunji/components/xunji-moban.vue';
96 import xunjiShuju from '@/pages/xunji/components/xunji-shuju.vue'; 62 import xunjiShuju from '@/pages/xunji/components/xunji-shuju.vue';
97 -import xunjiXunji from '@/pages/xunji/components/xunji-xunji.vue';  
98 import xunjiXunlianjihua from '@/pages/xunji/components/xunji-xunlianjihua.vue'; 63 import xunjiXunlianjihua from '@/pages/xunji/components/xunji-xunlianjihua.vue';
  64 +import TrainingFloating from '@/pages/TrainingFloating.vue';
99 65
100 -import TrainingFloating from '@/pages/TrainingFloating.vue'  
101 -import { useTrainingStore } from '@/sheep/store/trainingStore';  
102 -const trainingStore = useTrainingStore();  
103 -  
104 -// 状态管理  
105 const userStore = useUserStore(); 66 const userStore = useUserStore();
  67 +const drawerShow = ref(false);
  68 +const currentTab = ref(3);
106 69
107 -// --- 补全缺失的响应式变量定义 ---  
108 -const currentTab = ref(1);  
109 -const currentComponent = shallowRef(xunjiXunji); // 用于 H5 端动态组件切换(如保留原功能逻辑)  
110 -const drawer = ref(null); // uni-drawer 的组件实例引用  
111 -  
112 -// 安全区域相关变量(从工具函数异步或同步获取)  
113 -const menuButtonHeight = ref(getMenuButtonHeight ? getMenuButtonHeight() : 0);  
114 -const topSafeArea = ref(getTopSafeArea ? getTopSafeArea() : 0); 70 +const menuButtonHeight = ref(getMenuButtonHeight());
  71 +const topSafeArea = ref(getTopSafeArea());
  72 +const PageHeight = ref(topSafeArea.value);
  73 +// #ifdef MP-WEIXIN
  74 +PageHeight.value = menuButtonHeight.value + topSafeArea.value;
  75 +// #endif
115 76
116 -// 用户信息计算属性(防止 store 异步更新时报错)  
117 const userInfo = computed(() => userStore.userInfo || {}); 77 const userInfo = computed(() => userStore.userInfo || {});
118 78
119 -// --- 原有页面生命周期维护 ---  
120 -onHide(() => {  
121 - if (drawer.value) {  
122 - drawer.value.close();  
123 - }  
124 -});  
125 -  
126 onLoad((options) => { 79 onLoad((options) => {
127 if (!userStore.isLogin) { 80 if (!userStore.isLogin) {
128 - uni.redirectTo({  
129 - url: '/pages7/pages/index/login',  
130 - }); 81 + uni.redirectTo({ url: '/pages7/pages/index/login' });
131 return; 82 return;
132 } 83 }
133 - // 确保解析路由参数的正确性 84 + userStore.getInfo();
134 if (options && options.currentTab) { 85 if (options && options.currentTab) {
135 currentTab.value = Number(options.currentTab); 86 currentTab.value = Number(options.currentTab);
136 } 87 }
@@ -142,85 +93,40 @@ onShow(() => { @@ -142,85 +93,40 @@ onShow(() => {
142 const tabNum = Number(targetTab); 93 const tabNum = Number(targetTab);
143 currentTab.value = tabNum; 94 currentTab.value = tabNum;
144 handleTabClick(tabNum); 95 handleTabClick(tabNum);
145 - // 使用后清空缓存,避免下次进来自动跳转  
146 uni.removeStorageSync('xunjiTargetTab'); 96 uni.removeStorageSync('xunjiTargetTab');
147 } 97 }
  98 +});
148 99
149 -})  
150 -  
151 -// --- 原有业务方法补充与修复 ---  
152 -  
153 -// 打开抽屉  
154 -const openDrawer = () => {  
155 - if (drawer.value) {  
156 - drawer.value.open();  
157 - }  
158 -};  
159 -  
160 -// 修复模板直接调用 uni.navigateTo 的多端兼容问题  
161 const navigateTo = (url) => { 100 const navigateTo = (url) => {
162 uni.navigateTo({ url }); 101 uni.navigateTo({ url });
163 }; 102 };
164 103
165 -// 处理标签点击  
166 const handleTabClick = (index) => { 104 const handleTabClick = (index) => {
167 currentTab.value = index; 105 currentTab.value = index;
168 - // #ifdef H5  
169 - switch (index) {  
170 - case 0:  
171 - currentComponent.value = xunjiXunji;  
172 - break;  
173 - case 1:  
174 - currentComponent.value = xunjiXunlianjihua;  
175 - break;  
176 - case 2:  
177 - currentComponent.value = xunjiRili;  
178 - break;  
179 - case 3:  
180 - currentComponent.value = xunjiDongzuo;  
181 - break;  
182 - case 4:  
183 - currentComponent.value = xunjiMoban;  
184 - break;  
185 - case 5:  
186 - currentComponent.value = xunjiShuju;  
187 - break;  
188 - default:  
189 - currentComponent.value = xunjiXunlianjihua;  
190 - }  
191 - // #endif  
192 }; 106 };
193 107
194 -// 空实现占位(防止模板点击报错,保持你原有的未给出的方法定义)  
195 -const goWidgetLib = () => {  
196 - uni.navigateTo({ url: '/pages4/pages/xunji/widget' });  
197 -};  
198 -const goFeedback = () => { };  
199 -const goUserGroup = () => { }; 108 +
  109 +
200 const goTutorial = () => { }; 110 const goTutorial = () => { };
201 111
202 -defineExpose({  
203 - switchTabIndex: handleTabClick  
204 -}) 112 +defineExpose({ switchTabIndex: handleTabClick });
205 </script> 113 </script>
206 <style scoped lang="scss"> 114 <style scoped lang="scss">
207 .home-page { 115 .home-page {
208 width: 100%; 116 width: 100%;
209 - height: 100vh;  
210 // #ifdef H5 117 // #ifdef H5
211 - padding-bottom: 90rpx; 118 + height: calc(100vh - 50px);
212 // #endif 119 // #endif
213 - background-color: #f5f5f5;  
214 - box-sizing: border-box;  
215 - overflow: hidden;  
216 - display: flex;  
217 - flex-direction: column; 120 + // #ifdef MP-WEIXIN
  121 + height: calc(100vh - 50px);
  122 + // #endif
  123 + background-color: #fff;
218 124
219 .content { 125 .content {
220 - flex: 1; 126 +
221 box-sizing: border-box; 127 box-sizing: border-box;
222 overflow: hidden; 128 overflow: hidden;
223 - padding-top: 70rpx; 129 + padding-top: 65rpx;
224 // #ifdef MP-WEIXIN 130 // #ifdef MP-WEIXIN
225 padding-top: 200rpx; 131 padding-top: 200rpx;
226 // #endif 132 // #endif
@@ -231,37 +137,32 @@ defineExpose({ @@ -231,37 +137,32 @@ defineExpose({
231 display: flex; 137 display: flex;
232 align-items: flex-end; 138 align-items: flex-end;
233 justify-content: space-between; 139 justify-content: space-between;
234 - padding: 0px 20px;  
235 - padding-top: 20rpx; 140 + padding: 0 20px;
236 background-color: #fff; 141 background-color: #fff;
237 -  
238 position: fixed; 142 position: fixed;
239 top: 0; 143 top: 0;
240 left: 0; 144 left: 0;
241 right: 0; 145 right: 0;
242 z-index: 999; 146 z-index: 999;
243 box-sizing: border-box; 147 box-sizing: border-box;
244 -}  
245 148
246 -/* 标签项:默认样式 */  
247 -.tab-item { 149 + .tab-item {
248 font-size: 16px; 150 font-size: 16px;
249 color: #666; 151 color: #666;
250 padding: 5px 0; 152 padding: 5px 0;
251 padding-top: 30rpx; 153 padding-top: 30rpx;
252 -}  
253 154
254 -/* 激活态标签:下划线+深色文字 */  
255 -.tab-item.active { 155 + &.active {
256 color: #333; 156 color: #333;
257 border-bottom: 2px solid #000; 157 border-bottom: 2px solid #000;
258 font-weight: 500; 158 font-weight: 500;
259 -} 159 + }
  160 + }
260 161
261 -/* 菜单按钮:样式 */  
262 -.menu-btn { 162 + .menu-btn {
263 font-size: 20px; 163 font-size: 20px;
264 color: #666; 164 color: #666;
  165 + }
265 } 166 }
266 167
267 .card-subtitle { 168 .card-subtitle {
@@ -294,82 +195,37 @@ defineExpose({ @@ -294,82 +195,37 @@ defineExpose({
294 color: #666; 195 color: #666;
295 } 196 }
296 197
297 -/* 抽屉样式 */  
298 .drawer-content { 198 .drawer-content {
299 - background-color: #ffffff; 199 + width: 500rpx;
  200 + background-color: #f7f7f8;
300 height: 100%; 201 height: 100%;
301 - padding: 20rpx; 202 + padding: 0;
302 box-sizing: border-box; 203 box-sizing: border-box;
303 -}  
304 204
305 -/* 用户头像和信息区域 */  
306 -.user-header { 205 + .user-header {
307 display: flex; 206 display: flex;
308 align-items: center; 207 align-items: center;
309 - padding: 20rpx 0;  
310 - border-bottom: 1px solid #f0f0f0;  
311 -} 208 + padding: 40rpx 30rpx 30rpx;
  209 + background: #fff;
  210 + margin-bottom: 20rpx;
312 211
313 -.user-header .avatar {  
314 - width: 80rpx;  
315 - height: 80rpx; 212 + .avatar {
  213 + width: 90rpx;
  214 + height: 90rpx;
316 border-radius: 50%; 215 border-radius: 50%;
317 - margin-right: 20rpx;  
318 - margin-top: 0;  
319 - justify-content: flex-start;  
320 -} 216 + background: #f0f0f0;
  217 + flex-shrink: 0;
  218 + }
321 219
322 -.user-info {  
323 - display: flex;  
324 - flex-direction: column;  
325 -} 220 + .user-info {
  221 + margin-left: 20rpx;
326 222
327 -.nickname { 223 + .nickname {
328 font-size: 28rpx; 224 font-size: 28rpx;
329 font-weight: 500; 225 font-weight: 500;
330 - color: #333333;  
331 - margin-bottom: 8rpx;  
332 -}  
333 -  
334 -.user-id {  
335 - font-size: 24rpx;  
336 - color: #999999;  
337 -}  
338 -  
339 -/* 广告横幅 */  
340 -.ad-banner {  
341 - width: 100%;  
342 - height: 200rpx;  
343 - margin: 20rpx 0;  
344 - border-radius: 12rpx;  
345 -}  
346 -  
347 -/* 菜单列表 */  
348 -.menu-list {  
349 - margin-top: 20rpx;  
350 -}  
351 -  
352 -.menu-item {  
353 - display: flex;  
354 - align-items: center;  
355 - justify-content: space-between;  
356 - padding: 30rpx 0;  
357 - border-bottom: 1px solid #f0f0f0;  
358 -}  
359 -  
360 -.menu-item:last-child {  
361 - border-bottom: none;  
362 -}  
363 -  
364 -.menu-text {  
365 - font-size: 28rpx;  
366 - color: #333333;  
367 - margin-left: 20rpx;  
368 - flex: 1;  
369 -}  
370 -  
371 -/* 覆盖uni-icons默认颜色 */  
372 -:deep(.uni-icons) {  
373 - color: #666666; 226 + color: #333;
  227 + }
  228 + }
  229 + }
374 } 230 }
375 </style> 231 </style>
1 -<!--  
2 -基础版训练计划搜索页(仅关键词搜索)  
3 -页面位于:训记横向导航栏的‘计划’模块,右侧搜索图标点击后进入的页面就是,核心功能是提供一个简单的搜索界面,允许用户输入关键词搜索训练计划,还未完成  
4 --->  
5 <template> 1 <template>
6 <view class="search-page"> 2 <view class="search-page">
7 - <!-- 顶部搜索栏 -->  
8 <view class="search-header"> 3 <view class="search-header">
9 - <view class="search-input-wrapper">  
10 - <uni-icons type="search" size="18" color="#999" class="search-icon"></uni-icons>  
11 - <input ref="searchInput" v-model="searchKeyword" class="search-input" type="text" placeholder="搜索训练计划"  
12 - placeholder-style="color:#999" confirm-type="search" @confirm="handleSearch" focus />  
13 - </view>  
14 - <view class="cancel-btn" @click="handleCancel">  
15 - <text class="cancel-text">取消</text>  
16 - </view>  
17 - </view>  
18 -  
19 - <!-- 搜索结果区域 -->  
20 - <view class="search-results">  
21 - <!-- 未搜索时提示 -->  
22 - <view v-if="!hasSearched" class="empty-hint">  
23 - <text>请输入关键词搜索训练计划</text> 4 + <up-search v-model="searchKeyword" placeholder="搜索训练计划" placeholderColor="#999" shape="round"
  5 + :showAction="true" actionText="取消" :clearabled="true" @search="handleSearch" @custom="handleSearch"
  6 + @cancel="handleCancel" />
24 </view> 7 </view>
25 8
26 - <!-- 已搜索但无结果 -->  
27 - <view v-else-if="searchResults.length === 0" class="empty-hint">  
28 - <text>未找到相关训练计划</text>  
29 - </view>  
30 -  
31 - <!-- 搜索结果列表 --> 9 + <scroll-view class="search-results" scroll-y enable-flex>
  10 + <up-empty v-if="!hasSearched" text="请输入关键词搜索训练计划" mode="search" />
  11 + <up-empty v-else-if="searchResults.length === 0" text="未找到相关训练计划" mode="data" />
32 <view v-else class="results-list"> 12 <view v-else class="results-list">
33 <view v-for="plan in searchResults" :key="plan.id" class="result-item" @click="goToDetail(plan)"> 13 <view v-for="plan in searchResults" :key="plan.id" class="result-item" @click="goToDetail(plan)">
34 <image v-if="plan.cover" :src="plan.cover" class="result-cover" mode="aspectFill" /> 14 <image v-if="plan.cover" :src="plan.cover" class="result-cover" mode="aspectFill" />
@@ -38,77 +18,68 @@ @@ -38,77 +18,68 @@
38 </view> 18 </view>
39 </view> 19 </view>
40 </view> 20 </view>
41 - </view> 21 + </scroll-view>
42 </view> 22 </view>
43 </template> 23 </template>
44 24
45 <script setup> 25 <script setup>
46 -import { ref, onMounted } from 'vue';  
47 -import QueryPlanApi from '@/sheep/api/plan/queryplan' 26 +import { ref, computed, onMounted } from 'vue';
  27 +import QueryPlanApi from '@/sheep/api/plan/queryplan';
  28 +import { getNavbarHeight } from '@/utils/safeArea';
48 29
49 -// 响应式数据  
50 const searchKeyword = ref(''); 30 const searchKeyword = ref('');
51 const hasSearched = ref(false); 31 const hasSearched = ref(false);
52 const searchResults = ref([]); 32 const searchResults = ref([]);
53 -const allPlans = ref([]);  
54 -const searchInput = ref(null);  
55 -// 难度/频率/场景/人群 数字转文字映射(和列表页保持一致) 33 +const navBarHeight = ref(0);
  34 +
56 const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' }; 35 const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };
57 const frequencyText = { 0: '不限', 1: '1练/周', 2: '2练/周', 3: '3练/周', 4: '4练/周', 5: '5练/周', 6: '6练/周', 7: '7练/周' }; 36 const frequencyText = { 0: '不限', 1: '1练/周', 2: '2练/周', 3: '3练/周', 4: '4练/周', 5: '5练/周', 6: '6练/周', 7: '7练/周' };
58 -// 页面加载时获取全局数据 37 +
  38 +let isMpWeixin = false;
  39 +// #ifdef MP-WEIXIN
  40 +isMpWeixin = true;
  41 +// #endif
  42 +
  43 +
  44 +
  45 +
59 onMounted(() => { 46 onMounted(() => {
60 - // 自动聚焦输入框(uni-app 需要延迟)  
61 - setTimeout(() => {  
62 - if (searchInput.value && typeof searchInput.value.focus === 'function') {  
63 - searchInput.value.focus(); 47 + if (isMpWeixin) {
  48 + navBarHeight.value = getNavbarHeight();
64 } 49 }
65 - }, 300);  
66 }); 50 });
67 -// 返回  
68 -const goBack = () => {  
69 - uni.navigateBack();  
70 -};  
71 -// 执行接口搜索 51 +
72 const handleSearch = async () => { 52 const handleSearch = async () => {
73 const keyword = searchKeyword.value.trim(); 53 const keyword = searchKeyword.value.trim();
74 hasSearched.value = true; 54 hasSearched.value = true;
75 -  
76 if (!keyword) { 55 if (!keyword) {
77 searchResults.value = []; 56 searchResults.value = [];
78 return; 57 return;
79 } 58 }
80 -  
81 - // 调用接口:根据名称模糊查询计划  
82 try { 59 try {
83 const res = await QueryPlanApi.searchPlansByName(keyword); 60 const res = await QueryPlanApi.searchPlansByName(keyword);
84 - // 接口返回成功,处理数据  
85 if (res.code === 0) { 61 if (res.code === 0) {
86 - // 映射接口字段到页面字段  
87 searchResults.value = res.data.map(item => ({ 62 searchResults.value = res.data.map(item => ({
88 id: item.id, 63 id: item.id,
89 - title: item.name, // 接口字段name → 页面字段title  
90 - cover: item.urlCover, // 接口字段urlCover → 页面字段cover 64 + title: item.name,
  65 + cover: item.urlCover,
91 meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${frequencyText[item.frequencyPerWeek] || '1练/周'}` 66 meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${frequencyText[item.frequencyPerWeek] || '1练/周'}`
92 })); 67 }));
93 } else { 68 } else {
94 - // 接口返回错误,提示用户  
95 uni.showToast({ title: res.msg || '搜索失败', icon: 'none' }); 69 uni.showToast({ title: res.msg || '搜索失败', icon: 'none' });
96 searchResults.value = []; 70 searchResults.value = [];
97 } 71 }
98 } catch (err) { 72 } catch (err) {
99 - // 网络/接口异常处理  
100 uni.showToast({ title: '网络异常,请重试', icon: 'none' }); 73 uni.showToast({ title: '网络异常,请重试', icon: 'none' });
101 searchResults.value = []; 74 searchResults.value = [];
102 console.error('搜索接口调用失败:', err); 75 console.error('搜索接口调用失败:', err);
103 } 76 }
104 }; 77 };
105 78
106 -// 取消并返回  
107 const handleCancel = () => { 79 const handleCancel = () => {
108 uni.navigateBack(); 80 uni.navigateBack();
109 }; 81 };
110 82
111 -// 点击跳转详情(根据你项目结构调整)  
112 const goToDetail = (plan) => { 83 const goToDetail = (plan) => {
113 if (!plan?.id) { 84 if (!plan?.id) {
114 uni.showToast({ title: '计划信息异常', icon: 'none' }); 85 uni.showToast({ title: '计划信息异常', icon: 'none' });
@@ -122,88 +93,33 @@ const goToDetail = (plan) => { @@ -122,88 +93,33 @@ const goToDetail = (plan) => {
122 93
123 <style lang="scss" scoped> 94 <style lang="scss" scoped>
124 .search-page { 95 .search-page {
125 - // 整个页面往下偏移,避免被顶部导航栏遮挡  
126 - padding-top: 160rpx;  
127 height: 100vh; 96 height: 100vh;
128 background-color: #f5f5f5; 97 background-color: #f5f5f5;
129 display: flex; 98 display: flex;
130 flex-direction: column; 99 flex-direction: column;
131 -}  
132 100
133 -/* 顶部搜索栏 */  
134 -.search-header {  
135 - display: flex;  
136 - align-items: center;  
137 - padding: 20rpx 20rpx 20rpx 30rpx; 101 + .search-header {
  102 + padding: 14rpx 20rpx;
138 background-color: #ffffff; 103 background-color: #ffffff;
139 - // box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); 104 + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
140 z-index: 999; 105 z-index: 999;
141 - // 固定顶部搜索栏  
142 - position: fixed;  
143 - top: 160rpx;  
144 - left: 0;  
145 - right: 0;  
146 -}  
147 106
148 -.search-input-wrapper {  
149 - flex: 1;  
150 - background-color: #f5f5f5;  
151 - border-radius: 12rpx;  
152 - padding: 0 20rpx;  
153 - height: 64rpx;  
154 - display: flex;  
155 - align-items: center;  
156 - gap: 10rpx;  
157 -} 107 + position: sticky;
  108 + top: 0;
158 109
159 -.search-icon {  
160 - flex-shrink: 0;  
161 -}  
162 110
163 -.search-input {  
164 - flex: 1;  
165 - height: 100%;  
166 - font-size: 28rpx;  
167 - color: #333;  
168 - background: transparent;  
169 - border: none;  
170 -}  
171 -  
172 -.cancel-btn {  
173 - margin-left: 24rpx;  
174 - padding: 10rpx 0;  
175 - height: 64rpx;  
176 - display: flex;  
177 - align-items: center;  
178 -}  
179 -  
180 -.cancel-text {  
181 - font-size: 28rpx;  
182 - color: #333;  
183 - /* 主题绿色,与你的 UI 一致 */  
184 -} 111 + }
185 112
186 -/* 搜索结果区域 */  
187 -.search-results {  
188 - margin-top: 100rpx; // 改成 100rpx,避免被搜索栏遮挡 113 + .search-results {
189 flex: 1; 114 flex: 1;
190 padding: 30rpx 20rpx; 115 padding: 30rpx 20rpx;
191 - overflow-y: auto;  
192 -}  
193 -  
194 -.empty-hint {  
195 - text-align: center;  
196 - color: #999;  
197 - font-size: 28rpx;  
198 - margin-top: 200rpx;  
199 -} 116 + box-sizing: border-box;
200 117
201 -.results-list { 118 + .results-list {
202 display: flex; 119 display: flex;
203 flex-direction: column; 120 flex-direction: column;
204 -}  
205 121
206 -.result-item { 122 + .result-item {
207 display: flex; 123 display: flex;
208 background: #ffffff; 124 background: #ffffff;
209 border-radius: 16rpx; 125 border-radius: 16rpx;
@@ -211,24 +127,22 @@ const goToDetail = (plan) => { @@ -211,24 +127,22 @@ const goToDetail = (plan) => {
211 overflow: hidden; 127 overflow: hidden;
212 padding: 20rpx; 128 padding: 20rpx;
213 box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05); 129 box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
214 -}  
215 130
216 -.result-cover { 131 + .result-cover {
217 width: 160rpx; 132 width: 160rpx;
218 height: 160rpx; 133 height: 160rpx;
219 border-radius: 12rpx; 134 border-radius: 12rpx;
220 margin-right: 20rpx; 135 margin-right: 20rpx;
221 flex-shrink: 0; 136 flex-shrink: 0;
222 -} 137 + }
223 138
224 -.result-info { 139 + .result-info {
225 flex: 1; 140 flex: 1;
226 display: flex; 141 display: flex;
227 flex-direction: column; 142 flex-direction: column;
228 justify-content: center; 143 justify-content: center;
229 -}  
230 144
231 -.result-title { 145 + .result-title {
232 font-size: 28rpx; 146 font-size: 28rpx;
233 color: #333; 147 color: #333;
234 font-weight: bold; 148 font-weight: bold;
@@ -237,11 +151,16 @@ const goToDetail = (plan) => { @@ -237,11 +151,16 @@ const goToDetail = (plan) => {
237 -webkit-line-clamp: 2; 151 -webkit-line-clamp: 2;
238 -webkit-box-orient: vertical; 152 -webkit-box-orient: vertical;
239 overflow: hidden; 153 overflow: hidden;
240 -} 154 + }
241 155
242 -.result-meta { 156 + .result-meta {
243 font-size: 24rpx; 157 font-size: 24rpx;
244 color: #999; 158 color: #999;
245 margin-top: 10rpx; 159 margin-top: 10rpx;
  160 + }
  161 + }
  162 + }
  163 + }
  164 + }
246 } 165 }
247 </style> 166 </style>
1 <template> 1 <template>
2 <view class="profile-page"> 2 <view class="profile-page">
3 <view class="avatar-section"> 3 <view class="avatar-section">
  4 + <!-- #ifdef H5 -->
  5 + <button class="avatar-edit-btn" @click="handleH5ChooseAvatar">
  6 + <image :src="formData.avatar ||
  7 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
  8 + " mode="aspectFill" class="avatar-image"></image>
  9 + </button>
  10 + <!-- #endif -->
  11 + <!-- #ifdef MP-WEIXIN -->
4 <button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar"> 12 <button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar">
5 <image :src="formData.avatar || 13 <image :src="formData.avatar ||
6 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png' 14 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
7 " mode="aspectFill" class="avatar-image"></image> 15 " mode="aspectFill" class="avatar-image"></image>
8 </button> 16 </button>
  17 + <!-- #endif -->
9 </view> 18 </view>
10 19
11 <view class="info-list"> 20 <view class="info-list">
12 <view class="list-item" @click="gotoEditNickname"> 21 <view class="list-item" @click="gotoEditNickname">
13 <text class="item-title">昵称</text> 22 <text class="item-title">昵称</text>
14 - <view class="item-value">{{ formData.nickname || '未设置' }}</view> 23 + <view class="item-value">{{ formData.nickname || '-' }}</view>
15 <uni-icons type="right" color="#999" size="20" class="arrow-icon"></uni-icons> 24 <uni-icons type="right" color="#999" size="20" class="arrow-icon"></uni-icons>
16 </view> 25 </view>
17 26
@@ -204,9 +213,6 @@ const handleChooseAvatar = async (e) => { @@ -204,9 +213,6 @@ const handleChooseAvatar = async (e) => {
204 try { 213 try {
205 const res = await FileApi.uploadFile(avatarUrl); 214 const res = await FileApi.uploadFile(avatarUrl);
206 215
207 - console.log('res----------', res);  
208 -  
209 -  
210 formData.value.avatar = res.data; 216 formData.value.avatar = res.data;
211 217
212 } catch (err) { 218 } catch (err) {
@@ -215,6 +221,43 @@ const handleChooseAvatar = async (e) => { @@ -215,6 +221,43 @@ const handleChooseAvatar = async (e) => {
215 } 221 }
216 }; 222 };
217 223
  224 +// #ifdef H5
  225 +
  226 +/** H5 端选择头像上传 */
  227 +
  228 +const handleH5ChooseAvatar = () => {
  229 + uni.chooseImage({
  230 + count: 1,
  231 + sizeType: ['compressed'],
  232 + sourceType: ['album', 'camera'],
  233 + success: async (chooseRes) => {
  234 + const tempFilePath = chooseRes.tempFilePaths[0];
  235 + if (!tempFilePath) return;
  236 +
  237 + // 文件大小校验
  238 + uni.getFileInfo({
  239 + filePath: tempFilePath,
  240 + success: async (fileInfo) => {
  241 + if (fileInfo.size / 1024 / 1024 > 5) {
  242 + uni.showToast({ title: '图片大小不能超过5MB', icon: 'none' });
  243 + return;
  244 + }
  245 + try {
  246 + const res = await FileApi.uploadFile(tempFilePath);
  247 + formData.value.avatar = res.data;
  248 + uni.showToast({ title: '上传成功', icon: 'success' });
  249 + } catch (err) {
  250 + console.error('[H5] 头像上传失败:', err);
  251 + uni.showToast({ title: '上传失败', icon: 'none' });
  252 + }
  253 + },
  254 + });
  255 + },
  256 + });
  257 +};
  258 +
  259 +// #endif
  260 +
218 /** 261 /**
219 * 唤起并初始化昵称弹窗缓存 262 * 唤起并初始化昵称弹窗缓存
220 */ 263 */
@@ -434,15 +477,17 @@ const saveProfile = async () => { @@ -434,15 +477,17 @@ const saveProfile = async () => {
434 477
435 await UserApi.updateUser(payload); 478 await UserApi.updateUser(payload);
436 479
437 - // 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步  
438 if (userStore.getUserInfo) { 480 if (userStore.getUserInfo) {
439 await userStore.getUserInfo(); 481 await userStore.getUserInfo();
440 } 482 }
441 483
  484 + // 同步表单数据,避免 navigateBack 时 onBackPress 误判为"未保存"
  485 + formData.value = JSON.parse(JSON.stringify(userStore.userInfo));
  486 +
442 uni.showToast({ title: '保存成功', icon: 'success' }); 487 uni.showToast({ title: '保存成功', icon: 'success' });
443 488
444 setTimeout(() => { 489 setTimeout(() => {
445 - uni.navigateBack({ delta: 1 }); 490 + uni.navigateBack();
446 }, 1200); 491 }, 1200);
447 } catch (err) { 492 } catch (err) {
448 console.error('[API Error] 保存个人信息更新请求失败:', err); 493 console.error('[API Error] 保存个人信息更新请求失败:', err);
@@ -4,11 +4,7 @@ const { version } = packageInfo; @@ -4,11 +4,7 @@ const { version } = packageInfo;
4 4
5 // 开发环境配置 5 // 开发环境配置
6 export let baseUrl; 6 export let baseUrl;
7 -if (process.env.NODE_ENV === 'development') {  
8 - baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL;  
9 -} else {  
10 - baseUrl = import.meta.env.SHOPRO_BASE_URL;  
11 -} 7 +baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL;
12 if (typeof baseUrl === 'undefined') { 8 if (typeof baseUrl === 'undefined') {
13 console.error('请检查.env配置文件是否存在'); 9 console.error('请检查.env配置文件是否存在');
14 } else { 10 } else {
@@ -20,7 +16,7 @@ export const staticUrl = import.meta.env.SHOPRO_STATIC_URL; @@ -20,7 +16,7 @@ export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
20 export const tenantId = import.meta.env.SHOPRO_TENANT_ID; 16 export const tenantId = import.meta.env.SHOPRO_TENANT_ID;
21 export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH; 17 export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH;
22 export const h5Url = import.meta.env.SHOPRO_H5_URL; 18 export const h5Url = import.meta.env.SHOPRO_H5_URL;
23 - 19 +export const defaultAvatar = import.meta.env.DEFAULT_AVATAR;
24 export default { 20 export default {
25 baseUrl, 21 baseUrl,
26 apiPath, 22 apiPath,
@@ -28,4 +24,5 @@ export default { @@ -28,4 +24,5 @@ export default {
28 tenantId, 24 tenantId,
29 websocketPath, 25 websocketPath,
30 h5Url, 26 h5Url,
  27 + defaultAvatar,
31 }; 28 };