Authored by Bad

feat: 重构训练记录与超级组页面并更新API参数

... ... @@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=true
# 请求路径
VITE_BASE_URL='https://fitness.hcxtec.com'
VITE_BASE_URL='https://xunji.admin.geaktec.com'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
... ...
... ... @@ -62,7 +62,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
... ...
import request from '@/config/axios'
/** 前台用户信息 */
export interface Student {
id: number // ID
phone?: string // 手机号
identity?: string // 身份:1=学员、2=教练
nickname?: string // 昵称
avatar?: string // 头像
sex?: string // 性别:1=男、2=女
province?: string // 省
city?: string // 市
region?: string // 区
signature?: string // 个性签名(自我介绍)
backgroundImage?: string // 背景图片
openId?: string // 微信openId
isNewbie?: number // 是否是新人:1是0否
// 获得前台用户分页
export const getStudentPage = (params) => {
return request.get({
url: '/user/student/page',
params
})
}
// 获得用户详细
export const getStudent = (id) => {
return request.get({
url: '/user/student/get',
params: { id }
})
}
// 前台用户 API
export const StudentApi = {
// 查询前台用户分页
getUserPage: async (params: any) => {
return await request.get({ url: `/user/student/page`, params })
},
// 查询前台用户详情
getUser: async (id: number) => {
return await request.get({ url: `/user/student/get?id=` + id })
},
// 修改前台用户
updateUser: async (data: Student) => {
return await request.put({ url: `/user/student/update`, data })
},
// 导出前台用户 Excel
exportUser: async (params) => {
return await request.download({ url: `/user/student/export`, params })
},
// 获得学员健康资料
getUserHealth: async (id: number) => {
return await request.get({ url: `/user/student/health-data?userId=` + id })
}
// 更新用户
export const updateStudent = (data) => {
return request.put({
url: '/user/student/update',
data
})
}
// 获得学员健康资料
export const getStudentHealthData = (userId) => {
return request.get({
url: '/user/student/health-data',
params: { userId }
})
}
\ No newline at end of file
... ...
... ... @@ -48,7 +48,7 @@ export const SupersetsApi = {
},
// 获得超级组列表
getSupersetList: () => {
return request.get({ url: '/motion/supersets/supersets-simple-list' })
getSupersetList: (isPersonal: boolean) => {
return request.get({ url: '/motion/supersets/supersets-simple-list?isPersonal=' + isPersonal})
}
}
... ...
... ... @@ -87,7 +87,7 @@ export const ExercisesApi = {
})
},
// 获取动作分类下拉列表
getExercisesList: () => {
return request.get({ url: '/motion/supersets/simple-list' })
getExercisesList: (isPersonal: boolean) => {
return request.get({ url: '/motion/supersets/simple-list?isPersonal=' + isPersonal })
}
}
... ...
... ... @@ -52,7 +52,7 @@ export const TrainingTemplatesApi = {
})
},
// 获取模板下拉列表
getTemplateList: () => {
return request.get({ url: '/templates/simple-list' })
getTemplateList: (isPersonal: boolean) => {
return request.get({ url: '/templates/simple-list?isPersonal=' + isPersonal})
}
}
... ...
... ... @@ -50,5 +50,11 @@ export const trainingPlanApi = {
// 获得计划中的计划种类分页
getPlanCategoryPage: async (params: any) => {
return await request.get({ url: `/plan/Category/page`, params })
},
// 获取计划下拉列表
getPlanList: async () => {
return await request.get({ url: `/plan/simple-list`, })
}
}
... ...
<template>
<Dialog title="修改前台用户" v-model="dialogVisible" width="600px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px" v-loading="formLoading">
<el-form-item label="真实姓名" prop="realname">
<el-input v-model="formData.realname" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-radio-group v-model="formData.sex">
<el-radio :label="1">男</el-radio>
<el-radio :label="2">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="账号状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
<UploadImg v-model="formData.avatar" />
</el-form-item>
<el-form-item label="个性签名" prop="signature">
<el-input v-model="formData.signature" type="textarea" :rows="2" placeholder="请输入个性签名" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData.backgroundImage" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm">确 定</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import * as StudentApi from '@/api/store/student'
const message = useMessage() // 芋道全局提示组件
const dialogVisible = ref(false)
const formLoading = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const formData = reactive({
id: undefined,
realname: '',
phone: '',
nickname: '',
status: 1,
sex: 1,
avatar: '',
signature: '',
backgroundImage: ''
})
const formRules = reactive({
realname: [{ required: true, message: '真实姓名不能为空', trigger: 'blur' }],
nickname: [{ required: true, message: '昵称不能为空', trigger: 'blur' }],
phone: [{ required: true, message: '手机号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '账号状态不能为空', trigger: 'change' }],
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }]
})
/** 打开弹窗 */
const open = async (id) => {
dialogVisible.value = true
resetForm()
if (id) {
formLoading.value = true
try {
const data = await StudentApi.getStudent(id)
Object.assign(formData, data)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
const emit = defineEmits(['success'])
/** 提交表单 */
const submitForm = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
submitLoading.value = true
try {
await StudentApi.updateStudent(formData)
message.success('修改成功')
dialogVisible.value = false
emit('success')
} finally {
submitLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.id = undefined
formData.realname = ''
formData.phone = ''
formData.nickname = ''
formData.status = 1
formData.sex = 1
formData.avatar = ''
formData.signature = ''
formData.backgroundImage = ''
formRef.value?.resetFields()
}
</script>
\ No newline at end of file
... ...
<template>
<el-drawer title="学员健康资料" v-model="drawerVisible" size="600px">
<div v-loading="loading" class="health-data-container">
<el-descriptions :column="2" border v-if="healthData">
<el-descriptions-item label="学员姓名" :span="2">
{{ healthData.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="性别">
{{ healthData.genderDesc || (healthData.gender === 1 ? '男' : '女') }}
</el-descriptions-item>
<el-descriptions-item label="生日">
{{ healthData.birthday || '-' }}
</el-descriptions-item>
<el-descriptions-item label="身高">
{{ healthData.height ? `${healthData.height} cm` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前体重">
{{ healthData.weight ? `${healthData.weight} kg` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="目标体重" :span="2">
{{ healthData.targetWeight ? `${healthData.targetWeight} kg` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="健身基础">
{{ healthData.hasFitnessFoundationDesc || (healthData.hasFitnessFoundation === 1 ? '有' : '无') }}
</el-descriptions-item>
<el-descriptions-item label="训练频次">
{{ healthData.acceptableTrainingFrequency ? `${healthData.acceptableTrainingFrequency} 次/周` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="训练目标" :span="2">
{{ healthData.trainingGoalDesc || '-' }}
</el-descriptions-item>
<el-descriptions-item label="目标锻炼部位" :span="2">
<template v-if="healthData.targetMuscleParts && healthData.targetMuscleParts.length">
<el-tag
v-for="(part, index) in healthData.targetMuscleParts"
:key="index"
class="tag-item"
>
{{ part }}
</el-tag>
</template>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="身体疼痛部位" :span="2">
<template v-if="healthData.painAreas && healthData.painAreas.length">
<el-tag
v-for="(area, index) in healthData.painAreas"
:key="index"
type="danger"
class="tag-item"
>
{{ area === '0' ? '无' : area }}
</el-tag>
</template>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="是否存在疾病">
<el-tag :type="healthData.hasDisease ? 'danger' : 'success'">
{{ healthData.hasDisease ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="是否在服药">
<el-tag :type="healthData.isTakingMedication ? 'warning' : 'success'">
{{ healthData.isTakingMedication ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="日常健身场景" :span="2">
{{ healthData.fitnessSceneDesc || '-' }}
</el-descriptions-item>
<el-descriptions-item label="训练奖励方式" :span="2">
{{ healthData.rewardMethodDesc || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-empty v-else-if="!loading" description="暂无健康资料" />
</div>
</el-drawer>
</template>
<script setup>
import { ref } from 'vue'
import * as StudentApi from '@/api/store/student'
const drawerVisible = ref(false)
const loading = ref(false)
const healthData = ref(null)
/** 打开抽屉并加载数据 */
const open = async (userId) => {
drawerVisible.value = true
loading.value = true
healthData.value = null
try {
const res = await StudentApi.getStudentHealthData(userId)
healthData.value = res
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>
<style scoped>
.health-data-container {
padding: 8px;
}
.tag-item {
margin-right: 6px;
margin-bottom: 4px;
}
</style>
\ No newline at end of file
... ...
<template>
<!-- 搜索栏 -->
<ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="昵称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入昵称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input
v-model="queryParams.phone"
placeholder="请输入手机号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" /> 重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表栏 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" width="80" />
<el-table-column label="头像" align="center" prop="avatar" width="80">
<template #default="scope">
<el-avatar :src="scope.row.avatar" size="small" />
</template>
</el-table-column>
<el-table-column label="昵称" align="center" prop="nickname" />
<el-table-column label="真实姓名" align="center" prop="realname" />
<el-table-column label="手机号" align="center" prop="phone" width="120" />
<el-table-column label="性别" align="center" prop="sexStr" width="100">
<template #default="scope">
<el-tag :type="scope.row.sex === 1 ? 'primary' : 'danger'">
{{ scope.row.sexStr || (scope.row.sex === 1 ? '男' : '女') }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.statusStr || (scope.row.status === 1 ? '正常' : '禁用') }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm(scope.row.id)"
>
编辑
</el-button>
<el-button
link
type="primary"
@click="openHealthData(scope.row.id)"
>
健康资料
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:修改用户 -->
<StudentForm ref="formRef" @success="getList" />
<!-- 抽屉:学员健康资料 -->
<StudentHealthData ref="healthDataRef" />
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import * as StudentApi from '@/api/store/student'
import StudentForm from './StudentForm.vue'
import StudentHealthData from './StudentHealthData.vue'
const loading = ref(true)
const total = ref(0)
const list = ref([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
phone: undefined,
nickname: undefined
})
const queryFormRef = ref()
const formRef = ref()
const healthDataRef = ref()
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await StudentApi.getStudentPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
/** 打开修改弹窗 */
const openForm = (id) => {
formRef.value.open(id)
}
/** 打开健康资料抽屉 */
const openHealthData = (userId) => {
healthDataRef.value.open(userId)
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>
\ No newline at end of file
... ...
... ... @@ -10,7 +10,7 @@
<el-select v-model="formData.exerciseType" placeholder="请选择动作类型" class="w-full">
<el-option label="无氧运动" :value="0" />
<el-option label="有氧运动" :value="1" />
<el-option label="次训练" :value="2" />
<el-option label="次训练" :value="2" />
<el-option label="计时训练" :value="3" />
<el-option label="自重加重" :value="4" />
<el-option label="自重减重" :value="5" />
... ...
<template>
<el-dialog v-model="dialogVisible" title="训练记录详情" width="600px">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="700px">
<div v-loading="loading">
<!-- 基础信息(一列) -->
<!-- 基础信息 -->
<el-descriptions v-if="detailData" border :column="1" label-width="120px">
<el-descriptions-item label="学员">
{{ detailData.username || '-' }}
... ... @@ -13,15 +13,10 @@
<!-- 训练单元列表 -->
<div v-if="detailData.units && detailData.units.length > 0" class="unit-section">
<h3 style="margin: 20px 0 10px; color: #333;">训练单元列表</h3>
<h3 class="section-title">训练单元列表</h3>
<!-- 循环每个训练单元 -->
<div v-for="(unit, unitIndex) in detailData.units" :key="unit.id || unitIndex" class="unit-card">
<!-- 单元基础信息(改成一列) -->
<el-descriptions border :column="1" label-width="120px" size="small">
<!-- <el-descriptions-item label="单元ID">
{{ unit.id || '-' }}
</el-descriptions-item> -->
<el-descriptions-item label="单元名称">
{{ unit.name || '-' }}
</el-descriptions-item>
... ... @@ -33,27 +28,26 @@
</el-descriptions-item>
</el-descriptions>
<!-- 动作列表(改成一列描述列表) -->
<!-- 动作列表 -->
<div v-if="unit.exercises && unit.exercises.length > 0" class="exercise-section">
<h4 style="margin: 15px 0 8px; color: #409EFF;">动作列表</h4>
<h4 class="sub-title">动作列表</h4>
<!-- 循环每个动作 -->
<div v-for="(exercise, exIndex) in unit.exercises" :key="exercise.exerciseId || exIndex"
class="exercise-card">
<div v-for="(exercise, exIndex) in unit.exercises" :key="exercise.exerciseId || exIndex" class="exercise-card">
<el-descriptions border :column="1" label-width="120px" size="small">
<!-- <el-descriptions-item label="动作ID">
{{ exercise.exerciseId || '-' }}
</el-descriptions-item> -->
<el-descriptions-item label="排序">
{{ exercise.innerOrder || '-' }}
</el-descriptions-item>
<el-descriptions-item label="动作名称">
{{ exercise.name || '-' }}
</el-descriptions-item>
<!-- 训练组详情(一列) -->
<el-descriptions-item label="训练组详情">
<el-table v-if="exercise.sets && exercise.sets.length > 0" :data="exercise.sets" border size="small"
stripe style="width: 100%;">
<el-table
v-if="exercise.sets && exercise.sets.length > 0"
:data="exercise.sets"
border
size="small"
stripe
>
<el-table-column label="组号" width="80">
<template #default="scope">
{{ scope.$index + 1 }}
... ... @@ -68,9 +62,8 @@
</el-table-column>
<el-table-column prop="restTime" label="休息(秒)" width="100" />
</el-table>
<div v-else>无训练组数据</div>
<div v-else class="empty-text">无训练组数据</div>
</el-descriptions-item>
<!-- -->
</el-descriptions>
</div>
</div>
... ... @@ -79,69 +72,99 @@
</div>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button @click="dialogVisible = false">关 闭</el-button>
</template>
</el-dialog>
</Dialog>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord'
const loading = ref(false)
const detailData = ref({})
// ===================== Props & Emits =====================
const props = defineProps({
visible: { type: Boolean, default: false },
id: { type: [Number, String], default: null }
})
const emit = defineEmits(['update:visible'])
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const dialogTitle = ref('训练记录详情')
watch(() => props.id, (newId) => {
if (newId) getStuTrainingDetail(newId)
})
// 转换单元类型
// ===================== 状态 =====================
const loading = ref(false)
const detailData = ref({})
// ===================== 工具函数 =====================
const formatUnitType = (type) => {
if (type === 1) return '动作组'
if (type === 2) return '超级组'
return '未知类型'
}
// ===================== 数据加载 =====================
const getStuTrainingDetail = async (id) => {
loading.value = true
try {
const res = await stuTrainingRecordsApi.getStuTrainingRecordDetail(id)
detailData.value = res || {}
} catch (err) {
console.error('请求失败', err)
} finally {
loading.value = false
}
}
// ===================== 监听弹窗打开 / id 变化加载详情 =====================
watch(
[() => props.visible, () => props.id],
([visible, newId]) => {
if (visible && newId) getStuTrainingDetail(newId)
},
{ immediate: true }
)
</script>
<style scoped>
/* 单元卡片 */
.unit-section {
margin-top: 20px;
}
.section-title {
margin: 0 0 12px;
padding-left: 10px;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
border-left: 4px solid var(--el-color-primary);
}
.sub-title {
margin: 16px 0 10px;
font-size: 14px;
font-weight: 600;
color: var(--el-color-primary);
}
.unit-card {
padding: 15px;
margin-bottom: 20px;
background: #fafafa;
border: 1px solid #ebeef5;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
/* 动作卡片 */
.exercise-card {
padding: 10px;
margin-top: 15px;
background: #fff;
border: 1px solid #e4e7ed;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
}
.empty-text {
padding: 10px;
color: var(--el-text-color-secondary);
}
</style>
... ...
<template>
<div class="page-container">
<ContentWrap class="search-bar">
<div class="search-item">
<label class="search-label">学员</label>
<el-input v-model="searchForm.name" placeholder="请输入" class="search-input" @keyup.enter="handleSearch"
clearable />
<div class="search-buttons">
<!-- 搜索栏 -->
<ContentWrap class="mb-20px">
<el-form :inline="true" :model="queryParams">
<el-form-item label="学员" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入" clearable class="!w-220px"
@keyup.enter="handleSearch" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon>
<Search />
</el-icon>
搜索
<Icon icon="ep:search" class="mr-5px" />搜索
</el-button>
<el-button @click="handleReset">
<el-icon>
<Refresh />
</el-icon>
重置
<Icon icon="ep:refresh" class="mr-5px" />重置
</el-button>
<!-- <el-button type="primary" @click="openAddDialog">
<el-icon>
<Plus />
</el-icon>
新增
</el-button> -->
</div>
</div>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表区域 -->
<!-- <div class="table-container"> -->
<!-- 表格区域 -->
<ContentWrap>
<el-table :data="tableData" v-loading="loading" empty-text="暂无数据" border>
<el-table v-loading="loading" :data="tableData" empty-text="暂无数据" stripe>
<el-table-column prop="username" label="学员" align="center" />
<el-table-column prop="name" label="训练计划名称" align="center" />
<el-table-column label="组数" align="center" :formatter="formatSetCount" />
<el-table-column label="训练重量" align="center" :formatter="formatWeight" />
<el-table-column prop="duration" label="训练时长(分钟)" align="center" />
<!-- <el-table-column prop="date" label="训练日期" align="center" :formatter="dateFormatter" /> -->
<el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" align="center" :formatter="dateFormatter" />
<el-table-column prop="startTime" label="开始时间" align="center" :formatter="dateFormatter" />
<el-table-column prop="endTime" label="结束时间" align="center" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="text" size="small" style=" margin-right: 8px;color: #409EFF;"
@click="openDetailDialog(scope.row)">详情</el-button>
<el-button type="text" size="small" style="color: #F56C6C;"
@click="handleDelete(scope.row.id)">删除</el-button>
<el-button link type="primary" @click="openDetailDialog(scope.row)">详情</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-wrapper" style="padding: 12px 16px;">
<el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize"
:total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<Pagination :total="queryParams.total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- </div> -->
<!-- 详情弹窗 -->
<StuTrainingDetailDialog v-model:visible="detailVisible" :id="currentId" />
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// 导入图标
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord.ts'
import { ElMessage } from 'element-plus'
import { dateFormatter } from '@/utils/formatTime'
import lostImg from '@/assets/imgs/lost.png'
import { ref, reactive, onMounted } from 'vue'
import { stuTrainingRecordsApi } from '@/api/store/training/stuTrainingRecord'
import StuTrainingDetailDialog from './StuTrainingDetailDialog.vue'
// 搜索表单
const searchForm = reactive({
import { dateFormatter } from '@/utils/formatTime'
// ===================== 查询参数 =====================
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
total: 0,
name: ''
})
// 加载状态
const loading = ref(false)
// 表格数据(3组模拟数据)
const tableData = ref([])
// 分页参数
const pagination = reactive({
pageNo: 1,
pageSize: 10,
total: 0
})
// 详情弹窗
// ===================== 详情弹窗 =====================
const detailVisible = ref(false)
const currentId = ref(null) // 存储当前点击的id
// ========================列表格式化==========
// 格式化组数(在数字后加“组”)
const currentId = ref(null)
// ===================== 表格格式化 =====================
const formatSetCount = (row) => {
return row.setCount ? `${row.setCount}组` : '0组'
return row.setCount ? row.setCount + '组' : '0组'
}
// 格式化重量
const formatWeight = (row) => {
return row.weight ? `${row.weight}kg` : '0kg'
return row.weight ? row.weight + 'kg' : '0kg'
}
// 获取学员训练记录列表
// ===================== 数据加载 =====================
const getList = async () => {
loading.value = true
try {
// 搜索条件
const params = {
pageNo: pagination.pageNo + '',
pageSize: pagination.pageSize + '',
name: searchForm.name,
}
const res = await stuTrainingRecordsApi.getstuTrainingRecordPage(params)
const res = await stuTrainingRecordsApi.getstuTrainingRecordPage({
pageNo: queryParams.pageNo + '',
pageSize: queryParams.pageSize + '',
name: queryParams.name
})
tableData.value = res.list || []
pagination.total = res.total || 0
console.log('学员训练记录分页接口返回数据:', res)
queryParams.total = res.total || 0
} catch (err) {
console.error('❌ 请求失败:', err)
ElMessage.error('请求失败,请检查网络或接口')
} finally {
loading.value = false
}
}
// 搜索
// ===================== 搜索与重置 =====================
const handleSearch = () => {
pagination.pageNo = 1 // 搜索时重置到第一页
queryParams.pageNo = 1
getList()
}
// 重置
const handleReset = () => {
pagination.pageNo = 1
pagination.pageSize = 10
searchForm.name = ''
Object.assign(queryParams, { pageNo: 1, pageSize: 10, total: 0, name: '' })
getList()
}
// 详情
// ===================== 详情 =====================
const openDetailDialog = (row) => {
currentId.value = row.id
detailVisible.value = true
}
// 处理删除
// ===================== 删除 =====================
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定要删除吗?', '提示', {
... ... @@ -150,82 +116,27 @@ const handleDelete = async (id) => {
cancelButtonText: '取消',
type: 'warning'
})
// 能走到这里 = 用户点了确定
await stuTrainingRecordsApi.deleteStuTrainingRecordPage(id)
ElMessage.success('删除成功')
getList()
} catch (err) {
// 先打印,方便调试
console.log('实际错误内容:', err, typeof err)
// 核心判断:err 是字符串 'cancel'
if (err === 'cancel') {
ElMessage.info('已取消删除')
return // 直接退出,不执行后面的错误处理
if (err !== 'cancel') {
ElMessage.error('删除失败')
}
// 只有非 cancel 的错误才会走到这里
console.error('❌ 请求失败:', err)
ElMessage.error('请求失败,请检查网络或接口')
} finally {
getList() // 刷新列表
}
}
// 处理分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
getList()
}
const handleCurrentChange = (val) => {
pagination.pageNo = val
getList()
}
// ===================== 初始化 =====================
onMounted(() => {
getList()
})
</script>
<style scoped>
/* .page-container {
} */
/* 分页居右 */
.pagination-wrapper {
display: flex;
padding: 12px 16px;
justify-content: flex-end;
}
.search-bar {
/* margin-bottom: 20px; */
margin-bottom: 20px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
.search-item {
display: flex;
align-items: center;
gap: 10px;
padding: 15px 20px;
}
.search-label {
/* width: 160px; */
font-weight: 500;
}
.search-input {
width: 260px;
}
.search-buttons {
display: flex;
gap: 10px;
margin-left: 10px;
}
.table-container {
width: 100%;
<style scoped>
.page-container {
padding: 20px;
min-height: 100vh;
background-color: var(--el-bg-color-page);
box-sizing: border-box;
}
</style>
... ...
<template>
<div class="supersets-page">
<ContentWrap class="plan-search-bar">
<!-- 1. 搜索栏区域 -->
<div class="search-bar">
<div class="search-item">
<label class="search-label">超级组名称</label>
<el-input v-model="searchForm.name" placeholder="请输入" class="search-input" @keyup.enter="handleSearch"
clearable />
<!-- 按钮组 -->
<div class="search-buttons">
<!-- 搜索栏 -->
<ContentWrap class="mb-20px">
<el-form :inline="true" :model="queryParams" class="flex items-center">
<el-form-item label="超级组名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入"
clearable
class="!w-220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon>
<Search />
</el-icon>
搜索
<Icon icon="ep:search" class="mr-5px" />搜索
</el-button>
<el-button @click="handleReset">
<el-icon>
<Refresh />
</el-icon>
重置
<Icon icon="ep:refresh" class="mr-5px" />重置
</el-button>
<el-button type="primary" @click="openAddDialog">
<el-icon>
<Plus />
</el-icon>
新增
<Icon icon="ep:plus" class="mr-5px" />新增
</el-button>
</div>
</div>
</div>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 表格区域 -->
<ContentWrap>
<!-- 2. 表格展示区域 -->
<div class="table-container">
<el-table :data="tableData" style="width: 100%" empty-text="暂无数据" stripe>
<el-table v-loading="loading" :data="tableData" empty-text="暂无数据" stripe>
<el-table-column prop="name" label="超级组名称" align="center" min-width="200" />
<!-- <el-table-column label="封面" align="center" width="200">
<template #default>
<img :src="coverImgUrl" class="cover-img" alt="封面" />
</template>
</el-table-column> -->
<el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" width="200">
<template #default="scope">
<el-button type="text" class="edit-btn" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" class="delete-btn" @click="handleDelete(scope.row)">删除</el-button>
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 3. 分页区域 -->
<div class="pagination-container">
<span class="total">共{{ total }}条</span>
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option label="10条/页" :value="10" />
<el-option label="20条/页" :value="20" />
<el-option label="50条/页" :value="50" />
</el-select>
<el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :total="total"
layout="prev, pager, next, jumper" @size-change="handlePageSizeChange" @current-change="handlePageChange" />
</div>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 4. 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px" :close-on-click-modal="false"
@close="resetForm" class="supersets-dialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="add-form">
<!-- 新增/编辑弹窗 -->
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-form-item label="超级组名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="动作" prop="exerciseIds">
<el-select v-model="formData.exerciseIds" placeholder="请选择动作" multiple clearable style="width: 100%"
filterable>
<el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" />
<el-select
v-model="formData.exerciseIds"
placeholder="请选择动作"
multiple
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in exercisesList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 实现动作的主要,次要训练部位的回显,只能显示给用户看 -->
<!-- 主要训练部位(自动回显,只读) -->
<el-form-item label="主要训练部位">
<el-input :value="primaryMusclesText" disabled placeholder="选择动作后自动显示" style="width: 100%" />
<el-input :value="primaryMusclesText" disabled placeholder="选择动作后自动显示" />
</el-form-item>
<!-- 次要训练部位(自动回显,只读) -->
<el-form-item label="次要训练部位">
<el-input :value="secondaryMusclesText" disabled placeholder="选择动作后自动显示" style="width: 100%" />
<el-input :value="secondaryMusclesText" disabled placeholder="选择动作后自动显示" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="isEdit ? submitEditForm() : submitAddForm()">
{{ isEdit ? '修改' : '保存' }}
</el-button>
</div>
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</el-dialog>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, computed } from 'vue'
// import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, ElForm } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
<script setup>
import { ref, onMounted, reactive, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { SupersetsApi } from '@/api/store/training/Supersets'
import { ExercisesApi } from '@/api/store/training/pose'
// import { MotionCategoryApi } from '@/api/store/training/partCategory'
import { MusclesApi } from '@/api/store/training/muscle'
import { dateFormatter } from '@/utils/formatTime'
import coverImgUrl from '@/assets/imgs/lost.png'
// const router = useRouter()
const searchForm = ref({
// ===================== 查询参数 =====================
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: ''
})
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const tableData = ref<any[]>([])
const loading = ref(false)
const tableData = ref([])
// ===================== 弹窗状态 =====================
const dialogVisible = ref(false)
const isEdit = ref(false)
const dialogTitle = ref('超级组')
const formRef = ref<InstanceType<typeof ElForm>>()
const dialogTitle = ref('')
const formRef = ref(null)
const exercisesList = ref<any[]>([])
// const categoryList = ref<any[]>([])
const muscleList = ref<any[]>([])
// ===================== 选项数据 =====================
const exercisesList = ref([])
const detailExercises = ref([]) // 编辑时存储详情接口返回的动作数据(含肌肉名称)
// ===================== 表单数据 =====================
const formData = reactive({
id: 0,
name: '',
primaryMuscles: [] as number[],
secondaryMuscles: [] as number[],
exerciseIds: [] as number[],
category: ''
})
// ===================== 自动汇总显示动作的部位(只读) =====================
// 解析动作里的部位字符串
const parseMuscles = (str: string) => {
try { return JSON.parse(str) } catch { return [] }
}
// 主要训练部位汇总文本
// 根据ID获取肌肉名称
const getMuscleName = (id) => {
const muscle = muscleList.value.find(item => item.id == id)
return muscle ? muscle.name : id
}
// 肌肉回显
const primaryMusclesText = computed(() => {
if (!formData.exerciseIds.length) return ''
const idSet = new Set()
formData.exerciseIds.forEach(exId => {
const item = exercisesList.value.find(e => e.id === exId)
if (!item) return
const arr = parseMuscles(item.primaryMuscles || '[]')
arr.forEach(id => idSet.add(id))
})
return Array.from(idSet).map(id => (id)).join('、')
})
const secondaryMusclesText = computed(() => {
if (!formData.exerciseIds.length) return ''
const idSet = new Set()
console.log('formData.exerciseIds=', formData.exerciseIds);
formData.exerciseIds.forEach(exId => {
const item = exercisesList.value.find(e => e.id === exId)
console.log('item=', item);
if (!item) return
const arr = parseMuscles(item.secondaryMuscles || '[]')
console.log('arr=', arr);
arr.forEach(id => idSet.add(id))
})
return Array.from(idSet).map(id => getMuscleName(id)).join('、')
exerciseIds: []
})
const formRules = reactive({
... ... @@ -189,133 +128,113 @@ const formRules = reactive({
],
exerciseIds: [
{ required: true, message: '请选择组内动作', trigger: 'change' }
],
// primaryMuscles: [
// { required: true, message: '请选择主要训练部位', trigger: 'change' }
// ]
]
})
//加载数据
const loadExercisesList = async () => {
try {
const res = await ExercisesApi.getExercisesList()
exercisesList.value = res || []
} catch (err) {
console.error('加载动作列表失败:', err)
ElMessage.error('加载动作列表失败')
}
// ===================== 工具函数 =====================
const parseMuscles = (value) => {
if (Array.isArray(value)) return value
try { return JSON.parse(value) } catch { return [] }
}
// ===================== 计算属性:训练部位回显 =====================
// 查找动作对象:优先返回带肌肉名称(primaryMusclesName/secondaryMusclesName)的数据,
// 编辑时 detailExercises 可能不含名称字段,则回退到 exercisesList(含名称)
const findExercise = (exId) => {
const inDetail = detailExercises.value.find((e) => e.id === exId)
const inList = exercisesList.value.find((e) => e.id === exId)
if (inDetail && Array.isArray(inDetail.primaryMusclesName)) return inDetail
if (inList) return inList
return inDetail || inList
}
// field 传入名称数组字段(primaryMusclesName / secondaryMusclesName)
const aggregateMuscles = (nameField) => {
if (!formData.exerciseIds.length) return ''
const nameSet = new Set()
formData.exerciseIds.forEach((exId) => {
const item = findExercise(exId)
if (!item) return
const list = Array.isArray(item[nameField]) ? item[nameField] : parseMuscles(item[nameField] || [])
list.forEach((name) => nameSet.add(name))
})
return Array.from(nameSet).join('、')
}
// const loadCategoryList = async () => {
// try {
// const res = await MotionCategoryApi.getMotionCategoryPage({ pageNo: 1 + '', pageSize: 100 + '', name: '' })
// const data = res.data || res
// categoryList.value = data.list || []
// } catch (err) {
// console.error('加载部位分类失败:', err)
// ElMessage.error('加载部位分类失败')
// }
// }
const primaryMusclesText = computed(() => aggregateMuscles('primaryMusclesName'))
const secondaryMusclesText = computed(() => aggregateMuscles('secondaryMusclesName'))
const loadMuscleList = async () => {
// ===================== 数据加载 =====================
const loadExercisesList = async () => {
try {
const res = await MusclesApi.getsubCategoriesList()
muscleList.value = res || []
const res = await ExercisesApi.getExercisesList(false)
exercisesList.value = res || []
} catch (err) {
console.error('加载肌肉列表失败:', err)
ElMessage.error('加载肌肉列表失败')
ElMessage.error('加载动作列表失败')
}
}
const getList = async () => {
loading.value = true
try {
const res = await SupersetsApi.getSupersetsPage({
pageNo: pageNo.value,
pageSize: pageSize.value,
name: searchForm.value.name
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
name: queryParams.name
})
const responseData = res.data || res
console.log('超级组分页接口返回数据', res)
const { total: resTotal = 0, list = [] } = responseData
tableData.value = list
total.value = resTotal
console.log('tableData.value', tableData.value);
} catch (err) {
ElMessage.error('获取超级组列表失败')
console.error(err)
} finally {
loading.value = false
}
}
// ===================== 搜索与分页 =====================
const handleSearch = () => {
pageNo.value = 1
queryParams.pageNo = 1
getList()
}
const handleReset = () => {
searchForm.value = { name: '' }
pageNo.value = 1
getList()
}
const handlePageChange = (val: number) => {
pageNo.value = val
Object.assign(queryParams, { pageNo: 1, pageSize: 10, name: '' })
getList()
}
const handlePageSizeChange = (val: number) => {
pageSize.value = val
pageNo.value = 1
getList()
}
//处理新增弹窗
const openAddDialog = async () => {
// ===================== 新增 =====================
const openAddDialog = () => {
isEdit.value = false
dialogTitle.value = '新增超级组'
resetForm()
await Promise.all([
loadExercisesList(),
// loadCategoryList(),
loadMuscleList()
])
dialogVisible.value = true
}
//处理编辑弹窗
const handleEdit = async (row: any) => {
// ===================== 编辑 =====================
const handleEdit = async (row) => {
try {
isEdit.value = true
dialogTitle.value = '编辑超级组'
resetForm()
await Promise.all([
loadExercisesList(),
// loadCategoryList(),
loadMuscleList()
])
const res = await SupersetsApi.getSupersets(row.id)
//
const detail = res.data || res
console.log('detail单个超级组详情接口', detail)
console.log('res.data单个超级组详情接口', res.data)
const exerciseIds = (detail.exercises || []).map((item: any) => Number(item.id))
console.log('提取的动作ID数组', exerciseIds) // 验证是否正确提取
//回显
console.log('exerciseIds传递的参数', exerciseIds)
detailExercises.value = detail.exercises || []
const exerciseIds = (detail.exercises || []).map((item) => Number(item.id))
Object.assign(formData, {
id: detail.id,
name: detail.name || '',
exerciseIds: exerciseIds || [],
exerciseIds
})
dialogVisible.value = true
} catch (err) {
console.error('获取超级组详情失败:', err)
ElMessage.error('加载编辑数据失败')
}
}
const handleDelete = async (row: any) => {
// ===================== 删除 =====================
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(`确定要删除超级组「${row.name}」吗?`, '提示', {
confirmButtonText: '确定',
... ... @@ -325,221 +244,69 @@ const handleDelete = async (row: any) => {
await SupersetsApi.deleteSupersets(row.id)
ElMessage.success('删除成功')
getList()
} catch (err: any) {
} catch (err) {
if (err !== 'cancel') {
ElMessage.error(`删除失败:${err.message || '服务器错误'}`)
console.error(err)
ElMessage.error('删除失败')
}
}
}
// ===================== 表单操作 =====================
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields()
}
Object.assign(formData, {
id: 0,
name: '',
// primaryMuscles: [],
// secondaryMuscles: [],
exerciseIds: [],
// category: ''
})
detailExercises.value = []
Object.assign(formData, { id: 0, name: '', exerciseIds: [] })
}
const submitAddForm = async () => {
const submitForm = async () => {
if (!formRef.value) return
try {
await formRef.value.clearValidate()
const valid = await formRef.value.validate()
if (!valid) return
await SupersetsApi.createSupersets({
id: 0,
name: formData.name,
exerciseIds: formData.exerciseIds,
})
ElMessage.success('新增超级组成功!')
dialogVisible.value = false
getList()
} catch (err: any) {
console.error('新增超级组失败:', err)
ElMessage.error(`新增失败:${err.response?.data?.msg || err.message || '服务器错误'}`)
await formRef.value.validate()
} catch {
return
}
}
const submitEditForm = async () => {
if (!formRef.value) return
try {
await formRef.value.clearValidate()
const valid = await formRef.value.validate()
if (!valid) return
await SupersetsApi.updateSupersets({
const payload = {
id: formData.id,
name: formData.name,
exerciseIds: formData.exerciseIds,
})
exerciseIds: formData.exerciseIds
}
if (isEdit.value) {
await SupersetsApi.updateSupersets(payload)
ElMessage.success('修改超级组成功!')
} else {
await SupersetsApi.createSupersets(payload)
ElMessage.success('新增超级组成功!')
}
dialogVisible.value = false
getList()
} catch (err: any) {
console.error('修改超级组失败:', err)
ElMessage.error(`修改失败:${err.response?.data?.msg || err.message || '服务器错误'}`)
} catch (err) {
const msg = err?.response?.data?.msg || err?.message
if (msg && typeof msg !== 'object') {
ElMessage.error(msg)
}
}
}
// ===================== 弹窗关闭时重置表单 =====================
watch(dialogVisible, (val) => {
if (!val) resetForm()
})
// ===================== 初始化 =====================
onMounted(async () => {
await Promise.all([
loadExercisesList(),
loadMuscleList()
])
await loadExercisesList()
getList()
})
</script>
<style scoped>
.supersets-page {
min-height: 100vh;
padding: 20px;
overflow-x: hidden;
min-height: 100vh;
background-color: #f5f7fa;
box-sizing: border-box;
}
.plan-search-bar {
margin-bottom: 20px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
/* .search-bar {
display: flex;
align-items: center;
padding: 16px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
gap: 10px;
flex-wrap: nowrap;
} */
.search-item {
display: flex;
align-items: center;
gap: 10px;
padding: 15px 20px;
}
.search-label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.search-input {
width: 220px;
flex: 0 0 auto;
}
.search-buttons {
display: flex;
gap: 10px;
flex-shrink: 0;
margin: 0;
}
.fixed-btn {
display: flex;
width: 120px;
padding: 8px 0 !important;
justify-content: center;
align-items: center;
}
.cover-img {
display: block;
width: 100px;
height: 60px;
margin: 0 auto;
background-color: #f5f5f5;
border: 1px solid #e4e7ed;
border-radius: 15px;
object-fit: cover;
}
.edit-btn {
margin-right: 15px;
color: #409eff;
}
.delete-btn {
color: #f56c6c;
}
.table-container {
padding: 0;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
}
.pagination-container {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 16px;
background-color: #fff;
border-radius: 4px;
}
.total {
font-size: 14px;
color: #606266;
}
.page-size-select {
width: 120px;
}
:deep(.el-table) {
max-height: calc(100vh - 280px);
overflow-y: auto;
}
:deep(.supersets-dialog .el-dialog__header) {
padding: 16px 20px;
border-bottom: 1px solid #e6e6e6;
}
:deep(.supersets-dialog .el-dialog__title) {
font-size: 16px;
color: #333;
}
:deep(.supersets-dialog .el-dialog__body) {
padding: 24px 20px;
}
.add-form {
max-width: 100%;
}
:deep(.add-form .el-form-item__label) {
font-weight: 500;
color: #333;
}
:deep(.add-form .el-input__wrapper),
:deep(.add-form .el-select__wrapper) {
background-color: #f5f7fa;
border-radius: 4px;
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 16px;
padding-top: 16px;
border-top: 1px solid #e6e6e6;
}
</style>
... ...
<!-- 原型图叫文件夹,接口命名为模板大类 -->
<template>
<div class="page-container">
<!-- 搜索框区域 -->
<!-- <div class="equipments-page"> -->
<ContentWrap class="plan-search-bar">
<!-- 搜索栏 -->
<div class="search-bar">
<label class="search-label">文件夹名称</label>
<el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"
@keyup.enter="handleSearch" />
<div class="search-buttons">
<el-button type="primary" @click="handleSearch" size="default">
<el-icon>
<Search />
</el-icon>
搜索
<ContentWrap class="plan-search-bar">
<el-form :inline="true" :model="queryParams">
<el-form-item label="文件夹名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入"
clearable
class="!w-220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<Icon icon="ep:search" class="mr-5px" />搜索
</el-button>
<el-button @click="resetSearch" size="default">
<el-icon>
<Refresh />
</el-icon>
重置
<el-button @click="resetSearch">
<Icon icon="ep:refresh" class="mr-5px" />重置
</el-button>
<el-button type="primary" @click="handleAdd" size="default">
<el-icon>
<Plus />
</el-icon>
新增
<el-button type="primary" @click="handleAdd">
<Icon icon="ep:plus" class="mr-5px" />新增
</el-button>
</div>
</div>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 表格区域 -->
<ContentWrap>
<div class="table-container">
<el-table :data="tableData" v-loading="loading" style="width: 100%" empty-text="暂无数据" stripe>
<el-table
:data="tableData"
v-loading="loading"
empty-text="暂无数据"
stripe
>
<el-table-column prop="name" label="文件夹名字" align="center" />
<el-table-column prop="cover" label="封面" align="center" min-width="120">
<template #default="scope">
<div class="cover-wrapper">
<!-- 优先显示行数据封面,无则显示占位图 -->
<img class="cover-img" :src="scope.row.urlCover || lostImg" alt="模板封面" />
</div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" width="200">
<template #default="scope">
<el-button type="text" class="edit-btn" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" class="delete-btn" @click="handleDelete(scope.row.id)">删除</el-button>
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-wrapper" style="padding: 12px 16px;">
<el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize"
:total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<Pagination
:total="queryParams.total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 编辑/新增弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="700px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px">
<el-form-item label="大类分类名称:" prop="name" label-suffix="*">
<el-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit style="width: 100%" />
<el-form-item label="大类分类名称:" prop="name">
<el-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="模板" prop="templateIds">
<el-select v-model="formData.templateIds" placeholder="请选择模板" multiple clearable style="width: 100%"
filterable>
<el-select
v-model="formData.templateIds"
placeholder="请选择模板"
multiple
clearable
filterable
style="width: 100%"
>
<el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="封面" prop="urlCover" label-suffix="*">
<UploadImg v-model="formData.urlCover" placeholder="封面" maxlength="50" show-word-limit style="width: 100%" />
<el-form-item label="封面" prop="urlCover">
<UploadImg v-model="formData.urlCover" placeholder="封面" />
</el-form-item>
<el-form-item label="模板大类介绍" prop="description" label-suffix="*">
<el-input v-model="formData.description" placeholder="请输入模板大类介绍" maxlength="50" show-word-limit
style="width: 100%" />
<el-form-item label="模板大类介绍" prop="description">
<el-input
v-model="formData.description"
type="textarea"
placeholder="请输入模板大类介绍"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm" :loading="loading" :disabled="loading">确 定</el-button>
<el-button type="primary" @click="submitForm" :loading="loading">确 定</el-button>
</template>
</el-dialog>
<!-- 分页组件 -->
<!-- </div> -->
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { dateFormatter } from '@/utils/formatTime'
import { templateCategorysApi } from '@/api/store/training/templateCategorys'
import { TrainingTemplatesApi } from '@/api/store/training/templates'
import lostImg from '@/assets/imgs/lost.png'
import { ElMessage, ElMessageBox } from 'element-plus'
const tableData = ref<any[]>([])
// ===================== 列表状态 =====================
const tableData = ref([])
const loading = ref(false)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
total: 0,
name: ''
})
// ===================== 弹窗状态 =====================
const isEdit = ref(true)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const templateList = ref<any[]>([])
const templateList = ref([])
const formRules = reactive({
name: [
{ required: true, message: '请输入大类分类名称', trigger: 'blur' }
]
name: [{ required: true, message: '请输入大类分类名称', trigger: 'blur' }],
templateIds: [{ required: true, message: '请选择模板', trigger: 'change' }],
urlCover: [{ required: true, message: '请上传封面', trigger: 'change' }],
description: [{ required: true, message: '请输入模板大类介绍', trigger: 'blur' }]
})
const loadtemplateList = async () => {
try {
const res = await TrainingTemplatesApi.getTemplateList()
templateList.value = res || []
} catch (err) {
console.error('加载模板列表失败:', err)
ElMessage.error('加载模板列表失败')
}
}
// 弹窗参数
const formData = reactive({
id: 0,
name: '',
urlCover: '',
description: '',
// 下拉框
templateIds: [] as number[]
templateIds: []
})
const searchForm = reactive({
name: '',
})
// 分页参数
const pagination = reactive({
pageNo: 1,
pageSize: 10,
total: 0
})
// ===================== 模板下拉列表 =====================
const loadTemplateList = async () => {
try {
const res = await TrainingTemplatesApi.getTemplateList(false)
templateList.value = res || []
} catch {
ElMessage.error('加载模板列表失败')
}
}
//处理搜索
// ===================== 搜索与重置 =====================
const handleSearch = () => {
pagination.pageNo = 1 // 搜索时重置到第一页
queryParams.pageNo = 1
getList()
}
// 重置搜索条件
const resetSearch = () => {
searchForm.name = ''
pagination.pageNo = 1
pagination.pageSize = 10
queryParams.name = ''
queryParams.pageNo = 1
queryParams.pageSize = 10
getList()
}
// 获取列表
// ===================== 列表数据 =====================
const getList = async () => {
loading.value = true
try {
// 搜索条件
const params = {
pageNo: pagination.pageNo + '',
pageSize: pagination.pageSize + '',
name: searchForm.name,
pageNo: queryParams.pageNo + '',
pageSize: queryParams.pageSize + '',
name: queryParams.name
}
//
const res = await templateCategorysApi.getTemplateCategorysPage(params)
console.log('模板大类分页接口返回数据:', res)
tableData.value = res.list || []
pagination.total = res.total || 0
console.log('分页列表', res.list)
} catch (err) {
console.error('❌ 请求失败:', err)
queryParams.total = res.total || 0
} catch {
ElMessage.error('请求失败,请检查网络或接口')
} finally {
loading.value = false
}
}
// ===================== 表单重置 =====================
const resetForm = () => {
formData.id = 0
formData.name = ''
formData.description = ''
formData.urlCover = ''
formData.description = ''
formData.templateIds = []
}
//=======================处理新增==================
// ===================== 新增 =====================
const handleAdd = () => {
isEdit.value = false
dialogVisible.value = true
dialogTitle.value = '新增模板大类'
resetForm() // 调用重置函数清空表单
resetForm()
dialogVisible.value = true
}
//=====================处理编辑=================
const handleEdit = async (row: any) => {
// ===================== 编辑 =====================
const handleEdit = async (row) => {
isEdit.value = true
dialogTitle.value = '编辑模板大类'
// 先确保模板列表已加载
if (templateList.value.length === 0) {
await loadtemplateList()
await loadTemplateList()
}
// 表单回显
formData.id = row.id
formData.name = row.name
formData.urlCover = row.urlCover || ''
formData.description = row.description || ''
formData.templateIds = row.templateIds || []
console.log('编辑回显数据:', formData)
console.log('编辑行的ids', row.templateIds)
console.log('被输入编辑行的ids', formData.templateIds)
// 新增:打开编辑弹窗时,清空之前的验证状态
if (formRef.value) {
formRef.value.clearValidate()
}
dialogVisible.value = true
}
//=====================提交新增/编辑表单=================
// ===================== 弹窗关闭时清理校验 =====================
watch(dialogVisible, (val) => {
if (!val && formRef.value) {
formRef.value.clearValidate()
}
})
// ===================== 提交 =====================
const submitForm = async () => {
if (!formRef.value) return
try {
// 表单验证
await formRef.value.validate()
} catch {
return
}
loading.value = true
if (isEdit.value) {
// 编辑逻辑
const Editparams = {
const payload = {
id: formData.id,
name: formData.name,
urlCover: formData.urlCover,
description: formData.description,
templateIds: formData.templateIds
}
await templateCategorysApi.updateTemplateCategorys(Editparams)
try {
if (isEdit.value) {
await templateCategorysApi.updateTemplateCategorys(payload)
ElMessage.success('编辑成功')
} else {
// 新增逻辑(调用新增接口)
const params = {
id: formData.id,
name: formData.name,
urlCover: formData.urlCover,
description: formData.description,
templateIds: formData.templateIds
}
console.log('提交给后端的参数:', params) // 👈 加这行
const res = await templateCategorysApi.createTemplateCategorys(params)
console.log('新增之后的数据', res)
await templateCategorysApi.createTemplateCategorys(payload)
ElMessage.success('新增成功')
}
} catch (error) {
console.error('❌ 提交失败:', error)
} catch {
ElMessage.error('提交失败,请检查网络或接口')
} finally {
loading.value = false
dialogVisible.value = false
getList() // 刷新列表
getList()
}
}
//处理删除
const handleDelete = async (id: number) => {
// ===================== 删除 =====================
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
//调用删除接口
await templateCategorysApi.deleteTemplateCategorys(id)
ElMessage.success('删除成功')
} catch (err) {
console.error('❌ 请求失败:', err)
ElMessage.error('请求失败,请检查网络或接口')
} finally {
getList() // 刷新列表
if (err !== 'cancel') {
ElMessage.error('删除失败,请检查网络或接口')
}
}
// ===================分页方法==================
const handleCurrentChange = (val: number) => {
pagination.pageNo = val
} finally {
getList()
}
}
// 处理分页大小变化
const handleSizeChange = (val: number) => {
pagination.pageSize = val
getList()
}
// ===================== 初始化 =====================
onMounted(() => {
getList()
loadtemplateList()
loadTemplateList()
})
</script>
<style scoped>
.plan-search-bar {
margin-bottom: 20px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
.search-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 15px 20px;
}
.search-buttons {
display: flex;
gap: 10px;
margin-left: 10px;
}
.pagination-wrapper {
display: flex;
padding: 12px 16px;
justify-content: flex-end;
}
/* 新样式:更美观、不变大小、不影响其他布局 */
.cover-img {
width: 100px;
height: 50px !important;
border: 1px solid #e5e7eb;
height: 50px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
box-shadow: 0 1px 3px rgb(0 0 0 / 8%);
transition: transform 0.2s ease;
... ... @@ -341,12 +296,5 @@ onMounted(() => {
.cover-img:hover {
transform: scale(1.03);
/* 鼠标悬浮轻微放大 */
}
.delete-btn {
color: #f5222d;
;
}
</style>
... ...
... ... @@ -4,7 +4,7 @@
组数配置(
<span v-if="type === 0">无氧运动</span>
<span v-else-if="type === 1">有氧运动</span>
<span v-else-if="type === 2">次训练</span>
<span v-else-if="type === 2">次训练</span>
<span v-else-if="type === 3">计时训练</span>
<span v-else-if="type === 4">自重加重</span>
<span v-else-if="type === 5">自重减重</span>
... ...
... ... @@ -4,7 +4,7 @@
<el-form ref="formRef" :model="formData" :rules="rules">
<!-- 模板名称 -->
<el-form-item label-suffix="*" prop="templateName" label="模板名称">
<el-input type="text" v-model="formData.templateName" placeholder="请输入模板名称" />
<el-input v-model="formData.templateName" placeholder="请输入模板名称" />
</el-form-item>
<!-- 封面图 -->
<el-form-item label-suffix="*" prop="templateCover" label="封面图">
... ... @@ -12,11 +12,8 @@
</el-form-item>
<!-- 训练场景 -->
<el-form-item label-suffix="*" prop="scene" label="训练场景">
<el-select v-model="formData.scene" placeholder="请选择训练场景" style="width: 100%">
<el-option label="不限" :value="1" />
<el-option label="仅哑铃" :value="2" />
<el-option label="仅哑铃+杠铃" :value="3" />
<el-option label="健身房" :value="4" />
<el-select v-model="formData.scene" placeholder="请选择训练场景" class="w-full">
<el-option v-for="opt in SCENE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
... ... @@ -27,44 +24,52 @@
<!-- 动作组容器:循环渲染所有动作组 -->
<div v-for="(unit, unitIndex) in formData.units" :key="unitIndex" class="unit-card">
<!-- 单元类型(动作组/超级组) -->
<el-form-item label-suffix="*" :label="`单元${unitIndex + 1}类型`" :prop="`units[${unitIndex}].unitType`">
<el-select v-model="unit.unitType" placeholder="请选择单元类型" style="width:100%">
<el-option label="动作组" :value="1" />
<el-option label="超级组" :value="2" />
</el-select>
</el-form-item>
<!-- 这里实现选择超级组或者动作组名称 -->
<el-form-item label-suffix="*" :label="unit.unitType === 1 ? '动作组名称' : '超级组名称'" class="row-form-item">
<div class="row-wrap">
<el-select v-model="unit.id" @change="(val) => onSelectItem(unitIndex, val)">
<template v-if="unit.unitType === 1">
<!-- 需要接入动作接口 -->
<el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" />
</template>
<!-- 需要接入超级组接口 -->
<template v-else>
<el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" />
</template>
</el-select>
<!-- 动作组头部 动作组名称+删除和增加动作组按钮-->
<div class="unit-header">
<!-- 单元操作工具栏 -->
<div class="unit-toolbar">
<el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1">
<template #icon>
<Minus />
</template>
删除单元
</el-button>
<el-button @click="AddUnit()">
<el-button type="primary" @click="addUnit()">
<template #icon>
<Plus />
</template>
新增单元
</el-button>
</div>
</div>
<!-- 单元类型(动作组/超级组) -->
<el-form-item label-suffix="*" :label="`单元${unitIndex + 1}类型`" :prop="`units[${unitIndex}].unitType`">
<el-select
v-model="unit.unitType"
placeholder="请选择单元类型"
class="w-full"
@change="onUnitTypeChange(unitIndex)"
>
<el-option label="动作组" :value="UNIT_TYPE.GROUP" />
<el-option label="超级组" :value="UNIT_TYPE.SUPERSET" />
</el-select>
</el-form-item>
<!-- 选择超级组或者动作组名称 -->
<el-form-item label-suffix="*" :label="unit.unitType === UNIT_TYPE.GROUP ? '动作组名称' : '超级组名称'">
<el-select
v-model="unit.id"
:placeholder="unit.unitType === UNIT_TYPE.GROUP ? '请选择动作' : '请选择超级组'"
class="w-full"
@change="(val) => onSelectItem(unitIndex, val)"
>
<template v-if="unit.unitType === UNIT_TYPE.GROUP">
<el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" />
</template>
<template v-else>
<el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" />
</template>
</el-select>
</el-form-item>
<!-- 排序 -->
<el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`">
<el-input v-model="unit.sortOrder" min="1" max="99" />
... ... @@ -72,7 +77,7 @@
<!-- 循环:动作组 1 个、超级组 N 个,都会渲染 -->
<template v-for="(item, idx) in unit.exercises" :key="idx">
<div style="margin: 16px 0;">
<div class="exercise-item">
<div class="action-name">动作名称:{{ getExerciseName(item.exerciseId) }}</div>
<ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" />
</div>
... ... @@ -90,87 +95,87 @@
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, nextTick } from 'vue'
import { MusclesApi } from '@/api/store/training/muscle'
<script setup>
import { ref, onMounted, reactive, nextTick, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Minus, Plus } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
// import './TemplateAdd.css'
import { TrainingTemplatesApi } from '@/api/store/training/templates'
import { ExercisesApi } from '@/api/store/training/pose'
import ExerciseSetTable from '../templates/ExerciseSetTable.vue'
import { SupersetsApi } from '@/api/store/training/Supersets'
//定义参数
const formRef = ref<FormInstance>()
const muscleList = ref<any[]>([])
// ==================== 常量定义 ====================
const UNIT_TYPE = { GROUP: 1, SUPERSET: 2 }
const EXERCISE_TYPE_MAP = {
'无氧运动': 0,
'有氧运动': 1,
'记次训练': 2,
'计时训练': 3,
'自重加重': 4,
'自重减重': 5,
'间歇训练': 6
}
const SCENE_OPTIONS = [
{ label: '不限', value: 1 },
{ label: '仅哑铃', value: 2 },
{ label: '仅哑铃+杠铃', value: 3 },
{ label: '健身房', value: 4 }
]
// ==================== 数据工厂函数 ====================
const createDefaultSet = () => ({
weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1
})
const createDefaultExercise = () => ({
exerciseId: 0, exerciseType: 1, innerOrder: 1, sets: [createDefaultSet()]
})
const createDefaultUnit = (sortOrder) => ({
id: null, name: '', unitType: UNIT_TYPE.GROUP, sortOrder, exercises: [createDefaultExercise()]
})
// ==================== 响应式状态 ====================
const formRef = ref(null)
const loading = ref(false)
const router = useRouter()
const route = useRoute()
const exercisesList = ref<any[]>([])
const superGroupList = ref<any[]>([])
const exercisesList = ref([])
const superGroupList = ref([])
// 表单数据
const formData = reactive({
// templateId: 0, // 新增接口要求的根级别id字段
id: 0,
groupId: null,
templateName: '',
scene: null,
templateCover: '',
templateIntroduction: '',
units: [
{
id: 0,
name: '',
unitType: 1, //1=动作组,2=超级组
sortOrder: 1,
exercises: [
{
exerciseId: 0,
exerciseType: 1, //动作类型
innerOrder: 1,
sets: [
{
weight: 0,
reps: 0,
distance: 0,
duration: 0, // 接口唯一时间字段
restTime: 0,
setIndex: 1
}
]
}
]
}
]
units: [createDefaultUnit(1)]
})
//静态表单验证规则,对应不是循环的表单项检验
const rules = ref({
const rules = reactive({
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
templateCover: [{ required: true, message: '请上传封面图', trigger: 'change' }],
scene: [{ required: true, message: '请选择训练场景', trigger: 'change' }],
templateIntroduction: [{ required: true, message: '请输入模板简介', trigger: 'blur' }]
})
// ===========================加载数据======================================
//加载数据
const loadsuperGroupList = async () => {
// ==================== 数据加载 ====================
const loadSuperGroupList = async () => {
try {
const res = await SupersetsApi.getSupersetList()
const res = await SupersetsApi.getSupersetList(false)
superGroupList.value = res || []
} catch (err) {
console.error('加载动作列表失败:', err)
ElMessage.error('加载动作列表失败')
console.error('加载超级组列表失败:', err)
ElMessage.error('加载超级组列表失败')
}
}
//加载数据
const loadExercisesList = async () => {
try {
const res = await ExercisesApi.getExercisesList()
const res = await ExercisesApi.getExercisesList(false)
exercisesList.value = res || []
} catch (err) {
console.error('加载动作列表失败:', err)
... ... @@ -178,13 +183,11 @@ const loadExercisesList = async () => {
}
}
// ====================== 【关键】编辑回显 ======================
const loadTemplateDetail = async (id: number) => {
const loadTemplateDetail = async (id) => {
try {
loading.value = true
const res = await TrainingTemplatesApi.getTrainingTemplates(id)
const detail = res.data || res
console.log('编辑详细模板返回的数据detail', detail)
formData.id = detail.id || 0
formData.groupId = detail.groupId || null
... ... @@ -193,27 +196,18 @@ const loadTemplateDetail = async (id: number) => {
formData.templateCover = detail.urlCover || detail.templateCover || ''
formData.templateIntroduction = detail.introduction || ''
// 🔥 修复:超级组多动作回显(不再写死 [0])
if (detail.units && detail.units.length) {
formData.units = detail.units.map((unit: any) => ({
id: unit.unitId || 0,
formData.units = detail.units.map((unit) => ({
id: unit.unitId || null,
name: unit.unitName || '',
unitType: unit.unitType || 1,
unitType: unit.unitType || UNIT_TYPE.GROUP,
sortOrder: unit.sortOrder || 1,
// ✅ 这里修复:完整映射所有动作(动作组1个、超级组N个都正常)
exercises: unit.exercises?.map((item: any) => ({
exercises: unit.exercises?.map((item) => ({
exerciseId: item.exerciseId || 0,
exerciseType: item.exerciseType || 1,
innerOrder: item.innerOrder || 1,
sets: item.sets || [
{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }
]
})) || [{
exerciseId: 0,
exerciseType: 1,
innerOrder: 1,
sets: [{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }]
}]
sets: item.sets || [createDefaultSet()]
})) || [createDefaultExercise()]
}))
}
... ... @@ -221,80 +215,57 @@ const loadTemplateDetail = async (id: number) => {
updateDynamicRules()
formRef.value?.clearValidate()
})
} catch (err) {
ElMessage.error('获取详情失败')
console.error(err)
console.error('获取模板详情失败:', err)
ElMessage.error('获取模板详情失败')
} finally {
loading.value = false
}
}
//动态检验
// ==================== 动态表单校验 ====================
const updateDynamicRules = () => {
// 先清理所有动态校验规则(避免删除单元后残留旧规则报错)
Object.keys(rules.value).forEach(key => {
if (key.startsWith('units[')) { // 匹配所有循环生成的规则
delete rules.value[key]
Object.keys(rules).forEach((key) => {
if (key.startsWith('units[')) {
delete rules[key]
}
})
//必须使用units[${index}].unitType这种,不然验证找不到真实的数据,验证出错。
formData.units.forEach((unit, index) => {
rules.value[`units[${index}].unitType`] = [
rules[`units[${index}].unitType`] = [
{ required: true, message: '请选择单元类型', trigger: 'change' }
]
rules.value[`units[${index}].name`] = [
rules[`units[${index}].name`] = [
{ required: true, message: '请选择动作', trigger: 'change' }
]
rules.value[`units[${index}].sortOrder`] = [
rules[`units[${index}].sortOrder`] = [
{ required: true, message: '请输入排序', trigger: 'blur' }
]
})
// 清除校验状态,防止新增就爆红
nextTick(() => {
if (formRef.value) formRef.value.clearValidate()
})
}
// ---------------------- 交互方法 ----------------------
// 动作选择联动名称(新增unitIndex参数,绑定当前动作组)
const handleExerciseChange = (exerciseId: number, unitIndex: number) => {
const unit = formData.units[unitIndex]
// 根据类型找对应的数据
let selectedItem
if (unit.unitType === 1) {
selectedItem = exercisesList.value.find(item => item.id === exerciseId)
} else {
selectedItem = superGroupList.value.find(item => item.id === exerciseId)
}
if (selectedItem) {
unit.name = selectedItem.name
}
// ==================== 交互方法 ====================
const getExerciseName = (id) => {
if (!id) return '未选择动作'
const item = exercisesList.value.find((i) => i.id === id)
return item ? item.name : '未知动作'
}
// 根据ID获取动作名称
const getExerciseName = (id: number) => {
if (!id) return '未选择动作'
// 先从动作列表找
const item = exercisesList.value.find(i => i.id === id)
if (item) return item.name
return '未知动作'
const addUnit = () => {
formData.units.push(createDefaultUnit(formData.units.length + 1))
updateDynamicRules()
ElMessage.success('新增单元成功(可切换为超级组)')
}
// 删除动作组(接收unitIndex,删除指定动作组)
const delUnit = (unitIndex: number) => {
// 只保留至少 1 个单元
const delUnit = (unitIndex) => {
if (formData.units.length <= 1) {
ElMessage.warning('至少保留一个单元')
return
}
ElMessageBox.confirm('确定要删除这个单元吗?', '提示', { type: 'warning' }).then(() => {
formData.units.splice(unitIndex, 1)
// 重新排序
formData.units.forEach((unit, index) => {
unit.sortOrder = index + 1
})
... ... @@ -303,141 +274,70 @@ const delUnit = (unitIndex: number) => {
})
}
//增加单元(动作组/超级组)
const AddUnit = () => {
// 默认新增动作组,用户之后可在页面切换为超级组
const newUnit = {
id: 0,
name: '',
unitType: 1, // 默认动作组
sortOrder: formData.units.length + 1,
exercises: [
{
exerciseId: 0,
exerciseType: 1,
innerOrder: 1,
sets: [
{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }
]
}
]
}
formData.units.push(newUnit)
updateDynamicRules()
ElMessage.success('新增单元成功(可在上方切换为超级组)')
const onUnitTypeChange = (unitIndex) => {
const unit = formData.units[unitIndex]
// 单元类型切换后,原选中的动作组/超级组 id、名称及动作明细都不再适用,需重置
unit.id = null
unit.name = ''
unit.exercises = [createDefaultExercise()]
}
// 选择 动作 或 超级组 后,加载详情
const onSelectItem = async (unitIndex: number, selectId: number) => {
const onSelectItem = async (unitIndex, selectId) => {
const unit = formData.units[unitIndex]
// 清空之前的数据
unit.exercises = []
if (unit.unitType === 1) {
// ==================
// 1. 动作组:查单个动作详情
// ==================
if (unit.unitType === UNIT_TYPE.GROUP) {
// 动作组:查询单个动作详情
const res = await ExercisesApi.getExercises(selectId)
console.log('选择的动作组详情', res)
const detail = res
console.log('选择的动作详情:detail', detail)
console.log('【动作组详情完整数据】', JSON.stringify(detail, null, 2))
console.log('【动作名称】', detail.name)
console.log('【后端返回的原始exerciseType】', detail.exerciseType)
unit.name = detail.name || ''
// 关键2:只有动作详情是文字,做文字转数字映射
const typeMap: Record<string, number> = {
'无氧运动': 0,
'有氧运动': 1,
'记次训练': 2,
'计时训练': 3,
'自重加重': 4,
'自重减重': 5,
'间歇训练': 6
}
// 能映射就转数字,映射失败默认给0
const typeVal = typeMap[detail.exerciseType] ?? 0
unit.exercises.push({
exerciseId: detail.id,
exerciseType: typeVal,
exerciseType: EXERCISE_TYPE_MAP[detail.exerciseType] ?? 0,
innerOrder: 1,
sets: [{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }]
sets: [createDefaultSet()]
})
console.log('【映射后的exerciseType数字】', typeVal)
} else {
// ==================
// 2. 超级组:查超级组详情(返回多个动作)
// ==================
// 超级组:查询超级组详情(含多个动作)
const res = await SupersetsApi.getSupersets(selectId)
console.log('加载的超级组详情:', res)
const detail = res
unit.name = detail.name || ''
console.log('加载选中的超级组详情:', detail)
// 🔴 关键:超级组接口直接返回数字,不需要再做映射
unit.exercises = detail.exercises.map((item: any) => {
console.log('【超级组内动作原始exerciseType】', item.exerciseType)
// 直接用返回的数字即可
const typeVal = Number(item.exerciseType) || 0
console.log('【超级组内动作最终exerciseType数字】', typeVal)
return {
unit.exercises = detail.exercises.map((item) => ({
exerciseId: item.id,
exerciseType: typeVal,
exerciseType: Number(item.exerciseType) || 0,
innerOrder: item.innerOrder,
sets: item.sets || [{ weight: 0, reps: 0, restTime: 0, setIndex: 1 }]
}
})
}))
}
}
// 取消
// ==================== 表单提交 ====================
const handleCancel = () => {
ElMessage.info('取消保存表单内容')
formRef.value?.resetFields()
// 重置表单数据(可选)
formData.id = 0
formData.groupId = null
formData.templateName = ''
formData.scene = null
formData.templateCover = ''
formData.templateIntroduction = ''
formData.units = [
{
Object.assign(formData, {
id: 0,
name: '',
unitType: 1,
sortOrder: 1,
exercises: [
{
exerciseId: 0,
exerciseType: 1,
innerOrder: 1,
sets: [
{ weight: 0, reps: 0, distance: 0, duration: 0, restTime: 0, setIndex: 1 }
]
}
]
}
]
// 重置后更新校验规则
groupId: null,
templateName: '',
scene: null,
templateCover: '',
templateIntroduction: '',
units: [createDefaultUnit(1)]
})
updateDynamicRules()
// ✅ 关键:跳回主页面
router.push('/training/templates/templatesForm')
}
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch (err) {
} catch {
ElMessage.error('请按照要求填写')
return
}
// 动作组/超级组不能为空。
const hasEmptyExercise = formData.units.some(unit => !unit.exercises[0].exerciseId)
const hasEmptyExercise = formData.units.some((unit) => !unit.exercises[0].exerciseId)
if (hasEmptyExercise) {
ElMessage.warning('请为所有动作组选择动作名称')
return
... ... @@ -445,24 +345,23 @@ const handleSubmit = async () => {
loading.value = true
try {
// 按接口文档转换提交数据
const submitData = {
templateId: formData.id, // 关键:前端id → 后端templateId
templateId: formData.id,
groupId: formData.groupId,
templateName: formData.templateName,
scene: formData.scene,
templateCover: formData.templateCover,
templateIntroduction: formData.templateIntroduction,
units: formData.units.map(unit => ({
id: unit.id,
units: formData.units.map((unit) => ({
id: unit.id ?? 0,
name: unit.name,
unitType: unit.unitType,
sortOrder: unit.sortOrder,
exercises: unit.exercises.map(exercise => ({
exercises: unit.exercises.map((exercise) => ({
exerciseId: exercise.exerciseId,
exerciseType: exercise.exerciseType,
innerOrder: exercise.innerOrder,
sets: exercise.sets.map(set => ({
sets: exercise.sets.map((set) => ({
weight: set.weight,
reps: set.reps,
distance: set.distance,
... ... @@ -474,65 +373,35 @@ const handleSubmit = async () => {
}))
}
console.log('提交给后端的完整数据:', submitData)
let res: any
if (route.query.id) {
// 编辑:用转换后的submitData
res = await TrainingTemplatesApi.updateTrainingTemplates(submitData)
await TrainingTemplatesApi.updateTrainingTemplates(submitData)
} else {
// 新增:用转换后的submitData
res = await TrainingTemplatesApi.createTrainingTemplates(submitData)
await TrainingTemplatesApi.createTrainingTemplates(submitData)
}
console.log('接口返回结果:', res)
ElMessage.success('模板保存成功!')
// router.push('/training/templates/templatesForm')
router.back()
} catch (err: any) {
// 👇 修复日志打印,避免undefined
console.error("【完整错误对象】", err);
console.error("【错误信息】", err?.message || '未知错误');
console.error("【响应数据】", err?.response?.data || '无响应数据');
console.error("【响应状态】", err?.response?.status || '无状态码');
ElMessage.error(`保存失败:${err?.message || '网络/服务异常'}`);
} catch (err) {
console.error('保存失败:', err?.message || err)
ElMessage.error(`保存失败:${err?.message || '网络/服务异常'}`)
} finally {
loading.value = false
}
}
// 加载肌肉列表
const loadMuscleList = async () => {
try {
const res = await MusclesApi.getMusclesPage({ pageNo: '1', pageSize: '100', name: '' })
const data = res.data || res
muscleList.value = data.list || []
} catch (err) {
console.error('加载肌肉列表失败:', err)
ElMessage.error('加载肌肉列表失败')
}
}
//生命周期
// ==================== 生命周期 ====================
onMounted(async () => {
// groupList.value = [{ id: 1, name: "默认大类" }]
loadMuscleList()
loadExercisesList()
loadsuperGroupList()
// 进入页面判断是否编辑
loadSuperGroupList()
if (route.query.id) {
loadTemplateDetail(Number(route.query.id))
}
})
// 监听路由ID变化,自动回显
watch(
() => route.query.id,
(newId) => {
if (newId) {
loadTemplateDetail(Number(newId))
} else {
// handleCancel()
}
},
{ immediate: true }
... ... @@ -540,120 +409,78 @@ watch(
</script>
<style scoped>
:deep(.el-form-item__label) {
/* ==================== 表单项全局样式 ==================== */
::deep(.el-form-item__label) {
float: none !important;
width: 68px !important;
width: 88px !important;
padding: 0 !important;
margin-right: 20px !important;
margin-right: 16px !important;
line-height: 32px;
text-align: right;
white-space: nowrap;
}
/* 让表单内容区域横向排列 */
.row-form-item :deep(.el-form-item__content) {
display: flex;
align-items: center;
/* ==================== 工具类 ==================== */
.w-full {
width: 100%;
}
/* 下拉框 + 按钮 同行容器 */
.row-wrap {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
/* ==================== 页面布局 ==================== */
.page-contain {
height: 100vh;
overflow-y: auto;
background-color: #f5f7fa;
box-sizing: border-box;
}
/* 下拉框占剩余宽度 */
.sel {
flex: 1;
}
.templateName {
/* 按钮容器:不被压缩 */
.unit-header {
display: flex;
gap: 10px;
flex-shrink: 0;
padding: 30px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgb(0 0 0 / 5%);
}
/* 下面是你原来保留的样式,不动 */
/* 动作组卡片 */
/* ==================== 单元卡片 ==================== */
.unit-card {
margin: 20px 0;
border-radius: 6px;
}
/* 表格容器 */
.sets-table-wrapper {
margin: 20px 0;
overflow-x: auto;
}
.table-title {
margin-bottom: 10px;
font-size: 14px;
font-weight: 500;
color: #666;
}
/* 表格样式 */
.sets-table {
width: 100%;
text-align: center;
border-collapse: collapse;
padding: 16px 20px 20px;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 8px;
transition: border-color 0.2s;
}
.sets-table th {
padding: 10px 0;
font-weight: 500;
background: #fafafa;
.unit-card:hover {
border-color: #c0c4cc;
}
.sets-table td {
padding: 8px 0;
.unit-toolbar {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 4px;
}
.set-row:hover {
background: #f9f9f9;
.exercise-item {
margin: 16px 0;
}
.action-name {
color: #666;
margin-bottom: 8px;
font-size: 13px;
color: #606266;
}
/* 按钮居中容器 */
/* ==================== 按钮区域 ==================== */
.btn-center-wrapper {
display: flex;
width: 100%;
padding-top: 15px;
padding-top: 20px;
margin-top: 30px;
border-top: 1px solid #eee;
justify-content: center;
gap: 12px;
}
/* 封面图样式 */
.sub-image {
border: 1px solid #eee;
border-radius: 10px;
}
/* 页面滚动容器 */
.page-contain {
height: 100vh;
overflow-y: auto;
background-color: #f5f5f5;
box-sizing: border-box;
}
/* 表单容器 */
.templateName {
padding: 30px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgb(0 0 0 / 5%);
}
</style>
... ...
<template>
<Dialog v-model="dialogVisible" :title="title" width="800px" height="100%" append-to-body>
<!-- 这里直接用 SonFormData -->
<el-form ref="formRef" :model="SonFormData" label-width="120px" :rules="formRules" label-position="right">
<el-form-item label="计划名称" prop="name" label-suffix="*">
<Dialog v-model="dialogVisible" :title="title" width="800px">
<el-form ref="formRef" :model="SonFormData" label-width="120px" :rules="formRules">
<el-form-item label="计划名称" prop="name">
<el-input v-model="SonFormData.name" placeholder="请输入计划名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="封面图" prop="urlCover" class="form-item-margin" label-suffix="*">
<el-form-item label="封面图" prop="urlCover">
<UploadImg v-model="SonFormData.urlCover" />
</el-form-item>
<!-- 计划分类/后来加上 -->
<el-form-item label="计划分类" prop="categoryId" label-suffix="*">
<el-form-item label="计划分类" prop="categoryId">
<el-select v-model="SonFormData.categoryId" placeholder="请选择计划分类">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="训练场景" prop="trainingScene" label-suffix="*">
<el-select v-model="SonFormData.trainingScene">
<el-option label="不限" :value="1" />
<el-option label="健身房" :value="2" />
<el-option label="仅哑铃" :value="3" />
<el-option label="仅哑铃+杠铃" :value="4" />
<el-form-item label="训练场景" prop="trainingScene">
<el-select v-model="SonFormData.trainingScene" placeholder="请选择训练场景">
<el-option v-for="item in trainingSceneOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<!-- 难度的不限是后来加上的 -->
<el-form-item label="计划难度" prop="difficultyLevel" label-suffix="*">
<el-select v-model="SonFormData.difficultyLevel">
<!-- <el-option label="不限" :value="0" /> -->
<el-option label="初阶" :value="1" />
<el-option label="中阶" :value="2" />
<el-option label="高阶" :value="3" />
<el-form-item label="计划难度" prop="difficultyLevel">
<el-select v-model="SonFormData.difficultyLevel" placeholder="请选择难度">
<el-option v-for="item in difficultyLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="适合人群" prop="targetPeople" label-suffix="*">
<el-select v-model="SonFormData.targetPeople">
<el-option label="不限" :value="1" />
<el-option label="青少年" :value="2" />
<el-option label="成年人" :value="3" />
<el-option label="中年人" :value="4" />
<el-option label="运动损伤" :value="5" />
<el-form-item label="适合人群" prop="targetPeople">
<el-select v-model="SonFormData.targetPeople" placeholder="请选择适合人群">
<el-option v-for="item in targetPeopleOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="频率" prop="frequencyPerWeek" label-suffix="*">
<el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1">
<el-form-item label="频率" prop="frequencyPerWeek">
<el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1" placeholder="请选择频率">
<el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="计划周期(周)" prop="durationWeeks" label-suffix="*">
<el-form-item label="计划周期(周)" prop="durationWeeks">
<el-input-number v-model="SonFormData.durationWeeks" class="w-full" />
</el-form-item>
<!-- 选择模板,并且显示模板详情 -->
<div v-for="(templates, templatesIndex) in SonFormData.templateIds" :key="templatesIndex" style="width: 100%;">
<el-form-item label="模板" :prop="`templateIds[${templatesIndex}].templateId`" label-suffix="*"
style="width: 100%; margin: 0;" class="is-required">
<div style="display: flex; align-items: center; gap: 12px;width: 100%;">
<el-select v-model="templates.templateId" placeholder="请选择模板" style="flex: 1; width: 0;"
@change="() => loadTemplateDetail(templatesIndex, templates.templateId)"
@click="() => reloadTemplateDetail(templatesIndex)">
<!-- 模板选择区域 -->
<div v-for="(tpl, tplIdx) in SonFormData.templateIds" :key="tplIdx" class="template-block">
<div class="template-block-toolbar">
<el-button :disabled="SonFormData.templateIds.length <= 1" @click="removeTemplate(tplIdx)">
<Icon icon="ep:minus" class="mr-5px" />删除
</el-button>
<el-button type="primary" @click="addTemplate(tplIdx)">
<Icon icon="ep:plus" class="mr-5px" />新增
</el-button>
</div>
<el-form-item :prop="`templateIds[${tplIdx}].templateId`" label="模板">
<el-select v-model="tpl.templateId" placeholder="请选择模板" class="w-full"
@change="() => loadTemplateDetail(tplIdx, tpl.templateId)" @click="() => reloadTemplateDetail(tplIdx)">
<el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<div style="display: flex; gap: 8px; flex-shrink: 0;">
<el-button @click="deltemplate(templatesIndex)"
:disabled="SonFormData.templateIds.length <= 1">删除</el-button>
<el-button @click="Addtemplate(templatesIndex)">新增</el-button>
</div>
</div>
</el-form-item>
<el-form-item :prop="`templateIds[${templatesIndex}].sortOrder`" label="排序" label-suffix="*"
class="is-required">
<el-input-number v-model="templates.sortOrder" :min="1" :max="99" placeholder="排序号" style="width: 100%" />
<el-form-item :prop="`templateIds[${tplIdx}].sortOrder`" label="排序">
<el-input-number v-model="tpl.sortOrder" :min="1" :max="99" placeholder="排序号" class="w-full" />
</el-form-item>
<!-- 模板详情 -->
<!-- 模板详情回显(新增部分) -->
<el-form-item label="模板详情" v-if="templateDetailMap[templatesIndex]"
style="width: 100%; padding: 0; margin: 0 auto;" class="template-detail-container">
<el-form-item v-if="templateDetailMap[tplIdx]" label="模板详情" class="template-detail-container">
<el-collapse-transition>
<div v-loading="templateLoading[templatesIndex]">
<!-- 训练单元列表 -->
<el-table :data="templateDetailMap[templatesIndex].units || []" border
style="width: 100%; margin-bottom: 12px;" :show-header="true" class="expand-table-fix">
<el-table-column prop="unitName" label="训练单元名称" min-width="150" />
<el-table-column prop="sortOrder" label="排序" width="100" align="center" />
<!-- <el-table-column prop="setCount" label="组数" width="80" align="center" /> -->
</el-table>
<!-- 用纯DIV替代嵌套表格,彻底解决宽度问题 -->
<div v-for="unit in templateDetailMap[templatesIndex].units || []" :key="unit.unitId"
class="unit-container">
<!-- 训练单元行(可点击展开) -->
<div v-loading="templateLoading[tplIdx]">
<div v-for="unit in templateDetailMap[tplIdx].units || []" :key="unit.unitId" class="unit-container">
<div class="unit-row" @click="unit.isExpand = !unit.isExpand">
<span class="expand-icon">{{ unit.isExpand ? '▼' : '▶' }}</span>
<Icon icon="ep:arrow-right" class="expand-icon" :class="{ 'is-expanded': unit.isExpand }" />
<span class="unit-name">{{ unit.unitName }}</span>
</div>
<!-- 展开的动作列表 -->
<div v-if="unit.isExpand" class="exercise-container">
<div v-for="exercise in unit.exercises || []" :key="exercise.exerciseId" class="exercise-row">
<div class="exercise-header">
<!-- <span class="exercise-name">{{ exercise.exerciseName }}</span> -->
<!-- <span class="exercise-info">组数:{{ exercise.setCount }} | 重量:{{ exercise.weight }} | 次数:{{
exercise.reps }}</span> -->
<!-- <span class="exercise-expand" @click="exercise.isExpand = !exercise.isExpand">
{{ exercise.isExpand ? '▼ 收起组详情' : '▶ 展开详情' }}
</span> -->
</div>
<!-- 展开的组详情 -->
<div class="set-container">
<div v-for="exercise in unit.exercises || []" :key="exercise.exerciseId" class="exercise-item">
<div class="set-header">
<span>重量</span>
<span>距离</span>
... ... @@ -129,23 +94,18 @@
</div>
</div>
</div>
<!-- -->
</div>
</el-collapse-transition>
</el-form-item>
</div>
<!-- <el-form-item label="所需器械汇总" prop="equipmentsSummary" label-suffix="*">
<el-input v-model="SonFormData.equipmentsSummary" />
</el-form-item> -->
<el-form-item label="计划介绍" prop="introduction" label-suffix="*">
<el-form-item label="计划介绍" prop="introduction">
<el-input v-model="SonFormData.introduction" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
... ... @@ -155,16 +115,13 @@ import { ref, reactive, watch, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { TrainingTemplatesApi } from '@/api/store/training/templates'
import { trainingPlanApi } from '@/api/store/training/trainingPlan'
import { Minus, Plus } from '@element-plus/icons-vue'
import { Dialog } from '@/components/Dialog'
// import { Table } from '@/components/Table'
// 父组件传过来的值(只用来接收,不冲突)
// ==================== Props & Emits ====================
const props = defineProps({
visible: { type: Boolean, default: false },
title: String,
formData: { type: Object, default: null }
})
const emit = defineEmits(['submit', 'update:visible'])
const dialogVisible = computed({
... ... @@ -172,78 +129,71 @@ const dialogVisible = computed({
set: (val) => emit('update:visible', val)
})
// ========================
// 子组件自己的表单数据(唯一来源)
// ========================
const SonFormData = ref({
id: undefined,
categoryId: 0,
name: '',
urlCover: '',
introduction: '',
durationWeeks: 1,
frequencyPerWeek: 1,
difficultyLevel: 1,
targetPeople: 1,
trainingScene: 1,
equipmentsSummary: '',
// ==================== 表单默认值(仅保留渲染所必需的字段) ====================
const defaultFormData = () => ({
templateIds: [{ templateId: undefined, sortOrder: 1 }]
})
// 是否是编辑功能
// 新增:专门的编辑状态标记
// ==================== 状态 ====================
const SonFormData = ref(defaultFormData())
const isEdit = ref(false)
const formRef = ref()
const loading = ref(false)
const templateList = ref([])
const categoryList = ref([]) // 计划分类列表
// 存储每个模板的详情数据,key 是模板 index,value 是接口返回的详情
const categoryList = ref([])
const templateDetailMap = ref({})
// 模板详情加载状态
const templateLoading = ref({})
// 控制练的频率
// ==================== 计算属性 ====================
const templateCount = computed(() => SonFormData.value.templateIds.length)
// 频率选项:1个模板显示全部,多个模板只显示对应数量
const frequencyOptions = computed(() => {
const count = templateCount.value
if (count === 1) {
return [
{ label: '1练/周', value: 1 },
{ label: '2练/周', value: 2 },
{ label: '3练/周', value: 3 },
{ label: '4练/周', value: 4 },
{ label: '5练/周', value: 5 },
{ label: '6练/周', value: 6 },
]
} else {
// 多个模板 → 只显示和模板数量一样的选项
return [{ label: `${count}练/周`, value: count }]
if (templateCount.value > 1) {
return [{ label: templateCount.value + '练/周', value: templateCount.value }]
}
return [1, 2, 3, 4, 5, 6].map((n) => ({ label: n + '练/周', value: n }))
})
// ==================== 静态下拉选项 ====================
const trainingSceneOptions = [
{ label: '不限', value: 1 },
{ label: '健身房', value: 2 },
{ label: '仅哑铃', value: 3 },
{ label: '仅哑铃+杠铃', value: 4 }
]
const difficultyLevelOptions = [
{ label: '初阶', value: 1 },
{ label: '中阶', value: 2 },
{ label: '高阶', value: 3 }
]
const targetPeopleOptions = [
{ label: '不限', value: 1 },
{ label: '青少年', value: 2 },
{ label: '成年人', value: 3 },
{ label: '中年人', value: 4 },
{ label: '运动损伤', value: 5 }
]
// ==================== 频率同步 ====================
const syncFrequency = () => {
const count = templateCount.value
if (count > 1) {
// 超过1个模板 → 强制频率 = 模板数量
SonFormData.value.frequencyPerWeek = count
if (templateCount.value > 1) {
SonFormData.value.frequencyPerWeek = templateCount.value
}
}
// 监听模板数量变化,自动更新频率
watch(templateCount, () => {
syncFrequency()
})
watch(templateCount, syncFrequency)
// ==================== 表单验证规则 ====================
const formRules = reactive({
name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }],
frequencyPerWeek: [{ required: true, message: '请输入训练频率', trigger: 'change' }],
difficultyLevel: [{ required: true, message: '请输入难度', trigger: 'change' }],
targetPeople: [{ required: true, message: '请输入人群', trigger: 'change' }],
trainingScene: [{ required: true, message: '请输入场景', trigger: 'change' }],
urlCover: [{ required: true, message: '输入图片', trigger: 'change' }],
// equipmentsSummary: [{ required: true, message: '请输入所需器械汇总', trigger: 'blur' }],
difficultyLevel: [{ required: true, message: '请选择难度', trigger: 'change' }],
targetPeople: [{ required: true, message: '请选择人群', trigger: 'change' }],
trainingScene: [{ required: true, message: '请选择场景', trigger: 'change' }],
urlCover: [{ required: true, message: '请上传封面图', trigger: 'change' }],
introduction: [{ required: true, message: '请输入计划介绍', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择计划分类', trigger: 'change' }],
templateIds: [{
... ... @@ -254,93 +204,74 @@ const formRules = reactive({
return callback(new Error('至少添加一个模板'))
}
for (let i = 0; i < value.length; i++) {
const item = value[i]
if (!item.templateId) {
return callback(new Error(`第 ${i + 1} 个模板未选择`))
if (!value[i].templateId) {
return callback(new Error('第 ' + (i + 1) + ' 个模板未选择'))
}
if (!item.sortOrder || item.sortOrder < 1) {
return callback(new Error(`第 ${i + 1} 个排序号必须≥1`))
if (!value[i].sortOrder || value[i].sortOrder < 1) {
return callback(new Error('第 ' + (i + 1) + ' 个排序号必须≥1'))
}
}
callback()
}
}],
}]
})
watch(dialogVisible, async (val) => {
if (!val) return
// 重置模板详情缓存,解决旧数据残留问题
templateDetailMap.value = {}
templateLoading.value = {}
SonFormData.value = {
id: undefined,
categoryId: 0,
name: '',
urlCover: '',
introduction: '',
durationWeeks: 1,
frequencyPerWeek: 1,
difficultyLevel: 1,
targetPeople: 1,
trainingScene: 1,
equipmentsSummary: '',
templateIds: [{ templateId: undefined, sortOrder: 1 }]
}
// 新增
if (!props.formData || Object.keys(props.formData).length === 0 || props.formData.id == null) {
// 控制使用哪个接口
isEdit.value = false // 强制写死
// 新增:重置清除前面编辑可能留下的校验
// ==================== 弹窗打开时初始化 ====================
const initAdd = () => {
isEdit.value = false
SonFormData.value = defaultFormData()
formRef.value?.clearValidate()
console.log('新增接口isEdit的值', isEdit)
} else {
}
const initEdit = async () => {
isEdit.value = true
// 编辑:赋值
// 调计划详情接口
const res = await trainingPlanApi.getTrainingPlanDetail(props.formData.id)
console.log('计划详情数据res================', res)
console.log('计划详情数据res.data================', res.data)
console.log('计划详情数据res.equipmentsSummary================', res.equipmentsSummary)
if (!res || !res.id) {
ElMessage.error('获取计划详情失败')
console.log('获取计划详情失败')
return
}
SonFormData.value = res
// 2. 补 res 里缺失、但父组件有值的字段
// 补全父组件传入但详情未返回的字段
SonFormData.value.trainingScene = props.formData.trainingScene
SonFormData.value.targetPeople = props.formData.targetPeople
// 详情接口返回的字段和SonFormData的不一样,需要处理
SonFormData.value.categoryId = props.formData.categoryId
console.log("===== 父组件传过来的完整数据 阉割版=====")
console.log(props.formData)
// // 详情返回的是数组,但是这里要的是字符串
// 处理 equipmentsSummary 数组转字符串
if (Array.isArray(res.equipmentsSummary)) {
SonFormData.value.equipmentsSummary = res.equipmentsSummary.join(',')
}
// 赋值模板的id和排序
// 映射模板 ID 和排序
if (res.templates && res.templates.length) {
SonFormData.value.templateIds = res.templates.map(item => ({
SonFormData.value.templateIds = res.templates.map((item) => ({
templateId: item.id,
sortOrder: item.sortOrder
}))
}
// 防止模板为空,导致模板选项不出现(虽然不太可能,因为这个是必填项)
if (!SonFormData.value.templateIds || SonFormData.value.templateIds.length === 0) {
SonFormData.value.templateIds = [{ templateId: undefined, sortOrder: 1 }]
}
// // 编辑状态:自动加载已有模板的详情
SonFormData.value.templateIds.forEach((item, index) => {
if (item.templateId) {
loadTemplateDetail(index, item.templateId)
}
// 自动加载已有模板的详情
SonFormData.value.templateIds.forEach((item, idx) => {
if (item.templateId) loadTemplateDetail(idx, item.templateId)
})
syncFrequency()
formRef.value?.clearValidate()
}
watch(dialogVisible, async (val) => {
if (!val) return
templateDetailMap.value = {}
templateLoading.value = {}
SonFormData.value = defaultFormData()
if (!props.formData || Object.keys(props.formData).length === 0 || props.formData.id == null) {
initAdd()
} else {
await initEdit()
}
formRef.value?.clearValidate()
})
// ==========================加载数据======================
// 加载模板列表
// ==================== 数据加载 ====================
const loadTemplateList = async () => {
const res = await TrainingTemplatesApi.getTrainingTemplatesPage({ pageNo: 1, pageSize: 100 })
templateList.value = res.list || []
... ... @@ -348,49 +279,72 @@ const loadTemplateList = async () => {
ElMessage.warning('暂无可用训练模板,请先创建模板')
}
}
// 获取模板详情并缓存
const loadTemplateDetail = async (index, templateId) => {
if (!templateId) return
// 标记加载中
templateLoading.value[index] = true
try {
const res = await TrainingTemplatesApi.getTrainingTemplates(templateId)
// 给每个单元、动作加展开状态(默认收起)
res.units?.forEach(unit => {
res.units?.forEach((unit) => {
unit.isExpand = false
unit.exercises?.forEach(exercise => {
exercise.isExpand = false
unit.exercises?.forEach((ex) => { ex.isExpand = false })
})
})
// 缓存详情数据
templateDetailMap.value[index] = res
} catch (err) {
ElMessage.error(`获取模板${index + 1}详情失败`)
console.error(err)
ElMessage.error('获取模板' + (index + 1) + '详情失败')
} finally {
// 取消加载中
templateLoading.value[index] = false
}
}
// 加载计划分类列表
const loadCategoryList = async () => {
const res = await trainingPlanApi.getPlanCategoryPage({ pageNo: 1, pageSize: 100 })
categoryList.value = res.list || []
const res = await trainingPlanApi.getPlanList()
categoryList.value = res || []
}
// 强制重新加载当前 index 的模板详情(解决重复选择不触发问题)
const reloadTemplateDetail = (index) => {
const id = SonFormData.value.templateIds[index]?.templateId
if (id) loadTemplateDetail(index, id)
}
// 取消
const handleCancel = () => {
dialogVisible.value = false
ElMessage.info('取消保存')
// ==================== 模板增删 ====================
const addTemplate = (index) => {
const newIds = [...SonFormData.value.templateIds]
newIds.splice(index + 1, 0, {
templateId: undefined,
sortOrder: SonFormData.value.templateIds.length + 1
})
SonFormData.value.templateIds = newIds
reindexDetailMap()
syncFrequency()
}
const removeTemplate = (index) => {
if (SonFormData.value.templateIds.length <= 1) return
SonFormData.value.templateIds = SonFormData.value.templateIds.filter((_, i) => i !== index)
reindexDetailMap()
syncFrequency()
}
// 提交
const reindexDetailMap = () => {
const oldMap = { ...templateDetailMap.value }
templateDetailMap.value = {}
templateLoading.value = {}
SonFormData.value.templateIds.forEach((item, i) => {
if (item.templateId && oldMap[i] !== undefined) {
templateDetailMap.value[i] = oldMap[i]
}
})
}
// ==================== 提交 ====================
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch {
return
}
loading.value = true
try {
if (isEdit.value) {
... ... @@ -398,122 +352,112 @@ const handleSubmit = async () => {
} else {
await trainingPlanApi.createTrainingPlan(SonFormData.value)
}
console.log('调用接口后isEdit的值', isEdit)
ElMessage.success(isEdit.value ? '编辑成功' : '创建成功')
dialogVisible.value = false
emit('submit')
} catch (err) {
ElMessage.error((isEdit.value ? '编辑' : '创建') + '失败:' + err.message)
ElMessage.error((isEdit.value ? '编辑' : '创建') + '失败')
} finally {
loading.value = false
}
}
const Addtemplate = (index) => {
const newTemplate = {
templateId: 1,
sortOrder: SonFormData.value.templateIds.length + 1
}
const newTemplateIds = [...SonFormData.value.templateIds]
newTemplateIds.splice(index + 1, 0, newTemplate)
SonFormData.value.templateIds = newTemplateIds
// 重新同步所有模板详情(不移除、不清空,只重新对应位置)
const oldMap = { ...templateDetailMap.value }
templateDetailMap.value = {}
templateLoading.value = {}
SonFormData.value.templateIds.forEach((item, i) => {
if (item.templateId && oldMap[i] !== undefined) {
templateDetailMap.value[i] = oldMap[i]
}
})
syncFrequency()
}
const deltemplate = (index) => {
if (SonFormData.value.templateIds.length <= 1) return
SonFormData.value.templateIds = SonFormData.value.templateIds.filter((_, i) => i !== index)
// 重新同步所有模板详情(不移除、不清空,只重新对应位置)
const oldMap = { ...templateDetailMap.value }
templateDetailMap.value = {}
templateLoading.value = {}
SonFormData.value.templateIds.forEach((item, i) => {
if (item.templateId && oldMap[i] !== undefined) {
templateDetailMap.value[i] = oldMap[i]
}
})
syncFrequency()
}
// ==================== 初始化 ====================
onMounted(() => {
loadTemplateList()
loadCategoryList()
})
</script>
<style scoped>
/* 模板详情容器 */
/* ==================== 工具类 ==================== */
.w-full {
width: 100%;
}
/* ==================== 模板卡片 ==================== */
.template-block {
margin-bottom: 16px;
padding: 16px 20px 4px;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.template-block-toolbar {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 8px;
}
/* ==================== 模板详情 ==================== */
.template-detail-container {
width: 100%;
padding: 12px;
background: #f9fafb;
background: var(--el-fill-color-lighter);
border-radius: 6px;
box-sizing: border-box;
}
/* 训练单元容器 */
/* ==================== 训练单元 ==================== */
.unit-container {
width: 100%;
margin-bottom: 8px;
background: #fff;
border: 1px solid #dcdfe6;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
/* 训练单元行(点击展开) */
.unit-row {
display: flex;
align-items: center;
padding: 12px 16px;
font-size: 14px;
cursor: pointer;
border-bottom: 1px solid #ebeef5;
align-items: center;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.unit-row:hover {
background: #f5f7fa;
background: var(--el-fill-color-light);
}
.expand-icon {
margin-right: 8px;
color: #909399;
color: var(--el-text-color-secondary);
transition: transform 0.3s;
}
.unit-name {
font-weight: 500;
color: #303133;
.expand-icon.is-expanded {
transform: rotate(90deg);
}
/* 组详情容器 */
.set-container {
padding: 12px;
border-top: 1px solid #ebeef5;
.unit-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.set-header {
/* ==================== 训练组详情 ==================== */
.set-header,
.set-row {
display: grid;
padding: 8px 12px;
font-weight: 500;
color: #606266;
text-align: center;
background: #f5f7fa;
grid-template-columns: repeat(5, 1fr);
}
.set-header {
font-weight: 500;
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
}
.set-row {
display: grid;
padding: 8px 12px;
color: #303133;
text-align: center;
border-top: 1px solid #ebeef5;
grid-template-columns: repeat(5, 1fr);
color: var(--el-text-color-primary);
border-top: 1px solid var(--el-border-color-lighter);
}
.exercise-container {
padding: 4px;
}
</style>
... ...
<template>
<div class="page-container">
<el-card class="search-bar">
<div class="search-item">
<label class="search-label">计划名称</label>
<el-input v-model="searchForm.name" placeholder="请输入计划名称" class="search-input" @keyup.enter="handleSearch"
clearable />
<div class="search-buttons">
<!-- 搜索栏 -->
<ContentWrap>
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="计划名称">
<el-input
v-model="queryParams.name"
placeholder="请输入计划名称"
clearable
class="!w-220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon>
<Search />
</el-icon>
搜索
<Icon icon="ep:search" class="mr-5px" />搜索
</el-button>
<el-button @click="handleReset">
<el-icon>
<Refresh />
</el-icon>
重置
<Icon icon="ep:refresh" class="mr-5px" />重置
</el-button>
<el-button type="primary" @click="openAddDialog">
<el-icon>
<Plus />
</el-icon>
新增
<Icon icon="ep:plus" class="mr-5px" />新增
</el-button>
</div>
</div>
</el-card>
<!-- 列表区域 -->
<el-card class="table-card">
<el-table v-loading="loading" :data="tableData" stripe style="width: 100%;">
<el-table-column prop="name" label="计划名称" align="center" sortable="custom" />
</el-form-item>
</el-form>
</ContentWrap>
<!-- 表格区域 -->
<ContentWrap>
<el-table v-loading="loading" :data="tableData" stripe>
<el-table-column prop="name" label="计划名称" align="center" />
<el-table-column label="计划周期" align="center" :formatter="formatDurationWeeks" />
<el-table-column label="场景" align="center" :formatter="formatTrainingScene" />
<el-table-column label="训练频率" align="center" :formatter="formatFrequency" />
<el-table-column label="难度" align="center" :formatter="formatDifficulty" />
<el-table-column label="适合人群" align="center" :formatter="formatTargetPeople" />
<el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" align="center" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="text" size="small" style=" margin-right: 8px;color: #409EFF;"
@click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color: #F56C6C;"
@click="handleDelete(scope.row.id)">删除</el-button>
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-wrapper" style="padding: 12px 16px;">
<el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize"
:total="pagination.total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</el-card>
<!-- 弹窗组件 -->
<trainingPlanDialog v-model:visible="dialogVisible" :form-data="formData" :title="dialogTitle" @submit="getList" />
<Pagination
:total="queryParams.total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 弹窗 -->
<TrainingPlanDialog
v-model:visible="dialogVisible"
:form-data="formData"
:title="dialogTitle"
@submit="getList"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import lostImg from '@/assets/imgs/lost.png' // 丢失图片占位图
import { dateFormatter } from '@/utils/formatTime'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { trainingPlanApi } from '@/api/store/training/trainingPlan'
import TrainingPlanDialog from './trainingPlanDialog.vue'
import { dateFormatter } from '@/utils/formatTime'
// ==================== 常量映射 ====================
const DIFFICULTY_MAP = { 1: '初阶', 2: '中阶', 3: '高阶' }
const TARGET_PEOPLE_MAP = { 1: '不限', 2: '青少年', 3: '成年人', 4: '中年人', 5: '运动损伤' }
const TRAINING_SCENE_MAP = { 1: '不限', 2: '健身房', 3: '仅哑铃', 4: '仅哑铃 + 杠铃' }
// ==================== 查询参数 ====================
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
total: 0,
name: ''
})
const loading = ref(false)
const isEdit = ref(true)
const tableData = ref([])
// ==================== 弹窗状态 ====================
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formData = ref({
templateIds: [{ templateId: undefined, sortOrder: 1 }]
}) // 传给弹窗的表单数据
// 分页参数
const pagination = reactive({
pageNo: 1,
pageSize: 10,
total: 0
})
// 文本映射=============
// 难度映射
const difficultyMap = {
1: '初阶',
2: '中阶',
3: '高阶'
}
// 适合人群映射
const targetPeopleMap = {
1: '不限',
2: '青少年',
3: '成年人',
4: '中年人',
5: '运动损伤'
}
// 训练场景映射
const trainingSceneMap = {
1: '不限',
2: '健身房',
3: '仅哑铃',
4: '仅哑铃 + 杠铃'
}
// 格式化难度
const formatDifficulty = (row) => {
return difficultyMap[row.difficultyLevel] || '未知'
}
// 格式化适合人群
const formatTargetPeople = (row) => {
return targetPeopleMap[row.targetPeople] || '未知'
}
// 格式化训练场景
const formatTrainingScene = (row) => {
return trainingSceneMap[row.trainingScene] || '未知'
}
// 格式化计划周期(在数字后加“周”)
const formatDurationWeeks = (row) => {
return row.durationWeeks ? `${row.durationWeeks}周` : '0周'
}
// 格式化训练频率(数字 + 练/周)
const formatFrequency = (row) => {
return row.frequencyPerWeek ? `${row.frequencyPerWeek}练/周` : '0练/周'
}
// 搜索表单
const searchForm = reactive({
name: ''
})
const tableData = ref([])
// ==================== 格式化函数 ====================
const formatDifficulty = (row) => DIFFICULTY_MAP[row.difficultyLevel] || '未知'
const formatTargetPeople = (row) => TARGET_PEOPLE_MAP[row.targetPeople] || '未知'
const formatTrainingScene = (row) => TRAINING_SCENE_MAP[row.trainingScene] || '未知'
const formatDurationWeeks = (row) => row.durationWeeks ? row.durationWeeks + '周' : '0周'
const formatFrequency = (row) => row.frequencyPerWeek ? row.frequencyPerWeek + '练/周' : '0练/周'
// 获取学员训练记录列表
// ==================== 数据加载 ====================
const getList = async () => {
loading.value = true
try {
// 搜索条件
const params = {
pageNo: pagination.pageNo + '',
pageSize: pagination.pageSize + '',
name: searchForm.name,
}
const res = await trainingPlanApi.getTrainingPlan(params)
const res = await trainingPlanApi.getTrainingPlan({
pageNo: queryParams.pageNo + '',
pageSize: queryParams.pageSize + '',
name: queryParams.name
})
tableData.value = res.list || []
pagination.total = res.total || 0
console.log('计划分页接口返回数据:', res)
queryParams.total = res.total || 0
} catch (err) {
console.error('❌ 请求失败:', err)
ElMessage.error('请求失败,请检查网络或接口')
} finally {
loading.value = false
}
}
// 搜索
// ==================== 搜索与重置 ====================
const handleSearch = () => {
ElMessage.success('执行搜索:' + searchForm.name)
queryParams.pageNo = 1
getList()
}
// 重置
const handleReset = () => {
pagination.pageNo = 1
pagination.pageSize = 10
searchForm.name = ''
ElMessage.success('已重置搜索条件')
Object.assign(queryParams, { pageNo: 1, pageSize: 10, total: 0, name: '' })
getList()
}
// 新增
// ==================== 新增 ====================
const openAddDialog = () => {
dialogTitle.value = '新增训练计划'
// 新增时彻底重置表单,清空编辑残留
// 彻底重置 formData,100%清空所有编辑残留,和子组件 SonFormData 初始结构完全一致
formData.value = {
id: undefined,
categoryId: 0,
... ... @@ -194,20 +149,14 @@ const openAddDialog = () => {
dialogVisible.value = true
}
// 编辑
// ==================== 编辑 ====================
const handleEdit = (row) => {
dialogTitle.value = '编辑训练计划'
// 保留完整表单结构,只覆盖后端有的字段
formData.value = {
// 先保留完整结构(不丢字段,这里没有新增时的数据,只是我创建formData的初始数据)
...formData.value,
// 再覆盖编辑数据,row里面是列表的字段,列表的字段和新增的字段的数量是不一样的。
...row
}
formData.value = { ...formData.value, ...row }
dialogVisible.value = true
}
// 删除
// ==================== 删除 ====================
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定要删除吗?', '提示', {
... ... @@ -215,77 +164,31 @@ const handleDelete = async (id) => {
cancelButtonText: '取消',
type: 'warning'
})
// 能走到这里,说明用户点了确定
await trainingPlanApi.deleteTrainingPlan(id)
ElMessage.success('删除成功')
getList()
} catch (err) {
// 关键:判断是不是用户点击了取消
if (err === 'cancel' || err?.message === 'cancel') {
ElMessage.info('已取消删除')
return // 直接退出,不报错
if (err !== 'cancel') {
ElMessage.error('删除失败')
}
// 只有真正的请求错误才走这里
console.error('❌ 请求失败:', err)
ElMessage.error('请求失败,请检查网络或接口')
} finally {
getList() // 刷新列表
}
}
// 处理分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
getList()
}
//
const handleCurrentChange = (val) => {
pagination.pageNo = val
getList()
}
// ==================== 初始化 ====================
onMounted(() => {
getList()
})
</script>
<style scoped>
.search-bar {
margin-bottom: 20px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
.search-item {
display: flex;
align-items: center;
gap: 10px;
}
.search-label {
/* width: 160px; */
/* font-weight: 500; */
}
.search-input {
width: 260px;
}
.search-buttons {
display: flex;
gap: 10px;
margin-left: 10px;
}
.table-card {
margin-top: 5px;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
.page-container {
padding: 20px;
min-height: 100vh;
background-color: #f5f7fa;
box-sizing: border-box;
}
/* 分页居右 */
.pagination-wrapper {
display: flex;
padding: 12px 16px;
justify-content: flex-end;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
}
</style>
... ...