Showing
16 changed files
with
2076 additions
and
2683 deletions
| @@ -32,10 +32,14 @@ SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn | @@ -32,10 +32,14 @@ SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn | ||
| 32 | ### SHOPRO_STATIC_URL = https://file.sheepjs.com | 32 | ### SHOPRO_STATIC_URL = https://file.sheepjs.com |
| 33 | 33 | ||
| 34 | # 前端 H5 访问域名 | 34 | # 前端 H5 访问域名 |
| 35 | -SHOPRO_H5_URL=http://127.0.0.1:3000 | 35 | +SHOPRO_H5_URL=https://xunji.geaktec.com |
| 36 | 36 | ||
| 37 | # 是否开启直播 1 开启直播 | 0 关闭直播 | 37 | # 是否开启直播 1 开启直播 | 0 关闭直播 |
| 38 | SHOPRO_MPLIVE_ON=0 | 38 | SHOPRO_MPLIVE_ON=0 |
| 39 | 39 | ||
| 40 | # 租户ID 默认 1 | 40 | # 租户ID 默认 1 |
| 41 | SHOPRO_TENANT_ID=1 | 41 | SHOPRO_TENANT_ID=1 |
| 42 | + | ||
| 43 | + | ||
| 44 | +# 默认头像 | ||
| 45 | +DEFAULT_AVATAR=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png |
| @@ -6,6 +6,8 @@ | @@ -6,6 +6,8 @@ | ||
| 6 | :placeholder="false" | 6 | :placeholder="false" |
| 7 | activeColor="#000" | 7 | activeColor="#000" |
| 8 | :fixed="true" | 8 | :fixed="true" |
| 9 | + :bottom="0" | ||
| 10 | + :safeAreaInsetBottom="false" | ||
| 9 | > | 11 | > |
| 10 | <up-tabbar-item | 12 | <up-tabbar-item |
| 11 | v-for="(item, index) in tabList" | 13 | v-for="(item, index) in tabList" |
| 1 | { | 1 | { |
| 2 | "name": "智能健身", | 2 | "name": "智能健身", |
| 3 | - "appid": "__UNI__7CAA18B", | 3 | + "appid": "__UNI__8F24C84", |
| 4 | "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。", | 4 | "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。", |
| 5 | "versionName": "2025.10", | 5 | "versionName": "2025.10", |
| 6 | "versionCode": "183", | 6 | "versionCode": "183", |
| @@ -100,7 +100,6 @@ | @@ -100,7 +100,6 @@ | ||
| 100 | "backgroundColor": "#1a1a1a" | 100 | "backgroundColor": "#1a1a1a" |
| 101 | } | 101 | } |
| 102 | }, | 102 | }, |
| 103 | - | ||
| 104 | { | 103 | { |
| 105 | "path": "pages/xunji/xunji-dongzuo-lianxi", | 104 | "path": "pages/xunji/xunji-dongzuo-lianxi", |
| 106 | "style": { | 105 | "style": { |
| @@ -113,14 +112,12 @@ | @@ -113,14 +112,12 @@ | ||
| 113 | "navigationBarTitleText": "新增超级组" | 112 | "navigationBarTitleText": "新增超级组" |
| 114 | } | 113 | } |
| 115 | }, | 114 | }, |
| 116 | - | ||
| 117 | { | 115 | { |
| 118 | "path": "pages/xunji/wode-jihua-paike", | 116 | "path": "pages/xunji/wode-jihua-paike", |
| 119 | "style": { | 117 | "style": { |
| 120 | "navigationBarTitleText": "我的计划详情的排课设置" | 118 | "navigationBarTitleText": "我的计划详情的排课设置" |
| 121 | } | 119 | } |
| 122 | }, | 120 | }, |
| 123 | - | ||
| 124 | { | 121 | { |
| 125 | "path": "pages/xunji/dongzuo-muluguanli", | 122 | "path": "pages/xunji/dongzuo-muluguanli", |
| 126 | "style": { | 123 | "style": { |
| @@ -131,7 +128,8 @@ | @@ -131,7 +128,8 @@ | ||
| 131 | { | 128 | { |
| 132 | "path": "pages/xunji/jihua-search", | 129 | "path": "pages/xunji/jihua-search", |
| 133 | "style": { | 130 | "style": { |
| 134 | - "navigationBarTitleText": "训记-计划-搜索" | 131 | + "navigationBarTitleText": "计划-搜索", |
| 132 | + "navigationStyle": "default" | ||
| 135 | } | 133 | } |
| 136 | }, | 134 | }, |
| 137 | { | 135 | { |
| @@ -185,7 +183,6 @@ | @@ -185,7 +183,6 @@ | ||
| 185 | "navigationBarTitleText": "我的常见问题" | 183 | "navigationBarTitleText": "我的常见问题" |
| 186 | } | 184 | } |
| 187 | }, | 185 | }, |
| 188 | - | ||
| 189 | { | 186 | { |
| 190 | "path": "pages/user/wode-guanyu-hongxing", | 187 | "path": "pages/user/wode-guanyu-hongxing", |
| 191 | "style": { | 188 | "style": { |
| @@ -11,9 +11,7 @@ | @@ -11,9 +11,7 @@ | ||
| 11 | <text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text> | 11 | <text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text> |
| 12 | </view> | 12 | </view> |
| 13 | </view> | 13 | </view> |
| 14 | - <view class="right"> | ||
| 15 | - <uni-icons type="right" size="16" color="#000" /> | ||
| 16 | - </view> | 14 | + <uni-icons type="right" size="16" color="#999" /> |
| 17 | </view> | 15 | </view> |
| 18 | 16 | ||
| 19 | <!-- 未登录引导区 --> | 17 | <!-- 未登录引导区 --> |
| @@ -25,57 +23,8 @@ | @@ -25,57 +23,8 @@ | ||
| 25 | <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button> | 23 | <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button> |
| 26 | </view> | 24 | </view> |
| 27 | 25 | ||
| 28 | - <!-- --> | ||
| 29 | - <!-- <view class="vip-banner" hover-class="opacity-hover" @click="goAddVip"> | ||
| 30 | - <view class="vip-info"> | ||
| 31 | - <uni-icons type="vip-filled" size="22" color="#f1c40f" /> | ||
| 32 | - <text class="vip-text"> | ||
| 33 | - {{ userInfo.deposit === 1 ? '鸿星·尊享会员 | 已开通' : '鸿星·会员 | 开通只需押金¥199' }} | ||
| 34 | - </text> | ||
| 35 | - </view> | ||
| 36 | - <view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view> | ||
| 37 | - </view> --> | ||
| 38 | - | ||
| 39 | - <!-- 课程状态快速入口 --> | ||
| 40 | - <!-- <view class="section-card quick-entry"> | ||
| 41 | - <view v-for="entry in quickEntryConfig" :key="entry.type" class="entry-item" hover-class="opacity-hover" | ||
| 42 | - @click="handleQuickEntry(entry.type)"> | ||
| 43 | - <text class="num">{{ userInfo[entry.key] || 0 }}</text> | ||
| 44 | - <text class="label">{{ entry.label }}</text> | ||
| 45 | - </view> | ||
| 46 | - </view> --> | ||
| 47 | - | ||
| 48 | - <!-- 广告位 --> | ||
| 49 | - <!-- <view class="banner-box" @click="goJiamen"> | ||
| 50 | - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png" | ||
| 51 | - mode="aspectFill" class="banner-img" /> | ||
| 52 | - </view> --> | ||
| 53 | - | ||
| 54 | - <!-- 核心应用区 --> | ||
| 55 | - <!-- <view class="section-card apply-section"> | ||
| 56 | - <view v-for="(item, index) in APPLY_CONFIG_LIST" :key="index" class="apply-item" | ||
| 57 | - @click="authNavigateTo(item.url)"> | ||
| 58 | - <view class="icon-bg"> | ||
| 59 | - <image :src="item.icon" class="img" mode="aspectFit" /> | ||
| 60 | - </view> | ||
| 61 | - <text class="text">{{ item.text }}</text> | ||
| 62 | - </view> | ||
| 63 | - </view> --> | ||
| 64 | - | ||
| 65 | - <!-- 资产账户网格区 --> | ||
| 66 | - <!-- <view v-if="userStore.isLogin" class="section-card"> | ||
| 67 | - <view class="account-grid"> | ||
| 68 | - <view v-for="(acc, idx) in accountConfig" :key="idx" class="account-item" @click="authNavigateTo(acc.url)"> | ||
| 69 | - <text class="acc-lab">{{ acc.label }}</text> | ||
| 70 | - <text class="acc-val"> | ||
| 71 | - {{ formatAccountValue(userInfo[acc.key], acc.isFloat) }} | ||
| 72 | - <text v-if="acc.unit" class="unit">{{ acc.unit }}</text> | ||
| 73 | - </text> | ||
| 74 | - </view> | ||
| 75 | - </view> | ||
| 76 | - </view> --> | ||
| 77 | 26 | ||
| 78 | - <!-- 功能矩阵九宫格 --> | 27 | + <!-- 功能区 --> |
| 79 | <view class="section-card icon-grid-box"> | 28 | <view class="section-card icon-grid-box"> |
| 80 | <view class="icon-grid"> | 29 | <view class="icon-grid"> |
| 81 | <view v-for="(item, index) in FUNCTION_CONFIG_LIST" :key="index" class="icon-item" | 30 | <view v-for="(item, index) in FUNCTION_CONFIG_LIST" :key="index" class="icon-item" |
| @@ -95,15 +44,17 @@ | @@ -95,15 +44,17 @@ | ||
| 95 | </view> | 44 | </view> |
| 96 | 45 | ||
| 97 | <!-- 设置区 --> | 46 | <!-- 设置区 --> |
| 98 | - <view v-if="userStore.isLogin" class="section-card"> | ||
| 99 | - <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-shezhi')"> | ||
| 100 | - <text class="setting-text">个人设置</text> | 47 | + <view v-if="userStore.isLogin" class="section-card settings-section"> |
| 48 | + <view class="settings-title">个人设置</view> | ||
| 49 | + | ||
| 50 | + <view class="settings-item" @click="goToForgetPassword"> | ||
| 51 | + <text class="settings-label">修改密码</text> | ||
| 101 | <uni-icons type="right" size="14" color="#E0E0E0" /> | 52 | <uni-icons type="right" size="14" color="#E0E0E0" /> |
| 102 | </view> | 53 | </view> |
| 103 | - <!-- <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')"> | ||
| 104 | - <text class="setting-text">隐私中心</text> | ||
| 105 | - <uni-icons type="right" size="14" color="#E0E0E0" /> | ||
| 106 | - </view> --> | 54 | + |
| 55 | + <view class="logout-wrap"> | ||
| 56 | + <button class="logout-btn" @click="handleLogout">退出登录</button> | ||
| 57 | + </view> | ||
| 107 | </view> | 58 | </view> |
| 108 | 59 | ||
| 109 | <Tabbar /> | 60 | <Tabbar /> |
| @@ -113,21 +64,23 @@ | @@ -113,21 +64,23 @@ | ||
| 113 | 64 | ||
| 114 | <script setup> | 65 | <script setup> |
| 115 | import { ref } from 'vue'; | 66 | import { ref } from 'vue'; |
| 67 | +import { onShow } from '@dcloudio/uni-app'; | ||
| 116 | import UserApi from '@/sheep/api/member/user'; | 68 | import UserApi from '@/sheep/api/member/user'; |
| 117 | import MemberApi from '@/sheep/api/member/member'; | 69 | import MemberApi from '@/sheep/api/member/member'; |
| 70 | +import AuthUtil from '@/sheep/api/member/auth'; | ||
| 118 | import useUserStore from '@/sheep/store/user'; | 71 | import useUserStore from '@/sheep/store/user'; |
| 119 | -import { onShow } from '@dcloudio/uni-app'; | ||
| 120 | -import TrainingFloating from '@/pages/TrainingFloating.vue' | ||
| 121 | import { useTrainingStore } from '@/sheep/store/trainingStore'; | 72 | import { useTrainingStore } from '@/sheep/store/trainingStore'; |
| 73 | +import TrainingFloating from '@/pages/TrainingFloating.vue'; | ||
| 74 | + | ||
| 122 | const trainingStore = useTrainingStore(); | 75 | const trainingStore = useTrainingStore(); |
| 123 | 76 | ||
| 124 | 77 | ||
| 125 | -// 响应式数据挂载 | 78 | +// 状态 |
| 126 | const userStore = useUserStore(); | 79 | const userStore = useUserStore(); |
| 127 | const userInfo = ref({}); | 80 | const userInfo = ref({}); |
| 128 | const memberLevelName = ref(''); | 81 | const memberLevelName = ref(''); |
| 129 | 82 | ||
| 130 | -// 固定的 UI 配置 | 83 | +// 配置 |
| 131 | const APPLY_CONFIG_LIST = []; | 84 | const APPLY_CONFIG_LIST = []; |
| 132 | 85 | ||
| 133 | const FUNCTION_CONFIG_LIST = [ | 86 | const FUNCTION_CONFIG_LIST = [ |
| @@ -155,31 +108,23 @@ const FUNCTION_CONFIG_LIST = [ | @@ -155,31 +108,23 @@ const FUNCTION_CONFIG_LIST = [ | ||
| 155 | ]; | 108 | ]; |
| 156 | 109 | ||
| 157 | 110 | ||
| 158 | -// 课程计数状态配置映射 | ||
| 159 | const quickEntryConfig = [ | 111 | const quickEntryConfig = [ |
| 160 | { type: 1, key: 'courseNum', label: '待上课' }, | 112 | { type: 1, key: 'courseNum', label: '待上课' }, |
| 161 | { type: 2, key: 'courseWaitNum', label: '等待中' }, | 113 | { type: 2, key: 'courseWaitNum', label: '等待中' }, |
| 162 | { type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' }, | 114 | { type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' }, |
| 163 | ]; | 115 | ]; |
| 164 | 116 | ||
| 165 | -// 资产账户字段清洗配置映射 (对应接口数据类型:number 与 integer) | 117 | +// 资产账户 |
| 118 | + | ||
| 166 | const accountConfig = []; | 119 | const accountConfig = []; |
| 167 | 120 | ||
| 168 | -/** | ||
| 169 | - * JSDoc 核心资产数值洗涤函数 | ||
| 170 | - * @param {number|undefined} val 原始金钱/数量值 | ||
| 171 | - * @param {boolean} isFloat 是否需要保留两位小数 | ||
| 172 | - * @returns {string|number} 格式化后的安全渲染字符串 | ||
| 173 | - */ | ||
| 174 | const formatAccountValue = (val, isFloat) => { | 121 | const formatAccountValue = (val, isFloat) => { |
| 175 | if (val === undefined || val === null) return isFloat ? '0.00' : 0; | 122 | if (val === undefined || val === null) return isFloat ? '0.00' : 0; |
| 176 | return isFloat ? Number(val).toFixed(2) : Math.floor(val); | 123 | return isFloat ? Number(val).toFixed(2) : Math.floor(val); |
| 177 | }; | 124 | }; |
| 178 | 125 | ||
| 179 | -/** | ||
| 180 | - * 路由守卫拦截转发 | ||
| 181 | - * @param {string} url 目标绝对/相对地址 | ||
| 182 | - */ | 126 | +/** 路由守卫 */ |
| 127 | + | ||
| 183 | const authNavigateTo = (url) => { | 128 | const authNavigateTo = (url) => { |
| 184 | if (!userStore.isLogin) { | 129 | if (!userStore.isLogin) { |
| 185 | goLogin(); | 130 | goLogin(); |
| @@ -191,51 +136,26 @@ const authNavigateTo = (url) => { | @@ -191,51 +136,26 @@ const authNavigateTo = (url) => { | ||
| 191 | }); | 136 | }); |
| 192 | }; | 137 | }; |
| 193 | 138 | ||
| 194 | -/** | ||
| 195 | - * 统一处理功能网格的点击分发 | ||
| 196 | - * @param {Object} item 节点配置项 | ||
| 197 | - */ | 139 | +/** 功能网格点击分发 */ |
| 140 | + | ||
| 198 | const handleGridItemClick = (item) => { | 141 | const handleGridItemClick = (item) => { |
| 199 | if (item.text === '联系客服') { | 142 | if (item.text === '联系客服') { |
| 200 | // #ifndef MP-WEIXIN | 143 | // #ifndef MP-WEIXIN |
| 201 | - // 兜底非微信小程序平台(如H5、App),可以正常走原来的普通客服页面路由 | ||
| 202 | authNavigateTo(item.url); | 144 | authNavigateTo(item.url); |
| 203 | // #endif | 145 | // #endif |
| 204 | return; | 146 | return; |
| 205 | } | 147 | } |
| 206 | - | ||
| 207 | - // 其他正常功能,正常走路由拦截守卫 | ||
| 208 | authNavigateTo(item.url); | 148 | authNavigateTo(item.url); |
| 209 | }; | 149 | }; |
| 210 | 150 | ||
| 211 | -/** | ||
| 212 | - * 异步高内聚合并请求 | ||
| 213 | - * 解决因先后触发 setData 导致微信小程序底层 AppService 与 WebView 之间高频拥堵卡顿的问题 | ||
| 214 | - */ | 151 | +/** 拉取用户信息 */ |
| 152 | + | ||
| 215 | const fetchPageData = async () => { | 153 | const fetchPageData = async () => { |
| 216 | try { | 154 | try { |
| 217 | - // const [userRes, levelRes] = await Promise.all([ | ||
| 218 | - // UserApi.getUserInfo(), | ||
| 219 | - // // MemberApi.getMemberLevel(), | ||
| 220 | - // ]); | ||
| 221 | - | ||
| 222 | const userRes = await UserApi.getUserInfo(); | 155 | const userRes = await UserApi.getUserInfo(); |
| 223 | - | ||
| 224 | - console.log('后端返回个人信息:', userRes.data); | ||
| 225 | - const rawUser = userRes.data || {}; | ||
| 226 | - userInfo.value = rawUser; | ||
| 227 | - | ||
| 228 | - | ||
| 229 | - // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化 | ||
| 230 | - // const levels = levelRes.data?.detailList || []; | ||
| 231 | - // if (rawUser.level !== undefined) { | ||
| 232 | - // // const target = levels.find((item) => item.id === rawUser.level); | ||
| 233 | - // // memberLevelName.value = target ? target.name : ''; | ||
| 234 | - // } else { | ||
| 235 | - // // memberLevelName.value = ''; | ||
| 236 | - // } | 156 | + userInfo.value = userRes.data || {}; |
| 237 | } catch (error) { | 157 | } catch (error) { |
| 238 | - console.error('[API Error] 拉取个人资产信息流失败:', error); | 158 | + console.error('[API Error] 拉取个人信息失败:', error); |
| 239 | } | 159 | } |
| 240 | }; | 160 | }; |
| 241 | 161 | ||
| @@ -248,7 +168,33 @@ onShow(() => { | @@ -248,7 +168,33 @@ onShow(() => { | ||
| 248 | } | 168 | } |
| 249 | }); | 169 | }); |
| 250 | 170 | ||
| 251 | -// 路由跳转原子原子层 | 171 | +// 设置 |
| 172 | + | ||
| 173 | +const goToForgetPassword = () => { | ||
| 174 | + uni.navigateTo({ | ||
| 175 | + url: '/pages7/pages/index/reset-password?isModify=true', | ||
| 176 | + }); | ||
| 177 | +}; | ||
| 178 | + | ||
| 179 | +const handleLogout = () => { | ||
| 180 | + uni.showModal({ | ||
| 181 | + title: '提示', | ||
| 182 | + content: '确定要退出登录吗?', | ||
| 183 | + success: async (res) => { | ||
| 184 | + if (!res.confirm) return; | ||
| 185 | + try { | ||
| 186 | + await AuthUtil.logout(); | ||
| 187 | + userStore.logout(); | ||
| 188 | + uni.reLaunch({ url: '/pages7/pages/index/login' }); | ||
| 189 | + } catch (err) { | ||
| 190 | + console.error('[API Error] 退出登录失败:', err); | ||
| 191 | + uni.showToast({ title: '退出失败', icon: 'none' }); | ||
| 192 | + } | ||
| 193 | + }, | ||
| 194 | + }); | ||
| 195 | +}; | ||
| 196 | + | ||
| 197 | +// 导航 | ||
| 252 | const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' }); | 198 | const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' }); |
| 253 | const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao'); | 199 | const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao'); |
| 254 | const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan'); | 200 | const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan'); |
| @@ -258,286 +204,215 @@ const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shang | @@ -258,286 +204,215 @@ const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shang | ||
| 258 | </script> | 204 | </script> |
| 259 | 205 | ||
| 260 | <style scoped lang="scss"> | 206 | <style scoped lang="scss"> |
| 261 | -$brand-color: #ff6b00; | ||
| 262 | -$page-bg: #f8f8f8; | ||
| 263 | - | 207 | +// 变量 |
| 208 | +// 主色 | ||
| 209 | +$color-primary: #ff6b00; | ||
| 210 | +$color-danger: #ff4757; | ||
| 211 | +$color-danger-active: #e63946; | ||
| 212 | +$color-badge: #ff4d4f; | ||
| 213 | + | ||
| 214 | +// 文字 | ||
| 215 | +$color-text-primary: #333333; | ||
| 216 | +$color-text-secondary: #555555; | ||
| 217 | +$color-text-tertiary: #999999; | ||
| 218 | +$color-text-white: #ffffff; | ||
| 219 | + | ||
| 220 | +// 背景 | ||
| 221 | +$color-bg-page: #f7f7f8; | ||
| 222 | +$color-bg-card: #ffffff; | ||
| 223 | +$color-bg-avatar: #f0f0f0; | ||
| 224 | +$color-bg-hover: #fcfcfc; | ||
| 225 | + | ||
| 226 | +// 边框 | ||
| 227 | +$color-border: #f0f0f0; | ||
| 228 | + | ||
| 229 | +// 圆角 | ||
| 230 | +$radius-card: 24rpx; | ||
| 231 | +$radius-button: 16rpx; | ||
| 232 | +$radius-round: 50%; | ||
| 233 | +$radius-tag: 8rpx; | ||
| 234 | +$radius-badge: 16rpx; | ||
| 235 | + | ||
| 236 | +// 间距 | ||
| 237 | +$gap-section: 24rpx; | ||
| 238 | +$gap-page-h: 28rpx; | ||
| 239 | +$padding-card: 30rpx 28rpx; | ||
| 240 | + | ||
| 241 | +// 字号 | ||
| 242 | +$font-title: 32rpx; | ||
| 243 | +$font-body: 28rpx; | ||
| 244 | +$font-caption: 24rpx; | ||
| 245 | +$font-small: 22rpx; | ||
| 246 | +$font-mini: 18rpx; | ||
| 247 | + | ||
| 248 | +// 效果 | ||
| 249 | +$shadow-card: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); | ||
| 250 | +$transition-tap: background-color 0.2s ease, opacity 0.2s ease; | ||
| 251 | + | ||
| 252 | +// 页面 | ||
| 264 | .my-page { | 253 | .my-page { |
| 265 | min-height: 100vh; | 254 | min-height: 100vh; |
| 266 | - background-color: $page-bg; | ||
| 267 | - padding: 20rpx 28rpx calc(40rpx + env(safe-area-inset-bottom)); | ||
| 268 | - /* 解决部分 iOS 底部 Tabbar 高度塌陷 */ | 255 | + background-color: $color-bg-page; |
| 256 | + padding: 20rpx $gap-page-h calc(40rpx + env(safe-area-inset-bottom)); | ||
| 269 | box-sizing: border-box; | 257 | box-sizing: border-box; |
| 270 | 258 | ||
| 259 | + // 卡片 | ||
| 271 | .section-card { | 260 | .section-card { |
| 272 | - background: #ffffff; | ||
| 273 | - border-radius: 24rpx; | ||
| 274 | - padding: 30rpx 20rpx; | ||
| 275 | - margin-bottom: 24rpx; | ||
| 276 | - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03); | 261 | + background: $color-bg-card; |
| 262 | + border-radius: $radius-card; | ||
| 263 | + padding: $padding-card; | ||
| 264 | + margin-bottom: $gap-section; | ||
| 265 | + box-shadow: $shadow-card; | ||
| 277 | } | 266 | } |
| 278 | 267 | ||
| 268 | + // 登录头部 | ||
| 279 | .user-header { | 269 | .user-header { |
| 280 | display: flex; | 270 | display: flex; |
| 281 | align-items: center; | 271 | align-items: center; |
| 282 | - padding: 34rpx 30rpx; | 272 | + padding: 36rpx $gap-page-h; |
| 283 | 273 | ||
| 284 | .avatar { | 274 | .avatar { |
| 285 | width: 110rpx; | 275 | width: 110rpx; |
| 286 | height: 110rpx; | 276 | height: 110rpx; |
| 287 | - border-radius: 50%; | ||
| 288 | - background: #f0f0f0; | ||
| 289 | - border: 4rpx solid #fff; | 277 | + border-radius: $radius-round; |
| 278 | + background: $color-bg-avatar; | ||
| 279 | + border: 4rpx solid $color-bg-card; | ||
| 280 | + flex-shrink: 0; | ||
| 290 | } | 281 | } |
| 291 | 282 | ||
| 292 | .info-content { | 283 | .info-content { |
| 293 | margin-left: 24rpx; | 284 | margin-left: 24rpx; |
| 294 | flex: 1; | 285 | flex: 1; |
| 286 | + min-width: 0; | ||
| 295 | 287 | ||
| 296 | .name-row { | 288 | .name-row { |
| 297 | display: flex; | 289 | display: flex; |
| 298 | flex-direction: column; | 290 | flex-direction: column; |
| 291 | + gap: 8rpx; | ||
| 299 | 292 | ||
| 300 | .nickname { | 293 | .nickname { |
| 301 | - font-size: 34rpx; | ||
| 302 | - font-weight: bold; | ||
| 303 | - color: #333; | 294 | + font-size: 36rpx; |
| 295 | + font-weight: 600; | ||
| 296 | + color: $color-text-primary; | ||
| 304 | } | 297 | } |
| 305 | 298 | ||
| 306 | .tag { | 299 | .tag { |
| 307 | - font-size: 20rpx; | ||
| 308 | - color: $brand-color; | ||
| 309 | - | 300 | + display: inline-block; |
| 301 | + font-size: $font-mini; | ||
| 302 | + color: $color-primary; | ||
| 310 | padding: 4rpx 12rpx; | 303 | padding: 4rpx 12rpx; |
| 311 | - border-radius: 8rpx; | ||
| 312 | - | ||
| 313 | - vertical-align: middle; | 304 | + border-radius: $radius-tag; |
| 305 | + background: rgba($color-primary, 0.08); | ||
| 314 | } | 306 | } |
| 315 | } | 307 | } |
| 316 | } | 308 | } |
| 317 | 309 | ||
| 318 | - .right { | ||
| 319 | - display: flex; | ||
| 320 | - align-items: center; | ||
| 321 | - gap: 60rpx; | ||
| 322 | - | ||
| 323 | - .img { | ||
| 324 | - width: 60rpx; | ||
| 325 | - height: 60rpx; | ||
| 326 | - } | ||
| 327 | - } | ||
| 328 | } | 310 | } |
| 329 | 311 | ||
| 312 | + // 未登录引导 | ||
| 330 | .login-guide-box { | 313 | .login-guide-box { |
| 331 | display: flex; | 314 | display: flex; |
| 332 | justify-content: space-between; | 315 | justify-content: space-between; |
| 333 | align-items: center; | 316 | align-items: center; |
| 317 | + padding: 36rpx $gap-page-h; | ||
| 334 | 318 | ||
| 319 | + .guide-txt { | ||
| 335 | .title { | 320 | .title { |
| 336 | - font-size: 32rpx; | ||
| 337 | - font-weight: bold; | ||
| 338 | - color: #333; | 321 | + font-size: $font-title; |
| 322 | + font-weight: 600; | ||
| 323 | + color: $color-text-primary; | ||
| 324 | + display: block; | ||
| 339 | } | 325 | } |
| 340 | 326 | ||
| 341 | .desc { | 327 | .desc { |
| 342 | - font-size: 22rpx; | ||
| 343 | - color: #999; | ||
| 344 | - margin-top: 4rpx; | 328 | + font-size: $font-small; |
| 329 | + color: $color-text-tertiary; | ||
| 330 | + margin-top: 6rpx; | ||
| 345 | display: block; | 331 | display: block; |
| 346 | } | 332 | } |
| 333 | + } | ||
| 347 | 334 | ||
| 348 | .login-btn { | 335 | .login-btn { |
| 349 | margin: 0; | 336 | margin: 0; |
| 350 | - background: $brand-color; | ||
| 351 | - color: #fff; | ||
| 352 | - font-size: 24rpx; | 337 | + flex-shrink: 0; |
| 338 | + background: $color-primary; | ||
| 339 | + color: $color-text-white; | ||
| 340 | + font-size: $font-caption; | ||
| 353 | height: 64rpx; | 341 | height: 64rpx; |
| 354 | line-height: 64rpx; | 342 | line-height: 64rpx; |
| 355 | border-radius: 32rpx; | 343 | border-radius: 32rpx; |
| 356 | - padding: 0 30rpx; | 344 | + padding: 0 32rpx; |
| 345 | + transition: $transition-tap; | ||
| 357 | 346 | ||
| 358 | &::after { | 347 | &::after { |
| 359 | border: none; | 348 | border: none; |
| 360 | } | 349 | } |
| 361 | 350 | ||
| 362 | - /* 清理小程序 button 默认黑边线 */ | ||
| 363 | - } | ||
| 364 | - } | ||
| 365 | - | ||
| 366 | - .vip-banner { | ||
| 367 | - background: #2b2b2b; | ||
| 368 | - border-radius: 20rpx; | ||
| 369 | - padding: 24rpx 30rpx; | ||
| 370 | - display: flex; | ||
| 371 | - justify-content: space-between; | ||
| 372 | - align-items: center; | ||
| 373 | - margin-bottom: 24rpx; | ||
| 374 | - | ||
| 375 | - .vip-info { | ||
| 376 | - display: flex; | ||
| 377 | - align-items: center; | ||
| 378 | - } | ||
| 379 | - | ||
| 380 | - .vip-text { | ||
| 381 | - color: #f1c40f; | ||
| 382 | - font-size: 24rpx; | ||
| 383 | - margin-left: 14rpx; | ||
| 384 | - } | ||
| 385 | - | ||
| 386 | - .vip-btn { | ||
| 387 | - background: linear-gradient(90deg, #f1c40f, #f39c12); | ||
| 388 | - color: #333; | ||
| 389 | - font-size: 20rpx; | ||
| 390 | - font-weight: bold; | ||
| 391 | - padding: 8rpx 20rpx; | ||
| 392 | - border-radius: 30rpx; | ||
| 393 | - } | ||
| 394 | - } | ||
| 395 | - | ||
| 396 | - .quick-entry { | ||
| 397 | - display: flex; | ||
| 398 | - justify-content: space-around; | ||
| 399 | - | ||
| 400 | - .entry-item { | ||
| 401 | - text-align: center; | ||
| 402 | - | ||
| 403 | - .num { | ||
| 404 | - font-size: 38rpx; | ||
| 405 | - font-weight: bold; | ||
| 406 | - color: #333; | ||
| 407 | - } | ||
| 408 | - | ||
| 409 | - .label { | ||
| 410 | - font-size: 22rpx; | ||
| 411 | - color: #999; | ||
| 412 | - margin-top: 4rpx; | ||
| 413 | - display: block; | ||
| 414 | - } | ||
| 415 | - } | ||
| 416 | - } | ||
| 417 | - | ||
| 418 | - .banner-box { | ||
| 419 | - height: 160rpx; | ||
| 420 | - margin-bottom: 24rpx; | ||
| 421 | - border-radius: 20rpx; | ||
| 422 | - overflow: hidden; | ||
| 423 | - | ||
| 424 | - .banner-img { | ||
| 425 | - width: 100%; | ||
| 426 | - height: 100%; | ||
| 427 | - } | ||
| 428 | - } | ||
| 429 | - | ||
| 430 | - .apply-section { | ||
| 431 | - display: grid; | ||
| 432 | - grid-template-columns: repeat(5, 1fr); | ||
| 433 | - | ||
| 434 | - .apply-item { | ||
| 435 | - display: flex; | ||
| 436 | - flex-direction: column; | ||
| 437 | - align-items: center; | ||
| 438 | - | ||
| 439 | - .icon-bg { | ||
| 440 | - width: 80rpx; | ||
| 441 | - height: 80rpx; | ||
| 442 | - background: #f9f9f9; | ||
| 443 | - border-radius: 20rpx; | ||
| 444 | - display: flex; | ||
| 445 | - align-items: center; | ||
| 446 | - justify-content: center; | ||
| 447 | - margin-bottom: 12rpx; | ||
| 448 | - | ||
| 449 | - .img { | ||
| 450 | - width: 44rpx; | ||
| 451 | - height: 44rpx; | ||
| 452 | - } | ||
| 453 | - } | ||
| 454 | - | ||
| 455 | - .text { | ||
| 456 | - font-size: 22rpx; | ||
| 457 | - color: #666; | ||
| 458 | - } | ||
| 459 | - } | ||
| 460 | - } | ||
| 461 | - | ||
| 462 | - .account-grid { | ||
| 463 | - display: grid; | ||
| 464 | - grid-template-columns: repeat(4, 1fr); | ||
| 465 | - row-gap: 34rpx; | ||
| 466 | - | ||
| 467 | - .account-item { | ||
| 468 | - display: flex; | ||
| 469 | - flex-direction: column; | ||
| 470 | - align-items: center; | ||
| 471 | - | ||
| 472 | - .acc-val { | ||
| 473 | - font-size: 30rpx; | ||
| 474 | - font-weight: bold; | ||
| 475 | - color: #333; | ||
| 476 | - | ||
| 477 | - .unit { | ||
| 478 | - font-size: 20rpx; | ||
| 479 | - font-weight: normal; | ||
| 480 | - color: #666; | ||
| 481 | - margin-left: 2rpx; | ||
| 482 | - } | ||
| 483 | - } | ||
| 484 | - | ||
| 485 | - .acc-lab { | ||
| 486 | - font-size: 22rpx; | ||
| 487 | - color: #999; | ||
| 488 | - margin-top: 4rpx; | 351 | + &:active { |
| 352 | + opacity: 0.85; | ||
| 489 | } | 353 | } |
| 490 | } | 354 | } |
| 491 | } | 355 | } |
| 492 | 356 | ||
| 357 | + // 功能区 | ||
| 493 | .icon-grid { | 358 | .icon-grid { |
| 494 | display: grid; | 359 | display: grid; |
| 495 | grid-template-columns: repeat(4, 1fr); | 360 | grid-template-columns: repeat(4, 1fr); |
| 496 | - row-gap: 40rpx; | 361 | + row-gap: 36rpx; |
| 497 | 362 | ||
| 498 | .icon-item { | 363 | .icon-item { |
| 499 | display: flex; | 364 | display: flex; |
| 500 | flex-direction: column; | 365 | flex-direction: column; |
| 501 | align-items: center; | 366 | align-items: center; |
| 502 | position: relative; | 367 | position: relative; |
| 368 | + transition: $transition-tap; | ||
| 369 | + | ||
| 370 | + &:active { | ||
| 371 | + opacity: 0.7; | ||
| 372 | + } | ||
| 503 | 373 | ||
| 504 | .icon-img-wrap { | 374 | .icon-img-wrap { |
| 505 | position: relative; | 375 | position: relative; |
| 376 | + width: 90rpx; | ||
| 377 | + height: 90rpx; | ||
| 378 | + display: flex; | ||
| 379 | + align-items: center; | ||
| 380 | + justify-content: center; | ||
| 506 | margin-bottom: 12rpx; | 381 | margin-bottom: 12rpx; |
| 382 | + background: #f5f6f8; | ||
| 383 | + border-radius: 22rpx; | ||
| 507 | 384 | ||
| 508 | .icon-img { | 385 | .icon-img { |
| 509 | - width: 46rpx; | ||
| 510 | - height: 46rpx; | 386 | + width: 44rpx; |
| 387 | + height: 44rpx; | ||
| 511 | } | 388 | } |
| 512 | 389 | ||
| 513 | .badge { | 390 | .badge { |
| 514 | position: absolute; | 391 | position: absolute; |
| 515 | - top: -12rpx; | ||
| 516 | - right: -42rpx; | ||
| 517 | - /* 适当拓宽右移,容纳后端多字文本 */ | ||
| 518 | - background: #ff4d4f; | ||
| 519 | - color: #fff; | ||
| 520 | - font-size: 18rpx; | 392 | + top: -10rpx; |
| 393 | + right: -36rpx; | ||
| 394 | + background: $color-badge; | ||
| 395 | + color: $color-text-white; | ||
| 396 | + font-size: $font-mini; | ||
| 521 | padding: 2rpx 10rpx; | 397 | padding: 2rpx 10rpx; |
| 522 | - border-radius: 16rpx; | 398 | + border-radius: $radius-badge; |
| 523 | white-space: nowrap; | 399 | white-space: nowrap; |
| 524 | } | 400 | } |
| 525 | 401 | ||
| 526 | - /* 未读状态小红点 */ | ||
| 527 | .dot-badge { | 402 | .dot-badge { |
| 528 | position: absolute; | 403 | position: absolute; |
| 529 | - top: -4rpx; | ||
| 530 | - right: -4rpx; | 404 | + top: -2rpx; |
| 405 | + right: -2rpx; | ||
| 531 | width: 14rpx; | 406 | width: 14rpx; |
| 532 | height: 14rpx; | 407 | height: 14rpx; |
| 533 | - background: #ff4d4f; | ||
| 534 | - border-radius: 50%; | 408 | + background: $color-badge; |
| 409 | + border-radius: $radius-round; | ||
| 535 | } | 410 | } |
| 536 | } | 411 | } |
| 537 | 412 | ||
| 538 | .icon-text { | 413 | .icon-text { |
| 539 | - font-size: 24rpx; | ||
| 540 | - color: #555; | 414 | + font-size: $font-caption; |
| 415 | + color: $color-text-secondary; | ||
| 541 | } | 416 | } |
| 542 | 417 | ||
| 543 | .mp-contact-overlay-btn { | 418 | .mp-contact-overlay-btn { |
| @@ -547,12 +422,10 @@ $page-bg: #f8f8f8; | @@ -547,12 +422,10 @@ $page-bg: #f8f8f8; | ||
| 547 | width: 100% !important; | 422 | width: 100% !important; |
| 548 | height: 100% !important; | 423 | height: 100% !important; |
| 549 | opacity: 0 !important; | 424 | opacity: 0 !important; |
| 550 | - /* 核心:完全透明 */ | ||
| 551 | border: none !important; | 425 | border: none !important; |
| 552 | padding: 0 !important; | 426 | padding: 0 !important; |
| 553 | margin: 0 !important; | 427 | margin: 0 !important; |
| 554 | z-index: 10; | 428 | z-index: 10; |
| 555 | - /* 确保盖在图表和文字的最上方 */ | ||
| 556 | 429 | ||
| 557 | &::after { | 430 | &::after { |
| 558 | border: none !important; | 431 | border: none !important; |
| @@ -561,29 +434,58 @@ $page-bg: #f8f8f8; | @@ -561,29 +434,58 @@ $page-bg: #f8f8f8; | ||
| 561 | } | 434 | } |
| 562 | } | 435 | } |
| 563 | 436 | ||
| 564 | - .setting-item { | 437 | + // 设置区 |
| 438 | + .settings-section { | ||
| 439 | + padding: 30rpx $gap-page-h; | ||
| 440 | + | ||
| 441 | + .settings-title { | ||
| 442 | + font-size: 30rpx; | ||
| 443 | + font-weight: 600; | ||
| 444 | + color: $color-text-primary; | ||
| 445 | + margin-bottom: 8rpx; | ||
| 446 | + } | ||
| 447 | + } | ||
| 448 | + | ||
| 449 | + .settings-item { | ||
| 565 | display: flex; | 450 | display: flex; |
| 566 | justify-content: space-between; | 451 | justify-content: space-between; |
| 567 | align-items: center; | 452 | align-items: center; |
| 568 | - padding: 24rpx 0; | ||
| 569 | - border-bottom: 1rpx solid #f9f9f9; | 453 | + padding: 28rpx 0; |
| 454 | + border-bottom: 1rpx solid $color-border; | ||
| 570 | 455 | ||
| 571 | - &:last-child { | ||
| 572 | - border-bottom: none; | 456 | + .settings-label { |
| 457 | + font-size: $font-body; | ||
| 458 | + color: $color-text-primary; | ||
| 573 | } | 459 | } |
| 574 | - | ||
| 575 | - .setting-text { | ||
| 576 | - font-size: 28rpx; | ||
| 577 | - color: #444; | ||
| 578 | } | 460 | } |
| 461 | + | ||
| 462 | + .logout-wrap { | ||
| 463 | + margin-top: 40rpx; | ||
| 464 | + | ||
| 465 | + .logout-btn { | ||
| 466 | + width: 100%; | ||
| 467 | + background-color: $color-danger; | ||
| 468 | + color: $color-text-white; | ||
| 469 | + border: none; | ||
| 470 | + border-radius: $radius-button; | ||
| 471 | + padding: 26rpx 0; | ||
| 472 | + font-size: 30rpx; | ||
| 473 | + font-weight: 500; | ||
| 474 | + transition: $transition-tap; | ||
| 475 | + | ||
| 476 | + &::after { | ||
| 477 | + border: none; | ||
| 579 | } | 478 | } |
| 580 | 479 | ||
| 581 | - .opacity-hover { | ||
| 582 | - opacity: 0.7; | 480 | + &:active { |
| 481 | + background-color: $color-danger-active; | ||
| 482 | + } | ||
| 483 | + } | ||
| 583 | } | 484 | } |
| 584 | 485 | ||
| 486 | + // 触摸反馈 | ||
| 585 | .card-hover { | 487 | .card-hover { |
| 586 | - background-color: #fcfcfc; | 488 | + background-color: $color-bg-hover; |
| 587 | } | 489 | } |
| 588 | } | 490 | } |
| 589 | </style> | 491 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <up-popup :show="actionShow" mode="bottom" minHeight="90vh" @close="actionShow = false" bgColor="#1a1a1a"> | 2 | <up-popup :show="actionShow" mode="bottom" minHeight="90vh" @close="actionShow = false" bgColor="#1a1a1a"> |
| 3 | <scroll-view class="action-container" scroll-y> | 3 | <scroll-view class="action-container" scroll-y> |
| 4 | - <view class="header" v-if="type == 1"> | 4 | + <view class="header" v-if="type === 1"> |
| 5 | <view class="explain"> | 5 | <view class="explain"> |
| 6 | <!-- 3D 固定图片 --> | 6 | <!-- 3D 固定图片 --> |
| 7 | <image v-if="modeTab === 0" :src="actionDetail.url3dAnimation" class="media-content" mode="aspectFill" /> | 7 | <image v-if="modeTab === 0" :src="actionDetail.url3dAnimation" class="media-content" mode="aspectFill" /> |
| @@ -32,15 +32,15 @@ | @@ -32,15 +32,15 @@ | ||
| 32 | </view> | 32 | </view> |
| 33 | 33 | ||
| 34 | <view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial"> | 34 | <view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial"> |
| 35 | - <view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab == 0 }" | 35 | + <view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab === 0 }" |
| 36 | @click="switchModeTab(0)"> | 36 | @click="switchModeTab(0)"> |
| 37 | 3D | 37 | 3D |
| 38 | </view> | 38 | </view> |
| 39 | - <view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab == 1 }" | 39 | + <view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab === 1 }" |
| 40 | @click="switchModeTab(1)"> | 40 | @click="switchModeTab(1)"> |
| 41 | 真人 | 41 | 真人 |
| 42 | </view> | 42 | </view> |
| 43 | - <view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab == 2 }" | 43 | + <view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab === 2 }" |
| 44 | @click="switchModeTab(2)"> | 44 | @click="switchModeTab(2)"> |
| 45 | 讲解 | 45 | 讲解 |
| 46 | </view> | 46 | </view> |
| @@ -56,17 +56,18 @@ | @@ -56,17 +56,18 @@ | ||
| 56 | <!-- 只有个人创建的动作,才有这个编辑按钮--> | 56 | <!-- 只有个人创建的动作,才有这个编辑按钮--> |
| 57 | <template v-if="actionDetail?.isSystem === 0"> | 57 | <template v-if="actionDetail?.isSystem === 0"> |
| 58 | <view class="edit-icon" @click="edit"> | 58 | <view class="edit-icon" @click="edit"> |
| 59 | - <uni-icons type="more" size="24" color="#fff"></uni-icons> | 59 | + <up-icon name="more-dot-fill" size="24" color="#fff" /> |
| 60 | </view> | 60 | </view> |
| 61 | </template> | 61 | </template> |
| 62 | - | 62 | + <!-- #ifdef H5 --> |
| 63 | <button class="share-btn" open-type="share"> | 63 | <button class="share-btn" open-type="share"> |
| 64 | - <uni-icons type="paperplane" size="24" color="#fff"></uni-icons> | 64 | + <up-icon name="share-fill" size="24" color="#fff" /> |
| 65 | </button> | 65 | </button> |
| 66 | + <!-- #endif --> | ||
| 66 | <up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24" | 67 | <up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24" |
| 67 | @click="toggleCollect"> | 68 | @click="toggleCollect"> |
| 68 | </up-icon> | 69 | </up-icon> |
| 69 | - <!-- <FavoriteBtn :id="actionId" :type="type" /> --> | 70 | + |
| 70 | </view> | 71 | </view> |
| 71 | </view> | 72 | </view> |
| 72 | <!-- 要点,历史,平替动作标签 --> | 73 | <!-- 要点,历史,平替动作标签 --> |
| @@ -106,16 +107,16 @@ | @@ -106,16 +107,16 @@ | ||
| 106 | </view> | 107 | </view> |
| 107 | </view> | 108 | </view> |
| 108 | 109 | ||
| 109 | - <template v-if="type == 1"> | 110 | + <template v-if="type === 1"> |
| 110 | <textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea> | 111 | <textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea> |
| 111 | </template> | 112 | </template> |
| 112 | 113 | ||
| 113 | - <template v-if="type == 2"> | 114 | + <template v-if="type === 2"> |
| 114 | <textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea> | 115 | <textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea> |
| 115 | </template> | 116 | </template> |
| 116 | 117 | ||
| 117 | </view> | 118 | </view> |
| 118 | - <view class="memo-box" v-if="type == 1"> | 119 | + <view class="memo-box" v-if="type === 1"> |
| 119 | <view class="memo-title"> | 120 | <view class="memo-title"> |
| 120 | <view class="section-title">步骤</view> | 121 | <view class="section-title">步骤</view> |
| 121 | <view class="pencil-icon"> | 122 | <view class="pencil-icon"> |
| @@ -168,18 +169,7 @@ | @@ -168,18 +169,7 @@ | ||
| 168 | </view> | 169 | </view> |
| 169 | </view> | 170 | </view> |
| 170 | </view> | 171 | </view> |
| 171 | - <!-- 2 历史 --> | ||
| 172 | <view v-if="contentTab === 1" class="tab-pane slide-up"> | 172 | <view v-if="contentTab === 1" class="tab-pane slide-up"> |
| 173 | - <!-- <view class="stat-card highlight"> | ||
| 174 | - <text class="label">最长时长</text> | ||
| 175 | - <text class="value">01:02:03</text> | ||
| 176 | - <text class="date">记录于 2026/03/04</text> | ||
| 177 | - </view> --> | ||
| 178 | - | ||
| 179 | - <!-- <view class="chart-container" v-if="chartData && chartData.series"> | ||
| 180 | - <qiun-data-charts type="line" :opts="chartOpts" :chartData="chartData" /> | ||
| 181 | - </view> --> | ||
| 182 | - | ||
| 183 | <view class="history-list"> | 173 | <view class="history-list"> |
| 184 | <template v-if="historyList && historyList.length > 0"> | 174 | <template v-if="historyList && historyList.length > 0"> |
| 185 | <view class="history-item" v-for="item in historyList" :key="item.id"> | 175 | <view class="history-item" v-for="item in historyList" :key="item.id"> |
| @@ -216,7 +206,7 @@ | @@ -216,7 +206,7 @@ | ||
| 216 | <view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up"> | 206 | <view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up"> |
| 217 | <view class="substitute-list"> | 207 | <view class="substitute-list"> |
| 218 | <view class="sub-item" v-for="item in alternativeActions" :key="item.id" @click="openActionItem(item)"> | 208 | <view class="sub-item" v-for="item in alternativeActions" :key="item.id" @click="openActionItem(item)"> |
| 219 | - <image :src="item.url3dAnimation || lostImage" mode="aspectFill" class="img" /> | 209 | + <image :src="item.url3dAnimation" mode="aspectFill" class="img" /> |
| 220 | <view class="sub-info"> | 210 | <view class="sub-info"> |
| 221 | <text class="name">{{ item.name }}</text> | 211 | <text class="name">{{ item.name }}</text> |
| 222 | 212 | ||
| @@ -238,47 +228,35 @@ | @@ -238,47 +228,35 @@ | ||
| 238 | </template> | 228 | </template> |
| 239 | 229 | ||
| 240 | <script setup> | 230 | <script setup> |
| 241 | -import { onMounted, ref, nextTick } from 'vue'; | 231 | +import { ref, nextTick } from 'vue'; |
| 232 | +import dayjs from 'dayjs'; | ||
| 242 | import ExercisesApi from '@/sheep/api/motion/exercises'; | 233 | import ExercisesApi from '@/sheep/api/motion/exercises'; |
| 243 | import beizhu from '@/pages/xunji/components/beizhu.vue'; | 234 | import beizhu from '@/pages/xunji/components/beizhu.vue'; |
| 244 | import SupersetsApi from '@/sheep/api/motion/supersets'; | 235 | import SupersetsApi from '@/sheep/api/motion/supersets'; |
| 245 | import TrainingApi from '@/sheep/api/Training/traininghistory'; | 236 | import TrainingApi from '@/sheep/api/Training/traininghistory'; |
| 246 | import { onShareAppMessage } from '@dcloudio/uni-app'; | 237 | import { onShareAppMessage } from '@dcloudio/uni-app'; |
| 247 | -import { useTrainingStore } from '@/sheep/store/trainingStore' | 238 | +import { useTrainingStore } from '@/sheep/store/trainingStore'; |
| 248 | 239 | ||
| 240 | +const trainingStore = useTrainingStore(); | ||
| 241 | +const lostImage = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'; | ||
| 249 | 242 | ||
| 250 | -const trainingStore = useTrainingStore() | ||
| 251 | - | ||
| 252 | -// 静态配置 | ||
| 253 | - | ||
| 254 | -const alternativeActions = ref([]); // 平替动作列表接口数据 | ||
| 255 | -const loadedHistory = ref(false) | ||
| 256 | -const loadedAlternative = ref(false) | ||
| 257 | - | ||
| 258 | -// 响应式状态 | ||
| 259 | const actionShow = ref(false); | 243 | const actionShow = ref(false); |
| 260 | const modeTab = ref(0); | 244 | const modeTab = ref(0); |
| 261 | const contentTab = ref(0); | 245 | const contentTab = ref(0); |
| 262 | const isFavorite = ref(false); | 246 | const isFavorite = ref(false); |
| 263 | - | ||
| 264 | -// 记录当前动作的详细数据 | ||
| 265 | const actionDetail = ref({}); | 247 | const actionDetail = ref({}); |
| 266 | -// 记录当前动作的id | ||
| 267 | const actionId = ref(0); | 248 | const actionId = ref(0); |
| 268 | -// 记录是超级组还是动作组 1=动作组,2=超级组 | ||
| 269 | const type = ref(0); | 249 | const type = ref(0); |
| 270 | const showBeizhuRef = ref(null); | 250 | const showBeizhuRef = ref(null); |
| 251 | +const historyList = ref([]); | ||
| 252 | +const alternativeActions = ref([]); | ||
| 253 | +const loadedHistory = ref(false); | ||
| 254 | +const loadedAlternative = ref(false); | ||
| 271 | 255 | ||
| 272 | -const historyList = ref([]); // 训练历史列表接口数据 | ||
| 273 | -const lostImage = | ||
| 274 | - 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'; | ||
| 275 | - | ||
| 276 | -// 切换图表模式 | ||
| 277 | const switchModeTab = (index) => { | 256 | const switchModeTab = (index) => { |
| 278 | modeTab.value = index; | 257 | modeTab.value = index; |
| 279 | }; | 258 | }; |
| 280 | 259 | ||
| 281 | -// 判断链接是否为视频链接 | ||
| 282 | const isVideoUrl = (url) => { | 260 | const isVideoUrl = (url) => { |
| 283 | if (!url) return false; | 261 | if (!url) return false; |
| 284 | const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm']; | 262 | const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm']; |
| @@ -286,13 +264,10 @@ const isVideoUrl = (url) => { | @@ -286,13 +264,10 @@ const isVideoUrl = (url) => { | ||
| 286 | return videoExts.some(ext => lowerUrl.includes(ext)); | 264 | return videoExts.some(ext => lowerUrl.includes(ext)); |
| 287 | }; | 265 | }; |
| 288 | 266 | ||
| 289 | -// 跳转到视频播放的页面 | ||
| 290 | const playVideo = (url) => { | 267 | const playVideo = (url) => { |
| 291 | - uni.navigateTo({ | ||
| 292 | - url: '/pages4/pages/xunji/xunji-shiping?url=' + url, | ||
| 293 | - }); | 268 | + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-shiping?url=' + url }); |
| 294 | }; | 269 | }; |
| 295 | -// 获取动作收藏状态 | 270 | + |
| 296 | const checkExerciseFavorited = async () => { | 271 | const checkExerciseFavorited = async () => { |
| 297 | try { | 272 | try { |
| 298 | const res = await ExercisesApi.checkExerciseFavorited(actionId.value); | 273 | const res = await ExercisesApi.checkExerciseFavorited(actionId.value); |
| @@ -308,21 +283,16 @@ const toggleCollect = async () => { | @@ -308,21 +283,16 @@ const toggleCollect = async () => { | ||
| 308 | try { | 283 | try { |
| 309 | const status = isFavorite.value ? 0 : 1; | 284 | const status = isFavorite.value ? 0 : 1; |
| 310 | console.log('actionId.value=', actionId.value, 'status=', status); | 285 | console.log('actionId.value=', actionId.value, 'status=', status); |
| 311 | - if (type.value == 1) { | ||
| 312 | - console.log('动作收藏传递的参数', actionId.value, status); | 286 | + if (type.value === 1) { |
| 313 | await ExercisesApi.toggleFavorite(actionId.value, status); | 287 | await ExercisesApi.toggleFavorite(actionId.value, status); |
| 314 | - | ||
| 315 | } else { | 288 | } else { |
| 316 | - console.log('超级组收藏传递的参数', actionId.value, status); | ||
| 317 | await SupersetsApi.toggleFavorite(actionId.value, status); | 289 | await SupersetsApi.toggleFavorite(actionId.value, status); |
| 318 | } | 290 | } |
| 319 | isFavorite.value = !isFavorite.value; | 291 | isFavorite.value = !isFavorite.value; |
| 320 | uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' }); | 292 | uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' }); |
| 321 | } catch (err) { | 293 | } catch (err) { |
| 322 | console.log(err); | 294 | console.log(err); |
| 323 | - // 请求失败强制回滚UI | ||
| 324 | - // isFavorite.value = originState; | ||
| 325 | - uni.showToast({ title: '操作失败,请重试', icon: 'none' }); | 295 | + |
| 326 | } | 296 | } |
| 327 | }; | 297 | }; |
| 328 | 298 | ||
| @@ -375,26 +345,16 @@ const open = async (id, typeData) => { | @@ -375,26 +345,16 @@ const open = async (id, typeData) => { | ||
| 375 | contentTab.value = 0; | 345 | contentTab.value = 0; |
| 376 | console.log('动作详情页面接收id和类型:', actionId.value, type.value); | 346 | console.log('动作详情页面接收id和类型:', actionId.value, type.value); |
| 377 | // 如何判断是动作还是超级组 | 347 | // 如何判断是动作还是超级组 |
| 378 | - if (typeData == 1) { | 348 | + if (typeData === 1) { |
| 379 | await loadexercisedetail(actionId.value); | 349 | await loadexercisedetail(actionId.value); |
| 380 | - // await loadAlternativeActions(actionId.value); | ||
| 381 | - console.log('进入动作收藏检查接口'); | ||
| 382 | await checkExerciseFavorited(); | 350 | await checkExerciseFavorited(); |
| 383 | - | ||
| 384 | - | ||
| 385 | } else { | 351 | } else { |
| 386 | await loadsuperdetail(actionId.value); | 352 | await loadsuperdetail(actionId.value); |
| 387 | - console.log('进入超级组收藏检查接口'); | ||
| 388 | await checkSupersetFavorited(); | 353 | await checkSupersetFavorited(); |
| 389 | } | 354 | } |
| 390 | 355 | ||
| 391 | - console.log('请求参数actionId/typeData', actionId.value, type.value); | ||
| 392 | - | ||
| 393 | - // 重置加载训练历史列表和平替动作列表标记 | ||
| 394 | - loadedHistory.value = false | ||
| 395 | - loadedAlternative.value = false | ||
| 396 | - | ||
| 397 | - // loadTrainHistoryDetail(actionId.value, type.value); | 356 | + loadedHistory.value = false; |
| 357 | + loadedAlternative.value = false; | ||
| 398 | actionShow.value = true; | 358 | actionShow.value = true; |
| 399 | }; | 359 | }; |
| 400 | // 打开备注弹窗 | 360 | // 打开备注弹窗 |
| @@ -413,7 +373,7 @@ const handleNoteSave = async (content) => { | @@ -413,7 +373,7 @@ const handleNoteSave = async (content) => { | ||
| 413 | console.log('type.value=', type.value); | 373 | console.log('type.value=', type.value); |
| 414 | 374 | ||
| 415 | try { | 375 | try { |
| 416 | - if (type.value == 2) { | 376 | + if (type.value === 2) { |
| 417 | await SupersetsApi.addNotes({ | 377 | await SupersetsApi.addNotes({ |
| 418 | supersetsId: actionId.value, | 378 | supersetsId: actionId.value, |
| 419 | content: content, | 379 | content: content, |
| @@ -428,7 +388,7 @@ const handleNoteSave = async (content) => { | @@ -428,7 +388,7 @@ const handleNoteSave = async (content) => { | ||
| 428 | } catch (e) { | 388 | } catch (e) { |
| 429 | console.log(e); | 389 | console.log(e); |
| 430 | } finally { | 390 | } finally { |
| 431 | - if (type.value == 1) { | 391 | + if (type.value === 1) { |
| 432 | await loadexercisedetail(actionId.value) | 392 | await loadexercisedetail(actionId.value) |
| 433 | } else { | 393 | } else { |
| 434 | await loadsuperdetail(actionId.value) | 394 | await loadsuperdetail(actionId.value) |
| @@ -451,7 +411,7 @@ onShareAppMessage((res) => { | @@ -451,7 +411,7 @@ onShareAppMessage((res) => { | ||
| 451 | title: actionDetail.value.name || '健身动作分享', // 分享标题 | 411 | title: actionDetail.value.name || '健身动作分享', // 分享标题 |
| 452 | // path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径 | 412 | // path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径 |
| 453 | path: `/pages/xunji/xunji?currentTab=${3}`, | 413 | path: `/pages/xunji/xunji?currentTab=${3}`, |
| 454 | - imageUrl: actionDetail.value.urlImage || lostImage, // 分享图片 | 414 | + imageUrl: actionDetail.value.urlImage, // 分享图片 |
| 455 | }; | 415 | }; |
| 456 | }); | 416 | }); |
| 457 | 417 | ||
| @@ -517,53 +477,18 @@ const loadTrainHistoryDetail = async (id, type) => { | @@ -517,53 +477,18 @@ const loadTrainHistoryDetail = async (id, type) => { | ||
| 517 | // 格式化时间 | 477 | // 格式化时间 |
| 518 | const formatDate = (dateArr) => { | 478 | const formatDate = (dateArr) => { |
| 519 | if (!Array.isArray(dateArr) || dateArr.length < 3) return ''; | 479 | if (!Array.isArray(dateArr) || dateArr.length < 3) return ''; |
| 520 | - const [year, month, day] = dateArr; | ||
| 521 | - const date = new Date(year, month - 1, day); | ||
| 522 | - const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; | ||
| 523 | - const week = weekArr[date.getDay()]; | ||
| 524 | - return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')} ${week}`; | 480 | + return dayjs(new Date(dateArr[0], dateArr[1] - 1, dateArr[2])).format('YYYY/MM/DD dd'); |
| 525 | }; | 481 | }; |
| 526 | 482 | ||
| 527 | -// 格式化每组数据:自动拼接 weight/reps/duration/distance | ||
| 528 | const formatSetData = (set) => { | 483 | const formatSetData = (set) => { |
| 529 | const parts = []; | 484 | const parts = []; |
| 530 | - | ||
| 531 | - // 重量 | ||
| 532 | - if (set.weight != null && set.weight !== '') { | ||
| 533 | - parts.push(`${set.weight}kg`); | ||
| 534 | - } | ||
| 535 | - | ||
| 536 | - // 次数 | ||
| 537 | - if (set.reps != null && set.reps !== '') { | ||
| 538 | - parts.push(`${set.reps}次`); | ||
| 539 | - } | ||
| 540 | - | ||
| 541 | - // 时长(自动转 时:分:秒) | ||
| 542 | - if (set.duration != null && set.duration !== '') { | ||
| 543 | - parts.push(formatTime(set.duration)); | ||
| 544 | - } | ||
| 545 | - | ||
| 546 | - // 距离 | ||
| 547 | - if (set.distance != null && set.distance !== '') { | ||
| 548 | - parts.push(`${set.distance}m`); | ||
| 549 | - } | ||
| 550 | - | 485 | + if (set.weight != null && set.weight !== '') parts.push(`${set.weight}kg`); |
| 486 | + if (set.reps != null && set.reps !== '') parts.push(`${set.reps}次`); | ||
| 487 | + if (set.duration != null && set.duration !== '') parts.push(dayjs.duration(set.duration, 'seconds').format('HH:mm:ss')); | ||
| 488 | + if (set.distance != null && set.distance !== '') parts.push(`${set.distance}m`); | ||
| 551 | return parts.length > 0 ? parts.join(' × ') : '无数据'; | 489 | return parts.length > 0 ? parts.join(' × ') : '无数据'; |
| 552 | }; | 490 | }; |
| 553 | 491 | ||
| 554 | -// 新增:秒数转 00:00:00 格式 | ||
| 555 | -const formatTime = (seconds) => { | ||
| 556 | - const h = Math.floor(seconds / 3600); | ||
| 557 | - const m = Math.floor((seconds % 3600) / 60); | ||
| 558 | - const s = seconds % 60; | ||
| 559 | - | ||
| 560 | - const hh = h > 0 ? String(h).padStart(2, '0') + ':' : ''; | ||
| 561 | - const mm = String(m).padStart(2, '0') + ':'; | ||
| 562 | - const ss = String(s).padStart(2, '0'); | ||
| 563 | - | ||
| 564 | - return hh + mm + ss; | ||
| 565 | -}; | ||
| 566 | - | ||
| 567 | const edit = () => { | 492 | const edit = () => { |
| 568 | uni.navigateTo({ | 493 | uni.navigateTo({ |
| 569 | url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}` | 494 | url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}` |
| @@ -577,8 +502,6 @@ const openActionItem = (item) => { | @@ -577,8 +502,6 @@ const openActionItem = (item) => { | ||
| 577 | }; | 502 | }; |
| 578 | 503 | ||
| 579 | defineExpose({ open }); | 504 | defineExpose({ open }); |
| 580 | - | ||
| 581 | -onMounted(() => { }); | ||
| 582 | </script> | 505 | </script> |
| 583 | 506 | ||
| 584 | <style lang="scss" scoped> | 507 | <style lang="scss" scoped> |
| @@ -772,7 +695,6 @@ onMounted(() => { }); | @@ -772,7 +695,6 @@ onMounted(() => { }); | ||
| 772 | } | 695 | } |
| 773 | } | 696 | } |
| 774 | 697 | ||
| 775 | - // 要点板块样式 | ||
| 776 | .video-grid { | 698 | .video-grid { |
| 777 | display: grid; | 699 | display: grid; |
| 778 | grid-template-columns: 1fr 1fr; | 700 | grid-template-columns: 1fr 1fr; |
| @@ -816,7 +738,6 @@ onMounted(() => { }); | @@ -816,7 +738,6 @@ onMounted(() => { }); | ||
| 816 | color: #fff; | 738 | color: #fff; |
| 817 | height: 160rpx; | 739 | height: 160rpx; |
| 818 | border-radius: 16rpx; | 740 | border-radius: 16rpx; |
| 819 | - // padding: 20rpx; | ||
| 820 | font-size: 30rpx; | 741 | font-size: 30rpx; |
| 821 | border: 0; | 742 | border: 0; |
| 822 | } | 743 | } |
| @@ -827,7 +748,6 @@ onMounted(() => { }); | @@ -827,7 +748,6 @@ onMounted(() => { }); | ||
| 827 | flex-direction: column; | 748 | flex-direction: column; |
| 828 | gap: 16rpx; | 749 | gap: 16rpx; |
| 829 | background: #464646; | 750 | background: #464646; |
| 830 | - // padding: 20rpx; | ||
| 831 | box-sizing: border-box; | 751 | box-sizing: border-box; |
| 832 | border-radius: 16rpx; | 752 | border-radius: 16rpx; |
| 833 | 753 | ||
| @@ -877,7 +797,6 @@ onMounted(() => { }); | @@ -877,7 +797,6 @@ onMounted(() => { }); | ||
| 877 | color: #fff; | 797 | color: #fff; |
| 878 | } | 798 | } |
| 879 | 799 | ||
| 880 | - // 历史记录样式 | ||
| 881 | .stat-card { | 800 | .stat-card { |
| 882 | background: linear-gradient(135deg, #333, #222); | 801 | background: linear-gradient(135deg, #333, #222); |
| 883 | padding: 40rpx; | 802 | padding: 40rpx; |
| @@ -933,8 +852,6 @@ onMounted(() => { }); | @@ -933,8 +852,6 @@ onMounted(() => { }); | ||
| 933 | } | 852 | } |
| 934 | 853 | ||
| 935 | .tag { | 854 | .tag { |
| 936 | - // font-size: 20rpx; | ||
| 937 | - // background: #444; | ||
| 938 | margin: 5rpx 5rpx; | 855 | margin: 5rpx 5rpx; |
| 939 | border-radius: 4rpx; | 856 | border-radius: 4rpx; |
| 940 | } | 857 | } |
| @@ -948,7 +865,6 @@ onMounted(() => { }); | @@ -948,7 +865,6 @@ onMounted(() => { }); | ||
| 948 | margin-top: 20rpx; | 865 | margin-top: 20rpx; |
| 949 | 866 | ||
| 950 | .chip { | 867 | .chip { |
| 951 | - // background: #333; | ||
| 952 | padding: 8rpx 20rpx; | 868 | padding: 8rpx 20rpx; |
| 953 | font-size: 24rpx; | 869 | font-size: 24rpx; |
| 954 | display: flex; | 870 | display: flex; |
| @@ -965,7 +881,6 @@ onMounted(() => { }); | @@ -965,7 +881,6 @@ onMounted(() => { }); | ||
| 965 | } | 881 | } |
| 966 | } | 882 | } |
| 967 | 883 | ||
| 968 | - // 平替动作样式 | ||
| 969 | .sub-item { | 884 | .sub-item { |
| 970 | display: flex; | 885 | display: flex; |
| 971 | align-items: center; | 886 | align-items: center; |
| @@ -1024,7 +939,6 @@ onMounted(() => { }); | @@ -1024,7 +939,6 @@ onMounted(() => { }); | ||
| 1024 | } | 939 | } |
| 1025 | } | 940 | } |
| 1026 | 941 | ||
| 1027 | -// 动画 | ||
| 1028 | .slide-up { | 942 | .slide-up { |
| 1029 | animation: slideUp 0.4s ease-out; | 943 | animation: slideUp 0.4s ease-out; |
| 1030 | } | 944 | } |
| @@ -24,138 +24,83 @@ | @@ -24,138 +24,83 @@ | ||
| 24 | </template> | 24 | </template> |
| 25 | 25 | ||
| 26 | <script setup> | 26 | <script setup> |
| 27 | +import dayjs from 'dayjs'; | ||
| 27 | import { ref, computed, watch } from 'vue'; | 28 | import { ref, computed, watch } from 'vue'; |
| 28 | 29 | ||
| 29 | const emit = defineEmits(['dateTypeChange', 'dateRangeChange']); | 30 | const emit = defineEmits(['dateTypeChange', 'dateRangeChange']); |
| 30 | 31 | ||
| 31 | -// 当前日期类型: 'week' | 'month' | 32 | +// 当前日期类型与基准日期 |
| 32 | const currentDateType = ref('week'); | 33 | const currentDateType = ref('week'); |
| 33 | -// 基准日期,用于计算范围 | ||
| 34 | -const baseDate = ref(new Date()); | ||
| 35 | - | ||
| 36 | -// 格式化日期辅助函数 YYYY/MM/DD | ||
| 37 | -const formatDate = (date) => { | ||
| 38 | - const y = date.getFullYear(); | ||
| 39 | - const m = String(date.getMonth() + 1).padStart(2, '0'); | ||
| 40 | - const d = String(date.getDate()).padStart(2, '0'); | ||
| 41 | - return `${y}/${m}/${d}`; | ||
| 42 | -}; | ||
| 43 | -// 格式化日期辅助函数 YYYY-MM-DD | ||
| 44 | -const formatDateYmd = (date) => { | ||
| 45 | - const y = date.getFullYear(); | ||
| 46 | - const m = String(date.getMonth() + 1).padStart(2, '0'); | ||
| 47 | - const d = String(date.getDate()).padStart(2, '0'); | ||
| 48 | - return `${y}-${m}-${d}`; | ||
| 49 | -}; | 34 | +const baseDate = ref(dayjs()); |
| 50 | 35 | ||
| 51 | -const currentDateRange = computed(() => { | ||
| 52 | - const current = new Date(baseDate.value); | ||
| 53 | - let start, end; | 36 | +// ==================== 公共日期范围计算 ==================== |
| 54 | 37 | ||
| 38 | +/** 根据当前类型计算周期起止日期(dayjs 实例) */ | ||
| 39 | +const periodRange = computed(() => { | ||
| 40 | + const d = baseDate.value; | ||
| 55 | if (currentDateType.value === 'week') { | 41 | if (currentDateType.value === 'week') { |
| 56 | - const day = current.getDay(); | ||
| 57 | - const diffToMonday = day === 0 ? -6 : 1 - day; | ||
| 58 | - start = new Date(current); | ||
| 59 | - start.setDate(current.getDate() + diffToMonday); | ||
| 60 | - end = new Date(start); | ||
| 61 | - end.setDate(start.getDate() + 6); | ||
| 62 | - } else { | ||
| 63 | - start = new Date(current.getFullYear(), current.getMonth(), 1); | ||
| 64 | - end = new Date(current.getFullYear(), current.getMonth() + 1, 0); | 42 | + const dayOfWeek = d.day(); // 0=周日, 1=周一, ... |
| 43 | + const start = d.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day'); | ||
| 44 | + const end = start.add(6, 'day'); | ||
| 45 | + return { start, end }; | ||
| 65 | } | 46 | } |
| 66 | - | ||
| 67 | - return { | ||
| 68 | - start: formatDateYmd(start), | ||
| 69 | - end: formatDateYmd(end) | ||
| 70 | - }; | 47 | + // month |
| 48 | + return { start: d.startOf('month'), end: d.endOf('month') }; | ||
| 71 | }); | 49 | }); |
| 72 | 50 | ||
| 73 | -// 计算显示的日期范围文本 | ||
| 74 | -const dateRangeText = computed(() => { | ||
| 75 | - const current = new Date(baseDate.value); | ||
| 76 | - | ||
| 77 | - if (currentDateType.value === 'week') { | ||
| 78 | - // 计算本周的周一和周日 | ||
| 79 | - const day = current.getDay(); | ||
| 80 | - const diffToMonday = day === 0 ? -6 : 1 - day; // 调整到周一 | ||
| 81 | - | ||
| 82 | - const startOfWeek = new Date(current); | ||
| 83 | - startOfWeek.setDate(current.getDate() + diffToMonday); | ||
| 84 | - | ||
| 85 | - const endOfWeek = new Date(startOfWeek); | ||
| 86 | - endOfWeek.setDate(startOfWeek.getDate() + 6); | 51 | +// ==================== 计算属性 ==================== |
| 87 | 52 | ||
| 88 | - return `${formatDate(startOfWeek)} - ${formatDate(endOfWeek)}`; | ||
| 89 | - } else { | ||
| 90 | - // 计算本月的一号和最后一天 | ||
| 91 | - const startOfMonth = new Date(current.getFullYear(), current.getMonth(), 1); | ||
| 92 | - const endOfMonth = new Date(current.getFullYear(), current.getMonth() + 1, 0); | 53 | +/** 对外暴露的日期范围(YYYY-MM-DD) */ |
| 54 | +const currentDateRange = computed(() => ({ | ||
| 55 | + start: periodRange.value.start.format('YYYY-MM-DD'), | ||
| 56 | + end: periodRange.value.end.format('YYYY-MM-DD'), | ||
| 57 | +})); | ||
| 93 | 58 | ||
| 94 | - return `${formatDate(startOfMonth)} - ${formatDate(endOfMonth)}`; | ||
| 95 | - } | 59 | +/** 显示的日期范围文本 "YYYY/MM/DD - YYYY/MM/DD" */ |
| 60 | +const dateRangeText = computed(() => { | ||
| 61 | + const { start, end } = periodRange.value; | ||
| 62 | + return `${start.format('YYYY/MM/DD')} - ${end.format('YYYY/MM/DD')}`; | ||
| 96 | }); | 63 | }); |
| 97 | 64 | ||
| 98 | -// 新增: 计算下一个日期是否不可用(即是否为未来日期) | 65 | +/** 下一周期是否超过今天(超出则隐藏前进按钮) */ |
| 99 | const isNextDisabled = computed(() => { | 66 | const isNextDisabled = computed(() => { |
| 100 | - const newDate = new Date(baseDate.value); | ||
| 101 | - if (currentDateType.value === 'week') { | ||
| 102 | - newDate.setDate(newDate.getDate() + 7); | ||
| 103 | - } else { | ||
| 104 | - newDate.setMonth(newDate.getMonth() + 1); | ||
| 105 | - } | ||
| 106 | - | ||
| 107 | - // 重置时分秒进行比较 | ||
| 108 | - const now = new Date(); | ||
| 109 | - now.setHours(0, 0, 0, 0); | ||
| 110 | - const checkDate = new Date(newDate); | ||
| 111 | - checkDate.setHours(0, 0, 0, 0); | ||
| 112 | - | ||
| 113 | - return checkDate > now; | 67 | + const nextBase = currentDateType.value === 'week' |
| 68 | + ? baseDate.value.add(7, 'day') | ||
| 69 | + : baseDate.value.add(1, 'month'); | ||
| 70 | + return nextBase.startOf('day').isAfter(dayjs().startOf('day')); | ||
| 114 | }); | 71 | }); |
| 115 | 72 | ||
| 116 | -// 切换周/月 | 73 | +// ==================== 事件处理 ==================== |
| 74 | + | ||
| 117 | const switchDateType = (type) => { | 75 | const switchDateType = (type) => { |
| 118 | currentDateType.value = type; | 76 | currentDateType.value = type; |
| 119 | emit('dateTypeChange', type); | 77 | emit('dateTypeChange', type); |
| 120 | }; | 78 | }; |
| 121 | 79 | ||
| 122 | -// 上一段时间 | ||
| 123 | const handlePrev = () => { | 80 | const handlePrev = () => { |
| 124 | - const newDate = new Date(baseDate.value); | ||
| 125 | - if (currentDateType.value === 'week') { | ||
| 126 | - newDate.setDate(newDate.getDate() - 7); | ||
| 127 | - } else { | ||
| 128 | - newDate.setMonth(newDate.getMonth() - 1); | ||
| 129 | - } | ||
| 130 | - baseDate.value = newDate; | 81 | + baseDate.value = currentDateType.value === 'week' |
| 82 | + ? baseDate.value.subtract(7, 'day') | ||
| 83 | + : baseDate.value.subtract(1, 'month'); | ||
| 131 | }; | 84 | }; |
| 132 | 85 | ||
| 133 | -// 下一段时间 | ||
| 134 | const handleNext = () => { | 86 | const handleNext = () => { |
| 135 | - // 虽然按钮已隐藏,但保留逻辑判断以防直接调用或其他边界情况 | ||
| 136 | - if (isNextDisabled.value) { | ||
| 137 | - return; | ||
| 138 | - } | ||
| 139 | - | ||
| 140 | - const newDate = new Date(baseDate.value); | ||
| 141 | - if (currentDateType.value === 'week') { | ||
| 142 | - newDate.setDate(newDate.getDate() + 7); | ||
| 143 | - } else { | ||
| 144 | - newDate.setMonth(newDate.getMonth() + 1); | ||
| 145 | - } | ||
| 146 | - | ||
| 147 | - baseDate.value = newDate; | 87 | + if (isNextDisabled.value) return; |
| 88 | + baseDate.value = currentDateType.value === 'week' | ||
| 89 | + ? baseDate.value.add(7, 'day') | ||
| 90 | + : baseDate.value.add(1, 'month'); | ||
| 148 | }; | 91 | }; |
| 149 | 92 | ||
| 93 | +// ==================== 双向绑定 ==================== | ||
| 94 | + | ||
| 150 | watch( | 95 | watch( |
| 151 | [baseDate, currentDateType], | 96 | [baseDate, currentDateType], |
| 152 | () => { | 97 | () => { |
| 153 | emit('dateRangeChange', { | 98 | emit('dateRangeChange', { |
| 154 | startDate: currentDateRange.value.start, | 99 | startDate: currentDateRange.value.start, |
| 155 | - endDate: currentDateRange.value.end | 100 | + endDate: currentDateRange.value.end, |
| 156 | }); | 101 | }); |
| 157 | }, | 102 | }, |
| 158 | - { immediate: true } | 103 | + { immediate: true }, |
| 159 | ); | 104 | ); |
| 160 | </script> | 105 | </script> |
| 161 | 106 | ||
| @@ -180,10 +125,6 @@ watch( | @@ -180,10 +125,6 @@ watch( | ||
| 180 | padding: 6rpx; | 125 | padding: 6rpx; |
| 181 | box-sizing: border-box; | 126 | box-sizing: border-box; |
| 182 | 127 | ||
| 183 | - // #ifdef H5 | ||
| 184 | - margin-top: 30px; | ||
| 185 | - // #endif | ||
| 186 | - | ||
| 187 | .tab-item { | 128 | .tab-item { |
| 188 | display: flex; | 129 | display: flex; |
| 189 | align-items: center; | 130 | align-items: center; |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="exercise-page" @click="closeAddMenu"> | ||
| 3 | - <!-- 搜索框 --> | ||
| 4 | - <view class="search-bar"> | ||
| 5 | - <!-- <view class="search"> | ||
| 6 | - <uni-icons type="search" size="18"></uni-icons> | ||
| 7 | - <input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" /> | ||
| 8 | - </view> --> | ||
| 9 | - <view class="add-btn" @click.stop="addExercise"> | ||
| 10 | - <uni-icons type="plus" size="35" color="#333"></uni-icons> | ||
| 11 | - <view class="floating-menu" v-show="addshow" @click.stop> | ||
| 12 | - <view class="menu-item"> | ||
| 13 | - <uni-icons type="plus" size="24" color="#333"></uni-icons> | ||
| 14 | - <text class="menu-text" @click="addnewmotion">新增动作</text> | 2 | + <view class="exercise-page"> |
| 3 | + <!-- 顶部操作栏:搜索 + 快捷新增 --> | ||
| 4 | + <view class="top-bar"> | ||
| 5 | + <view class="search-wrapper"> | ||
| 6 | + <up-search v-model="searchKeyword" placeholder="搜索动作名称" :showAction="false" shape="round" bgColor="#f3f4f6" | ||
| 7 | + placeholderColor="#9ca3af" searchIconColor="#6b7280" @clear="searchKeyword = ''" /> | ||
| 15 | </view> | 8 | </view> |
| 16 | - <view class="menu-item"> | ||
| 17 | - <uni-icons type="link" size="24" color="#333"></uni-icons> | ||
| 18 | - <text class="menu-text" @click="addnewsupersets">新增超级组</text> | 9 | + |
| 10 | + <view class="add-btn-wrapper"> | ||
| 11 | + <view class="add-icon-btn" @click.stop="addShow = !addShow"> | ||
| 12 | + <up-icon name="plus" size="20" color="#111827" bold /> | ||
| 19 | </view> | 13 | </view> |
| 20 | - <view class="menu-item"> | ||
| 21 | - <uni-icons type="list" size="24" color="#333"></uni-icons> | ||
| 22 | - <text class="menu-text" @click="actionmanagement">动作目录管理</text> | 14 | + |
| 15 | + <!-- 下拉浮动菜单 --> | ||
| 16 | + <view class="floating-menu" v-if="addShow" @click.stop> | ||
| 17 | + <view class="menu-item" @click="addnewmotion"> | ||
| 18 | + <up-icon name="plus-circle" size="18" color="#10b981" /> | ||
| 19 | + <text class="menu-text">新增动作</text> | ||
| 23 | </view> | 20 | </view> |
| 21 | + <view class="menu-item" @click="addnewsupersets"> | ||
| 22 | + <up-icon name="attach" size="18" color="#3b82f6" /> | ||
| 23 | + <text class="menu-text">新增超级组</text> | ||
| 24 | </view> | 24 | </view> |
| 25 | + <view class="menu-item" @click="actionmanagement"> | ||
| 26 | + <up-icon name="list-dot" size="18" color="#f59e0b" /> | ||
| 27 | + <text class="menu-text">动作目录管理</text> | ||
| 25 | </view> | 28 | </view> |
| 26 | </view> | 29 | </view> |
| 27 | 30 | ||
| 28 | - <!-- 浮动菜单 --> | 31 | + <view class="menu-mask" v-if="addShow" @click="addShow = false" /> |
| 32 | + </view> | ||
| 33 | + </view> | ||
| 29 | 34 | ||
| 30 | - <!-- 左右布局 --> | 35 | + <!-- 主体左右双栏布局 --> |
| 31 | <view class="layout-container"> | 36 | <view class="layout-container"> |
| 32 | - <scroll-view scroll-y class="left-nav" enable-flex> | ||
| 33 | - <view class="nav-item" @click="handleCollectClick('collect')" :class="{ active: activeNav === 'collect' }"> | ||
| 34 | - 收藏 | 37 | + <!-- 左侧一级分类导航 --> |
| 38 | + <scroll-view scroll-y class="left-nav" :show-scrollbar="false"> | ||
| 39 | + <view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick"> | ||
| 40 | + <up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon> | ||
| 41 | + <text class="nav-text">收藏</text> | ||
| 35 | </view> | 42 | </view> |
| 43 | + | ||
| 36 | <view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }" | 44 | <view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }" |
| 37 | @click="switchNav(nav.id)"> | 45 | @click="switchNav(nav.id)"> |
| 38 | - {{ nav.name }} | 46 | + <text class="nav-text">{{ nav.name }}</text> |
| 39 | </view> | 47 | </view> |
| 40 | </scroll-view> | 48 | </scroll-view> |
| 41 | 49 | ||
| 42 | - <!-- 小动作列表 --> | ||
| 43 | - <scroll-view scroll-y class="right-content" enable-flex> | ||
| 44 | - <view class="tip" v-if="motionPart.length > 0"> | ||
| 45 | - <view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view> | ||
| 46 | - <view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }" | ||
| 47 | - @click="handlePartClick(item.id)"> | 50 | + <!-- 右侧二级筛选与动作网格内容 --> |
| 51 | + <scroll-view scroll-y class="right-content" :show-scrollbar="false"> | ||
| 52 | + <!-- 二级子部位 Chips 筛选条 --> | ||
| 53 | + <scroll-view scroll-x class="sub-part-scroll" v-if="motionPart.length > 0"> | ||
| 54 | + <view class="sub-part-wrapper"> | ||
| 55 | + <view class="part-chip" :class="{ active: activeMotionPart === '' }" @click="handlePartClick('')"> | ||
| 56 | + 全部 | ||
| 57 | + </view> | ||
| 58 | + <view v-for="item in motionPart" :key="item.id" class="part-chip" | ||
| 59 | + :class="{ active: activeMotionPart === item.id }" @click="handlePartClick(item.id)"> | ||
| 48 | {{ item.name }} | 60 | {{ item.name }} |
| 49 | </view> | 61 | </view> |
| 50 | </view> | 62 | </view> |
| 63 | + </scroll-view> | ||
| 51 | 64 | ||
| 65 | + <!-- 动作与超级组网格卡片区 --> | ||
| 52 | <view class="exercise-grid"> | 66 | <view class="exercise-grid"> |
| 53 | - <view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0"> | ||
| 54 | - <view class="equipment-list"> | ||
| 55 | - <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId"> | ||
| 56 | - <view class="equipment-name"> {{ item.equipmentName }} </view> | ||
| 57 | - <view class="action-list"> | ||
| 58 | - <view class="action-item" v-for="e in item.exercises" :key="e.id"> | ||
| 59 | - <image :src="e.url3dAnimation" mode="aspectFill" lazy-load class="action-img" | ||
| 60 | - @click="goToDetail(e.id, 1)"></image> | ||
| 61 | - <view class="action-name">{{ e.name }}</view> | ||
| 62 | - <view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view> | 67 | + <template v-if="displayExercises.length > 0 || superGroupInfo.length > 0"> |
| 68 | + <!-- 器械分组列表 --> | ||
| 69 | + <view class="equipment-group" v-for="item in displayExercises" :key="item.equipmentId"> | ||
| 70 | + <view class="equipment-title"> | ||
| 71 | + <view class="title-bar"></view> | ||
| 72 | + <text>{{ item.equipmentName || '通用器械' }}</text> | ||
| 63 | </view> | 73 | </view> |
| 74 | + | ||
| 75 | + <view class="action-grid"> | ||
| 76 | + <view class="action-card" v-for="e in item.exercises" :key="e.id" @click="goToDetail(e.id, 1)"> | ||
| 77 | + <view class="image-wrapper"> | ||
| 78 | + <image :src="e.url3dAnimation" mode="aspectFil" lazy-load class="action-img"></image> | ||
| 79 | + <view class="reps-badge" v-if="e.trainingReps"> | ||
| 80 | + {{ e.trainingReps }}次 | ||
| 64 | </view> | 81 | </view> |
| 65 | </view> | 82 | </view> |
| 66 | - | ||
| 67 | - <view class="supers" v-if="superGroupInfo.length > 0"> | ||
| 68 | - <view class="supers-name"> 超级组 </view> | ||
| 69 | - <view class="super" v-for="item in superGroupInfo" :key="item.id" @click="goToDetail(item.id, 2)"> | ||
| 70 | - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png" | ||
| 71 | - mode="aspectFill" lazy-load class="super-img"></image> | ||
| 72 | - <view class="right"> | ||
| 73 | - <view class="super-name">{{ item.name }}</view> | ||
| 74 | - <view class="parts">{{ item.primaryMuscles.join() }}</view> | 83 | + <view class="action-info"> |
| 84 | + <text class="action-name">{{ e.name }}</text> | ||
| 75 | </view> | 85 | </view> |
| 76 | </view> | 86 | </view> |
| 77 | </view> | 87 | </view> |
| 88 | + </view> | ||
| 89 | + | ||
| 90 | + <!-- 超级组展示模块 --> | ||
| 91 | + <view class="supers-group" v-if="superGroupInfo.length > 0"> | ||
| 92 | + <view class="equipment-title"> | ||
| 93 | + <view class="title-bar blue"></view> | ||
| 94 | + <text>超级组</text> | ||
| 95 | + </view> | ||
| 78 | 96 | ||
| 79 | - <!-- <view class="add-super"> | ||
| 80 | - <view class="header"> | ||
| 81 | - <up-icon name="plus" color="#000" size="15" bold></up-icon> | ||
| 82 | - <text>添加自定义动作</text> | 97 | + <view class="super-card" v-for="item in superGroupInfo" :key="item.id" @click="goToDetail(item.id, 2)"> |
| 98 | + <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png" | ||
| 99 | + mode="aspectFill" lazy-load class="super-img"></image> | ||
| 100 | + <view class="super-info"> | ||
| 101 | + <text class="super-name">{{ item.name }}</text> | ||
| 102 | + <text class="super-muscles" v-if="item.primaryMuscles"> | ||
| 103 | + {{ Array.isArray(item.primaryMuscles) ? item.primaryMuscles.join(' · ') : item.primaryMuscles }} | ||
| 104 | + </text> | ||
| 83 | </view> | 105 | </view> |
| 84 | - <view class="tip">添加官方库没有的动作</view> | ||
| 85 | - </view> --> | 106 | + <up-icon name="arrow-right" size="14" color="#9ca3af"></up-icon> |
| 86 | </view> | 107 | </view> |
| 87 | </view> | 108 | </view> |
| 109 | + </template> | ||
| 88 | 110 | ||
| 89 | - <!-- 空状态 --> | ||
| 90 | - <view class="empty-state" v-else> | ||
| 91 | - <text class="empty-text">暂无动作数据</text> | 111 | + <!-- 优雅的空状态 --> |
| 112 | + <view class="empty-wrapper" v-else> | ||
| 113 | + <up-empty mode="data" text="暂无相关动作数据"></up-empty> | ||
| 92 | </view> | 114 | </view> |
| 93 | </view> | 115 | </view> |
| 116 | + | ||
| 117 | + | ||
| 94 | </scroll-view> | 118 | </scroll-view> |
| 95 | </view> | 119 | </view> |
| 96 | 120 | ||
| 97 | - <!-- 详情弹窗 --> | 121 | + <!-- 动作详情弹窗组件 --> |
| 98 | <dongzuoXianqing ref="actionDetailRef" /> | 122 | <dongzuoXianqing ref="actionDetailRef" /> |
| 99 | - | ||
| 100 | - <!-- 底部安全区占位(关键!解决不同设备适配) --> | ||
| 101 | - <view class="safe-area-bottom"></view> | ||
| 102 | </view> | 123 | </view> |
| 103 | </template> | 124 | </template> |
| 104 | 125 | ||
| 105 | <script setup> | 126 | <script setup> |
| 106 | -import { ref, onMounted, nextTick } from 'vue'; | 127 | +import { ref, onMounted, computed } from 'vue'; |
| 107 | import ExercisesApi from '@/sheep/api/motion/exercises'; | 128 | import ExercisesApi from '@/sheep/api/motion/exercises'; |
| 108 | import SupersetsApi from '@/sheep/api/motion/supersets'; | 129 | import SupersetsApi from '@/sheep/api/motion/supersets'; |
| 109 | import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue'; | 130 | import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue'; |
| @@ -111,29 +132,35 @@ import { useActionStore } from '@/sheep/store/action'; | @@ -111,29 +132,35 @@ import { useActionStore } from '@/sheep/store/action'; | ||
| 111 | 132 | ||
| 112 | const actionStore = useActionStore(); | 133 | const actionStore = useActionStore(); |
| 113 | 134 | ||
| 114 | -// 响应式数据 | ||
| 115 | const activeNav = ref(''); | 135 | const activeNav = ref(''); |
| 116 | const navItems = ref([]); | 136 | const navItems = ref([]); |
| 117 | const exercises = ref([]); | 137 | const exercises = ref([]); |
| 118 | const superGroupInfo = ref([]); | 138 | const superGroupInfo = ref([]); |
| 119 | const motionPart = ref([]); | 139 | const motionPart = ref([]); |
| 120 | const activeMotionPart = ref(''); | 140 | const activeMotionPart = ref(''); |
| 121 | -const currentCategoryId = ref(''); | ||
| 122 | - | ||
| 123 | -const allExercises = ref([]); | ||
| 124 | - | ||
| 125 | -const addshow = ref(false); | ||
| 126 | - | ||
| 127 | -const superFavoriteList = ref([]); | ||
| 128 | - | 141 | +const searchKeyword = ref(''); |
| 142 | +const addShow = ref(false); | ||
| 129 | const actionDetailRef = ref(null); | 143 | const actionDetailRef = ref(null); |
| 130 | 144 | ||
| 131 | -// 关闭菜单 | ||
| 132 | -const closeAddMenu = () => { | ||
| 133 | - addshow.value = false; | ||
| 134 | -}; | 145 | +const displayExercises = computed(() => { |
| 146 | + if (!searchKeyword.value.trim()) { | ||
| 147 | + return exercises.value; | ||
| 148 | + } | ||
| 149 | + const kw = searchKeyword.value.toLowerCase().trim(); | ||
| 150 | + | ||
| 151 | + return exercises.value | ||
| 152 | + .map((group) => { | ||
| 153 | + const filtered = (group.exercises || []).filter((ex) => | ||
| 154 | + ex.name && ex.name.toLowerCase().includes(kw) | ||
| 155 | + ); | ||
| 156 | + return { | ||
| 157 | + ...group, | ||
| 158 | + exercises: filtered, | ||
| 159 | + }; | ||
| 160 | + }) | ||
| 161 | + .filter((group) => group.exercises && group.exercises.length > 0); | ||
| 162 | +}); | ||
| 135 | 163 | ||
| 136 | -// 部位筛选 | ||
| 137 | const handlePartClick = async (id) => { | 164 | const handlePartClick = async (id) => { |
| 138 | try { | 165 | try { |
| 139 | activeMotionPart.value = id; | 166 | activeMotionPart.value = id; |
| @@ -143,37 +170,40 @@ const handlePartClick = async (id) => { | @@ -143,37 +170,40 @@ const handlePartClick = async (id) => { | ||
| 143 | }); | 170 | }); |
| 144 | exercises.value = exerciseRes.data || []; | 171 | exercises.value = exerciseRes.data || []; |
| 145 | } catch (e) { | 172 | } catch (e) { |
| 146 | - console.error('加载动作失败:', e); | 173 | + console.error('加载子部位动作失败:', e); |
| 147 | } | 174 | } |
| 148 | }; | 175 | }; |
| 149 | 176 | ||
| 150 | -// 收藏 | ||
| 151 | -const handleCollectClick = async (id) => { | ||
| 152 | - activeNav.value = id; | 177 | +const handleCollectClick = () => { |
| 178 | + activeNav.value = 'collect'; | ||
| 179 | + activeMotionPart.value = ''; | ||
| 180 | + motionPart.value = []; | ||
| 153 | loadCollectList(); | 181 | loadCollectList(); |
| 154 | loadSuperFavoriteList(); | 182 | loadSuperFavoriteList(); |
| 155 | }; | 183 | }; |
| 156 | -// 动作收藏 | 184 | + |
| 185 | +// 加载动作收藏列表 | ||
| 157 | const loadCollectList = async () => { | 186 | const loadCollectList = async () => { |
| 158 | try { | 187 | try { |
| 159 | const res = await ExercisesApi.getFavoriteExercises(); | 188 | const res = await ExercisesApi.getFavoriteExercises(); |
| 160 | exercises.value = res.data || []; | 189 | exercises.value = res.data || []; |
| 161 | } catch (err) { | 190 | } catch (err) { |
| 162 | - console.error('加载动作收藏失败', err); | 191 | + console.error('加载动作收藏失败:', err); |
| 163 | } | 192 | } |
| 164 | }; | 193 | }; |
| 165 | -// 超级组收藏 | 194 | + |
| 195 | +// 加载超级组收藏列表 | ||
| 166 | const loadSuperFavoriteList = async () => { | 196 | const loadSuperFavoriteList = async () => { |
| 167 | try { | 197 | try { |
| 168 | const res = await SupersetsApi.getFavoriteSuperset(); | 198 | const res = await SupersetsApi.getFavoriteSuperset(); |
| 169 | superGroupInfo.value = res.data || []; | 199 | superGroupInfo.value = res.data || []; |
| 170 | } catch (err) { | 200 | } catch (err) { |
| 171 | - superFavoriteList.value = []; | ||
| 172 | - console.error('加载超级组收藏失败', err); | 201 | + superGroupInfo.value = []; |
| 202 | + console.error('加载超级组收藏失败:', err); | ||
| 173 | } | 203 | } |
| 174 | }; | 204 | }; |
| 175 | 205 | ||
| 176 | -// 加载分类 | 206 | +// 加载大类 |
| 177 | const loadCategories = async () => { | 207 | const loadCategories = async () => { |
| 178 | try { | 208 | try { |
| 179 | await actionStore.getloadCategories(); | 209 | await actionStore.getloadCategories(); |
| @@ -187,26 +217,27 @@ const loadCategories = async () => { | @@ -187,26 +217,27 @@ const loadCategories = async () => { | ||
| 187 | } | 217 | } |
| 188 | }; | 218 | }; |
| 189 | 219 | ||
| 190 | -// 加载动作 | 220 | +// 加载动作列表 |
| 191 | const loadExercises = async (categoriesId) => { | 221 | const loadExercises = async (categoriesId) => { |
| 192 | try { | 222 | try { |
| 193 | const partRes = await ExercisesApi.getMotionPart(categoriesId); | 223 | const partRes = await ExercisesApi.getMotionPart(categoriesId); |
| 194 | motionPart.value = partRes.data || []; | 224 | motionPart.value = partRes.data || []; |
| 195 | - console.log(motionPart.value, 'motionPart.value'); | ||
| 196 | 225 | ||
| 197 | const exerciseRes = await ExercisesApi.getexercises({ categoriesId }); | 226 | const exerciseRes = await ExercisesApi.getexercises({ categoriesId }); |
| 198 | - exercises.value = exerciseRes.data; | 227 | + exercises.value = exerciseRes.data || []; |
| 199 | } catch (error) { | 228 | } catch (error) { |
| 200 | console.error('加载动作失败:', error); | 229 | console.error('加载动作失败:', error); |
| 201 | } | 230 | } |
| 202 | }; | 231 | }; |
| 203 | 232 | ||
| 204 | -// 切换导航 | 233 | +// 切换一级导航 |
| 205 | const switchNav = (id) => { | 234 | const switchNav = (id) => { |
| 206 | if (activeNav.value === id) return; | 235 | if (activeNav.value === id) return; |
| 207 | activeNav.value = id; | 236 | activeNav.value = id; |
| 208 | activeMotionPart.value = ''; | 237 | activeMotionPart.value = ''; |
| 209 | - if (id == 'super') { | 238 | + searchKeyword.value = ''; |
| 239 | + | ||
| 240 | + if (id === 'super') { | ||
| 210 | exercises.value = []; | 241 | exercises.value = []; |
| 211 | motionPart.value = []; | 242 | motionPart.value = []; |
| 212 | loadsupersetsinfo(); | 243 | loadsupersetsinfo(); |
| @@ -216,21 +247,7 @@ const switchNav = (id) => { | @@ -216,21 +247,7 @@ const switchNav = (id) => { | ||
| 216 | } | 247 | } |
| 217 | }; | 248 | }; |
| 218 | 249 | ||
| 219 | -// 搜索 | ||
| 220 | -const onSearch = (e) => { | ||
| 221 | - const searchText = e.detail.value.trim(); | ||
| 222 | - if (!currentCategoryId.value) return; | ||
| 223 | - | ||
| 224 | - searchText ? searchExercises(searchText) : loadExercises(currentCategoryId.value); | ||
| 225 | -}; | ||
| 226 | - | ||
| 227 | -const searchExercises = (keyword) => { | ||
| 228 | - const key = keyword.toLowerCase().trim(); | ||
| 229 | - exercises.value = allExercises.value.filter((item) => { | ||
| 230 | - return item.name && item.name.toLowerCase().includes(key); | ||
| 231 | - }); | ||
| 232 | -}; | ||
| 233 | - | 250 | +// 加载超级组列表 |
| 234 | const loadsupersetsinfo = async () => { | 251 | const loadsupersetsinfo = async () => { |
| 235 | try { | 252 | try { |
| 236 | const response = await SupersetsApi.getsupersets(); | 253 | const response = await SupersetsApi.getsupersets(); |
| @@ -240,31 +257,22 @@ const loadsupersetsinfo = async () => { | @@ -240,31 +257,22 @@ const loadsupersetsinfo = async () => { | ||
| 240 | } | 257 | } |
| 241 | }; | 258 | }; |
| 242 | 259 | ||
| 243 | -// 页面跳转 | ||
| 244 | const goToDetail = (id, type) => { | 260 | const goToDetail = (id, type) => { |
| 245 | - console.log('type=', type); | ||
| 246 | - nextTick(() => { | ||
| 247 | - actionDetailRef.value.open(id, type); | ||
| 248 | - }); | ||
| 249 | -}; | ||
| 250 | - | ||
| 251 | -const addExercise = () => { | ||
| 252 | - addshow.value = !addshow.value; | 261 | + actionDetailRef.value?.open(id, type); |
| 253 | }; | 262 | }; |
| 254 | 263 | ||
| 255 | const addnewmotion = () => { | 264 | const addnewmotion = () => { |
| 265 | + addShow.value = false; | ||
| 256 | uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' }); | 266 | uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' }); |
| 257 | }; | 267 | }; |
| 258 | 268 | ||
| 259 | const addnewsupersets = () => { | 269 | const addnewsupersets = () => { |
| 260 | - uni.navigateTo({ | ||
| 261 | - url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent( | ||
| 262 | - JSON.stringify(navItems.value), | ||
| 263 | - )}`, | ||
| 264 | - }); | 270 | + addShow.value = false; |
| 271 | + uni.navigateTo({ url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(JSON.stringify(navItems.value))}` }); | ||
| 265 | }; | 272 | }; |
| 266 | 273 | ||
| 267 | const actionmanagement = () => { | 274 | const actionmanagement = () => { |
| 275 | + addShow.value = false; | ||
| 268 | uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' }); | 276 | uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' }); |
| 269 | }; | 277 | }; |
| 270 | 278 | ||
| @@ -272,352 +280,324 @@ onMounted(() => { | @@ -272,352 +280,324 @@ onMounted(() => { | ||
| 272 | loadCategories(); | 280 | loadCategories(); |
| 273 | }); | 281 | }); |
| 274 | </script> | 282 | </script> |
| 283 | + | ||
| 275 | <style lang="scss" scoped> | 284 | <style lang="scss" scoped> |
| 276 | .exercise-page { | 285 | .exercise-page { |
| 277 | - background-color: white; | ||
| 278 | - box-sizing: border-box; | ||
| 279 | - height: 100vh; | ||
| 280 | - display: flex; | ||
| 281 | - flex-direction: column; | ||
| 282 | - box-sizing: border-box; | ||
| 283 | - | ||
| 284 | - .search-bar { | ||
| 285 | - box-sizing: border-box; | ||
| 286 | - width: 100%; | ||
| 287 | - display: flex; | ||
| 288 | - align-items: center; | ||
| 289 | - justify-content: flex-end; | ||
| 290 | - padding: 20rpx; | ||
| 291 | - // background-color: #f5f5f5; | ||
| 292 | - border-radius: 12rpx; | ||
| 293 | - margin-top: 20rpx; | 286 | + height: 87vh; |
| 287 | + // #ifdef H5 | ||
| 288 | + height: calc(100vh - 44px); | ||
| 289 | + // #endif | ||
| 294 | 290 | ||
| 295 | - /* #ifdef H5 */ | ||
| 296 | - margin-top: 20px; | ||
| 297 | - /* #endif */ | 291 | + background-color: #ffffff; |
| 292 | + overflow: hidden; | ||
| 298 | 293 | ||
| 299 | - .search { | 294 | + /* 顶部搜索操作栏 */ |
| 295 | + .top-bar { | ||
| 300 | display: flex; | 296 | display: flex; |
| 301 | align-items: center; | 297 | align-items: center; |
| 302 | - padding: 10rpx 20rpx; | ||
| 303 | - flex: 1; | ||
| 304 | - border-radius: 30rpx; | ||
| 305 | - background-color: #f5f5f5; | 298 | + padding: 16rpx 24rpx; |
| 299 | + background-color: #ffffff; | ||
| 300 | + border-bottom: 1rpx solid #f3f4f6; | ||
| 301 | + z-index: 50; | ||
| 306 | 302 | ||
| 307 | - .search-icon { | ||
| 308 | - width: 30rpx; | ||
| 309 | - height: 30rpx; | ||
| 310 | - } | ||
| 311 | - | ||
| 312 | - .search-input { | 303 | + .search-wrapper { |
| 313 | flex: 1; | 304 | flex: 1; |
| 314 | - padding-left: 15rpx; | ||
| 315 | - font-size: 26rpx; | ||
| 316 | - color: #333; | ||
| 317 | - border: none; | ||
| 318 | - outline: none; | ||
| 319 | - background: transparent; | ||
| 320 | - } | 305 | + margin-right: 20rpx; |
| 321 | } | 306 | } |
| 322 | 307 | ||
| 323 | - .add-btn { | ||
| 324 | - width: 60rpx; | ||
| 325 | - height: 60rpx; | ||
| 326 | - background-color: transparent; | ||
| 327 | - border: none; | ||
| 328 | - margin-left: 10rpx; | 308 | + .add-btn-wrapper { |
| 309 | + position: relative; | ||
| 310 | + | ||
| 311 | + .add-icon-btn { | ||
| 312 | + width: 72rpx; | ||
| 313 | + height: 72rpx; | ||
| 314 | + border-radius: 50%; | ||
| 315 | + background-color: #f3f4f6; | ||
| 329 | display: flex; | 316 | display: flex; |
| 330 | justify-content: center; | 317 | justify-content: center; |
| 331 | align-items: center; | 318 | align-items: center; |
| 332 | - z-index: 99; | ||
| 333 | - position: relative; | 319 | + transition: background-color 0.2s; |
| 334 | 320 | ||
| 335 | - .plus-icon { | ||
| 336 | - width: 40rpx; | ||
| 337 | - height: 40rpx; | 321 | + &:active { |
| 322 | + background-color: #e5e7eb; | ||
| 323 | + } | ||
| 338 | } | 324 | } |
| 339 | 325 | ||
| 340 | .floating-menu { | 326 | .floating-menu { |
| 341 | position: absolute; | 327 | position: absolute; |
| 342 | - top: 30rpx; | ||
| 343 | - right: 30rpx; | ||
| 344 | - width: 300rpx; | ||
| 345 | - background-color: #fff; | ||
| 346 | - border-radius: 12rpx; | ||
| 347 | - box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); | ||
| 348 | - padding: 20rpx; | 328 | + top: 84rpx; |
| 329 | + right: 0; | ||
| 330 | + width: 320rpx; | ||
| 331 | + background-color: #ffffff; | ||
| 332 | + border-radius: 20rpx; | ||
| 333 | + box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.12); | ||
| 334 | + padding: 12rpx 0; | ||
| 349 | z-index: 100; | 335 | z-index: 100; |
| 336 | + border: 1rpx solid #f3f4f6; | ||
| 350 | 337 | ||
| 351 | .menu-item { | 338 | .menu-item { |
| 352 | display: flex; | 339 | display: flex; |
| 353 | align-items: center; | 340 | align-items: center; |
| 354 | - padding: 20rpx 0; | ||
| 355 | - border-bottom: 1rpx solid #eee; | 341 | + gap: 16rpx; |
| 342 | + padding: 24rpx 28rpx; | ||
| 343 | + transition: background-color 0.15s; | ||
| 356 | 344 | ||
| 357 | - .u-icon { | ||
| 358 | - margin-right: 16rpx; | 345 | + &:active { |
| 346 | + background-color: #f9fafb; | ||
| 359 | } | 347 | } |
| 360 | 348 | ||
| 361 | .menu-text { | 349 | .menu-text { |
| 362 | font-size: 28rpx; | 350 | font-size: 28rpx; |
| 363 | - color: #333; | 351 | + color: #1f2937; |
| 352 | + font-weight: 500; | ||
| 364 | } | 353 | } |
| 365 | - | ||
| 366 | - &:last-child { | ||
| 367 | - border-bottom: none; | ||
| 368 | } | 354 | } |
| 369 | } | 355 | } |
| 356 | + | ||
| 357 | + .menu-mask { | ||
| 358 | + position: fixed; | ||
| 359 | + top: 0; | ||
| 360 | + left: 0; | ||
| 361 | + right: 0; | ||
| 362 | + bottom: 0; | ||
| 363 | + z-index: 90; | ||
| 364 | + background-color: transparent; | ||
| 370 | } | 365 | } |
| 371 | } | 366 | } |
| 372 | } | 367 | } |
| 373 | 368 | ||
| 374 | .layout-container { | 369 | .layout-container { |
| 375 | - box-sizing: border-box; | 370 | + height: 100%; |
| 376 | display: flex; | 371 | display: flex; |
| 377 | - height: calc(82vh + 25rpx); | ||
| 378 | - background-color: #f5f5f5; | 372 | + overflow: hidden; |
| 379 | 373 | ||
| 380 | .left-nav { | 374 | .left-nav { |
| 381 | - width: 160rpx; | ||
| 382 | - background-color: white; | ||
| 383 | - border-right: 1px solid #eee; | ||
| 384 | - // 添加底部 padding,避免最后一项被底部导航栏遮挡 | ||
| 385 | - padding-bottom: 20rpx; | ||
| 386 | - // #ifdef MP-WEIXIN | ||
| 387 | - // 微信小程序端可能需要更多底部空间 | ||
| 388 | - padding-bottom: 200rpx; | 375 | + width: 180rpx; |
| 376 | + height: 100%; | ||
| 377 | + background-color: #f9fafb; | ||
| 378 | + border-right: 1rpx solid #f3f4f6; | ||
| 389 | 379 | ||
| 390 | - // #endif | ||
| 391 | .nav-item { | 380 | .nav-item { |
| 392 | - padding: 24rpx 0; | ||
| 393 | - text-align: center; | 381 | + display: flex; |
| 382 | + align-items: center; | ||
| 383 | + justify-content: center; | ||
| 384 | + gap: 8rpx; | ||
| 385 | + padding: 32rpx 16rpx; | ||
| 386 | + position: relative; | ||
| 387 | + transition: all 0.2s; | ||
| 388 | + | ||
| 389 | + .nav-text { | ||
| 394 | font-size: 26rpx; | 390 | font-size: 26rpx; |
| 395 | - color: #333; | 391 | + color: #4b5563; |
| 392 | + font-weight: 400; | ||
| 393 | + text-align: center; | ||
| 394 | + line-height: 1.3; | ||
| 396 | } | 395 | } |
| 397 | 396 | ||
| 398 | - .active { | ||
| 399 | - color: #16ad40; | ||
| 400 | - background-color: #e1f6e9; | ||
| 401 | - border-right: 3px solid #16ad40; | ||
| 402 | - font-weight: bold; | 397 | + &.active { |
| 398 | + background-color: #ffffff; | ||
| 399 | + | ||
| 400 | + .nav-text { | ||
| 401 | + color: #10b981; | ||
| 402 | + font-weight: 600; | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + &::before { | ||
| 406 | + content: ''; | ||
| 407 | + position: absolute; | ||
| 408 | + left: 0; | ||
| 409 | + top: 24rpx; | ||
| 410 | + bottom: 24rpx; | ||
| 411 | + width: 6rpx; | ||
| 412 | + background-color: #10b981; | ||
| 413 | + border-radius: 0 4rpx 4rpx 0; | ||
| 414 | + } | ||
| 415 | + } | ||
| 403 | } | 416 | } |
| 404 | } | 417 | } |
| 405 | 418 | ||
| 419 | + /* 右侧内容滚动区 */ | ||
| 406 | .right-content { | 420 | .right-content { |
| 407 | - box-sizing: border-box; | ||
| 408 | - flex: 1; | ||
| 409 | - padding: 20rpx; | 421 | + width: calc(100% - 180rpx); |
| 410 | height: 100%; | 422 | height: 100%; |
| 411 | - // 添加底部 padding,避免内容被底部导航栏遮挡 | ||
| 412 | - // 底部导航栏高度约 100rpx(50px),加上安全区域 | ||
| 413 | - padding-bottom: 20rpx; | ||
| 414 | - // #ifdef MP-WEIXIN | ||
| 415 | - // 微信小程序端可能需要更多底部空间 | ||
| 416 | - padding-bottom: 100rpx; | ||
| 417 | - | ||
| 418 | - // #endif | ||
| 419 | - .tip { | ||
| 420 | - display: flex; | ||
| 421 | - padding-left: 20rpx; | ||
| 422 | - align-items: center; | ||
| 423 | - column-gap: 15rpx; | ||
| 424 | - margin-bottom: 20rpx; | 423 | + background-color: #efefef; |
| 424 | + padding: 20rpx 24rpx 200rpx; | ||
| 425 | + box-sizing: border-box; | ||
| 425 | 426 | ||
| 426 | - .item { | ||
| 427 | - margin: 0; | ||
| 428 | - padding: 12rpx 20rpx; | ||
| 429 | - font-size: 22rpx; | ||
| 430 | - line-height: 1; | ||
| 431 | - background-color: #fff; | ||
| 432 | - border-radius: 20rpx; | 427 | + /* 二级子部位芯片滚动条 */ |
| 428 | + .sub-part-scroll { | ||
| 429 | + width: 100%; | ||
| 430 | + white-space: nowrap; | ||
| 431 | + margin-bottom: 24rpx; | ||
| 432 | + | ||
| 433 | + .sub-part-wrapper { | ||
| 434 | + display: inline-flex; | ||
| 435 | + gap: 16rpx; | ||
| 436 | + padding-bottom: 4rpx; | ||
| 437 | + | ||
| 438 | + .part-chip { | ||
| 439 | + display: inline-block; | ||
| 440 | + padding: 10rpx 24rpx; | ||
| 441 | + font-size: 24rpx; | ||
| 442 | + color: #4b5563; | ||
| 443 | + background-color: #f3f4f6; | ||
| 444 | + border-radius: 30rpx; | ||
| 445 | + transition: all 0.2s; | ||
| 433 | 446 | ||
| 434 | &.active { | 447 | &.active { |
| 435 | - background-color: #d8ede0; | ||
| 436 | - color: #43b05e; | 448 | + background-color: #ecfdf5; |
| 449 | + color: #10b981; | ||
| 450 | + font-weight: 600; | ||
| 451 | + } | ||
| 437 | } | 452 | } |
| 438 | } | 453 | } |
| 439 | } | 454 | } |
| 440 | 455 | ||
| 456 | + /* 动作网格区 */ | ||
| 441 | .exercise-grid { | 457 | .exercise-grid { |
| 442 | display: flex; | 458 | display: flex; |
| 443 | flex-direction: column; | 459 | flex-direction: column; |
| 444 | - gap: 24rpx; | 460 | + gap: 32rpx; |
| 445 | 461 | ||
| 446 | - .title { | 462 | + .equipment-group { |
| 463 | + .equipment-title { | ||
| 464 | + display: flex; | ||
| 465 | + align-items: center; | ||
| 466 | + gap: 12rpx; | ||
| 467 | + font-size: 28rpx; | ||
| 447 | font-weight: 600; | 468 | font-weight: 600; |
| 448 | - padding-left: 20rpx; | ||
| 449 | - font-size: 30rpx; | ||
| 450 | - } | ||
| 451 | - | ||
| 452 | - .content { | ||
| 453 | - width: 100%; | ||
| 454 | - | ||
| 455 | - // display: grid; | ||
| 456 | - // grid-template-columns: repeat(2, 1fr); | ||
| 457 | - // /* 改为3列布局 */ | ||
| 458 | - // gap: 16rpx; | ||
| 459 | - | ||
| 460 | - // .exercise-item { | ||
| 461 | - // display: flex; | ||
| 462 | - // height: 150rpx; | ||
| 463 | - // /* 减小项目高度 */ | ||
| 464 | - // flex-direction: column; | ||
| 465 | - // align-items: center; | ||
| 466 | - // justify-content: center; | ||
| 467 | - // background-color: white; | ||
| 468 | - // padding: 16rpx 8rpx; | ||
| 469 | - // box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08); | ||
| 470 | - | ||
| 471 | - // .exercise-img { | ||
| 472 | - // width: 90%; | ||
| 473 | - // height: 140rpx; | ||
| 474 | - // /* 固定图片高度 */ | ||
| 475 | - // border-radius: 8rpx; | ||
| 476 | - // margin-bottom: 12rpx; | ||
| 477 | - // } | ||
| 478 | - | ||
| 479 | - // .exercise-title { | ||
| 480 | - // font-size: 24rpx; | ||
| 481 | - // color: #333; | ||
| 482 | - // text-align: center; | ||
| 483 | - // line-height: 1.3; | ||
| 484 | - // max-width: 100%; | ||
| 485 | - | ||
| 486 | - // text-overflow: ellipsis; | ||
| 487 | - // display: -webkit-box; | ||
| 488 | - // -webkit-box-orient: vertical; | ||
| 489 | - // } | ||
| 490 | - // } | ||
| 491 | - .equipment-item { | ||
| 492 | - width: 100%; | ||
| 493 | - // margin-bottom: 40rpx; | ||
| 494 | - | ||
| 495 | - .equipment-name { | ||
| 496 | - color: #000; | ||
| 497 | - font-size: 32rpx; | 469 | + color: #111827; |
| 498 | margin-bottom: 20rpx; | 470 | margin-bottom: 20rpx; |
| 499 | 471 | ||
| 472 | + .title-bar { | ||
| 473 | + width: 8rpx; | ||
| 474 | + height: 28rpx; | ||
| 475 | + background-color: #10b981; | ||
| 476 | + border-radius: 4rpx; | ||
| 500 | 477 | ||
| 501 | - &:last-child { | ||
| 502 | - margin-bottom: 50rpx; | 478 | + &.blue { |
| 479 | + background-color: #3b82f6; | ||
| 480 | + } | ||
| 503 | } | 481 | } |
| 504 | } | 482 | } |
| 505 | 483 | ||
| 506 | - .action-list { | 484 | + .action-grid { |
| 507 | display: grid; | 485 | display: grid; |
| 508 | grid-template-columns: repeat(2, 1fr); | 486 | grid-template-columns: repeat(2, 1fr); |
| 509 | - grid-gap: 10rpx; | ||
| 510 | - | ||
| 511 | - .action-item { | 487 | + gap: 15rpx; |
| 488 | + | ||
| 489 | + .action-card { | ||
| 490 | + background-color: #ffffff; | ||
| 491 | + border-radius: 16rpx; | ||
| 492 | + border: 1rpx solid #f3f4f6; | ||
| 493 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03); | ||
| 494 | + overflow: hidden; | ||
| 512 | display: flex; | 495 | display: flex; |
| 513 | flex-direction: column; | 496 | flex-direction: column; |
| 514 | - justify-content: center; | ||
| 515 | - align-items: center; | ||
| 516 | - gap: 10rpx; | ||
| 517 | - background: #fff; | ||
| 518 | - border-radius: 10rpx; | ||
| 519 | - padding: 20rpx; | ||
| 520 | - box-sizing: border-box; | 497 | + transition: transform 0.15s; |
| 498 | + | ||
| 499 | + &:active { | ||
| 500 | + transform: scale(0.98); | ||
| 501 | + } | ||
| 502 | + | ||
| 503 | + .image-wrapper { | ||
| 521 | position: relative; | 504 | position: relative; |
| 505 | + width: 100%; | ||
| 506 | + height: 200rpx; | ||
| 507 | + background-color: #f9fafb; | ||
| 522 | 508 | ||
| 523 | .action-img { | 509 | .action-img { |
| 524 | width: 100%; | 510 | width: 100%; |
| 525 | - height: 250rpx; | ||
| 526 | - border-radius: 10rpx; | 511 | + height: 200rpx; |
| 527 | } | 512 | } |
| 528 | 513 | ||
| 529 | - .trainingReps { | 514 | + .reps-badge { |
| 530 | position: absolute; | 515 | position: absolute; |
| 531 | - top: 20rpx; | ||
| 532 | - right: 20rpx; | ||
| 533 | - z-index: 1; | ||
| 534 | - font-size: 22rpx; | 516 | + top: 12rpx; |
| 517 | + right: 12rpx; | ||
| 518 | + background: rgba(0, 0, 0, 0.6); | ||
| 519 | + backdrop-filter: blur(8px); | ||
| 520 | + color: #ffffff; | ||
| 521 | + font-size: 20rpx; | ||
| 522 | + padding: 4rpx 12rpx; | ||
| 523 | + border-radius: 20rpx; | ||
| 524 | + font-weight: 500; | ||
| 535 | } | 525 | } |
| 536 | - | ||
| 537 | - &:last-child { | ||
| 538 | - margin-bottom: 50rpx; | ||
| 539 | - padding-bottom: env(safe-area-inset-bottom); | ||
| 540 | } | 526 | } |
| 541 | 527 | ||
| 528 | + .action-info { | ||
| 529 | + padding: 16rpx 12rpx; | ||
| 530 | + display: flex; | ||
| 531 | + align-items: center; | ||
| 532 | + justify-content: center; | ||
| 533 | + | ||
| 534 | + .action-name { | ||
| 535 | + font-size: 24rpx; | ||
| 536 | + font-weight: 500; | ||
| 537 | + color: #1f2937; | ||
| 538 | + text-align: center; | ||
| 539 | + overflow: hidden; | ||
| 540 | + white-space: nowrap; | ||
| 541 | + text-overflow: ellipsis; | ||
| 542 | + width: 100%; | ||
| 542 | } | 543 | } |
| 543 | } | 544 | } |
| 544 | } | 545 | } |
| 545 | } | 546 | } |
| 546 | - | ||
| 547 | - .supers-name { | ||
| 548 | - color: #000; | ||
| 549 | - font-size: 32rpx; | ||
| 550 | - margin-bottom: 20rpx; | ||
| 551 | } | 547 | } |
| 552 | 548 | ||
| 553 | - .super { | 549 | + /* 超级组卡片样式 */ |
| 550 | + .supers-group { | ||
| 551 | + .super-card { | ||
| 554 | display: flex; | 552 | display: flex; |
| 555 | align-items: center; | 553 | align-items: center; |
| 556 | - background: #fff; | 554 | + background-color: #ffffff; |
| 555 | + border-radius: 16rpx; | ||
| 556 | + border: 1rpx solid #f3f4f6; | ||
| 557 | padding: 20rpx; | 557 | padding: 20rpx; |
| 558 | - box-sizing: border-box; | ||
| 559 | - border-radius: 10rpx; | 558 | + margin-bottom: 16rpx; |
| 559 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03); | ||
| 560 | gap: 20rpx; | 560 | gap: 20rpx; |
| 561 | - font-size: 26rpx; | ||
| 562 | - margin-bottom: 20rpx; | ||
| 563 | 561 | ||
| 564 | - .super-img { | ||
| 565 | - width: 120rpx; | ||
| 566 | - height: 120rpx; | ||
| 567 | - border-radius: 10rpx; | 562 | + &:active { |
| 563 | + background-color: #f9fafb; | ||
| 568 | } | 564 | } |
| 569 | 565 | ||
| 570 | - .super-name { | ||
| 571 | - margin-bottom: 10rpx; | ||
| 572 | - } | 566 | + .super-img { |
| 567 | + width: 100rpx; | ||
| 568 | + height: 100rpx; | ||
| 569 | + border-radius: 12rpx; | ||
| 570 | + background-color: #f3f4f6; | ||
| 571 | + flex-shrink: 0; | ||
| 573 | } | 572 | } |
| 574 | 573 | ||
| 575 | - .add-super { | ||
| 576 | - background: #fff; | ||
| 577 | - padding: 20rpx; | ||
| 578 | - box-sizing: border-box; | ||
| 579 | - border-radius: 10rpx; | ||
| 580 | - | ||
| 581 | - .header { | 574 | + .super-info { |
| 575 | + flex: 1; | ||
| 582 | display: flex; | 576 | display: flex; |
| 583 | - gap: 10rpx; | ||
| 584 | - align-items: center; | ||
| 585 | - margin-bottom: 20rpx; | 577 | + flex-direction: column; |
| 578 | + gap: 8rpx; | ||
| 579 | + | ||
| 580 | + .super-name { | ||
| 581 | + font-size: 28rpx; | ||
| 582 | + font-weight: 600; | ||
| 583 | + color: #111827; | ||
| 586 | } | 584 | } |
| 587 | 585 | ||
| 588 | - .tip { | ||
| 589 | - margin: 0rpx; | ||
| 590 | - padding: 0rpx; | 586 | + .super-muscles { |
| 591 | font-size: 22rpx; | 587 | font-size: 22rpx; |
| 588 | + color: #6b7280; | ||
| 592 | } | 589 | } |
| 593 | } | 590 | } |
| 594 | } | 591 | } |
| 595 | - | ||
| 596 | - .nav-item .category-group { | ||
| 597 | - /* 超级组特殊样式 */ | ||
| 598 | - background-color: #f0f9ff; | ||
| 599 | - font-weight: bold; | ||
| 600 | - border-top: 1px solid #eee; | ||
| 601 | } | 592 | } |
| 602 | 593 | ||
| 603 | - .empty-state { | ||
| 604 | - display: flex; | ||
| 605 | - justify-content: center; | ||
| 606 | - align-items: center; | ||
| 607 | - | ||
| 608 | - .empty-text { | ||
| 609 | - font-size: 28rpx; | ||
| 610 | - color: #999; | 594 | + .empty-wrapper { |
| 595 | + padding: 80rpx 0; | ||
| 611 | } | 596 | } |
| 612 | } | 597 | } |
| 598 | + | ||
| 599 | + | ||
| 613 | } | 600 | } |
| 614 | } | 601 | } |
| 615 | } | 602 | } |
| 616 | - | ||
| 617 | -.safe-area-bottom { | ||
| 618 | - height: env(safe-area-inset-bottom); // 适配iOS安全区 | ||
| 619 | - // 额外加上 Tabbar 的高度,你的 Tabbar 高度是 85px = 170rpx | ||
| 620 | - padding-bottom: 170rpx; | ||
| 621 | - box-sizing: border-box; | ||
| 622 | -} | ||
| 623 | </style> | 603 | </style> |
| 1 | -<!-- 第一层 模板大类 --> | ||
| 2 | <template> | 1 | <template> |
| 3 | - <view class="template-page" @click="closeAllDropdowns"> | ||
| 4 | - <!-- 筛选按钮 --> | ||
| 5 | - <view class="filter-section" @click.stop> | ||
| 6 | - <!-- 部位筛选 --> | 2 | + <view class="template-page"> |
| 3 | + <!-- 顶部筛选操作栏(固定不缩放) --> | ||
| 4 | + <view class="filter-section"> | ||
| 5 | + <!-- 部位筛选胶囊 --> | ||
| 7 | <view class="filter-wrapper"> | 6 | <view class="filter-wrapper"> |
| 8 | - <view class="filter-btn" :class="{ active: showPartDropdown }" @click="togglePartDropdown"> | 7 | + <view class="filter-btn" :class="{ active: showPartDropdown, selected: activePartId !== '0' }" |
| 8 | + @click.stop="togglePartDropdown"> | ||
| 9 | <text class="text">{{ activePart }}</text> | 9 | <text class="text">{{ activePart }}</text> |
| 10 | - <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="13"></uni-icons> | 10 | + <up-icon :name="showPartDropdown ? 'arrow-up-fill' : 'arrow-down-fill'" size="10" |
| 11 | + :color="showPartDropdown || activePartId !== '0' ? '#10b981' : '#6b7280'"></up-icon> | ||
| 11 | </view> | 12 | </view> |
| 13 | + | ||
| 12 | <!-- 部位下拉菜单 --> | 14 | <!-- 部位下拉菜单 --> |
| 13 | - <view class="dropdown-menu" v-if="showPartDropdown"> | ||
| 14 | - <view v-for="(item, index) in partList" :key="index" class="dropdown-item" | ||
| 15 | - :class="{ selected: activePart === item.title }" @click="selectPart(item)"> | ||
| 16 | - {{ item.title }} | 15 | + <view class="dropdown-menu" v-if="showPartDropdown" @click.stop> |
| 16 | + <view v-for="item in partList" :key="item.id" class="dropdown-item" | ||
| 17 | + :class="{ selected: activePartId === item.id }" @click="selectPart(item)"> | ||
| 18 | + <text>{{ item.title }}</text> | ||
| 19 | + <up-icon v-if="activePartId === item.id" name="checkbox-mark" size="14" color="#10b981" /> | ||
| 17 | </view> | 20 | </view> |
| 18 | </view> | 21 | </view> |
| 19 | </view> | 22 | </view> |
| 20 | 23 | ||
| 21 | - <!-- 场景筛选 --> | 24 | + <!-- 场景筛选胶囊 --> |
| 22 | <view class="filter-wrapper"> | 25 | <view class="filter-wrapper"> |
| 23 | - <view class="filter-btn" :class="{ active: showSceneDropdown }" @click="toggleSceneDropdown"> | 26 | + <view class="filter-btn" :class="{ active: showSceneDropdown, selected: activeSceneId !== '0' }" |
| 27 | + @click.stop="toggleSceneDropdown"> | ||
| 24 | <text class="text">{{ activeScene }}</text> | 28 | <text class="text">{{ activeScene }}</text> |
| 25 | - <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="13"></uni-icons> | 29 | + <up-icon :name="showSceneDropdown ? 'arrow-up-fill' : 'arrow-down-fill'" size="10" |
| 30 | + :color="showSceneDropdown || activeSceneId !== '0' ? '#10b981' : '#6b7280'"></up-icon> | ||
| 26 | </view> | 31 | </view> |
| 32 | + | ||
| 27 | <!-- 场景下拉菜单 --> | 33 | <!-- 场景下拉菜单 --> |
| 28 | - <view class="dropdown-menu" v-if="showSceneDropdown"> | ||
| 29 | - <view v-for="(item, index) in sceneList" :key="index" class="dropdown-item" | 34 | + <view class="dropdown-menu" v-if="showSceneDropdown" @click.stop> |
| 35 | + <view v-for="item in sceneList" :key="item.id" class="dropdown-item" | ||
| 30 | :class="{ selected: activeSceneId === item.id }" @click="selectScene(item)"> | 36 | :class="{ selected: activeSceneId === item.id }" @click="selectScene(item)"> |
| 31 | - {{ item.title }} | 37 | + <text>{{ item.title }}</text> |
| 38 | + <up-icon v-if="activeSceneId === item.id" name="checkbox-mark" size="14" color="#10b981" /> | ||
| 32 | </view> | 39 | </view> |
| 33 | </view> | 40 | </view> |
| 34 | </view> | 41 | </view> |
| 42 | + | ||
| 43 | + <!-- 下拉全屏遮罩蒙层 --> | ||
| 44 | + <view class="dropdown-mask" v-if="showPartDropdown || showSceneDropdown" @click="closeAllDropdowns"></view> | ||
| 35 | </view> | 45 | </view> |
| 36 | - <!-- 模板列表 (根据状态自动切换显示) --> | ||
| 37 | - <scroll-view class="template-list" enable-flex scroll-y> | ||
| 38 | - <!-- 用父容器包裹 v-if 分支,解决 key 冲突 --> | ||
| 39 | - <view v-if="!isFiltering" class="sub-template-list"> | ||
| 40 | - <view v-for="(item, index) in templateList" :key="index" class="template-item"> | ||
| 41 | - <image :src="item.urlCover" mode="aspectFill" class="template-img"></image> | ||
| 42 | - <view> | ||
| 43 | - <view class="template-content" @click="gototemplate(item)"> | ||
| 44 | - <view class="template-title">{{ item.name }}</view> | ||
| 45 | - <view class="template-count">{{ item.templatesCount }}个模板</view> | ||
| 46 | - <view class="template-desc">{{ item.description }}</view> | 46 | + |
| 47 | + <!-- 模板列表展示区(自适应滚动) --> | ||
| 48 | + <view class="list-container"> | ||
| 49 | + <scroll-view class="template-scroll" scroll-y :show-scrollbar="false"> | ||
| 50 | + <view class="scroll-inner"> | ||
| 51 | + <!-- 模式一:未筛选状态,展示【模板大类列表】 --> | ||
| 52 | + <template v-if="!isFiltering"> | ||
| 53 | + <view v-for="item in templateList" :key="item.id" class="template-card" @click="gototemplate(item)"> | ||
| 54 | + <image :src="item.urlCover" mode="aspectFill" class="cover-img" lazy-load /> | ||
| 55 | + <view class="card-content"> | ||
| 56 | + <view class="card-header"> | ||
| 57 | + <text class="title">{{ item.name }}</text> | ||
| 58 | + <text class="count-badge">{{ item.templatesCount || 0 }}个模板</text> | ||
| 47 | </view> | 59 | </view> |
| 60 | + <text class="desc" v-if="item.description">{{ item.description }}</text> | ||
| 48 | </view> | 61 | </view> |
| 49 | </view> | 62 | </view> |
| 63 | + </template> | ||
| 64 | + | ||
| 65 | + <!-- 模式二:已筛选状态,展示【条件检索后的细分模板列表】 --> | ||
| 66 | + <template v-else> | ||
| 67 | + <view v-for="template in filteredTemplateList" :key="template.id" class="template-card" | ||
| 68 | + @click="goToTemplateDetail(template)"> | ||
| 69 | + <image :src="template.urlCover" mode="aspectFill" class="cover-img" lazy-load /> | ||
| 70 | + <view class="card-content"> | ||
| 71 | + <text class="title">{{ template.name }}</text> | ||
| 72 | + | ||
| 73 | + <!-- 肌群说明 Chips --> | ||
| 74 | + <view class="muscle-tags"> | ||
| 75 | + <view class="tag primary" v-if="template.primaryMuscleNames && template.primaryMuscleNames.length"> | ||
| 76 | + <text class="tag-label">主要:</text> | ||
| 77 | + <text class="tag-val">{{ template.primaryMuscleNames.join(', ') }}</text> | ||
| 50 | </view> | 78 | </view> |
| 51 | 79 | ||
| 52 | - <!-- 用父容器包裹 v-else 分支,解决 key 冲突 --> | ||
| 53 | - <view v-else> | ||
| 54 | - <view v-for="(template, index) in filteredTemplateList" :key="index" class="template-item"> | ||
| 55 | - <image :src="template.urlCover" mode="aspectFill" class="template-img"></image> | ||
| 56 | - <view> | ||
| 57 | - <view class="template-content" @click="goToTemplateDetail(template)"> | ||
| 58 | - <view class="template-title">{{ template.name }}</view> | ||
| 59 | - <!-- 空数据判断移到了这里 --> | ||
| 60 | - <view class="template-count" v-if="template.primaryMuscles"> | ||
| 61 | - 主要训练部位:{{ template.primaryMuscleNames.join(', ') || '无' }} | ||
| 62 | - </view> | ||
| 63 | - <view class="template-desc" v-if="template.secondaryMuscles"> | ||
| 64 | - 次要训练部位:{{ template.secondaryMuscleNames.join(', ') || '无' }} | 80 | + <view class="tag secondary" |
| 81 | + v-if="template.secondaryMuscleNames && template.secondaryMuscleNames.length"> | ||
| 82 | + <text class="tag-label">次要:</text> | ||
| 83 | + <text class="tag-val">{{ template.secondaryMuscleNames.join(', ') }}</text> | ||
| 65 | </view> | 84 | </view> |
| 66 | </view> | 85 | </view> |
| 67 | </view> | 86 | </view> |
| 68 | </view> | 87 | </view> |
| 69 | 88 | ||
| 70 | - <!-- 空数据提示:移到 view-else 内部,且作为独立块 --> | ||
| 71 | - <view v-if="filteredTemplateList.length === 0" class="empty-tip"> | ||
| 72 | - 暂无符合条件的训练模板 | 89 | + <!-- 筛选无结果空状态 --> |
| 90 | + <view v-if="filteredTemplateList.length === 0" class="empty-wrapper"> | ||
| 91 | + <up-empty mode="search" text="暂无符合条件的训练模板"></up-empty> | ||
| 73 | </view> | 92 | </view> |
| 93 | + </template> | ||
| 74 | </view> | 94 | </view> |
| 95 | + | ||
| 96 | + <!-- 底部安全区垫高 --> | ||
| 97 | + <view class="bottom-spacer"></view> | ||
| 75 | </scroll-view> | 98 | </scroll-view> |
| 76 | </view> | 99 | </view> |
| 100 | + </view> | ||
| 77 | </template> | 101 | </template> |
| 78 | 102 | ||
| 79 | <script setup> | 103 | <script setup> |
| 80 | import { onMounted, ref } from 'vue'; | 104 | import { onMounted, ref } from 'vue'; |
| 81 | import TemplatesApi from '@/sheep/api/Template/Templates'; | 105 | import TemplatesApi from '@/sheep/api/Template/Templates'; |
| 82 | 106 | ||
| 107 | +// 列表响应式数据 | ||
| 83 | const templateList = ref([]); | 108 | const templateList = ref([]); |
| 84 | const filteredTemplateList = ref([]); | 109 | const filteredTemplateList = ref([]); |
| 85 | -// 2. 控制页面显示:false=显示大类,true=显示筛选后的模板 | 110 | + |
| 111 | +// 页面模式标记:false = 显示大类, true = 显示筛选后的细分模板 | ||
| 86 | const isFiltering = ref(false); | 112 | const isFiltering = ref(false); |
| 87 | -const activeSceneId = ref('0'); | ||
| 88 | 113 | ||
| 89 | -// 下拉框部位筛选相关状态 | 114 | +// 部位筛选状态 |
| 90 | const partList = ref([]); | 115 | const partList = ref([]); |
| 91 | -const rawPartData = ref([]); // 存储接口返回的原始部位数据 | 116 | +const rawPartData = ref([]); |
| 92 | const showPartDropdown = ref(false); | 117 | const showPartDropdown = ref(false); |
| 93 | const activePart = ref('部位'); | 118 | const activePart = ref('部位'); |
| 94 | -const activePartId = ref('0'); // 新增:默认选中"不限",id=0 | ||
| 95 | -// 场景筛选相关状态 | 119 | +const activePartId = ref('0'); // '0' 代表不限 |
| 120 | + | ||
| 121 | +// 场景筛选状态 | ||
| 96 | const sceneList = ref([ | 122 | const sceneList = ref([ |
| 97 | { id: '0', title: '不限' }, | 123 | { id: '0', title: '不限' }, |
| 98 | { id: '1', title: '健身房' }, | 124 | { id: '1', title: '健身房' }, |
| 99 | { id: '2', title: '仅哑铃' }, | 125 | { id: '2', title: '仅哑铃' }, |
| 100 | { id: '3', title: '仅哑铃+杠铃' }, | 126 | { id: '3', title: '仅哑铃+杠铃' }, |
| 101 | - // { id: 4, title: '办公室' }, | ||
| 102 | ]); | 127 | ]); |
| 103 | const showSceneDropdown = ref(false); | 128 | const showSceneDropdown = ref(false); |
| 104 | const activeScene = ref('场景'); | 129 | const activeScene = ref('场景'); |
| 130 | +const activeSceneId = ref('0'); // '0' 代表不限 | ||
| 131 | + | ||
| 132 | +// ====== API 数据加载 ====== | ||
| 105 | 133 | ||
| 106 | -// 获取模板大类列表 | ||
| 107 | -const TemplatesList = async () => { | 134 | +/** |
| 135 | + * 获取模板大类列表 | ||
| 136 | + */ | ||
| 137 | +const loadCategoryTemplates = async () => { | ||
| 108 | try { | 138 | try { |
| 109 | const response = await TemplatesApi.getTemplates(); | 139 | const response = await TemplatesApi.getTemplates(); |
| 110 | - templateList.value = response.data; | ||
| 111 | - console.log('模板大类接口返回数据', templateList.value) | 140 | + templateList.value = response.data || []; |
| 112 | } catch (error) { | 141 | } catch (error) { |
| 113 | - console.log('模板大类获取失败', error); | 142 | + console.error('模板大类获取失败:', error); |
| 114 | } | 143 | } |
| 115 | }; | 144 | }; |
| 116 | -// 获取所有细分锻炼部位(接口) | 145 | + |
| 146 | +/** | ||
| 147 | + * 获取所有细分锻炼部位 | ||
| 148 | + */ | ||
| 117 | const getPartCategories = async () => { | 149 | const getPartCategories = async () => { |
| 118 | try { | 150 | try { |
| 119 | const res = await TemplatesApi.getPartAllCategories(); | 151 | const res = await TemplatesApi.getPartAllCategories(); |
| 120 | if (res.code === 0) { | 152 | if (res.code === 0) { |
| 121 | - rawPartData.value = res.data; | ||
| 122 | - console.log('部位分类接口返回数据rawPartData.value:', rawPartData.value) | ||
| 123 | - // 下拉列表赋值 | 153 | + rawPartData.value = res.data || []; |
| 124 | partList.value = [ | 154 | partList.value = [ |
| 125 | - { title: '不限', id: '0' }, // 给"不限"加id='0',方便后续筛选 | ||
| 126 | - ...res.data.map(item => ({ | ||
| 127 | - title: item.name, // 接口返回的name作为显示标题 | ||
| 128 | - id: String(item.id) // 保存接口的id,用于后续筛选传参 | ||
| 129 | - })) | 155 | + { title: '不限', id: '0' }, |
| 156 | + ...rawPartData.value.map((item) => ({ | ||
| 157 | + title: item.name, | ||
| 158 | + id: String(item.id), | ||
| 159 | + })), | ||
| 130 | ]; | 160 | ]; |
| 131 | } | 161 | } |
| 132 | } catch (error) { | 162 | } catch (error) { |
| 133 | - console.log('部位列表获取失败', error); | 163 | + console.error('部位列表获取失败:', error); |
| 134 | } | 164 | } |
| 135 | }; | 165 | }; |
| 136 | -// 切换部位下拉菜单 | 166 | + |
| 167 | +// ====== 交互与筛选逻辑 ====== | ||
| 168 | + | ||
| 169 | +/** | ||
| 170 | + * 切换部位下拉框 | ||
| 171 | + */ | ||
| 137 | const togglePartDropdown = () => { | 172 | const togglePartDropdown = () => { |
| 138 | showPartDropdown.value = !showPartDropdown.value; | 173 | showPartDropdown.value = !showPartDropdown.value; |
| 139 | showSceneDropdown.value = false; | 174 | showSceneDropdown.value = false; |
| 140 | }; | 175 | }; |
| 141 | 176 | ||
| 142 | -// 切换场景下拉菜单 | 177 | +/** |
| 178 | + * 切换场景下拉框 | ||
| 179 | + */ | ||
| 143 | const toggleSceneDropdown = () => { | 180 | const toggleSceneDropdown = () => { |
| 144 | showSceneDropdown.value = !showSceneDropdown.value; | 181 | showSceneDropdown.value = !showSceneDropdown.value; |
| 145 | showPartDropdown.value = false; | 182 | showPartDropdown.value = false; |
| 146 | }; | 183 | }; |
| 147 | 184 | ||
| 148 | -// 选择部位 | 185 | +/** |
| 186 | + * 选择部位 | ||
| 187 | + */ | ||
| 149 | const selectPart = (item) => { | 188 | const selectPart = (item) => { |
| 150 | activePartId.value = item.id; | 189 | activePartId.value = item.id; |
| 151 | - // 判断是否是不限id=0 | ||
| 152 | - if (item.id === '0') { | ||
| 153 | - activePart.value = '部位'; | ||
| 154 | - } else { | ||
| 155 | - activePart.value = item.title; | ||
| 156 | - } | 190 | + activePart.value = item.id === '0' ? '部位' : item.title; |
| 157 | showPartDropdown.value = false; | 191 | showPartDropdown.value = false; |
| 158 | doFilter(); | 192 | doFilter(); |
| 159 | }; | 193 | }; |
| 160 | 194 | ||
| 161 | -// 选择场景 | 195 | +/** |
| 196 | + * 选择场景 | ||
| 197 | + */ | ||
| 162 | const selectScene = (item) => { | 198 | const selectScene = (item) => { |
| 163 | activeSceneId.value = item.id; | 199 | activeSceneId.value = item.id; |
| 164 | - // 判断是否是不限id=0 | ||
| 165 | - if (item.id === '0') { | ||
| 166 | - activeScene.value = '场景'; | ||
| 167 | - } else { | ||
| 168 | - activeScene.value = item.title; | ||
| 169 | - } | 200 | + activeScene.value = item.id === '0' ? '场景' : item.title; |
| 170 | showSceneDropdown.value = false; | 201 | showSceneDropdown.value = false; |
| 171 | doFilter(); | 202 | doFilter(); |
| 172 | }; | 203 | }; |
| 173 | 204 | ||
| 174 | -// 点击页面其他地方关闭所有下拉菜单 | 205 | +/** |
| 206 | + * 关闭所有下拉菜单 | ||
| 207 | + */ | ||
| 175 | const closeAllDropdowns = () => { | 208 | const closeAllDropdowns = () => { |
| 176 | showPartDropdown.value = false; | 209 | showPartDropdown.value = false; |
| 177 | showSceneDropdown.value = false; | 210 | showSceneDropdown.value = false; |
| 178 | }; | 211 | }; |
| 179 | -// 核心:执行筛选逻辑 | 212 | + |
| 213 | +/** | ||
| 214 | + * 执行筛选逻辑 | ||
| 215 | + */ | ||
| 180 | const doFilter = async () => { | 216 | const doFilter = async () => { |
| 181 | - const musclesId = activePartId.value; // 对应接口 musclesId | ||
| 182 | - const scene = activeSceneId.value; // 对应接口 scene | ||
| 183 | - // 2. 判断:是否是【全部不限】 | 217 | + const musclesId = activePartId.value; |
| 218 | + const scene = activeSceneId.value; | ||
| 219 | + | ||
| 220 | + // 条件判断:若均为"不限",切回展示【大类列表】 | ||
| 184 | if (musclesId === '0' && scene === '0') { | 221 | if (musclesId === '0' && scene === '0') { |
| 185 | - // 情况A:都不限 -> 恢复显示【模板大类】 | ||
| 186 | isFiltering.value = false; | 222 | isFiltering.value = false; |
| 187 | - // 重置按钮文字为默认 | ||
| 188 | activePart.value = '部位'; | 223 | activePart.value = '部位'; |
| 189 | activeScene.value = '场景'; | 224 | activeScene.value = '场景'; |
| 190 | - // 大类列表之前已经加载过了,直接显示 | ||
| 191 | return; | 225 | return; |
| 192 | } | 226 | } |
| 193 | - // 情况B:有筛选条件 -> 请求【筛选接口】 | 227 | + |
| 228 | + // 存在有效条件:发请求获取筛选列表 | ||
| 194 | try { | 229 | try { |
| 195 | - isFiltering.value = true; // 切换成模板列表模式 | 230 | + isFiltering.value = true; |
| 196 | const res = await TemplatesApi.queryTemplatesByCondition({ | 231 | const res = await TemplatesApi.queryTemplatesByCondition({ |
| 197 | - musclesId: musclesId, | ||
| 198 | - scene: scene | 232 | + musclesId, |
| 233 | + scene, | ||
| 199 | }); | 234 | }); |
| 200 | if (res.code === 0) { | 235 | if (res.code === 0) { |
| 201 | - // 把接口返回的模板数据存起来,页面会自动刷新 | ||
| 202 | - filteredTemplateList.value = res.data; | ||
| 203 | - console.log('筛选接口返回数据filteredTemplateList.value:', filteredTemplateList.value) | 236 | + filteredTemplateList.value = res.data || []; |
| 204 | } | 237 | } |
| 205 | } catch (error) { | 238 | } catch (error) { |
| 206 | - console.log('筛选失败', error); | ||
| 207 | - // 失败了也清空数据 | 239 | + console.error('筛选模板失败:', error); |
| 208 | filteredTemplateList.value = []; | 240 | filteredTemplateList.value = []; |
| 209 | } | 241 | } |
| 210 | }; | 242 | }; |
| 211 | -// 跳转到模板详情页,传入模板id | ||
| 212 | -// 跳转地址是F:\hongxing-app\hongxing-new\pages4\pages\xunji\xunji-moban-xiangqing.vue | ||
| 213 | -const goToTemplateDetail = (template) => { | 243 | + |
| 244 | +// ====== 路由跳转 ====== | ||
| 245 | + | ||
| 246 | +/** | ||
| 247 | + * 跳转至大类包含的模板列表 | ||
| 248 | + */ | ||
| 249 | +const gototemplate = (item) => { | ||
| 214 | uni.navigateTo({ | 250 | uni.navigateTo({ |
| 215 | - url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`, | 251 | + url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`, |
| 216 | }); | 252 | }); |
| 217 | }; | 253 | }; |
| 218 | -const gototemplate = (item) => { | ||
| 219 | - // 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4 | 254 | + |
| 255 | +/** | ||
| 256 | + * 跳转至具体模板详情页 | ||
| 257 | + */ | ||
| 258 | +const goToTemplateDetail = (template) => { | ||
| 220 | uni.navigateTo({ | 259 | uni.navigateTo({ |
| 221 | - url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`, | 260 | + url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`, |
| 222 | }); | 261 | }); |
| 223 | }; | 262 | }; |
| 224 | 263 | ||
| 225 | onMounted(async () => { | 264 | onMounted(async () => { |
| 226 | - await TemplatesList(); | ||
| 227 | - await getPartCategories(); | ||
| 228 | - console.log('进入模板大类详情'); | ||
| 229 | - | 265 | + await Promise.all([loadCategoryTemplates(), getPartCategories()]); |
| 230 | }); | 266 | }); |
| 231 | </script> | 267 | </script> |
| 232 | 268 | ||
| 233 | <style lang="scss" scoped> | 269 | <style lang="scss" scoped> |
| 234 | .template-page { | 270 | .template-page { |
| 235 | - width: 100vw; | ||
| 236 | - height: 100vh; | ||
| 237 | - box-sizing: border-box; | ||
| 238 | - | ||
| 239 | - .filter-section { | ||
| 240 | - position: fixed; | ||
| 241 | - // top: 80rpx; | 271 | + display: flex; |
| 272 | + flex-direction: column; | ||
| 273 | + height: calc(100vh - 44px); | ||
| 242 | // #ifdef MP-WEIXIN | 274 | // #ifdef MP-WEIXIN |
| 243 | - top: 234rpx; | 275 | + height: calc(100vh - 110px); |
| 244 | // #endif | 276 | // #endif |
| 277 | + width: 100vw; | ||
| 278 | + background-color: #f8fafc; | ||
| 279 | + overflow: hidden; | ||
| 245 | 280 | ||
| 246 | - /* #ifdef H5 */ | ||
| 247 | - top: 77px; | ||
| 248 | - /* #endif */ | ||
| 249 | - | ||
| 250 | - left: 0; | ||
| 251 | - right: 0; | ||
| 252 | - z-index: 999; | ||
| 253 | - | 281 | + /* 顶部筛选控制栏 */ |
| 282 | + .filter-section { | ||
| 283 | + flex-shrink: 0; | ||
| 284 | + position: relative; | ||
| 285 | + z-index: 50; | ||
| 254 | display: flex; | 286 | display: flex; |
| 255 | - gap: 24rpx; | ||
| 256 | - flex-wrap: wrap; | ||
| 257 | - padding: 20rpx; | ||
| 258 | - background-color: white; | 287 | + gap: 20rpx; |
| 288 | + padding: 20rpx 30rpx; | ||
| 289 | + background-color: #ffffff; | ||
| 290 | + border-bottom: 1rpx solid #f1f5f9; | ||
| 291 | + | ||
| 292 | + .filter-wrapper { | ||
| 293 | + position: relative; | ||
| 259 | 294 | ||
| 260 | .filter-btn { | 295 | .filter-btn { |
| 261 | display: flex; | 296 | display: flex; |
| 262 | align-items: center; | 297 | align-items: center; |
| 263 | - justify-content: center; | ||
| 264 | - width: auto; | ||
| 265 | - min-width: 70rpx; | ||
| 266 | - height: 50rpx; | ||
| 267 | - background: #fff; | ||
| 268 | - color: #333; | ||
| 269 | - font-size: 24rpx; | ||
| 270 | - border: 2rpx solid #ddd; | ||
| 271 | - border-radius: 25rpx; | ||
| 272 | - padding: 0 20rpx; | ||
| 273 | - white-space: nowrap; | 298 | + gap: 10rpx; |
| 299 | + height: 58rpx; | ||
| 300 | + padding: 0 24rpx; | ||
| 301 | + background-color: #f8fafc; | ||
| 302 | + border: 1rpx solid #e2e8f0; | ||
| 303 | + border-radius: 30rpx; | ||
| 304 | + transition: all 0.2s ease; | ||
| 305 | + | ||
| 306 | + .text { | ||
| 307 | + font-size: 26rpx; | ||
| 308 | + color: #334155; | ||
| 309 | + font-weight: 500; | ||
| 274 | } | 310 | } |
| 275 | 311 | ||
| 276 | - .filter-btn .text { | ||
| 277 | - margin-right: 8rpx; | ||
| 278 | - } | 312 | + &.active, |
| 313 | + &.selected { | ||
| 314 | + background-color: #ecfdf5; | ||
| 315 | + border-color: #10b981; | ||
| 279 | 316 | ||
| 280 | - .filter-btn.active { | ||
| 281 | - border-color: #43b05e; | ||
| 282 | - background: #e6f7f0; | ||
| 283 | - color: #43b05e; | 317 | + .text { |
| 318 | + color: #10b981; | ||
| 319 | + font-weight: 600; | ||
| 320 | + } | ||
| 284 | } | 321 | } |
| 285 | - | ||
| 286 | - .filter-wrapper { | ||
| 287 | - position: relative; | ||
| 288 | - z-index: 10; | ||
| 289 | } | 322 | } |
| 290 | 323 | ||
| 291 | - /* 下拉菜单样式 */ | 324 | + /* 下拉浮层 */ |
| 292 | .dropdown-menu { | 325 | .dropdown-menu { |
| 293 | position: absolute; | 326 | position: absolute; |
| 294 | - top: calc(100% + 8rpx); | 327 | + top: 72rpx; |
| 295 | left: 0; | 328 | left: 0; |
| 296 | - background: #fff; | ||
| 297 | - border-radius: 14rpx; | ||
| 298 | - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08); | ||
| 299 | - padding: 12rpx 0; | ||
| 300 | z-index: 100; | 329 | z-index: 100; |
| 301 | - min-width: 150rpx; | ||
| 302 | - max-height: 320rpx; | 330 | + min-width: 220rpx; |
| 331 | + max-height: 380rpx; | ||
| 332 | + background-color: #ffffff; | ||
| 333 | + border-radius: 16rpx; | ||
| 334 | + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1); | ||
| 335 | + border: 1rpx solid #f1f5f9; | ||
| 336 | + padding: 8rpx 0; | ||
| 303 | overflow-y: auto; | 337 | overflow-y: auto; |
| 304 | - border: 1rpx solid #f0f0f0; | ||
| 305 | - } | ||
| 306 | 338 | ||
| 307 | - /* 下拉菜单项样式 */ | ||
| 308 | .dropdown-item { | 339 | .dropdown-item { |
| 309 | - padding: 16rpx 24rpx; | 340 | + display: flex; |
| 341 | + align-items: center; | ||
| 342 | + justify-content: space-between; | ||
| 343 | + padding: 20rpx 28rpx; | ||
| 310 | font-size: 26rpx; | 344 | font-size: 26rpx; |
| 311 | - color: #333; | ||
| 312 | - white-space: nowrap; | 345 | + color: #334155; |
| 346 | + transition: background-color 0.15s; | ||
| 347 | + | ||
| 348 | + &:active { | ||
| 349 | + background-color: #f8fafc; | ||
| 313 | } | 350 | } |
| 314 | 351 | ||
| 315 | - /* 下拉菜单项悬停样式 */ | ||
| 316 | - .dropdown-item:hover { | ||
| 317 | - background-color: #43b05e; | 352 | + &.selected { |
| 353 | + background-color: #f0fdf4; | ||
| 354 | + color: #10b981; | ||
| 355 | + font-weight: 600; | ||
| 356 | + } | ||
| 357 | + } | ||
| 358 | + } | ||
| 318 | } | 359 | } |
| 319 | 360 | ||
| 320 | - /* 下拉菜单项选中样式 */ | ||
| 321 | - .dropdown-item.selected { | ||
| 322 | - background-color: #f0f9f4; | ||
| 323 | - color: #2e9d5a; | ||
| 324 | - font-weight: 500; | 361 | + /* 遮罩蒙层 */ |
| 362 | + .dropdown-mask { | ||
| 363 | + position: fixed; | ||
| 364 | + top: 0; | ||
| 365 | + left: 0; | ||
| 366 | + right: 0; | ||
| 367 | + bottom: 0; | ||
| 368 | + z-index: 40; | ||
| 369 | + background-color: rgba(0, 0, 0, 0.1); | ||
| 325 | } | 370 | } |
| 326 | } | 371 | } |
| 327 | 372 | ||
| 328 | - .template-list { | ||
| 329 | - margin-top: 120rpx; | ||
| 330 | - padding: 0 30rpx; | ||
| 331 | - box-sizing: border-box; | 373 | + /* 列表自适应滚动容器 */ |
| 374 | + .list-container { | ||
| 332 | flex: 1; | 375 | flex: 1; |
| 333 | - display: flex; | ||
| 334 | - flex-direction: column; | ||
| 335 | - | ||
| 336 | - // #ifdef MP-WEIXIN | ||
| 337 | - padding-bottom: 200rpx; | ||
| 338 | - height: calc(100vh - 274rpx); | ||
| 339 | - // #endif | 376 | + height: 0; |
| 377 | + /* 关键:接触 Flex 内容支撑限制 */ | ||
| 378 | + min-height: 0; | ||
| 379 | + overflow: hidden; | ||
| 340 | 380 | ||
| 341 | - /* #ifdef H5 */ | ||
| 342 | - margin-top: 87px; | ||
| 343 | - // 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px | ||
| 344 | - height: calc(100vh - 77px - 87px); | ||
| 345 | - /* #endif */ | 381 | + .template-scroll { |
| 382 | + width: 100%; | ||
| 383 | + height: 100%; | ||
| 346 | 384 | ||
| 347 | - .sub-template-list { | 385 | + .scroll-inner { |
| 386 | + padding: 24rpx 30rpx; | ||
| 348 | display: flex; | 387 | display: flex; |
| 349 | flex-direction: column; | 388 | flex-direction: column; |
| 350 | - gap: 10rpx; | ||
| 351 | - } | 389 | + gap: 20rpx; |
| 352 | 390 | ||
| 353 | - .template-item { | 391 | + /* 模板卡片组件样式 */ |
| 392 | + .template-card { | ||
| 354 | display: flex; | 393 | display: flex; |
| 355 | - background-color: white; | ||
| 356 | - border-radius: 16rpx; | ||
| 357 | - overflow: hidden; | ||
| 358 | - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); | 394 | + align-items: center; |
| 395 | + background-color: #ffffff; | ||
| 396 | + border-radius: 20rpx; | ||
| 359 | padding: 20rpx; | 397 | padding: 20rpx; |
| 398 | + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02); | ||
| 399 | + border: 1rpx solid #f1f5f9; | ||
| 400 | + transition: transform 0.15s; | ||
| 360 | 401 | ||
| 361 | - &:first-child { | ||
| 362 | - margin-top: 24rpx; | 402 | + &:active { |
| 403 | + transform: scale(0.985); | ||
| 404 | + background-color: #f8fafc; | ||
| 363 | } | 405 | } |
| 364 | 406 | ||
| 365 | - | ||
| 366 | - .template-img { | ||
| 367 | - width: 120rpx; | ||
| 368 | - height: 120rpx; | ||
| 369 | - border-radius: 12rpx; | 407 | + .cover-img { |
| 408 | + width: 128rpx; | ||
| 409 | + height: 128rpx; | ||
| 410 | + border-radius: 16rpx; | ||
| 370 | margin-right: 20rpx; | 411 | margin-right: 20rpx; |
| 412 | + background-color: #f1f5f9; | ||
| 413 | + flex-shrink: 0; | ||
| 371 | } | 414 | } |
| 372 | 415 | ||
| 373 | - .template-content { | 416 | + .card-content { |
| 374 | flex: 1; | 417 | flex: 1; |
| 418 | + display: flex; | ||
| 419 | + flex-direction: column; | ||
| 420 | + justify-content: center; | ||
| 421 | + overflow: hidden; | ||
| 422 | + | ||
| 423 | + .card-header { | ||
| 424 | + display: flex; | ||
| 425 | + align-items: center; | ||
| 426 | + justify-content: space-between; | ||
| 427 | + margin-bottom: 8rpx; | ||
| 428 | + | ||
| 429 | + .title { | ||
| 430 | + font-size: 30rpx; | ||
| 431 | + font-weight: 600; | ||
| 432 | + color: #0f172a; | ||
| 433 | + overflow: hidden; | ||
| 434 | + white-space: nowrap; | ||
| 435 | + text-overflow: ellipsis; | ||
| 436 | + flex: 1; | ||
| 437 | + margin-right: 12rpx; | ||
| 375 | } | 438 | } |
| 376 | 439 | ||
| 377 | - .template-title { | ||
| 378 | - font-size: 32rpx; | ||
| 379 | - color: #333; | ||
| 380 | - margin-bottom: 10rpx; | 440 | + .count-badge { |
| 441 | + font-size: 22rpx; | ||
| 442 | + color: #10b981; | ||
| 443 | + background-color: #ecfdf5; | ||
| 444 | + padding: 4rpx 12rpx; | ||
| 445 | + border-radius: 20rpx; | ||
| 446 | + font-weight: 500; | ||
| 447 | + flex-shrink: 0; | ||
| 448 | + } | ||
| 381 | } | 449 | } |
| 382 | 450 | ||
| 383 | - .template-count { | ||
| 384 | - font-size: 24rpx; | ||
| 385 | - color: #666; | ||
| 386 | - margin-bottom: 10rpx; | 451 | + .title { |
| 452 | + font-size: 30rpx; | ||
| 453 | + font-weight: 600; | ||
| 454 | + color: #0f172a; | ||
| 455 | + margin-bottom: 8rpx; | ||
| 387 | } | 456 | } |
| 388 | 457 | ||
| 389 | - .template-desc { | 458 | + .desc { |
| 390 | font-size: 24rpx; | 459 | font-size: 24rpx; |
| 391 | - color: #666; | 460 | + color: #64748b; |
| 392 | line-height: 1.4; | 461 | line-height: 1.4; |
| 462 | + display: -webkit-box; | ||
| 463 | + -webkit-box-orient: vertical; | ||
| 464 | + -webkit-line-clamp: 2; | ||
| 465 | + overflow: hidden; | ||
| 466 | + } | ||
| 467 | + | ||
| 468 | + /* 肌群 Tag 列表 */ | ||
| 469 | + .muscle-tags { | ||
| 470 | + display: flex; | ||
| 471 | + flex-direction: column; | ||
| 472 | + gap: 6rpx; | ||
| 473 | + margin-top: 4rpx; | ||
| 474 | + | ||
| 475 | + .tag { | ||
| 476 | + display: flex; | ||
| 477 | + align-items: center; | ||
| 478 | + font-size: 22rpx; | ||
| 479 | + line-height: 1.3; | ||
| 480 | + | ||
| 481 | + .tag-label { | ||
| 482 | + color: #94a3b8; | ||
| 483 | + flex-shrink: 0; | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + .tag-val { | ||
| 487 | + color: #475569; | ||
| 488 | + overflow: hidden; | ||
| 489 | + white-space: nowrap; | ||
| 490 | + text-overflow: ellipsis; | ||
| 393 | } | 491 | } |
| 394 | 492 | ||
| 395 | - &:last-child { | ||
| 396 | - margin-bottom: 5rpx; | 493 | + &.primary .tag-val { |
| 494 | + color: #059669; | ||
| 495 | + font-weight: 500; | ||
| 496 | + } | ||
| 497 | + } | ||
| 498 | + } | ||
| 499 | + } | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + .empty-wrapper { | ||
| 503 | + padding: 100rpx 0; | ||
| 504 | + } | ||
| 505 | + } | ||
| 397 | 506 | ||
| 398 | - /* #ifdef H5 */ | ||
| 399 | - margin-bottom: 30rpx; | ||
| 400 | - /* #endif */ | 507 | + .bottom-spacer { |
| 508 | + height: env(safe-area-inset-bottom); | ||
| 509 | + padding-bottom: 40rpx; | ||
| 401 | } | 510 | } |
| 402 | } | 511 | } |
| 403 | } | 512 | } |
| 1 | <template> | 1 | <template> |
| 2 | <view class="calendar-page"> | 2 | <view class="calendar-page"> |
| 3 | - <!-- 顶部栏:年月选择器和功能按钮 --> | 3 | + <!-- 顶部操作栏 --> |
| 4 | <view class="top-bar"> | 4 | <view class="top-bar"> |
| 5 | - <view class="month-picker"> | ||
| 6 | - <uni-datetime-picker type="date" v-model="currentMonth" :clear-icon="false" /> | ||
| 7 | - <up-icon name="arrow-down" color="#333" size="15"></up-icon> | 5 | + <!-- 年月选择胶囊 --> |
| 6 | + <picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange"> | ||
| 7 | + <view class="month-picker-pill"> | ||
| 8 | + <text class="month-text">{{ displayMonthText }}</text> | ||
| 9 | + <up-icon name="arrow-down-fill" color="#333333" size="10"></up-icon> | ||
| 8 | </view> | 10 | </view> |
| 11 | + </picker> | ||
| 12 | + | ||
| 13 | + <!-- 说明按钮 --> | ||
| 9 | <view class="action-buttons"> | 14 | <view class="action-buttons"> |
| 10 | - <button class="action-btn" @click="ShowExplainPopup = true">说明</button> | 15 | + <up-button text="说明" type="info" plain shape="circle" size="mini" |
| 16 | + customStyle="padding: 0 24rpx; height: 56rpx; font-size: 24rpx; border-color: #dcdfe6; color: #606266;" | ||
| 17 | + @click="showExplainPopup = true" /> | ||
| 11 | </view> | 18 | </view> |
| 12 | </view> | 19 | </view> |
| 13 | - <!-- 日历网格 --> | ||
| 14 | - <view class="calendar-grid"> | 20 | + |
| 21 | + <!-- 日历卡片容器 --> | ||
| 22 | + <view class="calendar-card"> | ||
| 23 | + <!-- 星期表头 --> | ||
| 15 | <view class="week-header"> | 24 | <view class="week-header"> |
| 16 | <text v-for="(day, index) in weekDays" :key="index" class="week-day"> | 25 | <text v-for="(day, index) in weekDays" :key="index" class="week-day"> |
| 17 | {{ day }} | 26 | {{ day }} |
| 18 | </text> | 27 | </text> |
| 19 | </view> | 28 | </view> |
| 29 | + | ||
| 30 | + <!-- 日期网格 (动态 5行 或 6行) --> | ||
| 20 | <view class="date-grid"> | 31 | <view class="date-grid"> |
| 21 | - <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{ today: date.today }" | ||
| 22 | - @tap="selectDate(date)"> | ||
| 23 | - <text v-if="!date.today" class="date-number">{{ date.day }}</text> | ||
| 24 | - <text v-else class="today-text">今</text> | 32 | + <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{ |
| 33 | + | ||
| 34 | + 'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0) | ||
| 35 | + }" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)"> | ||
| 36 | + <!-- 日期数字 / 今日徽章 --> | ||
| 37 | + <view class="date-header"> | ||
| 38 | + <text v-if="date.today" class="today-badge">今</text> | ||
| 39 | + <text v-else class="date-number"> | ||
| 40 | + {{ date.day }} | ||
| 41 | + </text> | ||
| 42 | + </view> | ||
| 25 | 43 | ||
| 26 | - <!-- 内容显示区域 --> | 44 | + <!-- 计划/容量数据列表 --> |
| 27 | <view class="plan-list" | 45 | <view class="plan-list" |
| 28 | v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)"> | 46 | v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)"> |
| 29 | - <template v-for="(item, idx) in getVisibleItems(date).items" :key="idx"> | ||
| 30 | - <view class="plan-name" :class="{ | ||
| 31 | - 'weight-item': item.type === 'weight', | ||
| 32 | - 'note-item': item.type === 'note' | ||
| 33 | - }" :style="item.bg ? { backgroundColor: item.bg } : ''"> | ||
| 34 | - {{ item.text }} | 47 | + <template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx"> |
| 48 | + <view class="plan-tag" :class="`tag-type-${item.type}`" | ||
| 49 | + :style="item.bg ? { backgroundColor: item.bg } : {}"> | ||
| 50 | + <text class="tag-text">{{ item.text }}</text> | ||
| 35 | </view> | 51 | </view> |
| 36 | </template> | 52 | </template> |
| 37 | 53 | ||
| 38 | - <!-- 统一只显示一个 + 剩余总数 --> | ||
| 39 | - <text v-if="getVisibleItems(date).remaining > 0" class="plan-extra"> | ||
| 40 | - +{{ getVisibleItems(date).remaining }} | ||
| 41 | - </text> | 54 | + <!-- 溢出剩余条数 --> |
| 55 | + <view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra"> | ||
| 56 | + +{{ getCellVisibleItems(date).remaining }} | ||
| 57 | + </view> | ||
| 42 | </view> | 58 | </view> |
| 43 | </view> | 59 | </view> |
| 44 | </view> | 60 | </view> |
| 45 | </view> | 61 | </view> |
| 46 | 62 | ||
| 47 | - <!-- <view class="gridCellContent" v-if="ShowGridCellPopup && selectedDate"> | ||
| 48 | - <GridCellContentPopup :date="selectedDate" @close="ShowGridCellPopup = false" /> | ||
| 49 | - </view> --> | ||
| 50 | - | 63 | + <!-- 单元格详情弹窗 --> |
| 51 | <GridCellContentPopup ref="gridPopupRef" :date="selectedDate" @refreshCalendar="handleRefreshCalendar" /> | 64 | <GridCellContentPopup ref="gridPopupRef" :date="selectedDate" @refreshCalendar="handleRefreshCalendar" /> |
| 52 | 65 | ||
| 53 | - <!-- 日历说明弹窗 --> | ||
| 54 | - <view class="explain-popup-overlay" v-if="ShowExplainPopup" @click.self="ShowExplainPopup = false"> | ||
| 55 | - <view class="explain-popup"> | 66 | + <!-- 日历图标说明弹窗 --> |
| 67 | + <up-popup :show="showExplainPopup" mode="bottom" round="24rpx" :safeAreaInsetBottom="true" | ||
| 68 | + @close="showExplainPopup = false"> | ||
| 69 | + <view class="explain-popup-content"> | ||
| 56 | <view class="explain-header"> | 70 | <view class="explain-header"> |
| 57 | - <text class="explain-title">日历说明</text> | ||
| 58 | - <view class="close-btn" @click="ShowExplainPopup = false"> | ||
| 59 | - <text>×</text> | ||
| 60 | - </view> | 71 | + <text class="explain-title">日历图标说明</text> |
| 72 | + <up-icon name="close" color="#909399" size="18" @click="showExplainPopup = false" /> | ||
| 61 | </view> | 73 | </view> |
| 74 | + | ||
| 62 | <view class="explain-list"> | 75 | <view class="explain-list"> |
| 63 | <view class="explain-item"> | 76 | <view class="explain-item"> |
| 64 | <view class="color-block blue"></view> | 77 | <view class="color-block blue"></view> |
| 65 | - <text class="item-label">分钟</text> | ||
| 66 | - <text class="item-desc">在场馆的运动时长</text> | 78 | + <view class="item-info"> |
| 79 | + <text class="item-label">时长</text> | ||
| 80 | + <text class="item-desc">在场馆的实际运动时长</text> | ||
| 81 | + </view> | ||
| 67 | </view> | 82 | </view> |
| 83 | + | ||
| 68 | <view class="explain-item"> | 84 | <view class="explain-item"> |
| 69 | <view class="color-block green"></view> | 85 | <view class="color-block green"></view> |
| 86 | + <view class="item-info"> | ||
| 70 | <text class="item-label">容量</text> | 87 | <text class="item-label">容量</text> |
| 71 | - <text class="item-desc">力量训练的总重量</text> | 88 | + <text class="item-desc">力量训练累计产生的总负重重量</text> |
| 89 | + </view> | ||
| 72 | </view> | 90 | </view> |
| 91 | + | ||
| 73 | <view class="explain-item"> | 92 | <view class="explain-item"> |
| 74 | <view class="color-block yellow"></view> | 93 | <view class="color-block yellow"></view> |
| 94 | + <view class="item-info"> | ||
| 75 | <text class="item-label">课程</text> | 95 | <text class="item-label">课程</text> |
| 76 | - <text class="item-desc">训记或已结课的线下课程</text> | 96 | + <text class="item-desc">训记完成或已结课的线下课程</text> |
| 77 | </view> | 97 | </view> |
| 98 | + </view> | ||
| 99 | + | ||
| 78 | <view class="explain-item"> | 100 | <view class="explain-item"> |
| 79 | <view class="color-block gray"></view> | 101 | <view class="color-block gray"></view> |
| 80 | - <text class="item-label">课程</text> | ||
| 81 | - <text class="item-desc">训记排课或待上课的线下课程</text> | 102 | + <view class="item-info"> |
| 103 | + <text class="item-label">排课</text> | ||
| 104 | + <text class="item-desc">训记排课计划或待上课的课程</text> | ||
| 82 | </view> | 105 | </view> |
| 83 | </view> | 106 | </view> |
| 84 | </view> | 107 | </view> |
| 85 | </view> | 108 | </view> |
| 109 | + </up-popup> | ||
| 86 | </view> | 110 | </view> |
| 87 | </template> | 111 | </template> |
| 88 | 112 | ||
| 89 | <script setup> | 113 | <script setup> |
| 90 | import { ref, watch, computed, nextTick } from 'vue'; | 114 | import { ref, watch, computed, nextTick } from 'vue'; |
| 115 | +import dayjs from 'dayjs'; | ||
| 91 | import DailyTemplateApi from '@/sheep/api/calendar/date'; | 116 | import DailyTemplateApi from '@/sheep/api/calendar/date'; |
| 92 | -import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue' | 117 | +import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue'; |
| 93 | 118 | ||
| 94 | -// ====== 响应式数据 ====== | 119 | +// 常量定义 |
| 120 | +const MAX_CELL_LINES = 3; | ||
| 95 | const weekDays = ref(['一', '二', '三', '四', '五', '六', '日']); | 121 | const weekDays = ref(['一', '二', '三', '四', '五', '六', '日']); |
| 122 | + | ||
| 123 | +// 响应式状态 | ||
| 96 | const dates = ref([]); | 124 | const dates = ref([]); |
| 97 | -const today = new Date(); | ||
| 98 | -const ShowExplainPopup = ref(false) | 125 | +const showExplainPopup = ref(false); |
| 99 | const selectedDate = ref(''); | 126 | const selectedDate = ref(''); |
| 100 | -// const ShowGridCellPopup = ref(false) | ||
| 101 | const gridPopupRef = ref(null); | 127 | const gridPopupRef = ref(null); |
| 102 | 128 | ||
| 103 | -// 自动获取当前年月 | ||
| 104 | -const now = new Date(); | ||
| 105 | -const currentMonth = ref( | ||
| 106 | - `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` | ||
| 107 | -); | 129 | +const currentMonth = ref(dayjs().format('YYYY-MM-DD')); |
| 108 | 130 | ||
| 109 | -// ====== 工具函数 ====== | ||
| 110 | -const formatDateKey = (y, m, d) => { | ||
| 111 | - return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; | ||
| 112 | -}; | 131 | +// 顶部年月显示 |
| 132 | +const displayMonthText = computed(() => { | ||
| 133 | + return dayjs(currentMonth.value).format('YYYY年M月'); | ||
| 134 | +}); | ||
| 113 | 135 | ||
| 114 | -const arrayToDateStr = (arr) => { | ||
| 115 | - if (!Array.isArray(arr) || arr.length !== 3) return null; | ||
| 116 | - const [y, m, d] = arr; | ||
| 117 | - return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; | 136 | +// 年月 Picker 触发 |
| 137 | +const onMonthChange = (e) => { | ||
| 138 | + if (e.detail && e.detail.value) { | ||
| 139 | + currentMonth.value = dayjs(e.detail.value).startOf('month').format('YYYY-MM-DD'); | ||
| 140 | + } | ||
| 118 | }; | 141 | }; |
| 119 | 142 | ||
| 120 | -// ====== 生成日历数据 ====== | ||
| 121 | -const generateCalendar = (year, month) => { | ||
| 122 | - const firstDay = new Date(year, month - 1, 1); | ||
| 123 | - const lastDay = new Date(year, month, 0).getDate(); | ||
| 124 | - let startWeekDay = firstDay.getDay(); | ||
| 125 | - const adjustedStart = startWeekDay === 0 ? 6 : startWeekDay - 1; | 143 | +// ====== 计算每个日期格子的背景颜色 ====== |
| 144 | +const getCellBgColor = (date) => { | ||
| 126 | 145 | ||
| 127 | - const calendarDates = []; | ||
| 128 | 146 | ||
| 129 | - // 上月填充 | ||
| 130 | - const prevMonthLastDay = new Date(year, month - 1, 0).getDate(); | ||
| 131 | - for (let i = adjustedStart; i > 0; i--) { | ||
| 132 | - const day = prevMonthLastDay - i + 1; | ||
| 133 | - const y = month === 1 ? year - 1 : year; | ||
| 134 | - const m = month === 1 ? 12 : month - 1; | ||
| 135 | - calendarDates.push({ | ||
| 136 | - day, | ||
| 137 | - today: false, | ||
| 138 | - dateKey: formatDateKey(y, m, day), | ||
| 139 | - }); | 147 | + // 2. 如果存在训练计划/备注/重量,优先采用训练本身的背景色 |
| 148 | + if (date.planList && date.planList.length > 0 && date.planList[0].bg) { | ||
| 149 | + return date.planList[0].bg; | ||
| 150 | + } | ||
| 151 | + if (date.noteList && date.noteList.length > 0 && date.noteList[0].backgroundColor) { | ||
| 152 | + return date.noteList[0].backgroundColor; | ||
| 140 | } | 153 | } |
| 141 | 154 | ||
| 142 | - // 本月 | ||
| 143 | - for (let d = 1; d <= lastDay; d++) { | ||
| 144 | - const isToday = | ||
| 145 | - year === today.getFullYear() && | ||
| 146 | - month === today.getMonth() + 1 && | ||
| 147 | - d === today.getDate(); | ||
| 148 | - calendarDates.push({ | ||
| 149 | - day: d, | ||
| 150 | - today: isToday, | ||
| 151 | - dateKey: formatDateKey(year, month, d), | ||
| 152 | - }); | 155 | + // 3. 有容量负重但无特定背景色:赋予柔和浅绿 |
| 156 | + if (date.weight && date.weight > 0) { | ||
| 157 | + return '#f0fdf4'; | ||
| 153 | } | 158 | } |
| 154 | 159 | ||
| 155 | - // 下月填充 | ||
| 156 | - const remaining = 42 - calendarDates.length; | ||
| 157 | - for (let d = 1; d <= remaining; d++) { | ||
| 158 | - const y = month === 12 ? year + 1 : year; | ||
| 159 | - const m = month === 12 ? 1 : month + 1; | 160 | + // 4. 有计划但未指定背景色:赋予柔和浅黄 |
| 161 | + if (date.planList && date.planList.length > 0) { | ||
| 162 | + return '#fefce8'; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + | ||
| 166 | + | ||
| 167 | + // 6. 普通无训练的本月休息日:基础卡片白色/浅灰 | ||
| 168 | + return '#ededed'; | ||
| 169 | +}; | ||
| 170 | + | ||
| 171 | + | ||
| 172 | +const generateCalendar = (monthStr) => { | ||
| 173 | + const current = dayjs(monthStr); | ||
| 174 | + const startOfMonth = current.startOf('month'); | ||
| 175 | + const daysInMonth = current.daysInMonth(); | ||
| 176 | + | ||
| 177 | + const dayOfWeek = startOfMonth.day(); // 0 表示周日 | ||
| 178 | + const startOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1; | ||
| 179 | + | ||
| 180 | + // 动态计算总单元格:35(5行)或 42(6行) | ||
| 181 | + const totalCellsNeeded = startOffset + daysInMonth; | ||
| 182 | + const totalGridSize = totalCellsNeeded > 35 ? 42 : 35; | ||
| 183 | + | ||
| 184 | + const gridStartDate = startOfMonth.subtract(startOffset, 'day'); | ||
| 185 | + const todayStr = dayjs().format('YYYY-MM-DD'); | ||
| 186 | + | ||
| 187 | + const calendarDates = []; | ||
| 188 | + for (let i = 0; i < totalGridSize; i++) { | ||
| 189 | + const cellDate = gridStartDate.add(i, 'day'); | ||
| 190 | + const dateKey = cellDate.format('YYYY-MM-DD'); | ||
| 191 | + | ||
| 160 | calendarDates.push({ | 192 | calendarDates.push({ |
| 161 | - day: d, | ||
| 162 | - today: false, | ||
| 163 | - dateKey: formatDateKey(y, m, d), | 193 | + day: cellDate.date(), |
| 194 | + today: dateKey === todayStr, | ||
| 195 | + isCurrentMonth: cellDate.month() === current.month(), | ||
| 196 | + dateKey, | ||
| 164 | }); | 197 | }); |
| 165 | } | 198 | } |
| 166 | 199 | ||
| @@ -168,26 +201,19 @@ const generateCalendar = (year, month) => { | @@ -168,26 +201,19 @@ const generateCalendar = (year, month) => { | ||
| 168 | }; | 201 | }; |
| 169 | 202 | ||
| 170 | // ====== 加载计划数据 ====== | 203 | // ====== 加载计划数据 ====== |
| 171 | -const loadPlans = async (year, month) => { | 204 | +const loadPlans = async (monthStr) => { |
| 172 | try { | 205 | try { |
| 173 | - // 关键:获取 上个月、当前月、下个月 → 保证前后日期都有数据 | ||
| 174 | - const prevMonth = month - 1 < 1 ? 12 : month - 1; | ||
| 175 | - const prevYear = month - 1 < 1 ? year - 1 : year; | ||
| 176 | - | ||
| 177 | - const nextMonth = month + 1 > 12 ? 1 : month + 1; | ||
| 178 | - const nextYear = month + 1 > 12 ? year + 1 : year; | ||
| 179 | - | ||
| 180 | - // 并行请求三个月数据 | ||
| 181 | - // const [prevRes, currRes, nextRes] = await Promise.all([ | ||
| 182 | - // DailyTemplateApi.getCalendarPlans(prevYear, prevMonth), | ||
| 183 | - // DailyTemplateApi.getCalendarPlans(year, month), | ||
| 184 | - // DailyTemplateApi.getCalendarPlans(nextYear, nextMonth), | ||
| 185 | - // ]); | ||
| 186 | - const prevRes = await DailyTemplateApi.getCalendarPlans(prevYear, prevMonth); | ||
| 187 | - const currRes = await DailyTemplateApi.getCalendarPlans(year, month); | ||
| 188 | - const nextRes = await DailyTemplateApi.getCalendarPlans(nextYear, nextMonth); | ||
| 189 | - | ||
| 190 | - // 合并三个月数据 | 206 | + const curr = dayjs(monthStr); |
| 207 | + const prev = curr.subtract(1, 'month'); | ||
| 208 | + const next = curr.add(1, 'month'); | ||
| 209 | + | ||
| 210 | + // 并行请求 3 个月数据 | ||
| 211 | + const [prevRes, currRes, nextRes] = await Promise.all([ | ||
| 212 | + DailyTemplateApi.getCalendarPlans(prev.year(), prev.month() + 1).catch(() => ({ data: [] })), | ||
| 213 | + DailyTemplateApi.getCalendarPlans(curr.year(), curr.month() + 1).catch(() => ({ data: [] })), | ||
| 214 | + DailyTemplateApi.getCalendarPlans(next.year(), next.month() + 1).catch(() => ({ data: [] })), | ||
| 215 | + ]); | ||
| 216 | + | ||
| 191 | const allPlans = [ | 217 | const allPlans = [ |
| 192 | ...(prevRes.data || []), | 218 | ...(prevRes.data || []), |
| 193 | ...(currRes.data || []), | 219 | ...(currRes.data || []), |
| @@ -199,24 +225,25 @@ const loadPlans = async (year, month) => { | @@ -199,24 +225,25 @@ const loadPlans = async (year, month) => { | ||
| 199 | const weightMap = {}; | 225 | const weightMap = {}; |
| 200 | 226 | ||
| 201 | allPlans.forEach((plan) => { | 227 | allPlans.forEach((plan) => { |
| 202 | - let dateStr; | 228 | + let dateStr = null; |
| 203 | if (Array.isArray(plan.trainDate)) { | 229 | if (Array.isArray(plan.trainDate)) { |
| 204 | - dateStr = arrayToDateStr(plan.trainDate); | 230 | + const [y, m, d] = plan.trainDate; |
| 231 | + dateStr = dayjs(`${y}-${m}-${d}`).format('YYYY-MM-DD'); | ||
| 205 | } else if (typeof plan.trainDate === 'string') { | 232 | } else if (typeof plan.trainDate === 'string') { |
| 206 | - dateStr = plan.trainDate; | 233 | + dateStr = dayjs(plan.trainDate).format('YYYY-MM-DD'); |
| 207 | } | 234 | } |
| 208 | 235 | ||
| 209 | - if (!dateStr) return; | 236 | + if (!dateStr || dateStr === 'Invalid Date') return; |
| 210 | 237 | ||
| 211 | - // 处理多条备注 | 238 | + // 备注处理 |
| 212 | if (Array.isArray(plan.notes) && plan.notes.length > 0) { | 239 | if (Array.isArray(plan.notes) && plan.notes.length > 0) { |
| 213 | - noteMap[dateStr] = plan.notes.map(note => ({ | 240 | + noteMap[dateStr] = plan.notes.map((note) => ({ |
| 214 | content: note.content || '', | 241 | content: note.content || '', |
| 215 | - backgroundColor: note.backgroundColor || '#ccc' | 242 | + backgroundColor: note.backgroundColor || '#e2e8f0', |
| 216 | })); | 243 | })); |
| 217 | } | 244 | } |
| 218 | 245 | ||
| 219 | - // 处理计划和重量 | 246 | + // 训练计划与容量处理 |
| 220 | planMap[dateStr] = planMap[dateStr] || []; | 247 | planMap[dateStr] = planMap[dateStr] || []; |
| 221 | if (Array.isArray(plan.templates)) { | 248 | if (Array.isArray(plan.templates)) { |
| 222 | let totalWeightForDay = 0; | 249 | let totalWeightForDay = 0; |
| @@ -224,7 +251,7 @@ const loadPlans = async (year, month) => { | @@ -224,7 +251,7 @@ const loadPlans = async (year, month) => { | ||
| 224 | if (template && template.name) { | 251 | if (template && template.name) { |
| 225 | planMap[dateStr].push({ | 252 | planMap[dateStr].push({ |
| 226 | name: template.name, | 253 | name: template.name, |
| 227 | - bg: template.backgroundColor || '#ffdd44' | 254 | + bg: template.backgroundColor || '#fef08a', |
| 228 | }); | 255 | }); |
| 229 | } | 256 | } |
| 230 | if (template && typeof template.totalWeight === 'number') { | 257 | if (template && typeof template.totalWeight === 'number') { |
| @@ -237,57 +264,37 @@ const loadPlans = async (year, month) => { | @@ -237,57 +264,37 @@ const loadPlans = async (year, month) => { | ||
| 237 | } | 264 | } |
| 238 | }); | 265 | }); |
| 239 | 266 | ||
| 240 | - const newDates = generateCalendar(year, month).map((date) => ({ | 267 | + dates.value = generateCalendar(monthStr).map((date) => ({ |
| 241 | ...date, | 268 | ...date, |
| 242 | planList: planMap[date.dateKey] || [], | 269 | planList: planMap[date.dateKey] || [], |
| 243 | noteList: noteMap[date.dateKey] || [], | 270 | noteList: noteMap[date.dateKey] || [], |
| 244 | weight: weightMap[date.dateKey] || null, | 271 | weight: weightMap[date.dateKey] || null, |
| 245 | })); | 272 | })); |
| 246 | - | ||
| 247 | - dates.value = newDates; | ||
| 248 | } catch (err) { | 273 | } catch (err) { |
| 249 | - console.error('加载日历失败:', err); | ||
| 250 | - const [y, m] = [ | ||
| 251 | - parseInt(currentMonth.value.split('-')[0]), | ||
| 252 | - parseInt(currentMonth.value.split('-')[1]), | ||
| 253 | - ]; | ||
| 254 | - dates.value = generateCalendar(y, m); | 274 | + console.error('加载日历数据失败:', err); |
| 275 | + dates.value = generateCalendar(monthStr); | ||
| 255 | } | 276 | } |
| 256 | - console.log('日历主页面处理的完的是数据:dates', dates); | ||
| 257 | - | ||
| 258 | }; | 277 | }; |
| 259 | -const selectedDateData = computed(() => { | ||
| 260 | - return dates.value.find(item => item.dateKey === selectedDate.value) || null; | ||
| 261 | -}); | ||
| 262 | 278 | ||
| 263 | -// ====================== 核心显示逻辑(已修复)====================== | ||
| 264 | -const maxLines = 3; // 一个格子最多显示3行 | ||
| 265 | - | ||
| 266 | -const getVisibleItems = (date) => { | 279 | +// ====== 视图格子显示控制 ====== |
| 280 | +const getCellVisibleItems = (date) => { | ||
| 267 | const items = []; | 281 | const items = []; |
| 268 | let remaining = 0; | 282 | let remaining = 0; |
| 269 | 283 | ||
| 270 | - // 1. 重量优先显示 | ||
| 271 | if (date.weight && date.weight > 0) { | 284 | if (date.weight && date.weight > 0) { |
| 272 | items.push({ type: 'weight', text: `${date.weight}kg` }); | 285 | items.push({ type: 'weight', text: `${date.weight}kg` }); |
| 273 | } | 286 | } |
| 274 | 287 | ||
| 275 | - // 2. 显示训练计划(已修改) | ||
| 276 | for (const plan of date.planList) { | 288 | for (const plan of date.planList) { |
| 277 | - if (items.length < maxLines) { | ||
| 278 | - items.push({ | ||
| 279 | - type: 'plan', | ||
| 280 | - text: plan.name, | ||
| 281 | - bg: plan.bg | ||
| 282 | - }); | 289 | + if (items.length < MAX_CELL_LINES) { |
| 290 | + items.push({ type: 'plan', text: plan.name, bg: plan.bg }); | ||
| 283 | } else { | 291 | } else { |
| 284 | remaining++; | 292 | remaining++; |
| 285 | } | 293 | } |
| 286 | } | 294 | } |
| 287 | 295 | ||
| 288 | - // 3. 显示备注 | ||
| 289 | for (const note of date.noteList) { | 296 | for (const note of date.noteList) { |
| 290 | - if (items.length < maxLines) { | 297 | + if (items.length < MAX_CELL_LINES) { |
| 291 | items.push({ type: 'note', text: note.content, bg: note.backgroundColor }); | 298 | items.push({ type: 'note', text: note.content, bg: note.backgroundColor }); |
| 292 | } else { | 299 | } else { |
| 293 | remaining++; | 300 | remaining++; |
| @@ -297,153 +304,79 @@ const getVisibleItems = (date) => { | @@ -297,153 +304,79 @@ const getVisibleItems = (date) => { | ||
| 297 | return { items, remaining }; | 304 | return { items, remaining }; |
| 298 | }; | 305 | }; |
| 299 | 306 | ||
| 300 | -// 接收子组件通知,刷新整个日历数据 | ||
| 301 | const handleRefreshCalendar = () => { | 307 | const handleRefreshCalendar = () => { |
| 302 | - const [year, month] = currentMonth.value.split('-').map(Number); | ||
| 303 | - loadPlans(year, month); | 308 | + loadPlans(currentMonth.value); |
| 304 | }; | 309 | }; |
| 305 | -// ==================================================================== | ||
| 306 | 310 | ||
| 307 | -// ====== 监听月份变化 ====== | ||
| 308 | watch( | 311 | watch( |
| 309 | currentMonth, | 312 | currentMonth, |
| 310 | (newVal) => { | 313 | (newVal) => { |
| 311 | - const [year, month] = newVal.split('-').map(Number); | ||
| 312 | - loadPlans(year, month); | 314 | + if (newVal) { |
| 315 | + loadPlans(newVal); | ||
| 316 | + } | ||
| 313 | }, | 317 | }, |
| 314 | - { immediate: true }, | 318 | + { immediate: true } |
| 315 | ); | 319 | ); |
| 316 | 320 | ||
| 317 | const selectDate = async (date) => { | 321 | const selectDate = async (date) => { |
| 318 | selectedDate.value = date.dateKey; | 322 | selectedDate.value = date.dateKey; |
| 319 | - // ShowGridCellPopup.value = true; | ||
| 320 | - await nextTick() | 323 | + await nextTick(); |
| 321 | gridPopupRef.value?.openPopup(); | 324 | gridPopupRef.value?.openPopup(); |
| 322 | }; | 325 | }; |
| 323 | - | ||
| 324 | </script> | 326 | </script> |
| 325 | 327 | ||
| 326 | <style lang="scss" scoped> | 328 | <style lang="scss" scoped> |
| 327 | .calendar-page { | 329 | .calendar-page { |
| 328 | height: 100%; | 330 | height: 100%; |
| 329 | - padding: 40rpx; | ||
| 330 | - background-color: white; | 331 | + padding: 30rpx 15rpx 20rpx; |
| 332 | + background-color: #fff; | ||
| 331 | box-sizing: border-box; | 333 | box-sizing: border-box; |
| 332 | 334 | ||
| 333 | .top-bar { | 335 | .top-bar { |
| 334 | display: flex; | 336 | display: flex; |
| 335 | justify-content: space-between; | 337 | justify-content: space-between; |
| 336 | align-items: center; | 338 | align-items: center; |
| 337 | - margin-bottom: 20rpx; | ||
| 338 | - margin-top: 35rpx; | 339 | + margin-bottom: 24rpx; |
| 339 | 340 | ||
| 340 | - .month-picker { | ||
| 341 | - height: 40rpx; | ||
| 342 | - border-radius: 50rpx; | ||
| 343 | - overflow: hidden; | ||
| 344 | - padding: 0 20rpx; | ||
| 345 | - gap: 10rpx; | ||
| 346 | - | ||
| 347 | - :deep(.uni-date-x--border) { | ||
| 348 | - border: none !important; | ||
| 349 | - } | ||
| 350 | - | ||
| 351 | - :deep(.uni-date__x-input) { | ||
| 352 | - height: 40rpx; | ||
| 353 | - line-height: 40rpx; | ||
| 354 | - padding: 0 10rpx; | ||
| 355 | - } | ||
| 356 | - | ||
| 357 | - // 微信小程序端样式适配 | ||
| 358 | - // #ifdef MP-WEIXIN | 341 | + .month-picker-pill { |
| 359 | display: flex; | 342 | display: flex; |
| 360 | align-items: center; | 343 | align-items: center; |
| 361 | - justify-content: center; | ||
| 362 | - min-width: 200rpx; | ||
| 363 | - box-sizing: border-box; | ||
| 364 | - | ||
| 365 | - :deep(.uni-date-x) { | ||
| 366 | - height: 35rpx; | ||
| 367 | - // min-height: 40rpx; | ||
| 368 | - border-radius: 50rpx; | ||
| 369 | - background-color: transparent; | ||
| 370 | - padding: 0; | ||
| 371 | - margin: 0; | ||
| 372 | - border: none !important; | ||
| 373 | - | ||
| 374 | - } | ||
| 375 | - | ||
| 376 | - :deep(.uni-date-editor--x) { | ||
| 377 | - height: 100%; | ||
| 378 | - width: 100%; | ||
| 379 | - display: flex; | ||
| 380 | - align-items: center; | ||
| 381 | - justify-content: center; | ||
| 382 | - border: none !important; | ||
| 383 | - } | ||
| 384 | - | ||
| 385 | - :deep(.uni-date__x-input) { | ||
| 386 | - // height: 40rpx; | ||
| 387 | - // line-height: 40rpx; | ||
| 388 | - font-size: 24rpx; | ||
| 389 | - color: #333; | ||
| 390 | - padding: 0 10rpx; | ||
| 391 | - text-align: center; | ||
| 392 | - flex: 1; | ||
| 393 | - overflow: hidden; | ||
| 394 | - text-overflow: ellipsis; | ||
| 395 | - white-space: nowrap; | ||
| 396 | - } | ||
| 397 | - | ||
| 398 | - :deep(.icon-calendar) { | ||
| 399 | - display: none !important; | ||
| 400 | - } | ||
| 401 | - | ||
| 402 | - // #endif | 344 | + gap: 12rpx; |
| 345 | + background-color: #ececec; | ||
| 346 | + padding: 12rpx 28rpx; | ||
| 347 | + border-radius: 40rpx; | ||
| 348 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 349 | + border: 1rpx solid #fff; | ||
| 350 | + | ||
| 351 | + .month-text { | ||
| 352 | + font-size: 30rpx; | ||
| 353 | + font-weight: 600; | ||
| 354 | + color: #1a1a1a; | ||
| 403 | } | 355 | } |
| 404 | } | 356 | } |
| 405 | 357 | ||
| 406 | .action-buttons { | 358 | .action-buttons { |
| 407 | display: flex; | 359 | display: flex; |
| 408 | - justify-content: flex-start; | ||
| 409 | - gap: 10rpx; | ||
| 410 | - | ||
| 411 | - // .action-btn { | ||
| 412 | - // padding: 20rpx 20rpx; | ||
| 413 | - // margin: 0; | ||
| 414 | - // line-height: 1; | ||
| 415 | - // border-radius: 20rpx; | ||
| 416 | - // border: 1px solid #666; | ||
| 417 | - // font-size: 20rpx; | ||
| 418 | - // } | ||
| 419 | - | ||
| 420 | - :deep(uni-button), | ||
| 421 | - :deep(wx-button) { | ||
| 422 | - padding: 10rpx 20rpx; | ||
| 423 | - margin: 0; | ||
| 424 | - line-height: 1; | ||
| 425 | - border-radius: 20rpx; | ||
| 426 | - border: 1px solid #666; | ||
| 427 | - font-size: 20rpx; | 360 | + align-items: center; |
| 428 | } | 361 | } |
| 429 | } | 362 | } |
| 430 | 363 | ||
| 431 | - .calendar-grid { | ||
| 432 | - padding-bottom: 120rpx; | ||
| 433 | - // #ifdef MP-WEIXIN | ||
| 434 | - padding-bottom: 140rpx; | ||
| 435 | - // #endif | 364 | + .calendar-card { |
| 365 | + background-color: #ffffff; | ||
| 366 | + border-radius: 24rpx; | ||
| 367 | + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03); | ||
| 436 | 368 | ||
| 437 | .week-header { | 369 | .week-header { |
| 438 | display: flex; | 370 | display: flex; |
| 439 | justify-content: space-between; | 371 | justify-content: space-between; |
| 440 | - margin-bottom: 10rpx; | 372 | + margin-bottom: 20rpx; |
| 441 | 373 | ||
| 442 | .week-day { | 374 | .week-day { |
| 443 | flex: 1; | 375 | flex: 1; |
| 444 | text-align: center; | 376 | text-align: center; |
| 445 | - font-size: 28rpx; | ||
| 446 | - color: #666; | 377 | + font-size: 26rpx; |
| 378 | + font-weight: 550; | ||
| 379 | + | ||
| 447 | } | 380 | } |
| 448 | } | 381 | } |
| 449 | 382 | ||
| @@ -453,19 +386,55 @@ const selectDate = async (date) => { | @@ -453,19 +386,55 @@ const selectDate = async (date) => { | ||
| 453 | gap: 10rpx; | 386 | gap: 10rpx; |
| 454 | } | 387 | } |
| 455 | 388 | ||
| 389 | + /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */ | ||
| 456 | .date-cell { | 390 | .date-cell { |
| 457 | display: flex; | 391 | display: flex; |
| 458 | flex-direction: column; | 392 | flex-direction: column; |
| 459 | align-items: center; | 393 | align-items: center; |
| 460 | justify-content: flex-start; | 394 | justify-content: flex-start; |
| 461 | - border: 1px solid #eee; | ||
| 462 | - font-size: 30rpx; | ||
| 463 | - color: #333; | ||
| 464 | - background-color: #f9fafc; | ||
| 465 | - padding-top: 10rpx; | 395 | + border-radius: 12rpx; |
| 466 | height: 200rpx; | 396 | height: 200rpx; |
| 467 | - overflow: hidden; | 397 | + padding: 8rpx 4rpx; |
| 468 | box-sizing: border-box; | 398 | box-sizing: border-box; |
| 399 | + transition: all 0.2s ease; | ||
| 400 | + | ||
| 401 | + &:active { | ||
| 402 | + filter: brightness(0.96); | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + &.other-month { | ||
| 406 | + opacity: 0.5; | ||
| 407 | + border-color: #f1f5f9; | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + &.is-today { | ||
| 411 | + border: 2rpx solid #3b82f6; | ||
| 412 | + } | ||
| 413 | + | ||
| 414 | + &.has-data { | ||
| 415 | + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02); | ||
| 416 | + } | ||
| 417 | + | ||
| 418 | + .date-header { | ||
| 419 | + height: 36rpx; | ||
| 420 | + display: flex; | ||
| 421 | + align-items: center; | ||
| 422 | + justify-content: center; | ||
| 423 | + margin-bottom: 6rpx; | ||
| 424 | + | ||
| 425 | + .date-number { | ||
| 426 | + font-size: 26rpx; | ||
| 427 | + font-weight: 600; | ||
| 428 | + color: #1f2937; | ||
| 429 | + | ||
| 430 | + | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + .today-badge { | ||
| 434 | + | ||
| 435 | + font-weight: 600; | ||
| 436 | + | ||
| 437 | + } | ||
| 469 | } | 438 | } |
| 470 | 439 | ||
| 471 | .plan-list { | 440 | .plan-list { |
| @@ -473,159 +442,126 @@ const selectDate = async (date) => { | @@ -473,159 +442,126 @@ const selectDate = async (date) => { | ||
| 473 | display: flex; | 442 | display: flex; |
| 474 | flex-direction: column; | 443 | flex-direction: column; |
| 475 | gap: 6rpx; | 444 | gap: 6rpx; |
| 476 | - margin-top: 10rpx; | ||
| 477 | - padding: 0; | 445 | + |
| 446 | + .plan-tag { | ||
| 447 | + width: 100%; | ||
| 448 | + border-radius: 6rpx; | ||
| 449 | + padding: 2rpx 4rpx; | ||
| 478 | box-sizing: border-box; | 450 | box-sizing: border-box; |
| 479 | - } | 451 | + display: flex; |
| 452 | + align-items: center; | ||
| 453 | + justify-content: center; | ||
| 454 | + background-color: rgba(255, 255, 255, 0.85); | ||
| 455 | + /* 使 Tag 悬浮于彩色格子之上 */ | ||
| 480 | 456 | ||
| 481 | - .weight, | ||
| 482 | - .plan-extra, | ||
| 483 | - .note-item, | ||
| 484 | - .plan-name { | 457 | + .tag-text { |
| 485 | font-size: 18rpx; | 458 | font-size: 18rpx; |
| 486 | - background: #ffdd44; | ||
| 487 | - padding: 4rpx 0; | ||
| 488 | - text-align: center; | 459 | + line-height: 22rpx; |
| 460 | + color: #111827; | ||
| 489 | overflow: hidden; | 461 | overflow: hidden; |
| 490 | white-space: nowrap; | 462 | white-space: nowrap; |
| 491 | text-overflow: ellipsis; | 463 | text-overflow: ellipsis; |
| 464 | + text-align: center; | ||
| 492 | width: 100%; | 465 | width: 100%; |
| 493 | - box-sizing: border-box; | ||
| 494 | } | 466 | } |
| 495 | 467 | ||
| 496 | - .weight, | ||
| 497 | - .weight-item { | ||
| 498 | - background: #b8ff66; | 468 | + &.tag-type-weight { |
| 469 | + background-color: #bbf7d0 !important; | ||
| 470 | + | ||
| 471 | + .tag-text { | ||
| 472 | + color: #14532d; | ||
| 473 | + font-weight: 700; | ||
| 499 | } | 474 | } |
| 500 | } | 475 | } |
| 501 | -} | ||
| 502 | 476 | ||
| 503 | -/* 日历说明弹窗样式 */ | ||
| 504 | -.explain-popup-overlay { | ||
| 505 | - position: fixed; | ||
| 506 | - top: 0; | ||
| 507 | - left: 0; | ||
| 508 | - right: 0; | ||
| 509 | - bottom: 0; | ||
| 510 | - background-color: rgba(0, 0, 0, 0.4); | ||
| 511 | - display: flex; | ||
| 512 | - justify-content: center; | ||
| 513 | - align-items: flex-end; | ||
| 514 | - z-index: 999; | 477 | + &.tag-type-note { |
| 478 | + background-color: #ffffff; | ||
| 515 | 479 | ||
| 516 | - .explain-popup { | ||
| 517 | - width: 100%; | 480 | + .tag-text { |
| 481 | + color: #374151; | ||
| 482 | + } | ||
| 483 | + } | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + .plan-extra { | ||
| 487 | + font-size: 16rpx; | ||
| 488 | + color: #4b5563; | ||
| 489 | + text-align: center; | ||
| 490 | + line-height: 20rpx; | ||
| 491 | + font-weight: 600; | ||
| 492 | + } | ||
| 493 | + } | ||
| 494 | + } | ||
| 495 | + } | ||
| 496 | +} | ||
| 497 | + | ||
| 498 | +.explain-popup-content { | ||
| 499 | + padding: 36rpx 32rpx 48rpx 32rpx; | ||
| 518 | background-color: #ffffff; | 500 | background-color: #ffffff; |
| 519 | - border-radius: 24rpx 24rpx 0 0; | ||
| 520 | - padding: 40rpx; | ||
| 521 | - box-sizing: border-box; | ||
| 522 | 501 | ||
| 523 | .explain-header { | 502 | .explain-header { |
| 524 | display: flex; | 503 | display: flex; |
| 525 | justify-content: space-between; | 504 | justify-content: space-between; |
| 526 | align-items: center; | 505 | align-items: center; |
| 527 | - margin-bottom: 40rpx; | 506 | + margin-bottom: 32rpx; |
| 528 | 507 | ||
| 529 | .explain-title { | 508 | .explain-title { |
| 530 | font-size: 32rpx; | 509 | font-size: 32rpx; |
| 531 | - font-weight: 60; | ||
| 532 | - color: #333333; | ||
| 533 | - } | ||
| 534 | - | ||
| 535 | - .close-btn { | ||
| 536 | - width: 48rpx; | ||
| 537 | - height: 48rpx; | ||
| 538 | - border-radius: 50%; | ||
| 539 | - background-color: #f5f5f5; | ||
| 540 | - display: flex; | ||
| 541 | - justify-content: center; | ||
| 542 | - align-items: center; | ||
| 543 | - | ||
| 544 | - text { | ||
| 545 | - font-size: 36rpx; | ||
| 546 | - color: #666666; | ||
| 547 | - line-height: 1; | ||
| 548 | - } | 510 | + font-weight: 600; |
| 511 | + color: #111827; | ||
| 549 | } | 512 | } |
| 550 | } | 513 | } |
| 551 | 514 | ||
| 552 | .explain-list { | 515 | .explain-list { |
| 516 | + display: flex; | ||
| 517 | + flex-direction: column; | ||
| 518 | + gap: 28rpx; | ||
| 519 | + | ||
| 553 | .explain-item { | 520 | .explain-item { |
| 554 | display: flex; | 521 | display: flex; |
| 555 | align-items: center; | 522 | align-items: center; |
| 556 | - margin-bottom: 32rpx; | ||
| 557 | 523 | ||
| 558 | .color-block { | 524 | .color-block { |
| 559 | - width: 40rpx; | ||
| 560 | - height: 40rpx; | 525 | + width: 36rpx; |
| 526 | + height: 36rpx; | ||
| 561 | border-radius: 8rpx; | 527 | border-radius: 8rpx; |
| 562 | - margin-right: 20rpx; | 528 | + margin-right: 24rpx; |
| 563 | 529 | ||
| 564 | &.blue { | 530 | &.blue { |
| 565 | - background-color: #a8d8ff; | 531 | + background-color: #bae6fd; |
| 566 | } | 532 | } |
| 567 | 533 | ||
| 568 | &.green { | 534 | &.green { |
| 569 | - background-color: #b8ff66; | 535 | + background-color: #bbf7d0; |
| 570 | } | 536 | } |
| 571 | 537 | ||
| 572 | &.yellow { | 538 | &.yellow { |
| 573 | - background-color: #ffdd44; | 539 | + background-color: #fef08a; |
| 574 | } | 540 | } |
| 575 | 541 | ||
| 576 | &.gray { | 542 | &.gray { |
| 577 | - background-color: #f0f0f0; | ||
| 578 | - border: 1rpx solid #eee; | 543 | + background-color: #e5e7eb; |
| 579 | } | 544 | } |
| 580 | } | 545 | } |
| 581 | 546 | ||
| 547 | + .item-info { | ||
| 548 | + display: flex; | ||
| 549 | + align-items: center; | ||
| 550 | + gap: 16rpx; | ||
| 551 | + | ||
| 582 | .item-label { | 552 | .item-label { |
| 583 | font-size: 28rpx; | 553 | font-size: 28rpx; |
| 584 | - color: #333333; | ||
| 585 | - width: 80rpx; | ||
| 586 | - margin-right: 20rpx; | 554 | + font-weight: 600; |
| 555 | + color: #1f2937; | ||
| 556 | + width: 72rpx; | ||
| 587 | } | 557 | } |
| 588 | 558 | ||
| 589 | .item-desc { | 559 | .item-desc { |
| 590 | - font-size: 28rpx; | ||
| 591 | - color: #666666; | ||
| 592 | - flex: 1; | ||
| 593 | - } | 560 | + font-size: 26rpx; |
| 561 | + color: #6b7280; | ||
| 594 | } | 562 | } |
| 595 | } | 563 | } |
| 596 | } | 564 | } |
| 597 | -} | ||
| 598 | - | ||
| 599 | -// .gridCellContent { | ||
| 600 | -// position: fixed; | ||
| 601 | -// top: 0; | ||
| 602 | -// left: 0; | ||
| 603 | -// right: 0; | ||
| 604 | -// bottom: 0; | ||
| 605 | -// background: rgba(0, 0, 0, 0.5); | ||
| 606 | -// z-index: 9999; | ||
| 607 | -// display: flex; | ||
| 608 | -// align-items: flex-end; | ||
| 609 | -// padding: 0 !important; | ||
| 610 | -// margin: 0 !important; | ||
| 611 | -// width: 100vw !important; | ||
| 612 | -// box-sizing: border-box; | ||
| 613 | -// } | ||
| 614 | -</style> | ||
| 615 | - | ||
| 616 | -<!-- #ifdef MP-WEIXIN --> | ||
| 617 | -<style lang="scss"> | ||
| 618 | -.month-picker { | ||
| 619 | - | ||
| 620 | - .uni-date-editor--x.uni-date-x--border, | ||
| 621 | - .uni-date-x--border { | ||
| 622 | - border: none !important; | ||
| 623 | - } | ||
| 624 | - | ||
| 625 | - .uni-date-x, | ||
| 626 | - .uni-date-editor--x { | ||
| 627 | - border: none !important; | ||
| 628 | } | 565 | } |
| 629 | } | 566 | } |
| 630 | </style> | 567 | </style> |
| 631 | -<!-- #endif --> |
| 1 | <template> | 1 | <template> |
| 2 | <view class="training-data-page"> | 2 | <view class="training-data-page"> |
| 3 | + <!-- 顶部时间范围选择器 --> | ||
| 4 | + <view class="top-time-picker"> | ||
| 3 | <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> | 5 | <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> |
| 6 | + </view> | ||
| 4 | 7 | ||
| 5 | - <!-- 过渡动画 --> | 8 | + <!-- 主体内容可滚动区域 --> |
| 9 | + <view class="main-content"> | ||
| 10 | + <scroll-view scroll-y class="scroll-area" :show-scrollbar="false"> | ||
| 6 | <up-transition :show="true" mode="slide-right" duration="300"> | 11 | <up-transition :show="true" mode="slide-right" duration="300"> |
| 7 | - <view class="content-scroll"> | ||
| 8 | - <!-- 肌肉部位数据区域 --> | ||
| 9 | - <view class="section"> | ||
| 10 | - <view class="muscle-data"> | ||
| 11 | - <view class="title">主训练肌肉数据</view> | ||
| 12 | - <view class="tabs"> | ||
| 13 | - <view class="tab" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">容量</view> | ||
| 14 | - <view class="tab" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">组数</view> | 12 | + <view class="scroll-inner"> |
| 13 | + <!-- 1. 肌肉部位数据区域 --> | ||
| 14 | + <view class="card-section"> | ||
| 15 | + <view class="section-header"> | ||
| 16 | + <text class="title">主训练肌肉数据</text> | ||
| 15 | </view> | 17 | </view> |
| 16 | - <view class="tip">{{ tabTip[partTabIndex] }}</view> | ||
| 17 | 18 | ||
| 18 | - <!-- 网格布局:一行4格 + 正方形 + 总计自适应 --> | ||
| 19 | - <view class="detail-part" :style="`--remain-span: ${remainSpan}`"> | ||
| 20 | - <view class="part-item" v-for="item in detailPartOptions" :key="item.id" | ||
| 21 | - @click="openMuscleDetail(item.id)"> | ||
| 22 | - <text class=" txt">{{ item.name }}</text> | ||
| 23 | - <text class="num">{{ item.value }}</text> | 19 | + <!-- 容量 / 组数 切换标签 --> |
| 20 | + <view class="tab-group"> | ||
| 21 | + <view class="tab-item" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0"> | ||
| 22 | + 容量 | ||
| 23 | + </view> | ||
| 24 | + <view class="tab-item" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1"> | ||
| 25 | + 组数 | ||
| 24 | </view> | 26 | </view> |
| 25 | - <view class="part-item total-item"> | ||
| 26 | - <text class="txt">总计</text> | ||
| 27 | - <text class="num">{{ totalPartValue }}</text> | ||
| 28 | </view> | 27 | </view> |
| 28 | + <text class="tip-text">{{ TREND_TIPS[partTabIndex] }}</text> | ||
| 29 | + | ||
| 30 | + <!-- 肌肉部位网格(1行4格 + 正方形 + 总计自适应占格) --> | ||
| 31 | + <view class="muscle-grid"> | ||
| 32 | + <view class="grid-item" v-for="item in detailPartOptions" :key="item.id" | ||
| 33 | + @click="openMuscleDetail(item.id)"> | ||
| 34 | + <text class="item-name">{{ item.name }}</text> | ||
| 35 | + <text class="item-value">{{ item.value }}</text> | ||
| 36 | + </view> | ||
| 37 | + | ||
| 38 | + <!-- 总计格子 --> | ||
| 39 | + <view class="grid-item total-item" :style="{ gridColumn: `span ${remainSpan}` }"> | ||
| 40 | + <text class="item-name">总计</text> | ||
| 41 | + <text class="item-value highlight">{{ totalPartValue }}</text> | ||
| 29 | </view> | 42 | </view> |
| 30 | </view> | 43 | </view> |
| 31 | </view> | 44 | </view> |
| 32 | 45 | ||
| 33 | - <!-- 趋势图表区域 --> | ||
| 34 | - <view class="section"> | ||
| 35 | - <view class="header"> | ||
| 36 | - <view class="title">趋势</view> | ||
| 37 | - <view class="mean"> | ||
| 38 | - <up-select v-model:current="partId" :label="partOptions.find(item => item.id === partId)?.name || '全部'" | ||
| 39 | - :options="partOptions" @select="onPartChange"></up-select> | 46 | + <!-- 2. 趋势图表区域 --> |
| 47 | + <view class="card-section"> | ||
| 48 | + <view class="section-header space-between"> | ||
| 49 | + <text class="title">趋势</text> | ||
| 50 | + <view class="part-select-wrapper"> | ||
| 51 | + <up-select v-model:current="partId" | ||
| 52 | + :label="partOptions.find((item) => item.id === partId)?.name || '全部'" :options="partOptions" | ||
| 53 | + @select="onPartChange"></up-select> | ||
| 40 | </view> | 54 | </view> |
| 41 | </view> | 55 | </view> |
| 42 | - <view class="tabs"> | ||
| 43 | - <view class="tab" v-for="item in trendTabs" :key="item.id" :class="{ active: NumberTabIndex === item.id }" | ||
| 44 | - @click="NumberTabIndex = item.id"> | 56 | + |
| 57 | + <!-- 趋势维度切换 Tabs --> | ||
| 58 | + <view class="tab-group flex-wrap"> | ||
| 59 | + <view class="tab-item" v-for="item in trendTabs" :key="item.id" | ||
| 60 | + :class="{ active: NumberTabIndex === item.id }" @click="NumberTabIndex = item.id"> | ||
| 45 | {{ item.name }} | 61 | {{ item.name }} |
| 46 | </view> | 62 | </view> |
| 47 | </view> | 63 | </view> |
| 48 | - <view class="tip">{{ tabTip[NumberTabIndex] }}</view> | 64 | + <text class="tip-text">{{ TREND_TIPS[NumberTabIndex] }}</text> |
| 49 | 65 | ||
| 50 | - <view class="statistical"> | 66 | + <!-- 统计图表(根据 showLastPeriod 切换 柱状图 / 折线图) --> |
| 67 | + <view class="chart-container"> | ||
| 51 | <template v-if="!showLastPeriod"> | 68 | <template v-if="!showLastPeriod"> |
| 52 | <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries" | 69 | <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries" |
| 53 | chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" /> | 70 | chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" /> |
| @@ -58,15 +75,18 @@ | @@ -58,15 +75,18 @@ | ||
| 58 | </template> | 75 | </template> |
| 59 | </view> | 76 | </view> |
| 60 | 77 | ||
| 61 | - <view class="chose"> | ||
| 62 | - <checkbox :checked="showLastPeriod" @click="toggleLastWeek" /> | 78 | + <!-- 显示上周/上月数据勾选框 --> |
| 79 | + <view class="toggle-checkbox" @click="toggleLastWeek"> | ||
| 80 | + <checkbox :checked="showLastPeriod" color="#10b981" style="transform: scale(0.8);" /> | ||
| 81 | + <text class="toggle-label"> | ||
| 63 | {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }} | 82 | {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }} |
| 83 | + </text> | ||
| 64 | </view> | 84 | </view> |
| 65 | 85 | ||
| 66 | - <!-- 数据对比 --> | ||
| 67 | - <view class="compare"> | ||
| 68 | - <view class="header-row"> | ||
| 69 | - <text class="title">对比</text> | 86 | + <!-- 3. 数据对比区域 --> |
| 87 | + <view class="compare-section"> | ||
| 88 | + <view class="compare-header"> | ||
| 89 | + <text class="sub-title">对比</text> | ||
| 70 | <view class="legend-group"> | 90 | <view class="legend-group"> |
| 71 | <view class="legend-item"> | 91 | <view class="legend-item"> |
| 72 | <view class="dot blue"></view> | 92 | <view class="dot blue"></view> |
| @@ -81,13 +101,15 @@ | @@ -81,13 +101,15 @@ | ||
| 81 | </view> | 101 | </view> |
| 82 | </view> | 102 | </view> |
| 83 | </view> | 103 | </view> |
| 104 | + | ||
| 105 | + <!-- 对比数据卡片 --> | ||
| 84 | <view class="compare-card"> | 106 | <view class="compare-card"> |
| 85 | <view class="data-list"> | 107 | <view class="data-list"> |
| 86 | <view v-for="(item, index) in listData" :key="index" class="data-row"> | 108 | <view v-for="(item, index) in listData" :key="index" class="data-row"> |
| 87 | <view class="label">{{ item.label }}</view> | 109 | <view class="label">{{ item.label }}</view> |
| 88 | - <view class="value">{{ item.lastPeriod }}</view> | ||
| 89 | - <view class="value">{{ item.currPeriod }}</view> | ||
| 90 | - <view class="value diff" :class="{ negative: item.diff < 0 }"> | 110 | + <view class="value text-secondary">{{ item.lastPeriod }}</view> |
| 111 | + <view class="value text-main">{{ item.currPeriod }}</view> | ||
| 112 | + <view class="value diff" :class="{ positive: item.diff > 0, negative: item.diff < 0 }"> | ||
| 91 | {{ item.diff > 0 ? '+' + item.diff : item.diff }} | 113 | {{ item.diff > 0 ? '+' + item.diff : item.diff }} |
| 92 | </view> | 114 | </view> |
| 93 | </view> | 115 | </view> |
| @@ -98,22 +120,17 @@ | @@ -98,22 +120,17 @@ | ||
| 98 | </view> | 120 | </view> |
| 99 | </up-transition> | 121 | </up-transition> |
| 100 | 122 | ||
| 101 | - <!-- 肌肉详情弹窗 --> | ||
| 102 | - <!-- <up-popup :show="muscleShow" mode="top" @close="muscleShow = false" safeAreaInsetTop> | ||
| 103 | - <view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }"> | ||
| 104 | - <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> | ||
| 105 | - <view class="statistical"> | ||
| 106 | - <LineChart v-if="muscleShow" @close="closeMuscleDetail" :categories="muscleChartCategories" :series="[ | ||
| 107 | - { name: '容量', data: muscleVolumeArr }, | ||
| 108 | - { name: '组数', data: muscleSetsArr } | ||
| 109 | - ]" chartType="column" /> | ||
| 110 | - </view> | 123 | + <!-- 底部安全区域间距 --> |
| 124 | + <view class="bottom-spacer"></view> | ||
| 125 | + </scroll-view> | ||
| 111 | </view> | 126 | </view> |
| 112 | - </up-popup> --> | ||
| 113 | - <up-popup :show="muscleShow" mode="top" @close="closeMuscleDetail" safeAreaInsetTop> | 127 | + |
| 128 | + <!-- 肌肉详情弹窗 --> | ||
| 129 | + <up-popup :show="muscleShow" mode="top" round="24rpx" @close="closeMuscleDetail" safeAreaInsetTop> | ||
| 114 | <view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }"> | 130 | <view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }"> |
| 115 | <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> | 131 | <timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" /> |
| 116 | 132 | ||
| 133 | + <view class="popup-body"> | ||
| 117 | <!-- 肌肉名称 --> | 134 | <!-- 肌肉名称 --> |
| 118 | <view class="muscle-name"> | 135 | <view class="muscle-name"> |
| 119 | {{ currentMuscleName }} | 136 | {{ currentMuscleName }} |
| @@ -121,27 +138,30 @@ | @@ -121,27 +138,30 @@ | ||
| 121 | 138 | ||
| 122 | <!-- 容量/组数 切换标签 --> | 139 | <!-- 容量/组数 切换标签 --> |
| 123 | <view class="muscle-tabs"> | 140 | <view class="muscle-tabs"> |
| 124 | - <view class="tab" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0">容量</view> | ||
| 125 | - <view class="tab" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1">组数</view> | 141 | + <view class="tab-item" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0"> |
| 142 | + 容量 | ||
| 143 | + </view> | ||
| 144 | + <view class="tab-item" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1"> | ||
| 145 | + 组数 | ||
| 146 | + </view> | ||
| 126 | </view> | 147 | </view> |
| 127 | - <view class="tip">{{ tabTip[muscleTabIndex] }}</view> | 148 | + <view class="tip">{{ TREND_TIPS[muscleTabIndex] }}</view> |
| 128 | 149 | ||
| 129 | <!-- 图表区域 --> | 150 | <!-- 图表区域 --> |
| 130 | <view class="statistical"> | 151 | <view class="statistical"> |
| 131 | - <!-- <LineChart v-if="muscleShow" :categories="muscleChartCategories" :series="muscleChartSeries" | ||
| 132 | - chartType="column" /> --> | ||
| 133 | <LineChart v-if="muscleShow && muscleChartCategories.length" | 152 | <LineChart v-if="muscleShow && muscleChartCategories.length" |
| 134 | :key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`" | 153 | :key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`" |
| 135 | :categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true" | 154 | :categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true" |
| 136 | :extra="{ column: { width: muscleChartColumnWidth } }" /> | 155 | :extra="{ column: { width: muscleChartColumnWidth } }" /> |
| 137 | </view> | 156 | </view> |
| 138 | </view> | 157 | </view> |
| 158 | + </view> | ||
| 139 | </up-popup> | 159 | </up-popup> |
| 140 | - | ||
| 141 | </view> | 160 | </view> |
| 142 | </template> | 161 | </template> |
| 143 | 162 | ||
| 144 | <script setup> | 163 | <script setup> |
| 164 | +import dayjs from 'dayjs'; | ||
| 145 | import LineChart from './LineChart.vue'; | 165 | import LineChart from './LineChart.vue'; |
| 146 | import { ref, computed, onMounted } from 'vue'; | 166 | import { ref, computed, onMounted } from 'vue'; |
| 147 | import timeSelect from './time-select.vue'; | 167 | import timeSelect from './time-select.vue'; |
| @@ -149,303 +169,263 @@ import { getMenuButtonHeight } from '@/utils/safeArea'; | @@ -149,303 +169,263 @@ import { getMenuButtonHeight } from '@/utils/safeArea'; | ||
| 149 | import TrainingDataApi from '@/sheep/api/TrainingData/Data.js'; | 169 | import TrainingDataApi from '@/sheep/api/TrainingData/Data.js'; |
| 150 | import LeftmotionApi from '@/sheep/api/motion/equipments'; | 170 | import LeftmotionApi from '@/sheep/api/motion/equipments'; |
| 151 | 171 | ||
| 152 | -const statusBarHeight = getMenuButtonHeight(); | ||
| 153 | - | ||
| 154 | -// 时间范围 | ||
| 155 | -const startDate = ref('') | ||
| 156 | -const endDate = ref('') | ||
| 157 | - | ||
| 158 | -// 肌肉弹窗 | ||
| 159 | -// 弹窗的容量/组数切换索引 | ||
| 160 | -const muscleTabIndex = ref(0) | ||
| 161 | -const muscleShow = ref(false); | ||
| 162 | -const currentMuscleName = ref('') | ||
| 163 | -const selectLabel = ref('全部') | ||
| 164 | -const partId = ref(''); | ||
| 165 | -const partOptions = ref([]); | ||
| 166 | - | ||
| 167 | -const weeklyData = ref(null); | 172 | +// ==================== 常量 ==================== |
| 168 | 173 | ||
| 169 | -// 加载锁 | ||
| 170 | -const weeklyLoading = ref(false); | ||
| 171 | -const trainingLoading = ref(false); | ||
| 172 | -const muscleListLoading = ref(false); | 174 | +const WEEKDAY_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; |
| 173 | 175 | ||
| 174 | -// 时间类型 week / month | ||
| 175 | -const MainCurrentDateType = ref('week'); | 176 | +/** 趋势维度文案 */ |
| 177 | +const TREND_DATA_KEYS = ['volume', 'setCount', 'duration', 'distance', 'count']; | ||
| 176 | 178 | ||
| 177 | -// 底部提示文案 | ||
| 178 | -const tabTip = ref([ | 179 | +const TREND_TIPS = [ |
| 179 | '重量X组数X次数,用来评估训练的整体强度(kg)', | 180 | '重量X组数X次数,用来评估训练的整体强度(kg)', |
| 180 | '该时间段内训练组数趋势(组)', | 181 | '该时间段内训练组数趋势(组)', |
| 181 | '累计计时类动作的时长(min)', | 182 | '累计计时类动作的时长(min)', |
| 182 | '累计距离类动作的距离(km)', | 183 | '累计距离类动作的距离(km)', |
| 183 | '累计次数类动作的次数(次)', | 184 | '累计次数类动作的次数(次)', |
| 184 | -]); | 185 | +]; |
| 185 | 186 | ||
| 186 | -// 趋势标签 | ||
| 187 | -const NumberTabIndex = ref(0) | 187 | +// ==================== 日期工具函数 ==================== |
| 188 | + | ||
| 189 | +/** 格式化日期为 YYYY-MM-DD */ | ||
| 190 | +const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD'); | ||
| 191 | + | ||
| 192 | +/** 获取包含指定日期的完整周日期列表(周一~周日),返回 dayjs 实例数组 */ | ||
| 193 | +const getWeekFullDates = (baseDateStr) => { | ||
| 194 | + const d = dayjs(baseDateStr); | ||
| 195 | + const dayOfWeek = d.day(); // 0=周日, 1=周一, ... | ||
| 196 | + const monday = d.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day').startOf('day'); | ||
| 197 | + return Array.from({ length: 7 }, (_, i) => monday.add(i, 'day')); | ||
| 198 | +}; | ||
| 199 | + | ||
| 200 | +/** 获取指定日期所在月份的全部日期列表,返回 dayjs 实例数组 */ | ||
| 201 | +const getMonthFullDates = (baseDateStr) => { | ||
| 202 | + const d = dayjs(baseDateStr); | ||
| 203 | + const daysInMonth = d.daysInMonth(); | ||
| 204 | + return Array.from({ length: daysInMonth }, (_, i) => | ||
| 205 | + d.date(i + 1).startOf('day') | ||
| 206 | + ); | ||
| 207 | +}; | ||
| 208 | + | ||
| 209 | +/** 解析 API 返回的日期数组 [year, month, day] 为 dayjs 实例 */ | ||
| 210 | +const parseApiDate = (apiDateArr) => { | ||
| 211 | + if (!Array.isArray(apiDateArr) || apiDateArr.length !== 3) return null; | ||
| 212 | + const [year, month, day] = apiDateArr; | ||
| 213 | + return dayjs(`${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); | ||
| 214 | +}; | ||
| 215 | + | ||
| 216 | +/** 趋势推荐维度按周/月数据加载 */ | ||
| 217 | +const loadCategoryData = (date) => { | ||
| 218 | + const loader = MainCurrentDateType.value === 'week' | ||
| 219 | + ? loadWeeklyByCategory | ||
| 220 | + : loadMonthlyByCategory; | ||
| 221 | + return loader(date); | ||
| 222 | +}; | ||
| 223 | + | ||
| 224 | +/** 肌肉详情按周/月数据加载 */ | ||
| 225 | +const loadMuscleData = (muscleId, date) => { | ||
| 226 | + const isWeekly = MainCurrentDateType.value === 'week'; | ||
| 227 | + return loadMuscleStats(muscleId, date, isWeekly); | ||
| 228 | +}; | ||
| 229 | + | ||
| 230 | +// ==================== 响应式状态 ==================== | ||
| 231 | + | ||
| 232 | +const statusBarHeight = getMenuButtonHeight(); | ||
| 233 | + | ||
| 234 | +// --- 时间范围 --- | ||
| 235 | +const startDate = ref(''); | ||
| 236 | +const endDate = ref(''); | ||
| 237 | +const MainCurrentDateType = ref('week'); // 'week' | 'month' | ||
| 238 | + | ||
| 239 | +// --- 筛选状态 --- | ||
| 240 | +const partId = ref(''); | ||
| 241 | +const partOptions = ref([]); | ||
| 242 | +const showLastPeriod = ref(false); | ||
| 243 | + | ||
| 244 | +// --- 趋势 Tab 状态 --- | ||
| 245 | +const NumberTabIndex = ref(0); | ||
| 188 | const trendTabs = ref([ | 246 | const trendTabs = ref([ |
| 189 | { id: 0, name: '容量' }, | 247 | { id: 0, name: '容量' }, |
| 190 | { id: 1, name: '组数' }, | 248 | { id: 1, name: '组数' }, |
| 191 | { id: 2, name: '时长' }, | 249 | { id: 2, name: '时长' }, |
| 192 | { id: 3, name: '距离' }, | 250 | { id: 3, name: '距离' }, |
| 193 | { id: 4, name: '次数' }, | 251 | { id: 4, name: '次数' }, |
| 194 | -]) | 252 | +]); |
| 195 | 253 | ||
| 196 | -// 肌肉部位标签 | 254 | +// --- 肌肉部位 Tab --- |
| 197 | const partTabIndex = ref(0); | 255 | const partTabIndex = ref(0); |
| 198 | 256 | ||
| 199 | -// 核心:格式化肌肉部位数据,支持容量/组数切换 | ||
| 200 | -const detailPartOptions = computed(() => { | ||
| 201 | - // 没有肌肉数据时返回空数组 | ||
| 202 | - if (!subCategorieList.value.length) return [] | 257 | +// --- 加载锁 --- |
| 258 | +const weeklyLoading = ref(false); | ||
| 259 | +const trainingLoading = ref(false); | ||
| 260 | +const muscleListLoading = ref(false); | ||
| 203 | 261 | ||
| 204 | - // 根据当前选中的标签,决定取哪个数据 | ||
| 205 | - const isVolume = partTabIndex.value === 0 // 0是容量,1是组数 | ||
| 206 | - const dataMap = isVolume ? trainingData.value.totalVolumeMap : trainingData.value.setCountMap | 262 | +// --- 数据 --- |
| 263 | +const weeklyData = ref(null); | ||
| 264 | +const subCategorieList = ref([]); | ||
| 265 | +const trainingData = ref({ setCountMap: {}, totalVolumeMap: {} }); | ||
| 207 | 266 | ||
| 208 | - // 把 subCategorieList 里的每一项,加上对应的数据 | ||
| 209 | - return subCategorieList.value.map(item => { | ||
| 210 | - return { | ||
| 211 | - id: item.id, | ||
| 212 | - name: item.name, | ||
| 213 | - // 从 dataMap 里取数据,没有的话默认0 | ||
| 214 | - value: dataMap[item.id] ?? 0 | ||
| 215 | - } | ||
| 216 | - }) | ||
| 217 | -}) | ||
| 218 | - | ||
| 219 | -const totalPartValue = computed(() => { | ||
| 220 | - return detailPartOptions.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0) | ||
| 221 | -}) | ||
| 222 | - | ||
| 223 | -// 弹窗用的完整日期列表(周/月模式下的所有日期) | ||
| 224 | -const muscleFullDates = ref([]) | ||
| 225 | -const muscleProcessedData = ref({ | ||
| 226 | - volumeArr: [], | ||
| 227 | - setsArr: [] | ||
| 228 | -}) | ||
| 229 | - | ||
| 230 | -// 核心:计算最后一行剩余格子数,用于总计自适应占格 | ||
| 231 | -const itemTotal = computed(() => detailPartOptions.value.length) | ||
| 232 | -const remainSpan = computed(() => { | ||
| 233 | - const mod = itemTotal.value % 4 | ||
| 234 | - return mod === 0 ? 4 : 4 - mod | ||
| 235 | -}) | 267 | +// --- 肌肉弹窗 --- |
| 268 | +const muscleShow = ref(false); | ||
| 269 | +const muscleTabIndex = ref(0); | ||
| 270 | +const currentMuscleId = ref(null); | ||
| 271 | +const currentMuscleName = ref(''); | ||
| 272 | +const muscleDetailData = ref({ muscleId: 0, dailyStatsList: [] }); | ||
| 273 | +const muscleFullDates = ref([]); | ||
| 274 | +const muscleProcessedData = ref({ volumeArr: [], setsArr: [] }); | ||
| 236 | 275 | ||
| 237 | -const showLastPeriod = ref(false); | 276 | +// ==================== 计算属性 ==================== |
| 238 | 277 | ||
| 239 | -// 周/月 图表类目 & 数据 | ||
| 240 | -const weekCategories = ref(['周一', '周二', '周三', '周四', '周五', '周六', '周日']); | 278 | +/** 肌肉部位数据列表(支持容量/组数切换) */ |
| 279 | +const detailPartOptions = computed(() => { | ||
| 280 | + if (!subCategorieList.value.length) return []; | ||
| 241 | 281 | ||
| 242 | -const lastMonthData = ref(Array(31).fill(0)); | 282 | + const isVolume = partTabIndex.value === 0; |
| 283 | + const dataMap = isVolume | ||
| 284 | + ? trainingData.value.totalVolumeMap | ||
| 285 | + : trainingData.value.setCountMap; | ||
| 243 | 286 | ||
| 244 | -// ✅ 修复版:强制生成完整的周/月日期,就算没数据也要显示横轴 | ||
| 245 | -const chartCategories = computed(() => { | ||
| 246 | - if (!weeklyData.value) return [] | 287 | + return subCategorieList.value.map((item) => ({ |
| 288 | + id: item.id, | ||
| 289 | + name: item.name, | ||
| 290 | + value: dataMap[item.id] ?? 0, | ||
| 291 | + })); | ||
| 292 | +}); | ||
| 247 | 293 | ||
| 248 | - if (MainCurrentDateType.value === 'week') { | ||
| 249 | - // 周模式:生成周一到周日的完整日期(月/日格式) | ||
| 250 | - const weekDates = getWeekFullDates(startDate.value).map(d => { | ||
| 251 | - const month = d.getMonth() + 1 | ||
| 252 | - const day = d.getDate() | ||
| 253 | - return `${month}/${day}` | ||
| 254 | - }) | ||
| 255 | - return weekDates | ||
| 256 | - } else { | ||
| 257 | - // 月模式:生成当月1日到月底的完整日期(纯日期格式) | ||
| 258 | - const monthDates = getMonthFullDates(startDate.value).map(d => { | ||
| 259 | - return `${d.getDate()}` | ||
| 260 | - }) | ||
| 261 | - return monthDates | ||
| 262 | - } | ||
| 263 | -}) | 294 | +/** 肌肉部位总计 */ |
| 295 | +const totalPartValue = computed(() => | ||
| 296 | + detailPartOptions.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0) | ||
| 297 | +); | ||
| 264 | 298 | ||
| 265 | -// 趋势图表动态柱子宽度:周20,月3 | ||
| 266 | -const trendChartColumnWidth = computed(() => { | ||
| 267 | - return MainCurrentDateType.value === 'week' ? 15 : 3 | ||
| 268 | -}) | 299 | +/** 总计格子剩余 span(补齐最后一行) */ |
| 300 | +const remainSpan = computed(() => { | ||
| 301 | + const mod = detailPartOptions.value.length % 4; | ||
| 302 | + return mod === 0 ? 4 : 4 - mod; | ||
| 303 | +}); | ||
| 269 | 304 | ||
| 305 | +/** 图表 X 轴日期标签 */ | ||
| 306 | +const chartCategories = computed(() => { | ||
| 307 | + if (!weeklyData.value) return []; | ||
| 270 | 308 | ||
| 309 | + const fullDates = MainCurrentDateType.value === 'week' | ||
| 310 | + ? getWeekFullDates(startDate.value) | ||
| 311 | + : getMonthFullDates(startDate.value); | ||
| 271 | 312 | ||
| 272 | -// 动态控制柱状图宽度:周15,月5 | ||
| 273 | -const muscleChartColumnWidth = computed(() => { | ||
| 274 | - return MainCurrentDateType.value === 'week' ? 15 : 3 | ||
| 275 | -}) | 313 | + return MainCurrentDateType.value === 'week' |
| 314 | + ? fullDates.map((d) => d.format('M/D')) | ||
| 315 | + : fullDates.map((d) => d.format('D')); | ||
| 316 | +}); | ||
| 276 | 317 | ||
| 277 | -const trendTabIndex = ref(0); | 318 | +/** 柱体宽度 */ |
| 319 | +const trendChartColumnWidth = computed(() => | ||
| 320 | + MainCurrentDateType.value === 'week' ? 15 : 3 | ||
| 321 | +); | ||
| 278 | 322 | ||
| 279 | -// ✅ 修复版:对比表数据,直接从接口取上周/上月总数据 | ||
| 280 | -const listData = computed(() => { | ||
| 281 | - if (!weeklyData.value) return [] | ||
| 282 | - | ||
| 283 | - const curr = weeklyData.value | ||
| 284 | - // 上周/上月的总数据,接口里的字段名是 lastWeekTotalXXX | ||
| 285 | - const last = weeklyData.value | ||
| 286 | - | ||
| 287 | - return [ | ||
| 288 | - { | ||
| 289 | - label: '容量', | ||
| 290 | - lastPeriod: last.lastWeekTotalVolume ?? 0, | ||
| 291 | - currPeriod: curr.totalVolume ?? 0, | ||
| 292 | - diff: (curr.totalVolume ?? 0) - (last.lastWeekTotalVolume ?? 0) | ||
| 293 | - }, | ||
| 294 | - { | ||
| 295 | - label: '组数', | ||
| 296 | - lastPeriod: last.lastWeekTotalSets ?? 0, | ||
| 297 | - currPeriod: curr.totalSets ?? 0, | ||
| 298 | - diff: (curr.totalSets ?? 0) - (last.lastWeekTotalSets ?? 0) | ||
| 299 | - }, | ||
| 300 | - { | ||
| 301 | - label: '时长', | ||
| 302 | - lastPeriod: last.lastWeekTotalDuration ?? 0, | ||
| 303 | - currPeriod: curr.totalDuration ?? 0, | ||
| 304 | - diff: (curr.totalDuration ?? 0) - (last.lastWeekTotalDuration ?? 0) | ||
| 305 | - }, | ||
| 306 | - { | ||
| 307 | - label: '距离', | ||
| 308 | - lastPeriod: last.lastWeekTotalDistance ?? 0, | ||
| 309 | - currPeriod: curr.totalDistance ?? 0, | ||
| 310 | - diff: (curr.totalDistance ?? 0) - (last.lastWeekTotalDistance ?? 0) | ||
| 311 | - }, | ||
| 312 | - { | ||
| 313 | - label: '次数', | ||
| 314 | - lastPeriod: last.lastWeekTotalCount ?? 0, | ||
| 315 | - currPeriod: curr.totalCount ?? 0, | ||
| 316 | - diff: (curr.totalCount ?? 0) - (last.lastWeekTotalCount ?? 0) | ||
| 317 | - } | ||
| 318 | - ] | ||
| 319 | -}) | 323 | +const muscleChartColumnWidth = computed(() => |
| 324 | + MainCurrentDateType.value === 'week' ? 15 : 3 | ||
| 325 | +); | ||
| 320 | 326 | ||
| 327 | +/** 趋势图 Series 数据 */ | ||
| 321 | const chartSeries = computed(() => { | 328 | const chartSeries = computed(() => { |
| 322 | - if (!weeklyData.value) return [] | 329 | + if (!weeklyData.value) return []; |
| 323 | 330 | ||
| 324 | - // 1. 获取当前周/月完整日期列表 | ||
| 325 | - let fullDateList = MainCurrentDateType.value === 'week' | 331 | + const isWeekly = MainCurrentDateType.value === 'week'; |
| 332 | + const fullDateList = isWeekly | ||
| 326 | ? getWeekFullDates(startDate.value) | 333 | ? getWeekFullDates(startDate.value) |
| 327 | - : getMonthFullDates(startDate.value) | ||
| 328 | - | ||
| 329 | - // 转成 YYYY-MM-DD | ||
| 330 | - const fullDateKeys = fullDateList.map(d => { | ||
| 331 | - const y = d.getFullYear() | ||
| 332 | - const m = String(d.getMonth() + 1).padStart(2, '0') | ||
| 333 | - const day = String(d.getDate()).padStart(2, '0') | ||
| 334 | - return `${y}-${m}-${day}` | ||
| 335 | - }) | ||
| 336 | - // weeklyData是周/月返回的数据 | ||
| 337 | - const currDataMap = MainCurrentDateType.value === 'week' | 334 | + : getMonthFullDates(startDate.value); |
| 335 | + | ||
| 336 | + const fullDateKeys = fullDateList.map((d) => formatDateKey(d)); | ||
| 337 | + | ||
| 338 | + const currDataMap = isWeekly | ||
| 338 | ? weeklyData.value.weeklyDataMap || {} | 339 | ? weeklyData.value.weeklyDataMap || {} |
| 339 | : weeklyData.value.monthlyDataMap || {}; | 340 | : weeklyData.value.monthlyDataMap || {}; |
| 340 | 341 | ||
| 341 | - const lastDataMap = MainCurrentDateType.value === 'week' | 342 | + const lastDataMap = isWeekly |
| 342 | ? weeklyData.value.lastWeekDataMap || {} | 343 | ? weeklyData.value.lastWeekDataMap || {} |
| 343 | : weeklyData.value.lastMonthDataMap || {}; | 344 | : weeklyData.value.lastMonthDataMap || {}; |
| 344 | - const keyMap = ['volume', 'setCount', 'duration', 'distance', 'count'] | ||
| 345 | - const dataKey = keyMap[NumberTabIndex.value] || 'volume' | ||
| 346 | 345 | ||
| 347 | - // 本周/本月数据 | ||
| 348 | - const currData = fullDateKeys.map(dateStr => currDataMap[dateStr]?.[dataKey] ?? 0) | 346 | + const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume'; |
| 349 | 347 | ||
| 350 | - // ✅ 加这行,看控制台打印的 currData 数组 | ||
| 351 | - console.log('【月数据数组 currData】', currData); | ||
| 352 | - console.log('【当前选中的 dataKey】', dataKey); | ||
| 353 | - console.log('【接口返回的 monthlyDataMap】', weeklyData.value.monthlyDataMap); | 348 | + // 当前周期数据 |
| 349 | + const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0); | ||
| 354 | 350 | ||
| 355 | - // ========================================== | ||
| 356 | - // 核心:周减7天 / 月减1个月(自动适配大小月) | ||
| 357 | - // ========================================== | ||
| 358 | - const lastData = fullDateKeys.map(dateStr => { | ||
| 359 | - const now = new Date(dateStr) | ||
| 360 | - const target = new Date(now) | ||
| 361 | - | ||
| 362 | - if (MainCurrentDateType.value === 'week') { | ||
| 363 | - target.setDate(target.getDate() - 7) | ||
| 364 | - } else { | ||
| 365 | - target.setMonth(target.getMonth() - 1) // ✅ JS自动处理大小月 | ||
| 366 | - } | ||
| 367 | - | ||
| 368 | - const y = target.getFullYear() | ||
| 369 | - const m = String(target.getMonth() + 1).padStart(2, '0') | ||
| 370 | - const day = String(target.getDate()).padStart(2, '0') | ||
| 371 | - const key = `${y}-${m}-${day}` | ||
| 372 | - | ||
| 373 | - return lastDataMap[key]?.[dataKey] ?? 0 | ||
| 374 | - }) | 351 | + // 上一周期数据(按日期偏移匹配) |
| 352 | + const lastData = fullDateKeys.map((dateStr) => { | ||
| 353 | + const targetKey = isWeekly | ||
| 354 | + ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD') | ||
| 355 | + : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD'); | ||
| 356 | + return lastDataMap[targetKey]?.[dataKey] ?? 0; | ||
| 357 | + }); | ||
| 375 | 358 | ||
| 376 | - const currName = MainCurrentDateType.value === 'week' ? '本周' : '本月' | ||
| 377 | - const lastName = MainCurrentDateType.value === 'week' ? '上周' : '上月' | 359 | + const currName = isWeekly ? '本周' : '本月'; |
| 360 | + const lastName = isWeekly ? '上周' : '上月'; | ||
| 378 | 361 | ||
| 379 | return showLastPeriod.value | 362 | return showLastPeriod.value |
| 380 | ? [ | 363 | ? [ |
| 381 | { name: lastName, data: lastData, color: '#5b8ff9' }, | 364 | { name: lastName, data: lastData, color: '#5b8ff9' }, |
| 382 | - { name: currName, data: currData, color: '#ffc53d' } | 365 | + { name: currName, data: currData, color: '#ffc53d' }, |
| 383 | ] | 366 | ] |
| 384 | - : [{ name: currName, data: currData, color: '#ffc53d' }] | ||
| 385 | -}) | 367 | + : [{ name: currName, data: currData, color: '#ffc53d' }]; |
| 368 | +}); | ||
| 386 | 369 | ||
| 387 | -// 处理肌肉弹窗的图标坐标数据 | ||
| 388 | -// 1. 根据当前日期,生成周模式的完整日期列表(周一到周日) | ||
| 389 | -const getWeekFullDates = (baseDateStr) => { | ||
| 390 | - const date = new Date(baseDateStr) | ||
| 391 | - const day = date.getDay() // 0=周日, 1=周一... | ||
| 392 | - const monday = new Date(date) | ||
| 393 | - monday.setDate(date.getDate() - (day === 0 ? 6 : day - 1)) | ||
| 394 | - | ||
| 395 | - const dates = [] | ||
| 396 | - for (let i = 0; i < 7; i++) { | ||
| 397 | - const d = new Date(monday) | ||
| 398 | - d.setDate(monday.getDate() + i) | ||
| 399 | - dates.push(d) | ||
| 400 | - } | ||
| 401 | - return dates | ||
| 402 | -} | 370 | +/** 周期对比数据 */ |
| 371 | +const listData = computed(() => { | ||
| 372 | + if (!weeklyData.value) return []; | ||
| 373 | + | ||
| 374 | + const d = weeklyData.value; | ||
| 375 | + const fields = [ | ||
| 376 | + { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume }, | ||
| 377 | + { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets }, | ||
| 378 | + { label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration }, | ||
| 379 | + { label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance }, | ||
| 380 | + { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, | ||
| 381 | + ]; | ||
| 382 | + | ||
| 383 | + return fields.map(({ label, last, curr }) => { | ||
| 384 | + const lastVal = last ?? 0; | ||
| 385 | + const currVal = curr ?? 0; | ||
| 386 | + return { | ||
| 387 | + label, | ||
| 388 | + lastPeriod: lastVal, | ||
| 389 | + currPeriod: currVal, | ||
| 390 | + diff: currVal - lastVal, | ||
| 391 | + }; | ||
| 392 | + }); | ||
| 393 | +}); | ||
| 403 | 394 | ||
| 404 | -// 2. 根据当前日期,生成月模式的完整日期列表(当月1日到月底) | ||
| 405 | -const getMonthFullDates = (baseDateStr) => { | ||
| 406 | - const date = new Date(baseDateStr) | ||
| 407 | - const year = date.getFullYear() | ||
| 408 | - const month = date.getMonth() | ||
| 409 | - const daysInMonth = new Date(year, month + 1, 0).getDate() | ||
| 410 | - | ||
| 411 | - const dates = [] | ||
| 412 | - for (let i = 1; i <= daysInMonth; i++) { | ||
| 413 | - dates.push(new Date(year, month, i)) | 395 | +/** 肌肉弹窗图表 X 轴 */ |
| 396 | +const muscleChartCategories = computed(() => { | ||
| 397 | + if (MainCurrentDateType.value === 'week') { | ||
| 398 | + return WEEKDAY_LABELS; | ||
| 414 | } | 399 | } |
| 415 | - return dates | ||
| 416 | -} | 400 | + if (!muscleFullDates.value.length) return []; |
| 401 | + return muscleFullDates.value.map((d) => dayjs(d).format('M/D')); | ||
| 402 | +}); | ||
| 417 | 403 | ||
| 418 | -// 3. 接口返回的 date 数组 [年,月,日] 转成 Date 对象 | ||
| 419 | -const parseApiDate = (apiDateArr) => { | ||
| 420 | - // apiDateArr 格式: [2026, 6, 1] | ||
| 421 | - if (!Array.isArray(apiDateArr) || apiDateArr.length !== 3) return null | ||
| 422 | - const [year, month, day] = apiDateArr | ||
| 423 | - return new Date(year, month - 1, day) // 注意: 月份是0-11 | ||
| 424 | -} | 404 | +const muscleVolumeArr = computed(() => muscleProcessedData.value.volumeArr || []); |
| 405 | +const muscleSetsArr = computed(() => muscleProcessedData.value.setsArr || []); | ||
| 406 | + | ||
| 407 | +/** 肌肉弹窗图表 Series */ | ||
| 408 | +const muscleChartSeries = computed(() => { | ||
| 409 | + const data = muscleTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value; | ||
| 410 | + const name = muscleTabIndex.value === 0 ? '容量' : '组数'; | ||
| 411 | + const color = muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9'; | ||
| 425 | 412 | ||
| 426 | -// 接口请求方法 | ||
| 427 | -// 根据动作分类获取指定日期所在周训练数据 | 413 | + return [{ name, data: data || [], color }]; |
| 414 | +}); | ||
| 415 | + | ||
| 416 | +// ==================== API 请求方法 ==================== | ||
| 417 | + | ||
| 418 | +/** 加载周分类趋势数据 */ | ||
| 428 | const loadWeeklyByCategory = async (date) => { | 419 | const loadWeeklyByCategory = async (date) => { |
| 429 | - if (weeklyLoading.value) return; | ||
| 430 | - if (!date) return; | 420 | + if (weeklyLoading.value || !date) return; |
| 431 | weeklyLoading.value = true; | 421 | weeklyLoading.value = true; |
| 432 | try { | 422 | try { |
| 433 | - let categoryId = '0' | ||
| 434 | - if (partId.value && partId.value !== '') { | ||
| 435 | - categoryId = partId.value | ||
| 436 | - } | ||
| 437 | - const res = await TrainingDataApi.weeklyByCategory(date, categoryId, showLastPeriod.value ? 'true' : 'false'); | ||
| 438 | - console.log('===== 周数据接口完整返回 ====='); | ||
| 439 | - console.log(res.data); | ||
| 440 | - console.log('本周weeklyDataMap:', res.data.weeklyDataMap); | ||
| 441 | - console.log('上周lastWeekDataMap:', res.data.lastWeekDataMap); | ||
| 442 | - console.log('上周总数据(lastWeekTotalXXX):', { | ||
| 443 | - volume: res.data.lastWeekTotalVolume, | ||
| 444 | - sets: res.data.lastWeekTotalSets, | ||
| 445 | - duration: res.data.lastWeekTotalDuration, | ||
| 446 | - distance: res.data.lastWeekTotalDistance, | ||
| 447 | - count: res.data.lastWeekTotalCount | ||
| 448 | - }); | 423 | + const categoryId = (partId.value && partId.value !== '') ? partId.value : '0'; |
| 424 | + const res = await TrainingDataApi.weeklyByCategory( | ||
| 425 | + date, | ||
| 426 | + categoryId, | ||
| 427 | + showLastPeriod.value ? 'true' : 'false', | ||
| 428 | + ); | ||
| 449 | weeklyData.value = res.data; | 429 | weeklyData.value = res.data; |
| 450 | } catch (err) { | 430 | } catch (err) { |
| 451 | console.error('动作分类周数据加载失败', err); | 431 | console.error('动作分类周数据加载失败', err); |
| @@ -454,19 +434,17 @@ const loadWeeklyByCategory = async (date) => { | @@ -454,19 +434,17 @@ const loadWeeklyByCategory = async (date) => { | ||
| 454 | } | 434 | } |
| 455 | }; | 435 | }; |
| 456 | 436 | ||
| 457 | -// 根据动作分类获取指定日期所在月训练数据(可选择包含上月数据) | 437 | +/** 加载月分类趋势数据 */ |
| 458 | const loadMonthlyByCategory = async (date) => { | 438 | const loadMonthlyByCategory = async (date) => { |
| 459 | - if (weeklyLoading.value) return; | ||
| 460 | - if (!date) return; | 439 | + if (weeklyLoading.value || !date) return; |
| 461 | weeklyLoading.value = true; | 440 | weeklyLoading.value = true; |
| 462 | try { | 441 | try { |
| 463 | - let categoryId = '0' | ||
| 464 | - if (partId.value && partId.value !== '') { | ||
| 465 | - categoryId = partId.value | ||
| 466 | - } | ||
| 467 | - console.log('是否返回上个的数据:', showLastPeriod.value); | ||
| 468 | - const res = await TrainingDataApi.monthlyByCategory(date, categoryId, showLastPeriod.value ? 'true' : 'false'); | ||
| 469 | - console.log('【动作分类月数据接口返回】', res.data); | 442 | + const categoryId = (partId.value && partId.value !== '') ? partId.value : '0'; |
| 443 | + const res = await TrainingDataApi.monthlyByCategory( | ||
| 444 | + date, | ||
| 445 | + categoryId, | ||
| 446 | + showLastPeriod.value ? 'true' : 'false', | ||
| 447 | + ); | ||
| 470 | weeklyData.value = res.data; | 448 | weeklyData.value = res.data; |
| 471 | } catch (err) { | 449 | } catch (err) { |
| 472 | console.error('动作分类月数据加载失败', err); | 450 | console.error('动作分类月数据加载失败', err); |
| @@ -475,56 +453,41 @@ const loadMonthlyByCategory = async (date) => { | @@ -475,56 +453,41 @@ const loadMonthlyByCategory = async (date) => { | ||
| 475 | } | 453 | } |
| 476 | }; | 454 | }; |
| 477 | 455 | ||
| 478 | -// 加载部位/动作分类 | ||
| 479 | -const loadbodyPartOptions = async () => { | ||
| 480 | - const res = await LeftmotionApi.getAllCategories(); | ||
| 481 | - partOptions.value = [ | ||
| 482 | - { id: '0', name: '全部' }, | ||
| 483 | - ...res.data.map(item => { | ||
| 484 | - return { | ||
| 485 | - id: String(item.id), | ||
| 486 | - name: item.name | ||
| 487 | - } | ||
| 488 | - }) | ||
| 489 | - ] | ||
| 490 | - console.log('部位列表:', partOptions.value); | ||
| 491 | -}; | ||
| 492 | -const currentMuscleId = ref(null) // 当前点击的肌肉ID | ||
| 493 | -// 加载肌肉数据 | ||
| 494 | -const subCategorieList = ref([]) | ||
| 495 | -// 训练数据(来自 loadTrainingData 接口) | ||
| 496 | -const trainingData = ref({ | ||
| 497 | - setCountMap: {}, // 组数数据 { id: count } | ||
| 498 | - totalVolumeMap: {} // 容量数据 { id: volume } | ||
| 499 | -}) | 456 | +/** 加载肌肉部位分类列表 */ |
| 500 | const loadGetSubCategorieList = async () => { | 457 | const loadGetSubCategorieList = async () => { |
| 501 | if (muscleListLoading.value) return; | 458 | if (muscleListLoading.value) return; |
| 502 | muscleListLoading.value = true; | 459 | muscleListLoading.value = true; |
| 503 | try { | 460 | try { |
| 504 | const res = await TrainingDataApi.GetSubCategorieList(); | 461 | const res = await TrainingDataApi.GetSubCategorieList(); |
| 505 | - subCategorieList.value = res.data; | ||
| 506 | - console.log('肌肉数据:', subCategorieList.value); | 462 | + subCategorieList.value = res.data || []; |
| 507 | } catch (err) { | 463 | } catch (err) { |
| 508 | console.error('加载失败:', err); | 464 | console.error('加载失败:', err); |
| 509 | } finally { | 465 | } finally { |
| 510 | muscleListLoading.value = false; | 466 | muscleListLoading.value = false; |
| 511 | } | 467 | } |
| 512 | }; | 468 | }; |
| 513 | -// 加载肌肉表格数据表格 | 469 | + |
| 470 | +/** 加载部位选项列表 */ | ||
| 471 | +const loadbodyPartOptions = async () => { | ||
| 472 | + const res = await LeftmotionApi.getAllCategories(); | ||
| 473 | + partOptions.value = [ | ||
| 474 | + { id: '0', name: '全部' }, | ||
| 475 | + ...res.data.map((item) => ({ id: String(item.id), name: item.name })), | ||
| 476 | + ]; | ||
| 477 | +}; | ||
| 478 | + | ||
| 479 | +/** 加载肌肉部位训练数据(容量/组数分布) */ | ||
| 514 | const loadTrainingData = async (sDate, eDate) => { | 480 | const loadTrainingData = async (sDate, eDate) => { |
| 515 | if (trainingLoading.value) return; | 481 | if (trainingLoading.value) return; |
| 516 | trainingLoading.value = true; | 482 | trainingLoading.value = true; |
| 517 | - const s = String(sDate) | ||
| 518 | - const e = String(eDate) | ||
| 519 | - if (!s || !e || s === 'undefined' || e === 'undefined') return | 483 | + const s = String(sDate); |
| 484 | + const e = String(eDate); | ||
| 485 | + if (!s || !e || s === 'undefined' || e === 'undefined') return; | ||
| 520 | try { | 486 | try { |
| 521 | const res = await TrainingDataApi.mainDataAnalysis(s, e); | 487 | const res = await TrainingDataApi.mainDataAnalysis(s, e); |
| 522 | - console.log('训练数据:', res.data); | ||
| 523 | if (res.data) { | 488 | if (res.data) { |
| 524 | - trainingData.value.setCountMap = res.data.setCountMap || {} | ||
| 525 | - trainingData.value.totalVolumeMap = res.data.totalVolumeMap || {} | ||
| 526 | - console.log('trainingData.value', trainingData.value); | ||
| 527 | - | 489 | + trainingData.value.setCountMap = res.data.setCountMap || {}; |
| 490 | + trainingData.value.totalVolumeMap = res.data.totalVolumeMap || {}; | ||
| 528 | } | 491 | } |
| 529 | } catch (err) { | 492 | } catch (err) { |
| 530 | console.error('加载失败:', err); | 493 | console.error('加载失败:', err); |
| @@ -532,395 +495,343 @@ const loadTrainingData = async (sDate, eDate) => { | @@ -532,395 +495,343 @@ const loadTrainingData = async (sDate, eDate) => { | ||
| 532 | trainingLoading.value = false; | 495 | trainingLoading.value = false; |
| 533 | } | 496 | } |
| 534 | }; | 497 | }; |
| 535 | -// 打开肌肉弹窗需要的数据 | ||
| 536 | -const openMuscleDetail = async (muscleId) => { | ||
| 537 | - console.log('点击了肌肉ID:', muscleId) | ||
| 538 | - currentMuscleId.value = muscleId | ||
| 539 | - muscleShow.value = true | ||
| 540 | - // 默认选中容量 | ||
| 541 | - muscleTabIndex.value = 0 | ||
| 542 | - | ||
| 543 | - // 从 subCategorieList 里找到肌肉名称 | ||
| 544 | - const muscle = subCategorieList.value.find(item => item.id === muscleId) | ||
| 545 | - currentMuscleName.value = muscle?.name || '' | ||
| 546 | 498 | ||
| 547 | - // 根据当前是周/月模式,加载对应接口 | ||
| 548 | - if (MainCurrentDateType.value === 'week') { | ||
| 549 | - await loadWeeklyMuscleStats(muscleId, startDate.value) | ||
| 550 | - } else { | ||
| 551 | - await loadMonthlyMuscleStats(muscleId, startDate.value) | ||
| 552 | - } | ||
| 553 | -} | 499 | +/** 加载单肌肉统计数据(周/月通用) */ |
| 500 | +const loadMuscleStats = async (muscleId, date, isWeekly) => { | ||
| 501 | + try { | ||
| 502 | + const res = isWeekly | ||
| 503 | + ? await TrainingDataApi.weeklyMuscleStats(muscleId, date) | ||
| 504 | + : await TrainingDataApi.monthlyMuscleStats(muscleId, date); | ||
| 554 | 505 | ||
| 555 | -const muscleDetailData = ref({ | ||
| 556 | - muscleId: 0, | ||
| 557 | - dailyStatsList: [] | ||
| 558 | -}) | 506 | + muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] }; |
| 559 | 507 | ||
| 560 | -// 加载 单肌肉-周统计 | ||
| 561 | -const loadWeeklyMuscleStats = async (muscleId, date) => { | ||
| 562 | - try { | ||
| 563 | - const res = await TrainingDataApi.weeklyMuscleStats(muscleId, date) | ||
| 564 | - console.log('单肌肉周数据:', res.data) | ||
| 565 | - muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] } | ||
| 566 | - | ||
| 567 | - // --- 核心:数据对齐处理 --- | ||
| 568 | - // 1. 生成本周完整日期列表 | ||
| 569 | - const fullDates = getWeekFullDates(date) | ||
| 570 | - muscleFullDates.value = fullDates | ||
| 571 | - | ||
| 572 | - // 2. 把接口数据按日期存成映射表 | ||
| 573 | - const dateMap = {} | ||
| 574 | - muscleDetailData.value.dailyStatsList.forEach(item => { | ||
| 575 | - const d = parseApiDate(item.date) | ||
| 576 | - if (d) { | ||
| 577 | - // 转成 YYYY-MM-DD 字符串当key | ||
| 578 | - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | ||
| 579 | - dateMap[key] = { | ||
| 580 | - volume: item.volume, | ||
| 581 | - sets: item.sets | ||
| 582 | - } | ||
| 583 | - } | ||
| 584 | - }) | ||
| 585 | - | ||
| 586 | - // 3. 按完整日期列表填充数据,没有数据填0 | ||
| 587 | - const volumeArr = [] | ||
| 588 | - const setsArr = [] | ||
| 589 | - fullDates.forEach(d => { | ||
| 590 | - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | ||
| 591 | - const data = dateMap[key] || { volume: 0, sets: 0 } | ||
| 592 | - volumeArr.push(data.volume) | ||
| 593 | - setsArr.push(data.sets) | ||
| 594 | - }) | ||
| 595 | - | ||
| 596 | - muscleProcessedData.value = { volumeArr, setsArr } | ||
| 597 | - } catch (err) { | ||
| 598 | - console.error('单肌肉周数据失败', err) | ||
| 599 | - } | ||
| 600 | -} | 508 | + const fullDates = isWeekly ? getWeekFullDates(date) : getMonthFullDates(date); |
| 509 | + muscleFullDates.value = fullDates; | ||
| 601 | 510 | ||
| 602 | -// 加载 单肌肉-月统计 | ||
| 603 | -const loadMonthlyMuscleStats = async (muscleId, date) => { | ||
| 604 | - try { | ||
| 605 | - const res = await TrainingDataApi.monthlyMuscleStats(muscleId, date) | ||
| 606 | - console.log('单肌肉月数据:', res.data) | ||
| 607 | - muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] } | ||
| 608 | - | ||
| 609 | - // --- 核心:数据对齐处理 --- | ||
| 610 | - // 1. 生成本月完整日期列表 | ||
| 611 | - const fullDates = getMonthFullDates(date) | ||
| 612 | - muscleFullDates.value = fullDates | ||
| 613 | - | ||
| 614 | - // 2. 把接口数据按日期存成映射表 | ||
| 615 | - const dateMap = {} | ||
| 616 | - muscleDetailData.value.dailyStatsList.forEach(item => { | ||
| 617 | - const d = parseApiDate(item.date) | 511 | + // 将 API 返回的 dailyStatsList 按日期建立索引 |
| 512 | + const dateMap = {}; | ||
| 513 | + muscleDetailData.value.dailyStatsList.forEach((item) => { | ||
| 514 | + const d = parseApiDate(item.date); | ||
| 618 | if (d) { | 515 | if (d) { |
| 619 | - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | ||
| 620 | - dateMap[key] = { | ||
| 621 | - volume: item.volume, | ||
| 622 | - sets: item.sets | ||
| 623 | - } | 516 | + dateMap[formatDateKey(d)] = { volume: item.volume, sets: item.sets }; |
| 624 | } | 517 | } |
| 625 | - }) | ||
| 626 | - | ||
| 627 | - // 3. 按完整日期列表填充数据,没有数据填0 | ||
| 628 | - const volumeArr = [] | ||
| 629 | - const setsArr = [] | ||
| 630 | - fullDates.forEach(d => { | ||
| 631 | - const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | ||
| 632 | - const data = dateMap[key] || { volume: 0, sets: 0 } | ||
| 633 | - volumeArr.push(data.volume) | ||
| 634 | - setsArr.push(data.sets) | ||
| 635 | - }) | ||
| 636 | - | ||
| 637 | - muscleProcessedData.value = { volumeArr, setsArr } | ||
| 638 | - } catch (err) { | ||
| 639 | - console.error('单肌肉月数据失败', err) | ||
| 640 | - } | ||
| 641 | -} | ||
| 642 | - | ||
| 643 | -const closeMuscleDetail = () => { | ||
| 644 | - | ||
| 645 | - muscleShow.value = false | ||
| 646 | - // currentMuscleId.value = null | ||
| 647 | - // currentMuscleName.value = '' | ||
| 648 | - // muscleTabIndex.value = 0 | ||
| 649 | - // muscleDetailData.value = { muscleId: 0, dailyStatsList: [] } | ||
| 650 | - // muscleFullDates.value = [] | ||
| 651 | - // muscleProcessedData.value = { volumeArr: [], setsArr: [] } | ||
| 652 | - setTimeout(() => { | ||
| 653 | - currentMuscleId.value = null; | ||
| 654 | - currentMuscleName.value = ''; | ||
| 655 | - muscleDetailData.value = { muscleId: 0, dailyStatsList: [] }; | ||
| 656 | - muscleFullDates.value = []; | ||
| 657 | - muscleProcessedData.value = { volumeArr: [], setsArr: [] }; | ||
| 658 | - }, 150); | ||
| 659 | -} | ||
| 660 | - | ||
| 661 | -// 处理好的容量数组(已对齐完整日期) | ||
| 662 | -const muscleVolumeArr = computed(() => { | ||
| 663 | - return muscleProcessedData.value.volumeArr || [] | ||
| 664 | -}) | ||
| 665 | - | ||
| 666 | -// 处理好的组数数组(已对齐完整日期) | ||
| 667 | -const muscleSetsArr = computed(() => { | ||
| 668 | - return muscleProcessedData.value.setsArr || [] | ||
| 669 | -}) | 518 | + }); |
| 670 | 519 | ||
| 671 | -// 弹窗图表的类目(周:周一到周日 / 月:具体日期) | ||
| 672 | -const muscleChartCategories = computed(() => { | ||
| 673 | - if (MainCurrentDateType.value === 'week') { | ||
| 674 | - return ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] | ||
| 675 | - } else { | ||
| 676 | - // 月模式:如果日期列表为空,返回空数组避免报错 | ||
| 677 | - if (!muscleFullDates.value.length) return [] | ||
| 678 | - return muscleFullDates.value.map(d => `${d.getMonth() + 1}/${d.getDate()}`) | ||
| 679 | - } | ||
| 680 | -}) | 520 | + // 按完整日期列表填充数据,缺失的补 0 |
| 521 | + const volumeArr = []; | ||
| 522 | + const setsArr = []; | ||
| 523 | + fullDates.forEach((d) => { | ||
| 524 | + const data = dateMap[formatDateKey(d)] || { volume: 0, sets: 0 }; | ||
| 525 | + volumeArr.push(data.volume); | ||
| 526 | + setsArr.push(data.sets); | ||
| 527 | + }); | ||
| 681 | 528 | ||
| 682 | -// 弹窗图表数据:根据 muscleTabIndex 只显示容量或组数 | ||
| 683 | -const muscleChartSeries = computed(() => { | ||
| 684 | - const data = muscleTabIndex.value === 0 | ||
| 685 | - ? muscleVolumeArr.value | ||
| 686 | - : muscleSetsArr.value | ||
| 687 | - | ||
| 688 | - // 兜底:如果数据为空,返回空数组 | ||
| 689 | - if (!data || !data.length) { | ||
| 690 | - return [{ | ||
| 691 | - name: muscleTabIndex.value === 0 ? '容量' : '组数', | ||
| 692 | - data: [], | ||
| 693 | - color: muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9' | ||
| 694 | - }] | 529 | + muscleProcessedData.value = { volumeArr, setsArr }; |
| 530 | + } catch (err) { | ||
| 531 | + console.error('单肌肉数据加载失败', err); | ||
| 695 | } | 532 | } |
| 533 | +}; | ||
| 696 | 534 | ||
| 697 | - return [{ | ||
| 698 | - name: muscleTabIndex.value === 0 ? '容量' : '组数', | ||
| 699 | - data: data, | ||
| 700 | - color: muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9' | ||
| 701 | - }] | ||
| 702 | -}) | ||
| 703 | - | 535 | +// ==================== 页面事件 ==================== |
| 704 | 536 | ||
| 705 | -// 事件方法 | 537 | +/** 筛选部位切换 */ |
| 706 | const onPartChange = (item) => { | 538 | const onPartChange = (item) => { |
| 707 | - console.log('选中的项:', item); | ||
| 708 | partId.value = item.id; | 539 | partId.value = item.id; |
| 709 | - if (MainCurrentDateType.value === 'week') { | ||
| 710 | - loadWeeklyByCategory(startDate.value); | ||
| 711 | - } else { | ||
| 712 | - loadMonthlyByCategory(startDate.value); | ||
| 713 | - } | 540 | + loadCategoryData(startDate.value); |
| 714 | }; | 541 | }; |
| 715 | 542 | ||
| 716 | -// 当前的日期类型周/月 | 543 | +/** 时间类型切换(周/月) */ |
| 717 | const handleTypeChange = async (type) => { | 544 | const handleTypeChange = async (type) => { |
| 718 | MainCurrentDateType.value = type; | 545 | MainCurrentDateType.value = type; |
| 719 | showLastPeriod.value = false; | 546 | showLastPeriod.value = false; |
| 720 | - loadTrainingData(startDate.value, endDate.value) | 547 | + loadTrainingData(startDate.value, endDate.value); |
| 721 | 548 | ||
| 722 | if (startDate.value) { | 549 | if (startDate.value) { |
| 723 | - if (type === 'week') { | ||
| 724 | - await loadWeeklyByCategory(startDate.value) | ||
| 725 | - } else { | ||
| 726 | - await loadMonthlyByCategory(startDate.value) | ||
| 727 | - } | 550 | + await loadCategoryData(startDate.value); |
| 728 | } | 551 | } |
| 729 | 552 | ||
| 730 | - // 如果弹窗打开,自动刷新肌肉数据 | 553 | + // 如果肌肉弹窗打开,同步刷新 |
| 731 | if (muscleShow.value && currentMuscleId.value) { | 554 | if (muscleShow.value && currentMuscleId.value) { |
| 732 | - if (type === 'week') { | ||
| 733 | - await loadWeeklyMuscleStats(currentMuscleId.value, startDate.value) | ||
| 734 | - } else { | ||
| 735 | - await loadMonthlyMuscleStats(currentMuscleId.value, startDate.value) | ||
| 736 | - } | 555 | + await loadMuscleData(currentMuscleId.value, startDate.value); |
| 737 | } | 556 | } |
| 738 | }; | 557 | }; |
| 739 | -// 处理日期改变 | 558 | + |
| 559 | +/** 日期范围变更 */ | ||
| 740 | const handleDateChange = async (data) => { | 560 | const handleDateChange = async (data) => { |
| 741 | - console.log(data.startDate, data.endDate); | ||
| 742 | startDate.value = data.startDate; | 561 | startDate.value = data.startDate; |
| 743 | endDate.value = data.endDate; | 562 | endDate.value = data.endDate; |
| 744 | - await loadTrainingData(startDate.value, endDate.value) | ||
| 745 | - if (MainCurrentDateType.value === 'week') { | ||
| 746 | - loadWeeklyByCategory(startDate.value); | ||
| 747 | - } else { | ||
| 748 | - loadMonthlyByCategory(startDate.value); | ||
| 749 | - } | ||
| 750 | - // 弹窗打开 → 刷新肌肉详情 | 563 | + await loadTrainingData(startDate.value, endDate.value); |
| 564 | + loadCategoryData(startDate.value); | ||
| 565 | + | ||
| 751 | if (muscleShow.value && currentMuscleId.value) { | 566 | if (muscleShow.value && currentMuscleId.value) { |
| 752 | - if (MainCurrentDateType.value === 'week') { | ||
| 753 | - await loadWeeklyMuscleStats(currentMuscleId.value, startDate.value) | ||
| 754 | - } else { | ||
| 755 | - await loadMonthlyMuscleStats(currentMuscleId.value, startDate.value) | ||
| 756 | - } | 567 | + await loadMuscleData(currentMuscleId.value, startDate.value); |
| 757 | } | 568 | } |
| 758 | }; | 569 | }; |
| 759 | -// 处理是否返回上周/月 | 570 | + |
| 571 | +/** 打开肌肉详情弹窗 */ | ||
| 572 | +const openMuscleDetail = async (muscleId) => { | ||
| 573 | + currentMuscleId.value = muscleId; | ||
| 574 | + muscleShow.value = true; | ||
| 575 | + muscleTabIndex.value = 0; | ||
| 576 | + | ||
| 577 | + const muscle = subCategorieList.value.find((item) => item.id === muscleId); | ||
| 578 | + currentMuscleName.value = muscle?.name || ''; | ||
| 579 | + | ||
| 580 | + await loadMuscleData(muscleId, startDate.value); | ||
| 581 | +}; | ||
| 582 | + | ||
| 583 | +/** 关闭肌肉详情弹窗 */ | ||
| 584 | +const closeMuscleDetail = () => { | ||
| 585 | + muscleShow.value = false; | ||
| 586 | + setTimeout(() => { | ||
| 587 | + currentMuscleId.value = null; | ||
| 588 | + currentMuscleName.value = ''; | ||
| 589 | + muscleDetailData.value = { muscleId: 0, dailyStatsList: [] }; | ||
| 590 | + muscleFullDates.value = []; | ||
| 591 | + muscleProcessedData.value = { volumeArr: [], setsArr: [] }; | ||
| 592 | + }, 150); | ||
| 593 | +}; | ||
| 594 | + | ||
| 595 | +/** 切换显示上一周期数据 */ | ||
| 760 | const toggleLastWeek = () => { | 596 | const toggleLastWeek = () => { |
| 761 | - showLastPeriod.value = !showLastPeriod.value | 597 | + showLastPeriod.value = !showLastPeriod.value; |
| 762 | if (startDate.value) { | 598 | if (startDate.value) { |
| 763 | - if (MainCurrentDateType.value === 'week') { | ||
| 764 | - loadWeeklyByCategory(startDate.value) | ||
| 765 | - } else { | ||
| 766 | - loadMonthlyByCategory(startDate.value) | ||
| 767 | - } | 599 | + loadCategoryData(startDate.value); |
| 768 | } | 600 | } |
| 769 | -} | 601 | +}; |
| 602 | + | ||
| 603 | +// ==================== 生命周期 ==================== | ||
| 770 | 604 | ||
| 771 | onMounted(async () => { | 605 | onMounted(async () => { |
| 772 | await loadbodyPartOptions(); | 606 | await loadbodyPartOptions(); |
| 773 | await loadGetSubCategorieList(); | 607 | await loadGetSubCategorieList(); |
| 774 | if (startDate.value && endDate.value) { | 608 | if (startDate.value && endDate.value) { |
| 775 | - await loadTrainingData(startDate.value, endDate.value) | ||
| 776 | - if (MainCurrentDateType.value === 'week') { | ||
| 777 | - await loadWeeklyByCategory(startDate.value) | ||
| 778 | - } else { | ||
| 779 | - await loadMonthlyByCategory(startDate.value) | ||
| 780 | - } | 609 | + await loadTrainingData(startDate.value, endDate.value); |
| 610 | + await loadCategoryData(startDate.value); | ||
| 781 | } | 611 | } |
| 782 | - | ||
| 783 | }); | 612 | }); |
| 784 | </script> | 613 | </script> |
| 785 | 614 | ||
| 786 | <style lang="scss" scoped> | 615 | <style lang="scss" scoped> |
| 787 | -.training-data-page { | ||
| 788 | - min-height: 100vh; | 616 | +// ==================== CSS 变量 ==================== |
| 617 | +$color-primary: #202932; | ||
| 618 | +$color-bg: #f8fafc; | ||
| 619 | +$color-card: #ffffff; | ||
| 620 | +$color-border: #f1f5f9; | ||
| 621 | +$color-text: #0f172a; | ||
| 622 | +$color-sub: #475569; | ||
| 623 | +$color-muted: #94a3b8; | ||
| 624 | +$color-green: #10b981; | ||
| 625 | +$color-green-bg: #ecfdf5; | ||
| 626 | +$color-green-border: #a7f3d0; | ||
| 627 | +$color-blue: #5b8ff9; | ||
| 628 | +$color-yellow: #ffc53d; | ||
| 629 | +$color-red: #ff7875; | ||
| 630 | +$radius-card: 24rpx; | ||
| 631 | +$radius-tab: 50rpx; | ||
| 632 | +$radius-item: 12rpx; | ||
| 633 | + | ||
| 634 | +// ==================== 公共 Tab 样式 ==================== | ||
| 635 | +.tab-group { | ||
| 789 | display: flex; | 636 | display: flex; |
| 790 | - flex-direction: column; | ||
| 791 | - padding: 30rpx 24rpx 0; | ||
| 792 | - box-sizing: border-box; | ||
| 793 | - overflow: auto; | 637 | + gap: 16rpx; |
| 638 | + margin-bottom: 12rpx; | ||
| 794 | 639 | ||
| 795 | - .content-scroll { | ||
| 796 | - height: calc(100vh - 400rpx); | ||
| 797 | - // #ifdef H5 | ||
| 798 | - height: calc(100vh - 185px); | ||
| 799 | - // #endif | ||
| 800 | - width: 100%; | ||
| 801 | - box-sizing: border-box; | ||
| 802 | - overflow: auto; | 640 | + &.flex-wrap { |
| 641 | + flex-wrap: wrap; | ||
| 803 | } | 642 | } |
| 643 | +} | ||
| 804 | 644 | ||
| 805 | - .section { | ||
| 806 | - padding: 30rpx 20rpx; | ||
| 807 | - box-sizing: border-box; | ||
| 808 | - background-color: #fff; | ||
| 809 | - border-radius: 15rpx; | ||
| 810 | - margin-top: 20rpx; | 645 | +.tab-item { |
| 646 | + border: 1rpx solid #e2e8f0; | ||
| 647 | + border-radius: $radius-tab; | ||
| 648 | + background-color: $color-card; | ||
| 649 | + padding: 10rpx 24rpx; | ||
| 650 | + font-size: 24rpx; | ||
| 651 | + color: $color-sub; | ||
| 652 | + transition: all 0.2s ease; | ||
| 653 | + cursor: pointer; | ||
| 811 | 654 | ||
| 812 | - &:last-child { | ||
| 813 | - padding-bottom: env(safe-area-inset-bottom); | ||
| 814 | - margin-bottom: 110rpx; | 655 | + &.active { |
| 656 | + background-color: $color-primary; | ||
| 657 | + color: $color-card; | ||
| 658 | + border-color: $color-primary; | ||
| 659 | + font-weight: 600; | ||
| 815 | } | 660 | } |
| 816 | 661 | ||
| 817 | - .title { | ||
| 818 | - font-size: 30rpx; | ||
| 819 | - font-weight: bold; | ||
| 820 | - margin-bottom: 20rpx; | 662 | + &:active { |
| 663 | + opacity: 0.8; | ||
| 821 | } | 664 | } |
| 665 | +} | ||
| 822 | 666 | ||
| 823 | - .tip { | 667 | +.tip-text { |
| 824 | font-size: 22rpx; | 668 | font-size: 22rpx; |
| 669 | + color: $color-muted; | ||
| 825 | margin-bottom: 20rpx; | 670 | margin-bottom: 20rpx; |
| 671 | + display: block; | ||
| 672 | +} | ||
| 673 | + | ||
| 674 | +// ==================== 页面主容器 ==================== | ||
| 675 | +.training-data-page { | ||
| 676 | + display: flex; | ||
| 677 | + flex-direction: column; | ||
| 678 | + | ||
| 679 | + width: 100vw; | ||
| 680 | + background-color: $color-bg; | ||
| 681 | + overflow: hidden; | ||
| 682 | + | ||
| 683 | + // 顶部固定时间选择栏 | ||
| 684 | + .top-time-picker { | ||
| 685 | + padding: 20rpx 24rpx; | ||
| 686 | + box-sizing: border-box; | ||
| 687 | + background-color: $color-card; | ||
| 688 | + border-bottom: 1rpx solid $color-border; | ||
| 689 | + z-index: 10; | ||
| 826 | } | 690 | } |
| 827 | 691 | ||
| 828 | - .img { | 692 | + // 主体可滚动区域 |
| 693 | + .main-content { | ||
| 694 | + flex: 1; | ||
| 695 | + height: 0; | ||
| 696 | + min-height: 0; | ||
| 697 | + overflow: hidden; | ||
| 698 | + | ||
| 699 | + .scroll-area { | ||
| 829 | width: 100%; | 700 | width: 100%; |
| 830 | - height: 400rpx; | ||
| 831 | - } | 701 | + height: 100%; |
| 832 | 702 | ||
| 833 | - .tabs { | 703 | + .scroll-inner { |
| 704 | + padding: 24rpx; | ||
| 834 | display: flex; | 705 | display: flex; |
| 835 | - gap: 20rpx; | 706 | + flex-direction: column; |
| 707 | + gap: 24rpx; | ||
| 708 | + } | ||
| 709 | + } | ||
| 710 | + } | ||
| 836 | 711 | ||
| 837 | - .tab { | ||
| 838 | - border: 1rpx solid #eee; | ||
| 839 | - border-radius: 50rpx; | ||
| 840 | - background-color: #fff; | ||
| 841 | - padding: 10rpx 20rpx; | ||
| 842 | - font-size: 24rpx; | 712 | + // 卡片通用容器 |
| 713 | + .card-section { | ||
| 714 | + background-color: $color-card; | ||
| 715 | + border-radius: $radius-card; | ||
| 716 | + padding: 28rpx 24rpx; | ||
| 717 | + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02); | ||
| 718 | + border: 1rpx solid $color-border; | ||
| 719 | + | ||
| 720 | + .section-header { | ||
| 721 | + display: flex; | ||
| 722 | + align-items: center; | ||
| 843 | margin-bottom: 20rpx; | 723 | margin-bottom: 20rpx; |
| 844 | 724 | ||
| 845 | - &.active { | ||
| 846 | - background-color: #202932; | ||
| 847 | - color: #fff; | ||
| 848 | - border: none; | 725 | + &.space-between { |
| 726 | + justify-content: space-between; | ||
| 727 | + } | ||
| 728 | + | ||
| 729 | + .title { | ||
| 730 | + font-size: 30rpx; | ||
| 731 | + font-weight: 700; | ||
| 732 | + color: $color-text; | ||
| 849 | } | 733 | } |
| 734 | + | ||
| 735 | + .part-select-wrapper { | ||
| 736 | + min-width: 140rpx; | ||
| 850 | } | 737 | } |
| 851 | } | 738 | } |
| 852 | 739 | ||
| 853 | - /* ========== 核心网格布局 已修复 ========== */ | ||
| 854 | - .detail-part { | 740 | + // 肌肉部位网格 |
| 741 | + .muscle-grid { | ||
| 855 | display: grid; | 742 | display: grid; |
| 856 | grid-template-columns: repeat(4, 1fr); | 743 | grid-template-columns: repeat(4, 1fr); |
| 857 | - gap: 5rpx; | 744 | + gap: 10rpx; |
| 858 | 745 | ||
| 859 | - .part-item { | 746 | + .grid-item { |
| 860 | width: 100%; | 747 | width: 100%; |
| 861 | height: 140rpx; | 748 | height: 140rpx; |
| 862 | - // aspect-ratio: 1 / 1; | ||
| 863 | - /* 强制正方形 */ | ||
| 864 | - background-color: #eee; | 749 | + background-color: $color-bg; |
| 750 | + border: 1rpx solid $color-border; | ||
| 751 | + border-radius: $radius-item; | ||
| 865 | display: flex; | 752 | display: flex; |
| 753 | + flex-direction: column; | ||
| 866 | align-items: center; | 754 | align-items: center; |
| 867 | justify-content: center; | 755 | justify-content: center; |
| 868 | - flex-direction: column; | ||
| 869 | - font-size: 24rpx; | ||
| 870 | gap: 10rpx; | 756 | gap: 10rpx; |
| 871 | - border-radius: 5rpx; | 757 | + transition: transform 0.15s, background-color 0.15s; |
| 872 | 758 | ||
| 873 | - .num { | ||
| 874 | - font-weight: bold; | ||
| 875 | - color: #000; | 759 | + &:active { |
| 760 | + transform: scale(0.96); | ||
| 761 | + background-color: #f1f5f9; | ||
| 876 | } | 762 | } |
| 763 | + | ||
| 764 | + .item-name { | ||
| 765 | + font-size: 24rpx; | ||
| 766 | + color: $color-sub; | ||
| 877 | } | 767 | } |
| 878 | 768 | ||
| 879 | - /* 总计自适应占格 */ | ||
| 880 | - .total-item { | ||
| 881 | - grid-column: span var(--remain-span); | ||
| 882 | - height: 140rpx; | 769 | + .item-value { |
| 770 | + font-size: 26rpx; | ||
| 771 | + font-weight: 700; | ||
| 772 | + color: #000000; | ||
| 773 | + | ||
| 774 | + &.highlight { | ||
| 775 | + color: $color-green; | ||
| 883 | } | 776 | } |
| 884 | } | 777 | } |
| 885 | 778 | ||
| 886 | - .header { | ||
| 887 | - display: flex; | ||
| 888 | - justify-content: space-between; | ||
| 889 | - align-items: center; | 779 | + &.total-item { |
| 780 | + background-color: $color-green-bg; | ||
| 781 | + border-color: $color-green-border; | ||
| 782 | + | ||
| 783 | + .item-name { | ||
| 784 | + color: #047857; | ||
| 785 | + font-weight: 600; | ||
| 786 | + } | ||
| 787 | + } | ||
| 788 | + } | ||
| 890 | } | 789 | } |
| 891 | 790 | ||
| 892 | - .statistical { | 791 | + // 图表容器 |
| 792 | + .chart-container { | ||
| 893 | width: 100%; | 793 | width: 100%; |
| 894 | height: 400rpx; | 794 | height: 400rpx; |
| 895 | } | 795 | } |
| 896 | 796 | ||
| 897 | - .chose { | 797 | + // 显示上期数据勾选 |
| 798 | + .toggle-checkbox { | ||
| 799 | + display: flex; | ||
| 800 | + align-items: center; | ||
| 801 | + margin-top: 20rpx; | ||
| 898 | font-size: 24rpx; | 802 | font-size: 24rpx; |
| 803 | + color: #333333; | ||
| 804 | + | ||
| 805 | + .toggle-label { | ||
| 806 | + margin-left: 8rpx; | ||
| 807 | + } | ||
| 899 | } | 808 | } |
| 900 | 809 | ||
| 901 | - .compare { | 810 | + // 周期对比区域 |
| 811 | + .compare-section { | ||
| 902 | margin-top: 30rpx; | 812 | margin-top: 30rpx; |
| 903 | 813 | ||
| 904 | - .header-row { | 814 | + .compare-header { |
| 905 | display: flex; | 815 | display: flex; |
| 906 | justify-content: space-between; | 816 | justify-content: space-between; |
| 907 | align-items: center; | 817 | align-items: center; |
| 818 | + margin-bottom: 20rpx; | ||
| 908 | 819 | ||
| 909 | - .title { | 820 | + .sub-title { |
| 910 | font-size: 36rpx; | 821 | font-size: 36rpx; |
| 911 | - font-weight: bold; | ||
| 912 | - color: #333; | 822 | + font-weight: 700; |
| 823 | + color: #333333; | ||
| 913 | } | 824 | } |
| 914 | 825 | ||
| 915 | .legend-group { | 826 | .legend-group { |
| 916 | display: flex; | 827 | display: flex; |
| 917 | - gap: 100rpx; | 828 | + gap: 32rpx; |
| 918 | 829 | ||
| 919 | .legend-item { | 830 | .legend-item { |
| 920 | display: flex; | 831 | display: flex; |
| 921 | align-items: center; | 832 | align-items: center; |
| 922 | font-size: 24rpx; | 833 | font-size: 24rpx; |
| 923 | - color: #666; | 834 | + color: #666666; |
| 924 | 835 | ||
| 925 | .dot { | 836 | .dot { |
| 926 | width: 20rpx; | 837 | width: 20rpx; |
| @@ -928,22 +839,18 @@ onMounted(async () => { | @@ -928,22 +839,18 @@ onMounted(async () => { | ||
| 928 | border-radius: 4rpx; | 839 | border-radius: 4rpx; |
| 929 | margin-right: 8rpx; | 840 | margin-right: 8rpx; |
| 930 | 841 | ||
| 931 | - &.blue { | ||
| 932 | - background-color: #5b8ff9; | ||
| 933 | - } | ||
| 934 | - | ||
| 935 | - &.yellow { | ||
| 936 | - background-color: #ffc53d; | ||
| 937 | - } | 842 | + &.blue { background-color: $color-blue; } |
| 843 | + &.yellow { background-color: $color-yellow; } | ||
| 938 | } | 844 | } |
| 939 | } | 845 | } |
| 940 | } | 846 | } |
| 941 | } | 847 | } |
| 942 | 848 | ||
| 943 | .compare-card { | 849 | .compare-card { |
| 944 | - background-color: #ffffff; | 850 | + background-color: $color-card; |
| 945 | border-radius: 20rpx; | 851 | border-radius: 20rpx; |
| 946 | border: 5rpx solid #efefef; | 852 | border: 5rpx solid #efefef; |
| 853 | + overflow: hidden; | ||
| 947 | 854 | ||
| 948 | .data-list { | 855 | .data-list { |
| 949 | .data-row { | 856 | .data-row { |
| @@ -953,31 +860,25 @@ onMounted(async () => { | @@ -953,31 +860,25 @@ onMounted(async () => { | ||
| 953 | border-bottom: 1rpx solid #f0f0f0; | 860 | border-bottom: 1rpx solid #f0f0f0; |
| 954 | font-size: 26rpx; | 861 | font-size: 26rpx; |
| 955 | 862 | ||
| 956 | - &:last-child { | ||
| 957 | - border-bottom: none; | ||
| 958 | - } | ||
| 959 | - | ||
| 960 | - &:nth-child(odd) { | ||
| 961 | - background-color: #f9fafc; | ||
| 962 | - } | 863 | + &:last-child { border-bottom: none; } |
| 864 | + &:nth-child(odd) { background-color: #f9fafc; } | ||
| 963 | 865 | ||
| 964 | .label { | 866 | .label { |
| 965 | flex: 1.5; | 867 | flex: 1.5; |
| 966 | - color: #333; | 868 | + color: #333333; |
| 967 | text-align: center; | 869 | text-align: center; |
| 968 | } | 870 | } |
| 969 | 871 | ||
| 970 | .value { | 872 | .value { |
| 971 | flex: 1; | 873 | flex: 1; |
| 972 | text-align: center; | 874 | text-align: center; |
| 973 | - color: #333; | 875 | + color: #333333; |
| 974 | 876 | ||
| 975 | &.diff { | 877 | &.diff { |
| 976 | - color: #666; | ||
| 977 | - } | 878 | + font-weight: 600; |
| 978 | 879 | ||
| 979 | - &.negative { | ||
| 980 | - color: #ff7875; | 880 | + &.positive { color: $color-green; } |
| 881 | + &.negative { color: $color-red; } | ||
| 981 | } | 882 | } |
| 982 | } | 883 | } |
| 983 | } | 884 | } |
| @@ -986,7 +887,16 @@ onMounted(async () => { | @@ -986,7 +887,16 @@ onMounted(async () => { | ||
| 986 | } | 887 | } |
| 987 | } | 888 | } |
| 988 | 889 | ||
| 989 | - .muscleShow-popup { | 890 | + // 底部安全间距 |
| 891 | + .bottom-spacer { | ||
| 892 | + height: env(safe-area-inset-bottom); | ||
| 893 | + padding-bottom: 110rpx; | ||
| 894 | + } | ||
| 895 | +} | ||
| 896 | + | ||
| 897 | +// ==================== 肌肉弹窗样式 ==================== | ||
| 898 | +.muscleShow-popup { | ||
| 899 | + padding-bottom: 30rpx; | ||
| 990 | 900 | ||
| 991 | /* #ifdef MP-WEIXIN */ | 901 | /* #ifdef MP-WEIXIN */ |
| 992 | min-height: 40vh; | 902 | min-height: 40vh; |
| @@ -996,6 +906,7 @@ onMounted(async () => { | @@ -996,6 +906,7 @@ onMounted(async () => { | ||
| 996 | min-height: 55vh; | 906 | min-height: 55vh; |
| 997 | /* #endif */ | 907 | /* #endif */ |
| 998 | 908 | ||
| 909 | + .popup-body { | ||
| 999 | .muscle-name { | 910 | .muscle-name { |
| 1000 | font-size: 32rpx; | 911 | font-size: 32rpx; |
| 1001 | font-weight: bold; | 912 | font-weight: bold; |
| @@ -1006,25 +917,12 @@ onMounted(async () => { | @@ -1006,25 +917,12 @@ onMounted(async () => { | ||
| 1006 | display: flex; | 917 | display: flex; |
| 1007 | gap: 20rpx; | 918 | gap: 20rpx; |
| 1008 | padding: 20rpx; | 919 | padding: 20rpx; |
| 1009 | - | ||
| 1010 | - .tab { | ||
| 1011 | - border: 1rpx solid #eee; | ||
| 1012 | - border-radius: 50rpx; | ||
| 1013 | - background-color: #fff; | ||
| 1014 | - padding: 10rpx 20rpx; | ||
| 1015 | - font-size: 24rpx; | ||
| 1016 | - | ||
| 1017 | - &.active { | ||
| 1018 | - background-color: #202932; | ||
| 1019 | - color: #fff; | ||
| 1020 | - border: none; | ||
| 1021 | - } | ||
| 1022 | - } | ||
| 1023 | } | 920 | } |
| 1024 | 921 | ||
| 1025 | .tip { | 922 | .tip { |
| 1026 | font-size: 22rpx; | 923 | font-size: 22rpx; |
| 1027 | padding: 0 20rpx 20rpx; | 924 | padding: 0 20rpx 20rpx; |
| 925 | + color: $color-muted; | ||
| 1028 | } | 926 | } |
| 1029 | 927 | ||
| 1030 | .statistical { | 928 | .statistical { |
| 1 | <template> | 1 | <template> |
| 2 | <view class="plan-page"> | 2 | <view class="plan-page"> |
| 3 | - <!-- 顶部横幅 --> | ||
| 4 | - <view class="banner-wrapper"> | ||
| 5 | - <swiper class="swiper"> | ||
| 6 | - <swiper-item class="swiper-item"> | ||
| 7 | - <image class="img" | ||
| 8 | - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png" | ||
| 9 | - mode="aspectFill" /> | ||
| 10 | - </swiper-item> | ||
| 11 | - <swiper-item class="swiper-item"> | ||
| 12 | - <image class="img" | ||
| 13 | - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/38_1773628032712.png" | ||
| 14 | - mode="aspectFill" /> | ||
| 15 | - </swiper-item> | ||
| 16 | - </swiper> | ||
| 17 | - </view> | ||
| 18 | 3 | ||
| 19 | - <!-- 筛选条 --> | ||
| 20 | - <!-- 后端计划分类的计划列表只返回了难度字段,所有只有这个字段的筛选生效了 --> | ||
| 21 | <view class="filter-bar"> | 4 | <view class="filter-bar"> |
| 22 | - <view v-for="(item, index) in filterList" :key="index" class="filter-item" | ||
| 23 | - :class="{ active: activeFilter === index }" @click=" | ||
| 24 | - activeFilter = index; | ||
| 25 | - toggleDrawer(index); | ||
| 26 | - "> | 5 | + <view v-for="(item, index) in filterList" :key="item" class="filter-item" |
| 6 | + :class="{ active: activeFilter === index }" @click="toggleDrawer(index)"> | ||
| 27 | <text class="text">{{ | 7 | <text class="text">{{ |
| 28 | selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}` | 8 | selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}` |
| 29 | }}</text> | 9 | }}</text> |
| @@ -33,20 +13,17 @@ | @@ -33,20 +13,17 @@ | ||
| 33 | <uni-icons type="search" size="20" color="#333" | 13 | <uni-icons type="search" size="20" color="#333" |
| 34 | style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx" @click="navigateToSearch" /> | 14 | style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx" @click="navigateToSearch" /> |
| 35 | </view> | 15 | </view> |
| 36 | - </view> | ||
| 37 | 16 | ||
| 38 | - <!-- 下拉抽屉组件 --> | ||
| 39 | <view class="filter-drawer" v-show="drawerShow" @click.stop> | 17 | <view class="filter-drawer" v-show="drawerShow" @click.stop> |
| 40 | <view class="drawer-content"> | 18 | <view class="drawer-content"> |
| 41 | - <view v-for="(item, idx) in filterDrawerData" :key="idx" class="drawer-item" @click="selectDrawerItem(item)"> | 19 | + <view v-for="item in filterDrawerData" :key="item" class="drawer-item" @click="selectDrawerItem(item)"> |
| 42 | <text class="drawer-item-text">{{ item }}</text> | 20 | <text class="drawer-item-text">{{ item }}</text> |
| 43 | </view> | 21 | </view> |
| 44 | </view> | 22 | </view> |
| 45 | </view> | 23 | </view> |
| 24 | + </view> | ||
| 46 | 25 | ||
| 47 | - <!-- 主体内容:左侧分类 + 右侧计划列表 --> | ||
| 48 | <view class="content-layout"> | 26 | <view class="content-layout"> |
| 49 | - <!-- 左侧分类 --> | ||
| 50 | <scroll-view class="sidebar" scroll-y enable-flex> | 27 | <scroll-view class="sidebar" scroll-y enable-flex> |
| 51 | <view v-for="(item, index) in categoryList" :key="item.id" class="category-item" | 28 | <view v-for="(item, index) in categoryList" :key="item.id" class="category-item" |
| 52 | :class="{ active: activeCategory === index }" @click="switchCategory(index)"> | 29 | :class="{ active: activeCategory === index }" @click="switchCategory(index)"> |
| @@ -54,7 +31,6 @@ | @@ -54,7 +31,6 @@ | ||
| 54 | </view> | 31 | </view> |
| 55 | </scroll-view> | 32 | </scroll-view> |
| 56 | 33 | ||
| 57 | - <!-- 右侧计划列表 --> | ||
| 58 | <scroll-view class="plan-list-wrap" scroll-y enable-flex> | 34 | <scroll-view class="plan-list-wrap" scroll-y enable-flex> |
| 59 | <view v-for="plan in currentPlanList" :key="plan.id" class="plan-card" @tap="navigateToDetail(plan)"> | 35 | <view v-for="plan in currentPlanList" :key="plan.id" class="plan-card" @tap="navigateToDetail(plan)"> |
| 60 | <image class="plan-cover" :src="plan.cover" mode="aspectFill" /> | 36 | <image class="plan-cover" :src="plan.cover" mode="aspectFill" /> |
| @@ -70,7 +46,6 @@ | @@ -70,7 +46,6 @@ | ||
| 70 | </view> | 46 | </view> |
| 71 | </view> | 47 | </view> |
| 72 | </view> | 48 | </view> |
| 73 | - <!-- 空状态 --> | ||
| 74 | <view v-if="currentPlanList.length === 0" class="empty-tip"> | 49 | <view v-if="currentPlanList.length === 0" class="empty-tip"> |
| 75 | <text>该分类暂无训练计划</text> | 50 | <text>该分类暂无训练计划</text> |
| 76 | </view> | 51 | </view> |
| @@ -81,11 +56,39 @@ | @@ -81,11 +56,39 @@ | ||
| 81 | 56 | ||
| 82 | <script setup> | 57 | <script setup> |
| 83 | import { computed, onMounted, ref } from 'vue'; | 58 | import { computed, onMounted, ref } from 'vue'; |
| 84 | -// ✅ getCurrentPages 是全局 API,无需 import | ||
| 85 | import QueryPlanApi from '@/sheep/api/plan/queryplan'; | 59 | import QueryPlanApi from '@/sheep/api/plan/queryplan'; |
| 60 | +import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea'; | ||
| 61 | +const menuButtonHeight = ref(getMenuButtonHeight()); | ||
| 62 | +const topSafeArea = ref(getTopSafeArea()); | ||
| 63 | +const PageHeight = ref(topSafeArea.value); | ||
| 64 | +// #ifdef MP-WEIXIN | ||
| 65 | +PageHeight.value = menuButtonHeight.value + topSafeArea.value; | ||
| 66 | +// #endif | ||
| 67 | +const filterList = ['频率', '难度', '场景', '人群']; | ||
| 68 | +const filterOptionsMap = { | ||
| 69 | + 频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'], | ||
| 70 | + 难度: ['不限', '初阶', '中阶', '高阶'], | ||
| 71 | + 场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'], | ||
| 72 | + 人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'], | ||
| 73 | +}; | ||
| 74 | +const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' }; | ||
| 75 | +const filterFieldMap = { 频率: 'frequency', 难度: 'difficultyLevel', 场景: 'scene', 人群: 'population' }; | ||
| 76 | +const filterMap = { | ||
| 77 | + 频率: { | ||
| 78 | + 不限: 0, | ||
| 79 | + '1练/周': 1, | ||
| 80 | + '2练/周': 2, | ||
| 81 | + '3练/周': 3, | ||
| 82 | + '4练/周': 4, | ||
| 83 | + '5练/周': 5, | ||
| 84 | + '6练/周': 6, | ||
| 85 | + '7练/周': 7, | ||
| 86 | + }, | ||
| 87 | + 难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 }, | ||
| 88 | + 场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 }, | ||
| 89 | + 人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 }, | ||
| 90 | +}; | ||
| 86 | 91 | ||
| 87 | -// ====== 筛选相关(保持原样)====== | ||
| 88 | -const filterList = ref(['频率', '难度', '场景', '人群']); | ||
| 89 | const drawerShow = ref(false); | 92 | const drawerShow = ref(false); |
| 90 | const activeFilter = ref(null); | 93 | const activeFilter = ref(null); |
| 91 | const selectedFilters = ref({ | 94 | const selectedFilters = ref({ |
| @@ -94,15 +97,9 @@ const selectedFilters = ref({ | @@ -94,15 +97,9 @@ const selectedFilters = ref({ | ||
| 94 | 场景: '不限', | 97 | 场景: '不限', |
| 95 | 人群: '不限', | 98 | 人群: '不限', |
| 96 | }); | 99 | }); |
| 97 | -const filterOptionsMap = { | ||
| 98 | - 频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'], | ||
| 99 | - 难度: ['不限', '初阶', '中阶', '高阶'], | ||
| 100 | - 场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'], | ||
| 101 | - 人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'], | ||
| 102 | -}; | ||
| 103 | 100 | ||
| 104 | const filterDrawerData = computed(() => { | 101 | const filterDrawerData = computed(() => { |
| 105 | - const currentFilterName = filterList.value[activeFilter.value]; | 102 | + const currentFilterName = filterList[activeFilter.value]; |
| 106 | return filterOptionsMap[currentFilterName] || []; | 103 | return filterOptionsMap[currentFilterName] || []; |
| 107 | }); | 104 | }); |
| 108 | 105 | ||
| @@ -116,72 +113,35 @@ const toggleDrawer = (index) => { | @@ -116,72 +113,35 @@ const toggleDrawer = (index) => { | ||
| 116 | }; | 113 | }; |
| 117 | 114 | ||
| 118 | const selectDrawerItem = (item) => { | 115 | const selectDrawerItem = (item) => { |
| 119 | - const currentFilterName = filterList.value[activeFilter.value]; | 116 | + const currentFilterName = filterList[activeFilter.value]; |
| 120 | selectedFilters.value[currentFilterName] = item; | 117 | selectedFilters.value[currentFilterName] = item; |
| 121 | drawerShow.value = false; | 118 | drawerShow.value = false; |
| 122 | console.log('筛选条件更新:', selectedFilters.value); | 119 | console.log('筛选条件更新:', selectedFilters.value); |
| 123 | }; | 120 | }; |
| 124 | 121 | ||
| 125 | -// ====== 分类与计划 ====== | ||
| 126 | const categoryList = ref([]); | 122 | const categoryList = ref([]); |
| 127 | const activeCategory = ref(0); | 123 | const activeCategory = ref(0); |
| 128 | -const planData = ref([]); // 存储所有已加载的计划(带 _categoryId) | ||
| 129 | -const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' }; | ||
| 130 | -// 完全匹配你后台的数字,复制这一段! | ||
| 131 | -const filterMap = { | ||
| 132 | - 频率: { | ||
| 133 | - 不限: 0, | ||
| 134 | - '1练/周': 1, | ||
| 135 | - '2练/周': 2, | ||
| 136 | - '3练/周': 3, | ||
| 137 | - '4练/周': 4, | ||
| 138 | - '5练/周': 5, | ||
| 139 | - '6练/周': 6, | ||
| 140 | - '7练/周': 7, | ||
| 141 | - }, | ||
| 142 | - 难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 }, | ||
| 143 | - 场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 }, | ||
| 144 | - 人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 }, | ||
| 145 | -}; | 124 | +const planData = ref([]); |
| 146 | const currentPlanList = computed(() => { | 125 | const currentPlanList = computed(() => { |
| 147 | const categoryId = categoryList.value[activeCategory.value]?.id; | 126 | const categoryId = categoryList.value[activeCategory.value]?.id; |
| 148 | if (categoryId == null) return []; | 127 | if (categoryId == null) return []; |
| 149 | 128 | ||
| 150 | let list = planData.value.filter((p) => p._categoryId === categoryId); | 129 | let list = planData.value.filter((p) => p._categoryId === categoryId); |
| 151 | 130 | ||
| 152 | - // 频率 | ||
| 153 | - if (selectedFilters.value.频率 !== '不限') { | ||
| 154 | - const val = filterMap.频率[selectedFilters.value.频率]; | ||
| 155 | - list = list.filter((item) => item.frequency === val); | ||
| 156 | - } | ||
| 157 | - | ||
| 158 | - // 难度 | ||
| 159 | - if (selectedFilters.value.难度 !== '不限') { | ||
| 160 | - const val = filterMap.难度[selectedFilters.value.难度]; | ||
| 161 | - list = list.filter((item) => item.difficultyLevel === val); | ||
| 162 | - } | ||
| 163 | - | ||
| 164 | - // 场景 | ||
| 165 | - if (selectedFilters.value.场景 !== '不限') { | ||
| 166 | - const val = filterMap.场景[selectedFilters.value.场景]; | ||
| 167 | - list = list.filter((item) => item.scene === val); | ||
| 168 | - } | ||
| 169 | - | ||
| 170 | - // 人群 | ||
| 171 | - if (selectedFilters.value.人群 !== '不限') { | ||
| 172 | - const val = filterMap.人群[selectedFilters.value.人群]; | ||
| 173 | - list = list.filter((item) => item.population === val); | 131 | + Object.keys(filterMap).forEach((key) => { |
| 132 | + if (selectedFilters.value[key] !== '不限') { | ||
| 133 | + const val = filterMap[key][selectedFilters.value[key]]; | ||
| 134 | + list = list.filter((item) => item[filterFieldMap[key]] === val); | ||
| 174 | } | 135 | } |
| 136 | + }); | ||
| 175 | 137 | ||
| 176 | return list; | 138 | return list; |
| 177 | }); | 139 | }); |
| 178 | 140 | ||
| 179 | -// ✅ 切换分类时加载数据 | ||
| 180 | const switchCategory = async (index) => { | 141 | const switchCategory = async (index) => { |
| 181 | activeCategory.value = index; | 142 | activeCategory.value = index; |
| 182 | const categoryId = categoryList.value[index]?.id; | 143 | const categoryId = categoryList.value[index]?.id; |
| 183 | if (categoryId != null) { | 144 | if (categoryId != null) { |
| 184 | - // 检查是否已加载过该分类 | ||
| 185 | const isLoaded = planData.value.some((p) => p._categoryId === categoryId); | 145 | const isLoaded = planData.value.some((p) => p._categoryId === categoryId); |
| 186 | if (!isLoaded) { | 146 | if (!isLoaded) { |
| 187 | await loadPlansByCategory(categoryId); | 147 | await loadPlansByCategory(categoryId); |
| @@ -189,51 +149,43 @@ const switchCategory = async (index) => { | @@ -189,51 +149,43 @@ const switchCategory = async (index) => { | ||
| 189 | } | 149 | } |
| 190 | }; | 150 | }; |
| 191 | 151 | ||
| 192 | -// ====== 数据加载 ====== | ||
| 193 | const loadCategories = async () => { | 152 | const loadCategories = async () => { |
| 194 | try { | 153 | try { |
| 195 | - // 获得计划分类,左侧导航栏显示用 | ||
| 196 | const res = await QueryPlanApi.getCategories(); | 154 | const res = await QueryPlanApi.getCategories(); |
| 197 | categoryList.value = res.data || []; | 155 | categoryList.value = res.data || []; |
| 198 | - console.log('加载计划列表categoryList.value:', categoryList.value); | 156 | + |
| 199 | if (categoryList.value.length > 0) { | 157 | if (categoryList.value.length > 0) { |
| 200 | - await switchCategory(0); // 加载第一个分类 | 158 | + await switchCategory(0); |
| 201 | } | 159 | } |
| 202 | } catch (err) { | 160 | } catch (err) { |
| 203 | console.error('加载分类失败:', err); | 161 | console.error('加载分类失败:', err); |
| 204 | - uni.showToast({ title: '加载分类失败', icon: 'none' }); | ||
| 205 | } | 162 | } |
| 206 | }; | 163 | }; |
| 207 | 164 | ||
| 208 | -// ✅ 核心:加载计划,并手动附加 _categoryId | ||
| 209 | const loadPlansByCategory = async (categoryId) => { | 165 | const loadPlansByCategory = async (categoryId) => { |
| 210 | try { | 166 | try { |
| 211 | - // 根据计划分类ID获得计划列表,右侧计划列表显示用 | ||
| 212 | const res = await QueryPlanApi.getPlanList(categoryId); | 167 | const res = await QueryPlanApi.getPlanList(categoryId); |
| 213 | - console.log('加载计划列表res.data:', res.data); | 168 | + |
| 214 | const plans = (res.data || []).map((item) => ({ | 169 | const plans = (res.data || []).map((item) => ({ |
| 215 | id: item.id, | 170 | id: item.id, |
| 216 | title: item.name, | 171 | title: item.name, |
| 217 | meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`, | 172 | meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`, |
| 218 | tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '', | 173 | tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '', |
| 219 | - cover: item.urlCover || '默认图', | 174 | + cover: item.urlCover, |
| 220 | _categoryId: categoryId, | 175 | _categoryId: categoryId, |
| 221 | - frequency: item.frequencyPerWeek, // 新增 | ||
| 222 | - difficultyLevel: item.difficultyLevel, // 新增 | ||
| 223 | - scene: item.trainingScene, // 新增 | ||
| 224 | - population: item.targetPeople, // 新增 | 176 | + frequency: item.frequencyPerWeek, |
| 177 | + difficultyLevel: item.difficultyLevel, | ||
| 178 | + scene: item.trainingScene, | ||
| 179 | + population: item.targetPeople, | ||
| 225 | })); | 180 | })); |
| 226 | - // 合并数据(避免重复) | ||
| 227 | planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans]; | 181 | planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans]; |
| 228 | - // 打印planData | ||
| 229 | - console.log('加载计划列表planData.value:', planData.value); | 182 | + |
| 230 | } catch (err) { | 183 | } catch (err) { |
| 231 | console.error('加载计划失败:', err); | 184 | console.error('加载计划失败:', err); |
| 232 | - uni.showToast({ title: '加载计划失败', icon: 'none' }); | 185 | + |
| 233 | } | 186 | } |
| 234 | }; | 187 | }; |
| 235 | 188 | ||
| 236 | -// 跳转到搜索页面 | ||
| 237 | const navigateToSearch = () => { | 189 | const navigateToSearch = () => { |
| 238 | const app = getApp(); | 190 | const app = getApp(); |
| 239 | app.globalData.allPlansForSearch = planData.value; | 191 | app.globalData.allPlansForSearch = planData.value; |
| @@ -241,88 +193,35 @@ const navigateToSearch = () => { | @@ -241,88 +193,35 @@ const navigateToSearch = () => { | ||
| 241 | url: '/pages4/pages/xunji/jihua-search', | 193 | url: '/pages4/pages/xunji/jihua-search', |
| 242 | }); | 194 | }); |
| 243 | }; | 195 | }; |
| 244 | -// 跳转到计划详情页 | ||
| 245 | const navigateToDetail = (plan) => { | 196 | const navigateToDetail = (plan) => { |
| 246 | uni.navigateTo({ | 197 | uni.navigateTo({ |
| 247 | url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`, | 198 | url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`, |
| 248 | }); | 199 | }); |
| 249 | }; | 200 | }; |
| 250 | 201 | ||
| 251 | -// ====== 返回按钮 ====== | ||
| 252 | -const back = () => { | ||
| 253 | - const pages = getCurrentPages(); | ||
| 254 | - if (pages.length > 1) { | ||
| 255 | - uni.navigateBack(); | ||
| 256 | - } else { | ||
| 257 | - uni.switchTab({ url: '/pages/index/index' }); // 请替换为你的首页路径 | ||
| 258 | - } | ||
| 259 | -}; | ||
| 260 | - | ||
| 261 | onMounted(() => { | 202 | onMounted(() => { |
| 262 | loadCategories(); | 203 | loadCategories(); |
| 263 | }); | 204 | }); |
| 264 | </script> | 205 | </script> |
| 265 | 206 | ||
| 266 | <style lang="scss" scoped> | 207 | <style lang="scss" scoped> |
| 267 | -/* ========== 新增:页面头部样式 ========== */ | ||
| 268 | -// .page-header { | ||
| 269 | -// display: flex; | ||
| 270 | -// align-items: center; | ||
| 271 | -// padding: 30rpx; | ||
| 272 | -// background: #fff; | ||
| 273 | -// box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); | ||
| 274 | -// position: sticky; | ||
| 275 | -// top: 0; | ||
| 276 | -// z-index: 999; | ||
| 277 | -// } | ||
| 278 | - | ||
| 279 | -// .page-title { | ||
| 280 | -// font-size: 32rpx; | ||
| 281 | -// font-weight: bold; | ||
| 282 | -// color: #333; | ||
| 283 | -// flex: 1; | ||
| 284 | -// text-align: center; | ||
| 285 | -// } | ||
| 286 | - | ||
| 287 | -/* ========== 以下是你原有的全部样式(完全保留) ========== */ | ||
| 288 | .plan-page { | 208 | .plan-page { |
| 289 | width: 100%; | 209 | width: 100%; |
| 290 | - height: 100vh; | ||
| 291 | background-color: #f5f5f5; | 210 | background-color: #f5f5f5; |
| 292 | box-sizing: border-box; | 211 | box-sizing: border-box; |
| 293 | - overflow: hidden; | ||
| 294 | - display: flex; | ||
| 295 | - flex-direction: column; | ||
| 296 | -} | ||
| 297 | - | ||
| 298 | -/* 顶部横幅 */ | ||
| 299 | -.banner-wrapper { | ||
| 300 | - height: 250rpx; | ||
| 301 | - width: 100%; | ||
| 302 | - margin: 20rpx 0; | ||
| 303 | - | ||
| 304 | - .swiper { | ||
| 305 | - width: 100%; | ||
| 306 | - height: 100%; | ||
| 307 | - | ||
| 308 | - .swiper-item { | ||
| 309 | - width: 100%; | ||
| 310 | - height: 100%; | ||
| 311 | - padding: 20rpx; | ||
| 312 | - box-sizing: border-box; | ||
| 313 | - | ||
| 314 | - .img { | ||
| 315 | - width: 100%; | ||
| 316 | - height: 220rpx; | ||
| 317 | - border-radius: 20rpx; | ||
| 318 | - } | ||
| 319 | - } | ||
| 320 | - } | 212 | + height: calc(100vh - 50px); |
| 213 | + // #ifdef MP-WEIXIN | ||
| 214 | + height: calc(100vh - 138px); | ||
| 215 | + // #endif | ||
| 321 | } | 216 | } |
| 322 | 217 | ||
| 323 | /* 筛选条 */ | 218 | /* 筛选条 */ |
| 324 | .filter-bar { | 219 | .filter-bar { |
| 325 | - margin: 0 20rpx 20rpx; | 220 | + position: relative; |
| 221 | + margin: 20rpx 20rpx; | ||
| 222 | + // #ifdef MP-WEIXIN | ||
| 223 | + margin-top: 40rpx; | ||
| 224 | + // #endif | ||
| 326 | padding: 12rpx 18rpx; | 225 | padding: 12rpx 18rpx; |
| 327 | border-radius: 40rpx; | 226 | border-radius: 40rpx; |
| 328 | display: flex; | 227 | display: flex; |
| @@ -369,14 +268,9 @@ onMounted(() => { | @@ -369,14 +268,9 @@ onMounted(() => { | ||
| 369 | //下拉抽屉 | 268 | //下拉抽屉 |
| 370 | .filter-drawer { | 269 | .filter-drawer { |
| 371 | position: absolute; | 270 | position: absolute; |
| 372 | - top: 560rpx; | ||
| 373 | - | ||
| 374 | - /* #ifdef H5 */ | ||
| 375 | - top: 247px; | ||
| 376 | - /* #endif */ | ||
| 377 | - | ||
| 378 | - left: 20rpx; | ||
| 379 | - right: 20rpx; | 271 | + top: 100%; |
| 272 | + left: -18rpx; | ||
| 273 | + right: -18rpx; | ||
| 380 | z-index: 999; | 274 | z-index: 999; |
| 381 | border-radius: 0 0 40rpx 40rpx; | 275 | border-radius: 0 0 40rpx 40rpx; |
| 382 | background-color: #fff; | 276 | background-color: #fff; |
| @@ -406,11 +300,10 @@ onMounted(() => { | @@ -406,11 +300,10 @@ onMounted(() => { | ||
| 406 | 300 | ||
| 407 | /* 主体布局 */ | 301 | /* 主体布局 */ |
| 408 | .content-layout { | 302 | .content-layout { |
| 409 | - flex: 1; | 303 | + height: 100%; |
| 410 | display: flex; | 304 | display: flex; |
| 411 | box-sizing: border-box; | 305 | box-sizing: border-box; |
| 412 | - overflow: hidden; | ||
| 413 | - min-height: 0; | 306 | + |
| 414 | } | 307 | } |
| 415 | 308 | ||
| 416 | /* 左侧分类 */ | 309 | /* 左侧分类 */ |
| @@ -418,10 +311,10 @@ onMounted(() => { | @@ -418,10 +311,10 @@ onMounted(() => { | ||
| 418 | width: 180rpx; | 311 | width: 180rpx; |
| 419 | background-color: #ffffff; | 312 | background-color: #ffffff; |
| 420 | border-radius: 16rpx; | 313 | border-radius: 16rpx; |
| 421 | - height: 72%; | ||
| 422 | - /* #ifdef H5 */ | 314 | + height: 88%; |
| 315 | + // #ifdef MP-WEIXIN | ||
| 423 | height: 86%; | 316 | height: 86%; |
| 424 | - /* #endif */ | 317 | + // #endif |
| 425 | box-sizing: border-box; | 318 | box-sizing: border-box; |
| 426 | overflow: hidden; | 319 | overflow: hidden; |
| 427 | 320 | ||
| @@ -455,10 +348,10 @@ onMounted(() => { | @@ -455,10 +348,10 @@ onMounted(() => { | ||
| 455 | flex: 1; | 348 | flex: 1; |
| 456 | padding: 0 20rpx; | 349 | padding: 0 20rpx; |
| 457 | box-sizing: border-box; | 350 | box-sizing: border-box; |
| 458 | - height: 72%; | ||
| 459 | - /* #ifdef H5 */ | 351 | + height: 88%; |
| 352 | + // #ifdef MP-WEIXIN | ||
| 460 | height: 86%; | 353 | height: 86%; |
| 461 | - /* #endif */ | 354 | + // #endif |
| 462 | overflow: hidden; | 355 | overflow: hidden; |
| 463 | } | 356 | } |
| 464 | 357 |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="home-page" v-if="userStore.isLogin"> | ||
| 3 | - <view class="tab-bar" :style="{ paddingTop: menuButtonHeight + topSafeArea + 'px' }"> | ||
| 4 | - <!-- <view class="tab-item" :class="{ active: currentTab === 0 }" @click="handleTabClick(0)">训记</view> --> | ||
| 5 | - <view class="tab-item" :class="{ active: currentTab === 1 }" @click="handleTabClick(1)">计划</view> | ||
| 6 | - <view class="tab-item" :class="{ active: currentTab === 2 }" @click="handleTabClick(2)">日历</view> | ||
| 7 | - <view class="tab-item" :class="{ active: currentTab === 3 }" @click="handleTabClick(3)">动作</view> | ||
| 8 | - <view class="tab-item" :class="{ active: currentTab === 4 }" @click="handleTabClick(4)">模板</view> | ||
| 9 | - <view class="tab-item" :class="{ active: currentTab === 5 }" @click="handleTabClick(5)">数据</view> | ||
| 10 | - <view class="menu-btn" @click="openDrawer"> | ||
| 11 | - <uni-icons type="bars" size="20"></uni-icons> | 2 | + <view class="home-page"> |
| 3 | + <view class="tab-bar" :style="{ paddingTop: PageHeight + 'px' }"> | ||
| 4 | + <view class="tab-item" :class="{ active: currentTab == 1 }" @click="handleTabClick(1)">计划</view> | ||
| 5 | + <view class="tab-item" :class="{ active: currentTab == 2 }" @click="handleTabClick(2)">日历</view> | ||
| 6 | + <view class="tab-item" :class="{ active: currentTab == 3 }" @click="handleTabClick(3)">动作</view> | ||
| 7 | + <view class="tab-item" :class="{ active: currentTab == 4 }" @click="handleTabClick(4)">模板</view> | ||
| 8 | + <view class="tab-item" :class="{ active: currentTab == 5 }" @click="handleTabClick(5)">数据</view> | ||
| 9 | + <view class="menu-btn" @click="drawerShow = true"> | ||
| 10 | + <up-icon name="list" size="20" /> | ||
| 12 | </view> | 11 | </view> |
| 13 | </view> | 12 | </view> |
| 14 | 13 | ||
| 15 | <view class="content"> | 14 | <view class="content"> |
| 16 | - <!-- <xunjiXunji v-if="currentTab === 0" /> --> | ||
| 17 | <xunjiXunlianjihua v-if="currentTab === 1" /> | 15 | <xunjiXunlianjihua v-if="currentTab === 1" /> |
| 18 | <xunjiRili v-if="currentTab === 2" /> | 16 | <xunjiRili v-if="currentTab === 2" /> |
| 19 | <xunjiDongzuo v-if="currentTab === 3" /> | 17 | <xunjiDongzuo v-if="currentTab === 3" /> |
| @@ -21,116 +19,69 @@ | @@ -21,116 +19,69 @@ | ||
| 21 | <xunjiShuju v-if="currentTab === 5" /> | 19 | <xunjiShuju v-if="currentTab === 5" /> |
| 22 | </view> | 20 | </view> |
| 23 | 21 | ||
| 24 | - <Tabbar /> | ||
| 25 | 22 | ||
| 26 | - <uni-drawer ref="drawer" mode="right" :mask="true" :width="280"> | ||
| 27 | - <view class="drawer-content" :style="{ paddingTop: menuButtonHeight + topSafeArea + 'px' }"> | 23 | + |
| 24 | + <up-popup :show="drawerShow" mode="right" :closeable="true" @close="drawerShow = false"> | ||
| 25 | + <view class="drawer-content" :style="{ paddingTop: PageHeight + 'px' }"> | ||
| 28 | <view class="user-header"> | 26 | <view class="user-header"> |
| 29 | - <image class="avatar" :src="userInfo.avatar || | ||
| 30 | - 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png' | ||
| 31 | - " mode="aspectFill"></image> | 27 | + <image |
| 28 | + class="avatar" | ||
| 29 | + :src="userInfo.avatar || defaultAvatar " | ||
| 30 | + mode="aspectFill" | ||
| 31 | + /> | ||
| 32 | <view class="user-info"> | 32 | <view class="user-info"> |
| 33 | - <text class="nickname">{{ userInfo.nickname || '未登录' }}</text> | ||
| 34 | - </view> | ||
| 35 | - </view> | ||
| 36 | - | ||
| 37 | - <image class="ad-banner" | ||
| 38 | - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png" | ||
| 39 | - mode="aspectFill"></image> | ||
| 40 | - | ||
| 41 | - <view class="menu-list"> | ||
| 42 | - <view class="menu-item" @click="navigateTo('/pages4/pages/xunji/xunji-wode-zhuye')"> | ||
| 43 | - <uni-icons type="person" size="18"></uni-icons> | ||
| 44 | - <text class="menu-text">我的主页</text> | ||
| 45 | - <uni-icons type="right" size="14"></uni-icons> | ||
| 46 | - </view> | ||
| 47 | - <view class="menu-item" @click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')"> | ||
| 48 | - <uni-icons type="wallet" size="18"></uni-icons> | ||
| 49 | - <text class="menu-text">我的模板</text> | ||
| 50 | - <uni-icons type="right" size="14"></uni-icons> | ||
| 51 | - </view> | ||
| 52 | - <view class="menu-item" @click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')"> | ||
| 53 | - <uni-icons type="list" size="18"></uni-icons> | ||
| 54 | - <text class="menu-text">我的计划</text> | ||
| 55 | - <uni-icons type="right" size="14"></uni-icons> | ||
| 56 | - </view> | ||
| 57 | - <view class="menu-item" @click="goWidgetLib"> | ||
| 58 | - <uni-icons type="more" size="18"></uni-icons> | ||
| 59 | - <text class="menu-text">小组件库</text> | ||
| 60 | - <uni-icons type="right" size="14"></uni-icons> | ||
| 61 | - </view> | ||
| 62 | - <view class="menu-item" @click="goFeedback"> | ||
| 63 | - <uni-icons type="chat" size="18"></uni-icons> | ||
| 64 | - <text class="menu-text">训记反馈建议</text> | ||
| 65 | - <uni-icons type="right" size="14"></uni-icons> | 33 | + <text class="nickname">{{ userInfo.nickname || '-' }}</text> |
| 66 | </view> | 34 | </view> |
| 67 | - <view class="menu-item" @click="goUserGroup"> | ||
| 68 | - <uni-icons type="chatboxes" size="18"></uni-icons> | ||
| 69 | - <text class="menu-text">训记内测用户群</text> | ||
| 70 | - <uni-icons type="right" size="14"></uni-icons> | ||
| 71 | </view> | 35 | </view> |
| 72 | - <view class="menu-item" @click="goTutorial"> | ||
| 73 | - <uni-icons type="help" size="18"></uni-icons> | ||
| 74 | - <text class="menu-text">训记使用教程</text> | ||
| 75 | - <uni-icons type="right" size="14"></uni-icons> | 36 | + <up-cell-group :border="false"> |
| 37 | + <up-cell title="我的主页" icon="account" :isLink="true" | ||
| 38 | + @click="navigateTo('/pages4/pages/xunji/xunji-wode-zhuye')" /> | ||
| 39 | + <up-cell title="我的模板" icon="file-text" :isLink="true" | ||
| 40 | + @click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')" /> | ||
| 41 | + <up-cell title="我的计划" icon="order" :isLink="true" | ||
| 42 | + @click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')" /> | ||
| 43 | + <up-cell title="训记使用教程" icon="question-circle" :isLink="true" @click="goTutorial" /> | ||
| 44 | + </up-cell-group> | ||
| 76 | </view> | 45 | </view> |
| 77 | - </view> | ||
| 78 | - </view> | ||
| 79 | - </uni-drawer> | 46 | + </up-popup> |
| 80 | 47 | ||
| 81 | <TrainingFloating /> | 48 | <TrainingFloating /> |
| 49 | + <Tabbar /> | ||
| 82 | </view> | 50 | </view> |
| 83 | - | ||
| 84 | </template> | 51 | </template> |
| 85 | 52 | ||
| 86 | <script setup> | 53 | <script setup> |
| 87 | -import { ref, shallowRef, computed, onMounted } from 'vue'; | ||
| 88 | -import { onLoad, onHide, onShow } from '@dcloudio/uni-app'; | 54 | +import { ref, computed } from 'vue'; |
| 55 | +import { onLoad, onShow } from '@dcloudio/uni-app'; | ||
| 89 | import useUserStore from '@/sheep/store/user'; | 56 | import useUserStore from '@/sheep/store/user'; |
| 90 | import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea'; | 57 | import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea'; |
| 91 | - | ||
| 92 | -// 组件导入 | 58 | +import { defaultAvatar } from '@/sheep/config/index'; |
| 93 | import xunjiRili from '@/pages/xunji/components/xunji-rili.vue'; | 59 | import xunjiRili from '@/pages/xunji/components/xunji-rili.vue'; |
| 94 | import xunjiDongzuo from '@/pages/xunji/components/xunji-dongzuo.vue'; | 60 | import xunjiDongzuo from '@/pages/xunji/components/xunji-dongzuo.vue'; |
| 95 | import xunjiMoban from '@/pages/xunji/components/xunji-moban.vue'; | 61 | import xunjiMoban from '@/pages/xunji/components/xunji-moban.vue'; |
| 96 | import xunjiShuju from '@/pages/xunji/components/xunji-shuju.vue'; | 62 | import xunjiShuju from '@/pages/xunji/components/xunji-shuju.vue'; |
| 97 | -import xunjiXunji from '@/pages/xunji/components/xunji-xunji.vue'; | ||
| 98 | import xunjiXunlianjihua from '@/pages/xunji/components/xunji-xunlianjihua.vue'; | 63 | import xunjiXunlianjihua from '@/pages/xunji/components/xunji-xunlianjihua.vue'; |
| 64 | +import TrainingFloating from '@/pages/TrainingFloating.vue'; | ||
| 99 | 65 | ||
| 100 | -import TrainingFloating from '@/pages/TrainingFloating.vue' | ||
| 101 | -import { useTrainingStore } from '@/sheep/store/trainingStore'; | ||
| 102 | -const trainingStore = useTrainingStore(); | ||
| 103 | - | ||
| 104 | -// 状态管理 | ||
| 105 | const userStore = useUserStore(); | 66 | const userStore = useUserStore(); |
| 67 | +const drawerShow = ref(false); | ||
| 68 | +const currentTab = ref(3); | ||
| 106 | 69 | ||
| 107 | -// --- 补全缺失的响应式变量定义 --- | ||
| 108 | -const currentTab = ref(1); | ||
| 109 | -const currentComponent = shallowRef(xunjiXunji); // 用于 H5 端动态组件切换(如保留原功能逻辑) | ||
| 110 | -const drawer = ref(null); // uni-drawer 的组件实例引用 | ||
| 111 | - | ||
| 112 | -// 安全区域相关变量(从工具函数异步或同步获取) | ||
| 113 | -const menuButtonHeight = ref(getMenuButtonHeight ? getMenuButtonHeight() : 0); | ||
| 114 | -const topSafeArea = ref(getTopSafeArea ? getTopSafeArea() : 0); | 70 | +const menuButtonHeight = ref(getMenuButtonHeight()); |
| 71 | +const topSafeArea = ref(getTopSafeArea()); | ||
| 72 | +const PageHeight = ref(topSafeArea.value); | ||
| 73 | +// #ifdef MP-WEIXIN | ||
| 74 | +PageHeight.value = menuButtonHeight.value + topSafeArea.value; | ||
| 75 | +// #endif | ||
| 115 | 76 | ||
| 116 | -// 用户信息计算属性(防止 store 异步更新时报错) | ||
| 117 | const userInfo = computed(() => userStore.userInfo || {}); | 77 | const userInfo = computed(() => userStore.userInfo || {}); |
| 118 | 78 | ||
| 119 | -// --- 原有页面生命周期维护 --- | ||
| 120 | -onHide(() => { | ||
| 121 | - if (drawer.value) { | ||
| 122 | - drawer.value.close(); | ||
| 123 | - } | ||
| 124 | -}); | ||
| 125 | - | ||
| 126 | onLoad((options) => { | 79 | onLoad((options) => { |
| 127 | if (!userStore.isLogin) { | 80 | if (!userStore.isLogin) { |
| 128 | - uni.redirectTo({ | ||
| 129 | - url: '/pages7/pages/index/login', | ||
| 130 | - }); | 81 | + uni.redirectTo({ url: '/pages7/pages/index/login' }); |
| 131 | return; | 82 | return; |
| 132 | } | 83 | } |
| 133 | - // 确保解析路由参数的正确性 | 84 | + userStore.getInfo(); |
| 134 | if (options && options.currentTab) { | 85 | if (options && options.currentTab) { |
| 135 | currentTab.value = Number(options.currentTab); | 86 | currentTab.value = Number(options.currentTab); |
| 136 | } | 87 | } |
| @@ -142,85 +93,40 @@ onShow(() => { | @@ -142,85 +93,40 @@ onShow(() => { | ||
| 142 | const tabNum = Number(targetTab); | 93 | const tabNum = Number(targetTab); |
| 143 | currentTab.value = tabNum; | 94 | currentTab.value = tabNum; |
| 144 | handleTabClick(tabNum); | 95 | handleTabClick(tabNum); |
| 145 | - // 使用后清空缓存,避免下次进来自动跳转 | ||
| 146 | uni.removeStorageSync('xunjiTargetTab'); | 96 | uni.removeStorageSync('xunjiTargetTab'); |
| 147 | } | 97 | } |
| 98 | +}); | ||
| 148 | 99 | ||
| 149 | -}) | ||
| 150 | - | ||
| 151 | -// --- 原有业务方法补充与修复 --- | ||
| 152 | - | ||
| 153 | -// 打开抽屉 | ||
| 154 | -const openDrawer = () => { | ||
| 155 | - if (drawer.value) { | ||
| 156 | - drawer.value.open(); | ||
| 157 | - } | ||
| 158 | -}; | ||
| 159 | - | ||
| 160 | -// 修复模板直接调用 uni.navigateTo 的多端兼容问题 | ||
| 161 | const navigateTo = (url) => { | 100 | const navigateTo = (url) => { |
| 162 | uni.navigateTo({ url }); | 101 | uni.navigateTo({ url }); |
| 163 | }; | 102 | }; |
| 164 | 103 | ||
| 165 | -// 处理标签点击 | ||
| 166 | const handleTabClick = (index) => { | 104 | const handleTabClick = (index) => { |
| 167 | currentTab.value = index; | 105 | currentTab.value = index; |
| 168 | - // #ifdef H5 | ||
| 169 | - switch (index) { | ||
| 170 | - case 0: | ||
| 171 | - currentComponent.value = xunjiXunji; | ||
| 172 | - break; | ||
| 173 | - case 1: | ||
| 174 | - currentComponent.value = xunjiXunlianjihua; | ||
| 175 | - break; | ||
| 176 | - case 2: | ||
| 177 | - currentComponent.value = xunjiRili; | ||
| 178 | - break; | ||
| 179 | - case 3: | ||
| 180 | - currentComponent.value = xunjiDongzuo; | ||
| 181 | - break; | ||
| 182 | - case 4: | ||
| 183 | - currentComponent.value = xunjiMoban; | ||
| 184 | - break; | ||
| 185 | - case 5: | ||
| 186 | - currentComponent.value = xunjiShuju; | ||
| 187 | - break; | ||
| 188 | - default: | ||
| 189 | - currentComponent.value = xunjiXunlianjihua; | ||
| 190 | - } | ||
| 191 | - // #endif | ||
| 192 | }; | 106 | }; |
| 193 | 107 | ||
| 194 | -// 空实现占位(防止模板点击报错,保持你原有的未给出的方法定义) | ||
| 195 | -const goWidgetLib = () => { | ||
| 196 | - uni.navigateTo({ url: '/pages4/pages/xunji/widget' }); | ||
| 197 | -}; | ||
| 198 | -const goFeedback = () => { }; | ||
| 199 | -const goUserGroup = () => { }; | 108 | + |
| 109 | + | ||
| 200 | const goTutorial = () => { }; | 110 | const goTutorial = () => { }; |
| 201 | 111 | ||
| 202 | -defineExpose({ | ||
| 203 | - switchTabIndex: handleTabClick | ||
| 204 | -}) | 112 | +defineExpose({ switchTabIndex: handleTabClick }); |
| 205 | </script> | 113 | </script> |
| 206 | <style scoped lang="scss"> | 114 | <style scoped lang="scss"> |
| 207 | .home-page { | 115 | .home-page { |
| 208 | width: 100%; | 116 | width: 100%; |
| 209 | - height: 100vh; | ||
| 210 | // #ifdef H5 | 117 | // #ifdef H5 |
| 211 | - padding-bottom: 90rpx; | 118 | + height: calc(100vh - 50px); |
| 212 | // #endif | 119 | // #endif |
| 213 | - background-color: #f5f5f5; | ||
| 214 | - box-sizing: border-box; | ||
| 215 | - overflow: hidden; | ||
| 216 | - display: flex; | ||
| 217 | - flex-direction: column; | 120 | + // #ifdef MP-WEIXIN |
| 121 | + height: calc(100vh - 50px); | ||
| 122 | + // #endif | ||
| 123 | + background-color: #fff; | ||
| 218 | 124 | ||
| 219 | .content { | 125 | .content { |
| 220 | - flex: 1; | 126 | + |
| 221 | box-sizing: border-box; | 127 | box-sizing: border-box; |
| 222 | overflow: hidden; | 128 | overflow: hidden; |
| 223 | - padding-top: 70rpx; | 129 | + padding-top: 65rpx; |
| 224 | // #ifdef MP-WEIXIN | 130 | // #ifdef MP-WEIXIN |
| 225 | padding-top: 200rpx; | 131 | padding-top: 200rpx; |
| 226 | // #endif | 132 | // #endif |
| @@ -231,37 +137,32 @@ defineExpose({ | @@ -231,37 +137,32 @@ defineExpose({ | ||
| 231 | display: flex; | 137 | display: flex; |
| 232 | align-items: flex-end; | 138 | align-items: flex-end; |
| 233 | justify-content: space-between; | 139 | justify-content: space-between; |
| 234 | - padding: 0px 20px; | ||
| 235 | - padding-top: 20rpx; | 140 | + padding: 0 20px; |
| 236 | background-color: #fff; | 141 | background-color: #fff; |
| 237 | - | ||
| 238 | position: fixed; | 142 | position: fixed; |
| 239 | top: 0; | 143 | top: 0; |
| 240 | left: 0; | 144 | left: 0; |
| 241 | right: 0; | 145 | right: 0; |
| 242 | z-index: 999; | 146 | z-index: 999; |
| 243 | box-sizing: border-box; | 147 | box-sizing: border-box; |
| 244 | -} | ||
| 245 | 148 | ||
| 246 | -/* 标签项:默认样式 */ | ||
| 247 | -.tab-item { | 149 | + .tab-item { |
| 248 | font-size: 16px; | 150 | font-size: 16px; |
| 249 | color: #666; | 151 | color: #666; |
| 250 | padding: 5px 0; | 152 | padding: 5px 0; |
| 251 | padding-top: 30rpx; | 153 | padding-top: 30rpx; |
| 252 | -} | ||
| 253 | 154 | ||
| 254 | -/* 激活态标签:下划线+深色文字 */ | ||
| 255 | -.tab-item.active { | 155 | + &.active { |
| 256 | color: #333; | 156 | color: #333; |
| 257 | border-bottom: 2px solid #000; | 157 | border-bottom: 2px solid #000; |
| 258 | font-weight: 500; | 158 | font-weight: 500; |
| 259 | -} | 159 | + } |
| 160 | + } | ||
| 260 | 161 | ||
| 261 | -/* 菜单按钮:样式 */ | ||
| 262 | -.menu-btn { | 162 | + .menu-btn { |
| 263 | font-size: 20px; | 163 | font-size: 20px; |
| 264 | color: #666; | 164 | color: #666; |
| 165 | + } | ||
| 265 | } | 166 | } |
| 266 | 167 | ||
| 267 | .card-subtitle { | 168 | .card-subtitle { |
| @@ -294,82 +195,37 @@ defineExpose({ | @@ -294,82 +195,37 @@ defineExpose({ | ||
| 294 | color: #666; | 195 | color: #666; |
| 295 | } | 196 | } |
| 296 | 197 | ||
| 297 | -/* 抽屉样式 */ | ||
| 298 | .drawer-content { | 198 | .drawer-content { |
| 299 | - background-color: #ffffff; | 199 | + width: 500rpx; |
| 200 | + background-color: #f7f7f8; | ||
| 300 | height: 100%; | 201 | height: 100%; |
| 301 | - padding: 20rpx; | 202 | + padding: 0; |
| 302 | box-sizing: border-box; | 203 | box-sizing: border-box; |
| 303 | -} | ||
| 304 | 204 | ||
| 305 | -/* 用户头像和信息区域 */ | ||
| 306 | -.user-header { | 205 | + .user-header { |
| 307 | display: flex; | 206 | display: flex; |
| 308 | align-items: center; | 207 | align-items: center; |
| 309 | - padding: 20rpx 0; | ||
| 310 | - border-bottom: 1px solid #f0f0f0; | ||
| 311 | -} | 208 | + padding: 40rpx 30rpx 30rpx; |
| 209 | + background: #fff; | ||
| 210 | + margin-bottom: 20rpx; | ||
| 312 | 211 | ||
| 313 | -.user-header .avatar { | ||
| 314 | - width: 80rpx; | ||
| 315 | - height: 80rpx; | 212 | + .avatar { |
| 213 | + width: 90rpx; | ||
| 214 | + height: 90rpx; | ||
| 316 | border-radius: 50%; | 215 | border-radius: 50%; |
| 317 | - margin-right: 20rpx; | ||
| 318 | - margin-top: 0; | ||
| 319 | - justify-content: flex-start; | ||
| 320 | -} | 216 | + background: #f0f0f0; |
| 217 | + flex-shrink: 0; | ||
| 218 | + } | ||
| 321 | 219 | ||
| 322 | -.user-info { | ||
| 323 | - display: flex; | ||
| 324 | - flex-direction: column; | ||
| 325 | -} | 220 | + .user-info { |
| 221 | + margin-left: 20rpx; | ||
| 326 | 222 | ||
| 327 | -.nickname { | 223 | + .nickname { |
| 328 | font-size: 28rpx; | 224 | font-size: 28rpx; |
| 329 | font-weight: 500; | 225 | font-weight: 500; |
| 330 | - color: #333333; | ||
| 331 | - margin-bottom: 8rpx; | ||
| 332 | -} | ||
| 333 | - | ||
| 334 | -.user-id { | ||
| 335 | - font-size: 24rpx; | ||
| 336 | - color: #999999; | ||
| 337 | -} | ||
| 338 | - | ||
| 339 | -/* 广告横幅 */ | ||
| 340 | -.ad-banner { | ||
| 341 | - width: 100%; | ||
| 342 | - height: 200rpx; | ||
| 343 | - margin: 20rpx 0; | ||
| 344 | - border-radius: 12rpx; | ||
| 345 | -} | ||
| 346 | - | ||
| 347 | -/* 菜单列表 */ | ||
| 348 | -.menu-list { | ||
| 349 | - margin-top: 20rpx; | ||
| 350 | -} | ||
| 351 | - | ||
| 352 | -.menu-item { | ||
| 353 | - display: flex; | ||
| 354 | - align-items: center; | ||
| 355 | - justify-content: space-between; | ||
| 356 | - padding: 30rpx 0; | ||
| 357 | - border-bottom: 1px solid #f0f0f0; | ||
| 358 | -} | ||
| 359 | - | ||
| 360 | -.menu-item:last-child { | ||
| 361 | - border-bottom: none; | ||
| 362 | -} | ||
| 363 | - | ||
| 364 | -.menu-text { | ||
| 365 | - font-size: 28rpx; | ||
| 366 | - color: #333333; | ||
| 367 | - margin-left: 20rpx; | ||
| 368 | - flex: 1; | ||
| 369 | -} | ||
| 370 | - | ||
| 371 | -/* 覆盖uni-icons默认颜色 */ | ||
| 372 | -:deep(.uni-icons) { | ||
| 373 | - color: #666666; | 226 | + color: #333; |
| 227 | + } | ||
| 228 | + } | ||
| 229 | + } | ||
| 374 | } | 230 | } |
| 375 | </style> | 231 | </style> |
| 1 | -<!-- | ||
| 2 | -基础版训练计划搜索页(仅关键词搜索) | ||
| 3 | -页面位于:训记横向导航栏的‘计划’模块,右侧搜索图标点击后进入的页面就是,核心功能是提供一个简单的搜索界面,允许用户输入关键词搜索训练计划,还未完成 | ||
| 4 | ---> | ||
| 5 | <template> | 1 | <template> |
| 6 | <view class="search-page"> | 2 | <view class="search-page"> |
| 7 | - <!-- 顶部搜索栏 --> | ||
| 8 | <view class="search-header"> | 3 | <view class="search-header"> |
| 9 | - <view class="search-input-wrapper"> | ||
| 10 | - <uni-icons type="search" size="18" color="#999" class="search-icon"></uni-icons> | ||
| 11 | - <input ref="searchInput" v-model="searchKeyword" class="search-input" type="text" placeholder="搜索训练计划" | ||
| 12 | - placeholder-style="color:#999" confirm-type="search" @confirm="handleSearch" focus /> | ||
| 13 | - </view> | ||
| 14 | - <view class="cancel-btn" @click="handleCancel"> | ||
| 15 | - <text class="cancel-text">取消</text> | ||
| 16 | - </view> | ||
| 17 | - </view> | ||
| 18 | - | ||
| 19 | - <!-- 搜索结果区域 --> | ||
| 20 | - <view class="search-results"> | ||
| 21 | - <!-- 未搜索时提示 --> | ||
| 22 | - <view v-if="!hasSearched" class="empty-hint"> | ||
| 23 | - <text>请输入关键词搜索训练计划</text> | 4 | + <up-search v-model="searchKeyword" placeholder="搜索训练计划" placeholderColor="#999" shape="round" |
| 5 | + :showAction="true" actionText="取消" :clearabled="true" @search="handleSearch" @custom="handleSearch" | ||
| 6 | + @cancel="handleCancel" /> | ||
| 24 | </view> | 7 | </view> |
| 25 | 8 | ||
| 26 | - <!-- 已搜索但无结果 --> | ||
| 27 | - <view v-else-if="searchResults.length === 0" class="empty-hint"> | ||
| 28 | - <text>未找到相关训练计划</text> | ||
| 29 | - </view> | ||
| 30 | - | ||
| 31 | - <!-- 搜索结果列表 --> | 9 | + <scroll-view class="search-results" scroll-y enable-flex> |
| 10 | + <up-empty v-if="!hasSearched" text="请输入关键词搜索训练计划" mode="search" /> | ||
| 11 | + <up-empty v-else-if="searchResults.length === 0" text="未找到相关训练计划" mode="data" /> | ||
| 32 | <view v-else class="results-list"> | 12 | <view v-else class="results-list"> |
| 33 | <view v-for="plan in searchResults" :key="plan.id" class="result-item" @click="goToDetail(plan)"> | 13 | <view v-for="plan in searchResults" :key="plan.id" class="result-item" @click="goToDetail(plan)"> |
| 34 | <image v-if="plan.cover" :src="plan.cover" class="result-cover" mode="aspectFill" /> | 14 | <image v-if="plan.cover" :src="plan.cover" class="result-cover" mode="aspectFill" /> |
| @@ -38,77 +18,68 @@ | @@ -38,77 +18,68 @@ | ||
| 38 | </view> | 18 | </view> |
| 39 | </view> | 19 | </view> |
| 40 | </view> | 20 | </view> |
| 41 | - </view> | 21 | + </scroll-view> |
| 42 | </view> | 22 | </view> |
| 43 | </template> | 23 | </template> |
| 44 | 24 | ||
| 45 | <script setup> | 25 | <script setup> |
| 46 | -import { ref, onMounted } from 'vue'; | ||
| 47 | -import QueryPlanApi from '@/sheep/api/plan/queryplan' | 26 | +import { ref, computed, onMounted } from 'vue'; |
| 27 | +import QueryPlanApi from '@/sheep/api/plan/queryplan'; | ||
| 28 | +import { getNavbarHeight } from '@/utils/safeArea'; | ||
| 48 | 29 | ||
| 49 | -// 响应式数据 | ||
| 50 | const searchKeyword = ref(''); | 30 | const searchKeyword = ref(''); |
| 51 | const hasSearched = ref(false); | 31 | const hasSearched = ref(false); |
| 52 | const searchResults = ref([]); | 32 | const searchResults = ref([]); |
| 53 | -const allPlans = ref([]); | ||
| 54 | -const searchInput = ref(null); | ||
| 55 | -// 难度/频率/场景/人群 数字转文字映射(和列表页保持一致) | 33 | +const navBarHeight = ref(0); |
| 34 | + | ||
| 56 | const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' }; | 35 | const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' }; |
| 57 | const frequencyText = { 0: '不限', 1: '1练/周', 2: '2练/周', 3: '3练/周', 4: '4练/周', 5: '5练/周', 6: '6练/周', 7: '7练/周' }; | 36 | const frequencyText = { 0: '不限', 1: '1练/周', 2: '2练/周', 3: '3练/周', 4: '4练/周', 5: '5练/周', 6: '6练/周', 7: '7练/周' }; |
| 58 | -// 页面加载时获取全局数据 | 37 | + |
| 38 | +let isMpWeixin = false; | ||
| 39 | +// #ifdef MP-WEIXIN | ||
| 40 | +isMpWeixin = true; | ||
| 41 | +// #endif | ||
| 42 | + | ||
| 43 | + | ||
| 44 | + | ||
| 45 | + | ||
| 59 | onMounted(() => { | 46 | onMounted(() => { |
| 60 | - // 自动聚焦输入框(uni-app 需要延迟) | ||
| 61 | - setTimeout(() => { | ||
| 62 | - if (searchInput.value && typeof searchInput.value.focus === 'function') { | ||
| 63 | - searchInput.value.focus(); | 47 | + if (isMpWeixin) { |
| 48 | + navBarHeight.value = getNavbarHeight(); | ||
| 64 | } | 49 | } |
| 65 | - }, 300); | ||
| 66 | }); | 50 | }); |
| 67 | -// 返回 | ||
| 68 | -const goBack = () => { | ||
| 69 | - uni.navigateBack(); | ||
| 70 | -}; | ||
| 71 | -// 执行接口搜索 | 51 | + |
| 72 | const handleSearch = async () => { | 52 | const handleSearch = async () => { |
| 73 | const keyword = searchKeyword.value.trim(); | 53 | const keyword = searchKeyword.value.trim(); |
| 74 | hasSearched.value = true; | 54 | hasSearched.value = true; |
| 75 | - | ||
| 76 | if (!keyword) { | 55 | if (!keyword) { |
| 77 | searchResults.value = []; | 56 | searchResults.value = []; |
| 78 | return; | 57 | return; |
| 79 | } | 58 | } |
| 80 | - | ||
| 81 | - // 调用接口:根据名称模糊查询计划 | ||
| 82 | try { | 59 | try { |
| 83 | const res = await QueryPlanApi.searchPlansByName(keyword); | 60 | const res = await QueryPlanApi.searchPlansByName(keyword); |
| 84 | - // 接口返回成功,处理数据 | ||
| 85 | if (res.code === 0) { | 61 | if (res.code === 0) { |
| 86 | - // 映射接口字段到页面字段 | ||
| 87 | searchResults.value = res.data.map(item => ({ | 62 | searchResults.value = res.data.map(item => ({ |
| 88 | id: item.id, | 63 | id: item.id, |
| 89 | - title: item.name, // 接口字段name → 页面字段title | ||
| 90 | - cover: item.urlCover, // 接口字段urlCover → 页面字段cover | 64 | + title: item.name, |
| 65 | + cover: item.urlCover, | ||
| 91 | meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${frequencyText[item.frequencyPerWeek] || '1练/周'}` | 66 | meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${frequencyText[item.frequencyPerWeek] || '1练/周'}` |
| 92 | })); | 67 | })); |
| 93 | } else { | 68 | } else { |
| 94 | - // 接口返回错误,提示用户 | ||
| 95 | uni.showToast({ title: res.msg || '搜索失败', icon: 'none' }); | 69 | uni.showToast({ title: res.msg || '搜索失败', icon: 'none' }); |
| 96 | searchResults.value = []; | 70 | searchResults.value = []; |
| 97 | } | 71 | } |
| 98 | } catch (err) { | 72 | } catch (err) { |
| 99 | - // 网络/接口异常处理 | ||
| 100 | uni.showToast({ title: '网络异常,请重试', icon: 'none' }); | 73 | uni.showToast({ title: '网络异常,请重试', icon: 'none' }); |
| 101 | searchResults.value = []; | 74 | searchResults.value = []; |
| 102 | console.error('搜索接口调用失败:', err); | 75 | console.error('搜索接口调用失败:', err); |
| 103 | } | 76 | } |
| 104 | }; | 77 | }; |
| 105 | 78 | ||
| 106 | -// 取消并返回 | ||
| 107 | const handleCancel = () => { | 79 | const handleCancel = () => { |
| 108 | uni.navigateBack(); | 80 | uni.navigateBack(); |
| 109 | }; | 81 | }; |
| 110 | 82 | ||
| 111 | -// 点击跳转详情(根据你项目结构调整) | ||
| 112 | const goToDetail = (plan) => { | 83 | const goToDetail = (plan) => { |
| 113 | if (!plan?.id) { | 84 | if (!plan?.id) { |
| 114 | uni.showToast({ title: '计划信息异常', icon: 'none' }); | 85 | uni.showToast({ title: '计划信息异常', icon: 'none' }); |
| @@ -122,88 +93,33 @@ const goToDetail = (plan) => { | @@ -122,88 +93,33 @@ const goToDetail = (plan) => { | ||
| 122 | 93 | ||
| 123 | <style lang="scss" scoped> | 94 | <style lang="scss" scoped> |
| 124 | .search-page { | 95 | .search-page { |
| 125 | - // 整个页面往下偏移,避免被顶部导航栏遮挡 | ||
| 126 | - padding-top: 160rpx; | ||
| 127 | height: 100vh; | 96 | height: 100vh; |
| 128 | background-color: #f5f5f5; | 97 | background-color: #f5f5f5; |
| 129 | display: flex; | 98 | display: flex; |
| 130 | flex-direction: column; | 99 | flex-direction: column; |
| 131 | -} | ||
| 132 | 100 | ||
| 133 | -/* 顶部搜索栏 */ | ||
| 134 | -.search-header { | ||
| 135 | - display: flex; | ||
| 136 | - align-items: center; | ||
| 137 | - padding: 20rpx 20rpx 20rpx 30rpx; | 101 | + .search-header { |
| 102 | + padding: 14rpx 20rpx; | ||
| 138 | background-color: #ffffff; | 103 | background-color: #ffffff; |
| 139 | - // box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); | 104 | + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); |
| 140 | z-index: 999; | 105 | z-index: 999; |
| 141 | - // 固定顶部搜索栏 | ||
| 142 | - position: fixed; | ||
| 143 | - top: 160rpx; | ||
| 144 | - left: 0; | ||
| 145 | - right: 0; | ||
| 146 | -} | ||
| 147 | 106 | ||
| 148 | -.search-input-wrapper { | ||
| 149 | - flex: 1; | ||
| 150 | - background-color: #f5f5f5; | ||
| 151 | - border-radius: 12rpx; | ||
| 152 | - padding: 0 20rpx; | ||
| 153 | - height: 64rpx; | ||
| 154 | - display: flex; | ||
| 155 | - align-items: center; | ||
| 156 | - gap: 10rpx; | ||
| 157 | -} | 107 | + position: sticky; |
| 108 | + top: 0; | ||
| 158 | 109 | ||
| 159 | -.search-icon { | ||
| 160 | - flex-shrink: 0; | ||
| 161 | -} | ||
| 162 | 110 | ||
| 163 | -.search-input { | ||
| 164 | - flex: 1; | ||
| 165 | - height: 100%; | ||
| 166 | - font-size: 28rpx; | ||
| 167 | - color: #333; | ||
| 168 | - background: transparent; | ||
| 169 | - border: none; | ||
| 170 | -} | ||
| 171 | - | ||
| 172 | -.cancel-btn { | ||
| 173 | - margin-left: 24rpx; | ||
| 174 | - padding: 10rpx 0; | ||
| 175 | - height: 64rpx; | ||
| 176 | - display: flex; | ||
| 177 | - align-items: center; | ||
| 178 | -} | ||
| 179 | - | ||
| 180 | -.cancel-text { | ||
| 181 | - font-size: 28rpx; | ||
| 182 | - color: #333; | ||
| 183 | - /* 主题绿色,与你的 UI 一致 */ | ||
| 184 | -} | 111 | + } |
| 185 | 112 | ||
| 186 | -/* 搜索结果区域 */ | ||
| 187 | -.search-results { | ||
| 188 | - margin-top: 100rpx; // 改成 100rpx,避免被搜索栏遮挡 | 113 | + .search-results { |
| 189 | flex: 1; | 114 | flex: 1; |
| 190 | padding: 30rpx 20rpx; | 115 | padding: 30rpx 20rpx; |
| 191 | - overflow-y: auto; | ||
| 192 | -} | ||
| 193 | - | ||
| 194 | -.empty-hint { | ||
| 195 | - text-align: center; | ||
| 196 | - color: #999; | ||
| 197 | - font-size: 28rpx; | ||
| 198 | - margin-top: 200rpx; | ||
| 199 | -} | 116 | + box-sizing: border-box; |
| 200 | 117 | ||
| 201 | -.results-list { | 118 | + .results-list { |
| 202 | display: flex; | 119 | display: flex; |
| 203 | flex-direction: column; | 120 | flex-direction: column; |
| 204 | -} | ||
| 205 | 121 | ||
| 206 | -.result-item { | 122 | + .result-item { |
| 207 | display: flex; | 123 | display: flex; |
| 208 | background: #ffffff; | 124 | background: #ffffff; |
| 209 | border-radius: 16rpx; | 125 | border-radius: 16rpx; |
| @@ -211,24 +127,22 @@ const goToDetail = (plan) => { | @@ -211,24 +127,22 @@ const goToDetail = (plan) => { | ||
| 211 | overflow: hidden; | 127 | overflow: hidden; |
| 212 | padding: 20rpx; | 128 | padding: 20rpx; |
| 213 | box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05); | 129 | box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05); |
| 214 | -} | ||
| 215 | 130 | ||
| 216 | -.result-cover { | 131 | + .result-cover { |
| 217 | width: 160rpx; | 132 | width: 160rpx; |
| 218 | height: 160rpx; | 133 | height: 160rpx; |
| 219 | border-radius: 12rpx; | 134 | border-radius: 12rpx; |
| 220 | margin-right: 20rpx; | 135 | margin-right: 20rpx; |
| 221 | flex-shrink: 0; | 136 | flex-shrink: 0; |
| 222 | -} | 137 | + } |
| 223 | 138 | ||
| 224 | -.result-info { | 139 | + .result-info { |
| 225 | flex: 1; | 140 | flex: 1; |
| 226 | display: flex; | 141 | display: flex; |
| 227 | flex-direction: column; | 142 | flex-direction: column; |
| 228 | justify-content: center; | 143 | justify-content: center; |
| 229 | -} | ||
| 230 | 144 | ||
| 231 | -.result-title { | 145 | + .result-title { |
| 232 | font-size: 28rpx; | 146 | font-size: 28rpx; |
| 233 | color: #333; | 147 | color: #333; |
| 234 | font-weight: bold; | 148 | font-weight: bold; |
| @@ -237,11 +151,16 @@ const goToDetail = (plan) => { | @@ -237,11 +151,16 @@ const goToDetail = (plan) => { | ||
| 237 | -webkit-line-clamp: 2; | 151 | -webkit-line-clamp: 2; |
| 238 | -webkit-box-orient: vertical; | 152 | -webkit-box-orient: vertical; |
| 239 | overflow: hidden; | 153 | overflow: hidden; |
| 240 | -} | 154 | + } |
| 241 | 155 | ||
| 242 | -.result-meta { | 156 | + .result-meta { |
| 243 | font-size: 24rpx; | 157 | font-size: 24rpx; |
| 244 | color: #999; | 158 | color: #999; |
| 245 | margin-top: 10rpx; | 159 | margin-top: 10rpx; |
| 160 | + } | ||
| 161 | + } | ||
| 162 | + } | ||
| 163 | + } | ||
| 164 | + } | ||
| 246 | } | 165 | } |
| 247 | </style> | 166 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <view class="profile-page"> | 2 | <view class="profile-page"> |
| 3 | <view class="avatar-section"> | 3 | <view class="avatar-section"> |
| 4 | + <!-- #ifdef H5 --> | ||
| 5 | + <button class="avatar-edit-btn" @click="handleH5ChooseAvatar"> | ||
| 6 | + <image :src="formData.avatar || | ||
| 7 | + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png' | ||
| 8 | + " mode="aspectFill" class="avatar-image"></image> | ||
| 9 | + </button> | ||
| 10 | + <!-- #endif --> | ||
| 11 | + <!-- #ifdef MP-WEIXIN --> | ||
| 4 | <button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar"> | 12 | <button class="avatar-edit-btn" open-type="chooseAvatar" @chooseavatar="handleChooseAvatar"> |
| 5 | <image :src="formData.avatar || | 13 | <image :src="formData.avatar || |
| 6 | 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png' | 14 | 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png' |
| 7 | " mode="aspectFill" class="avatar-image"></image> | 15 | " mode="aspectFill" class="avatar-image"></image> |
| 8 | </button> | 16 | </button> |
| 17 | + <!-- #endif --> | ||
| 9 | </view> | 18 | </view> |
| 10 | 19 | ||
| 11 | <view class="info-list"> | 20 | <view class="info-list"> |
| 12 | <view class="list-item" @click="gotoEditNickname"> | 21 | <view class="list-item" @click="gotoEditNickname"> |
| 13 | <text class="item-title">昵称</text> | 22 | <text class="item-title">昵称</text> |
| 14 | - <view class="item-value">{{ formData.nickname || '未设置' }}</view> | 23 | + <view class="item-value">{{ formData.nickname || '-' }}</view> |
| 15 | <uni-icons type="right" color="#999" size="20" class="arrow-icon"></uni-icons> | 24 | <uni-icons type="right" color="#999" size="20" class="arrow-icon"></uni-icons> |
| 16 | </view> | 25 | </view> |
| 17 | 26 | ||
| @@ -204,9 +213,6 @@ const handleChooseAvatar = async (e) => { | @@ -204,9 +213,6 @@ const handleChooseAvatar = async (e) => { | ||
| 204 | try { | 213 | try { |
| 205 | const res = await FileApi.uploadFile(avatarUrl); | 214 | const res = await FileApi.uploadFile(avatarUrl); |
| 206 | 215 | ||
| 207 | - console.log('res----------', res); | ||
| 208 | - | ||
| 209 | - | ||
| 210 | formData.value.avatar = res.data; | 216 | formData.value.avatar = res.data; |
| 211 | 217 | ||
| 212 | } catch (err) { | 218 | } catch (err) { |
| @@ -215,6 +221,43 @@ const handleChooseAvatar = async (e) => { | @@ -215,6 +221,43 @@ const handleChooseAvatar = async (e) => { | ||
| 215 | } | 221 | } |
| 216 | }; | 222 | }; |
| 217 | 223 | ||
| 224 | +// #ifdef H5 | ||
| 225 | + | ||
| 226 | +/** H5 端选择头像上传 */ | ||
| 227 | + | ||
| 228 | +const handleH5ChooseAvatar = () => { | ||
| 229 | + uni.chooseImage({ | ||
| 230 | + count: 1, | ||
| 231 | + sizeType: ['compressed'], | ||
| 232 | + sourceType: ['album', 'camera'], | ||
| 233 | + success: async (chooseRes) => { | ||
| 234 | + const tempFilePath = chooseRes.tempFilePaths[0]; | ||
| 235 | + if (!tempFilePath) return; | ||
| 236 | + | ||
| 237 | + // 文件大小校验 | ||
| 238 | + uni.getFileInfo({ | ||
| 239 | + filePath: tempFilePath, | ||
| 240 | + success: async (fileInfo) => { | ||
| 241 | + if (fileInfo.size / 1024 / 1024 > 5) { | ||
| 242 | + uni.showToast({ title: '图片大小不能超过5MB', icon: 'none' }); | ||
| 243 | + return; | ||
| 244 | + } | ||
| 245 | + try { | ||
| 246 | + const res = await FileApi.uploadFile(tempFilePath); | ||
| 247 | + formData.value.avatar = res.data; | ||
| 248 | + uni.showToast({ title: '上传成功', icon: 'success' }); | ||
| 249 | + } catch (err) { | ||
| 250 | + console.error('[H5] 头像上传失败:', err); | ||
| 251 | + uni.showToast({ title: '上传失败', icon: 'none' }); | ||
| 252 | + } | ||
| 253 | + }, | ||
| 254 | + }); | ||
| 255 | + }, | ||
| 256 | + }); | ||
| 257 | +}; | ||
| 258 | + | ||
| 259 | +// #endif | ||
| 260 | + | ||
| 218 | /** | 261 | /** |
| 219 | * 唤起并初始化昵称弹窗缓存 | 262 | * 唤起并初始化昵称弹窗缓存 |
| 220 | */ | 263 | */ |
| @@ -434,15 +477,17 @@ const saveProfile = async () => { | @@ -434,15 +477,17 @@ const saveProfile = async () => { | ||
| 434 | 477 | ||
| 435 | await UserApi.updateUser(payload); | 478 | await UserApi.updateUser(payload); |
| 436 | 479 | ||
| 437 | - // 异步刷新全局 Pinia 的 userInfo 用户属性,使全局头像和昵称视图无痛同步 | ||
| 438 | if (userStore.getUserInfo) { | 480 | if (userStore.getUserInfo) { |
| 439 | await userStore.getUserInfo(); | 481 | await userStore.getUserInfo(); |
| 440 | } | 482 | } |
| 441 | 483 | ||
| 484 | + // 同步表单数据,避免 navigateBack 时 onBackPress 误判为"未保存" | ||
| 485 | + formData.value = JSON.parse(JSON.stringify(userStore.userInfo)); | ||
| 486 | + | ||
| 442 | uni.showToast({ title: '保存成功', icon: 'success' }); | 487 | uni.showToast({ title: '保存成功', icon: 'success' }); |
| 443 | 488 | ||
| 444 | setTimeout(() => { | 489 | setTimeout(() => { |
| 445 | - uni.navigateBack({ delta: 1 }); | 490 | + uni.navigateBack(); |
| 446 | }, 1200); | 491 | }, 1200); |
| 447 | } catch (err) { | 492 | } catch (err) { |
| 448 | console.error('[API Error] 保存个人信息更新请求失败:', err); | 493 | console.error('[API Error] 保存个人信息更新请求失败:', err); |
| @@ -4,11 +4,7 @@ const { version } = packageInfo; | @@ -4,11 +4,7 @@ const { version } = packageInfo; | ||
| 4 | 4 | ||
| 5 | // 开发环境配置 | 5 | // 开发环境配置 |
| 6 | export let baseUrl; | 6 | export let baseUrl; |
| 7 | -if (process.env.NODE_ENV === 'development') { | ||
| 8 | - baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL; | ||
| 9 | -} else { | ||
| 10 | - baseUrl = import.meta.env.SHOPRO_BASE_URL; | ||
| 11 | -} | 7 | +baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL; |
| 12 | if (typeof baseUrl === 'undefined') { | 8 | if (typeof baseUrl === 'undefined') { |
| 13 | console.error('请检查.env配置文件是否存在'); | 9 | console.error('请检查.env配置文件是否存在'); |
| 14 | } else { | 10 | } else { |
| @@ -20,7 +16,7 @@ export const staticUrl = import.meta.env.SHOPRO_STATIC_URL; | @@ -20,7 +16,7 @@ export const staticUrl = import.meta.env.SHOPRO_STATIC_URL; | ||
| 20 | export const tenantId = import.meta.env.SHOPRO_TENANT_ID; | 16 | export const tenantId = import.meta.env.SHOPRO_TENANT_ID; |
| 21 | export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH; | 17 | export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH; |
| 22 | export const h5Url = import.meta.env.SHOPRO_H5_URL; | 18 | export const h5Url = import.meta.env.SHOPRO_H5_URL; |
| 23 | - | 19 | +export const defaultAvatar = import.meta.env.DEFAULT_AVATAR; |
| 24 | export default { | 20 | export default { |
| 25 | baseUrl, | 21 | baseUrl, |
| 26 | apiPath, | 22 | apiPath, |
| @@ -28,4 +24,5 @@ export default { | @@ -28,4 +24,5 @@ export default { | ||
| 28 | tenantId, | 24 | tenantId, |
| 29 | websocketPath, | 25 | websocketPath, |
| 30 | h5Url, | 26 | h5Url, |
| 27 | + defaultAvatar, | ||
| 31 | }; | 28 | }; |
-
Please register or login to post a comment