|
|
|
<!--
|
|
|
|
定义训练动作新增页面(xunji-dongzuo-xinzeng.vue),核心用于创建 / 新增自定义体能训练动作,位置在训记的动作右侧抽屉点击新增动作就是这个页面.
|
|
|
|
支持训练部位、动作类型、目录分类、封面 / 视频等多维度配置,最终提交保存到后端接口,适配移动端多端(小程序 / APP/H5)
|
|
|
|
-->
|
|
|
|
<template>
|
|
|
|
<view class="add-custom-exercise-page">
|
|
|
|
<!-- 肌肉部位图 -->
|
|
...
|
...
|
@@ -41,11 +37,19 @@ |
|
|
|
<view class="form-section">
|
|
|
|
<!-- 动作名称 -->
|
|
|
|
<view class="form-item">
|
|
|
|
<!-- <view class="label">动作名称</view>
|
|
|
|
<view class="input-group">
|
|
|
|
<input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称"
|
|
|
|
placeholder-class="input-placeholder" />
|
|
|
|
</view> -->
|
|
|
|
<view class="action-name">
|
|
|
|
<view class="label">动作名称</view>
|
|
|
|
<view class="input-group">
|
|
|
|
<input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称"
|
|
|
|
placeholder-class="input-placeholder" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<view class="upload-btn" @click="openCoverSelector">
|
|
|
|
<image :src="coverImagePath" class="preview-media" mode="aspectFill" />
|
|
|
|
<text class="btn-text">更换封面</text>
|
|
...
|
...
|
@@ -67,7 +71,7 @@ |
|
|
|
<text class="value">{{ actionType || '请选择' }}</text>
|
|
|
|
<!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
|
|
|
|
class="arrow-icon" mode="aspectFill" /> -->
|
|
|
|
<up-icon name="arrow-right" color="#333" size="16"></up-icon>
|
|
|
|
<up-icon v-if="!isEdit" name="arrow-right" color="#333" size="16"></up-icon>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
...
|
...
|
@@ -121,6 +125,10 @@ |
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<view class="form-item" v-if="isEdit" @click="deleteAction">
|
|
|
|
<view class="center-label">删除动作</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 保存按钮 -->
|
|
|
|
<view class="save-button">
|
|
|
|
<button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button>
|
|
...
|
...
|
@@ -176,6 +184,7 @@ |
|
|
|
|
|
|
|
<script setup>
|
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
import { onLoad } from '@dcloudio/uni-app';
|
|
|
|
import ExercisesApi from '@/sheep/api/motion/exercises';
|
|
|
|
import LeftmotionApi from '@/sheep/api/motion/equipments';
|
|
|
|
import EquipmentListApi from '@/sheep/api/motion/motionequipments';
|
|
...
|
...
|
@@ -185,6 +194,7 @@ const exerciseName = ref(''); |
|
|
|
const actionType = ref();
|
|
|
|
const directoryName = ref();
|
|
|
|
const selectedMusclesDisplay = ref('未选择');
|
|
|
|
const editActionId = ref(null)
|
|
|
|
|
|
|
|
// 选中索引
|
|
|
|
const actionTypeIndex = ref(0);
|
|
...
|
...
|
@@ -211,6 +221,145 @@ const actionTypeOptions = ref([ |
|
|
|
// console.error('加载部位失败', err);
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
exerciseName.value = ''
|
|
|
|
actionType.value = ''
|
|
|
|
actionTypeIndex.value = 0
|
|
|
|
directoryName.value = ''
|
|
|
|
bodyPartIndex.value = 0
|
|
|
|
equipmentIndex.value = 0
|
|
|
|
selectedMusclesDisplay.value = '未选择'
|
|
|
|
primaryMuscles.value = []
|
|
|
|
secondaryMuscles.value = []
|
|
|
|
coverImagePath.value = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
|
|
|
|
coverUploaded.value = false
|
|
|
|
videoFilePath.value = ''
|
|
|
|
videoUploaded.value = false
|
|
|
|
isVideoFile.value = false
|
|
|
|
urlTutorial.value = ''
|
|
|
|
videoDetailName.value = ''
|
|
|
|
// 同时关闭所有弹窗,防止上次弹窗残留
|
|
|
|
closeAllPopups()
|
|
|
|
}
|
|
|
|
|
|
|
|
const isEdit = ref(false)
|
|
|
|
const actionDetail = ref({});
|
|
|
|
|
|
|
|
onLoad(async (options) => {
|
|
|
|
|
|
|
|
isEdit.value = options.isEdit === 'true'
|
|
|
|
const editId = Number(options.id)
|
|
|
|
editActionId.value = editId
|
|
|
|
console.log('编辑状态', isEdit, '编辑动作ID', editId)
|
|
|
|
// 清空表单状态
|
|
|
|
resetForm()
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
loadbodyPartOptions(),
|
|
|
|
loadequipmentOptions(),
|
|
|
|
loadMuscleGroups()
|
|
|
|
])
|
|
|
|
if (isEdit.value) {
|
|
|
|
loadexercisedetail(editId)
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
// 加载单个动作详情
|
|
|
|
// const loadexercisedetail = async (id) => {
|
|
|
|
// if (!id || !Number(id)) return
|
|
|
|
// try {
|
|
|
|
// const response = await ExercisesApi.getExerciseById(id);
|
|
|
|
// actionDetail.value = response.data;
|
|
|
|
// console.log('打印动作详情', actionDetail.value);
|
|
|
|
// } catch (err) {
|
|
|
|
// console.error('加载动作详情失败:', err)
|
|
|
|
// actionDetail.value = {}
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
|
|
|
|
// 加载单个动作详情并回显表单
|
|
|
|
const loadexercisedetail = async (id) => {
|
|
|
|
if (!id || !Number(id)) return
|
|
|
|
try {
|
|
|
|
const response = await ExercisesApi.getExerciseById(id);
|
|
|
|
actionDetail.value = response.data;
|
|
|
|
const detail = actionDetail.value
|
|
|
|
console.log('打印动作详情', detail);
|
|
|
|
|
|
|
|
// 1. 动作名称
|
|
|
|
exerciseName.value = detail.name || ''
|
|
|
|
|
|
|
|
// 2. 动作类型回显(id匹配,编辑只读)
|
|
|
|
const targetType = actionTypeOptions.value.find(item => item.id === detail.exerciseType)
|
|
|
|
if (targetType) {
|
|
|
|
actionTypeIndex.value = actionTypeOptions.value.findIndex(item => item.id === detail.exerciseType)
|
|
|
|
actionType.value = targetType.name
|
|
|
|
}
|
|
|
|
|
|
|
|
// 3. 目录:身体部位categoryId + 器械equipmentId 匹配下标
|
|
|
|
const bodyIdx = bodyPartOptions.value.findIndex(item => item.id === detail.categoryId)
|
|
|
|
if (bodyIdx > -1) bodyPartIndex.value = bodyIdx
|
|
|
|
|
|
|
|
const equipIdx = equipmentOptions.value.findIndex(item => item.id === detail.equipmentId)
|
|
|
|
if (equipIdx > -1) equipmentIndex.value = equipIdx
|
|
|
|
// 拼接目录展示文字
|
|
|
|
const selBody = bodyPartOptions.value[bodyIdx]
|
|
|
|
const selEquip = equipmentOptions.value[equipIdx]
|
|
|
|
if (selBody && selEquip) {
|
|
|
|
directoryName.value = ` ${selBody.name}( ${selEquip.name})`
|
|
|
|
}
|
|
|
|
|
|
|
|
// 4. 肌肉部位:后端返回字符串 "[30,28,29]" 转数字数组
|
|
|
|
if (detail.primaryMuscles) {
|
|
|
|
primaryMuscles.value = JSON.parse(detail.primaryMuscles)
|
|
|
|
} else {
|
|
|
|
primaryMuscles.value = []
|
|
|
|
}
|
|
|
|
if (detail.secondaryMuscles) {
|
|
|
|
secondaryMuscles.value = JSON.parse(detail.secondaryMuscles)
|
|
|
|
} else {
|
|
|
|
secondaryMuscles.value = []
|
|
|
|
}
|
|
|
|
// 直接使用后端返回的拼接好的肌肉名称展示文本,不用重新拼接
|
|
|
|
const primaryStr = detail.primaryMuscleNames?.length ? detail.primaryMuscleNames.join('、') + '(主)' : ''
|
|
|
|
const secondaryStr = detail.secondaryMuscleNames?.length ? detail.secondaryMuscleNames.join('、') + '(次)' : ''
|
|
|
|
let display = ''
|
|
|
|
if (primaryStr) display += primaryStr
|
|
|
|
if (secondaryStr) {
|
|
|
|
if (display) display += ', '
|
|
|
|
display += secondaryStr
|
|
|
|
}
|
|
|
|
selectedMusclesDisplay.value = display || '未选择'
|
|
|
|
|
|
|
|
// 5. 封面图 url3dAnimation
|
|
|
|
if (detail.url3dAnimation) {
|
|
|
|
coverImagePath.value = detail.url3dAnimation
|
|
|
|
coverUploaded.value = true // 标记已有封面,跳过新增封面校验
|
|
|
|
}
|
|
|
|
|
|
|
|
// 6. 动作实拍视频/图片 urlRealPerson
|
|
|
|
if (detail.urlRealPerson) {
|
|
|
|
videoFilePath.value = detail.urlRealPerson
|
|
|
|
videoUploaded.value = true
|
|
|
|
// 简单判断是否视频(后端返回mp4则标记video)
|
|
|
|
isVideoFile.value = detail.urlRealPerson.includes('.mp4')
|
|
|
|
}
|
|
|
|
|
|
|
|
// 7. 详解视频 urlTutorial
|
|
|
|
if (detail.urlTutorial) {
|
|
|
|
urlTutorial.value = detail.urlTutorial
|
|
|
|
videoDetailName.value = '已选择详解视频'
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
console.error('加载动作详情失败:', err)
|
|
|
|
actionDetail.value = {}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 肌肉部位列表(从接口获取)
|
|
|
|
const muscleGroups = ref([]);
|
|
|
|
// 加载肌肉部位
|
|
...
|
...
|
@@ -253,6 +402,7 @@ const showVideoSelector = ref(false); |
|
|
|
|
|
|
|
// 弹窗控制
|
|
|
|
const openActionTypePicker = () => {
|
|
|
|
if (isEdit.value) return;
|
|
|
|
showActionTypePicker.value = true;
|
|
|
|
};
|
|
|
|
const openDirectoryPicker = () => {
|
|
...
|
...
|
@@ -394,7 +544,6 @@ const saveExercise = async () => { |
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('------------------');
|
|
|
|
console.log('--', equipmentOptions.value[equipmentIndex.value]);
|
|
|
|
console.log('--', bodyPartOptions.value[bodyPartIndex.value]);
|
|
|
|
// 获取 categoryId 和 equipmentId
|
|
...
|
...
|
@@ -435,14 +584,23 @@ const saveExercise = async () => { |
|
|
|
urlRealPerson: videoFilePath.value, //真人实拍视频
|
|
|
|
urlTutorial: urlTutorial.value, // 详解视频
|
|
|
|
};
|
|
|
|
// 编辑追加id
|
|
|
|
if (isEdit.value) {
|
|
|
|
payload.id = editActionId.value
|
|
|
|
}
|
|
|
|
console.log('【发送的 payload】', payload);
|
|
|
|
|
|
|
|
let res;
|
|
|
|
try {
|
|
|
|
const res = await ExercisesApi.createExercise(payload);
|
|
|
|
console.log('【接口返回】', res);
|
|
|
|
if (isEdit.value) {
|
|
|
|
res = await ExercisesApi.updateexercises(payload);
|
|
|
|
console.log('【更新接口返回】', res);
|
|
|
|
} else {
|
|
|
|
res = await ExercisesApi.createExercise(payload);
|
|
|
|
console.log('【创建接口返回】', res);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
|
|
|
// 可选:返回上一页或清空表单
|
|
|
|
uni.showToast({ title: isEdit.value ? '编辑成功' : '保存成功', icon: 'success' });
|
|
|
|
setTimeout(() => {
|
|
|
|
uni.navigateBack();
|
|
|
|
}, 1000);
|
|
...
|
...
|
@@ -455,6 +613,29 @@ const saveExercise = async () => { |
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 删除动作
|
|
|
|
const deleteAction = () => {
|
|
|
|
uni.showModal({
|
|
|
|
title: '提示',
|
|
|
|
content: '确定要删除该动作吗?删除后无法恢复',
|
|
|
|
success: async (res) => {
|
|
|
|
if (res.confirm) {
|
|
|
|
try {
|
|
|
|
const delRes = await ExercisesApi.deleteexercises(editActionId.value)
|
|
|
|
if (delRes.code === 0) {
|
|
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
|
|
setTimeout(() => uni.navigateBack(), 1000)
|
|
|
|
} else {
|
|
|
|
uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' })
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
uni.showToast({ title: '删除请求失败', icon: 'none' })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const openMusclePicker = () => {
|
|
|
|
showMusclePicker.value = true;
|
|
|
|
};
|
|
...
|
...
|
@@ -504,9 +685,9 @@ const confirmMuscleSelection = () => { |
|
|
|
};
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
loadbodyPartOptions();
|
|
|
|
loadequipmentOptions();
|
|
|
|
loadMuscleGroups();
|
|
|
|
// loadbodyPartOptions();
|
|
|
|
// loadequipmentOptions();
|
|
|
|
// loadMuscleGroups();
|
|
|
|
// loadexercisesGroups();
|
|
|
|
});
|
|
|
|
</script>
|
|
...
|
...
|
@@ -602,12 +783,21 @@ onMounted(() => { |
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
.action-name {
|
|
|
|
width: 285rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.label {
|
|
|
|
font-size: 32rpx;
|
|
|
|
color: #333;
|
|
|
|
margin-bottom: 10rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.center-label {
|
|
|
|
text-align: center;
|
|
|
|
color: #c44b4b;
|
|
|
|
}
|
|
|
|
|
|
|
|
.input-group {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
...
|
...
|
|