Authored by qxm

代码替换

... ... @@ -2,25 +2,34 @@
<template>
<div class="page-container">
<!-- 搜索与新增区域 -->
<ContentWrap>
<ContentWrap class="plan-search-bar">
<div class="search-bar">
<el-input v-model="searchForm.name" placeholder="肌肉名称" clearable @keyup.enter="handleSearch" />
<el-button @click="handleSearch">
<div class="search-item">
<label class="search-label">肌肉名称</label>
<el-input v-model="searchForm.name" placeholder="肌肉名称" clearable @keyup.enter="handleSearch"
class="search-input" />
<div class="search-buttons">
<el-button type="primary" @click="handleSearch">
<el-icon>
<Search />
</el-icon>搜索
</el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon>
<Refresh />
</el-icon>重置
</el-icon>
重置
</el-button>
<el-button @click="handleAdd">
<el-button type="primary" @click="handleAdd">
<el-icon>
<Plus />
</el-icon>新增
</el-icon>
新增
</el-button>
</div>
</div>
</div>
</ContentWrap>
<ContentWrap>
... ... @@ -30,8 +39,6 @@
<el-table-column prop="id" label="ID" sortable align="center" />
<!-- 肌肉名称列(对应接口的name字段) -->
<el-table-column prop="name" label="肌肉名称" align="center" />
<!-- 关联大类ID列 -->
<!-- <el-table-column prop="categoryId" label="关联大类ID" align="center" /> -->
<el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" align="center" min-width="100" :formatter="dateFormatter" />
<!-- 操作列 -->
... ... @@ -59,16 +66,13 @@
</div>
</ContentWrap>
<ContentWrap>
<!-- <ContentWrap> -->
<!-- 编辑弹窗 -->
<el-dialog v-model="editDialogVisible" title="编辑肌肉信息" @close="resetEditForm">
<el-form ref="editFormRef" :rules="editRules" :model="editForm" label-width="100px">
<el-form-item label="肌肉名称" prop="name">
<el-input v-model="editForm.name" placeholder="请输入肌肉名称" />
</el-form-item>
<!-- <el-form-item label="关联大类ID" prop="categoryId">
<el-input v-model.number="editForm.categoryId" type="number" placeholder="请输入关联大类ID" />
</el-form-item> -->
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
... ... @@ -82,16 +86,13 @@
<el-form-item label="肌肉名称" prop="name">
<el-input v-model="addForm.name" placeholder="请输入肌肉名称" />
</el-form-item>
<!-- <el-form-item label="关联大类ID" prop="categoryId">
<el-input v-model.number="addForm.categoryId" type="number" placeholder="请输入关联大类ID" />
</el-form-item> -->
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAddForm">确定</el-button>
</template>
</el-dialog>
</ContentWrap>
<!-- </ContentWrap> -->
</div>
</template>
... ... @@ -100,7 +101,7 @@ import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox, ElEmpty, ElForm, ElFormItem, ElInput, ElDialog } from 'element-plus'
// 导入你提供的接口文件
import { MusclesApi, type MusclesVO } from '@/api/store/training/muscle'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { dateFormatter } from '@/utils/formatTime'
// 表格数据(对应接口返回的列表)
... ... @@ -114,9 +115,9 @@ const searchForm = reactive({
// 分页参数
const pagination = reactive({
pageNo: 1, // 替换原currentPage
pageSize: 10, // 替换原pageSize
total: 0 // 替换原total
pageNo: 1,
pageSize: 10,
total: 0
})
// ---------------------- 编辑功能相关变量 ----------------------
... ... @@ -135,10 +136,6 @@ const editRules = reactive({
name: [
{ required: true, message: '请输入肌肉名称', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
categoryId: [
{ required: true, message: '请输入关联大类ID', trigger: 'blur' },
{ type: 'number', min: 1, message: '关联大类ID必须为正整数', trigger: 'blur' }
]
})
... ... @@ -157,10 +154,6 @@ const addRules = reactive({
name: [
{ required: true, message: '请输入肌肉名称', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
categoryId: [
{ required: true, message: '请输入关联大类ID', trigger: 'blur' },
{ type: 'number', min: 1, message: '关联大类ID必须为正整数', trigger: 'blur' }
]
})
... ... @@ -168,25 +161,23 @@ const addRules = reactive({
const getMusclesList = async () => {
try {
loading.value = true
// 构造请求参数:参数转字符串传递
// 构造请求参数
const params = {
pageNo: pagination.pageNo + '', // 转字符串
pageSize: pagination.pageSize + '', // 转字符串
name: searchForm.name // 搜索的肌肉名称
pageNo: pagination.pageNo + '',
pageSize: pagination.pageSize + '',
name: searchForm.name
}
// 调用接口获取数据
const res = await MusclesApi.getMusclesPage(params)
// 兼容不同的接口返回格式(优先取res.data,没有则取res本身)
const result = res.data || res
tableData.value = result.list || []
pagination.total = result.total || 0
console.log('肌肉分页接口返回数据:', res)
// 分页边界处理:如果当前页大于总页数,自动切到最后一页
// 分页边界处理
const totalPages = Math.ceil(pagination.total / pagination.pageSize)
if (pagination.pageNo > totalPages && totalPages > 0) {
pagination.pageNo = totalPages
getMusclesList() // 重新请求最后一页数据
getMusclesList()
}
} catch (error) {
ElMessage.error('获取肌肉列表失败,请重试')
... ... @@ -212,13 +203,13 @@ watch([() => pagination.total, () => pagination.pageSize], () => {
})
// 4. 事件处理函数
// 搜索:保持原有逻辑,仅调整分页参数名
// 搜索
const handleSearch = () => {
pagination.pageNo = 1 // 搜索时重置页码为1
pagination.pageNo = 1
getMusclesList()
}
// 重置:保持原有逻辑,仅调整分页参数名
// 重置
const handleReset = () => {
searchForm.name = ''
pagination.pageNo = 1
... ... @@ -237,7 +228,6 @@ const handleAdd = () => {
const resetAddForm = () => {
addForm.name = ''
addForm.categoryId = 0
// 清空表单验证状态
if (addFormRef.value) {
addFormRef.value.clearValidate()
}
... ... @@ -247,20 +237,16 @@ const resetAddForm = () => {
const submitAddForm = async () => {
if (!addFormRef.value) return
try {
// 表单验证
await addFormRef.value.validate()
// 调用新增接口(无需传id,由后端生成)
await MusclesApi.createMuscles({
...addForm,
id: 0 // 占位,后端会自动生成
id: 0
})
ElMessage.success('新增成功!')
// 关闭弹窗
addDialogVisible.value = false
// 重新加载列表数据
getMusclesList()
} catch (error) {
if (error !== 'cancel') { // 排除表单验证取消的情况
if (error !== 'cancel') {
ElMessage.error('新增失败,请重试')
console.error('新增失败:', error)
}
... ... @@ -271,14 +257,10 @@ const submitAddForm = async () => {
// 打开编辑弹窗并回显数据
const handleEdit = async (row: MusclesVO) => {
try {
// 清空编辑表单
resetEditForm()
// 打开弹窗
editDialogVisible.value = true
// 调用接口获取详情
const res = await MusclesApi.getMuscles(row.id)
const detail = res.data || res
// 回显数据到编辑表单
editForm.id = detail.id
editForm.name = detail.name
editForm.categoryId = detail.categoryId
... ... @@ -293,7 +275,6 @@ const resetEditForm = () => {
editForm.id = 0
editForm.name = ''
editForm.categoryId = 0
// 清空表单验证状态
if (editFormRef.value) {
editFormRef.value.clearValidate()
}
... ... @@ -303,17 +284,13 @@ const resetEditForm = () => {
const submitEditForm = async () => {
if (!editFormRef.value) return
try {
// 表单验证
await editFormRef.value.validate()
// 调用更新接口
await MusclesApi.updateMuscles({ ...editForm })
ElMessage.success('编辑成功!')
// 关闭弹窗
editDialogVisible.value = false
// 重新加载列表数据
getMusclesList()
} catch (error) {
if (error !== 'cancel') { // 排除表单验证取消的情况
if (error !== 'cancel') {
ElMessage.error('编辑失败,请重试')
console.error('编辑失败:', error)
}
... ... @@ -323,7 +300,6 @@ const submitEditForm = async () => {
// 删除
const handleDelete = async (row: MusclesVO) => {
try {
// 确认删除
await ElMessageBox.confirm(
'此操作将永久删除该肌肉信息,是否继续?',
'温馨提示',
... ... @@ -333,13 +309,11 @@ const handleDelete = async (row: MusclesVO) => {
type: 'warning'
}
)
// 调用删除接口
await MusclesApi.deleteMuscles(row.id)
ElMessage.success('删除成功!')
// 重新获取列表
getMusclesList()
} catch (error) {
if (error !== 'cancel') { // 排除用户取消的情况
if (error !== 'cancel') {
ElMessage.error('删除失败,请重试')
console.error('删除失败:', error)
}
... ... @@ -361,12 +335,44 @@ const handleCurrentChange = (val: number) => {
</script>
<style scoped>
.page-container {
/* min-height: 100vh;
background: #fff; */
.plan-search-bar {
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;
flex-wrap: wrap;
}
.search-label {
white-space: nowrap;
}
/* 输入框自适应宽度 */
.search-input {
max-width: 300px;
min-width: 200px;
flex: 1;
}
/* 按钮组靠右,不挤压 */
.search-buttons {
display: flex;
gap: 10px;
/* margin-left: auto; */
flex-wrap: wrap;
}
:deep(.plan-search-bar) {
padding: 0;
}
/* 新增分页样式 */
.pagination-wrapper {
display: flex;
padding: 12px 16px 0;
... ...
<!-- 部位分类页面,在接口文档没有找到部位分离的接口,用了动作分类的接口 -->
<template>
<div class="simple-category-page">
<ContentWrap>
<ContentWrap class="plan-search-bar">
<!-- 搜索栏 -->
<div class="search-bar" style="padding: 15px; margin-bottom: 10px; background: #fff; border-radius: 6px;">
<div style="display: flex; align-items: center; gap: 10px;">
... ... @@ -12,12 +12,14 @@
<el-icon>
<Search />
</el-icon>
搜索</el-button>
搜索
</el-button>
<el-button @click="resetQuery">
<el-icon>
<Refresh />
</el-icon>
重置</el-button>
重置
</el-button>
<!-- 新增按钮 -->
<el-button class="add-btn" @click="openAddForm">
<el-icon>
... ... @@ -69,7 +71,8 @@
</div>
</div>
</ContentWrap>
<ContentWrap>
<!-- <ContentWrap> -->
<!-- 新增/编辑弹窗(复用) -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">
... ... @@ -91,7 +94,7 @@
</el-button>
</template>
</el-dialog>
</ContentWrap>
<!-- </ContentWrap> -->
</div>
</template>
... ... @@ -285,7 +288,13 @@ onMounted(() => {
<style scoped>
.simple-category-page {
height: 100%;
background: #f5f5f5;
/* background: #f5f5f5; */
}
.plan-search-bar {
margin-bottom: 20px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
.add-btn {
... ...
... ... @@ -2,7 +2,7 @@
<div class="page-container">
<!-- 搜索栏 -->
<ContentWrap class="plan-search-bar">
<div class="search-bar-wrapper">
<!-- <div class="search-bar-wrapper"> -->
<div class="search-item">
<label class="search-label">计划大类名称</label>
<el-input v-model="searchForm.name" placeholder="请输入" class="search-input" clearable
... ... @@ -28,8 +28,10 @@
</el-button>
</div>
</div>
</div>
<!-- </div> -->
</ContentWrap>
<ContentWrap>
<!-- 表格 -->
<el-table v-loading="loading" :data="tableData" stripe style="width: 100%;">
... ... @@ -251,7 +253,6 @@ onMounted(() => {
/* 3. 标签样式(和计划页一致) */
.search-label {
white-space: nowrap;
}
/* 4. 输入框宽度(和计划页一致) */
... ...
... ... @@ -32,27 +32,27 @@
</el-form-item>
<el-form-item label="主要训练部位:" prop="primaryMuscles" label-suffix="*">
<el-select v-model="formDataModel.primaryMuscles" multiple placeholder="请输入主要训练部位" style="width: 100%">
<el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in primaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="次要训练部位:" prop="secondaryMuscles" label-suffix="*">
<el-select v-model="formDataModel.secondaryMuscles" multiple placeholder="请输入次要训练部位" style="width: 100%">
<el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in secondaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<!-- 封面图 -->
<el-form-item label="锻炼部位图" prop="urlImage" label-suffix="*" class="form-item-margin upload-img-item">
<!-- 锻炼部位图 -->
<el-form-item label="锻炼部位图" prop="urlImage" class="form-item-margin upload-img-item">
<UploadImg v-model="formDataModel.urlImage" />
</el-form-item>
<el-form-item label="3D动画地址" prop="url3dAnimation" label-suffix="*">
<UploadVideo v-model="formDataModel.url3dAnimation" />
<el-form-item label="3D动画地址" prop="url3dAnimation" label-suffix="*" class="upload-img-item upload-3d-item">
<UploadImg v-model="formDataModel.url3dAnimation" />
</el-form-item>
<el-form-item label="真人演示地址" prop="urlRealPerson">
<el-form-item label="真人演示地址" prop="urlRealPerson" label-suffix="*">
<UploadVideo v-model="formDataModel.urlRealPerson" />
</el-form-item>
<el-form-item label="教程地址" prop="urlTutorial">
<el-form-item label="教程地址" prop="urlTutorial" label-suffix="*">
<UploadVideo v-model="formDataModel.urlTutorial" />
</el-form-item>
<el-form-item label="动作步骤描述:" prop="stepDescription" label-suffix="*" class="desc-item">
... ... @@ -68,17 +68,20 @@
<script setup>
import { ref, reactive, watch, onMounted, nextTick } from 'vue'
import { ref, reactive, watch, onMounted, nextTick, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { MotionCategoryApi } from '@/api/store/training/partCategory'
import { MusclesApi } from '@/api/store/training/muscle'
import { EquipmentsApi } from '@/api/store/training/tool'
import { Editor } from '@/components/Editor'
import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue'
import { ExercisesApi } from '@/api/store/training/pose'
// 1. 自定义 v-model:弹窗显隐(对应父组件 v-model:visible)
const dialogVisible = defineModel('visible', { type: Boolean, default: false })
const defaultImg = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png'
// 2. 自定义 v-model:表单数据(对应父组件 v-model:formData)
const formDataModel = defineModel('formData', {
type: Object, // 新增这一行
... ... @@ -113,10 +116,43 @@ watch(dialogVisible, async (newVal) => {
loadToolList(),
loadMuscleList()
])
const form = formDataModel.value
// 调试打印
console.log('解析前-原始数据:', form)
console.log('解析前-primaryMuscles:', form.primaryMuscles, typeof form.primaryMuscles)
console.log('解析前-secondaryMuscles:', form.secondaryMuscles, typeof form.secondaryMuscles)
if (typeof form.primaryMuscles === 'string') {
try {
// 解析字符串 "[17, 16]" → 数组 [17, 16]
formDataModel.value.primaryMuscles = JSON.parse(form.primaryMuscles)
} catch (err) {
// 如果解析失败(比如空字符串、非法格式),就默认设为空数组
console.warn('primaryMuscles 解析失败,已重置为空数组', err)
formDataModel.value.primaryMuscles = []
}
}
// 处理 secondaryMuscles(和上面逻辑一样)
if (typeof form.secondaryMuscles === 'string') {
try {
formDataModel.value.secondaryMuscles = JSON.parse(form.secondaryMuscles)
} catch (err) {
console.warn('secondaryMuscles 解析失败,已重置为空数组', err)
formDataModel.value.secondaryMuscles = []
}
}
// -------------------------------------------------------------
// 解析完成后,再打印一次,确认变成了数组
console.log('解析后-primaryMuscles:', formDataModel.value.primaryMuscles, typeof formDataModel.value.primaryMuscles)
console.log('解析后-secondaryMuscles:', formDataModel.value.secondaryMuscles, typeof formDataModel.value.secondaryMuscles)
// 列表加载完后,强制更新表单,触发el-select回显
nextTick(() => {
if (formRef.value) {
formRef.value.clearValidate()
}
})
}
... ... @@ -140,13 +176,26 @@ const formRules = reactive({
categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }],
equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }],
exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }],
primaryMuscles: [{ required: true, message: '请输入主要训练部位', trigger: 'blur' }],
secondaryMuscles: [{ required: true, message: '请输入次要训练部位', trigger: 'blur' }],
urlImage: [{ required: true, message: '请上传图片', trigger: 'blur' }],
primaryMuscles: [{ required: true, message: '请输入主要训练部位', trigger: 'change' }],
secondaryMuscles: [{ required: true, message: '请输入次要训练部位', trigger: 'change' }],
urlImage: [{ required: false, message: '请上传图片', trigger: 'blur' }],
urlRealPerson: [{ required: true, message: '请上传真人演示地址', trigger: 'blur' }],
urlTutorial: [{ required: true, message: '请上传教程地址', trigger: 'blur' }],
url3dAnimation: [{ required: true, message: '请上传3D动图', trigger: 'blur' }],
stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }],
})
// 次要部位选项 = 全部肌肉 - 已选主要部位
const secondaryMuscleOptions = computed(() => {
const primaryIds = formDataModel.value.primaryMuscles || []
return muscleList.value.filter(item => !primaryIds.includes(item.id))
})
// 主要部位选项 = 全部肌肉 - 已选次要部位
const primaryMuscleOptions = computed(() => {
const secondaryIds = formDataModel.value.secondaryMuscles || []
return muscleList.value.filter(item => !secondaryIds.includes(item.id))
})
// 取消按钮:直接关闭弹窗(自动同步给父组件)
... ... @@ -159,27 +208,65 @@ const handleCancel = () => {
const handleSubmit = async () => {
if (!formRef.value) return
try {
console.log('123456');
await formRef.value.validate()
console.log('123456');
formLoading.value = true
// 👇 加这行!看原始数据是不是嵌套数组
console.log('原始primaryMuscles:', formDataModel.value.primaryMuscles)
console.log('原始secondaryMuscles:', formDataModel.value.secondaryMuscles)
// 👇 核心处理:将数组转为逗号分隔的字符串
const form = formDataModel.value
const submitData = {
...formDataModel.value,
// 如果数组为空,传空字符串 "" 否则会变成 "undefined"
primaryMuscles: formDataModel.value.primaryMuscles || [],
secondaryMuscles: formDataModel.value.secondaryMuscles || [],
urlImage: form.urlImage || defaultImg,
// primaryMuscles: formDataModel.value.primaryMuscles || [],
// secondaryMuscles: formDataModel.value.secondaryMuscles || [],
primaryMuscles: JSON.stringify(formDataModel.value.primaryMuscles || []),
secondaryMuscles: JSON.stringify(formDataModel.value.secondaryMuscles || []),
}
console.log('------------------');
if (submitData.id) {
await ExercisesApi.updateExercises(submitData) // 编辑:调用更新接口
console.log('动作编辑接口数据', submitData)
ElMessage.success('动作编辑成功!')
} else {
console.log('动作新增接口数据', submitData)
await ExercisesApi.addExercises(submitData) // 新增:调用创建接口
ElMessage.success('动作新增成功!')
}
// 触发提交事件,数据已通过 v-model 同步,直接传即可
emit('submit', submitData)
emit('submit')
} catch (error) {
ElMessage.error('表单校验失败,请检查必填项')
} finally {
formLoading.value = false
// resetForm()
}
}
// const handleFormSubmit = async (submitData) => {
// try {
// // ✅ 直接传数组,axios 自动转成 JSON 字符串
// const formatData = {
// ...submitData,
// primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),
// secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])
// }
// console.log('提交的数据', formatData);
// // 关闭弹窗
// handleDialogClose()
// // 重新查询数据
// handleQuery()
// } catch (err) {
// console.error('提交失败:', err)
// ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')
// }
// }
// 重置表单(只操作本地 model,自动同步父组件)
const resetForm = () => {
Object.assign(formDataModel.value, {
... ... @@ -278,13 +365,22 @@ defineExpose({ resetForm })
:deep(.el-form-item.upload-img-item .el-form-item__label) {
/* 上传组件高度高,让 label 顶部对齐 */
line-height: 120px !important;
}
/* .upload-img-item {
margin-bottom: 20rpx;
} */
/* 单独给封面图增加上下间距 */
.form-item-margin {
margin: 30px 5px !important;
}
.upload-3d-item {
margin-top: 20px !important;
}
.upload-box {
display: flex;
width: 120px;
... ...
... ... @@ -32,14 +32,15 @@
<!-- 表格区域 -->
<ContentWrap>
<el-table :data="tableData" stripe style="width: 100%" v-loading="loading" empty-text="暂无动作数据"
:header-cell-style="{ background: '#f5f7fa', textAlign: 'center' }" :cell-style="{ textAlign: 'center' }">
:header-cell-style="{ background: '#f5f7fa', textAlign: 'center' }"
:cell-style="{ textAlign: 'center', justifyContent: 'center' }">
<el-table-column prop="name" label="动作名称" min-width="120" />
<!-- 封面 -->
<el-table-column prop="urlImage" label="封面" align="center" min-width="120">
<el-table-column prop="url3dAnimation" label="封面" align="center" min-width="120">
<template #default="scope">
<div class="cover-wrapper">
<!-- 优先显示行数据封面,无则显示占位图 -->
<img class="cover-img" :src="scope.row.urlImage || lostImg" alt="模板封面" />
<img class="cover-img" :src="scope.row.url3dAnimation || lostImg" alt="模板封面" />
</div>
</template>
</el-table-column>
... ... @@ -59,10 +60,11 @@
</el-table>
<!-- 分页组件 -->
<div class="pagination-wrapper">
<span class="goto-text">共 {{ total }} 条</span>
<el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]" :total="total" background layout="prev, pager, next, jumper, ->, sizes, total"
<div class="pagination-wrapper" style="padding: 12px 16px;">
<!-- <span class="goto-text">共 {{ total }} 条</span> -->
<!-- 共 {{ total }} 条 -->
<el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" :total="total"
:page-sizes="[10, 20, 50]" background layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</ContentWrap>
... ... @@ -192,40 +194,46 @@ const handleDelete = async (row: ExercisesVO) => {
}
// 处理表单提交(新增/编辑)
const handleFormSubmit = async (submitData: ExercisesSaveReqVO) => {
try {
// ✅ 直接传数组,axios 自动转成 JSON 字符串
const formatData = {
...submitData,
primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),
secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])
}
if (submitData.id) {
await ExercisesApi.updateExercises(formatData) // 编辑:调用更新接口
console.log('动作编辑接口数据', submitData)
ElMessage.success('动作编辑成功!')
} else {
console.log('动作新增接口数据', submitData)
await ExercisesApi.addExercises(formatData) // 新增:调用创建接口
ElMessage.success('动作新增成功!')
}
// 关闭弹窗
handleDialogClose()
// 重新查询数据
// const handleFormSubmit = async (submitData: ExercisesSaveReqVO) => {
// try {
// // ✅ 直接传数组,axios 自动转成 JSON 字符串
// const formatData = {
// ...submitData,
// primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),
// secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])
// }
// console.log('提交的数据', formatData);
// if (submitData.id) {
// await ExercisesApi.updateExercises(formatData) // 编辑:调用更新接口
// console.log('动作编辑接口数据', submitData)
// ElMessage.success('动作编辑成功!')
// } else {
// console.log('动作新增接口数据', submitData)
// await ExercisesApi.addExercises(formatData) // 新增:调用创建接口
// ElMessage.success('动作新增成功!')
// }
// // 关闭弹窗
// handleDialogClose()
// // 重新查询数据
// handleQuery()
// } catch (err) {
// console.error('提交失败:', err)
// ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')
// }
// }
const handleFormSubmit = () => {
handleQuery()
} catch (err) {
console.error('提交失败:', err)
ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')
}
}
// 关闭弹窗
const handleDialogClose = () => {
dialogVisible.value = false
resetFormData()
}
// 关闭弹窗
// const handleDialogClose = () => {
// dialogVisible.value = false
// resetFormData()
// }
// 重置表单数据
const resetFormData = () => {
Object.assign(formData, {
... ...
<template>
<div class="page-container">
<el-card class="search-bar">
<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"
... ... @@ -26,10 +26,11 @@
</el-button> -->
</div>
</div>
</el-card>
</ContentWrap>
<!-- 列表区域 -->
<el-card class="table-container">
<!-- <div class="table-container"> -->
<ContentWrap>
<el-table :data="tableData" v-loading="loading" empty-text="暂无数据" border>
<el-table-column prop="username" label="学员" align="center" />
<el-table-column prop="name" label="训练计划名称" align="center" />
... ... @@ -54,7 +55,8 @@
: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>
</el-card>
</ContentWrap>
<!-- </div> -->
<!-- 详情弹窗 -->
<StuTrainingDetailDialog v-model:visible="detailVisible" :id="currentId" />
</div>
... ... @@ -184,9 +186,9 @@ onMounted(() => {
})
</script>
<style scoped>
.page-container {
/* padding: 20px; */
}
/* .page-container {
} */
/* 分页居右 */
.pagination-wrapper {
... ... @@ -196,13 +198,16 @@ onMounted(() => {
}
.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 {
... ...
<template>
<div class="supersets-page">
<ContentWrap>
<ContentWrap class="plan-search-bar">
<!-- 1. 搜索栏区域 -->
<div class="search-bar">
<div class="search-item">
... ... @@ -21,7 +21,7 @@
</el-icon>
重置
</el-button>
<el-button @click="openAddDialog">
<el-button type="primary" @click="openAddDialog">
<el-icon>
<Plus />
</el-icon>
... ... @@ -401,7 +401,12 @@ onMounted(async () => {
box-sizing: border-box;
}
.search-bar {
.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;
... ... @@ -410,13 +415,13 @@ onMounted(async () => {
border-radius: 4px;
gap: 10px;
flex-wrap: nowrap;
}
} */
.search-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 15px 20px;
}
.search-label {
... ...
... ... @@ -2,16 +2,14 @@
<template>
<div class="page-container">
<!-- 搜索框区域 -->
<div class="equipments-page">
<ContentWrap>
<!-- <div class="equipments-page"> -->
<ContentWrap class="plan-search-bar">
<!-- 搜索栏 -->
<div class="search-bar" style="padding: 16px; margin-bottom: 16px; background: #fff; border-radius: 4px;">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="文件夹名称:">
<div class="search-bar">
<label class="search-label">文件夹名称</label>
<el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"
@keyup.enter="handleSearch" />
</el-form-item>
<el-form-item>
<div class="search-buttons">
<el-button type="primary" @click="handleSearch" size="default">
<el-icon>
<Search />
... ... @@ -24,14 +22,13 @@
</el-icon>
重置
</el-button>
<el-button class="add-btn" @click="handleAdd" size="default">
<el-button type="primary" @click="handleAdd" size="default">
<el-icon>
<Plus />
</el-icon>
新增
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</ContentWrap>
<!-- 表格区域 -->
... ... @@ -57,14 +54,19 @@
</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>
</ContentWrap>
<!-- 编辑/新增弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" 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-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit style="width: 100%" />
</el-form-item>
<el-form-item label="模板" prop="templateIds">
<el-select v-model="formData.templateIds" placeholder="请选择模板" multiple clearable style="width: 100%"
... ... @@ -73,8 +75,7 @@
</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%" />
<UploadImg v-model="formData.urlCover" placeholder="封面" maxlength="50" show-word-limit style="width: 100%" />
</el-form-item>
<el-form-item label="模板大类介绍" prop="description" label-suffix="*">
<el-input v-model="formData.description" placeholder="请输入模板大类介绍" maxlength="50" show-word-limit
... ... @@ -89,12 +90,8 @@
<!-- 分页组件 -->
<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>
</div>
<!-- </div> -->
</div>
</template>
... ... @@ -306,6 +303,31 @@ onMounted(() => {
})
</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;
... ...
... ... @@ -41,6 +41,7 @@
<!-- 自重加重/减重 -->
<th v-if="type === 4 || type === 5" width="120">重量</th>
<th v-if="type === 4 || type === 5" width="120">次数</th>
<th v-if="type === 4 || type === 5" width="140">休息时间(秒)</th>
<th v-if="type === 6" width="140">组数</th>
... ... @@ -134,6 +135,10 @@
@change="val => handleFieldChange(setIndex, 'weight', val)" />
</td>
<td v-if="type === 4 || type === 5">
<el-input-number :model-value="set.reps" :min="0" style="width: 80%"
@change="val => handleFieldChange(setIndex, 'reps', val)" />
</td>
<td v-if="type === 4 || type === 5">
<el-input-number :model-value="set.restTime" :min="0" style="width: 80%"
@change="val => handleFieldChange(setIndex, 'restTime', val)" />
</td>
... ... @@ -217,7 +222,8 @@ watch(() => props.type, () => initAll(), { immediate: true })
// 新增
const addSet = () => {
const newSet = { weight: 0, reps: 0, duration: 0, distance: 0, restTime: 0 }
const newSetIndex = props.sets.length + 1;
const newSet = { weight: 0, reps: 0, duration: 0, distance: 0, restTime: 0, setIndex: newSetIndex }
const newSets = [...props.sets, newSet]
emit('update:sets', newSets)
}
... ... @@ -226,11 +232,17 @@ const addSet = () => {
const delSet = (index) => {
const newSets = [...props.sets]
newSets.splice(index, 1)
emit('update:sets', newSets)
const reorderedSets = newSets.map((item, i) => ({
...item,
setIndex: i + 1
}))
emit('update:sets', reorderedSets)
}
</script>
<style scoped>
/* 表格 */
.sets-table-wrapper {
margin: 16px 0;
}
... ... @@ -244,4 +256,42 @@ const delSet = (index) => {
width: 100%;
border-collapse: collapse;
}
/* 表格容器 */
.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;
}
.sets-table th,
.sets-table td {
padding: 12px 8px;
text-align: center;
vertical-align: middle;
border: 1px solid #ddd;
}
.sets-table th {
font-weight: 500;
background: #fafafa;
}
.set-row:hover {
background: #f9f9f9;
}
</style>
\ No newline at end of file
... ...
... ... @@ -36,7 +36,8 @@
</el-form-item>
<!-- 这里实现选择超级组或者动作组名称 -->
<el-form-item label-suffix="*" :label="unit.unitType === 1 ? '动作组名称' : '超级组名称'">
<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">
<!-- 需要接入动作接口 -->
... ... @@ -47,37 +48,32 @@
<el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" />
</template>
</el-select>
</el-form-item>
<!-- 动作组头部 动作组名称+删除和增加动作组按钮-->
<div class="unit-header">
<el-form-item>
<el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1">
<template #icon>
<Minus />
</template>
删除单元
</el-button>
</el-form-item>
<el-form-item>
<el-button @click="AddUnit()">
<template #icon>
<Plus />
</template>
新增单元
</el-button>
</el-form-item>
</div>
</div>
</el-form-item>
<!-- 排序 -->
<el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`">
<el-input v-model="unit.sortOrder" min="1" max="99" />
</el-form-item>
<!-- 循环:动作组 1 个、超级组 N 个,都会渲染 -->
<template v-for="(item, idx) in unit.exercises" :key="idx">
<div style="margin: 16px 0;">
<div>动作名称:{{ getExerciseName(item.exerciseId) }}</div>
<div class="action-name">动作名称:{{ getExerciseName(item.exerciseId) }}</div>
<ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" />
</div>
</template>
... ... @@ -96,15 +92,15 @@
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, nextTick } from 'vue'
import lostImg from '@/assets/imgs/lost.png'
import { MusclesApi } from '@/api/store/training/muscle'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Minus, Plus } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import './TemplateAdd.css'
// import './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>()
... ... @@ -114,7 +110,7 @@ const router = useRouter()
const route = useRoute()
const exercisesList = ref<any[]>([])
const superGroupList = ref<any[]>([])
import { SupersetsApi } from '@/api/store/training/Supersets'
// 表单数据
const formData = reactive({
... ... @@ -451,7 +447,7 @@ const handleSubmit = async () => {
loading.value = true
try {
// 👇 按接口文档转换提交数据
// 按接口文档转换提交数据
const submitData = {
templateId: formData.id, // 关键:前端id → 后端templateId
groupId: formData.groupId,
... ... @@ -543,3 +539,122 @@ watch(
{ immediate: true }
)
</script>
<style scoped>
:deep(.el-form-item__label) {
float: none !important;
width: 68px !important;
padding: 0 !important;
margin-right: 20px !important;
line-height: 32px;
text-align: right;
white-space: nowrap;
}
/* 让表单内容区域横向排列 */
.row-form-item :deep(.el-form-item__content) {
display: flex;
align-items: center;
width: 100%;
}
/* 下拉框 + 按钮 同行容器 */
.row-wrap {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
}
/* 下拉框占剩余宽度 */
.sel {
flex: 1;
}
/* 按钮容器:不被压缩 */
.unit-header {
display: flex;
gap: 10px;
flex-shrink: 0;
}
/* 下面是你原来保留的样式,不动 */
/* 动作组卡片 */
.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;
}
.sets-table th {
padding: 10px 0;
font-weight: 500;
background: #fafafa;
}
.sets-table td {
padding: 8px 0;
}
.set-row:hover {
background: #f9f9f9;
}
.action-name {
color: #666;
}
/* 按钮居中容器 */
.btn-center-wrapper {
display: flex;
width: 100%;
padding-top: 15px;
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>
<div class="page-container">
<!-- 顶部搜索与操作栏 -->
<ContentWrap>
<div class="search-bar">
<ContentWrap class="plan-search-bar">
<div class="search-item">
<div class="search-left">
<label class="search-label">模板名称: </label>
<el-input class="search-input" placeholder="请输入" v-model="searchForm.name" @keyup.enter="handleSearch" />
... ... @@ -29,6 +29,7 @@
</div>
</div>
</ContentWrap>
<ContentWrap>
<!-- 数据列表 -->
<div class="table-container">
... ... @@ -203,7 +204,7 @@ const load = async () => {
// 初始化加载列表
onMounted(async () => {
fetchList()
load()
// load()
})
</script>
... ... @@ -212,6 +213,19 @@ onMounted(async () => {
padding: 20px;
}
.plan-search-bar {
margin-bottom: 20px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
/* 2. 搜索项flex布局(核心,让标签、输入框、按钮横向对齐) */
.search-item {
display: flex;
align-items: center;
gap: 10px;
padding: 15px 20px;
}
.search-bar {
display: flex;
margin-right: 20px;
... ... @@ -222,7 +236,7 @@ onMounted(async () => {
}
.search-label {
font-weight: 500;
white-space: nowrap;
}
.search-input {
... ... @@ -232,7 +246,8 @@ onMounted(async () => {
.search-buttons {
display: flex;
gap: 5px;
gap: 10px;
margin-left: 10px;
}
.table-container {
... ... @@ -242,8 +257,9 @@ onMounted(async () => {
}
.pagination-wrapper {
margin-top: 20px;
text-align: right;
display: flex;
padding: 12px 16px;
justify-content: flex-end;
}
.cover-wrapper {
... ...
<template>
<!-- 用具分类页面 ,在接口文档没有找到用具分类的接口,用了器械的接口 -->
<ContentWrap>
<ContentWrap class="plan-search-bar">
<!-- 搜索栏 -->
<div class="search-bar">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="器械名称:">
<!-- <div class="search-bar"> -->
<div class="search-item">
<label class="search-label">肌肉名称</label>
<el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"
@keyup.enter="handleSearch" />
</el-form-item>
<el-form-item>
<div class="search-buttons">
<el-button type="primary" @click="handleSearch" size="default">
<el-icon>
<Search />
... ... @@ -21,15 +20,15 @@
</el-icon>
重置
</el-button>
<el-button class="add-btn" @click="handleAdd" size="default">
<el-button type="primary" @click="handleAdd" size="default">
<el-icon>
<Plus />
</el-icon>
新增
</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- </div> -->
</ContentWrap>
<ContentWrap>
... ... @@ -262,6 +261,24 @@ onMounted(() => {
background: #f5f7fa;
}
.plan-search-bar {
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;
}
/* 3. 标签样式(和计划页一致) */
.search-label {
white-space: nowrap;
}
.add-btn {
height: 32px;
padding: 0 16px;
... ...
... ... @@ -45,15 +45,8 @@
</el-form-item>
<el-form-item label="频率" prop="frequencyPerWeek" label-suffix="*">
<el-select v-model="SonFormData.frequencyPerWeek">
<el-option label="不限" :value="0" />
<el-option label="1练/周" :value="1" />
<el-option label="2练/周" :value="2" />
<el-option label="3练/周" :value="3" />
<el-option label="4练/周" :value="4" />
<el-option label="5练/周" :value="5" />
<el-option label="6练/周" :value="6" />
<el-option label="7练/周" :value="7" />
<el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1">
<el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
... ... @@ -209,6 +202,39 @@ 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 }]
}
})
const syncFrequency = () => {
const count = templateCount.value
if (count > 1) {
// 超过1个模板 → 强制频率 = 模板数量
SonFormData.value.frequencyPerWeek = count
}
}
// 监听模板数量变化,自动更新频率
watch(templateCount, () => {
syncFrequency()
})
const formRules = reactive({
name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }],
... ... @@ -309,6 +335,7 @@ watch(dialogVisible, async (val) => {
loadTemplateDetail(index, item.templateId)
}
})
syncFrequency()
}
formRef.value?.clearValidate()
})
... ... @@ -399,6 +426,7 @@ const Addtemplate = (index) => {
templateDetailMap.value[i] = oldMap[i]
}
})
syncFrequency()
}
const deltemplate = (index) => {
... ... @@ -413,7 +441,7 @@ const deltemplate = (index) => {
templateDetailMap.value[i] = oldMap[i]
}
})
syncFrequency()
}
onMounted(() => {
loadTemplateList()
... ...