Authored by Bad

修复bug

Too many changes to show.

To preserve performance only 10 of 10+ files are displayed.

@@ -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 {
@@ -140,7 +138,7 @@ @@ -140,7 +138,7 @@
140 "navigationBarTitleText": "" 138 "navigationBarTitleText": ""
141 } 139 }
142 }, 140 },
143 - { 141 + {
144 "path": "pages/xunji/xunji-wode-jihua", 142 "path": "pages/xunji/xunji-wode-jihua",
145 "style": { 143 "style": {
146 "navigationBarTitleText": "" 144 "navigationBarTitleText": ""
@@ -158,14 +156,14 @@ @@ -158,14 +156,14 @@
158 "navigationBarTitleText": "登录", 156 "navigationBarTitleText": "登录",
159 "navigationStyle": "default" 157 "navigationStyle": "default"
160 } 158 }
161 - },  
162 - {  
163 - "path": "pages/index/reset-password",  
164 - "style": {  
165 - "navigationBarTitleText": "找回密码", 159 + },
  160 + {
  161 + "path": "pages/index/reset-password",
  162 + "style": {
  163 + "navigationBarTitleText": "找回密码",
166 "navigationStyle": "default" 164 "navigationStyle": "default"
167 - }  
168 - } 165 + }
  166 + }
169 ] 167 ]
170 }, 168 },
171 { 169 {
@@ -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": {
@@ -210,7 +207,7 @@ @@ -210,7 +207,7 @@
210 "navigationBarTitleText": "设置", 207 "navigationBarTitleText": "设置",
211 "navigationStyle": "default" 208 "navigationStyle": "default"
212 } 209 }
213 - } 210 + }
214 ] 211 ]
215 } 212 }
216 ], 213 ],
@@ -237,4 +234,4 @@ @@ -237,4 +234,4 @@
237 } 234 }
238 ] 235 ]
239 } 236 }
240 -} 237 +}
@@ -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
335 - .title {  
336 - font-size: 32rpx;  
337 - font-weight: bold;  
338 - color: #333;  
339 - } 319 + .guide-txt {
  320 + .title {
  321 + font-size: $font-title;
  322 + font-weight: 600;
  323 + color: $color-text-primary;
  324 + display: block;
  325 + }
340 326
341 - .desc {  
342 - font-size: 22rpx;  
343 - color: #999;  
344 - margin-top: 4rpx;  
345 - display: block; 327 + .desc {
  328 + font-size: $font-small;
  329 + color: $color-text-tertiary;
  330 + margin-top: 6rpx;
  331 + display: block;
  332 + }
346 } 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;  
570 -  
571 - &:last-child {  
572 - border-bottom: none;  
573 - } 453 + padding: 28rpx 0;
  454 + border-bottom: 1rpx solid $color-border;
574 455
575 - .setting-text {  
576 - font-size: 28rpx;  
577 - color: #444; 456 + .settings-label {
  457 + font-size: $font-body;
  458 + color: $color-text-primary;
578 } 459 }
579 } 460 }
580 461
581 - .opacity-hover {  
582 - opacity: 0.7; 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;
  478 + }
  479 +
  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">
@@ -194,7 +184,7 @@ @@ -194,7 +184,7 @@
194 <!-- <view class="difficulty">困难</view> --> 184 <!-- <view class="difficulty">困难</view> -->
195 <view class="difficulty">{{ 185 <view class="difficulty">{{
196 item.weight ? item.weight + 'kg' : '无负重' 186 item.weight ? item.weight + 'kg' : '无负重'
197 - }}</view> 187 + }}</view>
198 </view> 188 </view>
199 <view class="group-chips"> 189 <view class="group-chips">
200 <view class="chip" v-for="(set, index) in item.setConfigList" :key="index"> 190 <view class="chip" v-for="(set, index) in item.setConfigList" :key="index">
@@ -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'  
248 - 238 +import { useTrainingStore } from '@/sheep/store/trainingStore';
249 239
250 -const trainingStore = useTrainingStore() 240 +const trainingStore = useTrainingStore();
  241 +const lostImage = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
251 242
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
@@ -343,7 +313,7 @@ const startTraining = () => { @@ -343,7 +313,7 @@ const startTraining = () => {
343 title: '当前已有正在进行的训练', 313 title: '当前已有正在进行的训练',
344 icon: 'none' 314 icon: 'none'
345 }); 315 });
346 - return; 316 + return;
347 } 317 }
348 318
349 console.log('动作详情页面的trainingStore.isTraining', trainingStore.isTraining); 319 console.log('动作详情页面的trainingStore.isTraining', trainingStore.isTraining);
@@ -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 = ''" />
  8 + </view>
  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 />
  13 + </view>
  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>
15 </view> 20 </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> 21 + <view class="menu-item" @click="addnewsupersets">
  22 + <up-icon name="attach" size="18" color="#3b82f6" />
  23 + <text class="menu-text">新增超级组</text>
19 </view> 24 </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> 25 + <view class="menu-item" @click="actionmanagement">
  26 + <up-icon name="list-dot" size="18" color="#f59e0b" />
  27 + <text class="menu-text">动作目录管理</text>
23 </view> 28 </view>
24 </view> 29 </view>
  30 +
  31 + <view class="menu-mask" v-if="addShow" @click="addShow = false" />
25 </view> 32 </view>
26 </view> 33 </view>
27 34
28 - <!-- 浮动菜单 -->  
29 -  
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)">  
48 - {{ item.name }} 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)">
  60 + {{ item.name }}
  61 + </view>
49 </view> 62 </view>
50 - </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>  
63 - </view>  
64 - </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>
65 </view> 73 </view>
66 74
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> 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 }}次
  81 + </view>
  82 + </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>
  107 + </view>
86 </view> 108 </view>
87 - </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%; 286 + height: 87vh;
  287 + // #ifdef H5
  288 + height: calc(100vh - 44px);
  289 + // #endif
  290 +
  291 + background-color: #ffffff;
  292 + overflow: hidden;
  293 +
  294 + /* 顶部搜索操作栏 */
  295 + .top-bar {
287 display: flex; 296 display: flex;
288 align-items: center; 297 align-items: center;
289 - justify-content: flex-end;  
290 - padding: 20rpx;  
291 - // background-color: #f5f5f5;  
292 - border-radius: 12rpx;  
293 - margin-top: 20rpx;  
294 -  
295 - /* #ifdef H5 */  
296 - margin-top: 20px;  
297 - /* #endif */  
298 -  
299 - .search {  
300 - display: flex;  
301 - align-items: center;  
302 - padding: 10rpx 20rpx;  
303 - flex: 1;  
304 - border-radius: 30rpx;  
305 - background-color: #f5f5f5;  
306 -  
307 - .search-icon {  
308 - width: 30rpx;  
309 - height: 30rpx;  
310 - } 298 + padding: 16rpx 24rpx;
  299 + background-color: #ffffff;
  300 + border-bottom: 1rpx solid #f3f4f6;
  301 + z-index: 50;
311 302
312 - .search-input {  
313 - flex: 1;  
314 - padding-left: 15rpx;  
315 - font-size: 26rpx;  
316 - color: #333;  
317 - border: none;  
318 - outline: none;  
319 - background: transparent;  
320 - } 303 + .search-wrapper {
  304 + flex: 1;
  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;  
329 - display: flex;  
330 - justify-content: center;  
331 - align-items: center;  
332 - z-index: 99; 308 + .add-btn-wrapper {
333 position: relative; 309 position: relative;
334 310
335 - .plus-icon {  
336 - width: 40rpx;  
337 - height: 40rpx; 311 + .add-icon-btn {
  312 + width: 72rpx;
  313 + height: 72rpx;
  314 + border-radius: 50%;
  315 + background-color: #f3f4f6;
  316 + display: flex;
  317 + justify-content: center;
  318 + align-items: center;
  319 + transition: background-color 0.2s;
  320 +
  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;  
364 - }  
365 -  
366 - &:last-child {  
367 - border-bottom: none; 351 + color: #1f2937;
  352 + font-weight: 500;
368 } 353 }
369 } 354 }
370 } 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;
  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;  
389 -  
390 - // #endif 375 + width: 180rpx;
  376 + height: 100%;
  377 + background-color: #f9fafb;
  378 + border-right: 1rpx solid #f3f4f6;
  379 +
391 .nav-item { 380 .nav-item {
392 - padding: 24rpx 0;  
393 - text-align: center;  
394 - font-size: 26rpx;  
395 - color: #333;  
396 - } 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 {
  390 + font-size: 26rpx;
  391 + color: #4b5563;
  392 + font-weight: 400;
  393 + text-align: center;
  394 + line-height: 1.3;
  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;  
425 -  
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;  
433 -  
434 - &.active {  
435 - background-color: #d8ede0;  
436 - color: #43b05e; 423 + background-color: #efefef;
  424 + padding: 20rpx 24rpx 200rpx;
  425 + box-sizing: border-box;
  426 +
  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;
  446 +
  447 + &.active {
  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 {  
447 - font-weight: 600;  
448 - padding-left: 20rpx;  
449 - font-size: 30rpx;  
450 - } 462 + .equipment-group {
  463 + .equipment-title {
  464 + display: flex;
  465 + align-items: center;
  466 + gap: 12rpx;
  467 + font-size: 28rpx;
  468 + font-weight: 600;
  469 + color: #111827;
  470 + margin-bottom: 20rpx;
  471 +
  472 + .title-bar {
  473 + width: 8rpx;
  474 + height: 28rpx;
  475 + background-color: #10b981;
  476 + border-radius: 4rpx;
451 477
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;  
498 - margin-bottom: 20rpx;  
499 -  
500 -  
501 - &:last-child {  
502 - margin-bottom: 50rpx; 478 + &.blue {
  479 + background-color: #3b82f6;
503 } 480 }
504 } 481 }
  482 + }
505 483
506 - .action-list {  
507 - display: grid;  
508 - grid-template-columns: repeat(2, 1fr);  
509 - grid-gap: 10rpx; 484 + .action-grid {
  485 + display: grid;
  486 + grid-template-columns: repeat(2, 1fr);
  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;
  495 + display: flex;
  496 + flex-direction: column;
  497 + transition: transform 0.15s;
  498 +
  499 + &:active {
  500 + transform: scale(0.98);
  501 + }
510 502
511 - .action-item {  
512 - display: flex;  
513 - 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; 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 }
  526 + }
536 527
537 - &:last-child {  
538 - margin-bottom: 50rpx;  
539 - padding-bottom: env(safe-area-inset-bottom);  
540 - } 528 + .action-info {
  529 + padding: 16rpx 12rpx;
  530 + display: flex;
  531 + align-items: center;
  532 + justify-content: center;
541 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%;
  543 + }
542 } 544 }
543 } 545 }
544 } 546 }
545 } 547 }
546 548
547 - .supers-name {  
548 - color: #000;  
549 - font-size: 32rpx;  
550 - margin-bottom: 20rpx;  
551 - }  
552 -  
553 - .super {  
554 - display: flex;  
555 - align-items: center;  
556 - background: #fff;  
557 - padding: 20rpx;  
558 - box-sizing: border-box;  
559 - border-radius: 10rpx;  
560 - gap: 20rpx;  
561 - font-size: 26rpx;  
562 - margin-bottom: 20rpx;  
563 -  
564 - .super-img {  
565 - width: 120rpx;  
566 - height: 120rpx;  
567 - border-radius: 10rpx;  
568 - } 549 + /* 超级组卡片样式 */
  550 + .supers-group {
  551 + .super-card {
  552 + display: flex;
  553 + align-items: center;
  554 + background-color: #ffffff;
  555 + border-radius: 16rpx;
  556 + border: 1rpx solid #f3f4f6;
  557 + padding: 20rpx;
  558 + margin-bottom: 16rpx;
  559 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
  560 + gap: 20rpx;
  561 +
  562 + &:active {
  563 + background-color: #f9fafb;
  564 + }
569 565
570 - .super-name {  
571 - margin-bottom: 10rpx;  
572 - }  
573 - } 566 + .super-img {
  567 + width: 100rpx;
  568 + height: 100rpx;
  569 + border-radius: 12rpx;
  570 + background-color: #f3f4f6;
  571 + flex-shrink: 0;
  572 + }
574 573
575 - .add-super {  
576 - background: #fff;  
577 - padding: 20rpx;  
578 - box-sizing: border-box;  
579 - border-radius: 10rpx; 574 + .super-info {
  575 + flex: 1;
  576 + display: flex;
  577 + flex-direction: column;
  578 + gap: 8rpx;
580 579
581 - .header {  
582 - display: flex;  
583 - gap: 10rpx;  
584 - align-items: center;  
585 - margin-bottom: 20rpx;  
586 - } 580 + .super-name {
  581 + font-size: 28rpx;
  582 + font-weight: 600;
  583 + color: #111827;
  584 + }
587 585
588 - .tip {  
589 - margin: 0rpx;  
590 - padding: 0rpx;  
591 - font-size: 22rpx; 586 + .super-muscles {
  587 + font-size: 22rpx;
  588 + color: #6b7280;
  589 + }
  590 + }
592 } 591 }
593 } 592 }
594 - }  
595 593
596 - .nav-item .category-group {  
597 - /* 超级组特殊样式 */  
598 - background-color: #f0f9ff;  
599 - font-weight: bold;  
600 - border-top: 1px solid #eee; 594 + .empty-wrapper {
  595 + padding: 80rpx 0;
  596 + }
601 } 597 }
602 598
603 - .empty-state {  
604 - display: flex;  
605 - justify-content: center;  
606 - align-items: center;  
607 599
608 - .empty-text {  
609 - font-size: 28rpx;  
610 - color: #999;  
611 - }  
612 - }  
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>  
47 - </view>  
48 - </view>  
49 - </view>  
50 - </view>  
51 46
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(', ') || '无' }} 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>
  59 + </view>
  60 + <text class="desc" v-if="item.description">{{ item.description }}</text>
62 </view> 61 </view>
63 - <view class="template-desc" v-if="template.secondaryMuscles">  
64 - 次要训练部位:{{ template.secondaryMuscleNames.join(', ') || '无' }} 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>
  78 + </view>
  79 +
  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>
  84 + </view>
  85 + </view>
65 </view> 86 </view>
66 </view> 87 </view>
67 - </view>  
68 - </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>
  92 + </view>
  93 + </template>
73 </view> 94 </view>
74 - </view>  
75 - </scroll-view> 95 +
  96 + <!-- 底部安全区垫高 -->
  97 + <view class="bottom-spacer"></view>
  98 + </scroll-view>
  99 + </view>
76 </view> 100 </view>
77 </template> 101 </template>
78 102
@@ -80,326 +104,411 @@ @@ -80,326 +104,411 @@
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' 代表不限
105 131
106 -// 获取模板大类列表  
107 -const TemplatesList = async () => { 132 +// ====== API 数据加载 ======
  133 +
  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 {
  271 + display: flex;
  272 + flex-direction: column;
  273 + height: calc(100vh - 44px);
  274 + // #ifdef MP-WEIXIN
  275 + height: calc(100vh - 110px);
  276 + // #endif
235 width: 100vw; 277 width: 100vw;
236 - height: 100vh;  
237 - box-sizing: border-box; 278 + background-color: #f8fafc;
  279 + overflow: hidden;
238 280
  281 + /* 顶部筛选控制栏 */
239 .filter-section { 282 .filter-section {
240 - position: fixed;  
241 - // top: 80rpx;  
242 - // #ifdef MP-WEIXIN  
243 - top: 234rpx;  
244 - // #endif  
245 -  
246 - /* #ifdef H5 */  
247 - top: 77px;  
248 - /* #endif */  
249 -  
250 - left: 0;  
251 - right: 0;  
252 - z-index: 999;  
253 - 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;  
259 -  
260 - .filter-btn {  
261 - display: flex;  
262 - 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;  
274 - }  
275 -  
276 - .filter-btn .text {  
277 - margin-right: 8rpx;  
278 - }  
279 -  
280 - .filter-btn.active {  
281 - border-color: #43b05e;  
282 - background: #e6f7f0;  
283 - color: #43b05e;  
284 - } 287 + gap: 20rpx;
  288 + padding: 20rpx 30rpx;
  289 + background-color: #ffffff;
  290 + border-bottom: 1rpx solid #f1f5f9;
285 291
286 .filter-wrapper { 292 .filter-wrapper {
287 position: relative; 293 position: relative;
288 - z-index: 10;  
289 - }  
290 294
291 - /* 下拉菜单样式 */  
292 - .dropdown-menu {  
293 - position: absolute;  
294 - top: calc(100% + 8rpx);  
295 - 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;  
301 - min-width: 150rpx;  
302 - max-height: 320rpx;  
303 - overflow-y: auto;  
304 - border: 1rpx solid #f0f0f0;  
305 - }  
306 -  
307 - /* 下拉菜单项样式 */  
308 - .dropdown-item {  
309 - padding: 16rpx 24rpx;  
310 - font-size: 26rpx;  
311 - color: #333;  
312 - white-space: nowrap;  
313 - } 295 + .filter-btn {
  296 + display: flex;
  297 + align-items: center;
  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;
  310 + }
  311 +
  312 + &.active,
  313 + &.selected {
  314 + background-color: #ecfdf5;
  315 + border-color: #10b981;
  316 +
  317 + .text {
  318 + color: #10b981;
  319 + font-weight: 600;
  320 + }
  321 + }
  322 + }
314 323
315 - /* 下拉菜单项悬停样式 */  
316 - .dropdown-item:hover {  
317 - background-color: #43b05e; 324 + /* 下拉浮层 */
  325 + .dropdown-menu {
  326 + position: absolute;
  327 + top: 72rpx;
  328 + left: 0;
  329 + z-index: 100;
  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;
  337 + overflow-y: auto;
  338 +
  339 + .dropdown-item {
  340 + display: flex;
  341 + align-items: center;
  342 + justify-content: space-between;
  343 + padding: 20rpx 28rpx;
  344 + font-size: 26rpx;
  345 + color: #334155;
  346 + transition: background-color 0.15s;
  347 +
  348 + &:active {
  349 + background-color: #f8fafc;
  350 + }
  351 +
  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  
340 -  
341 - /* #ifdef H5 */  
342 - margin-top: 87px;  
343 - // 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px  
344 - height: calc(100vh - 77px - 87px);  
345 - /* #endif */  
346 -  
347 - .sub-template-list {  
348 - display: flex;  
349 - flex-direction: column;  
350 - gap: 10rpx;  
351 - }  
352 -  
353 - .template-item {  
354 - 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);  
359 - padding: 20rpx;  
360 -  
361 - &:first-child {  
362 - margin-top: 24rpx; 376 + height: 0;
  377 + /* 关键:接触 Flex 内容支撑限制 */
  378 + min-height: 0;
  379 + overflow: hidden;
  380 +
  381 + .template-scroll {
  382 + width: 100%;
  383 + height: 100%;
  384 +
  385 + .scroll-inner {
  386 + padding: 24rpx 30rpx;
  387 + display: flex;
  388 + flex-direction: column;
  389 + gap: 20rpx;
  390 +
  391 + /* 模板卡片组件样式 */
  392 + .template-card {
  393 + display: flex;
  394 + align-items: center;
  395 + background-color: #ffffff;
  396 + border-radius: 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;
  401 +
  402 + &:active {
  403 + transform: scale(0.985);
  404 + background-color: #f8fafc;
  405 + }
  406 +
  407 + .cover-img {
  408 + width: 128rpx;
  409 + height: 128rpx;
  410 + border-radius: 16rpx;
  411 + margin-right: 20rpx;
  412 + background-color: #f1f5f9;
  413 + flex-shrink: 0;
  414 + }
  415 +
  416 + .card-content {
  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;
  438 + }
  439 +
  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 + }
  449 + }
  450 +
  451 + .title {
  452 + font-size: 30rpx;
  453 + font-weight: 600;
  454 + color: #0f172a;
  455 + margin-bottom: 8rpx;
  456 + }
  457 +
  458 + .desc {
  459 + font-size: 24rpx;
  460 + color: #64748b;
  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;
  491 + }
  492 +
  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 + }
363 } 505 }
364 506
365 -  
366 - .template-img {  
367 - width: 120rpx;  
368 - height: 120rpx;  
369 - border-radius: 12rpx;  
370 - margin-right: 20rpx;  
371 - }  
372 -  
373 - .template-content {  
374 - flex: 1;  
375 - }  
376 -  
377 - .template-title {  
378 - font-size: 32rpx;  
379 - color: #333;  
380 - margin-bottom: 10rpx;  
381 - }  
382 -  
383 - .template-count {  
384 - font-size: 24rpx;  
385 - color: #666;  
386 - margin-bottom: 10rpx;  
387 - }  
388 -  
389 - .template-desc {  
390 - font-size: 24rpx;  
391 - color: #666;  
392 - line-height: 1.4;  
393 - }  
394 -  
395 - &:last-child {  
396 - margin-bottom: 5rpx;  
397 -  
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 }
404 } 513 }
405 -</style> 514 +</style>
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>  
8 - </view> 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>
  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>
70 - <text class="item-label">容量</text>  
71 - <text class="item-desc">力量训练的总重量</text> 86 + <view class="item-info">
  87 + <text class="item-label">容量</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>
75 - <text class="item-label">课程</text>  
76 - <text class="item-desc">训记或已结课的线下课程</text> 94 + <view class="item-info">
  95 + <text class="item-label">课程</text>
  96 + <text class="item-desc">训记完成或已结课的线下课程</text>
  97 + </view>
77 </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>
  105 + </view>
82 </view> 106 </view>
83 </view> 107 </view>
84 </view> 108 </view>
85 - </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';
  158 + }
  159 +
  160 + // 4. 有计划但未指定背景色:赋予柔和浅黄
  161 + if (date.planList && date.planList.length > 0) {
  162 + return '#fefce8';
153 } 163 }
154 164
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; 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 -  
340 - .month-picker {  
341 - height: 40rpx;  
342 - border-radius: 50rpx;  
343 - overflow: hidden;  
344 - padding: 0 20rpx;  
345 - gap: 10rpx; 339 + margin-bottom: 24rpx;
346 340
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; 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;
400 } 355 }
401 -  
402 - // #endif  
403 } 356 }
404 - }  
405 357
406 - .action-buttons {  
407 - 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; 358 + .action-buttons {
  359 + display: flex;
  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,179 +386,182 @@ const selectDate = async (date) => { @@ -453,179 +386,182 @@ 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;  
468 - box-sizing: border-box;  
469 - }  
470 -  
471 - .plan-list {  
472 - width: 100%;  
473 - display: flex;  
474 - flex-direction: column;  
475 - gap: 6rpx;  
476 - margin-top: 10rpx;  
477 - padding: 0; 397 + padding: 8rpx 4rpx;
478 box-sizing: border-box; 398 box-sizing: border-box;
479 - } 399 + transition: all 0.2s ease;
480 400
481 - .weight,  
482 - .plan-extra,  
483 - .note-item,  
484 - .plan-name {  
485 - font-size: 18rpx;  
486 - background: #ffdd44;  
487 - padding: 4rpx 0;  
488 - text-align: center;  
489 - overflow: hidden;  
490 - white-space: nowrap;  
491 - text-overflow: ellipsis;  
492 - width: 100%;  
493 - box-sizing: border-box;  
494 - }  
495 -  
496 - .weight,  
497 - .weight-item {  
498 - background: #b8ff66;  
499 - }  
500 - }  
501 -} 401 + &:active {
  402 + filter: brightness(0.96);
  403 + }
502 404
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;  
515 -  
516 - .explain-popup {  
517 - width: 100%;  
518 - background-color: #ffffff;  
519 - border-radius: 24rpx 24rpx 0 0;  
520 - padding: 40rpx;  
521 - box-sizing: border-box; 405 + &.other-month {
  406 + opacity: 0.5;
  407 + border-color: #f1f5f9;
  408 + }
522 409
523 - .explain-header {  
524 - display: flex;  
525 - justify-content: space-between;  
526 - align-items: center;  
527 - margin-bottom: 40rpx; 410 + &.is-today {
  411 + border: 2rpx solid #3b82f6;
  412 + }
528 413
529 - .explain-title {  
530 - font-size: 32rpx;  
531 - font-weight: 60;  
532 - color: #333333; 414 + &.has-data {
  415 + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);
533 } 416 }
534 417
535 - .close-btn {  
536 - width: 48rpx;  
537 - height: 48rpx;  
538 - border-radius: 50%;  
539 - background-color: #f5f5f5; 418 + .date-header {
  419 + height: 36rpx;
540 display: flex; 420 display: flex;
541 - justify-content: center;  
542 align-items: center; 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;
543 436
544 - text {  
545 - font-size: 36rpx;  
546 - color: #666666;  
547 - line-height: 1;  
548 } 437 }
549 } 438 }
550 - }  
551 439
552 - .explain-list {  
553 - .explain-item { 440 + .plan-list {
  441 + width: 100%;
554 display: flex; 442 display: flex;
555 - align-items: center;  
556 - margin-bottom: 32rpx; 443 + flex-direction: column;
  444 + gap: 6rpx;
  445 +
  446 + .plan-tag {
  447 + width: 100%;
  448 + border-radius: 6rpx;
  449 + padding: 2rpx 4rpx;
  450 + box-sizing: border-box;
  451 + display: flex;
  452 + align-items: center;
  453 + justify-content: center;
  454 + background-color: rgba(255, 255, 255, 0.85);
  455 + /* 使 Tag 悬浮于彩色格子之上 */
  456 +
  457 + .tag-text {
  458 + font-size: 18rpx;
  459 + line-height: 22rpx;
  460 + color: #111827;
  461 + overflow: hidden;
  462 + white-space: nowrap;
  463 + text-overflow: ellipsis;
  464 + text-align: center;
  465 + width: 100%;
  466 + }
557 467
558 - .color-block {  
559 - width: 40rpx;  
560 - height: 40rpx;  
561 - border-radius: 8rpx;  
562 - margin-right: 20rpx; 468 + &.tag-type-weight {
  469 + background-color: #bbf7d0 !important;
563 470
564 - &.blue {  
565 - background-color: #a8d8ff; 471 + .tag-text {
  472 + color: #14532d;
  473 + font-weight: 700;
  474 + }
566 } 475 }
567 476
568 - &.green {  
569 - background-color: #b8ff66;  
570 - } 477 + &.tag-type-note {
  478 + background-color: #ffffff;
571 479
572 - &.yellow {  
573 - background-color: #ffdd44; 480 + .tag-text {
  481 + color: #374151;
  482 + }
574 } 483 }
  484 + }
575 485
576 - &.gray {  
577 - background-color: #f0f0f0;  
578 - border: 1rpx solid #eee;  
579 - } 486 + .plan-extra {
  487 + font-size: 16rpx;
  488 + color: #4b5563;
  489 + text-align: center;
  490 + line-height: 20rpx;
  491 + font-weight: 600;
580 } 492 }
  493 + }
  494 + }
  495 + }
  496 +}
  497 +
  498 +.explain-popup-content {
  499 + padding: 36rpx 32rpx 48rpx 32rpx;
  500 + background-color: #ffffff;
  501 +
  502 + .explain-header {
  503 + display: flex;
  504 + justify-content: space-between;
  505 + align-items: center;
  506 + margin-bottom: 32rpx;
  507 +
  508 + .explain-title {
  509 + font-size: 32rpx;
  510 + font-weight: 600;
  511 + color: #111827;
  512 + }
  513 + }
  514 +
  515 + .explain-list {
  516 + display: flex;
  517 + flex-direction: column;
  518 + gap: 28rpx;
  519 +
  520 + .explain-item {
  521 + display: flex;
  522 + align-items: center;
  523 +
  524 + .color-block {
  525 + width: 36rpx;
  526 + height: 36rpx;
  527 + border-radius: 8rpx;
  528 + margin-right: 24rpx;
  529 +
  530 + &.blue {
  531 + background-color: #bae6fd;
  532 + }
  533 +
  534 + &.green {
  535 + background-color: #bbf7d0;
  536 + }
  537 +
  538 + &.yellow {
  539 + background-color: #fef08a;
  540 + }
  541 +
  542 + &.gray {
  543 + background-color: #e5e7eb;
  544 + }
  545 + }
  546 +
  547 + .item-info {
  548 + display: flex;
  549 + align-items: center;
  550 + gap: 16rpx;
581 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; 560 + font-size: 26rpx;
  561 + color: #6b7280;
593 } 562 }
594 } 563 }
595 } 564 }
596 } 565 }
597 } 566 }
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 - }  
629 -}  
630 -</style>  
631 -<!-- #endif -->  
  567 +</style>