Authored by qxm

h5样式修改,每日模板修改和最小化可运行,动作详情的收藏功能,备注弹窗修复,还有动图彻底修复,动作排序弹窗。

... ... @@ -4,7 +4,7 @@ SHOPRO_VERSION=v2.4.1
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
# SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
# SHOPRO_BASE_URL=http://mall.hcxtec.com
SHOPRO_BASE_URL=https://xunji.geaktec.com
# SHOPRO_BASE_URL=https://xunji.geaktec.com
# 后端接口 - 测试环境(通过 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
... ...
# 2026-06-23 工作日志
## 悬浮球快照方案 — 完整改造方案
分析了 exercising 健身小程序中 trainingStore(Pinia)被三个功能模块(动作训练、超级组、每日模板编辑/修改)共享导致的数据冲突问题。
### 问题根因
- `grid-cell-content-popup.vue` 的编辑按钮 (`templateEdit`) 调用 `loadDailyTemplateForEdit()` 覆盖 store
- `wode-xinjian-moban.vue` 的 `goBack()` / `handleSave()` 调用 `clearTrainingStore()` 清空 store
- `meiri-moban-xiugai.vue` 的修改弹窗(弹窗模式)已通过快照解决,但编辑和新建是页面跳转模式,快照来不及归还
### 最终方案:悬浮球持有快照
核心思路:最小化时将 trainingStore 全量深拷贝到共享 reactive 模块 `trainingSnapshot`(存在 JS 堆内存,非 Pinia),悬浮球由此独立控制显隐和回显。
改动清单:
1. **新建** `sheep/store/trainingSnapshot.js` — 共享 reactive 快照模块
2. **重写** `pages/TrainingFloating.vue` — 自有 showBall + 回显逻辑 + 计时器补偿
3. **修改** `pages4/.../xunji-dongzuo-lianxi.vue` openMin() — 保存快照到共享模块
4. **删除** `sheep/store/trainingStore.js` clearTrainingStore() 中 `this.min = false`
5. **不改** grid-cell-content-popup.vue 和 wode-xinjian-moban.vue
6. **修复** meiri-moban-xiugai.vue 中 Object.assign → $patch
总计约 113 行,4 文件改动,2 文件不动。
... ...
... ... @@ -3,7 +3,7 @@
<up-tabbar
:value="activePath"
@change="handleTabChange"
:placeholder="true"
:placeholder="false"
activeColor="#000"
:fixed="true"
>
... ...
{
"name": "智能健身",
"appid": "__UNI__8F24C84",
"appid": "__UNI__7CAA18B",
"description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
"versionName": "2025.10",
"versionCode": "183",
... ... @@ -183,7 +183,7 @@
"versionCode": 100
},
"mp-weixin": {
"appid": "wxb827c923ce0aad4b",
"appid": "wxa54e6a259fb3338d",
"setting": {
"urlCheck": true,
"minified": true,
... ...
... ... @@ -24,7 +24,7 @@ const goToTrainingPage = () => {
// 悬浮球消失
trainingStore.min = false;
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}`,
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}&isTraining=true`,
});
};
... ...
... ... @@ -148,7 +148,7 @@ const FUNCTION_CONFIG_LIST = [
},
{
text: '关于鸿星',
text: '关于我们',
url: '/pages5/pages/user/wode-guanyu-hongxing',
icon: '/static/icons/antOutline-exclamation-circle 1@1x.png',
},
... ... @@ -218,11 +218,14 @@ const fetchPageData = async () => {
// UserApi.getUserInfo(),
// // MemberApi.getMemberLevel(),
// ]);
const userRes = UserApi.getUserInfo();
const userRes = await UserApi.getUserInfo();
console.log('后端返回个人信息:', userRes.data);
const rawUser = userRes.data || {};
userInfo.value = rawUser;
// 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化
// const levels = levelRes.data?.detailList || [];
// if (rawUser.level !== undefined) {
... ...
<template>
<up-popup :show="show" mode="bottom" round="16" :safeAreaInsetBottom="false">
<view class="desc-container">
<view class="title">
<view @click="show = false">
<up-icon name="close" color="#fff" size="20"></up-icon>
<!-- 全屏遮罩层 -->
<view v-if="show" class="mask" @click="maskClose">
<!-- 底部弹窗主体 -->
<view class="popup-wrap" @click.stop>
<view class="desc-container">
<view class="title">
<view @click="show = false">
<up-icon name="close" color="#fff" size="20"></up-icon>
</view>
<text class="titleName">动作备注</text>
<view class="btn" @click="saveNoteContent">保存</view>
</view>
<view class="area-box">
<!-- <up-textarea v-model="tempNoteContent" placeholder="此处填写个人备注" class="noteConten"
customStyle="border-radius:12rpx; padding:20rpx;background: #242424;" /> -->
<textarea class="textarea" v-model="tempNoteContent" placeholder="此处填写个人备注" />
</view>
<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>
</view>
</template>
<script setup>
... ... @@ -25,35 +28,83 @@ const show = ref(false);
const tempNoteContent = ref(''); // 临时编辑的备注内容
const emit = defineEmits(['saveSuccess']);
const props = defineProps({
oldNote: {
type: String,
default: ''
}
})
// 保存备注
const saveNoteContent = async () => {
const content = tempNoteContent.value.trim();
// 如果内容为空,直接返回,不提交
if (!content) {
uni.showToast({ title: '备注不能为空', icon: 'none' });
return;
}
emit('saveSuccess', content);
// 关闭弹窗
show.value = false;
};
// 打开弹窗
const open = () => {
tempNoteContent.value = props.oldNote
show.value = true;
};
// 遮罩点击关闭
const maskClose = () => {
show.value = false;
};
defineExpose({ open });
</script>
<style lang="scss" scoped>
// 全屏灰色遮罩
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
// 底部弹窗容器,控制弹出动画、圆角、背景
.popup-wrap {
width: 100%;
border-radius: 16rpx 16rpx 0 0; // 对应原来 round="16"
overflow: hidden;
// 弹出动画可选,不需要可删除
animation: popUp 0.24s ease-out;
}
@keyframes popUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.desc-container {
background-color: #1a1a1a;
background-color: #1a1a1a; // 这里直接改弹窗背景色,非常方便
padding: 40rpx 30rpx 40rpx;
height: 75vh;
.title {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
text-align: center;
color: #fff;
... ... @@ -61,6 +112,24 @@ defineExpose({ open });
margin-bottom: 40rpx;
}
.titleName {
flex: 1;
text-align: center;
}
.area-box {
.textarea {
width: 95%;
height: 300rpx;
background: #444;
color: #fff;
border-radius: 16rpx;
padding: 20rpx;
font-size: 30rpx;
border: 0;
}
}
.btn {
width: 100rpx;
height: 50rpx;
... ... @@ -72,15 +141,5 @@ defineExpose({ open });
justify-content: center;
font-size: 28rpx;
}
.input-box {
margin-bottom: 60rpx;
// 输入文字白色
:deep(.u-textarea__field) {
color: #fff !important;
}
}
}
</style>
</style>
\ No newline at end of file
... ...
... ... @@ -20,13 +20,13 @@
<view v-for="(item, index) in sortList" :key="item.unitId || index" class="drag-item-card">
<view class="item-left">
<up-icon name="trash" color="#fff" size="22" @click="deleteUnit(item, index)"></up-icon>
<image class="item-img" :src="item.exercises?.[0]?.urlImage || ''" v-if="item.unitType === 1"
mode="aspectFill" />
<image class="item-img" :src="item.exercises?.[0]?.url3dAnimation || item.exercises?.[0]?.exerciseCover ||
item.exercises?.[0]?.urlImage" v-if="item.unitType === 1" mode="aspectFill" />
<image class="item-img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png" v-else
mode="aspectFill" />
<view class="item-info">
<view class="item-name">{{ item.unitName || '未命名动作' }}</view>
<view class="item-name">{{ item.unitName || item.exercises?.[0]?.exerciseName || '未命名动作' }}</view>
<view class="item-tags" v-if="item.unitType === 2">超级组</view>
<view class="item-tags" v-if="item.unitType === 1">
{{ item.exercises?.[0]?.categoryDescription || '' }}
... ... @@ -56,6 +56,7 @@ const trainingStore = useTrainingStore()
// --- 动作排序 ---弹窗
const actionSortShow = ref(false);
const sortList = ref([])
// 这里是打开动作排序的方法
const openActionSort = () => {
sortList.value = trainingStore.actionDetail?.units?.map((unit, index) => ({
... ... @@ -63,8 +64,12 @@ const openActionSort = () => {
exercises: unit.exercises || []
})) || [];
actionSortShow.value = true;
console.log('打印排序列表:', sortList.value);
};
// const addActionsPopup = () => {
// if (actionSortShow.value === true) {
// actionSortShow.value = false
... ...
... ... @@ -515,34 +515,6 @@ const toggleSuperSetActive = (setIndex) => {
}
})
}
// 增加超级组的set组数
const addSuperSet = () => {
const exercises = props.actionDetail?.exercises || []
exercises.forEach(sub => {
if (superRecordMap.value[sub.id]) {
superRecordMap.value[sub.id].push({
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '',
weight: '',
reps: '',
duration: '',
restTime: '',
isActive: false
})
}
})
}
// 删除超级组的组数
const deleteSuperSet = (setIndex) => {
const exercises = props.actionDetail?.exercises || []
exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list && list.length > 1) {
list.splice(setIndex, 1)
}
})
}
// 合并后的 Picker 逻辑
const timeColumns = reactive([
... ... @@ -652,12 +624,63 @@ const addRow = () => {
restTime: '', isActive: false,
});
};
// 删除单动作行数,至少保留一行,提示。
const deleteRow = (index) => {
if (recordList.value.length > 1) {
recordList.value.splice(index, 1);
} else {
uni.showToast({
title: '至少保留一行数据',
icon: 'none'
})
}
};
// 增加超级组的set组数
const addSuperSet = () => {
const exercises = props.actionDetail?.exercises || []
exercises.forEach(sub => {
if (superRecordMap.value[sub.id]) {
superRecordMap.value[sub.id].push({
h: '00', m: '00', s: '00',
quickTimeDisplay: '60s',
distance: '',
weight: '',
reps: '',
duration: '',
restTime: '',
isActive: false
})
}
})
}
// 删除超级组的组数
const deleteSuperSet = (setIndex) => {
const exercises = props.actionDetail?.exercises || []
// 没有子动作直接返回
if (!exercises.length) return
// 取第一个子动作的组数,所有子动作数组长度保持同步
const firstSubId = exercises[0].id
const list = superRecordMap.value[firstSubId] || []
// 判断只剩一组,弹窗提示并终止删除
if (list.length <= 1) {
uni.showToast({
title: '至少保留一组数据',
icon: 'none'
})
return
}
exercises.forEach(sub => {
const list = superRecordMap.value[sub.id]
if (list && list.length > 1) {
list.splice(setIndex, 1)
}
})
}
const addGroup = () => {
if (props.type === 1) {
addRow()
... ...
... ... @@ -52,12 +52,20 @@
<view class="title-bar">
<text class="title">{{ actionDetail?.name }}</text>
<view class="action-icons">
<!-- <up-icon name="share-square" color="#fff" size="24"></up-icon> -->
<!-- 只有个人创建的动作,才有这个编辑按钮-->
<template v-if="actionDetail?.isSystem === 0">
<view class="edit-icon" @click="edit">
<uni-icons type="more" size="24" color="#fff"></uni-icons>
</view>
</template>
<button class="share-btn" open-type="share">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</button>
<up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24"
@click="toggleCollect"></up-icon>
@click="toggleCollect">
</up-icon>
<!-- <FavoriteBtn :id="actionId" :type="type" /> -->
</view>
</view>
... ... @@ -91,11 +99,35 @@
</view>
<view class="memo-box" @click="openBeizhu">
<view class="section-title">训练备注</view>
<up-textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注" autoHeight
customStyle="background: transparent; border: none; padding: 10rpx 0;" placeholderStyle="color: #666"
disabled border="none"></up-textarea>
<view class="memo-title">
<view class="section-title">训练备注</view>
<view class="pencil-icon">
<up-icon name="edit-pen" color="#fedc1f" size="20"></up-icon>
</view>
</view>
<template v-if="type == 1">
<textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea>
</template>
<template v-if="type == 2">
<textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea>
</template>
</view>
<view class="memo-box" v-if="type == 1">
<view class="memo-title">
<view class="section-title">步骤</view>
<view class="pencil-icon">
<up-icon name="edit-pen" color="#fedc1f" size="20"></up-icon>
</view>
</view>
<view class="steps-list">
<rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>
</view>
</view>
<!-- 动作列表,只有超级组才有 -->
<view class="section" v-if="type === 2">
<view class="section-title">动作列表</view>
... ... @@ -119,12 +151,7 @@
</view>
</view>
<view class="section" v-if="type == 1">
<view class="section-title">步骤</view>
<view class="steps-list">
<rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>
</view>
</view>
<view class="section">
<view class="section-title">训练部位</view>
... ... @@ -206,7 +233,7 @@
</view>
</scroll-view>
<!-- 备注弹窗组件 -->
<beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" />
<beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" :old-note="actionDetail.note" />
</up-popup>
</template>
... ... @@ -217,6 +244,10 @@ import beizhu from '@/pages/xunji/components/beizhu.vue';
import SupersetsApi from '@/sheep/api/motion/supersets';
import TrainingApi from '@/sheep/api/Training/traininghistory';
import { onShareAppMessage } from '@dcloudio/uni-app';
import { useTrainingStore } from '@/sheep/store/trainingStore'
const trainingStore = useTrainingStore()
// 静态配置
... ... @@ -266,23 +297,32 @@ const checkExerciseFavorited = async () => {
try {
const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
isFavorite.value = res.data;
console.log('获得动作的收藏状态', res.data);
} catch (err) {
console.log(err);
}
};
// 收藏
const toggleCollect = async () => {
try {
const status = isFavorite.value ? 0 : 1;
if (type == 1) {
console.log('actionId.value=', actionId.value, 'status=', status);
if (type.value == 1) {
console.log('动作收藏传递的参数', actionId.value, status);
await ExercisesApi.toggleFavorite(actionId.value, status);
} else {
console.log('超级组收藏传递的参数', actionId.value, status);
await SupersetsApi.toggleFavorite(actionId.value, status);
}
isFavorite.value = !isFavorite.value;
uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' });
} catch (err) {
console.log(err);
// 请求失败强制回滚UI
// isFavorite.value = originState;
uni.showToast({ title: '操作失败,请重试', icon: 'none' });
}
};
... ... @@ -297,8 +337,21 @@ const checkSupersetFavorited = async () => {
};
const startTraining = () => {
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
return;
}
console.log('动作详情页面的trainingStore.isTraining', trainingStore.isTraining);
trainingStore.isTraining = true;
console.log('动作详情页面的trainingStore.isTraining', trainingStore.isTraining);
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}`,
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}&isTraining=true`,
});
};
... ... @@ -325,9 +378,13 @@ const open = async (id, typeData) => {
if (typeData == 1) {
await loadexercisedetail(actionId.value);
// await loadAlternativeActions(actionId.value);
console.log('进入动作收藏检查接口');
await checkExerciseFavorited();
} else {
await loadsuperdetail(actionId.value);
console.log('进入超级组收藏检查接口');
await checkSupersetFavorited();
}
... ... @@ -351,6 +408,10 @@ const openBeizhu = () => {
// 接收子组件传过来的备注内容
const handleNoteSave = async (content) => {
console.log('备注内容:', content);
console.log('type.value=', type.value);
try {
if (type.value == 2) {
await SupersetsApi.addNotes({
... ... @@ -367,7 +428,7 @@ const handleNoteSave = async (content) => {
} catch (e) {
console.log(e);
} finally {
if (type == 1) {
if (type.value == 1) {
await loadexercisedetail(actionId.value)
} else {
await loadsuperdetail(actionId.value)
... ... @@ -398,6 +459,7 @@ onShareAppMessage((res) => {
const loadexercisedetail = async (id) => {
const response = await ExercisesApi.getExerciseById(id);
actionDetail.value = response.data;
console.log('显示动作详情:', actionDetail.value);
if (actionDetail.value.url3dAnimation) {
modeTab.value = 0;
} else if (actionDetail.value.urlRealPerson) {
... ... @@ -502,6 +564,12 @@ const formatTime = (seconds) => {
return hh + mm + ss;
};
const edit = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}`
});
};
// 点击超级组内部的动作 → 打开动作详情(复用同一个组件)
const openActionItem = (item) => {
... ... @@ -659,6 +727,10 @@ onMounted(() => { });
color: #ddd;
}
.pencil-icon {
margin-bottom: 24rpx;
}
.action-list {
.action-item {
display: flex;
... ... @@ -730,17 +802,23 @@ onMounted(() => { });
}
.memo-box {
background: #262626;
background: #464646;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 40rpx;
.textarea {
height: 50rpx;
.memo-title {
display: flex;
}
:deep(.u-textarea--disabled) {
background-color: #262626;
}
.textarea {
width: 95%;
color: #fff;
height: 160rpx;
border-radius: 16rpx;
// padding: 20rpx;
font-size: 30rpx;
border: 0;
}
}
... ... @@ -748,13 +826,13 @@ onMounted(() => { });
display: flex;
flex-direction: column;
gap: 16rpx;
background: #262626;
padding: 20rpx;
background: #464646;
// padding: 20rpx;
box-sizing: border-box;
border-radius: 16rpx;
.step-text {
font-size: 26rpx;
font-size: 30rpx;
line-height: 1.6;
color: #bbb;
list-style: none;
... ...
... ... @@ -5,7 +5,8 @@
<view class="popup-options">
<view class="popup-option" @click="handleAddFromPlan" v-if="planListCount > 0">
<view class=" icon-wrap">
<image src="/static/icons/plan.png" mode="aspectFit"></image>
<!-- <image src="/static/icons/plan.png" mode="aspectFit"></image> -->
<up-icon name="file-text-fill" color="#040000" size="24"></up-icon>
</view>
<view class="text-wrap" @click="">
<text class="option-title">从训练计划中添加</text>
... ... @@ -15,7 +16,7 @@
<view class="popup-option" @click="handleAddFromTemplate">
<view class="icon-wrap">
<image src="/static/icons/template.png" mode="aspectFit"></image>
<up-icon name="grid-fill" color="#040000" size="24"></up-icon>
</view>
<view class="text-wrap">
<text class="option-title">使用训练模板</text>
... ... @@ -25,7 +26,7 @@
<view class="popup-option" @click="handleFreeTraining">
<view class="icon-wrap">
<image src="/static/icons/free.png" mode="aspectFit"></image>
<up-icon name="clock-fill" color="#040000" size="24"></up-icon>
</view>
<view class="text-wrap">
<text class="option-title">自由训练</text>
... ... @@ -84,7 +85,7 @@ const handleAddFromTemplate = () => {
}
const handleFreeTraining = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' })
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' })
close()
}
... ...
... ... @@ -832,15 +832,16 @@ const calendarColorPickerSuccess = () => {
// 去训练
const startTraining = () => {
// console.log('++点击了去训练++');
// uni.navigateTo({
// url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3`,
// });
// if (!currentPlan.value) {
// uni.showToast({ title: '今日暂无训练', icon: 'none' })
// return
// }
// v-if="resdailyData.length > 0
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
return;
}
trainingStore.isTraining = true;
if (resdailyData.value.length > 0) {
trainingStore.isSystem = currentPlan.value.isSystem || false;
trainingStore.loadDailyTemplateForEdit(currentPlan.value);
... ...
... ... @@ -38,6 +38,9 @@ const popShow = ref(false)
// dongzuo实例ref,用于收集修改后数据
const dongzuoSingleRef = ref(null)
// 训练快照,用于在弹窗打开/关闭时保护正在进行的训练数据
const trainingSnapshot = ref(null)
const isRenderDongzuo = ref(false)
// 标记是否需要删除当前动作组
... ... @@ -187,9 +190,34 @@ const open = async (info) => {
isRenderDongzuo.value = false
needDeleteUnit.value = false
console.log('trainingStore.isTraining 值:', trainingStore.isTraining)
// ✅ 新增:如果有正在进行的训练,先把整个 store 状态装进口袋
if (trainingStore.isTraining) {
trainingSnapshot.value = JSON.parse(JSON.stringify({
id: trainingStore.id,
type: trainingStore.type,
actionDetail: trainingStore.actionDetail,
unitRecords: trainingStore.unitRecords,
trainingName: trainingStore.trainingName,
totalSeconds: trainingStore.totalSeconds,
trainingTimeText: trainingStore.trainingTimeText,
min: trainingStore.min,
isSystem: trainingStore.isSystem,
isTraining: trainingStore.isTraining,
}))
}
console.log('弹窗打开前,快照缓存:', trainingSnapshot.value);
console.log('打印全局数据trainingStore:', trainingStore);
console.log('info=', info);
// 每次打开先清空当前store临时数据,避免上次缓存污染
trainingStore.clearTrainingStore()
// 每次打开先清空当前store临时数据(保存计时功能,最小化可能在运行),避免上次缓存污染
trainingStore.resetContentOnly();
sourceInfo.value = JSON.parse(JSON.stringify(info))
// 关键:手动初始化当前unit的sets数据到Pinia.unitRecords(复用initTemplateRecords逻辑)
... ... @@ -237,7 +265,17 @@ const closePop = () => {
popShow.value = false
isRenderDongzuo.value = false
// 关闭清空临时数据
trainingStore.clearTrainingStore()
trainingStore.resetContentOnly()
// ✅ 新增:如果有快照,把训练数据还回去
if (trainingSnapshot.value) {
Object.assign(trainingStore, trainingSnapshot.value)
trainingSnapshot.value = null // 清空口袋
}
console.log('关闭弹窗后,快照缓存:', trainingSnapshot.value);
}
// 【保存修改:核心,组装参数调用更新接口】
... ... @@ -365,7 +403,16 @@ const saveEdit = async () => {
popShow.value = false
// 通知父页面刷新列表
emit('saveSuccess')
trainingStore.clearTrainingStore()
trainingStore.resetContentOnly()
// ✅ 新增:如果有快照,把训练数据还回去
if (trainingSnapshot.value) {
Object.assign(trainingStore, trainingSnapshot.value)
trainingSnapshot.value = null
}
} catch (err) {
console.error(err)
uni.showToast({ title: '修改失败', icon: 'none' })
... ...
... ... @@ -180,6 +180,10 @@ watch(
padding: 6rpx;
box-sizing: border-box;
// #ifdef H5
margin-top: 30px;
// #endif
.tab-item {
display: flex;
align-items: center;
... ...
... ... @@ -24,8 +24,7 @@
<button class="add-btn" @click.stop="addTemplateToToday(template.id)">添加</button>
</template>
<template v-else>
<button class="add-btn"
@click="uni.navigateTo({ url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${template.id}&type=3` })">
<button class="add-btn" @click.stop="goTrain(template)">
去训练
</button>
</template>
... ... @@ -60,6 +59,9 @@
import { ref, onMounted, watch } from 'vue';
import QueryPlanApi from '@/sheep/api/plan/queryplan';
import WodeJihuaLibiaoTancuang from '@/pages/xunji/components/wode-jihua-libiao-tancuang.vue'
import { useTrainingStore } from '@/sheep/store/trainingStore'
const trainingStore = useTrainingStore()
const showPlanList = ref(false);
const planListRef = ref(null);
... ... @@ -158,6 +160,26 @@ const addTemplateToToday = async (templateId) => {
}
};
// ====================== 去训练 跳转函数 ======================
const goTrain = (template) => {
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
return;
}
// 基础校验,防止id为空跳转异常
if (!template?.id) {
uni.showToast({ title: "模板ID异常", icon: "none" })
return
}
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${template.id}&type=3&isTraining=true`
})
}
// ====================== 【关键修复】监听父组件传的 planId ======================
watch(() => props.planId, (newId) => {
if (newId) {
... ...
... ... @@ -290,6 +290,11 @@ onMounted(() => {
padding: 20rpx;
// background-color: #f5f5f5;
border-radius: 12rpx;
margin-top: 20rpx;
/* #ifdef H5 */
margin-top: 20px;
/* #endif */
.search {
display: flex;
... ...
... ... @@ -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" @click="gototemplate(item)">
<view v-for="(item, index) in templateList" :key="index" class="template-item">
<image :src="item.urlCover" mode="aspectFill" class="template-img"></image>
<view>
<view class="template-content">
<view class="template-content" @click="gototemplate(item)">
<view class="template-title">{{ item.name }}</view>
<view class="template-count">{{ item.templatesCount }}个模板</view>
<view class="template-desc">{{ item.description }}</view>
... ... @@ -212,9 +212,9 @@ const gototemplate = (item) => {
});
};
onMounted(() => {
TemplatesList();
getPartCategories();
onMounted(async () => {
await TemplatesList();
await getPartCategories();
console.log('进入模板大类详情');
});
... ... @@ -222,17 +222,21 @@ onMounted(() => {
<style lang="scss" scoped>
.template-page {
width: 100%;
width: 100vw;
height: 100vh;
box-sizing: border-box;
.filter-section {
position: fixed;
top: 80rpx;
// top: 80rpx;
// #ifdef MP-WEIXIN
top: 234rpx;
// #endif
/* #ifdef H5 */
top: 77px;
/* #endif */
left: 0;
right: 0;
z-index: 999;
... ... @@ -260,66 +264,6 @@ onMounted(() => {
margin-right: 5rpx;
}
}
}
.template-list {
margin-top: 120rpx;
padding: 0 30rpx;
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 200rpx;
height: calc(100vh - 274rpx);
.sub-template-list {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.template-item {
display: flex;
background-color: white;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
padding: 20rpx;
.template-img {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.template-content {
flex: 1;
}
.template-title {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.template-count {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.template-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
&:last-child {
margin-bottom: 45rpx;
}
}
/* 筛选包装器 */
.filter-wrapper {
... ... @@ -347,7 +291,7 @@ onMounted(() => {
align-items: center;
justify-content: center;
width: auto;
min-width: 160rpx;
min-width: 125rpx;
height: 50rpx;
background: #fff;
color: #333;
... ... @@ -435,5 +379,79 @@ onMounted(() => {
font-weight: 500;
}
}
.template-list {
margin-top: 120rpx;
padding: 0 30rpx;
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
// #ifdef MP-WEIXIN
padding-bottom: 200rpx;
height: calc(100vh - 274rpx);
// #endif
/* #ifdef H5 */
margin-top: 87px;
// 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px
height: calc(100vh - 77px - 87px);
// padding-bottom: 40px;
/* #endif */
.sub-template-list {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.template-item {
display: flex;
background-color: white;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
padding: 20rpx;
&:first-child {
margin-top: 24rpx;
}
.template-img {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.template-content {
flex: 1;
}
.template-title {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.template-count {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.template-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
&:last-child {
margin-bottom: 45rpx;
}
}
}
}
</style>
... ...
... ... @@ -794,6 +794,9 @@ onMounted(async () => {
.content-scroll {
height: calc(100vh - 400rpx);
// #ifdef H5
height: calc(100vh - 185px);
// #endif
width: 100%;
box-sizing: border-box;
overflow: auto;
... ... @@ -984,8 +987,14 @@ onMounted(async () => {
}
.muscleShow-popup {
/* #ifdef MP-WEIXIN */
min-height: 40vh;
background-color: #fff;
/* #endif */
/* #ifdef H5 */
min-height: 55vh;
/* #endif */
.muscle-name {
font-size: 32rpx;
... ...
<template>
<view v-if="trainStore.trainingInfo.isMinimized" class="float-ball" @click="goBackTrain">
<text class="text">训练中</text>
<text class="time">{{ formatTime(trainStore.trainingInfo.totalSeconds) }}</text>
</view>
<view v-if="trainStore.trainingInfo.isMinimized" class="float-ball" @click="goBackTrain">
<text class="text">训练中</text>
<text class="time">{{ formatTime(trainStore.trainingInfo.totalSeconds) }}</text>
</view>
</template>
<script setup>
... ... @@ -11,45 +11,46 @@ const trainStore = useTrainingStore();
// 回到训练页
const goBackTrain = () => {
const info = trainStore.trainingInfo;
trainStore.restore();
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${info.exerciseId}&unitType=${info.unitType}`,
});
const info = trainStore.trainingInfo;
trainStore.restore();
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${info.exerciseId}&unitType=${info.unitType}&isTraining=true`,
});
};
// 时间格式化
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 m = Math.floor(s / 60).toString().padStart(2, '0');
const sec = (s % 60).toString().padStart(2, '0');
return `${m}:${sec}`;
};
</script>
<style scoped>
.float-ball {
position: fixed;
bottom: 200rpx;
right: 40rpx;
width: 120rpx;
height: 120rpx;
background: #ffd700;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 99999;
position: fixed;
bottom: 200rpx;
right: 40rpx;
width: 120rpx;
height: 120rpx;
background: #ffd700;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 99999;
}
.text {
font-size: 24rpx;
font-weight: bold;
font-size: 24rpx;
font-weight: bold;
}
.time {
font-size: 20rpx;
margin-top: 6rpx;
font-size: 20rpx;
margin-top: 6rpx;
}
</style>
\ No newline at end of file
... ...
... ... @@ -355,5 +355,10 @@ $cell-h: 110rpx;
font-size: 30rpx;
color: $text-white;
margin-top: 60rpx;
/* #ifdef H5 */
margin-bottom: 20px;
/* #endif */
}
</style>
\ No newline at end of file
... ...
... ... @@ -165,7 +165,7 @@
</template>
<script setup>
import { ref, reactive, computed, onBeforeUnmount, onMounted, nextTick, onUnmounted } from 'vue';
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'
... ... @@ -194,19 +194,38 @@ const trainingName = computed(() => trainingStore.trainingName)
const actionSortRef = ref(null)
watch(
() => trainingStore.isTraining,
(newVal, oldVal) => {
console.log('训练状态变化:', oldVal, '→', newVal)
if (newVal) {
// 正在训练逻辑
} else {
// 结束训练逻辑
}
},
{ immediate: true } // 页面初始化立即执行一次
)
// 动作排序弹窗
const openActionSort = () => {
console.log('调用前ref 的值:', actionSortRef.value)
console.log('调用前,ref 的值:', actionSortRef.value)
actionSortRef.value.openActionSort()
}
onLoad(async (options) => {
console.log('✅ 动作练习页面 onLoad 中的 options:', 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);
... ... @@ -508,7 +527,7 @@ const save = async () => {
// ✅ 情况1:【去训练】→ 保存历史
if (trainingStore.isTraining) {
await TrainingApi.createTrainHistory({ units });
uni.showToast({ title: '训练保存成功', icon: 'success' });
uni.showToast({ title: '训练提交成功', icon: 'success' });
}
// ✅ 情况2:【编辑模板】→ 更新模板
else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
... ... @@ -521,7 +540,7 @@ const save = async () => {
// ✅ 情况3:普通训练
else {
await TrainingApi.createTrainHistory({ units });
uni.showToast({ title: '保存成功', icon: 'success' });
uni.showToast({ title: '训练提交成功', icon: 'success' });
}
setTimeout(() => {
... ... @@ -571,7 +590,10 @@ const addActionsPopup = () => {
// 最小化
const openMin = () => {
console.log('最小化跳转前trainingStore.isTraining', trainingStore.isTraining);
console.log('trainingStore.min+++++++++++++', trainingStore.min);
console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining);
trainingStore.min = true;
// uni.navigateTo({
// url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径
... ...
<!--
训练动作详情页(xunji-dongzuo-xiangqing.vue)
核心用于展示单个体能训练动作的完整信息,并提供动作管理、训练启动、收藏分享等操作,适配移动端多端(微信小程序 / APP/H5 等)。
备注这个字段,没有接口,没有交互,只是前端渲染
-->
<template>
<view class="container">
<!-- 页面头部编辑/分享/收藏三个按钮 -->
<view class="page-header">
<text class="title">{{ exercisedetail.name }}</text>
<view class="header-icons">
<uni-icons type="more" size="24" color="#fff" @click="edit"></uni-icons>
<!-- 编辑按钮 -->
<view class="edit-icon" @click="edit">
<uni-icons type="more" size="24" color="#fff"></uni-icons>
</view>
<!-- 分享按钮 -->
<button class="share-btn" open-type="share">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</button>
<uni-icons :type="islove ? 'heart-filled' : 'heart'" size="24" color="#fff" @click="toggleCollect"></uni-icons>
</view>
</view>
<!--标签栏(可切换的 3 个页面)要点,历史,平替动作-->
... ... @@ -96,7 +95,7 @@
<!-- 1. 日期置顶 -->
<view class="card-date">
<text class="date-text">{{ item.date ? `${item.date[0]}/${item.date[1]}/${item.date[2]}` : '未知日期'
}}</text>
}}</text>
</view>
<!-- 2. 主体信息区域 -->
<view class="card-body">
... ... @@ -300,6 +299,8 @@ const toggleCollect = async () => {
// 收藏状态:传 1,取消收藏:传 0
const status = islove.value ? 1 : 0;
console.log('传递给收藏接口的statu', status);
// 调用收藏接口
await ExercisesApi.toggleFavorite(id.value, status);
... ... @@ -320,7 +321,7 @@ const checkCollectStatus = async () => {
if (!id.value) return;
try {
const res = await ExercisesApi.checkExerciseFavorited(id.value);
// 接口返回 true = 已收藏
console.log('动作收藏与否:', res);
if (res.data === true) {
// 后端说已收藏
islove.value = true; // 实心爱心
... ...
<!--
定义训练动作新增页面(xunji-dongzuo-xinzeng.vue),核心用于创建 / 新增自定义体能训练动作,位置在训记的动作右侧抽屉点击新增动作就是这个页面.
支持训练部位、动作类型、目录分类、封面 / 视频等多维度配置,最终提交保存到后端接口,适配移动端多端(小程序 / APP/H5)
-->
<template>
<view class="add-custom-exercise-page">
<!-- 肌肉部位图 -->
... ... @@ -41,11 +37,19 @@
<view class="form-section">
<!-- 动作名称 -->
<view class="form-item">
<view class="label">动作名称</view>
<!-- <view class="label">动作名称</view>
<view class="input-group">
<input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称"
placeholder-class="input-placeholder" />
</view> -->
<view class="action-name">
<view class="label">动作名称</view>
<view class="input-group">
<input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称"
placeholder-class="input-placeholder" />
</view>
</view>
<view class="upload-btn" @click="openCoverSelector">
<image :src="coverImagePath" class="preview-media" mode="aspectFill" />
<text class="btn-text">更换封面</text>
... ... @@ -67,7 +71,7 @@
<text class="value">{{ actionType || '请选择' }}</text>
<!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
class="arrow-icon" mode="aspectFill" /> -->
<up-icon name="arrow-right" color="#333" size="16"></up-icon>
<up-icon v-if="!isEdit" name="arrow-right" color="#333" size="16"></up-icon>
</view>
</view>
... ... @@ -121,6 +125,10 @@
</view>
</view>
<view class="form-item" v-if="isEdit" @click="deleteAction">
<view class="center-label">删除动作</view>
</view>
<!-- 保存按钮 -->
<view class="save-button">
<button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button>
... ... @@ -139,7 +147,7 @@
<picker-view-column>
<view class="picker-item" v-for="item in actionTypeOptions" :key="item">{{
item.name
}}</view>
}}</view>
</picker-view-column>
</picker-view>
</view>
... ... @@ -158,14 +166,14 @@
<picker-view-column>
<view class="picker-item" v-for="item in bodyPartOptions" :key="item">{{
item.name
}}</view>
}}</view>
</picker-view-column>
</picker-view>
<picker-view :value="[equipmentIndex]" @change="onEquipmentChange" class="picker-view half">
<picker-view-column>
<view class="picker-item" v-for="item in equipmentOptions" :key="item">{{
item.name
}}</view>
}}</view>
</picker-view-column>
</picker-view>
</view>
... ... @@ -176,6 +184,7 @@
<script setup>
import { onMounted, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import ExercisesApi from '@/sheep/api/motion/exercises';
import LeftmotionApi from '@/sheep/api/motion/equipments';
import EquipmentListApi from '@/sheep/api/motion/motionequipments';
... ... @@ -185,6 +194,7 @@ const exerciseName = ref('');
const actionType = ref();
const directoryName = ref();
const selectedMusclesDisplay = ref('未选择');
const editActionId = ref(null)
// 选中索引
const actionTypeIndex = ref(0);
... ... @@ -211,6 +221,145 @@ const actionTypeOptions = ref([
// console.error('加载部位失败', err);
// }
// };
const resetForm = () => {
exerciseName.value = ''
actionType.value = ''
actionTypeIndex.value = 0
directoryName.value = ''
bodyPartIndex.value = 0
equipmentIndex.value = 0
selectedMusclesDisplay.value = '未选择'
primaryMuscles.value = []
secondaryMuscles.value = []
coverImagePath.value = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
coverUploaded.value = false
videoFilePath.value = ''
videoUploaded.value = false
isVideoFile.value = false
urlTutorial.value = ''
videoDetailName.value = ''
// 同时关闭所有弹窗,防止上次弹窗残留
closeAllPopups()
}
const isEdit = ref(false)
const actionDetail = ref({});
onLoad(async (options) => {
isEdit.value = options.isEdit === 'true'
const editId = Number(options.id)
editActionId.value = editId
console.log('编辑状态', isEdit, '编辑动作ID', editId)
// 清空表单状态
resetForm()
await Promise.all([
loadbodyPartOptions(),
loadequipmentOptions(),
loadMuscleGroups()
])
if (isEdit.value) {
loadexercisedetail(editId)
}
})
// 加载单个动作详情
// const loadexercisedetail = async (id) => {
// if (!id || !Number(id)) return
// try {
// const response = await ExercisesApi.getExerciseById(id);
// actionDetail.value = response.data;
// console.log('打印动作详情', actionDetail.value);
// } catch (err) {
// console.error('加载动作详情失败:', err)
// actionDetail.value = {}
// }
// };
// 加载单个动作详情并回显表单
const loadexercisedetail = async (id) => {
if (!id || !Number(id)) return
try {
const response = await ExercisesApi.getExerciseById(id);
actionDetail.value = response.data;
const detail = actionDetail.value
console.log('打印动作详情', detail);
// 1. 动作名称
exerciseName.value = detail.name || ''
// 2. 动作类型回显(id匹配,编辑只读)
const targetType = actionTypeOptions.value.find(item => item.id === detail.exerciseType)
if (targetType) {
actionTypeIndex.value = actionTypeOptions.value.findIndex(item => item.id === detail.exerciseType)
actionType.value = targetType.name
}
// 3. 目录:身体部位categoryId + 器械equipmentId 匹配下标
const bodyIdx = bodyPartOptions.value.findIndex(item => item.id === detail.categoryId)
if (bodyIdx > -1) bodyPartIndex.value = bodyIdx
const equipIdx = equipmentOptions.value.findIndex(item => item.id === detail.equipmentId)
if (equipIdx > -1) equipmentIndex.value = equipIdx
// 拼接目录展示文字
const selBody = bodyPartOptions.value[bodyIdx]
const selEquip = equipmentOptions.value[equipIdx]
if (selBody && selEquip) {
directoryName.value = ` ${selBody.name}( ${selEquip.name})`
}
// 4. 肌肉部位:后端返回字符串 "[30,28,29]" 转数字数组
if (detail.primaryMuscles) {
primaryMuscles.value = JSON.parse(detail.primaryMuscles)
} else {
primaryMuscles.value = []
}
if (detail.secondaryMuscles) {
secondaryMuscles.value = JSON.parse(detail.secondaryMuscles)
} else {
secondaryMuscles.value = []
}
// 直接使用后端返回的拼接好的肌肉名称展示文本,不用重新拼接
const primaryStr = detail.primaryMuscleNames?.length ? detail.primaryMuscleNames.join('、') + '(主)' : ''
const secondaryStr = detail.secondaryMuscleNames?.length ? detail.secondaryMuscleNames.join('、') + '(次)' : ''
let display = ''
if (primaryStr) display += primaryStr
if (secondaryStr) {
if (display) display += ', '
display += secondaryStr
}
selectedMusclesDisplay.value = display || '未选择'
// 5. 封面图 url3dAnimation
if (detail.url3dAnimation) {
coverImagePath.value = detail.url3dAnimation
coverUploaded.value = true // 标记已有封面,跳过新增封面校验
}
// 6. 动作实拍视频/图片 urlRealPerson
if (detail.urlRealPerson) {
videoFilePath.value = detail.urlRealPerson
videoUploaded.value = true
// 简单判断是否视频(后端返回mp4则标记video)
isVideoFile.value = detail.urlRealPerson.includes('.mp4')
}
// 7. 详解视频 urlTutorial
if (detail.urlTutorial) {
urlTutorial.value = detail.urlTutorial
videoDetailName.value = '已选择详解视频'
}
} catch (err) {
console.error('加载动作详情失败:', err)
actionDetail.value = {}
}
};
// 肌肉部位列表(从接口获取)
const muscleGroups = ref([]);
// 加载肌肉部位
... ... @@ -253,6 +402,7 @@ const showVideoSelector = ref(false);
// 弹窗控制
const openActionTypePicker = () => {
if (isEdit.value) return;
showActionTypePicker.value = true;
};
const openDirectoryPicker = () => {
... ... @@ -394,7 +544,6 @@ const saveExercise = async () => {
return;
}
console.log('------------------');
console.log('--', equipmentOptions.value[equipmentIndex.value]);
console.log('--', bodyPartOptions.value[bodyPartIndex.value]);
// 获取 categoryId 和 equipmentId
... ... @@ -435,14 +584,23 @@ const saveExercise = async () => {
urlRealPerson: videoFilePath.value, //真人实拍视频
urlTutorial: urlTutorial.value, // 详解视频
};
// 编辑追加id
if (isEdit.value) {
payload.id = editActionId.value
}
console.log('【发送的 payload】', payload);
let res;
try {
const res = await ExercisesApi.createExercise(payload);
console.log('【接口返回】', res);
if (isEdit.value) {
res = await ExercisesApi.updateexercises(payload);
console.log('【更新接口返回】', res);
} else {
res = await ExercisesApi.createExercise(payload);
console.log('【创建接口返回】', res);
}
if (res.code === 0) {
uni.showToast({ title: '保存成功', icon: 'success' });
// 可选:返回上一页或清空表单
uni.showToast({ title: isEdit.value ? '编辑成功' : '保存成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 1000);
... ... @@ -455,6 +613,29 @@ const saveExercise = async () => {
}
};
// 删除动作
const deleteAction = () => {
uni.showModal({
title: '提示',
content: '确定要删除该动作吗?删除后无法恢复',
success: async (res) => {
if (res.confirm) {
try {
const delRes = await ExercisesApi.deleteexercises(editActionId.value)
if (delRes.code === 0) {
uni.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000)
} else {
uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: '删除请求失败', icon: 'none' })
}
}
}
})
}
const openMusclePicker = () => {
showMusclePicker.value = true;
};
... ... @@ -504,9 +685,9 @@ const confirmMuscleSelection = () => {
};
onMounted(() => {
loadbodyPartOptions();
loadequipmentOptions();
loadMuscleGroups();
// loadbodyPartOptions();
// loadequipmentOptions();
// loadMuscleGroups();
// loadexercisesGroups();
});
</script>
... ... @@ -602,12 +783,21 @@ onMounted(() => {
position: relative;
}
.action-name {
width: 285rpx;
}
.label {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.center-label {
text-align: center;
color: #c44b4b;
}
.input-group {
display: flex;
align-items: center;
... ...
... ... @@ -372,9 +372,18 @@ const handleUnitClick = (unit) => {
// 开始训练
const startTraining = () => {
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
return;
}
trainingStore.isSystem = 1;
trainingStore.isTraining = true;
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.value.id}&type=3`,
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.value.id}&type=3&isTraining=true`,
});
console.log('打印传递给动作训练页面的模板id', TemplateDetail.id);
... ... @@ -382,6 +391,14 @@ const startTraining = () => {
// startDaiTemplateTraining
const startDaiTemplateTraining = () => {
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
return;
}
trainingStore.isSystem = TemplateDetail.value.isSystem;
trainingStore.loadDailyTemplateForEdit(TemplateDetail.value);
trainingStore.initDailyTemplateRecords()
... ...
... ... @@ -80,7 +80,7 @@
</view>
<!-- ==========新增:未来7天训练日日历模块 end========== -->
<!-- 计划课程 -->
<!-- 计划课程(模板) -->
<view class="course-section">
<text class="section-title">计划课程</text>
<view class="course-card" v-for="item in plandetail.templates" :key="item.id"
... ... @@ -312,9 +312,18 @@ const openArrageClass = () => {
// 开始训练
const startTraining = (templateId) => {
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
return;
}
trainingStore.isTraining = true;
trainingStore.isSystem = 1;
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateId}&type=3`,
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateId}&type=3&isTraining=true`,
});
console.log('打印传递给动作训练页面的模板id', templateId);
... ...
... ... @@ -16,6 +16,10 @@
<button v-else class="unbind-btn" @click="unbindWeChat">解绑</button> -->
</view>
<view class="setting-item" @click.stop="goToForgetPassword">
<view class="setting-label">修改密码</view>
</view>
<!-- 推送通知开关 -->
<!-- <view class="setting-item notice">
<view class="content">
... ... @@ -181,6 +185,14 @@ const onRecommendChange = async (e) => {
}
};
// 忘记密码跳转
const goToForgetPassword = () => {
uni.navigateTo({
url: '/pages7/pages/index/reset-password?isModify=true',
});
};
// 退出登录
const logout = () => {
uni.showModal({
... ...
... ... @@ -6,32 +6,18 @@
<!-- 优化:为了兼容微信小程序等跨端环境,原生 SVG 转换为 Base64 嵌入标准 image 标签中,确保完美渲染 -->
<image class="logo-img" :src="logoSvgBase64" mode="aspectFit" />
</view>
<view class="brand-title">FitFlow 悦动健身</view>
<view class="brand-title">自己练</view>
<view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view>
</view>
<!-- 2. 登录方式 Tab 切换 -->
<view class="tab-container">
<view class="tab-item" :class="{ active: activeTab === 'sms' }" @click="switchTab('sms')">
<u-icon
name="phone"
size="18"
:color="activeTab === 'sms' ? '#0a1931' : '#7a8290'"
class="tab-icon"
/>
<u-icon name="phone" size="18" :color="activeTab === 'sms' ? '#0a1931' : '#7a8290'" class="tab-icon" />
<text>免密登录</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'password' }"
@click="switchTab('password')"
>
<u-icon
name="lock"
size="18"
:color="activeTab === 'password' ? '#0a1931' : '#7a8290'"
class="tab-icon"
/>
<view class="tab-item" :class="{ active: activeTab === 'password' }" @click="switchTab('password')">
<u-icon name="lock" size="18" :color="activeTab === 'password' ? '#0a1931' : '#7a8290'" class="tab-icon" />
<text>密码登录</text>
</view>
</view>
... ... @@ -43,14 +29,8 @@
<text class="form-label">手机号码</text>
<view class="input-box">
<u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
<input
type="number"
v-model="phoneNumber"
placeholder="请输入您的手机号"
placeholder-style="color: #a1a8b3"
maxlength="11"
class="native-input"
/>
<input type="number" v-model="phoneNumber" placeholder="请输入您的手机号" placeholder-style="color: #a1a8b3"
maxlength="11" class="native-input" />
</view>
</view>
... ... @@ -60,21 +40,11 @@
<view class="code-row">
<view class="input-box flex-1">
<u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
<input
type="number"
v-model="verifyCode"
placeholder="6位验证码"
placeholder-style="color: #a1a8b3"
maxlength="6"
class="native-input"
/>
<input type="number" v-model="verifyCode" placeholder="6位验证码" placeholder-style="color: #a1a8b3"
maxlength="6" class="native-input" />
</view>
<!-- 获取验证码按钮 -->
<view
class="code-btn"
:class="{ disabled: countdown > 0 || isSending }"
@click="handleGetCode"
>
<view class="code-btn" :class="{ disabled: countdown > 0 || isSending }" @click="handleGetCode">
<text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
</view>
</view>
... ... @@ -85,14 +55,8 @@
<text class="form-label">登录密码</text>
<view class="input-box">
<u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
<input
type="password"
v-model="password"
placeholder="请输入您的密码"
placeholder-style="color: #a1a8b3"
class="native-input"
:password="true"
/>
<input type="password" v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3"
class="native-input" :password="true" />
</view>
</view>
</view>
... ... @@ -121,439 +85,439 @@
</template>
<script setup>
import { ref, onUnmounted } from 'vue';
import sheep from '@/sheep';
import AuthUtil from '@/sheep/api/member/auth';
import { onHide } from '@dcloudio/uni-app';
// 用于跨端兼容渲染的 Base64 编码火焰 SVG (绿渐变)
const logoSvgBase64 =
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="%238fc31f" /><stop offset="100%" stop-color="%2300b074" /></linearGradient></defs><path d="M12 2C12 2 6 7.5 6 13C6 16.8 9.1 20 12 20C14.9 20 18 16.8 18 13C18 7.5 12 2 12 2ZM12 16C10.3 16 9 14.7 9 13C9 10.5 12 7.5 12 7.5C12 7.5 15 10.5 15 13C15 14.7 13.7 16 12 16Z" fill="url(%23g)" /></svg>';
const activeTab = ref('sms'); // sms: 免密, password: 密码
const phoneNumber = ref('');
const verifyCode = ref('');
const password = ref('');
const isAgreed = ref(false);
// 防抖及加载动画状态变量
const isSending = ref(false);
const isSubmitting = ref(false);
// 验证码倒计时逻辑
const countdown = ref(0);
let timer = null;
// 验证手机号码格式
const validatePhone = (phone) => {
return /^1[3-9]\d{9}$/.test(phone);
};
// 切换Tab,同时清理另一个 Tab 的输入
const switchTab = (type) => {
activeTab.value = type;
verifyCode.value = '';
password.value = '';
};
// 发送验证码
const handleGetCode = async () => {
if (countdown.value > 0 || isSending.value) return;
// 前置逻辑校验
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号码', icon: 'none' });
return;
}
if (!validatePhone(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
return;
}
import { ref, onUnmounted } from 'vue';
import sheep from '@/sheep';
import AuthUtil from '@/sheep/api/member/auth';
import { onHide } from '@dcloudio/uni-app';
// 用于跨端兼容渲染的 Base64 编码火焰 SVG (绿渐变)
const logoSvgBase64 =
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="%238fc31f" /><stop offset="100%" stop-color="%2300b074" /></linearGradient></defs><path d="M12 2C12 2 6 7.5 6 13C6 16.8 9.1 20 12 20C14.9 20 18 16.8 18 13C18 7.5 12 2 12 2ZM12 16C10.3 16 9 14.7 9 13C9 10.5 12 7.5 12 7.5C12 7.5 15 10.5 15 13C15 14.7 13.7 16 12 16Z" fill="url(%23g)" /></svg>';
const activeTab = ref('sms'); // sms: 免密, password: 密码
const phoneNumber = ref('');
const verifyCode = ref('');
const password = ref('');
const isAgreed = ref(false);
// 防抖及加载动画状态变量
const isSending = ref(false);
const isSubmitting = ref(false);
// 验证码倒计时逻辑
const countdown = ref(0);
let timer = null;
// 验证手机号码格式
const validatePhone = (phone) => {
return /^1[3-9]\d{9}$/.test(phone);
};
// 切换Tab,同时清理另一个 Tab 的输入
const switchTab = (type) => {
activeTab.value = type;
verifyCode.value = '';
password.value = '';
};
// 发送验证码
const handleGetCode = async () => {
if (countdown.value > 0 || isSending.value) return;
// 前置逻辑校验
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号码', icon: 'none' });
return;
}
if (!validatePhone(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
return;
}
isSending.value = true;
try {
// 成功发送验证码请求
const res = await AuthUtil.sendSmsCode({ phone: phoneNumber.value });
uni.showToast({ title: '验证码已发送', icon: 'success' });
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} catch (err) {
console.error('发送验证码失败异常:', err);
} finally {
isSending.value = false;
}
};
isSending.value = true;
try {
// 成功发送验证码请求
const res = await AuthUtil.sendSmsCode({ phone: phoneNumber.value });
uni.showToast({ title: '验证码已发送', icon: 'success' });
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} catch (err) {
console.error('发送验证码失败异常:', err);
} finally {
isSending.value = false;
}
};
// 登录按钮提交逻辑
const handleLogin = async () => {
if (isSubmitting.value) return;
// 登录按钮提交逻辑
const handleLogin = async () => {
if (isSubmitting.value) return;
// 1. 用户协议验证
if (!isAgreed.value) {
uni.showToast({ title: '请先阅读并同意用户协议及隐私政策', icon: 'none' });
return;
}
// 2. 基础手机号格式验证
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号码', icon: 'none' });
return;
}
if (!validatePhone(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
return;
}
// 1. 用户协议验证
if (!isAgreed.value) {
uni.showToast({ title: '请先阅读并同意用户协议及隐私政策', icon: 'none' });
// 3. Tab特定表单验证并请求
if (activeTab.value === 'sms') {
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
// 2. 基础手机号格式验证
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号码', icon: 'none' });
if (verifyCode.value.length < 4) {
uni.showToast({ title: '请输入完整的验证码', icon: 'none' });
return;
}
if (!validatePhone(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
await performLogin('sms');
} else {
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
// 3. Tab特定表单验证并请求
if (activeTab.value === 'sms') {
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
if (verifyCode.value.length < 4) {
uni.showToast({ title: '请输入完整的验证码', icon: 'none' });
return;
}
await performLogin('sms');
await performLogin('password');
}
};
// 执行最终请求与状态写入
const performLogin = async (type) => {
isSubmitting.value = true;
uni.showLoading({ title: '正在登录...', mask: true });
try {
let res = null;
if (type === 'sms') {
// 修复:原代码使用的 AuthApi 变更为导入的 AuthUtil 服务
res = await AuthUtil.loginBySms({
phone: phoneNumber.value,
code: verifyCode.value,
});
} else {
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
await performLogin('password');
res = await AuthUtil.loginByPhonePassword({
phone: phoneNumber.value,
password: password.value,
});
}
};
// 执行最终请求与状态写入
const performLogin = async (type) => {
isSubmitting.value = true;
uni.showLoading({ title: '正在登录...', mask: true });
try {
let res = null;
if (type === 'sms') {
// 修复:原代码使用的 AuthApi 变更为导入的 AuthUtil 服务
res = await AuthUtil.loginBySms({
phone: phoneNumber.value,
code: verifyCode.value,
});
} else {
res = await AuthUtil.loginByPhonePassword({
phone: phoneNumber.value,
password: password.value,
});
}
const authData = res.data || {};
const authData = res.data || {};
if (authData && authData.accessToken) {
// 1. 取得芋道商城 Pinia 内置的 user Store
const userStore = sheep.$store('user');
if (authData && authData.accessToken) {
// 1. 取得芋道商城 Pinia 内置的 user Store
const userStore = sheep.$store('user');
// 2. 写入 Token 到 Pinia 及持久态 LocalStorage
userStore.setToken(authData.accessToken, authData.refreshToken || '');
// 2. 写入 Token 到 Pinia 及持久态 LocalStorage
userStore.setToken(authData.accessToken, authData.refreshToken || '');
// 3. 登录成功后,主动拉取一次用户信息同步至本地状态
await userStore.getInfo();
// 3. 登录成功后,主动拉取一次用户信息同步至本地状态
await userStore.getInfo();
uni.showToast({
title: '登录成功',
icon: 'success',
});
uni.showToast({
title: '登录成功',
icon: 'success',
});
// 4. 跳转至目标页面
setTimeout(() => {
uni.switchTab({
url: '/pages/xunji/xunji',
});
}, 100);
} else {
uni.showToast({
title: res.msg || '登录失败,未获取到有效凭证',
icon: 'none',
// 4. 跳转至目标页面
setTimeout(() => {
uni.switchTab({
url: '/pages/xunji/xunji',
});
}
} catch (err) {
console.error('登录逻辑异常:', err);
} finally {
isSubmitting.value = false;
uni.hideLoading();
}
};
// 忘记密码跳转
const goToForgetPassword = () => {
uni.navigateTo({
url: '/pages7/pages/index/reset-password',
});
};
// 协议详情跳转
const goToAgreement = (type) => {
const url =
type === 'service'
? '/pages/public/richtext?id=service'
: '/pages/public/richtext?id=privacy';
uni.navigateTo({
url,
fail: () => {
uni.showToast({
title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,
icon: 'none',
});
},
});
};
// 页面卸载生命周期:清除定时器,规避潜在的内存泄露
onHide(() => {
if (timer) {
clearInterval(timer);
timer = null;
}, 100);
} else {
uni.showToast({
title: res.msg || '登录失败,未获取到有效凭证',
icon: 'none',
});
}
} catch (err) {
console.error('登录逻辑异常:', err);
} finally {
isSubmitting.value = false;
uni.hideLoading();
}
};
// 忘记密码跳转
const goToForgetPassword = () => {
uni.navigateTo({
url: '/pages7/pages/index/reset-password?isModify=false',
});
};
// 协议详情跳转
const goToAgreement = (type) => {
const url =
type === 'service'
? '/pages/public/richtext?id=service'
: '/pages/public/richtext?id=privacy';
uni.navigateTo({
url,
fail: () => {
uni.showToast({
title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,
icon: 'none',
});
},
});
};
// 页面卸载生命周期:清除定时器,规避潜在的内存泄露
onHide(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
</script>
<style lang="scss" scoped>
.login-wrapper {
height: 100vh;
// #ifdef H5
height: calc(100vh - 44px);
// #endif
background-color: #ffffff;
padding: 80rpx 50rpx;
.login-wrapper {
height: 100vh;
// #ifdef H5
height: calc(100vh - 44px);
// #endif
background-color: #ffffff;
padding: 80rpx 50rpx;
box-sizing: border-box;
// 1. Logo
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 70rpx;
margin-top: 40rpx;
.logo-box {
width: 140rpx;
height: 140rpx;
border-radius: 40rpx;
background: #ffffff;
border: 3rpx solid #d2ee9e;
box-shadow: 0 16rpx 40rpx rgba(111, 214, 32, 0.12);
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 30rpx;
.logo-img {
width: 76rpx;
height: 76rpx;
}
}
.brand-title {
font-size: 46rpx;
font-weight: bold;
color: #0a1931;
letter-spacing: 2rpx;
}
.brand-slogan {
font-size: 26rpx;
color: #9097a3;
margin-top: 12rpx;
}
}
// 2. Tab
.tab-container {
height: 96rpx;
background-color: #f1f3f7;
border-radius: 20rpx;
display: flex;
padding: 8rpx;
box-sizing: border-box;
margin-bottom: 50rpx;
// 1. Logo
.logo-section {
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 70rpx;
margin-top: 40rpx;
.logo-box {
width: 140rpx;
height: 140rpx;
border-radius: 40rpx;
background: #ffffff;
border: 3rpx solid #d2ee9e;
box-shadow: 0 16rpx 40rpx rgba(111, 214, 32, 0.12);
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 30rpx;
font-size: 28rpx;
color: #7a8290;
font-weight: 500;
transition: all 0.2s ease;
border-radius: 16rpx;
.logo-img {
width: 76rpx;
height: 76rpx;
}
.tab-icon {
margin-right: 12rpx;
}
.brand-title {
font-size: 46rpx;
font-weight: bold;
&.active {
background-color: #ffffff;
color: #0a1931;
letter-spacing: 2rpx;
font-weight: bold;
box-shadow: 0 4rpx 16rpx rgba(10, 25, 49, 0.05);
}
}
}
.brand-slogan {
// 3. Form
.form-container {
.form-item-wrapper {
margin-bottom: 36rpx;
.form-label {
font-size: 26rpx;
color: #9097a3;
margin-top: 12rpx;
color: #0a1931;
font-weight: bold;
display: block;
margin-bottom: 16rpx;
}
}
// 2. Tab
.tab-container {
height: 96rpx;
background-color: #f1f3f7;
border-radius: 20rpx;
display: flex;
padding: 8rpx;
box-sizing: border-box;
margin-bottom: 50rpx;
.tab-item {
flex: 1;
.input-box {
height: 104rpx;
background-color: #f4f6fa;
border: 2rpx solid #e1e6eb;
border-radius: 24rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
color: #7a8290;
font-weight: 500;
transition: all 0.2s ease;
border-radius: 16rpx;
padding: 0 30rpx;
box-sizing: border-box;
.tab-icon {
margin-right: 12rpx;
.input-icon {
margin-right: 16rpx;
}
&.active {
background-color: #ffffff;
.native-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #0a1931;
font-weight: bold;
box-shadow: 0 4rpx 16rpx rgba(10, 25, 49, 0.05);
}
}
}
// 3. Form
.form-container {
.form-item-wrapper {
margin-bottom: 36rpx;
// 验证码一栏的双列布局
.code-row {
display: flex;
align-items: center;
.form-label {
font-size: 26rpx;
color: #0a1931;
font-weight: bold;
display: block;
margin-bottom: 16rpx;
.flex-1 {
flex: 1;
}
.input-box {
.code-btn {
width: 220rpx;
height: 104rpx;
background-color: #f4f6fa;
border: 2rpx solid #e1e6eb;
border-radius: 24rpx;
background-color: #f9fdf2;
border: 2rpx solid #d0ee9c;
display: flex;
justify-content: center;
align-items: center;
padding: 0 30rpx;
box-sizing: border-box;
.input-icon {
margin-right: 16rpx;
}
.native-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #0a1931;
}
}
// 验证码一栏的双列布局
.code-row {
display: flex;
align-items: center;
margin-left: 20rpx;
font-size: 26rpx;
color: #8fc31f;
font-weight: bold;
transition: all 0.2s ease;
.flex-1 {
flex: 1;
&:active {
opacity: 0.8;
}
.code-btn {
width: 220rpx;
height: 104rpx;
border-radius: 24rpx;
background-color: #f9fdf2;
border: 2rpx solid #d0ee9c;
display: flex;
justify-content: center;
align-items: center;
margin-left: 20rpx;
font-size: 26rpx;
color: #8fc31f;
font-weight: bold;
transition: all 0.2s ease;
&:active {
opacity: 0.8;
}
&.disabled {
color: #b0b8c4;
background-color: #f4f6fa;
border-color: #e1e6eb;
pointer-events: none; // 禁用点击以避免多余的触发
}
&.disabled {
color: #b0b8c4;
background-color: #f4f6fa;
border-color: #e1e6eb;
pointer-events: none; // 禁用点击以避免多余的触发
}
}
}
}
}
.forget-password {
text-align: right;
margin-top: 20rpx;
font-size: 24rpx;
color: rgb(220, 54, 46);
}
.forget-password {
text-align: right;
margin-top: 20rpx;
font-size: 24rpx;
color: rgb(220, 54, 46);
}
// 4. Agreement
.agreement-container {
// 4. Agreement
.agreement-container {
display: flex;
align-items: flex-start;
margin-top: 40rpx;
margin-bottom: 50rpx;
padding: 0 6rpx;
.checkbox-box {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #c2c9d3;
border-radius: 8rpx;
margin-right: 16rpx;
margin-top: 4rpx;
display: flex;
align-items: flex-start;
margin-top: 40rpx;
margin-bottom: 50rpx;
padding: 0 6rpx;
.checkbox-box {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #c2c9d3;
border-radius: 8rpx;
margin-right: 16rpx;
margin-top: 4rpx;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.15s ease;
box-sizing: border-box;
justify-content: center;
align-items: center;
transition: all 0.15s ease;
box-sizing: border-box;
&.checked {
border-color: #00b074;
background-color: #00b074;
}
&.checked {
border-color: #00b074;
background-color: #00b074;
}
}
.agreement-text {
flex: 1;
font-size: 24rpx;
color: #7a8290;
line-height: 1.5;
.agreement-text {
flex: 1;
font-size: 24rpx;
color: #7a8290;
line-height: 1.5;
.link-text {
color: #8fc31f;
font-weight: 500;
}
.link-text {
color: #8fc31f;
font-weight: 500;
}
}
}
// 5. Submit Button
.submit-btn {
height: 104rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
letter-spacing: 2rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
// 5. Submit Button
.submit-btn {
height: 104rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
letter-spacing: 2rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
&.disabled-btn {
opacity: 0.75;
pointer-events: none; // 阻止后续的多余点击
}
&.disabled-btn {
opacity: 0.75;
pointer-events: none; // 阻止后续的多余点击
}
.btn-arrow {
margin-left: 10rpx;
}
.btn-arrow {
margin-left: 10rpx;
}
}
}
</style>
... ...
... ... @@ -7,14 +7,8 @@
<text class="form-label">手机号码</text>
<view class="input-box">
<u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
<input
type="number"
v-model="phone"
placeholder="请输入您的手机号"
placeholder-style="color: #a1a8b3"
maxlength="11"
class="native-input"
/>
<input type="number" v-model="phone" placeholder="请输入您的手机号" placeholder-style="color: #a1a8b3" maxlength="11"
class="native-input" />
</view>
</view>
... ... @@ -24,21 +18,11 @@
<view class="code-row">
<view class="input-box flex-1">
<u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
<input
type="number"
v-model="code"
placeholder="6位验证码"
placeholder-style="color: #a1a8b3"
maxlength="6"
class="native-input"
/>
<input type="number" v-model="code" placeholder="6位验证码" placeholder-style="color: #a1a8b3" maxlength="6"
class="native-input" />
</view>
<!-- 获取验证码按钮 -->
<view
class="code-btn"
:class="{ disabled: countdown > 0 || isSending }"
@click="handleSendCode"
>
<view class="code-btn" :class="{ disabled: countdown > 0 || isSending }" @click="handleSendCode">
<text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
</view>
</view>
... ... @@ -49,20 +33,10 @@
<text class="form-label">设置新密码</text>
<view class="input-box">
<u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
<input
:type="showPassword ? 'text' : 'password'"
v-model="password"
placeholder="请设置您的新密码"
placeholder-style="color: #a1a8b3"
class="native-input"
/>
<u-icon
:name="showPassword ? 'eye-fill' : 'eye'"
size="20"
color="#a1a8b3"
class="eye-icon"
@click="showPassword = !showPassword"
/>
<input :type="showPassword ? 'text' : 'password'" v-model="password" placeholder="请设置您的新密码"
placeholder-style="color: #a1a8b3" class="native-input" />
<u-icon :name="showPassword ? 'eye-fill' : 'eye'" size="20" color="#a1a8b3" class="eye-icon"
@click="showPassword = !showPassword" />
</view>
<text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text>
</view>
... ... @@ -72,20 +46,10 @@
<text class="form-label">确认密码</text>
<view class="input-box">
<u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
<input
:type="showConfirmPassword ? 'text' : 'password'"
v-model="confirmPassword"
placeholder="请再次输入新密码"
placeholder-style="color: #a1a8b3"
class="native-input"
/>
<u-icon
:name="showConfirmPassword ? 'eye-fill' : 'eye'"
size="20"
color="#a1a8b3"
class="eye-icon"
@click="showConfirmPassword = !showConfirmPassword"
/>
<input :type="showConfirmPassword ? 'text' : 'password'" v-model="confirmPassword" placeholder="请再次输入新密码"
placeholder-style="color: #a1a8b3" class="native-input" />
<u-icon :name="showConfirmPassword ? 'eye-fill' : 'eye'" size="20" color="#a1a8b3" class="eye-icon"
@click="showConfirmPassword = !showConfirmPassword" />
</view>
</view>
</view>
... ... @@ -98,256 +62,275 @@
</template>
<script setup>
import { ref } from 'vue';
import AuthUtil from '@/sheep/api/member/auth';
import { onHide } from '@dcloudio/uni-app';
const phone = ref('');
const code = ref('');
const password = ref('');
const confirmPassword = ref('');
// 密码显示隐藏控制
const showPassword = ref(false);
const showConfirmPassword = ref(false);
// 防抖及倒计时状态
const isSending = ref(false);
const isSubmitting = ref(false);
const countdown = ref(0);
let timer = null;
// 验证手机号码格式
const validatePhone = (num) => {
return /^1[3-9]\d{9}$/.test(num);
};
// 强密码复杂度检测:8~16位,大小写、数字、特殊字符
const validatePasswordStrength = (pwd) => {
const reg =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/;
return reg.test(pwd);
};
// 发送短信验证码
const handleSendCode = async () => {
if (countdown.value > 0 || isSending.value) return;
if (!phone.value) {
uni.showToast({ title: '请输入手机号码', icon: 'none' });
return;
}
if (!validatePhone(phone.value)) {
uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
return;
}
import { ref } from 'vue';
import AuthUtil from '@/sheep/api/member/auth';
import { onHide, onLoad } from '@dcloudio/uni-app';
const phone = ref('');
const code = ref('');
const password = ref('');
const confirmPassword = ref('');
// 密码显示隐藏控制
const showPassword = ref(false);
const showConfirmPassword = ref(false);
// 防抖及倒计时状态
const isSending = ref(false);
const isSubmitting = ref(false);
const countdown = ref(0);
let timer = null;
// 验证手机号码格式
const validatePhone = (num) => {
return /^1[3-9]\d{9}$/.test(num);
};
// 强密码复杂度检测:8~16位,大小写、数字、特殊字符
const validatePasswordStrength = (pwd) => {
const reg =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/;
return reg.test(pwd);
};
// 发送短信验证码
const handleSendCode = async () => {
if (countdown.value > 0 || isSending.value) return;
if (!phone.value) {
uni.showToast({ title: '请输入手机号码', icon: 'none' });
return;
}
if (!validatePhone(phone.value)) {
uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
return;
}
isSending.value = true;
try {
const res = await AuthUtil.sendSmsCode({ phone: phone.value });
if (res && (res.code === 0 || res.data === true)) {
uni.showToast({ title: '验证码已发送', icon: 'success' });
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} else {
uni.showToast({ title: res.msg || '获取验证码失败', icon: 'none' });
}
} catch (err) {
console.error('发送验证码失败异常:', err);
} finally {
isSending.value = false;
isSending.value = true;
try {
const res = await AuthUtil.sendSmsCode({ phone: phone.value });
if (res && (res.code === 0 || res.data === true)) {
uni.showToast({ title: '验证码已发送', icon: 'success' });
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} else {
uni.showToast({ title: res.msg || '获取验证码失败', icon: 'none' });
}
};
} catch (err) {
console.error('发送验证码失败异常:', err);
} finally {
isSending.value = false;
}
};
// 保存并提交重置密码
const handleSubmit = async () => {
if (isSubmitting.value) return;
// 保存并提交重置密码
const handleSubmit = async () => {
if (isSubmitting.value) return;
if (!phone.value || !validatePhone(phone.value)) {
uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
return;
}
if (!code.value || code.value.length < 4) {
uni.showToast({ title: '请输入短信验证码', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入您的新密码', icon: 'none' });
return;
}
// if (!validatePasswordStrength(password.value)) {
// uni.showToast({ title: '密码须为8~16位并包含大小写字母、数字及特殊字符', icon: 'none' });
// return;
// }
if (password.value !== confirmPassword.value) {
uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });
return;
}
if (!phone.value || !validatePhone(phone.value)) {
uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
return;
}
if (!code.value || code.value.length < 4) {
uni.showToast({ title: '请输入短信验证码', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入您的新密码', icon: 'none' });
return;
}
// if (!validatePasswordStrength(password.value)) {
// uni.showToast({ title: '密码须为8~16位并包含大小写字母、数字及特殊字符', icon: 'none' });
// return;
// }
if (password.value !== confirmPassword.value) {
uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });
return;
}
isSubmitting.value = true;
uni.showLoading({ title: '正在修改密码...', mask: true });
try {
const res = await AuthUtil.setPassword({
phone: phone.value,
code: code.value,
password: password.value,
});
uni.redirectTo({
url: '/pages7/pages/index/login',
});
} catch (err) {
console.error('重置密码运行异常:', err);
} finally {
isSubmitting.value = false;
uni.hideLoading();
}
};
isSubmitting.value = true;
uni.showLoading({ title: '正在修改密码...', mask: true });
try {
const res = await AuthUtil.setPassword({
phone: phone.value,
code: code.value,
password: password.value,
});
uni.redirectTo({
url: '/pages7/pages/index/login',
});
} catch (err) {
console.error('重置密码运行异常:', err);
} finally {
isSubmitting.value = false;
uni.hideLoading();
}
};
// 生命周期管理:清除定时器
onHide(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
const isForgetPassword = ref(false)
const isModify = ref(false);
onLoad((options) => {
// 路由参数是字符串,需要转布尔判断
isModify.value = options.isModify === 'true';
console.log('isModify.value=', isModify.value);
if (isModify.value) {
// 修改密码场景
uni.setNavigationBarTitle({ title: '修改密码' });
} else {
// 忘记密码场景
uni.setNavigationBarTitle({ title: '找回密码' });
}
});
// 生命周期管理:清除定时器
onHide(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
</script>
<style lang="scss" scoped>
.login-wrapper {
height: 100vh;
// #ifdef H5
height: calc(100vh - 44px);
// #endif
background-color: #ffffff;
padding: 40rpx 50rpx; // 移除顶部大 Logo 后,缩减顶部 Padding 保持呼吸感
box-sizing: border-box;
// 1. 表单容器
.form-container {
margin-top: 20rpx;
.form-item-wrapper {
margin-bottom: 36rpx;
.form-label {
font-size: 26rpx;
color: #0a1931;
font-weight: bold;
display: block;
margin-bottom: 16rpx;
}
.login-wrapper {
height: 100vh;
// #ifdef H5
height: calc(100vh - 44px);
// #endif
background-color: #ffffff;
padding: 40rpx 50rpx; // 移除顶部大 Logo 后,缩减顶部 Padding 保持呼吸感
box-sizing: border-box;
// 1. 表单容器
.form-container {
margin-top: 20rpx;
.form-item-wrapper {
margin-bottom: 36rpx;
.form-label {
font-size: 26rpx;
color: #0a1931;
font-weight: bold;
display: block;
margin-bottom: 16rpx;
}
// 输入框提示语
.input-hint {
display: block;
font-size: 24rpx;
color: #9097a3;
line-height: 1.5;
margin-top: 12rpx;
padding: 0 10rpx;
// 输入框提示语
.input-hint {
display: block;
font-size: 24rpx;
color: #9097a3;
line-height: 1.5;
margin-top: 12rpx;
padding: 0 10rpx;
}
.input-box {
height: 104rpx;
background-color: #f4f6fa;
border: 2rpx solid #e1e6eb;
border-radius: 24rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
box-sizing: border-box;
.input-icon {
margin-right: 16rpx;
}
.input-box {
height: 104rpx;
background-color: #f4f6fa;
border: 2rpx solid #e1e6eb;
border-radius: 24rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
box-sizing: border-box;
.eye-icon {
padding: 10rpx;
}
.input-icon {
margin-right: 16rpx;
}
.native-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #0a1931;
}
}
.eye-icon {
padding: 10rpx;
}
// 验证码双列布局
.code-row {
display: flex;
align-items: center;
.native-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #0a1931;
}
.flex-1 {
flex: 1;
}
// 验证码双列布局
.code-row {
.code-btn {
width: 220rpx;
height: 104rpx;
border-radius: 24rpx;
background-color: #f9fdf2;
border: 2rpx solid #d0ee9c;
display: flex;
justify-content: center;
align-items: center;
margin-left: 20rpx;
font-size: 26rpx;
color: #8fc31f;
font-weight: bold;
transition: all 0.2s ease;
.flex-1 {
flex: 1;
&:active {
opacity: 0.8;
}
.code-btn {
width: 220rpx;
height: 104rpx;
border-radius: 24rpx;
background-color: #f9fdf2;
border: 2rpx solid #d0ee9c;
display: flex;
justify-content: center;
align-items: center;
margin-left: 20rpx;
font-size: 26rpx;
color: #8fc31f;
font-weight: bold;
transition: all 0.2s ease;
&:active {
opacity: 0.8;
}
&.disabled {
color: #b0b8c4;
background-color: #f4f6fa;
border-color: #e1e6eb;
pointer-events: none;
}
&.disabled {
color: #b0b8c4;
background-color: #f4f6fa;
border-color: #e1e6eb;
pointer-events: none;
}
}
}
}
}
// 2. 确认修改按钮 (登录页同款高级渐变微立体阴影)
.submit-btn {
height: 104rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
letter-spacing: 2rpx;
margin-top: 80rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
// 2. 确认修改按钮 (登录页同款高级渐变微立体阴影)
.submit-btn {
height: 104rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
letter-spacing: 2rpx;
margin-top: 80rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
&.disabled-btn {
opacity: 0.75;
pointer-events: none;
}
&.disabled-btn {
opacity: 0.75;
pointer-events: none;
}
.btn-arrow {
margin-left: 10rpx;
}
.btn-arrow {
margin-left: 10rpx;
}
}
}
</style>
... ...
... ... @@ -94,8 +94,6 @@ const ExercisesApi = {
//更新动作
updateexercises: (updateData) => {
const url = '/app/motion/categories/update';
console.log('请求URL:', url);
return request({
url: '/app/motion/exercises/update',
method: 'PUT',
... ...
... ... @@ -4,7 +4,7 @@ const SettingApi = {
// 获取设置
getSetting: () => {
return request({
url: `/app/student/setting`,
url: `/app/user/setting`,
method: 'GET',
});
},
... ...
... ... @@ -392,6 +392,21 @@ export const useTrainingStore = defineStore('training', {
setTrainingTimeText(val) {
this.trainingTimeText = val;
},
// 每日模板修改的时候使用
resetContentOnly() {
this.id = null;
this.type = null;
this.actionDetail = {};
this.loading = false;
this.unitRecords = {};
this.trainingName = '';
this.dailyTemplateId = null;
this.isSystem = 1;
// 下面这些字段完全不碰,计时器自然运行:
// totalSeconds, isPause, timerInterval, trainingTimeText,
// showPicker, defaultTimeIndex, min, isTraining
},
// 清空
clearTrainingStore() {
this.clearTimer();
... ... @@ -403,6 +418,10 @@ export const useTrainingStore = defineStore('training', {
this.trainingTimeText = '';
this.min = false;
this.isSystem = 1;
this.trainingName = '';
this.defaultTimeIndex = [0, 0, 0];
this.showPicker = false;
this.isTraining = false;
},
},
... ...