Authored by Bad

feat: 优化训练日历与动作详情交互

Too many changes to show.

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

@@ -58,7 +58,8 @@ @@ -58,7 +58,8 @@
58 { 58 {
59 "path": "pages/xunji/xunji-wode-moban", 59 "path": "pages/xunji/xunji-wode-moban",
60 "style": { 60 "style": {
61 - "navigationBarTitleText": "我的模版" 61 + "navigationBarTitleText": "我的模版",
  62 + "navigationStyle": "default"
62 } 63 }
63 }, 64 },
64 { 65 {
@@ -82,7 +83,7 @@ @@ -82,7 +83,7 @@
82 { 83 {
83 "path": "pages/xunji/xunji-dongzuo-xinzeng", 84 "path": "pages/xunji/xunji-dongzuo-xinzeng",
84 "style": { 85 "style": {
85 - "navigationBarTitleText": "", 86 + "navigationBarTitleText": "新增自定义动作",
86 "navigationStyle": "default" 87 "navigationStyle": "default"
87 } 88 }
88 }, 89 },
@@ -180,13 +181,14 @@ @@ -180,13 +181,14 @@
180 { 181 {
181 "path": "pages/user/wode-changjian-wenti", 182 "path": "pages/user/wode-changjian-wenti",
182 "style": { 183 "style": {
183 - "navigationBarTitleText": "我的常见问题" 184 + "navigationBarTitleText": "常见问题",
  185 + "navigationStyle": "default"
184 } 186 }
185 }, 187 },
186 { 188 {
187 "path": "pages/user/wode-guanyu-hongxing", 189 "path": "pages/user/wode-guanyu-hongxing",
188 "style": { 190 "style": {
189 - "navigationBarTitleText": "我的关于鸿星" 191 + "navigationBarTitleText": "关于我们"
190 } 192 }
191 }, 193 },
192 { 194 {
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 <view @click="show = false"> 8 <view @click="show = false">
9 <up-icon name="close" color="#fff" size="20"></up-icon> 9 <up-icon name="close" color="#fff" size="20"></up-icon>
10 </view> 10 </view>
11 - <text class="titleName">动作备注</text> 11 + <text class="titleName">{{ title }}</text>
12 <view class="btn" @click="saveNoteContent">保存</view> 12 <view class="btn" @click="saveNoteContent">保存</view>
13 </view> 13 </view>
14 <view class="area-box"> 14 <view class="area-box">
@@ -30,6 +30,10 @@ const tempNoteContent = ref(''); // 临时编辑的备注内容 @@ -30,6 +30,10 @@ const tempNoteContent = ref(''); // 临时编辑的备注内容
30 const emit = defineEmits(['saveSuccess']); 30 const emit = defineEmits(['saveSuccess']);
31 31
32 const props = defineProps({ 32 const props = defineProps({
  33 + title: {
  34 + type: String,
  35 + default: '动作备注'
  36 + },
33 oldNote: { 37 oldNote: {
34 type: String, 38 type: String,
35 default: '' 39 default: ''
@@ -185,7 +185,7 @@ @@ -185,7 +185,7 @@
185 ====================================== --> 185 ====================================== -->
186 <view class="icons"> 186 <view class="icons">
187 <template v-if="!isEditing"> 187 <template v-if="!isEditing">
188 - <view class="icon" @click.stop="emitToggleActive(index)" :class="{ active: record.isActive }"> 188 + <view class="icon" v-if="isTick" @click.stop="emitToggleActive(index)" :class="{ active: record.isActive }">
189 <up-icon name="checkmark" :color="record.isActive ? '#000' : '#919191'" size="20" bold></up-icon> 189 <up-icon name="checkmark" :color="record.isActive ? '#000' : '#919191'" size="20" bold></up-icon>
190 </view> 190 </view>
191 </template> 191 </template>
@@ -210,6 +210,10 @@ const props = defineProps({ @@ -210,6 +210,10 @@ const props = defineProps({
210 userWeight: { 210 userWeight: {
211 type: Number, 211 type: Number,
212 default: 70 212 default: 70
  213 + },
  214 + isTick: {
  215 + type: Boolean,
  216 + default: true
213 } 217 }
214 }) 218 })
215 219
@@ -51,7 +51,7 @@ @@ -51,7 +51,7 @@
51 <action-set-input v-for="(item, index) in recordList" :key="index" 51 <action-set-input v-for="(item, index) in recordList" :key="index"
52 :exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing" 52 :exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing"
53 @openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow" 53 @openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow"
54 - @deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" /> 54 + @deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" :isTick="isTick"/>
55 </template> 55 </template>
56 56
57 <template v-else-if="type === 2"> 57 <template v-else-if="type === 2">
@@ -97,7 +97,7 @@ @@ -97,7 +97,7 @@
97 </view> 97 </view>
98 <super-set-input v-for="(sub, idx) in actionDetail.exercises" :key="sub.id" 98 <super-set-input v-for="(sub, idx) in actionDetail.exercises" :key="sub.id"
99 :sub-exercise-type="sub.exerciseType" :set-index="setIndex" :sub-index="idx" :user-weight="userWeight" 99 :sub-exercise-type="sub.exerciseType" :set-index="setIndex" :sub-index="idx" :user-weight="userWeight"
100 - :data="getSubActionData(setIndex, sub.id)" @open-time-picker="openSuperPicker(setIndex, sub.id)" /> 100 + :data="getSubActionData(setIndex, sub.id)" @open-time-picker="openSuperPicker(setIndex, sub.id)" :isTick="isTick"/>
101 </view> 101 </view>
102 </view> 102 </view>
103 </template> 103 </template>
@@ -171,7 +171,8 @@ const props = defineProps({ @@ -171,7 +171,8 @@ const props = defineProps({
171 unitIndex: Number, 171 unitIndex: Number,
172 actionDetail: { type: Object, default: () => ({}) }, 172 actionDetail: { type: Object, default: () => ({}) },
173 isDailyTemplates: { type: Boolean, default: false }, 173 isDailyTemplates: { type: Boolean, default: false },
174 - isUnitActive: { type: Boolean, default: false } 174 + isUnitActive: { type: Boolean, default: false },
  175 + isTick: { type: Boolean, default: true },
175 }) 176 })
176 177
177 // ===================== Emits ===================== 178 // ===================== Emits =====================
@@ -400,19 +401,22 @@ function selectDifficulty(label, color) { @@ -400,19 +401,22 @@ function selectDifficulty(label, color) {
400 401
401 function toggleActive(index) { 402 function toggleActive(index) {
402 const wasActive = recordList.value[index].isActive 403 const wasActive = recordList.value[index].isActive
  404 + // 在状态改变前获取当前组的休息秒数,用于取消时传递给父组件
  405 + const seconds = parseQuickTime(recordList.value[index].quickTimeDisplay)
403 recordList.value[index].isActive = !recordList.value[index].isActive 406 recordList.value[index].isActive = !recordList.value[index].isActive
404 const hasAnyActive = recordList.value.some(item => item.isActive) 407 const hasAnyActive = recordList.value.some(item => item.isActive)
405 emit('updateUnitActive', hasAnyActive) 408 emit('updateUnitActive', hasAnyActive)
406 if (recordList.value[index].isActive) { 409 if (recordList.value[index].isActive) {
407 - const seconds = parseQuickTime(recordList.value[index].quickTimeDisplay)  
408 emit('startRestTimer', seconds, props.unitIndex) 410 emit('startRestTimer', seconds, props.unitIndex)
409 } else if (wasActive) { 411 } else if (wasActive) {
410 - emit('cancelRestTimer', props.unitIndex) 412 + emit('cancelRestTimer', seconds, props.unitIndex)
411 } 413 }
412 } 414 }
413 415
414 function toggleSuperSetActive(setIndex) { 416 function toggleSuperSetActive(setIndex) {
415 const wasActive = superGroupSetData(setIndex).isActive 417 const wasActive = superGroupSetData(setIndex).isActive
  418 + // 在状态改变前获取当前组的休息秒数,用于取消时传递给父组件
  419 + const restSeconds = parseQuickTime(superGroupSetData(setIndex).quickTimeDisplay)
416 props.actionDetail.exercises.forEach(sub => { 420 props.actionDetail.exercises.forEach(sub => {
417 const list = superRecordMap.value[sub.id] 421 const list = superRecordMap.value[sub.id]
418 if (list?.[setIndex]) list[setIndex].isActive = !list[setIndex].isActive 422 if (list?.[setIndex]) list[setIndex].isActive = !list[setIndex].isActive
@@ -422,10 +426,9 @@ function toggleSuperSetActive(setIndex) { @@ -422,10 +426,9 @@ function toggleSuperSetActive(setIndex) {
422 ) 426 )
423 emit('updateUnitActive', hasAnyActive) 427 emit('updateUnitActive', hasAnyActive)
424 if (!wasActive) { 428 if (!wasActive) {
425 - const restSeconds = parseQuickTime(superGroupSetData(setIndex).quickTimeDisplay)  
426 emit('startRestTimer', restSeconds, props.unitIndex) 429 emit('startRestTimer', restSeconds, props.unitIndex)
427 } else { 430 } else {
428 - emit('cancelRestTimer', props.unitIndex) 431 + emit('cancelRestTimer', restSeconds, props.unitIndex)
429 } 432 }
430 } 433 }
431 434
@@ -119,9 +119,6 @@ @@ -119,9 +119,6 @@
119 <view class="memo-box" v-if="type === 1"> 119 <view class="memo-box" v-if="type === 1">
120 <view class="memo-title"> 120 <view class="memo-title">
121 <view class="section-title">步骤</view> 121 <view class="section-title">步骤</view>
122 - <view class="pencil-icon">  
123 - <up-icon name="edit-pen" color="#fedc1f" size="20"></up-icon>  
124 - </view>  
125 </view> 122 </view>
126 <view class="steps-list"> 123 <view class="steps-list">
127 <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text> 124 <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>
@@ -158,9 +155,9 @@ @@ -158,9 +155,9 @@
158 <view class="section-title">训练部位</view> 155 <view class="section-title">训练部位</view>
159 <view class="muscle-card"> 156 <view class="muscle-card">
160 <image 157 <image
161 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260612/muscle_back_1781252170914.png" 158 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg"
162 mode="widthFix" class="muscle-map" /> 159 mode="widthFix" class="muscle-map" />
163 - <view class="legend"> 160 + <!-- <view class="legend">
164 <view class="legend-item"> 161 <view class="legend-item">
165 <view class="legend-dot primary"></view> 162 <view class="legend-dot primary"></view>
166 <text>主要部位</text> 163 <text>主要部位</text>
@@ -168,8 +165,8 @@ @@ -168,8 +165,8 @@
168 <view class="legend-item"> 165 <view class="legend-item">
169 <view class="legend-dot secondary"></view> 166 <view class="legend-dot secondary"></view>
170 <text>次要部位</text> 167 <text>次要部位</text>
171 - </view>  
172 - </view> 168 + </view> -->
  169 + <!-- </view> -->
173 </view> 170 </view>
174 <view class="muscle-tags"> 171 <view class="muscle-tags">
175 <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length"> 172 <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length">
@@ -768,12 +765,14 @@ defineExpose({ open }); @@ -768,12 +765,14 @@ defineExpose({ open });
768 background: #464646; 765 background: #464646;
769 box-sizing: border-box; 766 box-sizing: border-box;
770 border-radius: 16rpx; 767 border-radius: 16rpx;
  768 + padding: 24rpx;
771 769
772 .step-text { 770 .step-text {
773 font-size: 30rpx; 771 font-size: 30rpx;
774 line-height: 1.6; 772 line-height: 1.6;
775 color: #bbb; 773 color: #bbb;
776 - list-style: none; 774 + white-space: pre-wrap;
  775 + word-break: break-all;
777 } 776 }
778 } 777 }
779 778
1 <template> 1 <template>
2 <!-- 添加自助训练底部弹窗 --> 2 <!-- 添加自助训练底部弹窗 -->
3 - <view v-if="visible" class="popup-overlay" @click="close">  
4 - <view class="popup-bottom" @click.stop>  
5 - <view class="popup-options">  
6 - <view class="popup-option" @click="handleAddFromPlan" v-if="planListCount > 0">  
7 - <view class=" icon-wrap">  
8 - <!-- <image src="/static/icons/plan.png" mode="aspectFit"></image> -->  
9 - <up-icon name="file-text-fill" color="#040000" size="24"></up-icon> 3 + <up-popup
  4 + :show="visible"
  5 + mode="bottom"
  6 + round="24rpx"
  7 + bg-color="#1a1a1a"
  8 + :close-on-click-overlay="true"
  9 + :safe-area-inset-bottom="true"
  10 + @close="close"
  11 + >
  12 + <view class="add-train-popup">
  13 + <!-- 拖拽指示条 -->
  14 + <view class="popup-indicator" />
  15 +
  16 + <!-- 菜单选项列表 -->
  17 + <view class="popup-menu">
  18 + <!-- 从训练计划中添加 -->
  19 + <view
  20 + v-if="planListCount > 0"
  21 + class="menu-item"
  22 + @click="handleAddFromPlan"
  23 + >
  24 + <view class="menu-icon">
  25 + <up-icon name="file-text-fill" color="#040000" size="24" />
10 </view> 26 </view>
11 - <view class="text-wrap" @click="">  
12 - <text class="option-title">从训练计划中添加</text>  
13 - <text class="option-desc">从我的训练计划中添加</text> 27 + <view class="menu-text">
  28 + <text class="menu-title">从训练计划中添加</text>
  29 + <text class="menu-desc">从我的训练计划中添加</text>
14 </view> 30 </view>
15 </view> 31 </view>
16 32
17 - <view class="popup-option" @click="handleAddFromTemplate">  
18 - <view class="icon-wrap">  
19 - <up-icon name="grid-fill" color="#040000" size="24"></up-icon> 33 + <!-- 使用训练模板 -->
  34 + <view class="menu-item" @click="handleAddFromTemplate">
  35 + <view class="menu-icon">
  36 + <up-icon name="grid-fill" color="#040000" size="24" />
20 </view> 37 </view>
21 - <view class="text-wrap">  
22 - <text class="option-title">使用训练模板</text>  
23 - <text class="option-desc">官方训练模板和自定义模板</text> 38 + <view class="menu-text">
  39 + <text class="menu-title">使用训练模板</text>
  40 + <text class="menu-desc">官方训练模板和自定义模板</text>
24 </view> 41 </view>
25 </view> 42 </view>
26 43
27 - <view class="popup-option" @click="handleFreeTraining">  
28 - <view class="icon-wrap">  
29 - <up-icon name="clock-fill" color="#040000" size="24"></up-icon> 44 + <!-- 自由训练 -->
  45 + <view class="menu-item" @click="handleFreeTraining">
  46 + <view class="menu-icon">
  47 + <up-icon name="clock-fill" color="#040000" size="24" />
30 </view> 48 </view>
31 - <view class="text-wrap">  
32 - <text class="option-title">自由训练</text>  
33 - <text class="option-desc">新建空白训练</text> 49 + <view class="menu-text">
  50 + <text class="menu-title">自由训练</text>
  51 + <text class="menu-desc">新建空白训练</text>
34 </view> 52 </view>
35 </view> 53 </view>
36 </view> 54 </view>
37 55
  56 + <!-- 关闭按钮 -->
38 <view class="close-btn" @click="close"> 57 <view class="close-btn" @click="close">
39 - <text>x</text> 58 + <up-icon name="close" color="#000" size="22" />
40 </view> 59 </view>
41 </view> 60 </view>
42 - <!-- 我的计划添加弹窗,显示最新添加的计划的模板列表 传递-->  
43 - <WodeJihuaTianjiaTancuang v-model:visible="showPlanPopup" @get-plan-list-length="getPlanListLength" :isAdd="true"  
44 - :plan-id="lastPlanId" :is-my-plan="true" />  
45 - </view> 61 + </up-popup>
  62 +
  63 + <!-- 我的计划添加弹窗(放在外部避免弹窗嵌套) -->
  64 + <WodeJihuaTianjiaTancuang
  65 + v-model:visible="showPlanPopup"
  66 + @get-plan-list-length="getPlanListLength"
  67 + :is-add="true"
  68 + :plan-id="lastPlanId"
  69 + :is-my-plan="true"
  70 + />
46 </template> 71 </template>
47 72
48 <script setup> 73 <script setup>
@@ -50,50 +75,57 @@ import { ref, onMounted, computed } from 'vue' @@ -50,50 +75,57 @@ import { ref, onMounted, computed } from 'vue'
50 import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue' 75 import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue'
51 import QueryPlanApi from '@/sheep/api/plan/queryplan' 76 import QueryPlanApi from '@/sheep/api/plan/queryplan'
52 77
53 -  
54 -// 控制训练计划弹窗显示  
55 -const showPlanPopup = ref(false)  
56 -const planListCount = ref(0)  
57 - 78 +// ==================== Props & Emits ====================
58 const props = defineProps({ 79 const props = defineProps({
59 visible: { 80 visible: {
60 type: Boolean, 81 type: Boolean,
61 - default: false  
62 - } 82 + default: false,
  83 + },
63 }) 84 })
64 85
65 const emit = defineEmits(['update:visible']) 86 const emit = defineEmits(['update:visible'])
66 87
67 -// 关闭弹窗 88 +// ==================== 响应式状态 ====================
  89 +const showPlanPopup = ref(false)
  90 +const planListCount = ref(0)
  91 +const planList = ref([])
  92 +
  93 +// ==================== 计算属性 ====================
  94 +/** 获取计划列表最后一条的 id,无数据返回 null */
  95 +const lastPlanId = computed(() => {
  96 + if (!Array.isArray(planList.value) || planList.value.length === 0) return null
  97 + return planList.value.at(-1)?.id ?? null
  98 +})
  99 +
  100 +// ==================== 方法 ====================
  101 +/** 关闭弹窗 */
68 const close = () => { 102 const close = () => {
69 emit('update:visible', false) 103 emit('update:visible', false)
70 } 104 }
71 105
72 -// 三个逻辑全部放在子组件内部  
73 -const handleAddFromPlan = () => {  
74 - // close() // 关闭当前弹窗  
75 - showPlanPopup.value = true  
76 -}  
77 - 106 +/** 获取子组件传递的计划列表长度 */
78 const getPlanListLength = (len) => { 107 const getPlanListLength = (len) => {
79 planListCount.value = len 108 planListCount.value = len
80 } 109 }
81 110
  111 +/** 从训练计划中添加:打开计划选择弹窗 */
  112 +const handleAddFromPlan = () => {
  113 + showPlanPopup.value = true
  114 +}
  115 +
  116 +/** 使用训练模板:跳转模板选择页 */
82 const handleAddFromTemplate = () => { 117 const handleAddFromTemplate = () => {
83 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' }) 118 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' })
84 close() 119 close()
85 } 120 }
86 121
  122 +/** 自由训练:跳转动作练习页 */
87 const handleFreeTraining = () => { 123 const handleFreeTraining = () => {
88 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' }) 124 uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' })
89 close() 125 close()
90 } 126 }
91 127
92 -// 获取我的计划列表的最后一个planid,传递到WodeJihuaTianjiaTancuang中  
93 -// 计划列表  
94 -const planList = ref([])  
95 -  
96 -// 获取我的全部训练计划 128 +/** 获取我的全部训练计划 */
97 const fetchMyPlanList = async () => { 129 const fetchMyPlanList = async () => {
98 try { 130 try {
99 const res = await QueryPlanApi.getMyPlan() 131 const res = await QueryPlanApi.getMyPlan()
@@ -108,73 +140,44 @@ const fetchMyPlanList = async () => { @@ -108,73 +140,44 @@ const fetchMyPlanList = async () => {
108 } 140 }
109 } 141 }
110 142
111 -// 计算属性:拿到列表最后一条计划id,无数据返回null  
112 -const lastPlanId = computed(() => {  
113 - if (!Array.isArray(planList.value) || planList.value.length === 0) return null  
114 - const lastPlan = planList.value.at(-1)  
115 - return lastPlan?.id ?? null  
116 -})  
117 - 143 +// ==================== 生命周期 ====================
118 onMounted(() => { 144 onMounted(() => {
119 fetchMyPlanList() 145 fetchMyPlanList()
120 }) 146 })
121 -  
122 -  
123 </script> 147 </script>
124 148
125 <style lang="scss" scoped> 149 <style lang="scss" scoped>
126 -/* 半透明黑色遮罩层 —— 内容靠底部!! */  
127 -.popup-overlay {  
128 - position: fixed;  
129 - top: 0;  
130 - left: 0;  
131 - width: 100vw;  
132 - height: 100vh;  
133 - background: rgba(0, 0, 0, 0.8);  
134 - z-index: 9999;  
135 - display: flex;  
136 - flex-direction: column;  
137 - /* 核心:靠下 */  
138 - justify-content: flex-end;  
139 - align-items: center;  
140 - /* 距离底部一点距离,不贴着最底下 */  
141 - padding-bottom: 150rpx;  
142 -}  
143 -  
144 -/* 弹窗主体容器 */  
145 -.popup-bottom {  
146 - width: 85%; 150 +.add-train-popup {
147 display: flex; 151 display: flex;
148 flex-direction: column; 152 flex-direction: column;
149 align-items: center; 153 align-items: center;
150 - gap: 70rpx;  
151 - margin-bottom: 100rpx;  
152 -} 154 + padding: 0 30rpx 40rpx;
  155 +
  156 + // 顶部拖拽指示条
  157 + .popup-indicator {
  158 + width: 72rpx;
  159 + height: 8rpx;
  160 + border-radius: 4rpx;
  161 + background: rgba(255, 255, 255, 0.3);
  162 + margin: 16rpx 0 32rpx;
  163 + }
153 164
154 -/* 选项列表 */  
155 -.popup-options { 165 + // 菜单选项列表
  166 + .popup-menu {
156 width: 100%; 167 width: 100%;
157 display: flex; 168 display: flex;
158 flex-direction: column; 169 flex-direction: column;
159 - justify-content: center;  
160 - align-items: center;  
161 - gap: 20rpx;  
162 -} 170 + gap: 16rpx;
163 171
164 -/* 单个选项项 */  
165 -.popup-option {  
166 - width: 100%;  
167 - // background: rgba(255, 255, 255, 0.1);  
168 - border-radius: 16rpx;  
169 - padding: 24rpx 30rpx; 172 + .menu-item {
170 display: flex; 173 display: flex;
171 align-items: center; 174 align-items: center;
172 -  
173 gap: 24rpx; 175 gap: 24rpx;
174 -} 176 + padding: 24rpx 30rpx;
  177 + border-radius: 16rpx;
  178 + }
175 179
176 -/* 黄色图标容器 */  
177 -.icon-wrap { 180 + .menu-icon {
178 width: 80rpx; 181 width: 80rpx;
179 height: 80rpx; 182 height: 80rpx;
180 border-radius: 50%; 183 border-radius: 50%;
@@ -183,33 +186,29 @@ onMounted(() => { @@ -183,33 +186,29 @@ onMounted(() => {
183 align-items: center; 186 align-items: center;
184 justify-content: center; 187 justify-content: center;
185 flex-shrink: 0; 188 flex-shrink: 0;
186 -  
187 - image {  
188 - width: 40rpx;  
189 - height: 40rpx;  
190 } 189 }
191 -}  
192 190
193 -/* 文字区域 */  
194 -.text-wrap { 191 + .menu-text {
195 display: flex; 192 display: flex;
196 flex-direction: column; 193 flex-direction: column;
197 gap: 8rpx; 194 gap: 8rpx;
198 -}  
199 195
200 -.option-title { 196 + .menu-title {
201 font-size: 32rpx; 197 font-size: 32rpx;
202 color: #fff; 198 color: #fff;
203 font-weight: 500; 199 font-weight: 500;
204 -} 200 + }
205 201
206 -.option-desc { 202 + .menu-desc {
207 font-size: 26rpx; 203 font-size: 26rpx;
208 color: rgba(255, 255, 255, 0.7); 204 color: rgba(255, 255, 255, 0.7);
209 -} 205 + }
  206 + }
  207 + }
210 208
211 -/* 底部关闭按钮 */  
212 -.close-btn { 209 + // 底部关闭按钮
  210 + .close-btn {
  211 + margin-top: 60rpx;
213 width: 88rpx; 212 width: 88rpx;
214 height: 88rpx; 213 height: 88rpx;
215 border-radius: 50%; 214 border-radius: 50%;
@@ -217,11 +216,6 @@ onMounted(() => { @@ -217,11 +216,6 @@ onMounted(() => {
217 display: flex; 216 display: flex;
218 align-items: center; 217 align-items: center;
219 justify-content: center; 218 justify-content: center;
220 -  
221 - text {  
222 - font-size: 40rpx;  
223 - color: #000;  
224 - line-height: 1;  
225 } 219 }
226 } 220 }
227 </style> 221 </style>
@@ -4,12 +4,12 @@ @@ -4,12 +4,12 @@
4 <!-- 顶部日期栏 --> 4 <!-- 顶部日期栏 -->
5 <view class="header-bar"> 5 <view class="header-bar">
6 <view class="close-btn" @click="closeCellPopup"> 6 <view class="close-btn" @click="closeCellPopup">
7 - <uni-icons type="down" size="24" color="#333"></uni-icons> 7 + <u-icon name="arrow-down" size="15" color="#333"></u-icon>
8 </view> 8 </view>
9 <view class="date-wrapper"> 9 <view class="date-wrapper">
10 <text class="date-text">{{ displayDate }}</text> 10 <text class="date-text">{{ displayDate }}</text>
11 </view> 11 </view>
12 - <button class="date-note-btn" @click.stop="handleDateNote">日期备注</button> 12 + <button class="date-note-btn" @click="handleDateNote">日期备注</button>
13 <button class="go-train-btn" @click.stop="startTraining"> 13 <button class="go-train-btn" @click.stop="startTraining">
14 <text class="go-train-text">去训练</text> 14 <text class="go-train-text">去训练</text>
15 <text class="go-train-tag">GO</text> 15 <text class="go-train-tag">GO</text>
@@ -21,10 +21,8 @@ @@ -21,10 +21,8 @@
21 <view class="noteContent" v-if="noteList.length > 0"> 21 <view class="noteContent" v-if="noteList.length > 0">
22 <text class="noteTitle">日程备注:</text> 22 <text class="noteTitle">日程备注:</text>
23 <view class="noteTags"> 23 <view class="noteTags">
24 - <view class="noteTag" v-for="(item, idx) in noteList" :key="item.id"  
25 - :style="{ backgroundColor: item.backgroundColor }" @click="handleEditNote(item)">  
26 - {{ item.content }}  
27 - </view> 24 + <u-tag class="noteTag" v-for="(item, idx) in noteList" :key="item.id" :text="item.content"
  25 + :bg-color="item.backgroundColor" color="#fff" shape="circle" @click="handleEditNote(item)" />
28 </view> 26 </view>
29 </view> 27 </view>
30 28
@@ -54,13 +52,10 @@ @@ -54,13 +52,10 @@
54 {{ currentPlan?.totalWeight }}kg</text> 52 {{ currentPlan?.totalWeight }}kg</text>
55 </view> 53 </view>
56 <view class="plan-header-btns"> 54 <view class="plan-header-btns">
57 - <button class="plan-btn more-btn" @click.stop="handlePlanMore">更多</button>  
58 - <button class="plan-btn copy-btn" @click.stop="openCopyCalendarPopup(currentPlan?.templateId)">  
59 - 复制到  
60 - </button>  
61 - <button class="plan-btn go-train-btn-small" @click.stop="handleTrainAgain"> 55 + <button class="plan-btn more-btn" @click="handlePlanMore">更多</button>
  56 + <button class="plan-btn copy-btn" @click="openCopyCalendarPopup(currentPlan?.templateId)">复制到</button>
  57 + <button class="plan-btn go-train-btn-small" @click="handleTrainAgain">
62 <text class="go-train-text-sm">今日再练</text> 58 <text class="go-train-text-sm">今日再练</text>
63 - <!-- <text class="go-train-tag-sm">GO</text> -->  
64 </button> 59 </button>
65 </view> 60 </view>
66 </view> 61 </view>
@@ -93,25 +88,8 @@ @@ -93,25 +88,8 @@
93 <view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx"> 88 <view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx">
94 <!-- 新增:把左边所有内容包起来 --> 89 <!-- 新增:把左边所有内容包起来 -->
95 <view class="set-left" :style="{ color: set.isCompleted === 1 ? '#333' : '#a2a2a2' }"> 90 <view class="set-left" :style="{ color: set.isCompleted === 1 ? '#333' : '#a2a2a2' }">
96 - <!-- <text class="indexData">{{ setIdx + 1 }}</text> -->  
97 - <text class="set-content" v-if="unit.exercises[0].exerciseType === 0">  
98 - {{ set.weight }}kg x {{ set.reps }}次  
99 - </text>  
100 - <text class="set-content" v-if="unit.exercises[0].exerciseType === 1">  
101 - {{ formatSecondsToHms(set.duration) }} x {{ set.distance }}km  
102 - </text>  
103 - <text class="set-content" v-if="unit.exercises[0].exerciseType === 2">  
104 - {{ set.reps }}次  
105 - </text>  
106 - <text class="set-content" v-if="unit.exercises[0].exerciseType === 3">  
107 - {{ formatSecondsToHms(set.duration) }}  
108 - </text>  
109 - <text class="set-content" v-if="[4, 5].includes(unit.exercises[0].exerciseType)">  
110 - {{ set.weight }}kg x {{ set.duration }}s x {{ set.restTime }}  
111 - </text>  
112 - <text class="set-content" v-if="unit.exercises[0].exerciseType === 6">  
113 - {{ set.reps }}组 x {{ set.duration }}秒 x {{ set.restTime || 0 }}秒/组休息  
114 - </text> 91 + <text class="set-content">{{ getSetDisplayText(set, unit.exercises[0].exerciseType)
  92 + }}</text>
115 </view> 93 </view>
116 <view class="set-right"> 94 <view class="set-right">
117 <text class="rest-time" v-if="set.restTime && unit.exercises[0].exerciseType != 6"> 95 <text class="rest-time" v-if="set.restTime && unit.exercises[0].exerciseType != 6">
@@ -127,7 +105,6 @@ @@ -127,7 +105,6 @@
127 <!-- 右侧:修改按钮 + 完成对勾 --> 105 <!-- 右侧:修改按钮 + 完成对勾 -->
128 <view class="action-right" @click="openXiuGai(unit, unitIdx)"> 106 <view class="action-right" @click="openXiuGai(unit, unitIdx)">
129 <button class="modify-btn">修改</button> 107 <button class="modify-btn">修改</button>
130 - <!-- <view class="check-icon">✓</view> -->  
131 </view> 108 </view>
132 </view> 109 </view>
133 <!-- 情况2:超级组 unitType=2 --> 110 <!-- 情况2:超级组 unitType=2 -->
@@ -168,28 +145,8 @@ @@ -168,28 +145,8 @@
168 145
169 <view class="set-content" 146 <view class="set-content"
170 :style="{ color: ex.sets[idx].isCompleted === 1 ? '#333' : '#a2a2a2' }"> 147 :style="{ color: ex.sets[idx].isCompleted === 1 ? '#333' : '#a2a2a2' }">
171 - <!-- 适配不同动作类型的显示文本 -->  
172 - <text v-if="ex.exerciseType === 0" class="detai-data">  
173 - {{ ex.sets[idx].weight }}kg x {{ ex.sets[idx].reps }}次  
174 - </text>  
175 - <text v-if="ex.exerciseType === 1" class="detai-data">  
176 - {{ formatSecondsToHms(ex.sets[idx].duration) }} x {{  
177 - ex.sets[idx].distance  
178 - }}km  
179 - </text>  
180 - <text v-if="ex.exerciseType === 2" class="detai-data">  
181 - {{ ex.sets[idx].reps }}次  
182 - </text>  
183 - <text v-if="ex.exerciseType === 3" class="detai-data">  
184 - {{ formatSecondsToHms(ex.sets[idx].duration) }}  
185 - </text>  
186 - <text v-if="[4, 5].includes(ex.exerciseType)" class="detai-data">  
187 - {{ ex.sets[idx].weight }}kg x {{ ex.sets[idx].reps }}次  
188 - </text>  
189 - <text v-if="ex.exerciseType === 6" class="detai-data">  
190 - {{ ex.sets[idx].reps }}次 x {{ ex.sets[idx].duration }}秒 x {{ ex.sets[idx].restTime  
191 - }}秒/组休息  
192 - </text> 148 + <text class="detai-data">{{ getSuperSetDisplayText(ex.sets[idx], ex.exerciseType)
  149 + }}</text>
193 <view class="rest-time" v-if="ex.sets[idx]?.restTime && ex.exerciseType !== 6" 150 <view class="rest-time" v-if="ex.sets[idx]?.restTime && ex.exerciseType !== 6"
194 :style="{ color: ex.sets[idx].isCompleted === 1 ? '#999 !important' : '#a2a2a2 !important' }"> 151 :style="{ color: ex.sets[idx].isCompleted === 1 ? '#999 !important' : '#a2a2a2 !important' }">
195 {{ ex.sets[idx].restTime }}s 152 {{ ex.sets[idx].restTime }}s
@@ -212,21 +169,13 @@ @@ -212,21 +169,13 @@
212 <!-- 空状态区域 --> 169 <!-- 空状态区域 -->
213 <view v-else class="empty-section"> 170 <view v-else class="empty-section">
214 <image class="empty-img" 171 <image class="empty-img"
215 - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" 172 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/空状态自助_1784875704380.png"
216 mode="aspectFit"> 173 mode="aspectFit">
217 </image> 174 </image>
218 <text class="empty-tip">今天没有安排</text> 175 <text class="empty-tip">今天没有安排</text>
219 <button class="add-train-btn" @click="openAddTrainPopup">+ 添加自助训练</button> 176 <button class="add-train-btn" @click="openAddTrainPopup">+ 添加自助训练</button>
220 177
221 - <!-- 底部课程类型卡片 -->  
222 - <!-- <view class="course-section">  
223 - <view class="course-item" v-for="(item, index) in courseList" :key="index"  
224 - @click="handleCourseClick(item.type)">  
225 - <image class="course-icon" :src="item.icon" mode="aspectFill"></image>  
226 - <text class="course-title">{{ item.title }}</text>  
227 - <text class="course-desc">{{ item.desc }}</text>  
228 - </view>  
229 - </view> --> 178 +
230 </view> 179 </view>
231 180
232 <AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" /> 181 <AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" />
@@ -298,59 +247,44 @@ import CalendarColorPicker from '@/pages/xunji/components/rili-components/Calend @@ -298,59 +247,44 @@ import CalendarColorPicker from '@/pages/xunji/components/rili-components/Calend
298 import { useTrainingStore } from '@/sheep/store/trainingStore' 247 import { useTrainingStore } from '@/sheep/store/trainingStore'
299 import MeiRiMoBanXiuGai from '../rili-components/meiri-moban-xiugai.vue' 248 import MeiRiMoBanXiuGai from '../rili-components/meiri-moban-xiugai.vue'
300 249
  250 +// ===== Props & Emits =====
301 const props = defineProps({ 251 const props = defineProps({
302 date: { type: String, default: '' }, 252 date: { type: String, default: '' },
303 - // noteList: {  
304 - // type: Array,  
305 - // default: () => []  
306 - // }  
307 }) 253 })
308 254
309 -const show = ref(false);  
310 const emit = defineEmits(['close', 'refreshCalendar']); 255 const emit = defineEmits(['close', 'refreshCalendar']);
311 -const noteList = ref([]); 256 +
  257 +// ===== 响应式状态 =====
  258 +const show = ref(false);
312 const showAddTrainPopup = ref(false); 259 const showAddTrainPopup = ref(false);
313 const selectedDate = ref(''); 260 const selectedDate = ref('');
314 const displayDate = ref(''); 261 const displayDate = ref('');
315 const resdailyData = ref([]); 262 const resdailyData = ref([]);
316 -// const noteList = ref([])  
317 const currentPlanIndex = ref(0); 263 const currentPlanIndex = ref(0);
  264 +const noteList = ref([]);
  265 +const noteHistoryList = ref([]);
318 266
  267 +// 弹窗相关
319 const isMoveMode = ref(false); 268 const isMoveMode = ref(false);
320 const isCopyMode = ref(false); 269 const isCopyMode = ref(false);
321 -const showRiqibeizhu = ref(false)  
322 -const moreShow = ref(false) 270 +const showRiqibeizhu = ref(false);
  271 +const moreShow = ref(false);
  272 +const showColorPopup = ref(false);
323 const calendarPopupRef = ref(null); 273 const calendarPopupRef = ref(null);
324 -const currentEditId = ref(null); // 新增这一行  
325 -const noteHistoryList = ref([]); 274 +const currentEditId = ref(null);
  275 +const selectedPlanId = ref(null);
326 276
327 -const showColorPopup = ref(false)  
328 -const selectedPlanId = ref(null) 277 +// Store / 子组件引用
  278 +const trainingStore = useTrainingStore();
  279 +const meirimobanRef = ref(null);
329 280
330 -const trainingStore = useTrainingStore() 281 +// 修改单元缓存
  282 +const editUnitInfo = ref({
  283 + unit: null,
  284 + unitIndex: null,
  285 + dailyTemplate: null,
  286 +});
331 287
332 -const meirimobanRef = ref(null)  
333 -// 没有训练模板时推荐课程  
334 -const courseList = ref([  
335 - {  
336 - type: 'group',  
337 - title: '团课',  
338 - desc: '大家一起练',  
339 - icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/group-course.png'  
340 - },  
341 - {  
342 - type: 'private',  
343 - title: '私教',  
344 - desc: '1对1专人指导',  
345 - icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/private-course.png'  
346 - },  
347 - {  
348 - type: 'smallClass',  
349 - title: '小班课',  
350 - desc: '28天极速瘦身',  
351 - icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/small-class.png'  
352 - }  
353 -]);  
354 288
355 // 打开弹窗 289 // 打开弹窗
356 const openPopup = async () => { 290 const openPopup = async () => {
@@ -370,14 +304,7 @@ defineExpose({ @@ -370,14 +304,7 @@ defineExpose({
370 closeCellPopup 304 closeCellPopup
371 }); 305 });
372 306
373 -// 修改  
374 -// 定义缓存变量,传给弹窗  
375 -const editUnitInfo = ref({  
376 - unit: null,  
377 - unitIndex: null,  
378 - dailyTemplate: null // currentPlan(整个每日模板对象,含id、templateId)  
379 -})  
380 -// 打开弹窗修改单个unit 307 +// ===== 弹窗:打开修改单个 unit =====
381 const openXiuGai = (unit, idx) => { 308 const openXiuGai = (unit, idx) => {
382 editUnitInfo.value = { 309 editUnitInfo.value = {
383 unit: JSON.parse(JSON.stringify(unit)), // 深拷贝防止原数据被实时篡改 310 unit: JSON.parse(JSON.stringify(unit)), // 深拷贝防止原数据被实时篡改
@@ -410,12 +337,14 @@ const handleAddTrain = async () => { @@ -410,12 +337,14 @@ const handleAddTrain = async () => {
410 // 更新主页面 337 // 更新主页面
411 emit('refreshCalendar'); 338 emit('refreshCalendar');
412 } 339 }
  340 +// ===== 计算属性 =====
413 // 当前正在显示的训记 341 // 当前正在显示的训记
414 const currentPlan = computed(() => { 342 const currentPlan = computed(() => {
415 if (!resdailyData.value.length) return null; 343 if (!resdailyData.value.length) return null;
416 return resdailyData.value[currentPlanIndex.value] || null; 344 return resdailyData.value[currentPlanIndex.value] || null;
417 }); 345 });
418 346
  347 +// ===== API:加载每日模板数据 =====
419 const loaddailytemplate = async () => { 348 const loaddailytemplate = async () => {
420 if (!selectedDate.value) return; 349 if (!selectedDate.value) return;
421 console.log('【子组件】开始加载数据...'); 350 console.log('【子组件】开始加载数据...');
@@ -458,7 +387,7 @@ const switchPlan = (index) => { @@ -458,7 +387,7 @@ const switchPlan = (index) => {
458 console.log('【切换标签后 - currentPlan】', currentPlan.value); 387 console.log('【切换标签后 - currentPlan】', currentPlan.value);
459 }; 388 };
460 389
461 -// 更多弹窗控制 390 +// ===== 更多弹窗:操作项 =====
462 const handlePlanMore = () => { 391 const handlePlanMore = () => {
463 if (!currentPlan.value) { // 加这个 392 if (!currentPlan.value) { // 加这个
464 return 393 return
@@ -472,6 +401,13 @@ const closeMorePopup = () => { @@ -472,6 +401,13 @@ const closeMorePopup = () => {
472 401
473 // 一键打勾(完成训练) 402 // 一键打勾(完成训练)
474 const handleCompleteCheck = async () => { 403 const handleCompleteCheck = async () => {
  404 + // 日期校验:只能对今天及以前的日期一键打勾
  405 + const today = dayjs().format('YYYY-MM-DD');
  406 + if (selectedDate.value > today) {
  407 + uni.showToast({ title: '一键打勾只能用于今天及以前的日期', icon: 'none' });
  408 + return;
  409 + }
  410 +
475 if (!currentPlan.value?.id) { 411 if (!currentPlan.value?.id) {
476 uni.showToast({ title: '未找到训练ID', icon: 'none' }) 412 uni.showToast({ title: '未找到训练ID', icon: 'none' })
477 return 413 return
@@ -494,13 +430,17 @@ const handleCompleteCheck = async () => { @@ -494,13 +430,17 @@ const handleCompleteCheck = async () => {
494 } 430 }
495 } 431 }
496 432
497 -// 保存到我的模板  
498 -// const saveToMyTemplate = (dailyTemplate) => {  
499 -// console.log('dailyTemplate', dailyTemplate);  
500 -  
501 -// }  
502 - 433 +// ===== 保存训练为个人模板 =====
503 const openSaveTemplateDialog = async (item) => { 434 const openSaveTemplateDialog = async (item) => {
  435 + // 关闭所有可能遮挡的弹窗
  436 + moreShow.value = false;
  437 + showColorPopup.value = false;
  438 + showAddTrainPopup.value = false;
  439 + showRiqibeizhu.value = false;
  440 + show.value = false
  441 + // 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭
  442 + await nextTick();
  443 +
504 // uni弹窗输入框:只填模板名称 444 // uni弹窗输入框:只填模板名称
505 uni.showModal({ 445 uni.showModal({
506 title: '确认保存为你的训练模板?', 446 title: '确认保存为你的训练模板?',
@@ -520,7 +460,6 @@ const openSaveTemplateDialog = async (item) => { @@ -520,7 +460,6 @@ const openSaveTemplateDialog = async (item) => {
520 // 发起创建接口 460 // 发起创建接口
521 console.log('reqData', reqData); 461 console.log('reqData', reqData);
522 await submitCreateTemplate(reqData) 462 await submitCreateTemplate(reqData)
523 - moreShow.value = false;  
524 } 463 }
525 } 464 }
526 }) 465 })
@@ -580,25 +519,42 @@ const closeAddTrainPopup = () => { @@ -580,25 +519,42 @@ const closeAddTrainPopup = () => {
580 showAddTrainPopup.value = false; 519 showAddTrainPopup.value = false;
581 }; 520 };
582 521
583 -// 弹窗选项点击事件  
584 -// const handleAddFromPlan = () => {  
585 -// uni.showToast({ title: '从训练计划添加', icon: 'none' });  
586 -// closeAddTrainPopup();  
587 -// };  
588 -// const handleAddFromTemplate = () => {  
589 -// uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' });  
590 -// closeAddTrainPopup();  
591 -// };  
592 -// const handleFreeTraining = () => {  
593 -// uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' });  
594 -// closeAddTrainPopup();  
595 -// };  
596 - 522 +// ===== 工具函数 =====
597 const formatSecondsToHms = (seconds) => { 523 const formatSecondsToHms = (seconds) => {
598 if (!seconds || isNaN(seconds)) return '00:00:00'; 524 if (!seconds || isNaN(seconds)) return '00:00:00';
599 return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss'); 525 return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss');
600 } 526 }
601 527
  528 +// ===== 工具函数:根据动作类型生成组次显示文本 =====
  529 +const getSetDisplayText = (set, exerciseType) => {
  530 + if (!set) return '';
  531 + switch (exerciseType) {
  532 + case 0: return `${set.weight || 0}kg x ${set.reps || 0}次`;
  533 + case 1: return `${formatSecondsToHms(set.duration)} x ${set.distance || 0}km`;
  534 + case 2: return `${set.reps || 0}次`;
  535 + case 3: return formatSecondsToHms(set.duration);
  536 + case 4:
  537 + case 5: return `${set.weight || 0}kg x ${set.duration || 0}s x ${set.restTime || 0}`;
  538 + case 6: return `${set.reps || 0}组 x ${set.duration || 0}秒 x ${set.restTime || 0}秒/组休息`;
  539 + default: return '';
  540 + }
  541 +};
  542 +
  543 +// 超级组中的动作显示(与普通动作在 type 4/5 上的展示略有差异)
  544 +const getSuperSetDisplayText = (set, exerciseType) => {
  545 + if (!set) return '';
  546 + switch (exerciseType) {
  547 + case 0: return `${set.weight || 0}kg x ${set.reps || 0}次`;
  548 + case 1: return `${formatSecondsToHms(set.duration)} x ${set.distance || 0}km`;
  549 + case 2: return `${set.reps || 0}次`;
  550 + case 3: return formatSecondsToHms(set.duration);
  551 + case 4:
  552 + case 5: return `${set.weight || 0}kg x ${set.reps || 0}次`;
  553 + case 6: return `${set.reps || 0}次 x ${set.duration || 0}秒 x ${set.restTime || 0}秒/组休息`;
  554 + default: return '';
  555 + }
  556 +};
  557 +
602 // 返回按钮逻辑 558 // 返回按钮逻辑
603 const goBack = () => { 559 const goBack = () => {
604 uni.navigateBack({ 560 uni.navigateBack({
@@ -609,19 +565,15 @@ const goBack = () => { @@ -609,19 +565,15 @@ const goBack = () => {
609 }); 565 });
610 }; 566 };
611 567
612 -// 日期格式化(兼容iOS 568 +// 日期格式化(使用 dayjs 处理,兼容性更好
613 const formatDateWithWeek = (dateStr) => { 569 const formatDateWithWeek = (dateStr) => {
614 if (!dateStr) return ''; 570 if (!dateStr) return '';
  571 + const d = dayjs(dateStr);
615 const weekMap = ['日', '一', '二', '三', '四', '五', '六']; 572 const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
616 - const iosSafeDate = new Date(dateStr.replace(/-/g, '/'));  
617 - const year = iosSafeDate.getFullYear();  
618 - const month = String(iosSafeDate.getMonth() + 1).padStart(2, '0');  
619 - const day = String(iosSafeDate.getDate()).padStart(2, '0');  
620 - const week = weekMap[iosSafeDate.getDay()];  
621 - return `${year}/${month}/${day} 周${week}`; 573 + return `${d.format('YYYY/MM/DD')} 周${weekMap[d.day()]}`;
622 }; 574 };
623 575
624 -// 打开日期备注组件 576 +// ===== 日期备注相关 =====
625 const handleDateNote = () => { 577 const handleDateNote = () => {
626 if (noteList.value.length >= 5) { 578 if (noteList.value.length >= 5) {
627 uni.showToast({ 579 uni.showToast({
@@ -648,7 +600,7 @@ const handleCloseNotePopup = () => { @@ -648,7 +600,7 @@ const handleCloseNotePopup = () => {
648 currentEditId.value = null; 600 currentEditId.value = null;
649 }; 601 };
650 602
651 -// 打开「添加到日历」弹窗 603 +// ===== 复制到 / 移动到 =====
652 const openCalendarPopup = (templateId) => { 604 const openCalendarPopup = (templateId) => {
653 console.log('点击了添加到日历按钮', templateId); // 新增日志 605 console.log('点击了添加到日历按钮', templateId); // 新增日志
654 console.log('子组件实例', calendarPopupRef.value); // 新增日志 606 console.log('子组件实例', calendarPopupRef.value); // 新增日志
@@ -695,6 +647,14 @@ const handleDeleteTemplate = async () => { @@ -695,6 +647,14 @@ const handleDeleteTemplate = async () => {
695 uni.showToast({ title: '未找到模板ID', icon: 'none' }); 647 uni.showToast({ title: '未找到模板ID', icon: 'none' });
696 return; 648 return;
697 } 649 }
  650 + // 关闭所有可能遮挡的弹窗
  651 + moreShow.value = false;
  652 + showColorPopup.value = false;
  653 + showAddTrainPopup.value = false;
  654 + showRiqibeizhu.value = false;
  655 + show.value = false
  656 + // 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭
  657 + await nextTick();
698 // 弹出确认框 658 // 弹出确认框
699 uni.showModal({ 659 uni.showModal({
700 title: '确认删除', 660 title: '确认删除',
@@ -820,7 +780,7 @@ const calendarColorPickerSuccess = () => { @@ -820,7 +780,7 @@ const calendarColorPickerSuccess = () => {
820 emit('refreshCalendar') 780 emit('refreshCalendar')
821 } 781 }
822 782
823 -// 去训练 783 +// ===== 去训练 / 每日模板编辑 =====
824 const startTraining = () => { 784 const startTraining = () => {
825 785
826 if (trainingStore.isTraining) { 786 if (trainingStore.isTraining) {
@@ -861,6 +821,7 @@ const templateEdit = (plan) => { @@ -861,6 +821,7 @@ const templateEdit = (plan) => {
861 console.log('开始进入编辑模板页面,模板ID:', plan.templateId, '每日模板ID:', plan.id); 821 console.log('开始进入编辑模板页面,模板ID:', plan.templateId, '每日模板ID:', plan.id);
862 }; 822 };
863 823
  824 +// ===== 监听 & 生命周期 =====
864 // 监听父组件传进来的 date 变化 825 // 监听父组件传进来的 date 变化
865 watch( 826 watch(
866 () => props.date, 827 () => props.date,
@@ -888,6 +849,19 @@ onMounted(() => { @@ -888,6 +849,19 @@ onMounted(() => {
888 </script> 849 </script>
889 850
890 <style lang="scss" scoped> 851 <style lang="scss" scoped>
  852 +// ===== 公共变量 =====
  853 +$color-primary: #333;
  854 +$color-accent: #ffc107;
  855 +$color-bg: #f2f3f5;
  856 +$color-white: #fff;
  857 +$color-danger: #ff4d4f;
  858 +$color-muted: #a2a2a2;
  859 +$color-card-bg: #f9f9f9;
  860 +$radius-sm: 8rpx;
  861 +$radius-md: 12rpx;
  862 +$radius-lg: 16rpx;
  863 +$radius-round: 40rpx;
  864 +
891 /* 全局容器 */ 865 /* 全局容器 */
892 .container { 866 .container {
893 display: flex; 867 display: flex;
@@ -941,9 +915,14 @@ onMounted(() => { @@ -941,9 +915,14 @@ onMounted(() => {
941 915
942 .date-text { 916 .date-text {
943 font-size: 28rpx; 917 font-size: 28rpx;
  918 + white-space: nowrap;
  919 + overflow: hidden;
  920 + text-overflow: ellipsis;
944 } 921 }
945 922
946 .date-note-btn { 923 .date-note-btn {
  924 + display: inline-flex;
  925 + align-items: center;
947 font-size: 28rpx; 926 font-size: 28rpx;
948 color: #333; 927 color: #333;
949 background: #e7e7e9; 928 background: #e7e7e9;
@@ -1066,11 +1045,11 @@ onMounted(() => { @@ -1066,11 +1045,11 @@ onMounted(() => {
1066 } 1045 }
1067 1046
1068 .go-train-btn-small { 1047 .go-train-btn-small {
1069 - // background: #ffc107;  
1070 color: #000; 1048 color: #000;
1071 display: flex; 1049 display: flex;
1072 align-items: center; 1050 align-items: center;
1073 padding: 12rpx 20rpx; 1051 padding: 12rpx 20rpx;
  1052 + background: inherit;
1074 } 1053 }
1075 1054
1076 .go-train-text-sm { 1055 .go-train-text-sm {
@@ -1286,34 +1265,34 @@ onMounted(() => { @@ -1286,34 +1265,34 @@ onMounted(() => {
1286 flex-direction: column; 1265 flex-direction: column;
1287 align-items: center; 1266 align-items: center;
1288 justify-content: center; 1267 justify-content: center;
1289 - // width: 500rpx;  
1290 - height: 500rpx;  
1291 - margin-top: 100rpx;  
1292 - padding: 20rpx 20rpx 60rpx; 1268 + padding: 80rpx 40rpx 120rpx;
1293 box-sizing: border-box; 1269 box-sizing: border-box;
1294 -  
1295 } 1270 }
1296 1271
1297 .empty-img { 1272 .empty-img {
1298 - width: 280rpx;  
1299 - height: 280rpx;  
1300 - margin-bottom: 30rpx; 1273 + width: 360rpx;
  1274 + height: 360rpx;
  1275 + margin-bottom: 40rpx;
  1276 + transform: scale(3);
1301 } 1277 }
1302 1278
1303 .empty-tip { 1279 .empty-tip {
1304 font-size: 32rpx; 1280 font-size: 32rpx;
1305 color: #666; 1281 color: #666;
1306 - margin-bottom: 60rpx; 1282 + font-weight: 500;
  1283 + margin-bottom: 80rpx;
  1284 + letter-spacing: 2rpx;
1307 } 1285 }
1308 1286
1309 .add-train-btn { 1287 .add-train-btn {
1310 font-size: 30rpx; 1288 font-size: 30rpx;
1311 color: #000; 1289 color: #000;
1312 - background: #ffc107;  
1313 - border-radius: 40rpx;  
1314 - padding: 20rpx 80rpx; 1290 + background: $color-accent;
  1291 + border-radius: $radius-round;
  1292 + padding: 24rpx 80rpx;
1315 border: none; 1293 border: none;
1316 - margin-bottom: 60rpx; 1294 + font-weight: 500;
  1295 + box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3);
1317 } 1296 }
1318 1297
1319 /* 底部课程类型区域 */ 1298 /* 底部课程类型区域 */
@@ -1485,6 +1464,7 @@ button::after { @@ -1485,6 +1464,7 @@ button::after {
1485 border: none; 1464 border: none;
1486 } 1465 }
1487 1466
  1467 +
1488 /* 训记切换标签 */ 1468 /* 训记切换标签 */
1489 .plan-tabs { 1469 .plan-tabs {
1490 white-space: nowrap; 1470 white-space: nowrap;
@@ -1512,93 +1492,87 @@ button::after { @@ -1512,93 +1492,87 @@ button::after {
1512 color: #fff; 1492 color: #fff;
1513 } 1493 }
1514 1494
1515 -/* ========== 超级组样式(已修复 无重复 无嵌套错误)========== */ 1495 +/* ========== 超级组样式 ========== */
1516 .superset-wrapper { 1496 .superset-wrapper {
1517 margin-bottom: 20rpx; 1497 margin-bottom: 20rpx;
1518 - background: #fff;  
1519 - border-radius: 12rpx; 1498 + background: $color-white;
  1499 + border-radius: $radius-md;
1520 overflow: hidden; 1500 overflow: hidden;
1521 -}  
1522 1501
1523 -.superset-wrapper .super-head { 1502 + .super-head {
1524 display: flex; 1503 display: flex;
1525 align-items: center; 1504 align-items: center;
1526 padding: 24rpx 20rpx; 1505 padding: 24rpx 20rpx;
1527 position: relative; 1506 position: relative;
1528 gap: 16rpx; 1507 gap: 16rpx;
1529 -} 1508 + }
1530 1509
1531 -.superset-wrapper .super-image { 1510 + .super-image {
1532 width: 75rpx; 1511 width: 75rpx;
1533 height: 75rpx; 1512 height: 75rpx;
1534 flex-shrink: 0; 1513 flex-shrink: 0;
1535 -} 1514 + }
1536 1515
1537 -.superset-wrapper .super-title-wrap { 1516 + .super-title-wrap {
1538 flex: 1; 1517 flex: 1;
1539 display: flex; 1518 display: flex;
1540 flex-direction: column; 1519 flex-direction: column;
1541 gap: 8rpx; 1520 gap: 8rpx;
1542 -} 1521 + }
1543 1522
1544 -.superset-wrapper .super-title { 1523 + .super-title {
1545 font-size: 32rpx; 1524 font-size: 32rpx;
1546 color: #111; 1525 color: #111;
1547 font-weight: 500; 1526 font-weight: 500;
1548 -} 1527 + }
1549 1528
1550 -.superset-wrapper .super-totalWeight { 1529 + .super-totalWeight {
1551 font-size: 24rpx; 1530 font-size: 24rpx;
1552 color: #666; 1531 color: #666;
1553 -} 1532 + }
1554 1533
1555 -.superset-wrapper .super-actionName { 1534 + .super-actionName {
1556 padding: 0 20rpx 20rpx 70rpx; 1535 padding: 0 20rpx 20rpx 70rpx;
1557 display: flex; 1536 display: flex;
1558 flex-direction: column; 1537 flex-direction: column;
1559 gap: 12rpx; 1538 gap: 12rpx;
1560 -}  
1561 1539
1562 -.superset-wrapper .name-item { 1540 + .action-name {
  1541 + font-size: 28rpx;
  1542 + color: $color-primary;
  1543 + }
  1544 + }
  1545 +
  1546 + .name-item {
1563 display: flex; 1547 display: flex;
1564 align-items: center; 1548 align-items: center;
1565 gap: 12rpx; 1549 gap: 12rpx;
1566 -} 1550 + }
1567 1551
1568 -.superset-wrapper .letter { 1552 + .letter {
1569 font-size: 28rpx; 1553 font-size: 28rpx;
1570 font-weight: bold; 1554 font-weight: bold;
1571 - color: #333;  
1572 -}  
1573 -  
1574 -.superset-wrapper .super-actionName .action-name {  
1575 - font-size: 28rpx;  
1576 - color: #333;  
1577 -} 1555 + color: $color-primary;
  1556 + }
1578 1557
1579 -.superset-wrapper .super-set-list { 1558 + .super-set-list {
1580 padding: 0 20rpx 20rpx 20rpx; 1559 padding: 0 20rpx 20rpx 20rpx;
1581 display: flex; 1560 display: flex;
1582 flex-direction: column; 1561 flex-direction: column;
1583 gap: 12rpx; 1562 gap: 12rpx;
1584 -} 1563 + }
1585 1564
1586 -.superset-wrapper .super-set-item { 1565 + .super-set-item {
1587 display: flex; 1566 display: flex;
1588 - // flex-direction: column; // 新增:垂直排列  
1589 - align-items: stretch; // 让每个动作占满宽度  
1590 - gap: 12rpx; // 垂直间距(可根据需要微调) 1567 + align-items: stretch;
  1568 + gap: 12rpx;
1591 padding: 16rpx; 1569 padding: 16rpx;
1592 background: #f9f9f9; 1570 background: #f9f9f9;
1593 border-radius: 12rpx; 1571 border-radius: 12rpx;
1594 -}  
1595 -  
1596 -// 为了让序号独立占一行  
1597 -.superset-wrapper .super-set-item {  
1598 margin-bottom: 8rpx; 1572 margin-bottom: 8rpx;
1599 -} 1573 + }
1600 1574
1601 -.superset-wrapper .set-number { 1575 + .set-number {
1602 font-size: 26rpx; 1576 font-size: 26rpx;
1603 background: #e4e4e4; 1577 background: #e4e4e4;
1604 color: #282828; 1578 color: #282828;
@@ -1608,27 +1582,27 @@ button::after { @@ -1608,27 +1582,27 @@ button::after {
1608 flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */ 1582 flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */
1609 border-radius: 50%; 1583 border-radius: 50%;
1610 // line-height: 60rpx; 1584 // line-height: 60rpx;
1611 -} 1585 + }
1612 1586
1613 -.superset-wrapper .set-column { 1587 + .set-column {
1614 display: flex; 1588 display: flex;
1615 flex-direction: column; 1589 flex-direction: column;
1616 -} 1590 + }
1617 1591
1618 -.superset-wrapper .action-in-set { 1592 + .action-in-set {
1619 // display: flex; 1593 // display: flex;
1620 // flex-direction: column; 1594 // flex-direction: column;
1621 width: 100%; 1595 width: 100%;
1622 display: flex; 1596 display: flex;
1623 align-items: center; 1597 align-items: center;
1624 margin-bottom: 8rpx; 1598 margin-bottom: 8rpx;
1625 -} 1599 + }
1626 1600
1627 -.superset-wrapper .action-in-set:last-child { 1601 + .action-in-set:last-child {
1628 margin-bottom: 0; 1602 margin-bottom: 0;
1629 -} 1603 + }
1630 1604
1631 -.superset-wrapper .action-letter { 1605 + .action-letter {
1632 display: inline-block; 1606 display: inline-block;
1633 font-size: 26rpx; 1607 font-size: 26rpx;
1634 color: #666; 1608 color: #666;
@@ -1636,28 +1610,27 @@ button::after { @@ -1636,28 +1610,27 @@ button::after {
1636 white-space: nowrap; 1610 white-space: nowrap;
1637 margin-right: 12rpx; 1611 margin-right: 12rpx;
1638 flex-shrink: 0; 1612 flex-shrink: 0;
1639 -} 1613 + }
1640 1614
1641 -.superset-wrapper .set-content { 1615 + .set-content {
1642 font-size: 26rpx; 1616 font-size: 26rpx;
1643 color: inherit; 1617 color: inherit;
1644 flex: 1; 1618 flex: 1;
1645 -} 1619 + }
1646 1620
1647 -.superset-wrapper .rest-time { 1621 + .rest-time {
1648 font-size: 24rpx; 1622 font-size: 24rpx;
1649 // color: inherit; 1623 // color: inherit;
1650 margin-left: 10rpx; 1624 margin-left: 10rpx;
1651 flex-shrink: 0; 1625 flex-shrink: 0;
1652 -} 1626 + }
1653 1627
1654 -.superset-wrapper .check { 1628 + .check {
1655 font-size: 26rpx; 1629 font-size: 26rpx;
1656 color: #333; 1630 color: #333;
1657 margin-left: 275rpx !important; 1631 margin-left: 275rpx !important;
1658 - /* 这里控制和左边内容的距离,数值你可以自己调 */  
1659 flex-shrink: 0; 1632 flex-shrink: 0;
1660 - /* 防止被压缩 */ 1633 + }
1661 } 1634 }
1662 1635
1663 .superset-title { 1636 .superset-title {
@@ -1673,16 +1646,14 @@ button::after { @@ -1673,16 +1646,14 @@ button::after {
1673 .noteContent { 1646 .noteContent {
1674 display: flex; 1647 display: flex;
1675 align-items: flex-start; 1648 align-items: flex-start;
1676 - /* 改成顶部对齐,而不是居中 */  
1677 gap: 12rpx; 1649 gap: 12rpx;
1678 padding: 20rpx; 1650 padding: 20rpx;
1679 - background: #fff; 1651 + background: $color-white;
1680 } 1652 }
1681 1653
1682 .noteTitle { 1654 .noteTitle {
1683 font-size: 26rpx; 1655 font-size: 26rpx;
1684 color: #666; 1656 color: #666;
1685 - /* 关键:固定宽度,保证标签容器的左边距稳定 */  
1686 flex-shrink: 0; 1657 flex-shrink: 0;
1687 width: 150rpx; 1658 width: 150rpx;
1688 } 1659 }
@@ -1694,11 +1665,10 @@ button::after { @@ -1694,11 +1665,10 @@ button::after {
1694 gap: 12rpx; 1665 gap: 12rpx;
1695 } 1666 }
1696 1667
1697 -.noteTag { 1668 +// u-tag 微调
  1669 +:deep(.noteTag) {
  1670 + width: auto !important;
1698 font-size: 24rpx; 1671 font-size: 24rpx;
1699 - color: #fff;  
1700 - padding: 6rpx 14rpx;  
1701 border-radius: 10rpx; 1672 border-radius: 10rpx;
1702 - line-height: 1.2;  
1703 } 1673 }
1704 </style> 1674 </style>
@@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
12 <scroll-view class="pop-content" scroll-y> 12 <scroll-view class="pop-content" scroll-y>
13 <dongzuo v-if="isRenderDongzuo" :id="currUnit.unitId" :type="currUnit.unitType" :actionDetail="convertAction" 13 <dongzuo v-if="isRenderDongzuo" :id="currUnit.unitId" :type="currUnit.unitType" :actionDetail="convertAction"
14 :unitIndex="currUnitIndex" ref="dongzuoSingleRef" @deleteAction.prevent="handleDeleteAction" 14 :unitIndex="currUnitIndex" ref="dongzuoSingleRef" @deleteAction.prevent="handleDeleteAction"
15 - @replace-action.prevent="handleReplaceAction" :is-daily-templates="true" /> 15 + @replace-action.prevent="handleReplaceAction" :is-daily-templates="true" :is-tick="false"/>
16 </scroll-view> 16 </scroll-view>
17 </view> 17 </view>
18 18
1 <template> 1 <template>
2 - <!-- 自定义底部弹窗 -->  
3 - <view v-if="showPopup" class="custom-mask" @click="handleMaskClick">  
4 - <view class="custom-popup" @click.stop> 2 + <!-- 底部弹窗:训练日历 -->
  3 + <up-popup
  4 + :show="showPopup"
  5 + mode="bottom"
  6 + round="24rpx"
  7 + :safeAreaInsetBottom="true"
  8 + @close="close"
  9 + >
  10 + <view class="calendar-popup-content">
5 <!-- 标题 + 月份导航 --> 11 <!-- 标题 + 月份导航 -->
6 <view class="popup-header"> 12 <view class="popup-header">
7 - <text class="title">训练日历</text>  
8 - <view class="month-nav">  
9 - <u-icon name="arrow-left" size="24" @click="prevMonth"></u-icon>  
10 - <text class="month-text">{{ currentYearMonth }}</text>  
11 - <u-icon name="arrow-right" size="24" @click="nextMonth"></u-icon> 13 + <text class="header-title">训练日历</text>
  14 + <view class="month-navigator">
  15 + <u-icon name="arrow-left" size="24" @click="prevMonth" />
  16 + <text class="month-label">{{ currentYearMonth }}</text>
  17 + <u-icon name="arrow-right" size="24" @click="nextMonth" />
12 </view> 18 </view>
13 </view> 19 </view>
14 20
15 - <!-- 自定义日历主体 -->  
16 - <view class="calendar-container"> 21 + <!-- 日历主体 -->
  22 + <view class="calendar-body">
17 <!-- 星期行 --> 23 <!-- 星期行 -->
18 - <view class="week-row">  
19 - <text v-for="week in weekDays" :key="week" class="week-day">{{ week }}</text> 24 + <view class="week-header">
  25 + <text
  26 + v-for="(day, idx) in WEEK_DAYS"
  27 + :key="idx"
  28 + class="week-item"
  29 + :class="{ weekend: idx === 0 || idx === 6 }"
  30 + >
  31 + {{ day }}
  32 + </text>
20 </view> 33 </view>
21 - <!-- 日期格子 -->  
22 - <view class="days-grid">  
23 - <view v-for="(day, idx) in calendarDays" :key="idx" class="day-cell" :class="{  
24 - 'other-month': !day.isCurrentMonth,  
25 - 'selected': isDateSelected(day.fullDate),  
26 - 'disabled': day.fullDate < minDateStr  
27 - }" @click="toggleDate(day)">  
28 - <text class="day-number">{{ day.day }}</text> 34 +
  35 + <!-- 日期网格 -->
  36 + <view class="date-grid">
  37 + <view
  38 + v-for="(item, idx) in calendarDays"
  39 + :key="idx"
  40 + class="date-cell"
  41 + :class="getDateCellClass(item)"
  42 + @click="toggleDate(item)"
  43 + >
  44 + <text class="date-text">{{ item.day }}</text>
29 </view> 45 </view>
30 </view> 46 </view>
31 </view> 47 </view>
32 48
33 - <!-- 底部确认按钮 --> 49 + <!-- 底部操作 -->
34 <view class="popup-footer"> 50 <view class="popup-footer">
35 - <u-button type="primary" size="large" :disabled="selectedDates.length === 0" @click="confirmAdd"> 51 + <view class="selected-hint" v-if="selectedDates.length">
  52 + 已选 {{ selectedDates.length }} 天
  53 + </view>
  54 + <u-button
  55 + type="primary"
  56 + size="large"
  57 + :disabled="!selectedDates.length"
  58 + @click="confirmAdd"
  59 + >
36 添加到以上日程 60 添加到以上日程
37 </u-button> 61 </u-button>
38 </view> 62 </view>
39 </view> 63 </view>
40 - </view> 64 + </up-popup>
41 </template> 65 </template>
42 66
43 <script setup> 67 <script setup>
44 -import { ref, computed, watch } from 'vue'; 68 +import { ref, computed } from 'vue';
  69 +import dayjs from 'dayjs';
45 import QueryPlanApi from '@/sheep/api/plan/queryplan'; 70 import QueryPlanApi from '@/sheep/api/plan/queryplan';
46 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; 71 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
47 72
  73 +// ==================== 常量 ====================
  74 +const DATE_FORMAT = 'YYYY-MM-DD';
  75 +const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'];
  76 +const TOTAL_GRID_CELLS = 42; // 6 行 × 7 列
  77 +
  78 +// ==================== Props & Emits ====================
48 const props = defineProps({ 79 const props = defineProps({
49 planId: { type: [String, Number], required: false }, 80 planId: { type: [String, Number], required: false },
50 templateId: { type: [String, Number], required: true, default: 0 }, 81 templateId: { type: [String, Number], required: true, default: 0 },
51 - isMove: {  
52 - type: Boolean,  
53 - default: false  
54 - }, 82 + isMove: { type: Boolean, default: false },
55 isCopy: { type: Boolean, default: false }, 83 isCopy: { type: Boolean, default: false },
56 - dailyTemplateId: { type: Number, default: 0 } 84 + dailyTemplateId: { type: Number, default: 0 },
57 }); 85 });
  86 +
58 const emit = defineEmits(['success', 'clearCopyFlag']); 87 const emit = defineEmits(['success', 'clearCopyFlag']);
59 88
60 -// 弹窗显示控制 89 +// ==================== 工具函数 ====================
  90 +/** 格式化 dayjs 实例为 YYYY-MM-DD 字符串 */
  91 +const fmtDate = (d) => d.format(DATE_FORMAT);
  92 +
  93 +/** 获取当天的 YYYY-MM-DD */
  94 +const getTodayStr = () => fmtDate(dayjs());
  95 +
  96 +// ==================== 响应式状态 ====================
61 const showPopup = ref(false); 97 const showPopup = ref(false);
62 98
63 -// 当前显示的日期(年、月)  
64 -const currentYear = ref(new Date().getFullYear());  
65 -const currentMonth = ref(new Date().getMonth() + 1); // 1-12 99 +// 当前视图的年月(用 dayjs 驱动)
  100 +const viewDate = ref(dayjs());
66 101
67 -// 已选择的日期列表(字符串数组,格式 YYYY-MM-DD) 102 +// 已选日期集合
68 const selectedDates = ref([]); 103 const selectedDates = ref([]);
69 104
70 // 最小可选日期(今天) 105 // 最小可选日期(今天)
71 -const today = new Date();  
72 -const minDateStr = `${today.getFullYear()}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}`; 106 +const minDateStr = getTodayStr();
73 107
74 -// 星期标题  
75 -const weekDays = ['日', '一', '二', '三', '四', '五', '六']; 108 +// ==================== 计算属性 ====================
  109 +/** 当前显示的年-月文本 */
  110 +const currentYearMonth = computed(() => viewDate.value.format('YYYY年MM月'));
76 111
77 -// 当前年月显示文本  
78 -const currentYearMonth = computed(() => `${currentYear.value}年${currentMonth.value.toString().padStart(2, '0')}月`);  
79 -  
80 -// 生成当前月份的日历数据(包含上个月和下个月的占位) 112 +/** 生成日历网格数据 */
81 const calendarDays = computed(() => { 113 const calendarDays = computed(() => {
82 - const year = currentYear.value;  
83 - const month = currentMonth.value;  
84 - const firstDayOfMonth = new Date(year, month - 1, 1);  
85 - const startWeekday = firstDayOfMonth.getDay(); // 0=周日  
86 - const daysInMonth = new Date(year, month, 0).getDate();  
87 -  
88 - // 获取上个月的最后几天  
89 - const prevMonthDate = new Date(year, month - 1, 0);  
90 - const daysInPrevMonth = prevMonthDate.getDate(); 114 + const vd = viewDate.value;
  115 + const year = vd.year();
  116 + const month = vd.month(); // 0-11
  117 +
  118 + // 当月第一天 & 当月天数
  119 + const firstDay = dayjs(new Date(year, month, 1));
  120 + const startWeekday = firstDay.day(); // 0=周日
  121 + const daysInMonth = firstDay.daysInMonth();
  122 +
  123 + // 上个月补位
91 const prevMonthDays = []; 124 const prevMonthDays = [];
  125 + const prevLastDay = firstDay.subtract(1, 'day');
  126 + const prevDaysInMonth = prevLastDay.date();
92 for (let i = startWeekday - 1; i >= 0; i--) { 127 for (let i = startWeekday - 1; i >= 0; i--) {
93 - const dayNum = daysInPrevMonth - i;  
94 - const fullDate = `${prevMonthDate.getFullYear()}-${(prevMonthDate.getMonth() + 1).toString().padStart(2, '0')}-${dayNum.toString().padStart(2, '0')}`;  
95 - prevMonthDays.push({  
96 - day: dayNum,  
97 - fullDate: fullDate,  
98 - isCurrentMonth: false,  
99 - }); 128 + const d = dayjs(new Date(year, month - 1, prevDaysInMonth - i));
  129 + prevMonthDays.push({ day: d.date(), fullDate: fmtDate(d), isCurrentMonth: false });
100 } 130 }
101 131
102 - // 当月天数  
103 - const currentMonthDays = []; 132 + // 当月
  133 + const curMonthDays = [];
104 for (let i = 1; i <= daysInMonth; i++) { 134 for (let i = 1; i <= daysInMonth; i++) {
105 - const fullDate = `${year}-${month.toString().padStart(2, '0')}-${i.toString().padStart(2, '0')}`;  
106 - currentMonthDays.push({  
107 - day: i,  
108 - fullDate: fullDate,  
109 - isCurrentMonth: true,  
110 - }); 135 + const d = dayjs(new Date(year, month, i));
  136 + curMonthDays.push({ day: i, fullDate: fmtDate(d), isCurrentMonth: true });
111 } 137 }
112 138
113 - // 下个月补齐(总共42格,6行)  
114 - const totalCells = 42;  
115 - const remaining = totalCells - (prevMonthDays.length + currentMonthDays.length); 139 + // 下个月补位
116 const nextMonthDays = []; 140 const nextMonthDays = [];
  141 + const remaining = TOTAL_GRID_CELLS - prevMonthDays.length - curMonthDays.length;
117 for (let i = 1; i <= remaining; i++) { 142 for (let i = 1; i <= remaining; i++) {
118 - const nextDate = new Date(year, month, i);  
119 - const fullDate = `${nextDate.getFullYear()}-${(nextDate.getMonth() + 1).toString().padStart(2, '0')}-${nextDate.getDate().toString().padStart(2, '0')}`;  
120 - nextMonthDays.push({  
121 - day: i,  
122 - fullDate: fullDate,  
123 - isCurrentMonth: false,  
124 - }); 143 + const d = dayjs(new Date(year, month + 1, i));
  144 + nextMonthDays.push({ day: i, fullDate: fmtDate(d), isCurrentMonth: false });
125 } 145 }
126 146
127 - return [...prevMonthDays, ...currentMonthDays, ...nextMonthDays]; 147 + return [...prevMonthDays, ...curMonthDays, ...nextMonthDays];
128 }); 148 });
129 149
130 -// 检查某天是否被选中  
131 -const isDateSelected = (fullDate) => {  
132 - return selectedDates.value.includes(fullDate);  
133 -}; 150 +// ==================== 方法 ====================
  151 +/** 判断日期是否被选中 */
  152 +const isDateSelected = (fullDate) => selectedDates.value.includes(fullDate);
134 153
135 -// 切换日期选中状态(仅允许选择今天及之后)  
136 -const toggleDate = (day) => {  
137 - if (day.fullDate < minDateStr) { 154 +/** 判断是否为今天 */
  155 +const isToday = (fullDate) => fullDate === getTodayStr();
  156 +
  157 +/** 获取日期格子的样式类 */
  158 +const getDateCellClass = (item) => ({
  159 + 'other-month': !item.isCurrentMonth,
  160 + selected: isDateSelected(item.fullDate),
  161 + disabled: item.fullDate < minDateStr,
  162 + today: isToday(item.fullDate),
  163 +});
  164 +
  165 +/** 切换日期选中状态 */
  166 +const toggleDate = (item) => {
  167 + if (item.fullDate < minDateStr) {
138 uni.showToast({ title: '不能选择过去的日期', icon: 'none' }); 168 uni.showToast({ title: '不能选择过去的日期', icon: 'none' });
139 return; 169 return;
140 } 170 }
141 - // 移动模式下,只能选择一个日期 171 + // 移动模式:单选
142 if (props.isMove) { 172 if (props.isMove) {
143 - selectedDates.value = [day.fullDate]; // 直接覆盖,永远只有1个 173 + selectedDates.value = [item.fullDate];
144 return; 174 return;
145 } 175 }
146 - const index = selectedDates.value.indexOf(day.fullDate);  
147 - if (index === -1) {  
148 - selectedDates.value.push(day.fullDate); 176 + // 普通模式:多选切换
  177 + const idx = selectedDates.value.indexOf(item.fullDate);
  178 + if (idx === -1) {
  179 + selectedDates.value.push(item.fullDate);
149 } else { 180 } else {
150 - selectedDates.value.splice(index, 1); 181 + selectedDates.value.splice(idx, 1);
151 } 182 }
152 }; 183 };
153 184
154 -// 月份切换 185 +/** 上一月 */
155 const prevMonth = () => { 186 const prevMonth = () => {
156 - if (currentMonth.value === 1) {  
157 - currentYear.value--;  
158 - currentMonth.value = 12;  
159 - } else {  
160 - currentMonth.value--;  
161 - } 187 + viewDate.value = viewDate.value.subtract(1, 'month');
162 }; 188 };
  189 +
  190 +/** 下一月 */
163 const nextMonth = () => { 191 const nextMonth = () => {
164 - if (currentMonth.value === 12) {  
165 - currentYear.value++;  
166 - currentMonth.value = 1;  
167 - } else {  
168 - currentMonth.value++;  
169 - } 192 + viewDate.value = viewDate.value.add(1, 'month');
  193 +};
  194 +
  195 +/** 重置到当前月份 */
  196 +const resetToToday = () => {
  197 + viewDate.value = dayjs();
170 }; 198 };
171 199
172 -// 关闭弹窗  
173 -const handleMaskClick = () => close(); 200 +/** 关闭弹窗 */
174 const close = () => { 201 const close = () => {
175 showPopup.value = false; 202 showPopup.value = false;
176 selectedDates.value = []; 203 selectedDates.value = [];
177 - // 重置到当前月份(可选)  
178 - currentYear.value = new Date().getFullYear();  
179 - currentMonth.value = new Date().getMonth() + 1;  
180 - emit('clearCopyFlag') 204 + resetToToday();
  205 + emit('clearCopyFlag');
181 }; 206 };
182 207
183 -// 打开弹窗(暴露给父组件) 208 +/** 打开弹窗 */
184 const open = () => { 209 const open = () => {
185 - console.log('子组件open方法被调用了');  
186 showPopup.value = true; 210 showPopup.value = true;
187 selectedDates.value = []; 211 selectedDates.value = [];
188 - currentYear.value = new Date().getFullYear();  
189 - currentMonth.value = new Date().getMonth() + 1; 212 + resetToToday();
190 }; 213 };
191 214
192 -// 确认添加 215 +/** 确认添加 */
193 const confirmAdd = async () => { 216 const confirmAdd = async () => {
194 - if (selectedDates.value.length === 0) { 217 + if (!selectedDates.value.length) {
195 uni.showToast({ title: '请选择训练日期', icon: 'none' }); 218 uni.showToast({ title: '请选择训练日期', icon: 'none' });
196 return; 219 return;
197 } 220 }
198 try { 221 try {
199 - // if (props.isMove) {  
200 - // const params = {  
201 - // id: props.templateId,  
202 - // trainingDate: selectedDates.value[0] || ''  
203 - // };  
204 - // // 添加到日历弹窗的参数  
205 - // console.log('移动到:params', params);  
206 - // await QueryPlanApi.updateDailyTemplateDate(params);  
207 - // uni.showToast({ title: '移动成功', icon: 'success' });  
208 - // } else {  
209 - // const params = {  
210 - // id: props.templateId,  
211 - // planId: props.planId,  
212 - // trainDateList: selectedDates.value  
213 - // };  
214 - // // 添加到日历弹窗的参数  
215 - // console.log('添加到params', params);  
216 - // await QueryPlanApi.addPlanToCalendar(params);  
217 - // uni.showToast({ title: '添加到日历成功', icon: 'success' });  
218 - // }  
219 - // ========== 新增复制分支 优先判断 ==========  
220 - console.log('rops.dailyTemplateId=', props.dailyTemplateId);  
221 if (props.isCopy) { 222 if (props.isCopy) {
222 - // 调用你新增的复制接口  
223 - await dailytemplateApi.copyTempleteDailyTemplate(props.dailyTemplateId, selectedDates.value) 223 + await dailytemplateApi.copyTempleteDailyTemplate(
  224 + props.dailyTemplateId,
  225 + selectedDates.value,
  226 + );
224 uni.showToast({ title: '复制成功', icon: 'success' }); 227 uni.showToast({ title: '复制成功', icon: 'success' });
225 - console.log('调用复制接口成功');  
226 -  
227 } else if (props.isMove) { 228 } else if (props.isMove) {
228 - // 移动到  
229 - const params = { 229 + await QueryPlanApi.updateDailyTemplateDate({
230 id: props.templateId, 230 id: props.templateId,
231 - trainingDate: selectedDates.value[0] || ''  
232 - };  
233 - await QueryPlanApi.updateDailyTemplateDate(params); 231 + trainingDate: selectedDates.value[0] || '',
  232 + });
234 uni.showToast({ title: '移动成功', icon: 'success' }); 233 uni.showToast({ title: '移动成功', icon: 'success' });
235 - console.log('移动接口成功');  
236 } else { 234 } else {
237 - //添加到日历  
238 - const params = { 235 + await QueryPlanApi.addPlanToCalendar({
239 id: props.templateId, 236 id: props.templateId,
240 planId: props.planId, 237 planId: props.planId,
241 - trainDateList: selectedDates.value  
242 - };  
243 - await QueryPlanApi.addPlanToCalendar(params); 238 + trainDateList: selectedDates.value,
  239 + });
244 uni.showToast({ title: '添加到日历成功', icon: 'success' }); 240 uni.showToast({ title: '添加到日历成功', icon: 'success' });
245 - console.log('添加到接口成功');  
246 } 241 }
247 -  
248 -  
249 close(); 242 close();
250 emit('success'); 243 emit('success');
251 } catch (err) { 244 } catch (err) {
@@ -258,120 +251,162 @@ defineExpose({ open, close }); @@ -258,120 +251,162 @@ defineExpose({ open, close });
258 </script> 251 </script>
259 252
260 <style scoped lang="scss"> 253 <style scoped lang="scss">
261 -.custom-mask {  
262 - position: fixed;  
263 - top: 0;  
264 - left: 0;  
265 - right: 0;  
266 - bottom: 0;  
267 - background-color: rgba(0, 0, 0, 0.5);  
268 - z-index: 9999;  
269 - display: flex;  
270 - align-items: flex-end;  
271 -}  
272 -  
273 -.custom-popup {  
274 - background: #fff;  
275 - border-radius: 24rpx 24rpx 0 0; 254 +/* ==================== 弹窗内容容器 ==================== */
  255 +.calendar-popup-content {
276 padding: 30rpx; 256 padding: 30rpx;
277 - box-sizing: border-box;  
278 - width: 100%;  
279 - max-height: 80vh;  
280 - overflow-y: auto; 257 + min-height: 50vh;
  258 + background: #fff;
281 } 259 }
282 260
  261 +/* ==================== 头部 ==================== */
283 .popup-header { 262 .popup-header {
284 display: flex; 263 display: flex;
285 justify-content: space-between; 264 justify-content: space-between;
286 align-items: center; 265 align-items: center;
287 - margin-bottom: 30rpx; 266 + margin-bottom: 28rpx;
288 267
289 - .title {  
290 - font-size: 32rpx;  
291 - font-weight: bold;  
292 - color: #333; 268 + .header-title {
  269 + font-size: 34rpx;
  270 + font-weight: 700;
  271 + color: #1a1a1a;
293 } 272 }
294 273
295 - .month-nav { 274 + .month-navigator {
296 display: flex; 275 display: flex;
297 align-items: center; 276 align-items: center;
298 - gap: 20rpx; 277 + gap: 24rpx;
299 278
300 - .month-text {  
301 - font-size: 28rpx; 279 + .month-label {
  280 + font-size: 30rpx;
  281 + font-weight: 500;
302 color: #333; 282 color: #333;
  283 + min-width: 140rpx;
  284 + text-align: center;
303 } 285 }
304 } 286 }
305 } 287 }
306 288
307 -.calendar-container {  
308 - margin-bottom: 30rpx; 289 +/* ==================== 日历主体 ==================== */
  290 +.calendar-body {
  291 + flex: 1;
309 } 292 }
310 293
311 -.week-row { 294 +.week-header {
312 display: flex; 295 display: flex;
313 - justify-content: space-around;  
314 - margin-bottom: 20rpx; 296 + margin-bottom: 16rpx;
  297 + padding: 0 4rpx;
315 298
316 - .week-day {  
317 - width: 14.28%; 299 + .week-item {
  300 + flex: 1;
318 text-align: center; 301 text-align: center;
319 - font-size: 28rpx; 302 + font-size: 26rpx;
320 color: #999; 303 color: #999;
  304 + padding: 8rpx 0;
  305 +
  306 + &.weekend {
  307 + color: #e6a23c;
  308 + }
321 } 309 }
322 } 310 }
323 311
324 -.days-grid { 312 +.date-grid {
325 display: flex; 313 display: flex;
326 flex-wrap: wrap; 314 flex-wrap: wrap;
327 } 315 }
328 316
329 -.day-cell {  
330 - width: 14.28%;  
331 - aspect-ratio: 1 / 1; 317 +.date-cell {
  318 + width: calc(100% / 7);
  319 + aspect-ratio: 1;
332 display: flex; 320 display: flex;
333 align-items: center; 321 align-items: center;
334 justify-content: center; 322 justify-content: center;
335 - border-radius: 8rpx;  
336 - margin-bottom: 8rpx; 323 + border-radius: 50%;
  324 + position: relative;
  325 + transition: background-color 0.2s, transform 0.15s;
337 326
338 - .day-number { 327 + .date-text {
339 font-size: 28rpx; 328 font-size: 28rpx;
340 color: #333; 329 color: #333;
  330 + position: relative;
  331 + z-index: 1;
341 } 332 }
342 333
343 - &.other-month .day-number {  
344 - color: #ccc; 334 + /* 非当月 */
  335 + &.other-month {
  336 + opacity: 0;
  337 + pointer-events: none;
345 } 338 }
346 339
  340 + /* 今天 */
  341 + &.today {
  342 + .date-text {
  343 + color: #409eff;
  344 + font-weight: 600;
  345 + &::after {
  346 + content: '';
  347 + position: absolute;
  348 + bottom: -4rpx;
  349 + left: 50%;
  350 + transform: translateX(-50%);
  351 + width: 8rpx;
  352 + height: 8rpx;
  353 + border-radius: 50%;
  354 + background: #409eff;
  355 + }
  356 + }
  357 + }
  358 +
  359 + /* 已选中 */
347 &.selected { 360 &.selected {
348 - background-color: #ffd700; 361 + background: linear-gradient(135deg, #ffd700, #ffb800);
  362 +
  363 + .date-text {
  364 + color: #1a1a1a;
  365 + font-weight: 700;
  366 + }
349 367
350 - .day-number {  
351 - color: #000;  
352 - font-weight: bold; 368 + &.today .date-text::after {
  369 + background: #1a1a1a;
353 } 370 }
354 } 371 }
355 372
  373 + /* 不可选(过去日期) */
356 &.disabled { 374 &.disabled {
357 - opacity: 0.4; 375 + opacity: 0.3;
358 pointer-events: none; 376 pointer-events: none;
359 } 377 }
  378 +
  379 + /* 可点击态 */
  380 + &:not(.disabled):not(.other-month):active {
  381 + transform: scale(0.9);
  382 + background-color: #f0f0f0;
  383 + }
360 } 384 }
361 385
  386 +/* ==================== 底部 ==================== */
362 .popup-footer { 387 .popup-footer {
363 - margin-top: 30rpx;  
364 - padding-top: 30rpx; 388 + margin-top: 24rpx;
  389 + padding-top: 20rpx;
  390 + border-top: 1rpx solid #f0f0f0;
  391 +
  392 + .selected-hint {
  393 + text-align: center;
  394 + font-size: 24rpx;
  395 + color: #999;
  396 + margin-bottom: 16rpx;
  397 + }
365 398
366 .u-button { 399 .u-button {
367 background-color: #ffd700 !important; 400 background-color: #ffd700 !important;
368 - color: #000;  
369 - font-weight: bold;  
370 - border-radius: 40rpx; 401 + color: #1a1a1a;
  402 + font-weight: 700;
  403 + font-size: 30rpx;
  404 + border-radius: 44rpx;
  405 + letter-spacing: 2rpx;
371 406
372 &.u-button--disabled { 407 &.u-button--disabled {
373 - background-color: #eee !important;  
374 - color: #999 !important; 408 + background-color: #e8e8e8 !important;
  409 + color: #bbb !important;
375 } 410 }
376 } 411 }
377 } 412 }
@@ -345,15 +345,23 @@ const chartSeries = computed(() => { @@ -345,15 +345,23 @@ const chartSeries = computed(() => {
345 345
346 const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume'; 346 const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
347 347
  348 + /** 单位转换:秒→分钟,米→公里,保留一位小数 */
  349 + const convertVal = (raw) => {
  350 + const num = Number(raw) || 0;
  351 + if (dataKey === 'duration') return parseFloat((num / 60).toFixed(1));
  352 + if (dataKey === 'distance') return parseFloat((num / 1000).toFixed(1));
  353 + return num;
  354 + };
  355 +
348 // 当前周期数据 356 // 当前周期数据
349 - const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0); 357 + const currData = fullDateKeys.map((dateStr) => convertVal(currDataMap[dateStr]?.[dataKey] ?? 0));
350 358
351 // 上一周期数据(按日期偏移匹配) 359 // 上一周期数据(按日期偏移匹配)
352 const lastData = fullDateKeys.map((dateStr) => { 360 const lastData = fullDateKeys.map((dateStr) => {
353 const targetKey = isWeekly 361 const targetKey = isWeekly
354 ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD') 362 ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD')
355 : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD'); 363 : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD');
356 - return lastDataMap[targetKey]?.[dataKey] ?? 0; 364 + return convertVal(lastDataMap[targetKey]?.[dataKey] ?? 0);
357 }); 365 });
358 366
359 const currName = isWeekly ? '本周' : '本月'; 367 const currName = isWeekly ? '本周' : '本月';
@@ -372,11 +380,15 @@ const listData = computed(() => { @@ -372,11 +380,15 @@ const listData = computed(() => {
372 if (!weeklyData.value) return []; 380 if (!weeklyData.value) return [];
373 381
374 const d = weeklyData.value; 382 const d = weeklyData.value;
  383 + /** 单位转换:秒→分钟,米→公里,保留一位小数 */
  384 + const toMin = (v) => parseFloat(((v ?? 0) / 60).toFixed(1));
  385 + const toKm = (v) => parseFloat(((v ?? 0) / 1000).toFixed(1));
  386 +
375 const fields = [ 387 const fields = [
376 { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume }, 388 { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },
377 { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets }, 389 { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },
378 - { label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration },  
379 - { label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance }, 390 + { label: '时长', last: toMin(d.lastWeekTotalDuration), curr: toMin(d.totalDuration) },
  391 + { label: '距离', last: toKm(d.lastWeekTotalDistance), curr: toKm(d.totalDistance) },
380 { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, 392 { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount },
381 ]; 393 ];
382 394
@@ -387,7 +399,7 @@ const listData = computed(() => { @@ -387,7 +399,7 @@ const listData = computed(() => {
387 label, 399 label,
388 lastPeriod: lastVal, 400 lastPeriod: lastVal,
389 currPeriod: currVal, 401 currPeriod: currVal,
390 - diff: currVal - lastVal, 402 + diff: parseFloat((currVal - lastVal).toFixed(1)),
391 }; 403 };
392 }); 404 });
393 }); 405 });
@@ -34,7 +34,7 @@ const emit = defineEmits(['success', 'close']) @@ -34,7 +34,7 @@ const emit = defineEmits(['success', 'close'])
34 // 组件状态 34 // 组件状态
35 const show = ref(false) 35 const show = ref(false)
36 const folderList = ref([]) 36 const folderList = ref([])
37 -const defaultCover = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png' // 默认封面,可替换为你的本地图片 37 +const defaultCover = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/文件夹_1784867551849.png' // 默认封面,可替换为你的本地图片
38 38
39 39
40 const props = defineProps({ 40 const props = defineProps({
@@ -233,6 +233,11 @@ const handleDeleteTemplate = async () => { @@ -233,6 +233,11 @@ const handleDeleteTemplate = async () => {
233 return; 233 return;
234 } 234 }
235 235
  236 + // 先保存模板ID,再关闭菜单弹窗,避免弹窗叠加
  237 + const templateId = currentTemplate.value.id;
  238 + closeTemplateMenu();
  239 +
  240 + setTimeout(() => {
236 uni.showModal({ 241 uni.showModal({
237 title: '确认删除', 242 title: '确认删除',
238 content: '确定要删除这个模板吗?删除后无法恢复', 243 content: '确定要删除这个模板吗?删除后无法恢复',
@@ -241,13 +246,11 @@ const handleDeleteTemplate = async () => { @@ -241,13 +246,11 @@ const handleDeleteTemplate = async () => {
241 if (res.confirm) { 246 if (res.confirm) {
242 try { 247 try {
243 uni.showLoading({ title: '删除中...' }); 248 uni.showLoading({ title: '删除中...' });
244 - await TemplatesApi.deleteCustTemplate(currentTemplate.value.id); 249 + await TemplatesApi.deleteCustTemplate(templateId);
245 uni.hideLoading(); 250 uni.hideLoading();
246 uni.showToast({ title: '删除成功', icon: 'success' }); 251 uni.showToast({ title: '删除成功', icon: 'success' });
247 - closeTemplateMenu();  
248 // 通知父组件刷新列表 252 // 通知父组件刷新列表
249 emit('refresh'); 253 emit('refresh');
250 -  
251 } catch (err) { 254 } catch (err) {
252 uni.hideLoading(); 255 uni.hideLoading();
253 console.error('删除失败:', err); 256 console.error('删除失败:', err);
@@ -256,6 +259,7 @@ const handleDeleteTemplate = async () => { @@ -256,6 +259,7 @@ const handleDeleteTemplate = async () => {
256 } 259 }
257 } 260 }
258 }); 261 });
  262 + }, 200);
259 }; 263 };
260 264
261 265
1 <template> 1 <template>
2 <view class="container"> 2 <view class="container">
  3 + <!-- 加载中占位 -->
  4 + <view class="loading-wrap" v-if="!isDataReady">
  5 + <text class="loading-text">加载中...</text>
  6 + </view>
  7 +
3 <!-- 我的分类 --> 8 <!-- 我的分类 -->
4 - <view class="section"> 9 + <view class="section" v-if="isDataReady">
5 <view class="section-header"> 10 <view class="section-header">
6 <text class="section-title">我的分类</text> 11 <text class="section-title">我的分类</text>
7 <text class="section-subtitle">点击隐藏分类</text> 12 <text class="section-subtitle">点击隐藏分类</text>
@@ -20,7 +25,7 @@ @@ -20,7 +25,7 @@
20 </view> 25 </view>
21 26
22 <!-- 已隐藏分类 --> 27 <!-- 已隐藏分类 -->
23 - <view class="section"> 28 + <view class="section" v-if="isDataReady">
24 <view class="section-header"> 29 <view class="section-header">
25 <text class="section-title">已隐藏分类</text> 30 <text class="section-title">已隐藏分类</text>
26 <text class="section-subtitle">点击恢复分类</text> 31 <text class="section-subtitle">点击恢复分类</text>
@@ -43,7 +48,6 @@ @@ -43,7 +48,6 @@
43 <!-- 底部按钮 --> 48 <!-- 底部按钮 -->
44 <view class="bottom-btns"> 49 <view class="bottom-btns">
45 <view class="edit-btn" @click="openEditPopup">编辑名称</view> 50 <view class="edit-btn" @click="openEditPopup">编辑名称</view>
46 - <!-- 优化:点击打开自定义新增弹窗 -->  
47 <view class="add-btn" @click="openAddPopup">+ 新增动作分类</view> 51 <view class="add-btn" @click="openAddPopup">+ 新增动作分类</view>
48 </view> 52 </view>
49 53
@@ -77,7 +81,7 @@ @@ -77,7 +81,7 @@
77 </view> 81 </view>
78 </up-popup> 82 </up-popup>
79 83
80 - <!-- ===================== 新增:自定义新增分类弹窗 ===================== --> 84 + <!-- 新增分类弹窗 -->
81 <up-popup :show="showAddPopup" mode="bottom" :mask-click="true" :safe-area-inset-bottom="true"> 85 <up-popup :show="showAddPopup" mode="bottom" :mask-click="true" :safe-area-inset-bottom="true">
82 <view class="add-popup-wrap"> 86 <view class="add-popup-wrap">
83 <!-- 头部 --> 87 <!-- 头部 -->
@@ -118,86 +122,92 @@ @@ -118,86 +122,92 @@
118 122
119 <script setup> 123 <script setup>
120 import { ref, computed } from 'vue'; 124 import { ref, computed } from 'vue';
121 - import { onLoad, onShow } from '@dcloudio/uni-app'; 125 + import { onShow } from '@dcloudio/uni-app';
122 import ExercisesApi from '@/sheep/api/motion/exercises'; 126 import ExercisesApi from '@/sheep/api/motion/exercises';
123 import { useActionStore } from '@/sheep/store/action'; 127 import { useActionStore } from '@/sheep/store/action';
124 128
125 - // 引入 Store  
126 const actionStore = useActionStore(); 129 const actionStore = useActionStore();
127 130
128 - // 从 Store 获取状态和方法  
129 const showCategories = computed(() => actionStore.showCategories); 131 const showCategories = computed(() => actionStore.showCategories);
130 const hiddenCategories = computed(() => actionStore.hiddenCategories); 132 const hiddenCategories = computed(() => actionStore.hiddenCategories);
  133 + const { hideCategory, showCategory, getloadCategories, reloadCategories } = actionStore;
131 134
132 - const { hideCategory, showCategory, getloadCategories } = actionStore; 135 + // 页面数据就绪标记(避免 allCategories 为空时渲染空白)
  136 + const isDataReady = ref(false);
133 137
134 - // 计算属性:获取隐藏分类的对象数组 138 + // 隐藏分类的对象数组(含名称)
135 const hiddenCategoryObjects = computed(() => { 139 const hiddenCategoryObjects = computed(() => {
  140 + if (!isDataReady.value) return [];
136 return actionStore.allCategories.filter(category => 141 return actionStore.allCategories.filter(category =>
137 hiddenCategories.value.includes(category.id) 142 hiddenCategories.value.includes(category.id)
138 ); 143 );
139 }); 144 });
140 145
141 - // 自定义分类列表(弹窗用) 146 + // Toast 提示常量
  147 + const TOAST = {
  148 + ADD_SUCCESS: '新增成功',
  149 + ADD_FAIL: '新增失败',
  150 + EDIT_SUCCESS: '修改成功',
  151 + EDIT_FAIL: '修改失败',
  152 + LOAD_FAIL: '加载失败',
  153 + };
  154 +
  155 + // 自定义分类列表(编辑弹窗用)
142 const customCategories = ref([]); 156 const customCategories = ref([]);
143 - // 当前要编辑的分类  
144 const currentEditCategory = ref(null); 157 const currentEditCategory = ref(null);
145 - // 控制自定义分类编辑弹窗  
146 const showEditPopup = ref(false); 158 const showEditPopup = ref(false);
147 - // 编辑弹窗输入值  
148 - const editInputValue = ref('');  
149 159
150 - // ===================== 新增:自定义新增弹窗变量 =====================  
151 - const showAddPopup = ref(false); // 控制新增弹窗显示  
152 - const addCategoryName = ref(''); // 新增分类名称 160 + // 新增分类弹窗变量
  161 + const showAddPopup = ref(false);
  162 + const addCategoryName = ref('');
153 163
154 - // 打开新增弹窗  
155 const openAddPopup = () => { 164 const openAddPopup = () => {
156 - addCategoryName.value = ''; // 清空输入框 165 + addCategoryName.value = '';
157 showAddPopup.value = true; 166 showAddPopup.value = true;
158 }; 167 };
159 - // 关闭新增弹窗 168 +
160 const closeAddPopup = () => { 169 const closeAddPopup = () => {
161 showAddPopup.value = false; 170 showAddPopup.value = false;
162 }; 171 };
163 172
164 - // ===================== 优化:执行新增分类(接口请求) =====================  
165 const doAddCategory = async () => { 173 const doAddCategory = async () => {
166 const name = addCategoryName.value.trim(); 174 const name = addCategoryName.value.trim();
167 if (!name) return; 175 if (!name) return;
168 176
169 try { 177 try {
170 await ExercisesApi.createCustomCategory(name); 178 await ExercisesApi.createCustomCategory(name);
171 - uni.showToast({ title: '新增成功', icon: 'success' }); 179 + uni.showToast({ title: TOAST.ADD_SUCCESS, icon: 'success' });
172 closeAddPopup(); 180 closeAddPopup();
173 - getloadCategories(); 181 + reloadCategories();
174 } catch (err) { 182 } catch (err) {
175 - uni.showToast({ title: '新增失败', icon: 'none' }); 183 + uni.showToast({ title: TOAST.ADD_FAIL, icon: 'none' });
176 console.error(err); 184 console.error(err);
177 } 185 }
178 }; 186 };
179 187
180 - // 打开自定义分类编辑弹窗  
181 - const openEditPopup = async () => {  
182 - try { 188 + // 获取自定义分类列表
  189 + const fetchCustomCategories = async () => {
183 const res = await ExercisesApi.getCustomCategories(); 190 const res = await ExercisesApi.getCustomCategories();
184 customCategories.value = res.data || []; 191 customCategories.value = res.data || [];
  192 + };
  193 +
  194 + // 打开编辑弹窗
  195 + const openEditPopup = async () => {
  196 + try {
  197 + await fetchCustomCategories();
185 showEditPopup.value = true; 198 showEditPopup.value = true;
186 } catch (err) { 199 } catch (err) {
187 console.error('加载自定义分类失败', err); 200 console.error('加载自定义分类失败', err);
188 - uni.showToast({ title: '加载失败', icon: 'none' }); 201 + uni.showToast({ title: TOAST.LOAD_FAIL, icon: 'none' });
189 } 202 }
190 }; 203 };
191 204
192 - // 关闭弹窗  
193 const closeEditPopup = () => { 205 const closeEditPopup = () => {
194 showEditPopup.value = false; 206 showEditPopup.value = false;
195 }; 207 };
196 208
197 - // 打开编辑弹窗  
198 const openEditModal = (item) => { 209 const openEditModal = (item) => {
199 currentEditCategory.value = item; 210 currentEditCategory.value = item;
200 - editInputValue.value = item.name;  
201 uni.showModal({ 211 uni.showModal({
202 title: '动作分类', 212 title: '动作分类',
203 editable: true, 213 editable: true,
@@ -207,27 +217,59 @@ @@ -207,27 +217,59 @@
207 if (res.confirm && res.content.trim()) { 217 if (res.confirm && res.content.trim()) {
208 try { 218 try {
209 await ExercisesApi.updateCustomCategory(item.id, res.content.trim()); 219 await ExercisesApi.updateCustomCategory(item.id, res.content.trim());
210 - uni.showToast({ title: '修改成功', icon: 'success' });  
211 - openEditPopup();  
212 - getloadCategories(); 220 + uni.showToast({ title: TOAST.EDIT_SUCCESS, icon: 'success' });
  221 + await fetchCustomCategories();
  222 + reloadCategories();
213 } catch (err) { 223 } catch (err) {
214 console.error('修改失败', err); 224 console.error('修改失败', err);
215 - uni.showToast({ title: '修改失败', icon: 'none' }); 225 + uni.showToast({ title: TOAST.EDIT_FAIL, icon: 'none' });
216 } 226 }
217 } 227 }
218 }, 228 },
219 }); 229 });
220 }; 230 };
221 231
222 - onShow(() => {  
223 - getloadCategories(); 232 + onShow(async () => {
  233 + await getloadCategories();
  234 + isDataReady.value = true;
224 }); 235 });
225 </script> 236 </script>
226 237
227 <style scoped lang="scss"> 238 <style scoped lang="scss">
  239 + // ========== 设计变量 ==========
  240 + $radius-sm: 10rpx;
  241 + $radius-md: 12rpx;
  242 + $radius-lg: 20rpx;
  243 + $radius-round: 36rpx;
  244 + $gap: 20rpx;
  245 + $color-primary: #ffd700;
  246 + $color-text: #333;
  247 + $color-text-secondary: #666;
  248 + $color-text-light: #999;
  249 + $color-border: #eee;
  250 + $color-bg: #f7f7f7;
  251 + $color-bg-light: #f9f9f9;
  252 + $transition-base: 0.2s ease;
  253 +
228 .container { 254 .container {
229 min-height: 100vh; 255 min-height: 100vh;
230 background-color: #fff; 256 background-color: #fff;
  257 + // 为底部固定栏留出空间
  258 + padding-bottom: 120rpx;
  259 + box-sizing: border-box;
  260 + }
  261 +
  262 + // 加载中占位
  263 + .loading-wrap {
  264 + display: flex;
  265 + justify-content: center;
  266 + align-items: center;
  267 + min-height: 60vh;
  268 + }
  269 +
  270 + .loading-text {
  271 + font-size: 28rpx;
  272 + color: $color-text-light;
231 } 273 }
232 274
233 .section { 275 .section {
@@ -243,20 +285,22 @@ @@ -243,20 +285,22 @@
243 285
244 .section-title { 286 .section-title {
245 font-size: 28rpx; 287 font-size: 28rpx;
246 - color: #333; 288 + color: $color-text;
247 font-weight: bold; 289 font-weight: bold;
248 } 290 }
249 291
250 .section-subtitle { 292 .section-subtitle {
251 font-size: 24rpx; 293 font-size: 24rpx;
252 - color: #999; 294 + color: $color-text-light;
253 margin-left: 16rpx; 295 margin-left: 16rpx;
254 } 296 }
255 297
256 - .category-grid { 298 + // 分类网格(展示 + 隐藏共用)
  299 + .category-grid,
  300 + .hidden-grid {
257 display: grid; 301 display: grid;
258 - grid-template-columns: repeat(4, 1fr);  
259 - gap: 20rpx; 302 + grid-template-columns: repeat(4, minmax(0, 1fr));
  303 + gap: $gap;
260 } 304 }
261 305
262 .category-item { 306 .category-item {
@@ -264,34 +308,36 @@ @@ -264,34 +308,36 @@
264 justify-content: center; 308 justify-content: center;
265 align-items: center; 309 align-items: center;
266 padding: 20rpx 16rpx; 310 padding: 20rpx 16rpx;
  311 + padding-right: 28rpx;
267 background-color: #fff; 312 background-color: #fff;
268 - border: 1rpx solid #eee;  
269 - border-radius: 10rpx; 313 + border: 1rpx solid $color-border;
  314 + border-radius: $radius-sm;
270 position: relative; 315 position: relative;
271 - transition: all 0.2s ease;  
272 - padding-right: 28rpx;  
273 - } 316 + transition: all $transition-base;
274 317
275 - .category-item:active { 318 + &:active {
276 transform: scale(0.96); 319 transform: scale(0.96);
277 background-color: #fafafa; 320 background-color: #fafafa;
278 } 321 }
  322 + }
279 323
280 .category-text { 324 .category-text {
281 font-size: 24rpx; 325 font-size: 24rpx;
282 - color: #333; 326 + color: $color-text;
283 text-align: center; 327 text-align: center;
  328 + overflow: hidden;
  329 + text-overflow: ellipsis;
  330 + white-space: nowrap;
284 } 331 }
285 332
286 .delete-icon { 333 .delete-icon {
287 position: absolute; 334 position: absolute;
288 right: 4rpx; 335 right: 4rpx;
289 top: 4rpx; 336 top: 4rpx;
290 - transform: none;  
291 - z-index: 99; 337 + z-index: 1;
292 padding: 4rpx; 338 padding: 4rpx;
293 opacity: 0.7; 339 opacity: 0.7;
294 - transition: all 0.2s ease; 340 + transition: all $transition-base;
295 } 341 }
296 342
297 .category-item:active .delete-icon { 343 .category-item:active .delete-icon {
@@ -299,26 +345,21 @@ @@ -299,26 +345,21 @@
299 color: #ff4d4f; 345 color: #ff4d4f;
300 } 346 }
301 347
302 - .hidden-grid {  
303 - display: grid;  
304 - grid-template-columns: repeat(4, 1fr);  
305 - gap: 20rpx;  
306 - }  
307 -  
308 .hidden-item { 348 .hidden-item {
309 display: flex; 349 display: flex;
310 justify-content: center; 350 justify-content: center;
311 align-items: center; 351 align-items: center;
312 padding: 20rpx 16rpx; 352 padding: 20rpx 16rpx;
313 - background: #f9f9f9;  
314 - border: 1rpx solid #eee;  
315 - border-radius: 10rpx;  
316 - transition: all 0.2s ease;  
317 - } 353 + background: $color-bg-light;
  354 + border: 1rpx solid $color-border;
  355 + border-radius: $radius-sm;
  356 + transition: all $transition-base;
318 357
319 - .hidden-item:active { 358 + &:active {
320 transform: scale(0.96); 359 transform: scale(0.96);
321 - background-color: #f2f2f2; 360 + background-color: #e8e8e8;
  361 + border-color: #ddd;
  362 + }
322 } 363 }
323 364
324 .hidden-empty { 365 .hidden-empty {
@@ -329,10 +370,13 @@ @@ -329,10 +370,13 @@
329 370
330 .hidden-text { 371 .hidden-text {
331 font-size: 24rpx; 372 font-size: 24rpx;
332 - color: #999; 373 + color: $color-text-light;
  374 + overflow: hidden;
  375 + text-overflow: ellipsis;
  376 + white-space: nowrap;
333 } 377 }
334 378
335 - /* 底部按钮 */ 379 + // 底部按钮栏
336 .bottom-btns { 380 .bottom-btns {
337 display: flex; 381 display: flex;
338 justify-content: space-between; 382 justify-content: space-between;
@@ -344,9 +388,8 @@ @@ -344,9 +388,8 @@
344 background-color: rgba(255, 255, 255, 0.95); 388 background-color: rgba(255, 255, 255, 0.95);
345 backdrop-filter: blur(10px); 389 backdrop-filter: blur(10px);
346 z-index: 10; 390 z-index: 10;
347 - padding: 20rpx 30rpx; 391 + padding: 20rpx 20rpx calc(20rpx + env(safe-area-inset-bottom));
348 box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); 392 box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
349 - padding: 20rpx;  
350 box-sizing: border-box; 393 box-sizing: border-box;
351 } 394 }
352 395
@@ -355,11 +398,11 @@ @@ -355,11 +398,11 @@
355 height: 72rpx; 398 height: 72rpx;
356 line-height: 72rpx; 399 line-height: 72rpx;
357 font-size: 26rpx; 400 font-size: 26rpx;
358 - color: #666; 401 + color: $color-text-secondary;
359 background-color: #f5f5f5; 402 background-color: #f5f5f5;
360 - border-radius: 36rpx; 403 + border-radius: $radius-round;
361 text-align: center; 404 text-align: center;
362 - transition: all 0.2s; 405 + transition: all $transition-base;
363 406
364 &:active { 407 &:active {
365 background-color: #e0e0e0; 408 background-color: #e0e0e0;
@@ -371,16 +414,16 @@ @@ -371,16 +414,16 @@
371 flex: 1; 414 flex: 1;
372 height: 72rpx; 415 height: 72rpx;
373 line-height: 72rpx; 416 line-height: 72rpx;
374 - background: linear-gradient(135deg, #ffd700 0%, #ffcc00 100%) !important;  
375 - color: #333 !important; 417 + background: linear-gradient(135deg, $color-primary 0%, #ffcc00 100%);
  418 + color: $color-text;
376 font-size: 28rpx; 419 font-size: 28rpx;
377 font-weight: 500; 420 font-weight: 500;
378 - border-radius: 36rpx;  
379 - margin-left: 20rpx; 421 + border-radius: $radius-round;
  422 + margin-left: $gap;
380 border: none; 423 border: none;
381 box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3); 424 box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3);
382 text-align: center; 425 text-align: center;
383 - transition: all 0.2s; 426 + transition: all $transition-base;
384 427
385 &:active { 428 &:active {
386 transform: scale(0.98); 429 transform: scale(0.98);
@@ -388,10 +431,10 @@ @@ -388,10 +431,10 @@
388 } 431 }
389 } 432 }
390 433
391 - /* 编辑弹窗 */ 434 + // 编辑分类弹窗
392 .popup-wrap { 435 .popup-wrap {
393 - background: #f7f7f7;  
394 - border-radius: 20rpx 20rpx 0 0; 436 + background: $color-bg;
  437 + border-radius: $radius-lg $radius-lg 0 0;
395 padding: 0 30rpx; 438 padding: 0 30rpx;
396 max-height: 70vh; 439 max-height: 70vh;
397 display: flex; 440 display: flex;
@@ -408,12 +451,12 @@ @@ -408,12 +451,12 @@
408 .popup-title { 451 .popup-title {
409 font-size: 36rpx; 452 font-size: 36rpx;
410 font-weight: 500; 453 font-weight: 500;
411 - color: #333; 454 + color: $color-text;
412 } 455 }
413 456
414 .popup-tip { 457 .popup-tip {
415 font-size: 28rpx; 458 font-size: 28rpx;
416 - color: #666; 459 + color: $color-text-secondary;
417 margin-bottom: 30rpx; 460 margin-bottom: 30rpx;
418 } 461 }
419 462
@@ -427,22 +470,33 @@ @@ -427,22 +470,33 @@
427 justify-content: space-between; 470 justify-content: space-between;
428 align-items: center; 471 align-items: center;
429 background: #fff; 472 background: #fff;
430 - border-radius: 12rpx; 473 + border-radius: $radius-md;
431 padding: 30rpx; 474 padding: 30rpx;
432 - margin-bottom: 20rpx; 475 + margin-bottom: $gap;
  476 + transition: all $transition-base;
  477 +
  478 + &:active {
  479 + background-color: #f5f5f5;
  480 + transform: scale(0.98);
  481 + }
433 } 482 }
434 483
435 .item-name { 484 .item-name {
436 font-size: 32rpx; 485 font-size: 32rpx;
437 - color: #333; 486 + color: $color-text;
  487 + flex: 1;
  488 + overflow: hidden;
  489 + text-overflow: ellipsis;
  490 + white-space: nowrap;
  491 + margin-right: 16rpx;
438 } 492 }
439 493
440 - /* ===================== 新增:自定义新增弹窗样式 ===================== */ 494 + // 新增分类弹窗
441 .add-popup-wrap { 495 .add-popup-wrap {
442 background: #fff; 496 background: #fff;
443 - border-radius: 20rpx 20rpx 0 0; 497 + border-radius: $radius-lg $radius-lg 0 0;
444 padding: 30rpx; 498 padding: 30rpx;
445 - padding-bottom: calc(var(--safe-area-inset-bottom) + 30rpx); 499 + padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
446 } 500 }
447 501
448 .add-popup-header { 502 .add-popup-header {
@@ -455,26 +509,27 @@ @@ -455,26 +509,27 @@
455 .add-popup-title { 509 .add-popup-title {
456 font-size: 38rpx; 510 font-size: 38rpx;
457 font-weight: bold; 511 font-weight: bold;
458 - color: #333; 512 + color: $color-text;
459 } 513 }
460 514
461 .add-input-box { 515 .add-input-box {
462 - background: #f7f7f7;  
463 - border-radius: 12rpx; 516 + background: $color-bg;
  517 + border-radius: $radius-md;
464 padding: 24rpx 20rpx; 518 padding: 24rpx 20rpx;
465 - margin-bottom: 20rpx; 519 + margin-bottom: $gap;
466 } 520 }
467 521
468 .add-input { 522 .add-input {
469 font-size: 32rpx; 523 font-size: 32rpx;
470 - color: #333; 524 + color: $color-text;
471 height: 48rpx; 525 height: 48rpx;
472 line-height: 48rpx; 526 line-height: 48rpx;
  527 + width: 100%;
473 } 528 }
474 529
475 .add-tip { 530 .add-tip {
476 font-size: 24rpx; 531 font-size: 24rpx;
477 - color: #999; 532 + color: $color-text-light;
478 margin-bottom: 40rpx; 533 margin-bottom: 40rpx;
479 } 534 }
480 535
@@ -487,14 +542,14 @@ @@ -487,14 +542,14 @@
487 height: 88rpx; 542 height: 88rpx;
488 line-height: 88rpx; 543 line-height: 88rpx;
489 font-size: 32rpx; 544 font-size: 32rpx;
490 - border-radius: 12rpx;  
491 - background-color: #ffd700; 545 + border-radius: $radius-md;
  546 + background-color: $color-primary;
492 color: #000; 547 color: #000;
493 border: none; 548 border: none;
494 549
495 &[disabled] { 550 &[disabled] {
496 background-color: #e0e0e0; 551 background-color: #e0e0e0;
497 - color: #999; 552 + color: $color-text-light;
498 } 553 }
499 } 554 }
500 </style> 555 </style>
1 <template> 1 <template>
2 - <view class="page-wrap">  
3 - <!-- 顶部搜索 -->  
4 - <view class="add-btn-wrapper" @click.stop="addExercise">  
5 - <uni-icons type="plus" size="35" color="#333"></uni-icons>  
6 - <view class="floating-menu" v-show="replaceAddshow" @click.stop>  
7 - <view class="menu-item" @click="addnewmotion">  
8 - <uni-icons type="plus" size="24" color="#333"></uni-icons>  
9 - <text class="menu-text">新增动作</text>  
10 - </view>  
11 - </view>  
12 - </view>  
13 - <view class="search-bar">  
14 - <!-- <view class="search-input-wrapper">  
15 - <uni-icons type="search" size="18" color="#999"></uni-icons>  
16 - <input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" />  
17 - </view> -->  
18 - </view> 2 + <view class="page-wrap" :style="{ paddingTop: PageHeight + 'px' }">
19 3
20 <!-- 左右主体布局 --> 4 <!-- 左右主体布局 -->
21 <view class="layout-container"> 5 <view class="layout-container">
@@ -29,18 +13,44 @@ @@ -29,18 +13,44 @@
29 13
30 <!-- 右侧动作列表 --> 14 <!-- 右侧动作列表 -->
31 <scroll-view scroll-y class="right-content"> 15 <scroll-view scroll-y class="right-content">
  16 + <!-- 初始引导:未选择分类 -->
  17 + <view class="content-tip init-guide" v-if="!activeNav">
  18 + <text class="tip-text">请选择左侧分类查看动作</text>
  19 + <text class="tip-subtext">选择分类后将展示该分类下的训练动作</text>
  20 + </view>
  21 +
  22 + <!-- 加载中 -->
  23 + <view class="content-tip loading-state" v-else-if="loading">
  24 + <text class="tip-text">加载中...</text>
  25 + </view>
  26 +
  27 + <!-- 加载失败 -->
  28 + <view class="content-tip error-state" v-else-if="loadError">
  29 + <text class="tip-text">加载失败,请重试</text>
  30 + <view class="retry-btn" @click="retryLoad">点击重试</view>
  31 + </view>
  32 +
  33 + <!-- 正常内容 -->
  34 + <template v-else>
32 <!-- 部位筛选标签 --> 35 <!-- 部位筛选标签 -->
33 - <view class="tip" v-if="motionPart.length > 0">  
34 - <view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view>  
35 - <view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }"  
36 - @click="handlePartClick(item.id)"> 36 + <scroll-view scroll-x show-scrollbar="false" class="part-filter" v-if="motionPart.length > 0">
  37 + <view class="part-filter-inner">
  38 + <view class="filter-item" :class="{ active: activeMotionPart === '' }" @click="handlePartClick('')">全部
  39 + </view>
  40 + <view class="filter-item" v-for="item in motionPart" :key="item.id"
  41 + :class="{ active: activeMotionPart === item.id }" @click="handlePartClick(item.id)">
37 {{ item.name }} 42 {{ item.name }}
38 </view> 43 </view>
39 </view> 44 </view>
  45 + </scroll-view>
40 46
41 - <view class="exercise-grid">  
42 - <view class="content" v-if="exercises.length > 0">  
43 - <view class="equipment-list"> 47 + <!-- 部位筛选为空提示 -->
  48 + <view class="content-tip part-empty-tip" v-else>
  49 + <text class="tip-text">该分类下暂无可筛选部位</text>
  50 + </view>
  51 +
  52 + <!-- 动作列表 -->
  53 + <view class="equipment-list" v-if="exercises.length > 0">
44 <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId"> 54 <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
45 <view class="equipment-name">{{ item.equipmentName }}</view> 55 <view class="equipment-name">{{ item.equipmentName }}</view>
46 <view class="action-list"> 56 <view class="action-list">
@@ -52,23 +62,27 @@ @@ -52,23 +62,27 @@
52 </view> 62 </view>
53 </view> 63 </view>
54 </view> 64 </view>
  65 +
  66 + <!-- 无动作数据 -->
  67 + <view class="content-tip empty-state" v-else>
  68 + <text class="tip-text">该分类下暂无动作</text>
  69 + <text class="tip-subtext">可尝试切换分类或部位查看其他动作</text>
55 </view> 70 </view>
56 - <view class="empty-state" v-else>  
57 - <text class="empty-text">暂无动作数据</text>  
58 - </view>  
59 - </view> 71 + </template>
60 </scroll-view> 72 </scroll-view>
61 </view> 73 </view>
62 74
63 - <!-- 底部横向已选标签栏 --> 75 + <!-- 底部已选标签栏 -->
64 <scroll-view class="selected-bar" scroll-x show-scrollbar="false" v-if="selectedActions.length > 0"> 76 <scroll-view class="selected-bar" scroll-x show-scrollbar="false" v-if="selectedActions.length > 0">
65 - <view class="selected-item" v-for="(item, index) in selectedActions" :key="index"> 77 + <view class="selected-bar-inner">
  78 + <view class="selected-item" v-for="item in selectedActions" :key="item.id">
66 <text class="name">{{ item.name }}</text> 79 <text class="name">{{ item.name }}</text>
67 <uni-icons type="close" size="16" color="#fff" @click="removeAction(item)"></uni-icons> 80 <uni-icons type="close" size="16" color="#fff" @click="removeAction(item)"></uni-icons>
68 </view> 81 </view>
  82 + </view>
69 </scroll-view> 83 </scroll-view>
70 84
71 - <!-- 页面底部按钮 --> 85 + <!-- 底部操作按钮 -->
72 <view class="bottom-btn-bar"> 86 <view class="bottom-btn-bar">
73 <view class="btn cancel-btn" @click="backPage">取消</view> 87 <view class="btn cancel-btn" @click="backPage">取消</view>
74 <view class="btn confirm-btn" :class="{ disabled: selectedActions.length < 2 }" @click="combineActions"> 88 <view class="btn confirm-btn" :class="{ disabled: selectedActions.length < 2 }" @click="combineActions">
@@ -76,29 +90,17 @@ @@ -76,29 +90,17 @@
76 </view> 90 </view>
77 </view> 91 </view>
78 92
79 - <!-- 底部弹出命名弹窗 --> 93 + <!-- 命名弹窗 -->
80 <up-popup v-model:show="showNamePopup" mode="bottom" round> 94 <up-popup v-model:show="showNamePopup" mode="bottom" round>
81 <view class="popup-content" @click.stop> 95 <view class="popup-content" @click.stop>
82 -  
83 <view class="popup-header"> 96 <view class="popup-header">
84 - <!-- <text class="close-btn" @click="closePopup">x</text> -->  
85 <up-icon name="close" color="#333" size="25" @click="closePopup"></up-icon> 97 <up-icon name="close" color="#333" size="25" @click="closePopup"></up-icon>
86 <text class="popup-title">命名超级组</text> 98 <text class="popup-title">命名超级组</text>
87 <text class="save-btn" @click="saveCombination">保存</text> 99 <text class="save-btn" @click="saveCombination">保存</text>
88 </view> 100 </view>
89 <view class="input-wrapper"> 101 <view class="input-wrapper">
90 <textarea v-model="combinationName" class="name-input" placeholder="请输入超级组名称" maxlength="20" focus /> 102 <textarea v-model="combinationName" class="name-input" placeholder="请输入超级组名称" maxlength="20" focus />
91 - <!-- <text class="hint">例如:胸肩超级组、背部组合训练</text> -->  
92 - </view>  
93 -  
94 - <!-- <view class="preview-section">  
95 - <text class="preview-title">已选动作 ({{ selectedActions.length }})</text>  
96 - <scroll-view scroll-x class="preview-scroll">  
97 - <view class="preview-tag" v-for="item in selectedActions" :key="item.id">  
98 - {{ item.name }}  
99 </view> 103 </view>
100 - </scroll-view>  
101 - </view> -->  
102 </view> 104 </view>
103 </up-popup> 105 </up-popup>
104 </view> 106 </view>
@@ -106,48 +108,61 @@ @@ -106,48 +108,61 @@
106 108
107 <script setup> 109 <script setup>
108 import { ref, computed } from 'vue' 110 import { ref, computed } from 'vue'
109 -import { onLoad, onShow } from '@dcloudio/uni-app'; 111 +import { onLoad } from '@dcloudio/uni-app'
110 import { useActionStore } from '@/sheep/store/action' 112 import { useActionStore } from '@/sheep/store/action'
111 import ExercisesApi from '@/sheep/api/motion/exercises' 113 import ExercisesApi from '@/sheep/api/motion/exercises'
112 import SupersetsApi from '@/sheep/api/motion/supersets' 114 import SupersetsApi from '@/sheep/api/motion/supersets'
113 - 115 +import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea';
  116 +const menuButtonHeight = ref(getMenuButtonHeight());
  117 +const topSafeArea = ref(getTopSafeArea());
  118 +const PageHeight = ref(topSafeArea.value);
  119 +// #ifdef MP-WEIXIN
  120 +PageHeight.value = menuButtonHeight.value + topSafeArea.value;
  121 +// #endif
114 const actionStore = useActionStore() 122 const actionStore = useActionStore()
115 123
116 -// 搜索  
117 -const searchValue = ref('') 124 +// ==================== 状态变量 ====================
118 125
119 -// 左侧分类、部位筛选 126 +// 左侧分类
120 const navItems = ref([]) 127 const navItems = ref([])
121 const activeNav = ref(null) 128 const activeNav = ref(null)
  129 +
  130 +// 部位筛选
122 const motionPart = ref([]) 131 const motionPart = ref([])
123 const activeMotionPart = ref('') 132 const activeMotionPart = ref('')
124 133
125 -// 原始全量动作、当前渲染动作 134 +// 动作数据
126 const exercises = ref([]) 135 const exercises = ref([])
127 -const allExercises = ref([]) 136 +const loading = ref(false)
  137 +const loadError = ref(false)
128 138
129 // 选中动作 139 // 选中动作
130 const selectedActions = ref([]) 140 const selectedActions = ref([])
131 const selectedIds = computed(() => new Set(selectedActions.value.map(item => item.id))) 141 const selectedIds = computed(() => new Set(selectedActions.value.map(item => item.id)))
132 142
133 -// 命名弹窗控制 143 +// 命名弹窗
134 const showNamePopup = ref(false) 144 const showNamePopup = ref(false)
135 const combinationName = ref('') 145 const combinationName = ref('')
136 146
137 -const replaceAddshow = ref(false); 147 +// ==================== 生命周期 ====================
138 148
139 -// 页面初始化(onLoad 页面生命周期)  
140 onLoad(async () => { 149 onLoad(async () => {
141 selectedActions.value = [] 150 selectedActions.value = []
142 combinationName.value = '' 151 combinationName.value = ''
143 await loadCategories() 152 await loadCategories()
144 }) 153 })
145 154
146 -// 加载全部分类 155 +// ==================== 数据加载 ====================
  156 +
  157 +/** 加载全部分类 */
147 const loadCategories = async () => { 158 const loadCategories = async () => {
148 try { 159 try {
149 await actionStore.getloadCategories() 160 await actionStore.getloadCategories()
150 - navItems.value = actionStore.showCategories 161 + // 过滤掉虚拟分类(如「超级组」id='super'),其 id 非数字,无法用于加载动作
  162 + navItems.value = actionStore.showCategories.filter(item => {
  163 + const id = item.id
  164 + return typeof id === 'number' || (typeof id === 'string' && /^\d+$/.test(id))
  165 + })
151 if (navItems.value.length > 0) { 166 if (navItems.value.length > 0) {
152 switchNav(navItems.value[0].id) 167 switchNav(navItems.value[0].id)
153 } 168 }
@@ -156,77 +171,81 @@ const loadCategories = async () => { @@ -156,77 +171,81 @@ const loadCategories = async () => {
156 } 171 }
157 } 172 }
158 173
159 -const addExercise = () => {  
160 - replaceAddshow.value = !replaceAddshow.value;  
161 -};  
162 -  
163 -const addnewmotion = () => {  
164 - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });  
165 -};  
166 -  
167 -// 切换左侧大分类 174 +/** 切换左侧大分类 */
168 const switchNav = async (id) => { 175 const switchNav = async (id) => {
169 if (activeNav.value === id) return 176 if (activeNav.value === id) return
170 activeNav.value = id 177 activeNav.value = id
171 activeMotionPart.value = '' 178 activeMotionPart.value = ''
172 - searchValue.value = '' 179 + loadError.value = false
173 await loadExercises(id) 180 await loadExercises(id)
174 } 181 }
175 182
176 -// 根据分类ID加载动作+部位 183 +/** 根据分类ID加载动作和部位 */
177 const loadExercises = async (categoriesId) => { 184 const loadExercises = async (categoriesId) => {
  185 + loading.value = true
  186 + loadError.value = false
178 try { 187 try {
179 - const partRes = await ExercisesApi.getMotionPart(categoriesId) 188 + const [partRes, exerciseRes] = await Promise.all([
  189 + ExercisesApi.getMotionPart(categoriesId),
  190 + ExercisesApi.getexercises({ categoriesId })
  191 + ])
180 motionPart.value = partRes.data || [] 192 motionPart.value = partRes.data || []
181 -  
182 - const res = await ExercisesApi.getexercises({ categoriesId })  
183 - exercises.value = res.data || []  
184 - allExercises.value = [...exercises.value] 193 + exercises.value = exerciseRes.data || []
185 } catch (err) { 194 } catch (err) {
186 console.error('加载动作失败', err) 195 console.error('加载动作失败', err)
  196 + loadError.value = true
  197 + motionPart.value = []
  198 + exercises.value = []
  199 + } finally {
  200 + loading.value = false
187 } 201 }
188 } 202 }
189 203
190 -// 子部位筛选 204 +/** 按子部位筛选动作 */
191 const handlePartClick = async (id) => { 205 const handlePartClick = async (id) => {
192 activeMotionPart.value = id 206 activeMotionPart.value = id
  207 + loading.value = true
  208 + loadError.value = false
  209 + try {
193 const res = await ExercisesApi.getexercises({ 210 const res = await ExercisesApi.getexercises({
194 categoriesId: activeNav.value, 211 categoriesId: activeNav.value,
195 subCategoriesId: id 212 subCategoriesId: id
196 }) 213 })
197 exercises.value = res.data || [] 214 exercises.value = res.data || []
  215 + } catch (err) {
  216 + console.error('筛选动作失败', err)
  217 + uni.showToast({ title: '加载失败', icon: 'none' })
  218 + loadError.value = true
  219 + } finally {
  220 + loading.value = false
  221 + }
198 } 222 }
199 223
200 -// 搜索过滤  
201 -const onSearch = () => {  
202 - const keyword = searchValue.value.trim().toLowerCase()  
203 - if (!keyword) {  
204 - exercises.value = [...allExercises.value]  
205 - return 224 +/** 重试加载 */
  225 +const retryLoad = () => {
  226 + if (activeNav.value) {
  227 + loadExercises(activeNav.value)
206 } 228 }
207 - exercises.value = allExercises.value.filter(item =>  
208 - item.exercises?.some(e => e.name.toLowerCase().includes(keyword))  
209 - )  
210 } 229 }
211 230
212 -// 点击选中/取消单个动作 231 +// ==================== 操作交互 ====================
  232 +
  233 +/** 选中/取消单个动作 */
213 const toggleAction = (action) => { 234 const toggleAction = (action) => {
214 - console.log('点击的动作', action, '选中前数组', selectedActions.value)  
215 const exist = selectedIds.value.has(action.id) 235 const exist = selectedIds.value.has(action.id)
216 if (exist) { 236 if (exist) {
217 selectedActions.value = selectedActions.value.filter(i => i.id !== action.id) 237 selectedActions.value = selectedActions.value.filter(i => i.id !== action.id)
218 } else { 238 } else {
219 - selectedActions.value.push({ ...action }) 239 + selectedActions.value = [...selectedActions.value, { ...action }]
220 } 240 }
221 - console.log('选中后', selectedActions.value)  
222 } 241 }
223 242
224 -// 底部标签移除动作 243 +/** 移除已选动作 */
225 const removeAction = (action) => { 244 const removeAction = (action) => {
226 selectedActions.value = selectedActions.value.filter(i => i.id !== action.id) 245 selectedActions.value = selectedActions.value.filter(i => i.id !== action.id)
227 } 246 }
228 247
229 -// 打开命名弹窗 248 +/** 打开命名弹窗 */
230 const combineActions = () => { 249 const combineActions = () => {
231 if (selectedActions.value.length < 2) { 250 if (selectedActions.value.length < 2) {
232 uni.showToast({ title: '至少选择2个动作', icon: 'none' }) 251 uni.showToast({ title: '至少选择2个动作', icon: 'none' })
@@ -236,12 +255,12 @@ const combineActions = () => { @@ -236,12 +255,12 @@ const combineActions = () => {
236 showNamePopup.value = true 255 showNamePopup.value = true
237 } 256 }
238 257
239 -// 关闭弹窗 258 +/** 关闭弹窗 */
240 const closePopup = () => { 259 const closePopup = () => {
241 showNamePopup.value = false 260 showNamePopup.value = false
242 } 261 }
243 262
244 -// 【原有创建超级组接口不动】保存超级组 263 +/** 保存超级组 */
245 const saveCombination = async () => { 264 const saveCombination = async () => {
246 const name = combinationName.value.trim() 265 const name = combinationName.value.trim()
247 if (!name) { 266 if (!name) {
@@ -258,7 +277,6 @@ const saveCombination = async () => { @@ -258,7 +277,6 @@ const saveCombination = async () => {
258 uni.showToast({ title: '创建成功', icon: 'success' }) 277 uni.showToast({ title: '创建成功', icon: 'success' })
259 selectedActions.value = [] 278 selectedActions.value = []
260 closePopup() 279 closePopup()
261 - // 创建成功返回上一页  
262 setTimeout(() => uni.navigateBack(), 800) 280 setTimeout(() => uni.navigateBack(), 800)
263 } else { 281 } else {
264 uni.showToast({ title: res.msg || '创建失败', icon: 'none' }) 282 uni.showToast({ title: res.msg || '创建失败', icon: 'none' })
@@ -268,99 +286,83 @@ const saveCombination = async () => { @@ -268,99 +286,83 @@ const saveCombination = async () => {
268 } 286 }
269 } 287 }
270 288
271 -// 返回上一页 289 +/** 返回上一页 */
272 const backPage = () => { 290 const backPage = () => {
273 uni.navigateBack() 291 uni.navigateBack()
274 } 292 }
275 </script> 293 </script>
276 294
277 <style lang="scss" scoped> 295 <style lang="scss" scoped>
278 -page {  
279 - background-color: #f5f5f5;  
280 -} 296 +// ==================== SCSS 变量 ====================
  297 +$color-primary: #f8d714;
  298 +$color-bg: #f5f5f5;
  299 +$color-bg-nav: #fafafa;
  300 +$color-text-primary: #333;
  301 +$color-text-secondary: #666;
  302 +$color-text-muted: #999;
  303 +$color-white: #fff;
  304 +$color-dark-bar: #2c2c2e;
  305 +$color-dark-tag: #444;
  306 +$color-border: #eee;
  307 +$color-disabled-bg: #eee;
  308 +$color-disabled-text: #bbb;
  309 +
  310 +$z-fixed: 100;
  311 +
  312 +$radius-sm: 12rpx;
  313 +$radius-md: 16rpx;
  314 +$radius-lg: 24rpx;
  315 +$radius-round: 40rpx;
  316 +
  317 +$spacing-xs: 8rpx;
  318 +$spacing-sm: 16rpx;
  319 +$spacing-md: 20rpx;
  320 +$spacing-lg: 30rpx;
  321 +
  322 +$font-xs: 24rpx;
  323 +$font-sm: 26rpx;
  324 +$font-md: 28rpx;
  325 +$font-lg: 30rpx;
  326 +$font-xl: 32rpx;
  327 +
  328 +
281 329
282 .page-wrap { 330 .page-wrap {
283 width: 100%; 331 width: 100%;
284 - min-height: 100vh; 332 + height: 100vh;
  333 + // #ifdef MP-WEIXIN
  334 + height: 88vh;
  335 + // #endif
285 display: flex; 336 display: flex;
286 flex-direction: column; 337 flex-direction: column;
  338 + overflow: hidden;
287 } 339 }
288 340
289 -.add-btn-wrapper {  
290 - position: fixed;  
291 - left: 28rpx;  
292 - top: calc(var(--status-bar-height) + 12rpx);  
293 - z-index: 9999;  
294 - width: 50rpx;  
295 - height: 50rpx;  
296 -}  
297 -  
298 -.search-bar {  
299 - padding-top: var(--status-bar-height);  
300 - height: 88rpx;  
301 - background: #fff;  
302 -}  
303 -  
304 -.floating-menu {  
305 - position: absolute;  
306 - top: 90rpx;  
307 - left: 20rpx;  
308 - background: #fff;  
309 - border-radius: 12rpx;  
310 - box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);  
311 - padding: 20rpx;  
312 - z-index: 9999;  
313 - width: 260rpx;  
314 -}  
315 -  
316 -.menu-item {  
317 - display: flex;  
318 - align-items: center;  
319 - gap: 16rpx;  
320 -}  
321 -  
322 -.menu-text {  
323 - box-sizing: border-box;  
324 - display: flex;  
325 - font-size: 28rpx;  
326 - // margin-left: 200rpx;  
327 -}  
328 -  
329 -  
330 -// .search-bar {  
331 -// padding: 20rpx 30rpx;  
332 -// background: #fff;  
333 -  
334 -// .search-input {  
335 -// width: 100%;  
336 -// height: 70rpx;  
337 -// border-radius: 35rpx;  
338 -// background: #f7f7f7;  
339 -// padding: 0 24rpx;  
340 -// font-size: 28rpx;  
341 -// border: 1rpx solid #eee;  
342 -// }  
343 -// }  
344 - 341 +// ==================== 主体布局 ====================
345 .layout-container { 342 .layout-container {
346 - flex: 1; 343 +
  344 + min-height: 0;
347 display: flex; 345 display: flex;
348 overflow: hidden; 346 overflow: hidden;
349 - padding-bottom: 130rpx; 347 + padding-bottom: calc(100rpx + 30rpx); // 底部按钮栏 + 间距
  348 +}
350 349
351 - .left-nav {  
352 - width: 150rpx;  
353 - background: #fafafa;  
354 - border-right: 1rpx solid #eee; 350 +// 左侧导航
  351 +.left-nav {
  352 + width: 180rpx;
  353 + background: $color-bg-nav;
  354 + border-right: 1rpx solid $color-border;
355 355
356 .nav-item { 356 .nav-item {
357 - padding: 30rpx 20rpx;  
358 - font-size: 26rpx;  
359 - color: #666; 357 + padding: $spacing-lg $spacing-sm;
  358 + font-size: $font-sm;
  359 + color: $color-text-secondary;
360 position: relative; 360 position: relative;
  361 + white-space: nowrap;
  362 + text-align: center;
361 363
362 &.active { 364 &.active {
363 - background: #fff; 365 + background: $color-white;
364 color: #000; 366 color: #000;
365 font-weight: 500; 367 font-weight: 500;
366 368
@@ -372,230 +374,263 @@ page { @@ -372,230 +374,263 @@ page {
372 transform: translateY(-50%); 374 transform: translateY(-50%);
373 width: 6rpx; 375 width: 6rpx;
374 height: 36rpx; 376 height: 36rpx;
375 - background: #f8d714;  
376 - } 377 + background: $color-primary;
377 } 378 }
378 } 379 }
379 } 380 }
  381 +}
380 382
381 - .right-content { 383 +// 右侧内容
  384 +.right-content {
382 flex: 1; 385 flex: 1;
383 - padding: 20rpx; 386 + padding: $spacing-md;
  387 + padding-top: $spacing-lg;
384 388
385 - .tip { 389 + // 通用内容提示块
  390 + .content-tip {
386 display: flex; 391 display: flex;
387 - flex-wrap: wrap; 392 + flex-direction: column;
  393 + align-items: center;
  394 + justify-content: center;
  395 + padding: 80rpx $spacing-lg;
  396 + text-align: center;
  397 +
  398 + .tip-text {
  399 + font-size: $font-md;
  400 + color: $color-text-muted;
  401 + }
  402 +
  403 + .tip-subtext {
  404 + margin-top: $spacing-sm;
  405 + font-size: $font-xs;
  406 + color: #bbb;
  407 + }
  408 + }
  409 +
  410 + // 初始引导
  411 + .init-guide {
  412 + .tip-text {
  413 + font-size: $font-lg;
  414 + color: $color-text-secondary;
  415 + font-weight: 500;
  416 + }
  417 + }
  418 +
  419 + // 错误重试按钮
  420 + .retry-btn {
  421 + margin-top: $spacing-md;
  422 + padding: 12rpx 40rpx;
  423 + font-size: $font-sm;
  424 + color: #000;
  425 + background: $color-primary;
  426 + border-radius: $radius-round;
  427 + }
  428 +
  429 + // 部位筛选标签(横向滚动)
  430 + .part-filter {
  431 + margin-bottom: $spacing-md;
  432 + white-space: nowrap;
  433 +
  434 + .part-filter-inner {
  435 + display: inline-flex;
  436 + flex-wrap: nowrap;
388 gap: 15rpx; 437 gap: 15rpx;
389 - margin-bottom: 20rpx; 438 + }
390 439
391 - .item {  
392 - padding: 10rpx 20rpx;  
393 - background: #fff;  
394 - border-radius: 16rpx;  
395 - font-size: 24rpx; 440 + .filter-item {
  441 + flex-shrink: 0;
  442 + padding: 10rpx 24rpx;
  443 + background: $color-white;
  444 + border-radius: $radius-sm;
  445 + font-size: $font-xs;
  446 + color: $color-text-secondary;
  447 + transition: all 0.2s;
396 448
397 &.active { 449 &.active {
398 - background: #f8d714; 450 + background: $color-primary;
399 color: #000; 451 color: #000;
  452 + font-weight: 500;
400 } 453 }
401 } 454 }
402 } 455 }
403 456
  457 + // 部位筛选为空
  458 + .part-empty-tip {
  459 + padding: 30rpx $spacing-lg 40rpx;
  460 + }
  461 +
404 .equipment-item { 462 .equipment-item {
405 - margin-bottom: 30rpx; 463 + margin-bottom: $spacing-lg;
406 464
407 .equipment-name { 465 .equipment-name {
408 - font-size: 28rpx; 466 + font-size: $font-md;
409 font-weight: 500; 467 font-weight: 500;
410 - margin-bottom: 16rpx; 468 + color: $color-text-secondary;
  469 + margin-bottom: $spacing-sm;
  470 + padding-left: $spacing-xs;
  471 + border-left: 6rpx solid $color-primary;
411 } 472 }
412 473
413 .action-list { 474 .action-list {
414 display: grid; 475 display: grid;
415 grid-template-columns: repeat(2, 1fr); 476 grid-template-columns: repeat(2, 1fr);
416 - gap: 16rpx; 477 + gap: $spacing-sm;
417 478
418 .action-item { 479 .action-item {
419 - background: #fff;  
420 - border-radius: 16rpx;  
421 - padding: 16rpx; 480 + background: $color-white;
  481 + border-radius: $radius-md;
  482 + padding: $spacing-sm;
422 display: flex; 483 display: flex;
423 flex-direction: column; 484 flex-direction: column;
424 align-items: center; 485 align-items: center;
425 - gap: 8rpx; 486 + gap: $spacing-xs;
  487 + border: 2rpx solid transparent;
  488 + transition: border-color 0.2s, transform 0.15s;
426 489
427 &.selected { 490 &.selected {
428 - border: 2rpx solid #f8d714; 491 + border-color: $color-primary;
  492 + }
  493 +
  494 + &:active {
  495 + transform: scale(0.97);
429 } 496 }
430 497
431 .action-img { 498 .action-img {
432 width: 100%; 499 width: 100%;
433 height: 200rpx; 500 height: 200rpx;
434 - border-radius: 12rpx; 501 + border-radius: $radius-sm;
435 } 502 }
436 503
437 .action-name { 504 .action-name {
438 - font-size: 24rpx;  
439 - color: #333; 505 + font-size: $font-xs;
  506 + color: $color-text-primary;
440 } 507 }
441 } 508 }
442 } 509 }
443 } 510 }
444 511
  512 + // 无动作空状态
445 .empty-state { 513 .empty-state {
446 padding-top: 100rpx; 514 padding-top: 100rpx;
447 - text-align: center;  
448 - color: #999;  
449 - font-size: 28rpx;  
450 - }  
451 } 515 }
452 } 516 }
453 517
  518 +// ==================== 底部已选标签栏 ====================
454 .selected-bar { 519 .selected-bar {
455 position: fixed; 520 position: fixed;
456 left: 0; 521 left: 0;
457 right: 0; 522 right: 0;
458 bottom: calc(100rpx + env(safe-area-inset-bottom)); 523 bottom: calc(100rpx + env(safe-area-inset-bottom));
459 - background: #2c2c2e;  
460 - padding: 20rpx 30rpx;  
461 - display: flex;  
462 - gap: 20rpx; 524 + background: $color-dark-bar;
  525 + padding: 20rpx 20rpx 40rpx;
463 white-space: nowrap; 526 white-space: nowrap;
  527 + z-index: $z-fixed;
  528 +
  529 + .selected-bar-inner {
  530 + display: inline-flex;
  531 + flex-wrap: nowrap;
  532 + gap: $spacing-md;
  533 + }
464 534
465 .selected-item { 535 .selected-item {
466 - background: #444;  
467 - color: #fff;  
468 - padding: 10rpx 20rpx; 536 + flex-shrink: 0;
  537 + background: $color-dark-tag;
  538 + color: $color-white;
  539 + padding: 10rpx $spacing-md;
469 border-radius: 30rpx; 540 border-radius: 30rpx;
470 display: flex; 541 display: flex;
471 align-items: center; 542 align-items: center;
472 gap: 10rpx; 543 gap: 10rpx;
473 544
474 .name { 545 .name {
475 - font-size: 26rpx; 546 + font-size: $font-sm;
476 } 547 }
477 } 548 }
478 } 549 }
479 550
  551 +// ==================== 底部操作按钮 ====================
480 .bottom-btn-bar { 552 .bottom-btn-bar {
481 position: fixed; 553 position: fixed;
482 bottom: 0; 554 bottom: 0;
483 left: 0; 555 left: 0;
484 right: 0; 556 right: 0;
485 display: flex; 557 display: flex;
486 - gap: 20rpx;  
487 - padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));  
488 - background: #fff; 558 + gap: $spacing-md;
  559 + padding: $spacing-md $spacing-lg calc(20rpx + env(safe-area-inset-bottom));
  560 + background: $color-white;
489 box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); 561 box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  562 + z-index: $z-fixed;
490 563
491 .btn { 564 .btn {
492 flex: 1; 565 flex: 1;
493 height: 80rpx; 566 height: 80rpx;
494 - border-radius: 40rpx; 567 + border-radius: $radius-round;
495 display: flex; 568 display: flex;
496 align-items: center; 569 align-items: center;
497 justify-content: center; 570 justify-content: center;
498 - font-size: 30rpx; 571 + font-size: $font-lg;
499 font-weight: bold; 572 font-weight: bold;
500 } 573 }
501 574
502 .cancel-btn { 575 .cancel-btn {
503 - background: #f5f5f5;  
504 - color: #333; 576 + background: $color-bg;
  577 + color: $color-text-primary;
505 } 578 }
506 579
507 .confirm-btn { 580 .confirm-btn {
508 - background: #f8d714; 581 + background: $color-primary;
509 color: #000; 582 color: #000;
510 583
511 &.disabled { 584 &.disabled {
512 - background: #eee;  
513 - color: #bbb; 585 + background: $color-disabled-bg;
  586 + color: $color-disabled-text;
514 } 587 }
515 } 588 }
516 } 589 }
517 590
  591 +// ==================== 命名弹窗 ====================
518 .popup-content { 592 .popup-content {
519 width: 100%; 593 width: 100%;
520 - background: #fff; 594 + background: $color-white;
521 box-sizing: border-box; 595 box-sizing: border-box;
522 - border-radius: 24rpx 24rpx 0 0;  
523 - padding: 30rpx;  
524 - height: 70vh; 596 + border-radius: $radius-lg $radius-lg 0 0;
  597 + padding: $spacing-lg;
  598 + max-height: 70vh;
525 599
526 .popup-header { 600 .popup-header {
527 display: flex; 601 display: flex;
528 justify-content: space-between; 602 justify-content: space-between;
529 - // align-items: center;  
530 - padding-bottom: 20rpx;  
531 - margin-bottom: 30rpx;  
532 -  
533 - .close-btn {  
534 - font-size: 32rpx;  
535 - color: #333;  
536 - background: none;  
537 - } 603 + align-items: center;
  604 + padding-bottom: $spacing-md;
  605 + margin-bottom: $spacing-lg;
538 606
539 .save-btn { 607 .save-btn {
540 - font-size: 28rpx;  
541 - background: #f8d714; 608 + font-size: $font-md;
  609 + background: $color-primary;
542 padding: 12rpx 32rpx; 610 padding: 12rpx 32rpx;
543 border-radius: 32rpx; 611 border-radius: 32rpx;
544 color: #000; 612 color: #000;
545 } 613 }
546 614
547 .popup-title { 615 .popup-title {
548 - font-size: 32rpx; 616 + font-size: $font-xl;
549 font-weight: bold; 617 font-weight: bold;
550 -  
551 } 618 }
552 } 619 }
553 620
554 .input-wrapper { 621 .input-wrapper {
555 - margin-bottom: 30rpx;  
556 - display: flex;  
557 - // justify-content: center; 622 + margin-bottom: $spacing-lg;
558 623
559 .name-input { 624 .name-input {
560 width: 100%; 625 width: 100%;
561 height: 300rpx; 626 height: 300rpx;
562 - // border: 1rpx solid #ffffff;  
563 background-color: #ececec; 627 background-color: #ececec;
564 - border-radius: 16rpx; 628 + border-radius: $radius-md;
565 padding: 24rpx; 629 padding: 24rpx;
566 - font-size: 28rpx; 630 + font-size: $font-md;
567 text-align: left; 631 text-align: left;
568 vertical-align: top; 632 vertical-align: top;
569 } 633 }
570 -  
571 - .hint {  
572 - font-size: 24rpx;  
573 - color: #999;  
574 - margin-top: 12rpx;  
575 - }  
576 - }  
577 -  
578 - .preview-section {  
579 - .preview-title {  
580 - font-size: 28rpx;  
581 - margin-bottom: 16rpx;  
582 - color: #666;  
583 - }  
584 -  
585 - .preview-scroll {  
586 - display: flex;  
587 - gap: 16rpx;  
588 - overflow-x: auto;  
589 -  
590 - .preview-tag {  
591 - background: #f0f9ff;  
592 - color: #f8d714;  
593 - padding: 8rpx 20rpx;  
594 - border-radius: 50rpx;  
595 - font-size: 24rpx;  
596 - white-space: nowrap;  
597 - }  
598 - }  
599 } 634 }
600 } 635 }
601 </style> 636 </style>
1 <template> 1 <template>
2 - <view class="template-edit-page" @click="dismissGuide"> 2 + <view class="template-edit-page" @click="dismissGuide" :style="{ paddingTop: PageHeight + 'px' }">
3 <!-- 顶部导航 --> 3 <!-- 顶部导航 -->
4 <view class="header"> 4 <view class="header">
5 <view class="close-btn" @click="goBack"> 5 <view class="close-btn" @click="goBack">
@@ -16,26 +16,19 @@ @@ -16,26 +16,19 @@
16 </text> 16 </text>
17 </view> 17 </view>
18 18
19 - <view  
20 - class="save-btn"  
21 - :class="{ disabled: unitCount === 0, active: unitCount > 0 }"  
22 - @click="handleSave"  
23 - > 19 + <view class="save-btn" :class="{ disabled: unitCount === 0, active: unitCount > 0 }" @click="handleSave">
24 保存模板 20 保存模板
25 </view> 21 </view>
26 </view> 22 </view>
27 23
28 <!-- 内容区 --> 24 <!-- 内容区 -->
29 - <view class="content-area"> 25 + <view class="content-wrapper">
  26 + <scroll-view scroll-y class="content-area">
30 <template v-if="unitCount"> 27 <template v-if="unitCount">
31 <view v-for="(unit, index) in units" :key="unit.unitId"> 28 <view v-for="(unit, index) in units" :key="unit.unitId">
32 - <dongzuo  
33 - :id="unit.unitId"  
34 - :type="unit.unitType"  
35 - :action-detail="trainingStore.convertUnitToActionDetail(unit)"  
36 - :ref="(el) => setActionRef(el, index)"  
37 - :unit-index="index"  
38 - /> 29 + <dongzuo :id="unit.unitId" :type="unit.unitType"
  30 + :action-detail="trainingStore.convertUnitToActionDetail(unit)" :ref="(el) => setActionRef(el, index)"
  31 + :unit-index="index" :isTick="false" />
39 </view> 32 </view>
40 </template> 33 </template>
41 34
@@ -45,6 +38,7 @@ @@ -45,6 +38,7 @@
45 点击最下方 + 开始添加动作 38 点击最下方 + 开始添加动作
46 </view> 39 </view>
47 </view> 40 </view>
  41 + </scroll-view>
48 </view> 42 </view>
49 43
50 <!-- 引导气泡 --> 44 <!-- 引导气泡 -->
@@ -81,7 +75,7 @@ @@ -81,7 +75,7 @@
81 </view> 75 </view>
82 76
83 <!-- 文本编辑弹窗 --> 77 <!-- 文本编辑弹窗 -->
84 - <view v-if="popupType" class="text-popup"> 78 + <up-popup :show="!!popupType" mode="bottom" round="24rpx" @close="closeTextPopup" bg-color="#333">
85 <view class="popup-header"> 79 <view class="popup-header">
86 <view class="popup-close-btn" @click="closeTextPopup"> 80 <view class="popup-close-btn" @click="closeTextPopup">
87 <up-icon name="close" color="#fff" size="20" bold /> 81 <up-icon name="close" color="#fff" size="20" bold />
@@ -90,28 +84,15 @@ @@ -90,28 +84,15 @@
90 <view class="popup-save-btn" @click="saveTextPopup">保存</view> 84 <view class="popup-save-btn" @click="saveTextPopup">保存</view>
91 </view> 85 </view>
92 <view class="popup-content"> 86 <view class="popup-content">
93 - <textarea  
94 - class="intro-textarea"  
95 - v-model="popupValue"  
96 - :placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'"  
97 - />  
98 - </view> 87 + <textarea class="intro-textarea" v-model="popupValue"
  88 + :placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'" />
99 </view> 89 </view>
  90 + </up-popup>
100 91
101 <!-- 场景选择器 --> 92 <!-- 场景选择器 -->
102 - <up-picker  
103 - :show="showScenePicker"  
104 - :columns="sceneColumns"  
105 - :default-index="[0]"  
106 - title="训练场景"  
107 - title-color="#ffffff"  
108 - cancel-color="#cccccc"  
109 - confirm-color="#409eff"  
110 - active-color="#ffd100"  
111 - @confirm="handleSceneConfirm"  
112 - @cancel="showScenePicker = false"  
113 - close-on-click-overlay  
114 - /> 93 + <up-picker :show="showScenePicker" :columns="sceneColumns" :default-index="[0]" title="训练场景" title-color="#ffffff"
  94 + cancel-color="#cccccc" confirm-color="#409eff" active-color="#ffd100" @confirm="handleSceneConfirm"
  95 + @cancel="showScenePicker = false" close-on-click-overlay />
115 96
116 <!-- 动作排序 / 新增 --> 97 <!-- 动作排序 / 新增 -->
117 <action-sort ref="actionSortRef" @open-add-actions="addExShow = true" /> 98 <action-sort ref="actionSortRef" @open-add-actions="addExShow = true" />
@@ -131,11 +112,17 @@ import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vu @@ -131,11 +112,17 @@ import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vu
131 import { useTrainingStore } from '@/sheep/store/trainingStore'; 112 import { useTrainingStore } from '@/sheep/store/trainingStore';
132 import FileApi from '@/sheep/api/infra/file'; 113 import FileApi from '@/sheep/api/infra/file';
133 import TemplatesApi from '@/sheep/api/Template/Templates'; 114 import TemplatesApi from '@/sheep/api/Template/Templates';
134 - 115 +import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea';
  116 +const menuButtonHeight = ref(getMenuButtonHeight());
  117 +const topSafeArea = ref(getTopSafeArea());
  118 +const PageHeight = ref(topSafeArea.value);
  119 +// #ifdef MP-WEIXIN
  120 +PageHeight.value = menuButtonHeight.value + topSafeArea.value;
  121 +// #endif
135 dayjs.extend(duration); 122 dayjs.extend(duration);
136 123
137 // ---- 常量 ---- 124 // ---- 常量 ----
138 -const NAVIGATE_DELAY = 1500; 125 +const NAVIGATE_DELAY = 1000;
139 126
140 const SCENE_LIST = [ 127 const SCENE_LIST = [
141 { id: 0, title: '不限' }, 128 { id: 0, title: '不限' },
@@ -367,7 +354,6 @@ $radius-circle: 50%; @@ -367,7 +354,6 @@ $radius-circle: 50%;
367 .template-edit-page { 354 .template-edit-page {
368 position: relative; 355 position: relative;
369 width: 100%; 356 width: 100%;
370 - height: 100vh;  
371 background-color: $color-bg-primary; 357 background-color: $color-bg-primary;
372 display: flex; 358 display: flex;
373 flex-direction: column; 359 flex-direction: column;
@@ -438,14 +424,20 @@ $radius-circle: 50%; @@ -438,14 +424,20 @@ $radius-circle: 50%;
438 } 424 }
439 425
440 // ---- 内容区 ---- 426 // ---- 内容区 ----
441 -.content-area {  
442 - flex: 1;  
443 - overflow-y: auto; 427 +.content-wrapper {
  428 +
  429 + height: 83vh;
444 padding-bottom: 20rpx; 430 padding-bottom: 20rpx;
  431 + box-sizing: border-box;
  432 + overflow: hidden;
  433 +}
  434 +
  435 +.content-area {
  436 + height: 100%;
445 } 437 }
446 438
447 .empty-area { 439 .empty-area {
448 - flex: 1; 440 + height: 100%;
449 display: flex; 441 display: flex;
450 align-items: center; 442 align-items: center;
451 justify-content: center; 443 justify-content: center;
@@ -461,8 +453,8 @@ $radius-circle: 50%; @@ -461,8 +453,8 @@ $radius-circle: 50%;
461 453
462 // ---- 引导气泡 ---- 454 // ---- 引导气泡 ----
463 .guide-bubble { 455 .guide-bubble {
464 - position: absolute;  
465 - bottom: 130rpx; 456 + position: fixed;
  457 + bottom: 200rpx;
466 left: 50%; 458 left: 50%;
467 transform: translateX(-50%); 459 transform: translateX(-50%);
468 background-color: #000; 460 background-color: #000;
@@ -485,6 +477,10 @@ $radius-circle: 50%; @@ -485,6 +477,10 @@ $radius-circle: 50%;
485 477
486 // ---- 底部栏 ---- 478 // ---- 底部栏 ----
487 .bottom-bar { 479 .bottom-bar {
  480 + position: fixed;
  481 + left: 0;
  482 + right: 0;
  483 + bottom: 0;
488 display: flex; 484 display: flex;
489 height: 110rpx; 485 height: 110rpx;
490 align-items: center; 486 align-items: center;
@@ -493,7 +489,6 @@ $radius-circle: 50%; @@ -493,7 +489,6 @@ $radius-circle: 50%;
493 padding-bottom: env(safe-area-inset-bottom); 489 padding-bottom: env(safe-area-inset-bottom);
494 background-color: $color-bg-primary; 490 background-color: $color-bg-primary;
495 border-top: 1rpx solid $color-border; 491 border-top: 1rpx solid $color-border;
496 - flex-shrink: 0;  
497 492
498 .bar-item { 493 .bar-item {
499 display: flex; 494 display: flex;
@@ -528,19 +523,6 @@ $radius-circle: 50%; @@ -528,19 +523,6 @@ $radius-circle: 50%;
528 } 523 }
529 524
530 // ---- 文本弹窗 ---- 525 // ---- 文本弹窗 ----
531 -.text-popup {  
532 - position: fixed;  
533 - left: 0;  
534 - right: 0;  
535 - bottom: 0;  
536 - height: 85vh;  
537 - background: $color-bg-secondary;  
538 - border-radius: $radius-lg $radius-lg 0 0;  
539 - z-index: 1000;  
540 - display: flex;  
541 - flex-direction: column;  
542 -}  
543 -  
544 .popup-header { 526 .popup-header {
545 display: flex; 527 display: flex;
546 justify-content: space-between; 528 justify-content: space-between;
@@ -78,14 +78,7 @@ @@ -78,14 +78,7 @@
78 <view 78 <view
79 class="rest-countdown" 79 class="rest-countdown"
80 v-if="restCountdownSeconds > 0" 80 v-if="restCountdownSeconds > 0"
81 - @click="  
82 - clearRestCountdown();  
83 - uncheckCurrentUnit();  
84 - if (restTimerWasRunning) {  
85 - trainingStore.toggleTimer();  
86 - restTimerWasRunning = false;  
87 - }  
88 - " 81 + @click="clearRestCountdown()"
89 > 82 >
90 <view class="countdown-circle"> 83 <view class="countdown-circle">
91 <!-- H5 端:使用 SVG 进度环 --> 84 <!-- H5 端:使用 SVG 进度环 -->
@@ -288,7 +281,6 @@ @@ -288,7 +281,6 @@
288 const restCountdownSeconds = ref(0); 281 const restCountdownSeconds = ref(0);
289 const restCountdownTotal = ref(0); 282 const restCountdownTotal = ref(0);
290 let restCountdownInterval = null; 283 let restCountdownInterval = null;
291 - let restTimerWasRunning = false;  
292 284
293 const restFormattedTime = computed(() => { 285 const restFormattedTime = computed(() => {
294 const min = Math.floor(restCountdownSeconds.value / 60); 286 const min = Math.floor(restCountdownSeconds.value / 60);
@@ -304,8 +296,6 @@ @@ -304,8 +296,6 @@
304 // H5 SVG 圆环周长(用于 stroke-dasharray) 296 // H5 SVG 圆环周长(用于 stroke-dasharray)
305 const CIRCUMFERENCE = 2 * Math.PI * 44; 297 const CIRCUMFERENCE = 2 * Math.PI * 44;
306 298
307 - const currentRestUnitIndex = ref(null);  
308 -  
309 const clearRestCountdown = () => { 299 const clearRestCountdown = () => {
310 if (restCountdownInterval) { 300 if (restCountdownInterval) {
311 clearInterval(restCountdownInterval); 301 clearInterval(restCountdownInterval);
@@ -315,39 +305,33 @@ @@ -315,39 +305,33 @@
315 restCountdownTotal.value = 0; 305 restCountdownTotal.value = 0;
316 }; 306 };
317 307
318 - const startRestCountdown = (seconds, unitIndex) => { 308 + const startRestCountdown = (seconds) => {
319 if (!seconds || seconds <= 0) return; 309 if (!seconds || seconds <= 0) return;
320 - clearRestCountdown(); 310 + // 叠加倒计时:如果已经在倒计时中则累加,否则开始新的倒计时
  311 + if (restCountdownInterval) {
  312 + restCountdownSeconds.value += seconds;
  313 + restCountdownTotal.value += seconds;
  314 + } else {
321 restCountdownTotal.value = seconds; 315 restCountdownTotal.value = seconds;
322 restCountdownSeconds.value = seconds; 316 restCountdownSeconds.value = seconds;
323 - currentRestUnitIndex.value = unitIndex;  
324 - restTimerWasRunning = !trainingStore.isPause;  
325 - if (restTimerWasRunning) trainingStore.toggleTimer();  
326 restCountdownInterval = setInterval(() => { 317 restCountdownInterval = setInterval(() => {
327 restCountdownSeconds.value--; 318 restCountdownSeconds.value--;
328 if (restCountdownSeconds.value <= 0) { 319 if (restCountdownSeconds.value <= 0) {
329 clearRestCountdown(); 320 clearRestCountdown();
330 - uncheckCurrentUnit();  
331 - if (restTimerWasRunning) trainingStore.toggleTimer(); 321 + // 倒计时结束:不取消勾选状态,不恢复主计时器(主计时器始终在运行)
332 } 322 }
333 }, 1000); 323 }, 1000);
334 - };  
335 -  
336 - function uncheckCurrentUnit() {  
337 - if (currentRestUnitIndex.value != null) {  
338 - const ref = dongzuoRefs.value[currentRestUnitIndex.value];  
339 - if (ref?.resetActiveSets) ref.resetActiveSets();  
340 - currentRestUnitIndex.value = null;  
341 - }  
342 } 324 }
  325 + // 不暂停主计时器,训练计时器始终继续
  326 + };
343 327
344 - function onCancelRestTimer(unitIndex) { 328 + function onCancelRestTimer(seconds) {
  329 + if (!restCountdownInterval) return;
  330 + // 取消勾选时减去对应秒数
  331 + restCountdownSeconds.value -= (seconds || 0);
  332 + restCountdownTotal.value -= (seconds || 0);
  333 + if (restCountdownSeconds.value <= 0) {
345 clearRestCountdown(); 334 clearRestCountdown();
346 - currentRestUnitIndex.value = unitIndex;  
347 - uncheckCurrentUnit();  
348 - if (restTimerWasRunning) {  
349 - trainingStore.toggleTimer();  
350 - restTimerWasRunning = false;  
351 } 335 }
352 } 336 }
353 337
@@ -545,8 +529,25 @@ @@ -545,8 +529,25 @@
545 } 529 }
546 } 530 }
547 531
548 - if (units.length === 0) {  
549 - uni.showToast({ title: '未获取到训练数据', icon: 'none' }); 532 + // 检查是否有打勾(已完成)的动作,只有打勾的动作才会被提交
  533 + const hasAnyChecked = units.some(u =>
  534 + u.exercises.some(e => e.sets && e.sets.length > 0)
  535 + );
  536 +
  537 + if (!hasAnyChecked) {
  538 + uni.showModal({
  539 + title: '提示',
  540 + content: '未完成任何动作,是否取消训练?',
  541 + confirmText: '取消训练',
  542 + cancelText: '继续训练',
  543 + confirmColor: '#FF3B30',
  544 + success: (res) => {
  545 + if (res.confirm) {
  546 + uni.navigateBack();
  547 + trainingStore.clearTrainingStore();
  548 + }
  549 + },
  550 + });
550 return; 551 return;
551 } 552 }
552 553