Authored by qxm

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

@@ -4,7 +4,7 @@ SHOPRO_VERSION=v2.4.1 @@ -4,7 +4,7 @@ SHOPRO_VERSION=v2.4.1
4 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development) 4 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
5 # SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn 5 # SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
6 # SHOPRO_BASE_URL=http://mall.hcxtec.com 6 # SHOPRO_BASE_URL=http://mall.hcxtec.com
7 -SHOPRO_BASE_URL=https://xunji.geaktec.com 7 +# SHOPRO_BASE_URL=https://xunji.geaktec.com
8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development) 8 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
9 # SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081 9 # SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080 10 # SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
  1 +# 2026-06-23 工作日志
  2 +
  3 +## 悬浮球快照方案 — 完整改造方案
  4 +
  5 +分析了 exercising 健身小程序中 trainingStore(Pinia)被三个功能模块(动作训练、超级组、每日模板编辑/修改)共享导致的数据冲突问题。
  6 +
  7 +### 问题根因
  8 +- `grid-cell-content-popup.vue` 的编辑按钮 (`templateEdit`) 调用 `loadDailyTemplateForEdit()` 覆盖 store
  9 +- `wode-xinjian-moban.vue` 的 `goBack()` / `handleSave()` 调用 `clearTrainingStore()` 清空 store
  10 +- `meiri-moban-xiugai.vue` 的修改弹窗(弹窗模式)已通过快照解决,但编辑和新建是页面跳转模式,快照来不及归还
  11 +
  12 +### 最终方案:悬浮球持有快照
  13 +
  14 +核心思路:最小化时将 trainingStore 全量深拷贝到共享 reactive 模块 `trainingSnapshot`(存在 JS 堆内存,非 Pinia),悬浮球由此独立控制显隐和回显。
  15 +
  16 +改动清单:
  17 +1. **新建** `sheep/store/trainingSnapshot.js` — 共享 reactive 快照模块
  18 +2. **重写** `pages/TrainingFloating.vue` — 自有 showBall + 回显逻辑 + 计时器补偿
  19 +3. **修改** `pages4/.../xunji-dongzuo-lianxi.vue` openMin() — 保存快照到共享模块
  20 +4. **删除** `sheep/store/trainingStore.js` clearTrainingStore() 中 `this.min = false`
  21 +5. **不改** grid-cell-content-popup.vue 和 wode-xinjian-moban.vue
  22 +6. **修复** meiri-moban-xiugai.vue 中 Object.assign → $patch
  23 +
  24 +总计约 113 行,4 文件改动,2 文件不动。
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 <up-tabbar 3 <up-tabbar
4 :value="activePath" 4 :value="activePath"
5 @change="handleTabChange" 5 @change="handleTabChange"
6 - :placeholder="true" 6 + :placeholder="false"
7 activeColor="#000" 7 activeColor="#000"
8 :fixed="true" 8 :fixed="true"
9 > 9 >
1 { 1 {
2 "name": "智能健身", 2 "name": "智能健身",
3 - "appid": "__UNI__8F24C84", 3 + "appid": "__UNI__7CAA18B",
4 "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。", 4 "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
5 "versionName": "2025.10", 5 "versionName": "2025.10",
6 "versionCode": "183", 6 "versionCode": "183",
@@ -183,7 +183,7 @@ @@ -183,7 +183,7 @@
183 "versionCode": 100 183 "versionCode": 100
184 }, 184 },
185 "mp-weixin": { 185 "mp-weixin": {
186 - "appid": "wxb827c923ce0aad4b", 186 + "appid": "wxa54e6a259fb3338d",
187 "setting": { 187 "setting": {
188 "urlCheck": true, 188 "urlCheck": true,
189 "minified": true, 189 "minified": true,
@@ -24,7 +24,7 @@ const goToTrainingPage = () => { @@ -24,7 +24,7 @@ const goToTrainingPage = () => {
24 // 悬浮球消失 24 // 悬浮球消失
25 trainingStore.min = false; 25 trainingStore.min = false;
26 uni.navigateTo({ 26 uni.navigateTo({
27 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}`, 27 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}&isTraining=true`,
28 }); 28 });
29 }; 29 };
30 30
@@ -148,7 +148,7 @@ const FUNCTION_CONFIG_LIST = [ @@ -148,7 +148,7 @@ const FUNCTION_CONFIG_LIST = [
148 }, 148 },
149 149
150 { 150 {
151 - text: '关于鸿星', 151 + text: '关于我们',
152 url: '/pages5/pages/user/wode-guanyu-hongxing', 152 url: '/pages5/pages/user/wode-guanyu-hongxing',
153 icon: '/static/icons/antOutline-exclamation-circle 1@1x.png', 153 icon: '/static/icons/antOutline-exclamation-circle 1@1x.png',
154 }, 154 },
@@ -218,11 +218,14 @@ const fetchPageData = async () => { @@ -218,11 +218,14 @@ const fetchPageData = async () => {
218 // UserApi.getUserInfo(), 218 // UserApi.getUserInfo(),
219 // // MemberApi.getMemberLevel(), 219 // // MemberApi.getMemberLevel(),
220 // ]); 220 // ]);
221 - const userRes = UserApi.getUserInfo();  
222 221
  222 + const userRes = await UserApi.getUserInfo();
  223 +
  224 + console.log('后端返回个人信息:', userRes.data);
223 const rawUser = userRes.data || {}; 225 const rawUser = userRes.data || {};
224 userInfo.value = rawUser; 226 userInfo.value = rawUser;
225 227
  228 +
226 // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化 229 // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化
227 // const levels = levelRes.data?.detailList || []; 230 // const levels = levelRes.data?.detailList || [];
228 // if (rawUser.level !== undefined) { 231 // if (rawUser.level !== undefined) {
1 <template> 1 <template>
2 - <up-popup :show="show" mode="bottom" round="16" :safeAreaInsetBottom="false">  
3 - <view class="desc-container">  
4 - <view class="title">  
5 - <view @click="show = false">  
6 - <up-icon name="close" color="#fff" size="20"></up-icon> 2 + <!-- 全屏遮罩层 -->
  3 + <view v-if="show" class="mask" @click="maskClose">
  4 + <!-- 底部弹窗主体 -->
  5 + <view class="popup-wrap" @click.stop>
  6 + <view class="desc-container">
  7 + <view class="title">
  8 + <view @click="show = false">
  9 + <up-icon name="close" color="#fff" size="20"></up-icon>
  10 + </view>
  11 + <text class="titleName">动作备注</text>
  12 + <view class="btn" @click="saveNoteContent">保存</view>
  13 + </view>
  14 + <view class="area-box">
  15 + <!-- <up-textarea v-model="tempNoteContent" placeholder="此处填写个人备注" class="noteConten"
  16 + customStyle="border-radius:12rpx; padding:20rpx;background: #242424;" /> -->
  17 + <textarea class="textarea" v-model="tempNoteContent" placeholder="此处填写个人备注" />
7 </view> 18 </view>
8 - <text class="titleName">  
9 - 动作备注  
10 - </text>  
11 - <view class="btn" @click="saveNoteContent">保存</view>  
12 - </view>  
13 - <view class="input-box">  
14 - <up-textarea v-model="tempNoteContent" placeholder="此处填写个人备注" class="noteConten"  
15 - customStyle="border-radius:12rpx; padding:20rpx;background: #242424;" />  
16 </view> 19 </view>
17 </view> 20 </view>
18 - </up-popup> 21 + </view>
19 </template> 22 </template>
20 23
21 <script setup> 24 <script setup>
@@ -25,35 +28,83 @@ const show = ref(false); @@ -25,35 +28,83 @@ const show = ref(false);
25 const tempNoteContent = ref(''); // 临时编辑的备注内容 28 const tempNoteContent = ref(''); // 临时编辑的备注内容
26 29
27 const emit = defineEmits(['saveSuccess']); 30 const emit = defineEmits(['saveSuccess']);
  31 +
  32 +const props = defineProps({
  33 + oldNote: {
  34 + type: String,
  35 + default: ''
  36 + }
  37 +})
  38 +
  39 +
28 // 保存备注 40 // 保存备注
29 const saveNoteContent = async () => { 41 const saveNoteContent = async () => {
30 const content = tempNoteContent.value.trim(); 42 const content = tempNoteContent.value.trim();
31 - // 如果内容为空,直接返回,不提交  
32 if (!content) { 43 if (!content) {
33 uni.showToast({ title: '备注不能为空', icon: 'none' }); 44 uni.showToast({ title: '备注不能为空', icon: 'none' });
34 return; 45 return;
35 } 46 }
36 emit('saveSuccess', content); 47 emit('saveSuccess', content);
37 - // 关闭弹窗  
38 show.value = false; 48 show.value = false;
39 }; 49 };
40 50
  51 +// 打开弹窗
41 const open = () => { 52 const open = () => {
  53 + tempNoteContent.value = props.oldNote
42 show.value = true; 54 show.value = true;
43 }; 55 };
44 56
  57 +// 遮罩点击关闭
  58 +const maskClose = () => {
  59 + show.value = false;
  60 +};
  61 +
45 defineExpose({ open }); 62 defineExpose({ open });
46 </script> 63 </script>
47 64
48 <style lang="scss" scoped> 65 <style lang="scss" scoped>
  66 +// 全屏灰色遮罩
  67 +.mask {
  68 + position: fixed;
  69 + top: 0;
  70 + left: 0;
  71 + right: 0;
  72 + bottom: 0;
  73 + background: rgba(0, 0, 0, 0.6);
  74 + z-index: 999;
  75 + display: flex;
  76 + flex-direction: column;
  77 + justify-content: flex-end;
  78 +}
  79 +
  80 +// 底部弹窗容器,控制弹出动画、圆角、背景
  81 +.popup-wrap {
  82 + width: 100%;
  83 + border-radius: 16rpx 16rpx 0 0; // 对应原来 round="16"
  84 + overflow: hidden;
  85 + // 弹出动画可选,不需要可删除
  86 + animation: popUp 0.24s ease-out;
  87 +}
  88 +
  89 +@keyframes popUp {
  90 + from {
  91 + transform: translateY(100%);
  92 + }
  93 +
  94 + to {
  95 + transform: translateY(0);
  96 + }
  97 +}
  98 +
49 .desc-container { 99 .desc-container {
50 - background-color: #1a1a1a; 100 + background-color: #1a1a1a; // 这里直接改弹窗背景色,非常方便
51 padding: 40rpx 30rpx 40rpx; 101 padding: 40rpx 30rpx 40rpx;
52 height: 75vh; 102 height: 75vh;
53 103
54 .title { 104 .title {
55 display: flex; 105 display: flex;
56 justify-content: space-between; 106 justify-content: space-between;
  107 + align-items: center;
57 font-weight: 500; 108 font-weight: 500;
58 text-align: center; 109 text-align: center;
59 color: #fff; 110 color: #fff;
@@ -61,6 +112,24 @@ defineExpose({ open }); @@ -61,6 +112,24 @@ defineExpose({ open });
61 margin-bottom: 40rpx; 112 margin-bottom: 40rpx;
62 } 113 }
63 114
  115 + .titleName {
  116 + flex: 1;
  117 + text-align: center;
  118 + }
  119 +
  120 + .area-box {
  121 + .textarea {
  122 + width: 95%;
  123 + height: 300rpx;
  124 + background: #444;
  125 + color: #fff;
  126 + border-radius: 16rpx;
  127 + padding: 20rpx;
  128 + font-size: 30rpx;
  129 + border: 0;
  130 + }
  131 + }
  132 +
64 .btn { 133 .btn {
65 width: 100rpx; 134 width: 100rpx;
66 height: 50rpx; 135 height: 50rpx;
@@ -72,15 +141,5 @@ defineExpose({ open }); @@ -72,15 +141,5 @@ defineExpose({ open });
72 justify-content: center; 141 justify-content: center;
73 font-size: 28rpx; 142 font-size: 28rpx;
74 } 143 }
75 -  
76 - .input-box {  
77 - margin-bottom: 60rpx;  
78 -  
79 - // 输入文字白色  
80 - :deep(.u-textarea__field) {  
81 - color: #fff !important;  
82 - }  
83 -  
84 - }  
85 } 144 }
86 -</style> 145 +</style>
@@ -20,13 +20,13 @@ @@ -20,13 +20,13 @@
20 <view v-for="(item, index) in sortList" :key="item.unitId || index" class="drag-item-card"> 20 <view v-for="(item, index) in sortList" :key="item.unitId || index" class="drag-item-card">
21 <view class="item-left"> 21 <view class="item-left">
22 <up-icon name="trash" color="#fff" size="22" @click="deleteUnit(item, index)"></up-icon> 22 <up-icon name="trash" color="#fff" size="22" @click="deleteUnit(item, index)"></up-icon>
23 - <image class="item-img" :src="item.exercises?.[0]?.urlImage || ''" v-if="item.unitType === 1"  
24 - mode="aspectFill" /> 23 + <image class="item-img" :src="item.exercises?.[0]?.url3dAnimation || item.exercises?.[0]?.exerciseCover ||
  24 + item.exercises?.[0]?.urlImage" v-if="item.unitType === 1" mode="aspectFill" />
25 <image class="item-img" 25 <image class="item-img"
26 src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png" v-else 26 src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png" v-else
27 mode="aspectFill" /> 27 mode="aspectFill" />
28 <view class="item-info"> 28 <view class="item-info">
29 - <view class="item-name">{{ item.unitName || '未命名动作' }}</view> 29 + <view class="item-name">{{ item.unitName || item.exercises?.[0]?.exerciseName || '未命名动作' }}</view>
30 <view class="item-tags" v-if="item.unitType === 2">超级组</view> 30 <view class="item-tags" v-if="item.unitType === 2">超级组</view>
31 <view class="item-tags" v-if="item.unitType === 1"> 31 <view class="item-tags" v-if="item.unitType === 1">
32 {{ item.exercises?.[0]?.categoryDescription || '' }} 32 {{ item.exercises?.[0]?.categoryDescription || '' }}
@@ -56,6 +56,7 @@ const trainingStore = useTrainingStore() @@ -56,6 +56,7 @@ const trainingStore = useTrainingStore()
56 // --- 动作排序 ---弹窗 56 // --- 动作排序 ---弹窗
57 const actionSortShow = ref(false); 57 const actionSortShow = ref(false);
58 const sortList = ref([]) 58 const sortList = ref([])
  59 +
59 // 这里是打开动作排序的方法 60 // 这里是打开动作排序的方法
60 const openActionSort = () => { 61 const openActionSort = () => {
61 sortList.value = trainingStore.actionDetail?.units?.map((unit, index) => ({ 62 sortList.value = trainingStore.actionDetail?.units?.map((unit, index) => ({
@@ -63,8 +64,12 @@ const openActionSort = () => { @@ -63,8 +64,12 @@ const openActionSort = () => {
63 exercises: unit.exercises || [] 64 exercises: unit.exercises || []
64 })) || []; 65 })) || [];
65 actionSortShow.value = true; 66 actionSortShow.value = true;
  67 +
  68 + console.log('打印排序列表:', sortList.value);
66 }; 69 };
67 70
  71 +
  72 +
68 // const addActionsPopup = () => { 73 // const addActionsPopup = () => {
69 // if (actionSortShow.value === true) { 74 // if (actionSortShow.value === true) {
70 // actionSortShow.value = false 75 // actionSortShow.value = false
@@ -515,34 +515,6 @@ const toggleSuperSetActive = (setIndex) => { @@ -515,34 +515,6 @@ const toggleSuperSetActive = (setIndex) => {
515 } 515 }
516 }) 516 })
517 } 517 }
518 -// 增加超级组的set组数  
519 -const addSuperSet = () => {  
520 - const exercises = props.actionDetail?.exercises || []  
521 - exercises.forEach(sub => {  
522 - if (superRecordMap.value[sub.id]) {  
523 - superRecordMap.value[sub.id].push({  
524 - h: '00', m: '00', s: '00',  
525 - quickTimeDisplay: '60s',  
526 - distance: '',  
527 - weight: '',  
528 - reps: '',  
529 - duration: '',  
530 - restTime: '',  
531 - isActive: false  
532 - })  
533 - }  
534 - })  
535 -}  
536 -// 删除超级组的组数  
537 -const deleteSuperSet = (setIndex) => {  
538 - const exercises = props.actionDetail?.exercises || []  
539 - exercises.forEach(sub => {  
540 - const list = superRecordMap.value[sub.id]  
541 - if (list && list.length > 1) {  
542 - list.splice(setIndex, 1)  
543 - }  
544 - })  
545 -}  
546 518
547 // 合并后的 Picker 逻辑 519 // 合并后的 Picker 逻辑
548 const timeColumns = reactive([ 520 const timeColumns = reactive([
@@ -652,12 +624,63 @@ const addRow = () => { @@ -652,12 +624,63 @@ const addRow = () => {
652 restTime: '', isActive: false, 624 restTime: '', isActive: false,
653 }); 625 });
654 }; 626 };
  627 +
  628 +// 删除单动作行数,至少保留一行,提示。
655 const deleteRow = (index) => { 629 const deleteRow = (index) => {
656 if (recordList.value.length > 1) { 630 if (recordList.value.length > 1) {
657 recordList.value.splice(index, 1); 631 recordList.value.splice(index, 1);
  632 + } else {
  633 + uni.showToast({
  634 + title: '至少保留一行数据',
  635 + icon: 'none'
  636 + })
658 } 637 }
659 }; 638 };
660 639
  640 +// 增加超级组的set组数
  641 +const addSuperSet = () => {
  642 + const exercises = props.actionDetail?.exercises || []
  643 + exercises.forEach(sub => {
  644 + if (superRecordMap.value[sub.id]) {
  645 + superRecordMap.value[sub.id].push({
  646 + h: '00', m: '00', s: '00',
  647 + quickTimeDisplay: '60s',
  648 + distance: '',
  649 + weight: '',
  650 + reps: '',
  651 + duration: '',
  652 + restTime: '',
  653 + isActive: false
  654 + })
  655 + }
  656 + })
  657 +}
  658 +// 删除超级组的组数
  659 +const deleteSuperSet = (setIndex) => {
  660 + const exercises = props.actionDetail?.exercises || []
  661 +
  662 + // 没有子动作直接返回
  663 + if (!exercises.length) return
  664 + // 取第一个子动作的组数,所有子动作数组长度保持同步
  665 + const firstSubId = exercises[0].id
  666 + const list = superRecordMap.value[firstSubId] || []
  667 + // 判断只剩一组,弹窗提示并终止删除
  668 + if (list.length <= 1) {
  669 + uni.showToast({
  670 + title: '至少保留一组数据',
  671 + icon: 'none'
  672 + })
  673 + return
  674 + }
  675 +
  676 + exercises.forEach(sub => {
  677 + const list = superRecordMap.value[sub.id]
  678 + if (list && list.length > 1) {
  679 + list.splice(setIndex, 1)
  680 + }
  681 + })
  682 +}
  683 +
661 const addGroup = () => { 684 const addGroup = () => {
662 if (props.type === 1) { 685 if (props.type === 1) {
663 addRow() 686 addRow()
@@ -52,12 +52,20 @@ @@ -52,12 +52,20 @@
52 <view class="title-bar"> 52 <view class="title-bar">
53 <text class="title">{{ actionDetail?.name }}</text> 53 <text class="title">{{ actionDetail?.name }}</text>
54 <view class="action-icons"> 54 <view class="action-icons">
55 - <!-- <up-icon name="share-square" color="#fff" size="24"></up-icon> --> 55 +
  56 + <!-- 只有个人创建的动作,才有这个编辑按钮-->
  57 + <template v-if="actionDetail?.isSystem === 0">
  58 + <view class="edit-icon" @click="edit">
  59 + <uni-icons type="more" size="24" color="#fff"></uni-icons>
  60 + </view>
  61 + </template>
  62 +
56 <button class="share-btn" open-type="share"> 63 <button class="share-btn" open-type="share">
57 <uni-icons type="paperplane" size="24" color="#fff"></uni-icons> 64 <uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
58 </button> 65 </button>
59 <up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24" 66 <up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24"
60 - @click="toggleCollect"></up-icon> 67 + @click="toggleCollect">
  68 + </up-icon>
61 <!-- <FavoriteBtn :id="actionId" :type="type" /> --> 69 <!-- <FavoriteBtn :id="actionId" :type="type" /> -->
62 </view> 70 </view>
63 </view> 71 </view>
@@ -91,11 +99,35 @@ @@ -91,11 +99,35 @@
91 </view> 99 </view>
92 100
93 <view class="memo-box" @click="openBeizhu"> 101 <view class="memo-box" @click="openBeizhu">
94 - <view class="section-title">训练备注</view>  
95 - <up-textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注" autoHeight  
96 - customStyle="background: transparent; border: none; padding: 10rpx 0;" placeholderStyle="color: #666"  
97 - disabled border="none"></up-textarea> 102 + <view class="memo-title">
  103 + <view class="section-title">训练备注</view>
  104 + <view class="pencil-icon">
  105 + <up-icon name="edit-pen" color="#fedc1f" size="20"></up-icon>
  106 + </view>
  107 + </view>
  108 +
  109 + <template v-if="type == 1">
  110 + <textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea>
  111 + </template>
  112 +
  113 + <template v-if="type == 2">
  114 + <textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea>
  115 + </template>
  116 +
  117 + </view>
  118 + <view class="memo-box" v-if="type == 1">
  119 + <view class="memo-title">
  120 + <view class="section-title">步骤</view>
  121 + <view class="pencil-icon">
  122 + <up-icon name="edit-pen" color="#fedc1f" size="20"></up-icon>
  123 + </view>
  124 + </view>
  125 + <view class="steps-list">
  126 + <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>
  127 + </view>
98 </view> 128 </view>
  129 +
  130 +
99 <!-- 动作列表,只有超级组才有 --> 131 <!-- 动作列表,只有超级组才有 -->
100 <view class="section" v-if="type === 2"> 132 <view class="section" v-if="type === 2">
101 <view class="section-title">动作列表</view> 133 <view class="section-title">动作列表</view>
@@ -119,12 +151,7 @@ @@ -119,12 +151,7 @@
119 </view> 151 </view>
120 </view> 152 </view>
121 153
122 - <view class="section" v-if="type == 1">  
123 - <view class="section-title">步骤</view>  
124 - <view class="steps-list">  
125 - <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>  
126 - </view>  
127 - </view> 154 +
128 155
129 <view class="section"> 156 <view class="section">
130 <view class="section-title">训练部位</view> 157 <view class="section-title">训练部位</view>
@@ -206,7 +233,7 @@ @@ -206,7 +233,7 @@
206 </view> 233 </view>
207 </scroll-view> 234 </scroll-view>
208 <!-- 备注弹窗组件 --> 235 <!-- 备注弹窗组件 -->
209 - <beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" /> 236 + <beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" :old-note="actionDetail.note" />
210 </up-popup> 237 </up-popup>
211 </template> 238 </template>
212 239
@@ -217,6 +244,10 @@ import beizhu from '@/pages/xunji/components/beizhu.vue'; @@ -217,6 +244,10 @@ import beizhu from '@/pages/xunji/components/beizhu.vue';
217 import SupersetsApi from '@/sheep/api/motion/supersets'; 244 import SupersetsApi from '@/sheep/api/motion/supersets';
218 import TrainingApi from '@/sheep/api/Training/traininghistory'; 245 import TrainingApi from '@/sheep/api/Training/traininghistory';
219 import { onShareAppMessage } from '@dcloudio/uni-app'; 246 import { onShareAppMessage } from '@dcloudio/uni-app';
  247 +import { useTrainingStore } from '@/sheep/store/trainingStore'
  248 +
  249 +
  250 +const trainingStore = useTrainingStore()
220 251
221 // 静态配置 252 // 静态配置
222 253
@@ -266,23 +297,32 @@ const checkExerciseFavorited = async () => { @@ -266,23 +297,32 @@ const checkExerciseFavorited = async () => {
266 try { 297 try {
267 const res = await ExercisesApi.checkExerciseFavorited(actionId.value); 298 const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
268 isFavorite.value = res.data; 299 isFavorite.value = res.data;
  300 + console.log('获得动作的收藏状态', res.data);
269 } catch (err) { 301 } catch (err) {
270 console.log(err); 302 console.log(err);
271 } 303 }
272 }; 304 };
  305 +
273 // 收藏 306 // 收藏
274 const toggleCollect = async () => { 307 const toggleCollect = async () => {
275 try { 308 try {
276 const status = isFavorite.value ? 0 : 1; 309 const status = isFavorite.value ? 0 : 1;
277 - if (type == 1) { 310 + console.log('actionId.value=', actionId.value, 'status=', status);
  311 + if (type.value == 1) {
  312 + console.log('动作收藏传递的参数', actionId.value, status);
278 await ExercisesApi.toggleFavorite(actionId.value, status); 313 await ExercisesApi.toggleFavorite(actionId.value, status);
  314 +
279 } else { 315 } else {
  316 + console.log('超级组收藏传递的参数', actionId.value, status);
280 await SupersetsApi.toggleFavorite(actionId.value, status); 317 await SupersetsApi.toggleFavorite(actionId.value, status);
281 } 318 }
282 -  
283 isFavorite.value = !isFavorite.value; 319 isFavorite.value = !isFavorite.value;
  320 + uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' });
284 } catch (err) { 321 } catch (err) {
285 console.log(err); 322 console.log(err);
  323 + // 请求失败强制回滚UI
  324 + // isFavorite.value = originState;
  325 + uni.showToast({ title: '操作失败,请重试', icon: 'none' });
286 } 326 }
287 }; 327 };
288 328
@@ -297,8 +337,21 @@ const checkSupersetFavorited = async () => { @@ -297,8 +337,21 @@ const checkSupersetFavorited = async () => {
297 }; 337 };
298 338
299 const startTraining = () => { 339 const startTraining = () => {
  340 +
  341 + if (trainingStore.isTraining) {
  342 + uni.showToast({
  343 + title: '当前已有正在进行的训练',
  344 + icon: 'none'
  345 + });
  346 + return;
  347 + }
  348 +
  349 + console.log('动作详情页面的trainingStore.isTraining', trainingStore.isTraining);
  350 + trainingStore.isTraining = true;
  351 + console.log('动作详情页面的trainingStore.isTraining', trainingStore.isTraining);
  352 +
300 uni.navigateTo({ 353 uni.navigateTo({
301 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}`, 354 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}&isTraining=true`,
302 }); 355 });
303 }; 356 };
304 357
@@ -325,9 +378,13 @@ const open = async (id, typeData) => { @@ -325,9 +378,13 @@ const open = async (id, typeData) => {
325 if (typeData == 1) { 378 if (typeData == 1) {
326 await loadexercisedetail(actionId.value); 379 await loadexercisedetail(actionId.value);
327 // await loadAlternativeActions(actionId.value); 380 // await loadAlternativeActions(actionId.value);
  381 + console.log('进入动作收藏检查接口');
328 await checkExerciseFavorited(); 382 await checkExerciseFavorited();
  383 +
  384 +
329 } else { 385 } else {
330 await loadsuperdetail(actionId.value); 386 await loadsuperdetail(actionId.value);
  387 + console.log('进入超级组收藏检查接口');
331 await checkSupersetFavorited(); 388 await checkSupersetFavorited();
332 } 389 }
333 390
@@ -351,6 +408,10 @@ const openBeizhu = () => { @@ -351,6 +408,10 @@ const openBeizhu = () => {
351 408
352 // 接收子组件传过来的备注内容 409 // 接收子组件传过来的备注内容
353 const handleNoteSave = async (content) => { 410 const handleNoteSave = async (content) => {
  411 +
  412 + console.log('备注内容:', content);
  413 + console.log('type.value=', type.value);
  414 +
354 try { 415 try {
355 if (type.value == 2) { 416 if (type.value == 2) {
356 await SupersetsApi.addNotes({ 417 await SupersetsApi.addNotes({
@@ -367,7 +428,7 @@ const handleNoteSave = async (content) => { @@ -367,7 +428,7 @@ const handleNoteSave = async (content) => {
367 } catch (e) { 428 } catch (e) {
368 console.log(e); 429 console.log(e);
369 } finally { 430 } finally {
370 - if (type == 1) { 431 + if (type.value == 1) {
371 await loadexercisedetail(actionId.value) 432 await loadexercisedetail(actionId.value)
372 } else { 433 } else {
373 await loadsuperdetail(actionId.value) 434 await loadsuperdetail(actionId.value)
@@ -398,6 +459,7 @@ onShareAppMessage((res) => { @@ -398,6 +459,7 @@ onShareAppMessage((res) => {
398 const loadexercisedetail = async (id) => { 459 const loadexercisedetail = async (id) => {
399 const response = await ExercisesApi.getExerciseById(id); 460 const response = await ExercisesApi.getExerciseById(id);
400 actionDetail.value = response.data; 461 actionDetail.value = response.data;
  462 + console.log('显示动作详情:', actionDetail.value);
401 if (actionDetail.value.url3dAnimation) { 463 if (actionDetail.value.url3dAnimation) {
402 modeTab.value = 0; 464 modeTab.value = 0;
403 } else if (actionDetail.value.urlRealPerson) { 465 } else if (actionDetail.value.urlRealPerson) {
@@ -502,6 +564,12 @@ const formatTime = (seconds) => { @@ -502,6 +564,12 @@ const formatTime = (seconds) => {
502 return hh + mm + ss; 564 return hh + mm + ss;
503 }; 565 };
504 566
  567 +const edit = () => {
  568 + uni.navigateTo({
  569 + url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}`
  570 + });
  571 +};
  572 +
505 573
506 // 点击超级组内部的动作 → 打开动作详情(复用同一个组件) 574 // 点击超级组内部的动作 → 打开动作详情(复用同一个组件)
507 const openActionItem = (item) => { 575 const openActionItem = (item) => {
@@ -659,6 +727,10 @@ onMounted(() => { }); @@ -659,6 +727,10 @@ onMounted(() => { });
659 color: #ddd; 727 color: #ddd;
660 } 728 }
661 729
  730 + .pencil-icon {
  731 + margin-bottom: 24rpx;
  732 + }
  733 +
662 .action-list { 734 .action-list {
663 .action-item { 735 .action-item {
664 display: flex; 736 display: flex;
@@ -730,17 +802,23 @@ onMounted(() => { }); @@ -730,17 +802,23 @@ onMounted(() => { });
730 } 802 }
731 803
732 .memo-box { 804 .memo-box {
733 - background: #262626; 805 + background: #464646;
734 padding: 24rpx; 806 padding: 24rpx;
735 border-radius: 16rpx; 807 border-radius: 16rpx;
736 margin-bottom: 40rpx; 808 margin-bottom: 40rpx;
737 809
738 - .textarea {  
739 - height: 50rpx; 810 + .memo-title {
  811 + display: flex;
  812 + }
740 813
741 - :deep(.u-textarea--disabled) {  
742 - background-color: #262626;  
743 - } 814 + .textarea {
  815 + width: 95%;
  816 + color: #fff;
  817 + height: 160rpx;
  818 + border-radius: 16rpx;
  819 + // padding: 20rpx;
  820 + font-size: 30rpx;
  821 + border: 0;
744 } 822 }
745 } 823 }
746 824
@@ -748,13 +826,13 @@ onMounted(() => { }); @@ -748,13 +826,13 @@ onMounted(() => { });
748 display: flex; 826 display: flex;
749 flex-direction: column; 827 flex-direction: column;
750 gap: 16rpx; 828 gap: 16rpx;
751 - background: #262626;  
752 - padding: 20rpx; 829 + background: #464646;
  830 + // padding: 20rpx;
753 box-sizing: border-box; 831 box-sizing: border-box;
754 border-radius: 16rpx; 832 border-radius: 16rpx;
755 833
756 .step-text { 834 .step-text {
757 - font-size: 26rpx; 835 + font-size: 30rpx;
758 line-height: 1.6; 836 line-height: 1.6;
759 color: #bbb; 837 color: #bbb;
760 list-style: none; 838 list-style: none;
@@ -5,7 +5,8 @@ @@ -5,7 +5,8 @@
5 <view class="popup-options"> 5 <view class="popup-options">
6 <view class="popup-option" @click="handleAddFromPlan" v-if="planListCount > 0"> 6 <view class="popup-option" @click="handleAddFromPlan" v-if="planListCount > 0">
7 <view class=" icon-wrap"> 7 <view class=" icon-wrap">
8 - <image src="/static/icons/plan.png" mode="aspectFit"></image> 8 + <!-- <image src="/static/icons/plan.png" mode="aspectFit"></image> -->
  9 + <up-icon name="file-text-fill" color="#040000" size="24"></up-icon>
9 </view> 10 </view>
10 <view class="text-wrap" @click=""> 11 <view class="text-wrap" @click="">
11 <text class="option-title">从训练计划中添加</text> 12 <text class="option-title">从训练计划中添加</text>
@@ -15,7 +16,7 @@ @@ -15,7 +16,7 @@
15 16
16 <view class="popup-option" @click="handleAddFromTemplate"> 17 <view class="popup-option" @click="handleAddFromTemplate">
17 <view class="icon-wrap"> 18 <view class="icon-wrap">
18 - <image src="/static/icons/template.png" mode="aspectFit"></image> 19 + <up-icon name="grid-fill" color="#040000" size="24"></up-icon>
19 </view> 20 </view>
20 <view class="text-wrap"> 21 <view class="text-wrap">
21 <text class="option-title">使用训练模板</text> 22 <text class="option-title">使用训练模板</text>
@@ -25,7 +26,7 @@ @@ -25,7 +26,7 @@
25 26
26 <view class="popup-option" @click="handleFreeTraining"> 27 <view class="popup-option" @click="handleFreeTraining">
27 <view class="icon-wrap"> 28 <view class="icon-wrap">
28 - <image src="/static/icons/free.png" mode="aspectFit"></image> 29 + <up-icon name="clock-fill" color="#040000" size="24"></up-icon>
29 </view> 30 </view>
30 <view class="text-wrap"> 31 <view class="text-wrap">
31 <text class="option-title">自由训练</text> 32 <text class="option-title">自由训练</text>
@@ -84,7 +85,7 @@ const handleAddFromTemplate = () => { @@ -84,7 +85,7 @@ const handleAddFromTemplate = () => {
84 } 85 }
85 86
86 const handleFreeTraining = () => { 87 const handleFreeTraining = () => {
87 - uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' }) 88 + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' })
88 close() 89 close()
89 } 90 }
90 91
@@ -832,15 +832,16 @@ const calendarColorPickerSuccess = () => { @@ -832,15 +832,16 @@ const calendarColorPickerSuccess = () => {
832 832
833 // 去训练 833 // 去训练
834 const startTraining = () => { 834 const startTraining = () => {
835 - // console.log('++点击了去训练++');  
836 - // uni.navigateTo({  
837 - // url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3`,  
838 - // });  
839 - // if (!currentPlan.value) {  
840 - // uni.showToast({ title: '今日暂无训练', icon: 'none' })  
841 - // return  
842 - // }  
843 - // v-if="resdailyData.length > 0 835 +
  836 + if (trainingStore.isTraining) {
  837 + uni.showToast({
  838 + title: '当前已有正在进行的训练',
  839 + icon: 'none'
  840 + });
  841 + return;
  842 + }
  843 +
  844 + trainingStore.isTraining = true;
844 if (resdailyData.value.length > 0) { 845 if (resdailyData.value.length > 0) {
845 trainingStore.isSystem = currentPlan.value.isSystem || false; 846 trainingStore.isSystem = currentPlan.value.isSystem || false;
846 trainingStore.loadDailyTemplateForEdit(currentPlan.value); 847 trainingStore.loadDailyTemplateForEdit(currentPlan.value);
@@ -38,6 +38,9 @@ const popShow = ref(false) @@ -38,6 +38,9 @@ const popShow = ref(false)
38 // dongzuo实例ref,用于收集修改后数据 38 // dongzuo实例ref,用于收集修改后数据
39 const dongzuoSingleRef = ref(null) 39 const dongzuoSingleRef = ref(null)
40 40
  41 +// 训练快照,用于在弹窗打开/关闭时保护正在进行的训练数据
  42 +const trainingSnapshot = ref(null)
  43 +
41 const isRenderDongzuo = ref(false) 44 const isRenderDongzuo = ref(false)
42 45
43 // 标记是否需要删除当前动作组 46 // 标记是否需要删除当前动作组
@@ -187,9 +190,34 @@ const open = async (info) => { @@ -187,9 +190,34 @@ const open = async (info) => {
187 isRenderDongzuo.value = false 190 isRenderDongzuo.value = false
188 needDeleteUnit.value = false 191 needDeleteUnit.value = false
189 192
  193 + console.log('trainingStore.isTraining 值:', trainingStore.isTraining)
  194 +
  195 + // ✅ 新增:如果有正在进行的训练,先把整个 store 状态装进口袋
  196 + if (trainingStore.isTraining) {
  197 + trainingSnapshot.value = JSON.parse(JSON.stringify({
  198 + id: trainingStore.id,
  199 + type: trainingStore.type,
  200 + actionDetail: trainingStore.actionDetail,
  201 + unitRecords: trainingStore.unitRecords,
  202 + trainingName: trainingStore.trainingName,
  203 + totalSeconds: trainingStore.totalSeconds,
  204 + trainingTimeText: trainingStore.trainingTimeText,
  205 + min: trainingStore.min,
  206 + isSystem: trainingStore.isSystem,
  207 + isTraining: trainingStore.isTraining,
  208 + }))
  209 + }
  210 +
  211 + console.log('弹窗打开前,快照缓存:', trainingSnapshot.value);
  212 +
  213 + console.log('打印全局数据trainingStore:', trainingStore);
  214 +
  215 +
  216 +
  217 +
190 console.log('info=', info); 218 console.log('info=', info);
191 - // 每次打开先清空当前store临时数据,避免上次缓存污染  
192 - trainingStore.clearTrainingStore() 219 + // 每次打开先清空当前store临时数据(保存计时功能,最小化可能在运行),避免上次缓存污染
  220 + trainingStore.resetContentOnly();
193 221
194 sourceInfo.value = JSON.parse(JSON.stringify(info)) 222 sourceInfo.value = JSON.parse(JSON.stringify(info))
195 // 关键:手动初始化当前unit的sets数据到Pinia.unitRecords(复用initTemplateRecords逻辑) 223 // 关键:手动初始化当前unit的sets数据到Pinia.unitRecords(复用initTemplateRecords逻辑)
@@ -237,7 +265,17 @@ const closePop = () => { @@ -237,7 +265,17 @@ const closePop = () => {
237 popShow.value = false 265 popShow.value = false
238 isRenderDongzuo.value = false 266 isRenderDongzuo.value = false
239 // 关闭清空临时数据 267 // 关闭清空临时数据
240 - trainingStore.clearTrainingStore() 268 + trainingStore.resetContentOnly()
  269 +
  270 +
  271 + // ✅ 新增:如果有快照,把训练数据还回去
  272 + if (trainingSnapshot.value) {
  273 + Object.assign(trainingStore, trainingSnapshot.value)
  274 + trainingSnapshot.value = null // 清空口袋
  275 + }
  276 +
  277 + console.log('关闭弹窗后,快照缓存:', trainingSnapshot.value);
  278 +
241 } 279 }
242 280
243 // 【保存修改:核心,组装参数调用更新接口】 281 // 【保存修改:核心,组装参数调用更新接口】
@@ -365,7 +403,16 @@ const saveEdit = async () => { @@ -365,7 +403,16 @@ const saveEdit = async () => {
365 popShow.value = false 403 popShow.value = false
366 // 通知父页面刷新列表 404 // 通知父页面刷新列表
367 emit('saveSuccess') 405 emit('saveSuccess')
368 - trainingStore.clearTrainingStore() 406 +
  407 + trainingStore.resetContentOnly()
  408 +
  409 + // ✅ 新增:如果有快照,把训练数据还回去
  410 + if (trainingSnapshot.value) {
  411 + Object.assign(trainingStore, trainingSnapshot.value)
  412 + trainingSnapshot.value = null
  413 + }
  414 +
  415 +
369 } catch (err) { 416 } catch (err) {
370 console.error(err) 417 console.error(err)
371 uni.showToast({ title: '修改失败', icon: 'none' }) 418 uni.showToast({ title: '修改失败', icon: 'none' })
@@ -180,6 +180,10 @@ watch( @@ -180,6 +180,10 @@ watch(
180 padding: 6rpx; 180 padding: 6rpx;
181 box-sizing: border-box; 181 box-sizing: border-box;
182 182
  183 + // #ifdef H5
  184 + margin-top: 30px;
  185 + // #endif
  186 +
183 .tab-item { 187 .tab-item {
184 display: flex; 188 display: flex;
185 align-items: center; 189 align-items: center;
@@ -24,8 +24,7 @@ @@ -24,8 +24,7 @@
24 <button class="add-btn" @click.stop="addTemplateToToday(template.id)">添加</button> 24 <button class="add-btn" @click.stop="addTemplateToToday(template.id)">添加</button>
25 </template> 25 </template>
26 <template v-else> 26 <template v-else>
27 - <button class="add-btn"  
28 - @click="uni.navigateTo({ url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${template.id}&type=3` })"> 27 + <button class="add-btn" @click.stop="goTrain(template)">
29 去训练 28 去训练
30 </button> 29 </button>
31 </template> 30 </template>
@@ -60,6 +59,9 @@ @@ -60,6 +59,9 @@
60 import { ref, onMounted, watch } from 'vue'; 59 import { ref, onMounted, watch } from 'vue';
61 import QueryPlanApi from '@/sheep/api/plan/queryplan'; 60 import QueryPlanApi from '@/sheep/api/plan/queryplan';
62 import WodeJihuaLibiaoTancuang from '@/pages/xunji/components/wode-jihua-libiao-tancuang.vue' 61 import WodeJihuaLibiaoTancuang from '@/pages/xunji/components/wode-jihua-libiao-tancuang.vue'
  62 +import { useTrainingStore } from '@/sheep/store/trainingStore'
  63 +
  64 +const trainingStore = useTrainingStore()
63 65
64 const showPlanList = ref(false); 66 const showPlanList = ref(false);
65 const planListRef = ref(null); 67 const planListRef = ref(null);
@@ -158,6 +160,26 @@ const addTemplateToToday = async (templateId) => { @@ -158,6 +160,26 @@ const addTemplateToToday = async (templateId) => {
158 } 160 }
159 }; 161 };
160 162
  163 +// ====================== 去训练 跳转函数 ======================
  164 +const goTrain = (template) => {
  165 +
  166 + if (trainingStore.isTraining) {
  167 + uni.showToast({
  168 + title: '当前已有正在进行的训练',
  169 + icon: 'none'
  170 + });
  171 + return;
  172 + }
  173 + // 基础校验,防止id为空跳转异常
  174 + if (!template?.id) {
  175 + uni.showToast({ title: "模板ID异常", icon: "none" })
  176 + return
  177 + }
  178 + uni.navigateTo({
  179 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${template.id}&type=3&isTraining=true`
  180 + })
  181 +}
  182 +
161 // ====================== 【关键修复】监听父组件传的 planId ====================== 183 // ====================== 【关键修复】监听父组件传的 planId ======================
162 watch(() => props.planId, (newId) => { 184 watch(() => props.planId, (newId) => {
163 if (newId) { 185 if (newId) {
@@ -290,6 +290,11 @@ onMounted(() => { @@ -290,6 +290,11 @@ onMounted(() => {
290 padding: 20rpx; 290 padding: 20rpx;
291 // background-color: #f5f5f5; 291 // background-color: #f5f5f5;
292 border-radius: 12rpx; 292 border-radius: 12rpx;
  293 + margin-top: 20rpx;
  294 +
  295 + /* #ifdef H5 */
  296 + margin-top: 20px;
  297 + /* #endif */
293 298
294 .search { 299 .search {
295 display: flex; 300 display: flex;
@@ -37,10 +37,10 @@ @@ -37,10 +37,10 @@
37 <scroll-view class="template-list" enable-flex scroll-y> 37 <scroll-view class="template-list" enable-flex scroll-y>
38 <!-- 用父容器包裹 v-if 分支,解决 key 冲突 --> 38 <!-- 用父容器包裹 v-if 分支,解决 key 冲突 -->
39 <view v-if="!isFiltering" class="sub-template-list"> 39 <view v-if="!isFiltering" class="sub-template-list">
40 - <view v-for="(item, index) in templateList" :key="index" class="template-item" @click="gototemplate(item)"> 40 + <view v-for="(item, index) in templateList" :key="index" class="template-item">
41 <image :src="item.urlCover" mode="aspectFill" class="template-img"></image> 41 <image :src="item.urlCover" mode="aspectFill" class="template-img"></image>
42 <view> 42 <view>
43 - <view class="template-content"> 43 + <view class="template-content" @click="gototemplate(item)">
44 <view class="template-title">{{ item.name }}</view> 44 <view class="template-title">{{ item.name }}</view>
45 <view class="template-count">{{ item.templatesCount }}个模板</view> 45 <view class="template-count">{{ item.templatesCount }}个模板</view>
46 <view class="template-desc">{{ item.description }}</view> 46 <view class="template-desc">{{ item.description }}</view>
@@ -212,9 +212,9 @@ const gototemplate = (item) => { @@ -212,9 +212,9 @@ const gototemplate = (item) => {
212 }); 212 });
213 }; 213 };
214 214
215 -onMounted(() => {  
216 - TemplatesList();  
217 - getPartCategories(); 215 +onMounted(async () => {
  216 + await TemplatesList();
  217 + await getPartCategories();
218 console.log('进入模板大类详情'); 218 console.log('进入模板大类详情');
219 219
220 }); 220 });
@@ -222,17 +222,21 @@ onMounted(() => { @@ -222,17 +222,21 @@ onMounted(() => {
222 222
223 <style lang="scss" scoped> 223 <style lang="scss" scoped>
224 .template-page { 224 .template-page {
225 - width: 100%; 225 + width: 100vw;
226 height: 100vh; 226 height: 100vh;
227 box-sizing: border-box; 227 box-sizing: border-box;
228 228
229 .filter-section { 229 .filter-section {
230 position: fixed; 230 position: fixed;
231 - top: 80rpx; 231 + // top: 80rpx;
232 // #ifdef MP-WEIXIN 232 // #ifdef MP-WEIXIN
233 top: 234rpx; 233 top: 234rpx;
234 // #endif 234 // #endif
235 235
  236 + /* #ifdef H5 */
  237 + top: 77px;
  238 + /* #endif */
  239 +
236 left: 0; 240 left: 0;
237 right: 0; 241 right: 0;
238 z-index: 999; 242 z-index: 999;
@@ -260,66 +264,6 @@ onMounted(() => { @@ -260,66 +264,6 @@ onMounted(() => {
260 margin-right: 5rpx; 264 margin-right: 5rpx;
261 } 265 }
262 } 266 }
263 - }  
264 -  
265 - .template-list {  
266 -  
267 - margin-top: 120rpx;  
268 - padding: 0 30rpx;  
269 - box-sizing: border-box;  
270 - flex: 1;  
271 - display: flex;  
272 - flex-direction: column;  
273 - padding-bottom: 200rpx;  
274 - height: calc(100vh - 274rpx);  
275 -  
276 - .sub-template-list {  
277 - display: flex;  
278 - flex-direction: column;  
279 - gap: 10rpx;  
280 - }  
281 -  
282 - .template-item {  
283 - display: flex;  
284 - background-color: white;  
285 - border-radius: 16rpx;  
286 - overflow: hidden;  
287 - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);  
288 - padding: 20rpx;  
289 -  
290 - .template-img {  
291 - width: 120rpx;  
292 - height: 120rpx;  
293 - border-radius: 12rpx;  
294 - margin-right: 20rpx;  
295 - }  
296 -  
297 - .template-content {  
298 - flex: 1;  
299 - }  
300 -  
301 - .template-title {  
302 - font-size: 32rpx;  
303 - color: #333;  
304 - margin-bottom: 10rpx;  
305 - }  
306 -  
307 - .template-count {  
308 - font-size: 24rpx;  
309 - color: #666;  
310 - margin-bottom: 10rpx;  
311 - }  
312 -  
313 - .template-desc {  
314 - font-size: 24rpx;  
315 - color: #666;  
316 - line-height: 1.4;  
317 - }  
318 -  
319 - &:last-child {  
320 - margin-bottom: 45rpx;  
321 - }  
322 - }  
323 267
324 /* 筛选包装器 */ 268 /* 筛选包装器 */
325 .filter-wrapper { 269 .filter-wrapper {
@@ -347,7 +291,7 @@ onMounted(() => { @@ -347,7 +291,7 @@ onMounted(() => {
347 align-items: center; 291 align-items: center;
348 justify-content: center; 292 justify-content: center;
349 width: auto; 293 width: auto;
350 - min-width: 160rpx; 294 + min-width: 125rpx;
351 height: 50rpx; 295 height: 50rpx;
352 background: #fff; 296 background: #fff;
353 color: #333; 297 color: #333;
@@ -435,5 +379,79 @@ onMounted(() => { @@ -435,5 +379,79 @@ onMounted(() => {
435 font-weight: 500; 379 font-weight: 500;
436 } 380 }
437 } 381 }
  382 +
  383 + .template-list {
  384 +
  385 + margin-top: 120rpx;
  386 + padding: 0 30rpx;
  387 + box-sizing: border-box;
  388 + flex: 1;
  389 + display: flex;
  390 + flex-direction: column;
  391 + // #ifdef MP-WEIXIN
  392 + padding-bottom: 200rpx;
  393 + height: calc(100vh - 274rpx);
  394 + // #endif
  395 +
  396 + /* #ifdef H5 */
  397 + margin-top: 87px;
  398 + // 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px
  399 + height: calc(100vh - 77px - 87px);
  400 + // padding-bottom: 40px;
  401 + /* #endif */
  402 +
  403 + .sub-template-list {
  404 + display: flex;
  405 + flex-direction: column;
  406 + gap: 10rpx;
  407 + }
  408 +
  409 + .template-item {
  410 + display: flex;
  411 + background-color: white;
  412 + border-radius: 16rpx;
  413 + overflow: hidden;
  414 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  415 + padding: 20rpx;
  416 +
  417 + &:first-child {
  418 + margin-top: 24rpx;
  419 + }
  420 +
  421 +
  422 + .template-img {
  423 + width: 120rpx;
  424 + height: 120rpx;
  425 + border-radius: 12rpx;
  426 + margin-right: 20rpx;
  427 + }
  428 +
  429 + .template-content {
  430 + flex: 1;
  431 + }
  432 +
  433 + .template-title {
  434 + font-size: 32rpx;
  435 + color: #333;
  436 + margin-bottom: 10rpx;
  437 + }
  438 +
  439 + .template-count {
  440 + font-size: 24rpx;
  441 + color: #666;
  442 + margin-bottom: 10rpx;
  443 + }
  444 +
  445 + .template-desc {
  446 + font-size: 24rpx;
  447 + color: #666;
  448 + line-height: 1.4;
  449 + }
  450 +
  451 + &:last-child {
  452 + margin-bottom: 45rpx;
  453 + }
  454 + }
  455 + }
438 } 456 }
439 </style> 457 </style>
@@ -794,6 +794,9 @@ onMounted(async () => { @@ -794,6 +794,9 @@ onMounted(async () => {
794 794
795 .content-scroll { 795 .content-scroll {
796 height: calc(100vh - 400rpx); 796 height: calc(100vh - 400rpx);
  797 + // #ifdef H5
  798 + height: calc(100vh - 185px);
  799 + // #endif
797 width: 100%; 800 width: 100%;
798 box-sizing: border-box; 801 box-sizing: border-box;
799 overflow: auto; 802 overflow: auto;
@@ -984,8 +987,14 @@ onMounted(async () => { @@ -984,8 +987,14 @@ onMounted(async () => {
984 } 987 }
985 988
986 .muscleShow-popup { 989 .muscleShow-popup {
  990 +
  991 + /* #ifdef MP-WEIXIN */
987 min-height: 40vh; 992 min-height: 40vh;
988 - background-color: #fff; 993 + /* #endif */
  994 +
  995 + /* #ifdef H5 */
  996 + min-height: 55vh;
  997 + /* #endif */
989 998
990 .muscle-name { 999 .muscle-name {
991 font-size: 32rpx; 1000 font-size: 32rpx;
1 <template> 1 <template>
2 - <view v-if="trainStore.trainingInfo.isMinimized" class="float-ball" @click="goBackTrain">  
3 - <text class="text">训练中</text>  
4 - <text class="time">{{ formatTime(trainStore.trainingInfo.totalSeconds) }}</text>  
5 - </view> 2 + <view v-if="trainStore.trainingInfo.isMinimized" class="float-ball" @click="goBackTrain">
  3 + <text class="text">训练中</text>
  4 + <text class="time">{{ formatTime(trainStore.trainingInfo.totalSeconds) }}</text>
  5 + </view>
6 </template> 6 </template>
7 7
8 <script setup> 8 <script setup>
@@ -11,45 +11,46 @@ const trainStore = useTrainingStore(); @@ -11,45 +11,46 @@ const trainStore = useTrainingStore();
11 11
12 // 回到训练页 12 // 回到训练页
13 const goBackTrain = () => { 13 const goBackTrain = () => {
14 - const info = trainStore.trainingInfo;  
15 - trainStore.restore();  
16 14
17 - uni.navigateTo({  
18 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${info.exerciseId}&unitType=${info.unitType}`,  
19 - }); 15 + const info = trainStore.trainingInfo;
  16 + trainStore.restore();
  17 +
  18 + uni.navigateTo({
  19 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${info.exerciseId}&unitType=${info.unitType}&isTraining=true`,
  20 + });
20 }; 21 };
21 22
22 // 时间格式化 23 // 时间格式化
23 const formatTime = (s) => { 24 const formatTime = (s) => {
24 - const m = Math.floor(s / 60).toString().padStart(2, '0');  
25 - const sec = (s % 60).toString().padStart(2, '0');  
26 - return `${m}:${sec}`; 25 + const m = Math.floor(s / 60).toString().padStart(2, '0');
  26 + const sec = (s % 60).toString().padStart(2, '0');
  27 + return `${m}:${sec}`;
27 }; 28 };
28 </script> 29 </script>
29 30
30 <style scoped> 31 <style scoped>
31 .float-ball { 32 .float-ball {
32 - position: fixed;  
33 - bottom: 200rpx;  
34 - right: 40rpx;  
35 - width: 120rpx;  
36 - height: 120rpx;  
37 - background: #ffd700;  
38 - border-radius: 50%;  
39 - display: flex;  
40 - flex-direction: column;  
41 - align-items: center;  
42 - justify-content: center;  
43 - z-index: 99999; 33 + position: fixed;
  34 + bottom: 200rpx;
  35 + right: 40rpx;
  36 + width: 120rpx;
  37 + height: 120rpx;
  38 + background: #ffd700;
  39 + border-radius: 50%;
  40 + display: flex;
  41 + flex-direction: column;
  42 + align-items: center;
  43 + justify-content: center;
  44 + z-index: 99999;
44 } 45 }
45 46
46 .text { 47 .text {
47 - font-size: 24rpx;  
48 - font-weight: bold; 48 + font-size: 24rpx;
  49 + font-weight: bold;
49 } 50 }
50 51
51 .time { 52 .time {
52 - font-size: 20rpx;  
53 - margin-top: 6rpx; 53 + font-size: 20rpx;
  54 + margin-top: 6rpx;
54 } 55 }
55 </style> 56 </style>
@@ -355,5 +355,10 @@ $cell-h: 110rpx; @@ -355,5 +355,10 @@ $cell-h: 110rpx;
355 font-size: 30rpx; 355 font-size: 30rpx;
356 color: $text-white; 356 color: $text-white;
357 margin-top: 60rpx; 357 margin-top: 60rpx;
  358 +
  359 + /* #ifdef H5 */
  360 + margin-bottom: 20px;
  361 + /* #endif */
  362 +
358 } 363 }
359 </style> 364 </style>
@@ -165,7 +165,7 @@ @@ -165,7 +165,7 @@
165 </template> 165 </template>
166 166
167 <script setup> 167 <script setup>
168 -import { ref, reactive, computed, onBeforeUnmount, onMounted, nextTick, onUnmounted } from 'vue'; 168 +import { ref, reactive, computed, onBeforeUnmount, onMounted, nextTick, onUnmounted, watch } from 'vue';
169 import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue'; 169 import dongzuo from '@/pages/xunji/components/dongzuo-lianxi/dongzuo.vue';
170 import { onLoad } from "@dcloudio/uni-app" 170 import { onLoad } from "@dcloudio/uni-app"
171 import TrainingApi from '@/sheep/api/Training/traininghistory' 171 import TrainingApi from '@/sheep/api/Training/traininghistory'
@@ -194,19 +194,38 @@ const trainingName = computed(() => trainingStore.trainingName) @@ -194,19 +194,38 @@ const trainingName = computed(() => trainingStore.trainingName)
194 194
195 const actionSortRef = ref(null) 195 const actionSortRef = ref(null)
196 196
  197 +watch(
  198 + () => trainingStore.isTraining,
  199 + (newVal, oldVal) => {
  200 + console.log('训练状态变化:', oldVal, '→', newVal)
  201 + if (newVal) {
  202 + // 正在训练逻辑
  203 + } else {
  204 + // 结束训练逻辑
  205 + }
  206 + },
  207 + { immediate: true } // 页面初始化立即执行一次
  208 +)
  209 +
197 // 动作排序弹窗 210 // 动作排序弹窗
198 const openActionSort = () => { 211 const openActionSort = () => {
199 - console.log('调用前ref 的值:', actionSortRef.value) 212 + console.log('调用前,ref 的值:', actionSortRef.value)
200 actionSortRef.value.openActionSort() 213 actionSortRef.value.openActionSort()
201 } 214 }
202 215
203 onLoad(async (options) => { 216 onLoad(async (options) => {
204 217
  218 + console.log('✅ 动作练习页面 onLoad 中的 options:', options);
  219 +
  220 +
205 const id = Number(options.id); 221 const id = Number(options.id);
206 const type = Number(options.type); 222 const type = Number(options.type);
207 const isTraining = options.isTraining === 'true'; 223 const isTraining = options.isTraining === 'true';
208 trainingStore.isTraining = isTraining; 224 trainingStore.isTraining = isTraining;
209 225
  226 + console.log('✅动作练习页面 onLoad 中的 options:', options);
  227 +
  228 +
210 const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null; 229 const dailyTemplateId = options.dailyTemplateId ? Number(options.dailyTemplateId) : null;
211 trainingStore.dailyTemplateId = dailyTemplateId; 230 trainingStore.dailyTemplateId = dailyTemplateId;
212 console.log('接收到的 dailyTemplateId:', dailyTemplateId); 231 console.log('接收到的 dailyTemplateId:', dailyTemplateId);
@@ -508,7 +527,7 @@ const save = async () => { @@ -508,7 +527,7 @@ const save = async () => {
508 // ✅ 情况1:【去训练】→ 保存历史 527 // ✅ 情况1:【去训练】→ 保存历史
509 if (trainingStore.isTraining) { 528 if (trainingStore.isTraining) {
510 await TrainingApi.createTrainHistory({ units }); 529 await TrainingApi.createTrainHistory({ units });
511 - uni.showToast({ title: '训练保存成功', icon: 'success' }); 530 + uni.showToast({ title: '训练提交成功', icon: 'success' });
512 } 531 }
513 // ✅ 情况2:【编辑模板】→ 更新模板 532 // ✅ 情况2:【编辑模板】→ 更新模板
514 else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) { 533 else if (trainingStore.type === 3 && trainingStore.dailyTemplateId) {
@@ -521,7 +540,7 @@ const save = async () => { @@ -521,7 +540,7 @@ const save = async () => {
521 // ✅ 情况3:普通训练 540 // ✅ 情况3:普通训练
522 else { 541 else {
523 await TrainingApi.createTrainHistory({ units }); 542 await TrainingApi.createTrainHistory({ units });
524 - uni.showToast({ title: '保存成功', icon: 'success' }); 543 + uni.showToast({ title: '训练提交成功', icon: 'success' });
525 } 544 }
526 545
527 setTimeout(() => { 546 setTimeout(() => {
@@ -571,7 +590,10 @@ const addActionsPopup = () => { @@ -571,7 +590,10 @@ const addActionsPopup = () => {
571 590
572 // 最小化 591 // 最小化
573 const openMin = () => { 592 const openMin = () => {
  593 + console.log('最小化跳转前trainingStore.isTraining', trainingStore.isTraining);
574 console.log('trainingStore.min+++++++++++++', trainingStore.min); 594 console.log('trainingStore.min+++++++++++++', trainingStore.min);
  595 +
  596 + console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining);
575 trainingStore.min = true; 597 trainingStore.min = true;
576 // uni.navigateTo({ 598 // uni.navigateTo({
577 // url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径 599 // url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径
1 -<!--  
2 - 训练动作详情页(xunji-dongzuo-xiangqing.vue)  
3 - 核心用于展示单个体能训练动作的完整信息,并提供动作管理、训练启动、收藏分享等操作,适配移动端多端(微信小程序 / APP/H5 等)。  
4 - 备注这个字段,没有接口,没有交互,只是前端渲染  
5 - -->  
6 <template> 1 <template>
7 <view class="container"> 2 <view class="container">
8 <!-- 页面头部编辑/分享/收藏三个按钮 --> 3 <!-- 页面头部编辑/分享/收藏三个按钮 -->
9 <view class="page-header"> 4 <view class="page-header">
10 <text class="title">{{ exercisedetail.name }}</text> 5 <text class="title">{{ exercisedetail.name }}</text>
11 <view class="header-icons"> 6 <view class="header-icons">
12 - <uni-icons type="more" size="24" color="#fff" @click="edit"></uni-icons> 7 + <!-- 编辑按钮 -->
  8 + <view class="edit-icon" @click="edit">
  9 + <uni-icons type="more" size="24" color="#fff"></uni-icons>
  10 + </view>
13 <!-- 分享按钮 --> 11 <!-- 分享按钮 -->
14 <button class="share-btn" open-type="share"> 12 <button class="share-btn" open-type="share">
15 <uni-icons type="paperplane" size="24" color="#fff"></uni-icons> 13 <uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
16 </button> 14 </button>
17 <uni-icons :type="islove ? 'heart-filled' : 'heart'" size="24" color="#fff" @click="toggleCollect"></uni-icons> 15 <uni-icons :type="islove ? 'heart-filled' : 'heart'" size="24" color="#fff" @click="toggleCollect"></uni-icons>
  16 +
18 </view> 17 </view>
19 </view> 18 </view>
20 <!--标签栏(可切换的 3 个页面)要点,历史,平替动作--> 19 <!--标签栏(可切换的 3 个页面)要点,历史,平替动作-->
@@ -96,7 +95,7 @@ @@ -96,7 +95,7 @@
96 <!-- 1. 日期置顶 --> 95 <!-- 1. 日期置顶 -->
97 <view class="card-date"> 96 <view class="card-date">
98 <text class="date-text">{{ item.date ? `${item.date[0]}/${item.date[1]}/${item.date[2]}` : '未知日期' 97 <text class="date-text">{{ item.date ? `${item.date[0]}/${item.date[1]}/${item.date[2]}` : '未知日期'
99 - }}</text> 98 + }}</text>
100 </view> 99 </view>
101 <!-- 2. 主体信息区域 --> 100 <!-- 2. 主体信息区域 -->
102 <view class="card-body"> 101 <view class="card-body">
@@ -300,6 +299,8 @@ const toggleCollect = async () => { @@ -300,6 +299,8 @@ const toggleCollect = async () => {
300 // 收藏状态:传 1,取消收藏:传 0 299 // 收藏状态:传 1,取消收藏:传 0
301 const status = islove.value ? 1 : 0; 300 const status = islove.value ? 1 : 0;
302 301
  302 + console.log('传递给收藏接口的statu', status);
  303 +
303 // 调用收藏接口 304 // 调用收藏接口
304 await ExercisesApi.toggleFavorite(id.value, status); 305 await ExercisesApi.toggleFavorite(id.value, status);
305 306
@@ -320,7 +321,7 @@ const checkCollectStatus = async () => { @@ -320,7 +321,7 @@ const checkCollectStatus = async () => {
320 if (!id.value) return; 321 if (!id.value) return;
321 try { 322 try {
322 const res = await ExercisesApi.checkExerciseFavorited(id.value); 323 const res = await ExercisesApi.checkExerciseFavorited(id.value);
323 - // 接口返回 true = 已收藏 324 + console.log('动作收藏与否:', res);
324 if (res.data === true) { 325 if (res.data === true) {
325 // 后端说已收藏 326 // 后端说已收藏
326 islove.value = true; // 实心爱心 327 islove.value = true; // 实心爱心
1 -<!--  
2 - 定义训练动作新增页面(xunji-dongzuo-xinzeng.vue),核心用于创建 / 新增自定义体能训练动作,位置在训记的动作右侧抽屉点击新增动作就是这个页面.  
3 - 支持训练部位、动作类型、目录分类、封面 / 视频等多维度配置,最终提交保存到后端接口,适配移动端多端(小程序 / APP/H5)  
4 - -->  
5 <template> 1 <template>
6 <view class="add-custom-exercise-page"> 2 <view class="add-custom-exercise-page">
7 <!-- 肌肉部位图 --> 3 <!-- 肌肉部位图 -->
@@ -41,11 +37,19 @@ @@ -41,11 +37,19 @@
41 <view class="form-section"> 37 <view class="form-section">
42 <!-- 动作名称 --> 38 <!-- 动作名称 -->
43 <view class="form-item"> 39 <view class="form-item">
44 - <view class="label">动作名称</view> 40 + <!-- <view class="label">动作名称</view>
45 <view class="input-group"> 41 <view class="input-group">
46 <input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称" 42 <input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称"
47 placeholder-class="input-placeholder" /> 43 placeholder-class="input-placeholder" />
  44 + </view> -->
  45 + <view class="action-name">
  46 + <view class="label">动作名称</view>
  47 + <view class="input-group">
  48 + <input v-model="exerciseName" class="input-field" placeholder="点击此处填写动作名称"
  49 + placeholder-class="input-placeholder" />
  50 + </view>
48 </view> 51 </view>
  52 +
49 <view class="upload-btn" @click="openCoverSelector"> 53 <view class="upload-btn" @click="openCoverSelector">
50 <image :src="coverImagePath" class="preview-media" mode="aspectFill" /> 54 <image :src="coverImagePath" class="preview-media" mode="aspectFill" />
51 <text class="btn-text">更换封面</text> 55 <text class="btn-text">更换封面</text>
@@ -67,7 +71,7 @@ @@ -67,7 +71,7 @@
67 <text class="value">{{ actionType || '请选择' }}</text> 71 <text class="value">{{ actionType || '请选择' }}</text>
68 <!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png" 72 <!-- <image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
69 class="arrow-icon" mode="aspectFill" /> --> 73 class="arrow-icon" mode="aspectFill" /> -->
70 - <up-icon name="arrow-right" color="#333" size="16"></up-icon> 74 + <up-icon v-if="!isEdit" name="arrow-right" color="#333" size="16"></up-icon>
71 </view> 75 </view>
72 </view> 76 </view>
73 77
@@ -121,6 +125,10 @@ @@ -121,6 +125,10 @@
121 </view> 125 </view>
122 </view> 126 </view>
123 127
  128 + <view class="form-item" v-if="isEdit" @click="deleteAction">
  129 + <view class="center-label">删除动作</view>
  130 + </view>
  131 +
124 <!-- 保存按钮 --> 132 <!-- 保存按钮 -->
125 <view class="save-button"> 133 <view class="save-button">
126 <button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button> 134 <button class="btn-save" :disabled="!exerciseName.trim()" @click="saveExercise">保存</button>
@@ -139,7 +147,7 @@ @@ -139,7 +147,7 @@
139 <picker-view-column> 147 <picker-view-column>
140 <view class="picker-item" v-for="item in actionTypeOptions" :key="item">{{ 148 <view class="picker-item" v-for="item in actionTypeOptions" :key="item">{{
141 item.name 149 item.name
142 - }}</view> 150 + }}</view>
143 </picker-view-column> 151 </picker-view-column>
144 </picker-view> 152 </picker-view>
145 </view> 153 </view>
@@ -158,14 +166,14 @@ @@ -158,14 +166,14 @@
158 <picker-view-column> 166 <picker-view-column>
159 <view class="picker-item" v-for="item in bodyPartOptions" :key="item">{{ 167 <view class="picker-item" v-for="item in bodyPartOptions" :key="item">{{
160 item.name 168 item.name
161 - }}</view> 169 + }}</view>
162 </picker-view-column> 170 </picker-view-column>
163 </picker-view> 171 </picker-view>
164 <picker-view :value="[equipmentIndex]" @change="onEquipmentChange" class="picker-view half"> 172 <picker-view :value="[equipmentIndex]" @change="onEquipmentChange" class="picker-view half">
165 <picker-view-column> 173 <picker-view-column>
166 <view class="picker-item" v-for="item in equipmentOptions" :key="item">{{ 174 <view class="picker-item" v-for="item in equipmentOptions" :key="item">{{
167 item.name 175 item.name
168 - }}</view> 176 + }}</view>
169 </picker-view-column> 177 </picker-view-column>
170 </picker-view> 178 </picker-view>
171 </view> 179 </view>
@@ -176,6 +184,7 @@ @@ -176,6 +184,7 @@
176 184
177 <script setup> 185 <script setup>
178 import { onMounted, ref } from 'vue'; 186 import { onMounted, ref } from 'vue';
  187 +import { onLoad } from '@dcloudio/uni-app';
179 import ExercisesApi from '@/sheep/api/motion/exercises'; 188 import ExercisesApi from '@/sheep/api/motion/exercises';
180 import LeftmotionApi from '@/sheep/api/motion/equipments'; 189 import LeftmotionApi from '@/sheep/api/motion/equipments';
181 import EquipmentListApi from '@/sheep/api/motion/motionequipments'; 190 import EquipmentListApi from '@/sheep/api/motion/motionequipments';
@@ -185,6 +194,7 @@ const exerciseName = ref(''); @@ -185,6 +194,7 @@ const exerciseName = ref('');
185 const actionType = ref(); 194 const actionType = ref();
186 const directoryName = ref(); 195 const directoryName = ref();
187 const selectedMusclesDisplay = ref('未选择'); 196 const selectedMusclesDisplay = ref('未选择');
  197 +const editActionId = ref(null)
188 198
189 // 选中索引 199 // 选中索引
190 const actionTypeIndex = ref(0); 200 const actionTypeIndex = ref(0);
@@ -211,6 +221,145 @@ const actionTypeOptions = ref([ @@ -211,6 +221,145 @@ const actionTypeOptions = ref([
211 // console.error('加载部位失败', err); 221 // console.error('加载部位失败', err);
212 // } 222 // }
213 // }; 223 // };
  224 +
  225 +const resetForm = () => {
  226 + exerciseName.value = ''
  227 + actionType.value = ''
  228 + actionTypeIndex.value = 0
  229 + directoryName.value = ''
  230 + bodyPartIndex.value = 0
  231 + equipmentIndex.value = 0
  232 + selectedMusclesDisplay.value = '未选择'
  233 + primaryMuscles.value = []
  234 + secondaryMuscles.value = []
  235 + coverImagePath.value = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
  236 + coverUploaded.value = false
  237 + videoFilePath.value = ''
  238 + videoUploaded.value = false
  239 + isVideoFile.value = false
  240 + urlTutorial.value = ''
  241 + videoDetailName.value = ''
  242 + // 同时关闭所有弹窗,防止上次弹窗残留
  243 + closeAllPopups()
  244 +}
  245 +
  246 +const isEdit = ref(false)
  247 +const actionDetail = ref({});
  248 +
  249 +onLoad(async (options) => {
  250 +
  251 + isEdit.value = options.isEdit === 'true'
  252 + const editId = Number(options.id)
  253 + editActionId.value = editId
  254 + console.log('编辑状态', isEdit, '编辑动作ID', editId)
  255 + // 清空表单状态
  256 + resetForm()
  257 +
  258 + await Promise.all([
  259 + loadbodyPartOptions(),
  260 + loadequipmentOptions(),
  261 + loadMuscleGroups()
  262 + ])
  263 + if (isEdit.value) {
  264 + loadexercisedetail(editId)
  265 + }
  266 +
  267 +})
  268 +
  269 +// 加载单个动作详情
  270 +// const loadexercisedetail = async (id) => {
  271 +// if (!id || !Number(id)) return
  272 +// try {
  273 +// const response = await ExercisesApi.getExerciseById(id);
  274 +// actionDetail.value = response.data;
  275 +// console.log('打印动作详情', actionDetail.value);
  276 +// } catch (err) {
  277 +// console.error('加载动作详情失败:', err)
  278 +// actionDetail.value = {}
  279 +// }
  280 +// };
  281 +
  282 +// 加载单个动作详情并回显表单
  283 +const loadexercisedetail = async (id) => {
  284 + if (!id || !Number(id)) return
  285 + try {
  286 + const response = await ExercisesApi.getExerciseById(id);
  287 + actionDetail.value = response.data;
  288 + const detail = actionDetail.value
  289 + console.log('打印动作详情', detail);
  290 +
  291 + // 1. 动作名称
  292 + exerciseName.value = detail.name || ''
  293 +
  294 + // 2. 动作类型回显(id匹配,编辑只读)
  295 + const targetType = actionTypeOptions.value.find(item => item.id === detail.exerciseType)
  296 + if (targetType) {
  297 + actionTypeIndex.value = actionTypeOptions.value.findIndex(item => item.id === detail.exerciseType)
  298 + actionType.value = targetType.name
  299 + }
  300 +
  301 + // 3. 目录:身体部位categoryId + 器械equipmentId 匹配下标
  302 + const bodyIdx = bodyPartOptions.value.findIndex(item => item.id === detail.categoryId)
  303 + if (bodyIdx > -1) bodyPartIndex.value = bodyIdx
  304 +
  305 + const equipIdx = equipmentOptions.value.findIndex(item => item.id === detail.equipmentId)
  306 + if (equipIdx > -1) equipmentIndex.value = equipIdx
  307 + // 拼接目录展示文字
  308 + const selBody = bodyPartOptions.value[bodyIdx]
  309 + const selEquip = equipmentOptions.value[equipIdx]
  310 + if (selBody && selEquip) {
  311 + directoryName.value = ` ${selBody.name}( ${selEquip.name})`
  312 + }
  313 +
  314 + // 4. 肌肉部位:后端返回字符串 "[30,28,29]" 转数字数组
  315 + if (detail.primaryMuscles) {
  316 + primaryMuscles.value = JSON.parse(detail.primaryMuscles)
  317 + } else {
  318 + primaryMuscles.value = []
  319 + }
  320 + if (detail.secondaryMuscles) {
  321 + secondaryMuscles.value = JSON.parse(detail.secondaryMuscles)
  322 + } else {
  323 + secondaryMuscles.value = []
  324 + }
  325 + // 直接使用后端返回的拼接好的肌肉名称展示文本,不用重新拼接
  326 + const primaryStr = detail.primaryMuscleNames?.length ? detail.primaryMuscleNames.join('、') + '(主)' : ''
  327 + const secondaryStr = detail.secondaryMuscleNames?.length ? detail.secondaryMuscleNames.join('、') + '(次)' : ''
  328 + let display = ''
  329 + if (primaryStr) display += primaryStr
  330 + if (secondaryStr) {
  331 + if (display) display += ', '
  332 + display += secondaryStr
  333 + }
  334 + selectedMusclesDisplay.value = display || '未选择'
  335 +
  336 + // 5. 封面图 url3dAnimation
  337 + if (detail.url3dAnimation) {
  338 + coverImagePath.value = detail.url3dAnimation
  339 + coverUploaded.value = true // 标记已有封面,跳过新增封面校验
  340 + }
  341 +
  342 + // 6. 动作实拍视频/图片 urlRealPerson
  343 + if (detail.urlRealPerson) {
  344 + videoFilePath.value = detail.urlRealPerson
  345 + videoUploaded.value = true
  346 + // 简单判断是否视频(后端返回mp4则标记video)
  347 + isVideoFile.value = detail.urlRealPerson.includes('.mp4')
  348 + }
  349 +
  350 + // 7. 详解视频 urlTutorial
  351 + if (detail.urlTutorial) {
  352 + urlTutorial.value = detail.urlTutorial
  353 + videoDetailName.value = '已选择详解视频'
  354 + }
  355 +
  356 + } catch (err) {
  357 + console.error('加载动作详情失败:', err)
  358 + actionDetail.value = {}
  359 + }
  360 +};
  361 +
  362 +
214 // 肌肉部位列表(从接口获取) 363 // 肌肉部位列表(从接口获取)
215 const muscleGroups = ref([]); 364 const muscleGroups = ref([]);
216 // 加载肌肉部位 365 // 加载肌肉部位
@@ -253,6 +402,7 @@ const showVideoSelector = ref(false); @@ -253,6 +402,7 @@ const showVideoSelector = ref(false);
253 402
254 // 弹窗控制 403 // 弹窗控制
255 const openActionTypePicker = () => { 404 const openActionTypePicker = () => {
  405 + if (isEdit.value) return;
256 showActionTypePicker.value = true; 406 showActionTypePicker.value = true;
257 }; 407 };
258 const openDirectoryPicker = () => { 408 const openDirectoryPicker = () => {
@@ -394,7 +544,6 @@ const saveExercise = async () => { @@ -394,7 +544,6 @@ const saveExercise = async () => {
394 return; 544 return;
395 } 545 }
396 546
397 - console.log('------------------');  
398 console.log('--', equipmentOptions.value[equipmentIndex.value]); 547 console.log('--', equipmentOptions.value[equipmentIndex.value]);
399 console.log('--', bodyPartOptions.value[bodyPartIndex.value]); 548 console.log('--', bodyPartOptions.value[bodyPartIndex.value]);
400 // 获取 categoryId 和 equipmentId 549 // 获取 categoryId 和 equipmentId
@@ -435,14 +584,23 @@ const saveExercise = async () => { @@ -435,14 +584,23 @@ const saveExercise = async () => {
435 urlRealPerson: videoFilePath.value, //真人实拍视频 584 urlRealPerson: videoFilePath.value, //真人实拍视频
436 urlTutorial: urlTutorial.value, // 详解视频 585 urlTutorial: urlTutorial.value, // 详解视频
437 }; 586 };
  587 + // 编辑追加id
  588 + if (isEdit.value) {
  589 + payload.id = editActionId.value
  590 + }
438 console.log('【发送的 payload】', payload); 591 console.log('【发送的 payload】', payload);
439 - 592 + let res;
440 try { 593 try {
441 - const res = await ExercisesApi.createExercise(payload);  
442 - console.log('【接口返回】', res); 594 + if (isEdit.value) {
  595 + res = await ExercisesApi.updateexercises(payload);
  596 + console.log('【更新接口返回】', res);
  597 + } else {
  598 + res = await ExercisesApi.createExercise(payload);
  599 + console.log('【创建接口返回】', res);
  600 + }
  601 +
443 if (res.code === 0) { 602 if (res.code === 0) {
444 - uni.showToast({ title: '保存成功', icon: 'success' });  
445 - // 可选:返回上一页或清空表单 603 + uni.showToast({ title: isEdit.value ? '编辑成功' : '保存成功', icon: 'success' });
446 setTimeout(() => { 604 setTimeout(() => {
447 uni.navigateBack(); 605 uni.navigateBack();
448 }, 1000); 606 }, 1000);
@@ -455,6 +613,29 @@ const saveExercise = async () => { @@ -455,6 +613,29 @@ const saveExercise = async () => {
455 } 613 }
456 }; 614 };
457 615
  616 +// 删除动作
  617 +const deleteAction = () => {
  618 + uni.showModal({
  619 + title: '提示',
  620 + content: '确定要删除该动作吗?删除后无法恢复',
  621 + success: async (res) => {
  622 + if (res.confirm) {
  623 + try {
  624 + const delRes = await ExercisesApi.deleteexercises(editActionId.value)
  625 + if (delRes.code === 0) {
  626 + uni.showToast({ title: '删除成功', icon: 'success' })
  627 + setTimeout(() => uni.navigateBack(), 1000)
  628 + } else {
  629 + uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' })
  630 + }
  631 + } catch (err) {
  632 + uni.showToast({ title: '删除请求失败', icon: 'none' })
  633 + }
  634 + }
  635 + }
  636 + })
  637 +}
  638 +
458 const openMusclePicker = () => { 639 const openMusclePicker = () => {
459 showMusclePicker.value = true; 640 showMusclePicker.value = true;
460 }; 641 };
@@ -504,9 +685,9 @@ const confirmMuscleSelection = () => { @@ -504,9 +685,9 @@ const confirmMuscleSelection = () => {
504 }; 685 };
505 686
506 onMounted(() => { 687 onMounted(() => {
507 - loadbodyPartOptions();  
508 - loadequipmentOptions();  
509 - loadMuscleGroups(); 688 + // loadbodyPartOptions();
  689 + // loadequipmentOptions();
  690 + // loadMuscleGroups();
510 // loadexercisesGroups(); 691 // loadexercisesGroups();
511 }); 692 });
512 </script> 693 </script>
@@ -602,12 +783,21 @@ onMounted(() => { @@ -602,12 +783,21 @@ onMounted(() => {
602 position: relative; 783 position: relative;
603 } 784 }
604 785
  786 +.action-name {
  787 + width: 285rpx;
  788 +}
  789 +
605 .label { 790 .label {
606 font-size: 32rpx; 791 font-size: 32rpx;
607 color: #333; 792 color: #333;
608 margin-bottom: 10rpx; 793 margin-bottom: 10rpx;
609 } 794 }
610 795
  796 +.center-label {
  797 + text-align: center;
  798 + color: #c44b4b;
  799 +}
  800 +
611 .input-group { 801 .input-group {
612 display: flex; 802 display: flex;
613 align-items: center; 803 align-items: center;
@@ -372,9 +372,18 @@ const handleUnitClick = (unit) => { @@ -372,9 +372,18 @@ const handleUnitClick = (unit) => {
372 // 开始训练 372 // 开始训练
373 const startTraining = () => { 373 const startTraining = () => {
374 374
  375 + if (trainingStore.isTraining) {
  376 + uni.showToast({
  377 + title: '当前已有正在进行的训练',
  378 + icon: 'none'
  379 + });
  380 + return;
  381 + }
  382 +
375 trainingStore.isSystem = 1; 383 trainingStore.isSystem = 1;
  384 + trainingStore.isTraining = true;
376 uni.navigateTo({ 385 uni.navigateTo({
377 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.value.id}&type=3`, 386 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.value.id}&type=3&isTraining=true`,
378 }); 387 });
379 console.log('打印传递给动作训练页面的模板id', TemplateDetail.id); 388 console.log('打印传递给动作训练页面的模板id', TemplateDetail.id);
380 389
@@ -382,6 +391,14 @@ const startTraining = () => { @@ -382,6 +391,14 @@ const startTraining = () => {
382 // startDaiTemplateTraining 391 // startDaiTemplateTraining
383 const startDaiTemplateTraining = () => { 392 const startDaiTemplateTraining = () => {
384 393
  394 + if (trainingStore.isTraining) {
  395 + uni.showToast({
  396 + title: '当前已有正在进行的训练',
  397 + icon: 'none'
  398 + });
  399 + return;
  400 + }
  401 +
385 trainingStore.isSystem = TemplateDetail.value.isSystem; 402 trainingStore.isSystem = TemplateDetail.value.isSystem;
386 trainingStore.loadDailyTemplateForEdit(TemplateDetail.value); 403 trainingStore.loadDailyTemplateForEdit(TemplateDetail.value);
387 trainingStore.initDailyTemplateRecords() 404 trainingStore.initDailyTemplateRecords()
@@ -80,7 +80,7 @@ @@ -80,7 +80,7 @@
80 </view> 80 </view>
81 <!-- ==========新增:未来7天训练日日历模块 end========== --> 81 <!-- ==========新增:未来7天训练日日历模块 end========== -->
82 82
83 - <!-- 计划课程 --> 83 + <!-- 计划课程(模板) -->
84 <view class="course-section"> 84 <view class="course-section">
85 <text class="section-title">计划课程</text> 85 <text class="section-title">计划课程</text>
86 <view class="course-card" v-for="item in plandetail.templates" :key="item.id" 86 <view class="course-card" v-for="item in plandetail.templates" :key="item.id"
@@ -312,9 +312,18 @@ const openArrageClass = () => { @@ -312,9 +312,18 @@ const openArrageClass = () => {
312 // 开始训练 312 // 开始训练
313 const startTraining = (templateId) => { 313 const startTraining = (templateId) => {
314 314
  315 + if (trainingStore.isTraining) {
  316 + uni.showToast({
  317 + title: '当前已有正在进行的训练',
  318 + icon: 'none'
  319 + });
  320 + return;
  321 + }
  322 +
  323 + trainingStore.isTraining = true;
315 trainingStore.isSystem = 1; 324 trainingStore.isSystem = 1;
316 uni.navigateTo({ 325 uni.navigateTo({
317 - url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateId}&type=3`, 326 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${templateId}&type=3&isTraining=true`,
318 }); 327 });
319 console.log('打印传递给动作训练页面的模板id', templateId); 328 console.log('打印传递给动作训练页面的模板id', templateId);
320 329
@@ -16,6 +16,10 @@ @@ -16,6 +16,10 @@
16 <button v-else class="unbind-btn" @click="unbindWeChat">解绑</button> --> 16 <button v-else class="unbind-btn" @click="unbindWeChat">解绑</button> -->
17 </view> 17 </view>
18 18
  19 + <view class="setting-item" @click.stop="goToForgetPassword">
  20 + <view class="setting-label">修改密码</view>
  21 + </view>
  22 +
19 <!-- 推送通知开关 --> 23 <!-- 推送通知开关 -->
20 <!-- <view class="setting-item notice"> 24 <!-- <view class="setting-item notice">
21 <view class="content"> 25 <view class="content">
@@ -181,6 +185,14 @@ const onRecommendChange = async (e) => { @@ -181,6 +185,14 @@ const onRecommendChange = async (e) => {
181 } 185 }
182 }; 186 };
183 187
  188 +// 忘记密码跳转
  189 +const goToForgetPassword = () => {
  190 + uni.navigateTo({
  191 + url: '/pages7/pages/index/reset-password?isModify=true',
  192 + });
  193 +};
  194 +
  195 +
184 // 退出登录 196 // 退出登录
185 const logout = () => { 197 const logout = () => {
186 uni.showModal({ 198 uni.showModal({
@@ -6,32 +6,18 @@ @@ -6,32 +6,18 @@
6 <!-- 优化:为了兼容微信小程序等跨端环境,原生 SVG 转换为 Base64 嵌入标准 image 标签中,确保完美渲染 --> 6 <!-- 优化:为了兼容微信小程序等跨端环境,原生 SVG 转换为 Base64 嵌入标准 image 标签中,确保完美渲染 -->
7 <image class="logo-img" :src="logoSvgBase64" mode="aspectFit" /> 7 <image class="logo-img" :src="logoSvgBase64" mode="aspectFit" />
8 </view> 8 </view>
9 - <view class="brand-title">FitFlow 悦动健身</view> 9 + <view class="brand-title">自己练</view>
10 <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view> 10 <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view>
11 </view> 11 </view>
12 12
13 <!-- 2. 登录方式 Tab 切换 --> 13 <!-- 2. 登录方式 Tab 切换 -->
14 <view class="tab-container"> 14 <view class="tab-container">
15 <view class="tab-item" :class="{ active: activeTab === 'sms' }" @click="switchTab('sms')"> 15 <view class="tab-item" :class="{ active: activeTab === 'sms' }" @click="switchTab('sms')">
16 - <u-icon  
17 - name="phone"  
18 - size="18"  
19 - :color="activeTab === 'sms' ? '#0a1931' : '#7a8290'"  
20 - class="tab-icon"  
21 - /> 16 + <u-icon name="phone" size="18" :color="activeTab === 'sms' ? '#0a1931' : '#7a8290'" class="tab-icon" />
22 <text>免密登录</text> 17 <text>免密登录</text>
23 </view> 18 </view>
24 - <view  
25 - class="tab-item"  
26 - :class="{ active: activeTab === 'password' }"  
27 - @click="switchTab('password')"  
28 - >  
29 - <u-icon  
30 - name="lock"  
31 - size="18"  
32 - :color="activeTab === 'password' ? '#0a1931' : '#7a8290'"  
33 - class="tab-icon"  
34 - /> 19 + <view class="tab-item" :class="{ active: activeTab === 'password' }" @click="switchTab('password')">
  20 + <u-icon name="lock" size="18" :color="activeTab === 'password' ? '#0a1931' : '#7a8290'" class="tab-icon" />
35 <text>密码登录</text> 21 <text>密码登录</text>
36 </view> 22 </view>
37 </view> 23 </view>
@@ -43,14 +29,8 @@ @@ -43,14 +29,8 @@
43 <text class="form-label">手机号码</text> 29 <text class="form-label">手机号码</text>
44 <view class="input-box"> 30 <view class="input-box">
45 <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" /> 31 <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
46 - <input  
47 - type="number"  
48 - v-model="phoneNumber"  
49 - placeholder="请输入您的手机号"  
50 - placeholder-style="color: #a1a8b3"  
51 - maxlength="11"  
52 - class="native-input"  
53 - /> 32 + <input type="number" v-model="phoneNumber" placeholder="请输入您的手机号" placeholder-style="color: #a1a8b3"
  33 + maxlength="11" class="native-input" />
54 </view> 34 </view>
55 </view> 35 </view>
56 36
@@ -60,21 +40,11 @@ @@ -60,21 +40,11 @@
60 <view class="code-row"> 40 <view class="code-row">
61 <view class="input-box flex-1"> 41 <view class="input-box flex-1">
62 <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" /> 42 <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
63 - <input  
64 - type="number"  
65 - v-model="verifyCode"  
66 - placeholder="6位验证码"  
67 - placeholder-style="color: #a1a8b3"  
68 - maxlength="6"  
69 - class="native-input"  
70 - /> 43 + <input type="number" v-model="verifyCode" placeholder="6位验证码" placeholder-style="color: #a1a8b3"
  44 + maxlength="6" class="native-input" />
71 </view> 45 </view>
72 <!-- 获取验证码按钮 --> 46 <!-- 获取验证码按钮 -->
73 - <view  
74 - class="code-btn"  
75 - :class="{ disabled: countdown > 0 || isSending }"  
76 - @click="handleGetCode"  
77 - > 47 + <view class="code-btn" :class="{ disabled: countdown > 0 || isSending }" @click="handleGetCode">
78 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text> 48 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
79 </view> 49 </view>
80 </view> 50 </view>
@@ -85,14 +55,8 @@ @@ -85,14 +55,8 @@
85 <text class="form-label">登录密码</text> 55 <text class="form-label">登录密码</text>
86 <view class="input-box"> 56 <view class="input-box">
87 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 57 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
88 - <input  
89 - type="password"  
90 - v-model="password"  
91 - placeholder="请输入您的密码"  
92 - placeholder-style="color: #a1a8b3"  
93 - class="native-input"  
94 - :password="true"  
95 - /> 58 + <input type="password" v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3"
  59 + class="native-input" :password="true" />
96 </view> 60 </view>
97 </view> 61 </view>
98 </view> 62 </view>
@@ -121,439 +85,439 @@ @@ -121,439 +85,439 @@
121 </template> 85 </template>
122 86
123 <script setup> 87 <script setup>
124 - import { ref, onUnmounted } from 'vue';  
125 - import sheep from '@/sheep';  
126 - import AuthUtil from '@/sheep/api/member/auth';  
127 - import { onHide } from '@dcloudio/uni-app';  
128 -  
129 - // 用于跨端兼容渲染的 Base64 编码火焰 SVG (绿渐变)  
130 - const logoSvgBase64 =  
131 - '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>';  
132 -  
133 - const activeTab = ref('sms'); // sms: 免密, password: 密码  
134 - const phoneNumber = ref('');  
135 - const verifyCode = ref('');  
136 - const password = ref('');  
137 - const isAgreed = ref(false);  
138 -  
139 - // 防抖及加载动画状态变量  
140 - const isSending = ref(false);  
141 - const isSubmitting = ref(false);  
142 -  
143 - // 验证码倒计时逻辑  
144 - const countdown = ref(0);  
145 - let timer = null;  
146 -  
147 - // 验证手机号码格式  
148 - const validatePhone = (phone) => {  
149 - return /^1[3-9]\d{9}$/.test(phone);  
150 - };  
151 -  
152 - // 切换Tab,同时清理另一个 Tab 的输入  
153 - const switchTab = (type) => {  
154 - activeTab.value = type;  
155 - verifyCode.value = '';  
156 - password.value = '';  
157 - };  
158 -  
159 - // 发送验证码  
160 - const handleGetCode = async () => {  
161 - if (countdown.value > 0 || isSending.value) return;  
162 -  
163 - // 前置逻辑校验  
164 - if (!phoneNumber.value) {  
165 - uni.showToast({ title: '请输入手机号码', icon: 'none' });  
166 - return;  
167 - }  
168 - if (!validatePhone(phoneNumber.value)) {  
169 - uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });  
170 - return;  
171 - } 88 +import { ref, onUnmounted } from 'vue';
  89 +import sheep from '@/sheep';
  90 +import AuthUtil from '@/sheep/api/member/auth';
  91 +import { onHide } from '@dcloudio/uni-app';
  92 +
  93 +// 用于跨端兼容渲染的 Base64 编码火焰 SVG (绿渐变)
  94 +const logoSvgBase64 =
  95 + '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>';
  96 +
  97 +const activeTab = ref('sms'); // sms: 免密, password: 密码
  98 +const phoneNumber = ref('');
  99 +const verifyCode = ref('');
  100 +const password = ref('');
  101 +const isAgreed = ref(false);
  102 +
  103 +// 防抖及加载动画状态变量
  104 +const isSending = ref(false);
  105 +const isSubmitting = ref(false);
  106 +
  107 +// 验证码倒计时逻辑
  108 +const countdown = ref(0);
  109 +let timer = null;
  110 +
  111 +// 验证手机号码格式
  112 +const validatePhone = (phone) => {
  113 + return /^1[3-9]\d{9}$/.test(phone);
  114 +};
  115 +
  116 +// 切换Tab,同时清理另一个 Tab 的输入
  117 +const switchTab = (type) => {
  118 + activeTab.value = type;
  119 + verifyCode.value = '';
  120 + password.value = '';
  121 +};
  122 +
  123 +// 发送验证码
  124 +const handleGetCode = async () => {
  125 + if (countdown.value > 0 || isSending.value) return;
  126 +
  127 + // 前置逻辑校验
  128 + if (!phoneNumber.value) {
  129 + uni.showToast({ title: '请输入手机号码', icon: 'none' });
  130 + return;
  131 + }
  132 + if (!validatePhone(phoneNumber.value)) {
  133 + uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
  134 + return;
  135 + }
172 136
173 - isSending.value = true;  
174 - try {  
175 - // 成功发送验证码请求  
176 - const res = await AuthUtil.sendSmsCode({ phone: phoneNumber.value });  
177 -  
178 - uni.showToast({ title: '验证码已发送', icon: 'success' });  
179 - countdown.value = 60;  
180 - timer = setInterval(() => {  
181 - countdown.value--;  
182 - if (countdown.value <= 0) {  
183 - clearInterval(timer);  
184 - timer = null;  
185 - }  
186 - }, 1000);  
187 - } catch (err) {  
188 - console.error('发送验证码失败异常:', err);  
189 - } finally {  
190 - isSending.value = false;  
191 - }  
192 - }; 137 + isSending.value = true;
  138 + try {
  139 + // 成功发送验证码请求
  140 + const res = await AuthUtil.sendSmsCode({ phone: phoneNumber.value });
  141 +
  142 + uni.showToast({ title: '验证码已发送', icon: 'success' });
  143 + countdown.value = 60;
  144 + timer = setInterval(() => {
  145 + countdown.value--;
  146 + if (countdown.value <= 0) {
  147 + clearInterval(timer);
  148 + timer = null;
  149 + }
  150 + }, 1000);
  151 + } catch (err) {
  152 + console.error('发送验证码失败异常:', err);
  153 + } finally {
  154 + isSending.value = false;
  155 + }
  156 +};
193 157
194 - // 登录按钮提交逻辑  
195 - const handleLogin = async () => {  
196 - if (isSubmitting.value) return; 158 +// 登录按钮提交逻辑
  159 +const handleLogin = async () => {
  160 + if (isSubmitting.value) return;
  161 +
  162 + // 1. 用户协议验证
  163 + if (!isAgreed.value) {
  164 + uni.showToast({ title: '请先阅读并同意用户协议及隐私政策', icon: 'none' });
  165 + return;
  166 + }
  167 +
  168 + // 2. 基础手机号格式验证
  169 + if (!phoneNumber.value) {
  170 + uni.showToast({ title: '请输入手机号码', icon: 'none' });
  171 + return;
  172 + }
  173 + if (!validatePhone(phoneNumber.value)) {
  174 + uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
  175 + return;
  176 + }
197 177
198 - // 1. 用户协议验证  
199 - if (!isAgreed.value) {  
200 - uni.showToast({ title: '请先阅读并同意用户协议及隐私政策', icon: 'none' }); 178 + // 3. Tab特定表单验证并请求
  179 + if (activeTab.value === 'sms') {
  180 + if (!verifyCode.value) {
  181 + uni.showToast({ title: '请输入验证码', icon: 'none' });
201 return; 182 return;
202 } 183 }
203 -  
204 - // 2. 基础手机号格式验证  
205 - if (!phoneNumber.value) {  
206 - uni.showToast({ title: '请输入手机号码', icon: 'none' }); 184 + if (verifyCode.value.length < 4) {
  185 + uni.showToast({ title: '请输入完整的验证码', icon: 'none' });
207 return; 186 return;
208 } 187 }
209 - if (!validatePhone(phoneNumber.value)) {  
210 - uni.showToast({ title: '请输入正确的手机号码', icon: 'none' }); 188 + await performLogin('sms');
  189 + } else {
  190 + if (!password.value) {
  191 + uni.showToast({ title: '请输入密码', icon: 'none' });
211 return; 192 return;
212 } 193 }
213 -  
214 - // 3. Tab特定表单验证并请求  
215 - if (activeTab.value === 'sms') {  
216 - if (!verifyCode.value) {  
217 - uni.showToast({ title: '请输入验证码', icon: 'none' });  
218 - return;  
219 - }  
220 - if (verifyCode.value.length < 4) {  
221 - uni.showToast({ title: '请输入完整的验证码', icon: 'none' });  
222 - return;  
223 - }  
224 - await performLogin('sms'); 194 + await performLogin('password');
  195 + }
  196 +};
  197 +
  198 +// 执行最终请求与状态写入
  199 +const performLogin = async (type) => {
  200 + isSubmitting.value = true;
  201 + uni.showLoading({ title: '正在登录...', mask: true });
  202 +
  203 + try {
  204 + let res = null;
  205 + if (type === 'sms') {
  206 + // 修复:原代码使用的 AuthApi 变更为导入的 AuthUtil 服务
  207 + res = await AuthUtil.loginBySms({
  208 + phone: phoneNumber.value,
  209 + code: verifyCode.value,
  210 + });
225 } else { 211 } else {
226 - if (!password.value) {  
227 - uni.showToast({ title: '请输入密码', icon: 'none' });  
228 - return;  
229 - }  
230 - await performLogin('password'); 212 + res = await AuthUtil.loginByPhonePassword({
  213 + phone: phoneNumber.value,
  214 + password: password.value,
  215 + });
231 } 216 }
232 - };  
233 -  
234 - // 执行最终请求与状态写入  
235 - const performLogin = async (type) => {  
236 - isSubmitting.value = true;  
237 - uni.showLoading({ title: '正在登录...', mask: true });  
238 -  
239 - try {  
240 - let res = null;  
241 - if (type === 'sms') {  
242 - // 修复:原代码使用的 AuthApi 变更为导入的 AuthUtil 服务  
243 - res = await AuthUtil.loginBySms({  
244 - phone: phoneNumber.value,  
245 - code: verifyCode.value,  
246 - });  
247 - } else {  
248 - res = await AuthUtil.loginByPhonePassword({  
249 - phone: phoneNumber.value,  
250 - password: password.value,  
251 - });  
252 - }  
253 217
254 - const authData = res.data || {}; 218 + const authData = res.data || {};
255 219
256 - if (authData && authData.accessToken) {  
257 - // 1. 取得芋道商城 Pinia 内置的 user Store  
258 - const userStore = sheep.$store('user'); 220 + if (authData && authData.accessToken) {
  221 + // 1. 取得芋道商城 Pinia 内置的 user Store
  222 + const userStore = sheep.$store('user');
259 223
260 - // 2. 写入 Token 到 Pinia 及持久态 LocalStorage  
261 - userStore.setToken(authData.accessToken, authData.refreshToken || ''); 224 + // 2. 写入 Token 到 Pinia 及持久态 LocalStorage
  225 + userStore.setToken(authData.accessToken, authData.refreshToken || '');
262 226
263 - // 3. 登录成功后,主动拉取一次用户信息同步至本地状态  
264 - await userStore.getInfo(); 227 + // 3. 登录成功后,主动拉取一次用户信息同步至本地状态
  228 + await userStore.getInfo();
265 229
266 - uni.showToast({  
267 - title: '登录成功',  
268 - icon: 'success',  
269 - }); 230 + uni.showToast({
  231 + title: '登录成功',
  232 + icon: 'success',
  233 + });
270 234
271 - // 4. 跳转至目标页面  
272 - setTimeout(() => {  
273 - uni.switchTab({  
274 - url: '/pages/xunji/xunji',  
275 - });  
276 - }, 100);  
277 - } else {  
278 - uni.showToast({  
279 - title: res.msg || '登录失败,未获取到有效凭证',  
280 - icon: 'none', 235 + // 4. 跳转至目标页面
  236 + setTimeout(() => {
  237 + uni.switchTab({
  238 + url: '/pages/xunji/xunji',
281 }); 239 });
282 - }  
283 - } catch (err) {  
284 - console.error('登录逻辑异常:', err);  
285 - } finally {  
286 - isSubmitting.value = false;  
287 - uni.hideLoading();  
288 - }  
289 - };  
290 -  
291 - // 忘记密码跳转  
292 - const goToForgetPassword = () => {  
293 - uni.navigateTo({  
294 - url: '/pages7/pages/index/reset-password',  
295 - });  
296 - };  
297 -  
298 - // 协议详情跳转  
299 - const goToAgreement = (type) => {  
300 - const url =  
301 - type === 'service'  
302 - ? '/pages/public/richtext?id=service'  
303 - : '/pages/public/richtext?id=privacy';  
304 - uni.navigateTo({  
305 - url,  
306 - fail: () => {  
307 - uni.showToast({  
308 - title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,  
309 - icon: 'none',  
310 - });  
311 - },  
312 - });  
313 - };  
314 -  
315 - // 页面卸载生命周期:清除定时器,规避潜在的内存泄露  
316 - onHide(() => {  
317 - if (timer) {  
318 - clearInterval(timer);  
319 - timer = null; 240 + }, 100);
  241 + } else {
  242 + uni.showToast({
  243 + title: res.msg || '登录失败,未获取到有效凭证',
  244 + icon: 'none',
  245 + });
320 } 246 }
  247 + } catch (err) {
  248 + console.error('登录逻辑异常:', err);
  249 + } finally {
  250 + isSubmitting.value = false;
  251 + uni.hideLoading();
  252 + }
  253 +};
  254 +
  255 +// 忘记密码跳转
  256 +const goToForgetPassword = () => {
  257 + uni.navigateTo({
  258 + url: '/pages7/pages/index/reset-password?isModify=false',
321 }); 259 });
  260 +};
  261 +
  262 +// 协议详情跳转
  263 +const goToAgreement = (type) => {
  264 + const url =
  265 + type === 'service'
  266 + ? '/pages/public/richtext?id=service'
  267 + : '/pages/public/richtext?id=privacy';
  268 + uni.navigateTo({
  269 + url,
  270 + fail: () => {
  271 + uni.showToast({
  272 + title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,
  273 + icon: 'none',
  274 + });
  275 + },
  276 + });
  277 +};
  278 +
  279 +// 页面卸载生命周期:清除定时器,规避潜在的内存泄露
  280 +onHide(() => {
  281 + if (timer) {
  282 + clearInterval(timer);
  283 + timer = null;
  284 + }
  285 +});
322 </script> 286 </script>
323 287
324 <style lang="scss" scoped> 288 <style lang="scss" scoped>
325 - .login-wrapper {  
326 - height: 100vh;  
327 - // #ifdef H5  
328 - height: calc(100vh - 44px);  
329 - // #endif  
330 - background-color: #ffffff;  
331 - padding: 80rpx 50rpx; 289 +.login-wrapper {
  290 + height: 100vh;
  291 + // #ifdef H5
  292 + height: calc(100vh - 44px);
  293 + // #endif
  294 + background-color: #ffffff;
  295 + padding: 80rpx 50rpx;
  296 + box-sizing: border-box;
  297 +
  298 + // 1. Logo
  299 + .logo-section {
  300 + display: flex;
  301 + flex-direction: column;
  302 + align-items: center;
  303 + margin-bottom: 70rpx;
  304 + margin-top: 40rpx;
  305 +
  306 + .logo-box {
  307 + width: 140rpx;
  308 + height: 140rpx;
  309 + border-radius: 40rpx;
  310 + background: #ffffff;
  311 + border: 3rpx solid #d2ee9e;
  312 + box-shadow: 0 16rpx 40rpx rgba(111, 214, 32, 0.12);
  313 + display: flex;
  314 + justify-content: center;
  315 + align-items: center;
  316 + margin-bottom: 30rpx;
  317 +
  318 + .logo-img {
  319 + width: 76rpx;
  320 + height: 76rpx;
  321 + }
  322 + }
  323 +
  324 + .brand-title {
  325 + font-size: 46rpx;
  326 + font-weight: bold;
  327 + color: #0a1931;
  328 + letter-spacing: 2rpx;
  329 + }
  330 +
  331 + .brand-slogan {
  332 + font-size: 26rpx;
  333 + color: #9097a3;
  334 + margin-top: 12rpx;
  335 + }
  336 + }
  337 +
  338 + // 2. Tab
  339 + .tab-container {
  340 + height: 96rpx;
  341 + background-color: #f1f3f7;
  342 + border-radius: 20rpx;
  343 + display: flex;
  344 + padding: 8rpx;
332 box-sizing: border-box; 345 box-sizing: border-box;
  346 + margin-bottom: 50rpx;
333 347
334 - // 1. Logo  
335 - .logo-section { 348 + .tab-item {
  349 + flex: 1;
336 display: flex; 350 display: flex;
337 - flex-direction: column; 351 + justify-content: center;
338 align-items: center; 352 align-items: center;
339 - margin-bottom: 70rpx;  
340 - margin-top: 40rpx;  
341 -  
342 - .logo-box {  
343 - width: 140rpx;  
344 - height: 140rpx;  
345 - border-radius: 40rpx;  
346 - background: #ffffff;  
347 - border: 3rpx solid #d2ee9e;  
348 - box-shadow: 0 16rpx 40rpx rgba(111, 214, 32, 0.12);  
349 - display: flex;  
350 - justify-content: center;  
351 - align-items: center;  
352 - margin-bottom: 30rpx; 353 + font-size: 28rpx;
  354 + color: #7a8290;
  355 + font-weight: 500;
  356 + transition: all 0.2s ease;
  357 + border-radius: 16rpx;
353 358
354 - .logo-img {  
355 - width: 76rpx;  
356 - height: 76rpx;  
357 - } 359 + .tab-icon {
  360 + margin-right: 12rpx;
358 } 361 }
359 362
360 - .brand-title {  
361 - font-size: 46rpx;  
362 - font-weight: bold; 363 + &.active {
  364 + background-color: #ffffff;
363 color: #0a1931; 365 color: #0a1931;
364 - letter-spacing: 2rpx; 366 + font-weight: bold;
  367 + box-shadow: 0 4rpx 16rpx rgba(10, 25, 49, 0.05);
365 } 368 }
  369 + }
  370 + }
366 371
367 - .brand-slogan { 372 + // 3. Form
  373 + .form-container {
  374 + .form-item-wrapper {
  375 + margin-bottom: 36rpx;
  376 +
  377 + .form-label {
368 font-size: 26rpx; 378 font-size: 26rpx;
369 - color: #9097a3;  
370 - margin-top: 12rpx; 379 + color: #0a1931;
  380 + font-weight: bold;
  381 + display: block;
  382 + margin-bottom: 16rpx;
371 } 383 }
372 - }  
373 -  
374 - // 2. Tab  
375 - .tab-container {  
376 - height: 96rpx;  
377 - background-color: #f1f3f7;  
378 - border-radius: 20rpx;  
379 - display: flex;  
380 - padding: 8rpx;  
381 - box-sizing: border-box;  
382 - margin-bottom: 50rpx;  
383 384
384 - .tab-item {  
385 - flex: 1; 385 + .input-box {
  386 + height: 104rpx;
  387 + background-color: #f4f6fa;
  388 + border: 2rpx solid #e1e6eb;
  389 + border-radius: 24rpx;
386 display: flex; 390 display: flex;
387 - justify-content: center;  
388 align-items: center; 391 align-items: center;
389 - font-size: 28rpx;  
390 - color: #7a8290;  
391 - font-weight: 500;  
392 - transition: all 0.2s ease;  
393 - border-radius: 16rpx; 392 + padding: 0 30rpx;
  393 + box-sizing: border-box;
394 394
395 - .tab-icon {  
396 - margin-right: 12rpx; 395 + .input-icon {
  396 + margin-right: 16rpx;
397 } 397 }
398 398
399 - &.active {  
400 - background-color: #ffffff; 399 + .native-input {
  400 + flex: 1;
  401 + height: 100%;
  402 + font-size: 28rpx;
401 color: #0a1931; 403 color: #0a1931;
402 - font-weight: bold;  
403 - box-shadow: 0 4rpx 16rpx rgba(10, 25, 49, 0.05);  
404 } 404 }
405 } 405 }
406 - }  
407 406
408 - // 3. Form  
409 - .form-container {  
410 - .form-item-wrapper {  
411 - margin-bottom: 36rpx; 407 + // 验证码一栏的双列布局
  408 + .code-row {
  409 + display: flex;
  410 + align-items: center;
412 411
413 - .form-label {  
414 - font-size: 26rpx;  
415 - color: #0a1931;  
416 - font-weight: bold;  
417 - display: block;  
418 - margin-bottom: 16rpx; 412 + .flex-1 {
  413 + flex: 1;
419 } 414 }
420 415
421 - .input-box { 416 + .code-btn {
  417 + width: 220rpx;
422 height: 104rpx; 418 height: 104rpx;
423 - background-color: #f4f6fa;  
424 - border: 2rpx solid #e1e6eb;  
425 border-radius: 24rpx; 419 border-radius: 24rpx;
  420 + background-color: #f9fdf2;
  421 + border: 2rpx solid #d0ee9c;
426 display: flex; 422 display: flex;
  423 + justify-content: center;
427 align-items: center; 424 align-items: center;
428 - padding: 0 30rpx;  
429 - box-sizing: border-box;  
430 -  
431 - .input-icon {  
432 - margin-right: 16rpx;  
433 - }  
434 -  
435 - .native-input {  
436 - flex: 1;  
437 - height: 100%;  
438 - font-size: 28rpx;  
439 - color: #0a1931;  
440 - }  
441 - }  
442 -  
443 - // 验证码一栏的双列布局  
444 - .code-row {  
445 - display: flex;  
446 - align-items: center; 425 + margin-left: 20rpx;
  426 + font-size: 26rpx;
  427 + color: #8fc31f;
  428 + font-weight: bold;
  429 + transition: all 0.2s ease;
447 430
448 - .flex-1 {  
449 - flex: 1; 431 + &:active {
  432 + opacity: 0.8;
450 } 433 }
451 434
452 - .code-btn {  
453 - width: 220rpx;  
454 - height: 104rpx;  
455 - border-radius: 24rpx;  
456 - background-color: #f9fdf2;  
457 - border: 2rpx solid #d0ee9c;  
458 - display: flex;  
459 - justify-content: center;  
460 - align-items: center;  
461 - margin-left: 20rpx;  
462 - font-size: 26rpx;  
463 - color: #8fc31f;  
464 - font-weight: bold;  
465 - transition: all 0.2s ease;  
466 -  
467 - &:active {  
468 - opacity: 0.8;  
469 - }  
470 -  
471 - &.disabled {  
472 - color: #b0b8c4;  
473 - background-color: #f4f6fa;  
474 - border-color: #e1e6eb;  
475 - pointer-events: none; // 禁用点击以避免多余的触发  
476 - } 435 + &.disabled {
  436 + color: #b0b8c4;
  437 + background-color: #f4f6fa;
  438 + border-color: #e1e6eb;
  439 + pointer-events: none; // 禁用点击以避免多余的触发
477 } 440 }
478 } 441 }
479 } 442 }
480 } 443 }
  444 + }
481 445
482 - .forget-password {  
483 - text-align: right;  
484 - margin-top: 20rpx;  
485 - font-size: 24rpx;  
486 - color: rgb(220, 54, 46);  
487 - } 446 + .forget-password {
  447 + text-align: right;
  448 + margin-top: 20rpx;
  449 + font-size: 24rpx;
  450 + color: rgb(220, 54, 46);
  451 + }
488 452
489 - // 4. Agreement  
490 - .agreement-container { 453 + // 4. Agreement
  454 + .agreement-container {
  455 + display: flex;
  456 + align-items: flex-start;
  457 + margin-top: 40rpx;
  458 + margin-bottom: 50rpx;
  459 + padding: 0 6rpx;
  460 +
  461 + .checkbox-box {
  462 + width: 32rpx;
  463 + height: 32rpx;
  464 + border: 2rpx solid #c2c9d3;
  465 + border-radius: 8rpx;
  466 + margin-right: 16rpx;
  467 + margin-top: 4rpx;
491 display: flex; 468 display: flex;
492 - align-items: flex-start;  
493 - margin-top: 40rpx;  
494 - margin-bottom: 50rpx;  
495 - padding: 0 6rpx;  
496 -  
497 - .checkbox-box {  
498 - width: 32rpx;  
499 - height: 32rpx;  
500 - border: 2rpx solid #c2c9d3;  
501 - border-radius: 8rpx;  
502 - margin-right: 16rpx;  
503 - margin-top: 4rpx;  
504 - display: flex;  
505 - justify-content: center;  
506 - align-items: center;  
507 - transition: all 0.15s ease;  
508 - box-sizing: border-box; 469 + justify-content: center;
  470 + align-items: center;
  471 + transition: all 0.15s ease;
  472 + box-sizing: border-box;
509 473
510 - &.checked {  
511 - border-color: #00b074;  
512 - background-color: #00b074;  
513 - } 474 + &.checked {
  475 + border-color: #00b074;
  476 + background-color: #00b074;
514 } 477 }
  478 + }
515 479
516 - .agreement-text {  
517 - flex: 1;  
518 - font-size: 24rpx;  
519 - color: #7a8290;  
520 - line-height: 1.5; 480 + .agreement-text {
  481 + flex: 1;
  482 + font-size: 24rpx;
  483 + color: #7a8290;
  484 + line-height: 1.5;
521 485
522 - .link-text {  
523 - color: #8fc31f;  
524 - font-weight: 500;  
525 - } 486 + .link-text {
  487 + color: #8fc31f;
  488 + font-weight: 500;
526 } 489 }
527 } 490 }
  491 + }
528 492
529 - // 5. Submit Button  
530 - .submit-btn {  
531 - height: 104rpx;  
532 - border-radius: 24rpx;  
533 - background: linear-gradient(135deg, #79d621 0%, #00b074 100%);  
534 - box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);  
535 - display: flex;  
536 - justify-content: center;  
537 - align-items: center;  
538 - color: #ffffff;  
539 - font-size: 32rpx;  
540 - font-weight: bold;  
541 - letter-spacing: 2rpx;  
542 - transition: all 0.2s ease;  
543 -  
544 - &:active {  
545 - transform: scale(0.98);  
546 - opacity: 0.9;  
547 - } 493 + // 5. Submit Button
  494 + .submit-btn {
  495 + height: 104rpx;
  496 + border-radius: 24rpx;
  497 + background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
  498 + box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
  499 + display: flex;
  500 + justify-content: center;
  501 + align-items: center;
  502 + color: #ffffff;
  503 + font-size: 32rpx;
  504 + font-weight: bold;
  505 + letter-spacing: 2rpx;
  506 + transition: all 0.2s ease;
  507 +
  508 + &:active {
  509 + transform: scale(0.98);
  510 + opacity: 0.9;
  511 + }
548 512
549 - &.disabled-btn {  
550 - opacity: 0.75;  
551 - pointer-events: none; // 阻止后续的多余点击  
552 - } 513 + &.disabled-btn {
  514 + opacity: 0.75;
  515 + pointer-events: none; // 阻止后续的多余点击
  516 + }
553 517
554 - .btn-arrow {  
555 - margin-left: 10rpx;  
556 - } 518 + .btn-arrow {
  519 + margin-left: 10rpx;
557 } 520 }
558 } 521 }
  522 +}
559 </style> 523 </style>
@@ -7,14 +7,8 @@ @@ -7,14 +7,8 @@
7 <text class="form-label">手机号码</text> 7 <text class="form-label">手机号码</text>
8 <view class="input-box"> 8 <view class="input-box">
9 <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" /> 9 <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
10 - <input  
11 - type="number"  
12 - v-model="phone"  
13 - placeholder="请输入您的手机号"  
14 - placeholder-style="color: #a1a8b3"  
15 - maxlength="11"  
16 - class="native-input"  
17 - /> 10 + <input type="number" v-model="phone" placeholder="请输入您的手机号" placeholder-style="color: #a1a8b3" maxlength="11"
  11 + class="native-input" />
18 </view> 12 </view>
19 </view> 13 </view>
20 14
@@ -24,21 +18,11 @@ @@ -24,21 +18,11 @@
24 <view class="code-row"> 18 <view class="code-row">
25 <view class="input-box flex-1"> 19 <view class="input-box flex-1">
26 <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" /> 20 <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
27 - <input  
28 - type="number"  
29 - v-model="code"  
30 - placeholder="6位验证码"  
31 - placeholder-style="color: #a1a8b3"  
32 - maxlength="6"  
33 - class="native-input"  
34 - /> 21 + <input type="number" v-model="code" placeholder="6位验证码" placeholder-style="color: #a1a8b3" maxlength="6"
  22 + class="native-input" />
35 </view> 23 </view>
36 <!-- 获取验证码按钮 --> 24 <!-- 获取验证码按钮 -->
37 - <view  
38 - class="code-btn"  
39 - :class="{ disabled: countdown > 0 || isSending }"  
40 - @click="handleSendCode"  
41 - > 25 + <view class="code-btn" :class="{ disabled: countdown > 0 || isSending }" @click="handleSendCode">
42 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text> 26 <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
43 </view> 27 </view>
44 </view> 28 </view>
@@ -49,20 +33,10 @@ @@ -49,20 +33,10 @@
49 <text class="form-label">设置新密码</text> 33 <text class="form-label">设置新密码</text>
50 <view class="input-box"> 34 <view class="input-box">
51 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 35 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
52 - <input  
53 - :type="showPassword ? 'text' : 'password'"  
54 - v-model="password"  
55 - placeholder="请设置您的新密码"  
56 - placeholder-style="color: #a1a8b3"  
57 - class="native-input"  
58 - />  
59 - <u-icon  
60 - :name="showPassword ? 'eye-fill' : 'eye'"  
61 - size="20"  
62 - color="#a1a8b3"  
63 - class="eye-icon"  
64 - @click="showPassword = !showPassword"  
65 - /> 36 + <input :type="showPassword ? 'text' : 'password'" v-model="password" placeholder="请设置您的新密码"
  37 + placeholder-style="color: #a1a8b3" class="native-input" />
  38 + <u-icon :name="showPassword ? 'eye-fill' : 'eye'" size="20" color="#a1a8b3" class="eye-icon"
  39 + @click="showPassword = !showPassword" />
66 </view> 40 </view>
67 <text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text> 41 <text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text>
68 </view> 42 </view>
@@ -72,20 +46,10 @@ @@ -72,20 +46,10 @@
72 <text class="form-label">确认密码</text> 46 <text class="form-label">确认密码</text>
73 <view class="input-box"> 47 <view class="input-box">
74 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" /> 48 <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
75 - <input  
76 - :type="showConfirmPassword ? 'text' : 'password'"  
77 - v-model="confirmPassword"  
78 - placeholder="请再次输入新密码"  
79 - placeholder-style="color: #a1a8b3"  
80 - class="native-input"  
81 - />  
82 - <u-icon  
83 - :name="showConfirmPassword ? 'eye-fill' : 'eye'"  
84 - size="20"  
85 - color="#a1a8b3"  
86 - class="eye-icon"  
87 - @click="showConfirmPassword = !showConfirmPassword"  
88 - /> 49 + <input :type="showConfirmPassword ? 'text' : 'password'" v-model="confirmPassword" placeholder="请再次输入新密码"
  50 + placeholder-style="color: #a1a8b3" class="native-input" />
  51 + <u-icon :name="showConfirmPassword ? 'eye-fill' : 'eye'" size="20" color="#a1a8b3" class="eye-icon"
  52 + @click="showConfirmPassword = !showConfirmPassword" />
89 </view> 53 </view>
90 </view> 54 </view>
91 </view> 55 </view>
@@ -98,256 +62,275 @@ @@ -98,256 +62,275 @@
98 </template> 62 </template>
99 63
100 <script setup> 64 <script setup>
101 - import { ref } from 'vue';  
102 - import AuthUtil from '@/sheep/api/member/auth';  
103 - import { onHide } from '@dcloudio/uni-app';  
104 - const phone = ref('');  
105 - const code = ref('');  
106 - const password = ref('');  
107 - const confirmPassword = ref('');  
108 -  
109 - // 密码显示隐藏控制  
110 - const showPassword = ref(false);  
111 - const showConfirmPassword = ref(false);  
112 -  
113 - // 防抖及倒计时状态  
114 - const isSending = ref(false);  
115 - const isSubmitting = ref(false);  
116 - const countdown = ref(0);  
117 - let timer = null;  
118 -  
119 - // 验证手机号码格式  
120 - const validatePhone = (num) => {  
121 - return /^1[3-9]\d{9}$/.test(num);  
122 - };  
123 -  
124 - // 强密码复杂度检测:8~16位,大小写、数字、特殊字符  
125 - const validatePasswordStrength = (pwd) => {  
126 - const reg =  
127 - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/;  
128 - return reg.test(pwd);  
129 - };  
130 -  
131 - // 发送短信验证码  
132 - const handleSendCode = async () => {  
133 - if (countdown.value > 0 || isSending.value) return;  
134 -  
135 - if (!phone.value) {  
136 - uni.showToast({ title: '请输入手机号码', icon: 'none' });  
137 - return;  
138 - }  
139 - if (!validatePhone(phone.value)) {  
140 - uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });  
141 - return;  
142 - } 65 +import { ref } from 'vue';
  66 +import AuthUtil from '@/sheep/api/member/auth';
  67 +import { onHide, onLoad } from '@dcloudio/uni-app';
  68 +const phone = ref('');
  69 +const code = ref('');
  70 +const password = ref('');
  71 +const confirmPassword = ref('');
  72 +
  73 +// 密码显示隐藏控制
  74 +const showPassword = ref(false);
  75 +const showConfirmPassword = ref(false);
  76 +
  77 +// 防抖及倒计时状态
  78 +const isSending = ref(false);
  79 +const isSubmitting = ref(false);
  80 +const countdown = ref(0);
  81 +let timer = null;
  82 +
  83 +// 验证手机号码格式
  84 +const validatePhone = (num) => {
  85 + return /^1[3-9]\d{9}$/.test(num);
  86 +};
  87 +
  88 +// 强密码复杂度检测:8~16位,大小写、数字、特殊字符
  89 +const validatePasswordStrength = (pwd) => {
  90 + const reg =
  91 + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/;
  92 + return reg.test(pwd);
  93 +};
  94 +
  95 +// 发送短信验证码
  96 +const handleSendCode = async () => {
  97 + if (countdown.value > 0 || isSending.value) return;
  98 +
  99 + if (!phone.value) {
  100 + uni.showToast({ title: '请输入手机号码', icon: 'none' });
  101 + return;
  102 + }
  103 + if (!validatePhone(phone.value)) {
  104 + uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
  105 + return;
  106 + }
143 107
144 - isSending.value = true;  
145 - try {  
146 - const res = await AuthUtil.sendSmsCode({ phone: phone.value });  
147 - if (res && (res.code === 0 || res.data === true)) {  
148 - uni.showToast({ title: '验证码已发送', icon: 'success' });  
149 - countdown.value = 60;  
150 - timer = setInterval(() => {  
151 - countdown.value--;  
152 - if (countdown.value <= 0) {  
153 - clearInterval(timer);  
154 - timer = null;  
155 - }  
156 - }, 1000);  
157 - } else {  
158 - uni.showToast({ title: res.msg || '获取验证码失败', icon: 'none' });  
159 - }  
160 - } catch (err) {  
161 - console.error('发送验证码失败异常:', err);  
162 - } finally {  
163 - isSending.value = false; 108 + isSending.value = true;
  109 + try {
  110 + const res = await AuthUtil.sendSmsCode({ phone: phone.value });
  111 + if (res && (res.code === 0 || res.data === true)) {
  112 + uni.showToast({ title: '验证码已发送', icon: 'success' });
  113 + countdown.value = 60;
  114 + timer = setInterval(() => {
  115 + countdown.value--;
  116 + if (countdown.value <= 0) {
  117 + clearInterval(timer);
  118 + timer = null;
  119 + }
  120 + }, 1000);
  121 + } else {
  122 + uni.showToast({ title: res.msg || '获取验证码失败', icon: 'none' });
164 } 123 }
165 - }; 124 + } catch (err) {
  125 + console.error('发送验证码失败异常:', err);
  126 + } finally {
  127 + isSending.value = false;
  128 + }
  129 +};
166 130
167 - // 保存并提交重置密码  
168 - const handleSubmit = async () => {  
169 - if (isSubmitting.value) return; 131 +// 保存并提交重置密码
  132 +const handleSubmit = async () => {
  133 + if (isSubmitting.value) return;
170 134
171 - if (!phone.value || !validatePhone(phone.value)) {  
172 - uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });  
173 - return;  
174 - }  
175 - if (!code.value || code.value.length < 4) {  
176 - uni.showToast({ title: '请输入短信验证码', icon: 'none' });  
177 - return;  
178 - }  
179 - if (!password.value) {  
180 - uni.showToast({ title: '请输入您的新密码', icon: 'none' });  
181 - return;  
182 - }  
183 - // if (!validatePasswordStrength(password.value)) {  
184 - // uni.showToast({ title: '密码须为8~16位并包含大小写字母、数字及特殊字符', icon: 'none' });  
185 - // return;  
186 - // }  
187 - if (password.value !== confirmPassword.value) {  
188 - uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });  
189 - return;  
190 - } 135 + if (!phone.value || !validatePhone(phone.value)) {
  136 + uni.showToast({ title: '请输入正确的11位手机号码', icon: 'none' });
  137 + return;
  138 + }
  139 + if (!code.value || code.value.length < 4) {
  140 + uni.showToast({ title: '请输入短信验证码', icon: 'none' });
  141 + return;
  142 + }
  143 + if (!password.value) {
  144 + uni.showToast({ title: '请输入您的新密码', icon: 'none' });
  145 + return;
  146 + }
  147 + // if (!validatePasswordStrength(password.value)) {
  148 + // uni.showToast({ title: '密码须为8~16位并包含大小写字母、数字及特殊字符', icon: 'none' });
  149 + // return;
  150 + // }
  151 + if (password.value !== confirmPassword.value) {
  152 + uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });
  153 + return;
  154 + }
191 155
192 - isSubmitting.value = true;  
193 - uni.showLoading({ title: '正在修改密码...', mask: true });  
194 -  
195 - try {  
196 - const res = await AuthUtil.setPassword({  
197 - phone: phone.value,  
198 - code: code.value,  
199 - password: password.value,  
200 - });  
201 -  
202 - uni.redirectTo({  
203 - url: '/pages7/pages/index/login',  
204 - });  
205 - } catch (err) {  
206 - console.error('重置密码运行异常:', err);  
207 - } finally {  
208 - isSubmitting.value = false;  
209 - uni.hideLoading();  
210 - }  
211 - }; 156 + isSubmitting.value = true;
  157 + uni.showLoading({ title: '正在修改密码...', mask: true });
  158 +
  159 + try {
  160 + const res = await AuthUtil.setPassword({
  161 + phone: phone.value,
  162 + code: code.value,
  163 + password: password.value,
  164 + });
  165 +
  166 + uni.redirectTo({
  167 + url: '/pages7/pages/index/login',
  168 + });
  169 + } catch (err) {
  170 + console.error('重置密码运行异常:', err);
  171 + } finally {
  172 + isSubmitting.value = false;
  173 + uni.hideLoading();
  174 + }
  175 +};
  176 +
  177 +// 生命周期管理:清除定时器
  178 +onHide(() => {
  179 + if (timer) {
  180 + clearInterval(timer);
  181 + timer = null;
  182 + }
  183 +});
  184 +
  185 +const isForgetPassword = ref(false)
  186 +
  187 +const isModify = ref(false);
  188 +
  189 +onLoad((options) => {
  190 + // 路由参数是字符串,需要转布尔判断
  191 + isModify.value = options.isModify === 'true';
  192 + console.log('isModify.value=', isModify.value);
  193 +
  194 + if (isModify.value) {
  195 + // 修改密码场景
  196 + uni.setNavigationBarTitle({ title: '修改密码' });
  197 + } else {
  198 + // 忘记密码场景
  199 + uni.setNavigationBarTitle({ title: '找回密码' });
  200 + }
  201 +});
212 202
213 - // 生命周期管理:清除定时器  
214 - onHide(() => {  
215 - if (timer) {  
216 - clearInterval(timer);  
217 - timer = null;  
218 - }  
219 - });  
220 </script> 203 </script>
221 204
222 <style lang="scss" scoped> 205 <style lang="scss" scoped>
223 - .login-wrapper {  
224 - height: 100vh;  
225 - // #ifdef H5  
226 - height: calc(100vh - 44px);  
227 - // #endif  
228 - background-color: #ffffff;  
229 - padding: 40rpx 50rpx; // 移除顶部大 Logo 后,缩减顶部 Padding 保持呼吸感  
230 - box-sizing: border-box;  
231 -  
232 - // 1. 表单容器  
233 - .form-container {  
234 - margin-top: 20rpx;  
235 -  
236 - .form-item-wrapper {  
237 - margin-bottom: 36rpx;  
238 -  
239 - .form-label {  
240 - font-size: 26rpx;  
241 - color: #0a1931;  
242 - font-weight: bold;  
243 - display: block;  
244 - margin-bottom: 16rpx;  
245 - } 206 +.login-wrapper {
  207 + height: 100vh;
  208 + // #ifdef H5
  209 + height: calc(100vh - 44px);
  210 + // #endif
  211 + background-color: #ffffff;
  212 + padding: 40rpx 50rpx; // 移除顶部大 Logo 后,缩减顶部 Padding 保持呼吸感
  213 + box-sizing: border-box;
  214 +
  215 + // 1. 表单容器
  216 + .form-container {
  217 + margin-top: 20rpx;
  218 +
  219 + .form-item-wrapper {
  220 + margin-bottom: 36rpx;
  221 +
  222 + .form-label {
  223 + font-size: 26rpx;
  224 + color: #0a1931;
  225 + font-weight: bold;
  226 + display: block;
  227 + margin-bottom: 16rpx;
  228 + }
246 229
247 - // 输入框提示语  
248 - .input-hint {  
249 - display: block;  
250 - font-size: 24rpx;  
251 - color: #9097a3;  
252 - line-height: 1.5;  
253 - margin-top: 12rpx;  
254 - padding: 0 10rpx; 230 + // 输入框提示语
  231 + .input-hint {
  232 + display: block;
  233 + font-size: 24rpx;
  234 + color: #9097a3;
  235 + line-height: 1.5;
  236 + margin-top: 12rpx;
  237 + padding: 0 10rpx;
  238 + }
  239 +
  240 + .input-box {
  241 + height: 104rpx;
  242 + background-color: #f4f6fa;
  243 + border: 2rpx solid #e1e6eb;
  244 + border-radius: 24rpx;
  245 + display: flex;
  246 + align-items: center;
  247 + padding: 0 30rpx;
  248 + box-sizing: border-box;
  249 +
  250 + .input-icon {
  251 + margin-right: 16rpx;
255 } 252 }
256 253
257 - .input-box {  
258 - height: 104rpx;  
259 - background-color: #f4f6fa;  
260 - border: 2rpx solid #e1e6eb;  
261 - border-radius: 24rpx;  
262 - display: flex;  
263 - align-items: center;  
264 - padding: 0 30rpx;  
265 - box-sizing: border-box; 254 + .eye-icon {
  255 + padding: 10rpx;
  256 + }
266 257
267 - .input-icon {  
268 - margin-right: 16rpx;  
269 - } 258 + .native-input {
  259 + flex: 1;
  260 + height: 100%;
  261 + font-size: 28rpx;
  262 + color: #0a1931;
  263 + }
  264 + }
270 265
271 - .eye-icon {  
272 - padding: 10rpx;  
273 - } 266 + // 验证码双列布局
  267 + .code-row {
  268 + display: flex;
  269 + align-items: center;
274 270
275 - .native-input {  
276 - flex: 1;  
277 - height: 100%;  
278 - font-size: 28rpx;  
279 - color: #0a1931;  
280 - } 271 + .flex-1 {
  272 + flex: 1;
281 } 273 }
282 274
283 - // 验证码双列布局  
284 - .code-row { 275 + .code-btn {
  276 + width: 220rpx;
  277 + height: 104rpx;
  278 + border-radius: 24rpx;
  279 + background-color: #f9fdf2;
  280 + border: 2rpx solid #d0ee9c;
285 display: flex; 281 display: flex;
  282 + justify-content: center;
286 align-items: center; 283 align-items: center;
  284 + margin-left: 20rpx;
  285 + font-size: 26rpx;
  286 + color: #8fc31f;
  287 + font-weight: bold;
  288 + transition: all 0.2s ease;
287 289
288 - .flex-1 {  
289 - flex: 1; 290 + &:active {
  291 + opacity: 0.8;
290 } 292 }
291 293
292 - .code-btn {  
293 - width: 220rpx;  
294 - height: 104rpx;  
295 - border-radius: 24rpx;  
296 - background-color: #f9fdf2;  
297 - border: 2rpx solid #d0ee9c;  
298 - display: flex;  
299 - justify-content: center;  
300 - align-items: center;  
301 - margin-left: 20rpx;  
302 - font-size: 26rpx;  
303 - color: #8fc31f;  
304 - font-weight: bold;  
305 - transition: all 0.2s ease;  
306 -  
307 - &:active {  
308 - opacity: 0.8;  
309 - }  
310 -  
311 - &.disabled {  
312 - color: #b0b8c4;  
313 - background-color: #f4f6fa;  
314 - border-color: #e1e6eb;  
315 - pointer-events: none;  
316 - } 294 + &.disabled {
  295 + color: #b0b8c4;
  296 + background-color: #f4f6fa;
  297 + border-color: #e1e6eb;
  298 + pointer-events: none;
317 } 299 }
318 } 300 }
319 } 301 }
320 } 302 }
  303 + }
321 304
322 - // 2. 确认修改按钮 (登录页同款高级渐变微立体阴影)  
323 - .submit-btn {  
324 - height: 104rpx;  
325 - border-radius: 24rpx;  
326 - background: linear-gradient(135deg, #79d621 0%, #00b074 100%);  
327 - box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);  
328 - display: flex;  
329 - justify-content: center;  
330 - align-items: center;  
331 - color: #ffffff;  
332 - font-size: 32rpx;  
333 - font-weight: bold;  
334 - letter-spacing: 2rpx;  
335 - margin-top: 80rpx;  
336 - transition: all 0.2s ease;  
337 -  
338 - &:active {  
339 - transform: scale(0.98);  
340 - opacity: 0.9;  
341 - } 305 + // 2. 确认修改按钮 (登录页同款高级渐变微立体阴影)
  306 + .submit-btn {
  307 + height: 104rpx;
  308 + border-radius: 24rpx;
  309 + background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
  310 + box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
  311 + display: flex;
  312 + justify-content: center;
  313 + align-items: center;
  314 + color: #ffffff;
  315 + font-size: 32rpx;
  316 + font-weight: bold;
  317 + letter-spacing: 2rpx;
  318 + margin-top: 80rpx;
  319 + transition: all 0.2s ease;
  320 +
  321 + &:active {
  322 + transform: scale(0.98);
  323 + opacity: 0.9;
  324 + }
342 325
343 - &.disabled-btn {  
344 - opacity: 0.75;  
345 - pointer-events: none;  
346 - } 326 + &.disabled-btn {
  327 + opacity: 0.75;
  328 + pointer-events: none;
  329 + }
347 330
348 - .btn-arrow {  
349 - margin-left: 10rpx;  
350 - } 331 + .btn-arrow {
  332 + margin-left: 10rpx;
351 } 333 }
352 } 334 }
  335 +}
353 </style> 336 </style>
@@ -94,8 +94,6 @@ const ExercisesApi = { @@ -94,8 +94,6 @@ const ExercisesApi = {
94 94
95 //更新动作 95 //更新动作
96 updateexercises: (updateData) => { 96 updateexercises: (updateData) => {
97 - const url = '/app/motion/categories/update';  
98 - console.log('请求URL:', url);  
99 return request({ 97 return request({
100 url: '/app/motion/exercises/update', 98 url: '/app/motion/exercises/update',
101 method: 'PUT', 99 method: 'PUT',
@@ -4,7 +4,7 @@ const SettingApi = { @@ -4,7 +4,7 @@ const SettingApi = {
4 // 获取设置 4 // 获取设置
5 getSetting: () => { 5 getSetting: () => {
6 return request({ 6 return request({
7 - url: `/app/student/setting`, 7 + url: `/app/user/setting`,
8 method: 'GET', 8 method: 'GET',
9 }); 9 });
10 }, 10 },
@@ -392,6 +392,21 @@ export const useTrainingStore = defineStore('training', { @@ -392,6 +392,21 @@ export const useTrainingStore = defineStore('training', {
392 setTrainingTimeText(val) { 392 setTrainingTimeText(val) {
393 this.trainingTimeText = val; 393 this.trainingTimeText = val;
394 }, 394 },
  395 +
  396 + // 每日模板修改的时候使用
  397 + resetContentOnly() {
  398 + this.id = null;
  399 + this.type = null;
  400 + this.actionDetail = {};
  401 + this.loading = false;
  402 + this.unitRecords = {};
  403 + this.trainingName = '';
  404 + this.dailyTemplateId = null;
  405 + this.isSystem = 1;
  406 + // 下面这些字段完全不碰,计时器自然运行:
  407 + // totalSeconds, isPause, timerInterval, trainingTimeText,
  408 + // showPicker, defaultTimeIndex, min, isTraining
  409 + },
395 // 清空 410 // 清空
396 clearTrainingStore() { 411 clearTrainingStore() {
397 this.clearTimer(); 412 this.clearTimer();
@@ -403,6 +418,10 @@ export const useTrainingStore = defineStore('training', { @@ -403,6 +418,10 @@ export const useTrainingStore = defineStore('training', {
403 this.trainingTimeText = ''; 418 this.trainingTimeText = '';
404 this.min = false; 419 this.min = false;
405 this.isSystem = 1; 420 this.isSystem = 1;
  421 + this.trainingName = '';
  422 + this.defaultTimeIndex = [0, 0, 0];
  423 + this.showPicker = false;
  424 + this.isTraining = false;
406 }, 425 },
407 }, 426 },
408 427