Authored by Bad

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

Too many changes to show.

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

@@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1 @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development) 8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081 9 SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080 10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
11 -# SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com 11 + SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/ 12 # SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080 13 ### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc 14 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
@@ -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)
251 195
252 -// 2. 体重选择器状态 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 +])
  214 +
  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) 223 +const defaultWeightIndex = ref([60, 0, 0])
262 224
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) // 取小数第一位 225 +// ===================== 难度 =====================
  226 +const showDifficulty = ref(false)
  227 +const initialDifficulty = { label: '难度', color: '#ffffff' }
  228 +const currentDifficulty = ref({ ...initialDifficulty })
269 229
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 -} 230 +// ===================== 训练数据 =====================
  231 +const recordList = ref([{
  232 + h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
  233 + distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
  234 +}])
275 235
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 -}  
284 -  
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; 263 +const exposeRecordList = computed(() => {
  264 + return recordList.value.filter(item => item.isActive).map(item => ({
  265 + ...item,
  266 + weight: getRealWeight(item)
  267 + }))
  268 +})
  269 +
  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) }))
349 } 277 }
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 278 + return map
  279 +})
355 280
356 - // 1. 如果是【模板训练】,删除对应项  
357 - if (trainingStore.type === 3) {  
358 - console.log('模板训练:删除第', props.unitIndex, '个单元')  
359 - trainingStore.actionDetail.units.splice(props.unitIndex, 1) 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
360 } 287 }
361 - else {  
362 - console.log('单个动作/超级组:清空')  
363 - trainingStore.clearTrainingStore() 288 + return 0
  289 +})
  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 + })
364 } 297 }
365 - // 关闭菜单  
366 - showTips.value = false  
367 - console.log('删除完成 ✅') 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 +})
368 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
369 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 };  
421 - } 377 + records = superRecordMap.value
422 } 378 }
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;  
437 - } 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 }
438 } 397 }
  398 + showDifficulty.value = false
  399 +}
439 400
440 - nextTick(() => {  
441 - emit('register', props.index, {  
442 - recordList: recordList.value,  
443 - superRecordMap: superRecordMap.value  
444 - }) 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)
  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
445 }) 419 })
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; 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)
460 } 429 }
461 - // 调用 Pinia 保存  
462 - trainingStore.saveUnitRecord(props.unitIndex, {  
463 - records: records,  
464 - userWeight: userWeight.value  
465 - });  
466 -}; 430 +}
467 431
468 -watch(recordList, () => {  
469 - // console.log('recordList变化,发送长度:', recordList.value.length)  
470 - emit('update:groupCount', recordList.value.length)  
471 -}, { deep: true, immediate: true }) 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 +}
472 440
473 -watch([recordList, superRecordMap, userWeight], () => {  
474 - saveToStore();  
475 - console.log('+++++++++打印全局训练数据 trainingStore.unitRecords++++++++', trainingStore.unitRecords);  
476 -}, { deep: true, immediate: false }); 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 +}
477 449
478 -const toggleActive = (index) => {  
479 - recordList.value[index].isActive = !recordList.value[index].isActive;  
480 -}; 450 +function onWeightConfirm(e) {
  451 + userWeight.value = parseFloat(`${e.value[0]}.${e.value[1]}`)
  452 + showWeightPicker.value = false
  453 +}
481 454
482 -// 超级组工具方法  
483 -const superGroupSetData = (setIndex) => {  
484 - const firstSub = props.actionDetail?.exercises?.[0]  
485 - if (!firstSub) return {}  
486 - return superRecordMap.value[firstSub.id]?.[setIndex] || {} 455 +// ===================== 备注 =====================
  456 +function openBeizhu() {
  457 + nextTick(() => {
  458 + if (showBeizhuRef.value) showBeizhuRef.value.open(props.actionDetail.id)
  459 + })
487 } 460 }
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 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 })
  468 + }
  469 + props.actionDetail.userNote = content
  470 + } catch (e) {
  471 + // 静默处理
  472 + }
  473 +}
  474 +
  475 +// ===================== 替换/删除动作 =====================
  476 +function handleReplaceAction() {
  477 + showTips.value = false
  478 + replacePopupShow.value = true
  479 +}
  480 +
  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()
498 } 493 }
  494 + showTips.value = false
  495 +}
  496 +
  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
499 } 507 }
500 508
501 -const openSuperPicker = (setIndex, actionId) => { 509 +function openQuickTimer(index) {
  510 + activeIndex.value = index
  511 + showQuickPicker.value = true
  512 +}
  513 +
  514 +function openAllQuickTimer() {
  515 + restEditMode.value = 'all'
  516 + showQuickPicker.value = true
  517 +}
  518 +
  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) })
  560 + }
596 } 561 }
597 - }  
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) => { 602 +
  603 +function deleteSuperSet(setIndex) {
660 const exercises = props.actionDetail?.exercises || [] 604 const exercises = props.actionDetail?.exercises || []
661 -  
662 - // 没有子动作直接返回  
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 -})  
707 -  
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 -}  
738 -  
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) // ✅ 传入子动作类型  
761 - }  
762 - })  
763 - }  
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) {  
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  
778 - }  
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 // 👈 重量 × 次数  
793 - }  
794 - })  
795 - }  
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  
802 -  
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  
808 - }  
809 - })  
810 - }  
811 - }  
812 - return sum  
813 -})  
814 -  
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  
823 - })  
824 - }  
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 629 +// ===================== Watchers =====================
  630 +watch(recordList, () => {
  631 + emit('update:groupCount', recordList.value.length)
  632 +}, { deep: true, immediate: true })
829 633
830 - superRecordMap.value[actionId].forEach(item => {  
831 - const realWeight = getRealWeight(item, subType)  
832 - const reps = Number(item.reps) || 0  
833 - sum += realWeight * reps  
834 - })  
835 - }  
836 - }  
837 - return sum  
838 -}) 634 +watch([recordList, superRecordMap, userWeight], saveToStore, { deep: true })
839 635
840 -// ==============================================  
841 -// 统一监听:普通动作 + 超级组 的自重减重(type=5)  
842 -// ==============================================  
843 watch( 636 watch(
844 () => [recordList.value, superRecordMap.value], 637 () => [recordList.value, superRecordMap.value],
845 () => { 638 () => {
846 - const userW = userWeight.value || 0;  
847 -  
848 - // --------------------------  
849 - // 1. 监听【普通动作】  
850 - // -------------------------- 639 + const userW = userWeight.value || 0
851 if (props.type === 1 && props.actionDetail?.exerciseType === 5) { 640 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 - }); 641 + recordList.value.forEach(item => {
  642 + if ((Number(item.weight) || 0) > userW) {
  643 + item.weight = ''
  644 + uni.showToast({ title: '辅助重量不能超过体重', icon: 'none', duration: 2000 })
861 } 645 }
862 - }); 646 + })
863 } 647 }
864 -  
865 - // --------------------------  
866 - // 2. 监听【超级组】(包含多个子动作)  
867 - // --------------------------  
868 if (props.type === 2) { 648 if (props.type === 2) {
869 - // 遍历超级组里的所有子动作  
870 - props.actionDetail?.exercises?.forEach((sub) => {  
871 - // 只校验 自重减重 type=5 649 + props.actionDetail?.exercises?.forEach(sub => {
872 if (sub.exerciseType === 5) { 650 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 - }); 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 })
883 } 655 }
884 - }); 656 + })
885 } 657 }
886 - }); 658 + })
887 } 659 }
888 }, 660 },
889 { deep: true } 661 { deep: true }
890 -); 662 +)
891 663
892 -defineExpose({  
893 - recordList: exposeRecordList, // 过滤后  
894 - superRecordMap: exposeSuperRecordMap, // 过滤后  
895 - totalSetCount, //全部的组数  
896 - checkedWeight, // 已勾选重量  
897 - totalWeight // 全部重量  
898 -}) 664 +// ===================== Lifecycle =====================
  665 +onMounted(() => {
  666 + const savedData = trainingStore.getUnitRecord(props.unitIndex)
  667 + userWeight.value = savedData.userWeight || 70
899 668
900 -// 打开备注弹窗  
901 -// const openBeizhu = () => {  
902 -// console.log('传递到备注组件的id', actionId.value)  
903 -// showBeizhuRef.value.open(actionId.value)  
904 -// } 669 + if (props.type === 1) {
  670 + if (savedData.records?.[props.actionDetail.id]) {
  671 + recordList.value = [...savedData.records[props.actionDetail.id]]
  672 + }
  673 + } else if (props.type === 2) {
  674 + if (savedData.records && Object.keys(savedData.records).length > 0) {
  675 + superRecordMap.value = { ...savedData.records }
  676 + }
  677 + }
905 678
906 -watch(() => props.actionDetail, () => {  
907 - console.log('actionDetail.exercises:', props.actionDetail?.exercises)  
908 -}, { immediate: true }) 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 + }]
  689 + })
  690 + superRecordMap.value = map
  691 + }
  692 + }
  693 +
  694 + nextTick(() => {
  695 + emit('register', props.index, {
  696 + recordList: recordList.value,
  697 + superRecordMap: superRecordMap.value
  698 + })
  699 + })
  700 +})
909 701
  702 +// ===================== Expose =====================
  703 +defineExpose({
  704 + recordList: exposeRecordList,
  705 + superRecordMap: exposeSuperRecordMap,
  706 + totalSetCount,
  707 + checkedWeight,
  708 + totalWeight,
  709 + resetActiveSets
  710 +})
910 </script> 711 </script>
911 712
912 <style lang="scss" scoped> 713 <style lang="scss" scoped>
  714 +// ===================== 变量 =====================
913 $bg-black: #303030; 715 $bg-black: #303030;
914 $card-bg: #1c1c1e; 716 $card-bg: #1c1c1e;
915 $item-bg: #2c2c2e; 717 $item-bg: #2c2c2e;
  718 +$input-bg: #1f1f1f;
  719 +$popup-bg: #3e3e3e;
916 $text-main: #ffffff; 720 $text-main: #ffffff;
917 $text-gray: #8e8e93; 721 $text-gray: #8e8e93;
  722 +$text-dim: #929292;
918 $theme-yellow: #f8d714; 723 $theme-yellow: #f8d714;
919 724
  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 +}
  737 +
  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;
  764 +
  765 + &.active {
  766 + background-color: $theme-yellow;
  767 + }
  768 +}
  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; 918 + color: $text-main;
1075 } 919 }
1076 } 920 }
1077 921
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;  
1231 - }  
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 - } 922 + .super-group {
  923 + width: 100%;
1287 } 924 }
1288 } 925 }
1289 926
1290 - // 新增超级组样式  
1291 - .super-group {  
1292 - width: 100%;  
1293 - }  
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 - }  
1306 -  
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; 955 + @include timer-selector;
  956 + }
1372 957
1373 - .text {  
1374 - font-size: 20rpx;  
1375 - color: #8e8e93;  
1376 - } 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;
  965 +
  966 + .action-icon {
  967 + @include action-icon-base;
1387 968
1388 - &.active {  
1389 - background-color: $theme-yellow; 969 + &.icon-del {
  970 + background-color: #58372d;
  971 + }
1390 } 972 }
1391 } 973 }
1392 } 974 }
1393 975
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;  
1403 - }  
1404 -  
1405 - .super-action-item {  
1406 - margin-bottom: 30rpx;  
1407 - }  
1408 -  
1409 - .super-action-title {  
1410 - font-size: 28rpx;  
1411 - color: #fff;  
1412 - padding: 10rpx 30rpx;  
1413 - font-weight: bold;  
1414 - }  
1415 - 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
@@ -429,4 +429,4 @@ $theme-yellow: #f8d714; @@ -429,4 +429,4 @@ $theme-yellow: #f8d714;
429 } 429 }
430 } 430 }
431 } 431 }
432 -</style>  
  432 +</style>
@@ -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" />  
160 - <view class="legend">  
161 - <view class="legend-item">  
162 - <view class="legend-color primary"></view>  
163 - <text class="legend-text">主要部位</text> 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" />
  163 + <view class="legend">
  164 + <view class="legend-item">
  165 + <view class="legend-dot primary"></view>
  166 + <text>主要部位</text>
  167 + </view>
  168 + <view class="legend-item">
  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>
164 </view> 180 </view>
165 - <view class="legend-item">  
166 - <view class="legend-color secondary"></view>  
167 - <text class="legend-text">次要部位</text> 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
762 - .muscle-map {  
763 - width: 100%;  
764 - border-radius: 16rpx;  
765 - } 780 + // ===================== 训练部位 =====================
  781 + .muscle-section {
  782 + .muscle-card {
  783 + position: relative;
  784 + background: #262626;
  785 + border-radius: 16rpx;
  786 + overflow: hidden;
766 787
767 - .legend {  
768 - display: flex;  
769 - justify-content: flex-end;  
770 - align-items: center;  
771 - gap: 20rpx;  
772 - margin-top: 20rpx;  
773 - } 788 + .muscle-map {
  789 + width: 100%;
  790 + display: block;
  791 + }
774 792
775 - .legend-item {  
776 - display: flex;  
777 - align-items: center;  
778 - gap: 10rpx;  
779 - } 793 + .legend {
  794 + position: absolute;
  795 + bottom: 12rpx;
  796 + right: 16rpx;
  797 + display: flex;
  798 + gap: 20rpx;
  799 + background: rgba(0, 0, 0, 0.65);
  800 + backdrop-filter: blur(6px);
  801 + padding: 10rpx 20rpx;
  802 + border-radius: 30rpx;
780 803
781 - .legend-color {  
782 - width: 16rpx;  
783 - height: 16rpx;  
784 - border-radius: 4rpx;  
785 - } 804 + .legend-item {
  805 + display: flex;
  806 + align-items: center;
  807 + gap: 8rpx;
  808 + font-size: 22rpx;
  809 + color: #ddd;
  810 + }
786 811
787 - .legend-color.primary {  
788 - background-color: #ffd700;  
789 - } 812 + .legend-dot {
  813 + width: 14rpx;
  814 + height: 14rpx;
  815 + border-radius: 4rpx;
790 816
791 - .legend-color.secondary {  
792 - background-color: #999;  
793 - } 817 + &.primary {
  818 + background-color: #ffd700;
  819 + }
794 820
795 - .legend-text {  
796 - font-size: 24rpx;  
797 - color: #fff;  
798 - } 821 + &.secondary {
  822 + background-color: #999;
  823 + }
  824 + }
  825 + }
  826 + }
799 827
800 - .stat-card {  
801 - background: linear-gradient(135deg, #333, #222);  
802 - padding: 40rpx;  
803 - border-radius: 20rpx;  
804 - display: flex;  
805 - flex-direction: column;  
806 - align-items: center;  
807 - border: 1rpx solid #444; 828 + .muscle-tags {
  829 + display: flex;
  830 + gap: 20rpx;
  831 + margin-top: 20rpx;
808 832
809 - .label {  
810 - font-size: 24rpx;  
811 - color: #888;  
812 - } 833 + .muscle-group {
  834 + flex: 1;
  835 + background: #262626;
  836 + border-radius: 16rpx;
  837 + padding: 24rpx;
813 838
814 - .value {  
815 - font-size: 60rpx;  
816 - font-weight: bold;  
817 - color: #fedc1f;  
818 - margin: 10rpx 0;  
819 - } 839 + .group-label {
  840 + font-size: 24rpx;
  841 + font-weight: 600;
  842 + margin-bottom: 16rpx;
820 843
821 - .date {  
822 - font-size: 22rpx;  
823 - color: #666; 844 + &.primary {
  845 + color: #ffd700;
  846 + }
  847 +
  848 + &.secondary {
  849 + color: #aaa;
  850 + }
  851 + }
  852 +
  853 + .pills {
  854 + display: flex;
  855 + flex-wrap: wrap;
  856 + gap: 12rpx;
  857 +
  858 + .pill {
  859 + font-size: 22rpx;
  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,16 +372,16 @@ onMounted(async () => { @@ -372,16 +372,16 @@ 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 {
382 width: 100%; 382 width: 100%;
383 height: 100%; 383 height: 100%;
384 - 384 +
385 .scroll-inner { 385 .scroll-inner {
386 padding: 24rpx 30rpx; 386 padding: 24rpx 30rpx;
387 display: flex; 387 display: flex;
@@ -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,21 +110,19 @@ defineExpose({ switchTabIndex: handleTabClick }); @@ -114,21 +110,19 @@ 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 {
126 - 120 +
127 box-sizing: border-box; 121 box-sizing: border-box;
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 -  
126 -const trainingStore = useTrainingStore()  
127 -  
128 -const canSave = ref(false);  
129 -const showGuide = ref(true);  
130 134
131 -const showIntroPopup = ref(false);  
132 -const templateIntro = ref(''); 135 +dayjs.extend(duration);
133 136
134 -const showTitlePopup = ref(false);  
135 -const templateTitle = ref(''); 137 +// ---- 常量 ----
  138 +const NAVIGATE_DELAY = 1500;
136 139
137 -const actionSortRef = ref(null)  
138 -  
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 +];
  146 +
  147 +const SCENE_COLUMNS = [SCENE_LIST.map((item) => item.title)];
145 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);
  176 +
  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 +});
148 184
149 -const sceneColumns = ref([  
150 - ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃']  
151 -]); 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 +});
152 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  
213 - }  
214 - else if (ex.exerciseType === 5) {  
215 - total += (userWeight - weight) * reps  
216 - }  
217 - })  
218 - })  
219 - })  
220 -  
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; 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;
  234 + }
  235 + templateTitle.value = popupValue.value.trim();
  236 + } else {
  237 + templateIntro.value = popupValue.value;
234 } 238 }
  239 + uni.showToast({ title: '保存成功', icon: 'success' });
  240 + closeTextPopup();
  241 +};
  242 +
  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 - } 282 +// ---- 保存 ----
  283 +const buildSavePayload = () => ({
  284 + templateId: trainingStore.actionDetail.id || null,
  285 + groupId: null,
  286 + templateName: templateTitle.value.trim(),
  287 + scene: selectedSceneId.value,
  288 + templateCover: coverImage.value || '',
  289 + templateIntroduction: templateIntro.value || '',
  290 + units: units.value.map((unit, unitIndex) => {
  291 + const record = trainingStore.unitRecords[unitIndex] || {};
  292 + return {
  293 + id: unit.unitId || 0,
  294 + name: unit.name || '',
  295 + unitType: unit.unitType || 1,
  296 + sortOrder: unitIndex,
  297 + exercises: (unit.exercises || []).map((ex, exIndex) => ({
  298 + exerciseId: ex.exerciseId || 0,
  299 + exerciseType: ex.exerciseType || 0,
  300 + innerOrder: exIndex,
  301 + sets: (record.records?.[ex.exerciseId] || []).map((set, setIndex) => {
  302 + if (ex.exerciseType === 6) {
  303 + return { weight: 0, reps: 0, distance: 0, duration: Number(set.duration) || 0, restTime: Number(set.restTime) || 0, setIndex };
  304 + }
  305 + return {
  306 + weight: Number(set.weight) || 0,
  307 + reps: Number(set.reps) || 0,
  308 + distance: Number(set.distance) || 0,
  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,
  312 + };
  313 + }),
  314 + })),
  315 + };
  316 + }),
  317 +});
341 318
  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; }
342 try { 322 try {
343 uni.showLoading({ title: '保存中...' }); 323 uni.showLoading({ title: '保存中...' });
344 - const postData = {  
345 - templateId: trainingStore.actionDetail.id || null,  
346 - groupId: null,  
347 - templateName: templateTitle.value.trim(),  
348 - scene: selectedSceneId.value,  
349 - templateCover: coverImage.value || '',  
350 - templateIntroduction: templateIntro.value || '',  
351 - units: units.map((unit, unitIndex) => {  
352 - const record = trainingStore.unitRecords[unitIndex] || {};  
353 - return {  
354 - id: unit.unitId || 0,  
355 - name: unit.name || '',  
356 - unitType: unit.unitType || 1,  
357 - sortOrder: unitIndex,  
358 - exercises: (unit.exercises || []).map((ex, exIndex) => {  
359 - const sets = record.records?.[ex.exerciseId] || [];  
360 - return {  
361 - exerciseId: ex.exerciseId || 0,  
362 - exerciseType: ex.exerciseType || 0,  
363 - innerOrder: exIndex,  
364 - sets: sets.map((set, setIndex) => {  
365 - let duration = 0;  
366 - let restTime = 0;  
367 - // 间歇训练单独处理  
368 - 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;  
379 - }  
380 -  
381 - return {  
382 - weight: Number(set.weight) || 0,  
383 - reps: Number(set.reps) || 0,  
384 - distance: Number(set.distance) || 0,  
385 - duration: duration,  
386 - restTime: restTime,  
387 - setIndex: setIndex,  
388 - };  
389 - }),  
390 - };  
391 - }),  
392 - };  
393 - }),  
394 - };  
395 -  
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); 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;
  587 + box-sizing: border-box;
660 } 588 }
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;  
754 -}  
755 -</style>  
  589 +</style>