Authored by qxm

训计提交

Too many changes to show.

To preserve performance only 10 of 10+ files are displayed.

<template>
<view class="floating-train-btn" v-if="trainingStore.min" @click="goToTrainingPage">
<view class="icon-wrap">
<up-icon name="plus-circle" color="#fff" size="24"></up-icon>
</view>
<view class="text-wrap">
<text class="status-text">训练中</text>
<text class="time-text">{{ formattedStartTime }}</text>
</view>
</view>
</template>
<script setup>
import { computed, watch, onMounted } from 'vue';
import { useTrainingStore } from '@/sheep/store/trainingStore';
const trainingStore = useTrainingStore();
// 格式化显示“开始时间”
const formattedStartTime = computed(() => trainingStore.trainingTimeText);
// 跳转到训练页面
const goToTrainingPage = () => {
// 悬浮球消失
trainingStore.min = false;
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}`,
});
};
watch(() => trainingStore.min, (newVal) => {
console.log('🟢 悬浮球组件监听到 min 变化:', newVal);
}, { immediate: true });
// 组件挂载时打印
onMounted(() => {
console.log('🟢 悬浮球组件已挂载,当前 min 值:', trainingStore.min);
});
</script>
<style lang="scss" scoped>
.floating-train-btn {
position: fixed;
right: 20rpx;
bottom: 200rpx;
display: flex;
align-items: center;
background-color: #39d353;
border-radius: 60rpx;
padding: 16rpx 20rpx;
box-shadow: 0 8rpx 20rpx rgba(57, 211, 83, 0.3);
z-index: 9999;
}
.icon-wrap {
margin-right: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.text-wrap {
display: flex;
flex-direction: column;
color: #fff;
}
.status-text {
font-size: 26rpx;
font-weight: bold;
}
.time-text {
font-size: 22rpx;
opacity: 0.9;
}
</style>
\ No newline at end of file
... ...
<template>
<view class="charts-box">
<qiun-data-charts
type="line"
:opts="opts"
:chartData="chartData"
:reshow="reshow"
:canvas2d="true"
/>
<!-- :canvas2d="true" tooltipFormat="showYLable" :onmovetip="true" -->
<qiun-data-charts :type="chartType" :opts="opts" :chartData="chartData" :reshow="reshow" :tooltipShow="true" />
</view>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
import { ref, reactive, watch } from 'vue';
// 1. 定义 Props 接收父组件数据
const props = defineProps({
// 1. 定义 Props 接收父组件数据
const props = defineProps({
// 传入的分类数据 (横坐标)
chartType: { // 新增
type: String,
default: 'line'
},
categories: {
type: Array,
default: () => [],
... ... @@ -30,12 +29,13 @@
type: Boolean,
default: false,
},
});
});
const chartData = ref({});
const chartData = ref({});
// 2. 图表配置项
const opts = reactive({
// 2. 图表配置项
const opts = reactive({
dataLabel: false,
color: [
'#1890FF',
'#91CB74',
... ... @@ -49,12 +49,19 @@
],
padding: [15, 10, 0, 15],
enableScroll: false,
legend: {},
legend: {
show: true,
position: 'top'
},
xAxis: {
disableGrid: true,
// itemCount: 7, // 一共显示7个标签:1、6、11、16、21、26、31
labelCount: 7,
// splitNumber: 6,
},
yAxis: {
gridType: 'dash',
axisLabel: { show: false },
dashLength: 2,
},
extra: {
... ... @@ -63,32 +70,63 @@
width: 2,
activeType: 'hollow',
},
// 新增下面这段
column: {
width: 30, // 柱子宽度
radius: [4, 4, 0, 0] // 柱子圆角
},
});
tooltip: {
legendShow: true,
}
},
});
// 3. 核心逻辑:格式化数据
// const formatData = () => {
// if (props.categories.length > 0) {
// chartData.value = {
// categories: props.categories,
// series: props.series,
// };
// }
// };
// 3. 核心逻辑:格式化数据
const formatData = () => {
if (props.categories.length > 0) {
const formatData = () => {
const cat = props.categories || [];
let ser = props.series || [];
console.log('cat=', cat, 'ser=', ser);
// 强制保证 series 每一项都有 data 数组
ser = ser.map(item => ({
...item,
data: item.data || [] // 最关键:没有data就给空数组
}));
if (cat.length > 0) {
chartData.value = {
categories: props.categories,
series: props.series,
categories: cat,
series: ser
};
}
};
};
// 4. 监听 Props 变化,当父组件传入新数据时自动重绘
watch(
() => [props.categories, props.series],
// 4. 监听 Props 变化,当父组件传入新数据时自动重绘
watch(
() => [props.categories, props.series, props.chartType],
() => {
formatData();
},
{ immediate: true, deep: true },
);
);
</script>
<style lang="scss" scoped>
.charts-box {
.charts-box {
width: 100%;
height: 100%;
}
}
</style>
... ...
<template>
<!-- 一行布局:保证所有内容在同一行 ✅ 你的核心要求 -->
<view class="record-row" :class="{ active: record.isActive }">
<view class="left">
<view class="left-header">
<!-- 组号:所有类型都有 -->
<view class="index-box">{{ index + 1 }}</view>
<!-- ======================================
类型 0:力量(重量 + 次数)+ 休息
====================================== -->
<view v-if="exerciseType === 0" class="goal-list">
<view class="goal-item">
<!-- 重量 -->
<view class="goal-input-box no-label-input">
<view class="values">
<up-input v-model="record.weight" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">kg</text>
</view>
</view>
<!-- 次数 -->
<view class="goal-input-box no-label-input">
<view class="values">
<up-input v-model="record.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">次</text>
</view>
</view>
</view>
</view>
<!-- ======================================
类型 1:有氧(时长 + 距离)+ 休息
====================================== -->
<view v-else-if="exerciseType === 1" class="goal-list">
<view class="goal-item">
<view class="input-col" @click.stop="emitOpenPicker(index)">
<view class="label">时长</view>
<view class="values">
<text class="num">{{ record.h }}</text>
<text class="unit">时</text>
<text class="num">{{ record.m }}</text>
<text class="unit">分</text>
<text class="num">{{ record.s }}</text>
<text class="unit">秒</text>
</view>
</view>
<view class="distance">
<view class="label">距离</view>
<view class="values">
<up-input v-model="record.distance" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">km</text>
</view>
</view>
</view>
</view>
<!-- ======================================
类型 2:纯次数 + 休息
====================================== -->
<view v-else-if="exerciseType === 2" class="goal-list">
<view class="goal-item">
<view class="goal-input-box">
<view class="values">
<up-input v-model="record.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">次</text>
</view>
</view>
</view>
</view>
<!-- ======================================
类型 3:纯时长 + 休息
====================================== -->
<view v-else-if="exerciseType === 3" class="input-col" @click.stop="emitOpenPicker(index)">
<view class="label">时长</view>
<view class="values">
<text class="num">{{ record.h }}</text>
<text class="unit">时</text>
<text class="num">{{ record.m }}</text>
<text class="unit">分</text>
<text class="num">{{ record.s }}</text>
<text class="unit">秒</text>
</view>
</view>
<!-- ======================================
类型 4:自重加重 + 休息
====================================== -->
<view v-else-if="exerciseType === 4" class="goal-list">
<view class="goal-item">
<!-- <view class="goal-input-box" hover-class="none" hover-stop-propagation="false">
<up-input :value="userWeight" @input="handleWeightChange" border="none" type="digit" color="#ffffff"
:maxlength="3" :customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
</view> -->
<view class="goal-input-box ">
<view class="values">
<up-input v-model="record.weight" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">kg</text>
</view>
</view>
<view class="goal-input-box">
<view class="values">
<up-input v-model="record.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">次</text>
</view>
</view>
</view>
</view>
<!-- ======================================
类型 5:自重减重 + 休息
====================================== -->
<view v-else-if="exerciseType === 5" class="goal-list">
<view class="goal-item">
<view class="goal-input-box">
<!-- <up-input :value="userWeight" @input="handleWeightChange" border="none" type="digit" color="#ffffff"
:maxlength="3" :customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" /> -->
<view class="values">
<up-input v-model="record.weight" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">kg</text>
</view>
</view>
<view class="goal-input-box">
<view class="values">
<up-input v-model="record.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">次</text>
</view>
</view>
</view>
</view>
<!-- ======================================
类型 6:间歇(循环、单次、间歇)无外部休息
====================================== -->
<view v-else-if="exerciseType === 6" class="goal-list">
<view class="goal-item interval">
<view class="goal-input-box reps">
<view class="label">循环</view>
<view class="values">
<up-input v-model="record.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">组</text>
</view>
</view>
<view class="goal-input-box time">
<view class="label">单次</view>
<view class="values">
<up-input v-model="record.duration" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">秒</text>
</view>
</view>
<view class="goal-input-box restTime">
<view class="label">间歇</view>
<view class="values">
<up-input v-model="record.restTime" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }" />
<text class="unit">秒</text>
</view>
</view>
</view>
</view>
<!-- ======================================
休息时间(除了 type6 都显示)
移入子组件内部 ✅
====================================== -->
<view v-if="!isEditing && exerciseType !== 6" class="timer-selector" @click.stop="emitOpenQuickTimer(index)">
<text class="text">{{ record.quickTimeDisplay || '60s' }}</text>
<up-icon name="arrow-down" color="#8e8e93" size="10"></up-icon>
</view>
</view>
</view>
<!-- ======================================
右侧:勾选 / 编辑按钮
====================================== -->
<view class="icons">
<template v-if="!isEditing">
<view class="icon" @click.stop="emitToggleActive(index)" :class="{ active: record.isActive }">
<up-icon name="checkmark" :color="record.isActive ? '#000' : '#919191'" size="20" bold></up-icon>
</view>
</template>
<template v-else>
<view class="icon" @click.stop="emitAddRow">
<up-icon name="plus-circle" color="#fff" size="20" bold></up-icon>
</view>
<view class="icon del" @click.stop="emitDeleteRow(index)">
<up-icon name="trash" color="#e63e1e" size="20" bold></up-icon>
</view>
</template>
</view>
</view>
</template>
<script setup>
const props = defineProps({
exerciseType: [Number, String],
record: Object,
index: Number,
isEditing: Boolean,
userWeight: {
type: Number,
default: 70
}
})
const emit = defineEmits([
'openPicker',
'openQuickTimer',
'toggleActive',
'addRow',
'deleteRow',
])
const emitOpenPicker = (index) => emit('openPicker', index)
const emitOpenQuickTimer = (index) => emit('openQuickTimer', index)
const emitToggleActive = (index) => emit('toggleActive', index)
const emitAddRow = () => emit('addRow')
const emitDeleteRow = (index) => emit('deleteRow', index)
</script>
<style lang="scss" scoped>
.record-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 10rpx 30rpx;
box-sizing: border-box;
&.active {
background-color: #4c4d3b;
}
.left {
display: flex;
flex-direction: column;
gap: 10rpx;
.left-header {
display: flex;
align-items: center;
gap: 20rpx;
}
}
.index-box {
width: 50rpx;
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #fff;
}
.input-col,
.distance {
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
padding: 5rpx 10rpx;
display: flex;
flex-direction: column;
justify-content: center;
// flex: 1;
// flex-shrink: 0;
.label {
font-size: 15rpx;
color: #8e8e93;
}
.values {
display: flex;
align-items: baseline;
.num {
font-size: 32rpx;
color: #fff;
}
.unit {
font-size: 15rpx;
color: #8e8e93;
margin-left: 10rpx;
margin-right: 10rpx;
}
}
}
.timer-selector {
width: 75rpx;
height: 75rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1rpx solid #a4a4a4;
.text {
font-size: 20rpx;
color: #8e8e93;
}
}
.goal-list {
display: flex;
gap: 20rpx;
}
.goal-item {
display: flex;
gap: 20rpx;
}
.goal-input-box {
display: flex;
flex-direction: column;
justify-content: center;
background: #1f1f1f;
width: 100rpx;
height: 70rpx;
border-radius: 10rpx;
padding: 0 10rpx;
.label {
font-size: 15rpx;
color: #8e8e93;
align-items: baseline;
margin-bottom: 2rpx;
}
.values {
display: flex;
align-items: flex-end;
}
.unit {
font-size: 15rpx;
color: #8e8e93;
}
}
.no-label-input {
// padding-top: 20rpx !important;
// align-items: flex-end;
justify-content: flex-end;
padding-bottom: 10rpx;
.values {
align-items: baseline;
}
.unit {
margin-left: 4rpx;
}
}
.interval {
display: flex;
gap: 20rpx;
}
.icons {
display: flex;
gap: 20rpx;
.icon {
width: 70rpx;
height: 70rpx;
background: #1f1f1f;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: #f8d714;
}
}
.del {
background-color: #58372d;
}
}
.weight-wrap-fix {
flex-direction: row !important;
align-items: flex-end !important;
gap: 6rpx;
padding-top: 8rpx;
}
}
</style>
\ No newline at end of file
... ...
<template>
<view class="popup-container" v-if="props.show" @click="closeAddMenu">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="add-btn-wrapper" @click.stop="addExercise">
<uni-icons type="plus" size="35" color="#333"></uni-icons>
<view class="floating-menu" v-show="addActionshow" @click.stop>
<view class="menu-item" @click="addnewmotion">
<uni-icons type="plus" size="24" color="#333"></uni-icons>
<text class="menu-text">新增动作</text>
</view>
</view>
</view>
</view>
<!-- 左右布局 -->
<view class="layout-container">
<!-- 左侧分类导航 -->
<scroll-view scroll-y class="left-nav">
<view class="nav-item" @click="handleCollectClick('collect')" :class="{ active: activeNav === 'collect' }">
收藏
</view>
<view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }"
@click="switchNav(nav.id)">
{{ nav.name }}
</view>
</scroll-view>
<!-- 右侧动作列表 -->
<scroll-view scroll-y class="right-content">
<!-- 部位筛选(可选) -->
<view class="tip" v-if="motionPart.length > 0">
<view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view>
<view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }"
@click="handlePartClick(item.id)">
{{ item.name }}
</view>
</view>
<view class="exercise-grid">
<view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0">
<!-- 普通动作列表 -->
<view class="equipment-list">
<view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
<view class="equipment-name"> {{ item.equipmentName }} </view>
<view class="action-list">
<!-- 动作卡片(可点击赋值) -->
<view class="action-item" v-for="e in item.exercises" :key="e.id"
:class="{ selected: selectedList.some(i => i.action.id === e.id) }" @click="selectAction(e)">
<image :src="e.url3dAnimation" mode="aspectFill" lazy-load class="action-img"></image>
<view class="action-name">{{ e.name }}</view>
<view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view>
</view>
</view>
</view>
<!-- 超级组列表(可选) -->
<view class="supers" v-if="superGroupInfo.length > 0">
<view class="supers-name"> 超级组 </view>
<view class="super" v-for="item in superGroupInfo" :key="item.id"
:class="{ selected: selectedList.some(i => i.action.id === item.id) }" @click="selectAction(item, 2)">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
mode="aspectFill" lazy-load class="super-img"></image>
<view class="right">
<view class="super-name">{{ item.name }}</view>
<view class="parts">{{ item.primaryMuscles.join(' ') }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无动作数据</text>
</view>
</view>
</scroll-view>
</view>
<!-- 底部已选动作栏 -->
<scroll-view class="selected-bar" scroll-x="true" show-scrollbar="false">
<view class="selected-item" v-for="(item, index) in selectedList" :key="index">
<text class="name">{{ item.action.name }}</text>
<uni-icons type="close" size="16" color="#fff" @click="selectedList.splice(index, 1)"></uni-icons>
</view>
</scroll-view>
<!-- 底部按钮栏 -->
<view class="bottom-btn-bar">
<view class="btn cancel-btn" @click="handleClose">取消</view>
<view class="btn confirm-btn" :class="{ disabled: selectedList.length === 0 }" @click="handleConfirm">
确认
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import SupersetsApi from '@/sheep/api/motion/supersets';
import { useActionStore } from '@/sheep/store/action';
import { useTrainingStore } from '@/sheep/store/trainingStore'
const trainingStore = useTrainingStore()
const actionStore = useActionStore();
const emit = defineEmits(['close', 'confirm']);
const addActionshow = ref(false);
// 数据
const activeNav = ref('collect'); // 默认选中收藏
const navItems = ref([]);
const exercises = ref([]);
const superGroupInfo = ref([]);
const motionPart = ref([]);
const activeMotionPart = ref('');
const selectedList = ref([]); // 多选列表:每一项都是 { action: 动作对象, type: 1=动作 2=超级组 }
const selectedType = ref(1); // 1=动作 2=超级组
const allExercises = ref([]);
const props = defineProps({
show: {
type: Boolean,
default: false
},
unitIndex: Number,
})
// 监听弹窗打开,加载数据
watch(() => props.show, (val) => {
if (val) {
selectedList.value = [];
// 加载收藏列表
loadCollectList();
loadSuperFavoriteList();
}
console.log('动作替换组件 unitIndex:', props.unitIndex)
});
// 关闭菜单
const closeAddMenu = () => {
addActionshow.value = false;
};
// 切换菜单显示/隐藏
const addExercise = () => {
addActionshow.value = !addActionshow.value;
};
// 新增动作跳转
const addnewmotion = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });
};
// 加载分类
const loadCategories = async () => {
try {
await actionStore.getloadCategories();
navItems.value = actionStore.showCategories;
if (navItems.value.length > 0) {
switchNav(navItems.value[0].id);
}
} catch (error) {
console.error('获取分类失败:', error);
}
};
// 加载收藏动作列表
const loadCollectList = async () => {
try {
const res = await ExercisesApi.getFavoriteExercises();
exercises.value = res.data || [];
allExercises.value = [...(res.data || [])];
} catch (err) {
console.error('加载动作收藏失败', err);
}
};
// 加载收藏超级组列表
const loadSuperFavoriteList = async () => {
try {
const res = await SupersetsApi.getFavoriteSuperset();
superGroupInfo.value = res.data || [];
} catch (err) {
console.error('加载超级组收藏失败', err);
}
};
// 切换左侧分类
const switchNav = (id) => {
if (activeNav.value === id) return;
activeNav.value = id;
activeMotionPart.value = '';
// selectedList.value = []; // 切换分类时重置选中
if (id === 'super') {
exercises.value = [];
motionPart.value = [];
loadsupersetsinfo();
} else {
superGroupInfo.value = [];
loadExercises(id);
}
};
// 加载普通动作
const loadExercises = async (categoriesId) => {
try {
const partRes = await ExercisesApi.getMotionPart(categoriesId);
motionPart.value = partRes.data || [];
const exerciseRes = await ExercisesApi.getexercises({ categoriesId });
exercises.value = exerciseRes.data;
allExercises.value = [...(exerciseRes.data || [])];
} catch (error) {
console.error('加载动作失败:', error);
}
};
// 加载超级组
const loadsupersetsinfo = async () => {
try {
const response = await SupersetsApi.getsupersets();
superGroupInfo.value = response.data || [];
} catch (error) {
console.error('获取超级组失败:', error);
}
};
// 部位筛选
const handlePartClick = async (id) => {
activeMotionPart.value = id;
const exerciseRes = await ExercisesApi.getexercises({
categoriesId: activeNav.value,
subCategoriesId: id
});
exercises.value = exerciseRes.data || [];
selectedList.value = []; // 切换部位时重置选中
};
// 搜索动作
const onSearch = (e) => {
const keyword = e.detail.value.trim().toLowerCase();
if (!keyword) {
exercises.value = [...allExercises.value];
return;
}
exercises.value = allExercises.value.filter(item =>
item.name?.toLowerCase().includes(keyword)
);
};
// 选择动作/超级组
const selectAction = (item, type = 1) => {
// 查找是否已选中
const index = selectedList.value.findIndex(i => i.action.id === item.id);
if (index > -1) {
// 已存在 → 删除
selectedList.value.splice(index, 1);
} else {
// 不存在 → 添加
selectedList.value.push({
action: item,
type: type
});
}
console.log("✅ 当前选中列表:", selectedList.value);
};
// 关闭组件
const handleClose = () => {
setTimeout(() => {
emit('close');
}, 800);
}
// 确认添加(带详情接口)
const handleConfirm = async () => {
if (selectedList.value.length === 0) {
uni.showToast({ title: '请选择动作', icon: 'none' });
return;
}
if (!trainingStore.actionDetail.units) {
trainingStore.actionDetail.units = [];
}
// 遍历 selectedList,调用详情接口拿到完整数据,再转成 unit
for (const item of selectedList.value) {
let unit = null;
if (item.type === 1) {
// 单个动作:调用详情接口
const res = await ExercisesApi.getExerciseById(item.action.id);
const detail = res.data;
console.log('+++接口返回的动作详情++++', detail);
console.log('detail.exerciseType =', detail.exerciseType);
unit = {
unitType: 1,
unitId: detail.id,
unitName: detail.name,
exercises: [
{
exerciseId: detail.id,
exerciseName: detail.name,
exerciseType: detail.exerciseType,
urlImage: detail.urlImage || detail.url3dAnimation,
categoryDescription: detail.categoryDescription,
equipmentDescription: detail.equipmentDescription,
}
]
};
} else if (item.type === 2) {
// 超级组:调用详情接口
const res = await SupersetsApi.getSupersetsInfo(item.action.id);
const detail = res.data;
console.log('+++接口返回的动作详情++++', detail);
console.log('detail.exerciseType =', detail.exerciseType);
unit = {
unitType: 2,
supersetId: detail.id,
unitName: detail.name,
exercises: detail.exercises.map(e => ({
exerciseId: e.id,
exerciseName: e.name,
exerciseType: e.exerciseType,
urlImage: e.urlImage || e.url3dAnimation,
}))
};
}
if (unit) {
trainingStore.actionDetail.units.push(unit);
}
}
console.log("✅ 已添加动作(含完整字段):", trainingStore.actionDetail.units);
emit('close');
selectedList.value = []; // 清空选择
};
onMounted(() => {
loadCategories();
});
</script>
<style lang="scss" scoped>
.popup-container {
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 999;
border-radius: 0;
.search-bar {
display: flex;
align-items: center;
height: calc(var(--status-bar-height) + 88rpx);
padding: 0 20rpx 0 20rpx;
padding-top: var(--status-bar-height);
background-color: #fff;
position: sticky;
top: 0;
z-index: 10;
}
// 浮动加号按钮 + 菜单
.add-btn-wrapper {
position: relative;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.floating-menu {
position: absolute;
top: 90rpx;
left: 20rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
padding: 20rpx;
z-index: 9999;
width: 260rpx;
}
.menu-item {
display: flex;
align-items: center;
gap: 16rpx;
cursor: pointer;
}
.menu-text {
font-size: 28rpx;
}
.layout-container {
flex: 1;
display: flex;
background-color: #f5f5f5;
overflow: hidden;
.left-nav {
width: 150rpx;
background-color: #fafafa;
border-right: 1px solid #eee;
.nav-item {
padding: 28rpx 16rpx;
text-align: left;
font-size: 26rpx;
color: #666;
position: relative;
&.active {
color: #000;
background-color: #fff;
border-right: none;
font-weight: 500;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 36rpx;
background-color: #f8d714;
}
}
}
}
.right-content {
flex: 1;
padding: 20rpx;
.tip {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
margin-bottom: 20rpx;
.item {
padding: 10rpx 18rpx;
font-size: 24rpx;
background-color: #fff;
border-radius: 16rpx;
color: #666;
&.active {
background-color: #f8d714;
color: #000;
}
}
}
.exercise-grid {
.equipment-item {
margin-bottom: 30rpx;
.equipment-name {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.action-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15rpx;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
background: #fff;
border-radius: 16rpx;
padding: 15rpx;
position: relative;
&.selected {
border: 2rpx solid #f8d714;
}
.action-img {
width: 100%;
height: 220rpx;
border-radius: 12rpx;
}
.action-name {
font-size: 24rpx;
color: #333;
}
.trainingReps {
position: absolute;
top: 15rpx;
right: 15rpx;
font-size: 22rpx;
color: #999;
}
}
}
}
.supers-name {
font-size: 28rpx;
color: #333;
margin: 20rpx 0 16rpx;
}
.super {
display: flex;
align-items: center;
background: #fff;
padding: 20rpx;
border-radius: 16rpx;
gap: 20rpx;
margin-bottom: 15rpx;
&.selected {
border: 2rpx solid #f8d714;
}
.super-img {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
}
.right {
flex: 1;
.super-name {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
}
.parts {
font-size: 22rpx;
color: #999;
}
}
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding-top: 100rpx;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
.bottom-btn-bar {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx 30rpx;
background-color: #fff;
.btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
font-weight: bold;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
.confirm-btn {
background-color: #f8d714;
color: #000;
&.disabled {
background-color: #eeeeee;
color: #bbbbbb;
}
}
}
.selected-bar {
background-color: #2c2c2e;
padding: 20rpx 30rpx;
white-space: nowrap;
display: flex;
gap: 20rpx;
.selected-item {
background-color: #444;
color: #fff;
padding: 10rpx 20rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
gap: 10rpx;
.name {
font-size: 26rpx;
}
}
}
}
</style>
\ No newline at end of file
... ...
<template>
<up-popup :show="historyShow" mode="bottom" round="24rpx" bgColor="#2c2c2e" @close="historyShow = false">
<scroll-view scroll-y class="history-popup">
<!-- 标题栏 -->
<view class="header">
<view class="title">{{ actionName }}</view>
</view>
<!-- 顶部数据卡片-->
<view class="top-cards">
<!-- <view class="card">
<view class="label">最高重量</view>
<view class="value">{{ maxWeight }} × {{ maxReps }}次</view>
<view class="date">{{ recordDate }}</view>
</view>
<view class="card">
<view class="label">最高容量</view>
<view class="value">{{ maxCapacity }}kg · 共{{ maxCapacitySets }}组</view>
<view class="date">{{ recordDate }}</view>
</view> -->
</view>
<!-- 历史记录列表 -->
<view class="list">
<view class="record-item" v-for="(item, index) in formattedSetList" :key="index">
<view class="date">{{ item.date }}</view>
<view class="name">{{ item.name }}</view>
<view class="info">共{{ item.setCount }}组 · 容量{{ item.capacity }}kg</view>
<view class="set-list">
<view class="set" v-for="(set, idx) in item.sets" :key="idx">
<view class="num">{{ set.idx }}</view>
<text class="text">{{ set.text }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, computed } from 'vue'
import TrainingApi from '@/sheep/api/Training/traininghistory'
const historyShow = ref(false)
const historyActionId = ref(0)
const historyList = ref([])
const props = defineProps({
historyType: [Number, String],
actionName: {
type: String,
default: '动作历史'
}
})
const emit = defineEmits(['close'])
const openHistoryPopup = (id) => {
console.log('传递到历史子组件的动作id', id)
historyActionId.value = id;
console.log('传递到历史子组件的动作idhistoryActionId', historyActionId.value)
if (id !== undefined && id !== null && id !== '') {
loadTrainHistoryDetail(id, props.historyType)
}
historyShow.value = true;
}
// 打开备注弹窗
const openBeizhu = () => {
nextTick(() => {
if (showBeizhuRef.value) {
showBeizhuRef.value.open(actionId.value);
}
});
};
// 接收子组件传过来的备注内容
// const handleNoteSave = async (content) => {
// try {
// if (type.value == 2) {
// await SupersetsApi.addNotes({
// supersetsId: actionId.value,
// content: content,
// });
// } else {
// await ExercisesApi.addNotes({
// exerciseId: actionId.value,
// content: content,
// });
// }
// actionDetail.value.userNote = content;
// } catch (e) {
// console.log(e);
// }
// };
// 加载训练历史
const loadTrainHistoryDetail = async (id, type) => {
try {
console.log("即将查询的历史ID =", id);
const res = await TrainingApi.getTrainHistoryList(id, type);
console.log('接口完整返回:', res);
historyList.value = res.data;
console.log('动作训练的训练历史列表接口返回结果historyList.value', historyList.value);
} catch (error) {
console.error('加载训练历史失败:', error);
}
}
// 1. 格式化日期数组为 YYYY/MM/DD 字符串
const formatDateArr = (dateArr) => {
if (!Array.isArray(dateArr) || dateArr.length < 3) return ''
const [year, month, day] = dateArr
return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')}`
}
// 2. 把每一组的 setConfigList 转成「可渲染的文本」
const formattedSetList = computed(() => {
return historyList.value.map(item => {
const sets = (item.setConfigList || []).map(set => {
// 自动拼接:优先显示重量/次数,再显示时长/距离
const parts = []
if (set.weight != null && set.weight !== '') {
parts.push(`${set.weight}kg`)
}
if (set.reps != null && set.reps !== '') {
parts.push(`${set.reps}次`)
}
if (set.duration != null && set.duration > 0) {
// 秒转成 HH:MM:SS
const h = Math.floor(set.duration / 3600)
const m = Math.floor((set.duration % 3600) / 60)
const s = set.duration % 60
const hh = h > 0 ? `${h}:` : ''
parts.push(`${hh}${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`)
}
if (set.distance != null && set.distance !== '') {
parts.push(`${set.distance}m`)
}
return {
idx: set.setIndex + 1,
text: parts.length > 0 ? parts.join(' × ') : '无数据'
}
})
return {
date: formatDateArr(item.date),
name: item.name || '训练记录',
setCount: item.setCount || 0,
// 容量:如果有重量,就是 重量×次数×组数;如果没有,留空或0
capacity: item.weight != null ? (item.weight * item.setCount) : 0,
sets: sets
}
})
})
// 3. 自动计算「最高重量、最高容量、最新日期」
const statData = computed(() => {
if (!historyList.value.length) return { maxWeight: '0kg', maxReps: 0, maxCapacity: '0', maxCapacitySets: 0, recordDate: '' }
let maxW = 0, maxR = 0, maxC = 0, maxCS = 0
let latestDate = ''
historyList.value.forEach(item => {
// 最高重量:遍历每组数据找最大 weight
item.setConfigList?.forEach(set => {
if (set.weight > maxW) {
maxW = set.weight
maxR = set.reps || 0
}
})
// 最高容量:按 重量×次数×组数 计算
const capacity = item.weight ? item.weight * (item.setConfigList?.length || 0) : 0
if (capacity > maxC) {
maxC = capacity
maxCS = item.setConfigList?.length || 0
}
// 最新日期:取最大的 date 数组
const currentDateStr = formatDateArr(item.date)
if (!latestDate || item.date > latestDate) {
latestDate = currentDateStr
}
})
return {
maxWeight: maxW > 0 ? `${maxW}kg` : '0kg',
maxReps: maxR,
maxCapacity: maxC,
maxCapacitySets: maxCS,
recordDate: latestDate
}
})
defineExpose({ openHistoryPopup });
</script>
<style lang="scss" scoped>
$bg-dark: #2c2c2e;
$card-bg: #3a3a3c;
$text-main: #ffffff;
$text-gray: #8e8e93;
$theme-yellow: #f8d714;
.history-popup {
width: 100%;
max-height: 80vh;
background-color: $bg-dark;
border-radius: 24rpx 24rpx 0 0;
padding: 30rpx;
box-sizing: border-box;
.header {
.title {
font-size: 40rpx;
font-weight: bold;
color: $text-main;
margin-bottom: 30rpx;
}
}
.top-cards {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
.card {
flex: 1;
background-color: $card-bg;
border-radius: 16rpx;
padding: 30rpx;
.label {
font-size: 28rpx;
color: $text-gray;
margin-bottom: 16rpx;
}
.value {
font-size: 36rpx;
font-weight: bold;
color: $theme-yellow;
margin-bottom: 10rpx;
}
.date {
font-size: 24rpx;
color: $text-gray;
}
}
}
.chart-card {
background-color: $card-bg;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.title {
font-size: 28rpx;
color: $text-main;
}
.unit {
font-size: 24rpx;
color: $text-gray;
}
}
.chart-placeholder {
height: 200rpx;
position: relative;
.line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 160rpx;
border-bottom: 1rpx solid #444;
.point {
position: absolute;
top: 0;
transform: translateX(-50%);
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: $theme-yellow;
margin: 0 auto;
}
.val {
display: block;
font-size: 26rpx;
color: $theme-yellow;
text-align: center;
margin-bottom: 10rpx;
}
}
}
.axis {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
.axis-item {
font-size: 24rpx;
color: $text-gray;
position: absolute;
transform: translateX(-50%);
}
}
}
}
.list {
.record-item {
background-color: $card-bg;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
.date {
font-size: 26rpx;
color: $text-gray;
margin-bottom: 20rpx;
}
.name {
font-size: 32rpx;
font-weight: bold;
color: $text-main;
margin-bottom: 16rpx;
}
.info {
font-size: 26rpx;
color: $text-gray;
margin-bottom: 20rpx;
}
.set-list {
.set {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.num {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background-color: #444;
color: $text-main;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
margin-right: 16rpx;
}
.text {
font-size: 28rpx;
color: $text-main;
}
}
}
}
}
}
</style>
\ No newline at end of file
... ...
<template>
<view>
<!-- 动作排序弹窗 -->
<up-popup :show="actionSortShow" mode="bottom" bgColor="#242424" round="24rpx">
<view class="action-sort-popup">
<view class="sort-header">
<view class="operate">
<view class="close" @click="actionSortShow = false">
<up-icon name="close" color="#fff" size="20"></up-icon>
</view>
<view class="title">动作排序</view>
<view class="add" @click="addActionsPopup">
<up-icon name="plus" color="#F2DC0B" size="15"></up-icon>
<text>添加</text>
</view>
</view>
</view>
<scroll-view scroll-y class="sort-scroll-view" :scroll-with-animation="true">
<view v-for="(item, index) in sortList" :key="item.unitId || index" class="drag-item-card">
<view class="item-left">
<up-icon name="trash" color="#fff" size="22" @click="deleteUnit(item, index)"></up-icon>
<image class="item-img" :src="item.exercises?.[0]?.urlImage || ''" v-if="item.unitType === 1"
mode="aspectFill" />
<image class="item-img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png" v-else
mode="aspectFill" />
<view class="item-info">
<view class="item-name">{{ item.unitName || '未命名动作' }}</view>
<view class="item-tags" v-if="item.unitType === 2">超级组</view>
<view class="item-tags" v-if="item.unitType === 1">
{{ item.exercises?.[0]?.categoryDescription || '' }}
{{ item.exercises?.[0]?.equipmentDescription || '' }}
</view>
</view>
</view>
<view class="item-right">
<up-icon name="plus-circle" color="#666" size="22" @click="copyUnit(item, index)"></up-icon>
</view>
</view>
</scroll-view>
</view>
</up-popup>
<!-- 动作新增组件 -->
<!-- <addActions :show="addActionsShow" @close="addActionsShow = false" /> -->
</view>
</template>
<script setup>
import { ref } from 'vue';
import { useTrainingStore } from '@/sheep/store/trainingStore'
const emit = defineEmits(['open-add-actions'])
const trainingStore = useTrainingStore()
// --- 动作排序 ---弹窗
const actionSortShow = ref(false);
const sortList = ref([])
// 这里是打开动作排序的方法
const openActionSort = () => {
sortList.value = trainingStore.actionDetail?.units?.map((unit, index) => ({
...unit,
exercises: unit.exercises || []
})) || [];
actionSortShow.value = true;
};
// const addActionsPopup = () => {
// if (actionSortShow.value === true) {
// actionSortShow.value = false
// }
// console.log("==================== 点击了加号!")
// addActionsShow.value = true // 打开弹窗
// console.log("✅ 弹窗打开:", addActionsShow.value)
// }
const addActionsPopup = () => {
actionSortShow.value = false
// 不在这里打开,而是通知父页面
emit('open-add-actions')
}
const deleteUnit = (item, index) => {
console.log('🗑️ 点击删除:', item)
console.log('🗑️ 删除索引:', index)
uni.showModal({
title: '确认删除',
content: `确定要删除【${item.unitName}】吗?`,
success: (res) => {
if (res.confirm) {
console.log('✅ 用户确认删除')
sortList.value.splice(index, 1)
trainingStore.actionDetail.units.splice(index, 1)
console.log('✅ 删除后剩余 units:', trainingStore.actionDetail.units)
} else {
console.log('❌ 用户取消删除')
}
}
})
}
// 复制当前 unit,并插入到下方
const copyUnit = (item, index) => {
console.log('📋 点击复制:', item)
console.log('📋 复制索引:', index)
const newUnit = JSON.parse(JSON.stringify(item))
sortList.value.splice(index + 1, 0, newUnit)
trainingStore.actionDetail.units.splice(index + 1, 0, newUnit)
console.log('✅ 复制成功!当前列表:', sortList.value)
uni.showToast({
title: '已复制到下方',
icon: 'success'
})
}
defineExpose({
openActionSort
})
</script>
<style lang="scss" scoped>
/* ========== 拖拽排序 优化样式 ========== */
.action-sort-popup {
width: 100%;
height: 80vh;
display: flex;
flex-direction: column;
background: #242424;
border-radius: 20rpx 20rpx 0 0;
}
.sort-header {
padding: 30rpx;
color: #fff;
height: 10vh;
box-sizing: border-box;
.operate {
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-size: 32rpx;
font-weight: bold;
}
.add {
display: flex;
align-items: center;
color: #f2dc0b;
font-size: 26rpx;
gap: 8rpx;
}
}
// .tip {
// font-size: 24rpx;
// color: #8e8e93;
// margin-top: 16rpx;
// }
}
.sort-scroll-view {
height: 70vh;
}
// .drag-area {
// width: 100%;
// position: relative;
// }
// .drag-view {
// width: 100%;
// position: absolute;
// transition: transform 0.2s ease;
// }
/* 拖拽项卡片 */
.drag-item-card {
width: 100%;
background: #2c2c2e;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
transition: all 0.2s ease;
border: 2rpx solid transparent;
}
/* 拖拽中高亮效果 */
.drag-item-card.is-active {
background: #3a3a3c;
border-color: #fbdf09;
transform: scale(1.02);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.4);
}
.item-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.item-img {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
object-fit: cover;
}
.item-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.item-name {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
.item-tags {
font-size: 22rpx;
color: #8e8e93;
}
.item-right {
display: flex;
align-items: center;
gap: 30rpx;
}
/* 拖拽手柄 */
.drag-handle {
padding: 20rpx;
/* 增大点击区域 */
color: #8e8e93;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
/* 激活状态下的高亮 */
.drag-item-card.is-active .drag-handle {
color: #fbdf09;
transform: scale(1.1);
}
/* 提示用户可长按 */
.drag-handle:active {
opacity: 0.7;
}
</style>
\ No newline at end of file
... ...
<template>
<view class="action-container" @click="closeDifficulty">
<view class="action-list" v-if="deleteActionShow === false">
<view class="card">
<view class="card-top" @click.stop="isExpanded = !isExpanded">
<view v-if="type === 1">
<image class="action-image" :src="actionDetail?.urlImage || lostImage" mode="aspectFill"></image>
</view>
<view v-if="type === 2">
<image class="action-image"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
mode="aspectFill"></image>
</view>
<view class="action-info">
<!-- 动作的 -->
<text class="name">{{ actionDetail?.name }}</text>
<text class="tags" v-if="type === 1">{{ actionDetail?.categoryDescription }} {{
actionDetail?.equipmentDescription }}</text>
<!-- 这里还缺kg/要搞个计算属性 -->
<text class="tags" v-if="type === 2">超级组</text>
<view class="dot-list" v-if="!isExpanded">
<view class="dot" v-for="(item, index) in recordList" :key="'dot-' + index"></view>
</view>
</view>
<view class="more-circle" @click.stop="toggleTips">
<up-icon name="more-dot-fill" color="#fff" size="15" class="icon"></up-icon>
<view class="tips" v-if="showTips">
<view class="tip" @click="openBeizhu">文字备注</view>
<view class="tip" @click="openAllQuickTimer">组间休息</view>
<view class="tip" @click="handleReplaceAction">动作替换</view>
<view class="tip" @click="deleteAction">删除动作</view>
</view>
</view>
</view>
<view class="main" v-if="isExpanded">
<!-- 当类型为4,5时,显示体重输入框 -->
<!--动作的 recordList 的输入控制 -->
<template v-if="type === 1">
<view class="userWeight" v-if="[4, 5].includes(actionDetail.exerciseType)">
<view class="row" hover-class="none" hover-stop-propagation="false">
<text class="label">体重</text>
<view class="userWeight-values" @click="openWeightPicker">
<text class="weight-text" style="color: #fbdc00; font-size: 20rpx;">
{{ displayWeight }}
</text>
<up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon>
</view>
</view>
<view class="explain" v-if="actionDetail?.exerciseType === 4">
该动作为自重加重,根据你的体重加上辅助器械重量计算容量,下方的填写重量为辅助器械重量。
</view>
<view class="explain" v-if="actionDetail?.exerciseType === 5">
该动作为自重减重,根据你的体重减去辅助器械重量计算容量,下方的填写重量为辅助器械重量。
</view>
</view>
<!-- 导入子组件 -->
<action-set-input v-for="(item, index) in recordList" :key="index"
:exercise-type="actionDetail.exerciseType" :record="item" :index="index" :is-editing="isEditing"
@openPicker="openPicker" @openQuickTimer="openQuickTimer" @toggleActive="toggleActive" @addRow="addRow"
@deleteRow="deleteRow" :user-weight="userWeight" @change-weight="userWeight = $event" />
</template>
<!-- 超级组的训练数据控制 -->
<template v-else-if="type === 2">
<view class="super-group">
<view class="img-list" v-if="type === 2">
<image v-for="item in (actionDetail?.exercises || [])" :key="item.id" :src="item?.urlImage || lostImage"
class="img"></image>
</view>
<!-- 超级组:如果内部包含自重加重/减重,显示体重输入v-if="hasWeightExercise" -->
<view class="userWeight" v-if="hasWeightExercise">
<view class="row">
<text class="label">体重</text>
<view class="userWeight-values" @click="openWeightPicker">
<text class="weight-text" style="color: #fbdc00; font-size: 20rpx;">
{{ displayWeight }}
</text>
<up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon>
</view>
</view>
<!-- <view class="explain" v-if="actionDetail?.exerciseType === 4">
该动作为自重加重,根据你的体重加上辅助器械重量计算容量,下方的填写重量为辅助器械重量。
</view>
<view class="explain" v-if="actionDetail?.exerciseType === 5">
该动作为自重减重,根据你的体重减去辅助器械重量计算容量,下方的填写重量为辅助器械重量。
</view> -->
</view>
<!-- 外层:循环【每一组】 -->
<view class="set-group" v-for="(setItem, setIndex) in superTotalSets" :key="setIndex">
<view class="set-header">
<view class="index-box">{{ setIndex + 1 }}</view>
<view class="set-action-names">
<text v-for="(sub, idx) in actionDetail.exercises" :key="sub.id">
{{ String.fromCharCode(65 + idx) }} {{ sub.name }}
</text>
</view>
<!-- 休息时间选择 -->
<view class="timer-selector" @click.stop="openSuperQuickTimer(setIndex)" v-if="!isEditing">
<text class="text">{{ superGroupSetData(setIndex).quickTimeDisplay || '60s' }}</text>
<up-icon name="arrow-down" color="#8e8e93" size="10"></up-icon>
</view>
<!-- 勾选框 -->
<view class="icon" @click.stop="toggleSuperSetActive(setIndex)"
:class="{ active: superGroupSetData(setIndex).isActive }" v-if="!isEditing">
<up-icon name="checkmark" :color="superGroupSetData(setIndex).isActive ? '#000' : '#919191'"
size="20" bold></up-icon>
</view>
<!-- 新增和删除按钮 -->
<view class="icons" v-if="isEditing">
<view class="icon" @click.stop="addSuperSet">
<up-icon name="plus-circle" color="#fff" size="20" bold></up-icon>
</view>
<view class="icon del" @click.stop="deleteSuperSet(setIndex)">
<up-icon name="trash" color="#e63e1e" size="20" bold></up-icon>
</view>
</view>
</view>
<!-- 内层:循环【组内的每个子动作A/B】 -->
<super-set-input v-for="(sub, idx) in actionDetail.exercises" :key="sub.id"
:sub-exercise-type="sub.exerciseType" :set-index="setIndex" :sub-index="idx" :user-weight="userWeight"
:data="getSubActionData(setIndex, sub.id)" @open-time-picker="openSuperPicker(setIndex, sub.id)" />
</view>
</view>
</template>
</view>
<view class="button-group" v-if="isExpanded">
<view class="btn-item" @click.stop="addGroup">加一组</view>
<view class="btn-item" @click="openHistory(props?.id)">历史</view>
<!-- 历史弹窗 -->
<actionHistory ref="historyPopupRef" :actionName="actionDetail.name" :historyType="props.type" />
<!-- 动作难度选择弹窗 -->
<view class="btn-item" @click.stop="showDifficulty = !showDifficulty">
<text :style="{ color: currentDifficulty.color }">{{ currentDifficulty.label }}</text>
<up-icon name="arrow-down" size="8" color="#fff"></up-icon>
<view class="difficulty-pop" v-if="showDifficulty">
<view class="difficulty-item" @click.stop="selectDifficulty('轻松', '#b2f055')">
<text :style="{ color: '#b2f055' }">轻松</text>
<up-icon v-if="currentDifficulty.label === '轻松'" name="checkmark" color="#b2f055" size="16"
bold></up-icon>
</view>
<view class="difficulty-item" @click.stop="selectDifficulty('正常', '#ffffff')">
<text :style="{ color: '#ffffff' }">正常</text>
<up-icon v-if="currentDifficulty.label === '正常'" name="checkmark" color="#ffffff" size="16"
bold></up-icon>
</view>
<view class="difficulty-item" @click.stop="selectDifficulty('困难', '#e26c2a')">
<text :style="{ color: '#e26c2a' }">困难</text>
<up-icon v-if="currentDifficulty.label === '困难'" name="checkmark" color="#e26c2a" size="16"
bold></up-icon>
</view>
</view>
</view>
<view class="btn-item" @click.stop="isEditing = !isEditing">
{{ isEditing ? '完成' : '编辑' }}
</view>
</view>
</view>
</view>
<up-picker :show="showPicker" :columns="timeColumns" :defaultIndex="defaultTimeIndex" title="请选择运动时长"
@confirm="onTimeConfirm" @cancel="showPicker = false" closeOnClickOverlay popupClass="custom-picker"></up-picker>
<up-picker :show="showQuickPicker" :columns="quickColumns" title="设置休息时长" @confirm="onQuickConfirm"
@cancel="showQuickPicker = false" closeOnClickOverlay popupClass="custom-picker"></up-picker>
<!-- 备注弹窗组件(可创建备注成功,但无法显示到动作详情弹窗组件) -->
<beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" />
<!-- 动作替换弹窗 -->
<replaceActionPopup :show="replacePopupShow" :unitIndex="unitIndex" @close="replacePopupShow = false" />
<!-- 体重选择器 -->
<up-picker :show="showWeightPicker" :columns="weightColumns" :defaultIndex="defaultWeightIndex" title="设置体重"
@confirm="onWeightConfirm" @cancel="showWeightPicker = false" closeOnClickOverlay
popupClass="custom-picker"></up-picker>
</view>
</template>
<script setup>
import { ref, reactive, watch, computed, nextTick, onMounted } from 'vue';
import beizhu from '@/pages/xunji/components/beizhu.vue';
// import actionHistory from '../components/dongzuo-lianxi-history.vue'
import actionHistory from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-lianxi-history.vue'
import SupersetsApi from '@/sheep/api/motion/supersets';
import ExercisesApi from '@/sheep/api/motion/exercises';
import { getCurrentInstance } from 'vue';
import { useTrainingStore } from '@/sheep/store/trainingStore'
// import replaceActionPopup from '../components/replace-action-popup.vue'
import replaceActionPopup from '@/pages/xunji/components/dongzuo-lianxi/replace-action-popup.vue'
// import actionSetInput from '@/pages4/components/action-set-input.vue'
import actionSetInput from '@/pages/xunji/components/dongzuo-lianxi/action-set-input.vue'
// import superSetInput from '../components/super-set-input.vue'
import superSetInput from '@/pages/xunji/components/dongzuo-lianxi/super-set-input.vue'
const trainingStore = useTrainingStore()
const deleteActionShow = ref(false)
const showTips = ref(false);
const isExpanded = ref(false);
const isEditing = ref(false);
const showPicker = ref(false);
const showQuickPicker = ref(false);
const activeIndex = ref(0);
const activeActionId = ref('');
const emit = defineEmits(['register', 'update:groupCount', 'replace-action', 'deleteAction', 'replaceActionForSave'])
const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
const historyPopupRef = ref(null)
const showBeizhuRef = ref(null);
const userWeight = ref(70)
// 休息时间设置模式:single=单行,all=全部组
const restEditMode = ref('single');
const replacePopupShow = ref(false)
// 接收父组件的数据
const props = defineProps({
id: [Number, String],
type: [Number, String],
index: Number, // 新增
unitIndex: Number,
actionDetail: {
type: Object,
default: () => ({})
},
isDailyTemplates: {
type: Boolean,
default: false
}
})
watch(() => props.id, () => {
console.log('dongzuo组件 id/type:', props.id, props.type)
console.log('打印props.actionDetail', props.actionDetail)
console.log('dongzuo组件 unitIndex:', props.unitIndex)
if (props.type === 2)
console.log('打印父组件传过来超级组动作列表的props.actionDetail.exercises', props.actionDetail.exercises);
}, { immediate: true })
emit('register', props.index, getCurrentInstance()?.proxy)
// 体重选择器
const displayWeight = computed(() => {
return `${userWeight.value}kg`
})
// 2. 体重选择器状态
const showWeightPicker = ref(false)
const weightColumns = reactive([
// 整数部分:10 ~ 250
Array.from({ length: 241 }, (_, i) => (i + 10).toString()),
// 小数部分:0 ~ 9
Array.from({ length: 10 }, (_, i) => i.toString()),
['千克(kg)'] // 第三列固定文字
])
const defaultWeightIndex = ref([60, 0, 0]) // 默认 70.0kg(10+60=70)
// 3. 打开体重选择器
const openWeightPicker = () => {
// 直接用数字处理,避免转字符串
const num = userWeight.value
const intPart = Math.floor(num)
const decPart = Math.round((num - intPart) * 10) // 取小数第一位
// 计算索引(整数部分:intPart - 10,小数部分:decPart)
const intIndex = Math.max(0, Math.min(intPart - 10, 240))
defaultWeightIndex.value = [intIndex, decPart, 0]
showWeightPicker.value = true
}
// 4. 体重选择器确认事件
const onWeightConfirm = (e) => {
const intPart = e.value[0] // 整数部分
const decPart = e.value[1] // 小数部分
// 拼接成完整体重
userWeight.value = parseFloat(`${intPart}.${decPart}`)
showWeightPicker.value = false
}
// 文字备注
// 打开备注弹窗
const openBeizhu = () => {
nextTick(() => {
if (showBeizhuRef.value) {
showBeizhuRef.value.open(props.actionDetail.id);
}
});
};
// 接收子组件传过来的备注内容
const handleNoteSave = async (content) => {
try {
if (props.type == 2) {
await SupersetsApi.addNotes({
supersetsId: props.id,
content: content,
});
} else {
await ExercisesApi.addNotes({
exerciseId: props.id,
content: content,
});
}
props.actionDetail.userNote = content;
} catch (e) {
console.log(e);
}
};
const toggleTips = () => {
showTips.value = !showTips.value
}
// 超级组是否包含间歇类型动作(用来控制组头休息时间选择器的显示)
const hasIntervalExercise = computed(() => {
if (props.type !== 2) return false
const exercises = props.actionDetail?.exercises || []
// 只要有一个子动作是间歇类型(6),就不显示组头的休息时间
return exercises.some(sub => sub.exerciseType === 6)
})
// 替换动作
const handleReplaceAction = () => {
showTips.value = false
replacePopupShow.value = true // 打开弹窗
console.log("✅ 弹窗打开:", replacePopupShow.value)
}
// 超级组是否包含 4(自重加重) / 5(自重减重)
const hasWeightExercise = computed(() => {
if (props.type !== 2) return false
const exercises = props.actionDetail?.exercises || []
return exercises.some(sub => [4, 5].includes(sub.exerciseType))
})
// 删除动作
const deleteAction = () => {
if (props.isDailyTemplates) {
showTips.value = false;
emit('deleteAction');
// trainingStore.actionDetail.units.splice(props.unitIndex, 1)
// trainingStore.clearTrainingStore()
return;
}
trainingStore.deleteUnitRecord(props.unitIndex);
console.log('训练类型 type =', trainingStore.type)
console.log('组件类型 props.type =', props.type)
console.log('unitIndex =', props.unitIndex)
deleteActionShow.value = true
// 1. 如果是【模板训练】,删除对应项
if (trainingStore.type === 3) {
console.log('模板训练:删除第', props.unitIndex, '个单元')
trainingStore.actionDetail.units.splice(props.unitIndex, 1)
}
else {
console.log('单个动作/超级组:清空')
trainingStore.clearTrainingStore()
}
// 关闭菜单
showTips.value = false
console.log('删除完成 ✅')
}
// 难度控制逻辑
const showDifficulty = ref(false);
const initialDifficulty = { label: '难度', color: '#ffffff' };
const currentDifficulty = ref({ ...initialDifficulty });
const selectDifficulty = (label, color) => {
if (currentDifficulty.value.label === label) {
currentDifficulty.value = { ...initialDifficulty };
} else {
currentDifficulty.value = { label, color };
}
showDifficulty.value = false;
};
// 点击背景关闭所有弹窗
const closeDifficulty = () => {
showDifficulty.value = false;
showTips.value = false;
};
// 超级组数据控制
const superRecordMap = ref({})
// 超级组:总组数(计算第一个动作的组数即可)
const superTotalSets = computed(() => {
if (props.type !== 2) return 0
const exercises = props.actionDetail?.exercises || []
if (exercises.length === 0) return 0
const firstId = exercises[0].id
console.log('打印超级组的组数:', superRecordMap.value[firstId]?.length);
return superRecordMap.value[firstId]?.length || 0
})
onMounted(() => {
// 从 Pinia 加载当前 unit 的训练数据
const savedData = trainingStore.getUnitRecord(props.unitIndex);
// 恢复体重
userWeight.value = savedData.userWeight || 70;
console.log('从pinia加载的数据:', savedData);
// 1. 先从 Pinia 恢复已有数据
if (props.type === 1) {
// 关键:只有当前是初始默认1条,才赋值,不再覆盖watch结果
if (savedData.records?.[props.actionDetail.id]) {
recordList.value = [...savedData.records[props.actionDetail.id]]
console.log('onMounted兜底赋值', recordList.value)
}
} else if (props.type === 2) {
if (savedData.records && Object.keys(savedData.records).length > 0) {
superRecordMap.value = { ...savedData.records };
}
}
// 2. 超级组兜底初始化:只要有子动作,且 superRecordMap 为空就初始化
if (props.type === 2 && props.actionDetail?.exercises?.length) {
const exercises = props.actionDetail.exercises;
// 判断:当前超级组还没任何结构 才初始化
const firstId = exercises[0].id;
if (!superRecordMap.value[firstId]) {
const map = {};
exercises.forEach(item => {
map[item.id] = [{
h: '00', m: '00', s: '00', quickTimeDisplay: '60s',
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
}];
});
superRecordMap.value = map;
}
}
nextTick(() => {
emit('register', props.index, {
recordList: recordList.value,
superRecordMap: superRecordMap.value
})
})
});
// 动作组数据记录
const recordList = ref([
{ h: '00', m: '00', s: '00', quickTimeDisplay: '60s', distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false },
]);
watch(recordList, () => {
// console.log('recordList变化,发送长度:', recordList.value.length)
emit('update:groupCount', recordList.value.length)
}, { deep: true, immediate: true })
const saveToStore = () => {
let records = {};
if (props.type === 1) {
// 普通动作
records[props.actionDetail.id] = recordList.value;
} else if (props.type === 2) {
// 超级组
records = superRecordMap.value;
}
// 调用 Pinia 保存
trainingStore.saveUnitRecord(props.unitIndex, {
records: records,
userWeight: userWeight.value
});
};
watch([recordList, superRecordMap, userWeight], () => {
saveToStore();
console.log('+++++++++打印全局训练数据 trainingStore.unitRecords++++++++', trainingStore.unitRecords);
}, { deep: true, immediate: false });
const toggleActive = (index) => {
recordList.value[index].isActive = !recordList.value[index].isActive;
};
// 超级组工具方法
const superGroupSetData = (setIndex) => {
const firstSub = props.actionDetail?.exercises?.[0]
if (!firstSub) return {}
return superRecordMap.value[firstSub.id]?.[setIndex] || {}
}
const getSubActionData = (setIndex, actionId) => {
return superRecordMap.value[actionId]?.[setIndex] || {
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '',
weight: '',
reps: '',
duration: '',
restTime: '',
isActive: false
}
}
const openSuperPicker = (setIndex, actionId) => {
activeIndex.value = setIndex
activeActionId.value = actionId; // 保存当前动作ID
showPicker.value = true
}
const openSuperQuickTimer = (setIndex) => {
activeIndex.value = setIndex
showQuickPicker.value = true
}
const toggleSuperSetActive = (setIndex) => {
props.actionDetail.exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list?.[setIndex]) {
list[setIndex].isActive = !list[setIndex].isActive
}
})
}
// 增加超级组的set组数
const addSuperSet = () => {
const exercises = props.actionDetail?.exercises || []
exercises.forEach(sub => {
if (superRecordMap.value[sub.id]) {
superRecordMap.value[sub.id].push({
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '',
weight: '',
reps: '',
duration: '',
restTime: '',
isActive: false
})
}
})
}
// 删除超级组的组数
const deleteSuperSet = (setIndex) => {
const exercises = props.actionDetail?.exercises || []
exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list && list.length > 1) {
list.splice(setIndex, 1)
}
})
}
// 合并后的 Picker 逻辑
const timeColumns = reactive([
Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时'),
Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),
Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),
]);
const defaultTimeIndex = ref([0, 0, 0]);
const quickColumns = reactive([
Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分'),
Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '秒'),
]);
const openPicker = (index) => {
activeIndex.value = index;
const item = recordList.value[index];
const hIdx = timeColumns[0].indexOf(item.h + '时');
const mIdx = timeColumns[1].indexOf(item.m + '分');
const sIdx = timeColumns[2].indexOf(item.s + '秒');
defaultTimeIndex.value = [hIdx, mIdx, sIdx];
showPicker.value = true;
};
//
const openQuickTimer = (index) => {
activeIndex.value = index;
showQuickPicker.value = true;
};
// 组间休息
const openAllQuickTimer = () => {
restEditMode.value = 'all'; // 重点:标记为全部修改
showQuickPicker.value = true;
};
const onTimeConfirm = (e) => {
const rawValues = e.value.map((val) => String(val).replace(/[时分秒]/g, ''))
const setIdx = activeIndex.value
if (props.type === 1) {
const item = recordList.value[setIdx]
item.h = rawValues[0]
item.m = rawValues[1]
item.s = rawValues[2]
} else if (props.type === 2) {
// 只修改当前点击的动作,不批量修改所有动作!
const actionId = activeActionId.value;
const list = superRecordMap.value[actionId];
if (list?.[setIdx]) {
list[setIdx].h = rawValues[0];
list[setIdx].m = rawValues[1];
list[setIdx].s = rawValues[2];
}
}
showPicker.value = false
}
const onQuickConfirm = (e) => {
const min = parseInt(e.value[0]) || 0
const sec = parseInt(e.value[1]) || 0
const totalSeconds = min * 60 + sec
const setIdx = activeIndex.value
// ============== 统一设置所有组 ==============
if (restEditMode.value === 'all') {
// 普通动作
if (props.type === 1) {
recordList.value.forEach(item => {
item.quickTimeDisplay = totalSeconds + 's'
})
}
// 超级组
else if (props.type === 2) {
const firstSub = props.actionDetail?.exercises?.[0]
if (!firstSub) return
const allSets = superRecordMap.value[firstSub.id]
if (!allSets) return
allSets.forEach(set => {
set.quickTimeDisplay = totalSeconds + 's'
})
}
}
// ============== 单行修改 ==============
else {
if (props.type === 1) {
recordList.value[setIdx].quickTimeDisplay = totalSeconds + 's'
} else if (props.type === 2) {
props.actionDetail.exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list?.[setIdx]) {
list[setIdx].quickTimeDisplay = totalSeconds + 's'
}
})
}
}
restEditMode.value = 'single'
showQuickPicker.value = false
}
// 单动作的增加/删除
const addRow = () => {
recordList.value.push({
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '', weight: '', reps: '', isActive: false,
});
};
const deleteRow = (index) => {
if (recordList.value.length > 1) {
recordList.value.splice(index, 1);
}
};
const addGroup = () => {
if (props.type === 1) {
addRow()
} else if (props.type === 2) {
addSuperSet()
}
}
// 历史
const openHistory = (id) => {
if (historyPopupRef.value && historyPopupRef.value.openHistoryPopup) {
historyPopupRef.value.openHistoryPopup(id);
}
}
// 动作名称渲染
const exerciseNamesWithLabel = computed(() => {
const exercises = props.actionDetail?.exercises || []
return exercises.map((item, index) => {
const label = String.fromCharCode(65 + index)
return `${label} ${item?.name || '未知动作'}`
})
})
// 获得真正的重量(支持普通动作 + 超级组)
const getRealWeight = (item, subExerciseType = null) => {
const userW = Number(userWeight.value) || 0
const inputW = Number(item.weight) || 0
// 重要:超级组子动作会传入自己的 exerciseType
let type = subExerciseType ?? props.actionDetail?.exerciseType
// console.log('【重量计算】', {
// 动作类型: type,
// 体重: userW,
// 输入重量: inputW,
// 最终重量:
// type === 0 ? inputW :
// type === 4 ? userW + inputW :
// type === 5 ? userW - inputW : 0
// })
let realWeight = 0;
if (type === 0) {
realWeight = inputW;
} else if (type === 4) {
realWeight = userW + inputW;
} else if (type === 5) {
realWeight = userW - inputW;
// 只做数据兜底修正,不在这里弹提示
if (realWeight < 0) {
realWeight = 0;
}
}
return realWeight;
}
// 只暴露 isActive = true 的数据
const exposeRecordList = computed(() => {
return recordList.value.filter(item => item.isActive).map(item => {
return {
...item,
weight: getRealWeight(item) // 👈 自动计算真实重量
}
})
})
const exposeSuperRecordMap = computed(() => {
const map = {}
for (const key in superRecordMap.value) {
// 找到当前子动作的类型
const subExercise = props.actionDetail?.exercises?.find(s => s.id == key)
const subType = subExercise?.exerciseType
map[key] = superRecordMap.value[key]
.filter(item => item.isActive)
.map(item => {
return {
...item,
weight: getRealWeight(item, subType) // ✅ 传入子动作类型
}
})
}
return map
})
// 总组数
const totalSetCount = computed(() => {
// 1. 普通动作 type === 1
if (props.type === 1) {
return recordList.value.length
}
// 2. 超级组 type === 2
if (props.type === 2) {
const exercises = props.actionDetail?.exercises || []
if (exercises.length === 0) return 0
const firstId = exercises[0].id
return superRecordMap.value[firstId]?.length || 0
}
return 0
})
// 重量
// 1. 已勾选的重量总和(黄色框框)→ 现在 = 真实重量 × 次数
const checkedWeight = computed(() => {
let sum = 0
// 普通动作
if (props.type === 1 && recordList.value) {
recordList.value.forEach(item => {
if (item.isActive) {
const realWeight = getRealWeight(item) // 👈 真实重量(自重/加重/减重)
const reps = Number(item.reps) || 0 // 👈 次数
sum += realWeight * reps // 👈 重量 × 次数
}
})
}
// 超级组
if (props.type === 2 && superRecordMap.value) {
for (const actionId in superRecordMap.value) {
// 拿到子动作类型
const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)
const subType = subExercise?.exerciseType
superRecordMap.value[actionId].forEach(item => {
if (item.isActive) {
const realWeight = getRealWeight(item, subType)
const reps = Number(item.reps) || 0
sum += realWeight * reps
}
})
}
}
return sum
})
// 2. 所有组的重量总和(不管勾没勾)→ 现在 = 真实重量 × 次数
const totalWeight = computed(() => {
let sum = 0
if (props.type === 1 && recordList.value) {
recordList.value.forEach(item => {
const realWeight = getRealWeight(item)
const reps = Number(item.reps) || 0
sum += realWeight * reps
})
}
if (props.type === 2 && superRecordMap.value) {
for (const actionId in superRecordMap.value) {
const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)
const subType = subExercise?.exerciseType
superRecordMap.value[actionId].forEach(item => {
const realWeight = getRealWeight(item, subType)
const reps = Number(item.reps) || 0
sum += realWeight * reps
})
}
}
return sum
})
// ==============================================
// 统一监听:普通动作 + 超级组 的自重减重(type=5)
// ==============================================
watch(
() => [recordList.value, superRecordMap.value],
() => {
const userW = userWeight.value || 0;
// --------------------------
// 1. 监听【普通动作】
// --------------------------
if (props.type === 1 && props.actionDetail?.exerciseType === 5) {
recordList.value.forEach((item) => {
const inputW = Number(item.weight) || 0;
if (inputW > userW) {
item.weight = "";
uni.showToast({
title: "辅助重量不能超过体重",
icon: "none",
duration: 2000,
});
}
});
}
// --------------------------
// 2. 监听【超级组】(包含多个子动作)
// --------------------------
if (props.type === 2) {
// 遍历超级组里的所有子动作
props.actionDetail?.exercises?.forEach((sub) => {
// 只校验 自重减重 type=5
if (sub.exerciseType === 5) {
const sets = superRecordMap.value[sub.id] || [];
sets.forEach((item) => {
const inputW = Number(item.weight) || 0;
if (inputW > userW) {
item.weight = "";
uni.showToast({
title: "辅助重量不能超过体重",
icon: "none",
duration: 2000,
});
}
});
}
});
}
},
{ deep: true }
);
defineExpose({
recordList: exposeRecordList, // 过滤后
superRecordMap: exposeSuperRecordMap, // 过滤后
totalSetCount, //全部的组数
checkedWeight, // 已勾选重量
totalWeight // 全部重量
})
// 打开备注弹窗
// const openBeizhu = () => {
// console.log('传递到备注组件的id', actionId.value)
// showBeizhuRef.value.open(actionId.value)
// }
watch(() => props.actionDetail, () => {
console.log('actionDetail.exercises:', props.actionDetail?.exercises)
}, { immediate: true })
</script>
<style lang="scss" scoped>
$bg-black: #303030;
$card-bg: #1c1c1e;
$item-bg: #2c2c2e;
$text-main: #ffffff;
$text-gray: #8e8e93;
$theme-yellow: #f8d714;
.action-container {
width: 100%;
/* 确保容器足够高以接收点击事件 */
background-color: $bg-black;
.action-list {
width: 100%;
.card {
border-radius: 24rpx;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
.card-top {
display: flex;
align-items: center;
padding: 20rpx 30rpx 0;
box-sizing: border-box;
.action-image {
width: 110rpx;
height: 110rpx;
border-radius: 16rpx;
background-color: #3a3a3c;
}
.action-info {
flex: 1;
margin-left: 24rpx;
.name {
font-size: 34rpx;
font-weight: 600;
color: $text-main;
}
.tags {
font-size: 24rpx;
color: $text-gray;
margin-top: 6rpx;
margin-left: 6rpx;
}
.dot-list {
display: flex;
gap: 10rpx;
margin-top: 8rpx;
.dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: $text-main;
}
}
}
.more-circle {
width: 60rpx;
height: 60rpx;
background: $item-bg;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid #a4a4a4;
position: relative;
.icon {
transform: rotate(90deg);
}
.tips {
position: absolute;
color: #fff;
background-color: #3e3e3e;
z-index: 6;
right: 10rpx;
top: 20rpx;
border-radius: 10rpx;
.tip {
width: 120rpx;
height: 40rpx;
padding: 20rpx 30rpx;
text-align: center;
font-size: 24rpx;
line-height: 40rpx;
}
}
}
}
.main {
width: 100%;
margin-top: 20rpx;
.img-list {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20rpx;
padding: 20rpx;
box-sizing: border-box;
.img {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
}
}
// .userWeight-values {
// display: flex;
// align-items: center;
// gap: 8rpx; // 控制“体重文本”和“编辑图标”的间距
// padding: 8rpx;
// .weight-text {
// color: #fadc00;
// font-size: 20rpx;
// }
// }
.userWeight {
padding: 16rpx 32rpx;
gap: 16rpx;
.row {
display: flex;
align-items: center;
gap: 16rpx;
.label {
font-size: 20rpx;
color: $text-main;
font-weight: 500;
}
.userWeight-values {
display: flex;
align-items: center;
gap: 8rpx; // 控制“体重文本”和“编辑图标”的间距
padding: 8rpx;
.weight-text {
color: #fadc00;
font-size: 20rpx;
}
}
}
.explain {
font-size: 20rpx;
color: #fff;
}
}
.record-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 10rpx 30rpx;
box-sizing: border-box;
&.active {
background-color: #4c4d3b;
}
.left {
display: flex;
flex-direction: column;
gap: 10rpx;
.left-header {
display: flex;
align-items: center;
gap: 20rpx;
.index-box {
width: 50rpx;
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: $text-main;
}
.input-col,
.distance {
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
padding: 5rpx 10rpx;
display: flex;
flex-direction: column;
justify-content: center;
.label {
font-size: 15rpx;
color: $text-gray;
margin-bottom: 2rpx;
}
.values {
display: flex;
align-items: baseline;
.num {
font-size: 32rpx;
color: $text-main;
}
.unit {
font-size: 15rpx;
color: $text-gray;
margin: 0 5rpx;
}
}
}
.distance {
min-width: 100rpx;
:deep(.u-input) {
padding: 0 !important;
}
.u-input__content__field {
font-size: 32rpx !important;
color: $text-main !important;
}
}
.timer-selector {
width: 75rpx;
height: 75rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1rpx solid #a4a4a4;
.text {
font-size: 20rpx;
color: $text-gray;
}
}
}
.exercise-name-list {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
}
.exercise-name {
font-size: 26rpx;
color: $text-main;
line-height: 1.2;
}
.goal-list {
width: 100%;
margin-top: 10rpx;
.goal-item {
display: flex;
align-items: center;
gap: 20rpx;
border-radius: 10rpx;
.serial {
width: 50rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #929292;
font-weight: bold;
}
.goal-input-box {
display: flex;
flex-direction: column;
justify-content: center;
background-color: #1f1f1f;
width: 100rpx;
height: 70rpx;
border-radius: 10rpx;
.values {
display: flex;
align-items: flex-end;
padding-left: 10rpx;
box-sizing: border-box;
:deep(.uni-input-input[type='number']) {
text-align: center;
}
.num {
font-size: 32rpx;
color: $text-main;
}
.unit {
font-size: 15rpx;
color: $text-gray;
margin: 0 5rpx;
}
// 确保 up-input 内部文字样式一致
}
}
}
// 新增的间歇运动样式
.interval {
display: flex;
gap: 20rpx;
margin-top: 10rpx;
.goal-input-box {
flex: none;
width: 120rpx;
.label {
font-size: 15rpx;
color: $text-gray;
margin-bottom: 2rpx;
}
}
}
}
}
.icons {
display: flex;
gap: 20rpx;
.icon {
width: 70rpx;
height: 70rpx;
background-color: #1f1f1f;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: $theme-yellow;
}
}
.del {
background-color: #58372d;
}
}
}
}
// 新增超级组样式
.super-group {
width: 100%;
}
.super-record-row {
align-items: center;
background-color: red;
min-height: 100rpx;
}
.super-left {
flex-direction: row !important;
align-items: center;
gap: 20rpx;
}
.set-group {
margin-bottom: 30rpx;
}
.set-header {
display: flex;
align-items: center;
padding: 10rpx 30rpx;
gap: 20rpx;
.icons {
display: flex;
gap: 20rpx;
.icon {
width: 70rpx;
height: 70rpx;
background-color: #1f1f1f;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.del {
background-color: #58372d;
}
}
.index-box {
width: 50rpx;
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #fff;
}
.set-action-names {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
text {
font-size: 26rpx;
color: #fff;
}
}
.timer-selector {
width: 75rpx;
height: 75rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1rpx solid #a4a4a4;
.text {
font-size: 20rpx;
color: #8e8e93;
}
}
.icon {
width: 70rpx;
height: 70rpx;
background-color: #1f1f1f;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: $theme-yellow;
}
}
}
.serial {
width: 50rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #929292;
font-weight: bold;
}
.super-action-item {
margin-bottom: 30rpx;
}
.super-action-title {
font-size: 28rpx;
color: #fff;
padding: 10rpx 30rpx;
font-weight: bold;
}
.button-group {
display: flex;
justify-content: space-between;
padding: 30rpx;
box-sizing: border-box;
.btn-item {
background-color: #1f1f1f;
font-size: 26rpx;
color: #fff;
padding: 15rpx 35rpx;
border-radius: 50rpx;
display: flex;
align-items: center;
gap: 10rpx;
position: relative;
.difficulty-pop {
position: absolute;
bottom: 80rpx;
left: 50%;
transform: translateX(-50%);
background-color: #3e3e3e;
border-radius: 16rpx;
overflow: hidden;
z-index: 99;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
.difficulty-item {
width: 180rpx;
height: 80rpx;
padding: 0 25rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 26rpx;
border-bottom: 1rpx solid #4e4e4e;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #4e4e4e;
}
}
}
}
}
}
}
}
</style>
... ...
<template>
<view class="popup-container" v-if="props.show" @click="closeAddMenu">
<!-- 搜索栏 -->
<view class="add-btn-wrapper" @click.stop="addExercise">
<uni-icons type="plus" size="35" color="#333"></uni-icons>
<view class="floating-menu" v-show="replaceAddshow" @click.stop>
<view class="menu-item" @click="addnewmotion">
<uni-icons type="plus" size="24" color="#333"></uni-icons>
<text class="menu-text">新增动作</text>
</view>
</view>
</view>
<view class="search-bar">
<!-- <view class="search-input-wrapper">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" />
</view> -->
</view>
<!-- 左右布局 -->
<view class="layout-container">
<!-- 左侧分类导航 -->
<scroll-view scroll-y class="left-nav">
<view class="nav-item" @click="handleCollectClick('collect')" :class="{ active: activeNav === 'collect' }">
收藏
</view>
<view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }"
@click="switchNav(nav.id)">
{{ nav.name }}
</view>
</scroll-view>
<!-- 右侧动作列表 -->
<scroll-view scroll-y class="right-content">
<!-- 部位筛选(可选) -->
<view class="tip" v-if="motionPart.length > 0">
<view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view>
<view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }"
@click="handlePartClick(item.id)">
{{ item.name }}
</view>
</view>
<view class="exercise-grid">
<view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0">
<!-- 普通动作列表 -->
<view class="equipment-list">
<view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
<view class="equipment-name"> {{ item.equipmentName }} </view>
<view class="action-list">
<view class="action-item" v-for="e in item.exercises" :key="e.id"
:class="{ selected: selectedAction?.id === e.id }" @click="selectAction(e)">
<image :src="e.url3dAnimation" mode="aspectFill" lazy-load class="action-img"></image>
<view class="action-name">{{ e.name }}</view>
<view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view>
</view>
</view>
</view>
<!-- 超级组列表(可选) -->
<view class="supers" v-if="superGroupInfo.length > 0">
<view class="supers-name"> 超级组 </view>
<view class="super" v-for="item in superGroupInfo" :key="item.id"
:class="{ selected: selectedAction?.id === item.id }" @click="selectAction(item, 2)">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
mode="aspectFill" lazy-load class="super-img"></image>
<view class="right">
<view class="super-name">{{ item.name }}</view>
<view class="parts">{{ item.primaryMuscles.join(' ') }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无动作数据</text>
</view>
</view>
</scroll-view>
</view>
<!-- 底部按钮栏 -->
<view class="bottom-btn-bar">
<view class="btn cancel-btn" @click="handleClose">取消</view>
<view class="btn confirm-btn" :class="{ disabled: !selectedAction }" @click="handleConfirm">
确认
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import SupersetsApi from '@/sheep/api/motion/supersets';
import { useActionStore } from '@/sheep/store/action';
import { useTrainingStore } from '@/sheep/store/trainingStore'
import { useTemplateActionStore } from '@/sheep/store/templateAction'
const templateStore = useTemplateActionStore()
const trainingStore = useTrainingStore()
const actionStore = useActionStore();
const emit = defineEmits(['close', 'confirm', 'replaceIdType']);
const replaceAddshow = ref(false);
// 数据
const activeNav = ref('collect'); // 默认选中收藏
const navItems = ref([]);
const exercises = ref([]);
const superGroupInfo = ref([]);
const motionPart = ref([]);
const activeMotionPart = ref('');
const selectedAction = ref(null); // 选中的动作/超级组
const selectedType = ref(1); // 1=动作 2=超级组
const allExercises = ref([]);
const props = defineProps({
show: {
type: Boolean,
default: false
},
unitIndex: Number,
})
// 监听弹窗打开,加载数据
watch(() => props.show, (val) => {
if (val) {
selectedAction.value = null;
// 加载收藏列表
loadCollectList();
loadSuperFavoriteList();
}
console.log('动作替换组件 unitIndex:', props.unitIndex)
});
// 关闭菜单
const closeAddMenu = () => {
replaceAddshow.value = false;
};
const addnewmotion = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });
};
const addExercise = () => {
replaceAddshow.value = !replaceAddshow.value;
};
// 加载分类
const loadCategories = async () => {
try {
await actionStore.getloadCategories();
navItems.value = actionStore.showCategories;
if (navItems.value.length > 0) {
switchNav(navItems.value[0].id);
}
} catch (error) {
console.error('获取分类失败:', error);
}
};
// 加载收藏动作列表
const loadCollectList = async () => {
try {
const res = await ExercisesApi.getFavoriteExercises();
exercises.value = res.data || [];
allExercises.value = [...(res.data || [])];
} catch (err) {
console.error('加载动作收藏失败', err);
}
};
// 加载收藏超级组列表
const loadSuperFavoriteList = async () => {
try {
const res = await SupersetsApi.getFavoriteSuperset();
superGroupInfo.value = res.data || [];
} catch (err) {
console.error('加载超级组收藏失败', err);
}
};
// 切换左侧分类
const switchNav = (id) => {
if (activeNav.value === id) return;
activeNav.value = id;
activeMotionPart.value = '';
selectedAction.value = null; // 切换分类时重置选中
if (id === 'super') {
exercises.value = [];
motionPart.value = [];
loadsupersetsinfo();
} else {
superGroupInfo.value = [];
loadExercises(id);
}
};
// 加载普通动作
const loadExercises = async (categoriesId) => {
try {
const partRes = await ExercisesApi.getMotionPart(categoriesId);
motionPart.value = partRes.data || [];
const exerciseRes = await ExercisesApi.getexercises({ categoriesId });
exercises.value = exerciseRes.data;
allExercises.value = [...(exerciseRes.data || [])];
} catch (error) {
console.error('加载动作失败:', error);
}
};
// 加载超级组
const loadsupersetsinfo = async () => {
try {
const response = await SupersetsApi.getsupersets();
superGroupInfo.value = response.data || [];
} catch (error) {
console.error('获取超级组失败:', error);
}
};
// 部位筛选
const handlePartClick = async (id) => {
activeMotionPart.value = id;
const exerciseRes = await ExercisesApi.getexercises({
categoriesId: activeNav.value,
subCategoriesId: id
});
exercises.value = exerciseRes.data || [];
selectedAction.value = null; // 切换部位时重置选中
};
// 搜索动作
const onSearch = (e) => {
const keyword = e.detail.value.trim().toLowerCase();
if (!keyword) {
exercises.value = [...allExercises.value];
return;
}
exercises.value = allExercises.value.filter(item =>
item.name?.toLowerCase().includes(keyword)
);
};
// 选择动作/超级组
const selectAction = (item, type = 1) => {
// 点击同一个动作时,支持取消选中(可选)
if (selectedAction.value?.id === item.id) {
selectedAction.value = null;
selectedType.value = 1;
return;
}
selectedAction.value = item;
selectedType.value = type;
console.log("✅ 选中动作:", item.name, "类型:", type, 'id', item.id);
console.log('打印输出selectedAction.value:', selectedAction.value)
};
// 关闭组件
const handleClose = () => {
setTimeout(() => {
emit('close');
}, 800);
}
// 确认替换,把选中的动作抛给父组件
const handleConfirm = async () => {
if (!selectedAction.value) return;
console.log("【完整选中动作】", selectedAction.value);
console.log("【当前store类型】", trainingStore.type);
// ==============================================
// 【1】如果是模板训练 → 先请求详情,再修改 units 数组
// ==============================================
if (trainingStore.type === 3) {
console.log("【模板模式】替换第", props.unitIndex, "个动作");
let newUnit = null;
if (selectedType.value === 1) {
// 替换成【单个动作】:先请求详情
const res = await ExercisesApi.getExerciseById(selectedAction.value.id);
const detail = res.data;
newUnit = {
unitType: 1,
unitId: detail.id,
unitName: detail.name,
exercises: [
{
exerciseId: detail.id,
exerciseName: detail.name,
exerciseType: detail.exerciseType || 1,
urlImage: detail.urlImage || detail.url3dAnimation,
categoryDescription: detail.categoryDescription,
equipmentDescription: detail.equipmentDescription,
}
]
};
} else if (selectedType.value === 2) {
// 替换成【超级组】:先请求详情
const res = await SupersetsApi.getSupersetsInfo(selectedAction.value.id);
const detail = res.data;
newUnit = {
unitType: 2,
supersetId: detail.id,
unitName: detail.name,
exercises: detail.exercises.map(e => ({
exerciseId: e.id,
exerciseName: e.name,
exerciseType: e.exerciseType || 1,
urlImage: e.urlImage || e.url3dAnimation,
}))
};
}
// ✅ 直接替换 Pinia 数组
trainingStore.actionDetail.units[props.unitIndex] = newUnit;
console.log("【替换后的新unit】", newUnit);
// ✅ 关闭弹窗
setTimeout(() => {
emit('close');
}, 800);
return;
}
// ==============================================
// 【2】原来逻辑:单个动作 / 超级组
// ==============================================
if (selectedType.value === 1 || 2) {
trainingStore.replaceAction(
selectedAction.value.id,
selectedType.value,
);
await trainingStore.loadTrainingDetail(selectedAction.value.id, selectedType.value);
}
emit('confirm', {
action: selectedAction.value,
type: selectedType.value
});
//每日模板专用 → 存入 Pinia
templateStore.setReplaceAction(
selectedAction.value.id,
selectedType.value
)
setTimeout(() => {
emit('close');
}, 800);
};
onMounted(() => {
loadCategories();
});
</script>
<style lang="scss" scoped>
.popup-container {
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 999;
border-radius: 0;
// .search-bar {
// display: flex;
// align-items: center;
// height: calc(var(--status-bar-height) + 88rpx);
// padding: 0 20rpx 0 20rpx;
// padding-top: var(--status-bar-height);
// background-color: #fff;
// position: sticky;
// top: 0;
// z-index: 10;
// .add-btn {
// width: 60rpx;
// height: 60rpx;
// display: flex;
// justify-content: center;
// align-items: center;
// margin-right: auto;
// }
// }
.search-bar {
padding-top: var(--status-bar-height);
height: 88rpx;
background: #fff;
}
.add-btn-wrapper {
position: fixed;
left: 28rpx;
top: calc(var(--status-bar-height) + 12rpx);
z-index: 9999;
width: 50rpx;
height: 50rpx;
}
.floating-menu {
position: absolute;
top: 90rpx;
left: 20rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
padding: 20rpx;
z-index: 9999;
width: 260rpx;
}
.menu-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.menu-text {
box-sizing: border-box;
display: flex;
font-size: 28rpx;
// margin-left: 200rpx;
}
.layout-container {
flex: 1;
display: flex;
background-color: #f5f5f5;
overflow: hidden;
.left-nav {
width: 150rpx;
background-color: #fafafa;
border-right: 1px solid #eee;
.nav-item {
padding: 28rpx 16rpx;
text-align: left;
font-size: 26rpx;
color: #666;
position: relative;
&.active {
color: #000;
background-color: #fff;
border-right: none;
font-weight: 500;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 36rpx;
background-color: #f8d714;
}
}
}
}
.right-content {
flex: 1;
padding: 20rpx;
.tip {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
margin-bottom: 20rpx;
.item {
padding: 10rpx 18rpx;
font-size: 24rpx;
background-color: #fff;
border-radius: 16rpx;
color: #666;
&.active {
background-color: #f8d714;
color: #000;
}
}
}
.exercise-grid {
.equipment-item {
margin-bottom: 30rpx;
.equipment-name {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.action-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15rpx;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
background: #fff;
border-radius: 16rpx;
padding: 15rpx;
position: relative;
&.selected {
border: 2rpx solid #f8d714;
}
.action-img {
width: 100%;
height: 220rpx;
border-radius: 12rpx;
}
.action-name {
font-size: 24rpx;
color: #333;
}
.trainingReps {
position: absolute;
top: 15rpx;
right: 15rpx;
font-size: 22rpx;
color: #999;
}
}
}
}
.supers-name {
font-size: 28rpx;
color: #333;
margin: 20rpx 0 16rpx;
}
.super {
display: flex;
align-items: center;
background: #fff;
padding: 20rpx;
border-radius: 16rpx;
gap: 20rpx;
margin-bottom: 15rpx;
&.selected {
border: 2rpx solid #f8d714;
}
.super-img {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
}
.right {
flex: 1;
.super-name {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
}
.parts {
font-size: 22rpx;
color: #999;
}
}
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding-top: 100rpx;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
.bottom-btn-bar {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx 30rpx;
background-color: #fff;
.btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
font-weight: bold;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
.confirm-btn {
background-color: #f8d714;
color: #000;
&.disabled {
background-color: #eeeeee;
color: #bbbbbb;
}
}
}
}
</style>
\ No newline at end of file
... ...
<template>
<!-- 超级组 子动作输入框 独立组件 -->
<view class="record-row super-record-row">
<view class="left super-left">
<view class="left-header super-left-header">
<view class="goal-list">
<view class="goal-item">
<view class="serial">
{{ subIndex + 1 }}{{ String.fromCharCode(65 + subIndex) }}</view>
<!-- ========================================== -->
<!-- 0:独立 重量+次数 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 0">
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.weight" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">kg</text>
</view>
</view>
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">次</text>
</view>
</view>
</template>
<!-- ========================================== -->
<!-- 2:独立 次数 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 2">
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">次</text>
</view>
</view>
</template>
<!-- ========================================== -->
<!-- 1:独立 时长 + 距离 【全部写在自己内部!】 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 1">
<!-- 时长:属于类型1 -->
<view class="input-col" @click.stop="onOpenTimePicker">
<view class="label">时长</view>
<view class="values">
<text class="num">{{ data.h }}</text>
<text class="unit">时</text>
<text class="num">{{ data.m }}</text>
<text class="unit">分</text>
<text class="num">{{ data.s }}</text>
<text class="unit">秒</text>
</view>
</view>
<!-- 距离:属于类型1 -->
<view class="distance">
<view class="label">距离</view>
<view class="values">
<up-input v-model="data.distance" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">km</text>
</view>
</view>
</template>
<!-- ========================================== -->
<!-- 3:独立 时长 【全部写在自己内部!】 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 3">
<!-- 时长:属于类型3 -->
<view class="input-col" @click.stop="onOpenTimePicker">
<view class="label">时长</view>
<view class="values">
<text class="num">{{ data.h }}</text>
<text class="unit">时</text>
<text class="num">{{ data.m }}</text>
<text class="unit">分</text>
<text class="num">{{ data.s }}</text>
<text class="unit">秒</text>
</view>
</view>
</template>
<!-- ========================================== -->
<!-- 4:独立 重量+次数 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 4">
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.weight" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">kg</text>
</view>
</view>
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">次</text>
</view>
</view>
</template>
<!-- ========================================== -->
<!-- 5:独立 重量+次数 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 5">
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.weight" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">kg</text>
</view>
</view>
<view class="goal-input-box">
<view class="values">
<up-input v-model="data.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">次</text>
</view>
</view>
</template>
<!-- ========================================== -->
<!-- 6:独立 间歇 -->
<!-- ========================================== -->
<template v-if="subExerciseType === 6">
<view class="goal-item interval">
<view class="goal-input-box reps">
<view class="label">循环</view>
<view class="values">
<up-input v-model="data.reps" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">组</text>
</view>
</view>
<view class="goal-input-box time">
<view class="label">单次</view>
<view class="values">
<up-input v-model="data.duration" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">秒</text>
</view>
</view>
<view class="goal-input-box restTime">
<view class="label">间歇</view>
<view class="values">
<up-input v-model="data.restTime" border="none" type="digit" color="#ffffff" :maxlength="3"
:customStyle="{ height: '40rpx', width: '60rpx', textAlign: 'center' }"></up-input>
<text class="unit">秒</text>
</view>
</view>
</view>
</template>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, onMounted } from 'vue';
const props = defineProps({
subExerciseType: { type: Number, required: true },
setIndex: { type: Number, required: true },
subIndex: { type: Number, required: true },
data: { type: Object, required: true },
userWeight: {
type: Number,
default: 70
}
})
const emit = defineEmits(['open-time-picker'])
const onOpenTimePicker = () => {
emit('open-time-picker')
}
onMounted(() => {
console.log('super-set-input 已挂载', {
subExerciseType: props.subExerciseType,
setIndex: props.setIndex,
subIndex: props.subIndex,
data: props.data
});
});
</script>
<style lang="scss" scoped>
$bg-black: #303030;
$card-bg: #1c1c1e;
$item-bg: #2c2c2e;
$text-main: #ffffff;
$text-gray: #8e8e93;
$theme-yellow: #f8d714;
.record-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 10rpx 30rpx;
box-sizing: border-box;
// background: #ecb4b4;
&.active {
background-color: #4c4d3b;
}
.left {
display: flex;
flex-direction: column;
gap: 10rpx;
.left-header {
display: flex;
align-items: center;
gap: 20rpx;
.serial {
display: flex !important;
width: 50rpx;
height: 70rpx;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #929292;
font-weight: bold;
}
.index-box {
width: 50rpx;
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: $text-main;
}
.input-col,
.distance {
height: 70rpx;
background: #1f1f1f;
border-radius: 10rpx;
padding: 5rpx 10rpx;
display: flex;
flex-direction: column;
justify-content: center;
.label {
font-size: 15rpx;
color: $text-gray;
margin-bottom: 2rpx;
}
.values {
display: flex;
align-items: baseline;
.num {
font-size: 32rpx;
color: $text-main;
}
.unit {
font-size: 15rpx;
color: $text-gray;
margin: 0 5rpx;
}
}
}
.distance {
min-width: 100rpx;
:deep(.u-input) {
padding: 0 !important;
}
.u-input__content__field {
font-size: 32rpx !important;
color: $text-main !important;
}
}
.timer-selector {
width: 75rpx;
height: 75rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1rpx solid #a4a4a4;
.text {
font-size: 20rpx;
color: $text-gray;
}
}
}
.exercise-name-list {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
}
.exercise-name {
font-size: 26rpx;
color: $text-main;
line-height: 1.2;
}
.goal-list {
width: 100%;
margin-top: 10rpx;
.goal-item {
display: flex;
align-items: center;
gap: 20rpx;
border-radius: 10rpx;
.serial {
width: 50rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #929292;
font-weight: bold;
}
.goal-input-box {
display: flex;
flex-direction: column;
justify-content: center;
background-color: #1f1f1f;
width: 100rpx;
height: 70rpx;
border-radius: 10rpx;
.values {
display: flex;
align-items: flex-end;
padding-left: 10rpx;
box-sizing: border-box;
:deep(.uni-input-input[type='number']) {
text-align: center;
}
.num {
font-size: 32rpx;
color: $text-main;
}
.unit {
font-size: 15rpx;
color: $text-gray;
margin: 0 5rpx;
}
}
}
}
.interval {
display: flex;
gap: 20rpx;
margin-top: 10rpx;
.goal-input-box {
flex: none;
width: 120rpx;
.label {
font-size: 15rpx;
color: $text-gray;
margin-bottom: 2rpx;
}
}
}
}
}
.icons {
display: flex;
gap: 20rpx;
.icon {
width: 70rpx;
height: 70rpx;
background-color: #1f1f1f;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: $theme-yellow;
}
}
.del {
background-color: #58372d;
}
}
}
</style>
\ No newline at end of file
... ...
<template>
<up-popup
:show="actionShow"
mode="bottom"
minHeight="90vh"
@close="actionShow = false"
bgColor="#1a1a1a"
>
<up-popup :show="actionShow" mode="bottom" minHeight="90vh" @close="actionShow = false" bgColor="#1a1a1a">
<scroll-view class="action-container" scroll-y>
<view class="header" v-if="type == 1">
<view class="explain">
<image
v-if="modeTab < 2"
:src="modeTab == 0 ? actionDetail.url3dAnimation : actionDetail.urlRealPerson"
class="media-content"
mode="aspectFill"
/>
<image v-if="modeTab < 2" :src="modeTab == 0 ? actionDetail.url3dAnimation : actionDetail.urlRealPerson"
class="media-content" mode="aspectFill" />
<view class="video" v-else @click="playVideo(actionDetail.urlTutorial)">
<video
:src="actionDetail.urlTutorial"
class="media-content"
:autoplay="false"
:show-center-play-btn="false"
:controls="false"
/>
<video :src="actionDetail.urlTutorial" class="media-content" :autoplay="false" :show-center-play-btn="false"
:controls="false" />
<view class="play-icon">
<up-icon name="play-right" size="28" class="icon" color="#fff"></up-icon>
</view>
... ... @@ -30,28 +15,16 @@
</view>
<view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial">
<view
class="tab-item"
v-if="actionDetail.url3dAnimation"
:class="{ active: modeTab == 0 }"
@click="switchModeTab(0)"
>
<view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab == 0 }"
@click="switchModeTab(0)">
3D
</view>
<view
class="tab-item"
v-if="actionDetail.urlRealPerson"
:class="{ active: modeTab == 1 }"
@click="switchModeTab(1)"
>
<view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab == 1 }"
@click="switchModeTab(1)">
真人
</view>
<view
class="tab-item"
v-if="actionDetail.urlTutorial"
:class="{ active: modeTab == 2 }"
@click="switchModeTab(2)"
>
<view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab == 2 }"
@click="switchModeTab(2)">
讲解
</view>
</view>
... ... @@ -66,12 +39,8 @@
<button class="share-btn" open-type="share">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</button>
<up-icon
:name="isFavorite ? 'star-fill' : 'star'"
:color="isFavorite ? '#fedc1f' : '#fff'"
size="24"
@click="toggleCollect"
></up-icon>
<up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24"
@click="toggleCollect"></up-icon>
<!-- <FavoriteBtn :id="actionId" :type="type" /> -->
</view>
</view>
... ... @@ -83,12 +52,7 @@
<view class="tab-item" :class="{ active: contentTab === 1 }" @click="contentTab = 1">
历史
</view>
<view
class="tab-item"
:class="{ active: contentTab === 2 }"
@click="contentTab = 2"
v-if="type === 1"
>
<view class="tab-item" :class="{ active: contentTab === 2 }" @click="contentTab = 2" v-if="type === 1">
平替动作
</view>
</view>
... ... @@ -102,44 +66,26 @@
<view class="video-grid">
<!-- <view class="video-card" v-for="i in 2" :key="i"></view> -->
<view class="video-card" @click="playVideo(actionDetail.urlTutorial)">
<video
:src="actionDetail.urlTutorial"
class="video"
:autoplay="false"
:show-center-play-btn="false"
:controls="false"
/>
<view class="play-overlay"
><up-icon name="play-circle-fill" color="#fff" size="30"></up-icon
></view>
<video :src="actionDetail.urlTutorial" class="video" :autoplay="false" :show-center-play-btn="false"
:controls="false" />
<view class="play-overlay"><up-icon name="play-circle-fill" color="#fff" size="30"></up-icon></view>
</view>
</view>
</view>
<view class="memo-box" @click="openBeizhu">
<view class="section-title">训练备注</view>
<up-textarea
class="textarea"
v-model="actionDetail.userNote"
placeholder="点击填写备注"
autoHeight
customStyle="background: transparent; border: none; padding: 10rpx 0;"
placeholderStyle="color: #666"
disabled
border="none"
></up-textarea>
<up-textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注" autoHeight
customStyle="background: transparent; border: none; padding: 10rpx 0;" placeholderStyle="color: #666"
disabled border="none"></up-textarea>
</view>
<!-- 动作列表,只有超级组才有 -->
<view class="section" v-if="type === 2">
<view class="section-title">动作列表</view>
<view class="action-list">
<!-- 动作循环列表 -->
<view
class="action-item"
v-for="item in actionDetail?.exercises"
:key="item.id"
@click="openActionItem(item)"
>
<view class="action-item" v-for="item in actionDetail?.exercises" :key="item.id"
@click="openActionItem(item)">
<image :src="item.url3dAnimation || lostImage" mode="aspectFill" class="img" />
<view class="middle">
<view class="name">{{ item.name }}</view>
... ... @@ -191,7 +137,7 @@
</view> -->
<view class="history-list">
<template v-if="historyList.length > 0">
<template v-if="historyList && historyList.length > 0">
<view class="history-item" v-for="item in historyList" :key="item.id">
<view class="item-header">
<text class="date">{{ formatDate(item.date) }}</text>
... ... @@ -225,12 +171,7 @@
<!-- 3 平替动作(只有动作组才有) -->
<view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up">
<view class="substitute-list">
<view
class="sub-item"
v-for="item in alternativeActions"
:key="item.id"
@click="openActionItem(item)"
>
<view class="sub-item" v-for="item in alternativeActions" :key="item.id" @click="openActionItem(item)">
<image :src="item.urlImage || lostImage" mode="aspectFill" class="img" />
<view class="sub-info">
<text class="name">{{ item.name }}</text>
... ... @@ -253,56 +194,56 @@
</template>
<script setup>
import { onMounted, ref, nextTick } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import beizhu from '@/pages/xunji/components/beizhu.vue';
import SupersetsApi from '@/sheep/api/motion/supersets';
import TrainingApi from '@/sheep/api/Training/traininghistory';
import { onShareAppMessage } from '@dcloudio/uni-app';
// 静态配置
const alternativeActions = ref([]); // 平替动作列表接口数据
// 响应式状态
const actionShow = ref(false);
const modeTab = ref(0);
const contentTab = ref(0);
const isFavorite = ref(false);
// 记录当前动作的详细数据
const actionDetail = ref({});
// 记录当前动作的id
const actionId = ref(0);
// 记录是超级组还是动作组 1=动作组,2=超级组
const type = ref(0);
const showBeizhuRef = ref(null);
const historyList = ref([]); // 训练历史列表接口数据
const lostImage =
import { onMounted, ref, nextTick } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import beizhu from '@/pages/xunji/components/beizhu.vue';
import SupersetsApi from '@/sheep/api/motion/supersets';
import TrainingApi from '@/sheep/api/Training/traininghistory';
import { onShareAppMessage } from '@dcloudio/uni-app';
// 静态配置
const alternativeActions = ref([]); // 平替动作列表接口数据
// 响应式状态
const actionShow = ref(false);
const modeTab = ref(0);
const contentTab = ref(0);
const isFavorite = ref(false);
// 记录当前动作的详细数据
const actionDetail = ref({});
// 记录当前动作的id
const actionId = ref(0);
// 记录是超级组还是动作组 1=动作组,2=超级组
const type = ref(0);
const showBeizhuRef = ref(null);
const historyList = ref([]); // 训练历史列表接口数据
const lostImage =
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
// 切换图表模式
const switchModeTab = (index) => {
// 切换图表模式
const switchModeTab = (index) => {
modeTab.value = index;
};
};
// 跳转到视频播放的页面
const playVideo = (url) => {
// 跳转到视频播放的页面
const playVideo = (url) => {
uni.navigateTo({
url: '/pages4/pages/xunji/xunji-shiping?url=' + url,
});
};
// 获取动作收藏状态
const checkExerciseFavorited = async () => {
};
// 获取动作收藏状态
const checkExerciseFavorited = async () => {
try {
const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
isFavorite.value = res.data;
} catch (err) {
console.log(err);
}
};
// 收藏
const toggleCollect = async () => {
};
// 收藏
const toggleCollect = async () => {
try {
const status = isFavorite.value ? 0 : 1;
if (type == 1) {
... ... @@ -315,25 +256,25 @@
} catch (err) {
console.log(err);
}
};
};
// 获取超级组收藏状态
const checkSupersetFavorited = async () => {
// 获取超级组收藏状态
const checkSupersetFavorited = async () => {
try {
const res = await SupersetsApi.checkSupersetFavorited(actionId.value);
isFavorite.value = res.data;
} catch (err) {
console.log(err);
}
};
};
const startTraining = () => {
const startTraining = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}`,
});
};
};
const open = (id, typeData) => {
const open = (id, typeData) => {
actionId.value = Number(id);
type.value = typeData;
... ... @@ -348,20 +289,20 @@
checkSupersetFavorited();
}
loadTrainHistoryDetail(actionId.value);
loadTrainHistoryDetail(actionId.value, type.value);
actionShow.value = true;
};
// 打开备注弹窗
const openBeizhu = () => {
};
// 打开备注弹窗
const openBeizhu = () => {
nextTick(() => {
if (showBeizhuRef.value) {
showBeizhuRef.value.open(actionId.value);
}
});
};
};
// 接收子组件传过来的备注内容
const handleNoteSave = async (content) => {
// 接收子组件传过来的备注内容
const handleNoteSave = async (content) => {
try {
if (type.value == 2) {
await SupersetsApi.addNotes({
... ... @@ -378,17 +319,17 @@
} catch (e) {
console.log(e);
}
};
};
// 启用分享菜单
// #ifdef MP-WEIXIN
wx.showShareMenu({
// 启用分享菜单
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
});
// #endif
// 定义分享内容
onShareAppMessage((res) => {
});
// #endif
// 定义分享内容
onShareAppMessage((res) => {
// res.from 可区分触发来源:'button'(按钮触发)或 'menu'(右上角菜单触发)[reference:3]
console.log('分享触发来源:', res.from);
return {
... ... @@ -397,10 +338,10 @@
path: `/pages/xunji/xunji?currentTab=${3}`,
imageUrl: actionDetail.value.urlImage || lostImage, // 分享图片
};
});
});
// 加载单个动作详情
const loadexercisedetail = async (id) => {
// 加载单个动作详情
const loadexercisedetail = async (id) => {
const response = await ExercisesApi.getExerciseById(id);
actionDetail.value = response.data;
if (actionDetail.value.url3dAnimation) {
... ... @@ -410,16 +351,16 @@
} else {
modeTab.value = 2;
}
};
};
// 加载超级组详情
const loadsuperdetail = async (id) => {
// 加载超级组详情
const loadsuperdetail = async (id) => {
const response = await SupersetsApi.getSupersetsInfo(id);
actionDetail.value = response.data;
console.log('显示超级组详情:', actionDetail.value);
};
// 2. 加载平替动作的函数
const loadAlternativeActions = async (id) => {
};
// 2. 加载平替动作的函数
const loadAlternativeActions = async (id) => {
if (!id || isNaN(Number(id))) {
console.warn('平替动作id非法,跳过请求:', id);
alternativeActions.value = [];
... ... @@ -436,30 +377,30 @@
console.error('加载平替动作失败:', error);
alternativeActions.value = [];
}
};
};
// 加载训练历史
const loadTrainHistoryDetail = async (id) => {
// 加载训练历史
const loadTrainHistoryDetail = async (id, type) => {
try {
const res = await TrainingApi.getTrainHistoryList(id);
const res = await TrainingApi.getTrainHistoryList(id, type);
historyList.value = res.data;
console.log('训练历史列表接口返回结果historyList.value', historyList.value);
} catch (error) {
console.error('加载训练历史失败:', error);
}
};
// 格式化时间
const formatDate = (dateArr) => {
};
// 格式化时间
const formatDate = (dateArr) => {
if (!Array.isArray(dateArr) || dateArr.length < 3) return '';
const [year, month, day] = dateArr;
const date = new Date(year, month - 1, day);
const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const week = weekArr[date.getDay()];
return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')} ${week}`;
};
};
// 格式化每组数据:自动拼接 weight/reps/duration/distance
const formatSetData = (set) => {
// 格式化每组数据:自动拼接 weight/reps/duration/distance
const formatSetData = (set) => {
const parts = [];
// 重量
... ... @@ -483,10 +424,10 @@
}
return parts.length > 0 ? parts.join(' × ') : '无数据';
};
};
// 新增:秒数转 00:00:00 格式
const formatTime = (seconds) => {
// 新增:秒数转 00:00:00 格式
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
... ... @@ -496,21 +437,21 @@
const ss = String(s).padStart(2, '0');
return hh + mm + ss;
};
};
// 点击超级组内部的动作 → 打开动作详情(复用同一个组件)
const openActionItem = (item) => {
// 点击超级组内部的动作 → 打开动作详情(复用同一个组件)
const openActionItem = (item) => {
open(item.id, 1);
};
};
defineExpose({ open });
defineExpose({ open });
onMounted(() => {});
onMounted(() => { });
</script>
<style lang="scss" scoped>
.action-container {
.action-container {
width: 100%;
height: 80vh;
background-color: #1a1a1a;
... ... @@ -529,6 +470,7 @@
width: 100%;
height: 100%;
}
.video {
width: 100%;
height: 100%;
... ... @@ -536,6 +478,7 @@
justify-content: center;
align-items: center;
position: relative;
.play-icon {
width: 50px;
height: 50px;
... ... @@ -560,6 +503,7 @@
border-radius: 12rpx;
backdrop-filter: blur(10px);
z-index: 99;
.tab-item {
padding: 8rpx 24rpx;
font-size: 24rpx;
... ... @@ -925,6 +869,7 @@
height: 120rpx;
background-color: #242424;
z-index: 999;
.btn {
width: 80%;
height: 80rpx;
... ... @@ -936,14 +881,14 @@
border-radius: 50rpx;
}
}
}
}
// 动画
.slide-up {
// 动画
.slide-up {
animation: slideUp 0.4s ease-out;
}
}
@keyframes slideUp {
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20rpx);
... ... @@ -953,5 +898,5 @@
opacity: 1;
transform: translateY(0);
}
}
}
</style>
... ...