Authored by Bad

feat: 优化日历训练计划及自由训练流程

- 新增用户协议、隐私协议及使用教程页面
- 调整每日模板与历史记录的数据展示逻辑
- 支持未来日期保存为训练模板并锁定当日开练功能
@@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1 @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development) 8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081 9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080 10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
11 - #SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com 11 +SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/ 12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080 13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc 14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
@@ -43,3 +43,5 @@ SHOPRO_TENANT_ID=1 @@ -43,3 +43,5 @@ SHOPRO_TENANT_ID=1
43 43
44 # 默认头像 44 # 默认头像
45 DEFAULT_AVATAR=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png 45 DEFAULT_AVATAR=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png
  46 +# appLogo
  47 +APP_LOGO=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260728/自己练logo_1785218679080.png
@@ -116,7 +116,7 @@ @@ -116,7 +116,7 @@
116 { 116 {
117 "path": "pages/xunji/wode-jihua-paike", 117 "path": "pages/xunji/wode-jihua-paike",
118 "style": { 118 "style": {
119 - "navigationBarTitleText": "我的计划详情的排课设置" 119 + "navigationBarTitleText": "计划排课"
120 } 120 }
121 }, 121 },
122 { 122 {
@@ -144,6 +144,13 @@ @@ -144,6 +144,13 @@
144 "style": { 144 "style": {
145 "navigationBarTitleText": "" 145 "navigationBarTitleText": ""
146 } 146 }
  147 + },
  148 + {
  149 + "path": "pages/xunji/shiyon-jiaochen",
  150 + "style": {
  151 + "navigationBarTitleText": "使用教程"
  152 +
  153 + }
147 } 154 }
148 ] 155 ]
149 }, 156 },
@@ -209,6 +216,18 @@ @@ -209,6 +216,18 @@
209 "navigationBarTitleText": "设置", 216 "navigationBarTitleText": "设置",
210 "navigationStyle": "default" 217 "navigationStyle": "default"
211 } 218 }
  219 + },
  220 + {
  221 + "path": "pages/user/yonhu-xieyi",
  222 + "style": {
  223 + "navigationBarTitleText": "用户协议"
  224 + }
  225 + },
  226 + {
  227 + "path": "pages/user/yinsi-xieyi",
  228 + "style": {
  229 + "navigationBarTitleText": ""
  230 + }
212 } 231 }
213 ] 232 ]
214 } 233 }
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 <!-- 未登录引导区 --> 17 <!-- 未登录引导区 -->
18 <view v-else class="section-card login-guide-box"> 18 <view v-else class="section-card login-guide-box">
19 <view class="guide-txt"> 19 <view class="guide-txt">
20 - <text class="title">欢迎加入鸿星运动</text> 20 + <text class="title">欢迎加入自己练运动</text>
21 <text class="desc">登录后即可享受课程预约及资产管理</text> 21 <text class="desc">登录后即可享受课程预约及资产管理</text>
22 </view> 22 </view>
23 <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button> 23 <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button>
@@ -52,8 +52,9 @@ @@ -52,8 +52,9 @@
52 <uni-icons type="right" size="14" color="#E0E0E0" /> 52 <uni-icons type="right" size="14" color="#E0E0E0" />
53 </view> 53 </view>
54 54
55 - <view class="logout-wrap">  
56 - <button class="logout-btn" @click="handleLogout">退出登录</button> 55 + <view class="settings-item logout-item" @click="handleLogout">
  56 + <text class="logout-label">退出登录</text>
  57 + <uni-icons type="right" size="14" color="#E0E0E0" />
57 </view> 58 </view>
58 </view> 59 </view>
59 60
@@ -457,30 +458,15 @@ $transition-tap: background-color 0.2s ease, opacity 0.2s ease; @@ -457,30 +458,15 @@ $transition-tap: background-color 0.2s ease, opacity 0.2s ease;
457 font-size: $font-body; 458 font-size: $font-body;
458 color: $color-text-primary; 459 color: $color-text-primary;
459 } 460 }
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;  
478 - }  
479 461
480 - &:active {  
481 - background-color: $color-danger-active; 462 + .logout-label {
  463 + font-size: $font-body;
  464 + color: $color-text-tertiary;
482 } 465 }
483 } 466 }
  467 +
  468 + .logout-item {
  469 + border-bottom: none;
484 } 470 }
485 471
486 // 触摸反馈 472 // 触摸反馈
@@ -262,6 +262,13 @@ const exerciseNamesWithLabel = computed(() => { @@ -262,6 +262,13 @@ const exerciseNamesWithLabel = computed(() => {
262 }) 262 })
263 263
264 const exposeRecordList = computed(() => { 264 const exposeRecordList = computed(() => {
  265 + // 每日模板编辑场景需要全量数据,不受 isActive 过滤影响
  266 + if (props.isDailyTemplates) {
  267 + return recordList.value.map(item => ({
  268 + ...item,
  269 + weight: getRealWeight(item)
  270 + }))
  271 + }
265 return recordList.value.filter(item => item.isActive).map(item => ({ 272 return recordList.value.filter(item => item.isActive).map(item => ({
266 ...item, 273 ...item,
267 weight: getRealWeight(item) 274 weight: getRealWeight(item)
@@ -272,9 +279,11 @@ const exposeSuperRecordMap = computed(() => { @@ -272,9 +279,11 @@ const exposeSuperRecordMap = computed(() => {
272 const map = {} 279 const map = {}
273 for (const key in superRecordMap.value) { 280 for (const key in superRecordMap.value) {
274 const subExercise = props.actionDetail?.exercises?.find(s => s.id == key) 281 const subExercise = props.actionDetail?.exercises?.find(s => s.id == key)
275 - map[key] = superRecordMap.value[key]  
276 - .filter(item => item.isActive)  
277 - .map(item => ({ ...item, weight: getRealWeight(item, subExercise?.exerciseType) })) 282 + // 每日模板编辑场景需要全量数据,不受 isActive 过滤影响
  283 + const list = props.isDailyTemplates
  284 + ? superRecordMap.value[key]
  285 + : superRecordMap.value[key]?.filter(item => item.isActive)
  286 + map[key] = (list || []).map(item => ({ ...item, weight: getRealWeight(item, subExercise?.exerciseType) }))
278 } 287 }
279 return map 288 return map
280 }) 289 })
@@ -578,10 +587,13 @@ function onQuickConfirm(e) { @@ -578,10 +587,13 @@ function onQuickConfirm(e) {
578 587
579 // ===================== 增删行/组 ===================== 588 // ===================== 增删行/组 =====================
580 function addRow() { 589 function addRow() {
581 - recordList.value.push({ 590 + const last = recordList.value[recordList.value.length - 1]
  591 + const newItem = last ? JSON.parse(JSON.stringify(last)) : {
582 h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60), 592 h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
583 distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false 593 distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
584 - }) 594 + }
  595 + newItem.isActive = false
  596 + recordList.value.push(newItem)
585 } 597 }
586 598
587 function deleteRow(index) { 599 function deleteRow(index) {
@@ -595,10 +607,14 @@ function deleteRow(index) { @@ -595,10 +607,14 @@ function deleteRow(index) {
595 function addSuperSet() { 607 function addSuperSet() {
596 (props.actionDetail?.exercises || []).forEach(sub => { 608 (props.actionDetail?.exercises || []).forEach(sub => {
597 if (superRecordMap.value[sub.id]) { 609 if (superRecordMap.value[sub.id]) {
598 - superRecordMap.value[sub.id].push({ 610 + const list = superRecordMap.value[sub.id]
  611 + const last = list[list.length - 1]
  612 + const newItem = last ? JSON.parse(JSON.stringify(last)) : {
599 h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60), 613 h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
600 distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false 614 distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
601 - }) 615 + }
  616 + newItem.isActive = false
  617 + list.push(newItem)
602 } 618 }
603 }) 619 })
604 } 620 }
@@ -155,7 +155,7 @@ @@ -155,7 +155,7 @@
155 <view class="section-title">训练部位</view> 155 <view class="section-title">训练部位</view>
156 <view class="muscle-card"> 156 <view class="muscle-card">
157 <image 157 <image
158 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg" 158 + :src="actionDetail.urlImage || 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg'"
159 mode="widthFix" class="muscle-map" /> 159 mode="widthFix" class="muscle-map" />
160 <!-- <view class="legend"> 160 <!-- <view class="legend">
161 <view class="legend-item"> 161 <view class="legend-item">
@@ -172,13 +172,16 @@ @@ -172,13 +172,16 @@
172 <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length"> 172 <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length">
173 <view class="group-label primary">主要训练部位</view> 173 <view class="group-label primary">主要训练部位</view>
174 <view class="pills"> 174 <view class="pills">
175 - <text class="pill primary-pill" v-for="(value, index) in actionDetail.primaryMuscleNames" :key="index">{{ value }}</text> 175 + <text class="pill primary-pill" v-for="(value, index) in actionDetail.primaryMuscleNames"
  176 + :key="index">{{
  177 + value }}</text>
176 </view> 178 </view>
177 </view> 179 </view>
178 <view class="muscle-group" v-if="actionDetail.secondaryMuscleNames?.length"> 180 <view class="muscle-group" v-if="actionDetail.secondaryMuscleNames?.length">
179 <view class="group-label secondary">次要训练部位</view> 181 <view class="group-label secondary">次要训练部位</view>
180 <view class="pills"> 182 <view class="pills">
181 - <text class="pill secondary-pill" v-for="(value, index) in actionDetail.secondaryMuscleNames" :key="index">{{ value }}</text> 183 + <text class="pill secondary-pill" v-for="(value, index) in actionDetail.secondaryMuscleNames"
  184 + :key="index">{{ value }}</text>
182 </view> 185 </view>
183 </view> 186 </view>
184 </view> 187 </view>
@@ -187,7 +190,7 @@ @@ -187,7 +190,7 @@
187 <view v-if="contentTab === 1" class="tab-pane slide-up"> 190 <view v-if="contentTab === 1" class="tab-pane slide-up">
188 <view class="history-list"> 191 <view class="history-list">
189 <template v-if="historyList && historyList.length > 0"> 192 <template v-if="historyList && historyList.length > 0">
190 - <view class="history-item" v-for="item in historyList" :key="item.id"> 193 + <view class="history-item" v-for="(item, index) in historyList" :key="index">
191 <view class="item-header"> 194 <view class="item-header">
192 <text class="date">{{ formatDate(item.date) }}</text> 195 <text class="date">{{ formatDate(item.date) }}</text>
193 <text class="tag">{{ item.name }}</text> 196 <text class="tag">{{ item.name }}</text>
@@ -198,14 +201,14 @@ @@ -198,14 +201,14 @@
198 <view class="dot">· </view> 201 <view class="dot">· </view>
199 <!-- <view class="difficulty">困难</view> --> 202 <!-- <view class="difficulty">困难</view> -->
200 <view class="difficulty">{{ 203 <view class="difficulty">{{
201 - item.weight ? item.weight + 'kg' : '无负重' 204 + item.weight != null ? item.weight + 'kg' : '无负重'
202 }}</view> 205 }}</view>
203 </view> 206 </view>
204 <view class="group-chips"> 207 <view class="group-chips">
205 <view class="chip" v-for="(set, index) in item.setConfigList" :key="index"> 208 <view class="chip" v-for="(set, index) in item.setConfigList" :key="index">
206 <view class="idx">{{ index + 1 }}</view> 209 <view class="idx">{{ index + 1 }}</view>
207 <view class="group-data"> 210 <view class="group-data">
208 - {{ formatSetData(set) }} 211 + {{ formatSetData(set, item.displayFields) }}
209 </view> 212 </view>
210 </view> 213 </view>
211 </view> 214 </view>
@@ -490,17 +493,32 @@ const loadTrainHistoryDetail = async (id, type) => { @@ -490,17 +493,32 @@ const loadTrainHistoryDetail = async (id, type) => {
490 } 493 }
491 }; 494 };
492 // 格式化时间 495 // 格式化时间
493 -const formatDate = (dateArr) => {  
494 - if (!Array.isArray(dateArr) || dateArr.length < 3) return '';  
495 - return dayjs(new Date(dateArr[0], dateArr[1] - 1, dateArr[2])).format('YYYY/MM/DD dd'); 496 +const formatDate = (date) => {
  497 + if (!date) return '';
  498 + // 支持数组格式 [2026, 7, 28] 和字符串格式
  499 + if (Array.isArray(date) && date.length >= 3) {
  500 + return dayjs(new Date(date[0], date[1] - 1, date[2])).format('YYYY/MM/DD');
  501 + }
  502 + return dayjs(date).format('YYYY/MM/DD');
496 }; 503 };
497 504
498 -const formatSetData = (set) => { 505 +const formatSetData = (set, displayFields) => {
  506 + if (!displayFields || displayFields.length === 0) return '无数据';
499 const parts = []; 507 const parts = [];
500 - if (set.weight != null && set.weight !== '') parts.push(`${set.weight}kg`);  
501 - if (set.reps != null && set.reps !== '') parts.push(`${set.reps}次`);  
502 - if (set.duration != null && set.duration !== '') parts.push(dayjs.duration(set.duration, 'seconds').format('HH:mm:ss'));  
503 - if (set.distance != null && set.distance !== '') parts.push(`${set.distance}m`); 508 + const fieldMap = {
  509 + weight: () => set.weight != null ? `${set.weight}kg` : null,
  510 + reps: () => set.reps != null ? `${set.reps}次` : null,
  511 + distance: () => set.distance != null ? `${set.distance}km` : null,
  512 + duration: () => set.duration != null ? dayjs.duration(set.duration, 'seconds').format('HH:mm:ss') : null,
  513 + restTime: () => set.restTime != null ? `休息${set.restTime}s` : null,
  514 + };
  515 + displayFields.forEach(field => {
  516 + const fn = fieldMap[field];
  517 + if (fn) {
  518 + const result = fn();
  519 + if (result) parts.push(result);
  520 + }
  521 + });
504 return parts.length > 0 ? parts.join(' × ') : '无数据'; 522 return parts.length > 0 ? parts.join(' × ') : '无数据';
505 }; 523 };
506 524
@@ -64,14 +64,17 @@ @@ -64,14 +64,17 @@
64 <WodeJihuaTianjiaTancuang 64 <WodeJihuaTianjiaTancuang
65 v-model:visible="showPlanPopup" 65 v-model:visible="showPlanPopup"
66 @get-plan-list-length="getPlanListLength" 66 @get-plan-list-length="getPlanListLength"
  67 + @success="handlePlanAddSuccess"
67 :is-add="true" 68 :is-add="true"
68 :plan-id="lastPlanId" 69 :plan-id="lastPlanId"
69 :is-my-plan="true" 70 :is-my-plan="true"
  71 + :selected-date="props.date"
70 /> 72 />
71 </template> 73 </template>
72 74
73 <script setup> 75 <script setup>
74 import { ref, onMounted, computed } from 'vue' 76 import { ref, onMounted, computed } from 'vue'
  77 +import dayjs from 'dayjs'
75 import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue' 78 import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue'
76 import QueryPlanApi from '@/sheep/api/plan/queryplan' 79 import QueryPlanApi from '@/sheep/api/plan/queryplan'
77 80
@@ -81,9 +84,13 @@ const props = defineProps({ @@ -81,9 +84,13 @@ const props = defineProps({
81 type: Boolean, 84 type: Boolean,
82 default: false, 85 default: false,
83 }, 86 },
  87 + date: {
  88 + type: String,
  89 + default: '',
  90 + },
84 }) 91 })
85 92
86 -const emit = defineEmits(['update:visible']) 93 +const emit = defineEmits(['update:visible', 'successAddTrain'])
87 94
88 // ==================== 响应式状态 ==================== 95 // ==================== 响应式状态 ====================
89 const showPlanPopup = ref(false) 96 const showPlanPopup = ref(false)
@@ -113,15 +120,29 @@ const handleAddFromPlan = () => { @@ -113,15 +120,29 @@ const handleAddFromPlan = () => {
113 showPlanPopup.value = true 120 showPlanPopup.value = true
114 } 121 }
115 122
  123 +/** 从训练计划添加成功后,通知父组件刷新 */
  124 +const handlePlanAddSuccess = () => {
  125 + emit('successAddTrain')
  126 +}
  127 +
116 /** 使用训练模板:跳转模板选择页 */ 128 /** 使用训练模板:跳转模板选择页 */
117 const handleAddFromTemplate = () => { 129 const handleAddFromTemplate = () => {
118 - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' }) 130 + uni.navigateTo({ url: `/pages4/pages/xunji/xunji-rili-tianjia-moban?date=${props.date}` })
119 close() 131 close()
120 } 132 }
121 133
122 -/** 自由训练:跳转动作练习页 */ 134 +/** 自由训练:非未来日期直接训练,未来日期保存为训练模板 */
123 const handleFreeTraining = () => { 135 const handleFreeTraining = () => {
124 - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' }) 136 + const today = dayjs().format('YYYY-MM-DD')
  137 + if (props.date && props.date > today) {
  138 + // 未来日期:跳转训练页构建动作,完成后保存为当日模板
  139 + uni.navigateTo({
  140 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?isFutureTemplate=true&targetDate=${props.date}`,
  141 + })
  142 + } else {
  143 + // 非未来日期:直接开始训练,携带目标日期确保训练记录日期正确
  144 + uni.navigateTo({ url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true&targetDate=${props.date}` })
  145 + }
125 close() 146 close()
126 } 147 }
127 148
@@ -27,21 +27,17 @@ @@ -27,21 +27,17 @@
27 </view> 27 </view>
28 28
29 <template v-if="resdailyData.length > 0"> 29 <template v-if="resdailyData.length > 0">
30 - <!-- Tab 按钮:如���有多个训记,用来源名作为标签 --> 30 + <!-- Tab 按钮:如有多个训记,用来源名作为标签 -->
31 <scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1"> 31 <scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1">
32 <button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData" 32 <button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData"
33 :key="index" @click.stop="switchPlan(index)"> 33 :key="index" @click.stop="switchPlan(index)">
34 - {{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' + (index + 1)) }} 34 + {{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' +
  35 + (index + 1)) }}
35 </button> 36 </button>
36 </scroll-view> 37 </scroll-view>
37 <!-- 训练内容区域 --> 38 <!-- 训练内容区域 -->
38 <view v-if="resdailyData.length > 0 && currentPlan" class="training-container"> 39 <view v-if="resdailyData.length > 0 && currentPlan" class="training-container">
39 - <!-- 训练来源标签 -->  
40 - <view class="source-tag-row" :class="getSourceClass(currentPlan.sourceType || 4)">  
41 - <text class="source-text">  
42 - {{ currentPlan.sourceType === 1 || currentPlan.sourceType === 2 ? (currentPlan.sourceName || currentPlan.name) : getSourceLabel(currentPlan.sourceType || 4) }}  
43 - </text>  
44 - </view> 40 +
45 <view class="unitCart"> 41 <view class="unitCart">
46 <!-- 训练计划头部卡片 --> 42 <!-- 训练计划头部卡片 -->
47 <view class="plan-header-card"> 43 <view class="plan-header-card">
@@ -60,8 +56,10 @@ @@ -60,8 +56,10 @@
60 <view class="plan-header-btns"> 56 <view class="plan-header-btns">
61 <button class="plan-btn more-btn" @click="handlePlanMore">更多</button> 57 <button class="plan-btn more-btn" @click="handlePlanMore">更多</button>
62 <button class="plan-btn copy-btn" @click="openCopyCalendarPopup(currentPlan?.templateId)">复制到</button> 58 <button class="plan-btn copy-btn" @click="openCopyCalendarPopup(currentPlan?.templateId)">复制到</button>
63 - <button class="plan-btn go-train-btn-small" @click="handleTrainAgain">  
64 - <text class="go-train-text-sm">今日再练</text> 59 + <button class="plan-btn go-train-btn-small" :class="{ locked: trainAgainDisabled }"
  60 + @click="handleTrainAgain">
  61 + <text class="go-train-text-sm">{{ trainAgainText }}</text>
  62 + <up-icon v-if="trainAgainText == '当日开练'" name="lock" size="15"></up-icon>
65 </button> 63 </button>
66 </view> 64 </view>
67 </view> 65 </view>
@@ -88,7 +86,7 @@ @@ -88,7 +86,7 @@
88 }}kg</text> 86 }}kg</text>
89 </view> 87 </view>
90 88
91 - <!-- 下半部分:组次行 → 向左对齐图片 ✅核心--> 89 + <!-- 下半部分:组次行 → 向左对齐图片 ✅核心 -->
92 <view class="action-bottom"> 90 <view class="action-bottom">
93 <view class="action-sets"> 91 <view class="action-sets">
94 <view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx"> 92 <view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx">
@@ -175,7 +173,7 @@ @@ -175,7 +173,7 @@
175 <!-- 空状态区域 --> 173 <!-- 空状态区域 -->
176 <view v-else class="empty-section"> 174 <view v-else class="empty-section">
177 <image class="empty-img" 175 <image class="empty-img"
178 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/空状态自助_1784875704380.png" 176 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260727/空状态自助_1785117390333.png"
179 mode="aspectFit"> 177 mode="aspectFit">
180 </image> 178 </image>
181 <text class="empty-tip">今天没有安排</text> 179 <text class="empty-tip">今天没有安排</text>
@@ -184,7 +182,7 @@ @@ -184,7 +182,7 @@
184 182
185 </view> 183 </view>
186 184
187 - <AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" /> 185 + <AddTrainPopup v-model:visible="showAddTrainPopup" :date="selectedDate" @successAddTrain="handleAddTrain" />
188 186
189 <!-- 日期备注弹窗 --> 187 <!-- 日期备注弹窗 -->
190 <RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId" 188 <RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId"
@@ -230,7 +228,7 @@ @@ -230,7 +228,7 @@
230 <!-- 复制到弹窗/移动到/添加到 --> 228 <!-- 复制到弹窗/移动到/添加到 -->
231 <AddToCalendarPopup ref="calendarPopupRef" :template-id="currentPlan?.templateId ?? 0" 229 <AddToCalendarPopup ref="calendarPopupRef" :template-id="currentPlan?.templateId ?? 0"
232 @success="handleCalendarSuccess" :mask-click="true" @click.stop 230 @success="handleCalendarSuccess" :mask-click="true" @click.stop
233 - :daily-template-id="currentPlan?.dailyTemplateId" :is-copy="isCopyMode" /> 231 + :daily-template-id="currentPlan?.id" :is-copy="isCopyMode" />
234 <!-- 设置日历颜色弹窗 --> 232 <!-- 设置日历颜色弹窗 -->
235 <CalendarColorPicker v-model:visible="showColorPopup" :daily-template-id="selectedPlanId" 233 <CalendarColorPicker v-model:visible="showColorPopup" :daily-template-id="selectedPlanId"
236 :current-color="currentPlan?.templateBackgroundColor || '#ffffff'" @success="calendarColorPickerSuccess" /> 234 :current-color="currentPlan?.templateBackgroundColor || '#ffffff'" @success="calendarColorPickerSuccess" />
@@ -242,7 +240,7 @@ @@ -242,7 +240,7 @@
242 240
243 <script setup> 241 <script setup>
244 import dayjs from 'dayjs'; 242 import dayjs from 'dayjs';
245 -import { ref, onMounted, watch, computed, nextTick } from 'vue'; 243 +import { ref, watch, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
246 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; 244 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
247 import TemplatesApi from '@/sheep/api/Template/Templates'; 245 import TemplatesApi from '@/sheep/api/Template/Templates';
248 import RiliRiqibeizhu from '@/pages/xunji/components/rili-components/rili-riqibeizhu.vue' 246 import RiliRiqibeizhu from '@/pages/xunji/components/rili-components/rili-riqibeizhu.vue'
@@ -278,7 +276,7 @@ const moreShow = ref(false); @@ -278,7 +276,7 @@ const moreShow = ref(false);
278 const showColorPopup = ref(false); 276 const showColorPopup = ref(false);
279 const calendarPopupRef = ref(null); 277 const calendarPopupRef = ref(null);
280 const currentEditId = ref(null); 278 const currentEditId = ref(null);
281 -const selectedPlanId = ref(null); 279 +const selectedPlanId = ref(0);
282 280
283 // Store / 子组件引用 281 // Store / 子组件引用
284 const trainingStore = useTrainingStore(); 282 const trainingStore = useTrainingStore();
@@ -325,7 +323,7 @@ const openXiuGai = (unit, idx) => { @@ -325,7 +323,7 @@ const openXiuGai = (unit, idx) => {
325 323
326 // 打开日历弹窗 324 // 打开日历弹窗
327 const openColorPopup = () => { 325 const openColorPopup = () => {
328 - if (!currentPlan.value) { // 加这个 326 + if (!currentPlan.value) {
329 uni.showToast({ title: '空白状态无法操作', icon: 'none' }) 327 uni.showToast({ title: '空白状态无法操作', icon: 'none' })
330 return 328 return
331 } 329 }
@@ -340,11 +338,10 @@ const openColorPopup = () => { @@ -340,11 +338,10 @@ const openColorPopup = () => {
340 338
341 const handleAddTrain = async () => { 339 const handleAddTrain = async () => {
342 await loaddailytemplate(); 340 await loaddailytemplate();
343 - // 更新主页面  
344 emit('refreshCalendar'); 341 emit('refreshCalendar');
345 } 342 }
  343 +
346 // ===== 计算属性 ===== 344 // ===== 计算属性 =====
347 -// 当前正在显示的训记  
348 const currentPlan = computed(() => { 345 const currentPlan = computed(() => {
349 if (!resdailyData.value.length) return null; 346 if (!resdailyData.value.length) return null;
350 return resdailyData.value[currentPlanIndex.value] || null; 347 return resdailyData.value[currentPlanIndex.value] || null;
@@ -353,29 +350,19 @@ const currentPlan = computed(() => { @@ -353,29 +350,19 @@ const currentPlan = computed(() => {
353 // ===== API:加载每日模板数据 ===== 350 // ===== API:加载每日模板数据 =====
354 const loaddailytemplate = async () => { 351 const loaddailytemplate = async () => {
355 if (!selectedDate.value) return; 352 if (!selectedDate.value) return;
356 - console.log('【子组件】开始加载数据...');  
357 try { 353 try {
358 const resdaily = await dailytemplateApi.getdailytemplate(String(selectedDate.value)); 354 const resdaily = await dailytemplateApi.getdailytemplate(String(selectedDate.value));
359 - console.log('打印每日模板数据:resdaily', resdaily);  
360 -  
361 const noteListFromApi = resdaily.data.notes; 355 const noteListFromApi = resdaily.data.notes;
362 noteList.value = noteListFromApi; 356 noteList.value = noteListFromApi;
363 - console.log('打印每日模板详情的备注列表:noteList', noteList.value);  
364 -  
365 const historyFromApi = resdaily.data.noteHistoryList || []; 357 const historyFromApi = resdaily.data.noteHistoryList || [];
366 - console.log('打印历史备注:', historyFromApi);  
367 noteHistoryList.value = historyFromApi; 358 noteHistoryList.value = historyFromApi;
368 -  
369 resdailyData.value = resdaily.data.templates || []; 359 resdailyData.value = resdaily.data.templates || [];
370 -  
371 - // 切换回第一个训记  
372 const oldIndex = currentPlanIndex.value; 360 const oldIndex = currentPlanIndex.value;
373 if (resdailyData.value.length > oldIndex) { 361 if (resdailyData.value.length > oldIndex) {
374 currentPlanIndex.value = oldIndex; 362 currentPlanIndex.value = oldIndex;
375 } else { 363 } else {
376 currentPlanIndex.value = 0; 364 currentPlanIndex.value = 0;
377 } 365 }
378 - console.log('+++++【子组件】+++++加载完成,训记列表:', resdailyData.value)  
379 } catch (error) { 366 } catch (error) {
380 console.error('加载训练模板失败:', error); 367 console.error('加载训练模板失败:', error);
381 resdailyData.value = []; 368 resdailyData.value = [];
@@ -383,21 +370,18 @@ const loaddailytemplate = async () => { @@ -383,21 +370,18 @@ const loaddailytemplate = async () => {
383 }; 370 };
384 371
385 const handleRenderSuccess = async () => { 372 const handleRenderSuccess = async () => {
386 -  
387 await loaddailytemplate(); 373 await loaddailytemplate();
388 - await nextTick() 374 + emit('refreshCalendar');
  375 + uni.$emit('calendarDataRefresh');
389 } 376 }
390 377
391 const switchPlan = (index) => { 378 const switchPlan = (index) => {
392 currentPlanIndex.value = index; 379 currentPlanIndex.value = index;
393 - console.log('【切换标签后 - currentPlan】', currentPlan.value);  
394 }; 380 };
395 381
396 // ===== 更多弹窗:操作项 ===== 382 // ===== 更多弹窗:操作项 =====
397 const handlePlanMore = () => { 383 const handlePlanMore = () => {
398 - if (!currentPlan.value) { // 加这个  
399 - return  
400 - } 384 + if (!currentPlan.value) return
401 moreShow.value = true; 385 moreShow.value = true;
402 } 386 }
403 387
@@ -407,28 +391,20 @@ const closeMorePopup = () => { @@ -407,28 +391,20 @@ const closeMorePopup = () => {
407 391
408 // 一键打勾(完成训练) 392 // 一键打勾(完成训练)
409 const handleCompleteCheck = async () => { 393 const handleCompleteCheck = async () => {
410 - // 日期校验:只能对今天及以前的日期一键打勾  
411 const today = dayjs().format('YYYY-MM-DD'); 394 const today = dayjs().format('YYYY-MM-DD');
412 if (selectedDate.value > today) { 395 if (selectedDate.value > today) {
413 uni.showToast({ title: '一键打勾只能用于今天及以前的日期', icon: 'none' }); 396 uni.showToast({ title: '一键打勾只能用于今天及以前的日期', icon: 'none' });
414 return; 397 return;
415 } 398 }
416 -  
417 if (!currentPlan.value?.id) { 399 if (!currentPlan.value?.id) {
418 uni.showToast({ title: '未找到训练ID', icon: 'none' }) 400 uni.showToast({ title: '未找到训练ID', icon: 'none' })
419 return 401 return
420 } 402 }
421 -  
422 - console.log('进入一键打勾函数训练id=', currentPlan.value.id);  
423 -  
424 try { 403 try {
425 - // 2. 调用接口  
426 await dailytemplateApi.completeDailyTemplate(currentPlan.value.id) 404 await dailytemplateApi.completeDailyTemplate(currentPlan.value.id)
427 emit('refreshCalendar'); 405 emit('refreshCalendar');
428 uni.showToast({ title: '一键打勾成功', icon: 'success' }) 406 uni.showToast({ title: '一键打勾成功', icon: 'success' })
429 closeMorePopup() 407 closeMorePopup()
430 -  
431 - // 刷新页面数据  
432 await loaddailytemplate(selectedDate.value) 408 await loaddailytemplate(selectedDate.value)
433 } catch (err) { 409 } catch (err) {
434 console.error('一键打勾失败:', err) 410 console.error('一键打勾失败:', err)
@@ -438,54 +414,39 @@ const handleCompleteCheck = async () => { @@ -438,54 +414,39 @@ const handleCompleteCheck = async () => {
438 414
439 // ===== 保存训练为个人模板 ===== 415 // ===== 保存训练为个人模板 =====
440 const openSaveTemplateDialog = async (item) => { 416 const openSaveTemplateDialog = async (item) => {
441 - // 关闭所有可能遮挡的弹窗  
442 moreShow.value = false; 417 moreShow.value = false;
443 showColorPopup.value = false; 418 showColorPopup.value = false;
444 showAddTrainPopup.value = false; 419 showAddTrainPopup.value = false;
445 showRiqibeizhu.value = false; 420 showRiqibeizhu.value = false;
446 show.value = false 421 show.value = false
447 - // 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭  
448 await nextTick(); 422 await nextTick();
449 -  
450 - // uni弹窗输入框:只填模板名称  
451 uni.showModal({ 423 uni.showModal({
452 title: '确认保存为你的训练模板?', 424 title: '确认保存为你的训练模板?',
453 content: '', 425 content: '',
454 - editable: true, // 开启输入框 426 + editable: true,
455 placeholderText: '请输入模板名称', 427 placeholderText: '请输入模板名称',
456 success: async (res) => { 428 success: async (res) => {
457 - // 点击确定  
458 if (res.confirm) { 429 if (res.confirm) {
459 const templateName = res.content.trim() 430 const templateName = res.content.trim()
460 - // 非名校验  
461 if (!templateName) { 431 if (!templateName) {
462 return uni.showToast({ title: '请填写模板名称', icon: 'none' }) 432 return uni.showToast({ title: '请填写模板名称', icon: 'none' })
463 } 433 }
464 - // 调用封装的数据转换函数,组装后端需要的入参  
465 const reqData = formatTrainToTemplate(item, templateName) 434 const reqData = formatTrainToTemplate(item, templateName)
466 - // 发起创建接口  
467 - console.log('reqData', reqData);  
468 await submitCreateTemplate(reqData) 435 await submitCreateTemplate(reqData)
469 } 436 }
470 } 437 }
471 }) 438 })
472 } 439 }
473 440
474 -/**  
475 - */  
476 const formatTrainToTemplate = (item, templateName) => { 441 const formatTrainToTemplate = (item, templateName) => {
477 - // 初始化基础结构  
478 const params = { 442 const params = {
479 templateId: item.templateId, 443 templateId: item.templateId,
480 - // groupId: 0, // 默认存入根目录,后续选文件夹再传对应groupId  
481 templateName: templateName, 444 templateName: templateName,
482 scene: item.scene || 0, 445 scene: item.scene || 0,
483 - templateCover: item.urlCover || '', // 封面取自训练封面  
484 - templateIntroduction: item.templateIntroduction || '', // 简介留空,可后续扩展 446 + templateCover: item.urlCover || '',
  447 + templateIntroduction: item.templateIntroduction || '',
485 units: [] 448 units: []
486 } 449 }
487 -  
488 - // 循环转换 units 结构(训练组→模板unit)  
489 params.units = item.units.map(unit => { 450 params.units = item.units.map(unit => {
490 return { 451 return {
491 id: unit.unitId, 452 id: unit.unitId,
@@ -495,16 +456,11 @@ const formatTrainToTemplate = (item, templateName) => { @@ -495,16 +456,11 @@ const formatTrainToTemplate = (item, templateName) => {
495 exercises: unit.exercises 456 exercises: unit.exercises
496 } 457 }
497 }) 458 })
498 -  
499 - console.log('params', params);  
500 -  
501 -  
502 return params 459 return params
503 } 460 }
504 461
505 const submitCreateTemplate = async (reqData) => { 462 const submitCreateTemplate = async (reqData) => {
506 try { 463 try {
507 - // 调用创建个人模板接口  
508 const res = await TemplatesApi.createCustTemplate(reqData) 464 const res = await TemplatesApi.createCustTemplate(reqData)
509 if (res.code === 0) { 465 if (res.code === 0) {
510 uni.showToast({ title: '模板保存成功' }) 466 uni.showToast({ title: '模板保存成功' })
@@ -521,9 +477,6 @@ const submitCreateTemplate = async (reqData) => { @@ -521,9 +477,6 @@ const submitCreateTemplate = async (reqData) => {
521 const openAddTrainPopup = () => { 477 const openAddTrainPopup = () => {
522 showAddTrainPopup.value = true; 478 showAddTrainPopup.value = true;
523 }; 479 };
524 -const closeAddTrainPopup = () => {  
525 - showAddTrainPopup.value = false;  
526 -};  
527 480
528 // ===== 工具函数 ===== 481 // ===== 工具函数 =====
529 const formatSecondsToHms = (seconds) => { 482 const formatSecondsToHms = (seconds) => {
@@ -531,7 +484,6 @@ const formatSecondsToHms = (seconds) => { @@ -531,7 +484,6 @@ const formatSecondsToHms = (seconds) => {
531 return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss'); 484 return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss');
532 } 485 }
533 486
534 -// ===== 工具函数:根据动作类型生成组次显示文本 =====  
535 const getSetDisplayText = (set, exerciseType) => { 487 const getSetDisplayText = (set, exerciseType) => {
536 if (!set) return ''; 488 if (!set) return '';
537 switch (exerciseType) { 489 switch (exerciseType) {
@@ -546,7 +498,6 @@ const getSetDisplayText = (set, exerciseType) => { @@ -546,7 +498,6 @@ const getSetDisplayText = (set, exerciseType) => {
546 } 498 }
547 }; 499 };
548 500
549 -// 超级组中的动作显示(与普通动作在 type 4/5 上的展示略有差异)  
550 const getSuperSetDisplayText = (set, exerciseType) => { 501 const getSuperSetDisplayText = (set, exerciseType) => {
551 if (!set) return ''; 502 if (!set) return '';
552 switch (exerciseType) { 503 switch (exerciseType) {
@@ -561,17 +512,6 @@ const getSuperSetDisplayText = (set, exerciseType) => { @@ -561,17 +512,6 @@ const getSuperSetDisplayText = (set, exerciseType) => {
561 } 512 }
562 }; 513 };
563 514
564 -// 返回按钮逻辑  
565 -const goBack = () => {  
566 - uni.navigateBack({  
567 - delta: 1,  
568 - fail: () => {  
569 - uni.redirectTo({ url: '/pages/xunji/xunji-rili' });  
570 - },  
571 - });  
572 -};  
573 -  
574 -// 日期格式化(使用 dayjs 处理,兼容性更好)  
575 const formatDateWithWeek = (dateStr) => { 515 const formatDateWithWeek = (dateStr) => {
576 if (!dateStr) return ''; 516 if (!dateStr) return '';
577 const d = dayjs(dateStr); 517 const d = dayjs(dateStr);
@@ -582,18 +522,13 @@ const formatDateWithWeek = (dateStr) => { @@ -582,18 +522,13 @@ const formatDateWithWeek = (dateStr) => {
582 // ===== 日期备注相关 ===== 522 // ===== 日期备注相关 =====
583 const handleDateNote = () => { 523 const handleDateNote = () => {
584 if (noteList.value.length >= 5) { 524 if (noteList.value.length >= 5) {
585 - uni.showToast({  
586 - title: '最多添加5条,已超出数量无法添加',  
587 - icon: 'none',  
588 - duration: 2000  
589 - }); 525 + uni.showToast({ title: '最多添加5条,已超出数量无法添加', icon: 'none', duration: 2000 });
590 return; 526 return;
591 } 527 }
592 showRiqibeizhu.value = true 528 showRiqibeizhu.value = true
593 } 529 }
594 530
595 const handleEditNote = (note) => { 531 const handleEditNote = (note) => {
596 - console.log('准备编辑备注,ID:', note.id);  
597 currentEditId.value = note.id; 532 currentEditId.value = note.id;
598 showRiqibeizhu.value = true; 533 showRiqibeizhu.value = true;
599 }; 534 };
@@ -608,8 +543,6 @@ const handleCloseNotePopup = () => { @@ -608,8 +543,6 @@ const handleCloseNotePopup = () => {
608 543
609 // ===== 复制到 / 移动到 ===== 544 // ===== 复制到 / 移动到 =====
610 const openCalendarPopup = (templateId) => { 545 const openCalendarPopup = (templateId) => {
611 - console.log('点击了添加到日历按钮', templateId); // 新增日志  
612 - console.log('子组件实例', calendarPopupRef.value); // 新增日志  
613 if (calendarPopupRef.value && typeof calendarPopupRef.value.open === 'function') { 546 if (calendarPopupRef.value && typeof calendarPopupRef.value.open === 'function') {
614 calendarPopupRef.value.open(); 547 calendarPopupRef.value.open();
615 } else { 548 } else {
@@ -617,19 +550,47 @@ const openCalendarPopup = (templateId) => { @@ -617,19 +550,47 @@ const openCalendarPopup = (templateId) => {
617 } 550 }
618 }; 551 };
619 552
  553 +// 今日再练 / 去训练 / 当日开练
  554 +const trainAgainText = computed(() => {
  555 + const today = dayjs().format('YYYY-MM-DD');
  556 + if (!selectedDate.value) return '今日再练';
  557 + if (selectedDate.value === today) return '今日再练';
  558 + if (selectedDate.value > today) return '当日开练';
  559 + return '去训练';
  560 +});
  561 +
  562 +const trainAgainDisabled = computed(() => {
  563 + const today = dayjs().format('YYYY-MM-DD');
  564 + return !!selectedDate.value && selectedDate.value > today;
  565 +});
620 566
621 -// 2. 今日再练方法  
622 const handleTrainAgain = async () => { 567 const handleTrainAgain = async () => {
  568 + const today = dayjs().format('YYYY-MM-DD');
  569 + const planDate = selectedDate.value || today;
  570 +
  571 + if (planDate > today) {
  572 + uni.showToast({ title: '训练将在当日解锁', icon: 'none' });
  573 + return;
  574 + }
  575 +
  576 + if (planDate < today) {
  577 + trainingStore.isSystem = currentPlan.value.isSystem || false;
  578 + trainingStore.loadDailyTemplateForEdit(currentPlan.value);
  579 + trainingStore.initDailyTemplateRecords();
  580 + uni.navigateTo({
  581 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3&dailyTemplateId=${currentPlan.value.id}&isTraining=true`,
  582 + });
  583 + return;
  584 + }
  585 +
623 if (!currentPlan.value?.templateId) { 586 if (!currentPlan.value?.templateId) {
624 uni.showToast({ title: '未找到训练计划', icon: 'none' }); 587 uni.showToast({ title: '未找到训练计划', icon: 'none' });
625 return; 588 return;
626 } 589 }
627 590
628 - const todayDate = new Date().toISOString().split('T')[0]; // 格式:2026-05-19  
629 const reqData = { 591 const reqData = {
630 id: currentPlan.value.templateId, 592 id: currentPlan.value.templateId,
631 - // planId: currentPlan.value.planId || 0,  
632 - trainDateList: [todayDate] // 训练日期列表,这里只传今天 593 + trainDateList: [today],
633 }; 594 };
634 595
635 try { 596 try {
@@ -637,7 +598,7 @@ const handleTrainAgain = async () => { @@ -637,7 +598,7 @@ const handleTrainAgain = async () => {
637 if (res.code === 0) { 598 if (res.code === 0) {
638 uni.showToast({ title: '今日再练成功', icon: 'success' }); 599 uni.showToast({ title: '今日再练成功', icon: 'success' });
639 loaddailytemplate(props.date); 600 loaddailytemplate(props.date);
640 - emit('refreshCalendar') 601 + emit('refreshCalendar');
641 } else { 602 } else {
642 uni.showToast({ title: res.msg || '添加失败', icon: 'none' }); 603 uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
643 } 604 }
@@ -646,22 +607,19 @@ const handleTrainAgain = async () => { @@ -646,22 +607,19 @@ const handleTrainAgain = async () => {
646 uni.showToast({ title: '网络异常,请重试', icon: 'none' }); 607 uni.showToast({ title: '网络异常,请重试', icon: 'none' });
647 } 608 }
648 }; 609 };
  610 +
649 // 删除训练 611 // 删除训练
650 const handleDeleteTemplate = async () => { 612 const handleDeleteTemplate = async () => {
651 - // 先判断有没有ID  
652 if (!currentPlan.value?.id) { 613 if (!currentPlan.value?.id) {
653 uni.showToast({ title: '未找到模板ID', icon: 'none' }); 614 uni.showToast({ title: '未找到模板ID', icon: 'none' });
654 return; 615 return;
655 } 616 }
656 - // 关闭所有可能遮挡的弹窗  
657 moreShow.value = false; 617 moreShow.value = false;
658 showColorPopup.value = false; 618 showColorPopup.value = false;
659 showAddTrainPopup.value = false; 619 showAddTrainPopup.value = false;
660 showRiqibeizhu.value = false; 620 showRiqibeizhu.value = false;
661 show.value = false 621 show.value = false
662 - // 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭  
663 await nextTick(); 622 await nextTick();
664 - // 弹出确认框  
665 uni.showModal({ 623 uni.showModal({
666 title: '确认删除', 624 title: '确认删除',
667 content: '确定要删除这条训练记录吗?删除后无法恢复', 625 content: '确定要删除这条训练记录吗?删除后无法恢复',
@@ -674,8 +632,8 @@ const handleDeleteTemplate = async () => { @@ -674,8 +632,8 @@ const handleDeleteTemplate = async () => {
674 await dailytemplateApi.deleteDailyTemplate(currentPlan.value.id); 632 await dailytemplateApi.deleteDailyTemplate(currentPlan.value.id);
675 emit('refreshCalendar'); 633 emit('refreshCalendar');
676 uni.showToast({ title: '删除成功', icon: 'success' }); 634 uni.showToast({ title: '删除成功', icon: 'success' });
677 - loaddailytemplate(props.date); // 刷新数据  
678 - moreShow.value = false; // 关闭弹窗 635 + loaddailytemplate(props.date);
  636 + moreShow.value = false;
679 } catch (err) { 637 } catch (err) {
680 console.error('删除失败', err); 638 console.error('删除失败', err);
681 uni.showToast({ title: '删除失败,请重试', icon: 'none' }); 639 uni.showToast({ title: '删除失败,请重试', icon: 'none' });
@@ -687,13 +645,8 @@ const handleDeleteTemplate = async () => { @@ -687,13 +645,8 @@ const handleDeleteTemplate = async () => {
687 // 更多弹窗的结束整个计划 645 // 更多弹窗的结束整个计划
688 const confirmDelete = async (id) => { 646 const confirmDelete = async (id) => {
689 try { 647 try {
690 - console.log('打印计划个人id:', id);  
691 -  
692 const res = await QueryPlanApi.deletePlan(id); 648 const res = await QueryPlanApi.deletePlan(id);
693 - console.log('删除结果:', res);  
694 -  
695 if (res.code === 0 && res.data) { 649 if (res.code === 0 && res.data) {
696 - // 2. 删除成功,从列表移除  
697 uni.showToast({ title: '计划已结束', icon: 'success' }); 650 uni.showToast({ title: '计划已结束', icon: 'success' });
698 emit('refreshCalendar'); 651 emit('refreshCalendar');
699 } else { 652 } else {
@@ -710,18 +663,12 @@ const confirmDelete = async (id) => { @@ -710,18 +663,12 @@ const confirmDelete = async (id) => {
710 663
711 // 移动到:只打开弹窗 664 // 移动到:只打开弹窗
712 const handleMoveTo = async () => { 665 const handleMoveTo = async () => {
713 - console.log('点击了更多弹窗的移动到');  
714 -  
715 - // 安全判断  
716 if (!currentPlan.value) { 666 if (!currentPlan.value) {
717 uni.showToast({ title: '未找到训练信息', icon: 'none' }); 667 uni.showToast({ title: '未找到训练信息', icon: 'none' });
718 return; 668 return;
719 } 669 }
720 isMoveMode.value = true; 670 isMoveMode.value = true;
721 - // 关闭更多弹窗  
722 moreShow.value = false; 671 moreShow.value = false;
723 -  
724 - // 只做一件事:打开日期选择  
725 nextTick(() => { 672 nextTick(() => {
726 openCalendarPopup(currentPlan.value.templateId); 673 openCalendarPopup(currentPlan.value.templateId);
727 }); 674 });
@@ -730,9 +677,8 @@ const handleMoveTo = async () => { @@ -730,9 +677,8 @@ const handleMoveTo = async () => {
730 // 复制到 677 // 复制到
731 const openCopyCalendarPopup = (templateId) => { 678 const openCopyCalendarPopup = (templateId) => {
732 if (!templateId) return uni.showToast({ title: '模板ID不存在', icon: 'none' }) 679 if (!templateId) return uni.showToast({ title: '模板ID不存在', icon: 'none' })
733 - console.log('currentPlan?.dailyTemplateId=', currentPlan?.dailyTemplateId);  
734 isCopyMode.value = true 680 isCopyMode.value = true
735 - isMoveMode.value = false // 清空移动标记,互斥 681 + isMoveMode.value = false
736 moreShow.value = false 682 moreShow.value = false
737 nextTick(() => { 683 nextTick(() => {
738 calendarPopupRef.value.open(); 684 calendarPopupRef.value.open();
@@ -740,35 +686,13 @@ const openCopyCalendarPopup = (templateId) => { @@ -740,35 +686,13 @@ const openCopyCalendarPopup = (templateId) => {
740 } 686 }
741 687
742 const handleCalendarSuccess = () => { 688 const handleCalendarSuccess = () => {
743 - // console.log('✅ 添加日历成功,当前模式:', isMoveMode.value ? '移动' : '复制');  
744 - // 复制模式:只提示成功,不用删原数据  
745 - // if (!isMoveMode.value) {  
746 - // uni.showToast({  
747 - // title: '复制成功',  
748 - // icon: 'success'  
749 - // });  
750 - // return;  
751 - // }  
752 - console.log('✅ 添加日历成功', {  
753 - move: isMoveMode.value,  
754 - copy: isCopyMode.value  
755 - });  
756 -  
757 - // 复制模式:仅刷新日历,不删除原数据  
758 if (isCopyMode.value) { 689 if (isCopyMode.value) {
759 - uni.showToast({  
760 - title: '复制成功',  
761 - icon: 'success'  
762 - });  
763 - // 重置标记 690 + uni.showToast({ title: '复制成功', icon: 'success' });
764 isCopyMode.value = false 691 isCopyMode.value = false
765 emit('refreshCalendar') 692 emit('refreshCalendar')
766 return; 693 return;
767 } 694 }
768 - // 移动模式:子组件已把数据新增到新日期,这里删除原日期的训练记录  
769 - if (!currentPlan.value?.id) {  
770 - return;  
771 - } 695 + if (!currentPlan.value?.id) return;
772 dailytemplateApi.deleteDailyTemplate(currentPlan.value.id) 696 dailytemplateApi.deleteDailyTemplate(currentPlan.value.id)
773 .then(() => { 697 .then(() => {
774 uni.showToast({ title: '移动成功', icon: 'success' }); 698 uni.showToast({ title: '移动成功', icon: 'success' });
@@ -788,15 +712,10 @@ const calendarColorPickerSuccess = () => { @@ -788,15 +712,10 @@ const calendarColorPickerSuccess = () => {
788 712
789 // ===== 去训练 / 每日模板编辑 ===== 713 // ===== 去训练 / 每日模板编辑 =====
790 const startTraining = () => { 714 const startTraining = () => {
791 -  
792 if (trainingStore.isTraining) { 715 if (trainingStore.isTraining) {
793 - uni.showToast({  
794 - title: '当前已有正在进行的训练',  
795 - icon: 'none'  
796 - }); 716 + uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' });
797 return; 717 return;
798 } 718 }
799 -  
800 trainingStore.isTraining = true; 719 trainingStore.isTraining = true;
801 if (resdailyData.value.length > 0) { 720 if (resdailyData.value.length > 0) {
802 trainingStore.isSystem = currentPlan.value.isSystem || false; 721 trainingStore.isSystem = currentPlan.value.isSystem || false;
@@ -805,52 +724,44 @@ const startTraining = () => { @@ -805,52 +724,44 @@ const startTraining = () => {
805 uni.navigateTo({ 724 uni.navigateTo({
806 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3&dailyTemplateId=${currentPlan.value.id}&isTraining=true`, 725 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3&dailyTemplateId=${currentPlan.value.id}&isTraining=true`,
807 }); 726 });
808 - console.log('去训练开始进入编辑模板页面,模板ID:', currentPlan.value.templateId, '每日模板ID:', currentPlan.value.id);  
809 -  
810 - console.log('打印传递给动作训练页面的模板id', currentPlan.value.templateId);  
811 } else { 727 } else {
812 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' }) 728 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' })
813 } 729 }
814 -  
815 -  
816 -  
817 }; 730 };
818 731
819 -// 每日模板编辑  
820 const templateEdit = (plan) => { 732 const templateEdit = (plan) => {
821 trainingStore.isSystem = plan.isSystem; 733 trainingStore.isSystem = plan.isSystem;
822 trainingStore.loadDailyTemplateForEdit(plan); 734 trainingStore.loadDailyTemplateForEdit(plan);
823 trainingStore.initDailyTemplateRecords() 735 trainingStore.initDailyTemplateRecords()
824 uni.navigateTo({ 736 uni.navigateTo({
825 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${plan.templateId}&type=3&dailyTemplateId=${plan.id}`, 737 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${plan.templateId}&type=3&dailyTemplateId=${plan.id}&isEdit=true`,
826 }); 738 });
827 - console.log('开始进入编辑模板页面,模板ID:', plan.templateId, '每日模板ID:', plan.id);  
828 }; 739 };
829 740
830 // ===== 监听 & 生命周期 ===== 741 // ===== 监听 & 生命周期 =====
831 -// 监听父组件传进来的 date 变化  
832 watch( 742 watch(
833 () => props.date, 743 () => props.date,
834 (newDate) => { 744 (newDate) => {
835 if (newDate && newDate.trim()) { 745 if (newDate && newDate.trim()) {
836 - console.log('子组件监听到日期变化:', newDate);  
837 selectedDate.value = newDate; 746 selectedDate.value = newDate;
838 displayDate.value = formatDateWithWeek(newDate); 747 displayDate.value = formatDateWithWeek(newDate);
839 loaddailytemplate(newDate); 748 loaddailytemplate(newDate);
840 - console.log('+++++++currentPlan:+++++++++++++++++', currentPlan.value)  
841 - // console.log('props.noteList=', props.noteList);  
842 -  
843 } 749 }
844 }, 750 },
845 { immediate: true } 751 { immediate: true }
846 ); 752 );
847 753
  754 +// 监听来自编辑/训练页面的数据刷新事件,自动刷新弹窗数据
  755 +const handleCalendarDataRefresh = () => {
  756 + loaddailytemplate();
  757 +};
848 758
849 onMounted(() => { 759 onMounted(() => {
850 - console.log('【rilicell子组件】已挂载!!!');  
851 - console.log('【子组件】props.date:', props.date);  
852 - // loaddailytemplate(props.date); 760 + uni.$on('calendarDataRefresh', handleCalendarDataRefresh);
  761 +});
853 762
  763 +onBeforeUnmount(() => {
  764 + uni.$off('calendarDataRefresh', handleCalendarDataRefresh);
854 }); 765 });
855 </script> 766 </script>
856 767
@@ -860,13 +771,6 @@ $color-primary: #333; @@ -860,13 +771,6 @@ $color-primary: #333;
860 $color-accent: #ffc107; 771 $color-accent: #ffc107;
861 $color-bg: #f2f3f5; 772 $color-bg: #f2f3f5;
862 $color-white: #fff; 773 $color-white: #fff;
863 -$color-danger: #ff4d4f;  
864 -$color-muted: #a2a2a2;  
865 -$color-card-bg: #f9f9f9;  
866 -$radius-sm: 8rpx;  
867 -$radius-md: 12rpx;  
868 -$radius-lg: 16rpx;  
869 -$radius-round: 40rpx;  
870 774
871 /* 全局容器 */ 775 /* 全局容器 */
872 .container { 776 .container {
@@ -888,27 +792,11 @@ $radius-round: 40rpx; @@ -888,27 +792,11 @@ $radius-round: 40rpx;
888 justify-content: space-between; 792 justify-content: space-between;
889 padding: 24rpx 20rpx; 793 padding: 24rpx 20rpx;
890 background-color: #fff; 794 background-color: #fff;
891 - // position: sticky;  
892 top: 0; 795 top: 0;
893 z-index: 10; 796 z-index: 10;
894 gap: 16rpx; 797 gap: 16rpx;
895 -}  
896 798
897 -.back-btn {  
898 - width: 60rpx;  
899 - height: 60rpx;  
900 - display: flex;  
901 - align-items: center;  
902 - justify-content: center;  
903 - flex-shrink: 0;  
904 -}  
905 -  
906 -.date-wrapper {  
907 - flex-grow: 1;  
908 - text-align: left;  
909 -}  
910 -  
911 -.close-btn { 799 + .close-btn {
912 width: 50rpx; 800 width: 50rpx;
913 height: 50rpx; 801 height: 50rpx;
914 display: flex; 802 display: flex;
@@ -917,16 +805,21 @@ $radius-round: 40rpx; @@ -917,16 +805,21 @@ $radius-round: 40rpx;
917 background: #f2f3f5; 805 background: #f2f3f5;
918 border-radius: 50%; 806 border-radius: 50%;
919 flex-shrink: 0; 807 flex-shrink: 0;
920 -} 808 + }
  809 +
  810 + .date-wrapper {
  811 + flex-grow: 1;
  812 + text-align: left;
  813 + }
921 814
922 -.date-text { 815 + .date-text {
923 font-size: 28rpx; 816 font-size: 28rpx;
924 white-space: nowrap; 817 white-space: nowrap;
925 overflow: hidden; 818 overflow: hidden;
926 text-overflow: ellipsis; 819 text-overflow: ellipsis;
927 -} 820 + }
928 821
929 -.date-note-btn { 822 + .date-note-btn {
930 display: inline-flex; 823 display: inline-flex;
931 align-items: center; 824 align-items: center;
932 font-size: 28rpx; 825 font-size: 28rpx;
@@ -937,9 +830,9 @@ $radius-round: 40rpx; @@ -937,9 +830,9 @@ $radius-round: 40rpx;
937 border: none; 830 border: none;
938 margin-right: 0; 831 margin-right: 0;
939 flex-shrink: 0; 832 flex-shrink: 0;
940 -} 833 + }
941 834
942 -.go-train-btn { 835 + .go-train-btn {
943 display: flex; 836 display: flex;
944 align-items: center; 837 align-items: center;
945 background: #ffc107; 838 background: #ffc107;
@@ -947,49 +840,22 @@ $radius-round: 40rpx; @@ -947,49 +840,22 @@ $radius-round: 40rpx;
947 padding: 12rpx 24rpx; 840 padding: 12rpx 24rpx;
948 border: none; 841 border: none;
949 flex-shrink: 0; 842 flex-shrink: 0;
950 -}  
951 843
952 -.go-train-text { 844 + .go-train-text {
953 font-size: 28rpx; 845 font-size: 28rpx;
954 color: #000; 846 color: #000;
955 margin-right: 8rpx; 847 margin-right: 8rpx;
956 font-weight: 500; 848 font-weight: 500;
957 -} 849 + }
958 850
959 -.go-train-tag { 851 + .go-train-tag {
960 font-size: 22rpx; 852 font-size: 22rpx;
961 color: #ffc107; 853 color: #ffc107;
962 background: #000; 854 background: #000;
963 border-radius: 50%; 855 border-radius: 50%;
964 padding: 4rpx 8rpx; 856 padding: 4rpx 8rpx;
965 font-weight: bold; 857 font-weight: bold;
966 -}  
967 -  
968 -/* 训练来源标签 */  
969 -.source-tag-row {  
970 - display: flex;  
971 - align-items: center;  
972 - padding: 16rpx 24rpx;  
973 - margin: 0rpx 20rpx;  
974 - border-radius: 12rpx;  
975 - font-size: 24rpx;  
976 - font-weight: 500;  
977 -  
978 - .source-text {  
979 - color: #fff;  
980 - }  
981 -  
982 - &.source-plan {  
983 - background: #3b82f6; /* 蓝色 - 计划 */  
984 - }  
985 - &.source-tpl {  
986 - background: #8b5cf6; /* 紫色 - 模板 */  
987 } 858 }
988 - &.source-custom {  
989 - background: #f59e0b; /* 橙色 - 自定义 */  
990 - }  
991 - &.source-free {  
992 - background: #10b981; /* 绿色 - 自由 */  
993 } 859 }
994 } 860 }
995 861
@@ -1003,102 +869,84 @@ $radius-round: 40rpx; @@ -1003,102 +869,84 @@ $radius-round: 40rpx;
1003 padding: 32rpx 24rpx; 869 padding: 32rpx 24rpx;
1004 margin: 20rpx; 870 margin: 20rpx;
1005 margin-bottom: 24rpx; 871 margin-bottom: 24rpx;
1006 -}  
1007 872
1008 -.plan-header-img { 873 + .plan-header-img {
1009 display: none; 874 display: none;
  875 + }
1010 876
1011 -}  
1012 -  
1013 -.plan-header-info { 877 + .plan-header-info {
1014 flex-grow: 1; 878 flex-grow: 1;
1015 margin-bottom: 32rpx; 879 margin-bottom: 32rpx;
1016 -} 880 + }
1017 881
1018 -.plan-header-top { 882 + .plan-header-top {
1019 display: flex; 883 display: flex;
1020 justify-content: space-between; 884 justify-content: space-between;
1021 align-items: center; 885 align-items: center;
1022 margin-bottom: 12rpx; 886 margin-bottom: 12rpx;
1023 -}  
1024 -  
1025 -.plan-heander {  
1026 - display: flex;  
1027 -} 887 + }
1028 888
1029 -.plan-title { 889 + .plan-title {
1030 font-size: 36rpx; 890 font-size: 36rpx;
1031 font-weight: bold; 891 font-weight: bold;
1032 display: block; 892 display: block;
1033 margin-bottom: 12rpx; 893 margin-bottom: 12rpx;
1034 color: #fff; 894 color: #fff;
1035 -} 895 + }
1036 896
1037 -// 卡片头部的编辑按钮  
1038 -.editButton { 897 + .editButton {
1039 display: flex; 898 display: flex;
1040 align-items: center; 899 align-items: center;
1041 gap: 4rpx; 900 gap: 4rpx;
1042 color: #fff; 901 color: #fff;
1043 font-size: 26rpx; 902 font-size: 26rpx;
1044 opacity: 0.8; 903 opacity: 0.8;
1045 - /* 让文字和图标稍微淡一点,和例图更像 */  
1046 border: 1rpx solid rgba(255, 255, 255, 0.7); 904 border: 1rpx solid rgba(255, 255, 255, 0.7);
1047 border-radius: 25rpx; 905 border-radius: 25rpx;
1048 padding: 6rpx 12rpx; 906 padding: 6rpx 12rpx;
1049 -}  
1050 - 907 + }
1051 908
1052 -.plan-meta { 909 + .plan-meta {
1053 font-size: 26rpx; 910 font-size: 26rpx;
1054 color: #ccc; 911 color: #ccc;
1055 display: block; 912 display: block;
1056 -} 913 + }
1057 914
1058 -.plan-header-btns { 915 + .plan-header-btns {
1059 display: flex; 916 display: flex;
1060 flex-direction: row; 917 flex-direction: row;
1061 align-items: center; 918 align-items: center;
1062 gap: 20rpx; 919 gap: 20rpx;
1063 justify-content: flex-start; 920 justify-content: flex-start;
1064 -}  
1065 921
1066 -.plan-btn { 922 + .plan-btn {
1067 font-size: 26rpx; 923 font-size: 26rpx;
1068 border-radius: 40rpx; 924 border-radius: 40rpx;
1069 border: none; 925 border: none;
1070 padding: 12rpx 28rpx; 926 padding: 12rpx 28rpx;
1071 line-height: 1; 927 line-height: 1;
1072 -} 928 + }
1073 929
1074 -.more-btn,  
1075 -.copy-btn { 930 + .more-btn,
  931 + .copy-btn {
1076 background: #fff; 932 background: #fff;
1077 color: #000; 933 color: #000;
1078 border: none; 934 border: none;
1079 -} 935 + }
1080 936
1081 -.go-train-btn-small { 937 + .go-train-btn-small {
1082 color: #000; 938 color: #000;
1083 display: flex; 939 display: flex;
1084 align-items: center; 940 align-items: center;
1085 padding: 12rpx 20rpx; 941 padding: 12rpx 20rpx;
1086 - background: inherit;  
1087 -}  
1088 942
1089 -.go-train-text-sm { 943 + .go-train-text-sm {
1090 font-size: 24rpx; 944 font-size: 24rpx;
1091 margin-right: 4rpx; 945 margin-right: 4rpx;
1092 font-weight: 500; 946 font-weight: 500;
1093 -}  
1094 -  
1095 -.go-train-tag-sm {  
1096 - font-size: 20rpx;  
1097 - background: #000;  
1098 - color: #ffc107;  
1099 - border-radius: 4rpx;  
1100 - padding: 2rpx 6rpx;  
1101 - font-weight: bold; 947 + }
  948 + }
  949 + }
1102 } 950 }
1103 951
1104 /* 训练动作列表 */ 952 /* 训练动作列表 */
@@ -1114,29 +962,49 @@ $radius-round: 40rpx; @@ -1114,29 +962,49 @@ $radius-round: 40rpx;
1114 display: flex; 962 display: flex;
1115 flex-direction: column; 963 flex-direction: column;
1116 gap: 12rpx; 964 gap: 12rpx;
1117 -}  
1118 -  
1119 -  
1120 -.set-index {  
1121 - display: none;  
1122 -}  
1123 -  
1124 965
1125 -  
1126 -.set-content { 966 + .set-content {
1127 flex-grow: 1; 967 flex-grow: 1;
1128 color: inherit; 968 color: inherit;
1129 display: flex; 969 display: flex;
1130 justify-content: space-between; 970 justify-content: space-between;
1131 white-space: nowrap; 971 white-space: nowrap;
  972 + }
1132 973
1133 -} 974 + .set-item {
  975 + display: flex;
  976 + align-items: center;
  977 + font-size: 26rpx;
  978 + color: #666;
  979 + justify-content: space-between;
1134 980
1135 -.detai-data {  
1136 - white-space: nowrap; 981 + .set-left {
  982 + display: flex;
  983 + align-items: center;
  984 + gap: 8rpx;
  985 + }
  986 +
  987 + .set-right {
  988 + display: flex;
  989 + gap: 20rpx;
  990 + }
  991 +
  992 + .rest-time {
  993 + font-size: 24rpx;
  994 + margin-left: 40rpx;
  995 + flex-shrink: 0;
  996 + }
  997 +
  998 + .check {
  999 + font-size: 26rpx;
  1000 + color: #333;
  1001 + margin-left: 20rpx;
  1002 + flex-shrink: 0;
  1003 + }
  1004 + }
1137 } 1005 }
1138 1006
1139 -// 主卡片:横向排列 1007 +/* 主卡片:横向排列 */
1140 .action-item { 1008 .action-item {
1141 display: flex; 1009 display: flex;
1142 align-items: flex-start; 1010 align-items: flex-start;
@@ -1145,134 +1013,60 @@ $radius-round: 40rpx; @@ -1145,134 +1013,60 @@ $radius-round: 40rpx;
1145 padding: 24rpx 20rpx; 1013 padding: 24rpx 20rpx;
1146 position: relative; 1014 position: relative;
1147 gap: 20rpx; 1015 gap: 20rpx;
1148 -}  
1149 1016
1150 -// 左侧:序号 + 图片  
1151 -.action-left { 1017 + .action-left {
1152 display: flex; 1018 display: flex;
1153 align-items: center; 1019 align-items: center;
1154 gap: 16rpx; 1020 gap: 16rpx;
1155 flex-shrink: 0; 1021 flex-shrink: 0;
1156 -} 1022 + }
1157 1023
1158 -// 大序号  
1159 -.action-index { 1024 + .action-index {
1160 font-size: 34rpx; 1025 font-size: 34rpx;
1161 font-weight: 600; 1026 font-weight: 600;
1162 color: #333; 1027 color: #333;
1163 line-height: 1; 1028 line-height: 1;
1164 -} 1029 + }
1165 1030
1166 -// 动作图片  
1167 -.action-img { 1031 + .action-img {
1168 width: 80rpx; 1032 width: 80rpx;
1169 height: 80rpx; 1033 height: 80rpx;
1170 border-radius: 8rpx; 1034 border-radius: 8rpx;
1171 -} 1035 + }
1172 1036
1173 -// 中间区域  
1174 -.action-middle { 1037 + .action-middle {
1175 flex: 1; 1038 flex: 1;
1176 display: flex; 1039 display: flex;
1177 flex-direction: column; 1040 flex-direction: column;
1178 -} 1041 + }
1179 1042
1180 -// 上半部分:名称 + 重量(左对齐)  
1181 -.action-top { 1043 + .action-top {
1182 margin-bottom: 8rpx; 1044 margin-bottom: 8rpx;
1183 -}  
1184 1045
1185 -.action-name { 1046 + .action-name {
1186 font-size: 32rpx; 1047 font-size: 32rpx;
1187 font-weight: 500; 1048 font-weight: 500;
1188 color: #111; 1049 color: #111;
1189 display: block; 1050 display: block;
1190 -} 1051 + }
1191 1052
1192 -.action-totalWeight { 1053 + .action-totalWeight {
1193 font-size: 26rpx; 1054 font-size: 26rpx;
1194 color: #333; 1055 color: #333;
1195 margin-top: 6rpx; 1056 margin-top: 6rpx;
1196 display: block; 1057 display: block;
1197 -} 1058 + }
  1059 + }
1198 1060
1199 -// 下半部分:组次行 → 向左对齐图片 ✅ 核心  
1200 -.action-bottom { 1061 + .action-bottom {
1201 margin-top: 6rpx; 1062 margin-top: 6rpx;
1202 margin-left: -96rpx; 1063 margin-left: -96rpx;
1203 - /* 往左移动,对齐图片位置 */  
1204 -}  
1205 -  
1206 -  
1207 -.set-item {  
1208 - display: flex;  
1209 - align-items: center;  
1210 - font-size: 26rpx;  
1211 - color: #666;  
1212 - justify-content: space-between;  
1213 -}  
1214 -  
1215 -.set-left {  
1216 - display: flex;  
1217 - align-items: center;  
1218 - gap: 8rpx;  
1219 - /* 控制 indexData、set-content、rest-time 之间的间距 */  
1220 -}  
1221 -  
1222 -.set-right {  
1223 - display: flex;  
1224 - gap: 20rpx;  
1225 -}  
1226 -  
1227 -// 灰色小圆点序号  
1228 -.indexData {  
1229 - width: 32rpx;  
1230 - height: 32rpx;  
1231 - border-radius: 50%;  
1232 - background: #f2f3f5;  
1233 - font-size: 22rpx;  
1234 - color: #999;  
1235 - display: inline-flex;  
1236 - align-items: center;  
1237 - justify-content: center;  
1238 - margin-right: 12rpx;  
1239 - flex-shrink: 0;  
1240 -}  
1241 -  
1242 -// .set-content {  
1243 -// color: #333;  
1244 -// }  
1245 -  
1246 -.rest-time {  
1247 - // margin-left: 100rpx;  
1248 - font-size: 24rpx;  
1249 - // color: inherit;  
1250 - margin-left: 40rpx;  
1251 - flex-shrink: 0;  
1252 -}  
1253 -  
1254 -.check {  
1255 - font-size: 26rpx;  
1256 - color: #333;  
1257 - margin-left: 20rpx;  
1258 - /* 这里控制和左边内容的距离,数值你可以自己调 */  
1259 - flex-shrink: 0;  
1260 - /* 防止被压缩 */  
1261 -} 1064 + }
1262 1065
1263 -// 右侧:修改 + 对勾  
1264 -// .action-right {  
1265 -// display: flex;  
1266 -// flex-direction: column;  
1267 -// align-items: center;  
1268 -// justify-content: space-between;  
1269 -// height: 100%;  
1270 -// }  
1271 -.action-right { 1066 + .action-right {
1272 flex-shrink: 0; 1067 flex-shrink: 0;
1273 -}  
1274 1068
1275 -.modify-btn { 1069 + .modify-btn {
1276 font-size: 26rpx; 1070 font-size: 26rpx;
1277 color: #333; 1071 color: #333;
1278 background: #f2f3f5; 1072 background: #f2f3f5;
@@ -1280,18 +1074,10 @@ $radius-round: 40rpx; @@ -1280,18 +1074,10 @@ $radius-round: 40rpx;
1280 border-radius: 40rpx; 1074 border-radius: 40rpx;
1281 padding: 8rpx 20rpx; 1075 padding: 8rpx 20rpx;
1282 line-height: 1; 1076 line-height: 1;
  1077 + }
  1078 + }
1283 } 1079 }
1284 1080
1285 -// .check-icon {  
1286 -// font-size: 30rpx;  
1287 -// color: #333;  
1288 -// margin-top: 24rpx;  
1289 -// }  
1290 -  
1291 -  
1292 -  
1293 -//=========================  
1294 -  
1295 /* 空状态区域 */ 1081 /* 空状态区域 */
1296 .empty-section { 1082 .empty-section {
1297 flex: 1; 1083 flex: 1;
@@ -1301,32 +1087,32 @@ $radius-round: 40rpx; @@ -1301,32 +1087,32 @@ $radius-round: 40rpx;
1301 justify-content: center; 1087 justify-content: center;
1302 padding: 80rpx 40rpx 120rpx; 1088 padding: 80rpx 40rpx 120rpx;
1303 box-sizing: border-box; 1089 box-sizing: border-box;
1304 -}  
1305 1090
1306 -.empty-img { 1091 + .empty-img {
1307 width: 360rpx; 1092 width: 360rpx;
1308 height: 360rpx; 1093 height: 360rpx;
1309 margin-bottom: 40rpx; 1094 margin-bottom: 40rpx;
1310 - transform: scale(3);  
1311 -} 1095 + transform: scale(1.5);
  1096 + }
1312 1097
1313 -.empty-tip { 1098 + .empty-tip {
1314 font-size: 32rpx; 1099 font-size: 32rpx;
1315 color: #666; 1100 color: #666;
1316 font-weight: 500; 1101 font-weight: 500;
1317 margin-bottom: 80rpx; 1102 margin-bottom: 80rpx;
1318 letter-spacing: 2rpx; 1103 letter-spacing: 2rpx;
1319 -} 1104 + }
1320 1105
1321 -.add-train-btn { 1106 + .add-train-btn {
1322 font-size: 30rpx; 1107 font-size: 30rpx;
1323 color: #000; 1108 color: #000;
1324 background: $color-accent; 1109 background: $color-accent;
1325 - border-radius: $radius-round; 1110 + border-radius: 40rpx;
1326 padding: 24rpx 80rpx; 1111 padding: 24rpx 80rpx;
1327 border: none; 1112 border: none;
1328 font-weight: 500; 1113 font-weight: 500;
1329 box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3); 1114 box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3);
  1115 + }
1330 } 1116 }
1331 1117
1332 /* 底部课程类型区域 */ 1118 /* 底部课程类型区域 */
@@ -1346,25 +1132,25 @@ $radius-round: 40rpx; @@ -1346,25 +1132,25 @@ $radius-round: 40rpx;
1346 background: #fff; 1132 background: #fff;
1347 border-radius: 12rpx; 1133 border-radius: 12rpx;
1348 padding: 30rpx 0; 1134 padding: 30rpx 0;
1349 -}  
1350 1135
1351 -.course-icon { 1136 + .course-icon {
1352 width: 100rpx; 1137 width: 100rpx;
1353 height: 100rpx; 1138 height: 100rpx;
1354 border-radius: 50%; 1139 border-radius: 50%;
1355 margin-bottom: 16rpx; 1140 margin-bottom: 16rpx;
1356 -} 1141 + }
1357 1142
1358 -.course-title { 1143 + .course-title {
1359 font-size: 28rpx; 1144 font-size: 28rpx;
1360 color: #333; 1145 color: #333;
1361 margin-bottom: 8rpx; 1146 margin-bottom: 8rpx;
1362 font-weight: 500; 1147 font-weight: 500;
1363 -} 1148 + }
1364 1149
1365 -.course-desc { 1150 + .course-desc {
1366 font-size: 24rpx; 1151 font-size: 24rpx;
1367 color: #999; 1152 color: #999;
  1153 + }
1368 } 1154 }
1369 1155
1370 /* 添加自助训练底部弹窗 */ 1156 /* 添加自助训练底部弹窗 */
@@ -1389,14 +1175,6 @@ $radius-round: 40rpx; @@ -1389,14 +1175,6 @@ $radius-round: 40rpx;
1389 box-sizing: border-box; 1175 box-sizing: border-box;
1390 } 1176 }
1391 1177
1392 -// .popup-indicator {  
1393 -// width: 80rpx;  
1394 -// height: 8rpx;  
1395 -// background: #ddd;  
1396 -// border-radius: 4rpx;  
1397 -// margin: 0 auto 30rpx;  
1398 -// }  
1399 -  
1400 .popup-title { 1178 .popup-title {
1401 font-size: 34rpx; 1179 font-size: 34rpx;
1402 color: #333; 1180 color: #333;
@@ -1410,16 +1188,12 @@ $radius-round: 40rpx; @@ -1410,16 +1188,12 @@ $radius-round: 40rpx;
1410 display: flex; 1188 display: flex;
1411 flex-direction: column; 1189 flex-direction: column;
1412 gap: 20rpx; 1190 gap: 20rpx;
1413 -}  
1414 1191
1415 -.popup-option-btn { 1192 + .popup-option-btn {
1416 width: 100%; 1193 width: 100%;
1417 height: 80rpx; 1194 height: 80rpx;
1418 font-size: 30rpx; 1195 font-size: 30rpx;
1419 - // color: #ffffff;  
1420 - // background: #f5f5f5;  
1421 border: none; 1196 border: none;
1422 - // border-radius: 12rpx;  
1423 display: flex; 1197 display: flex;
1424 flex-direction: column; 1198 flex-direction: column;
1425 align-items: center; 1199 align-items: center;
@@ -1429,27 +1203,26 @@ $radius-round: 40rpx; @@ -1429,27 +1203,26 @@ $radius-round: 40rpx;
1429 .small { 1203 .small {
1430 font-size: 18rpx; 1204 font-size: 18rpx;
1431 } 1205 }
1432 - 1206 + }
1433 } 1207 }
1434 1208
1435 -// 更多弹窗  
1436 -/* ========== 更多弹窗样式 ========== */  
1437 -:deep(.up-popup__content) {  
1438 - border-radius: 10rpx 10rpx 0 0;  
1439 - background: #f5f5f5;  
1440 - padding: 30rpx 20rpx;  
1441 -} 1209 +/* 更多弹窗 */
  1210 +// ::deep(.up-popup__content) {
  1211 +// border-radius: 10rpx 10rpx 0 0;
  1212 +// background: #f5f5f5;
  1213 +// padding: 30rpx 20rpx;
  1214 +// }
1442 1215
1443 .popup-header { 1216 .popup-header {
1444 text-align: center; 1217 text-align: center;
1445 margin-top: 30rpx; 1218 margin-top: 30rpx;
1446 margin-bottom: 30rpx; 1219 margin-bottom: 30rpx;
1447 -}  
1448 1220
1449 -.popup-title { 1221 + .popup-title {
1450 font-size: 34rpx; 1222 font-size: 34rpx;
1451 font-weight: 600; 1223 font-weight: 600;
1452 color: #111; 1224 color: #111;
  1225 + }
1453 } 1226 }
1454 1227
1455 .popup-list { 1228 .popup-list {
@@ -1464,15 +1237,23 @@ $radius-round: 40rpx; @@ -1464,15 +1237,23 @@ $radius-round: 40rpx;
1464 padding: 24rpx 20rpx; 1237 padding: 24rpx 20rpx;
1465 background: transparent; 1238 background: transparent;
1466 gap: 16rpx; 1239 gap: 16rpx;
1467 -}  
1468 1240
1469 -.popup-item:not(:last-child) { 1241 + &:not(:last-child) {
1470 border-bottom: 1rpx solid #eee; 1242 border-bottom: 1rpx solid #eee;
1471 -} 1243 + }
1472 1244
1473 -.item-text { 1245 + .item-text {
1474 font-size: 30rpx; 1246 font-size: 30rpx;
1475 color: #333; 1247 color: #333;
  1248 + }
  1249 +
  1250 + &.delete-item {
  1251 + background: #fff;
  1252 +
  1253 + .item-text {
  1254 + color: #ff4d4f;
  1255 + }
  1256 + }
1476 } 1257 }
1477 1258
1478 .popup-divider { 1259 .popup-divider {
@@ -1481,36 +1262,25 @@ $radius-round: 40rpx; @@ -1481,36 +1262,25 @@ $radius-round: 40rpx;
1481 margin: 20rpx 0; 1262 margin: 20rpx 0;
1482 } 1263 }
1483 1264
1484 -.delete-item {  
1485 - background: #fff;  
1486 -}  
1487 -  
1488 -.delete-text {  
1489 - color: #ff4d4f;  
1490 -}  
1491 -  
1492 -/* 按钮通用重置 */  
1493 -button { 1265 +.container {
  1266 + /* 按钮通用重置 */
  1267 + button {
1494 line-height: 1; 1268 line-height: 1;
1495 -}  
1496 1269
1497 -button::after { 1270 + &::after {
1498 border: none; 1271 border: none;
  1272 + }
  1273 + }
1499 } 1274 }
1500 1275
1501 -  
1502 /* 训记切换标签 */ 1276 /* 训记切换标签 */
1503 .plan-tabs { 1277 .plan-tabs {
1504 white-space: nowrap; 1278 white-space: nowrap;
1505 padding: 16rpx 20rpx; 1279 padding: 16rpx 20rpx;
1506 background: #fff; 1280 background: #fff;
1507 -  
1508 - // 给内部的标签留出间距,同时让它不换行  
1509 - // display: flex;  
1510 gap: 16rpx; 1281 gap: 16rpx;
1511 -}  
1512 1282
1513 -.tab-btn { 1283 + .tab-btn {
1514 display: inline-block; 1284 display: inline-block;
1515 padding: 10rpx 24rpx; 1285 padding: 10rpx 24rpx;
1516 border-radius: 32rpx; 1286 border-radius: 32rpx;
@@ -1519,18 +1289,19 @@ button::after { @@ -1519,18 +1289,19 @@ button::after {
1519 border: none; 1289 border: none;
1520 flex-shrink: 0; 1290 flex-shrink: 0;
1521 white-space: nowrap; 1291 white-space: nowrap;
1522 -}  
1523 1292
1524 -.tab-btn.active { 1293 + &.active {
1525 background: #000; 1294 background: #000;
1526 color: #fff; 1295 color: #fff;
  1296 + }
  1297 + }
1527 } 1298 }
1528 1299
1529 -/* ========== 超级组样式 ========== */ 1300 +/* 超级组样式 */
1530 .superset-wrapper { 1301 .superset-wrapper {
1531 margin-bottom: 20rpx; 1302 margin-bottom: 20rpx;
1532 background: $color-white; 1303 background: $color-white;
1533 - border-radius: $radius-md; 1304 + border-radius: 12rpx;
1534 overflow: hidden; 1305 overflow: hidden;
1535 1306
1536 .super-head { 1307 .super-head {
@@ -1613,9 +1384,8 @@ button::after { @@ -1613,9 +1384,8 @@ button::after {
1613 width: 37rpx; 1384 width: 37rpx;
1614 height: 37rpx; 1385 height: 37rpx;
1615 text-align: center; 1386 text-align: center;
1616 - flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */ 1387 + flex-shrink: 0;
1617 border-radius: 50%; 1388 border-radius: 50%;
1618 - // line-height: 60rpx;  
1619 } 1389 }
1620 1390
1621 .set-column { 1391 .set-column {
@@ -1624,23 +1394,20 @@ button::after { @@ -1624,23 +1394,20 @@ button::after {
1624 } 1394 }
1625 1395
1626 .action-in-set { 1396 .action-in-set {
1627 - // display: flex;  
1628 - // flex-direction: column;  
1629 width: 100%; 1397 width: 100%;
1630 display: flex; 1398 display: flex;
1631 align-items: center; 1399 align-items: center;
1632 margin-bottom: 8rpx; 1400 margin-bottom: 8rpx;
1633 - }  
1634 1401
1635 - .action-in-set:last-child { 1402 + &:last-child {
1636 margin-bottom: 0; 1403 margin-bottom: 0;
1637 } 1404 }
  1405 + }
1638 1406
1639 .action-letter { 1407 .action-letter {
1640 display: inline-block; 1408 display: inline-block;
1641 font-size: 26rpx; 1409 font-size: 26rpx;
1642 color: #666; 1410 color: #666;
1643 - // width: 35rpx;  
1644 white-space: nowrap; 1411 white-space: nowrap;
1645 margin-right: 12rpx; 1412 margin-right: 12rpx;
1646 flex-shrink: 0; 1413 flex-shrink: 0;
@@ -1650,11 +1417,14 @@ button::after { @@ -1650,11 +1417,14 @@ button::after {
1650 font-size: 26rpx; 1417 font-size: 26rpx;
1651 color: inherit; 1418 color: inherit;
1652 flex: 1; 1419 flex: 1;
  1420 +
  1421 + .detai-data {
  1422 + white-space: nowrap;
  1423 + }
1653 } 1424 }
1654 1425
1655 .rest-time { 1426 .rest-time {
1656 font-size: 24rpx; 1427 font-size: 24rpx;
1657 - // color: inherit;  
1658 margin-left: 10rpx; 1428 margin-left: 10rpx;
1659 flex-shrink: 0; 1429 flex-shrink: 0;
1660 } 1430 }
@@ -1665,44 +1435,43 @@ button::after { @@ -1665,44 +1435,43 @@ button::after {
1665 margin-left: 275rpx !important; 1435 margin-left: 275rpx !important;
1666 flex-shrink: 0; 1436 flex-shrink: 0;
1667 } 1437 }
1668 -}  
1669 1438
1670 -.superset-title { 1439 + .superset-title {
1671 font-size: 28rpx; 1440 font-size: 28rpx;
1672 font-weight: bold; 1441 font-weight: bold;
1673 padding: 12rpx 20rpx; 1442 padding: 12rpx 20rpx;
1674 background: #fff6cc; 1443 background: #fff6cc;
1675 border-radius: 12rpx; 1444 border-radius: 12rpx;
1676 margin-bottom: 12rpx; 1445 margin-bottom: 12rpx;
  1446 + }
1677 } 1447 }
1678 1448
1679 -// 日程备注 1449 +/* 日程备注 */
1680 .noteContent { 1450 .noteContent {
1681 display: flex; 1451 display: flex;
1682 align-items: flex-start; 1452 align-items: flex-start;
1683 gap: 12rpx; 1453 gap: 12rpx;
1684 padding: 20rpx; 1454 padding: 20rpx;
1685 background: $color-white; 1455 background: $color-white;
1686 -}  
1687 1456
1688 -.noteTitle { 1457 + .noteTitle {
1689 font-size: 26rpx; 1458 font-size: 26rpx;
1690 color: #666; 1459 color: #666;
1691 flex-shrink: 0; 1460 flex-shrink: 0;
1692 width: 150rpx; 1461 width: 150rpx;
1693 -} 1462 + }
1694 1463
1695 -.noteTags { 1464 + .noteTags {
1696 flex: 1; 1465 flex: 1;
1697 display: flex; 1466 display: flex;
1698 flex-wrap: wrap; 1467 flex-wrap: wrap;
1699 gap: 12rpx; 1468 gap: 12rpx;
  1469 + }
1700 } 1470 }
1701 1471
1702 -// u-tag 微调  
1703 -:deep(.noteTag) {  
1704 - width: auto !important;  
1705 - font-size: 24rpx;  
1706 - border-radius: 10rpx;  
1707 -} 1472 +// ::deep(.noteTag) {
  1473 +// width: auto !important;
  1474 +// font-size: 24rpx;
  1475 +// border-radius: 10rpx;
  1476 +// }
1708 </style> 1477 </style>
@@ -25,7 +25,7 @@ @@ -25,7 +25,7 @@
25 25
26 <script setup> 26 <script setup>
27 import dayjs from 'dayjs'; 27 import dayjs from 'dayjs';
28 -import { ref, computed, watch } from 'vue'; 28 +import { ref, computed, watch, onMounted } from 'vue';
29 29
30 const emit = defineEmits(['dateTypeChange', 'dateRangeChange']); 30 const emit = defineEmits(['dateTypeChange', 'dateRangeChange']);
31 31
@@ -102,6 +102,12 @@ watch( @@ -102,6 +102,12 @@ watch(
102 }, 102 },
103 { immediate: true }, 103 { immediate: true },
104 ); 104 );
  105 +
  106 +// 首次挂载时补发初始 dateTypeChange,确保父组件 handleTypeChange 被调用,
  107 +// 解决首次进入页面"周"数据不显示的问题(与手动切换行为保持一致)。
  108 +onMounted(() => {
  109 + emit('dateTypeChange', currentDateType.value);
  110 +});
105 </script> 111 </script>
106 112
107 <style scoped lang="scss"> 113 <style scoped lang="scss">
@@ -57,6 +57,7 @@ @@ -57,6 +57,7 @@
57 57
58 <script setup> 58 <script setup>
59 import { ref, onMounted, watch } from 'vue'; 59 import { ref, onMounted, watch } from 'vue';
  60 +import dayjs from 'dayjs';
60 import QueryPlanApi from '@/sheep/api/plan/queryplan'; 61 import QueryPlanApi from '@/sheep/api/plan/queryplan';
61 import WodeJihuaLibiaoTancuang from '@/pages/xunji/components/wode-jihua-libiao-tancuang.vue' 62 import WodeJihuaLibiaoTancuang from '@/pages/xunji/components/wode-jihua-libiao-tancuang.vue'
62 import { useTrainingStore } from '@/sheep/store/trainingStore' 63 import { useTrainingStore } from '@/sheep/store/trainingStore'
@@ -88,9 +89,13 @@ const props = defineProps({ @@ -88,9 +89,13 @@ const props = defineProps({
88 isMyPlan: { 89 isMyPlan: {
89 type: Boolean, 90 type: Boolean,
90 default: false 91 default: false
  92 + },
  93 + selectedDate: {
  94 + type: String,
  95 + default: () => dayjs().format('YYYY-MM-DD')
91 } 96 }
92 }) 97 })
93 -const emit = defineEmits(['update:visible', 'getPlanListLength']) 98 +const emit = defineEmits(['update:visible', 'getPlanListLength', 'success'])
94 99
95 // 计划详情 100 // 计划详情
96 const MyPlanDetail = ref({}) 101 const MyPlanDetail = ref({})
@@ -135,21 +140,21 @@ const onSelectPlan = (planId) => { @@ -135,21 +140,21 @@ const onSelectPlan = (planId) => {
135 }, 500) 140 }, 500)
136 } 141 }
137 142
138 -// ====================== 添加到今日 ====================== 143 +// ====================== 添加到选中日期 ======================
139 const addTemplateToToday = async (templateId) => { 144 const addTemplateToToday = async (templateId) => {
140 if (!templateId) { 145 if (!templateId) {
141 uni.showToast({ title: '未找到训练模板', icon: 'none' }); 146 uni.showToast({ title: '未找到训练模板', icon: 'none' });
142 return; 147 return;
143 } 148 }
144 - const todayDate = new Date().toISOString().split('T')[0];  
145 const reqData = { 149 const reqData = {
146 id: templateId, 150 id: templateId,
147 - trainDateList: [todayDate] 151 + trainDateList: [props.selectedDate]
148 }; 152 };
149 try { 153 try {
150 const res = await QueryPlanApi.addPlanToCalendar(reqData); 154 const res = await QueryPlanApi.addPlanToCalendar(reqData);
151 if (res.code === 0) { 155 if (res.code === 0) {
152 - uni.showToast({ title: '添加到今日成功', icon: 'success' }); 156 + uni.showToast({ title: '添加成功', icon: 'success' });
  157 + emit('success');
153 emit('update:visible', false); 158 emit('update:visible', false);
154 } else { 159 } else {
155 uni.showToast({ title: res.msg || '添加失败', icon: 'none' }); 160 uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 <view class="top-bar"> 4 <view class="top-bar">
5 <view class="search-wrapper"> 5 <view class="search-wrapper">
6 <up-search v-model="searchKeyword" placeholder="搜索动作名称" :showAction="false" shape="round" bgColor="#f3f4f6" 6 <up-search v-model="searchKeyword" placeholder="搜索动作名称" :showAction="false" shape="round" bgColor="#f3f4f6"
7 - placeholderColor="#9ca3af" searchIconColor="#6b7280" @clear="searchKeyword = ''" /> 7 + placeholderColor="#9ca3af" searchIconColor="#6b7280" @search="handleSearch" @clear="handleClearSearch" />
8 </view> 8 </view>
9 9
10 <view class="add-btn-wrapper"> 10 <view class="add-btn-wrapper">
@@ -36,13 +36,16 @@ @@ -36,13 +36,16 @@
36 <view class="layout-container"> 36 <view class="layout-container">
37 <!-- 左侧一级分类导航 --> 37 <!-- 左侧一级分类导航 -->
38 <scroll-view scroll-y class="left-nav" :show-scrollbar="false"> 38 <scroll-view scroll-y class="left-nav" :show-scrollbar="false">
  39 + <template v-if="!isSearchMode">
39 <view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick"> 40 <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 <up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>
41 <text class="nav-text">收藏</text> 42 <text class="nav-text">收藏</text>
42 </view> 43 </view>
  44 + </template>
43 45
44 - <view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }"  
45 - @click="switchNav(nav.id)"> 46 + <view v-for="nav in displayNavItems" :key="nav.id" class="nav-item"
  47 + :class="{ active: isSearchMode ? activeSearchCategoryId === nav.id : activeNav === nav.id }"
  48 + @click="handleNavClick(nav.id)">
46 <text class="nav-text">{{ nav.name }}</text> 49 <text class="nav-text">{{ nav.name }}</text>
47 </view> 50 </view>
48 </scroll-view> 51 </scroll-view>
@@ -142,28 +145,30 @@ const searchKeyword = ref(''); @@ -142,28 +145,30 @@ const searchKeyword = ref('');
142 const addShow = ref(false); 145 const addShow = ref(false);
143 const actionDetailRef = ref(null); 146 const actionDetailRef = ref(null);
144 147
  148 +// 搜索模式状态
  149 +const isSearchMode = computed(() => !!searchKeyword.value.trim());
  150 +const searchCategoryList = ref([]);
  151 +const activeSearchCategoryId = ref(null);
  152 +const searchCategoryData = ref([]);
  153 +
  154 +// 左栏导航项:搜索模式显示部位列表,正常模式显示分类列表
  155 +const displayNavItems = computed(() => {
  156 + return isSearchMode.value ? searchCategoryList.value : navItems.value;
  157 +});
  158 +
  159 +// 右栏动作列表:搜索模式返回选中部位的器械分组,正常模式直接返回
145 const displayExercises = computed(() => { 160 const displayExercises = computed(() => {
146 - if (!searchKeyword.value.trim()) {  
147 - return exercises.value; 161 + if (isSearchMode.value && activeSearchCategoryId.value) {
  162 + const category = searchCategoryData.value.find(item => item.categoryId === activeSearchCategoryId.value);
  163 + return category ? category.equipments : [];
148 } 164 }
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); 165 + return exercises.value;
162 }); 166 });
163 167
164 const handlePartClick = async (id) => { 168 const handlePartClick = async (id) => {
165 try { 169 try {
166 activeMotionPart.value = id; 170 activeMotionPart.value = id;
  171 + searchKeyword.value = '';
167 const exerciseRes = await ExercisesApi.getexercises({ 172 const exerciseRes = await ExercisesApi.getexercises({
168 categoriesId: activeNav.value, 173 categoriesId: activeNav.value,
169 subCategoriesId: id, 174 subCategoriesId: id,
@@ -174,6 +179,60 @@ const handlePartClick = async (id) => { @@ -174,6 +179,60 @@ const handlePartClick = async (id) => {
174 } 179 }
175 }; 180 };
176 181
  182 +// 服务端搜索动作
  183 +const handleSearch = async () => {
  184 + const keyword = searchKeyword.value.trim();
  185 + if (!keyword) return;
  186 +
  187 + // 收藏Tab和超级组Tab不支持关键词搜索
  188 + if (activeNav.value === 'collect' || activeNav.value === 'super') return;
  189 +
  190 + try {
  191 + await refreshCategories();
  192 + const exerciseRes = await ExercisesApi.getExercisesByCategory({
  193 + name: keyword,
  194 + });
  195 + const data = exerciseRes.data || [];
  196 + searchCategoryData.value = data;
  197 +
  198 + // 搜索结果按部位分组,提取部位列表作为左栏导航
  199 + searchCategoryList.value = data.map(item => ({
  200 + id: item.categoryId,
  201 + name: item.categoryName,
  202 + }));
  203 + // 默认选中第一个部位
  204 + activeSearchCategoryId.value = searchCategoryList.value.length > 0 ? searchCategoryList.value[0].id : null;
  205 + } catch (e) {
  206 + console.error('搜索动作失败:', e);
  207 + }
  208 +};
  209 +
  210 +// 清空搜索,重新加载当前分类数据
  211 +const handleClearSearch = async () => {
  212 + searchKeyword.value = '';
  213 + searchCategoryList.value = [];
  214 + activeSearchCategoryId.value = null;
  215 + searchCategoryData.value = [];
  216 +
  217 + if (activeNav.value === 'collect') {
  218 + loadCollectList();
  219 + } else if (activeNav.value === 'super') {
  220 + // 超级组不需要重新加载
  221 + } else if (activeNav.value) {
  222 + await refreshCategories();
  223 + loadExercises(activeNav.value);
  224 + }
  225 +};
  226 +
  227 +// 左栏导航点击:搜索模式按部位筛选,正常模式切换分类
  228 +const handleNavClick = (id) => {
  229 + if (isSearchMode.value) {
  230 + activeSearchCategoryId.value = id;
  231 + } else {
  232 + switchNav(id);
  233 + }
  234 +};
  235 +
177 const handleCollectClick = () => { 236 const handleCollectClick = () => {
178 activeNav.value = 'collect'; 237 activeNav.value = 'collect';
179 activeMotionPart.value = ''; 238 activeMotionPart.value = '';
@@ -203,18 +262,22 @@ const loadSuperFavoriteList = async () => { @@ -203,18 +262,22 @@ const loadSuperFavoriteList = async () => {
203 } 262 }
204 }; 263 };
205 264
206 -// 加载大类  
207 -const loadCategories = async () => { 265 +// 仅刷新分类列表,不切换当前选中分类
  266 +const refreshCategories = async () => {
208 try { 267 try {
209 await actionStore.getloadCategories(); 268 await actionStore.getloadCategories();
210 navItems.value = actionStore.showCategories; 269 navItems.value = actionStore.showCategories;
  270 + } catch (error) {
  271 + console.error('获取分类失败:', error);
  272 + }
  273 +};
211 274
  275 +// 加载大类(首次加载,自动切换到第一个分类)
  276 +const loadCategories = async () => {
  277 + await refreshCategories();
212 if (navItems.value.length > 0) { 278 if (navItems.value.length > 0) {
213 switchNav(navItems.value[0].id); 279 switchNav(navItems.value[0].id);
214 } 280 }
215 - } catch (error) {  
216 - console.error('获取分类失败:', error);  
217 - }  
218 }; 281 };
219 282
220 // 加载动作列表 283 // 加载动作列表
@@ -28,6 +28,8 @@ @@ -28,6 +28,8 @@
28 </view> 28 </view>
29 29
30 <!-- 日期网格 (动态 5行 或 6行) --> 30 <!-- 日期网格 (动态 5行 或 6行) -->
  31 +
  32 + <scroll-view class="scroll-view" scroll-y>
31 <view class="date-grid"> 33 <view class="date-grid">
32 <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{ 34 <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{
33 35
@@ -50,7 +52,6 @@ @@ -50,7 +52,6 @@
50 <text class="tag-text">{{ item.text }}</text> 52 <text class="tag-text">{{ item.text }}</text>
51 </view> 53 </view>
52 </template> 54 </template>
53 -  
54 <!-- 溢出剩余条数 --> 55 <!-- 溢出剩余条数 -->
55 <view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra"> 56 <view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">
56 +{{ getCellVisibleItems(date).remaining }} 57 +{{ getCellVisibleItems(date).remaining }}
@@ -58,6 +59,10 @@ @@ -58,6 +59,10 @@
58 </view> 59 </view>
59 </view> 60 </view>
60 </view> 61 </view>
  62 + </scroll-view>
  63 +
  64 +
  65 +
61 </view> 66 </view>
62 67
63 <!-- 单元格详情弹窗 --> 68 <!-- 单元格详情弹窗 -->
@@ -74,34 +79,50 @@ @@ -74,34 +79,50 @@
74 79
75 <view class="explain-list"> 80 <view class="explain-list">
76 <view class="explain-item"> 81 <view class="explain-item">
77 - <view class="color-block blue"></view> 82 + <view class="explain-item-left">
  83 + <view class="color-block yellow">
  84 +
  85 + </view>
78 <view class="item-info"> 86 <view class="item-info">
79 - <text class="item-label">时长</text>  
80 - <text class="item-desc">在场馆的实际运动时长</text> 87 + <text class="item-label">已完成</text>
  88 + <text class="item-desc">已完成或已结课的训练记录</text>
  89 + </view>
81 </view> 90 </view>
82 </view> 91 </view>
83 92
84 <view class="explain-item"> 93 <view class="explain-item">
85 - <view class="color-block green"></view> 94 + <view class="explain-item-left">
  95 + <view class="color-block gray">
  96 +
  97 + </view>
86 <view class="item-info"> 98 <view class="item-info">
87 - <text class="item-label">容量</text>  
88 - <text class="item-desc">力量训练累计产生的总负重重量</text> 99 + <text class="item-label">待训练</text>
  100 + <text class="item-desc">已安排的待训练计划</text>
  101 + </view>
89 </view> 102 </view>
90 </view> 103 </view>
91 104
92 <view class="explain-item"> 105 <view class="explain-item">
93 - <view class="color-block yellow"></view> 106 + <view class="explain-item-left">
  107 + <view class="color-block green">
  108 +
  109 + </view>
94 <view class="item-info"> 110 <view class="item-info">
95 - <text class="item-label">课程</text>  
96 - <text class="item-desc">训记完成或已结课的线下课程</text> 111 + <text class="item-label">训练容量</text>
  112 + <text class="item-desc">力量训练累计产生的总负重</text>
  113 + </view>
97 </view> 114 </view>
98 </view> 115 </view>
99 116
100 <view class="explain-item"> 117 <view class="explain-item">
101 - <view class="color-block gray"></view> 118 + <view class="explain-item-left">
  119 + <view class="color-block blue">
  120 +
  121 + </view>
102 <view class="item-info"> 122 <view class="item-info">
103 - <text class="item-label">排课</text>  
104 - <text class="item-desc">训记排课计划或待上课的课程</text> 123 + <text class="item-label">日程备注</text>
  124 + <text class="item-desc">相关日期的备注标记</text>
  125 + </view>
105 </view> 126 </view>
106 </view> 127 </view>
107 </view> 128 </view>
@@ -111,7 +132,7 @@ @@ -111,7 +132,7 @@
111 </template> 132 </template>
112 133
113 <script setup> 134 <script setup>
114 -import { ref, watch, computed, nextTick } from 'vue'; 135 +import { ref, watch, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
115 import dayjs from 'dayjs'; 136 import dayjs from 'dayjs';
116 import DailyTemplateApi from '@/sheep/api/calendar/date'; 137 import DailyTemplateApi from '@/sheep/api/calendar/date';
117 import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue'; 138 import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue';
@@ -250,7 +271,7 @@ const loadPlans = async (monthStr) => { @@ -250,7 +271,7 @@ const loadPlans = async (monthStr) => {
250 // 按 sourceType + sourceName 分组 271 // 按 sourceType + sourceName 分组
251 const sourceMap = new Map(); 272 const sourceMap = new Map();
252 plan.templates.forEach((template) => { 273 plan.templates.forEach((template) => {
253 - if (!template || !template.name) return; 274 + if (!template) return;
254 const sourceKey = template.sourceType 275 const sourceKey = template.sourceType
255 ? `src_${template.sourceType}_${template.sourceName || ''}` 276 ? `src_${template.sourceType}_${template.sourceName || ''}`
256 : `tpl_${template.dailyTemplateId}`; 277 : `tpl_${template.dailyTemplateId}`;
@@ -323,6 +344,14 @@ const handleRefreshCalendar = () => { @@ -323,6 +344,14 @@ const handleRefreshCalendar = () => {
323 loadPlans(currentMonth.value); 344 loadPlans(currentMonth.value);
324 }; 345 };
325 346
  347 +// 监听来自 xunji-moban-xiangqing 模板添加成功的刷新事件
  348 +onMounted(() => {
  349 + uni.$on('calendarDataRefresh', handleRefreshCalendar);
  350 +});
  351 +onBeforeUnmount(() => {
  352 + uni.$off('calendarDataRefresh', handleRefreshCalendar);
  353 +});
  354 +
326 watch( 355 watch(
327 currentMonth, 356 currentMonth,
328 (newVal) => { 357 (newVal) => {
@@ -395,12 +424,16 @@ const selectDate = async (date) => { @@ -395,12 +424,16 @@ const selectDate = async (date) => {
395 } 424 }
396 } 425 }
397 426
  427 + .scroll-view {
  428 + height: 100%;
  429 +
398 .date-grid { 430 .date-grid {
399 display: grid; 431 display: grid;
400 grid-template-columns: repeat(7, 1fr); 432 grid-template-columns: repeat(7, 1fr);
401 gap: 10rpx; 433 gap: 10rpx;
402 - }  
403 - 434 + // #ifdef MP-WEIXIN
  435 + padding-bottom: 100rpx;
  436 + // #endif
404 /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */ 437 /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
405 .date-cell { 438 .date-cell {
406 min-width: 0; 439 min-width: 0;
@@ -510,21 +543,27 @@ const selectDate = async (date) => { @@ -510,21 +543,27 @@ const selectDate = async (date) => {
510 } 543 }
511 } 544 }
512 } 545 }
  546 +
  547 +
  548 + }
  549 +
  550 +
  551 + }
513 } 552 }
514 553
515 .explain-popup-content { 554 .explain-popup-content {
516 - padding: 36rpx 32rpx 48rpx 32rpx;  
517 - background-color: #ffffff; 555 + padding: 40rpx 32rpx 60rpx 32rpx;
  556 + background-color: #f8f9fb;
518 557
519 .explain-header { 558 .explain-header {
520 display: flex; 559 display: flex;
521 justify-content: space-between; 560 justify-content: space-between;
522 align-items: center; 561 align-items: center;
523 - margin-bottom: 32rpx; 562 + margin-bottom: 36rpx;
524 563
525 .explain-title { 564 .explain-title {
526 - font-size: 32rpx;  
527 - font-weight: 600; 565 + font-size: 34rpx;
  566 + font-weight: 700;
528 color: #111827; 567 color: #111827;
529 } 568 }
530 } 569 }
@@ -532,50 +571,105 @@ const selectDate = async (date) => { @@ -532,50 +571,105 @@ const selectDate = async (date) => {
532 .explain-list { 571 .explain-list {
533 display: flex; 572 display: flex;
534 flex-direction: column; 573 flex-direction: column;
535 - gap: 28rpx; 574 + gap: 20rpx;
536 575
537 .explain-item { 576 .explain-item {
  577 + background: #fff;
  578 + border-radius: 20rpx;
  579 + padding: 24rpx 24rpx 24rpx 20rpx;
  580 + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  581 +
  582 + .explain-item-left {
538 display: flex; 583 display: flex;
539 align-items: center; 584 align-items: center;
  585 + gap: 20rpx;
  586 + }
540 587
541 .color-block { 588 .color-block {
542 - width: 36rpx;  
543 - height: 36rpx;  
544 - border-radius: 8rpx;  
545 - margin-right: 24rpx; 589 + width: 100rpx;
  590 + height: 92rpx;
  591 + border-radius: 14rpx;
  592 + flex-shrink: 0;
  593 + display: flex;
  594 + align-items: center;
  595 + justify-content: center;
  596 + position: relative;
546 597
547 - &.blue {  
548 - background-color: #bae6fd; 598 + &.yellow {
  599 + background-color: #ffdd44;
  600 + }
  601 +
  602 + &.gray {
  603 + background-color: #e8e8e8;
549 } 604 }
550 605
551 &.green { 606 &.green {
552 background-color: #bbf7d0; 607 background-color: #bbf7d0;
553 } 608 }
554 609
555 - &.yellow {  
556 - background-color: #fef08a; 610 + &.blue {
  611 + background-color: #bae6fd;
557 } 612 }
558 613
559 - &.gray {  
560 - background-color: #e5e7eb; 614 + .mini-cell-bg {
  615 + width: 58rpx;
  616 + height: 58rpx;
  617 + background: rgba(255, 255, 255, 0.7);
  618 + border-radius: 8rpx;
  619 + display: flex;
  620 + align-items: flex-end;
  621 + justify-content: center;
  622 + padding-bottom: 4rpx;
  623 + }
  624 +
  625 + .mini-tag {
  626 + font-size: 14rpx;
  627 + font-weight: 600;
  628 + border-radius: 4rpx;
  629 + padding: 1rpx 6rpx;
  630 + line-height: 20rpx;
  631 + white-space: nowrap;
  632 +
  633 + &.yellow-tag {
  634 + background: #fff3b0;
  635 + color: #8b6914;
  636 + }
  637 +
  638 + &.gray-tag {
  639 + background: #f0f0f0;
  640 + color: #555;
  641 + }
  642 +
  643 + &.green-tag {
  644 + background: #a7f3d0;
  645 + color: #14532d;
  646 + }
  647 +
  648 + &.blue-tag {
  649 + background: #7dd3fc;
  650 + color: #0c4a6e;
  651 + }
561 } 652 }
562 } 653 }
563 654
564 .item-info { 655 .item-info {
565 display: flex; 656 display: flex;
566 - align-items: center;  
567 - gap: 16rpx; 657 + flex-direction: column;
  658 + gap: 6rpx;
  659 + flex: 1;
  660 + min-width: 0;
568 661
569 .item-label { 662 .item-label {
570 - font-size: 28rpx; 663 + font-size: 30rpx;
571 font-weight: 600; 664 font-weight: 600;
572 color: #1f2937; 665 color: #1f2937;
573 - width: 72rpx; 666 + line-height: 1.3;
574 } 667 }
575 668
576 .item-desc { 669 .item-desc {
577 - font-size: 26rpx;  
578 - color: #6b7280; 670 + font-size: 24rpx;
  671 + color: #9ca3af;
  672 + line-height: 1.4;
579 } 673 }
580 } 674 }
581 } 675 }
@@ -50,7 +50,7 @@ @@ -50,7 +50,7 @@
50 <view class="part-select-wrapper"> 50 <view class="part-select-wrapper">
51 <up-select v-model:current="partId" 51 <up-select v-model:current="partId"
52 :label="partOptions.find((item) => item.id === partId)?.name || '全部'" :options="partOptions" 52 :label="partOptions.find((item) => item.id === partId)?.name || '全部'" :options="partOptions"
53 - @select="onPartChange"></up-select> 53 + maxHeight="400rpx" @select="onPartChange"></up-select>
54 </view> 54 </view>
55 </view> 55 </view>
56 56
@@ -65,22 +65,25 @@ @@ -65,22 +65,25 @@
65 65
66 <!-- 统计图表(根据 showLastPeriod 切换 柱状图 / 折线图) --> 66 <!-- 统计图表(根据 showLastPeriod 切换 柱状图 / 折线图) -->
67 <view class="chart-container"> 67 <view class="chart-container">
68 - <template v-if="!showLastPeriod">  
69 <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries" 68 <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
70 - chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />  
71 - </template>  
72 - <template v-else>  
73 - <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"  
74 - chartType="line" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />  
75 - </template> 69 + :chartType="showLastPeriod ? 'line' : 'column'"
  70 + :reshow="true"
  71 + :extra="{ column: { width: trendChartColumnWidth } }"
  72 + :chartId="'trend-chart'"
  73 + />
76 </view> 74 </view>
77 75
78 <!-- 显示上周/上月数据勾选框 --> 76 <!-- 显示上周/上月数据勾选框 -->
79 - <view class="toggle-checkbox" @click="toggleLastWeek">  
80 - <checkbox :checked="showLastPeriod" color="#10b981" style="transform: scale(0.8);" /> 77 + <view class="toggle-checkbox">
  78 + <up-checkbox-group @change="toggleLastWeek">
  79 + <up-checkbox name="showLastPeriod" :checked="showLastPeriod" activeColor="#10b981" size="20" shape="square">
  80 + <template #label>
81 <text class="toggle-label"> 81 <text class="toggle-label">
82 {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }} 82 {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
83 </text> 83 </text>
  84 + </template>
  85 + </up-checkbox>
  86 + </up-checkbox-group>
84 </view> 87 </view>
85 88
86 <!-- 3. 数据对比区域 --> 89 <!-- 3. 数据对比区域 -->
@@ -138,19 +141,19 @@ @@ -138,19 +141,19 @@
138 141
139 <!-- 容量/组数 切换标签 --> 142 <!-- 容量/组数 切换标签 -->
140 <view class="muscle-tabs"> 143 <view class="muscle-tabs">
141 - <view class="tab-item" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0"> 144 + <view class="tab-item" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">
142 容量 145 容量
143 </view> 146 </view>
144 - <view class="tab-item" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1"> 147 + <view class="tab-item" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">
145 组数 148 组数
146 </view> 149 </view>
147 </view> 150 </view>
148 - <view class="tip">{{ TREND_TIPS[muscleTabIndex] }}</view> 151 + <view class="tip">{{ TREND_TIPS[partTabIndex] }}</view>
149 152
150 <!-- 图表区域 --> 153 <!-- 图表区域 -->
151 <view class="statistical"> 154 <view class="statistical">
152 <LineChart v-if="muscleShow && muscleChartCategories.length" 155 <LineChart v-if="muscleShow && muscleChartCategories.length"
153 - :key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`" 156 + :key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${partTabIndex}`"
154 :categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true" 157 :categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true"
155 :extra="{ column: { width: muscleChartColumnWidth } }" /> 158 :extra="{ column: { width: muscleChartColumnWidth } }" />
156 </view> 159 </view>
@@ -266,7 +269,6 @@ const trainingData = ref({ setCountMap: {}, totalVolumeMap: {} }); @@ -266,7 +269,6 @@ const trainingData = ref({ setCountMap: {}, totalVolumeMap: {} });
266 269
267 // --- 肌肉弹窗 --- 270 // --- 肌肉弹窗 ---
268 const muscleShow = ref(false); 271 const muscleShow = ref(false);
269 -const muscleTabIndex = ref(0);  
270 const currentMuscleId = ref(null); 272 const currentMuscleId = ref(null);
271 const currentMuscleName = ref(''); 273 const currentMuscleName = ref('');
272 const muscleDetailData = ref({ muscleId: 0, dailyStatsList: [] }); 274 const muscleDetailData = ref({ muscleId: 0, dailyStatsList: [] });
@@ -345,11 +347,10 @@ const chartSeries = computed(() => { @@ -345,11 +347,10 @@ const chartSeries = computed(() => {
345 347
346 const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume'; 348 const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
347 349
348 - /** 单位转换:秒→分钟,米→公里,保留一位小数 */ 350 + /** 单位转换:秒→分钟,保留一位小数;距离后端已返回 km,无需转换 */
349 const convertVal = (raw) => { 351 const convertVal = (raw) => {
350 const num = Number(raw) || 0; 352 const num = Number(raw) || 0;
351 if (dataKey === 'duration') return parseFloat((num / 60).toFixed(1)); 353 if (dataKey === 'duration') return parseFloat((num / 60).toFixed(1));
352 - if (dataKey === 'distance') return parseFloat((num / 1000).toFixed(1));  
353 return num; 354 return num;
354 }; 355 };
355 356
@@ -380,15 +381,13 @@ const listData = computed(() => { @@ -380,15 +381,13 @@ const listData = computed(() => {
380 if (!weeklyData.value) return []; 381 if (!weeklyData.value) return [];
381 382
382 const d = weeklyData.value; 383 const d = weeklyData.value;
383 - /** 单位转换:秒→分钟,米→公里,保留一位小数 */ 384 + /** 单位转换:秒→分钟,保留一位小数;距离后端已返回 km,无需转换 */
384 const toMin = (v) => parseFloat(((v ?? 0) / 60).toFixed(1)); 385 const toMin = (v) => parseFloat(((v ?? 0) / 60).toFixed(1));
385 - const toKm = (v) => parseFloat(((v ?? 0) / 1000).toFixed(1));  
386 -  
387 const fields = [ 386 const fields = [
388 { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume }, 387 { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },
389 { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets }, 388 { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },
390 { label: '时长', last: toMin(d.lastWeekTotalDuration), curr: toMin(d.totalDuration) }, 389 { label: '时长', last: toMin(d.lastWeekTotalDuration), curr: toMin(d.totalDuration) },
391 - { label: '距离', last: toKm(d.lastWeekTotalDistance), curr: toKm(d.totalDistance) }, 390 + { label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance },
392 { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, 391 { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount },
393 ]; 392 ];
394 393
@@ -420,10 +419,10 @@ const convertUnit = (val, key) => { @@ -420,10 +419,10 @@ const convertUnit = (val, key) => {
420 419
421 /** 肌肉弹窗图表 X 轴 */ 420 /** 肌肉弹窗图表 X 轴 */
422 const muscleChartCategories = computed(() => { 421 const muscleChartCategories = computed(() => {
  422 + if (!muscleFullDates.value.length) return [];
423 if (MainCurrentDateType.value === 'week') { 423 if (MainCurrentDateType.value === 'week') {
424 return WEEKDAY_LABELS; 424 return WEEKDAY_LABELS;
425 } 425 }
426 - if (!muscleFullDates.value.length) return [];  
427 return muscleFullDates.value.map((d) => dayjs(d).format('M/D')); 426 return muscleFullDates.value.map((d) => dayjs(d).format('M/D'));
428 }); 427 });
429 428
@@ -432,9 +431,9 @@ const muscleSetsArr = computed(() => muscleProcessedData.value.setsArr || []); @@ -432,9 +431,9 @@ const muscleSetsArr = computed(() => muscleProcessedData.value.setsArr || []);
432 431
433 /** 肌肉弹窗图表 Series */ 432 /** 肌肉弹窗图表 Series */
434 const muscleChartSeries = computed(() => { 433 const muscleChartSeries = computed(() => {
435 - const data = muscleTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value;  
436 - const name = muscleTabIndex.value === 0 ? '容量' : '组数';  
437 - const color = muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9'; 434 + const data = partTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value;
  435 + const name = partTabIndex.value === 0 ? '容量' : '组数';
  436 + const color = partTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9';
438 437
439 return [{ name, data: data || [], color }]; 438 return [{ name, data: data || [], color }];
440 }); 439 });
@@ -566,20 +565,14 @@ const onPartChange = (item) => { @@ -566,20 +565,14 @@ const onPartChange = (item) => {
566 loadCategoryData(startDate.value); 565 loadCategoryData(startDate.value);
567 }; 566 };
568 567
569 -/** 时间类型切换(周/月) */  
570 -const handleTypeChange = async (type) => { 568 +/** 时间类型切换(周/月)
  569 + * 只负责更新类型状态,不加载数据。
  570 + * switchDateType 会同步触发 watch → emit dateRangeChange → handleDateChange,
  571 + * handleDateChange 拿到正确的日期后再统一加载数据,避免用旧日期请求接口。
  572 + */
  573 +const handleTypeChange = (type) => {
571 MainCurrentDateType.value = type; 574 MainCurrentDateType.value = type;
572 showLastPeriod.value = false; 575 showLastPeriod.value = false;
573 - loadTrainingData(startDate.value, endDate.value);  
574 -  
575 - if (startDate.value) {  
576 - await loadCategoryData(startDate.value);  
577 - }  
578 -  
579 - // 如果肌肉弹窗打开,同步刷新  
580 - if (muscleShow.value && currentMuscleId.value) {  
581 - await loadMuscleData(currentMuscleId.value, startDate.value);  
582 - }  
583 }; 576 };
584 577
585 /** 日期范围变更 */ 578 /** 日期范围变更 */
@@ -587,7 +580,7 @@ const handleDateChange = async (data) => { @@ -587,7 +580,7 @@ const handleDateChange = async (data) => {
587 startDate.value = data.startDate; 580 startDate.value = data.startDate;
588 endDate.value = data.endDate; 581 endDate.value = data.endDate;
589 await loadTrainingData(startDate.value, endDate.value); 582 await loadTrainingData(startDate.value, endDate.value);
590 - loadCategoryData(startDate.value); 583 + await loadCategoryData(startDate.value);
591 584
592 if (muscleShow.value && currentMuscleId.value) { 585 if (muscleShow.value && currentMuscleId.value) {
593 await loadMuscleData(currentMuscleId.value, startDate.value); 586 await loadMuscleData(currentMuscleId.value, startDate.value);
@@ -598,7 +591,8 @@ const handleDateChange = async (data) => { @@ -598,7 +591,8 @@ const handleDateChange = async (data) => {
598 const openMuscleDetail = async (muscleId) => { 591 const openMuscleDetail = async (muscleId) => {
599 currentMuscleId.value = muscleId; 592 currentMuscleId.value = muscleId;
600 muscleShow.value = true; 593 muscleShow.value = true;
601 - muscleTabIndex.value = 0; 594 + muscleFullDates.value = [];
  595 + muscleProcessedData.value = { volumeArr: [], setsArr: [] };
602 596
603 const muscle = subCategorieList.value.find((item) => item.id === muscleId); 597 const muscle = subCategorieList.value.find((item) => item.id === muscleId);
604 currentMuscleName.value = muscle?.name || ''; 598 currentMuscleName.value = muscle?.name || '';
@@ -619,8 +613,8 @@ const closeMuscleDetail = () => { @@ -619,8 +613,8 @@ const closeMuscleDetail = () => {
619 }; 613 };
620 614
621 /** 切换显示上一周期数据 */ 615 /** 切换显示上一周期数据 */
622 -const toggleLastWeek = () => {  
623 - showLastPeriod.value = !showLastPeriod.value; 616 +const toggleLastWeek = (vals) => {
  617 + showLastPeriod.value = vals.includes('showLastPeriod');
624 if (startDate.value) { 618 if (startDate.value) {
625 loadCategoryData(startDate.value); 619 loadCategoryData(startDate.value);
626 } 620 }
@@ -631,6 +625,7 @@ const toggleLastWeek = () => { @@ -631,6 +625,7 @@ const toggleLastWeek = () => {
631 onMounted(async () => { 625 onMounted(async () => {
632 await loadbodyPartOptions(); 626 await loadbodyPartOptions();
633 await loadGetSubCategorieList(); 627 await loadGetSubCategorieList();
  628 +
634 if (startDate.value && endDate.value) { 629 if (startDate.value && endDate.value) {
635 await loadTrainingData(startDate.value, endDate.value); 630 await loadTrainingData(startDate.value, endDate.value);
636 await loadCategoryData(startDate.value); 631 await loadCategoryData(startDate.value);
@@ -865,8 +860,13 @@ $radius-item: 12rpx; @@ -865,8 +860,13 @@ $radius-item: 12rpx;
865 border-radius: 4rpx; 860 border-radius: 4rpx;
866 margin-right: 8rpx; 861 margin-right: 8rpx;
867 862
868 - &.blue { background-color: $color-blue; }  
869 - &.yellow { background-color: $color-yellow; } 863 + &.blue {
  864 + background-color: $color-blue;
  865 + }
  866 +
  867 + &.yellow {
  868 + background-color: $color-yellow;
  869 + }
870 } 870 }
871 } 871 }
872 } 872 }
@@ -886,8 +886,13 @@ $radius-item: 12rpx; @@ -886,8 +886,13 @@ $radius-item: 12rpx;
886 border-bottom: 1rpx solid #f0f0f0; 886 border-bottom: 1rpx solid #f0f0f0;
887 font-size: 26rpx; 887 font-size: 26rpx;
888 888
889 - &:last-child { border-bottom: none; }  
890 - &:nth-child(odd) { background-color: #f9fafc; } 889 + &:last-child {
  890 + border-bottom: none;
  891 + }
  892 +
  893 + &:nth-child(odd) {
  894 + background-color: #f9fafc;
  895 + }
891 896
892 .label { 897 .label {
893 flex: 1.5; 898 flex: 1.5;
@@ -903,8 +908,13 @@ $radius-item: 12rpx; @@ -903,8 +908,13 @@ $radius-item: 12rpx;
903 &.diff { 908 &.diff {
904 font-weight: 600; 909 font-weight: 600;
905 910
906 - &.positive { color: $color-green; }  
907 - &.negative { color: $color-red; } 911 + &.positive {
  912 + color: $color-green;
  913 + }
  914 +
  915 + &.negative {
  916 + color: $color-red;
  917 + }
908 } 918 }
909 } 919 }
910 } 920 }
@@ -311,6 +311,9 @@ onMounted(() => { @@ -311,6 +311,9 @@ onMounted(() => {
311 width: 180rpx; 311 width: 180rpx;
312 background-color: #ffffff; 312 background-color: #ffffff;
313 border-radius: 16rpx; 313 border-radius: 16rpx;
  314 +
  315 + min-height: 70vh;
  316 +
314 height: 88%; 317 height: 88%;
315 // #ifdef MP-WEIXIN 318 // #ifdef MP-WEIXIN
316 height: 86%; 319 height: 86%;
@@ -348,6 +351,7 @@ onMounted(() => { @@ -348,6 +351,7 @@ onMounted(() => {
348 flex: 1; 351 flex: 1;
349 padding: 0 20rpx; 352 padding: 0 20rpx;
350 box-sizing: border-box; 353 box-sizing: border-box;
  354 + min-height: 70vh;
351 height: 88%; 355 height: 88%;
352 // #ifdef MP-WEIXIN 356 // #ifdef MP-WEIXIN
353 height: 86%; 357 height: 86%;
@@ -36,7 +36,7 @@ @@ -36,7 +36,7 @@
36 @click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')" /> 36 @click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')" />
37 <up-cell title="我的计划" icon="order" :isLink="true" 37 <up-cell title="我的计划" icon="order" :isLink="true"
38 @click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')" /> 38 @click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')" />
39 - <up-cell title="训记使用教程" icon="question-circle" :isLink="true" @click="goTutorial" /> 39 + <up-cell title="训记使用教程" icon="question-circle" :isLink="true" @click="navigateTo('/pages4/pages/xunji/shiyon-jiaochen')" />
40 </up-cell-group> 40 </up-cell-group>
41 </view> 41 </view>
42 </up-popup> 42 </up-popup>
@@ -61,7 +61,7 @@ import TrainingFloating from '@/pages/TrainingFloating.vue'; @@ -61,7 +61,7 @@ import TrainingFloating from '@/pages/TrainingFloating.vue';
61 61
62 const userStore = useUserStore(); 62 const userStore = useUserStore();
63 const drawerShow = ref(false); 63 const drawerShow = ref(false);
64 -const currentTab = ref(3); 64 +const currentTab = ref(1);
65 65
66 const menuButtonHeight = ref(getMenuButtonHeight()); 66 const menuButtonHeight = ref(getMenuButtonHeight());
67 const topSafeArea = ref(getTopSafeArea()); 67 const topSafeArea = ref(getTopSafeArea());
@@ -73,10 +73,7 @@ PageHeight.value = menuButtonHeight.value + topSafeArea.value; @@ -73,10 +73,7 @@ PageHeight.value = menuButtonHeight.value + topSafeArea.value;
73 const userInfo = computed(() => userStore.userInfo || {}); 73 const userInfo = computed(() => userStore.userInfo || {});
74 74
75 onLoad((options) => { 75 onLoad((options) => {
76 - if (!userStore.isLogin) {  
77 - uni.redirectTo({ url: '/pages7/pages/index/login' });  
78 - return;  
79 - } 76 +
80 userStore.getInfo(); 77 userStore.getInfo();
81 if (options && options.currentTab) { 78 if (options && options.currentTab) {
82 currentTab.value = Number(options.currentTab); 79 currentTab.value = Number(options.currentTab);
@@ -103,7 +100,7 @@ const handleTabClick = (index) => { @@ -103,7 +100,7 @@ const handleTabClick = (index) => {
103 100
104 101
105 102
106 -const goTutorial = () => { }; 103 +
107 104
108 defineExpose({ switchTabIndex: handleTabClick }); 105 defineExpose({ switchTabIndex: handleTabClick });
109 </script> 106 </script>
@@ -87,7 +87,7 @@ const sceneList = ref([ @@ -87,7 +87,7 @@ const sceneList = ref([
87 ]) 87 ])
88 88
89 // 缺省图 89 // 缺省图
90 -const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" 90 +const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/健身模板_1784871730506.png"
91 91
92 // ================================== 92 // ==================================
93 // 获取部位分类 93 // 获取部位分类
  1 +<template>
  2 + <view class="tutorial-container">
  3 + <!-- 顶部导航栏 -->
  4 + <u-navbar title="使用教程" :autoBack="true" bgColor="#ffffff"
  5 + titleStyle="color: #303133; font-weight: 600; font-size: 32rpx;" leftIconColor="#303133" :placeholder="true"
  6 + fixed />
  7 +
  8 + <view class="tutorial-body">
  9 + <!-- 1. 顶部应用概览 -->
  10 + <view class="overview-card">
  11 + <view class="overview-icon-wrapper">
  12 + <u-icon name="file-text-fill" color="#ffffff" size="28"></u-icon>
  13 + </view>
  14 + <view class="overview-content">
  15 + <text class="overview-title">什么是训记?</text>
  16 + <text class="overview-desc">
  17 + 训记是一款专业的健身训练记录工具,帮助你科学规划训练计划、精准记录每次训练数据,让健身更高效。
  18 + </text>
  19 + </view>
  20 + </view>
  21 +
  22 + <!-- 2. 核心功能 -->
  23 + <view class="section-container">
  24 + <view class="section-header">
  25 + <view class="header-line"></view>
  26 + <text class="header-title">核心功能</text>
  27 + <view class="header-line"></view>
  28 + </view>
  29 +
  30 + <view class="features-grid">
  31 + <view class="feature-card" v-for="(item, index) in features" :key="index">
  32 + <view class="feature-icon" :style="{ backgroundColor: item.bgColor }">
  33 + <u-icon :name="item.icon" :color="item.iconColor" size="22"></u-icon>
  34 + </view>
  35 + <text class="feature-name">{{ item.name }}</text>
  36 + <text class="feature-desc">{{ item.desc }}</text>
  37 + </view>
  38 + </view>
  39 + </view>
  40 +
  41 + <!-- 3. 使用指南(步骤流程) -->
  42 + <view class="section-container">
  43 + <view class="section-header">
  44 + <view class="header-line"></view>
  45 + <text class="header-title">使用指南</text>
  46 + <view class="header-line"></view>
  47 + </view>
  48 +
  49 + <view class="steps-wrapper">
  50 + <view class="step-card" v-for="(step, sIndex) in steps" :key="sIndex">
  51 + <view class="step-header">
  52 + <view class="step-badge">{{ sIndex + 1 }}</view>
  53 + <view class="step-titles">
  54 + <text class="step-main-title">{{ step.title }}</text>
  55 + <text class="step-sub-title">{{ step.subtitle }}</text>
  56 + </view>
  57 + </view>
  58 +
  59 + <view class="step-details">
  60 + <view class="detail-item" v-for="(detail, dIndex) in step.details" :key="dIndex">
  61 + <view class="dot"></view>
  62 + <text class="detail-text">{{ detail }}</text>
  63 + </view>
  64 + </view>
  65 +
  66 + <!-- 提示块:使用 uview-plus 标准图标 info-circle-fill -->
  67 + <view class="step-tip" v-if="step.tip">
  68 + <u-icon name="info-circle-fill" color="#e6a23c" size="16"></u-icon>
  69 + <text class="tip-text">{{ step.tip }}</text>
  70 + </view>
  71 + </view>
  72 + </view>
  73 + </view>
  74 +
  75 + <!-- 4. 常见问题 (手风琴交互) -->
  76 + <view class="section-container">
  77 + <view class="section-header">
  78 + <view class="header-line"></view>
  79 + <text class="header-title">常见问题</text>
  80 + <view class="header-line"></view>
  81 + </view>
  82 +
  83 + <view class="faq-wrapper">
  84 + <u-collapse accordion :border="false">
  85 + <u-collapse-item v-for="(faq, fIndex) in faqs" :key="fIndex" :name="fIndex">
  86 + <template #title>
  87 + <view class="faq-title-box">
  88 + <view class="faq-tag q-tag">Q</view>
  89 + <text class="faq-question-text">{{ faq.question }}</text>
  90 + </view>
  91 + </template>
  92 + <view class="faq-answer-box">
  93 + <view class="faq-tag a-tag">A</view>
  94 + <text class="faq-answer-text">{{ faq.answer }}</text>
  95 + </view>
  96 + </u-collapse-item>
  97 + </u-collapse>
  98 + </view>
  99 + </view>
  100 +
  101 + <!-- 5. 底部标语 -->
  102 + <view class="footer-section">
  103 + <text class="footer-text">训练贵在坚持,训记陪你一同成长</text>
  104 + </view>
  105 + </view>
  106 + </view>
  107 +</template>
  108 +
  109 +<script setup>
  110 +import { ref } from 'vue'
  111 +
  112 +// 1. 核心功能数据 (均为 uview-plus 官方内置标准图标名)
  113 +const features = ref([
  114 + {
  115 + icon: 'order',
  116 + iconColor: '#ff9900',
  117 + name: '训练计划',
  118 + desc: '创建和管理个性化训练计划',
  119 + bgColor: '#fff7cc'
  120 + },
  121 + {
  122 + icon: 'grid',
  123 + iconColor: '#19be6b',
  124 + name: '动作库',
  125 + desc: '丰富的训练动作库与详情',
  126 + bgColor: '#dbf5e6'
  127 + },
  128 + {
  129 + icon: 'calendar',
  130 + iconColor: '#2979ff',
  131 + name: '训练日历',
  132 + desc: '日历视图记录每日训练',
  133 + bgColor: '#ecf5ff'
  134 + },
  135 + {
  136 + icon: 'list-dot',
  137 + iconColor: '#fa3534',
  138 + name: '数据分析',
  139 + desc: '多维度训练数据统计分析',
  140 + bgColor: '#fef0f0'
  141 + }
  142 +])
  143 +
  144 +// 2. 使用步骤数据
  145 +const steps = ref([
  146 + {
  147 + title: '创建训练计划',
  148 + subtitle: '打造你的专属训练方案',
  149 + details: [
  150 + '进入「我的」页面,点击「训练计划」进入计划管理',
  151 + '选择「新建计划」,可从官方模板快速创建,也可自定义空白计划',
  152 + '设置训练周期、训练日安排,拖拽调整动作顺序',
  153 + '保存后即可在训练日历中按计划执行'
  154 + ],
  155 + tip: '建议新手从官方模板开始,逐步了解后再自定义计划'
  156 + },
  157 + {
  158 + title: '添加训练动作',
  159 + subtitle: '每个动作都值得被记录',
  160 + details: [
  161 + '在训练计划中点击「添加动作」,进入动作库',
  162 + '按部位分类浏览动作,或直接搜索动作名称',
  163 + '点击动作可查看详细说明、肌肉示意图和训练要点',
  164 + '选择动作后设置组数、次数、重量等训练参数'
  165 + ],
  166 + tip: '支持创建超级组,将两个动作组合交替训练,提升效率'
  167 + },
  168 + {
  169 + title: '使用训练日历',
  170 + subtitle: '一目了然的训练记录',
  171 + details: [
  172 + '进入「训练」页面查看训练日历,日历标记已训练日期',
  173 + '点击日期可添加当日训练内容,从计划模板一键导入',
  174 + '已完成的训练会用不同颜色标识,方便回顾',
  175 + '长按训练记录可编辑或删除'
  176 + ],
  177 + tip: '坚持每天打卡,日历会越来越丰富哦'
  178 + },
  179 + {
  180 + title: '记录训练数据',
  181 + subtitle: '精准追踪每次进步',
  182 + details: [
  183 + '训练过程中逐组录入重量、次数、休息时间等数据',
  184 + '内置计时器功能,方便控制组间休息时长',
  185 + '支持查看每个动作的历史训练数据对比',
  186 + '训练完成后自动汇总本次训练总量'
  187 + ],
  188 + tip: '详细记录每次数据,才能看到自己的进步轨迹'
  189 + },
  190 + {
  191 + title: '查看训练分析',
  192 + subtitle: '数据驱动,科学训练',
  193 + details: [
  194 + '在「数据」页面查看训练频率、训练量趋势图表',
  195 + '统计各部位训练次数,发现训练偏重或遗漏',
  196 + '查看个人纪录(PR),见证力量增长',
  197 + '训练日历热力图直观展示训练频率'
  198 + ],
  199 + tip: '定期回顾训练数据,及时调整训练计划方向'
  200 + }
  201 +])
  202 +
  203 +// 3. 常见问题数据
  204 +const faqs = ref([
  205 + {
  206 + question: '训记是免费的吗?',
  207 + answer: '训记基础功能完全免费,包括训练计划创建、动作记录、日历打卡等核心功能均可畅快使用。'
  208 + },
  209 + {
  210 + question: '训练数据会丢失吗?',
  211 + answer: '训练数据会自动保存在云端,只要登录同一账号,更换设备数据也不会丢失。'
  212 + },
  213 + {
  214 + question: '如何从模板快速创建计划?',
  215 + answer: '在「我的模板」或「模板中心」选择适合自己的训练模板,点击「使用模板」即可一键生成训练计划。'
  216 + },
  217 + {
  218 + question: '一个计划可以包含多少动作?',
  219 + answer: '训练计划对动作数量没有硬性限制,你可以根据实际训练需求自由添加和排序。'
  220 + },
  221 + {
  222 + question: '支持自定义动作吗?',
  223 + answer: '支持!你可以创建个人专属动作,自定义动作名称、目标部位和训练参数。'
  224 + }
  225 +])
  226 +</script>
  227 +
  228 +<style lang="scss" scoped>
  229 +.tutorial-container {
  230 + min-height: 100vh;
  231 + background-color: #f6f7f9;
  232 + padding-bottom: calc(40rpx + constant(safe-area-inset-bottom));
  233 + padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
  234 +}
  235 +
  236 +.tutorial-body {
  237 + padding: 24rpx 30rpx;
  238 +}
  239 +
  240 +/* 顶部概览卡片 */
  241 +.overview-card {
  242 + display: flex;
  243 + align-items: center;
  244 + background: linear-gradient(135deg, #2979ff 0%, #609cff 100%);
  245 + border-radius: 20rpx;
  246 + padding: 32rpx;
  247 + margin-bottom: 32rpx;
  248 + box-shadow: 0 8rpx 24rpx rgba(41, 121, 255, 0.15);
  249 +
  250 + .overview-icon-wrapper {
  251 + width: 90rpx;
  252 + height: 90rpx;
  253 + background-color: rgba(255, 255, 255, 0.2);
  254 + border-radius: 50%;
  255 + display: flex;
  256 + align-items: center;
  257 + justify-content: center;
  258 + margin-right: 24rpx;
  259 + flex-shrink: 0;
  260 + }
  261 +
  262 + .overview-content {
  263 + flex: 1;
  264 +
  265 + .overview-title {
  266 + font-size: 32rpx;
  267 + font-weight: 700;
  268 + color: #ffffff;
  269 + margin-bottom: 8rpx;
  270 + display: block;
  271 + }
  272 +
  273 + .overview-desc {
  274 + font-size: 24rpx;
  275 + color: rgba(255, 255, 255, 0.9);
  276 + line-height: 36rpx;
  277 + display: block;
  278 + }
  279 + }
  280 +}
  281 +
  282 +/* 通用分区标题 */
  283 +.section-container {
  284 + margin-bottom: 32rpx;
  285 +}
  286 +
  287 +.section-header {
  288 + display: flex;
  289 + align-items: center;
  290 + justify-content: center;
  291 + margin-bottom: 24rpx;
  292 +
  293 + .header-line {
  294 + width: 40rpx;
  295 + height: 2rpx;
  296 + background-color: #dcdfe6;
  297 + }
  298 +
  299 + .header-title {
  300 + font-size: 28rpx;
  301 + font-weight: 600;
  302 + color: #909399;
  303 + margin: 0 20rpx;
  304 + }
  305 +}
  306 +
  307 +/* 核心功能网格 */
  308 +.features-grid {
  309 + display: grid;
  310 + grid-template-columns: repeat(2, 1fr);
  311 + gap: 20rpx;
  312 +
  313 + .feature-card {
  314 + background-color: #ffffff;
  315 + border-radius: 16rpx;
  316 + padding: 28rpx 20rpx;
  317 + display: flex;
  318 + flex-direction: column;
  319 + align-items: center;
  320 + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
  321 +
  322 + .feature-icon {
  323 + width: 80rpx;
  324 + height: 80rpx;
  325 + border-radius: 20rpx;
  326 + display: flex;
  327 + align-items: center;
  328 + justify-content: center;
  329 + margin-bottom: 16rpx;
  330 + }
  331 +
  332 + .feature-name {
  333 + font-size: 28rpx;
  334 + font-weight: 600;
  335 + color: #303133;
  336 + margin-bottom: 8rpx;
  337 + }
  338 +
  339 + .feature-desc {
  340 + font-size: 22rpx;
  341 + color: #909399;
  342 + text-align: center;
  343 + line-height: 32rpx;
  344 + }
  345 + }
  346 +}
  347 +
  348 +/* 步骤流程 */
  349 +.steps-wrapper {
  350 + .step-card {
  351 + background-color: #ffffff;
  352 + border-radius: 16rpx;
  353 + padding: 28rpx;
  354 + margin-bottom: 20rpx;
  355 + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
  356 +
  357 + .step-header {
  358 + display: flex;
  359 + align-items: center;
  360 + margin-bottom: 20rpx;
  361 +
  362 + .step-badge {
  363 + width: 44rpx;
  364 + height: 44rpx;
  365 + background: #2979ff;
  366 + border-radius: 12rpx;
  367 + display: flex;
  368 + align-items: center;
  369 + justify-content: center;
  370 + font-size: 26rpx;
  371 + font-weight: 700;
  372 + color: #ffffff;
  373 + margin-right: 16rpx;
  374 + flex-shrink: 0;
  375 + }
  376 +
  377 + .step-titles {
  378 + display: flex;
  379 + flex-direction: column;
  380 +
  381 + .step-main-title {
  382 + font-size: 30rpx;
  383 + font-weight: 600;
  384 + color: #303133;
  385 + }
  386 +
  387 + .step-sub-title {
  388 + font-size: 22rpx;
  389 + color: #909399;
  390 + margin-top: 2rpx;
  391 + }
  392 + }
  393 + }
  394 +
  395 + .step-details {
  396 + padding-left: 60rpx;
  397 +
  398 + .detail-item {
  399 + display: flex;
  400 + align-items: flex-start;
  401 + margin-bottom: 12rpx;
  402 +
  403 + .dot {
  404 + width: 10rpx;
  405 + height: 10rpx;
  406 + background-color: #2979ff;
  407 + border-radius: 50%;
  408 + margin-top: 12rpx;
  409 + margin-right: 12rpx;
  410 + flex-shrink: 0;
  411 + }
  412 +
  413 + .detail-text {
  414 + font-size: 25rpx;
  415 + color: #606266;
  416 + line-height: 36rpx;
  417 + flex: 1;
  418 + }
  419 + }
  420 + }
  421 +
  422 + .step-tip {
  423 + display: flex;
  424 + align-items: center;
  425 + background-color: #fdf6ec;
  426 + border-radius: 8rpx;
  427 + padding: 12rpx 16rpx;
  428 + margin-top: 16rpx;
  429 + margin-left: 60rpx;
  430 +
  431 + .tip-text {
  432 + font-size: 22rpx;
  433 + color: #e6a23c;
  434 + margin-left: 10rpx;
  435 + line-height: 32rpx;
  436 + flex: 1;
  437 + }
  438 + }
  439 + }
  440 +}
  441 +
  442 +/* FAQ 折叠面板适配 */
  443 +.faq-wrapper {
  444 + background-color: #ffffff;
  445 + border-radius: 16rpx;
  446 + padding: 10rpx 20rpx;
  447 + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
  448 +
  449 + .faq-title-box {
  450 + display: flex;
  451 + align-items: center;
  452 +
  453 + .faq-question-text {
  454 + font-size: 26rpx;
  455 + font-weight: 600;
  456 + color: #303133;
  457 + }
  458 + }
  459 +
  460 + .faq-answer-box {
  461 + display: flex;
  462 + align-items: flex-start;
  463 + padding: 12rpx 0;
  464 +
  465 + .faq-answer-text {
  466 + font-size: 24rpx;
  467 + color: #606266;
  468 + line-height: 36rpx;
  469 + flex: 1;
  470 + }
  471 + }
  472 +
  473 + .faq-tag {
  474 + width: 36rpx;
  475 + height: 36rpx;
  476 + border-radius: 8rpx;
  477 + display: flex;
  478 + align-items: center;
  479 + justify-content: center;
  480 + font-size: 20rpx;
  481 + font-weight: 700;
  482 + margin-right: 16rpx;
  483 + flex-shrink: 0;
  484 +
  485 + &.q-tag {
  486 + background-color: #2979ff;
  487 + color: #ffffff;
  488 + }
  489 +
  490 + &.a-tag {
  491 + background-color: #e1f3d8;
  492 + color: #67c23a;
  493 + }
  494 + }
  495 +}
  496 +
  497 +/* 底部提示 */
  498 +.footer-section {
  499 + padding: 30rpx 0 10rpx;
  500 + text-align: center;
  501 +
  502 + .footer-text {
  503 + font-size: 22rpx;
  504 + color: #c0c4cc;
  505 + }
  506 +}
  507 +</style>
1 <template> 1 <template>
2 - <view class="paike-wrap">  
3 - <!-- 顶部导航 -->  
4 - <view class="page-header" hover-class="none">  
5 - <view class="nav-bar" :style="{  
6 - paddingTop: menuButtonInfo.top + 'px',  
7 - height: menuButtonInfo.height + 'px'  
8 - }">  
9 - <view class="nav-left" @click="goBack">  
10 - <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons> 2 + <view class="paike-container">
  3 + <!-- 1. 自定义顶部导航栏 (uview-plus) -->
  4 + <u-navbar
  5 + title="排课日历"
  6 + :auto-back="true"
  7 + placeholder
  8 + bg-color="#121212"
  9 + title-style="color: #FFFFFF; font-size: 34rpx; font-weight: 600;"
  10 + left-icon-color="#FFFFFF"
  11 + />
  12 +
  13 + <!-- 2. 星期吸顶头部 -->
  14 + <view class="week-header-sticky">
  15 + <view class="week-grid">
  16 + <view v-for="item in WEEK_TEXTS" :key="item" class="week-cell">
  17 + {{ item }}
11 </view> 18 </view>
12 - <view class="nav-title">计划排课</view>  
13 - <view class="nav-right"></view>  
14 </view> 19 </view>
15 </view> 20 </view>
16 - <!-- 星期头部:一二三四五六日 -->  
17 - <view class="week-head">  
18 - <view v-for="item in weekText" :key="item" class="week-cell">{{ item }}</view> 21 +
  22 + <!-- 3. 加载中状态 -->
  23 + <view v-if="loading" class="state-box">
  24 + <u-loading-icon
  25 + mode="semicircle"
  26 + color="#E9EE50"
  27 + text="正在加载排课数据..."
  28 + text-color="#999999"
  29 + vertical
  30 + />
  31 + </view>
  32 +
  33 + <!-- 4. 日历主体列表 -->
  34 + <view v-else-if="monthList.length > 0" class="calendar-content">
  35 + <view
  36 + v-for="monthItem in monthList"
  37 + :key="`${monthItem.year}-${monthItem.month}`"
  38 + class="month-card"
  39 + >
  40 + <!-- 月份标题 -->
  41 + <view class="month-header">
  42 + <text class="month-title">{{ monthItem.year }}年 {{ monthItem.month }}月</text>
19 </view> 43 </view>
20 - <!-- 日历主体容器 -->  
21 - <view class="calendar-box">  
22 - <!-- 月份标题行 -->  
23 - <view v-for="monthItem in monthList" :key="monthItem.month" class="month-wrap">  
24 - <view class="month-title">{{ monthItem.month }}月</view>  
25 - <!-- 当月日期网格 Grid7列 --> 44 +
  45 + <!-- 日期 7 列网格 -->
26 <view class="date-grid"> 46 <view class="date-grid">
27 - <!-- 前置空白占位格子(当月开头非周一填充空白) -->  
28 - <view v-for="empty in monthItem.emptyCount" :key="'empty' + empty" class="date-cell empty-cell"></view>  
29 - <!-- 有效日期格子 -->  
30 - <view v-for="day in monthItem.dayList" :key="day.day" class="date-cell"  
31 - @click="openTemplateDatil(day.DailyTemplateId, day.year, day.month, day.day)"  
32 - :class="{ hasClass: day.trainName }">  
33 - <!-- 日期数字 -->  
34 - <text class="day-num">{{ day.day }}</text>  
35 - <!-- 黄色排课标签 -->  
36 - <view class="train-label-box">  
37 - <view v-if="day.trainName" class="train-label">{{ day.trainName }}</view>  
38 - <view v-else class="empty-label"></view> 47 + <!-- 开头空白填充格 -->
  48 + <view
  49 + v-for="emptyIdx in monthItem.emptyCount"
  50 + :key="`empty-${emptyIdx}`"
  51 + class="date-cell cell-empty"
  52 + />
  53 +
  54 + <!-- 有效日期格 -->
  55 + <view
  56 + v-for="day in monthItem.dayList"
  57 + :key="`${day.year}-${day.month}-${day.day}`"
  58 + class="date-cell"
  59 + :class="{ 'has-schedule': day.trainName }"
  60 + @tap="handleDateClick(day)"
  61 + >
  62 + <!-- 日期数 -->
  63 + <text class="day-number">{{ day.day }}</text>
  64 +
  65 + <!-- 排课标签/未排课点位 -->
  66 + <view class="schedule-box">
  67 + <view v-if="day.trainName" class="schedule-label">
  68 + {{ day.trainName }}
  69 + </view>
  70 + <view v-else class="schedule-placeholder">
  71 + <view class="dot" />
  72 + </view>
  73 + </view>
39 </view> 74 </view>
40 </view> 75 </view>
41 </view> 76 </view>
  77 +
  78 + <!-- 底部提示 -->
  79 + <view class="bottom-tip">
  80 + <text>最多仅可设置到未来 {{ MONTHS_AHEAD }} 个月哦~</text>
  81 + </view>
42 </view> 82 </view>
  83 +
  84 + <!-- 5. 空数据状态 -->
  85 + <view v-else class="state-box">
  86 + <u-empty mode="data" text="暂无排课计划数据" icon-size="160rpx" />
43 </view> 87 </view>
44 - <!-- 底部提示文案(适配底部安全区) -->  
45 - <view class="bottom-tip">最多仅可设置到未来2个月哦~</view>  
46 </view> 88 </view>
47 </template> 89 </template>
48 90
49 <script setup> 91 <script setup>
50 -import { ref, onMounted } from 'vue'  
51 -import QueryPlanApi from '@/sheep/api/plan/queryplan';  
52 -import { onLoad } from '@dcloudio/uni-app';  
53 -// 顶部星期文案  
54 -const weekText = ['一', '二', '三', '四', '五', '六', '日'] 92 +import { ref, onMounted, onUnmounted } from 'vue'
  93 +import { onLoad } from '@dcloudio/uni-app'
  94 +import QueryPlanApi from '@/sheep/api/plan/queryplan'
55 95
56 -// 排课接口信息  
57 -const resData = ref({}) 96 +// ==================== 常量定义 ====================
  97 +const WEEK_TEXTS = ['一', '二', '三', '四', '五', '六', '日']
  98 +const MONTHS_AHEAD = 2 // 最多展示未来月份数(当前月 + 2个月)
58 99
59 -// 最终渲染日历数据 100 +// ==================== 响应式状态 ====================
  101 +const planId = ref(0)
  102 +const loading = ref(true)
60 const monthList = ref([]) 103 const monthList = ref([])
61 104
  105 +// ==================== 生命周期 ====================
  106 +onLoad((options) => {
  107 + planId.value = Number(options?.planid) || 0
  108 +})
62 109
63 -const planId = ref(0) 110 +onMounted(() => {
  111 + fetchPlanCalendar()
  112 + uni.$on('calendarDataRefresh', fetchPlanCalendar)
  113 +})
64 114
65 -onLoad((options) => {  
66 - // 接收从详情页传过来的 planid  
67 - planId.value = Number(options.planid) || 0  
68 - console.log('收到计划ID:', planId.value, typeof planId.value) 115 +onUnmounted(() => {
  116 + uni.$off('calendarDataRefresh', fetchPlanCalendar)
69 }) 117 })
70 118
71 -// 格式化日期、后端数据转日历  
72 -const formatCalendar = () => { 119 +// ==================== 工具函数(数据解耦) ====================
73 120
74 - console.log('进入格式化,resData全部数据:', resData.value)  
75 - // 没有data直接退出  
76 - if (!resData.value?.data || !Array.isArray(resData.value.data)) {  
77 - console.log('data不存在,终止格式化')  
78 - monthList.value = []  
79 - return 121 +/**
  122 + * 根据基础日期与月份偏移量,计算目标的年、月
  123 + */
  124 +const getTargetYearMonth = (baseDate, offsetMonth) => {
  125 + const d = new Date(baseDate.getFullYear(), baseDate.getMonth() + offsetMonth, 1)
  126 + return {
  127 + year: d.getFullYear(),
  128 + month: d.getMonth() + 1,
80 } 129 }
81 - const now = new Date()  
82 - const currentYear = now.getFullYear()  
83 - const currentMonth = now.getMonth() + 1  
84 - const today = now.getDate()  
85 -  
86 - const calcMonthArr = [currentMonth, currentMonth + 1, currentMonth + 2] 130 +}
87 131
88 - const groupObj = {} 132 +/**
  133 + * 计算某月某日是周几,并转换为“以周一为每周第一天”的前置空白格数量
  134 + */
  135 +const calcEmptySlots = (year, month, day = 1) => {
  136 + const dayOfWeek = new Date(year, month - 1, day).getDay()
  137 + const isoWeekDay = dayOfWeek === 0 ? 7 : dayOfWeek // 周日转为7
  138 + return isoWeekDay - 1
  139 +}
89 140
90 - resData.value.data.forEach(item => {  
91 - // trainingDate 是 [年, 月, 日] 数组,直接解构  
92 - // const [y, m, d] = item.trainingDate  
93 - // const name = item.templates?.[0]?.templateName || '' 141 +/**
  142 + * 将后端排课接口数组构建成映射 Map,Key: `${year}-${month}`, Value: { [day]: { trainName, DailyTemplateId } }
  143 + */
  144 +const buildTrainMap = (rawList = []) => {
  145 + const map = {}
  146 + if (!Array.isArray(rawList)) return map
94 147
95 - // const key = `${y}-${m}`  
96 - // if (!groupObj[key]) groupObj[key] = {}  
97 - // groupObj[key][d] = name 148 + rawList.forEach((item) => {
  149 + if (!item?.trainingDate || !Array.isArray(item.trainingDate)) return
98 const [y, m, d] = item.trainingDate 150 const [y, m, d] = item.trainingDate
99 const template = item.templates?.[0] 151 const template = item.templates?.[0]
100 - const name = template?.templateName || ''  
101 - const DailyTemplateId = template?.id || null  
102 -  
103 const key = `${y}-${m}` 152 const key = `${y}-${m}`
104 - if (!groupObj[key]) groupObj[key] = {}  
105 - groupObj[key][d] = {  
106 - trainName: name,  
107 - DailyTemplateId: DailyTemplateId 153 +
  154 + if (!map[key]) map[key] = {}
  155 + map[key][d] = {
  156 + trainName: template?.templateName || '',
  157 + DailyTemplateId: template?.id || null,
108 } 158 }
109 }) 159 })
  160 + return map
  161 +}
110 162
  163 +/**
  164 + * 生成多月份日历完整渲染结构
  165 + */
  166 +const generateCalendarList = (apiData) => {
  167 + const now = new Date()
  168 + const today = now.getDate()
  169 + const trainMap = buildTrainMap(apiData)
111 const result = [] 170 const result = []
112 - calcMonthArr.forEach((month, idx) => {  
113 - const year = currentYear + Math.floor((month - 1) / 12)  
114 - const realMonth = month > 12 ? month - 12 : month  
115 - const totalDay = new Date(year, realMonth, 0).getDate()  
116 - let firstWeek = new Date(`${year}-${realMonth}-01`).getDay()  
117 - firstWeek = firstWeek === 0 ? 7 : firstWeek  
118 - let emptyCount = firstWeek - 1  
119 171
120 - const dayList = []  
121 - const startDay = idx === 0 ? today : 1  
122 -  
123 - for (let d = startDay; d <= totalDay; d++) {  
124 - // const key = `${year}-${realMonth}`  
125 - // const trainName = groupObj[key]?.[d] || ''  
126 -  
127 - // dayList.push({ day: d, trainName })  
128 - const key = `${year}-${realMonth}`  
129 - const trainInfo = groupObj[key]?.[d] || {} 172 + for (let i = 0; i <= MONTHS_AHEAD; i++) {
  173 + const { year, month } = getTargetYearMonth(now, i)
  174 + const isCurrentMonth = i === 0
  175 + const startDay = isCurrentMonth ? today : 1
  176 + const totalDaysInMonth = new Date(year, month, 0).getDate()
  177 + const mapKey = `${year}-${month}`
130 178
  179 + const dayList = []
  180 + for (let d = startDay; d <= totalDaysInMonth; d++) {
  181 + const scheduleInfo = trainMap[mapKey]?.[d] || {}
131 dayList.push({ 182 dayList.push({
132 - year: year, // 年  
133 - month: realMonth, // 月 183 + year,
  184 + month,
134 day: d, 185 day: d,
135 - trainName: trainInfo.trainName || '',  
136 - // templateId: trainInfo.templateId || null  
137 - DailyTemplateId: trainInfo.DailyTemplateId || null 186 + trainName: scheduleInfo.trainName || '',
  187 + DailyTemplateId: scheduleInfo.DailyTemplateId || null,
138 }) 188 })
139 } 189 }
140 190
141 - if (idx === 0) {  
142 - const firstDayWeek = new Date(`${year}-${realMonth}-${startDay}`).getDay()  
143 - const currentWeek = firstDayWeek === 0 ? 7 : firstDayWeek  
144 - emptyCount = currentWeek - 1  
145 - }  
146 -  
147 result.push({ 191 result.push({
148 - month: realMonth,  
149 - emptyCount,  
150 - dayList  
151 - }) 192 + year,
  193 + month,
  194 + emptyCount: calcEmptySlots(year, month, startDay),
  195 + dayList,
152 }) 196 })
  197 + }
153 198
154 - monthList.value = result 199 + return result
155 } 200 }
156 201
157 -// 返回上一页  
158 -const goBack = () => uni.navigateBack()  
159 -  
160 -// 胶囊导航信息  
161 -const menuButtonInfo = ref({  
162 - top: 44,  
163 - height: 32  
164 -}); 202 +// ==================== 接口交互 & 事件处理 ====================
165 203
  204 +/** 加载排课数据 */
  205 +const fetchPlanCalendar = async () => {
  206 + if (!planId.value) {
  207 + loading.value = false
  208 + return
  209 + }
166 210
167 -// 获取排课日历数据  
168 -const loadPlanCalendar = async () => {  
169 - if (!planId.value) return 211 + loading.value = true
170 try { 212 try {
171 - const apiRes = await QueryPlanApi.getPlanArrangeCalendar(planId.value)  
172 - console.log('接口返回的计划日历数据:', apiRes.value);  
173 - resData.value = apiRes  
174 - // 渲染日历  
175 - formatCalendar() 213 + const res = await QueryPlanApi.getPlanArrangeCalendar(planId.value)
  214 + // 根据后端统一响应结构兜底处理
  215 + const data = res?.data || res
  216 + monthList.value = generateCalendarList(Array.isArray(data) ? data : [])
176 } catch (err) { 217 } catch (err) {
177 - console.error('日历接口报错', err) 218 + console.error('[PlanCalendar] 获取排课日历失败:', err)
  219 + uni.showToast({ title: '加载排课失败', icon: 'none' })
  220 + } finally {
  221 + loading.value = false
178 } 222 }
179 } 223 }
180 224
181 -//查看模板详情  
182 -const openTemplateDatil = (DailyTemplateId, year, month, day) => {  
183 - console.log('每日模板ID=', DailyTemplateId);  
184 - console.log('完整日期=', year, month, day);  
185 - const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` 225 +/** 日期卡片点击事件 */
  226 +const handleDateClick = (dayItem) => {
  227 + const { DailyTemplateId, year, month, day } = dayItem
186 if (!DailyTemplateId) return 228 if (!DailyTemplateId) return
187 - uni.navigateTo({  
188 - url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${DailyTemplateId}&date=${dateStr}&isOffice=false&isMyPlan=true&isDailytemplateId=true`,  
189 - });  
190 -};  
191 229
192 -onMounted(async () => {  
193 - // #ifdef MP-WEIXIN  
194 - try {  
195 - const rect = uni.getMenuButtonBoundingClientRect();  
196 - if (rect) {  
197 - menuButtonInfo.value = {  
198 - top: rect.top,  
199 - height: rect.height  
200 - };  
201 - }  
202 - } catch (e) { }  
203 - // #endif  
204 - // #ifndef MP-WEIXIN  
205 - try {  
206 - const systemInfo = uni.getSystemInfoSync();  
207 - const statusBarHeight = systemInfo.statusBarHeight || 20;  
208 - menuButtonInfo.value = {  
209 - top: statusBarHeight,  
210 - height: 44  
211 - };  
212 - } catch (e) { }  
213 - // #endif  
214 - // 加载排课信息  
215 - loadPlanCalendar()  
216 - // formatCalendar()  
217 -}); 230 + const formattedMonth = String(month).padStart(2, '0')
  231 + const formattedDay = String(day).padStart(2, '0')
  232 + const dateStr = `${year}-${formattedMonth}-${formattedDay}`
218 233
  234 + uni.navigateTo({
  235 + url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${DailyTemplateId}&date=${dateStr}&isOffice=false&isMyPlan=true&isDailytemplateId=true`,
  236 + })
  237 +}
219 </script> 238 </script>
220 239
221 <style scoped lang="scss"> 240 <style scoped lang="scss">
222 -$bg-color: #121212;  
223 -$text-white: #fff;  
224 -$text-gray: #2f2f2f;  
225 -$train-yellow: #e9ee50;  
226 -$cell-h: 110rpx;  
227 -  
228 -.paike-wrap {  
229 - background: $bg-color;  
230 - min-height: 100vh;  
231 - padding-bottom: env(safe-area-inset-bottom);  
232 - overflow: hidden;  
233 -} 241 +// ==================== 主题色系与变量 ====================
  242 +$bg-main: #121212;
  243 +$bg-card: #1e1e1e;
  244 +$bg-cell: #262626;
234 245
235 -.page-header {  
236 - width: 100%;  
237 - flex-shrink: 0;  
238 - background-color: $bg-color;  
239 -} 246 +$text-primary: #ffffff;
  247 +$text-secondary: #8c8c8c;
240 248
241 -.nav-bar {  
242 - display: flex;  
243 - align-items: center;  
244 - justify-content: space-between;  
245 - background-color: $bg-color;  
246 - padding-left: 16rpx;  
247 - padding-right: 16rpx;  
248 - box-sizing: content-box;  
249 -} 249 +$theme-yellow: #e9ee50;
  250 +$theme-yellow-light: rgba(233, 238, 80, 0.15);
  251 +$theme-yellow-border: rgba(233, 238, 80, 0.35);
250 252
251 -.nav-left {  
252 - width: 60rpx;  
253 - display: flex;  
254 - align-items: center;  
255 - flex-shrink: 0;  
256 -} 253 +$cell-height: 116rpx;
257 254
258 -.nav-title {  
259 - flex: 1;  
260 - text-align: center;  
261 - font-size: 36rpx;  
262 - font-weight: bold;  
263 - color: $text-white; 255 +// ==================== 容器根样式 ====================
  256 +.paike-container {
  257 + min-height: 100vh;
  258 + background-color: $bg-main;
  259 + padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
  260 + box-sizing: border-box;
264 } 261 }
265 262
266 -.nav-right {  
267 - width: 60rpx;  
268 - flex-shrink: 0;  
269 -} 263 +// ==================== 星期头吸顶 ====================
  264 +.week-header-sticky {
  265 + position: sticky;
  266 + /* #ifdef H5 */
  267 + top: 44px;
  268 + /* #endif */
  269 + /* #ifndef H5 */
  270 + top: 0;
  271 + /* #endif */
  272 + z-index: 10;
  273 + background-color: $bg-main;
  274 + padding: 20rpx 16rpx 12rpx;
  275 + border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
270 276
271 -.week-head {  
272 - margin-top: 10rpx; 277 + .week-grid {
273 display: grid; 278 display: grid;
274 grid-template-columns: repeat(7, 1fr); 279 grid-template-columns: repeat(7, 1fr);
275 - margin: 20rpx 0;  
276 280
277 .week-cell { 281 .week-cell {
278 text-align: center; 282 text-align: center;
279 - font-size: 25rpx;  
280 - color: $text-white; 283 + font-size: 24rpx;
  284 + font-weight: 500;
  285 + color: $text-secondary;
  286 + }
281 } 287 }
282 } 288 }
283 289
284 -.month-wrap {  
285 - margin-bottom: 30rpx;  
286 - background-color: #121212; 290 +// ==================== 状态容器 (Loading/Empty) ====================
  291 +.state-box {
  292 + display: flex;
  293 + align-items: center;
  294 + justify-content: center;
  295 + min-height: 500rpx;
  296 +}
  297 +
  298 +// ==================== 日历主体 ====================
  299 +.calendar-content {
  300 + padding: 20rpx 16rpx 0;
  301 +}
  302 +
  303 +.month-card {
  304 + margin-bottom: 32rpx;
  305 + background: $bg-card;
  306 + border-radius: 24rpx;
  307 + padding: 20rpx 12rpx 16rpx;
  308 + border: 1rpx solid rgba(255, 255, 255, 0.05);
  309 + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
287 310
288 - .month-title {  
289 - font-size: 25rpx;  
290 - color: $text-white;  
291 - padding: 15rpx 10rpx; 311 + .month-header {
292 text-align: center; 312 text-align: center;
  313 + padding-bottom: 20rpx;
  314 +
  315 + .month-title {
  316 + font-size: 30rpx;
  317 + font-weight: 600;
  318 + color: $text-primary;
  319 + letter-spacing: 1rpx;
  320 + }
293 } 321 }
294 322
295 .date-grid { 323 .date-grid {
296 display: grid; 324 display: grid;
297 grid-template-columns: repeat(7, 1fr); 325 grid-template-columns: repeat(7, 1fr);
298 - gap: 1rpx; 326 + gap: 8rpx;
299 } 327 }
300 328
301 .date-cell { 329 .date-cell {
302 - height: $cell-h;  
303 - background: $bg-color;  
304 - border-radius: 8rpx;  
305 - padding: 8rpx; 330 + height: $cell-height;
  331 + background-color: $bg-cell;
  332 + border-radius: 12rpx;
  333 + padding: 8rpx 6rpx;
  334 + box-sizing: border-box;
306 display: flex; 335 display: flex;
307 flex-direction: column; 336 flex-direction: column;
308 justify-content: space-between; 337 justify-content: space-between;
  338 + align-items: center;
  339 + transition: all 0.2s ease;
309 340
310 - .day-num {  
311 - font-size: 25rpx;  
312 - color: $text-white;  
313 - text-align: center;  
314 - display: block;  
315 - width: 100%; 341 + /* 点击效果 */
  342 + &:active {
  343 + transform: scale(0.95);
  344 + opacity: 0.8;
316 } 345 }
317 346
318 - .train-label-box {  
319 - min-height: 56rpx;  
320 - width: 100%; 347 + /* 空白填补格 */
  348 + &.cell-empty {
  349 + background-color: transparent !important;
  350 + pointer-events: none;
321 } 351 }
322 352
323 - .train-label {  
324 - background: $train-yellow;  
325 - color: #000;  
326 - font-size: 22rpx;  
327 - padding: 4rpx 3rpx;  
328 - border-radius: 2rpx;  
329 - line-height: 1.3;  
330 - overflow: hidden;  
331 - text-overflow: ellipsis;  
332 - display: -webkit-box;  
333 - -webkit-line-clamp: 2;  
334 - -webkit-box-orient: vertical; 353 + /* 有排课状态 */
  354 + &.has-schedule {
  355 + background-color: $theme-yellow-light;
  356 + border: 1rpx solid $theme-yellow-border;
  357 +
  358 + .day-number {
  359 + color: $theme-yellow;
  360 + font-weight: 700;
  361 + }
  362 + }
  363 +
  364 + .day-number {
  365 + font-size: 26rpx;
  366 + color: $text-primary;
  367 + font-weight: 500;
  368 + line-height: 1;
  369 + }
  370 +
  371 + /* 排课内容展示区 */
  372 + .schedule-box {
  373 + width: 100%;
335 height: 56rpx; 374 height: 56rpx;
  375 + display: flex;
  376 + align-items: center;
  377 + justify-content: center;
  378 +
  379 + .schedule-label {
  380 + width: 100%;
  381 + height: 100%;
  382 + background-color: $theme-yellow;
  383 + color: #000000;
  384 + font-size: 20rpx;
  385 + font-weight: 600;
  386 + border-radius: 6rpx;
  387 + padding: 4rpx;
  388 + box-sizing: border-box;
336 text-align: center; 389 text-align: center;
  390 + line-height: 1.25;
337 391
  392 + /* 超出两行省略 */
  393 + display: -webkit-box;
  394 + -webkit-line-clamp: 2;
  395 + line-clamp: 2;
  396 + -webkit-box-orient: vertical;
  397 + overflow: hidden;
  398 + word-break: break-all;
338 } 399 }
339 400
340 - .empty-label { 401 + /* 无排课时的精致小点,替代原本突兀的大灰块 */
  402 + .schedule-placeholder {
  403 + display: flex;
  404 + align-items: center;
  405 + justify-content: center;
341 width: 100%; 406 width: 100%;
342 - min-height: 56rpx;  
343 - background: $text-gray;  
344 - border-radius: 2rpx; 407 + height: 100%;
  408 +
  409 + .dot {
  410 + width: 8rpx;
  411 + height: 8rpx;
  412 + border-radius: 50%;
  413 + background-color: rgba(255, 255, 255, 0.15);
  414 + }
345 } 415 }
346 } 416 }
347 -  
348 - .empty-cell {  
349 - background: transparent !important;  
350 } 417 }
351 } 418 }
352 419
  420 +// ==================== 底部提示 ====================
353 .bottom-tip { 421 .bottom-tip {
354 text-align: center; 422 text-align: center;
355 - font-size: 30rpx;  
356 - color: $text-white;  
357 - margin-top: 60rpx;  
358 -  
359 - /* #ifdef H5 */  
360 - margin-bottom: 20px;  
361 - /* #endif */ 423 + padding: 24rpx 0 10rpx;
362 424
  425 + text {
  426 + font-size: 24rpx;
  427 + color: $text-secondary;
  428 + }
363 } 429 }
364 </style> 430 </style>
@@ -4,11 +4,13 @@ @@ -4,11 +4,13 @@
4 <up-navbar placeholder bgColor="transparent" /> 4 <up-navbar placeholder bgColor="transparent" />
5 <!-- #endif --> 5 <!-- #endif -->
6 <view class="status-header"> 6 <view class="status-header">
7 - <view class="timer-box" @click="show = true"> 7 + <view class="timer-box" @click="show = true" v-if="!isFutureTemplate">
8 <text class="time">{{ formattedTime.substring(3) }}</text> 8 <text class="time">{{ formattedTime.substring(3) }}</text>
9 <view class="triangle-icon"></view> 9 <view class="triangle-icon"></view>
10 </view> 10 </view>
11 - <view class="finish-btn" :class="{ disabled: !allUnitsChecked }" @click="save">完成</view> 11 + <view v-else class="timer-box-placeholder" />
  12 + <view class="finish-btn" :class="{ disabled: isFutureTemplate ? !hasAnyAction : !allUnitsChecked }" @click="save">
  13 + {{ isFutureTemplate ? '保存模板' : '完成' }}</view>
12 </view> 14 </view>
13 15
14 <view class="action-list"> 16 <view class="action-list">
@@ -25,18 +27,10 @@ @@ -25,18 +27,10 @@
25 <!-- 3. 模板训练 type=3 循环 --> 27 <!-- 3. 模板训练 type=3 循环 -->
26 <template v-if="actionDetail?.units?.length"> 28 <template v-if="actionDetail?.units?.length">
27 <view v-for="(unit, index) in actionDetail.units" :key="unit.unitId"> 29 <view v-for="(unit, index) in actionDetail.units" :key="unit.unitId">
28 - <dongzuo  
29 - :id="unit.unitId"  
30 - :type="unit.unitType"  
31 - :actionDetail="trainingStore.convertUnitToActionDetail(unit)"  
32 - :ref="(el) => setActionRef(el, index)"  
33 - :unitIndex="index"  
34 - :is-daily-templates="false"  
35 - :isUnitActive="unitActiveStates[index]"  
36 - @startRestTimer="startRestCountdown"  
37 - @cancelRestTimer="onCancelRestTimer"  
38 - @updateUnitActive="(val) => onUnitActiveChange(index, val)"  
39 - /> 30 + <dongzuo :id="unit.unitId" :type="unit.unitType" :actionDetail="trainingStore.convertUnitToActionDetail(unit)"
  31 + :ref="(el) => setActionRef(el, index)" :unitIndex="index" :is-daily-templates="false"
  32 + :isUnitActive="unitActiveStates[index]" @startRestTimer="startRestCountdown"
  33 + @cancelRestTimer="onCancelRestTimer" @updateUnitActive="(val) => onUnitActiveChange(index, val)" />
40 </view> 34 </view>
41 </template> 35 </template>
42 36
@@ -47,7 +41,7 @@ @@ -47,7 +41,7 @@
47 </view> 41 </view>
48 </view> 42 </view>
49 43
50 - <view v-if="!actionDetail?.units?.length" class="tip-bubble"> 44 + <view v-if="!actionDetail?.units?.length && !addActionsShow" class="tip-bubble">
51 <text>挑选想要的动作开始训练吧</text> 45 <text>挑选想要的动作开始训练吧</text>
52 </view> 46 </view>
53 47
@@ -75,25 +69,14 @@ @@ -75,25 +69,14 @@
75 </view> 69 </view>
76 </view> 70 </view>
77 <!-- 组间休息倒计时 --> 71 <!-- 组间休息倒计时 -->
78 - <view  
79 - class="rest-countdown"  
80 - v-if="restCountdownSeconds > 0"  
81 - @click="clearRestCountdown()"  
82 - > 72 + <view class="rest-countdown" v-if="restCountdownSeconds > 0" @click="clearRestCountdown()">
83 <view class="countdown-circle"> 73 <view class="countdown-circle">
84 <!-- H5 端:使用 SVG 进度环 --> 74 <!-- H5 端:使用 SVG 进度环 -->
85 <!-- #ifdef H5 --> 75 <!-- #ifdef H5 -->
86 <svg class="ring-svg" viewBox="0 0 100 100"> 76 <svg class="ring-svg" viewBox="0 0 100 100">
87 <circle class="ring-bg" cx="50" cy="50" r="44" /> 77 <circle class="ring-bg" cx="50" cy="50" r="44" />
88 - <circle  
89 - class="ring-progress"  
90 - cx="50"  
91 - cy="50"  
92 - r="44"  
93 - :stroke-dasharray="CIRCUMFERENCE"  
94 - :stroke-dashoffset="CIRCUMFERENCE * (1 - restProgress)"  
95 - transform="rotate(-90 50 50)"  
96 - /> 78 + <circle class="ring-progress" cx="50" cy="50" r="44" :stroke-dasharray="CIRCUMFERENCE"
  79 + :stroke-dashoffset="CIRCUMFERENCE * (1 - restProgress)" transform="rotate(-90 50 50)" />
97 </svg> 80 </svg>
98 <!-- #endif --> 81 <!-- #endif -->
99 <!-- 微信小程序端:不显示进度环 --> 82 <!-- 微信小程序端:不显示进度环 -->
@@ -104,14 +87,8 @@ @@ -104,14 +87,8 @@
104 </view> 87 </view>
105 </view> 88 </view>
106 <!-- 设置训练时间弹窗 --> 89 <!-- 设置训练时间弹窗 -->
107 - <up-popup  
108 - :show="show"  
109 - mode="top"  
110 - @close="show = false"  
111 - :safeAreaInsetBottom="false"  
112 - safeAreaInsetTop  
113 - bgColor="#2c2c2e"  
114 - > 90 + <up-popup :show="show" mode="top" @close="show = false" :safeAreaInsetBottom="false" safeAreaInsetTop
  91 + bgColor="#2c2c2e">
115 <view class="time-pop-container"> 92 <view class="time-pop-container">
116 <view class="pop-content"> 93 <view class="pop-content">
117 <view class="pop-title">设置训练时间</view> 94 <view class="pop-title">设置训练时间</view>
@@ -144,11 +121,7 @@ @@ -144,11 +121,7 @@
144 </view> 121 </view>
145 <view class="timer-right"> 122 <view class="timer-right">
146 <view class="action-btn gray-btn" v-if="isPause" @click="resetTimer">重置</view> 123 <view class="action-btn gray-btn" v-if="isPause" @click="resetTimer">重置</view>
147 - <view  
148 - class="action-btn green-btn"  
149 - :class="{ active: !isPause, bgactve: !isPause }"  
150 - @click="toggleTimer"  
151 - > 124 + <view class="action-btn green-btn" :class="{ active: !isPause, bgactve: !isPause }" @click="toggleTimer">
152 {{ isPause ? '继续计时' : '暂停计时' }} 125 {{ isPause ? '继续计时' : '暂停计时' }}
153 </view> 126 </view>
154 </view> 127 </view>
@@ -161,79 +134,39 @@ @@ -161,79 +134,39 @@
161 </up-popup> 134 </up-popup>
162 135
163 <!-- 设置训练日期 --> 136 <!-- 设置训练日期 -->
164 - <up-datetime-picker  
165 - :show="datePickerShow"  
166 - v-model="dateValue"  
167 - mode="date"  
168 - closeOnClickOverlay  
169 - @confirm="confirmDate"  
170 - @cancel="datePickerShow = false"  
171 - @close="datePickerShow = false"  
172 - > 137 + <up-datetime-picker :show="datePickerShow" v-model="dateValue" mode="date" closeOnClickOverlay
  138 + @confirm="confirmDate" @cancel="datePickerShow = false" @close="datePickerShow = false">
173 </up-datetime-picker> 139 </up-datetime-picker>
174 140
175 <!-- 设置起始时间 --> 141 <!-- 设置起始时间 -->
176 - <up-datetime-picker  
177 - :show="timePickerShow"  
178 - v-model="timeValue"  
179 - mode="time"  
180 - closeOnClickOverlay  
181 - @confirm="confirmTime"  
182 - @cancel="timePickerShow = false"  
183 - @close="timePickerShow = false"  
184 - > 142 + <up-datetime-picker :show="timePickerShow" v-model="timeValue" mode="time" closeOnClickOverlay
  143 + @confirm="confirmTime" @cancel="timePickerShow = false" @close="timePickerShow = false">
185 </up-datetime-picker> 144 </up-datetime-picker>
186 145
187 <!-- 设置训练时长计时 --> 146 <!-- 设置训练时长计时 -->
188 - <up-picker  
189 - :show="showPicker"  
190 - :columns="timeColumns"  
191 - :defaultIndex="defaultTimeIndex"  
192 - title="修改训练时长"  
193 - @confirm="onTimeConfirm"  
194 - @cancel="showPicker = false"  
195 - closeOnClickOverlay  
196 - > 147 + <up-picker :show="showPicker" :columns="timeColumns" :defaultIndex="defaultTimeIndex" title="修改训练时长"
  148 + @confirm="onTimeConfirm" @cancel="showPicker = false" closeOnClickOverlay>
197 </up-picker> 149 </up-picker>
198 150
199 <!-- 写总结弹窗 --> 151 <!-- 写总结弹窗 -->
200 - <up-popup  
201 - :show="summaryShow"  
202 - @close="summaryShow = false"  
203 - mode="bottom"  
204 - :safeAreaInsetBottom="false"  
205 - > 152 + <up-popup :show="summaryShow" @close="summaryShow = false" mode="bottom" :safeAreaInsetBottom="false">
206 <view class="summaryPopup"> 153 <view class="summaryPopup">
207 <view class="popup-header"> 154 <view class="popup-header">
208 <text class="popup-title">本次训练总结</text> 155 <text class="popup-title">本次训练总结</text>
209 <view class="save-btn" @click="saveSummary">保存</view> 156 <view class="save-btn" @click="saveSummary">保存</view>
210 </view> 157 </view>
211 - <textarea  
212 - class="summary-textarea"  
213 - v-model="summaryContent"  
214 - placeholder="记录本次训练感悟"  
215 - /> 158 + <textarea class="summary-textarea" v-model="summaryContent" placeholder="记录本次训练感悟" />
216 </view> 159 </view>
217 </up-popup> 160 </up-popup>
218 <!-- 命名弹窗 --> 161 <!-- 命名弹窗 -->
219 - <up-popup  
220 - :show="inputNameShow"  
221 - @close="inputNameShow = false"  
222 - mode="bottom"  
223 - :safeAreaInsetBottom="false"  
224 - > 162 + <up-popup :show="inputNameShow" @close="inputNameShow = false" mode="bottom" :safeAreaInsetBottom="false">
225 <view class="namePopup"> 163 <view class="namePopup">
226 <view class="popup-header"> 164 <view class="popup-header">
227 <text class="popup-title">训练名称</text> 165 <text class="popup-title">训练名称</text>
228 <view class="save-btn" @click="saveTrainingName">保存</view> 166 <view class="save-btn" @click="saveTrainingName">保存</view>
229 </view> 167 </view>
230 - <textarea  
231 - v-model="trainingStore.trainingName"  
232 - class="name-input"  
233 - placeholder="请输入训练名称"  
234 - placeholder-color="#8e8e93"  
235 - color="#fff"  
236 - /> 168 + <textarea v-model="trainingStore.trainingName" class="name-input" placeholder="请输入训练名称"
  169 + placeholder-color="#8e8e93" color="#fff" />
237 </view> 170 </view>
238 </up-popup> 171 </up-popup>
239 <!-- 动作新增组件 --> 172 <!-- 动作新增组件 -->
@@ -243,75 +176,86 @@ @@ -243,75 +176,86 @@
243 </template> 176 </template>
244 177
245 <script setup> 178 <script setup>
246 - import { ref, computed, nextTick, onUnmounted } from 'vue';  
247 - import dayjs from 'dayjs';  
248 - import { onLoad } from '@dcloudio/uni-app';  
249 - import TrainingApi from '@/sheep/api/Training/traininghistory';  
250 - import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';  
251 - import { useTrainingStore } from '@/sheep/store/trainingStore';  
252 - import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue';  
253 - import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue';  
254 - import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';  
255 - import { trainingSnapshot } from '@/sheep/store/trainingSnapshot';  
256 -  
257 - const trainingStore = useTrainingStore();  
258 -  
259 - const addActionsShow = ref(false);  
260 - const hasConverted = ref(false);  
261 - const summaryShow = ref(false);  
262 - const summaryContent = ref('');  
263 - const inputNameShow = ref(false);  
264 - const actionSortRef = ref(null);  
265 - const show = ref(false);  
266 - const datePickerShow = ref(false);  
267 - const timePickerShow = ref(false);  
268 - const dateValue = ref(Number(new Date()));  
269 - const timeValue = ref('14:13');  
270 - const trainingDateText = ref('');  
271 -  
272 - const totalSeconds = computed(() => trainingStore.totalSeconds || 0);  
273 - const isPause = computed(() => trainingStore.isPause);  
274 - const showPicker = computed(() => trainingStore.showPicker);  
275 - const defaultTimeIndex = computed(() => trainingStore.defaultTimeIndex);  
276 - const trainingName = computed(() => trainingStore.trainingName);  
277 - const trainingTimeText = computed(() => trainingStore.trainingTimeText);  
278 - const actionDetail = computed(() => trainingStore.actionDetail || {});  
279 -  
280 - // ===================== 组间休息倒计时 =====================  
281 - const restCountdownSeconds = ref(0);  
282 - const restCountdownTotal = ref(0);  
283 - let restCountdownInterval = null;  
284 -  
285 - const restFormattedTime = computed(() => { 179 +import { ref, computed, nextTick, onUnmounted } from 'vue';
  180 +import dayjs from 'dayjs';
  181 +import { onLoad } from '@dcloudio/uni-app';
  182 +import TrainingApi from '@/sheep/api/Training/traininghistory';
  183 +import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
  184 +import TemplatesApi from '@/sheep/api/Template/Templates';
  185 +import QueryPlanApi from '@/sheep/api/plan/queryplan';
  186 +import { useTrainingStore } from '@/sheep/store/trainingStore';
  187 +import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue';
  188 +import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue';
  189 +import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
  190 +import { trainingSnapshot } from '@/sheep/store/trainingSnapshot';
  191 +
  192 +const trainingStore = useTrainingStore();
  193 +
  194 +const addActionsShow = ref(false);
  195 +const hasConverted = ref(false);
  196 +const summaryShow = ref(false);
  197 +const summaryContent = ref('');
  198 +const inputNameShow = ref(false);
  199 +const actionSortRef = ref(null);
  200 +const show = ref(false);
  201 +const datePickerShow = ref(false);
  202 +const timePickerShow = ref(false);
  203 +const dateValue = ref(Number(new Date()));
  204 +const timeValue = ref('14:13');
  205 +const trainingDateText = ref('');
  206 +// 未来日期模板模式
  207 +const isFutureTemplate = ref(false);
  208 +const targetDate = ref('');
  209 +
  210 +const totalSeconds = computed(() => trainingStore.totalSeconds || 0);
  211 +const isPause = computed(() => trainingStore.isPause);
  212 +const showPicker = computed(() => trainingStore.showPicker);
  213 +const defaultTimeIndex = computed(() => trainingStore.defaultTimeIndex);
  214 +const trainingName = computed(() => trainingStore.trainingName);
  215 +const trainingTimeText = computed(() => trainingStore.trainingTimeText);
  216 +const actionDetail = computed(() => trainingStore.actionDetail || {});
  217 +// 未来模板模式:是否有任何动作
  218 +const hasAnyAction = computed(() => (actionDetail.value?.units || []).length > 0);
  219 +
  220 +/** 可靠的训练日期:优先使用传入的目标日期,兜底从显示文本提取 */
  221 +const reliableTrainingDate = computed(() => {
  222 + if (targetDate.value) return targetDate.value;
  223 + if (trainingDateText.value) return trainingDateText.value.split(' ')[0];
  224 + return dayjs().format('YYYY-MM-DD');
  225 +});
  226 +
  227 +// ===================== 组间休息倒计时 =====================
  228 +const restCountdownSeconds = ref(0);
  229 +const restCountdownTotal = ref(0);
  230 +let restCountdownInterval = null;
  231 +
  232 +const restFormattedTime = computed(() => {
286 const min = Math.floor(restCountdownSeconds.value / 60); 233 const min = Math.floor(restCountdownSeconds.value / 60);
287 const sec = restCountdownSeconds.value % 60; 234 const sec = restCountdownSeconds.value % 60;
288 return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; 235 return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
289 - }); 236 +});
290 237
291 - const restProgress = computed(() => { 238 +const restProgress = computed(() => {
292 if (restCountdownTotal.value <= 0) return 0; 239 if (restCountdownTotal.value <= 0) return 0;
293 return 1 - restCountdownSeconds.value / restCountdownTotal.value; 240 return 1 - restCountdownSeconds.value / restCountdownTotal.value;
294 - }); 241 +});
295 242
296 - // H5 SVG 圆环周长(用于 stroke-dasharray)  
297 - const CIRCUMFERENCE = 2 * Math.PI * 44; 243 +// H5 SVG 圆环周长(用于 stroke-dasharray)
  244 +const CIRCUMFERENCE = 2 * Math.PI * 44;
298 245
299 - const clearRestCountdown = () => { 246 +const clearRestCountdown = () => {
300 if (restCountdownInterval) { 247 if (restCountdownInterval) {
301 clearInterval(restCountdownInterval); 248 clearInterval(restCountdownInterval);
302 restCountdownInterval = null; 249 restCountdownInterval = null;
303 } 250 }
304 restCountdownSeconds.value = 0; 251 restCountdownSeconds.value = 0;
305 restCountdownTotal.value = 0; 252 restCountdownTotal.value = 0;
306 - }; 253 +};
307 254
308 - const startRestCountdown = (seconds) => { 255 +const startRestCountdown = (seconds) => {
309 if (!seconds || seconds <= 0) return; 256 if (!seconds || seconds <= 0) return;
310 - // 叠加倒计时:如果已经在倒计时中则累加,否则开始新的倒计时  
311 - if (restCountdownInterval) {  
312 - restCountdownSeconds.value += seconds;  
313 - restCountdownTotal.value += seconds;  
314 - } else { 257 + // 每次打勾用最新时间重新开始计时(替换当前倒计时,不叠加)
  258 + clearRestCountdown();
315 restCountdownTotal.value = seconds; 259 restCountdownTotal.value = seconds;
316 restCountdownSeconds.value = seconds; 260 restCountdownSeconds.value = seconds;
317 restCountdownInterval = setInterval(() => { 261 restCountdownInterval = setInterval(() => {
@@ -321,34 +265,37 @@ @@ -321,34 +265,37 @@
321 // 倒计时结束:不取消勾选状态,不恢复主计时器(主计时器始终在运行) 265 // 倒计时结束:不取消勾选状态,不恢复主计时器(主计时器始终在运行)
322 } 266 }
323 }, 1000); 267 }, 1000);
324 - }  
325 - // 不暂停主计时器,训练计时器始终继续  
326 - }; 268 +};
327 269
328 - function onCancelRestTimer(seconds) {  
329 - if (!restCountdownInterval) return;  
330 - // 取消勾选时减去对应秒数  
331 - restCountdownSeconds.value -= (seconds || 0);  
332 - restCountdownTotal.value -= (seconds || 0);  
333 - if (restCountdownSeconds.value <= 0) { 270 +function onCancelRestTimer() {
334 clearRestCountdown(); 271 clearRestCountdown();
335 - }  
336 - } 272 +}
337 273
338 - onUnmounted(() => { 274 +onUnmounted(() => {
339 clearRestCountdown(); 275 clearRestCountdown();
340 - }); 276 +});
341 277
342 - const openActionSort = () => { 278 +const openActionSort = () => {
343 actionSortRef.value.openActionSort(); 279 actionSortRef.value.openActionSort();
344 - }; 280 +};
345 281
346 - onLoad(async (options) => { 282 +onLoad(async (options) => {
347 const id = Number(options.id); 283 const id = Number(options.id);
348 const type = Number(options.type); 284 const type = Number(options.type);
349 const isTraining = options.isTraining === 'true'; 285 const isTraining = options.isTraining === 'true';
350 trainingStore.isTraining = isTraining; 286 trainingStore.isTraining = isTraining;
351 287
  288 + // 未来日期模板模式
  289 + isFutureTemplate.value = options.isFutureTemplate === 'true';
  290 + targetDate.value = options.targetDate || '';
  291 + // 初始化日期选择器为目标日期,防止打开时间设置弹窗时默认显示今天导致日期被意外覆盖
  292 + if (targetDate.value) {
  293 + dateValue.value = dayjs(targetDate.value).valueOf();
  294 + }
  295 + if (isFutureTemplate.value) {
  296 + trainingStore.clearTimer();
  297 + }
  298 +
352 const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null; 299 const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null;
353 trainingStore.dailyTemplateId = dailyTemplateId; 300 trainingStore.dailyTemplateId = dailyTemplateId;
354 // 直接让 store 去请求 + 保存数据 301 // 直接让 store 去请求 + 保存数据
@@ -365,121 +312,142 @@ @@ -365,121 +312,142 @@
365 } 312 }
366 initTrainingName(); 313 initTrainingName();
367 314
368 - trainingDateText.value = dayjs().format('YYYY-MM-DD dd'); 315 + // 编辑模式:默认勾选所有动作的全部组 + 激活整个unit背景
  316 + const isEdit = options.isEdit === 'true';
  317 + if (isEdit) {
  318 + const records = trainingStore.unitRecords;
  319 + Object.keys(records).forEach((unitIdx) => {
  320 + const unitRecord = records[unitIdx];
  321 + Object.keys(unitRecord.records).forEach((exId) => {
  322 + (unitRecord.records[exId] || []).forEach((set) => {
  323 + set.isActive = true;
  324 + });
  325 + });
  326 + });
  327 + // 激活所有 unit 的整体背景
  328 + const totalUnits = actionDetail.value?.units?.length || 0;
  329 + unitActiveStates.value = Array.from({ length: totalUnits }, () => true);
  330 + }
  331 +
  332 + // 未来模板模式:展示目标日期;训练模式有 targetDate 则用它,否则今天
  333 + trainingDateText.value = isFutureTemplate.value
  334 + ? dayjs(targetDate.value).format('YYYY-MM-DD dd')
  335 + : (targetDate.value ? dayjs(targetDate.value).format('YYYY-MM-DD dd') : dayjs().format('YYYY-MM-DD dd'));
  336 +
369 if (!trainingStore.trainingTimeText) { 337 if (!trainingStore.trainingTimeText) {
370 trainingStore.setTrainingTimeText(dayjs().format('HH:mm')); 338 trainingStore.setTrainingTimeText(dayjs().format('HH:mm'));
371 } 339 }
372 if (isTraining) { 340 if (isTraining) {
373 trainingStore.toggleTimer(); 341 trainingStore.toggleTimer();
374 } 342 }
375 - }); 343 +});
376 344
377 - const initTrainingName = () => { 345 +const initTrainingName = () => {
378 trainingStore.setTrainingName(actionDetail.value?.templateName || '训练'); 346 trainingStore.setTrainingName(actionDetail.value?.templateName || '训练');
379 - }; 347 +};
380 348
381 - const totalSets = computed(() => { 349 +const totalSets = computed(() => {
382 let count = 0; 350 let count = 0;
383 dongzuoRefs.value.forEach((ref) => { 351 dongzuoRefs.value.forEach((ref) => {
384 if (ref?.totalSetCount) count += ref.totalSetCount; 352 if (ref?.totalSetCount) count += ref.totalSetCount;
385 }); 353 });
386 return count; 354 return count;
387 - }); 355 +});
388 356
389 - const totalCheckedWeight = computed(() => { 357 +const totalCheckedWeight = computed(() => {
390 let sum = 0; 358 let sum = 0;
391 dongzuoRefs.value.forEach((ref) => { 359 dongzuoRefs.value.forEach((ref) => {
392 if (ref?.checkedWeight) sum += ref.checkedWeight; 360 if (ref?.checkedWeight) sum += ref.checkedWeight;
393 }); 361 });
394 return sum; 362 return sum;
395 - }); 363 +});
396 364
397 - const allTotalWeight = computed(() => { 365 +const allTotalWeight = computed(() => {
398 let sum = 0; 366 let sum = 0;
399 dongzuoRefs.value.forEach((ref) => { 367 dongzuoRefs.value.forEach((ref) => {
400 if (ref?.totalWeight) sum += ref.totalWeight; 368 if (ref?.totalWeight) sum += ref.totalWeight;
401 }); 369 });
402 return sum; 370 return sum;
403 - }); 371 +});
404 372
405 - // 获得子组件模板的数据  
406 - const dongzuoRefs = ref([]);  
407 - const setActionRef = (el, index) => { 373 +// 获得子组件模板的数据
  374 +const dongzuoRefs = ref([]);
  375 +const setActionRef = (el, index) => {
408 if (el) dongzuoRefs.value[index] = el; 376 if (el) dongzuoRefs.value[index] = el;
409 - }; 377 +};
410 378
411 - // 每个 unit 是否有勾选的组  
412 - const unitActiveStates = ref([])  
413 - const onUnitActiveChange = (index, val) => { 379 +// 每个 unit 是否有勾选的组
  380 +const unitActiveStates = ref([])
  381 +const onUnitActiveChange = (index, val) => {
414 unitActiveStates.value[index] = val 382 unitActiveStates.value[index] = val
415 unitActiveStates.value = [...unitActiveStates.value] 383 unitActiveStates.value = [...unitActiveStates.value]
416 - } 384 +}
417 385
418 - // 全部 unit 已勾选后才能提交  
419 - const allUnitsChecked = computed(() => { 386 +// 全部 unit 已勾选后才能提交
  387 +const allUnitsChecked = computed(() => {
420 const list = actionDetail.value?.units || [] 388 const list = actionDetail.value?.units || []
421 if (list.length === 0) return false 389 if (list.length === 0) return false
422 if (unitActiveStates.value.length !== list.length) return false 390 if (unitActiveStates.value.length !== list.length) return false
423 return unitActiveStates.value.every((v) => v === true) 391 return unitActiveStates.value.every((v) => v === true)
424 - }) 392 +})
425 393
426 - const openSummaryPopup = () => { 394 +const openSummaryPopup = () => {
427 summaryShow.value = true; 395 summaryShow.value = true;
428 summaryContent.value = ''; 396 summaryContent.value = '';
429 - }; 397 +};
430 398
431 - const saveSummary = () => { 399 +const saveSummary = () => {
432 summaryShow.value = false; 400 summaryShow.value = false;
433 summaryContent.value = ''; 401 summaryContent.value = '';
434 - }; 402 +};
435 403
436 - const openTrainingNamePopup = () => { 404 +const openTrainingNamePopup = () => {
437 inputNameShow.value = true; 405 inputNameShow.value = true;
438 - }; 406 +};
439 407
440 - const saveTrainingName = () => { 408 +const saveTrainingName = () => {
441 trainingStore.setTrainingName(trainingStore.trainingName); 409 trainingStore.setTrainingName(trainingStore.trainingName);
442 if (actionDetail.value) { 410 if (actionDetail.value) {
443 trainingStore.actionDetail.templateName = trainingStore.trainingName; 411 trainingStore.actionDetail.templateName = trainingStore.trainingName;
444 } 412 }
445 inputNameShow.value = false; 413 inputNameShow.value = false;
446 - }; 414 +};
447 415
448 - const formattedTime = computed(() => 416 +const formattedTime = computed(() =>
449 dayjs.duration(totalSeconds.value, 'seconds').format('HH:mm:ss'), 417 dayjs.duration(totalSeconds.value, 'seconds').format('HH:mm:ss'),
450 - );  
451 - const toggleTimer = () => trainingStore.toggleTimer();  
452 - const resetTimer = () => trainingStore.resetTimer(); 418 +);
  419 +const toggleTimer = () => trainingStore.toggleTimer();
  420 +const resetTimer = () => trainingStore.resetTimer();
453 421
454 - const timeColumns = ref([ 422 +const timeColumns = ref([
455 Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'), 423 Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'),
456 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'), 424 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),
457 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'), 425 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),
458 - ]); 426 +]);
459 427
460 - const handleTimerClick = () => { 428 +const handleTimerClick = () => {
461 if (isPause.value) { 429 if (isPause.value) {
462 trainingStore.openTimePicker(); 430 trainingStore.openTimePicker();
463 } 431 }
464 - }; 432 +};
465 433
466 - const onTimeConfirm = (e) => { 434 +const onTimeConfirm = (e) => {
467 const rawValues = e.value.map((val) => parseInt(String(val).replace(/[时分秒]/g, ''))); 435 const rawValues = e.value.map((val) => parseInt(String(val).replace(/[时分秒]/g, '')));
468 const seconds = rawValues[0] * 3600 + rawValues[1] * 60 + rawValues[2]; 436 const seconds = rawValues[0] * 3600 + rawValues[1] * 60 + rawValues[2];
469 trainingStore.setTotalSeconds(seconds); 437 trainingStore.setTotalSeconds(seconds);
470 trainingStore.closeTimePicker(); 438 trainingStore.closeTimePicker();
471 - };  
472 - const confirmDate = (e) => { 439 +};
  440 +const confirmDate = (e) => {
473 trainingDateText.value = dayjs(e.value).format('YYYY-MM-DD dd'); 441 trainingDateText.value = dayjs(e.value).format('YYYY-MM-DD dd');
474 datePickerShow.value = false; 442 datePickerShow.value = false;
475 - }; 443 +};
476 444
477 - const confirmTime = (e) => { 445 +const confirmTime = (e) => {
478 trainingStore.setTrainingTimeText(e.value); 446 trainingStore.setTrainingTimeText(e.value);
479 timePickerShow.value = false; 447 timePickerShow.value = false;
480 - }; 448 +};
481 449
482 - const mapSets = (recordItems, exerciseType) => 450 +const mapSets = (recordItems, exerciseType) =>
483 recordItems.map((item, index) => { 451 recordItems.map((item, index) => {
484 const isInterval = exerciseType === 6; 452 const isInterval = exerciseType === 6;
485 const duration = isInterval 453 const duration = isInterval
@@ -498,8 +466,16 @@ @@ -498,8 +466,16 @@
498 }; 466 };
499 }); 467 });
500 468
501 - const save = async () => {  
502 - // 前置校验:所有 unit 必须已勾选(顶部 ✓)才能提交 469 +const save = async () => {
  470 + // 前置校验
  471 + if (isFutureTemplate.value) {
  472 + // 未来模板模式:必须至少有一个动作
  473 + if (!hasAnyAction.value) {
  474 + uni.showToast({ title: '请先添加训练动作', icon: 'none' });
  475 + return;
  476 + }
  477 + } else {
  478 + // 训练模式:所有 unit 必须已勾选(顶部 ✓)才能提交
503 if (!allUnitsChecked.value) { 479 if (!allUnitsChecked.value) {
504 uni.showModal({ 480 uni.showModal({
505 title: '提示', 481 title: '提示',
@@ -509,13 +485,66 @@ @@ -509,13 +485,66 @@
509 }); 485 });
510 return; 486 return;
511 } 487 }
  488 + }
512 489
513 await nextTick(); 490 await nextTick();
514 - const units = []; 491 +
  492 + try {
  493 + // ========== 未来日期模板模式:直接从模板结构构建 units,创建模板 → 添加到日历 ==========
  494 + if (isFutureTemplate.value && targetDate.value) {
515 const unitList = actionDetail.value?.units || []; 495 const unitList = actionDetail.value?.units || [];
516 496
517 - for (let i = 0; i < unitList.length; i++) {  
518 - const unit = unitList[i]; 497 + // 直接从模板 actionDetail.units 提取,不依赖子组件 refs 的 recordList/superRecordMap
  498 + const templateUnits = unitList.map((unit, i) => ({
  499 + id: null,
  500 + name: i === 0 ? trainingName.value : (unit.unitName || ''),
  501 + unitType: unit.unitType,
  502 + sortOrder: i + 1,
  503 + exercises: unit.exercises,
  504 + }));
  505 +
  506 + const templateRes = await TemplatesApi.createCustTemplate({
  507 + templateName: trainingName.value,
  508 + scene: 0,
  509 + templateCover: '',
  510 + templateIntroduction: '',
  511 + units: templateUnits,
  512 + });
  513 +
  514 + if (templateRes.code == 0) {
  515 + // 兼容 templateRes.data 为数字(旧版)或对象(新版)的场景
  516 + const templateId = typeof templateRes.data === 'object'
  517 + ? (templateRes.data?.templateId || templateRes.data?.id || templateRes.data)
  518 + : templateRes.data;
  519 +
  520 + try {
  521 + await QueryPlanApi.addPlanToCalendar({
  522 + id: templateId,
  523 + trainDateList: [targetDate.value],
  524 + });
  525 + uni.$emit('calendarDataRefresh');
  526 + uni.showToast({ title: '已添加到训练日历', icon: 'success' });
  527 + } catch {
  528 + // 添加到日历失败,但模板已保存成功,单独提示
  529 + uni.showToast({ title: '模板已创建,但添加到日历失败', icon: 'none' });
  530 + }
  531 + } else {
  532 + uni.showToast({ title: templateRes.msg || '添加失败', icon: 'none' });
  533 + }
  534 +
  535 + setTimeout(() => {
  536 + uni.navigateBack();
  537 + trainingStore.clearTrainingStore();
  538 + }, 1500);
  539 + return;
  540 + }
  541 +
  542 + // ========== 训练/编辑模式:从子组件 refs 提取训练数据 ==========
  543 + const units = [];
  544 + const unitListForTrain = actionDetail.value?.units || [];
  545 +
  546 + for (let i = 0; i < unitListForTrain.length; i++) {
  547 + const unit = unitListForTrain[i];
519 const child = dongzuoRefs.value[i]; 548 const child = dongzuoRefs.value[i];
520 if (!child) continue; 549 if (!child) continue;
521 550
@@ -548,7 +577,7 @@ @@ -548,7 +577,7 @@
548 } 577 }
549 } 578 }
550 579
551 - // 检查是否有打勾(已完成)的动作,只有打勾的动作才会被提交 580 + // 检查是否有有效数据
552 const hasAnyChecked = units.some(u => 581 const hasAnyChecked = units.some(u =>
553 u.exercises.some(e => e.sets && e.sets.length > 0) 582 u.exercises.some(e => e.sets && e.sets.length > 0)
554 ); 583 );
@@ -570,10 +599,47 @@ @@ -570,10 +599,47 @@
570 return; 599 return;
571 } 600 }
572 601
573 - try {  
574 if (trainingStore.isTraining) { 602 if (trainingStore.isTraining) {
575 - await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value }); 603 + // 自由训练到空日期(有 targetDate 但无 dailyTemplateId):
  604 + // 需要先创建模板并添加到目标日期,才能让日历在对应日期上显示该训练
  605 + if (targetDate.value && !trainingStore.dailyTemplateId) {
  606 + try {
  607 + // 1. 基于训练动作结构创建个人模板
  608 + const templateUnits = unitListForTrain.map((u, idx) => ({
  609 + id: null,
  610 + name: idx === 0 ? trainingName.value : (u.unitName || ''),
  611 + unitType: u.unitType,
  612 + sortOrder: idx + 1,
  613 + exercises: u.exercises,
  614 + }));
  615 +
  616 + const templateRes = await TemplatesApi.createCustTemplate({
  617 + templateName: trainingName.value,
  618 + scene: 0,
  619 + templateCover: '',
  620 + templateIntroduction: '',
  621 + units: templateUnits,
  622 + });
  623 +
  624 + if (templateRes.code == 0) {
  625 + const templateId = typeof templateRes.data === 'object'
  626 + ? (templateRes.data?.templateId || templateRes.data?.id || templateRes.data)
  627 + : templateRes.data;
  628 +
  629 + // 2. 将模板添加到目标日期日历,确保训练显示在所选日期
  630 + await QueryPlanApi.addPlanToCalendar({
  631 + id: templateId,
  632 + trainDateList: [targetDate.value],
  633 + });
  634 + }
  635 + } catch (e) {
  636 + console.error('创建自由训练每日模板失败:', e);
  637 + }
  638 + }
  639 +
  640 + await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value, trainingDate: reliableTrainingDate.value });
576 uni.showToast({ title: '训练提交成功', icon: 'success' }); 641 uni.showToast({ title: '训练提交成功', icon: 'success' });
  642 + uni.$emit('calendarDataRefresh');
577 } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) { 643 } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
578 await dailytemplateApi.updateDailyTemplate({ 644 await dailytemplateApi.updateDailyTemplate({
579 dailyTemplateId: trainingStore.dailyTemplateId, 645 dailyTemplateId: trainingStore.dailyTemplateId,
@@ -581,9 +647,11 @@ @@ -581,9 +647,11 @@
581 allCompleted: allUnitsChecked.value, 647 allCompleted: allUnitsChecked.value,
582 }); 648 });
583 uni.showToast({ title: '模板修改成功', icon: 'success' }); 649 uni.showToast({ title: '模板修改成功', icon: 'success' });
  650 + uni.$emit('calendarDataRefresh');
584 } else { 651 } else {
585 - await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value }); 652 + await TrainingApi.createTrainHistory({ units, allCompleted: allUnitsChecked.value, trainingDate: reliableTrainingDate.value });
586 uni.showToast({ title: '训练提交成功', icon: 'success' }); 653 uni.showToast({ title: '训练提交成功', icon: 'success' });
  654 + uni.$emit('calendarDataRefresh');
587 } 655 }
588 setTimeout(() => { 656 setTimeout(() => {
589 uni.navigateBack(); 657 uni.navigateBack();
@@ -593,9 +661,9 @@ @@ -593,9 +661,9 @@
593 console.error('保存失败:', err); 661 console.error('保存失败:', err);
594 uni.showToast({ title: '保存失败', icon: 'none' }); 662 uni.showToast({ title: '保存失败', icon: 'none' });
595 } 663 }
596 - };  
597 - // 删除训练  
598 - const deleteTraining = () => { 664 +};
  665 +// 删除训练
  666 +const deleteTraining = () => {
599 uni.showModal({ 667 uni.showModal({
600 title: '确认删除', 668 title: '确认删除',
601 content: '确定要删除本次训练记录吗?删除后无法恢复', 669 content: '确定要删除本次训练记录吗?删除后无法恢复',
@@ -612,13 +680,13 @@ @@ -612,13 +680,13 @@
612 } 680 }
613 }, 681 },
614 }); 682 });
615 - }; 683 +};
616 684
617 - const addActionsPopup = () => { 685 +const addActionsPopup = () => {
618 addActionsShow.value = true; 686 addActionsShow.value = true;
619 - }; 687 +};
620 688
621 - const openMin = () => { 689 +const openMin = () => {
622 trainingStore.min = true; 690 trainingStore.min = true;
623 trainingSnapshot.data = JSON.parse( 691 trainingSnapshot.data = JSON.parse(
624 JSON.stringify({ 692 JSON.stringify({
@@ -650,17 +718,17 @@ @@ -650,17 +718,17 @@
650 } 718 }
651 719
652 uni.navigateBack(); 720 uni.navigateBack();
653 - }; 721 +};
654 </script> 722 </script>
655 723
656 <style lang="scss" scoped> 724 <style lang="scss" scoped>
657 - .container { 725 +.container {
658 min-height: 100vh; 726 min-height: 100vh;
659 background-color: #242424; 727 background-color: #242424;
660 padding-bottom: 120rpx; 728 padding-bottom: 120rpx;
661 - } 729 +}
662 730
663 - .status-header { 731 +.status-header {
664 display: flex; 732 display: flex;
665 justify-content: space-between; 733 justify-content: space-between;
666 align-items: center; 734 align-items: center;
@@ -695,6 +763,11 @@ @@ -695,6 +763,11 @@
695 } 763 }
696 } 764 }
697 765
  766 + .timer-box-placeholder {
  767 + width: 120rpx;
  768 + flex-shrink: 0;
  769 + }
  770 +
698 .finish-btn { 771 .finish-btn {
699 background-color: #fbdf09; 772 background-color: #fbdf09;
700 color: #000; 773 color: #000;
@@ -711,9 +784,9 @@ @@ -711,9 +784,9 @@
711 pointer-events: none; 784 pointer-events: none;
712 } 785 }
713 } 786 }
714 - } 787 +}
715 788
716 - .action-list { 789 +.action-list {
717 padding: 0 0 20rpx; 790 padding: 0 0 20rpx;
718 791
719 .header { 792 .header {
@@ -739,9 +812,9 @@ @@ -739,9 +812,9 @@
739 font-size: 24rpx; 812 font-size: 24rpx;
740 } 813 }
741 } 814 }
742 - } 815 +}
743 816
744 - .empty-state { 817 +.empty-state {
745 display: flex; 818 display: flex;
746 flex-direction: column; 819 flex-direction: column;
747 align-items: center; 820 align-items: center;
@@ -758,11 +831,15 @@ @@ -758,11 +831,15 @@
758 .empty-subtext { 831 .empty-subtext {
759 font-size: 28rpx; 832 font-size: 28rpx;
760 } 833 }
761 - } 834 +}
762 835
763 - .tip-bubble { 836 +.tip-bubble {
764 position: fixed; 837 position: fixed;
765 - bottom: 230rpx; 838 + bottom: 200rpx;
  839 + // #ifdef H5
  840 + bottom: 160rpx;
  841 + // #endif
  842 +
766 left: 52%; 843 left: 52%;
767 transform: translateX(-50%); 844 transform: translateX(-50%);
768 background-color: #000; 845 background-color: #000;
@@ -782,9 +859,9 @@ @@ -782,9 +859,9 @@
782 border-right: 10rpx solid transparent; 859 border-right: 10rpx solid transparent;
783 border-top: 10rpx solid #000; 860 border-top: 10rpx solid #000;
784 } 861 }
785 - } 862 +}
786 863
787 - .footer-bar { 864 +.footer-bar {
788 position: fixed; 865 position: fixed;
789 bottom: 0; 866 bottom: 0;
790 left: 0; 867 left: 0;
@@ -838,9 +915,9 @@ @@ -838,9 +915,9 @@
838 position: relative; 915 position: relative;
839 z-index: 99999; 916 z-index: 99999;
840 } 917 }
841 - } 918 +}
842 919
843 - .time-pop-container { 920 +.time-pop-container {
844 padding: 40rpx 30rpx; 921 padding: 40rpx 30rpx;
845 color: #fff; 922 color: #fff;
846 923
@@ -956,9 +1033,9 @@ @@ -956,9 +1033,9 @@
956 border-radius: 4rpx; 1033 border-radius: 4rpx;
957 margin: 40rpx auto 0; 1034 margin: 40rpx auto 0;
958 } 1035 }
959 - } 1036 +}
960 1037
961 - .summaryPopup { 1038 +.summaryPopup {
962 width: 100vw; 1039 width: 100vw;
963 height: 75vh; 1040 height: 75vh;
964 background-color: #2c2c2e; 1041 background-color: #2c2c2e;
@@ -1004,9 +1081,9 @@ @@ -1004,9 +1081,9 @@
1004 color: #8e8e93; 1081 color: #8e8e93;
1005 } 1082 }
1006 } 1083 }
1007 - } 1084 +}
1008 1085
1009 - .namePopup { 1086 +.namePopup {
1010 width: 100%; 1087 width: 100%;
1011 height: 75vh; 1088 height: 75vh;
1012 background-color: #2c2c2e; 1089 background-color: #2c2c2e;
@@ -1051,18 +1128,18 @@ @@ -1051,18 +1128,18 @@
1051 color: #8e8e93; 1128 color: #8e8e93;
1052 } 1129 }
1053 } 1130 }
1054 - } 1131 +}
1055 1132
1056 - // ===================== 组间休息倒计时 =====================  
1057 - .rest-countdown { 1133 +// ===================== 组间休息倒计时 =====================
  1134 +.rest-countdown {
1058 position: fixed; 1135 position: fixed;
1059 bottom: 180rpx; 1136 bottom: 180rpx;
1060 right: 30rpx; 1137 right: 30rpx;
1061 z-index: 100; 1138 z-index: 100;
1062 animation: countdown-enter 0.3s ease-out; 1139 animation: countdown-enter 0.3s ease-out;
1063 - } 1140 +}
1064 1141
1065 - @keyframes countdown-enter { 1142 +@keyframes countdown-enter {
1066 from { 1143 from {
1067 opacity: 0; 1144 opacity: 0;
1068 transform: scale(0.5); 1145 transform: scale(0.5);
@@ -1072,9 +1149,9 @@ @@ -1072,9 +1149,9 @@
1072 opacity: 1; 1149 opacity: 1;
1073 transform: scale(1); 1150 transform: scale(1);
1074 } 1151 }
1075 - } 1152 +}
1076 1153
1077 - .countdown-circle { 1154 +.countdown-circle {
1078 position: relative; 1155 position: relative;
1079 width: 160rpx; 1156 width: 160rpx;
1080 height: 160rpx; 1157 height: 160rpx;
@@ -1127,5 +1204,5 @@ @@ -1127,5 +1204,5 @@
1127 color: #8e8e93; 1204 color: #8e8e93;
1128 margin-top: 4rpx; 1205 margin-top: 4rpx;
1129 } 1206 }
1130 - } 1207 +}
1131 </style> 1208 </style>
@@ -71,7 +71,7 @@ @@ -71,7 +71,7 @@
71 <view class="name">{{ unit.unitName || `超级组 ${unitIndex + 1}` }}</view> 71 <view class="name">{{ unit.unitName || `超级组 ${unitIndex + 1}` }}</view>
72 <view class="PartTool"> 72 <view class="PartTool">
73 <template v-if="unit.unitType === 1"> 73 <template v-if="unit.unitType === 1">
74 - {{ unit.exercises[0]?.categoryDescription }} {{ unit.exercises[0]?.equipmentDescription }} 74 + {{ getUnitDesc(unit) }}
75 </template> 75 </template>
76 <template v-else>超级组</template> 76 <template v-else>超级组</template>
77 </view> 77 </view>
@@ -102,7 +102,7 @@ @@ -102,7 +102,7 @@
102 <!-- 超级组内动作名称 --> 102 <!-- 超级组内动作名称 -->
103 <view class="super-actionName"> 103 <view class="super-actionName">
104 <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="name-item"> 104 <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="name-item">
105 - <text class="letter">{{ String.fromCharCode(65 + exIdx) }} </text> 105 + <text class="letter">{{ getLabelChar(exIdx) }} </text>
106 <text class="action-name">{{ ex.exerciseName || '动作名称' }}</text> 106 <text class="action-name">{{ ex.exerciseName || '动作名称' }}</text>
107 </view> 107 </view>
108 </view> 108 </view>
@@ -114,7 +114,7 @@ @@ -114,7 +114,7 @@
114 <view class="set-column"> 114 <view class="set-column">
115 <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="action-in-set"> 115 <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="action-in-set">
116 <template v-if="ex.sets[idx - 1]"> 116 <template v-if="ex.sets[idx - 1]">
117 - <text class="action-letter">{{ idx }}{{ String.fromCharCode(65 + exIdx) }}</text> 117 + <text class="action-letter">{{ idx }}{{ getLabelChar(exIdx) }}</text>
118 <view class="set-content"> 118 <view class="set-content">
119 <text>{{ formatSetText(ex.exerciseType, ex.sets[idx - 1]) }}</text> 119 <text>{{ formatSetText(ex.exerciseType, ex.sets[idx - 1]) }}</text>
120 <text v-if="ex.sets[idx - 1].restTime && ex.exerciseType !== 6" class="rest">{{ ex.sets[idx - 120 <text v-if="ex.sets[idx - 1].restTime && ex.exerciseType !== 6" class="rest">{{ ex.sets[idx -
@@ -136,8 +136,15 @@ @@ -136,8 +136,15 @@
136 136
137 <!-- ==================== 底部操作栏 ==================== --> 137 <!-- ==================== 底部操作栏 ==================== -->
138 <view class="bottom-actions"> 138 <view class="bottom-actions">
  139 + <!-- 模板添加模式 -->
  140 + <template v-if="isFromTemplateAdd">
  141 + <button class="add-calendar-btn" @click="confirmAddToCalendar">
  142 + 模板添加到日程({{ addDateText }})
  143 + </button>
  144 + </template>
  145 +
139 <!-- 我的计划模式 --> 146 <!-- 我的计划模式 -->
140 - <template v-if="isMyPlan"> 147 + <template v-else-if="isMyPlan">
141 <button class="start-btn" :class="{ locked: !isUnlocked }" :disabled="!isUnlocked" 148 <button class="start-btn" :class="{ locked: !isUnlocked }" :disabled="!isUnlocked"
142 @click="startDaiTemplateTraining"> 149 @click="startDaiTemplateTraining">
143 {{ isUnlocked ? '立即开始训练' : '训练在当日解锁' }} 150 {{ isUnlocked ? '立即开始训练' : '训练在当日解锁' }}
@@ -212,7 +219,7 @@ import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue'; @@ -212,7 +219,7 @@ import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue';
212 // ==================== 常量 ==================== 219 // ==================== 常量 ====================
213 const DELETE_DELAY = 800; 220 const DELETE_DELAY = 800;
214 const NAVIGATE_DELAY = 1500; 221 const NAVIGATE_DELAY = 1500;
215 -const MUSCLE_IMG = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png'; 222 +const MUSCLE_IMG = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg';
216 const SUPER_GROUP_IMG = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png'; 223 const SUPER_GROUP_IMG = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png';
217 const LOST_IMAGE = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'; 224 const LOST_IMAGE = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
218 225
@@ -240,8 +247,10 @@ const showColorPopup = ref(false); @@ -240,8 +247,10 @@ const showColorPopup = ref(false);
240 const isOffice = ref(false); 247 const isOffice = ref(false);
241 const isMyPlan = ref(false); 248 const isMyPlan = ref(false);
242 const isDailytemplateId = ref(false); 249 const isDailytemplateId = ref(false);
  250 +const isFromTemplateAdd = ref(false);
243 const isMove = ref(false); 251 const isMove = ref(false);
244 const date = ref(''); 252 const date = ref('');
  253 +const selectedAddDate = ref('');
245 254
246 const templateId = ref(''); 255 const templateId = ref('');
247 const calendarId = ref(0); 256 const calendarId = ref(0);
@@ -263,106 +272,99 @@ const isUnlocked = computed(() => { @@ -263,106 +272,99 @@ const isUnlocked = computed(() => {
263 if (!templateDetail.value?.trainingDate) return false; 272 if (!templateDetail.value?.trainingDate) return false;
264 const [year, month, day] = templateDetail.value.trainingDate; 273 const [year, month, day] = templateDetail.value.trainingDate;
265 const unlockDate = dayjs(`${year}-${month}-${day}`).startOf('day'); 274 const unlockDate = dayjs(`${year}-${month}-${day}`).startOf('day');
266 - return dayjs().startOf('day').isSameOrAfter(unlockDate); 275 + return dayjs().startOf('day').diff(unlockDate, 'day') >= 0;
  276 +});
  277 +
  278 +/** 模板添加模式:格式化日期显示 */
  279 +const addDateText = computed(() => {
  280 + if (!selectedAddDate.value) return '';
  281 + const d = dayjs(selectedAddDate.value);
  282 + const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
  283 + return `${d.format('MM/DD')} ${weekMap[d.day()]}`;
267 }); 284 });
268 285
269 -// ==================== 工具函数 ==================== 286 +/** 纯函数:索引→字母 A/B/C... */
  287 +const getLabelChar = (idx) => String.fromCharCode(65 + idx);
  288 +
  289 +/** 纯函数:unit 的描述文本 */
  290 +const getUnitDesc = (unit) =>
  291 + unit?.exercises?.[0] ? `${unit.exercises[0].categoryDescription ?? ''} ${unit.exercises[0].equipmentDescription ?? ''}` : '';
270 292
271 /** 秒数 → HH:mm:ss */ 293 /** 秒数 → HH:mm:ss */
272 -function formatSeconds(seconds) { 294 +const formatSeconds = (seconds) => {
273 if (!seconds || isNaN(seconds)) return '00:00:00'; 295 if (!seconds || isNaN(seconds)) return '00:00:00';
274 return dayjs.duration(seconds, 'seconds').format('HH:mm:ss'); 296 return dayjs.duration(seconds, 'seconds').format('HH:mm:ss');
275 -} 297 +};
276 298
277 /** 根据动作类型格式化组次文本 */ 299 /** 根据动作类型格式化组次文本 */
278 -function formatSetText(exerciseType, detail) {  
279 - const fn = SET_TEXT_FORMATTERS[exerciseType];  
280 - return fn ? fn(detail) : '';  
281 -} 300 +const formatSetText = (exerciseType, detail) => SET_TEXT_FORMATTERS[exerciseType]?.(detail) ?? '';
282 301
283 /** 获取 unit 封面图 */ 302 /** 获取 unit 封面图 */
284 -function getUnitCover(unit) {  
285 - if (unit.unitType === 1) {  
286 - return unit.exercises?.[0]?.exerciseCover || LOST_IMAGE;  
287 - }  
288 - return SUPER_GROUP_IMG;  
289 -} 303 +const getUnitCover = (unit) =>
  304 + unit?.unitType === 1
  305 + ? (unit.exercises?.[0]?.exerciseCover || LOST_IMAGE)
  306 + : SUPER_GROUP_IMG;
290 307
291 /** 单个 unit 内所有动作的总组数 */ 308 /** 单个 unit 内所有动作的总组数 */
292 -function getUnitSetCount(unit) {  
293 - if (!unit?.exercises?.length) return 0;  
294 - let count = 0;  
295 - unit.exercises.forEach((item) => {  
296 - if (item.sets?.length) count += item.sets.length;  
297 - });  
298 - return count;  
299 -} 309 +const getUnitSetCount = (unit) =>
  310 + unit?.exercises?.reduce((total, item) => total + (item.sets?.length || 0), 0) ?? 0;
300 311
301 /** 超级组内最大组数 */ 312 /** 超级组内最大组数 */
302 -function getSuperSetMaxGroupCount(exercises) {  
303 - if (!Array.isArray(exercises) || exercises.length === 0) return 0;  
304 - return exercises.reduce((max, ex) => {  
305 - const len = Array.isArray(ex.sets) ? ex.sets.length : 0;  
306 - return Math.max(max, len);  
307 - }, 0);  
308 -} 313 +const getSuperSetMaxGroupCount = (exercises) =>
  314 + exercises?.reduce((max, ex) => Math.max(max, ex.sets?.length || 0), 0) ?? 0;
309 315
310 // ==================== 数据加载 ==================== 316 // ==================== 数据加载 ====================
311 -async function loadTemplateDetail() { 317 +const loadTemplateDetail = async () => {
312 try { 318 try {
313 - let res;  
314 - if (isOffice.value) {  
315 - res = await TemplatesApi.getTemplateDetail(templateId.value);  
316 - } else if (isDailytemplateId.value) {  
317 - res = await QueryPlanApi.getDailyTemplateDetail(templateId.value);  
318 - } else {  
319 - res = await TemplatesApi.getCustTemplateDetail(templateId.value);  
320 - } 319 + const res = isOffice.value
  320 + ? await TemplatesApi.getTemplateDetail(templateId.value)
  321 + : isDailytemplateId.value
  322 + ? await QueryPlanApi.getDailyTemplateDetail(templateId.value)
  323 + : await TemplatesApi.getCustTemplateDetail(templateId.value);
321 templateDetail.value = res.data; 324 templateDetail.value = res.data;
322 templateUnits.value = res.data.units || []; 325 templateUnits.value = res.data.units || [];
323 } catch (err) { 326 } catch (err) {
324 console.error('❌ 模板详情加载失败:', err); 327 console.error('❌ 模板详情加载失败:', err);
325 } 328 }
326 -} 329 +};
327 330
328 // ==================== 日历颜色 ==================== 331 // ==================== 日历颜色 ====================
329 -function openColorPopup() { 332 +const openColorPopup = () => {
330 if (!templateDetail.value.id) { 333 if (!templateDetail.value.id) {
331 uni.showToast({ title: '未找到模板ID', icon: 'none' }); 334 uni.showToast({ title: '未找到模板ID', icon: 'none' });
332 return; 335 return;
333 } 336 }
334 showColorPopup.value = true; 337 showColorPopup.value = true;
335 -} 338 +};
336 339
337 // ==================== 添加到/移动到日历 ==================== 340 // ==================== 添加到/移动到日历 ====================
338 -function handleCalendarSuccess() { 341 +const handleCalendarSuccess = () => {
339 loadTemplateDetail(); 342 loadTemplateDetail();
340 -} 343 + uni.$emit('calendarDataRefresh');
  344 +};
341 345
342 -function openCalendarPopup() { 346 +const openCalendarPopup = () => {
343 calendarId.value = templateDetail.value.id; 347 calendarId.value = templateDetail.value.id;
344 calendarPopupRef.value?.open(); 348 calendarPopupRef.value?.open();
345 -} 349 +};
346 350
347 -function openMovePopup() { 351 +const openMovePopup = () => {
348 calendarId.value = templateDetail.value.dailyTemplateId; 352 calendarId.value = templateDetail.value.dailyTemplateId;
349 isMove.value = true; 353 isMove.value = true;
350 calendarPopupRef.value?.open(); 354 calendarPopupRef.value?.open();
351 -} 355 +};
352 356
353 // ==================== 动作详情弹窗 ==================== 357 // ==================== 动作详情弹窗 ====================
354 -function handleUnitClick(unit) { 358 +const handleUnitClick = (unit) => {
355 if (unit.unitType === 1) { 359 if (unit.unitType === 1) {
356 - const id = unit.exercises[0].exerciseId;  
357 - nextTick(() => actionDetailRef.value?.open(id, 1)); 360 + nextTick(() => actionDetailRef.value?.open(unit.exercises[0].exerciseId, 1));
358 } else if (unit.unitType === 2) { 361 } else if (unit.unitType === 2) {
359 - const id = unit.supersetId;  
360 - nextTick(() => actionDetailRef.value?.open(id, 2)); 362 + nextTick(() => actionDetailRef.value?.open(unit.supersetId, 2));
361 } 363 }
362 -} 364 +};
363 365
364 // ==================== 开始训练 ==================== 366 // ==================== 开始训练 ====================
365 -function startTraining() { 367 +const startTraining = () => {
366 if (trainingStore.isTraining) { 368 if (trainingStore.isTraining) {
367 uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' }); 369 uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' });
368 return; 370 return;
@@ -372,9 +374,9 @@ function startTraining() { @@ -372,9 +374,9 @@ function startTraining() {
372 uni.navigateTo({ 374 uni.navigateTo({
373 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&isTraining=true`, 375 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&isTraining=true`,
374 }); 376 });
375 -} 377 +};
376 378
377 -function startDaiTemplateTraining() { 379 +const startDaiTemplateTraining = () => {
378 if (trainingStore.isTraining) { 380 if (trainingStore.isTraining) {
379 uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' }); 381 uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' });
380 return; 382 return;
@@ -385,20 +387,45 @@ function startDaiTemplateTraining() { @@ -385,20 +387,45 @@ function startDaiTemplateTraining() {
385 uni.navigateTo({ 387 uni.navigateTo({
386 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&dailyTemplateId=${templateDetail.value.dailyTemplateId}&isTraining=true`, 388 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&dailyTemplateId=${templateDetail.value.dailyTemplateId}&isTraining=true`,
387 }); 389 });
388 -} 390 +};
  391 +
  392 +// ==================== 模板添加模式:直接添加到日程 ====================
  393 +const confirmAddToCalendar = async () => {
  394 + if (!templateDetail.value?.id) {
  395 + uni.showToast({ title: '未找到模板', icon: 'none' });
  396 + return;
  397 + }
  398 + const targetDate = selectedAddDate.value || dayjs().format('YYYY-MM-DD');
  399 + try {
  400 + const res = await QueryPlanApi.addPlanToCalendar({
  401 + id: templateDetail.value.id,
  402 + trainDateList: [targetDate],
  403 + });
  404 + if (res.code === 0) {
  405 + uni.$emit('calendarDataRefresh');
  406 + uni.showToast({ title: '添加成功', icon: 'success' });
  407 + setTimeout(() => uni.navigateBack(), 800);
  408 + } else {
  409 + uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
  410 + }
  411 + } catch (err) {
  412 + console.error('模板添加到日程失败:', err);
  413 + uni.showToast({ title: '网络异常,请重试', icon: 'none' });
  414 + }
  415 +};
389 416
390 // ==================== 编辑模板 ==================== 417 // ==================== 编辑模板 ====================
391 -function templateEdit() { 418 +const templateEdit = () => {
392 trainingStore.isSystem = templateDetail.value.isSystem; 419 trainingStore.isSystem = templateDetail.value.isSystem;
393 trainingStore.loadDailyTemplateForEdit(templateDetail.value); 420 trainingStore.loadDailyTemplateForEdit(templateDetail.value);
394 trainingStore.initDailyTemplateRecords(); 421 trainingStore.initDailyTemplateRecords();
395 uni.navigateTo({ 422 uni.navigateTo({
396 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&dailyTemplateId=${templateDetail.value.dailyTemplateId}`, 423 url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&dailyTemplateId=${templateDetail.value.dailyTemplateId}`,
397 }); 424 });
398 -} 425 +};
399 426
400 // ==================== 删除操作 ==================== 427 // ==================== 删除操作 ====================
401 -async function handleDeleteTemplate(dailyTemplateId) { 428 +const handleDeleteTemplate = async (dailyTemplateId) => {
402 if (!dailyTemplateId) { 429 if (!dailyTemplateId) {
403 uni.showToast({ title: '未找到每日模板ID', icon: 'none' }); 430 uni.showToast({ title: '未找到每日模板ID', icon: 'none' });
404 return; 431 return;
@@ -420,9 +447,9 @@ async function handleDeleteTemplate(dailyTemplateId) { @@ -420,9 +447,9 @@ async function handleDeleteTemplate(dailyTemplateId) {
420 console.error('删除失败', err); 447 console.error('删除失败', err);
421 uni.showToast({ title: '删除失败,请重试', icon: 'none' }); 448 uni.showToast({ title: '删除失败,请重试', icon: 'none' });
422 } 449 }
423 -} 450 +};
424 451
425 -async function confirmDelete(planId) { 452 +const confirmDelete = async (planId) => {
426 try { 453 try {
427 const res = await QueryPlanApi.deletePlan(planId); 454 const res = await QueryPlanApi.deletePlan(planId);
428 if (res.code === 0 && res.data) { 455 if (res.code === 0 && res.data) {
@@ -436,15 +463,11 @@ async function confirmDelete(planId) { @@ -436,15 +463,11 @@ async function confirmDelete(planId) {
436 } finally { 463 } finally {
437 setTimeout(() => { 464 setTimeout(() => {
438 const pages = getCurrentPages(); 465 const pages = getCurrentPages();
439 - const backNum = pages.length - 2;  
440 - if (backNum > 0) {  
441 - uni.navigateBack({ delta: backNum });  
442 - } else {  
443 - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-wode-jihua' });  
444 - } 466 + const delta = pages.length - 2;
  467 + delta > 0 ? uni.navigateBack({ delta }) : uni.navigateTo({ url: '/pages4/pages/xunji/xunji-wode-jihua' });
445 }, NAVIGATE_DELAY); 468 }, NAVIGATE_DELAY);
446 } 469 }
447 -} 470 +};
448 471
449 // ==================== 生命周期 ==================== 472 // ==================== 生命周期 ====================
450 onLoad((options) => { 473 onLoad((options) => {
@@ -452,7 +475,9 @@ onLoad((options) => { @@ -452,7 +475,9 @@ onLoad((options) => {
452 isOffice.value = options.isOffice === 'true'; 475 isOffice.value = options.isOffice === 'true';
453 isMyPlan.value = options.isMyPlan === 'true'; 476 isMyPlan.value = options.isMyPlan === 'true';
454 isDailytemplateId.value = options.isDailytemplateId === 'true'; 477 isDailytemplateId.value = options.isDailytemplateId === 'true';
  478 + isFromTemplateAdd.value = options.from === 'addFromTemplate';
455 date.value = options.date || ''; 479 date.value = options.date || '';
  480 + selectedAddDate.value = options.date || '';
456 481
457 loadTemplateDetail(); 482 loadTemplateDetail();
458 }); 483 });
@@ -773,6 +798,18 @@ $radius-circle: 50%; @@ -773,6 +798,18 @@ $radius-circle: 50%;
773 } 798 }
774 } 799 }
775 800
  801 + .add-calendar-btn {
  802 + flex: 1;
  803 + height: 80rpx;
  804 + background-color: $accent;
  805 + color: #000;
  806 + font-size: $font-md;
  807 + font-weight: bold;
  808 + border-radius: $radius-round;
  809 + border: none;
  810 + outline: none;
  811 + }
  812 +
776 .move-to { 813 .move-to {
777 display: flex; 814 display: flex;
778 justify-content: center; 815 justify-content: center;
@@ -98,6 +98,7 @@ const templateList = ref({}) @@ -98,6 +98,7 @@ const templateList = ref({})
98 const isBigTemOffice = ref(false) 98 const isBigTemOffice = ref(false)
99 const folderList = ref([]) 99 const folderList = ref([])
100 const templateMenuRef = ref(null) 100 const templateMenuRef = ref(null)
  101 +const selectedDate = ref('') // 从 xunji-rili-tianjia-moban 透传的日期
101 102
102 // ==================== 筛选 ==================== 103 // ==================== 筛选 ====================
103 const filteredTemplates = ref([]) 104 const filteredTemplates = ref([])
@@ -216,9 +217,12 @@ const showTemplateMenu = (item) => { @@ -216,9 +217,12 @@ const showTemplateMenu = (item) => {
216 } 217 }
217 218
218 const godetail = (items) => { 219 const godetail = (items) => {
219 - uni.navigateTo({  
220 - url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${items.id}&isOffice=true`,  
221 - }) 220 + let url = `/pages4/pages/xunji/xunji-moban-xiangqing?id=${items.id}&isOffice=true`;
  221 + // 只有从添加日程流(带日期参数)进入时,才标记为模板添加模式
  222 + if (selectedDate.value) {
  223 + url += `&date=${selectedDate.value}&from=addFromTemplate`;
  224 + }
  225 + uni.navigateTo({ url });
222 } 226 }
223 227
224 const handleRefreshTemplate = () => { 228 const handleRefreshTemplate = () => {
@@ -229,6 +233,9 @@ const handleRefreshTemplate = () => { @@ -229,6 +233,9 @@ const handleRefreshTemplate = () => {
229 onLoad((options) => { 233 onLoad((options) => {
230 id.value = options.id 234 id.value = options.id
231 isBigTemOffice.value = options.isBigTemOffice === 'true' 235 isBigTemOffice.value = options.isBigTemOffice === 'true'
  236 + if (options.date) {
  237 + selectedDate.value = options.date
  238 + }
232 239
233 console.log('接收的文件夹列表:', options.foldList) 240 console.log('接收的文件夹列表:', options.foldList)
234 if (options.foldList) { 241 if (options.foldList) {
@@ -108,6 +108,7 @@ @@ -108,6 +108,7 @@
108 108
109 <script setup> 109 <script setup>
110 import { onMounted, ref } from 'vue'; 110 import { onMounted, ref } from 'vue';
  111 +import { onLoad } from '@dcloudio/uni-app';
111 import TemplatesApi from '@/sheep/api/Template/Templates'; 112 import TemplatesApi from '@/sheep/api/Template/Templates';
112 113
113 // 获取模板列表 114 // 获取模板列表
@@ -117,6 +118,8 @@ const part = ref(false); // 部位是否选中 @@ -117,6 +118,8 @@ const part = ref(false); // 部位是否选中
117 const scene = ref(false); // 场景是否选中 118 const scene = ref(false); // 场景是否选中
118 const isFiltering = ref(false); 119 const isFiltering = ref(false);
119 120
  121 +const selectedDate = ref(''); // 从添加入口传过来的日期
  122 +
120 const activePartId = ref('0'); 123 const activePartId = ref('0');
121 const activePart = ref('不限'); 124 const activePart = ref('不限');
122 const activeSceneId = ref('0'); 125 const activeSceneId = ref('0');
@@ -255,16 +258,21 @@ const goBack = () => { @@ -255,16 +258,21 @@ const goBack = () => {
255 const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"; 258 const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png";
256 // 个人模板点击跳转(直接进详情) 259 // 个人模板点击跳转(直接进详情)
257 const goToPersonalDetail = (item) => { 260 const goToPersonalDetail = (item) => {
258 - uni.navigateTo({  
259 - url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${item.id}`  
260 - }); 261 + // 一旦进入过此页(来自 add-train-popup 入口),所有个人模板点击都视为模板添加流
  262 + let url = `/pages4/pages/xunji/xunji-moban-xiangqing?id=${item.id}&from=addFromTemplate`;
  263 + if (selectedDate.value) {
  264 + url += `&date=${selectedDate.value}`;
  265 + }
  266 + uni.navigateTo({ url });
261 }; 267 };
262 268
263 // 跳转到模板详情页 269 // 跳转到模板详情页
264 const goToTemplate = (item) => { 270 const goToTemplate = (item) => {
265 - uni.navigateTo({  
266 - url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`  
267 - }); 271 + let url = `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`;
  272 + if (selectedDate.value) {
  273 + url += `&date=${selectedDate.value}`;
  274 + }
  275 + uni.navigateTo({ url });
268 }; 276 };
269 277
270 // 获取胶囊按钮信息,用于动态适配导航栏高度 278 // 获取胶囊按钮信息,用于动态适配导航栏高度
@@ -273,6 +281,13 @@ const menuButtonInfo = ref({ @@ -273,6 +281,13 @@ const menuButtonInfo = ref({
273 height: 32 281 height: 32
274 }); 282 });
275 283
  284 +// 接收从 add-train-popup 透传的日期
  285 +onLoad((options) => {
  286 + if (options?.date) {
  287 + selectedDate.value = options.date;
  288 + }
  289 +});
  290 +
276 onMounted(() => { 291 onMounted(() => {
277 getTemplatesList(); 292 getTemplatesList();
278 getPartCategories(); 293 getPartCategories();
@@ -327,18 +327,16 @@ const selectScene = (item) => { @@ -327,18 +327,16 @@ const selectScene = (item) => {
327 activeScene.value = item.title; 327 activeScene.value = item.title;
328 activeSceneId.value = item.id; 328 activeSceneId.value = item.id;
329 showSceneDropdown.value = false; 329 showSceneDropdown.value = false;
  330 + console.log(activeSceneId.value,"======================");
  331 +
330 doFilter(); // 筛选 332 doFilter(); // 筛选
331 }; 333 };
332 334
333 const doFilter = async () => { 335 const doFilter = async () => {
334 - const muscleId = activePartId.value;  
335 - const sceneId = activeSceneId.value;  
336 - console.log('muscleId=', muscleId);  
337 - console.log('sceneId', sceneId);  
338 336
339 337
340 try { 338 try {
341 - const res = await TemplatesApi.queryCustTemplate(muscleId, sceneId); 339 + const res = await TemplatesApi.queryCustTemplate(null,activePartId.value, activeSceneId.value);
342 console.log('筛选之后的自定义模板列表:', res); 340 console.log('筛选之后的自定义模板列表:', res);
343 341
344 templateList.value = res.data || []; 342 templateList.value = res.data || [];
1 <template> 1 <template>
2 <view class="container"> 2 <view class="container">
3 - <view class="safe-area-top" :style="{ paddingTop: topSafeArea + 'px' }" @click="goBack">  
4 - <uni-nav-bar left-icon="left" backgroundColor="transparent" :border="false" fixed dark /> 3 + <!-- 1. 自定义悬浮顶部导航栏(带安全区) -->
  4 + <view class="header-nav" :style="{ paddingTop: topSafeArea + 'px' }">
  5 + <view class="nav-back-btn" @click="goBack">
  6 + <uni-icons type="left" size="20" color="#ffffff" />
5 </view> 7 </view>
6 -  
7 - <!-- 背景图片 -->  
8 - <view class="backgroundImage">  
9 - <image  
10 - :src="  
11 - backgroundImage  
12 - ? backgroundImage  
13 - : 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'  
14 - "  
15 - class="img"  
16 - mode="aspectFill"  
17 - ></image>  
18 </view> 8 </view>
19 - <!-- 内容 -->  
20 - <view class="content">  
21 - <view class="info">  
22 - <view class="left">  
23 - <view class="avatar">  
24 - <image  
25 - :src="  
26 - avatar  
27 - ? avatar  
28 - : 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'  
29 - "  
30 - class="img"  
31 - mode="aspectFill"  
32 - ></image> 9 +
  10 + <!-- 2. 顶部背景大图 -->
  11 + <view class="banner-wrapper">
  12 + <image :src="backgroundImage || DEFAULT_BG_IMAGE" class="banner-img" mode="aspectFill"></image>
  13 + <view class="banner-mask"></view>
33 </view> 14 </view>
34 - <view class="user-info">  
35 - <view class="nickname">{{ nickname }}</view>  
36 - <view class="user-tip"> 运动爱好者 </view> 15 +
  16 + <!-- 3. 主体内容区域(自然滚动流) -->
  17 + <view class="main-card">
  18 + <!-- 个人信息头部 -->
  19 + <view class="profile-header">
  20 + <view class="avatar-box">
  21 + <image :src="avatar || DEFAULT_AVATAR" class="avatar-img" mode="aspectFill"></image>
37 </view> 22 </view>
  23 + <view class="user-meta">
  24 + <text class="nickname">{{ nickname || '运动达人' }}</text>
  25 + <view class="badge-tag">
  26 + <uni-icons type="fire-filled" size="12" color="#ff6600" />
  27 + <text class="badge-text">运动爱好者</text>
38 </view> 28 </view>
39 - <view class="right">  
40 - <view class="edit-btn" @click="openPopup">编辑</view>  
41 </view> 29 </view>
  30 + <view class="edit-btn" @click="openPopup">
  31 + <uni-icons type="compose" size="14" color="#ffffff" />
  32 + <text class="btn-text">编辑</text>
42 </view> 33 </view>
43 - <view class="signature" @click="goToEditSignature">  
44 - {{ signature ? signature : '点击这里,填写签名' }}  
45 </view> 34 </view>
46 - <view class="medal" v-if="medalList.length > 0">  
47 - <view class="title"> 勋章 </view>  
48 - <view class="medal-list">  
49 - <view class="medal-item" v-for="(item, index) in medalList" :key="index">  
50 - <image :src="item.image" class="img" mode="aspectFill"></image> 35 +
  36 + <!-- 个性签名 -->
  37 + <view class="signature-wrapper" @click="goToEditSignature">
  38 + <text class="signature-text">{{ signature || '点击这里,填写你的专属签名...' }}</text>
  39 + <uni-icons type="right" size="12" color="#c0c4cc" />
51 </view> 40 </view>
52 - <uni-icons  
53 - type="right"  
54 - size="24"  
55 - color="#999"  
56 - @click="uni.navigateTo({ url: '/pages5/pages/user/wode-xunzhang' })"  
57 - ></uni-icons> 41 +
  42 + <!-- 核心运动数据统计看板 -->
  43 + <view class="stats-card">
  44 + <view class="stat-item">
  45 + <view class="stat-num-box">
  46 + <text class="stat-num">{{ exerciseMinuteCount || 0 }}</text>
  47 + <text class="stat-unit">min</text>
58 </view> 48 </view>
  49 + <text class="stat-label">运动时长</text>
59 </view> 50 </view>
60 - <view class="trajectory">  
61 - <view class="header">  
62 - <view class="title"> 运动轨迹 </view>  
63 - <view class="data">  
64 - 在鸿星运动<text class="time">{{ exerciseMinuteCount }}</text  
65 - >min,消耗<text class="energy">{{ calorieCount }}</text  
66 - >kcal 51 +
67 </view> 52 </view>
  53 +
  54 + <!-- 运动轨迹打卡阵列 -->
  55 + <view class="trajectory-section">
  56 + <view class="section-header">
  57 + <text class="section-title">运动轨迹</text>
  58 + <text class="section-sub">记录坚持的每一个脚印</text>
68 </view> 59 </view>
69 - <view class="graph">  
70 - <!-- 点阵区域(基于motionMatrix渲染) -->  
71 - <view class="track-grid">  
72 - <!-- 星期标签(固定“一、三、五、日”) -->  
73 - <view class="week-labels">  
74 - <text  
75 - class="week-item"  
76 - v-for="(week, idx) in ['一', '二', '三', '四', '五', '六', '日']"  
77 - :key="idx"  
78 - >  
79 - {{ week !== '二' && week !== '六' && week !== '四' ? week : '' }} 60 +
  61 + <view class="track-grid-container">
  62 + <!-- 星期侧栏 -->
  63 + <view class="week-column">
  64 + <text class="week-label" v-for="(week, idx) in WEEK_LABELS" :key="idx">
  65 + {{ VISIBLE_WEEK_DAYS.includes(week) ? week : '' }}
80 </text> 66 </text>
81 </view> 67 </view>
82 - <view class="month-dots">  
83 - <!-- 月份标签(改用后端返回的motionXAxis) -->  
84 - <view class="month-tabs">  
85 - <text class="month-item" v-for="(month, idx) in motionXAxis" :key="idx"> 68 +
  69 + <!-- 右侧点阵与月份标题 -->
  70 + <view class="dots-area">
  71 + <!-- 月份横轴 -->
  72 + <view class="month-row">
  73 + <text class="month-label" v-for="(month, idx) in motionXAxis" :key="idx">
86 {{ month }} 74 {{ month }}
87 </text> 75 </text>
88 </view> 76 </view>
89 - <!-- 点阵(遍历motionMatrix渲染,false为灰色,可扩展true为彩色) -->  
90 - <view class="grid-dots"> 77 +
  78 + <!-- 7行打卡点阵 -->
  79 + <view class="grid-matrix">
91 <view class="dot-row" v-for="(row, rowIdx) in motionMatrix" :key="rowIdx"> 80 <view class="dot-row" v-for="(row, rowIdx) in motionMatrix" :key="rowIdx">
92 - <view  
93 - class="dot"  
94 - v-for="(isActive, colIdx) in row"  
95 - :key="colIdx"  
96 - :class="{ active: isActive }"  
97 - ></view> 81 + <view class="dot-item" v-for="(isActive, colIdx) in row" :key="colIdx" :class="{ active: isActive }">
98 </view> 82 </view>
99 </view> 83 </view>
100 </view> 84 </view>
@@ -102,319 +86,438 @@ @@ -102,319 +86,438 @@
102 </view> 86 </view>
103 </view> 87 </view>
104 </view> 88 </view>
105 - <!-- 弹框 -->  
106 - <uni-popup ref="popup" background-color="#fff" type="bottom">  
107 - <view class="action-popup">  
108 - <view class="popup-item" @click="goToEditProfile">编辑资料</view>  
109 - <view class="popup-item" @click="handleChangeBg">更换背景图</view>  
110 - <view class="popup-item cancel" @click="handleCancel">取消</view> 89 +
  90 + <!-- 底部操作弹窗 -->
  91 + <uni-popup ref="popup" background-color="transparent" type="bottom">
  92 + <view class="action-sheet">
  93 + <view class="sheet-group">
  94 + <view class="sheet-item" @click="goToEditProfile">编辑资料</view>
  95 + <view class="sheet-item" @click="handleChangeBg">更换背景图</view>
  96 + </view>
  97 + <view class="sheet-group cancel-group">
  98 + <view class="sheet-item cancel-item" @click="handleCancel">取消</view>
  99 + </view>
111 </view> 100 </view>
112 </uni-popup> 101 </uni-popup>
113 </view> 102 </view>
114 </template> 103 </template>
  104 +
115 <script setup> 105 <script setup>
116 - import { ref } from 'vue';  
117 - import UserApi from '@/sheep/api/member/user.js';  
118 - import { onShow } from '@dcloudio/uni-app';  
119 - import { getTopSafeArea } from '@/utils/safeArea'; 106 +import { ref } from 'vue';
  107 +import UserApi from '@/sheep/api/member/user.js';
  108 +import FileApi from '@/sheep/api/infra/file.js';
  109 +import { onShow } from '@dcloudio/uni-app';
  110 +import { getTopSafeArea } from '@/utils/safeArea';
120 111
121 - let topSafeArea = 0;  
122 - // #ifdef MP-WEIXIN  
123 - topSafeArea = getTopSafeArea();  
124 - // #endif  
125 - const popup = ref(null); // 定义弹窗引用 112 +// ==================== 常量定义 ====================
  113 +const DEFAULT_AVATAR =
  114 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png';
  115 +const DEFAULT_BG_IMAGE =
  116 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/健身模板_1784871730506.png';
  117 +const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日'];
  118 +const VISIBLE_WEEK_DAYS = ['一', '三', '五', '日']; // 运动轨迹仅显示一三五日
126 119
127 - const backgroundImage = ref(''); // 背景图片 120 +let topSafeArea = 0;
  121 +// #ifdef MP-WEIXIN
  122 +topSafeArea = getTopSafeArea();
  123 +// #endif
128 124
129 - const nickname = ref(''); // 昵称 125 +// ==================== 响应式状态 ====================
  126 +const popup = ref(null);
  127 +const backgroundImage = ref('');
  128 +const nickname = ref('');
  129 +const avatar = ref('');
  130 +const signature = ref('');
  131 +const exerciseMinuteCount = ref(0);
  132 +const calorieCount = ref(0);
  133 +const motionXAxis = ref([]);
  134 +const motionMatrix = ref([]);
  135 +const medalList = ref([]);
130 136
131 - const avatar = ref(''); // 头像  
132 - const signature = ref(''); // 签名  
133 - const exerciseMinuteCount = ref(0); // 运动分钟数  
134 - const calorieCount = ref(0); // 消耗卡路里  
135 - const motionXAxis = ref([]); // 运动轨迹X轴  
136 - const motionMatrix = ref([]); // 运动轨迹矩阵  
137 - const medalList = ref([]); // 奖牌列表  
138 - onShow(() => {  
139 - getMotionData();  
140 - });  
141 - // 返回上一页  
142 - const goBack = () => { 137 +// ==================== 生命周期 ====================
  138 +onShow(() => {
  139 + fetchUserProfile();
  140 +});
  141 +
  142 +// ==================== 数据获取 ====================
  143 +const fetchUserProfile = async () => {
  144 + try {
  145 + const { data } = await UserApi.getTrajectory();
  146 + nickname.value = data.nickname;
  147 + avatar.value = data.avatar;
  148 + signature.value = data.signature;
  149 + exerciseMinuteCount.value = data.exerciseMinuteCount;
  150 + calorieCount.value = data.calorieCount;
  151 + motionXAxis.value = data.motionXAxis || [];
  152 + motionMatrix.value = data.motionMatrix || [];
  153 + backgroundImage.value = data.backgroundImage;
  154 + medalList.value = data.medalList || [];
  155 + } catch (err) {
  156 + uni.showToast({ title: '数据加载失败', icon: 'none' });
  157 + }
  158 +};
  159 +
  160 +// ==================== 页面导航 ====================
  161 +const goBack = () => {
  162 + const pages = getCurrentPages();
  163 + if (pages.length > 1) {
143 uni.navigateBack(); 164 uni.navigateBack();
144 - };  
145 - // 打开弹窗  
146 - const openPopup = () => {  
147 - popup.value.open();  
148 - };  
149 - // 关闭弹窗  
150 - const handleCancel = () => { 165 + } else {
  166 + uni.reSwitch({ url: '/pages/index/index' });
  167 + }
  168 +};
  169 +
  170 +const goToEditProfile = () => {
151 popup.value.close(); 171 popup.value.close();
152 - };  
153 - // 获取运动轨迹数据  
154 - const getMotionData = async () => {  
155 - const res = await UserApi.getTrajectory();  
156 - nickname.value = res.data.nickname;  
157 - avatar.value = res.data.avatar;  
158 - signature.value = res.data.signature;  
159 - exerciseMinuteCount.value = res.data.exerciseMinuteCount;  
160 - calorieCount.value = res.data.calorieCount;  
161 - motionXAxis.value = res.data.motionXAxis;  
162 - motionMatrix.value = res.data.motionMatrix;  
163 - backgroundImage.value = res.data.backgroundImage;  
164 - medalList.value = res.data.medalList || [];  
165 - };  
166 - // 更换背景图  
167 - const handleChangeBg = () => { 172 + uni.navigateTo({ url: '/pages5/pages/user/wode-geren-ziliao' });
  173 +};
  174 +
  175 +const goToEditSignature = () => {
  176 + uni.navigateTo({
  177 + url: `/pages4/pages/xunji/xunji-wode-qianming?signature=${encodeURIComponent(signature.value || '')}`,
  178 + });
  179 +};
  180 +
  181 +// ==================== 弹窗控制 ====================
  182 +const openPopup = () => {
  183 + popup.value.open();
  184 +};
  185 +
  186 +const handleCancel = () => {
168 popup.value.close(); 187 popup.value.close();
169 - // 调用选择图片接口 188 +};
  189 +
  190 +// ==================== 图片更换交互 ====================
  191 +const handleChangeBg = async () => {
  192 + let tempFilePath = '';
  193 + try {
  194 + // #ifdef MP-WEIXIN
  195 + const wxRes = await new Promise((resolve, reject) => {
170 uni.chooseMedia({ 196 uni.chooseMedia({
171 - count: 1, // 仅选1张背景图  
172 - mediaType: ['image'], // 只选图片  
173 - success: (res) => {  
174 - // 选择成功,res.tempFiles是选中的文件数组  
175 -  
176 - UserApi.updateBgImage({  
177 - data: res.tempFiles[0].tempFilePath,  
178 - }).then(() => {  
179 - getMotionData(); 197 + count: 1,
  198 + mediaType: ['image'],
  199 + success: resolve,
  200 + fail: reject,
180 }); 201 });
181 - },  
182 - fail: (err) => {  
183 - // 选择失败(如用户取消、权限不足)  
184 - uni.showToast({  
185 - title: '选择图片失败',  
186 - icon: 'none',  
187 }); 202 });
188 - }, 203 + tempFilePath = wxRes.tempFiles[0].tempFilePath;
  204 + // #endif
  205 +
  206 + // #ifndef MP-WEIXIN
  207 + const h5Res = await new Promise((resolve, reject) => {
  208 + uni.chooseImage({
  209 + count: 1,
  210 + sizeType: ['compressed'],
  211 + sourceType: ['album', 'camera'],
  212 + success: resolve,
  213 + fail: reject,
  214 + });
189 }); 215 });
190 - };  
191 - //  
192 - const goToEditProfile = () => { 216 + tempFilePath = h5Res.tempFilePaths[0];
  217 + // #endif
  218 +
193 popup.value.close(); 219 popup.value.close();
194 - uni.navigateTo({ url: '/pages5/pages/user/wode-geren-ziliao' });  
195 - };  
196 - // 跳转编辑签名  
197 - const goToEditSignature = () => {  
198 - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-wode-qianming?signature=' + signature.value });  
199 - }; 220 + uni.showLoading({ title: '上传中...' });
  221 + const uploadRes = await FileApi.uploadFile(tempFilePath);
  222 + uni.hideLoading();
  223 +
  224 + if (!uploadRes || !uploadRes.data) {
  225 + uni.showToast({ title: '上传失败', icon: 'none' });
  226 + return;
  227 + }
  228 +
  229 + await UserApi.updateBgImage({ data: uploadRes.data });
  230 + await fetchUserProfile();
  231 + uni.showToast({ title: '背景更换成功', icon: 'success' });
  232 + } catch (err) {
  233 + popup.value.close();
  234 + uni.hideLoading();
  235 + if (err?.errMsg?.includes('cancel')) return;
  236 + uni.showToast({ title: '更换背景失败', icon: 'none' });
  237 + }
  238 +};
200 </script> 239 </script>
201 240
202 <style lang="scss" scoped> 241 <style lang="scss" scoped>
203 - .container {  
204 - width: 100%;  
205 - height: 100%; 242 +.container {
  243 + min-height: 100vh;
  244 + background-color: #f7f8fa;
  245 + position: relative;
  246 +}
206 247
207 - .backgroundImage {  
208 - width: 100%;  
209 - height: 350rpx;  
210 - position: absolute; 248 +/* 顶部固定导航按钮 */
  249 +.header-nav {
  250 + position: fixed;
211 top: 0; 251 top: 0;
212 - background-color: bisque;  
213 - .img { 252 + left: 0;
  253 + right: 0;
  254 + z-index: 100;
  255 + padding-left: 24rpx;
  256 +
  257 + .nav-back-btn {
  258 + width: 64rpx;
  259 + height: 64rpx;
  260 + border-radius: 50%;
  261 + background-color: rgba(0, 0, 0, 0.3);
  262 + backdrop-filter: blur(8px);
  263 + display: flex;
  264 + align-items: center;
  265 + justify-content: center;
  266 + margin-top: 12rpx;
  267 + }
  268 +}
  269 +
  270 +/* 顶部背景图容器 */
  271 +.banner-wrapper {
  272 + width: 100%;
  273 + height: 420rpx;
  274 + position: relative;
  275 +
  276 + .banner-img {
214 width: 100%; 277 width: 100%;
215 height: 100%; 278 height: 100%;
216 } 279 }
217 - }  
218 - .content { 280 +
  281 + .banner-mask {
219 position: absolute; 282 position: absolute;
220 - top: 320rpx;  
221 - border-top-left-radius: 15px;  
222 - border-top-right-radius: 15px;  
223 - padding: 0 20rpx;  
224 - background-color: #fff;  
225 - width: 100%;  
226 - box-sizing: border-box;  
227 - height: calc(100% - 320rpx);  
228 - .info {  
229 - display: flex;  
230 - justify-content: space-between;  
231 - align-items: center; 283 + bottom: 0;
  284 + left: 0;
  285 + right: 0;
  286 + height: 120rpx;
  287 + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
  288 + }
  289 +}
  290 +
  291 +/* 主体内容卡片(自然流向上覆盖) */
  292 +.main-card {
232 position: relative; 293 position: relative;
233 - margin-bottom: 130rpx;  
234 - .left {  
235 - height: 140rpx; 294 + margin-top: -60rpx;
  295 + background-color: #ffffff;
  296 + border-top-left-radius: 36rpx;
  297 + border-top-right-radius: 36rpx;
  298 + padding: 32rpx 28rpx 60rpx;
  299 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.04);
  300 +
  301 + /* 个人信息 */
  302 + .profile-header {
236 display: flex; 303 display: flex;
237 - box-sizing: border-box;  
238 - align-items: center;  
239 - position: absolute;  
240 - top: -20rpx; 304 + align-items: flex-end;
  305 + margin-bottom: 24rpx;
241 306
242 - .avatar {  
243 - width: 140rpx;  
244 - height: 140rpx; 307 + .avatar-box {
  308 + width: 130rpx;
  309 + height: 130rpx;
245 border-radius: 50%; 310 border-radius: 50%;
  311 + border: 6rpx solid #ffffff;
  312 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
  313 + margin-top: -70rpx;
  314 + margin-right: 20rpx;
  315 + flex-shrink: 0;
246 overflow: hidden; 316 overflow: hidden;
247 - background: #fff;  
248 - padding: 8rpx;  
249 - box-sizing: border-box;  
250 - margin-right: 15rpx;  
251 - .img { 317 + background-color: #ffffff;
  318 +
  319 + .avatar-img {
252 width: 100%; 320 width: 100%;
253 height: 100%; 321 height: 100%;
254 - border-radius: 50%;  
255 } 322 }
256 } 323 }
257 - .user-info { 324 +
  325 + .user-meta {
  326 + flex: 1;
  327 +
258 .nickname { 328 .nickname {
259 - font-size: 28rpx;  
260 - font-weight: 500;  
261 - color: #333333; 329 + font-size: 32rpx;
  330 + font-weight: 700;
  331 + color: #1d2129;
  332 + line-height: 44rpx;
  333 + display: block;
262 margin-bottom: 8rpx; 334 margin-bottom: 8rpx;
263 } 335 }
264 - .user-tip {  
265 - background-color: #f7c29f;  
266 - padding: 5rpx 8rpx;  
267 - border-radius: 5rpx; 336 +
  337 + .badge-tag {
  338 + display: inline-flex;
  339 + align-items: center;
  340 + background-color: #fff0e6;
  341 + padding: 4rpx 12rpx;
  342 + border-radius: 20rpx;
  343 +
  344 + .badge-text {
268 font-size: 20rpx; 345 font-size: 20rpx;
269 - font-weight: 400;  
270 - color: #a15624; 346 + font-weight: 600;
  347 + color: #ff6600;
  348 + margin-left: 4rpx;
271 } 349 }
272 } 350 }
273 } 351 }
274 - .right {  
275 - height: 140rpx; 352 +
  353 + .edit-btn {
276 display: flex; 354 display: flex;
277 - box-sizing: border-box;  
278 align-items: center; 355 align-items: center;
279 - position: absolute;  
280 - top: -20rpx;  
281 - right: 0;  
282 - .edit-btn {  
283 - background-color: #e68f55;  
284 - padding: 10rpx 20rpx;  
285 - border-radius: 5rpx; 356 + background: linear-gradient(135deg, #ff8533 0%, #ff6600 100%);
  357 + padding: 12rpx 24rpx;
  358 + border-radius: 30rpx;
  359 + box-shadow: 0 4rpx 12rpx rgba(255, 102, 0, 0.25);
  360 +
  361 + .btn-text {
286 font-size: 24rpx; 362 font-size: 24rpx;
287 - font-weight: 400;  
288 - color: #fff;  
289 - } 363 + color: #ffffff;
  364 + font-weight: 500;
  365 + margin-left: 4rpx;
290 } 366 }
291 } 367 }
292 - .medal {  
293 - width: 100%;  
294 - margin-bottom: 40rpx;  
295 - .title {  
296 - font-size: 30rpx;  
297 - font-weight: 550;  
298 - color: #333333;  
299 - margin-bottom: 20rpx;  
300 } 368 }
301 - .medal-list { 369 +
  370 + /* 签名框 */
  371 + .signature-wrapper {
302 display: flex; 372 display: flex;
303 - justify-content: space-between;  
304 align-items: center; 373 align-items: center;
  374 + justify-content: space-between;
  375 + background-color: #f7f8fa;
  376 + padding: 16rpx 20rpx;
  377 + border-radius: 12rpx;
  378 + margin-bottom: 32rpx;
305 379
306 - .medal-item {  
307 - width: 90%;  
308 - gap: 30rpx; 380 + .signature-text {
  381 + font-size: 24rpx;
  382 + color: #86909c;
  383 + flex: 1;
  384 + margin-right: 12rpx;
  385 + overflow: hidden;
  386 + text-overflow: ellipsis;
  387 + white-space: nowrap;
  388 + }
  389 + }
309 390
310 - background: #fff;  
311 - padding: 8rpx;  
312 - box-sizing: border-box; 391 + /* 运动统计看板 */
  392 + .stats-card {
313 display: flex; 393 display: flex;
314 align-items: center; 394 align-items: center;
315 - .img {  
316 - width: 100rpx;  
317 - height: 100rpx;  
318 - border-radius: 50%; 395 + background: linear-gradient(135deg, #2b303d 0%, #1d2129 100%);
  396 + border-radius: 20rpx;
  397 + padding: 32rpx 24rpx;
  398 + margin-bottom: 40rpx;
  399 + box-shadow: 0 8rpx 20rpx rgba(29, 33, 41, 0.12);
  400 +
  401 + .stat-item {
  402 + flex: 1;
  403 + display: flex;
  404 + flex-direction: column;
  405 + align-items: center;
  406 +
  407 + .stat-num-box {
  408 + display: flex;
  409 + align-items: baseline;
  410 +
  411 + .stat-num {
  412 + font-size: 44rpx;
  413 + font-weight: 800;
  414 + color: #ffffff;
  415 + font-family: 'DIN Alternate', sans-serif;
319 } 416 }
  417 +
  418 + .stat-unit {
  419 + font-size: 22rpx;
  420 + color: #ff9e59;
  421 + margin-left: 6rpx;
  422 + font-weight: 600;
320 } 423 }
321 } 424 }
  425 +
  426 + .stat-label {
  427 + font-size: 22rpx;
  428 + color: #a4a9b3;
  429 + margin-top: 6rpx;
322 } 430 }
323 - .signature {  
324 - font-size: 20rpx;  
325 - font-weight: 400;  
326 - color: #999999;  
327 - margin-top: 20rpx;  
328 - margin-bottom: 80rpx;  
329 } 431 }
330 - .trajectory {  
331 - .header {  
332 - display: flex;  
333 - justify-content: space-between;  
334 - align-items: center;  
335 - .title {  
336 - font-size: 28rpx;  
337 - font-weight: 500;  
338 - color: #333333; 432 +
  433 + .stat-divider {
  434 + width: 2rpx;
  435 + height: 48rpx;
  436 + background-color: rgba(255, 255, 255, 0.15);
339 } 437 }
340 - .data {  
341 - font-size: 20rpx;  
342 - font-weight: 400;  
343 - color: #999999;  
344 - .time,  
345 - .energy {  
346 - margin: 0 5rpx;  
347 - font-size: 24rpx;  
348 - font-weight: 500;  
349 - color: rgb(84, 198, 233);  
350 } 438 }
  439 +
  440 + /* 运动轨迹区块 */
  441 + .trajectory-section {
  442 + background-color: #ffffff;
  443 +
  444 + .section-header {
  445 + margin-bottom: 24rpx;
  446 +
  447 + .section-title {
  448 + font-size: 30rpx;
  449 + font-weight: 700;
  450 + color: #1d2129;
  451 + margin-right: 12rpx;
  452 + }
  453 +
  454 + .section-sub {
  455 + font-size: 22rpx;
  456 + color: #86909c;
351 } 457 }
352 } 458 }
353 - .graph {  
354 - padding: 10rpx 5rpx;  
355 459
356 - // 星期标签和点阵容器  
357 - .track-grid { 460 + /* 点阵网格系统 */
  461 + .track-grid-container {
358 display: flex; 462 display: flex;
359 align-items: flex-start; 463 align-items: flex-start;
360 - padding: 0 8rpx;  
361 - .week-labels { 464 + background-color: #fafafa;
  465 + border-radius: 16rpx;
  466 + padding: 20rpx 16rpx;
  467 + border: 1rpx solid #f2f3f5;
  468 +
  469 + /* 星期标签列 */
  470 + .week-column {
362 display: flex; 471 display: flex;
363 flex-direction: column; 472 flex-direction: column;
364 - margin-right: 18rpx;  
365 - padding-top: 48rpx;  
366 - .week-item {  
367 - font-size: 20rpx;  
368 - color: #999999;  
369 - height: 40rpx;  
370 - width: 20rpx; 473 + justify-content: space-between;
  474 + margin-right: 16rpx;
  475 + padding-top: 36rpx; // 避开月份行的高度
  476 + height: 180rpx; // 匹配 7 行点阵的高
  477 +
  478 + .week-label {
  479 + font-size: 18rpx;
  480 + color: #c0c4cc;
  481 + height: 20rpx;
  482 + line-height: 20rpx;
371 text-align: center; 483 text-align: center;
372 } 484 }
373 } 485 }
374 - .month-dots {  
375 - // 月份标签样式  
376 - .month-tabs { 486 +
  487 + /* 点阵与月份区 */
  488 + .dots-area {
  489 + flex: 1;
  490 +
  491 + .month-row {
377 display: flex; 492 display: flex;
378 justify-content: space-between; 493 justify-content: space-between;
  494 + margin-bottom: 12rpx;
379 495
380 - margin-bottom: 20rpx;  
381 - .month-item {  
382 - width: calc(100% / 6);  
383 - font-size: 22rpx;  
384 - color: #999999;  
385 - text-align: start;  
386 - line-height: 30rpx;  
387 - height: 30rpx;  
388 - }  
389 - } 496 + .month-label {
  497 + font-size: 20rpx;
  498 + color: #86909c;
390 } 499 }
391 } 500 }
392 501
393 - // 点阵样式  
394 - .grid-dots {  
395 - flex: 1; 502 + .grid-matrix {
396 display: flex; 503 display: flex;
397 flex-direction: column; 504 flex-direction: column;
398 - align-items: flex-start; 505 + gap: 8rpx;
  506 +
399 .dot-row { 507 .dot-row {
400 display: flex; 508 display: flex;
401 - margin-bottom: 11rpx;  
402 - align-items: center;  
403 - height: 30rpx;  
404 - .dot {  
405 - // 原无运动样式  
406 - width: 15rpx;  
407 - height: 15rpx;  
408 - border-radius: 50%;  
409 - background: #f8f8f8; // 无运动:浅灰  
410 - margin-right: 11rpx;  
411 - border: 1rpx solid #e5e5e5;  
412 - box-sizing: border-box; 509 + justify-content: space-between;
  510 +
  511 + .dot-item {
  512 + width: 18rpx;
  513 + height: 18rpx;
  514 + border-radius: 4rpx;
  515 + background-color: #ebedf0;
  516 + transition: all 0.2s ease;
413 517
414 - // 有运动样式(可自定义颜色)  
415 &.active { 518 &.active {
416 - background: #409eff; // 有运动:蓝色(示例)  
417 - border: none; 519 + background-color: #ff6600;
  520 + box-shadow: 0 2rpx 8rpx rgba(255, 102, 0, 0.35);
418 } 521 }
419 } 522 }
420 } 523 }
@@ -422,25 +525,42 @@ @@ -422,25 +525,42 @@
422 } 525 }
423 } 526 }
424 } 527 }
425 - // 底部操作弹窗  
426 - .action-popup {  
427 - background: #fff;  
428 - border-top-left-radius: 24rpx;  
429 - border-top-right-radius: 24rpx;  
430 - padding: 24rpx 0; 528 +}
431 529
432 - .popup-item {  
433 - font-size: 32rpx;  
434 - color: #333; 530 +/* 底部操作弹窗 ActionSheet */
  531 +.action-sheet {
  532 + padding: 0 20rpx 40rpx;
  533 +
  534 + .sheet-group {
  535 + background-color: #ffffff;
  536 + border-radius: 24rpx;
  537 + overflow: hidden;
  538 + margin-bottom: 16rpx;
  539 +
  540 + .sheet-item {
  541 + font-size: 30rpx;
  542 + color: #1d2129;
435 text-align: center; 543 text-align: center;
436 - padding: 32rpx 0;  
437 - border-bottom: 1px solid #f5f5f5; 544 + padding: 30rpx 0;
  545 + border-bottom: 1rpx solid #f2f3f5;
438 546
439 - &.cancel {  
440 - color: #ff4d4f; 547 + &:last-child {
441 border-bottom: none; 548 border-bottom: none;
442 } 549 }
  550 +
  551 + &:active {
  552 + background-color: #f7f8fa;
  553 + }
  554 + }
  555 +
  556 + &.cancel-group {
  557 + margin-bottom: 0;
  558 +
  559 + .cancel-item {
  560 + color: #ff4d4f;
  561 + font-weight: 600;
443 } 562 }
444 } 563 }
445 } 564 }
  565 +}
446 </style> 566 </style>
@@ -4,17 +4,31 @@ @@ -4,17 +4,31 @@
4 <uni-nav-bar :title="plandetail.name" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" /> 4 <uni-nav-bar :title="plandetail.name" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" />
5 <!-- Banner 区域 --> 5 <!-- Banner 区域 -->
6 <view class="banner-section"> 6 <view class="banner-section">
7 - <image :src="plandetail.urlCover" class="banner-img"> 7 + <image :src="plandetail.urlCover" class="banner-img" />
8 <!-- 收藏 --> 8 <!-- 收藏 -->
9 - <uni-icons :type="isFavorite ? 'heart-filled' : 'heart'" size="24" color="#fff" class="collect-btn"  
10 - @click="toggleAddPlan">  
11 - </uni-icons>  
12 - <!-- 分享按钮 -->  
13 - <button class="share-btn" open-type="share">  
14 - <uni-icons type="paperplane" size="24" color="#fff"></uni-icons> 9 + <view
  10 + class="banner-btn collect-btn"
  11 + :class="{ 'is-fav': isFavorite }"
  12 + @click="toggleAddPlan"
  13 + >
  14 + <up-icon
  15 + :name="isFavorite ? 'heart-fill' : 'heart'"
  16 + size="22"
  17 + :color="isFavorite ? '#ffd700' : '#fff'"
  18 + />
  19 + </view>
  20 + <!-- 分享按钮:微信端用 open-type="share",H5 端用普通按钮 -->
  21 + <!-- #ifdef MP-WEIXIN -->
  22 + <button class="banner-btn share-btn" open-type="share">
  23 + <up-icon name="share" size="22" color="#fff" />
15 </button> 24 </button>
16 - </image>  
17 - <!-- <view class="hot-tag">火爆</view> --> 25 + <!-- #endif -->
  26 + <!-- #ifndef MP-WEIXIN -->
  27 + <view class="banner-btn share-btn" @click="handleShare">
  28 + <up-icon name="share" size="22" color="#fff" />
  29 + </view>
  30 + <!-- #endif -->
  31 +
18 </view> 32 </view>
19 <!-- 计划信息卡片 --> 33 <!-- 计划信息卡片 -->
20 <view class="info-cards"> 34 <view class="info-cards">
@@ -45,40 +59,44 @@ @@ -45,40 +59,44 @@
45 59
46 <!-- 器械标签 --> 60 <!-- 器械标签 -->
47 <view class="equipment-tags"> 61 <view class="equipment-tags">
48 - <up-icon name="file-text" color="#999" size="25"></up-icon> 62 + <view class="equipment-label">
  63 + <up-icon name="tags" color="#999" size="22"></up-icon>
49 <text>器械</text> 64 <text>器械</text>
50 - <view v-for="items in plandetail.equipmentsSummaryNames" :key="items">  
51 - <view class="tag-item">  
52 - <text>{{ items }}</text>  
53 </view> 65 </view>
  66 + <view class="tag-item" v-for="items in plandetail.equipmentsSummaryNames" :key="items">
  67 + <text>{{ items }}</text>
54 </view> 68 </view>
55 </view> 69 </view>
56 70
57 - <!-- ==========新增:未来7天训练日日历模块 start========== --> 71 + <!-- 未来7天训练日日历模块 -->
58 <view class="calendar-section" v-if="isMyPlan === true"> 72 <view class="calendar-section" v-if="isMyPlan === true">
59 <view class="cal-header"> 73 <view class="cal-header">
  74 + <view class="cal-title-row">
  75 + <up-icon name="calendar" size="22" color="#e9ee50" />
60 <text class="cal-title">训练日</text> 76 <text class="cal-title">训练日</text>
61 - <button class="cal-set-btn" @click="openArrageClass">排课设置</button>  
62 </view> 77 </view>
63 - <!-- 星期日期头部 -->  
64 - <view class="cal-week-row">  
65 - <view v-for="(item, idx) in calWeekList" :key="idx" class="cal-day-item">  
66 - <text class="day-top">{{ item.topText }}</text>  
67 - <text class="day-num">{{ item.num }}</text> 78 + <view class="cal-set-btn" @click="openArrageClass">排课设置</view>
68 </view> 79 </view>
  80 + <!-- 7天日历 -->
  81 + <view class="cal-grid">
  82 + <view v-for="(item, idx) in calWeekList" :key="idx" class="cal-col" :class="{ 'is-today': idx === 0 }">
  83 + <!-- 星期 + 日期 -->
  84 + <view class="cal-date">
  85 + <text class="day-week">{{ item.topText }}</text>
  86 + <text class="day-num">{{ item.num }}</text>
69 </view> 87 </view>
70 - <!-- 下方课程/休息 -->  
71 - <view class="cal-content-row">  
72 - <view v-for="(item, idx) in calWeekList" :key="idx" class="cal-day-item">  
73 - <!-- 有训练就展示黄色课程,没有就是休 -->  
74 - <view v-if="item.trainName" class="train-tag" @click="openTemplateDatil(item.dailytemplateId, item.date)"> 88 + <!-- 课程/休息标签 -->
  89 + <view
  90 + v-if="item.trainName"
  91 + class="train-tag"
  92 + @click="openTemplateDatil(item.dailytemplateId, item.date)"
  93 + >
75 <text class="tag-text">{{ item.trainName }}</text> 94 <text class="tag-text">{{ item.trainName }}</text>
76 </view> 95 </view>
77 <view v-else class="rest-tag">休</view> 96 <view v-else class="rest-tag">休</view>
78 </view> 97 </view>
79 </view> 98 </view>
80 </view> 99 </view>
81 - <!-- ==========新增:未来7天训练日日历模块 end========== -->  
82 100
83 <!-- 计划课程(模板) --> 101 <!-- 计划课程(模板) -->
84 <view class="course-section"> 102 <view class="course-section">
@@ -95,20 +113,17 @@ @@ -95,20 +113,17 @@
95 <text class="meta-item">{{ item.totalSets }} 组</text> 113 <text class="meta-item">{{ item.totalSets }} 组</text>
96 <text class="meta-item">{{ item.totalWeight }}kg</text> 114 <text class="meta-item">{{ item.totalWeight }}kg</text>
97 </view> 115 </view>
98 - <uni-icons uni-icons type="right" size="10" color="#fff"></uni-icons> 116 + <up-icon name="arrow-right" size="10" color="#fff"></up-icon>
99 </view> 117 </view>
100 <text v-for="(items, index) in item.primaryMuscleNames" :key="index"> 118 <text v-for="(items, index) in item.primaryMuscleNames" :key="index">
101 <text class="course-parts">{{ items }}</text> 119 <text class="course-parts">{{ items }}</text>
102 </text> 120 </text>
103 121
104 <view class="course-actions"> 122 <view class="course-actions">
105 - <button type="default" size="mini" class="btn"  
106 - style="background-color: #333; color: white; border-radius: 20rpx" 123 + <button type="default" size="mini" class="btn btn-dark"
107 @click.stop="openCalendarPopup(plandetail.id, item.id)">添加到日历</button> 124 @click.stop="openCalendarPopup(plandetail.id, item.id)">添加到日历</button>
108 125
109 - <button type="primary" size="mini" class="btn"  
110 - style="background-color: #ffd700; color: black; border-radius: 20rpx"  
111 - @click.stop="startTraining(item.id)"> 126 + <button type="primary" size="mini" class="btn btn-gold" @click.stop="startTraining(item.id)">
112 <text>去训练</text> 127 <text>去训练</text>
113 <view class="go-text">GO</view> 128 <view class="go-text">GO</view>
114 </button> 129 </button>
@@ -128,38 +143,32 @@ @@ -128,38 +143,32 @@
128 <view class="button-group"> 143 <view class="button-group">
129 <template v-if="!isPlanAdded && !isMyPlan"> 144 <template v-if="!isPlanAdded && !isMyPlan">
130 <!-- 官方计划的底部按钮(未添加) --> 145 <!-- 官方计划的底部按钮(未添加) -->
131 - <button type="default" @click="openTrainingPopup" class="btn" size="large"  
132 - style="background-color: white; flex: 1"> 146 + <button type="default" @click="openTrainingPopup" class="btn end" size="large">
133 单节课程训练 147 单节课程训练
134 </button> 148 </button>
135 - <button type="default" @click="openChooseWeekPopup" class="btn" size="large"  
136 - style="background-color: #ffd700; flex: 1.5"> 149 + <button type="default" @click="openChooseWeekPopup" class="btn ones" size="large">
137 加入计划 150 加入计划
138 </button> 151 </button>
139 </template> 152 </template>
140 <!-- 官方计划已添加 --> 153 <!-- 官方计划已添加 -->
141 <template v-else-if="isPlanAdded && !isMyPlan"> 154 <template v-else-if="isPlanAdded && !isMyPlan">
142 - <button type="default" @click.stop="confirmDelete(plandetail.id)" class="btn end" size="large"  
143 - style="background-color: white; flex: 1"> 155 + <button type="default" @click.stop="confirmDelete(plandetail.id)" class="btn end" size="large">
144 结束计划 156 结束计划
145 </button> 157 </button>
146 - <button type="default" @click="openTrainingPopup" class="btn ones" size="large"  
147 - style="background-color: #ffd700; flex: 1.5"> 158 + <button type="default" @click="openTrainingPopup" class="btn ones" size="large">
148 马上开练 159 马上开练
149 </button> 160 </button>
150 </template> 161 </template>
151 <!-- 个人计划 --> 162 <!-- 个人计划 -->
152 <template v-else> 163 <template v-else>
153 - <button type="default" class="btn end" @click="openTrainingPopup" size="large"  
154 - style="background-color: white; flex: 1"> 164 + <button type="default" class="btn end" @click="openTrainingPopup" size="large">
155 马上开练 165 马上开练
156 </button> 166 </button>
157 - <button type="default" @click="openArrageClass" class="btn ones" size="large"  
158 - style="background-color: #ffd700; flex: 1.5"> 167 + <button type="default" @click="openArrageClass" class="btn ones" size="large">
159 更改排课 168 更改排课
160 </button> 169 </button>
161 <view class="Icon" @click="showMorePopup = true"> 170 <view class="Icon" @click="showMorePopup = true">
162 - <uni-icons type="more-filled" size="22" color="#fff" /> 171 + <up-icon name="more-dot-fill" size="22" color="#fff" />
163 更多 172 更多
164 </view> 173 </view>
165 <!-- 更多弹窗(原生 view 实现,精准定位) --> 174 <!-- 更多弹窗(原生 view 实现,精准定位) -->
@@ -167,12 +176,12 @@ @@ -167,12 +176,12 @@
167 <!-- 弹窗本体:精准定位在「更多」按钮上方 --> 176 <!-- 弹窗本体:精准定位在「更多」按钮上方 -->
168 <view class="more-popup" @click.stop> 177 <view class="more-popup" @click.stop>
169 <view class="more-item" @click="restartPlan"> 178 <view class="more-item" @click="restartPlan">
170 - <uni-icons type="refresh" size="22" color="#fff"></uni-icons> 179 + <up-icon name="reload" size="22" color="#fff"></up-icon>
171 <text class="item-text">重新开始计划</text> 180 <text class="item-text">重新开始计划</text>
172 </view> 181 </view>
173 <!-- 注意这里结束整个计划,用的每日模板的删除个人计划接口,传递的是官方计划planId --> 182 <!-- 注意这里结束整个计划,用的每日模板的删除个人计划接口,传递的是官方计划planId -->
174 <view class="more-item" @click.stop="confirmDelete(plandetail.planId)"> 183 <view class="more-item" @click.stop="confirmDelete(plandetail.planId)">
175 - <uni-icons type="close" size="22" color="#fff"></uni-icons> 184 + <up-icon name="close" size="22" color="#fff"></up-icon>
176 <text class="item-text">结束整个计划</text> 185 <text class="item-text">结束整个计划</text>
177 </view> 186 </view>
178 </view> 187 </view>
@@ -180,11 +189,11 @@ @@ -180,11 +189,11 @@
180 </template> 189 </template>
181 </view> 190 </view>
182 <!-- 加入计划/重新开始计划的弹窗 --> 191 <!-- 加入计划/重新开始计划的弹窗 -->
183 - <WeekSelectPopup v-model:visible="showWeekPopup" :max-count="plandetail.frequencyPerWeek" :plan-detail="plandetail" 192 + <WeekSelectPopup v-model:visible="showWeekPopup" :max-count="plandetail.frequencyPerWeek || 7" :plan-detail="plandetail"
184 @success="loadDetailUpdate" :is-update="isUpdate" /> 193 @success="loadDetailUpdate" :is-update="isUpdate" />
185 194
186 <!-- 添加到日历弹窗子组件 --> 195 <!-- 添加到日历弹窗子组件 -->
187 - <AddToCalendarPopup ref="calendarPopupRef" :plan-id="plandetail.id" :template-id="currentTemplateId" 196 + <AddToCalendarPopup ref="calendarPopupRef" :plan-id="plandetail.id || ''" :template-id="currentTemplateId"
188 @success="handleCalendarSuccess" /> 197 @success="handleCalendarSuccess" />
189 198
190 <!-- 去训练弹窗,显示当前计划的所有模板,可以直接跳转到训练页面,区分个人计划和官方计划(传递的是官方的计划id) --> 199 <!-- 去训练弹窗,显示当前计划的所有模板,可以直接跳转到训练页面,区分个人计划和官方计划(传递的是官方的计划id) -->
@@ -197,23 +206,18 @@ @@ -197,23 +206,18 @@
197 </template> 206 </template>
198 207
199 <script setup> 208 <script setup>
200 -import { onMounted, ref } from 'vue';  
201 -import { getBottomSafeArea } from '@/utils/safeArea.js'; 209 +import { onMounted, onUnmounted, ref } from 'vue';
202 import QueryPlanApi from '@/sheep/api/plan/queryplan'; 210 import QueryPlanApi from '@/sheep/api/plan/queryplan';
203 import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'; 211 import { onLoad, onShareAppMessage } from '@dcloudio/uni-app';
204 import AddToCalendarPopup from '@/pages/xunji/components/tianjia-dao-rili.vue'; 212 import AddToCalendarPopup from '@/pages/xunji/components/tianjia-dao-rili.vue';
205 import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue' 213 import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue'
206 import WeekSelectPopup from '@/pages4/components/week-select-popup.vue' 214 import WeekSelectPopup from '@/pages4/components/week-select-popup.vue'
207 import { useTrainingStore } from '@/sheep/store/trainingStore' 215 import { useTrainingStore } from '@/sheep/store/trainingStore'
  216 +import dayjs from 'dayjs';
208 217
209 const trainingStore = useTrainingStore() 218 const trainingStore = useTrainingStore()
210 219
211 // 获取页面参数 220 // 获取页面参数
212 -const route = defineProps(['planid']); // Vue 3 + uni-app 支持  
213 -// 或者使用 getCurrentPages()(兼容性更好)  
214 -const pages = getCurrentPages();  
215 -const currentPage = pages[pages.length - 1];  
216 -const planIdFromUrl = currentPage.options?.planid;  
217 221
218 // const plandetail = ref([]); 222 // const plandetail = ref([]);
219 const plandetail = ref({}); 223 const plandetail = ref({});
@@ -224,17 +228,8 @@ const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' }; @@ -224,17 +228,8 @@ const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };
224 const showWeekPopup = ref(false) 228 const showWeekPopup = ref(false)
225 // 用户选择的星期:[1,3] 代表周一、周三 229 // 用户选择的星期:[1,3] 代表周一、周三
226 // const selectedWeekList = ref([]) 230 // const selectedWeekList = ref([])
227 -// 星期列表(用于弹窗显示)  
228 -// 星期列表(匹配例图:周一/二/三 + 下方“休”字)  
229 -const weekOptions = [  
230 - { shortName: '一', fullName: '周一', value: 1 },  
231 - { shortName: '二', fullName: '周二', value: 2 },  
232 - { shortName: '三', fullName: '周三', value: 3 },  
233 - { shortName: '四', fullName: '周四', value: 4 },  
234 - { shortName: '五', fullName: '周五', value: 5 },  
235 - { shortName: '六', fullName: '周六', value: 6 },  
236 - { shortName: '日', fullName: '周日', value: 7 },  
237 -] 231 +// 星期映射(dayjs day() → 中文星期)
  232 +const WEEK_DAY_MAP = ['日', '一', '二', '三', '四', '五', '六'];
238 233
239 const isUpdate = ref(false) 234 const isUpdate = ref(false)
240 235
@@ -243,36 +238,24 @@ const calWeekList = ref([]) @@ -243,36 +238,24 @@ const calWeekList = ref([])
243 // 「添加到日历」弹窗实例 238 // 「添加到日历」弹窗实例
244 const calendarPopupRef = ref(null); 239 const calendarPopupRef = ref(null);
245 // 当前要添加到日历的课程模板ID 240 // 当前要添加到日历的课程模板ID
246 -const currentTemplateId = ref(null); 241 +const currentTemplateId = ref('');
247 242
248 const showPlanPopup = ref(false) 243 const showPlanPopup = ref(false)
249 244
250 // 更多弹窗显隐 245 // 更多弹窗显隐
251 const showMorePopup = ref(false) 246 const showMorePopup = ref(false)
252 247
253 -// 接口weeklySchedule转日历渲染数据(修复版:星期根据真实日期计算,100%对齐num) 248 +/** 接口 weeklySchedule 转日历渲染数据(使用 dayjs 计算星期) */
254 const formatWeekData = (weekArr) => { 249 const formatWeekData = (weekArr) => {
255 - // JS星期:0=日,1=一,2=二,3=三,4=四,5=五,6=六  
256 - const weekMap = ['日', '一', '二', '三', '四', '五', '六'];  
257 -  
258 return weekArr.map((item, index) => { 250 return weekArr.map((item, index) => {
259 - // 日期数字  
260 - const dayNum = item.date[2];  
261 - // 核心修复:用真实日期计算星期几,不再用dayIndex!  
262 const [y, m, d] = item.date; 251 const [y, m, d] = item.date;
263 - const realDate = new Date(y, m - 1, d);  
264 - const realWeekText = weekMap[realDate.getDay()];  
265 -  
266 - // 第一天显示「今」,其他显示真实星期  
267 - let topStr = index === 0 ? '今' : realWeekText;  
268 -  
269 - // 训练名称  
270 - const trainStr = item.scheduled ? item.templateName : ''; 252 + const dateObj = dayjs(`${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`);
  253 + const realWeekText = WEEK_DAY_MAP[dateObj.day()];
271 254
272 return { 255 return {
273 - topText: topStr,  
274 - num: String(dayNum),  
275 - trainName: trainStr, 256 + topText: index === 0 ? '今' : realWeekText,
  257 + num: String(d),
  258 + trainName: item.scheduled ? item.templateName : '',
276 templateId: item.templateId, 259 templateId: item.templateId,
277 dailytemplateId: item.dailytemplateId, 260 dailytemplateId: item.dailytemplateId,
278 date: item.date, 261 date: item.date,
@@ -303,7 +286,6 @@ const openTrainingPopup = () => { @@ -303,7 +286,6 @@ const openTrainingPopup = () => {
303 286
304 // 跳转到排课页面 287 // 跳转到排课页面
305 const openArrageClass = () => { 288 const openArrageClass = () => {
306 - console.log('跳转到排课设置传递的id', plandetail.value.id);  
307 uni.navigateTo({ 289 uni.navigateTo({
308 url: `/pages4/pages/xunji/wode-jihua-paike?planid=${plandetail.value.id}` 290 url: `/pages4/pages/xunji/wode-jihua-paike?planid=${plandetail.value.id}`
309 }) 291 })
@@ -329,15 +311,12 @@ const startTraining = (templateId) => { @@ -329,15 +311,12 @@ const startTraining = (templateId) => {
329 311
330 }; 312 };
331 313
332 -//查看模板详情 314 +/** 查看每日模板详情 */
333 const openTemplateDatil = (templateId, dateArr) => { 315 const openTemplateDatil = (templateId, dateArr) => {
334 - console.log('templateId=', templateId);  
335 - console.log('dateArr=', dateArr);  
336 - if (!templateId) return 316 + if (!templateId) return;
337 317
338 const [y, m, d] = dateArr; 318 const [y, m, d] = dateArr;
339 - const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;  
340 - console.log('日期:', dateStr); 319 + const dateStr = dayjs(`${y}-${m}-${d}`).format('YYYY-MM-DD');
341 uni.navigateTo({ 320 uni.navigateTo({
342 url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${templateId}&date=${dateStr}&isOffice=false&isMyPlan=true&isDailytemplateId=true`, 321 url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${templateId}&date=${dateStr}&isOffice=false&isMyPlan=true&isDailytemplateId=true`,
343 }); 322 });
@@ -410,18 +389,30 @@ const goToTemplateDetail = (id) => { @@ -410,18 +389,30 @@ const goToTemplateDetail = (id) => {
410 url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${id}` 389 url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${id}`
411 }); 390 });
412 }; 391 };
413 -const handleRightClick = (index) => {  
414 - if (index === 0) {  
415 - // 收藏功能(保留原提示逻辑)  
416 - uni.showToast({ title: '已收藏', icon: 'success' });  
417 - } else if (index === 1) {  
418 - // 分享功能(保留原 showShareMenu 配置)  
419 - uni.showShareMenu({  
420 - withCredentials: true, 392 +/** H5 端分享(兼容微信端外浏览器) */
  393 +const handleShare = () => {
  394 + // #ifdef H5
  395 + const url = window.location.href;
  396 + const title = plandetail.value?.name || '健身计划分享';
  397 + if (navigator.share) {
  398 + navigator.share({
  399 + title,
  400 + url,
  401 + }).catch(() => {});
  402 + } else {
  403 + // 兜底:复制链接
  404 + uni.setClipboardData({
  405 + data: url,
  406 + success: () => {
  407 + uni.showToast({ title: '链接已复制', icon: 'success' });
  408 + },
421 }); 409 });
422 } 410 }
  411 + // #endif
423 }; 412 };
424 413
  414 +
  415 +
425 //获取计划详情 416 //获取计划详情
426 const loadDetail = async () => { 417 const loadDetail = async () => {
427 if (isMyPlan.value === true) { 418 if (isMyPlan.value === true) {
@@ -551,12 +542,14 @@ onLoad((options) => { @@ -551,12 +542,14 @@ onLoad((options) => {
551 }); 542 });
552 onMounted(() => { 543 onMounted(() => {
553 loadDetail(); 544 loadDetail();
  545 + uni.$on('calendarDataRefresh', loadDetail);
  546 +});
  547 +onUnmounted(() => {
  548 + uni.$off('calendarDataRefresh', loadDetail);
554 }); 549 });
555 </script> 550 </script>
556 551
557 <style scoped lang="scss"> 552 <style scoped lang="scss">
558 -$base-font: 22rpx;  
559 -  
560 .fitness-plan-page { 553 .fitness-plan-page {
561 width: 100%; 554 width: 100%;
562 min-height: 100vh; 555 min-height: 100vh;
@@ -565,36 +558,57 @@ $base-font: 22rpx; @@ -565,36 +558,57 @@ $base-font: 22rpx;
565 padding-bottom: 70rpx; 558 padding-bottom: 70rpx;
566 } 559 }
567 560
568 -/* 收藏按钮 */  
569 -.collect-btn {  
570 - width: 60rpx;  
571 - height: 60rpx; 561 +/* Banner 悬浮按钮通用样式 */
  562 +.banner-btn {
572 position: absolute; 563 position: absolute;
573 - display: flex;  
574 top: 20rpx; 564 top: 20rpx;
575 - right: 90rpx;  
576 - z-index: 11; 565 + display: flex;
577 align-items: center; 566 align-items: center;
578 justify-content: center; 567 justify-content: center;
  568 + width: 64rpx;
  569 + height: 64rpx;
  570 + border-radius: 50%;
  571 + background: rgba(0, 0, 0, 0.45);
  572 + backdrop-filter: blur(8rpx);
  573 + z-index: 11;
  574 + transition: transform 0.2s, background 0.2s;
579 } 575 }
580 576
  577 +.banner-btn:active {
  578 + transform: scale(0.88);
  579 +}
  580 +
  581 +/* 收藏按钮 */
  582 +.collect-btn {
  583 + right: 100rpx;
  584 +}
  585 +
  586 +.collect-btn.is-fav {
  587 + background: rgba(255, 215, 0, 0.15);
  588 + border: 2rpx solid rgba(255, 215, 0, 0.5);
  589 + animation: favPop 0.35s ease;
  590 +}
  591 +
  592 +@keyframes favPop {
  593 + 0% { transform: scale(1); }
  594 + 40% { transform: scale(1.25); }
  595 + 100% { transform: scale(1); }
  596 +}
  597 +
  598 +/* 分享按钮 */
581 .share-btn { 599 .share-btn {
582 - position: absolute;  
583 - display: flex;  
584 - top: 20rpx;  
585 - /* 距离顶部距离,可按需调整 */  
586 right: 20rpx; 600 right: 20rpx;
587 - /* 距离右侧距离 */  
588 - background: transparent; 601 + background: rgba(0, 0, 0, 0.45);
589 border: none; 602 border: none;
590 padding: 0; 603 padding: 0;
591 margin: 0; 604 margin: 0;
592 - z-index: 10;  
593 - /* 确保按钮可点击区域足够 */  
594 - width: 60rpx;  
595 - height: 60rpx;  
596 - align-items: center;  
597 - justify-content: center; 605 + line-height: 1;
  606 + box-sizing: border-box;
  607 +}
  608 +
  609 +/* 清除 button 默认样式(微信端 open-type=share 的 button) */
  610 +.share-btn::after {
  611 + border: none;
598 } 612 }
599 613
600 /* Banner 区域 */ 614 /* Banner 区域 */
@@ -658,211 +672,170 @@ $base-font: 22rpx; @@ -658,211 +672,170 @@ $base-font: 22rpx;
658 margin-bottom: 10rpx; 672 margin-bottom: 10rpx;
659 } 673 }
660 674
661 -.info-card .icon-clock,  
662 -.info-card .icon-wave,  
663 -.info-card .icon-up {  
664 - width: 32rpx;  
665 - height: 32rpx;  
666 - margin: 0 auto 10rpx;  
667 - opacity: 0.8;  
668 -}  
669 -  
670 .info-card .desc { 675 .info-card .desc {
671 font-size: 24rpx; 676 font-size: 24rpx;
672 color: #999; 677 color: #999;
673 } 678 }
674 679
  680 +.number-unit {
  681 + display: flex;
  682 + align-items: baseline;
  683 + justify-content: center;
  684 + gap: 4rpx;
  685 + margin-bottom: 10rpx;
  686 +}
  687 +
675 /* 器械标签 */ 688 /* 器械标签 */
676 .equipment-tags { 689 .equipment-tags {
677 display: flex; 690 display: flex;
678 flex-wrap: wrap; 691 flex-wrap: wrap;
  692 + align-items: center;
679 padding: 20rpx; 693 padding: 20rpx;
680 - // gap: 6rpx; 694 + gap: 12rpx;
681 } 695 }
682 696
  697 +.equipment-label {
  698 + display: flex;
  699 + align-items: center;
  700 + gap: 6rpx;
  701 + color: #999;
  702 + font-size: 26rpx;
  703 + margin-right: 4rpx;
  704 +}
683 705
684 .tag-item { 706 .tag-item {
685 - padding: 6rpx 12rpx;  
686 - border-radius: 4rpx;  
687 - font-size: 28rpx; 707 + padding: 6rpx 16rpx;
  708 + border-radius: 8rpx;
  709 + font-size: 24rpx;
688 color: #ccc; 710 color: #ccc;
  711 + background-color: rgba(255, 255, 255, 0.08);
  712 + border: 1rpx solid rgba(255, 255, 255, 0.1);
689 } 713 }
690 714
691 -// 训练日日历模块样式 1:1还原截图 715 +// ==================== 训练日日历模块 ====================
692 .calendar-section { 716 .calendar-section {
693 - margin: 40rpx 20rpx;  
694 - padding: 0;  
695 - border-top: 1rpx solid rgba(255, 255, 255, 0.15);  
696 - padding-top: 32rpx; 717 + margin: 20rpx 24rpx;
  718 + padding: 28rpx 24rpx;
  719 + background: rgba(255, 255, 255, 0.04);
  720 + border-radius: 16rpx;
  721 + border: 1rpx solid rgba(255, 255, 255, 0.06);
  722 + box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.2);
697 723
698 .cal-header { 724 .cal-header {
699 display: flex; 725 display: flex;
700 justify-content: space-between; 726 justify-content: space-between;
701 align-items: center; 727 align-items: center;
702 - margin-bottom: 32rpx; 728 + margin-bottom: 24rpx;
  729 +
  730 + .cal-title-row {
  731 + display: flex;
  732 + align-items: center;
  733 + gap: 10rpx;
703 734
704 .cal-title { 735 .cal-title {
705 - font-size: 32rpx;  
706 - color: #ffffff;  
707 - font-weight: 500; 736 + font-size: 30rpx;
  737 + color: #fff;
  738 + font-weight: 600;
  739 + }
708 } 740 }
709 741
710 .cal-set-btn { 742 .cal-set-btn {
711 color: #fff; 743 color: #fff;
712 background: transparent; 744 background: transparent;
713 font-size: 22rpx; 745 font-size: 22rpx;
714 - padding: 3rpx 13rpx; 746 + padding: 6rpx 18rpx;
715 border-radius: 999rpx; 747 border-radius: 999rpx;
716 - border: 1rpx solid #fff; 748 + border: 1rpx solid rgba(255, 255, 255, 0.3);
  749 + line-height: 1.4;
  750 + }
717 } 751 }
  752 +
  753 + // 7 列日历网格
  754 + .cal-grid {
  755 + display: grid;
  756 + grid-template-columns: repeat(7, 1fr);
  757 + gap: 8rpx;
718 } 758 }
719 759
720 - /* 顶部 今/六/日 星期+数字 */  
721 - .cal-week-row { 760 + .cal-col {
722 display: flex; 761 display: flex;
723 - justify-content: space-between;  
724 - margin-bottom: 12rpx; 762 + flex-direction: column;
  763 + align-items: center;
725 764
726 - .cal-day-item {  
727 - width: 14.28%; 765 + .cal-date {
728 display: flex; 766 display: flex;
729 flex-direction: column; 767 flex-direction: column;
730 align-items: center; 768 align-items: center;
  769 + margin-bottom: 10rpx;
731 770
732 - .day-top {  
733 - font-size: 30rpx;  
734 - color: #cccccc; 771 + .day-week {
  772 + font-size: 22rpx;
  773 + color: #999;
  774 + margin-bottom: 4rpx;
735 } 775 }
736 776
737 .day-num { 777 .day-num {
738 - font-size: 32rpx; 778 + font-size: 30rpx;
739 color: #fff; 779 color: #fff;
740 - margin-top: 6rpx;  
741 - } 780 + font-weight: 500;
742 } 781 }
743 } 782 }
744 783
745 - /* 下方黄色标签/休 */  
746 - // .cal-content-row {  
747 - // display: flex;  
748 - // justify-content: space-between;  
749 -  
750 - // .cal-day-item {  
751 - // display: flex;  
752 - // height: 80rpx;  
753 - // align-items: center;  
754 - // justify-content: center;  
755 - // width: 12.5%;  
756 - // text-align: center;  
757 -  
758 - // // .train-tag {  
759 - // // background: #e9ee50;  
760 - // // color: #000000;  
761 - // // font-size: 22rpx;  
762 - // // padding: 10rpx 3rpx;  
763 - // // border-radius: 6rpx;  
764 - // // line-height: 1.3;  
765 - // // }  
766 -  
767 - // // .rest-tag {  
768 - // // background: rgba(255, 255, 255, 0.08);  
769 - // // color: #999999;  
770 - // // font-size: 28rpx;  
771 - // // padding: 12rpx 0;  
772 - // // border-radius: 6rpx;  
773 - // // }  
774 - // /* 统一:训练标签 + 休息标签 样式 */  
775 - // .train-tag,  
776 - // .rest-tag {  
777 - // width: 90% !important;  
778 - // /* 固定宽度 */  
779 - // height: 60rpx !important;  
780 - // /* 固定高度 */  
781 - // line-height: 60rpx !important;  
782 - // /* 文字垂直居中 */  
783 - // font-size: 22rpx !important;  
784 - // /* 固定文字大小 */  
785 - // border-radius: 6rpx !important;  
786 - // display: flex !important;  
787 - // align-items: center !important;  
788 - // justify-content: center !important;  
789 - // box-sizing: border-box !important;  
790 - // overflow: hidden;  
791 - // /* 文字超长隐藏,不撑开盒子 */  
792 - // white-space: nowrap;  
793 - // /* 不换行 */  
794 - // }  
795 -  
796 - // .train-tag {  
797 - // background: #e9ee50 !important;  
798 - // color: #000000 !important;  
799 - // }  
800 -  
801 - // /* 休息标签颜色 */  
802 - // .rest-tag {  
803 - // background: rgba(255, 255, 255, 0.08) !important;  
804 - // color: #999999 !important;  
805 - // }  
806 -  
807 - // }  
808 - // }  
809 - /* 下方黄色标签/休整行 */  
810 - .cal-content-row {  
811 - display: flex;  
812 - flex-direction: row;  
813 - justify-content: space-between;  
814 - gap: 0rpx;  
815 -  
816 - /* 单日容器:7等分宽度,垂直居中放标签 */  
817 - .cal-day-item {  
818 - width: 14.28% !important;  
819 - /* 100/7=14.28%,精准7列均分,代替12.5%,之前宽度错导致不齐!!重点 */  
820 - display: flex;  
821 - justify-content: center;  
822 - align-items: center;  
823 - min-height: 72rpx;  
824 - /* 固定容器高度 */  
825 - padding: 0 4rpx; 784 + // 今日高亮
  785 + &.is-today {
  786 + .day-week {
  787 + color: #e9ee50;
  788 + font-weight: 600;
  789 + }
  790 + .day-num {
  791 + color: #e9ee50;
  792 + font-weight: 700;
  793 + }
  794 + }
  795 + }
826 796
827 - /* 两个标签统一尺寸 */ 797 + // 训练 / 休息标签统一尺寸
828 .train-tag, 798 .train-tag,
829 .rest-tag { 799 .rest-tag {
830 width: 100%; 800 width: 100%;
831 - height: 62rpx;  
832 - border-radius: 6rpx; 801 + height: 58rpx;
  802 + border-radius: 8rpx;
833 display: flex; 803 display: flex;
834 align-items: center; 804 align-items: center;
835 justify-content: center; 805 justify-content: center;
836 - font-size: 22rpx; 806 + font-size: 20rpx;
837 box-sizing: border-box; 807 box-sizing: border-box;
  808 + line-height: 1.2;
  809 + }
  810 +
  811 + .train-tag {
  812 + background: #e9ee50;
  813 + color: #000;
  814 + font-weight: 600;
  815 + box-shadow: 0 2rpx 6rpx rgba(233, 238, 80, 0.25);
  816 +
  817 + &:active {
  818 + transform: scale(0.94);
  819 + transition: transform 0.12s;
838 } 820 }
839 821
840 .tag-text { 822 .tag-text {
841 width: 100%; 823 width: 100%;
842 - font-size: 22rpx; 824 + font-size: 20rpx;
843 color: #000; 825 color: #000;
844 white-space: nowrap; 826 white-space: nowrap;
845 overflow: hidden; 827 overflow: hidden;
846 text-overflow: ellipsis; 828 text-overflow: ellipsis;
847 text-align: center; 829 text-align: center;
  830 + padding: 0 4rpx;
848 } 831 }
849 -  
850 - .train-tag {  
851 - background: #e9ee50;  
852 - color: #000;  
853 } 832 }
854 833
855 .rest-tag { 834 .rest-tag {
856 - background: rgba(255, 255, 255, 0.08);  
857 - color: #999;  
858 - }  
859 - } 835 + background: rgba(255, 255, 255, 0.05);
  836 + color: #666;
  837 + font-size: 20rpx;
860 } 838 }
861 -  
862 -  
863 -  
864 - padding-bottom:32rpx;  
865 - border-bottom:1rpx solid rgba(255, 255, 255, 0.15);  
866 } 839 }
867 840
868 /* 计划课程 */ 841 /* 计划课程 */
@@ -941,7 +914,6 @@ $base-font: 22rpx; @@ -941,7 +914,6 @@ $base-font: 22rpx;
941 margin-top: 10rpx; 914 margin-top: 10rpx;
942 915
943 .btn { 916 .btn {
944 -  
945 margin: 0; 917 margin: 0;
946 font-size: 22rpx; 918 font-size: 22rpx;
947 919
@@ -956,17 +928,27 @@ $base-font: 22rpx; @@ -956,17 +928,27 @@ $base-font: 22rpx;
956 margin-left: 8rpx; 928 margin-left: 8rpx;
957 } 929 }
958 } 930 }
959 -}  
960 931
961 -.arrow-right {  
962 - width: 24rpx;  
963 - height: 24rpx;  
964 - opacity: 0.7;  
965 - transition: opacity 0.3s ease;  
966 -} 932 + .btn-dark {
  933 + background-color: #333;
  934 + color: white;
  935 + border-radius: 20rpx;
  936 + margin: 0;
  937 + font-size: 22rpx;
  938 + }
  939 +
  940 + .btn-gold {
  941 + background-color: #ffd700;
  942 + color: black;
  943 + border-radius: 20rpx;
  944 + margin: 0;
  945 + font-size: 22rpx;
967 946
968 -.course-card:hover .arrow-right {  
969 - opacity: 1; 947 + .go-text {
  948 + background-color: #121212;
  949 + color: #ffd700;
  950 + }
  951 + }
970 } 952 }
971 953
972 /* 按钮区域 */ 954 /* 按钮区域 */
@@ -993,6 +975,16 @@ $base-font: 22rpx; @@ -993,6 +975,16 @@ $base-font: 22rpx;
993 text-align: center; 975 text-align: center;
994 } 976 }
995 977
  978 + .end {
  979 + background-color: white;
  980 + flex: 1;
  981 + }
  982 +
  983 + .ones {
  984 + background-color: #ffd700;
  985 + flex: 1.5;
  986 + }
  987 +
996 .Icon { 988 .Icon {
997 display: flex; 989 display: flex;
998 flex-direction: column; 990 flex-direction: column;
@@ -1044,174 +1036,9 @@ $base-font: 22rpx; @@ -1044,174 +1036,9 @@ $base-font: 22rpx;
1044 width: 100%; 1036 width: 100%;
1045 padding: 20rpx; 1037 padding: 20rpx;
1046 box-sizing: border-box; 1038 box-sizing: border-box;
1047 - background-color: #121212; // 匹配原图深色背景 1039 + background-color: #121212;
1048 color: #fff; 1040 color: #fff;
1049 - font-size: $base-font;  
1050 font-size: 24rpx; 1041 font-size: 24rpx;
1051 padding-bottom: 100rpx; 1042 padding-bottom: 100rpx;
1052 } 1043 }
1053 -  
1054 -// 标题样式  
1055 -.plan-title {  
1056 - width: 100%;  
1057 - text-align: start;  
1058 - font-size: 32rpx;  
1059 - font-weight: bold;  
1060 - color: #fff;  
1061 -}  
1062 -  
1063 -// 计划描述  
1064 -.plan-desc {  
1065 - margin-bottom: 5vw;  
1066 - opacity: 0.9;  
1067 -}  
1068 -  
1069 -// 分块区域  
1070 -.plan-section {  
1071 - margin-bottom: 5vw;  
1072 -  
1073 - // 分块标题  
1074 - .section-title {  
1075 - font-size: 5.333vw;  
1076 - font-weight: 600;  
1077 - margin-bottom: 2vw;  
1078 - color: #f0f0f0;  
1079 - }  
1080 -  
1081 - // 列表项  
1082 - .section-list {  
1083 - .list-item {  
1084 - margin-bottom: 1.5vw;  
1085 - opacity: 0.85;  
1086 - }  
1087 - }  
1088 -  
1089 - // 内容块  
1090 - .section-content {  
1091 - opacity: 0.85;  
1092 - }  
1093 -}  
1094 -  
1095 -/* 底部弹窗遮罩 */  
1096 -.popup-overlay {  
1097 - position: fixed;  
1098 - top: 0;  
1099 - left: 0;  
1100 - right: 0;  
1101 - bottom: 0;  
1102 - background: rgba(0, 0, 0, 0.6);  
1103 - z-index: 9999;  
1104 - display: flex;  
1105 - align-items: flex-end;  
1106 - /* 关键:底部对齐 */  
1107 -}  
1108 -  
1109 -/* 底部弹窗主体 */  
1110 -.popup-bottom {  
1111 - width: 100%;  
1112 - background: #1e1e1e;  
1113 - border-radius: 24rpx 24rpx 0 0;  
1114 - padding: 30rpx;  
1115 - box-sizing: border-box;  
1116 -}  
1117 -  
1118 -/* 顶部拖动条 */  
1119 -.popup-indicator {  
1120 - width: 80rpx;  
1121 - height: 8rpx;  
1122 - background: #444;  
1123 - border-radius: 4rpx;  
1124 - margin: 0 auto 30rpx;  
1125 -}  
1126 -  
1127 -/* 弹窗标题 */  
1128 -.popup-title {  
1129 - font-size: 34rpx;  
1130 - color: #fff;  
1131 - text-align: center;  
1132 - font-weight: bold;  
1133 - display: block;  
1134 - margin-bottom: 30rpx;  
1135 -}  
1136 -  
1137 -/* 顶部说明 + 计数 */  
1138 -.popup-header {  
1139 - display: flex;  
1140 - justify-content: space-between;  
1141 - align-items: center;  
1142 - margin-bottom: 30rpx;  
1143 -  
1144 - .tip-left {  
1145 - font-size: 28rpx;  
1146 - color: #fff;  
1147 - }  
1148 -  
1149 - .tip-right {  
1150 - font-size: 28rpx;  
1151 - color: #ffd700;  
1152 - }  
1153 -}  
1154 -  
1155 -/* 星期选择网格(7列) */  
1156 -.week-grid {  
1157 - display: flex;  
1158 - justify-content: space-between;  
1159 - margin-bottom: 40rpx;  
1160 -}  
1161 -  
1162 -.week-day {  
1163 - width: 80rpx;  
1164 - height: 100rpx;  
1165 - display: flex;  
1166 - flex-direction: column;  
1167 - align-items: center;  
1168 - justify-content: center;  
1169 - background: #333;  
1170 - border-radius: 12rpx;  
1171 - color: #fff;  
1172 - font-size: 28rpx;  
1173 -  
1174 - .rest-text {  
1175 - font-size: 24rpx;  
1176 - color: #999;  
1177 - margin-top: 8rpx;  
1178 - }  
1179 -}  
1180 -  
1181 -/* 选中状态样式(和例图一致) */  
1182 -.week-day.selected {  
1183 - background: #ffd700;  
1184 - color: #000;  
1185 -  
1186 - .rest-text {  
1187 - color: #000;  
1188 - }  
1189 -}  
1190 -  
1191 -/* 底部提示文案 */  
1192 -.popup-desc {  
1193 - font-size: 26rpx;  
1194 - color: #999;  
1195 - line-height: 1.6;  
1196 - margin-bottom: 40rpx;  
1197 -}  
1198 -  
1199 -/* 确认按钮 */  
1200 -.confirm-btn {  
1201 - width: 100%;  
1202 - height: 80rpx;  
1203 - background: #ffd700;  
1204 - color: #000;  
1205 - border-radius: 40rpx;  
1206 - font-size: 30rpx;  
1207 - font-weight: bold;  
1208 - border: none;  
1209 -}  
1210 -  
1211 -/* 按钮禁用状态(和例图一致:灰色、不可点) */  
1212 -.confirm-btn.disabled {  
1213 - background: #444;  
1214 - color: #666;  
1215 - pointer-events: none;  
1216 -}  
1217 </style> 1044 </style>
@@ -477,15 +477,13 @@ const saveProfile = async () => { @@ -477,15 +477,13 @@ const saveProfile = async () => {
477 477
478 await UserApi.updateUser(payload); 478 await UserApi.updateUser(payload);
479 479
480 - if (userStore.getUserInfo) {  
481 - await userStore.getUserInfo();  
482 - } 480 + // 刷新 Store 中的用户信息(Pinia persist 插件会自动同步到 localStorage)
  481 + await userStore.getInfo();
483 482
484 // 同步表单数据,避免 navigateBack 时 onBackPress 误判为"未保存" 483 // 同步表单数据,避免 navigateBack 时 onBackPress 误判为"未保存"
485 formData.value = JSON.parse(JSON.stringify(userStore.userInfo)); 484 formData.value = JSON.parse(JSON.stringify(userStore.userInfo));
486 485
487 - uni.showToast({ title: '保存成功', icon: 'success' });  
488 - 486 + // 注意:updateUser API 已配置 showSuccess: true,拦截器会自动弹出"保存成功"提示
489 setTimeout(() => { 487 setTimeout(() => {
490 uni.navigateBack(); 488 uni.navigateBack();
491 }, 1200); 489 }, 1200);
@@ -17,8 +17,8 @@ @@ -17,8 +17,8 @@
17 <view class="about-card"> 17 <view class="about-card">
18 <view class="about-title">品牌理念</view> 18 <view class="about-title">品牌理念</view>
19 <view class="about-content"> 19 <view class="about-content">
20 - 红星健身致力于为每一位健身爱好者提供<span  
21 - class="highlight">科学、高效、个性化</span>的训练解决方案。我们相信,健身不仅是改变体型的手段,更是一种积极向上的生活方式。无论你是健身新手还是资深训练者,红星健身都将陪伴你完成每一次突破,见证你的每一点进步。 20 + 自己练健身致力于为每一位健身爱好者提供<span
  21 + class="highlight">科学、高效、个性化</span>的训练解决方案。我们相信,健身不仅是改变体型的手段,更是一种积极向上的生活方式。无论你是健身新手还是资深训练者,自己练健身都将陪伴你完成每一次突破,见证你的每一点进步。
22 </view> 22 </view>
23 </view> 23 </view>
24 24
@@ -35,23 +35,6 @@ @@ -35,23 +35,6 @@
35 </view> 35 </view>
36 </view> 36 </view>
37 37
38 - <!-- <view class="about-card">  
39 - <view class="about-title">联系我们</view>  
40 - <view class="contact-list">  
41 - <view class="contact-item">  
42 - <view class="contact-label">微信公众号</view>  
43 - <view class="contact-value">红星健身</view>  
44 - </view>  
45 - <view class="contact-item">  
46 - <view class="contact-label">客服邮箱</view>  
47 - <view class="contact-value">support@hongxingfitness.com</view>  
48 - </view>  
49 - <view class="contact-item">  
50 - <view class="contact-label">官方网站</view>  
51 - <view class="contact-value">www.hongxingfitness.com</view>  
52 - </view>  
53 - </view>  
54 - </view> -->  
55 38
56 <view class="about-card"> 39 <view class="about-card">
57 <view class="about-title">用户协议与隐私</view> 40 <view class="about-title">用户协议与隐私</view>
@@ -68,19 +51,15 @@ @@ -68,19 +51,15 @@
68 </view> 51 </view>
69 </view> 52 </view>
70 53
71 - <!-- 版本信息 -->  
72 - <view class="version-info">  
73 - <text>当前版本 1.0.0</text>  
74 - <text class="copyright">Copyright © 2026 红星健身 All Rights Reserved</text>  
75 - </view> 54 +
  55 +
76 </view> 56 </view>
77 </template> 57 </template>
78 58
79 -<script>  
80 -export default {  
81 - data() {  
82 - return {  
83 - features: [ 59 +<script setup>
  60 +import { ref } from 'vue';
  61 +
  62 +const features = ref([
84 { 63 {
85 name: '个性化训练计划', 64 name: '个性化训练计划',
86 desc: '根据你的目标和水平,智能生成专属训练方案', 65 desc: '根据你的目标和水平,智能生成专属训练方案',
@@ -97,21 +76,22 @@ export default { @@ -97,21 +76,22 @@ export default {
97 name: '训练日历管理', 76 name: '训练日历管理',
98 desc: '灵活安排训练日程,养成规律健身习惯', 77 desc: '灵活安排训练日程,养成规律健身习惯',
99 }, 78 },
100 - ],  
101 - };  
102 - },  
103 - methods: {  
104 - goBack() { 79 +]);
  80 +
  81 +const goBack = () => {
105 uni.navigateBack(); 82 uni.navigateBack();
106 - },  
107 - openAgreement(type) {  
108 - const titles = {  
109 - service: '服务协议',  
110 - privacy: '隐私政策',  
111 - };  
112 - uni.showToast({ title: titles[type] || '功能开发中', icon: 'none' });  
113 - },  
114 - }, 83 +};
  84 +
  85 +const openAgreement = (type) => {
  86 + if (type == 'service') {
  87 + uni.navigateTo({
  88 + url: '/pages5/pages/user/yonhu-xieyi'
  89 + })
  90 + } else {
  91 + uni.navigateTo({
  92 + url: '/pages5/pages/user/yinsi-xieyi'
  93 + })
  94 + }
115 }; 95 };
116 </script> 96 </script>
117 97
@@ -234,9 +234,25 @@ import { ref, reactive, toRaw } from 'vue'; @@ -234,9 +234,25 @@ import { ref, reactive, toRaw } from 'vue';
234 import { onShow } from '@dcloudio/uni-app'; 234 import { onShow } from '@dcloudio/uni-app';
235 import UserApi from '@/sheep/api/member/user'; 235 import UserApi from '@/sheep/api/member/user';
236 import dayjs from 'dayjs'; 236 import dayjs from 'dayjs';
  237 +import {
  238 + formOptions,
  239 + GOAL_LABEL_MAP,
  240 + REWARD_LABEL_MAP,
  241 + SCENE_LABEL_MAP,
  242 + lookupLabel,
  243 +} from '../../../sheep/helper/health-form-options';
237 244
238 // ======================================================== 245 // ========================================================
239 -// 响应式单态核心数据集 246 +// 常量定义
  247 +// ========================================================
  248 +
  249 +/** 身高/体重输入上限阈值 */
  250 +const MAX_NUMERIC_INPUT = 300;
  251 +/** 保存成功后跳转延迟(ms) */
  252 +const SAVE_REDIRECT_DELAY = 1500;
  253 +
  254 +// ========================================================
  255 +// 响应式状态
240 // ======================================================== 256 // ========================================================
241 257
242 const Dateshow = ref(false); 258 const Dateshow = ref(false);
@@ -245,11 +261,11 @@ const inputPopup = ref(false); @@ -245,11 +261,11 @@ const inputPopup = ref(false);
245 const checkPopup = ref(false); 261 const checkPopup = ref(false);
246 const isSaving = ref(false); 262 const isSaving = ref(false);
247 263
248 -// 状态机寄存指针管理 264 +// 弹窗上下文:记录当前操作字段和标题
249 const currentKey = ref(''); 265 const currentKey = ref('');
250 const currentTitle = ref(''); 266 const currentTitle = ref('');
251 -const bufferValue = ref(null); // 纯量缓冲区  
252 -const bufferArray = ref([]); // 数组队列多选缓冲区 267 +const bufferValue = ref(null); // 单选/输入弹窗缓冲值
  268 +const bufferArray = ref([]); // 多选弹窗缓冲数组
253 const datePickerValue = ref(Date.now()); 269 const datePickerValue = ref(Date.now());
254 270
255 const pdata = reactive({ 271 const pdata = reactive({
@@ -270,72 +286,23 @@ const pdata = reactive({ @@ -270,72 +286,23 @@ const pdata = reactive({
270 fitnessScene: 0, 286 fitnessScene: 0,
271 }); 287 });
272 288
273 -// 数据字典隔离配置  
274 -const formOptions = {  
275 - gender: [  
276 - { label: '男', value: 1 },  
277 - { label: '女', value: 2 },  
278 - ],  
279 - hasFitnessFoundation: [  
280 - { label: '有', value: 1 },  
281 - { label: '无', value: 2 },  
282 - ],  
283 - acceptableTrainingFrequency: [  
284 - { label: '1练/2练/3练', value: 1 },  
285 - { label: '4练/5练/6练', value: 2 },  
286 - ],  
287 - targetMuscleParts: [  
288 - { label: '肩颈', value: '肩颈' },  
289 - { label: '斜方肌', value: '斜方肌' },  
290 - { label: '手臂', value: '手臂' },  
291 - { label: '胸部', value: '胸部' },  
292 - { label: '背部', value: '背部' },  
293 - { label: '腹部', value: '腹部' },  
294 - { label: '臀部', value: '臀部' },  
295 - { label: '腿部', value: '腿部' },  
296 - ],  
297 - trainingGoal: [  
298 - { label: '减脂', value: 1 },  
299 - { label: '增肌', value: 2 },  
300 - { label: '塑形', value: 3 },  
301 - { label: '拉伸/体态调整', value: 4 },  
302 - ],  
303 - painAreas: [  
304 - { label: '肩颈', value: '肩颈' },  
305 - { label: '手腕', value: '手腕' },  
306 - { label: '腰部', value: '腰部' },  
307 - { label: '脚踝', value: '脚踝' },  
308 - { label: '膝盖', value: '膝盖' },  
309 - ],  
310 - hasDisease: [  
311 - { label: '有', value: 1 },  
312 - { label: '无', value: 2 },  
313 - ],  
314 - isTakingMedication: [  
315 - { label: '是', value: 1 },  
316 - { label: '否', value: 2 },  
317 - ],  
318 - rewardMethod: [  
319 - { label: '买件新衣服/装备', value: 1 },  
320 - { label: '去旅行', value: 2 },  
321 - { label: '和朋友聚会', value: 3 },  
322 - { label: '其他', value: 4 },  
323 - ],  
324 - fitnessScene: [  
325 - { label: '健身房', value: 1 },  
326 - { label: '家', value: 2 },  
327 - { label: '宿舍', value: 3 },  
328 - ],  
329 -};  
330 -  
331 // ======================================================== 289 // ========================================================
332 -// 文本高阶转义映射器 290 +// 文本格式化
333 // ======================================================== 291 // ========================================================
  292 +
  293 +/** 格式化日期为 YYYY-MM-DD */
334 const formatDate = (val) => (val ? dayjs(val).format('YYYY-MM-DD') : ''); 294 const formatDate = (val) => (val ? dayjs(val).format('YYYY-MM-DD') : '');
335 -const formatGoal = (val) => ({ 1: '减脂', 2: '增肌', 3: '塑形', 4: '拉伸/体态调整' }[val] || '');  
336 -const formatReward = (val) =>  
337 - ({ 1: '买件新衣服/装备', 2: '去旅行', 3: '和朋友聚会', 4: '其他' }[val] || '');  
338 -const formatScene = (val) => ({ 1: '健身房', 2: '家', 3: '宿舍' }[val] || ''); 295 +/** 格式化训练目标 */
  296 +const formatGoal = (val) => lookupLabel(val, GOAL_LABEL_MAP);
  297 +/** 格式化奖励方式 */
  298 +const formatReward = (val) => lookupLabel(val, REWARD_LABEL_MAP);
  299 +/** 格式化健身场景 */
  300 +const formatScene = (val) => lookupLabel(val, SCENE_LABEL_MAP);
  301 +
  302 +/**
  303 + * 将后端布尔字段(true/false)转为前端选项值(1:是 2:否 0:未填)
  304 + */
  305 +const boolToOption = (val) => (val === true ? 1 : val === false ? 2 : 0);
339 306
340 // ======================================================== 307 // ========================================================
341 // 核心弹窗调度器(精细化防污染管控) 308 // 核心弹窗调度器(精细化防污染管控)
@@ -372,8 +339,7 @@ const closeInputPopup = () => { @@ -372,8 +339,7 @@ const closeInputPopup = () => {
372 339
373 const submitInput = () => { 340 const submitInput = () => {
374 const num = parseFloat(bufferValue.value); 341 const num = parseFloat(bufferValue.value);
375 - // 安全阈值边界拦截校验  
376 - if (isNaN(num) || num <= 0 || num > 300) { 342 + if (isNaN(num) || num <= 0 || num > MAX_NUMERIC_INPUT) {
377 uni.showToast({ title: '请输入合理区间值', icon: 'none' }); 343 uni.showToast({ title: '请输入合理区间值', icon: 'none' });
378 return; 344 return;
379 } 345 }
@@ -422,11 +388,14 @@ const getHealthData = async () => { @@ -422,11 +388,14 @@ const getHealthData = async () => {
422 388
423 Object.keys(pdata).forEach((key) => { 389 Object.keys(pdata).forEach((key) => {
424 if (key === 'hasDisease' || key === 'isTakingMedication') { 390 if (key === 'hasDisease' || key === 'isTakingMedication') {
425 - pdata[key] = data[key] === true ? 1 : data[key] === false ? 2 : 0; 391 + // 后端布尔值 → 前端选项值(1:是 2:否 0:未填)
  392 + pdata[key] = boolToOption(data[key]);
426 } else if (key === 'targetMuscleParts' || key === 'painAreas') { 393 } else if (key === 'targetMuscleParts' || key === 'painAreas') {
  394 + // 数组字段确保为有效数组
427 pdata[key] = Array.isArray(data[key]) ? data[key] : []; 395 pdata[key] = Array.isArray(data[key]) ? data[key] : [];
428 } else { 396 } else {
429 - pdata[key] = data[key] !== undefined && data[key] !== null ? data[key] : ''; 397 + // 其余字段直接映射,null/undefined 兜底为空字符串
  398 + pdata[key] = data[key] != null ? data[key] : '';
430 } 399 }
431 }); 400 });
432 401
@@ -434,12 +403,17 @@ const getHealthData = async () => { @@ -434,12 +403,17 @@ const getHealthData = async () => {
434 datePickerValue.value = dayjs(pdata.birthday).valueOf(); 403 datePickerValue.value = dayjs(pdata.birthday).valueOf();
435 } 404 }
436 } catch (error) { 405 } catch (error) {
437 - console.error('拉取档案失败:', error); 406 + console.error('获取健康资料失败:', error);
438 } 407 }
439 }; 408 };
440 409
  410 +/**
  411 + * 将前端选项值转为布尔(供后端接口使用)
  412 + */
  413 +const optionToBool = (val) => val === 1;
  414 +
441 const saveHealthInfo = async () => { 415 const saveHealthInfo = async () => {
442 - // 基础必填拦截防空检测 416 + // 必填校验
443 if (!pdata.gender || !pdata.birthday || !pdata.height || !pdata.weight) { 417 if (!pdata.gender || !pdata.birthday || !pdata.height || !pdata.weight) {
444 uni.showToast({ title: '请完整填写真实核心资料', icon: 'none' }); 418 uni.showToast({ title: '请完整填写真实核心资料', icon: 'none' });
445 return; 419 return;
@@ -451,8 +425,9 @@ const saveHealthInfo = async () => { @@ -451,8 +425,9 @@ const saveHealthInfo = async () => {
451 try { 425 try {
452 const payload = { 426 const payload = {
453 ...toRaw(pdata), 427 ...toRaw(pdata),
454 - hasDisease: pdata.hasDisease === 1,  
455 - isTakingMedication: pdata.isTakingMedication === 1, 428 + // 前端选项值(1/2)→ 后端布尔值(true/false)
  429 + hasDisease: optionToBool(pdata.hasDisease),
  430 + isTakingMedication: optionToBool(pdata.isTakingMedication),
456 }; 431 };
457 432
458 if (pdata.id) { 433 if (pdata.id) {
@@ -464,9 +439,9 @@ const saveHealthInfo = async () => { @@ -464,9 +439,9 @@ const saveHealthInfo = async () => {
464 uni.showToast({ title: '专属计划已生成', icon: 'success' }); 439 uni.showToast({ title: '专属计划已生成', icon: 'success' });
465 setTimeout(() => { 440 setTimeout(() => {
466 uni.navigateBack(); 441 uni.navigateBack();
467 - }, 1500); 442 + }, SAVE_REDIRECT_DELAY);
468 } catch (error) { 443 } catch (error) {
469 - console.error('提交健康资料异常:', error); 444 + console.error('保存健康资料失败:', error);
470 uni.showToast({ title: '保存失败,请重试', icon: 'none' }); 445 uni.showToast({ title: '保存失败,请重试', icon: 'none' });
471 } finally { 446 } finally {
472 isSaving.value = false; 447 isSaving.value = false;
  1 +<template>
  2 + <view class="privacy-page">
  3 + <!-- 导航栏 -->
  4 + <uni-nav-bar title="隐私协议" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" />
  5 + <!-- 协议内容区 -->
  6 + <view class="content-section">
  7 + <!-- 引言 -->
  8 + <view class="privacy-card">
  9 + <view class="card-title">引言</view>
  10 + <view class="card-body">
  11 + <view class="paragraph">
  12 + 自己练健身(以下简称"我们")深知个人信息对您的重要性,我们将严格遵守法律法规要求,采取相应的安全保护措施,尽力保护您的个人信息安全可控。鉴于此,我们制定本《隐私政策》(以下简称"本政策"),并提醒您:
  13 + </view>
  14 + <view class="paragraph">
  15 + 本政策适用于自己练健身提供的所有产品和服务。如我们及关联公司的产品或服务中使用了自己练健身提供的产品或服务但未设独立隐私政策的,则本政策同样适用于该部分产品或服务。
  16 + </view>
  17 + <view class="paragraph">
  18 + <text
  19 + class="text-bold">请您在使用我们的产品或服务前,仔细阅读并了解本政策,</text>以确保您充分理解后再做出适当选择。您使用或继续使用我们的服务,即表示您同意我们按照本政策收集、使用、储存和分享您的相关信息。
  20 + </view>
  21 + </view>
  22 + </view>
  23 +
  24 + <!-- 第一章 -->
  25 + <view class="privacy-card">
  26 + <view class="card-title">一、我们收集的信息</view>
  27 + <view class="card-body">
  28 + <view class="paragraph">
  29 + 在您使用自己练健身服务的过程中,我们会按照如下方式收集您在使用服务时主动提供或因为使用服务而产生的信息,用以向您提供服务、优化我们的服务以及保障您的账户安全:
  30 + </view>
  31 +
  32 + <view class="section-title">1.1 您主动提供的信息</view>
  33 + <view class="list-item">
  34 + <text class="list-dot">•</text>
  35 + <text class="list-text"><text
  36 + class="text-bold">注册与个人资料:</text>当您注册自己练健身账号时,您需要提供手机号码、昵称、性别、出生日期、身高、体重等基本信息,以便我们为您生成个性化的训练方案。</text>
  37 + </view>
  38 + <view class="list-item">
  39 + <text class="list-dot">•</text>
  40 + <text class="list-text"><text
  41 + class="text-bold">健康资料:</text>在设定健身目标时,您可以选择填写训练水平、目标肌群、健康状况等信息,帮助我们精准匹配训练内容。</text>
  42 + </view>
  43 + <view class="list-item">
  44 + <text class="list-dot">•</text>
  45 + <text class="list-text"><text
  46 + class="text-bold">反馈与客服:</text>当您联系我们的客服或提交意见反馈时,我们会收集您的联系方式和沟通内容,以便解决您的问题。</text>
  47 + </view>
  48 +
  49 + <view class="section-title">1.2 使用服务时自动收集的信息</view>
  50 + <view class="list-item">
  51 + <text class="list-dot">•</text>
  52 + <text class="list-text"><text
  53 + class="text-bold">设备信息:</text>包括设备型号、操作系统版本、唯一设备标识符、IP地址、网络类型等,用于保障服务稳定运行和安全防护。</text>
  54 + </view>
  55 + <view class="list-item">
  56 + <text class="list-dot">•</text>
  57 + <text class="list-text"><text
  58 + class="text-bold">训练数据:</text>当您使用训练记录功能时,我们会收集您的训练动作、组数、次数、重量、训练时长等数据,用于生成您的训练报告和进度分析。</text>
  59 + </view>
  60 + <view class="list-item">
  61 + <text class="list-dot">•</text>
  62 + <text class="list-text"><text
  63 + class="text-bold">使用日志:</text>包括您访问的页面、点击的操作、使用时长、崩溃日志等,帮助我们优化产品体验。</text>
  64 + </view>
  65 + <view class="list-item">
  66 + <text class="list-dot">•</text>
  67 + <text class="list-text"><text
  68 + class="text-bold">位置信息:</text>当您使用附近健身房或运动场地推荐功能时,我们会申请获取您的地理位置权限。您可以随时在设备设置中关闭位置权限。</text>
  69 + </view>
  70 + </view>
  71 + </view>
  72 +
  73 + <!-- 第二章 -->
  74 + <view class="privacy-card">
  75 + <view class="card-title">二、信息的使用方式</view>
  76 + <view class="card-body">
  77 + <view class="paragraph">我们将收集的信息用于以下目的:</view>
  78 + <view class="list-item">
  79 + <text class="list-dot">•</text>
  80 + <text class="list-text"><text
  81 + class="text-bold">提供核心服务:</text>基于您的个人资料和训练数据,为您生成个性化训练计划、提供动作示范与讲解、展示训练进度和数据分析。</text>
  82 + </view>
  83 + <view class="list-item">
  84 + <text class="list-dot">•</text>
  85 + <text class="list-text"><text
  86 + class="text-bold">产品优化:</text>分析用户的使用习惯和偏好,改进我们的功能设计,提升用户体验。</text>
  87 + </view>
  88 + <view class="list-item">
  89 + <text class="list-dot">•</text>
  90 + <text class="list-text"><text class="text-bold">安全保障:</text>检测和防范安全风险、欺诈行为,保护您和我们的合法权益。</text>
  91 + </view>
  92 + <view class="list-item">
  93 + <text class="list-dot">•</text>
  94 + <text class="list-text"><text class="text-bold">客户服务:</text>处理您的咨询、投诉和反馈,为您提供及时有效的帮助。</text>
  95 + </view>
  96 + <view class="list-item">
  97 + <text class="list-dot">•</text>
  98 + <text class="list-text"><text
  99 + class="text-bold">推荐与通知:</text>基于您的训练偏好,向您推送相关训练课程资讯和活动通知。您可以随时在"我的→设置→隐私设置"中关闭推送。</text>
  100 + </view>
  101 + </view>
  102 + </view>
  103 +
  104 + <!-- 第三章 -->
  105 + <view class="privacy-card">
  106 + <view class="card-title">三、信息的存储与安全</view>
  107 + <view class="card-body">
  108 + <view class="section-title">3.1 存储地点</view>
  109 + <view class="paragraph">
  110 + 我们会将您的个人信息存储于中华人民共和国境内。如需跨境传输,我们将严格按照法律法规要求进行安全评估,并取得您的单独同意。
  111 + </view>
  112 +
  113 + <view class="section-title">3.2 存储期限</view>
  114 + <view class="paragraph">
  115 + 我们仅在实现本政策所述目的所必需的期限内保留您的个人信息,除非法律法规有强制性的留存要求。当您的账号被注销后,我们将在合理期限内对您的个人信息进行删除或匿名化处理。
  116 + </view>
  117 +
  118 + <view class="section-title">3.3 安全措施</view>
  119 + <view class="paragraph">
  120 + 我们已采用符合业界标准的安全防护措施,包括但不限于数据加密传输(SSL/TLS)、访问控制、防火墙、数据备份等技术手段,保护您的个人信息免遭未经授权的访问、使用、修改或泄露。同时建立了数据安全管理制度和应急响应机制,以应对信息安全事件。
  121 + </view>
  122 + </view>
  123 + </view>
  124 +
  125 + <!-- 第四章 -->
  126 + <view class="privacy-card">
  127 + <view class="card-title">四、信息的共享与披露</view>
  128 + <view class="card-body">
  129 + <view class="paragraph">
  130 + 我们不会与第三方公司、组织和个人共享您的个人信息,但以下情况除外:
  131 + </view>
  132 + <view class="list-item">
  133 + <text class="list-dot">•</text>
  134 + <text class="list-text">
  135 + <text class="text-bold">获得您的明确授权:</text>在获得您的明确同意后,我们会向第三方共享您授权的信息。
  136 + </text>
  137 + </view>
  138 + <view class="list-item">
  139 + <text class="list-dot">•</text>
  140 + <text class="list-text">
  141 + <text class="text-bold">法律法规要求:</text>根据法律法规规定、诉讼或行政、司法机关要求,我们可能对外提供您的个人信息。
  142 + </text>
  143 + </view>
  144 + <view class="list-item">
  145 + <text class="list-dot">•</text>
  146 + <text class="list-text">
  147 + <text
  148 + class="text-bold">与关联公司共享:</text>我们可能与关联公司共享必要的个人信息,且受本政策约束。关联公司如要改变个人信息的处理目的,将再次征求您的授权同意。
  149 + </text>
  150 + </view>
  151 + <view class="list-item">
  152 + <text class="list-dot">•</text>
  153 + <text class="list-text">
  154 + <text
  155 + class="text-bold">与服务提供商共享:</text>为向您提供更好的服务,我们可能委托第三方服务提供商(如云存储服务商、推送服务商)处理您的个人信息。我们会与供应商签署严格的保密协议,要求其按照我们的指示和本政策采取保护措施。
  156 + </text>
  157 + </view>
  158 + </view>
  159 + </view>
  160 +
  161 + <!-- 第五章 -->
  162 + <view class="privacy-card">
  163 + <view class="card-title">五、您的权利</view>
  164 + <view class="card-body">
  165 + <view class="paragraph">按照相关法律法规,我们保障您对自己的个人信息行使以下权利:</view>
  166 +
  167 + <view class="list-item">
  168 + <text class="list-dot">•</text>
  169 + <text class="list-text">
  170 + <text class="text-bold">查阅与更正:</text>您可以在"我的→个人资料"中查阅和修改您的基本个人信息。如您发现我们收集、处理的个人信息有误,您有权要求更正。
  171 + </text>
  172 + </view>
  173 + <view class="list-item">
  174 + <text class="list-dot">•</text>
  175 + <text class="list-text">
  176 + <text
  177 + class="text-bold">删除个人信息:</text>在以下情形中,您可以向我们提出删除个人信息的请求:(1)处理目的已实现或无法实现;(2)我们停止提供产品或服务;(3)您撤回同意;(4)我们违反法律法规处理您的信息。
  178 + </text>
  179 + </view>
  180 + <view class="list-item">
  181 + <text class="list-dot">•</text>
  182 + <text class="list-text">
  183 + <text
  184 + class="text-bold">撤回同意:</text>您可以在"我的→设置→隐私设置"中撤回您对推送通知、个性化推荐等功能的授权。撤回同意不影响撤回前基于授权已进行的个人信息处理活动的效力。
  185 + </text>
  186 + </view>
  187 + <view class="list-item">
  188 + <text class="list-dot">•</text>
  189 + <text class="list-text">
  190 + <text
  191 + class="text-bold">注销账号:</text>您可以在"我的→设置"中申请注销账号。账号注销后,我们将停止为您提供产品或服务,并依法删除或匿名化处理您的个人信息。
  192 + </text>
  193 + </view>
  194 + </view>
  195 + </view>
  196 +
  197 + <!-- 第六章 -->
  198 + <view class="privacy-card">
  199 + <view class="card-title">六、未成年人保护</view>
  200 + <view class="card-body">
  201 + <view class="paragraph">
  202 + 我们高度重视未成年人个人信息的保护。如果您是<text
  203 + class="text-highlight">未满18周岁的未成年人</text>,在使用我们的服务前,应事先取得您父母或法定监护人的书面同意。对于经父母或监护人同意而收集的未成年人个人信息,我们只会在法律法规允许、父母或监护人明确同意或者保护未成年人所必要的情况下使用或披露。
  204 + </view>
  205 + <view class="paragraph">
  206 + 如果监护人发现我们在未获其同意的情况下收集了未成年人的个人信息,请及时联系我们,我们将尽快核实并删除相关数据。
  207 + </view>
  208 + </view>
  209 + </view>
  210 +
  211 + <!-- 第七章 -->
  212 + <view class="privacy-card">
  213 + <view class="card-title">七、隐私政策的更新</view>
  214 + <view class="card-body">
  215 + <view class="paragraph">
  216 + 我们可能适时修订本政策。未经您明确同意,我们不会削减您按照本政策所应享有的权利。对于重大变更,我们会通过应用内弹窗通知、推送通知或其他显著方式告知您。
  217 + </view>
  218 + <view class="paragraph">
  219 + <text class="text-bold">重大变更包括但不限于:</text>
  220 + </view>
  221 + <view class="list-item">
  222 + <text class="list-dot">•</text>
  223 + <text class="list-text">我们的服务模式发生重大变化,如处理个人信息的目的、类型、方式等。</text>
  224 + </view>
  225 + <view class="list-item">
  226 + <text class="list-dot">•</text>
  227 + <text class="list-text">我们在控制权等方面发生重大变化,如并购重组等引起的所有者变更。</text>
  228 + </view>
  229 + <view class="list-item">
  230 + <text class="list-dot">•</text>
  231 + <text class="list-text">您参与个人信息处理方面的权利及其行使方式发生重大变化。</text>
  232 + </view>
  233 + </view>
  234 + </view>
  235 +
  236 + <!-- 第八章 -->
  237 + <!-- <view class="privacy-card">
  238 + <view class="card-title">八、联系我们</view>
  239 + <view class="card-body">
  240 + <view class="paragraph">
  241 + 如您对本政策或您的个人信息相关事宜有任何疑问、意见或投诉,请通过以下方式与我们联系:
  242 + </view>
  243 + <view class="contact-list">
  244 + <view class="contact-item">
  245 + <text class="contact-label">应用内反馈:</text>
  246 + <text class="contact-value">我的 → 联系客服 → 意见反馈</text>
  247 + </view>
  248 + <view class="contact-item">
  249 + <text class="contact-label">邮箱:</text>
  250 + <text class="contact-value">privacy@hongxingfitness.com</text>
  251 + </view>
  252 + </view>
  253 + <view class="paragraph" style="margin-top: 24rpx;">
  254 + 我们将在收到您的反馈后<text class="text-highlight">15个工作日内</text>予以回复和处理。如您对我们的处理结果不满意,您还可以向相关监管部门进行投诉。
  255 + </view>
  256 + </view>
  257 + </view> -->
  258 + </view>
  259 +
  260 + <!-- 底部信息 -->
  261 + <view class="footer-info">
  262 + <text class="footer-text">自己练健身 · 隐私政策</text>
  263 + <text class="footer-copyright">Copyright © 2026 自己练健身 All Rights Reserved</text>
  264 + </view>
  265 +
  266 + <!-- 底部安全区占位 -->
  267 + <u-safe-bottom />
  268 + </view>
  269 +</template>
  270 +
  271 +<script setup>
  272 +/**
  273 + * 隐私协议静态页面
  274 + * 基于芋道商城(yudao-mall-uniapp)框架开发
  275 + * 使用 uview-plus 组件库
  276 + */
  277 +
  278 +// 返回上一页
  279 +const goBack = () => {
  280 + uni.navigateBack({
  281 + delta: 1,
  282 + fail: () => {
  283 + // 如果是从外部直接打开(无上一页),跳转到首页
  284 + uni.switchTab({
  285 + url: '/pages/xunji/xunji',
  286 + });
  287 + },
  288 + });
  289 +};
  290 +</script>
  291 +
  292 +<style lang="scss" scoped>
  293 +.privacy-page {
  294 + position: relative;
  295 + width: 100%;
  296 + min-height: 100vh;
  297 + background-color: #1a1a1a;
  298 + color: #fff;
  299 + padding-bottom: 60rpx;
  300 +}
  301 +
  302 +/* ========== 页面标题 ========== */
  303 +.page-header {
  304 + padding: 40rpx 30rpx 30rpx;
  305 + background: linear-gradient(135deg, #1e1e1e 0%, #252525 100%);
  306 + border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
  307 + text-align: center;
  308 +}
  309 +
  310 +.header-title {
  311 + font-size: 40rpx;
  312 + font-weight: bold;
  313 + color: #fff;
  314 + margin-bottom: 12rpx;
  315 +}
  316 +
  317 +.header-subtitle {
  318 + font-size: 24rpx;
  319 + color: rgba(255, 255, 255, 0.45);
  320 + line-height: 1.6;
  321 +}
  322 +
  323 +/* ========== 协议内容区 ========== */
  324 +.content-section {
  325 + padding: 24rpx 24rpx 0;
  326 +}
  327 +
  328 +.privacy-card {
  329 + background-color: rgba(255, 255, 255, 0.05);
  330 + border-radius: 16rpx;
  331 + padding: 28rpx 24rpx;
  332 + margin-bottom: 20rpx;
  333 + border: 1rpx solid rgba(255, 255, 255, 0.08);
  334 +}
  335 +
  336 +.card-title {
  337 + font-size: 30rpx;
  338 + font-weight: bold;
  339 + margin-bottom: 22rpx;
  340 + padding-left: 14rpx;
  341 + border-left: 5rpx solid #ff6b00;
  342 + color: #fff;
  343 +}
  344 +
  345 +.card-body {
  346 + display: flex;
  347 + flex-direction: column;
  348 + gap: 16rpx;
  349 +}
  350 +
  351 +.section-title {
  352 + font-size: 27rpx;
  353 + font-weight: bold;
  354 + color: rgba(255, 255, 255, 0.9);
  355 + margin-top: 8rpx;
  356 + margin-bottom: 4rpx;
  357 +}
  358 +
  359 +.paragraph {
  360 + font-size: 26rpx;
  361 + color: rgba(255, 255, 255, 0.75);
  362 + line-height: 1.8;
  363 + text-align: justify;
  364 +}
  365 +
  366 +.text-bold {
  367 + font-weight: bold;
  368 + color: rgba(255, 255, 255, 0.95);
  369 +}
  370 +
  371 +.text-highlight {
  372 + color: #ff6b00;
  373 + font-weight: bold;
  374 +}
  375 +
  376 +/* ========== 列表项 ========== */
  377 +.list-item {
  378 + display: flex;
  379 + align-items: flex-start;
  380 + gap: 12rpx;
  381 + padding-left: 4rpx;
  382 +}
  383 +
  384 +.list-dot {
  385 + font-size: 26rpx;
  386 + color: #ff6b00;
  387 + line-height: 1.8;
  388 + flex-shrink: 0;
  389 +}
  390 +
  391 +.list-text {
  392 + font-size: 26rpx;
  393 + color: rgba(255, 255, 255, 0.75);
  394 + line-height: 1.8;
  395 + flex: 1;
  396 + text-align: justify;
  397 +}
  398 +
  399 +/* ========== 联系方式 ========== */
  400 +.contact-list {
  401 + display: flex;
  402 + flex-direction: column;
  403 + gap: 12rpx;
  404 + padding-left: 4rpx;
  405 +}
  406 +
  407 +.contact-item {
  408 + display: flex;
  409 + align-items: flex-start;
  410 + gap: 8rpx;
  411 +}
  412 +
  413 +.contact-label {
  414 + font-size: 26rpx;
  415 + color: rgba(255, 255, 255, 0.6);
  416 + flex-shrink: 0;
  417 +}
  418 +
  419 +.contact-value {
  420 + font-size: 26rpx;
  421 + color: rgba(255, 255, 255, 0.85);
  422 + line-height: 1.6;
  423 +}
  424 +
  425 +/* ========== 底部信息 ========== */
  426 +.footer-info {
  427 + text-align: center;
  428 + padding: 40rpx 0 20rpx;
  429 + display: flex;
  430 + flex-direction: column;
  431 + gap: 8rpx;
  432 +}
  433 +
  434 +.footer-text {
  435 + font-size: 22rpx;
  436 + color: rgba(255, 255, 255, 0.35);
  437 +}
  438 +
  439 +.footer-copyright {
  440 + font-size: 20rpx;
  441 + color: rgba(255, 255, 255, 0.25);
  442 +}
  443 +</style>
  1 +<template>
  2 + <view class="agreement-page">
  3 + <!-- 导航栏 -->
  4 + <uni-nav-bar title="用户协议" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" />
  5 +
  6 + <!-- 协议内容区域 -->
  7 + <scroll-view scroll-y class="agreement-scroll" :style="{ height: scrollHeight + 'px' }">
  8 + <view class="agreement-content">
  9 +
  10 + <!-- 导言 -->
  11 + <view class="agreement-section">
  12 + <view class="section-body">
  13 + <text class="section-text">
  14 + 欢迎使用自己练健身(以下简称"本应用"或"我们")提供的服务。请您在使用本应用前仔细阅读本《用户服务协议》(以下简称"本协议")。<text class="text-bold">您通过点击同意、注册登录或实际使用本应用的行为,即表示您已充分阅读、理解并接受本协议的全部内容</text>。如您不同意本协议的任何条款,请立即停止注册或使用本应用。
  15 + </text>
  16 + <text class="section-text">
  17 + 本协议是您与自己练健身之间就您使用本应用服务所订立的有效协议。我们有权根据法律法规及运营需要适时修订本协议,修订后的协议将在本应用内公示。若您在修订后继续使用本应用,即视为您已接受修订后的协议。
  18 + </text>
  19 + </view>
  20 + </view>
  21 +
  22 + <!-- 一、服务说明 -->
  23 + <view class="agreement-section">
  24 + <view class="section-title">
  25 + <text class="title-text">一、服务说明</text>
  26 + </view>
  27 + <view class="section-body">
  28 + <text class="section-text">
  29 + 1.1 自己练健身是一款面向健身爱好者的训练管理与记录工具,提供包括但不限于以下服务:训练计划制定与管理、动作库浏览与学习、训练数据记录与分析、训练日历管理等。
  30 + </text>
  31 + <text class="section-text">
  32 + 1.2 本应用提供的健身建议、训练方案等内容仅供参考,不构成专业的医疗建议。<text class="text-bold">在开始任何训练计划前,您应当咨询专业医生或健康顾问,确认自身身体状况适合进行相应的体育活动。</text>
  33 + </text>
  34 + <text class="section-text">
  35 + 1.3 我们保留根据业务发展需要,随时增加、调整或终止部分或全部服务的权利,且无需事先通知您。服务变更将在应用内公示后生效。
  36 + </text>
  37 + </view>
  38 + </view>
  39 +
  40 + <!-- 二、账号注册与管理 -->
  41 + <view class="agreement-section">
  42 + <view class="section-title">
  43 + <text class="title-text">二、账号注册与管理</text>
  44 + </view>
  45 + <view class="section-body">
  46 + <text class="section-text">
  47 + 2.1 您在使用本应用前需要注册账号。注册时,您应当提供真实、准确、完整的个人信息,并在信息变更时及时更新。因您提供的信息不真实、不准确或不完整所导致的一切后果由您自行承担。
  48 + </text>
  49 + <text class="section-text">
  50 + 2.2 您注册的账号仅限于您本人使用,不得以任何形式转让、出借或授权他人使用。您应当妥善保管账号及密码信息,对通过您的账号所进行的一切活动承担全部责任。
  51 + </text>
  52 + <text class="section-text">
  53 + 2.3 如发现您的账号存在异常登录或被他人非法使用的情形,您应当立即通知我们。因您未及时通知而导致的一切损失,由您自行承担。
  54 + </text>
  55 + <text class="section-text">
  56 + 2.4 您有权随时注销您的账号。账号注销后,您的个人信息将按照本应用隐私政策的规定进行处理。账号一旦注销,将无法恢复。
  57 + </text>
  58 + </view>
  59 + </view>
  60 +
  61 + <!-- 三、用户行为规范 -->
  62 + <view class="agreement-section">
  63 + <view class="section-title">
  64 + <text class="title-text">三、用户行为规范</text>
  65 + </view>
  66 + <view class="section-body">
  67 + <text class="section-text">
  68 + 3.1 您在使用本应用过程中,应当遵守中华人民共和国的法律法规,不得利用本应用从事违法违规活动,包括但不限于:
  69 + </text>
  70 + <text class="section-text indent-text">
  71 + (1)发布、传播危害国家安全、破坏国家统一、损害国家荣誉和利益的内容;
  72 + </text>
  73 + <text class="section-text indent-text">
  74 + (2)发布、传播煽动民族仇恨、民族歧视、破坏民族团结的内容;
  75 + </text>
  76 + <text class="section-text indent-text">
  77 + (3)发布、传播淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的内容;
  78 + </text>
  79 + <text class="section-text indent-text">
  80 + (4)发布、传播侮辱、诽谤他人,侵害他人合法权益的内容;
  81 + </text>
  82 + <text class="section-text indent-text">
  83 + (5)发布、传播虚假信息,扰乱社会秩序、破坏社会稳定的内容;
  84 + </text>
  85 + <text class="section-text indent-text">
  86 + (6)利用技术手段恶意攻击本应用服务器,破坏本应用的正常运行;
  87 + </text>
  88 + <text class="section-text indent-text">
  89 + (7)利用本应用从事任何可能影响其他用户正常使用的行为。
  90 + </text>
  91 + <text class="section-text">
  92 + 3.2 您应尊重他人的知识产权,不得在本应用上发布、分享任何侵犯他人著作权、商标权、专利权等知识产权的内容。
  93 + </text>
  94 + <text class="section-text">
  95 + 3.3 如您违反上述规定,我们有权采取包括但不限于以下措施:警告、限制功能使用、暂停或终止服务、注销账号,并保留追究法律责任的权利。
  96 + </text>
  97 + </view>
  98 + </view>
  99 +
  100 + <!-- 四、服务变更与终止 -->
  101 + <view class="agreement-section">
  102 + <view class="section-title">
  103 + <text class="title-text">四、服务变更、中断与终止</text>
  104 + </view>
  105 + <view class="section-body">
  106 + <text class="section-text">
  107 + 4.1 我们可能会根据业务发展需要,对服务的全部或部分内容进行调整、变更或终止,并在合理期限内通过应用公告、推送通知等方式告知您。
  108 + </text>
  109 + <text class="section-text">
  110 + 4.2 在下列情形下,我们有权不经通知即中断或终止向您提供服务:
  111 + </text>
  112 + <text class="section-text indent-text">
  113 + (1)您违反本协议约定,我们根据违约情况决定终止服务的;
  114 + </text>
  115 + <text class="section-text indent-text">
  116 + (2)您注册时提供的信息不真实、不准确,或未及时更新的;
  117 + </text>
  118 + <text class="section-text indent-text">
  119 + (3)根据法律法规规定或政府部门的要求;
  120 + </text>
  121 + <text class="section-text indent-text">
  122 + (4)为维护社会公共利益或保护其他用户合法权益所必需的。
  123 + </text>
  124 + <text class="section-text">
  125 + 4.3 因系统维护升级、网络故障、技术调整等原因可能导致服务中断,我们将尽量提前通知您并尽快恢复服务。对于计划内的维护,我们将至少提前24小时通过应用公告通知。
  126 + </text>
  127 + </view>
  128 + </view>
  129 +
  130 + <!-- 五、知识产权 -->
  131 + <view class="agreement-section">
  132 + <view class="section-title">
  133 + <text class="title-text">五、知识产权</text>
  134 + </view>
  135 + <view class="section-body">
  136 + <text class="section-text">
  137 + 5.1 本应用的所有内容,包括但不限于文字、图片、视频、音频、图标、界面设计、软件代码、数据分析模型等,其知识产权均归自己练健身或其权利人所有,受中华人民共和国著作权法、商标法、专利法等相关法律法规的保护。
  138 + </text>
  139 + <text class="section-text">
  140 + 5.2 未经我们或相关权利人的书面许可,您不得以任何方式(包括但不限于复制、修改、传播、展示、镜像、上载、下载)使用本应用的任何内容,也不得对本应用进行反向工程、反编译或反汇编。
  141 + </text>
  142 + <text class="section-text">
  143 + 5.3 您在使用本应用过程中上传、发布的内容,您保留对该内容的所有权。但您授予我们全球范围内免费、非独家、可再许可的使用权,以便我们运营和改进本应用服务。
  144 + </text>
  145 + </view>
  146 + </view>
  147 +
  148 + <!-- 六、免责声明 -->
  149 + <view class="agreement-section">
  150 + <view class="section-title">
  151 + <text class="title-text">六、免责声明</text>
  152 + </view>
  153 + <view class="section-body">
  154 + <text class="section-text">
  155 + 6.1 <text class="text-bold">健康风险提示:</text>本应用提供的训练计划、动作指导、营养建议等内容仅供一般性参考,不构成专业的医疗、健身或营养建议。您在使用本应用过程中自愿承担因参与体育活动可能产生的一切风险,包括但不限于运动损伤、身体不适等。我们强烈建议您在开始任何新的训练计划前咨询专业医生。
  156 + </text>
  157 + <text class="section-text">
  158 + 6.2 <text class="text-bold">数据准确性:</text>本应用中的训练数据(如消耗卡路里、训练量等)为基于算法估算的结果,可能与实际情况存在偏差,不应用于精确的健康监测或医疗诊断。
  159 + </text>
  160 + <text class="section-text">
  161 + 6.3 <text class="text-bold">不可抗力:</text>因自然灾害、战争、政府行为、法律法规变化、黑客攻击、电信运营商故障、第三方服务故障等不可抗力或我们不能预见、不能控制的原因导致本应用服务中断或出现其他不利影响的,我们不承担责任。
  162 + </text>
  163 + <text class="section-text">
  164 + 6.4 本应用可能包含指向第三方网站或服务的链接。这些链接仅为方便您使用而提供,我们对该等第三方网站或服务的内容、隐私政策或行为不承担任何责任。
  165 + </text>
  166 + </view>
  167 + </view>
  168 +
  169 + <!-- 七、责任限制 -->
  170 + <view class="agreement-section">
  171 + <view class="section-title">
  172 + <text class="title-text">七、责任限制</text>
  173 + </view>
  174 + <view class="section-body">
  175 + <text class="section-text">
  176 + 7.1 在法律允许的最大范围内,我们对因使用或无法使用本应用而产生的任何直接、间接、附带、特殊、惩罚性或结果性损失(包括但不限于人身伤害、数据丢失、业务中断、利润损失等)不承担责任,无论该等损失是否基于合同、侵权或其他法律理论,即使我们已被告知发生该等损失的可能性。
  177 + </text>
  178 + <text class="section-text">
  179 + 7.2 如我们因任何原因需对您承担责任,则在法律允许的最大范围内,我们的累计责任总额不超过您在事件发生前十二(12)个月内向我们已支付的费用总额。
  180 + </text>
  181 + </view>
  182 + </view>
  183 +
  184 + <!-- 八、隐私保护 -->
  185 + <view class="agreement-section">
  186 + <view class="section-title">
  187 + <text class="title-text">八、隐私保护</text>
  188 + </view>
  189 + <view class="section-body">
  190 + <text class="section-text">
  191 + 8.1 保护您的个人信息是我们的一项重要原则。我们将按照《自己练健身隐私政策》的规定,收集、使用、存储和保护您的个人信息。您可以通过本应用"设置-隐私政策"页面查阅完整的隐私政策内容。
  192 + </text>
  193 + <text class="section-text">
  194 + 8.2 我们承诺不会将您的个人信息出售或非法提供给任何第三方,但以下情形除外:
  195 + </text>
  196 + <text class="section-text indent-text">
  197 + (1)事先获得您的明确授权同意;
  198 + </text>
  199 + <text class="section-text indent-text">
  200 + (2)根据法律法规规定或政府部门、司法机关的强制性要求;
  201 + </text>
  202 + <text class="section-text indent-text">
  203 + (3)为维护我们的合法权益,如查找、预防、处理欺诈或安全问题;
  204 + </text>
  205 + <text class="section-text indent-text">
  206 + (4)为向您提供服务之目的,与我们的合作伙伴共享必要信息(如云服务提供商等)。
  207 + </text>
  208 + <text class="section-text">
  209 + 8.3 我们采用业界通行的安全技术和措施来保护您的个人信息安全,防止信息被未经授权的访问、使用或泄露。但由于互联网环境的特殊性,我们无法保证信息传输和存储的绝对安全。
  210 + </text>
  211 + </view>
  212 + </view>
  213 +
  214 + <!-- 九、争议解决 -->
  215 + <view class="agreement-section">
  216 + <view class="section-title">
  217 + <text class="title-text">九、争议解决</text>
  218 + </view>
  219 + <view class="section-body">
  220 + <text class="section-text">
  221 + 9.1 本协议的订立、执行、解释及争议解决均适用中华人民共和国法律(为本协议之目的,不包括香港特别行政区、澳门特别行政区和台湾地区的法律)。
  222 + </text>
  223 + <text class="section-text">
  224 + 9.2 因本协议引起的或与本协议有关的任何争议,双方应首先通过友好协商解决。协商不成的,任何一方均有权将争议提交至我们有管辖权的人民法院通过诉讼解决。
  225 + </text>
  226 + </view>
  227 + </view>
  228 +
  229 + <!-- 十、其他条款 -->
  230 + <view class="agreement-section">
  231 + <view class="section-title">
  232 + <text class="title-text">十、其他条款</text>
  233 + </view>
  234 + <view class="section-body">
  235 + <text class="section-text">
  236 + 10.1 本协议中部分条款被认定为无效或不可执行的,不影响其他条款的效力,其余条款继续有效。
  237 + </text>
  238 + <text class="section-text">
  239 + 10.2 我们未行使或延迟行使本协议项下的任何权利,不视为对该权利的放弃。任何权利的放弃应以书面形式作出。
  240 + </text>
  241 + <text class="section-text">
  242 + 10.3 本协议条款的标题仅为阅读方便而设,不影响条款本身的含义和解释。
  243 + </text>
  244 + <text class="section-text">
  245 + 10.4 如您对本协议有任何疑问、意见或建议,可通过以下方式联系我们:
  246 + </text>
  247 + <text class="section-text indent-text">
  248 + 应用内:通过"我的-联系客服"页面提交反馈
  249 + </text>
  250 + </view>
  251 + </view>
  252 +
  253 + <!-- 底部间距 -->
  254 + <view class="agreement-footer">
  255 + <text class="footer-text">自己练健身团队</text>
  256 + </view>
  257 + </view>
  258 + </scroll-view>
  259 + </view>
  260 +</template>
  261 +
  262 +<script setup>
  263 +import { ref, onMounted } from 'vue';
  264 +
  265 +// 滚动区域高度
  266 +const scrollHeight = ref(0);
  267 +
  268 +// 计算滚动区域高度(屏幕高度 - 导航栏高度 - 状态栏高度)
  269 +const calcScrollHeight = () => {
  270 + const systemInfo = uni.getSystemInfoSync();
  271 + // 导航栏高度:状态栏高度 + 胶囊按钮高度(约44px + statusBarHeight)
  272 + const statusBarHeight = systemInfo.statusBarHeight || 0;
  273 + // uni-nav-bar 默认高度约 44px(不含状态栏,因 status-bar=true 状态栏由组件处理)
  274 + const navBarHeight = 44;
  275 + // 将 px 转换为实际可使用高度
  276 + scrollHeight.value = systemInfo.windowHeight - statusBarHeight - navBarHeight;
  277 +};
  278 +
  279 +onMounted(() => {
  280 + calcScrollHeight();
  281 +});
  282 +
  283 +// 返回上一页
  284 +const goBack = () => {
  285 + uni.navigateBack({
  286 + delta: 1,
  287 + fail: () => {
  288 + // 如果无法返回(比如直接打开该页面),跳转到首页
  289 + uni.switchTab({ url: '/pages/xunji/xunji' });
  290 + },
  291 + });
  292 +};
  293 +</script>
  294 +
  295 +<style lang="scss" scoped>
  296 +// 颜色变量
  297 +$bg-color: #1a1a1a;
  298 +$card-bg: rgba(255, 255, 255, 0.05);
  299 +$border-color: rgba(255, 255, 255, 0.08);
  300 +$text-primary: #ffffff;
  301 +$text-secondary: rgba(255, 255, 255, 0.75);
  302 +$text-tertiary: rgba(255, 255, 255, 0.45);
  303 +$highlight-color: #ff6b00;
  304 +$highlight-dim: rgba(255, 107, 0, 0.15);
  305 +
  306 +.agreement-page {
  307 + width: 100%;
  308 + height: 100vh;
  309 + background-color: $bg-color;
  310 + color: $text-primary;
  311 + display: flex;
  312 + flex-direction: column;
  313 +}
  314 +
  315 +.agreement-scroll {
  316 + flex: 1;
  317 +}
  318 +
  319 +.agreement-content {
  320 + padding: 24rpx 30rpx 60rpx;
  321 +}
  322 +
  323 +// 协议头部
  324 +.agreement-header {
  325 + display: flex;
  326 + flex-direction: column;
  327 + align-items: center;
  328 + padding: 40rpx 0 30rpx;
  329 + border-bottom: 2rpx solid $border-color;
  330 + margin-bottom: 30rpx;
  331 +
  332 + .agreement-title {
  333 + font-size: 40rpx;
  334 + font-weight: bold;
  335 + color: $text-primary;
  336 + letter-spacing: 2rpx;
  337 + margin-bottom: 16rpx;
  338 + }
  339 +
  340 + .agreement-date {
  341 + font-size: 24rpx;
  342 + color: $text-tertiary;
  343 + line-height: 1.6;
  344 + }
  345 +}
  346 +
  347 +// 协议章节
  348 +.agreement-section {
  349 + margin-bottom: 36rpx;
  350 +
  351 + .section-title {
  352 + margin-bottom: 18rpx;
  353 + padding-left: 14rpx;
  354 + border-left: 5rpx solid $highlight-color;
  355 +
  356 + .title-text {
  357 + font-size: 32rpx;
  358 + font-weight: bold;
  359 + color: $text-primary;
  360 + letter-spacing: 1rpx;
  361 + }
  362 + }
  363 +
  364 + .section-body {
  365 + padding-left: 8rpx;
  366 + }
  367 +
  368 + .section-text {
  369 + display: block;
  370 + font-size: 28rpx;
  371 + color: $text-secondary;
  372 + line-height: 1.85;
  373 + margin-bottom: 14rpx;
  374 + text-align: justify;
  375 +
  376 + &.indent-text {
  377 + padding-left: 30rpx;
  378 + }
  379 +
  380 + .text-bold {
  381 + font-weight: bold;
  382 + color: $text-primary;
  383 + }
  384 + }
  385 +}
  386 +
  387 +// 底部区域
  388 +.agreement-footer {
  389 + text-align: center;
  390 + padding: 40rpx 0 20rpx;
  391 +
  392 + .footer-text {
  393 + font-size: 26rpx;
  394 + color: $text-tertiary;
  395 + }
  396 +}
  397 +</style>
@@ -2,11 +2,7 @@ @@ -2,11 +2,7 @@
2 <view class="login-wrapper"> 2 <view class="login-wrapper">
3 <!-- 1. Logo 及品牌信息 --> 3 <!-- 1. Logo 及品牌信息 -->
4 <view class="logo-section"> 4 <view class="logo-section">
5 - <view class="logo-box">  
6 - <!-- 优化:为了兼容微信小程序等跨端环境,原生 SVG 转换为 Base64 嵌入标准 image 标签中,确保完美渲染 -->  
7 - <image class="logo-img" :src="logoSvgBase64" mode="aspectFit" />  
8 - </view>  
9 - <view class="brand-title">自己练</view> 5 + <image class="logo-img" :src="appLogo" mode="aspectFit" />
10 <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view> 6 <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view>
11 </view> 7 </view>
12 8
@@ -55,7 +51,7 @@ @@ -55,7 +51,7 @@
55 <text class="form-label">登录密码</text> 51 <text class="form-label">登录密码</text>
56 <view class="input-box"> 52 <view class="input-box">
57 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 53 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
58 - <input type="password" v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3" 54 + <input v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3"
59 class="native-input" :password="!passwordShow" /> 55 class="native-input" :password="!passwordShow" />
60 <!-- --> 56 <!-- -->
61 <u-icon :name="passwordShow ? 'eye' : 'eye-off'" size="20" color="#a1a8b3" class="eye-icon" 57 <u-icon :name="passwordShow ? 'eye' : 'eye-off'" size="20" color="#a1a8b3" class="eye-icon"
@@ -88,14 +84,13 @@ @@ -88,14 +84,13 @@
88 </template> 84 </template>
89 85
90 <script setup> 86 <script setup>
91 -import { ref, onUnmounted } from 'vue'; 87 +import { ref, } from 'vue';
92 import sheep from '@/sheep'; 88 import sheep from '@/sheep';
93 import AuthUtil from '@/sheep/api/member/auth'; 89 import AuthUtil from '@/sheep/api/member/auth';
94 import { onHide } from '@dcloudio/uni-app'; 90 import { onHide } from '@dcloudio/uni-app';
  91 +import {appLogo} from '@/sheep/config/index'
  92 +
95 93
96 -// 用于跨端兼容渲染的 Base64 编码火焰 SVG (绿渐变)  
97 -const logoSvgBase64 =  
98 - 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="%238fc31f" /><stop offset="100%" stop-color="%2300b074" /></linearGradient></defs><path d="M12 2C12 2 6 7.5 6 13C6 16.8 9.1 20 12 20C14.9 20 18 16.8 18 13C18 7.5 12 2 12 2ZM12 16C10.3 16 9 14.7 9 13C9 10.5 12 7.5 12 7.5C12 7.5 15 10.5 15 13C15 14.7 13.7 16 12 16Z" fill="url(%23g)" /></svg>';  
99 94
100 const activeTab = ref('sms'); // sms: 免密, password: 密码 95 const activeTab = ref('sms'); // sms: 免密, password: 密码
101 const phoneNumber = ref(''); 96 const phoneNumber = ref('');
@@ -267,19 +262,10 @@ const goToForgetPassword = () => { @@ -267,19 +262,10 @@ const goToForgetPassword = () => {
267 const goToAgreement = (type) => { 262 const goToAgreement = (type) => {
268 const url = 263 const url =
269 type === 'service' 264 type === 'service'
270 - ? '/pages/public/richtext?id=service'  
271 - : '/pages/public/richtext?id=privacy';  
272 - uni.navigateTo({  
273 - url,  
274 - fail: () => {  
275 - uni.showToast({  
276 - title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,  
277 - icon: 'none',  
278 - });  
279 - },  
280 - });  
281 -};  
282 - 265 + ? '/pages5/pages/user/yonhu-xieyi'
  266 + : '/pages5/pages/user/yinsi-xieyi';
  267 + uni.navigateTo({ url });
  268 +}
283 // 页面卸载生命周期:清除定时器,规避潜在的内存泄露 269 // 页面卸载生命周期:清除定时器,规避潜在的内存泄露
284 onHide(() => { 270 onHide(() => {
285 if (timer) { 271 if (timer) {
@@ -304,38 +290,19 @@ onHide(() => { @@ -304,38 +290,19 @@ onHide(() => {
304 display: flex; 290 display: flex;
305 flex-direction: column; 291 flex-direction: column;
306 align-items: center; 292 align-items: center;
307 - margin-bottom: 70rpx; 293 + margin-bottom: 60rpx;
308 margin-top: 40rpx; 294 margin-top: 40rpx;
309 295
310 - .logo-box {  
311 - width: 140rpx;  
312 - height: 140rpx;  
313 - border-radius: 40rpx;  
314 - background: #ffffff;  
315 - border: 3rpx solid #d2ee9e;  
316 - box-shadow: 0 16rpx 40rpx rgba(111, 214, 32, 0.12);  
317 - display: flex;  
318 - justify-content: center;  
319 - align-items: center;  
320 - margin-bottom: 30rpx;  
321 -  
322 .logo-img { 296 .logo-img {
323 - width: 76rpx;  
324 - height: 76rpx;  
325 - }  
326 - }  
327 -  
328 - .brand-title {  
329 - font-size: 46rpx;  
330 - font-weight: bold;  
331 - color: #0a1931;  
332 - letter-spacing: 2rpx; 297 + width: 320rpx;
  298 + height: 320rpx;
333 } 299 }
334 300
335 .brand-slogan { 301 .brand-slogan {
336 font-size: 26rpx; 302 font-size: 26rpx;
337 color: #9097a3; 303 color: #9097a3;
338 - margin-top: 12rpx; 304 + margin-top: 20rpx;
  305 + letter-spacing: 2rpx;
339 } 306 }
340 } 307 }
341 308
1 <template> 1 <template>
2 <view class="login-wrapper"> 2 <view class="login-wrapper">
3 - <!-- 主表单区域:复用与登录页相同的 form-container 设计语言 --> 3 + <!-- 主表单区域 -->
4 <view class="form-container"> 4 <view class="form-container">
5 <!-- 1. 手机号码 --> 5 <!-- 1. 手机号码 -->
6 <view class="form-item-wrapper"> 6 <view class="form-item-wrapper">
7 <text class="form-label">手机号码</text> 7 <text class="form-label">手机号码</text>
8 <view class="input-box"> 8 <view class="input-box">
  9 + <u-input v-model="formData.phone" type="number" maxlength="11" placeholder="请输入您的手机号"
  10 + placeholder-style="color: #a1a8b3" border="none" class="custom-u-input">
  11 + <template #prefix>
9 <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" /> 12 <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
10 - <input type="number" v-model="phone" placeholder="请输入您的手机号" placeholder-style="color: #a1a8b3" maxlength="11"  
11 - class="native-input" /> 13 + </template>
  14 + </u-input>
12 </view> 15 </view>
13 </view> 16 </view>
14 17
15 - <!-- 2. 短信验证码:复用登录页免密模式的双列布局 --> 18 + <!-- 2. 短信验证码 -->
16 <view class="form-item-wrapper"> 19 <view class="form-item-wrapper">
17 <text class="form-label">验证码</text> 20 <text class="form-label">验证码</text>
18 <view class="code-row"> 21 <view class="code-row">
19 <view class="input-box flex-1"> 22 <view class="input-box flex-1">
  23 + <u-input v-model="formData.code" type="number" maxlength="6" placeholder="6位验证码"
  24 + placeholder-style="color: #a1a8b3" border="none" class="custom-u-input">
  25 + <template #prefix>
20 <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" /> 26 <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
21 - <input type="number" v-model="code" placeholder="6位验证码" placeholder-style="color: #a1a8b3" maxlength="6"  
22 - class="native-input" /> 27 + </template>
  28 + </u-input>
23 </view> 29 </view>
  30 +
24 <!-- 获取验证码按钮 --> 31 <!-- 获取验证码按钮 -->
25 - <view class="code-btn" :class="{ disabled: countdown > 0 || isSending }" @click="handleSendCode"> 32 + <button class="code-btn" :class="{ disabled: isCodeBtnDisabled }" :disabled="isCodeBtnDisabled"
  33 + @click="handleSendCode">
26 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text> 34 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
27 - </view> 35 + </button>
28 </view> 36 </view>
29 </view> 37 </view>
30 38
@@ -32,11 +40,12 @@ @@ -32,11 +40,12 @@
32 <view class="form-item-wrapper"> 40 <view class="form-item-wrapper">
33 <text class="form-label">设置新密码</text> 41 <text class="form-label">设置新密码</text>
34 <view class="input-box"> 42 <view class="input-box">
  43 + <u-input v-model="formData.password" type="password" placeholder="请设置您的新密码" placeholder-style="color: #a1a8b3"
  44 + border="none" class="custom-u-input">
  45 + <template #prefix>
35 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 46 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
36 - <input :type="showPassword ? 'text' : 'password'" v-model="password" placeholder="请设置您的新密码"  
37 - placeholder-style="color: #a1a8b3" class="native-input" />  
38 - <u-icon :name="showPassword ? 'eye-fill' : 'eye'" size="20" color="#a1a8b3" class="eye-icon"  
39 - @click="showPassword = !showPassword" /> 47 + </template>
  48 + </u-input>
40 </view> 49 </view>
41 <text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text> 50 <text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text>
42 </view> 51 </view>
@@ -45,16 +54,17 @@ @@ -45,16 +54,17 @@
45 <view class="form-item-wrapper"> 54 <view class="form-item-wrapper">
46 <text class="form-label">确认密码</text> 55 <text class="form-label">确认密码</text>
47 <view class="input-box"> 56 <view class="input-box">
  57 + <u-input v-model="formData.confirmPassword" type="password" placeholder="请再次输入新密码"
  58 + placeholder-style="color: #a1a8b3" border="none" class="custom-u-input">
  59 + <template #prefix>
48 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 60 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
49 - <input :type="showConfirmPassword ? 'text' : 'password'" v-model="confirmPassword" placeholder="请再次输入新密码"  
50 - placeholder-style="color: #a1a8b3" class="native-input" />  
51 - <u-icon :name="showConfirmPassword ? 'eye-fill' : 'eye'" size="20" color="#a1a8b3" class="eye-icon"  
52 - @click="showConfirmPassword = !showConfirmPassword" /> 61 + </template>
  62 + </u-input>
53 </view> 63 </view>
54 </view> 64 </view>
55 </view> 65 </view>
56 66
57 - <!-- 5. 提交按钮:复用登录页高质感渐变色圆角按钮 --> 67 + <!-- 5. 提交按钮 -->
58 <view class="submit-btn" :class="{ 'disabled-btn': isSubmitting }" @click="handleSubmit"> 68 <view class="submit-btn" :class="{ 'disabled-btn': isSubmitting }" @click="handleSubmit">
59 <text>{{ isSubmitting ? '保存中...' : '确认修改' }}</text> 69 <text>{{ isSubmitting ? '保存中...' : '确认修改' }}</text>
60 </view> 70 </view>
@@ -62,95 +72,107 @@ @@ -62,95 +72,107 @@
62 </template> 72 </template>
63 73
64 <script setup> 74 <script setup>
65 -import { ref } from 'vue'; 75 +import { ref, reactive, computed, onBeforeUnmount } from 'vue';
  76 +import { onLoad, onHide, onUnload } from '@dcloudio/uni-app';
66 import AuthUtil from '@/sheep/api/member/auth'; 77 import AuthUtil from '@/sheep/api/member/auth';
67 -import { onHide, onLoad } from '@dcloudio/uni-app';  
68 -const phone = ref('');  
69 -const code = ref('');  
70 -const password = ref('');  
71 -const confirmPassword = ref('');  
72 78
73 -// 密码显示隐藏控制  
74 -const showPassword = ref(false);  
75 -const showConfirmPassword = ref(false); 79 +// 1. 表单响应式数据归类
  80 +const formData = reactive({
  81 + phone: '',
  82 + code: '',
  83 + password: '',
  84 + confirmPassword: '',
  85 +});
76 86
77 -// 防抖及倒计时状态 87 +// 2. 交互与状态控制
78 const isSending = ref(false); 88 const isSending = ref(false);
79 const isSubmitting = ref(false); 89 const isSubmitting = ref(false);
80 const countdown = ref(0); 90 const countdown = ref(0);
  91 +const isModify = ref(false);
81 let timer = null; 92 let timer = null;
82 93
83 -// 验证手机号码格式  
84 -const validatePhone = (num) => {  
85 - return /^1[3-9]\d{9}$/.test(num); 94 +// 计算属性:验证码按钮禁用状态
  95 +const isCodeBtnDisabled = computed(() => countdown.value > 0 || isSending.value);
  96 +
  97 +// 3. 正则表达式配置
  98 +const REGEX = {
  99 + PHONE: /^1[3-9]\d{9}$/,
  100 + STRONG_PWD: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/,
  101 +};
  102 +
  103 +/** Toast 提示封装 */
  104 +const showToast = (title, icon = 'none') => {
  105 + uni.showToast({ title, icon });
86 }; 106 };
87 107
88 -// 强密码复杂度检测:8~16位,大小写、数字、特殊字符  
89 -const validatePasswordStrength = (pwd) => {  
90 - const reg =  
91 - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/;  
92 - return reg.test(pwd); 108 +/** 清除倒计时定时器 */
  109 +const clearCountdownTimer = () => {
  110 + if (timer) {
  111 + clearInterval(timer);
  112 + timer = null;
  113 + }
93 }; 114 };
94 115
95 -// 发送短信验证码 116 +/** 开启倒计时 */
  117 +const startCountdown = (seconds = 60) => {
  118 + clearCountdownTimer();
  119 + countdown.value = seconds;
  120 + timer = setInterval(() => {
  121 + countdown.value--;
  122 + if (countdown.value <= 0) {
  123 + clearCountdownTimer();
  124 + }
  125 + }, 1000);
  126 +};
  127 +
  128 +// 4. 事件处理:发送短信验证码
96 const handleSendCode = async () => { 129 const handleSendCode = async () => {
97 - if (countdown.value > 0 || isSending.value) return; 130 + if (isCodeBtnDisabled.value) return;
98 131
99 - if (!phone.value) {  
100 - uni.showToast({ title: '请输入手机号码', icon: 'none' });  
101 - return; 132 + if (!formData.phone) {
  133 + return showToast('请输入手机号码');
102 } 134 }
103 - if (!validatePhone(phone.value)) {  
104 - uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });  
105 - return; 135 + if (!REGEX.PHONE.test(formData.phone)) {
  136 + return showToast('请输入正确的11位手机号码');
106 } 137 }
107 138
108 isSending.value = true; 139 isSending.value = true;
109 try { 140 try {
110 - const res = await AuthUtil.sendSmsCode({ phone: phone.value }); 141 + const res = await AuthUtil.sendSmsCode({ phone: formData.phone });
111 if (res && (res.code === 0 || res.data === true)) { 142 if (res && (res.code === 0 || res.data === true)) {
112 - uni.showToast({ title: '验证码已发送', icon: 'success' });  
113 - countdown.value = 60;  
114 - timer = setInterval(() => {  
115 - countdown.value--;  
116 - if (countdown.value <= 0) {  
117 - clearInterval(timer);  
118 - timer = null;  
119 - }  
120 - }, 1000); 143 + showToast('验证码已发送', 'success');
  144 + startCountdown(60);
121 } else { 145 } else {
122 - uni.showToast({ title: res.msg || '获取验证码失败', icon: 'none' }); 146 + showToast(res?.msg || '获取验证码失败');
123 } 147 }
124 } catch (err) { 148 } catch (err) {
125 console.error('发送验证码失败异常:', err); 149 console.error('发送验证码失败异常:', err);
  150 + showToast('网络开小差了,请稍后再试');
126 } finally { 151 } finally {
127 isSending.value = false; 152 isSending.value = false;
128 } 153 }
129 }; 154 };
130 155
131 -// 保存并提交重置密码 156 +// 5. 事件处理:提交修改密码
132 const handleSubmit = async () => { 157 const handleSubmit = async () => {
133 if (isSubmitting.value) return; 158 if (isSubmitting.value) return;
134 159
135 - if (!phone.value || !validatePhone(phone.value)) {  
136 - uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });  
137 - return; 160 + // 表单完整性校验
  161 + if (!formData.phone || !REGEX.PHONE.test(formData.phone)) {
  162 + return showToast('请输入正确的11位手机号码');
138 } 163 }
139 - if (!code.value || code.value.length < 4) {  
140 - uni.showToast({ title: '请输入短信验证码', icon: 'none' });  
141 - return; 164 + if (!formData.code || formData.code.length < 4) {
  165 + return showToast('请输入正确的短信验证码');
142 } 166 }
143 - if (!password.value) {  
144 - uni.showToast({ title: '请输入您的新密码', icon: 'none' });  
145 - return; 167 + if (!formData.password) {
  168 + return showToast('请输入您的新密码');
146 } 169 }
147 - // if (!validatePasswordStrength(password.value)) {  
148 - // uni.showToast({ title: '密码须为8~16位并包含大小写字母、数字及特殊字符', icon: 'none' });  
149 - // return; 170 + // 如需强密码校验,取消下一行注释:
  171 + // if (!REGEX.STRONG_PWD.test(formData.password)) {
  172 + // return showToast('密码须为8~16位并包含大小写字母、数字及特殊字符');
150 // } 173 // }
151 - if (password.value !== confirmPassword.value) {  
152 - uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });  
153 - return; 174 + if (formData.password !== formData.confirmPassword) {
  175 + return showToast('两次输入的密码不一致');
154 } 176 }
155 177
156 isSubmitting.value = true; 178 isSubmitting.value = true;
@@ -158,48 +180,41 @@ const handleSubmit = async () => { @@ -158,48 +180,41 @@ const handleSubmit = async () => {
158 180
159 try { 181 try {
160 const res = await AuthUtil.setPassword({ 182 const res = await AuthUtil.setPassword({
161 - phone: phone.value,  
162 - code: code.value,  
163 - password: password.value, 183 + phone: formData.phone,
  184 + code: formData.code,
  185 + password: formData.password,
164 }); 186 });
165 187
  188 + if (res && res.code === 0) {
  189 + showToast('密码修改成功', 'success');
  190 + setTimeout(() => {
166 uni.redirectTo({ 191 uni.redirectTo({
167 url: '/pages7/pages/index/login', 192 url: '/pages7/pages/index/login',
168 }); 193 });
  194 + }, 1500);
  195 + } else {
  196 + showToast(res?.msg || '修改失败,请重试');
  197 + }
169 } catch (err) { 198 } catch (err) {
170 console.error('重置密码运行异常:', err); 199 console.error('重置密码运行异常:', err);
  200 + showToast('网络繁忙,请稍后再试');
171 } finally { 201 } finally {
172 isSubmitting.value = false; 202 isSubmitting.value = false;
173 uni.hideLoading(); 203 uni.hideLoading();
174 } 204 }
175 }; 205 };
176 206
177 -// 生命周期管理:清除定时器  
178 -onHide(() => {  
179 - if (timer) {  
180 - clearInterval(timer);  
181 - timer = null;  
182 - }  
183 -});  
184 -  
185 -const isForgetPassword = ref(false)  
186 -  
187 -const isModify = ref(false);  
188 - 207 +// 6. 生命周期管理
189 onLoad((options) => { 208 onLoad((options) => {
190 - // 路由参数是字符串,需要转布尔判断  
191 - isModify.value = options.isModify === 'true';  
192 - console.log('isModify.value=', isModify.value);  
193 -  
194 - if (isModify.value) {  
195 - // 修改密码场景  
196 - uni.setNavigationBarTitle({ title: '修改密码' });  
197 - } else {  
198 - // 忘记密码场景  
199 - uni.setNavigationBarTitle({ title: '找回密码' });  
200 - } 209 + isModify.value = options?.isModify === 'true';
  210 + const pageTitle = isModify.value ? '修改密码' : '找回密码';
  211 + uni.setNavigationBarTitle({ title: pageTitle });
201 }); 212 });
202 213
  214 +// 清理定时器,防止泄漏
  215 +onHide(clearCountdownTimer);
  216 +onUnload(clearCountdownTimer);
  217 +onBeforeUnmount(clearCountdownTimer);
203 </script> 218 </script>
204 219
205 <style lang="scss" scoped> 220 <style lang="scss" scoped>
@@ -209,10 +224,9 @@ onLoad((options) => { @@ -209,10 +224,9 @@ onLoad((options) => {
209 height: calc(100vh - 44px); 224 height: calc(100vh - 44px);
210 // #endif 225 // #endif
211 background-color: #ffffff; 226 background-color: #ffffff;
212 - padding: 40rpx 50rpx; // 移除顶部大 Logo 后,缩减顶部 Padding 保持呼吸感 227 + padding: 40rpx 50rpx;
213 box-sizing: border-box; 228 box-sizing: border-box;
214 229
215 - // 1. 表单容器  
216 .form-container { 230 .form-container {
217 margin-top: 20rpx; 231 margin-top: 20rpx;
218 232
@@ -227,7 +241,6 @@ onLoad((options) => { @@ -227,7 +241,6 @@ onLoad((options) => {
227 margin-bottom: 16rpx; 241 margin-bottom: 16rpx;
228 } 242 }
229 243
230 - // 输入框提示语  
231 .input-hint { 244 .input-hint {
232 display: block; 245 display: block;
233 font-size: 24rpx; 246 font-size: 24rpx;
@@ -244,26 +257,21 @@ onLoad((options) => { @@ -244,26 +257,21 @@ onLoad((options) => {
244 border-radius: 24rpx; 257 border-radius: 24rpx;
245 display: flex; 258 display: flex;
246 align-items: center; 259 align-items: center;
247 - padding: 0 30rpx; 260 + padding: 0 20rpx;
248 box-sizing: border-box; 261 box-sizing: border-box;
249 262
250 - .input-icon {  
251 - margin-right: 16rpx;  
252 - }  
253 -  
254 - .eye-icon {  
255 - padding: 10rpx;  
256 - }  
257 -  
258 - .native-input {  
259 - flex: 1; 263 + .custom-u-input {
  264 + width: 100%;
260 height: 100%; 265 height: 100%;
261 font-size: 28rpx; 266 font-size: 28rpx;
262 - color: #0a1931; 267 + }
  268 +
  269 + .input-icon {
  270 + margin-right: 12rpx;
263 } 271 }
264 } 272 }
265 273
266 - // 验证码双列布局 274 + /* 双列布局:验证码 */
267 .code-row { 275 .code-row {
268 display: flex; 276 display: flex;
269 align-items: center; 277 align-items: center;
@@ -285,8 +293,14 @@ onLoad((options) => { @@ -285,8 +293,14 @@ onLoad((options) => {
285 font-size: 26rpx; 293 font-size: 26rpx;
286 color: #8fc31f; 294 color: #8fc31f;
287 font-weight: bold; 295 font-weight: bold;
  296 + padding: 0;
  297 + line-height: normal;
288 transition: all 0.2s ease; 298 transition: all 0.2s ease;
289 299
  300 + &::after {
  301 + border: none;
  302 + }
  303 +
290 &:active { 304 &:active {
291 opacity: 0.8; 305 opacity: 0.8;
292 } 306 }
@@ -302,7 +316,6 @@ onLoad((options) => { @@ -302,7 +316,6 @@ onLoad((options) => {
302 } 316 }
303 } 317 }
304 318
305 - // 2. 确认修改按钮 (登录页同款高级渐变微立体阴影)  
306 .submit-btn { 319 .submit-btn {
307 height: 104rpx; 320 height: 104rpx;
308 border-radius: 24rpx; 321 border-radius: 24rpx;
@@ -327,10 +340,6 @@ onLoad((options) => { @@ -327,10 +340,6 @@ onLoad((options) => {
327 opacity: 0.75; 340 opacity: 0.75;
328 pointer-events: none; 341 pointer-events: none;
329 } 342 }
330 -  
331 - .btn-arrow {  
332 - margin-left: 10rpx;  
333 - }  
334 } 343 }
335 } 344 }
336 </style> 345 </style>
@@ -356,7 +356,7 @@ const UserApi = { @@ -356,7 +356,7 @@ const UserApi = {
356 // 运动轨迹 356 // 运动轨迹
357 getTrajectory: () => { 357 getTrajectory: () => {
358 return request({ 358 return request({
359 - url: '/app/student/motion', 359 + url: '/app/user/motion',
360 method: 'GET', 360 method: 'GET',
361 custom: { 361 custom: {
362 showLoading: false, 362 showLoading: false,
@@ -367,7 +367,7 @@ const UserApi = { @@ -367,7 +367,7 @@ const UserApi = {
367 // 更换背景图 367 // 更换背景图
368 updateBgImage: (data) => { 368 updateBgImage: (data) => {
369 return request({ 369 return request({
370 - url: '/app/student/updateBackgroundImage', 370 + url: '/app/user/updateBackgroundImage',
371 method: 'POST', 371 method: 'POST',
372 data, 372 data,
373 custom: { 373 custom: {
@@ -380,7 +380,7 @@ const UserApi = { @@ -380,7 +380,7 @@ const UserApi = {
380 // 保存个性签名 380 // 保存个性签名
381 updateSignature: (data) => { 381 updateSignature: (data) => {
382 return request({ 382 return request({
383 - url: '/app/student/updateSignature', 383 + url: '/app/user/updateSignature',
384 method: 'POST', 384 method: 'POST',
385 data, 385 data,
386 custom: { 386 custom: {
1 import request from '@/sheep/request'; 1 import request from '@/sheep/request';
2 2
3 const ExercisesApi = { 3 const ExercisesApi = {
4 - // // 获取动作列表  
5 - // // 根据分类获取动作列表(简化版本)  
6 - // getExercisesByCategory: (categoriesId, subCategoriesId = null) => {  
7 - // return ExercisesApi.getExercises({  
8 - // categoriesId,  
9 - // subCategoriesId  
10 - // });  
11 - // },  
12 4
13 - // // 根据器械获取动作列表  
14 - // getExercisesByEquipment: (categoriesId, equipmentsId) => {  
15 - // return ExercisesApi.getExercises({  
16 - // categoriesId,  
17 - // equipmentsId  
18 - // });  
19 - // }, 5 +
  6 + // 获得动作列表(部位 → 器械 → 动作 三级分组)
  7 + getExercisesByCategory: (params) => {
  8 + return request({
  9 + url: '/app/motion/exercises/get-by-category',
  10 + method: 'GET',
  11 + params,
  12 + });
  13 + },
  14 +
  15 +
20 16
21 // 1.获取动作锻炼部位列表 17 // 1.获取动作锻炼部位列表
22 getMotionPart: (id) => { 18 getMotionPart: (id) => {
@@ -159,6 +159,9 @@ const QueryPlanApi = { @@ -159,6 +159,9 @@ const QueryPlanApi = {
159 params: { 159 params: {
160 Id, 160 Id,
161 }, 161 },
  162 + custom:{
  163 + showLoading:false,
  164 + }
162 }); 165 });
163 }, 166 },
164 167
@@ -17,6 +17,7 @@ export const tenantId = import.meta.env.SHOPRO_TENANT_ID; @@ -17,6 +17,7 @@ export const tenantId = import.meta.env.SHOPRO_TENANT_ID;
17 export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH; 17 export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH;
18 export const h5Url = import.meta.env.SHOPRO_H5_URL; 18 export const h5Url = import.meta.env.SHOPRO_H5_URL;
19 export const defaultAvatar = import.meta.env.DEFAULT_AVATAR; 19 export const defaultAvatar = import.meta.env.DEFAULT_AVATAR;
  20 +export const appLogo = import.meta.env.APP_LOGO;
20 export default { 21 export default {
21 baseUrl, 22 baseUrl,
22 apiPath, 23 apiPath,
@@ -25,4 +26,5 @@ export default { @@ -25,4 +26,5 @@ export default {
25 websocketPath, 26 websocketPath,
26 h5Url, 27 h5Url,
27 defaultAvatar, 28 defaultAvatar,
  29 + appLogo,
28 }; 30 };
  1 +/**
  2 + * 健康资料表单数据字典
  3 + * 集中管理所有选项配置和文本映射,便于复用和维护
  4 + */
  5 +
  6 +// ==================== 选项配置 ====================
  7 +
  8 +export const GENDER_OPTIONS = [
  9 + { label: '男', value: 1 },
  10 + { label: '女', value: 2 },
  11 +];
  12 +
  13 +export const FITNESS_FOUNDATION_OPTIONS = [
  14 + { label: '有', value: 1 },
  15 + { label: '无', value: 2 },
  16 +];
  17 +
  18 +export const TRAINING_FREQUENCY_OPTIONS = [
  19 + { label: '1练/2练/3练', value: 1 },
  20 + { label: '4练/5练/6练', value: 2 },
  21 +];
  22 +
  23 +export const TARGET_MUSCLE_OPTIONS = [
  24 + { label: '肩颈', value: '肩颈' },
  25 + { label: '斜方肌', value: '斜方肌' },
  26 + { label: '手臂', value: '手臂' },
  27 + { label: '胸部', value: '胸部' },
  28 + { label: '背部', value: '背部' },
  29 + { label: '腹部', value: '腹部' },
  30 + { label: '臀部', value: '臀部' },
  31 + { label: '腿部', value: '腿部' },
  32 +];
  33 +
  34 +export const TRAINING_GOAL_OPTIONS = [
  35 + { label: '减脂', value: 1 },
  36 + { label: '增肌', value: 2 },
  37 + { label: '塑形', value: 3 },
  38 + { label: '拉伸/体态调整', value: 4 },
  39 +];
  40 +
  41 +export const PAIN_AREA_OPTIONS = [
  42 + { label: '肩颈', value: '肩颈' },
  43 + { label: '手腕', value: '手腕' },
  44 + { label: '腰部', value: '腰部' },
  45 + { label: '脚踝', value: '脚踝' },
  46 + { label: '膝盖', value: '膝盖' },
  47 +];
  48 +
  49 +export const DISEASE_OPTIONS = [
  50 + { label: '有', value: 1 },
  51 + { label: '无', value: 2 },
  52 +];
  53 +
  54 +export const MEDICATION_OPTIONS = [
  55 + { label: '是', value: 1 },
  56 + { label: '否', value: 2 },
  57 +];
  58 +
  59 +export const REWARD_OPTIONS = [
  60 + { label: '买件新衣服/装备', value: 1 },
  61 + { label: '去旅行', value: 2 },
  62 + { label: '和朋友聚会', value: 3 },
  63 + { label: '其他', value: 4 },
  64 +];
  65 +
  66 +export const FITNESS_SCENE_OPTIONS = [
  67 + { label: '健身房', value: 1 },
  68 + { label: '家', value: 2 },
  69 + { label: '宿舍', value: 3 },
  70 +];
  71 +
  72 +// ==================== 汇总映射表 ====================
  73 +
  74 +/** 表单选项字典,按字段 key 索引 */
  75 +export const formOptions = {
  76 + gender: GENDER_OPTIONS,
  77 + hasFitnessFoundation: FITNESS_FOUNDATION_OPTIONS,
  78 + acceptableTrainingFrequency: TRAINING_FREQUENCY_OPTIONS,
  79 + targetMuscleParts: TARGET_MUSCLE_OPTIONS,
  80 + trainingGoal: TRAINING_GOAL_OPTIONS,
  81 + painAreas: PAIN_AREA_OPTIONS,
  82 + hasDisease: DISEASE_OPTIONS,
  83 + isTakingMedication: MEDICATION_OPTIONS,
  84 + rewardMethod: REWARD_OPTIONS,
  85 + fitnessScene: FITNESS_SCENE_OPTIONS,
  86 +};
  87 +
  88 +// ==================== 文本映射表 ====================
  89 +
  90 +/** 训练目标文案映射 */
  91 +export const GOAL_LABEL_MAP = {
  92 + 1: '减脂',
  93 + 2: '增肌',
  94 + 3: '塑形',
  95 + 4: '拉伸/体态调整',
  96 +};
  97 +
  98 +/** 奖励方式文案映射 */
  99 +export const REWARD_LABEL_MAP = {
  100 + 1: '买件新衣服/装备',
  101 + 2: '去旅行',
  102 + 3: '和朋友聚会',
  103 + 4: '其他',
  104 +};
  105 +
  106 +/** 健身场景文案映射 */
  107 +export const SCENE_LABEL_MAP = {
  108 + 1: '健身房',
  109 + 2: '家',
  110 + 3: '宿舍',
  111 +};
  112 +
  113 +/**
  114 + * 通用查表格式化函数
  115 + * @param {*} value 待映射的值
  116 + * @param {Object} map 映射表
  117 + * @returns {string} 对应的文案,未匹配返回空字符串
  118 + */
  119 +export function lookupLabel(value, map) {
  120 + return map[value] || '';
  121 +}
@@ -11,7 +11,7 @@ import mpliveMainfestPlugin from './sheep/libs/mplive-manifest-plugin'; @@ -11,7 +11,7 @@ import mpliveMainfestPlugin from './sheep/libs/mplive-manifest-plugin';
11 export default ({ command, mode }) => { 11 export default ({ command, mode }) => {
12 const env = loadEnv(mode, __dirname, 'SHOPRO_'); 12 const env = loadEnv(mode, __dirname, 'SHOPRO_');
13 return { 13 return {
14 - envPrefix: 'SHOPRO_', 14 + envPrefix: ['SHOPRO_', 'APP_', 'DEFAULT_'],
15 plugins: [ 15 plugins: [
16 uni(), 16 uni(),
17 // viteCompression({ 17 // viteCompression({