|
|
|
<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%;
|
|
|
|
}
|
...
|
...
|
|