Authored by Bad

修复bug

Too many changes to show.

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

... ... @@ -32,10 +32,14 @@ SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
### SHOPRO_STATIC_URL = https://file.sheepjs.com
# 前端 H5 访问域名
SHOPRO_H5_URL=http://127.0.0.1:3000
SHOPRO_H5_URL=https://xunji.geaktec.com
# 是否开启直播 1 开启直播 | 0 关闭直播
SHOPRO_MPLIVE_ON=0
# 租户ID 默认 1
SHOPRO_TENANT_ID=1
# 默认头像
DEFAULT_AVATAR=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png
... ...
... ... @@ -6,6 +6,8 @@
:placeholder="false"
activeColor="#000"
:fixed="true"
:bottom="0"
:safeAreaInsetBottom="false"
>
<up-tabbar-item
v-for="(item, index) in tabList"
... ...
{
"name": "智能健身",
"appid": "__UNI__7CAA18B",
"appid": "__UNI__8F24C84",
"description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
"versionName": "2025.10",
"versionCode": "183",
... ...
... ... @@ -100,7 +100,6 @@
"backgroundColor": "#1a1a1a"
}
},
{
"path": "pages/xunji/xunji-dongzuo-lianxi",
"style": {
... ... @@ -113,14 +112,12 @@
"navigationBarTitleText": "新增超级组"
}
},
{
"path": "pages/xunji/wode-jihua-paike",
"style": {
"navigationBarTitleText": "我的计划详情的排课设置"
}
},
{
"path": "pages/xunji/dongzuo-muluguanli",
"style": {
... ... @@ -131,7 +128,8 @@
{
"path": "pages/xunji/jihua-search",
"style": {
"navigationBarTitleText": "训记-计划-搜索"
"navigationBarTitleText": "计划-搜索",
"navigationStyle": "default"
}
},
{
... ... @@ -185,7 +183,6 @@
"navigationBarTitleText": "我的常见问题"
}
},
{
"path": "pages/user/wode-guanyu-hongxing",
"style": {
... ...
... ... @@ -11,9 +11,7 @@
<text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text>
</view>
</view>
<view class="right">
<uni-icons type="right" size="16" color="#000" />
</view>
<uni-icons type="right" size="16" color="#999" />
</view>
<!-- 未登录引导区 -->
... ... @@ -25,57 +23,8 @@
<button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button>
</view>
<!-- -->
<!-- <view class="vip-banner" hover-class="opacity-hover" @click="goAddVip">
<view class="vip-info">
<uni-icons type="vip-filled" size="22" color="#f1c40f" />
<text class="vip-text">
{{ userInfo.deposit === 1 ? '鸿星·尊享会员 | 已开通' : '鸿星·会员 | 开通只需押金¥199' }}
</text>
</view>
<view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view>
</view> -->
<!-- 课程状态快速入口 -->
<!-- <view class="section-card quick-entry">
<view v-for="entry in quickEntryConfig" :key="entry.type" class="entry-item" hover-class="opacity-hover"
@click="handleQuickEntry(entry.type)">
<text class="num">{{ userInfo[entry.key] || 0 }}</text>
<text class="label">{{ entry.label }}</text>
</view>
</view> -->
<!-- 广告位 -->
<!-- <view class="banner-box" @click="goJiamen">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png"
mode="aspectFill" class="banner-img" />
</view> -->
<!-- 核心应用区 -->
<!-- <view class="section-card apply-section">
<view v-for="(item, index) in APPLY_CONFIG_LIST" :key="index" class="apply-item"
@click="authNavigateTo(item.url)">
<view class="icon-bg">
<image :src="item.icon" class="img" mode="aspectFit" />
</view>
<text class="text">{{ item.text }}</text>
</view>
</view> -->
<!-- 资产账户网格区 -->
<!-- <view v-if="userStore.isLogin" class="section-card">
<view class="account-grid">
<view v-for="(acc, idx) in accountConfig" :key="idx" class="account-item" @click="authNavigateTo(acc.url)">
<text class="acc-lab">{{ acc.label }}</text>
<text class="acc-val">
{{ formatAccountValue(userInfo[acc.key], acc.isFloat) }}
<text v-if="acc.unit" class="unit">{{ acc.unit }}</text>
</text>
</view>
</view>
</view> -->
<!-- 功能矩阵九宫格 -->
<!-- 功能 -->
<view class="section-card icon-grid-box">
<view class="icon-grid">
<view v-for="(item, index) in FUNCTION_CONFIG_LIST" :key="index" class="icon-item"
... ... @@ -95,15 +44,17 @@
</view>
<!-- 设置区 -->
<view v-if="userStore.isLogin" class="section-card">
<view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-shezhi')">
<text class="setting-text">个人设置</text>
<view v-if="userStore.isLogin" class="section-card settings-section">
<view class="settings-title">个人设置</view>
<view class="settings-item" @click="goToForgetPassword">
<text class="settings-label">修改密码</text>
<uni-icons type="right" size="14" color="#E0E0E0" />
</view>
<!-- <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')">
<text class="setting-text">隐私中心</text>
<uni-icons type="right" size="14" color="#E0E0E0" />
</view> -->
<view class="logout-wrap">
<button class="logout-btn" @click="handleLogout">退出登录</button>
</view>
</view>
<Tabbar />
... ... @@ -113,21 +64,23 @@
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import UserApi from '@/sheep/api/member/user';
import MemberApi from '@/sheep/api/member/member';
import AuthUtil from '@/sheep/api/member/auth';
import useUserStore from '@/sheep/store/user';
import { onShow } from '@dcloudio/uni-app';
import TrainingFloating from '@/pages/TrainingFloating.vue'
import { useTrainingStore } from '@/sheep/store/trainingStore';
import TrainingFloating from '@/pages/TrainingFloating.vue';
const trainingStore = useTrainingStore();
// 响应式数据挂载
// 状态
const userStore = useUserStore();
const userInfo = ref({});
const memberLevelName = ref('');
// 固定的 UI 配置
// 配置
const APPLY_CONFIG_LIST = [];
const FUNCTION_CONFIG_LIST = [
... ... @@ -155,31 +108,23 @@ const FUNCTION_CONFIG_LIST = [
];
// 课程计数状态配置映射
const quickEntryConfig = [
{ type: 1, key: 'courseNum', label: '待上课' },
{ type: 2, key: 'courseWaitNum', label: '等待中' },
{ type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' },
];
// 资产账户字段清洗配置映射 (对应接口数据类型:number 与 integer)
// 资产账户
const accountConfig = [];
/**
* JSDoc 核心资产数值洗涤函数
* @param {number|undefined} val 原始金钱/数量值
* @param {boolean} isFloat 是否需要保留两位小数
* @returns {string|number} 格式化后的安全渲染字符串
*/
const formatAccountValue = (val, isFloat) => {
if (val === undefined || val === null) return isFloat ? '0.00' : 0;
return isFloat ? Number(val).toFixed(2) : Math.floor(val);
};
/**
* 路由守卫拦截转发
* @param {string} url 目标绝对/相对地址
*/
/** 路由守卫 */
const authNavigateTo = (url) => {
if (!userStore.isLogin) {
goLogin();
... ... @@ -191,51 +136,26 @@ const authNavigateTo = (url) => {
});
};
/**
* 统一处理功能网格的点击分发
* @param {Object} item 节点配置项
*/
/** 功能网格点击分发 */
const handleGridItemClick = (item) => {
if (item.text === '联系客服') {
// #ifndef MP-WEIXIN
// 兜底非微信小程序平台(如H5、App),可以正常走原来的普通客服页面路由
authNavigateTo(item.url);
// #endif
return;
}
// 其他正常功能,正常走路由拦截守卫
authNavigateTo(item.url);
};
/**
* 异步高内聚合并请求
* 解决因先后触发 setData 导致微信小程序底层 AppService 与 WebView 之间高频拥堵卡顿的问题
*/
/** 拉取用户信息 */
const fetchPageData = async () => {
try {
// const [userRes, levelRes] = await Promise.all([
// UserApi.getUserInfo(),
// // MemberApi.getMemberLevel(),
// ]);
const userRes = await UserApi.getUserInfo();
console.log('后端返回个人信息:', userRes.data);
const rawUser = userRes.data || {};
userInfo.value = rawUser;
// 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化
// const levels = levelRes.data?.detailList || [];
// if (rawUser.level !== undefined) {
// // const target = levels.find((item) => item.id === rawUser.level);
// // memberLevelName.value = target ? target.name : '';
// } else {
// // memberLevelName.value = '';
// }
userInfo.value = userRes.data || {};
} catch (error) {
console.error('[API Error] 拉取个人资产信息流失败:', error);
console.error('[API Error] 拉取个人信息失败:', error);
}
};
... ... @@ -248,7 +168,33 @@ onShow(() => {
}
});
// 路由跳转原子原子层
// 设置
const goToForgetPassword = () => {
uni.navigateTo({
url: '/pages7/pages/index/reset-password?isModify=true',
});
};
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: async (res) => {
if (!res.confirm) return;
try {
await AuthUtil.logout();
userStore.logout();
uni.reLaunch({ url: '/pages7/pages/index/login' });
} catch (err) {
console.error('[API Error] 退出登录失败:', err);
uni.showToast({ title: '退出失败', icon: 'none' });
}
},
});
};
// 导航
const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' });
const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao');
const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan');
... ... @@ -258,286 +204,215 @@ const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shang
</script>
<style scoped lang="scss">
$brand-color: #ff6b00;
$page-bg: #f8f8f8;
// 变量
// 主色
$color-primary: #ff6b00;
$color-danger: #ff4757;
$color-danger-active: #e63946;
$color-badge: #ff4d4f;
// 文字
$color-text-primary: #333333;
$color-text-secondary: #555555;
$color-text-tertiary: #999999;
$color-text-white: #ffffff;
// 背景
$color-bg-page: #f7f7f8;
$color-bg-card: #ffffff;
$color-bg-avatar: #f0f0f0;
$color-bg-hover: #fcfcfc;
// 边框
$color-border: #f0f0f0;
// 圆角
$radius-card: 24rpx;
$radius-button: 16rpx;
$radius-round: 50%;
$radius-tag: 8rpx;
$radius-badge: 16rpx;
// 间距
$gap-section: 24rpx;
$gap-page-h: 28rpx;
$padding-card: 30rpx 28rpx;
// 字号
$font-title: 32rpx;
$font-body: 28rpx;
$font-caption: 24rpx;
$font-small: 22rpx;
$font-mini: 18rpx;
// 效果
$shadow-card: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
$transition-tap: background-color 0.2s ease, opacity 0.2s ease;
// 页面
.my-page {
min-height: 100vh;
background-color: $page-bg;
padding: 20rpx 28rpx calc(40rpx + env(safe-area-inset-bottom));
/* 解决部分 iOS 底部 Tabbar 高度塌陷 */
background-color: $color-bg-page;
padding: 20rpx $gap-page-h calc(40rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
// 卡片
.section-card {
background: #ffffff;
border-radius: 24rpx;
padding: 30rpx 20rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
background: $color-bg-card;
border-radius: $radius-card;
padding: $padding-card;
margin-bottom: $gap-section;
box-shadow: $shadow-card;
}
// 登录头部
.user-header {
display: flex;
align-items: center;
padding: 34rpx 30rpx;
padding: 36rpx $gap-page-h;
.avatar {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background: #f0f0f0;
border: 4rpx solid #fff;
border-radius: $radius-round;
background: $color-bg-avatar;
border: 4rpx solid $color-bg-card;
flex-shrink: 0;
}
.info-content {
margin-left: 24rpx;
flex: 1;
min-width: 0;
.name-row {
display: flex;
flex-direction: column;
gap: 8rpx;
.nickname {
font-size: 34rpx;
font-weight: bold;
color: #333;
font-size: 36rpx;
font-weight: 600;
color: $color-text-primary;
}
.tag {
font-size: 20rpx;
color: $brand-color;
display: inline-block;
font-size: $font-mini;
color: $color-primary;
padding: 4rpx 12rpx;
border-radius: 8rpx;
vertical-align: middle;
border-radius: $radius-tag;
background: rgba($color-primary, 0.08);
}
}
}
.right {
display: flex;
align-items: center;
gap: 60rpx;
.img {
width: 60rpx;
height: 60rpx;
}
}
}
// 未登录引导
.login-guide-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 36rpx $gap-page-h;
.guide-txt {
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
font-size: $font-title;
font-weight: 600;
color: $color-text-primary;
display: block;
}
.desc {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
font-size: $font-small;
color: $color-text-tertiary;
margin-top: 6rpx;
display: block;
}
}
.login-btn {
margin: 0;
background: $brand-color;
color: #fff;
font-size: 24rpx;
flex-shrink: 0;
background: $color-primary;
color: $color-text-white;
font-size: $font-caption;
height: 64rpx;
line-height: 64rpx;
border-radius: 32rpx;
padding: 0 30rpx;
padding: 0 32rpx;
transition: $transition-tap;
&::after {
border: none;
}
/* 清理小程序 button 默认黑边线 */
}
}
.vip-banner {
background: #2b2b2b;
border-radius: 20rpx;
padding: 24rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.vip-info {
display: flex;
align-items: center;
}
.vip-text {
color: #f1c40f;
font-size: 24rpx;
margin-left: 14rpx;
}
.vip-btn {
background: linear-gradient(90deg, #f1c40f, #f39c12);
color: #333;
font-size: 20rpx;
font-weight: bold;
padding: 8rpx 20rpx;
border-radius: 30rpx;
}
}
.quick-entry {
display: flex;
justify-content: space-around;
.entry-item {
text-align: center;
.num {
font-size: 38rpx;
font-weight: bold;
color: #333;
}
.label {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
display: block;
}
}
}
.banner-box {
height: 160rpx;
margin-bottom: 24rpx;
border-radius: 20rpx;
overflow: hidden;
.banner-img {
width: 100%;
height: 100%;
}
}
.apply-section {
display: grid;
grid-template-columns: repeat(5, 1fr);
.apply-item {
display: flex;
flex-direction: column;
align-items: center;
.icon-bg {
width: 80rpx;
height: 80rpx;
background: #f9f9f9;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
.img {
width: 44rpx;
height: 44rpx;
}
}
.text {
font-size: 22rpx;
color: #666;
}
}
}
.account-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
row-gap: 34rpx;
.account-item {
display: flex;
flex-direction: column;
align-items: center;
.acc-val {
font-size: 30rpx;
font-weight: bold;
color: #333;
.unit {
font-size: 20rpx;
font-weight: normal;
color: #666;
margin-left: 2rpx;
}
}
.acc-lab {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
&:active {
opacity: 0.85;
}
}
}
// 功能区
.icon-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
row-gap: 40rpx;
row-gap: 36rpx;
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
transition: $transition-tap;
&:active {
opacity: 0.7;
}
.icon-img-wrap {
position: relative;
width: 90rpx;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
background: #f5f6f8;
border-radius: 22rpx;
.icon-img {
width: 46rpx;
height: 46rpx;
width: 44rpx;
height: 44rpx;
}
.badge {
position: absolute;
top: -12rpx;
right: -42rpx;
/* 适当拓宽右移,容纳后端多字文本 */
background: #ff4d4f;
color: #fff;
font-size: 18rpx;
top: -10rpx;
right: -36rpx;
background: $color-badge;
color: $color-text-white;
font-size: $font-mini;
padding: 2rpx 10rpx;
border-radius: 16rpx;
border-radius: $radius-badge;
white-space: nowrap;
}
/* 未读状态小红点 */
.dot-badge {
position: absolute;
top: -4rpx;
right: -4rpx;
top: -2rpx;
right: -2rpx;
width: 14rpx;
height: 14rpx;
background: #ff4d4f;
border-radius: 50%;
background: $color-badge;
border-radius: $radius-round;
}
}
.icon-text {
font-size: 24rpx;
color: #555;
font-size: $font-caption;
color: $color-text-secondary;
}
.mp-contact-overlay-btn {
... ... @@ -547,12 +422,10 @@ $page-bg: #f8f8f8;
width: 100% !important;
height: 100% !important;
opacity: 0 !important;
/* 核心:完全透明 */
border: none !important;
padding: 0 !important;
margin: 0 !important;
z-index: 10;
/* 确保盖在图表和文字的最上方 */
&::after {
border: none !important;
... ... @@ -561,29 +434,58 @@ $page-bg: #f8f8f8;
}
}
.setting-item {
// 设置区
.settings-section {
padding: 30rpx $gap-page-h;
.settings-title {
font-size: 30rpx;
font-weight: 600;
color: $color-text-primary;
margin-bottom: 8rpx;
}
}
.settings-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f9f9f9;
padding: 28rpx 0;
border-bottom: 1rpx solid $color-border;
&:last-child {
border-bottom: none;
.settings-label {
font-size: $font-body;
color: $color-text-primary;
}
.setting-text {
font-size: 28rpx;
color: #444;
}
.logout-wrap {
margin-top: 40rpx;
.logout-btn {
width: 100%;
background-color: $color-danger;
color: $color-text-white;
border: none;
border-radius: $radius-button;
padding: 26rpx 0;
font-size: 30rpx;
font-weight: 500;
transition: $transition-tap;
&::after {
border: none;
}
.opacity-hover {
opacity: 0.7;
&:active {
background-color: $color-danger-active;
}
}
}
// 触摸反馈
.card-hover {
background-color: #fcfcfc;
background-color: $color-bg-hover;
}
}
</style>
... ...
<template>
<up-popup :show="actionShow" mode="bottom" minHeight="90vh" @close="actionShow = false" bgColor="#1a1a1a">
<scroll-view class="action-container" scroll-y>
<view class="header" v-if="type == 1">
<view class="header" v-if="type === 1">
<view class="explain">
<!-- 3D 固定图片 -->
<image v-if="modeTab === 0" :src="actionDetail.url3dAnimation" class="media-content" mode="aspectFill" />
... ... @@ -32,15 +32,15 @@
</view>
<view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial">
<view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab == 0 }"
<view class="tab-item" v-if="actionDetail.url3dAnimation" :class="{ active: modeTab === 0 }"
@click="switchModeTab(0)">
3D
</view>
<view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab == 1 }"
<view class="tab-item" v-if="actionDetail.urlRealPerson" :class="{ active: modeTab === 1 }"
@click="switchModeTab(1)">
真人
</view>
<view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab == 2 }"
<view class="tab-item" v-if="actionDetail.urlTutorial" :class="{ active: modeTab === 2 }"
@click="switchModeTab(2)">
讲解
</view>
... ... @@ -56,17 +56,18 @@
<!-- 只有个人创建的动作,才有这个编辑按钮-->
<template v-if="actionDetail?.isSystem === 0">
<view class="edit-icon" @click="edit">
<uni-icons type="more" size="24" color="#fff"></uni-icons>
<up-icon name="more-dot-fill" size="24" color="#fff" />
</view>
</template>
<!-- #ifdef H5 -->
<button class="share-btn" open-type="share">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
<up-icon name="share-fill" size="24" color="#fff" />
</button>
<!-- #endif -->
<up-icon :name="isFavorite ? 'star-fill' : 'star'" :color="isFavorite ? '#fedc1f' : '#fff'" size="24"
@click="toggleCollect">
</up-icon>
<!-- <FavoriteBtn :id="actionId" :type="type" /> -->
</view>
</view>
<!-- 要点,历史,平替动作标签 -->
... ... @@ -106,16 +107,16 @@
</view>
</view>
<template v-if="type == 1">
<template v-if="type === 1">
<textarea class="textarea" v-model="actionDetail.note" placeholder="点击填写备注"></textarea>
</template>
<template v-if="type == 2">
<template v-if="type === 2">
<textarea class="textarea" v-model="actionDetail.userNote" placeholder="点击填写备注"></textarea>
</template>
</view>
<view class="memo-box" v-if="type == 1">
<view class="memo-box" v-if="type === 1">
<view class="memo-title">
<view class="section-title">步骤</view>
<view class="pencil-icon">
... ... @@ -168,18 +169,7 @@
</view>
</view>
</view>
<!-- 2 历史 -->
<view v-if="contentTab === 1" class="tab-pane slide-up">
<!-- <view class="stat-card highlight">
<text class="label">最长时长</text>
<text class="value">01:02:03</text>
<text class="date">记录于 2026/03/04</text>
</view> -->
<!-- <view class="chart-container" v-if="chartData && chartData.series">
<qiun-data-charts type="line" :opts="chartOpts" :chartData="chartData" />
</view> -->
<view class="history-list">
<template v-if="historyList && historyList.length > 0">
<view class="history-item" v-for="item in historyList" :key="item.id">
... ... @@ -216,7 +206,7 @@
<view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up">
<view class="substitute-list">
<view class="sub-item" v-for="item in alternativeActions" :key="item.id" @click="openActionItem(item)">
<image :src="item.url3dAnimation || lostImage" mode="aspectFill" class="img" />
<image :src="item.url3dAnimation" mode="aspectFill" class="img" />
<view class="sub-info">
<text class="name">{{ item.name }}</text>
... ... @@ -238,47 +228,35 @@
</template>
<script setup>
import { onMounted, ref, nextTick } from 'vue';
import { ref, nextTick } from 'vue';
import dayjs from 'dayjs';
import ExercisesApi from '@/sheep/api/motion/exercises';
import beizhu from '@/pages/xunji/components/beizhu.vue';
import SupersetsApi from '@/sheep/api/motion/supersets';
import TrainingApi from '@/sheep/api/Training/traininghistory';
import { onShareAppMessage } from '@dcloudio/uni-app';
import { useTrainingStore } from '@/sheep/store/trainingStore'
import { useTrainingStore } from '@/sheep/store/trainingStore';
const trainingStore = useTrainingStore();
const lostImage = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
const trainingStore = useTrainingStore()
// 静态配置
const alternativeActions = ref([]); // 平替动作列表接口数据
const loadedHistory = ref(false)
const loadedAlternative = ref(false)
// 响应式状态
const actionShow = ref(false);
const modeTab = ref(0);
const contentTab = ref(0);
const isFavorite = ref(false);
// 记录当前动作的详细数据
const actionDetail = ref({});
// 记录当前动作的id
const actionId = ref(0);
// 记录是超级组还是动作组 1=动作组,2=超级组
const type = ref(0);
const showBeizhuRef = ref(null);
const historyList = ref([]);
const alternativeActions = ref([]);
const loadedHistory = ref(false);
const loadedAlternative = ref(false);
const historyList = ref([]); // 训练历史列表接口数据
const lostImage =
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
// 切换图表模式
const switchModeTab = (index) => {
modeTab.value = index;
};
// 判断链接是否为视频链接
const isVideoUrl = (url) => {
if (!url) return false;
const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
... ... @@ -286,13 +264,10 @@ const isVideoUrl = (url) => {
return videoExts.some(ext => lowerUrl.includes(ext));
};
// 跳转到视频播放的页面
const playVideo = (url) => {
uni.navigateTo({
url: '/pages4/pages/xunji/xunji-shiping?url=' + url,
});
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-shiping?url=' + url });
};
// 获取动作收藏状态
const checkExerciseFavorited = async () => {
try {
const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
... ... @@ -308,21 +283,16 @@ const toggleCollect = async () => {
try {
const status = isFavorite.value ? 0 : 1;
console.log('actionId.value=', actionId.value, 'status=', status);
if (type.value == 1) {
console.log('动作收藏传递的参数', actionId.value, status);
if (type.value === 1) {
await ExercisesApi.toggleFavorite(actionId.value, status);
} else {
console.log('超级组收藏传递的参数', actionId.value, status);
await SupersetsApi.toggleFavorite(actionId.value, status);
}
isFavorite.value = !isFavorite.value;
uni.showToast({ title: status === 1 ? '收藏成功' : '取消收藏成功' });
} catch (err) {
console.log(err);
// 请求失败强制回滚UI
// isFavorite.value = originState;
uni.showToast({ title: '操作失败,请重试', icon: 'none' });
}
};
... ... @@ -375,26 +345,16 @@ const open = async (id, typeData) => {
contentTab.value = 0;
console.log('动作详情页面接收id和类型:', actionId.value, type.value);
// 如何判断是动作还是超级组
if (typeData == 1) {
if (typeData === 1) {
await loadexercisedetail(actionId.value);
// await loadAlternativeActions(actionId.value);
console.log('进入动作收藏检查接口');
await checkExerciseFavorited();
} else {
await loadsuperdetail(actionId.value);
console.log('进入超级组收藏检查接口');
await checkSupersetFavorited();
}
console.log('请求参数actionId/typeData', actionId.value, type.value);
// 重置加载训练历史列表和平替动作列表标记
loadedHistory.value = false
loadedAlternative.value = false
// loadTrainHistoryDetail(actionId.value, type.value);
loadedHistory.value = false;
loadedAlternative.value = false;
actionShow.value = true;
};
// 打开备注弹窗
... ... @@ -413,7 +373,7 @@ const handleNoteSave = async (content) => {
console.log('type.value=', type.value);
try {
if (type.value == 2) {
if (type.value === 2) {
await SupersetsApi.addNotes({
supersetsId: actionId.value,
content: content,
... ... @@ -428,7 +388,7 @@ const handleNoteSave = async (content) => {
} catch (e) {
console.log(e);
} finally {
if (type.value == 1) {
if (type.value === 1) {
await loadexercisedetail(actionId.value)
} else {
await loadsuperdetail(actionId.value)
... ... @@ -451,7 +411,7 @@ onShareAppMessage((res) => {
title: actionDetail.value.name || '健身动作分享', // 分享标题
// path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径
path: `/pages/xunji/xunji?currentTab=${3}`,
imageUrl: actionDetail.value.urlImage || lostImage, // 分享图片
imageUrl: actionDetail.value.urlImage, // 分享图片
};
});
... ... @@ -517,53 +477,18 @@ const loadTrainHistoryDetail = async (id, type) => {
// 格式化时间
const formatDate = (dateArr) => {
if (!Array.isArray(dateArr) || dateArr.length < 3) return '';
const [year, month, day] = dateArr;
const date = new Date(year, month - 1, day);
const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const week = weekArr[date.getDay()];
return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')} ${week}`;
return dayjs(new Date(dateArr[0], dateArr[1] - 1, dateArr[2])).format('YYYY/MM/DD dd');
};
// 格式化每组数据:自动拼接 weight/reps/duration/distance
const formatSetData = (set) => {
const parts = [];
// 重量
if (set.weight != null && set.weight !== '') {
parts.push(`${set.weight}kg`);
}
// 次数
if (set.reps != null && set.reps !== '') {
parts.push(`${set.reps}次`);
}
// 时长(自动转 时:分:秒)
if (set.duration != null && set.duration !== '') {
parts.push(formatTime(set.duration));
}
// 距离
if (set.distance != null && set.distance !== '') {
parts.push(`${set.distance}m`);
}
if (set.weight != null && set.weight !== '') parts.push(`${set.weight}kg`);
if (set.reps != null && set.reps !== '') parts.push(`${set.reps}次`);
if (set.duration != null && set.duration !== '') parts.push(dayjs.duration(set.duration, 'seconds').format('HH:mm:ss'));
if (set.distance != null && set.distance !== '') parts.push(`${set.distance}m`);
return parts.length > 0 ? parts.join(' × ') : '无数据';
};
// 新增:秒数转 00:00:00 格式
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const hh = h > 0 ? String(h).padStart(2, '0') + ':' : '';
const mm = String(m).padStart(2, '0') + ':';
const ss = String(s).padStart(2, '0');
return hh + mm + ss;
};
const edit = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-xinzeng?isEdit=true&id=${actionId.value}`
... ... @@ -577,8 +502,6 @@ const openActionItem = (item) => {
};
defineExpose({ open });
onMounted(() => { });
</script>
<style lang="scss" scoped>
... ... @@ -772,7 +695,6 @@ onMounted(() => { });
}
}
// 要点板块样式
.video-grid {
display: grid;
grid-template-columns: 1fr 1fr;
... ... @@ -816,7 +738,6 @@ onMounted(() => { });
color: #fff;
height: 160rpx;
border-radius: 16rpx;
// padding: 20rpx;
font-size: 30rpx;
border: 0;
}
... ... @@ -827,7 +748,6 @@ onMounted(() => { });
flex-direction: column;
gap: 16rpx;
background: #464646;
// padding: 20rpx;
box-sizing: border-box;
border-radius: 16rpx;
... ... @@ -877,7 +797,6 @@ onMounted(() => { });
color: #fff;
}
// 历史记录样式
.stat-card {
background: linear-gradient(135deg, #333, #222);
padding: 40rpx;
... ... @@ -933,8 +852,6 @@ onMounted(() => { });
}
.tag {
// font-size: 20rpx;
// background: #444;
margin: 5rpx 5rpx;
border-radius: 4rpx;
}
... ... @@ -948,7 +865,6 @@ onMounted(() => { });
margin-top: 20rpx;
.chip {
// background: #333;
padding: 8rpx 20rpx;
font-size: 24rpx;
display: flex;
... ... @@ -965,7 +881,6 @@ onMounted(() => { });
}
}
// 平替动作样式
.sub-item {
display: flex;
align-items: center;
... ... @@ -1024,7 +939,6 @@ onMounted(() => { });
}
}
// 动画
.slide-up {
animation: slideUp 0.4s ease-out;
}
... ...
... ... @@ -24,138 +24,83 @@
</template>
<script setup>
import dayjs from 'dayjs';
import { ref, computed, watch } from 'vue';
const emit = defineEmits(['dateTypeChange', 'dateRangeChange']);
// 当前日期类型: 'week' | 'month'
// 当前日期类型与基准日期
const currentDateType = ref('week');
// 基准日期,用于计算范围
const baseDate = ref(new Date());
// 格式化日期辅助函数 YYYY/MM/DD
const formatDate = (date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}/${m}/${d}`;
};
// 格式化日期辅助函数 YYYY-MM-DD
const formatDateYmd = (date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
const baseDate = ref(dayjs());
const currentDateRange = computed(() => {
const current = new Date(baseDate.value);
let start, end;
// ==================== 公共日期范围计算 ====================
/** 根据当前类型计算周期起止日期(dayjs 实例) */
const periodRange = computed(() => {
const d = baseDate.value;
if (currentDateType.value === 'week') {
const day = current.getDay();
const diffToMonday = day === 0 ? -6 : 1 - day;
start = new Date(current);
start.setDate(current.getDate() + diffToMonday);
end = new Date(start);
end.setDate(start.getDate() + 6);
} else {
start = new Date(current.getFullYear(), current.getMonth(), 1);
end = new Date(current.getFullYear(), current.getMonth() + 1, 0);
const dayOfWeek = d.day(); // 0=周日, 1=周一, ...
const start = d.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day');
const end = start.add(6, 'day');
return { start, end };
}
return {
start: formatDateYmd(start),
end: formatDateYmd(end)
};
// month
return { start: d.startOf('month'), end: d.endOf('month') };
});
// 计算显示的日期范围文本
const dateRangeText = computed(() => {
const current = new Date(baseDate.value);
if (currentDateType.value === 'week') {
// 计算本周的周一和周日
const day = current.getDay();
const diffToMonday = day === 0 ? -6 : 1 - day; // 调整到周一
const startOfWeek = new Date(current);
startOfWeek.setDate(current.getDate() + diffToMonday);
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
// ==================== 计算属性 ====================
return `${formatDate(startOfWeek)} - ${formatDate(endOfWeek)}`;
} else {
// 计算本月的一号和最后一天
const startOfMonth = new Date(current.getFullYear(), current.getMonth(), 1);
const endOfMonth = new Date(current.getFullYear(), current.getMonth() + 1, 0);
/** 对外暴露的日期范围(YYYY-MM-DD) */
const currentDateRange = computed(() => ({
start: periodRange.value.start.format('YYYY-MM-DD'),
end: periodRange.value.end.format('YYYY-MM-DD'),
}));
return `${formatDate(startOfMonth)} - ${formatDate(endOfMonth)}`;
}
/** 显示的日期范围文本 "YYYY/MM/DD - YYYY/MM/DD" */
const dateRangeText = computed(() => {
const { start, end } = periodRange.value;
return `${start.format('YYYY/MM/DD')} - ${end.format('YYYY/MM/DD')}`;
});
// 新增: 计算下一个日期是否不可用(即是否为未来日期)
/** 下一周期是否超过今天(超出则隐藏前进按钮) */
const isNextDisabled = computed(() => {
const newDate = new Date(baseDate.value);
if (currentDateType.value === 'week') {
newDate.setDate(newDate.getDate() + 7);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
// 重置时分秒进行比较
const now = new Date();
now.setHours(0, 0, 0, 0);
const checkDate = new Date(newDate);
checkDate.setHours(0, 0, 0, 0);
return checkDate > now;
const nextBase = currentDateType.value === 'week'
? baseDate.value.add(7, 'day')
: baseDate.value.add(1, 'month');
return nextBase.startOf('day').isAfter(dayjs().startOf('day'));
});
// 切换周/月
// ==================== 事件处理 ====================
const switchDateType = (type) => {
currentDateType.value = type;
emit('dateTypeChange', type);
};
// 上一段时间
const handlePrev = () => {
const newDate = new Date(baseDate.value);
if (currentDateType.value === 'week') {
newDate.setDate(newDate.getDate() - 7);
} else {
newDate.setMonth(newDate.getMonth() - 1);
}
baseDate.value = newDate;
baseDate.value = currentDateType.value === 'week'
? baseDate.value.subtract(7, 'day')
: baseDate.value.subtract(1, 'month');
};
// 下一段时间
const handleNext = () => {
// 虽然按钮已隐藏,但保留逻辑判断以防直接调用或其他边界情况
if (isNextDisabled.value) {
return;
}
const newDate = new Date(baseDate.value);
if (currentDateType.value === 'week') {
newDate.setDate(newDate.getDate() + 7);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
baseDate.value = newDate;
if (isNextDisabled.value) return;
baseDate.value = currentDateType.value === 'week'
? baseDate.value.add(7, 'day')
: baseDate.value.add(1, 'month');
};
// ==================== 双向绑定 ====================
watch(
[baseDate, currentDateType],
() => {
emit('dateRangeChange', {
startDate: currentDateRange.value.start,
endDate: currentDateRange.value.end
endDate: currentDateRange.value.end,
});
},
{ immediate: true }
{ immediate: true },
);
</script>
... ... @@ -180,10 +125,6 @@ watch(
padding: 6rpx;
box-sizing: border-box;
// #ifdef H5
margin-top: 30px;
// #endif
.tab-item {
display: flex;
align-items: center;
... ...
<template>
<view class="exercise-page" @click="closeAddMenu">
<!-- 搜索框 -->
<view class="search-bar">
<!-- <view class="search">
<uni-icons type="search" size="18"></uni-icons>
<input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" />
</view> -->
<view class="add-btn" @click.stop="addExercise">
<uni-icons type="plus" size="35" color="#333"></uni-icons>
<view class="floating-menu" v-show="addshow" @click.stop>
<view class="menu-item">
<uni-icons type="plus" size="24" color="#333"></uni-icons>
<text class="menu-text" @click="addnewmotion">新增动作</text>
<view class="exercise-page">
<!-- 顶部操作栏:搜索 + 快捷新增 -->
<view class="top-bar">
<view class="search-wrapper">
<up-search v-model="searchKeyword" placeholder="搜索动作名称" :showAction="false" shape="round" bgColor="#f3f4f6"
placeholderColor="#9ca3af" searchIconColor="#6b7280" @clear="searchKeyword = ''" />
</view>
<view class="menu-item">
<uni-icons type="link" size="24" color="#333"></uni-icons>
<text class="menu-text" @click="addnewsupersets">新增超级组</text>
<view class="add-btn-wrapper">
<view class="add-icon-btn" @click.stop="addShow = !addShow">
<up-icon name="plus" size="20" color="#111827" bold />
</view>
<view class="menu-item">
<uni-icons type="list" size="24" color="#333"></uni-icons>
<text class="menu-text" @click="actionmanagement">动作目录管理</text>
<!-- 下拉浮动菜单 -->
<view class="floating-menu" v-if="addShow" @click.stop>
<view class="menu-item" @click="addnewmotion">
<up-icon name="plus-circle" size="18" color="#10b981" />
<text class="menu-text">新增动作</text>
</view>
<view class="menu-item" @click="addnewsupersets">
<up-icon name="attach" size="18" color="#3b82f6" />
<text class="menu-text">新增超级组</text>
</view>
<view class="menu-item" @click="actionmanagement">
<up-icon name="list-dot" size="18" color="#f59e0b" />
<text class="menu-text">动作目录管理</text>
</view>
</view>
<!-- 浮动菜单 -->
<view class="menu-mask" v-if="addShow" @click="addShow = false" />
</view>
</view>
<!-- 左右布局 -->
<!-- 主体左右双栏布局 -->
<view class="layout-container">
<scroll-view scroll-y class="left-nav" enable-flex>
<view class="nav-item" @click="handleCollectClick('collect')" :class="{ active: activeNav === 'collect' }">
收藏
<!-- 左侧一级分类导航 -->
<scroll-view scroll-y class="left-nav" :show-scrollbar="false">
<view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick">
<up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>
<text class="nav-text">收藏</text>
</view>
<view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }"
@click="switchNav(nav.id)">
{{ nav.name }}
<text class="nav-text">{{ nav.name }}</text>
</view>
</scroll-view>
<!-- 小动作列表 -->
<scroll-view scroll-y class="right-content" enable-flex>
<view class="tip" v-if="motionPart.length > 0">
<view class="item" @click="handlePartClick('')" :class="{ active: activeMotionPart == '' }">全部</view>
<view class="item" v-for="item in motionPart" :key="item.id" :class="{ active: activeMotionPart == item.id }"
@click="handlePartClick(item.id)">
<!-- 右侧二级筛选与动作网格内容 -->
<scroll-view scroll-y class="right-content" :show-scrollbar="false">
<!-- 二级子部位 Chips 筛选条 -->
<scroll-view scroll-x class="sub-part-scroll" v-if="motionPart.length > 0">
<view class="sub-part-wrapper">
<view class="part-chip" :class="{ active: activeMotionPart === '' }" @click="handlePartClick('')">
全部
</view>
<view v-for="item in motionPart" :key="item.id" class="part-chip"
:class="{ active: activeMotionPart === item.id }" @click="handlePartClick(item.id)">
{{ item.name }}
</view>
</view>
</scroll-view>
<!-- 动作与超级组网格卡片区 -->
<view class="exercise-grid">
<view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0">
<view class="equipment-list">
<view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
<view class="equipment-name"> {{ item.equipmentName }} </view>
<view class="action-list">
<view class="action-item" v-for="e in item.exercises" :key="e.id">
<image :src="e.url3dAnimation" mode="aspectFill" lazy-load class="action-img"
@click="goToDetail(e.id, 1)"></image>
<view class="action-name">{{ e.name }}</view>
<view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view>
<template v-if="displayExercises.length > 0 || superGroupInfo.length > 0">
<!-- 器械分组列表 -->
<view class="equipment-group" v-for="item in displayExercises" :key="item.equipmentId">
<view class="equipment-title">
<view class="title-bar"></view>
<text>{{ item.equipmentName || '通用器械' }}</text>
</view>
<view class="action-grid">
<view class="action-card" v-for="e in item.exercises" :key="e.id" @click="goToDetail(e.id, 1)">
<view class="image-wrapper">
<image :src="e.url3dAnimation" mode="aspectFil" lazy-load class="action-img"></image>
<view class="reps-badge" v-if="e.trainingReps">
{{ e.trainingReps }}次
</view>
</view>
<view class="supers" v-if="superGroupInfo.length > 0">
<view class="supers-name"> 超级组 </view>
<view class="super" v-for="item in superGroupInfo" :key="item.id" @click="goToDetail(item.id, 2)">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
mode="aspectFill" lazy-load class="super-img"></image>
<view class="right">
<view class="super-name">{{ item.name }}</view>
<view class="parts">{{ item.primaryMuscles.join() }}</view>
<view class="action-info">
<text class="action-name">{{ e.name }}</text>
</view>
</view>
</view>
</view>
<!-- 超级组展示模块 -->
<view class="supers-group" v-if="superGroupInfo.length > 0">
<view class="equipment-title">
<view class="title-bar blue"></view>
<text>超级组</text>
</view>
<!-- <view class="add-super">
<view class="header">
<up-icon name="plus" color="#000" size="15" bold></up-icon>
<text>添加自定义动作</text>
<view class="super-card" v-for="item in superGroupInfo" :key="item.id" @click="goToDetail(item.id, 2)">
<image src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
mode="aspectFill" lazy-load class="super-img"></image>
<view class="super-info">
<text class="super-name">{{ item.name }}</text>
<text class="super-muscles" v-if="item.primaryMuscles">
{{ Array.isArray(item.primaryMuscles) ? item.primaryMuscles.join(' · ') : item.primaryMuscles }}
</text>
</view>
<view class="tip">添加官方库没有的动作</view>
</view> -->
<up-icon name="arrow-right" size="14" color="#9ca3af"></up-icon>
</view>
</view>
</template>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无动作数据</text>
<!-- 优雅的空状态 -->
<view class="empty-wrapper" v-else>
<up-empty mode="data" text="暂无相关动作数据"></up-empty>
</view>
</view>
</scroll-view>
</view>
<!-- 详情弹窗 -->
<!-- 动作详情弹窗组件 -->
<dongzuoXianqing ref="actionDetailRef" />
<!-- 底部安全区占位(关键!解决不同设备适配) -->
<view class="safe-area-bottom"></view>
</view>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
import { ref, onMounted, computed } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import SupersetsApi from '@/sheep/api/motion/supersets';
import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue';
... ... @@ -111,29 +132,35 @@ import { useActionStore } from '@/sheep/store/action';
const actionStore = useActionStore();
// 响应式数据
const activeNav = ref('');
const navItems = ref([]);
const exercises = ref([]);
const superGroupInfo = ref([]);
const motionPart = ref([]);
const activeMotionPart = ref('');
const currentCategoryId = ref('');
const allExercises = ref([]);
const addshow = ref(false);
const superFavoriteList = ref([]);
const searchKeyword = ref('');
const addShow = ref(false);
const actionDetailRef = ref(null);
// 关闭菜单
const closeAddMenu = () => {
addshow.value = false;
};
const displayExercises = computed(() => {
if (!searchKeyword.value.trim()) {
return exercises.value;
}
const kw = searchKeyword.value.toLowerCase().trim();
return exercises.value
.map((group) => {
const filtered = (group.exercises || []).filter((ex) =>
ex.name && ex.name.toLowerCase().includes(kw)
);
return {
...group,
exercises: filtered,
};
})
.filter((group) => group.exercises && group.exercises.length > 0);
});
// 部位筛选
const handlePartClick = async (id) => {
try {
activeMotionPart.value = id;
... ... @@ -143,37 +170,40 @@ const handlePartClick = async (id) => {
});
exercises.value = exerciseRes.data || [];
} catch (e) {
console.error('加载动作失败:', e);
console.error('加载子部位动作失败:', e);
}
};
// 收藏
const handleCollectClick = async (id) => {
activeNav.value = id;
const handleCollectClick = () => {
activeNav.value = 'collect';
activeMotionPart.value = '';
motionPart.value = [];
loadCollectList();
loadSuperFavoriteList();
};
// 动作收藏
// 加载动作收藏列表
const loadCollectList = async () => {
try {
const res = await ExercisesApi.getFavoriteExercises();
exercises.value = res.data || [];
} catch (err) {
console.error('加载动作收藏失败', err);
console.error('加载动作收藏失败:', err);
}
};
// 超级组收藏
// 加载超级组收藏列表
const loadSuperFavoriteList = async () => {
try {
const res = await SupersetsApi.getFavoriteSuperset();
superGroupInfo.value = res.data || [];
} catch (err) {
superFavoriteList.value = [];
console.error('加载超级组收藏失败', err);
superGroupInfo.value = [];
console.error('加载超级组收藏失败:', err);
}
};
// 加载
// 加载
const loadCategories = async () => {
try {
await actionStore.getloadCategories();
... ... @@ -187,26 +217,27 @@ const loadCategories = async () => {
}
};
// 加载动作
// 加载动作列表
const loadExercises = async (categoriesId) => {
try {
const partRes = await ExercisesApi.getMotionPart(categoriesId);
motionPart.value = partRes.data || [];
console.log(motionPart.value, 'motionPart.value');
const exerciseRes = await ExercisesApi.getexercises({ categoriesId });
exercises.value = exerciseRes.data;
exercises.value = exerciseRes.data || [];
} catch (error) {
console.error('加载动作失败:', error);
}
};
// 切换导航
// 切换一级导航
const switchNav = (id) => {
if (activeNav.value === id) return;
activeNav.value = id;
activeMotionPart.value = '';
if (id == 'super') {
searchKeyword.value = '';
if (id === 'super') {
exercises.value = [];
motionPart.value = [];
loadsupersetsinfo();
... ... @@ -216,21 +247,7 @@ const switchNav = (id) => {
}
};
// 搜索
const onSearch = (e) => {
const searchText = e.detail.value.trim();
if (!currentCategoryId.value) return;
searchText ? searchExercises(searchText) : loadExercises(currentCategoryId.value);
};
const searchExercises = (keyword) => {
const key = keyword.toLowerCase().trim();
exercises.value = allExercises.value.filter((item) => {
return item.name && item.name.toLowerCase().includes(key);
});
};
// 加载超级组列表
const loadsupersetsinfo = async () => {
try {
const response = await SupersetsApi.getsupersets();
... ... @@ -240,31 +257,22 @@ const loadsupersetsinfo = async () => {
}
};
// 页面跳转
const goToDetail = (id, type) => {
console.log('type=', type);
nextTick(() => {
actionDetailRef.value.open(id, type);
});
};
const addExercise = () => {
addshow.value = !addshow.value;
actionDetailRef.value?.open(id, type);
};
const addnewmotion = () => {
addShow.value = false;
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });
};
const addnewsupersets = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(
JSON.stringify(navItems.value),
)}`,
});
addShow.value = false;
uni.navigateTo({ url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(JSON.stringify(navItems.value))}` });
};
const actionmanagement = () => {
addShow.value = false;
uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' });
};
... ... @@ -272,352 +280,324 @@ onMounted(() => {
loadCategories();
});
</script>
<style lang="scss" scoped>
.exercise-page {
background-color: white;
box-sizing: border-box;
height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
.search-bar {
box-sizing: border-box;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 20rpx;
// background-color: #f5f5f5;
border-radius: 12rpx;
margin-top: 20rpx;
height: 87vh;
// #ifdef H5
height: calc(100vh - 44px);
// #endif
/* #ifdef H5 */
margin-top: 20px;
/* #endif */
background-color: #ffffff;
overflow: hidden;
.search {
/* 顶部搜索操作栏 */
.top-bar {
display: flex;
align-items: center;
padding: 10rpx 20rpx;
flex: 1;
border-radius: 30rpx;
background-color: #f5f5f5;
padding: 16rpx 24rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f3f4f6;
z-index: 50;
.search-icon {
width: 30rpx;
height: 30rpx;
}
.search-input {
.search-wrapper {
flex: 1;
padding-left: 15rpx;
font-size: 26rpx;
color: #333;
border: none;
outline: none;
background: transparent;
}
margin-right: 20rpx;
}
.add-btn {
width: 60rpx;
height: 60rpx;
background-color: transparent;
border: none;
margin-left: 10rpx;
.add-btn-wrapper {
position: relative;
.add-icon-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #f3f4f6;
display: flex;
justify-content: center;
align-items: center;
z-index: 99;
position: relative;
transition: background-color 0.2s;
.plus-icon {
width: 40rpx;
height: 40rpx;
&:active {
background-color: #e5e7eb;
}
}
.floating-menu {
position: absolute;
top: 30rpx;
right: 30rpx;
width: 300rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
padding: 20rpx;
top: 84rpx;
right: 0;
width: 320rpx;
background-color: #ffffff;
border-radius: 20rpx;
box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.12);
padding: 12rpx 0;
z-index: 100;
border: 1rpx solid #f3f4f6;
.menu-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
gap: 16rpx;
padding: 24rpx 28rpx;
transition: background-color 0.15s;
.u-icon {
margin-right: 16rpx;
&:active {
background-color: #f9fafb;
}
.menu-text {
font-size: 28rpx;
color: #333;
color: #1f2937;
font-weight: 500;
}
&:last-child {
border-bottom: none;
}
}
.menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 90;
background-color: transparent;
}
}
}
.layout-container {
box-sizing: border-box;
height: 100%;
display: flex;
height: calc(82vh + 25rpx);
background-color: #f5f5f5;
overflow: hidden;
.left-nav {
width: 160rpx;
background-color: white;
border-right: 1px solid #eee;
// 添加底部 padding,避免最后一项被底部导航栏遮挡
padding-bottom: 20rpx;
// #ifdef MP-WEIXIN
// 微信小程序端可能需要更多底部空间
padding-bottom: 200rpx;
width: 180rpx;
height: 100%;
background-color: #f9fafb;
border-right: 1rpx solid #f3f4f6;
// #endif
.nav-item {
padding: 24rpx 0;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 32rpx 16rpx;
position: relative;
transition: all 0.2s;
.nav-text {
font-size: 26rpx;
color: #333;
color: #4b5563;
font-weight: 400;
text-align: center;
line-height: 1.3;
}
.active {
color: #16ad40;
background-color: #e1f6e9;
border-right: 3px solid #16ad40;
font-weight: bold;
&.active {
background-color: #ffffff;
.nav-text {
color: #10b981;
font-weight: 600;
}
&::before {
content: '';
position: absolute;
left: 0;
top: 24rpx;
bottom: 24rpx;
width: 6rpx;
background-color: #10b981;
border-radius: 0 4rpx 4rpx 0;
}
}
}
}
/* 右侧内容滚动区 */
.right-content {
box-sizing: border-box;
flex: 1;
padding: 20rpx;
width: calc(100% - 180rpx);
height: 100%;
// 添加底部 padding,避免内容被底部导航栏遮挡
// 底部导航栏高度约 100rpx(50px),加上安全区域
padding-bottom: 20rpx;
// #ifdef MP-WEIXIN
// 微信小程序端可能需要更多底部空间
padding-bottom: 100rpx;
// #endif
.tip {
display: flex;
padding-left: 20rpx;
align-items: center;
column-gap: 15rpx;
margin-bottom: 20rpx;
background-color: #efefef;
padding: 20rpx 24rpx 200rpx;
box-sizing: border-box;
.item {
margin: 0;
padding: 12rpx 20rpx;
font-size: 22rpx;
line-height: 1;
background-color: #fff;
border-radius: 20rpx;
/* 二级子部位芯片滚动条 */
.sub-part-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 24rpx;
.sub-part-wrapper {
display: inline-flex;
gap: 16rpx;
padding-bottom: 4rpx;
.part-chip {
display: inline-block;
padding: 10rpx 24rpx;
font-size: 24rpx;
color: #4b5563;
background-color: #f3f4f6;
border-radius: 30rpx;
transition: all 0.2s;
&.active {
background-color: #d8ede0;
color: #43b05e;
background-color: #ecfdf5;
color: #10b981;
font-weight: 600;
}
}
}
}
/* 动作网格区 */
.exercise-grid {
display: flex;
flex-direction: column;
gap: 24rpx;
gap: 32rpx;
.title {
.equipment-group {
.equipment-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 600;
padding-left: 20rpx;
font-size: 30rpx;
}
.content {
width: 100%;
// display: grid;
// grid-template-columns: repeat(2, 1fr);
// /* 改为3列布局 */
// gap: 16rpx;
// .exercise-item {
// display: flex;
// height: 150rpx;
// /* 减小项目高度 */
// flex-direction: column;
// align-items: center;
// justify-content: center;
// background-color: white;
// padding: 16rpx 8rpx;
// box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
// .exercise-img {
// width: 90%;
// height: 140rpx;
// /* 固定图片高度 */
// border-radius: 8rpx;
// margin-bottom: 12rpx;
// }
// .exercise-title {
// font-size: 24rpx;
// color: #333;
// text-align: center;
// line-height: 1.3;
// max-width: 100%;
// text-overflow: ellipsis;
// display: -webkit-box;
// -webkit-box-orient: vertical;
// }
// }
.equipment-item {
width: 100%;
// margin-bottom: 40rpx;
.equipment-name {
color: #000;
font-size: 32rpx;
color: #111827;
margin-bottom: 20rpx;
.title-bar {
width: 8rpx;
height: 28rpx;
background-color: #10b981;
border-radius: 4rpx;
&:last-child {
margin-bottom: 50rpx;
&.blue {
background-color: #3b82f6;
}
}
}
.action-list {
.action-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10rpx;
.action-item {
gap: 15rpx;
.action-card {
background-color: #ffffff;
border-radius: 16rpx;
border: 1rpx solid #f3f4f6;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10rpx;
background: #fff;
border-radius: 10rpx;
padding: 20rpx;
box-sizing: border-box;
transition: transform 0.15s;
&:active {
transform: scale(0.98);
}
.image-wrapper {
position: relative;
width: 100%;
height: 200rpx;
background-color: #f9fafb;
.action-img {
width: 100%;
height: 250rpx;
border-radius: 10rpx;
height: 200rpx;
}
.trainingReps {
.reps-badge {
position: absolute;
top: 20rpx;
right: 20rpx;
z-index: 1;
font-size: 22rpx;
top: 12rpx;
right: 12rpx;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
color: #ffffff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 500;
}
&:last-child {
margin-bottom: 50rpx;
padding-bottom: env(safe-area-inset-bottom);
}
.action-info {
padding: 16rpx 12rpx;
display: flex;
align-items: center;
justify-content: center;
.action-name {
font-size: 24rpx;
font-weight: 500;
color: #1f2937;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
}
}
}
}
.supers-name {
color: #000;
font-size: 32rpx;
margin-bottom: 20rpx;
}
.super {
/* 超级组卡片样式 */
.supers-group {
.super-card {
display: flex;
align-items: center;
background: #fff;
background-color: #ffffff;
border-radius: 16rpx;
border: 1rpx solid #f3f4f6;
padding: 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
gap: 20rpx;
font-size: 26rpx;
margin-bottom: 20rpx;
.super-img {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
&:active {
background-color: #f9fafb;
}
.super-name {
margin-bottom: 10rpx;
}
.super-img {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
background-color: #f3f4f6;
flex-shrink: 0;
}
.add-super {
background: #fff;
padding: 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
.header {
.super-info {
flex: 1;
display: flex;
gap: 10rpx;
align-items: center;
margin-bottom: 20rpx;
flex-direction: column;
gap: 8rpx;
.super-name {
font-size: 28rpx;
font-weight: 600;
color: #111827;
}
.tip {
margin: 0rpx;
padding: 0rpx;
.super-muscles {
font-size: 22rpx;
color: #6b7280;
}
}
}
.nav-item .category-group {
/* 超级组特殊样式 */
background-color: #f0f9ff;
font-weight: bold;
border-top: 1px solid #eee;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
.empty-text {
font-size: 28rpx;
color: #999;
.empty-wrapper {
padding: 80rpx 0;
}
}
}
}
}
.safe-area-bottom {
height: env(safe-area-inset-bottom); // 适配iOS安全区
// 额外加上 Tabbar 的高度,你的 Tabbar 高度是 85px = 170rpx
padding-bottom: 170rpx;
box-sizing: border-box;
}
</style>
\ No newline at end of file
... ...
<!-- 第一层 模板大类 -->
<template>
<view class="template-page" @click="closeAllDropdowns">
<!-- 筛选按钮 -->
<view class="filter-section" @click.stop>
<!-- 部位筛选 -->
<view class="template-page">
<!-- 顶部筛选操作栏(固定不缩放) -->
<view class="filter-section">
<!-- 部位筛选胶囊 -->
<view class="filter-wrapper">
<view class="filter-btn" :class="{ active: showPartDropdown }" @click="togglePartDropdown">
<view class="filter-btn" :class="{ active: showPartDropdown, selected: activePartId !== '0' }"
@click.stop="togglePartDropdown">
<text class="text">{{ activePart }}</text>
<uni-icons :type="showPartDropdown ? 'up' : 'down'" size="13"></uni-icons>
<up-icon :name="showPartDropdown ? 'arrow-up-fill' : 'arrow-down-fill'" size="10"
:color="showPartDropdown || activePartId !== '0' ? '#10b981' : '#6b7280'"></up-icon>
</view>
<!-- 部位下拉菜单 -->
<view class="dropdown-menu" v-if="showPartDropdown">
<view v-for="(item, index) in partList" :key="index" class="dropdown-item"
:class="{ selected: activePart === item.title }" @click="selectPart(item)">
{{ item.title }}
<view class="dropdown-menu" v-if="showPartDropdown" @click.stop>
<view v-for="item in partList" :key="item.id" class="dropdown-item"
:class="{ selected: activePartId === item.id }" @click="selectPart(item)">
<text>{{ item.title }}</text>
<up-icon v-if="activePartId === item.id" name="checkbox-mark" size="14" color="#10b981" />
</view>
</view>
</view>
<!-- 场景筛选 -->
<!-- 场景筛选胶囊 -->
<view class="filter-wrapper">
<view class="filter-btn" :class="{ active: showSceneDropdown }" @click="toggleSceneDropdown">
<view class="filter-btn" :class="{ active: showSceneDropdown, selected: activeSceneId !== '0' }"
@click.stop="toggleSceneDropdown">
<text class="text">{{ activeScene }}</text>
<uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="13"></uni-icons>
<up-icon :name="showSceneDropdown ? 'arrow-up-fill' : 'arrow-down-fill'" size="10"
:color="showSceneDropdown || activeSceneId !== '0' ? '#10b981' : '#6b7280'"></up-icon>
</view>
<!-- 场景下拉菜单 -->
<view class="dropdown-menu" v-if="showSceneDropdown">
<view v-for="(item, index) in sceneList" :key="index" class="dropdown-item"
<view class="dropdown-menu" v-if="showSceneDropdown" @click.stop>
<view v-for="item in sceneList" :key="item.id" class="dropdown-item"
:class="{ selected: activeSceneId === item.id }" @click="selectScene(item)">
{{ item.title }}
<text>{{ item.title }}</text>
<up-icon v-if="activeSceneId === item.id" name="checkbox-mark" size="14" color="#10b981" />
</view>
</view>
</view>
<!-- 下拉全屏遮罩蒙层 -->
<view class="dropdown-mask" v-if="showPartDropdown || showSceneDropdown" @click="closeAllDropdowns"></view>
</view>
<!-- 模板列表 (根据状态自动切换显示) -->
<scroll-view class="template-list" enable-flex scroll-y>
<!-- 用父容器包裹 v-if 分支,解决 key 冲突 -->
<view v-if="!isFiltering" class="sub-template-list">
<view v-for="(item, index) in templateList" :key="index" class="template-item">
<image :src="item.urlCover" mode="aspectFill" class="template-img"></image>
<view>
<view class="template-content" @click="gototemplate(item)">
<view class="template-title">{{ item.name }}</view>
<view class="template-count">{{ item.templatesCount }}个模板</view>
<view class="template-desc">{{ item.description }}</view>
<!-- 模板列表展示区(自适应滚动) -->
<view class="list-container">
<scroll-view class="template-scroll" scroll-y :show-scrollbar="false">
<view class="scroll-inner">
<!-- 模式一:未筛选状态,展示【模板大类列表】 -->
<template v-if="!isFiltering">
<view v-for="item in templateList" :key="item.id" class="template-card" @click="gototemplate(item)">
<image :src="item.urlCover" mode="aspectFill" class="cover-img" lazy-load />
<view class="card-content">
<view class="card-header">
<text class="title">{{ item.name }}</text>
<text class="count-badge">{{ item.templatesCount || 0 }}个模板</text>
</view>
<text class="desc" v-if="item.description">{{ item.description }}</text>
</view>
</view>
</template>
<!-- 模式二:已筛选状态,展示【条件检索后的细分模板列表】 -->
<template v-else>
<view v-for="template in filteredTemplateList" :key="template.id" class="template-card"
@click="goToTemplateDetail(template)">
<image :src="template.urlCover" mode="aspectFill" class="cover-img" lazy-load />
<view class="card-content">
<text class="title">{{ template.name }}</text>
<!-- 肌群说明 Chips -->
<view class="muscle-tags">
<view class="tag primary" v-if="template.primaryMuscleNames && template.primaryMuscleNames.length">
<text class="tag-label">主要:</text>
<text class="tag-val">{{ template.primaryMuscleNames.join(', ') }}</text>
</view>
<!-- 用父容器包裹 v-else 分支,解决 key 冲突 -->
<view v-else>
<view v-for="(template, index) in filteredTemplateList" :key="index" class="template-item">
<image :src="template.urlCover" mode="aspectFill" class="template-img"></image>
<view>
<view class="template-content" @click="goToTemplateDetail(template)">
<view class="template-title">{{ template.name }}</view>
<!-- 空数据判断移到了这里 -->
<view class="template-count" v-if="template.primaryMuscles">
主要训练部位:{{ template.primaryMuscleNames.join(', ') || '无' }}
</view>
<view class="template-desc" v-if="template.secondaryMuscles">
次要训练部位:{{ template.secondaryMuscleNames.join(', ') || '无' }}
<view class="tag secondary"
v-if="template.secondaryMuscleNames && template.secondaryMuscleNames.length">
<text class="tag-label">次要:</text>
<text class="tag-val">{{ template.secondaryMuscleNames.join(', ') }}</text>
</view>
</view>
</view>
</view>
<!-- 空数据提示:移到 view-else 内部,且作为独立块 -->
<view v-if="filteredTemplateList.length === 0" class="empty-tip">
暂无符合条件的训练模板
<!-- 筛选无结果空状态 -->
<view v-if="filteredTemplateList.length === 0" class="empty-wrapper">
<up-empty mode="search" text="暂无符合条件的训练模板"></up-empty>
</view>
</template>
</view>
<!-- 底部安全区垫高 -->
<view class="bottom-spacer"></view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import TemplatesApi from '@/sheep/api/Template/Templates';
// 列表响应式数据
const templateList = ref([]);
const filteredTemplateList = ref([]);
// 2. 控制页面显示:false=显示大类,true=显示筛选后的模板
// 页面模式标记:false = 显示大类, true = 显示筛选后的细分模板
const isFiltering = ref(false);
const activeSceneId = ref('0');
// 下拉框部位筛选相关状态
// 部位筛选状态
const partList = ref([]);
const rawPartData = ref([]); // 存储接口返回的原始部位数据
const rawPartData = ref([]);
const showPartDropdown = ref(false);
const activePart = ref('部位');
const activePartId = ref('0'); // 新增:默认选中"不限",id=0
// 场景筛选相关状态
const activePartId = ref('0'); // '0' 代表不限
// 场景筛选状态
const sceneList = ref([
{ id: '0', title: '不限' },
{ id: '1', title: '健身房' },
{ id: '2', title: '仅哑铃' },
{ id: '3', title: '仅哑铃+杠铃' },
// { id: 4, title: '办公室' },
]);
const showSceneDropdown = ref(false);
const activeScene = ref('场景');
const activeSceneId = ref('0'); // '0' 代表不限
// ====== API 数据加载 ======
// 获取模板大类列表
const TemplatesList = async () => {
/**
* 获取模板大类列表
*/
const loadCategoryTemplates = async () => {
try {
const response = await TemplatesApi.getTemplates();
templateList.value = response.data;
console.log('模板大类接口返回数据', templateList.value)
templateList.value = response.data || [];
} catch (error) {
console.log('模板大类获取失败', error);
console.error('模板大类获取失败:', error);
}
};
// 获取所有细分锻炼部位(接口)
/**
* 获取所有细分锻炼部位
*/
const getPartCategories = async () => {
try {
const res = await TemplatesApi.getPartAllCategories();
if (res.code === 0) {
rawPartData.value = res.data;
console.log('部位分类接口返回数据rawPartData.value:', rawPartData.value)
// 下拉列表赋值
rawPartData.value = res.data || [];
partList.value = [
{ title: '不限', id: '0' }, // 给"不限"加id='0',方便后续筛选
...res.data.map(item => ({
title: item.name, // 接口返回的name作为显示标题
id: String(item.id) // 保存接口的id,用于后续筛选传参
}))
{ title: '不限', id: '0' },
...rawPartData.value.map((item) => ({
title: item.name,
id: String(item.id),
})),
];
}
} catch (error) {
console.log('部位列表获取失败', error);
console.error('部位列表获取失败:', error);
}
};
// 切换部位下拉菜单
// ====== 交互与筛选逻辑 ======
/**
* 切换部位下拉框
*/
const togglePartDropdown = () => {
showPartDropdown.value = !showPartDropdown.value;
showSceneDropdown.value = false;
};
// 切换场景下拉菜单
/**
* 切换场景下拉框
*/
const toggleSceneDropdown = () => {
showSceneDropdown.value = !showSceneDropdown.value;
showPartDropdown.value = false;
};
// 选择部位
/**
* 选择部位
*/
const selectPart = (item) => {
activePartId.value = item.id;
// 判断是否是不限id=0
if (item.id === '0') {
activePart.value = '部位';
} else {
activePart.value = item.title;
}
activePart.value = item.id === '0' ? '部位' : item.title;
showPartDropdown.value = false;
doFilter();
};
// 选择场景
/**
* 选择场景
*/
const selectScene = (item) => {
activeSceneId.value = item.id;
// 判断是否是不限id=0
if (item.id === '0') {
activeScene.value = '场景';
} else {
activeScene.value = item.title;
}
activeScene.value = item.id === '0' ? '场景' : item.title;
showSceneDropdown.value = false;
doFilter();
};
// 点击页面其他地方关闭所有下拉菜单
/**
* 关闭所有下拉菜单
*/
const closeAllDropdowns = () => {
showPartDropdown.value = false;
showSceneDropdown.value = false;
};
// 核心:执行筛选逻辑
/**
* 执行筛选逻辑
*/
const doFilter = async () => {
const musclesId = activePartId.value; // 对应接口 musclesId
const scene = activeSceneId.value; // 对应接口 scene
// 2. 判断:是否是【全部不限】
const musclesId = activePartId.value;
const scene = activeSceneId.value;
// 条件判断:若均为"不限",切回展示【大类列表】
if (musclesId === '0' && scene === '0') {
// 情况A:都不限 -> 恢复显示【模板大类】
isFiltering.value = false;
// 重置按钮文字为默认
activePart.value = '部位';
activeScene.value = '场景';
// 大类列表之前已经加载过了,直接显示
return;
}
// 情况B:有筛选条件 -> 请求【筛选接口】
// 存在有效条件:发请求获取筛选列表
try {
isFiltering.value = true; // 切换成模板列表模式
isFiltering.value = true;
const res = await TemplatesApi.queryTemplatesByCondition({
musclesId: musclesId,
scene: scene
musclesId,
scene,
});
if (res.code === 0) {
// 把接口返回的模板数据存起来,页面会自动刷新
filteredTemplateList.value = res.data;
console.log('筛选接口返回数据filteredTemplateList.value:', filteredTemplateList.value)
filteredTemplateList.value = res.data || [];
}
} catch (error) {
console.log('筛选失败', error);
// 失败了也清空数据
console.error('筛选模板失败:', error);
filteredTemplateList.value = [];
}
};
// 跳转到模板详情页,传入模板id
// 跳转地址是F:\hongxing-app\hongxing-new\pages4\pages\xunji\xunji-moban-xiangqing.vue
const goToTemplateDetail = (template) => {
// ====== 路由跳转 ======
/**
* 跳转至大类包含的模板列表
*/
const gototemplate = (item) => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`,
url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`,
});
};
const gototemplate = (item) => {
// 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4
/**
* 跳转至具体模板详情页
*/
const goToTemplateDetail = (template) => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban?id=${item.id}&isBigTemOffice=true`,
url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`,
});
};
onMounted(async () => {
await TemplatesList();
await getPartCategories();
console.log('进入模板大类详情');
await Promise.all([loadCategoryTemplates(), getPartCategories()]);
});
</script>
<style lang="scss" scoped>
.template-page {
width: 100vw;
height: 100vh;
box-sizing: border-box;
.filter-section {
position: fixed;
// top: 80rpx;
display: flex;
flex-direction: column;
height: calc(100vh - 44px);
// #ifdef MP-WEIXIN
top: 234rpx;
height: calc(100vh - 110px);
// #endif
width: 100vw;
background-color: #f8fafc;
overflow: hidden;
/* #ifdef H5 */
top: 77px;
/* #endif */
left: 0;
right: 0;
z-index: 999;
/* 顶部筛选控制栏 */
.filter-section {
flex-shrink: 0;
position: relative;
z-index: 50;
display: flex;
gap: 24rpx;
flex-wrap: wrap;
padding: 20rpx;
background-color: white;
gap: 20rpx;
padding: 20rpx 30rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f1f5f9;
.filter-wrapper {
position: relative;
.filter-btn {
display: flex;
align-items: center;
justify-content: center;
width: auto;
min-width: 70rpx;
height: 50rpx;
background: #fff;
color: #333;
font-size: 24rpx;
border: 2rpx solid #ddd;
border-radius: 25rpx;
padding: 0 20rpx;
white-space: nowrap;
gap: 10rpx;
height: 58rpx;
padding: 0 24rpx;
background-color: #f8fafc;
border: 1rpx solid #e2e8f0;
border-radius: 30rpx;
transition: all 0.2s ease;
.text {
font-size: 26rpx;
color: #334155;
font-weight: 500;
}
.filter-btn .text {
margin-right: 8rpx;
}
&.active,
&.selected {
background-color: #ecfdf5;
border-color: #10b981;
.filter-btn.active {
border-color: #43b05e;
background: #e6f7f0;
color: #43b05e;
.text {
color: #10b981;
font-weight: 600;
}
}
.filter-wrapper {
position: relative;
z-index: 10;
}
/* 下拉菜单样式 */
/* 下拉浮层 */
.dropdown-menu {
position: absolute;
top: calc(100% + 8rpx);
top: 72rpx;
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: 150rpx;
max-height: 320rpx;
min-width: 220rpx;
max-height: 380rpx;
background-color: #ffffff;
border-radius: 16rpx;
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid #f1f5f9;
padding: 8rpx 0;
overflow-y: auto;
border: 1rpx solid #f0f0f0;
}
/* 下拉菜单项样式 */
.dropdown-item {
padding: 16rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 28rpx;
font-size: 26rpx;
color: #333;
white-space: nowrap;
color: #334155;
transition: background-color 0.15s;
&:active {
background-color: #f8fafc;
}
/* 下拉菜单项悬停样式 */
.dropdown-item:hover {
background-color: #43b05e;
&.selected {
background-color: #f0fdf4;
color: #10b981;
font-weight: 600;
}
}
}
}
/* 下拉菜单项选中样式 */
.dropdown-item.selected {
background-color: #f0f9f4;
color: #2e9d5a;
font-weight: 500;
/* 遮罩蒙层 */
.dropdown-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 40;
background-color: rgba(0, 0, 0, 0.1);
}
}
.template-list {
margin-top: 120rpx;
padding: 0 30rpx;
box-sizing: border-box;
/* 列表自适应滚动容器 */
.list-container {
flex: 1;
display: flex;
flex-direction: column;
// #ifdef MP-WEIXIN
padding-bottom: 200rpx;
height: calc(100vh - 274rpx);
// #endif
height: 0;
/* 关键:接触 Flex 内容支撑限制 */
min-height: 0;
overflow: hidden;
/* #ifdef H5 */
margin-top: 87px;
// 关键修复:给H5固定高度,扣除顶部筛选栏77px + 自身margin-top87px
height: calc(100vh - 77px - 87px);
/* #endif */
.template-scroll {
width: 100%;
height: 100%;
.sub-template-list {
.scroll-inner {
padding: 24rpx 30rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
}
gap: 20rpx;
.template-item {
/* 模板卡片组件样式 */
.template-card {
display: flex;
background-color: white;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
align-items: center;
background-color: #ffffff;
border-radius: 20rpx;
padding: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
border: 1rpx solid #f1f5f9;
transition: transform 0.15s;
&:first-child {
margin-top: 24rpx;
&:active {
transform: scale(0.985);
background-color: #f8fafc;
}
.template-img {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
.cover-img {
width: 128rpx;
height: 128rpx;
border-radius: 16rpx;
margin-right: 20rpx;
background-color: #f1f5f9;
flex-shrink: 0;
}
.template-content {
.card-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
.title {
font-size: 30rpx;
font-weight: 600;
color: #0f172a;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
margin-right: 12rpx;
}
.template-title {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
.count-badge {
font-size: 22rpx;
color: #10b981;
background-color: #ecfdf5;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 500;
flex-shrink: 0;
}
}
.template-count {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
.title {
font-size: 30rpx;
font-weight: 600;
color: #0f172a;
margin-bottom: 8rpx;
}
.template-desc {
.desc {
font-size: 24rpx;
color: #666;
color: #64748b;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* 肌群 Tag 列表 */
.muscle-tags {
display: flex;
flex-direction: column;
gap: 6rpx;
margin-top: 4rpx;
.tag {
display: flex;
align-items: center;
font-size: 22rpx;
line-height: 1.3;
.tag-label {
color: #94a3b8;
flex-shrink: 0;
}
.tag-val {
color: #475569;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:last-child {
margin-bottom: 5rpx;
&.primary .tag-val {
color: #059669;
font-weight: 500;
}
}
}
}
}
.empty-wrapper {
padding: 100rpx 0;
}
}
/* #ifdef H5 */
margin-bottom: 30rpx;
/* #endif */
.bottom-spacer {
height: env(safe-area-inset-bottom);
padding-bottom: 40rpx;
}
}
}
... ...
<template>
<view class="calendar-page">
<!-- 顶部栏:年月选择器和功能按钮 -->
<!-- 顶部操作栏 -->
<view class="top-bar">
<view class="month-picker">
<uni-datetime-picker type="date" v-model="currentMonth" :clear-icon="false" />
<up-icon name="arrow-down" color="#333" size="15"></up-icon>
<!-- 年月选择胶囊 -->
<picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange">
<view class="month-picker-pill">
<text class="month-text">{{ displayMonthText }}</text>
<up-icon name="arrow-down-fill" color="#333333" size="10"></up-icon>
</view>
</picker>
<!-- 说明按钮 -->
<view class="action-buttons">
<button class="action-btn" @click="ShowExplainPopup = true">说明</button>
<up-button text="说明" type="info" plain shape="circle" size="mini"
customStyle="padding: 0 24rpx; height: 56rpx; font-size: 24rpx; border-color: #dcdfe6; color: #606266;"
@click="showExplainPopup = true" />
</view>
</view>
<!-- 日历网格 -->
<view class="calendar-grid">
<!-- 日历卡片容器 -->
<view class="calendar-card">
<!-- 星期表头 -->
<view class="week-header">
<text v-for="(day, index) in weekDays" :key="index" class="week-day">
{{ day }}
</text>
</view>
<!-- 日期网格 (动态 5行 或 6行) -->
<view class="date-grid">
<view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{ today: date.today }"
@tap="selectDate(date)">
<text v-if="!date.today" class="date-number">{{ date.day }}</text>
<text v-else class="today-text">今</text>
<view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{
'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0)
}" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)">
<!-- 日期数字 / 今日徽章 -->
<view class="date-header">
<text v-if="date.today" class="today-badge">今</text>
<text v-else class="date-number">
{{ date.day }}
</text>
</view>
<!-- 内容显示区域 -->
<!-- 计划/容量数据列表 -->
<view class="plan-list"
v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)">
<template v-for="(item, idx) in getVisibleItems(date).items" :key="idx">
<view class="plan-name" :class="{
'weight-item': item.type === 'weight',
'note-item': item.type === 'note'
}" :style="item.bg ? { backgroundColor: item.bg } : ''">
{{ item.text }}
<template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx">
<view class="plan-tag" :class="`tag-type-${item.type}`"
:style="item.bg ? { backgroundColor: item.bg } : {}">
<text class="tag-text">{{ item.text }}</text>
</view>
</template>
<!-- 统一只显示一个 + 剩余总数 -->
<text v-if="getVisibleItems(date).remaining > 0" class="plan-extra">
+{{ getVisibleItems(date).remaining }}
</text>
<!-- 溢出剩余条数 -->
<view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">
+{{ getCellVisibleItems(date).remaining }}
</view>
</view>
</view>
</view>
</view>
<!-- <view class="gridCellContent" v-if="ShowGridCellPopup && selectedDate">
<GridCellContentPopup :date="selectedDate" @close="ShowGridCellPopup = false" />
</view> -->
<!-- 单元格详情弹窗 -->
<GridCellContentPopup ref="gridPopupRef" :date="selectedDate" @refreshCalendar="handleRefreshCalendar" />
<!-- 日历说明弹窗 -->
<view class="explain-popup-overlay" v-if="ShowExplainPopup" @click.self="ShowExplainPopup = false">
<view class="explain-popup">
<!-- 日历图标说明弹窗 -->
<up-popup :show="showExplainPopup" mode="bottom" round="24rpx" :safeAreaInsetBottom="true"
@close="showExplainPopup = false">
<view class="explain-popup-content">
<view class="explain-header">
<text class="explain-title">日历说明</text>
<view class="close-btn" @click="ShowExplainPopup = false">
<text>×</text>
</view>
<text class="explain-title">日历图标说明</text>
<up-icon name="close" color="#909399" size="18" @click="showExplainPopup = false" />
</view>
<view class="explain-list">
<view class="explain-item">
<view class="color-block blue"></view>
<text class="item-label">分钟</text>
<text class="item-desc">在场馆的运动时长</text>
<view class="item-info">
<text class="item-label">时长</text>
<text class="item-desc">在场馆的实际运动时长</text>
</view>
</view>
<view class="explain-item">
<view class="color-block green"></view>
<view class="item-info">
<text class="item-label">容量</text>
<text class="item-desc">力量训练的总重量</text>
<text class="item-desc">力量训练累计产生的总负重重量</text>
</view>
</view>
<view class="explain-item">
<view class="color-block yellow"></view>
<view class="item-info">
<text class="item-label">课程</text>
<text class="item-desc">训记或已结课的线下课程</text>
<text class="item-desc">训记完成或已结课的线下课程</text>
</view>
</view>
<view class="explain-item">
<view class="color-block gray"></view>
<text class="item-label">课程</text>
<text class="item-desc">训记排课或待上课的线下课程</text>
<view class="item-info">
<text class="item-label">排课</text>
<text class="item-desc">训记排课计划或待上课的课程</text>
</view>
</view>
</view>
</view>
</up-popup>
</view>
</template>
<script setup>
import { ref, watch, computed, nextTick } from 'vue';
import dayjs from 'dayjs';
import DailyTemplateApi from '@/sheep/api/calendar/date';
import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue'
import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue';
// ====== 响应式数据 ======
// 常量定义
const MAX_CELL_LINES = 3;
const weekDays = ref(['一', '二', '三', '四', '五', '六', '日']);
// 响应式状态
const dates = ref([]);
const today = new Date();
const ShowExplainPopup = ref(false)
const showExplainPopup = ref(false);
const selectedDate = ref('');
// const ShowGridCellPopup = ref(false)
const gridPopupRef = ref(null);
// 自动获取当前年月
const now = new Date();
const currentMonth = ref(
`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
);
const currentMonth = ref(dayjs().format('YYYY-MM-DD'));
// ====== 工具函数 ======
const formatDateKey = (y, m, d) => {
return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
};
// 顶部年月显示
const displayMonthText = computed(() => {
return dayjs(currentMonth.value).format('YYYY年M月');
});
const arrayToDateStr = (arr) => {
if (!Array.isArray(arr) || arr.length !== 3) return null;
const [y, m, d] = arr;
return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
// 年月 Picker 触发
const onMonthChange = (e) => {
if (e.detail && e.detail.value) {
currentMonth.value = dayjs(e.detail.value).startOf('month').format('YYYY-MM-DD');
}
};
// ====== 生成日历数据 ======
const generateCalendar = (year, month) => {
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0).getDate();
let startWeekDay = firstDay.getDay();
const adjustedStart = startWeekDay === 0 ? 6 : startWeekDay - 1;
// ====== 计算每个日期格子的背景颜色 ======
const getCellBgColor = (date) => {
const calendarDates = [];
// 上月填充
const prevMonthLastDay = new Date(year, month - 1, 0).getDate();
for (let i = adjustedStart; i > 0; i--) {
const day = prevMonthLastDay - i + 1;
const y = month === 1 ? year - 1 : year;
const m = month === 1 ? 12 : month - 1;
calendarDates.push({
day,
today: false,
dateKey: formatDateKey(y, m, day),
});
// 2. 如果存在训练计划/备注/重量,优先采用训练本身的背景色
if (date.planList && date.planList.length > 0 && date.planList[0].bg) {
return date.planList[0].bg;
}
if (date.noteList && date.noteList.length > 0 && date.noteList[0].backgroundColor) {
return date.noteList[0].backgroundColor;
}
// 本月
for (let d = 1; d <= lastDay; d++) {
const isToday =
year === today.getFullYear() &&
month === today.getMonth() + 1 &&
d === today.getDate();
calendarDates.push({
day: d,
today: isToday,
dateKey: formatDateKey(year, month, d),
});
// 3. 有容量负重但无特定背景色:赋予柔和浅绿
if (date.weight && date.weight > 0) {
return '#f0fdf4';
}
// 下月填充
const remaining = 42 - calendarDates.length;
for (let d = 1; d <= remaining; d++) {
const y = month === 12 ? year + 1 : year;
const m = month === 12 ? 1 : month + 1;
// 4. 有计划但未指定背景色:赋予柔和浅黄
if (date.planList && date.planList.length > 0) {
return '#fefce8';
}
// 6. 普通无训练的本月休息日:基础卡片白色/浅灰
return '#ededed';
};
const generateCalendar = (monthStr) => {
const current = dayjs(monthStr);
const startOfMonth = current.startOf('month');
const daysInMonth = current.daysInMonth();
const dayOfWeek = startOfMonth.day(); // 0 表示周日
const startOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
// 动态计算总单元格:35(5行)或 42(6行)
const totalCellsNeeded = startOffset + daysInMonth;
const totalGridSize = totalCellsNeeded > 35 ? 42 : 35;
const gridStartDate = startOfMonth.subtract(startOffset, 'day');
const todayStr = dayjs().format('YYYY-MM-DD');
const calendarDates = [];
for (let i = 0; i < totalGridSize; i++) {
const cellDate = gridStartDate.add(i, 'day');
const dateKey = cellDate.format('YYYY-MM-DD');
calendarDates.push({
day: d,
today: false,
dateKey: formatDateKey(y, m, d),
day: cellDate.date(),
today: dateKey === todayStr,
isCurrentMonth: cellDate.month() === current.month(),
dateKey,
});
}
... ... @@ -168,26 +201,19 @@ const generateCalendar = (year, month) => {
};
// ====== 加载计划数据 ======
const loadPlans = async (year, month) => {
const loadPlans = async (monthStr) => {
try {
// 关键:获取 上个月、当前月、下个月 → 保证前后日期都有数据
const prevMonth = month - 1 < 1 ? 12 : month - 1;
const prevYear = month - 1 < 1 ? year - 1 : year;
const nextMonth = month + 1 > 12 ? 1 : month + 1;
const nextYear = month + 1 > 12 ? year + 1 : year;
// 并行请求三个月数据
// const [prevRes, currRes, nextRes] = await Promise.all([
// DailyTemplateApi.getCalendarPlans(prevYear, prevMonth),
// DailyTemplateApi.getCalendarPlans(year, month),
// DailyTemplateApi.getCalendarPlans(nextYear, nextMonth),
// ]);
const prevRes = await DailyTemplateApi.getCalendarPlans(prevYear, prevMonth);
const currRes = await DailyTemplateApi.getCalendarPlans(year, month);
const nextRes = await DailyTemplateApi.getCalendarPlans(nextYear, nextMonth);
// 合并三个月数据
const curr = dayjs(monthStr);
const prev = curr.subtract(1, 'month');
const next = curr.add(1, 'month');
// 并行请求 3 个月数据
const [prevRes, currRes, nextRes] = await Promise.all([
DailyTemplateApi.getCalendarPlans(prev.year(), prev.month() + 1).catch(() => ({ data: [] })),
DailyTemplateApi.getCalendarPlans(curr.year(), curr.month() + 1).catch(() => ({ data: [] })),
DailyTemplateApi.getCalendarPlans(next.year(), next.month() + 1).catch(() => ({ data: [] })),
]);
const allPlans = [
...(prevRes.data || []),
...(currRes.data || []),
... ... @@ -199,24 +225,25 @@ const loadPlans = async (year, month) => {
const weightMap = {};
allPlans.forEach((plan) => {
let dateStr;
let dateStr = null;
if (Array.isArray(plan.trainDate)) {
dateStr = arrayToDateStr(plan.trainDate);
const [y, m, d] = plan.trainDate;
dateStr = dayjs(`${y}-${m}-${d}`).format('YYYY-MM-DD');
} else if (typeof plan.trainDate === 'string') {
dateStr = plan.trainDate;
dateStr = dayjs(plan.trainDate).format('YYYY-MM-DD');
}
if (!dateStr) return;
if (!dateStr || dateStr === 'Invalid Date') return;
// 处理多条备注
// 备注处理
if (Array.isArray(plan.notes) && plan.notes.length > 0) {
noteMap[dateStr] = plan.notes.map(note => ({
noteMap[dateStr] = plan.notes.map((note) => ({
content: note.content || '',
backgroundColor: note.backgroundColor || '#ccc'
backgroundColor: note.backgroundColor || '#e2e8f0',
}));
}
// 处理计划和重量
// 训练计划与容量处理
planMap[dateStr] = planMap[dateStr] || [];
if (Array.isArray(plan.templates)) {
let totalWeightForDay = 0;
... ... @@ -224,7 +251,7 @@ const loadPlans = async (year, month) => {
if (template && template.name) {
planMap[dateStr].push({
name: template.name,
bg: template.backgroundColor || '#ffdd44'
bg: template.backgroundColor || '#fef08a',
});
}
if (template && typeof template.totalWeight === 'number') {
... ... @@ -237,57 +264,37 @@ const loadPlans = async (year, month) => {
}
});
const newDates = generateCalendar(year, month).map((date) => ({
dates.value = generateCalendar(monthStr).map((date) => ({
...date,
planList: planMap[date.dateKey] || [],
noteList: noteMap[date.dateKey] || [],
weight: weightMap[date.dateKey] || null,
}));
dates.value = newDates;
} catch (err) {
console.error('加载日历失败:', err);
const [y, m] = [
parseInt(currentMonth.value.split('-')[0]),
parseInt(currentMonth.value.split('-')[1]),
];
dates.value = generateCalendar(y, m);
console.error('加载日历数据失败:', err);
dates.value = generateCalendar(monthStr);
}
console.log('日历主页面处理的完的是数据:dates', dates);
};
const selectedDateData = computed(() => {
return dates.value.find(item => item.dateKey === selectedDate.value) || null;
});
// ====================== 核心显示逻辑(已修复)======================
const maxLines = 3; // 一个格子最多显示3行
const getVisibleItems = (date) => {
// ====== 视图格子显示控制 ======
const getCellVisibleItems = (date) => {
const items = [];
let remaining = 0;
// 1. 重量优先显示
if (date.weight && date.weight > 0) {
items.push({ type: 'weight', text: `${date.weight}kg` });
}
// 2. 显示训练计划(已修改)
for (const plan of date.planList) {
if (items.length < maxLines) {
items.push({
type: 'plan',
text: plan.name,
bg: plan.bg
});
if (items.length < MAX_CELL_LINES) {
items.push({ type: 'plan', text: plan.name, bg: plan.bg });
} else {
remaining++;
}
}
// 3. 显示备注
for (const note of date.noteList) {
if (items.length < maxLines) {
if (items.length < MAX_CELL_LINES) {
items.push({ type: 'note', text: note.content, bg: note.backgroundColor });
} else {
remaining++;
... ... @@ -297,153 +304,79 @@ const getVisibleItems = (date) => {
return { items, remaining };
};
// 接收子组件通知,刷新整个日历数据
const handleRefreshCalendar = () => {
const [year, month] = currentMonth.value.split('-').map(Number);
loadPlans(year, month);
loadPlans(currentMonth.value);
};
// ====================================================================
// ====== 监听月份变化 ======
watch(
currentMonth,
(newVal) => {
const [year, month] = newVal.split('-').map(Number);
loadPlans(year, month);
if (newVal) {
loadPlans(newVal);
}
},
{ immediate: true },
{ immediate: true }
);
const selectDate = async (date) => {
selectedDate.value = date.dateKey;
// ShowGridCellPopup.value = true;
await nextTick()
await nextTick();
gridPopupRef.value?.openPopup();
};
</script>
<style lang="scss" scoped>
.calendar-page {
height: 100%;
padding: 40rpx;
background-color: white;
padding: 30rpx 15rpx 20rpx;
background-color: #fff;
box-sizing: border-box;
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
margin-top: 35rpx;
margin-bottom: 24rpx;
.month-picker {
height: 40rpx;
border-radius: 50rpx;
overflow: hidden;
padding: 0 20rpx;
gap: 10rpx;
:deep(.uni-date-x--border) {
border: none !important;
}
:deep(.uni-date__x-input) {
height: 40rpx;
line-height: 40rpx;
padding: 0 10rpx;
}
// 微信小程序端样式适配
// #ifdef MP-WEIXIN
.month-picker-pill {
display: flex;
align-items: center;
justify-content: center;
min-width: 200rpx;
box-sizing: border-box;
:deep(.uni-date-x) {
height: 35rpx;
// min-height: 40rpx;
border-radius: 50rpx;
background-color: transparent;
padding: 0;
margin: 0;
border: none !important;
}
:deep(.uni-date-editor--x) {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
border: none !important;
}
:deep(.uni-date__x-input) {
// height: 40rpx;
// line-height: 40rpx;
font-size: 24rpx;
color: #333;
padding: 0 10rpx;
text-align: center;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.icon-calendar) {
display: none !important;
}
// #endif
gap: 12rpx;
background-color: #ececec;
padding: 12rpx 28rpx;
border-radius: 40rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #fff;
.month-text {
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
}
}
.action-buttons {
display: flex;
justify-content: flex-start;
gap: 10rpx;
// .action-btn {
// padding: 20rpx 20rpx;
// margin: 0;
// line-height: 1;
// border-radius: 20rpx;
// border: 1px solid #666;
// font-size: 20rpx;
// }
:deep(uni-button),
:deep(wx-button) {
padding: 10rpx 20rpx;
margin: 0;
line-height: 1;
border-radius: 20rpx;
border: 1px solid #666;
font-size: 20rpx;
align-items: center;
}
}
.calendar-grid {
padding-bottom: 120rpx;
// #ifdef MP-WEIXIN
padding-bottom: 140rpx;
// #endif
.calendar-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
.week-header {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
margin-bottom: 20rpx;
.week-day {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
font-size: 26rpx;
font-weight: 550;
}
}
... ... @@ -453,19 +386,55 @@ const selectDate = async (date) => {
gap: 10rpx;
}
/* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
.date-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border: 1px solid #eee;
font-size: 30rpx;
color: #333;
background-color: #f9fafc;
padding-top: 10rpx;
border-radius: 12rpx;
height: 200rpx;
overflow: hidden;
padding: 8rpx 4rpx;
box-sizing: border-box;
transition: all 0.2s ease;
&:active {
filter: brightness(0.96);
}
&.other-month {
opacity: 0.5;
border-color: #f1f5f9;
}
&.is-today {
border: 2rpx solid #3b82f6;
}
&.has-data {
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);
}
.date-header {
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6rpx;
.date-number {
font-size: 26rpx;
font-weight: 600;
color: #1f2937;
}
.today-badge {
font-weight: 600;
}
}
.plan-list {
... ... @@ -473,159 +442,126 @@ const selectDate = async (date) => {
display: flex;
flex-direction: column;
gap: 6rpx;
margin-top: 10rpx;
padding: 0;
.plan-tag {
width: 100%;
border-radius: 6rpx;
padding: 2rpx 4rpx;
box-sizing: border-box;
}
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.85);
/* 使 Tag 悬浮于彩色格子之上 */
.weight,
.plan-extra,
.note-item,
.plan-name {
.tag-text {
font-size: 18rpx;
background: #ffdd44;
padding: 4rpx 0;
text-align: center;
line-height: 22rpx;
color: #111827;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
width: 100%;
box-sizing: border-box;
}
.weight,
.weight-item {
background: #b8ff66;
&.tag-type-weight {
background-color: #bbf7d0 !important;
.tag-text {
color: #14532d;
font-weight: 700;
}
}
}
/* 日历说明弹窗样式 */
.explain-popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 999;
&.tag-type-note {
background-color: #ffffff;
.explain-popup {
width: 100%;
.tag-text {
color: #374151;
}
}
}
.plan-extra {
font-size: 16rpx;
color: #4b5563;
text-align: center;
line-height: 20rpx;
font-weight: 600;
}
}
}
}
}
.explain-popup-content {
padding: 36rpx 32rpx 48rpx 32rpx;
background-color: #ffffff;
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx;
box-sizing: border-box;
.explain-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
margin-bottom: 32rpx;
.explain-title {
font-size: 32rpx;
font-weight: 60;
color: #333333;
}
.close-btn {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
text {
font-size: 36rpx;
color: #666666;
line-height: 1;
}
font-weight: 600;
color: #111827;
}
}
.explain-list {
display: flex;
flex-direction: column;
gap: 28rpx;
.explain-item {
display: flex;
align-items: center;
margin-bottom: 32rpx;
.color-block {
width: 40rpx;
height: 40rpx;
width: 36rpx;
height: 36rpx;
border-radius: 8rpx;
margin-right: 20rpx;
margin-right: 24rpx;
&.blue {
background-color: #a8d8ff;
background-color: #bae6fd;
}
&.green {
background-color: #b8ff66;
background-color: #bbf7d0;
}
&.yellow {
background-color: #ffdd44;
background-color: #fef08a;
}
&.gray {
background-color: #f0f0f0;
border: 1rpx solid #eee;
background-color: #e5e7eb;
}
}
.item-info {
display: flex;
align-items: center;
gap: 16rpx;
.item-label {
font-size: 28rpx;
color: #333333;
width: 80rpx;
margin-right: 20rpx;
font-weight: 600;
color: #1f2937;
width: 72rpx;
}
.item-desc {
font-size: 28rpx;
color: #666666;
flex: 1;
}
font-size: 26rpx;
color: #6b7280;
}
}
}
}
// .gridCellContent {
// position: fixed;
// top: 0;
// left: 0;
// right: 0;
// bottom: 0;
// background: rgba(0, 0, 0, 0.5);
// z-index: 9999;
// display: flex;
// align-items: flex-end;
// padding: 0 !important;
// margin: 0 !important;
// width: 100vw !important;
// box-sizing: border-box;
// }
</style>
<!-- #ifdef MP-WEIXIN -->
<style lang="scss">
.month-picker {
.uni-date-editor--x.uni-date-x--border,
.uni-date-x--border {
border: none !important;
}
.uni-date-x,
.uni-date-editor--x {
border: none !important;
}
}
</style>
\ No newline at end of file
<!-- #endif -->
\ No newline at end of file
... ...