Authored by qxm

训计日历的间歇动作完善,训练图片修改,复制到接口,备注

<template>
<up-popup :show="show" mode="bottom" round="16" closeable @close="show = false" :safeAreaInsetBottom="false">
<up-popup :show="show" mode="bottom" round="16" :safeAreaInsetBottom="false">
<view class="desc-container">
<view class="title">动作备注</view>
<view class="input-box">
<up-textarea
v-model="tempNoteContent"
placeholder="此处填写个人备注"
autoHeight
border="none"
customStyle="background: #242424; padding: 20rpx; border-radius: 12rpx; color: #fff"
placeholderStyle="color: #999"
></up-textarea>
<view class="title">
<view @click="show = false">
<up-icon name="close" color="#fff" size="20"></up-icon>
</view>
<view class="footer">
<text class="titleName">
动作备注
</text>
<view class="btn" @click="saveNoteContent">保存</view>
</view>
<view class="input-box">
<up-textarea v-model="tempNoteContent" placeholder="此处填写个人备注" class="noteConten"
customStyle="border-radius:12rpx; padding:20rpx;background: #242424;" />
</view>
</view>
</up-popup>
</template>
<script setup>
import { ref } from 'vue';
import { ref } from 'vue';
const show = ref(false);
const tempNoteContent = ref(''); // 临时编辑的备注内容
const show = ref(false);
const tempNoteContent = ref(''); // 临时编辑的备注内容
const emit = defineEmits(['saveSuccess']);
// 保存备注
const saveNoteContent = async () => {
const emit = defineEmits(['saveSuccess']);
// 保存备注
const saveNoteContent = async () => {
const content = tempNoteContent.value.trim();
// 如果内容为空,直接返回,不提交
if (!content) {
... ... @@ -39,52 +36,51 @@
emit('saveSuccess', content);
// 关闭弹窗
show.value = false;
};
};
const open = () => {
const open = () => {
show.value = true;
};
};
defineExpose({ open });
defineExpose({ open });
</script>
<style lang="scss" scoped>
.desc-container {
.desc-container {
background-color: #1a1a1a;
padding: 40rpx 30rpx 40rpx;
height: 75vh;
.title {
color: #fff;
font-size: 32rpx;
display: flex;
justify-content: space-between;
font-weight: 500;
text-align: center;
color: #fff;
font-size: 32rpx;
margin-bottom: 40rpx;
}
.btn {
width: 100rpx;
height: 50rpx;
background-color: #fedc1f;
color: #000;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.input-box {
margin-bottom: 60rpx;
/* 穿透修改 u-textarea 内部文字颜色 */
// 输入文字白色
:deep(.u-textarea__field) {
color: #ccc !important;
}
color: #fff !important;
}
.footer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.btn {
width: 100%;
height: 80rpx;
background-color: #fedc1f;
color: #333;
border-radius: 12rpx;
text-align: center;
line-height: 80rpx;
}
}
}
}
</style>
... ...
... ... @@ -305,7 +305,8 @@ const handleConfirm = async () => {
exerciseId: detail.id,
exerciseName: detail.name,
exerciseType: detail.exerciseType,
urlImage: detail.urlImage || detail.url3dAnimation,
urlImage: detail.url3dAnimation || '',
// urlImage: detail.urlImage || detail.url3dAnimation,
categoryDescription: detail.categoryDescription,
equipmentDescription: detail.equipmentDescription,
}
... ... @@ -326,7 +327,8 @@ const handleConfirm = async () => {
exerciseId: e.id,
exerciseName: e.name,
exerciseType: e.exerciseType,
urlImage: e.urlImage || e.url3dAnimation,
urlImage: e.url3dAnimation || e.exerciseCover,
// exerciseCover
}))
};
}
... ...
... ... @@ -282,7 +282,6 @@ const onWeightConfirm = (e) => {
showWeightPicker.value = false
}
// 文字备注
// 打开备注弹窗
const openBeizhu = () => {
nextTick(() => {
... ... @@ -652,7 +651,8 @@ const addRow = () => {
recordList.value.push({
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '', weight: '', reps: '', isActive: false,
distance: '', weight: '', reps: '', duration: '',
restTime: '', isActive: false,
});
};
const deleteRow = (index) => {
... ...
... ... @@ -347,6 +347,12 @@ const handleNoteSave = async (content) => {
actionDetail.value.userNote = content;
} catch (e) {
console.log(e);
} finally {
if (type == 1) {
await loadexercisedetail(actionId.value)
} else {
await loadsuperdetail(actionId.value)
}
}
};
... ...
... ... @@ -55,7 +55,9 @@
</view>
<view class="plan-header-btns">
<button class="plan-btn more-btn" @click.stop="handlePlanMore">更多</button>
<button class="plan-btn copy-btn" @click.stop="openCalendarPopup(currentPlan?.templateId)">复制到</button>
<button class="plan-btn copy-btn" @click.stop="openCopyCalendarPopup(currentPlan?.templateId)">
复制到
</button>
<button class="plan-btn go-train-btn-small" @click.stop="handleTrainAgain">
<text class="go-train-text-sm">今日再练</text>
<!-- <text class="go-train-tag-sm">GO</text> -->
... ... @@ -93,10 +95,10 @@
<view class="set-left" :style="{ color: set.isCompleted === 1 ? '#333' : '#a2a2a2' }">
<!-- <text class="indexData">{{ setIdx + 1 }}</text> -->
<text class="set-content" v-if="unit.exercises[0].exerciseType === 0">
{{ set.weight }}kg × {{ set.reps }}次
{{ set.weight }}kg x {{ set.reps }}次
</text>
<text class="set-content" v-if="unit.exercises[0].exerciseType === 1">
{{ formatSecondsToHms(set.duration) }} × {{ set.distance }}km
{{ formatSecondsToHms(set.duration) }} x {{ set.distance }}km
</text>
<text class="set-content" v-if="unit.exercises[0].exerciseType === 2">
{{ set.reps }}次
... ... @@ -108,17 +110,19 @@
{{ set.weight }}kg x {{ set.duration }}s x {{ set.restTime }}
</text>
<text class="set-content" v-if="unit.exercises[0].exerciseType === 6">
{{ set.reps }}次 x {{ formatSecondsToHms(set.duration) }}
{{ set.reps }}组 x {{ set.duration }}秒 x {{ set.restTime || 0 }}秒/组休息
</text>
</view>
<view class="set-right">
<text class="rest-time" v-if="set.restTime && unit.exercises[0].exerciseType != 6">
{{ set.restTime || 0 }}s
</text>
</view>
<text class="check" v-if="set.isCompleted === 1">✓</text>
</view>
</view>
</view>
</view>
</view>
<!-- 右侧:修改按钮 + 完成对勾 -->
<view class="action-right" @click="openXiuGai(unit, unitIdx)">
... ... @@ -165,31 +169,32 @@
<view class="set-content"
:style="{ color: ex.sets[idx].isCompleted === 1 ? '#333' : '#a2a2a2' }">
<!-- 适配不同动作类型的显示文本 -->
<text v-if="ex.exerciseType === 0">
{{ ex.sets[idx].weight }}kg × {{ ex.sets[idx].reps }}次
<text v-if="ex.exerciseType === 0" class="detai-data">
{{ ex.sets[idx].weight }}kg x {{ ex.sets[idx].reps }}次
</text>
<text v-if="ex.exerciseType === 1">
{{ formatSecondsToHms(ex.sets[idx].duration) }} × {{
<text v-if="ex.exerciseType === 1" class="detai-data">
{{ formatSecondsToHms(ex.sets[idx].duration) }} x {{
ex.sets[idx].distance
}}km
</text>
<text v-if="ex.exerciseType === 2">
<text v-if="ex.exerciseType === 2" class="detai-data">
{{ ex.sets[idx].reps }}次
</text>
<text v-if="ex.exerciseType === 3">
<text v-if="ex.exerciseType === 3" class="detai-data">
{{ formatSecondsToHms(ex.sets[idx].duration) }}
</text>
<text v-if="[4, 5].includes(ex.exerciseType)">
{{ ex.sets[idx].weight }}kg × {{ ex.sets[idx].reps }}次
<text v-if="[4, 5].includes(ex.exerciseType)" class="detai-data">
{{ ex.sets[idx].weight }}kg x {{ ex.sets[idx].reps }}次
</text>
<text v-if="ex.exerciseType === 6">
{{ formatSecondsToHms(ex.sets[idx].duration) }}
<text v-if="ex.exerciseType === 6" class="detai-data">
{{ ex.sets[idx].reps }}次 x {{ ex.sets[idx].duration }}秒 x {{ ex.sets[idx].restTime
}}秒/组休息
</text>
</view>
<view class="rest-time" v-if="ex.sets[idx]?.restTime && ex.exerciseType !== 6"
:style="{ color: ex.sets[idx].isCompleted === 1 ? '#999 !important' : '#a2a2a2 !important' }">
{{ ex.sets[idx].restTime }}s
</view>
</view>
<text class="check" v-if="ex.sets[idx].isCompleted === 1">✓</text>
</template>
</view>
... ... @@ -225,7 +230,7 @@
</view> -->
</view>
<AddTrainPopup v-model:visible="showAddTrainPopup" />
<AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" />
<!-- 日期备注弹窗 -->
<RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId"
... ... @@ -268,12 +273,13 @@
<text class="item-text delete-text">删除整个计划</text>
</view>
</up-popup>
<!-- 复制到弹窗 -->
<!-- 复制到弹窗/移动到/添加到 -->
<AddToCalendarPopup ref="calendarPopupRef" :template-id="currentPlan?.templateId ?? 0"
@success="handleCalendarSuccess" :mask-click="true" @click.stop />
@success="handleCalendarSuccess" :mask-click="true" @click.stop
:daily-template-id="currentPlan?.dailyTemplateId" :is-copy="isCopyMode" />
<!-- 设置日历颜色弹窗 -->
<CalendarColorPicker v-model:visible="showColorPopup" :daily-template-id="selectedPlanId"
:current-color="currentPlan?.templateBackgroundColor || '#ffffff'" @success="loaddailytemplate" />
:current-color="currentPlan?.templateBackgroundColor || '#ffffff'" @success="calendarColorPickerSuccess" />
<MeiRiMoBanXiuGai ref="meirimobanRef" @saveSuccess="loaddailytemplate(selectedDate)" />
</view>
... ... @@ -311,6 +317,7 @@ const resdailyData = ref([]);
const currentPlanIndex = ref(0);
const isMoveMode = ref(false);
const isCopyMode = ref(false);
const showRiqibeizhu = ref(false)
const moreShow = ref(false)
const calendarPopupRef = ref(null);
... ... @@ -398,6 +405,11 @@ const openColorPopup = () => {
showColorPopup.value = true
}
const handleAddTrain = async () => {
await loaddailytemplate();
// 更新主页面
emit('refreshCalendar');
}
// 当前正在显示的训记
const currentPlan = computed(() => {
if (!resdailyData.value.length) return null;
... ... @@ -672,6 +684,7 @@ const handleTrainAgain = async () => {
if (res.code === 0) {
uni.showToast({ title: '今日再练成功', icon: 'success' });
loaddailytemplate(props.date);
emit('refreshCalendar')
} else {
uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
}
... ... @@ -753,15 +766,45 @@ const handleMoveTo = async () => {
});
};
// 复制到
const openCopyCalendarPopup = (templateId) => {
if (!templateId) return uni.showToast({ title: '模板ID不存在', icon: 'none' })
console.log('currentPlan?.dailyTemplateId=', currentPlan?.dailyTemplateId);
isCopyMode.value = true
isMoveMode.value = false // 清空移动标记,互斥
moreShow.value = false
nextTick(() => {
calendarPopupRef.value.open();
});
}
const handleCalendarSuccess = () => {
console.log('✅ 添加日历成功,当前模式:', isMoveMode.value ? '移动' : '复制');
if (!isMoveMode.value) {
// console.log('✅ 添加日历成功,当前模式:', isMoveMode.value ? '移动' : '复制');
// 复制模式:只提示成功,不用删原数据
// if (!isMoveMode.value) {
// uni.showToast({
// title: '复制成功',
// icon: 'success'
// });
// return;
// }
console.log('✅ 添加日历成功', {
move: isMoveMode.value,
copy: isCopyMode.value
});
// 复制模式:仅刷新日历,不删除原数据
if (isCopyMode.value) {
uni.showToast({
title: '复制成功',
icon: 'success'
});
// 重置标记
isCopyMode.value = false
emit('refreshCalendar')
return;
}
// 移动模式:子组件已把数据新增到新日期,这里删除原日期的训练记录
if (!currentPlan.value?.id) {
return;
}
... ... @@ -769,6 +812,7 @@ const handleCalendarSuccess = () => {
.then(() => {
uni.showToast({ title: '移动成功', icon: 'success' });
loaddailytemplate(selectedDate.value);
emit('refreshCalendar')
})
.catch(err => {
uni.showToast({ title: '移动失败', icon: 'none' });
... ... @@ -776,6 +820,11 @@ const handleCalendarSuccess = () => {
isMoveMode.value = false
};
const calendarColorPickerSuccess = () => {
loaddailytemplate(selectedDate.value)
emit('refreshCalendar')
}
// 去训练
const startTraining = () => {
// console.log('++点击了去训练++');
... ... @@ -1068,6 +1117,14 @@ onMounted(() => {
.set-content {
flex-grow: 1;
color: inherit;
display: flex;
justify-content: space-between;
white-space: nowrap;
}
.detai-data {
white-space: nowrap;
}
// 主卡片:横向排列
... ... @@ -1153,6 +1210,11 @@ onMounted(() => {
/* 控制 indexData、set-content、rest-time 之间的间距 */
}
.set-right {
display: flex;
gap: 20rpx;
}
// 灰色小圆点序号
.indexData {
width: 32rpx;
... ...
... ... @@ -372,18 +372,30 @@ const saveEdit = async () => {
if (unit.unitType === 1) {
// 单动作:exercises只有1个
const exItem = targetExercises[0]
const exerciseType = exItem.exerciseType;
console.log('----------exerciseType---------', exerciseType);
// expose出来的recordList
const newSets = dongzuoSingleRef.value.recordList.map((item, idx) => {
// 组件数据转回后端sets结构(和原页面save组装逻辑一致)
const duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0
const rest = Number(item.quickTimeDisplay.replace('s', '')) || 0
let duration = 0
let restTime = 0
// 间歇动作单独取值
if (exerciseType === 6) {
duration = Number(item.duration) || 0
restTime = Number(item.restTime) || 0
} else {
// 普通动作时分秒换算
duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0
restTime = Number(item.quickTimeDisplay.replace('s', '')) || 0
}
return {
setIndex: idx + 1,
weight: Number(item.weight) || 0,
reps: Number(item.reps) || 0,
distance: Number(item.distance) || 0,
duration,
restTime: rest,
// restTime: rest,
restTime,
isCompleted: item.isActive ? 1 : 0
}
})
... ... @@ -392,17 +404,30 @@ const saveEdit = async () => {
// 超级组:superRecordMap{exId:[]}
const superMap = dongzuoSingleRef.value.superRecordMap
targetExercises.forEach(ex => {
// 获得当前超级组的动作类型
const exerciseType = ex.exerciseType;
console.log('----------exerciseType---------', exerciseType);
const setArr = superMap[ex.exerciseId] || []
ex.sets = setArr.map((item, idx) => {
const duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0
const rest = Number(item.quickTimeDisplay.replace('s', '')) || 0
let duration = 0
let restTime = 0
// 子动作是间歇则取专属字段
if (exerciseType === 6) {
duration = Number(item.duration) || 0
restTime = Number(item.restTime) || 0
} else {
duration = Number(item.h) * 3600 + Number(item.m) * 60 + Number(item.s) || 0
restTime = Number(item.quickTimeDisplay.replace('s', '')) || 0
}
return {
setIndex: idx + 1,
weight: Number(item.weight) || 0,
reps: Number(item.reps) || 0,
distance: Number(item.distance) || 0,
duration,
restTime: rest,
// restTime: rest,
restTime,
isCompleted: item.isActive ? 1 : 0
}
})
... ... @@ -473,7 +498,7 @@ defineExpose({ open })
// padding: 0 20rpx 120rpx;
margin: 0 20rpx;
height: calc(100vh - 120rpx);
background: #f5f5f5;
background: #242424;
max-height: 70vh;
}
</style>
... ...
... ... @@ -43,6 +43,7 @@
<script setup>
import { ref, computed, watch } from 'vue';
import QueryPlanApi from '@/sheep/api/plan/queryplan';
import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
const props = defineProps({
planId: { type: [String, Number], required: false },
... ... @@ -50,9 +51,11 @@ const props = defineProps({
isMove: {
type: Boolean,
default: false
}
},
isCopy: { type: Boolean, default: false },
dailyTemplateId: { type: Number, default: 0 }
});
const emit = defineEmits(['success']);
const emit = defineEmits(['success', 'clearCopyFlag']);
// 弹窗显示控制
const showPopup = ref(false);
... ... @@ -174,6 +177,7 @@ const close = () => {
// 重置到当前月份(可选)
currentYear.value = new Date().getFullYear();
currentMonth.value = new Date().getMonth() + 1;
emit('clearCopyFlag')
};
// 打开弹窗(暴露给父组件)
... ... @@ -192,27 +196,56 @@ const confirmAdd = async () => {
return;
}
try {
if (props.isMove) {
// if (props.isMove) {
// const params = {
// id: props.templateId,
// trainingDate: selectedDates.value[0] || ''
// };
// // 添加到日历弹窗的参数
// console.log('移动到:params', params);
// await QueryPlanApi.updateDailyTemplateDate(params);
// uni.showToast({ title: '移动成功', icon: 'success' });
// } else {
// const params = {
// id: props.templateId,
// planId: props.planId,
// trainDateList: selectedDates.value
// };
// // 添加到日历弹窗的参数
// console.log('添加到params', params);
// await QueryPlanApi.addPlanToCalendar(params);
// uni.showToast({ title: '添加到日历成功', icon: 'success' });
// }
// ========== 新增复制分支 优先判断 ==========
console.log('rops.dailyTemplateId=', props.dailyTemplateId);
if (props.isCopy) {
// 调用你新增的复制接口
await dailytemplateApi.copyTempleteDailyTemplate(props.dailyTemplateId, selectedDates.value)
uni.showToast({ title: '复制成功', icon: 'success' });
console.log('调用复制接口成功');
} else if (props.isMove) {
// 移动到
const params = {
id: props.templateId,
trainingDate: selectedDates.value[0] || ''
};
// 添加到日历弹窗的参数
console.log('移动到:params', params);
await QueryPlanApi.updateDailyTemplateDate(params);
uni.showToast({ title: '移动成功', icon: 'success' });
console.log('移动接口成功');
} else {
//添加到日历
const params = {
id: props.templateId,
planId: props.planId,
trainDateList: selectedDates.value
};
// 添加到日历弹窗的参数
console.log('添加到params', params);
await QueryPlanApi.addPlanToCalendar(params);
uni.showToast({ title: '添加到日历成功', icon: 'success' });
console.log('添加到接口成功');
}
close();
emit('success');
} catch (err) {
... ...
... ... @@ -37,10 +37,10 @@
<scroll-view class="template-list" enable-flex scroll-y>
<!-- 用父容器包裹 v-if 分支,解决 key 冲突 -->
<view v-if="!isFiltering" class="sub-template-list">
<view v-for="(item, index) in templateList" :key="index" class="template-item">
<view v-for="(item, index) in templateList" :key="index" class="template-item" @click="gototemplate(item)">
<image :src="item.urlCover" mode="aspectFill" class="template-img"></image>
<view>
<view class="template-content" @click="gototemplate(item)">
<view class="template-content">
<view class="template-title">{{ item.name }}</view>
<view class="template-count">{{ item.templatesCount }}个模板</view>
<view class="template-desc">{{ item.description }}</view>
... ...
... ... @@ -10,13 +10,13 @@
<view class="title">动作库</view>
<view class="desc">真人视频讲解</view>
</view>
<view class="item">
<view class="item" @click="goToTrainPlan">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/22_1773626449583.png"
class="img"></image>
<view class="title">训练计划</view>
<view class="desc">周期性目标达成</view>
</view>
<view class="item">
<view class="item" @click="goToTrainTemplate">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/23_1773627239158.png"
class="img"></image>
<view class="title">训练模板</view>
... ... @@ -122,7 +122,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import QueryPlanApi from '@/sheep/api/plan/queryplan';
import coachUpdatesApi from '@/sheep/api/Coach/CoachUpdates'; // 请确认路径正确
import useUserStore from '@/sheep/store/user';
... ... @@ -312,12 +312,55 @@ const handleDynamicLike = async (item, index) => {
console.log('点赞失败', err);
}
};
// const goXunjiTab = (tabIndex) => {
// uni.navigateTo({
// url: `/pages4/pages/xunji/xunji-main?currentTab=${tabIndex}`
// })
// }
// 实现动作库跳转,跳转到F:\hongxing-app\hongxing-new\pages\xunji\components\xunji-dongzuo.vue
// const goToActionLibrary = () => {
// uni.navigateTo({
// url: '/pages/xunji/components/xunji-dongzuo',
// });
// };
const instance = getCurrentInstance()
// 获取父页面(pages/xunji/xunji.vue)
const parentPage = instance.parent
// 切换训记Tab内部子页面(适配底部tabBar)
const switchXunjiTab = (tabIndex) => {
console.log('准备跳转训记Tab,目标索引:', tabIndex);
// 存入本地缓存,训记页面onShow读取
uni.setStorageSync('xunjiTargetTab', tabIndex);
// 修正路径:去掉开头斜杠,匹配tabBar标准格式
uni.switchTab({
url: 'pages/xunji/xunji',
success: () => {
console.log('switchTab切换训记页面成功');
},
fail: (err) => {
console.error('switchTab切换失败', err);
uni.removeStorageSync('xunjiTargetTab');
uni.showToast({ title: '跳转失败,请检查tabBar配置', icon: 'none' });
}
})
}
const goToActionLibrary = () => {
uni.navigateTo({
url: '/pages/xunji/components/xunji-dongzuo',
});
};
parentPage?.exposed?.switchTabIndex(3)
}
// 训练计划
const goToTrainPlan = () => {
parentPage?.exposed?.switchTabIndex(1)
}
// 训练模板
const goToTrainTemplate = () => {
parentPage?.exposed?.switchTabIndex(4)
}
// 实现
// 初始化
onMounted(() => {
... ...
... ... @@ -85,7 +85,7 @@
<script setup>
import { ref, shallowRef, computed, onMounted } from 'vue';
import { onLoad, onHide } from '@dcloudio/uni-app';
import { onLoad, onHide, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/sheep/store/user';
import { getMenuButtonHeight, getTopSafeArea } from '@/utils/safeArea';
... ... @@ -136,6 +136,18 @@ onLoad((options) => {
}
});
onShow(() => {
const targetTab = uni.getStorageSync('xunjiTargetTab');
if (targetTab !== '' && targetTab != null) {
const tabNum = Number(targetTab);
currentTab.value = tabNum;
handleTabClick(tabNum);
// 使用后清空缓存,避免下次进来自动跳转
uni.removeStorageSync('xunjiTargetTab');
}
})
// --- 原有业务方法补充与修复 ---
// 打开抽屉
... ... @@ -186,6 +198,10 @@ const goWidgetLib = () => {
const goFeedback = () => { };
const goUserGroup = () => { };
const goTutorial = () => { };
defineExpose({
switchTabIndex: handleTabClick
})
</script>
<style scoped lang="scss">
.home-page {
... ...
... ... @@ -19,7 +19,7 @@
{{ TemplateDetail?.equipmentsUsed }}
</template>
<template v-else>
{{ TemplateDetail?.equipmentNames.join(',') }}
{{ (TemplateDetail?.equipmentNames || []).join(',') }}
</template>
</text>
... ... @@ -35,14 +35,14 @@
<!-- 主要训练部位 -->
<text class="section-title">主要训练部位: </text>
<text class="description">
{{ TemplateDetail.primaryMuscleNames?.join(', ') || '暂无数据' }}
{{ (TemplateDetail.primaryMuscleNames || []).join(', ') || '暂无数据' }}
</text>
<!-- 换行 -->
<view class="divider"></view>
<!-- 次要训练部位 -->
<text class="section-title">次要训练部位: </text>
<text class="description">
{{ TemplateDetail.secondaryMuscleNames?.join(', ') || '暂无数据' }}
{{ (TemplateDetail.secondaryMuscleNames || []).join(', ') || '暂无数据' }}
</text>
</view>
</view>
... ... @@ -89,43 +89,6 @@
</view>
<!-- 动作内容循环:只循环内容,不循环卡片 -->
<!-- <view v-for="(item, index) in unit.exercises" :key="index" class="exercise-item-inner">
<view class="exercise-header">
<view class="exercise-info">
<template v-if="unit.unitType === 2">
<text class="super-exercise-name">{{ item.exerciseName }}</text>
</template>
</view>
</view>
<view v-if="item.sets && item.sets.length > 0" class="exercise-details">
<view v-for="(detail, i) in item.sets" :key="i" class="detail-row">
<view class="detail-label">{{ i + 1 }}</view>
<view class="sets-data">
<view class="detail-value" v-if="[0, 4, 5].includes(item.exerciseType)">
{{ detail.weight }}kg × {{ detail.reps }}次
</view>
<view class="detail-value" v-if="item.exerciseType === 1">
{{ detail.distance }}km x {{ formatSeconds(detail.duration) }}
</view>
<view class="detail-value" v-if="item.exerciseType === 2">
{{ detail.reps }}次
</view>
<view class="detail-value" v-if="item.exerciseType === 3">
{{ detail.duration }}次
</view>
<view class="detail-value" v-if="item.exerciseType === 6">
{{ detail.duration }}组 x {{ formatSeconds(detail.duration) }}
</view>
<view class="rest" v-if="detail.restTime">
{{ detail.restTime }}s
</view>
</view>
</view>
</view>
</view> -->
<!-- 情况1:普通动作 unitType = 1 -->
<view v-if="unit.unitType === 1 && unit.exercises && unit.exercises.length" class="normal-action-wrap">
<!-- 取第一个动作(普通动作unit里永远只有1个exercise) -->
... ... @@ -238,7 +201,7 @@
<uni-icons type="refresh" size="22" color="#fff"></uni-icons>
<text class="item-text">删除此次排课</text>
</view>
<view class="more-item" @click.stop="confirmDelete(plandetail.groupId)">
<view class="more-item" @click.stop="confirmDelete(TemplateDetail.planId)">
<uni-icons type="close" size="22" color="#fff"></uni-icons>
<text class="item-text">结束整个计划</text>
</view>
... ... @@ -356,7 +319,7 @@ const loadtemplatedetail = async () => {
}
TemplateDetail.value = res.data;
TemplateUnits.value = res.data.units || [];
console.log('✅ 模板详情:', TemplateDetail.value);
console.log('✅ TemplateDetail.value模板详情:', TemplateDetail.value);
console.log('✅ 模板unts详情:', TemplateUnits.value);
} catch (err) {
... ... @@ -486,6 +449,8 @@ const handleDeleteTemplate = async (dailyTemplateId) => {
await dailytemplateApi.deleteDailyTemplate(dailyTemplateId);
uni.showToast({ title: '删除成功', icon: 'success' });
// moreShow.value = false; // 关闭弹窗
// 返回上一页
setTimeout(() => uni.navigateBack(), 800)
} catch (err) {
console.error('删除失败', err);
uni.showToast({ title: '删除失败,请重试', icon: 'none' });
... ... @@ -493,7 +458,7 @@ const handleDeleteTemplate = async (dailyTemplateId) => {
}
});
};
// 删除整个个人计划
const confirmDelete = async (id) => {
try {
console.log('打印计划个人id:', id);
... ... @@ -503,7 +468,7 @@ const confirmDelete = async (id) => {
if (res.code === 0 && res.data) {
// 2. 删除成功,从列表移除
planList.value = planList.value.filter(item => item.id !== id);
// planList.value = planList.value.filter(item => item.id !== id);
uni.showToast({ title: '计划已结束', icon: 'success' });
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
... ... @@ -512,8 +477,25 @@ const confirmDelete = async (id) => {
console.error('删除计划失败:', err);
uni.showToast({ title: '网络请求失败', icon: 'none' });
} finally {
// 3. 关闭弹窗
activePlanId.value = null;
{
setTimeout(() => {
// 获取页面栈
const pages = getCurrentPages();
// 这里返回上上个(我的计划列表页面)
const backNum = pages.length - 2;
if (backNum > 0) {
// 回退对应层数
uni.navigateBack({
delta: backNum
});
} else {
// 兜底:页面栈不足时防止报错,直接返回我的计划列表页面
uni.navigateTo({
url: '/pages4/pages/xunji/xunji-wode-jihua'
})
}
}, 1500)
}
}
};
... ...
... ... @@ -6,7 +6,7 @@
<view class="banner-section">
<image :src="plandetail.urlCover" class="banner-img">
<!-- 收藏 -->
<uni-icons :type="isPlanAdded ? 'heart-filled' : 'heart'" size="24" color="#fff" class="collect-btn"
<uni-icons :type="isFavorite ? 'heart-filled' : 'heart'" size="24" color="#fff" class="collect-btn"
@click="toggleAddPlan">
</uni-icons>
<!-- 分享按钮 -->
... ... @@ -126,8 +126,9 @@
<!-- 按钮区域 -->
<view class="button-group">
<template v-if="!plandetail.isAdded && !isMyPlan">
<button type="default" @click="showPlanPopup = true" class="btn" size="large"
<template v-if="!isPlanAdded && !isMyPlan">
<!-- 官方计划的底部按钮(未添加) -->
<button type="default" @click="openTrainingPopup" class="btn" size="large"
style="background-color: white; flex: 1">
单节课程训练
</button>
... ... @@ -136,18 +137,20 @@
加入计划
</button>
</template>
<template v-else-if="plandetail.isAdded && !isMyPlan">
<!-- 官方计划已添加 -->
<template v-else-if="isPlanAdded && !isMyPlan">
<button type="default" @click.stop="confirmDelete(plandetail.id)" class="btn end" size="large"
style="background-color: white; flex: 1">
结束计划
</button>
<button type="default" @click="showPlanPopup = true" class="btn ones" size="large"
<button type="default" @click="openTrainingPopup" class="btn ones" size="large"
style="background-color: #ffd700; flex: 1.5">
马上开练
</button>
</template>
<!-- 个人计划 -->
<template v-else>
<button type="default" class="btn end" @click="showPlanPopup = true" size="large"
<button type="default" class="btn end" @click="openTrainingPopup" size="large"
style="background-color: white; flex: 1">
马上开练
</button>
... ... @@ -176,18 +179,18 @@
</view>
</template>
</view>
<!-- 加入计划的弹窗 -->
<!-- 加入计划/重新开始计划的弹窗 -->
<WeekSelectPopup v-model:visible="showWeekPopup" :max-count="plandetail.frequencyPerWeek" :plan-detail="plandetail"
@success="loadDetail" :is-update="isUpdate" />
@success="loadDetailUpdate" :is-update="isUpdate" />
<!-- 添加到日历弹窗子组件 -->
<AddToCalendarPopup ref="calendarPopupRef" :plan-id="plandetail.id" :template-id="currentTemplateId"
@success="handleCalendarSuccess" />
<!-- 去训练弹窗,显示当前计划的所有模板,可以直接跳转到训练页面 -->
<!-- 去训练弹窗,显示当前计划的所有模板,可以直接跳转到训练页面,区分个人计划和官方计划(传递的是官方的计划id) -->
<template v-if="plandetail?.id">
<WodeJihuaTianjiaTancuang v-if="showPlanPopup" v-model:visible="showPlanPopup" :isAdd="false"
:plan-title="'选择课程立即开始训练'" :plan-id="plandetail.id" :is-my-plan="false"/>
:plan-title="'选择课程立即开始训练'" :plan-id="trainingPlanId" :is-my-plan="false" />
</template>
</view>
... ... @@ -211,8 +214,7 @@ const route = defineProps(['planid']); // Vue 3 + uni-app 支持
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const planIdFromUrl = currentPage.options?.planid;
// 计划是否已添加(收藏)
const isPlanAdded = ref(false);
// const plandetail = ref([]);
const plandetail = ref({});
//传参
... ... @@ -248,23 +250,6 @@ const showPlanPopup = ref(false)
// 更多弹窗显隐
const showMorePopup = ref(false)
// 接口weeklySchedule转日历渲染数据
// const formatWeekData = (weekArr) => {
// const weekMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六', 7: '日' }
// return weekArr.map((item, index) => {
// const dayNum = item.date[2]
// let topStr = index === 0 ? '今' : weekMap[item.dayIndex]
// const trainStr = item.scheduled ? item.templateName : ''
// return {
// topText: topStr,
// num: String(dayNum),
// trainName: trainStr,
// templateId: item.templateId,
// dailytemplateId: item.dailytemplateId,
// }
// })
// }
// 接口weeklySchedule转日历渲染数据(修复版:星期根据真实日期计算,100%对齐num)
const formatWeekData = (weekArr) => {
// JS星期:0=日,1=一,2=二,3=三,4=四,5=五,6=六
... ... @@ -304,6 +289,17 @@ const restartPlan = () => {
console.log('重新开始计划,planId:', plandetail.value.id)
uni.showToast({ title: '已重新开始计划', icon: 'success' })
}
const trainingPlanId = ref(0)
const openTrainingPopup = () => {
showPlanPopup.value = true;
if (isMyPlan.value) {
trainingPlanId.value = plandetail.value.planId
} else {
trainingPlanId.value = plandetail.value.id
}
console.log('trainingPlanId=', trainingPlanId.value);
}
// 跳转到排课页面
const openArrageClass = () => {
... ... @@ -365,10 +361,11 @@ const confirmDelete = async (id) => {
if (res.code === 0 && res.data) {
uni.showToast({ title: '计划已结束', icon: 'success' });
if (isMyPlan.value) {
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
}
... ... @@ -376,8 +373,11 @@ const confirmDelete = async (id) => {
console.error('删除计划失败:', err);
uni.showToast({ title: '网络请求失败', icon: 'none' });
} finally {
if (!isMyPlan.value) {
loadDetail();
}
}
}
// 用户点取消 → 什么都不做
else if (res.cancel) {
... ... @@ -433,37 +433,55 @@ const loadDetail = async () => {
}
};
// ====================== 计划收藏/添加功能 ======================
const loadDetailUpdate = () => {
loadDetail();
isAddToMyPlanList();
}
// 检查当前计划是否已经添加
const checkPlanAddedStatus = async () => {
// // 检查当前计划是否已经添加到我的计划列表
// 计划是否已添加
const isPlanAdded = ref(false);
const isAddToMyPlanList = async () => {
if (!id.value) return;
try {
const res = await QueryPlanApi.isAddPlan(id.value);
// 接口返回 true = 已添加
const res = await QueryPlanApi.isAddPlan(Number(id.value));
// 后端返回布尔值,赋值状态
isPlanAdded.value = res.data === true;
console.log("计划是否已添加:", isPlanAdded.value);
console.log("当前计划是否已加入我的计划:", isPlanAdded.value);
} catch (err) {
console.error("检查计划添加状态失败:", err);
console.error("查询是否加入计划失败:", err);
isPlanAdded.value = false;
}
};
const isFavorite = ref(false)
// 检查当前计划是否已经收藏
const checkPlanAddedStatus = async () => {
if (!id.value) return;
try {
const res = await QueryPlanApi.checkFavorite(id.value);
console.log('计划是否已经收藏:', res);
isFavorite.value = res.data === true;
console.log("计划是否已经收藏:", isFavorite.value);
} catch (err) {
console.error("检查计划收藏状态失败:", err);
isFavorite.value = false;
}
};
// 切换添加/取消收藏计划
const toggleAddPlan = async () => {
if (!id.value) {
uni.showToast({ title: "计划ID不存在", icon: "none" });
return;
}
const originalStatus = isPlanAdded.value;
// 乐观更新UI(点了立刻变爱心)
isPlanAdded.value = !originalStatus;
const originalStatus = isFavorite.value;
// 点了立刻变爱心)
isFavorite.value = !originalStatus;
try {
// 调用添加计划接口
await QueryPlanApi.addPlan(id.value);
const params = isFavorite.value ? 1 : 0;
await QueryPlanApi.toggleFavorite(Number(id.value), params);
if (!originalStatus) {
uni.showToast({ title: "收藏计划成功" });
} else {
... ... @@ -471,7 +489,7 @@ const toggleAddPlan = async () => {
}
} catch (err) {
// 失败回滚
isPlanAdded.value = originalStatus;
isFavorite.value = originalStatus;
console.error("操作失败:", err);
uni.showToast({ title: "操作失败", icon: "none" });
}
... ... @@ -491,6 +509,7 @@ const openCalendarPopup = (planId, templateId) => {
}
};
// 日历添加成功的回调(可选,可做刷新/提示)
const handleCalendarSuccess = () => {
console.log('课程已成功添加到日历');
... ... @@ -512,7 +531,8 @@ onLoad((options) => {
console.log("计划详情接收的参数:", options);
id.value = options.planid;
isMyPlan.value = options.isMyPlan === "true";
checkPlanAddedStatus(); // 检查计划是否已添加
checkPlanAddedStatus(); // 检查计划是否已收藏
isAddToMyPlanList(); // 新增:查询是否加入我的计划
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: true,
... ...
... ... @@ -69,6 +69,17 @@ const dailytemplateApi = {
data,
});
},
// 复制每日模板
copyTempleteDailyTemplate: (sourceTemplateId,targetDates) => {
return request({
url: `/app/daily/template/copy`,
method: 'POST',
data:{
sourceTemplateId,
targetDates
}
});
},
};
export default dailytemplateApi;
... ...
... ... @@ -162,7 +162,7 @@ const QueryPlanApi = {
});
},
// 移动到
// 移动到/复制到
updateDailyTemplateDate: (data) => {
return request({
url: '/app/plan/updateDailyTemplateDate',
... ...
... ... @@ -227,7 +227,8 @@ export const useTrainingStore = defineStore('training', {
return {
id: act.exerciseId,
name: act.exerciseName,
urlImage: act.exerciseCover || act.url3dAnimation || act.urlImage,
// urlImage: act.exerciseCover || act.url3dAnimation || act.urlImage,
urlImage: act.url3dAnimation || act.exerciseCover || act.urlImage,
// urlImage
exerciseType: act.exerciseType,
categoryDescription: act.categoryDescription,
... ...