Showing
29 changed files
with
2573 additions
and
2807 deletions
| @@ -58,7 +58,8 @@ | @@ -58,7 +58,8 @@ | ||
| 58 | { | 58 | { |
| 59 | "path": "pages/xunji/xunji-wode-moban", | 59 | "path": "pages/xunji/xunji-wode-moban", |
| 60 | "style": { | 60 | "style": { |
| 61 | - "navigationBarTitleText": "我的模版" | 61 | + "navigationBarTitleText": "我的模版", |
| 62 | + "navigationStyle": "default" | ||
| 62 | } | 63 | } |
| 63 | }, | 64 | }, |
| 64 | { | 65 | { |
| @@ -82,7 +83,7 @@ | @@ -82,7 +83,7 @@ | ||
| 82 | { | 83 | { |
| 83 | "path": "pages/xunji/xunji-dongzuo-xinzeng", | 84 | "path": "pages/xunji/xunji-dongzuo-xinzeng", |
| 84 | "style": { | 85 | "style": { |
| 85 | - "navigationBarTitleText": "", | 86 | + "navigationBarTitleText": "新增自定义动作", |
| 86 | "navigationStyle": "default" | 87 | "navigationStyle": "default" |
| 87 | } | 88 | } |
| 88 | }, | 89 | }, |
| @@ -180,13 +181,14 @@ | @@ -180,13 +181,14 @@ | ||
| 180 | { | 181 | { |
| 181 | "path": "pages/user/wode-changjian-wenti", | 182 | "path": "pages/user/wode-changjian-wenti", |
| 182 | "style": { | 183 | "style": { |
| 183 | - "navigationBarTitleText": "我的常见问题" | 184 | + "navigationBarTitleText": "常见问题", |
| 185 | + "navigationStyle": "default" | ||
| 184 | } | 186 | } |
| 185 | }, | 187 | }, |
| 186 | { | 188 | { |
| 187 | "path": "pages/user/wode-guanyu-hongxing", | 189 | "path": "pages/user/wode-guanyu-hongxing", |
| 188 | "style": { | 190 | "style": { |
| 189 | - "navigationBarTitleText": "我的关于鸿星" | 191 | + "navigationBarTitleText": "关于我们" |
| 190 | } | 192 | } |
| 191 | }, | 193 | }, |
| 192 | { | 194 | { |
| @@ -8,7 +8,7 @@ | @@ -8,7 +8,7 @@ | ||
| 8 | <view @click="show = false"> | 8 | <view @click="show = false"> |
| 9 | <up-icon name="close" color="#fff" size="20"></up-icon> | 9 | <up-icon name="close" color="#fff" size="20"></up-icon> |
| 10 | </view> | 10 | </view> |
| 11 | - <text class="titleName">动作备注</text> | 11 | + <text class="titleName">{{ title }}</text> |
| 12 | <view class="btn" @click="saveNoteContent">保存</view> | 12 | <view class="btn" @click="saveNoteContent">保存</view> |
| 13 | </view> | 13 | </view> |
| 14 | <view class="area-box"> | 14 | <view class="area-box"> |
| @@ -30,6 +30,10 @@ const tempNoteContent = ref(''); // 临时编辑的备注内容 | @@ -30,6 +30,10 @@ const tempNoteContent = ref(''); // 临时编辑的备注内容 | ||
| 30 | const emit = defineEmits(['saveSuccess']); | 30 | const emit = defineEmits(['saveSuccess']); |
| 31 | 31 | ||
| 32 | const props = defineProps({ | 32 | const props = defineProps({ |
| 33 | + title: { | ||
| 34 | + type: String, | ||
| 35 | + default: '动作备注' | ||
| 36 | + }, | ||
| 33 | oldNote: { | 37 | oldNote: { |
| 34 | type: String, | 38 | type: String, |
| 35 | default: '' | 39 | default: '' |
| @@ -185,7 +185,7 @@ | @@ -185,7 +185,7 @@ | ||
| 185 | ====================================== --> | 185 | ====================================== --> |
| 186 | <view class="icons"> | 186 | <view class="icons"> |
| 187 | <template v-if="!isEditing"> | 187 | <template v-if="!isEditing"> |
| 188 | - <view class="icon" @click.stop="emitToggleActive(index)" :class="{ active: record.isActive }"> | 188 | + <view class="icon" v-if="isTick" @click.stop="emitToggleActive(index)" :class="{ active: record.isActive }"> |
| 189 | <up-icon name="checkmark" :color="record.isActive ? '#000' : '#919191'" size="20" bold></up-icon> | 189 | <up-icon name="checkmark" :color="record.isActive ? '#000' : '#919191'" size="20" bold></up-icon> |
| 190 | </view> | 190 | </view> |
| 191 | </template> | 191 | </template> |
| @@ -210,6 +210,10 @@ const props = defineProps({ | @@ -210,6 +210,10 @@ const props = defineProps({ | ||
| 210 | userWeight: { | 210 | userWeight: { |
| 211 | type: Number, | 211 | type: Number, |
| 212 | default: 70 | 212 | default: 70 |
| 213 | + }, | ||
| 214 | + isTick: { | ||
| 215 | + type: Boolean, | ||
| 216 | + default: true | ||
| 213 | } | 217 | } |
| 214 | }) | 218 | }) |
| 215 | 219 |
| @@ -51,7 +51,7 @@ | @@ -51,7 +51,7 @@ | ||
| 51 | <action-set-input v-for="(item, index) in recordList" :key="index" | 51 | <action-set-input v-for="(item, index) in recordList" :key="index" |
| 52 | :exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing" | 52 | :exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing" |
| 53 | @openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow" | 53 | @openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow" |
| 54 | - @deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" /> | 54 | + @deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" :isTick="isTick"/> |
| 55 | </template> | 55 | </template> |
| 56 | 56 | ||
| 57 | <template v-else-if="type === 2"> | 57 | <template v-else-if="type === 2"> |
| @@ -97,7 +97,7 @@ | @@ -97,7 +97,7 @@ | ||
| 97 | </view> | 97 | </view> |
| 98 | <super-set-input v-for="(sub, idx) in actionDetail.exercises" :key="sub.id" | 98 | <super-set-input v-for="(sub, idx) in actionDetail.exercises" :key="sub.id" |
| 99 | :sub-exercise-type="sub.exerciseType" :set-index="setIndex" :sub-index="idx" :user-weight="userWeight" | 99 | :sub-exercise-type="sub.exerciseType" :set-index="setIndex" :sub-index="idx" :user-weight="userWeight" |
| 100 | - :data="getSubActionData(setIndex, sub.id)" @open-time-picker="openSuperPicker(setIndex, sub.id)" /> | 100 | + :data="getSubActionData(setIndex, sub.id)" @open-time-picker="openSuperPicker(setIndex, sub.id)" :isTick="isTick"/> |
| 101 | </view> | 101 | </view> |
| 102 | </view> | 102 | </view> |
| 103 | </template> | 103 | </template> |
| @@ -171,7 +171,8 @@ const props = defineProps({ | @@ -171,7 +171,8 @@ const props = defineProps({ | ||
| 171 | unitIndex: Number, | 171 | unitIndex: Number, |
| 172 | actionDetail: { type: Object, default: () => ({}) }, | 172 | actionDetail: { type: Object, default: () => ({}) }, |
| 173 | isDailyTemplates: { type: Boolean, default: false }, | 173 | isDailyTemplates: { type: Boolean, default: false }, |
| 174 | - isUnitActive: { type: Boolean, default: false } | 174 | + isUnitActive: { type: Boolean, default: false }, |
| 175 | + isTick: { type: Boolean, default: true }, | ||
| 175 | }) | 176 | }) |
| 176 | 177 | ||
| 177 | // ===================== Emits ===================== | 178 | // ===================== Emits ===================== |
| @@ -400,19 +401,22 @@ function selectDifficulty(label, color) { | @@ -400,19 +401,22 @@ function selectDifficulty(label, color) { | ||
| 400 | 401 | ||
| 401 | function toggleActive(index) { | 402 | function toggleActive(index) { |
| 402 | const wasActive = recordList.value[index].isActive | 403 | const wasActive = recordList.value[index].isActive |
| 404 | + // 在状态改变前获取当前组的休息秒数,用于取消时传递给父组件 | ||
| 405 | + const seconds = parseQuickTime(recordList.value[index].quickTimeDisplay) | ||
| 403 | recordList.value[index].isActive = !recordList.value[index].isActive | 406 | recordList.value[index].isActive = !recordList.value[index].isActive |
| 404 | const hasAnyActive = recordList.value.some(item => item.isActive) | 407 | const hasAnyActive = recordList.value.some(item => item.isActive) |
| 405 | emit('updateUnitActive', hasAnyActive) | 408 | emit('updateUnitActive', hasAnyActive) |
| 406 | if (recordList.value[index].isActive) { | 409 | if (recordList.value[index].isActive) { |
| 407 | - const seconds = parseQuickTime(recordList.value[index].quickTimeDisplay) | ||
| 408 | emit('startRestTimer', seconds, props.unitIndex) | 410 | emit('startRestTimer', seconds, props.unitIndex) |
| 409 | } else if (wasActive) { | 411 | } else if (wasActive) { |
| 410 | - emit('cancelRestTimer', props.unitIndex) | 412 | + emit('cancelRestTimer', seconds, props.unitIndex) |
| 411 | } | 413 | } |
| 412 | } | 414 | } |
| 413 | 415 | ||
| 414 | function toggleSuperSetActive(setIndex) { | 416 | function toggleSuperSetActive(setIndex) { |
| 415 | const wasActive = superGroupSetData(setIndex).isActive | 417 | const wasActive = superGroupSetData(setIndex).isActive |
| 418 | + // 在状态改变前获取当前组的休息秒数,用于取消时传递给父组件 | ||
| 419 | + const restSeconds = parseQuickTime(superGroupSetData(setIndex).quickTimeDisplay) | ||
| 416 | props.actionDetail.exercises.forEach(sub => { | 420 | props.actionDetail.exercises.forEach(sub => { |
| 417 | const list = superRecordMap.value[sub.id] | 421 | const list = superRecordMap.value[sub.id] |
| 418 | if (list?.[setIndex]) list[setIndex].isActive = !list[setIndex].isActive | 422 | if (list?.[setIndex]) list[setIndex].isActive = !list[setIndex].isActive |
| @@ -422,10 +426,9 @@ function toggleSuperSetActive(setIndex) { | @@ -422,10 +426,9 @@ function toggleSuperSetActive(setIndex) { | ||
| 422 | ) | 426 | ) |
| 423 | emit('updateUnitActive', hasAnyActive) | 427 | emit('updateUnitActive', hasAnyActive) |
| 424 | if (!wasActive) { | 428 | if (!wasActive) { |
| 425 | - const restSeconds = parseQuickTime(superGroupSetData(setIndex).quickTimeDisplay) | ||
| 426 | emit('startRestTimer', restSeconds, props.unitIndex) | 429 | emit('startRestTimer', restSeconds, props.unitIndex) |
| 427 | } else { | 430 | } else { |
| 428 | - emit('cancelRestTimer', props.unitIndex) | 431 | + emit('cancelRestTimer', restSeconds, props.unitIndex) |
| 429 | } | 432 | } |
| 430 | } | 433 | } |
| 431 | 434 |
| @@ -119,9 +119,6 @@ | @@ -119,9 +119,6 @@ | ||
| 119 | <view class="memo-box" v-if="type === 1"> | 119 | <view class="memo-box" v-if="type === 1"> |
| 120 | <view class="memo-title"> | 120 | <view class="memo-title"> |
| 121 | <view class="section-title">步骤</view> | 121 | <view class="section-title">步骤</view> |
| 122 | - <view class="pencil-icon"> | ||
| 123 | - <up-icon name="edit-pen" color="#fedc1f" size="20"></up-icon> | ||
| 124 | - </view> | ||
| 125 | </view> | 122 | </view> |
| 126 | <view class="steps-list"> | 123 | <view class="steps-list"> |
| 127 | <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text> | 124 | <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text> |
| @@ -158,9 +155,9 @@ | @@ -158,9 +155,9 @@ | ||
| 158 | <view class="section-title">训练部位</view> | 155 | <view class="section-title">训练部位</view> |
| 159 | <view class="muscle-card"> | 156 | <view class="muscle-card"> |
| 160 | <image | 157 | <image |
| 161 | - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260612/muscle_back_1781252170914.png" | 158 | + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg" |
| 162 | mode="widthFix" class="muscle-map" /> | 159 | mode="widthFix" class="muscle-map" /> |
| 163 | - <view class="legend"> | 160 | + <!-- <view class="legend"> |
| 164 | <view class="legend-item"> | 161 | <view class="legend-item"> |
| 165 | <view class="legend-dot primary"></view> | 162 | <view class="legend-dot primary"></view> |
| 166 | <text>主要部位</text> | 163 | <text>主要部位</text> |
| @@ -168,8 +165,8 @@ | @@ -168,8 +165,8 @@ | ||
| 168 | <view class="legend-item"> | 165 | <view class="legend-item"> |
| 169 | <view class="legend-dot secondary"></view> | 166 | <view class="legend-dot secondary"></view> |
| 170 | <text>次要部位</text> | 167 | <text>次要部位</text> |
| 171 | - </view> | ||
| 172 | - </view> | 168 | + </view> --> |
| 169 | + <!-- </view> --> | ||
| 173 | </view> | 170 | </view> |
| 174 | <view class="muscle-tags"> | 171 | <view class="muscle-tags"> |
| 175 | <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length"> | 172 | <view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length"> |
| @@ -768,12 +765,14 @@ defineExpose({ open }); | @@ -768,12 +765,14 @@ defineExpose({ open }); | ||
| 768 | background: #464646; | 765 | background: #464646; |
| 769 | box-sizing: border-box; | 766 | box-sizing: border-box; |
| 770 | border-radius: 16rpx; | 767 | border-radius: 16rpx; |
| 768 | + padding: 24rpx; | ||
| 771 | 769 | ||
| 772 | .step-text { | 770 | .step-text { |
| 773 | font-size: 30rpx; | 771 | font-size: 30rpx; |
| 774 | line-height: 1.6; | 772 | line-height: 1.6; |
| 775 | color: #bbb; | 773 | color: #bbb; |
| 776 | - list-style: none; | 774 | + white-space: pre-wrap; |
| 775 | + word-break: break-all; | ||
| 777 | } | 776 | } |
| 778 | } | 777 | } |
| 779 | 778 |
| 1 | <template> | 1 | <template> |
| 2 | <!-- 添加自助训练底部弹窗 --> | 2 | <!-- 添加自助训练底部弹窗 --> |
| 3 | - <view v-if="visible" class="popup-overlay" @click="close"> | ||
| 4 | - <view class="popup-bottom" @click.stop> | ||
| 5 | - <view class="popup-options"> | ||
| 6 | - <view class="popup-option" @click="handleAddFromPlan" v-if="planListCount > 0"> | ||
| 7 | - <view class=" icon-wrap"> | ||
| 8 | - <!-- <image src="/static/icons/plan.png" mode="aspectFit"></image> --> | ||
| 9 | - <up-icon name="file-text-fill" color="#040000" size="24"></up-icon> | 3 | + <up-popup |
| 4 | + :show="visible" | ||
| 5 | + mode="bottom" | ||
| 6 | + round="24rpx" | ||
| 7 | + bg-color="#1a1a1a" | ||
| 8 | + :close-on-click-overlay="true" | ||
| 9 | + :safe-area-inset-bottom="true" | ||
| 10 | + @close="close" | ||
| 11 | + > | ||
| 12 | + <view class="add-train-popup"> | ||
| 13 | + <!-- 拖拽指示条 --> | ||
| 14 | + <view class="popup-indicator" /> | ||
| 15 | + | ||
| 16 | + <!-- 菜单选项列表 --> | ||
| 17 | + <view class="popup-menu"> | ||
| 18 | + <!-- 从训练计划中添加 --> | ||
| 19 | + <view | ||
| 20 | + v-if="planListCount > 0" | ||
| 21 | + class="menu-item" | ||
| 22 | + @click="handleAddFromPlan" | ||
| 23 | + > | ||
| 24 | + <view class="menu-icon"> | ||
| 25 | + <up-icon name="file-text-fill" color="#040000" size="24" /> | ||
| 10 | </view> | 26 | </view> |
| 11 | - <view class="text-wrap" @click=""> | ||
| 12 | - <text class="option-title">从训练计划中添加</text> | ||
| 13 | - <text class="option-desc">从我的训练计划中添加</text> | 27 | + <view class="menu-text"> |
| 28 | + <text class="menu-title">从训练计划中添加</text> | ||
| 29 | + <text class="menu-desc">从我的训练计划中添加</text> | ||
| 14 | </view> | 30 | </view> |
| 15 | </view> | 31 | </view> |
| 16 | 32 | ||
| 17 | - <view class="popup-option" @click="handleAddFromTemplate"> | ||
| 18 | - <view class="icon-wrap"> | ||
| 19 | - <up-icon name="grid-fill" color="#040000" size="24"></up-icon> | 33 | + <!-- 使用训练模板 --> |
| 34 | + <view class="menu-item" @click="handleAddFromTemplate"> | ||
| 35 | + <view class="menu-icon"> | ||
| 36 | + <up-icon name="grid-fill" color="#040000" size="24" /> | ||
| 20 | </view> | 37 | </view> |
| 21 | - <view class="text-wrap"> | ||
| 22 | - <text class="option-title">使用训练模板</text> | ||
| 23 | - <text class="option-desc">官方训练模板和自定义模板</text> | 38 | + <view class="menu-text"> |
| 39 | + <text class="menu-title">使用训练模板</text> | ||
| 40 | + <text class="menu-desc">官方训练模板和自定义模板</text> | ||
| 24 | </view> | 41 | </view> |
| 25 | </view> | 42 | </view> |
| 26 | 43 | ||
| 27 | - <view class="popup-option" @click="handleFreeTraining"> | ||
| 28 | - <view class="icon-wrap"> | ||
| 29 | - <up-icon name="clock-fill" color="#040000" size="24"></up-icon> | 44 | + <!-- 自由训练 --> |
| 45 | + <view class="menu-item" @click="handleFreeTraining"> | ||
| 46 | + <view class="menu-icon"> | ||
| 47 | + <up-icon name="clock-fill" color="#040000" size="24" /> | ||
| 30 | </view> | 48 | </view> |
| 31 | - <view class="text-wrap"> | ||
| 32 | - <text class="option-title">自由训练</text> | ||
| 33 | - <text class="option-desc">新建空白训练</text> | 49 | + <view class="menu-text"> |
| 50 | + <text class="menu-title">自由训练</text> | ||
| 51 | + <text class="menu-desc">新建空白训练</text> | ||
| 34 | </view> | 52 | </view> |
| 35 | </view> | 53 | </view> |
| 36 | </view> | 54 | </view> |
| 37 | 55 | ||
| 56 | + <!-- 关闭按钮 --> | ||
| 38 | <view class="close-btn" @click="close"> | 57 | <view class="close-btn" @click="close"> |
| 39 | - <text>x</text> | 58 | + <up-icon name="close" color="#000" size="22" /> |
| 40 | </view> | 59 | </view> |
| 41 | </view> | 60 | </view> |
| 42 | - <!-- 我的计划添加弹窗,显示最新添加的计划的模板列表 传递--> | ||
| 43 | - <WodeJihuaTianjiaTancuang v-model:visible="showPlanPopup" @get-plan-list-length="getPlanListLength" :isAdd="true" | ||
| 44 | - :plan-id="lastPlanId" :is-my-plan="true" /> | ||
| 45 | - </view> | 61 | + </up-popup> |
| 62 | + | ||
| 63 | + <!-- 我的计划添加弹窗(放在外部避免弹窗嵌套) --> | ||
| 64 | + <WodeJihuaTianjiaTancuang | ||
| 65 | + v-model:visible="showPlanPopup" | ||
| 66 | + @get-plan-list-length="getPlanListLength" | ||
| 67 | + :is-add="true" | ||
| 68 | + :plan-id="lastPlanId" | ||
| 69 | + :is-my-plan="true" | ||
| 70 | + /> | ||
| 46 | </template> | 71 | </template> |
| 47 | 72 | ||
| 48 | <script setup> | 73 | <script setup> |
| @@ -50,50 +75,57 @@ import { ref, onMounted, computed } from 'vue' | @@ -50,50 +75,57 @@ import { ref, onMounted, computed } from 'vue' | ||
| 50 | import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue' | 75 | import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue' |
| 51 | import QueryPlanApi from '@/sheep/api/plan/queryplan' | 76 | import QueryPlanApi from '@/sheep/api/plan/queryplan' |
| 52 | 77 | ||
| 53 | - | ||
| 54 | -// 控制训练计划弹窗显示 | ||
| 55 | -const showPlanPopup = ref(false) | ||
| 56 | -const planListCount = ref(0) | ||
| 57 | - | 78 | +// ==================== Props & Emits ==================== |
| 58 | const props = defineProps({ | 79 | const props = defineProps({ |
| 59 | visible: { | 80 | visible: { |
| 60 | type: Boolean, | 81 | type: Boolean, |
| 61 | - default: false | ||
| 62 | - } | 82 | + default: false, |
| 83 | + }, | ||
| 63 | }) | 84 | }) |
| 64 | 85 | ||
| 65 | const emit = defineEmits(['update:visible']) | 86 | const emit = defineEmits(['update:visible']) |
| 66 | 87 | ||
| 67 | -// 关闭弹窗 | 88 | +// ==================== 响应式状态 ==================== |
| 89 | +const showPlanPopup = ref(false) | ||
| 90 | +const planListCount = ref(0) | ||
| 91 | +const planList = ref([]) | ||
| 92 | + | ||
| 93 | +// ==================== 计算属性 ==================== | ||
| 94 | +/** 获取计划列表最后一条的 id,无数据返回 null */ | ||
| 95 | +const lastPlanId = computed(() => { | ||
| 96 | + if (!Array.isArray(planList.value) || planList.value.length === 0) return null | ||
| 97 | + return planList.value.at(-1)?.id ?? null | ||
| 98 | +}) | ||
| 99 | + | ||
| 100 | +// ==================== 方法 ==================== | ||
| 101 | +/** 关闭弹窗 */ | ||
| 68 | const close = () => { | 102 | const close = () => { |
| 69 | emit('update:visible', false) | 103 | emit('update:visible', false) |
| 70 | } | 104 | } |
| 71 | 105 | ||
| 72 | -// 三个逻辑全部放在子组件内部 | ||
| 73 | -const handleAddFromPlan = () => { | ||
| 74 | - // close() // 关闭当前弹窗 | ||
| 75 | - showPlanPopup.value = true | ||
| 76 | -} | ||
| 77 | - | 106 | +/** 获取子组件传递的计划列表长度 */ |
| 78 | const getPlanListLength = (len) => { | 107 | const getPlanListLength = (len) => { |
| 79 | planListCount.value = len | 108 | planListCount.value = len |
| 80 | } | 109 | } |
| 81 | 110 | ||
| 111 | +/** 从训练计划中添加:打开计划选择弹窗 */ | ||
| 112 | +const handleAddFromPlan = () => { | ||
| 113 | + showPlanPopup.value = true | ||
| 114 | +} | ||
| 115 | + | ||
| 116 | +/** 使用训练模板:跳转模板选择页 */ | ||
| 82 | const handleAddFromTemplate = () => { | 117 | const handleAddFromTemplate = () => { |
| 83 | uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' }) | 118 | uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' }) |
| 84 | close() | 119 | close() |
| 85 | } | 120 | } |
| 86 | 121 | ||
| 122 | +/** 自由训练:跳转动作练习页 */ | ||
| 87 | const handleFreeTraining = () => { | 123 | const handleFreeTraining = () => { |
| 88 | uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' }) | 124 | uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' }) |
| 89 | close() | 125 | close() |
| 90 | } | 126 | } |
| 91 | 127 | ||
| 92 | -// 获取我的计划列表的最后一个planid,传递到WodeJihuaTianjiaTancuang中 | ||
| 93 | -// 计划列表 | ||
| 94 | -const planList = ref([]) | ||
| 95 | - | ||
| 96 | -// 获取我的全部训练计划 | 128 | +/** 获取我的全部训练计划 */ |
| 97 | const fetchMyPlanList = async () => { | 129 | const fetchMyPlanList = async () => { |
| 98 | try { | 130 | try { |
| 99 | const res = await QueryPlanApi.getMyPlan() | 131 | const res = await QueryPlanApi.getMyPlan() |
| @@ -108,73 +140,44 @@ const fetchMyPlanList = async () => { | @@ -108,73 +140,44 @@ const fetchMyPlanList = async () => { | ||
| 108 | } | 140 | } |
| 109 | } | 141 | } |
| 110 | 142 | ||
| 111 | -// 计算属性:拿到列表最后一条计划id,无数据返回null | ||
| 112 | -const lastPlanId = computed(() => { | ||
| 113 | - if (!Array.isArray(planList.value) || planList.value.length === 0) return null | ||
| 114 | - const lastPlan = planList.value.at(-1) | ||
| 115 | - return lastPlan?.id ?? null | ||
| 116 | -}) | ||
| 117 | - | 143 | +// ==================== 生命周期 ==================== |
| 118 | onMounted(() => { | 144 | onMounted(() => { |
| 119 | fetchMyPlanList() | 145 | fetchMyPlanList() |
| 120 | }) | 146 | }) |
| 121 | - | ||
| 122 | - | ||
| 123 | </script> | 147 | </script> |
| 124 | 148 | ||
| 125 | <style lang="scss" scoped> | 149 | <style lang="scss" scoped> |
| 126 | -/* 半透明黑色遮罩层 —— 内容靠底部!! */ | ||
| 127 | -.popup-overlay { | ||
| 128 | - position: fixed; | ||
| 129 | - top: 0; | ||
| 130 | - left: 0; | ||
| 131 | - width: 100vw; | ||
| 132 | - height: 100vh; | ||
| 133 | - background: rgba(0, 0, 0, 0.8); | ||
| 134 | - z-index: 9999; | ||
| 135 | - display: flex; | ||
| 136 | - flex-direction: column; | ||
| 137 | - /* 核心:靠下 */ | ||
| 138 | - justify-content: flex-end; | ||
| 139 | - align-items: center; | ||
| 140 | - /* 距离底部一点距离,不贴着最底下 */ | ||
| 141 | - padding-bottom: 150rpx; | ||
| 142 | -} | ||
| 143 | - | ||
| 144 | -/* 弹窗主体容器 */ | ||
| 145 | -.popup-bottom { | ||
| 146 | - width: 85%; | 150 | +.add-train-popup { |
| 147 | display: flex; | 151 | display: flex; |
| 148 | flex-direction: column; | 152 | flex-direction: column; |
| 149 | align-items: center; | 153 | align-items: center; |
| 150 | - gap: 70rpx; | ||
| 151 | - margin-bottom: 100rpx; | ||
| 152 | -} | 154 | + padding: 0 30rpx 40rpx; |
| 155 | + | ||
| 156 | + // 顶部拖拽指示条 | ||
| 157 | + .popup-indicator { | ||
| 158 | + width: 72rpx; | ||
| 159 | + height: 8rpx; | ||
| 160 | + border-radius: 4rpx; | ||
| 161 | + background: rgba(255, 255, 255, 0.3); | ||
| 162 | + margin: 16rpx 0 32rpx; | ||
| 163 | + } | ||
| 153 | 164 | ||
| 154 | -/* 选项列表 */ | ||
| 155 | -.popup-options { | 165 | + // 菜单选项列表 |
| 166 | + .popup-menu { | ||
| 156 | width: 100%; | 167 | width: 100%; |
| 157 | display: flex; | 168 | display: flex; |
| 158 | flex-direction: column; | 169 | flex-direction: column; |
| 159 | - justify-content: center; | ||
| 160 | - align-items: center; | ||
| 161 | - gap: 20rpx; | ||
| 162 | -} | 170 | + gap: 16rpx; |
| 163 | 171 | ||
| 164 | -/* 单个选项项 */ | ||
| 165 | -.popup-option { | ||
| 166 | - width: 100%; | ||
| 167 | - // background: rgba(255, 255, 255, 0.1); | ||
| 168 | - border-radius: 16rpx; | ||
| 169 | - padding: 24rpx 30rpx; | 172 | + .menu-item { |
| 170 | display: flex; | 173 | display: flex; |
| 171 | align-items: center; | 174 | align-items: center; |
| 172 | - | ||
| 173 | gap: 24rpx; | 175 | gap: 24rpx; |
| 174 | -} | 176 | + padding: 24rpx 30rpx; |
| 177 | + border-radius: 16rpx; | ||
| 178 | + } | ||
| 175 | 179 | ||
| 176 | -/* 黄色图标容器 */ | ||
| 177 | -.icon-wrap { | 180 | + .menu-icon { |
| 178 | width: 80rpx; | 181 | width: 80rpx; |
| 179 | height: 80rpx; | 182 | height: 80rpx; |
| 180 | border-radius: 50%; | 183 | border-radius: 50%; |
| @@ -183,33 +186,29 @@ onMounted(() => { | @@ -183,33 +186,29 @@ onMounted(() => { | ||
| 183 | align-items: center; | 186 | align-items: center; |
| 184 | justify-content: center; | 187 | justify-content: center; |
| 185 | flex-shrink: 0; | 188 | flex-shrink: 0; |
| 186 | - | ||
| 187 | - image { | ||
| 188 | - width: 40rpx; | ||
| 189 | - height: 40rpx; | ||
| 190 | } | 189 | } |
| 191 | -} | ||
| 192 | 190 | ||
| 193 | -/* 文字区域 */ | ||
| 194 | -.text-wrap { | 191 | + .menu-text { |
| 195 | display: flex; | 192 | display: flex; |
| 196 | flex-direction: column; | 193 | flex-direction: column; |
| 197 | gap: 8rpx; | 194 | gap: 8rpx; |
| 198 | -} | ||
| 199 | 195 | ||
| 200 | -.option-title { | 196 | + .menu-title { |
| 201 | font-size: 32rpx; | 197 | font-size: 32rpx; |
| 202 | color: #fff; | 198 | color: #fff; |
| 203 | font-weight: 500; | 199 | font-weight: 500; |
| 204 | -} | 200 | + } |
| 205 | 201 | ||
| 206 | -.option-desc { | 202 | + .menu-desc { |
| 207 | font-size: 26rpx; | 203 | font-size: 26rpx; |
| 208 | color: rgba(255, 255, 255, 0.7); | 204 | color: rgba(255, 255, 255, 0.7); |
| 209 | -} | 205 | + } |
| 206 | + } | ||
| 207 | + } | ||
| 210 | 208 | ||
| 211 | -/* 底部关闭按钮 */ | ||
| 212 | -.close-btn { | 209 | + // 底部关闭按钮 |
| 210 | + .close-btn { | ||
| 211 | + margin-top: 60rpx; | ||
| 213 | width: 88rpx; | 212 | width: 88rpx; |
| 214 | height: 88rpx; | 213 | height: 88rpx; |
| 215 | border-radius: 50%; | 214 | border-radius: 50%; |
| @@ -217,11 +216,6 @@ onMounted(() => { | @@ -217,11 +216,6 @@ onMounted(() => { | ||
| 217 | display: flex; | 216 | display: flex; |
| 218 | align-items: center; | 217 | align-items: center; |
| 219 | justify-content: center; | 218 | justify-content: center; |
| 220 | - | ||
| 221 | - text { | ||
| 222 | - font-size: 40rpx; | ||
| 223 | - color: #000; | ||
| 224 | - line-height: 1; | ||
| 225 | } | 219 | } |
| 226 | } | 220 | } |
| 227 | </style> | 221 | </style> |
| @@ -4,12 +4,12 @@ | @@ -4,12 +4,12 @@ | ||
| 4 | <!-- 顶部日期栏 --> | 4 | <!-- 顶部日期栏 --> |
| 5 | <view class="header-bar"> | 5 | <view class="header-bar"> |
| 6 | <view class="close-btn" @click="closeCellPopup"> | 6 | <view class="close-btn" @click="closeCellPopup"> |
| 7 | - <uni-icons type="down" size="24" color="#333"></uni-icons> | 7 | + <u-icon name="arrow-down" size="15" color="#333"></u-icon> |
| 8 | </view> | 8 | </view> |
| 9 | <view class="date-wrapper"> | 9 | <view class="date-wrapper"> |
| 10 | <text class="date-text">{{ displayDate }}</text> | 10 | <text class="date-text">{{ displayDate }}</text> |
| 11 | </view> | 11 | </view> |
| 12 | - <button class="date-note-btn" @click.stop="handleDateNote">日期备注</button> | 12 | + <button class="date-note-btn" @click="handleDateNote">日期备注</button> |
| 13 | <button class="go-train-btn" @click.stop="startTraining"> | 13 | <button class="go-train-btn" @click.stop="startTraining"> |
| 14 | <text class="go-train-text">去训练</text> | 14 | <text class="go-train-text">去训练</text> |
| 15 | <text class="go-train-tag">GO</text> | 15 | <text class="go-train-tag">GO</text> |
| @@ -21,10 +21,8 @@ | @@ -21,10 +21,8 @@ | ||
| 21 | <view class="noteContent" v-if="noteList.length > 0"> | 21 | <view class="noteContent" v-if="noteList.length > 0"> |
| 22 | <text class="noteTitle">日程备注:</text> | 22 | <text class="noteTitle">日程备注:</text> |
| 23 | <view class="noteTags"> | 23 | <view class="noteTags"> |
| 24 | - <view class="noteTag" v-for="(item, idx) in noteList" :key="item.id" | ||
| 25 | - :style="{ backgroundColor: item.backgroundColor }" @click="handleEditNote(item)"> | ||
| 26 | - {{ item.content }} | ||
| 27 | - </view> | 24 | + <u-tag class="noteTag" v-for="(item, idx) in noteList" :key="item.id" :text="item.content" |
| 25 | + :bg-color="item.backgroundColor" color="#fff" shape="circle" @click="handleEditNote(item)" /> | ||
| 28 | </view> | 26 | </view> |
| 29 | </view> | 27 | </view> |
| 30 | 28 | ||
| @@ -54,13 +52,10 @@ | @@ -54,13 +52,10 @@ | ||
| 54 | {{ currentPlan?.totalWeight }}kg</text> | 52 | {{ currentPlan?.totalWeight }}kg</text> |
| 55 | </view> | 53 | </view> |
| 56 | <view class="plan-header-btns"> | 54 | <view class="plan-header-btns"> |
| 57 | - <button class="plan-btn more-btn" @click.stop="handlePlanMore">更多</button> | ||
| 58 | - <button class="plan-btn copy-btn" @click.stop="openCopyCalendarPopup(currentPlan?.templateId)"> | ||
| 59 | - 复制到 | ||
| 60 | - </button> | ||
| 61 | - <button class="plan-btn go-train-btn-small" @click.stop="handleTrainAgain"> | 55 | + <button class="plan-btn more-btn" @click="handlePlanMore">更多</button> |
| 56 | + <button class="plan-btn copy-btn" @click="openCopyCalendarPopup(currentPlan?.templateId)">复制到</button> | ||
| 57 | + <button class="plan-btn go-train-btn-small" @click="handleTrainAgain"> | ||
| 62 | <text class="go-train-text-sm">今日再练</text> | 58 | <text class="go-train-text-sm">今日再练</text> |
| 63 | - <!-- <text class="go-train-tag-sm">GO</text> --> | ||
| 64 | </button> | 59 | </button> |
| 65 | </view> | 60 | </view> |
| 66 | </view> | 61 | </view> |
| @@ -93,25 +88,8 @@ | @@ -93,25 +88,8 @@ | ||
| 93 | <view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx"> | 88 | <view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx"> |
| 94 | <!-- 新增:把左边所有内容包起来 --> | 89 | <!-- 新增:把左边所有内容包起来 --> |
| 95 | <view class="set-left" :style="{ color: set.isCompleted === 1 ? '#333' : '#a2a2a2' }"> | 90 | <view class="set-left" :style="{ color: set.isCompleted === 1 ? '#333' : '#a2a2a2' }"> |
| 96 | - <!-- <text class="indexData">{{ setIdx + 1 }}</text> --> | ||
| 97 | - <text class="set-content" v-if="unit.exercises[0].exerciseType === 0"> | ||
| 98 | - {{ set.weight }}kg x {{ set.reps }}次 | ||
| 99 | - </text> | ||
| 100 | - <text class="set-content" v-if="unit.exercises[0].exerciseType === 1"> | ||
| 101 | - {{ formatSecondsToHms(set.duration) }} x {{ set.distance }}km | ||
| 102 | - </text> | ||
| 103 | - <text class="set-content" v-if="unit.exercises[0].exerciseType === 2"> | ||
| 104 | - {{ set.reps }}次 | ||
| 105 | - </text> | ||
| 106 | - <text class="set-content" v-if="unit.exercises[0].exerciseType === 3"> | ||
| 107 | - {{ formatSecondsToHms(set.duration) }} | ||
| 108 | - </text> | ||
| 109 | - <text class="set-content" v-if="[4, 5].includes(unit.exercises[0].exerciseType)"> | ||
| 110 | - {{ set.weight }}kg x {{ set.duration }}s x {{ set.restTime }} | ||
| 111 | - </text> | ||
| 112 | - <text class="set-content" v-if="unit.exercises[0].exerciseType === 6"> | ||
| 113 | - {{ set.reps }}组 x {{ set.duration }}秒 x {{ set.restTime || 0 }}秒/组休息 | ||
| 114 | - </text> | 91 | + <text class="set-content">{{ getSetDisplayText(set, unit.exercises[0].exerciseType) |
| 92 | + }}</text> | ||
| 115 | </view> | 93 | </view> |
| 116 | <view class="set-right"> | 94 | <view class="set-right"> |
| 117 | <text class="rest-time" v-if="set.restTime && unit.exercises[0].exerciseType != 6"> | 95 | <text class="rest-time" v-if="set.restTime && unit.exercises[0].exerciseType != 6"> |
| @@ -127,7 +105,6 @@ | @@ -127,7 +105,6 @@ | ||
| 127 | <!-- 右侧:修改按钮 + 完成对勾 --> | 105 | <!-- 右侧:修改按钮 + 完成对勾 --> |
| 128 | <view class="action-right" @click="openXiuGai(unit, unitIdx)"> | 106 | <view class="action-right" @click="openXiuGai(unit, unitIdx)"> |
| 129 | <button class="modify-btn">修改</button> | 107 | <button class="modify-btn">修改</button> |
| 130 | - <!-- <view class="check-icon">✓</view> --> | ||
| 131 | </view> | 108 | </view> |
| 132 | </view> | 109 | </view> |
| 133 | <!-- 情况2:超级组 unitType=2 --> | 110 | <!-- 情况2:超级组 unitType=2 --> |
| @@ -168,28 +145,8 @@ | @@ -168,28 +145,8 @@ | ||
| 168 | 145 | ||
| 169 | <view class="set-content" | 146 | <view class="set-content" |
| 170 | :style="{ color: ex.sets[idx].isCompleted === 1 ? '#333' : '#a2a2a2' }"> | 147 | :style="{ color: ex.sets[idx].isCompleted === 1 ? '#333' : '#a2a2a2' }"> |
| 171 | - <!-- 适配不同动作类型的显示文本 --> | ||
| 172 | - <text v-if="ex.exerciseType === 0" class="detai-data"> | ||
| 173 | - {{ ex.sets[idx].weight }}kg x {{ ex.sets[idx].reps }}次 | ||
| 174 | - </text> | ||
| 175 | - <text v-if="ex.exerciseType === 1" class="detai-data"> | ||
| 176 | - {{ formatSecondsToHms(ex.sets[idx].duration) }} x {{ | ||
| 177 | - ex.sets[idx].distance | ||
| 178 | - }}km | ||
| 179 | - </text> | ||
| 180 | - <text v-if="ex.exerciseType === 2" class="detai-data"> | ||
| 181 | - {{ ex.sets[idx].reps }}次 | ||
| 182 | - </text> | ||
| 183 | - <text v-if="ex.exerciseType === 3" class="detai-data"> | ||
| 184 | - {{ formatSecondsToHms(ex.sets[idx].duration) }} | ||
| 185 | - </text> | ||
| 186 | - <text v-if="[4, 5].includes(ex.exerciseType)" class="detai-data"> | ||
| 187 | - {{ ex.sets[idx].weight }}kg x {{ ex.sets[idx].reps }}次 | ||
| 188 | - </text> | ||
| 189 | - <text v-if="ex.exerciseType === 6" class="detai-data"> | ||
| 190 | - {{ ex.sets[idx].reps }}次 x {{ ex.sets[idx].duration }}秒 x {{ ex.sets[idx].restTime | ||
| 191 | - }}秒/组休息 | ||
| 192 | - </text> | 148 | + <text class="detai-data">{{ getSuperSetDisplayText(ex.sets[idx], ex.exerciseType) |
| 149 | + }}</text> | ||
| 193 | <view class="rest-time" v-if="ex.sets[idx]?.restTime && ex.exerciseType !== 6" | 150 | <view class="rest-time" v-if="ex.sets[idx]?.restTime && ex.exerciseType !== 6" |
| 194 | :style="{ color: ex.sets[idx].isCompleted === 1 ? '#999 !important' : '#a2a2a2 !important' }"> | 151 | :style="{ color: ex.sets[idx].isCompleted === 1 ? '#999 !important' : '#a2a2a2 !important' }"> |
| 195 | {{ ex.sets[idx].restTime }}s | 152 | {{ ex.sets[idx].restTime }}s |
| @@ -212,21 +169,13 @@ | @@ -212,21 +169,13 @@ | ||
| 212 | <!-- 空状态区域 --> | 169 | <!-- 空状态区域 --> |
| 213 | <view v-else class="empty-section"> | 170 | <view v-else class="empty-section"> |
| 214 | <image class="empty-img" | 171 | <image class="empty-img" |
| 215 | - src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" | 172 | + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/空状态自助_1784875704380.png" |
| 216 | mode="aspectFit"> | 173 | mode="aspectFit"> |
| 217 | </image> | 174 | </image> |
| 218 | <text class="empty-tip">今天没有安排</text> | 175 | <text class="empty-tip">今天没有安排</text> |
| 219 | <button class="add-train-btn" @click="openAddTrainPopup">+ 添加自助训练</button> | 176 | <button class="add-train-btn" @click="openAddTrainPopup">+ 添加自助训练</button> |
| 220 | 177 | ||
| 221 | - <!-- 底部课程类型卡片 --> | ||
| 222 | - <!-- <view class="course-section"> | ||
| 223 | - <view class="course-item" v-for="(item, index) in courseList" :key="index" | ||
| 224 | - @click="handleCourseClick(item.type)"> | ||
| 225 | - <image class="course-icon" :src="item.icon" mode="aspectFill"></image> | ||
| 226 | - <text class="course-title">{{ item.title }}</text> | ||
| 227 | - <text class="course-desc">{{ item.desc }}</text> | ||
| 228 | - </view> | ||
| 229 | - </view> --> | 178 | + |
| 230 | </view> | 179 | </view> |
| 231 | 180 | ||
| 232 | <AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" /> | 181 | <AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" /> |
| @@ -298,59 +247,44 @@ import CalendarColorPicker from '@/pages/xunji/components/rili-components/Calend | @@ -298,59 +247,44 @@ import CalendarColorPicker from '@/pages/xunji/components/rili-components/Calend | ||
| 298 | import { useTrainingStore } from '@/sheep/store/trainingStore' | 247 | import { useTrainingStore } from '@/sheep/store/trainingStore' |
| 299 | import MeiRiMoBanXiuGai from '../rili-components/meiri-moban-xiugai.vue' | 248 | import MeiRiMoBanXiuGai from '../rili-components/meiri-moban-xiugai.vue' |
| 300 | 249 | ||
| 250 | +// ===== Props & Emits ===== | ||
| 301 | const props = defineProps({ | 251 | const props = defineProps({ |
| 302 | date: { type: String, default: '' }, | 252 | date: { type: String, default: '' }, |
| 303 | - // noteList: { | ||
| 304 | - // type: Array, | ||
| 305 | - // default: () => [] | ||
| 306 | - // } | ||
| 307 | }) | 253 | }) |
| 308 | 254 | ||
| 309 | -const show = ref(false); | ||
| 310 | const emit = defineEmits(['close', 'refreshCalendar']); | 255 | const emit = defineEmits(['close', 'refreshCalendar']); |
| 311 | -const noteList = ref([]); | 256 | + |
| 257 | +// ===== 响应式状态 ===== | ||
| 258 | +const show = ref(false); | ||
| 312 | const showAddTrainPopup = ref(false); | 259 | const showAddTrainPopup = ref(false); |
| 313 | const selectedDate = ref(''); | 260 | const selectedDate = ref(''); |
| 314 | const displayDate = ref(''); | 261 | const displayDate = ref(''); |
| 315 | const resdailyData = ref([]); | 262 | const resdailyData = ref([]); |
| 316 | -// const noteList = ref([]) | ||
| 317 | const currentPlanIndex = ref(0); | 263 | const currentPlanIndex = ref(0); |
| 264 | +const noteList = ref([]); | ||
| 265 | +const noteHistoryList = ref([]); | ||
| 318 | 266 | ||
| 267 | +// 弹窗相关 | ||
| 319 | const isMoveMode = ref(false); | 268 | const isMoveMode = ref(false); |
| 320 | const isCopyMode = ref(false); | 269 | const isCopyMode = ref(false); |
| 321 | -const showRiqibeizhu = ref(false) | ||
| 322 | -const moreShow = ref(false) | 270 | +const showRiqibeizhu = ref(false); |
| 271 | +const moreShow = ref(false); | ||
| 272 | +const showColorPopup = ref(false); | ||
| 323 | const calendarPopupRef = ref(null); | 273 | const calendarPopupRef = ref(null); |
| 324 | -const currentEditId = ref(null); // 新增这一行 | ||
| 325 | -const noteHistoryList = ref([]); | 274 | +const currentEditId = ref(null); |
| 275 | +const selectedPlanId = ref(null); | ||
| 326 | 276 | ||
| 327 | -const showColorPopup = ref(false) | ||
| 328 | -const selectedPlanId = ref(null) | 277 | +// Store / 子组件引用 |
| 278 | +const trainingStore = useTrainingStore(); | ||
| 279 | +const meirimobanRef = ref(null); | ||
| 329 | 280 | ||
| 330 | -const trainingStore = useTrainingStore() | 281 | +// 修改单元缓存 |
| 282 | +const editUnitInfo = ref({ | ||
| 283 | + unit: null, | ||
| 284 | + unitIndex: null, | ||
| 285 | + dailyTemplate: null, | ||
| 286 | +}); | ||
| 331 | 287 | ||
| 332 | -const meirimobanRef = ref(null) | ||
| 333 | -// 没有训练模板时推荐课程 | ||
| 334 | -const courseList = ref([ | ||
| 335 | - { | ||
| 336 | - type: 'group', | ||
| 337 | - title: '团课', | ||
| 338 | - desc: '大家一起练', | ||
| 339 | - icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/group-course.png' | ||
| 340 | - }, | ||
| 341 | - { | ||
| 342 | - type: 'private', | ||
| 343 | - title: '私教', | ||
| 344 | - desc: '1对1专人指导', | ||
| 345 | - icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/private-course.png' | ||
| 346 | - }, | ||
| 347 | - { | ||
| 348 | - type: 'smallClass', | ||
| 349 | - title: '小班课', | ||
| 350 | - desc: '28天极速瘦身', | ||
| 351 | - icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/small-class.png' | ||
| 352 | - } | ||
| 353 | -]); | ||
| 354 | 288 | ||
| 355 | // 打开弹窗 | 289 | // 打开弹窗 |
| 356 | const openPopup = async () => { | 290 | const openPopup = async () => { |
| @@ -370,14 +304,7 @@ defineExpose({ | @@ -370,14 +304,7 @@ defineExpose({ | ||
| 370 | closeCellPopup | 304 | closeCellPopup |
| 371 | }); | 305 | }); |
| 372 | 306 | ||
| 373 | -// 修改 | ||
| 374 | -// 定义缓存变量,传给弹窗 | ||
| 375 | -const editUnitInfo = ref({ | ||
| 376 | - unit: null, | ||
| 377 | - unitIndex: null, | ||
| 378 | - dailyTemplate: null // currentPlan(整个每日模板对象,含id、templateId) | ||
| 379 | -}) | ||
| 380 | -// 打开弹窗修改单个unit | 307 | +// ===== 弹窗:打开修改单个 unit ===== |
| 381 | const openXiuGai = (unit, idx) => { | 308 | const openXiuGai = (unit, idx) => { |
| 382 | editUnitInfo.value = { | 309 | editUnitInfo.value = { |
| 383 | unit: JSON.parse(JSON.stringify(unit)), // 深拷贝防止原数据被实时篡改 | 310 | unit: JSON.parse(JSON.stringify(unit)), // 深拷贝防止原数据被实时篡改 |
| @@ -410,12 +337,14 @@ const handleAddTrain = async () => { | @@ -410,12 +337,14 @@ const handleAddTrain = async () => { | ||
| 410 | // 更新主页面 | 337 | // 更新主页面 |
| 411 | emit('refreshCalendar'); | 338 | emit('refreshCalendar'); |
| 412 | } | 339 | } |
| 340 | +// ===== 计算属性 ===== | ||
| 413 | // 当前正在显示的训记 | 341 | // 当前正在显示的训记 |
| 414 | const currentPlan = computed(() => { | 342 | const currentPlan = computed(() => { |
| 415 | if (!resdailyData.value.length) return null; | 343 | if (!resdailyData.value.length) return null; |
| 416 | return resdailyData.value[currentPlanIndex.value] || null; | 344 | return resdailyData.value[currentPlanIndex.value] || null; |
| 417 | }); | 345 | }); |
| 418 | 346 | ||
| 347 | +// ===== API:加载每日模板数据 ===== | ||
| 419 | const loaddailytemplate = async () => { | 348 | const loaddailytemplate = async () => { |
| 420 | if (!selectedDate.value) return; | 349 | if (!selectedDate.value) return; |
| 421 | console.log('【子组件】开始加载数据...'); | 350 | console.log('【子组件】开始加载数据...'); |
| @@ -458,7 +387,7 @@ const switchPlan = (index) => { | @@ -458,7 +387,7 @@ const switchPlan = (index) => { | ||
| 458 | console.log('【切换标签后 - currentPlan】', currentPlan.value); | 387 | console.log('【切换标签后 - currentPlan】', currentPlan.value); |
| 459 | }; | 388 | }; |
| 460 | 389 | ||
| 461 | -// 更多弹窗控制 | 390 | +// ===== 更多弹窗:操作项 ===== |
| 462 | const handlePlanMore = () => { | 391 | const handlePlanMore = () => { |
| 463 | if (!currentPlan.value) { // 加这个 | 392 | if (!currentPlan.value) { // 加这个 |
| 464 | return | 393 | return |
| @@ -472,6 +401,13 @@ const closeMorePopup = () => { | @@ -472,6 +401,13 @@ const closeMorePopup = () => { | ||
| 472 | 401 | ||
| 473 | // 一键打勾(完成训练) | 402 | // 一键打勾(完成训练) |
| 474 | const handleCompleteCheck = async () => { | 403 | const handleCompleteCheck = async () => { |
| 404 | + // 日期校验:只能对今天及以前的日期一键打勾 | ||
| 405 | + const today = dayjs().format('YYYY-MM-DD'); | ||
| 406 | + if (selectedDate.value > today) { | ||
| 407 | + uni.showToast({ title: '一键打勾只能用于今天及以前的日期', icon: 'none' }); | ||
| 408 | + return; | ||
| 409 | + } | ||
| 410 | + | ||
| 475 | if (!currentPlan.value?.id) { | 411 | if (!currentPlan.value?.id) { |
| 476 | uni.showToast({ title: '未找到训练ID', icon: 'none' }) | 412 | uni.showToast({ title: '未找到训练ID', icon: 'none' }) |
| 477 | return | 413 | return |
| @@ -494,13 +430,17 @@ const handleCompleteCheck = async () => { | @@ -494,13 +430,17 @@ const handleCompleteCheck = async () => { | ||
| 494 | } | 430 | } |
| 495 | } | 431 | } |
| 496 | 432 | ||
| 497 | -// 保存到我的模板 | ||
| 498 | -// const saveToMyTemplate = (dailyTemplate) => { | ||
| 499 | -// console.log('dailyTemplate', dailyTemplate); | ||
| 500 | - | ||
| 501 | -// } | ||
| 502 | - | 433 | +// ===== 保存训练为个人模板 ===== |
| 503 | const openSaveTemplateDialog = async (item) => { | 434 | const openSaveTemplateDialog = async (item) => { |
| 435 | + // 关闭所有可能遮挡的弹窗 | ||
| 436 | + moreShow.value = false; | ||
| 437 | + showColorPopup.value = false; | ||
| 438 | + showAddTrainPopup.value = false; | ||
| 439 | + showRiqibeizhu.value = false; | ||
| 440 | + show.value = false | ||
| 441 | + // 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭 | ||
| 442 | + await nextTick(); | ||
| 443 | + | ||
| 504 | // uni弹窗输入框:只填模板名称 | 444 | // uni弹窗输入框:只填模板名称 |
| 505 | uni.showModal({ | 445 | uni.showModal({ |
| 506 | title: '确认保存为你的训练模板?', | 446 | title: '确认保存为你的训练模板?', |
| @@ -520,7 +460,6 @@ const openSaveTemplateDialog = async (item) => { | @@ -520,7 +460,6 @@ const openSaveTemplateDialog = async (item) => { | ||
| 520 | // 发起创建接口 | 460 | // 发起创建接口 |
| 521 | console.log('reqData', reqData); | 461 | console.log('reqData', reqData); |
| 522 | await submitCreateTemplate(reqData) | 462 | await submitCreateTemplate(reqData) |
| 523 | - moreShow.value = false; | ||
| 524 | } | 463 | } |
| 525 | } | 464 | } |
| 526 | }) | 465 | }) |
| @@ -580,25 +519,42 @@ const closeAddTrainPopup = () => { | @@ -580,25 +519,42 @@ const closeAddTrainPopup = () => { | ||
| 580 | showAddTrainPopup.value = false; | 519 | showAddTrainPopup.value = false; |
| 581 | }; | 520 | }; |
| 582 | 521 | ||
| 583 | -// 弹窗选项点击事件 | ||
| 584 | -// const handleAddFromPlan = () => { | ||
| 585 | -// uni.showToast({ title: '从训练计划添加', icon: 'none' }); | ||
| 586 | -// closeAddTrainPopup(); | ||
| 587 | -// }; | ||
| 588 | -// const handleAddFromTemplate = () => { | ||
| 589 | -// uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' }); | ||
| 590 | -// closeAddTrainPopup(); | ||
| 591 | -// }; | ||
| 592 | -// const handleFreeTraining = () => { | ||
| 593 | -// uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' }); | ||
| 594 | -// closeAddTrainPopup(); | ||
| 595 | -// }; | ||
| 596 | - | 522 | +// ===== 工具函数 ===== |
| 597 | const formatSecondsToHms = (seconds) => { | 523 | const formatSecondsToHms = (seconds) => { |
| 598 | if (!seconds || isNaN(seconds)) return '00:00:00'; | 524 | if (!seconds || isNaN(seconds)) return '00:00:00'; |
| 599 | return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss'); | 525 | return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss'); |
| 600 | } | 526 | } |
| 601 | 527 | ||
| 528 | +// ===== 工具函数:根据动作类型生成组次显示文本 ===== | ||
| 529 | +const getSetDisplayText = (set, exerciseType) => { | ||
| 530 | + if (!set) return ''; | ||
| 531 | + switch (exerciseType) { | ||
| 532 | + case 0: return `${set.weight || 0}kg x ${set.reps || 0}次`; | ||
| 533 | + case 1: return `${formatSecondsToHms(set.duration)} x ${set.distance || 0}km`; | ||
| 534 | + case 2: return `${set.reps || 0}次`; | ||
| 535 | + case 3: return formatSecondsToHms(set.duration); | ||
| 536 | + case 4: | ||
| 537 | + case 5: return `${set.weight || 0}kg x ${set.duration || 0}s x ${set.restTime || 0}`; | ||
| 538 | + case 6: return `${set.reps || 0}组 x ${set.duration || 0}秒 x ${set.restTime || 0}秒/组休息`; | ||
| 539 | + default: return ''; | ||
| 540 | + } | ||
| 541 | +}; | ||
| 542 | + | ||
| 543 | +// 超级组中的动作显示(与普通动作在 type 4/5 上的展示略有差异) | ||
| 544 | +const getSuperSetDisplayText = (set, exerciseType) => { | ||
| 545 | + if (!set) return ''; | ||
| 546 | + switch (exerciseType) { | ||
| 547 | + case 0: return `${set.weight || 0}kg x ${set.reps || 0}次`; | ||
| 548 | + case 1: return `${formatSecondsToHms(set.duration)} x ${set.distance || 0}km`; | ||
| 549 | + case 2: return `${set.reps || 0}次`; | ||
| 550 | + case 3: return formatSecondsToHms(set.duration); | ||
| 551 | + case 4: | ||
| 552 | + case 5: return `${set.weight || 0}kg x ${set.reps || 0}次`; | ||
| 553 | + case 6: return `${set.reps || 0}次 x ${set.duration || 0}秒 x ${set.restTime || 0}秒/组休息`; | ||
| 554 | + default: return ''; | ||
| 555 | + } | ||
| 556 | +}; | ||
| 557 | + | ||
| 602 | // 返回按钮逻辑 | 558 | // 返回按钮逻辑 |
| 603 | const goBack = () => { | 559 | const goBack = () => { |
| 604 | uni.navigateBack({ | 560 | uni.navigateBack({ |
| @@ -609,19 +565,15 @@ const goBack = () => { | @@ -609,19 +565,15 @@ const goBack = () => { | ||
| 609 | }); | 565 | }); |
| 610 | }; | 566 | }; |
| 611 | 567 | ||
| 612 | -// 日期格式化(兼容iOS) | 568 | +// 日期格式化(使用 dayjs 处理,兼容性更好) |
| 613 | const formatDateWithWeek = (dateStr) => { | 569 | const formatDateWithWeek = (dateStr) => { |
| 614 | if (!dateStr) return ''; | 570 | if (!dateStr) return ''; |
| 571 | + const d = dayjs(dateStr); | ||
| 615 | const weekMap = ['日', '一', '二', '三', '四', '五', '六']; | 572 | const weekMap = ['日', '一', '二', '三', '四', '五', '六']; |
| 616 | - const iosSafeDate = new Date(dateStr.replace(/-/g, '/')); | ||
| 617 | - const year = iosSafeDate.getFullYear(); | ||
| 618 | - const month = String(iosSafeDate.getMonth() + 1).padStart(2, '0'); | ||
| 619 | - const day = String(iosSafeDate.getDate()).padStart(2, '0'); | ||
| 620 | - const week = weekMap[iosSafeDate.getDay()]; | ||
| 621 | - return `${year}/${month}/${day} 周${week}`; | 573 | + return `${d.format('YYYY/MM/DD')} 周${weekMap[d.day()]}`; |
| 622 | }; | 574 | }; |
| 623 | 575 | ||
| 624 | -// 打开日期备注组件 | 576 | +// ===== 日期备注相关 ===== |
| 625 | const handleDateNote = () => { | 577 | const handleDateNote = () => { |
| 626 | if (noteList.value.length >= 5) { | 578 | if (noteList.value.length >= 5) { |
| 627 | uni.showToast({ | 579 | uni.showToast({ |
| @@ -648,7 +600,7 @@ const handleCloseNotePopup = () => { | @@ -648,7 +600,7 @@ const handleCloseNotePopup = () => { | ||
| 648 | currentEditId.value = null; | 600 | currentEditId.value = null; |
| 649 | }; | 601 | }; |
| 650 | 602 | ||
| 651 | -// 打开「添加到日历」弹窗 | 603 | +// ===== 复制到 / 移动到 ===== |
| 652 | const openCalendarPopup = (templateId) => { | 604 | const openCalendarPopup = (templateId) => { |
| 653 | console.log('点击了添加到日历按钮', templateId); // 新增日志 | 605 | console.log('点击了添加到日历按钮', templateId); // 新增日志 |
| 654 | console.log('子组件实例', calendarPopupRef.value); // 新增日志 | 606 | console.log('子组件实例', calendarPopupRef.value); // 新增日志 |
| @@ -695,6 +647,14 @@ const handleDeleteTemplate = async () => { | @@ -695,6 +647,14 @@ const handleDeleteTemplate = async () => { | ||
| 695 | uni.showToast({ title: '未找到模板ID', icon: 'none' }); | 647 | uni.showToast({ title: '未找到模板ID', icon: 'none' }); |
| 696 | return; | 648 | return; |
| 697 | } | 649 | } |
| 650 | + // 关闭所有可能遮挡的弹窗 | ||
| 651 | + moreShow.value = false; | ||
| 652 | + showColorPopup.value = false; | ||
| 653 | + showAddTrainPopup.value = false; | ||
| 654 | + showRiqibeizhu.value = false; | ||
| 655 | + show.value = false | ||
| 656 | + // 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭 | ||
| 657 | + await nextTick(); | ||
| 698 | // 弹出确认框 | 658 | // 弹出确认框 |
| 699 | uni.showModal({ | 659 | uni.showModal({ |
| 700 | title: '确认删除', | 660 | title: '确认删除', |
| @@ -820,7 +780,7 @@ const calendarColorPickerSuccess = () => { | @@ -820,7 +780,7 @@ const calendarColorPickerSuccess = () => { | ||
| 820 | emit('refreshCalendar') | 780 | emit('refreshCalendar') |
| 821 | } | 781 | } |
| 822 | 782 | ||
| 823 | -// 去训练 | 783 | +// ===== 去训练 / 每日模板编辑 ===== |
| 824 | const startTraining = () => { | 784 | const startTraining = () => { |
| 825 | 785 | ||
| 826 | if (trainingStore.isTraining) { | 786 | if (trainingStore.isTraining) { |
| @@ -861,6 +821,7 @@ const templateEdit = (plan) => { | @@ -861,6 +821,7 @@ const templateEdit = (plan) => { | ||
| 861 | console.log('开始进入编辑模板页面,模板ID:', plan.templateId, '每日模板ID:', plan.id); | 821 | console.log('开始进入编辑模板页面,模板ID:', plan.templateId, '每日模板ID:', plan.id); |
| 862 | }; | 822 | }; |
| 863 | 823 | ||
| 824 | +// ===== 监听 & 生命周期 ===== | ||
| 864 | // 监听父组件传进来的 date 变化 | 825 | // 监听父组件传进来的 date 变化 |
| 865 | watch( | 826 | watch( |
| 866 | () => props.date, | 827 | () => props.date, |
| @@ -888,6 +849,19 @@ onMounted(() => { | @@ -888,6 +849,19 @@ onMounted(() => { | ||
| 888 | </script> | 849 | </script> |
| 889 | 850 | ||
| 890 | <style lang="scss" scoped> | 851 | <style lang="scss" scoped> |
| 852 | +// ===== 公共变量 ===== | ||
| 853 | +$color-primary: #333; | ||
| 854 | +$color-accent: #ffc107; | ||
| 855 | +$color-bg: #f2f3f5; | ||
| 856 | +$color-white: #fff; | ||
| 857 | +$color-danger: #ff4d4f; | ||
| 858 | +$color-muted: #a2a2a2; | ||
| 859 | +$color-card-bg: #f9f9f9; | ||
| 860 | +$radius-sm: 8rpx; | ||
| 861 | +$radius-md: 12rpx; | ||
| 862 | +$radius-lg: 16rpx; | ||
| 863 | +$radius-round: 40rpx; | ||
| 864 | + | ||
| 891 | /* 全局容器 */ | 865 | /* 全局容器 */ |
| 892 | .container { | 866 | .container { |
| 893 | display: flex; | 867 | display: flex; |
| @@ -941,9 +915,14 @@ onMounted(() => { | @@ -941,9 +915,14 @@ onMounted(() => { | ||
| 941 | 915 | ||
| 942 | .date-text { | 916 | .date-text { |
| 943 | font-size: 28rpx; | 917 | font-size: 28rpx; |
| 918 | + white-space: nowrap; | ||
| 919 | + overflow: hidden; | ||
| 920 | + text-overflow: ellipsis; | ||
| 944 | } | 921 | } |
| 945 | 922 | ||
| 946 | .date-note-btn { | 923 | .date-note-btn { |
| 924 | + display: inline-flex; | ||
| 925 | + align-items: center; | ||
| 947 | font-size: 28rpx; | 926 | font-size: 28rpx; |
| 948 | color: #333; | 927 | color: #333; |
| 949 | background: #e7e7e9; | 928 | background: #e7e7e9; |
| @@ -1066,11 +1045,11 @@ onMounted(() => { | @@ -1066,11 +1045,11 @@ onMounted(() => { | ||
| 1066 | } | 1045 | } |
| 1067 | 1046 | ||
| 1068 | .go-train-btn-small { | 1047 | .go-train-btn-small { |
| 1069 | - // background: #ffc107; | ||
| 1070 | color: #000; | 1048 | color: #000; |
| 1071 | display: flex; | 1049 | display: flex; |
| 1072 | align-items: center; | 1050 | align-items: center; |
| 1073 | padding: 12rpx 20rpx; | 1051 | padding: 12rpx 20rpx; |
| 1052 | + background: inherit; | ||
| 1074 | } | 1053 | } |
| 1075 | 1054 | ||
| 1076 | .go-train-text-sm { | 1055 | .go-train-text-sm { |
| @@ -1286,34 +1265,34 @@ onMounted(() => { | @@ -1286,34 +1265,34 @@ onMounted(() => { | ||
| 1286 | flex-direction: column; | 1265 | flex-direction: column; |
| 1287 | align-items: center; | 1266 | align-items: center; |
| 1288 | justify-content: center; | 1267 | justify-content: center; |
| 1289 | - // width: 500rpx; | ||
| 1290 | - height: 500rpx; | ||
| 1291 | - margin-top: 100rpx; | ||
| 1292 | - padding: 20rpx 20rpx 60rpx; | 1268 | + padding: 80rpx 40rpx 120rpx; |
| 1293 | box-sizing: border-box; | 1269 | box-sizing: border-box; |
| 1294 | - | ||
| 1295 | } | 1270 | } |
| 1296 | 1271 | ||
| 1297 | .empty-img { | 1272 | .empty-img { |
| 1298 | - width: 280rpx; | ||
| 1299 | - height: 280rpx; | ||
| 1300 | - margin-bottom: 30rpx; | 1273 | + width: 360rpx; |
| 1274 | + height: 360rpx; | ||
| 1275 | + margin-bottom: 40rpx; | ||
| 1276 | + transform: scale(3); | ||
| 1301 | } | 1277 | } |
| 1302 | 1278 | ||
| 1303 | .empty-tip { | 1279 | .empty-tip { |
| 1304 | font-size: 32rpx; | 1280 | font-size: 32rpx; |
| 1305 | color: #666; | 1281 | color: #666; |
| 1306 | - margin-bottom: 60rpx; | 1282 | + font-weight: 500; |
| 1283 | + margin-bottom: 80rpx; | ||
| 1284 | + letter-spacing: 2rpx; | ||
| 1307 | } | 1285 | } |
| 1308 | 1286 | ||
| 1309 | .add-train-btn { | 1287 | .add-train-btn { |
| 1310 | font-size: 30rpx; | 1288 | font-size: 30rpx; |
| 1311 | color: #000; | 1289 | color: #000; |
| 1312 | - background: #ffc107; | ||
| 1313 | - border-radius: 40rpx; | ||
| 1314 | - padding: 20rpx 80rpx; | 1290 | + background: $color-accent; |
| 1291 | + border-radius: $radius-round; | ||
| 1292 | + padding: 24rpx 80rpx; | ||
| 1315 | border: none; | 1293 | border: none; |
| 1316 | - margin-bottom: 60rpx; | 1294 | + font-weight: 500; |
| 1295 | + box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3); | ||
| 1317 | } | 1296 | } |
| 1318 | 1297 | ||
| 1319 | /* 底部课程类型区域 */ | 1298 | /* 底部课程类型区域 */ |
| @@ -1485,6 +1464,7 @@ button::after { | @@ -1485,6 +1464,7 @@ button::after { | ||
| 1485 | border: none; | 1464 | border: none; |
| 1486 | } | 1465 | } |
| 1487 | 1466 | ||
| 1467 | + | ||
| 1488 | /* 训记切换标签 */ | 1468 | /* 训记切换标签 */ |
| 1489 | .plan-tabs { | 1469 | .plan-tabs { |
| 1490 | white-space: nowrap; | 1470 | white-space: nowrap; |
| @@ -1512,93 +1492,87 @@ button::after { | @@ -1512,93 +1492,87 @@ button::after { | ||
| 1512 | color: #fff; | 1492 | color: #fff; |
| 1513 | } | 1493 | } |
| 1514 | 1494 | ||
| 1515 | -/* ========== 超级组样式(已修复 无重复 无嵌套错误)========== */ | 1495 | +/* ========== 超级组样式 ========== */ |
| 1516 | .superset-wrapper { | 1496 | .superset-wrapper { |
| 1517 | margin-bottom: 20rpx; | 1497 | margin-bottom: 20rpx; |
| 1518 | - background: #fff; | ||
| 1519 | - border-radius: 12rpx; | 1498 | + background: $color-white; |
| 1499 | + border-radius: $radius-md; | ||
| 1520 | overflow: hidden; | 1500 | overflow: hidden; |
| 1521 | -} | ||
| 1522 | 1501 | ||
| 1523 | -.superset-wrapper .super-head { | 1502 | + .super-head { |
| 1524 | display: flex; | 1503 | display: flex; |
| 1525 | align-items: center; | 1504 | align-items: center; |
| 1526 | padding: 24rpx 20rpx; | 1505 | padding: 24rpx 20rpx; |
| 1527 | position: relative; | 1506 | position: relative; |
| 1528 | gap: 16rpx; | 1507 | gap: 16rpx; |
| 1529 | -} | 1508 | + } |
| 1530 | 1509 | ||
| 1531 | -.superset-wrapper .super-image { | 1510 | + .super-image { |
| 1532 | width: 75rpx; | 1511 | width: 75rpx; |
| 1533 | height: 75rpx; | 1512 | height: 75rpx; |
| 1534 | flex-shrink: 0; | 1513 | flex-shrink: 0; |
| 1535 | -} | 1514 | + } |
| 1536 | 1515 | ||
| 1537 | -.superset-wrapper .super-title-wrap { | 1516 | + .super-title-wrap { |
| 1538 | flex: 1; | 1517 | flex: 1; |
| 1539 | display: flex; | 1518 | display: flex; |
| 1540 | flex-direction: column; | 1519 | flex-direction: column; |
| 1541 | gap: 8rpx; | 1520 | gap: 8rpx; |
| 1542 | -} | 1521 | + } |
| 1543 | 1522 | ||
| 1544 | -.superset-wrapper .super-title { | 1523 | + .super-title { |
| 1545 | font-size: 32rpx; | 1524 | font-size: 32rpx; |
| 1546 | color: #111; | 1525 | color: #111; |
| 1547 | font-weight: 500; | 1526 | font-weight: 500; |
| 1548 | -} | 1527 | + } |
| 1549 | 1528 | ||
| 1550 | -.superset-wrapper .super-totalWeight { | 1529 | + .super-totalWeight { |
| 1551 | font-size: 24rpx; | 1530 | font-size: 24rpx; |
| 1552 | color: #666; | 1531 | color: #666; |
| 1553 | -} | 1532 | + } |
| 1554 | 1533 | ||
| 1555 | -.superset-wrapper .super-actionName { | 1534 | + .super-actionName { |
| 1556 | padding: 0 20rpx 20rpx 70rpx; | 1535 | padding: 0 20rpx 20rpx 70rpx; |
| 1557 | display: flex; | 1536 | display: flex; |
| 1558 | flex-direction: column; | 1537 | flex-direction: column; |
| 1559 | gap: 12rpx; | 1538 | gap: 12rpx; |
| 1560 | -} | ||
| 1561 | 1539 | ||
| 1562 | -.superset-wrapper .name-item { | 1540 | + .action-name { |
| 1541 | + font-size: 28rpx; | ||
| 1542 | + color: $color-primary; | ||
| 1543 | + } | ||
| 1544 | + } | ||
| 1545 | + | ||
| 1546 | + .name-item { | ||
| 1563 | display: flex; | 1547 | display: flex; |
| 1564 | align-items: center; | 1548 | align-items: center; |
| 1565 | gap: 12rpx; | 1549 | gap: 12rpx; |
| 1566 | -} | 1550 | + } |
| 1567 | 1551 | ||
| 1568 | -.superset-wrapper .letter { | 1552 | + .letter { |
| 1569 | font-size: 28rpx; | 1553 | font-size: 28rpx; |
| 1570 | font-weight: bold; | 1554 | font-weight: bold; |
| 1571 | - color: #333; | ||
| 1572 | -} | ||
| 1573 | - | ||
| 1574 | -.superset-wrapper .super-actionName .action-name { | ||
| 1575 | - font-size: 28rpx; | ||
| 1576 | - color: #333; | ||
| 1577 | -} | 1555 | + color: $color-primary; |
| 1556 | + } | ||
| 1578 | 1557 | ||
| 1579 | -.superset-wrapper .super-set-list { | 1558 | + .super-set-list { |
| 1580 | padding: 0 20rpx 20rpx 20rpx; | 1559 | padding: 0 20rpx 20rpx 20rpx; |
| 1581 | display: flex; | 1560 | display: flex; |
| 1582 | flex-direction: column; | 1561 | flex-direction: column; |
| 1583 | gap: 12rpx; | 1562 | gap: 12rpx; |
| 1584 | -} | 1563 | + } |
| 1585 | 1564 | ||
| 1586 | -.superset-wrapper .super-set-item { | 1565 | + .super-set-item { |
| 1587 | display: flex; | 1566 | display: flex; |
| 1588 | - // flex-direction: column; // 新增:垂直排列 | ||
| 1589 | - align-items: stretch; // 让每个动作占满宽度 | ||
| 1590 | - gap: 12rpx; // 垂直间距(可根据需要微调) | 1567 | + align-items: stretch; |
| 1568 | + gap: 12rpx; | ||
| 1591 | padding: 16rpx; | 1569 | padding: 16rpx; |
| 1592 | background: #f9f9f9; | 1570 | background: #f9f9f9; |
| 1593 | border-radius: 12rpx; | 1571 | border-radius: 12rpx; |
| 1594 | -} | ||
| 1595 | - | ||
| 1596 | -// 为了让序号独立占一行 | ||
| 1597 | -.superset-wrapper .super-set-item { | ||
| 1598 | margin-bottom: 8rpx; | 1572 | margin-bottom: 8rpx; |
| 1599 | -} | 1573 | + } |
| 1600 | 1574 | ||
| 1601 | -.superset-wrapper .set-number { | 1575 | + .set-number { |
| 1602 | font-size: 26rpx; | 1576 | font-size: 26rpx; |
| 1603 | background: #e4e4e4; | 1577 | background: #e4e4e4; |
| 1604 | color: #282828; | 1578 | color: #282828; |
| @@ -1608,27 +1582,27 @@ button::after { | @@ -1608,27 +1582,27 @@ button::after { | ||
| 1608 | flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */ | 1582 | flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */ |
| 1609 | border-radius: 50%; | 1583 | border-radius: 50%; |
| 1610 | // line-height: 60rpx; | 1584 | // line-height: 60rpx; |
| 1611 | -} | 1585 | + } |
| 1612 | 1586 | ||
| 1613 | -.superset-wrapper .set-column { | 1587 | + .set-column { |
| 1614 | display: flex; | 1588 | display: flex; |
| 1615 | flex-direction: column; | 1589 | flex-direction: column; |
| 1616 | -} | 1590 | + } |
| 1617 | 1591 | ||
| 1618 | -.superset-wrapper .action-in-set { | 1592 | + .action-in-set { |
| 1619 | // display: flex; | 1593 | // display: flex; |
| 1620 | // flex-direction: column; | 1594 | // flex-direction: column; |
| 1621 | width: 100%; | 1595 | width: 100%; |
| 1622 | display: flex; | 1596 | display: flex; |
| 1623 | align-items: center; | 1597 | align-items: center; |
| 1624 | margin-bottom: 8rpx; | 1598 | margin-bottom: 8rpx; |
| 1625 | -} | 1599 | + } |
| 1626 | 1600 | ||
| 1627 | -.superset-wrapper .action-in-set:last-child { | 1601 | + .action-in-set:last-child { |
| 1628 | margin-bottom: 0; | 1602 | margin-bottom: 0; |
| 1629 | -} | 1603 | + } |
| 1630 | 1604 | ||
| 1631 | -.superset-wrapper .action-letter { | 1605 | + .action-letter { |
| 1632 | display: inline-block; | 1606 | display: inline-block; |
| 1633 | font-size: 26rpx; | 1607 | font-size: 26rpx; |
| 1634 | color: #666; | 1608 | color: #666; |
| @@ -1636,28 +1610,27 @@ button::after { | @@ -1636,28 +1610,27 @@ button::after { | ||
| 1636 | white-space: nowrap; | 1610 | white-space: nowrap; |
| 1637 | margin-right: 12rpx; | 1611 | margin-right: 12rpx; |
| 1638 | flex-shrink: 0; | 1612 | flex-shrink: 0; |
| 1639 | -} | 1613 | + } |
| 1640 | 1614 | ||
| 1641 | -.superset-wrapper .set-content { | 1615 | + .set-content { |
| 1642 | font-size: 26rpx; | 1616 | font-size: 26rpx; |
| 1643 | color: inherit; | 1617 | color: inherit; |
| 1644 | flex: 1; | 1618 | flex: 1; |
| 1645 | -} | 1619 | + } |
| 1646 | 1620 | ||
| 1647 | -.superset-wrapper .rest-time { | 1621 | + .rest-time { |
| 1648 | font-size: 24rpx; | 1622 | font-size: 24rpx; |
| 1649 | // color: inherit; | 1623 | // color: inherit; |
| 1650 | margin-left: 10rpx; | 1624 | margin-left: 10rpx; |
| 1651 | flex-shrink: 0; | 1625 | flex-shrink: 0; |
| 1652 | -} | 1626 | + } |
| 1653 | 1627 | ||
| 1654 | -.superset-wrapper .check { | 1628 | + .check { |
| 1655 | font-size: 26rpx; | 1629 | font-size: 26rpx; |
| 1656 | color: #333; | 1630 | color: #333; |
| 1657 | margin-left: 275rpx !important; | 1631 | margin-left: 275rpx !important; |
| 1658 | - /* 这里控制和左边内容的距离,数值你可以自己调 */ | ||
| 1659 | flex-shrink: 0; | 1632 | flex-shrink: 0; |
| 1660 | - /* 防止被压缩 */ | 1633 | + } |
| 1661 | } | 1634 | } |
| 1662 | 1635 | ||
| 1663 | .superset-title { | 1636 | .superset-title { |
| @@ -1673,16 +1646,14 @@ button::after { | @@ -1673,16 +1646,14 @@ button::after { | ||
| 1673 | .noteContent { | 1646 | .noteContent { |
| 1674 | display: flex; | 1647 | display: flex; |
| 1675 | align-items: flex-start; | 1648 | align-items: flex-start; |
| 1676 | - /* 改成顶部对齐,而不是居中 */ | ||
| 1677 | gap: 12rpx; | 1649 | gap: 12rpx; |
| 1678 | padding: 20rpx; | 1650 | padding: 20rpx; |
| 1679 | - background: #fff; | 1651 | + background: $color-white; |
| 1680 | } | 1652 | } |
| 1681 | 1653 | ||
| 1682 | .noteTitle { | 1654 | .noteTitle { |
| 1683 | font-size: 26rpx; | 1655 | font-size: 26rpx; |
| 1684 | color: #666; | 1656 | color: #666; |
| 1685 | - /* 关键:固定宽度,保证标签容器的左边距稳定 */ | ||
| 1686 | flex-shrink: 0; | 1657 | flex-shrink: 0; |
| 1687 | width: 150rpx; | 1658 | width: 150rpx; |
| 1688 | } | 1659 | } |
| @@ -1694,11 +1665,10 @@ button::after { | @@ -1694,11 +1665,10 @@ button::after { | ||
| 1694 | gap: 12rpx; | 1665 | gap: 12rpx; |
| 1695 | } | 1666 | } |
| 1696 | 1667 | ||
| 1697 | -.noteTag { | 1668 | +// u-tag 微调 |
| 1669 | +:deep(.noteTag) { | ||
| 1670 | + width: auto !important; | ||
| 1698 | font-size: 24rpx; | 1671 | font-size: 24rpx; |
| 1699 | - color: #fff; | ||
| 1700 | - padding: 6rpx 14rpx; | ||
| 1701 | border-radius: 10rpx; | 1672 | border-radius: 10rpx; |
| 1702 | - line-height: 1.2; | ||
| 1703 | } | 1673 | } |
| 1704 | </style> | 1674 | </style> |
| @@ -12,7 +12,7 @@ | @@ -12,7 +12,7 @@ | ||
| 12 | <scroll-view class="pop-content" scroll-y> | 12 | <scroll-view class="pop-content" scroll-y> |
| 13 | <dongzuo v-if="isRenderDongzuo" :id="currUnit.unitId" :type="currUnit.unitType" :actionDetail="convertAction" | 13 | <dongzuo v-if="isRenderDongzuo" :id="currUnit.unitId" :type="currUnit.unitType" :actionDetail="convertAction" |
| 14 | :unitIndex="currUnitIndex" ref="dongzuoSingleRef" @deleteAction.prevent="handleDeleteAction" | 14 | :unitIndex="currUnitIndex" ref="dongzuoSingleRef" @deleteAction.prevent="handleDeleteAction" |
| 15 | - @replace-action.prevent="handleReplaceAction" :is-daily-templates="true" /> | 15 | + @replace-action.prevent="handleReplaceAction" :is-daily-templates="true" :is-tick="false"/> |
| 16 | </scroll-view> | 16 | </scroll-view> |
| 17 | </view> | 17 | </view> |
| 18 | 18 |
| 1 | <template> | 1 | <template> |
| 2 | - <!-- 自定义底部弹窗 --> | ||
| 3 | - <view v-if="showPopup" class="custom-mask" @click="handleMaskClick"> | ||
| 4 | - <view class="custom-popup" @click.stop> | 2 | + <!-- 底部弹窗:训练日历 --> |
| 3 | + <up-popup | ||
| 4 | + :show="showPopup" | ||
| 5 | + mode="bottom" | ||
| 6 | + round="24rpx" | ||
| 7 | + :safeAreaInsetBottom="true" | ||
| 8 | + @close="close" | ||
| 9 | + > | ||
| 10 | + <view class="calendar-popup-content"> | ||
| 5 | <!-- 标题 + 月份导航 --> | 11 | <!-- 标题 + 月份导航 --> |
| 6 | <view class="popup-header"> | 12 | <view class="popup-header"> |
| 7 | - <text class="title">训练日历</text> | ||
| 8 | - <view class="month-nav"> | ||
| 9 | - <u-icon name="arrow-left" size="24" @click="prevMonth"></u-icon> | ||
| 10 | - <text class="month-text">{{ currentYearMonth }}</text> | ||
| 11 | - <u-icon name="arrow-right" size="24" @click="nextMonth"></u-icon> | 13 | + <text class="header-title">训练日历</text> |
| 14 | + <view class="month-navigator"> | ||
| 15 | + <u-icon name="arrow-left" size="24" @click="prevMonth" /> | ||
| 16 | + <text class="month-label">{{ currentYearMonth }}</text> | ||
| 17 | + <u-icon name="arrow-right" size="24" @click="nextMonth" /> | ||
| 12 | </view> | 18 | </view> |
| 13 | </view> | 19 | </view> |
| 14 | 20 | ||
| 15 | - <!-- 自定义日历主体 --> | ||
| 16 | - <view class="calendar-container"> | 21 | + <!-- 日历主体 --> |
| 22 | + <view class="calendar-body"> | ||
| 17 | <!-- 星期行 --> | 23 | <!-- 星期行 --> |
| 18 | - <view class="week-row"> | ||
| 19 | - <text v-for="week in weekDays" :key="week" class="week-day">{{ week }}</text> | 24 | + <view class="week-header"> |
| 25 | + <text | ||
| 26 | + v-for="(day, idx) in WEEK_DAYS" | ||
| 27 | + :key="idx" | ||
| 28 | + class="week-item" | ||
| 29 | + :class="{ weekend: idx === 0 || idx === 6 }" | ||
| 30 | + > | ||
| 31 | + {{ day }} | ||
| 32 | + </text> | ||
| 20 | </view> | 33 | </view> |
| 21 | - <!-- 日期格子 --> | ||
| 22 | - <view class="days-grid"> | ||
| 23 | - <view v-for="(day, idx) in calendarDays" :key="idx" class="day-cell" :class="{ | ||
| 24 | - 'other-month': !day.isCurrentMonth, | ||
| 25 | - 'selected': isDateSelected(day.fullDate), | ||
| 26 | - 'disabled': day.fullDate < minDateStr | ||
| 27 | - }" @click="toggleDate(day)"> | ||
| 28 | - <text class="day-number">{{ day.day }}</text> | 34 | + |
| 35 | + <!-- 日期网格 --> | ||
| 36 | + <view class="date-grid"> | ||
| 37 | + <view | ||
| 38 | + v-for="(item, idx) in calendarDays" | ||
| 39 | + :key="idx" | ||
| 40 | + class="date-cell" | ||
| 41 | + :class="getDateCellClass(item)" | ||
| 42 | + @click="toggleDate(item)" | ||
| 43 | + > | ||
| 44 | + <text class="date-text">{{ item.day }}</text> | ||
| 29 | </view> | 45 | </view> |
| 30 | </view> | 46 | </view> |
| 31 | </view> | 47 | </view> |
| 32 | 48 | ||
| 33 | - <!-- 底部确认按钮 --> | 49 | + <!-- 底部操作 --> |
| 34 | <view class="popup-footer"> | 50 | <view class="popup-footer"> |
| 35 | - <u-button type="primary" size="large" :disabled="selectedDates.length === 0" @click="confirmAdd"> | 51 | + <view class="selected-hint" v-if="selectedDates.length"> |
| 52 | + 已选 {{ selectedDates.length }} 天 | ||
| 53 | + </view> | ||
| 54 | + <u-button | ||
| 55 | + type="primary" | ||
| 56 | + size="large" | ||
| 57 | + :disabled="!selectedDates.length" | ||
| 58 | + @click="confirmAdd" | ||
| 59 | + > | ||
| 36 | 添加到以上日程 | 60 | 添加到以上日程 |
| 37 | </u-button> | 61 | </u-button> |
| 38 | </view> | 62 | </view> |
| 39 | </view> | 63 | </view> |
| 40 | - </view> | 64 | + </up-popup> |
| 41 | </template> | 65 | </template> |
| 42 | 66 | ||
| 43 | <script setup> | 67 | <script setup> |
| 44 | -import { ref, computed, watch } from 'vue'; | 68 | +import { ref, computed } from 'vue'; |
| 69 | +import dayjs from 'dayjs'; | ||
| 45 | import QueryPlanApi from '@/sheep/api/plan/queryplan'; | 70 | import QueryPlanApi from '@/sheep/api/plan/queryplan'; |
| 46 | import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; | 71 | import dailytemplateApi from '@/sheep/api/Template/Dailytemplate'; |
| 47 | 72 | ||
| 73 | +// ==================== 常量 ==================== | ||
| 74 | +const DATE_FORMAT = 'YYYY-MM-DD'; | ||
| 75 | +const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六']; | ||
| 76 | +const TOTAL_GRID_CELLS = 42; // 6 行 × 7 列 | ||
| 77 | + | ||
| 78 | +// ==================== Props & Emits ==================== | ||
| 48 | const props = defineProps({ | 79 | const props = defineProps({ |
| 49 | planId: { type: [String, Number], required: false }, | 80 | planId: { type: [String, Number], required: false }, |
| 50 | templateId: { type: [String, Number], required: true, default: 0 }, | 81 | templateId: { type: [String, Number], required: true, default: 0 }, |
| 51 | - isMove: { | ||
| 52 | - type: Boolean, | ||
| 53 | - default: false | ||
| 54 | - }, | 82 | + isMove: { type: Boolean, default: false }, |
| 55 | isCopy: { type: Boolean, default: false }, | 83 | isCopy: { type: Boolean, default: false }, |
| 56 | - dailyTemplateId: { type: Number, default: 0 } | 84 | + dailyTemplateId: { type: Number, default: 0 }, |
| 57 | }); | 85 | }); |
| 86 | + | ||
| 58 | const emit = defineEmits(['success', 'clearCopyFlag']); | 87 | const emit = defineEmits(['success', 'clearCopyFlag']); |
| 59 | 88 | ||
| 60 | -// 弹窗显示控制 | 89 | +// ==================== 工具函数 ==================== |
| 90 | +/** 格式化 dayjs 实例为 YYYY-MM-DD 字符串 */ | ||
| 91 | +const fmtDate = (d) => d.format(DATE_FORMAT); | ||
| 92 | + | ||
| 93 | +/** 获取当天的 YYYY-MM-DD */ | ||
| 94 | +const getTodayStr = () => fmtDate(dayjs()); | ||
| 95 | + | ||
| 96 | +// ==================== 响应式状态 ==================== | ||
| 61 | const showPopup = ref(false); | 97 | const showPopup = ref(false); |
| 62 | 98 | ||
| 63 | -// 当前显示的日期(年、月) | ||
| 64 | -const currentYear = ref(new Date().getFullYear()); | ||
| 65 | -const currentMonth = ref(new Date().getMonth() + 1); // 1-12 | 99 | +// 当前视图的年月(用 dayjs 驱动) |
| 100 | +const viewDate = ref(dayjs()); | ||
| 66 | 101 | ||
| 67 | -// 已选择的日期列表(字符串数组,格式 YYYY-MM-DD) | 102 | +// 已选日期集合 |
| 68 | const selectedDates = ref([]); | 103 | const selectedDates = ref([]); |
| 69 | 104 | ||
| 70 | // 最小可选日期(今天) | 105 | // 最小可选日期(今天) |
| 71 | -const today = new Date(); | ||
| 72 | -const minDateStr = `${today.getFullYear()}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}`; | 106 | +const minDateStr = getTodayStr(); |
| 73 | 107 | ||
| 74 | -// 星期标题 | ||
| 75 | -const weekDays = ['日', '一', '二', '三', '四', '五', '六']; | 108 | +// ==================== 计算属性 ==================== |
| 109 | +/** 当前显示的年-月文本 */ | ||
| 110 | +const currentYearMonth = computed(() => viewDate.value.format('YYYY年MM月')); | ||
| 76 | 111 | ||
| 77 | -// 当前年月显示文本 | ||
| 78 | -const currentYearMonth = computed(() => `${currentYear.value}年${currentMonth.value.toString().padStart(2, '0')}月`); | ||
| 79 | - | ||
| 80 | -// 生成当前月份的日历数据(包含上个月和下个月的占位) | 112 | +/** 生成日历网格数据 */ |
| 81 | const calendarDays = computed(() => { | 113 | const calendarDays = computed(() => { |
| 82 | - const year = currentYear.value; | ||
| 83 | - const month = currentMonth.value; | ||
| 84 | - const firstDayOfMonth = new Date(year, month - 1, 1); | ||
| 85 | - const startWeekday = firstDayOfMonth.getDay(); // 0=周日 | ||
| 86 | - const daysInMonth = new Date(year, month, 0).getDate(); | ||
| 87 | - | ||
| 88 | - // 获取上个月的最后几天 | ||
| 89 | - const prevMonthDate = new Date(year, month - 1, 0); | ||
| 90 | - const daysInPrevMonth = prevMonthDate.getDate(); | 114 | + const vd = viewDate.value; |
| 115 | + const year = vd.year(); | ||
| 116 | + const month = vd.month(); // 0-11 | ||
| 117 | + | ||
| 118 | + // 当月第一天 & 当月天数 | ||
| 119 | + const firstDay = dayjs(new Date(year, month, 1)); | ||
| 120 | + const startWeekday = firstDay.day(); // 0=周日 | ||
| 121 | + const daysInMonth = firstDay.daysInMonth(); | ||
| 122 | + | ||
| 123 | + // 上个月补位 | ||
| 91 | const prevMonthDays = []; | 124 | const prevMonthDays = []; |
| 125 | + const prevLastDay = firstDay.subtract(1, 'day'); | ||
| 126 | + const prevDaysInMonth = prevLastDay.date(); | ||
| 92 | for (let i = startWeekday - 1; i >= 0; i--) { | 127 | for (let i = startWeekday - 1; i >= 0; i--) { |
| 93 | - const dayNum = daysInPrevMonth - i; | ||
| 94 | - const fullDate = `${prevMonthDate.getFullYear()}-${(prevMonthDate.getMonth() + 1).toString().padStart(2, '0')}-${dayNum.toString().padStart(2, '0')}`; | ||
| 95 | - prevMonthDays.push({ | ||
| 96 | - day: dayNum, | ||
| 97 | - fullDate: fullDate, | ||
| 98 | - isCurrentMonth: false, | ||
| 99 | - }); | 128 | + const d = dayjs(new Date(year, month - 1, prevDaysInMonth - i)); |
| 129 | + prevMonthDays.push({ day: d.date(), fullDate: fmtDate(d), isCurrentMonth: false }); | ||
| 100 | } | 130 | } |
| 101 | 131 | ||
| 102 | - // 当月天数 | ||
| 103 | - const currentMonthDays = []; | 132 | + // 当月 |
| 133 | + const curMonthDays = []; | ||
| 104 | for (let i = 1; i <= daysInMonth; i++) { | 134 | for (let i = 1; i <= daysInMonth; i++) { |
| 105 | - const fullDate = `${year}-${month.toString().padStart(2, '0')}-${i.toString().padStart(2, '0')}`; | ||
| 106 | - currentMonthDays.push({ | ||
| 107 | - day: i, | ||
| 108 | - fullDate: fullDate, | ||
| 109 | - isCurrentMonth: true, | ||
| 110 | - }); | 135 | + const d = dayjs(new Date(year, month, i)); |
| 136 | + curMonthDays.push({ day: i, fullDate: fmtDate(d), isCurrentMonth: true }); | ||
| 111 | } | 137 | } |
| 112 | 138 | ||
| 113 | - // 下个月补齐(总共42格,6行) | ||
| 114 | - const totalCells = 42; | ||
| 115 | - const remaining = totalCells - (prevMonthDays.length + currentMonthDays.length); | 139 | + // 下个月补位 |
| 116 | const nextMonthDays = []; | 140 | const nextMonthDays = []; |
| 141 | + const remaining = TOTAL_GRID_CELLS - prevMonthDays.length - curMonthDays.length; | ||
| 117 | for (let i = 1; i <= remaining; i++) { | 142 | for (let i = 1; i <= remaining; i++) { |
| 118 | - const nextDate = new Date(year, month, i); | ||
| 119 | - const fullDate = `${nextDate.getFullYear()}-${(nextDate.getMonth() + 1).toString().padStart(2, '0')}-${nextDate.getDate().toString().padStart(2, '0')}`; | ||
| 120 | - nextMonthDays.push({ | ||
| 121 | - day: i, | ||
| 122 | - fullDate: fullDate, | ||
| 123 | - isCurrentMonth: false, | ||
| 124 | - }); | 143 | + const d = dayjs(new Date(year, month + 1, i)); |
| 144 | + nextMonthDays.push({ day: i, fullDate: fmtDate(d), isCurrentMonth: false }); | ||
| 125 | } | 145 | } |
| 126 | 146 | ||
| 127 | - return [...prevMonthDays, ...currentMonthDays, ...nextMonthDays]; | 147 | + return [...prevMonthDays, ...curMonthDays, ...nextMonthDays]; |
| 128 | }); | 148 | }); |
| 129 | 149 | ||
| 130 | -// 检查某天是否被选中 | ||
| 131 | -const isDateSelected = (fullDate) => { | ||
| 132 | - return selectedDates.value.includes(fullDate); | ||
| 133 | -}; | 150 | +// ==================== 方法 ==================== |
| 151 | +/** 判断日期是否被选中 */ | ||
| 152 | +const isDateSelected = (fullDate) => selectedDates.value.includes(fullDate); | ||
| 134 | 153 | ||
| 135 | -// 切换日期选中状态(仅允许选择今天及之后) | ||
| 136 | -const toggleDate = (day) => { | ||
| 137 | - if (day.fullDate < minDateStr) { | 154 | +/** 判断是否为今天 */ |
| 155 | +const isToday = (fullDate) => fullDate === getTodayStr(); | ||
| 156 | + | ||
| 157 | +/** 获取日期格子的样式类 */ | ||
| 158 | +const getDateCellClass = (item) => ({ | ||
| 159 | + 'other-month': !item.isCurrentMonth, | ||
| 160 | + selected: isDateSelected(item.fullDate), | ||
| 161 | + disabled: item.fullDate < minDateStr, | ||
| 162 | + today: isToday(item.fullDate), | ||
| 163 | +}); | ||
| 164 | + | ||
| 165 | +/** 切换日期选中状态 */ | ||
| 166 | +const toggleDate = (item) => { | ||
| 167 | + if (item.fullDate < minDateStr) { | ||
| 138 | uni.showToast({ title: '不能选择过去的日期', icon: 'none' }); | 168 | uni.showToast({ title: '不能选择过去的日期', icon: 'none' }); |
| 139 | return; | 169 | return; |
| 140 | } | 170 | } |
| 141 | - // 移动模式下,只能选择一个日期 | 171 | + // 移动模式:单选 |
| 142 | if (props.isMove) { | 172 | if (props.isMove) { |
| 143 | - selectedDates.value = [day.fullDate]; // 直接覆盖,永远只有1个 | 173 | + selectedDates.value = [item.fullDate]; |
| 144 | return; | 174 | return; |
| 145 | } | 175 | } |
| 146 | - const index = selectedDates.value.indexOf(day.fullDate); | ||
| 147 | - if (index === -1) { | ||
| 148 | - selectedDates.value.push(day.fullDate); | 176 | + // 普通模式:多选切换 |
| 177 | + const idx = selectedDates.value.indexOf(item.fullDate); | ||
| 178 | + if (idx === -1) { | ||
| 179 | + selectedDates.value.push(item.fullDate); | ||
| 149 | } else { | 180 | } else { |
| 150 | - selectedDates.value.splice(index, 1); | 181 | + selectedDates.value.splice(idx, 1); |
| 151 | } | 182 | } |
| 152 | }; | 183 | }; |
| 153 | 184 | ||
| 154 | -// 月份切换 | 185 | +/** 上一月 */ |
| 155 | const prevMonth = () => { | 186 | const prevMonth = () => { |
| 156 | - if (currentMonth.value === 1) { | ||
| 157 | - currentYear.value--; | ||
| 158 | - currentMonth.value = 12; | ||
| 159 | - } else { | ||
| 160 | - currentMonth.value--; | ||
| 161 | - } | 187 | + viewDate.value = viewDate.value.subtract(1, 'month'); |
| 162 | }; | 188 | }; |
| 189 | + | ||
| 190 | +/** 下一月 */ | ||
| 163 | const nextMonth = () => { | 191 | const nextMonth = () => { |
| 164 | - if (currentMonth.value === 12) { | ||
| 165 | - currentYear.value++; | ||
| 166 | - currentMonth.value = 1; | ||
| 167 | - } else { | ||
| 168 | - currentMonth.value++; | ||
| 169 | - } | 192 | + viewDate.value = viewDate.value.add(1, 'month'); |
| 193 | +}; | ||
| 194 | + | ||
| 195 | +/** 重置到当前月份 */ | ||
| 196 | +const resetToToday = () => { | ||
| 197 | + viewDate.value = dayjs(); | ||
| 170 | }; | 198 | }; |
| 171 | 199 | ||
| 172 | -// 关闭弹窗 | ||
| 173 | -const handleMaskClick = () => close(); | 200 | +/** 关闭弹窗 */ |
| 174 | const close = () => { | 201 | const close = () => { |
| 175 | showPopup.value = false; | 202 | showPopup.value = false; |
| 176 | selectedDates.value = []; | 203 | selectedDates.value = []; |
| 177 | - // 重置到当前月份(可选) | ||
| 178 | - currentYear.value = new Date().getFullYear(); | ||
| 179 | - currentMonth.value = new Date().getMonth() + 1; | ||
| 180 | - emit('clearCopyFlag') | 204 | + resetToToday(); |
| 205 | + emit('clearCopyFlag'); | ||
| 181 | }; | 206 | }; |
| 182 | 207 | ||
| 183 | -// 打开弹窗(暴露给父组件) | 208 | +/** 打开弹窗 */ |
| 184 | const open = () => { | 209 | const open = () => { |
| 185 | - console.log('子组件open方法被调用了'); | ||
| 186 | showPopup.value = true; | 210 | showPopup.value = true; |
| 187 | selectedDates.value = []; | 211 | selectedDates.value = []; |
| 188 | - currentYear.value = new Date().getFullYear(); | ||
| 189 | - currentMonth.value = new Date().getMonth() + 1; | 212 | + resetToToday(); |
| 190 | }; | 213 | }; |
| 191 | 214 | ||
| 192 | -// 确认添加 | 215 | +/** 确认添加 */ |
| 193 | const confirmAdd = async () => { | 216 | const confirmAdd = async () => { |
| 194 | - if (selectedDates.value.length === 0) { | 217 | + if (!selectedDates.value.length) { |
| 195 | uni.showToast({ title: '请选择训练日期', icon: 'none' }); | 218 | uni.showToast({ title: '请选择训练日期', icon: 'none' }); |
| 196 | return; | 219 | return; |
| 197 | } | 220 | } |
| 198 | try { | 221 | try { |
| 199 | - // if (props.isMove) { | ||
| 200 | - // const params = { | ||
| 201 | - // id: props.templateId, | ||
| 202 | - // trainingDate: selectedDates.value[0] || '' | ||
| 203 | - // }; | ||
| 204 | - // // 添加到日历弹窗的参数 | ||
| 205 | - // console.log('移动到:params', params); | ||
| 206 | - // await QueryPlanApi.updateDailyTemplateDate(params); | ||
| 207 | - // uni.showToast({ title: '移动成功', icon: 'success' }); | ||
| 208 | - // } else { | ||
| 209 | - // const params = { | ||
| 210 | - // id: props.templateId, | ||
| 211 | - // planId: props.planId, | ||
| 212 | - // trainDateList: selectedDates.value | ||
| 213 | - // }; | ||
| 214 | - // // 添加到日历弹窗的参数 | ||
| 215 | - // console.log('添加到params', params); | ||
| 216 | - // await QueryPlanApi.addPlanToCalendar(params); | ||
| 217 | - // uni.showToast({ title: '添加到日历成功', icon: 'success' }); | ||
| 218 | - // } | ||
| 219 | - // ========== 新增复制分支 优先判断 ========== | ||
| 220 | - console.log('rops.dailyTemplateId=', props.dailyTemplateId); | ||
| 221 | if (props.isCopy) { | 222 | if (props.isCopy) { |
| 222 | - // 调用你新增的复制接口 | ||
| 223 | - await dailytemplateApi.copyTempleteDailyTemplate(props.dailyTemplateId, selectedDates.value) | 223 | + await dailytemplateApi.copyTempleteDailyTemplate( |
| 224 | + props.dailyTemplateId, | ||
| 225 | + selectedDates.value, | ||
| 226 | + ); | ||
| 224 | uni.showToast({ title: '复制成功', icon: 'success' }); | 227 | uni.showToast({ title: '复制成功', icon: 'success' }); |
| 225 | - console.log('调用复制接口成功'); | ||
| 226 | - | ||
| 227 | } else if (props.isMove) { | 228 | } else if (props.isMove) { |
| 228 | - // 移动到 | ||
| 229 | - const params = { | 229 | + await QueryPlanApi.updateDailyTemplateDate({ |
| 230 | id: props.templateId, | 230 | id: props.templateId, |
| 231 | - trainingDate: selectedDates.value[0] || '' | ||
| 232 | - }; | ||
| 233 | - await QueryPlanApi.updateDailyTemplateDate(params); | 231 | + trainingDate: selectedDates.value[0] || '', |
| 232 | + }); | ||
| 234 | uni.showToast({ title: '移动成功', icon: 'success' }); | 233 | uni.showToast({ title: '移动成功', icon: 'success' }); |
| 235 | - console.log('移动接口成功'); | ||
| 236 | } else { | 234 | } else { |
| 237 | - //添加到日历 | ||
| 238 | - const params = { | 235 | + await QueryPlanApi.addPlanToCalendar({ |
| 239 | id: props.templateId, | 236 | id: props.templateId, |
| 240 | planId: props.planId, | 237 | planId: props.planId, |
| 241 | - trainDateList: selectedDates.value | ||
| 242 | - }; | ||
| 243 | - await QueryPlanApi.addPlanToCalendar(params); | 238 | + trainDateList: selectedDates.value, |
| 239 | + }); | ||
| 244 | uni.showToast({ title: '添加到日历成功', icon: 'success' }); | 240 | uni.showToast({ title: '添加到日历成功', icon: 'success' }); |
| 245 | - console.log('添加到接口成功'); | ||
| 246 | } | 241 | } |
| 247 | - | ||
| 248 | - | ||
| 249 | close(); | 242 | close(); |
| 250 | emit('success'); | 243 | emit('success'); |
| 251 | } catch (err) { | 244 | } catch (err) { |
| @@ -258,120 +251,162 @@ defineExpose({ open, close }); | @@ -258,120 +251,162 @@ defineExpose({ open, close }); | ||
| 258 | </script> | 251 | </script> |
| 259 | 252 | ||
| 260 | <style scoped lang="scss"> | 253 | <style scoped lang="scss"> |
| 261 | -.custom-mask { | ||
| 262 | - position: fixed; | ||
| 263 | - top: 0; | ||
| 264 | - left: 0; | ||
| 265 | - right: 0; | ||
| 266 | - bottom: 0; | ||
| 267 | - background-color: rgba(0, 0, 0, 0.5); | ||
| 268 | - z-index: 9999; | ||
| 269 | - display: flex; | ||
| 270 | - align-items: flex-end; | ||
| 271 | -} | ||
| 272 | - | ||
| 273 | -.custom-popup { | ||
| 274 | - background: #fff; | ||
| 275 | - border-radius: 24rpx 24rpx 0 0; | 254 | +/* ==================== 弹窗内容容器 ==================== */ |
| 255 | +.calendar-popup-content { | ||
| 276 | padding: 30rpx; | 256 | padding: 30rpx; |
| 277 | - box-sizing: border-box; | ||
| 278 | - width: 100%; | ||
| 279 | - max-height: 80vh; | ||
| 280 | - overflow-y: auto; | 257 | + min-height: 50vh; |
| 258 | + background: #fff; | ||
| 281 | } | 259 | } |
| 282 | 260 | ||
| 261 | +/* ==================== 头部 ==================== */ | ||
| 283 | .popup-header { | 262 | .popup-header { |
| 284 | display: flex; | 263 | display: flex; |
| 285 | justify-content: space-between; | 264 | justify-content: space-between; |
| 286 | align-items: center; | 265 | align-items: center; |
| 287 | - margin-bottom: 30rpx; | 266 | + margin-bottom: 28rpx; |
| 288 | 267 | ||
| 289 | - .title { | ||
| 290 | - font-size: 32rpx; | ||
| 291 | - font-weight: bold; | ||
| 292 | - color: #333; | 268 | + .header-title { |
| 269 | + font-size: 34rpx; | ||
| 270 | + font-weight: 700; | ||
| 271 | + color: #1a1a1a; | ||
| 293 | } | 272 | } |
| 294 | 273 | ||
| 295 | - .month-nav { | 274 | + .month-navigator { |
| 296 | display: flex; | 275 | display: flex; |
| 297 | align-items: center; | 276 | align-items: center; |
| 298 | - gap: 20rpx; | 277 | + gap: 24rpx; |
| 299 | 278 | ||
| 300 | - .month-text { | ||
| 301 | - font-size: 28rpx; | 279 | + .month-label { |
| 280 | + font-size: 30rpx; | ||
| 281 | + font-weight: 500; | ||
| 302 | color: #333; | 282 | color: #333; |
| 283 | + min-width: 140rpx; | ||
| 284 | + text-align: center; | ||
| 303 | } | 285 | } |
| 304 | } | 286 | } |
| 305 | } | 287 | } |
| 306 | 288 | ||
| 307 | -.calendar-container { | ||
| 308 | - margin-bottom: 30rpx; | 289 | +/* ==================== 日历主体 ==================== */ |
| 290 | +.calendar-body { | ||
| 291 | + flex: 1; | ||
| 309 | } | 292 | } |
| 310 | 293 | ||
| 311 | -.week-row { | 294 | +.week-header { |
| 312 | display: flex; | 295 | display: flex; |
| 313 | - justify-content: space-around; | ||
| 314 | - margin-bottom: 20rpx; | 296 | + margin-bottom: 16rpx; |
| 297 | + padding: 0 4rpx; | ||
| 315 | 298 | ||
| 316 | - .week-day { | ||
| 317 | - width: 14.28%; | 299 | + .week-item { |
| 300 | + flex: 1; | ||
| 318 | text-align: center; | 301 | text-align: center; |
| 319 | - font-size: 28rpx; | 302 | + font-size: 26rpx; |
| 320 | color: #999; | 303 | color: #999; |
| 304 | + padding: 8rpx 0; | ||
| 305 | + | ||
| 306 | + &.weekend { | ||
| 307 | + color: #e6a23c; | ||
| 308 | + } | ||
| 321 | } | 309 | } |
| 322 | } | 310 | } |
| 323 | 311 | ||
| 324 | -.days-grid { | 312 | +.date-grid { |
| 325 | display: flex; | 313 | display: flex; |
| 326 | flex-wrap: wrap; | 314 | flex-wrap: wrap; |
| 327 | } | 315 | } |
| 328 | 316 | ||
| 329 | -.day-cell { | ||
| 330 | - width: 14.28%; | ||
| 331 | - aspect-ratio: 1 / 1; | 317 | +.date-cell { |
| 318 | + width: calc(100% / 7); | ||
| 319 | + aspect-ratio: 1; | ||
| 332 | display: flex; | 320 | display: flex; |
| 333 | align-items: center; | 321 | align-items: center; |
| 334 | justify-content: center; | 322 | justify-content: center; |
| 335 | - border-radius: 8rpx; | ||
| 336 | - margin-bottom: 8rpx; | 323 | + border-radius: 50%; |
| 324 | + position: relative; | ||
| 325 | + transition: background-color 0.2s, transform 0.15s; | ||
| 337 | 326 | ||
| 338 | - .day-number { | 327 | + .date-text { |
| 339 | font-size: 28rpx; | 328 | font-size: 28rpx; |
| 340 | color: #333; | 329 | color: #333; |
| 330 | + position: relative; | ||
| 331 | + z-index: 1; | ||
| 341 | } | 332 | } |
| 342 | 333 | ||
| 343 | - &.other-month .day-number { | ||
| 344 | - color: #ccc; | 334 | + /* 非当月 */ |
| 335 | + &.other-month { | ||
| 336 | + opacity: 0; | ||
| 337 | + pointer-events: none; | ||
| 345 | } | 338 | } |
| 346 | 339 | ||
| 340 | + /* 今天 */ | ||
| 341 | + &.today { | ||
| 342 | + .date-text { | ||
| 343 | + color: #409eff; | ||
| 344 | + font-weight: 600; | ||
| 345 | + &::after { | ||
| 346 | + content: ''; | ||
| 347 | + position: absolute; | ||
| 348 | + bottom: -4rpx; | ||
| 349 | + left: 50%; | ||
| 350 | + transform: translateX(-50%); | ||
| 351 | + width: 8rpx; | ||
| 352 | + height: 8rpx; | ||
| 353 | + border-radius: 50%; | ||
| 354 | + background: #409eff; | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + } | ||
| 358 | + | ||
| 359 | + /* 已选中 */ | ||
| 347 | &.selected { | 360 | &.selected { |
| 348 | - background-color: #ffd700; | 361 | + background: linear-gradient(135deg, #ffd700, #ffb800); |
| 362 | + | ||
| 363 | + .date-text { | ||
| 364 | + color: #1a1a1a; | ||
| 365 | + font-weight: 700; | ||
| 366 | + } | ||
| 349 | 367 | ||
| 350 | - .day-number { | ||
| 351 | - color: #000; | ||
| 352 | - font-weight: bold; | 368 | + &.today .date-text::after { |
| 369 | + background: #1a1a1a; | ||
| 353 | } | 370 | } |
| 354 | } | 371 | } |
| 355 | 372 | ||
| 373 | + /* 不可选(过去日期) */ | ||
| 356 | &.disabled { | 374 | &.disabled { |
| 357 | - opacity: 0.4; | 375 | + opacity: 0.3; |
| 358 | pointer-events: none; | 376 | pointer-events: none; |
| 359 | } | 377 | } |
| 378 | + | ||
| 379 | + /* 可点击态 */ | ||
| 380 | + &:not(.disabled):not(.other-month):active { | ||
| 381 | + transform: scale(0.9); | ||
| 382 | + background-color: #f0f0f0; | ||
| 383 | + } | ||
| 360 | } | 384 | } |
| 361 | 385 | ||
| 386 | +/* ==================== 底部 ==================== */ | ||
| 362 | .popup-footer { | 387 | .popup-footer { |
| 363 | - margin-top: 30rpx; | ||
| 364 | - padding-top: 30rpx; | 388 | + margin-top: 24rpx; |
| 389 | + padding-top: 20rpx; | ||
| 390 | + border-top: 1rpx solid #f0f0f0; | ||
| 391 | + | ||
| 392 | + .selected-hint { | ||
| 393 | + text-align: center; | ||
| 394 | + font-size: 24rpx; | ||
| 395 | + color: #999; | ||
| 396 | + margin-bottom: 16rpx; | ||
| 397 | + } | ||
| 365 | 398 | ||
| 366 | .u-button { | 399 | .u-button { |
| 367 | background-color: #ffd700 !important; | 400 | background-color: #ffd700 !important; |
| 368 | - color: #000; | ||
| 369 | - font-weight: bold; | ||
| 370 | - border-radius: 40rpx; | 401 | + color: #1a1a1a; |
| 402 | + font-weight: 700; | ||
| 403 | + font-size: 30rpx; | ||
| 404 | + border-radius: 44rpx; | ||
| 405 | + letter-spacing: 2rpx; | ||
| 371 | 406 | ||
| 372 | &.u-button--disabled { | 407 | &.u-button--disabled { |
| 373 | - background-color: #eee !important; | ||
| 374 | - color: #999 !important; | 408 | + background-color: #e8e8e8 !important; |
| 409 | + color: #bbb !important; | ||
| 375 | } | 410 | } |
| 376 | } | 411 | } |
| 377 | } | 412 | } |
| @@ -345,15 +345,23 @@ const chartSeries = computed(() => { | @@ -345,15 +345,23 @@ const chartSeries = computed(() => { | ||
| 345 | 345 | ||
| 346 | const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume'; | 346 | const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume'; |
| 347 | 347 | ||
| 348 | + /** 单位转换:秒→分钟,米→公里,保留一位小数 */ | ||
| 349 | + const convertVal = (raw) => { | ||
| 350 | + const num = Number(raw) || 0; | ||
| 351 | + if (dataKey === 'duration') return parseFloat((num / 60).toFixed(1)); | ||
| 352 | + if (dataKey === 'distance') return parseFloat((num / 1000).toFixed(1)); | ||
| 353 | + return num; | ||
| 354 | + }; | ||
| 355 | + | ||
| 348 | // 当前周期数据 | 356 | // 当前周期数据 |
| 349 | - const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0); | 357 | + const currData = fullDateKeys.map((dateStr) => convertVal(currDataMap[dateStr]?.[dataKey] ?? 0)); |
| 350 | 358 | ||
| 351 | // 上一周期数据(按日期偏移匹配) | 359 | // 上一周期数据(按日期偏移匹配) |
| 352 | const lastData = fullDateKeys.map((dateStr) => { | 360 | const lastData = fullDateKeys.map((dateStr) => { |
| 353 | const targetKey = isWeekly | 361 | const targetKey = isWeekly |
| 354 | ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD') | 362 | ? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD') |
| 355 | : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD'); | 363 | : dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD'); |
| 356 | - return lastDataMap[targetKey]?.[dataKey] ?? 0; | 364 | + return convertVal(lastDataMap[targetKey]?.[dataKey] ?? 0); |
| 357 | }); | 365 | }); |
| 358 | 366 | ||
| 359 | const currName = isWeekly ? '本周' : '本月'; | 367 | const currName = isWeekly ? '本周' : '本月'; |
| @@ -372,11 +380,15 @@ const listData = computed(() => { | @@ -372,11 +380,15 @@ const listData = computed(() => { | ||
| 372 | if (!weeklyData.value) return []; | 380 | if (!weeklyData.value) return []; |
| 373 | 381 | ||
| 374 | const d = weeklyData.value; | 382 | const d = weeklyData.value; |
| 383 | + /** 单位转换:秒→分钟,米→公里,保留一位小数 */ | ||
| 384 | + const toMin = (v) => parseFloat(((v ?? 0) / 60).toFixed(1)); | ||
| 385 | + const toKm = (v) => parseFloat(((v ?? 0) / 1000).toFixed(1)); | ||
| 386 | + | ||
| 375 | const fields = [ | 387 | const fields = [ |
| 376 | { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume }, | 388 | { label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume }, |
| 377 | { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets }, | 389 | { label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets }, |
| 378 | - { label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration }, | ||
| 379 | - { label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance }, | 390 | + { label: '时长', last: toMin(d.lastWeekTotalDuration), curr: toMin(d.totalDuration) }, |
| 391 | + { label: '距离', last: toKm(d.lastWeekTotalDistance), curr: toKm(d.totalDistance) }, | ||
| 380 | { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, | 392 | { label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount }, |
| 381 | ]; | 393 | ]; |
| 382 | 394 | ||
| @@ -387,7 +399,7 @@ const listData = computed(() => { | @@ -387,7 +399,7 @@ const listData = computed(() => { | ||
| 387 | label, | 399 | label, |
| 388 | lastPeriod: lastVal, | 400 | lastPeriod: lastVal, |
| 389 | currPeriod: currVal, | 401 | currPeriod: currVal, |
| 390 | - diff: currVal - lastVal, | 402 | + diff: parseFloat((currVal - lastVal).toFixed(1)), |
| 391 | }; | 403 | }; |
| 392 | }); | 404 | }); |
| 393 | }); | 405 | }); |
| @@ -34,7 +34,7 @@ const emit = defineEmits(['success', 'close']) | @@ -34,7 +34,7 @@ const emit = defineEmits(['success', 'close']) | ||
| 34 | // 组件状态 | 34 | // 组件状态 |
| 35 | const show = ref(false) | 35 | const show = ref(false) |
| 36 | const folderList = ref([]) | 36 | const folderList = ref([]) |
| 37 | -const defaultCover = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png' // 默认封面,可替换为你的本地图片 | 37 | +const defaultCover = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/文件夹_1784867551849.png' // 默认封面,可替换为你的本地图片 |
| 38 | 38 | ||
| 39 | 39 | ||
| 40 | const props = defineProps({ | 40 | const props = defineProps({ |
| @@ -233,6 +233,11 @@ const handleDeleteTemplate = async () => { | @@ -233,6 +233,11 @@ const handleDeleteTemplate = async () => { | ||
| 233 | return; | 233 | return; |
| 234 | } | 234 | } |
| 235 | 235 | ||
| 236 | + // 先保存模板ID,再关闭菜单弹窗,避免弹窗叠加 | ||
| 237 | + const templateId = currentTemplate.value.id; | ||
| 238 | + closeTemplateMenu(); | ||
| 239 | + | ||
| 240 | + setTimeout(() => { | ||
| 236 | uni.showModal({ | 241 | uni.showModal({ |
| 237 | title: '确认删除', | 242 | title: '确认删除', |
| 238 | content: '确定要删除这个模板吗?删除后无法恢复', | 243 | content: '确定要删除这个模板吗?删除后无法恢复', |
| @@ -241,13 +246,11 @@ const handleDeleteTemplate = async () => { | @@ -241,13 +246,11 @@ const handleDeleteTemplate = async () => { | ||
| 241 | if (res.confirm) { | 246 | if (res.confirm) { |
| 242 | try { | 247 | try { |
| 243 | uni.showLoading({ title: '删除中...' }); | 248 | uni.showLoading({ title: '删除中...' }); |
| 244 | - await TemplatesApi.deleteCustTemplate(currentTemplate.value.id); | 249 | + await TemplatesApi.deleteCustTemplate(templateId); |
| 245 | uni.hideLoading(); | 250 | uni.hideLoading(); |
| 246 | uni.showToast({ title: '删除成功', icon: 'success' }); | 251 | uni.showToast({ title: '删除成功', icon: 'success' }); |
| 247 | - closeTemplateMenu(); | ||
| 248 | // 通知父组件刷新列表 | 252 | // 通知父组件刷新列表 |
| 249 | emit('refresh'); | 253 | emit('refresh'); |
| 250 | - | ||
| 251 | } catch (err) { | 254 | } catch (err) { |
| 252 | uni.hideLoading(); | 255 | uni.hideLoading(); |
| 253 | console.error('删除失败:', err); | 256 | console.error('删除失败:', err); |
| @@ -256,6 +259,7 @@ const handleDeleteTemplate = async () => { | @@ -256,6 +259,7 @@ const handleDeleteTemplate = async () => { | ||
| 256 | } | 259 | } |
| 257 | } | 260 | } |
| 258 | }); | 261 | }); |
| 262 | + }, 200); | ||
| 259 | }; | 263 | }; |
| 260 | 264 | ||
| 261 | 265 |
| 1 | <template> | 1 | <template> |
| 2 | <view class="container"> | 2 | <view class="container"> |
| 3 | + <!-- 加载中占位 --> | ||
| 4 | + <view class="loading-wrap" v-if="!isDataReady"> | ||
| 5 | + <text class="loading-text">加载中...</text> | ||
| 6 | + </view> | ||
| 7 | + | ||
| 3 | <!-- 我的分类 --> | 8 | <!-- 我的分类 --> |
| 4 | - <view class="section"> | 9 | + <view class="section" v-if="isDataReady"> |
| 5 | <view class="section-header"> | 10 | <view class="section-header"> |
| 6 | <text class="section-title">我的分类</text> | 11 | <text class="section-title">我的分类</text> |
| 7 | <text class="section-subtitle">点击隐藏分类</text> | 12 | <text class="section-subtitle">点击隐藏分类</text> |
| @@ -20,7 +25,7 @@ | @@ -20,7 +25,7 @@ | ||
| 20 | </view> | 25 | </view> |
| 21 | 26 | ||
| 22 | <!-- 已隐藏分类 --> | 27 | <!-- 已隐藏分类 --> |
| 23 | - <view class="section"> | 28 | + <view class="section" v-if="isDataReady"> |
| 24 | <view class="section-header"> | 29 | <view class="section-header"> |
| 25 | <text class="section-title">已隐藏分类</text> | 30 | <text class="section-title">已隐藏分类</text> |
| 26 | <text class="section-subtitle">点击恢复分类</text> | 31 | <text class="section-subtitle">点击恢复分类</text> |
| @@ -43,7 +48,6 @@ | @@ -43,7 +48,6 @@ | ||
| 43 | <!-- 底部按钮 --> | 48 | <!-- 底部按钮 --> |
| 44 | <view class="bottom-btns"> | 49 | <view class="bottom-btns"> |
| 45 | <view class="edit-btn" @click="openEditPopup">编辑名称</view> | 50 | <view class="edit-btn" @click="openEditPopup">编辑名称</view> |
| 46 | - <!-- 优化:点击打开自定义新增弹窗 --> | ||
| 47 | <view class="add-btn" @click="openAddPopup">+ 新增动作分类</view> | 51 | <view class="add-btn" @click="openAddPopup">+ 新增动作分类</view> |
| 48 | </view> | 52 | </view> |
| 49 | 53 | ||
| @@ -77,7 +81,7 @@ | @@ -77,7 +81,7 @@ | ||
| 77 | </view> | 81 | </view> |
| 78 | </up-popup> | 82 | </up-popup> |
| 79 | 83 | ||
| 80 | - <!-- ===================== 新增:自定义新增分类弹窗 ===================== --> | 84 | + <!-- 新增分类弹窗 --> |
| 81 | <up-popup :show="showAddPopup" mode="bottom" :mask-click="true" :safe-area-inset-bottom="true"> | 85 | <up-popup :show="showAddPopup" mode="bottom" :mask-click="true" :safe-area-inset-bottom="true"> |
| 82 | <view class="add-popup-wrap"> | 86 | <view class="add-popup-wrap"> |
| 83 | <!-- 头部 --> | 87 | <!-- 头部 --> |
| @@ -118,86 +122,92 @@ | @@ -118,86 +122,92 @@ | ||
| 118 | 122 | ||
| 119 | <script setup> | 123 | <script setup> |
| 120 | import { ref, computed } from 'vue'; | 124 | import { ref, computed } from 'vue'; |
| 121 | - import { onLoad, onShow } from '@dcloudio/uni-app'; | 125 | + import { onShow } from '@dcloudio/uni-app'; |
| 122 | import ExercisesApi from '@/sheep/api/motion/exercises'; | 126 | import ExercisesApi from '@/sheep/api/motion/exercises'; |
| 123 | import { useActionStore } from '@/sheep/store/action'; | 127 | import { useActionStore } from '@/sheep/store/action'; |
| 124 | 128 | ||
| 125 | - // 引入 Store | ||
| 126 | const actionStore = useActionStore(); | 129 | const actionStore = useActionStore(); |
| 127 | 130 | ||
| 128 | - // 从 Store 获取状态和方法 | ||
| 129 | const showCategories = computed(() => actionStore.showCategories); | 131 | const showCategories = computed(() => actionStore.showCategories); |
| 130 | const hiddenCategories = computed(() => actionStore.hiddenCategories); | 132 | const hiddenCategories = computed(() => actionStore.hiddenCategories); |
| 133 | + const { hideCategory, showCategory, getloadCategories, reloadCategories } = actionStore; | ||
| 131 | 134 | ||
| 132 | - const { hideCategory, showCategory, getloadCategories } = actionStore; | 135 | + // 页面数据就绪标记(避免 allCategories 为空时渲染空白) |
| 136 | + const isDataReady = ref(false); | ||
| 133 | 137 | ||
| 134 | - // 计算属性:获取隐藏分类的对象数组 | 138 | + // 隐藏分类的对象数组(含名称) |
| 135 | const hiddenCategoryObjects = computed(() => { | 139 | const hiddenCategoryObjects = computed(() => { |
| 140 | + if (!isDataReady.value) return []; | ||
| 136 | return actionStore.allCategories.filter(category => | 141 | return actionStore.allCategories.filter(category => |
| 137 | hiddenCategories.value.includes(category.id) | 142 | hiddenCategories.value.includes(category.id) |
| 138 | ); | 143 | ); |
| 139 | }); | 144 | }); |
| 140 | 145 | ||
| 141 | - // 自定义分类列表(弹窗用) | 146 | + // Toast 提示常量 |
| 147 | + const TOAST = { | ||
| 148 | + ADD_SUCCESS: '新增成功', | ||
| 149 | + ADD_FAIL: '新增失败', | ||
| 150 | + EDIT_SUCCESS: '修改成功', | ||
| 151 | + EDIT_FAIL: '修改失败', | ||
| 152 | + LOAD_FAIL: '加载失败', | ||
| 153 | + }; | ||
| 154 | + | ||
| 155 | + // 自定义分类列表(编辑弹窗用) | ||
| 142 | const customCategories = ref([]); | 156 | const customCategories = ref([]); |
| 143 | - // 当前要编辑的分类 | ||
| 144 | const currentEditCategory = ref(null); | 157 | const currentEditCategory = ref(null); |
| 145 | - // 控制自定义分类编辑弹窗 | ||
| 146 | const showEditPopup = ref(false); | 158 | const showEditPopup = ref(false); |
| 147 | - // 编辑弹窗输入值 | ||
| 148 | - const editInputValue = ref(''); | ||
| 149 | 159 | ||
| 150 | - // ===================== 新增:自定义新增弹窗变量 ===================== | ||
| 151 | - const showAddPopup = ref(false); // 控制新增弹窗显示 | ||
| 152 | - const addCategoryName = ref(''); // 新增分类名称 | 160 | + // 新增分类弹窗变量 |
| 161 | + const showAddPopup = ref(false); | ||
| 162 | + const addCategoryName = ref(''); | ||
| 153 | 163 | ||
| 154 | - // 打开新增弹窗 | ||
| 155 | const openAddPopup = () => { | 164 | const openAddPopup = () => { |
| 156 | - addCategoryName.value = ''; // 清空输入框 | 165 | + addCategoryName.value = ''; |
| 157 | showAddPopup.value = true; | 166 | showAddPopup.value = true; |
| 158 | }; | 167 | }; |
| 159 | - // 关闭新增弹窗 | 168 | + |
| 160 | const closeAddPopup = () => { | 169 | const closeAddPopup = () => { |
| 161 | showAddPopup.value = false; | 170 | showAddPopup.value = false; |
| 162 | }; | 171 | }; |
| 163 | 172 | ||
| 164 | - // ===================== 优化:执行新增分类(接口请求) ===================== | ||
| 165 | const doAddCategory = async () => { | 173 | const doAddCategory = async () => { |
| 166 | const name = addCategoryName.value.trim(); | 174 | const name = addCategoryName.value.trim(); |
| 167 | if (!name) return; | 175 | if (!name) return; |
| 168 | 176 | ||
| 169 | try { | 177 | try { |
| 170 | await ExercisesApi.createCustomCategory(name); | 178 | await ExercisesApi.createCustomCategory(name); |
| 171 | - uni.showToast({ title: '新增成功', icon: 'success' }); | 179 | + uni.showToast({ title: TOAST.ADD_SUCCESS, icon: 'success' }); |
| 172 | closeAddPopup(); | 180 | closeAddPopup(); |
| 173 | - getloadCategories(); | 181 | + reloadCategories(); |
| 174 | } catch (err) { | 182 | } catch (err) { |
| 175 | - uni.showToast({ title: '新增失败', icon: 'none' }); | 183 | + uni.showToast({ title: TOAST.ADD_FAIL, icon: 'none' }); |
| 176 | console.error(err); | 184 | console.error(err); |
| 177 | } | 185 | } |
| 178 | }; | 186 | }; |
| 179 | 187 | ||
| 180 | - // 打开自定义分类编辑弹窗 | ||
| 181 | - const openEditPopup = async () => { | ||
| 182 | - try { | 188 | + // 获取自定义分类列表 |
| 189 | + const fetchCustomCategories = async () => { | ||
| 183 | const res = await ExercisesApi.getCustomCategories(); | 190 | const res = await ExercisesApi.getCustomCategories(); |
| 184 | customCategories.value = res.data || []; | 191 | customCategories.value = res.data || []; |
| 192 | + }; | ||
| 193 | + | ||
| 194 | + // 打开编辑弹窗 | ||
| 195 | + const openEditPopup = async () => { | ||
| 196 | + try { | ||
| 197 | + await fetchCustomCategories(); | ||
| 185 | showEditPopup.value = true; | 198 | showEditPopup.value = true; |
| 186 | } catch (err) { | 199 | } catch (err) { |
| 187 | console.error('加载自定义分类失败', err); | 200 | console.error('加载自定义分类失败', err); |
| 188 | - uni.showToast({ title: '加载失败', icon: 'none' }); | 201 | + uni.showToast({ title: TOAST.LOAD_FAIL, icon: 'none' }); |
| 189 | } | 202 | } |
| 190 | }; | 203 | }; |
| 191 | 204 | ||
| 192 | - // 关闭弹窗 | ||
| 193 | const closeEditPopup = () => { | 205 | const closeEditPopup = () => { |
| 194 | showEditPopup.value = false; | 206 | showEditPopup.value = false; |
| 195 | }; | 207 | }; |
| 196 | 208 | ||
| 197 | - // 打开编辑弹窗 | ||
| 198 | const openEditModal = (item) => { | 209 | const openEditModal = (item) => { |
| 199 | currentEditCategory.value = item; | 210 | currentEditCategory.value = item; |
| 200 | - editInputValue.value = item.name; | ||
| 201 | uni.showModal({ | 211 | uni.showModal({ |
| 202 | title: '动作分类', | 212 | title: '动作分类', |
| 203 | editable: true, | 213 | editable: true, |
| @@ -207,27 +217,59 @@ | @@ -207,27 +217,59 @@ | ||
| 207 | if (res.confirm && res.content.trim()) { | 217 | if (res.confirm && res.content.trim()) { |
| 208 | try { | 218 | try { |
| 209 | await ExercisesApi.updateCustomCategory(item.id, res.content.trim()); | 219 | await ExercisesApi.updateCustomCategory(item.id, res.content.trim()); |
| 210 | - uni.showToast({ title: '修改成功', icon: 'success' }); | ||
| 211 | - openEditPopup(); | ||
| 212 | - getloadCategories(); | 220 | + uni.showToast({ title: TOAST.EDIT_SUCCESS, icon: 'success' }); |
| 221 | + await fetchCustomCategories(); | ||
| 222 | + reloadCategories(); | ||
| 213 | } catch (err) { | 223 | } catch (err) { |
| 214 | console.error('修改失败', err); | 224 | console.error('修改失败', err); |
| 215 | - uni.showToast({ title: '修改失败', icon: 'none' }); | 225 | + uni.showToast({ title: TOAST.EDIT_FAIL, icon: 'none' }); |
| 216 | } | 226 | } |
| 217 | } | 227 | } |
| 218 | }, | 228 | }, |
| 219 | }); | 229 | }); |
| 220 | }; | 230 | }; |
| 221 | 231 | ||
| 222 | - onShow(() => { | ||
| 223 | - getloadCategories(); | 232 | + onShow(async () => { |
| 233 | + await getloadCategories(); | ||
| 234 | + isDataReady.value = true; | ||
| 224 | }); | 235 | }); |
| 225 | </script> | 236 | </script> |
| 226 | 237 | ||
| 227 | <style scoped lang="scss"> | 238 | <style scoped lang="scss"> |
| 239 | + // ========== 设计变量 ========== | ||
| 240 | + $radius-sm: 10rpx; | ||
| 241 | + $radius-md: 12rpx; | ||
| 242 | + $radius-lg: 20rpx; | ||
| 243 | + $radius-round: 36rpx; | ||
| 244 | + $gap: 20rpx; | ||
| 245 | + $color-primary: #ffd700; | ||
| 246 | + $color-text: #333; | ||
| 247 | + $color-text-secondary: #666; | ||
| 248 | + $color-text-light: #999; | ||
| 249 | + $color-border: #eee; | ||
| 250 | + $color-bg: #f7f7f7; | ||
| 251 | + $color-bg-light: #f9f9f9; | ||
| 252 | + $transition-base: 0.2s ease; | ||
| 253 | + | ||
| 228 | .container { | 254 | .container { |
| 229 | min-height: 100vh; | 255 | min-height: 100vh; |
| 230 | background-color: #fff; | 256 | background-color: #fff; |
| 257 | + // 为底部固定栏留出空间 | ||
| 258 | + padding-bottom: 120rpx; | ||
| 259 | + box-sizing: border-box; | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + // 加载中占位 | ||
| 263 | + .loading-wrap { | ||
| 264 | + display: flex; | ||
| 265 | + justify-content: center; | ||
| 266 | + align-items: center; | ||
| 267 | + min-height: 60vh; | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + .loading-text { | ||
| 271 | + font-size: 28rpx; | ||
| 272 | + color: $color-text-light; | ||
| 231 | } | 273 | } |
| 232 | 274 | ||
| 233 | .section { | 275 | .section { |
| @@ -243,20 +285,22 @@ | @@ -243,20 +285,22 @@ | ||
| 243 | 285 | ||
| 244 | .section-title { | 286 | .section-title { |
| 245 | font-size: 28rpx; | 287 | font-size: 28rpx; |
| 246 | - color: #333; | 288 | + color: $color-text; |
| 247 | font-weight: bold; | 289 | font-weight: bold; |
| 248 | } | 290 | } |
| 249 | 291 | ||
| 250 | .section-subtitle { | 292 | .section-subtitle { |
| 251 | font-size: 24rpx; | 293 | font-size: 24rpx; |
| 252 | - color: #999; | 294 | + color: $color-text-light; |
| 253 | margin-left: 16rpx; | 295 | margin-left: 16rpx; |
| 254 | } | 296 | } |
| 255 | 297 | ||
| 256 | - .category-grid { | 298 | + // 分类网格(展示 + 隐藏共用) |
| 299 | + .category-grid, | ||
| 300 | + .hidden-grid { | ||
| 257 | display: grid; | 301 | display: grid; |
| 258 | - grid-template-columns: repeat(4, 1fr); | ||
| 259 | - gap: 20rpx; | 302 | + grid-template-columns: repeat(4, minmax(0, 1fr)); |
| 303 | + gap: $gap; | ||
| 260 | } | 304 | } |
| 261 | 305 | ||
| 262 | .category-item { | 306 | .category-item { |
| @@ -264,34 +308,36 @@ | @@ -264,34 +308,36 @@ | ||
| 264 | justify-content: center; | 308 | justify-content: center; |
| 265 | align-items: center; | 309 | align-items: center; |
| 266 | padding: 20rpx 16rpx; | 310 | padding: 20rpx 16rpx; |
| 311 | + padding-right: 28rpx; | ||
| 267 | background-color: #fff; | 312 | background-color: #fff; |
| 268 | - border: 1rpx solid #eee; | ||
| 269 | - border-radius: 10rpx; | 313 | + border: 1rpx solid $color-border; |
| 314 | + border-radius: $radius-sm; | ||
| 270 | position: relative; | 315 | position: relative; |
| 271 | - transition: all 0.2s ease; | ||
| 272 | - padding-right: 28rpx; | ||
| 273 | - } | 316 | + transition: all $transition-base; |
| 274 | 317 | ||
| 275 | - .category-item:active { | 318 | + &:active { |
| 276 | transform: scale(0.96); | 319 | transform: scale(0.96); |
| 277 | background-color: #fafafa; | 320 | background-color: #fafafa; |
| 278 | } | 321 | } |
| 322 | + } | ||
| 279 | 323 | ||
| 280 | .category-text { | 324 | .category-text { |
| 281 | font-size: 24rpx; | 325 | font-size: 24rpx; |
| 282 | - color: #333; | 326 | + color: $color-text; |
| 283 | text-align: center; | 327 | text-align: center; |
| 328 | + overflow: hidden; | ||
| 329 | + text-overflow: ellipsis; | ||
| 330 | + white-space: nowrap; | ||
| 284 | } | 331 | } |
| 285 | 332 | ||
| 286 | .delete-icon { | 333 | .delete-icon { |
| 287 | position: absolute; | 334 | position: absolute; |
| 288 | right: 4rpx; | 335 | right: 4rpx; |
| 289 | top: 4rpx; | 336 | top: 4rpx; |
| 290 | - transform: none; | ||
| 291 | - z-index: 99; | 337 | + z-index: 1; |
| 292 | padding: 4rpx; | 338 | padding: 4rpx; |
| 293 | opacity: 0.7; | 339 | opacity: 0.7; |
| 294 | - transition: all 0.2s ease; | 340 | + transition: all $transition-base; |
| 295 | } | 341 | } |
| 296 | 342 | ||
| 297 | .category-item:active .delete-icon { | 343 | .category-item:active .delete-icon { |
| @@ -299,26 +345,21 @@ | @@ -299,26 +345,21 @@ | ||
| 299 | color: #ff4d4f; | 345 | color: #ff4d4f; |
| 300 | } | 346 | } |
| 301 | 347 | ||
| 302 | - .hidden-grid { | ||
| 303 | - display: grid; | ||
| 304 | - grid-template-columns: repeat(4, 1fr); | ||
| 305 | - gap: 20rpx; | ||
| 306 | - } | ||
| 307 | - | ||
| 308 | .hidden-item { | 348 | .hidden-item { |
| 309 | display: flex; | 349 | display: flex; |
| 310 | justify-content: center; | 350 | justify-content: center; |
| 311 | align-items: center; | 351 | align-items: center; |
| 312 | padding: 20rpx 16rpx; | 352 | padding: 20rpx 16rpx; |
| 313 | - background: #f9f9f9; | ||
| 314 | - border: 1rpx solid #eee; | ||
| 315 | - border-radius: 10rpx; | ||
| 316 | - transition: all 0.2s ease; | ||
| 317 | - } | 353 | + background: $color-bg-light; |
| 354 | + border: 1rpx solid $color-border; | ||
| 355 | + border-radius: $radius-sm; | ||
| 356 | + transition: all $transition-base; | ||
| 318 | 357 | ||
| 319 | - .hidden-item:active { | 358 | + &:active { |
| 320 | transform: scale(0.96); | 359 | transform: scale(0.96); |
| 321 | - background-color: #f2f2f2; | 360 | + background-color: #e8e8e8; |
| 361 | + border-color: #ddd; | ||
| 362 | + } | ||
| 322 | } | 363 | } |
| 323 | 364 | ||
| 324 | .hidden-empty { | 365 | .hidden-empty { |
| @@ -329,10 +370,13 @@ | @@ -329,10 +370,13 @@ | ||
| 329 | 370 | ||
| 330 | .hidden-text { | 371 | .hidden-text { |
| 331 | font-size: 24rpx; | 372 | font-size: 24rpx; |
| 332 | - color: #999; | 373 | + color: $color-text-light; |
| 374 | + overflow: hidden; | ||
| 375 | + text-overflow: ellipsis; | ||
| 376 | + white-space: nowrap; | ||
| 333 | } | 377 | } |
| 334 | 378 | ||
| 335 | - /* 底部按钮 */ | 379 | + // 底部按钮栏 |
| 336 | .bottom-btns { | 380 | .bottom-btns { |
| 337 | display: flex; | 381 | display: flex; |
| 338 | justify-content: space-between; | 382 | justify-content: space-between; |
| @@ -344,9 +388,8 @@ | @@ -344,9 +388,8 @@ | ||
| 344 | background-color: rgba(255, 255, 255, 0.95); | 388 | background-color: rgba(255, 255, 255, 0.95); |
| 345 | backdrop-filter: blur(10px); | 389 | backdrop-filter: blur(10px); |
| 346 | z-index: 10; | 390 | z-index: 10; |
| 347 | - padding: 20rpx 30rpx; | 391 | + padding: 20rpx 20rpx calc(20rpx + env(safe-area-inset-bottom)); |
| 348 | box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); | 392 | box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); |
| 349 | - padding: 20rpx; | ||
| 350 | box-sizing: border-box; | 393 | box-sizing: border-box; |
| 351 | } | 394 | } |
| 352 | 395 | ||
| @@ -355,11 +398,11 @@ | @@ -355,11 +398,11 @@ | ||
| 355 | height: 72rpx; | 398 | height: 72rpx; |
| 356 | line-height: 72rpx; | 399 | line-height: 72rpx; |
| 357 | font-size: 26rpx; | 400 | font-size: 26rpx; |
| 358 | - color: #666; | 401 | + color: $color-text-secondary; |
| 359 | background-color: #f5f5f5; | 402 | background-color: #f5f5f5; |
| 360 | - border-radius: 36rpx; | 403 | + border-radius: $radius-round; |
| 361 | text-align: center; | 404 | text-align: center; |
| 362 | - transition: all 0.2s; | 405 | + transition: all $transition-base; |
| 363 | 406 | ||
| 364 | &:active { | 407 | &:active { |
| 365 | background-color: #e0e0e0; | 408 | background-color: #e0e0e0; |
| @@ -371,16 +414,16 @@ | @@ -371,16 +414,16 @@ | ||
| 371 | flex: 1; | 414 | flex: 1; |
| 372 | height: 72rpx; | 415 | height: 72rpx; |
| 373 | line-height: 72rpx; | 416 | line-height: 72rpx; |
| 374 | - background: linear-gradient(135deg, #ffd700 0%, #ffcc00 100%) !important; | ||
| 375 | - color: #333 !important; | 417 | + background: linear-gradient(135deg, $color-primary 0%, #ffcc00 100%); |
| 418 | + color: $color-text; | ||
| 376 | font-size: 28rpx; | 419 | font-size: 28rpx; |
| 377 | font-weight: 500; | 420 | font-weight: 500; |
| 378 | - border-radius: 36rpx; | ||
| 379 | - margin-left: 20rpx; | 421 | + border-radius: $radius-round; |
| 422 | + margin-left: $gap; | ||
| 380 | border: none; | 423 | border: none; |
| 381 | box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3); | 424 | box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3); |
| 382 | text-align: center; | 425 | text-align: center; |
| 383 | - transition: all 0.2s; | 426 | + transition: all $transition-base; |
| 384 | 427 | ||
| 385 | &:active { | 428 | &:active { |
| 386 | transform: scale(0.98); | 429 | transform: scale(0.98); |
| @@ -388,10 +431,10 @@ | @@ -388,10 +431,10 @@ | ||
| 388 | } | 431 | } |
| 389 | } | 432 | } |
| 390 | 433 | ||
| 391 | - /* 编辑弹窗 */ | 434 | + // 编辑分类弹窗 |
| 392 | .popup-wrap { | 435 | .popup-wrap { |
| 393 | - background: #f7f7f7; | ||
| 394 | - border-radius: 20rpx 20rpx 0 0; | 436 | + background: $color-bg; |
| 437 | + border-radius: $radius-lg $radius-lg 0 0; | ||
| 395 | padding: 0 30rpx; | 438 | padding: 0 30rpx; |
| 396 | max-height: 70vh; | 439 | max-height: 70vh; |
| 397 | display: flex; | 440 | display: flex; |
| @@ -408,12 +451,12 @@ | @@ -408,12 +451,12 @@ | ||
| 408 | .popup-title { | 451 | .popup-title { |
| 409 | font-size: 36rpx; | 452 | font-size: 36rpx; |
| 410 | font-weight: 500; | 453 | font-weight: 500; |
| 411 | - color: #333; | 454 | + color: $color-text; |
| 412 | } | 455 | } |
| 413 | 456 | ||
| 414 | .popup-tip { | 457 | .popup-tip { |
| 415 | font-size: 28rpx; | 458 | font-size: 28rpx; |
| 416 | - color: #666; | 459 | + color: $color-text-secondary; |
| 417 | margin-bottom: 30rpx; | 460 | margin-bottom: 30rpx; |
| 418 | } | 461 | } |
| 419 | 462 | ||
| @@ -427,22 +470,33 @@ | @@ -427,22 +470,33 @@ | ||
| 427 | justify-content: space-between; | 470 | justify-content: space-between; |
| 428 | align-items: center; | 471 | align-items: center; |
| 429 | background: #fff; | 472 | background: #fff; |
| 430 | - border-radius: 12rpx; | 473 | + border-radius: $radius-md; |
| 431 | padding: 30rpx; | 474 | padding: 30rpx; |
| 432 | - margin-bottom: 20rpx; | 475 | + margin-bottom: $gap; |
| 476 | + transition: all $transition-base; | ||
| 477 | + | ||
| 478 | + &:active { | ||
| 479 | + background-color: #f5f5f5; | ||
| 480 | + transform: scale(0.98); | ||
| 481 | + } | ||
| 433 | } | 482 | } |
| 434 | 483 | ||
| 435 | .item-name { | 484 | .item-name { |
| 436 | font-size: 32rpx; | 485 | font-size: 32rpx; |
| 437 | - color: #333; | 486 | + color: $color-text; |
| 487 | + flex: 1; | ||
| 488 | + overflow: hidden; | ||
| 489 | + text-overflow: ellipsis; | ||
| 490 | + white-space: nowrap; | ||
| 491 | + margin-right: 16rpx; | ||
| 438 | } | 492 | } |
| 439 | 493 | ||
| 440 | - /* ===================== 新增:自定义新增弹窗样式 ===================== */ | 494 | + // 新增分类弹窗 |
| 441 | .add-popup-wrap { | 495 | .add-popup-wrap { |
| 442 | background: #fff; | 496 | background: #fff; |
| 443 | - border-radius: 20rpx 20rpx 0 0; | 497 | + border-radius: $radius-lg $radius-lg 0 0; |
| 444 | padding: 30rpx; | 498 | padding: 30rpx; |
| 445 | - padding-bottom: calc(var(--safe-area-inset-bottom) + 30rpx); | 499 | + padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx); |
| 446 | } | 500 | } |
| 447 | 501 | ||
| 448 | .add-popup-header { | 502 | .add-popup-header { |
| @@ -455,26 +509,27 @@ | @@ -455,26 +509,27 @@ | ||
| 455 | .add-popup-title { | 509 | .add-popup-title { |
| 456 | font-size: 38rpx; | 510 | font-size: 38rpx; |
| 457 | font-weight: bold; | 511 | font-weight: bold; |
| 458 | - color: #333; | 512 | + color: $color-text; |
| 459 | } | 513 | } |
| 460 | 514 | ||
| 461 | .add-input-box { | 515 | .add-input-box { |
| 462 | - background: #f7f7f7; | ||
| 463 | - border-radius: 12rpx; | 516 | + background: $color-bg; |
| 517 | + border-radius: $radius-md; | ||
| 464 | padding: 24rpx 20rpx; | 518 | padding: 24rpx 20rpx; |
| 465 | - margin-bottom: 20rpx; | 519 | + margin-bottom: $gap; |
| 466 | } | 520 | } |
| 467 | 521 | ||
| 468 | .add-input { | 522 | .add-input { |
| 469 | font-size: 32rpx; | 523 | font-size: 32rpx; |
| 470 | - color: #333; | 524 | + color: $color-text; |
| 471 | height: 48rpx; | 525 | height: 48rpx; |
| 472 | line-height: 48rpx; | 526 | line-height: 48rpx; |
| 527 | + width: 100%; | ||
| 473 | } | 528 | } |
| 474 | 529 | ||
| 475 | .add-tip { | 530 | .add-tip { |
| 476 | font-size: 24rpx; | 531 | font-size: 24rpx; |
| 477 | - color: #999; | 532 | + color: $color-text-light; |
| 478 | margin-bottom: 40rpx; | 533 | margin-bottom: 40rpx; |
| 479 | } | 534 | } |
| 480 | 535 | ||
| @@ -487,14 +542,14 @@ | @@ -487,14 +542,14 @@ | ||
| 487 | height: 88rpx; | 542 | height: 88rpx; |
| 488 | line-height: 88rpx; | 543 | line-height: 88rpx; |
| 489 | font-size: 32rpx; | 544 | font-size: 32rpx; |
| 490 | - border-radius: 12rpx; | ||
| 491 | - background-color: #ffd700; | 545 | + border-radius: $radius-md; |
| 546 | + background-color: $color-primary; | ||
| 492 | color: #000; | 547 | color: #000; |
| 493 | border: none; | 548 | border: none; |
| 494 | 549 | ||
| 495 | &[disabled] { | 550 | &[disabled] { |
| 496 | background-color: #e0e0e0; | 551 | background-color: #e0e0e0; |
| 497 | - color: #999; | 552 | + color: $color-text-light; |
| 498 | } | 553 | } |
| 499 | } | 554 | } |
| 500 | </style> | 555 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="page-wrap"> | ||
| 3 | - <!-- 顶部搜索 --> | ||
| 4 | - <view class="add-btn-wrapper" @click.stop="addExercise"> | ||
| 5 | - <uni-icons type="plus" size="35" color="#333"></uni-icons> | ||
| 6 | - <view class="floating-menu" v-show="replaceAddshow" @click.stop> | ||
| 7 | - <view class="menu-item" @click="addnewmotion"> | ||
| 8 | - <uni-icons type="plus" size="24" color="#333"></uni-icons> | ||
| 9 | - <text class="menu-text">新增动作</text> | ||
| 10 | - </view> | ||
| 11 | - </view> | ||
| 12 | - </view> | ||
| 13 | - <view class="search-bar"> | ||
| 14 | - <!-- <view class="search-input-wrapper"> | ||
| 15 | - <uni-icons type="search" size="18" color="#999"></uni-icons> | ||
| 16 | - <input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" /> | ||
| 17 | - </view> --> | ||
| 18 | - </view> | 2 | + <view class="page-wrap" :style="{ paddingTop: PageHeight + 'px' }"> |
| 19 | 3 | ||
| 20 | <!-- 左右主体布局 --> | 4 | <!-- 左右主体布局 --> |
| 21 | <view class="layout-container"> | 5 | <view class="layout-container"> |
| @@ -29,18 +13,44 @@ | @@ -29,18 +13,44 @@ | ||
| 29 | 13 | ||
| 30 | <!-- 右侧动作列表 --> | 14 | <!-- 右侧动作列表 --> |
| 31 | <scroll-view scroll-y class="right-content"> | 15 | <scroll-view scroll-y class="right-content"> |
| 16 | + <!-- 初始引导:未选择分类 --> | ||
| 17 | + <view class="content-tip init-guide" v-if="!activeNav"> | ||
| 18 | + <text class="tip-text">请选择左侧分类查看动作</text> | ||
| 19 | + <text class="tip-subtext">选择分类后将展示该分类下的训练动作</text> | ||
| 20 | + </view> | ||
| 21 | + | ||
| 22 | + <!-- 加载中 --> | ||
| 23 | + <view class="content-tip loading-state" v-else-if="loading"> | ||
| 24 | + <text class="tip-text">加载中...</text> | ||
| 25 | + </view> | ||
| 26 | + | ||
| 27 | + <!-- 加载失败 --> | ||
| 28 | + <view class="content-tip error-state" v-else-if="loadError"> | ||
| 29 | + <text class="tip-text">加载失败,请重试</text> | ||
| 30 | + <view class="retry-btn" @click="retryLoad">点击重试</view> | ||
| 31 | + </view> | ||
| 32 | + | ||
| 33 | + <!-- 正常内容 --> | ||
| 34 | + <template v-else> | ||
| 32 | <!-- 部位筛选标签 --> | 35 | <!-- 部位筛选标签 --> |
| 33 | - <view class="tip" v-if="motionPart.length > 0"> | ||
| 34 | - <view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view> | ||
| 35 | - <view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }" | ||
| 36 | - @click="handlePartClick(item.id)"> | 36 | + <scroll-view scroll-x show-scrollbar="false" class="part-filter" v-if="motionPart.length > 0"> |
| 37 | + <view class="part-filter-inner"> | ||
| 38 | + <view class="filter-item" :class="{ active: activeMotionPart === '' }" @click="handlePartClick('')">全部 | ||
| 39 | + </view> | ||
| 40 | + <view class="filter-item" v-for="item in motionPart" :key="item.id" | ||
| 41 | + :class="{ active: activeMotionPart === item.id }" @click="handlePartClick(item.id)"> | ||
| 37 | {{ item.name }} | 42 | {{ item.name }} |
| 38 | </view> | 43 | </view> |
| 39 | </view> | 44 | </view> |
| 45 | + </scroll-view> | ||
| 40 | 46 | ||
| 41 | - <view class="exercise-grid"> | ||
| 42 | - <view class="content" v-if="exercises.length > 0"> | ||
| 43 | - <view class="equipment-list"> | 47 | + <!-- 部位筛选为空提示 --> |
| 48 | + <view class="content-tip part-empty-tip" v-else> | ||
| 49 | + <text class="tip-text">该分类下暂无可筛选部位</text> | ||
| 50 | + </view> | ||
| 51 | + | ||
| 52 | + <!-- 动作列表 --> | ||
| 53 | + <view class="equipment-list" v-if="exercises.length > 0"> | ||
| 44 | <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId"> | 54 | <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId"> |
| 45 | <view class="equipment-name">{{ item.equipmentName }}</view> | 55 | <view class="equipment-name">{{ item.equipmentName }}</view> |
| 46 | <view class="action-list"> | 56 | <view class="action-list"> |
| @@ -52,23 +62,27 @@ | @@ -52,23 +62,27 @@ | ||
| 52 | </view> | 62 | </view> |
| 53 | </view> | 63 | </view> |
| 54 | </view> | 64 | </view> |
| 65 | + | ||
| 66 | + <!-- 无动作数据 --> | ||
| 67 | + <view class="content-tip empty-state" v-else> | ||
| 68 | + <text class="tip-text">该分类下暂无动作</text> | ||
| 69 | + <text class="tip-subtext">可尝试切换分类或部位查看其他动作</text> | ||
| 55 | </view> | 70 | </view> |
| 56 | - <view class="empty-state" v-else> | ||
| 57 | - <text class="empty-text">暂无动作数据</text> | ||
| 58 | - </view> | ||
| 59 | - </view> | 71 | + </template> |
| 60 | </scroll-view> | 72 | </scroll-view> |
| 61 | </view> | 73 | </view> |
| 62 | 74 | ||
| 63 | - <!-- 底部横向已选标签栏 --> | 75 | + <!-- 底部已选标签栏 --> |
| 64 | <scroll-view class="selected-bar" scroll-x show-scrollbar="false" v-if="selectedActions.length > 0"> | 76 | <scroll-view class="selected-bar" scroll-x show-scrollbar="false" v-if="selectedActions.length > 0"> |
| 65 | - <view class="selected-item" v-for="(item, index) in selectedActions" :key="index"> | 77 | + <view class="selected-bar-inner"> |
| 78 | + <view class="selected-item" v-for="item in selectedActions" :key="item.id"> | ||
| 66 | <text class="name">{{ item.name }}</text> | 79 | <text class="name">{{ item.name }}</text> |
| 67 | <uni-icons type="close" size="16" color="#fff" @click="removeAction(item)"></uni-icons> | 80 | <uni-icons type="close" size="16" color="#fff" @click="removeAction(item)"></uni-icons> |
| 68 | </view> | 81 | </view> |
| 82 | + </view> | ||
| 69 | </scroll-view> | 83 | </scroll-view> |
| 70 | 84 | ||
| 71 | - <!-- 页面底部按钮 --> | 85 | + <!-- 底部操作按钮 --> |
| 72 | <view class="bottom-btn-bar"> | 86 | <view class="bottom-btn-bar"> |
| 73 | <view class="btn cancel-btn" @click="backPage">取消</view> | 87 | <view class="btn cancel-btn" @click="backPage">取消</view> |
| 74 | <view class="btn confirm-btn" :class="{ disabled: selectedActions.length < 2 }" @click="combineActions"> | 88 | <view class="btn confirm-btn" :class="{ disabled: selectedActions.length < 2 }" @click="combineActions"> |
| @@ -76,29 +90,17 @@ | @@ -76,29 +90,17 @@ | ||
| 76 | </view> | 90 | </view> |
| 77 | </view> | 91 | </view> |
| 78 | 92 | ||
| 79 | - <!-- 底部弹出命名弹窗 --> | 93 | + <!-- 命名弹窗 --> |
| 80 | <up-popup v-model:show="showNamePopup" mode="bottom" round> | 94 | <up-popup v-model:show="showNamePopup" mode="bottom" round> |
| 81 | <view class="popup-content" @click.stop> | 95 | <view class="popup-content" @click.stop> |
| 82 | - | ||
| 83 | <view class="popup-header"> | 96 | <view class="popup-header"> |
| 84 | - <!-- <text class="close-btn" @click="closePopup">x</text> --> | ||
| 85 | <up-icon name="close" color="#333" size="25" @click="closePopup"></up-icon> | 97 | <up-icon name="close" color="#333" size="25" @click="closePopup"></up-icon> |
| 86 | <text class="popup-title">命名超级组</text> | 98 | <text class="popup-title">命名超级组</text> |
| 87 | <text class="save-btn" @click="saveCombination">保存</text> | 99 | <text class="save-btn" @click="saveCombination">保存</text> |
| 88 | </view> | 100 | </view> |
| 89 | <view class="input-wrapper"> | 101 | <view class="input-wrapper"> |
| 90 | <textarea v-model="combinationName" class="name-input" placeholder="请输入超级组名称" maxlength="20" focus /> | 102 | <textarea v-model="combinationName" class="name-input" placeholder="请输入超级组名称" maxlength="20" focus /> |
| 91 | - <!-- <text class="hint">例如:胸肩超级组、背部组合训练</text> --> | ||
| 92 | - </view> | ||
| 93 | - | ||
| 94 | - <!-- <view class="preview-section"> | ||
| 95 | - <text class="preview-title">已选动作 ({{ selectedActions.length }})</text> | ||
| 96 | - <scroll-view scroll-x class="preview-scroll"> | ||
| 97 | - <view class="preview-tag" v-for="item in selectedActions" :key="item.id"> | ||
| 98 | - {{ item.name }} | ||
| 99 | </view> | 103 | </view> |
| 100 | - </scroll-view> | ||
| 101 | - </view> --> | ||
| 102 | </view> | 104 | </view> |
| 103 | </up-popup> | 105 | </up-popup> |
| 104 | </view> | 106 | </view> |
| @@ -106,48 +108,61 @@ | @@ -106,48 +108,61 @@ | ||
| 106 | 108 | ||
| 107 | <script setup> | 109 | <script setup> |
| 108 | import { ref, computed } from 'vue' | 110 | import { ref, computed } from 'vue' |
| 109 | -import { onLoad, onShow } from '@dcloudio/uni-app'; | 111 | +import { onLoad } from '@dcloudio/uni-app' |
| 110 | import { useActionStore } from '@/sheep/store/action' | 112 | import { useActionStore } from '@/sheep/store/action' |
| 111 | import ExercisesApi from '@/sheep/api/motion/exercises' | 113 | import ExercisesApi from '@/sheep/api/motion/exercises' |
| 112 | import SupersetsApi from '@/sheep/api/motion/supersets' | 114 | import SupersetsApi from '@/sheep/api/motion/supersets' |
| 113 | - | 115 | +import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea'; |
| 116 | +const menuButtonHeight = ref(getMenuButtonHeight()); | ||
| 117 | +const topSafeArea = ref(getTopSafeArea()); | ||
| 118 | +const PageHeight = ref(topSafeArea.value); | ||
| 119 | +// #ifdef MP-WEIXIN | ||
| 120 | +PageHeight.value = menuButtonHeight.value + topSafeArea.value; | ||
| 121 | +// #endif | ||
| 114 | const actionStore = useActionStore() | 122 | const actionStore = useActionStore() |
| 115 | 123 | ||
| 116 | -// 搜索 | ||
| 117 | -const searchValue = ref('') | 124 | +// ==================== 状态变量 ==================== |
| 118 | 125 | ||
| 119 | -// 左侧分类、部位筛选 | 126 | +// 左侧分类 |
| 120 | const navItems = ref([]) | 127 | const navItems = ref([]) |
| 121 | const activeNav = ref(null) | 128 | const activeNav = ref(null) |
| 129 | + | ||
| 130 | +// 部位筛选 | ||
| 122 | const motionPart = ref([]) | 131 | const motionPart = ref([]) |
| 123 | const activeMotionPart = ref('') | 132 | const activeMotionPart = ref('') |
| 124 | 133 | ||
| 125 | -// 原始全量动作、当前渲染动作 | 134 | +// 动作数据 |
| 126 | const exercises = ref([]) | 135 | const exercises = ref([]) |
| 127 | -const allExercises = ref([]) | 136 | +const loading = ref(false) |
| 137 | +const loadError = ref(false) | ||
| 128 | 138 | ||
| 129 | // 选中动作 | 139 | // 选中动作 |
| 130 | const selectedActions = ref([]) | 140 | const selectedActions = ref([]) |
| 131 | const selectedIds = computed(() => new Set(selectedActions.value.map(item => item.id))) | 141 | const selectedIds = computed(() => new Set(selectedActions.value.map(item => item.id))) |
| 132 | 142 | ||
| 133 | -// 命名弹窗控制 | 143 | +// 命名弹窗 |
| 134 | const showNamePopup = ref(false) | 144 | const showNamePopup = ref(false) |
| 135 | const combinationName = ref('') | 145 | const combinationName = ref('') |
| 136 | 146 | ||
| 137 | -const replaceAddshow = ref(false); | 147 | +// ==================== 生命周期 ==================== |
| 138 | 148 | ||
| 139 | -// 页面初始化(onLoad 页面生命周期) | ||
| 140 | onLoad(async () => { | 149 | onLoad(async () => { |
| 141 | selectedActions.value = [] | 150 | selectedActions.value = [] |
| 142 | combinationName.value = '' | 151 | combinationName.value = '' |
| 143 | await loadCategories() | 152 | await loadCategories() |
| 144 | }) | 153 | }) |
| 145 | 154 | ||
| 146 | -// 加载全部分类 | 155 | +// ==================== 数据加载 ==================== |
| 156 | + | ||
| 157 | +/** 加载全部分类 */ | ||
| 147 | const loadCategories = async () => { | 158 | const loadCategories = async () => { |
| 148 | try { | 159 | try { |
| 149 | await actionStore.getloadCategories() | 160 | await actionStore.getloadCategories() |
| 150 | - navItems.value = actionStore.showCategories | 161 | + // 过滤掉虚拟分类(如「超级组」id='super'),其 id 非数字,无法用于加载动作 |
| 162 | + navItems.value = actionStore.showCategories.filter(item => { | ||
| 163 | + const id = item.id | ||
| 164 | + return typeof id === 'number' || (typeof id === 'string' && /^\d+$/.test(id)) | ||
| 165 | + }) | ||
| 151 | if (navItems.value.length > 0) { | 166 | if (navItems.value.length > 0) { |
| 152 | switchNav(navItems.value[0].id) | 167 | switchNav(navItems.value[0].id) |
| 153 | } | 168 | } |
| @@ -156,77 +171,81 @@ const loadCategories = async () => { | @@ -156,77 +171,81 @@ const loadCategories = async () => { | ||
| 156 | } | 171 | } |
| 157 | } | 172 | } |
| 158 | 173 | ||
| 159 | -const addExercise = () => { | ||
| 160 | - replaceAddshow.value = !replaceAddshow.value; | ||
| 161 | -}; | ||
| 162 | - | ||
| 163 | -const addnewmotion = () => { | ||
| 164 | - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' }); | ||
| 165 | -}; | ||
| 166 | - | ||
| 167 | -// 切换左侧大分类 | 174 | +/** 切换左侧大分类 */ |
| 168 | const switchNav = async (id) => { | 175 | const switchNav = async (id) => { |
| 169 | if (activeNav.value === id) return | 176 | if (activeNav.value === id) return |
| 170 | activeNav.value = id | 177 | activeNav.value = id |
| 171 | activeMotionPart.value = '' | 178 | activeMotionPart.value = '' |
| 172 | - searchValue.value = '' | 179 | + loadError.value = false |
| 173 | await loadExercises(id) | 180 | await loadExercises(id) |
| 174 | } | 181 | } |
| 175 | 182 | ||
| 176 | -// 根据分类ID加载动作+部位 | 183 | +/** 根据分类ID加载动作和部位 */ |
| 177 | const loadExercises = async (categoriesId) => { | 184 | const loadExercises = async (categoriesId) => { |
| 185 | + loading.value = true | ||
| 186 | + loadError.value = false | ||
| 178 | try { | 187 | try { |
| 179 | - const partRes = await ExercisesApi.getMotionPart(categoriesId) | 188 | + const [partRes, exerciseRes] = await Promise.all([ |
| 189 | + ExercisesApi.getMotionPart(categoriesId), | ||
| 190 | + ExercisesApi.getexercises({ categoriesId }) | ||
| 191 | + ]) | ||
| 180 | motionPart.value = partRes.data || [] | 192 | motionPart.value = partRes.data || [] |
| 181 | - | ||
| 182 | - const res = await ExercisesApi.getexercises({ categoriesId }) | ||
| 183 | - exercises.value = res.data || [] | ||
| 184 | - allExercises.value = [...exercises.value] | 193 | + exercises.value = exerciseRes.data || [] |
| 185 | } catch (err) { | 194 | } catch (err) { |
| 186 | console.error('加载动作失败', err) | 195 | console.error('加载动作失败', err) |
| 196 | + loadError.value = true | ||
| 197 | + motionPart.value = [] | ||
| 198 | + exercises.value = [] | ||
| 199 | + } finally { | ||
| 200 | + loading.value = false | ||
| 187 | } | 201 | } |
| 188 | } | 202 | } |
| 189 | 203 | ||
| 190 | -// 子部位筛选 | 204 | +/** 按子部位筛选动作 */ |
| 191 | const handlePartClick = async (id) => { | 205 | const handlePartClick = async (id) => { |
| 192 | activeMotionPart.value = id | 206 | activeMotionPart.value = id |
| 207 | + loading.value = true | ||
| 208 | + loadError.value = false | ||
| 209 | + try { | ||
| 193 | const res = await ExercisesApi.getexercises({ | 210 | const res = await ExercisesApi.getexercises({ |
| 194 | categoriesId: activeNav.value, | 211 | categoriesId: activeNav.value, |
| 195 | subCategoriesId: id | 212 | subCategoriesId: id |
| 196 | }) | 213 | }) |
| 197 | exercises.value = res.data || [] | 214 | exercises.value = res.data || [] |
| 215 | + } catch (err) { | ||
| 216 | + console.error('筛选动作失败', err) | ||
| 217 | + uni.showToast({ title: '加载失败', icon: 'none' }) | ||
| 218 | + loadError.value = true | ||
| 219 | + } finally { | ||
| 220 | + loading.value = false | ||
| 221 | + } | ||
| 198 | } | 222 | } |
| 199 | 223 | ||
| 200 | -// 搜索过滤 | ||
| 201 | -const onSearch = () => { | ||
| 202 | - const keyword = searchValue.value.trim().toLowerCase() | ||
| 203 | - if (!keyword) { | ||
| 204 | - exercises.value = [...allExercises.value] | ||
| 205 | - return | 224 | +/** 重试加载 */ |
| 225 | +const retryLoad = () => { | ||
| 226 | + if (activeNav.value) { | ||
| 227 | + loadExercises(activeNav.value) | ||
| 206 | } | 228 | } |
| 207 | - exercises.value = allExercises.value.filter(item => | ||
| 208 | - item.exercises?.some(e => e.name.toLowerCase().includes(keyword)) | ||
| 209 | - ) | ||
| 210 | } | 229 | } |
| 211 | 230 | ||
| 212 | -// 点击选中/取消单个动作 | 231 | +// ==================== 操作交互 ==================== |
| 232 | + | ||
| 233 | +/** 选中/取消单个动作 */ | ||
| 213 | const toggleAction = (action) => { | 234 | const toggleAction = (action) => { |
| 214 | - console.log('点击的动作', action, '选中前数组', selectedActions.value) | ||
| 215 | const exist = selectedIds.value.has(action.id) | 235 | const exist = selectedIds.value.has(action.id) |
| 216 | if (exist) { | 236 | if (exist) { |
| 217 | selectedActions.value = selectedActions.value.filter(i => i.id !== action.id) | 237 | selectedActions.value = selectedActions.value.filter(i => i.id !== action.id) |
| 218 | } else { | 238 | } else { |
| 219 | - selectedActions.value.push({ ...action }) | 239 | + selectedActions.value = [...selectedActions.value, { ...action }] |
| 220 | } | 240 | } |
| 221 | - console.log('选中后', selectedActions.value) | ||
| 222 | } | 241 | } |
| 223 | 242 | ||
| 224 | -// 底部标签移除动作 | 243 | +/** 移除已选动作 */ |
| 225 | const removeAction = (action) => { | 244 | const removeAction = (action) => { |
| 226 | selectedActions.value = selectedActions.value.filter(i => i.id !== action.id) | 245 | selectedActions.value = selectedActions.value.filter(i => i.id !== action.id) |
| 227 | } | 246 | } |
| 228 | 247 | ||
| 229 | -// 打开命名弹窗 | 248 | +/** 打开命名弹窗 */ |
| 230 | const combineActions = () => { | 249 | const combineActions = () => { |
| 231 | if (selectedActions.value.length < 2) { | 250 | if (selectedActions.value.length < 2) { |
| 232 | uni.showToast({ title: '至少选择2个动作', icon: 'none' }) | 251 | uni.showToast({ title: '至少选择2个动作', icon: 'none' }) |
| @@ -236,12 +255,12 @@ const combineActions = () => { | @@ -236,12 +255,12 @@ const combineActions = () => { | ||
| 236 | showNamePopup.value = true | 255 | showNamePopup.value = true |
| 237 | } | 256 | } |
| 238 | 257 | ||
| 239 | -// 关闭弹窗 | 258 | +/** 关闭弹窗 */ |
| 240 | const closePopup = () => { | 259 | const closePopup = () => { |
| 241 | showNamePopup.value = false | 260 | showNamePopup.value = false |
| 242 | } | 261 | } |
| 243 | 262 | ||
| 244 | -// 【原有创建超级组接口不动】保存超级组 | 263 | +/** 保存超级组 */ |
| 245 | const saveCombination = async () => { | 264 | const saveCombination = async () => { |
| 246 | const name = combinationName.value.trim() | 265 | const name = combinationName.value.trim() |
| 247 | if (!name) { | 266 | if (!name) { |
| @@ -258,7 +277,6 @@ const saveCombination = async () => { | @@ -258,7 +277,6 @@ const saveCombination = async () => { | ||
| 258 | uni.showToast({ title: '创建成功', icon: 'success' }) | 277 | uni.showToast({ title: '创建成功', icon: 'success' }) |
| 259 | selectedActions.value = [] | 278 | selectedActions.value = [] |
| 260 | closePopup() | 279 | closePopup() |
| 261 | - // 创建成功返回上一页 | ||
| 262 | setTimeout(() => uni.navigateBack(), 800) | 280 | setTimeout(() => uni.navigateBack(), 800) |
| 263 | } else { | 281 | } else { |
| 264 | uni.showToast({ title: res.msg || '创建失败', icon: 'none' }) | 282 | uni.showToast({ title: res.msg || '创建失败', icon: 'none' }) |
| @@ -268,99 +286,83 @@ const saveCombination = async () => { | @@ -268,99 +286,83 @@ const saveCombination = async () => { | ||
| 268 | } | 286 | } |
| 269 | } | 287 | } |
| 270 | 288 | ||
| 271 | -// 返回上一页 | 289 | +/** 返回上一页 */ |
| 272 | const backPage = () => { | 290 | const backPage = () => { |
| 273 | uni.navigateBack() | 291 | uni.navigateBack() |
| 274 | } | 292 | } |
| 275 | </script> | 293 | </script> |
| 276 | 294 | ||
| 277 | <style lang="scss" scoped> | 295 | <style lang="scss" scoped> |
| 278 | -page { | ||
| 279 | - background-color: #f5f5f5; | ||
| 280 | -} | 296 | +// ==================== SCSS 变量 ==================== |
| 297 | +$color-primary: #f8d714; | ||
| 298 | +$color-bg: #f5f5f5; | ||
| 299 | +$color-bg-nav: #fafafa; | ||
| 300 | +$color-text-primary: #333; | ||
| 301 | +$color-text-secondary: #666; | ||
| 302 | +$color-text-muted: #999; | ||
| 303 | +$color-white: #fff; | ||
| 304 | +$color-dark-bar: #2c2c2e; | ||
| 305 | +$color-dark-tag: #444; | ||
| 306 | +$color-border: #eee; | ||
| 307 | +$color-disabled-bg: #eee; | ||
| 308 | +$color-disabled-text: #bbb; | ||
| 309 | + | ||
| 310 | +$z-fixed: 100; | ||
| 311 | + | ||
| 312 | +$radius-sm: 12rpx; | ||
| 313 | +$radius-md: 16rpx; | ||
| 314 | +$radius-lg: 24rpx; | ||
| 315 | +$radius-round: 40rpx; | ||
| 316 | + | ||
| 317 | +$spacing-xs: 8rpx; | ||
| 318 | +$spacing-sm: 16rpx; | ||
| 319 | +$spacing-md: 20rpx; | ||
| 320 | +$spacing-lg: 30rpx; | ||
| 321 | + | ||
| 322 | +$font-xs: 24rpx; | ||
| 323 | +$font-sm: 26rpx; | ||
| 324 | +$font-md: 28rpx; | ||
| 325 | +$font-lg: 30rpx; | ||
| 326 | +$font-xl: 32rpx; | ||
| 327 | + | ||
| 328 | + | ||
| 281 | 329 | ||
| 282 | .page-wrap { | 330 | .page-wrap { |
| 283 | width: 100%; | 331 | width: 100%; |
| 284 | - min-height: 100vh; | 332 | + height: 100vh; |
| 333 | + // #ifdef MP-WEIXIN | ||
| 334 | + height: 88vh; | ||
| 335 | + // #endif | ||
| 285 | display: flex; | 336 | display: flex; |
| 286 | flex-direction: column; | 337 | flex-direction: column; |
| 338 | + overflow: hidden; | ||
| 287 | } | 339 | } |
| 288 | 340 | ||
| 289 | -.add-btn-wrapper { | ||
| 290 | - position: fixed; | ||
| 291 | - left: 28rpx; | ||
| 292 | - top: calc(var(--status-bar-height) + 12rpx); | ||
| 293 | - z-index: 9999; | ||
| 294 | - width: 50rpx; | ||
| 295 | - height: 50rpx; | ||
| 296 | -} | ||
| 297 | - | ||
| 298 | -.search-bar { | ||
| 299 | - padding-top: var(--status-bar-height); | ||
| 300 | - height: 88rpx; | ||
| 301 | - background: #fff; | ||
| 302 | -} | ||
| 303 | - | ||
| 304 | -.floating-menu { | ||
| 305 | - position: absolute; | ||
| 306 | - top: 90rpx; | ||
| 307 | - left: 20rpx; | ||
| 308 | - background: #fff; | ||
| 309 | - border-radius: 12rpx; | ||
| 310 | - box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); | ||
| 311 | - padding: 20rpx; | ||
| 312 | - z-index: 9999; | ||
| 313 | - width: 260rpx; | ||
| 314 | -} | ||
| 315 | - | ||
| 316 | -.menu-item { | ||
| 317 | - display: flex; | ||
| 318 | - align-items: center; | ||
| 319 | - gap: 16rpx; | ||
| 320 | -} | ||
| 321 | - | ||
| 322 | -.menu-text { | ||
| 323 | - box-sizing: border-box; | ||
| 324 | - display: flex; | ||
| 325 | - font-size: 28rpx; | ||
| 326 | - // margin-left: 200rpx; | ||
| 327 | -} | ||
| 328 | - | ||
| 329 | - | ||
| 330 | -// .search-bar { | ||
| 331 | -// padding: 20rpx 30rpx; | ||
| 332 | -// background: #fff; | ||
| 333 | - | ||
| 334 | -// .search-input { | ||
| 335 | -// width: 100%; | ||
| 336 | -// height: 70rpx; | ||
| 337 | -// border-radius: 35rpx; | ||
| 338 | -// background: #f7f7f7; | ||
| 339 | -// padding: 0 24rpx; | ||
| 340 | -// font-size: 28rpx; | ||
| 341 | -// border: 1rpx solid #eee; | ||
| 342 | -// } | ||
| 343 | -// } | ||
| 344 | - | 341 | +// ==================== 主体布局 ==================== |
| 345 | .layout-container { | 342 | .layout-container { |
| 346 | - flex: 1; | 343 | + |
| 344 | + min-height: 0; | ||
| 347 | display: flex; | 345 | display: flex; |
| 348 | overflow: hidden; | 346 | overflow: hidden; |
| 349 | - padding-bottom: 130rpx; | 347 | + padding-bottom: calc(100rpx + 30rpx); // 底部按钮栏 + 间距 |
| 348 | +} | ||
| 350 | 349 | ||
| 351 | - .left-nav { | ||
| 352 | - width: 150rpx; | ||
| 353 | - background: #fafafa; | ||
| 354 | - border-right: 1rpx solid #eee; | 350 | +// 左侧导航 |
| 351 | +.left-nav { | ||
| 352 | + width: 180rpx; | ||
| 353 | + background: $color-bg-nav; | ||
| 354 | + border-right: 1rpx solid $color-border; | ||
| 355 | 355 | ||
| 356 | .nav-item { | 356 | .nav-item { |
| 357 | - padding: 30rpx 20rpx; | ||
| 358 | - font-size: 26rpx; | ||
| 359 | - color: #666; | 357 | + padding: $spacing-lg $spacing-sm; |
| 358 | + font-size: $font-sm; | ||
| 359 | + color: $color-text-secondary; | ||
| 360 | position: relative; | 360 | position: relative; |
| 361 | + white-space: nowrap; | ||
| 362 | + text-align: center; | ||
| 361 | 363 | ||
| 362 | &.active { | 364 | &.active { |
| 363 | - background: #fff; | 365 | + background: $color-white; |
| 364 | color: #000; | 366 | color: #000; |
| 365 | font-weight: 500; | 367 | font-weight: 500; |
| 366 | 368 | ||
| @@ -372,230 +374,263 @@ page { | @@ -372,230 +374,263 @@ page { | ||
| 372 | transform: translateY(-50%); | 374 | transform: translateY(-50%); |
| 373 | width: 6rpx; | 375 | width: 6rpx; |
| 374 | height: 36rpx; | 376 | height: 36rpx; |
| 375 | - background: #f8d714; | ||
| 376 | - } | 377 | + background: $color-primary; |
| 377 | } | 378 | } |
| 378 | } | 379 | } |
| 379 | } | 380 | } |
| 381 | +} | ||
| 380 | 382 | ||
| 381 | - .right-content { | 383 | +// 右侧内容 |
| 384 | +.right-content { | ||
| 382 | flex: 1; | 385 | flex: 1; |
| 383 | - padding: 20rpx; | 386 | + padding: $spacing-md; |
| 387 | + padding-top: $spacing-lg; | ||
| 384 | 388 | ||
| 385 | - .tip { | 389 | + // 通用内容提示块 |
| 390 | + .content-tip { | ||
| 386 | display: flex; | 391 | display: flex; |
| 387 | - flex-wrap: wrap; | 392 | + flex-direction: column; |
| 393 | + align-items: center; | ||
| 394 | + justify-content: center; | ||
| 395 | + padding: 80rpx $spacing-lg; | ||
| 396 | + text-align: center; | ||
| 397 | + | ||
| 398 | + .tip-text { | ||
| 399 | + font-size: $font-md; | ||
| 400 | + color: $color-text-muted; | ||
| 401 | + } | ||
| 402 | + | ||
| 403 | + .tip-subtext { | ||
| 404 | + margin-top: $spacing-sm; | ||
| 405 | + font-size: $font-xs; | ||
| 406 | + color: #bbb; | ||
| 407 | + } | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + // 初始引导 | ||
| 411 | + .init-guide { | ||
| 412 | + .tip-text { | ||
| 413 | + font-size: $font-lg; | ||
| 414 | + color: $color-text-secondary; | ||
| 415 | + font-weight: 500; | ||
| 416 | + } | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + // 错误重试按钮 | ||
| 420 | + .retry-btn { | ||
| 421 | + margin-top: $spacing-md; | ||
| 422 | + padding: 12rpx 40rpx; | ||
| 423 | + font-size: $font-sm; | ||
| 424 | + color: #000; | ||
| 425 | + background: $color-primary; | ||
| 426 | + border-radius: $radius-round; | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + // 部位筛选标签(横向滚动) | ||
| 430 | + .part-filter { | ||
| 431 | + margin-bottom: $spacing-md; | ||
| 432 | + white-space: nowrap; | ||
| 433 | + | ||
| 434 | + .part-filter-inner { | ||
| 435 | + display: inline-flex; | ||
| 436 | + flex-wrap: nowrap; | ||
| 388 | gap: 15rpx; | 437 | gap: 15rpx; |
| 389 | - margin-bottom: 20rpx; | 438 | + } |
| 390 | 439 | ||
| 391 | - .item { | ||
| 392 | - padding: 10rpx 20rpx; | ||
| 393 | - background: #fff; | ||
| 394 | - border-radius: 16rpx; | ||
| 395 | - font-size: 24rpx; | 440 | + .filter-item { |
| 441 | + flex-shrink: 0; | ||
| 442 | + padding: 10rpx 24rpx; | ||
| 443 | + background: $color-white; | ||
| 444 | + border-radius: $radius-sm; | ||
| 445 | + font-size: $font-xs; | ||
| 446 | + color: $color-text-secondary; | ||
| 447 | + transition: all 0.2s; | ||
| 396 | 448 | ||
| 397 | &.active { | 449 | &.active { |
| 398 | - background: #f8d714; | 450 | + background: $color-primary; |
| 399 | color: #000; | 451 | color: #000; |
| 452 | + font-weight: 500; | ||
| 400 | } | 453 | } |
| 401 | } | 454 | } |
| 402 | } | 455 | } |
| 403 | 456 | ||
| 457 | + // 部位筛选为空 | ||
| 458 | + .part-empty-tip { | ||
| 459 | + padding: 30rpx $spacing-lg 40rpx; | ||
| 460 | + } | ||
| 461 | + | ||
| 404 | .equipment-item { | 462 | .equipment-item { |
| 405 | - margin-bottom: 30rpx; | 463 | + margin-bottom: $spacing-lg; |
| 406 | 464 | ||
| 407 | .equipment-name { | 465 | .equipment-name { |
| 408 | - font-size: 28rpx; | 466 | + font-size: $font-md; |
| 409 | font-weight: 500; | 467 | font-weight: 500; |
| 410 | - margin-bottom: 16rpx; | 468 | + color: $color-text-secondary; |
| 469 | + margin-bottom: $spacing-sm; | ||
| 470 | + padding-left: $spacing-xs; | ||
| 471 | + border-left: 6rpx solid $color-primary; | ||
| 411 | } | 472 | } |
| 412 | 473 | ||
| 413 | .action-list { | 474 | .action-list { |
| 414 | display: grid; | 475 | display: grid; |
| 415 | grid-template-columns: repeat(2, 1fr); | 476 | grid-template-columns: repeat(2, 1fr); |
| 416 | - gap: 16rpx; | 477 | + gap: $spacing-sm; |
| 417 | 478 | ||
| 418 | .action-item { | 479 | .action-item { |
| 419 | - background: #fff; | ||
| 420 | - border-radius: 16rpx; | ||
| 421 | - padding: 16rpx; | 480 | + background: $color-white; |
| 481 | + border-radius: $radius-md; | ||
| 482 | + padding: $spacing-sm; | ||
| 422 | display: flex; | 483 | display: flex; |
| 423 | flex-direction: column; | 484 | flex-direction: column; |
| 424 | align-items: center; | 485 | align-items: center; |
| 425 | - gap: 8rpx; | 486 | + gap: $spacing-xs; |
| 487 | + border: 2rpx solid transparent; | ||
| 488 | + transition: border-color 0.2s, transform 0.15s; | ||
| 426 | 489 | ||
| 427 | &.selected { | 490 | &.selected { |
| 428 | - border: 2rpx solid #f8d714; | 491 | + border-color: $color-primary; |
| 492 | + } | ||
| 493 | + | ||
| 494 | + &:active { | ||
| 495 | + transform: scale(0.97); | ||
| 429 | } | 496 | } |
| 430 | 497 | ||
| 431 | .action-img { | 498 | .action-img { |
| 432 | width: 100%; | 499 | width: 100%; |
| 433 | height: 200rpx; | 500 | height: 200rpx; |
| 434 | - border-radius: 12rpx; | 501 | + border-radius: $radius-sm; |
| 435 | } | 502 | } |
| 436 | 503 | ||
| 437 | .action-name { | 504 | .action-name { |
| 438 | - font-size: 24rpx; | ||
| 439 | - color: #333; | 505 | + font-size: $font-xs; |
| 506 | + color: $color-text-primary; | ||
| 440 | } | 507 | } |
| 441 | } | 508 | } |
| 442 | } | 509 | } |
| 443 | } | 510 | } |
| 444 | 511 | ||
| 512 | + // 无动作空状态 | ||
| 445 | .empty-state { | 513 | .empty-state { |
| 446 | padding-top: 100rpx; | 514 | padding-top: 100rpx; |
| 447 | - text-align: center; | ||
| 448 | - color: #999; | ||
| 449 | - font-size: 28rpx; | ||
| 450 | - } | ||
| 451 | } | 515 | } |
| 452 | } | 516 | } |
| 453 | 517 | ||
| 518 | +// ==================== 底部已选标签栏 ==================== | ||
| 454 | .selected-bar { | 519 | .selected-bar { |
| 455 | position: fixed; | 520 | position: fixed; |
| 456 | left: 0; | 521 | left: 0; |
| 457 | right: 0; | 522 | right: 0; |
| 458 | bottom: calc(100rpx + env(safe-area-inset-bottom)); | 523 | bottom: calc(100rpx + env(safe-area-inset-bottom)); |
| 459 | - background: #2c2c2e; | ||
| 460 | - padding: 20rpx 30rpx; | ||
| 461 | - display: flex; | ||
| 462 | - gap: 20rpx; | 524 | + background: $color-dark-bar; |
| 525 | + padding: 20rpx 20rpx 40rpx; | ||
| 463 | white-space: nowrap; | 526 | white-space: nowrap; |
| 527 | + z-index: $z-fixed; | ||
| 528 | + | ||
| 529 | + .selected-bar-inner { | ||
| 530 | + display: inline-flex; | ||
| 531 | + flex-wrap: nowrap; | ||
| 532 | + gap: $spacing-md; | ||
| 533 | + } | ||
| 464 | 534 | ||
| 465 | .selected-item { | 535 | .selected-item { |
| 466 | - background: #444; | ||
| 467 | - color: #fff; | ||
| 468 | - padding: 10rpx 20rpx; | 536 | + flex-shrink: 0; |
| 537 | + background: $color-dark-tag; | ||
| 538 | + color: $color-white; | ||
| 539 | + padding: 10rpx $spacing-md; | ||
| 469 | border-radius: 30rpx; | 540 | border-radius: 30rpx; |
| 470 | display: flex; | 541 | display: flex; |
| 471 | align-items: center; | 542 | align-items: center; |
| 472 | gap: 10rpx; | 543 | gap: 10rpx; |
| 473 | 544 | ||
| 474 | .name { | 545 | .name { |
| 475 | - font-size: 26rpx; | 546 | + font-size: $font-sm; |
| 476 | } | 547 | } |
| 477 | } | 548 | } |
| 478 | } | 549 | } |
| 479 | 550 | ||
| 551 | +// ==================== 底部操作按钮 ==================== | ||
| 480 | .bottom-btn-bar { | 552 | .bottom-btn-bar { |
| 481 | position: fixed; | 553 | position: fixed; |
| 482 | bottom: 0; | 554 | bottom: 0; |
| 483 | left: 0; | 555 | left: 0; |
| 484 | right: 0; | 556 | right: 0; |
| 485 | display: flex; | 557 | display: flex; |
| 486 | - gap: 20rpx; | ||
| 487 | - padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom)); | ||
| 488 | - background: #fff; | 558 | + gap: $spacing-md; |
| 559 | + padding: $spacing-md $spacing-lg calc(20rpx + env(safe-area-inset-bottom)); | ||
| 560 | + background: $color-white; | ||
| 489 | box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); | 561 | box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); |
| 562 | + z-index: $z-fixed; | ||
| 490 | 563 | ||
| 491 | .btn { | 564 | .btn { |
| 492 | flex: 1; | 565 | flex: 1; |
| 493 | height: 80rpx; | 566 | height: 80rpx; |
| 494 | - border-radius: 40rpx; | 567 | + border-radius: $radius-round; |
| 495 | display: flex; | 568 | display: flex; |
| 496 | align-items: center; | 569 | align-items: center; |
| 497 | justify-content: center; | 570 | justify-content: center; |
| 498 | - font-size: 30rpx; | 571 | + font-size: $font-lg; |
| 499 | font-weight: bold; | 572 | font-weight: bold; |
| 500 | } | 573 | } |
| 501 | 574 | ||
| 502 | .cancel-btn { | 575 | .cancel-btn { |
| 503 | - background: #f5f5f5; | ||
| 504 | - color: #333; | 576 | + background: $color-bg; |
| 577 | + color: $color-text-primary; | ||
| 505 | } | 578 | } |
| 506 | 579 | ||
| 507 | .confirm-btn { | 580 | .confirm-btn { |
| 508 | - background: #f8d714; | 581 | + background: $color-primary; |
| 509 | color: #000; | 582 | color: #000; |
| 510 | 583 | ||
| 511 | &.disabled { | 584 | &.disabled { |
| 512 | - background: #eee; | ||
| 513 | - color: #bbb; | 585 | + background: $color-disabled-bg; |
| 586 | + color: $color-disabled-text; | ||
| 514 | } | 587 | } |
| 515 | } | 588 | } |
| 516 | } | 589 | } |
| 517 | 590 | ||
| 591 | +// ==================== 命名弹窗 ==================== | ||
| 518 | .popup-content { | 592 | .popup-content { |
| 519 | width: 100%; | 593 | width: 100%; |
| 520 | - background: #fff; | 594 | + background: $color-white; |
| 521 | box-sizing: border-box; | 595 | box-sizing: border-box; |
| 522 | - border-radius: 24rpx 24rpx 0 0; | ||
| 523 | - padding: 30rpx; | ||
| 524 | - height: 70vh; | 596 | + border-radius: $radius-lg $radius-lg 0 0; |
| 597 | + padding: $spacing-lg; | ||
| 598 | + max-height: 70vh; | ||
| 525 | 599 | ||
| 526 | .popup-header { | 600 | .popup-header { |
| 527 | display: flex; | 601 | display: flex; |
| 528 | justify-content: space-between; | 602 | justify-content: space-between; |
| 529 | - // align-items: center; | ||
| 530 | - padding-bottom: 20rpx; | ||
| 531 | - margin-bottom: 30rpx; | ||
| 532 | - | ||
| 533 | - .close-btn { | ||
| 534 | - font-size: 32rpx; | ||
| 535 | - color: #333; | ||
| 536 | - background: none; | ||
| 537 | - } | 603 | + align-items: center; |
| 604 | + padding-bottom: $spacing-md; | ||
| 605 | + margin-bottom: $spacing-lg; | ||
| 538 | 606 | ||
| 539 | .save-btn { | 607 | .save-btn { |
| 540 | - font-size: 28rpx; | ||
| 541 | - background: #f8d714; | 608 | + font-size: $font-md; |
| 609 | + background: $color-primary; | ||
| 542 | padding: 12rpx 32rpx; | 610 | padding: 12rpx 32rpx; |
| 543 | border-radius: 32rpx; | 611 | border-radius: 32rpx; |
| 544 | color: #000; | 612 | color: #000; |
| 545 | } | 613 | } |
| 546 | 614 | ||
| 547 | .popup-title { | 615 | .popup-title { |
| 548 | - font-size: 32rpx; | 616 | + font-size: $font-xl; |
| 549 | font-weight: bold; | 617 | font-weight: bold; |
| 550 | - | ||
| 551 | } | 618 | } |
| 552 | } | 619 | } |
| 553 | 620 | ||
| 554 | .input-wrapper { | 621 | .input-wrapper { |
| 555 | - margin-bottom: 30rpx; | ||
| 556 | - display: flex; | ||
| 557 | - // justify-content: center; | 622 | + margin-bottom: $spacing-lg; |
| 558 | 623 | ||
| 559 | .name-input { | 624 | .name-input { |
| 560 | width: 100%; | 625 | width: 100%; |
| 561 | height: 300rpx; | 626 | height: 300rpx; |
| 562 | - // border: 1rpx solid #ffffff; | ||
| 563 | background-color: #ececec; | 627 | background-color: #ececec; |
| 564 | - border-radius: 16rpx; | 628 | + border-radius: $radius-md; |
| 565 | padding: 24rpx; | 629 | padding: 24rpx; |
| 566 | - font-size: 28rpx; | 630 | + font-size: $font-md; |
| 567 | text-align: left; | 631 | text-align: left; |
| 568 | vertical-align: top; | 632 | vertical-align: top; |
| 569 | } | 633 | } |
| 570 | - | ||
| 571 | - .hint { | ||
| 572 | - font-size: 24rpx; | ||
| 573 | - color: #999; | ||
| 574 | - margin-top: 12rpx; | ||
| 575 | - } | ||
| 576 | - } | ||
| 577 | - | ||
| 578 | - .preview-section { | ||
| 579 | - .preview-title { | ||
| 580 | - font-size: 28rpx; | ||
| 581 | - margin-bottom: 16rpx; | ||
| 582 | - color: #666; | ||
| 583 | - } | ||
| 584 | - | ||
| 585 | - .preview-scroll { | ||
| 586 | - display: flex; | ||
| 587 | - gap: 16rpx; | ||
| 588 | - overflow-x: auto; | ||
| 589 | - | ||
| 590 | - .preview-tag { | ||
| 591 | - background: #f0f9ff; | ||
| 592 | - color: #f8d714; | ||
| 593 | - padding: 8rpx 20rpx; | ||
| 594 | - border-radius: 50rpx; | ||
| 595 | - font-size: 24rpx; | ||
| 596 | - white-space: nowrap; | ||
| 597 | - } | ||
| 598 | - } | ||
| 599 | } | 634 | } |
| 600 | } | 635 | } |
| 601 | </style> | 636 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="template-edit-page" @click="dismissGuide"> | 2 | + <view class="template-edit-page" @click="dismissGuide" :style="{ paddingTop: PageHeight + 'px' }"> |
| 3 | <!-- 顶部导航 --> | 3 | <!-- 顶部导航 --> |
| 4 | <view class="header"> | 4 | <view class="header"> |
| 5 | <view class="close-btn" @click="goBack"> | 5 | <view class="close-btn" @click="goBack"> |
| @@ -16,26 +16,19 @@ | @@ -16,26 +16,19 @@ | ||
| 16 | </text> | 16 | </text> |
| 17 | </view> | 17 | </view> |
| 18 | 18 | ||
| 19 | - <view | ||
| 20 | - class="save-btn" | ||
| 21 | - :class="{ disabled: unitCount === 0, active: unitCount > 0 }" | ||
| 22 | - @click="handleSave" | ||
| 23 | - > | 19 | + <view class="save-btn" :class="{ disabled: unitCount === 0, active: unitCount > 0 }" @click="handleSave"> |
| 24 | 保存模板 | 20 | 保存模板 |
| 25 | </view> | 21 | </view> |
| 26 | </view> | 22 | </view> |
| 27 | 23 | ||
| 28 | <!-- 内容区 --> | 24 | <!-- 内容区 --> |
| 29 | - <view class="content-area"> | 25 | + <view class="content-wrapper"> |
| 26 | + <scroll-view scroll-y class="content-area"> | ||
| 30 | <template v-if="unitCount"> | 27 | <template v-if="unitCount"> |
| 31 | <view v-for="(unit, index) in units" :key="unit.unitId"> | 28 | <view v-for="(unit, index) in units" :key="unit.unitId"> |
| 32 | - <dongzuo | ||
| 33 | - :id="unit.unitId" | ||
| 34 | - :type="unit.unitType" | ||
| 35 | - :action-detail="trainingStore.convertUnitToActionDetail(unit)" | ||
| 36 | - :ref="(el) => setActionRef(el, index)" | ||
| 37 | - :unit-index="index" | ||
| 38 | - /> | 29 | + <dongzuo :id="unit.unitId" :type="unit.unitType" |
| 30 | + :action-detail="trainingStore.convertUnitToActionDetail(unit)" :ref="(el) => setActionRef(el, index)" | ||
| 31 | + :unit-index="index" :isTick="false" /> | ||
| 39 | </view> | 32 | </view> |
| 40 | </template> | 33 | </template> |
| 41 | 34 | ||
| @@ -45,6 +38,7 @@ | @@ -45,6 +38,7 @@ | ||
| 45 | 点击最下方 + 开始添加动作 | 38 | 点击最下方 + 开始添加动作 |
| 46 | </view> | 39 | </view> |
| 47 | </view> | 40 | </view> |
| 41 | + </scroll-view> | ||
| 48 | </view> | 42 | </view> |
| 49 | 43 | ||
| 50 | <!-- 引导气泡 --> | 44 | <!-- 引导气泡 --> |
| @@ -81,7 +75,7 @@ | @@ -81,7 +75,7 @@ | ||
| 81 | </view> | 75 | </view> |
| 82 | 76 | ||
| 83 | <!-- 文本编辑弹窗 --> | 77 | <!-- 文本编辑弹窗 --> |
| 84 | - <view v-if="popupType" class="text-popup"> | 78 | + <up-popup :show="!!popupType" mode="bottom" round="24rpx" @close="closeTextPopup" bg-color="#333"> |
| 85 | <view class="popup-header"> | 79 | <view class="popup-header"> |
| 86 | <view class="popup-close-btn" @click="closeTextPopup"> | 80 | <view class="popup-close-btn" @click="closeTextPopup"> |
| 87 | <up-icon name="close" color="#fff" size="20" bold /> | 81 | <up-icon name="close" color="#fff" size="20" bold /> |
| @@ -90,28 +84,15 @@ | @@ -90,28 +84,15 @@ | ||
| 90 | <view class="popup-save-btn" @click="saveTextPopup">保存</view> | 84 | <view class="popup-save-btn" @click="saveTextPopup">保存</view> |
| 91 | </view> | 85 | </view> |
| 92 | <view class="popup-content"> | 86 | <view class="popup-content"> |
| 93 | - <textarea | ||
| 94 | - class="intro-textarea" | ||
| 95 | - v-model="popupValue" | ||
| 96 | - :placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'" | ||
| 97 | - /> | ||
| 98 | - </view> | 87 | + <textarea class="intro-textarea" v-model="popupValue" |
| 88 | + :placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'" /> | ||
| 99 | </view> | 89 | </view> |
| 90 | + </up-popup> | ||
| 100 | 91 | ||
| 101 | <!-- 场景选择器 --> | 92 | <!-- 场景选择器 --> |
| 102 | - <up-picker | ||
| 103 | - :show="showScenePicker" | ||
| 104 | - :columns="sceneColumns" | ||
| 105 | - :default-index="[0]" | ||
| 106 | - title="训练场景" | ||
| 107 | - title-color="#ffffff" | ||
| 108 | - cancel-color="#cccccc" | ||
| 109 | - confirm-color="#409eff" | ||
| 110 | - active-color="#ffd100" | ||
| 111 | - @confirm="handleSceneConfirm" | ||
| 112 | - @cancel="showScenePicker = false" | ||
| 113 | - close-on-click-overlay | ||
| 114 | - /> | 93 | + <up-picker :show="showScenePicker" :columns="sceneColumns" :default-index="[0]" title="训练场景" title-color="#ffffff" |
| 94 | + cancel-color="#cccccc" confirm-color="#409eff" active-color="#ffd100" @confirm="handleSceneConfirm" | ||
| 95 | + @cancel="showScenePicker = false" close-on-click-overlay /> | ||
| 115 | 96 | ||
| 116 | <!-- 动作排序 / 新增 --> | 97 | <!-- 动作排序 / 新增 --> |
| 117 | <action-sort ref="actionSortRef" @open-add-actions="addExShow = true" /> | 98 | <action-sort ref="actionSortRef" @open-add-actions="addExShow = true" /> |
| @@ -131,11 +112,17 @@ import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vu | @@ -131,11 +112,17 @@ import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vu | ||
| 131 | import { useTrainingStore } from '@/sheep/store/trainingStore'; | 112 | import { useTrainingStore } from '@/sheep/store/trainingStore'; |
| 132 | import FileApi from '@/sheep/api/infra/file'; | 113 | import FileApi from '@/sheep/api/infra/file'; |
| 133 | import TemplatesApi from '@/sheep/api/Template/Templates'; | 114 | import TemplatesApi from '@/sheep/api/Template/Templates'; |
| 134 | - | 115 | +import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea'; |
| 116 | +const menuButtonHeight = ref(getMenuButtonHeight()); | ||
| 117 | +const topSafeArea = ref(getTopSafeArea()); | ||
| 118 | +const PageHeight = ref(topSafeArea.value); | ||
| 119 | +// #ifdef MP-WEIXIN | ||
| 120 | +PageHeight.value = menuButtonHeight.value + topSafeArea.value; | ||
| 121 | +// #endif | ||
| 135 | dayjs.extend(duration); | 122 | dayjs.extend(duration); |
| 136 | 123 | ||
| 137 | // ---- 常量 ---- | 124 | // ---- 常量 ---- |
| 138 | -const NAVIGATE_DELAY = 1500; | 125 | +const NAVIGATE_DELAY = 1000; |
| 139 | 126 | ||
| 140 | const SCENE_LIST = [ | 127 | const SCENE_LIST = [ |
| 141 | { id: 0, title: '不限' }, | 128 | { id: 0, title: '不限' }, |
| @@ -367,7 +354,6 @@ $radius-circle: 50%; | @@ -367,7 +354,6 @@ $radius-circle: 50%; | ||
| 367 | .template-edit-page { | 354 | .template-edit-page { |
| 368 | position: relative; | 355 | position: relative; |
| 369 | width: 100%; | 356 | width: 100%; |
| 370 | - height: 100vh; | ||
| 371 | background-color: $color-bg-primary; | 357 | background-color: $color-bg-primary; |
| 372 | display: flex; | 358 | display: flex; |
| 373 | flex-direction: column; | 359 | flex-direction: column; |
| @@ -438,14 +424,20 @@ $radius-circle: 50%; | @@ -438,14 +424,20 @@ $radius-circle: 50%; | ||
| 438 | } | 424 | } |
| 439 | 425 | ||
| 440 | // ---- 内容区 ---- | 426 | // ---- 内容区 ---- |
| 441 | -.content-area { | ||
| 442 | - flex: 1; | ||
| 443 | - overflow-y: auto; | 427 | +.content-wrapper { |
| 428 | + | ||
| 429 | + height: 83vh; | ||
| 444 | padding-bottom: 20rpx; | 430 | padding-bottom: 20rpx; |
| 431 | + box-sizing: border-box; | ||
| 432 | + overflow: hidden; | ||
| 433 | +} | ||
| 434 | + | ||
| 435 | +.content-area { | ||
| 436 | + height: 100%; | ||
| 445 | } | 437 | } |
| 446 | 438 | ||
| 447 | .empty-area { | 439 | .empty-area { |
| 448 | - flex: 1; | 440 | + height: 100%; |
| 449 | display: flex; | 441 | display: flex; |
| 450 | align-items: center; | 442 | align-items: center; |
| 451 | justify-content: center; | 443 | justify-content: center; |
| @@ -461,8 +453,8 @@ $radius-circle: 50%; | @@ -461,8 +453,8 @@ $radius-circle: 50%; | ||
| 461 | 453 | ||
| 462 | // ---- 引导气泡 ---- | 454 | // ---- 引导气泡 ---- |
| 463 | .guide-bubble { | 455 | .guide-bubble { |
| 464 | - position: absolute; | ||
| 465 | - bottom: 130rpx; | 456 | + position: fixed; |
| 457 | + bottom: 200rpx; | ||
| 466 | left: 50%; | 458 | left: 50%; |
| 467 | transform: translateX(-50%); | 459 | transform: translateX(-50%); |
| 468 | background-color: #000; | 460 | background-color: #000; |
| @@ -485,6 +477,10 @@ $radius-circle: 50%; | @@ -485,6 +477,10 @@ $radius-circle: 50%; | ||
| 485 | 477 | ||
| 486 | // ---- 底部栏 ---- | 478 | // ---- 底部栏 ---- |
| 487 | .bottom-bar { | 479 | .bottom-bar { |
| 480 | + position: fixed; | ||
| 481 | + left: 0; | ||
| 482 | + right: 0; | ||
| 483 | + bottom: 0; | ||
| 488 | display: flex; | 484 | display: flex; |
| 489 | height: 110rpx; | 485 | height: 110rpx; |
| 490 | align-items: center; | 486 | align-items: center; |
| @@ -493,7 +489,6 @@ $radius-circle: 50%; | @@ -493,7 +489,6 @@ $radius-circle: 50%; | ||
| 493 | padding-bottom: env(safe-area-inset-bottom); | 489 | padding-bottom: env(safe-area-inset-bottom); |
| 494 | background-color: $color-bg-primary; | 490 | background-color: $color-bg-primary; |
| 495 | border-top: 1rpx solid $color-border; | 491 | border-top: 1rpx solid $color-border; |
| 496 | - flex-shrink: 0; | ||
| 497 | 492 | ||
| 498 | .bar-item { | 493 | .bar-item { |
| 499 | display: flex; | 494 | display: flex; |
| @@ -528,19 +523,6 @@ $radius-circle: 50%; | @@ -528,19 +523,6 @@ $radius-circle: 50%; | ||
| 528 | } | 523 | } |
| 529 | 524 | ||
| 530 | // ---- 文本弹窗 ---- | 525 | // ---- 文本弹窗 ---- |
| 531 | -.text-popup { | ||
| 532 | - position: fixed; | ||
| 533 | - left: 0; | ||
| 534 | - right: 0; | ||
| 535 | - bottom: 0; | ||
| 536 | - height: 85vh; | ||
| 537 | - background: $color-bg-secondary; | ||
| 538 | - border-radius: $radius-lg $radius-lg 0 0; | ||
| 539 | - z-index: 1000; | ||
| 540 | - display: flex; | ||
| 541 | - flex-direction: column; | ||
| 542 | -} | ||
| 543 | - | ||
| 544 | .popup-header { | 526 | .popup-header { |
| 545 | display: flex; | 527 | display: flex; |
| 546 | justify-content: space-between; | 528 | justify-content: space-between; |
| @@ -78,14 +78,7 @@ | @@ -78,14 +78,7 @@ | ||
| 78 | <view | 78 | <view |
| 79 | class="rest-countdown" | 79 | class="rest-countdown" |
| 80 | v-if="restCountdownSeconds > 0" | 80 | v-if="restCountdownSeconds > 0" |
| 81 | - @click=" | ||
| 82 | - clearRestCountdown(); | ||
| 83 | - uncheckCurrentUnit(); | ||
| 84 | - if (restTimerWasRunning) { | ||
| 85 | - trainingStore.toggleTimer(); | ||
| 86 | - restTimerWasRunning = false; | ||
| 87 | - } | ||
| 88 | - " | 81 | + @click="clearRestCountdown()" |
| 89 | > | 82 | > |
| 90 | <view class="countdown-circle"> | 83 | <view class="countdown-circle"> |
| 91 | <!-- H5 端:使用 SVG 进度环 --> | 84 | <!-- H5 端:使用 SVG 进度环 --> |
| @@ -288,7 +281,6 @@ | @@ -288,7 +281,6 @@ | ||
| 288 | const restCountdownSeconds = ref(0); | 281 | const restCountdownSeconds = ref(0); |
| 289 | const restCountdownTotal = ref(0); | 282 | const restCountdownTotal = ref(0); |
| 290 | let restCountdownInterval = null; | 283 | let restCountdownInterval = null; |
| 291 | - let restTimerWasRunning = false; | ||
| 292 | 284 | ||
| 293 | const restFormattedTime = computed(() => { | 285 | const restFormattedTime = computed(() => { |
| 294 | const min = Math.floor(restCountdownSeconds.value / 60); | 286 | const min = Math.floor(restCountdownSeconds.value / 60); |
| @@ -304,8 +296,6 @@ | @@ -304,8 +296,6 @@ | ||
| 304 | // H5 SVG 圆环周长(用于 stroke-dasharray) | 296 | // H5 SVG 圆环周长(用于 stroke-dasharray) |
| 305 | const CIRCUMFERENCE = 2 * Math.PI * 44; | 297 | const CIRCUMFERENCE = 2 * Math.PI * 44; |
| 306 | 298 | ||
| 307 | - const currentRestUnitIndex = ref(null); | ||
| 308 | - | ||
| 309 | const clearRestCountdown = () => { | 299 | const clearRestCountdown = () => { |
| 310 | if (restCountdownInterval) { | 300 | if (restCountdownInterval) { |
| 311 | clearInterval(restCountdownInterval); | 301 | clearInterval(restCountdownInterval); |
| @@ -315,39 +305,33 @@ | @@ -315,39 +305,33 @@ | ||
| 315 | restCountdownTotal.value = 0; | 305 | restCountdownTotal.value = 0; |
| 316 | }; | 306 | }; |
| 317 | 307 | ||
| 318 | - const startRestCountdown = (seconds, unitIndex) => { | 308 | + const startRestCountdown = (seconds) => { |
| 319 | if (!seconds || seconds <= 0) return; | 309 | if (!seconds || seconds <= 0) return; |
| 320 | - clearRestCountdown(); | 310 | + // 叠加倒计时:如果已经在倒计时中则累加,否则开始新的倒计时 |
| 311 | + if (restCountdownInterval) { | ||
| 312 | + restCountdownSeconds.value += seconds; | ||
| 313 | + restCountdownTotal.value += seconds; | ||
| 314 | + } else { | ||
| 321 | restCountdownTotal.value = seconds; | 315 | restCountdownTotal.value = seconds; |
| 322 | restCountdownSeconds.value = seconds; | 316 | restCountdownSeconds.value = seconds; |
| 323 | - currentRestUnitIndex.value = unitIndex; | ||
| 324 | - restTimerWasRunning = !trainingStore.isPause; | ||
| 325 | - if (restTimerWasRunning) trainingStore.toggleTimer(); | ||
| 326 | restCountdownInterval = setInterval(() => { | 317 | restCountdownInterval = setInterval(() => { |
| 327 | restCountdownSeconds.value--; | 318 | restCountdownSeconds.value--; |
| 328 | if (restCountdownSeconds.value <= 0) { | 319 | if (restCountdownSeconds.value <= 0) { |
| 329 | clearRestCountdown(); | 320 | clearRestCountdown(); |
| 330 | - uncheckCurrentUnit(); | ||
| 331 | - if (restTimerWasRunning) trainingStore.toggleTimer(); | 321 | + // 倒计时结束:不取消勾选状态,不恢复主计时器(主计时器始终在运行) |
| 332 | } | 322 | } |
| 333 | }, 1000); | 323 | }, 1000); |
| 334 | - }; | ||
| 335 | - | ||
| 336 | - function uncheckCurrentUnit() { | ||
| 337 | - if (currentRestUnitIndex.value != null) { | ||
| 338 | - const ref = dongzuoRefs.value[currentRestUnitIndex.value]; | ||
| 339 | - if (ref?.resetActiveSets) ref.resetActiveSets(); | ||
| 340 | - currentRestUnitIndex.value = null; | ||
| 341 | - } | ||
| 342 | } | 324 | } |
| 325 | + // 不暂停主计时器,训练计时器始终继续 | ||
| 326 | + }; | ||
| 343 | 327 | ||
| 344 | - function onCancelRestTimer(unitIndex) { | 328 | + function onCancelRestTimer(seconds) { |
| 329 | + if (!restCountdownInterval) return; | ||
| 330 | + // 取消勾选时减去对应秒数 | ||
| 331 | + restCountdownSeconds.value -= (seconds || 0); | ||
| 332 | + restCountdownTotal.value -= (seconds || 0); | ||
| 333 | + if (restCountdownSeconds.value <= 0) { | ||
| 345 | clearRestCountdown(); | 334 | clearRestCountdown(); |
| 346 | - currentRestUnitIndex.value = unitIndex; | ||
| 347 | - uncheckCurrentUnit(); | ||
| 348 | - if (restTimerWasRunning) { | ||
| 349 | - trainingStore.toggleTimer(); | ||
| 350 | - restTimerWasRunning = false; | ||
| 351 | } | 335 | } |
| 352 | } | 336 | } |
| 353 | 337 | ||
| @@ -545,8 +529,25 @@ | @@ -545,8 +529,25 @@ | ||
| 545 | } | 529 | } |
| 546 | } | 530 | } |
| 547 | 531 | ||
| 548 | - if (units.length === 0) { | ||
| 549 | - uni.showToast({ title: '未获取到训练数据', icon: 'none' }); | 532 | + // 检查是否有打勾(已完成)的动作,只有打勾的动作才会被提交 |
| 533 | + const hasAnyChecked = units.some(u => | ||
| 534 | + u.exercises.some(e => e.sets && e.sets.length > 0) | ||
| 535 | + ); | ||
| 536 | + | ||
| 537 | + if (!hasAnyChecked) { | ||
| 538 | + uni.showModal({ | ||
| 539 | + title: '提示', | ||
| 540 | + content: '未完成任何动作,是否取消训练?', | ||
| 541 | + confirmText: '取消训练', | ||
| 542 | + cancelText: '继续训练', | ||
| 543 | + confirmColor: '#FF3B30', | ||
| 544 | + success: (res) => { | ||
| 545 | + if (res.confirm) { | ||
| 546 | + uni.navigateBack(); | ||
| 547 | + trainingStore.clearTrainingStore(); | ||
| 548 | + } | ||
| 549 | + }, | ||
| 550 | + }); | ||
| 550 | return; | 551 | return; |
| 551 | } | 552 | } |
| 552 | 553 |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="add-custom-exercise-page"> | ||
| 3 | - <!-- 肌肉部位图 --> | ||
| 4 | - <view class="muscle-area" @click="openMusclePicker"> | ||
| 5 | - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png" | ||
| 6 | - class="muscle-image-front" mode="aspectFill" /> | ||
| 7 | - </view> | ||
| 8 | - <!-- 肌肉部位选择弹窗 - 美化版 --> | ||
| 9 | - <view v-if="showMusclePicker" class="popup-overlay" @click="closeAllPopups"> | ||
| 10 | - <view class="popup-content" @click.stop> | ||
| 11 | - <view class="popup-header"> | ||
| 12 | - <view class="close-btn" @click="closeAllPopups">x</view> | ||
| 13 | - <view class="title">选择训练部位</view> | ||
| 14 | - <view class="confirm-btn" @click="confirmMuscleSelection">完成</view> | ||
| 15 | - </view> | ||
| 16 | - <view class="muscle-list-beautified"> | ||
| 17 | - <view v-for="group in muscleGroups" :key="group.id" class="muscle-item-beautified"> | ||
| 18 | - <text class="muscle-name">{{ group.name }}</text> | ||
| 19 | - <!-- 主要部位按钮 --> | ||
| 20 | - <view class="selector-btn primary" :class="{ active: primaryMuscles.includes(group.id) }" | ||
| 21 | - @click.stop="togglePrimary(group.id)"> | ||
| 22 | - <view class="selector-dot primary"></view> | ||
| 23 | - <text class="selector-label">主要</text> | ||
| 24 | - </view> | ||
| 25 | - <!-- 次要部位按钮 --> | ||
| 26 | - <view class="selector-btn secondary" :class="{ active: secondaryMuscles.includes(group.id) }" | ||
| 27 | - @click.stop="toggleSecondary(group.id)"> | ||
| 28 | - <view class="selector-dot secondary"></view> | ||
| 29 | - <text class="selector-label">次要</text> | ||
| 30 | - </view> | ||
| 31 | - </view> | ||
| 32 | - </view> | 2 | + <view class="page"> |
| 3 | + <!-- ==================== 肌肉部位图 ==================== --> | ||
| 4 | + <view class="muscle-card" @click="showMusclePicker = true"> | ||
| 5 | + <image class="muscle-img" | ||
| 6 | + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg" | ||
| 7 | + mode="aspectFill" /> | ||
| 8 | + <view class="muscle-tip"> | ||
| 9 | + <up-icon name="edit-pen" color="#fff" size="14" /> | ||
| 10 | + <text>点击选择训练部位</text> | ||
| 33 | </view> | 11 | </view> |
| 34 | </view> | 12 | </view> |
| 35 | 13 | ||
| 36 | - <!-- 表单区域 --> | ||
| 37 | - <view class="form-section"> | ||
| 38 | - <!-- 动作名称 --> | ||
| 39 | - <view class="form-item"> | ||
| 40 | - <!-- <view class="label">动作名称</view> | ||
| 41 | - <view class="input-group"> | ||
| 42 | - <input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称" | ||
| 43 | - placeholder-class="input-placeholder" /> | ||
| 44 | - </view> --> | ||
| 45 | - <view class="action-name"> | ||
| 46 | - <view class="label">动作名称</view> | ||
| 47 | - <view class="input-group"> | ||
| 48 | - <input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称" | ||
| 49 | - placeholder-class="input-placeholder" /> | ||
| 50 | - </view> | 14 | + <!-- ==================== 表单卡片 ==================== --> |
| 15 | + <view class="form-card"> | ||
| 16 | + <!-- 动作名称 + 封面 --> | ||
| 17 | + <view class="form-row name-row"> | ||
| 18 | + <view class="name-input-area"> | ||
| 19 | + <text class="form-label">动作名称</text> | ||
| 20 | + <input v-model="exerciseName" class="name-input" placeholder="点击此处填写动作名称" placeholder-class="ph" /> | ||
| 51 | </view> | 21 | </view> |
| 52 | - | ||
| 53 | - <view class="upload-btn" @click="openCoverSelector"> | ||
| 54 | - <image :src="coverImagePath" class="preview-media" mode="aspectFill" /> | ||
| 55 | - <text class="btn-text">更换封面</text> | 22 | + <view class="cover-upload" @click="openCoverSelector"> |
| 23 | + <image :src="coverImagePath" class="cover-preview" mode="aspectFill" /> | ||
| 24 | + <text class="cover-label">更换封面</text> | ||
| 56 | </view> | 25 | </view> |
| 57 | </view> | 26 | </view> |
| 58 | - <!-- 新增:训练部位 --> | ||
| 59 | - <view class="form-item" @click="openMusclePicker"> | ||
| 60 | - <view class="label">训练部位</view> | ||
| 61 | - <view class="value-wrapper"> | ||
| 62 | - <text class="value">{{ selectedMusclesDisplay }}</text> | ||
| 63 | - <up-icon name="arrow-right" color="#333" size="16"></up-icon> | 27 | + |
| 28 | + <!-- 训练部位 --> | ||
| 29 | + <view class="form-row select-row" @click="showMusclePicker = true"> | ||
| 30 | + <text class="form-label">训练部位</text> | ||
| 31 | + <view class="select-value"> | ||
| 32 | + <text class="sel-text">{{ selectedMusclesDisplay }}</text> | ||
| 33 | + <up-icon name="arrow-right" color="#999" size="14" /> | ||
| 64 | </view> | 34 | </view> |
| 65 | </view> | 35 | </view> |
| 66 | 36 | ||
| 67 | <!-- 动作类型 --> | 37 | <!-- 动作类型 --> |
| 68 | - <view class="form-item" @click="openActionTypePicker"> | ||
| 69 | - <view class="label">动作类型</view> | ||
| 70 | - <view class="value-wrapper"> | ||
| 71 | - <text class="value">{{ actionType || '请选择' }}</text> | ||
| 72 | - <!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" | ||
| 73 | - class="arrow-icon" mode="aspectFill" /> --> | ||
| 74 | - <up-icon v-if="!isEdit" name="arrow-right" color="#333" size="16"></up-icon> | 38 | + <view class="form-row select-row" @click="openActionTypePicker"> |
| 39 | + <text class="form-label">动作类型</text> | ||
| 40 | + <view class="select-value"> | ||
| 41 | + <text class="sel-text">{{ actionType || '请选择' }}</text> | ||
| 42 | + <up-icon v-if="!isEdit" name="arrow-right" color="#999" size="14" /> | ||
| 75 | </view> | 43 | </view> |
| 76 | </view> | 44 | </view> |
| 77 | 45 | ||
| 78 | <!-- 目录管理 --> | 46 | <!-- 目录管理 --> |
| 79 | - <view class="form-item" @click="openDirectoryPicker"> | ||
| 80 | - <view class="label">目录管理</view> | ||
| 81 | - <view class="value-wrapper"> | ||
| 82 | - <text class="value">{{ directoryName || '请选择' }}</text> | ||
| 83 | - <up-icon name="arrow-right" color="#333" size="16"></up-icon> | 47 | + <view class="form-row select-row" @click="showDirectoryPicker = true"> |
| 48 | + <text class="form-label">目录管理</text> | ||
| 49 | + <view class="select-value"> | ||
| 50 | + <text class="sel-text">{{ directoryName || '请选择' }}</text> | ||
| 51 | + <up-icon name="arrow-right" color="#999" size="14" /> | ||
| 84 | </view> | 52 | </view> |
| 85 | </view> | 53 | </view> |
| 86 | 54 | ||
| 87 | <!-- 动作视频 --> | 55 | <!-- 动作视频 --> |
| 88 | - <view class="form-item"> | ||
| 89 | - <view class="label">动作视频</view> | ||
| 90 | - <view class="value-wrapper"> | ||
| 91 | - <text class="value">(非必填)</text> | ||
| 92 | - </view> | ||
| 93 | - <view class="upload-btn" @click="openVideoSelector"> | ||
| 94 | - <!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" | ||
| 95 | - class="camera-icon" mode="aspectFill" />videoFilePath.value --> | 56 | + <view class="form-row media-row"> |
| 57 | + <text class="form-label">动作视频 <text class="optional">(非必填)</text></text> | ||
| 58 | + <view class="media-upload" @click="openVideoSelector"> | ||
| 96 | <template v-if="videoFilePath"> | 59 | <template v-if="videoFilePath"> |
| 97 | - <video v-if="isVideoFile" :src="videoFilePath" class="preview-media" :controls="false" | ||
| 98 | - :show-fullscreen-btn="false" :enable-progress-gesture="false"></video> | ||
| 99 | - <!-- 图片类型 --> | ||
| 100 | - <image v-else :src="videoFilePath" class="preview-media" mode="aspectFill" /> | 60 | + <video v-if="isVideoFile" :src="videoFilePath" class="media-preview" :controls="false" |
| 61 | + :show-fullscreen-btn="false" :enable-progress-gesture="false" /> | ||
| 62 | + <image v-else :src="videoFilePath" class="media-preview" mode="aspectFill" /> | ||
| 63 | + <view class="media-replace-btn">重新选择</view> | ||
| 101 | </template> | 64 | </template> |
| 102 | <template v-else> | 65 | <template v-else> |
| 103 | - <up-icon name="camera" color="#333" size="16"></up-icon> | ||
| 104 | - <text class="btn-text">图片/视频</text> | 66 | + <up-icon name="camera" color="#999" size="28" /> |
| 67 | + <text class="media-placeholder">图片 / 视频</text> | ||
| 105 | </template> | 68 | </template> |
| 106 | - | ||
| 107 | </view> | 69 | </view> |
| 108 | </view> | 70 | </view> |
| 109 | 71 | ||
| 110 | <!-- 视频详解 --> | 72 | <!-- 视频详解 --> |
| 111 | - <view class="form-item" @click="chooseTutorialVideo"> | ||
| 112 | - <view class="label">视频详解</view> | ||
| 113 | - <view class="value-wrapper"> | ||
| 114 | - <text class="value">{{ videoDetailName || '上传详解视频(非必填)' }}</text> | ||
| 115 | - <view class="upload-btn"> | 73 | + <view class="form-row media-row"> |
| 74 | + <text class="form-label">视频详解 <text class="optional">(非必填)</text></text> | ||
| 75 | + <view class="media-upload" @click="chooseTutorialVideo"> | ||
| 116 | <template v-if="urlTutorial"> | 76 | <template v-if="urlTutorial"> |
| 117 | - <video :src="urlTutorial" class="preview-media" :controls="false" :show-fullscreen-btn="false" | ||
| 118 | - :enable-progress-gesture="false"></video> | 77 | + <video :src="urlTutorial" class="media-preview" :controls="false" :show-fullscreen-btn="false" |
| 78 | + :enable-progress-gesture="false" /> | ||
| 79 | + <view class="media-replace-btn">重新选择</view> | ||
| 119 | </template> | 80 | </template> |
| 120 | <template v-else> | 81 | <template v-else> |
| 121 | - <up-icon name="camera" color="#333" size="16" /> | ||
| 122 | - <text class="btn-text">添加视频</text> | 82 | + <up-icon name="play-right" color="#999" size="28" /> |
| 83 | + <text class="media-placeholder">添加详解视频</text> | ||
| 123 | </template> | 84 | </template> |
| 124 | </view> | 85 | </view> |
| 125 | </view> | 86 | </view> |
| 126 | - </view> | ||
| 127 | 87 | ||
| 128 | - <view class="form-item" v-if="isEdit" @click="deleteAction"> | ||
| 129 | - <view class="center-label">删除动作</view> | 88 | + <!-- 删除动作(编辑模式) --> |
| 89 | + <view v-if="isEdit" class="form-row delete-row" @click="deleteAction"> | ||
| 90 | + <up-icon name="trash" color="#e74c3c" size="18" /> | ||
| 91 | + <text class="delete-text">删除动作</text> | ||
| 92 | + </view> | ||
| 130 | </view> | 93 | </view> |
| 131 | 94 | ||
| 132 | - <!-- 保存按钮 --> | ||
| 133 | - <view class="save-button"> | ||
| 134 | - <button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button> | 95 | + <!-- 保存按钮(卡片外、贴近底部) --> |
| 96 | + <view class="save-bar"> | ||
| 97 | + <view class="btn-save" :class="{ disabled: !exerciseName.trim() }" @click="saveExercise"> | ||
| 98 | + 保存 | ||
| 135 | </view> | 99 | </view> |
| 136 | </view> | 100 | </view> |
| 137 | 101 | ||
| 138 | - <!-- 动作类型选择弹窗 --> | ||
| 139 | - <view v-if="showActionTypePicker" class="popup-overlay" @click="closeAllPopups"> | ||
| 140 | - <view class="popup-content" @click.stop> | ||
| 141 | - <view class="popup-header"> | ||
| 142 | - <view class="close-btn" @click="closeAllPopups">×</view> | ||
| 143 | - <view class="title">选择动作类型</view> | ||
| 144 | - <view class="confirm-btn" @click="confirmActionType">确定</view> | 102 | + <!-- ==================== 弹窗:肌肉部位选择 ==================== --> |
| 103 | + <up-popup :show="showMusclePicker" mode="bottom" round="16rpx" @close="showMusclePicker = false" | ||
| 104 | + :safe-area-inset-bottom="true"> | ||
| 105 | + <view class="popup-body"> | ||
| 106 | + <view class="popup-head"> | ||
| 107 | + <text class="popup-title">选择训练部位</text> | ||
| 108 | + <view class="popup-close" @click="showMusclePicker = false"> | ||
| 109 | + <up-icon name="close" color="#666" size="18" /> | ||
| 145 | </view> | 110 | </view> |
| 146 | - <picker-view :value="[actionTypeIndex]" @change="onActionTypeChange" class="picker-view"> | ||
| 147 | - <picker-view-column> | ||
| 148 | - <view class="picker-item" v-for="item in actionTypeOptions" :key="item">{{ | ||
| 149 | - item.name | ||
| 150 | - }}</view> | ||
| 151 | - </picker-view-column> | ||
| 152 | - </picker-view> | ||
| 153 | </view> | 111 | </view> |
| 112 | + <view class="muscle-list"> | ||
| 113 | + <view v-for="group in muscleGroups" :key="group.id" class="muscle-item"> | ||
| 114 | + <text class="muscle-name">{{ group.name }}</text> | ||
| 115 | + <view class="muscle-actions"> | ||
| 116 | + <view class="tag primary" :class="{ on: primaryMuscles.includes(group.id) }" | ||
| 117 | + @click.stop="togglePrimary(group.id)"> | ||
| 118 | + <text>主要</text> | ||
| 119 | + </view> | ||
| 120 | + <view class="tag secondary" :class="{ on: secondaryMuscles.includes(group.id) }" | ||
| 121 | + @click.stop="toggleSecondary(group.id)"> | ||
| 122 | + <text>次要</text> | ||
| 154 | </view> | 123 | </view> |
| 155 | - | ||
| 156 | - <!-- 目录管理弹窗 --> | ||
| 157 | - <view v-if="showDirectoryPicker" class="popup-overlay" @click="closeAllPopups"> | ||
| 158 | - <view class="popup-content" @click.stop> | ||
| 159 | - <view class="popup-header"> | ||
| 160 | - <view class="close-btn" @click="closeAllPopups">×</view> | ||
| 161 | - <view class="title">目录管理</view> | ||
| 162 | - <view class="confirm-btn" @click="confirmDirectory">确定</view> | ||
| 163 | </view> | 124 | </view> |
| 164 | - <view class="double-picker"> | ||
| 165 | - <picker-view :value="[bodyPartIndex]" @change="onBodyPartChange" class="picker-view half"> | ||
| 166 | - <picker-view-column> | ||
| 167 | - <view class="picker-item" v-for="item in bodyPartOptions" :key="item">{{ | ||
| 168 | - item.name | ||
| 169 | - }}</view> | ||
| 170 | - </picker-view-column> | ||
| 171 | - </picker-view> | ||
| 172 | - <picker-view :value="[equipmentIndex]" @change="onEquipmentChange" class="picker-view half"> | ||
| 173 | - <picker-view-column> | ||
| 174 | - <view class="picker-item" v-for="item in equipmentOptions" :key="item">{{ | ||
| 175 | - item.name | ||
| 176 | - }}</view> | ||
| 177 | - </picker-view-column> | ||
| 178 | - </picker-view> | ||
| 179 | </view> | 125 | </view> |
| 180 | </view> | 126 | </view> |
| 127 | + <view class="popup-foot"> | ||
| 128 | + <view class="popup-done" @click="confirmMuscleSelection">完成</view> | ||
| 181 | </view> | 129 | </view> |
| 182 | </view> | 130 | </view> |
| 131 | + </up-popup> | ||
| 132 | + | ||
| 133 | + <!-- ==================== 弹窗:动作类型选择 ==================== --> | ||
| 134 | + <up-picker | ||
| 135 | + :show="showActionTypePicker" | ||
| 136 | + :columns="actionTypeColumns" | ||
| 137 | + :default-index="[actionTypeIndex]" | ||
| 138 | + title="选择动作类型" | ||
| 139 | + @confirm="onActionTypeConfirm" | ||
| 140 | + @cancel="showActionTypePicker = false" | ||
| 141 | + close-on-click-overlay | ||
| 142 | + /> | ||
| 143 | + | ||
| 144 | + <!-- ==================== 弹窗:目录管理 ==================== --> | ||
| 145 | + <up-picker | ||
| 146 | + :show="showDirectoryPicker" | ||
| 147 | + :columns="directoryColumns" | ||
| 148 | + :default-index="[bodyPartIndex, equipmentIndex]" | ||
| 149 | + title="目录管理" | ||
| 150 | + @confirm="onDirectoryConfirm" | ||
| 151 | + @cancel="showDirectoryPicker = false" | ||
| 152 | + close-on-click-overlay | ||
| 153 | + /> | ||
| 154 | + </view> | ||
| 183 | </template> | 155 | </template> |
| 184 | 156 | ||
| 185 | <script setup> | 157 | <script setup> |
| 186 | -import { onMounted, ref } from 'vue'; | 158 | +import { ref, computed } from 'vue'; |
| 187 | import { onLoad } from '@dcloudio/uni-app'; | 159 | import { onLoad } from '@dcloudio/uni-app'; |
| 188 | import ExercisesApi from '@/sheep/api/motion/exercises'; | 160 | import ExercisesApi from '@/sheep/api/motion/exercises'; |
| 189 | import LeftmotionApi from '@/sheep/api/motion/equipments'; | 161 | import LeftmotionApi from '@/sheep/api/motion/equipments'; |
| 190 | import EquipmentListApi from '@/sheep/api/motion/motionequipments'; | 162 | import EquipmentListApi from '@/sheep/api/motion/motionequipments'; |
| 191 | import FileApi from '@/sheep/api/infra/file'; | 163 | import FileApi from '@/sheep/api/infra/file'; |
| 192 | -// 表单数据 | 164 | + |
| 165 | +// ==================== 基础状态 ==================== | ||
| 166 | +const isEdit = ref(false); | ||
| 167 | +const editActionId = ref(null); | ||
| 168 | +const actionDetail = ref({}); | ||
| 169 | + | ||
| 170 | +// ==================== 表单数据 ==================== | ||
| 193 | const exerciseName = ref(''); | 171 | const exerciseName = ref(''); |
| 194 | -const actionType = ref(); | ||
| 195 | -const directoryName = ref(); | 172 | +const actionType = ref(''); |
| 173 | +const directoryName = ref(''); | ||
| 196 | const selectedMusclesDisplay = ref('未选择'); | 174 | const selectedMusclesDisplay = ref('未选择'); |
| 197 | -const editActionId = ref(null) | ||
| 198 | 175 | ||
| 199 | -// 选中索引 | 176 | +// ==================== picker 索引 ==================== |
| 200 | const actionTypeIndex = ref(0); | 177 | const actionTypeIndex = ref(0); |
| 201 | const bodyPartIndex = ref(0); | 178 | const bodyPartIndex = ref(0); |
| 202 | const equipmentIndex = ref(0); | 179 | const equipmentIndex = ref(0); |
| 203 | 180 | ||
| 204 | -// 这是后端固定写死的,不要改 | 181 | +// ==================== 动作类型选项(后端固定) ==================== |
| 205 | const actionTypeOptions = ref([ | 182 | const actionTypeOptions = ref([ |
| 206 | { id: 0, name: '无氧运动' }, | 183 | { id: 0, name: '无氧运动' }, |
| 207 | { id: 1, name: '有氧运动' }, | 184 | { id: 1, name: '有氧运动' }, |
| @@ -210,343 +187,290 @@ const actionTypeOptions = ref([ | @@ -210,343 +187,290 @@ const actionTypeOptions = ref([ | ||
| 210 | { id: 4, name: '自重加重' }, | 187 | { id: 4, name: '自重加重' }, |
| 211 | { id: 5, name: '自重减重' }, | 188 | { id: 5, name: '自重减重' }, |
| 212 | { id: 6, name: '间歇训练' }, | 189 | { id: 6, name: '间歇训练' }, |
| 213 | -]) | ||
| 214 | -// // 加载动作分类(类型) | ||
| 215 | -// const loadexercisesGroups = async () => { | ||
| 216 | -// try { | ||
| 217 | -// const res = await ExercisesApi.getAllMotionCategories(); | ||
| 218 | -// actionTypeOptions.value = res.data; | ||
| 219 | -// console.log('加载动作分类', actionTypeOptions.value) | ||
| 220 | -// } catch (err) { | ||
| 221 | -// console.error('加载部位失败', err); | ||
| 222 | -// } | ||
| 223 | -// }; | 190 | +]); |
| 224 | 191 | ||
| 225 | -const resetForm = () => { | ||
| 226 | - exerciseName.value = '' | ||
| 227 | - actionType.value = '' | ||
| 228 | - actionTypeIndex.value = 0 | ||
| 229 | - directoryName.value = '' | ||
| 230 | - bodyPartIndex.value = 0 | ||
| 231 | - equipmentIndex.value = 0 | ||
| 232 | - selectedMusclesDisplay.value = '未选择' | ||
| 233 | - primaryMuscles.value = [] | ||
| 234 | - secondaryMuscles.value = [] | ||
| 235 | - coverImagePath.value = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png' | ||
| 236 | - coverUploaded.value = false | ||
| 237 | - videoFilePath.value = '' | ||
| 238 | - videoUploaded.value = false | ||
| 239 | - isVideoFile.value = false | ||
| 240 | - urlTutorial.value = '' | ||
| 241 | - videoDetailName.value = '' | ||
| 242 | - // 同时关闭所有弹窗,防止上次弹窗残留 | ||
| 243 | - closeAllPopups() | ||
| 244 | -} | 192 | +// up-picker 需要的 columns 格式:[['无氧运动', '有氧运动', ...]] |
| 193 | +const actionTypeColumns = computed(() => [actionTypeOptions.value.map((item) => item.name)]); | ||
| 245 | 194 | ||
| 246 | -const isEdit = ref(false) | ||
| 247 | -const actionDetail = ref({}); | 195 | +// ==================== 肌肉部位 ==================== |
| 196 | +const muscleGroups = ref([]); | ||
| 197 | +const primaryMuscles = ref([]); | ||
| 198 | +const secondaryMuscles = ref([]); | ||
| 199 | + | ||
| 200 | +const loadMuscleGroups = async () => { | ||
| 201 | + try { | ||
| 202 | + const res = await ExercisesApi.getMotionPartPage(); | ||
| 203 | + muscleGroups.value = res.data; | ||
| 204 | + } catch (err) { | ||
| 205 | + console.error('加载肌肉部位失败', err); | ||
| 206 | + } | ||
| 207 | +}; | ||
| 208 | + | ||
| 209 | +// ==================== 目录选项 ==================== | ||
| 210 | +const bodyPartOptions = ref([]); | ||
| 211 | +const loadbodyPartOptions = async () => { | ||
| 212 | + const res = await LeftmotionApi.getAllCategories(); | ||
| 213 | + bodyPartOptions.value = res.data; | ||
| 214 | +}; | ||
| 215 | + | ||
| 216 | +const equipmentOptions = ref([]); | ||
| 217 | +const loadequipmentOptions = async () => { | ||
| 218 | + const res = await EquipmentListApi.Getallequipments(); | ||
| 219 | + equipmentOptions.value = res.data; | ||
| 220 | +}; | ||
| 221 | + | ||
| 222 | +// up-picker 双列格式:[['大腿', '大臂', ...], ['器械', '杠铃', ...]] | ||
| 223 | +const directoryColumns = computed(() => [ | ||
| 224 | + bodyPartOptions.value.map((item) => item.name), | ||
| 225 | + equipmentOptions.value.map((item) => item.name), | ||
| 226 | +]); | ||
| 227 | + | ||
| 228 | +// ==================== 弹窗状态 ==================== | ||
| 229 | +const showMusclePicker = ref(false); | ||
| 230 | +const showActionTypePicker = ref(false); | ||
| 231 | +const showDirectoryPicker = ref(false); | ||
| 232 | + | ||
| 233 | +// ==================== 上传状态 ==================== | ||
| 234 | +const DEFAULT_COVER = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/动作_1784878189633.jpg'; | ||
| 235 | +const coverImagePath = ref(DEFAULT_COVER); | ||
| 236 | +const coverUploaded = ref(false); | ||
| 237 | +const videoFilePath = ref(''); | ||
| 238 | +const videoUploaded = ref(false); | ||
| 239 | +const isVideoFile = ref(false); | ||
| 240 | +const urlTutorial = ref(''); | ||
| 241 | +const videoDetailName = ref(''); | ||
| 248 | 242 | ||
| 243 | +// ==================== 表单重置 ==================== | ||
| 244 | +const resetForm = () => { | ||
| 245 | + exerciseName.value = ''; | ||
| 246 | + actionType.value = ''; | ||
| 247 | + actionTypeIndex.value = 0; | ||
| 248 | + directoryName.value = ''; | ||
| 249 | + bodyPartIndex.value = 0; | ||
| 250 | + equipmentIndex.value = 0; | ||
| 251 | + selectedMusclesDisplay.value = '未选择'; | ||
| 252 | + primaryMuscles.value = []; | ||
| 253 | + secondaryMuscles.value = []; | ||
| 254 | + coverImagePath.value = DEFAULT_COVER; | ||
| 255 | + coverUploaded.value = false; | ||
| 256 | + videoFilePath.value = ''; | ||
| 257 | + videoUploaded.value = false; | ||
| 258 | + isVideoFile.value = false; | ||
| 259 | + urlTutorial.value = ''; | ||
| 260 | + videoDetailName.value = ''; | ||
| 261 | + showMusclePicker.value = false; | ||
| 262 | + showActionTypePicker.value = false; | ||
| 263 | + showDirectoryPicker.value = false; | ||
| 264 | +}; | ||
| 265 | + | ||
| 266 | +// ==================== 页面加载 ==================== | ||
| 249 | onLoad(async (options) => { | 267 | onLoad(async (options) => { |
| 268 | + isEdit.value = options.isEdit === 'true'; | ||
| 269 | + const editId = Number(options.id); | ||
| 270 | + editActionId.value = editId; | ||
| 250 | 271 | ||
| 251 | - isEdit.value = options.isEdit === 'true' | ||
| 252 | - const editId = Number(options.id) | ||
| 253 | - editActionId.value = editId | ||
| 254 | - console.log('编辑状态', isEdit, '编辑动作ID', editId) | ||
| 255 | - // 清空表单状态 | ||
| 256 | - resetForm() | 272 | + resetForm(); |
| 257 | 273 | ||
| 258 | await Promise.all([ | 274 | await Promise.all([ |
| 259 | loadbodyPartOptions(), | 275 | loadbodyPartOptions(), |
| 260 | loadequipmentOptions(), | 276 | loadequipmentOptions(), |
| 261 | - loadMuscleGroups() | ||
| 262 | - ]) | 277 | + loadMuscleGroups(), |
| 278 | + ]); | ||
| 279 | + | ||
| 263 | if (isEdit.value) { | 280 | if (isEdit.value) { |
| 264 | - loadexercisedetail(editId) | 281 | + loadexercisedetail(editId); |
| 265 | } | 282 | } |
| 283 | +}); | ||
| 266 | 284 | ||
| 267 | -}) | ||
| 268 | - | ||
| 269 | -// 加载单个动作详情 | ||
| 270 | -// const loadexercisedetail = async (id) => { | ||
| 271 | -// if (!id || !Number(id)) return | ||
| 272 | -// try { | ||
| 273 | -// const response = await ExercisesApi.getExerciseById(id); | ||
| 274 | -// actionDetail.value = response.data; | ||
| 275 | -// console.log('打印动作详情', actionDetail.value); | ||
| 276 | -// } catch (err) { | ||
| 277 | -// console.error('加载动作详情失败:', err) | ||
| 278 | -// actionDetail.value = {} | ||
| 279 | -// } | ||
| 280 | -// }; | ||
| 281 | - | ||
| 282 | -// 加载单个动作详情并回显表单 | 285 | +// ==================== 编辑回显 ==================== |
| 283 | const loadexercisedetail = async (id) => { | 286 | const loadexercisedetail = async (id) => { |
| 284 | - if (!id || !Number(id)) return | 287 | + if (!id || !Number(id)) return; |
| 285 | try { | 288 | try { |
| 286 | const response = await ExercisesApi.getExerciseById(id); | 289 | const response = await ExercisesApi.getExerciseById(id); |
| 287 | actionDetail.value = response.data; | 290 | actionDetail.value = response.data; |
| 288 | - const detail = actionDetail.value | ||
| 289 | - console.log('打印动作详情', detail); | 291 | + const detail = actionDetail.value; |
| 290 | 292 | ||
| 291 | - // 1. 动作名称 | ||
| 292 | - exerciseName.value = detail.name || '' | 293 | + exerciseName.value = detail.name || ''; |
| 293 | 294 | ||
| 294 | - // 2. 动作类型回显(id匹配,编辑只读) | ||
| 295 | - const targetType = actionTypeOptions.value.find(item => item.id === detail.exerciseType) | 295 | + const targetType = actionTypeOptions.value.find((item) => item.id === detail.exerciseType); |
| 296 | if (targetType) { | 296 | if (targetType) { |
| 297 | - actionTypeIndex.value = actionTypeOptions.value.findIndex(item => item.id === detail.exerciseType) | ||
| 298 | - actionType.value = targetType.name | 297 | + actionTypeIndex.value = actionTypeOptions.value.findIndex((item) => item.id === detail.exerciseType); |
| 298 | + actionType.value = targetType.name; | ||
| 299 | } | 299 | } |
| 300 | 300 | ||
| 301 | - // 3. 目录:身体部位categoryId + 器械equipmentId 匹配下标 | ||
| 302 | - const bodyIdx = bodyPartOptions.value.findIndex(item => item.id === detail.categoryId) | ||
| 303 | - if (bodyIdx > -1) bodyPartIndex.value = bodyIdx | ||
| 304 | - | ||
| 305 | - const equipIdx = equipmentOptions.value.findIndex(item => item.id === detail.equipmentId) | ||
| 306 | - if (equipIdx > -1) equipmentIndex.value = equipIdx | ||
| 307 | - // 拼接目录展示文字 | ||
| 308 | - const selBody = bodyPartOptions.value[bodyIdx] | ||
| 309 | - const selEquip = equipmentOptions.value[equipIdx] | 301 | + const bodyIdx = bodyPartOptions.value.findIndex((item) => item.id === detail.categoryId); |
| 302 | + if (bodyIdx > -1) bodyPartIndex.value = bodyIdx; | ||
| 303 | + const equipIdx = equipmentOptions.value.findIndex((item) => item.id === detail.equipmentId); | ||
| 304 | + if (equipIdx > -1) equipmentIndex.value = equipIdx; | ||
| 305 | + const selBody = bodyPartOptions.value[bodyIdx]; | ||
| 306 | + const selEquip = equipmentOptions.value[equipIdx]; | ||
| 310 | if (selBody && selEquip) { | 307 | if (selBody && selEquip) { |
| 311 | - directoryName.value = ` ${selBody.name}( ${selEquip.name})` | 308 | + directoryName.value = `${selBody.name} (${selEquip.name})`; |
| 312 | } | 309 | } |
| 313 | 310 | ||
| 314 | - // 4. 肌肉部位:后端返回字符串 "[30,28,29]" 转数字数组 | ||
| 315 | - if (detail.primaryMuscles) { | ||
| 316 | - primaryMuscles.value = JSON.parse(detail.primaryMuscles) | ||
| 317 | - } else { | ||
| 318 | - primaryMuscles.value = [] | ||
| 319 | - } | ||
| 320 | - if (detail.secondaryMuscles) { | ||
| 321 | - secondaryMuscles.value = JSON.parse(detail.secondaryMuscles) | ||
| 322 | - } else { | ||
| 323 | - secondaryMuscles.value = [] | ||
| 324 | - } | ||
| 325 | - // 直接使用后端返回的拼接好的肌肉名称展示文本,不用重新拼接 | ||
| 326 | - const primaryStr = detail.primaryMuscleNames?.length ? detail.primaryMuscleNames.join('、') + '(主)' : '' | ||
| 327 | - const secondaryStr = detail.secondaryMuscleNames?.length ? detail.secondaryMuscleNames.join('、') + '(次)' : '' | ||
| 328 | - let display = '' | ||
| 329 | - if (primaryStr) display += primaryStr | 311 | + primaryMuscles.value = detail.primaryMuscles ? JSON.parse(detail.primaryMuscles) : []; |
| 312 | + secondaryMuscles.value = detail.secondaryMuscles ? JSON.parse(detail.secondaryMuscles) : []; | ||
| 313 | + const primaryStr = detail.primaryMuscleNames?.length ? detail.primaryMuscleNames.join('、') + '(主)' : ''; | ||
| 314 | + const secondaryStr = detail.secondaryMuscleNames?.length ? detail.secondaryMuscleNames.join('、') + '(次)' : ''; | ||
| 315 | + let display = ''; | ||
| 316 | + if (primaryStr) display += primaryStr; | ||
| 330 | if (secondaryStr) { | 317 | if (secondaryStr) { |
| 331 | - if (display) display += ', ' | ||
| 332 | - display += secondaryStr | 318 | + if (display) display += ', '; |
| 319 | + display += secondaryStr; | ||
| 333 | } | 320 | } |
| 334 | - selectedMusclesDisplay.value = display || '未选择' | 321 | + selectedMusclesDisplay.value = display || '未选择'; |
| 335 | 322 | ||
| 336 | - // 5. 封面图 url3dAnimation | ||
| 337 | if (detail.url3dAnimation) { | 323 | if (detail.url3dAnimation) { |
| 338 | - coverImagePath.value = detail.url3dAnimation | ||
| 339 | - coverUploaded.value = true // 标记已有封面,跳过新增封面校验 | 324 | + coverImagePath.value = detail.url3dAnimation; |
| 325 | + coverUploaded.value = true; | ||
| 340 | } | 326 | } |
| 341 | - | ||
| 342 | - // 6. 动作实拍视频/图片 urlRealPerson | ||
| 343 | if (detail.urlRealPerson) { | 327 | if (detail.urlRealPerson) { |
| 344 | - videoFilePath.value = detail.urlRealPerson | ||
| 345 | - videoUploaded.value = true | ||
| 346 | - // 简单判断是否视频(后端返回mp4则标记video) | ||
| 347 | - isVideoFile.value = detail.urlRealPerson.includes('.mp4') | 328 | + videoFilePath.value = detail.urlRealPerson; |
| 329 | + videoUploaded.value = true; | ||
| 330 | + isVideoFile.value = detail.urlRealPerson.includes('.mp4'); | ||
| 348 | } | 331 | } |
| 349 | - | ||
| 350 | - // 7. 详解视频 urlTutorial | ||
| 351 | if (detail.urlTutorial) { | 332 | if (detail.urlTutorial) { |
| 352 | - urlTutorial.value = detail.urlTutorial | ||
| 353 | - videoDetailName.value = '已选择详解视频' | 333 | + urlTutorial.value = detail.urlTutorial; |
| 334 | + videoDetailName.value = '已选择详解视频'; | ||
| 354 | } | 335 | } |
| 355 | - | ||
| 356 | } catch (err) { | 336 | } catch (err) { |
| 357 | - console.error('加载动作详情失败:', err) | ||
| 358 | - actionDetail.value = {} | 337 | + console.error('加载动作详情失败:', err); |
| 338 | + actionDetail.value = {}; | ||
| 359 | } | 339 | } |
| 360 | }; | 340 | }; |
| 361 | 341 | ||
| 362 | - | ||
| 363 | -// 肌肉部位列表(从接口获取) | ||
| 364 | -const muscleGroups = ref([]); | ||
| 365 | -// 加载肌肉部位 | ||
| 366 | -const loadMuscleGroups = async () => { | ||
| 367 | - try { | ||
| 368 | - const res = await ExercisesApi.getMotionPartPage(); | ||
| 369 | - muscleGroups.value = res.data; | ||
| 370 | - console.log('加载部位分类:', muscleGroups.value) | ||
| 371 | - } catch (err) { | ||
| 372 | - console.error('加载部位失败', err); | ||
| 373 | - } | 342 | +// ==================== 动作类型 picker ==================== |
| 343 | +const openActionTypePicker = () => { | ||
| 344 | + if (isEdit.value) return; | ||
| 345 | + showActionTypePicker.value = true; | ||
| 374 | }; | 346 | }; |
| 375 | 347 | ||
| 376 | -// 主要和次要部位(存储 ID 数组) | ||
| 377 | -const primaryMuscles = ref([]); // 主要部位 ID 列表 | ||
| 378 | -const secondaryMuscles = ref([]); // 次要部位 ID 列表 | 348 | +const onActionTypeConfirm = (res) => { |
| 349 | + const idx = res.indexs[0]; | ||
| 350 | + actionType.value = actionTypeOptions.value[idx].name; | ||
| 351 | + actionTypeIndex.value = idx; | ||
| 352 | + showActionTypePicker.value = false; | ||
| 353 | +}; | ||
| 379 | 354 | ||
| 380 | -// 弹窗控制 | ||
| 381 | -const showMusclePicker = ref(false); | 355 | +// ==================== 目录 picker ==================== |
| 356 | +const onDirectoryConfirm = (res) => { | ||
| 357 | + const [bodyIdx, equipIdx] = res.indexs; | ||
| 358 | + bodyPartIndex.value = bodyIdx; | ||
| 359 | + equipmentIndex.value = equipIdx; | ||
| 360 | + const selBody = bodyPartOptions.value[bodyIdx]; | ||
| 361 | + const selEquip = equipmentOptions.value[equipIdx]; | ||
| 362 | + directoryName.value = `${selBody.name} (${selEquip.name})`; | ||
| 363 | + showDirectoryPicker.value = false; | ||
| 364 | +}; | ||
| 382 | 365 | ||
| 383 | -// 部位分类 | ||
| 384 | -const bodyPartOptions = ref([]); | ||
| 385 | -const loadbodyPartOptions = async () => { | ||
| 386 | - const res = await LeftmotionApi.getAllCategories(); | ||
| 387 | - bodyPartOptions.value = res.data; | 366 | +// ==================== 肌肉部位选择 ==================== |
| 367 | +const togglePrimary = (id) => { | ||
| 368 | + if (primaryMuscles.value.includes(id)) { | ||
| 369 | + primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id); | ||
| 370 | + } else { | ||
| 371 | + primaryMuscles.value.push(id); | ||
| 372 | + secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id); | ||
| 373 | + } | ||
| 388 | }; | 374 | }; |
| 389 | 375 | ||
| 390 | -// 用具分类 | ||
| 391 | -const equipmentOptions = ref([]); | ||
| 392 | -const loadequipmentOptions = async () => { | ||
| 393 | - const res = await EquipmentListApi.Getallequipments(); | ||
| 394 | - equipmentOptions.value = res.data; | 376 | +const toggleSecondary = (id) => { |
| 377 | + if (secondaryMuscles.value.includes(id)) { | ||
| 378 | + secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id); | ||
| 379 | + } else { | ||
| 380 | + secondaryMuscles.value.push(id); | ||
| 381 | + primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id); | ||
| 382 | + } | ||
| 395 | }; | 383 | }; |
| 396 | 384 | ||
| 397 | -// 弹窗状态 | ||
| 398 | -const showActionTypePicker = ref(false); | ||
| 399 | -const showDirectoryPicker = ref(false); | ||
| 400 | -const showCoverSelector = ref(false); | ||
| 401 | -const showVideoSelector = ref(false); | 385 | +const confirmMuscleSelection = () => { |
| 386 | + const primaryNames = primaryMuscles.value | ||
| 387 | + .map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '') | ||
| 388 | + .filter(Boolean); | ||
| 389 | + const secondaryNames = secondaryMuscles.value | ||
| 390 | + .map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '') | ||
| 391 | + .filter(Boolean); | ||
| 402 | 392 | ||
| 403 | -// 弹窗控制 | ||
| 404 | -const openActionTypePicker = () => { | ||
| 405 | - if (isEdit.value) return; | ||
| 406 | - showActionTypePicker.value = true; | ||
| 407 | -}; | ||
| 408 | -const openDirectoryPicker = () => { | ||
| 409 | - showDirectoryPicker.value = true; | 393 | + let displayText = ''; |
| 394 | + if (primaryNames.length) displayText += primaryNames.join('、') + '(主)'; | ||
| 395 | + if (secondaryNames.length) { | ||
| 396 | + if (displayText) displayText += ', '; | ||
| 397 | + displayText += secondaryNames.join('、') + '(次)'; | ||
| 398 | + } | ||
| 399 | + selectedMusclesDisplay.value = displayText || '未选择'; | ||
| 400 | + showMusclePicker.value = false; | ||
| 410 | }; | 401 | }; |
| 411 | 402 | ||
| 412 | -// 封面图片路径 | ||
| 413 | -const coverImagePath = ref('https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'); | ||
| 414 | -// 视频路径(可选) | ||
| 415 | -const videoFilePath = ref(''); | ||
| 416 | -// ✅ 新增:标记是否上传成功(用于判断) | ||
| 417 | -const coverUploaded = ref(false); | ||
| 418 | -const videoUploaded = ref(false); | ||
| 419 | -// 视频详解 | ||
| 420 | -const urlTutorial = ref(''); | ||
| 421 | -const videoDetailName = ref(''); | ||
| 422 | - | ||
| 423 | -// ==================== 视频详解(上传组件)==================== | ||
| 424 | -const chooseTutorialVideo = () => { | ||
| 425 | - uni.chooseMedia({ | 403 | +// ==================== 文件上传 ==================== |
| 404 | +const openCoverSelector = () => { | ||
| 405 | + uni.chooseImage({ | ||
| 426 | count: 1, | 406 | count: 1, |
| 427 | - mediaType: ['video'], // 只允许选视频 | ||
| 428 | success: async (res) => { | 407 | success: async (res) => { |
| 429 | - const file = res.tempFiles[0]; | ||
| 430 | - const result = await FileApi.uploadFile(file.tempFilePath); | ||
| 431 | - urlTutorial.value = result.data; | ||
| 432 | - videoDetailName.value = '已选择详解视频'; | ||
| 433 | - uni.showToast({ title: '选择成功', icon: 'success' }); | 408 | + const path = res.tempFilePaths[0]; |
| 409 | + uni.showLoading({ title: '上传中...' }); | ||
| 410 | + try { | ||
| 411 | + const result = await FileApi.uploadFile(path); | ||
| 412 | + coverImagePath.value = result.data; | ||
| 413 | + coverUploaded.value = true; | ||
| 414 | + uni.showToast({ title: '封面上传成功', icon: 'success' }); | ||
| 415 | + } catch (err) { | ||
| 416 | + uni.showToast({ title: '上传失败', icon: 'none' }); | ||
| 417 | + } finally { | ||
| 418 | + uni.hideLoading(); | ||
| 419 | + } | ||
| 434 | }, | 420 | }, |
| 435 | fail: () => { | 421 | fail: () => { |
| 436 | - uni.showToast({ title: '选择失败', icon: 'none' }); | ||
| 437 | - } | ||
| 438 | - }) | 422 | + coverUploaded.value = false; |
| 423 | + uni.showToast({ title: '封面选择失败', icon: 'none' }); | ||
| 424 | + }, | ||
| 425 | + }); | ||
| 439 | }; | 426 | }; |
| 440 | -const isVideoFile = ref(false) | ||
| 441 | -// 打开视频/图片选择器 + 上传成功判断 这里上传的是3D的那个字段 | 427 | + |
| 442 | const openVideoSelector = () => { | 428 | const openVideoSelector = () => { |
| 443 | uni.chooseMedia({ | 429 | uni.chooseMedia({ |
| 444 | count: 1, | 430 | count: 1, |
| 445 | mediaType: ['image', 'video'], | 431 | mediaType: ['image', 'video'], |
| 446 | success: async (res) => { | 432 | success: async (res) => { |
| 447 | const file = res.tempFiles[0]; | 433 | const file = res.tempFiles[0]; |
| 448 | - console.log('选择的文件类型:', file.fileType); | ||
| 449 | isVideoFile.value = file.fileType === 'video'; | 434 | isVideoFile.value = file.fileType === 'video'; |
| 450 | - // 上传到服务器 | ||
| 451 | const result = await FileApi.uploadFile(file.tempFilePath); | 435 | const result = await FileApi.uploadFile(file.tempFilePath); |
| 452 | videoFilePath.value = result.data; | 436 | videoFilePath.value = result.data; |
| 453 | - videoUploaded.value = true; // 标记视频上传成功 | 437 | + videoUploaded.value = true; |
| 454 | uni.showToast({ | 438 | uni.showToast({ |
| 455 | title: file.fileType === 'video' ? '视频选择成功' : '图片选择成功', | 439 | title: file.fileType === 'video' ? '视频选择成功' : '图片选择成功', |
| 456 | - icon: "success" | 440 | + icon: 'success', |
| 457 | }); | 441 | }); |
| 458 | }, | 442 | }, |
| 459 | fail: () => { | 443 | fail: () => { |
| 460 | - videoUploaded.value = false; // ❌ 选择失败 | ||
| 461 | - uni.showToast({ title: "选择失败", icon: "none" }); | ||
| 462 | - } | ||
| 463 | - }) | 444 | + videoUploaded.value = false; |
| 445 | + uni.showToast({ title: '选择失败', icon: 'none' }); | ||
| 446 | + }, | ||
| 447 | + }); | ||
| 464 | }; | 448 | }; |
| 465 | 449 | ||
| 466 | -// 封面选择 + 真实上传 | ||
| 467 | -const openCoverSelector = () => { | ||
| 468 | - uni.chooseImage({ | 450 | +const chooseTutorialVideo = () => { |
| 451 | + uni.chooseMedia({ | ||
| 469 | count: 1, | 452 | count: 1, |
| 453 | + mediaType: ['video'], | ||
| 470 | success: async (res) => { | 454 | success: async (res) => { |
| 471 | - const path = res.tempFilePaths[0]; | ||
| 472 | - uni.showLoading({ title: "上传中..." }); | ||
| 473 | - // 封面上传 | ||
| 474 | - try { | ||
| 475 | - const result = await FileApi.uploadFile(path); | ||
| 476 | - console.log(res, 'res'); | ||
| 477 | - | ||
| 478 | - coverImagePath.value = result.data; | ||
| 479 | - console.log('coverImagePath', coverImagePath.value); | ||
| 480 | - | ||
| 481 | - coverUploaded.value = true; | ||
| 482 | - uni.showToast({ | ||
| 483 | - title: "封面上传成功", | ||
| 484 | - icon: "success" | ||
| 485 | - }); | ||
| 486 | - } catch (err) { | ||
| 487 | - uni.showToast({ | ||
| 488 | - title: "上传失败", | ||
| 489 | - icon: "none" | ||
| 490 | - }); | ||
| 491 | - } finally { | ||
| 492 | - uni.hideLoading(); | ||
| 493 | - } | 455 | + const file = res.tempFiles[0]; |
| 456 | + const result = await FileApi.uploadFile(file.tempFilePath); | ||
| 457 | + urlTutorial.value = result.data; | ||
| 458 | + videoDetailName.value = '已选择详解视频'; | ||
| 459 | + uni.showToast({ title: '选择成功', icon: 'success' }); | ||
| 494 | }, | 460 | }, |
| 495 | fail: () => { | 461 | fail: () => { |
| 496 | - coverUploaded.value = false; | ||
| 497 | - uni.showToast({ | ||
| 498 | - title: "封面选择失败", | ||
| 499 | - icon: "none" | ||
| 500 | - }); | ||
| 501 | - } | 462 | + uni.showToast({ title: '选择失败', icon: 'none' }); |
| 463 | + }, | ||
| 502 | }); | 464 | }); |
| 503 | }; | 465 | }; |
| 504 | 466 | ||
| 505 | -const closeAllPopups = () => { | ||
| 506 | - showActionTypePicker.value = false; | ||
| 507 | - showDirectoryPicker.value = false; | ||
| 508 | - showCoverSelector.value = false; | ||
| 509 | - showVideoSelector.value = false; | ||
| 510 | - showMusclePicker.value = false; | ||
| 511 | - // showVideoDetailPicker.value = false; | ||
| 512 | -}; | ||
| 513 | - | ||
| 514 | -// 动作类型 | ||
| 515 | -const onActionTypeChange = (e) => { | ||
| 516 | - actionTypeIndex.value = e.detail.value[0]; | ||
| 517 | -}; | ||
| 518 | -// 动作分类列表 | ||
| 519 | -const confirmActionType = () => { | ||
| 520 | - const actionstype = actionTypeOptions.value[actionTypeIndex.value]; | ||
| 521 | - actionType.value = `${actionstype.name}`; | ||
| 522 | - closeAllPopups(); | ||
| 523 | -}; | ||
| 524 | - | ||
| 525 | -// 目录管理 | ||
| 526 | -const onBodyPartChange = (e) => { | ||
| 527 | - bodyPartIndex.value = e.detail.value[0]; | ||
| 528 | -}; | ||
| 529 | -const onEquipmentChange = (e) => { | ||
| 530 | - equipmentIndex.value = e.detail.value[0]; | ||
| 531 | -}; | ||
| 532 | -const confirmDirectory = () => { | ||
| 533 | - const selectedBodyPart = bodyPartOptions.value[bodyPartIndex.value]; | ||
| 534 | - const selectedEquipment = equipmentOptions.value[equipmentIndex.value]; | ||
| 535 | - directoryName.value = ` ${selectedBodyPart.name}( ${selectedEquipment.name})`; | ||
| 536 | - closeAllPopups(); | ||
| 537 | -}; | ||
| 538 | - | ||
| 539 | - | ||
| 540 | -// =================================保存动作==================== | 467 | +// ==================== 保存 & 删除 ==================== |
| 541 | const saveExercise = async () => { | 468 | const saveExercise = async () => { |
| 542 | if (!exerciseName.value.trim()) { | 469 | if (!exerciseName.value.trim()) { |
| 543 | uni.showToast({ title: '请输入动作名称', icon: 'none' }); | 470 | uni.showToast({ title: '请输入动作名称', icon: 'none' }); |
| 544 | return; | 471 | return; |
| 545 | } | 472 | } |
| 546 | 473 | ||
| 547 | - console.log('--', equipmentOptions.value[equipmentIndex.value]); | ||
| 548 | - console.log('--', bodyPartOptions.value[bodyPartIndex.value]); | ||
| 549 | - // 获取 categoryId 和 equipmentId | ||
| 550 | const selectedBodyPart = bodyPartOptions.value[bodyPartIndex.value]; | 474 | const selectedBodyPart = bodyPartOptions.value[bodyPartIndex.value]; |
| 551 | const selectedEquipment = equipmentOptions.value[equipmentIndex.value]; | 475 | const selectedEquipment = equipmentOptions.value[equipmentIndex.value]; |
| 552 | if (!selectedBodyPart || !selectedEquipment) { | 476 | if (!selectedBodyPart || !selectedEquipment) { |
| @@ -554,66 +478,55 @@ const saveExercise = async () => { | @@ -554,66 +478,55 @@ const saveExercise = async () => { | ||
| 554 | return; | 478 | return; |
| 555 | } | 479 | } |
| 556 | 480 | ||
| 557 | - // 动作类型映射:label -> value | ||
| 558 | const selectedActionType = actionTypeOptions.value[actionTypeIndex.value]; | 481 | const selectedActionType = actionTypeOptions.value[actionTypeIndex.value]; |
| 559 | if (!selectedActionType) { | 482 | if (!selectedActionType) { |
| 560 | uni.showToast({ title: '请选择动作类型', icon: 'none' }); | 483 | uni.showToast({ title: '请选择动作类型', icon: 'none' }); |
| 561 | return; | 484 | return; |
| 562 | } | 485 | } |
| 563 | - // 提示选择封面 | 486 | + |
| 564 | if (!coverUploaded.value) { | 487 | if (!coverUploaded.value) { |
| 565 | uni.showToast({ title: '请选择封面', icon: 'none' }); | 488 | uni.showToast({ title: '请选择封面', icon: 'none' }); |
| 566 | return; | 489 | return; |
| 567 | } | 490 | } |
| 568 | 491 | ||
| 569 | - // 视频非必填,所以不强制,但可以提示 | ||
| 570 | if (videoFilePath.value && !videoUploaded.value) { | 492 | if (videoFilePath.value && !videoUploaded.value) { |
| 571 | uni.showToast({ title: '视频未选择成功', icon: 'none' }); | 493 | uni.showToast({ title: '视频未选择成功', icon: 'none' }); |
| 572 | return; | 494 | return; |
| 573 | } | 495 | } |
| 574 | - // 构建请求参数 | 496 | + |
| 575 | const payload = { | 497 | const payload = { |
| 576 | name: exerciseName.value.trim(), | 498 | name: exerciseName.value.trim(), |
| 577 | - categoryId: selectedBodyPart.id, // 注意:必须是 id,不是 name | ||
| 578 | - equipmentId: selectedEquipment.id, // 同上 | ||
| 579 | - exerciseType: selectedActionType.id, // 这里传数字 1/2/3 | ||
| 580 | - primaryMuscles: JSON.stringify(primaryMuscles.value), //后端要求改json格式 | 499 | + categoryId: selectedBodyPart.id, |
| 500 | + equipmentId: selectedEquipment.id, | ||
| 501 | + exerciseType: selectedActionType.id, | ||
| 502 | + primaryMuscles: JSON.stringify(primaryMuscles.value), | ||
| 581 | secondaryMuscles: JSON.stringify(secondaryMuscles.value), | 503 | secondaryMuscles: JSON.stringify(secondaryMuscles.value), |
| 582 | - // urlImage: coverImagePath.value, // 封面地址 | ||
| 583 | url3dAnimation: coverImagePath.value, | 504 | url3dAnimation: coverImagePath.value, |
| 584 | - urlRealPerson: videoFilePath.value, //真人实拍视频 | ||
| 585 | - urlTutorial: urlTutorial.value, // 详解视频 | 505 | + urlRealPerson: videoFilePath.value, |
| 506 | + urlTutorial: urlTutorial.value, | ||
| 586 | }; | 507 | }; |
| 587 | - // 编辑追加id | 508 | + |
| 588 | if (isEdit.value) { | 509 | if (isEdit.value) { |
| 589 | - payload.id = editActionId.value | 510 | + payload.id = editActionId.value; |
| 590 | } | 511 | } |
| 591 | - console.log('【发送的 payload】', payload); | ||
| 592 | - let res; | 512 | + |
| 593 | try { | 513 | try { |
| 594 | - if (isEdit.value) { | ||
| 595 | - res = await ExercisesApi.updateexercises(payload); | ||
| 596 | - console.log('【更新接口返回】', res); | ||
| 597 | - } else { | ||
| 598 | - res = await ExercisesApi.createExercise(payload); | ||
| 599 | - console.log('【创建接口返回】', res); | ||
| 600 | - } | 514 | + const res = isEdit.value |
| 515 | + ? await ExercisesApi.updateexercises(payload) | ||
| 516 | + : await ExercisesApi.createExercise(payload); | ||
| 601 | 517 | ||
| 602 | if (res.code === 0) { | 518 | if (res.code === 0) { |
| 603 | uni.showToast({ title: isEdit.value ? '编辑成功' : '保存成功', icon: 'success' }); | 519 | uni.showToast({ title: isEdit.value ? '编辑成功' : '保存成功', icon: 'success' }); |
| 604 | - setTimeout(() => { | ||
| 605 | - uni.navigateBack(); | ||
| 606 | - }, 1000); | 520 | + setTimeout(() => uni.navigateBack(), 1000); |
| 607 | } else { | 521 | } else { |
| 608 | uni.showToast({ title: res.msg || '保存失败', icon: 'none' }); | 522 | uni.showToast({ title: res.msg || '保存失败', icon: 'none' }); |
| 609 | } | 523 | } |
| 610 | } catch (error) { | 524 | } catch (error) { |
| 611 | - console.error('创建动作失败:', error); | 525 | + console.error('保存动作失败:', error); |
| 612 | uni.showToast({ title: '网络错误,请重试', icon: 'none' }); | 526 | uni.showToast({ title: '网络错误,请重试', icon: 'none' }); |
| 613 | } | 527 | } |
| 614 | }; | 528 | }; |
| 615 | 529 | ||
| 616 | -// 删除动作 | ||
| 617 | const deleteAction = () => { | 530 | const deleteAction = () => { |
| 618 | uni.showModal({ | 531 | uni.showModal({ |
| 619 | title: '提示', | 532 | title: '提示', |
| @@ -621,501 +534,384 @@ const deleteAction = () => { | @@ -621,501 +534,384 @@ const deleteAction = () => { | ||
| 621 | success: async (res) => { | 534 | success: async (res) => { |
| 622 | if (res.confirm) { | 535 | if (res.confirm) { |
| 623 | try { | 536 | try { |
| 624 | - const delRes = await ExercisesApi.deleteexercises(editActionId.value) | 537 | + const delRes = await ExercisesApi.deleteexercises(editActionId.value); |
| 625 | if (delRes.code === 0) { | 538 | if (delRes.code === 0) { |
| 626 | - uni.showToast({ title: '删除成功', icon: 'success' }) | ||
| 627 | - setTimeout(() => uni.navigateBack(), 1000) | 539 | + uni.showToast({ title: '删除成功', icon: 'success' }); |
| 540 | + setTimeout(() => uni.navigateBack(), 1000); | ||
| 628 | } else { | 541 | } else { |
| 629 | - uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' }) | 542 | + uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' }); |
| 630 | } | 543 | } |
| 631 | } catch (err) { | 544 | } catch (err) { |
| 632 | - uni.showToast({ title: '删除请求失败', icon: 'none' }) | ||
| 633 | - } | ||
| 634 | - } | ||
| 635 | - } | ||
| 636 | - }) | ||
| 637 | -} | ||
| 638 | - | ||
| 639 | -const openMusclePicker = () => { | ||
| 640 | - showMusclePicker.value = true; | ||
| 641 | -}; | ||
| 642 | - | ||
| 643 | -const togglePrimary = (id) => { | ||
| 644 | - if (primaryMuscles.value.includes(id)) { | ||
| 645 | - primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id); | ||
| 646 | - } else { | ||
| 647 | - primaryMuscles.value.push(id); | ||
| 648 | - // 自动取消次要 | ||
| 649 | - secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id); | ||
| 650 | - } | ||
| 651 | -}; | ||
| 652 | - | ||
| 653 | -const toggleSecondary = (id) => { | ||
| 654 | - if (secondaryMuscles.value.includes(id)) { | ||
| 655 | - secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id); | ||
| 656 | - } else { | ||
| 657 | - secondaryMuscles.value.push(id); | ||
| 658 | - // 自动取消主要 | ||
| 659 | - primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id); | ||
| 660 | - } | ||
| 661 | -}; | ||
| 662 | - | ||
| 663 | -const confirmMuscleSelection = () => { | ||
| 664 | - // 构建显示文本,例如:“胸部(主), 背部(次)” | ||
| 665 | - const primaryNames = primaryMuscles.value | ||
| 666 | - .map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '') | ||
| 667 | - .filter(Boolean); | ||
| 668 | - const secondaryNames = secondaryMuscles.value | ||
| 669 | - .map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '') | ||
| 670 | - .filter(Boolean); | ||
| 671 | - | ||
| 672 | - let displayText = ''; | ||
| 673 | - if (primaryNames.length) { | ||
| 674 | - displayText += primaryNames.join('、') + '(主)'; | 545 | + uni.showToast({ title: '删除请求失败', icon: 'none' }); |
| 675 | } | 546 | } |
| 676 | - if (secondaryNames.length) { | ||
| 677 | - if (displayText) displayText += ', '; | ||
| 678 | - displayText += secondaryNames.join('、') + '(次)'; | ||
| 679 | } | 547 | } |
| 680 | - | ||
| 681 | - // 可以存到一个新字段,比如 selectedMusclesDisplay | ||
| 682 | - selectedMusclesDisplay.value = displayText || '未选择'; | ||
| 683 | - | ||
| 684 | - closeAllPopups(); | 548 | + }, |
| 549 | + }); | ||
| 685 | }; | 550 | }; |
| 686 | - | ||
| 687 | -onMounted(() => { | ||
| 688 | - // loadbodyPartOptions(); | ||
| 689 | - // loadequipmentOptions(); | ||
| 690 | - // loadMuscleGroups(); | ||
| 691 | - // loadexercisesGroups(); | ||
| 692 | -}); | ||
| 693 | </script> | 551 | </script> |
| 694 | 552 | ||
| 695 | - | ||
| 696 | -<style scoped> | ||
| 697 | -.add-custom-exercise-page { | ||
| 698 | - background-color: #1a1a1a; | 553 | +<style lang="scss" scoped> |
| 554 | +// ==================== 变量 ==================== | ||
| 555 | +$bg-dark: #1c1c1e; | ||
| 556 | +$card-bg: #fff; | ||
| 557 | +$card-radius: 24rpx; | ||
| 558 | +$primary: #ff9500; | ||
| 559 | +$primary-dark: #e68600; | ||
| 560 | +$text-main: #1a1a1a; | ||
| 561 | +$text-sub: #666; | ||
| 562 | +$text-hint: #999; | ||
| 563 | +$divider: #f0f0f0; | ||
| 564 | + | ||
| 565 | +// ==================== 页面 ==================== | ||
| 566 | +.page { | ||
| 699 | min-height: 100vh; | 567 | min-height: 100vh; |
| 700 | - padding: 20rpx; | ||
| 701 | - color: white; | ||
| 702 | -} | ||
| 703 | - | ||
| 704 | -/* 肌肉部位图 */ | ||
| 705 | - | ||
| 706 | -.muscle-container { | ||
| 707 | - display: flex; | ||
| 708 | - justify-content: space-between; | ||
| 709 | - align-items: center; | ||
| 710 | - gap: 40rpx; | 568 | + background: $bg-dark; |
| 569 | + padding: 24rpx 24rpx 0; | ||
| 570 | + color: #fff; | ||
| 711 | } | 571 | } |
| 712 | 572 | ||
| 713 | -/* 肌肉部位图容器 - 占满红色框,无多余内边距 */ | ||
| 714 | -.muscle-area { | ||
| 715 | - background: #1a1a1a; | ||
| 716 | - border-radius: 20rpx; | ||
| 717 | - /* 移除原有padding,让容器完全贴合红色框 */ | ||
| 718 | - padding: 0; | ||
| 719 | - margin: 20rpx; | 573 | +// ==================== 肌肉部位卡片 ==================== |
| 574 | +.muscle-card { | ||
| 720 | position: relative; | 575 | position: relative; |
| 721 | - /* 固定容器高度,适配移动端,可根据需求调整 */ | ||
| 722 | - height: 500rpx; | 576 | + margin-bottom: 24rpx; |
| 577 | + border-radius: $card-radius; | ||
| 578 | + height: 440rpx; | ||
| 723 | overflow: hidden; | 579 | overflow: hidden; |
| 724 | -} | ||
| 725 | 580 | ||
| 726 | -/* 肌肉图样式 - 100%占满容器,保持比例不拉伸 */ | ||
| 727 | -.muscle-image-front { | 581 | + .muscle-img { |
| 728 | width: 100%; | 582 | width: 100%; |
| 729 | height: 100%; | 583 | height: 100%; |
| 730 | - /* aspectFill:保持比例,裁剪超出部分,完美占满容器 */ | ||
| 731 | - /* 若要完整显示图片,改为 mode="aspectFit",同时调整容器高度 */ | ||
| 732 | - object-fit: cover; | ||
| 733 | - opacity: 1; | ||
| 734 | - /* 移除原有半透明,清晰显示 */ | ||
| 735 | -} | ||
| 736 | - | ||
| 737 | -.muscle-image-back { | ||
| 738 | - transform: rotateY(180deg); | ||
| 739 | -} | ||
| 740 | - | ||
| 741 | -.legend { | ||
| 742 | - display: flex; | ||
| 743 | - justify-content: center; | ||
| 744 | - margin-top: 20rpx; | ||
| 745 | - font-size: 24rpx; | ||
| 746 | - color: #ccc; | ||
| 747 | -} | 584 | + } |
| 748 | 585 | ||
| 749 | -.legend-item { | 586 | + .muscle-tip { |
| 587 | + position: absolute; | ||
| 588 | + bottom: 20rpx; | ||
| 589 | + left: 50%; | ||
| 590 | + transform: translateX(-50%); | ||
| 750 | display: flex; | 591 | display: flex; |
| 751 | align-items: center; | 592 | align-items: center; |
| 752 | - margin: 0 20rpx; | ||
| 753 | -} | ||
| 754 | - | ||
| 755 | -.dot { | ||
| 756 | - width: 16rpx; | ||
| 757 | - height: 16rpx; | ||
| 758 | - border-radius: 50%; | ||
| 759 | - margin-right: 10rpx; | ||
| 760 | -} | ||
| 761 | - | ||
| 762 | -.primary { | ||
| 763 | - background-color: #ff9900; | ||
| 764 | -} | ||
| 765 | - | ||
| 766 | -.secondary { | ||
| 767 | - background-color: #66ccff; | 593 | + gap: 8rpx; |
| 594 | + padding: 12rpx 28rpx; | ||
| 595 | + background: rgba(0, 0, 0, 0.55); | ||
| 596 | + border-radius: 40rpx; | ||
| 597 | + backdrop-filter: blur(10rpx); | ||
| 598 | + font-size: 24rpx; | ||
| 599 | + color: rgba(255, 255, 255, 0.9); | ||
| 600 | + } | ||
| 768 | } | 601 | } |
| 769 | 602 | ||
| 770 | -/* 表单区域 */ | ||
| 771 | -.form-section { | ||
| 772 | - background: white; | ||
| 773 | - border-radius: 20rpx; | 603 | +// ==================== 表单卡片 ==================== |
| 604 | +.form-card { | ||
| 605 | + background: $card-bg; | ||
| 606 | + border-radius: $card-radius; | ||
| 774 | overflow: hidden; | 607 | overflow: hidden; |
| 775 | - margin-top: 20rpx; | ||
| 776 | } | 608 | } |
| 777 | 609 | ||
| 778 | -.form-item { | ||
| 779 | - display: flex; | ||
| 780 | - flex-direction: column; | ||
| 781 | - padding: 30rpx 30rpx 30rpx 40rpx; | ||
| 782 | - border-bottom: 1rpx solid #eee; | ||
| 783 | - position: relative; | ||
| 784 | -} | 610 | +// ---- 通用行 ---- |
| 611 | +.form-row { | ||
| 612 | + padding: 28rpx 32rpx; | ||
| 613 | + border-bottom: 1rpx solid $divider; | ||
| 614 | + transition: background 0.15s; | ||
| 785 | 615 | ||
| 786 | -.action-name { | ||
| 787 | - width: 285rpx; | ||
| 788 | -} | 616 | + &:last-of-type { |
| 617 | + border-bottom: none; | ||
| 618 | + } | ||
| 789 | 619 | ||
| 790 | -.label { | ||
| 791 | - font-size: 32rpx; | ||
| 792 | - color: #333; | ||
| 793 | - margin-bottom: 10rpx; | ||
| 794 | -} | 620 | + .form-label { |
| 621 | + font-size: 30rpx; | ||
| 622 | + font-weight: 600; | ||
| 623 | + color: $text-main; | ||
| 624 | + display: block; | ||
| 795 | 625 | ||
| 796 | -.center-label { | ||
| 797 | - text-align: center; | ||
| 798 | - color: #c44b4b; | 626 | + .optional { |
| 627 | + font-size: 24rpx; | ||
| 628 | + font-weight: 400; | ||
| 629 | + color: $text-hint; | ||
| 630 | + } | ||
| 631 | + } | ||
| 799 | } | 632 | } |
| 800 | 633 | ||
| 801 | -.input-group { | 634 | +// ---- 动作名称行(横排) ---- |
| 635 | +.name-row { | ||
| 802 | display: flex; | 636 | display: flex; |
| 803 | - align-items: center; | ||
| 804 | - position: relative; | ||
| 805 | -} | 637 | + align-items: flex-start; |
| 638 | + gap: 24rpx; | ||
| 806 | 639 | ||
| 807 | -.input-field { | 640 | + .name-input-area { |
| 808 | flex: 1; | 641 | flex: 1; |
| 809 | - font-size: 28rpx; | ||
| 810 | - color: #333; | ||
| 811 | - padding: 0; | ||
| 812 | - background: none; | ||
| 813 | - border: none; | ||
| 814 | - outline: none; | ||
| 815 | -} | ||
| 816 | - | ||
| 817 | -.input-placeholder { | ||
| 818 | - color: #999; | ||
| 819 | -} | 642 | + min-width: 0; |
| 820 | 643 | ||
| 821 | -.icon-wrapper { | ||
| 822 | - width: 40rpx; | ||
| 823 | - height: 40rpx; | ||
| 824 | - margin-left: 10rpx; | ||
| 825 | -} | 644 | + .form-label { |
| 645 | + margin-bottom: 12rpx; | ||
| 646 | + } | ||
| 826 | 647 | ||
| 827 | -.edit-icon { | ||
| 828 | - width: 24rpx; | ||
| 829 | - height: 24rpx; | ||
| 830 | -} | 648 | + .name-input { |
| 649 | + height: 72rpx; | ||
| 650 | + font-size: 28rpx; | ||
| 651 | + color: $text-main; | ||
| 652 | + background: #f5f5f5; | ||
| 653 | + border-radius: 12rpx; | ||
| 654 | + padding: 0 20rpx; | ||
| 655 | + } | ||
| 656 | + } | ||
| 831 | 657 | ||
| 832 | -.cover-btn { | ||
| 833 | - position: absolute; | ||
| 834 | - right: 30rpx; | ||
| 835 | - top: 30rpx; | 658 | + .cover-upload { |
| 659 | + flex-shrink: 0; | ||
| 836 | display: flex; | 660 | display: flex; |
| 837 | flex-direction: column; | 661 | flex-direction: column; |
| 838 | align-items: center; | 662 | align-items: center; |
| 839 | - justify-content: center; | ||
| 840 | - width: 100rpx; | ||
| 841 | - height: 100rpx; | ||
| 842 | - background: #f0f0f0; | ||
| 843 | - border-radius: 10rpx; | ||
| 844 | - padding: 10rpx; | ||
| 845 | -} | 663 | + gap: 8rpx; |
| 846 | 664 | ||
| 847 | -.cover-icon { | ||
| 848 | - width: 40rpx; | ||
| 849 | - height: 40rpx; | ||
| 850 | -} | 665 | + .cover-preview { |
| 666 | + width: 140rpx; | ||
| 667 | + height: 140rpx; | ||
| 668 | + border-radius: 14rpx; | ||
| 669 | + background: #f5f5f5; | ||
| 670 | + } | ||
| 851 | 671 | ||
| 852 | -.btn-text { | ||
| 853 | - font-size: 20rpx; | ||
| 854 | - color: #333; | ||
| 855 | - margin-top: 5rpx; | 672 | + .cover-label { |
| 673 | + font-size: 22rpx; | ||
| 674 | + color: $text-hint; | ||
| 675 | + } | ||
| 676 | + } | ||
| 856 | } | 677 | } |
| 857 | 678 | ||
| 858 | -.value-wrapper { | 679 | +// ---- 选择行(训练部位/动作类型/目录管理) ---- |
| 680 | +.select-row { | ||
| 859 | display: flex; | 681 | display: flex; |
| 682 | + align-items: center; | ||
| 860 | justify-content: space-between; | 683 | justify-content: space-between; |
| 684 | + | ||
| 685 | + &:active { | ||
| 686 | + background: #fafafa; | ||
| 687 | + } | ||
| 688 | + | ||
| 689 | + .select-value { | ||
| 690 | + display: flex; | ||
| 861 | align-items: center; | 691 | align-items: center; |
| 862 | - font-size: 28rpx; | ||
| 863 | - color: #333; | ||
| 864 | - margin-top: 10rpx; | ||
| 865 | -} | 692 | + gap: 8rpx; |
| 693 | + max-width: 60%; | ||
| 694 | + overflow: hidden; | ||
| 866 | 695 | ||
| 867 | -.value { | 696 | + .sel-text { |
| 697 | + font-size: 26rpx; | ||
| 868 | color: #28c76f; | 698 | color: #28c76f; |
| 869 | - font-weight: bold; | ||
| 870 | -} | ||
| 871 | - | ||
| 872 | -.arrow-icon { | ||
| 873 | - width: 20rpx; | ||
| 874 | - height: 20rpx; | ||
| 875 | - transform: rotate(90deg); | 699 | + font-weight: 600; |
| 700 | + white-space: nowrap; | ||
| 701 | + overflow: hidden; | ||
| 702 | + text-overflow: ellipsis; | ||
| 703 | + } | ||
| 704 | + } | ||
| 876 | } | 705 | } |
| 877 | 706 | ||
| 878 | -.upload-btn { | ||
| 879 | - position: absolute; | ||
| 880 | - right: 30rpx; | ||
| 881 | - top: 30rpx; | 707 | +// ---- 媒体上传行(动作视频/视频详解) ---- |
| 708 | +.media-row { | ||
| 709 | + .media-upload { | ||
| 710 | + margin-top: 20rpx; | ||
| 882 | display: flex; | 711 | display: flex; |
| 883 | flex-direction: column; | 712 | flex-direction: column; |
| 884 | align-items: center; | 713 | align-items: center; |
| 885 | justify-content: center; | 714 | justify-content: center; |
| 886 | - width: 100rpx; | ||
| 887 | - height: 100rpx; | ||
| 888 | - background: #f0f0f0; | ||
| 889 | - border-radius: 10rpx; | ||
| 890 | - padding: 10rpx; | ||
| 891 | -} | ||
| 892 | - | ||
| 893 | -.preview-media { | ||
| 894 | - width: 100rpx; | ||
| 895 | - height: 100rpx; | ||
| 896 | - border-radius: 10rpx; | ||
| 897 | - object-fit: cover; | ||
| 898 | -} | ||
| 899 | - | ||
| 900 | -/* 隐藏视频默认控制条(可选) */ | ||
| 901 | -.upload-btn ::deep video::-webkit-media-controls { | ||
| 902 | - display: none !important; | ||
| 903 | -} | 715 | + width: 100%; |
| 716 | + height: 200rpx; | ||
| 717 | + background: #f8f8f8; | ||
| 718 | + border-radius: 16rpx; | ||
| 719 | + border: 2rpx dashed #ddd; | ||
| 720 | + gap: 12rpx; | ||
| 721 | + overflow: hidden; | ||
| 722 | + position: relative; | ||
| 723 | + transition: border-color 0.2s, background 0.2s; | ||
| 904 | 724 | ||
| 905 | -.camera-icon { | ||
| 906 | - width: 40rpx; | ||
| 907 | - height: 40rpx; | ||
| 908 | -} | 725 | + &:active { |
| 726 | + background: #f0f0f0; | ||
| 727 | + border-color: #bbb; | ||
| 728 | + } | ||
| 909 | 729 | ||
| 910 | -.upload-text { | 730 | + .media-placeholder { |
| 911 | font-size: 24rpx; | 731 | font-size: 24rpx; |
| 912 | - color: #333; | ||
| 913 | - margin-top: 5rpx; | ||
| 914 | -} | ||
| 915 | - | ||
| 916 | -.play-icon { | ||
| 917 | - width: 30rpx; | ||
| 918 | - height: 30rpx; | ||
| 919 | -} | ||
| 920 | - | ||
| 921 | -.save-button { | ||
| 922 | - margin: 40rpx 30rpx 0; | ||
| 923 | -} | 732 | + color: $text-hint; |
| 733 | + } | ||
| 924 | 734 | ||
| 925 | -.btn-save { | 735 | + .media-preview { |
| 736 | + position: absolute; | ||
| 737 | + inset: 0; | ||
| 926 | width: 100%; | 738 | width: 100%; |
| 927 | - height: 80rpx; | ||
| 928 | - background: linear-gradient(90deg, #ff9900, #ff6600); | ||
| 929 | - color: white; | ||
| 930 | - border-radius: 40rpx; | ||
| 931 | - font-size: 32rpx; | ||
| 932 | - font-weight: bold; | ||
| 933 | - border: none; | ||
| 934 | - outline: none; | ||
| 935 | -} | ||
| 936 | - | ||
| 937 | -/* 弹窗通用样式 */ | ||
| 938 | -.popup-overlay { | ||
| 939 | - position: fixed; | ||
| 940 | - top: 0; | ||
| 941 | - left: 0; | ||
| 942 | - right: 0; | ||
| 943 | - bottom: 0; | ||
| 944 | - background-color: rgba(0, 0, 0, 0.4); | ||
| 945 | - z-index: 999; | ||
| 946 | - display: flex; | ||
| 947 | - justify-content: flex-end; | ||
| 948 | - align-items: flex-end; | ||
| 949 | -} | 739 | + height: 100%; |
| 740 | + object-fit: cover; | ||
| 741 | + } | ||
| 950 | 742 | ||
| 951 | -.popup-content { | ||
| 952 | - width: 100%; | ||
| 953 | - background: white; | ||
| 954 | - border-radius: 16rpx 16rpx 0 0; | ||
| 955 | - max-height: 70vh; | ||
| 956 | - overflow: hidden; | 743 | + .media-replace-btn { |
| 744 | + position: absolute; | ||
| 745 | + bottom: 16rpx; | ||
| 746 | + right: 16rpx; | ||
| 747 | + padding: 6rpx 18rpx; | ||
| 748 | + background: rgba(0, 0, 0, 0.55); | ||
| 749 | + border-radius: 20rpx; | ||
| 750 | + font-size: 22rpx; | ||
| 751 | + color: #fff; | ||
| 752 | + backdrop-filter: blur(8rpx); | ||
| 753 | + } | ||
| 754 | + } | ||
| 957 | } | 755 | } |
| 958 | 756 | ||
| 959 | -.popup-header { | 757 | +// ---- 删除行 ---- |
| 758 | +.delete-row { | ||
| 960 | display: flex; | 759 | display: flex; |
| 961 | - justify-content: space-between; | ||
| 962 | align-items: center; | 760 | align-items: center; |
| 963 | - padding: 24rpx 32rpx; | ||
| 964 | - border-bottom: 1rpx solid #eee; | ||
| 965 | -} | 761 | + justify-content: center; |
| 762 | + gap: 10rpx; | ||
| 763 | + padding: 32rpx; | ||
| 966 | 764 | ||
| 967 | -.close-btn { | ||
| 968 | - font-size: 40rpx; | ||
| 969 | - color: #333; | ||
| 970 | -} | 765 | + .delete-text { |
| 766 | + font-size: 30rpx; | ||
| 767 | + color: #e74c3c; | ||
| 768 | + font-weight: 500; | ||
| 769 | + } | ||
| 971 | 770 | ||
| 972 | -.title { | ||
| 973 | - font-size: 32rpx; | ||
| 974 | - font-weight: bold; | ||
| 975 | - margin: 0; | 771 | + &:active { |
| 772 | + background: #fef2f2; | ||
| 773 | + } | ||
| 976 | } | 774 | } |
| 977 | 775 | ||
| 978 | -.confirm-btn { | ||
| 979 | - width: 80rpx; | ||
| 980 | - height: 40rpx; | ||
| 981 | - line-height: 40rpx; | ||
| 982 | - background-color: #ff9900; | ||
| 983 | - color: white; | ||
| 984 | - border-radius: 20rpx; | 776 | +// ==================== 保存按钮栏 ==================== |
| 777 | +.save-bar { | ||
| 778 | + padding: 24rpx 0; | ||
| 779 | + // 预留底部安全区 | ||
| 780 | + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); | ||
| 781 | + | ||
| 782 | + .btn-save { | ||
| 783 | + height: 88rpx; | ||
| 784 | + line-height: 88rpx; | ||
| 985 | text-align: center; | 785 | text-align: center; |
| 986 | - font-size: 28rpx; | ||
| 987 | -} | 786 | + background: linear-gradient(135deg, $primary, $primary-dark); |
| 787 | + color: #fff; | ||
| 788 | + border-radius: 44rpx; | ||
| 789 | + font-size: 32rpx; | ||
| 790 | + font-weight: 700; | ||
| 791 | + letter-spacing: 2rpx; | ||
| 792 | + transition: opacity 0.2s, transform 0.15s; | ||
| 988 | 793 | ||
| 989 | -.picker-view { | ||
| 990 | - height: 200rpx; | ||
| 991 | - width: 100%; | ||
| 992 | -} | 794 | + &:active { |
| 795 | + transform: scale(0.98); | ||
| 796 | + } | ||
| 993 | 797 | ||
| 994 | -.picker-item { | ||
| 995 | - display: flex; | ||
| 996 | - align-items: center; | ||
| 997 | - justify-content: center; | ||
| 998 | - font-size: 32rpx; | ||
| 999 | - color: #333; | ||
| 1000 | - height: 100%; | 798 | + &.disabled { |
| 799 | + opacity: 0.4; | ||
| 800 | + pointer-events: none; | ||
| 801 | + } | ||
| 802 | + } | ||
| 1001 | } | 803 | } |
| 1002 | 804 | ||
| 1003 | -.double-picker { | 805 | +// ==================== 弹窗通用 ==================== |
| 806 | +.popup-body { | ||
| 807 | + background: #fff; | ||
| 808 | + border-radius: 16rpx 16rpx 0 0; | ||
| 809 | + max-height: 65vh; | ||
| 1004 | display: flex; | 810 | display: flex; |
| 811 | + flex-direction: column; | ||
| 1005 | } | 812 | } |
| 1006 | 813 | ||
| 1007 | -.double-picker .picker-view.half { | ||
| 1008 | - width: 50%; | ||
| 1009 | -} | ||
| 1010 | - | ||
| 1011 | -/* 封面预览 */ | ||
| 1012 | -.cover-preview { | 814 | +.popup-head { |
| 1013 | display: flex; | 815 | display: flex; |
| 816 | + align-items: center; | ||
| 1014 | justify-content: center; | 817 | justify-content: center; |
| 1015 | - margin: 30rpx 0; | ||
| 1016 | -} | 818 | + padding: 28rpx 32rpx; |
| 819 | + border-bottom: 1rpx solid $divider; | ||
| 820 | + position: relative; | ||
| 1017 | 821 | ||
| 1018 | -.cover-image { | ||
| 1019 | - width: 200rpx; | ||
| 1020 | - height: 200rpx; | ||
| 1021 | -} | 822 | + .popup-title { |
| 823 | + font-size: 32rpx; | ||
| 824 | + font-weight: 700; | ||
| 825 | + color: $text-main; | ||
| 826 | + } | ||
| 1022 | 827 | ||
| 1023 | -/* 上传区域 */ | ||
| 1024 | -.upload-area { | 828 | + .popup-close { |
| 829 | + position: absolute; | ||
| 830 | + right: 32rpx; | ||
| 831 | + top: 50%; | ||
| 832 | + transform: translateY(-50%); | ||
| 833 | + width: 48rpx; | ||
| 834 | + height: 48rpx; | ||
| 1025 | display: flex; | 835 | display: flex; |
| 1026 | - flex-direction: column; | ||
| 1027 | align-items: center; | 836 | align-items: center; |
| 1028 | justify-content: center; | 837 | justify-content: center; |
| 1029 | - padding: 40rpx; | 838 | + } |
| 1030 | } | 839 | } |
| 1031 | 840 | ||
| 1032 | -.upload-icon { | ||
| 1033 | - width: 60rpx; | ||
| 1034 | - height: 60rpx; | ||
| 1035 | -} | 841 | +.popup-foot { |
| 842 | + padding: 20rpx 32rpx; | ||
| 843 | + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); | ||
| 844 | + border-top: 1rpx solid $divider; | ||
| 1036 | 845 | ||
| 1037 | -.upload-text { | ||
| 1038 | - font-size: 28rpx; | ||
| 1039 | - color: #666; | ||
| 1040 | - margin-top: 10rpx; | ||
| 1041 | -} | 846 | + .popup-done { |
| 847 | + height: 80rpx; | ||
| 848 | + line-height: 80rpx; | ||
| 849 | + text-align: center; | ||
| 850 | + background: linear-gradient(135deg, $primary, $primary-dark); | ||
| 851 | + color: #fff; | ||
| 852 | + border-radius: 40rpx; | ||
| 853 | + font-size: 30rpx; | ||
| 854 | + font-weight: 600; | ||
| 855 | + transition: transform 0.15s; | ||
| 1042 | 856 | ||
| 1043 | -/* === 美化后的肌肉选择列表 === */ | ||
| 1044 | -.muscle-list-beautified { | ||
| 1045 | - padding: 20rpx 32rpx; | 857 | + &:active { |
| 858 | + transform: scale(0.97); | ||
| 859 | + } | ||
| 860 | + } | ||
| 1046 | } | 861 | } |
| 1047 | 862 | ||
| 1048 | -.muscle-item-beautified { | 863 | +// ==================== 肌肉选择列表 ==================== |
| 864 | +.muscle-list { | ||
| 865 | + overflow-y: auto; | ||
| 866 | + padding: 8rpx 0; | ||
| 867 | + flex: 1; | ||
| 868 | + | ||
| 869 | + .muscle-item { | ||
| 1049 | display: flex; | 870 | display: flex; |
| 1050 | align-items: center; | 871 | align-items: center; |
| 1051 | justify-content: space-between; | 872 | justify-content: space-between; |
| 1052 | - padding: 24rpx 0; | ||
| 1053 | - border-bottom: 1rpx solid #f0f0f0; | ||
| 1054 | -} | 873 | + padding: 22rpx 32rpx; |
| 874 | + border-bottom: 1rpx solid $divider; | ||
| 1055 | 875 | ||
| 1056 | -.muscle-name { | ||
| 1057 | - font-size: 32rpx; | ||
| 1058 | - color: #333; | 876 | + .muscle-name { |
| 877 | + font-size: 30rpx; | ||
| 1059 | font-weight: 500; | 878 | font-weight: 500; |
| 879 | + color: $text-main; | ||
| 1060 | flex: 1; | 880 | flex: 1; |
| 1061 | -} | 881 | + } |
| 1062 | 882 | ||
| 1063 | -.selector-btn { | 883 | + .muscle-actions { |
| 1064 | display: flex; | 884 | display: flex; |
| 1065 | - align-items: center; | ||
| 1066 | - justify-content: center; | ||
| 1067 | - gap: 8rpx; | ||
| 1068 | - padding: 12rpx 20rpx; | ||
| 1069 | - border-radius: 50rpx; | ||
| 1070 | - background-color: #f8f8f8; | ||
| 1071 | - transition: all 0.2s ease; | ||
| 1072 | - min-width: 120rpx; | ||
| 1073 | - text-align: center; | ||
| 1074 | -} | ||
| 1075 | - | ||
| 1076 | -.selector-btn.primary { | ||
| 1077 | - border: 2rpx solid #ffcc99; | ||
| 1078 | -} | ||
| 1079 | - | ||
| 1080 | -.selector-btn.primary.active { | ||
| 1081 | - background-color: #fff3e6; | ||
| 1082 | - border-color: #ff9900; | ||
| 1083 | - box-shadow: 0 4rpx 12rpx rgba(255, 153, 0, 0.2); | ||
| 1084 | -} | ||
| 1085 | - | ||
| 1086 | -.selector-btn.secondary { | ||
| 1087 | - border: 2rpx solid #cce6ff; | ||
| 1088 | -} | ||
| 1089 | - | ||
| 1090 | -.selector-btn.secondary.active { | ||
| 1091 | - background-color: #e6f7ff; | ||
| 1092 | - border-color: #66ccff; | ||
| 1093 | - box-shadow: 0 4rpx 12rpx rgba(102, 204, 255, 0.2); | ||
| 1094 | -} | ||
| 1095 | - | ||
| 1096 | -.selector-dot { | ||
| 1097 | - width: 16rpx; | ||
| 1098 | - height: 16rpx; | ||
| 1099 | - border-radius: 50%; | ||
| 1100 | -} | ||
| 1101 | - | ||
| 1102 | -.selector-dot.primary { | ||
| 1103 | - background-color: #ff9900; | ||
| 1104 | -} | ||
| 1105 | - | ||
| 1106 | -.selector-dot.secondary { | ||
| 1107 | - background-color: #66ccff; | 885 | + gap: 16rpx; |
| 886 | + } | ||
| 887 | + } | ||
| 1108 | } | 888 | } |
| 1109 | 889 | ||
| 1110 | -.selector-label { | ||
| 1111 | - font-size: 26rpx; | ||
| 1112 | - color: #666; | 890 | +// ==================== 主要/次要 tag ==================== |
| 891 | +.tag { | ||
| 892 | + padding: 10rpx 24rpx; | ||
| 893 | + border-radius: 40rpx; | ||
| 894 | + font-size: 24rpx; | ||
| 1113 | font-weight: 500; | 895 | font-weight: 500; |
| 1114 | -} | 896 | + transition: all 0.2s; |
| 897 | + background: #f5f5f5; | ||
| 898 | + color: #999; | ||
| 899 | + border: 2rpx solid transparent; | ||
| 1115 | 900 | ||
| 1116 | -.selector-btn.primary.active .selector-label, | ||
| 1117 | -.selector-btn.secondary.active .selector-label { | ||
| 1118 | - color: #333; | ||
| 1119 | - font-weight: bold; | 901 | + &.primary { |
| 902 | + &.on { | ||
| 903 | + background: #fff6ec; | ||
| 904 | + color: $primary; | ||
| 905 | + border-color: $primary; | ||
| 906 | + } | ||
| 907 | + } | ||
| 908 | + | ||
| 909 | + &.secondary { | ||
| 910 | + &.on { | ||
| 911 | + background: #ecf6ff; | ||
| 912 | + color: #3b9cff; | ||
| 913 | + border-color: #3b9cff; | ||
| 914 | + } | ||
| 915 | + } | ||
| 1120 | } | 916 | } |
| 1121 | </style> | 917 | </style> |
| 1 | -<!-- 模板大类详情页 就是进入横栏的模板,然后点击列表的模板,进入到的页面 --> | ||
| 2 | -<!-- 第二层某一个模板大类的模板列表 --> | 1 | +<!-- 模板大类详情页:进入横栏模板后,点击列表模板进入的详情页 --> |
| 3 | <template> | 2 | <template> |
| 4 | <view class="plan-detail-page"> | 3 | <view class="plan-detail-page"> |
| 5 | - <!-- 顶部安全区占位 --> | ||
| 6 | - <!-- <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> --> | ||
| 7 | - <!-- 顶部导航栏 这里是模板大类的名字--> | ||
| 8 | - <!-- <view class="header"> | ||
| 9 | - <view class="back-btn" @click="navigateBack"> | ||
| 10 | - <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons> | ||
| 11 | - </view> | ||
| 12 | - <view class="title">{{ templateList.name }}</view> | 4 | + <u-navbar :title="templateList.name || ''" :autoBack="true" bgColor="rgba(26, 26, 26, 0.9)" titleColor="#fff" |
| 5 | + leftIconColor="#fff" :placeholder="true" :safeAreaInsetTop="true" fixed /> | ||
| 13 | 6 | ||
| 14 | - <view class="description"> | ||
| 15 | - <text>{{ templateList?.description }}</text> | ||
| 16 | - </view> | ||
| 17 | - </view> --> | ||
| 18 | - | ||
| 19 | - <view class="page-header" hover-class="none"> | ||
| 20 | - <!-- 动态导航栏:高度与胶囊对齐 --> | ||
| 21 | - <view class="nav-bar" :style="{ | ||
| 22 | - paddingTop: menuButtonInfo.top + 'px', | ||
| 23 | - height: menuButtonInfo.height + 'px' | ||
| 24 | - }"> | ||
| 25 | - <view class="nav-left" @click="goBack"> | ||
| 26 | - <uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons> | ||
| 27 | - </view> | ||
| 28 | - <view class="nav-title">{{ templateList.name }}</view> | ||
| 29 | - <view class="nav-right"></view> | ||
| 30 | - </view> | ||
| 31 | - <view class="description"> | ||
| 32 | - {{ templateList?.description }} | ||
| 33 | - </view> | ||
| 34 | - | ||
| 35 | - </view> | ||
| 36 | 7 | ||
| 37 | <scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}"> | 8 | <scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}"> |
| 38 | - | 9 | + <!-- 筛选栏 --> |
| 39 | <view class="filter-bar"> | 10 | <view class="filter-bar"> |
| 40 | - | ||
| 41 | <!-- 部位筛选 --> | 11 | <!-- 部位筛选 --> |
| 42 | <view class="filter-wrapper"> | 12 | <view class="filter-wrapper"> |
| 43 | <view class="filter-item" @click="togglePartDropdown"> | 13 | <view class="filter-item" @click="togglePartDropdown"> |
| 44 | <text>{{ activePart }}</text> | 14 | <text>{{ activePart }}</text> |
| 45 | - <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="22" color="#333"></uni-icons> | 15 | + <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="22" color="#333" /> |
| 46 | </view> | 16 | </view> |
| 47 | - | ||
| 48 | - <!-- 下拉菜单 移动到这里面!! --> | ||
| 49 | <view class="dropdown-menu" v-if="showPartDropdown"> | 17 | <view class="dropdown-menu" v-if="showPartDropdown"> |
| 50 | <view class="dropdown-item" :class="{ selected: activePartId === item.id }" v-for="item in partList" | 18 | <view class="dropdown-item" :class="{ selected: activePartId === item.id }" v-for="item in partList" |
| 51 | :key="item.id" @click="selectPart(item)"> | 19 | :key="item.id" @click="selectPart(item)"> |
| @@ -58,9 +26,8 @@ | @@ -58,9 +26,8 @@ | ||
| 58 | <view class="filter-wrapper"> | 26 | <view class="filter-wrapper"> |
| 59 | <view class="filter-item" @click="toggleSceneDropdown"> | 27 | <view class="filter-item" @click="toggleSceneDropdown"> |
| 60 | <text>{{ activeScene }}</text> | 28 | <text>{{ activeScene }}</text> |
| 61 | - <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="22" color="#333"></uni-icons> | 29 | + <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="22" color="#333" /> |
| 62 | </view> | 30 | </view> |
| 63 | - | ||
| 64 | <view class="dropdown-menu" v-if="showSceneDropdown"> | 31 | <view class="dropdown-menu" v-if="showSceneDropdown"> |
| 65 | <view class="dropdown-item" :class="{ selected: activeSceneId === item.id }" v-for="item in sceneList" | 32 | <view class="dropdown-item" :class="{ selected: activeSceneId === item.id }" v-for="item in sceneList" |
| 66 | :key="item.id" @click="selectScene(item)"> | 33 | :key="item.id" @click="selectScene(item)"> |
| @@ -68,10 +35,9 @@ | @@ -68,10 +35,9 @@ | ||
| 68 | </view> | 35 | </view> |
| 69 | </view> | 36 | </view> |
| 70 | </view> | 37 | </view> |
| 71 | - | ||
| 72 | </view> | 38 | </view> |
| 73 | 39 | ||
| 74 | - <!-- 新增:两列网格容器 --> | 40 | + <!-- 两列网格卡片 --> |
| 75 | <view class="grid-container"> | 41 | <view class="grid-container"> |
| 76 | <view class="plan-card" v-for="(items, index) in filteredTemplates" :key="index" @click="godetail(items)"> | 42 | <view class="plan-card" v-for="(items, index) in filteredTemplates" :key="index" @click="godetail(items)"> |
| 77 | <image :src="items.urlCover || lostImage" mode="aspectFill" class="card-cover" @error="handleImageError" /> | 43 | <image :src="items.urlCover || lostImage" mode="aspectFill" class="card-cover" @error="handleImageError" /> |
| @@ -101,6 +67,7 @@ | @@ -101,6 +67,7 @@ | ||
| 101 | </text> | 67 | </text> |
| 102 | </view> | 68 | </view> |
| 103 | </view> | 69 | </view> |
| 70 | + | ||
| 104 | </view> | 71 | </view> |
| 105 | </scroll-view> | 72 | </scroll-view> |
| 106 | 73 | ||
| @@ -109,425 +76,329 @@ | @@ -109,425 +76,329 @@ | ||
| 109 | </template> | 76 | </template> |
| 110 | 77 | ||
| 111 | <script setup> | 78 | <script setup> |
| 112 | -import { ref, onMounted } from 'vue'; | ||
| 113 | -import { onLoad } from '@dcloudio/uni-app'; | ||
| 114 | -import TemplatesApi from '@/sheep/api/Template/Templates'; | 79 | +import { ref } from 'vue' |
| 80 | +import { onLoad } from '@dcloudio/uni-app' | ||
| 81 | +import TemplatesApi from '@/sheep/api/Template/Templates' | ||
| 115 | import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue' | 82 | import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue' |
| 116 | 83 | ||
| 117 | -const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" | ||
| 118 | -const statusBarHeight = ref(0); | ||
| 119 | -const headerHeight = ref(44); | ||
| 120 | -const id = ref(); | ||
| 121 | -const templateList = ref({}); | ||
| 122 | -const isBigTemOffice = ref(false); // 新增:标记是否官方模板大类 | ||
| 123 | - | ||
| 124 | -// ================== 筛选功能 ================== | ||
| 125 | -const filteredTemplates = ref([]); // 筛选后列表 | ||
| 126 | - | ||
| 127 | -// 部位(从接口拿) | ||
| 128 | -const partList = ref([]); | ||
| 129 | -const rawPartData = ref([]); | ||
| 130 | -const activePartId = ref('0'); | ||
| 131 | -const activePart = ref('部位'); | ||
| 132 | - | ||
| 133 | -// 场景(固定) | ||
| 134 | -const activeSceneId = ref('0'); | ||
| 135 | -const activeScene = ref('场景'); | ||
| 136 | -const sceneList = ref([ | 84 | +// ==================== 常量 ==================== |
| 85 | +const lostImage = | ||
| 86 | + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/健身模板_1784871730506.png' | ||
| 87 | + | ||
| 88 | +const SCENE_LIST = [ | ||
| 137 | { id: '0', title: '不限' }, | 89 | { id: '0', title: '不限' }, |
| 138 | { id: '1', title: '健身房' }, | 90 | { id: '1', title: '健身房' }, |
| 139 | { id: '2', title: '仅哑铃' }, | 91 | { id: '2', title: '仅哑铃' }, |
| 140 | { id: '3', title: '仅哑铃+杠铃' }, | 92 | { id: '3', title: '仅哑铃+杠铃' }, |
| 141 | -]); | 93 | +] |
| 142 | 94 | ||
| 143 | -// 模板右上角的符号 | 95 | +// ==================== 页面数据 ==================== |
| 96 | +const id = ref() | ||
| 97 | +const templateList = ref({}) | ||
| 98 | +const isBigTemOffice = ref(false) | ||
| 144 | const folderList = ref([]) | 99 | const folderList = ref([]) |
| 145 | const templateMenuRef = ref(null) | 100 | const templateMenuRef = ref(null) |
| 146 | 101 | ||
| 147 | -// 下拉显示控制 | ||
| 148 | -const showPartDropdown = ref(false); | ||
| 149 | -const showSceneDropdown = ref(false); | ||
| 150 | -// 返回上一页函数 | ||
| 151 | -const goBack = () => { | ||
| 152 | - uni.navigateBack(); | ||
| 153 | -}; | ||
| 154 | -// 获取部位分类(接口,和首页完全一样) | 102 | +// ==================== 筛选 ==================== |
| 103 | +const filteredTemplates = ref([]) | ||
| 104 | + | ||
| 105 | +// 部位 | ||
| 106 | +const rawPartData = ref([]) | ||
| 107 | +const partList = ref([]) | ||
| 108 | +const activePartId = ref('0') | ||
| 109 | +const activePart = ref('部位') | ||
| 110 | +const showPartDropdown = ref(false) | ||
| 111 | + | ||
| 112 | +// 场景 | ||
| 113 | +const sceneList = ref(SCENE_LIST) | ||
| 114 | +const activeSceneId = ref('0') | ||
| 115 | +const activeScene = ref('场景') | ||
| 116 | +const showSceneDropdown = ref(false) | ||
| 117 | + | ||
| 118 | +// 非官方模板大类时显示省略号菜单 | ||
| 119 | +const isIcon = ref(false) | ||
| 120 | + | ||
| 121 | +// ==================== API 请求 ==================== | ||
| 155 | const getPartCategories = async () => { | 122 | const getPartCategories = async () => { |
| 156 | try { | 123 | try { |
| 157 | - const res = await TemplatesApi.getPartAllCategories(); | 124 | + const res = await TemplatesApi.getPartAllCategories() |
| 158 | if (res.code === 0) { | 125 | if (res.code === 0) { |
| 159 | - rawPartData.value = res.data; | 126 | + rawPartData.value = res.data |
| 160 | partList.value = [ | 127 | partList.value = [ |
| 161 | { title: '不限', id: '0' }, | 128 | { title: '不限', id: '0' }, |
| 162 | - ...res.data.map(item => ({ | ||
| 163 | - title: item.name, | ||
| 164 | - id: String(item.id) | ||
| 165 | - })) | ||
| 166 | - ]; | 129 | + ...res.data.map((item) => ({ title: item.name, id: String(item.id) })), |
| 130 | + ] | ||
| 167 | } | 131 | } |
| 168 | } catch (error) { | 132 | } catch (error) { |
| 169 | - console.log('部位获取失败', error); | 133 | + console.log('部位获取失败', error) |
| 170 | } | 134 | } |
| 171 | -}; | ||
| 172 | - | ||
| 173 | -const togglePartDropdown = () => { | ||
| 174 | - showPartDropdown.value = !showPartDropdown.value; | ||
| 175 | - showSceneDropdown.value = false; | ||
| 176 | -}; | ||
| 177 | - | ||
| 178 | -const toggleSceneDropdown = () => { | ||
| 179 | - showSceneDropdown.value = !showSceneDropdown.value; | ||
| 180 | - showPartDropdown.value = false; | ||
| 181 | -}; | ||
| 182 | - | ||
| 183 | -const handleImageError = (e) => { | ||
| 184 | - console.error('封面图加载失败,请检查路径:'); | ||
| 185 | - // 可选:设置默认图(需提前准备) | ||
| 186 | - // plan.value.cover = '/static/images/default-plan.jpg'; | ||
| 187 | -}; | ||
| 188 | - | 135 | +} |
| 189 | 136 | ||
| 190 | const loadTemplates = async (id) => { | 137 | const loadTemplates = async (id) => { |
| 191 | - let response | ||
| 192 | - // ====================== | 138 | + try { |
| 193 | // 官方模板大类 | 139 | // 官方模板大类 |
| 194 | - // ====================== | ||
| 195 | if (isBigTemOffice.value) { | 140 | if (isBigTemOffice.value) { |
| 196 | - response = await TemplatesApi.selecttemplates(id); | 141 | + const response = await TemplatesApi.selecttemplates(id) |
| 142 | + templateList.value = response.data | ||
| 143 | + filteredTemplates.value = response.data?.templates || [] | ||
| 144 | + } else { | ||
| 145 | + // 个人模板大类(接口待接入,暂用空数据) | ||
| 146 | + // const response = await TemplatesApi.getCustTemplateGroupDetail(id) | ||
| 147 | + templateList.value = { templates: [] } | ||
| 148 | + filteredTemplates.value = [] | ||
| 197 | } | 149 | } |
| 198 | - | ||
| 199 | - // ====================== | ||
| 200 | - // 个人模板大类 | ||
| 201 | - // ====================== | ||
| 202 | - else { | ||
| 203 | - // 个人模板大类接口 | ||
| 204 | - // response = await TemplatesApi.getCustTemplateGroupDetail(id); | ||
| 205 | - response = { data: { templates: [] } }; // 临时空数据,不报错 | 150 | + console.log('模板大类列表:', templateList.value) |
| 151 | + } catch (e) { | ||
| 152 | + console.error('加载模板失败', e) | ||
| 153 | + templateList.value = {} | ||
| 154 | + filteredTemplates.value = [] | ||
| 206 | } | 155 | } |
| 156 | +} | ||
| 207 | 157 | ||
| 208 | - templateList.value = response.data; | ||
| 209 | - console.log('模板大类列表:', templateList.value); | ||
| 210 | - | ||
| 211 | - filteredTemplates.value = response.data.templates || []; | ||
| 212 | -}; | ||
| 213 | - | ||
| 214 | -// 前端筛选:部位 + 场景 | 158 | +// ==================== 筛选方法 ==================== |
| 215 | const doFilter = () => { | 159 | const doFilter = () => { |
| 216 | - const list = templateList.value.templates || []; | ||
| 217 | - const partId = activePartId.value; | ||
| 218 | - const sceneId = activeSceneId.value; | 160 | + const list = templateList.value.templates || [] |
| 161 | + const partId = activePartId.value | ||
| 162 | + const sceneId = activeSceneId.value | ||
| 163 | + | ||
| 164 | + console.log('👉 当前选中场景id:', sceneId) | ||
| 219 | 165 | ||
| 220 | - console.log("👉 当前选中场景id:", sceneId); | ||
| 221 | - // 全部不限 → 显示全部 | ||
| 222 | if (partId === '0' && sceneId === '0') { | 166 | if (partId === '0' && sceneId === '0') { |
| 223 | - filteredTemplates.value = list; | ||
| 224 | - return; | 167 | + filteredTemplates.value = list |
| 168 | + return | ||
| 225 | } | 169 | } |
| 226 | - // 过滤 | ||
| 227 | - const result = list.filter(item => { | ||
| 228 | - console.log("👉 单条场景字段:", item.scene, item.fitScene); | ||
| 229 | - // 1. 部位匹配 | ||
| 230 | - let partMatch = true; | ||
| 231 | - if (partId !== '0') { | ||
| 232 | - const targetPart = partList.value.find(p => p.id === partId); | ||
| 233 | - partMatch = item.primaryMuscleNames?.includes(targetPart.title); | ||
| 234 | - } | ||
| 235 | - // 2. 场景匹配 | ||
| 236 | - let sceneMatch = true; | ||
| 237 | - if (sceneId !== '0') { | ||
| 238 | - sceneMatch = String(item.scene) === sceneId; | ||
| 239 | - } | ||
| 240 | - return partMatch && sceneMatch; | ||
| 241 | - }); | ||
| 242 | - filteredTemplates.value = result; | ||
| 243 | -}; | ||
| 244 | -// 选择部位 | 170 | + |
| 171 | + filteredTemplates.value = list.filter((item) => { | ||
| 172 | + const partMatch = | ||
| 173 | + partId === '0' || | ||
| 174 | + (() => { | ||
| 175 | + const targetPart = partList.value.find((p) => p.id === partId) | ||
| 176 | + return item.primaryMuscleNames?.includes(targetPart?.title) | ||
| 177 | + })() | ||
| 178 | + | ||
| 179 | + const sceneMatch = sceneId == 0 || item.scene == sceneId | ||
| 180 | + return partMatch && sceneMatch | ||
| 181 | + }) | ||
| 182 | +} | ||
| 183 | + | ||
| 245 | const selectPart = (item) => { | 184 | const selectPart = (item) => { |
| 246 | - activePart.value = item.title; | ||
| 247 | - activePartId.value = item.id; | ||
| 248 | - showPartDropdown.value = false; | ||
| 249 | - doFilter(); | ||
| 250 | -}; | 185 | + activePart.value = item.title |
| 186 | + activePartId.value = item.id | ||
| 187 | + showPartDropdown.value = false | ||
| 188 | + doFilter() | ||
| 189 | +} | ||
| 251 | 190 | ||
| 252 | -// 选择场景 | ||
| 253 | const selectScene = (item) => { | 191 | const selectScene = (item) => { |
| 254 | - activeScene.value = item.title; | ||
| 255 | - activeSceneId.value = item.id; | ||
| 256 | - showSceneDropdown.value = false; | ||
| 257 | - doFilter(); | ||
| 258 | -}; | ||
| 259 | - | ||
| 260 | -// 关闭所有下拉 | ||
| 261 | -const closeAllDropdowns = () => { | ||
| 262 | - showPartDropdown.value = false; | ||
| 263 | - showSceneDropdown.value = false; | ||
| 264 | -}; | ||
| 265 | -// | ||
| 266 | -const showTemplateMenu = (item) => { | ||
| 267 | - templateMenuRef.value.showTemplateMenu(item) | ||
| 268 | - console.log('显示当前选中省略号模板详情:', item); | 192 | + activeScene.value = item.title |
| 193 | + activeSceneId.value = item.id | ||
| 194 | + showSceneDropdown.value = false | ||
| 195 | + doFilter() | ||
| 196 | +} | ||
| 269 | 197 | ||
| 270 | -}; | 198 | +const togglePartDropdown = () => { |
| 199 | + showPartDropdown.value = !showPartDropdown.value | ||
| 200 | + showSceneDropdown.value = false | ||
| 201 | +} | ||
| 202 | + | ||
| 203 | +const toggleSceneDropdown = () => { | ||
| 204 | + showSceneDropdown.value = !showSceneDropdown.value | ||
| 205 | + showPartDropdown.value = false | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +// ==================== 导航与事件 ==================== | ||
| 209 | +const handleImageError = (e) => { | ||
| 210 | + console.error('封面图加载失败,请检查路径:', e) | ||
| 211 | +} | ||
| 212 | + | ||
| 213 | +const showTemplateMenu = (item) => { | ||
| 214 | + templateMenuRef.value?.showTemplateMenu(item) | ||
| 215 | + console.log('显示当前选中省略号模板详情:', item) | ||
| 216 | +} | ||
| 271 | 217 | ||
| 272 | -//查看模板详情 | ||
| 273 | const godetail = (items) => { | 218 | const godetail = (items) => { |
| 274 | uni.navigateTo({ | 219 | uni.navigateTo({ |
| 275 | url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${items.id}&isOffice=true`, | 220 | url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${items.id}&isOffice=true`, |
| 276 | - }); | ||
| 277 | -}; | ||
| 278 | -let isIcon = false | ||
| 279 | -// 这里收到模板大类传来的id,用它来查询模板大类包含的模板 | ||
| 280 | -onLoad((options) => { | ||
| 281 | - id.value = options.id; | ||
| 282 | - isBigTemOffice.value = options.isBigTemOffice === 'true'; | ||
| 283 | - | ||
| 284 | - console.log("接收的文件夹列表:", options.foldList); | ||
| 285 | - // 解析回来 | ||
| 286 | - if (options.foldList) { | ||
| 287 | - folderList.value = JSON.parse(decodeURIComponent(options.foldList)); | ||
| 288 | - isIcon = true | ||
| 289 | - } | ||
| 290 | - console.log("模板大类ID:", id.value); | ||
| 291 | - console.log("是否官方大类:", isBigTemOffice.value); | 221 | + }) |
| 222 | +} | ||
| 292 | 223 | ||
| 293 | - loadTemplates(id.value); | ||
| 294 | -}); | ||
| 295 | -onMounted(() => { | ||
| 296 | - const systemInfo = uni.getSystemInfoSync(); | ||
| 297 | - statusBarHeight.value = systemInfo.statusBarHeight; | ||
| 298 | - getPartCategories(); | 224 | +const handleRefreshTemplate = () => { |
| 225 | + loadTemplates(id.value) | ||
| 226 | +} | ||
| 299 | 227 | ||
| 228 | +// ==================== 生命周期 ==================== | ||
| 229 | +onLoad((options) => { | ||
| 230 | + id.value = options.id | ||
| 231 | + isBigTemOffice.value = options.isBigTemOffice === 'true' | ||
| 300 | 232 | ||
| 301 | - // 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐 | ||
| 302 | - // #ifdef MP-WEIXIN | ||
| 303 | - try { | ||
| 304 | - const rect = uni.getMenuButtonBoundingClientRect(); | ||
| 305 | - if (rect) { | ||
| 306 | - menuButtonInfo.value = { | ||
| 307 | - top: rect.top, | ||
| 308 | - height: rect.height | ||
| 309 | - }; | ||
| 310 | - } | ||
| 311 | - } catch (e) { | ||
| 312 | - console.log('获取胶囊信息失败,使用默认值', e); | 233 | + console.log('接收的文件夹列表:', options.foldList) |
| 234 | + if (options.foldList) { | ||
| 235 | + folderList.value = JSON.parse(decodeURIComponent(options.foldList)) | ||
| 236 | + isIcon.value = true | ||
| 313 | } | 237 | } |
| 314 | - // #endif | ||
| 315 | 238 | ||
| 316 | - // #ifndef MP-WEIXIN | ||
| 317 | - // 非微信小程序环境,使用系统状态栏高度 + 标准导航栏高度 | ||
| 318 | - try { | ||
| 319 | - const systemInfo = uni.getSystemInfoSync(); | ||
| 320 | - const statusBarHeight = systemInfo.statusBarHeight || 20; | ||
| 321 | - menuButtonInfo.value = { | ||
| 322 | - top: statusBarHeight, | ||
| 323 | - height: 44 // 标准导航栏高度 | ||
| 324 | - }; | ||
| 325 | - } catch (e) { | ||
| 326 | - console.log('获取系统信息失败', e); | ||
| 327 | - } | ||
| 328 | - // #endif | ||
| 329 | -}); | 239 | + console.log('模板大类ID:', id.value) |
| 240 | + console.log('是否官方大类:', isBigTemOffice.value) | ||
| 330 | 241 | ||
| 331 | -const menuButtonInfo = ref({ | ||
| 332 | - top: 44, // 默认值,避免获取失败时样式异常 | ||
| 333 | - height: 32 | ||
| 334 | -}); | 242 | + loadTemplates(id.value) |
| 243 | +}) | ||
| 335 | 244 | ||
| 245 | +getPartCategories() | ||
| 336 | </script> | 246 | </script> |
| 337 | 247 | ||
| 338 | <style lang="scss" scoped> | 248 | <style lang="scss" scoped> |
| 339 | -/* 样式保持不变 */ | 249 | +// ==================== CSS 变量 ==================== |
| 250 | +$color-bg: #f5f5f5; | ||
| 251 | +$color-dark: #1a1a1a; | ||
| 252 | +$color-white: #fff; | ||
| 253 | +$glass-bg: rgba(26, 26, 26, 0.9); | ||
| 254 | +$radius-card: 16rpx; | ||
| 255 | +$radius-pill: 30rpx; | ||
| 256 | +$shadow-card: 0 4rpx 16rpx rgba(0, 0, 0, 0.05); | ||
| 257 | +$shadow-dropdown: 0 6rpx 16rpx rgba(0, 0, 0, 0.08); | ||
| 258 | + | ||
| 259 | +// ==================== 页面布局 ==================== | ||
| 340 | .plan-detail-page { | 260 | .plan-detail-page { |
| 341 | width: 100%; | 261 | width: 100%; |
| 342 | - height: 100vh; | 262 | + height: 100%; |
| 343 | display: flex; | 263 | display: flex; |
| 344 | flex-direction: column; | 264 | flex-direction: column; |
| 345 | - background-color: #f5f5f5; | 265 | + background-color: $color-bg; |
| 346 | box-sizing: border-box; | 266 | box-sizing: border-box; |
| 347 | } | 267 | } |
| 348 | 268 | ||
| 349 | -// .status-bar-placeholder { | ||
| 350 | -// width: 100%; | ||
| 351 | -// background-color: #1a1a1a; | ||
| 352 | -// } | ||
| 353 | - | ||
| 354 | -.page-header { | ||
| 355 | - position: fixed; | ||
| 356 | - width: 100%; | ||
| 357 | - top: 0; | ||
| 358 | - left: 0; | ||
| 359 | - right: 0; | ||
| 360 | - background-color: rgba(26, 26, 26, 0.9); | ||
| 361 | - z-index: 999; | ||
| 362 | - flex-shrink: 0; | ||
| 363 | - // background-color: #fff; | ||
| 364 | - | 269 | +// ==================== 顶部导航 ==================== |
| 270 | +.header-description { | ||
| 271 | + padding: 20rpx 30rpx 20rpx; | ||
| 272 | + color: $color-white; | ||
| 273 | + font-size: 28rpx; | ||
| 274 | + line-height: 46rpx; | ||
| 275 | + background-color: $glass-bg; | ||
| 365 | } | 276 | } |
| 366 | 277 | ||
| 367 | -/* 动态导航栏:高度与胶囊完全对齐 */ | ||
| 368 | -.nav-bar { | ||
| 369 | - display: flex; | ||
| 370 | - align-items: center; | ||
| 371 | - justify-content: space-between; | ||
| 372 | - // background-color: #1a1a1a; | ||
| 373 | - /* 左右留出安全间距,避免内容贴边 */ | ||
| 374 | - padding-left: 16rpx; | ||
| 375 | - padding-right: 16rpx; | ||
| 376 | - /* 高度和顶部内边距由动态 style 控制 */ | ||
| 377 | - box-sizing: content-box; | 278 | +// ==================== 内容区域 ==================== |
| 279 | +.content { | ||
| 280 | + min-height: calc(100vh - 80px); | ||
| 378 | } | 281 | } |
| 379 | 282 | ||
| 380 | -/* 左侧返回按钮区域 - 固定宽度确保居中对齐 */ | ||
| 381 | -.nav-left { | ||
| 382 | - width: 60rpx; | 283 | +// ==================== 筛选栏 ==================== |
| 284 | +.filter-bar { | ||
| 285 | + padding: 20rpx 30rpx; | ||
| 383 | display: flex; | 286 | display: flex; |
| 384 | - align-items: center; | ||
| 385 | - justify-content: flex-start; | ||
| 386 | - flex-shrink: 0; | ||
| 387 | -} | ||
| 388 | - | ||
| 389 | -.back-icon { | ||
| 390 | - display: block; | ||
| 391 | -} | ||
| 392 | - | ||
| 393 | -/* 标题区域 - 自适应居中 */ | ||
| 394 | -.nav-title { | ||
| 395 | - flex: 1; | ||
| 396 | - text-align: center; | ||
| 397 | - font-size: 36rpx; | ||
| 398 | - font-weight: bold; | ||
| 399 | - color: #fff; | ||
| 400 | - | ||
| 401 | - line-height: 1; | ||
| 402 | -} | ||
| 403 | - | ||
| 404 | -.nav-right { | ||
| 405 | - width: 60rpx; | ||
| 406 | - flex-shrink: 0; | 287 | + gap: 20rpx; |
| 288 | + background-color: $color-white; | ||
| 407 | } | 289 | } |
| 408 | 290 | ||
| 409 | -.description { | ||
| 410 | - padding: 20rpx 30rpx 20rpx; | ||
| 411 | - color: #fff; | ||
| 412 | - // background-color: #1a1a1a; | ||
| 413 | - font-size: 28rpx; | ||
| 414 | - line-height: 46rpx; | ||
| 415 | - | 291 | +.filter-wrapper { |
| 292 | + position: relative; | ||
| 416 | } | 293 | } |
| 417 | 294 | ||
| 418 | - | ||
| 419 | -// | ||
| 420 | - | ||
| 421 | -.header { | ||
| 422 | - width: 100%; | ||
| 423 | - // position: relative; | 295 | +.filter-item { |
| 296 | + height: 60rpx; | ||
| 424 | display: flex; | 297 | display: flex; |
| 425 | align-items: center; | 298 | align-items: center; |
| 426 | justify-content: center; | 299 | justify-content: center; |
| 427 | - background-color: #1a1a1a; | ||
| 428 | - color: #fff; | ||
| 429 | - font-size: 36rpx; | ||
| 430 | - font-weight: 600; | ||
| 431 | - // z-index: 10; | ||
| 432 | - // z-index: 999; | 300 | + padding: 0 15rpx; |
| 301 | + line-height: 60rpx; | ||
| 302 | + border-radius: $radius-pill; | ||
| 303 | + background-color: $color-white; | ||
| 304 | + border: 1px solid #ddd; | ||
| 305 | + color: #333; | ||
| 306 | + font-size: 28rpx; | ||
| 307 | + font-weight: 400; | ||
| 308 | + gap: 8rpx; | ||
| 309 | + | ||
| 310 | + &:active { | ||
| 311 | + background-color: $color-bg; | ||
| 312 | + } | ||
| 433 | } | 313 | } |
| 434 | 314 | ||
| 435 | -.back-btn { | 315 | +.dropdown-menu { |
| 436 | position: absolute; | 316 | position: absolute; |
| 437 | - left: 20rpx; | ||
| 438 | - top: 50%; | ||
| 439 | - transform: translateY(-50%); | ||
| 440 | - width: 50rpx; | ||
| 441 | - height: 50rpx; | ||
| 442 | - display: flex; | ||
| 443 | - align-items: center; | ||
| 444 | - justify-content: center; | ||
| 445 | - z-index: 11; | ||
| 446 | - color: #fff; | 317 | + top: calc(100% + 8rpx); |
| 318 | + left: 0; | ||
| 319 | + background: $color-white; | ||
| 320 | + border-radius: 14rpx; | ||
| 321 | + box-shadow: $shadow-dropdown; | ||
| 322 | + padding: 12rpx 0; | ||
| 323 | + min-width: 160rpx; | ||
| 324 | + max-height: 320rpx; | ||
| 325 | + overflow-y: auto; | ||
| 326 | + z-index: 9999; | ||
| 447 | } | 327 | } |
| 448 | 328 | ||
| 449 | -.back-icon { | ||
| 450 | - font-size: 32rpx; | ||
| 451 | - color: #fff; | ||
| 452 | - font-weight: bold; | ||
| 453 | -} | 329 | +.dropdown-item { |
| 330 | + padding: 16rpx 24rpx; | ||
| 331 | + font-size: 28rpx; | ||
| 332 | + line-height: 1.4; | ||
| 333 | + color: $color-dark; | ||
| 334 | + background-color: $color-white; | ||
| 335 | + white-space: nowrap; | ||
| 454 | 336 | ||
| 455 | -.content { | ||
| 456 | - flex: 1; | ||
| 457 | - min-height: 0; | ||
| 458 | - padding-top: 20rpx; | ||
| 459 | - // margin-top: v-bind(statusBarHeight + headerHeight + 'px'); | ||
| 460 | - margin-top: 236rpx; | ||
| 461 | - | ||
| 462 | - /* #ifdef H5 */ | ||
| 463 | - min-height: 890px; | ||
| 464 | - margin-top: 100px; | ||
| 465 | - /* #endif */ | 337 | + &.selected { |
| 338 | + background: #f0f9f4; | ||
| 339 | + color: #2e9d5a; | ||
| 340 | + font-weight: 500; | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + &:active { | ||
| 344 | + opacity: 0.8; | ||
| 345 | + } | ||
| 466 | } | 346 | } |
| 467 | 347 | ||
| 348 | +// ==================== 网格卡片 ==================== | ||
| 468 | .grid-container { | 349 | .grid-container { |
| 469 | display: grid; | 350 | display: grid; |
| 470 | - // 核心:两列等宽布局 | ||
| 471 | grid-template-columns: repeat(2, 1fr); | 351 | grid-template-columns: repeat(2, 1fr); |
| 472 | - // 列之间的间距(左右两个卡片的空隙) | ||
| 473 | column-gap: 20rpx; | 352 | column-gap: 20rpx; |
| 474 | - // 行之间的间距(上下两个卡片的空隙) | ||
| 475 | row-gap: 20rpx; | 353 | row-gap: 20rpx; |
| 476 | - // 适配小程序安全区域,避免贴边 | ||
| 477 | - padding-bottom: 40rpx; | ||
| 478 | padding: 0 20rpx 40rpx; | 354 | padding: 0 20rpx 40rpx; |
| 479 | } | 355 | } |
| 480 | 356 | ||
| 481 | -/* 调整卡片样式,适配网格布局 */ | ||
| 482 | .plan-card { | 357 | .plan-card { |
| 483 | - // 移除原来的 margin-top,改用 grid 的 row-gap 控制间距 | ||
| 484 | margin-top: 0; | 358 | margin-top: 0; |
| 485 | width: 100%; | 359 | width: 100%; |
| 486 | - background-color: #fff; | ||
| 487 | - border-radius: 16rpx; | 360 | + background-color: $color-white; |
| 361 | + border-radius: $radius-card; | ||
| 488 | overflow: hidden; | 362 | overflow: hidden; |
| 489 | - box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05); | 363 | + box-shadow: $shadow-card; |
| 490 | position: relative; | 364 | position: relative; |
| 491 | } | 365 | } |
| 492 | 366 | ||
| 493 | .card-cover { | 367 | .card-cover { |
| 494 | width: 100%; | 368 | width: 100%; |
| 495 | - // 保持你原来的高度,和参考图一致 | ||
| 496 | height: 300rpx; | 369 | height: 300rpx; |
| 497 | } | 370 | } |
| 498 | 371 | ||
| 499 | .card-info { | 372 | .card-info { |
| 500 | - // 核心优化:增强背景对比度 + 视觉层次 | ||
| 501 | position: absolute; | 373 | position: absolute; |
| 502 | left: 0; | 374 | left: 0; |
| 503 | right: 0; | 375 | right: 0; |
| 504 | bottom: 0; | 376 | bottom: 0; |
| 505 | top: 0; | 377 | top: 0; |
| 506 | padding: 20rpx 20rpx 24rpx; | 378 | padding: 20rpx 20rpx 24rpx; |
| 507 | - // 渐变遮罩:从下往上由深到浅,完美衬托文字,不遮挡图片主体 | ||
| 508 | - background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.6) 60%, rgba(0, 0, 0, 0) 100%); | ||
| 509 | - // 文字阴影:进一步强化文字辨识度 | 379 | + background: linear-gradient(to top, |
| 380 | + rgba(0, 0, 0, 0.85) 0%, | ||
| 381 | + rgba(0, 0, 0, 0.6) 60%, | ||
| 382 | + rgba(0, 0, 0, 0) 100%); | ||
| 510 | text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5); | 383 | text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5); |
| 511 | - display: flex; // 新增:开启弹性布局 | ||
| 512 | - flex-direction: column; // 新增:内部元素垂直排列 | ||
| 513 | - justify-content: space-between; // 新增:标题在最上,其他内容挤在最下 | 384 | + display: flex; |
| 385 | + flex-direction: column; | ||
| 386 | + justify-content: space-between; | ||
| 514 | } | 387 | } |
| 515 | 388 | ||
| 516 | .card-title { | 389 | .card-title { |
| 517 | - font-size: 35rpx; // 字号微增 | ||
| 518 | - font-weight: 700; // 字重加粗 | ||
| 519 | - color: #ffffff; // 纯白保证最高对比度 | 390 | + font-size: 35rpx; |
| 391 | + font-weight: 700; | ||
| 392 | + color: #ffffff; | ||
| 520 | margin-top: 0; | 393 | margin-top: 0; |
| 521 | line-height: 1.3; | 394 | line-height: 1.3; |
| 522 | display: flex; | 395 | display: flex; |
| 523 | justify-content: space-between; | 396 | justify-content: space-between; |
| 524 | 397 | ||
| 525 | - | ||
| 526 | .name { | 398 | .name { |
| 527 | max-width: 70%; | 399 | max-width: 70%; |
| 528 | white-space: pre-wrap; | 400 | white-space: pre-wrap; |
| 529 | word-wrap: break-word; | 401 | word-wrap: break-word; |
| 530 | - /* 最多显示 2 行,超出省略 */ | ||
| 531 | display: -webkit-box; | 402 | display: -webkit-box; |
| 532 | line-clamp: 2; | 403 | line-clamp: 2; |
| 533 | -webkit-box-orient: vertical; | 404 | -webkit-box-orient: vertical; |
| @@ -536,12 +407,9 @@ const menuButtonInfo = ref({ | @@ -536,12 +407,9 @@ const menuButtonInfo = ref({ | ||
| 536 | } | 407 | } |
| 537 | 408 | ||
| 538 | .card-stats { | 409 | .card-stats { |
| 539 | - // 原来的横向布局改成纵向 | ||
| 540 | display: flex; | 410 | display: flex; |
| 541 | - flex-direction: column; // 关键:让三个stat-item从上到下排成一列 | ||
| 542 | - gap: 4rpx; // 调整每个项目之间的间距 | ||
| 543 | - // margin-bottom: 16rpx; | ||
| 544 | - // 去掉原来的align-items: center,改成左对齐 | 411 | + flex-direction: column; |
| 412 | + gap: 4rpx; | ||
| 545 | align-items: flex-start; | 413 | align-items: flex-start; |
| 546 | } | 414 | } |
| 547 | 415 | ||
| @@ -553,97 +421,23 @@ const menuButtonInfo = ref({ | @@ -553,97 +421,23 @@ const menuButtonInfo = ref({ | ||
| 553 | } | 421 | } |
| 554 | 422 | ||
| 555 | .stat-number { | 423 | .stat-number { |
| 556 | - font-size: 32rpx; // 字号放大 | ||
| 557 | - font-weight: 700; // 加粗 | 424 | + font-size: 32rpx; |
| 425 | + font-weight: 700; | ||
| 558 | color: #ffffff; | 426 | color: #ffffff; |
| 559 | line-height: 1.2; | 427 | line-height: 1.2; |
| 560 | } | 428 | } |
| 561 | 429 | ||
| 562 | .stat-label { | 430 | .stat-label { |
| 563 | font-size: 26rpx; | 431 | font-size: 26rpx; |
| 564 | - color: rgba(255, 255, 255, 0.9); // 提高透明度,更清晰 | 432 | + color: rgba(255, 255, 255, 0.9); |
| 565 | line-height: 1.2; | 433 | line-height: 1.2; |
| 566 | } | 434 | } |
| 567 | 435 | ||
| 568 | -.card-tags { | ||
| 569 | - margin-top: 8rpx; | ||
| 570 | -} | ||
| 571 | - | ||
| 572 | .tags-text { | 436 | .tags-text { |
| 573 | font-size: 24rpx; | 437 | font-size: 24rpx; |
| 574 | color: rgba(255, 255, 255, 0.9); | 438 | color: rgba(255, 255, 255, 0.9); |
| 575 | - /* 让文本不换行,超出部分由:maxLines="1"控制省略 */ | ||
| 576 | white-space: nowrap; | 439 | white-space: nowrap; |
| 577 | overflow: hidden; | 440 | overflow: hidden; |
| 578 | text-overflow: ellipsis; | 441 | text-overflow: ellipsis; |
| 579 | } | 442 | } |
| 580 | - | ||
| 581 | -/* 筛选栏 */ | ||
| 582 | -.filter-bar { | ||
| 583 | - padding: 20rpx 30rpx; | ||
| 584 | - display: flex; | ||
| 585 | - gap: 20rpx; | ||
| 586 | - background-color: #fff; | ||
| 587 | -} | ||
| 588 | - | ||
| 589 | -.filter-item { | ||
| 590 | - height: 60rpx; | ||
| 591 | - display: flex; | ||
| 592 | - align-items: center; | ||
| 593 | - justify-content: center; | ||
| 594 | - padding: 0 15rpx; | ||
| 595 | - line-height: 60rpx; | ||
| 596 | - border-radius: 30rpx; | ||
| 597 | - background-color: #fff; | ||
| 598 | - border: 1px solid #ddd; | ||
| 599 | - color: #333; | ||
| 600 | - font-size: 28rpx; | ||
| 601 | - font-weight: 400; | ||
| 602 | - gap: 8rpx; | ||
| 603 | - | ||
| 604 | - &:active { | ||
| 605 | - background-color: #f5f5f5; | ||
| 606 | - } | ||
| 607 | -} | ||
| 608 | - | ||
| 609 | -/* 筛选器容器 */ | ||
| 610 | -.filter-wrapper { | ||
| 611 | - position: relative; | ||
| 612 | - // z-index: 999; | ||
| 613 | -} | ||
| 614 | - | ||
| 615 | -/* 下拉菜单 */ | ||
| 616 | -.dropdown-menu { | ||
| 617 | - position: absolute; | ||
| 618 | - top: calc(100% + 8rpx); | ||
| 619 | - left: 0; | ||
| 620 | - background: #fff; | ||
| 621 | - border-radius: 14rpx; | ||
| 622 | - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08); | ||
| 623 | - padding: 12rpx 0; | ||
| 624 | - min-width: 160rpx; | ||
| 625 | - max-height: 320rpx; | ||
| 626 | - overflow-y: auto; | ||
| 627 | - z-index: 9999; | ||
| 628 | -} | ||
| 629 | - | ||
| 630 | -/* 下拉菜单文字 */ | ||
| 631 | -.dropdown-item { | ||
| 632 | - padding: 16rpx 24rpx; | ||
| 633 | - font-size: 28rpx; | ||
| 634 | - line-height: 1.4; | ||
| 635 | - color: #1a1a1a; | ||
| 636 | - background-color: #fff; | ||
| 637 | - white-space: nowrap; | ||
| 638 | - | ||
| 639 | - &.selected { | ||
| 640 | - background: #f0f9f4; | ||
| 641 | - color: #2e9d5a; | ||
| 642 | - font-weight: 500; | ||
| 643 | - } | ||
| 644 | - | ||
| 645 | - &:active { | ||
| 646 | - opacity: 0.8; | ||
| 647 | - } | ||
| 648 | -} | ||
| 649 | </style> | 443 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <view class="template-select-page"> | 2 | <view class="template-select-page"> |
| 3 | - <view class="page-header" hover-class="none"> | ||
| 4 | - <!-- 动态导航栏:高度与胶囊对齐 --> | ||
| 5 | - <view class="nav-bar" :style="{ | ||
| 6 | - paddingTop: menuButtonInfo.top + 'px', | ||
| 7 | - height: menuButtonInfo.height + 'px' | ||
| 8 | - }"> | ||
| 9 | - <view class="nav-left" @click="goBack"> | ||
| 10 | - <uni-icons class="back-icon" type="left" size="28"></uni-icons> | ||
| 11 | - </view> | ||
| 12 | - <view class="nav-title">选择模板</view> | ||
| 13 | - <view class="nav-right"></view> | ||
| 14 | - </view> | ||
| 15 | - | 3 | + <view class="page-header"> |
| 16 | <!-- 标签页 --> | 4 | <!-- 标签页 --> |
| 17 | <view class="tab-bar"> | 5 | <view class="tab-bar"> |
| 18 | <!-- 左边的筛选按钮组 --> | 6 | <!-- 左边的筛选按钮组 --> |
| @@ -21,7 +9,7 @@ | @@ -21,7 +9,7 @@ | ||
| 21 | <view class="filter-wrapper"> | 9 | <view class="filter-wrapper"> |
| 22 | <view class="tab-item" :class="{ active: activePartId !== null }" @click="togglePartDropdown"> | 10 | <view class="tab-item" :class="{ active: activePartId !== null }" @click="togglePartDropdown"> |
| 23 | {{ activePartId === null ? '部位' : activePart }} | 11 | {{ activePartId === null ? '部位' : activePart }} |
| 24 | - <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons> | 12 | + <up-icon :name="showPartDropdown ? 'arrow-up' : 'arrow-down'" size="18" color="#333"></up-icon> |
| 25 | </view> | 13 | </view> |
| 26 | <view class="dropdown-menu" v-if="showPartDropdown"> | 14 | <view class="dropdown-menu" v-if="showPartDropdown"> |
| 27 | <view class="dropdown-item" :class="{ selected: activePartId === item.id }" v-for="item in partList" | 15 | <view class="dropdown-item" :class="{ selected: activePartId === item.id }" v-for="item in partList" |
| @@ -34,7 +22,7 @@ | @@ -34,7 +22,7 @@ | ||
| 34 | <view class="filter-wrapper"> | 22 | <view class="filter-wrapper"> |
| 35 | <view class="tab-item" :class="{ active: activeSceneId !== null }" @click="toggleSceneDropdown"> | 23 | <view class="tab-item" :class="{ active: activeSceneId !== null }" @click="toggleSceneDropdown"> |
| 36 | {{ activeSceneId === null ? '场景' : activeScene }} | 24 | {{ activeSceneId === null ? '场景' : activeScene }} |
| 37 | - <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons> | 25 | + <up-icon :name="showSceneDropdown ? 'arrow-up' : 'arrow-down'" size="18" color="#333"></up-icon> |
| 38 | </view> | 26 | </view> |
| 39 | <view class="dropdown-menu" v-if="showSceneDropdown"> | 27 | <view class="dropdown-menu" v-if="showSceneDropdown"> |
| 40 | <view class="dropdown-item" :class="{ selected: activeSceneId === item.id }" v-for="item in sceneList" | 28 | <view class="dropdown-item" :class="{ selected: activeSceneId === item.id }" v-for="item in sceneList" |
| @@ -46,30 +34,7 @@ | @@ -46,30 +34,7 @@ | ||
| 46 | </view> | 34 | </view> |
| 47 | <!-- 右边的加号 --> | 35 | <!-- 右边的加号 --> |
| 48 | <view class="add-btn" @click="toggleAddMenu"> | 36 | <view class="add-btn" @click="toggleAddMenu"> |
| 49 | - <uni-icons type="plus" size="35" color="#333"></uni-icons> | ||
| 50 | - <view v-if="showAddMenu" class="add-menu-mask" @click.stop="closeAddMenu"></view> | ||
| 51 | - <!-- 加号菜单弹窗 --> | ||
| 52 | - <view v-if="showAddMenu" class="add-menu-popup"> | ||
| 53 | - <view class="menu-item" @click="handleAddMenuClick"> | ||
| 54 | - <view class="menu-icon"> | ||
| 55 | - <!-- 新建模板图标,这里用uni-icons代替,也可以自己换图片 --> | ||
| 56 | - <uni-icons type="compose" size="24" color="#333"></uni-icons> | ||
| 57 | - </view> | ||
| 58 | - <text>新建模板</text> | ||
| 59 | - </view> | ||
| 60 | - <view class="menu-item" @click.stop="openCreateFolder"> | ||
| 61 | - <view class="menu-icon"> | ||
| 62 | - <uni-icons type="folder-add" size="24" color="#333"></uni-icons> | ||
| 63 | - </view> | ||
| 64 | - <text>新建文件夹</text> | ||
| 65 | - </view> | ||
| 66 | - <!-- <view class="menu-item"> | ||
| 67 | - <view class="menu-icon"> | ||
| 68 | - <uni-icons type="scan" size="24" color="#333"></uni-icons> | ||
| 69 | - </view> | ||
| 70 | - <text>扫码获取模板</text> | ||
| 71 | - </view> --> | ||
| 72 | - </view> | 37 | + <up-icon name="plus" size="35" color="#333"></up-icon> |
| 73 | </view> | 38 | </view> |
| 74 | </view> | 39 | </view> |
| 75 | </view> | 40 | </view> |
| @@ -77,8 +42,8 @@ | @@ -77,8 +42,8 @@ | ||
| 77 | <view class="List"> | 42 | <view class="List"> |
| 78 | <!-- 文件夹列表 --> | 43 | <!-- 文件夹列表 --> |
| 79 | <scroll-view scroll-y="true" class="files"> | 44 | <scroll-view scroll-y="true" class="files"> |
| 80 | - <view class="file-card" v-for="folder in folderList" :key="folder.id" @click="goToFolderDetail(folder)"> | ||
| 81 | - <template v-if="folder.templatesCount > 0"> | 45 | + <view v-for="folder in folderList" :key="folder.id"> |
| 46 | + <view class="file-card" v-if="folder.templatesCount > 0" @click="goToFolderDetail(folder)"> | ||
| 82 | <view class="left"> | 47 | <view class="left"> |
| 83 | <image class="img" :src="folder.urlCover || defaultFolderCover" mode="aspectFill" /> | 48 | <image class="img" :src="folder.urlCover || defaultFolderCover" mode="aspectFill" /> |
| 84 | <view class="template-state"> | 49 | <view class="template-state"> |
| @@ -87,11 +52,11 @@ | @@ -87,11 +52,11 @@ | ||
| 87 | </view> | 52 | </view> |
| 88 | </view> | 53 | </view> |
| 89 | <view class="right" @click.stop="showFolderMenu(folder)"> | 54 | <view class="right" @click.stop="showFolderMenu(folder)"> |
| 90 | - <uni-icons type="more-filled" size="22" color="#333" /> | 55 | + <up-icon name="more-dot-fill" size="22" color="#333" /> |
| 91 | </view> | 56 | </view> |
| 92 | - | ||
| 93 | - </template> | ||
| 94 | </view> | 57 | </view> |
| 58 | + </view> | ||
| 59 | + | ||
| 95 | </scroll-view> | 60 | </scroll-view> |
| 96 | 61 | ||
| 97 | <!-- 个人模板 --> | 62 | <!-- 个人模板 --> |
| @@ -104,7 +69,7 @@ | @@ -104,7 +69,7 @@ | ||
| 104 | <view class="card-top"> | 69 | <view class="card-top"> |
| 105 | <text class="card-title">{{ item.name }}</text> | 70 | <text class="card-title">{{ item.name }}</text> |
| 106 | <view @click.stop="showTemplateMenu(item)"> | 71 | <view @click.stop="showTemplateMenu(item)"> |
| 107 | - <uni-icons type="more-filled" size="22" color="#fff" /> | 72 | + <up-icon name="more-dot-fill" size="22" color="#fff" /> |
| 108 | </view> | 73 | </view> |
| 109 | </view> | 74 | </view> |
| 110 | <view class="card-stats"> | 75 | <view class="card-stats"> |
| @@ -131,28 +96,27 @@ | @@ -131,28 +96,27 @@ | ||
| 131 | </view> | 96 | </view> |
| 132 | 97 | ||
| 133 | <!-- 文件夹操作弹窗 --> | 98 | <!-- 文件夹操作弹窗 --> |
| 134 | - <uni-popup ref="folderMenuPopup" type="bottom" background-color="#fff"> | 99 | + <up-popup :show="showFolderPopup" mode="bottom" bgColor="#fff" @close="closeFolderMenu"> |
| 135 | <view class="folder-menu-popup"> | 100 | <view class="folder-menu-popup"> |
| 136 | <view class="popup-title">{{ currentFolder?.name }}</view> | 101 | <view class="popup-title">{{ currentFolder?.name }}</view> |
| 137 | <view class="menu-item" @click="handleRename"> | 102 | <view class="menu-item" @click="handleRename"> |
| 138 | - <uni-icons type="compose" size="22" color="#333" /> | 103 | + <up-icon name="edit-pen" size="22" color="#333" /> |
| 139 | <text>重命名</text> | 104 | <text>重命名</text> |
| 140 | </view> | 105 | </view> |
| 141 | <view class="menu-item danger" @click="handleDelete"> | 106 | <view class="menu-item danger" @click="handleDelete"> |
| 142 | - <uni-icons type="trash" size="22" color="#e53935" /> | 107 | + <up-icon name="trash" size="22" color="#e53935" /> |
| 143 | <text>删除文件夹</text> | 108 | <text>删除文件夹</text> |
| 144 | </view> | 109 | </view> |
| 145 | - <!-- <view class="cancel-btn" @click="closeFolderMenu">取消</view> --> | ||
| 146 | </view> | 110 | </view> |
| 147 | - </uni-popup> | 111 | + </up-popup> |
| 148 | 112 | ||
| 149 | <!-- 新建文件夹弹窗 --> | 113 | <!-- 新建文件夹弹窗 --> |
| 150 | - <uni-popup ref="createFolderPopup" type="bottom" background-color="#fff"> | 114 | + <up-popup :show="showCreateFolderPopup" mode="bottom" bgColor="#fff" @close="closeCreateFolder"> |
| 151 | <view class="create-folder-popup"> | 115 | <view class="create-folder-popup"> |
| 152 | <!-- 顶部条 --> | 116 | <!-- 顶部条 --> |
| 153 | <view class="popup-header"> | 117 | <view class="popup-header"> |
| 154 | <view class="close-btn" @click="closeCreateFolder"> | 118 | <view class="close-btn" @click="closeCreateFolder"> |
| 155 | - <uni-icons type="closeempty" size="24" color="#333" /> | 119 | + <up-icon name="close" size="24" color="#333" /> |
| 156 | </view> | 120 | </view> |
| 157 | <view class="popup-title">文件夹名称</view> | 121 | <view class="popup-title">文件夹名称</view> |
| 158 | <view class="save-btn" @click="handleCreateFolder">保存</view> | 122 | <view class="save-btn" @click="handleCreateFolder">保存</view> |
| @@ -162,7 +126,26 @@ | @@ -162,7 +126,26 @@ | ||
| 162 | <textarea class="text-area" v-model="newFolderName" placeholder="请输入文件夹名称" maxlength="20" auto-height /> | 126 | <textarea class="text-area" v-model="newFolderName" placeholder="请输入文件夹名称" maxlength="20" auto-height /> |
| 163 | </view> | 127 | </view> |
| 164 | </view> | 128 | </view> |
| 165 | - </uni-popup> | 129 | + </up-popup> |
| 130 | + | ||
| 131 | + <!-- 加号菜单遮罩层 --> | ||
| 132 | + <view v-if="showAddMenu" class="add-menu-mask" @click.stop="closeAddMenu"></view> | ||
| 133 | + <!-- 加号菜单弹窗 --> | ||
| 134 | + <view v-if="showAddMenu" class="add-menu-popup" | ||
| 135 | + :style="{ top: menuPosition.top + 'px', right: menuPosition.right + 'px' }"> | ||
| 136 | + <view class="menu-item" @click="handleAddMenuClick"> | ||
| 137 | + <view class="menu-icon"> | ||
| 138 | + <up-icon name="edit-pen" size="24" color="#333"></up-icon> | ||
| 139 | + </view> | ||
| 140 | + <text>新建模板</text> | ||
| 141 | + </view> | ||
| 142 | + <view class="menu-item" @click.stop="openCreateFolder"> | ||
| 143 | + <view class="menu-icon"> | ||
| 144 | + <up-icon name="file-text" size="24" color="#333"></up-icon> | ||
| 145 | + </view> | ||
| 146 | + <text>新建文件夹</text> | ||
| 147 | + </view> | ||
| 148 | + </view> | ||
| 166 | 149 | ||
| 167 | <TemplateMenuPopup ref="templateMenuRef" @refresh="handleRefreshTemplate" :folder-list="folderList" /> | 150 | <TemplateMenuPopup ref="templateMenuRef" @refresh="handleRefreshTemplate" :folder-list="folderList" /> |
| 168 | 151 | ||
| @@ -175,7 +158,7 @@ import { onShow } from '@dcloudio/uni-app'; | @@ -175,7 +158,7 @@ import { onShow } from '@dcloudio/uni-app'; | ||
| 175 | import TemplatesApi from '@/sheep/api/Template/Templates'; | 158 | import TemplatesApi from '@/sheep/api/Template/Templates'; |
| 176 | import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue' | 159 | import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue' |
| 177 | 160 | ||
| 178 | -const defaultFolderCover = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"; | 161 | +const defaultFolderCover = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/文件夹_1784867551849.png"; |
| 179 | 162 | ||
| 180 | // 获取模板列表 | 163 | // 获取模板列表 |
| 181 | const templateList = ref([]); | 164 | const templateList = ref([]); |
| @@ -194,11 +177,11 @@ const partList = ref([]); | @@ -194,11 +177,11 @@ const partList = ref([]); | ||
| 194 | const rawPartData = ref([]); | 177 | const rawPartData = ref([]); |
| 195 | 178 | ||
| 196 | // 文件夹弹窗相关 | 179 | // 文件夹弹窗相关 |
| 197 | -const folderMenuPopup = ref(null); | 180 | +const showFolderPopup = ref(false); |
| 198 | const currentFolder = ref(null); | 181 | const currentFolder = ref(null); |
| 199 | 182 | ||
| 200 | // 新建文件夹弹窗相关 | 183 | // 新建文件夹弹窗相关 |
| 201 | -const createFolderPopup = ref(null); | 184 | +const showCreateFolderPopup = ref(false); |
| 202 | const newFolderName = ref(''); | 185 | const newFolderName = ref(''); |
| 203 | 186 | ||
| 204 | // 编辑文件夹 | 187 | // 编辑文件夹 |
| @@ -208,7 +191,7 @@ const editingFolderId = ref(null); | @@ -208,7 +191,7 @@ const editingFolderId = ref(null); | ||
| 208 | // 菜单模板 | 191 | // 菜单模板 |
| 209 | const templateMenuRef = ref(null) | 192 | const templateMenuRef = ref(null) |
| 210 | 193 | ||
| 211 | -const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"; | 194 | +const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/健身模板_1784871730506.png"; |
| 212 | 195 | ||
| 213 | const sceneList = ref([ | 196 | const sceneList = ref([ |
| 214 | { id: null, title: '不限' }, | 197 | { id: null, title: '不限' }, |
| @@ -276,14 +259,13 @@ const openCreateFolder = () => { | @@ -276,14 +259,13 @@ const openCreateFolder = () => { | ||
| 276 | console.log('DOM更新后 showAddMenu.value =', showAddMenu.value); | 259 | console.log('DOM更新后 showAddMenu.value =', showAddMenu.value); |
| 277 | isRenaming.value = false; | 260 | isRenaming.value = false; |
| 278 | newFolderName.value = ''; | 261 | newFolderName.value = ''; |
| 279 | - createFolderPopup.value?.open(); | 262 | + showCreateFolderPopup.value = true; |
| 280 | }); | 263 | }); |
| 281 | }; | 264 | }; |
| 282 | 265 | ||
| 283 | // 关闭新建文件夹弹窗 | 266 | // 关闭新建文件夹弹窗 |
| 284 | const closeCreateFolder = () => { | 267 | const closeCreateFolder = () => { |
| 285 | - createFolderPopup.value?.close(); | ||
| 286 | - // 关闭弹窗时,重置模式标记 | 268 | + showCreateFolderPopup.value = false; |
| 287 | newFolderName.value = ''; | 269 | newFolderName.value = ''; |
| 288 | isRenaming.value = false; | 270 | isRenaming.value = false; |
| 289 | editingFolderId.value = null; | 271 | editingFolderId.value = null; |
| @@ -368,12 +350,12 @@ const doFilter = async () => { | @@ -368,12 +350,12 @@ const doFilter = async () => { | ||
| 368 | // 显示文件夹菜单弹窗 | 350 | // 显示文件夹菜单弹窗 |
| 369 | const showFolderMenu = (folder) => { | 351 | const showFolderMenu = (folder) => { |
| 370 | currentFolder.value = folder; | 352 | currentFolder.value = folder; |
| 371 | - folderMenuPopup.value?.open(); | 353 | + showFolderPopup.value = true; |
| 372 | }; | 354 | }; |
| 373 | 355 | ||
| 374 | // 关闭文件夹菜单弹窗 | 356 | // 关闭文件夹菜单弹窗 |
| 375 | const closeFolderMenu = () => { | 357 | const closeFolderMenu = () => { |
| 376 | - folderMenuPopup.value?.close(); | 358 | + showFolderPopup.value = false; |
| 377 | currentFolder.value = null; | 359 | currentFolder.value = null; |
| 378 | }; | 360 | }; |
| 379 | 361 | ||
| @@ -384,44 +366,70 @@ const handleRename = () => { | @@ -384,44 +366,70 @@ const handleRename = () => { | ||
| 384 | newFolderName.value = currentFolder.value.name; | 366 | newFolderName.value = currentFolder.value.name; |
| 385 | closeFolderMenu(); | 367 | closeFolderMenu(); |
| 386 | nextTick(() => { | 368 | nextTick(() => { |
| 387 | - createFolderPopup.value?.open(); | 369 | + showCreateFolderPopup.value = true; |
| 388 | }); | 370 | }); |
| 389 | }; | 371 | }; |
| 390 | 372 | ||
| 391 | // 删除文件夹 | 373 | // 删除文件夹 |
| 392 | const handleDelete = () => { | 374 | const handleDelete = () => { |
| 375 | + // 先捕获 folderId,再关闭弹窗,避免弹窗关闭时将 currentFolder 置空 | ||
| 376 | + const folderId = currentFolder.value?.id; | ||
| 377 | + if (!folderId) { | ||
| 378 | + uni.showToast({ title: '文件夹信息丢失,请重试', icon: 'none' }); | ||
| 379 | + closeFolderMenu(); | ||
| 380 | + return; | ||
| 381 | + } | ||
| 382 | + closeFolderMenu(); | ||
| 393 | uni.showModal({ | 383 | uni.showModal({ |
| 394 | title: '确认删除?', | 384 | title: '确认删除?', |
| 395 | content: '删除后无法恢复', | 385 | content: '删除后无法恢复', |
| 396 | confirmColor: '#e53935', | 386 | confirmColor: '#e53935', |
| 397 | success: async (res) => { | 387 | success: async (res) => { |
| 398 | if (res.confirm) { | 388 | if (res.confirm) { |
| 399 | - try { | 389 | + |
| 400 | uni.showLoading({ title: '删除中...' }); | 390 | uni.showLoading({ title: '删除中...' }); |
| 401 | - await TemplatesApi.deleteCustTemplateGroup(currentFolder.value.id); | ||
| 402 | - uni.hideLoading(); | 391 | + const res = await TemplatesApi.deleteCustTemplateGroup(folderId); |
| 392 | + | ||
| 393 | + | ||
| 394 | + | ||
| 403 | uni.showToast({ title: '删除成功' }); | 395 | uni.showToast({ title: '删除成功' }); |
| 404 | - // 重新拉取最新文件夹列表 | ||
| 405 | getFolderList(); | 396 | getFolderList(); |
| 406 | - } catch (err) { | 397 | + |
| 398 | + | ||
| 407 | uni.hideLoading(); | 399 | uni.hideLoading(); |
| 408 | - console.error('删除失败:', err); | ||
| 409 | - uni.showToast({ title: '删除失败', icon: 'none' }); | ||
| 410 | - } | ||
| 411 | - } | ||
| 412 | - closeFolderMenu(); | 400 | + |
| 413 | } | 401 | } |
| 414 | - }) | 402 | + }, |
| 403 | + }); | ||
| 415 | }; | 404 | }; |
| 416 | 405 | ||
| 417 | const showAddMenu = ref(false); | 406 | const showAddMenu = ref(false); |
| 407 | +const menuPosition = ref({ top: 0, right: 0 }); | ||
| 418 | 408 | ||
| 419 | // 点击加号按钮,显示/隐藏菜单 | 409 | // 点击加号按钮,显示/隐藏菜单 |
| 420 | const toggleAddMenu = () => { | 410 | const toggleAddMenu = () => { |
| 421 | - showAddMenu.value = !showAddMenu.value; | ||
| 422 | // 关闭其他下拉菜单 | 411 | // 关闭其他下拉菜单 |
| 423 | showPartDropdown.value = false; | 412 | showPartDropdown.value = false; |
| 424 | showSceneDropdown.value = false; | 413 | showSceneDropdown.value = false; |
| 414 | + | ||
| 415 | + if (!showAddMenu.value) { | ||
| 416 | + // 先获取加号按钮位置,再显示菜单弹窗 | ||
| 417 | + uni.createSelectorQuery() | ||
| 418 | + .select('.add-btn') | ||
| 419 | + .boundingClientRect() | ||
| 420 | + .exec((res) => { | ||
| 421 | + if (res[0]) { | ||
| 422 | + const sysInfo = uni.getSystemInfoSync(); | ||
| 423 | + menuPosition.value = { | ||
| 424 | + top: res[0].bottom + 10, | ||
| 425 | + right: sysInfo.windowWidth - res[0].right, | ||
| 426 | + }; | ||
| 427 | + } | ||
| 428 | + showAddMenu.value = true; | ||
| 429 | + }); | ||
| 430 | + } else { | ||
| 431 | + showAddMenu.value = false; | ||
| 432 | + } | ||
| 425 | }; | 433 | }; |
| 426 | 434 | ||
| 427 | // 模板图标菜单弹窗 | 435 | // 模板图标菜单弹窗 |
| @@ -433,21 +441,20 @@ const showTemplateMenu = (item) => { | @@ -433,21 +441,20 @@ const showTemplateMenu = (item) => { | ||
| 433 | // 点击菜单选项后关闭弹窗 | 441 | // 点击菜单选项后关闭弹窗 |
| 434 | const handleAddMenuClick = () => { | 442 | const handleAddMenuClick = () => { |
| 435 | showAddMenu.value = false; | 443 | showAddMenu.value = false; |
| 444 | + showPartDropdown.value = false; | ||
| 445 | + showSceneDropdown.value = false; | ||
| 446 | + | ||
| 436 | uni.navigateTo({ | 447 | uni.navigateTo({ |
| 437 | url: '/pages4/pages/xunji/wode-xinjian-moban', | 448 | url: '/pages4/pages/xunji/wode-xinjian-moban', |
| 438 | }); | 449 | }); |
| 450 | + | ||
| 439 | }; | 451 | }; |
| 440 | 452 | ||
| 441 | -// 点击遮罩关闭弹窗 | 453 | +// 关闭遮罩 |
| 442 | const closeAddMenu = () => { | 454 | const closeAddMenu = () => { |
| 443 | showAddMenu.value = false; | 455 | showAddMenu.value = false; |
| 444 | }; | 456 | }; |
| 445 | 457 | ||
| 446 | -// 返回上一页 | ||
| 447 | -const goBack = () => { | ||
| 448 | - uni.navigateBack(); | ||
| 449 | -}; | ||
| 450 | - | ||
| 451 | const goToFolderDetail = (item) => { | 458 | const goToFolderDetail = (item) => { |
| 452 | // 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4 | 459 | // 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4 |
| 453 | uni.navigateTo({ | 460 | uni.navigateTo({ |
| @@ -463,11 +470,7 @@ const goToPersonalDetail = (item) => { | @@ -463,11 +470,7 @@ const goToPersonalDetail = (item) => { | ||
| 463 | }); | 470 | }); |
| 464 | }; | 471 | }; |
| 465 | 472 | ||
| 466 | -// 获取胶囊按钮信息,用于动态适配导航栏高度 | ||
| 467 | -const menuButtonInfo = ref({ | ||
| 468 | - top: 44, // 默认值,避免获取失败时样式异常 | ||
| 469 | - height: 32 | ||
| 470 | -}); | 473 | + |
| 471 | 474 | ||
| 472 | // 添加 onShow 生命周期 | 475 | // 添加 onShow 生命周期 |
| 473 | onShow(() => { | 476 | onShow(() => { |
| @@ -479,6 +482,8 @@ onShow(() => { | @@ -479,6 +482,8 @@ onShow(() => { | ||
| 479 | activeScene.value = '不限'; | 482 | activeScene.value = '不限'; |
| 480 | showPartDropdown.value = false; | 483 | showPartDropdown.value = false; |
| 481 | showSceneDropdown.value = false; | 484 | showSceneDropdown.value = false; |
| 485 | + showAddMenu.value = false; | ||
| 486 | + | ||
| 482 | 487 | ||
| 483 | console.log('onShow刷新页面'); | 488 | console.log('onShow刷新页面'); |
| 484 | 489 | ||
| @@ -507,34 +512,6 @@ onMounted(() => { | @@ -507,34 +512,6 @@ onMounted(() => { | ||
| 507 | handleRefreshTemplate(); | 512 | handleRefreshTemplate(); |
| 508 | }); | 513 | }); |
| 509 | 514 | ||
| 510 | - // 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐 | ||
| 511 | - // #ifdef MP-WEIXIN | ||
| 512 | - try { | ||
| 513 | - const rect = uni.getMenuButtonBoundingClientRect(); | ||
| 514 | - if (rect) { | ||
| 515 | - menuButtonInfo.value = { | ||
| 516 | - top: rect.top, | ||
| 517 | - height: rect.height | ||
| 518 | - }; | ||
| 519 | - } | ||
| 520 | - } catch (e) { | ||
| 521 | - console.log('获取胶囊信息失败,使用默认值', e); | ||
| 522 | - } | ||
| 523 | - // #endif | ||
| 524 | - | ||
| 525 | - // #ifndef MP-WEIXIN | ||
| 526 | - // 非微信小程序环境,使用系统状态栏高度 + 标准导航栏高度 | ||
| 527 | - try { | ||
| 528 | - const systemInfo = uni.getSystemInfoSync(); | ||
| 529 | - const statusBarHeight = systemInfo.statusBarHeight || 20; | ||
| 530 | - menuButtonInfo.value = { | ||
| 531 | - top: statusBarHeight, | ||
| 532 | - height: 44 // 标准导航栏高度 | ||
| 533 | - }; | ||
| 534 | - } catch (e) { | ||
| 535 | - console.log('获取系统信息失败', e); | ||
| 536 | - } | ||
| 537 | - // #endif | ||
| 538 | }); | 515 | }); |
| 539 | 516 | ||
| 540 | onUnmounted(() => { | 517 | onUnmounted(() => { |
| @@ -549,61 +526,10 @@ onUnmounted(() => { | @@ -549,61 +526,10 @@ onUnmounted(() => { | ||
| 549 | background-color: #f6f6f6; | 526 | background-color: #f6f6f6; |
| 550 | min-height: 100vh; | 527 | min-height: 100vh; |
| 551 | overflow: hidden; | 528 | overflow: hidden; |
| 552 | - padding-top: calc(var(--menu-top, 44px) + var(--menu-height, 32px) + 80rpx); | ||
| 553 | -} | ||
| 554 | 529 | ||
| 555 | -.page-header { | ||
| 556 | - position: fixed; | ||
| 557 | - width: 100%; | ||
| 558 | - top: 0; | ||
| 559 | - left: 0; | ||
| 560 | - right: 0; | ||
| 561 | - z-index: 999; | ||
| 562 | - flex-shrink: 0; | ||
| 563 | - background-color: #fff; | ||
| 564 | } | 530 | } |
| 565 | 531 | ||
| 566 | -/* 动态导航栏:高度与胶囊完全对齐 */ | ||
| 567 | -.nav-bar { | ||
| 568 | - display: flex; | ||
| 569 | - align-items: center; | ||
| 570 | - justify-content: space-between; | ||
| 571 | - background-color: #fff; | ||
| 572 | - /* 左右留出安全间距,避免内容贴边 */ | ||
| 573 | - padding-left: 16rpx; | ||
| 574 | - padding-right: 16rpx; | ||
| 575 | - /* 高度和顶部内边距由动态 style 控制 */ | ||
| 576 | - box-sizing: content-box; | ||
| 577 | -} | ||
| 578 | - | ||
| 579 | -/* 左侧返回按钮区域 - 固定宽度确保居中对齐 */ | ||
| 580 | -.nav-left { | ||
| 581 | - width: 60rpx; | ||
| 582 | - display: flex; | ||
| 583 | - align-items: center; | ||
| 584 | - justify-content: flex-start; | ||
| 585 | - flex-shrink: 0; | ||
| 586 | -} | ||
| 587 | - | ||
| 588 | -.back-icon { | ||
| 589 | - display: block; | ||
| 590 | -} | ||
| 591 | - | ||
| 592 | -/* 标题区域 - 自适应居中 */ | ||
| 593 | -.nav-title { | ||
| 594 | - flex: 1; | ||
| 595 | - text-align: center; | ||
| 596 | - font-size: 36rpx; | ||
| 597 | - font-weight: bold; | ||
| 598 | - color: #333; | ||
| 599 | - | ||
| 600 | - line-height: 1; | ||
| 601 | -} | ||
| 602 | 532 | ||
| 603 | -.nav-right { | ||
| 604 | - width: 60rpx; | ||
| 605 | - flex-shrink: 0; | ||
| 606 | -} | ||
| 607 | 533 | ||
| 608 | /* 标签页 */ | 534 | /* 标签页 */ |
| 609 | .tab-bar { | 535 | .tab-bar { |
| @@ -773,6 +699,7 @@ onUnmounted(() => { | @@ -773,6 +699,7 @@ onUnmounted(() => { | ||
| 773 | position: absolute; | 699 | position: absolute; |
| 774 | top: calc(100% + 8rpx); | 700 | top: calc(100% + 8rpx); |
| 775 | left: 0; | 701 | left: 0; |
| 702 | + z-index: 999; | ||
| 776 | background: #fff; | 703 | background: #fff; |
| 777 | border-radius: 16rpx; | 704 | border-radius: 16rpx; |
| 778 | box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); | 705 | box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); |
| @@ -931,9 +858,7 @@ onUnmounted(() => { | @@ -931,9 +858,7 @@ onUnmounted(() => { | ||
| 931 | 858 | ||
| 932 | /* 加号菜单弹窗:直接放在加号下方 */ | 859 | /* 加号菜单弹窗:直接放在加号下方 */ |
| 933 | .add-menu-popup { | 860 | .add-menu-popup { |
| 934 | - position: absolute; | ||
| 935 | - top: calc(100% + 20rpx); | ||
| 936 | - right: 0; // 右对齐加号按钮 | 861 | + position: fixed; |
| 937 | background-color: #fff; | 862 | background-color: #fff; |
| 938 | border-radius: 16rpx; | 863 | border-radius: 16rpx; |
| 939 | box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); | 864 | box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="level-page"> | ||
| 3 | - <!-- 导航栏 --> | ||
| 4 | - <uni-nav-bar title="华创信" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" /> | ||
| 5 | - <view class="top-right"> | ||
| 6 | - <!-- <u-icon name="document" size="24" color="#fff" @click="showRules"></u-icon> --> | ||
| 7 | - <text class="right-text">华创信</text> | ||
| 8 | - </view> | 2 | + <view class="faq-page"> |
| 3 | + | ||
| 9 | 4 | ||
| 10 | - <!-- 用户信息 --> | ||
| 11 | - <view class="user-info"> | ||
| 12 | - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" | ||
| 13 | - mode="aspectFill" class="avatar"></image> | ||
| 14 | - <text class="username">华创信开发者</text> | 5 | + <!-- FAQ 列表 --> |
| 6 | + <view class="faq-container"> | ||
| 7 | + <view v-for="category in faqCategories" :key="category.id" class="category-block"> | ||
| 8 | + <!-- 分类标题 --> | ||
| 9 | + <view class="category-header"> | ||
| 10 | + <text class="category-dot"></text> | ||
| 11 | + <text class="category-title">{{ category.title }}</text> | ||
| 15 | </view> | 12 | </view> |
| 16 | 13 | ||
| 17 | - <!-- 更多权益 --> | ||
| 18 | - <view class="more-benefits"> | ||
| 19 | - <view class="more-text">华创信开发者</view> | ||
| 20 | - <view class="more-text">系统开发中</view> | ||
| 21 | - <view class="more-text">敬请期待~</view> | 14 | + <!-- 问题列表 --> |
| 15 | + <view class="faq-list"> | ||
| 16 | + <view | ||
| 17 | + v-for="(item, index) in category.items" | ||
| 18 | + :key="item.id" | ||
| 19 | + class="faq-item" | ||
| 20 | + :class="{ 'faq-item-last': index === category.items.length - 1 }" | ||
| 21 | + > | ||
| 22 | + <view class="faq-question" @click="toggleItem(category.id, item.id)"> | ||
| 23 | + <text class="question-text">{{ item.question }}</text> | ||
| 24 | + <text class="arrow-icon" :class="{ expanded: isExpanded(category.id, item.id) }">▼</text> | ||
| 25 | + </view> | ||
| 26 | + <view class="faq-answer" :class="{ show: isExpanded(category.id, item.id) }"> | ||
| 27 | + <view class="answer-divider"></view> | ||
| 28 | + <text class="answer-text">{{ item.answer }}</text> | ||
| 29 | + </view> | ||
| 30 | + </view> | ||
| 31 | + </view> | ||
| 32 | + </view> | ||
| 22 | </view> | 33 | </view> |
| 23 | </view> | 34 | </view> |
| 24 | </template> | 35 | </template> |
| 25 | 36 | ||
| 26 | <script> | 37 | <script> |
| 27 | export default { | 38 | export default { |
| 39 | + data() { | ||
| 40 | + return { | ||
| 41 | + expandedItems: {}, | ||
| 42 | + faqCategories: [ | ||
| 43 | + { | ||
| 44 | + id: 1, | ||
| 45 | + title: '训练相关', | ||
| 46 | + items: [ | ||
| 47 | + { | ||
| 48 | + id: 11, | ||
| 49 | + question: '新手应该怎么制定训练计划?', | ||
| 50 | + answer: '建议从每周3-4次训练开始,每次45-60分钟。前两周以熟悉动作为主,选择较轻的重量,每个动作做3组,每组12-15次。优先选择复合动作(如深蹲、卧推、硬拉),逐步建立动作模式后再提升强度。' | ||
| 51 | + }, | ||
| 52 | + { | ||
| 53 | + id: 12, | ||
| 54 | + question: '一周训练几次比较合适?', | ||
| 55 | + answer: '初学者建议每周3-4次,中级训练者每周4-5次,高级训练者每周5-6次。关键是保证训练质量而非数量,确保每组有足够的恢复时间。建议采用"练一休一"或"练二休一"的节奏。' | ||
| 56 | + }, | ||
| 57 | + { | ||
| 58 | + id: 13, | ||
| 59 | + question: '每组应该做几个,做几组?', | ||
| 60 | + answer: '增肌目标:每组8-12次,每个动作3-4组;力量增长:每组3-6次,每个动作4-5组;耐力训练:每组15-20次,每个动作2-3组。组间休息时间:增肌60-90秒,力量训练2-3分钟。' | ||
| 61 | + }, | ||
| 62 | + { | ||
| 63 | + id: 14, | ||
| 64 | + question: '有氧运动和力量训练怎么安排?', | ||
| 65 | + answer: '如果目标是增肌,建议力量训练后进行20-30分钟低强度有氧,或将有氧安排在休息日。如果目标是减脂,可以在力量训练后做30-45分钟中等强度有氧。不建议在力量训练前做长时间有氧,以免影响训练表现。' | ||
| 66 | + }, | ||
| 67 | + { | ||
| 68 | + id: 15, | ||
| 69 | + question: '训练后肌肉酸痛怎么办?', | ||
| 70 | + answer: '延迟性肌肉酸痛(DOMS)是正常现象,通常在训练后24-72小时内出现。可以通过充分拉伸、轻度活动、热敷、充足睡眠和补充蛋白质来缓解。如果疼痛异常剧烈或持续时间超过5天,建议就医检查。' | ||
| 71 | + } | ||
| 72 | + ] | ||
| 73 | + }, | ||
| 74 | + { | ||
| 75 | + id: 2, | ||
| 76 | + title: '饮食营养', | ||
| 77 | + items: [ | ||
| 78 | + { | ||
| 79 | + id: 21, | ||
| 80 | + question: '健身期间应该怎么吃?', | ||
| 81 | + answer: '遵循"高蛋白、适量碳水、低脂肪"原则。每餐应有优质蛋白(鸡胸肉、鱼、蛋、豆制品),搭配复合碳水(糙米、红薯、全麦面包)和蔬菜。每日蛋白质摄入建议为体重(kg)x 1.6-2.2克。' | ||
| 82 | + }, | ||
| 83 | + { | ||
| 84 | + id: 22, | ||
| 85 | + question: '要不要喝蛋白粉?', | ||
| 86 | + answer: '蛋白粉是方便的蛋白质补充方式,但不是必需品。优先通过食物摄取蛋白质,如果日常饮食难以满足蛋白质需求(尤其是训练后30分钟内),可以使用蛋白粉作为补充。建议选择乳清蛋白或植物蛋白粉。' | ||
| 87 | + }, | ||
| 88 | + { | ||
| 89 | + id: 23, | ||
| 90 | + question: '训练前需要吃东西吗?', | ||
| 91 | + answer: '建议训练前1-2小时摄入一顿含碳水和蛋白质的轻餐,例如:全麦面包+鸡蛋、香蕉+酸奶。训练前30分钟可以补充一根香蕉或一小把坚果。避免在训练前立即进食大量食物,以免引起胃部不适。' | ||
| 92 | + } | ||
| 93 | + ] | ||
| 94 | + }, | ||
| 95 | + { | ||
| 96 | + id: 3, | ||
| 97 | + title: '功能使用', | ||
| 98 | + items: [ | ||
| 99 | + { | ||
| 100 | + id: 31, | ||
| 101 | + question: '如何添加自定义训练动作?', | ||
| 102 | + answer: '进入"动作管理"页面,点击"新增动作",填写动作名称、选择目标肌群、设置动作类型后保存即可。添加后的动作会出现在您的动作库中,可在创建训练模板时选择使用。' | ||
| 103 | + }, | ||
| 104 | + { | ||
| 105 | + id: 32, | ||
| 106 | + question: '怎么创建训练模板?', | ||
| 107 | + answer: '进入"我的模板"页面,点击"新建模板",为模板命名后添加训练动作。可以设置每个动作的组数、次数和组间休息时间。创建完成后,每次训练可以直接使用模板,快速开始训练。' | ||
| 108 | + }, | ||
| 109 | + { | ||
| 110 | + id: 33, | ||
| 111 | + question: '训练数据会自动保存吗?', | ||
| 112 | + answer: '训练过程中数据会实时暂存,完成训练后请点击"结束训练"按钮以确保数据被完整保存到服务器。如果中途意外退出,系统会尝试恢复未完成的训练记录。' | ||
| 113 | + }, | ||
| 114 | + { | ||
| 115 | + id: 34, | ||
| 116 | + question: '如何查看历史训练记录?', | ||
| 117 | + answer: '在首页或训练日历中,可以按日期查看历史训练记录。点击具体日期可查看当天的训练详情,包括完成的动作、组数、重量和训练时长等信息。' | ||
| 118 | + } | ||
| 119 | + ] | ||
| 120 | + }, | ||
| 121 | + { | ||
| 122 | + id: 4, | ||
| 123 | + title: '其他问题', | ||
| 124 | + items: [ | ||
| 125 | + { | ||
| 126 | + id: 41, | ||
| 127 | + question: '训练多久能看到效果?', | ||
| 128 | + answer: '每个人情况不同,一般在坚持规律训练和合理饮食的前提下:4-6周开始感觉力量提升,8-12周可以看到体型变化,3-6个月出现较为明显的变化。请保持耐心,健身是一场长期旅程。' | ||
| 129 | + }, | ||
| 130 | + { | ||
| 131 | + id: 42, | ||
| 132 | + question: '经期/生病期间可以训练吗?', | ||
| 133 | + answer: '经期可以进行低强度训练(如散步、瑜伽),但应避免高强度力量训练和高冲击动作。生病期间建议充分休息,待身体完全康复后再逐步恢复训练,尤其是发烧时绝对不要训练。' | ||
| 134 | + }, | ||
| 135 | + { | ||
| 136 | + id: 43, | ||
| 137 | + question: '遇到问题如何联系我们?', | ||
| 138 | + answer: '如有任何问题或建议,可通过"我的-意见反馈"提交,或发送邮件至客服邮箱。我们会在1-3个工作日内回复处理,感谢您的支持与理解。' | ||
| 139 | + } | ||
| 140 | + ] | ||
| 141 | + } | ||
| 142 | + ] | ||
| 143 | + }; | ||
| 144 | + }, | ||
| 28 | methods: { | 145 | methods: { |
| 29 | goBack() { | 146 | goBack() { |
| 30 | uni.navigateBack(); | 147 | uni.navigateBack(); |
| 31 | }, | 148 | }, |
| 32 | - showRules() { | ||
| 33 | - uni.showToast({ title: '查看开发内容', icon: 'success' }); | ||
| 34 | - }, | 149 | + toggleItem(categoryId, itemId) { |
| 150 | + const key = `${categoryId}_${itemId}`; | ||
| 151 | + if (this.expandedItems[key]) { | ||
| 152 | + this.$delete(this.expandedItems, key); | ||
| 153 | + } else { | ||
| 154 | + this.$set(this.expandedItems, key, true); | ||
| 155 | + } | ||
| 35 | }, | 156 | }, |
| 157 | + isExpanded(categoryId, itemId) { | ||
| 158 | + const key = `${categoryId}_${itemId}`; | ||
| 159 | + return !!this.expandedItems[key]; | ||
| 160 | + } | ||
| 161 | + } | ||
| 36 | }; | 162 | }; |
| 37 | </script> | 163 | </script> |
| 38 | 164 | ||
| 39 | <style scoped> | 165 | <style scoped> |
| 40 | -.level-page { | ||
| 41 | - position: relative; | 166 | +.faq-page { |
| 42 | width: 100%; | 167 | width: 100%; |
| 43 | min-height: 100vh; | 168 | min-height: 100vh; |
| 44 | background-color: #1a1a1a; | 169 | background-color: #1a1a1a; |
| 45 | - color: white; | 170 | + color: #ffffff; |
| 46 | padding-bottom: 40rpx; | 171 | padding-bottom: 40rpx; |
| 47 | } | 172 | } |
| 48 | 173 | ||
| 49 | -.top-right { | ||
| 50 | - position: absolute; | ||
| 51 | - top: 20rpx; | ||
| 52 | - right: 20rpx; | ||
| 53 | - display: flex; | ||
| 54 | - align-items: center; | ||
| 55 | - gap: 10rpx; | ||
| 56 | - color: white; | ||
| 57 | - font-size: 24rpx; | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -.user-info { | ||
| 61 | - display: flex; | ||
| 62 | - align-items: center; | ||
| 63 | - gap: 20rpx; | 174 | +/* 分类区块 */ |
| 175 | +.faq-container { | ||
| 64 | padding: 20rpx; | 176 | padding: 20rpx; |
| 65 | - margin-top: 20rpx; | ||
| 66 | -} | ||
| 67 | - | ||
| 68 | -.avatar { | ||
| 69 | - width: 60rpx; | ||
| 70 | - height: 60rpx; | ||
| 71 | - border-radius: 50%; | ||
| 72 | - object-fit: cover; | ||
| 73 | -} | ||
| 74 | - | ||
| 75 | -.username { | ||
| 76 | - font-size: 28rpx; | ||
| 77 | -} | ||
| 78 | - | ||
| 79 | -.level-card { | ||
| 80 | - width: 600rpx; | ||
| 81 | - height: 300rpx; | ||
| 82 | - background-color: rgba(173, 216, 230, 0.3); | ||
| 83 | - border-radius: 20rpx; | ||
| 84 | - padding: 30rpx; | ||
| 85 | - margin: 20rpx auto; | ||
| 86 | - position: relative; | ||
| 87 | - overflow: hidden; | ||
| 88 | } | 177 | } |
| 89 | 178 | ||
| 90 | -.level-name { | ||
| 91 | - font-size: 40rpx; | ||
| 92 | - font-weight: bold; | ||
| 93 | - margin-bottom: 10rpx; | 179 | +.category-block { |
| 180 | + margin-bottom: 30rpx; | ||
| 94 | } | 181 | } |
| 95 | 182 | ||
| 96 | -.level-en { | ||
| 97 | - font-size: 36rpx; | ||
| 98 | - font-weight: bold; | 183 | +/* 分类标题 */ |
| 184 | +.category-header { | ||
| 185 | + display: flex; | ||
| 186 | + align-items: center; | ||
| 187 | + gap: 16rpx; | ||
| 99 | margin-bottom: 20rpx; | 188 | margin-bottom: 20rpx; |
| 189 | + padding-left: 10rpx; | ||
| 100 | } | 190 | } |
| 101 | 191 | ||
| 102 | -.require { | ||
| 103 | - font-size: 24rpx; | ||
| 104 | - margin-bottom: 20rpx; | 192 | +.category-dot { |
| 193 | + width: 12rpx; | ||
| 194 | + height: 12rpx; | ||
| 195 | + border-radius: 50%; | ||
| 196 | + background-color: #007aff; | ||
| 197 | + flex-shrink: 0; | ||
| 105 | } | 198 | } |
| 106 | 199 | ||
| 107 | -.detail { | ||
| 108 | - font-size: 24rpx; | ||
| 109 | - color: #ccc; | 200 | +.category-title { |
| 201 | + font-size: 32rpx; | ||
| 202 | + font-weight: bold; | ||
| 203 | + color: #ffffff; | ||
| 110 | } | 204 | } |
| 111 | 205 | ||
| 112 | -.progress-bar { | ||
| 113 | - width: 600rpx; | ||
| 114 | - height: 20rpx; | ||
| 115 | - background-color: #333; | ||
| 116 | - border-radius: 10rpx; | ||
| 117 | - margin: 20rpx auto; | ||
| 118 | - position: relative; | 206 | +/* FAQ 列表 */ |
| 207 | +.faq-list { | ||
| 208 | + background-color: rgba(255, 255, 255, 0.06); | ||
| 209 | + border-radius: 16rpx; | ||
| 119 | overflow: hidden; | 210 | overflow: hidden; |
| 120 | } | 211 | } |
| 121 | 212 | ||
| 122 | -.progress-track { | ||
| 123 | - width: 100%; | ||
| 124 | - height: 100%; | ||
| 125 | - background-color: #333; | ||
| 126 | - border-radius: 10rpx; | 213 | +.faq-item { |
| 214 | + border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); | ||
| 127 | } | 215 | } |
| 128 | 216 | ||
| 129 | -.progress-fill { | ||
| 130 | - width: 20%; | ||
| 131 | - height: 100%; | ||
| 132 | - background-color: #007aff; | ||
| 133 | - border-radius: 10rpx; | 217 | +.faq-item-last { |
| 218 | + border-bottom: none; | ||
| 134 | } | 219 | } |
| 135 | 220 | ||
| 136 | -.progress-dot { | ||
| 137 | - position: absolute; | ||
| 138 | - top: 50%; | ||
| 139 | - left: 20%; | ||
| 140 | - transform: translate(-50%, -50%); | ||
| 141 | - width: 12rpx; | ||
| 142 | - height: 12rpx; | ||
| 143 | - background-color: #007aff; | ||
| 144 | - border-radius: 50%; | ||
| 145 | -} | ||
| 146 | - | ||
| 147 | -.benefits-section { | ||
| 148 | - margin: 20rpx; | ||
| 149 | - padding: 0 20rpx; | ||
| 150 | -} | ||
| 151 | - | ||
| 152 | -.section-title { | ||
| 153 | - font-size: 32rpx; | ||
| 154 | - margin-bottom: 20rpx; | ||
| 155 | - padding-left: 10rpx; | ||
| 156 | -} | ||
| 157 | - | ||
| 158 | -.benefit-list { | 221 | +/* 问题行 */ |
| 222 | +.faq-question { | ||
| 159 | display: flex; | 223 | display: flex; |
| 160 | - flex-direction: row; | 224 | + align-items: center; |
| 161 | justify-content: space-between; | 225 | justify-content: space-between; |
| 162 | - gap: 16rpx; | 226 | + padding: 28rpx 24rpx; |
| 227 | + cursor: pointer; | ||
| 163 | } | 228 | } |
| 164 | 229 | ||
| 165 | -.benefit-item { | ||
| 166 | - width: 180rpx; | ||
| 167 | - background-color: rgba(255, 255, 255, 0.1); | ||
| 168 | - border-radius: 12rpx; | ||
| 169 | - padding: 16rpx 12rpx; | ||
| 170 | - text-align: center; | ||
| 171 | - box-sizing: border-box; | 230 | +.question-text { |
| 231 | + flex: 1; | ||
| 232 | + font-size: 28rpx; | ||
| 233 | + color: #e0e0e0; | ||
| 234 | + line-height: 1.5; | ||
| 235 | + padding-right: 20rpx; | ||
| 172 | } | 236 | } |
| 173 | 237 | ||
| 174 | -.icon { | ||
| 175 | - width: 36rpx; | ||
| 176 | - height: 36rpx; | ||
| 177 | - object-fit: cover; | ||
| 178 | - margin-bottom: 8rpx; | 238 | +/* 展开/收起箭头 */ |
| 239 | +.arrow-icon { | ||
| 240 | + font-size: 22rpx; | ||
| 241 | + color: #999; | ||
| 242 | + flex-shrink: 0; | ||
| 243 | + transition: transform 0.3s ease; | ||
| 179 | } | 244 | } |
| 180 | 245 | ||
| 181 | -.benefit-name { | ||
| 182 | - font-size: 22rpx; | ||
| 183 | - margin-bottom: 4rpx; | ||
| 184 | - font-weight: bold; | 246 | +.arrow-icon.expanded { |
| 247 | + transform: rotate(180deg); | ||
| 248 | + color: #007aff; | ||
| 185 | } | 249 | } |
| 186 | 250 | ||
| 187 | -.benefit-desc { | ||
| 188 | - font-size: 18rpx; | ||
| 189 | - color: #ccc; | ||
| 190 | - line-height: 1.4; | 251 | +/* 答案区域 */ |
| 252 | +.faq-answer { | ||
| 253 | + max-height: 0; | ||
| 254 | + overflow: hidden; | ||
| 255 | + transition: max-height 0.35s ease, padding 0.35s ease; | ||
| 191 | } | 256 | } |
| 192 | 257 | ||
| 193 | -.time-tag { | ||
| 194 | - font-size: 18rpx; | ||
| 195 | - color: white; | ||
| 196 | - background-color: #ff6b00; | ||
| 197 | - padding: 4rpx 8rpx; | ||
| 198 | - border-radius: 8rpx; | ||
| 199 | - margin-top: 8rpx; | ||
| 200 | - display: inline-block; | 258 | +.faq-answer.show { |
| 259 | + max-height: 1200rpx; | ||
| 201 | } | 260 | } |
| 202 | 261 | ||
| 203 | -.more-benefits { | ||
| 204 | - margin: 30rpx 20rpx 0; | ||
| 205 | - padding: 20rpx; | ||
| 206 | - background-color: rgba(255, 255, 255, 0.1); | ||
| 207 | - border-radius: 12rpx; | ||
| 208 | - text-align: center; | 262 | +.answer-divider { |
| 263 | + height: 1rpx; | ||
| 264 | + background-color: rgba(255, 255, 255, 0.06); | ||
| 265 | + margin: 0 24rpx; | ||
| 209 | } | 266 | } |
| 210 | 267 | ||
| 211 | -.more-text { | ||
| 212 | - font-size: 24rpx; | ||
| 213 | - color: #ccc; | ||
| 214 | - margin-bottom: 10rpx; | 268 | +.answer-text { |
| 269 | + display: block; | ||
| 270 | + padding: 24rpx 24rpx 32rpx 24rpx; | ||
| 271 | + font-size: 26rpx; | ||
| 272 | + color: #b0b0b0; | ||
| 273 | + line-height: 1.8; | ||
| 215 | } | 274 | } |
| 216 | </style> | 275 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <view class="level-page"> | 2 | <view class="level-page"> |
| 3 | <!-- 导航栏 --> | 3 | <!-- 导航栏 --> |
| 4 | - <uni-nav-bar title="华创信" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" /> | ||
| 5 | - <view class="top-right"> | ||
| 6 | - <!-- <u-icon name="document" size="24" color="#fff" @click="showRules"></u-icon> --> | ||
| 7 | - <text class="right-text">华创信</text> | ||
| 8 | - </view> | 4 | + <uni-nav-bar title="关于我们" left-icon="left" @click-left="goBack" :fixed="true" :status-bar="true" /> |
| 9 | 5 | ||
| 10 | - <!-- 用户信息 --> | 6 | + <!-- 用户信息 / 品牌标识 --> |
| 11 | <view class="user-info"> | 7 | <view class="user-info"> |
| 12 | - <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" | ||
| 13 | - mode="aspectFill" class="avatar"></image> | ||
| 14 | - <text class="username">华创信开发者</text> | 8 | + |
| 9 | + <view class="brand-info"> | ||
| 10 | + | ||
| 11 | + <text class="brand-slogan">科学训练,重塑自我</text> | ||
| 12 | + </view> | ||
| 13 | + </view> | ||
| 14 | + | ||
| 15 | + <!-- 介绍区域 --> | ||
| 16 | + <view class="about-section"> | ||
| 17 | + <view class="about-card"> | ||
| 18 | + <view class="about-title">品牌理念</view> | ||
| 19 | + <view class="about-content"> | ||
| 20 | + 红星健身致力于为每一位健身爱好者提供<span | ||
| 21 | + class="highlight">科学、高效、个性化</span>的训练解决方案。我们相信,健身不仅是改变体型的手段,更是一种积极向上的生活方式。无论你是健身新手还是资深训练者,红星健身都将陪伴你完成每一次突破,见证你的每一点进步。 | ||
| 22 | + </view> | ||
| 23 | + </view> | ||
| 24 | + | ||
| 25 | + <view class="about-card"> | ||
| 26 | + <view class="about-title">核心功能</view> | ||
| 27 | + <view class="feature-list"> | ||
| 28 | + <view class="feature-item" v-for="(item, index) in features" :key="index"> | ||
| 29 | + <view class="feature-num">{{ index + 1 }}</view> | ||
| 30 | + <view class="feature-text"> | ||
| 31 | + <view class="feature-name">{{ item.name }}</view> | ||
| 32 | + <view class="feature-desc">{{ item.desc }}</view> | ||
| 33 | + </view> | ||
| 34 | + </view> | ||
| 35 | + </view> | ||
| 15 | </view> | 36 | </view> |
| 16 | 37 | ||
| 17 | - <!-- 更多权益 --> | ||
| 18 | - <view class="more-benefits"> | ||
| 19 | - <view class="more-text">华创信开发者</view> | ||
| 20 | - <view class="more-text">系统开发中</view> | ||
| 21 | - <view class="more-text">敬请期待~</view> | 38 | + <!-- <view class="about-card"> |
| 39 | + <view class="about-title">联系我们</view> | ||
| 40 | + <view class="contact-list"> | ||
| 41 | + <view class="contact-item"> | ||
| 42 | + <view class="contact-label">微信公众号</view> | ||
| 43 | + <view class="contact-value">红星健身</view> | ||
| 44 | + </view> | ||
| 45 | + <view class="contact-item"> | ||
| 46 | + <view class="contact-label">客服邮箱</view> | ||
| 47 | + <view class="contact-value">support@hongxingfitness.com</view> | ||
| 48 | + </view> | ||
| 49 | + <view class="contact-item"> | ||
| 50 | + <view class="contact-label">官方网站</view> | ||
| 51 | + <view class="contact-value">www.hongxingfitness.com</view> | ||
| 52 | + </view> | ||
| 53 | + </view> | ||
| 54 | + </view> --> | ||
| 55 | + | ||
| 56 | + <view class="about-card"> | ||
| 57 | + <view class="about-title">用户协议与隐私</view> | ||
| 58 | + <view class="link-list"> | ||
| 59 | + <view class="link-item" @click="openAgreement('service')"> | ||
| 60 | + <text>服务协议</text> | ||
| 61 | + <up-icon name="arrow-right" color="#fff" size="20"></up-icon> | ||
| 62 | + </view> | ||
| 63 | + <view class="link-item" @click="openAgreement('privacy')"> | ||
| 64 | + <text>隐私政策</text> | ||
| 65 | + <up-icon name="arrow-right" color="#fff" size="20"></up-icon> | ||
| 66 | + </view> | ||
| 67 | + </view> | ||
| 68 | + </view> | ||
| 69 | + </view> | ||
| 70 | + | ||
| 71 | + <!-- 版本信息 --> | ||
| 72 | + <view class="version-info"> | ||
| 73 | + <text>当前版本 1.0.0</text> | ||
| 74 | + <text class="copyright">Copyright © 2026 红星健身 All Rights Reserved</text> | ||
| 22 | </view> | 75 | </view> |
| 23 | </view> | 76 | </view> |
| 24 | </template> | 77 | </template> |
| 25 | 78 | ||
| 26 | <script> | 79 | <script> |
| 27 | export default { | 80 | export default { |
| 81 | + data() { | ||
| 82 | + return { | ||
| 83 | + features: [ | ||
| 84 | + { | ||
| 85 | + name: '个性化训练计划', | ||
| 86 | + desc: '根据你的目标和水平,智能生成专属训练方案', | ||
| 87 | + }, | ||
| 88 | + { | ||
| 89 | + name: '海量动作库', | ||
| 90 | + desc: '覆盖全身肌群的标准动作示范与详细讲解', | ||
| 91 | + }, | ||
| 92 | + { | ||
| 93 | + name: '训练数据追踪', | ||
| 94 | + desc: '记录每一次训练,可视化你的成长轨迹', | ||
| 95 | + }, | ||
| 96 | + { | ||
| 97 | + name: '训练日历管理', | ||
| 98 | + desc: '灵活安排训练日程,养成规律健身习惯', | ||
| 99 | + }, | ||
| 100 | + ], | ||
| 101 | + }; | ||
| 102 | + }, | ||
| 28 | methods: { | 103 | methods: { |
| 29 | goBack() { | 104 | goBack() { |
| 30 | uni.navigateBack(); | 105 | uni.navigateBack(); |
| 31 | }, | 106 | }, |
| 32 | - showRules() { | ||
| 33 | - uni.showToast({ title: '查看开发内容', icon: 'success' }); | 107 | + openAgreement(type) { |
| 108 | + const titles = { | ||
| 109 | + service: '服务协议', | ||
| 110 | + privacy: '隐私政策', | ||
| 111 | + }; | ||
| 112 | + uni.showToast({ title: titles[type] || '功能开发中', icon: 'none' }); | ||
| 34 | }, | 113 | }, |
| 35 | }, | 114 | }, |
| 36 | }; | 115 | }; |
| @@ -43,174 +122,185 @@ export default { | @@ -43,174 +122,185 @@ export default { | ||
| 43 | min-height: 100vh; | 122 | min-height: 100vh; |
| 44 | background-color: #1a1a1a; | 123 | background-color: #1a1a1a; |
| 45 | color: white; | 124 | color: white; |
| 46 | - padding-bottom: 40rpx; | ||
| 47 | -} | ||
| 48 | - | ||
| 49 | -.top-right { | ||
| 50 | - position: absolute; | ||
| 51 | - top: 20rpx; | ||
| 52 | - right: 20rpx; | ||
| 53 | - display: flex; | ||
| 54 | - align-items: center; | ||
| 55 | - gap: 10rpx; | ||
| 56 | - color: white; | ||
| 57 | - font-size: 24rpx; | 125 | + padding-bottom: 60rpx; |
| 58 | } | 126 | } |
| 59 | 127 | ||
| 128 | +/* ========== 品牌标识 ========== */ | ||
| 60 | .user-info { | 129 | .user-info { |
| 61 | display: flex; | 130 | display: flex; |
| 62 | align-items: center; | 131 | align-items: center; |
| 63 | - gap: 20rpx; | ||
| 64 | - padding: 20rpx; | ||
| 65 | - margin-top: 20rpx; | 132 | + gap: 24rpx; |
| 133 | + padding: 40rpx 30rpx; | ||
| 134 | + background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%); | ||
| 135 | + border-bottom: 1rpx solid rgba(255, 255, 255, 0.06); | ||
| 66 | } | 136 | } |
| 67 | 137 | ||
| 68 | .avatar { | 138 | .avatar { |
| 69 | - width: 60rpx; | ||
| 70 | - height: 60rpx; | 139 | + width: 100rpx; |
| 140 | + height: 100rpx; | ||
| 71 | border-radius: 50%; | 141 | border-radius: 50%; |
| 72 | object-fit: cover; | 142 | object-fit: cover; |
| 143 | + border: 3rpx solid rgba(255, 107, 0, 0.4); | ||
| 144 | +} | ||
| 145 | + | ||
| 146 | +.brand-info { | ||
| 147 | + display: flex; | ||
| 148 | + flex-direction: column; | ||
| 149 | + gap: 6rpx; | ||
| 73 | } | 150 | } |
| 74 | 151 | ||
| 75 | .username { | 152 | .username { |
| 76 | - font-size: 28rpx; | 153 | + font-size: 36rpx; |
| 154 | + font-weight: bold; | ||
| 155 | + color: #fff; | ||
| 77 | } | 156 | } |
| 78 | 157 | ||
| 79 | -.level-card { | ||
| 80 | - width: 600rpx; | ||
| 81 | - height: 300rpx; | ||
| 82 | - background-color: rgba(173, 216, 230, 0.3); | ||
| 83 | - border-radius: 20rpx; | ||
| 84 | - padding: 30rpx; | ||
| 85 | - margin: 20rpx auto; | ||
| 86 | - position: relative; | ||
| 87 | - overflow: hidden; | 158 | +.brand-slogan { |
| 159 | + font-size: 24rpx; | ||
| 160 | + color: rgba(255, 255, 255, 0.5); | ||
| 161 | + letter-spacing: 2rpx; | ||
| 88 | } | 162 | } |
| 89 | 163 | ||
| 90 | -.level-name { | ||
| 91 | - font-size: 40rpx; | ||
| 92 | - font-weight: bold; | ||
| 93 | - margin-bottom: 10rpx; | 164 | +/* ========== 关于我们内容区 ========== */ |
| 165 | +.about-section { | ||
| 166 | + padding: 24rpx 24rpx 0; | ||
| 94 | } | 167 | } |
| 95 | 168 | ||
| 96 | -.level-en { | ||
| 97 | - font-size: 36rpx; | ||
| 98 | - font-weight: bold; | 169 | +.about-card { |
| 170 | + background-color: rgba(255, 255, 255, 0.05); | ||
| 171 | + border-radius: 16rpx; | ||
| 172 | + padding: 28rpx 24rpx; | ||
| 99 | margin-bottom: 20rpx; | 173 | margin-bottom: 20rpx; |
| 174 | + border: 1rpx solid rgba(255, 255, 255, 0.08); | ||
| 100 | } | 175 | } |
| 101 | 176 | ||
| 102 | -.require { | ||
| 103 | - font-size: 24rpx; | 177 | +.about-title { |
| 178 | + font-size: 30rpx; | ||
| 179 | + font-weight: bold; | ||
| 104 | margin-bottom: 20rpx; | 180 | margin-bottom: 20rpx; |
| 181 | + padding-left: 14rpx; | ||
| 182 | + border-left: 5rpx solid #ff6b00; | ||
| 183 | + color: #fff; | ||
| 105 | } | 184 | } |
| 106 | 185 | ||
| 107 | -.detail { | ||
| 108 | - font-size: 24rpx; | ||
| 109 | - color: #ccc; | 186 | +.about-content { |
| 187 | + font-size: 26rpx; | ||
| 188 | + color: rgba(255, 255, 255, 0.75); | ||
| 189 | + line-height: 1.8; | ||
| 190 | + text-align: justify; | ||
| 110 | } | 191 | } |
| 111 | 192 | ||
| 112 | -.progress-bar { | ||
| 113 | - width: 600rpx; | ||
| 114 | - height: 20rpx; | ||
| 115 | - background-color: #333; | ||
| 116 | - border-radius: 10rpx; | ||
| 117 | - margin: 20rpx auto; | ||
| 118 | - position: relative; | ||
| 119 | - overflow: hidden; | 193 | +.highlight { |
| 194 | + color: #ff6b00; | ||
| 195 | + font-weight: bold; | ||
| 120 | } | 196 | } |
| 121 | 197 | ||
| 122 | -.progress-track { | ||
| 123 | - width: 100%; | ||
| 124 | - height: 100%; | ||
| 125 | - background-color: #333; | ||
| 126 | - border-radius: 10rpx; | ||
| 127 | -} | ||
| 128 | - | ||
| 129 | -.progress-fill { | ||
| 130 | - width: 20%; | ||
| 131 | - height: 100%; | ||
| 132 | - background-color: #007aff; | ||
| 133 | - border-radius: 10rpx; | ||
| 134 | -} | ||
| 135 | - | ||
| 136 | -.progress-dot { | ||
| 137 | - position: absolute; | ||
| 138 | - top: 50%; | ||
| 139 | - left: 20%; | ||
| 140 | - transform: translate(-50%, -50%); | ||
| 141 | - width: 12rpx; | ||
| 142 | - height: 12rpx; | ||
| 143 | - background-color: #007aff; | 198 | +/* ========== 核心功能列表 ========== */ |
| 199 | +.feature-list { | ||
| 200 | + display: flex; | ||
| 201 | + flex-direction: column; | ||
| 202 | + gap: 16rpx; | ||
| 203 | +} | ||
| 204 | + | ||
| 205 | +.feature-item { | ||
| 206 | + display: flex; | ||
| 207 | + align-items: flex-start; | ||
| 208 | + gap: 20rpx; | ||
| 209 | + padding: 14rpx 0; | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +.feature-num { | ||
| 213 | + width: 48rpx; | ||
| 214 | + height: 48rpx; | ||
| 215 | + background: linear-gradient(135deg, rgba(255, 107, 0, 0.25), rgba(255, 107, 0, 0.08)); | ||
| 144 | border-radius: 50%; | 216 | border-radius: 50%; |
| 217 | + display: flex; | ||
| 218 | + align-items: center; | ||
| 219 | + justify-content: center; | ||
| 220 | + font-size: 22rpx; | ||
| 221 | + font-weight: bold; | ||
| 222 | + color: #ff6b00; | ||
| 223 | + flex-shrink: 0; | ||
| 145 | } | 224 | } |
| 146 | 225 | ||
| 147 | -.benefits-section { | ||
| 148 | - margin: 20rpx; | ||
| 149 | - padding: 0 20rpx; | 226 | +.feature-text { |
| 227 | + flex: 1; | ||
| 150 | } | 228 | } |
| 151 | 229 | ||
| 152 | -.section-title { | ||
| 153 | - font-size: 32rpx; | ||
| 154 | - margin-bottom: 20rpx; | ||
| 155 | - padding-left: 10rpx; | 230 | +.feature-name { |
| 231 | + font-size: 28rpx; | ||
| 232 | + font-weight: bold; | ||
| 233 | + color: #fff; | ||
| 234 | + margin-bottom: 4rpx; | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +.feature-desc { | ||
| 238 | + font-size: 24rpx; | ||
| 239 | + color: rgba(255, 255, 255, 0.5); | ||
| 240 | + line-height: 1.5; | ||
| 156 | } | 241 | } |
| 157 | 242 | ||
| 158 | -.benefit-list { | 243 | +/* ========== 联系我们 ========== */ |
| 244 | +.contact-list { | ||
| 245 | + display: flex; | ||
| 246 | + flex-direction: column; | ||
| 247 | + gap: 14rpx; | ||
| 248 | +} | ||
| 249 | + | ||
| 250 | +.contact-item { | ||
| 159 | display: flex; | 251 | display: flex; |
| 160 | - flex-direction: row; | ||
| 161 | justify-content: space-between; | 252 | justify-content: space-between; |
| 162 | - gap: 16rpx; | 253 | + align-items: center; |
| 254 | + padding: 8rpx 0; | ||
| 163 | } | 255 | } |
| 164 | 256 | ||
| 165 | -.benefit-item { | ||
| 166 | - width: 180rpx; | ||
| 167 | - background-color: rgba(255, 255, 255, 0.1); | ||
| 168 | - border-radius: 12rpx; | ||
| 169 | - padding: 16rpx 12rpx; | ||
| 170 | - text-align: center; | ||
| 171 | - box-sizing: border-box; | 257 | +.contact-label { |
| 258 | + font-size: 26rpx; | ||
| 259 | + color: rgba(255, 255, 255, 0.6); | ||
| 172 | } | 260 | } |
| 173 | 261 | ||
| 174 | -.icon { | ||
| 175 | - width: 36rpx; | ||
| 176 | - height: 36rpx; | ||
| 177 | - object-fit: cover; | ||
| 178 | - margin-bottom: 8rpx; | 262 | +.contact-value { |
| 263 | + font-size: 26rpx; | ||
| 264 | + color: rgba(255, 255, 255, 0.9); | ||
| 179 | } | 265 | } |
| 180 | 266 | ||
| 181 | -.benefit-name { | ||
| 182 | - font-size: 22rpx; | ||
| 183 | - margin-bottom: 4rpx; | ||
| 184 | - font-weight: bold; | 267 | +/* ========== 链接列表 ========== */ |
| 268 | +.link-list { | ||
| 269 | + display: flex; | ||
| 270 | + flex-direction: column; | ||
| 185 | } | 271 | } |
| 186 | 272 | ||
| 187 | -.benefit-desc { | ||
| 188 | - font-size: 18rpx; | ||
| 189 | - color: #ccc; | ||
| 190 | - line-height: 1.4; | 273 | +.link-item { |
| 274 | + display: flex; | ||
| 275 | + justify-content: space-between; | ||
| 276 | + align-items: center; | ||
| 277 | + padding: 20rpx 0; | ||
| 278 | + font-size: 28rpx; | ||
| 279 | + color: rgba(255, 255, 255, 0.8); | ||
| 280 | + border-bottom: 1rpx solid rgba(255, 255, 255, 0.06); | ||
| 191 | } | 281 | } |
| 192 | 282 | ||
| 193 | -.time-tag { | ||
| 194 | - font-size: 18rpx; | ||
| 195 | - color: white; | ||
| 196 | - background-color: #ff6b00; | ||
| 197 | - padding: 4rpx 8rpx; | ||
| 198 | - border-radius: 8rpx; | ||
| 199 | - margin-top: 8rpx; | ||
| 200 | - display: inline-block; | ||
| 201 | -} | ||
| 202 | - | ||
| 203 | -.more-benefits { | ||
| 204 | - margin: 30rpx 20rpx 0; | ||
| 205 | - padding: 20rpx; | ||
| 206 | - background-color: rgba(255, 255, 255, 0.1); | ||
| 207 | - border-radius: 12rpx; | 283 | +.link-item:last-child { |
| 284 | + border-bottom: none; | ||
| 285 | +} | ||
| 286 | + | ||
| 287 | +.link-arrow { | ||
| 288 | + color: rgba(255, 255, 255, 0.3); | ||
| 289 | + font-size: 26rpx; | ||
| 290 | +} | ||
| 291 | + | ||
| 292 | +/* ========== 版本信息 ========== */ | ||
| 293 | +.version-info { | ||
| 208 | text-align: center; | 294 | text-align: center; |
| 295 | + padding: 40rpx 0 20rpx; | ||
| 296 | + display: flex; | ||
| 297 | + flex-direction: column; | ||
| 298 | + gap: 8rpx; | ||
| 299 | + font-size: 22rpx; | ||
| 300 | + color: rgba(255, 255, 255, 0.35); | ||
| 209 | } | 301 | } |
| 210 | 302 | ||
| 211 | -.more-text { | ||
| 212 | - font-size: 24rpx; | ||
| 213 | - color: #ccc; | ||
| 214 | - margin-bottom: 10rpx; | 303 | +.copyright { |
| 304 | + font-size: 20rpx; | ||
| 215 | } | 305 | } |
| 216 | </style> | 306 | </style> |
| @@ -16,14 +16,23 @@ export const useActionStore = defineStore( | @@ -16,14 +16,23 @@ export const useActionStore = defineStore( | ||
| 16 | return allCategories.value.filter((item) => !hiddenCategories.value.includes(item.id)); | 16 | return allCategories.value.filter((item) => !hiddenCategories.value.includes(item.id)); |
| 17 | }); | 17 | }); |
| 18 | 18 | ||
| 19 | - // 获取分类 | 19 | + // 获取分类(首次加载,有缓存则跳过) |
| 20 | const getloadCategories = async () => { | 20 | const getloadCategories = async () => { |
| 21 | if (allCategories.value.length > 0) return; | 21 | if (allCategories.value.length > 0) return; |
| 22 | 22 | ||
| 23 | const response = await LeftmotionApi.getAllCategories(); | 23 | const response = await LeftmotionApi.getAllCategories(); |
| 24 | - // 把接口数据存进去 | ||
| 25 | allCategories.value = response.data || []; | 24 | allCategories.value = response.data || []; |
| 26 | - // 追加超级组分类(保持原有逻辑) | 25 | + // 追加超级组分类 |
| 26 | + allCategories.value.push({ | ||
| 27 | + id: 'super', | ||
| 28 | + name: '超级组', | ||
| 29 | + }); | ||
| 30 | + }; | ||
| 31 | + | ||
| 32 | + // 强制刷新分类(编辑/新增后使用) | ||
| 33 | + const reloadCategories = async () => { | ||
| 34 | + const response = await LeftmotionApi.getAllCategories(); | ||
| 35 | + allCategories.value = response.data || []; | ||
| 27 | allCategories.value.push({ | 36 | allCategories.value.push({ |
| 28 | id: 'super', | 37 | id: 'super', |
| 29 | name: '超级组', | 38 | name: '超级组', |
| @@ -47,6 +56,7 @@ export const useActionStore = defineStore( | @@ -47,6 +56,7 @@ export const useActionStore = defineStore( | ||
| 47 | showCategories, | 56 | showCategories, |
| 48 | hiddenCategories, | 57 | hiddenCategories, |
| 49 | getloadCategories, | 58 | getloadCategories, |
| 59 | + reloadCategories, | ||
| 50 | hideCategory, | 60 | hideCategory, |
| 51 | showCategory, | 61 | showCategory, |
| 52 | }; | 62 | }; |
| @@ -57,8 +67,11 @@ export const useActionStore = defineStore( | @@ -57,8 +67,11 @@ export const useActionStore = defineStore( | ||
| 57 | enabled: true, | 67 | enabled: true, |
| 58 | strategies: [ | 68 | strategies: [ |
| 59 | { | 69 | { |
| 60 | - key: 'action-category-store', // 修正 key 名称 | 70 | + key: 'action-category-store', |
| 61 | paths: ['hiddenCategories'], | 71 | paths: ['hiddenCategories'], |
| 72 | + // 关键修复:插件默认 save 用 uni.setStorage(localStorage)、load 用 window.sessionStorage, | ||
| 73 | + // 两端不一致会导致刷新后数据丢失。显式指定 storage 为 localStorage,保证存读同源。 | ||
| 74 | + storage: typeof window !== 'undefined' ? window.localStorage : undefined, | ||
| 62 | }, | 75 | }, |
| 63 | ], | 76 | ], |
| 64 | }, | 77 | }, |
| @@ -118,6 +118,8 @@ const app = defineStore({ | @@ -118,6 +118,8 @@ const app = defineStore({ | ||
| 118 | strategies: [ | 118 | strategies: [ |
| 119 | { | 119 | { |
| 120 | key: 'app-store', | 120 | key: 'app-store', |
| 121 | + // 修复插件 H5 存读后端不一致:save 走 localStorage、load 走 sessionStorage,显式指定 storage 保证同源 | ||
| 122 | + storage: typeof window !== 'undefined' ? window.localStorage : undefined, | ||
| 121 | }, | 123 | }, |
| 122 | ], | 124 | ], |
| 123 | }, | 125 | }, |
| @@ -113,6 +113,8 @@ const cart = defineStore({ | @@ -113,6 +113,8 @@ const cart = defineStore({ | ||
| 113 | strategies: [ | 113 | strategies: [ |
| 114 | { | 114 | { |
| 115 | key: 'cart-store', | 115 | key: 'cart-store', |
| 116 | + // 修复插件 H5 存读后端不一致:save 走 localStorage、load 走 sessionStorage,显式指定 storage 保证同源 | ||
| 117 | + storage: typeof window !== 'undefined' ? window.localStorage : undefined, | ||
| 116 | }, | 118 | }, |
| 117 | ], | 119 | ], |
| 118 | }, | 120 | }, |
| @@ -21,6 +21,9 @@ const modal = defineStore({ | @@ -21,6 +21,9 @@ const modal = defineStore({ | ||
| 21 | { | 21 | { |
| 22 | key: 'modal-store', | 22 | key: 'modal-store', |
| 23 | paths: ['lastTimer', 'advHistory'], | 23 | paths: ['lastTimer', 'advHistory'], |
| 24 | + // 与 action.js 同样的修复:插件默认 save 用 uni.setStorage(localStorage)、load 用 window.sessionStorage, | ||
| 25 | + // 两端不一致会导致 H5 刷新后数据丢失。显式指定 storage 为 localStorage,保证存读同源。 | ||
| 26 | + storage: typeof window !== 'undefined' ? window.localStorage : undefined, | ||
| 24 | }, | 27 | }, |
| 25 | ], | 28 | ], |
| 26 | }, | 29 | }, |
| @@ -24,6 +24,8 @@ const sys = defineStore({ | @@ -24,6 +24,8 @@ const sys = defineStore({ | ||
| 24 | strategies: [ | 24 | strategies: [ |
| 25 | { | 25 | { |
| 26 | key: 'sys-store', | 26 | key: 'sys-store', |
| 27 | + // 修复插件 H5 存读后端不一致:save 走 localStorage、load 走 sessionStorage,显式指定 storage 保证同源 | ||
| 28 | + storage: typeof window !== 'undefined' ? window.localStorage : undefined, | ||
| 27 | }, | 29 | }, |
| 28 | ], | 30 | ], |
| 29 | }, | 31 | }, |
| @@ -58,7 +58,7 @@ const useTrainingStore = defineStore( | @@ -58,7 +58,7 @@ const useTrainingStore = defineStore( | ||
| 58 | // 持久化:关闭APP再进来,训练还在! | 58 | // 持久化:关闭APP再进来,训练还在! |
| 59 | persist: { | 59 | persist: { |
| 60 | enabled: true, | 60 | enabled: true, |
| 61 | - strategies: [{ key: 'training-store' }], | 61 | + strategies: [{ key: 'training-store', storage: typeof window !== 'undefined' ? window.localStorage : undefined }], |
| 62 | }, | 62 | }, |
| 63 | }, | 63 | }, |
| 64 | ); | 64 | ); |
| 1 | import { defineStore } from 'pinia'; | 1 | import { defineStore } from 'pinia'; |
| 2 | +import dayjs from 'dayjs'; | ||
| 2 | import ExercisesApi from '@/sheep/api/motion/exercises'; | 3 | import ExercisesApi from '@/sheep/api/motion/exercises'; |
| 3 | import SupersetsApi from '@/sheep/api/motion/supersets'; | 4 | import SupersetsApi from '@/sheep/api/motion/supersets'; |
| 4 | import TemplatesApi from '@/sheep/api/Template/Templates'; | 5 | import TemplatesApi from '@/sheep/api/Template/Templates'; |
| 5 | 6 | ||
| 7 | +// ====================== | ||
| 8 | +// 常量 | ||
| 9 | +// ====================== | ||
| 10 | +const DEFAULT_USER_WEIGHT = 70; | ||
| 11 | +const DEFAULT_REST_TIME = 60; | ||
| 12 | + | ||
| 13 | +// ====================== | ||
| 14 | +// 工具函数 | ||
| 15 | +// ====================== | ||
| 16 | + | ||
| 17 | +/** 秒数 → { h, m, s } 两位数字符串 */ | ||
| 18 | +function secondsToHMS(totalSec) { | ||
| 19 | + const d = dayjs.duration(totalSec || 0, 'seconds'); | ||
| 20 | + return { | ||
| 21 | + h: String(Math.floor(d.as('hours'))).padStart(2, '0'), | ||
| 22 | + m: String(d.minutes()).padStart(2, '0'), | ||
| 23 | + s: String(d.seconds()).padStart(2, '0'), | ||
| 24 | + }; | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +/** 单组数据 → 页面标准格式 */ | ||
| 28 | +function convertSetToRecord(set) { | ||
| 29 | + return { | ||
| 30 | + weight: set.weight ?? '', | ||
| 31 | + reps: set.reps ?? '', | ||
| 32 | + duration: set.duration ?? '', | ||
| 33 | + distance: set.distance ?? '', | ||
| 34 | + restTime: set.restTime ?? '', | ||
| 35 | + ...secondsToHMS(set.duration), | ||
| 36 | + quickTimeDisplay: (set.restTime || DEFAULT_REST_TIME) + 's', | ||
| 37 | + isActive: (set.isCompleted ?? 0) === 1, | ||
| 38 | + }; | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +/** 批量转换 sets → records */ | ||
| 42 | +function convertSetsToRecords(sets) { | ||
| 43 | + return (sets || []).map(convertSetToRecord); | ||
| 44 | +} | ||
| 45 | + | ||
| 6 | export const useTrainingStore = defineStore('training', { | 46 | export const useTrainingStore = defineStore('training', { |
| 7 | state: () => ({ | 47 | state: () => ({ |
| 8 | id: null, // 页面传过来的 id | 48 | id: null, // 页面传过来的 id |
| @@ -88,13 +128,13 @@ export const useTrainingStore = defineStore('training', { | @@ -88,13 +128,13 @@ export const useTrainingStore = defineStore('training', { | ||
| 88 | units.forEach((unit, unitIndex) => { | 128 | units.forEach((unit, unitIndex) => { |
| 89 | let records = {}; | 129 | let records = {}; |
| 90 | // 按顺序赋值,不依赖 exerciseId,动作可改、可换 | 130 | // 按顺序赋值,不依赖 exerciseId,动作可改、可换 |
| 91 | - unit.exercises?.forEach((ex, exIndex) => { | 131 | + unit.exercises?.forEach((ex) => { |
| 92 | records[ex.exerciseId] = ex.sets || []; | 132 | records[ex.exerciseId] = ex.sets || []; |
| 93 | }); | 133 | }); |
| 94 | 134 | ||
| 95 | this.unitRecords[unitIndex] = { | 135 | this.unitRecords[unitIndex] = { |
| 96 | - records: records, | ||
| 97 | - userWeight: 70, | 136 | + records, |
| 137 | + userWeight: DEFAULT_USER_WEIGHT, | ||
| 98 | }; | 138 | }; |
| 99 | }); | 139 | }); |
| 100 | 140 | ||
| @@ -114,41 +154,17 @@ export const useTrainingStore = defineStore('training', { | @@ -114,41 +154,17 @@ export const useTrainingStore = defineStore('training', { | ||
| 114 | const exercises = unit.exercises || []; | 154 | const exercises = unit.exercises || []; |
| 115 | 155 | ||
| 116 | exercises.forEach((ex) => { | 156 | exercises.forEach((ex) => { |
| 117 | - const sets = ex.sets || []; | ||
| 118 | - | ||
| 119 | - // 转换 sets → 页面标准格式 | ||
| 120 | - const converted = sets.map((set) => { | ||
| 121 | - const totalSec = set.duration || 0; | ||
| 122 | - const h = String(Math.floor(totalSec / 3600)).padStart(2, '0'); | ||
| 123 | - const m = String(Math.floor((totalSec % 3600) / 60)).padStart(2, '0'); | ||
| 124 | - const s = String(totalSec % 60).padStart(2, '0'); | ||
| 125 | - return { | ||
| 126 | - weight: set.weight ?? '', | ||
| 127 | - reps: set.reps ?? '', | ||
| 128 | - duration: set.duration ?? '', | ||
| 129 | - distance: set.distance ?? '', | ||
| 130 | - restTime: set.restTime ?? '', | ||
| 131 | - h, | ||
| 132 | - m, | ||
| 133 | - s, | ||
| 134 | - quickTimeDisplay: (set.restTime || 60) + 's', | ||
| 135 | - // isActive: false, | ||
| 136 | - isActive: (set.isCompleted ?? 0) === 1, | ||
| 137 | - }; | ||
| 138 | - }); | ||
| 139 | - | ||
| 140 | - records[ex.exerciseId] = converted; | 157 | + records[ex.exerciseId] = convertSetsToRecords(ex.sets); |
| 141 | }); | 158 | }); |
| 142 | 159 | ||
| 143 | - // 直接存入 Pinia | ||
| 144 | this.unitRecords[unitIndex] = { | 160 | this.unitRecords[unitIndex] = { |
| 145 | - records: records, | ||
| 146 | - userWeight: 70, | 161 | + records, |
| 162 | + userWeight: DEFAULT_USER_WEIGHT, | ||
| 147 | }; | 163 | }; |
| 148 | }); | 164 | }); |
| 149 | }, | 165 | }, |
| 150 | 166 | ||
| 151 | - //新增:每日模板专用赋值(真实训练记录) | 167 | + // 每日模板专用赋值(真实训练记录) |
| 152 | initDailyTemplateRecords() { | 168 | initDailyTemplateRecords() { |
| 153 | console.log('✅ 每日模板:直接赋值真实训练记录'); | 169 | console.log('✅ 每日模板:直接赋值真实训练记录'); |
| 154 | this.unitRecords = {}; | 170 | this.unitRecords = {}; |
| @@ -157,44 +173,13 @@ export const useTrainingStore = defineStore('training', { | @@ -157,44 +173,13 @@ export const useTrainingStore = defineStore('training', { | ||
| 157 | 173 | ||
| 158 | units.forEach((unit, unitIndex) => { | 174 | units.forEach((unit, unitIndex) => { |
| 159 | let records = {}; | 175 | let records = {}; |
| 160 | - const exercises = unit.exercises || []; | ||
| 161 | - | ||
| 162 | - // exercises.forEach((ex) => { | ||
| 163 | - // records[ex.exerciseId] = ex.sets || []; | ||
| 164 | - // }); | ||
| 165 | - exercises.forEach((ex) => { | ||
| 166 | - const sets = ex.sets || []; | ||
| 167 | - | ||
| 168 | - // 转换 sets → 页面标准格式 | ||
| 169 | - const converted = sets.map((set) => { | ||
| 170 | - const totalSec = set.duration || 0; | ||
| 171 | - const h = String(Math.floor(totalSec / 3600)).padStart(2, '0'); | ||
| 172 | - const m = String(Math.floor((totalSec % 3600) / 60)).padStart(2, '0'); | ||
| 173 | - const s = String(totalSec % 60).padStart(2, '0'); | ||
| 174 | - return { | ||
| 175 | - weight: set.weight ?? '', | ||
| 176 | - reps: set.reps ?? '', | ||
| 177 | - duration: set.duration ?? '', | ||
| 178 | - distance: set.distance ?? '', | ||
| 179 | - restTime: set.restTime ?? '', | ||
| 180 | - h, | ||
| 181 | - m, | ||
| 182 | - s, | ||
| 183 | - quickTimeDisplay: (set.restTime || 60) + 's', | ||
| 184 | - // isActive: false, | ||
| 185 | - isActive: (set.isCompleted ?? 0) === 1, | ||
| 186 | - }; | ||
| 187 | - }); | ||
| 188 | - | ||
| 189 | - converted.forEach((item, idx) => { | ||
| 190 | - console.log(`第${idx}组:`, item.h, item.m, item.s); | ||
| 191 | - }); | ||
| 192 | - records[ex.exerciseId] = converted; | 176 | + (unit.exercises || []).forEach((ex) => { |
| 177 | + records[ex.exerciseId] = convertSetsToRecords(ex.sets); | ||
| 193 | }); | 178 | }); |
| 194 | 179 | ||
| 195 | this.unitRecords[unitIndex] = { | 180 | this.unitRecords[unitIndex] = { |
| 196 | - records: records, | ||
| 197 | - userWeight: 70, | 181 | + records, |
| 182 | + userWeight: DEFAULT_USER_WEIGHT, | ||
| 198 | }; | 183 | }; |
| 199 | }); | 184 | }); |
| 200 | }, | 185 | }, |
| @@ -208,7 +193,6 @@ export const useTrainingStore = defineStore('training', { | @@ -208,7 +193,6 @@ export const useTrainingStore = defineStore('training', { | ||
| 208 | return (list || []).map((item) => ({ | 193 | return (list || []).map((item) => ({ |
| 209 | id: item.exerciseId, | 194 | id: item.exerciseId, |
| 210 | name: item.exerciseName, | 195 | name: item.exerciseName, |
| 211 | - // urlImage: item.exerciseCover || item.url3dAnimation, | ||
| 212 | urlImage: item.url3dAnimation || item.exerciseCover || item.urlImage, | 196 | urlImage: item.url3dAnimation || item.exerciseCover || item.urlImage, |
| 213 | exerciseType: item.exerciseType, | 197 | exerciseType: item.exerciseType, |
| 214 | })); | 198 | })); |
| @@ -227,9 +211,7 @@ export const useTrainingStore = defineStore('training', { | @@ -227,9 +211,7 @@ export const useTrainingStore = defineStore('training', { | ||
| 227 | return { | 211 | return { |
| 228 | id: act.exerciseId, | 212 | id: act.exerciseId, |
| 229 | name: act.exerciseName, | 213 | name: act.exerciseName, |
| 230 | - // urlImage: act.exerciseCover || act.url3dAnimation || act.urlImage, | ||
| 231 | urlImage: act.url3dAnimation || act.exerciseCover || act.urlImage, | 214 | urlImage: act.url3dAnimation || act.exerciseCover || act.urlImage, |
| 232 | - // urlImage | ||
| 233 | exerciseType: act.exerciseType, | 215 | exerciseType: act.exerciseType, |
| 234 | categoryDescription: act.categoryDescription, | 216 | categoryDescription: act.categoryDescription, |
| 235 | equipmentDescription: act.equipmentDescription, | 217 | equipmentDescription: act.equipmentDescription, |
| @@ -265,7 +247,6 @@ export const useTrainingStore = defineStore('training', { | @@ -265,7 +247,6 @@ export const useTrainingStore = defineStore('training', { | ||
| 265 | exerciseId: this.actionDetail.id, | 247 | exerciseId: this.actionDetail.id, |
| 266 | exerciseName: this.actionDetail.name, | 248 | exerciseName: this.actionDetail.name, |
| 267 | exerciseType: this.actionDetail.exerciseType, | 249 | exerciseType: this.actionDetail.exerciseType, |
| 268 | - // urlImage: this.actionDetail.urlImage || this.actionDetail.url3dAnimation, | ||
| 269 | urlImage: this.actionDetail.url3dAnimation || this.actionDetail.urlImage, | 250 | urlImage: this.actionDetail.url3dAnimation || this.actionDetail.urlImage, |
| 270 | categoryDescription: this.actionDetail.categoryDescription, | 251 | categoryDescription: this.actionDetail.categoryDescription, |
| 271 | equipmentDescription: this.actionDetail.equipmentDescription, | 252 | equipmentDescription: this.actionDetail.equipmentDescription, |
| @@ -303,7 +284,7 @@ export const useTrainingStore = defineStore('training', { | @@ -303,7 +284,7 @@ export const useTrainingStore = defineStore('training', { | ||
| 303 | saveUnitRecord(unitIndex, data) { | 284 | saveUnitRecord(unitIndex, data) { |
| 304 | this.unitRecords[unitIndex] = { | 285 | this.unitRecords[unitIndex] = { |
| 305 | records: data.records || {}, | 286 | records: data.records || {}, |
| 306 | - userWeight: data.userWeight ?? 70, | 287 | + userWeight: data.userWeight ?? DEFAULT_USER_WEIGHT, |
| 307 | }; | 288 | }; |
| 308 | }, | 289 | }, |
| 309 | 290 | ||
| @@ -312,7 +293,7 @@ export const useTrainingStore = defineStore('training', { | @@ -312,7 +293,7 @@ export const useTrainingStore = defineStore('training', { | ||
| 312 | return ( | 293 | return ( |
| 313 | this.unitRecords[unitIndex] || { | 294 | this.unitRecords[unitIndex] || { |
| 314 | records: {}, | 295 | records: {}, |
| 315 | - userWeight: 70, | 296 | + userWeight: DEFAULT_USER_WEIGHT, |
| 316 | } | 297 | } |
| 317 | ); | 298 | ); |
| 318 | }, | 299 | }, |
| @@ -328,7 +309,7 @@ export const useTrainingStore = defineStore('training', { | @@ -328,7 +309,7 @@ export const useTrainingStore = defineStore('training', { | ||
| 328 | if (!this.unitRecords[unitIndex]) { | 309 | if (!this.unitRecords[unitIndex]) { |
| 329 | this.unitRecords[unitIndex] = { | 310 | this.unitRecords[unitIndex] = { |
| 330 | records: {}, | 311 | records: {}, |
| 331 | - userWeight: 70, | 312 | + userWeight: DEFAULT_USER_WEIGHT, |
| 332 | }; | 313 | }; |
| 333 | } | 314 | } |
| 334 | 315 | ||
| @@ -369,10 +350,8 @@ export const useTrainingStore = defineStore('training', { | @@ -369,10 +350,8 @@ export const useTrainingStore = defineStore('training', { | ||
| 369 | this.totalSeconds = seconds; | 350 | this.totalSeconds = seconds; |
| 370 | }, | 351 | }, |
| 371 | openTimePicker() { | 352 | openTimePicker() { |
| 372 | - const h = Math.floor(this.totalSeconds / 3600); | ||
| 373 | - const m = Math.floor((this.totalSeconds % 3600) / 60); | ||
| 374 | - const s = this.totalSeconds % 60; | ||
| 375 | - this.defaultTimeIndex = [h, m, s]; | 353 | + const d = dayjs.duration(this.totalSeconds, 'seconds'); |
| 354 | + this.defaultTimeIndex = [Math.floor(d.as('hours')), d.minutes(), d.seconds()]; | ||
| 376 | this.showPicker = true; | 355 | this.showPicker = true; |
| 377 | }, | 356 | }, |
| 378 | closeTimePicker() { | 357 | closeTimePicker() { |
| @@ -416,7 +395,6 @@ export const useTrainingStore = defineStore('training', { | @@ -416,7 +395,6 @@ export const useTrainingStore = defineStore('training', { | ||
| 416 | this.loading = false; | 395 | this.loading = false; |
| 417 | this.unitRecords = {}; | 396 | this.unitRecords = {}; |
| 418 | this.trainingTimeText = ''; | 397 | this.trainingTimeText = ''; |
| 419 | - // this.min = false; | ||
| 420 | this.isSystem = 1; | 398 | this.isSystem = 1; |
| 421 | this.trainingName = ''; | 399 | this.trainingName = ''; |
| 422 | this.defaultTimeIndex = [0, 0, 0]; | 400 | this.defaultTimeIndex = [0, 0, 0]; |
| @@ -217,6 +217,8 @@ const useUserStore = defineStore( | @@ -217,6 +217,8 @@ const useUserStore = defineStore( | ||
| 217 | strategies: [ | 217 | strategies: [ |
| 218 | { | 218 | { |
| 219 | key: 'user-store', | 219 | key: 'user-store', |
| 220 | + // 修复插件 H5 存读后端不一致:save 走 localStorage、load 走 sessionStorage,显式指定 storage 保证同源 | ||
| 221 | + storage: typeof window !== 'undefined' ? window.localStorage : undefined, | ||
| 220 | }, | 222 | }, |
| 221 | ], | 223 | ], |
| 222 | }, | 224 | }, |
-
Please register or login to post a comment