Authored by Bad

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

- 新增用户协议、隐私协议及使用教程页面
- 调整每日模板与历史记录的数据展示逻辑
- 支持未来日期保存为训练模板并锁定当日开练功能

Too many changes to show.

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

@@ -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,7 +144,14 @@ @@ -144,7 +144,14 @@
144 "style": { 144 "style": {
145 "navigationBarTitleText": "" 145 "navigationBarTitleText": ""
146 } 146 }
147 - } 147 + },
  148 + {
  149 + "path": "pages/xunji/shiyon-jiaochen",
  150 + "style": {
  151 + "navigationBarTitleText": "使用教程"
  152 +
  153 + }
  154 + }
148 ] 155 ]
149 }, 156 },
150 { 157 {
@@ -209,7 +216,19 @@ @@ -209,7 +216,19 @@
209 "navigationBarTitleText": "设置", 216 "navigationBarTitleText": "设置",
210 "navigationStyle": "default" 217 "navigationStyle": "default"
211 } 218 }
212 - } 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 + }
  231 + }
213 ] 232 ]
214 } 233 }
215 ], 234 ],
@@ -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,32 +458,17 @@ $transition-tap: background-color 0.2s ease, opacity 0.2s ease; @@ -457,32 +458,17 @@ $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;  
482 - } 462 + .logout-label {
  463 + font-size: $font-body;
  464 + color: $color-text-tertiary;
483 } 465 }
484 } 466 }
485 467
  468 + .logout-item {
  469 + border-bottom: none;
  470 + }
  471 +
486 // 触摸反馈 472 // 触摸反馈
487 .card-hover { 473 .card-hover {
488 background-color: $color-bg-hover; 474 background-color: $color-bg-hover;
@@ -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,108 +792,70 @@ $radius-round: 40rpx; @@ -888,108 +792,70 @@ $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 -  
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 {  
912 - width: 50rpx;  
913 - height: 50rpx;  
914 - display: flex;  
915 - align-items: center;  
916 - justify-content: center;  
917 - background: #f2f3f5;  
918 - border-radius: 50%;  
919 - flex-shrink: 0;  
920 -}  
921 -  
922 -.date-text {  
923 - font-size: 28rpx;  
924 - white-space: nowrap;  
925 - overflow: hidden;  
926 - text-overflow: ellipsis;  
927 -}  
928 798
929 -.date-note-btn {  
930 - display: inline-flex;  
931 - align-items: center;  
932 - font-size: 28rpx;  
933 - color: #333;  
934 - background: #e7e7e9;  
935 - border-radius: 40rpx;  
936 - padding: 12rpx 28rpx;  
937 - border: none;  
938 - margin-right: 0;  
939 - flex-shrink: 0;  
940 -} 799 + .close-btn {
  800 + width: 50rpx;
  801 + height: 50rpx;
  802 + display: flex;
  803 + align-items: center;
  804 + justify-content: center;
  805 + background: #f2f3f5;
  806 + border-radius: 50%;
  807 + flex-shrink: 0;
  808 + }
941 809
942 -.go-train-btn {  
943 - display: flex;  
944 - align-items: center;  
945 - background: #ffc107;  
946 - border-radius: 40rpx;  
947 - padding: 12rpx 24rpx;  
948 - border: none;  
949 - flex-shrink: 0;  
950 -} 810 + .date-wrapper {
  811 + flex-grow: 1;
  812 + text-align: left;
  813 + }
951 814
952 -.go-train-text {  
953 - font-size: 28rpx;  
954 - color: #000;  
955 - margin-right: 8rpx;  
956 - font-weight: 500;  
957 -} 815 + .date-text {
  816 + font-size: 28rpx;
  817 + white-space: nowrap;
  818 + overflow: hidden;
  819 + text-overflow: ellipsis;
  820 + }
958 821
959 -.go-train-tag {  
960 - font-size: 22rpx;  
961 - color: #ffc107;  
962 - background: #000;  
963 - border-radius: 50%;  
964 - padding: 4rpx 8rpx;  
965 - font-weight: bold;  
966 -} 822 + .date-note-btn {
  823 + display: inline-flex;
  824 + align-items: center;
  825 + font-size: 28rpx;
  826 + color: #333;
  827 + background: #e7e7e9;
  828 + border-radius: 40rpx;
  829 + padding: 12rpx 28rpx;
  830 + border: none;
  831 + margin-right: 0;
  832 + flex-shrink: 0;
  833 + }
967 834
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; 835 + .go-train-btn {
  836 + display: flex;
  837 + align-items: center;
  838 + background: #ffc107;
  839 + border-radius: 40rpx;
  840 + padding: 12rpx 24rpx;
  841 + border: none;
  842 + flex-shrink: 0;
977 843
978 - .source-text {  
979 - color: #fff;  
980 - } 844 + .go-train-text {
  845 + font-size: 28rpx;
  846 + color: #000;
  847 + margin-right: 8rpx;
  848 + font-weight: 500;
  849 + }
981 850
982 - &.source-plan {  
983 - background: #3b82f6; /* 蓝色 - 计划 */  
984 - }  
985 - &.source-tpl {  
986 - background: #8b5cf6; /* 紫色 - 模板 */  
987 - }  
988 - &.source-custom {  
989 - background: #f59e0b; /* 橙色 - 自定义 */  
990 - }  
991 - &.source-free {  
992 - background: #10b981; /* 绿色 - 自由 */ 851 + .go-train-tag {
  852 + font-size: 22rpx;
  853 + color: #ffc107;
  854 + background: #000;
  855 + border-radius: 50%;
  856 + padding: 4rpx 8rpx;
  857 + font-weight: bold;
  858 + }
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 -  
1008 -.plan-header-img {  
1009 - display: none;  
1010 -  
1011 -}  
1012 -  
1013 -.plan-header-info {  
1014 - flex-grow: 1;  
1015 - margin-bottom: 32rpx;  
1016 -}  
1017 -  
1018 -.plan-header-top {  
1019 - display: flex;  
1020 - justify-content: space-between;  
1021 - align-items: center;  
1022 - margin-bottom: 12rpx;  
1023 -}  
1024 -  
1025 -.plan-heander {  
1026 - display: flex;  
1027 -}  
1028 872
1029 -.plan-title {  
1030 - font-size: 36rpx;  
1031 - font-weight: bold;  
1032 - display: block;  
1033 - margin-bottom: 12rpx;  
1034 - color: #fff;  
1035 -} 873 + .plan-header-img {
  874 + display: none;
  875 + }
1036 876
1037 -// 卡片头部的编辑按钮  
1038 -.editButton {  
1039 - display: flex;  
1040 - align-items: center;  
1041 - gap: 4rpx;  
1042 - color: #fff;  
1043 - font-size: 26rpx;  
1044 - opacity: 0.8;  
1045 - /* 让文字和图标稍微淡一点,和例图更像 */  
1046 - border: 1rpx solid rgba(255, 255, 255, 0.7);  
1047 - border-radius: 25rpx;  
1048 - padding: 6rpx 12rpx;  
1049 -} 877 + .plan-header-info {
  878 + flex-grow: 1;
  879 + margin-bottom: 32rpx;
  880 + }
1050 881
  882 + .plan-header-top {
  883 + display: flex;
  884 + justify-content: space-between;
  885 + align-items: center;
  886 + margin-bottom: 12rpx;
  887 + }
1051 888
1052 -.plan-meta {  
1053 - font-size: 26rpx;  
1054 - color: #ccc;  
1055 - display: block;  
1056 -} 889 + .plan-title {
  890 + font-size: 36rpx;
  891 + font-weight: bold;
  892 + display: block;
  893 + margin-bottom: 12rpx;
  894 + color: #fff;
  895 + }
1057 896
1058 -.plan-header-btns {  
1059 - display: flex;  
1060 - flex-direction: row;  
1061 - align-items: center;  
1062 - gap: 20rpx;  
1063 - justify-content: flex-start;  
1064 -} 897 + .editButton {
  898 + display: flex;
  899 + align-items: center;
  900 + gap: 4rpx;
  901 + color: #fff;
  902 + font-size: 26rpx;
  903 + opacity: 0.8;
  904 + border: 1rpx solid rgba(255, 255, 255, 0.7);
  905 + border-radius: 25rpx;
  906 + padding: 6rpx 12rpx;
  907 + }
1065 908
1066 -.plan-btn {  
1067 - font-size: 26rpx;  
1068 - border-radius: 40rpx;  
1069 - border: none;  
1070 - padding: 12rpx 28rpx;  
1071 - line-height: 1;  
1072 -} 909 + .plan-meta {
  910 + font-size: 26rpx;
  911 + color: #ccc;
  912 + display: block;
  913 + }
1073 914
1074 -.more-btn,  
1075 -.copy-btn {  
1076 - background: #fff;  
1077 - color: #000;  
1078 - border: none;  
1079 -} 915 + .plan-header-btns {
  916 + display: flex;
  917 + flex-direction: row;
  918 + align-items: center;
  919 + gap: 20rpx;
  920 + justify-content: flex-start;
  921 +
  922 + .plan-btn {
  923 + font-size: 26rpx;
  924 + border-radius: 40rpx;
  925 + border: none;
  926 + padding: 12rpx 28rpx;
  927 + line-height: 1;
  928 + }
1080 929
1081 -.go-train-btn-small {  
1082 - color: #000;  
1083 - display: flex;  
1084 - align-items: center;  
1085 - padding: 12rpx 20rpx;  
1086 - background: inherit;  
1087 -} 930 + .more-btn,
  931 + .copy-btn {
  932 + background: #fff;
  933 + color: #000;
  934 + border: none;
  935 + }
1088 936
1089 -.go-train-text-sm {  
1090 - font-size: 24rpx;  
1091 - margin-right: 4rpx;  
1092 - font-weight: 500;  
1093 -} 937 + .go-train-btn-small {
  938 + color: #000;
  939 + display: flex;
  940 + align-items: center;
  941 + padding: 12rpx 20rpx;
1094 942
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; 943 + .go-train-text-sm {
  944 + font-size: 24rpx;
  945 + margin-right: 4rpx;
  946 + font-weight: 500;
  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 965
1120 -.set-index {  
1121 - display: none;  
1122 -} 966 + .set-content {
  967 + flex-grow: 1;
  968 + color: inherit;
  969 + display: flex;
  970 + justify-content: space-between;
  971 + white-space: nowrap;
  972 + }
1123 973
  974 + .set-item {
  975 + display: flex;
  976 + align-items: center;
  977 + font-size: 26rpx;
  978 + color: #666;
  979 + justify-content: space-between;
1124 980
  981 + .set-left {
  982 + display: flex;
  983 + align-items: center;
  984 + gap: 8rpx;
  985 + }
1125 986
1126 -.set-content {  
1127 - flex-grow: 1;  
1128 - color: inherit;  
1129 - display: flex;  
1130 - justify-content: space-between;  
1131 - white-space: nowrap; 987 + .set-right {
  988 + display: flex;
  989 + gap: 20rpx;
  990 + }
1132 991
1133 -} 992 + .rest-time {
  993 + font-size: 24rpx;
  994 + margin-left: 40rpx;
  995 + flex-shrink: 0;
  996 + }
1134 997
1135 -.detai-data {  
1136 - white-space: nowrap; 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,153 +1013,71 @@ $radius-round: 40rpx; @@ -1145,153 +1013,71 @@ $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 -  
1150 -// 左侧:序号 + 图片  
1151 -.action-left {  
1152 - display: flex;  
1153 - align-items: center;  
1154 - gap: 16rpx;  
1155 - flex-shrink: 0;  
1156 -}  
1157 -  
1158 -// 大序号  
1159 -.action-index {  
1160 - font-size: 34rpx;  
1161 - font-weight: 600;  
1162 - color: #333;  
1163 - line-height: 1;  
1164 -}  
1165 -  
1166 -// 动作图片  
1167 -.action-img {  
1168 - width: 80rpx;  
1169 - height: 80rpx;  
1170 - border-radius: 8rpx;  
1171 -}  
1172 -  
1173 -// 中间区域  
1174 -.action-middle {  
1175 - flex: 1;  
1176 - display: flex;  
1177 - flex-direction: column;  
1178 -}  
1179 -  
1180 -// 上半部分:名称 + 重量(左对齐)  
1181 -.action-top {  
1182 - margin-bottom: 8rpx;  
1183 -}  
1184 -  
1185 -.action-name {  
1186 - font-size: 32rpx;  
1187 - font-weight: 500;  
1188 - color: #111;  
1189 - display: block;  
1190 -}  
1191 -  
1192 -.action-totalWeight {  
1193 - font-size: 26rpx;  
1194 - color: #333;  
1195 - margin-top: 6rpx;  
1196 - display: block;  
1197 -}  
1198 -  
1199 -// 下半部分:组次行 → 向左对齐图片 ✅ 核心  
1200 -.action-bottom {  
1201 - margin-top: 6rpx;  
1202 - margin-left: -96rpx;  
1203 - /* 往左移动,对齐图片位置 */  
1204 -}  
1205 1016
  1017 + .action-left {
  1018 + display: flex;
  1019 + align-items: center;
  1020 + gap: 16rpx;
  1021 + flex-shrink: 0;
  1022 + }
1206 1023
1207 -.set-item {  
1208 - display: flex;  
1209 - align-items: center;  
1210 - font-size: 26rpx;  
1211 - color: #666;  
1212 - justify-content: space-between;  
1213 -} 1024 + .action-index {
  1025 + font-size: 34rpx;
  1026 + font-weight: 600;
  1027 + color: #333;
  1028 + line-height: 1;
  1029 + }
1214 1030
1215 -.set-left {  
1216 - display: flex;  
1217 - align-items: center;  
1218 - gap: 8rpx;  
1219 - /* 控制 indexData、set-content、rest-time 之间的间距 */  
1220 -} 1031 + .action-img {
  1032 + width: 80rpx;
  1033 + height: 80rpx;
  1034 + border-radius: 8rpx;
  1035 + }
1221 1036
1222 -.set-right {  
1223 - display: flex;  
1224 - gap: 20rpx;  
1225 -} 1037 + .action-middle {
  1038 + flex: 1;
  1039 + display: flex;
  1040 + flex-direction: column;
  1041 + }
1226 1042
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 -} 1043 + .action-top {
  1044 + margin-bottom: 8rpx;
1241 1045
1242 -// .set-content {  
1243 -// color: #333;  
1244 -// } 1046 + .action-name {
  1047 + font-size: 32rpx;
  1048 + font-weight: 500;
  1049 + color: #111;
  1050 + display: block;
  1051 + }
1245 1052
1246 -.rest-time {  
1247 - // margin-left: 100rpx;  
1248 - font-size: 24rpx;  
1249 - // color: inherit;  
1250 - margin-left: 40rpx;  
1251 - flex-shrink: 0;  
1252 -} 1053 + .action-totalWeight {
  1054 + font-size: 26rpx;
  1055 + color: #333;
  1056 + margin-top: 6rpx;
  1057 + display: block;
  1058 + }
  1059 + }
1253 1060
1254 -.check {  
1255 - font-size: 26rpx;  
1256 - color: #333;  
1257 - margin-left: 20rpx;  
1258 - /* 这里控制和左边内容的距离,数值你可以自己调 */  
1259 - flex-shrink: 0;  
1260 - /* 防止被压缩 */  
1261 -} 1061 + .action-bottom {
  1062 + margin-top: 6rpx;
  1063 + margin-left: -96rpx;
  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 {  
1272 - flex-shrink: 0;  
1273 -} 1066 + .action-right {
  1067 + flex-shrink: 0;
1274 1068
1275 -.modify-btn {  
1276 - font-size: 26rpx;  
1277 - color: #333;  
1278 - background: #f2f3f5;  
1279 - border: none;  
1280 - border-radius: 40rpx;  
1281 - padding: 8rpx 20rpx;  
1282 - line-height: 1; 1069 + .modify-btn {
  1070 + font-size: 26rpx;
  1071 + color: #333;
  1072 + background: #f2f3f5;
  1073 + border: none;
  1074 + border-radius: 40rpx;
  1075 + padding: 8rpx 20rpx;
  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 {  
1307 - width: 360rpx;  
1308 - height: 360rpx;  
1309 - margin-bottom: 40rpx;  
1310 - transform: scale(3);  
1311 -} 1091 + .empty-img {
  1092 + width: 360rpx;
  1093 + height: 360rpx;
  1094 + margin-bottom: 40rpx;
  1095 + transform: scale(1.5);
  1096 + }
1312 1097
1313 -.empty-tip {  
1314 - font-size: 32rpx;  
1315 - color: #666;  
1316 - font-weight: 500;  
1317 - margin-bottom: 80rpx;  
1318 - letter-spacing: 2rpx;  
1319 -} 1098 + .empty-tip {
  1099 + font-size: 32rpx;
  1100 + color: #666;
  1101 + font-weight: 500;
  1102 + margin-bottom: 80rpx;
  1103 + letter-spacing: 2rpx;
  1104 + }
1320 1105
1321 -.add-train-btn {  
1322 - font-size: 30rpx;  
1323 - color: #000;  
1324 - background: $color-accent;  
1325 - border-radius: $radius-round;  
1326 - padding: 24rpx 80rpx;  
1327 - border: none;  
1328 - font-weight: 500;  
1329 - box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3); 1106 + .add-train-btn {
  1107 + font-size: 30rpx;
  1108 + color: #000;
  1109 + background: $color-accent;
  1110 + border-radius: 40rpx;
  1111 + padding: 24rpx 80rpx;
  1112 + border: none;
  1113 + font-weight: 500;
  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 {  
1352 - width: 100rpx;  
1353 - height: 100rpx;  
1354 - border-radius: 50%;  
1355 - margin-bottom: 16rpx;  
1356 -} 1136 + .course-icon {
  1137 + width: 100rpx;
  1138 + height: 100rpx;
  1139 + border-radius: 50%;
  1140 + margin-bottom: 16rpx;
  1141 + }
1357 1142
1358 -.course-title {  
1359 - font-size: 28rpx;  
1360 - color: #333;  
1361 - margin-bottom: 8rpx;  
1362 - font-weight: 500;  
1363 -} 1143 + .course-title {
  1144 + font-size: 28rpx;
  1145 + color: #333;
  1146 + margin-bottom: 8rpx;
  1147 + font-weight: 500;
  1148 + }
1364 1149
1365 -.course-desc {  
1366 - font-size: 24rpx;  
1367 - color: #999; 1150 + .course-desc {
  1151 + font-size: 24rpx;
  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,46 +1188,41 @@ $radius-round: 40rpx; @@ -1410,46 +1188,41 @@ $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 {  
1416 - width: 100%;  
1417 - height: 80rpx;  
1418 - font-size: 30rpx;  
1419 - // color: #ffffff;  
1420 - // background: #f5f5f5;  
1421 - border: none;  
1422 - // border-radius: 12rpx;  
1423 - display: flex;  
1424 - flex-direction: column;  
1425 - align-items: center;  
1426 - justify-content: center;  
1427 - gap: 15rpx; 1192 + .popup-option-btn {
  1193 + width: 100%;
  1194 + height: 80rpx;
  1195 + font-size: 30rpx;
  1196 + border: none;
  1197 + display: flex;
  1198 + flex-direction: column;
  1199 + align-items: center;
  1200 + justify-content: center;
  1201 + gap: 15rpx;
1428 1202
1429 - .small {  
1430 - font-size: 18rpx; 1203 + .small {
  1204 + font-size: 18rpx;
  1205 + }
1431 } 1206 }
1432 -  
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 {  
1450 - font-size: 34rpx;  
1451 - font-weight: 600;  
1452 - color: #111; 1221 + .popup-title {
  1222 + font-size: 34rpx;
  1223 + font-weight: 600;
  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) {  
1470 - border-bottom: 1rpx solid #eee;  
1471 -} 1241 + &:not(:last-child) {
  1242 + border-bottom: 1rpx solid #eee;
  1243 + }
1472 1244
1473 -.item-text {  
1474 - font-size: 30rpx;  
1475 - color: #333; 1245 + .item-text {
  1246 + font-size: 30rpx;
  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,56 +1262,46 @@ $radius-round: 40rpx; @@ -1481,56 +1262,46 @@ $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 {  
1494 - line-height: 1;  
1495 -} 1265 +.container {
  1266 + /* 按钮通用重置 */
  1267 + button {
  1268 + line-height: 1;
1496 1269
1497 -button::after {  
1498 - border: none; 1270 + &::after {
  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 {  
1514 - display: inline-block;  
1515 - padding: 10rpx 24rpx;  
1516 - border-radius: 32rpx;  
1517 - font-size: 26rpx;  
1518 - background: #f2f2f2;  
1519 - border: none;  
1520 - flex-shrink: 0;  
1521 - white-space: nowrap;  
1522 -} 1283 + .tab-btn {
  1284 + display: inline-block;
  1285 + padding: 10rpx 24rpx;
  1286 + border-radius: 32rpx;
  1287 + font-size: 26rpx;
  1288 + background: #f2f2f2;
  1289 + border: none;
  1290 + flex-shrink: 0;
  1291 + white-space: nowrap;
1523 1292
1524 -.tab-btn.active {  
1525 - background: #000;  
1526 - color: #fff; 1293 + &.active {
  1294 + background: #000;
  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 {  
1636 - margin-bottom: 0; 1402 + &:last-child {
  1403 + margin-bottom: 0;
  1404 + }
1637 } 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 {  
1671 - font-size: 28rpx;  
1672 - font-weight: bold;  
1673 - padding: 12rpx 20rpx;  
1674 - background: #fff6cc;  
1675 - border-radius: 12rpx;  
1676 - margin-bottom: 12rpx; 1439 + .superset-title {
  1440 + font-size: 28rpx;
  1441 + font-weight: bold;
  1442 + padding: 12rpx 20rpx;
  1443 + background: #fff6cc;
  1444 + border-radius: 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 {  
1689 - font-size: 26rpx;  
1690 - color: #666;  
1691 - flex-shrink: 0;  
1692 - width: 150rpx;  
1693 -} 1457 + .noteTitle {
  1458 + font-size: 26rpx;
  1459 + color: #666;
  1460 + flex-shrink: 0;
  1461 + width: 150rpx;
  1462 + }
1694 1463
1695 -.noteTags {  
1696 - flex: 1;  
1697 - display: flex;  
1698 - flex-wrap: wrap;  
1699 - gap: 12rpx; 1464 + .noteTags {
  1465 + flex: 1;
  1466 + display: flex;
  1467 + flex-wrap: wrap;
  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 -}  
1708 -</style>  
  1472 +// ::deep(.noteTag) {
  1473 +// width: auto !important;
  1474 +// font-size: 24rpx;
  1475 +// border-radius: 10rpx;
  1476 +// }
  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 - <view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick">  
40 - <up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>  
41 - <text class="nav-text">收藏</text>  
42 - </view> 39 + <template v-if="!isSearchMode">
  40 + <view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick">
  41 + <up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>
  42 + <text class="nav-text">收藏</text>
  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,20 +262,24 @@ const loadSuperFavoriteList = async () => { @@ -203,20 +262,24 @@ 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;
211 -  
212 - if (navItems.value.length > 0) {  
213 - switchNav(navItems.value[0].id);  
214 - }  
215 } catch (error) { 270 } catch (error) {
216 console.error('获取分类失败:', error); 271 console.error('获取分类失败:', error);
217 } 272 }
218 }; 273 };
219 274
  275 +// 加载大类(首次加载,自动切换到第一个分类)
  276 +const loadCategories = async () => {
  277 + await refreshCategories();
  278 + if (navItems.value.length > 0) {
  279 + switchNav(navItems.value[0].id);
  280 + }
  281 +};
  282 +
220 // 加载动作列表 283 // 加载动作列表
221 const loadExercises = async (categoriesId) => { 284 const loadExercises = async (categoriesId) => {
222 try { 285 try {
@@ -374,7 +437,7 @@ onMounted(() => { @@ -374,7 +437,7 @@ onMounted(() => {
374 .left-nav { 437 .left-nav {
375 width: 180rpx; 438 width: 180rpx;
376 height: 88%; 439 height: 88%;
377 - 440 +
378 background-color: #f9fafb; 441 background-color: #f9fafb;
379 border-right: 1rpx solid #f3f4f6; 442 border-right: 1rpx solid #f3f4f6;
380 443
@@ -28,36 +28,41 @@ @@ -28,36 +28,41 @@
28 </view> 28 </view>
29 29
30 <!-- 日期网格 (动态 5行 或 6行) --> 30 <!-- 日期网格 (动态 5行 或 6行) -->
31 - <view class="date-grid">  
32 - <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{  
33 -  
34 - 'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0)  
35 - }" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)">  
36 - <!-- 日期数字 / 今日徽章 -->  
37 - <view class="date-header">  
38 - <text v-if="date.today" class="today-badge">今</text>  
39 - <text v-else class="date-number">  
40 - {{ date.day }}  
41 - </text>  
42 - </view>  
43 31
44 - <!-- 计划/容量数据列表 -->  
45 - <view class="plan-list"  
46 - v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)">  
47 - <template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx">  
48 - <view class="plan-tag" :class="`tag-type-${item.type}`"  
49 - :style="item.bg ? { backgroundColor: item.bg } : {}">  
50 - <text class="tag-text">{{ item.text }}</text>  
51 - </view>  
52 - </template> 32 + <scroll-view class="scroll-view" scroll-y>
  33 + <view class="date-grid">
  34 + <view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{
  35 +
  36 + 'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0)
  37 + }" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)">
  38 + <!-- 日期数字 / 今日徽章 -->
  39 + <view class="date-header">
  40 + <text v-if="date.today" class="today-badge">今</text>
  41 + <text v-else class="date-number">
  42 + {{ date.day }}
  43 + </text>
  44 + </view>
53 45
54 - <!-- 溢出剩余条数 -->  
55 - <view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">  
56 - +{{ getCellVisibleItems(date).remaining }} 46 + <!-- 计划/容量数据列表 -->
  47 + <view class="plan-list"
  48 + v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)">
  49 + <template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx">
  50 + <view class="plan-tag" :class="`tag-type-${item.type}`"
  51 + :style="item.bg ? { backgroundColor: item.bg } : {}">
  52 + <text class="tag-text">{{ item.text }}</text>
  53 + </view>
  54 + </template>
  55 + <!-- 溢出剩余条数 -->
  56 + <view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">
  57 + +{{ getCellVisibleItems(date).remaining }}
  58 + </view>
57 </view> 59 </view>
58 </view> 60 </view>
59 </view> 61 </view>
60 - </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>  
78 - <view class="item-info">  
79 - <text class="item-label">时长</text>  
80 - <text class="item-desc">在场馆的实际运动时长</text> 82 + <view class="explain-item-left">
  83 + <view class="color-block yellow">
  84 +
  85 + </view>
  86 + <view class="item-info">
  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>  
86 - <view class="item-info">  
87 - <text class="item-label">容量</text>  
88 - <text class="item-desc">力量训练累计产生的总负重重量</text> 94 + <view class="explain-item-left">
  95 + <view class="color-block gray">
  96 +
  97 + </view>
  98 + <view class="item-info">
  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>  
94 - <view class="item-info">  
95 - <text class="item-label">课程</text>  
96 - <text class="item-desc">训记完成或已结课的线下课程</text> 106 + <view class="explain-item-left">
  107 + <view class="color-block green">
  108 +
  109 + </view>
  110 + <view class="item-info">
  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>  
102 - <view class="item-info">  
103 - <text class="item-label">排课</text>  
104 - <text class="item-desc">训记排课计划或待上课的课程</text> 118 + <view class="explain-item-left">
  119 + <view class="color-block blue">
  120 +
  121 + </view>
  122 + <view class="item-info">
  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,136 +424,146 @@ const selectDate = async (date) => { @@ -395,136 +424,146 @@ const selectDate = async (date) => {
395 } 424 }
396 } 425 }
397 426
398 - .date-grid {  
399 - display: grid;  
400 - grid-template-columns: repeat(7, 1fr);  
401 - gap: 10rpx;  
402 - }  
403 -  
404 - /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */  
405 - .date-cell {  
406 - min-width: 0;  
407 - overflow: hidden;  
408 - display: flex;  
409 - flex-direction: column;  
410 - align-items: center;  
411 - justify-content: flex-start;  
412 - border-radius: 12rpx;  
413 - height: 200rpx;  
414 - padding: 8rpx 4rpx;  
415 - box-sizing: border-box;  
416 - transition: all 0.2s ease;  
417 -  
418 - &:active {  
419 - filter: brightness(0.96);  
420 - }  
421 -  
422 - &.other-month {  
423 - opacity: 0.5;  
424 - border-color: #f9fafc;  
425 - } 427 + .scroll-view {
  428 + height: 100%;
  429 +
  430 + .date-grid {
  431 + display: grid;
  432 + grid-template-columns: repeat(7, 1fr);
  433 + gap: 10rpx;
  434 + // #ifdef MP-WEIXIN
  435 + padding-bottom: 100rpx;
  436 + // #endif
  437 + /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
  438 + .date-cell {
  439 + min-width: 0;
  440 + overflow: hidden;
  441 + display: flex;
  442 + flex-direction: column;
  443 + align-items: center;
  444 + justify-content: flex-start;
  445 + border-radius: 12rpx;
  446 + height: 200rpx;
  447 + padding: 8rpx 4rpx;
  448 + box-sizing: border-box;
  449 + transition: all 0.2s ease;
426 450
427 - &.is-today {  
428 - border: 2rpx solid #3b82f6;  
429 - } 451 + &:active {
  452 + filter: brightness(0.96);
  453 + }
430 454
431 - &.has-data {  
432 - box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);  
433 - } 455 + &.other-month {
  456 + opacity: 0.5;
  457 + border-color: #f9fafc;
  458 + }
434 459
435 - .date-header {  
436 - height: 36rpx;  
437 - display: flex;  
438 - align-items: center;  
439 - justify-content: center;  
440 - margin-bottom: 6rpx; 460 + &.is-today {
  461 + border: 2rpx solid #3b82f6;
  462 + }
441 463
442 - .date-number {  
443 - font-size: 26rpx;  
444 - font-weight: 600;  
445 - color: #1f2937; 464 + &.has-data {
  465 + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);
  466 + }
446 467
  468 + .date-header {
  469 + height: 36rpx;
  470 + display: flex;
  471 + align-items: center;
  472 + justify-content: center;
  473 + margin-bottom: 6rpx;
447 474
448 - } 475 + .date-number {
  476 + font-size: 26rpx;
  477 + font-weight: 600;
  478 + color: #1f2937;
449 479
450 - .today-badge {  
451 480
452 - font-weight: 600; 481 + }
453 482
454 - }  
455 - } 483 + .today-badge {
456 484
457 - .plan-list {  
458 - width: 100%;  
459 - display: flex;  
460 - flex-direction: column;  
461 - gap: 6rpx; 485 + font-weight: 600;
462 486
463 - .plan-tag {  
464 - width: 100%;  
465 - border-radius: 6rpx;  
466 - padding: 2rpx 4rpx;  
467 - box-sizing: border-box;  
468 - display: flex;  
469 - align-items: center;  
470 - justify-content: center;  
471 - background-color: rgba(255, 255, 255, 0.85);  
472 - /* 使 Tag 悬浮于彩色格子之上 */  
473 -  
474 - .tag-text {  
475 - font-size: 18rpx;  
476 - line-height: 22rpx;  
477 - color: #111827;  
478 - overflow: hidden;  
479 - white-space: nowrap;  
480 - text-overflow: ellipsis;  
481 - text-align: center;  
482 - width: 100%;  
483 - }  
484 -  
485 - &.tag-type-weight {  
486 - background-color: #bbf7d0 !important;  
487 -  
488 - .tag-text {  
489 - color: #14532d;  
490 - font-weight: 700;  
491 } 487 }
492 } 488 }
493 489
494 - &.tag-type-note {  
495 - background-color: #ffffff; 490 + .plan-list {
  491 + width: 100%;
  492 + display: flex;
  493 + flex-direction: column;
  494 + gap: 6rpx;
  495 +
  496 + .plan-tag {
  497 + width: 100%;
  498 + border-radius: 6rpx;
  499 + padding: 2rpx 4rpx;
  500 + box-sizing: border-box;
  501 + display: flex;
  502 + align-items: center;
  503 + justify-content: center;
  504 + background-color: rgba(255, 255, 255, 0.85);
  505 + /* 使 Tag 悬浮于彩色格子之上 */
  506 +
  507 + .tag-text {
  508 + font-size: 18rpx;
  509 + line-height: 22rpx;
  510 + color: #111827;
  511 + overflow: hidden;
  512 + white-space: nowrap;
  513 + text-overflow: ellipsis;
  514 + text-align: center;
  515 + width: 100%;
  516 + }
  517 +
  518 + &.tag-type-weight {
  519 + background-color: #bbf7d0 !important;
  520 +
  521 + .tag-text {
  522 + color: #14532d;
  523 + font-weight: 700;
  524 + }
  525 + }
  526 +
  527 + &.tag-type-note {
  528 + background-color: #ffffff;
  529 +
  530 + .tag-text {
  531 + color: #374151;
  532 + }
  533 + }
  534 + }
496 535
497 - .tag-text {  
498 - color: #374151; 536 + .plan-extra {
  537 + font-size: 16rpx;
  538 + color: #4b5563;
  539 + text-align: center;
  540 + line-height: 20rpx;
  541 + font-weight: 600;
499 } 542 }
500 } 543 }
501 } 544 }
502 -  
503 - .plan-extra {  
504 - font-size: 16rpx;  
505 - color: #4b5563;  
506 - text-align: center;  
507 - line-height: 20rpx;  
508 - font-weight: 600;  
509 - }  
510 } 545 }
  546 +
  547 +
511 } 548 }
  549 +
  550 +
512 } 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 {
538 - display: flex;  
539 - align-items: center; 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 {
  583 + display: flex;
  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"  
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> 68 + <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
  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);" />  
81 - <text class="toggle-label">  
82 - {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}  
83 - </text> 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">
  82 + {{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
  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
@@ -369,9 +370,9 @@ const chartSeries = computed(() => { @@ -369,9 +370,9 @@ const chartSeries = computed(() => {
369 370
370 return showLastPeriod.value 371 return showLastPeriod.value
371 ? [ 372 ? [
372 - { name: lastName, data: lastData, color: '#5b8ff9' },  
373 - { name: currName, data: currData, color: '#ffc53d' },  
374 - ] 373 + { name: lastName, data: lastData, color: '#5b8ff9' },
  374 + { name: currName, data: currData, color: '#ffc53d' },
  375 + ]
375 : [{ name: currName, data: currData, color: '#ffc53d' }]; 376 : [{ name: currName, data: currData, color: '#ffc53d' }];
376 }); 377 });
377 378
@@ -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);
@@ -701,7 +696,7 @@ $radius-item: 12rpx; @@ -701,7 +696,7 @@ $radius-item: 12rpx;
701 .training-data-page { 696 .training-data-page {
702 display: flex; 697 display: flex;
703 flex-direction: column; 698 flex-direction: column;
704 - 699 +
705 width: 100vw; 700 width: 100vw;
706 background-color: $color-bg; 701 background-color: $color-bg;
707 overflow: hidden; 702 overflow: hidden;
@@ -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 + />
19 </view> 31 </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列 --> 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>
  43 + </view>
  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>
39 </view> 73 </view>
40 </view> 74 </view>
41 </view> 75 </view>
42 </view> 76 </view>
  77 +
  78 + <!-- 底部提示 -->
  79 + <view class="bottom-tip">
  80 + <text>最多仅可设置到未来 {{ MONTHS_AHEAD }} 个月哦~</text>
  81 + </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 -  
120 - const dayList = []  
121 - const startDay = idx === 0 ? today : 1  
122 171
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 192 + year,
  193 + month,
  194 + emptyCount: calcEmptySlots(year, month, startDay),
  195 + dayList,
151 }) 196 })
152 - }) 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; 241 +// ==================== 主题色系与变量 ====================
  242 +$bg-main: #121212;
  243 +$bg-card: #1e1e1e;
  244 +$bg-cell: #262626;
  245 +
  246 +$text-primary: #ffffff;
  247 +$text-secondary: #8c8c8c;
  248 +
  249 +$theme-yellow: #e9ee50;
  250 +$theme-yellow-light: rgba(233, 238, 80, 0.15);
  251 +$theme-yellow-border: rgba(233, 238, 80, 0.35);
  252 +
  253 +$cell-height: 116rpx;
  254 +
  255 +// ==================== 容器根样式 ====================
  256 +.paike-container {
230 min-height: 100vh; 257 min-height: 100vh;
231 - padding-bottom: env(safe-area-inset-bottom);  
232 - overflow: hidden; 258 + background-color: $bg-main;
  259 + padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
  260 + box-sizing: border-box;
233 } 261 }
234 262
235 -.page-header {  
236 - width: 100%;  
237 - flex-shrink: 0;  
238 - background-color: $bg-color;  
239 -} 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);
240 276
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; 277 + .week-grid {
  278 + display: grid;
  279 + grid-template-columns: repeat(7, 1fr);
  280 +
  281 + .week-cell {
  282 + text-align: center;
  283 + font-size: 24rpx;
  284 + font-weight: 500;
  285 + color: $text-secondary;
  286 + }
  287 + }
249 } 288 }
250 289
251 -.nav-left {  
252 - width: 60rpx; 290 +// ==================== 状态容器 (Loading/Empty) ====================
  291 +.state-box {
253 display: flex; 292 display: flex;
254 align-items: center; 293 align-items: center;
255 - flex-shrink: 0; 294 + justify-content: center;
  295 + min-height: 500rpx;
256 } 296 }
257 297
258 -.nav-title {  
259 - flex: 1;  
260 - text-align: center;  
261 - font-size: 36rpx;  
262 - font-weight: bold;  
263 - color: $text-white; 298 +// ==================== 日历主体 ====================
  299 +.calendar-content {
  300 + padding: 20rpx 16rpx 0;
264 } 301 }
265 302
266 -.nav-right {  
267 - width: 60rpx;  
268 - flex-shrink: 0;  
269 -} 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);
270 310
271 -.week-head {  
272 - margin-top: 10rpx;  
273 - display: grid;  
274 - grid-template-columns: repeat(7, 1fr);  
275 - margin: 20rpx 0;  
276 -  
277 - .week-cell { 311 + .month-header {
278 text-align: center; 312 text-align: center;
279 - font-size: 25rpx;  
280 - color: $text-white;  
281 - }  
282 -} 313 + padding-bottom: 20rpx;
283 314
284 -.month-wrap {  
285 - margin-bottom: 30rpx;  
286 - background-color: #121212;  
287 -  
288 - .month-title {  
289 - font-size: 25rpx;  
290 - color: $text-white;  
291 - padding: 15rpx 10rpx;  
292 - text-align: center; 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;  
335 - height: 56rpx;  
336 - text-align: center; 353 + /* 有排课状态 */
  354 + &.has-schedule {
  355 + background-color: $theme-yellow-light;
  356 + border: 1rpx solid $theme-yellow-border;
337 357
  358 + .day-number {
  359 + color: $theme-yellow;
  360 + font-weight: 700;
  361 + }
338 } 362 }
339 363
340 - .empty-label {  
341 - width: 100%;  
342 - min-height: 56rpx;  
343 - background: $text-gray;  
344 - border-radius: 2rpx; 364 + .day-number {
  365 + font-size: 26rpx;
  366 + color: $text-primary;
  367 + font-weight: 500;
  368 + line-height: 1;
345 } 369 }
346 - }  
347 370
348 - .empty-cell {  
349 - background: transparent !important; 371 + /* 排课内容展示区 */
  372 + .schedule-box {
  373 + width: 100%;
  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;
  389 + text-align: center;
  390 + line-height: 1.25;
  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;
  399 + }
  400 +
  401 + /* 无排课时的精致小点,替代原本突兀的大灰块 */
  402 + .schedule-placeholder {
  403 + display: flex;
  404 + align-items: center;
  405 + justify-content: center;
  406 + width: 100%;
  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 + }
  415 + }
  416 + }
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>