Authored by Bad

fix

... ... @@ -23,17 +23,17 @@ export interface ExercisesVO {
// 创建/更新动作的请求参数结构
export interface ExercisesSaveReqVO {
id?: number
name: string
categoryId: number
equipmentId: number
exerciseType: number
name?: string
categoryId?: number
equipmentId?: number
exerciseType?: number
urlImage?: string
url3dAnimation: string
urlRealPerson: string
urlTutorial: string
stepDescription: string
primaryMuscles: ''
secondaryMuscles: ''
url3dAnimation?: string
urlRealPerson?: string
urlTutorial?: string
stepDescription?: string
primaryMuscles?: string[]
secondaryMuscles?: string[]
}
// 分页/导出查询参数
... ...
<template>
<!-- 直接用 defineModel 生成的 dialogVisible 做 v-model -->
<el-dialog v-model="dialogVisible" :title="title" width="800px" append-to-body>
<!-- 表单直接绑定 formDataModel(自动双向同步) -->
<el-form ref="formRef" :model="formDataModel" :rules="formRules" label-width="100px">
<el-form-item label="动作名称:" prop="name" label-suffix="*">
<el-input v-model="formDataModel.name" placeholder="请输入动作名称" maxlength="50" show-word-limit
style="width: 100%" />
<!-- 采用芋道全局封装的 Dialog 组件,内置滚动和自适应处理 -->
<Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-form-item label="动作名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入动作名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="动作类型:" prop="exerciseType" label-suffix="*">
<el-select v-model="formDataModel.exerciseType" placeholder="请选择动作类型" style="width: 100%">
<el-form-item label="动作类型" prop="exerciseType">
<el-select v-model="formData.exerciseType" placeholder="请选择动作类型" class="w-full">
<el-option label="无氧运动" :value="0" />
<el-option label="有氧运动" :value="1" />
<el-option label="计次训练" :value="2" />
... ... @@ -18,58 +28,77 @@
<el-option label="间歇训练" :value="6" />
</el-select>
</el-form-item>
<!-- 部位分类:下拉选择框 -->
<el-form-item label="部位分类:" prop="categoryId" label-suffix="*">
<el-select v-model="formDataModel.categoryId" placeholder="请选择部位分类" style="width: 100%"
:loading="categoryList.length === 0">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="Number(item.id)" />
<el-form-item label="部位分类" prop="categoryId">
<el-select v-model="formData.categoryId" placeholder="请选择部位分类" class="w-full">
<el-option
v-for="item in categoryList"
:key="item.id"
:label="item.name"
:value="Number(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="用具分类:" prop="equipmentId" label-suffix="*">
<el-select v-model="formDataModel.equipmentId" placeholder="请选择用具分类" style="width: 100%">
<el-form-item label="用具分类" prop="equipmentId">
<el-select v-model="formData.equipmentId" placeholder="请选择用具分类" class="w-full">
<el-option v-for="item in toolList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</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 primaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" />
<el-form-item label="主要训练部位" prop="primaryMuscles">
<el-select
v-model="formData.primaryMuscles"
multiple
placeholder="请选择主要训练部位"
class="w-full"
>
<el-option v-for="item in muscleList" :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 secondaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" />
<el-form-item label="次要训练部位" prop="secondaryMuscles">
<el-select
v-model="formData.secondaryMuscles"
multiple
placeholder="请选择次要训练部位"
class="w-full"
>
<el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<!-- 锻炼部位图 -->
<el-form-item label="锻炼部位图" prop="urlImage" class="form-item-margin upload-img-item">
<UploadImg v-model="formDataModel.urlImage" />
<el-form-item label="锻炼部位图" prop="urlImage">
<UploadImg v-model="formData.urlImage" />
</el-form-item>
<el-form-item label="3D动画地址" prop="url3dAnimation" label-suffix="*" class="upload-img-item upload-3d-item">
<UploadImg v-model="formDataModel.url3dAnimation" />
<el-form-item label="3D动画地址" prop="url3dAnimation">
<UploadImg v-model="formData.url3dAnimation" />
</el-form-item>
<el-form-item label="真人演示地址" prop="urlRealPerson" label-suffix="*">
<UploadVideo v-model="formDataModel.urlRealPerson" />
<el-form-item label="真人演示地址" prop="urlRealPerson">
<UploadVideo v-model="formData.urlRealPerson" />
</el-form-item>
<el-form-item label="教程地址" prop="urlTutorial" label-suffix="*">
<UploadVideo v-model="formDataModel.urlTutorial" />
<el-form-item label="教程地址" prop="urlTutorial">
<UploadVideo v-model="formData.urlTutorial" />
</el-form-item>
<el-form-item label="动作步骤描述:" prop="stepDescription" label-suffix="*" class="desc-item">
<Editor v-model="formDataModel.stepDescription" placeholder="请输入动作步骤描述" />
<el-form-item label="动作步骤描述" prop="stepDescription">
<Editor v-model="formData.stepDescription" placeholder="请输入动作步骤描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="handleSubmit">保存</el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确定</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</el-dialog>
</Dialog>
</template>
<script setup>
import { ref, reactive, watch, onMounted, nextTick, computed } from 'vue'
import { ElMessage } from 'element-plus'
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { MotionCategoryApi } from '@/api/store/training/partCategory'
import { MusclesApi } from '@/api/store/training/muscle'
import { EquipmentsApi } from '@/api/store/training/tool'
... ... @@ -77,204 +106,113 @@ 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, // 新增这一行
default: () => ({
id: undefined,
name: '',
categoryId: undefined,
equipmentId: undefined,
exerciseType: undefined,
// 封面图
urlImage: '',
url3dAnimation: '',
urlRealPerson: '',
urlTutorial: '',
stepDescription: '',
primaryMuscles: [],
secondaryMuscles: []
})
})
const muscleList = ref([]) // 肌肉列表(弹窗选择用)
const categoryList = ref([]) // 部位分类列表(弹窗选择用)
const toolList = ref([]) // 用具列表(弹窗选择用)
// 加这个监听,看是否收到父组件的 visible=true
watch(dialogVisible, async (newVal) => {
console.log('子组件收到 visible 变化:', newVal)
if (newVal) {
// 弹窗打开时,重新加载列表,确保选项数据存在
await Promise.all([
loadCategoryList(),
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()
}
})
}
})
// 接收父组件传的标题(普通 prop)
const props = defineProps({
title: { type: String, required: true, default: '新增动作' }
defineOptions({ name: 'ActionFormDialog' })
const message = useMessage() // 引入芋道全局提示组件
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref('') // 弹窗标题
const formLoading = ref(false) // 遮罩状态
const formType = ref('') // 表单行为:'create' 或 'update'
const formRef = ref<FormInstance>()
const formData = ref<any>({
id: undefined,
name: '',
categoryId: undefined,
equipmentId: undefined,
exerciseType: undefined,
urlImage: '',
url3dAnimation: '',
urlRealPerson: '',
urlTutorial: '',
stepDescription: '',
primaryMuscles: [],
secondaryMuscles: []
})
// 只定义提交事件(取消/关闭由 v-model 自动处理)
const emit = defineEmits(['submit'])
// 表单相关
const formLoading = ref(false)
const formRef = ref()
// 基础下拉选项数据集
const muscleList = ref<any[]>([])
const categoryList = ref<any[]>([])
const toolList = ref<any[]>([])
// 表单校验规则(不变)
const formRules = reactive({
const formRules = reactive<FormRules>({
name: [{ required: true, message: '请输入动作名称', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }],
equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }],
exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }],
primaryMuscles: [{ required: true, message: '请输入主要训练部位', trigger: 'change' }],
secondaryMuscles: [{ required: true, message: '请输入次要训练部位', trigger: 'change' }],
urlImage: [{ required: false, message: '请上传图片', trigger: 'blur' }],
primaryMuscles: [{ required: true, message: '请选择主要训练部位', trigger: 'change' }],
secondaryMuscles: [{ required: true, message: '请选择次要训练部位', trigger: 'change' }],
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))
})
// 安全解析 JSON,避免前端异常崩溃
const safeParseArray = (val: any): any[] => {
if (!val) return []
if (Array.isArray(val)) return val
try {
const parsed = typeof val === 'string' ? JSON.parse(val) : val
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
// 异步按需加载字典数据
const loadOptions = async () => {
if (muscleList.value.length === 0) {
const res = await MusclesApi.getMusclesPage({ pageNo: 1, pageSize: 100, name: '' })
muscleList.value = res.data?.list || res.list || []
}
if (categoryList.value.length === 0) {
const res = await MotionCategoryApi.getMotionCategoryPage({ pageNo: 1, pageSize: 100, name: '' })
categoryList.value = res.data?.list || res.list || []
}
if (toolList.value.length === 0) {
const res = await EquipmentsApi.getEquipmentsPage({ pageNo: 1, pageSize: 100, name: '' })
toolList.value = res.data?.list || res.list || []
}
}
// 取消按钮:直接关闭弹窗(自动同步给父组件)
const handleCancel = () => {
// 暴露给父组件调用的 open 方法(标准芋道设计模式)
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'create' ? '新增动作' : '编辑动作'
formType.value = type
resetForm()
dialogVisible.value = false // 自动触发 update:visible,父组件数据同步更新
}
// 提交表单:只需要把数据传给父组件
const handleSubmit = async () => {
if (!formRef.value) return
try {
console.log('123456');
// 1. 加载分类等选择数据
await loadOptions()
await formRef.value.validate()
console.log('123456');
// 2. 如果是编辑动作,异步拉取最新后端行数据并格式化回显
if (id) {
formLoading.value = true
console.log('原始primaryMuscles:', formDataModel.value.primaryMuscles)
console.log('原始secondaryMuscles:', formDataModel.value.secondaryMuscles)
const form = formDataModel.value
const submitData = {
...formDataModel.value,
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('动作新增成功!')
try {
const data = await ExercisesApi.getExercises(id)
console.log(data,"getExercises");
// 处理回显:将后端保存的 String (如 "[1,2]") 格式化为 el-select 所需的 Array
data.primaryMuscles = safeParseArray(JSON.parse(data.primaryMuscles || '[]'))
data.secondaryMuscles = safeParseArray(JSON.parse(data.secondaryMuscles || '[]'))
formData.value = data
} catch (err) {
console.error(err)
} finally {
formLoading.value = false
}
// 触发提交事件,数据已通过 v-model 同步,直接传即可
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, {
formData.value = {
id: undefined,
name: '',
categoryId: undefined,
equipmentId: undefined,
exerciseType: 0,
exerciseType: undefined,
urlImage: '',
url3dAnimation: '',
urlRealPerson: '',
... ... @@ -282,131 +220,53 @@ const resetForm = () => {
stepDescription: '',
primaryMuscles: [],
secondaryMuscles: []
})
}
formRef.value?.resetFields()
}
//=====================加载数据====================
//加载肌肉列表(弹窗选择肌肉)
const loadMuscleList = async () => {
try {
const res = await MusclesApi.getMusclesPage({ pageNo: '1', pageSize: '100', name: '' })
const data = res.data || res
muscleList.value = data.list || []
} catch (err) {
console.error('加载肌肉列表失败:', err)
ElMessage.error('加载肌肉列表失败')
}
}
// 暴露 open 方法供父组件利用 ref 调用
defineExpose({ open })
/**
* 加载部位分类列表(弹窗选择部位分类)
*/
const loadCategoryList = async () => {
try {
const res = await MotionCategoryApi.getMotionCategoryPage({ pageNo: '1', pageSize: '100', name: '' })
const data = res.data || res
categoryList.value = data.list || []
console.log('部位分类:', categoryList.value)
} catch (err) {
console.error('加载部位分类失败:', err)
ElMessage.error('加载部位分类失败')
}
}
// 定义 success 回调事件,向父页面告知操作结果
const emit = defineEmits(['success'])
const loadToolList = async () => {
// 表单提交保存
const submitForm = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
formLoading.value = true
try {
const res = await EquipmentsApi.getEquipmentsPage({ pageNo: '1', pageSize: '100', name: '' })
const data = res.list || []
toolList.value = data
console.log('用具分类:', toolList.value)
// 组装提交参数:将选中数组 JSON 序列化为后端所需的 String 格式
const submitData = {
...formData.value,
primaryMuscles: JSON.stringify(formData.value.primaryMuscles || []),
secondaryMuscles: JSON.stringify(formData.value.secondaryMuscles || [])
}
console.log(submitData,"submitData");
if (formType.value === 'create') {
await ExercisesApi.addExercises(submitData)
message.success('新增成功')
} else {
await ExercisesApi.updateExercises(submitData)
message.success('修改成功')
}
dialogVisible.value = false
emit('success') // 触发父页面重新加载列表
} catch (err) {
console.error('加载用具列表失败:', err)
ElMessage.error('加载用具列表失败')
console.error(err)
} finally {
formLoading.value = false
}
}
onMounted(() => {
loadMuscleList()
loadCategoryList()
loadToolList()
})
// 暴露重置方法
defineExpose({ resetForm })
</script>
<style scoped>
/* 1. 强制表单使用 flex 布局,label 和内容水平并排 */
:deep(.el-form-item) {
display: flex !important;
align-items: flex-start !important;
}
/* 2. 统一设置 label 宽度和对齐方式,同时给右侧留固定间距 */
:deep(.el-form-item__label) {
float: none !important;
width: 120px !important;
padding: 0 !important;
margin-right: 20px !important;
line-height: 32px;
text-align: right;
white-space: nowrap;
}
/* 3. 让内容区域自动占满剩余宽度 */
:deep(.el-form-item__content) {
margin-left: 0 !important;
line-height: 32px;
flex: 1;
}
/* 4. 给所有上传类表单项单独适配垂直对齐 */
: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;
height: 120px;
cursor: pointer;
background: #f7f8fa;
border: 1px dashed #dcdfe6;
border-radius: 4px;
flex-direction: column;
align-items: center;
justify-content: center;
}
.plus {
margin-bottom: 4px;
font-size: 28px;
color: #999;
}
.tip {
font-size: 12px;
color: #999;
}
.url-text {
margin-left: 10px;
line-height: 120px;
/* 使用简单的全局通用间距配置,移除过多深层样式覆盖 */
.w-full {
width: 100%;
}
</style>
\ No newline at end of file
... ...
<template>
<!-- 整个页面容器 -->
<div class="page-container">
<!-- 搜索栏区域:匹配参考图布局 -->
<!-- 搜索栏区域:芋道行内标准表单 -->
<ContentWrap>
<div class="search-bar-wrapper">
<div class="search-row">
<span class="search-label">动作名称</span>
<el-input v-model="searchForm.name" placeholder="请输入" class="search-input" clearable
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px" class="-mb-15px">
<el-form-item label="动作名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入动作名称" clearable class="!w-240px"
@keyup.enter="handleQuery" />
<el-button type="primary" class="search-btn" @click="handleQuery">
<el-icon>
<Search />
</el-icon>
搜索
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button class="reset-btn" @click="resetQuery">
<el-icon>
<Refresh />
</el-icon>
重置
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button class="add-btn" @click="handleAdd">
<el-icon>
<Plus />
</el-icon>
新增
<el-button type="primary" plain @click="openForm('create')">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</div>
</div>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 表格区域 -->
<ContentWrap>
<el-table :data="tableData" stripe style="width: 100%" v-loading="loading" empty-text="暂无动作数据"
<el-table v-loading="loading" :data="list" stripe style="width: 100%" empty-text="暂无动作数据"
: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="url3dAnimation" label="封面" align="center" min-width="120">
<template #default="scope">
<div class="cover-wrapper">
<!-- 优先显示行数据封面,无则显示占位图 -->
<img class="cover-img" :src="scope.row.url3dAnimation || lostImg" alt="模板封面" />
<el-image class="cover-img" :src="scope.row.url3dAnimation || lostImg"
:preview-src-list="scope.row.url3dAnimation ? [scope.row.url3dAnimation] : [lostImg]" preview-teleported
fit="cover" />
</div>
</template>
</el-table-column>
<!-- 用具分类 -->
<el-table-column label="用具分类" prop="equipmentName" min-width="100" />
<!-- 部位分类 -->
<el-table-column label="部位分类" min-width="100" prop="categoryName" />
<el-table-column prop="createTime" label="创建时间" min-width="180" :formatter="dateFormatter" />
<el-table-column prop="updateTime" label="更新时间" min-width="180" :formatter="dateFormatter" />
<!-- 操作列 -->
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button type="text" class="text-blue-600" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" class="text-red-600" @click="handleDelete(scope.row)">删除</el-button>
<el-button link type="primary" @click="openForm('update', scope.row.id)">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-wrapper" style="padding: 12px 16px;">
<!-- <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>
<Pagination v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total"
@pagination="getList" />
</ContentWrap>
<!-- 引入独立的弹窗组件 -->
<template>
<ActionFormDialog v-model:visible="dialogVisible" v-model:formData="formData" :title="dialogTitle"
@submit="handleFormSubmit" />
</template>
<!-- 独立的弹窗组件:不再需要复杂的双向绑定 -->
<ActionFormDialog ref="formRef" @success="getList" />
</div>
</template>
<script setup lang="ts">
// 导入依赖模块
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { dateFormatter } from '@/utils/formatTime' // 工具函数:时间格式化
import lostImg from '@/assets/imgs/lost.png' // 丢失图片占位图
// 导入新创建的弹窗组件
import { dateFormatter } from '@/utils/formatTime'
import lostImg from '@/assets/imgs/lost.png'
import ActionFormDialog from '@/views/store/training/pose-category/ActionFormDialog.vue'
import { ExercisesApi, type PageParam, type ExercisesVO, type ExercisesSaveReqVO } from '@/api/store/training/pose'
import { ExercisesApi } from '@/api/store/training/pose'
// 组件名称定义
defineOptions({ name: 'SystemActionManage' })
// 表格核心数据模块
const loading = ref<boolean>(false) // 表格加载状态
const tableData = ref<ExercisesVO[]>([]) // 表格数据源
const total = ref<number>(0) // 数据总条数
const message = useMessage() // 芋道消息组件
// 分页参数模块
const pagination = reactive<{ pageNo: number; pageSize: number }>({
pageNo: 1, // 当前页码
pageSize: 10 // 每页条数
})
const queryFormRef = ref()
const formRef = ref() // 弹窗组件的 ref 句柄
const loading = ref<boolean>(false)
const list = ref<any[]>([])
const total = ref<number>(0)
// 动作名称搜索关键词
const searchForm = reactive<{ name: string }>({
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: ''
})
// 弹窗相关模块(简化)
const dialogVisible = ref<boolean>(false) // 弹窗显隐控制
const dialogTitle = ref('新增动作') // 弹窗标题(新增/编辑)
const formData = reactive<ExercisesSaveReqVO>({ // 表单数据模型
id: undefined,
name: '',
categoryId: undefined,
equipmentId: undefined,
exerciseType: undefined,
urlImage: '',
url3dAnimation: '',
urlRealPerson: '',
urlTutorial: '',
stepDescription: '',
primaryMuscles: '',
secondaryMuscles: ''
})
// 查询动作列表(核心接口调用)
const handleQuery = async () => {
// 获取列表数据
const getList = async () => {
loading.value = true
try {
const params: PageParam = { pageNo: pagination.pageNo, pageSize: pagination.pageSize, name: searchForm.name }
const res = await ExercisesApi.getExercisesPage(params)
console.log('✅ 动作接口请求成功:', res)
tableData.value = res.list || []
const res = await ExercisesApi.getExercisesPage(queryParams)
list.value = res.list || []
total.value = res.total || 0
} catch (err) {
console.error('查询失败:', err)
ElMessage.error('查询失败,请稍后重试')
tableData.value = []
console.error(err)
list.value = []
total.value = 0
} finally {
loading.value = false
}
}
// 重置搜索条件
const resetQuery = () => {
searchForm.name = ''
pagination.pageNo = 1
handleQuery()
// 触发查询
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
// 新增动作:打开弹窗
const handleAdd = () => {
console.log('点击新增,准备打开弹窗') // 加日志调试
// 重置表单数据
resetFormData()
dialogTitle.value = '新增动作'
dialogVisible.value = true
// 重置查询条件
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
// 编辑动作:打开弹窗并回显数据
const handleEdit = (row: ExercisesVO) => {
// 重置表单数据后赋值
resetFormData()
// ✅ 关键修复:把后端返回的字符串转成数组,再赋值
const formatRow = {
...row,
// 解析 JSON 字符串为数组
primaryMuscles: row.primaryMuscles ? JSON.parse(row.primaryMuscles) : [],
secondaryMuscles: row.secondaryMuscles ? JSON.parse(row.secondaryMuscles) : []
}
Object.assign(formData, formatRow)
dialogTitle.value = '编辑动作'
dialogVisible.value = true
console.log('点击新增,dialogVisible =', dialogVisible.value) // 加日志调试
// 标准打开弹窗方式(通过组件 ref 句柄触发)
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
// 删除动作:确认后调用删除接口
const handleDelete = async (row: ExercisesVO) => {
// 标准删除动作(采用 useMessage)
const handleDelete = async (id: number) => {
try {
await ElMessageBox.confirm(`确定要删除动作「${row.name}」吗?删除后不可恢复!`, '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
})
await ExercisesApi.deleteExercises(row.id)
ElMessage.success('删除成功')
handleQuery()
await message.confirm('确定要删除当前动作吗?删除后不可恢复!')
await ExercisesApi.deleteExercises(id)
message.success('删除成功')
getList()
} catch (err) {
if (err !== 'cancel') {
console.error('删除失败:', err)
ElMessage.error('删除失败,请稍后重试')
}
console.warn(err)
}
}
// 处理表单提交(新增/编辑)
// 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()
dialogVisible.value = false
resetFormData()
}
// 关闭弹窗
// const handleDialogClose = () => {
// dialogVisible.value = false
// resetFormData()
// }
// 重置表单数据
const resetFormData = () => {
Object.assign(formData, {
id: undefined,
name: '',
categoryId: 0,
equipmentId: 0,
exerciseType: 0,
url3dAnimation: '',
urlRealPerson: '',
urlTutorial: '',
stepDescription: '',
primaryMuscles: [],
secondaryMuscles: []
})
}
//------------------分页模块-----------------
// 每页条数变更事件
const handleSizeChange = (val: number) => {
pagination.pageSize = val
handleQuery()
}
// 页码变更事件
const handleCurrentChange = (val: number) => {
pagination.pageNo = val
handleQuery()
}
// 图片加载失败处理(备用图)
const handleImgError = (e: Event) => {
const img = e.target as HTMLImageElement
img.src = lostImg
}
// 生命周期模块
onMounted(async () => {
handleQuery()
// 生命周期挂载
onMounted(() => {
getList()
})
</script>
<style scoped>
/* 页面样式模块(保持不变) */
.page-container {
width: 100%;
padding: 16px;
... ... @@ -285,101 +147,21 @@ onMounted(async () => {
box-sizing: border-box;
}
.search-bar-wrapper {
padding: 16px;
margin-bottom: 16px;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%)
}
.search-row {
display: flex;
align-items: center;
gap: 12px;
}
.search-label {
font-size: 14px;
color: #303133;
white-space: nowrap;
}
.search-input {
width: 280px;
height: 32px;
}
.search-btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
color: #fff;
background-color: #409eff;
border-color: #409eff;
}
.reset-btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
color: #606266;
background-color: #fff;
border-color: #dcdfe6;
}
.add-btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
color: #409eff;
background-color: #fff;
border-color: #409eff;
}
.table-container {
padding: 16px;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
}
.cover-wrap {
.cover-wrapper {
display: flex;
width: 60px;
height: 60px;
width: 100px;
height: 50px;
margin: 0 auto;
border: 1px solid #e4e7ed;
border-radius: 4px;
border-radius: 6px;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: #f5f7fa;
}
.cover-img {
width: 100px;
height: 50px !important;
border-radius: 6px;
object-fit: cover;
}
.pagination-wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 16px;
}
.goto-text {
margin-right: 12px;
font-size: 14px;
color: #606266;
}
.text-blue-600 {
color: #1677ff;
}
.text-red-600 {
color: #f5222d;
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
... ...