Authored by Bad

feat: 添加获取计划分类下拉列表的API调用并优化相关组件

... ... @@ -54,7 +54,10 @@ export const trainingPlanApi = {
// 获取计划下拉列表
getPlanList: async () => {
return await request.get({ url: `/plan/simple-list`, })
},
// 获取计划分类下拉列表
getPlanCategoryList: async () => {
return await request.get({ url: `/plan/category/simple-list`, })
}
}
... ...
<template>
<!-- 采用芋道全局封装的 Dialog 组件,内置滚动和自适应处理 -->
<!-- 采用芋道全局封装的 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">
... ... @@ -70,27 +70,23 @@
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
<script setup>
import { MotionCategoryApi } from '@/api/store/training/partCategory'
import { MusclesApi } from '@/api/store/training/muscle'
import { EquipmentsApi } from '@/api/store/training/tool'
import { Editor } from '@/components/Editor'
import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue'
import { ExercisesApi } from '@/api/store/training/pose'
defineOptions({ name: 'ActionFormDialog' })
const message = useMessage() // 引入芋道全局提示组件
const message = useMessage() // 芋道全局提示组件
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref('') // 弹窗标题
const formLoading = ref(false) // 遮罩状态
const formType = ref('') // 表单行为:'create' 或 'update'
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formRef = ref<FormInstance>()
const formData = ref<any>({
const formRef = ref()
const formData = ref({
id: undefined,
name: '',
categoryId: undefined,
... ... @@ -106,37 +102,34 @@ const formData = ref<any>({
})
// 基础下拉选项数据集
const muscleList = ref<any[]>([])
const categoryList = ref<any[]>([])
const toolList = ref<any[]>([])
const muscleList = ref([])
const categoryList = ref([])
const toolList = ref([])
// 可选主要训练部位:排除已选次要部位
const primaryAvailableMuscles = computed(() => {
const secondaryIds = new Set(formData.value.secondaryMuscles ?? [])
return muscleList.value.filter(item => !secondaryIds.has(item.id))
return muscleList.value.filter((item) => !secondaryIds.has(item.id))
})
// 可选次要训练部位:排除已选主要部位
const secondaryAvailableMuscles = computed(() => {
const primaryIds = new Set(formData.value.primaryMuscles ?? [])
return muscleList.value.filter(item => !primaryIds.has(item.id))
return muscleList.value.filter((item) => !primaryIds.has(item.id))
})
const formRules = reactive<FormRules>({
// 表单校验规则(已移除 primaryMuscles、secondaryMuscles、urlRealPerson、url3dAnimation 的必填规则)
const formRules = reactive({
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' }],
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' }],
stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }]
})
// 安全解析 JSON,避免前端异常崩溃
const safeParseArray = (val: any): any[] => {
// 安全解析 JSON 字符串/数组,容错处理
const safeParseArray = (val) => {
if (!val) return []
if (Array.isArray(val)) return val
try {
... ... @@ -152,26 +145,19 @@ const loadOptions = async () => {
if (muscleList.value.length === 0) {
const res = await MusclesApi.getsubCategoriesList()
muscleList.value = res || []
console.log('细分锻炼部位列表/肌肉res:', res);
console.log('muscleList 赋值后', muscleList.value)
}
if (categoryList.value.length === 0) {
const res = await MotionCategoryApi.getCategoriesList()
categoryList.value = res || []
console.log('部位列表res:', res);
console.log('categoryList 赋值后', categoryList.value)
}
if (toolList.value.length === 0) {
const res = await EquipmentsApi.getEquipmentList()
toolList.value = res || []
console.log('器械列表res:', res);
console.log('categoryList 赋值后', categoryList.value)
}
}
// 暴露给父组件调用的 open 方法(标准芋道设计模式)
const open = async (type: string, id?: number) => {
// 暴露给父组件调用的 open 方法
const open = async (type, id) => {
dialogVisible.value = true
dialogTitle.value = type === 'create' ? '新增动作' : '编辑动作'
formType.value = type
... ... @@ -185,10 +171,9 @@ const open = async (type: string, id?: number) => {
formLoading.value = true
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 || '[]'))
// 回显处理:安全解析后端保存的数组或 JSON 字符串
data.primaryMuscles = safeParseArray(data.primaryMuscles)
data.secondaryMuscles = safeParseArray(data.secondaryMuscles)
formData.value = data
} catch (err) {
console.error(err)
... ... @@ -231,15 +216,13 @@ const submitForm = async () => {
formLoading.value = true
try {
// 组装提交参数:将选中数组 JSON 序列化为后端所需的 String 格式
// 组装提交参数:即使未选择部位,也会转为空数组 JSON 字符串 "[]" 传给后端
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('新增成功')
... ... @@ -248,7 +231,7 @@ const submitForm = async () => {
message.success('修改成功')
}
dialogVisible.value = false
emit('success') // 触发父页面重新加载列表
emit('success')
} catch (err) {
console.error(err)
} finally {
... ... @@ -258,7 +241,6 @@ const submitForm = async () => {
</script>
<style scoped>
/* 使用简单的全局通用间距配置,移除过多深层样式覆盖 */
.w-full {
width: 100%;
}
... ...
... ... @@ -66,7 +66,7 @@
</div>
</template>
<script setup lang="ts">
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { dateFormatter } from '@/utils/formatTime'
import lostImg from '@/assets/imgs/lost.png'
... ... @@ -78,10 +78,10 @@ defineOptions({ name: 'SystemActionManage' })
const message = useMessage() // 芋道消息组件
const queryFormRef = ref()
const formRef = ref() // 弹窗组件的 ref 句柄
const loading = ref<boolean>(false)
const list = ref<any[]>([])
const total = ref<number>(0)
const formRef = ref()
const loading = ref(false)
const list = ref([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
... ... @@ -119,12 +119,12 @@ const resetQuery = () => {
}
// 标准打开弹窗方式(通过组件 ref 句柄触发)
const openForm = (type: string, id?: number) => {
const openForm = (type, id) => {
formRef.value.open(type, id)
}
// 标准删除动作(采用 useMessage)
const handleDelete = async (id: number) => {
const handleDelete = async (id) => {
try {
await message.confirm('确定要删除当前动作吗?删除后不可恢复!')
await ExercisesApi.deleteExercises(id)
... ...
... ... @@ -298,8 +298,14 @@ const loadTemplateDetail = async (index, templateId) => {
}
const loadCategoryList = async () => {
const res = await trainingPlanApi.getPlanList()
try {
const res = await trainingPlanApi.getPlanCategoryList()
categoryList.value = res || []
} catch (err) {
console.log(err);
}
}
const reloadTemplateDetail = (index) => {
... ...