TemplateAdd.vue
18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
<template>
<div class="page-contain">
<div class="templateName">
<el-form ref="formRef" :model="formData" :rules="rules">
<!-- 模板名称 -->
<el-form-item label-suffix="*" prop="templateName" label="模板名称">
<el-input type="text" v-model="formData.templateName" placeholder="请输入模板名称" />
</el-form-item>
<!-- 封面图 -->
<el-form-item label-suffix="*" prop="templateCover" label="封面图">
<UploadImg v-model="formData.templateCover" :directory="'course'" />
</el-form-item>
<!-- 训练场景 -->
<el-form-item label-suffix="*" prop="scene" label="训练场景">
<el-select v-model="formData.scene" placeholder="请选择训练场景" style="width: 100%">
<el-option label="不限" :value="1" />
<el-option label="仅哑铃" :value="2" />
<el-option label="仅哑铃+杠铃" :value="3" />
<el-option label="健身房" :value="4" />
</el-select>
</el-form-item>
<!-- 模板简介 -->
<el-form-item label-suffix="*" prop="templateIntroduction" label="模板简介">
<el-input v-model="formData.templateIntroduction" placeholder="请输入" type="textarea" :rows="3" />
</el-form-item>
<!-- 动作组容器:循环渲染所有动作组 -->
<div v-for="(unit, unitIndex) in formData.units" :key="unitIndex" class="unit-card">
<!-- 单元类型(动作组/超级组) -->
<el-form-item label-suffix="*" :label="`单元${unitIndex + 1}类型`" :prop="`units[${unitIndex}].unitType`">
<el-select v-model="unit.unitType" placeholder="请选择单元类型" style="width:100%">
<el-option label="动作组" :value="1" />
<el-option label="超级组" :value="2" />
</el-select>
</el-form-item>
<!-- 这里实现选择超级组或者动作组名称 -->
<el-form-item label-suffix="*" :label="unit.unitType === 1 ? '动作组名称' : '超级组名称'">
<el-select v-model="unit.id" @change="(val) => onSelectItem(unitIndex, val)">
<template v-if="unit.unitType === 1">
<!-- 需要接入动作接口 -->
<el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" />
</template>
<!-- 需要接入超级组接口 -->
<template v-else>
<el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" />
</template>
</el-select>
</el-form-item>
<!-- 动作组头部 动作组名称+删除和增加动作组按钮-->
<div class="unit-header">
<el-form-item>
<el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1">
<template #icon>
<Minus />
</template>
删除单元
</el-button>
</el-form-item>
<el-form-item>
<el-button @click="AddUnit()">
<template #icon>
<Plus />
</template>
新增单元
</el-button>
</el-form-item>
</div>
<!-- 排序 -->
<el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`">
<el-input v-model="unit.sortOrder" min="1" max="99" />
</el-form-item>
<!-- 循环:动作组 1 个、超级组 N 个,都会渲染 -->
<template v-for="(item, idx) in unit.exercises" :key="idx">
<div style="margin: 16px 0;">
<div>动作名称:{{ getExerciseName(item.exerciseId) }}</div>
<ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" />
</div>
</template>
</div>
<!-- 取消/保存按钮 -->
<el-form-item>
<div class="btn-center-wrapper">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button>
</div>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, nextTick } from 'vue'
import lostImg from '@/assets/imgs/lost.png'
import { MusclesApi } from '@/api/store/training/muscle'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Minus, Plus } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import './TemplateAdd.css'
import { TrainingTemplatesApi } from '@/api/store/training/templates'
import { ExercisesApi } from '@/api/store/training/pose'
import ExerciseSetTable from '../templates/ExerciseSetTable.vue'
//定义参数
const formRef = ref<FormInstance>()
const muscleList = ref<any[]>([])
const loading = ref(false)
const router = useRouter()
const route = useRoute()
const exercisesList = ref<any[]>([])
const superGroupList = ref<any[]>([])
import { SupersetsApi } from '@/api/store/training/Supersets'
// 表单数据
const formData = reactive({
// templateId: 0, // 新增接口要求的根级别id字段
id: 0,
groupId: null,
templateName: '',
scene: null,
templateCover: '',
templateIntroduction: '',
units: [
{
id: 0,
name: '',
unitType: 1, //1=动作组,2=超级组
sortOrder: 1,
exercises: [
{
exerciseId: 0,
exerciseType: 1, //动作类型
innerOrder: 1,
sets: [
{
weight: 0,
reps: 0,
distance: 0,
duration: 0, // 接口唯一时间字段
restTime: 0,
setIndex: 1
}
]
}
]
}
]
})
//静态表单验证规则,对应不是循环的表单项检验
const rules = ref({
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
scene: [{ required: true, message: '请选择训练场景', trigger: 'change' }],
templateIntroduction: [{ required: true, message: '请输入模板简介', trigger: 'blur' }]
})
// ===========================加载数据======================================
//加载数据
const loadsuperGroupList = async () => {
try {
const res = await SupersetsApi.getSupersetsPage({ pageNo: 1, pageSize: 100, name: '' })
const data = res.data || res
superGroupList.value = data.list || []
} catch (err) {
console.error('加载动作列表失败:', err)
ElMessage.error('加载动作列表失败')
}
}
//加载数据
const loadExercisesList = async () => {
try {
const res = await ExercisesApi.getExercisesPage({ pageNo: 1, pageSize: 100, name: '' })
const data = res.data || res
exercisesList.value = data.list || []
} catch (err) {
console.error('加载动作列表失败:', err)
ElMessage.error('加载动作列表失败')
}
}
// ====================== 【关键】编辑回显 ======================
const loadTemplateDetail = async (id: number) => {
try {
loading.value = true
const res = await TrainingTemplatesApi.getTrainingTemplates(id)
const detail = res.data || res
console.log('编辑详细模板返回的数据detail', detail)
formData.id = detail.id || 0
formData.groupId = detail.groupId || null
formData.templateName = detail.templateName || ''
formData.scene = detail.scene || null
formData.templateCover = detail.urlCover || detail.templateCover || ''
formData.templateIntroduction = detail.introduction || ''
// 🔥 修复:超级组多动作回显(不再写死 [0])
if (detail.units && detail.units.length) {
formData.units = detail.units.map((unit: any) => ({
id: unit.unitId || 0,
name: unit.unitName || '',
unitType: unit.unitType || 1,
sortOrder: unit.sortOrder || 1,
// ✅ 这里修复:完整映射所有动作(动作组1个、超级组N个都正常)
exercises: unit.exercises?.map((item: any) => ({
exerciseId: item.exerciseId || 0,
exerciseType: item.exerciseType || 1,
innerOrder: item.innerOrder || 1,
sets: item.sets || [
{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }
]
})) || [{
exerciseId: 0,
exerciseType: 1,
innerOrder: 1,
sets: [{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }]
}]
}))
}
nextTick(() => {
updateDynamicRules()
formRef.value?.clearValidate()
})
} catch (err) {
ElMessage.error('获取详情失败')
console.error(err)
} finally {
loading.value = false
}
}
//动态检验
const updateDynamicRules = () => {
// 先清理所有动态校验规则(避免删除单元后残留旧规则报错)
Object.keys(rules.value).forEach(key => {
if (key.startsWith('units[')) { // 匹配所有循环生成的规则
delete rules.value[key]
}
})
//必须使用units[${index}].unitType这种,不然验证找不到真实的数据,验证出错。
formData.units.forEach((unit, index) => {
rules.value[`units[${index}].unitType`] = [
{ required: true, message: '请选择单元类型', trigger: 'change' }
]
rules.value[`units[${index}].name`] = [
{ required: true, message: '请选择动作', trigger: 'change' }
]
rules.value[`units[${index}].sortOrder`] = [
{ required: true, message: '请输入排序', trigger: 'blur' }
]
})
// 清除校验状态,防止新增就爆红
nextTick(() => {
if (formRef.value) formRef.value.clearValidate()
})
}
// ---------------------- 交互方法 ----------------------
// 动作选择联动名称(新增unitIndex参数,绑定当前动作组)
const handleExerciseChange = (exerciseId: number, unitIndex: number) => {
const unit = formData.units[unitIndex]
// 根据类型找对应的数据
let selectedItem
if (unit.unitType === 1) {
selectedItem = exercisesList.value.find(item => item.id === exerciseId)
} else {
selectedItem = superGroupList.value.find(item => item.id === exerciseId)
}
if (selectedItem) {
unit.name = selectedItem.name
}
}
// 根据ID获取动作名称
const getExerciseName = (id: number) => {
if (!id) return '未选择动作'
// 先从动作列表找
const item = exercisesList.value.find(i => i.id === id)
if (item) return item.name
return '未知动作'
}
// 删除动作组(接收unitIndex,删除指定动作组)
const delUnit = (unitIndex: number) => {
// 只保留至少 1 个单元
if (formData.units.length <= 1) {
ElMessage.warning('至少保留一个单元')
return
}
ElMessageBox.confirm('确定要删除这个单元吗?', '提示', { type: 'warning' }).then(() => {
formData.units.splice(unitIndex, 1)
// 重新排序
formData.units.forEach((unit, index) => {
unit.sortOrder = index + 1
})
updateDynamicRules()
ElMessage.success('单元删除成功')
})
}
//增加单元(动作组/超级组)
const AddUnit = () => {
// 默认新增动作组,用户之后可在页面切换为超级组
const newUnit = {
id: 0,
name: '',
unitType: 1, // 默认动作组
sortOrder: formData.units.length + 1,
exercises: [
{
exerciseId: 0,
exerciseType: 1,
innerOrder: 1,
sets: [
{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }
]
}
]
}
formData.units.push(newUnit)
updateDynamicRules()
ElMessage.success('新增单元成功(可在上方切换为超级组)')
}
// 选择 动作 或 超级组 后,加载详情
const onSelectItem = async (unitIndex: number, selectId: number) => {
const unit = formData.units[unitIndex]
// 清空之前的数据
unit.exercises = []
if (unit.unitType === 1) {
// ==================
// 1. 动作组:查单个动作详情
// ==================
const res = await ExercisesApi.getExercises(selectId)
console.log('选择的动作组详情', res)
const detail = res
console.log('选择的动作详情:detail', detail)
console.log('【动作组详情完整数据】', JSON.stringify(detail, null, 2))
console.log('【动作名称】', detail.name)
console.log('【后端返回的原始exerciseType】', detail.exerciseType)
unit.name = detail.name || ''
// 关键2:只有动作详情是文字,做文字转数字映射
const typeMap: Record<string, number> = {
'无氧运动': 0,
'有氧运动': 1,
'记次训练': 2,
'计时训练': 3,
'自重加重': 4,
'自重减重': 5,
'间歇训练': 6
}
// 能映射就转数字,映射失败默认给0
const typeVal = typeMap[detail.exerciseType] ?? 0
unit.exercises.push({
exerciseId: detail.id,
exerciseType: typeVal,
innerOrder: 1,
sets: [{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }]
})
console.log('【映射后的exerciseType数字】', typeVal)
} else {
// ==================
// 2. 超级组:查超级组详情(返回多个动作)
// ==================
const res = await SupersetsApi.getSupersets(selectId)
console.log('加载的超级组详情:', res)
const detail = res
unit.name = detail.name || ''
console.log('加载选中的超级组详情:', detail)
// 🔴 关键:超级组接口直接返回数字,不需要再做映射
unit.exercises = detail.exercises.map((item: any) => {
console.log('【超级组内动作原始exerciseType】', item.exerciseType)
// 直接用返回的数字即可
const typeVal = Number(item.exerciseType) || 0
console.log('【超级组内动作最终exerciseType数字】', typeVal)
return {
exerciseId: item.id,
exerciseType: typeVal,
innerOrder: item.innerOrder,
sets: item.sets || [{ weight: 0, reps: 0, restTime: 0, setIndex: 1 }]
}
})
}
}
// 取消
const handleCancel = () => {
ElMessage.info('取消保存表单内容')
formRef.value?.resetFields()
// 重置表单数据(可选)
formData.id = 0
formData.groupId = null
formData.templateName = ''
formData.scene = null
formData.templateCover = ''
formData.templateIntroduction = ''
formData.units = [
{
id: 0,
name: '',
unitType: 1,
sortOrder: 1,
exercises: [
{
exerciseId: 0,
exerciseType: 1,
innerOrder: 1,
sets: [
{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }
]
}
]
}
]
// 重置后更新校验规则
updateDynamicRules()
// ✅ 关键:跳回主页面
router.push('/training/templates/templatesForm')
}
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch (err) {
ElMessage.error('请按照要求填写')
return
}
// 动作组/超级组不能为空。
const hasEmptyExercise = formData.units.some(unit => !unit.exercises[0].exerciseId)
if (hasEmptyExercise) {
ElMessage.warning('请为所有动作组选择动作名称')
return
}
loading.value = true
try {
// 👇 按接口文档转换提交数据
const submitData = {
templateId: formData.id, // 关键:前端id → 后端templateId
groupId: formData.groupId,
templateName: formData.templateName,
scene: formData.scene,
templateCover: formData.templateCover,
templateIntroduction: formData.templateIntroduction,
units: formData.units.map(unit => ({
id: unit.id,
name: unit.name,
unitType: unit.unitType,
sortOrder: unit.sortOrder,
exercises: unit.exercises.map(exercise => ({
exerciseId: exercise.exerciseId,
exerciseType: exercise.exerciseType,
innerOrder: exercise.innerOrder,
sets: exercise.sets.map(set => ({
weight: set.weight,
reps: set.reps,
distance: set.distance,
duration: set.duration,
restTime: set.restTime,
setIndex: set.setIndex
}))
}))
}))
}
console.log('提交给后端的完整数据:', submitData)
let res: any
if (route.query.id) {
// 编辑:用转换后的submitData
res = await TrainingTemplatesApi.updateTrainingTemplates(submitData)
} else {
// 新增:用转换后的submitData
res = await TrainingTemplatesApi.createTrainingTemplates(submitData)
}
console.log('接口返回结果:', res)
ElMessage.success('模板保存成功!')
router.push('/training/templates/templatesForm')
} catch (err: any) {
// 👇 修复日志打印,避免undefined
console.error("【完整错误对象】", err);
console.error("【错误信息】", err?.message || '未知错误');
console.error("【响应数据】", err?.response?.data || '无响应数据');
console.error("【响应状态】", err?.response?.status || '无状态码');
ElMessage.error(`保存失败:${err?.message || '网络/服务异常'}`);
} finally {
loading.value = false
}
}
// 加载肌肉列表
const loadMuscleList = async () => {
try {
const res = await MusclesApi.getMusclesPage({ pageNo: '1', pageSize: '100', name: '' })
const data = res.data || res
muscleList.value = data.list || []
} catch (err) {
console.error('加载肌肉列表失败:', err)
ElMessage.error('加载肌肉列表失败')
}
}
//生命周期
onMounted(async () => {
// groupList.value = [{ id: 1, name: "默认大类" }]
loadMuscleList()
loadExercisesList()
loadsuperGroupList()
// 进入页面判断是否编辑
if (route.query.id) {
loadTemplateDetail(Number(route.query.id))
}
})
// 监听路由ID变化,自动回显
watch(
() => route.query.id,
(newId) => {
if (newId) {
loadTemplateDetail(Number(newId))
} else {
// handleCancel()
}
},
{ immediate: true }
)
</script>