Authored by qxm

修改日历和模板

@@ -6,9 +6,9 @@ SHOPRO_VERSION=v2.4.1 @@ -6,9 +6,9 @@ SHOPRO_VERSION=v2.4.1
6 # SHOPRO_BASE_URL=http://mall.hcxtec.com 6 # SHOPRO_BASE_URL=http://mall.hcxtec.com
7 # SHOPRO_BASE_URL=https://xunji.geaktec.com 7 # SHOPRO_BASE_URL=https://xunji.geaktec.com
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
1 -# 2026-06-23 工作日志  
2 -  
3 -## 悬浮球快照方案 — 完整改造方案  
4 -  
5 -分析了 exercising 健身小程序中 trainingStore(Pinia)被三个功能模块(动作训练、超级组、每日模板编辑/修改)共享导致的数据冲突问题。  
6 -  
7 -### 问题根因  
8 -- `grid-cell-content-popup.vue` 的编辑按钮 (`templateEdit`) 调用 `loadDailyTemplateForEdit()` 覆盖 store  
9 -- `wode-xinjian-moban.vue` 的 `goBack()` / `handleSave()` 调用 `clearTrainingStore()` 清空 store  
10 -- `meiri-moban-xiugai.vue` 的修改弹窗(弹窗模式)已通过快照解决,但编辑和新建是页面跳转模式,快照来不及归还  
11 -  
12 -### 最终方案:悬浮球持有快照  
13 -  
14 -核心思路:最小化时将 trainingStore 全量深拷贝到共享 reactive 模块 `trainingSnapshot`(存在 JS 堆内存,非 Pinia),悬浮球由此独立控制显隐和回显。  
15 -  
16 -改动清单:  
17 -1. **新建** `sheep/store/trainingSnapshot.js` — 共享 reactive 快照模块  
18 -2. **重写** `pages/TrainingFloating.vue` — 自有 showBall + 回显逻辑 + 计时器补偿  
19 -3. **修改** `pages4/.../xunji-dongzuo-lianxi.vue` openMin() — 保存快照到共享模块  
20 -4. **删除** `sheep/store/trainingStore.js` clearTrainingStore() 中 `this.min = false`  
21 -5. **不改** grid-cell-content-popup.vue 和 wode-xinjian-moban.vue  
22 -6. **修复** meiri-moban-xiugai.vue 中 Object.assign → $patch  
23 -  
24 -总计约 113 行,4 文件改动,2 文件不动。  
1 <template> 1 <template>
2 - <view class="floating-train-btn" v-if="trainingStore.min" @click="goToTrainingPage"> 2 + <view class="floating-train-btn" v-if="showBall" @click="goToTrainingPage">
3 <view class="icon-wrap"> 3 <view class="icon-wrap">
4 <up-icon name="plus-circle" color="#fff" size="24"></up-icon> 4 <up-icon name="plus-circle" color="#fff" size="24"></up-icon>
5 </view> 5 </view>
@@ -13,28 +13,91 @@ @@ -13,28 +13,91 @@
13 <script setup> 13 <script setup>
14 import { computed, watch, onMounted } from 'vue'; 14 import { computed, watch, onMounted } from 'vue';
15 import { useTrainingStore } from '@/sheep/store/trainingStore'; 15 import { useTrainingStore } from '@/sheep/store/trainingStore';
  16 +import { trainingSnapshot } from '@/sheep/store/trainingSnapshot'
16 17
17 const trainingStore = useTrainingStore(); 18 const trainingStore = useTrainingStore();
18 19
19 -// 格式化显示“开始时间”  
20 -const formattedStartTime = computed(() => trainingStore.trainingTimeText);  
21 20
22 -// 跳转到训练页面 21 +const showBall = computed(() => trainingSnapshot.visible && trainingSnapshot.data !== null)
  22 +
  23 +// 格式化显示时间(快照中的 trainingTimeText)
  24 +const formattedStartTime = computed(() => {
  25 + if (!trainingSnapshot.data) return ''
  26 + return trainingSnapshot.data.trainingTimeText || ''
  27 +})
  28 +
  29 +// ========== 点击悬浮球:还原快照 → 跳转训练页 ==========
23 const goToTrainingPage = () => { 30 const goToTrainingPage = () => {
24 - // 悬浮球消失  
25 - trainingStore.min = false; 31 + if (!trainingSnapshot.data) return
  32 +
  33 + // 安全检查:如果 store 里已经有不属于本次快照的训练数据(用户在编辑中)
  34 + if (trainingStore.id && trainingStore.id !== trainingSnapshot.data.id) {
  35 + uni.showModal({
  36 + title: '提示',
  37 + content: '当前已有另一组训练数据,恢复会覆盖它,确认吗?',
  38 + confirmText: '确认恢复',
  39 + cancelText: '取消',
  40 + success: (res) => {
  41 + if (res.confirm) doRestoreAndGo()
  42 + }
  43 + })
  44 + return
  45 + }
  46 +
  47 + doRestoreAndGo()
  48 +}
  49 +
  50 +const doRestoreAndGo = () => {
  51 + const snap = trainingSnapshot.data
  52 +
  53 + // 1. 用 $patch 还原全量 state(Pinia 官方批量更新,保证响应式触发)
  54 + trainingStore.$patch(snap)
  55 +
  56 + // 2. 补偿计时器:如果快照时计时器在跑,加上经过的时间
  57 + if (trainingSnapshot.timerWasRunning && trainingSnapshot.timestamp) {
  58 + const elapsed = Math.floor((Date.now() - trainingSnapshot.timestamp) / 1000)
  59 + trainingStore.totalSeconds = (snap.totalSeconds || 0) + elapsed
  60 + }
  61 +
  62 + // 3. 如果快照时计时器在跑,重新启动 setInterval
  63 + if (trainingSnapshot.timerWasRunning) {
  64 + trainingStore.isPause = false
  65 + trainingStore.timerInterval = setInterval(() => {
  66 + trainingStore.totalSeconds++
  67 + }, 1000)
  68 + }
  69 +
  70 + // 4. 清空快照,隐藏悬浮球
  71 + trainingSnapshot.data = null
  72 + trainingSnapshot.timestamp = null
  73 + trainingSnapshot.timerWasRunning = false
  74 + trainingSnapshot.visible = false
  75 +
  76 + // 5. 跳转训练页面
26 uni.navigateTo({ 77 uni.navigateTo({
27 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}&isTraining=true`,  
28 - });  
29 -}; 78 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${snap.id}&type=${snap.type}&isTraining=true`,
  79 + })
  80 +}
  81 +
  82 +// 格式化显示“开始时间”
  83 +// const formattedStartTime = computed(() => trainingStore.trainingTimeText);
  84 +
  85 +// 跳转到训练页面
  86 +// const goToTrainingPage = () => {
  87 +// // 悬浮球消失
  88 +// trainingStore.min = false;
  89 +// uni.navigateTo({
  90 +// url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}`,
  91 +// });
  92 +// };
30 93
31 -watch(() => trainingStore.min, (newVal) => {  
32 - console.log('🟢 悬浮球组件监听到 min 变化:', newVal); 94 +watch(() => showBall.value, (newVal) => {
  95 + console.log('🟢 悬浮球组件监听到 showBall 变化:', newVal);
33 }, { immediate: true }); 96 }, { immediate: true });
34 97
35 // 组件挂载时打印 98 // 组件挂载时打印
36 onMounted(() => { 99 onMounted(() => {
37 - console.log('🟢 悬浮球组件已挂载,当前 min 值:', trainingStore.min); 100 + console.log('🟢 悬浮球组件已挂载,当前 showBall 值:', showBall.value);
38 }); 101 });
39 </script> 102 </script>
40 103
1 <template> 1 <template>
2 <view class="charts-box"> 2 <view class="charts-box">
3 - <!-- :canvas2d="true" tooltipFormat="showYLable" :onmovetip="true" --> 3 + <!-- :canvas2d="true"(导致穿透的原因) tooltipFormat="showYLable" :onmovetip="true" -->
4 <qiun-data-charts :type="chartType" :opts="opts" :chartData="chartData" :reshow="reshow" :tooltipShow="true" /> 4 <qiun-data-charts :type="chartType" :opts="opts" :chartData="chartData" :reshow="reshow" :tooltipShow="true" />
5 </view> 5 </view>
6 </template> 6 </template>
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 <view class="goal-list"> 7 <view class="goal-list">
8 <view class="goal-item"> 8 <view class="goal-item">
9 <view class="serial"> 9 <view class="serial">
10 - {{ subIndex + 1 }}{{ String.fromCharCode(65 + subIndex) }}</view> 10 + {{ setIndex + 1 }}{{ String.fromCharCode(65 + subIndex) }}</view>
11 <!-- ========================================== --> 11 <!-- ========================================== -->
12 <!-- 0:独立 重量+次数 --> 12 <!-- 0:独立 重量+次数 -->
13 <!-- ========================================== --> 13 <!-- ========================================== -->
@@ -178,7 +178,8 @@ const props = defineProps({ @@ -178,7 +178,8 @@ const props = defineProps({
178 userWeight: { 178 userWeight: {
179 type: Number, 179 type: Number,
180 default: 70 180 default: 70
181 - } 181 + },
  182 +
182 }) 183 })
183 184
184 const emit = defineEmits(['open-time-picker']) 185 const emit = defineEmits(['open-time-picker'])
@@ -84,7 +84,7 @@ @@ -84,7 +84,7 @@
84 <view class="action-top"> 84 <view class="action-top">
85 <text class="action-name">{{ unit.exercises[0]?.exerciseName || '动作名称' }}</text> 85 <text class="action-name">{{ unit.exercises[0]?.exerciseName || '动作名称' }}</text>
86 <text class="action-totalWeight">{{ unit.totalWeight 86 <text class="action-totalWeight">{{ unit.totalWeight
87 - }}kg</text> 87 + }}kg</text>
88 </view> 88 </view>
89 89
90 <!-- 下半部分:组次行 → 向左对齐图片 ✅核心--> 90 <!-- 下半部分:组次行 → 向左对齐图片 ✅核心-->
@@ -233,8 +233,8 @@ @@ -233,8 +233,8 @@
233 233
234 <!-- 日期备注弹窗 --> 234 <!-- 日期备注弹窗 -->
235 <RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId" 235 <RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId"
236 - :history-list="noteHistoryList" @save="loaddailytemplate" @close="handleCloseNotePopup"  
237 - @refreshMain="handleRefreshMain" /> 236 + :history-list="noteHistoryList" :note-content-list="noteList" @save="loaddailytemplate"
  237 + @close="handleCloseNotePopup" @refreshMain="handleRefreshMain" />
238 238
239 <!-- 更多弹窗 --> 239 <!-- 更多弹窗 -->
240 <up-popup :show="moreShow" mode="bottom" mask-click @close="closeMorePopup" :safe-area-inset-bottom="true"> 240 <up-popup :show="moreShow" mode="bottom" mask-click @close="closeMorePopup" :safe-area-inset-bottom="true">
@@ -424,7 +424,7 @@ const loaddailytemplate = async () => { @@ -424,7 +424,7 @@ const loaddailytemplate = async () => {
424 424
425 const noteListFromApi = resdaily.data.notes; 425 const noteListFromApi = resdaily.data.notes;
426 noteList.value = noteListFromApi; 426 noteList.value = noteListFromApi;
427 - console.log('打印每日模板详情的备注列表:noteList', noteList); 427 + console.log('打印每日模板详情的备注列表:noteList', noteList.value);
428 428
429 const historyFromApi = resdaily.data.noteHistoryList || []; 429 const historyFromApi = resdaily.data.noteHistoryList || [];
430 console.log('打印历史备注:', historyFromApi); 430 console.log('打印历史备注:', historyFromApi);
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 <!-- 顶部导航栏 --> 5 <!-- 顶部导航栏 -->
6 <view class="popup-header"> 6 <view class="popup-header">
7 <view class="close-btn" @click="handleClose"> 7 <view class="close-btn" @click="handleClose">
8 - <text class="close-icon">×</text> 8 + <up-icon name="close" color="#333" size="20"></up-icon>
9 </view> 9 </view>
10 <text class="popup-title"> {{ isEdit ? '编辑日程备注' : '新增日程备注' }}</text> 10 <text class="popup-title"> {{ isEdit ? '编辑日程备注' : '新增日程备注' }}</text>
11 <button class="save-btn" @click="handleSave">保存</button> 11 <button class="save-btn" @click="handleSave">保存</button>
@@ -55,7 +55,8 @@ const props = defineProps({ @@ -55,7 +55,8 @@ const props = defineProps({
55 visible: { type: Boolean, default: false }, 55 visible: { type: Boolean, default: false },
56 date: { type: String, required: true }, 56 date: { type: String, required: true },
57 noteId: { type: [Number, String], default: null }, 57 noteId: { type: [Number, String], default: null },
58 - historyList: { type: Array, default: () => [] } 58 + historyList: { type: Array, default: () => [] },
  59 + noteContentList: { type: Array, default: () => [] }
59 }); 60 });
60 61
61 const emit = defineEmits(['update:visible', 'save', 'close', 'refreshMain']); 62 const emit = defineEmits(['update:visible', 'save', 'close', 'refreshMain']);
@@ -110,13 +111,43 @@ const selectHistoryNote = (item) => { @@ -110,13 +111,43 @@ const selectHistoryNote = (item) => {
110 const handleClose = () => { 111 const handleClose = () => {
111 emit('update:visible', false); 112 emit('update:visible', false);
112 emit('close'); 113 emit('close');
  114 + // 清空文本
  115 + noteContent.value = '';
  116 + selectedColorIndex.value = 0;
113 }; 117 };
114 const handleSave = async () => { 118 const handleSave = async () => {
115 - if (!noteContent.value.trim()) return uni.showToast({ title: "请输入内容" }); 119 + const content = noteContent.value.trim()
  120 + if (!content) return uni.showToast({ title: "请输入内容" });
  121 + // 判断输入内容是否已存在于当日备注列表
  122 +
  123 + // 全部场景都校验重复
  124 + const isRepeat = props.noteContentList.some(item => {
  125 + if (isEdit.value && item.id === Number(props.noteId)) {
  126 + return false
  127 + }
  128 + // 内容相同则判定重复
  129 + return item.content === content
  130 + })
  131 +
  132 + console.log('isRepeat---', isRepeat);
  133 + if (isRepeat) {
  134 + uni.showToast({ title: "操作失败:备注存在重复值", icon: "none" })
  135 + return;
  136 + }
  137 +
  138 +
  139 + if (isRepeat) {
  140 + uni.showToast({ title: "操作失败:备注存在重复值", icon: "none" })
  141 + return;
  142 + }
  143 +
116 let data = { 144 let data = {
117 content: noteContent.value, 145 content: noteContent.value,
118 backgroundColor: colorList.value[selectedColorIndex.value] 146 backgroundColor: colorList.value[selectedColorIndex.value]
119 }; 147 };
  148 +
  149 + // 当日已经有的备注不允许再次输入,提示返回
  150 +
120 try { 151 try {
121 if (isEdit.value) { 152 if (isEdit.value) {
122 // 编辑:必须传 ID 153 // 编辑:必须传 ID
@@ -90,7 +90,7 @@ const activeSceneId = ref('0'); @@ -90,7 +90,7 @@ const activeSceneId = ref('0');
90 const partList = ref([]); 90 const partList = ref([]);
91 const rawPartData = ref([]); // 存储接口返回的原始部位数据 91 const rawPartData = ref([]); // 存储接口返回的原始部位数据
92 const showPartDropdown = ref(false); 92 const showPartDropdown = ref(false);
93 -const activePart = ref('不限'); 93 +const activePart = ref('部位');
94 const activePartId = ref('0'); // 新增:默认选中"不限",id=0 94 const activePartId = ref('0'); // 新增:默认选中"不限",id=0
95 // 场景筛选相关状态 95 // 场景筛选相关状态
96 const sceneList = ref([ 96 const sceneList = ref([
@@ -101,7 +101,7 @@ const sceneList = ref([ @@ -101,7 +101,7 @@ const sceneList = ref([
101 // { id: 4, title: '办公室' }, 101 // { id: 4, title: '办公室' },
102 ]); 102 ]);
103 const showSceneDropdown = ref(false); 103 const showSceneDropdown = ref(false);
104 -const activeScene = ref('不限'); 104 +const activeScene = ref('场景');
105 105
106 // 获取模板大类列表 106 // 获取模板大类列表
107 const TemplatesList = async () => { 107 const TemplatesList = async () => {
@@ -147,20 +147,27 @@ const toggleSceneDropdown = () => { @@ -147,20 +147,27 @@ const toggleSceneDropdown = () => {
147 147
148 // 选择部位 148 // 选择部位
149 const selectPart = (item) => { 149 const selectPart = (item) => {
150 - activePart.value = item.title;  
151 activePartId.value = item.id; 150 activePartId.value = item.id;
  151 + // 判断是否是不限id=0
  152 + if (item.id === '0') {
  153 + activePart.value = '部位';
  154 + } else {
  155 + activePart.value = item.title;
  156 + }
152 showPartDropdown.value = false; 157 showPartDropdown.value = false;
153 - // 加这一行:选完部位立刻筛选  
154 doFilter(); 158 doFilter();
155 }; 159 };
156 160
157 // 选择场景 161 // 选择场景
158 const selectScene = (item) => { 162 const selectScene = (item) => {
159 - activeScene.value = item.title;  
160 - // 关键:把选中的场景ID存起来  
161 activeSceneId.value = item.id; 163 activeSceneId.value = item.id;
  164 + // 判断是否是不限id=0
  165 + if (item.id === '0') {
  166 + activeScene.value = '场景';
  167 + } else {
  168 + activeScene.value = item.title;
  169 + }
162 showSceneDropdown.value = false; 170 showSceneDropdown.value = false;
163 - // 选择完,立即执行筛选!  
164 doFilter(); 171 doFilter();
165 }; 172 };
166 173
@@ -177,6 +184,9 @@ const doFilter = async () => { @@ -177,6 +184,9 @@ const doFilter = async () => {
177 if (musclesId === '0' && scene === '0') { 184 if (musclesId === '0' && scene === '0') {
178 // 情况A:都不限 -> 恢复显示【模板大类】 185 // 情况A:都不限 -> 恢复显示【模板大类】
179 isFiltering.value = false; 186 isFiltering.value = false;
  187 + // 重置按钮文字为默认
  188 + activePart.value = '部位';
  189 + activeScene.value = '场景';
180 // 大类列表之前已经加载过了,直接显示 190 // 大类列表之前已经加载过了,直接显示
181 return; 191 return;
182 } 192 }
@@ -252,46 +262,7 @@ onMounted(async () => { @@ -252,46 +262,7 @@ onMounted(async () => {
252 align-items: center; 262 align-items: center;
253 justify-content: center; 263 justify-content: center;
254 width: auto; 264 width: auto;
255 - min-width: 150rpx;  
256 - height: 50rpx;  
257 - background-color: #fff;  
258 - color: #333;  
259 - font-size: 24rpx;  
260 - border: 2rpx solid #ddd;  
261 - border-radius: 25rpx;  
262 -  
263 - .text {  
264 - margin-right: 5rpx;  
265 - }  
266 - }  
267 -  
268 - /* 筛选包装器 */  
269 - .filter-wrapper {  
270 - position: relative;  
271 - z-index: 10;  
272 - }  
273 -  
274 - /* 筛选按钮激活状态 */  
275 - .filter-btn.active {  
276 - border-color: #43b05e;  
277 - background-color: #e6f7f0;  
278 - color: #43b05e;  
279 - }  
280 -  
281 - .filter-section {  
282 - display: flex;  
283 - gap: 24rpx;  
284 - padding: 20rpx 30rpx;  
285 - background-color: white;  
286 - flex-wrap: wrap;  
287 - }  
288 -  
289 - .filter-btn {  
290 - display: flex;  
291 - align-items: center;  
292 - justify-content: center;  
293 - width: auto;  
294 - min-width: 125rpx; 265 + min-width: 70rpx;
295 height: 50rpx; 266 height: 50rpx;
296 background: #fff; 267 background: #fff;
297 color: #333; 268 color: #333;
@@ -317,33 +288,6 @@ onMounted(async () => { @@ -317,33 +288,6 @@ onMounted(async () => {
317 z-index: 10; 288 z-index: 10;
318 } 289 }
319 290
320 - .dropdown-menu {  
321 - position: absolute;  
322 - top: calc(100% + 8rpx);  
323 - left: 0;  
324 - background: #fff;  
325 - border-radius: 14rpx;  
326 - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);  
327 - padding: 12rpx 0;  
328 - z-index: 100;  
329 - min-width: 160rpx;  
330 - max-height: 320rpx;  
331 - overflow-y: auto;  
332 - }  
333 -  
334 - .dropdown-item {  
335 - padding: 16rpx 24rpx;  
336 - font-size: 26rpx;  
337 - color: #333;  
338 - white-space: nowrap;  
339 - }  
340 -  
341 - .dropdown-item.selected {  
342 - background: #f0f9f4;  
343 - color: #2e9d5a;  
344 - font-weight: 500;  
345 - }  
346 -  
347 /* 下拉菜单样式 */ 291 /* 下拉菜单样式 */
348 .dropdown-menu { 292 .dropdown-menu {
349 position: absolute; 293 position: absolute;
@@ -365,6 +309,7 @@ onMounted(async () => { @@ -365,6 +309,7 @@ onMounted(async () => {
365 padding: 16rpx 24rpx; 309 padding: 16rpx 24rpx;
366 font-size: 26rpx; 310 font-size: 26rpx;
367 color: #333; 311 color: #333;
  312 + white-space: nowrap;
368 } 313 }
369 314
370 /* 下拉菜单项悬停样式 */ 315 /* 下拉菜单项悬停样式 */
@@ -381,13 +326,13 @@ onMounted(async () => { @@ -381,13 +326,13 @@ onMounted(async () => {
381 } 326 }
382 327
383 .template-list { 328 .template-list {
384 -  
385 margin-top: 120rpx; 329 margin-top: 120rpx;
386 padding: 0 30rpx; 330 padding: 0 30rpx;
387 box-sizing: border-box; 331 box-sizing: border-box;
388 flex: 1; 332 flex: 1;
389 display: flex; 333 display: flex;
390 flex-direction: column; 334 flex-direction: column;
  335 +
391 // #ifdef MP-WEIXIN 336 // #ifdef MP-WEIXIN
392 padding-bottom: 200rpx; 337 padding-bottom: 200rpx;
393 height: calc(100vh - 274rpx); 338 height: calc(100vh - 274rpx);
@@ -397,7 +342,6 @@ onMounted(async () => { @@ -397,7 +342,6 @@ onMounted(async () => {
397 margin-top: 87px; 342 margin-top: 87px;
398 // 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px 343 // 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px
399 height: calc(100vh - 77px - 87px); 344 height: calc(100vh - 77px - 87px);
400 - // padding-bottom: 40px;  
401 /* #endif */ 345 /* #endif */
402 346
403 .sub-template-list { 347 .sub-template-list {
@@ -449,7 +393,11 @@ onMounted(async () => { @@ -449,7 +393,11 @@ onMounted(async () => {
449 } 393 }
450 394
451 &:last-child { 395 &:last-child {
452 - margin-bottom: 45rpx; 396 + margin-bottom: 5rpx;
  397 +
  398 + /* #ifdef H5 */
  399 + margin-bottom: 30rpx;
  400 + /* #endif */
453 } 401 }
454 } 402 }
455 } 403 }
@@ -351,6 +351,7 @@ const selectDate = async (date) => { @@ -351,6 +351,7 @@ const selectDate = async (date) => {
351 :deep(.uni-date__x-input) { 351 :deep(.uni-date__x-input) {
352 height: 40rpx; 352 height: 40rpx;
353 line-height: 40rpx; 353 line-height: 40rpx;
  354 + padding: 0 10rpx;
354 } 355 }
355 356
356 // 微信小程序端样式适配 357 // 微信小程序端样式适配
@@ -362,13 +363,14 @@ const selectDate = async (date) => { @@ -362,13 +363,14 @@ const selectDate = async (date) => {
362 box-sizing: border-box; 363 box-sizing: border-box;
363 364
364 :deep(.uni-date-x) { 365 :deep(.uni-date-x) {
365 - height: 100%;  
366 - min-height: 40rpx; 366 + height: 35rpx;
  367 + // min-height: 40rpx;
367 border-radius: 50rpx; 368 border-radius: 50rpx;
368 background-color: transparent; 369 background-color: transparent;
369 padding: 0; 370 padding: 0;
370 margin: 0; 371 margin: 0;
371 border: none !important; 372 border: none !important;
  373 +
372 } 374 }
373 375
374 :deep(.uni-date-editor--x) { 376 :deep(.uni-date-editor--x) {
@@ -381,11 +383,11 @@ const selectDate = async (date) => { @@ -381,11 +383,11 @@ const selectDate = async (date) => {
381 } 383 }
382 384
383 :deep(.uni-date__x-input) { 385 :deep(.uni-date__x-input) {
384 - height: 40rpx;  
385 - line-height: 40rpx; 386 + // height: 40rpx;
  387 + // line-height: 40rpx;
386 font-size: 24rpx; 388 font-size: 24rpx;
387 color: #333; 389 color: #333;
388 - padding: 0; 390 + padding: 0 10rpx;
389 text-align: center; 391 text-align: center;
390 flex: 1; 392 flex: 1;
391 overflow: hidden; 393 overflow: hidden;
@@ -49,12 +49,12 @@ @@ -49,12 +49,12 @@
49 49
50 <view class="statistical"> 50 <view class="statistical">
51 <template v-if="!showLastPeriod"> 51 <template v-if="!showLastPeriod">
52 - <LineChart :categories="chartCategories" :series="chartSeries" chartType="column" :reshow="true"  
53 - :extra="{ column: { width: trendChartColumnWidth } }" /> 52 + <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
  53 + chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
54 </template> 54 </template>
55 <template v-else> 55 <template v-else>
56 - <LineChart :categories="chartCategories" :series="chartSeries" chartType="line" :reshow="true"  
57 - :extra="{ column: { width: trendChartColumnWidth } }" /> 56 + <LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
  57 + chartType="line" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
58 </template> 58 </template>
59 </view> 59 </view>
60 60
@@ -4,18 +4,14 @@ @@ -4,18 +4,14 @@
4 <view class="banner-wrapper"> 4 <view class="banner-wrapper">
5 <swiper class="swiper"> 5 <swiper class="swiper">
6 <swiper-item class="swiper-item"> 6 <swiper-item class="swiper-item">
7 - <image  
8 - class="img" 7 + <image class="img"
9 src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png" 8 src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png"
10 - mode="aspectFill"  
11 - /> 9 + mode="aspectFill" />
12 </swiper-item> 10 </swiper-item>
13 <swiper-item class="swiper-item"> 11 <swiper-item class="swiper-item">
14 - <image  
15 - class="img" 12 + <image class="img"
16 src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/38_1773628032712.png" 13 src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/38_1773628032712.png"
17 - mode="aspectFill"  
18 - /> 14 + mode="aspectFill" />
19 </swiper-item> 15 </swiper-item>
20 </swiper> 16 </swiper>
21 </view> 17 </view>
@@ -23,41 +19,26 @@ @@ -23,41 +19,26 @@
23 <!-- 筛选条 --> 19 <!-- 筛选条 -->
24 <!-- 后端计划分类的计划列表只返回了难度字段,所有只有这个字段的筛选生效了 --> 20 <!-- 后端计划分类的计划列表只返回了难度字段,所有只有这个字段的筛选生效了 -->
25 <view class="filter-bar"> 21 <view class="filter-bar">
26 - <view  
27 - v-for="(item, index) in filterList"  
28 - :key="index"  
29 - class="filter-item"  
30 - :class="{ active: activeFilter === index }"  
31 - @click=" 22 + <view v-for="(item, index) in filterList" :key="index" class="filter-item"
  23 + :class="{ active: activeFilter === index }" @click="
32 activeFilter = index; 24 activeFilter = index;
33 - toggleDrawer(index);  
34 - "  
35 - > 25 + toggleDrawer(index);
  26 + ">
36 <text class="text">{{ 27 <text class="text">{{
37 selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}` 28 selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}`
38 }}</text> 29 }}</text>
39 <uni-icons type="bottom" size="14" color="#666" /> 30 <uni-icons type="bottom" size="14" color="#666" />
40 </view> 31 </view>
41 <view class="filter-search"> 32 <view class="filter-search">
42 - <uni-icons  
43 - type="search"  
44 - size="20"  
45 - color="#333"  
46 - style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx"  
47 - @click="navigateToSearch"  
48 - /> 33 + <uni-icons type="search" size="20" color="#333"
  34 + style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx" @click="navigateToSearch" />
49 </view> 35 </view>
50 </view> 36 </view>
51 37
52 <!-- 下拉抽屉组件 --> 38 <!-- 下拉抽屉组件 -->
53 <view class="filter-drawer" v-show="drawerShow" @click.stop> 39 <view class="filter-drawer" v-show="drawerShow" @click.stop>
54 <view class="drawer-content"> 40 <view class="drawer-content">
55 - <view  
56 - v-for="(item, idx) in filterDrawerData"  
57 - :key="idx"  
58 - class="drawer-item"  
59 - @click="selectDrawerItem(item)"  
60 - > 41 + <view v-for="(item, idx) in filterDrawerData" :key="idx" class="drawer-item" @click="selectDrawerItem(item)">
61 <text class="drawer-item-text">{{ item }}</text> 42 <text class="drawer-item-text">{{ item }}</text>
62 </view> 43 </view>
63 </view> 44 </view>
@@ -67,33 +48,19 @@ @@ -67,33 +48,19 @@
67 <view class="content-layout"> 48 <view class="content-layout">
68 <!-- 左侧分类 --> 49 <!-- 左侧分类 -->
69 <scroll-view class="sidebar" scroll-y enable-flex> 50 <scroll-view class="sidebar" scroll-y enable-flex>
70 - <view  
71 - v-for="(item, index) in categoryList"  
72 - :key="item.id"  
73 - class="category-item"  
74 - :class="{ active: activeCategory === index }"  
75 - @click="switchCategory(index)"  
76 - > 51 + <view v-for="(item, index) in categoryList" :key="item.id" class="category-item"
  52 + :class="{ active: activeCategory === index }" @click="switchCategory(index)">
77 <text class="text">{{ item.name }}</text> 53 <text class="text">{{ item.name }}</text>
78 </view> 54 </view>
79 </scroll-view> 55 </scroll-view>
80 56
81 <!-- 右侧计划列表 --> 57 <!-- 右侧计划列表 -->
82 <scroll-view class="plan-list-wrap" scroll-y enable-flex> 58 <scroll-view class="plan-list-wrap" scroll-y enable-flex>
83 - <view  
84 - v-for="plan in currentPlanList"  
85 - :key="plan.id"  
86 - class="plan-card"  
87 - @tap="navigateToDetail(plan)"  
88 - > 59 + <view v-for="plan in currentPlanList" :key="plan.id" class="plan-card" @tap="navigateToDetail(plan)">
89 <image class="plan-cover" :src="plan.cover" mode="aspectFill" /> 60 <image class="plan-cover" :src="plan.cover" mode="aspectFill" />
90 <view class="plan-info"> 61 <view class="plan-info">
91 <view class="plan-tag-row"> 62 <view class="plan-tag-row">
92 - <view  
93 - v-if="plan.tag"  
94 - class="plan-tag"  
95 - :class="plan.tag === '火爆' ? 'tag-hot' : 'tag-new'"  
96 - > 63 + <view v-if="plan.tag" class="plan-tag" :class="plan.tag === '火爆' ? 'tag-hot' : 'tag-new'">
97 <text>{{ plan.tag }}</text> 64 <text>{{ plan.tag }}</text>
98 </view> 65 </view>
99 </view> 66 </view>
@@ -113,438 +80,449 @@ @@ -113,438 +80,449 @@
113 </template> 80 </template>
114 81
115 <script setup> 82 <script setup>
116 - import { computed, onMounted, ref } from 'vue';  
117 - // ✅ getCurrentPages 是全局 API,无需 import  
118 - import QueryPlanApi from '@/sheep/api/plan/queryplan';  
119 -  
120 - // ====== 筛选相关(保持原样)======  
121 - const filterList = ref(['频率', '难度', '场景', '人群']);  
122 - const drawerShow = ref(false);  
123 - const activeFilter = ref(null);  
124 - const selectedFilters = ref({  
125 - 频率: '不限',  
126 - 难度: '不限',  
127 - 场景: '不限',  
128 - 人群: '不限',  
129 - });  
130 - const filterOptionsMap = {  
131 - 频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'],  
132 - 难度: ['不限', '初阶', '中阶', '高阶'],  
133 - 场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'],  
134 - 人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'],  
135 - };  
136 -  
137 - const filterDrawerData = computed(() => {  
138 - const currentFilterName = filterList.value[activeFilter.value];  
139 - return filterOptionsMap[currentFilterName] || [];  
140 - });  
141 -  
142 - const toggleDrawer = (index) => {  
143 - if (activeFilter.value === index) {  
144 - drawerShow.value = !drawerShow.value;  
145 - } else {  
146 - drawerShow.value = true;  
147 - }  
148 - activeFilter.value = index;  
149 - };  
150 -  
151 - const selectDrawerItem = (item) => {  
152 - const currentFilterName = filterList.value[activeFilter.value];  
153 - selectedFilters.value[currentFilterName] = item;  
154 - drawerShow.value = false;  
155 - console.log('筛选条件更新:', selectedFilters.value);  
156 - };  
157 -  
158 - // ====== 分类与计划 ======  
159 - const categoryList = ref([]);  
160 - const activeCategory = ref(0);  
161 - const planData = ref([]); // 存储所有已加载的计划(带 _categoryId)  
162 - const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };  
163 - // 完全匹配你后台的数字,复制这一段!  
164 - const filterMap = {  
165 - 频率: {  
166 - 不限: 0,  
167 - '1练/周': 1,  
168 - '2练/周': 2,  
169 - '3练/周': 3,  
170 - '4练/周': 4,  
171 - '5练/周': 5,  
172 - '6练/周': 6,  
173 - '7练/周': 7,  
174 - },  
175 - 难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 },  
176 - 场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 },  
177 - 人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 },  
178 - };  
179 - const currentPlanList = computed(() => {  
180 - const categoryId = categoryList.value[activeCategory.value]?.id;  
181 - if (categoryId == null) return [];  
182 -  
183 - let list = planData.value.filter((p) => p._categoryId === categoryId);  
184 -  
185 - // 频率  
186 - if (selectedFilters.value.频率 !== '不限') {  
187 - const val = filterMap.频率[selectedFilters.value.频率];  
188 - list = list.filter((item) => item.frequency === val);  
189 - }  
190 -  
191 - // 难度  
192 - if (selectedFilters.value.难度 !== '不限') {  
193 - const val = filterMap.难度[selectedFilters.value.难度];  
194 - list = list.filter((item) => item.difficultyLevel === val);  
195 - } 83 +import { computed, onMounted, ref } from 'vue';
  84 +// ✅ getCurrentPages 是全局 API,无需 import
  85 +import QueryPlanApi from '@/sheep/api/plan/queryplan';
  86 +
  87 +// ====== 筛选相关(保持原样)======
  88 +const filterList = ref(['频率', '难度', '场景', '人群']);
  89 +const drawerShow = ref(false);
  90 +const activeFilter = ref(null);
  91 +const selectedFilters = ref({
  92 + 频率: '不限',
  93 + 难度: '不限',
  94 + 场景: '不限',
  95 + 人群: '不限',
  96 +});
  97 +const filterOptionsMap = {
  98 + 频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'],
  99 + 难度: ['不限', '初阶', '中阶', '高阶'],
  100 + 场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'],
  101 + 人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'],
  102 +};
  103 +
  104 +const filterDrawerData = computed(() => {
  105 + const currentFilterName = filterList.value[activeFilter.value];
  106 + return filterOptionsMap[currentFilterName] || [];
  107 +});
  108 +
  109 +const toggleDrawer = (index) => {
  110 + if (activeFilter.value === index) {
  111 + drawerShow.value = !drawerShow.value;
  112 + } else {
  113 + drawerShow.value = true;
  114 + }
  115 + activeFilter.value = index;
  116 +};
  117 +
  118 +const selectDrawerItem = (item) => {
  119 + const currentFilterName = filterList.value[activeFilter.value];
  120 + selectedFilters.value[currentFilterName] = item;
  121 + drawerShow.value = false;
  122 + console.log('筛选条件更新:', selectedFilters.value);
  123 +};
  124 +
  125 +// ====== 分类与计划 ======
  126 +const categoryList = ref([]);
  127 +const activeCategory = ref(0);
  128 +const planData = ref([]); // 存储所有已加载的计划(带 _categoryId)
  129 +const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };
  130 +// 完全匹配你后台的数字,复制这一段!
  131 +const filterMap = {
  132 + 频率: {
  133 + 不限: 0,
  134 + '1练/周': 1,
  135 + '2练/周': 2,
  136 + '3练/周': 3,
  137 + '4练/周': 4,
  138 + '5练/周': 5,
  139 + '6练/周': 6,
  140 + '7练/周': 7,
  141 + },
  142 + 难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 },
  143 + 场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 },
  144 + 人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 },
  145 +};
  146 +const currentPlanList = computed(() => {
  147 + const categoryId = categoryList.value[activeCategory.value]?.id;
  148 + if (categoryId == null) return [];
  149 +
  150 + let list = planData.value.filter((p) => p._categoryId === categoryId);
  151 +
  152 + // 频率
  153 + if (selectedFilters.value.频率 !== '不限') {
  154 + const val = filterMap.频率[selectedFilters.value.频率];
  155 + list = list.filter((item) => item.frequency === val);
  156 + }
196 157
197 - // 场景  
198 - if (selectedFilters.value.场景 !== '不限') {  
199 - const val = filterMap.场景[selectedFilters.value.场景];  
200 - list = list.filter((item) => item.scene === val);  
201 - } 158 + // 难度
  159 + if (selectedFilters.value.难度 !== '不限') {
  160 + const val = filterMap.难度[selectedFilters.value.难度];
  161 + list = list.filter((item) => item.difficultyLevel === val);
  162 + }
202 163
203 - // 人群  
204 - if (selectedFilters.value.人群 !== '不限') {  
205 - const val = filterMap.人群[selectedFilters.value.人群];  
206 - list = list.filter((item) => item.population === val);  
207 - } 164 + // 场景
  165 + if (selectedFilters.value.场景 !== '不限') {
  166 + const val = filterMap.场景[selectedFilters.value.场景];
  167 + list = list.filter((item) => item.scene === val);
  168 + }
208 169
209 - return list;  
210 - }); 170 + // 人群
  171 + if (selectedFilters.value.人群 !== '不限') {
  172 + const val = filterMap.人群[selectedFilters.value.人群];
  173 + list = list.filter((item) => item.population === val);
  174 + }
211 175
212 - // ✅ 切换分类时加载数据  
213 - const switchCategory = async (index) => {  
214 - activeCategory.value = index;  
215 - const categoryId = categoryList.value[index]?.id;  
216 - if (categoryId != null) {  
217 - // 检查是否已加载过该分类  
218 - const isLoaded = planData.value.some((p) => p._categoryId === categoryId);  
219 - if (!isLoaded) {  
220 - await loadPlansByCategory(categoryId);  
221 - }  
222 - }  
223 - };  
224 -  
225 - // ====== 数据加载 ======  
226 - const loadCategories = async () => {  
227 - try {  
228 - // 获得计划分类,左侧导航栏显示用  
229 - const res = await QueryPlanApi.getCategories();  
230 - categoryList.value = res.data || [];  
231 - console.log('加载计划列表categoryList.value:', categoryList.value);  
232 - if (categoryList.value.length > 0) {  
233 - await switchCategory(0); // 加载第一个分类  
234 - }  
235 - } catch (err) {  
236 - console.error('加载分类失败:', err);  
237 - uni.showToast({ title: '加载分类失败', icon: 'none' });  
238 - }  
239 - };  
240 -  
241 - // ✅ 核心:加载计划,并手动附加 _categoryId  
242 - const loadPlansByCategory = async (categoryId) => {  
243 - try {  
244 - // 根据计划分类ID获得计划列表,右侧计划列表显示用  
245 - const res = await QueryPlanApi.getPlanList(categoryId);  
246 - console.log('加载计划列表res.data:', res.data);  
247 - const plans = (res.data || []).map((item) => ({  
248 - id: item.id,  
249 - title: item.name,  
250 - meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`,  
251 - tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '',  
252 - cover: item.urlCover || '默认图',  
253 - _categoryId: categoryId,  
254 - frequency: item.frequencyPerWeek, // 新增  
255 - difficultyLevel: item.difficultyLevel, // 新增  
256 - scene: item.trainingScene, // 新增  
257 - population: item.targetPeople, // 新增  
258 - }));  
259 - // 合并数据(避免重复)  
260 - planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans];  
261 - // 打印planData  
262 - console.log('加载计划列表planData.value:', planData.value);  
263 - } catch (err) {  
264 - console.error('加载计划失败:', err);  
265 - uni.showToast({ title: '加载计划失败', icon: 'none' }); 176 + return list;
  177 +});
  178 +
  179 +// ✅ 切换分类时加载数据
  180 +const switchCategory = async (index) => {
  181 + activeCategory.value = index;
  182 + const categoryId = categoryList.value[index]?.id;
  183 + if (categoryId != null) {
  184 + // 检查是否已加载过该分类
  185 + const isLoaded = planData.value.some((p) => p._categoryId === categoryId);
  186 + if (!isLoaded) {
  187 + await loadPlansByCategory(categoryId);
266 } 188 }
267 - };  
268 -  
269 - // 跳转到搜索页面  
270 - const navigateToSearch = () => {  
271 - const app = getApp();  
272 - app.globalData.allPlansForSearch = planData.value;  
273 - uni.navigateTo({  
274 - url: '/pages4/pages/xunji/jihua-search',  
275 - });  
276 - };  
277 - // 跳转到计划详情页  
278 - const navigateToDetail = (plan) => {  
279 - uni.navigateTo({  
280 - url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`,  
281 - });  
282 - };  
283 -  
284 - // ====== 返回按钮 ======  
285 - const back = () => {  
286 - const pages = getCurrentPages();  
287 - if (pages.length > 1) {  
288 - uni.navigateBack();  
289 - } else {  
290 - uni.switchTab({ url: '/pages/index/index' }); // 请替换为你的首页路径 189 + }
  190 +};
  191 +
  192 +// ====== 数据加载 ======
  193 +const loadCategories = async () => {
  194 + try {
  195 + // 获得计划分类,左侧导航栏显示用
  196 + const res = await QueryPlanApi.getCategories();
  197 + categoryList.value = res.data || [];
  198 + console.log('加载计划列表categoryList.value:', categoryList.value);
  199 + if (categoryList.value.length > 0) {
  200 + await switchCategory(0); // 加载第一个分类
291 } 201 }
292 - };  
293 -  
294 - onMounted(() => {  
295 - loadCategories(); 202 + } catch (err) {
  203 + console.error('加载分类失败:', err);
  204 + uni.showToast({ title: '加载分类失败', icon: 'none' });
  205 + }
  206 +};
  207 +
  208 +// ✅ 核心:加载计划,并手动附加 _categoryId
  209 +const loadPlansByCategory = async (categoryId) => {
  210 + try {
  211 + // 根据计划分类ID获得计划列表,右侧计划列表显示用
  212 + const res = await QueryPlanApi.getPlanList(categoryId);
  213 + console.log('加载计划列表res.data:', res.data);
  214 + const plans = (res.data || []).map((item) => ({
  215 + id: item.id,
  216 + title: item.name,
  217 + meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`,
  218 + tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '',
  219 + cover: item.urlCover || '默认图',
  220 + _categoryId: categoryId,
  221 + frequency: item.frequencyPerWeek, // 新增
  222 + difficultyLevel: item.difficultyLevel, // 新增
  223 + scene: item.trainingScene, // 新增
  224 + population: item.targetPeople, // 新增
  225 + }));
  226 + // 合并数据(避免重复)
  227 + planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans];
  228 + // 打印planData
  229 + console.log('加载计划列表planData.value:', planData.value);
  230 + } catch (err) {
  231 + console.error('加载计划失败:', err);
  232 + uni.showToast({ title: '加载计划失败', icon: 'none' });
  233 + }
  234 +};
  235 +
  236 +// 跳转到搜索页面
  237 +const navigateToSearch = () => {
  238 + const app = getApp();
  239 + app.globalData.allPlansForSearch = planData.value;
  240 + uni.navigateTo({
  241 + url: '/pages4/pages/xunji/jihua-search',
296 }); 242 });
297 -</script>  
298 -  
299 -<style lang="scss" scoped>  
300 - /* ========== 新增:页面头部样式 ========== */  
301 - .page-header {  
302 - display: flex;  
303 - align-items: center;  
304 - padding: 30rpx;  
305 - background: #fff;  
306 - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);  
307 - position: sticky;  
308 - top: 0;  
309 - z-index: 999; 243 +};
  244 +// 跳转到计划详情页
  245 +const navigateToDetail = (plan) => {
  246 + uni.navigateTo({
  247 + url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`,
  248 + });
  249 +};
  250 +
  251 +// ====== 返回按钮 ======
  252 +const back = () => {
  253 + const pages = getCurrentPages();
  254 + if (pages.length > 1) {
  255 + uni.navigateBack();
  256 + } else {
  257 + uni.switchTab({ url: '/pages/index/index' }); // 请替换为你的首页路径
310 } 258 }
  259 +};
311 260
312 - .page-title {  
313 - font-size: 32rpx;  
314 - font-weight: bold;  
315 - color: #333;  
316 - flex: 1;  
317 - text-align: center;  
318 - } 261 +onMounted(() => {
  262 + loadCategories();
  263 +});
  264 +</script>
319 265
320 - /* ========== 以下是你原有的全部样式(完全保留) ========== */  
321 - .plan-page { 266 +<style lang="scss" scoped>
  267 +/* ========== 新增:页面头部样式 ========== */
  268 +// .page-header {
  269 +// display: flex;
  270 +// align-items: center;
  271 +// padding: 30rpx;
  272 +// background: #fff;
  273 +// box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  274 +// position: sticky;
  275 +// top: 0;
  276 +// z-index: 999;
  277 +// }
  278 +
  279 +// .page-title {
  280 +// font-size: 32rpx;
  281 +// font-weight: bold;
  282 +// color: #333;
  283 +// flex: 1;
  284 +// text-align: center;
  285 +// }
  286 +
  287 +/* ========== 以下是你原有的全部样式(完全保留) ========== */
  288 +.plan-page {
  289 + width: 100%;
  290 + height: 100vh;
  291 + background-color: #f5f5f5;
  292 + box-sizing: border-box;
  293 + overflow: hidden;
  294 + display: flex;
  295 + flex-direction: column;
  296 +}
  297 +
  298 +/* 顶部横幅 */
  299 +.banner-wrapper {
  300 + height: 250rpx;
  301 + width: 100%;
  302 + margin: 20rpx 0;
  303 +
  304 + .swiper {
322 width: 100%; 305 width: 100%;
323 height: 100%; 306 height: 100%;
324 - background-color: #f5f5f5;  
325 - box-sizing: border-box;  
326 - overflow: hidden;  
327 - display: flex;  
328 - flex-direction: column;  
329 - }  
330 -  
331 - /* 顶部横幅 */  
332 - .banner-wrapper {  
333 - height: 250rpx;  
334 - width: 100%;  
335 - margin: 20rpx 0;  
336 307
337 - .swiper { 308 + .swiper-item {
338 width: 100%; 309 width: 100%;
339 height: 100%; 310 height: 100%;
  311 + padding: 20rpx;
  312 + box-sizing: border-box;
340 313
341 - .swiper-item { 314 + .img {
342 width: 100%; 315 width: 100%;
343 - height: 100%;  
344 - padding: 20rpx;  
345 - box-sizing: border-box;  
346 -  
347 - .img {  
348 - width: 100%;  
349 - height: 220rpx;  
350 - border-radius: 20rpx;  
351 - } 316 + height: 220rpx;
  317 + border-radius: 20rpx;
352 } 318 }
353 } 319 }
354 } 320 }
355 -  
356 - /* 筛选条 */  
357 - .filter-bar {  
358 - margin: 0 20rpx 20rpx;  
359 - padding: 12rpx 18rpx;  
360 - border-radius: 40rpx;  
361 - display: flex; 321 +}
  322 +
  323 +/* 筛选条 */
  324 +.filter-bar {
  325 + margin: 0 20rpx 20rpx;
  326 + padding: 12rpx 18rpx;
  327 + border-radius: 40rpx;
  328 + display: flex;
  329 + align-items: center;
  330 + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.03);
  331 +
  332 + .filter-item {
  333 + flex-direction: row;
362 align-items: center; 334 align-items: center;
363 - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.03);  
364 -  
365 - .filter-item {  
366 - flex-direction: row;  
367 - align-items: center;  
368 - padding: 8rpx 18rpx;  
369 - border-radius: 30rpx;  
370 - border: 1px solid #c5c5c5;  
371 - margin-right: 10rpx;  
372 - background-color: #f7f7f7;  
373 - display: flex;  
374 -  
375 - .text {  
376 - font-size: 24rpx;  
377 - color: #666;  
378 - margin-right: 6rpx;  
379 - }  
380 -  
381 - &.active {  
382 - background-color: #e6f7f0;  
383 -  
384 - .text {  
385 - color: #26c165;  
386 - }  
387 - }  
388 - }  
389 -  
390 - .filter-search {  
391 - margin-left: auto;  
392 - width: 56rpx;  
393 - height: 56rpx;  
394 - border-radius: 50%;  
395 - background-color: #f7f7f7;  
396 - display: flex;  
397 - align-items: center;  
398 - justify-content: center;  
399 - }  
400 - } 335 + padding: 8rpx 18rpx;
  336 + border-radius: 30rpx;
  337 + border: 1px solid #c5c5c5;
  338 + margin-right: 10rpx;
  339 + background-color: #f7f7f7;
  340 + display: flex;
401 341
402 - //下拉抽屉  
403 - .filter-drawer {  
404 - position: absolute;  
405 - top: 560rpx;  
406 - left: 20rpx;  
407 - right: 20rpx;  
408 - z-index: 999;  
409 - border-radius: 0 0 40rpx 40rpx;  
410 - background-color: #fff;  
411 - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);  
412 - overflow: hidden;  
413 -  
414 - .drawer-content {  
415 - padding: 10rpx 0; 342 + .text {
  343 + font-size: 24rpx;
  344 + color: #666;
  345 + margin-right: 6rpx;
416 } 346 }
417 347
418 - .drawer-item {  
419 - padding: 20rpx 30rpx;  
420 - font-size: 26rpx;  
421 - color: #333;  
422 - transition: background-color 0.2s ease; 348 + &.active {
  349 + background-color: #e6f7f0;
423 350
424 - &:active {  
425 - background-color: #f5f5f5;  
426 - }  
427 -  
428 - .drawer-item-text {  
429 - display: block;  
430 - width: 100%; 351 + .text {
  352 + color: #26c165;
431 } 353 }
432 } 354 }
433 } 355 }
434 356
435 - /* 主体布局 */  
436 - .content-layout {  
437 - flex: 1; 357 + .filter-search {
  358 + margin-left: auto;
  359 + width: 56rpx;
  360 + height: 56rpx;
  361 + border-radius: 50%;
  362 + background-color: #f7f7f7;
438 display: flex; 363 display: flex;
439 - box-sizing: border-box;  
440 - overflow: hidden;  
441 - min-height: 0; 364 + align-items: center;
  365 + justify-content: center;
  366 + }
  367 +}
  368 +
  369 +//下拉抽屉
  370 +.filter-drawer {
  371 + position: absolute;
  372 + top: 560rpx;
  373 +
  374 + /* #ifdef H5 */
  375 + top: 247px;
  376 + /* #endif */
  377 +
  378 + left: 20rpx;
  379 + right: 20rpx;
  380 + z-index: 999;
  381 + border-radius: 0 0 40rpx 40rpx;
  382 + background-color: #fff;
  383 + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
  384 + overflow: hidden;
  385 +
  386 + .drawer-content {
  387 + padding: 10rpx 0;
442 } 388 }
443 389
444 - /* 左侧分类 */  
445 - .sidebar {  
446 - width: 180rpx;  
447 - background-color: #ffffff;  
448 - border-radius: 16rpx;  
449 - height: 100%;  
450 - box-sizing: border-box;  
451 - overflow: hidden; 390 + .drawer-item {
  391 + padding: 20rpx 30rpx;
  392 + font-size: 26rpx;
  393 + color: #333;
  394 + transition: background-color 0.2s ease;
452 395
453 - .category-item {  
454 - padding: 22rpx 24rpx;  
455 - font-size: 24rpx;  
456 - color: #666;  
457 - position: relative; 396 + &:active {
  397 + background-color: #f5f5f5;
  398 + }
458 399
459 - &.active {  
460 - background-color: #e8f8f0;  
461 - color: #26c165;  
462 - font-weight: 600; 400 + .drawer-item-text {
  401 + display: block;
  402 + width: 100%;
  403 + }
  404 + }
  405 +}
  406 +
  407 +/* 主体布局 */
  408 +.content-layout {
  409 + flex: 1;
  410 + display: flex;
  411 + box-sizing: border-box;
  412 + overflow: hidden;
  413 + min-height: 0;
  414 +}
  415 +
  416 +/* 左侧分类 */
  417 +.sidebar {
  418 + width: 180rpx;
  419 + background-color: #ffffff;
  420 + border-radius: 16rpx;
  421 + height: 72%;
  422 + /* #ifdef H5 */
  423 + height: 86%;
  424 + /* #endif */
  425 + box-sizing: border-box;
  426 + overflow: hidden;
  427 +
  428 + .category-item {
  429 + padding: 22rpx 24rpx;
  430 + font-size: 24rpx;
  431 + color: #666;
  432 + position: relative;
463 433
464 - &::before {  
465 - content: '';  
466 - position: absolute;  
467 - right: 0;  
468 - top: 16rpx;  
469 - bottom: 16rpx;  
470 - width: 6rpx;  
471 - border-radius: 0 4rpx 4rpx 0;  
472 - background-color: #26c165;  
473 - } 434 + &.active {
  435 + background-color: #e8f8f0;
  436 + color: #26c165;
  437 + font-weight: 600;
  438 +
  439 + &::before {
  440 + content: '';
  441 + position: absolute;
  442 + right: 0;
  443 + top: 16rpx;
  444 + bottom: 16rpx;
  445 + width: 6rpx;
  446 + border-radius: 0 4rpx 4rpx 0;
  447 + background-color: #26c165;
474 } 448 }
475 } 449 }
476 } 450 }
477 -  
478 - /* 右侧计划列表 */  
479 - .plan-list-wrap {  
480 - flex: 1;  
481 - padding: 0 20rpx;  
482 - box-sizing: border-box;  
483 - height: 100%;  
484 - overflow: hidden; 451 +}
  452 +
  453 +/* 右侧计划列表 */
  454 +.plan-list-wrap {
  455 + flex: 1;
  456 + padding: 0 20rpx;
  457 + box-sizing: border-box;
  458 + height: 72%;
  459 + /* #ifdef H5 */
  460 + height: 86%;
  461 + /* #endif */
  462 + overflow: hidden;
  463 +}
  464 +
  465 +.plan-card {
  466 + position: relative;
  467 + margin-bottom: 16rpx;
  468 + border-radius: 16rpx;
  469 +
  470 + .plan-cover {
  471 + width: 100%;
  472 + height: 230rpx;
  473 + border-radius: 5rpx;
485 } 474 }
486 475
487 - .plan-card {  
488 - position: relative;  
489 - margin-bottom: 16rpx;  
490 - border-radius: 16rpx;  
491 -  
492 - .plan-cover {  
493 - width: 100%;  
494 - height: 230rpx;  
495 - border-radius: 5rpx;  
496 - } 476 + .plan-info {
  477 + position: absolute;
  478 + left: 20rpx;
  479 + right: 20rpx;
  480 + bottom: 20rpx;
  481 + color: #fff;
497 482
498 - .plan-info {  
499 - position: absolute;  
500 - left: 20rpx;  
501 - right: 20rpx;  
502 - bottom: 20rpx;  
503 - color: #fff;  
504 -  
505 - .plan-tag-row {  
506 - margin-bottom: 8rpx;  
507 -  
508 - .plan-tag {  
509 - display: inline-flex;  
510 - padding: 4rpx 10rpx;  
511 - border-radius: 6rpx;  
512 - font-size: 20rpx;  
513 - line-height: 1;  
514 -  
515 - &.tag-hot {  
516 - background-color: #ff4d4f;  
517 - }  
518 -  
519 - &.tag-new {  
520 - background-color: #ffbb00;  
521 - }  
522 - }  
523 - } 483 + .plan-tag-row {
  484 + margin-bottom: 8rpx;
524 485
525 - .plan-text {  
526 - display: flex;  
527 - flex-direction: column; 486 + .plan-tag {
  487 + display: inline-flex;
  488 + padding: 4rpx 10rpx;
  489 + border-radius: 6rpx;
  490 + font-size: 20rpx;
  491 + line-height: 1;
528 492
529 - .plan-title {  
530 - font-size: 28rpx;  
531 - font-weight: 600;  
532 - margin-bottom: 6rpx; 493 + &.tag-hot {
  494 + background-color: #ff4d4f;
533 } 495 }
534 496
535 - .plan-meta {  
536 - font-size: 22rpx;  
537 - color: rgba(255, 255, 255, 0.8); 497 + &.tag-new {
  498 + background-color: #ffbb00;
538 } 499 }
539 } 500 }
540 } 501 }
541 - }  
542 502
543 - /* 空状态 */  
544 - .empty-tip {  
545 - text-align: center;  
546 - padding: 100rpx 0;  
547 - color: #999;  
548 - font-size: 28rpx; 503 + .plan-text {
  504 + display: flex;
  505 + flex-direction: column;
  506 +
  507 + .plan-title {
  508 + font-size: 28rpx;
  509 + font-weight: 600;
  510 + margin-bottom: 6rpx;
  511 + }
  512 +
  513 + .plan-meta {
  514 + font-size: 22rpx;
  515 + color: rgba(255, 255, 255, 0.8);
  516 + }
  517 + }
549 } 518 }
  519 +}
  520 +
  521 +/* 空状态 */
  522 +.empty-tip {
  523 + text-align: center;
  524 + padding: 100rpx 0;
  525 + color: #999;
  526 + font-size: 28rpx;
  527 +}
550 </style> 528 </style>
@@ -317,8 +317,13 @@ const saveTemplateTitle = () => { @@ -317,8 +317,13 @@ const saveTemplateTitle = () => {
317 317
318 // 返回上一页 318 // 返回上一页
319 const goBack = () => { 319 const goBack = () => {
320 - uni.navigateBack();  
321 trainingStore.clearTrainingStore() 320 trainingStore.clearTrainingStore()
  321 + setTimeout(() => {
  322 + // 发送全局刷新信号,列表页会立刻执行刷新
  323 + uni.$emit('refreshTemplateList');
  324 + uni.navigateBack();
  325 + }, 1500);
  326 +
322 }; 327 };
323 328
324 // 保存模板(完整修复间歇训练 type=6) 329 // 保存模板(完整修复间歇训练 type=6)
@@ -399,12 +404,10 @@ const handleSave = async () => { @@ -399,12 +404,10 @@ const handleSave = async () => {
399 console.log('保存成功', res); 404 console.log('保存成功', res);
400 uni.hideLoading(); 405 uni.hideLoading();
401 uni.showToast({ title: '保存成功' }); 406 uni.showToast({ title: '保存成功' });
402 - 407 + trainingStore.clearTrainingStore();
403 setTimeout(() => { 408 setTimeout(() => {
404 - uni.redirectTo({  
405 - url: '/pages4/pages/xunji/xunji-wode-moban'  
406 - });  
407 - trainingStore.clearTrainingStore(); 409 + uni.navigateBack();
  410 +
408 }, 1500); 411 }, 1500);
409 412
410 } catch (err) { 413 } catch (err) {
@@ -173,6 +173,7 @@ import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; @@ -173,6 +173,7 @@ import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
173 import { useTrainingStore } from '@/sheep/store/trainingStore' 173 import { useTrainingStore } from '@/sheep/store/trainingStore'
174 import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue' 174 import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue'
175 import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue' 175 import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue'
  176 +import { trainingSnapshot } from '@/sheep/store/trainingSnapshot'
176 177
177 const trainingStore = useTrainingStore() 178 const trainingStore = useTrainingStore()
178 const hasConverted = ref(false); 179 const hasConverted = ref(false);
@@ -595,6 +596,39 @@ const openMin = () => { @@ -595,6 +596,39 @@ const openMin = () => {
595 596
596 console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining); 597 console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining);
597 trainingStore.min = true; 598 trainingStore.min = true;
  599 + // 保存快照
  600 + trainingSnapshot.data = JSON.parse(JSON.stringify({
  601 + id: trainingStore.id,
  602 + type: trainingStore.type,
  603 + actionDetail: trainingStore.actionDetail,
  604 + loading: trainingStore.loading,
  605 + unitRecords: trainingStore.unitRecords,
  606 + trainingName: trainingStore.trainingName,
  607 + totalSeconds: trainingStore.totalSeconds,
  608 + isPause: trainingStore.isPause,
  609 + showPicker: trainingStore.showPicker,
  610 + defaultTimeIndex: trainingStore.defaultTimeIndex,
  611 + trainingTimeText: trainingStore.trainingTimeText,
  612 + min: trainingStore.min,
  613 + isSystem: trainingStore.isSystem,
  614 + dailyTemplateId: trainingStore.dailyTemplateId,
  615 + isTraining: trainingStore.isTraining,
  616 + }))
  617 +
  618 + // 2. 记录时间戳和计时器状态
  619 + trainingSnapshot.timestamp = Date.now()
  620 + trainingSnapshot.timerWasRunning = !trainingStore.isPause
  621 +
  622 + // 3. 显示悬浮球
  623 + trainingSnapshot.visible = true
  624 +
  625 + // 4. 如果计时器在跑,先暂停(setInterval 跨页面会丢失,回显时用时间戳补偿)
  626 + if (!trainingStore.isPause) {
  627 + trainingStore.toggleTimer()
  628 + }
  629 +
  630 + console.log('🟡 快照已保存:id=', trainingSnapshot.data.id,
  631 + 'totalSeconds=', trainingSnapshot.data.totalSeconds)
598 // uni.navigateTo({ 632 // uni.navigateTo({
599 // url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径 633 // url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径
600 // }); 634 // });
@@ -55,7 +55,10 @@ @@ -55,7 +55,10 @@
55 </view> 55 </view>
56 <!-- 动作列表 --> 56 <!-- 动作列表 -->
57 <view class="section"> 57 <view class="section">
58 - <text class="section-title">动作列表</text> 58 + <view class="section-top">
  59 + <text class="section-title">动作列表</text>
  60 + <view v-if="date" class="right-btn" @click="templateEdit(TemplateDetail)">修改训练内容</view>
  61 + </view>
59 <view v-if="TemplateUnits.length > 0"> 62 <view v-if="TemplateUnits.length > 0">
60 <!-- 循环 unit,每个 unit 一个卡片 --> 63 <!-- 循环 unit,每个 unit 一个卡片 -->
61 <view v-for="(unit, unitIndex) in TemplateUnits" :key="unitIndex" class="exercise-item" 64 <view v-for="(unit, unitIndex) in TemplateUnits" :key="unitIndex" class="exercise-item"
@@ -111,9 +114,9 @@ @@ -111,9 +114,9 @@
111 {{ formatSeconds(detail.duration) }} 114 {{ formatSeconds(detail.duration) }}
112 </view> 115 </view>
113 <view class="detail-value" v-if="unit.exercises[0].exerciseType === 6"> 116 <view class="detail-value" v-if="unit.exercises[0].exerciseType === 6">
114 - {{ detail.duration }}组 x {{ formatSeconds(detail.duration) }} 117 + {{ detail.reps }}组 x {{ detail.duration }}秒 x {{ detail.restTime }}秒/组休息
115 </view> 118 </view>
116 - <view class="rest" v-if="detail.restTime"> 119 + <view class="rest" v-if="detail.restTime && unit.exercises[0].exerciseType !== 6">
117 {{ detail.restTime }}s 120 {{ detail.restTime }}s
118 </view> 121 </view>
119 </view> 122 </view>
@@ -158,9 +161,10 @@ @@ -158,9 +161,10 @@
158 {{ formatSeconds(ex.sets[idx - 1].duration) }} 161 {{ formatSeconds(ex.sets[idx - 1].duration) }}
159 </text> 162 </text>
160 <text v-if="ex.exerciseType === 6"> 163 <text v-if="ex.exerciseType === 6">
161 - {{ ex.sets[idx - 1].duration }}组 x {{ formatSeconds(ex.sets[idx - 1].duration) }} 164 + {{ ex.sets[idx - 1].reps }}组 x {{ ex.sets[idx - 1].duration }}秒 x {{ ex.sets[idx - 1].restTime
  165 + }}秒/组休息
162 </text> 166 </text>
163 - <view class="rest" v-if="ex.sets[idx - 1].restTime"> 167 + <view class="rest" v-if="ex.sets[idx - 1].restTime && ex.exerciseType !== 6">
164 {{ ex.sets[idx - 1].restTime }}s 168 {{ ex.sets[idx - 1].restTime }}s
165 </view> 169 </view>
166 </view> 170 </view>
@@ -534,6 +538,20 @@ const isUnlocked = computed(() => { @@ -534,6 +538,20 @@ const isUnlocked = computed(() => {
534 const openActionItem = (item) => { 538 const openActionItem = (item) => {
535 open(item.id, 1); 539 open(item.id, 1);
536 }; 540 };
  541 +// 修改每日模板训练内容
  542 +const templateEdit = (TemplateDetail) => {
  543 +
  544 + console.log('开始进入编辑模板页面,模板ID:', TemplateDetail.id, '每日模板ID:', TemplateDetail.dailyTemplateId);
  545 +
  546 + trainingStore.isSystem = TemplateDetail.isSystem;
  547 + trainingStore.loadDailyTemplateForEdit(TemplateDetail);
  548 + trainingStore.initDailyTemplateRecords()
  549 +
  550 + uni.navigateTo({
  551 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.id}&type=3&dailyTemplateId=${TemplateDetail.dailyTemplateId}`,
  552 + });
  553 +
  554 +};
537 555
538 const isMyPlan = ref(false) 556 const isMyPlan = ref(false)
539 const isDailytemplateId = ref(false) 557 const isDailytemplateId = ref(false)
@@ -645,6 +663,46 @@ onLoad((options) => { @@ -645,6 +663,46 @@ onLoad((options) => {
645 border-bottom: 1px solid #333; 663 border-bottom: 1px solid #333;
646 } 664 }
647 665
  666 +.section-top {
  667 + display: flex;
  668 + justify-content: space-between;
  669 + align-items: center;
  670 + margin-bottom: 20rpx;
  671 +}
  672 +
  673 +.section-title {
  674 + font-size: 32rpx;
  675 + font-weight: bold;
  676 + // margin-bottom: 20rpx;
  677 +}
  678 +
  679 +.right-btn {
  680 + font-size: 24rpx;
  681 + width: 140rpx;
  682 + /* #ifdef H5 */
  683 + width: 99px;
  684 + /* #endif */
  685 + height: 37rpx;
  686 + padding: 10rpx 20rpx;
  687 + background: rgba(90, 90, 90, 0.45);
  688 + justify-content: center;
  689 + display: flex;
  690 + align-items: center;
  691 + border-radius: 20rpx;
  692 +}
  693 +
  694 +.description {
  695 + font-size: 28rpx;
  696 + margin-bottom: 10rpx;
  697 +}
  698 +
  699 +.empty-tip {
  700 + text-align: center;
  701 + padding: 40rpx 0;
  702 + color: #999;
  703 + font-size: 28rpx;
  704 +}
  705 +
648 .section-title { 706 .section-title {
649 font-size: 32rpx; 707 font-size: 32rpx;
650 font-weight: bold; 708 font-weight: bold;
@@ -3,20 +3,39 @@ @@ -3,20 +3,39 @@
3 <template> 3 <template>
4 <view class="plan-detail-page"> 4 <view class="plan-detail-page">
5 <!-- 顶部安全区占位 --> 5 <!-- 顶部安全区占位 -->
6 - <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> 6 + <!-- <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> -->
7 <!-- 顶部导航栏 这里是模板大类的名字--> 7 <!-- 顶部导航栏 这里是模板大类的名字-->
8 - <view class="header" :style="{ height: headerHeight + 'px' }"> 8 + <!-- <view class="header">
9 <view class="back-btn" @click="navigateBack"> 9 <view class="back-btn" @click="navigateBack">
10 <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons> 10 <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>
11 </view> 11 </view>
12 <view class="title">{{ templateList.name }}</view> 12 <view class="title">{{ templateList.name }}</view>
13 - </view>  
14 13
15 - <scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}">  
16 <view class="description"> 14 <view class="description">
17 - <text>{{ templateList.description }}</text> 15 + <text>{{ templateList?.description }}</text>
  16 + </view>
  17 + </view> -->
  18 +
  19 + <view class="page-header" hover-class="none">
  20 + <!-- 动态导航栏:高度与胶囊对齐 -->
  21 + <view class="nav-bar" :style="{
  22 + paddingTop: menuButtonInfo.top + 'px',
  23 + height: menuButtonInfo.height + 'px'
  24 + }">
  25 + <view class="nav-left" @click="goBack">
  26 + <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>
  27 + </view>
  28 + <view class="nav-title">{{ templateList.name }}</view>
  29 + <view class="nav-right"></view>
  30 + </view>
  31 + <view class="description">
  32 + {{ templateList?.description }}
18 </view> 33 </view>
19 34
  35 + </view>
  36 +
  37 + <scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}">
  38 +
20 <view class="filter-bar"> 39 <view class="filter-bar">
21 40
22 <!-- 部位筛选 --> 41 <!-- 部位筛选 -->
@@ -109,11 +128,11 @@ const filteredTemplates = ref([]); // 筛选后列表 @@ -109,11 +128,11 @@ const filteredTemplates = ref([]); // 筛选后列表
109 const partList = ref([]); 128 const partList = ref([]);
110 const rawPartData = ref([]); 129 const rawPartData = ref([]);
111 const activePartId = ref('0'); 130 const activePartId = ref('0');
112 -const activePart = ref('不限'); 131 +const activePart = ref('部位');
113 132
114 // 场景(固定) 133 // 场景(固定)
115 const activeSceneId = ref('0'); 134 const activeSceneId = ref('0');
116 -const activeScene = ref('不限'); 135 +const activeScene = ref('场景');
117 const sceneList = ref([ 136 const sceneList = ref([
118 { id: '0', title: '不限' }, 137 { id: '0', title: '不限' },
119 { id: '1', title: '健身房' }, 138 { id: '1', title: '健身房' },
@@ -129,7 +148,7 @@ const templateMenuRef = ref(null) @@ -129,7 +148,7 @@ const templateMenuRef = ref(null)
129 const showPartDropdown = ref(false); 148 const showPartDropdown = ref(false);
130 const showSceneDropdown = ref(false); 149 const showSceneDropdown = ref(false);
131 // 返回上一页函数 150 // 返回上一页函数
132 -const navigateBack = () => { 151 +const goBack = () => {
133 uni.navigateBack(); 152 uni.navigateBack();
134 }; 153 };
135 // 获取部位分类(接口,和首页完全一样) 154 // 获取部位分类(接口,和首页完全一样)
@@ -269,7 +288,7 @@ onLoad((options) => { @@ -269,7 +288,7 @@ onLoad((options) => {
269 isIcon = true 288 isIcon = true
270 } 289 }
271 console.log("模板大类ID:", id.value); 290 console.log("模板大类ID:", id.value);
272 - console.log("是否官方大类:", isBigTemOffice); 291 + console.log("是否官方大类:", isBigTemOffice.value);
273 292
274 loadTemplates(id.value); 293 loadTemplates(id.value);
275 }); 294 });
@@ -277,6 +296,41 @@ onMounted(() => { @@ -277,6 +296,41 @@ onMounted(() => {
277 const systemInfo = uni.getSystemInfoSync(); 296 const systemInfo = uni.getSystemInfoSync();
278 statusBarHeight.value = systemInfo.statusBarHeight; 297 statusBarHeight.value = systemInfo.statusBarHeight;
279 getPartCategories(); 298 getPartCategories();
  299 +
  300 +
  301 + // 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐
  302 + // #ifdef MP-WEIXIN
  303 + try {
  304 + const rect = uni.getMenuButtonBoundingClientRect();
  305 + if (rect) {
  306 + menuButtonInfo.value = {
  307 + top: rect.top,
  308 + height: rect.height
  309 + };
  310 + }
  311 + } catch (e) {
  312 + console.log('获取胶囊信息失败,使用默认值', e);
  313 + }
  314 + // #endif
  315 +
  316 + // #ifndef MP-WEIXIN
  317 + // 非微信小程序环境,使用系统状态栏高度 + 标准导航栏高度
  318 + try {
  319 + const systemInfo = uni.getSystemInfoSync();
  320 + const statusBarHeight = systemInfo.statusBarHeight || 20;
  321 + menuButtonInfo.value = {
  322 + top: statusBarHeight,
  323 + height: 44 // 标准导航栏高度
  324 + };
  325 + } catch (e) {
  326 + console.log('获取系统信息失败', e);
  327 + }
  328 + // #endif
  329 +});
  330 +
  331 +const menuButtonInfo = ref({
  332 + top: 44, // 默认值,避免获取失败时样式异常
  333 + height: 32
280 }); 334 });
281 335
282 </script> 336 </script>
@@ -286,29 +340,96 @@ onMounted(() => { @@ -286,29 +340,96 @@ onMounted(() => {
286 .plan-detail-page { 340 .plan-detail-page {
287 width: 100%; 341 width: 100%;
288 height: 100vh; 342 height: 100vh;
289 - background-color: #f5f5f5;  
290 - box-sizing: border-box;  
291 display: flex; 343 display: flex;
292 flex-direction: column; 344 flex-direction: column;
  345 + background-color: #f5f5f5;
  346 + box-sizing: border-box;
293 } 347 }
294 348
295 -.status-bar-placeholder { 349 +// .status-bar-placeholder {
  350 +// width: 100%;
  351 +// background-color: #1a1a1a;
  352 +// }
  353 +
  354 +.page-header {
  355 + position: fixed;
296 width: 100%; 356 width: 100%;
297 - background-color: #1a1a1a; 357 + top: 0;
  358 + left: 0;
  359 + right: 0;
  360 + background-color: rgba(26, 26, 26, 0.9);
  361 + z-index: 999;
  362 + flex-shrink: 0;
  363 + // background-color: #fff;
  364 +
  365 +}
  366 +
  367 +/* 动态导航栏:高度与胶囊完全对齐 */
  368 +.nav-bar {
  369 + display: flex;
  370 + align-items: center;
  371 + justify-content: space-between;
  372 + // background-color: #1a1a1a;
  373 + /* 左右留出安全间距,避免内容贴边 */
  374 + padding-left: 16rpx;
  375 + padding-right: 16rpx;
  376 + /* 高度和顶部内边距由动态 style 控制 */
  377 + box-sizing: content-box;
  378 +}
  379 +
  380 +/* 左侧返回按钮区域 - 固定宽度确保居中对齐 */
  381 +.nav-left {
  382 + width: 60rpx;
  383 + display: flex;
  384 + align-items: center;
  385 + justify-content: flex-start;
  386 + flex-shrink: 0;
298 } 387 }
299 388
  389 +.back-icon {
  390 + display: block;
  391 +}
  392 +
  393 +/* 标题区域 - 自适应居中 */
  394 +.nav-title {
  395 + flex: 1;
  396 + text-align: center;
  397 + font-size: 36rpx;
  398 + font-weight: bold;
  399 + color: #fff;
  400 +
  401 + line-height: 1;
  402 +}
  403 +
  404 +.nav-right {
  405 + width: 60rpx;
  406 + flex-shrink: 0;
  407 +}
  408 +
  409 +.description {
  410 + padding: 20rpx 30rpx 20rpx;
  411 + color: #fff;
  412 + // background-color: #1a1a1a;
  413 + font-size: 28rpx;
  414 + line-height: 46rpx;
  415 +
  416 +}
  417 +
  418 +
  419 +//
  420 +
300 .header { 421 .header {
301 width: 100%; 422 width: 100%;
302 - position: relative;  
303 - background-color: #1a1a1a;  
304 - color: #fff; 423 + // position: relative;
305 display: flex; 424 display: flex;
306 align-items: center; 425 align-items: center;
307 justify-content: center; 426 justify-content: center;
  427 + background-color: #1a1a1a;
  428 + color: #fff;
308 font-size: 36rpx; 429 font-size: 36rpx;
309 font-weight: 600; 430 font-weight: 600;
310 - z-index: 10;  
311 - z-index: 999; 431 + // z-index: 10;
  432 + // z-index: 999;
312 } 433 }
313 434
314 .back-btn { 435 .back-btn {
@@ -335,7 +456,13 @@ onMounted(() => { @@ -335,7 +456,13 @@ onMounted(() => {
335 flex: 1; 456 flex: 1;
336 min-height: 0; 457 min-height: 0;
337 padding-top: 20rpx; 458 padding-top: 20rpx;
338 - margin-top: v-bind(statusBarHeight + headerHeight + 'px'); 459 + // margin-top: v-bind(statusBarHeight + headerHeight + 'px');
  460 + margin-top: 236rpx;
  461 +
  462 + /* #ifdef H5 */
  463 + min-height: 890px;
  464 + margin-top: 100px;
  465 + /* #endif */
339 } 466 }
340 467
341 .grid-container { 468 .grid-container {
@@ -369,15 +496,6 @@ onMounted(() => { @@ -369,15 +496,6 @@ onMounted(() => {
369 height: 300rpx; 496 height: 300rpx;
370 } 497 }
371 498
372 -.description {  
373 - padding: 30rpx 30rpx 20rpx;  
374 - color: #666;  
375 - font-size: 28rpx;  
376 - line-height: 46rpx;  
377 - background-color: #fff;  
378 - border-bottom: 1px solid #eee;  
379 -}  
380 -  
381 .card-info { 499 .card-info {
382 // 核心优化:增强背景对比度 + 视觉层次 500 // 核心优化:增强背景对比度 + 视觉层次
383 position: absolute; 501 position: absolute;
@@ -469,19 +587,19 @@ onMounted(() => { @@ -469,19 +587,19 @@ onMounted(() => {
469 } 587 }
470 588
471 .filter-item { 589 .filter-item {
  590 + height: 60rpx;
472 display: flex; 591 display: flex;
473 align-items: center; 592 align-items: center;
474 justify-content: center; 593 justify-content: center;
475 - padding: 0 30rpx;  
476 - height: 60rpx; 594 + padding: 0 15rpx;
477 line-height: 60rpx; 595 line-height: 60rpx;
478 border-radius: 30rpx; 596 border-radius: 30rpx;
  597 + background-color: #fff;
479 border: 1px solid #ddd; 598 border: 1px solid #ddd;
480 color: #333; 599 color: #333;
481 font-size: 28rpx; 600 font-size: 28rpx;
482 font-weight: 400; 601 font-weight: 400;
483 gap: 8rpx; 602 gap: 8rpx;
484 - background-color: #fff;  
485 603
486 &:active { 604 &:active {
487 background-color: #f5f5f5; 605 background-color: #f5f5f5;
@@ -491,7 +609,7 @@ onMounted(() => { @@ -491,7 +609,7 @@ onMounted(() => {
491 /* 筛选器容器 */ 609 /* 筛选器容器 */
492 .filter-wrapper { 610 .filter-wrapper {
493 position: relative; 611 position: relative;
494 - z-index: 999; 612 + // z-index: 999;
495 } 613 }
496 614
497 /* 下拉菜单 */ 615 /* 下拉菜单 */
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 <view class="filter-group"> 19 <view class="filter-group">
20 <!-- 部位 --> 20 <!-- 部位 -->
21 <view class="filter-wrapper"> 21 <view class="filter-wrapper">
22 - <view class="tab-item" :class="{ active: activePartId !== '0' }" @click="togglePartDropdown"> 22 + <view class="tab-item" :class="{ active: activePartId !== null }" @click="togglePartDropdown">
23 {{ activePartId === null ? '部位' : activePart }} 23 {{ activePartId === null ? '部位' : activePart }}
24 <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons> 24 <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons>
25 </view> 25 </view>
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 </view> 32 </view>
33 <!-- 场景 --> 33 <!-- 场景 -->
34 <view class="filter-wrapper"> 34 <view class="filter-wrapper">
35 - <view class="tab-item" :class="{ active: activeSceneId !== '0' }" @click="toggleSceneDropdown"> 35 + <view class="tab-item" :class="{ active: activeSceneId !== null }" @click="toggleSceneDropdown">
36 {{ activeSceneId === null ? '场景' : activeScene }} 36 {{ activeSceneId === null ? '场景' : activeScene }}
37 <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons> 37 <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons>
38 </view> 38 </view>
@@ -170,7 +170,8 @@ @@ -170,7 +170,8 @@
170 </template> 170 </template>
171 171
172 <script setup> 172 <script setup>
173 -import { onMounted, ref, nextTick } from 'vue'; 173 +import { onMounted, ref, nextTick, onUnmounted } from 'vue';
  174 +import { onShow } from '@dcloudio/uni-app';
174 import TemplatesApi from '@/sheep/api/Template/Templates'; 175 import TemplatesApi from '@/sheep/api/Template/Templates';
175 import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue' 176 import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue'
176 177
@@ -468,12 +469,44 @@ const menuButtonInfo = ref({ @@ -468,12 +469,44 @@ const menuButtonInfo = ref({
468 height: 32 469 height: 32
469 }); 470 });
470 471
  472 +// 添加 onShow 生命周期
  473 +onShow(() => {
  474 +
  475 + console.log('==== onShow执行了 ====');
  476 + activePartId.value = null;
  477 + activePart.value = '不限';
  478 + activeSceneId.value = null;
  479 + activeScene.value = '不限';
  480 + showPartDropdown.value = false;
  481 + showSceneDropdown.value = false;
  482 +
  483 + console.log('onShow刷新页面');
  484 +
  485 +
  486 + getMyTemplates();
  487 + getFolderList();
  488 +});
  489 +
471 onMounted(() => { 490 onMounted(() => {
472 // TemplatesList(); 491 // TemplatesList();
473 getMyTemplates(); 492 getMyTemplates();
474 getPartCategories(); 493 getPartCategories();
475 getFolderList(); 494 getFolderList();
476 495
  496 +
  497 + uni.$on('refreshTemplateList', () => {
  498 + console.log('收到刷新事件,强制刷新模板');
  499 + // 重置筛选条件,和onShow逻辑保持一致
  500 + activePartId.value = null;
  501 + activePart.value = '不限';
  502 + activeSceneId.value = null;
  503 + activeScene.value = '不限';
  504 + showPartDropdown.value = false;
  505 + showSceneDropdown.value = false;
  506 + // 执行刷新
  507 + handleRefreshTemplate();
  508 + });
  509 +
477 // 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐 510 // 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐
478 // #ifdef MP-WEIXIN 511 // #ifdef MP-WEIXIN
479 try { 512 try {
@@ -503,6 +536,10 @@ onMounted(() => { @@ -503,6 +536,10 @@ onMounted(() => {
503 } 536 }
504 // #endif 537 // #endif
505 }); 538 });
  539 +
  540 +onUnmounted(() => {
  541 + uni.$off('refreshTemplateList');
  542 +});
506 </script> 543 </script>
507 544
508 <style scoped lang="scss"> 545 <style scoped lang="scss">
@@ -729,7 +766,7 @@ onMounted(() => { @@ -729,7 +766,7 @@ onMounted(() => {
729 /* 正确的下拉菜单 */ 766 /* 正确的下拉菜单 */
730 .filter-wrapper { 767 .filter-wrapper {
731 position: relative; 768 position: relative;
732 - z-index: 9999; 769 + // z-index: 9999
733 } 770 }
734 771
735 .dropdown-menu { 772 .dropdown-menu {
@@ -198,16 +198,26 @@ const logout = () => { @@ -198,16 +198,26 @@ const logout = () => {
198 uni.showModal({ 198 uni.showModal({
199 title: '提示', 199 title: '提示',
200 content: '确定要退出登录吗?', 200 content: '确定要退出登录吗?',
201 - success: (res) => {  
202 - if (res.confirm) { 201 + success: async (res) => {
  202 + // if (res.confirm) {
  203 + // // 调用退出登录接口
  204 + // AuthUtil.logout().then(() => {
  205 + // // 清空用户信息
  206 + // userStore.logout();
  207 +
  208 + // // 跳转到登录页
  209 + // uni.reLaunch({ url: '/pages/index/index' });
  210 + // });
  211 + // }
  212 + try {
203 // 调用退出登录接口 213 // 调用退出登录接口
204 - AuthUtil.logout().then(() => {  
205 - // 清空用户信息  
206 - userStore.logout();  
207 -  
208 - // 跳转到登录页  
209 - uni.reLaunch({ url: '/pages/index/index' });  
210 - }); 214 + await AuthUtil.logout();
  215 + // 清空用户信息
  216 + userStore.logout();
  217 + // 跳转到正确登录页
  218 + uni.reLaunch({ url: '/pages7/pages/index/login' });
  219 + } catch (err) {
  220 + uni.showToast({ title: '退出失败', icon: 'none' });
211 } 221 }
212 }, 222 },
213 }); 223 });
@@ -56,7 +56,10 @@ @@ -56,7 +56,10 @@
56 <view class="input-box"> 56 <view class="input-box">
57 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 57 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
58 <input type="password" v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3" 58 <input type="password" v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3"
59 - class="native-input" :password="true" /> 59 + class="native-input" :password="!passwordShow" />
  60 + <!-- -->
  61 + <u-icon :name="passwordShow ? 'eye' : 'eye-off'" size="20" color="#a1a8b3" class="eye-icon"
  62 + @click="passwordShow = !passwordShow" />
60 </view> 63 </view>
61 </view> 64 </view>
62 </view> 65 </view>
@@ -70,7 +73,7 @@ @@ -70,7 +73,7 @@
70 <u-icon v-if="isAgreed" name="checkbox-mark" size="12" color="#ffffff" /> 73 <u-icon v-if="isAgreed" name="checkbox-mark" size="12" color="#ffffff" />
71 </view> 74 </view>
72 <view class="agreement-text"> 75 <view class="agreement-text">
73 - 我已阅读并同意 FitFlow 76 + 我已阅读并同意 自己练
74 <text class="link-text" @click.stop="goToAgreement('service')">《用户服务协议》</text> 77 <text class="link-text" @click.stop="goToAgreement('service')">《用户服务协议》</text>
75 78
76 <text class="link-text" @click.stop="goToAgreement('privacy')">《隐私政策》</text> 79 <text class="link-text" @click.stop="goToAgreement('privacy')">《隐私政策》</text>
@@ -98,6 +101,7 @@ const activeTab = ref('sms'); // sms: 免密, password: 密码 @@ -98,6 +101,7 @@ const activeTab = ref('sms'); // sms: 免密, password: 密码
98 const phoneNumber = ref(''); 101 const phoneNumber = ref('');
99 const verifyCode = ref(''); 102 const verifyCode = ref('');
100 const password = ref(''); 103 const password = ref('');
  104 +const passwordShow = ref(false) // 控制密码是否明文显示
101 const isAgreed = ref(false); 105 const isAgreed = ref(false);
102 106
103 // 防抖及加载动画状态变量 107 // 防抖及加载动画状态变量
@@ -396,6 +400,10 @@ onHide(() => { @@ -396,6 +400,10 @@ onHide(() => {
396 margin-right: 16rpx; 400 margin-right: 16rpx;
397 } 401 }
398 402
  403 + .eye-icon {
  404 + margin-left: 16rpx;
  405 + }
  406 +
399 .native-input { 407 .native-input {
400 flex: 1; 408 flex: 1;
401 height: 100%; 409 height: 100%;
@@ -29,7 +29,6 @@ const options = { @@ -29,7 +29,6 @@ const options = {
29 isToken: true, 29 isToken: true,
30 }; 30 };
31 31
32 -  
33 /** 跳转到登录页(navigateTo 失败时 reLaunch,避免页面栈异常) */ 32 /** 跳转到登录页(navigateTo 失败时 reLaunch,避免页面栈异常) */
34 function navigateToLogin() { 33 function navigateToLogin() {
35 uni.navigateTo({ 34 uni.navigateTo({
  1 +// sheep/store/trainingSnapshot.js
  2 +import { reactive } from 'vue';
  3 +
  4 +/**
  5 + * 训练快照 — 悬浮球与训练页面的共享数据桥梁
  6 + * 不是 Pinia store,是模块级 reactive 对象,存在 JS 堆内存中
  7 + * 页面跳转、clearTrainingStore() 都碰不到它
  8 + */
  9 +export const trainingSnapshot = reactive({
  10 + /** 全量 store 状态快照(JSON 深拷贝) */
  11 + data: null,
  12 + /** 快照时间戳(毫秒),用于回显时补偿计时器 */
  13 + timestamp: null,
  14 + /** 快照时计时器是否正在运行 */
  15 + timerWasRunning: false,
  16 + /** 悬浮球是否应该显示(独立于 trainingStore.min) */
  17 + visible: false,
  18 +});
@@ -416,7 +416,7 @@ export const useTrainingStore = defineStore('training', { @@ -416,7 +416,7 @@ export const useTrainingStore = defineStore('training', {
416 this.loading = false; 416 this.loading = false;
417 this.unitRecords = {}; 417 this.unitRecords = {};
418 this.trainingTimeText = ''; 418 this.trainingTimeText = '';
419 - this.min = false; 419 + // this.min = false;
420 this.isSystem = 1; 420 this.isSystem = 1;
421 this.trainingName = ''; 421 this.trainingName = '';
422 this.defaultTimeIndex = [0, 0, 0]; 422 this.defaultTimeIndex = [0, 0, 0];