Authored by Bad

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

@@ -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,120 +140,82 @@ const fetchMyPlanList = async () => { @@ -108,120 +140,82 @@ 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%;  
147 - display: flex;  
148 - flex-direction: column;  
149 - align-items: center;  
150 - gap: 70rpx;  
151 - margin-bottom: 100rpx;  
152 -}  
153 -  
154 -/* 选项列表 */  
155 -.popup-options {  
156 - width: 100%; 150 +.add-train-popup {
157 display: flex; 151 display: flex;
158 flex-direction: column; 152 flex-direction: column;
159 - justify-content: center;  
160 align-items: center; 153 align-items: center;
161 - gap: 20rpx;  
162 -}  
163 -  
164 -/* 单个选项项 */  
165 -.popup-option {  
166 - width: 100%;  
167 - // background: rgba(255, 255, 255, 0.1);  
168 - border-radius: 16rpx;  
169 - padding: 24rpx 30rpx;  
170 - display: flex;  
171 - align-items: center;  
172 -  
173 - gap: 24rpx;  
174 -}  
175 -  
176 -/* 黄色图标容器 */  
177 -.icon-wrap {  
178 - width: 80rpx;  
179 - height: 80rpx;  
180 - border-radius: 50%;  
181 - background: #ffd60a;  
182 - display: flex;  
183 - align-items: center;  
184 - justify-content: center;  
185 - flex-shrink: 0;  
186 -  
187 - image {  
188 - width: 40rpx;  
189 - height: 40rpx; 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;
190 } 163 }
191 -}  
192 -  
193 -/* 文字区域 */  
194 -.text-wrap {  
195 - display: flex;  
196 - flex-direction: column;  
197 - gap: 8rpx;  
198 -}  
199 164
200 -.option-title {  
201 - font-size: 32rpx;  
202 - color: #fff;  
203 - font-weight: 500;  
204 -} 165 + // 菜单选项列表
  166 + .popup-menu {
  167 + width: 100%;
  168 + display: flex;
  169 + flex-direction: column;
  170 + gap: 16rpx;
  171 +
  172 + .menu-item {
  173 + display: flex;
  174 + align-items: center;
  175 + gap: 24rpx;
  176 + padding: 24rpx 30rpx;
  177 + border-radius: 16rpx;
  178 + }
205 179
206 -.option-desc {  
207 - font-size: 26rpx;  
208 - color: rgba(255, 255, 255, 0.7);  
209 -} 180 + .menu-icon {
  181 + width: 80rpx;
  182 + height: 80rpx;
  183 + border-radius: 50%;
  184 + background: #ffd60a;
  185 + display: flex;
  186 + align-items: center;
  187 + justify-content: center;
  188 + flex-shrink: 0;
  189 + }
210 190
211 -/* 底部关闭按钮 */  
212 -.close-btn {  
213 - width: 88rpx;  
214 - height: 88rpx;  
215 - border-radius: 50%;  
216 - background: #fff;  
217 - display: flex;  
218 - align-items: center;  
219 - justify-content: center; 191 + .menu-text {
  192 + display: flex;
  193 + flex-direction: column;
  194 + gap: 8rpx;
  195 +
  196 + .menu-title {
  197 + font-size: 32rpx;
  198 + color: #fff;
  199 + font-weight: 500;
  200 + }
  201 +
  202 + .menu-desc {
  203 + font-size: 26rpx;
  204 + color: rgba(255, 255, 255, 0.7);
  205 + }
  206 + }
  207 + }
220 208
221 - text {  
222 - font-size: 40rpx;  
223 - color: #000;  
224 - line-height: 1; 209 + // 底部关闭按钮
  210 + .close-btn {
  211 + margin-top: 60rpx;
  212 + width: 88rpx;
  213 + height: 88rpx;
  214 + border-radius: 50%;
  215 + background: #fff;
  216 + display: flex;
  217 + align-items: center;
  218 + justify-content: center;
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>
@@ -84,7 +79,7 @@ @@ -84,7 +79,7 @@
84 <view class="action-top"> 79 <view class="action-top">
85 <text class="action-name">{{ unit.exercises[0]?.exerciseName || '动作名称' }}</text> 80 <text class="action-name">{{ unit.exercises[0]?.exerciseName || '动作名称' }}</text>
86 <text class="action-totalWeight">{{ unit.totalWeight 81 <text class="action-totalWeight">{{ unit.totalWeight
87 - }}kg</text> 82 + }}kg</text>
88 </view> 83 </view>
89 84
90 <!-- 下半部分:组次行 → 向左对齐图片 ✅核心--> 85 <!-- 下半部分:组次行 → 向左对齐图片 ✅核心-->
@@ -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,152 +1492,145 @@ button::after { @@ -1512,152 +1492,145 @@ 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 -  
1523 -.superset-wrapper .super-head {  
1524 - display: flex;  
1525 - align-items: center;  
1526 - padding: 24rpx 20rpx;  
1527 - position: relative;  
1528 - gap: 16rpx;  
1529 -}  
1530 1501
1531 -.superset-wrapper .super-image {  
1532 - width: 75rpx;  
1533 - height: 75rpx;  
1534 - flex-shrink: 0;  
1535 -} 1502 + .super-head {
  1503 + display: flex;
  1504 + align-items: center;
  1505 + padding: 24rpx 20rpx;
  1506 + position: relative;
  1507 + gap: 16rpx;
  1508 + }
1536 1509
1537 -.superset-wrapper .super-title-wrap {  
1538 - flex: 1;  
1539 - display: flex;  
1540 - flex-direction: column;  
1541 - gap: 8rpx;  
1542 -} 1510 + .super-image {
  1511 + width: 75rpx;
  1512 + height: 75rpx;
  1513 + flex-shrink: 0;
  1514 + }
1543 1515
1544 -.superset-wrapper .super-title {  
1545 - font-size: 32rpx;  
1546 - color: #111;  
1547 - font-weight: 500;  
1548 -} 1516 + .super-title-wrap {
  1517 + flex: 1;
  1518 + display: flex;
  1519 + flex-direction: column;
  1520 + gap: 8rpx;
  1521 + }
1549 1522
1550 -.superset-wrapper .super-totalWeight {  
1551 - font-size: 24rpx;  
1552 - color: #666;  
1553 -} 1523 + .super-title {
  1524 + font-size: 32rpx;
  1525 + color: #111;
  1526 + font-weight: 500;
  1527 + }
1554 1528
1555 -.superset-wrapper .super-actionName {  
1556 - padding: 0 20rpx 20rpx 70rpx;  
1557 - display: flex;  
1558 - flex-direction: column;  
1559 - gap: 12rpx;  
1560 -} 1529 + .super-totalWeight {
  1530 + font-size: 24rpx;
  1531 + color: #666;
  1532 + }
1561 1533
1562 -.superset-wrapper .name-item {  
1563 - display: flex;  
1564 - align-items: center;  
1565 - gap: 12rpx;  
1566 -} 1534 + .super-actionName {
  1535 + padding: 0 20rpx 20rpx 70rpx;
  1536 + display: flex;
  1537 + flex-direction: column;
  1538 + gap: 12rpx;
1567 1539
1568 -.superset-wrapper .letter {  
1569 - font-size: 28rpx;  
1570 - font-weight: bold;  
1571 - color: #333;  
1572 -} 1540 + .action-name {
  1541 + font-size: 28rpx;
  1542 + color: $color-primary;
  1543 + }
  1544 + }
1573 1545
1574 -.superset-wrapper .super-actionName .action-name {  
1575 - font-size: 28rpx;  
1576 - color: #333;  
1577 -} 1546 + .name-item {
  1547 + display: flex;
  1548 + align-items: center;
  1549 + gap: 12rpx;
  1550 + }
1578 1551
1579 -.superset-wrapper .super-set-list {  
1580 - padding: 0 20rpx 20rpx 20rpx;  
1581 - display: flex;  
1582 - flex-direction: column;  
1583 - gap: 12rpx;  
1584 -} 1552 + .letter {
  1553 + font-size: 28rpx;
  1554 + font-weight: bold;
  1555 + color: $color-primary;
  1556 + }
1585 1557
1586 -.superset-wrapper .super-set-item {  
1587 - display: flex;  
1588 - // flex-direction: column; // 新增:垂直排列  
1589 - align-items: stretch; // 让每个动作占满宽度  
1590 - gap: 12rpx; // 垂直间距(可根据需要微调)  
1591 - padding: 16rpx;  
1592 - background: #f9f9f9;  
1593 - border-radius: 12rpx;  
1594 -} 1558 + .super-set-list {
  1559 + padding: 0 20rpx 20rpx 20rpx;
  1560 + display: flex;
  1561 + flex-direction: column;
  1562 + gap: 12rpx;
  1563 + }
1595 1564
1596 -// 为了让序号独立占一行  
1597 -.superset-wrapper .super-set-item {  
1598 - margin-bottom: 8rpx;  
1599 -} 1565 + .super-set-item {
  1566 + display: flex;
  1567 + align-items: stretch;
  1568 + gap: 12rpx;
  1569 + padding: 16rpx;
  1570 + background: #f9f9f9;
  1571 + border-radius: 12rpx;
  1572 + margin-bottom: 8rpx;
  1573 + }
1600 1574
1601 -.superset-wrapper .set-number {  
1602 - font-size: 26rpx;  
1603 - background: #e4e4e4;  
1604 - color: #282828;  
1605 - width: 37rpx;  
1606 - height: 37rpx;  
1607 - text-align: center;  
1608 - flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */  
1609 - border-radius: 50%;  
1610 - // line-height: 60rpx;  
1611 -} 1575 + .set-number {
  1576 + font-size: 26rpx;
  1577 + background: #e4e4e4;
  1578 + color: #282828;
  1579 + width: 37rpx;
  1580 + height: 37rpx;
  1581 + text-align: center;
  1582 + flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */
  1583 + border-radius: 50%;
  1584 + // line-height: 60rpx;
  1585 + }
1612 1586
1613 -.superset-wrapper .set-column {  
1614 - display: flex;  
1615 - flex-direction: column;  
1616 -} 1587 + .set-column {
  1588 + display: flex;
  1589 + flex-direction: column;
  1590 + }
1617 1591
1618 -.superset-wrapper .action-in-set {  
1619 - // display: flex;  
1620 - // flex-direction: column;  
1621 - width: 100%;  
1622 - display: flex;  
1623 - align-items: center;  
1624 - margin-bottom: 8rpx;  
1625 -} 1592 + .action-in-set {
  1593 + // display: flex;
  1594 + // flex-direction: column;
  1595 + width: 100%;
  1596 + display: flex;
  1597 + align-items: center;
  1598 + margin-bottom: 8rpx;
  1599 + }
1626 1600
1627 -.superset-wrapper .action-in-set:last-child {  
1628 - margin-bottom: 0;  
1629 -} 1601 + .action-in-set:last-child {
  1602 + margin-bottom: 0;
  1603 + }
1630 1604
1631 -.superset-wrapper .action-letter {  
1632 - display: inline-block;  
1633 - font-size: 26rpx;  
1634 - color: #666;  
1635 - // width: 35rpx;  
1636 - white-space: nowrap;  
1637 - margin-right: 12rpx;  
1638 - flex-shrink: 0;  
1639 -} 1605 + .action-letter {
  1606 + display: inline-block;
  1607 + font-size: 26rpx;
  1608 + color: #666;
  1609 + // width: 35rpx;
  1610 + white-space: nowrap;
  1611 + margin-right: 12rpx;
  1612 + flex-shrink: 0;
  1613 + }
1640 1614
1641 -.superset-wrapper .set-content {  
1642 - font-size: 26rpx;  
1643 - color: inherit;  
1644 - flex: 1;  
1645 -} 1615 + .set-content {
  1616 + font-size: 26rpx;
  1617 + color: inherit;
  1618 + flex: 1;
  1619 + }
1646 1620
1647 -.superset-wrapper .rest-time {  
1648 - font-size: 24rpx;  
1649 - // color: inherit;  
1650 - margin-left: 10rpx;  
1651 - flex-shrink: 0;  
1652 -} 1621 + .rest-time {
  1622 + font-size: 24rpx;
  1623 + // color: inherit;
  1624 + margin-left: 10rpx;
  1625 + flex-shrink: 0;
  1626 + }
1653 1627
1654 -.superset-wrapper .check {  
1655 - font-size: 26rpx;  
1656 - color: #333;  
1657 - margin-left: 275rpx !important;  
1658 - /* 这里控制和左边内容的距离,数值你可以自己调 */  
1659 - flex-shrink: 0;  
1660 - /* 防止被压缩 */ 1628 + .check {
  1629 + font-size: 26rpx;
  1630 + color: #333;
  1631 + margin-left: 275rpx !important;
  1632 + flex-shrink: 0;
  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);
  153 +
  154 +/** 判断是否为今天 */
  155 +const isToday = (fullDate) => fullDate === getTodayStr();
134 156
135 -// 切换日期选中状态(仅允许选择今天及之后)  
136 -const toggleDate = (day) => {  
137 - if (day.fullDate < minDateStr) { 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,121 +251,163 @@ defineExpose({ open, close }); @@ -258,121 +251,163 @@ 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;
  332 + }
  333 +
  334 + /* 非当月 */
  335 + &.other-month {
  336 + opacity: 0;
  337 + pointer-events: none;
341 } 338 }
342 339
343 - &.other-month .day-number {  
344 - color: #ccc; 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 + }
345 } 357 }
346 358
  359 + /* 已选中 */
347 &.selected { 360 &.selected {
348 - background-color: #ffd700; 361 + background: linear-gradient(135deg, #ffd700, #ffb800);
349 362
350 - .day-number {  
351 - color: #000;  
352 - font-weight: bold; 363 + .date-text {
  364 + color: #1a1a1a;
  365 + font-weight: 700;
  366 + }
  367 +
  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 }
378 -</style>  
  413 +</style>
@@ -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,29 +233,33 @@ const handleDeleteTemplate = async () => { @@ -233,29 +233,33 @@ const handleDeleteTemplate = async () => {
233 return; 233 return;
234 } 234 }
235 235
236 - uni.showModal({  
237 - title: '确认删除',  
238 - content: '确定要删除这个模板吗?删除后无法恢复',  
239 - confirmColor: '#ff4444',  
240 - success: async (res) => {  
241 - if (res.confirm) {  
242 - try {  
243 - uni.showLoading({ title: '删除中...' });  
244 - await TemplatesApi.deleteCustTemplate(currentTemplate.value.id);  
245 - uni.hideLoading();  
246 - uni.showToast({ title: '删除成功', icon: 'success' });  
247 - closeTemplateMenu();  
248 - // 通知父组件刷新列表  
249 - emit('refresh');  
250 -  
251 - } catch (err) {  
252 - uni.hideLoading();  
253 - console.error('删除失败:', err);  
254 - uni.showToast({ title: '删除失败', icon: 'none' }); 236 + // 先保存模板ID,再关闭菜单弹窗,避免弹窗叠加
  237 + const templateId = currentTemplate.value.id;
  238 + closeTemplateMenu();
  239 +
  240 + setTimeout(() => {
  241 + uni.showModal({
  242 + title: '确认删除',
  243 + content: '确定要删除这个模板吗?删除后无法恢复',
  244 + confirmColor: '#ff4444',
  245 + success: async (res) => {
  246 + if (res.confirm) {
  247 + try {
  248 + uni.showLoading({ title: '删除中...' });
  249 + await TemplatesApi.deleteCustTemplate(templateId);
  250 + uni.hideLoading();
  251 + uni.showToast({ title: '删除成功', icon: 'success' });
  252 + // 通知父组件刷新列表
  253 + emit('refresh');
  254 + } catch (err) {
  255 + uni.hideLoading();
  256 + console.error('删除失败:', err);
  257 + uni.showToast({ title: '删除失败', icon: 'none' });
  258 + }
255 } 259 }
256 } 260 }
257 - }  
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);
131 -  
132 - const { hideCategory, showCategory, getloadCategories } = actionStore; 133 + const { hideCategory, showCategory, getloadCategories, reloadCategories } = actionStore;
  134 +
  135 + // 页面数据就绪标记(避免 allCategories 为空时渲染空白)
  136 + const isDataReady = ref(false);
133 137
134 - // 计算属性:获取隐藏分类的对象数组 138 + // 隐藏分类的对象数组(含名称)
135 const hiddenCategoryObjects = computed(() => { 139 const hiddenCategoryObjects = computed(() => {
136 - return actionStore.allCategories.filter(category => 140 + if (!isDataReady.value) return [];
  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 - // 打开自定义分类编辑弹窗 188 + // 获取自定义分类列表
  189 + const fetchCustomCategories = async () => {
  190 + const res = await ExercisesApi.getCustomCategories();
  191 + customCategories.value = res.data || [];
  192 + };
  193 +
  194 + // 打开编辑弹窗
181 const openEditPopup = async () => { 195 const openEditPopup = async () => {
182 try { 196 try {
183 - const res = await ExercisesApi.getCustomCategories();  
184 - customCategories.value = res.data || []; 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 {  
276 - transform: scale(0.96);  
277 - background-color: #fafafa; 318 + &:active {
  319 + transform: scale(0.96);
  320 + background-color: #fafafa;
  321 + }
278 } 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 {  
320 - transform: scale(0.96);  
321 - background-color: #f2f2f2; 358 + &:active {
  359 + transform: scale(0.96);
  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,46 +13,76 @@ @@ -29,46 +13,76 @@
29 13
30 <!-- 右侧动作列表 --> 14 <!-- 右侧动作列表 -->
31 <scroll-view scroll-y class="right-content"> 15 <scroll-view scroll-y class="right-content">
32 - <!-- 部位筛选标签 -->  
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)">  
37 - {{ item.name }}  
38 - </view> 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>
39 </view> 25 </view>
40 26
41 - <view class="exercise-grid">  
42 - <view class="content" v-if="exercises.length > 0">  
43 - <view class="equipment-list">  
44 - <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">  
45 - <view class="equipment-name">{{ item.equipmentName }}</view>  
46 - <view class="action-list">  
47 - <view class="action-item" v-for="e in item.exercises" :key="e.id"  
48 - :class="{ selected: selectedIds.has(e.id) }" @click="toggleAction(e)">  
49 - <image :src="e.url3dAnimation" mode="aspectFill" class="action-img"></image>  
50 - <view class="action-name">{{ e.name }}</view>  
51 - </view> 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>
  35 + <!-- 部位筛选标签 -->
  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)">
  42 + {{ item.name }}
  43 + </view>
  44 + </view>
  45 + </scroll-view>
  46 +
  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">
  54 + <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
  55 + <view class="equipment-name">{{ item.equipmentName }}</view>
  56 + <view class="action-list">
  57 + <view class="action-item" v-for="e in item.exercises" :key="e.id"
  58 + :class="{ selected: selectedIds.has(e.id) }" @click="toggleAction(e)">
  59 + <image :src="e.url3dAnimation" mode="aspectFill" class="action-img"></image>
  60 + <view class="action-name">{{ e.name }}</view>
52 </view> 61 </view>
53 </view> 62 </view>
54 </view> 63 </view>
55 </view> 64 </view>
56 - <view class="empty-state" v-else>  
57 - <text class="empty-text">暂无动作数据</text> 65 +
  66 + <!-- 无动作数据 -->
  67 + <view class="content-tip empty-state" v-else>
  68 + <text class="tip-text">该分类下暂无动作</text>
  69 + <text class="tip-subtext">可尝试切换分类或部位查看其他动作</text>
58 </view> 70 </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">  
66 - <text class="name">{{ item.name }}</text>  
67 - <uni-icons type="close" size="16" color="#fff" @click="removeAction(item)"></uni-icons> 77 + <view class="selected-bar-inner">
  78 + <view class="selected-item" v-for="item in selectedActions" :key="item.id">
  79 + <text class="name">{{ item.name }}</text>
  80 + <uni-icons type="close" size="16" color="#fff" @click="removeAction(item)"></uni-icons>
  81 + </view>
68 </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> 103 </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>  
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
193 - const res = await ExercisesApi.getexercises({  
194 - categoriesId: activeNav.value,  
195 - subCategoriesId: id  
196 - })  
197 - exercises.value = res.data || [] 207 + loading.value = true
  208 + loadError.value = false
  209 + try {
  210 + const res = await ExercisesApi.getexercises({
  211 + categoriesId: activeNav.value,
  212 + subCategoriesId: id
  213 + })
  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,334 +286,351 @@ const saveCombination = async () => { @@ -268,334 +286,351 @@ 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 -} 341 +// ==================== 主体布局 ====================
  342 +.layout-container {
297 343
298 -.search-bar {  
299 - padding-top: var(--status-bar-height);  
300 - height: 88rpx;  
301 - background: #fff; 344 + min-height: 0;
  345 + display: flex;
  346 + overflow: hidden;
  347 + padding-bottom: calc(100rpx + 30rpx); // 底部按钮栏 + 间距
302 } 348 }
303 349
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; 350 +// 左侧导航
  351 +.left-nav {
  352 + width: 180rpx;
  353 + background: $color-bg-nav;
  354 + border-right: 1rpx solid $color-border;
  355 +
  356 + .nav-item {
  357 + padding: $spacing-lg $spacing-sm;
  358 + font-size: $font-sm;
  359 + color: $color-text-secondary;
  360 + position: relative;
  361 + white-space: nowrap;
  362 + text-align: center;
  363 +
  364 + &.active {
  365 + background: $color-white;
  366 + color: #000;
  367 + font-weight: 500;
  368 +
  369 + &::before {
  370 + content: "";
  371 + position: absolute;
  372 + left: 0;
  373 + top: 50%;
  374 + transform: translateY(-50%);
  375 + width: 6rpx;
  376 + height: 36rpx;
  377 + background: $color-primary;
  378 + }
  379 + }
  380 + }
314 } 381 }
315 382
316 -.menu-item {  
317 - display: flex;  
318 - align-items: center;  
319 - gap: 16rpx;  
320 -} 383 +// 右侧内容
  384 +.right-content {
  385 + flex: 1;
  386 + padding: $spacing-md;
  387 + padding-top: $spacing-lg;
321 388
322 -.menu-text {  
323 - box-sizing: border-box;  
324 - display: flex;  
325 - font-size: 28rpx;  
326 - // margin-left: 200rpx;  
327 -} 389 + // 通用内容提示块
  390 + .content-tip {
  391 + display: flex;
  392 + flex-direction: column;
  393 + align-items: center;
  394 + justify-content: center;
  395 + padding: 80rpx $spacing-lg;
  396 + text-align: center;
328 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 + }
329 409
330 -// .search-bar {  
331 -// padding: 20rpx 30rpx;  
332 -// background: #fff; 410 + // 初始引导
  411 + .init-guide {
  412 + .tip-text {
  413 + font-size: $font-lg;
  414 + color: $color-text-secondary;
  415 + font-weight: 500;
  416 + }
  417 + }
333 418
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 -// } 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 + }
344 428
345 -.layout-container {  
346 - flex: 1;  
347 - display: flex;  
348 - overflow: hidden;  
349 - padding-bottom: 130rpx; 429 + // 部位筛选标签(横向滚动)
  430 + .part-filter {
  431 + margin-bottom: $spacing-md;
  432 + white-space: nowrap;
350 433
351 - .left-nav {  
352 - width: 150rpx;  
353 - background: #fafafa;  
354 - border-right: 1rpx solid #eee; 434 + .part-filter-inner {
  435 + display: inline-flex;
  436 + flex-wrap: nowrap;
  437 + gap: 15rpx;
  438 + }
355 439
356 - .nav-item {  
357 - padding: 30rpx 20rpx;  
358 - font-size: 26rpx;  
359 - color: #666;  
360 - position: relative; 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;
361 448
362 &.active { 449 &.active {
363 - background: #fff; 450 + background: $color-primary;
364 color: #000; 451 color: #000;
365 font-weight: 500; 452 font-weight: 500;
366 -  
367 - &::before {  
368 - content: "";  
369 - position: absolute;  
370 - left: 0;  
371 - top: 50%;  
372 - transform: translateY(-50%);  
373 - width: 6rpx;  
374 - height: 36rpx;  
375 - background: #f8d714;  
376 - }  
377 } 453 }
378 } 454 }
379 } 455 }
380 456
381 - .right-content {  
382 - flex: 1;  
383 - padding: 20rpx; 457 + // 部位筛选为空
  458 + .part-empty-tip {
  459 + padding: 30rpx $spacing-lg 40rpx;
  460 + }
384 461
385 - .tip {  
386 - display: flex;  
387 - flex-wrap: wrap;  
388 - gap: 15rpx;  
389 - margin-bottom: 20rpx; 462 + .equipment-item {
  463 + margin-bottom: $spacing-lg;
390 464
391 - .item {  
392 - padding: 10rpx 20rpx;  
393 - background: #fff;  
394 - border-radius: 16rpx;  
395 - font-size: 24rpx; 465 + .equipment-name {
  466 + font-size: $font-md;
  467 + font-weight: 500;
  468 + color: $color-text-secondary;
  469 + margin-bottom: $spacing-sm;
  470 + padding-left: $spacing-xs;
  471 + border-left: 6rpx solid $color-primary;
  472 + }
396 473
397 - &.active {  
398 - background: #f8d714;  
399 - color: #000; 474 + .action-list {
  475 + display: grid;
  476 + grid-template-columns: repeat(2, 1fr);
  477 + gap: $spacing-sm;
  478 +
  479 + .action-item {
  480 + background: $color-white;
  481 + border-radius: $radius-md;
  482 + padding: $spacing-sm;
  483 + display: flex;
  484 + flex-direction: column;
  485 + align-items: center;
  486 + gap: $spacing-xs;
  487 + border: 2rpx solid transparent;
  488 + transition: border-color 0.2s, transform 0.15s;
  489 +
  490 + &.selected {
  491 + border-color: $color-primary;
400 } 492 }
401 - }  
402 - }  
403 493
404 - .equipment-item {  
405 - margin-bottom: 30rpx; 494 + &:active {
  495 + transform: scale(0.97);
  496 + }
406 497
407 - .equipment-name {  
408 - font-size: 28rpx;  
409 - font-weight: 500;  
410 - margin-bottom: 16rpx;  
411 - } 498 + .action-img {
  499 + width: 100%;
  500 + height: 200rpx;
  501 + border-radius: $radius-sm;
  502 + }
412 503
413 - .action-list {  
414 - display: grid;  
415 - grid-template-columns: repeat(2, 1fr);  
416 - gap: 16rpx;  
417 -  
418 - .action-item {  
419 - background: #fff;  
420 - border-radius: 16rpx;  
421 - padding: 16rpx;  
422 - display: flex;  
423 - flex-direction: column;  
424 - align-items: center;  
425 - gap: 8rpx;  
426 -  
427 - &.selected {  
428 - border: 2rpx solid #f8d714;  
429 - }  
430 -  
431 - .action-img {  
432 - width: 100%;  
433 - height: 200rpx;  
434 - border-radius: 12rpx;  
435 - }  
436 -  
437 - .action-name {  
438 - font-size: 24rpx;  
439 - color: #333;  
440 - } 504 + .action-name {
  505 + font-size: $font-xs;
  506 + color: $color-text-primary;
441 } 507 }
442 } 508 }
443 } 509 }
  510 + }
444 511
445 - .empty-state {  
446 - padding-top: 100rpx;  
447 - text-align: center;  
448 - color: #999;  
449 - font-size: 28rpx;  
450 - } 512 + // 无动作空状态
  513 + .empty-state {
  514 + padding-top: 100rpx;
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,35 +16,29 @@ @@ -16,35 +16,29 @@
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">  
30 - <template v-if="unitCount">  
31 - <view v-for="(unit, index) in units" :key="unit.unitId">  
32 - <dongzuo  
33 - :id="unit.unitId"  
34 - :type="unit.unitType"  
35 - :action-detail="trainingStore.convertUnitToActionDetail(unit)"  
36 - :ref="(el) => setActionRef(el, index)"  
37 - :unit-index="index"  
38 - /> 25 + <view class="content-wrapper">
  26 + <scroll-view scroll-y class="content-area">
  27 + <template v-if="unitCount">
  28 + <view v-for="(unit, index) in units" :key="unit.unitId">
  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" />
  32 + </view>
  33 + </template>
  34 +
  35 + <view v-else class="empty-area">
  36 + <view class="empty-text">
  37 + 尚未添加任何动作<br />
  38 + 点击最下方 + 开始添加动作
  39 + </view>
39 </view> 40 </view>
40 - </template>  
41 -  
42 - <view v-else class="empty-area">  
43 - <view class="empty-text">  
44 - 尚未添加任何动作<br />  
45 - 点击最下方 + 开始添加动作  
46 - </view>  
47 - </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 - /> 87 + <textarea class="intro-textarea" v-model="popupValue"
  88 + :placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'" />
98 </view> 89 </view>
99 - </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();  
321 - restCountdownTotal.value = seconds;  
322 - restCountdownSeconds.value = seconds;  
323 - currentRestUnitIndex.value = unitIndex;  
324 - restTimerWasRunning = !trainingStore.isPause;  
325 - if (restTimerWasRunning) trainingStore.toggleTimer();  
326 - restCountdownInterval = setInterval(() => {  
327 - restCountdownSeconds.value--;  
328 - if (restCountdownSeconds.value <= 0) {  
329 - clearRestCountdown();  
330 - uncheckCurrentUnit();  
331 - if (restTimerWasRunning) trainingStore.toggleTimer();  
332 - }  
333 - }, 1000);  
334 - };  
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; 310 + // 叠加倒计时:如果已经在倒计时中则累加,否则开始新的倒计时
  311 + if (restCountdownInterval) {
  312 + restCountdownSeconds.value += seconds;
  313 + restCountdownTotal.value += seconds;
  314 + } else {
  315 + restCountdownTotal.value = seconds;
  316 + restCountdownSeconds.value = seconds;
  317 + restCountdownInterval = setInterval(() => {
  318 + restCountdownSeconds.value--;
  319 + if (restCountdownSeconds.value <= 0) {
  320 + clearRestCountdown();
  321 + // 倒计时结束:不取消勾选状态,不恢复主计时器(主计时器始终在运行)
  322 + }
  323 + }, 1000);
341 } 324 }
342 - } 325 + // 不暂停主计时器,训练计时器始终继续
  326 + };
343 327
344 - function onCancelRestTimer(unitIndex) {  
345 - clearRestCountdown();  
346 - currentRestUnitIndex.value = unitIndex;  
347 - uncheckCurrentUnit();  
348 - if (restTimerWasRunning) {  
349 - trainingStore.toggleTimer();  
350 - restTimerWasRunning = false; 328 + function onCancelRestTimer(seconds) {
  329 + if (!restCountdownInterval) return;
  330 + // 取消勾选时减去对应秒数
  331 + restCountdownSeconds.value -= (seconds || 0);
  332 + restCountdownTotal.value -= (seconds || 0);
  333 + if (restCountdownSeconds.value <= 0) {
  334 + clearRestCountdown();
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">  
116 - <template v-if="urlTutorial">  
117 - <video :src="urlTutorial" class="preview-media" :controls="false" :show-fullscreen-btn="false"  
118 - :enable-progress-gesture="false"></video>  
119 - </template>  
120 - <template v-else>  
121 - <up-icon name="camera" color="#333" size="16" />  
122 - <text class="btn-text">添加视频</text>  
123 - </template>  
124 - </view> 73 + <view class="form-row media-row">
  74 + <text class="form-label">视频详解 <text class="optional">(非必填)</text></text>
  75 + <view class="media-upload" @click="chooseTutorialVideo">
  76 + <template v-if="urlTutorial">
  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>
  80 + </template>
  81 + <template v-else>
  82 + <up-icon name="play-right" color="#999" size="28" />
  83 + <text class="media-placeholder">添加详解视频</text>
  84 + </template>
125 </view> 85 </view>
126 </view> 86 </view>
127 87
128 - <view class="form-item" v-if="isEdit" @click="deleteAction">  
129 - <view class="center-label">删除动作</view>  
130 - </view>  
131 -  
132 - <!-- 保存按钮 -->  
133 - <view class="save-button">  
134 - <button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button> 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>
135 </view> 92 </view>
136 </view> 93 </view>
137 94
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>  
145 - </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> 95 + <!-- 保存按钮(卡片外、贴近底部) -->
  96 + <view class="save-bar">
  97 + <view class="btn-save" :class="{ disabled: !exerciseName.trim() }" @click="saveExercise">
  98 + 保存
153 </view> 99 </view>
154 </view> 100 </view>
155 101
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> 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" />
  110 + </view>
163 </view> 111 </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> 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>
  123 + </view>
  124 + </view>
  125 + </view>
  126 + </view>
  127 + <view class="popup-foot">
  128 + <view class="popup-done" @click="confirmMuscleSelection">完成</view>
179 </view> 129 </view>
180 </view> 130 </view>
181 - </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 + />
182 </view> 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 +};
248 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('');
  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' }) 545 + uni.showToast({ title: '删除请求失败', icon: 'none' });
633 } 546 }
634 } 547 }
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('、') + '(主)';  
675 - }  
676 - if (secondaryNames.length) {  
677 - if (displayText) displayText += ', ';  
678 - displayText += secondaryNames.join('、') + '(次)';  
679 - }  
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; 568 + background: $bg-dark;
  569 + padding: 24rpx 24rpx 0;
  570 + color: #fff;
702 } 571 }
703 572
704 -/* 肌肉部位图 */  
705 -  
706 -.muscle-container {  
707 - display: flex;  
708 - justify-content: space-between;  
709 - align-items: center;  
710 - gap: 40rpx;  
711 -}  
712 -  
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 -  
726 -/* 肌肉图样式 - 100%占满容器,保持比例不拉伸 */  
727 -.muscle-image-front {  
728 - width: 100%;  
729 - 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 580
741 -.legend {  
742 - display: flex;  
743 - justify-content: center;  
744 - margin-top: 20rpx;  
745 - font-size: 24rpx;  
746 - color: #ccc;  
747 -}  
748 -  
749 -.legend-item {  
750 - display: flex;  
751 - 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 -} 581 + .muscle-img {
  582 + width: 100%;
  583 + height: 100%;
  584 + }
765 585
766 -.secondary {  
767 - background-color: #66ccff; 586 + .muscle-tip {
  587 + position: absolute;
  588 + bottom: 20rpx;
  589 + left: 50%;
  590 + transform: translateX(-50%);
  591 + display: flex;
  592 + align-items: center;
  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 -}  
785 -  
786 -.action-name {  
787 - width: 285rpx;  
788 -} 610 +// ---- 通用行 ----
  611 +.form-row {
  612 + padding: 28rpx 32rpx;
  613 + border-bottom: 1rpx solid $divider;
  614 + transition: background 0.15s;
789 615
790 -.label {  
791 - font-size: 32rpx;  
792 - color: #333;  
793 - margin-bottom: 10rpx;  
794 -}  
795 -  
796 -.center-label {  
797 - text-align: center;  
798 - color: #c44b4b;  
799 -}  
800 -  
801 -.input-group {  
802 - display: flex;  
803 - align-items: center;  
804 - position: relative;  
805 -}  
806 -  
807 -.input-field {  
808 - 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 -} 616 + &:last-of-type {
  617 + border-bottom: none;
  618 + }
820 619
821 -.icon-wrapper {  
822 - width: 40rpx;  
823 - height: 40rpx;  
824 - margin-left: 10rpx;  
825 -} 620 + .form-label {
  621 + font-size: 30rpx;
  622 + font-weight: 600;
  623 + color: $text-main;
  624 + display: block;
826 625
827 -.edit-icon {  
828 - width: 24rpx;  
829 - height: 24rpx; 626 + .optional {
  627 + font-size: 24rpx;
  628 + font-weight: 400;
  629 + color: $text-hint;
  630 + }
  631 + }
830 } 632 }
831 633
832 -.cover-btn {  
833 - position: absolute;  
834 - right: 30rpx;  
835 - top: 30rpx; 634 +// ---- 动作名称行(横排) ----
  635 +.name-row {
836 display: flex; 636 display: flex;
837 - flex-direction: column;  
838 - align-items: center;  
839 - justify-content: center;  
840 - width: 100rpx;  
841 - height: 100rpx;  
842 - background: #f0f0f0;  
843 - border-radius: 10rpx;  
844 - padding: 10rpx;  
845 -} 637 + align-items: flex-start;
  638 + gap: 24rpx;
846 639
847 -.cover-icon {  
848 - width: 40rpx;  
849 - height: 40rpx;  
850 -} 640 + .name-input-area {
  641 + flex: 1;
  642 + min-width: 0;
851 643
852 -.btn-text {  
853 - font-size: 20rpx;  
854 - color: #333;  
855 - margin-top: 5rpx;  
856 -} 644 + .form-label {
  645 + margin-bottom: 12rpx;
  646 + }
857 647
858 -.value-wrapper {  
859 - display: flex;  
860 - justify-content: space-between;  
861 - align-items: center;  
862 - font-size: 28rpx;  
863 - color: #333;  
864 - margin-top: 10rpx;  
865 -} 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 + }
866 657
867 -.value {  
868 - color: #28c76f;  
869 - font-weight: bold;  
870 -} 658 + .cover-upload {
  659 + flex-shrink: 0;
  660 + display: flex;
  661 + flex-direction: column;
  662 + align-items: center;
  663 + gap: 8rpx;
  664 +
  665 + .cover-preview {
  666 + width: 140rpx;
  667 + height: 140rpx;
  668 + border-radius: 14rpx;
  669 + background: #f5f5f5;
  670 + }
871 671
872 -.arrow-icon {  
873 - width: 20rpx;  
874 - height: 20rpx;  
875 - transform: rotate(90deg); 672 + .cover-label {
  673 + font-size: 22rpx;
  674 + color: $text-hint;
  675 + }
  676 + }
876 } 677 }
877 678
878 -.upload-btn {  
879 - position: absolute;  
880 - right: 30rpx;  
881 - top: 30rpx; 679 +// ---- 选择行(训练部位/动作类型/目录管理) ----
  680 +.select-row {
882 display: flex; 681 display: flex;
883 - flex-direction: column;  
884 align-items: center; 682 align-items: center;
885 - 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 -}  
904 -  
905 -.camera-icon {  
906 - width: 40rpx;  
907 - height: 40rpx;  
908 -}  
909 -  
910 -.upload-text {  
911 - 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 -}  
924 -  
925 -.btn-save {  
926 - 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 -} 683 + justify-content: space-between;
950 684
951 -.popup-content {  
952 - width: 100%;  
953 - background: white;  
954 - border-radius: 16rpx 16rpx 0 0;  
955 - max-height: 70vh;  
956 - overflow: hidden;  
957 -} 685 + &:active {
  686 + background: #fafafa;
  687 + }
958 688
959 -.popup-header {  
960 - display: flex;  
961 - justify-content: space-between;  
962 - align-items: center;  
963 - padding: 24rpx 32rpx;  
964 - border-bottom: 1rpx solid #eee; 689 + .select-value {
  690 + display: flex;
  691 + align-items: center;
  692 + gap: 8rpx;
  693 + max-width: 60%;
  694 + overflow: hidden;
  695 +
  696 + .sel-text {
  697 + font-size: 26rpx;
  698 + color: #28c76f;
  699 + font-weight: 600;
  700 + white-space: nowrap;
  701 + overflow: hidden;
  702 + text-overflow: ellipsis;
  703 + }
  704 + }
965 } 705 }
966 706
967 -.close-btn {  
968 - font-size: 40rpx;  
969 - color: #333;  
970 -} 707 +// ---- 媒体上传行(动作视频/视频详解) ----
  708 +.media-row {
  709 + .media-upload {
  710 + margin-top: 20rpx;
  711 + display: flex;
  712 + flex-direction: column;
  713 + align-items: center;
  714 + justify-content: center;
  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;
  724 +
  725 + &:active {
  726 + background: #f0f0f0;
  727 + border-color: #bbb;
  728 + }
971 729
972 -.title {  
973 - font-size: 32rpx;  
974 - font-weight: bold;  
975 - margin: 0;  
976 -} 730 + .media-placeholder {
  731 + font-size: 24rpx;
  732 + color: $text-hint;
  733 + }
977 734
978 -.confirm-btn {  
979 - width: 80rpx;  
980 - height: 40rpx;  
981 - line-height: 40rpx;  
982 - background-color: #ff9900;  
983 - color: white;  
984 - border-radius: 20rpx;  
985 - text-align: center;  
986 - font-size: 28rpx;  
987 -} 735 + .media-preview {
  736 + position: absolute;
  737 + inset: 0;
  738 + width: 100%;
  739 + height: 100%;
  740 + object-fit: cover;
  741 + }
988 742
989 -.picker-view {  
990 - height: 200rpx;  
991 - width: 100%; 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 + }
992 } 755 }
993 756
994 -.picker-item { 757 +// ---- 删除行 ----
  758 +.delete-row {
995 display: flex; 759 display: flex;
996 align-items: center; 760 align-items: center;
997 justify-content: center; 761 justify-content: center;
998 - font-size: 32rpx;  
999 - color: #333;  
1000 - height: 100%;  
1001 -} 762 + gap: 10rpx;
  763 + padding: 32rpx;
1002 764
1003 -.double-picker {  
1004 - display: flex;  
1005 -} 765 + .delete-text {
  766 + font-size: 30rpx;
  767 + color: #e74c3c;
  768 + font-weight: 500;
  769 + }
1006 770
1007 -.double-picker .picker-view.half {  
1008 - width: 50%; 771 + &:active {
  772 + background: #fef2f2;
  773 + }
1009 } 774 }
1010 775
1011 -/* 封面预览 */  
1012 -.cover-preview {  
1013 - display: flex;  
1014 - justify-content: center;  
1015 - margin: 30rpx 0;  
1016 -} 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;
  785 + text-align: center;
  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;
  793 +
  794 + &:active {
  795 + transform: scale(0.98);
  796 + }
1017 797
1018 -.cover-image {  
1019 - width: 200rpx;  
1020 - height: 200rpx; 798 + &.disabled {
  799 + opacity: 0.4;
  800 + pointer-events: none;
  801 + }
  802 + }
1021 } 803 }
1022 804
1023 -/* 上传区域 */  
1024 -.upload-area { 805 +// ==================== 弹窗通用 ====================
  806 +.popup-body {
  807 + background: #fff;
  808 + border-radius: 16rpx 16rpx 0 0;
  809 + max-height: 65vh;
1025 display: flex; 810 display: flex;
1026 flex-direction: column; 811 flex-direction: column;
1027 - align-items: center;  
1028 - justify-content: center;  
1029 - padding: 40rpx;  
1030 } 812 }
1031 813
1032 -.upload-icon {  
1033 - width: 60rpx;  
1034 - height: 60rpx;  
1035 -}  
1036 -  
1037 -.upload-text {  
1038 - font-size: 28rpx;  
1039 - color: #666;  
1040 - margin-top: 10rpx;  
1041 -}  
1042 -  
1043 -/* === 美化后的肌肉选择列表 === */  
1044 -.muscle-list-beautified {  
1045 - padding: 20rpx 32rpx;  
1046 -}  
1047 -  
1048 -.muscle-item-beautified {  
1049 - display: flex;  
1050 - align-items: center;  
1051 - justify-content: space-between;  
1052 - padding: 24rpx 0;  
1053 - border-bottom: 1rpx solid #f0f0f0;  
1054 -}  
1055 -  
1056 -.muscle-name {  
1057 - font-size: 32rpx;  
1058 - color: #333;  
1059 - font-weight: 500;  
1060 - flex: 1;  
1061 -}  
1062 -  
1063 -.selector-btn { 814 +.popup-head {
1064 display: flex; 815 display: flex;
1065 align-items: center; 816 align-items: center;
1066 justify-content: center; 817 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 -} 818 + padding: 28rpx 32rpx;
  819 + border-bottom: 1rpx solid $divider;
  820 + position: relative;
1079 821
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 -} 822 + .popup-title {
  823 + font-size: 32rpx;
  824 + font-weight: 700;
  825 + color: $text-main;
  826 + }
1085 827
1086 -.selector-btn.secondary {  
1087 - border: 2rpx solid #cce6ff; 828 + .popup-close {
  829 + position: absolute;
  830 + right: 32rpx;
  831 + top: 50%;
  832 + transform: translateY(-50%);
  833 + width: 48rpx;
  834 + height: 48rpx;
  835 + display: flex;
  836 + align-items: center;
  837 + justify-content: center;
  838 + }
1088 } 839 }
1089 840
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); 841 +.popup-foot {
  842 + padding: 20rpx 32rpx;
  843 + padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  844 + border-top: 1rpx solid $divider;
  845 +
  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;
  856 +
  857 + &:active {
  858 + transform: scale(0.97);
  859 + }
  860 + }
1094 } 861 }
1095 862
1096 -.selector-dot {  
1097 - width: 16rpx;  
1098 - height: 16rpx;  
1099 - border-radius: 50%;  
1100 -} 863 +// ==================== 肌肉选择列表 ====================
  864 +.muscle-list {
  865 + overflow-y: auto;
  866 + padding: 8rpx 0;
  867 + flex: 1;
1101 868
1102 -.selector-dot.primary {  
1103 - background-color: #ff9900;  
1104 -} 869 + .muscle-item {
  870 + display: flex;
  871 + align-items: center;
  872 + justify-content: space-between;
  873 + padding: 22rpx 32rpx;
  874 + border-bottom: 1rpx solid $divider;
  875 +
  876 + .muscle-name {
  877 + font-size: 30rpx;
  878 + font-weight: 500;
  879 + color: $text-main;
  880 + flex: 1;
  881 + }
1105 882
1106 -.selector-dot.secondary {  
1107 - background-color: #66ccff; 883 + .muscle-actions {
  884 + display: flex;
  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;
  900 +
  901 + &.primary {
  902 + &.on {
  903 + background: #fff6ec;
  904 + color: $primary;
  905 + border-color: $primary;
  906 + }
  907 + }
1115 908
1116 -.selector-btn.primary.active .selector-label,  
1117 -.selector-btn.secondary.active .selector-label {  
1118 - color: #333;  
1119 - font-weight: bold; 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)">
@@ -54,13 +22,12 @@ @@ -54,13 +22,12 @@
54 </view> 22 </view>
55 </view> 23 </view>
56 24
57 - <!-- 场景筛选 --> 25 + <!-- 场景筛选 -->
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 - // ======================  
193 - // 官方模板大类  
194 - // ======================  
195 - if (isBigTemOffice.value) {  
196 - response = await TemplatesApi.selecttemplates(id);  
197 - }  
198 -  
199 - // ======================  
200 - // 个人模板大类  
201 - // ======================  
202 - else {  
203 - // 个人模板大类接口  
204 - // response = await TemplatesApi.getCustTemplateGroupDetail(id);  
205 - response = { data: { templates: [] } }; // 临时空数据,不报错 138 + try {
  139 + // 官方模板大类
  140 + if (isBigTemOffice.value) {
  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 = []
  149 + }
  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> 57 + </view>
94 </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 {  
400 - uni.showLoading({ title: '删除中...' });  
401 - await TemplatesApi.deleteCustTemplateGroup(currentFolder.value.id);  
402 - uni.hideLoading();  
403 - uni.showToast({ title: '删除成功' });  
404 - // 重新拉取最新文件夹列表  
405 - getFolderList();  
406 - } catch (err) {  
407 - uni.hideLoading();  
408 - console.error('删除失败:', err);  
409 - uni.showToast({ title: '删除失败', icon: 'none' });  
410 - } 389 +
  390 + uni.showLoading({ title: '删除中...' });
  391 + const res = await TemplatesApi.deleteCustTemplateGroup(folderId);
  392 +
  393 +
  394 +
  395 + uni.showToast({ title: '删除成功' });
  396 + getFolderList();
  397 +
  398 +
  399 + uni.hideLoading();
  400 +
411 } 401 }
412 - closeFolderMenu();  
413 - }  
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">
9 3
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>  
15 - </view>  
16 4
17 - <!-- 更多权益 -->  
18 - <view class="more-benefits">  
19 - <view class="more-text">华创信开发者</view>  
20 - <view class="more-text">系统开发中</view>  
21 - <view class="more-text">敬请期待~</view> 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>
  12 + </view>
  13 +
  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) }">&#9660;</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' }); 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 + }
34 }, 156 },
35 - }, 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 } 177 }
74 178
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 -}  
89 -  
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;  
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;  
144 - border-radius: 50%;  
145 -}  
146 -  
147 -.benefits-section {  
148 - margin: 20rpx;  
149 - padding: 0 20rpx; 213 +.faq-item {
  214 + border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
150 } 215 }
151 216
152 -.section-title {  
153 - font-size: 32rpx;  
154 - margin-bottom: 20rpx;  
155 - padding-left: 10rpx; 217 +.faq-item-last {
  218 + border-bottom: none;
156 } 219 }
157 220
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>
  36 + </view>
  37 +
  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>
15 </view> 69 </view>
16 70
17 - <!-- 更多权益 -->  
18 - <view class="more-benefits">  
19 - <view class="more-text">华创信开发者</view>  
20 - <view class="more-text">系统开发中</view>  
21 - <view class="more-text">敬请期待~</view> 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;
156 } 235 }
157 236
158 -.benefit-list { 237 +.feature-desc {
  238 + font-size: 24rpx;
  239 + color: rgba(255, 255, 255, 0.5);
  240 + line-height: 1.5;
  241 +}
  242 +
  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;
  271 +}
  272 +
  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);
185 } 281 }
186 282
187 -.benefit-desc {  
188 - font-size: 18rpx;  
189 - color: #ccc;  
190 - line-height: 1.4; 283 +.link-item:last-child {
  284 + border-bottom: none;
191 } 285 }
192 286
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; 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,7 +217,9 @@ const useUserStore = defineStore( @@ -217,7 +217,9 @@ const useUserStore = defineStore(
217 strategies: [ 217 strategies: [
218 { 218 {
219 key: 'user-store', 219 key: 'user-store',
220 - }, 220 + // 修复插件 H5 存读后端不一致:save 走 localStorage、load 走 sessionStorage,显式指定 storage 保证同源
  221 + storage: typeof window !== 'undefined' ? window.localStorage : undefined,
  222 + },
221 ], 223 ],
222 }, 224 },
223 }, 225 },