|
|
|
<template>
|
|
|
|
<Dialog v-model="dialogVisible" :title="title" width="800px" height="100%" append-to-body>
|
|
|
|
<!-- 这里直接用 SonFormData -->
|
|
|
|
<el-form ref="formRef" :model="SonFormData" label-width="120px" :rules="formRules" label-position="right">
|
|
|
|
<el-form-item label="计划名称" prop="name" label-suffix="*">
|
|
|
|
<Dialog v-model="dialogVisible" :title="title" width="800px">
|
|
|
|
<el-form ref="formRef" :model="SonFormData" label-width="120px" :rules="formRules">
|
|
|
|
<el-form-item label="计划名称" prop="name">
|
|
|
|
<el-input v-model="SonFormData.name" placeholder="请输入计划名称" maxlength="50" show-word-limit />
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item label="封面图" prop="urlCover" class="form-item-margin" label-suffix="*">
|
|
|
|
<el-form-item label="封面图" prop="urlCover">
|
|
|
|
<UploadImg v-model="SonFormData.urlCover" />
|
|
|
|
</el-form-item>
|
|
|
|
<!-- 计划分类/后来加上 -->
|
|
|
|
<el-form-item label="计划分类" prop="categoryId" label-suffix="*">
|
|
|
|
|
|
|
|
<el-form-item label="计划分类" prop="categoryId">
|
|
|
|
<el-select v-model="SonFormData.categoryId" placeholder="请选择计划分类">
|
|
|
|
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
|
|
|
|
</el-select>
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item label="训练场景" prop="trainingScene" label-suffix="*">
|
|
|
|
<el-select v-model="SonFormData.trainingScene">
|
|
|
|
<el-option label="不限" :value="1" />
|
|
|
|
<el-option label="健身房" :value="2" />
|
|
|
|
<el-option label="仅哑铃" :value="3" />
|
|
|
|
<el-option label="仅哑铃+杠铃" :value="4" />
|
|
|
|
<el-form-item label="训练场景" prop="trainingScene">
|
|
|
|
<el-select v-model="SonFormData.trainingScene" placeholder="请选择训练场景">
|
|
|
|
<el-option v-for="item in trainingSceneOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
</el-select>
|
|
|
|
</el-form-item>
|
|
|
|
<!-- 难度的不限是后来加上的 -->
|
|
|
|
<el-form-item label="计划难度" prop="difficultyLevel" label-suffix="*">
|
|
|
|
<el-select v-model="SonFormData.difficultyLevel">
|
|
|
|
<!-- <el-option label="不限" :value="0" /> -->
|
|
|
|
<el-option label="初阶" :value="1" />
|
|
|
|
<el-option label="中阶" :value="2" />
|
|
|
|
<el-option label="高阶" :value="3" />
|
|
|
|
|
|
|
|
<el-form-item label="计划难度" prop="difficultyLevel">
|
|
|
|
<el-select v-model="SonFormData.difficultyLevel" placeholder="请选择难度">
|
|
|
|
<el-option v-for="item in difficultyLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
</el-select>
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item label="适合人群" prop="targetPeople" label-suffix="*">
|
|
|
|
<el-select v-model="SonFormData.targetPeople">
|
|
|
|
<el-option label="不限" :value="1" />
|
|
|
|
<el-option label="青少年" :value="2" />
|
|
|
|
<el-option label="成年人" :value="3" />
|
|
|
|
<el-option label="中年人" :value="4" />
|
|
|
|
<el-option label="运动损伤" :value="5" />
|
|
|
|
<el-form-item label="适合人群" prop="targetPeople">
|
|
|
|
<el-select v-model="SonFormData.targetPeople" placeholder="请选择适合人群">
|
|
|
|
<el-option v-for="item in targetPeopleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
</el-select>
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item label="频率" prop="frequencyPerWeek" label-suffix="*">
|
|
|
|
<el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1">
|
|
|
|
<el-form-item label="频率" prop="frequencyPerWeek">
|
|
|
|
<el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1" placeholder="请选择频率">
|
|
|
|
<el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
</el-select>
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item label="计划周期(周)" prop="durationWeeks" label-suffix="*">
|
|
|
|
<el-form-item label="计划周期(周)" prop="durationWeeks">
|
|
|
|
<el-input-number v-model="SonFormData.durationWeeks" class="w-full" />
|
|
|
|
</el-form-item>
|
|
|
|
<!-- 选择模板,并且显示模板详情 -->
|
|
|
|
<div v-for="(templates, templatesIndex) in SonFormData.templateIds" :key="templatesIndex" style="width: 100%;">
|
|
|
|
<el-form-item label="模板" :prop="`templateIds[${templatesIndex}].templateId`" label-suffix="*"
|
|
|
|
style="width: 100%; margin: 0;" class="is-required">
|
|
|
|
<div style="display: flex; align-items: center; gap: 12px;width: 100%;">
|
|
|
|
<el-select v-model="templates.templateId" placeholder="请选择模板" style="flex: 1; width: 0;"
|
|
|
|
@change="() => loadTemplateDetail(templatesIndex, templates.templateId)"
|
|
|
|
@click="() => reloadTemplateDetail(templatesIndex)">
|
|
|
|
|
|
|
|
<!-- 模板选择区域 -->
|
|
|
|
<div v-for="(tpl, tplIdx) in SonFormData.templateIds" :key="tplIdx" class="template-block">
|
|
|
|
<div class="template-block-toolbar">
|
|
|
|
<el-button :disabled="SonFormData.templateIds.length <= 1" @click="removeTemplate(tplIdx)">
|
|
|
|
<Icon icon="ep:minus" class="mr-5px" />删除
|
|
|
|
</el-button>
|
|
|
|
<el-button type="primary" @click="addTemplate(tplIdx)">
|
|
|
|
<Icon icon="ep:plus" class="mr-5px" />新增
|
|
|
|
</el-button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<el-form-item :prop="`templateIds[${tplIdx}].templateId`" label="模板">
|
|
|
|
<el-select v-model="tpl.templateId" placeholder="请选择模板" class="w-full"
|
|
|
|
@change="() => loadTemplateDetail(tplIdx, tpl.templateId)" @click="() => reloadTemplateDetail(tplIdx)">
|
|
|
|
<el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" />
|
|
|
|
</el-select>
|
|
|
|
<div style="display: flex; gap: 8px; flex-shrink: 0;">
|
|
|
|
<el-button @click="deltemplate(templatesIndex)"
|
|
|
|
:disabled="SonFormData.templateIds.length <= 1">删除</el-button>
|
|
|
|
<el-button @click="Addtemplate(templatesIndex)">新增</el-button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :prop="`templateIds[${templatesIndex}].sortOrder`" label="排序" label-suffix="*"
|
|
|
|
class="is-required">
|
|
|
|
<el-input-number v-model="templates.sortOrder" :min="1" :max="99" placeholder="排序号" style="width: 100%" />
|
|
|
|
<el-form-item :prop="`templateIds[${tplIdx}].sortOrder`" label="排序">
|
|
|
|
<el-input-number v-model="tpl.sortOrder" :min="1" :max="99" placeholder="排序号" class="w-full" />
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<!-- 模板详情 -->
|
|
|
|
<!-- 模板详情回显(新增部分) -->
|
|
|
|
<el-form-item label="模板详情" v-if="templateDetailMap[templatesIndex]"
|
|
|
|
style="width: 100%; padding: 0; margin: 0 auto;" class="template-detail-container">
|
|
|
|
<el-form-item v-if="templateDetailMap[tplIdx]" label="模板详情" class="template-detail-container">
|
|
|
|
<el-collapse-transition>
|
|
|
|
<div v-loading="templateLoading[templatesIndex]">
|
|
|
|
<!-- 训练单元列表 -->
|
|
|
|
<el-table :data="templateDetailMap[templatesIndex].units || []" border
|
|
|
|
style="width: 100%; margin-bottom: 12px;" :show-header="true" class="expand-table-fix">
|
|
|
|
<el-table-column prop="unitName" label="训练单元名称" min-width="150" />
|
|
|
|
<el-table-column prop="sortOrder" label="排序" width="100" align="center" />
|
|
|
|
<!-- <el-table-column prop="setCount" label="组数" width="80" align="center" /> -->
|
|
|
|
</el-table>
|
|
|
|
<!-- 用纯DIV替代嵌套表格,彻底解决宽度问题 -->
|
|
|
|
<div v-for="unit in templateDetailMap[templatesIndex].units || []" :key="unit.unitId"
|
|
|
|
class="unit-container">
|
|
|
|
<!-- 训练单元行(可点击展开) -->
|
|
|
|
<div v-loading="templateLoading[tplIdx]">
|
|
|
|
<div v-for="unit in templateDetailMap[tplIdx].units || []" :key="unit.unitId" class="unit-container">
|
|
|
|
<div class="unit-row" @click="unit.isExpand = !unit.isExpand">
|
|
|
|
<span class="expand-icon">{{ unit.isExpand ? '▼' : '▶' }}</span>
|
|
|
|
<Icon icon="ep:arrow-right" class="expand-icon" :class="{ 'is-expanded': unit.isExpand }" />
|
|
|
|
<span class="unit-name">{{ unit.unitName }}</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 展开的动作列表 -->
|
|
|
|
<div v-if="unit.isExpand" class="exercise-container">
|
|
|
|
<div v-for="exercise in unit.exercises || []" :key="exercise.exerciseId" class="exercise-row">
|
|
|
|
<div class="exercise-header">
|
|
|
|
<!-- <span class="exercise-name">{{ exercise.exerciseName }}</span> -->
|
|
|
|
<!-- <span class="exercise-info">组数:{{ exercise.setCount }} | 重量:{{ exercise.weight }} | 次数:{{
|
|
|
|
exercise.reps }}</span> -->
|
|
|
|
<!-- <span class="exercise-expand" @click="exercise.isExpand = !exercise.isExpand">
|
|
|
|
{{ exercise.isExpand ? '▼ 收起组详情' : '▶ 展开详情' }}
|
|
|
|
</span> -->
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 展开的组详情 -->
|
|
|
|
<div class="set-container">
|
|
|
|
<div v-for="exercise in unit.exercises || []" :key="exercise.exerciseId" class="exercise-item">
|
|
|
|
<div class="set-header">
|
|
|
|
<span>重量</span>
|
|
|
|
<span>距离</span>
|
|
...
|
...
|
@@ -129,23 +94,18 @@ |
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- -->
|
|
|
|
</div>
|
|
|
|
</el-collapse-transition>
|
|
|
|
</el-form-item>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- <el-form-item label="所需器械汇总" prop="equipmentsSummary" label-suffix="*">
|
|
|
|
<el-input v-model="SonFormData.equipmentsSummary" />
|
|
|
|
</el-form-item> -->
|
|
|
|
<el-form-item label="计划介绍" prop="introduction" label-suffix="*">
|
|
|
|
<el-form-item label="计划介绍" prop="introduction">
|
|
|
|
<el-input v-model="SonFormData.introduction" type="textarea" :rows="3" />
|
|
|
|
</el-form-item>
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
<el-button @click="handleCancel">取消</el-button>
|
|
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button>
|
|
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">确 定</el-button>
|
|
|
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
|
|
|
</template>
|
|
|
|
</Dialog>
|
|
|
|
</template>
|
|
...
|
...
|
@@ -155,16 +115,13 @@ import { ref, reactive, watch, onMounted, computed } from 'vue' |
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
import { TrainingTemplatesApi } from '@/api/store/training/templates'
|
|
|
|
import { trainingPlanApi } from '@/api/store/training/trainingPlan'
|
|
|
|
import { Minus, Plus } from '@element-plus/icons-vue'
|
|
|
|
import { Dialog } from '@/components/Dialog'
|
|
|
|
// import { Table } from '@/components/Table'
|
|
|
|
// 父组件传过来的值(只用来接收,不冲突)
|
|
|
|
|
|
|
|
// ==================== Props & Emits ====================
|
|
|
|
const props = defineProps({
|
|
|
|
visible: { type: Boolean, default: false },
|
|
|
|
title: String,
|
|
|
|
formData: { type: Object, default: null }
|
|
|
|
})
|
|
|
|
|
|
|
|
const emit = defineEmits(['submit', 'update:visible'])
|
|
|
|
|
|
|
|
const dialogVisible = computed({
|
|
...
|
...
|
@@ -172,78 +129,71 @@ const dialogVisible = computed({ |
|
|
|
set: (val) => emit('update:visible', val)
|
|
|
|
})
|
|
|
|
|
|
|
|
// ========================
|
|
|
|
// 子组件自己的表单数据(唯一来源)
|
|
|
|
// ========================
|
|
|
|
const SonFormData = ref({
|
|
|
|
id: undefined,
|
|
|
|
categoryId: 0,
|
|
|
|
name: '',
|
|
|
|
urlCover: '',
|
|
|
|
introduction: '',
|
|
|
|
durationWeeks: 1,
|
|
|
|
frequencyPerWeek: 1,
|
|
|
|
difficultyLevel: 1,
|
|
|
|
targetPeople: 1,
|
|
|
|
trainingScene: 1,
|
|
|
|
equipmentsSummary: '',
|
|
|
|
// ==================== 表单默认值(仅保留渲染所必需的字段) ====================
|
|
|
|
const defaultFormData = () => ({
|
|
|
|
templateIds: [{ templateId: undefined, sortOrder: 1 }]
|
|
|
|
})
|
|
|
|
|
|
|
|
// 是否是编辑功能
|
|
|
|
// 新增:专门的编辑状态标记
|
|
|
|
// ==================== 状态 ====================
|
|
|
|
const SonFormData = ref(defaultFormData())
|
|
|
|
const isEdit = ref(false)
|
|
|
|
const formRef = ref()
|
|
|
|
const loading = ref(false)
|
|
|
|
const templateList = ref([])
|
|
|
|
const categoryList = ref([]) // 计划分类列表
|
|
|
|
// 存储每个模板的详情数据,key 是模板 index,value 是接口返回的详情
|
|
|
|
const categoryList = ref([])
|
|
|
|
const templateDetailMap = ref({})
|
|
|
|
// 模板详情加载状态
|
|
|
|
const templateLoading = ref({})
|
|
|
|
|
|
|
|
// 控制练的频率
|
|
|
|
// ==================== 计算属性 ====================
|
|
|
|
const templateCount = computed(() => SonFormData.value.templateIds.length)
|
|
|
|
// 频率选项:1个模板显示全部,多个模板只显示对应数量
|
|
|
|
|
|
|
|
const frequencyOptions = computed(() => {
|
|
|
|
const count = templateCount.value
|
|
|
|
if (count === 1) {
|
|
|
|
return [
|
|
|
|
{ label: '1练/周', value: 1 },
|
|
|
|
{ label: '2练/周', value: 2 },
|
|
|
|
{ label: '3练/周', value: 3 },
|
|
|
|
{ label: '4练/周', value: 4 },
|
|
|
|
{ label: '5练/周', value: 5 },
|
|
|
|
{ label: '6练/周', value: 6 },
|
|
|
|
]
|
|
|
|
} else {
|
|
|
|
// 多个模板 → 只显示和模板数量一样的选项
|
|
|
|
return [{ label: `${count}练/周`, value: count }]
|
|
|
|
if (templateCount.value > 1) {
|
|
|
|
return [{ label: templateCount.value + '练/周', value: templateCount.value }]
|
|
|
|
}
|
|
|
|
return [1, 2, 3, 4, 5, 6].map((n) => ({ label: n + '练/周', value: n }))
|
|
|
|
})
|
|
|
|
|
|
|
|
// ==================== 静态下拉选项 ====================
|
|
|
|
const trainingSceneOptions = [
|
|
|
|
{ label: '不限', value: 1 },
|
|
|
|
{ label: '健身房', value: 2 },
|
|
|
|
{ label: '仅哑铃', value: 3 },
|
|
|
|
{ label: '仅哑铃+杠铃', value: 4 }
|
|
|
|
]
|
|
|
|
|
|
|
|
const difficultyLevelOptions = [
|
|
|
|
{ label: '初阶', value: 1 },
|
|
|
|
{ label: '中阶', value: 2 },
|
|
|
|
{ label: '高阶', value: 3 }
|
|
|
|
]
|
|
|
|
|
|
|
|
const targetPeopleOptions = [
|
|
|
|
{ label: '不限', value: 1 },
|
|
|
|
{ label: '青少年', value: 2 },
|
|
|
|
{ label: '成年人', value: 3 },
|
|
|
|
{ label: '中年人', value: 4 },
|
|
|
|
{ label: '运动损伤', value: 5 }
|
|
|
|
]
|
|
|
|
|
|
|
|
// ==================== 频率同步 ====================
|
|
|
|
const syncFrequency = () => {
|
|
|
|
const count = templateCount.value
|
|
|
|
if (count > 1) {
|
|
|
|
// 超过1个模板 → 强制频率 = 模板数量
|
|
|
|
SonFormData.value.frequencyPerWeek = count
|
|
|
|
if (templateCount.value > 1) {
|
|
|
|
SonFormData.value.frequencyPerWeek = templateCount.value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 监听模板数量变化,自动更新频率
|
|
|
|
watch(templateCount, () => {
|
|
|
|
syncFrequency()
|
|
|
|
})
|
|
|
|
watch(templateCount, syncFrequency)
|
|
|
|
|
|
|
|
// ==================== 表单验证规则 ====================
|
|
|
|
const formRules = reactive({
|
|
|
|
name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
|
|
|
|
durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }],
|
|
|
|
frequencyPerWeek: [{ required: true, message: '请输入训练频率', trigger: 'change' }],
|
|
|
|
difficultyLevel: [{ required: true, message: '请输入难度', trigger: 'change' }],
|
|
|
|
targetPeople: [{ required: true, message: '请输入人群', trigger: 'change' }],
|
|
|
|
trainingScene: [{ required: true, message: '请输入场景', trigger: 'change' }],
|
|
|
|
urlCover: [{ required: true, message: '输入图片', trigger: 'change' }],
|
|
|
|
// equipmentsSummary: [{ required: true, message: '请输入所需器械汇总', trigger: 'blur' }],
|
|
|
|
difficultyLevel: [{ required: true, message: '请选择难度', trigger: 'change' }],
|
|
|
|
targetPeople: [{ required: true, message: '请选择人群', trigger: 'change' }],
|
|
|
|
trainingScene: [{ required: true, message: '请选择场景', trigger: 'change' }],
|
|
|
|
urlCover: [{ required: true, message: '请上传封面图', trigger: 'change' }],
|
|
|
|
introduction: [{ required: true, message: '请输入计划介绍', trigger: 'blur' }],
|
|
|
|
categoryId: [{ required: true, message: '请选择计划分类', trigger: 'change' }],
|
|
|
|
templateIds: [{
|
|
...
|
...
|
@@ -254,93 +204,74 @@ const formRules = reactive({ |
|
|
|
return callback(new Error('至少添加一个模板'))
|
|
|
|
}
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
|
|
const item = value[i]
|
|
|
|
if (!item.templateId) {
|
|
|
|
return callback(new Error(`第 ${i + 1} 个模板未选择`))
|
|
|
|
if (!value[i].templateId) {
|
|
|
|
return callback(new Error('第 ' + (i + 1) + ' 个模板未选择'))
|
|
|
|
}
|
|
|
|
if (!item.sortOrder || item.sortOrder < 1) {
|
|
|
|
return callback(new Error(`第 ${i + 1} 个排序号必须≥1`))
|
|
|
|
if (!value[i].sortOrder || value[i].sortOrder < 1) {
|
|
|
|
return callback(new Error('第 ' + (i + 1) + ' 个排序号必须≥1'))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
}],
|
|
|
|
}]
|
|
|
|
})
|
|
|
|
|
|
|
|
watch(dialogVisible, async (val) => {
|
|
|
|
if (!val) return
|
|
|
|
// 重置模板详情缓存,解决旧数据残留问题
|
|
|
|
templateDetailMap.value = {}
|
|
|
|
templateLoading.value = {}
|
|
|
|
SonFormData.value = {
|
|
|
|
id: undefined,
|
|
|
|
categoryId: 0,
|
|
|
|
name: '',
|
|
|
|
urlCover: '',
|
|
|
|
introduction: '',
|
|
|
|
durationWeeks: 1,
|
|
|
|
frequencyPerWeek: 1,
|
|
|
|
difficultyLevel: 1,
|
|
|
|
targetPeople: 1,
|
|
|
|
trainingScene: 1,
|
|
|
|
equipmentsSummary: '',
|
|
|
|
templateIds: [{ templateId: undefined, sortOrder: 1 }]
|
|
|
|
}
|
|
|
|
// 新增
|
|
|
|
if (!props.formData || Object.keys(props.formData).length === 0 || props.formData.id == null) {
|
|
|
|
// 控制使用哪个接口
|
|
|
|
isEdit.value = false // 强制写死
|
|
|
|
// 新增:重置清除前面编辑可能留下的校验
|
|
|
|
// ==================== 弹窗打开时初始化 ====================
|
|
|
|
const initAdd = () => {
|
|
|
|
isEdit.value = false
|
|
|
|
SonFormData.value = defaultFormData()
|
|
|
|
formRef.value?.clearValidate()
|
|
|
|
console.log('新增接口isEdit的值', isEdit)
|
|
|
|
} else {
|
|
|
|
}
|
|
|
|
|
|
|
|
const initEdit = async () => {
|
|
|
|
isEdit.value = true
|
|
|
|
// 编辑:赋值
|
|
|
|
// 调计划详情接口
|
|
|
|
const res = await trainingPlanApi.getTrainingPlanDetail(props.formData.id)
|
|
|
|
console.log('计划详情数据res================', res)
|
|
|
|
console.log('计划详情数据res.data================', res.data)
|
|
|
|
console.log('计划详情数据res.equipmentsSummary================', res.equipmentsSummary)
|
|
|
|
if (!res || !res.id) {
|
|
|
|
ElMessage.error('获取计划详情失败')
|
|
|
|
console.log('获取计划详情失败')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
SonFormData.value = res
|
|
|
|
// 2. 补 res 里缺失、但父组件有值的字段
|
|
|
|
// 补全父组件传入但详情未返回的字段
|
|
|
|
SonFormData.value.trainingScene = props.formData.trainingScene
|
|
|
|
SonFormData.value.targetPeople = props.formData.targetPeople
|
|
|
|
// 详情接口返回的字段和SonFormData的不一样,需要处理
|
|
|
|
SonFormData.value.categoryId = props.formData.categoryId
|
|
|
|
console.log("===== 父组件传过来的完整数据 阉割版=====")
|
|
|
|
console.log(props.formData)
|
|
|
|
// // 详情返回的是数组,但是这里要的是字符串
|
|
|
|
// 处理 equipmentsSummary 数组转字符串
|
|
|
|
if (Array.isArray(res.equipmentsSummary)) {
|
|
|
|
SonFormData.value.equipmentsSummary = res.equipmentsSummary.join(',')
|
|
|
|
}
|
|
|
|
// 赋值模板的id和排序
|
|
|
|
// 映射模板 ID 和排序
|
|
|
|
if (res.templates && res.templates.length) {
|
|
|
|
SonFormData.value.templateIds = res.templates.map(item => ({
|
|
|
|
SonFormData.value.templateIds = res.templates.map((item) => ({
|
|
|
|
templateId: item.id,
|
|
|
|
sortOrder: item.sortOrder
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
// 防止模板为空,导致模板选项不出现(虽然不太可能,因为这个是必填项)
|
|
|
|
if (!SonFormData.value.templateIds || SonFormData.value.templateIds.length === 0) {
|
|
|
|
SonFormData.value.templateIds = [{ templateId: undefined, sortOrder: 1 }]
|
|
|
|
}
|
|
|
|
// // 编辑状态:自动加载已有模板的详情
|
|
|
|
SonFormData.value.templateIds.forEach((item, index) => {
|
|
|
|
if (item.templateId) {
|
|
|
|
loadTemplateDetail(index, item.templateId)
|
|
|
|
}
|
|
|
|
// 自动加载已有模板的详情
|
|
|
|
SonFormData.value.templateIds.forEach((item, idx) => {
|
|
|
|
if (item.templateId) loadTemplateDetail(idx, item.templateId)
|
|
|
|
})
|
|
|
|
syncFrequency()
|
|
|
|
formRef.value?.clearValidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
watch(dialogVisible, async (val) => {
|
|
|
|
if (!val) return
|
|
|
|
templateDetailMap.value = {}
|
|
|
|
templateLoading.value = {}
|
|
|
|
SonFormData.value = defaultFormData()
|
|
|
|
|
|
|
|
if (!props.formData || Object.keys(props.formData).length === 0 || props.formData.id == null) {
|
|
|
|
initAdd()
|
|
|
|
} else {
|
|
|
|
await initEdit()
|
|
|
|
}
|
|
|
|
formRef.value?.clearValidate()
|
|
|
|
})
|
|
|
|
// ==========================加载数据======================
|
|
|
|
// 加载模板列表
|
|
|
|
|
|
|
|
// ==================== 数据加载 ====================
|
|
|
|
const loadTemplateList = async () => {
|
|
|
|
const res = await TrainingTemplatesApi.getTrainingTemplatesPage({ pageNo: 1, pageSize: 100 })
|
|
|
|
templateList.value = res.list || []
|
|
...
|
...
|
@@ -348,49 +279,72 @@ const loadTemplateList = async () => { |
|
|
|
ElMessage.warning('暂无可用训练模板,请先创建模板')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 获取模板详情并缓存
|
|
|
|
|
|
|
|
const loadTemplateDetail = async (index, templateId) => {
|
|
|
|
if (!templateId) return
|
|
|
|
// 标记加载中
|
|
|
|
templateLoading.value[index] = true
|
|
|
|
try {
|
|
|
|
const res = await TrainingTemplatesApi.getTrainingTemplates(templateId)
|
|
|
|
// 给每个单元、动作加展开状态(默认收起)
|
|
|
|
res.units?.forEach(unit => {
|
|
|
|
res.units?.forEach((unit) => {
|
|
|
|
unit.isExpand = false
|
|
|
|
unit.exercises?.forEach(exercise => {
|
|
|
|
exercise.isExpand = false
|
|
|
|
unit.exercises?.forEach((ex) => { ex.isExpand = false })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
// 缓存详情数据
|
|
|
|
templateDetailMap.value[index] = res
|
|
|
|
} catch (err) {
|
|
|
|
ElMessage.error(`获取模板${index + 1}详情失败`)
|
|
|
|
console.error(err)
|
|
|
|
ElMessage.error('获取模板' + (index + 1) + '详情失败')
|
|
|
|
} finally {
|
|
|
|
// 取消加载中
|
|
|
|
templateLoading.value[index] = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 加载计划分类列表
|
|
|
|
const loadCategoryList = async () => {
|
|
|
|
const res = await trainingPlanApi.getPlanCategoryPage({ pageNo: 1, pageSize: 100 })
|
|
|
|
categoryList.value = res.list || []
|
|
|
|
const res = await trainingPlanApi.getPlanList()
|
|
|
|
categoryList.value = res || []
|
|
|
|
}
|
|
|
|
// 强制重新加载当前 index 的模板详情(解决重复选择不触发问题)
|
|
|
|
|
|
|
|
const reloadTemplateDetail = (index) => {
|
|
|
|
const id = SonFormData.value.templateIds[index]?.templateId
|
|
|
|
if (id) loadTemplateDetail(index, id)
|
|
|
|
}
|
|
|
|
// 取消
|
|
|
|
const handleCancel = () => {
|
|
|
|
dialogVisible.value = false
|
|
|
|
ElMessage.info('取消保存')
|
|
|
|
|
|
|
|
// ==================== 模板增删 ====================
|
|
|
|
const addTemplate = (index) => {
|
|
|
|
const newIds = [...SonFormData.value.templateIds]
|
|
|
|
newIds.splice(index + 1, 0, {
|
|
|
|
templateId: undefined,
|
|
|
|
sortOrder: SonFormData.value.templateIds.length + 1
|
|
|
|
})
|
|
|
|
SonFormData.value.templateIds = newIds
|
|
|
|
reindexDetailMap()
|
|
|
|
syncFrequency()
|
|
|
|
}
|
|
|
|
|
|
|
|
const removeTemplate = (index) => {
|
|
|
|
if (SonFormData.value.templateIds.length <= 1) return
|
|
|
|
SonFormData.value.templateIds = SonFormData.value.templateIds.filter((_, i) => i !== index)
|
|
|
|
reindexDetailMap()
|
|
|
|
syncFrequency()
|
|
|
|
}
|
|
|
|
// 提交
|
|
|
|
|
|
|
|
const reindexDetailMap = () => {
|
|
|
|
const oldMap = { ...templateDetailMap.value }
|
|
|
|
templateDetailMap.value = {}
|
|
|
|
templateLoading.value = {}
|
|
|
|
SonFormData.value.templateIds.forEach((item, i) => {
|
|
|
|
if (item.templateId && oldMap[i] !== undefined) {
|
|
|
|
templateDetailMap.value[i] = oldMap[i]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// ==================== 提交 ====================
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
if (!formRef.value) return
|
|
|
|
try {
|
|
|
|
await formRef.value.validate()
|
|
|
|
} catch {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
loading.value = true
|
|
|
|
try {
|
|
|
|
if (isEdit.value) {
|
|
...
|
...
|
@@ -398,122 +352,112 @@ const handleSubmit = async () => { |
|
|
|
} else {
|
|
|
|
await trainingPlanApi.createTrainingPlan(SonFormData.value)
|
|
|
|
}
|
|
|
|
console.log('调用接口后isEdit的值', isEdit)
|
|
|
|
ElMessage.success(isEdit.value ? '编辑成功' : '创建成功')
|
|
|
|
dialogVisible.value = false
|
|
|
|
emit('submit')
|
|
|
|
} catch (err) {
|
|
|
|
ElMessage.error((isEdit.value ? '编辑' : '创建') + '失败:' + err.message)
|
|
|
|
ElMessage.error((isEdit.value ? '编辑' : '创建') + '失败')
|
|
|
|
} finally {
|
|
|
|
loading.value = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const Addtemplate = (index) => {
|
|
|
|
const newTemplate = {
|
|
|
|
templateId: 1,
|
|
|
|
sortOrder: SonFormData.value.templateIds.length + 1
|
|
|
|
}
|
|
|
|
const newTemplateIds = [...SonFormData.value.templateIds]
|
|
|
|
newTemplateIds.splice(index + 1, 0, newTemplate)
|
|
|
|
SonFormData.value.templateIds = newTemplateIds
|
|
|
|
// 重新同步所有模板详情(不移除、不清空,只重新对应位置)
|
|
|
|
const oldMap = { ...templateDetailMap.value }
|
|
|
|
templateDetailMap.value = {}
|
|
|
|
templateLoading.value = {}
|
|
|
|
SonFormData.value.templateIds.forEach((item, i) => {
|
|
|
|
if (item.templateId && oldMap[i] !== undefined) {
|
|
|
|
templateDetailMap.value[i] = oldMap[i]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
syncFrequency()
|
|
|
|
}
|
|
|
|
|
|
|
|
const deltemplate = (index) => {
|
|
|
|
if (SonFormData.value.templateIds.length <= 1) return
|
|
|
|
SonFormData.value.templateIds = SonFormData.value.templateIds.filter((_, i) => i !== index)
|
|
|
|
// 重新同步所有模板详情(不移除、不清空,只重新对应位置)
|
|
|
|
const oldMap = { ...templateDetailMap.value }
|
|
|
|
templateDetailMap.value = {}
|
|
|
|
templateLoading.value = {}
|
|
|
|
SonFormData.value.templateIds.forEach((item, i) => {
|
|
|
|
if (item.templateId && oldMap[i] !== undefined) {
|
|
|
|
templateDetailMap.value[i] = oldMap[i]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
syncFrequency()
|
|
|
|
}
|
|
|
|
// ==================== 初始化 ====================
|
|
|
|
onMounted(() => {
|
|
|
|
loadTemplateList()
|
|
|
|
loadCategoryList()
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
/* 模板详情容器 */
|
|
|
|
/* ==================== 工具类 ==================== */
|
|
|
|
.w-full {
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ==================== 模板卡片 ==================== */
|
|
|
|
.template-block {
|
|
|
|
margin-bottom: 16px;
|
|
|
|
padding: 16px 20px 4px;
|
|
|
|
background: var(--el-fill-color-lighter);
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
border-radius: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.template-block-toolbar {
|
|
|
|
display: flex;
|
|
|
|
justify-content: flex-end;
|
|
|
|
gap: 10px;
|
|
|
|
margin-bottom: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ==================== 模板详情 ==================== */
|
|
|
|
.template-detail-container {
|
|
|
|
width: 100%;
|
|
|
|
padding: 12px;
|
|
|
|
background: #f9fafb;
|
|
|
|
background: var(--el-fill-color-lighter);
|
|
|
|
border-radius: 6px;
|
|
|
|
box-sizing: border-box;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 训练单元容器 */
|
|
|
|
/* ==================== 训练单元 ==================== */
|
|
|
|
.unit-container {
|
|
|
|
width: 100%;
|
|
|
|
margin-bottom: 8px;
|
|
|
|
background: #fff;
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
background: var(--el-bg-color);
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
border-radius: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 训练单元行(点击展开) */
|
|
|
|
.unit-row {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
padding: 12px 16px;
|
|
|
|
font-size: 14px;
|
|
|
|
cursor: pointer;
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
align-items: center;
|
|
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
|
}
|
|
|
|
|
|
|
|
.unit-row:hover {
|
|
|
|
background: #f5f7fa;
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
}
|
|
|
|
|
|
|
|
.expand-icon {
|
|
|
|
margin-right: 8px;
|
|
|
|
color: #909399;
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
transition: transform 0.3s;
|
|
|
|
}
|
|
|
|
|
|
|
|
.unit-name {
|
|
|
|
font-weight: 500;
|
|
|
|
color: #303133;
|
|
|
|
.expand-icon.is-expanded {
|
|
|
|
transform: rotate(90deg);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 组详情容器 */
|
|
|
|
.set-container {
|
|
|
|
padding: 12px;
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
.unit-name {
|
|
|
|
font-weight: 500;
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
}
|
|
|
|
|
|
|
|
.set-header {
|
|
|
|
/* ==================== 训练组详情 ==================== */
|
|
|
|
.set-header,
|
|
|
|
.set-row {
|
|
|
|
display: grid;
|
|
|
|
padding: 8px 12px;
|
|
|
|
font-weight: 500;
|
|
|
|
color: #606266;
|
|
|
|
text-align: center;
|
|
|
|
background: #f5f7fa;
|
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
|
}
|
|
|
|
|
|
|
|
.set-header {
|
|
|
|
font-weight: 500;
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
}
|
|
|
|
|
|
|
|
.set-row {
|
|
|
|
display: grid;
|
|
|
|
padding: 8px 12px;
|
|
|
|
color: #303133;
|
|
|
|
text-align: center;
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
|
|
}
|
|
|
|
|
|
|
|
.exercise-container {
|
|
|
|
padding: 4px;
|
|
|
|
}
|
|
|
|
</style> |
...
|
...
|
|