Authored by qxm

代码替换

@@ -2,24 +2,33 @@ @@ -2,24 +2,33 @@
2 <template> 2 <template>
3 <div class="page-container"> 3 <div class="page-container">
4 <!-- 搜索与新增区域 --> 4 <!-- 搜索与新增区域 -->
5 - <ContentWrap> 5 + <ContentWrap class="plan-search-bar">
6 <div class="search-bar"> 6 <div class="search-bar">
7 - <el-input v-model="searchForm.name" placeholder="肌肉名称" clearable @keyup.enter="handleSearch" />  
8 - <el-button @click="handleSearch">  
9 - <el-icon>  
10 - <Search />  
11 - </el-icon>搜索  
12 - </el-button>  
13 - <el-button @click="handleReset">  
14 - <el-icon>  
15 - <Refresh />  
16 - </el-icon>重置  
17 - </el-button>  
18 - <el-button @click="handleAdd">  
19 - <el-icon>  
20 - <Plus />  
21 - </el-icon>新增  
22 - </el-button> 7 + <div class="search-item">
  8 + <label class="search-label">肌肉名称</label>
  9 + <el-input v-model="searchForm.name" placeholder="肌肉名称" clearable @keyup.enter="handleSearch"
  10 + class="search-input" />
  11 + <div class="search-buttons">
  12 + <el-button type="primary" @click="handleSearch">
  13 + <el-icon>
  14 + <Search />
  15 + </el-icon>
  16 + 搜索
  17 + </el-button>
  18 + <el-button @click="handleReset">
  19 + <el-icon>
  20 + <Refresh />
  21 + </el-icon>
  22 + 重置
  23 + </el-button>
  24 + <el-button type="primary" @click="handleAdd">
  25 + <el-icon>
  26 + <Plus />
  27 + </el-icon>
  28 + 新增
  29 + </el-button>
  30 + </div>
  31 + </div>
23 </div> 32 </div>
24 </ContentWrap> 33 </ContentWrap>
25 34
@@ -30,8 +39,6 @@ @@ -30,8 +39,6 @@
30 <el-table-column prop="id" label="ID" sortable align="center" /> 39 <el-table-column prop="id" label="ID" sortable align="center" />
31 <!-- 肌肉名称列(对应接口的name字段) --> 40 <!-- 肌肉名称列(对应接口的name字段) -->
32 <el-table-column prop="name" label="肌肉名称" align="center" /> 41 <el-table-column prop="name" label="肌肉名称" align="center" />
33 - <!-- 关联大类ID列 -->  
34 - <!-- <el-table-column prop="categoryId" label="关联大类ID" align="center" /> -->  
35 <el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" /> 42 <el-table-column prop="createTime" label="创建时间" align="center" :formatter="dateFormatter" />
36 <el-table-column prop="updateTime" label="更新时间" align="center" min-width="100" :formatter="dateFormatter" /> 43 <el-table-column prop="updateTime" label="更新时间" align="center" min-width="100" :formatter="dateFormatter" />
37 <!-- 操作列 --> 44 <!-- 操作列 -->
@@ -59,39 +66,33 @@ @@ -59,39 +66,33 @@
59 </div> 66 </div>
60 </ContentWrap> 67 </ContentWrap>
61 68
62 - <ContentWrap>  
63 - <!-- 编辑弹窗 -->  
64 - <el-dialog v-model="editDialogVisible" title="编辑肌肉信息" @close="resetEditForm">  
65 - <el-form ref="editFormRef" :rules="editRules" :model="editForm" label-width="100px">  
66 - <el-form-item label="肌肉名称" prop="name">  
67 - <el-input v-model="editForm.name" placeholder="请输入肌肉名称" />  
68 - </el-form-item>  
69 - <!-- <el-form-item label="关联大类ID" prop="categoryId">  
70 - <el-input v-model.number="editForm.categoryId" type="number" placeholder="请输入关联大类ID" />  
71 - </el-form-item> -->  
72 - </el-form>  
73 - <template #footer>  
74 - <el-button @click="editDialogVisible = false">取消</el-button>  
75 - <el-button type="primary" @click="submitEditForm">确定</el-button>  
76 - </template>  
77 - </el-dialog> 69 + <!-- <ContentWrap> -->
  70 + <!-- 编辑弹窗 -->
  71 + <el-dialog v-model="editDialogVisible" title="编辑肌肉信息" @close="resetEditForm">
  72 + <el-form ref="editFormRef" :rules="editRules" :model="editForm" label-width="100px">
  73 + <el-form-item label="肌肉名称" prop="name">
  74 + <el-input v-model="editForm.name" placeholder="请输入肌肉名称" />
  75 + </el-form-item>
  76 + </el-form>
  77 + <template #footer>
  78 + <el-button @click="editDialogVisible = false">取消</el-button>
  79 + <el-button type="primary" @click="submitEditForm">确定</el-button>
  80 + </template>
  81 + </el-dialog>
78 82
79 - <!-- 新增弹窗 -->  
80 - <el-dialog v-model="addDialogVisible" title="新增肌肉信息" @close="resetAddForm">  
81 - <el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="100px">  
82 - <el-form-item label="肌肉名称" prop="name">  
83 - <el-input v-model="addForm.name" placeholder="请输入肌肉名称" />  
84 - </el-form-item>  
85 - <!-- <el-form-item label="关联大类ID" prop="categoryId">  
86 - <el-input v-model.number="addForm.categoryId" type="number" placeholder="请输入关联大类ID" />  
87 - </el-form-item> -->  
88 - </el-form>  
89 - <template #footer>  
90 - <el-button @click="addDialogVisible = false">取消</el-button>  
91 - <el-button type="primary" @click="submitAddForm">确定</el-button>  
92 - </template>  
93 - </el-dialog>  
94 - </ContentWrap> 83 + <!-- 新增弹窗 -->
  84 + <el-dialog v-model="addDialogVisible" title="新增肌肉信息" @close="resetAddForm">
  85 + <el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="100px">
  86 + <el-form-item label="肌肉名称" prop="name">
  87 + <el-input v-model="addForm.name" placeholder="请输入肌肉名称" />
  88 + </el-form-item>
  89 + </el-form>
  90 + <template #footer>
  91 + <el-button @click="addDialogVisible = false">取消</el-button>
  92 + <el-button type="primary" @click="submitAddForm">确定</el-button>
  93 + </template>
  94 + </el-dialog>
  95 + <!-- </ContentWrap> -->
95 </div> 96 </div>
96 </template> 97 </template>
97 98
@@ -100,7 +101,7 @@ import { ref, reactive, onMounted, watch } from 'vue' @@ -100,7 +101,7 @@ import { ref, reactive, onMounted, watch } from 'vue'
100 import { ElMessage, ElMessageBox, ElEmpty, ElForm, ElFormItem, ElInput, ElDialog } from 'element-plus' 101 import { ElMessage, ElMessageBox, ElEmpty, ElForm, ElFormItem, ElInput, ElDialog } from 'element-plus'
101 // 导入你提供的接口文件 102 // 导入你提供的接口文件
102 import { MusclesApi, type MusclesVO } from '@/api/store/training/muscle' 103 import { MusclesApi, type MusclesVO } from '@/api/store/training/muscle'
103 - 104 +import { Search, Refresh, Plus } from '@element-plus/icons-vue'
104 import { dateFormatter } from '@/utils/formatTime' 105 import { dateFormatter } from '@/utils/formatTime'
105 106
106 // 表格数据(对应接口返回的列表) 107 // 表格数据(对应接口返回的列表)
@@ -114,9 +115,9 @@ const searchForm = reactive({ @@ -114,9 +115,9 @@ const searchForm = reactive({
114 115
115 // 分页参数 116 // 分页参数
116 const pagination = reactive({ 117 const pagination = reactive({
117 - pageNo: 1, // 替换原currentPage  
118 - pageSize: 10, // 替换原pageSize  
119 - total: 0 // 替换原total 118 + pageNo: 1,
  119 + pageSize: 10,
  120 + total: 0
120 }) 121 })
121 122
122 // ---------------------- 编辑功能相关变量 ---------------------- 123 // ---------------------- 编辑功能相关变量 ----------------------
@@ -135,10 +136,6 @@ const editRules = reactive({ @@ -135,10 +136,6 @@ const editRules = reactive({
135 name: [ 136 name: [
136 { required: true, message: '请输入肌肉名称', trigger: 'blur' }, 137 { required: true, message: '请输入肌肉名称', trigger: 'blur' },
137 { min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' } 138 { min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
138 - ],  
139 - categoryId: [  
140 - { required: true, message: '请输入关联大类ID', trigger: 'blur' },  
141 - { type: 'number', min: 1, message: '关联大类ID必须为正整数', trigger: 'blur' }  
142 ] 139 ]
143 }) 140 })
144 141
@@ -157,10 +154,6 @@ const addRules = reactive({ @@ -157,10 +154,6 @@ const addRules = reactive({
157 name: [ 154 name: [
158 { required: true, message: '请输入肌肉名称', trigger: 'blur' }, 155 { required: true, message: '请输入肌肉名称', trigger: 'blur' },
159 { min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' } 156 { min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
160 - ],  
161 - categoryId: [  
162 - { required: true, message: '请输入关联大类ID', trigger: 'blur' },  
163 - { type: 'number', min: 1, message: '关联大类ID必须为正整数', trigger: 'blur' }  
164 ] 157 ]
165 }) 158 })
166 159
@@ -168,25 +161,23 @@ const addRules = reactive({ @@ -168,25 +161,23 @@ const addRules = reactive({
168 const getMusclesList = async () => { 161 const getMusclesList = async () => {
169 try { 162 try {
170 loading.value = true 163 loading.value = true
171 - // 构造请求参数:参数转字符串传递 164 + // 构造请求参数
172 const params = { 165 const params = {
173 - pageNo: pagination.pageNo + '', // 转字符串  
174 - pageSize: pagination.pageSize + '', // 转字符串  
175 - name: searchForm.name // 搜索的肌肉名称 166 + pageNo: pagination.pageNo + '',
  167 + pageSize: pagination.pageSize + '',
  168 + name: searchForm.name
176 } 169 }
177 // 调用接口获取数据 170 // 调用接口获取数据
178 const res = await MusclesApi.getMusclesPage(params) 171 const res = await MusclesApi.getMusclesPage(params)
179 - // 兼容不同的接口返回格式(优先取res.data,没有则取res本身)  
180 const result = res.data || res 172 const result = res.data || res
181 tableData.value = result.list || [] 173 tableData.value = result.list || []
182 pagination.total = result.total || 0 174 pagination.total = result.total || 0
183 - console.log('肌肉分页接口返回数据:', res)  
184 175
185 - // 分页边界处理:如果当前页大于总页数,自动切到最后一页 176 + // 分页边界处理
186 const totalPages = Math.ceil(pagination.total / pagination.pageSize) 177 const totalPages = Math.ceil(pagination.total / pagination.pageSize)
187 if (pagination.pageNo > totalPages && totalPages > 0) { 178 if (pagination.pageNo > totalPages && totalPages > 0) {
188 pagination.pageNo = totalPages 179 pagination.pageNo = totalPages
189 - getMusclesList() // 重新请求最后一页数据 180 + getMusclesList()
190 } 181 }
191 } catch (error) { 182 } catch (error) {
192 ElMessage.error('获取肌肉列表失败,请重试') 183 ElMessage.error('获取肌肉列表失败,请重试')
@@ -212,13 +203,13 @@ watch([() => pagination.total, () => pagination.pageSize], () => { @@ -212,13 +203,13 @@ watch([() => pagination.total, () => pagination.pageSize], () => {
212 }) 203 })
213 204
214 // 4. 事件处理函数 205 // 4. 事件处理函数
215 -// 搜索:保持原有逻辑,仅调整分页参数名 206 +// 搜索
216 const handleSearch = () => { 207 const handleSearch = () => {
217 - pagination.pageNo = 1 // 搜索时重置页码为1 208 + pagination.pageNo = 1
218 getMusclesList() 209 getMusclesList()
219 } 210 }
220 211
221 -// 重置:保持原有逻辑,仅调整分页参数名 212 +// 重置
222 const handleReset = () => { 213 const handleReset = () => {
223 searchForm.name = '' 214 searchForm.name = ''
224 pagination.pageNo = 1 215 pagination.pageNo = 1
@@ -237,7 +228,6 @@ const handleAdd = () => { @@ -237,7 +228,6 @@ const handleAdd = () => {
237 const resetAddForm = () => { 228 const resetAddForm = () => {
238 addForm.name = '' 229 addForm.name = ''
239 addForm.categoryId = 0 230 addForm.categoryId = 0
240 - // 清空表单验证状态  
241 if (addFormRef.value) { 231 if (addFormRef.value) {
242 addFormRef.value.clearValidate() 232 addFormRef.value.clearValidate()
243 } 233 }
@@ -247,20 +237,16 @@ const resetAddForm = () => { @@ -247,20 +237,16 @@ const resetAddForm = () => {
247 const submitAddForm = async () => { 237 const submitAddForm = async () => {
248 if (!addFormRef.value) return 238 if (!addFormRef.value) return
249 try { 239 try {
250 - // 表单验证  
251 await addFormRef.value.validate() 240 await addFormRef.value.validate()
252 - // 调用新增接口(无需传id,由后端生成)  
253 await MusclesApi.createMuscles({ 241 await MusclesApi.createMuscles({
254 ...addForm, 242 ...addForm,
255 - id: 0 // 占位,后端会自动生成 243 + id: 0
256 }) 244 })
257 ElMessage.success('新增成功!') 245 ElMessage.success('新增成功!')
258 - // 关闭弹窗  
259 addDialogVisible.value = false 246 addDialogVisible.value = false
260 - // 重新加载列表数据  
261 getMusclesList() 247 getMusclesList()
262 } catch (error) { 248 } catch (error) {
263 - if (error !== 'cancel') { // 排除表单验证取消的情况 249 + if (error !== 'cancel') {
264 ElMessage.error('新增失败,请重试') 250 ElMessage.error('新增失败,请重试')
265 console.error('新增失败:', error) 251 console.error('新增失败:', error)
266 } 252 }
@@ -271,14 +257,10 @@ const submitAddForm = async () => { @@ -271,14 +257,10 @@ const submitAddForm = async () => {
271 // 打开编辑弹窗并回显数据 257 // 打开编辑弹窗并回显数据
272 const handleEdit = async (row: MusclesVO) => { 258 const handleEdit = async (row: MusclesVO) => {
273 try { 259 try {
274 - // 清空编辑表单  
275 resetEditForm() 260 resetEditForm()
276 - // 打开弹窗  
277 editDialogVisible.value = true 261 editDialogVisible.value = true
278 - // 调用接口获取详情  
279 const res = await MusclesApi.getMuscles(row.id) 262 const res = await MusclesApi.getMuscles(row.id)
280 const detail = res.data || res 263 const detail = res.data || res
281 - // 回显数据到编辑表单  
282 editForm.id = detail.id 264 editForm.id = detail.id
283 editForm.name = detail.name 265 editForm.name = detail.name
284 editForm.categoryId = detail.categoryId 266 editForm.categoryId = detail.categoryId
@@ -293,7 +275,6 @@ const resetEditForm = () => { @@ -293,7 +275,6 @@ const resetEditForm = () => {
293 editForm.id = 0 275 editForm.id = 0
294 editForm.name = '' 276 editForm.name = ''
295 editForm.categoryId = 0 277 editForm.categoryId = 0
296 - // 清空表单验证状态  
297 if (editFormRef.value) { 278 if (editFormRef.value) {
298 editFormRef.value.clearValidate() 279 editFormRef.value.clearValidate()
299 } 280 }
@@ -303,17 +284,13 @@ const resetEditForm = () => { @@ -303,17 +284,13 @@ const resetEditForm = () => {
303 const submitEditForm = async () => { 284 const submitEditForm = async () => {
304 if (!editFormRef.value) return 285 if (!editFormRef.value) return
305 try { 286 try {
306 - // 表单验证  
307 await editFormRef.value.validate() 287 await editFormRef.value.validate()
308 - // 调用更新接口  
309 await MusclesApi.updateMuscles({ ...editForm }) 288 await MusclesApi.updateMuscles({ ...editForm })
310 ElMessage.success('编辑成功!') 289 ElMessage.success('编辑成功!')
311 - // 关闭弹窗  
312 editDialogVisible.value = false 290 editDialogVisible.value = false
313 - // 重新加载列表数据  
314 getMusclesList() 291 getMusclesList()
315 } catch (error) { 292 } catch (error) {
316 - if (error !== 'cancel') { // 排除表单验证取消的情况 293 + if (error !== 'cancel') {
317 ElMessage.error('编辑失败,请重试') 294 ElMessage.error('编辑失败,请重试')
318 console.error('编辑失败:', error) 295 console.error('编辑失败:', error)
319 } 296 }
@@ -323,7 +300,6 @@ const submitEditForm = async () => { @@ -323,7 +300,6 @@ const submitEditForm = async () => {
323 // 删除 300 // 删除
324 const handleDelete = async (row: MusclesVO) => { 301 const handleDelete = async (row: MusclesVO) => {
325 try { 302 try {
326 - // 确认删除  
327 await ElMessageBox.confirm( 303 await ElMessageBox.confirm(
328 '此操作将永久删除该肌肉信息,是否继续?', 304 '此操作将永久删除该肌肉信息,是否继续?',
329 '温馨提示', 305 '温馨提示',
@@ -333,13 +309,11 @@ const handleDelete = async (row: MusclesVO) => { @@ -333,13 +309,11 @@ const handleDelete = async (row: MusclesVO) => {
333 type: 'warning' 309 type: 'warning'
334 } 310 }
335 ) 311 )
336 - // 调用删除接口  
337 await MusclesApi.deleteMuscles(row.id) 312 await MusclesApi.deleteMuscles(row.id)
338 ElMessage.success('删除成功!') 313 ElMessage.success('删除成功!')
339 - // 重新获取列表  
340 getMusclesList() 314 getMusclesList()
341 } catch (error) { 315 } catch (error) {
342 - if (error !== 'cancel') { // 排除用户取消的情况 316 + if (error !== 'cancel') {
343 ElMessage.error('删除失败,请重试') 317 ElMessage.error('删除失败,请重试')
344 console.error('删除失败:', error) 318 console.error('删除失败:', error)
345 } 319 }
@@ -361,12 +335,44 @@ const handleCurrentChange = (val: number) => { @@ -361,12 +335,44 @@ const handleCurrentChange = (val: number) => {
361 </script> 335 </script>
362 336
363 <style scoped> 337 <style scoped>
364 -.page-container {  
365 - /* min-height: 100vh;  
366 - background: #fff; */ 338 +.plan-search-bar {
  339 + margin-bottom: 20px;
  340 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
  341 +}
  342 +
  343 +/* 核心修复:自动换行 + 自适应布局 */
  344 +.search-item {
  345 + display: flex;
  346 + align-items: center;
  347 + gap: 10px;
  348 + padding: 15px 20px;
  349 + flex-wrap: wrap;
  350 +}
  351 +
  352 +.search-label {
  353 + white-space: nowrap;
  354 +}
  355 +
  356 +/* 输入框自适应宽度 */
  357 +.search-input {
  358 + max-width: 300px;
  359 + min-width: 200px;
  360 + flex: 1;
  361 +}
  362 +
  363 +/* 按钮组靠右,不挤压 */
  364 +.search-buttons {
  365 + display: flex;
  366 + gap: 10px;
  367 +
  368 + /* margin-left: auto; */
  369 + flex-wrap: wrap;
  370 +}
  371 +
  372 +:deep(.plan-search-bar) {
  373 + padding: 0;
367 } 374 }
368 375
369 -/* 新增分页样式 */  
370 .pagination-wrapper { 376 .pagination-wrapper {
371 display: flex; 377 display: flex;
372 padding: 12px 16px 0; 378 padding: 12px 16px 0;
1 <!-- 部位分类页面,在接口文档没有找到部位分离的接口,用了动作分类的接口 --> 1 <!-- 部位分类页面,在接口文档没有找到部位分离的接口,用了动作分类的接口 -->
2 <template> 2 <template>
3 <div class="simple-category-page"> 3 <div class="simple-category-page">
4 - <ContentWrap> 4 + <ContentWrap class="plan-search-bar">
5 <!-- 搜索栏 --> 5 <!-- 搜索栏 -->
6 <div class="search-bar" style="padding: 15px; margin-bottom: 10px; background: #fff; border-radius: 6px;"> 6 <div class="search-bar" style="padding: 15px; margin-bottom: 10px; background: #fff; border-radius: 6px;">
7 <div style="display: flex; align-items: center; gap: 10px;"> 7 <div style="display: flex; align-items: center; gap: 10px;">
@@ -12,12 +12,14 @@ @@ -12,12 +12,14 @@
12 <el-icon> 12 <el-icon>
13 <Search /> 13 <Search />
14 </el-icon> 14 </el-icon>
15 - 搜索</el-button> 15 + 搜索
  16 + </el-button>
16 <el-button @click="resetQuery"> 17 <el-button @click="resetQuery">
17 <el-icon> 18 <el-icon>
18 <Refresh /> 19 <Refresh />
19 </el-icon> 20 </el-icon>
20 - 重置</el-button> 21 + 重置
  22 + </el-button>
21 <!-- 新增按钮 --> 23 <!-- 新增按钮 -->
22 <el-button class="add-btn" @click="openAddForm"> 24 <el-button class="add-btn" @click="openAddForm">
23 <el-icon> 25 <el-icon>
@@ -69,29 +71,30 @@ @@ -69,29 +71,30 @@
69 </div> 71 </div>
70 </div> 72 </div>
71 </ContentWrap> 73 </ContentWrap>
72 - <ContentWrap>  
73 - <!-- 新增/编辑弹窗(复用) -->  
74 - <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">  
75 - <el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">  
76 - <el-form-item label="部位分类名称:" prop="name" label-suffix="*">  
77 - <el-input v-model="formData.name" placeholder="请输入部位分类名称" maxlength="50" show-word-limit  
78 - style="width: 400px; margin-left: 10px;" />  
79 - </el-form-item>  
80 - <!-- -->  
81 - <el-form-item label="关联的肌肉部位:" prop="subCategoryIds" label-suffix="*">  
82 - <el-select v-model="formData.subCategoryIds" multiple placeholder="请选择肌肉部位" style="width: 100%">  
83 - <el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />  
84 - </el-select>  
85 - </el-form-item>  
86 - </el-form>  
87 - <template #footer>  
88 - <el-button @click="dialogVisible = false">取消</el-button>  
89 - <el-button type="primary" @click="submitForm" :loading="formLoading" :disabled="formLoading">  
90 - 确定  
91 - </el-button>  
92 - </template>  
93 - </el-dialog>  
94 - </ContentWrap> 74 +
  75 + <!-- <ContentWrap> -->
  76 + <!-- 新增/编辑弹窗(复用) -->
  77 + <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
  78 + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">
  79 + <el-form-item label="部位分类名称:" prop="name" label-suffix="*">
  80 + <el-input v-model="formData.name" placeholder="请输入部位分类名称" maxlength="50" show-word-limit
  81 + style="width: 400px; margin-left: 10px;" />
  82 + </el-form-item>
  83 + <!-- -->
  84 + <el-form-item label="关联的肌肉部位:" prop="subCategoryIds" label-suffix="*">
  85 + <el-select v-model="formData.subCategoryIds" multiple placeholder="请选择肌肉部位" style="width: 100%">
  86 + <el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />
  87 + </el-select>
  88 + </el-form-item>
  89 + </el-form>
  90 + <template #footer>
  91 + <el-button @click="dialogVisible = false">取消</el-button>
  92 + <el-button type="primary" @click="submitForm" :loading="formLoading" :disabled="formLoading">
  93 + 确定
  94 + </el-button>
  95 + </template>
  96 + </el-dialog>
  97 + <!-- </ContentWrap> -->
95 </div> 98 </div>
96 </template> 99 </template>
97 100
@@ -285,7 +288,13 @@ onMounted(() => { @@ -285,7 +288,13 @@ onMounted(() => {
285 <style scoped> 288 <style scoped>
286 .simple-category-page { 289 .simple-category-page {
287 height: 100%; 290 height: 100%;
288 - background: #f5f5f5; 291 +
  292 + /* background: #f5f5f5; */
  293 +}
  294 +
  295 +.plan-search-bar {
  296 + margin-bottom: 20px;
  297 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
289 } 298 }
290 299
291 .add-btn { 300 .add-btn {
@@ -2,34 +2,36 @@ @@ -2,34 +2,36 @@
2 <div class="page-container"> 2 <div class="page-container">
3 <!-- 搜索栏 --> 3 <!-- 搜索栏 -->
4 <ContentWrap class="plan-search-bar"> 4 <ContentWrap class="plan-search-bar">
5 - <div class="search-bar-wrapper">  
6 - <div class="search-item">  
7 - <label class="search-label">计划大类名称</label>  
8 - <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" clearable  
9 - @keyup.enter="handleSearch" />  
10 - <div class="search-buttons">  
11 - <el-button type="primary" @click="handleSearch">  
12 - <el-icon>  
13 - <Search />  
14 - </el-icon>  
15 - 搜索  
16 - </el-button>  
17 - <el-button @click="resetSearch">  
18 - <el-icon>  
19 - <Refresh />  
20 - </el-icon>  
21 - 重置  
22 - </el-button>  
23 - <el-button type="primary" @click="handleAdd">  
24 - <el-icon>  
25 - <Plus />  
26 - </el-icon>  
27 - 新增  
28 - </el-button>  
29 - </div> 5 + <!-- <div class="search-bar-wrapper"> -->
  6 + <div class="search-item">
  7 + <label class="search-label">计划大类名称</label>
  8 + <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" clearable
  9 + @keyup.enter="handleSearch" />
  10 + <div class="search-buttons">
  11 + <el-button type="primary" @click="handleSearch">
  12 + <el-icon>
  13 + <Search />
  14 + </el-icon>
  15 + 搜索
  16 + </el-button>
  17 + <el-button @click="resetSearch">
  18 + <el-icon>
  19 + <Refresh />
  20 + </el-icon>
  21 + 重置
  22 + </el-button>
  23 + <el-button type="primary" @click="handleAdd">
  24 + <el-icon>
  25 + <Plus />
  26 + </el-icon>
  27 + 新增
  28 + </el-button>
30 </div> 29 </div>
31 </div> 30 </div>
  31 + <!-- </div> -->
32 </ContentWrap> 32 </ContentWrap>
  33 +
  34 +
33 <ContentWrap> 35 <ContentWrap>
34 <!-- 表格 --> 36 <!-- 表格 -->
35 <el-table v-loading="loading" :data="tableData" stripe style="width: 100%;"> 37 <el-table v-loading="loading" :data="tableData" stripe style="width: 100%;">
@@ -251,7 +253,6 @@ onMounted(() => { @@ -251,7 +253,6 @@ onMounted(() => {
251 /* 3. 标签样式(和计划页一致) */ 253 /* 3. 标签样式(和计划页一致) */
252 .search-label { 254 .search-label {
253 white-space: nowrap; 255 white-space: nowrap;
254 -  
255 } 256 }
256 257
257 /* 4. 输入框宽度(和计划页一致) */ 258 /* 4. 输入框宽度(和计划页一致) */
@@ -32,30 +32,30 @@ @@ -32,30 +32,30 @@
32 </el-form-item> 32 </el-form-item>
33 <el-form-item label="主要训练部位:" prop="primaryMuscles" label-suffix="*"> 33 <el-form-item label="主要训练部位:" prop="primaryMuscles" label-suffix="*">
34 <el-select v-model="formDataModel.primaryMuscles" multiple placeholder="请输入主要训练部位" style="width: 100%"> 34 <el-select v-model="formDataModel.primaryMuscles" multiple placeholder="请输入主要训练部位" style="width: 100%">
35 - <el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" /> 35 + <el-option v-for="item in primaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" />
36 </el-select> 36 </el-select>
37 </el-form-item> 37 </el-form-item>
38 <el-form-item label="次要训练部位:" prop="secondaryMuscles" label-suffix="*"> 38 <el-form-item label="次要训练部位:" prop="secondaryMuscles" label-suffix="*">
39 <el-select v-model="formDataModel.secondaryMuscles" multiple placeholder="请输入次要训练部位" style="width: 100%"> 39 <el-select v-model="formDataModel.secondaryMuscles" multiple placeholder="请输入次要训练部位" style="width: 100%">
40 - <el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" /> 40 + <el-option v-for="item in secondaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" />
41 </el-select> 41 </el-select>
42 </el-form-item> 42 </el-form-item>
43 - <!-- 封面图 -->  
44 - <el-form-item label="锻炼部位图" prop="urlImage" label-suffix="*" class="form-item-margin upload-img-item"> 43 + <!-- 锻炼部位图 -->
  44 + <el-form-item label="锻炼部位图" prop="urlImage" class="form-item-margin upload-img-item">
45 <UploadImg v-model="formDataModel.urlImage" /> 45 <UploadImg v-model="formDataModel.urlImage" />
46 </el-form-item> 46 </el-form-item>
47 - <el-form-item label="3D动画地址" prop="url3dAnimation" label-suffix="*">  
48 - <UploadVideo v-model="formDataModel.url3dAnimation" /> 47 + <el-form-item label="3D动画地址" prop="url3dAnimation" label-suffix="*" class="upload-img-item upload-3d-item">
  48 + <UploadImg v-model="formDataModel.url3dAnimation" />
49 </el-form-item> 49 </el-form-item>
50 50
51 - <el-form-item label="真人演示地址" prop="urlRealPerson"> 51 + <el-form-item label="真人演示地址" prop="urlRealPerson" label-suffix="*">
52 <UploadVideo v-model="formDataModel.urlRealPerson" /> 52 <UploadVideo v-model="formDataModel.urlRealPerson" />
53 </el-form-item> 53 </el-form-item>
54 54
55 - <el-form-item label="教程地址" prop="urlTutorial"> 55 + <el-form-item label="教程地址" prop="urlTutorial" label-suffix="*">
56 <UploadVideo v-model="formDataModel.urlTutorial" /> 56 <UploadVideo v-model="formDataModel.urlTutorial" />
57 </el-form-item> 57 </el-form-item>
58 - <el-form-item label="动作步骤描述:" prop="stepDescription" label-suffix="*" class="desc-item"> 58 + <el-form-item label="动作步骤描述:" prop="stepDescription" label-suffix="*" class="desc-item">
59 <Editor v-model="formDataModel.stepDescription" placeholder="请输入动作步骤描述" /> 59 <Editor v-model="formDataModel.stepDescription" placeholder="请输入动作步骤描述" />
60 </el-form-item> 60 </el-form-item>
61 </el-form> 61 </el-form>
@@ -68,17 +68,20 @@ @@ -68,17 +68,20 @@
68 68
69 <script setup> 69 <script setup>
70 70
71 -import { ref, reactive, watch, onMounted, nextTick } from 'vue' 71 +import { ref, reactive, watch, onMounted, nextTick, computed } from 'vue'
72 import { ElMessage } from 'element-plus' 72 import { ElMessage } from 'element-plus'
73 import { MotionCategoryApi } from '@/api/store/training/partCategory' 73 import { MotionCategoryApi } from '@/api/store/training/partCategory'
74 import { MusclesApi } from '@/api/store/training/muscle' 74 import { MusclesApi } from '@/api/store/training/muscle'
75 import { EquipmentsApi } from '@/api/store/training/tool' 75 import { EquipmentsApi } from '@/api/store/training/tool'
76 import { Editor } from '@/components/Editor' 76 import { Editor } from '@/components/Editor'
77 import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue' 77 import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue'
  78 +import { ExercisesApi } from '@/api/store/training/pose'
78 79
79 // 1. 自定义 v-model:弹窗显隐(对应父组件 v-model:visible) 80 // 1. 自定义 v-model:弹窗显隐(对应父组件 v-model:visible)
80 const dialogVisible = defineModel('visible', { type: Boolean, default: false }) 81 const dialogVisible = defineModel('visible', { type: Boolean, default: false })
81 82
  83 +const defaultImg = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png'
  84 +
82 // 2. 自定义 v-model:表单数据(对应父组件 v-model:formData) 85 // 2. 自定义 v-model:表单数据(对应父组件 v-model:formData)
83 const formDataModel = defineModel('formData', { 86 const formDataModel = defineModel('formData', {
84 type: Object, // 新增这一行 87 type: Object, // 新增这一行
@@ -113,10 +116,43 @@ watch(dialogVisible, async (newVal) => { @@ -113,10 +116,43 @@ watch(dialogVisible, async (newVal) => {
113 loadToolList(), 116 loadToolList(),
114 loadMuscleList() 117 loadMuscleList()
115 ]) 118 ])
  119 + const form = formDataModel.value
  120 + // 调试打印
  121 + console.log('解析前-原始数据:', form)
  122 + console.log('解析前-primaryMuscles:', form.primaryMuscles, typeof form.primaryMuscles)
  123 + console.log('解析前-secondaryMuscles:', form.secondaryMuscles, typeof form.secondaryMuscles)
  124 + if (typeof form.primaryMuscles === 'string') {
  125 + try {
  126 + // 解析字符串 "[17, 16]" → 数组 [17, 16]
  127 + formDataModel.value.primaryMuscles = JSON.parse(form.primaryMuscles)
  128 + } catch (err) {
  129 + // 如果解析失败(比如空字符串、非法格式),就默认设为空数组
  130 + console.warn('primaryMuscles 解析失败,已重置为空数组', err)
  131 + formDataModel.value.primaryMuscles = []
  132 + }
  133 + }
  134 +
  135 + // 处理 secondaryMuscles(和上面逻辑一样)
  136 + if (typeof form.secondaryMuscles === 'string') {
  137 + try {
  138 + formDataModel.value.secondaryMuscles = JSON.parse(form.secondaryMuscles)
  139 + } catch (err) {
  140 + console.warn('secondaryMuscles 解析失败,已重置为空数组', err)
  141 + formDataModel.value.secondaryMuscles = []
  142 + }
  143 + }
  144 + // -------------------------------------------------------------
  145 +
  146 + // 解析完成后,再打印一次,确认变成了数组
  147 + console.log('解析后-primaryMuscles:', formDataModel.value.primaryMuscles, typeof formDataModel.value.primaryMuscles)
  148 + console.log('解析后-secondaryMuscles:', formDataModel.value.secondaryMuscles, typeof formDataModel.value.secondaryMuscles)
  149 +
116 // 列表加载完后,强制更新表单,触发el-select回显 150 // 列表加载完后,强制更新表单,触发el-select回显
117 nextTick(() => { 151 nextTick(() => {
118 if (formRef.value) { 152 if (formRef.value) {
119 formRef.value.clearValidate() 153 formRef.value.clearValidate()
  154 +
  155 +
120 } 156 }
121 }) 157 })
122 } 158 }
@@ -140,13 +176,26 @@ const formRules = reactive({ @@ -140,13 +176,26 @@ const formRules = reactive({
140 categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }], 176 categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }],
141 equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }], 177 equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }],
142 exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }], 178 exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }],
143 - primaryMuscles: [{ required: true, message: '请输入主要训练部位', trigger: 'blur' }],  
144 - secondaryMuscles: [{ required: true, message: '请输入次要训练部位', trigger: 'blur' }],  
145 - urlImage: [{ required: true, message: '请上传图片', trigger: 'blur' }], 179 + primaryMuscles: [{ required: true, message: '请输入主要训练部位', trigger: 'change' }],
  180 + secondaryMuscles: [{ required: true, message: '请输入次要训练部位', trigger: 'change' }],
  181 + urlImage: [{ required: false, message: '请上传图片', trigger: 'blur' }],
  182 + urlRealPerson: [{ required: true, message: '请上传真人演示地址', trigger: 'blur' }],
  183 + urlTutorial: [{ required: true, message: '请上传教程地址', trigger: 'blur' }],
146 url3dAnimation: [{ required: true, message: '请上传3D动图', trigger: 'blur' }], 184 url3dAnimation: [{ required: true, message: '请上传3D动图', trigger: 'blur' }],
147 stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }], 185 stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }],
148 }) 186 })
149 187
  188 +// 次要部位选项 = 全部肌肉 - 已选主要部位
  189 +const secondaryMuscleOptions = computed(() => {
  190 + const primaryIds = formDataModel.value.primaryMuscles || []
  191 + return muscleList.value.filter(item => !primaryIds.includes(item.id))
  192 +})
  193 +
  194 +// 主要部位选项 = 全部肌肉 - 已选次要部位
  195 +const primaryMuscleOptions = computed(() => {
  196 + const secondaryIds = formDataModel.value.secondaryMuscles || []
  197 + return muscleList.value.filter(item => !secondaryIds.includes(item.id))
  198 +})
150 199
151 200
152 // 取消按钮:直接关闭弹窗(自动同步给父组件) 201 // 取消按钮:直接关闭弹窗(自动同步给父组件)
@@ -159,27 +208,65 @@ const handleCancel = () => { @@ -159,27 +208,65 @@ const handleCancel = () => {
159 const handleSubmit = async () => { 208 const handleSubmit = async () => {
160 if (!formRef.value) return 209 if (!formRef.value) return
161 try { 210 try {
  211 + console.log('123456');
  212 +
162 await formRef.value.validate() 213 await formRef.value.validate()
  214 + console.log('123456');
163 formLoading.value = true 215 formLoading.value = true
164 - // 👇 加这行!看原始数据是不是嵌套数组  
165 console.log('原始primaryMuscles:', formDataModel.value.primaryMuscles) 216 console.log('原始primaryMuscles:', formDataModel.value.primaryMuscles)
166 console.log('原始secondaryMuscles:', formDataModel.value.secondaryMuscles) 217 console.log('原始secondaryMuscles:', formDataModel.value.secondaryMuscles)
167 - // 👇 核心处理:将数组转为逗号分隔的字符串 218 +
  219 + const form = formDataModel.value
168 const submitData = { 220 const submitData = {
169 ...formDataModel.value, 221 ...formDataModel.value,
170 - // 如果数组为空,传空字符串 "" 否则会变成 "undefined"  
171 - primaryMuscles: formDataModel.value.primaryMuscles || [],  
172 - secondaryMuscles: formDataModel.value.secondaryMuscles || [], 222 + urlImage: form.urlImage || defaultImg,
  223 + // primaryMuscles: formDataModel.value.primaryMuscles || [],
  224 + // secondaryMuscles: formDataModel.value.secondaryMuscles || [],
  225 + primaryMuscles: JSON.stringify(formDataModel.value.primaryMuscles || []),
  226 + secondaryMuscles: JSON.stringify(formDataModel.value.secondaryMuscles || []),
  227 + }
  228 + console.log('------------------');
  229 +
  230 + if (submitData.id) {
  231 + await ExercisesApi.updateExercises(submitData) // 编辑:调用更新接口
  232 + console.log('动作编辑接口数据', submitData)
  233 + ElMessage.success('动作编辑成功!')
  234 + } else {
  235 + console.log('动作新增接口数据', submitData)
  236 + await ExercisesApi.addExercises(submitData) // 新增:调用创建接口
  237 + ElMessage.success('动作新增成功!')
173 } 238 }
174 // 触发提交事件,数据已通过 v-model 同步,直接传即可 239 // 触发提交事件,数据已通过 v-model 同步,直接传即可
175 - emit('submit', submitData) 240 + emit('submit')
176 } catch (error) { 241 } catch (error) {
177 ElMessage.error('表单校验失败,请检查必填项') 242 ElMessage.error('表单校验失败,请检查必填项')
178 } finally { 243 } finally {
179 formLoading.value = false 244 formLoading.value = false
  245 + // resetForm()
180 } 246 }
181 } 247 }
182 248
  249 +
  250 +// const handleFormSubmit = async (submitData) => {
  251 +// try {
  252 +// // ✅ 直接传数组,axios 自动转成 JSON 字符串
  253 +// const formatData = {
  254 +// ...submitData,
  255 +// primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),
  256 +// secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])
  257 +// }
  258 +// console.log('提交的数据', formatData);
  259 +
  260 +// // 关闭弹窗
  261 +// handleDialogClose()
  262 +// // 重新查询数据
  263 +// handleQuery()
  264 +// } catch (err) {
  265 +// console.error('提交失败:', err)
  266 +// ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')
  267 +// }
  268 +// }
  269 +
183 // 重置表单(只操作本地 model,自动同步父组件) 270 // 重置表单(只操作本地 model,自动同步父组件)
184 const resetForm = () => { 271 const resetForm = () => {
185 Object.assign(formDataModel.value, { 272 Object.assign(formDataModel.value, {
@@ -278,13 +365,22 @@ defineExpose({ resetForm }) @@ -278,13 +365,22 @@ defineExpose({ resetForm })
278 :deep(.el-form-item.upload-img-item .el-form-item__label) { 365 :deep(.el-form-item.upload-img-item .el-form-item__label) {
279 /* 上传组件高度高,让 label 顶部对齐 */ 366 /* 上传组件高度高,让 label 顶部对齐 */
280 line-height: 120px !important; 367 line-height: 120px !important;
  368 +
281 } 369 }
282 370
  371 +/* .upload-img-item {
  372 + margin-bottom: 20rpx;
  373 +} */
  374 +
283 /* 单独给封面图增加上下间距 */ 375 /* 单独给封面图增加上下间距 */
284 .form-item-margin { 376 .form-item-margin {
285 margin: 30px 5px !important; 377 margin: 30px 5px !important;
286 } 378 }
287 379
  380 +.upload-3d-item {
  381 + margin-top: 20px !important;
  382 +}
  383 +
288 .upload-box { 384 .upload-box {
289 display: flex; 385 display: flex;
290 width: 120px; 386 width: 120px;
@@ -32,14 +32,15 @@ @@ -32,14 +32,15 @@
32 <!-- 表格区域 --> 32 <!-- 表格区域 -->
33 <ContentWrap> 33 <ContentWrap>
34 <el-table :data="tableData" stripe style="width: 100%" v-loading="loading" empty-text="暂无动作数据" 34 <el-table :data="tableData" stripe style="width: 100%" v-loading="loading" empty-text="暂无动作数据"
35 - :header-cell-style="{ background: '#f5f7fa', textAlign: 'center' }" :cell-style="{ textAlign: 'center' }"> 35 + :header-cell-style="{ background: '#f5f7fa', textAlign: 'center' }"
  36 + :cell-style="{ textAlign: 'center', justifyContent: 'center' }">
36 <el-table-column prop="name" label="动作名称" min-width="120" /> 37 <el-table-column prop="name" label="动作名称" min-width="120" />
37 <!-- 封面 --> 38 <!-- 封面 -->
38 - <el-table-column prop="urlImage" label="封面" align="center" min-width="120"> 39 + <el-table-column prop="url3dAnimation" label="封面" align="center" min-width="120">
39 <template #default="scope"> 40 <template #default="scope">
40 <div class="cover-wrapper"> 41 <div class="cover-wrapper">
41 <!-- 优先显示行数据封面,无则显示占位图 --> 42 <!-- 优先显示行数据封面,无则显示占位图 -->
42 - <img class="cover-img" :src="scope.row.urlImage || lostImg" alt="模板封面" /> 43 + <img class="cover-img" :src="scope.row.url3dAnimation || lostImg" alt="模板封面" />
43 </div> 44 </div>
44 </template> 45 </template>
45 </el-table-column> 46 </el-table-column>
@@ -59,10 +60,11 @@ @@ -59,10 +60,11 @@
59 </el-table> 60 </el-table>
60 61
61 <!-- 分页组件 --> 62 <!-- 分页组件 -->
62 - <div class="pagination-wrapper">  
63 - <span class="goto-text">共 {{ total }} 条</span>  
64 - <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize"  
65 - :page-sizes="[10, 20, 50, 100]" :total="total" background layout="prev, pager, next, jumper, ->, sizes, total" 63 + <div class="pagination-wrapper" style="padding: 12px 16px;">
  64 + <!-- <span class="goto-text">共 {{ total }} 条</span> -->
  65 + <!-- 共 {{ total }} 条 -->
  66 + <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" :total="total"
  67 + :page-sizes="[10, 20, 50]" background layout="total, sizes, prev, pager, next, jumper"
66 @size-change="handleSizeChange" @current-change="handleCurrentChange" /> 68 @size-change="handleSizeChange" @current-change="handleCurrentChange" />
67 </div> 69 </div>
68 </ContentWrap> 70 </ContentWrap>
@@ -192,40 +194,46 @@ const handleDelete = async (row: ExercisesVO) => { @@ -192,40 +194,46 @@ const handleDelete = async (row: ExercisesVO) => {
192 } 194 }
193 195
194 // 处理表单提交(新增/编辑) 196 // 处理表单提交(新增/编辑)
195 -const handleFormSubmit = async (submitData: ExercisesSaveReqVO) => {  
196 - try {  
197 - // ✅ 直接传数组,axios 自动转成 JSON 字符串  
198 - const formatData = {  
199 - ...submitData,  
200 - primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),  
201 - secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])  
202 - }  
203 -  
204 - if (submitData.id) {  
205 - await ExercisesApi.updateExercises(formatData) // 编辑:调用更新接口  
206 - console.log('动作编辑接口数据', submitData)  
207 - ElMessage.success('动作编辑成功!')  
208 - } else {  
209 - console.log('动作新增接口数据', submitData)  
210 - await ExercisesApi.addExercises(formatData) // 新增:调用创建接口  
211 - ElMessage.success('动作新增成功!')  
212 - }  
213 - // 关闭弹窗  
214 - handleDialogClose()  
215 - // 重新查询数据  
216 - handleQuery()  
217 - } catch (err) {  
218 - console.error('提交失败:', err)  
219 - ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')  
220 - }  
221 -}  
222 -  
223 -// 关闭弹窗  
224 -const handleDialogClose = () => { 197 +// const handleFormSubmit = async (submitData: ExercisesSaveReqVO) => {
  198 +// try {
  199 +// // ✅ 直接传数组,axios 自动转成 JSON 字符串
  200 +// const formatData = {
  201 +// ...submitData,
  202 +// primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),
  203 +// secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])
  204 +// }
  205 +// console.log('提交的数据', formatData);
  206 +// if (submitData.id) {
  207 +// await ExercisesApi.updateExercises(formatData) // 编辑:调用更新接口
  208 +// console.log('动作编辑接口数据', submitData)
  209 +// ElMessage.success('动作编辑成功!')
  210 +// } else {
  211 +// console.log('动作新增接口数据', submitData)
  212 +// await ExercisesApi.addExercises(formatData) // 新增:调用创建接口
  213 +// ElMessage.success('动作新增成功!')
  214 +// }
  215 +// // 关闭弹窗
  216 +// handleDialogClose()
  217 +// // 重新查询数据
  218 +// handleQuery()
  219 +// } catch (err) {
  220 +// console.error('提交失败:', err)
  221 +// ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')
  222 +// }
  223 +// }
  224 +
  225 +const handleFormSubmit = () => {
  226 + handleQuery()
225 dialogVisible.value = false 227 dialogVisible.value = false
226 resetFormData() 228 resetFormData()
227 } 229 }
228 230
  231 +// 关闭弹窗
  232 +// const handleDialogClose = () => {
  233 +// dialogVisible.value = false
  234 +// resetFormData()
  235 +// }
  236 +
229 // 重置表单数据 237 // 重置表单数据
230 const resetFormData = () => { 238 const resetFormData = () => {
231 Object.assign(formData, { 239 Object.assign(formData, {
1 <template> 1 <template>
2 <div class="page-container"> 2 <div class="page-container">
3 - <el-card class="search-bar"> 3 + <ContentWrap class="search-bar">
4 <div class="search-item"> 4 <div class="search-item">
5 <label class="search-label">学员</label> 5 <label class="search-label">学员</label>
6 <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" @keyup.enter="handleSearch" 6 <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" @keyup.enter="handleSearch"
@@ -26,10 +26,11 @@ @@ -26,10 +26,11 @@
26 </el-button> --> 26 </el-button> -->
27 </div> 27 </div>
28 </div> 28 </div>
29 - </el-card> 29 + </ContentWrap>
30 30
31 <!-- 列表区域 --> 31 <!-- 列表区域 -->
32 - <el-card class="table-container"> 32 + <!-- <div class="table-container"> -->
  33 + <ContentWrap>
33 <el-table :data="tableData" v-loading="loading" empty-text="暂无数据" border> 34 <el-table :data="tableData" v-loading="loading" empty-text="暂无数据" border>
34 <el-table-column prop="username" label="学员" align="center" /> 35 <el-table-column prop="username" label="学员" align="center" />
35 <el-table-column prop="name" label="训练计划名称" align="center" /> 36 <el-table-column prop="name" label="训练计划名称" align="center" />
@@ -54,7 +55,8 @@ @@ -54,7 +55,8 @@
54 :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" 55 :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
55 style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> 56 style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
56 </div> 57 </div>
57 - </el-card> 58 + </ContentWrap>
  59 + <!-- </div> -->
58 <!-- 详情弹窗 --> 60 <!-- 详情弹窗 -->
59 <StuTrainingDetailDialog v-model:visible="detailVisible" :id="currentId" /> 61 <StuTrainingDetailDialog v-model:visible="detailVisible" :id="currentId" />
60 </div> 62 </div>
@@ -184,9 +186,9 @@ onMounted(() => { @@ -184,9 +186,9 @@ onMounted(() => {
184 }) 186 })
185 </script> 187 </script>
186 <style scoped> 188 <style scoped>
187 -.page-container {  
188 - /* padding: 20px; */  
189 -} 189 +/* .page-container {
  190 +
  191 +} */
190 192
191 /* 分页居右 */ 193 /* 分页居右 */
192 .pagination-wrapper { 194 .pagination-wrapper {
@@ -196,13 +198,16 @@ onMounted(() => { @@ -196,13 +198,16 @@ onMounted(() => {
196 } 198 }
197 199
198 .search-bar { 200 .search-bar {
  201 + /* margin-bottom: 20px; */
199 margin-bottom: 20px; 202 margin-bottom: 20px;
  203 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
200 } 204 }
201 205
202 .search-item { 206 .search-item {
203 display: flex; 207 display: flex;
204 align-items: center; 208 align-items: center;
205 gap: 10px; 209 gap: 10px;
  210 + padding: 15px 20px;
206 } 211 }
207 212
208 .search-label { 213 .search-label {
1 <template> 1 <template>
2 <div class="supersets-page"> 2 <div class="supersets-page">
3 - <ContentWrap> 3 + <ContentWrap class="plan-search-bar">
4 <!-- 1. 搜索栏区域 --> 4 <!-- 1. 搜索栏区域 -->
5 <div class="search-bar"> 5 <div class="search-bar">
6 <div class="search-item"> 6 <div class="search-item">
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 </el-icon> 21 </el-icon>
22 重置 22 重置
23 </el-button> 23 </el-button>
24 - <el-button @click="openAddDialog"> 24 + <el-button type="primary" @click="openAddDialog">
25 <el-icon> 25 <el-icon>
26 <Plus /> 26 <Plus />
27 </el-icon> 27 </el-icon>
@@ -401,7 +401,12 @@ onMounted(async () => { @@ -401,7 +401,12 @@ onMounted(async () => {
401 box-sizing: border-box; 401 box-sizing: border-box;
402 } 402 }
403 403
404 -.search-bar { 404 +.plan-search-bar {
  405 + margin-bottom: 20px;
  406 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
  407 +}
  408 +
  409 +/* .search-bar {
405 display: flex; 410 display: flex;
406 align-items: center; 411 align-items: center;
407 padding: 16px; 412 padding: 16px;
@@ -410,13 +415,13 @@ onMounted(async () => { @@ -410,13 +415,13 @@ onMounted(async () => {
410 border-radius: 4px; 415 border-radius: 4px;
411 gap: 10px; 416 gap: 10px;
412 flex-wrap: nowrap; 417 flex-wrap: nowrap;
413 -} 418 +} */
414 419
415 .search-item { 420 .search-item {
416 display: flex; 421 display: flex;
417 align-items: center; 422 align-items: center;
418 gap: 10px; 423 gap: 10px;
419 - width: 100%; 424 + padding: 15px 20px;
420 } 425 }
421 426
422 .search-label { 427 .search-label {
@@ -2,99 +2,96 @@ @@ -2,99 +2,96 @@
2 <template> 2 <template>
3 <div class="page-container"> 3 <div class="page-container">
4 <!-- 搜索框区域 --> 4 <!-- 搜索框区域 -->
5 - <div class="equipments-page">  
6 - <ContentWrap>  
7 - <!-- 搜索栏 -->  
8 - <div class="search-bar" style="padding: 16px; margin-bottom: 16px; background: #fff; border-radius: 4px;">  
9 - <el-form :inline="true" :model="searchForm" class="demo-form-inline">  
10 - <el-form-item label="文件夹名称:">  
11 - <el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"  
12 - @keyup.enter="handleSearch" />  
13 - </el-form-item>  
14 - <el-form-item>  
15 - <el-button type="primary" @click="handleSearch" size="default">  
16 - <el-icon>  
17 - <Search />  
18 - </el-icon>  
19 - 搜索  
20 - </el-button>  
21 - <el-button @click="resetSearch" size="default">  
22 - <el-icon>  
23 - <Refresh />  
24 - </el-icon>  
25 - 重置  
26 - </el-button>  
27 - <el-button class="add-btn" @click="handleAdd" size="default">  
28 - <el-icon>  
29 - <Plus />  
30 - </el-icon>  
31 - 新增  
32 - </el-button>  
33 - </el-form-item>  
34 - </el-form> 5 + <!-- <div class="equipments-page"> -->
  6 + <ContentWrap class="plan-search-bar">
  7 + <!-- 搜索栏 -->
  8 + <div class="search-bar">
  9 + <label class="search-label">文件夹名称</label>
  10 + <el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"
  11 + @keyup.enter="handleSearch" />
  12 + <div class="search-buttons">
  13 + <el-button type="primary" @click="handleSearch" size="default">
  14 + <el-icon>
  15 + <Search />
  16 + </el-icon>
  17 + 搜索
  18 + </el-button>
  19 + <el-button @click="resetSearch" size="default">
  20 + <el-icon>
  21 + <Refresh />
  22 + </el-icon>
  23 + 重置
  24 + </el-button>
  25 + <el-button type="primary" @click="handleAdd" size="default">
  26 + <el-icon>
  27 + <Plus />
  28 + </el-icon>
  29 + 新增
  30 + </el-button>
35 </div> 31 </div>
36 - </ContentWrap>  
37 - <!-- 表格区域 -->  
38 - <ContentWrap>  
39 - <div class="table-container">  
40 - <el-table :data="tableData" v-loading="loading" style="width: 100%" empty-text="暂无数据" stripe>  
41 - <el-table-column prop="name" label="文件夹名字" align="center" />  
42 - <el-table-column prop="cover" label="封面" align="center" min-width="120">  
43 - <template #default="scope">  
44 - <div class="cover-wrapper">  
45 - <!-- 优先显示行数据封面,无则显示占位图 -->  
46 - <img class="cover-img" :src="scope.row.urlCover || lostImg" alt="模板封面" />  
47 - </div>  
48 - </template>  
49 - </el-table-column>  
50 - <el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" />  
51 - <el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" />  
52 - <el-table-column label="操作" align="center" width="200">  
53 - <template #default="scope">  
54 - <el-button type="text" class="edit-btn" @click="handleEdit(scope.row)">编辑</el-button>  
55 - <el-button type="text" class="delete-btn" @click="handleDelete(scope.row.id)">删除</el-button>  
56 - </template>  
57 - </el-table-column>  
58 - </el-table>  
59 - </div>  
60 - </ContentWrap>  
61 -  
62 - <!-- 编辑/新增弹窗 -->  
63 - <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">  
64 - <el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px">  
65 - <el-form-item label="大类分类名称:" prop="name" label-suffix="*">  
66 - <el-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit  
67 - style="width: 100%" />  
68 - </el-form-item>  
69 - <el-form-item label="模板" prop="templateIds">  
70 - <el-select v-model="formData.templateIds" placeholder="请选择模板" multiple clearable style="width: 100%"  
71 - filterable>  
72 - <el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" />  
73 - </el-select>  
74 - </el-form-item>  
75 - <el-form-item label="封面" prop="urlCover" label-suffix="*">  
76 - <UploadImg v-model="formData.urlCover" placeholder="封面" maxlength="50" show-word-limit  
77 - style="width: 100%" />  
78 - </el-form-item>  
79 - <el-form-item label="模板大类介绍" prop="description" label-suffix="*">  
80 - <el-input v-model="formData.description" placeholder="请输入模板大类介绍" maxlength="50" show-word-limit  
81 - style="width: 100%" />  
82 - </el-form-item>  
83 - </el-form>  
84 - <template #footer>  
85 - <el-button @click="dialogVisible = false">取 消</el-button>  
86 - <el-button type="primary" @click="submitForm" :loading="loading" :disabled="loading">确 定</el-button>  
87 - </template>  
88 - </el-dialog>  
89 - 32 + </div>
  33 + </ContentWrap>
  34 + <!-- 表格区域 -->
  35 + <ContentWrap>
  36 + <div class="table-container">
  37 + <el-table :data="tableData" v-loading="loading" style="width: 100%" empty-text="暂无数据" stripe>
  38 + <el-table-column prop="name" label="文件夹名字" align="center" />
  39 + <el-table-column prop="cover" label="封面" align="center" min-width="120">
  40 + <template #default="scope">
  41 + <div class="cover-wrapper">
  42 + <!-- 优先显示行数据封面,无则显示占位图 -->
  43 + <img class="cover-img" :src="scope.row.urlCover || lostImg" alt="模板封面" />
  44 + </div>
  45 + </template>
  46 + </el-table-column>
  47 + <el-table-column prop="createTime" label="创建时间" align="center" width="220" :formatter="dateFormatter" />
  48 + <el-table-column prop="updateTime" label="更新时间" align="center" min-width="180" :formatter="dateFormatter" />
  49 + <el-table-column label="操作" align="center" width="200">
  50 + <template #default="scope">
  51 + <el-button type="text" class="edit-btn" @click="handleEdit(scope.row)">编辑</el-button>
  52 + <el-button type="text" class="delete-btn" @click="handleDelete(scope.row.id)">删除</el-button>
  53 + </template>
  54 + </el-table-column>
  55 + </el-table>
  56 + </div>
90 57
91 - <!-- 分页组件 -->  
92 <div class="pagination-wrapper" style="padding: 12px 16px;"> 58 <div class="pagination-wrapper" style="padding: 12px 16px;">
93 <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" 59 <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize"
94 :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" 60 :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
95 style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> 61 style="text-align: left;" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
96 </div> 62 </div>
97 - </div> 63 + </ContentWrap>
  64 +
  65 + <!-- 编辑/新增弹窗 -->
  66 + <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
  67 + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px">
  68 + <el-form-item label="大类分类名称:" prop="name" label-suffix="*">
  69 + <el-input v-model="formData.name" placeholder="请输入分类名称" maxlength="50" show-word-limit style="width: 100%" />
  70 + </el-form-item>
  71 + <el-form-item label="模板" prop="templateIds">
  72 + <el-select v-model="formData.templateIds" placeholder="请选择模板" multiple clearable style="width: 100%"
  73 + filterable>
  74 + <el-option v-for="item in templateList" :key="item.id" :label="item.name" :value="item.id" />
  75 + </el-select>
  76 + </el-form-item>
  77 + <el-form-item label="封面" prop="urlCover" label-suffix="*">
  78 + <UploadImg v-model="formData.urlCover" placeholder="封面" maxlength="50" show-word-limit style="width: 100%" />
  79 + </el-form-item>
  80 + <el-form-item label="模板大类介绍" prop="description" label-suffix="*">
  81 + <el-input v-model="formData.description" placeholder="请输入模板大类介绍" maxlength="50" show-word-limit
  82 + style="width: 100%" />
  83 + </el-form-item>
  84 + </el-form>
  85 + <template #footer>
  86 + <el-button @click="dialogVisible = false">取 消</el-button>
  87 + <el-button type="primary" @click="submitForm" :loading="loading" :disabled="loading">确 定</el-button>
  88 + </template>
  89 + </el-dialog>
  90 +
  91 +
  92 + <!-- 分页组件 -->
  93 +
  94 + <!-- </div> -->
98 95
99 </div> 96 </div>
100 </template> 97 </template>
@@ -306,6 +303,31 @@ onMounted(() => { @@ -306,6 +303,31 @@ onMounted(() => {
306 }) 303 })
307 </script> 304 </script>
308 <style scoped> 305 <style scoped>
  306 +.plan-search-bar {
  307 + margin-bottom: 20px;
  308 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
  309 +}
  310 +
  311 +.search-bar {
  312 + display: flex;
  313 + align-items: center;
  314 + gap: 10px;
  315 + padding: 15px 20px;
  316 +
  317 +}
  318 +
  319 +.search-buttons {
  320 + display: flex;
  321 + gap: 10px;
  322 + margin-left: 10px;
  323 +}
  324 +
  325 +.pagination-wrapper {
  326 + display: flex;
  327 + padding: 12px 16px;
  328 + justify-content: flex-end;
  329 +}
  330 +
309 /* 新样式:更美观、不变大小、不影响其他布局 */ 331 /* 新样式:更美观、不变大小、不影响其他布局 */
310 .cover-img { 332 .cover-img {
311 width: 100px; 333 width: 100px;
@@ -41,6 +41,7 @@ @@ -41,6 +41,7 @@
41 41
42 <!-- 自重加重/减重 --> 42 <!-- 自重加重/减重 -->
43 <th v-if="type === 4 || type === 5" width="120">重量</th> 43 <th v-if="type === 4 || type === 5" width="120">重量</th>
  44 + <th v-if="type === 4 || type === 5" width="120">次数</th>
44 <th v-if="type === 4 || type === 5" width="140">休息时间(秒)</th> 45 <th v-if="type === 4 || type === 5" width="140">休息时间(秒)</th>
45 46
46 <th v-if="type === 6" width="140">组数</th> 47 <th v-if="type === 6" width="140">组数</th>
@@ -134,6 +135,10 @@ @@ -134,6 +135,10 @@
134 @change="val => handleFieldChange(setIndex, 'weight', val)" /> 135 @change="val => handleFieldChange(setIndex, 'weight', val)" />
135 </td> 136 </td>
136 <td v-if="type === 4 || type === 5"> 137 <td v-if="type === 4 || type === 5">
  138 + <el-input-number :model-value="set.reps" :min="0" style="width: 80%"
  139 + @change="val => handleFieldChange(setIndex, 'reps', val)" />
  140 + </td>
  141 + <td v-if="type === 4 || type === 5">
137 <el-input-number :model-value="set.restTime" :min="0" style="width: 80%" 142 <el-input-number :model-value="set.restTime" :min="0" style="width: 80%"
138 @change="val => handleFieldChange(setIndex, 'restTime', val)" /> 143 @change="val => handleFieldChange(setIndex, 'restTime', val)" />
139 </td> 144 </td>
@@ -217,7 +222,8 @@ watch(() => props.type, () => initAll(), { immediate: true }) @@ -217,7 +222,8 @@ watch(() => props.type, () => initAll(), { immediate: true })
217 222
218 // 新增 223 // 新增
219 const addSet = () => { 224 const addSet = () => {
220 - const newSet = { weight: 0, reps: 0, duration: 0, distance: 0, restTime: 0 } 225 + const newSetIndex = props.sets.length + 1;
  226 + const newSet = { weight: 0, reps: 0, duration: 0, distance: 0, restTime: 0, setIndex: newSetIndex }
221 const newSets = [...props.sets, newSet] 227 const newSets = [...props.sets, newSet]
222 emit('update:sets', newSets) 228 emit('update:sets', newSets)
223 } 229 }
@@ -226,11 +232,17 @@ const addSet = () => { @@ -226,11 +232,17 @@ const addSet = () => {
226 const delSet = (index) => { 232 const delSet = (index) => {
227 const newSets = [...props.sets] 233 const newSets = [...props.sets]
228 newSets.splice(index, 1) 234 newSets.splice(index, 1)
229 - emit('update:sets', newSets) 235 + const reorderedSets = newSets.map((item, i) => ({
  236 + ...item,
  237 + setIndex: i + 1
  238 + }))
  239 +
  240 + emit('update:sets', reorderedSets)
230 } 241 }
231 </script> 242 </script>
232 243
233 <style scoped> 244 <style scoped>
  245 +/* 表格 */
234 .sets-table-wrapper { 246 .sets-table-wrapper {
235 margin: 16px 0; 247 margin: 16px 0;
236 } 248 }
@@ -244,4 +256,42 @@ const delSet = (index) => { @@ -244,4 +256,42 @@ const delSet = (index) => {
244 width: 100%; 256 width: 100%;
245 border-collapse: collapse; 257 border-collapse: collapse;
246 } 258 }
  259 +
  260 +/* 表格容器 */
  261 +.sets-table-wrapper {
  262 + margin: 20px 0;
  263 + overflow-x: auto;
  264 +}
  265 +
  266 +.table-title {
  267 + margin-bottom: 10px;
  268 + font-size: 14px;
  269 + font-weight: 500;
  270 + color: #666;
  271 +}
  272 +
  273 +/* 表格核心样式 */
  274 +.sets-table {
  275 + width: 100%;
  276 + text-align: center;
  277 + border-collapse: collapse;
  278 +}
  279 +
  280 +.sets-table th,
  281 +.sets-table td {
  282 + padding: 12px 8px;
  283 + text-align: center;
  284 + vertical-align: middle;
  285 + border: 1px solid #ddd;
  286 +
  287 +}
  288 +
  289 +.sets-table th {
  290 + font-weight: 500;
  291 + background: #fafafa;
  292 +}
  293 +
  294 +.set-row:hover {
  295 + background: #f9f9f9;
  296 +}
247 </style> 297 </style>
@@ -36,48 +36,44 @@ @@ -36,48 +36,44 @@
36 </el-form-item> 36 </el-form-item>
37 37
38 <!-- 这里实现选择超级组或者动作组名称 --> 38 <!-- 这里实现选择超级组或者动作组名称 -->
39 - <el-form-item label-suffix="*" :label="unit.unitType === 1 ? '动作组名称' : '超级组名称'">  
40 - <el-select v-model="unit.id" @change="(val) => onSelectItem(unitIndex, val)">  
41 - <template v-if="unit.unitType === 1">  
42 - <!-- 需要接入动作接口 -->  
43 - <el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" />  
44 - </template>  
45 - <!-- 需要接入超级组接口 -->  
46 - <template v-else>  
47 - <el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" />  
48 - </template>  
49 - </el-select>  
50 - </el-form-item>  
51 -  
52 - <!-- 动作组头部 动作组名称+删除和增加动作组按钮-->  
53 - <div class="unit-header">  
54 - <el-form-item>  
55 - <el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1">  
56 - <template #icon>  
57 - <Minus /> 39 + <el-form-item label-suffix="*" :label="unit.unitType === 1 ? '动作组名称' : '超级组名称'" class="row-form-item">
  40 + <div class="row-wrap">
  41 + <el-select v-model="unit.id" @change="(val) => onSelectItem(unitIndex, val)">
  42 + <template v-if="unit.unitType === 1">
  43 + <!-- 需要接入动作接口 -->
  44 + <el-option v-for="item in exercisesList" :key="item.id" :label="item.name" :value="item.id" />
58 </template> 45 </template>
59 - 删除单元  
60 - </el-button>  
61 - </el-form-item>  
62 -  
63 - <el-form-item>  
64 - <el-button @click="AddUnit()">  
65 - <template #icon>  
66 - <Plus /> 46 + <!-- 需要接入超级组接口 -->
  47 + <template v-else>
  48 + <el-option v-for="item in superGroupList" :key="item.id" :label="item.name" :value="item.id" />
67 </template> 49 </template>
68 - 新增单元  
69 - </el-button>  
70 - </el-form-item>  
71 - </div>  
72 - 50 + </el-select>
  51 + <!-- 动作组头部 动作组名称+删除和增加动作组按钮-->
  52 + <div class="unit-header">
  53 + <el-button @click="delUnit(unitIndex)" :disabled="formData.units.length <= 1">
  54 + <template #icon>
  55 + <Minus />
  56 + </template>
  57 + 删除单元
  58 + </el-button>
  59 + <el-button @click="AddUnit()">
  60 + <template #icon>
  61 + <Plus />
  62 + </template>
  63 + 新增单元
  64 + </el-button>
  65 + </div>
  66 + </div>
  67 + </el-form-item>
73 <!-- 排序 --> 68 <!-- 排序 -->
74 <el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`"> 69 <el-form-item label="排序" :prop="`units[${unitIndex}].sortOrder`">
75 <el-input v-model="unit.sortOrder" min="1" max="99" /> 70 <el-input v-model="unit.sortOrder" min="1" max="99" />
76 </el-form-item> 71 </el-form-item>
  72 +
77 <!-- 循环:动作组 1 个、超级组 N 个,都会渲染 --> 73 <!-- 循环:动作组 1 个、超级组 N 个,都会渲染 -->
78 <template v-for="(item, idx) in unit.exercises" :key="idx"> 74 <template v-for="(item, idx) in unit.exercises" :key="idx">
79 <div style="margin: 16px 0;"> 75 <div style="margin: 16px 0;">
80 - <div>动作名称:{{ getExerciseName(item.exerciseId) }}</div> 76 + <div class="action-name">动作名称:{{ getExerciseName(item.exerciseId) }}</div>
81 <ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" /> 77 <ExerciseSetTable :type="item.exerciseType" :sets="item.sets" @update:sets="val => (item.sets = val)" />
82 </div> 78 </div>
83 </template> 79 </template>
@@ -96,15 +92,15 @@ @@ -96,15 +92,15 @@
96 </template> 92 </template>
97 <script setup lang="ts"> 93 <script setup lang="ts">
98 import { ref, onMounted, reactive, nextTick } from 'vue' 94 import { ref, onMounted, reactive, nextTick } from 'vue'
99 -import lostImg from '@/assets/imgs/lost.png'  
100 import { MusclesApi } from '@/api/store/training/muscle' 95 import { MusclesApi } from '@/api/store/training/muscle'
101 import { ElMessage, ElMessageBox } from 'element-plus' 96 import { ElMessage, ElMessageBox } from 'element-plus'
102 import { Minus, Plus } from '@element-plus/icons-vue' 97 import { Minus, Plus } from '@element-plus/icons-vue'
103 import type { FormInstance } from 'element-plus' 98 import type { FormInstance } from 'element-plus'
104 -import './TemplateAdd.css' 99 +// import './TemplateAdd.css'
105 import { TrainingTemplatesApi } from '@/api/store/training/templates' 100 import { TrainingTemplatesApi } from '@/api/store/training/templates'
106 import { ExercisesApi } from '@/api/store/training/pose' 101 import { ExercisesApi } from '@/api/store/training/pose'
107 import ExerciseSetTable from '../templates/ExerciseSetTable.vue' 102 import ExerciseSetTable from '../templates/ExerciseSetTable.vue'
  103 +import { SupersetsApi } from '@/api/store/training/Supersets'
108 104
109 //定义参数 105 //定义参数
110 const formRef = ref<FormInstance>() 106 const formRef = ref<FormInstance>()
@@ -114,7 +110,7 @@ const router = useRouter() @@ -114,7 +110,7 @@ const router = useRouter()
114 const route = useRoute() 110 const route = useRoute()
115 const exercisesList = ref<any[]>([]) 111 const exercisesList = ref<any[]>([])
116 const superGroupList = ref<any[]>([]) 112 const superGroupList = ref<any[]>([])
117 -import { SupersetsApi } from '@/api/store/training/Supersets' 113 +
118 114
119 // 表单数据 115 // 表单数据
120 const formData = reactive({ 116 const formData = reactive({
@@ -451,7 +447,7 @@ const handleSubmit = async () => { @@ -451,7 +447,7 @@ const handleSubmit = async () => {
451 447
452 loading.value = true 448 loading.value = true
453 try { 449 try {
454 - // 👇 按接口文档转换提交数据 450 + // 按接口文档转换提交数据
455 const submitData = { 451 const submitData = {
456 templateId: formData.id, // 关键:前端id → 后端templateId 452 templateId: formData.id, // 关键:前端id → 后端templateId
457 groupId: formData.groupId, 453 groupId: formData.groupId,
@@ -543,3 +539,122 @@ watch( @@ -543,3 +539,122 @@ watch(
543 { immediate: true } 539 { immediate: true }
544 ) 540 )
545 </script> 541 </script>
  542 +
  543 +<style scoped>
  544 +:deep(.el-form-item__label) {
  545 + float: none !important;
  546 + width: 68px !important;
  547 + padding: 0 !important;
  548 + margin-right: 20px !important;
  549 + line-height: 32px;
  550 + text-align: right;
  551 + white-space: nowrap;
  552 +}
  553 +
  554 +/* 让表单内容区域横向排列 */
  555 +.row-form-item :deep(.el-form-item__content) {
  556 + display: flex;
  557 + align-items: center;
  558 + width: 100%;
  559 +}
  560 +
  561 +/* 下拉框 + 按钮 同行容器 */
  562 +.row-wrap {
  563 + display: flex;
  564 + align-items: center;
  565 + gap: 14px;
  566 + width: 100%;
  567 +}
  568 +
  569 +/* 下拉框占剩余宽度 */
  570 +.sel {
  571 + flex: 1;
  572 +}
  573 +
  574 +/* 按钮容器:不被压缩 */
  575 +.unit-header {
  576 + display: flex;
  577 + gap: 10px;
  578 + flex-shrink: 0;
  579 +}
  580 +
  581 +/* 下面是你原来保留的样式,不动 */
  582 +
  583 +/* 动作组卡片 */
  584 +.unit-card {
  585 + margin: 20px 0;
  586 + border-radius: 6px;
  587 +}
  588 +
  589 +/* 表格容器 */
  590 +.sets-table-wrapper {
  591 + margin: 20px 0;
  592 + overflow-x: auto;
  593 +}
  594 +
  595 +.table-title {
  596 + margin-bottom: 10px;
  597 + font-size: 14px;
  598 + font-weight: 500;
  599 + color: #666;
  600 +}
  601 +
  602 +/* 表格样式 */
  603 +.sets-table {
  604 + width: 100%;
  605 + text-align: center;
  606 + border-collapse: collapse;
  607 +}
  608 +
  609 +.sets-table th {
  610 + padding: 10px 0;
  611 + font-weight: 500;
  612 + background: #fafafa;
  613 +}
  614 +
  615 +.sets-table td {
  616 + padding: 8px 0;
  617 +}
  618 +
  619 +.set-row:hover {
  620 + background: #f9f9f9;
  621 +}
  622 +
  623 +.action-name {
  624 + color: #666;
  625 +}
  626 +
  627 +/* 按钮居中容器 */
  628 +.btn-center-wrapper {
  629 + display: flex;
  630 + width: 100%;
  631 + padding-top: 15px;
  632 + margin-top: 30px;
  633 + border-top: 1px solid #eee;
  634 + justify-content: center;
  635 + gap: 12px;
  636 +}
  637 +
  638 +/* 封面图样式 */
  639 +.sub-image {
  640 + border: 1px solid #eee;
  641 + border-radius: 10px;
  642 +}
  643 +
  644 +/* 页面滚动容器 */
  645 +.page-contain {
  646 + height: 100vh;
  647 + overflow-y: auto;
  648 + background-color: #f5f5f5;
  649 + box-sizing: border-box;
  650 +}
  651 +
  652 +/* 表单容器 */
  653 +.templateName {
  654 + padding: 30px;
  655 + margin: 0 auto;
  656 + background: #fff;
  657 + border-radius: 8px;
  658 + box-shadow: 0 2px 12px rgb(0 0 0 / 5%);
  659 +}
  660 +</style>
1 <template> 1 <template>
2 <div class="page-container"> 2 <div class="page-container">
3 <!-- 顶部搜索与操作栏 --> 3 <!-- 顶部搜索与操作栏 -->
4 - <ContentWrap>  
5 - <div class="search-bar"> 4 + <ContentWrap class="plan-search-bar">
  5 + <div class="search-item">
6 <div class="search-left"> 6 <div class="search-left">
7 <label class="search-label">模板名称: </label> 7 <label class="search-label">模板名称: </label>
8 <el-input class="search-input" placeholder="请输入" v-model="searchForm.name" @keyup.enter="handleSearch" /> 8 <el-input class="search-input" placeholder="请输入" v-model="searchForm.name" @keyup.enter="handleSearch" />
@@ -29,6 +29,7 @@ @@ -29,6 +29,7 @@
29 </div> 29 </div>
30 </div> 30 </div>
31 </ContentWrap> 31 </ContentWrap>
  32 +
32 <ContentWrap> 33 <ContentWrap>
33 <!-- 数据列表 --> 34 <!-- 数据列表 -->
34 <div class="table-container"> 35 <div class="table-container">
@@ -203,7 +204,7 @@ const load = async () => { @@ -203,7 +204,7 @@ const load = async () => {
203 // 初始化加载列表 204 // 初始化加载列表
204 onMounted(async () => { 205 onMounted(async () => {
205 fetchList() 206 fetchList()
206 - load() 207 + // load()
207 }) 208 })
208 </script> 209 </script>
209 210
@@ -212,6 +213,19 @@ onMounted(async () => { @@ -212,6 +213,19 @@ onMounted(async () => {
212 padding: 20px; 213 padding: 20px;
213 } 214 }
214 215
  216 +.plan-search-bar {
  217 + margin-bottom: 20px;
  218 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
  219 +}
  220 +
  221 +/* 2. 搜索项flex布局(核心,让标签、输入框、按钮横向对齐) */
  222 +.search-item {
  223 + display: flex;
  224 + align-items: center;
  225 + gap: 10px;
  226 + padding: 15px 20px;
  227 +}
  228 +
215 .search-bar { 229 .search-bar {
216 display: flex; 230 display: flex;
217 margin-right: 20px; 231 margin-right: 20px;
@@ -222,7 +236,7 @@ onMounted(async () => { @@ -222,7 +236,7 @@ onMounted(async () => {
222 } 236 }
223 237
224 .search-label { 238 .search-label {
225 - font-weight: 500; 239 + white-space: nowrap;
226 } 240 }
227 241
228 .search-input { 242 .search-input {
@@ -232,7 +246,8 @@ onMounted(async () => { @@ -232,7 +246,8 @@ onMounted(async () => {
232 246
233 .search-buttons { 247 .search-buttons {
234 display: flex; 248 display: flex;
235 - gap: 5px; 249 + gap: 10px;
  250 + margin-left: 10px;
236 } 251 }
237 252
238 .table-container { 253 .table-container {
@@ -242,8 +257,9 @@ onMounted(async () => { @@ -242,8 +257,9 @@ onMounted(async () => {
242 } 257 }
243 258
244 .pagination-wrapper { 259 .pagination-wrapper {
245 - margin-top: 20px;  
246 - text-align: right; 260 + display: flex;
  261 + padding: 12px 16px;
  262 + justify-content: flex-end;
247 } 263 }
248 264
249 .cover-wrapper { 265 .cover-wrapper {
1 <template> 1 <template>
2 <!-- 用具分类页面 ,在接口文档没有找到用具分类的接口,用了器械的接口 --> 2 <!-- 用具分类页面 ,在接口文档没有找到用具分类的接口,用了器械的接口 -->
3 - <ContentWrap> 3 + <ContentWrap class="plan-search-bar">
4 <!-- 搜索栏 --> 4 <!-- 搜索栏 -->
5 - <div class="search-bar">  
6 - <el-form :inline="true" :model="searchForm" class="demo-form-inline">  
7 - <el-form-item label="器械名称:">  
8 - <el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"  
9 - @keyup.enter="handleSearch" />  
10 - </el-form-item>  
11 - <el-form-item>  
12 - <el-button type="primary" @click="handleSearch" size="default">  
13 - <el-icon>  
14 - <Search />  
15 - </el-icon>  
16 - 搜索  
17 - </el-button>  
18 - <el-button @click="resetSearch" size="default">  
19 - <el-icon>  
20 - <Refresh />  
21 - </el-icon>  
22 - 重置  
23 - </el-button>  
24 - <el-button class="add-btn" @click="handleAdd" size="default">  
25 - <el-icon>  
26 - <Plus />  
27 - </el-icon>  
28 - 新增  
29 - </el-button>  
30 - </el-form-item>  
31 - </el-form> 5 + <!-- <div class="search-bar"> -->
  6 + <div class="search-item">
  7 + <label class="search-label">肌肉名称</label>
  8 + <el-input v-model="searchForm.name" placeholder="请输入" style="width: 200px;" size="default"
  9 + @keyup.enter="handleSearch" />
  10 + <div class="search-buttons">
  11 + <el-button type="primary" @click="handleSearch" size="default">
  12 + <el-icon>
  13 + <Search />
  14 + </el-icon>
  15 + 搜索
  16 + </el-button>
  17 + <el-button @click="resetSearch" size="default">
  18 + <el-icon>
  19 + <Refresh />
  20 + </el-icon>
  21 + 重置
  22 + </el-button>
  23 + <el-button type="primary" @click="handleAdd" size="default">
  24 + <el-icon>
  25 + <Plus />
  26 + </el-icon>
  27 + 新增
  28 + </el-button>
  29 + </div>
32 </div> 30 </div>
  31 + <!-- </div> -->
33 </ContentWrap> 32 </ContentWrap>
34 33
35 <ContentWrap> 34 <ContentWrap>
@@ -262,6 +261,24 @@ onMounted(() => { @@ -262,6 +261,24 @@ onMounted(() => {
262 background: #f5f7fa; 261 background: #f5f7fa;
263 } 262 }
264 263
  264 +.plan-search-bar {
  265 + margin-bottom: 20px;
  266 + box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
  267 +}
  268 +
  269 +.search-item {
  270 + display: flex;
  271 + align-items: center;
  272 + gap: 10px;
  273 + padding: 15px 20px;
  274 +}
  275 +
  276 +/* 3. 标签样式(和计划页一致) */
  277 +.search-label {
  278 + white-space: nowrap;
  279 +}
  280 +
  281 +
265 .add-btn { 282 .add-btn {
266 height: 32px; 283 height: 32px;
267 padding: 0 16px; 284 padding: 0 16px;
@@ -45,15 +45,8 @@ @@ -45,15 +45,8 @@
45 </el-form-item> 45 </el-form-item>
46 46
47 <el-form-item label="频率" prop="frequencyPerWeek" label-suffix="*"> 47 <el-form-item label="频率" prop="frequencyPerWeek" label-suffix="*">
48 - <el-select v-model="SonFormData.frequencyPerWeek">  
49 - <el-option label="不限" :value="0" />  
50 - <el-option label="1练/周" :value="1" />  
51 - <el-option label="2练/周" :value="2" />  
52 - <el-option label="3练/周" :value="3" />  
53 - <el-option label="4练/周" :value="4" />  
54 - <el-option label="5练/周" :value="5" />  
55 - <el-option label="6练/周" :value="6" />  
56 - <el-option label="7练/周" :value="7" /> 48 + <el-select v-model="SonFormData.frequencyPerWeek" :disabled="templateCount > 1">
  49 + <el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" />
57 </el-select> 50 </el-select>
58 </el-form-item> 51 </el-form-item>
59 52
@@ -209,6 +202,39 @@ const templateDetailMap = ref({}) @@ -209,6 +202,39 @@ const templateDetailMap = ref({})
209 // 模板详情加载状态 202 // 模板详情加载状态
210 const templateLoading = ref({}) 203 const templateLoading = ref({})
211 204
  205 +// 控制练的频率
  206 +const templateCount = computed(() => SonFormData.value.templateIds.length)
  207 +// 频率选项:1个模板显示全部,多个模板只显示对应数量
  208 +const frequencyOptions = computed(() => {
  209 + const count = templateCount.value
  210 + if (count === 1) {
  211 + return [
  212 + { label: '1练/周', value: 1 },
  213 + { label: '2练/周', value: 2 },
  214 + { label: '3练/周', value: 3 },
  215 + { label: '4练/周', value: 4 },
  216 + { label: '5练/周', value: 5 },
  217 + { label: '6练/周', value: 6 },
  218 + ]
  219 + } else {
  220 + // 多个模板 → 只显示和模板数量一样的选项
  221 + return [{ label: `${count}练/周`, value: count }]
  222 + }
  223 +})
  224 +
  225 +const syncFrequency = () => {
  226 + const count = templateCount.value
  227 + if (count > 1) {
  228 + // 超过1个模板 → 强制频率 = 模板数量
  229 + SonFormData.value.frequencyPerWeek = count
  230 + }
  231 +}
  232 +
  233 +// 监听模板数量变化,自动更新频率
  234 +watch(templateCount, () => {
  235 + syncFrequency()
  236 +})
  237 +
212 const formRules = reactive({ 238 const formRules = reactive({
213 name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }], 239 name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
214 durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }], 240 durationWeeks: [{ required: true, message: '请输入计划周期', trigger: 'blur' }],
@@ -309,6 +335,7 @@ watch(dialogVisible, async (val) => { @@ -309,6 +335,7 @@ watch(dialogVisible, async (val) => {
309 loadTemplateDetail(index, item.templateId) 335 loadTemplateDetail(index, item.templateId)
310 } 336 }
311 }) 337 })
  338 + syncFrequency()
312 } 339 }
313 formRef.value?.clearValidate() 340 formRef.value?.clearValidate()
314 }) 341 })
@@ -399,6 +426,7 @@ const Addtemplate = (index) => { @@ -399,6 +426,7 @@ const Addtemplate = (index) => {
399 templateDetailMap.value[i] = oldMap[i] 426 templateDetailMap.value[i] = oldMap[i]
400 } 427 }
401 }) 428 })
  429 + syncFrequency()
402 } 430 }
403 431
404 const deltemplate = (index) => { 432 const deltemplate = (index) => {
@@ -413,7 +441,7 @@ const deltemplate = (index) => { @@ -413,7 +441,7 @@ const deltemplate = (index) => {
413 templateDetailMap.value[i] = oldMap[i] 441 templateDetailMap.value[i] = oldMap[i]
414 } 442 }
415 }) 443 })
416 - 444 + syncFrequency()
417 } 445 }
418 onMounted(() => { 446 onMounted(() => {
419 loadTemplateList() 447 loadTemplateList()