|
|
|
<template>
|
|
|
|
<view class="add-custom-exercise-page">
|
|
|
|
<!-- 肌肉部位图 -->
|
|
|
|
<view class="muscle-area" @click="openMusclePicker">
|
|
|
|
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png"
|
|
|
|
class="muscle-image-front" mode="aspectFill" />
|
|
|
|
</view>
|
|
|
|
<!-- 肌肉部位选择弹窗 - 美化版 -->
|
|
|
|
<view v-if="showMusclePicker" class="popup-overlay" @click="closeAllPopups">
|
|
|
|
<view class="popup-content" @click.stop>
|
|
|
|
<view class="popup-header">
|
|
|
|
<view class="close-btn" @click="closeAllPopups">x</view>
|
|
|
|
<view class="title">选择训练部位</view>
|
|
|
|
<view class="confirm-btn" @click="confirmMuscleSelection">完成</view>
|
|
|
|
</view>
|
|
|
|
<view class="muscle-list-beautified">
|
|
|
|
<view v-for="group in muscleGroups" :key="group.id" class="muscle-item-beautified">
|
|
|
|
<text class="muscle-name">{{ group.name }}</text>
|
|
|
|
<!-- 主要部位按钮 -->
|
|
|
|
<view class="selector-btn primary" :class="{ active: primaryMuscles.includes(group.id) }"
|
|
|
|
@click.stop="togglePrimary(group.id)">
|
|
|
|
<view class="selector-dot primary"></view>
|
|
|
|
<text class="selector-label">主要</text>
|
|
|
|
</view>
|
|
|
|
<!-- 次要部位按钮 -->
|
|
|
|
<view class="selector-btn secondary" :class="{ active: secondaryMuscles.includes(group.id) }"
|
|
|
|
@click.stop="toggleSecondary(group.id)">
|
|
|
|
<view class="selector-dot secondary"></view>
|
|
|
|
<text class="selector-label">次要</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="page">
|
|
|
|
<!-- ==================== 肌肉部位图 ==================== -->
|
|
|
|
<view class="muscle-card" @click="showMusclePicker = true">
|
|
|
|
<image class="muscle-img"
|
|
|
|
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg"
|
|
|
|
mode="aspectFill" />
|
|
|
|
<view class="muscle-tip">
|
|
|
|
<up-icon name="edit-pen" color="#fff" size="14" />
|
|
|
|
<text>点击选择训练部位</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 表单区域 -->
|
|
|
|
<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 class="form-card">
|
|
|
|
<!-- 动作名称 + 封面 -->
|
|
|
|
<view class="form-row name-row">
|
|
|
|
<view class="name-input-area">
|
|
|
|
<text class="form-label">动作名称</text>
|
|
|
|
<input v-model="exerciseName" class="name-input" placeholder="点击此处填写动作名称" placeholder-class="ph" />
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<view class="upload-btn" @click="openCoverSelector">
|
|
|
|
<image :src="coverImagePath" class="preview-media" mode="aspectFill" />
|
|
|
|
<text class="btn-text">更换封面</text>
|
|
|
|
<view class="cover-upload" @click="openCoverSelector">
|
|
|
|
<image :src="coverImagePath" class="cover-preview" mode="aspectFill" />
|
|
|
|
<text class="cover-label">更换封面</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<!-- 新增:训练部位 -->
|
|
|
|
<view class="form-item" @click="openMusclePicker">
|
|
|
|
<view class="label">训练部位</view>
|
|
|
|
<view class="value-wrapper">
|
|
|
|
<text class="value">{{ selectedMusclesDisplay }}</text>
|
|
|
|
<up-icon name="arrow-right" color="#333" size="16"></up-icon>
|
|
|
|
|
|
|
|
<!-- 训练部位 -->
|
|
|
|
<view class="form-row select-row" @click="showMusclePicker = true">
|
|
|
|
<text class="form-label">训练部位</text>
|
|
|
|
<view class="select-value">
|
|
|
|
<text class="sel-text">{{ selectedMusclesDisplay }}</text>
|
|
|
|
<up-icon name="arrow-right" color="#999" size="14" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 动作类型 -->
|
|
|
|
<view class="form-item" @click="openActionTypePicker">
|
|
|
|
<view class="label">动作类型</view>
|
|
|
|
<view class="value-wrapper">
|
|
|
|
<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 v-if="!isEdit" name="arrow-right" color="#333" size="16"></up-icon>
|
|
|
|
<view class="form-row select-row" @click="openActionTypePicker">
|
|
|
|
<text class="form-label">动作类型</text>
|
|
|
|
<view class="select-value">
|
|
|
|
<text class="sel-text">{{ actionType || '请选择' }}</text>
|
|
|
|
<up-icon v-if="!isEdit" name="arrow-right" color="#999" size="14" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 目录管理 -->
|
|
|
|
<view class="form-item" @click="openDirectoryPicker">
|
|
|
|
<view class="label">目录管理</view>
|
|
|
|
<view class="value-wrapper">
|
|
|
|
<text class="value">{{ directoryName || '请选择' }}</text>
|
|
|
|
<up-icon name="arrow-right" color="#333" size="16"></up-icon>
|
|
|
|
<view class="form-row select-row" @click="showDirectoryPicker = true">
|
|
|
|
<text class="form-label">目录管理</text>
|
|
|
|
<view class="select-value">
|
|
|
|
<text class="sel-text">{{ directoryName || '请选择' }}</text>
|
|
|
|
<up-icon name="arrow-right" color="#999" size="14" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 动作视频 -->
|
|
|
|
<view class="form-item">
|
|
|
|
<view class="label">动作视频</view>
|
|
|
|
<view class="value-wrapper">
|
|
|
|
<text class="value">(非必填)</text>
|
|
|
|
</view>
|
|
|
|
<view class="upload-btn" @click="openVideoSelector">
|
|
|
|
<!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
|
|
|
|
class="camera-icon" mode="aspectFill" />videoFilePath.value -->
|
|
|
|
<view class="form-row media-row">
|
|
|
|
<text class="form-label">动作视频 <text class="optional">(非必填)</text></text>
|
|
|
|
<view class="media-upload" @click="openVideoSelector">
|
|
|
|
<template v-if="videoFilePath">
|
|
|
|
<video v-if="isVideoFile" :src="videoFilePath" class="preview-media" :controls="false"
|
|
|
|
:show-fullscreen-btn="false" :enable-progress-gesture="false"></video>
|
|
|
|
<!-- 图片类型 -->
|
|
|
|
<image v-else :src="videoFilePath" class="preview-media" mode="aspectFill" />
|
|
|
|
<video v-if="isVideoFile" :src="videoFilePath" class="media-preview" :controls="false"
|
|
|
|
:show-fullscreen-btn="false" :enable-progress-gesture="false" />
|
|
|
|
<image v-else :src="videoFilePath" class="media-preview" mode="aspectFill" />
|
|
|
|
<view class="media-replace-btn">重新选择</view>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<up-icon name="camera" color="#333" size="16"></up-icon>
|
|
|
|
<text class="btn-text">图片/视频</text>
|
|
|
|
<up-icon name="camera" color="#999" size="28" />
|
|
|
|
<text class="media-placeholder">图片 / 视频</text>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 视频详解 -->
|
|
|
|
<view class="form-item" @click="chooseTutorialVideo">
|
|
|
|
<view class="label">视频详解</view>
|
|
|
|
<view class="value-wrapper">
|
|
|
|
<text class="value">{{ videoDetailName || '上传详解视频(非必填)' }}</text>
|
|
|
|
<view class="upload-btn">
|
|
|
|
<view class="form-row media-row">
|
|
|
|
<text class="form-label">视频详解 <text class="optional">(非必填)</text></text>
|
|
|
|
<view class="media-upload" @click="chooseTutorialVideo">
|
|
|
|
<template v-if="urlTutorial">
|
|
|
|
<video :src="urlTutorial" class="preview-media" :controls="false" :show-fullscreen-btn="false"
|
|
|
|
:enable-progress-gesture="false"></video>
|
|
|
|
<video :src="urlTutorial" class="media-preview" :controls="false" :show-fullscreen-btn="false"
|
|
|
|
:enable-progress-gesture="false" />
|
|
|
|
<view class="media-replace-btn">重新选择</view>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<up-icon name="camera" color="#333" size="16" />
|
|
|
|
<text class="btn-text">添加视频</text>
|
|
|
|
<up-icon name="play-right" color="#999" size="28" />
|
|
|
|
<text class="media-placeholder">添加详解视频</text>
|
|
|
|
</template>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<view class="form-item" v-if="isEdit" @click="deleteAction">
|
|
|
|
<view class="center-label">删除动作</view>
|
|
|
|
<!-- 删除动作(编辑模式) -->
|
|
|
|
<view v-if="isEdit" class="form-row delete-row" @click="deleteAction">
|
|
|
|
<up-icon name="trash" color="#e74c3c" size="18" />
|
|
|
|
<text class="delete-text">删除动作</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 保存按钮 -->
|
|
|
|
<view class="save-button">
|
|
|
|
<button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button>
|
|
|
|
<!-- 保存按钮(卡片外、贴近底部) -->
|
|
|
|
<view class="save-bar">
|
|
|
|
<view class="btn-save" :class="{ disabled: !exerciseName.trim() }" @click="saveExercise">
|
|
|
|
保存
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 动作类型选择弹窗 -->
|
|
|
|
<view v-if="showActionTypePicker" class="popup-overlay" @click="closeAllPopups">
|
|
|
|
<view class="popup-content" @click.stop>
|
|
|
|
<view class="popup-header">
|
|
|
|
<view class="close-btn" @click="closeAllPopups">×</view>
|
|
|
|
<view class="title">选择动作类型</view>
|
|
|
|
<view class="confirm-btn" @click="confirmActionType">确定</view>
|
|
|
|
<!-- ==================== 弹窗:肌肉部位选择 ==================== -->
|
|
|
|
<up-popup :show="showMusclePicker" mode="bottom" round="16rpx" @close="showMusclePicker = false"
|
|
|
|
:safe-area-inset-bottom="true">
|
|
|
|
<view class="popup-body">
|
|
|
|
<view class="popup-head">
|
|
|
|
<text class="popup-title">选择训练部位</text>
|
|
|
|
<view class="popup-close" @click="showMusclePicker = false">
|
|
|
|
<up-icon name="close" color="#666" size="18" />
|
|
|
|
</view>
|
|
|
|
<picker-view :value="[actionTypeIndex]" @change="onActionTypeChange" class="picker-view">
|
|
|
|
<picker-view-column>
|
|
|
|
<view class="picker-item" v-for="item in actionTypeOptions" :key="item">{{
|
|
|
|
item.name
|
|
|
|
}}</view>
|
|
|
|
</picker-view-column>
|
|
|
|
</picker-view>
|
|
|
|
</view>
|
|
|
|
<view class="muscle-list">
|
|
|
|
<view v-for="group in muscleGroups" :key="group.id" class="muscle-item">
|
|
|
|
<text class="muscle-name">{{ group.name }}</text>
|
|
|
|
<view class="muscle-actions">
|
|
|
|
<view class="tag primary" :class="{ on: primaryMuscles.includes(group.id) }"
|
|
|
|
@click.stop="togglePrimary(group.id)">
|
|
|
|
<text>主要</text>
|
|
|
|
</view>
|
|
|
|
<view class="tag secondary" :class="{ on: secondaryMuscles.includes(group.id) }"
|
|
|
|
@click.stop="toggleSecondary(group.id)">
|
|
|
|
<text>次要</text>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 目录管理弹窗 -->
|
|
|
|
<view v-if="showDirectoryPicker" class="popup-overlay" @click="closeAllPopups">
|
|
|
|
<view class="popup-content" @click.stop>
|
|
|
|
<view class="popup-header">
|
|
|
|
<view class="close-btn" @click="closeAllPopups">×</view>
|
|
|
|
<view class="title">目录管理</view>
|
|
|
|
<view class="confirm-btn" @click="confirmDirectory">确定</view>
|
|
|
|
</view>
|
|
|
|
<view class="double-picker">
|
|
|
|
<picker-view :value="[bodyPartIndex]" @change="onBodyPartChange" class="picker-view half">
|
|
|
|
<picker-view-column>
|
|
|
|
<view class="picker-item" v-for="item in bodyPartOptions" :key="item">{{
|
|
|
|
item.name
|
|
|
|
}}</view>
|
|
|
|
</picker-view-column>
|
|
|
|
</picker-view>
|
|
|
|
<picker-view :value="[equipmentIndex]" @change="onEquipmentChange" class="picker-view half">
|
|
|
|
<picker-view-column>
|
|
|
|
<view class="picker-item" v-for="item in equipmentOptions" :key="item">{{
|
|
|
|
item.name
|
|
|
|
}}</view>
|
|
|
|
</picker-view-column>
|
|
|
|
</picker-view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="popup-foot">
|
|
|
|
<view class="popup-done" @click="confirmMuscleSelection">完成</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</up-popup>
|
|
|
|
|
|
|
|
<!-- ==================== 弹窗:动作类型选择 ==================== -->
|
|
|
|
<up-picker
|
|
|
|
:show="showActionTypePicker"
|
|
|
|
:columns="actionTypeColumns"
|
|
|
|
:default-index="[actionTypeIndex]"
|
|
|
|
title="选择动作类型"
|
|
|
|
@confirm="onActionTypeConfirm"
|
|
|
|
@cancel="showActionTypePicker = false"
|
|
|
|
close-on-click-overlay
|
|
|
|
/>
|
|
|
|
|
|
|
|
<!-- ==================== 弹窗:目录管理 ==================== -->
|
|
|
|
<up-picker
|
|
|
|
:show="showDirectoryPicker"
|
|
|
|
:columns="directoryColumns"
|
|
|
|
:default-index="[bodyPartIndex, equipmentIndex]"
|
|
|
|
title="目录管理"
|
|
|
|
@confirm="onDirectoryConfirm"
|
|
|
|
@cancel="showDirectoryPicker = false"
|
|
|
|
close-on-click-overlay
|
|
|
|
/>
|
|
|
|
</view>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
import { ref, computed } 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';
|
|
|
|
import FileApi from '@/sheep/api/infra/file';
|
|
|
|
// 表单数据
|
|
|
|
|
|
|
|
// ==================== 基础状态 ====================
|
|
|
|
const isEdit = ref(false);
|
|
|
|
const editActionId = ref(null);
|
|
|
|
const actionDetail = ref({});
|
|
|
|
|
|
|
|
// ==================== 表单数据 ====================
|
|
|
|
const exerciseName = ref('');
|
|
|
|
const actionType = ref();
|
|
|
|
const directoryName = ref();
|
|
|
|
const actionType = ref('');
|
|
|
|
const directoryName = ref('');
|
|
|
|
const selectedMusclesDisplay = ref('未选择');
|
|
|
|
const editActionId = ref(null)
|
|
|
|
|
|
|
|
// 选中索引
|
|
|
|
// ==================== picker 索引 ====================
|
|
|
|
const actionTypeIndex = ref(0);
|
|
|
|
const bodyPartIndex = ref(0);
|
|
|
|
const equipmentIndex = ref(0);
|
|
|
|
|
|
|
|
// 这是后端固定写死的,不要改
|
|
|
|
// ==================== 动作类型选项(后端固定) ====================
|
|
|
|
const actionTypeOptions = ref([
|
|
|
|
{ id: 0, name: '无氧运动' },
|
|
|
|
{ id: 1, name: '有氧运动' },
|
|
...
|
...
|
@@ -210,343 +187,290 @@ const actionTypeOptions = ref([ |
|
|
|
{ id: 4, name: '自重加重' },
|
|
|
|
{ id: 5, name: '自重减重' },
|
|
|
|
{ id: 6, name: '间歇训练' },
|
|
|
|
])
|
|
|
|
// // 加载动作分类(类型)
|
|
|
|
// const loadexercisesGroups = async () => {
|
|
|
|
// try {
|
|
|
|
// const res = await ExercisesApi.getAllMotionCategories();
|
|
|
|
// actionTypeOptions.value = res.data;
|
|
|
|
// console.log('加载动作分类', actionTypeOptions.value)
|
|
|
|
// } catch (err) {
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
// up-picker 需要的 columns 格式:[['无氧运动', '有氧运动', ...]]
|
|
|
|
const actionTypeColumns = computed(() => [actionTypeOptions.value.map((item) => item.name)]);
|
|
|
|
|
|
|
|
const isEdit = ref(false)
|
|
|
|
const actionDetail = ref({});
|
|
|
|
// ==================== 肌肉部位 ====================
|
|
|
|
const muscleGroups = ref([]);
|
|
|
|
const primaryMuscles = ref([]);
|
|
|
|
const secondaryMuscles = ref([]);
|
|
|
|
|
|
|
|
const loadMuscleGroups = async () => {
|
|
|
|
try {
|
|
|
|
const res = await ExercisesApi.getMotionPartPage();
|
|
|
|
muscleGroups.value = res.data;
|
|
|
|
} catch (err) {
|
|
|
|
console.error('加载肌肉部位失败', err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// ==================== 目录选项 ====================
|
|
|
|
const bodyPartOptions = ref([]);
|
|
|
|
const loadbodyPartOptions = async () => {
|
|
|
|
const res = await LeftmotionApi.getAllCategories();
|
|
|
|
bodyPartOptions.value = res.data;
|
|
|
|
};
|
|
|
|
|
|
|
|
const equipmentOptions = ref([]);
|
|
|
|
const loadequipmentOptions = async () => {
|
|
|
|
const res = await EquipmentListApi.Getallequipments();
|
|
|
|
equipmentOptions.value = res.data;
|
|
|
|
};
|
|
|
|
|
|
|
|
// up-picker 双列格式:[['大腿', '大臂', ...], ['器械', '杠铃', ...]]
|
|
|
|
const directoryColumns = computed(() => [
|
|
|
|
bodyPartOptions.value.map((item) => item.name),
|
|
|
|
equipmentOptions.value.map((item) => item.name),
|
|
|
|
]);
|
|
|
|
|
|
|
|
// ==================== 弹窗状态 ====================
|
|
|
|
const showMusclePicker = ref(false);
|
|
|
|
const showActionTypePicker = ref(false);
|
|
|
|
const showDirectoryPicker = ref(false);
|
|
|
|
|
|
|
|
// ==================== 上传状态 ====================
|
|
|
|
const DEFAULT_COVER = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/动作_1784878189633.jpg';
|
|
|
|
const coverImagePath = ref(DEFAULT_COVER);
|
|
|
|
const coverUploaded = ref(false);
|
|
|
|
const videoFilePath = ref('');
|
|
|
|
const videoUploaded = ref(false);
|
|
|
|
const isVideoFile = ref(false);
|
|
|
|
const urlTutorial = ref('');
|
|
|
|
const videoDetailName = ref('');
|
|
|
|
|
|
|
|
// ==================== 表单重置 ====================
|
|
|
|
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 = DEFAULT_COVER;
|
|
|
|
coverUploaded.value = false;
|
|
|
|
videoFilePath.value = '';
|
|
|
|
videoUploaded.value = false;
|
|
|
|
isVideoFile.value = false;
|
|
|
|
urlTutorial.value = '';
|
|
|
|
videoDetailName.value = '';
|
|
|
|
showMusclePicker.value = false;
|
|
|
|
showActionTypePicker.value = false;
|
|
|
|
showDirectoryPicker.value = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// ==================== 页面加载 ====================
|
|
|
|
onLoad(async (options) => {
|
|
|
|
isEdit.value = options.isEdit === 'true';
|
|
|
|
const editId = Number(options.id);
|
|
|
|
editActionId.value = editId;
|
|
|
|
|
|
|
|
isEdit.value = options.isEdit === 'true'
|
|
|
|
const editId = Number(options.id)
|
|
|
|
editActionId.value = editId
|
|
|
|
console.log('编辑状态', isEdit, '编辑动作ID', editId)
|
|
|
|
// 清空表单状态
|
|
|
|
resetForm()
|
|
|
|
resetForm();
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
loadbodyPartOptions(),
|
|
|
|
loadequipmentOptions(),
|
|
|
|
loadMuscleGroups()
|
|
|
|
])
|
|
|
|
loadMuscleGroups(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (isEdit.value) {
|
|
|
|
loadexercisedetail(editId)
|
|
|
|
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
|
|
|
|
if (!id || !Number(id)) return;
|
|
|
|
try {
|
|
|
|
const response = await ExercisesApi.getExerciseById(id);
|
|
|
|
actionDetail.value = response.data;
|
|
|
|
const detail = actionDetail.value
|
|
|
|
console.log('打印动作详情', detail);
|
|
|
|
const detail = actionDetail.value;
|
|
|
|
|
|
|
|
// 1. 动作名称
|
|
|
|
exerciseName.value = detail.name || ''
|
|
|
|
exerciseName.value = detail.name || '';
|
|
|
|
|
|
|
|
// 2. 动作类型回显(id匹配,编辑只读)
|
|
|
|
const targetType = actionTypeOptions.value.find(item => item.id === detail.exerciseType)
|
|
|
|
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
|
|
|
|
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]
|
|
|
|
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})`
|
|
|
|
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
|
|
|
|
primaryMuscles.value = detail.primaryMuscles ? JSON.parse(detail.primaryMuscles) : [];
|
|
|
|
secondaryMuscles.value = detail.secondaryMuscles ? JSON.parse(detail.secondaryMuscles) : [];
|
|
|
|
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
|
|
|
|
if (display) display += ', ';
|
|
|
|
display += secondaryStr;
|
|
|
|
}
|
|
|
|
selectedMusclesDisplay.value = display || '未选择'
|
|
|
|
selectedMusclesDisplay.value = display || '未选择';
|
|
|
|
|
|
|
|
// 5. 封面图 url3dAnimation
|
|
|
|
if (detail.url3dAnimation) {
|
|
|
|
coverImagePath.value = detail.url3dAnimation
|
|
|
|
coverUploaded.value = true // 标记已有封面,跳过新增封面校验
|
|
|
|
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')
|
|
|
|
videoFilePath.value = detail.urlRealPerson;
|
|
|
|
videoUploaded.value = true;
|
|
|
|
isVideoFile.value = detail.urlRealPerson.includes('.mp4');
|
|
|
|
}
|
|
|
|
|
|
|
|
// 7. 详解视频 urlTutorial
|
|
|
|
if (detail.urlTutorial) {
|
|
|
|
urlTutorial.value = detail.urlTutorial
|
|
|
|
videoDetailName.value = '已选择详解视频'
|
|
|
|
urlTutorial.value = detail.urlTutorial;
|
|
|
|
videoDetailName.value = '已选择详解视频';
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
console.error('加载动作详情失败:', err)
|
|
|
|
actionDetail.value = {}
|
|
|
|
console.error('加载动作详情失败:', err);
|
|
|
|
actionDetail.value = {};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 肌肉部位列表(从接口获取)
|
|
|
|
const muscleGroups = ref([]);
|
|
|
|
// 加载肌肉部位
|
|
|
|
const loadMuscleGroups = async () => {
|
|
|
|
try {
|
|
|
|
const res = await ExercisesApi.getMotionPartPage();
|
|
|
|
muscleGroups.value = res.data;
|
|
|
|
console.log('加载部位分类:', muscleGroups.value)
|
|
|
|
} catch (err) {
|
|
|
|
console.error('加载部位失败', err);
|
|
|
|
}
|
|
|
|
// ==================== 动作类型 picker ====================
|
|
|
|
const openActionTypePicker = () => {
|
|
|
|
if (isEdit.value) return;
|
|
|
|
showActionTypePicker.value = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 主要和次要部位(存储 ID 数组)
|
|
|
|
const primaryMuscles = ref([]); // 主要部位 ID 列表
|
|
|
|
const secondaryMuscles = ref([]); // 次要部位 ID 列表
|
|
|
|
const onActionTypeConfirm = (res) => {
|
|
|
|
const idx = res.indexs[0];
|
|
|
|
actionType.value = actionTypeOptions.value[idx].name;
|
|
|
|
actionTypeIndex.value = idx;
|
|
|
|
showActionTypePicker.value = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 弹窗控制
|
|
|
|
const showMusclePicker = ref(false);
|
|
|
|
// ==================== 目录 picker ====================
|
|
|
|
const onDirectoryConfirm = (res) => {
|
|
|
|
const [bodyIdx, equipIdx] = res.indexs;
|
|
|
|
bodyPartIndex.value = bodyIdx;
|
|
|
|
equipmentIndex.value = equipIdx;
|
|
|
|
const selBody = bodyPartOptions.value[bodyIdx];
|
|
|
|
const selEquip = equipmentOptions.value[equipIdx];
|
|
|
|
directoryName.value = `${selBody.name} (${selEquip.name})`;
|
|
|
|
showDirectoryPicker.value = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 部位分类
|
|
|
|
const bodyPartOptions = ref([]);
|
|
|
|
const loadbodyPartOptions = async () => {
|
|
|
|
const res = await LeftmotionApi.getAllCategories();
|
|
|
|
bodyPartOptions.value = res.data;
|
|
|
|
// ==================== 肌肉部位选择 ====================
|
|
|
|
const togglePrimary = (id) => {
|
|
|
|
if (primaryMuscles.value.includes(id)) {
|
|
|
|
primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id);
|
|
|
|
} else {
|
|
|
|
primaryMuscles.value.push(id);
|
|
|
|
secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 用具分类
|
|
|
|
const equipmentOptions = ref([]);
|
|
|
|
const loadequipmentOptions = async () => {
|
|
|
|
const res = await EquipmentListApi.Getallequipments();
|
|
|
|
equipmentOptions.value = res.data;
|
|
|
|
const toggleSecondary = (id) => {
|
|
|
|
if (secondaryMuscles.value.includes(id)) {
|
|
|
|
secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id);
|
|
|
|
} else {
|
|
|
|
secondaryMuscles.value.push(id);
|
|
|
|
primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 弹窗状态
|
|
|
|
const showActionTypePicker = ref(false);
|
|
|
|
const showDirectoryPicker = ref(false);
|
|
|
|
const showCoverSelector = ref(false);
|
|
|
|
const showVideoSelector = ref(false);
|
|
|
|
const confirmMuscleSelection = () => {
|
|
|
|
const primaryNames = primaryMuscles.value
|
|
|
|
.map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '')
|
|
|
|
.filter(Boolean);
|
|
|
|
const secondaryNames = secondaryMuscles.value
|
|
|
|
.map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '')
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
// 弹窗控制
|
|
|
|
const openActionTypePicker = () => {
|
|
|
|
if (isEdit.value) return;
|
|
|
|
showActionTypePicker.value = true;
|
|
|
|
};
|
|
|
|
const openDirectoryPicker = () => {
|
|
|
|
showDirectoryPicker.value = true;
|
|
|
|
let displayText = '';
|
|
|
|
if (primaryNames.length) displayText += primaryNames.join('、') + '(主)';
|
|
|
|
if (secondaryNames.length) {
|
|
|
|
if (displayText) displayText += ', ';
|
|
|
|
displayText += secondaryNames.join('、') + '(次)';
|
|
|
|
}
|
|
|
|
selectedMusclesDisplay.value = displayText || '未选择';
|
|
|
|
showMusclePicker.value = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 封面图片路径
|
|
|
|
const coverImagePath = ref('https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png');
|
|
|
|
// 视频路径(可选)
|
|
|
|
const videoFilePath = ref('');
|
|
|
|
// ✅ 新增:标记是否上传成功(用于判断)
|
|
|
|
const coverUploaded = ref(false);
|
|
|
|
const videoUploaded = ref(false);
|
|
|
|
// 视频详解
|
|
|
|
const urlTutorial = ref('');
|
|
|
|
const videoDetailName = ref('');
|
|
|
|
|
|
|
|
// ==================== 视频详解(上传组件)====================
|
|
|
|
const chooseTutorialVideo = () => {
|
|
|
|
uni.chooseMedia({
|
|
|
|
// ==================== 文件上传 ====================
|
|
|
|
const openCoverSelector = () => {
|
|
|
|
uni.chooseImage({
|
|
|
|
count: 1,
|
|
|
|
mediaType: ['video'], // 只允许选视频
|
|
|
|
success: async (res) => {
|
|
|
|
const file = res.tempFiles[0];
|
|
|
|
const result = await FileApi.uploadFile(file.tempFilePath);
|
|
|
|
urlTutorial.value = result.data;
|
|
|
|
videoDetailName.value = '已选择详解视频';
|
|
|
|
uni.showToast({ title: '选择成功', icon: 'success' });
|
|
|
|
const path = res.tempFilePaths[0];
|
|
|
|
uni.showLoading({ title: '上传中...' });
|
|
|
|
try {
|
|
|
|
const result = await FileApi.uploadFile(path);
|
|
|
|
coverImagePath.value = result.data;
|
|
|
|
coverUploaded.value = true;
|
|
|
|
uni.showToast({ title: '封面上传成功', icon: 'success' });
|
|
|
|
} catch (err) {
|
|
|
|
uni.showToast({ title: '上传失败', icon: 'none' });
|
|
|
|
} finally {
|
|
|
|
uni.hideLoading();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fail: () => {
|
|
|
|
uni.showToast({ title: '选择失败', icon: 'none' });
|
|
|
|
}
|
|
|
|
})
|
|
|
|
coverUploaded.value = false;
|
|
|
|
uni.showToast({ title: '封面选择失败', icon: 'none' });
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
const isVideoFile = ref(false)
|
|
|
|
// 打开视频/图片选择器 + 上传成功判断 这里上传的是3D的那个字段
|
|
|
|
|
|
|
|
const openVideoSelector = () => {
|
|
|
|
uni.chooseMedia({
|
|
|
|
count: 1,
|
|
|
|
mediaType: ['image', 'video'],
|
|
|
|
success: async (res) => {
|
|
|
|
const file = res.tempFiles[0];
|
|
|
|
console.log('选择的文件类型:', file.fileType);
|
|
|
|
isVideoFile.value = file.fileType === 'video';
|
|
|
|
// 上传到服务器
|
|
|
|
const result = await FileApi.uploadFile(file.tempFilePath);
|
|
|
|
videoFilePath.value = result.data;
|
|
|
|
videoUploaded.value = true; // 标记视频上传成功
|
|
|
|
videoUploaded.value = true;
|
|
|
|
uni.showToast({
|
|
|
|
title: file.fileType === 'video' ? '视频选择成功' : '图片选择成功',
|
|
|
|
icon: "success"
|
|
|
|
icon: 'success',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
fail: () => {
|
|
|
|
videoUploaded.value = false; // ❌ 选择失败
|
|
|
|
uni.showToast({ title: "选择失败", icon: "none" });
|
|
|
|
}
|
|
|
|
})
|
|
|
|
videoUploaded.value = false;
|
|
|
|
uni.showToast({ title: '选择失败', icon: 'none' });
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// 封面选择 + 真实上传
|
|
|
|
const openCoverSelector = () => {
|
|
|
|
uni.chooseImage({
|
|
|
|
const chooseTutorialVideo = () => {
|
|
|
|
uni.chooseMedia({
|
|
|
|
count: 1,
|
|
|
|
mediaType: ['video'],
|
|
|
|
success: async (res) => {
|
|
|
|
const path = res.tempFilePaths[0];
|
|
|
|
uni.showLoading({ title: "上传中..." });
|
|
|
|
// 封面上传
|
|
|
|
try {
|
|
|
|
const result = await FileApi.uploadFile(path);
|
|
|
|
console.log(res, 'res');
|
|
|
|
|
|
|
|
coverImagePath.value = result.data;
|
|
|
|
console.log('coverImagePath', coverImagePath.value);
|
|
|
|
|
|
|
|
coverUploaded.value = true;
|
|
|
|
uni.showToast({
|
|
|
|
title: "封面上传成功",
|
|
|
|
icon: "success"
|
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
uni.showToast({
|
|
|
|
title: "上传失败",
|
|
|
|
icon: "none"
|
|
|
|
});
|
|
|
|
} finally {
|
|
|
|
uni.hideLoading();
|
|
|
|
}
|
|
|
|
const file = res.tempFiles[0];
|
|
|
|
const result = await FileApi.uploadFile(file.tempFilePath);
|
|
|
|
urlTutorial.value = result.data;
|
|
|
|
videoDetailName.value = '已选择详解视频';
|
|
|
|
uni.showToast({ title: '选择成功', icon: 'success' });
|
|
|
|
},
|
|
|
|
fail: () => {
|
|
|
|
coverUploaded.value = false;
|
|
|
|
uni.showToast({
|
|
|
|
title: "封面选择失败",
|
|
|
|
icon: "none"
|
|
|
|
});
|
|
|
|
}
|
|
|
|
uni.showToast({ title: '选择失败', icon: 'none' });
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const closeAllPopups = () => {
|
|
|
|
showActionTypePicker.value = false;
|
|
|
|
showDirectoryPicker.value = false;
|
|
|
|
showCoverSelector.value = false;
|
|
|
|
showVideoSelector.value = false;
|
|
|
|
showMusclePicker.value = false;
|
|
|
|
// showVideoDetailPicker.value = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 动作类型
|
|
|
|
const onActionTypeChange = (e) => {
|
|
|
|
actionTypeIndex.value = e.detail.value[0];
|
|
|
|
};
|
|
|
|
// 动作分类列表
|
|
|
|
const confirmActionType = () => {
|
|
|
|
const actionstype = actionTypeOptions.value[actionTypeIndex.value];
|
|
|
|
actionType.value = `${actionstype.name}`;
|
|
|
|
closeAllPopups();
|
|
|
|
};
|
|
|
|
|
|
|
|
// 目录管理
|
|
|
|
const onBodyPartChange = (e) => {
|
|
|
|
bodyPartIndex.value = e.detail.value[0];
|
|
|
|
};
|
|
|
|
const onEquipmentChange = (e) => {
|
|
|
|
equipmentIndex.value = e.detail.value[0];
|
|
|
|
};
|
|
|
|
const confirmDirectory = () => {
|
|
|
|
const selectedBodyPart = bodyPartOptions.value[bodyPartIndex.value];
|
|
|
|
const selectedEquipment = equipmentOptions.value[equipmentIndex.value];
|
|
|
|
directoryName.value = ` ${selectedBodyPart.name}( ${selectedEquipment.name})`;
|
|
|
|
closeAllPopups();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// =================================保存动作====================
|
|
|
|
// ==================== 保存 & 删除 ====================
|
|
|
|
const saveExercise = async () => {
|
|
|
|
if (!exerciseName.value.trim()) {
|
|
|
|
uni.showToast({ title: '请输入动作名称', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('--', equipmentOptions.value[equipmentIndex.value]);
|
|
|
|
console.log('--', bodyPartOptions.value[bodyPartIndex.value]);
|
|
|
|
// 获取 categoryId 和 equipmentId
|
|
|
|
const selectedBodyPart = bodyPartOptions.value[bodyPartIndex.value];
|
|
|
|
const selectedEquipment = equipmentOptions.value[equipmentIndex.value];
|
|
|
|
if (!selectedBodyPart || !selectedEquipment) {
|
|
...
|
...
|
@@ -554,66 +478,55 @@ const saveExercise = async () => { |
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 动作类型映射:label -> value
|
|
|
|
const selectedActionType = actionTypeOptions.value[actionTypeIndex.value];
|
|
|
|
if (!selectedActionType) {
|
|
|
|
uni.showToast({ title: '请选择动作类型', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// 提示选择封面
|
|
|
|
|
|
|
|
if (!coverUploaded.value) {
|
|
|
|
uni.showToast({ title: '请选择封面', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 视频非必填,所以不强制,但可以提示
|
|
|
|
if (videoFilePath.value && !videoUploaded.value) {
|
|
|
|
uni.showToast({ title: '视频未选择成功', icon: 'none' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// 构建请求参数
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
name: exerciseName.value.trim(),
|
|
|
|
categoryId: selectedBodyPart.id, // 注意:必须是 id,不是 name
|
|
|
|
equipmentId: selectedEquipment.id, // 同上
|
|
|
|
exerciseType: selectedActionType.id, // 这里传数字 1/2/3
|
|
|
|
primaryMuscles: JSON.stringify(primaryMuscles.value), //后端要求改json格式
|
|
|
|
categoryId: selectedBodyPart.id,
|
|
|
|
equipmentId: selectedEquipment.id,
|
|
|
|
exerciseType: selectedActionType.id,
|
|
|
|
primaryMuscles: JSON.stringify(primaryMuscles.value),
|
|
|
|
secondaryMuscles: JSON.stringify(secondaryMuscles.value),
|
|
|
|
// urlImage: coverImagePath.value, // 封面地址
|
|
|
|
url3dAnimation: coverImagePath.value,
|
|
|
|
urlRealPerson: videoFilePath.value, //真人实拍视频
|
|
|
|
urlTutorial: urlTutorial.value, // 详解视频
|
|
|
|
urlRealPerson: videoFilePath.value,
|
|
|
|
urlTutorial: urlTutorial.value,
|
|
|
|
};
|
|
|
|
// 编辑追加id
|
|
|
|
|
|
|
|
if (isEdit.value) {
|
|
|
|
payload.id = editActionId.value
|
|
|
|
payload.id = editActionId.value;
|
|
|
|
}
|
|
|
|
console.log('【发送的 payload】', payload);
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (isEdit.value) {
|
|
|
|
res = await ExercisesApi.updateexercises(payload);
|
|
|
|
console.log('【更新接口返回】', res);
|
|
|
|
} else {
|
|
|
|
res = await ExercisesApi.createExercise(payload);
|
|
|
|
console.log('【创建接口返回】', res);
|
|
|
|
}
|
|
|
|
const res = isEdit.value
|
|
|
|
? await ExercisesApi.updateexercises(payload)
|
|
|
|
: await ExercisesApi.createExercise(payload);
|
|
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
uni.showToast({ title: isEdit.value ? '编辑成功' : '保存成功', icon: 'success' });
|
|
|
|
setTimeout(() => {
|
|
|
|
uni.navigateBack();
|
|
|
|
}, 1000);
|
|
|
|
setTimeout(() => uni.navigateBack(), 1000);
|
|
|
|
} else {
|
|
|
|
uni.showToast({ title: res.msg || '保存失败', icon: 'none' });
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('创建动作失败:', error);
|
|
|
|
console.error('保存动作失败:', error);
|
|
|
|
uni.showToast({ title: '网络错误,请重试', icon: 'none' });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 删除动作
|
|
|
|
const deleteAction = () => {
|
|
|
|
uni.showModal({
|
|
|
|
title: '提示',
|
|
...
|
...
|
@@ -621,501 +534,384 @@ const deleteAction = () => { |
|
|
|
success: async (res) => {
|
|
|
|
if (res.confirm) {
|
|
|
|
try {
|
|
|
|
const delRes = await ExercisesApi.deleteexercises(editActionId.value)
|
|
|
|
const delRes = await ExercisesApi.deleteexercises(editActionId.value);
|
|
|
|
if (delRes.code === 0) {
|
|
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
|
|
setTimeout(() => uni.navigateBack(), 1000)
|
|
|
|
uni.showToast({ title: '删除成功', icon: 'success' });
|
|
|
|
setTimeout(() => uni.navigateBack(), 1000);
|
|
|
|
} else {
|
|
|
|
uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' })
|
|
|
|
uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' });
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
uni.showToast({ title: '删除请求失败', icon: 'none' })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const openMusclePicker = () => {
|
|
|
|
showMusclePicker.value = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
const togglePrimary = (id) => {
|
|
|
|
if (primaryMuscles.value.includes(id)) {
|
|
|
|
primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id);
|
|
|
|
} else {
|
|
|
|
primaryMuscles.value.push(id);
|
|
|
|
// 自动取消次要
|
|
|
|
secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const toggleSecondary = (id) => {
|
|
|
|
if (secondaryMuscles.value.includes(id)) {
|
|
|
|
secondaryMuscles.value = secondaryMuscles.value.filter((i) => i !== id);
|
|
|
|
} else {
|
|
|
|
secondaryMuscles.value.push(id);
|
|
|
|
// 自动取消主要
|
|
|
|
primaryMuscles.value = primaryMuscles.value.filter((i) => i !== id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const confirmMuscleSelection = () => {
|
|
|
|
// 构建显示文本,例如:“胸部(主), 背部(次)”
|
|
|
|
const primaryNames = primaryMuscles.value
|
|
|
|
.map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '')
|
|
|
|
.filter(Boolean);
|
|
|
|
const secondaryNames = secondaryMuscles.value
|
|
|
|
.map((id) => muscleGroups.value.find((g) => g.id === id)?.name || '')
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
let displayText = '';
|
|
|
|
if (primaryNames.length) {
|
|
|
|
displayText += primaryNames.join('、') + '(主)';
|
|
|
|
uni.showToast({ title: '删除请求失败', icon: 'none' });
|
|
|
|
}
|
|
|
|
if (secondaryNames.length) {
|
|
|
|
if (displayText) displayText += ', ';
|
|
|
|
displayText += secondaryNames.join('、') + '(次)';
|
|
|
|
}
|
|
|
|
|
|
|
|
// 可以存到一个新字段,比如 selectedMusclesDisplay
|
|
|
|
selectedMusclesDisplay.value = displayText || '未选择';
|
|
|
|
|
|
|
|
closeAllPopups();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
// loadbodyPartOptions();
|
|
|
|
// loadequipmentOptions();
|
|
|
|
// loadMuscleGroups();
|
|
|
|
// loadexercisesGroups();
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
.add-custom-exercise-page {
|
|
|
|
background-color: #1a1a1a;
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
// ==================== 变量 ====================
|
|
|
|
$bg-dark: #1c1c1e;
|
|
|
|
$card-bg: #fff;
|
|
|
|
$card-radius: 24rpx;
|
|
|
|
$primary: #ff9500;
|
|
|
|
$primary-dark: #e68600;
|
|
|
|
$text-main: #1a1a1a;
|
|
|
|
$text-sub: #666;
|
|
|
|
$text-hint: #999;
|
|
|
|
$divider: #f0f0f0;
|
|
|
|
|
|
|
|
// ==================== 页面 ====================
|
|
|
|
.page {
|
|
|
|
min-height: 100vh;
|
|
|
|
padding: 20rpx;
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 肌肉部位图 */
|
|
|
|
|
|
|
|
.muscle-container {
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
align-items: center;
|
|
|
|
gap: 40rpx;
|
|
|
|
background: $bg-dark;
|
|
|
|
padding: 24rpx 24rpx 0;
|
|
|
|
color: #fff;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 肌肉部位图容器 - 占满红色框,无多余内边距 */
|
|
|
|
.muscle-area {
|
|
|
|
background: #1a1a1a;
|
|
|
|
border-radius: 20rpx;
|
|
|
|
/* 移除原有padding,让容器完全贴合红色框 */
|
|
|
|
padding: 0;
|
|
|
|
margin: 20rpx;
|
|
|
|
// ==================== 肌肉部位卡片 ====================
|
|
|
|
.muscle-card {
|
|
|
|
position: relative;
|
|
|
|
/* 固定容器高度,适配移动端,可根据需求调整 */
|
|
|
|
height: 500rpx;
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
border-radius: $card-radius;
|
|
|
|
height: 440rpx;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 肌肉图样式 - 100%占满容器,保持比例不拉伸 */
|
|
|
|
.muscle-image-front {
|
|
|
|
.muscle-img {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
/* aspectFill:保持比例,裁剪超出部分,完美占满容器 */
|
|
|
|
/* 若要完整显示图片,改为 mode="aspectFit",同时调整容器高度 */
|
|
|
|
object-fit: cover;
|
|
|
|
opacity: 1;
|
|
|
|
/* 移除原有半透明,清晰显示 */
|
|
|
|
}
|
|
|
|
|
|
|
|
.muscle-image-back {
|
|
|
|
transform: rotateY(180deg);
|
|
|
|
}
|
|
|
|
|
|
|
|
.legend {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
margin-top: 20rpx;
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: #ccc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.legend-item {
|
|
|
|
.muscle-tip {
|
|
|
|
position: absolute;
|
|
|
|
bottom: 20rpx;
|
|
|
|
left: 50%;
|
|
|
|
transform: translateX(-50%);
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
margin: 0 20rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
width: 16rpx;
|
|
|
|
height: 16rpx;
|
|
|
|
border-radius: 50%;
|
|
|
|
margin-right: 10rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.primary {
|
|
|
|
background-color: #ff9900;
|
|
|
|
}
|
|
|
|
|
|
|
|
.secondary {
|
|
|
|
background-color: #66ccff;
|
|
|
|
gap: 8rpx;
|
|
|
|
padding: 12rpx 28rpx;
|
|
|
|
background: rgba(0, 0, 0, 0.55);
|
|
|
|
border-radius: 40rpx;
|
|
|
|
backdrop-filter: blur(10rpx);
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 表单区域 */
|
|
|
|
.form-section {
|
|
|
|
background: white;
|
|
|
|
border-radius: 20rpx;
|
|
|
|
// ==================== 表单卡片 ====================
|
|
|
|
.form-card {
|
|
|
|
background: $card-bg;
|
|
|
|
border-radius: $card-radius;
|
|
|
|
overflow: hidden;
|
|
|
|
margin-top: 20rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.form-item {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
padding: 30rpx 30rpx 30rpx 40rpx;
|
|
|
|
border-bottom: 1rpx solid #eee;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
// ---- 通用行 ----
|
|
|
|
.form-row {
|
|
|
|
padding: 28rpx 32rpx;
|
|
|
|
border-bottom: 1rpx solid $divider;
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
|
|
|
.action-name {
|
|
|
|
width: 285rpx;
|
|
|
|
}
|
|
|
|
&:last-of-type {
|
|
|
|
border-bottom: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.label {
|
|
|
|
font-size: 32rpx;
|
|
|
|
color: #333;
|
|
|
|
margin-bottom: 10rpx;
|
|
|
|
}
|
|
|
|
.form-label {
|
|
|
|
font-size: 30rpx;
|
|
|
|
font-weight: 600;
|
|
|
|
color: $text-main;
|
|
|
|
display: block;
|
|
|
|
|
|
|
|
.center-label {
|
|
|
|
text-align: center;
|
|
|
|
color: #c44b4b;
|
|
|
|
.optional {
|
|
|
|
font-size: 24rpx;
|
|
|
|
font-weight: 400;
|
|
|
|
color: $text-hint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.input-group {
|
|
|
|
// ---- 动作名称行(横排) ----
|
|
|
|
.name-row {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
align-items: flex-start;
|
|
|
|
gap: 24rpx;
|
|
|
|
|
|
|
|
.input-field {
|
|
|
|
.name-input-area {
|
|
|
|
flex: 1;
|
|
|
|
font-size: 28rpx;
|
|
|
|
color: #333;
|
|
|
|
padding: 0;
|
|
|
|
background: none;
|
|
|
|
border: none;
|
|
|
|
outline: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.input-placeholder {
|
|
|
|
color: #999;
|
|
|
|
}
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
.icon-wrapper {
|
|
|
|
width: 40rpx;
|
|
|
|
height: 40rpx;
|
|
|
|
margin-left: 10rpx;
|
|
|
|
}
|
|
|
|
.form-label {
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.edit-icon {
|
|
|
|
width: 24rpx;
|
|
|
|
height: 24rpx;
|
|
|
|
}
|
|
|
|
.name-input {
|
|
|
|
height: 72rpx;
|
|
|
|
font-size: 28rpx;
|
|
|
|
color: $text-main;
|
|
|
|
background: #f5f5f5;
|
|
|
|
border-radius: 12rpx;
|
|
|
|
padding: 0 20rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.cover-btn {
|
|
|
|
position: absolute;
|
|
|
|
right: 30rpx;
|
|
|
|
top: 30rpx;
|
|
|
|
.cover-upload {
|
|
|
|
flex-shrink: 0;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
width: 100rpx;
|
|
|
|
height: 100rpx;
|
|
|
|
background: #f0f0f0;
|
|
|
|
border-radius: 10rpx;
|
|
|
|
padding: 10rpx;
|
|
|
|
}
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
|
|
.cover-icon {
|
|
|
|
width: 40rpx;
|
|
|
|
height: 40rpx;
|
|
|
|
}
|
|
|
|
.cover-preview {
|
|
|
|
width: 140rpx;
|
|
|
|
height: 140rpx;
|
|
|
|
border-radius: 14rpx;
|
|
|
|
background: #f5f5f5;
|
|
|
|
}
|
|
|
|
|
|
|
|
.btn-text {
|
|
|
|
font-size: 20rpx;
|
|
|
|
color: #333;
|
|
|
|
margin-top: 5rpx;
|
|
|
|
.cover-label {
|
|
|
|
font-size: 22rpx;
|
|
|
|
color: $text-hint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.value-wrapper {
|
|
|
|
// ---- 选择行(训练部位/动作类型/目录管理) ----
|
|
|
|
.select-row {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
background: #fafafa;
|
|
|
|
}
|
|
|
|
|
|
|
|
.select-value {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
font-size: 28rpx;
|
|
|
|
color: #333;
|
|
|
|
margin-top: 10rpx;
|
|
|
|
}
|
|
|
|
gap: 8rpx;
|
|
|
|
max-width: 60%;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
.value {
|
|
|
|
.sel-text {
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: #28c76f;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.arrow-icon {
|
|
|
|
width: 20rpx;
|
|
|
|
height: 20rpx;
|
|
|
|
transform: rotate(90deg);
|
|
|
|
font-weight: 600;
|
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.upload-btn {
|
|
|
|
position: absolute;
|
|
|
|
right: 30rpx;
|
|
|
|
top: 30rpx;
|
|
|
|
// ---- 媒体上传行(动作视频/视频详解) ----
|
|
|
|
.media-row {
|
|
|
|
.media-upload {
|
|
|
|
margin-top: 20rpx;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
width: 100rpx;
|
|
|
|
height: 100rpx;
|
|
|
|
background: #f0f0f0;
|
|
|
|
border-radius: 10rpx;
|
|
|
|
padding: 10rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.preview-media {
|
|
|
|
width: 100rpx;
|
|
|
|
height: 100rpx;
|
|
|
|
border-radius: 10rpx;
|
|
|
|
object-fit: cover;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 隐藏视频默认控制条(可选) */
|
|
|
|
.upload-btn ::deep video::-webkit-media-controls {
|
|
|
|
display: none !important;
|
|
|
|
}
|
|
|
|
width: 100%;
|
|
|
|
height: 200rpx;
|
|
|
|
background: #f8f8f8;
|
|
|
|
border-radius: 16rpx;
|
|
|
|
border: 2rpx dashed #ddd;
|
|
|
|
gap: 12rpx;
|
|
|
|
overflow: hidden;
|
|
|
|
position: relative;
|
|
|
|
transition: border-color 0.2s, background 0.2s;
|
|
|
|
|
|
|
|
.camera-icon {
|
|
|
|
width: 40rpx;
|
|
|
|
height: 40rpx;
|
|
|
|
}
|
|
|
|
&:active {
|
|
|
|
background: #f0f0f0;
|
|
|
|
border-color: #bbb;
|
|
|
|
}
|
|
|
|
|
|
|
|
.upload-text {
|
|
|
|
.media-placeholder {
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: #333;
|
|
|
|
margin-top: 5rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.play-icon {
|
|
|
|
width: 30rpx;
|
|
|
|
height: 30rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.save-button {
|
|
|
|
margin: 40rpx 30rpx 0;
|
|
|
|
}
|
|
|
|
color: $text-hint;
|
|
|
|
}
|
|
|
|
|
|
|
|
.btn-save {
|
|
|
|
.media-preview {
|
|
|
|
position: absolute;
|
|
|
|
inset: 0;
|
|
|
|
width: 100%;
|
|
|
|
height: 80rpx;
|
|
|
|
background: linear-gradient(90deg, #ff9900, #ff6600);
|
|
|
|
color: white;
|
|
|
|
border-radius: 40rpx;
|
|
|
|
font-size: 32rpx;
|
|
|
|
font-weight: bold;
|
|
|
|
border: none;
|
|
|
|
outline: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 弹窗通用样式 */
|
|
|
|
.popup-overlay {
|
|
|
|
position: fixed;
|
|
|
|
top: 0;
|
|
|
|
left: 0;
|
|
|
|
right: 0;
|
|
|
|
bottom: 0;
|
|
|
|
background-color: rgba(0, 0, 0, 0.4);
|
|
|
|
z-index: 999;
|
|
|
|
display: flex;
|
|
|
|
justify-content: flex-end;
|
|
|
|
align-items: flex-end;
|
|
|
|
}
|
|
|
|
height: 100%;
|
|
|
|
object-fit: cover;
|
|
|
|
}
|
|
|
|
|
|
|
|
.popup-content {
|
|
|
|
width: 100%;
|
|
|
|
background: white;
|
|
|
|
border-radius: 16rpx 16rpx 0 0;
|
|
|
|
max-height: 70vh;
|
|
|
|
overflow: hidden;
|
|
|
|
.media-replace-btn {
|
|
|
|
position: absolute;
|
|
|
|
bottom: 16rpx;
|
|
|
|
right: 16rpx;
|
|
|
|
padding: 6rpx 18rpx;
|
|
|
|
background: rgba(0, 0, 0, 0.55);
|
|
|
|
border-radius: 20rpx;
|
|
|
|
font-size: 22rpx;
|
|
|
|
color: #fff;
|
|
|
|
backdrop-filter: blur(8rpx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.popup-header {
|
|
|
|
// ---- 删除行 ----
|
|
|
|
.delete-row {
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
align-items: center;
|
|
|
|
padding: 24rpx 32rpx;
|
|
|
|
border-bottom: 1rpx solid #eee;
|
|
|
|
}
|
|
|
|
justify-content: center;
|
|
|
|
gap: 10rpx;
|
|
|
|
padding: 32rpx;
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
font-size: 40rpx;
|
|
|
|
color: #333;
|
|
|
|
}
|
|
|
|
.delete-text {
|
|
|
|
font-size: 30rpx;
|
|
|
|
color: #e74c3c;
|
|
|
|
font-weight: 500;
|
|
|
|
}
|
|
|
|
|
|
|
|
.title {
|
|
|
|
font-size: 32rpx;
|
|
|
|
font-weight: bold;
|
|
|
|
margin: 0;
|
|
|
|
&:active {
|
|
|
|
background: #fef2f2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.confirm-btn {
|
|
|
|
width: 80rpx;
|
|
|
|
height: 40rpx;
|
|
|
|
line-height: 40rpx;
|
|
|
|
background-color: #ff9900;
|
|
|
|
color: white;
|
|
|
|
border-radius: 20rpx;
|
|
|
|
// ==================== 保存按钮栏 ====================
|
|
|
|
.save-bar {
|
|
|
|
padding: 24rpx 0;
|
|
|
|
// 预留底部安全区
|
|
|
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
|
|
.btn-save {
|
|
|
|
height: 88rpx;
|
|
|
|
line-height: 88rpx;
|
|
|
|
text-align: center;
|
|
|
|
font-size: 28rpx;
|
|
|
|
}
|
|
|
|
background: linear-gradient(135deg, $primary, $primary-dark);
|
|
|
|
color: #fff;
|
|
|
|
border-radius: 44rpx;
|
|
|
|
font-size: 32rpx;
|
|
|
|
font-weight: 700;
|
|
|
|
letter-spacing: 2rpx;
|
|
|
|
transition: opacity 0.2s, transform 0.15s;
|
|
|
|
|
|
|
|
.picker-view {
|
|
|
|
height: 200rpx;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
&:active {
|
|
|
|
transform: scale(0.98);
|
|
|
|
}
|
|
|
|
|
|
|
|
.picker-item {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
font-size: 32rpx;
|
|
|
|
color: #333;
|
|
|
|
height: 100%;
|
|
|
|
&.disabled {
|
|
|
|
opacity: 0.4;
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.double-picker {
|
|
|
|
// ==================== 弹窗通用 ====================
|
|
|
|
.popup-body {
|
|
|
|
background: #fff;
|
|
|
|
border-radius: 16rpx 16rpx 0 0;
|
|
|
|
max-height: 65vh;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
}
|
|
|
|
|
|
|
|
.double-picker .picker-view.half {
|
|
|
|
width: 50%;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 封面预览 */
|
|
|
|
.cover-preview {
|
|
|
|
.popup-head {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
margin: 30rpx 0;
|
|
|
|
}
|
|
|
|
padding: 28rpx 32rpx;
|
|
|
|
border-bottom: 1rpx solid $divider;
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
.cover-image {
|
|
|
|
width: 200rpx;
|
|
|
|
height: 200rpx;
|
|
|
|
}
|
|
|
|
.popup-title {
|
|
|
|
font-size: 32rpx;
|
|
|
|
font-weight: 700;
|
|
|
|
color: $text-main;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 上传区域 */
|
|
|
|
.upload-area {
|
|
|
|
.popup-close {
|
|
|
|
position: absolute;
|
|
|
|
right: 32rpx;
|
|
|
|
top: 50%;
|
|
|
|
transform: translateY(-50%);
|
|
|
|
width: 48rpx;
|
|
|
|
height: 48rpx;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
padding: 40rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.upload-icon {
|
|
|
|
width: 60rpx;
|
|
|
|
height: 60rpx;
|
|
|
|
}
|
|
|
|
.popup-foot {
|
|
|
|
padding: 20rpx 32rpx;
|
|
|
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
|
|
border-top: 1rpx solid $divider;
|
|
|
|
|
|
|
|
.upload-text {
|
|
|
|
font-size: 28rpx;
|
|
|
|
color: #666;
|
|
|
|
margin-top: 10rpx;
|
|
|
|
}
|
|
|
|
.popup-done {
|
|
|
|
height: 80rpx;
|
|
|
|
line-height: 80rpx;
|
|
|
|
text-align: center;
|
|
|
|
background: linear-gradient(135deg, $primary, $primary-dark);
|
|
|
|
color: #fff;
|
|
|
|
border-radius: 40rpx;
|
|
|
|
font-size: 30rpx;
|
|
|
|
font-weight: 600;
|
|
|
|
transition: transform 0.15s;
|
|
|
|
|
|
|
|
/* === 美化后的肌肉选择列表 === */
|
|
|
|
.muscle-list-beautified {
|
|
|
|
padding: 20rpx 32rpx;
|
|
|
|
&:active {
|
|
|
|
transform: scale(0.97);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.muscle-item-beautified {
|
|
|
|
// ==================== 肌肉选择列表 ====================
|
|
|
|
.muscle-list {
|
|
|
|
overflow-y: auto;
|
|
|
|
padding: 8rpx 0;
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
.muscle-item {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: space-between;
|
|
|
|
padding: 24rpx 0;
|
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
}
|
|
|
|
padding: 22rpx 32rpx;
|
|
|
|
border-bottom: 1rpx solid $divider;
|
|
|
|
|
|
|
|
.muscle-name {
|
|
|
|
font-size: 32rpx;
|
|
|
|
color: #333;
|
|
|
|
.muscle-name {
|
|
|
|
font-size: 30rpx;
|
|
|
|
font-weight: 500;
|
|
|
|
color: $text-main;
|
|
|
|
flex: 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-btn {
|
|
|
|
.muscle-actions {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
gap: 8rpx;
|
|
|
|
padding: 12rpx 20rpx;
|
|
|
|
border-radius: 50rpx;
|
|
|
|
background-color: #f8f8f8;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
min-width: 120rpx;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-btn.primary {
|
|
|
|
border: 2rpx solid #ffcc99;
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-btn.primary.active {
|
|
|
|
background-color: #fff3e6;
|
|
|
|
border-color: #ff9900;
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(255, 153, 0, 0.2);
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-btn.secondary {
|
|
|
|
border: 2rpx solid #cce6ff;
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-btn.secondary.active {
|
|
|
|
background-color: #e6f7ff;
|
|
|
|
border-color: #66ccff;
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(102, 204, 255, 0.2);
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-dot {
|
|
|
|
width: 16rpx;
|
|
|
|
height: 16rpx;
|
|
|
|
border-radius: 50%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-dot.primary {
|
|
|
|
background-color: #ff9900;
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-dot.secondary {
|
|
|
|
background-color: #66ccff;
|
|
|
|
gap: 16rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.selector-label {
|
|
|
|
font-size: 26rpx;
|
|
|
|
color: #666;
|
|
|
|
// ==================== 主要/次要 tag ====================
|
|
|
|
.tag {
|
|
|
|
padding: 10rpx 24rpx;
|
|
|
|
border-radius: 40rpx;
|
|
|
|
font-size: 24rpx;
|
|
|
|
font-weight: 500;
|
|
|
|
}
|
|
|
|
transition: all 0.2s;
|
|
|
|
background: #f5f5f5;
|
|
|
|
color: #999;
|
|
|
|
border: 2rpx solid transparent;
|
|
|
|
|
|
|
|
.selector-btn.primary.active .selector-label,
|
|
|
|
.selector-btn.secondary.active .selector-label {
|
|
|
|
color: #333;
|
|
|
|
font-weight: bold;
|
|
|
|
&.primary {
|
|
|
|
&.on {
|
|
|
|
background: #fff6ec;
|
|
|
|
color: $primary;
|
|
|
|
border-color: $primary;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.secondary {
|
|
|
|
&.on {
|
|
|
|
background: #ecf6ff;
|
|
|
|
color: #3b9cff;
|
|
|
|
border-color: #3b9cff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style> |
...
|
...
|
|