|
1
|
<template>
|
1
|
<template>
|
|
2
|
- <!-- 采用芋道全局封装的 Dialog 组件,内置滚动和自适应处理 -->
|
2
|
+ <!-- 采用芋道全局封装的 Dialog 组件 -->
|
|
3
|
<Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
|
3
|
<Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
|
|
4
|
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="120px">
|
4
|
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="120px">
|
|
5
|
<el-form-item label="动作名称" prop="name">
|
5
|
<el-form-item label="动作名称" prop="name">
|
|
@@ -70,27 +70,23 @@ |
|
@@ -70,27 +70,23 @@ |
|
70
|
</Dialog>
|
70
|
</Dialog>
|
|
71
|
</template>
|
71
|
</template>
|
|
72
|
|
72
|
|
|
73
|
-<script setup lang="ts">
|
|
|
|
74
|
-import { ref, reactive } from 'vue'
|
|
|
|
75
|
-import type { FormInstance, FormRules } from 'element-plus'
|
73
|
+<script setup>
|
|
76
|
import { MotionCategoryApi } from '@/api/store/training/partCategory'
|
74
|
import { MotionCategoryApi } from '@/api/store/training/partCategory'
|
|
77
|
import { MusclesApi } from '@/api/store/training/muscle'
|
75
|
import { MusclesApi } from '@/api/store/training/muscle'
|
|
78
|
import { EquipmentsApi } from '@/api/store/training/tool'
|
76
|
import { EquipmentsApi } from '@/api/store/training/tool'
|
|
79
|
-import { Editor } from '@/components/Editor'
|
|
|
|
80
|
-import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue'
|
|
|
|
81
|
import { ExercisesApi } from '@/api/store/training/pose'
|
77
|
import { ExercisesApi } from '@/api/store/training/pose'
|
|
82
|
|
78
|
|
|
83
|
defineOptions({ name: 'ActionFormDialog' })
|
79
|
defineOptions({ name: 'ActionFormDialog' })
|
|
84
|
|
80
|
|
|
85
|
-const message = useMessage() // 引入芋道全局提示组件
|
81
|
+const message = useMessage() // 芋道全局提示组件
|
|
86
|
|
82
|
|
|
87
|
-const dialogVisible = ref(false) // 弹窗可见性
|
|
|
|
88
|
-const dialogTitle = ref('') // 弹窗标题
|
|
|
|
89
|
-const formLoading = ref(false) // 遮罩状态
|
|
|
|
90
|
-const formType = ref('') // 表单行为:'create' 或 'update'
|
83
|
+const dialogVisible = ref(false)
|
|
|
|
84
|
+const dialogTitle = ref('')
|
|
|
|
85
|
+const formLoading = ref(false)
|
|
|
|
86
|
+const formType = ref('')
|
|
91
|
|
87
|
|
|
92
|
-const formRef = ref<FormInstance>()
|
|
|
|
93
|
-const formData = ref<any>({
|
88
|
+const formRef = ref()
|
|
|
|
89
|
+const formData = ref({
|
|
94
|
id: undefined,
|
90
|
id: undefined,
|
|
95
|
name: '',
|
91
|
name: '',
|
|
96
|
categoryId: undefined,
|
92
|
categoryId: undefined,
|
|
@@ -106,37 +102,34 @@ const formData = ref<any>({ |
|
@@ -106,37 +102,34 @@ const formData = ref<any>({ |
|
106
|
})
|
102
|
})
|
|
107
|
|
103
|
|
|
108
|
// 基础下拉选项数据集
|
104
|
// 基础下拉选项数据集
|
|
109
|
-const muscleList = ref<any[]>([])
|
|
|
|
110
|
-const categoryList = ref<any[]>([])
|
|
|
|
111
|
-const toolList = ref<any[]>([])
|
105
|
+const muscleList = ref([])
|
|
|
|
106
|
+const categoryList = ref([])
|
|
|
|
107
|
+const toolList = ref([])
|
|
112
|
|
108
|
|
|
113
|
// 可选主要训练部位:排除已选次要部位
|
109
|
// 可选主要训练部位:排除已选次要部位
|
|
114
|
const primaryAvailableMuscles = computed(() => {
|
110
|
const primaryAvailableMuscles = computed(() => {
|
|
115
|
const secondaryIds = new Set(formData.value.secondaryMuscles ?? [])
|
111
|
const secondaryIds = new Set(formData.value.secondaryMuscles ?? [])
|
|
116
|
- return muscleList.value.filter(item => !secondaryIds.has(item.id))
|
112
|
+ return muscleList.value.filter((item) => !secondaryIds.has(item.id))
|
|
117
|
})
|
113
|
})
|
|
118
|
|
114
|
|
|
119
|
// 可选次要训练部位:排除已选主要部位
|
115
|
// 可选次要训练部位:排除已选主要部位
|
|
120
|
const secondaryAvailableMuscles = computed(() => {
|
116
|
const secondaryAvailableMuscles = computed(() => {
|
|
121
|
const primaryIds = new Set(formData.value.primaryMuscles ?? [])
|
117
|
const primaryIds = new Set(formData.value.primaryMuscles ?? [])
|
|
122
|
- return muscleList.value.filter(item => !primaryIds.has(item.id))
|
118
|
+ return muscleList.value.filter((item) => !primaryIds.has(item.id))
|
|
123
|
})
|
119
|
})
|
|
124
|
|
120
|
|
|
125
|
-const formRules = reactive<FormRules>({
|
121
|
+// 表单校验规则(已移除 primaryMuscles、secondaryMuscles、urlRealPerson、url3dAnimation 的必填规则)
|
|
|
|
122
|
+const formRules = reactive({
|
|
126
|
name: [{ required: true, message: '请输入动作名称', trigger: 'blur' }],
|
123
|
name: [{ required: true, message: '请输入动作名称', trigger: 'blur' }],
|
|
127
|
categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }],
|
124
|
categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }],
|
|
128
|
equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }],
|
125
|
equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }],
|
|
129
|
exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }],
|
126
|
exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }],
|
|
130
|
- primaryMuscles: [{ required: true, message: '请选择主要训练部位', trigger: 'change' }],
|
|
|
|
131
|
- secondaryMuscles: [{ required: true, message: '请选择次要训练部位', trigger: 'change' }],
|
|
|
|
132
|
- urlRealPerson: [{ required: true, message: '请上传真人演示地址', trigger: 'blur' }],
|
|
|
|
133
|
urlTutorial: [{ required: true, message: '请上传教程地址', trigger: 'blur' }],
|
127
|
urlTutorial: [{ required: true, message: '请上传教程地址', trigger: 'blur' }],
|
|
134
|
- url3dAnimation: [{ required: true, message: '请上传3D动图', trigger: 'blur' }],
|
|
|
|
135
|
- stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }],
|
128
|
+ stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }]
|
|
136
|
})
|
129
|
})
|
|
137
|
|
130
|
|
|
138
|
-// 安全解析 JSON,避免前端异常崩溃
|
|
|
|
139
|
-const safeParseArray = (val: any): any[] => {
|
131
|
+// 安全解析 JSON 字符串/数组,容错处理
|
|
|
|
132
|
+const safeParseArray = (val) => {
|
|
140
|
if (!val) return []
|
133
|
if (!val) return []
|
|
141
|
if (Array.isArray(val)) return val
|
134
|
if (Array.isArray(val)) return val
|
|
142
|
try {
|
135
|
try {
|
|
@@ -152,26 +145,19 @@ const loadOptions = async () => { |
|
@@ -152,26 +145,19 @@ const loadOptions = async () => { |
|
152
|
if (muscleList.value.length === 0) {
|
145
|
if (muscleList.value.length === 0) {
|
|
153
|
const res = await MusclesApi.getsubCategoriesList()
|
146
|
const res = await MusclesApi.getsubCategoriesList()
|
|
154
|
muscleList.value = res || []
|
147
|
muscleList.value = res || []
|
|
155
|
- console.log('细分锻炼部位列表/肌肉res:', res);
|
|
|
|
156
|
- console.log('muscleList 赋值后', muscleList.value)
|
|
|
|
157
|
-
|
|
|
|
158
|
}
|
148
|
}
|
|
159
|
if (categoryList.value.length === 0) {
|
149
|
if (categoryList.value.length === 0) {
|
|
160
|
const res = await MotionCategoryApi.getCategoriesList()
|
150
|
const res = await MotionCategoryApi.getCategoriesList()
|
|
161
|
categoryList.value = res || []
|
151
|
categoryList.value = res || []
|
|
162
|
- console.log('部位列表res:', res);
|
|
|
|
163
|
- console.log('categoryList 赋值后', categoryList.value)
|
|
|
|
164
|
}
|
152
|
}
|
|
165
|
if (toolList.value.length === 0) {
|
153
|
if (toolList.value.length === 0) {
|
|
166
|
const res = await EquipmentsApi.getEquipmentList()
|
154
|
const res = await EquipmentsApi.getEquipmentList()
|
|
167
|
toolList.value = res || []
|
155
|
toolList.value = res || []
|
|
168
|
- console.log('器械列表res:', res);
|
|
|
|
169
|
- console.log('categoryList 赋值后', categoryList.value)
|
|
|
|
170
|
}
|
156
|
}
|
|
171
|
}
|
157
|
}
|
|
172
|
|
158
|
|
|
173
|
-// 暴露给父组件调用的 open 方法(标准芋道设计模式)
|
|
|
|
174
|
-const open = async (type: string, id?: number) => {
|
159
|
+// 暴露给父组件调用的 open 方法
|
|
|
|
160
|
+const open = async (type, id) => {
|
|
175
|
dialogVisible.value = true
|
161
|
dialogVisible.value = true
|
|
176
|
dialogTitle.value = type === 'create' ? '新增动作' : '编辑动作'
|
162
|
dialogTitle.value = type === 'create' ? '新增动作' : '编辑动作'
|
|
177
|
formType.value = type
|
163
|
formType.value = type
|
|
@@ -185,10 +171,9 @@ const open = async (type: string, id?: number) => { |
|
@@ -185,10 +171,9 @@ const open = async (type: string, id?: number) => { |
|
185
|
formLoading.value = true
|
171
|
formLoading.value = true
|
|
186
|
try {
|
172
|
try {
|
|
187
|
const data = await ExercisesApi.getExercises(id)
|
173
|
const data = await ExercisesApi.getExercises(id)
|
|
188
|
- console.log(data, "getExercises");
|
|
|
|
189
|
- // 处理回显:将后端保存的 String (如 "[1,2]") 格式化为 el-select 所需的 Array
|
|
|
|
190
|
- data.primaryMuscles = safeParseArray(JSON.parse(data.primaryMuscles || '[]'))
|
|
|
|
191
|
- data.secondaryMuscles = safeParseArray(JSON.parse(data.secondaryMuscles || '[]'))
|
174
|
+ // 回显处理:安全解析后端保存的数组或 JSON 字符串
|
|
|
|
175
|
+ data.primaryMuscles = safeParseArray(data.primaryMuscles)
|
|
|
|
176
|
+ data.secondaryMuscles = safeParseArray(data.secondaryMuscles)
|
|
192
|
formData.value = data
|
177
|
formData.value = data
|
|
193
|
} catch (err) {
|
178
|
} catch (err) {
|
|
194
|
console.error(err)
|
179
|
console.error(err)
|
|
@@ -231,15 +216,13 @@ const submitForm = async () => { |
|
@@ -231,15 +216,13 @@ const submitForm = async () => { |
|
231
|
|
216
|
|
|
232
|
formLoading.value = true
|
217
|
formLoading.value = true
|
|
233
|
try {
|
218
|
try {
|
|
234
|
- // 组装提交参数:将选中数组 JSON 序列化为后端所需的 String 格式
|
219
|
+ // 组装提交参数:即使未选择部位,也会转为空数组 JSON 字符串 "[]" 传给后端
|
|
235
|
const submitData = {
|
220
|
const submitData = {
|
|
236
|
...formData.value,
|
221
|
...formData.value,
|
|
237
|
primaryMuscles: JSON.stringify(formData.value.primaryMuscles || []),
|
222
|
primaryMuscles: JSON.stringify(formData.value.primaryMuscles || []),
|
|
238
|
secondaryMuscles: JSON.stringify(formData.value.secondaryMuscles || [])
|
223
|
secondaryMuscles: JSON.stringify(formData.value.secondaryMuscles || [])
|
|
239
|
}
|
224
|
}
|
|
240
|
|
225
|
|
|
241
|
- console.log(submitData, "submitData");
|
|
|
|
242
|
-
|
|
|
|
243
|
if (formType.value === 'create') {
|
226
|
if (formType.value === 'create') {
|
|
244
|
await ExercisesApi.addExercises(submitData)
|
227
|
await ExercisesApi.addExercises(submitData)
|
|
245
|
message.success('新增成功')
|
228
|
message.success('新增成功')
|
|
@@ -248,7 +231,7 @@ const submitForm = async () => { |
|
@@ -248,7 +231,7 @@ const submitForm = async () => { |
|
248
|
message.success('修改成功')
|
231
|
message.success('修改成功')
|
|
249
|
}
|
232
|
}
|
|
250
|
dialogVisible.value = false
|
233
|
dialogVisible.value = false
|
|
251
|
- emit('success') // 触发父页面重新加载列表
|
234
|
+ emit('success')
|
|
252
|
} catch (err) {
|
235
|
} catch (err) {
|
|
253
|
console.error(err)
|
236
|
console.error(err)
|
|
254
|
} finally {
|
237
|
} finally {
|
|
@@ -258,7 +241,6 @@ const submitForm = async () => { |
|
@@ -258,7 +241,6 @@ const submitForm = async () => { |
|
258
|
</script>
|
241
|
</script>
|
|
259
|
|
242
|
|
|
260
|
<style scoped>
|
243
|
<style scoped>
|
|
261
|
-/* 使用简单的全局通用间距配置,移除过多深层样式覆盖 */
|
|
|
|
262
|
.w-full {
|
244
|
.w-full {
|
|
263
|
width: 100%;
|
245
|
width: 100%;
|
|
264
|
}
|
246
|
}
|