Authored by Bad

refactor(dongzuo): 重构动作组件并优化休息计时逻辑

@@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1 @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development) 8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081 9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080 10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
11 -# SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com 11 + SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/ 12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080 13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc 14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
@@ -38,6 +38,7 @@ @@ -38,6 +38,7 @@
38 </template> 38 </template>
39 39
40 <script setup> 40 <script setup>
  41 +import dayjs from 'dayjs';
41 import { ref, computed } from 'vue' 42 import { ref, computed } from 'vue'
42 import TrainingApi from '@/sheep/api/Training/traininghistory' 43 import TrainingApi from '@/sheep/api/Training/traininghistory'
43 44
@@ -127,12 +128,7 @@ const formattedSetList = computed(() => { @@ -127,12 +128,7 @@ const formattedSetList = computed(() => {
127 parts.push(`${set.reps}次`) 128 parts.push(`${set.reps}次`)
128 } 129 }
129 if (set.duration != null && set.duration > 0) { 130 if (set.duration != null && set.duration > 0) {
130 - // 秒转成 HH:MM:SS  
131 - const h = Math.floor(set.duration / 3600)  
132 - const m = Math.floor((set.duration % 3600) / 60)  
133 - const s = set.duration % 60  
134 - const hh = h > 0 ? `${h}:` : ''  
135 - parts.push(`${hh}${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`) 131 + parts.push(dayjs.duration(set.duration, 'seconds').format('H:mm:ss'));
136 } 132 }
137 if (set.distance != null && set.distance !== '') { 133 if (set.distance != null && set.distance !== '') {
138 parts.push(`${set.distance}m`) 134 parts.push(`${set.distance}m`)
1 <template> 1 <template>
2 <view class="action-container" @click="closeDifficulty"> 2 <view class="action-container" @click="closeDifficulty">
3 <view class="action-list" v-if="deleteActionShow === false"> 3 <view class="action-list" v-if="deleteActionShow === false">
4 - <view class="card"> 4 + <view class="card" :class="{ 'unit-active': isUnitActive }">
5 <view class="card-top" @click.stop="isExpanded = !isExpanded"> 5 <view class="card-top" @click.stop="isExpanded = !isExpanded">
6 <view v-if="type === 1"> 6 <view v-if="type === 1">
7 <image class="action-image" :src="actionDetail?.urlImage || lostImage" mode="aspectFill"></image> 7 <image class="action-image" :src="actionDetail?.urlImage || lostImage" mode="aspectFill"></image>
@@ -12,11 +12,9 @@ @@ -12,11 +12,9 @@
12 mode="aspectFill"></image> 12 mode="aspectFill"></image>
13 </view> 13 </view>
14 <view class="action-info"> 14 <view class="action-info">
15 - <!-- 动作的 -->  
16 <text class="name">{{ actionDetail?.name }}</text> 15 <text class="name">{{ actionDetail?.name }}</text>
17 <text class="tags" v-if="type === 1">{{ actionDetail?.categoryDescription }} {{ 16 <text class="tags" v-if="type === 1">{{ actionDetail?.categoryDescription }} {{
18 actionDetail?.equipmentDescription }}</text> 17 actionDetail?.equipmentDescription }}</text>
19 - <!-- 这里还缺kg/要搞个计算属性 -->  
20 <text class="tags" v-if="type === 2">超级组</text> 18 <text class="tags" v-if="type === 2">超级组</text>
21 <view class="dot-list" v-if="!isExpanded"> 19 <view class="dot-list" v-if="!isExpanded">
22 <view class="dot" v-for="(item, index) in recordList" :key="'dot-' + index"></view> 20 <view class="dot" v-for="(item, index) in recordList" :key="'dot-' + index"></view>
@@ -34,16 +32,12 @@ @@ -34,16 +32,12 @@
34 </view> 32 </view>
35 33
36 <view class="main" v-if="isExpanded"> 34 <view class="main" v-if="isExpanded">
37 - <!-- 当类型为4,5时,显示体重输入框 -->  
38 - <!--动作的 recordList 的输入控制 -->  
39 <template v-if="type === 1"> 35 <template v-if="type === 1">
40 <view class="userWeight" v-if="[4, 5].includes(actionDetail.exerciseType)"> 36 <view class="userWeight" v-if="[4, 5].includes(actionDetail.exerciseType)">
41 <view class="row" hover-class="none" hover-stop-propagation="false"> 37 <view class="row" hover-class="none" hover-stop-propagation="false">
42 <text class="label">体重</text> 38 <text class="label">体重</text>
43 <view class="userWeight-values" @click="openWeightPicker"> 39 <view class="userWeight-values" @click="openWeightPicker">
44 - <text class="weight-text" style="color: #fbdc00; font-size: 20rpx;">  
45 - {{ displayWeight }}  
46 - </text> 40 + <text class="weight-text">{{ displayWeight }}</text>
47 <up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon> 41 <up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon>
48 </view> 42 </view>
49 </view> 43 </view>
@@ -54,38 +48,27 @@ @@ -54,38 +48,27 @@
54 该动作为自重减重,根据你的体重减去辅助器械重量计算容量,下方的填写重量为辅助器械重量。 48 该动作为自重减重,根据你的体重减去辅助器械重量计算容量,下方的填写重量为辅助器械重量。
55 </view> 49 </view>
56 </view> 50 </view>
57 - <!-- 导入子组件 -->  
58 <action-set-input v-for="(item, index) in recordList" :key="index" 51 <action-set-input v-for="(item, index) in recordList" :key="index"
59 :exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing" 52 :exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing"
60 @openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow" 53 @openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow"
61 @deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" /> 54 @deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" />
62 </template> 55 </template>
63 - <!-- 超级组的训练数据控制 --> 56 +
64 <template v-else-if="type === 2"> 57 <template v-else-if="type === 2">
65 <view class="super-group"> 58 <view class="super-group">
66 <view class="img-list" v-if="type === 2"> 59 <view class="img-list" v-if="type === 2">
67 <image v-for="item in (actionDetail?.exercises || [])" :key="item.id" :src="item?.urlImage || lostImage" 60 <image v-for="item in (actionDetail?.exercises || [])" :key="item.id" :src="item?.urlImage || lostImage"
68 class="img"></image> 61 class="img"></image>
69 </view> 62 </view>
70 - <!-- 超级组:如果内部包含自重加重/减重,显示体重输入v-if="hasWeightExercise" -->  
71 <view class="userWeight" v-if="hasWeightExercise"> 63 <view class="userWeight" v-if="hasWeightExercise">
72 <view class="row"> 64 <view class="row">
73 <text class="label">体重</text> 65 <text class="label">体重</text>
74 <view class="userWeight-values" @click="openWeightPicker"> 66 <view class="userWeight-values" @click="openWeightPicker">
75 - <text class="weight-text" style="color: #fbdc00; font-size: 20rpx;">  
76 - {{ displayWeight }}  
77 - </text> 67 + <text class="weight-text">{{ displayWeight }}</text>
78 <up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon> 68 <up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon>
79 </view> 69 </view>
80 </view> 70 </view>
81 - <!-- <view class="explain" v-if="actionDetail?.exerciseType === 4">  
82 - 该动作为自重加重,根据你的体重加上辅助器械重量计算容量,下方的填写重量为辅助器械重量。  
83 - </view>  
84 - <view class="explain" v-if="actionDetail?.exerciseType === 5">  
85 - 该动作为自重减重,根据你的体重减去辅助器械重量计算容量,下方的填写重量为辅助器械重量。  
86 - </view> -->  
87 </view> 71 </view>
88 - <!-- 外层:循环【每一组】 -->  
89 <view class="set-group" v-for="(setItem, setIndex) in superTotalSets" :key="setIndex"> 72 <view class="set-group" v-for="(setItem, setIndex) in superTotalSets" :key="setIndex">
90 <view class="set-header"> 73 <view class="set-header">
91 <view class="index-box">{{ setIndex + 1 }}</view> 74 <view class="index-box">{{ setIndex + 1 }}</view>
@@ -94,48 +77,39 @@ @@ -94,48 +77,39 @@
94 {{ String.fromCharCode(65 + idx) }} {{ sub.name }} 77 {{ String.fromCharCode(65 + idx) }} {{ sub.name }}
95 </text> 78 </text>
96 </view> 79 </view>
97 - <!-- 休息时间选择 -->  
98 <view class="timer-selector" @click.stop="openSuperQuickTimer(setIndex)" v-if="!isEditing"> 80 <view class="timer-selector" @click.stop="openSuperQuickTimer(setIndex)" v-if="!isEditing">
99 <text class="text">{{ superGroupSetData(setIndex).quickTimeDisplay || '60s' }}</text> 81 <text class="text">{{ superGroupSetData(setIndex).quickTimeDisplay || '60s' }}</text>
100 <up-icon name="arrow-down" color="#8e8e93" size="10"></up-icon> 82 <up-icon name="arrow-down" color="#8e8e93" size="10"></up-icon>
101 </view> 83 </view>
102 - <!-- 勾选框 -->  
103 - <view class="icon" @click.stop="toggleSuperSetActive(setIndex)" 84 + <view class="check-icon" @click.stop="toggleSuperSetActive(setIndex)"
104 :class="{ active: superGroupSetData(setIndex).isActive }" v-if="!isEditing"> 85 :class="{ active: superGroupSetData(setIndex).isActive }" v-if="!isEditing">
105 <up-icon name="checkmark" :color="superGroupSetData(setIndex).isActive ? '#000' : '#919191'" 86 <up-icon name="checkmark" :color="superGroupSetData(setIndex).isActive ? '#000' : '#919191'"
106 size="20" bold></up-icon> 87 size="20" bold></up-icon>
107 </view> 88 </view>
108 - <!-- 新增和删除按钮 -->  
109 - <view class="icons" v-if="isEditing">  
110 - <view class="icon" @click.stop="addSuperSet"> 89 + <view class="action-icons" v-if="isEditing">
  90 + <view class="action-icon" @click.stop="addSuperSet">
111 <up-icon name="plus-circle" color="#fff" size="20" bold></up-icon> 91 <up-icon name="plus-circle" color="#fff" size="20" bold></up-icon>
112 </view> 92 </view>
113 - <view class="icon del" @click.stop="deleteSuperSet(setIndex)"> 93 + <view class="action-icon icon-del" @click.stop="deleteSuperSet(setIndex)">
114 <up-icon name="trash" color="#e63e1e" size="20" bold></up-icon> 94 <up-icon name="trash" color="#e63e1e" size="20" bold></up-icon>
115 </view> 95 </view>
116 </view> 96 </view>
117 </view> 97 </view>
118 -  
119 - <!-- 内层:循环【组内的每个子动作A/B】 -->  
120 <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"
121 :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"
122 :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)" />
123 </view> 101 </view>
124 </view> 102 </view>
125 </template> 103 </template>
126 -  
127 </view> 104 </view>
128 105
129 <view class="button-group" v-if="isExpanded"> 106 <view class="button-group" v-if="isExpanded">
130 <view class="btn-item" @click.stop="addGroup">加一组</view> 107 <view class="btn-item" @click.stop="addGroup">加一组</view>
131 <view class="btn-item" @click="openHistory(props?.id)">历史</view> 108 <view class="btn-item" @click="openHistory(props?.id)">历史</view>
132 - <!-- 历史弹窗 -->  
133 <actionHistory ref="historyPopupRef" :actionName="actionDetail.name" :historyType="props.type" /> 109 <actionHistory ref="historyPopupRef" :actionName="actionDetail.name" :historyType="props.type" />
134 - <!-- 动作难度选择弹窗 -->  
135 <view class="btn-item" @click.stop="showDifficulty = !showDifficulty"> 110 <view class="btn-item" @click.stop="showDifficulty = !showDifficulty">
136 <text :style="{ color: currentDifficulty.color }">{{ currentDifficulty.label }}</text> 111 <text :style="{ color: currentDifficulty.color }">{{ currentDifficulty.label }}</text>
137 <up-icon name="arrow-down" size="8" color="#fff"></up-icon> 112 <up-icon name="arrow-down" size="8" color="#fff"></up-icon>
138 -  
139 <view class="difficulty-pop" v-if="showDifficulty"> 113 <view class="difficulty-pop" v-if="showDifficulty">
140 <view class="difficulty-item" @click.stop="selectDifficulty('轻松', '#b2f055')"> 114 <view class="difficulty-item" @click.stop="selectDifficulty('轻松', '#b2f055')">
141 <text :style="{ color: '#b2f055' }">轻松</text> 115 <text :style="{ color: '#b2f055' }">轻松</text>
@@ -154,7 +128,6 @@ @@ -154,7 +128,6 @@
154 </view> 128 </view>
155 </view> 129 </view>
156 </view> 130 </view>
157 -  
158 <view class="btn-item" @click.stop="isEditing = !isEditing"> 131 <view class="btn-item" @click.stop="isEditing = !isEditing">
159 {{ isEditing ? '完成' : '编辑' }} 132 {{ isEditing ? '完成' : '编辑' }}
160 </view> 133 </view>
@@ -168,760 +141,639 @@ @@ -168,760 +141,639 @@
168 <up-picker :show="showQuickPicker" :columns="quickColumns" title="设置休息时长" @confirm="onQuickConfirm" 141 <up-picker :show="showQuickPicker" :columns="quickColumns" title="设置休息时长" @confirm="onQuickConfirm"
169 @cancel="showQuickPicker = false" closeOnClickOverlay popupClass="custom-picker"></up-picker> 142 @cancel="showQuickPicker = false" closeOnClickOverlay popupClass="custom-picker"></up-picker>
170 143
171 - <!-- 备注弹窗组件(可创建备注成功,但无法显示到动作详情弹窗组件) -->  
172 <beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" /> 144 <beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" />
173 - <!-- 动作替换弹窗 -->  
174 <replaceActionPopup :show="replacePopupShow" :unitIndex="unitIndex" @close="replacePopupShow = false" /> 145 <replaceActionPopup :show="replacePopupShow" :unitIndex="unitIndex" @close="replacePopupShow = false" />
175 146
176 - <!-- 体重选择器 -->  
177 <up-picker :show="showWeightPicker" :columns="weightColumns" :defaultIndex="defaultWeightIndex" title="设置体重" 147 <up-picker :show="showWeightPicker" :columns="weightColumns" :defaultIndex="defaultWeightIndex" title="设置体重"
178 @confirm="onWeightConfirm" @cancel="showWeightPicker = false" closeOnClickOverlay 148 @confirm="onWeightConfirm" @cancel="showWeightPicker = false" closeOnClickOverlay
179 popupClass="custom-picker"></up-picker> 149 popupClass="custom-picker"></up-picker>
180 -  
181 </view> 150 </view>
182 </template> 151 </template>
183 152
184 <script setup> 153 <script setup>
185 -import { ref, reactive, watch, computed, nextTick, onMounted } from 'vue'; 154 +import { ref, reactive, watch, computed, nextTick, onMounted, getCurrentInstance } from 'vue';
  155 +import dayjs from 'dayjs';
  156 +import duration from 'dayjs/plugin/duration';
  157 +import { useTrainingStore } from '@/sheep/store/trainingStore'
186 import beizhu from '@/pages/xunji/components/beizhu.vue'; 158 import beizhu from '@/pages/xunji/components/beizhu.vue';
187 -// import actionHistory from '../components/dongzuo-lianxi-history.vue'  
188 import actionHistory from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-lianxi-history.vue' 159 import actionHistory from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-lianxi-history.vue'
189 -import SupersetsApi from '@/sheep/api/motion/supersets';  
190 -import ExercisesApi from '@/sheep/api/motion/exercises';  
191 -import { getCurrentInstance } from 'vue';  
192 -import { useTrainingStore } from '@/sheep/store/trainingStore'  
193 -// import replaceActionPopup from '../components/replace-action-popup.vue'  
194 import replaceActionPopup from '@/pages/xunji/components/dongzuo-lianxi/replace-action-popup.vue' 160 import replaceActionPopup from '@/pages/xunji/components/dongzuo-lianxi/replace-action-popup.vue'
195 -// import actionSetInput from '@/pages4/components/action-set-input.vue'  
196 import actionSetInput from '@/pages/xunji/components/dongzuo-lianxi/action-set-input.vue' 161 import actionSetInput from '@/pages/xunji/components/dongzuo-lianxi/action-set-input.vue'
197 -// import superSetInput from '../components/super-set-input.vue'  
198 import superSetInput from '@/pages/xunji/components/dongzuo-lianxi/super-set-input.vue' 162 import superSetInput from '@/pages/xunji/components/dongzuo-lianxi/super-set-input.vue'
  163 +import SupersetsApi from '@/sheep/api/motion/supersets';
  164 +import ExercisesApi from '@/sheep/api/motion/exercises';
199 165
200 -  
201 -const trainingStore = useTrainingStore()  
202 -const deleteActionShow = ref(false)  
203 -const showTips = ref(false);  
204 -const isExpanded = ref(false);  
205 -const isEditing = ref(false);  
206 -const showPicker = ref(false);  
207 -const showQuickPicker = ref(false);  
208 -const activeIndex = ref(0);  
209 -const activeActionId = ref('');  
210 -const emit = defineEmits(['register', 'update:groupCount', 'replace-action', 'deleteAction', 'replaceActionForSave'])  
211 -const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"  
212 -  
213 -const historyPopupRef = ref(null)  
214 -const showBeizhuRef = ref(null);  
215 -const userWeight = ref(70)  
216 -// 休息时间设置模式:single=单行,all=全部组  
217 -const restEditMode = ref('single');  
218 -const replacePopupShow = ref(false)  
219 -  
220 -// 接收父组件的数据 166 +// ===================== Props =====================
221 const props = defineProps({ 167 const props = defineProps({
222 id: [Number, String], 168 id: [Number, String],
223 type: [Number, String], 169 type: [Number, String],
224 - index: Number, // 新增 170 + index: Number,
225 unitIndex: Number, 171 unitIndex: Number,
226 - actionDetail: {  
227 - type: Object,  
228 - default: () => ({})  
229 - },  
230 - isDailyTemplates: {  
231 - type: Boolean,  
232 - default: false  
233 - } 172 + actionDetail: { type: Object, default: () => ({}) },
  173 + isDailyTemplates: { type: Boolean, default: false },
  174 + isUnitActive: { type: Boolean, default: false }
234 }) 175 })
235 176
236 -watch(() => props.id, () => {  
237 - console.log('dongzuo组件 id/type:', props.id, props.type)  
238 - console.log('打印props.actionDetail', props.actionDetail)  
239 - console.log('dongzuo组件 unitIndex:', props.unitIndex)  
240 - if (props.type === 2)  
241 - console.log('打印父组件传过来超级组动作列表的props.actionDetail.exercises', props.actionDetail.exercises); 177 +// ===================== Emits =====================
  178 +const emit = defineEmits(['register', 'update:groupCount', 'replace-action', 'deleteAction', 'replaceActionForSave', 'startRestTimer', 'cancelRestTimer', 'updateUnitActive'])
242 179
243 -}, { immediate: true }) 180 +// ===================== 常量 =====================
  181 +dayjs.extend(duration)
  182 +const lostImage = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
244 183
245 -emit('register', props.index, getCurrentInstance()?.proxy) 184 +// ===================== Store =====================
  185 +const trainingStore = useTrainingStore()
246 186
247 -// 体重选择器  
248 -const displayWeight = computed(() => {  
249 - return `${userWeight.value}kg`  
250 -}) 187 +// ===================== 基础状态 =====================
  188 +const deleteActionShow = ref(false)
  189 +const showTips = ref(false)
  190 +const isExpanded = ref(false)
  191 +const isEditing = ref(false)
  192 +const replacePopupShow = ref(false)
  193 +const historyPopupRef = ref(null)
  194 +const showBeizhuRef = ref(null)
  195 +
  196 +// ===================== Picker 状态 =====================
  197 +const showPicker = ref(false)
  198 +const showQuickPicker = ref(false)
  199 +const activeIndex = ref(0)
  200 +const activeActionId = ref('')
  201 +const restEditMode = ref('single')
  202 +
  203 +const timeColumns = reactive([
  204 + Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'),
  205 + Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),
  206 + Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),
  207 +])
  208 +const defaultTimeIndex = ref([0, 0, 0])
  209 +
  210 +const quickColumns = reactive([
  211 + Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),
  212 + Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),
  213 +])
251 214
252 -// 2. 体重选择器状态 215 +// ===================== 体重 =====================
  216 +const userWeight = ref(70)
253 const showWeightPicker = ref(false) 217 const showWeightPicker = ref(false)
254 const weightColumns = reactive([ 218 const weightColumns = reactive([
255 - // 整数部分:10 ~ 250  
256 Array.from({ length: 241 }, (_, i) => (i + 10).toString()), 219 Array.from({ length: 241 }, (_, i) => (i + 10).toString()),
257 - // 小数部分:0 ~ 9  
258 Array.from({ length: 10 }, (_, i) => i.toString()), 220 Array.from({ length: 10 }, (_, i) => i.toString()),
259 - ['千克(kg)'] // 第三列固定文字 221 + ['千克(kg)']
260 ]) 222 ])
261 -const defaultWeightIndex = ref([60, 0, 0]) // 默认 70.0kg(10+60=70)  
262 -  
263 -// 3. 打开体重选择器  
264 -const openWeightPicker = () => {  
265 - // 直接用数字处理,避免转字符串  
266 - const num = userWeight.value  
267 - const intPart = Math.floor(num)  
268 - const decPart = Math.round((num - intPart) * 10) // 取小数第一位 223 +const defaultWeightIndex = ref([60, 0, 0])
269 224
270 - // 计算索引(整数部分:intPart - 10,小数部分:decPart)  
271 - const intIndex = Math.max(0, Math.min(intPart - 10, 240))  
272 - defaultWeightIndex.value = [intIndex, decPart, 0]  
273 - showWeightPicker.value = true  
274 -} 225 +// ===================== 难度 =====================
  226 +const showDifficulty = ref(false)
  227 +const initialDifficulty = { label: '难度', color: '#ffffff' }
  228 +const currentDifficulty = ref({ ...initialDifficulty })
275 229
276 -// 4. 体重选择器确认事件  
277 -const onWeightConfirm = (e) => {  
278 - const intPart = e.value[0] // 整数部分  
279 - const decPart = e.value[1] // 小数部分  
280 - // 拼接成完整体重  
281 - userWeight.value = parseFloat(`${intPart}.${decPart}`)  
282 - showWeightPicker.value = false  
283 -} 230 +// ===================== 训练数据 =====================
  231 +const recordList = ref([{
  232 + h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
  233 + distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
  234 +}])
284 235
285 -// 打开备注弹窗  
286 -const openBeizhu = () => {  
287 - nextTick(() => {  
288 - if (showBeizhuRef.value) {  
289 - showBeizhuRef.value.open(props.actionDetail.id);  
290 - }  
291 - });  
292 -}; 236 +const superRecordMap = ref({})
293 237
294 -// 接收子组件传过来的备注内容  
295 -const handleNoteSave = async (content) => {  
296 - try {  
297 - if (props.type == 2) {  
298 - await SupersetsApi.addNotes({  
299 - supersetsId: props.id,  
300 - content: content,  
301 - });  
302 - } else {  
303 - await ExercisesApi.addNotes({  
304 - exerciseId: props.id,  
305 - content: content,  
306 - });  
307 - }  
308 - props.actionDetail.userNote = content;  
309 - } catch (e) {  
310 - console.log(e);  
311 - }  
312 -}; 238 +// 向父组件注册组件实例引用
  239 +emit('register', props.index, getCurrentInstance()?.proxy)
313 240
314 -const toggleTips = () => {  
315 - showTips.value = !showTips.value  
316 -} 241 +// ===================== Computed =====================
  242 +const displayWeight = computed(() => `${userWeight.value}kg`)
317 243
318 -// 超级组是否包含间歇类型动作(用来控制组头休息时间选择器的显示)  
319 -const hasIntervalExercise = computed(() => { 244 +const hasWeightExercise = computed(() => {
320 if (props.type !== 2) return false 245 if (props.type !== 2) return false
321 - const exercises = props.actionDetail?.exercises || []  
322 - // 只要有一个子动作是间歇类型(6),就不显示组头的休息时间  
323 - return exercises.some(sub => sub.exerciseType === 6) 246 + return (props.actionDetail?.exercises || []).some(sub => [4, 5].includes(sub.exerciseType))
324 }) 247 })
325 -// 替换动作  
326 -const handleReplaceAction = () => {  
327 - showTips.value = false  
328 - replacePopupShow.value = true // 打开弹窗  
329 - console.log("✅ 弹窗打开:", replacePopupShow.value)  
330 -}  
331 248
332 -// 超级组是否包含 4(自重加重) / 5(自重减重)  
333 -const hasWeightExercise = computed(() => {  
334 - if (props.type !== 2) return false 249 +const superTotalSets = computed(() => {
  250 + if (props.type !== 2) return 0
335 const exercises = props.actionDetail?.exercises || [] 251 const exercises = props.actionDetail?.exercises || []
336 - return exercises.some(sub => [4, 5].includes(sub.exerciseType)) 252 + if (exercises.length === 0) return 0
  253 + return superRecordMap.value[exercises[0].id]?.length || 0
337 }) 254 })
338 255
  256 +const exerciseNamesWithLabel = computed(() => {
  257 + return (props.actionDetail?.exercises || []).map((item, index) => {
  258 + const label = String.fromCharCode(65 + index)
  259 + return `${label} ${item?.name || '未知动作'}`
  260 + })
  261 +})
339 262
340 -// 删除动作  
341 -const deleteAction = () => {  
342 - // 如何是每日模板的就返回一个删除印象  
343 - if (props.isDailyTemplates) {  
344 - showTips.value = false;  
345 - emit('deleteAction');  
346 - // trainingStore.actionDetail.units.splice(props.unitIndex, 1)  
347 - // trainingStore.clearTrainingStore()  
348 - return;  
349 - }  
350 - trainingStore.deleteUnitRecord(props.unitIndex);  
351 - console.log('训练类型 type =', trainingStore.type)  
352 - console.log('组件类型 props.type =', props.type)  
353 - console.log('unitIndex =', props.unitIndex)  
354 - deleteActionShow.value = true 263 +const exposeRecordList = computed(() => {
  264 + return recordList.value.filter(item => item.isActive).map(item => ({
  265 + ...item,
  266 + weight: getRealWeight(item)
  267 + }))
  268 +})
355 269
356 - // 1. 如果是【模板训练】,删除对应项  
357 - if (trainingStore.type === 3) {  
358 - console.log('模板训练:删除第', props.unitIndex, '个单元')  
359 - trainingStore.actionDetail.units.splice(props.unitIndex, 1) 270 +const exposeSuperRecordMap = computed(() => {
  271 + const map = {}
  272 + for (const key in superRecordMap.value) {
  273 + const subExercise = props.actionDetail?.exercises?.find(s => s.id == key)
  274 + map[key] = superRecordMap.value[key]
  275 + .filter(item => item.isActive)
  276 + .map(item => ({ ...item, weight: getRealWeight(item, subExercise?.exerciseType) }))
360 } 277 }
361 - else {  
362 - console.log('单个动作/超级组:清空')  
363 - trainingStore.clearTrainingStore() 278 + return map
  279 +})
  280 +
  281 +const totalSetCount = computed(() => {
  282 + if (props.type === 1) return recordList.value.length
  283 + if (props.type === 2) {
  284 + const exercises = props.actionDetail?.exercises || []
  285 + if (exercises.length === 0) return 0
  286 + return superRecordMap.value[exercises[0].id]?.length || 0
364 } 287 }
365 - // 关闭菜单  
366 - showTips.value = false  
367 - console.log('删除完成 ✅') 288 + return 0
  289 +})
368 290
  291 +const checkedWeight = computed(() => {
  292 + let sum = 0
  293 + if (props.type === 1 && recordList.value) {
  294 + recordList.value.forEach(item => {
  295 + if (item.isActive) sum += getRealWeight(item) * (Number(item.reps) || 0)
  296 + })
  297 + }
  298 + if (props.type === 2 && superRecordMap.value) {
  299 + for (const actionId in superRecordMap.value) {
  300 + const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)
  301 + superRecordMap.value[actionId].forEach(item => {
  302 + if (item.isActive) sum += getRealWeight(item, subExercise?.exerciseType) * (Number(item.reps) || 0)
  303 + })
  304 + }
  305 + }
  306 + return sum
  307 +})
369 308
  309 +const totalWeight = computed(() => {
  310 + let sum = 0
  311 + if (props.type === 1 && recordList.value) {
  312 + recordList.value.forEach(item => {
  313 + sum += getRealWeight(item) * (Number(item.reps) || 0)
  314 + })
  315 + }
  316 + if (props.type === 2 && superRecordMap.value) {
  317 + for (const actionId in superRecordMap.value) {
  318 + const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)
  319 + superRecordMap.value[actionId].forEach(item => {
  320 + sum += getRealWeight(item, subExercise?.exerciseType) * (Number(item.reps) || 0)
  321 + })
  322 + }
  323 + }
  324 + return sum
  325 +})
  326 +
  327 +// ===================== 工具函数 =====================
  328 +function getRealWeight(item, subExerciseType = null) {
  329 + const userW = Number(userWeight.value) || 0
  330 + const inputW = Number(item.weight) || 0
  331 + const type = subExerciseType ?? props.actionDetail?.exerciseType
  332 +
  333 + if (type === 0) return inputW
  334 + if (type === 4) return userW + inputW
  335 + if (type === 5) return Math.max(0, userW - inputW)
  336 + return 0
370 } 337 }
371 338
372 -// 难度控制逻辑  
373 -const showDifficulty = ref(false);  
374 -const initialDifficulty = { label: '难度', color: '#ffffff' };  
375 -const currentDifficulty = ref({ ...initialDifficulty }); 339 +function superGroupSetData(setIndex) {
  340 + const firstSub = props.actionDetail?.exercises?.[0]
  341 + if (!firstSub) return {}
  342 + return superRecordMap.value[firstSub.id]?.[setIndex] || {}
  343 +}
376 344
377 -const selectDifficulty = (label, color) => {  
378 - if (currentDifficulty.value.label === label) {  
379 - currentDifficulty.value = { ...initialDifficulty };  
380 - } else {  
381 - currentDifficulty.value = { label, color }; 345 +function getSubActionData(setIndex, actionId) {
  346 + return superRecordMap.value[actionId]?.[setIndex] || {
  347 + h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
  348 + distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
382 } 349 }
383 - showDifficulty.value = false;  
384 -};  
385 -  
386 -// 点击背景关闭所有弹窗  
387 -const closeDifficulty = () => {  
388 - showDifficulty.value = false;  
389 - showTips.value = false;  
390 -}; 350 +}
391 351
392 -// 超级组数据控制  
393 -const superRecordMap = ref({})  
394 -// 超级组:总组数(计算第一个动作的组数即可)  
395 -const superTotalSets = computed(() => {  
396 - if (props.type !== 2) return 0  
397 - const exercises = props.actionDetail?.exercises || []  
398 - if (exercises.length === 0) return 0  
399 - const firstId = exercises[0].id  
400 - console.log('打印超级组的组数:', superRecordMap.value[firstId]?.length);  
401 - return superRecordMap.value[firstId]?.length || 0  
402 -}) 352 +function formatQuickTime(totalSeconds) {
  353 + const dur = dayjs.duration(totalSeconds * 1000)
  354 + const minutes = Math.floor(dur.asMinutes())
  355 + const seconds = dur.seconds()
  356 + if (minutes > 0) return `${minutes}分${seconds > 0 ? seconds + '秒' : ''}`
  357 + return `${seconds}s`
  358 +}
403 359
404 -onMounted(() => {  
405 - // 从 Pinia 加载当前 unit 的训练数据  
406 - const savedData = trainingStore.getUnitRecord(props.unitIndex);  
407 - // 恢复体重  
408 - userWeight.value = savedData.userWeight || 70;  
409 - console.log('从pinia加载的数据:', savedData); 360 +function parseQuickTime(display) {
  361 + if (!display) return 60
  362 + // 匹配 "X分Y秒" / "X分" / "Ys"
  363 + const full = display.match(/^(\d+)分(\d+)秒$/)
  364 + if (full) return parseInt(full[1]) * 60 + parseInt(full[2])
  365 + const minOnly = display.match(/^(\d+)分$/)
  366 + if (minOnly) return parseInt(minOnly[1]) * 60
  367 + const secOnly = display.match(/^(\d+)s$/)
  368 + if (secOnly) return parseInt(secOnly[1])
  369 + return 60
  370 +}
410 371
411 - // 1. 先从 Pinia 恢复已有数据 372 +function saveToStore() {
  373 + let records = {}
412 if (props.type === 1) { 374 if (props.type === 1) {
413 - // 关键:只有当前是初始默认1条,才赋值,不再覆盖watch结果  
414 - if (savedData.records?.[props.actionDetail.id]) {  
415 - recordList.value = [...savedData.records[props.actionDetail.id]]  
416 - console.log('onMounted兜底赋值', recordList.value)  
417 - } 375 + records[props.actionDetail.id] = recordList.value
418 } else if (props.type === 2) { 376 } else if (props.type === 2) {
419 - if (savedData.records && Object.keys(savedData.records).length > 0) {  
420 - superRecordMap.value = { ...savedData.records }; 377 + records = superRecordMap.value
421 } 378 }
  379 + trainingStore.saveUnitRecord(props.unitIndex, { records, userWeight: userWeight.value })
  380 +}
  381 +
  382 +// ===================== UI 交互 =====================
  383 +function toggleTips() {
  384 + showTips.value = !showTips.value
  385 +}
  386 +
  387 +function closeDifficulty() {
  388 + showDifficulty.value = false
  389 + showTips.value = false
  390 +}
  391 +
  392 +function selectDifficulty(label, color) {
  393 + if (currentDifficulty.value.label === label) {
  394 + currentDifficulty.value = { ...initialDifficulty }
  395 + } else {
  396 + currentDifficulty.value = { label, color }
422 } 397 }
423 - // 2. 超级组兜底初始化:只要有子动作,且 superRecordMap 为空就初始化  
424 - if (props.type === 2 && props.actionDetail?.exercises?.length) {  
425 - const exercises = props.actionDetail.exercises;  
426 - // 判断:当前超级组还没任何结构 才初始化  
427 - const firstId = exercises[0].id;  
428 - if (!superRecordMap.value[firstId]) {  
429 - const map = {};  
430 - exercises.forEach(item => {  
431 - map[item.id] = [{  
432 - h: '00', m: '00', s: '00', quickTimeDisplay: '60s',  
433 - distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false  
434 - }];  
435 - });  
436 - superRecordMap.value = map; 398 + showDifficulty.value = false
  399 +}
  400 +
  401 +function toggleActive(index) {
  402 + const wasActive = recordList.value[index].isActive
  403 + recordList.value[index].isActive = !recordList.value[index].isActive
  404 + const hasAnyActive = recordList.value.some(item => item.isActive)
  405 + emit('updateUnitActive', hasAnyActive)
  406 + if (recordList.value[index].isActive) {
  407 + const seconds = parseQuickTime(recordList.value[index].quickTimeDisplay)
  408 + emit('startRestTimer', seconds, props.unitIndex)
  409 + } else if (wasActive) {
  410 + emit('cancelRestTimer', props.unitIndex)
437 } 411 }
  412 +}
  413 +
  414 +function toggleSuperSetActive(setIndex) {
  415 + const wasActive = superGroupSetData(setIndex).isActive
  416 + props.actionDetail.exercises.forEach(sub => {
  417 + const list = superRecordMap.value[sub.id]
  418 + if (list?.[setIndex]) list[setIndex].isActive = !list[setIndex].isActive
  419 + })
  420 + const hasAnyActive = props.actionDetail.exercises.some(sub =>
  421 + superRecordMap.value[sub.id]?.some(item => item.isActive)
  422 + )
  423 + emit('updateUnitActive', hasAnyActive)
  424 + if (!wasActive) {
  425 + const restSeconds = parseQuickTime(superGroupSetData(setIndex).quickTimeDisplay)
  426 + emit('startRestTimer', restSeconds, props.unitIndex)
  427 + } else {
  428 + emit('cancelRestTimer', props.unitIndex)
438 } 429 }
  430 +}
439 431
  432 +// 供父组件调用:取消该组件内所有勾选
  433 +function resetActiveSets() {
  434 + recordList.value.forEach(item => { item.isActive = false })
  435 + for (const key in superRecordMap.value) {
  436 + superRecordMap.value[key]?.forEach(item => { item.isActive = false })
  437 + }
  438 + emit('updateUnitActive', false)
  439 +}
  440 +
  441 +// ===================== 体重选择器 =====================
  442 +function openWeightPicker() {
  443 + const num = userWeight.value
  444 + const intIndex = Math.max(0, Math.min(Math.floor(num) - 10, 240))
  445 + const decPart = Math.round((num - Math.floor(num)) * 10)
  446 + defaultWeightIndex.value = [intIndex, decPart, 0]
  447 + showWeightPicker.value = true
  448 +}
  449 +
  450 +function onWeightConfirm(e) {
  451 + userWeight.value = parseFloat(`${e.value[0]}.${e.value[1]}`)
  452 + showWeightPicker.value = false
  453 +}
  454 +
  455 +// ===================== 备注 =====================
  456 +function openBeizhu() {
440 nextTick(() => { 457 nextTick(() => {
441 - emit('register', props.index, {  
442 - recordList: recordList.value,  
443 - superRecordMap: superRecordMap.value  
444 - }) 458 + if (showBeizhuRef.value) showBeizhuRef.value.open(props.actionDetail.id)
445 }) 459 })
446 -});  
447 -// 动作组数据记录  
448 -const recordList = ref([  
449 - { h: '00', m: '00', s: '00', quickTimeDisplay: '60s', distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false },  
450 -]);  
451 -  
452 -const saveToStore = () => {  
453 - let records = {};  
454 - if (props.type === 1) {  
455 - // 普通动作  
456 - records[props.actionDetail.id] = recordList.value;  
457 - } else if (props.type === 2) {  
458 - // 超级组  
459 - records = superRecordMap.value; 460 +}
  461 +
  462 +async function handleNoteSave(content) {
  463 + try {
  464 + if (props.type == 2) {
  465 + await SupersetsApi.addNotes({ supersetsId: props.id, content })
  466 + } else {
  467 + await ExercisesApi.addNotes({ exerciseId: props.id, content })
460 } 468 }
461 - // 调用 Pinia 保存  
462 - trainingStore.saveUnitRecord(props.unitIndex, {  
463 - records: records,  
464 - userWeight: userWeight.value  
465 - });  
466 -}; 469 + props.actionDetail.userNote = content
  470 + } catch (e) {
  471 + // 静默处理
  472 + }
  473 +}
467 474
468 -watch(recordList, () => {  
469 - // console.log('recordList变化,发送长度:', recordList.value.length)  
470 - emit('update:groupCount', recordList.value.length)  
471 -}, { deep: true, immediate: true }) 475 +// ===================== 替换/删除动作 =====================
  476 +function handleReplaceAction() {
  477 + showTips.value = false
  478 + replacePopupShow.value = true
  479 +}
472 480
473 -watch([recordList, superRecordMap, userWeight], () => {  
474 - saveToStore();  
475 - console.log('+++++++++打印全局训练数据 trainingStore.unitRecords++++++++', trainingStore.unitRecords);  
476 -}, { deep: true, immediate: false }); 481 +function deleteAction() {
  482 + if (props.isDailyTemplates) {
  483 + showTips.value = false
  484 + emit('deleteAction')
  485 + return
  486 + }
  487 + trainingStore.deleteUnitRecord(props.unitIndex)
  488 + deleteActionShow.value = true
  489 + if (trainingStore.type === 3) {
  490 + trainingStore.actionDetail.units.splice(props.unitIndex, 1)
  491 + } else {
  492 + trainingStore.clearTrainingStore()
  493 + }
  494 + showTips.value = false
  495 +}
477 496
478 -const toggleActive = (index) => {  
479 - recordList.value[index].isActive = !recordList.value[index].isActive;  
480 -}; 497 +// ===================== 时间选择器 =====================
  498 +function openPicker(index) {
  499 + activeIndex.value = index
  500 + const item = recordList.value[index]
  501 + defaultTimeIndex.value = [
  502 + timeColumns[0].indexOf(item.h + '时'),
  503 + timeColumns[1].indexOf(item.m + '分'),
  504 + timeColumns[2].indexOf(item.s + '秒')
  505 + ]
  506 + showPicker.value = true
  507 +}
481 508
482 -// 超级组工具方法  
483 -const superGroupSetData = (setIndex) => {  
484 - const firstSub = props.actionDetail?.exercises?.[0]  
485 - if (!firstSub) return {}  
486 - return superRecordMap.value[firstSub.id]?.[setIndex] || {} 509 +function openQuickTimer(index) {
  510 + activeIndex.value = index
  511 + showQuickPicker.value = true
487 } 512 }
488 -const getSubActionData = (setIndex, actionId) => {  
489 - return superRecordMap.value[actionId]?.[setIndex] || {  
490 - h: '00', m: '00', s: '00',  
491 - quickTimeDisplay: '60s',  
492 - distance: '',  
493 - weight: '',  
494 - reps: '',  
495 - duration: '',  
496 - restTime: '',  
497 - isActive: false  
498 - } 513 +
  514 +function openAllQuickTimer() {
  515 + restEditMode.value = 'all'
  516 + showQuickPicker.value = true
499 } 517 }
500 518
501 -const openSuperPicker = (setIndex, actionId) => { 519 +function openSuperPicker(setIndex, actionId) {
502 activeIndex.value = setIndex 520 activeIndex.value = setIndex
503 - activeActionId.value = actionId; // 保存当前动作ID 521 + activeActionId.value = actionId
504 showPicker.value = true 522 showPicker.value = true
505 } 523 }
506 -const openSuperQuickTimer = (setIndex) => { 524 +
  525 +function openSuperQuickTimer(setIndex) {
507 activeIndex.value = setIndex 526 activeIndex.value = setIndex
508 showQuickPicker.value = true 527 showQuickPicker.value = true
509 } 528 }
510 -const toggleSuperSetActive = (setIndex) => {  
511 - props.actionDetail.exercises.forEach(sub => {  
512 - const list = superRecordMap.value[sub.id]  
513 - if (list?.[setIndex]) {  
514 - list[setIndex].isActive = !list[setIndex].isActive  
515 - }  
516 - })  
517 -}  
518 529
519 -// 合并后的 Picker 逻辑  
520 -const timeColumns = reactive([  
521 - Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'),  
522 - Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),  
523 - Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),  
524 -]);  
525 -const defaultTimeIndex = ref([0, 0, 0]);  
526 -const quickColumns = reactive([  
527 - Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),  
528 - Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),  
529 -]);  
530 -  
531 -const openPicker = (index) => {  
532 - activeIndex.value = index;  
533 - const item = recordList.value[index];  
534 - const hIdx = timeColumns[0].indexOf(item.h + '时');  
535 - const mIdx = timeColumns[1].indexOf(item.m + '分');  
536 - const sIdx = timeColumns[2].indexOf(item.s + '秒');  
537 - defaultTimeIndex.value = [hIdx, mIdx, sIdx];  
538 - showPicker.value = true;  
539 -};  
540 -//  
541 -const openQuickTimer = (index) => {  
542 - activeIndex.value = index;  
543 - showQuickPicker.value = true;  
544 -};  
545 -// 组间休息  
546 -const openAllQuickTimer = () => {  
547 - restEditMode.value = 'all'; // 重点:标记为全部修改  
548 - showQuickPicker.value = true;  
549 -};  
550 -  
551 -const onTimeConfirm = (e) => {  
552 - const rawValues = e.value.map((val) => String(val).replace(/[时分秒]/g, '')) 530 +function onTimeConfirm(e) {
  531 + const rawValues = e.value.map(val => String(val).replace(/[时分秒]/g, ''))
553 const setIdx = activeIndex.value 532 const setIdx = activeIndex.value
554 533
555 if (props.type === 1) { 534 if (props.type === 1) {
556 const item = recordList.value[setIdx] 535 const item = recordList.value[setIdx]
557 - item.h = rawValues[0]  
558 - item.m = rawValues[1]  
559 - item.s = rawValues[2] 536 + item.h = rawValues[0]; item.m = rawValues[1]; item.s = rawValues[2]
560 } else if (props.type === 2) { 537 } else if (props.type === 2) {
561 - // 只修改当前点击的动作,不批量修改所有动作!  
562 - const actionId = activeActionId.value;  
563 - const list = superRecordMap.value[actionId]; 538 + const list = superRecordMap.value[activeActionId.value]
564 if (list?.[setIdx]) { 539 if (list?.[setIdx]) {
565 - list[setIdx].h = rawValues[0];  
566 - list[setIdx].m = rawValues[1];  
567 - list[setIdx].s = rawValues[2]; 540 + list[setIdx].h = rawValues[0]; list[setIdx].m = rawValues[1]; list[setIdx].s = rawValues[2]
568 } 541 }
569 } 542 }
570 showPicker.value = false 543 showPicker.value = false
571 } 544 }
572 545
573 -const onQuickConfirm = (e) => {  
574 - const min = parseInt(e.value[0]) || 0  
575 - const sec = parseInt(e.value[1]) || 0  
576 - const totalSeconds = min * 60 + sec 546 +function onQuickConfirm(e) {
  547 + const totalSeconds = dayjs.duration({
  548 + minutes: parseInt(e.value[0]) || 0,
  549 + seconds: parseInt(e.value[1]) || 0
  550 + }).asSeconds()
577 const setIdx = activeIndex.value 551 const setIdx = activeIndex.value
578 552
579 - // ============== 统一设置所有组 ==============  
580 if (restEditMode.value === 'all') { 553 if (restEditMode.value === 'all') {
581 - // 普通动作  
582 if (props.type === 1) { 554 if (props.type === 1) {
583 - recordList.value.forEach(item => {  
584 - item.quickTimeDisplay = totalSeconds + 's'  
585 - })  
586 - }  
587 - // 超级组  
588 - else if (props.type === 2) { 555 + recordList.value.forEach(item => { item.quickTimeDisplay = formatQuickTime(totalSeconds) })
  556 + } else if (props.type === 2) {
589 const firstSub = props.actionDetail?.exercises?.[0] 557 const firstSub = props.actionDetail?.exercises?.[0]
590 - if (!firstSub) return  
591 - const allSets = superRecordMap.value[firstSub.id]  
592 - if (!allSets) return  
593 - allSets.forEach(set => {  
594 - set.quickTimeDisplay = totalSeconds + 's'  
595 - }) 558 + if (firstSub) {
  559 + superRecordMap.value[firstSub.id]?.forEach(set => { set.quickTimeDisplay = formatQuickTime(totalSeconds) })
596 } 560 }
597 } 561 }
598 -  
599 - // ============== 单行修改 ==============  
600 - else { 562 + } else {
601 if (props.type === 1) { 563 if (props.type === 1) {
602 - recordList.value[setIdx].quickTimeDisplay = totalSeconds + 's' 564 + recordList.value[setIdx].quickTimeDisplay = formatQuickTime(totalSeconds)
603 } else if (props.type === 2) { 565 } else if (props.type === 2) {
604 props.actionDetail.exercises.forEach(sub => { 566 props.actionDetail.exercises.forEach(sub => {
605 const list = superRecordMap.value[sub.id] 567 const list = superRecordMap.value[sub.id]
606 - if (list?.[setIdx]) {  
607 - list[setIdx].quickTimeDisplay = totalSeconds + 's'  
608 - } 568 + if (list?.[setIdx]) list[setIdx].quickTimeDisplay = formatQuickTime(totalSeconds)
609 }) 569 })
610 } 570 }
611 } 571 }
612 -  
613 restEditMode.value = 'single' 572 restEditMode.value = 'single'
614 showQuickPicker.value = false 573 showQuickPicker.value = false
615 } 574 }
616 575
617 -  
618 -// 单动作的增加/删除  
619 -const addRow = () => { 576 +// ===================== 增删行/组 =====================
  577 +function addRow() {
620 recordList.value.push({ 578 recordList.value.push({
621 - h: '00', m: '00', s: '00',  
622 - quickTimeDisplay: '60s',  
623 - distance: '', weight: '', reps: '', duration: '',  
624 - restTime: '', isActive: false,  
625 - });  
626 -};  
627 -  
628 -// 删除单动作行数,至少保留一行,提示。  
629 -const deleteRow = (index) => { 579 + h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
  580 + distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
  581 + })
  582 +}
  583 +
  584 +function deleteRow(index) {
630 if (recordList.value.length > 1) { 585 if (recordList.value.length > 1) {
631 - recordList.value.splice(index, 1); 586 + recordList.value.splice(index, 1)
632 } else { 587 } else {
633 - uni.showToast({  
634 - title: '至少保留一行数据',  
635 - icon: 'none'  
636 - }) 588 + uni.showToast({ title: '至少保留一行数据', icon: 'none' })
637 } 589 }
638 -}; 590 +}
639 591
640 -// 增加超级组的set组数  
641 -const addSuperSet = () => {  
642 - const exercises = props.actionDetail?.exercises || []  
643 - exercises.forEach(sub => { 592 +function addSuperSet() {
  593 + (props.actionDetail?.exercises || []).forEach(sub => {
644 if (superRecordMap.value[sub.id]) { 594 if (superRecordMap.value[sub.id]) {
645 superRecordMap.value[sub.id].push({ 595 superRecordMap.value[sub.id].push({
646 - h: '00', m: '00', s: '00',  
647 - quickTimeDisplay: '60s',  
648 - distance: '',  
649 - weight: '',  
650 - reps: '',  
651 - duration: '',  
652 - restTime: '',  
653 - isActive: false 596 + h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
  597 + distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
654 }) 598 })
655 } 599 }
656 }) 600 })
657 } 601 }
658 -// 删除超级组的组数  
659 -const deleteSuperSet = (setIndex) => {  
660 - const exercises = props.actionDetail?.exercises || []  
661 602
662 - // 没有子动作直接返回 603 +function deleteSuperSet(setIndex) {
  604 + const exercises = props.actionDetail?.exercises || []
663 if (!exercises.length) return 605 if (!exercises.length) return
664 - // 取第一个子动作的组数,所有子动作数组长度保持同步  
665 - const firstSubId = exercises[0].id  
666 - const list = superRecordMap.value[firstSubId] || []  
667 - // 判断只剩一组,弹窗提示并终止删除 606 + const list = superRecordMap.value[exercises[0].id] || []
668 if (list.length <= 1) { 607 if (list.length <= 1) {
669 - uni.showToast({  
670 - title: '至少保留一组数据',  
671 - icon: 'none'  
672 - }) 608 + uni.showToast({ title: '至少保留一组数据', icon: 'none' })
673 return 609 return
674 } 610 }
675 -  
676 exercises.forEach(sub => { 611 exercises.forEach(sub => {
677 const list = superRecordMap.value[sub.id] 612 const list = superRecordMap.value[sub.id]
678 - if (list && list.length > 1) {  
679 - list.splice(setIndex, 1)  
680 - } 613 + if (list && list.length > 1) list.splice(setIndex, 1)
681 }) 614 })
682 } 615 }
683 616
684 -const addGroup = () => {  
685 - if (props.type === 1) {  
686 - addRow()  
687 - } else if (props.type === 2) {  
688 - addSuperSet()  
689 - } 617 +function addGroup() {
  618 + if (props.type === 1) addRow()
  619 + else if (props.type === 2) addSuperSet()
690 } 620 }
691 621
692 -// 历史  
693 -const openHistory = (id) => {  
694 - if (historyPopupRef.value && historyPopupRef.value.openHistoryPopup) {  
695 - historyPopupRef.value.openHistoryPopup(id); 622 +// ===================== 历史 =====================
  623 +function openHistory(id) {
  624 + if (historyPopupRef.value?.openHistoryPopup) {
  625 + historyPopupRef.value.openHistoryPopup(id)
696 } 626 }
697 } 627 }
698 628
699 -// 动作名称渲染  
700 -const exerciseNamesWithLabel = computed(() => {  
701 - const exercises = props.actionDetail?.exercises || []  
702 - return exercises.map((item, index) => {  
703 - const label = String.fromCharCode(65 + index)  
704 - return `${label} ${item?.name || '未知动作'}`  
705 - })  
706 -}) 629 +// ===================== Watchers =====================
  630 +watch(recordList, () => {
  631 + emit('update:groupCount', recordList.value.length)
  632 +}, { deep: true, immediate: true })
707 633
708 -// 获得真正的重量(支持普通动作 + 超级组)  
709 -const getRealWeight = (item, subExerciseType = null) => {  
710 - const userW = Number(userWeight.value) || 0  
711 - const inputW = Number(item.weight) || 0  
712 - // 重要:超级组子动作会传入自己的 exerciseType  
713 - let type = subExerciseType ?? props.actionDetail?.exerciseType  
714 -  
715 - // console.log('【重量计算】', {  
716 - // 动作类型: type,  
717 - // 体重: userW,  
718 - // 输入重量: inputW,  
719 - // 最终重量:  
720 - // type === 0 ? inputW :  
721 - // type === 4 ? userW + inputW :  
722 - // type === 5 ? userW - inputW : 0  
723 - // })  
724 - let realWeight = 0;  
725 - if (type === 0) {  
726 - realWeight = inputW;  
727 - } else if (type === 4) {  
728 - realWeight = userW + inputW;  
729 - } else if (type === 5) {  
730 - realWeight = userW - inputW;  
731 - // 只做数据兜底修正,不在这里弹提示  
732 - if (realWeight < 0) {  
733 - realWeight = 0;  
734 - }  
735 - }  
736 - return realWeight;  
737 -} 634 +watch([recordList, superRecordMap, userWeight], saveToStore, { deep: true })
738 635
739 -// 只暴露 isActive = true 的数据  
740 -const exposeRecordList = computed(() => {  
741 - return recordList.value.filter(item => item.isActive).map(item => {  
742 - return {  
743 - ...item,  
744 - weight: getRealWeight(item) // 👈 自动计算真实重量  
745 - }  
746 - })  
747 -})  
748 -  
749 -const exposeSuperRecordMap = computed(() => {  
750 - const map = {}  
751 - for (const key in superRecordMap.value) {  
752 - // 找到当前子动作的类型  
753 - const subExercise = props.actionDetail?.exercises?.find(s => s.id == key)  
754 - const subType = subExercise?.exerciseType  
755 - map[key] = superRecordMap.value[key]  
756 - .filter(item => item.isActive)  
757 - .map(item => {  
758 - return {  
759 - ...item,  
760 - weight: getRealWeight(item, subType) // ✅ 传入子动作类型 636 +watch(
  637 + () => [recordList.value, superRecordMap.value],
  638 + () => {
  639 + const userW = userWeight.value || 0
  640 + if (props.type === 1 && props.actionDetail?.exerciseType === 5) {
  641 + recordList.value.forEach(item => {
  642 + if ((Number(item.weight) || 0) > userW) {
  643 + item.weight = ''
  644 + uni.showToast({ title: '辅助重量不能超过体重', icon: 'none', duration: 2000 })
761 } 645 }
762 }) 646 })
763 } 647 }
764 - return map  
765 -})  
766 -// 总组数  
767 -const totalSetCount = computed(() => {  
768 - // 1. 普通动作 type === 1  
769 - if (props.type === 1) {  
770 - return recordList.value.length  
771 - }  
772 - // 2. 超级组 type === 2  
773 if (props.type === 2) { 648 if (props.type === 2) {
774 - const exercises = props.actionDetail?.exercises || []  
775 - if (exercises.length === 0) return 0  
776 - const firstId = exercises[0].id  
777 - return superRecordMap.value[firstId]?.length || 0 649 + props.actionDetail?.exercises?.forEach(sub => {
  650 + if (sub.exerciseType === 5) {
  651 + (superRecordMap.value[sub.id] || []).forEach(item => {
  652 + if ((Number(item.weight) || 0) > userW) {
  653 + item.weight = ''
  654 + uni.showToast({ title: '辅助重量不能超过体重', icon: 'none', duration: 2000 })
778 } 655 }
779 - return 0  
780 -})  
781 -  
782 -// 重量  
783 -// 1. 已勾选的重量总和(黄色框框)→ 现在 = 真实重量 × 次数  
784 -const checkedWeight = computed(() => {  
785 - let sum = 0  
786 - // 普通动作  
787 - if (props.type === 1 && recordList.value) {  
788 - recordList.value.forEach(item => {  
789 - if (item.isActive) {  
790 - const realWeight = getRealWeight(item) // 👈 真实重量(自重/加重/减重)  
791 - const reps = Number(item.reps) || 0 // 👈 次数  
792 - sum += realWeight * reps // 👈 重量 × 次数 656 + })
793 } 657 }
794 }) 658 })
795 } 659 }
796 - // 超级组  
797 - if (props.type === 2 && superRecordMap.value) {  
798 - for (const actionId in superRecordMap.value) {  
799 - // 拿到子动作类型  
800 - const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)  
801 - const subType = subExercise?.exerciseType 660 + },
  661 + { deep: true }
  662 +)
802 663
803 - superRecordMap.value[actionId].forEach(item => {  
804 - if (item.isActive) {  
805 - const realWeight = getRealWeight(item, subType)  
806 - const reps = Number(item.reps) || 0  
807 - sum += realWeight * reps 664 +// ===================== Lifecycle =====================
  665 +onMounted(() => {
  666 + const savedData = trainingStore.getUnitRecord(props.unitIndex)
  667 + userWeight.value = savedData.userWeight || 70
  668 +
  669 + if (props.type === 1) {
  670 + if (savedData.records?.[props.actionDetail.id]) {
  671 + recordList.value = [...savedData.records[props.actionDetail.id]]
808 } 672 }
809 - }) 673 + } else if (props.type === 2) {
  674 + if (savedData.records && Object.keys(savedData.records).length > 0) {
  675 + superRecordMap.value = { ...savedData.records }
810 } 676 }
811 } 677 }
812 - return sum  
813 -})  
814 678
815 -// 2. 所有组的重量总和(不管勾没勾)→ 现在 = 真实重量 × 次数  
816 -const totalWeight = computed(() => {  
817 - let sum = 0  
818 - if (props.type === 1 && recordList.value) {  
819 - recordList.value.forEach(item => {  
820 - const realWeight = getRealWeight(item)  
821 - const reps = Number(item.reps) || 0  
822 - sum += realWeight * reps 679 + // 超级组兜底初始化
  680 + if (props.type === 2 && props.actionDetail?.exercises?.length) {
  681 + const exercises = props.actionDetail.exercises
  682 + if (!superRecordMap.value[exercises[0].id]) {
  683 + const map = {}
  684 + exercises.forEach(item => {
  685 + map[item.id] = [{
  686 + h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
  687 + distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
  688 + }]
823 }) 689 })
  690 + superRecordMap.value = map
  691 + }
824 } 692 }
825 - if (props.type === 2 && superRecordMap.value) {  
826 - for (const actionId in superRecordMap.value) {  
827 - const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)  
828 - const subType = subExercise?.exerciseType  
829 693
830 - superRecordMap.value[actionId].forEach(item => {  
831 - const realWeight = getRealWeight(item, subType)  
832 - const reps = Number(item.reps) || 0  
833 - sum += realWeight * reps 694 + nextTick(() => {
  695 + emit('register', props.index, {
  696 + recordList: recordList.value,
  697 + superRecordMap: superRecordMap.value
  698 + })
834 }) 699 })
835 - }  
836 - }  
837 - return sum  
838 }) 700 })
839 701
840 -// ==============================================  
841 -// 统一监听:普通动作 + 超级组 的自重减重(type=5)  
842 -// ==============================================  
843 -watch(  
844 - () => [recordList.value, superRecordMap.value],  
845 - () => {  
846 - const userW = userWeight.value || 0;  
847 -  
848 - // --------------------------  
849 - // 1. 监听【普通动作】  
850 - // --------------------------  
851 - if (props.type === 1 && props.actionDetail?.exerciseType === 5) {  
852 - recordList.value.forEach((item) => {  
853 - const inputW = Number(item.weight) || 0;  
854 - if (inputW > userW) {  
855 - item.weight = "";  
856 - uni.showToast({  
857 - title: "辅助重量不能超过体重",  
858 - icon: "none",  
859 - duration: 2000,  
860 - });  
861 - }  
862 - });  
863 - }  
864 -  
865 - // --------------------------  
866 - // 2. 监听【超级组】(包含多个子动作)  
867 - // --------------------------  
868 - if (props.type === 2) {  
869 - // 遍历超级组里的所有子动作  
870 - props.actionDetail?.exercises?.forEach((sub) => {  
871 - // 只校验 自重减重 type=5  
872 - if (sub.exerciseType === 5) {  
873 - const sets = superRecordMap.value[sub.id] || [];  
874 - sets.forEach((item) => {  
875 - const inputW = Number(item.weight) || 0;  
876 - if (inputW > userW) {  
877 - item.weight = "";  
878 - uni.showToast({  
879 - title: "辅助重量不能超过体重",  
880 - icon: "none",  
881 - duration: 2000,  
882 - });  
883 - }  
884 - });  
885 - }  
886 - });  
887 - }  
888 - },  
889 - { deep: true }  
890 -);  
891 - 702 +// ===================== Expose =====================
892 defineExpose({ 703 defineExpose({
893 - recordList: exposeRecordList, // 过滤后  
894 - superRecordMap: exposeSuperRecordMap, // 过滤后  
895 - totalSetCount, //全部的组数  
896 - checkedWeight, // 已勾选重量  
897 - totalWeight // 全部重量 704 + recordList: exposeRecordList,
  705 + superRecordMap: exposeSuperRecordMap,
  706 + totalSetCount,
  707 + checkedWeight,
  708 + totalWeight,
  709 + resetActiveSets
898 }) 710 })
  711 +</script>
899 712
900 -// 打开备注弹窗  
901 -// const openBeizhu = () => {  
902 -// console.log('传递到备注组件的id', actionId.value)  
903 -// showBeizhuRef.value.open(actionId.value)  
904 -// } 713 +<style lang="scss" scoped>
  714 +// ===================== 变量 =====================
  715 +$bg-black: #303030;
  716 +$card-bg: #1c1c1e;
  717 +$item-bg: #2c2c2e;
  718 +$input-bg: #1f1f1f;
  719 +$popup-bg: #3e3e3e;
  720 +$text-main: #ffffff;
  721 +$text-gray: #8e8e93;
  722 +$text-dim: #929292;
  723 +$theme-yellow: #f8d714;
905 724
906 -watch(() => props.actionDetail, () => {  
907 - console.log('actionDetail.exercises:', props.actionDetail?.exercises)  
908 -}, { immediate: true }) 725 +// ===================== 公共:序号框 =====================
  726 +@mixin index-box {
  727 + width: 50rpx;
  728 + height: 70rpx;
  729 + background: $input-bg;
  730 + border-radius: 10rpx;
  731 + display: flex;
  732 + align-items: center;
  733 + justify-content: center;
  734 + font-size: 30rpx;
  735 + color: $text-main;
  736 +}
909 737
910 -</script> 738 +// ===================== 公共:计时选择器 =====================
  739 +@mixin timer-selector {
  740 + width: 75rpx;
  741 + height: 75rpx;
  742 + display: flex;
  743 + flex-direction: column;
  744 + align-items: center;
  745 + justify-content: center;
  746 + border-radius: 50%;
  747 + border: 1rpx solid #a4a4a4;
  748 +
  749 + .text {
  750 + font-size: 20rpx;
  751 + color: $text-gray;
  752 + }
  753 +}
  754 +
  755 +// ===================== 公共:操作图标按钮 =====================
  756 +@mixin action-icon-base {
  757 + width: 70rpx;
  758 + height: 70rpx;
  759 + background-color: $input-bg;
  760 + border-radius: 12rpx;
  761 + display: flex;
  762 + align-items: center;
  763 + justify-content: center;
911 764
912 -<style lang="scss" scoped>  
913 -$bg-black: #303030;  
914 -$card-bg: #1c1c1e;  
915 -$item-bg: #2c2c2e;  
916 -$text-main: #ffffff;  
917 -$text-gray: #8e8e93;  
918 -$theme-yellow: #f8d714; 765 + &.active {
  766 + background-color: $theme-yellow;
  767 + }
  768 +}
919 769
  770 +// ===================== 容器 =====================
920 .action-container { 771 .action-container {
921 width: 100%; 772 width: 100%;
922 - /* 确保容器足够高以接收点击事件 */  
923 background-color: $bg-black; 773 background-color: $bg-black;
924 774
  775 +
  776 +
925 .action-list { 777 .action-list {
926 width: 100%; 778 width: 100%;
927 779
@@ -930,6 +782,10 @@ $theme-yellow: #f8d714; @@ -930,6 +782,10 @@ $theme-yellow: #f8d714;
930 margin-bottom: 20rpx; 782 margin-bottom: 20rpx;
931 padding-bottom: 20rpx; 783 padding-bottom: 20rpx;
932 784
  785 + &.unit-active {
  786 + background-color: #4c4d3b;
  787 + }
  788 +
933 .card-top { 789 .card-top {
934 display: flex; 790 display: flex;
935 align-items: center; 791 align-items: center;
@@ -992,7 +848,7 @@ $theme-yellow: #f8d714; @@ -992,7 +848,7 @@ $theme-yellow: #f8d714;
992 .tips { 848 .tips {
993 position: absolute; 849 position: absolute;
994 color: #fff; 850 color: #fff;
995 - background-color: #3e3e3e; 851 + background-color: $popup-bg;
996 z-index: 6; 852 z-index: 6;
997 right: 10rpx; 853 right: 10rpx;
998 top: 20rpx; 854 top: 20rpx;
@@ -1029,18 +885,6 @@ $theme-yellow: #f8d714; @@ -1029,18 +885,6 @@ $theme-yellow: #f8d714;
1029 } 885 }
1030 } 886 }
1031 887
1032 - // .userWeight-values {  
1033 - // display: flex;  
1034 - // align-items: center;  
1035 - // gap: 8rpx; // 控制“体重文本”和“编辑图标”的间距  
1036 - // padding: 8rpx;  
1037 -  
1038 - // .weight-text {  
1039 - // color: #fadc00;  
1040 - // font-size: 20rpx;  
1041 - // }  
1042 - // }  
1043 -  
1044 .userWeight { 888 .userWeight {
1045 padding: 16rpx 32rpx; 889 padding: 16rpx 32rpx;
1046 gap: 16rpx; 890 gap: 16rpx;
@@ -1059,7 +903,7 @@ $theme-yellow: #f8d714; @@ -1059,7 +903,7 @@ $theme-yellow: #f8d714;
1059 .userWeight-values { 903 .userWeight-values {
1060 display: flex; 904 display: flex;
1061 align-items: center; 905 align-items: center;
1062 - gap: 8rpx; // 控制“体重文本”和“编辑图标”的间距 906 + gap: 8rpx;
1063 padding: 8rpx; 907 padding: 8rpx;
1064 908
1065 .weight-text { 909 .weight-text {
@@ -1071,242 +915,16 @@ $theme-yellow: #f8d714; @@ -1071,242 +915,16 @@ $theme-yellow: #f8d714;
1071 915
1072 .explain { 916 .explain {
1073 font-size: 20rpx; 917 font-size: 20rpx;
1074 - color: #fff;  
1075 - }  
1076 - }  
1077 -  
1078 - .record-row {  
1079 - display: flex;  
1080 - align-items: flex-start;  
1081 - justify-content: space-between;  
1082 - padding: 10rpx 30rpx;  
1083 - box-sizing: border-box;  
1084 -  
1085 - &.active {  
1086 - background-color: #4c4d3b;  
1087 - }  
1088 -  
1089 - .left {  
1090 - display: flex;  
1091 - flex-direction: column;  
1092 - gap: 10rpx;  
1093 -  
1094 - .left-header {  
1095 - display: flex;  
1096 - align-items: center;  
1097 - gap: 20rpx;  
1098 -  
1099 - .index-box {  
1100 - width: 50rpx;  
1101 - height: 70rpx;  
1102 - background: #1f1f1f;  
1103 - border-radius: 10rpx;  
1104 - display: flex;  
1105 - align-items: center;  
1106 - justify-content: center;  
1107 - font-size: 30rpx;  
1108 - color: $text-main;  
1109 - }  
1110 -  
1111 - .input-col,  
1112 - .distance {  
1113 - height: 70rpx;  
1114 - background: #1f1f1f;  
1115 - border-radius: 10rpx;  
1116 - padding: 5rpx 10rpx;  
1117 - display: flex;  
1118 - flex-direction: column;  
1119 - justify-content: center;  
1120 -  
1121 - .label {  
1122 - font-size: 15rpx;  
1123 - color: $text-gray;  
1124 - margin-bottom: 2rpx;  
1125 - }  
1126 -  
1127 - .values {  
1128 - display: flex;  
1129 - align-items: baseline;  
1130 -  
1131 - .num {  
1132 - font-size: 32rpx;  
1133 - color: $text-main;  
1134 - }  
1135 -  
1136 - .unit {  
1137 - font-size: 15rpx;  
1138 - color: $text-gray;  
1139 - margin: 0 5rpx;  
1140 - }  
1141 - }  
1142 - }  
1143 -  
1144 - .distance {  
1145 - min-width: 100rpx;  
1146 -  
1147 - :deep(.u-input) {  
1148 - padding: 0 !important;  
1149 - }  
1150 -  
1151 - .u-input__content__field {  
1152 - font-size: 32rpx !important;  
1153 - color: $text-main !important;  
1154 - }  
1155 - }  
1156 -  
1157 - .timer-selector {  
1158 - width: 75rpx;  
1159 - height: 75rpx;  
1160 - display: flex;  
1161 - flex-direction: column;  
1162 - align-items: center;  
1163 - justify-content: center;  
1164 - border-radius: 50%;  
1165 - border: 1rpx solid #a4a4a4;  
1166 -  
1167 - .text {  
1168 - font-size: 20rpx;  
1169 - color: $text-gray;  
1170 - }  
1171 - }  
1172 - }  
1173 -  
1174 - .exercise-name-list {  
1175 - display: flex;  
1176 - flex-direction: column;  
1177 - gap: 8rpx;  
1178 - flex: 1;  
1179 - }  
1180 -  
1181 - .exercise-name {  
1182 - font-size: 26rpx;  
1183 - color: $text-main;  
1184 - line-height: 1.2;  
1185 - }  
1186 -  
1187 - .goal-list {  
1188 - width: 100%;  
1189 - margin-top: 10rpx;  
1190 -  
1191 - .goal-item {  
1192 - display: flex;  
1193 - align-items: center;  
1194 - gap: 20rpx;  
1195 -  
1196 - border-radius: 10rpx;  
1197 -  
1198 - .serial {  
1199 - width: 50rpx;  
1200 - height: 70rpx;  
1201 - display: flex;  
1202 - align-items: center;  
1203 - justify-content: center;  
1204 - font-size: 30rpx;  
1205 - color: #929292;  
1206 - font-weight: bold;  
1207 - }  
1208 -  
1209 - .goal-input-box {  
1210 - display: flex;  
1211 - flex-direction: column;  
1212 - justify-content: center;  
1213 - background-color: #1f1f1f;  
1214 - width: 100rpx;  
1215 - height: 70rpx;  
1216 - border-radius: 10rpx;  
1217 -  
1218 - .values {  
1219 - display: flex;  
1220 - align-items: flex-end;  
1221 - padding-left: 10rpx;  
1222 - box-sizing: border-box;  
1223 -  
1224 - :deep(.uni-input-input[type='number']) {  
1225 - text-align: center;  
1226 - }  
1227 -  
1228 - .num {  
1229 - font-size: 32rpx;  
1230 color: $text-main; 918 color: $text-main;
1231 } 919 }
1232 -  
1233 - .unit {  
1234 - font-size: 15rpx;  
1235 - color: $text-gray;  
1236 - margin: 0 5rpx;  
1237 - }  
1238 -  
1239 - // 确保 up-input 内部文字样式一致  
1240 - }  
1241 - }  
1242 -  
1243 - }  
1244 -  
1245 - // 新增的间歇运动样式  
1246 - .interval {  
1247 - display: flex;  
1248 - gap: 20rpx;  
1249 - margin-top: 10rpx;  
1250 -  
1251 - .goal-input-box {  
1252 - flex: none;  
1253 - width: 120rpx;  
1254 -  
1255 - .label {  
1256 - font-size: 15rpx;  
1257 - color: $text-gray;  
1258 - margin-bottom: 2rpx;  
1259 - }  
1260 - }  
1261 - }  
1262 - }  
1263 - }  
1264 -  
1265 - .icons {  
1266 - display: flex;  
1267 - gap: 20rpx;  
1268 -  
1269 - .icon {  
1270 - width: 70rpx;  
1271 - height: 70rpx;  
1272 - background-color: #1f1f1f;  
1273 - border-radius: 12rpx;  
1274 - display: flex;  
1275 - align-items: center;  
1276 - justify-content: center;  
1277 -  
1278 - &.active {  
1279 - background-color: $theme-yellow;  
1280 - }  
1281 - }  
1282 -  
1283 - .del {  
1284 - background-color: #58372d;  
1285 - }  
1286 - }  
1287 - }  
1288 } 920 }
1289 921
1290 - // 新增超级组样式  
1291 .super-group { 922 .super-group {
1292 width: 100%; 923 width: 100%;
1293 } 924 }
1294 -  
1295 - .super-record-row {  
1296 - align-items: center;  
1297 - background-color: red;  
1298 - min-height: 100rpx;  
1299 - }  
1300 -  
1301 - .super-left {  
1302 - flex-direction: row !important;  
1303 - align-items: center;  
1304 - gap: 20rpx;  
1305 } 925 }
1306 926
1307 -  
1308 -  
1309 - 927 + // 超级组:每组标题行
1310 .set-group { 928 .set-group {
1311 margin-bottom: 30rpx; 929 margin-bottom: 30rpx;
1312 } 930 }
@@ -1317,35 +935,8 @@ $theme-yellow: #f8d714; @@ -1317,35 +935,8 @@ $theme-yellow: #f8d714;
1317 padding: 10rpx 30rpx; 935 padding: 10rpx 30rpx;
1318 gap: 20rpx; 936 gap: 20rpx;
1319 937
1320 - .icons {  
1321 - display: flex;  
1322 - gap: 20rpx;  
1323 -  
1324 - .icon {  
1325 - width: 70rpx;  
1326 - height: 70rpx;  
1327 - background-color: #1f1f1f;  
1328 - border-radius: 12rpx;  
1329 - display: flex;  
1330 - align-items: center;  
1331 - justify-content: center;  
1332 - }  
1333 -  
1334 - .del {  
1335 - background-color: #58372d;  
1336 - }  
1337 - }  
1338 -  
1339 .index-box { 938 .index-box {
1340 - width: 50rpx;  
1341 - height: 70rpx;  
1342 - background: #1f1f1f;  
1343 - border-radius: 10rpx;  
1344 - display: flex;  
1345 - align-items: center;  
1346 - justify-content: center;  
1347 - font-size: 30rpx;  
1348 - color: #fff; 939 + @include index-box;
1349 } 940 }
1350 941
1351 .set-action-names { 942 .set-action-names {
@@ -1356,73 +947,44 @@ $theme-yellow: #f8d714; @@ -1356,73 +947,44 @@ $theme-yellow: #f8d714;
1356 947
1357 text { 948 text {
1358 font-size: 26rpx; 949 font-size: 26rpx;
1359 - color: #fff; 950 + color: $text-main;
1360 } 951 }
1361 } 952 }
1362 953
1363 .timer-selector { 954 .timer-selector {
1364 - width: 75rpx;  
1365 - height: 75rpx;  
1366 - display: flex;  
1367 - flex-direction: column;  
1368 - align-items: center;  
1369 - justify-content: center;  
1370 - border-radius: 50%;  
1371 - border: 1rpx solid #a4a4a4;  
1372 -  
1373 - .text {  
1374 - font-size: 20rpx;  
1375 - color: #8e8e93; 955 + @include timer-selector;
1376 } 956 }
  957 +
  958 + .check-icon {
  959 + @include action-icon-base;
1377 } 960 }
1378 961
1379 - .icon {  
1380 - width: 70rpx;  
1381 - height: 70rpx;  
1382 - background-color: #1f1f1f;  
1383 - border-radius: 12rpx; 962 + .action-icons {
1384 display: flex; 963 display: flex;
1385 - align-items: center;  
1386 - justify-content: center; 964 + gap: 20rpx;
1387 965
1388 - &.active {  
1389 - background-color: $theme-yellow;  
1390 - }  
1391 - }  
1392 - } 966 + .action-icon {
  967 + @include action-icon-base;
1393 968
1394 - .serial {  
1395 - width: 50rpx;  
1396 - height: 70rpx;  
1397 - display: flex;  
1398 - align-items: center;  
1399 - justify-content: center;  
1400 - font-size: 30rpx;  
1401 - color: #929292;  
1402 - font-weight: bold; 969 + &.icon-del {
  970 + background-color: #58372d;
  971 + }
1403 } 972 }
1404 -  
1405 - .super-action-item {  
1406 - margin-bottom: 30rpx;  
1407 } 973 }
1408 -  
1409 - .super-action-title {  
1410 - font-size: 28rpx;  
1411 - color: #fff;  
1412 - padding: 10rpx 30rpx;  
1413 - font-weight: bold;  
1414 } 974 }
1415 975
  976 + // 按钮组
1416 .button-group { 977 .button-group {
1417 display: flex; 978 display: flex;
1418 justify-content: space-between; 979 justify-content: space-between;
1419 padding: 30rpx; 980 padding: 30rpx;
1420 box-sizing: border-box; 981 box-sizing: border-box;
  982 + gap: 10rpx;
1421 983
1422 .btn-item { 984 .btn-item {
1423 - background-color: #1f1f1f; 985 + background-color: $input-bg;
1424 font-size: 26rpx; 986 font-size: 26rpx;
1425 - color: #fff; 987 + color: $text-main;
1426 padding: 15rpx 35rpx; 988 padding: 15rpx 35rpx;
1427 border-radius: 50rpx; 989 border-radius: 50rpx;
1428 display: flex; 990 display: flex;
@@ -1435,7 +997,7 @@ $theme-yellow: #f8d714; @@ -1435,7 +997,7 @@ $theme-yellow: #f8d714;
1435 bottom: 80rpx; 997 bottom: 80rpx;
1436 left: 50%; 998 left: 50%;
1437 transform: translateX(-50%); 999 transform: translateX(-50%);
1438 - background-color: #3e3e3e; 1000 + background-color: $popup-bg;
1439 border-radius: 16rpx; 1001 border-radius: 16rpx;
1440 overflow: hidden; 1002 overflow: hidden;
1441 z-index: 99; 1003 z-index: 99;
@@ -1465,6 +1027,5 @@ $theme-yellow: #f8d714; @@ -1465,6 +1027,5 @@ $theme-yellow: #f8d714;
1465 } 1027 }
1466 } 1028 }
1467 } 1029 }
1468 -  
1469 } 1030 }
1470 </style> 1031 </style>
1 <template> 1 <template>
2 <!-- 超级组 子动作输入框 独立组件 --> 2 <!-- 超级组 子动作输入框 独立组件 -->
3 - <view class="record-row super-record-row"> 3 + <view class="record-row super-record-row" :class="{ active: data.isActive }">
4 <view class="left super-left"> 4 <view class="left super-left">
5 <view class="left-header super-left-header"> 5 <view class="left-header super-left-header">
6 6
@@ -59,7 +59,7 @@ @@ -59,7 +59,7 @@
59 <up-icon name="more-dot-fill" size="24" color="#fff" /> 59 <up-icon name="more-dot-fill" size="24" color="#fff" />
60 </view> 60 </view>
61 </template> 61 </template>
62 - <!-- #ifdef H5 --> 62 + <!-- #ifdef MP-WEIXIN -->
63 <button class="share-btn" open-type="share"> 63 <button class="share-btn" open-type="share">
64 <up-icon name="share-fill" size="24" color="#fff" /> 64 <up-icon name="share-fill" size="24" color="#fff" />
65 </button> 65 </button>
@@ -154,17 +154,35 @@ @@ -154,17 +154,35 @@
154 154
155 155
156 156
157 - <view class="section"> 157 + <view class="section muscle-section">
158 <view class="section-title">训练部位</view> 158 <view class="section-title">训练部位</view>
159 - <image :src="actionDetail.urlImage" mode="widthFix" class="muscle-map" /> 159 + <view class="muscle-card">
  160 + <image
  161 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260612/muscle_back_1781252170914.png"
  162 + mode="widthFix" class="muscle-map" />
160 <view class="legend"> 163 <view class="legend">
161 <view class="legend-item"> 164 <view class="legend-item">
162 - <view class="legend-color primary"></view>  
163 - <text class="legend-text">主要部位</text> 165 + <view class="legend-dot primary"></view>
  166 + <text>主要部位</text>
164 </view> 167 </view>
165 <view class="legend-item"> 168 <view class="legend-item">
166 - <view class="legend-color secondary"></view>  
167 - <text class="legend-text">次要部位</text> 169 + <view class="legend-dot secondary"></view>
  170 + <text>次要部位</text>
  171 + </view>
  172 + </view>
  173 + </view>
  174 + <view class="muscle-tags">
  175 + <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length">
  176 + <view class="group-label primary">主要训练部位</view>
  177 + <view class="pills">
  178 + <text class="pill primary-pill" v-for="(value, index) in actionDetail.primaryMuscleNames" :key="index">{{ value }}</text>
  179 + </view>
  180 + </view>
  181 + <view class="muscle-group" v-if="actionDetail.secondaryMuscleNames?.length">
  182 + <view class="group-label secondary">次要训练部位</view>
  183 + <view class="pills">
  184 + <text class="pill secondary-pill" v-for="(value, index) in actionDetail.secondaryMuscleNames" :key="index">{{ value }}</text>
  185 + </view>
168 </view> 186 </view>
169 </view> 187 </view>
170 </view> 188 </view>
@@ -759,68 +777,103 @@ defineExpose({ open }); @@ -759,68 +777,103 @@ defineExpose({ open });
759 } 777 }
760 } 778 }
761 779
  780 + // ===================== 训练部位 =====================
  781 + .muscle-section {
  782 + .muscle-card {
  783 + position: relative;
  784 + background: #262626;
  785 + border-radius: 16rpx;
  786 + overflow: hidden;
  787 +
762 .muscle-map { 788 .muscle-map {
763 width: 100%; 789 width: 100%;
764 - border-radius: 16rpx; 790 + display: block;
765 } 791 }
766 792
767 .legend { 793 .legend {
  794 + position: absolute;
  795 + bottom: 12rpx;
  796 + right: 16rpx;
768 display: flex; 797 display: flex;
769 - justify-content: flex-end;  
770 - align-items: center;  
771 gap: 20rpx; 798 gap: 20rpx;
772 - margin-top: 20rpx;  
773 - } 799 + background: rgba(0, 0, 0, 0.65);
  800 + backdrop-filter: blur(6px);
  801 + padding: 10rpx 20rpx;
  802 + border-radius: 30rpx;
774 803
775 .legend-item { 804 .legend-item {
776 display: flex; 805 display: flex;
777 align-items: center; 806 align-items: center;
778 - gap: 10rpx; 807 + gap: 8rpx;
  808 + font-size: 22rpx;
  809 + color: #ddd;
779 } 810 }
780 811
781 - .legend-color {  
782 - width: 16rpx;  
783 - height: 16rpx; 812 + .legend-dot {
  813 + width: 14rpx;
  814 + height: 14rpx;
784 border-radius: 4rpx; 815 border-radius: 4rpx;
785 - }  
786 816
787 - .legend-color.primary { 817 + &.primary {
788 background-color: #ffd700; 818 background-color: #ffd700;
789 } 819 }
790 820
791 - .legend-color.secondary { 821 + &.secondary {
792 background-color: #999; 822 background-color: #999;
793 } 823 }
794 -  
795 - .legend-text {  
796 - font-size: 24rpx;  
797 - color: #fff; 824 + }
  825 + }
798 } 826 }
799 827
800 - .stat-card {  
801 - background: linear-gradient(135deg, #333, #222);  
802 - padding: 40rpx;  
803 - border-radius: 20rpx; 828 + .muscle-tags {
804 display: flex; 829 display: flex;
805 - flex-direction: column;  
806 - align-items: center;  
807 - border: 1rpx solid #444; 830 + gap: 20rpx;
  831 + margin-top: 20rpx;
  832 +
  833 + .muscle-group {
  834 + flex: 1;
  835 + background: #262626;
  836 + border-radius: 16rpx;
  837 + padding: 24rpx;
808 838
809 - .label { 839 + .group-label {
810 font-size: 24rpx; 840 font-size: 24rpx;
811 - color: #888; 841 + font-weight: 600;
  842 + margin-bottom: 16rpx;
  843 +
  844 + &.primary {
  845 + color: #ffd700;
812 } 846 }
813 847
814 - .value {  
815 - font-size: 60rpx;  
816 - font-weight: bold;  
817 - color: #fedc1f;  
818 - margin: 10rpx 0; 848 + &.secondary {
  849 + color: #aaa;
  850 + }
819 } 851 }
820 852
821 - .date { 853 + .pills {
  854 + display: flex;
  855 + flex-wrap: wrap;
  856 + gap: 12rpx;
  857 +
  858 + .pill {
822 font-size: 22rpx; 859 font-size: 22rpx;
823 - color: #666; 860 + padding: 8rpx 18rpx;
  861 + border-radius: 20rpx;
  862 +
  863 + &.primary-pill {
  864 + background: rgba(255, 215, 0, 0.15);
  865 + color: #ffd700;
  866 + border: 1rpx solid rgba(255, 215, 0, 0.3);
  867 + }
  868 +
  869 + &.secondary-pill {
  870 + background: rgba(170, 170, 170, 0.12);
  871 + color: #aaa;
  872 + border: 1rpx solid rgba(170, 170, 170, 0.25);
  873 + }
  874 + }
  875 + }
  876 + }
824 } 877 }
825 } 878 }
826 879
@@ -286,6 +286,7 @@ @@ -286,6 +286,7 @@
286 </template> 286 </template>
287 287
288 <script setup> 288 <script setup>
  289 +import dayjs from 'dayjs';
289 import { ref, onMounted, watch, computed, nextTick } from 'vue'; 290 import { ref, onMounted, watch, computed, nextTick } from 'vue';
290 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; 291 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
291 import TemplatesApi from '@/sheep/api/Template/Templates'; 292 import TemplatesApi from '@/sheep/api/Template/Templates';
@@ -594,19 +595,8 @@ const closeAddTrainPopup = () => { @@ -594,19 +595,8 @@ const closeAddTrainPopup = () => {
594 // }; 595 // };
595 596
596 const formatSecondsToHms = (seconds) => { 597 const formatSecondsToHms = (seconds) => {
597 - // 处理空值/非数字  
598 - if (!seconds || isNaN(seconds)) return '00:00:00'  
599 - const total = parseInt(seconds, 10)  
600 - // 计算时分秒  
601 - const h = Math.floor(total / 3600)  
602 - const m = Math.floor((total % 3600) / 60)  
603 - const s = total % 60  
604 - // 补零 + 拼接  
605 - const hour = String(h).padStart(2, '0')  
606 - const min = String(m).padStart(2, '0')  
607 - const sec = String(s).padStart(2, '0')  
608 -  
609 - return `${hour}:${min}:${sec}` 598 + if (!seconds || isNaN(seconds)) return '00:00:00';
  599 + return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss');
610 } 600 }
611 601
612 // 返回按钮逻辑 602 // 返回按钮逻辑
@@ -283,7 +283,7 @@ onMounted(() => { @@ -283,7 +283,7 @@ onMounted(() => {
283 283
284 <style lang="scss" scoped> 284 <style lang="scss" scoped>
285 .exercise-page { 285 .exercise-page {
286 - height: 87vh; 286 + height: 86vh;
287 // #ifdef H5 287 // #ifdef H5
288 height: calc(100vh - 44px); 288 height: calc(100vh - 44px);
289 // #endif 289 // #endif
@@ -373,7 +373,8 @@ onMounted(() => { @@ -373,7 +373,8 @@ onMounted(() => {
373 373
374 .left-nav { 374 .left-nav {
375 width: 180rpx; 375 width: 180rpx;
376 - height: 100%; 376 + height: 88%;
  377 +
377 background-color: #f9fafb; 378 background-color: #f9fafb;
378 border-right: 1rpx solid #f3f4f6; 379 border-right: 1rpx solid #f3f4f6;
379 380
@@ -372,10 +372,10 @@ onMounted(async () => { @@ -372,10 +372,10 @@ onMounted(async () => {
372 372
373 /* 列表自适应滚动容器 */ 373 /* 列表自适应滚动容器 */
374 .list-container { 374 .list-container {
375 - flex: 1;  
376 - height: 0;  
377 - /* 关键:接触 Flex 内容支撑限制 */  
378 - min-height: 0; 375 +
  376 + padding-bottom: 40rpx;
  377 +
  378 +
379 overflow: hidden; 379 overflow: hidden;
380 380
381 .template-scroll { 381 .template-scroll {
@@ -388,6 +388,8 @@ const selectDate = async (date) => { @@ -388,6 +388,8 @@ const selectDate = async (date) => {
388 388
389 /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */ 389 /* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
390 .date-cell { 390 .date-cell {
  391 + min-width: 0;
  392 + overflow: hidden;
391 display: flex; 393 display: flex;
392 flex-direction: column; 394 flex-direction: column;
393 align-items: center; 395 align-items: center;
@@ -404,7 +406,7 @@ const selectDate = async (date) => { @@ -404,7 +406,7 @@ const selectDate = async (date) => {
404 406
405 &.other-month { 407 &.other-month {
406 opacity: 0.5; 408 opacity: 0.5;
407 - border-color: #f1f5f9; 409 + border-color: #f9fafc;
408 } 410 }
409 411
410 &.is-today { 412 &.is-today {
@@ -24,11 +24,7 @@ @@ -24,11 +24,7 @@
24 <up-popup :show="drawerShow" mode="right" :closeable="true" @close="drawerShow = false"> 24 <up-popup :show="drawerShow" mode="right" :closeable="true" @close="drawerShow = false">
25 <view class="drawer-content" :style="{ paddingTop: PageHeight + 'px' }"> 25 <view class="drawer-content" :style="{ paddingTop: PageHeight + 'px' }">
26 <view class="user-header"> 26 <view class="user-header">
27 - <image  
28 - class="avatar"  
29 - :src="userInfo.avatar || defaultAvatar "  
30 - mode="aspectFill"  
31 - /> 27 + <image class="avatar" :src="userInfo.avatar || defaultAvatar" mode="aspectFill" />
32 <view class="user-info"> 28 <view class="user-info">
33 <text class="nickname">{{ userInfo.nickname || '-' }}</text> 29 <text class="nickname">{{ userInfo.nickname || '-' }}</text>
34 </view> 30 </view>
@@ -114,12 +110,10 @@ defineExpose({ switchTabIndex: handleTabClick }); @@ -114,12 +110,10 @@ defineExpose({ switchTabIndex: handleTabClick });
114 <style scoped lang="scss"> 110 <style scoped lang="scss">
115 .home-page { 111 .home-page {
116 width: 100%; 112 width: 100%;
117 - // #ifdef H5  
118 - height: calc(100vh - 50px);  
119 - // #endif  
120 - // #ifdef MP-WEIXIN 113 +
121 height: calc(100vh - 50px); 114 height: calc(100vh - 50px);
122 - // #endif 115 +
  116 +
123 background-color: #fff; 117 background-color: #fff;
124 118
125 .content { 119 .content {
@@ -128,7 +122,7 @@ defineExpose({ switchTabIndex: handleTabClick }); @@ -128,7 +122,7 @@ defineExpose({ switchTabIndex: handleTabClick });
128 overflow: hidden; 122 overflow: hidden;
129 padding-top: 65rpx; 123 padding-top: 65rpx;
130 // #ifdef MP-WEIXIN 124 // #ifdef MP-WEIXIN
131 - padding-top: 200rpx; 125 + padding-top: 220rpx;
132 // #endif 126 // #endif
133 } 127 }
134 } 128 }
@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 </template> 6 </template>
7 7
8 <script setup> 8 <script setup>
  9 +import dayjs from 'dayjs';
9 import useTrainingStore from '@/sheep/store/training'; 10 import useTrainingStore from '@/sheep/store/training';
10 const trainStore = useTrainingStore(); 11 const trainStore = useTrainingStore();
11 12
@@ -20,12 +21,7 @@ const goBackTrain = () => { @@ -20,12 +21,7 @@ const goBackTrain = () => {
20 }); 21 });
21 }; 22 };
22 23
23 -// 时间格式化  
24 -const formatTime = (s) => {  
25 - const m = Math.floor(s / 60).toString().padStart(2, '0');  
26 - const sec = (s % 60).toString().padStart(2, '0');  
27 - return `${m}:${sec}`;  
28 -}; 24 +const formatTime = (s) => dayjs.duration(s, 'seconds').format('mm:ss');
29 </script> 25 </script>
30 26
31 <style scoped> 27 <style scoped>
@@ -38,6 +38,7 @@ @@ -38,6 +38,7 @@
38 </template> 38 </template>
39 39
40 <script setup> 40 <script setup>
  41 +import dayjs from 'dayjs';
41 import { ref, computed } from 'vue' 42 import { ref, computed } from 'vue'
42 import TrainingApi from '@/sheep/api/Training/traininghistory' 43 import TrainingApi from '@/sheep/api/Training/traininghistory'
43 44
@@ -104,12 +105,7 @@ const formattedSetList = computed(() => { @@ -104,12 +105,7 @@ const formattedSetList = computed(() => {
104 parts.push(`${set.reps}次`) 105 parts.push(`${set.reps}次`)
105 } 106 }
106 if (set.duration != null && set.duration > 0) { 107 if (set.duration != null && set.duration > 0) {
107 - // 秒转成 HH:MM:SS  
108 - const h = Math.floor(set.duration / 3600)  
109 - const m = Math.floor((set.duration % 3600) / 60)  
110 - const s = set.duration % 60  
111 - const hh = h > 0 ? `${h}:` : ''  
112 - parts.push(`${hh}${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`) 108 + parts.push(dayjs.duration(set.duration, 'seconds').format('H:mm:ss'));
113 } 109 }
114 if (set.distance != null && set.distance !== '') { 110 if (set.distance != null && set.distance !== '') {
115 parts.push(`${set.distance}m`) 111 parts.push(`${set.distance}m`)
1 <template> 1 <template>
2 <view class="action-container" @click="closeDifficulty"> 2 <view class="action-container" @click="closeDifficulty">
  3 +
  4 +
  5 +
3 <view class="action-list"> 6 <view class="action-list">
4 <view class="card"> 7 <view class="card">
5 <view class="card-top" @click.stop="isExpanded = !isExpanded"> 8 <view class="card-top" @click.stop="isExpanded = !isExpanded">
1 <template> 1 <template>
2 - <view class="template-edit-page">  
3 - <!-- 顶部导航栏 --> 2 + <view class="template-edit-page" @click="dismissGuide">
  3 + <!-- 顶部导航 -->
4 <view class="header"> 4 <view class="header">
5 <view class="close-btn" @click="goBack"> 5 <view class="close-btn" @click="goBack">
6 - <up-icon name="close" color="#fff" size="20" bold></up-icon> 6 + <up-icon name="close" color="#fff" size="20" bold />
7 </view> 7 </view>
8 - <view class="title-area">  
9 - <view class="title-row" @click="openTitlePopup">  
10 - <view class="title-text">{{ templateTitle || '点击输入标题' }}</view>  
11 - <uni-icons type="compose" size="24" color="#fff" /> 8 +
  9 + <view class="title-area" @click="openTextPopup('title')">
  10 + <view class="title-row">
  11 + <text class="title-text">{{ templateTitle || '点击输入标题' }}</text>
  12 + <up-icon name="edit-pen" size="24" color="#fff" />
12 </view> 13 </view>
13 - <view class="stats-text">{{ (trainingStore.actionDetail?.units || []).length }}个动作 ·  
14 - {{ totalSets }}组 · {{ totalWeight }}kg</view> 14 + <text class="stats-text">
  15 + {{ unitCount }}个动作 · {{ totalSets }}组 · {{ totalWeight }}kg
  16 + </text>
15 </view> 17 </view>
16 - <view class="save-btn" :class="{  
17 - disabled: trainingStore.actionDetail?.units?.length === 0,  
18 - active: trainingStore.actionDetail?.units?.length > 0  
19 - }" @click="handleSave"> 18 +
  19 + <view
  20 + class="save-btn"
  21 + :class="{ disabled: unitCount === 0, active: unitCount > 0 }"
  22 + @click="handleSave"
  23 + >
20 保存模板 24 保存模板
21 </view> 25 </view>
22 </view> 26 </view>
23 27
  28 + <!-- 内容区 -->
24 <view class="content-area"> 29 <view class="content-area">
25 - <template v-if="trainingStore.actionDetail?.units?.length">  
26 - <view v-for="(unit, index) in trainingStore.actionDetail?.units" :key="unit.unitId">  
27 - <dongzuo :id="unit.unitId" :type="unit.unitType" :actionDetail="trainingStore.convertUnitToActionDetail(unit)"  
28 - :ref="el => setActionRef(el, index)" :unitIndex="index" /> 30 + <template v-if="unitCount">
  31 + <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 </view> 39 </view>
30 </template> 40 </template>
31 41
32 - <!-- 中间空白提示区域 -->  
33 <view v-else class="empty-area"> 42 <view v-else class="empty-area">
34 <view class="empty-text"> 43 <view class="empty-text">
35 - 尚未添加任何动作  
36 - <br /> 44 + 尚未添加任何动作<br />
37 点击最下方 + 开始添加动作 45 点击最下方 + 开始添加动作
38 </view> 46 </view>
39 </view> 47 </view>
40 </view> 48 </view>
41 49
42 - <!-- 新手引导提示气泡 -->  
43 - <view class="guide-bubble" v-if="trainingStore.actionDetail?.units?.length === 0"> 50 + <!-- 引导气泡 -->
  51 + <view v-if="showGuide && unitCount === 0" class="guide-bubble" @click.stop>
44 挑选想要的动作开始训练吧 52 挑选想要的动作开始训练吧
45 - <view class="bubble-arrow"></view> 53 + <view class="bubble-arrow" />
46 </view> 54 </view>
47 55
48 - <!-- 底部操作栏 --> 56 + <!-- 底部栏 -->
49 <view class="bottom-bar"> 57 <view class="bottom-bar">
50 <view class="bar-item" @click="openActionSort"> 58 <view class="bar-item" @click="openActionSort">
51 - <text class="item-num">{{ trainingStore.actionDetail?.units?.length }}</text> 59 + <text class="item-num">{{ unitCount }}</text>
52 <text class="item-label">动作排序</text> 60 <text class="item-label">动作排序</text>
53 </view> 61 </view>
54 - <view class="bar-item" @click="openIntroPopup">  
55 - <uni-icons type="list" size="24" color="#fff" /> 62 +
  63 + <view class="bar-item" @click="openTextPopup('intro')">
  64 + <up-icon name="list" size="24" color="#fff" />
56 <text class="item-label">模板介绍</text> 65 <text class="item-label">模板介绍</text>
57 </view> 66 </view>
  67 +
58 <view class="bar-item add-btn" @click="handleAddAction"> 68 <view class="bar-item add-btn" @click="handleAddAction">
59 - <up-icon name="plus" color="#000" size="20" bold></up-icon> 69 + <up-icon name="plus" color="#000" size="20" bold />
60 </view> 70 </view>
  71 +
61 <view class="bar-item" @click="showScenePicker = true"> 72 <view class="bar-item" @click="showScenePicker = true">
62 - <uni-icons type="gear" size="24" color="#fff" /> 73 + <up-icon name="setting" size="24" color="#fff" />
63 <text class="item-label">训练场景</text> 74 <text class="item-label">训练场景</text>
64 </view> 75 </view>
  76 +
65 <view class="bar-item" @click="handleEditCover"> 77 <view class="bar-item" @click="handleEditCover">
66 - <uni-icons type="image" size="24" color="#fff" /> 78 + <up-icon name="photo" size="24" color="#fff" />
67 <text class="item-label">编辑封面</text> 79 <text class="item-label">编辑封面</text>
68 </view> 80 </view>
69 </view> 81 </view>
70 - <!-- 模板介绍弹窗 -->  
71 - <view class="intro-popup" v-if="showIntroPopup">  
72 - <view class="popup-header">  
73 - <view class="popup-close-btn" @click="closeIntroPopup">  
74 - <up-icon name="close" color="#fff" size="20" bold></up-icon>  
75 - </view>  
76 - <view class="popup-title">模板介绍</view>  
77 - <view class="popup-save-btn" @click="saveIntro">保存</view>  
78 - </view>  
79 - <view class="popup-content">  
80 - <textarea class="intro-textarea" v-model="templateIntro" placeholder="在此处输入模板介绍" />  
81 - </view>  
82 - </view>  
83 82
84 - <!-- 模板标题弹窗 -->  
85 - <view class="intro-popup" v-if="showTitlePopup"> 83 + <!-- 文本编辑弹窗 -->
  84 + <view v-if="popupType" class="text-popup">
86 <view class="popup-header"> 85 <view class="popup-header">
87 - <view class="popup-close-btn" @click="closeTitlePopup">  
88 - <up-icon name="close" color="#fff" size="20" bold></up-icon> 86 + <view class="popup-close-btn" @click="closeTextPopup">
  87 + <up-icon name="close" color="#fff" size="20" bold />
89 </view> 88 </view>
90 - <view class="popup-title">模板标题</view>  
91 - <view class="popup-save-btn" @click="saveTemplateTitle">保存</view> 89 + <text class="popup-title">{{ popupType === 'intro' ? '模板介绍' : '模板标题' }}</text>
  90 + <view class="popup-save-btn" @click="saveTextPopup">保存</view>
92 </view> 91 </view>
93 <view class="popup-content"> 92 <view class="popup-content">
94 - <textarea class="intro-textarea" v-model="templateTitle" placeholder="请输入模板标题" /> 93 + <textarea
  94 + class="intro-textarea"
  95 + v-model="popupValue"
  96 + :placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'"
  97 + />
95 </view> 98 </view>
96 </view> 99 </view>
97 100
98 -  
99 - <up-picker :show="showScenePicker" :columns="sceneColumns" :defaultIndex="[0]" title="训练场景" title-color="#ffffff"  
100 - cancel-color="#cccccc" confirm-color="#409eff" active-color="#ffd100" @confirm="handleSceneConfirm"  
101 - @cancel="showScenePicker = false" closeOnClickOverlay />  
102 -  
103 - <!-- 动作排序组件 --> 101 + <!-- 场景选择器 -->
  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 + />
  115 +
  116 + <!-- 动作排序 / 新增 -->
104 <action-sort ref="actionSortRef" @open-add-actions="addExShow = true" /> 117 <action-sort ref="actionSortRef" @open-add-actions="addExShow = true" />
105 -  
106 - <!-- 动作新增组件 -->  
107 - <addActions :show="addExShow" @close="addExShow = false" /> 118 + <add-actions :show="addExShow" @close="addExShow = false" />
108 </view> 119 </view>
109 </template> 120 </template>
110 121
111 <script setup> 122 <script setup>
112 import { ref, computed } from 'vue'; 123 import { ref, computed } from 'vue';
113 -import { onLoad } from "@dcloudio/uni-app" 124 +import { onLoad } from '@dcloudio/uni-app';
  125 +import dayjs from 'dayjs';
  126 +import duration from 'dayjs/plugin/duration';
  127 +
114 import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue'; 128 import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
115 -import { useTrainingStore } from '@/sheep/store/trainingStore' 129 +import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue';
  130 +import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue';
  131 +import { useTrainingStore } from '@/sheep/store/trainingStore';
116 import FileApi from '@/sheep/api/infra/file'; 132 import FileApi from '@/sheep/api/infra/file';
117 -import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue'  
118 import TemplatesApi from '@/sheep/api/Template/Templates'; 133 import TemplatesApi from '@/sheep/api/Template/Templates';
119 -import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue'  
120 -  
121 -const coverImage = ref('');  
122 -const addExShow = ref(false)  
123 -const hasConverted = ref(false)  
124 -const dongzuoRefs = ref([]) // 获得子组件模板的数据  
125 134
126 -const trainingStore = useTrainingStore()  
127 -  
128 -const canSave = ref(false);  
129 -const showGuide = ref(true);  
130 -  
131 -const showIntroPopup = ref(false);  
132 -const templateIntro = ref('');  
133 -  
134 -const showTitlePopup = ref(false);  
135 -const templateTitle = ref(''); 135 +dayjs.extend(duration);
136 136
137 -const actionSortRef = ref(null) 137 +// ---- 常量 ----
  138 +const NAVIGATE_DELAY = 1500;
138 139
139 -const sceneList = ref([ 140 +const SCENE_LIST = [
140 { id: 0, title: '不限' }, 141 { id: 0, title: '不限' },
141 { id: 1, title: '健身房' }, 142 { id: 1, title: '健身房' },
142 { id: 2, title: '仅哑铃' }, 143 { id: 2, title: '仅哑铃' },
143 { id: 3, title: '仅哑铃+杠铃' }, 144 { id: 3, title: '仅哑铃+杠铃' },
144 -]); 145 +];
145 146
  147 +const SCENE_COLUMNS = [SCENE_LIST.map((item) => item.title)];
  148 +
  149 +const WEIGHT_CALC = {
  150 + 0: (weight, reps) => weight * reps,
  151 + 4: (weight, reps, userWeight) => (userWeight + weight) * reps,
  152 + 5: (weight, reps, userWeight) => (userWeight - weight) * reps,
  153 +};
  154 +
  155 +// ---- Store ----
  156 +const trainingStore = useTrainingStore();
  157 +
  158 +// ---- 状态 ----
  159 +const addExShow = ref(false);
  160 +const hasConverted = ref(false);
  161 +const dongzuoRefs = ref([]);
  162 +const templateTitle = ref('');
  163 +const templateIntro = ref('');
  164 +const coverImage = ref('');
146 const showScenePicker = ref(false); 165 const showScenePicker = ref(false);
147 const selectedSceneId = ref(0); 166 const selectedSceneId = ref(0);
  167 +const sceneColumns = ref(SCENE_COLUMNS);
  168 +const showGuide = ref(true);
  169 +const actionSortRef = ref(null);
  170 +const popupType = ref(null); // 'intro' | 'title' | null
  171 +const popupValue = ref('');
  172 +
  173 +// ---- 派生 ----
  174 +const units = computed(() => trainingStore.actionDetail?.units || []);
  175 +const unitCount = computed(() => units.value.length);
148 176
149 -const sceneColumns = ref([  
150 - ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃']  
151 -]); 177 +const totalSets = computed(() => {
  178 + let count = 0;
  179 + dongzuoRefs.value.forEach((ref) => {
  180 + if (ref?.totalSetCount) count += ref.totalSetCount;
  181 + });
  182 + return count;
  183 +});
152 184
  185 +const totalWeight = computed(() => {
  186 + const records = trainingStore.unitRecords || {};
  187 + let total = 0;
  188 + units.value.forEach((unit, unitIndex) => {
  189 + const record = records[unitIndex];
  190 + if (!record?.records) return;
  191 + unit.exercises?.forEach((ex) => {
  192 + const sets = record.records[ex.exerciseId];
  193 + if (!Array.isArray(sets)) return;
  194 + const calcFn = WEIGHT_CALC[ex.exerciseType];
  195 + if (!calcFn) return;
  196 + const userWeight = record.userWeight || 70;
  197 + sets.forEach((set) => {
  198 + total += calcFn(Number(set.weight) || 0, Number(set.reps) || 0, userWeight);
  199 + });
  200 + });
  201 + });
  202 + return total;
  203 +});
  204 +
  205 +// ---- 生命周期 ----
153 onLoad(() => { 206 onLoad(() => {
154 if (trainingStore.type !== 3 && !hasConverted.value) { 207 if (trainingStore.type !== 3 && !hasConverted.value) {
155 trainingStore.convertToTemplate(); 208 trainingStore.convertToTemplate();
156 hasConverted.value = true; 209 hasConverted.value = true;
157 - console.log('✅ 已完成模板转换(仅执行一次)', trainingStore.actionDetail);  
158 } 210 }
159 - console.log('在主页面转换为模板架构后的全局数据', trainingStore.type, trainingStore.actionDetail)  
160 - console.log('units.length:', trainingStore.actionDetail?.units?.length);  
161 -  
162 - // ==============================================  
163 - // 编辑页回显(标题、介绍、场景、封面)  
164 - // ==============================================  
165 - const detail = trainingStore.actionDetail || {}  
166 - templateTitle.value = detail.templateName || ''  
167 - templateIntro.value = detail.introduction || ''  
168 - selectedSceneId.value = detail.scene || 0  
169 - coverImage.value = detail.urlCover || ''  
170 - 211 + const detail = trainingStore.actionDetail || {};
  212 + templateTitle.value = detail.templateName || '';
  213 + templateIntro.value = detail.introduction || '';
  214 + selectedSceneId.value = detail.scene || 0;
  215 + coverImage.value = detail.urlCover || '';
171 }); 216 });
172 217
173 -// 动作排序弹窗  
174 -const openActionSort = () => {  
175 - console.log('调用前,ref 的值:', actionSortRef.value)  
176 - actionSortRef.value.openActionSort()  
177 -} 218 +// ---- 子组件引用 ----
  219 +const setActionRef = (el, index) => {
  220 + if (el) dongzuoRefs.value[index] = el;
  221 +};
178 222
179 -// 全部的组数  
180 -const totalSets = computed(() => {  
181 - let count = 0  
182 - dongzuoRefs.value.forEach(ref => {  
183 - if (ref?.totalSetCount) count += ref.totalSetCount  
184 - })  
185 - return count  
186 -})  
187 -  
188 -// 计算总训练重量(kg × 次数,仅统计力量类动作)  
189 -const totalWeight = computed(() => {  
190 - const units = trainingStore.actionDetail?.units || []  
191 - const records = trainingStore.unitRecords || {}  
192 - let total = 0  
193 -  
194 - units.forEach((unit, unitIndex) => {  
195 - const record = records[unitIndex]  
196 - if (!record?.records) return  
197 -  
198 - unit.exercises?.forEach(ex => {  
199 - const sets = record.records[ex.exerciseId]  
200 - if (!Array.isArray(sets)) return  
201 -  
202 - sets.forEach(set => {  
203 - let weight = Number(set.weight) || 0  
204 - const reps = Number(set.reps) || 0  
205 - const userWeight = record.userWeight || 70  
206 -  
207 - // 只计算 0、4、5 类型  
208 - if (ex.exerciseType === 0) {  
209 - total += weight * reps  
210 - }  
211 - else if (ex.exerciseType === 4) {  
212 - total += (userWeight + weight) * reps 223 +// ---- 文本弹窗 ----
  224 +const openTextPopup = (type) => {
  225 + popupType.value = type;
  226 + popupValue.value = type === 'intro' ? templateIntro.value : templateTitle.value;
  227 +};
  228 +const closeTextPopup = () => { popupType.value = null; };
  229 +const saveTextPopup = () => {
  230 + if (popupType.value === 'title') {
  231 + if (!popupValue.value.trim()) {
  232 + uni.showToast({ title: '请输入模板标题', icon: 'none' });
  233 + return;
213 } 234 }
214 - else if (ex.exerciseType === 5) {  
215 - total += (userWeight - weight) * reps 235 + templateTitle.value = popupValue.value.trim();
  236 + } else {
  237 + templateIntro.value = popupValue.value;
216 } 238 }
217 - })  
218 - })  
219 - }) 239 + uni.showToast({ title: '保存成功', icon: 'success' });
  240 + closeTextPopup();
  241 +};
220 242
221 - return total  
222 -})  
223 -// 场景确认  
224 -const handleSceneConfirm = (e) => {  
225 - console.log('up-picker confirm 原始参数:', e);  
226 - // 从 indexs 里拿到选中的索引(单列就是 0 号)  
227 - const selectedIndex = e.indexs[0];  
228 - const selectedItem = sceneList.value[selectedIndex];  
229 -  
230 - if (selectedItem) {  
231 - console.log("选中ID:", selectedItem.id);  
232 - console.log("选中标题:", selectedItem.title);  
233 - selectedSceneId.value = selectedItem.id;  
234 - } 243 +// ---- 动作排序 / 添加 ----
  244 +const openActionSort = () => actionSortRef.value?.openActionSort();
  245 +const handleAddAction = () => { addExShow.value = true; showGuide.value = false; };
235 246
  247 +// ---- 场景选择 ----
  248 +const handleSceneConfirm = (e) => {
  249 + const item = SCENE_LIST[e.indexs[0]];
  250 + if (item) selectedSceneId.value = item.id;
236 showScenePicker.value = false; 251 showScenePicker.value = false;
237 }; 252 };
238 253
239 -// dongzuo组件  
240 -const setActionRef = (el, index) => {  
241 - if (el) {  
242 - dongzuoRefs.value[index] = el  
243 - }  
244 - // console.log('✅ 成功收集子组件实例:', dongzuoRefs.value)  
245 -}  
246 -  
247 -// 编辑封面 254 +// ---- 编辑封面 ----
248 const handleEditCover = async () => { 255 const handleEditCover = async () => {
249 try { 256 try {
250 - // 1. 选择图片  
251 - const res = await uni.chooseImage({  
252 - count: 1, // 只选1张(封面)  
253 - sizeType: ['compressed'], // 压缩图  
254 - sourceType: ['album', 'camera'], // 相册+相机  
255 - });  
256 - 257 + const res = await uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'] });
257 const filePath = res.tempFilePaths[0]; 258 const filePath = res.tempFilePaths[0];
258 if (!filePath) return; 259 if (!filePath) return;
259 -  
260 - // 2. 调用你现成的 FileApi 上传文件  
261 const uploadRes = await FileApi.uploadFile(filePath, 'template-cover'); 260 const uploadRes = await FileApi.uploadFile(filePath, 'template-cover');
262 -  
263 - // 3. 上传成功  
264 - if (uploadRes && uploadRes.data) {  
265 - // 后端返回的图片地址 261 + if (uploadRes?.data) {
266 coverImage.value = uploadRes.data; 262 coverImage.value = uploadRes.data;
267 - console.log('封面上传成功:', coverImage.value);  
268 - uni.showToast({ title: '封面设置成功' }); 263 + uni.showToast({ title: '封面设置成功', icon: 'success' });
269 } 264 }
270 } catch (err) { 265 } catch (err) {
271 - console.log('上传封面失败:', err); 266 + console.error('上传封面失败:', err);
272 uni.showToast({ title: '上传失败', icon: 'none' }); 267 uni.showToast({ title: '上传失败', icon: 'none' });
273 } 268 }
274 }; 269 };
275 270
276 -// 模板介绍  
277 -const openIntroPopup = () => {  
278 - showIntroPopup.value = true;  
279 -};  
280 -const closeIntroPopup = () => {  
281 - setTimeout(() => {  
282 - showIntroPopup.value = false;  
283 - }, 2000);  
284 -};  
285 -// 保存模板介绍  
286 -const saveIntro = () => {  
287 - console.log(templateIntro.value);  
288 - uni.showToast({ title: '保存成功' });  
289 - closeIntroPopup();  
290 -};  
291 -  
292 -// 模板标题  
293 -const openTitlePopup = () => {  
294 - showTitlePopup.value = true;  
295 -};  
296 -  
297 -const closeTitlePopup = () => {  
298 - showTitlePopup.value = false;  
299 -};  
300 -  
301 -const saveTemplateTitle = () => {  
302 - if (!templateTitle.value.trim()) {  
303 - uni.showToast({  
304 - title: '请输入模板标题',  
305 - icon: 'none'  
306 - });  
307 - return;  
308 - }  
309 -  
310 - // 保存后关闭弹窗  
311 - closeTitlePopup();  
312 - uni.showToast({  
313 - title: '标题保存成功',  
314 - icon: 'success'  
315 - });  
316 -}; 271 +// ---- 导航 ----
  272 +const dismissGuide = () => { showGuide.value = false; };
317 273
318 -// 返回上一页  
319 const goBack = () => { 274 const goBack = () => {
320 - trainingStore.clearTrainingStore() 275 + trainingStore.clearTrainingStore();
321 setTimeout(() => { 276 setTimeout(() => {
322 - // 发送全局刷新信号,列表页会立刻执行刷新  
323 uni.$emit('refreshTemplateList'); 277 uni.$emit('refreshTemplateList');
324 uni.navigateBack(); 278 uni.navigateBack();
325 - }, 1500);  
326 - 279 + }, NAVIGATE_DELAY);
327 }; 280 };
328 281
329 -// 保存模板(完整修复间歇训练 type=6)  
330 -const handleSave = async () => {  
331 - const units = trainingStore.actionDetail?.units;  
332 - if (!units || units.length === 0) {  
333 - uni.showToast({ title: '请先添加动作', icon: 'none' });  
334 - return;  
335 - }  
336 - if (!templateTitle.value.trim()) {  
337 - uni.showToast({ title: '请输入模板标题', icon: 'none' });  
338 - // openTitlePopup();  
339 - return;  
340 - }  
341 -  
342 - try {  
343 - uni.showLoading({ title: '保存中...' });  
344 - const postData = { 282 +// ---- 保存 ----
  283 +const buildSavePayload = () => ({
345 templateId: trainingStore.actionDetail.id || null, 284 templateId: trainingStore.actionDetail.id || null,
346 groupId: null, 285 groupId: null,
347 templateName: templateTitle.value.trim(), 286 templateName: templateTitle.value.trim(),
348 scene: selectedSceneId.value, 287 scene: selectedSceneId.value,
349 templateCover: coverImage.value || '', 288 templateCover: coverImage.value || '',
350 templateIntroduction: templateIntro.value || '', 289 templateIntroduction: templateIntro.value || '',
351 - units: units.map((unit, unitIndex) => { 290 + units: units.value.map((unit, unitIndex) => {
352 const record = trainingStore.unitRecords[unitIndex] || {}; 291 const record = trainingStore.unitRecords[unitIndex] || {};
353 return { 292 return {
354 id: unit.unitId || 0, 293 id: unit.unitId || 0,
355 name: unit.name || '', 294 name: unit.name || '',
356 unitType: unit.unitType || 1, 295 unitType: unit.unitType || 1,
357 sortOrder: unitIndex, 296 sortOrder: unitIndex,
358 - exercises: (unit.exercises || []).map((ex, exIndex) => {  
359 - const sets = record.records?.[ex.exerciseId] || [];  
360 - return { 297 + exercises: (unit.exercises || []).map((ex, exIndex) => ({
361 exerciseId: ex.exerciseId || 0, 298 exerciseId: ex.exerciseId || 0,
362 exerciseType: ex.exerciseType || 0, 299 exerciseType: ex.exerciseType || 0,
363 innerOrder: exIndex, 300 innerOrder: exIndex,
364 - sets: sets.map((set, setIndex) => {  
365 - let duration = 0;  
366 - let restTime = 0;  
367 - // 间歇训练单独处理 301 + sets: (record.records?.[ex.exerciseId] || []).map((set, setIndex) => {
368 if (ex.exerciseType === 6) { 302 if (ex.exerciseType === 6) {
369 - duration = Number(set.duration) || 0;  
370 - restTime = Number(set.restTime) || 0;  
371 - }  
372 - // 其它  
373 - else {  
374 - duration =  
375 - (Number(set.h) || 0) * 3600 +  
376 - (Number(set.m) || 0) * 60 +  
377 - (Number(set.s) || 0);  
378 - restTime = parseInt(set.quickTimeDisplay) || 60; 303 + return { weight: 0, reps: 0, distance: 0, duration: Number(set.duration) || 0, restTime: Number(set.restTime) || 0, setIndex };
379 } 304 }
380 -  
381 return { 305 return {
382 weight: Number(set.weight) || 0, 306 weight: Number(set.weight) || 0,
383 reps: Number(set.reps) || 0, 307 reps: Number(set.reps) || 0,
384 distance: Number(set.distance) || 0, 308 distance: Number(set.distance) || 0,
385 - duration: duration,  
386 - restTime: restTime,  
387 - setIndex: setIndex,  
388 - };  
389 - }), 309 + duration: dayjs.duration({ hours: Number(set.h) || 0, minutes: Number(set.m) || 0, seconds: Number(set.s) || 0 }).asSeconds(),
  310 + restTime: parseInt(set.quickTimeDisplay) || 60,
  311 + setIndex,
390 }; 312 };
391 }), 313 }),
  314 + })),
392 }; 315 };
393 }), 316 }),
394 - }; 317 +});
395 318
396 - // 调用接口  
397 - let res;  
398 - // 判断:有 ID 就更新,没有 ID 就新建  
399 - if (trainingStore.actionDetail.id) {  
400 - res = await TemplatesApi.updateCustTemplate(postData);  
401 - } else {  
402 - res = await TemplatesApi.createCustTemplate(postData);  
403 - }  
404 - console.log('保存成功', res); 319 +const handleSave = async () => {
  320 + if (!unitCount.value) { uni.showToast({ title: '请先添加动作', icon: 'none' }); return; }
  321 + if (!templateTitle.value.trim()) { uni.showToast({ title: '请输入模板标题', icon: 'none' }); return; }
  322 + try {
  323 + uni.showLoading({ title: '保存中...' });
  324 + const api = trainingStore.actionDetail.id
  325 + ? TemplatesApi.updateCustTemplate
  326 + : TemplatesApi.createCustTemplate;
  327 + await api(buildSavePayload());
405 uni.hideLoading(); 328 uni.hideLoading();
406 - uni.showToast({ title: '保存成功' }); 329 + uni.showToast({ title: '保存成功', icon: 'success' });
407 trainingStore.clearTrainingStore(); 330 trainingStore.clearTrainingStore();
408 - setTimeout(() => {  
409 - uni.navigateBack();  
410 -  
411 - }, 1500);  
412 - 331 + setTimeout(() => uni.navigateBack(), NAVIGATE_DELAY);
413 } catch (err) { 332 } catch (err) {
414 uni.hideLoading(); 333 uni.hideLoading();
415 console.error('保存失败', err); 334 console.error('保存失败', err);
416 uni.showToast({ title: '保存失败', icon: 'none' }); 335 uni.showToast({ title: '保存失败', icon: 'none' });
417 } 336 }
418 }; 337 };
419 -  
420 -// 添加动作  
421 -const handleAddAction = () => {  
422 - console.log('添加动作');  
423 - addExShow.value = true  
424 - showGuide.value = false;  
425 -};  
426 </script> 338 </script>
427 339
428 <style scoped lang="scss"> 340 <style scoped lang="scss">
  341 +// ---- 变量 ----
  342 +$color-bg-primary: #1a1a1a;
  343 +$color-bg-secondary: #333;
  344 +$color-bg-input: #444;
  345 +$color-accent: #ffd100;
  346 +$color-text-primary: #fff;
  347 +$color-text-secondary: #ccc;
  348 +$color-text-tertiary: #999;
  349 +$color-text-muted: #888;
  350 +$color-border: #333;
  351 +
  352 +$font-size-xs: 24rpx;
  353 +$font-size-sm: 26rpx;
  354 +$font-size-base: 28rpx;
  355 +$font-size-md: 30rpx;
  356 +$font-size-lg: 32rpx;
  357 +$font-size-xl: 34rpx;
  358 +$font-size-xxl: 36rpx;
  359 +
  360 +$radius-sm: 12rpx;
  361 +$radius-base: 16rpx;
  362 +$radius-lg: 24rpx;
  363 +$radius-round: 40rpx;
  364 +$radius-circle: 50%;
  365 +
  366 +// ---- 容器 ----
429 .template-edit-page { 367 .template-edit-page {
  368 + position: relative;
430 width: 100%; 369 width: 100%;
431 height: 100vh; 370 height: 100vh;
432 - background-color: #1a1a1a; 371 + background-color: $color-bg-primary;
433 display: flex; 372 display: flex;
434 flex-direction: column; 373 flex-direction: column;
435 - color: #fff; 374 + color: $color-text-primary;
436 font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; 375 font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
437 } 376 }
438 377
439 -/* 顶部导航栏 */ 378 +// ---- 头部 ----
440 .header { 379 .header {
441 - margin-top: 150rpx;  
442 display: flex; 380 display: flex;
443 align-items: flex-start; 381 align-items: flex-start;
444 justify-content: space-between; 382 justify-content: space-between;
@@ -448,8 +386,8 @@ const handleAddAction = () => { @@ -448,8 +386,8 @@ const handleAddAction = () => {
448 .close-btn { 386 .close-btn {
449 width: 60rpx; 387 width: 60rpx;
450 height: 60rpx; 388 height: 60rpx;
451 - background-color: #333;  
452 - border-radius: 50%; 389 + background-color: $color-bg-secondary;
  390 + border-radius: $radius-circle;
453 display: flex; 391 display: flex;
454 align-items: center; 392 align-items: center;
455 justify-content: center; 393 justify-content: center;
@@ -465,34 +403,31 @@ const handleAddAction = () => { @@ -465,34 +403,31 @@ const handleAddAction = () => {
465 403
466 .title-row { 404 .title-row {
467 display: flex; 405 display: flex;
  406 + align-items: center;
468 gap: 15rpx; 407 gap: 15rpx;
469 } 408 }
470 409
471 .title-text { 410 .title-text {
472 - font-size: 36rpx; 411 + font-size: $font-size-xxl;
473 font-weight: 600; 412 font-weight: 600;
474 - margin-bottom: 10rpx;  
475 - display: flex;  
476 - align-items: center;  
477 - gap: 10rpx;  
478 } 413 }
479 414
480 .stats-text { 415 .stats-text {
481 - font-size: 26rpx;  
482 - color: #999; 416 + font-size: $font-size-sm;
  417 + color: $color-text-tertiary;
483 } 418 }
484 } 419 }
485 420
486 .save-btn { 421 .save-btn {
487 padding: 16rpx 32rpx; 422 padding: 16rpx 32rpx;
488 - background-color: #333;  
489 - border-radius: 40rpx;  
490 - font-size: 28rpx;  
491 - color: #999; 423 + background-color: $color-bg-secondary;
  424 + border-radius: $radius-round;
  425 + font-size: $font-size-base;
  426 + color: $color-text-tertiary;
492 flex-shrink: 0; 427 flex-shrink: 0;
493 428
494 &.active { 429 &.active {
495 - background-color: #ffdc00; 430 + background-color: $color-accent;
496 color: #000; 431 color: #000;
497 } 432 }
498 433
@@ -502,16 +437,13 @@ const handleAddAction = () => { @@ -502,16 +437,13 @@ const handleAddAction = () => {
502 } 437 }
503 } 438 }
504 439
505 -// 中间内容区域 440 +// ---- 内容区 ----
506 .content-area { 441 .content-area {
507 flex: 1; 442 flex: 1;
508 overflow-y: auto; 443 overflow-y: auto;
509 padding-bottom: 20rpx; 444 padding-bottom: 20rpx;
510 - display: flex;  
511 - flex-direction: column;  
512 } 445 }
513 446
514 -/* 中间空白区域 */  
515 .empty-area { 447 .empty-area {
516 flex: 1; 448 flex: 1;
517 display: flex; 449 display: flex;
@@ -520,24 +452,24 @@ const handleAddAction = () => { @@ -520,24 +452,24 @@ const handleAddAction = () => {
520 padding: 40rpx; 452 padding: 40rpx;
521 453
522 .empty-text { 454 .empty-text {
523 - font-size: 30rpx;  
524 - color: #888; 455 + font-size: $font-size-md;
  456 + color: $color-text-muted;
525 text-align: center; 457 text-align: center;
526 line-height: 1.6; 458 line-height: 1.6;
527 } 459 }
528 } 460 }
529 461
530 -/* 新手引导气泡 */ 462 +// ---- 引导气泡 ----
531 .guide-bubble { 463 .guide-bubble {
532 position: absolute; 464 position: absolute;
533 - bottom: 220rpx; 465 + bottom: 130rpx;
534 left: 50%; 466 left: 50%;
535 transform: translateX(-50%); 467 transform: translateX(-50%);
536 background-color: #000; 468 background-color: #000;
537 - color: #fff; 469 + color: $color-text-primary;
538 padding: 20rpx 30rpx; 470 padding: 20rpx 30rpx;
539 - border-radius: 12rpx;  
540 - font-size: 30rpx; 471 + border-radius: $radius-sm;
  472 + font-size: $font-size-md;
541 z-index: 10; 473 z-index: 10;
542 474
543 .bubble-arrow { 475 .bubble-arrow {
@@ -545,49 +477,47 @@ const handleAddAction = () => { @@ -545,49 +477,47 @@ const handleAddAction = () => {
545 bottom: -16rpx; 477 bottom: -16rpx;
546 left: 50%; 478 left: 50%;
547 transform: translateX(-50%); 479 transform: translateX(-50%);
548 - width: 0;  
549 - height: 0;  
550 border-left: 12rpx solid transparent; 480 border-left: 12rpx solid transparent;
551 border-right: 12rpx solid transparent; 481 border-right: 12rpx solid transparent;
552 border-top: 16rpx solid #000; 482 border-top: 16rpx solid #000;
553 } 483 }
554 } 484 }
555 485
556 -/* 底部操作栏 */ 486 +// ---- 底部栏 ----
557 .bottom-bar { 487 .bottom-bar {
558 display: flex; 488 display: flex;
559 height: 110rpx; 489 height: 110rpx;
560 align-items: center; 490 align-items: center;
561 justify-content: space-around; 491 justify-content: space-around;
562 padding: 20rpx 0; 492 padding: 20rpx 0;
563 - background-color: #1a1a1a;  
564 - border-top: 1rpx solid #333;  
565 - flex-shrink: 0;  
566 padding-bottom: env(safe-area-inset-bottom); 493 padding-bottom: env(safe-area-inset-bottom);
  494 + background-color: $color-bg-primary;
  495 + border-top: 1rpx solid $color-border;
  496 + flex-shrink: 0;
567 497
568 .bar-item { 498 .bar-item {
569 display: flex; 499 display: flex;
570 flex-direction: column; 500 flex-direction: column;
571 align-items: center; 501 align-items: center;
572 gap: 8rpx; 502 gap: 8rpx;
573 - font-size: 24rpx;  
574 - color: #fff; 503 + font-size: $font-size-xs;
  504 + color: $color-text-primary;
575 505
576 .item-num { 506 .item-num {
577 - font-size: 32rpx; 507 + font-size: $font-size-lg;
578 font-weight: 600; 508 font-weight: 600;
579 } 509 }
580 510
581 .item-label { 511 .item-label {
582 - font-size: 24rpx;  
583 - color: #ccc; 512 + font-size: $font-size-xs;
  513 + color: $color-text-secondary;
584 } 514 }
585 515
586 &.add-btn { 516 &.add-btn {
587 width: 100rpx; 517 width: 100rpx;
588 height: 100rpx; 518 height: 100rpx;
589 - background-color: #fff;  
590 - border-radius: 50%; 519 + background-color: $color-text-primary;
  520 + border-radius: $radius-circle;
591 display: flex; 521 display: flex;
592 align-items: center; 522 align-items: center;
593 justify-content: center; 523 justify-content: center;
@@ -597,28 +527,18 @@ const handleAddAction = () => { @@ -597,28 +527,18 @@ const handleAddAction = () => {
597 } 527 }
598 } 528 }
599 529
600 -.intro-popup-mask {  
601 - position: fixed;  
602 - top: 0;  
603 - left: 0;  
604 - right: 0;  
605 - bottom: 0;  
606 - background: rgba(0, 0, 0, 0.6);  
607 - z-index: 999;  
608 -}  
609 -  
610 -.intro-popup { 530 +// ---- 文本弹窗 ----
  531 +.text-popup {
611 position: fixed; 532 position: fixed;
612 left: 0; 533 left: 0;
613 right: 0; 534 right: 0;
614 bottom: 0; 535 bottom: 0;
615 height: 85vh; 536 height: 85vh;
616 - background: #333;  
617 - border-radius: 24rpx 24rpx 0 0; 537 + background: $color-bg-secondary;
  538 + border-radius: $radius-lg $radius-lg 0 0;
618 z-index: 1000; 539 z-index: 1000;
619 display: flex; 540 display: flex;
620 flex-direction: column; 541 flex-direction: column;
621 - // transition: transform 0.3s  
622 } 542 }
623 543
624 .popup-header { 544 .popup-header {
@@ -628,128 +548,42 @@ const handleAddAction = () => { @@ -628,128 +548,42 @@ const handleAddAction = () => {
628 padding: 40rpx 30rpx 20rpx; 548 padding: 40rpx 30rpx 20rpx;
629 } 549 }
630 550
  551 +.popup-close-btn {
  552 + width: 60rpx;
  553 + height: 60rpx;
  554 + display: flex;
  555 + align-items: center;
  556 + justify-content: center;
  557 +}
  558 +
631 .popup-title { 559 .popup-title {
632 - font-size: 34rpx;  
633 - color: #fff; 560 + font-size: $font-size-xl;
  561 + color: $color-text-primary;
634 font-weight: bold; 562 font-weight: bold;
635 } 563 }
636 564
637 .popup-save-btn { 565 .popup-save-btn {
638 - background: #ffd100; 566 + background: $color-accent;
639 color: #000; 567 color: #000;
640 padding: 12rpx 24rpx; 568 padding: 12rpx 24rpx;
641 - border-radius: 40rpx;  
642 - font-size: 28rpx; 569 + border-radius: $radius-round;
  570 + font-size: $font-size-base;
643 } 571 }
644 572
645 .popup-content { 573 .popup-content {
646 flex: 1; 574 flex: 1;
647 padding: 30rpx; 575 padding: 30rpx;
648 - flex-shrink: 0;  
649 } 576 }
650 577
651 .intro-textarea { 578 .intro-textarea {
652 - width: 95%; 579 + width: 100%;
653 height: 300rpx; 580 height: 300rpx;
654 - background: #444;  
655 - color: #fff;  
656 - border-radius: 16rpx; 581 + background: $color-bg-input;
  582 + color: $color-text-primary;
  583 + border-radius: $radius-base;
657 padding: 20rpx; 584 padding: 20rpx;
658 - font-size: 30rpx; 585 + font-size: $font-size-md;
659 border: 0; 586 border: 0;
660 -}  
661 -  
662 -// 场景弹窗遮罩  
663 -.scene-popup-mask {  
664 - position: fixed;  
665 - top: 0;  
666 - left: 0;  
667 - right: 0;  
668 - bottom: 0;  
669 - background: rgba(0, 0, 0, 0.6);  
670 - z-index: 999;  
671 -}  
672 -  
673 -// 场景弹窗本体  
674 -.scene-popup {  
675 - position: fixed;  
676 - left: 0;  
677 - right: 0;  
678 - bottom: 0;  
679 - background: #333;  
680 - border-radius: 24rpx 24rpx 0 0;  
681 - z-index: 1000;  
682 - display: flex;  
683 - flex-direction: column;  
684 - padding-bottom: env(safe-area-inset-bottom);  
685 - // 固定高度(和截图比例一致)  
686 - height: 60vh;  
687 - max-height: 70vh;  
688 -}  
689 -  
690 -// 顶部横条  
691 -.scene-popup::before {  
692 - content: '';  
693 - position: absolute;  
694 - top: 16rpx;  
695 - left: 50%;  
696 - transform: translateX(-50%);  
697 - width: 80rpx;  
698 - height: 8rpx;  
699 - background: #666;  
700 - border-radius: 4rpx;  
701 -}  
702 -  
703 -// 弹窗头部  
704 -.scene-popup-header {  
705 - display: flex;  
706 - align-items: center;  
707 - justify-content: space-between;  
708 - padding: 40rpx 30rpx 20rpx;  
709 -  
710 - .scene-close-btn {  
711 - width: 60rpx;  
712 - height: 60rpx;  
713 - display: flex;  
714 - align-items: center;  
715 - justify-content: center;  
716 - }  
717 -  
718 - .scene-popup-title {  
719 - font-size: 34rpx;  
720 - color: #fff;  
721 - font-weight: bold;  
722 - }  
723 -}  
724 -  
725 -// 场景选项列表  
726 -.scene-list {  
727 - flex: 1;  
728 - overflow-y: auto;  
729 -  
730 - .scene-item {  
731 - padding: 30rpx;  
732 - text-align: center;  
733 - font-size: 32rpx;  
734 - color: #999;  
735 - border-bottom: 1rpx solid #444;  
736 -  
737 - &.active {  
738 - color: #fff;  
739 - font-weight: bold;  
740 - }  
741 - }  
742 -}  
743 -  
744 -// 底部确认按钮  
745 -.scene-confirm-btn {  
746 - margin: 30rpx;  
747 - padding: 24rpx;  
748 - background: #ffd100;  
749 - color: #000;  
750 - border-radius: 40rpx;  
751 - font-size: 32rpx;  
752 - font-weight: bold;  
753 - text-align: center; 587 + box-sizing: border-box;
754 } 588 }
755 </style> 589 </style>
1 <template> 1 <template>
2 <view class="container"> 2 <view class="container">
3 <!-- #ifdef MP-WEIXIN --> 3 <!-- #ifdef MP-WEIXIN -->
4 - <up-navbar placeholder bgColor="transparent">  
5 - <template #left>  
6 - <view></view>  
7 - </template>  
8 - </up-navbar> 4 + <up-navbar placeholder bgColor="transparent" />
9 <!-- #endif --> 5 <!-- #endif -->
10 <view class="status-header"> 6 <view class="status-header">
11 <view class="timer-box" @click="show = true"> 7 <view class="timer-box" @click="show = true">
@@ -18,19 +14,29 @@ @@ -18,19 +14,29 @@
18 <view class="action-list"> 14 <view class="action-list">
19 <view class="header"> 15 <view class="header">
20 <view class="title-left" @click="openTrainingNamePopup"> 16 <view class="title-left" @click="openTrainingNamePopup">
21 - <up-icon name="edit-pen" color="#8e8e93" size="22"></up-icon> 17 + <up-icon name="edit-pen" color="#8e8e93" size="22" />
22 <text class="title-name">{{ trainingName }}</text> 18 <text class="title-name">{{ trainingName }}</text>
23 </view> 19 </view>
24 <text class="title-stats"> 20 <text class="title-stats">
25 - {{ (trainingStore.actionDetail?.units || []).length }} 个动作· {{ totalSets }}组 · {{ totalCheckedWeight }}/{{  
26 - allTotalWeight }}kg 21 + {{ (actionDetail?.units || []).length }} 个动作· {{ totalSets }}组 ·
  22 + {{ totalCheckedWeight }}/{{ allTotalWeight }}kg
27 </text> 23 </text>
28 </view> 24 </view>
29 <!-- 3. 模板训练 type=3 循环 --> 25 <!-- 3. 模板训练 type=3 循环 -->
30 - <template v-if="trainingStore.actionDetail?.units?.length">  
31 - <view v-for="(unit, index) in trainingStore.actionDetail?.units" :key="unit.unitId">  
32 - <dongzuo :id="unit.unitId" :type="unit.unitType" :actionDetail="trainingStore.convertUnitToActionDetail(unit)"  
33 - :ref="el => setActionRef(el, index)" :unitIndex="index" :is-daily-templates="false" /> 26 + <template v-if="actionDetail?.units?.length">
  27 + <view v-for="(unit, index) in actionDetail.units" :key="unit.unitId">
  28 + <dongzuo
  29 + :id="unit.unitId"
  30 + :type="unit.unitType"
  31 + :actionDetail="trainingStore.convertUnitToActionDetail(unit)"
  32 + :ref="(el) => setActionRef(el, index)"
  33 + :unitIndex="index"
  34 + :is-daily-templates="false"
  35 + :isUnitActive="unitActiveStates[index]"
  36 + @startRestTimer="startRestCountdown"
  37 + @cancelRestTimer="onCancelRestTimer"
  38 + @updateUnitActive="(val) => onUnitActiveChange(index, val)"
  39 + />
34 </view> 40 </view>
35 </template> 41 </template>
36 42
@@ -39,42 +45,80 @@ @@ -39,42 +45,80 @@
39 <text class="empty-text">尚未添加任何动作</text> 45 <text class="empty-text">尚未添加任何动作</text>
40 <text class="empty-subtext">点击最下方 + 开始添加动作</text> 46 <text class="empty-subtext">点击最下方 + 开始添加动作</text>
41 </view> 47 </view>
42 -  
43 - <!-- 完成按钮 -->  
44 - <view class="finish-btn" @click="save">完成</view>  
45 </view> 48 </view>
46 49
47 - <view v-if="!trainingStore.actionDetail?.units?.length" class="tip-bubble"> 50 + <view v-if="!actionDetail?.units?.length" class="tip-bubble">
48 <text>挑选想要的动作开始训练吧</text> 51 <text>挑选想要的动作开始训练吧</text>
49 </view> 52 </view>
50 53
51 <view class="footer-bar"> 54 <view class="footer-bar">
52 <view class="footer-item" @click="openActionSort"> 55 <view class="footer-item" @click="openActionSort">
53 - <text class="count">{{ trainingStore.actionDetail?.units?.length || 0 }}</text> 56 + <text class="count">{{ (actionDetail?.units || []).length }}</text>
54 <text class="label">动作排序</text> 57 <text class="label">动作排序</text>
55 </view> 58 </view>
56 <view class="footer-item" @click="openSummaryPopup"> 59 <view class="footer-item" @click="openSummaryPopup">
57 - <up-icon name="edit-pen" color="#fff" size="24"></up-icon> 60 + <up-icon name="edit-pen" color="#fff" size="24" />
58 <text class="label">写总结</text> 61 <text class="label">写总结</text>
59 </view> 62 </view>
60 <view class="add-btn-wrapper"> 63 <view class="add-btn-wrapper">
61 <view class="add-btn" @click="addActionsPopup"> 64 <view class="add-btn" @click="addActionsPopup">
62 - <up-icon name="plus" color="#000" size="20" bold></up-icon> 65 + <up-icon name="plus" color="#000" size="20" bold />
63 </view> 66 </view>
64 </view> 67 </view>
65 <view class="footer-item" @click="openMin"> 68 <view class="footer-item" @click="openMin">
66 - <up-icon name="minus-square-fill" color="#fff" size="24"></up-icon> 69 + <up-icon name="minus-square-fill" color="#fff" size="24" />
67 <text class="label">最小化</text> 70 <text class="label">最小化</text>
68 </view> 71 </view>
69 <view class="footer-item" @click="deleteTraining"> 72 <view class="footer-item" @click="deleteTraining">
70 - <up-icon name="trash" color="#fff" size="24"></up-icon> 73 + <up-icon name="trash" color="#fff" size="24" />
71 <text class="label">删除</text> 74 <text class="label">删除</text>
72 </view> 75 </view>
73 </view> 76 </view>
74 - 77 + <!-- 组间休息倒计时 -->
  78 + <view
  79 + class="rest-countdown"
  80 + v-if="restCountdownSeconds > 0"
  81 + @click="
  82 + clearRestCountdown();
  83 + uncheckCurrentUnit();
  84 + if (restTimerWasRunning) {
  85 + trainingStore.toggleTimer();
  86 + restTimerWasRunning = false;
  87 + }
  88 + "
  89 + >
  90 + <view class="countdown-circle">
  91 + <!-- H5 端:使用 SVG 进度环 -->
  92 + <!-- #ifdef H5 -->
  93 + <svg class="ring-svg" viewBox="0 0 100 100">
  94 + <circle class="ring-bg" cx="50" cy="50" r="44" />
  95 + <circle
  96 + class="ring-progress"
  97 + cx="50"
  98 + cy="50"
  99 + r="44"
  100 + :stroke-dasharray="CIRCUMFERENCE"
  101 + :stroke-dashoffset="CIRCUMFERENCE * (1 - restProgress)"
  102 + transform="rotate(-90 50 50)"
  103 + />
  104 + </svg>
  105 + <!-- #endif -->
  106 + <!-- 微信小程序端:不显示进度环 -->
  107 + <view class="countdown-inner">
  108 + <text class="rest-time">{{ restFormattedTime }}</text>
  109 + <text class="rest-total">{{ restCountdownTotal }}s</text>
  110 + </view>
  111 + </view>
  112 + </view>
75 <!-- 设置训练时间弹窗 --> 113 <!-- 设置训练时间弹窗 -->
76 - <up-popup :show="show" mode="top" @close="show = false" :safeAreaInsetBottom="false" safeAreaInsetTop  
77 - bgColor="#2c2c2e"> 114 + <up-popup
  115 + :show="show"
  116 + mode="top"
  117 + @close="show = false"
  118 + :safeAreaInsetBottom="false"
  119 + safeAreaInsetTop
  120 + bgColor="#2c2c2e"
  121 + >
78 <view class="time-pop-container"> 122 <view class="time-pop-container">
79 <view class="pop-content"> 123 <view class="pop-content">
80 <view class="pop-title">设置训练时间</view> 124 <view class="pop-title">设置训练时间</view>
@@ -107,7 +151,11 @@ @@ -107,7 +151,11 @@
107 </view> 151 </view>
108 <view class="timer-right"> 152 <view class="timer-right">
109 <view class="action-btn gray-btn" v-if="isPause" @click="resetTimer">重置</view> 153 <view class="action-btn gray-btn" v-if="isPause" @click="resetTimer">重置</view>
110 - <view class="action-btn green-btn" :class="{ active: !isPause, bgactve: !isPause }" @click="toggleTimer"> 154 + <view
  155 + class="action-btn green-btn"
  156 + :class="{ active: !isPause, bgactve: !isPause }"
  157 + @click="toggleTimer"
  158 + >
111 {{ isPause ? '继续计时' : '暂停计时' }} 159 {{ isPause ? '继续计时' : '暂停计时' }}
112 </view> 160 </view>
113 </view> 161 </view>
@@ -120,116 +168,205 @@ @@ -120,116 +168,205 @@
120 </up-popup> 168 </up-popup>
121 169
122 <!-- 设置训练日期 --> 170 <!-- 设置训练日期 -->
123 - <up-datetime-picker :show="datePickerShow" v-model="dateValue" mode="date" closeOnClickOverlay  
124 - @confirm="confirmDate" @cancel="datePickerShow = false" @close="datePickerShow = false"> 171 + <up-datetime-picker
  172 + :show="datePickerShow"
  173 + v-model="dateValue"
  174 + mode="date"
  175 + closeOnClickOverlay
  176 + @confirm="confirmDate"
  177 + @cancel="datePickerShow = false"
  178 + @close="datePickerShow = false"
  179 + >
125 </up-datetime-picker> 180 </up-datetime-picker>
126 181
127 <!-- 设置起始时间 --> 182 <!-- 设置起始时间 -->
128 - <up-datetime-picker :show="timePickerShow" v-model="timeValue" mode="time" closeOnClickOverlay  
129 - @confirm="confirmTime" @cancel="timePickerShow = false" @close="timePickerShow = false"> 183 + <up-datetime-picker
  184 + :show="timePickerShow"
  185 + v-model="timeValue"
  186 + mode="time"
  187 + closeOnClickOverlay
  188 + @confirm="confirmTime"
  189 + @cancel="timePickerShow = false"
  190 + @close="timePickerShow = false"
  191 + >
130 </up-datetime-picker> 192 </up-datetime-picker>
131 193
132 <!-- 设置训练时长计时 --> 194 <!-- 设置训练时长计时 -->
133 - <up-picker :show="showPicker" :columns="timeColumns" :defaultIndex="defaultTimeIndex" title="修改训练时长"  
134 - @confirm="onTimeConfirm" @cancel="showPicker = false" closeOnClickOverlay> 195 + <up-picker
  196 + :show="showPicker"
  197 + :columns="timeColumns"
  198 + :defaultIndex="defaultTimeIndex"
  199 + title="修改训练时长"
  200 + @confirm="onTimeConfirm"
  201 + @cancel="showPicker = false"
  202 + closeOnClickOverlay
  203 + >
135 </up-picker> 204 </up-picker>
136 205
137 -  
138 - <action-sort ref="actionSortRef" @open-add-actions="addActionsShow = true" />  
139 -  
140 <!-- 写总结弹窗 --> 206 <!-- 写总结弹窗 -->
141 - <up-popup :show="summaryShow" @close="summaryShow = false" mode="bottom" :safeAreaInsetBottom="false"> 207 + <up-popup
  208 + :show="summaryShow"
  209 + @close="summaryShow = false"
  210 + mode="bottom"
  211 + :safeAreaInsetBottom="false"
  212 + >
142 <view class="summaryPopup"> 213 <view class="summaryPopup">
143 <view class="popup-header"> 214 <view class="popup-header">
144 <text class="popup-title">本次训练总结</text> 215 <text class="popup-title">本次训练总结</text>
145 <view class="save-btn" @click="saveSummary">保存</view> 216 <view class="save-btn" @click="saveSummary">保存</view>
146 </view> 217 </view>
147 - <textarea class="summary-textarea" v-model="summaryContent" placeholder="记录本次训练感悟" /> 218 + <textarea
  219 + class="summary-textarea"
  220 + v-model="summaryContent"
  221 + placeholder="记录本次训练感悟"
  222 + />
148 </view> 223 </view>
149 </up-popup> 224 </up-popup>
150 <!-- 命名弹窗 --> 225 <!-- 命名弹窗 -->
151 - <up-popup :show="inputNameShow" @close="inputNameShow = false" mode="bottom" :safeAreaInsetBottom="false"> 226 + <up-popup
  227 + :show="inputNameShow"
  228 + @close="inputNameShow = false"
  229 + mode="bottom"
  230 + :safeAreaInsetBottom="false"
  231 + >
152 <view class="namePopup"> 232 <view class="namePopup">
153 <view class="popup-header"> 233 <view class="popup-header">
154 <text class="popup-title">训练名称</text> 234 <text class="popup-title">训练名称</text>
155 <view class="save-btn" @click="saveTrainingName">保存</view> 235 <view class="save-btn" @click="saveTrainingName">保存</view>
156 </view> 236 </view>
157 - <textarea v-model="trainingStore.trainingName" class="name-input" placeholder="请输入训练名称"  
158 - placeholder-color="#8e8e93" color="#fff" /> 237 + <textarea
  238 + v-model="trainingStore.trainingName"
  239 + class="name-input"
  240 + placeholder="请输入训练名称"
  241 + placeholder-color="#8e8e93"
  242 + color="#fff"
  243 + />
159 </view> 244 </view>
160 </up-popup> 245 </up-popup>
161 <!-- 动作新增组件 --> 246 <!-- 动作新增组件 -->
162 <addActions :show="addActionsShow" @close="addActionsShow = false" /> 247 <addActions :show="addActionsShow" @close="addActionsShow = false" />
163 - 248 + <action-sort ref="actionSortRef" @open-add-actions="addActionsShow = true" />
164 </view> 249 </view>
165 </template> 250 </template>
166 251
167 <script setup> 252 <script setup>
168 -import { ref, reactive, computed, onBeforeUnmount, onMounted, nextTick, onUnmounted, watch } from 'vue';  
169 -import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';  
170 -import { onLoad } from "@dcloudio/uni-app"  
171 -import TrainingApi from '@/sheep/api/Training/traininghistory'  
172 -import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';  
173 -import { useTrainingStore } from '@/sheep/store/trainingStore'  
174 -import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue'  
175 -import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue'  
176 -import { trainingSnapshot } from '@/sheep/store/trainingSnapshot'  
177 -  
178 -const trainingStore = useTrainingStore()  
179 -const hasConverted = ref(false);  
180 -const addActionsShow = ref(false)  
181 -const ActionRef = ref(null)  
182 -// --- 计时核心逻辑 ---  
183 -const totalSeconds = computed(() => trainingStore.totalSeconds);  
184 -const isPause = computed(() => trainingStore.isPause);  
185 -const showPicker = computed(() => trainingStore.showPicker);  
186 -const defaultTimeIndex = computed(() => trainingStore.defaultTimeIndex);  
187 -  
188 -  
189 -const groupCount = ref(0)  
190 -const summaryShow = ref(false)  
191 -const summaryContent = ref('')  
192 -// 训练名称  
193 -const inputNameShow = ref(false)  
194 -const trainingName = computed(() => trainingStore.trainingName)  
195 -  
196 -const actionSortRef = ref(null)  
197 -  
198 -watch(  
199 - () => trainingStore.isTraining,  
200 - (newVal, oldVal) => {  
201 - console.log('训练状态变化:', oldVal, '→', newVal)  
202 - if (newVal) {  
203 - // 正在训练逻辑  
204 - } else {  
205 - // 结束训练逻辑 253 + import { ref, computed, nextTick, onUnmounted } from 'vue';
  254 + import dayjs from 'dayjs';
  255 + import { onLoad } from '@dcloudio/uni-app';
  256 + import TrainingApi from '@/sheep/api/Training/traininghistory';
  257 + import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
  258 + import { useTrainingStore } from '@/sheep/store/trainingStore';
  259 + import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue';
  260 + import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue';
  261 + import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
  262 + import { trainingSnapshot } from '@/sheep/store/trainingSnapshot';
  263 +
  264 + const trainingStore = useTrainingStore();
  265 +
  266 + const addActionsShow = ref(false);
  267 + const hasConverted = ref(false);
  268 + const summaryShow = ref(false);
  269 + const summaryContent = ref('');
  270 + const inputNameShow = ref(false);
  271 + const actionSortRef = ref(null);
  272 + const show = ref(false);
  273 + const datePickerShow = ref(false);
  274 + const timePickerShow = ref(false);
  275 + const dateValue = ref(Number(new Date()));
  276 + const timeValue = ref('14:13');
  277 + const trainingDateText = ref('');
  278 +
  279 + const totalSeconds = computed(() => trainingStore.totalSeconds || 0);
  280 + const isPause = computed(() => trainingStore.isPause);
  281 + const showPicker = computed(() => trainingStore.showPicker);
  282 + const defaultTimeIndex = computed(() => trainingStore.defaultTimeIndex);
  283 + const trainingName = computed(() => trainingStore.trainingName);
  284 + const trainingTimeText = computed(() => trainingStore.trainingTimeText);
  285 + const actionDetail = computed(() => trainingStore.actionDetail || {});
  286 +
  287 + // ===================== 组间休息倒计时 =====================
  288 + const restCountdownSeconds = ref(0);
  289 + const restCountdownTotal = ref(0);
  290 + let restCountdownInterval = null;
  291 + let restTimerWasRunning = false;
  292 +
  293 + const restFormattedTime = computed(() => {
  294 + const min = Math.floor(restCountdownSeconds.value / 60);
  295 + const sec = restCountdownSeconds.value % 60;
  296 + return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
  297 + });
  298 +
  299 + const restProgress = computed(() => {
  300 + if (restCountdownTotal.value <= 0) return 0;
  301 + return 1 - restCountdownSeconds.value / restCountdownTotal.value;
  302 + });
  303 +
  304 + // H5 SVG 圆环周长(用于 stroke-dasharray)
  305 + const CIRCUMFERENCE = 2 * Math.PI * 44;
  306 +
  307 + const currentRestUnitIndex = ref(null);
  308 +
  309 + const clearRestCountdown = () => {
  310 + if (restCountdownInterval) {
  311 + clearInterval(restCountdownInterval);
  312 + restCountdownInterval = null;
206 } 313 }
207 - },  
208 - { immediate: true } // 页面初始化立即执行一次  
209 -) 314 + restCountdownSeconds.value = 0;
  315 + restCountdownTotal.value = 0;
  316 + };
  317 +
  318 + const startRestCountdown = (seconds, unitIndex) => {
  319 + if (!seconds || seconds <= 0) return;
  320 + clearRestCountdown();
  321 + restCountdownTotal.value = seconds;
  322 + restCountdownSeconds.value = seconds;
  323 + currentRestUnitIndex.value = unitIndex;
  324 + restTimerWasRunning = !trainingStore.isPause;
  325 + if (restTimerWasRunning) trainingStore.toggleTimer();
  326 + restCountdownInterval = setInterval(() => {
  327 + restCountdownSeconds.value--;
  328 + if (restCountdownSeconds.value <= 0) {
  329 + clearRestCountdown();
  330 + uncheckCurrentUnit();
  331 + if (restTimerWasRunning) trainingStore.toggleTimer();
  332 + }
  333 + }, 1000);
  334 + };
210 335
211 -// 动作排序弹窗  
212 -const openActionSort = () => {  
213 - console.log('调用前,ref 的值:', actionSortRef.value)  
214 - actionSortRef.value.openActionSort()  
215 -} 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 + }
216 343
217 -onLoad(async (options) => { 344 + function onCancelRestTimer(unitIndex) {
  345 + clearRestCountdown();
  346 + currentRestUnitIndex.value = unitIndex;
  347 + uncheckCurrentUnit();
  348 + if (restTimerWasRunning) {
  349 + trainingStore.toggleTimer();
  350 + restTimerWasRunning = false;
  351 + }
  352 + }
218 353
219 - console.log('✅ 动作练习页面 onLoad 中的 options:', options); 354 + onUnmounted(() => {
  355 + clearRestCountdown();
  356 + });
220 357
  358 + const openActionSort = () => {
  359 + actionSortRef.value.openActionSort();
  360 + };
221 361
  362 + onLoad(async (options) => {
222 const id = Number(options.id); 363 const id = Number(options.id);
223 const type = Number(options.type); 364 const type = Number(options.type);
224 const isTraining = options.isTraining === 'true'; 365 const isTraining = options.isTraining === 'true';
225 trainingStore.isTraining = isTraining; 366 trainingStore.isTraining = isTraining;
226 367
227 - console.log('✅动作练习页面 onLoad 中的 options:', options);  
228 -  
229 -  
230 const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null; 368 const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null;
231 trainingStore.dailyTemplateId = dailyTemplateId; 369 trainingStore.dailyTemplateId = dailyTemplateId;
232 - console.log('接收到的 dailyTemplateId:', dailyTemplateId);  
233 // 直接让 store 去请求 + 保存数据 370 // 直接让 store 去请求 + 保存数据
234 if (!dailyTemplateId) { 371 if (!dailyTemplateId) {
235 await trainingStore.loadTrainingDetail(id, type); 372 await trainingStore.loadTrainingDetail(id, type);
@@ -241,363 +378,229 @@ onLoad(async (options) => { @@ -241,363 +378,229 @@ onLoad(async (options) => {
241 if (!hasConverted.value) { 378 if (!hasConverted.value) {
242 trainingStore.convertToTemplate(); 379 trainingStore.convertToTemplate();
243 hasConverted.value = true; 380 hasConverted.value = true;
244 - console.log('✅ 已完成模板转换(仅执行一次)', trainingStore.actionDetail);  
245 } 381 }
246 - console.log('在主页面转换为模板架构后的全局数据', trainingStore.type, trainingStore.actionDetail)  
247 initTrainingName(); 382 initTrainingName();
248 383
249 - //动态初始化训练日期为当天  
250 - const today = new Date();  
251 - const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][today.getDay()]; 384 + trainingDateText.value = dayjs().format('YYYY-MM-DD dd');
252 if (!trainingStore.trainingTimeText) { 385 if (!trainingStore.trainingTimeText) {
253 - const now = new Date();  
254 - const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');  
255 - trainingStore.setTrainingTimeText(timeStr);  
256 - }  
257 -  
258 - console.log('✅ 动态初始化训练日期:', trainingStore.trainingTimeText);  
259 -});  
260 -  
261 -  
262 -  
263 -const initTrainingName = () => {  
264 - const name = trainingStore.actionDetail?.templateName || '训练';  
265 - trainingStore.setTrainingName(name); // 存进 Pinia  
266 -};  
267 -// 全部的组数  
268 -const totalSets = computed(() => {  
269 - let count = 0  
270 - dongzuoRefs.value.forEach(ref => {  
271 - if (ref?.totalSetCount) count += ref.totalSetCount  
272 - })  
273 - return count  
274 -})  
275 -  
276 -// 已勾选的总重量  
277 -const totalCheckedWeight = computed(() => {  
278 - let sum = 0  
279 - dongzuoRefs.value.forEach(ref => {  
280 - if (ref?.checkedWeight) sum += ref.checkedWeight  
281 - })  
282 - return sum  
283 -})  
284 -  
285 -// 所有组的总重量  
286 -const allTotalWeight = computed(() => {  
287 - let sum = 0  
288 - dongzuoRefs.value.forEach(ref => {  
289 - if (ref?.totalWeight) sum += ref.totalWeight  
290 - })  
291 - return sum  
292 -})  
293 -  
294 -// 获得子组件模板的数据  
295 -const dongzuoRefs = ref([])  
296 -const setActionRef = (el, index) => {  
297 - if (el) {  
298 - dongzuoRefs.value[index] = el  
299 - }  
300 - // console.log('✅ 成功收集子组件实例:', dongzuoRefs.value)  
301 -}  
302 -  
303 -  
304 -  
305 -// 打开/关闭总结弹窗  
306 -const openSummaryPopup = () => {  
307 - summaryShow.value = true  
308 - summaryContent.value = '' // 打开时清空内容  
309 -}  
310 -  
311 -// 保存总结  
312 -const saveSummary = () => {  
313 - console.log('保存的训练总结:', summaryContent.value)  
314 - summaryShow.value = false  
315 - summaryContent.value = ''  
316 -}  
317 -  
318 -// 打开命名弹窗  
319 -const openTrainingNamePopup = () => { 386 + trainingStore.setTrainingTimeText(dayjs().format('HH:mm'));
  387 + }
  388 + if (isTraining) {
  389 + trainingStore.toggleTimer();
  390 + }
  391 + });
  392 +
  393 + const initTrainingName = () => {
  394 + trainingStore.setTrainingName(actionDetail.value?.templateName || '训练');
  395 + };
  396 +
  397 + const totalSets = computed(() => {
  398 + let count = 0;
  399 + dongzuoRefs.value.forEach((ref) => {
  400 + if (ref?.totalSetCount) count += ref.totalSetCount;
  401 + });
  402 + return count;
  403 + });
  404 +
  405 + const totalCheckedWeight = computed(() => {
  406 + let sum = 0;
  407 + dongzuoRefs.value.forEach((ref) => {
  408 + if (ref?.checkedWeight) sum += ref.checkedWeight;
  409 + });
  410 + return sum;
  411 + });
  412 +
  413 + const allTotalWeight = computed(() => {
  414 + let sum = 0;
  415 + dongzuoRefs.value.forEach((ref) => {
  416 + if (ref?.totalWeight) sum += ref.totalWeight;
  417 + });
  418 + return sum;
  419 + });
  420 +
  421 + // 获得子组件模板的数据
  422 + const dongzuoRefs = ref([]);
  423 + const setActionRef = (el, index) => {
  424 + if (el) dongzuoRefs.value[index] = el;
  425 + };
  426 +
  427 + // 每个 unit 是否有勾选的组
  428 + const unitActiveStates = ref([])
  429 + const onUnitActiveChange = (index, val) => {
  430 + unitActiveStates.value[index] = val
  431 + unitActiveStates.value = [...unitActiveStates.value]
  432 + }
  433 +
  434 + const openSummaryPopup = () => {
  435 + summaryShow.value = true;
  436 + summaryContent.value = '';
  437 + };
  438 +
  439 + const saveSummary = () => {
  440 + summaryShow.value = false;
  441 + summaryContent.value = '';
  442 + };
  443 +
  444 + const openTrainingNamePopup = () => {
320 inputNameShow.value = true; 445 inputNameShow.value = true;
321 -}; 446 + };
322 447
323 -// 保存训练名称  
324 -const saveTrainingName = () => { 448 + const saveTrainingName = () => {
325 trainingStore.setTrainingName(trainingStore.trainingName); 449 trainingStore.setTrainingName(trainingStore.trainingName);
326 - if (trainingStore.actionDetail) { 450 + if (actionDetail.value) {
327 trainingStore.actionDetail.templateName = trainingStore.trainingName; 451 trainingStore.actionDetail.templateName = trainingStore.trainingName;
328 } 452 }
329 inputNameShow.value = false; 453 inputNameShow.value = false;
330 -};  
331 -  
332 -// 格式化时间  
333 -const formattedTime = computed(() => {  
334 - const h = Math.floor(totalSeconds.value / 3600)  
335 - .toString()  
336 - .padStart(2, '0');  
337 - const m = Math.floor((totalSeconds.value % 3600) / 60)  
338 - .toString()  
339 - .padStart(2, '0');  
340 - const s = (totalSeconds.value % 60).toString().padStart(2, '0');  
341 - return `${h}:${m}:${s}`;  
342 -});  
343 -// 继续或者暂停计时  
344 -const toggleTimer = () => trainingStore.toggleTimer();  
345 -// 重置时间  
346 -const resetTimer = () => trainingStore.resetTimer();  
347 -  
348 -// --- Picker 修改时间 ---  
349 -  
350 -  
351 -const timeColumns = reactive([ 454 + };
  455 +
  456 + const formattedTime = computed(() =>
  457 + dayjs.duration(totalSeconds.value, 'seconds').format('HH:mm:ss'),
  458 + );
  459 + const toggleTimer = () => trainingStore.toggleTimer();
  460 + const resetTimer = () => trainingStore.resetTimer();
  461 +
  462 + const timeColumns = ref([
352 Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'), 463 Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'),
353 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'), 464 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),
354 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'), 465 Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),
355 -]); 466 + ]);
356 467
357 -const handleTimerClick = () => { 468 + const handleTimerClick = () => {
358 if (isPause.value) { 469 if (isPause.value) {
359 trainingStore.openTimePicker(); 470 trainingStore.openTimePicker();
360 } 471 }
361 -}; 472 + };
362 473
363 -const onTimeConfirm = (e) => { 474 + const onTimeConfirm = (e) => {
364 const rawValues = e.value.map((val) => parseInt(String(val).replace(/[时分秒]/g, ''))); 475 const rawValues = e.value.map((val) => parseInt(String(val).replace(/[时分秒]/g, '')));
365 const seconds = rawValues[0] * 3600 + rawValues[1] * 60 + rawValues[2]; 476 const seconds = rawValues[0] * 3600 + rawValues[1] * 60 + rawValues[2];
366 trainingStore.setTotalSeconds(seconds); 477 trainingStore.setTotalSeconds(seconds);
367 trainingStore.closeTimePicker(); 478 trainingStore.closeTimePicker();
368 -};  
369 -// --- 基础弹窗 ---  
370 -const show = ref(false);  
371 -const datePickerShow = ref(false);  
372 -const timePickerShow = ref(false);  
373 -  
374 -const dateValue = ref(Number(new Date()));  
375 -const timeValue = ref('14:13');  
376 -const trainingDateText = ref('2026-04-26 周日');  
377 -// const trainingTimeText = ref('14:13');  
378 -// const trainingTimeText = ref(new Date().getHours().toString().padStart(2, '0') + ':' + new Date().getMinutes().toString().padStart(2, '0'));  
379 -const trainingTimeText = computed(() => trainingStore.trainingTimeText);  
380 -  
381 -const confirmDate = (e) => {  
382 - const date = new Date(e.value);  
383 - const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()];  
384 - trainingDateText.value = `${date.getFullYear()}-${(date.getMonth() + 1)  
385 - .toString()  
386 - .padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${week}`; 479 + };
  480 + const confirmDate = (e) => {
  481 + trainingDateText.value = dayjs(e.value).format('YYYY-MM-DD dd');
387 datePickerShow.value = false; 482 datePickerShow.value = false;
388 -}; 483 + };
389 484
390 -const confirmTime = (e) => { 485 + const confirmTime = (e) => {
391 trainingStore.setTrainingTimeText(e.value); 486 trainingStore.setTrainingTimeText(e.value);
392 timePickerShow.value = false; 487 timePickerShow.value = false;
393 -};  
394 -  
395 -const save = async () => {  
396 - await nextTick()  
397 - let units = [] 488 + };
  489 +
  490 + const mapSets = (recordItems, exerciseType) =>
  491 + recordItems.map((item, index) => {
  492 + const isInterval = exerciseType === 6;
  493 + const duration = isInterval
  494 + ? Number(item.duration) || 0
  495 + : Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0;
  496 + const restTime = isInterval
  497 + ? Number(item.restTime) || 0
  498 + : parseInt(item.quickTimeDisplay) || 0;
  499 + return {
  500 + weight: Number(item.weight) || 0,
  501 + reps: Number(item.reps) || 0,
  502 + distance: Number(item.distance) || 0,
  503 + duration,
  504 + restTime,
  505 + setIndex: index + 1,
  506 + };
  507 + });
398 508
399 - const unitList = trainingStore.actionDetail.units || []  
400 - console.log('✅ 最终要提交的 unitList:', unitList)  
401 - console.log('✅ 子组件实例 tempList:', dongzuoRefs.value) 509 + const save = async () => {
  510 + await nextTick();
  511 + const units = [];
  512 + const unitList = actionDetail.value?.units || [];
402 513
403 for (let i = 0; i < unitList.length; i++) { 514 for (let i = 0; i < unitList.length; i++) {
404 - const unit = unitList[i]  
405 - const child = dongzuoRefs.value[i] 515 + const unit = unitList[i];
  516 + const child = dongzuoRefs.value[i];
  517 + if (!child) continue;
406 518
407 - if (!child) {  
408 - console.warn(`第 ${i} 个动作组件未获取到!`)  
409 - continue  
410 - }  
411 -  
412 - // ==========================  
413 - // 1. 单个动作  
414 - // ==========================  
415 if (unit.unitType === 1) { 519 if (unit.unitType === 1) {
416 - if (!child.recordList) continue  
417 -  
418 - const exerciseType = unit.exercises[0].exerciseType  
419 - console.log(`\n🔹 第 ${i} 个动作 - 运动类型 =`, exerciseType) // 👈 打印运动类型  
420 -  
421 - const exercises = [{  
422 - exerciseId: unit.exercises[0].exerciseId,  
423 - exerciseType: exerciseType,  
424 - sets: child.recordList.map((item, index) => {  
425 - let duration = 0  
426 - let restTime = 0  
427 -  
428 - // ==============================================  
429 - // 判断是不是间歇运动  
430 - // ==============================================  
431 - if (exerciseType === 6) {  
432 - // ✅ 间歇运动:取你输入的 duration、restTime  
433 - duration = Number(item.duration) || 0  
434 - restTime = Number(item.restTime) || 0  
435 - console.log(`✅ 组 ${index + 1}【间歇运动】输入的值:`, {  
436 - duration: item.duration,  
437 - restTime: item.restTime,  
438 - reps: item.reps  
439 - }) // 控制台打印  
440 - } else {  
441 - // ✅ 普通运动:保持原来逻辑  
442 - duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0  
443 - restTime = parseInt(item.quickTimeDisplay) || 0  
444 - }  
445 -  
446 - return {  
447 - weight: Number(item.weight) || 0,  
448 - reps: Number(item.reps) || 0,  
449 - distance: Number(item.distance) || 0,  
450 - duration: duration,  
451 - restTime: restTime,  
452 - setIndex: index + 1  
453 - }  
454 - })  
455 - }] 520 + if (!child.recordList) continue;
  521 + const exerciseType = unit.exercises[0].exerciseType;
456 units.push({ 522 units.push({
457 name: i === 0 ? trainingName.value : unit.unitName, 523 name: i === 0 ? trainingName.value : unit.unitName,
458 unitType: 1, 524 unitType: 1,
459 - exercises  
460 - })  
461 - }  
462 -  
463 - // ==========================  
464 - // 2. 超级组  
465 - // ==========================  
466 - else if (unit.unitType === 2) {  
467 - if (!child.superRecordMap) continue  
468 -  
469 - const exercises = unit.exercises.map((sub, idx) => {  
470 - const setList = child.superRecordMap[sub.exerciseId] || []  
471 - const subExerciseType = sub.exerciseType  
472 -  
473 - console.log(`超级组子动作 ${idx + 1} - 运动类型 =`, subExerciseType)  
474 -  
475 - return {  
476 - exerciseId: sub.exerciseId,  
477 - exerciseType: subExerciseType,  
478 - innerOrder: idx + 1,  
479 - sets: setList.map((item, index) => {  
480 - let duration = 0  
481 - let restTime = 0  
482 -  
483 - // ==============================================  
484 - // 超级组里的间歇运动  
485 - // ==============================================  
486 - if (subExerciseType === 6) {  
487 - duration = Number(item.duration) || 0  
488 - restTime = Number(item.restTime) || 0  
489 - console.log(`组 ${index + 1}【超级组间歇运动】输入的值:`, {  
490 - duration: item.duration,  
491 - restTime: item.restTime,  
492 - reps: item.reps  
493 - }) // 控制台打印  
494 - } else {  
495 - duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0  
496 - restTime = parseInt(item.quickTimeDisplay) || 0  
497 - }  
498 -  
499 - return {  
500 - weight: Number(item.weight) || 0,  
501 - reps: Number(item.reps) || 0,  
502 - distance: Number(item.distance) || 0,  
503 - duration: duration, // 修复后  
504 - restTime: restTime, // 修复后  
505 - setIndex: index + 1  
506 - }  
507 - })  
508 - }  
509 - }) 525 + exercises: [
  526 + {
  527 + exerciseId: unit.exercises[0].exerciseId,
  528 + exerciseType,
  529 + sets: mapSets(child.recordList, exerciseType),
  530 + },
  531 + ],
  532 + });
  533 + } else if (unit.unitType === 2) {
  534 + if (!child.superRecordMap) continue;
510 units.push({ 535 units.push({
511 name: i === 0 ? trainingName.value : unit.unitName, 536 name: i === 0 ? trainingName.value : unit.unitName,
512 unitType: 2, 537 unitType: 2,
513 - exercises  
514 - }) 538 + exercises: unit.exercises.map((sub, idx) => ({
  539 + exerciseId: sub.exerciseId,
  540 + exerciseType: sub.exerciseType,
  541 + innerOrder: idx + 1,
  542 + sets: mapSets(child.superRecordMap[sub.exerciseId] || [], sub.exerciseType),
  543 + })),
  544 + });
515 } 545 }
516 } 546 }
517 547
518 - console.log('\n最终提交后台的完整数据:', JSON.stringify(units, null, 2))  
519 -  
520 if (units.length === 0) { 548 if (units.length === 0) {
521 - uni.showToast({ title: '未获取到训练数据', icon: 'none' })  
522 - return 549 + uni.showToast({ title: '未获取到训练数据', icon: 'none' });
  550 + return;
523 } 551 }
524 552
525 - console.log('trainingStore.isTraining=', trainingStore.isTraining);  
526 -  
527 try { 553 try {
528 - // ✅ 情况1:【去训练】→ 保存历史  
529 if (trainingStore.isTraining) { 554 if (trainingStore.isTraining) {
530 await TrainingApi.createTrainHistory({ units }); 555 await TrainingApi.createTrainHistory({ units });
531 uni.showToast({ title: '训练提交成功', icon: 'success' }); 556 uni.showToast({ title: '训练提交成功', icon: 'success' });
532 - }  
533 - // ✅ 情况2:【编辑模板】→ 更新模板  
534 - else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) { 557 + } else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
535 await dailytemplateApi.updateDailyTemplate({ 558 await dailytemplateApi.updateDailyTemplate({
536 dailyTemplateId: trainingStore.dailyTemplateId, 559 dailyTemplateId: trainingStore.dailyTemplateId,
537 - units: units 560 + units,
538 }); 561 });
539 uni.showToast({ title: '模板修改成功', icon: 'success' }); 562 uni.showToast({ title: '模板修改成功', icon: 'success' });
540 - }  
541 - // ✅ 情况3:普通训练  
542 - else { 563 + } else {
543 await TrainingApi.createTrainHistory({ units }); 564 await TrainingApi.createTrainHistory({ units });
544 uni.showToast({ title: '训练提交成功', icon: 'success' }); 565 uni.showToast({ title: '训练提交成功', icon: 'success' });
545 } 566 }
546 -  
547 setTimeout(() => { 567 setTimeout(() => {
548 uni.navigateBack(); 568 uni.navigateBack();
549 trainingStore.clearTrainingStore(); 569 trainingStore.clearTrainingStore();
550 - }, 1500)  
551 - trainingStore.clearTrainingStore() 570 + }, 1500);
552 } catch (err) { 571 } catch (err) {
553 - console.error('❌ 保存失败:', err)  
554 - uni.showToast({ title: '保存失败', icon: 'none' }) 572 + console.error('保存失败:', err);
  573 + uni.showToast({ title: '保存失败', icon: 'none' });
555 } 574 }
556 -}  
557 -// 删除训练  
558 -const deleteTraining = () => { 575 + };
  576 + // 删除训练
  577 + const deleteTraining = () => {
559 uni.showModal({ 578 uni.showModal({
560 title: '确认删除', 579 title: '确认删除',
561 content: '确定要删除本次训练记录吗?删除后无法恢复', 580 content: '确定要删除本次训练记录吗?删除后无法恢复',
562 confirmText: '确认删除', 581 confirmText: '确认删除',
563 - confirmColor: '#FF3B30', // 红色强调删除 582 + confirmColor: '#FF3B30',
564 cancelText: '取消', 583 cancelText: '取消',
565 success: (res) => { 584 success: (res) => {
566 - // 用户点击确认  
567 if (res.confirm) { 585 if (res.confirm) {
568 - uni.showToast({  
569 - title: '删除成功',  
570 - icon: 'success',  
571 - duration: 1500  
572 - });  
573 - setTimeout(async () => { 586 + uni.showToast({ title: '删除成功', icon: 'success', duration: 1500 });
  587 + setTimeout(() => {
574 uni.navigateBack(); 588 uni.navigateBack();
575 - await nextTick();  
576 - trainingStore.clearTrainingStore() 589 + trainingStore.clearTrainingStore();
577 }, 1500); 590 }, 1500);
578 } 591 }
579 - else if (res.cancel) {  
580 - console.log('用户取消删除');  
581 - }  
582 - } 592 + },
583 }); 593 });
584 -};  
585 -// 动作新增组件  
586 -const addActionsPopup = () => {  
587 - console.log('打开新增弹窗');  
588 - addActionsShow.value = true // 打开弹窗  
589 -  
590 -} 594 + };
591 595
592 -// 最小化  
593 -const openMin = () => {  
594 - console.log('最小化跳转前trainingStore.isTraining', trainingStore.isTraining);  
595 - console.log('trainingStore.min+++++++++++++', trainingStore.min); 596 + const addActionsPopup = () => {
  597 + addActionsShow.value = true;
  598 + };
596 599
597 - console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining); 600 + const openMin = () => {
598 trainingStore.min = true; 601 trainingStore.min = true;
599 - // 保存快照  
600 - trainingSnapshot.data = JSON.parse(JSON.stringify({ 602 + trainingSnapshot.data = JSON.parse(
  603 + JSON.stringify({
601 id: trainingStore.id, 604 id: trainingStore.id,
602 type: trainingStore.type, 605 type: trainingStore.type,
603 actionDetail: trainingStore.actionDetail, 606 actionDetail: trainingStore.actionDetail,
@@ -613,44 +616,30 @@ const openMin = () => { @@ -613,44 +616,30 @@ const openMin = () => {
613 isSystem: trainingStore.isSystem, 616 isSystem: trainingStore.isSystem,
614 dailyTemplateId: trainingStore.dailyTemplateId, 617 dailyTemplateId: trainingStore.dailyTemplateId,
615 isTraining: trainingStore.isTraining, 618 isTraining: trainingStore.isTraining,
616 - })) 619 + }),
  620 + );
617 621
618 // 2. 记录时间戳和计时器状态 622 // 2. 记录时间戳和计时器状态
619 - trainingSnapshot.timestamp = Date.now()  
620 - trainingSnapshot.timerWasRunning = !trainingStore.isPause 623 + trainingSnapshot.timestamp = Date.now();
  624 + trainingSnapshot.timerWasRunning = !trainingStore.isPause;
  625 + trainingSnapshot.visible = true;
621 626
622 - // 3. 显示悬浮球  
623 - trainingSnapshot.visible = true  
624 -  
625 - // 4. 如果计时器在跑,先暂停(setInterval 跨页面会丢失,回显时用时间戳补偿)  
626 if (!trainingStore.isPause) { 627 if (!trainingStore.isPause) {
627 - trainingStore.toggleTimer() 628 + trainingStore.toggleTimer();
628 } 629 }
629 630
630 - console.log('🟡 快照已保存:id=', trainingSnapshot.data.id,  
631 - 'totalSeconds=', trainingSnapshot.data.totalSeconds)  
632 - // uni.navigateTo({  
633 - // url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径  
634 - // });  
635 - console.log('++++++++++++++最小化开始trainingStore.min+++++++++++++', trainingStore.min);  
636 -  
637 uni.navigateBack(); 631 uni.navigateBack();
638 -}  
639 -  
640 -onUnmounted(() => {  
641 - // trainingStore.clearTrainingStore()  
642 -})  
643 - 632 + };
644 </script> 633 </script>
645 634
646 <style lang="scss" scoped> 635 <style lang="scss" scoped>
647 -.container { 636 + .container {
648 min-height: 100vh; 637 min-height: 100vh;
649 - background-color: #1c1c1e; 638 + background-color: #242424;
650 padding-bottom: 120rpx; 639 padding-bottom: 120rpx;
651 -} 640 + }
652 641
653 -.status-header { 642 + .status-header {
654 display: flex; 643 display: flex;
655 justify-content: space-between; 644 justify-content: space-between;
656 align-items: center; 645 align-items: center;
@@ -693,10 +682,10 @@ onUnmounted(() => { @@ -693,10 +682,10 @@ onUnmounted(() => {
693 font-weight: bold; 682 font-weight: bold;
694 font-size: 28rpx; 683 font-size: 28rpx;
695 } 684 }
696 -} 685 + }
697 686
698 -.action-list {  
699 - padding: 20rpx; 687 + .action-list {
  688 + padding: 0 0 20rpx;
700 689
701 .header { 690 .header {
702 display: flex; 691 display: flex;
@@ -721,9 +710,9 @@ onUnmounted(() => { @@ -721,9 +710,9 @@ onUnmounted(() => {
721 font-size: 24rpx; 710 font-size: 24rpx;
722 } 711 }
723 } 712 }
724 -} 713 + }
725 714
726 -.empty-state { 715 + .empty-state {
727 display: flex; 716 display: flex;
728 flex-direction: column; 717 flex-direction: column;
729 align-items: center; 718 align-items: center;
@@ -740,12 +729,11 @@ onUnmounted(() => { @@ -740,12 +729,11 @@ onUnmounted(() => {
740 .empty-subtext { 729 .empty-subtext {
741 font-size: 28rpx; 730 font-size: 28rpx;
742 } 731 }
743 -} 732 + }
744 733
745 -/* 👇 底部气泡提示样式 */  
746 -.tip-bubble { 734 + .tip-bubble {
747 position: fixed; 735 position: fixed;
748 - bottom: 230rpx; // 刚好在底部导航上方 736 + bottom: 230rpx;
749 left: 52%; 737 left: 52%;
750 transform: translateX(-50%); 738 transform: translateX(-50%);
751 background-color: #000; 739 background-color: #000;
@@ -755,7 +743,6 @@ onUnmounted(() => { @@ -755,7 +743,6 @@ onUnmounted(() => {
755 font-size: 28rpx; 743 font-size: 28rpx;
756 z-index: 9999; 744 z-index: 9999;
757 745
758 - // 小三角  
759 &::after { 746 &::after {
760 content: ''; 747 content: '';
761 position: absolute; 748 position: absolute;
@@ -766,9 +753,9 @@ onUnmounted(() => { @@ -766,9 +753,9 @@ onUnmounted(() => {
766 border-right: 10rpx solid transparent; 753 border-right: 10rpx solid transparent;
767 border-top: 10rpx solid #000; 754 border-top: 10rpx solid #000;
768 } 755 }
769 -} 756 + }
770 757
771 -.footer-bar { 758 + .footer-bar {
772 position: fixed; 759 position: fixed;
773 bottom: 0; 760 bottom: 0;
774 left: 0; 761 left: 0;
@@ -801,16 +788,13 @@ onUnmounted(() => { @@ -801,16 +788,13 @@ onUnmounted(() => {
801 788
802 .add-btn-wrapper { 789 .add-btn-wrapper {
803 margin-top: -60rpx; 790 margin-top: -60rpx;
804 - width: 120rpx !important;  
805 - /* 必须加 */  
806 - height: 120rpx !important;  
807 - /* 必须加 */ 791 + width: 120rpx;
  792 + height: 120rpx;
808 display: flex; 793 display: flex;
809 justify-content: center; 794 justify-content: center;
810 align-items: center; 795 align-items: center;
811 position: relative; 796 position: relative;
812 - z-index: 9999 !important;  
813 - /* 必须加 */ 797 + z-index: 9999;
814 } 798 }
815 799
816 .add-btn { 800 .add-btn {
@@ -823,14 +807,11 @@ onUnmounted(() => { @@ -823,14 +807,11 @@ onUnmounted(() => {
823 align-items: center; 807 align-items: center;
824 box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3); 808 box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
825 position: relative; 809 position: relative;
826 - z-index: 99999 !important;  
827 - /* 必须加 */ 810 + z-index: 99999;
  811 + }
828 } 812 }
829 813
830 - //  
831 -}  
832 -  
833 -.time-pop-container { 814 + .time-pop-container {
834 padding: 40rpx 30rpx; 815 padding: 40rpx 30rpx;
835 color: #fff; 816 color: #fff;
836 817
@@ -946,11 +927,9 @@ onUnmounted(() => { @@ -946,11 +927,9 @@ onUnmounted(() => {
946 border-radius: 4rpx; 927 border-radius: 4rpx;
947 margin: 40rpx auto 0; 928 margin: 40rpx auto 0;
948 } 929 }
949 -}  
950 - 930 + }
951 931
952 -// 写总结弹窗样式  
953 -.summaryPopup { 932 + .summaryPopup {
954 width: 100vw; 933 width: 100vw;
955 height: 75vh; 934 height: 75vh;
956 background-color: #2c2c2e; 935 background-color: #2c2c2e;
@@ -972,7 +951,7 @@ onUnmounted(() => { @@ -972,7 +951,7 @@ onUnmounted(() => {
972 951
973 .save-btn { 952 .save-btn {
974 background-color: #f8d714; 953 background-color: #f8d714;
975 - color: #000000; 954 + color: #000;
976 font-size: 28rpx; 955 font-size: 28rpx;
977 font-weight: 500; 956 font-weight: 500;
978 padding: 12rpx 30rpx; 957 padding: 12rpx 30rpx;
@@ -996,10 +975,9 @@ onUnmounted(() => { @@ -996,10 +975,9 @@ onUnmounted(() => {
996 color: #8e8e93; 975 color: #8e8e93;
997 } 976 }
998 } 977 }
999 -} 978 + }
1000 979
1001 -// 训练命名弹窗样式(与总结弹窗风格完全统一)  
1002 -.namePopup { 980 + .namePopup {
1003 width: 100%; 981 width: 100%;
1004 height: 75vh; 982 height: 75vh;
1005 background-color: #2c2c2e; 983 background-color: #2c2c2e;
@@ -1022,7 +1000,7 @@ onUnmounted(() => { @@ -1022,7 +1000,7 @@ onUnmounted(() => {
1022 1000
1023 .save-btn { 1001 .save-btn {
1024 background-color: #f8d714; 1002 background-color: #f8d714;
1025 - color: #000000; 1003 + color: #000;
1026 font-size: 28rpx; 1004 font-size: 28rpx;
1027 font-weight: 500; 1005 font-weight: 500;
1028 padding: 12rpx 30rpx; 1006 padding: 12rpx 30rpx;
@@ -1031,18 +1009,94 @@ onUnmounted(() => { @@ -1031,18 +1009,94 @@ onUnmounted(() => {
1031 } 1009 }
1032 1010
1033 .name-input { 1011 .name-input {
1034 - width: calc(100% - 60rpx); // 关键:设置宽度,让它左右留空,不贴边  
1035 - margin: 0 auto; // 关键:水平居中 1012 + width: calc(100% - 60rpx);
  1013 + margin: 0 auto;
1036 background-color: #1f1f1f; 1014 background-color: #1f1f1f;
1037 border-radius: 12rpx; 1015 border-radius: 12rpx;
1038 padding: 20rpx; 1016 padding: 20rpx;
1039 font-size: 28rpx; 1017 font-size: 28rpx;
1040 color: #fff; 1018 color: #fff;
1041 - min-height: 220rpx; // 可选:增加一点高度,和例图更像 1019 + min-height: 220rpx;
1042 1020
1043 &::placeholder { 1021 &::placeholder {
1044 color: #8e8e93; 1022 color: #8e8e93;
1045 } 1023 }
1046 } 1024 }
1047 -} 1025 + }
  1026 +
  1027 + // ===================== 组间休息倒计时 =====================
  1028 + .rest-countdown {
  1029 + position: fixed;
  1030 + bottom: 180rpx;
  1031 + right: 30rpx;
  1032 + z-index: 100;
  1033 + animation: countdown-enter 0.3s ease-out;
  1034 + }
  1035 +
  1036 + @keyframes countdown-enter {
  1037 + from {
  1038 + opacity: 0;
  1039 + transform: scale(0.5);
  1040 + }
  1041 +
  1042 + to {
  1043 + opacity: 1;
  1044 + transform: scale(1);
  1045 + }
  1046 + }
  1047 +
  1048 + .countdown-circle {
  1049 + position: relative;
  1050 + width: 160rpx;
  1051 + height: 160rpx;
  1052 + border-radius: 50%;
  1053 + border: 6rpx solid #4caf50;
  1054 + display: flex;
  1055 + align-items: center;
  1056 + justify-content: center;
  1057 +
  1058 + .ring-svg {
  1059 + position: absolute;
  1060 + top: 0;
  1061 + left: 0;
  1062 + width: 100%;
  1063 + height: 100%;
  1064 + }
  1065 +
  1066 + .ring-bg {
  1067 + fill: none;
  1068 + stroke: #2c2c2e;
  1069 + stroke-width: 6;
  1070 + }
  1071 +
  1072 + .ring-progress {
  1073 + fill: none;
  1074 + stroke: #4caf50;
  1075 + stroke-width: 6;
  1076 + stroke-linecap: round;
  1077 + transition: stroke-dashoffset 0.5s linear;
  1078 + }
  1079 +
  1080 + .countdown-inner {
  1081 + display: flex;
  1082 + flex-direction: column;
  1083 + align-items: center;
  1084 + justify-content: center;
  1085 + position: relative;
  1086 + z-index: 1;
  1087 + }
  1088 +
  1089 + .rest-time {
  1090 + font-size: 36rpx;
  1091 + font-weight: bold;
  1092 + color: #ffffff;
  1093 + font-family: monospace;
  1094 + }
  1095 +
  1096 + .rest-total {
  1097 + font-size: 20rpx;
  1098 + color: #8e8e93;
  1099 + margin-top: 4rpx;
  1100 + }
  1101 + }
1048 </style> 1102 </style>
1 <template> 1 <template>
2 <view class="train-template-page"> 2 <view class="train-template-page">
3 - <!-- 头部 -->  
4 - <view class="header">  
5 - <view class="back-btn" @click="goBack">  
6 - <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>  
7 - </view>  
8 - <text class="title">{{ TemplateDetail.templateName }}</text>  
9 - </view>  
10 - <!-- 统计信息 -->  
11 - <view class="stats">  
12 - <text class="stat-item">  
13 - <text class="icon">📊</text> {{ TemplateDetail.exerciseCount }}个动作 · {{ TemplateDetail.totalSets }}组 ·  
14 - {{ TemplateDetail.totalWeight }}kg  
15 - </text>  
16 - <text class="stat-item">  
17 - <text class="icon">⚙️</text>  
18 - <template v-if="isOffice">  
19 - {{ TemplateDetail?.equipmentsUsed }}  
20 - </template>  
21 - <template v-else>  
22 - {{ (TemplateDetail?.equipmentNames || []).join(',') }} 3 + <!-- ==================== 导航栏 ==================== -->
  4 + <up-navbar :auto-back="true" bg-color="#121212" left-icon-color="#fff" placeholder>
  5 + <template #center>
  6 + <text class="navbar-title">{{ templateDetail.templateName }}</text>
23 </template> 7 </template>
  8 + </up-navbar>
24 9
  10 + <!-- ==================== 统计信息 ==================== -->
  11 + <view class="stats">
  12 + <view class="stat-item">
  13 + <up-icon name="grid" color="#ffd100" size="18" />
  14 + <text class="stat-text">
  15 + {{ templateDetail.exerciseCount }}个动作 · {{ templateDetail.totalSets }}组 ·
  16 + {{ templateDetail.totalWeight }}kg
25 </text> 17 </text>
26 </view> 18 </view>
27 - <!-- 肌肉图 --> 19 + <view class="stat-item">
  20 + <up-icon name="setting" color="#ffd100" size="18" />
  21 + <text class="stat-text">{{ equipmentDisplayText }}</text>
  22 + </view>
  23 + </view>
  24 +
  25 + <!-- ==================== 肌肉图 ==================== -->
28 <view class="muscle-map"> 26 <view class="muscle-map">
29 - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png"  
30 - mode="aspectFill" class="muscle-img" /> 27 + <image :src="MUSCLE_IMG" mode="aspectFill" class="muscle-img" />
31 </view> 28 </view>
32 - <!-- 显示主要和次要训练部位 -->  
33 - <view> 29 +
  30 + <!-- ==================== 训练部位 ==================== -->
34 <view class="primary-muscle"> 31 <view class="primary-muscle">
35 - <!-- 主要训练部位 -->  
36 - <text class="section-title">主要训练部位: </text> 32 + <view class="muscle-row">
  33 + <text class="section-title">主要训练部位:</text>
37 <text class="description"> 34 <text class="description">
38 - {{ (TemplateDetail.primaryMuscleNames || []).join(', ') || '暂无数据' }} 35 + {{ (templateDetail.primaryMuscleNames || []).join('、') || '暂无数据' }}
39 </text> 36 </text>
40 - <!-- 换行 -->  
41 - <view class="divider"></view>  
42 - <!-- 次要训练部位 -->  
43 - <text class="section-title">次要训练部位: </text> 37 + </view>
  38 + <view class="divider" />
  39 + <view class="muscle-row">
  40 + <text class="section-title">次要训练部位:</text>
44 <text class="description"> 41 <text class="description">
45 - {{ (TemplateDetail.secondaryMuscleNames || []).join(', ') || '暂无数据' }} 42 + {{ (templateDetail.secondaryMuscleNames || []).join('、') || '暂无数据' }}
46 </text> 43 </text>
47 </view> 44 </view>
48 </view> 45 </view>
49 - <!-- 模板介绍 --> 46 +
  47 + <!-- ==================== 模板介绍 ==================== -->
50 <view class="section"> 48 <view class="section">
51 - <text class="section-title">模板介绍: </text>  
52 - <text class="description">  
53 - {{ TemplateDetail.introduction }}  
54 - </text> 49 + <text class="section-title">模板介绍:</text>
  50 + <text class="description">{{ templateDetail.introduction }}</text>
55 </view> 51 </view>
56 - <!-- 动作列表 --> 52 +
  53 + <!-- ==================== 动作列表 ==================== -->
57 <view class="section"> 54 <view class="section">
58 <view class="section-top"> 55 <view class="section-top">
59 <text class="section-title">动作列表</text> 56 <text class="section-title">动作列表</text>
60 - <view v-if="date" class="right-btn" @click="templateEdit(TemplateDetail)">修改训练内容</view> 57 + <view v-if="date" class="right-btn" @click="templateEdit">修改训练内容</view>
61 </view> 58 </view>
62 - <view v-if="TemplateUnits.length > 0">  
63 - <!-- 循环 unit,每个 unit 一个卡片 -->  
64 - <view v-for="(unit, unitIndex) in TemplateUnits" :key="unitIndex" class="exercise-item" 59 +
  60 + <template v-if="templateUnits.length">
  61 + <view v-for="(unit, unitIndex) in templateUnits" :key="unitIndex" class="exercise-item"
65 @click="handleUnitClick(unit)"> 62 @click="handleUnitClick(unit)">
  63 + <!-- 单元头部 -->
66 <view class="unit-header"> 64 <view class="unit-header">
67 <view class="left"> 65 <view class="left">
68 <text class="exercise-number">{{ unitIndex + 1 }}</text> 66 <text class="exercise-number">{{ unitIndex + 1 }}</text>
69 - <!-- 图片 -->  
70 - <view v-if="unit.unitType === 1" class="unitImage">  
71 - <!-- 3D字段 -->  
72 - <image :src="unit.exercises[0]?.exerciseCover || lostImage" mode="aspectFill" class="unit-icon" />  
73 - </view>  
74 - <view v-else class="unitImage">  
75 - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"  
76 - mode="aspectFill" class="unit-icon" /> 67 + <view class="unitImage">
  68 + <image :src="getUnitCover(unit)" mode="aspectFill" class="unit-icon" />
77 </view> 69 </view>
78 <view class="unit-title"> 70 <view class="unit-title">
79 <view class="name">{{ unit.unitName || `超级组 ${unitIndex + 1}` }}</view> 71 <view class="name">{{ unit.unitName || `超级组 ${unitIndex + 1}` }}</view>
@@ -87,86 +79,46 @@ @@ -87,86 +79,46 @@
87 </view> 79 </view>
88 </view> 80 </view>
89 <view class="right"> 81 <view class="right">
90 - <up-icon name="arrow-left" color="#fff" size="24"></up-icon> 82 + <up-icon name="arrow-left" color="#fff" size="18" />
91 </view> 83 </view>
92 -  
93 </view> 84 </view>
94 85
95 - <!-- 情况1:普通动作 unitType = 1 -->  
96 - <view v-if="unit.unitType === 1 && unit.exercises && unit.exercises.length" class="normal-action-wrap">  
97 - <!-- 取第一个动作(普通动作unit里永远只有1个exercise) --> 86 + <!-- 普通动作:unitType === 1 -->
  87 + <view v-if="unit.unitType === 1 && unit.exercises?.length" class="normal-action-wrap">
98 <view class="action-content"> 88 <view class="action-content">
99 - <!-- 单动作组次列表 -->  
100 <view v-for="(detail, i) in unit.exercises[0].sets || []" :key="i" class="detail-row"> 89 <view v-for="(detail, i) in unit.exercises[0].sets || []" :key="i" class="detail-row">
101 - <view class="detail-label">{{ i + 1 }}</view> 90 + <text class="detail-label">{{ i + 1 }}</text>
102 <view class="sets-data"> 91 <view class="sets-data">
103 - <!-- 保留你原来所有 exerciseType 判断 & 文本渲染逻辑 -->  
104 - <view class="detail-value" v-if="[0, 4, 5].includes(unit.exercises[0].exerciseType)">  
105 - {{ detail.weight }}kg × {{ detail.reps }}次  
106 - </view>  
107 - <view class="detail-value" v-if="unit.exercises[0].exerciseType === 1">  
108 - {{ detail.distance }}km x {{ formatSeconds(detail.duration) }}  
109 - </view>  
110 - <view class="detail-value" v-if="unit.exercises[0].exerciseType === 2">  
111 - {{ detail.reps }}次  
112 - </view>  
113 - <view class="detail-value" v-if="unit.exercises[0].exerciseType === 3">  
114 - {{ formatSeconds(detail.duration) }}  
115 - </view>  
116 - <view class="detail-value" v-if="unit.exercises[0].exerciseType === 6">  
117 - {{ detail.reps }}组 x {{ detail.duration }}秒 x {{ detail.restTime }}秒/组休息  
118 - </view>  
119 - <view class="rest" v-if="detail.restTime && unit.exercises[0].exerciseType !== 6">  
120 - {{ detail.restTime }}s  
121 - </view> 92 + <text class="detail-value">{{ formatSetText(unit.exercises[0].exerciseType, detail) }}</text>
  93 + <text v-if="detail.restTime && unit.exercises[0].exerciseType !== 6" class="rest">{{ detail.restTime
  94 + }}s</text>
122 </view> 95 </view>
123 </view> 96 </view>
124 </view> 97 </view>
125 </view> 98 </view>
126 99
127 - <!-- 情况2:超级组 unitType = 2 --> 100 + <!-- 超级组:unitType === 2 -->
128 <view v-else-if="unit.unitType === 2" class="super-action-wrap"> 101 <view v-else-if="unit.unitType === 2" class="super-action-wrap">
129 - <!-- 1. 超级组内部所有动作名称列表(A/B/C... + 动作名) --> 102 + <!-- 超级组内动作名称 -->
130 <view class="super-actionName"> 103 <view class="super-actionName">
131 - <view class="name-item" v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId"> 104 + <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="name-item">
132 <text class="letter">{{ String.fromCharCode(65 + exIdx) }} </text> 105 <text class="letter">{{ String.fromCharCode(65 + exIdx) }} </text>
133 <text class="action-name">{{ ex.exerciseName || '动作名称' }}</text> 106 <text class="action-name">{{ ex.exerciseName || '动作名称' }}</text>
134 </view> 107 </view>
135 </view> 108 </view>
136 109
137 - <!-- 2. 超级组:按【组号】循环(取第一个动作的组数作为总组数) --> 110 + <!-- 超级组按组号循环 -->
138 <view class="super-set-list"> 111 <view class="super-set-list">
139 <view v-for="idx in getSuperSetMaxGroupCount(unit.exercises)" :key="idx" class="super-set-item"> 112 <view v-for="idx in getSuperSetMaxGroupCount(unit.exercises)" :key="idx" class="super-set-item">
140 <text class="detail-label">{{ idx }}</text> 113 <text class="detail-label">{{ idx }}</text>
141 <view class="set-column"> 114 <view class="set-column">
142 - <!-- 遍历当前组下的每一个动作 -->  
143 <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="action-in-set"> 115 <view v-for="(ex, exIdx) in unit.exercises" :key="ex.exerciseId" class="action-in-set">
144 - <!-- 容错:当前动作无该组则不渲染 -->  
145 <template v-if="ex.sets[idx - 1]"> 116 <template v-if="ex.sets[idx - 1]">
146 - <view class="action-letter">  
147 - {{ idx }}{{ String.fromCharCode(65 + exIdx) }}  
148 - </view> 117 + <text class="action-letter">{{ idx }}{{ String.fromCharCode(65 + exIdx) }}</text>
149 <view class="set-content"> 118 <view class="set-content">
150 - <!-- 复用你原有的 动作类型 文本逻辑 -->  
151 - <text v-if="[0, 4, 5].includes(ex.exerciseType)">  
152 - {{ ex.sets[idx - 1].weight }}kg × {{ ex.sets[idx - 1].reps }}次  
153 - </text>  
154 - <text v-if="ex.exerciseType === 1">  
155 - {{ ex.sets[idx - 1].distance }}km x {{ formatSeconds(ex.sets[idx - 1].duration) }}  
156 - </text>  
157 - <text v-if="ex.exerciseType === 2">  
158 - {{ ex.sets[idx - 1].reps }}次  
159 - </text>  
160 - <text v-if="ex.exerciseType === 3">  
161 - {{ formatSeconds(ex.sets[idx - 1].duration) }}  
162 - </text>  
163 - <text v-if="ex.exerciseType === 6">  
164 - {{ ex.sets[idx - 1].reps }}组 x {{ ex.sets[idx - 1].duration }}秒 x {{ ex.sets[idx - 1].restTime  
165 - }}秒/组休息  
166 - </text>  
167 - <view class="rest" v-if="ex.sets[idx - 1].restTime && ex.exerciseType !== 6">  
168 - {{ ex.sets[idx - 1].restTime }}s  
169 - </view> 119 + <text>{{ formatSetText(ex.exerciseType, ex.sets[idx - 1]) }}</text>
  120 + <text v-if="ex.sets[idx - 1].restTime && ex.exerciseType !== 6" class="rest">{{ ex.sets[idx -
  121 + 1].restTime }}s</text>
170 </view> 122 </view>
171 </template> 123 </template>
172 </view> 124 </view>
@@ -174,322 +126,306 @@ @@ -174,322 +126,306 @@
174 </view> 126 </view>
175 </view> 127 </view>
176 </view> 128 </view>
177 -  
178 -  
179 - </view>  
180 </view> 129 </view>
  130 + </template>
  131 +
181 <view v-else class="empty-tip"> 132 <view v-else class="empty-tip">
182 <text>暂无动作数据</text> 133 <text>暂无动作数据</text>
183 </view> 134 </view>
184 </view> 135 </view>
185 - <!-- 底部按钮 --> 136 +
  137 + <!-- ==================== 底部操作栏 ==================== -->
186 <view class="bottom-actions"> 138 <view class="bottom-actions">
  139 + <!-- 我的计划模式 -->
187 <template v-if="isMyPlan"> 140 <template v-if="isMyPlan">
188 - <button class="start-btn" :class="{ 'locked': !isUnlocked }" :disabled="!isUnlocked" 141 + <button class="start-btn" :class="{ locked: !isUnlocked }" :disabled="!isUnlocked"
189 @click="startDaiTemplateTraining"> 142 @click="startDaiTemplateTraining">
190 {{ isUnlocked ? '立即开始训练' : '训练在当日解锁' }} 143 {{ isUnlocked ? '立即开始训练' : '训练在当日解锁' }}
191 </button> 144 </button>
192 - <view class="move-to" @click="openMovePopup">  
193 - 移动到  
194 - </view>  
195 - <view class="Icon" @click="showMorePopup = true">  
196 - <uni-icons type="more-filled" size="22" color="#fff" />  
197 - <view class="icon-name">更多</view> 145 + <view class="move-to" @click="openMovePopup">移动到</view>
  146 + <view class="icon-group" @click="showMorePopup = true">
  147 + <up-icon name="more-dot-fill" size="22" color="#fff" />
  148 + <text class="icon-name">更多</text>
198 </view> 149 </view>
199 - <!-- 更多弹窗(原生 view 实现,精准定位) -->  
200 - <view class="more-overlay" v-if="showMorePopup" @click="showMorePopup = false">  
201 - <!-- 弹窗本体:精准定位在「更多」按钮上方 --> 150 +
  151 + <!-- 更多弹窗 -->
  152 + <view v-if="showMorePopup" class="more-overlay" @click="showMorePopup = false">
202 <view class="more-popup" @click.stop> 153 <view class="more-popup" @click.stop>
203 - <view class="more-item" @click="handleDeleteTemplate(TemplateDetail.dailyTemplateId)">  
204 - <!-- 需要每日模板的id -->  
205 - <uni-icons type="refresh" size="22" color="#fff"></uni-icons> 154 + <view class="more-item" @click="handleDeleteTemplate(templateDetail.dailyTemplateId)">
  155 + <up-icon name="reload" size="18" color="#fff" />
206 <text class="item-text">删除此次排课</text> 156 <text class="item-text">删除此次排课</text>
207 </view> 157 </view>
208 - <view class="more-item" @click.stop="confirmDelete(TemplateDetail.planId)">  
209 - <uni-icons type="close" size="22" color="#fff"></uni-icons> 158 + <view class="more-item" @click.stop="confirmDelete(templateDetail.planId)">
  159 + <up-icon name="close" size="18" color="#fff" />
210 <text class="item-text">结束整个计划</text> 160 <text class="item-text">结束整个计划</text>
211 </view> 161 </view>
212 </view> 162 </view>
213 </view> 163 </view>
214 </template> 164 </template>
  165 +
  166 + <!-- 普通模式 -->
215 <template v-else> 167 <template v-else>
216 - <button class="start-btn" @click="startTraining">  
217 - 开始训练  
218 - </button> 168 + <button class="start-btn" @click="startTraining">开始训练</button>
219 <view class="action-group" @click="openColorPopup"> 169 <view class="action-group" @click="openColorPopup">
220 - <view class="color-circle" :style="{ backgroundColor: TemplateDetail?.backgroundcolor || '#ffd60a' }"></view> 170 + <view class="color-circle" :style="{ backgroundColor: templateDetail?.backgroundcolor || '#ffd60a' }" />
221 <text class="action-text">日历颜色</text> 171 <text class="action-text">日历颜色</text>
222 </view> 172 </view>
223 - <view class="action-group" @click="openCalendarPopup(TemplateDetail.id)">  
224 - <uni-icons type="calendar" size="24" color="#aaa"></uni-icons> 173 + <view class="action-group" @click="openCalendarPopup">
  174 + <up-icon name="calendar" size="22" color="#aaa" />
225 <text class="action-text">加入日程</text> 175 <text class="action-text">加入日程</text>
226 </view> 176 </view>
227 -  
228 </template> 177 </template>
229 </view> 178 </view>
230 179
231 - <!-- 设置日历颜色 -->  
232 - <CalendarColorPicker v-model:visible="showColorPopup" :daily-template-id="TemplateDetail?.id"  
233 - @success="handleCalendarSuccess" :is-template="true" :current-color="TemplateDetail?.backgroundcolor" /> 180 + <!-- ==================== 弹窗 / 子组件 ==================== -->
  181 + <CalendarColorPicker v-model:visible="showColorPopup" :daily-template-id="templateDetail?.id" :is-template="true"
  182 + :current-color="templateDetail?.backgroundcolor" @success="handleCalendarSuccess" />
234 183
235 - <!-- 添加到/移动到日历弹窗子组件 -->  
236 - <AddToCalendarPopup ref="calendarPopupRef" :template-id="CalendarId" @success="handleCalendarSuccess"  
237 - :is-move="isMove" /> 184 + <AddToCalendarPopup ref="calendarPopupRef" :template-id="calendarId" :is-move="isMove"
  185 + @success="handleCalendarSuccess" />
238 186
239 - <!-- 详情弹窗 -->  
240 <dongzuoXianqing ref="actionDetailRef" /> 187 <dongzuoXianqing ref="actionDetailRef" />
241 -  
242 </view> 188 </view>
243 </template> 189 </template>
  190 +
244 <script setup> 191 <script setup>
245 -import { onMounted, ref, computed, nextTick } from 'vue'; 192 +// ==================== Vue / Uni 导入 ====================
  193 +import { ref, computed, nextTick } from 'vue';
246 import { onLoad } from '@dcloudio/uni-app'; 194 import { onLoad } from '@dcloudio/uni-app';
  195 +
  196 +// ==================== 工具库 ====================
  197 +import dayjs from 'dayjs';
  198 +
  199 +// ==================== API 导入 ====================
247 import TemplatesApi from '@/sheep/api/Template/Templates'; 200 import TemplatesApi from '@/sheep/api/Template/Templates';
248 -import AddToCalendarPopup from '@/pages/xunji/components/tianjia-dao-rili.vue'  
249 -import CalendarColorPicker from '@/pages/xunji/components/rili-components/CalendarColorPicker.vue'  
250 -import { useTrainingStore } from '@/sheep/store/trainingStore'  
251 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; 201 import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
252 import QueryPlanApi from '@/sheep/api/plan/queryplan'; 202 import QueryPlanApi from '@/sheep/api/plan/queryplan';
253 203
  204 +// ==================== Store 导入 ====================
  205 +import { useTrainingStore } from '@/sheep/store/trainingStore';
  206 +
  207 +// ==================== 组件导入 ====================
  208 +import AddToCalendarPopup from '@/pages/xunji/components/tianjia-dao-rili.vue';
  209 +import CalendarColorPicker from '@/pages/xunji/components/rili-components/CalendarColorPicker.vue';
254 import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue'; 210 import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue';
255 211
256 -const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" 212 +// ==================== 常量 ====================
  213 +const DELETE_DELAY = 800;
  214 +const NAVIGATE_DELAY = 1500;
  215 +const MUSCLE_IMG = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png';
  216 +const SUPER_GROUP_IMG = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png';
  217 +const LOST_IMAGE = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
  218 +
  219 +// 动作类型 → 组次文本格式化
  220 +const SET_TEXT_FORMATTERS = {
  221 + 0: (d) => `${d.weight}kg × ${d.reps}次`,
  222 + 1: (d) => `${d.distance}km × ${formatSeconds(d.duration)}`,
  223 + 2: (d) => `${d.reps}次`,
  224 + 3: (d) => formatSeconds(d.duration),
  225 + 4: (d) => `${d.weight}kg × ${d.reps}次`,
  226 + 5: (d) => `${d.weight}kg × ${d.reps}次`,
  227 + 6: (d) => `${d.reps}组 × ${d.duration}秒 × ${d.restTime}秒/组休息`,
  228 +};
  229 +
  230 +// ==================== Store ====================
  231 +const trainingStore = useTrainingStore();
257 232
  233 +// ==================== 组件引用 ====================
258 const actionDetailRef = ref(null); 234 const actionDetailRef = ref(null);
  235 +const calendarPopupRef = ref(null);
259 236
260 -const showMorePopup = ref(false)  
261 -// 响应式变量  
262 -const isStarting = ref(false);  
263 -const temid = ref('');  
264 -const isOffice = ref(false); // 用来存是否官方模板 237 +// ==================== 响应式状态 ====================
  238 +const showMorePopup = ref(false);
  239 +const showColorPopup = ref(false);
  240 +const isOffice = ref(false);
  241 +const isMyPlan = ref(false);
  242 +const isDailytemplateId = ref(false);
  243 +const isMove = ref(false);
  244 +const date = ref('');
265 245
266 -const TemplateDetail = ref({});  
267 -const TemplateUnits = ref([]);  
268 -const calendarPopupRef = ref(null); 246 +const templateId = ref('');
  247 +const calendarId = ref(0);
  248 +const templateDetail = ref({});
  249 +const templateUnits = ref([]);
269 250
270 -const showColorPopup = ref(false) 251 +// ==================== Computed ====================
271 252
272 -const trainingStore = useTrainingStore() 253 +/** 器械显示文本 */
  254 +const equipmentDisplayText = computed(() => {
  255 + if (isOffice.value) {
  256 + return templateDetail.value?.equipmentsUsed || '';
  257 + }
  258 + return (templateDetail.value?.equipmentNames || []).join('、');
  259 +});
  260 +
  261 +/** 训练是否已解锁(当日或之后) */
  262 +const isUnlocked = computed(() => {
  263 + if (!templateDetail.value?.trainingDate) return false;
  264 + const [year, month, day] = templateDetail.value.trainingDate;
  265 + const unlockDate = dayjs(`${year}-${month}-${day}`).startOf('day');
  266 + return dayjs().startOf('day').isSameOrAfter(unlockDate);
  267 +});
273 268
274 -// 日历  
275 -const isMove = ref(false)  
276 -const CalendarId = ref(0) 269 +// ==================== 工具函数 ====================
277 270
  271 +/** 秒数 → HH:mm:ss */
  272 +function formatSeconds(seconds) {
  273 + if (!seconds || isNaN(seconds)) return '00:00:00';
  274 + return dayjs.duration(seconds, 'seconds').format('HH:mm:ss');
  275 +}
278 276
279 -// 计算 单个 unit 里面所有动作的总组数  
280 -const getUnitSetCount = (unit) => {  
281 - if (!unit || !unit.exercises?.length) return 0; 277 +/** 根据动作类型格式化组次文本 */
  278 +function formatSetText(exerciseType, detail) {
  279 + const fn = SET_TEXT_FORMATTERS[exerciseType];
  280 + return fn ? fn(detail) : '';
  281 +}
282 282
283 - let count = 0;  
284 - unit.exercises.forEach(item => {  
285 - if (item.sets?.length) {  
286 - count += item.sets.length; 283 +/** 获取 unit 封面图 */
  284 +function getUnitCover(unit) {
  285 + if (unit.unitType === 1) {
  286 + return unit.exercises?.[0]?.exerciseCover || LOST_IMAGE;
287 } 287 }
  288 + return SUPER_GROUP_IMG;
  289 +}
  290 +
  291 +/** 单个 unit 内所有动作的总组数 */
  292 +function getUnitSetCount(unit) {
  293 + if (!unit?.exercises?.length) return 0;
  294 + let count = 0;
  295 + unit.exercises.forEach((item) => {
  296 + if (item.sets?.length) count += item.sets.length;
288 }); 297 });
289 return count; 298 return count;
290 -};  
291 -  
292 -const formatSeconds = (seconds) => {  
293 - if (!seconds || isNaN(seconds)) return '00:00:00'  
294 -  
295 - const h = Math.floor(seconds / 3600)  
296 - const m = Math.floor((seconds % 3600) / 60)  
297 - const s = Math.floor(seconds % 60)  
298 -  
299 - // 自动补 0,保证两位数  
300 - const hh = String(h).padStart(2, '0')  
301 - const mm = String(m).padStart(2, '0')  
302 - const ss = String(s).padStart(2, '0') 299 +}
303 300
304 - return `${hh}:${mm}:${ss}` 301 +/** 超级组内最大组数 */
  302 +function getSuperSetMaxGroupCount(exercises) {
  303 + if (!Array.isArray(exercises) || exercises.length === 0) return 0;
  304 + return exercises.reduce((max, ex) => {
  305 + const len = Array.isArray(ex.sets) ? ex.sets.length : 0;
  306 + return Math.max(max, len);
  307 + }, 0);
305 } 308 }
306 -//  
307 -const loadtemplatedetail = async () => { 309 +
  310 +// ==================== 数据加载 ====================
  311 +async function loadTemplateDetail() {
308 try { 312 try {
309 let res; 313 let res;
310 - // 官方模板  
311 if (isOffice.value) { 314 if (isOffice.value) {
312 - res = await TemplatesApi.getTemplateDetail(temid.value);  
313 - console.log('官方模板详情:', res);  
314 - }  
315 - // 每日模板  
316 - else if (isDailytemplateId.value) {  
317 - res = await QueryPlanApi.getDailyTemplateDetail(temid.value);  
318 - console.log('每日模板详情:', res);  
319 - // 个人模板 315 + res = await TemplatesApi.getTemplateDetail(templateId.value);
  316 + } else if (isDailytemplateId.value) {
  317 + res = await QueryPlanApi.getDailyTemplateDetail(templateId.value);
320 } else { 318 } else {
321 - res = await TemplatesApi.getCustTemplateDetail(temid.value);  
322 - console.log('个人模板详情:', res); 319 + res = await TemplatesApi.getCustTemplateDetail(templateId.value);
323 } 320 }
324 - TemplateDetail.value = res.data;  
325 - TemplateUnits.value = res.data.units || [];  
326 - console.log('✅ TemplateDetail.value模板详情:', TemplateDetail.value);  
327 - console.log('✅ 模板unts详情:', TemplateUnits.value);  
328 - 321 + templateDetail.value = res.data;
  322 + templateUnits.value = res.data.units || [];
329 } catch (err) { 323 } catch (err) {
330 - console.log('❌ 请求失败:', err); 324 + console.error('❌ 模板详情加载失败:', err);
331 } 325 }
332 -};  
333 -  
334 -// 返回  
335 -const goBack = () => {  
336 - uni.navigateBack();  
337 -}; 326 +}
338 327
339 -// 设置日历颜色  
340 -const openColorPopup = () => {  
341 - if (!TemplateDetail.value.id) {  
342 - uni.showToast({ title: '未找到模板ID', icon: 'none' })  
343 - return 328 +// ==================== 日历颜色 ====================
  329 +function openColorPopup() {
  330 + if (!templateDetail.value.id) {
  331 + uni.showToast({ title: '未找到模板ID', icon: 'none' });
  332 + return;
344 } 333 }
345 - showColorPopup.value = true 334 + showColorPopup.value = true;
346 } 335 }
347 336
348 -// 获取超级组内所有动作中最大的组数  
349 -const getSuperSetMaxGroupCount = (exercises) => {  
350 - if (!Array.isArray(exercises) || exercises.length === 0) return 0  
351 - return exercises.reduce((max, ex) => {  
352 - const len = Array.isArray(ex.sets) ? ex.sets.length : 0  
353 - return Math.max(max, len)  
354 - }, 0) 337 +// ==================== 添加到/移动到日历 ====================
  338 +function handleCalendarSuccess() {
  339 + loadTemplateDetail();
355 } 340 }
356 341
  342 +function openCalendarPopup() {
  343 + calendarId.value = templateDetail.value.id;
  344 + calendarPopupRef.value?.open();
  345 +}
357 346
358 -//  
359 -const handleUnitClick = (unit) => {  
360 - // 取当前unit的id(动作组/超级组都用这个)  
361 - console.log('unit', unit);  
362 - if (unit.unitType === 1) {  
363 - const id = unit.exercises[0].exerciseId  
364 - nextTick(() => {  
365 - actionDetailRef.value.open(id, 1);  
366 - }); 347 +function openMovePopup() {
  348 + calendarId.value = templateDetail.value.dailyTemplateId;
  349 + isMove.value = true;
  350 + calendarPopupRef.value?.open();
  351 +}
367 352
  353 +// ==================== 动作详情弹窗 ====================
  354 +function handleUnitClick(unit) {
  355 + if (unit.unitType === 1) {
  356 + const id = unit.exercises[0].exerciseId;
  357 + nextTick(() => actionDetailRef.value?.open(id, 1));
368 } else if (unit.unitType === 2) { 358 } else if (unit.unitType === 2) {
369 const id = unit.supersetId; 359 const id = unit.supersetId;
370 - nextTick(() => {  
371 - actionDetailRef.value.open(id, 2);  
372 - }); 360 + nextTick(() => actionDetailRef.value?.open(id, 2));
373 } 361 }
374 -};  
375 -  
376 -// 开始训练  
377 -const startTraining = () => { 362 +}
378 363
  364 +// ==================== 开始训练 ====================
  365 +function startTraining() {
379 if (trainingStore.isTraining) { 366 if (trainingStore.isTraining) {
380 - uni.showToast({  
381 - title: '当前已有正在进行的训练',  
382 - icon: 'none'  
383 - }); 367 + uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' });
384 return; 368 return;
385 } 369 }
386 -  
387 trainingStore.isSystem = 1; 370 trainingStore.isSystem = 1;
388 trainingStore.isTraining = true; 371 trainingStore.isTraining = true;
389 uni.navigateTo({ 372 uni.navigateTo({
390 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.value.id}&type=3&isTraining=true`, 373 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&isTraining=true`,
391 }); 374 });
392 - console.log('打印传递给动作训练页面的模板id', TemplateDetail.id);  
393 -  
394 -};  
395 -// startDaiTemplateTraining  
396 -const startDaiTemplateTraining = () => { 375 +}
397 376
  377 +function startDaiTemplateTraining() {
398 if (trainingStore.isTraining) { 378 if (trainingStore.isTraining) {
399 - uni.showToast({  
400 - title: '当前已有正在进行的训练',  
401 - icon: 'none'  
402 - }); 379 + uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' });
403 return; 380 return;
404 } 381 }
405 -  
406 - trainingStore.isSystem = TemplateDetail.value.isSystem;  
407 - trainingStore.loadDailyTemplateForEdit(TemplateDetail.value);  
408 - trainingStore.initDailyTemplateRecords() 382 + trainingStore.isSystem = templateDetail.value.isSystem;
  383 + trainingStore.loadDailyTemplateForEdit(templateDetail.value);
  384 + trainingStore.initDailyTemplateRecords();
409 uni.navigateTo({ 385 uni.navigateTo({
410 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.value.id}&type=3&dailyTemplateId=${TemplateDetail.value.dailyTemplateId}&isTraining=true`, 386 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&dailyTemplateId=${templateDetail.value.dailyTemplateId}&isTraining=true`,
411 }); 387 });
412 - console.log('开始进入每日moan训练页面,模板ID:', TemplateDetail.value.id, '每日模板ID:', TemplateDetail.value.dailyTemplateId);  
413 -  
414 - console.log('打印传递给动作训练页面的模板id', TemplateDetail.value.id);  
415 -  
416 -};  
417 -  
418 -  
419 -const handleCalendarSuccess = () => {  
420 - // console.log('课程已成功添加到日历');  
421 - // 这里可以加:刷新课程状态、更新按钮文字等逻辑  
422 - loadtemplatedetail();  
423 -};  
424 -  
425 -const openCalendarPopup = (planId) => {  
426 - console.log('点击了添加到日历按钮', planId); // 新增日志  
427 - console.log('子组件实例', calendarPopupRef.value); // 新增日志  
428 - CalendarId.value = TemplateDetail.value.id;  
429 - // 测试是否有open方法  
430 - if (calendarPopupRef.value && typeof calendarPopupRef.value.open === 'function') {  
431 - calendarPopupRef.value.open();  
432 - } else {  
433 - console.error('子组件没有open方法!', calendarPopupRef.value);  
434 - }  
435 -};  
436 -// 传递的是每日模板的id  
437 -const openMovePopup = () => {  
438 - console.log('点击了移动到按钮'); // 新增日志  
439 - console.log('子组件实例', calendarPopupRef.value); // 新增日志  
440 - CalendarId.value = TemplateDetail.value.dailyTemplateId;  
441 - console.log(); 388 +}
442 389
443 - isMove.value = true;  
444 - // 测试是否有open方法  
445 - if (calendarPopupRef.value && typeof calendarPopupRef.value.open === 'function') {  
446 - calendarPopupRef.value.open();  
447 - } else {  
448 - console.error('子组件没有open方法!', calendarPopupRef.value);  
449 - }  
450 -}; 390 +// ==================== 编辑模板 ====================
  391 +function templateEdit() {
  392 + trainingStore.isSystem = templateDetail.value.isSystem;
  393 + trainingStore.loadDailyTemplateForEdit(templateDetail.value);
  394 + trainingStore.initDailyTemplateRecords();
  395 + uni.navigateTo({
  396 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateDetail.value.id}&type=3&dailyTemplateId=${templateDetail.value.dailyTemplateId}`,
  397 + });
  398 +}
451 399
452 -// 我的模板板块处理  
453 -// 删除此次排课  
454 -const handleDeleteTemplate = async (dailyTemplateId) => {  
455 - // 先判断有没有ID 400 +// ==================== 删除操作 ====================
  401 +async function handleDeleteTemplate(dailyTemplateId) {
456 if (!dailyTemplateId) { 402 if (!dailyTemplateId) {
457 uni.showToast({ title: '未找到每日模板ID', icon: 'none' }); 403 uni.showToast({ title: '未找到每日模板ID', icon: 'none' });
458 return; 404 return;
459 } 405 }
460 - // 弹出确认框  
461 - uni.showModal({ 406 + const { confirm } = await uni.showModal({
462 title: '确认删除', 407 title: '确认删除',
463 content: '确定要删除这条训练记录吗?删除后无法恢复', 408 content: '确定要删除这条训练记录吗?删除后无法恢复',
464 confirmText: '删除', 409 confirmText: '删除',
465 confirmColor: '#ff4444', 410 confirmColor: '#ff4444',
466 cancelText: '取消', 411 cancelText: '取消',
467 - success: async (res) => {  
468 - if (!res.confirm) return; 412 + });
  413 + if (!confirm) return;
  414 +
469 try { 415 try {
470 await dailytemplateApi.deleteDailyTemplate(dailyTemplateId); 416 await dailytemplateApi.deleteDailyTemplate(dailyTemplateId);
471 uni.showToast({ title: '删除成功', icon: 'success' }); 417 uni.showToast({ title: '删除成功', icon: 'success' });
472 - // moreShow.value = false; // 关闭弹窗  
473 - // 返回上一页  
474 - setTimeout(() => uni.navigateBack(), 800) 418 + setTimeout(() => uni.navigateBack(), DELETE_DELAY);
475 } catch (err) { 419 } catch (err) {
476 console.error('删除失败', err); 420 console.error('删除失败', err);
477 uni.showToast({ title: '删除失败,请重试', icon: 'none' }); 421 uni.showToast({ title: '删除失败,请重试', icon: 'none' });
478 } 422 }
479 - }  
480 - });  
481 -};  
482 -// 删除整个个人计划  
483 -const confirmDelete = async (id) => {  
484 - try {  
485 - console.log('打印计划个人id:', id);  
486 -  
487 - const res = await QueryPlanApi.deletePlan(id);  
488 - console.log('删除结果:', res); 423 +}
489 424
  425 +async function confirmDelete(planId) {
  426 + try {
  427 + const res = await QueryPlanApi.deletePlan(planId);
490 if (res.code === 0 && res.data) { 428 if (res.code === 0 && res.data) {
491 - // 2. 删除成功,从列表移除  
492 - // planList.value = planList.value.filter(item => item.id !== id);  
493 uni.showToast({ title: '计划已结束', icon: 'success' }); 429 uni.showToast({ title: '计划已结束', icon: 'success' });
494 } else { 430 } else {
495 uni.showToast({ title: res.msg || '操作失败', icon: 'none' }); 431 uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
@@ -498,341 +434,230 @@ const confirmDelete = async (id) => { @@ -498,341 +434,230 @@ const confirmDelete = async (id) => {
498 console.error('删除计划失败:', err); 434 console.error('删除计划失败:', err);
499 uni.showToast({ title: '网络请求失败', icon: 'none' }); 435 uni.showToast({ title: '网络请求失败', icon: 'none' });
500 } finally { 436 } finally {
501 - {  
502 setTimeout(() => { 437 setTimeout(() => {
503 - // 获取页面栈  
504 const pages = getCurrentPages(); 438 const pages = getCurrentPages();
505 - // 这里返回上上个(我的计划列表页面)  
506 const backNum = pages.length - 2; 439 const backNum = pages.length - 2;
507 if (backNum > 0) { 440 if (backNum > 0) {
508 - // 回退对应层数  
509 - uni.navigateBack({  
510 - delta: backNum  
511 - }); 441 + uni.navigateBack({ delta: backNum });
512 } else { 442 } else {
513 - // 兜底:页面栈不足时防止报错,直接返回我的计划列表页面  
514 - uni.navigateTo({  
515 - url: '/pages4/pages/xunji/xunji-wode-jihua'  
516 - })  
517 - }  
518 - }, 1500) 443 + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-wode-jihua' });
519 } 444 }
  445 + }, NAVIGATE_DELAY);
520 } 446 }
521 -};  
522 -  
523 -const isUnlocked = computed(() => {  
524 - if (!TemplateDetail.value?.trainingDate) return false  
525 -  
526 - const [year, month, day] = TemplateDetail.value.trainingDate  
527 -  
528 - const unlockDate = new Date(year, month - 1, day)  
529 -  
530 - const today = new Date()  
531 - today.setHours(0, 0, 0, 0)  
532 - unlockDate.setHours(0, 0, 0, 0)  
533 -  
534 - return today >= unlockDate  
535 -})  
536 -  
537 -// 点击超级组内部的动作 → 打开动作详情(复用同一个组件)  
538 -const openActionItem = (item) => {  
539 - open(item.id, 1);  
540 -};  
541 -// 修改每日模板训练内容  
542 -const templateEdit = (TemplateDetail) => {  
543 -  
544 - console.log('开始进入编辑模板页面,模板ID:', TemplateDetail.id, '每日模板ID:', TemplateDetail.dailyTemplateId);  
545 -  
546 - trainingStore.isSystem = TemplateDetail.isSystem;  
547 - trainingStore.loadDailyTemplateForEdit(TemplateDetail);  
548 - trainingStore.initDailyTemplateRecords()  
549 -  
550 - uni.navigateTo({  
551 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.id}&type=3&dailyTemplateId=${TemplateDetail.dailyTemplateId}`,  
552 - });  
553 -  
554 -}; 447 +}
555 448
556 -const isMyPlan = ref(false)  
557 -const isDailytemplateId = ref(false)  
558 -const date = ref('')  
559 -// 生命周期 449 +// ==================== 生命周期 ====================
560 onLoad((options) => { 450 onLoad((options) => {
561 -  
562 - console.log('收到日期:', options.date) // 2025-04-20  
563 - date.value = options.date;  
564 - temid.value = options.id; 451 + templateId.value = options.id;
565 isOffice.value = options.isOffice === 'true'; 452 isOffice.value = options.isOffice === 'true';
566 isMyPlan.value = options.isMyPlan === 'true'; 453 isMyPlan.value = options.isMyPlan === 'true';
567 - isDailytemplateId.value = options.isDailytemplateId === 'true'  
568 -  
569 - console.log("✅ onLoad 收到的 options:", options); // 先看整个 options 里有什么  
570 - console.log("模板ID:", temid.value);  
571 - console.log("是否官方:", isOffice);  
572 - console.log("是否是我的模板:", isDailytemplateId.value);  
573 -  
574 -  
575 - loadtemplatedetail(); 454 + isDailytemplateId.value = options.isDailytemplateId === 'true';
  455 + date.value = options.date || '';
576 456
  457 + loadTemplateDetail();
577 }); 458 });
578 -  
579 </script> 459 </script>
580 460
581 <style scoped lang="scss"> 461 <style scoped lang="scss">
  462 +// ==================== CSS 变量 ====================
  463 +$bg-primary: #121212;
  464 +$bg-secondary: #1e1e1e;
  465 +$bg-tertiary: #333;
  466 +$bg-card: #4e4e4e;
  467 +$accent: #ffd100;
  468 +$text-primary: #fff;
  469 +$text-secondary: #ccc;
  470 +$text-tertiary: #999;
  471 +$text-muted: #aaa;
  472 +$text-dim: #666;
  473 +$border-color: #333;
  474 +$border-light: #444;
  475 +
  476 +$font-xs: 20rpx;
  477 +$font-sm: 24rpx;
  478 +$font-base: 28rpx;
  479 +$font-md: 30rpx;
  480 +$font-lg: 32rpx;
  481 +
  482 +$radius-sm: 10rpx;
  483 +$radius-base: 16rpx;
  484 +$radius-round: 40rpx;
  485 +$radius-circle: 50%;
  486 +
  487 +// ==================== 导航栏 ====================
  488 +.navbar-title {
  489 + font-size: $font-lg;
  490 + font-weight: bold;
  491 + color: $text-primary;
  492 +}
  493 +
  494 +// ==================== 页面容器 ====================
582 .train-template-page { 495 .train-template-page {
583 - background-color: #121212;  
584 - color: #ffffff; 496 + background-color: $bg-primary;
  497 + color: $text-primary;
585 min-height: 100vh; 498 min-height: 100vh;
586 font-size: 14px; 499 font-size: 14px;
587 line-height: 1.6; 500 line-height: 1.6;
588 padding-bottom: 160rpx; 501 padding-bottom: 160rpx;
589 } 502 }
590 503
591 -.header {  
592 - display: flex;  
593 - align-items: center;  
594 - padding: calc(var(--status-bar-height, 0px) + 20rpx) 30rpx 20rpx;  
595 - background-color: #000;  
596 - position: relative;  
597 - z-index: 10;  
598 -}  
599 -  
600 -.back-icon {  
601 - width: 40rpx;  
602 - height: 40rpx;  
603 - margin-right: 20rpx;  
604 -}  
605 -  
606 -.title {  
607 - font-size: 32rpx;  
608 - font-weight: bold;  
609 - flex: 0.9;  
610 - text-align: center;  
611 -}  
612 - 504 +// ==================== 统计区域 ====================
613 .stats { 505 .stats {
614 padding: 20rpx 30rpx; 506 padding: 20rpx 30rpx;
615 - border-bottom: 1px solid #333;  
616 -} 507 + border-bottom: 1px solid $border-color;
617 508
618 -.stat-item {  
619 - margin-bottom: 10rpx;  
620 - font-size: 28rpx; 509 + .stat-item {
621 display: flex; 510 display: flex;
622 align-items: center; 511 align-items: center;
623 -} 512 + gap: 10rpx;
  513 + margin-bottom: 10rpx;
624 514
625 -.icon {  
626 - margin-right: 10rpx; 515 + .stat-text {
  516 + font-size: $font-base;
  517 + }
  518 + }
627 } 519 }
628 520
  521 +// ==================== 肌肉图 ====================
629 .muscle-map { 522 .muscle-map {
630 - display: flex;  
631 - justify-content: space-between;  
632 - padding: 20rpx 0;  
633 width: 100%; 523 width: 100%;
634 -} 524 + padding: 20rpx 0;
635 525
636 -.muscle-img { 526 + .muscle-img {
637 width: 100%; 527 width: 100%;
638 object-fit: contain; 528 object-fit: contain;
639 - /* 关键:完整显示不裁切 */  
640 - border-radius: 10rpx;  
641 -}  
642 -  
643 -.right {  
644 - transform: rotateY(180deg); 529 + border-radius: $radius-sm;
  530 + }
645 } 531 }
646 532
647 -/* 新增:主要/次要训练部位容器样式,强制左对齐 */ 533 +// ==================== 训练部位 ====================
648 .primary-muscle { 534 .primary-muscle {
649 padding: 30rpx; 535 padding: 30rpx;
650 - // margin-top: 20rpx;  
651 - border-bottom: 1px solid #333; 536 + border-bottom: 1px solid $border-color;
652 display: flex; 537 display: flex;
653 flex-direction: column; 538 flex-direction: column;
654 - align-items: flex-start;  
655 - /* 关键:所有内容左对齐 */  
656 gap: 15rpx; 539 gap: 15rpx;
657 - /* 两行之间的间距,可按需调整 */ 540 +
  541 + .muscle-row {
  542 + display: flex;
  543 + flex-wrap: wrap;
  544 + }
  545 +
  546 + .divider {
  547 + width: 100%;
  548 + height: 1rpx;
  549 + background-color: $border-color;
  550 + margin: 5rpx 0;
  551 + }
658 } 552 }
659 553
  554 +// ==================== 通用区块 ====================
660 .section { 555 .section {
661 padding: 30rpx; 556 padding: 30rpx;
662 - // margin-top: 20rpx;  
663 - border-bottom: 1px solid #333;  
664 -}  
665 557
666 -.section-top { 558 + box-sizing: border-box;
  559 +
  560 + .section-top {
667 display: flex; 561 display: flex;
668 justify-content: space-between; 562 justify-content: space-between;
669 align-items: center; 563 align-items: center;
670 margin-bottom: 20rpx; 564 margin-bottom: 20rpx;
671 -}  
672 -  
673 -.section-title {  
674 - font-size: 32rpx;  
675 - font-weight: bold;  
676 - // margin-bottom: 20rpx;  
677 -} 565 + }
678 566
679 -.right-btn {  
680 - font-size: 24rpx;  
681 - width: 140rpx; 567 + .right-btn {
  568 + font-size: $font-sm;
682 /* #ifdef H5 */ 569 /* #ifdef H5 */
683 width: 99px; 570 width: 99px;
684 /* #endif */ 571 /* #endif */
  572 + /* #ifndef H5 */
  573 + width: 140rpx;
  574 + /* #endif */
685 height: 37rpx; 575 height: 37rpx;
686 padding: 10rpx 20rpx; 576 padding: 10rpx 20rpx;
687 background: rgba(90, 90, 90, 0.45); 577 background: rgba(90, 90, 90, 0.45);
688 - justify-content: center;  
689 display: flex; 578 display: flex;
690 align-items: center; 579 align-items: center;
  580 + justify-content: center;
691 border-radius: 20rpx; 581 border-radius: 20rpx;
692 -}  
693 -  
694 -.description {  
695 - font-size: 28rpx;  
696 - margin-bottom: 10rpx;  
697 -} 582 + }
698 583
699 -.empty-tip { 584 + .empty-tip {
700 text-align: center; 585 text-align: center;
701 padding: 40rpx 0; 586 padding: 40rpx 0;
702 - color: #999;  
703 - font-size: 28rpx; 587 + color: $text-tertiary;
  588 + font-size: $font-base;
  589 + }
704 } 590 }
705 591
706 .section-title { 592 .section-title {
707 - font-size: 32rpx; 593 + font-size: $font-lg;
708 font-weight: bold; 594 font-weight: bold;
709 - // margin-bottom: 20rpx;  
710 } 595 }
711 596
712 .description { 597 .description {
713 - font-size: 28rpx; 598 + font-size: $font-base;
714 margin-bottom: 10rpx; 599 margin-bottom: 10rpx;
715 } 600 }
716 601
717 -.empty-tip {  
718 - text-align: center;  
719 - padding: 40rpx 0;  
720 - color: #999;  
721 - font-size: 28rpx;  
722 -}  
723 -  
724 -/* --- 修复核心:unit 卡片布局 --- */ 602 +// ==================== 动作卡片 ====================
725 .exercise-item { 603 .exercise-item {
726 - background-color: #1e1e1e;  
727 - border-radius: 16rpx; 604 + background-color: $bg-secondary;
  605 + border-radius: $radius-base;
728 padding: 20rpx; 606 padding: 20rpx;
729 margin-bottom: 20rpx; 607 margin-bottom: 20rpx;
730 - border: 1px solid #333; 608 + border: 1px solid $border-color;
731 } 609 }
732 610
733 -/* 超级组内动作项容器:给分隔线留空间 */  
734 -.exercise-item-inner {  
735 - padding: 10rpx 0;  
736 -}  
737 -  
738 -/* 超级组动作名称样式:匹配你现有字体/颜色风格 */  
739 -.super-exercise-name {  
740 - font-size: 30rpx;  
741 - font-weight: 500;  
742 - color: #fff;  
743 - line-height: 80rpx;  
744 - /* 和图片高度对齐,保持视觉一致 */  
745 -}  
746 -  
747 -/* 超级组动作分隔线:和你现有分割线风格统一 */  
748 -.divider-line {  
749 - width: 100%;  
750 - height: 1rpx;  
751 - background-color: #333;  
752 - margin: 15rpx 0;  
753 -}  
754 -  
755 -/* 核心修复:让单元标题和图标在同一行 */  
756 .unit-header { 611 .unit-header {
757 display: flex; 612 display: flex;
758 - justify-content: space-between; // 文字靠左,图片靠右  
759 - align-items: center; // 垂直居中 613 + justify-content: space-between;
  614 + align-items: center;
760 margin-bottom: 20rpx; 615 margin-bottom: 20rpx;
761 padding-bottom: 10rpx; 616 padding-bottom: 10rpx;
762 - border-bottom: 1px solid #333; 617 + border-bottom: 1px solid $border-color;
763 gap: 20rpx; 618 gap: 20rpx;
764 - // margin: 40rpx; 619 +
  620 + .left {
  621 + display: flex;
  622 + gap: 12rpx;
  623 + }
  624 +
  625 + .right {
  626 + transform: rotateY(180deg);
  627 + }
765 628
766 .unit-title { 629 .unit-title {
767 - color: #fff; 630 + color: $text-primary;
768 margin-left: 10rpx; 631 margin-left: 10rpx;
769 632
770 -  
771 - .PartTool {  
772 - font-size: 20rpx; 633 + .name {
  634 + font-size: $font-md;
  635 + font-weight: bold;
773 } 636 }
774 637
  638 + .PartTool,
775 .totalSetCount { 639 .totalSetCount {
776 - font-size: 20rpx; 640 + font-size: $font-xs;
777 } 641 }
778 -  
779 } 642 }
780 } 643 }
781 644
782 -.left {  
783 - display: flex;  
784 - gap: 12rpx;  
785 -}  
786 -  
787 -.unit-title {  
788 - font-size: 30rpx;  
789 - font-weight: bold;  
790 - color: #fff;  
791 - // 去掉绝对宽度,让它自然占据空间  
792 -}  
793 -  
794 -.unit-icon {  
795 - width: 150rpx; // 匹配你截图的橙色方块大小  
796 - height: 150rpx;  
797 - border-radius: 10rpx;  
798 - object-fit: cover;  
799 -}  
800 -  
801 -/* --- 动作头部样式 --- */  
802 -.exercise-header {  
803 - display: flex;  
804 - align-items: center;  
805 - margin-bottom: 20rpx;  
806 -}  
807 -  
808 .exercise-number { 645 .exercise-number {
809 - font-size: 28rpx; 646 + font-size: $font-base;
810 font-weight: bold; 647 font-weight: bold;
811 - color: #fff; 648 + color: $text-primary;
812 width: 30rpx; 649 width: 30rpx;
813 text-align: center; 650 text-align: center;
814 - margin-right: 20rpx;  
815 - left: 5rpx;  
816 } 651 }
817 652
818 -.exercise-icon {  
819 - width: 80rpx;  
820 - height: 80rpx;  
821 - margin-right: 20rpx;  
822 - border-radius: 10rpx; 653 +.unit-icon {
  654 + width: 150rpx;
  655 + height: 150rpx;
  656 + border-radius: $radius-sm;
823 object-fit: cover; 657 object-fit: cover;
824 } 658 }
825 659
826 -.exercise-info {  
827 - flex: 1; // 关键:让信息区域占据剩余空间,防止挤压  
828 -}  
829 -  
830 -/* 你注释掉的 exercise-name/exercise-desc 不需要样式 */  
831 -  
832 -.exercise-details {  
833 - margin-bottom: 20rpx;  
834 -}  
835 - 660 +// ==================== 组次详情行 ====================
836 .detail-row { 661 .detail-row {
837 display: flex; 662 display: flex;
838 align-items: center; 663 align-items: center;
@@ -841,15 +666,15 @@ onLoad((options) => { @@ -841,15 +666,15 @@ onLoad((options) => {
841 } 666 }
842 667
843 .detail-label { 668 .detail-label {
844 - background: #4e4e4e; 669 + background: $bg-card;
845 width: 30rpx; 670 width: 30rpx;
846 height: 30rpx; 671 height: 30rpx;
847 - border-radius: 50%; 672 + border-radius: $radius-circle;
848 display: flex; 673 display: flex;
849 align-items: center; 674 align-items: center;
850 justify-content: center; 675 justify-content: center;
851 - color: #ccc;  
852 - font-size: 24rpx; 676 + color: $text-secondary;
  677 + font-size: $font-sm;
853 flex-shrink: 0; 678 flex-shrink: 0;
854 } 679 }
855 680
@@ -858,36 +683,39 @@ onLoad((options) => { @@ -858,36 +683,39 @@ onLoad((options) => {
858 display: flex; 683 display: flex;
859 align-items: center; 684 align-items: center;
860 justify-content: space-between; 685 justify-content: space-between;
861 -  
862 - .rest {  
863 - font-size: 24rpx;  
864 - }  
865 } 686 }
866 687
  688 +.rest {
  689 + font-size: $font-sm;
  690 +}
867 691
868 -// 超级组列表数据  
869 -super-actionName .name-item { 692 +// ==================== 超级组 ====================
  693 +.super-actionName {
  694 + .name-item {
  695 + display: flex;
870 gap: 10rpx; 696 gap: 10rpx;
871 -} 697 + padding: 4rpx 0;
872 698
873 -.letter {  
874 - margin-right: 10rpx; 699 + .letter {
  700 + font-weight: bold;
  701 + }
  702 +
  703 + .action-name {
  704 + color: $text-secondary;
  705 + }
  706 + }
875 } 707 }
876 708
877 .super-set-item { 709 .super-set-item {
878 - display: flex; // 水平排列 710 + display: flex;
879 padding: 16rpx 0; 711 padding: 16rpx 0;
880 712
881 -  
882 .detail-label { 713 .detail-label {
883 margin-right: 10rpx; 714 margin-right: 10rpx;
884 margin-top: 4rpx; 715 margin-top: 4rpx;
885 } 716 }
886 } 717 }
887 718
888 -// .set-column {  
889 -// display: flex;  
890 -// }  
891 .set-column { 719 .set-column {
892 flex: 1; 720 flex: 1;
893 } 721 }
@@ -898,90 +726,80 @@ super-actionName .name-item { @@ -898,90 +726,80 @@ super-actionName .name-item {
898 gap: 10rpx; 726 gap: 10rpx;
899 } 727 }
900 728
  729 +.action-letter {
  730 + font-size: $font-sm;
  731 + color: $text-secondary;
  732 +}
  733 +
901 .set-content { 734 .set-content {
902 display: flex; 735 display: flex;
903 gap: 10rpx; 736 gap: 10rpx;
904 justify-content: space-between; 737 justify-content: space-between;
905 flex: 1; 738 flex: 1;
906 align-items: center; 739 align-items: center;
907 -  
908 - .rest {  
909 - font-size: 24rpx;  
910 - }  
911 -  
912 -}  
913 -  
914 -  
915 -  
916 -.detail-value {  
917 - flex: 1;  
918 - // font-size: 24rpx;  
919 } 740 }
920 741
921 -/* --- 底部按钮 --- */ 742 +// ==================== 底部操作栏 ====================
922 .bottom-actions { 743 .bottom-actions {
923 display: flex; 744 display: flex;
924 justify-content: space-between; 745 justify-content: space-between;
925 - /* 左右分散 */  
926 align-items: center; 746 align-items: center;
927 - /* 垂直居中 */  
928 padding: 30rpx 40rpx; 747 padding: 30rpx 40rpx;
929 - /* 左右内边距加大,让整体更宽松 */  
930 background-color: #000; 748 background-color: #000;
931 position: fixed; 749 position: fixed;
932 bottom: 0; 750 bottom: 0;
933 left: 0; 751 left: 0;
934 right: 0; 752 right: 0;
935 - border-top: 1px solid #333; 753 + border-top: 1px solid $border-color;
936 gap: 40rpx; 754 gap: 40rpx;
937 - /* 右侧两个操作项之间的间距(核心) */  
938 -}  
939 755
940 -.start-btn { 756 + .start-btn {
941 width: 300rpx; 757 width: 300rpx;
942 height: 80rpx; 758 height: 80rpx;
943 - background-color: #ffd700; 759 + background-color: $accent;
944 color: #000; 760 color: #000;
945 - font-size: 30rpx; 761 + font-size: $font-md;
946 font-weight: bold; 762 font-weight: bold;
947 - border-radius: 40rpx; 763 + border-radius: $radius-round;
948 border: none; 764 border: none;
949 outline: none; 765 outline: none;
950 margin-right: auto; 766 margin-right: auto;
951 767
952 - // 新增:未解锁状态样式(和例图一致)  
953 &.locked { 768 &.locked {
954 - background-color: #333; // 深灰色背景  
955 - color: #666; // 浅灰色文字  
956 - opacity: 0.7; // 整体透明度降低  
957 - pointer-events: none; // 即使没加 disabled,也禁止点击(双重保险) 769 + background-color: $bg-tertiary;
  770 + color: $text-dim;
  771 + opacity: 0.7;
  772 + pointer-events: none;
  773 + }
958 } 774 }
959 775
960 -}  
961 -  
962 -.move-to { 776 + .move-to {
963 display: flex; 777 display: flex;
964 justify-content: center; 778 justify-content: center;
965 align-items: center; 779 align-items: center;
966 width: 200rpx; 780 width: 200rpx;
967 height: 80rpx; 781 height: 80rpx;
968 - background-color: #ffffff; 782 + background-color: $text-primary;
969 color: #000; 783 color: #000;
970 - font-size: 30rpx; 784 + font-size: $font-md;
971 font-weight: bold; 785 font-weight: bold;
972 - border-radius: 40rpx;  
973 - border: none;  
974 - outline: none;  
975 -} 786 + border-radius: $radius-round;
  787 + }
976 788
  789 + .icon-group {
  790 + display: flex;
  791 + flex-direction: column;
  792 + align-items: center;
  793 + gap: 6rpx;
977 794
978 -.start-btn.loading {  
979 - opacity: 0.7;  
980 - pointer-events: none; 795 + .icon-name {
  796 + font-size: $font-sm;
  797 + color: $text-muted;
  798 + }
  799 + }
981 } 800 }
982 801
983 -  
984 -// 更多弹窗遮罩 802 +// ==================== 更多弹窗 ====================
985 .more-overlay { 803 .more-overlay {
986 position: fixed; 804 position: fixed;
987 top: 0; 805 top: 0;
@@ -991,16 +809,13 @@ super-actionName .name-item { @@ -991,16 +809,13 @@ super-actionName .name-item {
991 z-index: 999; 809 z-index: 999;
992 } 810 }
993 811
994 -// 更多弹窗本体:精准定位在右下角「更多」按钮上方  
995 .more-popup { 812 .more-popup {
996 position: fixed; 813 position: fixed;
997 right: 30rpx; 814 right: 30rpx;
998 - /* 距离右侧距离 */  
999 bottom: 130rpx; 815 bottom: 130rpx;
1000 - /* 距离底部按钮高度(可微调) */  
1001 width: 300rpx; 816 width: 300rpx;
1002 - background-color: #333;  
1003 - border-radius: 16rpx; 817 + background-color: $bg-tertiary;
  818 + border-radius: $radius-base;
1004 overflow: hidden; 819 overflow: hidden;
1005 z-index: 1000; 820 z-index: 1000;
1006 821
@@ -1011,80 +826,35 @@ super-actionName .name-item { @@ -1011,80 +826,35 @@ super-actionName .name-item {
1011 gap: 16rpx; 826 gap: 16rpx;
1012 827
1013 .item-text { 828 .item-text {
1014 - font-size: 28rpx;  
1015 - color: #fff;  
1016 - } 829 + font-size: $font-base;
  830 + color: $text-primary;
1017 } 831 }
1018 832
1019 - // 分割线  
1020 - .more-item+.more-item {  
1021 - border-top: 1rpx solid #444; 833 + +.more-item {
  834 + border-top: 1rpx solid $border-light;
  835 + }
1022 } 836 }
1023 } 837 }
1024 838
  839 +// ==================== 操作按钮组(日历颜色/加入日程) ====================
1025 .action-group { 840 .action-group {
1026 display: flex; 841 display: flex;
1027 flex-direction: column; 842 flex-direction: column;
1028 align-items: center; 843 align-items: center;
1029 justify-content: center; 844 justify-content: center;
1030 line-height: 1; 845 line-height: 1;
1031 - /* 不需要额外 margin,靠父级 gap 控制间距 */  
1032 846
1033 .color-circle { 847 .color-circle {
1034 width: 40rpx; 848 width: 40rpx;
1035 height: 40rpx; 849 height: 40rpx;
1036 - background: rgb(211, 43, 169);  
1037 - border-radius: 50%; 850 + border-radius: $radius-circle;
1038 margin-bottom: 10rpx; 851 margin-bottom: 10rpx;
1039 - border: 2rpx solid #fff; 852 + border: 2rpx solid $text-primary;
1040 } 853 }
1041 854
1042 - .Icon {  
1043 - display: flex;  
1044 - flex-direction: column; 855 + .action-text {
  856 + font-size: $font-sm;
  857 + color: $text-muted;
1045 } 858 }
1046 -  
1047 - // 更多弹窗本体:精准定位在右下角「更多」按钮上方  
1048 - .more-popup {  
1049 - position: fixed;  
1050 - right: 30rpx;  
1051 - /* 距离右侧距离 */  
1052 - bottom: 130rpx;  
1053 - /* 距离底部按钮高度(可微调) */  
1054 - width: 300rpx;  
1055 - background-color: #333;  
1056 - border-radius: 16rpx;  
1057 - overflow: hidden;  
1058 - z-index: 1000;  
1059 -  
1060 - .more-item {  
1061 - display: flex;  
1062 - align-items: center;  
1063 - padding: 24rpx 28rpx;  
1064 - gap: 16rpx;  
1065 -  
1066 - .item-text {  
1067 - font-size: 28rpx;  
1068 - color: #fff;  
1069 - }  
1070 - }  
1071 -  
1072 - // 分割线  
1073 - .more-item+.more-item {  
1074 - border-top: 1rpx solid #444;  
1075 - }  
1076 - }  
1077 -}  
1078 -  
1079 -.calendar-icon,  
1080 -.add-icon {  
1081 - width: 40rpx;  
1082 - height: 40rpx;  
1083 - margin-bottom: 10rpx;  
1084 -}  
1085 -  
1086 -.action-text {  
1087 - font-size: 24rpx;  
1088 - color: #aaa;  
1089 } 859 }
1090 </style> 860 </style>