Authored by Bad

refactor(dongzuo): 重构动作组件并优化休息计时逻辑

Too many changes to show.

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

... ... @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
# SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
# SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
# SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
... ...
... ... @@ -38,6 +38,7 @@
</template>
<script setup>
import dayjs from 'dayjs';
import { ref, computed } from 'vue'
import TrainingApi from '@/sheep/api/Training/traininghistory'
... ... @@ -127,12 +128,7 @@ const formattedSetList = computed(() => {
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')}`)
parts.push(dayjs.duration(set.duration, 'seconds').format('H:mm:ss'));
}
if (set.distance != null && set.distance !== '') {
parts.push(`${set.distance}m`)
... ...
<template>
<view class="action-container" @click="closeDifficulty">
<view class="action-list" v-if="deleteActionShow === false">
<view class="card">
<view class="card" :class="{ 'unit-active': isUnitActive }">
<view class="card-top" @click.stop="isExpanded = !isExpanded">
<view v-if="type === 1">
<image class="action-image" :src="actionDetail?.urlImage || lostImage" mode="aspectFill"></image>
... ... @@ -12,11 +12,9 @@
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>
... ... @@ -34,16 +32,12 @@
</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>
<text class="weight-text">{{ displayWeight }}</text>
<up-icon name="edit-pen" color="#8e8e93" size="20"></up-icon>
</view>
</view>
... ... @@ -54,38 +48,27 @@
该动作为自重减重,根据你的体重减去辅助器械重量计算容量,下方的填写重量为辅助器械重量。
</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>
<text class="weight-text">{{ 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>
... ... @@ -94,48 +77,39 @@
{{ 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)"
<view class="check-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">
<view class="action-icons" v-if="isEditing">
<view class="action-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)">
<view class="action-icon 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>
... ... @@ -154,7 +128,6 @@
</view>
</view>
</view>
<view class="btn-item" @click.stop="isEditing = !isEditing">
{{ isEditing ? '完成' : '编辑' }}
</view>
... ... @@ -168,760 +141,639 @@
<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 { ref, reactive, watch, computed, nextTick, onMounted, getCurrentInstance } from 'vue';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { useTrainingStore } from '@/sheep/store/trainingStore'
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'
import SupersetsApi from '@/sheep/api/motion/supersets';
import ExercisesApi from '@/sheep/api/motion/exercises';
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)
// 接收父组件的数据
// ===================== Props =====================
const props = defineProps({
id: [Number, String],
type: [Number, String],
index: Number, // 新增
index: Number,
unitIndex: Number,
actionDetail: {
type: Object,
default: () => ({})
},
isDailyTemplates: {
type: Boolean,
default: false
}
actionDetail: { type: Object, default: () => ({}) },
isDailyTemplates: { type: Boolean, default: false },
isUnitActive: { 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);
// ===================== Emits =====================
const emit = defineEmits(['register', 'update:groupCount', 'replace-action', 'deleteAction', 'replaceActionForSave', 'startRestTimer', 'cancelRestTimer', 'updateUnitActive'])
}, { immediate: true })
// ===================== 常量 =====================
dayjs.extend(duration)
const lostImage = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
emit('register', props.index, getCurrentInstance()?.proxy)
// ===================== Store =====================
const trainingStore = useTrainingStore()
// 体重选择器
const displayWeight = computed(() => {
return `${userWeight.value}kg`
})
// ===================== 基础状态 =====================
const deleteActionShow = ref(false)
const showTips = ref(false)
const isExpanded = ref(false)
const isEditing = ref(false)
const replacePopupShow = ref(false)
const historyPopupRef = ref(null)
const showBeizhuRef = ref(null)
// ===================== Picker 状态 =====================
const showPicker = ref(false)
const showQuickPicker = ref(false)
const activeIndex = ref(0)
const activeActionId = ref('')
const restEditMode = ref('single')
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') + '秒'),
])
// 2. 体重选择器状态
// ===================== 体重 =====================
const userWeight = ref(70)
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)'] // 第三列固定文字
['千克(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) // 取小数第一位
const defaultWeightIndex = ref([60, 0, 0])
// 计算索引(整数部分:intPart - 10,小数部分:decPart)
const intIndex = Math.max(0, Math.min(intPart - 10, 240))
defaultWeightIndex.value = [intIndex, decPart, 0]
showWeightPicker.value = true
}
// ===================== 难度 =====================
const showDifficulty = ref(false)
const initialDifficulty = { label: '难度', color: '#ffffff' }
const currentDifficulty = ref({ ...initialDifficulty })
// 4. 体重选择器确认事件
const onWeightConfirm = (e) => {
const intPart = e.value[0] // 整数部分
const decPart = e.value[1] // 小数部分
// 拼接成完整体重
userWeight.value = parseFloat(`${intPart}.${decPart}`)
showWeightPicker.value = false
}
// ===================== 训练数据 =====================
const recordList = ref([{
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
}])
// 打开备注弹窗
const openBeizhu = () => {
nextTick(() => {
if (showBeizhuRef.value) {
showBeizhuRef.value.open(props.actionDetail.id);
}
});
};
const superRecordMap = ref({})
// 接收子组件传过来的备注内容
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);
}
};
// 向父组件注册组件实例引用
emit('register', props.index, getCurrentInstance()?.proxy)
const toggleTips = () => {
showTips.value = !showTips.value
}
// ===================== Computed =====================
const displayWeight = computed(() => `${userWeight.value}kg`)
// 超级组是否包含间歇类型动作(用来控制组头休息时间选择器的显示)
const hasIntervalExercise = computed(() => {
const hasWeightExercise = computed(() => {
if (props.type !== 2) return false
const exercises = props.actionDetail?.exercises || []
// 只要有一个子动作是间歇类型(6),就不显示组头的休息时间
return exercises.some(sub => sub.exerciseType === 6)
return (props.actionDetail?.exercises || []).some(sub => [4, 5].includes(sub.exerciseType))
})
// 替换动作
const handleReplaceAction = () => {
showTips.value = false
replacePopupShow.value = true // 打开弹窗
console.log("✅ 弹窗打开:", replacePopupShow.value)
}
// 超级组是否包含 4(自重加重) / 5(自重减重)
const hasWeightExercise = computed(() => {
if (props.type !== 2) return false
const superTotalSets = computed(() => {
if (props.type !== 2) return 0
const exercises = props.actionDetail?.exercises || []
return exercises.some(sub => [4, 5].includes(sub.exerciseType))
if (exercises.length === 0) return 0
return superRecordMap.value[exercises[0].id]?.length || 0
})
const exerciseNamesWithLabel = computed(() => {
return (props.actionDetail?.exercises || []).map((item, index) => {
const label = String.fromCharCode(65 + index)
return `${label} ${item?.name || '未知动作'}`
})
})
// 删除动作
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
const exposeRecordList = computed(() => {
return recordList.value.filter(item => item.isActive).map(item => ({
...item,
weight: getRealWeight(item)
}))
})
// 1. 如果是【模板训练】,删除对应项
if (trainingStore.type === 3) {
console.log('模板训练:删除第', props.unitIndex, '个单元')
trainingStore.actionDetail.units.splice(props.unitIndex, 1)
const exposeSuperRecordMap = computed(() => {
const map = {}
for (const key in superRecordMap.value) {
const subExercise = props.actionDetail?.exercises?.find(s => s.id == key)
map[key] = superRecordMap.value[key]
.filter(item => item.isActive)
.map(item => ({ ...item, weight: getRealWeight(item, subExercise?.exerciseType) }))
}
else {
console.log('单个动作/超级组:清空')
trainingStore.clearTrainingStore()
return map
})
const totalSetCount = computed(() => {
if (props.type === 1) return recordList.value.length
if (props.type === 2) {
const exercises = props.actionDetail?.exercises || []
if (exercises.length === 0) return 0
return superRecordMap.value[exercises[0].id]?.length || 0
}
// 关闭菜单
showTips.value = false
console.log('删除完成 ✅')
return 0
})
const checkedWeight = computed(() => {
let sum = 0
if (props.type === 1 && recordList.value) {
recordList.value.forEach(item => {
if (item.isActive) sum += getRealWeight(item) * (Number(item.reps) || 0)
})
}
if (props.type === 2 && superRecordMap.value) {
for (const actionId in superRecordMap.value) {
const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)
superRecordMap.value[actionId].forEach(item => {
if (item.isActive) sum += getRealWeight(item, subExercise?.exerciseType) * (Number(item.reps) || 0)
})
}
}
return sum
})
const totalWeight = computed(() => {
let sum = 0
if (props.type === 1 && recordList.value) {
recordList.value.forEach(item => {
sum += getRealWeight(item) * (Number(item.reps) || 0)
})
}
if (props.type === 2 && superRecordMap.value) {
for (const actionId in superRecordMap.value) {
const subExercise = props.actionDetail?.exercises?.find(s => s.id == actionId)
superRecordMap.value[actionId].forEach(item => {
sum += getRealWeight(item, subExercise?.exerciseType) * (Number(item.reps) || 0)
})
}
}
return sum
})
// ===================== 工具函数 =====================
function getRealWeight(item, subExerciseType = null) {
const userW = Number(userWeight.value) || 0
const inputW = Number(item.weight) || 0
const type = subExerciseType ?? props.actionDetail?.exerciseType
if (type === 0) return inputW
if (type === 4) return userW + inputW
if (type === 5) return Math.max(0, userW - inputW)
return 0
}
// 难度控制逻辑
const showDifficulty = ref(false);
const initialDifficulty = { label: '难度', color: '#ffffff' };
const currentDifficulty = ref({ ...initialDifficulty });
function superGroupSetData(setIndex) {
const firstSub = props.actionDetail?.exercises?.[0]
if (!firstSub) return {}
return superRecordMap.value[firstSub.id]?.[setIndex] || {}
}
const selectDifficulty = (label, color) => {
if (currentDifficulty.value.label === label) {
currentDifficulty.value = { ...initialDifficulty };
} else {
currentDifficulty.value = { label, color };
function getSubActionData(setIndex, actionId) {
return superRecordMap.value[actionId]?.[setIndex] || {
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
}
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
})
function formatQuickTime(totalSeconds) {
const dur = dayjs.duration(totalSeconds * 1000)
const minutes = Math.floor(dur.asMinutes())
const seconds = dur.seconds()
if (minutes > 0) return `${minutes}分${seconds > 0 ? seconds + '秒' : ''}`
return `${seconds}s`
}
onMounted(() => {
// 从 Pinia 加载当前 unit 的训练数据
const savedData = trainingStore.getUnitRecord(props.unitIndex);
// 恢复体重
userWeight.value = savedData.userWeight || 70;
console.log('从pinia加载的数据:', savedData);
function parseQuickTime(display) {
if (!display) return 60
// 匹配 "X分Y秒" / "X分" / "Ys"
const full = display.match(/^(\d+)分(\d+)秒$/)
if (full) return parseInt(full[1]) * 60 + parseInt(full[2])
const minOnly = display.match(/^(\d+)分$/)
if (minOnly) return parseInt(minOnly[1]) * 60
const secOnly = display.match(/^(\d+)s$/)
if (secOnly) return parseInt(secOnly[1])
return 60
}
// 1. 先从 Pinia 恢复已有数据
function saveToStore() {
let records = {}
if (props.type === 1) {
// 关键:只有当前是初始默认1条,才赋值,不再覆盖watch结果
if (savedData.records?.[props.actionDetail.id]) {
recordList.value = [...savedData.records[props.actionDetail.id]]
console.log('onMounted兜底赋值', recordList.value)
}
records[props.actionDetail.id] = recordList.value
} else if (props.type === 2) {
if (savedData.records && Object.keys(savedData.records).length > 0) {
superRecordMap.value = { ...savedData.records };
records = superRecordMap.value
}
trainingStore.saveUnitRecord(props.unitIndex, { records, userWeight: userWeight.value })
}
// ===================== UI 交互 =====================
function toggleTips() {
showTips.value = !showTips.value
}
function closeDifficulty() {
showDifficulty.value = false
showTips.value = false
}
function selectDifficulty(label, color) {
if (currentDifficulty.value.label === label) {
currentDifficulty.value = { ...initialDifficulty }
} else {
currentDifficulty.value = { label, color }
}
// 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;
showDifficulty.value = false
}
function toggleActive(index) {
const wasActive = recordList.value[index].isActive
recordList.value[index].isActive = !recordList.value[index].isActive
const hasAnyActive = recordList.value.some(item => item.isActive)
emit('updateUnitActive', hasAnyActive)
if (recordList.value[index].isActive) {
const seconds = parseQuickTime(recordList.value[index].quickTimeDisplay)
emit('startRestTimer', seconds, props.unitIndex)
} else if (wasActive) {
emit('cancelRestTimer', props.unitIndex)
}
}
function toggleSuperSetActive(setIndex) {
const wasActive = superGroupSetData(setIndex).isActive
props.actionDetail.exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list?.[setIndex]) list[setIndex].isActive = !list[setIndex].isActive
})
const hasAnyActive = props.actionDetail.exercises.some(sub =>
superRecordMap.value[sub.id]?.some(item => item.isActive)
)
emit('updateUnitActive', hasAnyActive)
if (!wasActive) {
const restSeconds = parseQuickTime(superGroupSetData(setIndex).quickTimeDisplay)
emit('startRestTimer', restSeconds, props.unitIndex)
} else {
emit('cancelRestTimer', props.unitIndex)
}
}
// 供父组件调用:取消该组件内所有勾选
function resetActiveSets() {
recordList.value.forEach(item => { item.isActive = false })
for (const key in superRecordMap.value) {
superRecordMap.value[key]?.forEach(item => { item.isActive = false })
}
emit('updateUnitActive', false)
}
// ===================== 体重选择器 =====================
function openWeightPicker() {
const num = userWeight.value
const intIndex = Math.max(0, Math.min(Math.floor(num) - 10, 240))
const decPart = Math.round((num - Math.floor(num)) * 10)
defaultWeightIndex.value = [intIndex, decPart, 0]
showWeightPicker.value = true
}
function onWeightConfirm(e) {
userWeight.value = parseFloat(`${e.value[0]}.${e.value[1]}`)
showWeightPicker.value = false
}
// ===================== 备注 =====================
function openBeizhu() {
nextTick(() => {
emit('register', props.index, {
recordList: recordList.value,
superRecordMap: superRecordMap.value
})
if (showBeizhuRef.value) showBeizhuRef.value.open(props.actionDetail.id)
})
});
// 动作组数据记录
const recordList = ref([
{ h: '00', m: '00', s: '00', quickTimeDisplay: '60s', distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false },
]);
const saveToStore = () => {
let records = {};
if (props.type === 1) {
// 普通动作
records[props.actionDetail.id] = recordList.value;
} else if (props.type === 2) {
// 超级组
records = superRecordMap.value;
}
async function handleNoteSave(content) {
try {
if (props.type == 2) {
await SupersetsApi.addNotes({ supersetsId: props.id, content })
} else {
await ExercisesApi.addNotes({ exerciseId: props.id, content })
}
// 调用 Pinia 保存
trainingStore.saveUnitRecord(props.unitIndex, {
records: records,
userWeight: userWeight.value
});
};
props.actionDetail.userNote = content
} catch (e) {
// 静默处理
}
}
watch(recordList, () => {
// console.log('recordList变化,发送长度:', recordList.value.length)
emit('update:groupCount', recordList.value.length)
}, { deep: true, immediate: true })
// ===================== 替换/删除动作 =====================
function handleReplaceAction() {
showTips.value = false
replacePopupShow.value = true
}
watch([recordList, superRecordMap, userWeight], () => {
saveToStore();
console.log('+++++++++打印全局训练数据 trainingStore.unitRecords++++++++', trainingStore.unitRecords);
}, { deep: true, immediate: false });
function deleteAction() {
if (props.isDailyTemplates) {
showTips.value = false
emit('deleteAction')
return
}
trainingStore.deleteUnitRecord(props.unitIndex)
deleteActionShow.value = true
if (trainingStore.type === 3) {
trainingStore.actionDetail.units.splice(props.unitIndex, 1)
} else {
trainingStore.clearTrainingStore()
}
showTips.value = false
}
const toggleActive = (index) => {
recordList.value[index].isActive = !recordList.value[index].isActive;
};
// ===================== 时间选择器 =====================
function openPicker(index) {
activeIndex.value = index
const item = recordList.value[index]
defaultTimeIndex.value = [
timeColumns[0].indexOf(item.h + '时'),
timeColumns[1].indexOf(item.m + '分'),
timeColumns[2].indexOf(item.s + '秒')
]
showPicker.value = true
}
// 超级组工具方法
const superGroupSetData = (setIndex) => {
const firstSub = props.actionDetail?.exercises?.[0]
if (!firstSub) return {}
return superRecordMap.value[firstSub.id]?.[setIndex] || {}
function openQuickTimer(index) {
activeIndex.value = index
showQuickPicker.value = true
}
const getSubActionData = (setIndex, actionId) => {
return superRecordMap.value[actionId]?.[setIndex] || {
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '',
weight: '',
reps: '',
duration: '',
restTime: '',
isActive: false
}
function openAllQuickTimer() {
restEditMode.value = 'all'
showQuickPicker.value = true
}
const openSuperPicker = (setIndex, actionId) => {
function openSuperPicker(setIndex, actionId) {
activeIndex.value = setIndex
activeActionId.value = actionId; // 保存当前动作ID
activeActionId.value = actionId
showPicker.value = true
}
const openSuperQuickTimer = (setIndex) => {
function 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
}
})
}
// 合并后的 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, ''))
function 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]
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];
const list = superRecordMap.value[activeActionId.value]
if (list?.[setIdx]) {
list[setIdx].h = rawValues[0];
list[setIdx].m = rawValues[1];
list[setIdx].s = rawValues[2];
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
function onQuickConfirm(e) {
const totalSeconds = dayjs.duration({
minutes: parseInt(e.value[0]) || 0,
seconds: parseInt(e.value[1]) || 0
}).asSeconds()
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) {
recordList.value.forEach(item => { item.quickTimeDisplay = formatQuickTime(totalSeconds) })
} 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'
})
if (firstSub) {
superRecordMap.value[firstSub.id]?.forEach(set => { set.quickTimeDisplay = formatQuickTime(totalSeconds) })
}
}
// ============== 单行修改 ==============
else {
} else {
if (props.type === 1) {
recordList.value[setIdx].quickTimeDisplay = totalSeconds + 's'
recordList.value[setIdx].quickTimeDisplay = formatQuickTime(totalSeconds)
} else if (props.type === 2) {
props.actionDetail.exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list?.[setIdx]) {
list[setIdx].quickTimeDisplay = totalSeconds + 's'
}
if (list?.[setIdx]) list[setIdx].quickTimeDisplay = formatQuickTime(totalSeconds)
})
}
}
restEditMode.value = 'single'
showQuickPicker.value = false
}
// 单动作的增加/删除
const addRow = () => {
// ===================== 增删行/组 =====================
function addRow() {
recordList.value.push({
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '', weight: '', reps: '', duration: '',
restTime: '', isActive: false,
});
};
// 删除单动作行数,至少保留一行,提示。
const deleteRow = (index) => {
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
})
}
function deleteRow(index) {
if (recordList.value.length > 1) {
recordList.value.splice(index, 1);
recordList.value.splice(index, 1)
} else {
uni.showToast({
title: '至少保留一行数据',
icon: 'none'
})
uni.showToast({ title: '至少保留一行数据', icon: 'none' })
}
};
}
// 增加超级组的set组数
const addSuperSet = () => {
const exercises = props.actionDetail?.exercises || []
exercises.forEach(sub => {
function addSuperSet() {
(props.actionDetail?.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
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
})
}
})
}
// 删除超级组的组数
const deleteSuperSet = (setIndex) => {
const exercises = props.actionDetail?.exercises || []
// 没有子动作直接返回
function deleteSuperSet(setIndex) {
const exercises = props.actionDetail?.exercises || []
if (!exercises.length) return
// 取第一个子动作的组数,所有子动作数组长度保持同步
const firstSubId = exercises[0].id
const list = superRecordMap.value[firstSubId] || []
// 判断只剩一组,弹窗提示并终止删除
const list = superRecordMap.value[exercises[0].id] || []
if (list.length <= 1) {
uni.showToast({
title: '至少保留一组数据',
icon: 'none'
})
uni.showToast({ title: '至少保留一组数据', icon: 'none' })
return
}
exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list && list.length > 1) {
list.splice(setIndex, 1)
}
if (list && list.length > 1) list.splice(setIndex, 1)
})
}
const addGroup = () => {
if (props.type === 1) {
addRow()
} else if (props.type === 2) {
addSuperSet()
}
function 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);
// ===================== 历史 =====================
function openHistory(id) {
if (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 || '未知动作'}`
})
})
// ===================== Watchers =====================
watch(recordList, () => {
emit('update:groupCount', recordList.value.length)
}, { deep: true, immediate: true })
// 获得真正的重量(支持普通动作 + 超级组)
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;
}
watch([recordList, superRecordMap, userWeight], saveToStore, { deep: true })
// 只暴露 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) // ✅ 传入子动作类型
watch(
() => [recordList.value, superRecordMap.value],
() => {
const userW = userWeight.value || 0
if (props.type === 1 && props.actionDetail?.exerciseType === 5) {
recordList.value.forEach(item => {
if ((Number(item.weight) || 0) > userW) {
item.weight = ''
uni.showToast({ title: '辅助重量不能超过体重', icon: 'none', duration: 2000 })
}
})
}
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
props.actionDetail?.exercises?.forEach(sub => {
if (sub.exerciseType === 5) {
(superRecordMap.value[sub.id] || []).forEach(item => {
if ((Number(item.weight) || 0) > userW) {
item.weight = ''
uni.showToast({ title: '辅助重量不能超过体重', icon: 'none', duration: 2000 })
}
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
},
{ deep: true }
)
superRecordMap.value[actionId].forEach(item => {
if (item.isActive) {
const realWeight = getRealWeight(item, subType)
const reps = Number(item.reps) || 0
sum += realWeight * reps
// ===================== Lifecycle =====================
onMounted(() => {
const savedData = trainingStore.getUnitRecord(props.unitIndex)
userWeight.value = savedData.userWeight || 70
if (props.type === 1) {
if (savedData.records?.[props.actionDetail.id]) {
recordList.value = [...savedData.records[props.actionDetail.id]]
}
})
} else if (props.type === 2) {
if (savedData.records && Object.keys(savedData.records).length > 0) {
superRecordMap.value = { ...savedData.records }
}
}
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 && props.actionDetail?.exercises?.length) {
const exercises = props.actionDetail.exercises
if (!superRecordMap.value[exercises[0].id]) {
const map = {}
exercises.forEach(item => {
map[item.id] = [{
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
}]
})
superRecordMap.value = map
}
}
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
nextTick(() => {
emit('register', props.index, {
recordList: recordList.value,
superRecordMap: superRecordMap.value
})
})
}
}
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 }
);
// ===================== Expose =====================
defineExpose({
recordList: exposeRecordList, // 过滤后
superRecordMap: exposeSuperRecordMap, // 过滤后
totalSetCount, //全部的组数
checkedWeight, // 已勾选重量
totalWeight // 全部重量
recordList: exposeRecordList,
superRecordMap: exposeSuperRecordMap,
totalSetCount,
checkedWeight,
totalWeight,
resetActiveSets
})
</script>
// 打开备注弹窗
// const openBeizhu = () => {
// console.log('传递到备注组件的id', actionId.value)
// showBeizhuRef.value.open(actionId.value)
// }
<style lang="scss" scoped>
// ===================== 变量 =====================
$bg-black: #303030;
$card-bg: #1c1c1e;
$item-bg: #2c2c2e;
$input-bg: #1f1f1f;
$popup-bg: #3e3e3e;
$text-main: #ffffff;
$text-gray: #8e8e93;
$text-dim: #929292;
$theme-yellow: #f8d714;
watch(() => props.actionDetail, () => {
console.log('actionDetail.exercises:', props.actionDetail?.exercises)
}, { immediate: true })
// ===================== 公共:序号框 =====================
@mixin index-box {
width: 50rpx;
height: 70rpx;
background: $input-bg;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: $text-main;
}
</script>
// ===================== 公共:计时选择器 =====================
@mixin 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;
}
}
// ===================== 公共:操作图标按钮 =====================
@mixin action-icon-base {
width: 70rpx;
height: 70rpx;
background-color: $input-bg;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
<style lang="scss" scoped>
$bg-black: #303030;
$card-bg: #1c1c1e;
$item-bg: #2c2c2e;
$text-main: #ffffff;
$text-gray: #8e8e93;
$theme-yellow: #f8d714;
&.active {
background-color: $theme-yellow;
}
}
// ===================== 容器 =====================
.action-container {
width: 100%;
/* 确保容器足够高以接收点击事件 */
background-color: $bg-black;
.action-list {
width: 100%;
... ... @@ -930,6 +782,10 @@ $theme-yellow: #f8d714;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
&.unit-active {
background-color: #4c4d3b;
}
.card-top {
display: flex;
align-items: center;
... ... @@ -992,7 +848,7 @@ $theme-yellow: #f8d714;
.tips {
position: absolute;
color: #fff;
background-color: #3e3e3e;
background-color: $popup-bg;
z-index: 6;
right: 10rpx;
top: 20rpx;
... ... @@ -1029,18 +885,6 @@ $theme-yellow: #f8d714;
}
}
// .userWeight-values {
// display: flex;
// align-items: center;
// gap: 8rpx; // 控制“体重文本”和“编辑图标”的间距
// padding: 8rpx;
// .weight-text {
// color: #fadc00;
// font-size: 20rpx;
// }
// }
.userWeight {
padding: 16rpx 32rpx;
gap: 16rpx;
... ... @@ -1059,7 +903,7 @@ $theme-yellow: #f8d714;
.userWeight-values {
display: flex;
align-items: center;
gap: 8rpx; // 控制“体重文本”和“编辑图标”的间距
gap: 8rpx;
padding: 8rpx;
.weight-text {
... ... @@ -1071,242 +915,16 @@ $theme-yellow: #f8d714;
.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;
}
... ... @@ -1317,35 +935,8 @@ $theme-yellow: #f8d714;
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;
@include index-box;
}
.set-action-names {
... ... @@ -1356,73 +947,44 @@ $theme-yellow: #f8d714;
text {
font-size: 26rpx;
color: #fff;
color: $text-main;
}
}
.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;
@include timer-selector;
}
.check-icon {
@include action-icon-base;
}
.icon {
width: 70rpx;
height: 70rpx;
background-color: #1f1f1f;
border-radius: 12rpx;
.action-icons {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
&.active {
background-color: $theme-yellow;
}
}
}
.action-icon {
@include action-icon-base;
.serial {
width: 50rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #929292;
font-weight: bold;
&.icon-del {
background-color: #58372d;
}
}
.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;
gap: 10rpx;
.btn-item {
background-color: #1f1f1f;
background-color: $input-bg;
font-size: 26rpx;
color: #fff;
color: $text-main;
padding: 15rpx 35rpx;
border-radius: 50rpx;
display: flex;
... ... @@ -1435,7 +997,7 @@ $theme-yellow: #f8d714;
bottom: 80rpx;
left: 50%;
transform: translateX(-50%);
background-color: #3e3e3e;
background-color: $popup-bg;
border-radius: 16rpx;
overflow: hidden;
z-index: 99;
... ... @@ -1465,6 +1027,5 @@ $theme-yellow: #f8d714;
}
}
}
}
</style>
... ...
<template>
<!-- 超级组 子动作输入框 独立组件 -->
<view class="record-row super-record-row">
<view class="record-row super-record-row" :class="{ active: data.isActive }">
<view class="left super-left">
<view class="left-header super-left-header">
... ...
... ... @@ -59,7 +59,7 @@
<up-icon name="more-dot-fill" size="24" color="#fff" />
</view>
</template>
<!-- #ifdef H5 -->
<!-- #ifdef MP-WEIXIN -->
<button class="share-btn" open-type="share">
<up-icon name="share-fill" size="24" color="#fff" />
</button>
... ... @@ -154,17 +154,35 @@
<view class="section">
<view class="section muscle-section">
<view class="section-title">训练部位</view>
<image :src="actionDetail.urlImage" mode="widthFix" class="muscle-map" />
<view class="muscle-card">
<image
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260612/muscle_back_1781252170914.png"
mode="widthFix" class="muscle-map" />
<view class="legend">
<view class="legend-item">
<view class="legend-color primary"></view>
<text class="legend-text">主要部位</text>
<view class="legend-dot primary"></view>
<text>主要部位</text>
</view>
<view class="legend-item">
<view class="legend-color secondary"></view>
<text class="legend-text">次要部位</text>
<view class="legend-dot secondary"></view>
<text>次要部位</text>
</view>
</view>
</view>
<view class="muscle-tags">
<view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length">
<view class="group-label primary">主要训练部位</view>
<view class="pills">
<text class="pill primary-pill" v-for="(value, index) in actionDetail.primaryMuscleNames" :key="index">{{ value }}</text>
</view>
</view>
<view class="muscle-group" v-if="actionDetail.secondaryMuscleNames?.length">
<view class="group-label secondary">次要训练部位</view>
<view class="pills">
<text class="pill secondary-pill" v-for="(value, index) in actionDetail.secondaryMuscleNames" :key="index">{{ value }}</text>
</view>
</view>
</view>
</view>
... ... @@ -759,68 +777,103 @@ defineExpose({ open });
}
}
// ===================== 训练部位 =====================
.muscle-section {
.muscle-card {
position: relative;
background: #262626;
border-radius: 16rpx;
overflow: hidden;
.muscle-map {
width: 100%;
border-radius: 16rpx;
display: block;
}
.legend {
position: absolute;
bottom: 12rpx;
right: 16rpx;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
margin-top: 20rpx;
}
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(6px);
padding: 10rpx 20rpx;
border-radius: 30rpx;
.legend-item {
display: flex;
align-items: center;
gap: 10rpx;
gap: 8rpx;
font-size: 22rpx;
color: #ddd;
}
.legend-color {
width: 16rpx;
height: 16rpx;
.legend-dot {
width: 14rpx;
height: 14rpx;
border-radius: 4rpx;
}
.legend-color.primary {
&.primary {
background-color: #ffd700;
}
.legend-color.secondary {
&.secondary {
background-color: #999;
}
.legend-text {
font-size: 24rpx;
color: #fff;
}
}
}
.stat-card {
background: linear-gradient(135deg, #333, #222);
padding: 40rpx;
border-radius: 20rpx;
.muscle-tags {
display: flex;
flex-direction: column;
align-items: center;
border: 1rpx solid #444;
gap: 20rpx;
margin-top: 20rpx;
.muscle-group {
flex: 1;
background: #262626;
border-radius: 16rpx;
padding: 24rpx;
.label {
.group-label {
font-size: 24rpx;
color: #888;
font-weight: 600;
margin-bottom: 16rpx;
&.primary {
color: #ffd700;
}
.value {
font-size: 60rpx;
font-weight: bold;
color: #fedc1f;
margin: 10rpx 0;
&.secondary {
color: #aaa;
}
}
.date {
.pills {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.pill {
font-size: 22rpx;
color: #666;
padding: 8rpx 18rpx;
border-radius: 20rpx;
&.primary-pill {
background: rgba(255, 215, 0, 0.15);
color: #ffd700;
border: 1rpx solid rgba(255, 215, 0, 0.3);
}
&.secondary-pill {
background: rgba(170, 170, 170, 0.12);
color: #aaa;
border: 1rpx solid rgba(170, 170, 170, 0.25);
}
}
}
}
}
}
... ...
... ... @@ -286,6 +286,7 @@
</template>
<script setup>
import dayjs from 'dayjs';
import { ref, onMounted, watch, computed, nextTick } from 'vue';
import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
import TemplatesApi from '@/sheep/api/Template/Templates';
... ... @@ -594,19 +595,8 @@ const closeAddTrainPopup = () => {
// };
const formatSecondsToHms = (seconds) => {
// 处理空值/非数字
if (!seconds || isNaN(seconds)) return '00:00:00'
const total = parseInt(seconds, 10)
// 计算时分秒
const h = Math.floor(total / 3600)
const m = Math.floor((total % 3600) / 60)
const s = total % 60
// 补零 + 拼接
const hour = String(h).padStart(2, '0')
const min = String(m).padStart(2, '0')
const sec = String(s).padStart(2, '0')
return `${hour}:${min}:${sec}`
if (!seconds || isNaN(seconds)) return '00:00:00';
return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss');
}
// 返回按钮逻辑
... ...
... ... @@ -283,7 +283,7 @@ onMounted(() => {
<style lang="scss" scoped>
.exercise-page {
height: 87vh;
height: 86vh;
// #ifdef H5
height: calc(100vh - 44px);
// #endif
... ... @@ -373,7 +373,8 @@ onMounted(() => {
.left-nav {
width: 180rpx;
height: 100%;
height: 88%;
background-color: #f9fafb;
border-right: 1rpx solid #f3f4f6;
... ...
... ... @@ -372,10 +372,10 @@ onMounted(async () => {
/* 列表自适应滚动容器 */
.list-container {
flex: 1;
height: 0;
/* 关键:接触 Flex 内容支撑限制 */
min-height: 0;
padding-bottom: 40rpx;
overflow: hidden;
.template-scroll {
... ...
... ... @@ -388,6 +388,8 @@ const selectDate = async (date) => {
/* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
.date-cell {
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
... ... @@ -404,7 +406,7 @@ const selectDate = async (date) => {
&.other-month {
opacity: 0.5;
border-color: #f1f5f9;
border-color: #f9fafc;
}
&.is-today {
... ...
... ... @@ -24,11 +24,7 @@
<up-popup :show="drawerShow" mode="right" :closeable="true" @close="drawerShow = false">
<view class="drawer-content" :style="{ paddingTop: PageHeight + 'px' }">
<view class="user-header">
<image
class="avatar"
:src="userInfo.avatar || defaultAvatar "
mode="aspectFill"
/>
<image class="avatar" :src="userInfo.avatar || defaultAvatar" mode="aspectFill" />
<view class="user-info">
<text class="nickname">{{ userInfo.nickname || '-' }}</text>
</view>
... ... @@ -114,12 +110,10 @@ defineExpose({ switchTabIndex: handleTabClick });
<style scoped lang="scss">
.home-page {
width: 100%;
// #ifdef H5
height: calc(100vh - 50px);
// #endif
// #ifdef MP-WEIXIN
height: calc(100vh - 50px);
// #endif
background-color: #fff;
.content {
... ... @@ -128,7 +122,7 @@ defineExpose({ switchTabIndex: handleTabClick });
overflow: hidden;
padding-top: 65rpx;
// #ifdef MP-WEIXIN
padding-top: 200rpx;
padding-top: 220rpx;
// #endif
}
}
... ...
... ... @@ -6,6 +6,7 @@
</template>
<script setup>
import dayjs from 'dayjs';
import useTrainingStore from '@/sheep/store/training';
const trainStore = useTrainingStore();
... ... @@ -20,12 +21,7 @@ const goBackTrain = () => {
});
};
// 时间格式化
const formatTime = (s) => {
const m = Math.floor(s / 60).toString().padStart(2, '0');
const sec = (s % 60).toString().padStart(2, '0');
return `${m}:${sec}`;
};
const formatTime = (s) => dayjs.duration(s, 'seconds').format('mm:ss');
</script>
<style scoped>
... ...
... ... @@ -38,6 +38,7 @@
</template>
<script setup>
import dayjs from 'dayjs';
import { ref, computed } from 'vue'
import TrainingApi from '@/sheep/api/Training/traininghistory'
... ... @@ -104,12 +105,7 @@ const formattedSetList = computed(() => {
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')}`)
parts.push(dayjs.duration(set.duration, 'seconds').format('H:mm:ss'));
}
if (set.distance != null && set.distance !== '') {
parts.push(`${set.distance}m`)
... ...
<template>
<view class="action-container" @click="closeDifficulty">
<view class="action-list">
<view class="card">
<view class="card-top" @click.stop="isExpanded = !isExpanded">
... ...
<template>
<view class="template-edit-page">
<!-- 顶部导航栏 -->
<view class="template-edit-page" @click="dismissGuide">
<!-- 顶部导航 -->
<view class="header">
<view class="close-btn" @click="goBack">
<up-icon name="close" color="#fff" size="20" bold></up-icon>
<up-icon name="close" color="#fff" size="20" bold />
</view>
<view class="title-area">
<view class="title-row" @click="openTitlePopup">
<view class="title-text">{{ templateTitle || '点击输入标题' }}</view>
<uni-icons type="compose" size="24" color="#fff" />
<view class="title-area" @click="openTextPopup('title')">
<view class="title-row">
<text class="title-text">{{ templateTitle || '点击输入标题' }}</text>
<up-icon name="edit-pen" size="24" color="#fff" />
</view>
<view class="stats-text">{{ (trainingStore.actionDetail?.units || []).length }}个动作 ·
{{ totalSets }}组 · {{ totalWeight }}kg</view>
<text class="stats-text">
{{ unitCount }}个动作 · {{ totalSets }}组 · {{ totalWeight }}kg
</text>
</view>
<view class="save-btn" :class="{
disabled: trainingStore.actionDetail?.units?.length === 0,
active: trainingStore.actionDetail?.units?.length > 0
}" @click="handleSave">
<view
class="save-btn"
:class="{ disabled: unitCount === 0, active: unitCount > 0 }"
@click="handleSave"
>
保存模板
</view>
</view>
<!-- 内容区 -->
<view class="content-area">
<template v-if="trainingStore.actionDetail?.units?.length">
<view v-for="(unit, index) in trainingStore.actionDetail?.units" :key="unit.unitId">
<dongzuo :id="unit.unitId" :type="unit.unitType" :actionDetail="trainingStore.convertUnitToActionDetail(unit)"
:ref="el => setActionRef(el, index)" :unitIndex="index" />
<template v-if="unitCount">
<view v-for="(unit, index) in units" :key="unit.unitId">
<dongzuo
:id="unit.unitId"
:type="unit.unitType"
:action-detail="trainingStore.convertUnitToActionDetail(unit)"
:ref="(el) => setActionRef(el, index)"
:unit-index="index"
/>
</view>
</template>
<!-- 中间空白提示区域 -->
<view v-else class="empty-area">
<view class="empty-text">
尚未添加任何动作
<br />
尚未添加任何动作<br />
点击最下方 + 开始添加动作
</view>
</view>
</view>
<!-- 新手引导提示气泡 -->
<view class="guide-bubble" v-if="trainingStore.actionDetail?.units?.length === 0">
<!-- 引导气泡 -->
<view v-if="showGuide && unitCount === 0" class="guide-bubble" @click.stop>
挑选想要的动作开始训练吧
<view class="bubble-arrow"></view>
<view class="bubble-arrow" />
</view>
<!-- 底部操作栏 -->
<!-- 底部栏 -->
<view class="bottom-bar">
<view class="bar-item" @click="openActionSort">
<text class="item-num">{{ trainingStore.actionDetail?.units?.length }}</text>
<text class="item-num">{{ unitCount }}</text>
<text class="item-label">动作排序</text>
</view>
<view class="bar-item" @click="openIntroPopup">
<uni-icons type="list" size="24" color="#fff" />
<view class="bar-item" @click="openTextPopup('intro')">
<up-icon name="list" size="24" color="#fff" />
<text class="item-label">模板介绍</text>
</view>
<view class="bar-item add-btn" @click="handleAddAction">
<up-icon name="plus" color="#000" size="20" bold></up-icon>
<up-icon name="plus" color="#000" size="20" bold />
</view>
<view class="bar-item" @click="showScenePicker = true">
<uni-icons type="gear" size="24" color="#fff" />
<up-icon name="setting" size="24" color="#fff" />
<text class="item-label">训练场景</text>
</view>
<view class="bar-item" @click="handleEditCover">
<uni-icons type="image" size="24" color="#fff" />
<up-icon name="photo" size="24" color="#fff" />
<text class="item-label">编辑封面</text>
</view>
</view>
<!-- 模板介绍弹窗 -->
<view class="intro-popup" v-if="showIntroPopup">
<view class="popup-header">
<view class="popup-close-btn" @click="closeIntroPopup">
<up-icon name="close" color="#fff" size="20" bold></up-icon>
</view>
<view class="popup-title">模板介绍</view>
<view class="popup-save-btn" @click="saveIntro">保存</view>
</view>
<view class="popup-content">
<textarea class="intro-textarea" v-model="templateIntro" placeholder="在此处输入模板介绍" />
</view>
</view>
<!-- 模板标题弹窗 -->
<view class="intro-popup" v-if="showTitlePopup">
<!-- 文本编辑弹窗 -->
<view v-if="popupType" class="text-popup">
<view class="popup-header">
<view class="popup-close-btn" @click="closeTitlePopup">
<up-icon name="close" color="#fff" size="20" bold></up-icon>
<view class="popup-close-btn" @click="closeTextPopup">
<up-icon name="close" color="#fff" size="20" bold />
</view>
<view class="popup-title">模板标题</view>
<view class="popup-save-btn" @click="saveTemplateTitle">保存</view>
<text class="popup-title">{{ popupType === 'intro' ? '模板介绍' : '模板标题' }}</text>
<view class="popup-save-btn" @click="saveTextPopup">保存</view>
</view>
<view class="popup-content">
<textarea class="intro-textarea" v-model="templateTitle" placeholder="请输入模板标题" />
<textarea
class="intro-textarea"
v-model="popupValue"
:placeholder="popupType === 'intro' ? '在此处输入模板介绍' : '请输入模板标题'"
/>
</view>
</view>
<up-picker :show="showScenePicker" :columns="sceneColumns" :defaultIndex="[0]" title="训练场景" title-color="#ffffff"
cancel-color="#cccccc" confirm-color="#409eff" active-color="#ffd100" @confirm="handleSceneConfirm"
@cancel="showScenePicker = false" closeOnClickOverlay />
<!-- 动作排序组件 -->
<!-- 场景选择器 -->
<up-picker
:show="showScenePicker"
:columns="sceneColumns"
:default-index="[0]"
title="训练场景"
title-color="#ffffff"
cancel-color="#cccccc"
confirm-color="#409eff"
active-color="#ffd100"
@confirm="handleSceneConfirm"
@cancel="showScenePicker = false"
close-on-click-overlay
/>
<!-- 动作排序 / 新增 -->
<action-sort ref="actionSortRef" @open-add-actions="addExShow = true" />
<!-- 动作新增组件 -->
<addActions :show="addExShow" @close="addExShow = false" />
<add-actions :show="addExShow" @close="addExShow = false" />
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onLoad } from "@dcloudio/uni-app"
import { onLoad } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
import { useTrainingStore } from '@/sheep/store/trainingStore'
import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue';
import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue';
import { useTrainingStore } from '@/sheep/store/trainingStore';
import FileApi from '@/sheep/api/infra/file';
import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue'
import TemplatesApi from '@/sheep/api/Template/Templates';
import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue'
const coverImage = ref('');
const addExShow = ref(false)
const hasConverted = ref(false)
const dongzuoRefs = ref([]) // 获得子组件模板的数据
const trainingStore = useTrainingStore()
const canSave = ref(false);
const showGuide = ref(true);
const showIntroPopup = ref(false);
const templateIntro = ref('');
const showTitlePopup = ref(false);
const templateTitle = ref('');
dayjs.extend(duration);
const actionSortRef = ref(null)
// ---- 常量 ----
const NAVIGATE_DELAY = 1500;
const sceneList = ref([
const SCENE_LIST = [
{ id: 0, title: '不限' },
{ id: 1, title: '健身房' },
{ id: 2, title: '仅哑铃' },
{ id: 3, title: '仅哑铃+杠铃' },
]);
];
const SCENE_COLUMNS = [SCENE_LIST.map((item) => item.title)];
const WEIGHT_CALC = {
0: (weight, reps) => weight * reps,
4: (weight, reps, userWeight) => (userWeight + weight) * reps,
5: (weight, reps, userWeight) => (userWeight - weight) * reps,
};
// ---- Store ----
const trainingStore = useTrainingStore();
// ---- 状态 ----
const addExShow = ref(false);
const hasConverted = ref(false);
const dongzuoRefs = ref([]);
const templateTitle = ref('');
const templateIntro = ref('');
const coverImage = ref('');
const showScenePicker = ref(false);
const selectedSceneId = ref(0);
const sceneColumns = ref(SCENE_COLUMNS);
const showGuide = ref(true);
const actionSortRef = ref(null);
const popupType = ref(null); // 'intro' | 'title' | null
const popupValue = ref('');
// ---- 派生 ----
const units = computed(() => trainingStore.actionDetail?.units || []);
const unitCount = computed(() => units.value.length);
const sceneColumns = ref([
['不限', '健身房', '仅哑铃', '仅哑铃+杠铃']
]);
const totalSets = computed(() => {
let count = 0;
dongzuoRefs.value.forEach((ref) => {
if (ref?.totalSetCount) count += ref.totalSetCount;
});
return count;
});
const totalWeight = computed(() => {
const records = trainingStore.unitRecords || {};
let total = 0;
units.value.forEach((unit, unitIndex) => {
const record = records[unitIndex];
if (!record?.records) return;
unit.exercises?.forEach((ex) => {
const sets = record.records[ex.exerciseId];
if (!Array.isArray(sets)) return;
const calcFn = WEIGHT_CALC[ex.exerciseType];
if (!calcFn) return;
const userWeight = record.userWeight || 70;
sets.forEach((set) => {
total += calcFn(Number(set.weight) || 0, Number(set.reps) || 0, userWeight);
});
});
});
return total;
});
// ---- 生命周期 ----
onLoad(() => {
if (trainingStore.type !== 3 && !hasConverted.value) {
trainingStore.convertToTemplate();
hasConverted.value = true;
console.log('✅ 已完成模板转换(仅执行一次)', trainingStore.actionDetail);
}
console.log('在主页面转换为模板架构后的全局数据', trainingStore.type, trainingStore.actionDetail)
console.log('units.length:', trainingStore.actionDetail?.units?.length);
// ==============================================
// 编辑页回显(标题、介绍、场景、封面)
// ==============================================
const detail = trainingStore.actionDetail || {}
templateTitle.value = detail.templateName || ''
templateIntro.value = detail.introduction || ''
selectedSceneId.value = detail.scene || 0
coverImage.value = detail.urlCover || ''
const detail = trainingStore.actionDetail || {};
templateTitle.value = detail.templateName || '';
templateIntro.value = detail.introduction || '';
selectedSceneId.value = detail.scene || 0;
coverImage.value = detail.urlCover || '';
});
// 动作排序弹窗
const openActionSort = () => {
console.log('调用前,ref 的值:', actionSortRef.value)
actionSortRef.value.openActionSort()
}
// ---- 子组件引用 ----
const setActionRef = (el, index) => {
if (el) dongzuoRefs.value[index] = el;
};
// 全部的组数
const totalSets = computed(() => {
let count = 0
dongzuoRefs.value.forEach(ref => {
if (ref?.totalSetCount) count += ref.totalSetCount
})
return count
})
// 计算总训练重量(kg × 次数,仅统计力量类动作)
const totalWeight = computed(() => {
const units = trainingStore.actionDetail?.units || []
const records = trainingStore.unitRecords || {}
let total = 0
units.forEach((unit, unitIndex) => {
const record = records[unitIndex]
if (!record?.records) return
unit.exercises?.forEach(ex => {
const sets = record.records[ex.exerciseId]
if (!Array.isArray(sets)) return
sets.forEach(set => {
let weight = Number(set.weight) || 0
const reps = Number(set.reps) || 0
const userWeight = record.userWeight || 70
// 只计算 0、4、5 类型
if (ex.exerciseType === 0) {
total += weight * reps
}
else if (ex.exerciseType === 4) {
total += (userWeight + weight) * reps
// ---- 文本弹窗 ----
const openTextPopup = (type) => {
popupType.value = type;
popupValue.value = type === 'intro' ? templateIntro.value : templateTitle.value;
};
const closeTextPopup = () => { popupType.value = null; };
const saveTextPopup = () => {
if (popupType.value === 'title') {
if (!popupValue.value.trim()) {
uni.showToast({ title: '请输入模板标题', icon: 'none' });
return;
}
else if (ex.exerciseType === 5) {
total += (userWeight - weight) * reps
templateTitle.value = popupValue.value.trim();
} else {
templateIntro.value = popupValue.value;
}
})
})
})
uni.showToast({ title: '保存成功', icon: 'success' });
closeTextPopup();
};
return total
})
// 场景确认
const handleSceneConfirm = (e) => {
console.log('up-picker confirm 原始参数:', e);
// 从 indexs 里拿到选中的索引(单列就是 0 号)
const selectedIndex = e.indexs[0];
const selectedItem = sceneList.value[selectedIndex];
if (selectedItem) {
console.log("选中ID:", selectedItem.id);
console.log("选中标题:", selectedItem.title);
selectedSceneId.value = selectedItem.id;
}
// ---- 动作排序 / 添加 ----
const openActionSort = () => actionSortRef.value?.openActionSort();
const handleAddAction = () => { addExShow.value = true; showGuide.value = false; };
// ---- 场景选择 ----
const handleSceneConfirm = (e) => {
const item = SCENE_LIST[e.indexs[0]];
if (item) selectedSceneId.value = item.id;
showScenePicker.value = false;
};
// dongzuo组件
const setActionRef = (el, index) => {
if (el) {
dongzuoRefs.value[index] = el
}
// console.log('✅ 成功收集子组件实例:', dongzuoRefs.value)
}
// 编辑封面
// ---- 编辑封面 ----
const handleEditCover = async () => {
try {
// 1. 选择图片
const res = await uni.chooseImage({
count: 1, // 只选1张(封面)
sizeType: ['compressed'], // 压缩图
sourceType: ['album', 'camera'], // 相册+相机
});
const res = await uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'] });
const filePath = res.tempFilePaths[0];
if (!filePath) return;
// 2. 调用你现成的 FileApi 上传文件
const uploadRes = await FileApi.uploadFile(filePath, 'template-cover');
// 3. 上传成功
if (uploadRes && uploadRes.data) {
// 后端返回的图片地址
if (uploadRes?.data) {
coverImage.value = uploadRes.data;
console.log('封面上传成功:', coverImage.value);
uni.showToast({ title: '封面设置成功' });
uni.showToast({ title: '封面设置成功', icon: 'success' });
}
} catch (err) {
console.log('上传封面失败:', err);
console.error('上传封面失败:', err);
uni.showToast({ title: '上传失败', icon: 'none' });
}
};
// 模板介绍
const openIntroPopup = () => {
showIntroPopup.value = true;
};
const closeIntroPopup = () => {
setTimeout(() => {
showIntroPopup.value = false;
}, 2000);
};
// 保存模板介绍
const saveIntro = () => {
console.log(templateIntro.value);
uni.showToast({ title: '保存成功' });
closeIntroPopup();
};
// 模板标题
const openTitlePopup = () => {
showTitlePopup.value = true;
};
const closeTitlePopup = () => {
showTitlePopup.value = false;
};
const saveTemplateTitle = () => {
if (!templateTitle.value.trim()) {
uni.showToast({
title: '请输入模板标题',
icon: 'none'
});
return;
}
// 保存后关闭弹窗
closeTitlePopup();
uni.showToast({
title: '标题保存成功',
icon: 'success'
});
};
// ---- 导航 ----
const dismissGuide = () => { showGuide.value = false; };
// 返回上一页
const goBack = () => {
trainingStore.clearTrainingStore()
trainingStore.clearTrainingStore();
setTimeout(() => {
// 发送全局刷新信号,列表页会立刻执行刷新
uni.$emit('refreshTemplateList');
uni.navigateBack();
}, 1500);
}, NAVIGATE_DELAY);
};
// 保存模板(完整修复间歇训练 type=6)
const handleSave = async () => {
const units = trainingStore.actionDetail?.units;
if (!units || units.length === 0) {
uni.showToast({ title: '请先添加动作', icon: 'none' });
return;
}
if (!templateTitle.value.trim()) {
uni.showToast({ title: '请输入模板标题', icon: 'none' });
// openTitlePopup();
return;
}
try {
uni.showLoading({ title: '保存中...' });
const postData = {
// ---- 保存 ----
const buildSavePayload = () => ({
templateId: trainingStore.actionDetail.id || null,
groupId: null,
templateName: templateTitle.value.trim(),
scene: selectedSceneId.value,
templateCover: coverImage.value || '',
templateIntroduction: templateIntro.value || '',
units: units.map((unit, unitIndex) => {
units: units.value.map((unit, unitIndex) => {
const record = trainingStore.unitRecords[unitIndex] || {};
return {
id: unit.unitId || 0,
name: unit.name || '',
unitType: unit.unitType || 1,
sortOrder: unitIndex,
exercises: (unit.exercises || []).map((ex, exIndex) => {
const sets = record.records?.[ex.exerciseId] || [];
return {
exercises: (unit.exercises || []).map((ex, exIndex) => ({
exerciseId: ex.exerciseId || 0,
exerciseType: ex.exerciseType || 0,
innerOrder: exIndex,
sets: sets.map((set, setIndex) => {
let duration = 0;
let restTime = 0;
// 间歇训练单独处理
sets: (record.records?.[ex.exerciseId] || []).map((set, setIndex) => {
if (ex.exerciseType === 6) {
duration = Number(set.duration) || 0;
restTime = Number(set.restTime) || 0;
}
// 其它
else {
duration =
(Number(set.h) || 0) * 3600 +
(Number(set.m) || 0) * 60 +
(Number(set.s) || 0);
restTime = parseInt(set.quickTimeDisplay) || 60;
return { weight: 0, reps: 0, distance: 0, duration: Number(set.duration) || 0, restTime: Number(set.restTime) || 0, setIndex };
}
return {
weight: Number(set.weight) || 0,
reps: Number(set.reps) || 0,
distance: Number(set.distance) || 0,
duration: duration,
restTime: restTime,
setIndex: setIndex,
};
}),
duration: dayjs.duration({ hours: Number(set.h) || 0, minutes: Number(set.m) || 0, seconds: Number(set.s) || 0 }).asSeconds(),
restTime: parseInt(set.quickTimeDisplay) || 60,
setIndex,
};
}),
})),
};
}),
};
});
// 调用接口
let res;
// 判断:有 ID 就更新,没有 ID 就新建
if (trainingStore.actionDetail.id) {
res = await TemplatesApi.updateCustTemplate(postData);
} else {
res = await TemplatesApi.createCustTemplate(postData);
}
console.log('保存成功', res);
const handleSave = async () => {
if (!unitCount.value) { uni.showToast({ title: '请先添加动作', icon: 'none' }); return; }
if (!templateTitle.value.trim()) { uni.showToast({ title: '请输入模板标题', icon: 'none' }); return; }
try {
uni.showLoading({ title: '保存中...' });
const api = trainingStore.actionDetail.id
? TemplatesApi.updateCustTemplate
: TemplatesApi.createCustTemplate;
await api(buildSavePayload());
uni.hideLoading();
uni.showToast({ title: '保存成功' });
uni.showToast({ title: '保存成功', icon: 'success' });
trainingStore.clearTrainingStore();
setTimeout(() => {
uni.navigateBack();
}, 1500);
setTimeout(() => uni.navigateBack(), NAVIGATE_DELAY);
} catch (err) {
uni.hideLoading();
console.error('保存失败', err);
uni.showToast({ title: '保存失败', icon: 'none' });
}
};
// 添加动作
const handleAddAction = () => {
console.log('添加动作');
addExShow.value = true
showGuide.value = false;
};
</script>
<style scoped lang="scss">
// ---- 变量 ----
$color-bg-primary: #1a1a1a;
$color-bg-secondary: #333;
$color-bg-input: #444;
$color-accent: #ffd100;
$color-text-primary: #fff;
$color-text-secondary: #ccc;
$color-text-tertiary: #999;
$color-text-muted: #888;
$color-border: #333;
$font-size-xs: 24rpx;
$font-size-sm: 26rpx;
$font-size-base: 28rpx;
$font-size-md: 30rpx;
$font-size-lg: 32rpx;
$font-size-xl: 34rpx;
$font-size-xxl: 36rpx;
$radius-sm: 12rpx;
$radius-base: 16rpx;
$radius-lg: 24rpx;
$radius-round: 40rpx;
$radius-circle: 50%;
// ---- 容器 ----
.template-edit-page {
position: relative;
width: 100%;
height: 100vh;
background-color: #1a1a1a;
background-color: $color-bg-primary;
display: flex;
flex-direction: column;
color: #fff;
color: $color-text-primary;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
}
/* 顶部导航栏 */
// ---- 头部 ----
.header {
margin-top: 150rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
... ... @@ -448,8 +386,8 @@ const handleAddAction = () => {
.close-btn {
width: 60rpx;
height: 60rpx;
background-color: #333;
border-radius: 50%;
background-color: $color-bg-secondary;
border-radius: $radius-circle;
display: flex;
align-items: center;
justify-content: center;
... ... @@ -465,34 +403,31 @@ const handleAddAction = () => {
.title-row {
display: flex;
align-items: center;
gap: 15rpx;
}
.title-text {
font-size: 36rpx;
font-size: $font-size-xxl;
font-weight: 600;
margin-bottom: 10rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.stats-text {
font-size: 26rpx;
color: #999;
font-size: $font-size-sm;
color: $color-text-tertiary;
}
}
.save-btn {
padding: 16rpx 32rpx;
background-color: #333;
border-radius: 40rpx;
font-size: 28rpx;
color: #999;
background-color: $color-bg-secondary;
border-radius: $radius-round;
font-size: $font-size-base;
color: $color-text-tertiary;
flex-shrink: 0;
&.active {
background-color: #ffdc00;
background-color: $color-accent;
color: #000;
}
... ... @@ -502,16 +437,13 @@ const handleAddAction = () => {
}
}
// 中间内容区域
// ---- 内容区 ----
.content-area {
flex: 1;
overflow-y: auto;
padding-bottom: 20rpx;
display: flex;
flex-direction: column;
}
/* 中间空白区域 */
.empty-area {
flex: 1;
display: flex;
... ... @@ -520,24 +452,24 @@ const handleAddAction = () => {
padding: 40rpx;
.empty-text {
font-size: 30rpx;
color: #888;
font-size: $font-size-md;
color: $color-text-muted;
text-align: center;
line-height: 1.6;
}
}
/* 新手引导气泡 */
// ---- 引导气泡 ----
.guide-bubble {
position: absolute;
bottom: 220rpx;
bottom: 130rpx;
left: 50%;
transform: translateX(-50%);
background-color: #000;
color: #fff;
color: $color-text-primary;
padding: 20rpx 30rpx;
border-radius: 12rpx;
font-size: 30rpx;
border-radius: $radius-sm;
font-size: $font-size-md;
z-index: 10;
.bubble-arrow {
... ... @@ -545,49 +477,47 @@ const handleAddAction = () => {
bottom: -16rpx;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
border-top: 16rpx solid #000;
}
}
/* 底部操作栏 */
// ---- 底部栏 ----
.bottom-bar {
display: flex;
height: 110rpx;
align-items: center;
justify-content: space-around;
padding: 20rpx 0;
background-color: #1a1a1a;
border-top: 1rpx solid #333;
flex-shrink: 0;
padding-bottom: env(safe-area-inset-bottom);
background-color: $color-bg-primary;
border-top: 1rpx solid $color-border;
flex-shrink: 0;
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #fff;
font-size: $font-size-xs;
color: $color-text-primary;
.item-num {
font-size: 32rpx;
font-size: $font-size-lg;
font-weight: 600;
}
.item-label {
font-size: 24rpx;
color: #ccc;
font-size: $font-size-xs;
color: $color-text-secondary;
}
&.add-btn {
width: 100rpx;
height: 100rpx;
background-color: #fff;
border-radius: 50%;
background-color: $color-text-primary;
border-radius: $radius-circle;
display: flex;
align-items: center;
justify-content: center;
... ... @@ -597,28 +527,18 @@ const handleAddAction = () => {
}
}
.intro-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
}
.intro-popup {
// ---- 文本弹窗 ----
.text-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 85vh;
background: #333;
border-radius: 24rpx 24rpx 0 0;
background: $color-bg-secondary;
border-radius: $radius-lg $radius-lg 0 0;
z-index: 1000;
display: flex;
flex-direction: column;
// transition: transform 0.3s
}
.popup-header {
... ... @@ -628,128 +548,42 @@ const handleAddAction = () => {
padding: 40rpx 30rpx 20rpx;
}
.popup-close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.popup-title {
font-size: 34rpx;
color: #fff;
font-size: $font-size-xl;
color: $color-text-primary;
font-weight: bold;
}
.popup-save-btn {
background: #ffd100;
background: $color-accent;
color: #000;
padding: 12rpx 24rpx;
border-radius: 40rpx;
font-size: 28rpx;
border-radius: $radius-round;
font-size: $font-size-base;
}
.popup-content {
flex: 1;
padding: 30rpx;
flex-shrink: 0;
}
.intro-textarea {
width: 95%;
width: 100%;
height: 300rpx;
background: #444;
color: #fff;
border-radius: 16rpx;
background: $color-bg-input;
color: $color-text-primary;
border-radius: $radius-base;
padding: 20rpx;
font-size: 30rpx;
font-size: $font-size-md;
border: 0;
}
// 场景弹窗遮罩
.scene-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
}
// 场景弹窗本体
.scene-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #333;
border-radius: 24rpx 24rpx 0 0;
z-index: 1000;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
// 固定高度(和截图比例一致)
height: 60vh;
max-height: 70vh;
}
// 顶部横条
.scene-popup::before {
content: '';
position: absolute;
top: 16rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 8rpx;
background: #666;
border-radius: 4rpx;
}
// 弹窗头部
.scene-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 30rpx 20rpx;
.scene-close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.scene-popup-title {
font-size: 34rpx;
color: #fff;
font-weight: bold;
}
}
// 场景选项列表
.scene-list {
flex: 1;
overflow-y: auto;
.scene-item {
padding: 30rpx;
text-align: center;
font-size: 32rpx;
color: #999;
border-bottom: 1rpx solid #444;
&.active {
color: #fff;
font-weight: bold;
}
}
}
// 底部确认按钮
.scene-confirm-btn {
margin: 30rpx;
padding: 24rpx;
background: #ffd100;
color: #000;
border-radius: 40rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
box-sizing: border-box;
}
</style>
... ...
<template>
<view class="container">
<!-- #ifdef MP-WEIXIN -->
<up-navbar placeholder bgColor="transparent">
<template #left>
<view></view>
</template>
</up-navbar>
<up-navbar placeholder bgColor="transparent" />
<!-- #endif -->
<view class="status-header">
<view class="timer-box" @click="show = true">
... ... @@ -18,19 +14,29 @@
<view class="action-list">
<view class="header">
<view class="title-left" @click="openTrainingNamePopup">
<up-icon name="edit-pen" color="#8e8e93" size="22"></up-icon>
<up-icon name="edit-pen" color="#8e8e93" size="22" />
<text class="title-name">{{ trainingName }}</text>
</view>
<text class="title-stats">
{{ (trainingStore.actionDetail?.units || []).length }} 个动作· {{ totalSets }}组 · {{ totalCheckedWeight }}/{{
allTotalWeight }}kg
{{ (actionDetail?.units || []).length }} 个动作· {{ totalSets }}组 ·
{{ totalCheckedWeight }}/{{ allTotalWeight }}kg
</text>
</view>
<!-- 3. 模板训练 type=3 循环 -->
<template v-if="trainingStore.actionDetail?.units?.length">
<view v-for="(unit, index) in trainingStore.actionDetail?.units" :key="unit.unitId">
<dongzuo :id="unit.unitId" :type="unit.unitType" :actionDetail="trainingStore.convertUnitToActionDetail(unit)"
:ref="el => setActionRef(el, index)" :unitIndex="index" :is-daily-templates="false" />
<template v-if="actionDetail?.units?.length">
<view v-for="(unit, index) in actionDetail.units" :key="unit.unitId">
<dongzuo
:id="unit.unitId"
:type="unit.unitType"
:actionDetail="trainingStore.convertUnitToActionDetail(unit)"
:ref="(el) => setActionRef(el, index)"
:unitIndex="index"
:is-daily-templates="false"
:isUnitActive="unitActiveStates[index]"
@startRestTimer="startRestCountdown"
@cancelRestTimer="onCancelRestTimer"
@updateUnitActive="(val) => onUnitActiveChange(index, val)"
/>
</view>
</template>
... ... @@ -39,42 +45,80 @@
<text class="empty-text">尚未添加任何动作</text>
<text class="empty-subtext">点击最下方 + 开始添加动作</text>
</view>
<!-- 完成按钮 -->
<view class="finish-btn" @click="save">完成</view>
</view>
<view v-if="!trainingStore.actionDetail?.units?.length" class="tip-bubble">
<view v-if="!actionDetail?.units?.length" class="tip-bubble">
<text>挑选想要的动作开始训练吧</text>
</view>
<view class="footer-bar">
<view class="footer-item" @click="openActionSort">
<text class="count">{{ trainingStore.actionDetail?.units?.length || 0 }}</text>
<text class="count">{{ (actionDetail?.units || []).length }}</text>
<text class="label">动作排序</text>
</view>
<view class="footer-item" @click="openSummaryPopup">
<up-icon name="edit-pen" color="#fff" size="24"></up-icon>
<up-icon name="edit-pen" color="#fff" size="24" />
<text class="label">写总结</text>
</view>
<view class="add-btn-wrapper">
<view class="add-btn" @click="addActionsPopup">
<up-icon name="plus" color="#000" size="20" bold></up-icon>
<up-icon name="plus" color="#000" size="20" bold />
</view>
</view>
<view class="footer-item" @click="openMin">
<up-icon name="minus-square-fill" color="#fff" size="24"></up-icon>
<up-icon name="minus-square-fill" color="#fff" size="24" />
<text class="label">最小化</text>
</view>
<view class="footer-item" @click="deleteTraining">
<up-icon name="trash" color="#fff" size="24"></up-icon>
<up-icon name="trash" color="#fff" size="24" />
<text class="label">删除</text>
</view>
</view>
<!-- 组间休息倒计时 -->
<view
class="rest-countdown"
v-if="restCountdownSeconds > 0"
@click="
clearRestCountdown();
uncheckCurrentUnit();
if (restTimerWasRunning) {
trainingStore.toggleTimer();
restTimerWasRunning = false;
}
"
>
<view class="countdown-circle">
<!-- H5 端:使用 SVG 进度环 -->
<!-- #ifdef H5 -->
<svg class="ring-svg" viewBox="0 0 100 100">
<circle class="ring-bg" cx="50" cy="50" r="44" />
<circle
class="ring-progress"
cx="50"
cy="50"
r="44"
:stroke-dasharray="CIRCUMFERENCE"
:stroke-dashoffset="CIRCUMFERENCE * (1 - restProgress)"
transform="rotate(-90 50 50)"
/>
</svg>
<!-- #endif -->
<!-- 微信小程序端:不显示进度环 -->
<view class="countdown-inner">
<text class="rest-time">{{ restFormattedTime }}</text>
<text class="rest-total">{{ restCountdownTotal }}s</text>
</view>
</view>
</view>
<!-- 设置训练时间弹窗 -->
<up-popup :show="show" mode="top" @close="show = false" :safeAreaInsetBottom="false" safeAreaInsetTop
bgColor="#2c2c2e">
<up-popup
:show="show"
mode="top"
@close="show = false"
:safeAreaInsetBottom="false"
safeAreaInsetTop
bgColor="#2c2c2e"
>
<view class="time-pop-container">
<view class="pop-content">
<view class="pop-title">设置训练时间</view>
... ... @@ -107,7 +151,11 @@
</view>
<view class="timer-right">
<view class="action-btn gray-btn" v-if="isPause" @click="resetTimer">重置</view>
<view class="action-btn green-btn" :class="{ active: !isPause, bgactve: !isPause }" @click="toggleTimer">
<view
class="action-btn green-btn"
:class="{ active: !isPause, bgactve: !isPause }"
@click="toggleTimer"
>
{{ isPause ? '继续计时' : '暂停计时' }}
</view>
</view>
... ... @@ -120,116 +168,205 @@
</up-popup>
<!-- 设置训练日期 -->
<up-datetime-picker :show="datePickerShow" v-model="dateValue" mode="date" closeOnClickOverlay
@confirm="confirmDate" @cancel="datePickerShow = false" @close="datePickerShow = false">
<up-datetime-picker
:show="datePickerShow"
v-model="dateValue"
mode="date"
closeOnClickOverlay
@confirm="confirmDate"
@cancel="datePickerShow = false"
@close="datePickerShow = false"
>
</up-datetime-picker>
<!-- 设置起始时间 -->
<up-datetime-picker :show="timePickerShow" v-model="timeValue" mode="time" closeOnClickOverlay
@confirm="confirmTime" @cancel="timePickerShow = false" @close="timePickerShow = false">
<up-datetime-picker
:show="timePickerShow"
v-model="timeValue"
mode="time"
closeOnClickOverlay
@confirm="confirmTime"
@cancel="timePickerShow = false"
@close="timePickerShow = false"
>
</up-datetime-picker>
<!-- 设置训练时长计时 -->
<up-picker :show="showPicker" :columns="timeColumns" :defaultIndex="defaultTimeIndex" title="修改训练时长"
@confirm="onTimeConfirm" @cancel="showPicker = false" closeOnClickOverlay>
<up-picker
:show="showPicker"
:columns="timeColumns"
:defaultIndex="defaultTimeIndex"
title="修改训练时长"
@confirm="onTimeConfirm"
@cancel="showPicker = false"
closeOnClickOverlay
>
</up-picker>
<action-sort ref="actionSortRef" @open-add-actions="addActionsShow = true" />
<!-- 写总结弹窗 -->
<up-popup :show="summaryShow" @close="summaryShow = false" mode="bottom" :safeAreaInsetBottom="false">
<up-popup
:show="summaryShow"
@close="summaryShow = false"
mode="bottom"
:safeAreaInsetBottom="false"
>
<view class="summaryPopup">
<view class="popup-header">
<text class="popup-title">本次训练总结</text>
<view class="save-btn" @click="saveSummary">保存</view>
</view>
<textarea class="summary-textarea" v-model="summaryContent" placeholder="记录本次训练感悟" />
<textarea
class="summary-textarea"
v-model="summaryContent"
placeholder="记录本次训练感悟"
/>
</view>
</up-popup>
<!-- 命名弹窗 -->
<up-popup :show="inputNameShow" @close="inputNameShow = false" mode="bottom" :safeAreaInsetBottom="false">
<up-popup
:show="inputNameShow"
@close="inputNameShow = false"
mode="bottom"
:safeAreaInsetBottom="false"
>
<view class="namePopup">
<view class="popup-header">
<text class="popup-title">训练名称</text>
<view class="save-btn" @click="saveTrainingName">保存</view>
</view>
<textarea v-model="trainingStore.trainingName" class="name-input" placeholder="请输入训练名称"
placeholder-color="#8e8e93" color="#fff" />
<textarea
v-model="trainingStore.trainingName"
class="name-input"
placeholder="请输入训练名称"
placeholder-color="#8e8e93"
color="#fff"
/>
</view>
</up-popup>
<!-- 动作新增组件 -->
<addActions :show="addActionsShow" @close="addActionsShow = false" />
<action-sort ref="actionSortRef" @open-add-actions="addActionsShow = true" />
</view>
</template>
<script setup>
import { ref, reactive, computed, onBeforeUnmount, onMounted, nextTick, onUnmounted, watch } from 'vue';
import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
import { onLoad } from "@dcloudio/uni-app"
import TrainingApi from '@/sheep/api/Training/traininghistory'
import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
import { useTrainingStore } from '@/sheep/store/trainingStore'
import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue'
import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue'
import { trainingSnapshot } from '@/sheep/store/trainingSnapshot'
const trainingStore = useTrainingStore()
const hasConverted = ref(false);
const addActionsShow = ref(false)
const ActionRef = ref(null)
// --- 计时核心逻辑 ---
const totalSeconds = computed(() => trainingStore.totalSeconds);
const isPause = computed(() => trainingStore.isPause);
const showPicker = computed(() => trainingStore.showPicker);
const defaultTimeIndex = computed(() => trainingStore.defaultTimeIndex);
const groupCount = ref(0)
const summaryShow = ref(false)
const summaryContent = ref('')
// 训练名称
const inputNameShow = ref(false)
const trainingName = computed(() => trainingStore.trainingName)
const actionSortRef = ref(null)
watch(
() => trainingStore.isTraining,
(newVal, oldVal) => {
console.log('训练状态变化:', oldVal, '→', newVal)
if (newVal) {
// 正在训练逻辑
} else {
// 结束训练逻辑
import { ref, computed, nextTick, onUnmounted } from 'vue';
import dayjs from 'dayjs';
import { onLoad } from '@dcloudio/uni-app';
import TrainingApi from '@/sheep/api/Training/traininghistory';
import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
import { useTrainingStore } from '@/sheep/store/trainingStore';
import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue';
import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue';
import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
import { trainingSnapshot } from '@/sheep/store/trainingSnapshot';
const trainingStore = useTrainingStore();
const addActionsShow = ref(false);
const hasConverted = ref(false);
const summaryShow = ref(false);
const summaryContent = ref('');
const inputNameShow = ref(false);
const actionSortRef = ref(null);
const show = ref(false);
const datePickerShow = ref(false);
const timePickerShow = ref(false);
const dateValue = ref(Number(new Date()));
const timeValue = ref('14:13');
const trainingDateText = ref('');
const totalSeconds = computed(() => trainingStore.totalSeconds || 0);
const isPause = computed(() => trainingStore.isPause);
const showPicker = computed(() => trainingStore.showPicker);
const defaultTimeIndex = computed(() => trainingStore.defaultTimeIndex);
const trainingName = computed(() => trainingStore.trainingName);
const trainingTimeText = computed(() => trainingStore.trainingTimeText);
const actionDetail = computed(() => trainingStore.actionDetail || {});
// ===================== 组间休息倒计时 =====================
const restCountdownSeconds = ref(0);
const restCountdownTotal = ref(0);
let restCountdownInterval = null;
let restTimerWasRunning = false;
const restFormattedTime = computed(() => {
const min = Math.floor(restCountdownSeconds.value / 60);
const sec = restCountdownSeconds.value % 60;
return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
});
const restProgress = computed(() => {
if (restCountdownTotal.value <= 0) return 0;
return 1 - restCountdownSeconds.value / restCountdownTotal.value;
});
// H5 SVG 圆环周长(用于 stroke-dasharray)
const CIRCUMFERENCE = 2 * Math.PI * 44;
const currentRestUnitIndex = ref(null);
const clearRestCountdown = () => {
if (restCountdownInterval) {
clearInterval(restCountdownInterval);
restCountdownInterval = null;
}
},
{ immediate: true } // 页面初始化立即执行一次
)
restCountdownSeconds.value = 0;
restCountdownTotal.value = 0;
};
const startRestCountdown = (seconds, unitIndex) => {
if (!seconds || seconds <= 0) return;
clearRestCountdown();
restCountdownTotal.value = seconds;
restCountdownSeconds.value = seconds;
currentRestUnitIndex.value = unitIndex;
restTimerWasRunning = !trainingStore.isPause;
if (restTimerWasRunning) trainingStore.toggleTimer();
restCountdownInterval = setInterval(() => {
restCountdownSeconds.value--;
if (restCountdownSeconds.value <= 0) {
clearRestCountdown();
uncheckCurrentUnit();
if (restTimerWasRunning) trainingStore.toggleTimer();
}
}, 1000);
};
// 动作排序弹窗
const openActionSort = () => {
console.log('调用前,ref 的值:', actionSortRef.value)
actionSortRef.value.openActionSort()
}
function uncheckCurrentUnit() {
if (currentRestUnitIndex.value != null) {
const ref = dongzuoRefs.value[currentRestUnitIndex.value];
if (ref?.resetActiveSets) ref.resetActiveSets();
currentRestUnitIndex.value = null;
}
}
onLoad(async (options) => {
function onCancelRestTimer(unitIndex) {
clearRestCountdown();
currentRestUnitIndex.value = unitIndex;
uncheckCurrentUnit();
if (restTimerWasRunning) {
trainingStore.toggleTimer();
restTimerWasRunning = false;
}
}
console.log('✅ 动作练习页面 onLoad 中的 options:', options);
onUnmounted(() => {
clearRestCountdown();
});
const openActionSort = () => {
actionSortRef.value.openActionSort();
};
onLoad(async (options) => {
const id = Number(options.id);
const type = Number(options.type);
const isTraining = options.isTraining === 'true';
trainingStore.isTraining = isTraining;
console.log('✅动作练习页面 onLoad 中的 options:', options);
const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null;
trainingStore.dailyTemplateId = dailyTemplateId;
console.log('接收到的 dailyTemplateId:', dailyTemplateId);
// 直接让 store 去请求 + 保存数据
if (!dailyTemplateId) {
await trainingStore.loadTrainingDetail(id, type);
... ... @@ -241,363 +378,229 @@ onLoad(async (options) => {
if (!hasConverted.value) {
trainingStore.convertToTemplate();
hasConverted.value = true;
console.log('✅ 已完成模板转换(仅执行一次)', trainingStore.actionDetail);
}
console.log('在主页面转换为模板架构后的全局数据', trainingStore.type, trainingStore.actionDetail)
initTrainingName();
//动态初始化训练日期为当天
const today = new Date();
const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][today.getDay()];
trainingDateText.value = dayjs().format('YYYY-MM-DD dd');
if (!trainingStore.trainingTimeText) {
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
trainingStore.setTrainingTimeText(timeStr);
}
console.log('✅ 动态初始化训练日期:', trainingStore.trainingTimeText);
});
const initTrainingName = () => {
const name = trainingStore.actionDetail?.templateName || '训练';
trainingStore.setTrainingName(name); // 存进 Pinia
};
// 全部的组数
const totalSets = computed(() => {
let count = 0
dongzuoRefs.value.forEach(ref => {
if (ref?.totalSetCount) count += ref.totalSetCount
})
return count
})
// 已勾选的总重量
const totalCheckedWeight = computed(() => {
let sum = 0
dongzuoRefs.value.forEach(ref => {
if (ref?.checkedWeight) sum += ref.checkedWeight
})
return sum
})
// 所有组的总重量
const allTotalWeight = computed(() => {
let sum = 0
dongzuoRefs.value.forEach(ref => {
if (ref?.totalWeight) sum += ref.totalWeight
})
return sum
})
// 获得子组件模板的数据
const dongzuoRefs = ref([])
const setActionRef = (el, index) => {
if (el) {
dongzuoRefs.value[index] = el
}
// console.log('✅ 成功收集子组件实例:', dongzuoRefs.value)
}
// 打开/关闭总结弹窗
const openSummaryPopup = () => {
summaryShow.value = true
summaryContent.value = '' // 打开时清空内容
}
// 保存总结
const saveSummary = () => {
console.log('保存的训练总结:', summaryContent.value)
summaryShow.value = false
summaryContent.value = ''
}
// 打开命名弹窗
const openTrainingNamePopup = () => {
trainingStore.setTrainingTimeText(dayjs().format('HH:mm'));
}
if (isTraining) {
trainingStore.toggleTimer();
}
});
const initTrainingName = () => {
trainingStore.setTrainingName(actionDetail.value?.templateName || '训练');
};
const totalSets = computed(() => {
let count = 0;
dongzuoRefs.value.forEach((ref) => {
if (ref?.totalSetCount) count += ref.totalSetCount;
});
return count;
});
const totalCheckedWeight = computed(() => {
let sum = 0;
dongzuoRefs.value.forEach((ref) => {
if (ref?.checkedWeight) sum += ref.checkedWeight;
});
return sum;
});
const allTotalWeight = computed(() => {
let sum = 0;
dongzuoRefs.value.forEach((ref) => {
if (ref?.totalWeight) sum += ref.totalWeight;
});
return sum;
});
// 获得子组件模板的数据
const dongzuoRefs = ref([]);
const setActionRef = (el, index) => {
if (el) dongzuoRefs.value[index] = el;
};
// 每个 unit 是否有勾选的组
const unitActiveStates = ref([])
const onUnitActiveChange = (index, val) => {
unitActiveStates.value[index] = val
unitActiveStates.value = [...unitActiveStates.value]
}
const openSummaryPopup = () => {
summaryShow.value = true;
summaryContent.value = '';
};
const saveSummary = () => {
summaryShow.value = false;
summaryContent.value = '';
};
const openTrainingNamePopup = () => {
inputNameShow.value = true;
};
};
// 保存训练名称
const saveTrainingName = () => {
const saveTrainingName = () => {
trainingStore.setTrainingName(trainingStore.trainingName);
if (trainingStore.actionDetail) {
if (actionDetail.value) {
trainingStore.actionDetail.templateName = trainingStore.trainingName;
}
inputNameShow.value = false;
};
// 格式化时间
const formattedTime = computed(() => {
const h = Math.floor(totalSeconds.value / 3600)
.toString()
.padStart(2, '0');
const m = Math.floor((totalSeconds.value % 3600) / 60)
.toString()
.padStart(2, '0');
const s = (totalSeconds.value % 60).toString().padStart(2, '0');
return `${h}:${m}:${s}`;
});
// 继续或者暂停计时
const toggleTimer = () => trainingStore.toggleTimer();
// 重置时间
const resetTimer = () => trainingStore.resetTimer();
// --- Picker 修改时间 ---
const timeColumns = reactive([
};
const formattedTime = computed(() =>
dayjs.duration(totalSeconds.value, 'seconds').format('HH:mm:ss'),
);
const toggleTimer = () => trainingStore.toggleTimer();
const resetTimer = () => trainingStore.resetTimer();
const timeColumns = ref([
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 handleTimerClick = () => {
const handleTimerClick = () => {
if (isPause.value) {
trainingStore.openTimePicker();
}
};
};
const onTimeConfirm = (e) => {
const onTimeConfirm = (e) => {
const rawValues = e.value.map((val) => parseInt(String(val).replace(/[时分秒]/g, '')));
const seconds = rawValues[0] * 3600 + rawValues[1] * 60 + rawValues[2];
trainingStore.setTotalSeconds(seconds);
trainingStore.closeTimePicker();
};
// --- 基础弹窗 ---
const show = ref(false);
const datePickerShow = ref(false);
const timePickerShow = ref(false);
const dateValue = ref(Number(new Date()));
const timeValue = ref('14:13');
const trainingDateText = ref('2026-04-26 周日');
// const trainingTimeText = ref('14:13');
// const trainingTimeText = ref(new Date().getHours().toString().padStart(2, '0') + ':' + new Date().getMinutes().toString().padStart(2, '0'));
const trainingTimeText = computed(() => trainingStore.trainingTimeText);
const confirmDate = (e) => {
const date = new Date(e.value);
const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()];
trainingDateText.value = `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${week}`;
};
const confirmDate = (e) => {
trainingDateText.value = dayjs(e.value).format('YYYY-MM-DD dd');
datePickerShow.value = false;
};
};
const confirmTime = (e) => {
const confirmTime = (e) => {
trainingStore.setTrainingTimeText(e.value);
timePickerShow.value = false;
};
const save = async () => {
await nextTick()
let units = []
};
const mapSets = (recordItems, exerciseType) =>
recordItems.map((item, index) => {
const isInterval = exerciseType === 6;
const duration = isInterval
? Number(item.duration) || 0
: Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0;
const restTime = isInterval
? Number(item.restTime) || 0
: parseInt(item.quickTimeDisplay) || 0;
return {
weight: Number(item.weight) || 0,
reps: Number(item.reps) || 0,
distance: Number(item.distance) || 0,
duration,
restTime,
setIndex: index + 1,
};
});
const unitList = trainingStore.actionDetail.units || []
console.log('✅ 最终要提交的 unitList:', unitList)
console.log('✅ 子组件实例 tempList:', dongzuoRefs.value)
const save = async () => {
await nextTick();
const units = [];
const unitList = actionDetail.value?.units || [];
for (let i = 0; i < unitList.length; i++) {
const unit = unitList[i]
const child = dongzuoRefs.value[i]
const unit = unitList[i];
const child = dongzuoRefs.value[i];
if (!child) continue;
if (!child) {
console.warn(`第 ${i} 个动作组件未获取到!`)
continue
}
// ==========================
// 1. 单个动作
// ==========================
if (unit.unitType === 1) {
if (!child.recordList) continue
const exerciseType = unit.exercises[0].exerciseType
console.log(`\n🔹 第 ${i} 个动作 - 运动类型 =`, exerciseType) // 👈 打印运动类型
const exercises = [{
exerciseId: unit.exercises[0].exerciseId,
exerciseType: exerciseType,
sets: child.recordList.map((item, index) => {
let duration = 0
let restTime = 0
// ==============================================
// 判断是不是间歇运动
// ==============================================
if (exerciseType === 6) {
// ✅ 间歇运动:取你输入的 duration、restTime
duration = Number(item.duration) || 0
restTime = Number(item.restTime) || 0
console.log(`✅ 组 ${index + 1}【间歇运动】输入的值:`, {
duration: item.duration,
restTime: item.restTime,
reps: item.reps
}) // 控制台打印
} else {
// ✅ 普通运动:保持原来逻辑
duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0
restTime = parseInt(item.quickTimeDisplay) || 0
}
return {
weight: Number(item.weight) || 0,
reps: Number(item.reps) || 0,
distance: Number(item.distance) || 0,
duration: duration,
restTime: restTime,
setIndex: index + 1
}
})
}]
if (!child.recordList) continue;
const exerciseType = unit.exercises[0].exerciseType;
units.push({
name: i === 0 ? trainingName.value : unit.unitName,
unitType: 1,
exercises
})
}
// ==========================
// 2. 超级组
// ==========================
else if (unit.unitType === 2) {
if (!child.superRecordMap) continue
const exercises = unit.exercises.map((sub, idx) => {
const setList = child.superRecordMap[sub.exerciseId] || []
const subExerciseType = sub.exerciseType
console.log(`超级组子动作 ${idx + 1} - 运动类型 =`, subExerciseType)
return {
exerciseId: sub.exerciseId,
exerciseType: subExerciseType,
innerOrder: idx + 1,
sets: setList.map((item, index) => {
let duration = 0
let restTime = 0
// ==============================================
// 超级组里的间歇运动
// ==============================================
if (subExerciseType === 6) {
duration = Number(item.duration) || 0
restTime = Number(item.restTime) || 0
console.log(`组 ${index + 1}【超级组间歇运动】输入的值:`, {
duration: item.duration,
restTime: item.restTime,
reps: item.reps
}) // 控制台打印
} else {
duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0
restTime = parseInt(item.quickTimeDisplay) || 0
}
return {
weight: Number(item.weight) || 0,
reps: Number(item.reps) || 0,
distance: Number(item.distance) || 0,
duration: duration, // 修复后
restTime: restTime, // 修复后
setIndex: index + 1
}
})
}
})
exercises: [
{
exerciseId: unit.exercises[0].exerciseId,
exerciseType,
sets: mapSets(child.recordList, exerciseType),
},
],
});
} else if (unit.unitType === 2) {
if (!child.superRecordMap) continue;
units.push({
name: i === 0 ? trainingName.value : unit.unitName,
unitType: 2,
exercises
})
exercises: unit.exercises.map((sub, idx) => ({
exerciseId: sub.exerciseId,
exerciseType: sub.exerciseType,
innerOrder: idx + 1,
sets: mapSets(child.superRecordMap[sub.exerciseId] || [], sub.exerciseType),
})),
});
}
}
console.log('\n最终提交后台的完整数据:', JSON.stringify(units, null, 2))
if (units.length === 0) {
uni.showToast({ title: '未获取到训练数据', icon: 'none' })
return
uni.showToast({ title: '未获取到训练数据', icon: 'none' });
return;
}
console.log('trainingStore.isTraining=', trainingStore.isTraining);
try {
// ✅ 情况1:【去训练】→ 保存历史
if (trainingStore.isTraining) {
await TrainingApi.createTrainHistory({ units });
uni.showToast({ title: '训练提交成功', icon: 'success' });
}
// ✅ 情况2:【编辑模板】→ 更新模板
else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
} else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
await dailytemplateApi.updateDailyTemplate({
dailyTemplateId: trainingStore.dailyTemplateId,
units: units
units,
});
uni.showToast({ title: '模板修改成功', icon: 'success' });
}
// ✅ 情况3:普通训练
else {
} else {
await TrainingApi.createTrainHistory({ units });
uni.showToast({ title: '训练提交成功', icon: 'success' });
}
setTimeout(() => {
uni.navigateBack();
trainingStore.clearTrainingStore();
}, 1500)
trainingStore.clearTrainingStore()
}, 1500);
} catch (err) {
console.error('❌ 保存失败:', err)
uni.showToast({ title: '保存失败', icon: 'none' })
console.error('保存失败:', err);
uni.showToast({ title: '保存失败', icon: 'none' });
}
}
// 删除训练
const deleteTraining = () => {
};
// 删除训练
const deleteTraining = () => {
uni.showModal({
title: '确认删除',
content: '确定要删除本次训练记录吗?删除后无法恢复',
confirmText: '确认删除',
confirmColor: '#FF3B30', // 红色强调删除
confirmColor: '#FF3B30',
cancelText: '取消',
success: (res) => {
// 用户点击确认
if (res.confirm) {
uni.showToast({
title: '删除成功',
icon: 'success',
duration: 1500
});
setTimeout(async () => {
uni.showToast({ title: '删除成功', icon: 'success', duration: 1500 });
setTimeout(() => {
uni.navigateBack();
await nextTick();
trainingStore.clearTrainingStore()
trainingStore.clearTrainingStore();
}, 1500);
}
else if (res.cancel) {
console.log('用户取消删除');
}
}
},
});
};
// 动作新增组件
const addActionsPopup = () => {
console.log('打开新增弹窗');
addActionsShow.value = true // 打开弹窗
}
};
// 最小化
const openMin = () => {
console.log('最小化跳转前trainingStore.isTraining', trainingStore.isTraining);
console.log('trainingStore.min+++++++++++++', trainingStore.min);
const addActionsPopup = () => {
addActionsShow.value = true;
};
console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining);
const openMin = () => {
trainingStore.min = true;
// 保存快照
trainingSnapshot.data = JSON.parse(JSON.stringify({
trainingSnapshot.data = JSON.parse(
JSON.stringify({
id: trainingStore.id,
type: trainingStore.type,
actionDetail: trainingStore.actionDetail,
... ... @@ -613,44 +616,30 @@ const openMin = () => {
isSystem: trainingStore.isSystem,
dailyTemplateId: trainingStore.dailyTemplateId,
isTraining: trainingStore.isTraining,
}))
}),
);
// 2. 记录时间戳和计时器状态
trainingSnapshot.timestamp = Date.now()
trainingSnapshot.timerWasRunning = !trainingStore.isPause
trainingSnapshot.timestamp = Date.now();
trainingSnapshot.timerWasRunning = !trainingStore.isPause;
trainingSnapshot.visible = true;
// 3. 显示悬浮球
trainingSnapshot.visible = true
// 4. 如果计时器在跑,先暂停(setInterval 跨页面会丢失,回显时用时间戳补偿)
if (!trainingStore.isPause) {
trainingStore.toggleTimer()
trainingStore.toggleTimer();
}
console.log('🟡 快照已保存:id=', trainingSnapshot.data.id,
'totalSeconds=', trainingSnapshot.data.totalSeconds)
// uni.navigateTo({
// url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径
// });
console.log('++++++++++++++最小化开始trainingStore.min+++++++++++++', trainingStore.min);
uni.navigateBack();
}
onUnmounted(() => {
// trainingStore.clearTrainingStore()
})
};
</script>
<style lang="scss" scoped>
.container {
.container {
min-height: 100vh;
background-color: #1c1c1e;
background-color: #242424;
padding-bottom: 120rpx;
}
}
.status-header {
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
... ... @@ -693,10 +682,10 @@ onUnmounted(() => {
font-weight: bold;
font-size: 28rpx;
}
}
}
.action-list {
padding: 20rpx;
.action-list {
padding: 0 0 20rpx;
.header {
display: flex;
... ... @@ -721,9 +710,9 @@ onUnmounted(() => {
font-size: 24rpx;
}
}
}
}
.empty-state {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
... ... @@ -740,12 +729,11 @@ onUnmounted(() => {
.empty-subtext {
font-size: 28rpx;
}
}
}
/* 👇 底部气泡提示样式 */
.tip-bubble {
.tip-bubble {
position: fixed;
bottom: 230rpx; // 刚好在底部导航上方
bottom: 230rpx;
left: 52%;
transform: translateX(-50%);
background-color: #000;
... ... @@ -755,7 +743,6 @@ onUnmounted(() => {
font-size: 28rpx;
z-index: 9999;
// 小三角
&::after {
content: '';
position: absolute;
... ... @@ -766,9 +753,9 @@ onUnmounted(() => {
border-right: 10rpx solid transparent;
border-top: 10rpx solid #000;
}
}
}
.footer-bar {
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
... ... @@ -801,16 +788,13 @@ onUnmounted(() => {
.add-btn-wrapper {
margin-top: -60rpx;
width: 120rpx !important;
/* 必须加 */
height: 120rpx !important;
/* 必须加 */
width: 120rpx;
height: 120rpx;
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 9999 !important;
/* 必须加 */
z-index: 9999;
}
.add-btn {
... ... @@ -823,14 +807,11 @@ onUnmounted(() => {
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
position: relative;
z-index: 99999 !important;
/* 必须加 */
z-index: 99999;
}
}
//
}
.time-pop-container {
.time-pop-container {
padding: 40rpx 30rpx;
color: #fff;
... ... @@ -946,11 +927,9 @@ onUnmounted(() => {
border-radius: 4rpx;
margin: 40rpx auto 0;
}
}
}
// 写总结弹窗样式
.summaryPopup {
.summaryPopup {
width: 100vw;
height: 75vh;
background-color: #2c2c2e;
... ... @@ -972,7 +951,7 @@ onUnmounted(() => {
.save-btn {
background-color: #f8d714;
color: #000000;
color: #000;
font-size: 28rpx;
font-weight: 500;
padding: 12rpx 30rpx;
... ... @@ -996,10 +975,9 @@ onUnmounted(() => {
color: #8e8e93;
}
}
}
}
// 训练命名弹窗样式(与总结弹窗风格完全统一)
.namePopup {
.namePopup {
width: 100%;
height: 75vh;
background-color: #2c2c2e;
... ... @@ -1022,7 +1000,7 @@ onUnmounted(() => {
.save-btn {
background-color: #f8d714;
color: #000000;
color: #000;
font-size: 28rpx;
font-weight: 500;
padding: 12rpx 30rpx;
... ... @@ -1031,18 +1009,94 @@ onUnmounted(() => {
}
.name-input {
width: calc(100% - 60rpx); // 关键:设置宽度,让它左右留空,不贴边
margin: 0 auto; // 关键:水平居中
width: calc(100% - 60rpx);
margin: 0 auto;
background-color: #1f1f1f;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
color: #fff;
min-height: 220rpx; // 可选:增加一点高度,和例图更像
min-height: 220rpx;
&::placeholder {
color: #8e8e93;
}
}
}
}
// ===================== 组间休息倒计时 =====================
.rest-countdown {
position: fixed;
bottom: 180rpx;
right: 30rpx;
z-index: 100;
animation: countdown-enter 0.3s ease-out;
}
@keyframes countdown-enter {
from {
opacity: 0;
transform: scale(0.5);
}
to {
opacity: 1;
transform: scale(1);
}
}
.countdown-circle {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 6rpx solid #4caf50;
display: flex;
align-items: center;
justify-content: center;
.ring-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ring-bg {
fill: none;
stroke: #2c2c2e;
stroke-width: 6;
}
.ring-progress {
fill: none;
stroke: #4caf50;
stroke-width: 6;
stroke-linecap: round;
transition: stroke-dashoffset 0.5s linear;
}
.countdown-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
}
.rest-time {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
font-family: monospace;
}
.rest-total {
font-size: 20rpx;
color: #8e8e93;
margin-top: 4rpx;
}
}
</style>
... ...