feat: 重构训练记录与超级组页面并更新API参数
Showing
19 changed files
with
1350 additions
and
1646 deletions
| @@ -4,7 +4,7 @@ NODE_ENV=production | @@ -4,7 +4,7 @@ NODE_ENV=production | ||
| 4 | VITE_DEV=true | 4 | VITE_DEV=true |
| 5 | 5 | ||
| 6 | # 请求路径 | 6 | # 请求路径 |
| 7 | -VITE_BASE_URL='https://fitness.hcxtec.com' | 7 | +VITE_BASE_URL='https://xunji.admin.geaktec.com' |
| 8 | 8 | ||
| 9 | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 | 9 | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 |
| 10 | VITE_UPLOAD_TYPE=server | 10 | VITE_UPLOAD_TYPE=server |
| @@ -62,7 +62,7 @@ | @@ -62,7 +62,7 @@ | ||
| 62 | "editor.defaultFormatter": "esbenp.prettier-vscode" | 62 | "editor.defaultFormatter": "esbenp.prettier-vscode" |
| 63 | }, | 63 | }, |
| 64 | "[typescript]": { | 64 | "[typescript]": { |
| 65 | - "editor.defaultFormatter": "esbenp.prettier-vscode" | 65 | + "editor.defaultFormatter": "vscode.typescript-language-features" |
| 66 | }, | 66 | }, |
| 67 | "[typescriptreact]": { | 67 | "[typescriptreact]": { |
| 68 | "editor.defaultFormatter": "esbenp.prettier-vscode" | 68 | "editor.defaultFormatter": "esbenp.prettier-vscode" |
| 1 | import request from '@/config/axios' | 1 | import request from '@/config/axios' |
| 2 | 2 | ||
| 3 | -/** 前台用户信息 */ | ||
| 4 | -export interface Student { | ||
| 5 | - id: number // ID | ||
| 6 | - phone?: string // 手机号 | ||
| 7 | - identity?: string // 身份:1=学员、2=教练 | ||
| 8 | - nickname?: string // 昵称 | ||
| 9 | - avatar?: string // 头像 | ||
| 10 | - sex?: string // 性别:1=男、2=女 | ||
| 11 | - province?: string // 省 | ||
| 12 | - city?: string // 市 | ||
| 13 | - region?: string // 区 | ||
| 14 | - signature?: string // 个性签名(自我介绍) | ||
| 15 | - backgroundImage?: string // 背景图片 | ||
| 16 | - openId?: string // 微信openId | ||
| 17 | - isNewbie?: number // 是否是新人:1是0否 | 3 | +// 获得前台用户分页 |
| 4 | +export const getStudentPage = (params) => { | ||
| 5 | + return request.get({ | ||
| 6 | + url: '/user/student/page', | ||
| 7 | + | ||
| 8 | + params | ||
| 9 | + }) | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +// 获得用户详细 | ||
| 13 | +export const getStudent = (id) => { | ||
| 14 | + return request.get({ | ||
| 15 | + url: '/user/student/get', | ||
| 16 | + | ||
| 17 | + params: { id } | ||
| 18 | + }) | ||
| 18 | } | 19 | } |
| 19 | 20 | ||
| 20 | -// 前台用户 API | ||
| 21 | -export const StudentApi = { | ||
| 22 | - // 查询前台用户分页 | ||
| 23 | - getUserPage: async (params: any) => { | ||
| 24 | - return await request.get({ url: `/user/student/page`, params }) | ||
| 25 | - }, | ||
| 26 | - | ||
| 27 | - // 查询前台用户详情 | ||
| 28 | - getUser: async (id: number) => { | ||
| 29 | - return await request.get({ url: `/user/student/get?id=` + id }) | ||
| 30 | - }, | ||
| 31 | - | ||
| 32 | - // 修改前台用户 | ||
| 33 | - updateUser: async (data: Student) => { | ||
| 34 | - return await request.put({ url: `/user/student/update`, data }) | ||
| 35 | - }, | ||
| 36 | - | ||
| 37 | - // 导出前台用户 Excel | ||
| 38 | - exportUser: async (params) => { | ||
| 39 | - return await request.download({ url: `/user/student/export`, params }) | ||
| 40 | - }, | ||
| 41 | - | ||
| 42 | - // 获得学员健康资料 | ||
| 43 | - getUserHealth: async (id: number) => { | ||
| 44 | - return await request.get({ url: `/user/student/health-data?userId=` + id }) | ||
| 45 | - } | 21 | +// 更新用户 |
| 22 | +export const updateStudent = (data) => { | ||
| 23 | + return request.put({ | ||
| 24 | + url: '/user/student/update', | ||
| 25 | + | ||
| 26 | + data | ||
| 27 | + }) | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +// 获得学员健康资料 | ||
| 31 | +export const getStudentHealthData = (userId) => { | ||
| 32 | + return request.get({ | ||
| 33 | + url: '/user/student/health-data', | ||
| 34 | + | ||
| 35 | + params: { userId } | ||
| 36 | + }) | ||
| 46 | } | 37 | } |
| @@ -48,7 +48,7 @@ export const SupersetsApi = { | @@ -48,7 +48,7 @@ export const SupersetsApi = { | ||
| 48 | }, | 48 | }, |
| 49 | 49 | ||
| 50 | // 获得超级组列表 | 50 | // 获得超级组列表 |
| 51 | - getSupersetList: () => { | ||
| 52 | - return request.get({ url: '/motion/supersets/supersets-simple-list' }) | 51 | + getSupersetList: (isPersonal: boolean) => { |
| 52 | + return request.get({ url: '/motion/supersets/supersets-simple-list?isPersonal=' + isPersonal}) | ||
| 53 | } | 53 | } |
| 54 | } | 54 | } |
| @@ -87,7 +87,7 @@ export const ExercisesApi = { | @@ -87,7 +87,7 @@ export const ExercisesApi = { | ||
| 87 | }) | 87 | }) |
| 88 | }, | 88 | }, |
| 89 | // 获取动作分类下拉列表 | 89 | // 获取动作分类下拉列表 |
| 90 | - getExercisesList: () => { | ||
| 91 | - return request.get({ url: '/motion/supersets/simple-list' }) | 90 | + getExercisesList: (isPersonal: boolean) => { |
| 91 | + return request.get({ url: '/motion/supersets/simple-list?isPersonal=' + isPersonal }) | ||
| 92 | } | 92 | } |
| 93 | } | 93 | } |
| @@ -52,7 +52,7 @@ export const TrainingTemplatesApi = { | @@ -52,7 +52,7 @@ export const TrainingTemplatesApi = { | ||
| 52 | }) | 52 | }) |
| 53 | }, | 53 | }, |
| 54 | // 获取模板下拉列表 | 54 | // 获取模板下拉列表 |
| 55 | - getTemplateList: () => { | ||
| 56 | - return request.get({ url: '/templates/simple-list' }) | 55 | + getTemplateList: (isPersonal: boolean) => { |
| 56 | + return request.get({ url: '/templates/simple-list?isPersonal=' + isPersonal}) | ||
| 57 | } | 57 | } |
| 58 | } | 58 | } |
| @@ -50,5 +50,11 @@ export const trainingPlanApi = { | @@ -50,5 +50,11 @@ export const trainingPlanApi = { | ||
| 50 | // 获得计划中的计划种类分页 | 50 | // 获得计划中的计划种类分页 |
| 51 | getPlanCategoryPage: async (params: any) => { | 51 | getPlanCategoryPage: async (params: any) => { |
| 52 | return await request.get({ url: `/plan/Category/page`, params }) | 52 | return await request.get({ url: `/plan/Category/page`, params }) |
| 53 | + }, | ||
| 54 | + // 获取计划下拉列表 | ||
| 55 | + getPlanList: async () => { | ||
| 56 | + return await request.get({ url: `/plan/simple-list`, }) | ||
| 53 | } | 57 | } |
| 58 | + | ||
| 59 | + | ||
| 54 | } | 60 | } |
src/views/store/student/StudentForm.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <Dialog title="修改前台用户" v-model="dialogVisible" width="600px"> | ||
| 3 | + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px" v-loading="formLoading"> | ||
| 4 | + <el-form-item label="真实姓名" prop="realname"> | ||
| 5 | + <el-input v-model="formData.realname" placeholder="请输入真实姓名" /> | ||
| 6 | + </el-form-item> | ||
| 7 | + <el-form-item label="昵称" prop="nickname"> | ||
| 8 | + <el-input v-model="formData.nickname" placeholder="请输入昵称" /> | ||
| 9 | + </el-form-item> | ||
| 10 | + <el-form-item label="手机号" prop="phone"> | ||
| 11 | + <el-input v-model="formData.phone" placeholder="请输入手机号" /> | ||
| 12 | + </el-form-item> | ||
| 13 | + <el-form-item label="性别" prop="sex"> | ||
| 14 | + <el-radio-group v-model="formData.sex"> | ||
| 15 | + <el-radio :label="1">男</el-radio> | ||
| 16 | + <el-radio :label="2">女</el-radio> | ||
| 17 | + </el-radio-group> | ||
| 18 | + </el-form-item> | ||
| 19 | + <el-form-item label="账号状态" prop="status"> | ||
| 20 | + <el-radio-group v-model="formData.status"> | ||
| 21 | + <el-radio :label="1">正常</el-radio> | ||
| 22 | + <el-radio :label="0">禁用</el-radio> | ||
| 23 | + </el-radio-group> | ||
| 24 | + </el-form-item> | ||
| 25 | + <el-form-item label="头像" prop="avatar"> | ||
| 26 | + <UploadImg v-model="formData.avatar" /> | ||
| 27 | + </el-form-item> | ||
| 28 | + <el-form-item label="个性签名" prop="signature"> | ||
| 29 | + <el-input v-model="formData.signature" type="textarea" :rows="2" placeholder="请输入个性签名" /> | ||
| 30 | + </el-form-item> | ||
| 31 | + <el-form-item label="背景图片" prop="backgroundImage"> | ||
| 32 | + <UploadImg v-model="formData.backgroundImage" /> | ||
| 33 | + </el-form-item> | ||
| 34 | + </el-form> | ||
| 35 | + <template #footer> | ||
| 36 | + <el-button @click="dialogVisible = false">取 消</el-button> | ||
| 37 | + <el-button type="primary" :loading="submitLoading" @click="submitForm">确 定</el-button> | ||
| 38 | + </template> | ||
| 39 | + </Dialog> | ||
| 40 | +</template> | ||
| 41 | + | ||
| 42 | +<script setup> | ||
| 43 | +import { ref, reactive } from 'vue' | ||
| 44 | +import * as StudentApi from '@/api/store/student' | ||
| 45 | + | ||
| 46 | +const message = useMessage() // 芋道全局提示组件 | ||
| 47 | + | ||
| 48 | +const dialogVisible = ref(false) | ||
| 49 | +const formLoading = ref(false) | ||
| 50 | +const submitLoading = ref(false) | ||
| 51 | +const formRef = ref() | ||
| 52 | + | ||
| 53 | +const formData = reactive({ | ||
| 54 | + id: undefined, | ||
| 55 | + realname: '', | ||
| 56 | + phone: '', | ||
| 57 | + nickname: '', | ||
| 58 | + status: 1, | ||
| 59 | + sex: 1, | ||
| 60 | + avatar: '', | ||
| 61 | + signature: '', | ||
| 62 | + backgroundImage: '' | ||
| 63 | +}) | ||
| 64 | + | ||
| 65 | +const formRules = reactive({ | ||
| 66 | + realname: [{ required: true, message: '真实姓名不能为空', trigger: 'blur' }], | ||
| 67 | + nickname: [{ required: true, message: '昵称不能为空', trigger: 'blur' }], | ||
| 68 | + phone: [{ required: true, message: '手机号不能为空', trigger: 'blur' }], | ||
| 69 | + status: [{ required: true, message: '账号状态不能为空', trigger: 'change' }], | ||
| 70 | + sex: [{ required: true, message: '性别不能为空', trigger: 'change' }] | ||
| 71 | +}) | ||
| 72 | + | ||
| 73 | +/** 打开弹窗 */ | ||
| 74 | +const open = async (id) => { | ||
| 75 | + dialogVisible.value = true | ||
| 76 | + resetForm() | ||
| 77 | + if (id) { | ||
| 78 | + formLoading.value = true | ||
| 79 | + try { | ||
| 80 | + const data = await StudentApi.getStudent(id) | ||
| 81 | + Object.assign(formData, data) | ||
| 82 | + } finally { | ||
| 83 | + formLoading.value = false | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +defineExpose({ open }) | ||
| 89 | + | ||
| 90 | +const emit = defineEmits(['success']) | ||
| 91 | + | ||
| 92 | +/** 提交表单 */ | ||
| 93 | +const submitForm = async () => { | ||
| 94 | + if (!formRef.value) return | ||
| 95 | + const valid = await formRef.value.validate() | ||
| 96 | + if (!valid) return | ||
| 97 | + | ||
| 98 | + submitLoading.value = true | ||
| 99 | + try { | ||
| 100 | + await StudentApi.updateStudent(formData) | ||
| 101 | + message.success('修改成功') | ||
| 102 | + dialogVisible.value = false | ||
| 103 | + emit('success') | ||
| 104 | + } finally { | ||
| 105 | + submitLoading.value = false | ||
| 106 | + } | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +/** 重置表单 */ | ||
| 110 | +const resetForm = () => { | ||
| 111 | + formData.id = undefined | ||
| 112 | + formData.realname = '' | ||
| 113 | + formData.phone = '' | ||
| 114 | + formData.nickname = '' | ||
| 115 | + formData.status = 1 | ||
| 116 | + formData.sex = 1 | ||
| 117 | + formData.avatar = '' | ||
| 118 | + formData.signature = '' | ||
| 119 | + formData.backgroundImage = '' | ||
| 120 | + formRef.value?.resetFields() | ||
| 121 | +} | ||
| 122 | +</script> |
| 1 | +<template> | ||
| 2 | + <el-drawer title="学员健康资料" v-model="drawerVisible" size="600px"> | ||
| 3 | + <div v-loading="loading" class="health-data-container"> | ||
| 4 | + <el-descriptions :column="2" border v-if="healthData"> | ||
| 5 | + <el-descriptions-item label="学员姓名" :span="2"> | ||
| 6 | + {{ healthData.name || '-' }} | ||
| 7 | + </el-descriptions-item> | ||
| 8 | + <el-descriptions-item label="性别"> | ||
| 9 | + {{ healthData.genderDesc || (healthData.gender === 1 ? '男' : '女') }} | ||
| 10 | + </el-descriptions-item> | ||
| 11 | + <el-descriptions-item label="生日"> | ||
| 12 | + {{ healthData.birthday || '-' }} | ||
| 13 | + </el-descriptions-item> | ||
| 14 | + <el-descriptions-item label="身高"> | ||
| 15 | + {{ healthData.height ? `${healthData.height} cm` : '-' }} | ||
| 16 | + </el-descriptions-item> | ||
| 17 | + <el-descriptions-item label="当前体重"> | ||
| 18 | + {{ healthData.weight ? `${healthData.weight} kg` : '-' }} | ||
| 19 | + </el-descriptions-item> | ||
| 20 | + <el-descriptions-item label="目标体重" :span="2"> | ||
| 21 | + {{ healthData.targetWeight ? `${healthData.targetWeight} kg` : '-' }} | ||
| 22 | + </el-descriptions-item> | ||
| 23 | + <el-descriptions-item label="健身基础"> | ||
| 24 | + {{ healthData.hasFitnessFoundationDesc || (healthData.hasFitnessFoundation === 1 ? '有' : '无') }} | ||
| 25 | + </el-descriptions-item> | ||
| 26 | + <el-descriptions-item label="训练频次"> | ||
| 27 | + {{ healthData.acceptableTrainingFrequency ? `${healthData.acceptableTrainingFrequency} 次/周` : '-' }} | ||
| 28 | + </el-descriptions-item> | ||
| 29 | + <el-descriptions-item label="训练目标" :span="2"> | ||
| 30 | + {{ healthData.trainingGoalDesc || '-' }} | ||
| 31 | + </el-descriptions-item> | ||
| 32 | + <el-descriptions-item label="目标锻炼部位" :span="2"> | ||
| 33 | + <template v-if="healthData.targetMuscleParts && healthData.targetMuscleParts.length"> | ||
| 34 | + <el-tag | ||
| 35 | + v-for="(part, index) in healthData.targetMuscleParts" | ||
| 36 | + :key="index" | ||
| 37 | + class="tag-item" | ||
| 38 | + > | ||
| 39 | + {{ part }} | ||
| 40 | + </el-tag> | ||
| 41 | + </template> | ||
| 42 | + <span v-else>-</span> | ||
| 43 | + </el-descriptions-item> | ||
| 44 | + <el-descriptions-item label="身体疼痛部位" :span="2"> | ||
| 45 | + <template v-if="healthData.painAreas && healthData.painAreas.length"> | ||
| 46 | + <el-tag | ||
| 47 | + v-for="(area, index) in healthData.painAreas" | ||
| 48 | + :key="index" | ||
| 49 | + type="danger" | ||
| 50 | + class="tag-item" | ||
| 51 | + > | ||
| 52 | + {{ area === '0' ? '无' : area }} | ||
| 53 | + </el-tag> | ||
| 54 | + </template> | ||
| 55 | + <span v-else>-</span> | ||
| 56 | + </el-descriptions-item> | ||
| 57 | + <el-descriptions-item label="是否存在疾病"> | ||
| 58 | + <el-tag :type="healthData.hasDisease ? 'danger' : 'success'"> | ||
| 59 | + {{ healthData.hasDisease ? '是' : '否' }} | ||
| 60 | + </el-tag> | ||
| 61 | + </el-descriptions-item> | ||
| 62 | + <el-descriptions-item label="是否在服药"> | ||
| 63 | + <el-tag :type="healthData.isTakingMedication ? 'warning' : 'success'"> | ||
| 64 | + {{ healthData.isTakingMedication ? '是' : '否' }} | ||
| 65 | + </el-tag> | ||
| 66 | + </el-descriptions-item> | ||
| 67 | + <el-descriptions-item label="日常健身场景" :span="2"> | ||
| 68 | + {{ healthData.fitnessSceneDesc || '-' }} | ||
| 69 | + </el-descriptions-item> | ||
| 70 | + <el-descriptions-item label="训练奖励方式" :span="2"> | ||
| 71 | + {{ healthData.rewardMethodDesc || '-' }} | ||
| 72 | + </el-descriptions-item> | ||
| 73 | + </el-descriptions> | ||
| 74 | + | ||
| 75 | + <el-empty v-else-if="!loading" description="暂无健康资料" /> | ||
| 76 | + </div> | ||
| 77 | + </el-drawer> | ||
| 78 | +</template> | ||
| 79 | + | ||
| 80 | +<script setup> | ||
| 81 | +import { ref } from 'vue' | ||
| 82 | +import * as StudentApi from '@/api/store/student' | ||
| 83 | + | ||
| 84 | +const drawerVisible = ref(false) | ||
| 85 | +const loading = ref(false) | ||
| 86 | +const healthData = ref(null) | ||
| 87 | + | ||
| 88 | +/** 打开抽屉并加载数据 */ | ||
| 89 | +const open = async (userId) => { | ||
| 90 | + drawerVisible.value = true | ||
| 91 | + loading.value = true | ||
| 92 | + healthData.value = null | ||
| 93 | + try { | ||
| 94 | + const res = await StudentApi.getStudentHealthData(userId) | ||
| 95 | + healthData.value = res | ||
| 96 | + } finally { | ||
| 97 | + loading.value = false | ||
| 98 | + } | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +defineExpose({ open }) | ||
| 102 | +</script> | ||
| 103 | + | ||
| 104 | +<style scoped> | ||
| 105 | +.health-data-container { | ||
| 106 | + padding: 8px; | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +.tag-item { | ||
| 110 | + margin-right: 6px; | ||
| 111 | + margin-bottom: 4px; | ||
| 112 | +} | ||
| 113 | +</style> |
src/views/store/student/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <!-- 搜索栏 --> | ||
| 3 | + <ContentWrap> | ||
| 4 | + <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> | ||
| 5 | + <el-form-item label="昵称" prop="nickname"> | ||
| 6 | + <el-input | ||
| 7 | + v-model="queryParams.nickname" | ||
| 8 | + placeholder="请输入昵称" | ||
| 9 | + clearable | ||
| 10 | + @keyup.enter="handleQuery" | ||
| 11 | + class="!w-240px" | ||
| 12 | + /> | ||
| 13 | + </el-form-item> | ||
| 14 | + <el-form-item label="手机号" prop="phone"> | ||
| 15 | + <el-input | ||
| 16 | + v-model="queryParams.phone" | ||
| 17 | + placeholder="请输入手机号" | ||
| 18 | + clearable | ||
| 19 | + @keyup.enter="handleQuery" | ||
| 20 | + class="!w-240px" | ||
| 21 | + /> | ||
| 22 | + </el-form-item> | ||
| 23 | + <el-form-item> | ||
| 24 | + <el-button @click="handleQuery"><Icon icon="ep:search" /> 搜索</el-button> | ||
| 25 | + <el-button @click="resetQuery"><Icon icon="ep:refresh" /> 重置</el-button> | ||
| 26 | + </el-form-item> | ||
| 27 | + </el-form> | ||
| 28 | + </ContentWrap> | ||
| 29 | + | ||
| 30 | + <!-- 列表栏 --> | ||
| 31 | + <ContentWrap> | ||
| 32 | + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> | ||
| 33 | + <el-table-column label="编号" align="center" prop="id" width="80" /> | ||
| 34 | + <el-table-column label="头像" align="center" prop="avatar" width="80"> | ||
| 35 | + <template #default="scope"> | ||
| 36 | + <el-avatar :src="scope.row.avatar" size="small" /> | ||
| 37 | + </template> | ||
| 38 | + </el-table-column> | ||
| 39 | + <el-table-column label="昵称" align="center" prop="nickname" /> | ||
| 40 | + <el-table-column label="真实姓名" align="center" prop="realname" /> | ||
| 41 | + <el-table-column label="手机号" align="center" prop="phone" width="120" /> | ||
| 42 | + <el-table-column label="性别" align="center" prop="sexStr" width="100"> | ||
| 43 | + <template #default="scope"> | ||
| 44 | + <el-tag :type="scope.row.sex === 1 ? 'primary' : 'danger'"> | ||
| 45 | + {{ scope.row.sexStr || (scope.row.sex === 1 ? '男' : '女') }} | ||
| 46 | + </el-tag> | ||
| 47 | + </template> | ||
| 48 | + </el-table-column> | ||
| 49 | + <el-table-column label="状态" align="center" prop="status" width="100"> | ||
| 50 | + <template #default="scope"> | ||
| 51 | + <el-tag :type="scope.row.status === 1 ? 'success' : 'info'"> | ||
| 52 | + {{ scope.row.statusStr || (scope.row.status === 1 ? '正常' : '禁用') }} | ||
| 53 | + </el-tag> | ||
| 54 | + </template> | ||
| 55 | + </el-table-column> | ||
| 56 | + <el-table-column label="操作" align="center" width="180"> | ||
| 57 | + <template #default="scope"> | ||
| 58 | + <el-button | ||
| 59 | + link | ||
| 60 | + type="primary" | ||
| 61 | + @click="openForm(scope.row.id)" | ||
| 62 | + > | ||
| 63 | + 编辑 | ||
| 64 | + </el-button> | ||
| 65 | + <el-button | ||
| 66 | + link | ||
| 67 | + type="primary" | ||
| 68 | + @click="openHealthData(scope.row.id)" | ||
| 69 | + > | ||
| 70 | + 健康资料 | ||
| 71 | + </el-button> | ||
| 72 | + </template> | ||
| 73 | + </el-table-column> | ||
| 74 | + </el-table> | ||
| 75 | + | ||
| 76 | + <!-- 分页 --> | ||
| 77 | + <Pagination | ||
| 78 | + :total="total" | ||
| 79 | + v-model:page="queryParams.pageNo" | ||
| 80 | + v-model:limit="queryParams.pageSize" | ||
| 81 | + @pagination="getList" | ||
| 82 | + /> | ||
| 83 | + </ContentWrap> | ||
| 84 | + | ||
| 85 | + <!-- 表单弹窗:修改用户 --> | ||
| 86 | + <StudentForm ref="formRef" @success="getList" /> | ||
| 87 | + | ||
| 88 | + <!-- 抽屉:学员健康资料 --> | ||
| 89 | + <StudentHealthData ref="healthDataRef" /> | ||
| 90 | +</template> | ||
| 91 | + | ||
| 92 | +<script setup> | ||
| 93 | +import { ref, reactive, onMounted } from 'vue' | ||
| 94 | +import * as StudentApi from '@/api/store/student' | ||
| 95 | +import StudentForm from './StudentForm.vue' | ||
| 96 | +import StudentHealthData from './StudentHealthData.vue' | ||
| 97 | + | ||
| 98 | +const loading = ref(true) | ||
| 99 | +const total = ref(0) | ||
| 100 | +const list = ref([]) | ||
| 101 | +const queryParams = reactive({ | ||
| 102 | + pageNo: 1, | ||
| 103 | + pageSize: 10, | ||
| 104 | + phone: undefined, | ||
| 105 | + nickname: undefined | ||
| 106 | +}) | ||
| 107 | +const queryFormRef = ref() | ||
| 108 | +const formRef = ref() | ||
| 109 | +const healthDataRef = ref() | ||
| 110 | + | ||
| 111 | +/** 查询列表 */ | ||
| 112 | +const getList = async () => { | ||
| 113 | + loading.value = true | ||
| 114 | + try { | ||
| 115 | + const data = await StudentApi.getStudentPage(queryParams) | ||
| 116 | + list.value = data.list | ||
| 117 | + total.value = data.total | ||
| 118 | + } finally { | ||
| 119 | + loading.value = false | ||
| 120 | + } | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +/** 搜索按钮操作 */ | ||
| 124 | +const handleQuery = () => { | ||
| 125 | + queryParams.pageNo = 1 | ||
| 126 | + getList() | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +/** 重置按钮操作 */ | ||
| 130 | +const resetQuery = () => { | ||
| 131 | + queryFormRef.value?.resetFields() | ||
| 132 | + handleQuery() | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +/** 打开修改弹窗 */ | ||
| 136 | +const openForm = (id) => { | ||
| 137 | + formRef.value.open(id) | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +/** 打开健康资料抽屉 */ | ||
| 141 | +const openHealthData = (userId) => { | ||
| 142 | + healthDataRef.value.open(userId) | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +/** 初始化 */ | ||
| 146 | +onMounted(() => { | ||
| 147 | + getList() | ||
| 148 | +}) | ||
| 149 | +</script> |
| @@ -10,7 +10,7 @@ | @@ -10,7 +10,7 @@ | ||
| 10 | <el-select v-model="formData.exerciseType" placeholder="请选择动作类型" class="w-full"> | 10 | <el-select v-model="formData.exerciseType" placeholder="请选择动作类型" class="w-full"> |
| 11 | <el-option label="无氧运动" :value="0" /> | 11 | <el-option label="无氧运动" :value="0" /> |
| 12 | <el-option label="有氧运动" :value="1" /> | 12 | <el-option label="有氧运动" :value="1" /> |
| 13 | - <el-option label="计次训练" :value="2" /> | 13 | + <el-option label="记次训练" :value="2" /> |
| 14 | <el-option label="计时训练" :value="3" /> | 14 | <el-option label="计时训练" :value="3" /> |
| 15 | <el-option label="自重加重" :value="4" /> | 15 | <el-option label="自重加重" :value="4" /> |
| 16 | <el-option label="自重减重" :value="5" /> | 16 | <el-option label="自重减重" :value="5" /> |
| 1 | <template> | 1 | <template> |
| 2 | - <el-dialog v-model="dialogVisible" title="训练记录详情" width="600px"> | 2 | + <Dialog :title="dialogTitle" v-model="dialogVisible" width="700px"> |
| 3 | <div v-loading="loading"> | 3 | <div v-loading="loading"> |
| 4 | - <!-- 基础信息(一列) --> | 4 | + <!-- 基础信息 --> |
| 5 | <el-descriptions v-if="detailData" border :column="1" label-width="120px"> | 5 | <el-descriptions v-if="detailData" border :column="1" label-width="120px"> |
| 6 | <el-descriptions-item label="学员"> | 6 | <el-descriptions-item label="学员"> |
| 7 | {{ detailData.username || '-' }} | 7 | {{ detailData.username || '-' }} |
| @@ -13,15 +13,10 @@ | @@ -13,15 +13,10 @@ | ||
| 13 | 13 | ||
| 14 | <!-- 训练单元列表 --> | 14 | <!-- 训练单元列表 --> |
| 15 | <div v-if="detailData.units && detailData.units.length > 0" class="unit-section"> | 15 | <div v-if="detailData.units && detailData.units.length > 0" class="unit-section"> |
| 16 | - <h3 style="margin: 20px 0 10px; color: #333;">训练单元列表</h3> | 16 | + <h3 class="section-title">训练单元列表</h3> |
| 17 | 17 | ||
| 18 | - <!-- 循环每个训练单元 --> | ||
| 19 | <div v-for="(unit, unitIndex) in detailData.units" :key="unit.id || unitIndex" class="unit-card"> | 18 | <div v-for="(unit, unitIndex) in detailData.units" :key="unit.id || unitIndex" class="unit-card"> |
| 20 | - <!-- 单元基础信息(改成一列) --> | ||
| 21 | <el-descriptions border :column="1" label-width="120px" size="small"> | 19 | <el-descriptions border :column="1" label-width="120px" size="small"> |
| 22 | - <!-- <el-descriptions-item label="单元ID"> | ||
| 23 | - {{ unit.id || '-' }} | ||
| 24 | - </el-descriptions-item> --> | ||
| 25 | <el-descriptions-item label="单元名称"> | 20 | <el-descriptions-item label="单元名称"> |
| 26 | {{ unit.name || '-' }} | 21 | {{ unit.name || '-' }} |
| 27 | </el-descriptions-item> | 22 | </el-descriptions-item> |
| @@ -33,27 +28,26 @@ | @@ -33,27 +28,26 @@ | ||
| 33 | </el-descriptions-item> | 28 | </el-descriptions-item> |
| 34 | </el-descriptions> | 29 | </el-descriptions> |
| 35 | 30 | ||
| 36 | - <!-- 动作列表(改成一列描述列表) --> | 31 | + <!-- 动作列表 --> |
| 37 | <div v-if="unit.exercises && unit.exercises.length > 0" class="exercise-section"> | 32 | <div v-if="unit.exercises && unit.exercises.length > 0" class="exercise-section"> |
| 38 | - <h4 style="margin: 15px 0 8px; color: #409EFF;">动作列表</h4> | 33 | + <h4 class="sub-title">动作列表</h4> |
| 39 | 34 | ||
| 40 | - <!-- 循环每个动作 --> | ||
| 41 | - <div v-for="(exercise, exIndex) in unit.exercises" :key="exercise.exerciseId || exIndex" | ||
| 42 | - class="exercise-card"> | 35 | + <div v-for="(exercise, exIndex) in unit.exercises" :key="exercise.exerciseId || exIndex" class="exercise-card"> |
| 43 | <el-descriptions border :column="1" label-width="120px" size="small"> | 36 | <el-descriptions border :column="1" label-width="120px" size="small"> |
| 44 | - <!-- <el-descriptions-item label="动作ID"> | ||
| 45 | - {{ exercise.exerciseId || '-' }} | ||
| 46 | - </el-descriptions-item> --> | ||
| 47 | <el-descriptions-item label="排序"> | 37 | <el-descriptions-item label="排序"> |
| 48 | {{ exercise.innerOrder || '-' }} | 38 | {{ exercise.innerOrder || '-' }} |
| 49 | </el-descriptions-item> | 39 | </el-descriptions-item> |
| 50 | <el-descriptions-item label="动作名称"> | 40 | <el-descriptions-item label="动作名称"> |
| 51 | {{ exercise.name || '-' }} | 41 | {{ exercise.name || '-' }} |
| 52 | </el-descriptions-item> | 42 | </el-descriptions-item> |
| 53 | - <!-- 训练组详情(一列) --> | ||
| 54 | <el-descriptions-item label="训练组详情"> | 43 | <el-descriptions-item label="训练组详情"> |
| 55 | - <el-table v-if="exercise.sets && exercise.sets.length > 0" :data="exercise.sets" border size="small" | ||
| 56 | - stripe style="width: 100%;"> | 44 | + <el-table |
| 45 | + v-if="exercise.sets && exercise.sets.length > 0" | ||
| 46 | + :data="exercise.sets" | ||
| 47 | + border | ||
| 48 | + size="small" | ||
| 49 | + stripe | ||
| 50 | + > | ||
| 57 | <el-table-column label="组号" width="80"> | 51 | <el-table-column label="组号" width="80"> |
| 58 | <template #default="scope"> | 52 | <template #default="scope"> |
| 59 | {{ scope.$index + 1 }} | 53 | {{ scope.$index + 1 }} |
| @@ -68,9 +62,8 @@ | @@ -68,9 +62,8 @@ | ||
| 68 | </el-table-column> | 62 | </el-table-column> |
| 69 | <el-table-column prop="restTime" label="休息(秒)" width="100" /> | 63 | <el-table-column prop="restTime" label="休息(秒)" width="100" /> |
| 70 | </el-table> | 64 | </el-table> |
| 71 | - <div v-else>无训练组数据</div> | 65 | + <div v-else class="empty-text">无训练组数据</div> |
| 72 | </el-descriptions-item> | 66 | </el-descriptions-item> |
| 73 | - <!-- --> | ||
| 74 | </el-descriptions> | 67 | </el-descriptions> |
| 75 | </div> | 68 | </div> |
| 76 | </div> | 69 | </div> |
| @@ -79,69 +72,99 @@ | @@ -79,69 +72,99 @@ | ||
| 79 | </div> | 72 | </div> |
| 80 | 73 | ||
| 81 | <template #footer> | 74 | <template #footer> |
| 82 | - <el-button @click="dialogVisible = false">关闭</el-button> | 75 | + <el-button @click="dialogVisible = false">关 闭</el-button> |
| 83 | </template> | 76 | </template> |
| 84 | - </el-dialog> | 77 | + </Dialog> |
| 85 | </template> | 78 | </template> |
| 86 | 79 | ||
| 87 | - | ||
| 88 | <script setup> | 80 | <script setup> |
| 89 | import { ref, watch, computed } from 'vue' | 81 | import { ref, watch, computed } from 'vue' |
| 90 | import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord' | 82 | import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord' |
| 91 | 83 | ||
| 92 | -const loading = ref(false) | ||
| 93 | -const detailData = ref({}) | ||
| 94 | - | 84 | +// ===================== Props & Emits ===================== |
| 95 | const props = defineProps({ | 85 | const props = defineProps({ |
| 96 | visible: { type: Boolean, default: false }, | 86 | visible: { type: Boolean, default: false }, |
| 97 | id: { type: [Number, String], default: null } | 87 | id: { type: [Number, String], default: null } |
| 98 | }) | 88 | }) |
| 99 | - | ||
| 100 | const emit = defineEmits(['update:visible']) | 89 | const emit = defineEmits(['update:visible']) |
| 101 | 90 | ||
| 102 | const dialogVisible = computed({ | 91 | const dialogVisible = computed({ |
| 103 | get: () => props.visible, | 92 | get: () => props.visible, |
| 104 | set: (val) => emit('update:visible', val) | 93 | set: (val) => emit('update:visible', val) |
| 105 | }) | 94 | }) |
| 95 | +const dialogTitle = ref('训练记录详情') | ||
| 106 | 96 | ||
| 107 | -watch(() => props.id, (newId) => { | ||
| 108 | - if (newId) getStuTrainingDetail(newId) | ||
| 109 | -}) | ||
| 110 | -// 转换单元类型 | 97 | +// ===================== 状态 ===================== |
| 98 | +const loading = ref(false) | ||
| 99 | +const detailData = ref({}) | ||
| 100 | + | ||
| 101 | +// ===================== 工具函数 ===================== | ||
| 111 | const formatUnitType = (type) => { | 102 | const formatUnitType = (type) => { |
| 112 | if (type === 1) return '动作组' | 103 | if (type === 1) return '动作组' |
| 113 | if (type === 2) return '超级组' | 104 | if (type === 2) return '超级组' |
| 114 | return '未知类型' | 105 | return '未知类型' |
| 115 | } | 106 | } |
| 107 | + | ||
| 108 | +// ===================== 数据加载 ===================== | ||
| 116 | const getStuTrainingDetail = async (id) => { | 109 | const getStuTrainingDetail = async (id) => { |
| 117 | loading.value = true | 110 | loading.value = true |
| 118 | try { | 111 | try { |
| 119 | const res = await stuTrainingRecordsApi.getStuTrainingRecordDetail(id) | 112 | const res = await stuTrainingRecordsApi.getStuTrainingRecordDetail(id) |
| 120 | detailData.value = res || {} | 113 | detailData.value = res || {} |
| 121 | - } catch (err) { | ||
| 122 | - console.error('请求失败', err) | ||
| 123 | } finally { | 114 | } finally { |
| 124 | loading.value = false | 115 | loading.value = false |
| 125 | } | 116 | } |
| 126 | } | 117 | } |
| 118 | + | ||
| 119 | +// ===================== 监听弹窗打开 / id 变化加载详情 ===================== | ||
| 120 | +watch( | ||
| 121 | + [() => props.visible, () => props.id], | ||
| 122 | + ([visible, newId]) => { | ||
| 123 | + if (visible && newId) getStuTrainingDetail(newId) | ||
| 124 | + }, | ||
| 125 | + { immediate: true } | ||
| 126 | +) | ||
| 127 | </script> | 127 | </script> |
| 128 | 128 | ||
| 129 | <style scoped> | 129 | <style scoped> |
| 130 | -/* 单元卡片 */ | 130 | +.unit-section { |
| 131 | + margin-top: 20px; | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +.section-title { | ||
| 135 | + margin: 0 0 12px; | ||
| 136 | + padding-left: 10px; | ||
| 137 | + font-size: 16px; | ||
| 138 | + font-weight: 600; | ||
| 139 | + color: var(--el-text-color-primary); | ||
| 140 | + border-left: 4px solid var(--el-color-primary); | ||
| 141 | +} | ||
| 142 | + | ||
| 143 | +.sub-title { | ||
| 144 | + margin: 16px 0 10px; | ||
| 145 | + font-size: 14px; | ||
| 146 | + font-weight: 600; | ||
| 147 | + color: var(--el-color-primary); | ||
| 148 | +} | ||
| 149 | + | ||
| 131 | .unit-card { | 150 | .unit-card { |
| 132 | padding: 15px; | 151 | padding: 15px; |
| 133 | margin-bottom: 20px; | 152 | margin-bottom: 20px; |
| 134 | - background: #fafafa; | ||
| 135 | - border: 1px solid #ebeef5; | 153 | + background: var(--el-fill-color-lighter); |
| 154 | + border: 1px solid var(--el-border-color-lighter); | ||
| 136 | border-radius: 8px; | 155 | border-radius: 8px; |
| 137 | } | 156 | } |
| 138 | 157 | ||
| 139 | -/* 动作卡片 */ | ||
| 140 | .exercise-card { | 158 | .exercise-card { |
| 141 | padding: 10px; | 159 | padding: 10px; |
| 142 | margin-top: 15px; | 160 | margin-top: 15px; |
| 143 | - background: #fff; | ||
| 144 | - border: 1px solid #e4e7ed; | 161 | + background: var(--el-bg-color); |
| 162 | + border: 1px solid var(--el-border-color-light); | ||
| 145 | border-radius: 6px; | 163 | border-radius: 6px; |
| 146 | } | 164 | } |
| 165 | + | ||
| 166 | +.empty-text { | ||
| 167 | + padding: 10px; | ||
| 168 | + color: var(--el-text-color-secondary); | ||
| 169 | +} | ||
| 147 | </style> | 170 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <div class="page-container"> | 2 | <div class="page-container"> |
| 3 | - <ContentWrap class="search-bar"> | ||
| 4 | - <div class="search-item"> | ||
| 5 | - <label class="search-label">学员</label> | ||
| 6 | - <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" @keyup.enter="handleSearch" | ||
| 7 | - clearable /> | ||
| 8 | - <div class="search-buttons"> | 3 | + <!-- 搜索栏 --> |
| 4 | + <ContentWrap class="mb-20px"> | ||
| 5 | + <el-form :inline="true" :model="queryParams"> | ||
| 6 | + <el-form-item label="学员" prop="name"> | ||
| 7 | + <el-input v-model="queryParams.name" placeholder="请输入" clearable class="!w-220px" | ||
| 8 | + @keyup.enter="handleSearch" /> | ||
| 9 | + </el-form-item> | ||
| 10 | + <el-form-item> | ||
| 9 | <el-button type="primary" @click="handleSearch"> | 11 | <el-button type="primary" @click="handleSearch"> |
| 10 | - <el-icon> | ||
| 11 | - <Search /> | ||
| 12 | - </el-icon> | ||
| 13 | - 搜索 | 12 | + <Icon icon="ep:search" class="mr-5px" />搜索 |
| 14 | </el-button> | 13 | </el-button> |
| 15 | <el-button @click="handleReset"> | 14 | <el-button @click="handleReset"> |
| 16 | - <el-icon> | ||
| 17 | - <Refresh /> | ||
| 18 | - </el-icon> | ||
| 19 | - 重置 | 15 | + <Icon icon="ep:refresh" class="mr-5px" />重置 |
| 20 | </el-button> | 16 | </el-button> |
| 21 | - <!-- <el-button type="primary" @click="openAddDialog"> | ||
| 22 | - <el-icon> | ||
| 23 | - <Plus /> | ||
| 24 | - </el-icon> | ||
| 25 | - 新增 | ||
| 26 | - </el-button> --> | ||
| 27 | - </div> | ||
| 28 | - </div> | 17 | + </el-form-item> |
| 18 | + </el-form> | ||
| 29 | </ContentWrap> | 19 | </ContentWrap> |
| 30 | 20 | ||
| 31 | - <!-- 列表区域 --> | ||
| 32 | - <!-- <div class="table-container"> --> | 21 | + <!-- 表格区域 --> |
| 33 | <ContentWrap> | 22 | <ContentWrap> |
| 34 | - <el-table :data="tableData" v-loading="loading" empty-text="暂无数据" border> | 23 | + <el-table v-loading="loading" :data="tableData" empty-text="暂无数据" stripe> |
| 35 | <el-table-column prop="username" label="学员" align="center" /> | 24 | <el-table-column prop="username" label="学员" align="center" /> |
| 36 | <el-table-column prop="name" label="训练计划名称" align="center" /> | 25 | <el-table-column prop="name" label="训练计划名称" align="center" /> |
| 37 | <el-table-column label="组数" align="center" :formatter="formatSetCount" /> | 26 | <el-table-column label="组数" align="center" :formatter="formatSetCount" /> |
| 38 | <el-table-column label="训练重量" align="center" :formatter="formatWeight" /> | 27 | <el-table-column label="训练重量" align="center" :formatter="formatWeight" /> |
| 39 | <el-table-column prop="duration" label="训练时长(分钟)" align="center" /> | 28 | <el-table-column prop="duration" label="训练时长(分钟)" align="center" /> |
| 40 | - <!-- <el-table-column prop="date" label="训练日期" align="center" :formatter="dateFormatter" /> --> | ||
| 41 | - <el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" /> | ||
| 42 | - <el-table-column prop="updateTime" label="更新时间" align="center" :formatter="dateFormatter" /> | 29 | + <el-table-column prop="startTime" label="开始时间" align="center" :formatter="dateFormatter" /> |
| 30 | + <el-table-column prop="endTime" label="结束时间" align="center" :formatter="dateFormatter" /> | ||
| 43 | <el-table-column label="操作" align="center" width="150"> | 31 | <el-table-column label="操作" align="center" width="150"> |
| 44 | <template #default="scope"> | 32 | <template #default="scope"> |
| 45 | - <el-button type="text" size="small" style=" margin-right: 8px;color: #409EFF;" | ||
| 46 | - @click="openDetailDialog(scope.row)">详情</el-button> | ||
| 47 | - <el-button type="text" size="small" style="color: #F56C6C;" | ||
| 48 | - @click="handleDelete(scope.row.id)">删除</el-button> | 33 | + <el-button link type="primary" @click="openDetailDialog(scope.row)">详情</el-button> |
| 34 | + <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button> | ||
| 49 | </template> | 35 | </template> |
| 50 | </el-table-column> | 36 | </el-table-column> |
| 51 | </el-table> | 37 | </el-table> |
| 52 | - <!-- 分页组件 --> | ||
| 53 | - <div class="pagination-wrapper" style="padding: 12px 16px;"> | ||
| 54 | - <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" | ||
| 55 | - :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" | ||
| 56 | - style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> | ||
| 57 | - </div> | 38 | + <Pagination :total="queryParams.total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" |
| 39 | + @pagination="getList" /> | ||
| 58 | </ContentWrap> | 40 | </ContentWrap> |
| 59 | - <!-- </div> --> | 41 | + |
| 60 | <!-- 详情弹窗 --> | 42 | <!-- 详情弹窗 --> |
| 61 | <StuTrainingDetailDialog v-model:visible="detailVisible" :id="currentId" /> | 43 | <StuTrainingDetailDialog v-model:visible="detailVisible" :id="currentId" /> |
| 62 | </div> | 44 | </div> |
| 63 | </template> | 45 | </template> |
| 64 | 46 | ||
| 65 | <script setup> | 47 | <script setup> |
| 66 | -import { ref, reactive } from 'vue' | ||
| 67 | -// 导入图标 | ||
| 68 | -import { Search, Refresh, Plus } from '@element-plus/icons-vue' | ||
| 69 | -import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord.ts' | ||
| 70 | -import { ElMessage } from 'element-plus' | ||
| 71 | -import { dateFormatter } from '@/utils/formatTime' | ||
| 72 | -import lostImg from '@/assets/imgs/lost.png' | 48 | +import { ref, reactive, onMounted } from 'vue' |
| 49 | + | ||
| 50 | +import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord' | ||
| 73 | import StuTrainingDetailDialog from './StuTrainingDetailDialog.vue' | 51 | import StuTrainingDetailDialog from './StuTrainingDetailDialog.vue' |
| 74 | -// 搜索表单 | ||
| 75 | -const searchForm = reactive({ | 52 | +import { dateFormatter } from '@/utils/formatTime' |
| 53 | + | ||
| 54 | +// ===================== 查询参数 ===================== | ||
| 55 | +const queryParams = reactive({ | ||
| 56 | + pageNo: 1, | ||
| 57 | + pageSize: 10, | ||
| 58 | + total: 0, | ||
| 76 | name: '' | 59 | name: '' |
| 77 | }) | 60 | }) |
| 78 | - | ||
| 79 | -// 加载状态 | ||
| 80 | const loading = ref(false) | 61 | const loading = ref(false) |
| 81 | - | ||
| 82 | -// 表格数据(3组模拟数据) | ||
| 83 | const tableData = ref([]) | 62 | const tableData = ref([]) |
| 84 | 63 | ||
| 85 | -// 分页参数 | ||
| 86 | -const pagination = reactive({ | ||
| 87 | - pageNo: 1, | ||
| 88 | - pageSize: 10, | ||
| 89 | - total: 0 | ||
| 90 | -}) | ||
| 91 | -// 详情弹窗 | 64 | +// ===================== 详情弹窗 ===================== |
| 92 | const detailVisible = ref(false) | 65 | const detailVisible = ref(false) |
| 93 | -const currentId = ref(null) // 存储当前点击的id | ||
| 94 | -// ========================列表格式化========== | ||
| 95 | -// 格式化组数(在数字后加“组”) | 66 | +const currentId = ref(null) |
| 67 | + | ||
| 68 | +// ===================== 表格格式化 ===================== | ||
| 96 | const formatSetCount = (row) => { | 69 | const formatSetCount = (row) => { |
| 97 | - return row.setCount ? `${row.setCount}组` : '0组' | 70 | + return row.setCount ? row.setCount + '组' : '0组' |
| 98 | } | 71 | } |
| 99 | -// 格式化重量 | ||
| 100 | const formatWeight = (row) => { | 72 | const formatWeight = (row) => { |
| 101 | - return row.weight ? `${row.weight}kg` : '0kg' | 73 | + return row.weight ? row.weight + 'kg' : '0kg' |
| 102 | } | 74 | } |
| 103 | -// 获取学员训练记录列表 | 75 | + |
| 76 | +// ===================== 数据加载 ===================== | ||
| 104 | const getList = async () => { | 77 | const getList = async () => { |
| 105 | loading.value = true | 78 | loading.value = true |
| 106 | try { | 79 | try { |
| 107 | - // 搜索条件 | ||
| 108 | - const params = { | ||
| 109 | - pageNo: pagination.pageNo + '', | ||
| 110 | - pageSize: pagination.pageSize + '', | ||
| 111 | - name: searchForm.name, | ||
| 112 | - } | ||
| 113 | - const res = await stuTrainingRecordsApi.getstuTrainingRecordPage(params) | 80 | + const res = await stuTrainingRecordsApi.getstuTrainingRecordPage({ |
| 81 | + pageNo: queryParams.pageNo + '', | ||
| 82 | + pageSize: queryParams.pageSize + '', | ||
| 83 | + name: queryParams.name | ||
| 84 | + }) | ||
| 114 | tableData.value = res.list || [] | 85 | tableData.value = res.list || [] |
| 115 | - pagination.total = res.total || 0 | ||
| 116 | - console.log('学员训练记录分页接口返回数据:', res) | 86 | + queryParams.total = res.total || 0 |
| 117 | } catch (err) { | 87 | } catch (err) { |
| 118 | - console.error('❌ 请求失败:', err) | ||
| 119 | ElMessage.error('请求失败,请检查网络或接口') | 88 | ElMessage.error('请求失败,请检查网络或接口') |
| 120 | } finally { | 89 | } finally { |
| 121 | loading.value = false | 90 | loading.value = false |
| 122 | } | 91 | } |
| 123 | } | 92 | } |
| 124 | -// 搜索 | 93 | + |
| 94 | +// ===================== 搜索与重置 ===================== | ||
| 125 | const handleSearch = () => { | 95 | const handleSearch = () => { |
| 126 | - pagination.pageNo = 1 // 搜索时重置到第一页 | 96 | + queryParams.pageNo = 1 |
| 127 | getList() | 97 | getList() |
| 128 | } | 98 | } |
| 129 | 99 | ||
| 130 | -// 重置 | ||
| 131 | const handleReset = () => { | 100 | const handleReset = () => { |
| 132 | - pagination.pageNo = 1 | ||
| 133 | - pagination.pageSize = 10 | ||
| 134 | - searchForm.name = '' | 101 | + Object.assign(queryParams, { pageNo: 1, pageSize: 10, total: 0, name: '' }) |
| 135 | getList() | 102 | getList() |
| 136 | - | ||
| 137 | } | 103 | } |
| 138 | 104 | ||
| 139 | -// 详情 | 105 | +// ===================== 详情 ===================== |
| 140 | const openDetailDialog = (row) => { | 106 | const openDetailDialog = (row) => { |
| 141 | currentId.value = row.id | 107 | currentId.value = row.id |
| 142 | detailVisible.value = true | 108 | detailVisible.value = true |
| 143 | } | 109 | } |
| 144 | 110 | ||
| 145 | -// 处理删除 | 111 | +// ===================== 删除 ===================== |
| 146 | const handleDelete = async (id) => { | 112 | const handleDelete = async (id) => { |
| 147 | try { | 113 | try { |
| 148 | await ElMessageBox.confirm('确定要删除吗?', '提示', { | 114 | await ElMessageBox.confirm('确定要删除吗?', '提示', { |
| @@ -150,82 +116,27 @@ const handleDelete = async (id) => { | @@ -150,82 +116,27 @@ const handleDelete = async (id) => { | ||
| 150 | cancelButtonText: '取消', | 116 | cancelButtonText: '取消', |
| 151 | type: 'warning' | 117 | type: 'warning' |
| 152 | }) | 118 | }) |
| 153 | - | ||
| 154 | - // 能走到这里 = 用户点了确定 | ||
| 155 | await stuTrainingRecordsApi.deleteStuTrainingRecordPage(id) | 119 | await stuTrainingRecordsApi.deleteStuTrainingRecordPage(id) |
| 156 | ElMessage.success('删除成功') | 120 | ElMessage.success('删除成功') |
| 121 | + getList() | ||
| 157 | } catch (err) { | 122 | } catch (err) { |
| 158 | - // 先打印,方便调试 | ||
| 159 | - console.log('实际错误内容:', err, typeof err) | ||
| 160 | - // 核心判断:err 是字符串 'cancel' | ||
| 161 | - if (err === 'cancel') { | ||
| 162 | - ElMessage.info('已取消删除') | ||
| 163 | - return // 直接退出,不执行后面的错误处理 | 123 | + if (err !== 'cancel') { |
| 124 | + ElMessage.error('删除失败') | ||
| 164 | } | 125 | } |
| 165 | - // 只有非 cancel 的错误才会走到这里 | ||
| 166 | - console.error('❌ 请求失败:', err) | ||
| 167 | - ElMessage.error('请求失败,请检查网络或接口') | ||
| 168 | - } finally { | ||
| 169 | - getList() // 刷新列表 | ||
| 170 | } | 126 | } |
| 171 | } | 127 | } |
| 172 | 128 | ||
| 173 | -// 处理分页大小变化 | ||
| 174 | -const handleSizeChange = (val) => { | ||
| 175 | - pagination.pageSize = val | ||
| 176 | - getList() | ||
| 177 | -} | ||
| 178 | - | ||
| 179 | -const handleCurrentChange = (val) => { | ||
| 180 | - pagination.pageNo = val | ||
| 181 | - getList() | ||
| 182 | -} | ||
| 183 | - | 129 | +// ===================== 初始化 ===================== |
| 184 | onMounted(() => { | 130 | onMounted(() => { |
| 185 | getList() | 131 | getList() |
| 186 | }) | 132 | }) |
| 187 | </script> | 133 | </script> |
| 188 | -<style scoped> | ||
| 189 | -/* .page-container { | ||
| 190 | - | ||
| 191 | -} */ | ||
| 192 | - | ||
| 193 | -/* 分页居右 */ | ||
| 194 | -.pagination-wrapper { | ||
| 195 | - display: flex; | ||
| 196 | - padding: 12px 16px; | ||
| 197 | - justify-content: flex-end; | ||
| 198 | -} | ||
| 199 | 134 | ||
| 200 | -.search-bar { | ||
| 201 | - /* margin-bottom: 20px; */ | ||
| 202 | - margin-bottom: 20px; | ||
| 203 | - box-shadow: 0 1px 2px rgb(0 0 0 / 5%); | ||
| 204 | -} | ||
| 205 | - | ||
| 206 | -.search-item { | ||
| 207 | - display: flex; | ||
| 208 | - align-items: center; | ||
| 209 | - gap: 10px; | ||
| 210 | - padding: 15px 20px; | ||
| 211 | -} | ||
| 212 | - | ||
| 213 | -.search-label { | ||
| 214 | - /* width: 160px; */ | ||
| 215 | - font-weight: 500; | ||
| 216 | -} | ||
| 217 | - | ||
| 218 | -.search-input { | ||
| 219 | - width: 260px; | ||
| 220 | -} | ||
| 221 | - | ||
| 222 | -.search-buttons { | ||
| 223 | - display: flex; | ||
| 224 | - gap: 10px; | ||
| 225 | - margin-left: 10px; | ||
| 226 | -} | ||
| 227 | - | ||
| 228 | -.table-container { | ||
| 229 | - width: 100%; | 135 | +<style scoped> |
| 136 | +.page-container { | ||
| 137 | + padding: 20px; | ||
| 138 | + min-height: 100vh; | ||
| 139 | + background-color: var(--el-bg-color-page); | ||
| 140 | + box-sizing: border-box; | ||
| 230 | } | 141 | } |
| 231 | </style> | 142 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <div class="supersets-page"> | 2 | <div class="supersets-page"> |
| 3 | - <ContentWrap class="plan-search-bar"> | ||
| 4 | - <!-- 1. 搜索栏区域 --> | ||
| 5 | - <div class="search-bar"> | ||
| 6 | - <div class="search-item"> | ||
| 7 | - <label class="search-label">超级组名称</label> | ||
| 8 | - <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" @keyup.enter="handleSearch" | ||
| 9 | - clearable /> | ||
| 10 | - <!-- 按钮组 --> | ||
| 11 | - <div class="search-buttons"> | 3 | + <!-- 搜索栏 --> |
| 4 | + <ContentWrap class="mb-20px"> | ||
| 5 | + <el-form :inline="true" :model="queryParams" class="flex items-center"> | ||
| 6 | + <el-form-item label="超级组名称" prop="name"> | ||
| 7 | + <el-input | ||
| 8 | + v-model="queryParams.name" | ||
| 9 | + placeholder="请输入" | ||
| 10 | + clearable | ||
| 11 | + class="!w-220px" | ||
| 12 | + @keyup.enter="handleSearch" | ||
| 13 | + /> | ||
| 14 | + </el-form-item> | ||
| 15 | + <el-form-item> | ||
| 12 | <el-button type="primary" @click="handleSearch"> | 16 | <el-button type="primary" @click="handleSearch"> |
| 13 | - <el-icon> | ||
| 14 | - <Search /> | ||
| 15 | - </el-icon> | ||
| 16 | - 搜索 | 17 | + <Icon icon="ep:search" class="mr-5px" />搜索 |
| 17 | </el-button> | 18 | </el-button> |
| 18 | <el-button @click="handleReset"> | 19 | <el-button @click="handleReset"> |
| 19 | - <el-icon> | ||
| 20 | - <Refresh /> | ||
| 21 | - </el-icon> | ||
| 22 | - 重置 | 20 | + <Icon icon="ep:refresh" class="mr-5px" />重置 |
| 23 | </el-button> | 21 | </el-button> |
| 24 | <el-button type="primary" @click="openAddDialog"> | 22 | <el-button type="primary" @click="openAddDialog"> |
| 25 | - <el-icon> | ||
| 26 | - <Plus /> | ||
| 27 | - </el-icon> | ||
| 28 | - 新增 | 23 | + <Icon icon="ep:plus" class="mr-5px" />新增 |
| 29 | </el-button> | 24 | </el-button> |
| 30 | - </div> | ||
| 31 | - </div> | ||
| 32 | - </div> | 25 | + </el-form-item> |
| 26 | + </el-form> | ||
| 33 | </ContentWrap> | 27 | </ContentWrap> |
| 28 | + | ||
| 29 | + <!-- 表格区域 --> | ||
| 34 | <ContentWrap> | 30 | <ContentWrap> |
| 35 | - <!-- 2. 表格展示区域 --> | ||
| 36 | - <div class="table-container"> | ||
| 37 | - <el-table :data="tableData" style="width: 100%" empty-text="暂无数据" stripe> | 31 | + <el-table v-loading="loading" :data="tableData" empty-text="暂无数据" stripe> |
| 38 | <el-table-column prop="name" label="超级组名称" align="center" min-width="200" /> | 32 | <el-table-column prop="name" label="超级组名称" align="center" min-width="200" /> |
| 39 | - <!-- <el-table-column label="封面" align="center" width="200"> | ||
| 40 | - <template #default> | ||
| 41 | - <img :src="coverImgUrl" class="cover-img" alt="封面" /> | ||
| 42 | - </template> | ||
| 43 | -</el-table-column> --> | ||
| 44 | <el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" /> | 33 | <el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" /> |
| 45 | <el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" /> | 34 | <el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" /> |
| 46 | <el-table-column label="操作" align="center" width="200"> | 35 | <el-table-column label="操作" align="center" width="200"> |
| 47 | <template #default="scope"> | 36 | <template #default="scope"> |
| 48 | - <el-button type="text" class="edit-btn" @click="handleEdit(scope.row)">编辑</el-button> | ||
| 49 | - <el-button type="text" class="delete-btn" @click="handleDelete(scope.row)">删除</el-button> | 37 | + <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button> |
| 38 | + <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button> | ||
| 50 | </template> | 39 | </template> |
| 51 | </el-table-column> | 40 | </el-table-column> |
| 52 | </el-table> | 41 | </el-table> |
| 53 | - </div> | ||
| 54 | - | ||
| 55 | - <!-- 3. 分页区域 --> | ||
| 56 | - <div class="pagination-container"> | ||
| 57 | - <span class="total">共{{ total }}条</span> | ||
| 58 | - <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> | ||
| 59 | - <el-option label="10条/页" :value="10" /> | ||
| 60 | - <el-option label="20条/页" :value="20" /> | ||
| 61 | - <el-option label="50条/页" :value="50" /> | ||
| 62 | - </el-select> | ||
| 63 | - <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :total="total" | ||
| 64 | - layout="prev, pager, next, jumper" @size-change="handlePageSizeChange" @current-change="handlePageChange" /> | ||
| 65 | - </div> | 42 | + <Pagination |
| 43 | + :total="total" | ||
| 44 | + v-model:page="queryParams.pageNo" | ||
| 45 | + v-model:limit="queryParams.pageSize" | ||
| 46 | + @pagination="getList" | ||
| 47 | + /> | ||
| 66 | </ContentWrap> | 48 | </ContentWrap> |
| 67 | - <!-- 4. 新增/编辑弹窗 --> | ||
| 68 | - <el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px" :close-on-click-modal="false" | ||
| 69 | - @close="resetForm" class="supersets-dialog"> | ||
| 70 | - <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="add-form"> | 49 | + |
| 50 | + <!-- 新增/编辑弹窗 --> | ||
| 51 | + <Dialog :title="dialogTitle" v-model="dialogVisible" width="800px"> | ||
| 52 | + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px"> | ||
| 71 | <el-form-item label="超级组名称" prop="name"> | 53 | <el-form-item label="超级组名称" prop="name"> |
| 72 | <el-input v-model="formData.name" placeholder="请输入" clearable /> | 54 | <el-input v-model="formData.name" placeholder="请输入" clearable /> |
| 73 | </el-form-item> | 55 | </el-form-item> |
| 74 | <el-form-item label="动作" prop="exerciseIds"> | 56 | <el-form-item label="动作" prop="exerciseIds"> |
| 75 | - <el-select v-model="formData.exerciseIds" placeholder="请选择动作" multiple clearable style="width: 100%" | ||
| 76 | - filterable> | ||
| 77 | - <el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" /> | 57 | + <el-select |
| 58 | + v-model="formData.exerciseIds" | ||
| 59 | + placeholder="请选择动作" | ||
| 60 | + multiple | ||
| 61 | + clearable | ||
| 62 | + filterable | ||
| 63 | + style="width: 100%" | ||
| 64 | + > | ||
| 65 | + <el-option | ||
| 66 | + v-for="item in exercisesList" | ||
| 67 | + :key="item.id" | ||
| 68 | + :label="item.name" | ||
| 69 | + :value="item.id" | ||
| 70 | + /> | ||
| 78 | </el-select> | 71 | </el-select> |
| 79 | </el-form-item> | 72 | </el-form-item> |
| 80 | - <!-- 实现动作的主要,次要训练部位的回显,只能显示给用户看 --> | ||
| 81 | <!-- 主要训练部位(自动回显,只读) --> | 73 | <!-- 主要训练部位(自动回显,只读) --> |
| 82 | <el-form-item label="主要训练部位"> | 74 | <el-form-item label="主要训练部位"> |
| 83 | - <el-input :value="primaryMusclesText" disabled placeholder="选择动作后自动显示" style="width: 100%" /> | 75 | + <el-input :value="primaryMusclesText" disabled placeholder="选择动作后自动显示" /> |
| 84 | </el-form-item> | 76 | </el-form-item> |
| 85 | - | ||
| 86 | <!-- 次要训练部位(自动回显,只读) --> | 77 | <!-- 次要训练部位(自动回显,只读) --> |
| 87 | <el-form-item label="次要训练部位"> | 78 | <el-form-item label="次要训练部位"> |
| 88 | - <el-input :value="secondaryMusclesText" disabled placeholder="选择动作后自动显示" style="width: 100%" /> | 79 | + <el-input :value="secondaryMusclesText" disabled placeholder="选择动作后自动显示" /> |
| 89 | </el-form-item> | 80 | </el-form-item> |
| 90 | </el-form> | 81 | </el-form> |
| 91 | <template #footer> | 82 | <template #footer> |
| 92 | - <div class="dialog-footer"> | ||
| 93 | - <el-button @click="dialogVisible = false">取消</el-button> | ||
| 94 | - <el-button type="primary" @click="isEdit ? submitEditForm() : submitAddForm()"> | ||
| 95 | - {{ isEdit ? '修改' : '保存' }} | ||
| 96 | - </el-button> | ||
| 97 | - </div> | 83 | + <el-button type="primary" @click="submitForm">确 定</el-button> |
| 84 | + <el-button @click="dialogVisible = false">取 消</el-button> | ||
| 98 | </template> | 85 | </template> |
| 99 | - </el-dialog> | 86 | + </Dialog> |
| 100 | </div> | 87 | </div> |
| 101 | </template> | 88 | </template> |
| 102 | 89 | ||
| 103 | -<script setup lang="ts"> | ||
| 104 | -import { ref, onMounted, reactive, computed } from 'vue' | ||
| 105 | -// import { useRouter } from 'vue-router' | ||
| 106 | -import { ElMessage, ElMessageBox, ElForm } from 'element-plus' | ||
| 107 | -import { Search, Refresh, Plus } from '@element-plus/icons-vue' | 90 | +<script setup> |
| 91 | +import { ref, onMounted, reactive, computed, watch } from 'vue' | ||
| 92 | +import { ElMessage, ElMessageBox } from 'element-plus' | ||
| 108 | import { SupersetsApi } from '@/api/store/training/Supersets' | 93 | import { SupersetsApi } from '@/api/store/training/Supersets' |
| 109 | import { ExercisesApi } from '@/api/store/training/pose' | 94 | import { ExercisesApi } from '@/api/store/training/pose' |
| 110 | -// import { MotionCategoryApi } from '@/api/store/training/partCategory' | ||
| 111 | -import { MusclesApi } from '@/api/store/training/muscle' | ||
| 112 | import { dateFormatter } from '@/utils/formatTime' | 95 | import { dateFormatter } from '@/utils/formatTime' |
| 113 | -import coverImgUrl from '@/assets/imgs/lost.png' | ||
| 114 | -// const router = useRouter() | ||
| 115 | -const searchForm = ref({ | 96 | + |
| 97 | +// ===================== 查询参数 ===================== | ||
| 98 | +const queryParams = reactive({ | ||
| 99 | + pageNo: 1, | ||
| 100 | + pageSize: 10, | ||
| 116 | name: '' | 101 | name: '' |
| 117 | }) | 102 | }) |
| 118 | -const pageNo = ref(1) | ||
| 119 | -const pageSize = ref(10) | ||
| 120 | const total = ref(0) | 103 | const total = ref(0) |
| 121 | -const tableData = ref<any[]>([]) | 104 | +const loading = ref(false) |
| 105 | +const tableData = ref([]) | ||
| 106 | + | ||
| 107 | +// ===================== 弹窗状态 ===================== | ||
| 122 | const dialogVisible = ref(false) | 108 | const dialogVisible = ref(false) |
| 123 | const isEdit = ref(false) | 109 | const isEdit = ref(false) |
| 124 | -const dialogTitle = ref('超级组') | ||
| 125 | -const formRef = ref<InstanceType<typeof ElForm>>() | 110 | +const dialogTitle = ref('') |
| 111 | +const formRef = ref(null) | ||
| 126 | 112 | ||
| 127 | -const exercisesList = ref<any[]>([]) | ||
| 128 | -// const categoryList = ref<any[]>([]) | ||
| 129 | -const muscleList = ref<any[]>([]) | 113 | +// ===================== 选项数据 ===================== |
| 114 | +const exercisesList = ref([]) | ||
| 115 | +const detailExercises = ref([]) // 编辑时存储详情接口返回的动作数据(含肌肉名称) | ||
| 130 | 116 | ||
| 117 | +// ===================== 表单数据 ===================== | ||
| 131 | const formData = reactive({ | 118 | const formData = reactive({ |
| 132 | id: 0, | 119 | id: 0, |
| 133 | name: '', | 120 | name: '', |
| 134 | - primaryMuscles: [] as number[], | ||
| 135 | - secondaryMuscles: [] as number[], | ||
| 136 | - exerciseIds: [] as number[], | ||
| 137 | - category: '' | ||
| 138 | -}) | ||
| 139 | - | ||
| 140 | -// ===================== 自动汇总显示动作的部位(只读) ===================== | ||
| 141 | -// 解析动作里的部位字符串 | ||
| 142 | -const parseMuscles = (str: string) => { | ||
| 143 | - try { return JSON.parse(str) } catch { return [] } | ||
| 144 | -} | ||
| 145 | - | ||
| 146 | -// 主要训练部位汇总文本 | ||
| 147 | -// 根据ID获取肌肉名称 | ||
| 148 | -const getMuscleName = (id) => { | ||
| 149 | - const muscle = muscleList.value.find(item => item.id == id) | ||
| 150 | - return muscle ? muscle.name : id | ||
| 151 | -} | ||
| 152 | -// 肌肉回显 | ||
| 153 | -const primaryMusclesText = computed(() => { | ||
| 154 | - if (!formData.exerciseIds.length) return '' | ||
| 155 | - const idSet = new Set() | ||
| 156 | - | ||
| 157 | - formData.exerciseIds.forEach(exId => { | ||
| 158 | - const item = exercisesList.value.find(e => e.id === exId) | ||
| 159 | - if (!item) return | ||
| 160 | - const arr = parseMuscles(item.primaryMuscles || '[]') | ||
| 161 | - arr.forEach(id => idSet.add(id)) | ||
| 162 | - }) | ||
| 163 | - | ||
| 164 | - return Array.from(idSet).map(id => (id)).join('、') | ||
| 165 | -}) | ||
| 166 | - | ||
| 167 | -const secondaryMusclesText = computed(() => { | ||
| 168 | - if (!formData.exerciseIds.length) return '' | ||
| 169 | - const idSet = new Set() | ||
| 170 | - | ||
| 171 | - console.log('formData.exerciseIds=', formData.exerciseIds); | ||
| 172 | - | ||
| 173 | - formData.exerciseIds.forEach(exId => { | ||
| 174 | - const item = exercisesList.value.find(e => e.id === exId) | ||
| 175 | - console.log('item=', item); | ||
| 176 | - if (!item) return | ||
| 177 | - const arr = parseMuscles(item.secondaryMuscles || '[]') | ||
| 178 | - console.log('arr=', arr); | ||
| 179 | - arr.forEach(id => idSet.add(id)) | ||
| 180 | - }) | ||
| 181 | - | ||
| 182 | - return Array.from(idSet).map(id => getMuscleName(id)).join('、') | 121 | + exerciseIds: [] |
| 183 | }) | 122 | }) |
| 184 | 123 | ||
| 185 | const formRules = reactive({ | 124 | const formRules = reactive({ |
| @@ -189,133 +128,113 @@ const formRules = reactive({ | @@ -189,133 +128,113 @@ const formRules = reactive({ | ||
| 189 | ], | 128 | ], |
| 190 | exerciseIds: [ | 129 | exerciseIds: [ |
| 191 | { required: true, message: '请选择组内动作', trigger: 'change' } | 130 | { required: true, message: '请选择组内动作', trigger: 'change' } |
| 192 | - ], | ||
| 193 | - // primaryMuscles: [ | ||
| 194 | - // { required: true, message: '请选择主要训练部位', trigger: 'change' } | ||
| 195 | - // ] | 131 | + ] |
| 196 | }) | 132 | }) |
| 197 | -//加载数据 | ||
| 198 | -const loadExercisesList = async () => { | ||
| 199 | - try { | ||
| 200 | - const res = await ExercisesApi.getExercisesList() | ||
| 201 | 133 | ||
| 202 | - exercisesList.value = res || [] | ||
| 203 | - } catch (err) { | ||
| 204 | - console.error('加载动作列表失败:', err) | ||
| 205 | - ElMessage.error('加载动作列表失败') | ||
| 206 | - } | 134 | +// ===================== 工具函数 ===================== |
| 135 | +const parseMuscles = (value) => { | ||
| 136 | + if (Array.isArray(value)) return value | ||
| 137 | + try { return JSON.parse(value) } catch { return [] } | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +// ===================== 计算属性:训练部位回显 ===================== | ||
| 141 | +// 查找动作对象:优先返回带肌肉名称(primaryMusclesName/secondaryMusclesName)的数据, | ||
| 142 | +// 编辑时 detailExercises 可能不含名称字段,则回退到 exercisesList(含名称) | ||
| 143 | +const findExercise = (exId) => { | ||
| 144 | + const inDetail = detailExercises.value.find((e) => e.id === exId) | ||
| 145 | + const inList = exercisesList.value.find((e) => e.id === exId) | ||
| 146 | + if (inDetail && Array.isArray(inDetail.primaryMusclesName)) return inDetail | ||
| 147 | + if (inList) return inList | ||
| 148 | + return inDetail || inList | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +// field 传入名称数组字段(primaryMusclesName / secondaryMusclesName) | ||
| 152 | +const aggregateMuscles = (nameField) => { | ||
| 153 | + if (!formData.exerciseIds.length) return '' | ||
| 154 | + const nameSet = new Set() | ||
| 155 | + formData.exerciseIds.forEach((exId) => { | ||
| 156 | + const item = findExercise(exId) | ||
| 157 | + if (!item) return | ||
| 158 | + const list = Array.isArray(item[nameField]) ? item[nameField] : parseMuscles(item[nameField] || []) | ||
| 159 | + list.forEach((name) => nameSet.add(name)) | ||
| 160 | + }) | ||
| 161 | + return Array.from(nameSet).join('、') | ||
| 207 | } | 162 | } |
| 208 | 163 | ||
| 209 | -// const loadCategoryList = async () => { | ||
| 210 | -// try { | ||
| 211 | -// const res = await MotionCategoryApi.getMotionCategoryPage({ pageNo: 1 + '', pageSize: 100 + '', name: '' }) | ||
| 212 | -// const data = res.data || res | ||
| 213 | -// categoryList.value = data.list || [] | ||
| 214 | -// } catch (err) { | ||
| 215 | -// console.error('加载部位分类失败:', err) | ||
| 216 | -// ElMessage.error('加载部位分类失败') | ||
| 217 | -// } | ||
| 218 | -// } | 164 | +const primaryMusclesText = computed(() => aggregateMuscles('primaryMusclesName')) |
| 165 | +const secondaryMusclesText = computed(() => aggregateMuscles('secondaryMusclesName')) | ||
| 219 | 166 | ||
| 220 | -const loadMuscleList = async () => { | 167 | +// ===================== 数据加载 ===================== |
| 168 | +const loadExercisesList = async () => { | ||
| 221 | try { | 169 | try { |
| 222 | - const res = await MusclesApi.getsubCategoriesList() | ||
| 223 | - muscleList.value = res || [] | 170 | + const res = await ExercisesApi.getExercisesList(false) |
| 171 | + exercisesList.value = res || [] | ||
| 224 | } catch (err) { | 172 | } catch (err) { |
| 225 | - console.error('加载肌肉列表失败:', err) | ||
| 226 | - ElMessage.error('加载肌肉列表失败') | 173 | + ElMessage.error('加载动作列表失败') |
| 227 | } | 174 | } |
| 228 | } | 175 | } |
| 229 | 176 | ||
| 230 | const getList = async () => { | 177 | const getList = async () => { |
| 178 | + loading.value = true | ||
| 231 | try { | 179 | try { |
| 232 | const res = await SupersetsApi.getSupersetsPage({ | 180 | const res = await SupersetsApi.getSupersetsPage({ |
| 233 | - pageNo: pageNo.value, | ||
| 234 | - pageSize: pageSize.value, | ||
| 235 | - name: searchForm.value.name | 181 | + pageNo: queryParams.pageNo, |
| 182 | + pageSize: queryParams.pageSize, | ||
| 183 | + name: queryParams.name | ||
| 236 | }) | 184 | }) |
| 237 | const responseData = res.data || res | 185 | const responseData = res.data || res |
| 238 | - console.log('超级组分页接口返回数据', res) | ||
| 239 | const { total: resTotal = 0, list = [] } = responseData | 186 | const { total: resTotal = 0, list = [] } = responseData |
| 240 | - | ||
| 241 | tableData.value = list | 187 | tableData.value = list |
| 242 | total.value = resTotal | 188 | total.value = resTotal |
| 243 | - console.log('tableData.value', tableData.value); | ||
| 244 | - | ||
| 245 | } catch (err) { | 189 | } catch (err) { |
| 246 | ElMessage.error('获取超级组列表失败') | 190 | ElMessage.error('获取超级组列表失败') |
| 247 | - console.error(err) | 191 | + } finally { |
| 192 | + loading.value = false | ||
| 248 | } | 193 | } |
| 249 | } | 194 | } |
| 250 | 195 | ||
| 196 | +// ===================== 搜索与分页 ===================== | ||
| 251 | const handleSearch = () => { | 197 | const handleSearch = () => { |
| 252 | - pageNo.value = 1 | 198 | + queryParams.pageNo = 1 |
| 253 | getList() | 199 | getList() |
| 254 | } | 200 | } |
| 255 | 201 | ||
| 256 | const handleReset = () => { | 202 | const handleReset = () => { |
| 257 | - searchForm.value = { name: '' } | ||
| 258 | - pageNo.value = 1 | ||
| 259 | - getList() | ||
| 260 | -} | ||
| 261 | - | ||
| 262 | -const handlePageChange = (val: number) => { | ||
| 263 | - pageNo.value = val | 203 | + Object.assign(queryParams, { pageNo: 1, pageSize: 10, name: '' }) |
| 264 | getList() | 204 | getList() |
| 265 | } | 205 | } |
| 266 | 206 | ||
| 267 | -const handlePageSizeChange = (val: number) => { | ||
| 268 | - pageSize.value = val | ||
| 269 | - pageNo.value = 1 | ||
| 270 | - getList() | ||
| 271 | -} | ||
| 272 | -//处理新增弹窗 | ||
| 273 | -const openAddDialog = async () => { | 207 | +// ===================== 新增 ===================== |
| 208 | +const openAddDialog = () => { | ||
| 274 | isEdit.value = false | 209 | isEdit.value = false |
| 275 | dialogTitle.value = '新增超级组' | 210 | dialogTitle.value = '新增超级组' |
| 276 | resetForm() | 211 | resetForm() |
| 277 | - | ||
| 278 | - await Promise.all([ | ||
| 279 | - loadExercisesList(), | ||
| 280 | - // loadCategoryList(), | ||
| 281 | - loadMuscleList() | ||
| 282 | - ]) | ||
| 283 | - | ||
| 284 | dialogVisible.value = true | 212 | dialogVisible.value = true |
| 285 | } | 213 | } |
| 286 | -//处理编辑弹窗 | ||
| 287 | -const handleEdit = async (row: any) => { | 214 | + |
| 215 | +// ===================== 编辑 ===================== | ||
| 216 | +const handleEdit = async (row) => { | ||
| 288 | try { | 217 | try { |
| 289 | isEdit.value = true | 218 | isEdit.value = true |
| 290 | dialogTitle.value = '编辑超级组' | 219 | dialogTitle.value = '编辑超级组' |
| 291 | resetForm() | 220 | resetForm() |
| 292 | - await Promise.all([ | ||
| 293 | - loadExercisesList(), | ||
| 294 | - // loadCategoryList(), | ||
| 295 | - loadMuscleList() | ||
| 296 | - ]) | ||
| 297 | const res = await SupersetsApi.getSupersets(row.id) | 221 | const res = await SupersetsApi.getSupersets(row.id) |
| 298 | - // | ||
| 299 | const detail = res.data || res | 222 | const detail = res.data || res |
| 300 | - console.log('detail单个超级组详情接口', detail) | ||
| 301 | - console.log('res.data单个超级组详情接口', res.data) | ||
| 302 | - const exerciseIds = (detail.exercises || []).map((item: any) => Number(item.id)) | ||
| 303 | - console.log('提取的动作ID数组', exerciseIds) // 验证是否正确提取 | ||
| 304 | - //回显 | ||
| 305 | - console.log('exerciseIds传递的参数', exerciseIds) | 223 | + detailExercises.value = detail.exercises || [] |
| 224 | + const exerciseIds = (detail.exercises || []).map((item) => Number(item.id)) | ||
| 306 | Object.assign(formData, { | 225 | Object.assign(formData, { |
| 307 | id: detail.id, | 226 | id: detail.id, |
| 308 | name: detail.name || '', | 227 | name: detail.name || '', |
| 309 | - exerciseIds: exerciseIds || [], | 228 | + exerciseIds |
| 310 | }) | 229 | }) |
| 311 | dialogVisible.value = true | 230 | dialogVisible.value = true |
| 312 | } catch (err) { | 231 | } catch (err) { |
| 313 | - console.error('获取超级组详情失败:', err) | ||
| 314 | ElMessage.error('加载编辑数据失败') | 232 | ElMessage.error('加载编辑数据失败') |
| 315 | } | 233 | } |
| 316 | } | 234 | } |
| 317 | 235 | ||
| 318 | -const handleDelete = async (row: any) => { | 236 | +// ===================== 删除 ===================== |
| 237 | +const handleDelete = async (row) => { | ||
| 319 | try { | 238 | try { |
| 320 | await ElMessageBox.confirm(`确定要删除超级组「${row.name}」吗?`, '提示', { | 239 | await ElMessageBox.confirm(`确定要删除超级组「${row.name}」吗?`, '提示', { |
| 321 | confirmButtonText: '确定', | 240 | confirmButtonText: '确定', |
| @@ -325,221 +244,69 @@ const handleDelete = async (row: any) => { | @@ -325,221 +244,69 @@ const handleDelete = async (row: any) => { | ||
| 325 | await SupersetsApi.deleteSupersets(row.id) | 244 | await SupersetsApi.deleteSupersets(row.id) |
| 326 | ElMessage.success('删除成功') | 245 | ElMessage.success('删除成功') |
| 327 | getList() | 246 | getList() |
| 328 | - } catch (err: any) { | 247 | + } catch (err) { |
| 329 | if (err !== 'cancel') { | 248 | if (err !== 'cancel') { |
| 330 | - ElMessage.error(`删除失败:${err.message || '服务器错误'}`) | ||
| 331 | - console.error(err) | 249 | + ElMessage.error('删除失败') |
| 332 | } | 250 | } |
| 333 | } | 251 | } |
| 334 | } | 252 | } |
| 335 | 253 | ||
| 254 | +// ===================== 表单操作 ===================== | ||
| 336 | const resetForm = () => { | 255 | const resetForm = () => { |
| 337 | if (formRef.value) { | 256 | if (formRef.value) { |
| 338 | formRef.value.resetFields() | 257 | formRef.value.resetFields() |
| 339 | } | 258 | } |
| 340 | - Object.assign(formData, { | ||
| 341 | - id: 0, | ||
| 342 | - name: '', | ||
| 343 | - // primaryMuscles: [], | ||
| 344 | - // secondaryMuscles: [], | ||
| 345 | - exerciseIds: [], | ||
| 346 | - // category: '' | ||
| 347 | - }) | 259 | + detailExercises.value = [] |
| 260 | + Object.assign(formData, { id: 0, name: '', exerciseIds: [] }) | ||
| 348 | } | 261 | } |
| 349 | 262 | ||
| 350 | -const submitAddForm = async () => { | 263 | +const submitForm = async () => { |
| 351 | if (!formRef.value) return | 264 | if (!formRef.value) return |
| 352 | try { | 265 | try { |
| 353 | - await formRef.value.clearValidate() | ||
| 354 | - const valid = await formRef.value.validate() | ||
| 355 | - if (!valid) return | ||
| 356 | - await SupersetsApi.createSupersets({ | ||
| 357 | - id: 0, | ||
| 358 | - name: formData.name, | ||
| 359 | - exerciseIds: formData.exerciseIds, | ||
| 360 | - }) | ||
| 361 | - ElMessage.success('新增超级组成功!') | ||
| 362 | - dialogVisible.value = false | ||
| 363 | - getList() | ||
| 364 | - } catch (err: any) { | ||
| 365 | - console.error('新增超级组失败:', err) | ||
| 366 | - ElMessage.error(`新增失败:${err.response?.data?.msg || err.message || '服务器错误'}`) | 266 | + await formRef.value.validate() |
| 267 | + } catch { | ||
| 268 | + return | ||
| 367 | } | 269 | } |
| 368 | -} | ||
| 369 | - | ||
| 370 | -const submitEditForm = async () => { | ||
| 371 | - if (!formRef.value) return | ||
| 372 | try { | 270 | try { |
| 373 | - await formRef.value.clearValidate() | ||
| 374 | - const valid = await formRef.value.validate() | ||
| 375 | - if (!valid) return | ||
| 376 | - await SupersetsApi.updateSupersets({ | 271 | + const payload = { |
| 377 | id: formData.id, | 272 | id: formData.id, |
| 378 | name: formData.name, | 273 | name: formData.name, |
| 379 | - exerciseIds: formData.exerciseIds, | ||
| 380 | - }) | ||
| 381 | - | 274 | + exerciseIds: formData.exerciseIds |
| 275 | + } | ||
| 276 | + if (isEdit.value) { | ||
| 277 | + await SupersetsApi.updateSupersets(payload) | ||
| 382 | ElMessage.success('修改超级组成功!') | 278 | ElMessage.success('修改超级组成功!') |
| 279 | + } else { | ||
| 280 | + await SupersetsApi.createSupersets(payload) | ||
| 281 | + ElMessage.success('新增超级组成功!') | ||
| 282 | + } | ||
| 383 | dialogVisible.value = false | 283 | dialogVisible.value = false |
| 384 | getList() | 284 | getList() |
| 385 | - } catch (err: any) { | ||
| 386 | - console.error('修改超级组失败:', err) | ||
| 387 | - ElMessage.error(`修改失败:${err.response?.data?.msg || err.message || '服务器错误'}`) | 285 | + } catch (err) { |
| 286 | + const msg = err?.response?.data?.msg || err?.message | ||
| 287 | + if (msg && typeof msg !== 'object') { | ||
| 288 | + ElMessage.error(msg) | ||
| 289 | + } | ||
| 388 | } | 290 | } |
| 389 | } | 291 | } |
| 390 | 292 | ||
| 293 | +// ===================== 弹窗关闭时重置表单 ===================== | ||
| 294 | +watch(dialogVisible, (val) => { | ||
| 295 | + if (!val) resetForm() | ||
| 296 | +}) | ||
| 297 | + | ||
| 298 | +// ===================== 初始化 ===================== | ||
| 391 | onMounted(async () => { | 299 | onMounted(async () => { |
| 392 | - await Promise.all([ | ||
| 393 | - loadExercisesList(), | ||
| 394 | - loadMuscleList() | ||
| 395 | - ]) | 300 | + await loadExercisesList() |
| 396 | getList() | 301 | getList() |
| 397 | }) | 302 | }) |
| 398 | </script> | 303 | </script> |
| 399 | 304 | ||
| 400 | <style scoped> | 305 | <style scoped> |
| 401 | .supersets-page { | 306 | .supersets-page { |
| 402 | - min-height: 100vh; | ||
| 403 | padding: 20px; | 307 | padding: 20px; |
| 404 | - overflow-x: hidden; | 308 | + min-height: 100vh; |
| 405 | background-color: #f5f7fa; | 309 | background-color: #f5f7fa; |
| 406 | box-sizing: border-box; | 310 | box-sizing: border-box; |
| 407 | } | 311 | } |
| 408 | - | ||
| 409 | -.plan-search-bar { | ||
| 410 | - margin-bottom: 20px; | ||
| 411 | - box-shadow: 0 1px 2px rgb(0 0 0 / 5%); | ||
| 412 | -} | ||
| 413 | - | ||
| 414 | -/* .search-bar { | ||
| 415 | - display: flex; | ||
| 416 | - align-items: center; | ||
| 417 | - padding: 16px; | ||
| 418 | - margin-bottom: 20px; | ||
| 419 | - background-color: #fff; | ||
| 420 | - border-radius: 4px; | ||
| 421 | - gap: 10px; | ||
| 422 | - flex-wrap: nowrap; | ||
| 423 | -} */ | ||
| 424 | - | ||
| 425 | -.search-item { | ||
| 426 | - display: flex; | ||
| 427 | - align-items: center; | ||
| 428 | - gap: 10px; | ||
| 429 | - padding: 15px 20px; | ||
| 430 | -} | ||
| 431 | - | ||
| 432 | -.search-label { | ||
| 433 | - font-size: 14px; | ||
| 434 | - color: #606266; | ||
| 435 | - white-space: nowrap; | ||
| 436 | -} | ||
| 437 | - | ||
| 438 | -.search-input { | ||
| 439 | - width: 220px; | ||
| 440 | - flex: 0 0 auto; | ||
| 441 | -} | ||
| 442 | - | ||
| 443 | -.search-buttons { | ||
| 444 | - display: flex; | ||
| 445 | - gap: 10px; | ||
| 446 | - flex-shrink: 0; | ||
| 447 | - margin: 0; | ||
| 448 | -} | ||
| 449 | - | ||
| 450 | -.fixed-btn { | ||
| 451 | - display: flex; | ||
| 452 | - width: 120px; | ||
| 453 | - padding: 8px 0 !important; | ||
| 454 | - justify-content: center; | ||
| 455 | - align-items: center; | ||
| 456 | -} | ||
| 457 | - | ||
| 458 | -.cover-img { | ||
| 459 | - display: block; | ||
| 460 | - width: 100px; | ||
| 461 | - height: 60px; | ||
| 462 | - margin: 0 auto; | ||
| 463 | - background-color: #f5f5f5; | ||
| 464 | - border: 1px solid #e4e7ed; | ||
| 465 | - border-radius: 15px; | ||
| 466 | - object-fit: cover; | ||
| 467 | -} | ||
| 468 | - | ||
| 469 | -.edit-btn { | ||
| 470 | - margin-right: 15px; | ||
| 471 | - color: #409eff; | ||
| 472 | -} | ||
| 473 | - | ||
| 474 | -.delete-btn { | ||
| 475 | - color: #f56c6c; | ||
| 476 | -} | ||
| 477 | - | ||
| 478 | -.table-container { | ||
| 479 | - padding: 0; | ||
| 480 | - margin-bottom: 20px; | ||
| 481 | - background-color: #fff; | ||
| 482 | - border-radius: 4px; | ||
| 483 | -} | ||
| 484 | - | ||
| 485 | -.pagination-container { | ||
| 486 | - display: flex; | ||
| 487 | - align-items: center; | ||
| 488 | - justify-content: flex-end; | ||
| 489 | - gap: 10px; | ||
| 490 | - padding: 16px; | ||
| 491 | - background-color: #fff; | ||
| 492 | - border-radius: 4px; | ||
| 493 | -} | ||
| 494 | - | ||
| 495 | -.total { | ||
| 496 | - font-size: 14px; | ||
| 497 | - color: #606266; | ||
| 498 | -} | ||
| 499 | - | ||
| 500 | -.page-size-select { | ||
| 501 | - width: 120px; | ||
| 502 | -} | ||
| 503 | - | ||
| 504 | -:deep(.el-table) { | ||
| 505 | - max-height: calc(100vh - 280px); | ||
| 506 | - overflow-y: auto; | ||
| 507 | -} | ||
| 508 | - | ||
| 509 | -:deep(.supersets-dialog .el-dialog__header) { | ||
| 510 | - padding: 16px 20px; | ||
| 511 | - border-bottom: 1px solid #e6e6e6; | ||
| 512 | -} | ||
| 513 | - | ||
| 514 | -:deep(.supersets-dialog .el-dialog__title) { | ||
| 515 | - font-size: 16px; | ||
| 516 | - color: #333; | ||
| 517 | -} | ||
| 518 | - | ||
| 519 | -:deep(.supersets-dialog .el-dialog__body) { | ||
| 520 | - padding: 24px 20px; | ||
| 521 | -} | ||
| 522 | - | ||
| 523 | -.add-form { | ||
| 524 | - max-width: 100%; | ||
| 525 | -} | ||
| 526 | - | ||
| 527 | -:deep(.add-form .el-form-item__label) { | ||
| 528 | - font-weight: 500; | ||
| 529 | - color: #333; | ||
| 530 | -} | ||
| 531 | - | ||
| 532 | -:deep(.add-form .el-input__wrapper), | ||
| 533 | -:deep(.add-form .el-select__wrapper) { | ||
| 534 | - background-color: #f5f7fa; | ||
| 535 | - border-radius: 4px; | ||
| 536 | -} | ||
| 537 | - | ||
| 538 | -.dialog-footer { | ||
| 539 | - display: flex; | ||
| 540 | - justify-content: center; | ||
| 541 | - gap: 16px; | ||
| 542 | - padding-top: 16px; | ||
| 543 | - border-top: 1px solid #e6e6e6; | ||
| 544 | -} | ||
| 545 | </style> | 312 | </style> |
| 1 | <!-- 原型图叫文件夹,接口命名为模板大类 --> | 1 | <!-- 原型图叫文件夹,接口命名为模板大类 --> |
| 2 | <template> | 2 | <template> |
| 3 | <div class="page-container"> | 3 | <div class="page-container"> |
| 4 | - <!-- 搜索框区域 --> | ||
| 5 | - <!-- <div class="equipments-page"> --> | ||
| 6 | - <ContentWrap class="plan-search-bar"> | ||
| 7 | <!-- 搜索栏 --> | 4 | <!-- 搜索栏 --> |
| 8 | - <div class="search-bar"> | ||
| 9 | - <label class="search-label">文件夹名称</label> | ||
| 10 | - <el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default" | ||
| 11 | - @keyup.enter="handleSearch" /> | ||
| 12 | - <div class="search-buttons"> | ||
| 13 | - <el-button type="primary" @click="handleSearch" size="default"> | ||
| 14 | - <el-icon> | ||
| 15 | - <Search /> | ||
| 16 | - </el-icon> | ||
| 17 | - 搜索 | 5 | + <ContentWrap class="plan-search-bar"> |
| 6 | + <el-form :inline="true" :model="queryParams"> | ||
| 7 | + <el-form-item label="文件夹名称" prop="name"> | ||
| 8 | + <el-input | ||
| 9 | + v-model="queryParams.name" | ||
| 10 | + placeholder="请输入" | ||
| 11 | + clearable | ||
| 12 | + class="!w-220px" | ||
| 13 | + @keyup.enter="handleSearch" | ||
| 14 | + /> | ||
| 15 | + </el-form-item> | ||
| 16 | + <el-form-item> | ||
| 17 | + <el-button type="primary" @click="handleSearch"> | ||
| 18 | + <Icon icon="ep:search" class="mr-5px" />搜索 | ||
| 18 | </el-button> | 19 | </el-button> |
| 19 | - <el-button @click="resetSearch" size="default"> | ||
| 20 | - <el-icon> | ||
| 21 | - <Refresh /> | ||
| 22 | - </el-icon> | ||
| 23 | - 重置 | 20 | + <el-button @click="resetSearch"> |
| 21 | + <Icon icon="ep:refresh" class="mr-5px" />重置 | ||
| 24 | </el-button> | 22 | </el-button> |
| 25 | - <el-button type="primary" @click="handleAdd" size="default"> | ||
| 26 | - <el-icon> | ||
| 27 | - <Plus /> | ||
| 28 | - </el-icon> | ||
| 29 | - 新增 | 23 | + <el-button type="primary" @click="handleAdd"> |
| 24 | + <Icon icon="ep:plus" class="mr-5px" />新增 | ||
| 30 | </el-button> | 25 | </el-button> |
| 31 | - </div> | ||
| 32 | - </div> | 26 | + </el-form-item> |
| 27 | + </el-form> | ||
| 33 | </ContentWrap> | 28 | </ContentWrap> |
| 29 | + | ||
| 34 | <!-- 表格区域 --> | 30 | <!-- 表格区域 --> |
| 35 | <ContentWrap> | 31 | <ContentWrap> |
| 36 | - <div class="table-container"> | ||
| 37 | - <el-table :data="tableData" v-loading="loading" style="width: 100%" empty-text="暂无数据" stripe> | 32 | + <el-table |
| 33 | + :data="tableData" | ||
| 34 | + v-loading="loading" | ||
| 35 | + empty-text="暂无数据" | ||
| 36 | + stripe | ||
| 37 | + > | ||
| 38 | <el-table-column prop="name" label="文件夹名字" align="center" /> | 38 | <el-table-column prop="name" label="文件夹名字" align="center" /> |
| 39 | <el-table-column prop="cover" label="封面" align="center" min-width="120"> | 39 | <el-table-column prop="cover" label="封面" align="center" min-width="120"> |
| 40 | <template #default="scope"> | 40 | <template #default="scope"> |
| 41 | - <div class="cover-wrapper"> | ||
| 42 | - <!-- 优先显示行数据封面,无则显示占位图 --> | ||
| 43 | <img class="cover-img" :src="scope.row.urlCover || lostImg" alt="模板封面" /> | 41 | <img class="cover-img" :src="scope.row.urlCover || lostImg" alt="模板封面" /> |
| 44 | - </div> | ||
| 45 | </template> | 42 | </template> |
| 46 | </el-table-column> | 43 | </el-table-column> |
| 47 | <el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" /> | 44 | <el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" /> |
| 48 | <el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" /> | 45 | <el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" /> |
| 49 | <el-table-column label="操作" align="center" width="200"> | 46 | <el-table-column label="操作" align="center" width="200"> |
| 50 | <template #default="scope"> | 47 | <template #default="scope"> |
| 51 | - <el-button type="text" class="edit-btn" @click="handleEdit(scope.row)">编辑</el-button> | ||
| 52 | - <el-button type="text" class="delete-btn" @click="handleDelete(scope.row.id)">删除</el-button> | 48 | + <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button> |
| 49 | + <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button> | ||
| 53 | </template> | 50 | </template> |
| 54 | </el-table-column> | 51 | </el-table-column> |
| 55 | </el-table> | 52 | </el-table> |
| 56 | - </div> | ||
| 57 | 53 | ||
| 58 | - <div class="pagination-wrapper" style="padding: 12px 16px;"> | ||
| 59 | - <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" | ||
| 60 | - :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" | ||
| 61 | - style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> | ||
| 62 | - </div> | 54 | + <Pagination |
| 55 | + :total="queryParams.total" | ||
| 56 | + v-model:page="queryParams.pageNo" | ||
| 57 | + v-model:limit="queryParams.pageSize" | ||
| 58 | + @pagination="getList" | ||
| 59 | + /> | ||
| 63 | </ContentWrap> | 60 | </ContentWrap> |
| 64 | 61 | ||
| 65 | <!-- 编辑/新增弹窗 --> | 62 | <!-- 编辑/新增弹窗 --> |
| 66 | - <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px"> | 63 | + <Dialog :title="dialogTitle" v-model="dialogVisible" width="700px"> |
| 67 | <el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px"> | 64 | <el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px"> |
| 68 | - <el-form-item label="大类分类名称:" prop="name" label-suffix="*"> | ||
| 69 | - <el-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit style="width: 100%" /> | 65 | + <el-form-item label="大类分类名称:" prop="name"> |
| 66 | + <el-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit /> | ||
| 70 | </el-form-item> | 67 | </el-form-item> |
| 71 | <el-form-item label="模板" prop="templateIds"> | 68 | <el-form-item label="模板" prop="templateIds"> |
| 72 | - <el-select v-model="formData.templateIds" placeholder="请选择模板" multiple clearable style="width: 100%" | ||
| 73 | - filterable> | 69 | + <el-select |
| 70 | + v-model="formData.templateIds" | ||
| 71 | + placeholder="请选择模板" | ||
| 72 | + multiple | ||
| 73 | + clearable | ||
| 74 | + filterable | ||
| 75 | + style="width: 100%" | ||
| 76 | + > | ||
| 74 | <el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" /> | 77 | <el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" /> |
| 75 | </el-select> | 78 | </el-select> |
| 76 | </el-form-item> | 79 | </el-form-item> |
| 77 | - <el-form-item label="封面" prop="urlCover" label-suffix="*"> | ||
| 78 | - <UploadImg v-model="formData.urlCover" placeholder="封面" maxlength="50" show-word-limit style="width: 100%" /> | 80 | + <el-form-item label="封面" prop="urlCover"> |
| 81 | + <UploadImg v-model="formData.urlCover" placeholder="封面" /> | ||
| 79 | </el-form-item> | 82 | </el-form-item> |
| 80 | - <el-form-item label="模板大类介绍" prop="description" label-suffix="*"> | ||
| 81 | - <el-input v-model="formData.description" placeholder="请输入模板大类介绍" maxlength="50" show-word-limit | ||
| 82 | - style="width: 100%" /> | 83 | + <el-form-item label="模板大类介绍" prop="description"> |
| 84 | + <el-input | ||
| 85 | + v-model="formData.description" | ||
| 86 | + type="textarea" | ||
| 87 | + placeholder="请输入模板大类介绍" | ||
| 88 | + maxlength="50" | ||
| 89 | + show-word-limit | ||
| 90 | + /> | ||
| 83 | </el-form-item> | 91 | </el-form-item> |
| 84 | </el-form> | 92 | </el-form> |
| 85 | <template #footer> | 93 | <template #footer> |
| 86 | <el-button @click="dialogVisible = false">取 消</el-button> | 94 | <el-button @click="dialogVisible = false">取 消</el-button> |
| 87 | - <el-button type="primary" @click="submitForm" :loading="loading" :disabled="loading">确 定</el-button> | 95 | + <el-button type="primary" @click="submitForm" :loading="loading">确 定</el-button> |
| 88 | </template> | 96 | </template> |
| 89 | - </el-dialog> | ||
| 90 | - | ||
| 91 | - | ||
| 92 | - <!-- 分页组件 --> | ||
| 93 | - | ||
| 94 | - <!-- </div> --> | ||
| 95 | - | 97 | + </Dialog> |
| 96 | </div> | 98 | </div> |
| 97 | </template> | 99 | </template> |
| 98 | 100 | ||
| 99 | -<script setup lang="ts"> | ||
| 100 | -import { ref, onMounted, reactive } from 'vue' | ||
| 101 | -import { Plus, Search, Refresh } from '@element-plus/icons-vue' | 101 | +<script setup> |
| 102 | +import { ref, reactive, onMounted, watch } from 'vue' | ||
| 102 | import { dateFormatter } from '@/utils/formatTime' | 103 | import { dateFormatter } from '@/utils/formatTime' |
| 103 | import { templateCategorysApi } from '@/api/store/training/templateCategorys' | 104 | import { templateCategorysApi } from '@/api/store/training/templateCategorys' |
| 104 | import { TrainingTemplatesApi } from '@/api/store/training/templates' | 105 | import { TrainingTemplatesApi } from '@/api/store/training/templates' |
| 105 | import lostImg from '@/assets/imgs/lost.png' | 106 | import lostImg from '@/assets/imgs/lost.png' |
| 106 | import { ElMessage, ElMessageBox } from 'element-plus' | 107 | import { ElMessage, ElMessageBox } from 'element-plus' |
| 107 | -const tableData = ref<any[]>([]) | 108 | + |
| 109 | +// ===================== 列表状态 ===================== | ||
| 110 | +const tableData = ref([]) | ||
| 108 | const loading = ref(false) | 111 | const loading = ref(false) |
| 112 | +const queryParams = reactive({ | ||
| 113 | + pageNo: 1, | ||
| 114 | + pageSize: 10, | ||
| 115 | + total: 0, | ||
| 116 | + name: '' | ||
| 117 | +}) | ||
| 118 | + | ||
| 119 | +// ===================== 弹窗状态 ===================== | ||
| 109 | const isEdit = ref(true) | 120 | const isEdit = ref(true) |
| 110 | const dialogVisible = ref(false) | 121 | const dialogVisible = ref(false) |
| 111 | const dialogTitle = ref('') | 122 | const dialogTitle = ref('') |
| 112 | const formRef = ref() | 123 | const formRef = ref() |
| 113 | -const templateList = ref<any[]>([]) | 124 | +const templateList = ref([]) |
| 114 | 125 | ||
| 115 | const formRules = reactive({ | 126 | const formRules = reactive({ |
| 116 | - name: [ | ||
| 117 | - { required: true, message: '请输入大类分类名称', trigger: 'blur' } | ||
| 118 | - ] | 127 | + name: [{ required: true, message: '请输入大类分类名称', trigger: 'blur' }], |
| 128 | + templateIds: [{ required: true, message: '请选择模板', trigger: 'change' }], | ||
| 129 | + urlCover: [{ required: true, message: '请上传封面', trigger: 'change' }], | ||
| 130 | + description: [{ required: true, message: '请输入模板大类介绍', trigger: 'blur' }] | ||
| 119 | }) | 131 | }) |
| 120 | 132 | ||
| 121 | -const loadtemplateList = async () => { | ||
| 122 | - try { | ||
| 123 | - const res = await TrainingTemplatesApi.getTemplateList() | ||
| 124 | - | ||
| 125 | - templateList.value = res || [] | ||
| 126 | - } catch (err) { | ||
| 127 | - console.error('加载模板列表失败:', err) | ||
| 128 | - ElMessage.error('加载模板列表失败') | ||
| 129 | - } | ||
| 130 | -} | ||
| 131 | -// 弹窗参数 | ||
| 132 | const formData = reactive({ | 133 | const formData = reactive({ |
| 133 | id: 0, | 134 | id: 0, |
| 134 | name: '', | 135 | name: '', |
| 135 | urlCover: '', | 136 | urlCover: '', |
| 136 | description: '', | 137 | description: '', |
| 137 | - // 下拉框 | ||
| 138 | - templateIds: [] as number[] | 138 | + templateIds: [] |
| 139 | }) | 139 | }) |
| 140 | -const searchForm = reactive({ | ||
| 141 | - name: '', | ||
| 142 | -}) | ||
| 143 | - | ||
| 144 | -// 分页参数 | ||
| 145 | -const pagination = reactive({ | ||
| 146 | - pageNo: 1, | ||
| 147 | - pageSize: 10, | ||
| 148 | - total: 0 | ||
| 149 | 140 | ||
| 150 | -}) | 141 | +// ===================== 模板下拉列表 ===================== |
| 142 | +const loadTemplateList = async () => { | ||
| 143 | + try { | ||
| 144 | + const res = await TrainingTemplatesApi.getTemplateList(false) | ||
| 145 | + templateList.value = res || [] | ||
| 146 | + } catch { | ||
| 147 | + ElMessage.error('加载模板列表失败') | ||
| 148 | + } | ||
| 149 | +} | ||
| 151 | 150 | ||
| 152 | -//处理搜索 | 151 | +// ===================== 搜索与重置 ===================== |
| 153 | const handleSearch = () => { | 152 | const handleSearch = () => { |
| 154 | - pagination.pageNo = 1 // 搜索时重置到第一页 | 153 | + queryParams.pageNo = 1 |
| 155 | getList() | 154 | getList() |
| 156 | } | 155 | } |
| 157 | -// 重置搜索条件 | 156 | + |
| 158 | const resetSearch = () => { | 157 | const resetSearch = () => { |
| 159 | - searchForm.name = '' | ||
| 160 | - pagination.pageNo = 1 | ||
| 161 | - pagination.pageSize = 10 | 158 | + queryParams.name = '' |
| 159 | + queryParams.pageNo = 1 | ||
| 160 | + queryParams.pageSize = 10 | ||
| 162 | getList() | 161 | getList() |
| 163 | } | 162 | } |
| 164 | 163 | ||
| 165 | -// 获取列表 | 164 | +// ===================== 列表数据 ===================== |
| 166 | const getList = async () => { | 165 | const getList = async () => { |
| 167 | loading.value = true | 166 | loading.value = true |
| 168 | try { | 167 | try { |
| 169 | - // 搜索条件 | ||
| 170 | const params = { | 168 | const params = { |
| 171 | - pageNo: pagination.pageNo + '', | ||
| 172 | - pageSize: pagination.pageSize + '', | ||
| 173 | - name: searchForm.name, | 169 | + pageNo: queryParams.pageNo + '', |
| 170 | + pageSize: queryParams.pageSize + '', | ||
| 171 | + name: queryParams.name | ||
| 174 | } | 172 | } |
| 175 | - // | ||
| 176 | const res = await templateCategorysApi.getTemplateCategorysPage(params) | 173 | const res = await templateCategorysApi.getTemplateCategorysPage(params) |
| 177 | - console.log('模板大类分页接口返回数据:', res) | ||
| 178 | tableData.value = res.list || [] | 174 | tableData.value = res.list || [] |
| 179 | - pagination.total = res.total || 0 | ||
| 180 | - console.log('分页列表', res.list) | ||
| 181 | - } catch (err) { | ||
| 182 | - console.error('❌ 请求失败:', err) | 175 | + queryParams.total = res.total || 0 |
| 176 | + } catch { | ||
| 183 | ElMessage.error('请求失败,请检查网络或接口') | 177 | ElMessage.error('请求失败,请检查网络或接口') |
| 184 | } finally { | 178 | } finally { |
| 185 | loading.value = false | 179 | loading.value = false |
| 186 | } | 180 | } |
| 187 | } | 181 | } |
| 188 | 182 | ||
| 183 | +// ===================== 表单重置 ===================== | ||
| 189 | const resetForm = () => { | 184 | const resetForm = () => { |
| 190 | formData.id = 0 | 185 | formData.id = 0 |
| 191 | formData.name = '' | 186 | formData.name = '' |
| 192 | - formData.description = '' | ||
| 193 | formData.urlCover = '' | 187 | formData.urlCover = '' |
| 188 | + formData.description = '' | ||
| 194 | formData.templateIds = [] | 189 | formData.templateIds = [] |
| 195 | - | ||
| 196 | } | 190 | } |
| 197 | 191 | ||
| 198 | -//=======================处理新增================== | 192 | +// ===================== 新增 ===================== |
| 199 | const handleAdd = () => { | 193 | const handleAdd = () => { |
| 200 | isEdit.value = false | 194 | isEdit.value = false |
| 201 | - dialogVisible.value = true | ||
| 202 | dialogTitle.value = '新增模板大类' | 195 | dialogTitle.value = '新增模板大类' |
| 203 | - resetForm() // 调用重置函数清空表单 | 196 | + resetForm() |
| 197 | + dialogVisible.value = true | ||
| 204 | } | 198 | } |
| 205 | 199 | ||
| 206 | - | ||
| 207 | -//=====================处理编辑================= | ||
| 208 | -const handleEdit = async (row: any) => { | 200 | +// ===================== 编辑 ===================== |
| 201 | +const handleEdit = async (row) => { | ||
| 209 | isEdit.value = true | 202 | isEdit.value = true |
| 210 | dialogTitle.value = '编辑模板大类' | 203 | dialogTitle.value = '编辑模板大类' |
| 211 | - // 先确保模板列表已加载 | ||
| 212 | if (templateList.value.length === 0) { | 204 | if (templateList.value.length === 0) { |
| 213 | - await loadtemplateList() | 205 | + await loadTemplateList() |
| 214 | } | 206 | } |
| 215 | - // 表单回显 | ||
| 216 | formData.id = row.id | 207 | formData.id = row.id |
| 217 | formData.name = row.name | 208 | formData.name = row.name |
| 218 | formData.urlCover = row.urlCover || '' | 209 | formData.urlCover = row.urlCover || '' |
| 219 | formData.description = row.description || '' | 210 | formData.description = row.description || '' |
| 220 | formData.templateIds = row.templateIds || [] | 211 | formData.templateIds = row.templateIds || [] |
| 221 | - console.log('编辑回显数据:', formData) | ||
| 222 | - console.log('编辑行的ids', row.templateIds) | ||
| 223 | - console.log('被输入编辑行的ids', formData.templateIds) | ||
| 224 | - // 新增:打开编辑弹窗时,清空之前的验证状态 | ||
| 225 | - if (formRef.value) { | ||
| 226 | - formRef.value.clearValidate() | ||
| 227 | - } | ||
| 228 | dialogVisible.value = true | 212 | dialogVisible.value = true |
| 229 | } | 213 | } |
| 230 | -//=====================提交新增/编辑表单================= | 214 | + |
| 215 | +// ===================== 弹窗关闭时清理校验 ===================== | ||
| 216 | +watch(dialogVisible, (val) => { | ||
| 217 | + if (!val && formRef.value) { | ||
| 218 | + formRef.value.clearValidate() | ||
| 219 | + } | ||
| 220 | +}) | ||
| 221 | + | ||
| 222 | +// ===================== 提交 ===================== | ||
| 231 | const submitForm = async () => { | 223 | const submitForm = async () => { |
| 224 | + if (!formRef.value) return | ||
| 232 | try { | 225 | try { |
| 233 | - // 表单验证 | ||
| 234 | await formRef.value.validate() | 226 | await formRef.value.validate() |
| 227 | + } catch { | ||
| 228 | + return | ||
| 229 | + } | ||
| 235 | loading.value = true | 230 | loading.value = true |
| 236 | - if (isEdit.value) { | ||
| 237 | - // 编辑逻辑 | ||
| 238 | - const Editparams = { | 231 | + const payload = { |
| 239 | id: formData.id, | 232 | id: formData.id, |
| 240 | name: formData.name, | 233 | name: formData.name, |
| 241 | urlCover: formData.urlCover, | 234 | urlCover: formData.urlCover, |
| 242 | description: formData.description, | 235 | description: formData.description, |
| 243 | templateIds: formData.templateIds | 236 | templateIds: formData.templateIds |
| 244 | } | 237 | } |
| 245 | - await templateCategorysApi.updateTemplateCategorys(Editparams) | 238 | + try { |
| 239 | + if (isEdit.value) { | ||
| 240 | + await templateCategorysApi.updateTemplateCategorys(payload) | ||
| 246 | ElMessage.success('编辑成功') | 241 | ElMessage.success('编辑成功') |
| 247 | } else { | 242 | } else { |
| 248 | - // 新增逻辑(调用新增接口) | ||
| 249 | - const params = { | ||
| 250 | - id: formData.id, | ||
| 251 | - name: formData.name, | ||
| 252 | - urlCover: formData.urlCover, | ||
| 253 | - description: formData.description, | ||
| 254 | - templateIds: formData.templateIds | ||
| 255 | - } | ||
| 256 | - console.log('提交给后端的参数:', params) // 👈 加这行 | ||
| 257 | - const res = await templateCategorysApi.createTemplateCategorys(params) | ||
| 258 | - console.log('新增之后的数据', res) | 243 | + await templateCategorysApi.createTemplateCategorys(payload) |
| 259 | ElMessage.success('新增成功') | 244 | ElMessage.success('新增成功') |
| 260 | } | 245 | } |
| 261 | - } catch (error) { | ||
| 262 | - console.error('❌ 提交失败:', error) | 246 | + } catch { |
| 263 | ElMessage.error('提交失败,请检查网络或接口') | 247 | ElMessage.error('提交失败,请检查网络或接口') |
| 264 | } finally { | 248 | } finally { |
| 265 | loading.value = false | 249 | loading.value = false |
| 266 | dialogVisible.value = false | 250 | dialogVisible.value = false |
| 267 | - getList() // 刷新列表 | 251 | + getList() |
| 268 | } | 252 | } |
| 269 | } | 253 | } |
| 270 | 254 | ||
| 271 | -//处理删除 | ||
| 272 | -const handleDelete = async (id: number) => { | 255 | +// ===================== 删除 ===================== |
| 256 | +const handleDelete = async (id) => { | ||
| 273 | try { | 257 | try { |
| 274 | await ElMessageBox.confirm('确定要删除吗?', '提示', { | 258 | await ElMessageBox.confirm('确定要删除吗?', '提示', { |
| 275 | confirmButtonText: '确定', | 259 | confirmButtonText: '确定', |
| 276 | cancelButtonText: '取消', | 260 | cancelButtonText: '取消', |
| 277 | type: 'warning' | 261 | type: 'warning' |
| 278 | }) | 262 | }) |
| 279 | - //调用删除接口 | ||
| 280 | await templateCategorysApi.deleteTemplateCategorys(id) | 263 | await templateCategorysApi.deleteTemplateCategorys(id) |
| 281 | ElMessage.success('删除成功') | 264 | ElMessage.success('删除成功') |
| 282 | } catch (err) { | 265 | } catch (err) { |
| 283 | - console.error('❌ 请求失败:', err) | ||
| 284 | - ElMessage.error('请求失败,请检查网络或接口') | ||
| 285 | - } finally { | ||
| 286 | - getList() // 刷新列表 | 266 | + if (err !== 'cancel') { |
| 267 | + ElMessage.error('删除失败,请检查网络或接口') | ||
| 287 | } | 268 | } |
| 288 | -} | ||
| 289 | -// ===================分页方法================== | ||
| 290 | -const handleCurrentChange = (val: number) => { | ||
| 291 | - pagination.pageNo = val | 269 | + } finally { |
| 292 | getList() | 270 | getList() |
| 271 | + } | ||
| 293 | } | 272 | } |
| 294 | 273 | ||
| 295 | -// 处理分页大小变化 | ||
| 296 | -const handleSizeChange = (val: number) => { | ||
| 297 | - pagination.pageSize = val | ||
| 298 | - getList() | ||
| 299 | -} | 274 | +// ===================== 初始化 ===================== |
| 300 | onMounted(() => { | 275 | onMounted(() => { |
| 301 | getList() | 276 | getList() |
| 302 | - loadtemplateList() | 277 | + loadTemplateList() |
| 303 | }) | 278 | }) |
| 304 | </script> | 279 | </script> |
| 280 | + | ||
| 305 | <style scoped> | 281 | <style scoped> |
| 306 | .plan-search-bar { | 282 | .plan-search-bar { |
| 307 | margin-bottom: 20px; | 283 | margin-bottom: 20px; |
| 308 | box-shadow: 0 1px 2px rgb(0 0 0 / 5%); | 284 | box-shadow: 0 1px 2px rgb(0 0 0 / 5%); |
| 309 | } | 285 | } |
| 310 | 286 | ||
| 311 | -.search-bar { | ||
| 312 | - display: flex; | ||
| 313 | - align-items: center; | ||
| 314 | - gap: 10px; | ||
| 315 | - padding: 15px 20px; | ||
| 316 | - | ||
| 317 | -} | ||
| 318 | - | ||
| 319 | -.search-buttons { | ||
| 320 | - display: flex; | ||
| 321 | - gap: 10px; | ||
| 322 | - margin-left: 10px; | ||
| 323 | -} | ||
| 324 | - | ||
| 325 | -.pagination-wrapper { | ||
| 326 | - display: flex; | ||
| 327 | - padding: 12px 16px; | ||
| 328 | - justify-content: flex-end; | ||
| 329 | -} | ||
| 330 | - | ||
| 331 | -/* 新样式:更美观、不变大小、不影响其他布局 */ | ||
| 332 | .cover-img { | 287 | .cover-img { |
| 333 | width: 100px; | 288 | width: 100px; |
| 334 | - height: 50px !important; | ||
| 335 | - border: 1px solid #e5e7eb; | 289 | + height: 50px; |
| 290 | + border: 1px solid var(--el-border-color); | ||
| 336 | border-radius: 6px; | 291 | border-radius: 6px; |
| 337 | box-shadow: 0 1px 3px rgb(0 0 0 / 8%); | 292 | box-shadow: 0 1px 3px rgb(0 0 0 / 8%); |
| 338 | transition: transform 0.2s ease; | 293 | transition: transform 0.2s ease; |
| @@ -341,12 +296,5 @@ onMounted(() => { | @@ -341,12 +296,5 @@ onMounted(() => { | ||
| 341 | 296 | ||
| 342 | .cover-img:hover { | 297 | .cover-img:hover { |
| 343 | transform: scale(1.03); | 298 | transform: scale(1.03); |
| 344 | - | ||
| 345 | - /* 鼠标悬浮轻微放大 */ | ||
| 346 | -} | ||
| 347 | - | ||
| 348 | -.delete-btn { | ||
| 349 | - color: #f5222d; | ||
| 350 | - ; | ||
| 351 | } | 299 | } |
| 352 | </style> | 300 | </style> |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | 组数配置( | 4 | 组数配置( |
| 5 | <span v-if="type === 0">无氧运动</span> | 5 | <span v-if="type === 0">无氧运动</span> |
| 6 | <span v-else-if="type === 1">有氧运动</span> | 6 | <span v-else-if="type === 1">有氧运动</span> |
| 7 | - <span v-else-if="type === 2">计次训练</span> | 7 | + <span v-else-if="type === 2">记次训练</span> |
| 8 | <span v-else-if="type === 3">计时训练</span> | 8 | <span v-else-if="type === 3">计时训练</span> |
| 9 | <span v-else-if="type === 4">自重加重</span> | 9 | <span v-else-if="type === 4">自重加重</span> |
| 10 | <span v-else-if="type === 5">自重减重</span> | 10 | <span v-else-if="type === 5">自重减重</span> |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | <el-form ref="formRef" :model="formData" :rules="rules"> | 4 | <el-form ref="formRef" :model="formData" :rules="rules"> |
| 5 | <!-- 模板名称 --> | 5 | <!-- 模板名称 --> |
| 6 | <el-form-item label-suffix="*" prop="templateName" label="模板名称"> | 6 | <el-form-item label-suffix="*" prop="templateName" label="模板名称"> |
| 7 | - <el-input type="text" v-model="formData.templateName" placeholder="请输入模板名称" /> | 7 | + <el-input v-model="formData.templateName" placeholder="请输入模板名称" /> |
| 8 | </el-form-item> | 8 | </el-form-item> |
| 9 | <!-- 封面图 --> | 9 | <!-- 封面图 --> |
| 10 | <el-form-item label-suffix="*" prop="templateCover" label="封面图"> | 10 | <el-form-item label-suffix="*" prop="templateCover" label="封面图"> |
| @@ -12,11 +12,8 @@ | @@ -12,11 +12,8 @@ | ||
| 12 | </el-form-item> | 12 | </el-form-item> |
| 13 | <!-- 训练场景 --> | 13 | <!-- 训练场景 --> |
| 14 | <el-form-item label-suffix="*" prop="scene" label="训练场景"> | 14 | <el-form-item label-suffix="*" prop="scene" label="训练场景"> |
| 15 | - <el-select v-model="formData.scene" placeholder="请选择训练场景" style="width: 100%"> | ||
| 16 | - <el-option label="不限" :value="1" /> | ||
| 17 | - <el-option label="仅哑铃" :value="2" /> | ||
| 18 | - <el-option label="仅哑铃+杠铃" :value="3" /> | ||
| 19 | - <el-option label="健身房" :value="4" /> | 15 | + <el-select v-model="formData.scene" placeholder="请选择训练场景" class="w-full"> |
| 16 | + <el-option v-for="opt in SCENE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> | ||
| 20 | </el-select> | 17 | </el-select> |
| 21 | </el-form-item> | 18 | </el-form-item> |
| 22 | 19 | ||
| @@ -27,44 +24,52 @@ | @@ -27,44 +24,52 @@ | ||
| 27 | 24 | ||
| 28 | <!-- 动作组容器:循环渲染所有动作组 --> | 25 | <!-- 动作组容器:循环渲染所有动作组 --> |
| 29 | <div v-for="(unit, unitIndex) in formData.units" :key="unitIndex" class="unit-card"> | 26 | <div v-for="(unit, unitIndex) in formData.units" :key="unitIndex" class="unit-card"> |
| 30 | - <!-- 单元类型(动作组/超级组) --> | ||
| 31 | - <el-form-item label-suffix="*" :label="`单元${unitIndex + 1}类型`" :prop="`units[${unitIndex}].unitType`"> | ||
| 32 | - <el-select v-model="unit.unitType" placeholder="请选择单元类型" style="width:100%"> | ||
| 33 | - <el-option label="动作组" :value="1" /> | ||
| 34 | - <el-option label="超级组" :value="2" /> | ||
| 35 | - </el-select> | ||
| 36 | - </el-form-item> | ||
| 37 | - | ||
| 38 | - <!-- 这里实现选择超级组或者动作组名称 --> | ||
| 39 | - <el-form-item label-suffix="*" :label="unit.unitType === 1 ? '动作组名称' : '超级组名称'" class="row-form-item"> | ||
| 40 | - <div class="row-wrap"> | ||
| 41 | - <el-select v-model="unit.id" @change="(val) => onSelectItem(unitIndex, val)"> | ||
| 42 | - <template v-if="unit.unitType === 1"> | ||
| 43 | - <!-- 需要接入动作接口 --> | ||
| 44 | - <el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" /> | ||
| 45 | - </template> | ||
| 46 | - <!-- 需要接入超级组接口 --> | ||
| 47 | - <template v-else> | ||
| 48 | - <el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" /> | ||
| 49 | - </template> | ||
| 50 | - </el-select> | ||
| 51 | - <!-- 动作组头部 动作组名称+删除和增加动作组按钮--> | ||
| 52 | - <div class="unit-header"> | 27 | + <!-- 单元操作工具栏 --> |
| 28 | + <div class="unit-toolbar"> | ||
| 53 | <el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1"> | 29 | <el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1"> |
| 54 | <template #icon> | 30 | <template #icon> |
| 55 | <Minus /> | 31 | <Minus /> |
| 56 | </template> | 32 | </template> |
| 57 | 删除单元 | 33 | 删除单元 |
| 58 | </el-button> | 34 | </el-button> |
| 59 | - <el-button @click="AddUnit()"> | 35 | + <el-button type="primary" @click="addUnit()"> |
| 60 | <template #icon> | 36 | <template #icon> |
| 61 | <Plus /> | 37 | <Plus /> |
| 62 | </template> | 38 | </template> |
| 63 | 新增单元 | 39 | 新增单元 |
| 64 | </el-button> | 40 | </el-button> |
| 65 | </div> | 41 | </div> |
| 66 | - </div> | 42 | + |
| 43 | + <!-- 单元类型(动作组/超级组) --> | ||
| 44 | + <el-form-item label-suffix="*" :label="`单元${unitIndex + 1}类型`" :prop="`units[${unitIndex}].unitType`"> | ||
| 45 | + <el-select | ||
| 46 | + v-model="unit.unitType" | ||
| 47 | + placeholder="请选择单元类型" | ||
| 48 | + class="w-full" | ||
| 49 | + @change="onUnitTypeChange(unitIndex)" | ||
| 50 | + > | ||
| 51 | + <el-option label="动作组" :value="UNIT_TYPE.GROUP" /> | ||
| 52 | + <el-option label="超级组" :value="UNIT_TYPE.SUPERSET" /> | ||
| 53 | + </el-select> | ||
| 54 | + </el-form-item> | ||
| 55 | + | ||
| 56 | + <!-- 选择超级组或者动作组名称 --> | ||
| 57 | + <el-form-item label-suffix="*" :label="unit.unitType === UNIT_TYPE.GROUP ? '动作组名称' : '超级组名称'"> | ||
| 58 | + <el-select | ||
| 59 | + v-model="unit.id" | ||
| 60 | + :placeholder="unit.unitType === UNIT_TYPE.GROUP ? '请选择动作' : '请选择超级组'" | ||
| 61 | + class="w-full" | ||
| 62 | + @change="(val) => onSelectItem(unitIndex, val)" | ||
| 63 | + > | ||
| 64 | + <template v-if="unit.unitType === UNIT_TYPE.GROUP"> | ||
| 65 | + <el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" /> | ||
| 66 | + </template> | ||
| 67 | + <template v-else> | ||
| 68 | + <el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" /> | ||
| 69 | + </template> | ||
| 70 | + </el-select> | ||
| 67 | </el-form-item> | 71 | </el-form-item> |
| 72 | + | ||
| 68 | <!-- 排序 --> | 73 | <!-- 排序 --> |
| 69 | <el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`"> | 74 | <el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`"> |
| 70 | <el-input v-model="unit.sortOrder" min="1" max="99" /> | 75 | <el-input v-model="unit.sortOrder" min="1" max="99" /> |
| @@ -72,7 +77,7 @@ | @@ -72,7 +77,7 @@ | ||
| 72 | 77 | ||
| 73 | <!-- 循环:动作组 1 个、超级组 N 个,都会渲染 --> | 78 | <!-- 循环:动作组 1 个、超级组 N 个,都会渲染 --> |
| 74 | <template v-for="(item, idx) in unit.exercises" :key="idx"> | 79 | <template v-for="(item, idx) in unit.exercises" :key="idx"> |
| 75 | - <div style="margin: 16px 0;"> | 80 | + <div class="exercise-item"> |
| 76 | <div class="action-name">动作名称:{{ getExerciseName(item.exerciseId) }}</div> | 81 | <div class="action-name">动作名称:{{ getExerciseName(item.exerciseId) }}</div> |
| 77 | <ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" /> | 82 | <ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" /> |
| 78 | </div> | 83 | </div> |
| @@ -90,87 +95,87 @@ | @@ -90,87 +95,87 @@ | ||
| 90 | </div> | 95 | </div> |
| 91 | </div> | 96 | </div> |
| 92 | </template> | 97 | </template> |
| 93 | -<script setup lang="ts"> | ||
| 94 | -import { ref, onMounted, reactive, nextTick } from 'vue' | ||
| 95 | -import { MusclesApi } from '@/api/store/training/muscle' | 98 | +<script setup> |
| 99 | +import { ref, onMounted, reactive, nextTick, watch } from 'vue' | ||
| 96 | import { ElMessage, ElMessageBox } from 'element-plus' | 100 | import { ElMessage, ElMessageBox } from 'element-plus' |
| 97 | import { Minus, Plus } from '@element-plus/icons-vue' | 101 | import { Minus, Plus } from '@element-plus/icons-vue' |
| 98 | -import type { FormInstance } from 'element-plus' | ||
| 99 | -// import './TemplateAdd.css' | ||
| 100 | import { TrainingTemplatesApi } from '@/api/store/training/templates' | 102 | import { TrainingTemplatesApi } from '@/api/store/training/templates' |
| 101 | import { ExercisesApi } from '@/api/store/training/pose' | 103 | import { ExercisesApi } from '@/api/store/training/pose' |
| 102 | import ExerciseSetTable from '../templates/ExerciseSetTable.vue' | 104 | import ExerciseSetTable from '../templates/ExerciseSetTable.vue' |
| 103 | import { SupersetsApi } from '@/api/store/training/Supersets' | 105 | import { SupersetsApi } from '@/api/store/training/Supersets' |
| 104 | 106 | ||
| 105 | -//定义参数 | ||
| 106 | -const formRef = ref<FormInstance>() | ||
| 107 | -const muscleList = ref<any[]>([]) | 107 | +// ==================== 常量定义 ==================== |
| 108 | +const UNIT_TYPE = { GROUP: 1, SUPERSET: 2 } | ||
| 109 | + | ||
| 110 | +const EXERCISE_TYPE_MAP = { | ||
| 111 | + '无氧运动': 0, | ||
| 112 | + '有氧运动': 1, | ||
| 113 | + '记次训练': 2, | ||
| 114 | + '计时训练': 3, | ||
| 115 | + '自重加重': 4, | ||
| 116 | + '自重减重': 5, | ||
| 117 | + '间歇训练': 6 | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +const SCENE_OPTIONS = [ | ||
| 121 | + { label: '不限', value: 1 }, | ||
| 122 | + { label: '仅哑铃', value: 2 }, | ||
| 123 | + { label: '仅哑铃+杠铃', value: 3 }, | ||
| 124 | + { label: '健身房', value: 4 } | ||
| 125 | +] | ||
| 126 | + | ||
| 127 | +// ==================== 数据工厂函数 ==================== | ||
| 128 | +const createDefaultSet = () => ({ | ||
| 129 | + weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 | ||
| 130 | +}) | ||
| 131 | + | ||
| 132 | +const createDefaultExercise = () => ({ | ||
| 133 | + exerciseId: 0, exerciseType: 1, innerOrder: 1, sets: [createDefaultSet()] | ||
| 134 | +}) | ||
| 135 | + | ||
| 136 | +const createDefaultUnit = (sortOrder) => ({ | ||
| 137 | + id: null, name: '', unitType: UNIT_TYPE.GROUP, sortOrder, exercises: [createDefaultExercise()] | ||
| 138 | +}) | ||
| 139 | + | ||
| 140 | +// ==================== 响应式状态 ==================== | ||
| 141 | +const formRef = ref(null) | ||
| 108 | const loading = ref(false) | 142 | const loading = ref(false) |
| 109 | const router = useRouter() | 143 | const router = useRouter() |
| 110 | const route = useRoute() | 144 | const route = useRoute() |
| 111 | -const exercisesList = ref<any[]>([]) | ||
| 112 | -const superGroupList = ref<any[]>([]) | ||
| 113 | - | 145 | +const exercisesList = ref([]) |
| 146 | +const superGroupList = ref([]) | ||
| 114 | 147 | ||
| 115 | -// 表单数据 | ||
| 116 | const formData = reactive({ | 148 | const formData = reactive({ |
| 117 | - // templateId: 0, // 新增接口要求的根级别id字段 | ||
| 118 | id: 0, | 149 | id: 0, |
| 119 | groupId: null, | 150 | groupId: null, |
| 120 | templateName: '', | 151 | templateName: '', |
| 121 | scene: null, | 152 | scene: null, |
| 122 | templateCover: '', | 153 | templateCover: '', |
| 123 | templateIntroduction: '', | 154 | templateIntroduction: '', |
| 124 | - units: [ | ||
| 125 | - { | ||
| 126 | - id: 0, | ||
| 127 | - name: '', | ||
| 128 | - unitType: 1, //1=动作组,2=超级组 | ||
| 129 | - sortOrder: 1, | ||
| 130 | - exercises: [ | ||
| 131 | - { | ||
| 132 | - exerciseId: 0, | ||
| 133 | - exerciseType: 1, //动作类型 | ||
| 134 | - innerOrder: 1, | ||
| 135 | - sets: [ | ||
| 136 | - { | ||
| 137 | - weight: 0, | ||
| 138 | - reps: 0, | ||
| 139 | - distance: 0, | ||
| 140 | - duration: 0, // 接口唯一时间字段 | ||
| 141 | - restTime: 0, | ||
| 142 | - setIndex: 1 | ||
| 143 | - } | ||
| 144 | - ] | ||
| 145 | - } | ||
| 146 | - ] | ||
| 147 | - } | ||
| 148 | - ] | 155 | + units: [createDefaultUnit(1)] |
| 149 | }) | 156 | }) |
| 150 | 157 | ||
| 151 | -//静态表单验证规则,对应不是循环的表单项检验 | ||
| 152 | -const rules = ref({ | 158 | +const rules = reactive({ |
| 153 | templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], | 159 | templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], |
| 160 | + templateCover: [{ required: true, message: '请上传封面图', trigger: 'change' }], | ||
| 154 | scene: [{ required: true, message: '请选择训练场景', trigger: 'change' }], | 161 | scene: [{ required: true, message: '请选择训练场景', trigger: 'change' }], |
| 155 | templateIntroduction: [{ required: true, message: '请输入模板简介', trigger: 'blur' }] | 162 | templateIntroduction: [{ required: true, message: '请输入模板简介', trigger: 'blur' }] |
| 156 | }) | 163 | }) |
| 157 | 164 | ||
| 158 | -// ===========================加载数据====================================== | ||
| 159 | -//加载数据 | ||
| 160 | -const loadsuperGroupList = async () => { | 165 | +// ==================== 数据加载 ==================== |
| 166 | +const loadSuperGroupList = async () => { | ||
| 161 | try { | 167 | try { |
| 162 | - const res = await SupersetsApi.getSupersetList() | 168 | + const res = await SupersetsApi.getSupersetList(false) |
| 163 | superGroupList.value = res || [] | 169 | superGroupList.value = res || [] |
| 164 | } catch (err) { | 170 | } catch (err) { |
| 165 | - console.error('加载动作列表失败:', err) | ||
| 166 | - ElMessage.error('加载动作列表失败') | 171 | + console.error('加载超级组列表失败:', err) |
| 172 | + ElMessage.error('加载超级组列表失败') | ||
| 167 | } | 173 | } |
| 168 | } | 174 | } |
| 169 | 175 | ||
| 170 | -//加载数据 | ||
| 171 | const loadExercisesList = async () => { | 176 | const loadExercisesList = async () => { |
| 172 | try { | 177 | try { |
| 173 | - const res = await ExercisesApi.getExercisesList() | 178 | + const res = await ExercisesApi.getExercisesList(false) |
| 174 | exercisesList.value = res || [] | 179 | exercisesList.value = res || [] |
| 175 | } catch (err) { | 180 | } catch (err) { |
| 176 | console.error('加载动作列表失败:', err) | 181 | console.error('加载动作列表失败:', err) |
| @@ -178,13 +183,11 @@ const loadExercisesList = async () => { | @@ -178,13 +183,11 @@ const loadExercisesList = async () => { | ||
| 178 | } | 183 | } |
| 179 | } | 184 | } |
| 180 | 185 | ||
| 181 | -// ====================== 【关键】编辑回显 ====================== | ||
| 182 | -const loadTemplateDetail = async (id: number) => { | 186 | +const loadTemplateDetail = async (id) => { |
| 183 | try { | 187 | try { |
| 184 | loading.value = true | 188 | loading.value = true |
| 185 | const res = await TrainingTemplatesApi.getTrainingTemplates(id) | 189 | const res = await TrainingTemplatesApi.getTrainingTemplates(id) |
| 186 | const detail = res.data || res | 190 | const detail = res.data || res |
| 187 | - console.log('编辑详细模板返回的数据detail', detail) | ||
| 188 | 191 | ||
| 189 | formData.id = detail.id || 0 | 192 | formData.id = detail.id || 0 |
| 190 | formData.groupId = detail.groupId || null | 193 | formData.groupId = detail.groupId || null |
| @@ -193,27 +196,18 @@ const loadTemplateDetail = async (id: number) => { | @@ -193,27 +196,18 @@ const loadTemplateDetail = async (id: number) => { | ||
| 193 | formData.templateCover = detail.urlCover || detail.templateCover || '' | 196 | formData.templateCover = detail.urlCover || detail.templateCover || '' |
| 194 | formData.templateIntroduction = detail.introduction || '' | 197 | formData.templateIntroduction = detail.introduction || '' |
| 195 | 198 | ||
| 196 | - // 🔥 修复:超级组多动作回显(不再写死 [0]) | ||
| 197 | if (detail.units && detail.units.length) { | 199 | if (detail.units && detail.units.length) { |
| 198 | - formData.units = detail.units.map((unit: any) => ({ | ||
| 199 | - id: unit.unitId || 0, | 200 | + formData.units = detail.units.map((unit) => ({ |
| 201 | + id: unit.unitId || null, | ||
| 200 | name: unit.unitName || '', | 202 | name: unit.unitName || '', |
| 201 | - unitType: unit.unitType || 1, | 203 | + unitType: unit.unitType || UNIT_TYPE.GROUP, |
| 202 | sortOrder: unit.sortOrder || 1, | 204 | sortOrder: unit.sortOrder || 1, |
| 203 | - // ✅ 这里修复:完整映射所有动作(动作组1个、超级组N个都正常) | ||
| 204 | - exercises: unit.exercises?.map((item: any) => ({ | 205 | + exercises: unit.exercises?.map((item) => ({ |
| 205 | exerciseId: item.exerciseId || 0, | 206 | exerciseId: item.exerciseId || 0, |
| 206 | exerciseType: item.exerciseType || 1, | 207 | exerciseType: item.exerciseType || 1, |
| 207 | innerOrder: item.innerOrder || 1, | 208 | innerOrder: item.innerOrder || 1, |
| 208 | - sets: item.sets || [ | ||
| 209 | - { weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 } | ||
| 210 | - ] | ||
| 211 | - })) || [{ | ||
| 212 | - exerciseId: 0, | ||
| 213 | - exerciseType: 1, | ||
| 214 | - innerOrder: 1, | ||
| 215 | - sets: [{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }] | ||
| 216 | - }] | 209 | + sets: item.sets || [createDefaultSet()] |
| 210 | + })) || [createDefaultExercise()] | ||
| 217 | })) | 211 | })) |
| 218 | } | 212 | } |
| 219 | 213 | ||
| @@ -221,80 +215,57 @@ const loadTemplateDetail = async (id: number) => { | @@ -221,80 +215,57 @@ const loadTemplateDetail = async (id: number) => { | ||
| 221 | updateDynamicRules() | 215 | updateDynamicRules() |
| 222 | formRef.value?.clearValidate() | 216 | formRef.value?.clearValidate() |
| 223 | }) | 217 | }) |
| 224 | - | ||
| 225 | } catch (err) { | 218 | } catch (err) { |
| 226 | - ElMessage.error('获取详情失败') | ||
| 227 | - console.error(err) | 219 | + console.error('获取模板详情失败:', err) |
| 220 | + ElMessage.error('获取模板详情失败') | ||
| 228 | } finally { | 221 | } finally { |
| 229 | loading.value = false | 222 | loading.value = false |
| 230 | } | 223 | } |
| 231 | } | 224 | } |
| 232 | 225 | ||
| 233 | -//动态检验 | 226 | +// ==================== 动态表单校验 ==================== |
| 234 | const updateDynamicRules = () => { | 227 | const updateDynamicRules = () => { |
| 235 | - // 先清理所有动态校验规则(避免删除单元后残留旧规则报错) | ||
| 236 | - Object.keys(rules.value).forEach(key => { | ||
| 237 | - if (key.startsWith('units[')) { // 匹配所有循环生成的规则 | ||
| 238 | - delete rules.value[key] | 228 | + Object.keys(rules).forEach((key) => { |
| 229 | + if (key.startsWith('units[')) { | ||
| 230 | + delete rules[key] | ||
| 239 | } | 231 | } |
| 240 | }) | 232 | }) |
| 241 | - //必须使用units[${index}].unitType这种,不然验证找不到真实的数据,验证出错。 | ||
| 242 | formData.units.forEach((unit, index) => { | 233 | formData.units.forEach((unit, index) => { |
| 243 | - rules.value[`units[${index}].unitType`] = [ | 234 | + rules[`units[${index}].unitType`] = [ |
| 244 | { required: true, message: '请选择单元类型', trigger: 'change' } | 235 | { required: true, message: '请选择单元类型', trigger: 'change' } |
| 245 | ] | 236 | ] |
| 246 | - rules.value[`units[${index}].name`] = [ | 237 | + rules[`units[${index}].name`] = [ |
| 247 | { required: true, message: '请选择动作', trigger: 'change' } | 238 | { required: true, message: '请选择动作', trigger: 'change' } |
| 248 | ] | 239 | ] |
| 249 | - rules.value[`units[${index}].sortOrder`] = [ | 240 | + rules[`units[${index}].sortOrder`] = [ |
| 250 | { required: true, message: '请输入排序', trigger: 'blur' } | 241 | { required: true, message: '请输入排序', trigger: 'blur' } |
| 251 | ] | 242 | ] |
| 252 | }) | 243 | }) |
| 253 | - | ||
| 254 | - // 清除校验状态,防止新增就爆红 | ||
| 255 | nextTick(() => { | 244 | nextTick(() => { |
| 256 | if (formRef.value) formRef.value.clearValidate() | 245 | if (formRef.value) formRef.value.clearValidate() |
| 257 | }) | 246 | }) |
| 258 | } | 247 | } |
| 259 | 248 | ||
| 260 | -// ---------------------- 交互方法 ---------------------- | ||
| 261 | -// 动作选择联动名称(新增unitIndex参数,绑定当前动作组) | ||
| 262 | -const handleExerciseChange = (exerciseId: number, unitIndex: number) => { | ||
| 263 | - const unit = formData.units[unitIndex] | ||
| 264 | - | ||
| 265 | - // 根据类型找对应的数据 | ||
| 266 | - let selectedItem | ||
| 267 | - if (unit.unitType === 1) { | ||
| 268 | - selectedItem = exercisesList.value.find(item => item.id === exerciseId) | ||
| 269 | - } else { | ||
| 270 | - selectedItem = superGroupList.value.find(item => item.id === exerciseId) | ||
| 271 | - } | ||
| 272 | - | ||
| 273 | - if (selectedItem) { | ||
| 274 | - unit.name = selectedItem.name | ||
| 275 | - } | 249 | +// ==================== 交互方法 ==================== |
| 250 | +const getExerciseName = (id) => { | ||
| 251 | + if (!id) return '未选择动作' | ||
| 252 | + const item = exercisesList.value.find((i) => i.id === id) | ||
| 253 | + return item ? item.name : '未知动作' | ||
| 276 | } | 254 | } |
| 277 | 255 | ||
| 278 | -// 根据ID获取动作名称 | ||
| 279 | -const getExerciseName = (id: number) => { | ||
| 280 | - if (!id) return '未选择动作' | ||
| 281 | - // 先从动作列表找 | ||
| 282 | - const item = exercisesList.value.find(i => i.id === id) | ||
| 283 | - if (item) return item.name | ||
| 284 | - return '未知动作' | 256 | +const addUnit = () => { |
| 257 | + formData.units.push(createDefaultUnit(formData.units.length + 1)) | ||
| 258 | + updateDynamicRules() | ||
| 259 | + ElMessage.success('新增单元成功(可切换为超级组)') | ||
| 285 | } | 260 | } |
| 286 | 261 | ||
| 287 | -// 删除动作组(接收unitIndex,删除指定动作组) | ||
| 288 | -const delUnit = (unitIndex: number) => { | ||
| 289 | - // 只保留至少 1 个单元 | 262 | +const delUnit = (unitIndex) => { |
| 290 | if (formData.units.length <= 1) { | 263 | if (formData.units.length <= 1) { |
| 291 | ElMessage.warning('至少保留一个单元') | 264 | ElMessage.warning('至少保留一个单元') |
| 292 | return | 265 | return |
| 293 | } | 266 | } |
| 294 | - | ||
| 295 | ElMessageBox.confirm('确定要删除这个单元吗?', '提示', { type: 'warning' }).then(() => { | 267 | ElMessageBox.confirm('确定要删除这个单元吗?', '提示', { type: 'warning' }).then(() => { |
| 296 | formData.units.splice(unitIndex, 1) | 268 | formData.units.splice(unitIndex, 1) |
| 297 | - // 重新排序 | ||
| 298 | formData.units.forEach((unit, index) => { | 269 | formData.units.forEach((unit, index) => { |
| 299 | unit.sortOrder = index + 1 | 270 | unit.sortOrder = index + 1 |
| 300 | }) | 271 | }) |
| @@ -303,141 +274,70 @@ const delUnit = (unitIndex: number) => { | @@ -303,141 +274,70 @@ const delUnit = (unitIndex: number) => { | ||
| 303 | }) | 274 | }) |
| 304 | } | 275 | } |
| 305 | 276 | ||
| 306 | -//增加单元(动作组/超级组) | ||
| 307 | -const AddUnit = () => { | ||
| 308 | - // 默认新增动作组,用户之后可在页面切换为超级组 | ||
| 309 | - const newUnit = { | ||
| 310 | - id: 0, | ||
| 311 | - name: '', | ||
| 312 | - unitType: 1, // 默认动作组 | ||
| 313 | - sortOrder: formData.units.length + 1, | ||
| 314 | - exercises: [ | ||
| 315 | - { | ||
| 316 | - exerciseId: 0, | ||
| 317 | - exerciseType: 1, | ||
| 318 | - innerOrder: 1, | ||
| 319 | - sets: [ | ||
| 320 | - { weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 } | ||
| 321 | - ] | ||
| 322 | - } | ||
| 323 | - ] | ||
| 324 | - } | ||
| 325 | - formData.units.push(newUnit) | ||
| 326 | - updateDynamicRules() | ||
| 327 | - ElMessage.success('新增单元成功(可在上方切换为超级组)') | 277 | +const onUnitTypeChange = (unitIndex) => { |
| 278 | + const unit = formData.units[unitIndex] | ||
| 279 | + // 单元类型切换后,原选中的动作组/超级组 id、名称及动作明细都不再适用,需重置 | ||
| 280 | + unit.id = null | ||
| 281 | + unit.name = '' | ||
| 282 | + unit.exercises = [createDefaultExercise()] | ||
| 328 | } | 283 | } |
| 329 | 284 | ||
| 330 | -// 选择 动作 或 超级组 后,加载详情 | ||
| 331 | -const onSelectItem = async (unitIndex: number, selectId: number) => { | 285 | +const onSelectItem = async (unitIndex, selectId) => { |
| 332 | const unit = formData.units[unitIndex] | 286 | const unit = formData.units[unitIndex] |
| 333 | - | ||
| 334 | - // 清空之前的数据 | ||
| 335 | unit.exercises = [] | 287 | unit.exercises = [] |
| 336 | 288 | ||
| 337 | - if (unit.unitType === 1) { | ||
| 338 | - // ================== | ||
| 339 | - // 1. 动作组:查单个动作详情 | ||
| 340 | - // ================== | 289 | + if (unit.unitType === UNIT_TYPE.GROUP) { |
| 290 | + // 动作组:查询单个动作详情 | ||
| 341 | const res = await ExercisesApi.getExercises(selectId) | 291 | const res = await ExercisesApi.getExercises(selectId) |
| 342 | - console.log('选择的动作组详情', res) | ||
| 343 | const detail = res | 292 | const detail = res |
| 344 | - console.log('选择的动作详情:detail', detail) | ||
| 345 | - console.log('【动作组详情完整数据】', JSON.stringify(detail, null, 2)) | ||
| 346 | - console.log('【动作名称】', detail.name) | ||
| 347 | - console.log('【后端返回的原始exerciseType】', detail.exerciseType) | ||
| 348 | - | ||
| 349 | unit.name = detail.name || '' | 293 | unit.name = detail.name || '' |
| 350 | - // 关键2:只有动作详情是文字,做文字转数字映射 | ||
| 351 | - const typeMap: Record<string, number> = { | ||
| 352 | - '无氧运动': 0, | ||
| 353 | - '有氧运动': 1, | ||
| 354 | - '记次训练': 2, | ||
| 355 | - '计时训练': 3, | ||
| 356 | - '自重加重': 4, | ||
| 357 | - '自重减重': 5, | ||
| 358 | - '间歇训练': 6 | ||
| 359 | - } | ||
| 360 | - // 能映射就转数字,映射失败默认给0 | ||
| 361 | - const typeVal = typeMap[detail.exerciseType] ?? 0 | ||
| 362 | unit.exercises.push({ | 294 | unit.exercises.push({ |
| 363 | exerciseId: detail.id, | 295 | exerciseId: detail.id, |
| 364 | - exerciseType: typeVal, | 296 | + exerciseType: EXERCISE_TYPE_MAP[detail.exerciseType] ?? 0, |
| 365 | innerOrder: 1, | 297 | innerOrder: 1, |
| 366 | - sets: [{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }] | 298 | + sets: [createDefaultSet()] |
| 367 | }) | 299 | }) |
| 368 | - | ||
| 369 | - console.log('【映射后的exerciseType数字】', typeVal) | ||
| 370 | - | ||
| 371 | } else { | 300 | } else { |
| 372 | - // ================== | ||
| 373 | - // 2. 超级组:查超级组详情(返回多个动作) | ||
| 374 | - // ================== | 301 | + // 超级组:查询超级组详情(含多个动作) |
| 375 | const res = await SupersetsApi.getSupersets(selectId) | 302 | const res = await SupersetsApi.getSupersets(selectId) |
| 376 | - console.log('加载的超级组详情:', res) | ||
| 377 | const detail = res | 303 | const detail = res |
| 378 | unit.name = detail.name || '' | 304 | unit.name = detail.name || '' |
| 379 | - console.log('加载选中的超级组详情:', detail) | ||
| 380 | - | ||
| 381 | - // 🔴 关键:超级组接口直接返回数字,不需要再做映射 | ||
| 382 | - unit.exercises = detail.exercises.map((item: any) => { | ||
| 383 | - console.log('【超级组内动作原始exerciseType】', item.exerciseType) | ||
| 384 | - // 直接用返回的数字即可 | ||
| 385 | - const typeVal = Number(item.exerciseType) || 0 | ||
| 386 | - console.log('【超级组内动作最终exerciseType数字】', typeVal) | ||
| 387 | - return { | 305 | + unit.exercises = detail.exercises.map((item) => ({ |
| 388 | exerciseId: item.id, | 306 | exerciseId: item.id, |
| 389 | - exerciseType: typeVal, | 307 | + exerciseType: Number(item.exerciseType) || 0, |
| 390 | innerOrder: item.innerOrder, | 308 | innerOrder: item.innerOrder, |
| 391 | sets: item.sets || [{ weight: 0, reps: 0, restTime: 0, setIndex: 1 }] | 309 | sets: item.sets || [{ weight: 0, reps: 0, restTime: 0, setIndex: 1 }] |
| 392 | - } | ||
| 393 | - }) | 310 | + })) |
| 394 | } | 311 | } |
| 395 | } | 312 | } |
| 396 | 313 | ||
| 397 | -// 取消 | 314 | +// ==================== 表单提交 ==================== |
| 398 | const handleCancel = () => { | 315 | const handleCancel = () => { |
| 399 | ElMessage.info('取消保存表单内容') | 316 | ElMessage.info('取消保存表单内容') |
| 400 | formRef.value?.resetFields() | 317 | formRef.value?.resetFields() |
| 401 | - // 重置表单数据(可选) | ||
| 402 | - formData.id = 0 | ||
| 403 | - formData.groupId = null | ||
| 404 | - formData.templateName = '' | ||
| 405 | - formData.scene = null | ||
| 406 | - formData.templateCover = '' | ||
| 407 | - formData.templateIntroduction = '' | ||
| 408 | - formData.units = [ | ||
| 409 | - { | 318 | + Object.assign(formData, { |
| 410 | id: 0, | 319 | id: 0, |
| 411 | - name: '', | ||
| 412 | - unitType: 1, | ||
| 413 | - sortOrder: 1, | ||
| 414 | - exercises: [ | ||
| 415 | - { | ||
| 416 | - exerciseId: 0, | ||
| 417 | - exerciseType: 1, | ||
| 418 | - innerOrder: 1, | ||
| 419 | - sets: [ | ||
| 420 | - { weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 } | ||
| 421 | - ] | ||
| 422 | - } | ||
| 423 | - ] | ||
| 424 | - } | ||
| 425 | - ] | ||
| 426 | - // 重置后更新校验规则 | 320 | + groupId: null, |
| 321 | + templateName: '', | ||
| 322 | + scene: null, | ||
| 323 | + templateCover: '', | ||
| 324 | + templateIntroduction: '', | ||
| 325 | + units: [createDefaultUnit(1)] | ||
| 326 | + }) | ||
| 427 | updateDynamicRules() | 327 | updateDynamicRules() |
| 428 | - // ✅ 关键:跳回主页面 | ||
| 429 | router.push('/training/templates/templatesForm') | 328 | router.push('/training/templates/templatesForm') |
| 430 | } | 329 | } |
| 330 | + | ||
| 431 | const handleSubmit = async () => { | 331 | const handleSubmit = async () => { |
| 432 | if (!formRef.value) return | 332 | if (!formRef.value) return |
| 433 | try { | 333 | try { |
| 434 | await formRef.value.validate() | 334 | await formRef.value.validate() |
| 435 | - } catch (err) { | 335 | + } catch { |
| 436 | ElMessage.error('请按照要求填写') | 336 | ElMessage.error('请按照要求填写') |
| 437 | return | 337 | return |
| 438 | } | 338 | } |
| 439 | - // 动作组/超级组不能为空。 | ||
| 440 | - const hasEmptyExercise = formData.units.some(unit => !unit.exercises[0].exerciseId) | 339 | + |
| 340 | + const hasEmptyExercise = formData.units.some((unit) => !unit.exercises[0].exerciseId) | ||
| 441 | if (hasEmptyExercise) { | 341 | if (hasEmptyExercise) { |
| 442 | ElMessage.warning('请为所有动作组选择动作名称') | 342 | ElMessage.warning('请为所有动作组选择动作名称') |
| 443 | return | 343 | return |
| @@ -445,24 +345,23 @@ const handleSubmit = async () => { | @@ -445,24 +345,23 @@ const handleSubmit = async () => { | ||
| 445 | 345 | ||
| 446 | loading.value = true | 346 | loading.value = true |
| 447 | try { | 347 | try { |
| 448 | - // 按接口文档转换提交数据 | ||
| 449 | const submitData = { | 348 | const submitData = { |
| 450 | - templateId: formData.id, // 关键:前端id → 后端templateId | 349 | + templateId: formData.id, |
| 451 | groupId: formData.groupId, | 350 | groupId: formData.groupId, |
| 452 | templateName: formData.templateName, | 351 | templateName: formData.templateName, |
| 453 | scene: formData.scene, | 352 | scene: formData.scene, |
| 454 | templateCover: formData.templateCover, | 353 | templateCover: formData.templateCover, |
| 455 | templateIntroduction: formData.templateIntroduction, | 354 | templateIntroduction: formData.templateIntroduction, |
| 456 | - units: formData.units.map(unit => ({ | ||
| 457 | - id: unit.id, | 355 | + units: formData.units.map((unit) => ({ |
| 356 | + id: unit.id ?? 0, | ||
| 458 | name: unit.name, | 357 | name: unit.name, |
| 459 | unitType: unit.unitType, | 358 | unitType: unit.unitType, |
| 460 | sortOrder: unit.sortOrder, | 359 | sortOrder: unit.sortOrder, |
| 461 | - exercises: unit.exercises.map(exercise => ({ | 360 | + exercises: unit.exercises.map((exercise) => ({ |
| 462 | exerciseId: exercise.exerciseId, | 361 | exerciseId: exercise.exerciseId, |
| 463 | exerciseType: exercise.exerciseType, | 362 | exerciseType: exercise.exerciseType, |
| 464 | innerOrder: exercise.innerOrder, | 363 | innerOrder: exercise.innerOrder, |
| 465 | - sets: exercise.sets.map(set => ({ | 364 | + sets: exercise.sets.map((set) => ({ |
| 466 | weight: set.weight, | 365 | weight: set.weight, |
| 467 | reps: set.reps, | 366 | reps: set.reps, |
| 468 | distance: set.distance, | 367 | distance: set.distance, |
| @@ -474,65 +373,35 @@ const handleSubmit = async () => { | @@ -474,65 +373,35 @@ const handleSubmit = async () => { | ||
| 474 | })) | 373 | })) |
| 475 | } | 374 | } |
| 476 | 375 | ||
| 477 | - console.log('提交给后端的完整数据:', submitData) | ||
| 478 | - | ||
| 479 | - let res: any | ||
| 480 | if (route.query.id) { | 376 | if (route.query.id) { |
| 481 | - // 编辑:用转换后的submitData | ||
| 482 | - res = await TrainingTemplatesApi.updateTrainingTemplates(submitData) | 377 | + await TrainingTemplatesApi.updateTrainingTemplates(submitData) |
| 483 | } else { | 378 | } else { |
| 484 | - // 新增:用转换后的submitData | ||
| 485 | - res = await TrainingTemplatesApi.createTrainingTemplates(submitData) | 379 | + await TrainingTemplatesApi.createTrainingTemplates(submitData) |
| 486 | } | 380 | } |
| 487 | - | ||
| 488 | - console.log('接口返回结果:', res) | ||
| 489 | ElMessage.success('模板保存成功!') | 381 | ElMessage.success('模板保存成功!') |
| 490 | - // router.push('/training/templates/templatesForm') | ||
| 491 | router.back() | 382 | router.back() |
| 492 | - } catch (err: any) { | ||
| 493 | - // 👇 修复日志打印,避免undefined | ||
| 494 | - console.error("【完整错误对象】", err); | ||
| 495 | - console.error("【错误信息】", err?.message || '未知错误'); | ||
| 496 | - console.error("【响应数据】", err?.response?.data || '无响应数据'); | ||
| 497 | - console.error("【响应状态】", err?.response?.status || '无状态码'); | ||
| 498 | - ElMessage.error(`保存失败:${err?.message || '网络/服务异常'}`); | 383 | + } catch (err) { |
| 384 | + console.error('保存失败:', err?.message || err) | ||
| 385 | + ElMessage.error(`保存失败:${err?.message || '网络/服务异常'}`) | ||
| 499 | } finally { | 386 | } finally { |
| 500 | loading.value = false | 387 | loading.value = false |
| 501 | } | 388 | } |
| 502 | } | 389 | } |
| 503 | 390 | ||
| 504 | -// 加载肌肉列表 | ||
| 505 | -const loadMuscleList = async () => { | ||
| 506 | - try { | ||
| 507 | - const res = await MusclesApi.getMusclesPage({ pageNo: '1', pageSize: '100', name: '' }) | ||
| 508 | - const data = res.data || res | ||
| 509 | - muscleList.value = data.list || [] | ||
| 510 | - } catch (err) { | ||
| 511 | - console.error('加载肌肉列表失败:', err) | ||
| 512 | - ElMessage.error('加载肌肉列表失败') | ||
| 513 | - } | ||
| 514 | -} | ||
| 515 | -//生命周期 | 391 | +// ==================== 生命周期 ==================== |
| 516 | onMounted(async () => { | 392 | onMounted(async () => { |
| 517 | - // groupList.value = [{ id: 1, name: "默认大类" }] | ||
| 518 | - loadMuscleList() | ||
| 519 | loadExercisesList() | 393 | loadExercisesList() |
| 520 | - loadsuperGroupList() | ||
| 521 | - | ||
| 522 | - // 进入页面判断是否编辑 | 394 | + loadSuperGroupList() |
| 523 | if (route.query.id) { | 395 | if (route.query.id) { |
| 524 | loadTemplateDetail(Number(route.query.id)) | 396 | loadTemplateDetail(Number(route.query.id)) |
| 525 | } | 397 | } |
| 526 | }) | 398 | }) |
| 527 | 399 | ||
| 528 | -// 监听路由ID变化,自动回显 | ||
| 529 | watch( | 400 | watch( |
| 530 | () => route.query.id, | 401 | () => route.query.id, |
| 531 | (newId) => { | 402 | (newId) => { |
| 532 | if (newId) { | 403 | if (newId) { |
| 533 | loadTemplateDetail(Number(newId)) | 404 | loadTemplateDetail(Number(newId)) |
| 534 | - } else { | ||
| 535 | - // handleCancel() | ||
| 536 | } | 405 | } |
| 537 | }, | 406 | }, |
| 538 | { immediate: true } | 407 | { immediate: true } |
| @@ -540,120 +409,78 @@ watch( | @@ -540,120 +409,78 @@ watch( | ||
| 540 | </script> | 409 | </script> |
| 541 | 410 | ||
| 542 | <style scoped> | 411 | <style scoped> |
| 543 | -:deep(.el-form-item__label) { | 412 | +/* ==================== 表单项全局样式 ==================== */ |
| 413 | +::deep(.el-form-item__label) { | ||
| 544 | float: none !important; | 414 | float: none !important; |
| 545 | - width: 68px !important; | 415 | + width: 88px !important; |
| 546 | padding: 0 !important; | 416 | padding: 0 !important; |
| 547 | - margin-right: 20px !important; | 417 | + margin-right: 16px !important; |
| 548 | line-height: 32px; | 418 | line-height: 32px; |
| 549 | text-align: right; | 419 | text-align: right; |
| 550 | white-space: nowrap; | 420 | white-space: nowrap; |
| 551 | } | 421 | } |
| 552 | 422 | ||
| 553 | -/* 让表单内容区域横向排列 */ | ||
| 554 | -.row-form-item :deep(.el-form-item__content) { | ||
| 555 | - display: flex; | ||
| 556 | - align-items: center; | 423 | +/* ==================== 工具类 ==================== */ |
| 424 | +.w-full { | ||
| 557 | width: 100%; | 425 | width: 100%; |
| 558 | } | 426 | } |
| 559 | 427 | ||
| 560 | -/* 下拉框 + 按钮 同行容器 */ | ||
| 561 | -.row-wrap { | ||
| 562 | - display: flex; | ||
| 563 | - align-items: center; | ||
| 564 | - gap: 14px; | ||
| 565 | - width: 100%; | 428 | +/* ==================== 页面布局 ==================== */ |
| 429 | +.page-contain { | ||
| 430 | + height: 100vh; | ||
| 431 | + overflow-y: auto; | ||
| 432 | + background-color: #f5f7fa; | ||
| 433 | + box-sizing: border-box; | ||
| 566 | } | 434 | } |
| 567 | 435 | ||
| 568 | -/* 下拉框占剩余宽度 */ | ||
| 569 | -.sel { | ||
| 570 | - flex: 1; | ||
| 571 | -} | 436 | +.templateName { |
| 572 | 437 | ||
| 573 | -/* 按钮容器:不被压缩 */ | ||
| 574 | -.unit-header { | ||
| 575 | - display: flex; | ||
| 576 | - gap: 10px; | ||
| 577 | - flex-shrink: 0; | 438 | + padding: 30px; |
| 439 | + margin: 0 auto; | ||
| 440 | + background: #fff; | ||
| 441 | + border-radius: 8px; | ||
| 442 | + box-shadow: 0 2px 12px rgb(0 0 0 / 5%); | ||
| 578 | } | 443 | } |
| 579 | 444 | ||
| 580 | -/* 下面是你原来保留的样式,不动 */ | ||
| 581 | - | ||
| 582 | -/* 动作组卡片 */ | 445 | +/* ==================== 单元卡片 ==================== */ |
| 583 | .unit-card { | 446 | .unit-card { |
| 584 | margin: 20px 0; | 447 | margin: 20px 0; |
| 585 | - border-radius: 6px; | ||
| 586 | -} | ||
| 587 | - | ||
| 588 | -/* 表格容器 */ | ||
| 589 | -.sets-table-wrapper { | ||
| 590 | - margin: 20px 0; | ||
| 591 | - overflow-x: auto; | ||
| 592 | -} | ||
| 593 | - | ||
| 594 | -.table-title { | ||
| 595 | - margin-bottom: 10px; | ||
| 596 | - font-size: 14px; | ||
| 597 | - font-weight: 500; | ||
| 598 | - color: #666; | ||
| 599 | -} | ||
| 600 | - | ||
| 601 | -/* 表格样式 */ | ||
| 602 | -.sets-table { | ||
| 603 | - width: 100%; | ||
| 604 | - text-align: center; | ||
| 605 | - border-collapse: collapse; | 448 | + padding: 16px 20px 20px; |
| 449 | + background: #fafbfc; | ||
| 450 | + border: 1px solid #ebeef5; | ||
| 451 | + border-radius: 8px; | ||
| 452 | + transition: border-color 0.2s; | ||
| 606 | } | 453 | } |
| 607 | 454 | ||
| 608 | -.sets-table th { | ||
| 609 | - padding: 10px 0; | ||
| 610 | - font-weight: 500; | ||
| 611 | - background: #fafafa; | 455 | +.unit-card:hover { |
| 456 | + border-color: #c0c4cc; | ||
| 612 | } | 457 | } |
| 613 | 458 | ||
| 614 | -.sets-table td { | ||
| 615 | - padding: 8px 0; | 459 | +.unit-toolbar { |
| 460 | + display: flex; | ||
| 461 | + justify-content: flex-end; | ||
| 462 | + gap: 10px; | ||
| 463 | + margin-bottom: 4px; | ||
| 616 | } | 464 | } |
| 617 | 465 | ||
| 618 | -.set-row:hover { | ||
| 619 | - background: #f9f9f9; | 466 | +.exercise-item { |
| 467 | + margin: 16px 0; | ||
| 620 | } | 468 | } |
| 621 | 469 | ||
| 622 | .action-name { | 470 | .action-name { |
| 623 | - color: #666; | 471 | + margin-bottom: 8px; |
| 472 | + font-size: 13px; | ||
| 473 | + color: #606266; | ||
| 624 | } | 474 | } |
| 625 | 475 | ||
| 626 | -/* 按钮居中容器 */ | 476 | +/* ==================== 按钮区域 ==================== */ |
| 627 | .btn-center-wrapper { | 477 | .btn-center-wrapper { |
| 628 | display: flex; | 478 | display: flex; |
| 629 | width: 100%; | 479 | width: 100%; |
| 630 | - padding-top: 15px; | 480 | + padding-top: 20px; |
| 631 | margin-top: 30px; | 481 | margin-top: 30px; |
| 632 | border-top: 1px solid #eee; | 482 | border-top: 1px solid #eee; |
| 633 | justify-content: center; | 483 | justify-content: center; |
| 634 | gap: 12px; | 484 | gap: 12px; |
| 635 | } | 485 | } |
| 636 | - | ||
| 637 | -/* 封面图样式 */ | ||
| 638 | -.sub-image { | ||
| 639 | - border: 1px solid #eee; | ||
| 640 | - border-radius: 10px; | ||
| 641 | -} | ||
| 642 | - | ||
| 643 | -/* 页面滚动容器 */ | ||
| 644 | -.page-contain { | ||
| 645 | - height: 100vh; | ||
| 646 | - overflow-y: auto; | ||
| 647 | - background-color: #f5f5f5; | ||
| 648 | - box-sizing: border-box; | ||
| 649 | -} | ||
| 650 | - | ||
| 651 | -/* 表单容器 */ | ||
| 652 | -.templateName { | ||
| 653 | - padding: 30px; | ||
| 654 | - margin: 0 auto; | ||
| 655 | - background: #fff; | ||
| 656 | - border-radius: 8px; | ||
| 657 | - box-shadow: 0 2px 12px rgb(0 0 0 / 5%); | ||
| 658 | -} | ||
| 659 | </style> | 486 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | - <Dialog v-model="dialogVisible" :title="title" width="800px" height="100%" append-to-body> | ||
| 3 | - <!-- 这里直接用 SonFormData --> | ||
| 4 | - <el-form ref="formRef" :model="SonFormData" label-width="120px" :rules="formRules" label-position="right"> | ||
| 5 | - <el-form-item label="计划名称" prop="name" label-suffix="*"> | 2 | + <Dialog v-model="dialogVisible" :title="title" width="800px"> |
| 3 | + <el-form ref="formRef" :model="SonFormData" label-width="120px" :rules="formRules"> | ||
| 4 | + <el-form-item label="计划名称" prop="name"> | ||
| 6 | <el-input v-model="SonFormData.name" placeholder="请输入计划名称" maxlength="50" show-word-limit /> | 5 | <el-input v-model="SonFormData.name" placeholder="请输入计划名称" maxlength="50" show-word-limit /> |
| 7 | </el-form-item> | 6 | </el-form-item> |
| 8 | 7 | ||
| 9 | - <el-form-item label="封面图" prop="urlCover" class="form-item-margin" label-suffix="*"> | 8 | + <el-form-item label="封面图" prop="urlCover"> |
| 10 | <UploadImg v-model="SonFormData.urlCover" /> | 9 | <UploadImg v-model="SonFormData.urlCover" /> |
| 11 | </el-form-item> | 10 | </el-form-item> |
| 12 | - <!-- 计划分类/后来加上 --> | ||
| 13 | - <el-form-item label="计划分类" prop="categoryId" label-suffix="*"> | 11 | + |
| 12 | + <el-form-item label="计划分类" prop="categoryId"> | ||
| 14 | <el-select v-model="SonFormData.categoryId" placeholder="请选择计划分类"> | 13 | <el-select v-model="SonFormData.categoryId" placeholder="请选择计划分类"> |
| 15 | <el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" /> | 14 | <el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" /> |
| 16 | </el-select> | 15 | </el-select> |
| 17 | </el-form-item> | 16 | </el-form-item> |
| 18 | 17 | ||
| 19 | - <el-form-item label="训练场景" prop="trainingScene" label-suffix="*"> | ||
| 20 | - <el-select v-model="SonFormData.trainingScene"> | ||
| 21 | - <el-option label="不限" :value="1" /> | ||
| 22 | - <el-option label="健身房" :value="2" /> | ||
| 23 | - <el-option label="仅哑铃" :value="3" /> | ||
| 24 | - <el-option label="仅哑铃+杠铃" :value="4" /> | 18 | + <el-form-item label="训练场景" prop="trainingScene"> |
| 19 | + <el-select v-model="SonFormData.trainingScene" placeholder="请选择训练场景"> | ||
| 20 | + <el-option v-for="item in trainingSceneOptions" :key="item.value" :label="item.label" :value="item.value" /> | ||
| 25 | </el-select> | 21 | </el-select> |
| 26 | </el-form-item> | 22 | </el-form-item> |
| 27 | - <!-- 难度的不限是后来加上的 --> | ||
| 28 | - <el-form-item label="计划难度" prop="difficultyLevel" label-suffix="*"> | ||
| 29 | - <el-select v-model="SonFormData.difficultyLevel"> | ||
| 30 | - <!-- <el-option label="不限" :value="0" /> --> | ||
| 31 | - <el-option label="初阶" :value="1" /> | ||
| 32 | - <el-option label="中阶" :value="2" /> | ||
| 33 | - <el-option label="高阶" :value="3" /> | 23 | + |
| 24 | + <el-form-item label="计划难度" prop="difficultyLevel"> | ||
| 25 | + <el-select v-model="SonFormData.difficultyLevel" placeholder="请选择难度"> | ||
| 26 | + <el-option v-for="item in difficultyLevelOptions" :key="item.value" :label="item.label" :value="item.value" /> | ||
| 34 | </el-select> | 27 | </el-select> |
| 35 | </el-form-item> | 28 | </el-form-item> |
| 36 | 29 | ||
| 37 | - <el-form-item label="适合人群" prop="targetPeople" label-suffix="*"> | ||
| 38 | - <el-select v-model="SonFormData.targetPeople"> | ||
| 39 | - <el-option label="不限" :value="1" /> | ||
| 40 | - <el-option label="青少年" :value="2" /> | ||
| 41 | - <el-option label="成年人" :value="3" /> | ||
| 42 | - <el-option label="中年人" :value="4" /> | ||
| 43 | - <el-option label="运动损伤" :value="5" /> | 30 | + <el-form-item label="适合人群" prop="targetPeople"> |
| 31 | + <el-select v-model="SonFormData.targetPeople" placeholder="请选择适合人群"> | ||
| 32 | + <el-option v-for="item in targetPeopleOptions" :key="item.value" :label="item.label" :value="item.value" /> | ||
| 44 | </el-select> | 33 | </el-select> |
| 45 | </el-form-item> | 34 | </el-form-item> |
| 46 | 35 | ||
| 47 | - <el-form-item label="频率" prop="frequencyPerWeek" label-suffix="*"> | ||
| 48 | - <el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1"> | 36 | + <el-form-item label="频率" prop="frequencyPerWeek"> |
| 37 | + <el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1" placeholder="请选择频率"> | ||
| 49 | <el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" /> | 38 | <el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" /> |
| 50 | </el-select> | 39 | </el-select> |
| 51 | </el-form-item> | 40 | </el-form-item> |
| 52 | 41 | ||
| 53 | - <el-form-item label="计划周期(周)" prop="durationWeeks" label-suffix="*"> | 42 | + <el-form-item label="计划周期(周)" prop="durationWeeks"> |
| 54 | <el-input-number v-model="SonFormData.durationWeeks" class="w-full" /> | 43 | <el-input-number v-model="SonFormData.durationWeeks" class="w-full" /> |
| 55 | </el-form-item> | 44 | </el-form-item> |
| 56 | - <!-- 选择模板,并且显示模板详情 --> | ||
| 57 | - <div v-for="(templates, templatesIndex) in SonFormData.templateIds" :key="templatesIndex" style="width: 100%;"> | ||
| 58 | - <el-form-item label="模板" :prop="`templateIds[${templatesIndex}].templateId`" label-suffix="*" | ||
| 59 | - style="width: 100%; margin: 0;" class="is-required"> | ||
| 60 | - <div style="display: flex; align-items: center; gap: 12px;width: 100%;"> | ||
| 61 | - <el-select v-model="templates.templateId" placeholder="请选择模板" style="flex: 1; width: 0;" | ||
| 62 | - @change="() => loadTemplateDetail(templatesIndex, templates.templateId)" | ||
| 63 | - @click="() => reloadTemplateDetail(templatesIndex)"> | 45 | + |
| 46 | + <!-- 模板选择区域 --> | ||
| 47 | + <div v-for="(tpl, tplIdx) in SonFormData.templateIds" :key="tplIdx" class="template-block"> | ||
| 48 | + <div class="template-block-toolbar"> | ||
| 49 | + <el-button :disabled="SonFormData.templateIds.length <= 1" @click="removeTemplate(tplIdx)"> | ||
| 50 | + <Icon icon="ep:minus" class="mr-5px" />删除 | ||
| 51 | + </el-button> | ||
| 52 | + <el-button type="primary" @click="addTemplate(tplIdx)"> | ||
| 53 | + <Icon icon="ep:plus" class="mr-5px" />新增 | ||
| 54 | + </el-button> | ||
| 55 | + </div> | ||
| 56 | + | ||
| 57 | + <el-form-item :prop="`templateIds[${tplIdx}].templateId`" label="模板"> | ||
| 58 | + <el-select v-model="tpl.templateId" placeholder="请选择模板" class="w-full" | ||
| 59 | + @change="() => loadTemplateDetail(tplIdx, tpl.templateId)" @click="() => reloadTemplateDetail(tplIdx)"> | ||
| 64 | <el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" /> | 60 | <el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" /> |
| 65 | </el-select> | 61 | </el-select> |
| 66 | - <div style="display: flex; gap: 8px; flex-shrink: 0;"> | ||
| 67 | - <el-button @click="deltemplate(templatesIndex)" | ||
| 68 | - :disabled="SonFormData.templateIds.length <= 1">删除</el-button> | ||
| 69 | - <el-button @click="Addtemplate(templatesIndex)">新增</el-button> | ||
| 70 | - </div> | ||
| 71 | - </div> | ||
| 72 | </el-form-item> | 62 | </el-form-item> |
| 73 | 63 | ||
| 74 | - <el-form-item :prop="`templateIds[${templatesIndex}].sortOrder`" label="排序" label-suffix="*" | ||
| 75 | - class="is-required"> | ||
| 76 | - <el-input-number v-model="templates.sortOrder" :min="1" :max="99" placeholder="排序号" style="width: 100%" /> | 64 | + <el-form-item :prop="`templateIds[${tplIdx}].sortOrder`" label="排序"> |
| 65 | + <el-input-number v-model="tpl.sortOrder" :min="1" :max="99" placeholder="排序号" class="w-full" /> | ||
| 77 | </el-form-item> | 66 | </el-form-item> |
| 67 | + | ||
| 78 | <!-- 模板详情 --> | 68 | <!-- 模板详情 --> |
| 79 | - <!-- 模板详情回显(新增部分) --> | ||
| 80 | - <el-form-item label="模板详情" v-if="templateDetailMap[templatesIndex]" | ||
| 81 | - style="width: 100%; padding: 0; margin: 0 auto;" class="template-detail-container"> | 69 | + <el-form-item v-if="templateDetailMap[tplIdx]" label="模板详情" class="template-detail-container"> |
| 82 | <el-collapse-transition> | 70 | <el-collapse-transition> |
| 83 | - <div v-loading="templateLoading[templatesIndex]"> | ||
| 84 | - <!-- 训练单元列表 --> | ||
| 85 | - <el-table :data="templateDetailMap[templatesIndex].units || []" border | ||
| 86 | - style="width: 100%; margin-bottom: 12px;" :show-header="true" class="expand-table-fix"> | ||
| 87 | - <el-table-column prop="unitName" label="训练单元名称" min-width="150" /> | ||
| 88 | - <el-table-column prop="sortOrder" label="排序" width="100" align="center" /> | ||
| 89 | - <!-- <el-table-column prop="setCount" label="组数" width="80" align="center" /> --> | ||
| 90 | - </el-table> | ||
| 91 | - <!-- 用纯DIV替代嵌套表格,彻底解决宽度问题 --> | ||
| 92 | - <div v-for="unit in templateDetailMap[templatesIndex].units || []" :key="unit.unitId" | ||
| 93 | - class="unit-container"> | ||
| 94 | - <!-- 训练单元行(可点击展开) --> | 71 | + <div v-loading="templateLoading[tplIdx]"> |
| 72 | + <div v-for="unit in templateDetailMap[tplIdx].units || []" :key="unit.unitId" class="unit-container"> | ||
| 95 | <div class="unit-row" @click="unit.isExpand = !unit.isExpand"> | 73 | <div class="unit-row" @click="unit.isExpand = !unit.isExpand"> |
| 96 | - <span class="expand-icon">{{ unit.isExpand ? '▼' : '▶' }}</span> | 74 | + <Icon icon="ep:arrow-right" class="expand-icon" :class="{ 'is-expanded': unit.isExpand }" /> |
| 97 | <span class="unit-name">{{ unit.unitName }}</span> | 75 | <span class="unit-name">{{ unit.unitName }}</span> |
| 98 | </div> | 76 | </div> |
| 99 | - | ||
| 100 | - <!-- 展开的动作列表 --> | ||
| 101 | <div v-if="unit.isExpand" class="exercise-container"> | 77 | <div v-if="unit.isExpand" class="exercise-container"> |
| 102 | - <div v-for="exercise in unit.exercises || []" :key="exercise.exerciseId" class="exercise-row"> | ||
| 103 | - <div class="exercise-header"> | ||
| 104 | - <!-- <span class="exercise-name">{{ exercise.exerciseName }}</span> --> | ||
| 105 | - <!-- <span class="exercise-info">组数:{{ exercise.setCount }} | 重量:{{ exercise.weight }} | 次数:{{ | ||
| 106 | - exercise.reps }}</span> --> | ||
| 107 | - <!-- <span class="exercise-expand" @click="exercise.isExpand = !exercise.isExpand"> | ||
| 108 | - {{ exercise.isExpand ? '▼ 收起组详情' : '▶ 展开详情' }} | ||
| 109 | - </span> --> | ||
| 110 | - </div> | ||
| 111 | - | ||
| 112 | - <!-- 展开的组详情 --> | ||
| 113 | - <div class="set-container"> | 78 | + <div v-for="exercise in unit.exercises || []" :key="exercise.exerciseId" class="exercise-item"> |
| 114 | <div class="set-header"> | 79 | <div class="set-header"> |
| 115 | <span>重量</span> | 80 | <span>重量</span> |
| 116 | <span>距离</span> | 81 | <span>距离</span> |
| @@ -129,23 +94,18 @@ | @@ -129,23 +94,18 @@ | ||
| 129 | </div> | 94 | </div> |
| 130 | </div> | 95 | </div> |
| 131 | </div> | 96 | </div> |
| 132 | - <!-- --> | ||
| 133 | - </div> | ||
| 134 | </el-collapse-transition> | 97 | </el-collapse-transition> |
| 135 | </el-form-item> | 98 | </el-form-item> |
| 136 | </div> | 99 | </div> |
| 137 | 100 | ||
| 138 | - <!-- <el-form-item label="所需器械汇总" prop="equipmentsSummary" label-suffix="*"> | ||
| 139 | - <el-input v-model="SonFormData.equipmentsSummary" /> | ||
| 140 | - </el-form-item> --> | ||
| 141 | - <el-form-item label="计划介绍" prop="introduction" label-suffix="*"> | 101 | + <el-form-item label="计划介绍" prop="introduction"> |
| 142 | <el-input v-model="SonFormData.introduction" type="textarea" :rows="3" /> | 102 | <el-input v-model="SonFormData.introduction" type="textarea" :rows="3" /> |
| 143 | </el-form-item> | 103 | </el-form-item> |
| 144 | </el-form> | 104 | </el-form> |
| 145 | 105 | ||
| 146 | <template #footer> | 106 | <template #footer> |
| 147 | - <el-button @click="handleCancel">取消</el-button> | ||
| 148 | - <el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button> | 107 | + <el-button type="primary" :loading="loading" @click="handleSubmit">确 定</el-button> |
| 108 | + <el-button @click="dialogVisible = false">取 消</el-button> | ||
| 149 | </template> | 109 | </template> |
| 150 | </Dialog> | 110 | </Dialog> |
| 151 | </template> | 111 | </template> |
| @@ -155,16 +115,13 @@ import { ref, reactive, watch, onMounted, computed } from 'vue' | @@ -155,16 +115,13 @@ import { ref, reactive, watch, onMounted, computed } from 'vue' | ||
| 155 | import { ElMessage } from 'element-plus' | 115 | import { ElMessage } from 'element-plus' |
| 156 | import { TrainingTemplatesApi } from '@/api/store/training/templates' | 116 | import { TrainingTemplatesApi } from '@/api/store/training/templates' |
| 157 | import { trainingPlanApi } from '@/api/store/training/trainingPlan' | 117 | import { trainingPlanApi } from '@/api/store/training/trainingPlan' |
| 158 | -import { Minus, Plus } from '@element-plus/icons-vue' | ||
| 159 | -import { Dialog } from '@/components/Dialog' | ||
| 160 | -// import { Table } from '@/components/Table' | ||
| 161 | -// 父组件传过来的值(只用来接收,不冲突) | 118 | + |
| 119 | +// ==================== Props & Emits ==================== | ||
| 162 | const props = defineProps({ | 120 | const props = defineProps({ |
| 163 | visible: { type: Boolean, default: false }, | 121 | visible: { type: Boolean, default: false }, |
| 164 | title: String, | 122 | title: String, |
| 165 | formData: { type: Object, default: null } | 123 | formData: { type: Object, default: null } |
| 166 | }) | 124 | }) |
| 167 | - | ||
| 168 | const emit = defineEmits(['submit', 'update:visible']) | 125 | const emit = defineEmits(['submit', 'update:visible']) |
| 169 | 126 | ||
| 170 | const dialogVisible = computed({ | 127 | const dialogVisible = computed({ |
| @@ -172,78 +129,71 @@ const dialogVisible = computed({ | @@ -172,78 +129,71 @@ const dialogVisible = computed({ | ||
| 172 | set: (val) => emit('update:visible', val) | 129 | set: (val) => emit('update:visible', val) |
| 173 | }) | 130 | }) |
| 174 | 131 | ||
| 175 | -// ======================== | ||
| 176 | -// 子组件自己的表单数据(唯一来源) | ||
| 177 | -// ======================== | ||
| 178 | -const SonFormData = ref({ | ||
| 179 | - id: undefined, | ||
| 180 | - categoryId: 0, | ||
| 181 | - name: '', | ||
| 182 | - urlCover: '', | ||
| 183 | - introduction: '', | ||
| 184 | - durationWeeks: 1, | ||
| 185 | - frequencyPerWeek: 1, | ||
| 186 | - difficultyLevel: 1, | ||
| 187 | - targetPeople: 1, | ||
| 188 | - trainingScene: 1, | ||
| 189 | - equipmentsSummary: '', | 132 | +// ==================== 表单默认值(仅保留渲染所必需的字段) ==================== |
| 133 | +const defaultFormData = () => ({ | ||
| 190 | templateIds: [{ templateId: undefined, sortOrder: 1 }] | 134 | templateIds: [{ templateId: undefined, sortOrder: 1 }] |
| 191 | }) | 135 | }) |
| 192 | 136 | ||
| 193 | -// 是否是编辑功能 | ||
| 194 | -// 新增:专门的编辑状态标记 | 137 | +// ==================== 状态 ==================== |
| 138 | +const SonFormData = ref(defaultFormData()) | ||
| 195 | const isEdit = ref(false) | 139 | const isEdit = ref(false) |
| 196 | const formRef = ref() | 140 | const formRef = ref() |
| 197 | const loading = ref(false) | 141 | const loading = ref(false) |
| 198 | const templateList = ref([]) | 142 | const templateList = ref([]) |
| 199 | -const categoryList = ref([]) // 计划分类列表 | ||
| 200 | -// 存储每个模板的详情数据,key 是模板 index,value 是接口返回的详情 | 143 | +const categoryList = ref([]) |
| 201 | const templateDetailMap = ref({}) | 144 | const templateDetailMap = ref({}) |
| 202 | -// 模板详情加载状态 | ||
| 203 | const templateLoading = ref({}) | 145 | const templateLoading = ref({}) |
| 204 | 146 | ||
| 205 | -// 控制练的频率 | 147 | +// ==================== 计算属性 ==================== |
| 206 | const templateCount = computed(() => SonFormData.value.templateIds.length) | 148 | const templateCount = computed(() => SonFormData.value.templateIds.length) |
| 207 | -// 频率选项:1个模板显示全部,多个模板只显示对应数量 | 149 | + |
| 208 | const frequencyOptions = computed(() => { | 150 | const frequencyOptions = computed(() => { |
| 209 | - const count = templateCount.value | ||
| 210 | - if (count === 1) { | ||
| 211 | - return [ | ||
| 212 | - { label: '1练/周', value: 1 }, | ||
| 213 | - { label: '2练/周', value: 2 }, | ||
| 214 | - { label: '3练/周', value: 3 }, | ||
| 215 | - { label: '4练/周', value: 4 }, | ||
| 216 | - { label: '5练/周', value: 5 }, | ||
| 217 | - { label: '6练/周', value: 6 }, | ||
| 218 | - ] | ||
| 219 | - } else { | ||
| 220 | - // 多个模板 → 只显示和模板数量一样的选项 | ||
| 221 | - return [{ label: `${count}练/周`, value: count }] | 151 | + if (templateCount.value > 1) { |
| 152 | + return [{ label: templateCount.value + '练/周', value: templateCount.value }] | ||
| 222 | } | 153 | } |
| 154 | + return [1, 2, 3, 4, 5, 6].map((n) => ({ label: n + '练/周', value: n })) | ||
| 223 | }) | 155 | }) |
| 224 | 156 | ||
| 157 | +// ==================== 静态下拉选项 ==================== | ||
| 158 | +const trainingSceneOptions = [ | ||
| 159 | + { label: '不限', value: 1 }, | ||
| 160 | + { label: '健身房', value: 2 }, | ||
| 161 | + { label: '仅哑铃', value: 3 }, | ||
| 162 | + { label: '仅哑铃+杠铃', value: 4 } | ||
| 163 | +] | ||
| 164 | + | ||
| 165 | +const difficultyLevelOptions = [ | ||
| 166 | + { label: '初阶', value: 1 }, | ||
| 167 | + { label: '中阶', value: 2 }, | ||
| 168 | + { label: '高阶', value: 3 } | ||
| 169 | +] | ||
| 170 | + | ||
| 171 | +const targetPeopleOptions = [ | ||
| 172 | + { label: '不限', value: 1 }, | ||
| 173 | + { label: '青少年', value: 2 }, | ||
| 174 | + { label: '成年人', value: 3 }, | ||
| 175 | + { label: '中年人', value: 4 }, | ||
| 176 | + { label: '运动损伤', value: 5 } | ||
| 177 | +] | ||
| 178 | + | ||
| 179 | +// ==================== 频率同步 ==================== | ||
| 225 | const syncFrequency = () => { | 180 | const syncFrequency = () => { |
| 226 | - const count = templateCount.value | ||
| 227 | - if (count > 1) { | ||
| 228 | - // 超过1个模板 → 强制频率 = 模板数量 | ||
| 229 | - SonFormData.value.frequencyPerWeek = count | 181 | + if (templateCount.value > 1) { |
| 182 | + SonFormData.value.frequencyPerWeek = templateCount.value | ||
| 230 | } | 183 | } |
| 231 | } | 184 | } |
| 232 | 185 | ||
| 233 | -// 监听模板数量变化,自动更新频率 | ||
| 234 | -watch(templateCount, () => { | ||
| 235 | - syncFrequency() | ||
| 236 | -}) | 186 | +watch(templateCount, syncFrequency) |
| 237 | 187 | ||
| 188 | +// ==================== 表单验证规则 ==================== | ||
| 238 | const formRules = reactive({ | 189 | const formRules = reactive({ |
| 239 | name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }], | 190 | name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }], |
| 240 | durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }], | 191 | durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }], |
| 241 | frequencyPerWeek: [{ required: true, message: '请输入训练频率', trigger: 'change' }], | 192 | frequencyPerWeek: [{ required: true, message: '请输入训练频率', trigger: 'change' }], |
| 242 | - difficultyLevel: [{ required: true, message: '请输入难度', trigger: 'change' }], | ||
| 243 | - targetPeople: [{ required: true, message: '请输入人群', trigger: 'change' }], | ||
| 244 | - trainingScene: [{ required: true, message: '请输入场景', trigger: 'change' }], | ||
| 245 | - urlCover: [{ required: true, message: '输入图片', trigger: 'change' }], | ||
| 246 | - // equipmentsSummary: [{ required: true, message: '请输入所需器械汇总', trigger: 'blur' }], | 193 | + difficultyLevel: [{ required: true, message: '请选择难度', trigger: 'change' }], |
| 194 | + targetPeople: [{ required: true, message: '请选择人群', trigger: 'change' }], | ||
| 195 | + trainingScene: [{ required: true, message: '请选择场景', trigger: 'change' }], | ||
| 196 | + urlCover: [{ required: true, message: '请上传封面图', trigger: 'change' }], | ||
| 247 | introduction: [{ required: true, message: '请输入计划介绍', trigger: 'blur' }], | 197 | introduction: [{ required: true, message: '请输入计划介绍', trigger: 'blur' }], |
| 248 | categoryId: [{ required: true, message: '请选择计划分类', trigger: 'change' }], | 198 | categoryId: [{ required: true, message: '请选择计划分类', trigger: 'change' }], |
| 249 | templateIds: [{ | 199 | templateIds: [{ |
| @@ -254,93 +204,74 @@ const formRules = reactive({ | @@ -254,93 +204,74 @@ const formRules = reactive({ | ||
| 254 | return callback(new Error('至少添加一个模板')) | 204 | return callback(new Error('至少添加一个模板')) |
| 255 | } | 205 | } |
| 256 | for (let i = 0; i < value.length; i++) { | 206 | for (let i = 0; i < value.length; i++) { |
| 257 | - const item = value[i] | ||
| 258 | - if (!item.templateId) { | ||
| 259 | - return callback(new Error(`第 ${i + 1} 个模板未选择`)) | 207 | + if (!value[i].templateId) { |
| 208 | + return callback(new Error('第 ' + (i + 1) + ' 个模板未选择')) | ||
| 260 | } | 209 | } |
| 261 | - if (!item.sortOrder || item.sortOrder < 1) { | ||
| 262 | - return callback(new Error(`第 ${i + 1} 个排序号必须≥1`)) | 210 | + if (!value[i].sortOrder || value[i].sortOrder < 1) { |
| 211 | + return callback(new Error('第 ' + (i + 1) + ' 个排序号必须≥1')) | ||
| 263 | } | 212 | } |
| 264 | } | 213 | } |
| 265 | callback() | 214 | callback() |
| 266 | } | 215 | } |
| 267 | - }], | 216 | + }] |
| 268 | }) | 217 | }) |
| 269 | 218 | ||
| 270 | -watch(dialogVisible, async (val) => { | ||
| 271 | - if (!val) return | ||
| 272 | - // 重置模板详情缓存,解决旧数据残留问题 | ||
| 273 | - templateDetailMap.value = {} | ||
| 274 | - templateLoading.value = {} | ||
| 275 | - SonFormData.value = { | ||
| 276 | - id: undefined, | ||
| 277 | - categoryId: 0, | ||
| 278 | - name: '', | ||
| 279 | - urlCover: '', | ||
| 280 | - introduction: '', | ||
| 281 | - durationWeeks: 1, | ||
| 282 | - frequencyPerWeek: 1, | ||
| 283 | - difficultyLevel: 1, | ||
| 284 | - targetPeople: 1, | ||
| 285 | - trainingScene: 1, | ||
| 286 | - equipmentsSummary: '', | ||
| 287 | - templateIds: [{ templateId: undefined, sortOrder: 1 }] | ||
| 288 | - } | ||
| 289 | - // 新增 | ||
| 290 | - if (!props.formData || Object.keys(props.formData).length === 0 || props.formData.id == null) { | ||
| 291 | - // 控制使用哪个接口 | ||
| 292 | - isEdit.value = false // 强制写死 | ||
| 293 | - // 新增:重置清除前面编辑可能留下的校验 | 219 | +// ==================== 弹窗打开时初始化 ==================== |
| 220 | +const initAdd = () => { | ||
| 221 | + isEdit.value = false | ||
| 222 | + SonFormData.value = defaultFormData() | ||
| 294 | formRef.value?.clearValidate() | 223 | formRef.value?.clearValidate() |
| 295 | - console.log('新增接口isEdit的值', isEdit) | ||
| 296 | - } else { | 224 | +} |
| 225 | + | ||
| 226 | +const initEdit = async () => { | ||
| 297 | isEdit.value = true | 227 | isEdit.value = true |
| 298 | - // 编辑:赋值 | ||
| 299 | - // 调计划详情接口 | ||
| 300 | const res = await trainingPlanApi.getTrainingPlanDetail(props.formData.id) | 228 | const res = await trainingPlanApi.getTrainingPlanDetail(props.formData.id) |
| 301 | - console.log('计划详情数据res================', res) | ||
| 302 | - console.log('计划详情数据res.data================', res.data) | ||
| 303 | - console.log('计划详情数据res.equipmentsSummary================', res.equipmentsSummary) | ||
| 304 | if (!res || !res.id) { | 229 | if (!res || !res.id) { |
| 305 | ElMessage.error('获取计划详情失败') | 230 | ElMessage.error('获取计划详情失败') |
| 306 | - console.log('获取计划详情失败') | ||
| 307 | return | 231 | return |
| 308 | } | 232 | } |
| 309 | SonFormData.value = res | 233 | SonFormData.value = res |
| 310 | - // 2. 补 res 里缺失、但父组件有值的字段 | 234 | + // 补全父组件传入但详情未返回的字段 |
| 311 | SonFormData.value.trainingScene = props.formData.trainingScene | 235 | SonFormData.value.trainingScene = props.formData.trainingScene |
| 312 | SonFormData.value.targetPeople = props.formData.targetPeople | 236 | SonFormData.value.targetPeople = props.formData.targetPeople |
| 313 | - // 详情接口返回的字段和SonFormData的不一样,需要处理 | ||
| 314 | SonFormData.value.categoryId = props.formData.categoryId | 237 | SonFormData.value.categoryId = props.formData.categoryId |
| 315 | - console.log("===== 父组件传过来的完整数据 阉割版=====") | ||
| 316 | - console.log(props.formData) | ||
| 317 | - // // 详情返回的是数组,但是这里要的是字符串 | 238 | + // 处理 equipmentsSummary 数组转字符串 |
| 318 | if (Array.isArray(res.equipmentsSummary)) { | 239 | if (Array.isArray(res.equipmentsSummary)) { |
| 319 | SonFormData.value.equipmentsSummary = res.equipmentsSummary.join(',') | 240 | SonFormData.value.equipmentsSummary = res.equipmentsSummary.join(',') |
| 320 | } | 241 | } |
| 321 | - // 赋值模板的id和排序 | 242 | + // 映射模板 ID 和排序 |
| 322 | if (res.templates && res.templates.length) { | 243 | if (res.templates && res.templates.length) { |
| 323 | - SonFormData.value.templateIds = res.templates.map(item => ({ | 244 | + SonFormData.value.templateIds = res.templates.map((item) => ({ |
| 324 | templateId: item.id, | 245 | templateId: item.id, |
| 325 | sortOrder: item.sortOrder | 246 | sortOrder: item.sortOrder |
| 326 | })) | 247 | })) |
| 327 | } | 248 | } |
| 328 | - // 防止模板为空,导致模板选项不出现(虽然不太可能,因为这个是必填项) | ||
| 329 | if (!SonFormData.value.templateIds || SonFormData.value.templateIds.length === 0) { | 249 | if (!SonFormData.value.templateIds || SonFormData.value.templateIds.length === 0) { |
| 330 | SonFormData.value.templateIds = [{ templateId: undefined, sortOrder: 1 }] | 250 | SonFormData.value.templateIds = [{ templateId: undefined, sortOrder: 1 }] |
| 331 | } | 251 | } |
| 332 | - // // 编辑状态:自动加载已有模板的详情 | ||
| 333 | - SonFormData.value.templateIds.forEach((item, index) => { | ||
| 334 | - if (item.templateId) { | ||
| 335 | - loadTemplateDetail(index, item.templateId) | ||
| 336 | - } | 252 | + // 自动加载已有模板的详情 |
| 253 | + SonFormData.value.templateIds.forEach((item, idx) => { | ||
| 254 | + if (item.templateId) loadTemplateDetail(idx, item.templateId) | ||
| 337 | }) | 255 | }) |
| 338 | syncFrequency() | 256 | syncFrequency() |
| 257 | + formRef.value?.clearValidate() | ||
| 258 | +} | ||
| 259 | + | ||
| 260 | +watch(dialogVisible, async (val) => { | ||
| 261 | + if (!val) return | ||
| 262 | + templateDetailMap.value = {} | ||
| 263 | + templateLoading.value = {} | ||
| 264 | + SonFormData.value = defaultFormData() | ||
| 265 | + | ||
| 266 | + if (!props.formData || Object.keys(props.formData).length === 0 || props.formData.id == null) { | ||
| 267 | + initAdd() | ||
| 268 | + } else { | ||
| 269 | + await initEdit() | ||
| 339 | } | 270 | } |
| 340 | formRef.value?.clearValidate() | 271 | formRef.value?.clearValidate() |
| 341 | }) | 272 | }) |
| 342 | -// ==========================加载数据====================== | ||
| 343 | -// 加载模板列表 | 273 | + |
| 274 | +// ==================== 数据加载 ==================== | ||
| 344 | const loadTemplateList = async () => { | 275 | const loadTemplateList = async () => { |
| 345 | const res = await TrainingTemplatesApi.getTrainingTemplatesPage({ pageNo: 1, pageSize: 100 }) | 276 | const res = await TrainingTemplatesApi.getTrainingTemplatesPage({ pageNo: 1, pageSize: 100 }) |
| 346 | templateList.value = res.list || [] | 277 | templateList.value = res.list || [] |
| @@ -348,49 +279,72 @@ const loadTemplateList = async () => { | @@ -348,49 +279,72 @@ const loadTemplateList = async () => { | ||
| 348 | ElMessage.warning('暂无可用训练模板,请先创建模板') | 279 | ElMessage.warning('暂无可用训练模板,请先创建模板') |
| 349 | } | 280 | } |
| 350 | } | 281 | } |
| 351 | -// 获取模板详情并缓存 | 282 | + |
| 352 | const loadTemplateDetail = async (index, templateId) => { | 283 | const loadTemplateDetail = async (index, templateId) => { |
| 353 | if (!templateId) return | 284 | if (!templateId) return |
| 354 | - // 标记加载中 | ||
| 355 | templateLoading.value[index] = true | 285 | templateLoading.value[index] = true |
| 356 | try { | 286 | try { |
| 357 | const res = await TrainingTemplatesApi.getTrainingTemplates(templateId) | 287 | const res = await TrainingTemplatesApi.getTrainingTemplates(templateId) |
| 358 | - // 给每个单元、动作加展开状态(默认收起) | ||
| 359 | - res.units?.forEach(unit => { | 288 | + res.units?.forEach((unit) => { |
| 360 | unit.isExpand = false | 289 | unit.isExpand = false |
| 361 | - unit.exercises?.forEach(exercise => { | ||
| 362 | - exercise.isExpand = false | 290 | + unit.exercises?.forEach((ex) => { ex.isExpand = false }) |
| 363 | }) | 291 | }) |
| 364 | - }) | ||
| 365 | - // 缓存详情数据 | ||
| 366 | templateDetailMap.value[index] = res | 292 | templateDetailMap.value[index] = res |
| 367 | } catch (err) { | 293 | } catch (err) { |
| 368 | - ElMessage.error(`获取模板${index + 1}详情失败`) | ||
| 369 | - console.error(err) | 294 | + ElMessage.error('获取模板' + (index + 1) + '详情失败') |
| 370 | } finally { | 295 | } finally { |
| 371 | - // 取消加载中 | ||
| 372 | templateLoading.value[index] = false | 296 | templateLoading.value[index] = false |
| 373 | } | 297 | } |
| 374 | } | 298 | } |
| 375 | 299 | ||
| 376 | -// 加载计划分类列表 | ||
| 377 | const loadCategoryList = async () => { | 300 | const loadCategoryList = async () => { |
| 378 | - const res = await trainingPlanApi.getPlanCategoryPage({ pageNo: 1, pageSize: 100 }) | ||
| 379 | - categoryList.value = res.list || [] | 301 | + const res = await trainingPlanApi.getPlanList() |
| 302 | + categoryList.value = res || [] | ||
| 380 | } | 303 | } |
| 381 | -// 强制重新加载当前 index 的模板详情(解决重复选择不触发问题) | 304 | + |
| 382 | const reloadTemplateDetail = (index) => { | 305 | const reloadTemplateDetail = (index) => { |
| 383 | const id = SonFormData.value.templateIds[index]?.templateId | 306 | const id = SonFormData.value.templateIds[index]?.templateId |
| 384 | if (id) loadTemplateDetail(index, id) | 307 | if (id) loadTemplateDetail(index, id) |
| 385 | } | 308 | } |
| 386 | -// 取消 | ||
| 387 | -const handleCancel = () => { | ||
| 388 | - dialogVisible.value = false | ||
| 389 | - ElMessage.info('取消保存') | 309 | + |
| 310 | +// ==================== 模板增删 ==================== | ||
| 311 | +const addTemplate = (index) => { | ||
| 312 | + const newIds = [...SonFormData.value.templateIds] | ||
| 313 | + newIds.splice(index + 1, 0, { | ||
| 314 | + templateId: undefined, | ||
| 315 | + sortOrder: SonFormData.value.templateIds.length + 1 | ||
| 316 | + }) | ||
| 317 | + SonFormData.value.templateIds = newIds | ||
| 318 | + reindexDetailMap() | ||
| 319 | + syncFrequency() | ||
| 320 | +} | ||
| 321 | + | ||
| 322 | +const removeTemplate = (index) => { | ||
| 323 | + if (SonFormData.value.templateIds.length <= 1) return | ||
| 324 | + SonFormData.value.templateIds = SonFormData.value.templateIds.filter((_, i) => i !== index) | ||
| 325 | + reindexDetailMap() | ||
| 326 | + syncFrequency() | ||
| 390 | } | 327 | } |
| 391 | -// 提交 | 328 | + |
| 329 | +const reindexDetailMap = () => { | ||
| 330 | + const oldMap = { ...templateDetailMap.value } | ||
| 331 | + templateDetailMap.value = {} | ||
| 332 | + templateLoading.value = {} | ||
| 333 | + SonFormData.value.templateIds.forEach((item, i) => { | ||
| 334 | + if (item.templateId && oldMap[i] !== undefined) { | ||
| 335 | + templateDetailMap.value[i] = oldMap[i] | ||
| 336 | + } | ||
| 337 | + }) | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +// ==================== 提交 ==================== | ||
| 392 | const handleSubmit = async () => { | 341 | const handleSubmit = async () => { |
| 342 | + if (!formRef.value) return | ||
| 343 | + try { | ||
| 393 | await formRef.value.validate() | 344 | await formRef.value.validate() |
| 345 | + } catch { | ||
| 346 | + return | ||
| 347 | + } | ||
| 394 | loading.value = true | 348 | loading.value = true |
| 395 | try { | 349 | try { |
| 396 | if (isEdit.value) { | 350 | if (isEdit.value) { |
| @@ -398,122 +352,112 @@ const handleSubmit = async () => { | @@ -398,122 +352,112 @@ const handleSubmit = async () => { | ||
| 398 | } else { | 352 | } else { |
| 399 | await trainingPlanApi.createTrainingPlan(SonFormData.value) | 353 | await trainingPlanApi.createTrainingPlan(SonFormData.value) |
| 400 | } | 354 | } |
| 401 | - console.log('调用接口后isEdit的值', isEdit) | ||
| 402 | ElMessage.success(isEdit.value ? '编辑成功' : '创建成功') | 355 | ElMessage.success(isEdit.value ? '编辑成功' : '创建成功') |
| 403 | dialogVisible.value = false | 356 | dialogVisible.value = false |
| 404 | emit('submit') | 357 | emit('submit') |
| 405 | } catch (err) { | 358 | } catch (err) { |
| 406 | - ElMessage.error((isEdit.value ? '编辑' : '创建') + '失败:' + err.message) | 359 | + ElMessage.error((isEdit.value ? '编辑' : '创建') + '失败') |
| 407 | } finally { | 360 | } finally { |
| 408 | loading.value = false | 361 | loading.value = false |
| 409 | } | 362 | } |
| 410 | } | 363 | } |
| 411 | 364 | ||
| 412 | -const Addtemplate = (index) => { | ||
| 413 | - const newTemplate = { | ||
| 414 | - templateId: 1, | ||
| 415 | - sortOrder: SonFormData.value.templateIds.length + 1 | ||
| 416 | - } | ||
| 417 | - const newTemplateIds = [...SonFormData.value.templateIds] | ||
| 418 | - newTemplateIds.splice(index + 1, 0, newTemplate) | ||
| 419 | - SonFormData.value.templateIds = newTemplateIds | ||
| 420 | - // 重新同步所有模板详情(不移除、不清空,只重新对应位置) | ||
| 421 | - const oldMap = { ...templateDetailMap.value } | ||
| 422 | - templateDetailMap.value = {} | ||
| 423 | - templateLoading.value = {} | ||
| 424 | - SonFormData.value.templateIds.forEach((item, i) => { | ||
| 425 | - if (item.templateId && oldMap[i] !== undefined) { | ||
| 426 | - templateDetailMap.value[i] = oldMap[i] | ||
| 427 | - } | ||
| 428 | - }) | ||
| 429 | - syncFrequency() | ||
| 430 | -} | ||
| 431 | - | ||
| 432 | -const deltemplate = (index) => { | ||
| 433 | - if (SonFormData.value.templateIds.length <= 1) return | ||
| 434 | - SonFormData.value.templateIds = SonFormData.value.templateIds.filter((_, i) => i !== index) | ||
| 435 | - // 重新同步所有模板详情(不移除、不清空,只重新对应位置) | ||
| 436 | - const oldMap = { ...templateDetailMap.value } | ||
| 437 | - templateDetailMap.value = {} | ||
| 438 | - templateLoading.value = {} | ||
| 439 | - SonFormData.value.templateIds.forEach((item, i) => { | ||
| 440 | - if (item.templateId && oldMap[i] !== undefined) { | ||
| 441 | - templateDetailMap.value[i] = oldMap[i] | ||
| 442 | - } | ||
| 443 | - }) | ||
| 444 | - syncFrequency() | ||
| 445 | -} | 365 | +// ==================== 初始化 ==================== |
| 446 | onMounted(() => { | 366 | onMounted(() => { |
| 447 | loadTemplateList() | 367 | loadTemplateList() |
| 448 | loadCategoryList() | 368 | loadCategoryList() |
| 449 | }) | 369 | }) |
| 450 | </script> | 370 | </script> |
| 371 | + | ||
| 451 | <style scoped> | 372 | <style scoped> |
| 452 | -/* 模板详情容器 */ | 373 | +/* ==================== 工具类 ==================== */ |
| 374 | +.w-full { | ||
| 375 | + width: 100%; | ||
| 376 | +} | ||
| 377 | + | ||
| 378 | +/* ==================== 模板卡片 ==================== */ | ||
| 379 | +.template-block { | ||
| 380 | + margin-bottom: 16px; | ||
| 381 | + padding: 16px 20px 4px; | ||
| 382 | + background: var(--el-fill-color-lighter); | ||
| 383 | + border: 1px solid var(--el-border-color-lighter); | ||
| 384 | + border-radius: 8px; | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +.template-block-toolbar { | ||
| 388 | + display: flex; | ||
| 389 | + justify-content: flex-end; | ||
| 390 | + gap: 10px; | ||
| 391 | + margin-bottom: 8px; | ||
| 392 | +} | ||
| 393 | + | ||
| 394 | +/* ==================== 模板详情 ==================== */ | ||
| 453 | .template-detail-container { | 395 | .template-detail-container { |
| 454 | width: 100%; | 396 | width: 100%; |
| 455 | padding: 12px; | 397 | padding: 12px; |
| 456 | - background: #f9fafb; | 398 | + background: var(--el-fill-color-lighter); |
| 457 | border-radius: 6px; | 399 | border-radius: 6px; |
| 458 | box-sizing: border-box; | 400 | box-sizing: border-box; |
| 459 | } | 401 | } |
| 460 | 402 | ||
| 461 | -/* 训练单元容器 */ | 403 | +/* ==================== 训练单元 ==================== */ |
| 462 | .unit-container { | 404 | .unit-container { |
| 463 | width: 100%; | 405 | width: 100%; |
| 464 | margin-bottom: 8px; | 406 | margin-bottom: 8px; |
| 465 | - background: #fff; | ||
| 466 | - border: 1px solid #dcdfe6; | 407 | + background: var(--el-bg-color); |
| 408 | + border: 1px solid var(--el-border-color); | ||
| 467 | border-radius: 4px; | 409 | border-radius: 4px; |
| 468 | } | 410 | } |
| 469 | 411 | ||
| 470 | -/* 训练单元行(点击展开) */ | ||
| 471 | .unit-row { | 412 | .unit-row { |
| 472 | display: flex; | 413 | display: flex; |
| 414 | + align-items: center; | ||
| 473 | padding: 12px 16px; | 415 | padding: 12px 16px; |
| 474 | font-size: 14px; | 416 | font-size: 14px; |
| 475 | cursor: pointer; | 417 | cursor: pointer; |
| 476 | - border-bottom: 1px solid #ebeef5; | ||
| 477 | - align-items: center; | 418 | + border-bottom: 1px solid var(--el-border-color-lighter); |
| 478 | } | 419 | } |
| 479 | 420 | ||
| 480 | .unit-row:hover { | 421 | .unit-row:hover { |
| 481 | - background: #f5f7fa; | 422 | + background: var(--el-fill-color-light); |
| 482 | } | 423 | } |
| 483 | 424 | ||
| 484 | .expand-icon { | 425 | .expand-icon { |
| 485 | margin-right: 8px; | 426 | margin-right: 8px; |
| 486 | - color: #909399; | 427 | + color: var(--el-text-color-secondary); |
| 487 | transition: transform 0.3s; | 428 | transition: transform 0.3s; |
| 488 | } | 429 | } |
| 489 | 430 | ||
| 490 | -.unit-name { | ||
| 491 | - font-weight: 500; | ||
| 492 | - color: #303133; | 431 | +.expand-icon.is-expanded { |
| 432 | + transform: rotate(90deg); | ||
| 493 | } | 433 | } |
| 494 | 434 | ||
| 495 | -/* 组详情容器 */ | ||
| 496 | -.set-container { | ||
| 497 | - padding: 12px; | ||
| 498 | - border-top: 1px solid #ebeef5; | 435 | +.unit-name { |
| 436 | + font-weight: 500; | ||
| 437 | + color: var(--el-text-color-primary); | ||
| 499 | } | 438 | } |
| 500 | 439 | ||
| 501 | -.set-header { | 440 | +/* ==================== 训练组详情 ==================== */ |
| 441 | +.set-header, | ||
| 442 | +.set-row { | ||
| 502 | display: grid; | 443 | display: grid; |
| 503 | padding: 8px 12px; | 444 | padding: 8px 12px; |
| 504 | - font-weight: 500; | ||
| 505 | - color: #606266; | ||
| 506 | text-align: center; | 445 | text-align: center; |
| 507 | - background: #f5f7fa; | ||
| 508 | grid-template-columns: repeat(5, 1fr); | 446 | grid-template-columns: repeat(5, 1fr); |
| 509 | } | 447 | } |
| 510 | 448 | ||
| 449 | +.set-header { | ||
| 450 | + font-weight: 500; | ||
| 451 | + color: var(--el-text-color-regular); | ||
| 452 | + background: var(--el-fill-color-light); | ||
| 453 | +} | ||
| 454 | + | ||
| 511 | .set-row { | 455 | .set-row { |
| 512 | - display: grid; | ||
| 513 | - padding: 8px 12px; | ||
| 514 | - color: #303133; | ||
| 515 | - text-align: center; | ||
| 516 | - border-top: 1px solid #ebeef5; | ||
| 517 | - grid-template-columns: repeat(5, 1fr); | 456 | + color: var(--el-text-color-primary); |
| 457 | + border-top: 1px solid var(--el-border-color-lighter); | ||
| 458 | +} | ||
| 459 | + | ||
| 460 | +.exercise-container { | ||
| 461 | + padding: 4px; | ||
| 518 | } | 462 | } |
| 519 | </style> | 463 | </style> |
| 1 | <template> | 1 | <template> |
| 2 | <div class="page-container"> | 2 | <div class="page-container"> |
| 3 | - <el-card class="search-bar"> | ||
| 4 | - <div class="search-item"> | ||
| 5 | - <label class="search-label">计划名称</label> | ||
| 6 | - <el-input v-model="searchForm.name" placeholder="请输入计划名称" class="search-input" @keyup.enter="handleSearch" | ||
| 7 | - clearable /> | ||
| 8 | - <div class="search-buttons"> | 3 | + <!-- 搜索栏 --> |
| 4 | + <ContentWrap> | ||
| 5 | + <el-form :inline="true" :model="queryParams" class="search-form"> | ||
| 6 | + <el-form-item label="计划名称"> | ||
| 7 | + <el-input | ||
| 8 | + v-model="queryParams.name" | ||
| 9 | + placeholder="请输入计划名称" | ||
| 10 | + clearable | ||
| 11 | + class="!w-220px" | ||
| 12 | + @keyup.enter="handleSearch" | ||
| 13 | + /> | ||
| 14 | + </el-form-item> | ||
| 15 | + <el-form-item> | ||
| 9 | <el-button type="primary" @click="handleSearch"> | 16 | <el-button type="primary" @click="handleSearch"> |
| 10 | - <el-icon> | ||
| 11 | - <Search /> | ||
| 12 | - </el-icon> | ||
| 13 | - 搜索 | 17 | + <Icon icon="ep:search" class="mr-5px" />搜索 |
| 14 | </el-button> | 18 | </el-button> |
| 15 | <el-button @click="handleReset"> | 19 | <el-button @click="handleReset"> |
| 16 | - <el-icon> | ||
| 17 | - <Refresh /> | ||
| 18 | - </el-icon> | ||
| 19 | - 重置 | 20 | + <Icon icon="ep:refresh" class="mr-5px" />重置 |
| 20 | </el-button> | 21 | </el-button> |
| 21 | <el-button type="primary" @click="openAddDialog"> | 22 | <el-button type="primary" @click="openAddDialog"> |
| 22 | - <el-icon> | ||
| 23 | - <Plus /> | ||
| 24 | - </el-icon> | ||
| 25 | - 新增 | 23 | + <Icon icon="ep:plus" class="mr-5px" />新增 |
| 26 | </el-button> | 24 | </el-button> |
| 27 | - </div> | ||
| 28 | - </div> | ||
| 29 | - </el-card> | ||
| 30 | - <!-- 列表区域 --> | ||
| 31 | - <el-card class="table-card"> | ||
| 32 | - <el-table v-loading="loading" :data="tableData" stripe style="width: 100%;"> | ||
| 33 | - <el-table-column prop="name" label="计划名称" align="center" sortable="custom" /> | 25 | + </el-form-item> |
| 26 | + </el-form> | ||
| 27 | + </ContentWrap> | ||
| 28 | + | ||
| 29 | + <!-- 表格区域 --> | ||
| 30 | + <ContentWrap> | ||
| 31 | + <el-table v-loading="loading" :data="tableData" stripe> | ||
| 32 | + <el-table-column prop="name" label="计划名称" align="center" /> | ||
| 34 | <el-table-column label="计划周期" align="center" :formatter="formatDurationWeeks" /> | 33 | <el-table-column label="计划周期" align="center" :formatter="formatDurationWeeks" /> |
| 35 | <el-table-column label="场景" align="center" :formatter="formatTrainingScene" /> | 34 | <el-table-column label="场景" align="center" :formatter="formatTrainingScene" /> |
| 36 | <el-table-column label="训练频率" align="center" :formatter="formatFrequency" /> | 35 | <el-table-column label="训练频率" align="center" :formatter="formatFrequency" /> |
| 37 | <el-table-column label="难度" align="center" :formatter="formatDifficulty" /> | 36 | <el-table-column label="难度" align="center" :formatter="formatDifficulty" /> |
| 38 | <el-table-column label="适合人群" align="center" :formatter="formatTargetPeople" /> | 37 | <el-table-column label="适合人群" align="center" :formatter="formatTargetPeople" /> |
| 39 | <el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" /> | 38 | <el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" /> |
| 40 | - <el-table-column prop="updateTime" label="更新时间" :formatter="dateFormatter" /> | 39 | + <el-table-column prop="updateTime" label="更新时间" align="center" :formatter="dateFormatter" /> |
| 41 | <el-table-column label="操作" align="center" width="150"> | 40 | <el-table-column label="操作" align="center" width="150"> |
| 42 | <template #default="scope"> | 41 | <template #default="scope"> |
| 43 | - <el-button type="text" size="small" style=" margin-right: 8px;color: #409EFF;" | ||
| 44 | - @click="handleEdit(scope.row)">编辑</el-button> | ||
| 45 | - <el-button type="text" size="small" style="color: #F56C6C;" | ||
| 46 | - @click="handleDelete(scope.row.id)">删除</el-button> | 42 | + <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button> |
| 43 | + <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button> | ||
| 47 | </template> | 44 | </template> |
| 48 | </el-table-column> | 45 | </el-table-column> |
| 49 | </el-table> | 46 | </el-table> |
| 50 | 47 | ||
| 51 | - <!-- 分页组件 --> | ||
| 52 | - <div class="pagination-wrapper" style="padding: 12px 16px;"> | ||
| 53 | - <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" | ||
| 54 | - :total="pagination.total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" | ||
| 55 | - style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> | ||
| 56 | - </div> | ||
| 57 | - </el-card> | ||
| 58 | - <!-- 弹窗组件 --> | ||
| 59 | - <trainingPlanDialog v-model:visible="dialogVisible" :form-data="formData" :title="dialogTitle" @submit="getList" /> | 48 | + <Pagination |
| 49 | + :total="queryParams.total" | ||
| 50 | + v-model:page="queryParams.pageNo" | ||
| 51 | + v-model:limit="queryParams.pageSize" | ||
| 52 | + @pagination="getList" | ||
| 53 | + /> | ||
| 54 | + </ContentWrap> | ||
| 55 | + | ||
| 56 | + <!-- 弹窗 --> | ||
| 57 | + <TrainingPlanDialog | ||
| 58 | + v-model:visible="dialogVisible" | ||
| 59 | + :form-data="formData" | ||
| 60 | + :title="dialogTitle" | ||
| 61 | + @submit="getList" | ||
| 62 | + /> | ||
| 60 | </div> | 63 | </div> |
| 61 | </template> | 64 | </template> |
| 62 | 65 | ||
| 63 | <script setup> | 66 | <script setup> |
| 64 | -import { ref, reactive } from 'vue' | ||
| 65 | -import { ElMessage } from 'element-plus' | ||
| 66 | -import { Search, Refresh, Plus } from '@element-plus/icons-vue' | ||
| 67 | -import lostImg from '@/assets/imgs/lost.png' // 丢失图片占位图 | ||
| 68 | -import { dateFormatter } from '@/utils/formatTime' | 67 | +import { ref, reactive, onMounted } from 'vue' |
| 68 | +import { ElMessage, ElMessageBox } from 'element-plus' | ||
| 69 | + | ||
| 69 | import { trainingPlanApi } from '@/api/store/training/trainingPlan' | 70 | import { trainingPlanApi } from '@/api/store/training/trainingPlan' |
| 70 | import TrainingPlanDialog from './trainingPlanDialog.vue' | 71 | import TrainingPlanDialog from './trainingPlanDialog.vue' |
| 72 | +import { dateFormatter } from '@/utils/formatTime' | ||
| 73 | + | ||
| 74 | +// ==================== 常量映射 ==================== | ||
| 75 | +const DIFFICULTY_MAP = { 1: '初阶', 2: '中阶', 3: '高阶' } | ||
| 76 | +const TARGET_PEOPLE_MAP = { 1: '不限', 2: '青少年', 3: '成年人', 4: '中年人', 5: '运动损伤' } | ||
| 77 | +const TRAINING_SCENE_MAP = { 1: '不限', 2: '健身房', 3: '仅哑铃', 4: '仅哑铃 + 杠铃' } | ||
| 78 | + | ||
| 79 | +// ==================== 查询参数 ==================== | ||
| 80 | +const queryParams = reactive({ | ||
| 81 | + pageNo: 1, | ||
| 82 | + pageSize: 10, | ||
| 83 | + total: 0, | ||
| 84 | + name: '' | ||
| 85 | +}) | ||
| 71 | const loading = ref(false) | 86 | const loading = ref(false) |
| 72 | -const isEdit = ref(true) | 87 | +const tableData = ref([]) |
| 88 | + | ||
| 89 | +// ==================== 弹窗状态 ==================== | ||
| 73 | const dialogVisible = ref(false) | 90 | const dialogVisible = ref(false) |
| 74 | const dialogTitle = ref('') | 91 | const dialogTitle = ref('') |
| 75 | const formData = ref({ | 92 | const formData = ref({ |
| 76 | templateIds: [{ templateId: undefined, sortOrder: 1 }] | 93 | templateIds: [{ templateId: undefined, sortOrder: 1 }] |
| 77 | -}) // 传给弹窗的表单数据 | ||
| 78 | -// 分页参数 | ||
| 79 | -const pagination = reactive({ | ||
| 80 | - pageNo: 1, | ||
| 81 | - pageSize: 10, | ||
| 82 | - total: 0 | ||
| 83 | }) | 94 | }) |
| 84 | -// 文本映射============= | ||
| 85 | -// 难度映射 | ||
| 86 | -const difficultyMap = { | ||
| 87 | - 1: '初阶', | ||
| 88 | - 2: '中阶', | ||
| 89 | - 3: '高阶' | ||
| 90 | -} | ||
| 91 | - | ||
| 92 | -// 适合人群映射 | ||
| 93 | -const targetPeopleMap = { | ||
| 94 | - 1: '不限', | ||
| 95 | - 2: '青少年', | ||
| 96 | - 3: '成年人', | ||
| 97 | - 4: '中年人', | ||
| 98 | - 5: '运动损伤' | ||
| 99 | -} | ||
| 100 | - | ||
| 101 | -// 训练场景映射 | ||
| 102 | -const trainingSceneMap = { | ||
| 103 | - 1: '不限', | ||
| 104 | - 2: '健身房', | ||
| 105 | - 3: '仅哑铃', | ||
| 106 | - 4: '仅哑铃 + 杠铃' | ||
| 107 | -} | ||
| 108 | -// 格式化难度 | ||
| 109 | -const formatDifficulty = (row) => { | ||
| 110 | - return difficultyMap[row.difficultyLevel] || '未知' | ||
| 111 | -} | ||
| 112 | - | ||
| 113 | -// 格式化适合人群 | ||
| 114 | -const formatTargetPeople = (row) => { | ||
| 115 | - return targetPeopleMap[row.targetPeople] || '未知' | ||
| 116 | -} | ||
| 117 | - | ||
| 118 | -// 格式化训练场景 | ||
| 119 | -const formatTrainingScene = (row) => { | ||
| 120 | - return trainingSceneMap[row.trainingScene] || '未知' | ||
| 121 | -} | ||
| 122 | - | ||
| 123 | -// 格式化计划周期(在数字后加“周”) | ||
| 124 | -const formatDurationWeeks = (row) => { | ||
| 125 | - return row.durationWeeks ? `${row.durationWeeks}周` : '0周' | ||
| 126 | -} | ||
| 127 | -// 格式化训练频率(数字 + 练/周) | ||
| 128 | -const formatFrequency = (row) => { | ||
| 129 | - return row.frequencyPerWeek ? `${row.frequencyPerWeek}练/周` : '0练/周' | ||
| 130 | -} | ||
| 131 | 95 | ||
| 132 | -// 搜索表单 | ||
| 133 | -const searchForm = reactive({ | ||
| 134 | - name: '' | ||
| 135 | -}) | ||
| 136 | -const tableData = ref([]) | 96 | +// ==================== 格式化函数 ==================== |
| 97 | +const formatDifficulty = (row) => DIFFICULTY_MAP[row.difficultyLevel] || '未知' | ||
| 98 | +const formatTargetPeople = (row) => TARGET_PEOPLE_MAP[row.targetPeople] || '未知' | ||
| 99 | +const formatTrainingScene = (row) => TRAINING_SCENE_MAP[row.trainingScene] || '未知' | ||
| 100 | +const formatDurationWeeks = (row) => row.durationWeeks ? row.durationWeeks + '周' : '0周' | ||
| 101 | +const formatFrequency = (row) => row.frequencyPerWeek ? row.frequencyPerWeek + '练/周' : '0练/周' | ||
| 137 | 102 | ||
| 138 | -// 获取学员训练记录列表 | 103 | +// ==================== 数据加载 ==================== |
| 139 | const getList = async () => { | 104 | const getList = async () => { |
| 140 | loading.value = true | 105 | loading.value = true |
| 141 | try { | 106 | try { |
| 142 | - // 搜索条件 | ||
| 143 | - const params = { | ||
| 144 | - pageNo: pagination.pageNo + '', | ||
| 145 | - pageSize: pagination.pageSize + '', | ||
| 146 | - name: searchForm.name, | ||
| 147 | - } | ||
| 148 | - const res = await trainingPlanApi.getTrainingPlan(params) | 107 | + const res = await trainingPlanApi.getTrainingPlan({ |
| 108 | + pageNo: queryParams.pageNo + '', | ||
| 109 | + pageSize: queryParams.pageSize + '', | ||
| 110 | + name: queryParams.name | ||
| 111 | + }) | ||
| 149 | tableData.value = res.list || [] | 112 | tableData.value = res.list || [] |
| 150 | - pagination.total = res.total || 0 | ||
| 151 | - console.log('计划分页接口返回数据:', res) | 113 | + queryParams.total = res.total || 0 |
| 152 | } catch (err) { | 114 | } catch (err) { |
| 153 | - console.error('❌ 请求失败:', err) | ||
| 154 | ElMessage.error('请求失败,请检查网络或接口') | 115 | ElMessage.error('请求失败,请检查网络或接口') |
| 155 | } finally { | 116 | } finally { |
| 156 | loading.value = false | 117 | loading.value = false |
| 157 | } | 118 | } |
| 158 | } | 119 | } |
| 159 | 120 | ||
| 160 | -// 搜索 | 121 | +// ==================== 搜索与重置 ==================== |
| 161 | const handleSearch = () => { | 122 | const handleSearch = () => { |
| 162 | - ElMessage.success('执行搜索:' + searchForm.name) | 123 | + queryParams.pageNo = 1 |
| 163 | getList() | 124 | getList() |
| 164 | } | 125 | } |
| 165 | 126 | ||
| 166 | -// 重置 | ||
| 167 | const handleReset = () => { | 127 | const handleReset = () => { |
| 168 | - pagination.pageNo = 1 | ||
| 169 | - pagination.pageSize = 10 | ||
| 170 | - searchForm.name = '' | ||
| 171 | - ElMessage.success('已重置搜索条件') | 128 | + Object.assign(queryParams, { pageNo: 1, pageSize: 10, total: 0, name: '' }) |
| 172 | getList() | 129 | getList() |
| 173 | } | 130 | } |
| 174 | 131 | ||
| 175 | -// 新增 | 132 | +// ==================== 新增 ==================== |
| 176 | const openAddDialog = () => { | 133 | const openAddDialog = () => { |
| 177 | dialogTitle.value = '新增训练计划' | 134 | dialogTitle.value = '新增训练计划' |
| 178 | - // 新增时彻底重置表单,清空编辑残留 | ||
| 179 | - // 彻底重置 formData,100%清空所有编辑残留,和子组件 SonFormData 初始结构完全一致 | ||
| 180 | formData.value = { | 135 | formData.value = { |
| 181 | id: undefined, | 136 | id: undefined, |
| 182 | categoryId: 0, | 137 | categoryId: 0, |
| @@ -194,20 +149,14 @@ const openAddDialog = () => { | @@ -194,20 +149,14 @@ const openAddDialog = () => { | ||
| 194 | dialogVisible.value = true | 149 | dialogVisible.value = true |
| 195 | } | 150 | } |
| 196 | 151 | ||
| 197 | -// 编辑 | 152 | +// ==================== 编辑 ==================== |
| 198 | const handleEdit = (row) => { | 153 | const handleEdit = (row) => { |
| 199 | dialogTitle.value = '编辑训练计划' | 154 | dialogTitle.value = '编辑训练计划' |
| 200 | - // 保留完整表单结构,只覆盖后端有的字段 | ||
| 201 | - formData.value = { | ||
| 202 | - // 先保留完整结构(不丢字段,这里没有新增时的数据,只是我创建formData的初始数据) | ||
| 203 | - ...formData.value, | ||
| 204 | - // 再覆盖编辑数据,row里面是列表的字段,列表的字段和新增的字段的数量是不一样的。 | ||
| 205 | - ...row | ||
| 206 | - } | 155 | + formData.value = { ...formData.value, ...row } |
| 207 | dialogVisible.value = true | 156 | dialogVisible.value = true |
| 208 | } | 157 | } |
| 209 | 158 | ||
| 210 | -// 删除 | 159 | +// ==================== 删除 ==================== |
| 211 | const handleDelete = async (id) => { | 160 | const handleDelete = async (id) => { |
| 212 | try { | 161 | try { |
| 213 | await ElMessageBox.confirm('确定要删除吗?', '提示', { | 162 | await ElMessageBox.confirm('确定要删除吗?', '提示', { |
| @@ -215,77 +164,31 @@ const handleDelete = async (id) => { | @@ -215,77 +164,31 @@ const handleDelete = async (id) => { | ||
| 215 | cancelButtonText: '取消', | 164 | cancelButtonText: '取消', |
| 216 | type: 'warning' | 165 | type: 'warning' |
| 217 | }) | 166 | }) |
| 218 | - // 能走到这里,说明用户点了确定 | ||
| 219 | await trainingPlanApi.deleteTrainingPlan(id) | 167 | await trainingPlanApi.deleteTrainingPlan(id) |
| 220 | ElMessage.success('删除成功') | 168 | ElMessage.success('删除成功') |
| 169 | + getList() | ||
| 221 | } catch (err) { | 170 | } catch (err) { |
| 222 | - // 关键:判断是不是用户点击了取消 | ||
| 223 | - if (err === 'cancel' || err?.message === 'cancel') { | ||
| 224 | - ElMessage.info('已取消删除') | ||
| 225 | - return // 直接退出,不报错 | 171 | + if (err !== 'cancel') { |
| 172 | + ElMessage.error('删除失败') | ||
| 226 | } | 173 | } |
| 227 | - // 只有真正的请求错误才走这里 | ||
| 228 | - console.error('❌ 请求失败:', err) | ||
| 229 | - ElMessage.error('请求失败,请检查网络或接口') | ||
| 230 | - } finally { | ||
| 231 | - getList() // 刷新列表 | ||
| 232 | } | 174 | } |
| 233 | } | 175 | } |
| 234 | 176 | ||
| 235 | -// 处理分页大小变化 | ||
| 236 | -const handleSizeChange = (val) => { | ||
| 237 | - pagination.pageSize = val | ||
| 238 | - getList() | ||
| 239 | -} | ||
| 240 | -// | ||
| 241 | -const handleCurrentChange = (val) => { | ||
| 242 | - pagination.pageNo = val | ||
| 243 | - getList() | ||
| 244 | -} | 177 | +// ==================== 初始化 ==================== |
| 245 | onMounted(() => { | 178 | onMounted(() => { |
| 246 | getList() | 179 | getList() |
| 247 | }) | 180 | }) |
| 248 | </script> | 181 | </script> |
| 249 | 182 | ||
| 250 | <style scoped> | 183 | <style scoped> |
| 251 | -.search-bar { | ||
| 252 | - margin-bottom: 20px; | ||
| 253 | - box-shadow: 0 1px 2px rgb(0 0 0 / 5%); | ||
| 254 | -} | ||
| 255 | - | ||
| 256 | -.search-item { | ||
| 257 | - display: flex; | ||
| 258 | - align-items: center; | ||
| 259 | - gap: 10px; | ||
| 260 | -} | ||
| 261 | - | ||
| 262 | -.search-label { | ||
| 263 | - /* width: 160px; */ | ||
| 264 | - | ||
| 265 | - /* font-weight: 500; */ | ||
| 266 | -} | ||
| 267 | - | ||
| 268 | -.search-input { | ||
| 269 | - width: 260px; | ||
| 270 | -} | ||
| 271 | - | ||
| 272 | -.search-buttons { | ||
| 273 | - display: flex; | ||
| 274 | - gap: 10px; | ||
| 275 | - margin-left: 10px; | ||
| 276 | -} | ||
| 277 | - | ||
| 278 | -.table-card { | ||
| 279 | - margin-top: 5px; | ||
| 280 | - background: #fff; | ||
| 281 | - border-radius: 4px; | ||
| 282 | - box-shadow: 0 1px 2px rgb(0 0 0 / 5%); | 184 | +.page-container { |
| 185 | + padding: 20px; | ||
| 186 | + min-height: 100vh; | ||
| 187 | + background-color: #f5f7fa; | ||
| 188 | + box-sizing: border-box; | ||
| 283 | } | 189 | } |
| 284 | 190 | ||
| 285 | -/* 分页居右 */ | ||
| 286 | -.pagination-wrapper { | ||
| 287 | - display: flex; | ||
| 288 | - padding: 12px 16px; | ||
| 289 | - justify-content: flex-end; | 191 | +.search-form :deep(.el-form-item) { |
| 192 | + margin-bottom: 0; | ||
| 290 | } | 193 | } |
| 291 | </style> | 194 | </style> |
-
Please register or login to post a comment