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