Authored by qxm

修改日历和模板

... ... @@ -6,9 +6,9 @@ SHOPRO_VERSION=v2.4.1
# SHOPRO_BASE_URL=http://mall.hcxtec.com
# SHOPRO_BASE_URL=https://xunji.geaktec.com
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
# SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
# SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
# SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
# SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
... ...
# 2026-06-23 工作日志
## 悬浮球快照方案 — 完整改造方案
分析了 exercising 健身小程序中 trainingStore(Pinia)被三个功能模块(动作训练、超级组、每日模板编辑/修改)共享导致的数据冲突问题。
### 问题根因
- `grid-cell-content-popup.vue` 的编辑按钮 (`templateEdit`) 调用 `loadDailyTemplateForEdit()` 覆盖 store
- `wode-xinjian-moban.vue` 的 `goBack()` / `handleSave()` 调用 `clearTrainingStore()` 清空 store
- `meiri-moban-xiugai.vue` 的修改弹窗(弹窗模式)已通过快照解决,但编辑和新建是页面跳转模式,快照来不及归还
### 最终方案:悬浮球持有快照
核心思路:最小化时将 trainingStore 全量深拷贝到共享 reactive 模块 `trainingSnapshot`(存在 JS 堆内存,非 Pinia),悬浮球由此独立控制显隐和回显。
改动清单:
1. **新建** `sheep/store/trainingSnapshot.js` — 共享 reactive 快照模块
2. **重写** `pages/TrainingFloating.vue` — 自有 showBall + 回显逻辑 + 计时器补偿
3. **修改** `pages4/.../xunji-dongzuo-lianxi.vue` openMin() — 保存快照到共享模块
4. **删除** `sheep/store/trainingStore.js` clearTrainingStore() 中 `this.min = false`
5. **不改** grid-cell-content-popup.vue 和 wode-xinjian-moban.vue
6. **修复** meiri-moban-xiugai.vue 中 Object.assign → $patch
总计约 113 行,4 文件改动,2 文件不动。
<template>
<view class="floating-train-btn" v-if="trainingStore.min" @click="goToTrainingPage">
<view class="floating-train-btn" v-if="showBall" @click="goToTrainingPage">
<view class="icon-wrap">
<up-icon name="plus-circle" color="#fff" size="24"></up-icon>
</view>
... ... @@ -13,28 +13,91 @@
<script setup>
import { computed, watch, onMounted } from 'vue';
import { useTrainingStore } from '@/sheep/store/trainingStore';
import { trainingSnapshot } from '@/sheep/store/trainingSnapshot'
const trainingStore = useTrainingStore();
// 格式化显示“开始时间”
const formattedStartTime = computed(() => trainingStore.trainingTimeText);
// 跳转到训练页面
const showBall = computed(() => trainingSnapshot.visible && trainingSnapshot.data !== null)
// 格式化显示时间(快照中的 trainingTimeText)
const formattedStartTime = computed(() => {
if (!trainingSnapshot.data) return ''
return trainingSnapshot.data.trainingTimeText || ''
})
// ========== 点击悬浮球:还原快照 → 跳转训练页 ==========
const goToTrainingPage = () => {
// 悬浮球消失
trainingStore.min = false;
if (!trainingSnapshot.data) return
// 安全检查:如果 store 里已经有不属于本次快照的训练数据(用户在编辑中)
if (trainingStore.id && trainingStore.id !== trainingSnapshot.data.id) {
uni.showModal({
title: '提示',
content: '当前已有另一组训练数据,恢复会覆盖它,确认吗?',
confirmText: '确认恢复',
cancelText: '取消',
success: (res) => {
if (res.confirm) doRestoreAndGo()
}
})
return
}
doRestoreAndGo()
}
const doRestoreAndGo = () => {
const snap = trainingSnapshot.data
// 1. 用 $patch 还原全量 state(Pinia 官方批量更新,保证响应式触发)
trainingStore.$patch(snap)
// 2. 补偿计时器:如果快照时计时器在跑,加上经过的时间
if (trainingSnapshot.timerWasRunning && trainingSnapshot.timestamp) {
const elapsed = Math.floor((Date.now() - trainingSnapshot.timestamp) / 1000)
trainingStore.totalSeconds = (snap.totalSeconds || 0) + elapsed
}
// 3. 如果快照时计时器在跑,重新启动 setInterval
if (trainingSnapshot.timerWasRunning) {
trainingStore.isPause = false
trainingStore.timerInterval = setInterval(() => {
trainingStore.totalSeconds++
}, 1000)
}
// 4. 清空快照,隐藏悬浮球
trainingSnapshot.data = null
trainingSnapshot.timestamp = null
trainingSnapshot.timerWasRunning = false
trainingSnapshot.visible = false
// 5. 跳转训练页面
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}&isTraining=true`,
});
};
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${snap.id}&type=${snap.type}&isTraining=true`,
})
}
// 格式化显示“开始时间”
// const formattedStartTime = computed(() => trainingStore.trainingTimeText);
// 跳转到训练页面
// const goToTrainingPage = () => {
// // 悬浮球消失
// trainingStore.min = false;
// uni.navigateTo({
// url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${trainingStore.id}&type=${trainingStore.type}`,
// });
// };
watch(() => trainingStore.min, (newVal) => {
console.log('🟢 悬浮球组件监听到 min 变化:', newVal);
watch(() => showBall.value, (newVal) => {
console.log('🟢 悬浮球组件监听到 showBall 变化:', newVal);
}, { immediate: true });
// 组件挂载时打印
onMounted(() => {
console.log('🟢 悬浮球组件已挂载,当前 min 值:', trainingStore.min);
console.log('🟢 悬浮球组件已挂载,当前 showBall 值:', showBall.value);
});
</script>
... ...
<template>
<view class="charts-box">
<!-- :canvas2d="true" tooltipFormat="showYLable" :onmovetip="true" -->
<!-- :canvas2d="true"(导致穿透的原因) tooltipFormat="showYLable" :onmovetip="true" -->
<qiun-data-charts :type="chartType" :opts="opts" :chartData="chartData" :reshow="reshow" :tooltipShow="true" />
</view>
</template>
... ...
... ... @@ -7,7 +7,7 @@
<view class="goal-list">
<view class="goal-item">
<view class="serial">
{{ subIndex + 1 }}{{ String.fromCharCode(65 + subIndex) }}</view>
{{ setIndex + 1 }}{{ String.fromCharCode(65 + subIndex) }}</view>
<!-- ========================================== -->
<!-- 0:独立 重量+次数 -->
<!-- ========================================== -->
... ... @@ -178,7 +178,8 @@ const props = defineProps({
userWeight: {
type: Number,
default: 70
}
},
})
const emit = defineEmits(['open-time-picker'])
... ...
... ... @@ -84,7 +84,7 @@
<view class="action-top">
<text class="action-name">{{ unit.exercises[0]?.exerciseName || '动作名称' }}</text>
<text class="action-totalWeight">{{ unit.totalWeight
}}kg</text>
}}kg</text>
</view>
<!-- 下半部分:组次行 → 向左对齐图片 ✅核心-->
... ... @@ -233,8 +233,8 @@
<!-- 日期备注弹窗 -->
<RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId"
:history-list="noteHistoryList" @save="loaddailytemplate" @close="handleCloseNotePopup"
@refreshMain="handleRefreshMain" />
:history-list="noteHistoryList" :note-content-list="noteList" @save="loaddailytemplate"
@close="handleCloseNotePopup" @refreshMain="handleRefreshMain" />
<!-- 更多弹窗 -->
<up-popup :show="moreShow" mode="bottom" mask-click @close="closeMorePopup" :safe-area-inset-bottom="true">
... ... @@ -424,7 +424,7 @@ const loaddailytemplate = async () => {
const noteListFromApi = resdaily.data.notes;
noteList.value = noteListFromApi;
console.log('打印每日模板详情的备注列表:noteList', noteList);
console.log('打印每日模板详情的备注列表:noteList', noteList.value);
const historyFromApi = resdaily.data.noteHistoryList || [];
console.log('打印历史备注:', historyFromApi);
... ...
... ... @@ -5,7 +5,7 @@
<!-- 顶部导航栏 -->
<view class="popup-header">
<view class="close-btn" @click="handleClose">
<text class="close-icon">×</text>
<up-icon name="close" color="#333" size="20"></up-icon>
</view>
<text class="popup-title"> {{ isEdit ? '编辑日程备注' : '新增日程备注' }}</text>
<button class="save-btn" @click="handleSave">保存</button>
... ... @@ -55,7 +55,8 @@ const props = defineProps({
visible: { type: Boolean, default: false },
date: { type: String, required: true },
noteId: { type: [Number, String], default: null },
historyList: { type: Array, default: () => [] }
historyList: { type: Array, default: () => [] },
noteContentList: { type: Array, default: () => [] }
});
const emit = defineEmits(['update:visible', 'save', 'close', 'refreshMain']);
... ... @@ -110,13 +111,43 @@ const selectHistoryNote = (item) => {
const handleClose = () => {
emit('update:visible', false);
emit('close');
// 清空文本
noteContent.value = '';
selectedColorIndex.value = 0;
};
const handleSave = async () => {
if (!noteContent.value.trim()) return uni.showToast({ title: "请输入内容" });
const content = noteContent.value.trim()
if (!content) return uni.showToast({ title: "请输入内容" });
// 判断输入内容是否已存在于当日备注列表
// 全部场景都校验重复
const isRepeat = props.noteContentList.some(item => {
if (isEdit.value && item.id === Number(props.noteId)) {
return false
}
// 内容相同则判定重复
return item.content === content
})
console.log('isRepeat---', isRepeat);
if (isRepeat) {
uni.showToast({ title: "操作失败:备注存在重复值", icon: "none" })
return;
}
if (isRepeat) {
uni.showToast({ title: "操作失败:备注存在重复值", icon: "none" })
return;
}
let data = {
content: noteContent.value,
backgroundColor: colorList.value[selectedColorIndex.value]
};
// 当日已经有的备注不允许再次输入,提示返回
try {
if (isEdit.value) {
// 编辑:必须传 ID
... ...
... ... @@ -90,7 +90,7 @@ const activeSceneId = ref('0');
const partList = ref([]);
const rawPartData = ref([]); // 存储接口返回的原始部位数据
const showPartDropdown = ref(false);
const activePart = ref('不限');
const activePart = ref('部位');
const activePartId = ref('0'); // 新增:默认选中"不限",id=0
// 场景筛选相关状态
const sceneList = ref([
... ... @@ -101,7 +101,7 @@ const sceneList = ref([
// { id: 4, title: '办公室' },
]);
const showSceneDropdown = ref(false);
const activeScene = ref('不限');
const activeScene = ref('场景');
// 获取模板大类列表
const TemplatesList = async () => {
... ... @@ -147,20 +147,27 @@ const toggleSceneDropdown = () => {
// 选择部位
const selectPart = (item) => {
activePart.value = item.title;
activePartId.value = item.id;
// 判断是否是不限id=0
if (item.id === '0') {
activePart.value = '部位';
} else {
activePart.value = item.title;
}
showPartDropdown.value = false;
// 加这一行:选完部位立刻筛选
doFilter();
};
// 选择场景
const selectScene = (item) => {
activeScene.value = item.title;
// 关键:把选中的场景ID存起来
activeSceneId.value = item.id;
// 判断是否是不限id=0
if (item.id === '0') {
activeScene.value = '场景';
} else {
activeScene.value = item.title;
}
showSceneDropdown.value = false;
// 选择完,立即执行筛选!
doFilter();
};
... ... @@ -177,6 +184,9 @@ const doFilter = async () => {
if (musclesId === '0' && scene === '0') {
// 情况A:都不限 -> 恢复显示【模板大类】
isFiltering.value = false;
// 重置按钮文字为默认
activePart.value = '部位';
activeScene.value = '场景';
// 大类列表之前已经加载过了,直接显示
return;
}
... ... @@ -252,46 +262,7 @@ onMounted(async () => {
align-items: center;
justify-content: center;
width: auto;
min-width: 150rpx;
height: 50rpx;
background-color: #fff;
color: #333;
font-size: 24rpx;
border: 2rpx solid #ddd;
border-radius: 25rpx;
.text {
margin-right: 5rpx;
}
}
/* 筛选包装器 */
.filter-wrapper {
position: relative;
z-index: 10;
}
/* 筛选按钮激活状态 */
.filter-btn.active {
border-color: #43b05e;
background-color: #e6f7f0;
color: #43b05e;
}
.filter-section {
display: flex;
gap: 24rpx;
padding: 20rpx 30rpx;
background-color: white;
flex-wrap: wrap;
}
.filter-btn {
display: flex;
align-items: center;
justify-content: center;
width: auto;
min-width: 125rpx;
min-width: 70rpx;
height: 50rpx;
background: #fff;
color: #333;
... ... @@ -317,33 +288,6 @@ onMounted(async () => {
z-index: 10;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 8rpx);
left: 0;
background: #fff;
border-radius: 14rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
padding: 12rpx 0;
z-index: 100;
min-width: 160rpx;
max-height: 320rpx;
overflow-y: auto;
}
.dropdown-item {
padding: 16rpx 24rpx;
font-size: 26rpx;
color: #333;
white-space: nowrap;
}
.dropdown-item.selected {
background: #f0f9f4;
color: #2e9d5a;
font-weight: 500;
}
/* 下拉菜单样式 */
.dropdown-menu {
position: absolute;
... ... @@ -365,6 +309,7 @@ onMounted(async () => {
padding: 16rpx 24rpx;
font-size: 26rpx;
color: #333;
white-space: nowrap;
}
/* 下拉菜单项悬停样式 */
... ... @@ -381,13 +326,13 @@ onMounted(async () => {
}
.template-list {
margin-top: 120rpx;
padding: 0 30rpx;
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
// #ifdef MP-WEIXIN
padding-bottom: 200rpx;
height: calc(100vh - 274rpx);
... ... @@ -397,7 +342,6 @@ onMounted(async () => {
margin-top: 87px;
// 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px
height: calc(100vh - 77px - 87px);
// padding-bottom: 40px;
/* #endif */
.sub-template-list {
... ... @@ -449,7 +393,11 @@ onMounted(async () => {
}
&:last-child {
margin-bottom: 45rpx;
margin-bottom: 5rpx;
/* #ifdef H5 */
margin-bottom: 30rpx;
/* #endif */
}
}
}
... ...
... ... @@ -351,6 +351,7 @@ const selectDate = async (date) => {
:deep(.uni-date__x-input) {
height: 40rpx;
line-height: 40rpx;
padding: 0 10rpx;
}
// 微信小程序端样式适配
... ... @@ -362,13 +363,14 @@ const selectDate = async (date) => {
box-sizing: border-box;
:deep(.uni-date-x) {
height: 100%;
min-height: 40rpx;
height: 35rpx;
// min-height: 40rpx;
border-radius: 50rpx;
background-color: transparent;
padding: 0;
margin: 0;
border: none !important;
}
:deep(.uni-date-editor--x) {
... ... @@ -381,11 +383,11 @@ const selectDate = async (date) => {
}
:deep(.uni-date__x-input) {
height: 40rpx;
line-height: 40rpx;
// height: 40rpx;
// line-height: 40rpx;
font-size: 24rpx;
color: #333;
padding: 0;
padding: 0 10rpx;
text-align: center;
flex: 1;
overflow: hidden;
... ...
... ... @@ -49,12 +49,12 @@
<view class="statistical">
<template v-if="!showLastPeriod">
<LineChart :categories="chartCategories" :series="chartSeries" chartType="column" :reshow="true"
:extra="{ column: { width: trendChartColumnWidth } }" />
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
</template>
<template v-else>
<LineChart :categories="chartCategories" :series="chartSeries" chartType="line" :reshow="true"
:extra="{ column: { width: trendChartColumnWidth } }" />
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
chartType="line" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
</template>
</view>
... ...
... ... @@ -4,18 +4,14 @@
<view class="banner-wrapper">
<swiper class="swiper">
<swiper-item class="swiper-item">
<image
class="img"
<image class="img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/37_1773628025534.png"
mode="aspectFill"
/>
mode="aspectFill" />
</swiper-item>
<swiper-item class="swiper-item">
<image
class="img"
<image class="img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/38_1773628032712.png"
mode="aspectFill"
/>
mode="aspectFill" />
</swiper-item>
</swiper>
</view>
... ... @@ -23,41 +19,26 @@
<!-- 筛选条 -->
<!-- 后端计划分类的计划列表只返回了难度字段,所有只有这个字段的筛选生效了 -->
<view class="filter-bar">
<view
v-for="(item, index) in filterList"
:key="index"
class="filter-item"
:class="{ active: activeFilter === index }"
@click="
<view v-for="(item, index) in filterList" :key="index" class="filter-item"
:class="{ active: activeFilter === index }" @click="
activeFilter = index;
toggleDrawer(index);
"
>
toggleDrawer(index);
">
<text class="text">{{
selectedFilters[item] === '不限' ? item : `${selectedFilters[item]}`
}}</text>
<uni-icons type="bottom" size="14" color="#666" />
</view>
<view class="filter-search">
<uni-icons
type="search"
size="20"
color="#333"
style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx"
@click="navigateToSearch"
/>
<uni-icons type="search" size="20" color="#333"
style="border: 1px solid #c5c5c5; border-radius: 30rpx; padding: 4rpx 6rpx" @click="navigateToSearch" />
</view>
</view>
<!-- 下拉抽屉组件 -->
<view class="filter-drawer" v-show="drawerShow" @click.stop>
<view class="drawer-content">
<view
v-for="(item, idx) in filterDrawerData"
:key="idx"
class="drawer-item"
@click="selectDrawerItem(item)"
>
<view v-for="(item, idx) in filterDrawerData" :key="idx" class="drawer-item" @click="selectDrawerItem(item)">
<text class="drawer-item-text">{{ item }}</text>
</view>
</view>
... ... @@ -67,33 +48,19 @@
<view class="content-layout">
<!-- 左侧分类 -->
<scroll-view class="sidebar" scroll-y enable-flex>
<view
v-for="(item, index) in categoryList"
:key="item.id"
class="category-item"
:class="{ active: activeCategory === index }"
@click="switchCategory(index)"
>
<view v-for="(item, index) in categoryList" :key="item.id" class="category-item"
:class="{ active: activeCategory === index }" @click="switchCategory(index)">
<text class="text">{{ item.name }}</text>
</view>
</scroll-view>
<!-- 右侧计划列表 -->
<scroll-view class="plan-list-wrap" scroll-y enable-flex>
<view
v-for="plan in currentPlanList"
:key="plan.id"
class="plan-card"
@tap="navigateToDetail(plan)"
>
<view v-for="plan in currentPlanList" :key="plan.id" class="plan-card" @tap="navigateToDetail(plan)">
<image class="plan-cover" :src="plan.cover" mode="aspectFill" />
<view class="plan-info">
<view class="plan-tag-row">
<view
v-if="plan.tag"
class="plan-tag"
:class="plan.tag === '火爆' ? 'tag-hot' : 'tag-new'"
>
<view v-if="plan.tag" class="plan-tag" :class="plan.tag === '火爆' ? 'tag-hot' : 'tag-new'">
<text>{{ plan.tag }}</text>
</view>
</view>
... ... @@ -113,438 +80,449 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue';
// ✅ getCurrentPages 是全局 API,无需 import
import QueryPlanApi from '@/sheep/api/plan/queryplan';
// ====== 筛选相关(保持原样)======
const filterList = ref(['频率', '难度', '场景', '人群']);
const drawerShow = ref(false);
const activeFilter = ref(null);
const selectedFilters = ref({
频率: '不限',
难度: '不限',
场景: '不限',
人群: '不限',
});
const filterOptionsMap = {
频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'],
难度: ['不限', '初阶', '中阶', '高阶'],
场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'],
人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'],
};
const filterDrawerData = computed(() => {
const currentFilterName = filterList.value[activeFilter.value];
return filterOptionsMap[currentFilterName] || [];
});
const toggleDrawer = (index) => {
if (activeFilter.value === index) {
drawerShow.value = !drawerShow.value;
} else {
drawerShow.value = true;
}
activeFilter.value = index;
};
const selectDrawerItem = (item) => {
const currentFilterName = filterList.value[activeFilter.value];
selectedFilters.value[currentFilterName] = item;
drawerShow.value = false;
console.log('筛选条件更新:', selectedFilters.value);
};
// ====== 分类与计划 ======
const categoryList = ref([]);
const activeCategory = ref(0);
const planData = ref([]); // 存储所有已加载的计划(带 _categoryId)
const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };
// 完全匹配你后台的数字,复制这一段!
const filterMap = {
频率: {
不限: 0,
'1练/周': 1,
'2练/周': 2,
'3练/周': 3,
'4练/周': 4,
'5练/周': 5,
'6练/周': 6,
'7练/周': 7,
},
难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 },
场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 },
人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 },
};
const currentPlanList = computed(() => {
const categoryId = categoryList.value[activeCategory.value]?.id;
if (categoryId == null) return [];
let list = planData.value.filter((p) => p._categoryId === categoryId);
// 频率
if (selectedFilters.value.频率 !== '不限') {
const val = filterMap.频率[selectedFilters.value.频率];
list = list.filter((item) => item.frequency === val);
}
// 难度
if (selectedFilters.value.难度 !== '不限') {
const val = filterMap.难度[selectedFilters.value.难度];
list = list.filter((item) => item.difficultyLevel === val);
}
import { computed, onMounted, ref } from 'vue';
// ✅ getCurrentPages 是全局 API,无需 import
import QueryPlanApi from '@/sheep/api/plan/queryplan';
// ====== 筛选相关(保持原样)======
const filterList = ref(['频率', '难度', '场景', '人群']);
const drawerShow = ref(false);
const activeFilter = ref(null);
const selectedFilters = ref({
频率: '不限',
难度: '不限',
场景: '不限',
人群: '不限',
});
const filterOptionsMap = {
频率: ['不限', '1练/周', '2练/周', '3练/周', '4练/周', '5练/周'],
难度: ['不限', '初阶', '中阶', '高阶'],
场景: ['不限', '健身房', '仅哑铃', '仅哑铃+杠铃'],
人群: ['不限', '青少年', '成年人', '中老年', '运动损伤'],
};
const filterDrawerData = computed(() => {
const currentFilterName = filterList.value[activeFilter.value];
return filterOptionsMap[currentFilterName] || [];
});
const toggleDrawer = (index) => {
if (activeFilter.value === index) {
drawerShow.value = !drawerShow.value;
} else {
drawerShow.value = true;
}
activeFilter.value = index;
};
const selectDrawerItem = (item) => {
const currentFilterName = filterList.value[activeFilter.value];
selectedFilters.value[currentFilterName] = item;
drawerShow.value = false;
console.log('筛选条件更新:', selectedFilters.value);
};
// ====== 分类与计划 ======
const categoryList = ref([]);
const activeCategory = ref(0);
const planData = ref([]); // 存储所有已加载的计划(带 _categoryId)
const difficultyText = { 1: '初阶', 2: '中阶', 3: '高阶' };
// 完全匹配你后台的数字,复制这一段!
const filterMap = {
频率: {
不限: 0,
'1练/周': 1,
'2练/周': 2,
'3练/周': 3,
'4练/周': 4,
'5练/周': 5,
'6练/周': 6,
'7练/周': 7,
},
难度: { 不限: 0, 初阶: 1, 中阶: 2, 高阶: 3 },
场景: { 不限: 1, 健身房: 2, 仅哑铃: 3, '仅哑铃+杠铃': 4 },
人群: { 不限: 1, 青少年: 2, 成年人: 3, 中老年: 4, 运动损伤: 5 },
};
const currentPlanList = computed(() => {
const categoryId = categoryList.value[activeCategory.value]?.id;
if (categoryId == null) return [];
let list = planData.value.filter((p) => p._categoryId === categoryId);
// 频率
if (selectedFilters.value.频率 !== '不限') {
const val = filterMap.频率[selectedFilters.value.频率];
list = list.filter((item) => item.frequency === val);
}
// 场景
if (selectedFilters.value.场景 !== '不限') {
const val = filterMap.场景[selectedFilters.value.场景];
list = list.filter((item) => item.scene === val);
}
// 难度
if (selectedFilters.value.难度 !== '不限') {
const val = filterMap.难度[selectedFilters.value.难度];
list = list.filter((item) => item.difficultyLevel === val);
}
// 人群
if (selectedFilters.value.人群 !== '不限') {
const val = filterMap.人群[selectedFilters.value.人群];
list = list.filter((item) => item.population === val);
}
// 场景
if (selectedFilters.value.场景 !== '不限') {
const val = filterMap.场景[selectedFilters.value.场景];
list = list.filter((item) => item.scene === val);
}
return list;
});
// 人群
if (selectedFilters.value.人群 !== '不限') {
const val = filterMap.人群[selectedFilters.value.人群];
list = list.filter((item) => item.population === val);
}
// ✅ 切换分类时加载数据
const switchCategory = async (index) => {
activeCategory.value = index;
const categoryId = categoryList.value[index]?.id;
if (categoryId != null) {
// 检查是否已加载过该分类
const isLoaded = planData.value.some((p) => p._categoryId === categoryId);
if (!isLoaded) {
await loadPlansByCategory(categoryId);
}
}
};
// ====== 数据加载 ======
const loadCategories = async () => {
try {
// 获得计划分类,左侧导航栏显示用
const res = await QueryPlanApi.getCategories();
categoryList.value = res.data || [];
console.log('加载计划列表categoryList.value:', categoryList.value);
if (categoryList.value.length > 0) {
await switchCategory(0); // 加载第一个分类
}
} catch (err) {
console.error('加载分类失败:', err);
uni.showToast({ title: '加载分类失败', icon: 'none' });
}
};
// ✅ 核心:加载计划,并手动附加 _categoryId
const loadPlansByCategory = async (categoryId) => {
try {
// 根据计划分类ID获得计划列表,右侧计划列表显示用
const res = await QueryPlanApi.getPlanList(categoryId);
console.log('加载计划列表res.data:', res.data);
const plans = (res.data || []).map((item) => ({
id: item.id,
title: item.name,
meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`,
tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '',
cover: item.urlCover || '默认图',
_categoryId: categoryId,
frequency: item.frequencyPerWeek, // 新增
difficultyLevel: item.difficultyLevel, // 新增
scene: item.trainingScene, // 新增
population: item.targetPeople, // 新增
}));
// 合并数据(避免重复)
planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans];
// 打印planData
console.log('加载计划列表planData.value:', planData.value);
} catch (err) {
console.error('加载计划失败:', err);
uni.showToast({ title: '加载计划失败', icon: 'none' });
return list;
});
// ✅ 切换分类时加载数据
const switchCategory = async (index) => {
activeCategory.value = index;
const categoryId = categoryList.value[index]?.id;
if (categoryId != null) {
// 检查是否已加载过该分类
const isLoaded = planData.value.some((p) => p._categoryId === categoryId);
if (!isLoaded) {
await loadPlansByCategory(categoryId);
}
};
// 跳转到搜索页面
const navigateToSearch = () => {
const app = getApp();
app.globalData.allPlansForSearch = planData.value;
uni.navigateTo({
url: '/pages4/pages/xunji/jihua-search',
});
};
// 跳转到计划详情页
const navigateToDetail = (plan) => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`,
});
};
// ====== 返回按钮 ======
const back = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: '/pages/index/index' }); // 请替换为你的首页路径
}
};
// ====== 数据加载 ======
const loadCategories = async () => {
try {
// 获得计划分类,左侧导航栏显示用
const res = await QueryPlanApi.getCategories();
categoryList.value = res.data || [];
console.log('加载计划列表categoryList.value:', categoryList.value);
if (categoryList.value.length > 0) {
await switchCategory(0); // 加载第一个分类
}
};
onMounted(() => {
loadCategories();
} catch (err) {
console.error('加载分类失败:', err);
uni.showToast({ title: '加载分类失败', icon: 'none' });
}
};
// ✅ 核心:加载计划,并手动附加 _categoryId
const loadPlansByCategory = async (categoryId) => {
try {
// 根据计划分类ID获得计划列表,右侧计划列表显示用
const res = await QueryPlanApi.getPlanList(categoryId);
console.log('加载计划列表res.data:', res.data);
const plans = (res.data || []).map((item) => ({
id: item.id,
title: item.name,
meta: `${difficultyText[item.difficultyLevel] || '初阶'} · ${item.enrollmentCount}人练过`,
tag: item.enrollmentCount > 500 ? '火爆' : item.enrollmentCount > 0 ? 'New' : '',
cover: item.urlCover || '默认图',
_categoryId: categoryId,
frequency: item.frequencyPerWeek, // 新增
difficultyLevel: item.difficultyLevel, // 新增
scene: item.trainingScene, // 新增
population: item.targetPeople, // 新增
}));
// 合并数据(避免重复)
planData.value = [...planData.value.filter((p) => p._categoryId !== categoryId), ...plans];
// 打印planData
console.log('加载计划列表planData.value:', planData.value);
} catch (err) {
console.error('加载计划失败:', err);
uni.showToast({ title: '加载计划失败', icon: 'none' });
}
};
// 跳转到搜索页面
const navigateToSearch = () => {
const app = getApp();
app.globalData.allPlansForSearch = planData.value;
uni.navigateTo({
url: '/pages4/pages/xunji/jihua-search',
});
</script>
<style lang="scss" scoped>
/* ========== 新增:页面头部样式 ========== */
.page-header {
display: flex;
align-items: center;
padding: 30rpx;
background: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
position: sticky;
top: 0;
z-index: 999;
};
// 跳转到计划详情页
const navigateToDetail = (plan) => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-xunlian-jihua?planid=${plan.id}`,
});
};
// ====== 返回按钮 ======
const back = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: '/pages/index/index' }); // 请替换为你的首页路径
}
};
.page-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
}
onMounted(() => {
loadCategories();
});
</script>
/* ========== 以下是你原有的全部样式(完全保留) ========== */
.plan-page {
<style lang="scss" scoped>
/* ========== 新增:页面头部样式 ========== */
// .page-header {
// display: flex;
// align-items: center;
// padding: 30rpx;
// background: #fff;
// box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
// position: sticky;
// top: 0;
// z-index: 999;
// }
// .page-title {
// font-size: 32rpx;
// font-weight: bold;
// color: #333;
// flex: 1;
// text-align: center;
// }
/* ========== 以下是你原有的全部样式(完全保留) ========== */
.plan-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 顶部横幅 */
.banner-wrapper {
height: 250rpx;
width: 100%;
margin: 20rpx 0;
.swiper {
width: 100%;
height: 100%;
background-color: #f5f5f5;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 顶部横幅 */
.banner-wrapper {
height: 250rpx;
width: 100%;
margin: 20rpx 0;
.swiper {
.swiper-item {
width: 100%;
height: 100%;
padding: 20rpx;
box-sizing: border-box;
.swiper-item {
.img {
width: 100%;
height: 100%;
padding: 20rpx;
box-sizing: border-box;
.img {
width: 100%;
height: 220rpx;
border-radius: 20rpx;
}
height: 220rpx;
border-radius: 20rpx;
}
}
}
/* 筛选条 */
.filter-bar {
margin: 0 20rpx 20rpx;
padding: 12rpx 18rpx;
border-radius: 40rpx;
display: flex;
}
/* 筛选条 */
.filter-bar {
margin: 0 20rpx 20rpx;
padding: 12rpx 18rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.03);
.filter-item {
flex-direction: row;
align-items: center;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.03);
.filter-item {
flex-direction: row;
align-items: center;
padding: 8rpx 18rpx;
border-radius: 30rpx;
border: 1px solid #c5c5c5;
margin-right: 10rpx;
background-color: #f7f7f7;
display: flex;
.text {
font-size: 24rpx;
color: #666;
margin-right: 6rpx;
}
&.active {
background-color: #e6f7f0;
.text {
color: #26c165;
}
}
}
.filter-search {
margin-left: auto;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background-color: #f7f7f7;
display: flex;
align-items: center;
justify-content: center;
}
}
padding: 8rpx 18rpx;
border-radius: 30rpx;
border: 1px solid #c5c5c5;
margin-right: 10rpx;
background-color: #f7f7f7;
display: flex;
//下拉抽屉
.filter-drawer {
position: absolute;
top: 560rpx;
left: 20rpx;
right: 20rpx;
z-index: 999;
border-radius: 0 0 40rpx 40rpx;
background-color: #fff;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
overflow: hidden;
.drawer-content {
padding: 10rpx 0;
.text {
font-size: 24rpx;
color: #666;
margin-right: 6rpx;
}
.drawer-item {
padding: 20rpx 30rpx;
font-size: 26rpx;
color: #333;
transition: background-color 0.2s ease;
&.active {
background-color: #e6f7f0;
&:active {
background-color: #f5f5f5;
}
.drawer-item-text {
display: block;
width: 100%;
.text {
color: #26c165;
}
}
}
/* 主体布局 */
.content-layout {
flex: 1;
.filter-search {
margin-left: auto;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background-color: #f7f7f7;
display: flex;
box-sizing: border-box;
overflow: hidden;
min-height: 0;
align-items: center;
justify-content: center;
}
}
//下拉抽屉
.filter-drawer {
position: absolute;
top: 560rpx;
/* #ifdef H5 */
top: 247px;
/* #endif */
left: 20rpx;
right: 20rpx;
z-index: 999;
border-radius: 0 0 40rpx 40rpx;
background-color: #fff;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
overflow: hidden;
.drawer-content {
padding: 10rpx 0;
}
/* 左侧分类 */
.sidebar {
width: 180rpx;
background-color: #ffffff;
border-radius: 16rpx;
height: 100%;
box-sizing: border-box;
overflow: hidden;
.drawer-item {
padding: 20rpx 30rpx;
font-size: 26rpx;
color: #333;
transition: background-color 0.2s ease;
.category-item {
padding: 22rpx 24rpx;
font-size: 24rpx;
color: #666;
position: relative;
&:active {
background-color: #f5f5f5;
}
&.active {
background-color: #e8f8f0;
color: #26c165;
font-weight: 600;
.drawer-item-text {
display: block;
width: 100%;
}
}
}
/* 主体布局 */
.content-layout {
flex: 1;
display: flex;
box-sizing: border-box;
overflow: hidden;
min-height: 0;
}
/* 左侧分类 */
.sidebar {
width: 180rpx;
background-color: #ffffff;
border-radius: 16rpx;
height: 72%;
/* #ifdef H5 */
height: 86%;
/* #endif */
box-sizing: border-box;
overflow: hidden;
.category-item {
padding: 22rpx 24rpx;
font-size: 24rpx;
color: #666;
position: relative;
&::before {
content: '';
position: absolute;
right: 0;
top: 16rpx;
bottom: 16rpx;
width: 6rpx;
border-radius: 0 4rpx 4rpx 0;
background-color: #26c165;
}
&.active {
background-color: #e8f8f0;
color: #26c165;
font-weight: 600;
&::before {
content: '';
position: absolute;
right: 0;
top: 16rpx;
bottom: 16rpx;
width: 6rpx;
border-radius: 0 4rpx 4rpx 0;
background-color: #26c165;
}
}
}
/* 右侧计划列表 */
.plan-list-wrap {
flex: 1;
padding: 0 20rpx;
box-sizing: border-box;
height: 100%;
overflow: hidden;
}
/* 右侧计划列表 */
.plan-list-wrap {
flex: 1;
padding: 0 20rpx;
box-sizing: border-box;
height: 72%;
/* #ifdef H5 */
height: 86%;
/* #endif */
overflow: hidden;
}
.plan-card {
position: relative;
margin-bottom: 16rpx;
border-radius: 16rpx;
.plan-cover {
width: 100%;
height: 230rpx;
border-radius: 5rpx;
}
.plan-card {
position: relative;
margin-bottom: 16rpx;
border-radius: 16rpx;
.plan-cover {
width: 100%;
height: 230rpx;
border-radius: 5rpx;
}
.plan-info {
position: absolute;
left: 20rpx;
right: 20rpx;
bottom: 20rpx;
color: #fff;
.plan-info {
position: absolute;
left: 20rpx;
right: 20rpx;
bottom: 20rpx;
color: #fff;
.plan-tag-row {
margin-bottom: 8rpx;
.plan-tag {
display: inline-flex;
padding: 4rpx 10rpx;
border-radius: 6rpx;
font-size: 20rpx;
line-height: 1;
&.tag-hot {
background-color: #ff4d4f;
}
&.tag-new {
background-color: #ffbb00;
}
}
}
.plan-tag-row {
margin-bottom: 8rpx;
.plan-text {
display: flex;
flex-direction: column;
.plan-tag {
display: inline-flex;
padding: 4rpx 10rpx;
border-radius: 6rpx;
font-size: 20rpx;
line-height: 1;
.plan-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 6rpx;
&.tag-hot {
background-color: #ff4d4f;
}
.plan-meta {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.8);
&.tag-new {
background-color: #ffbb00;
}
}
}
}
/* 空状态 */
.empty-tip {
text-align: center;
padding: 100rpx 0;
color: #999;
font-size: 28rpx;
.plan-text {
display: flex;
flex-direction: column;
.plan-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 6rpx;
}
.plan-meta {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.8);
}
}
}
}
/* 空状态 */
.empty-tip {
text-align: center;
padding: 100rpx 0;
color: #999;
font-size: 28rpx;
}
</style>
... ...
... ... @@ -317,8 +317,13 @@ const saveTemplateTitle = () => {
// 返回上一页
const goBack = () => {
uni.navigateBack();
trainingStore.clearTrainingStore()
setTimeout(() => {
// 发送全局刷新信号,列表页会立刻执行刷新
uni.$emit('refreshTemplateList');
uni.navigateBack();
}, 1500);
};
// 保存模板(完整修复间歇训练 type=6)
... ... @@ -399,12 +404,10 @@ const handleSave = async () => {
console.log('保存成功', res);
uni.hideLoading();
uni.showToast({ title: '保存成功' });
trainingStore.clearTrainingStore();
setTimeout(() => {
uni.redirectTo({
url: '/pages4/pages/xunji/xunji-wode-moban'
});
trainingStore.clearTrainingStore();
uni.navigateBack();
}, 1500);
} catch (err) {
... ...
... ... @@ -173,6 +173,7 @@ import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
import { useTrainingStore } from '@/sheep/store/trainingStore'
import addActions from '@/pages/xunji/components/dongzuo-lianxi/add-actions.vue'
import ActionSort from '@/pages/xunji/components/dongzuo-lianxi/dongzuo-paixu.vue'
import { trainingSnapshot } from '@/sheep/store/trainingSnapshot'
const trainingStore = useTrainingStore()
const hasConverted = ref(false);
... ... @@ -595,6 +596,39 @@ const openMin = () => {
console.log('最小化跳转后trainingStore.isTraining', trainingStore.isTraining);
trainingStore.min = true;
// 保存快照
trainingSnapshot.data = JSON.parse(JSON.stringify({
id: trainingStore.id,
type: trainingStore.type,
actionDetail: trainingStore.actionDetail,
loading: trainingStore.loading,
unitRecords: trainingStore.unitRecords,
trainingName: trainingStore.trainingName,
totalSeconds: trainingStore.totalSeconds,
isPause: trainingStore.isPause,
showPicker: trainingStore.showPicker,
defaultTimeIndex: trainingStore.defaultTimeIndex,
trainingTimeText: trainingStore.trainingTimeText,
min: trainingStore.min,
isSystem: trainingStore.isSystem,
dailyTemplateId: trainingStore.dailyTemplateId,
isTraining: trainingStore.isTraining,
}))
// 2. 记录时间戳和计时器状态
trainingSnapshot.timestamp = Date.now()
trainingSnapshot.timerWasRunning = !trainingStore.isPause
// 3. 显示悬浮球
trainingSnapshot.visible = true
// 4. 如果计时器在跑,先暂停(setInterval 跨页面会丢失,回显时用时间戳补偿)
if (!trainingStore.isPause) {
trainingStore.toggleTimer()
}
console.log('🟡 快照已保存:id=', trainingSnapshot.data.id,
'totalSeconds=', trainingSnapshot.data.totalSeconds)
// uni.navigateTo({
// url: '/pages/xunji/components/xunji-dongzuo', // 改成你训练页面的实际路径
// });
... ...
... ... @@ -55,7 +55,10 @@
</view>
<!-- 动作列表 -->
<view class="section">
<text class="section-title">动作列表</text>
<view class="section-top">
<text class="section-title">动作列表</text>
<view v-if="date" class="right-btn" @click="templateEdit(TemplateDetail)">修改训练内容</view>
</view>
<view v-if="TemplateUnits.length > 0">
<!-- 循环 unit,每个 unit 一个卡片 -->
<view v-for="(unit, unitIndex) in TemplateUnits" :key="unitIndex" class="exercise-item"
... ... @@ -111,9 +114,9 @@
{{ formatSeconds(detail.duration) }}
</view>
<view class="detail-value" v-if="unit.exercises[0].exerciseType === 6">
{{ detail.duration }}组 x {{ formatSeconds(detail.duration) }}
{{ detail.reps }}组 x {{ detail.duration }}秒 x {{ detail.restTime }}秒/组休息
</view>
<view class="rest" v-if="detail.restTime">
<view class="rest" v-if="detail.restTime && unit.exercises[0].exerciseType !== 6">
{{ detail.restTime }}s
</view>
</view>
... ... @@ -158,9 +161,10 @@
{{ formatSeconds(ex.sets[idx - 1].duration) }}
</text>
<text v-if="ex.exerciseType === 6">
{{ ex.sets[idx - 1].duration }}组 x {{ formatSeconds(ex.sets[idx - 1].duration) }}
{{ ex.sets[idx - 1].reps }}组 x {{ ex.sets[idx - 1].duration }}秒 x {{ ex.sets[idx - 1].restTime
}}秒/组休息
</text>
<view class="rest" v-if="ex.sets[idx - 1].restTime">
<view class="rest" v-if="ex.sets[idx - 1].restTime && ex.exerciseType !== 6">
{{ ex.sets[idx - 1].restTime }}s
</view>
</view>
... ... @@ -534,6 +538,20 @@ const isUnlocked = computed(() => {
const openActionItem = (item) => {
open(item.id, 1);
};
// 修改每日模板训练内容
const templateEdit = (TemplateDetail) => {
console.log('开始进入编辑模板页面,模板ID:', TemplateDetail.id, '每日模板ID:', TemplateDetail.dailyTemplateId);
trainingStore.isSystem = TemplateDetail.isSystem;
trainingStore.loadDailyTemplateForEdit(TemplateDetail);
trainingStore.initDailyTemplateRecords()
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${TemplateDetail.id}&type=3&dailyTemplateId=${TemplateDetail.dailyTemplateId}`,
});
};
const isMyPlan = ref(false)
const isDailytemplateId = ref(false)
... ... @@ -645,6 +663,46 @@ onLoad((options) => {
border-bottom: 1px solid #333;
}
.section-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
// margin-bottom: 20rpx;
}
.right-btn {
font-size: 24rpx;
width: 140rpx;
/* #ifdef H5 */
width: 99px;
/* #endif */
height: 37rpx;
padding: 10rpx 20rpx;
background: rgba(90, 90, 90, 0.45);
justify-content: center;
display: flex;
align-items: center;
border-radius: 20rpx;
}
.description {
font-size: 28rpx;
margin-bottom: 10rpx;
}
.empty-tip {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 28rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
... ...
... ... @@ -3,20 +3,39 @@
<template>
<view class="plan-detail-page">
<!-- 顶部安全区占位 -->
<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view> -->
<!-- 顶部导航栏 这里是模板大类的名字-->
<view class="header" :style="{ height: headerHeight + 'px' }">
<!-- <view class="header">
<view class="back-btn" @click="navigateBack">
<uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>
</view>
<view class="title">{{ templateList.name }}</view>
</view>
<scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}">
<view class="description">
<text>{{ templateList.description }}</text>
<text>{{ templateList?.description }}</text>
</view>
</view> -->
<view class="page-header" hover-class="none">
<!-- 动态导航栏:高度与胶囊对齐 -->
<view class="nav-bar" :style="{
paddingTop: menuButtonInfo.top + 'px',
height: menuButtonInfo.height + 'px'
}">
<view class="nav-left" @click="goBack">
<uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>
</view>
<view class="nav-title">{{ templateList.name }}</view>
<view class="nav-right"></view>
</view>
<view class="description">
{{ templateList?.description }}
</view>
</view>
<scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}">
<view class="filter-bar">
<!-- 部位筛选 -->
... ... @@ -109,11 +128,11 @@ const filteredTemplates = ref([]); // 筛选后列表
const partList = ref([]);
const rawPartData = ref([]);
const activePartId = ref('0');
const activePart = ref('不限');
const activePart = ref('部位');
// 场景(固定)
const activeSceneId = ref('0');
const activeScene = ref('不限');
const activeScene = ref('场景');
const sceneList = ref([
{ id: '0', title: '不限' },
{ id: '1', title: '健身房' },
... ... @@ -129,7 +148,7 @@ const templateMenuRef = ref(null)
const showPartDropdown = ref(false);
const showSceneDropdown = ref(false);
// 返回上一页函数
const navigateBack = () => {
const goBack = () => {
uni.navigateBack();
};
// 获取部位分类(接口,和首页完全一样)
... ... @@ -269,7 +288,7 @@ onLoad((options) => {
isIcon = true
}
console.log("模板大类ID:", id.value);
console.log("是否官方大类:", isBigTemOffice);
console.log("是否官方大类:", isBigTemOffice.value);
loadTemplates(id.value);
});
... ... @@ -277,6 +296,41 @@ onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight;
getPartCategories();
// 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐
// #ifdef MP-WEIXIN
try {
const rect = uni.getMenuButtonBoundingClientRect();
if (rect) {
menuButtonInfo.value = {
top: rect.top,
height: rect.height
};
}
} catch (e) {
console.log('获取胶囊信息失败,使用默认值', e);
}
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境,使用系统状态栏高度 + 标准导航栏高度
try {
const systemInfo = uni.getSystemInfoSync();
const statusBarHeight = systemInfo.statusBarHeight || 20;
menuButtonInfo.value = {
top: statusBarHeight,
height: 44 // 标准导航栏高度
};
} catch (e) {
console.log('获取系统信息失败', e);
}
// #endif
});
const menuButtonInfo = ref({
top: 44, // 默认值,避免获取失败时样式异常
height: 32
});
</script>
... ... @@ -286,29 +340,96 @@ onMounted(() => {
.plan-detail-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
box-sizing: border-box;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
box-sizing: border-box;
}
.status-bar-placeholder {
// .status-bar-placeholder {
// width: 100%;
// background-color: #1a1a1a;
// }
.page-header {
position: fixed;
width: 100%;
background-color: #1a1a1a;
top: 0;
left: 0;
right: 0;
background-color: rgba(26, 26, 26, 0.9);
z-index: 999;
flex-shrink: 0;
// background-color: #fff;
}
/* 动态导航栏:高度与胶囊完全对齐 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
// background-color: #1a1a1a;
/* 左右留出安全间距,避免内容贴边 */
padding-left: 16rpx;
padding-right: 16rpx;
/* 高度和顶部内边距由动态 style 控制 */
box-sizing: content-box;
}
/* 左侧返回按钮区域 - 固定宽度确保居中对齐 */
.nav-left {
width: 60rpx;
display: flex;
align-items: center;
justify-content: flex-start;
flex-shrink: 0;
}
.back-icon {
display: block;
}
/* 标题区域 - 自适应居中 */
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: bold;
color: #fff;
line-height: 1;
}
.nav-right {
width: 60rpx;
flex-shrink: 0;
}
.description {
padding: 20rpx 30rpx 20rpx;
color: #fff;
// background-color: #1a1a1a;
font-size: 28rpx;
line-height: 46rpx;
}
//
.header {
width: 100%;
position: relative;
background-color: #1a1a1a;
color: #fff;
// position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #1a1a1a;
color: #fff;
font-size: 36rpx;
font-weight: 600;
z-index: 10;
z-index: 999;
// z-index: 10;
// z-index: 999;
}
.back-btn {
... ... @@ -335,7 +456,13 @@ onMounted(() => {
flex: 1;
min-height: 0;
padding-top: 20rpx;
margin-top: v-bind(statusBarHeight + headerHeight + 'px');
// margin-top: v-bind(statusBarHeight + headerHeight + 'px');
margin-top: 236rpx;
/* #ifdef H5 */
min-height: 890px;
margin-top: 100px;
/* #endif */
}
.grid-container {
... ... @@ -369,15 +496,6 @@ onMounted(() => {
height: 300rpx;
}
.description {
padding: 30rpx 30rpx 20rpx;
color: #666;
font-size: 28rpx;
line-height: 46rpx;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.card-info {
// 核心优化:增强背景对比度 + 视觉层次
position: absolute;
... ... @@ -469,19 +587,19 @@ onMounted(() => {
}
.filter-item {
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 30rpx;
height: 60rpx;
padding: 0 15rpx;
line-height: 60rpx;
border-radius: 30rpx;
background-color: #fff;
border: 1px solid #ddd;
color: #333;
font-size: 28rpx;
font-weight: 400;
gap: 8rpx;
background-color: #fff;
&:active {
background-color: #f5f5f5;
... ... @@ -491,7 +609,7 @@ onMounted(() => {
/* 筛选器容器 */
.filter-wrapper {
position: relative;
z-index: 999;
// z-index: 999;
}
/* 下拉菜单 */
... ...
... ... @@ -19,7 +19,7 @@
<view class="filter-group">
<!-- 部位 -->
<view class="filter-wrapper">
<view class="tab-item" :class="{ active: activePartId !== '0' }" @click="togglePartDropdown">
<view class="tab-item" :class="{ active: activePartId !== null }" @click="togglePartDropdown">
{{ activePartId === null ? '部位' : activePart }}
<uni-icons :type="showPartDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons>
</view>
... ... @@ -32,7 +32,7 @@
</view>
<!-- 场景 -->
<view class="filter-wrapper">
<view class="tab-item" :class="{ active: activeSceneId !== '0' }" @click="toggleSceneDropdown">
<view class="tab-item" :class="{ active: activeSceneId !== null }" @click="toggleSceneDropdown">
{{ activeSceneId === null ? '场景' : activeScene }}
<uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="18" color="#333"></uni-icons>
</view>
... ... @@ -170,7 +170,8 @@
</template>
<script setup>
import { onMounted, ref, nextTick } from 'vue';
import { onMounted, ref, nextTick, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import TemplatesApi from '@/sheep/api/Template/Templates';
import TemplateMenuPopup from '@/pages4/components/TemplateMenuPopup.vue'
... ... @@ -468,12 +469,44 @@ const menuButtonInfo = ref({
height: 32
});
// 添加 onShow 生命周期
onShow(() => {
console.log('==== onShow执行了 ====');
activePartId.value = null;
activePart.value = '不限';
activeSceneId.value = null;
activeScene.value = '不限';
showPartDropdown.value = false;
showSceneDropdown.value = false;
console.log('onShow刷新页面');
getMyTemplates();
getFolderList();
});
onMounted(() => {
// TemplatesList();
getMyTemplates();
getPartCategories();
getFolderList();
uni.$on('refreshTemplateList', () => {
console.log('收到刷新事件,强制刷新模板');
// 重置筛选条件,和onShow逻辑保持一致
activePartId.value = null;
activePart.value = '不限';
activeSceneId.value = null;
activeScene.value = '不限';
showPartDropdown.value = false;
showSceneDropdown.value = false;
// 执行刷新
handleRefreshTemplate();
});
// 获取小程序胶囊位置信息,实现导航栏与胶囊完美对齐
// #ifdef MP-WEIXIN
try {
... ... @@ -503,6 +536,10 @@ onMounted(() => {
}
// #endif
});
onUnmounted(() => {
uni.$off('refreshTemplateList');
});
</script>
<style scoped lang="scss">
... ... @@ -729,7 +766,7 @@ onMounted(() => {
/* 正确的下拉菜单 */
.filter-wrapper {
position: relative;
z-index: 9999;
// z-index: 9999
}
.dropdown-menu {
... ...
... ... @@ -198,16 +198,26 @@ const logout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
success: async (res) => {
// if (res.confirm) {
// // 调用退出登录接口
// AuthUtil.logout().then(() => {
// // 清空用户信息
// userStore.logout();
// // 跳转到登录页
// uni.reLaunch({ url: '/pages/index/index' });
// });
// }
try {
// 调用退出登录接口
AuthUtil.logout().then(() => {
// 清空用户信息
userStore.logout();
// 跳转到登录页
uni.reLaunch({ url: '/pages/index/index' });
});
await AuthUtil.logout();
// 清空用户信息
userStore.logout();
// 跳转到正确登录页
uni.reLaunch({ url: '/pages7/pages/index/login' });
} catch (err) {
uni.showToast({ title: '退出失败', icon: 'none' });
}
},
});
... ...
... ... @@ -56,7 +56,10 @@
<view class="input-box">
<u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
<input type="password" v-model="password" placeholder="请输入您的密码" placeholder-style="color: #a1a8b3"
class="native-input" :password="true" />
class="native-input" :password="!passwordShow" />
<!-- -->
<u-icon :name="passwordShow ? 'eye' : 'eye-off'" size="20" color="#a1a8b3" class="eye-icon"
@click="passwordShow = !passwordShow" />
</view>
</view>
</view>
... ... @@ -70,7 +73,7 @@
<u-icon v-if="isAgreed" name="checkbox-mark" size="12" color="#ffffff" />
</view>
<view class="agreement-text">
我已阅读并同意 FitFlow
我已阅读并同意 自己练
<text class="link-text" @click.stop="goToAgreement('service')">《用户服务协议》</text>
<text class="link-text" @click.stop="goToAgreement('privacy')">《隐私政策》</text>
... ... @@ -98,6 +101,7 @@ const activeTab = ref('sms'); // sms: 免密, password: 密码
const phoneNumber = ref('');
const verifyCode = ref('');
const password = ref('');
const passwordShow = ref(false) // 控制密码是否明文显示
const isAgreed = ref(false);
// 防抖及加载动画状态变量
... ... @@ -396,6 +400,10 @@ onHide(() => {
margin-right: 16rpx;
}
.eye-icon {
margin-left: 16rpx;
}
.native-input {
flex: 1;
height: 100%;
... ...
... ... @@ -29,7 +29,6 @@ const options = {
isToken: true,
};
/** 跳转到登录页(navigateTo 失败时 reLaunch,避免页面栈异常) */
function navigateToLogin() {
uni.navigateTo({
... ...
// sheep/store/trainingSnapshot.js
import { reactive } from 'vue';
/**
* 训练快照 — 悬浮球与训练页面的共享数据桥梁
* 不是 Pinia store,是模块级 reactive 对象,存在 JS 堆内存中
* 页面跳转、clearTrainingStore() 都碰不到它
*/
export const trainingSnapshot = reactive({
/** 全量 store 状态快照(JSON 深拷贝) */
data: null,
/** 快照时间戳(毫秒),用于回显时补偿计时器 */
timestamp: null,
/** 快照时计时器是否正在运行 */
timerWasRunning: false,
/** 悬浮球是否应该显示(独立于 trainingStore.min) */
visible: false,
});
... ...
... ... @@ -416,7 +416,7 @@ export const useTrainingStore = defineStore('training', {
this.loading = false;
this.unitRecords = {};
this.trainingTimeText = '';
this.min = false;
// this.min = false;
this.isSystem = 1;
this.trainingName = '';
this.defaultTimeIndex = [0, 0, 0];
... ...