|
|
|
<template>
|
|
|
|
<view class="training-data-page">
|
|
|
|
<timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />
|
|
|
|
|
|
|
|
<!-- 过渡动画 -->
|
|
|
|
<up-transition :show="true" mode="slide-right" duration="300">
|
|
|
|
<view class="content-scroll">
|
|
|
|
<!-- 肌肉部位数据区域 -->
|
|
|
|
<view class="section">
|
|
|
|
<view class="muscle-data">
|
|
|
|
<view class="title">主训练肌肉数据</view>
|
|
|
|
<view class="tabs">
|
|
|
|
<view class="tab" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">容量</view>
|
|
|
|
<view class="tab" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">组数</view>
|
|
|
|
</view>
|
|
|
|
<view class="tip">{{ tabTip[partTabIndex] }}</view>
|
|
|
|
|
|
|
|
<!-- 网格布局:一行4格 + 正方形 + 总计自适应 -->
|
|
|
|
<view class="detail-part" :style="`--remain-span: ${remainSpan}`">
|
|
|
|
<view class="part-item" v-for="item in detailPartOptions" :key="item.id"
|
|
|
|
@click="openMuscleDetail(item.id)">
|
|
|
|
<text class=" txt">{{ item.name }}</text>
|
|
|
|
<text class="num">{{ item.value }}</text>
|
|
|
|
<!-- 顶部时间范围选择器 -->
|
|
|
|
<view class="top-time-picker">
|
|
|
|
<timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 主体内容可滚动区域 -->
|
|
|
|
<view class="main-content">
|
|
|
|
<scroll-view scroll-y class="scroll-area" :show-scrollbar="false">
|
|
|
|
<up-transition :show="true" mode="slide-right" duration="300">
|
|
|
|
<view class="scroll-inner">
|
|
|
|
<!-- 1. 肌肉部位数据区域 -->
|
|
|
|
<view class="card-section">
|
|
|
|
<view class="section-header">
|
|
|
|
<text class="title">主训练肌肉数据</text>
|
|
|
|
</view>
|
|
|
|
<view class="part-item total-item">
|
|
|
|
<text class="txt">总计</text>
|
|
|
|
<text class="num">{{ totalPartValue }}</text>
|
|
|
|
|
|
|
|
<!-- 容量 / 组数 切换标签 -->
|
|
|
|
<view class="tab-group">
|
|
|
|
<view class="tab-item" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">
|
|
|
|
容量
|
|
|
|
</view>
|
|
|
|
<view class="tab-item" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">
|
|
|
|
组数
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<text class="tip-text">{{ TREND_TIPS[partTabIndex] }}</text>
|
|
|
|
|
|
|
|
<!-- 肌肉部位网格(1行4格 + 正方形 + 总计自适应占格) -->
|
|
|
|
<view class="muscle-grid">
|
|
|
|
<view class="grid-item" v-for="item in detailPartOptions" :key="item.id"
|
|
|
|
@click="openMuscleDetail(item.id)">
|
|
|
|
<text class="item-name">{{ item.name }}</text>
|
|
|
|
<text class="item-value">{{ item.value }}</text>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 趋势图表区域 -->
|
|
|
|
<view class="section">
|
|
|
|
<view class="header">
|
|
|
|
<view class="title">趋势</view>
|
|
|
|
<view class="mean">
|
|
|
|
<up-select v-model:current="partId" :label="partOptions.find(item => item.id === partId)?.name || '全部'"
|
|
|
|
:options="partOptions" @select="onPartChange"></up-select>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="tabs">
|
|
|
|
<view class="tab" v-for="item in trendTabs" :key="item.id" :class="{ active: NumberTabIndex === item.id }"
|
|
|
|
@click="NumberTabIndex = item.id">
|
|
|
|
{{ item.name }}
|
|
|
|
<!-- 总计格子 -->
|
|
|
|
<view class="grid-item total-item" :style="{ gridColumn: `span ${remainSpan}` }">
|
|
|
|
<text class="item-name">总计</text>
|
|
|
|
<text class="item-value highlight">{{ totalPartValue }}</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="tip">{{ tabTip[NumberTabIndex] }}</view>
|
|
|
|
|
|
|
|
<view class="statistical">
|
|
|
|
<template v-if="!showLastPeriod">
|
|
|
|
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
|
|
|
|
chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
|
|
|
|
chartType="line" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
|
|
|
|
</template>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<view class="chose">
|
|
|
|
<checkbox :checked="showLastPeriod" @click="toggleLastWeek" />
|
|
|
|
{{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 数据对比 -->
|
|
|
|
<view class="compare">
|
|
|
|
<view class="header-row">
|
|
|
|
<text class="title">对比</text>
|
|
|
|
<view class="legend-group">
|
|
|
|
<view class="legend-item">
|
|
|
|
<view class="dot blue"></view>
|
|
|
|
<text>{{ MainCurrentDateType === 'week' ? '上周' : '上月' }}</text>
|
|
|
|
</view>
|
|
|
|
<view class="legend-item">
|
|
|
|
<view class="dot yellow"></view>
|
|
|
|
<text>{{ MainCurrentDateType === 'week' ? '本周' : '本月' }}</text>
|
|
|
|
<!-- 2. 趋势图表区域 -->
|
|
|
|
<view class="card-section">
|
|
|
|
<view class="section-header space-between">
|
|
|
|
<text class="title">趋势</text>
|
|
|
|
<view class="part-select-wrapper">
|
|
|
|
<up-select v-model:current="partId"
|
|
|
|
:label="partOptions.find((item) => item.id === partId)?.name || '全部'" :options="partOptions"
|
|
|
|
@select="onPartChange"></up-select>
|
|
|
|
</view>
|
|
|
|
<view class="legend-item">
|
|
|
|
<text>对比</text>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 趋势维度切换 Tabs -->
|
|
|
|
<view class="tab-group flex-wrap">
|
|
|
|
<view class="tab-item" v-for="item in trendTabs" :key="item.id"
|
|
|
|
:class="{ active: NumberTabIndex === item.id }" @click="NumberTabIndex = item.id">
|
|
|
|
{{ item.name }}
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="compare-card">
|
|
|
|
<view class="data-list">
|
|
|
|
<view v-for="(item, index) in listData" :key="index" class="data-row">
|
|
|
|
<view class="label">{{ item.label }}</view>
|
|
|
|
<view class="value">{{ item.lastPeriod }}</view>
|
|
|
|
<view class="value">{{ item.currPeriod }}</view>
|
|
|
|
<view class="value diff" :class="{ negative: item.diff < 0 }">
|
|
|
|
{{ item.diff > 0 ? '+' + item.diff : item.diff }}
|
|
|
|
<text class="tip-text">{{ TREND_TIPS[NumberTabIndex] }}</text>
|
|
|
|
|
|
|
|
<!-- 统计图表(根据 showLastPeriod 切换 柱状图 / 折线图) -->
|
|
|
|
<view class="chart-container">
|
|
|
|
<template v-if="!showLastPeriod">
|
|
|
|
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
|
|
|
|
chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
|
|
|
|
chartType="line" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
|
|
|
|
</template>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 显示上周/上月数据勾选框 -->
|
|
|
|
<view class="toggle-checkbox" @click="toggleLastWeek">
|
|
|
|
<checkbox :checked="showLastPeriod" color="#10b981" style="transform: scale(0.8);" />
|
|
|
|
<text class="toggle-label">
|
|
|
|
{{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
|
|
|
|
</text>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 3. 数据对比区域 -->
|
|
|
|
<view class="compare-section">
|
|
|
|
<view class="compare-header">
|
|
|
|
<text class="sub-title">对比</text>
|
|
|
|
<view class="legend-group">
|
|
|
|
<view class="legend-item">
|
|
|
|
<view class="dot blue"></view>
|
|
|
|
<text>{{ MainCurrentDateType === 'week' ? '上周' : '上月' }}</text>
|
|
|
|
</view>
|
|
|
|
<view class="legend-item">
|
|
|
|
<view class="dot yellow"></view>
|
|
|
|
<text>{{ MainCurrentDateType === 'week' ? '本周' : '本月' }}</text>
|
|
|
|
</view>
|
|
|
|
<view class="legend-item">
|
|
|
|
<text>对比</text>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 对比数据卡片 -->
|
|
|
|
<view class="compare-card">
|
|
|
|
<view class="data-list">
|
|
|
|
<view v-for="(item, index) in listData" :key="index" class="data-row">
|
|
|
|
<view class="label">{{ item.label }}</view>
|
|
|
|
<view class="value text-secondary">{{ item.lastPeriod }}</view>
|
|
|
|
<view class="value text-main">{{ item.currPeriod }}</view>
|
|
|
|
<view class="value diff" :class="{ positive: item.diff > 0, negative: item.diff < 0 }">
|
|
|
|
{{ item.diff > 0 ? '+' + item.diff : item.diff }}
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</up-transition>
|
|
|
|
</up-transition>
|
|
|
|
|
|
|
|
<!-- 底部安全区域间距 -->
|
|
|
|
<view class="bottom-spacer"></view>
|
|
|
|
</scroll-view>
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 肌肉详情弹窗 -->
|
|
|
|
<!-- <up-popup :show="muscleShow" mode="top" @close="muscleShow = false" safeAreaInsetTop>
|
|
|
|
<view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }">
|
|
|
|
<timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />
|
|
|
|
<view class="statistical">
|
|
|
|
<LineChart v-if="muscleShow" @close="closeMuscleDetail" :categories="muscleChartCategories" :series="[
|
|
|
|
{ name: '容量', data: muscleVolumeArr },
|
|
|
|
{ name: '组数', data: muscleSetsArr }
|
|
|
|
]" chartType="column" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</up-popup> -->
|
|
|
|
<up-popup :show="muscleShow" mode="top" @close="closeMuscleDetail" safeAreaInsetTop>
|
|
|
|
<up-popup :show="muscleShow" mode="top" round="24rpx" @close="closeMuscleDetail" safeAreaInsetTop>
|
|
|
|
<view class="muscleShow-popup" :style="{ paddingTop: statusBarHeight + 'px' }">
|
|
|
|
<timeSelect @dateTypeChange="handleTypeChange" @dateRangeChange="handleDateChange" />
|
|
|
|
|
|
|
|
<!-- 肌肉名称 -->
|
|
|
|
<view class="muscle-name">
|
|
|
|
{{ currentMuscleName }}
|
|
|
|
</view>
|
|
|
|
<view class="popup-body">
|
|
|
|
<!-- 肌肉名称 -->
|
|
|
|
<view class="muscle-name">
|
|
|
|
{{ currentMuscleName }}
|
|
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 容量/组数 切换标签 -->
|
|
|
|
<view class="muscle-tabs">
|
|
|
|
<view class="tab" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0">容量</view>
|
|
|
|
<view class="tab" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1">组数</view>
|
|
|
|
</view>
|
|
|
|
<view class="tip">{{ tabTip[muscleTabIndex] }}</view>
|
|
|
|
|
|
|
|
<!-- 图表区域 -->
|
|
|
|
<view class="statistical">
|
|
|
|
<!-- <LineChart v-if="muscleShow" :categories="muscleChartCategories" :series="muscleChartSeries"
|
|
|
|
chartType="column" /> -->
|
|
|
|
<LineChart v-if="muscleShow && muscleChartCategories.length"
|
|
|
|
:key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`"
|
|
|
|
:categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true"
|
|
|
|
:extra="{ column: { width: muscleChartColumnWidth } }" />
|
|
|
|
<!-- 容量/组数 切换标签 -->
|
|
|
|
<view class="muscle-tabs">
|
|
|
|
<view class="tab-item" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0">
|
|
|
|
容量
|
|
|
|
</view>
|
|
|
|
<view class="tab-item" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1">
|
|
|
|
组数
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
<view class="tip">{{ TREND_TIPS[muscleTabIndex] }}</view>
|
|
|
|
|
|
|
|
<!-- 图表区域 -->
|
|
|
|
<view class="statistical">
|
|
|
|
<LineChart v-if="muscleShow && muscleChartCategories.length"
|
|
|
|
:key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`"
|
|
|
|
:categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true"
|
|
|
|
:extra="{ column: { width: muscleChartColumnWidth } }" />
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</view>
|
|
|
|
</up-popup>
|
|
|
|
|
|
|
|
</view>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
import LineChart from './LineChart.vue';
|
|
|
|
import { ref, computed, onMounted } from 'vue';
|
|
|
|
import timeSelect from './time-select.vue';
|
|
...
|
...
|
@@ -149,303 +169,263 @@ import { getMenuButtonHeight } from '@/utils/safeArea'; |
|
|
|
import TrainingDataApi from '@/sheep/api/TrainingData/Data.js';
|
|
|
|
import LeftmotionApi from '@/sheep/api/motion/equipments';
|
|
|
|
|
|
|
|
const statusBarHeight = getMenuButtonHeight();
|
|
|
|
// ==================== 常量 ====================
|
|
|
|
|
|
|
|
// 时间范围
|
|
|
|
const startDate = ref('')
|
|
|
|
const endDate = ref('')
|
|
|
|
const WEEKDAY_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
|
|
|
|
|
|
|
// 肌肉弹窗
|
|
|
|
// 弹窗的容量/组数切换索引
|
|
|
|
const muscleTabIndex = ref(0)
|
|
|
|
const muscleShow = ref(false);
|
|
|
|
const currentMuscleName = ref('')
|
|
|
|
const selectLabel = ref('全部')
|
|
|
|
const partId = ref('');
|
|
|
|
const partOptions = ref([]);
|
|
|
|
|
|
|
|
const weeklyData = ref(null);
|
|
|
|
|
|
|
|
// 加载锁
|
|
|
|
const weeklyLoading = ref(false);
|
|
|
|
const trainingLoading = ref(false);
|
|
|
|
const muscleListLoading = ref(false);
|
|
|
|
/** 趋势维度文案 */
|
|
|
|
const TREND_DATA_KEYS = ['volume', 'setCount', 'duration', 'distance', 'count'];
|
|
|
|
|
|
|
|
// 时间类型 week / month
|
|
|
|
const MainCurrentDateType = ref('week');
|
|
|
|
|
|
|
|
// 底部提示文案
|
|
|
|
const tabTip = ref([
|
|
|
|
const TREND_TIPS = [
|
|
|
|
'重量X组数X次数,用来评估训练的整体强度(kg)',
|
|
|
|
'该时间段内训练组数趋势(组)',
|
|
|
|
'累计计时类动作的时长(min)',
|
|
|
|
'累计距离类动作的距离(km)',
|
|
|
|
'累计次数类动作的次数(次)',
|
|
|
|
]);
|
|
|
|
];
|
|
|
|
|
|
|
|
// ==================== 日期工具函数 ====================
|
|
|
|
|
|
|
|
/** 格式化日期为 YYYY-MM-DD */
|
|
|
|
const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD');
|
|
|
|
|
|
|
|
/** 获取包含指定日期的完整周日期列表(周一~周日),返回 dayjs 实例数组 */
|
|
|
|
const getWeekFullDates = (baseDateStr) => {
|
|
|
|
const d = dayjs(baseDateStr);
|
|
|
|
const dayOfWeek = d.day(); // 0=周日, 1=周一, ...
|
|
|
|
const monday = d.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day').startOf('day');
|
|
|
|
return Array.from({ length: 7 }, (_, i) => monday.add(i, 'day'));
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 获取指定日期所在月份的全部日期列表,返回 dayjs 实例数组 */
|
|
|
|
const getMonthFullDates = (baseDateStr) => {
|
|
|
|
const d = dayjs(baseDateStr);
|
|
|
|
const daysInMonth = d.daysInMonth();
|
|
|
|
return Array.from({ length: daysInMonth }, (_, i) =>
|
|
|
|
d.date(i + 1).startOf('day')
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 解析 API 返回的日期数组 [year, month, day] 为 dayjs 实例 */
|
|
|
|
const parseApiDate = (apiDateArr) => {
|
|
|
|
if (!Array.isArray(apiDateArr) || apiDateArr.length !== 3) return null;
|
|
|
|
const [year, month, day] = apiDateArr;
|
|
|
|
return dayjs(`${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 趋势推荐维度按周/月数据加载 */
|
|
|
|
const loadCategoryData = (date) => {
|
|
|
|
const loader = MainCurrentDateType.value === 'week'
|
|
|
|
? loadWeeklyByCategory
|
|
|
|
: loadMonthlyByCategory;
|
|
|
|
return loader(date);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 肌肉详情按周/月数据加载 */
|
|
|
|
const loadMuscleData = (muscleId, date) => {
|
|
|
|
const isWeekly = MainCurrentDateType.value === 'week';
|
|
|
|
return loadMuscleStats(muscleId, date, isWeekly);
|
|
|
|
};
|
|
|
|
|
|
|
|
// ==================== 响应式状态 ====================
|
|
|
|
|
|
|
|
// 趋势标签
|
|
|
|
const NumberTabIndex = ref(0)
|
|
|
|
const statusBarHeight = getMenuButtonHeight();
|
|
|
|
|
|
|
|
// --- 时间范围 ---
|
|
|
|
const startDate = ref('');
|
|
|
|
const endDate = ref('');
|
|
|
|
const MainCurrentDateType = ref('week'); // 'week' | 'month'
|
|
|
|
|
|
|
|
// --- 筛选状态 ---
|
|
|
|
const partId = ref('');
|
|
|
|
const partOptions = ref([]);
|
|
|
|
const showLastPeriod = ref(false);
|
|
|
|
|
|
|
|
// --- 趋势 Tab 状态 ---
|
|
|
|
const NumberTabIndex = ref(0);
|
|
|
|
const trendTabs = ref([
|
|
|
|
{ id: 0, name: '容量' },
|
|
|
|
{ id: 1, name: '组数' },
|
|
|
|
{ id: 2, name: '时长' },
|
|
|
|
{ id: 3, name: '距离' },
|
|
|
|
{ id: 4, name: '次数' },
|
|
|
|
])
|
|
|
|
]);
|
|
|
|
|
|
|
|
// 肌肉部位标签
|
|
|
|
// --- 肌肉部位 Tab ---
|
|
|
|
const partTabIndex = ref(0);
|
|
|
|
|
|
|
|
// 核心:格式化肌肉部位数据,支持容量/组数切换
|
|
|
|
const detailPartOptions = computed(() => {
|
|
|
|
// 没有肌肉数据时返回空数组
|
|
|
|
if (!subCategorieList.value.length) return []
|
|
|
|
|
|
|
|
// 根据当前选中的标签,决定取哪个数据
|
|
|
|
const isVolume = partTabIndex.value === 0 // 0是容量,1是组数
|
|
|
|
const dataMap = isVolume ? trainingData.value.totalVolumeMap : trainingData.value.setCountMap
|
|
|
|
|
|
|
|
// 把 subCategorieList 里的每一项,加上对应的数据
|
|
|
|
return subCategorieList.value.map(item => {
|
|
|
|
return {
|
|
|
|
id: item.id,
|
|
|
|
name: item.name,
|
|
|
|
// 从 dataMap 里取数据,没有的话默认0
|
|
|
|
value: dataMap[item.id] ?? 0
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
const totalPartValue = computed(() => {
|
|
|
|
return detailPartOptions.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0)
|
|
|
|
})
|
|
|
|
|
|
|
|
// 弹窗用的完整日期列表(周/月模式下的所有日期)
|
|
|
|
const muscleFullDates = ref([])
|
|
|
|
const muscleProcessedData = ref({
|
|
|
|
volumeArr: [],
|
|
|
|
setsArr: []
|
|
|
|
})
|
|
|
|
|
|
|
|
// 核心:计算最后一行剩余格子数,用于总计自适应占格
|
|
|
|
const itemTotal = computed(() => detailPartOptions.value.length)
|
|
|
|
const remainSpan = computed(() => {
|
|
|
|
const mod = itemTotal.value % 4
|
|
|
|
return mod === 0 ? 4 : 4 - mod
|
|
|
|
})
|
|
|
|
// --- 加载锁 ---
|
|
|
|
const weeklyLoading = ref(false);
|
|
|
|
const trainingLoading = ref(false);
|
|
|
|
const muscleListLoading = ref(false);
|
|
|
|
|
|
|
|
const showLastPeriod = ref(false);
|
|
|
|
// --- 数据 ---
|
|
|
|
const weeklyData = ref(null);
|
|
|
|
const subCategorieList = ref([]);
|
|
|
|
const trainingData = ref({ setCountMap: {}, totalVolumeMap: {} });
|
|
|
|
|
|
|
|
// 周/月 图表类目 & 数据
|
|
|
|
const weekCategories = ref(['周一', '周二', '周三', '周四', '周五', '周六', '周日']);
|
|
|
|
// --- 肌肉弹窗 ---
|
|
|
|
const muscleShow = ref(false);
|
|
|
|
const muscleTabIndex = ref(0);
|
|
|
|
const currentMuscleId = ref(null);
|
|
|
|
const currentMuscleName = ref('');
|
|
|
|
const muscleDetailData = ref({ muscleId: 0, dailyStatsList: [] });
|
|
|
|
const muscleFullDates = ref([]);
|
|
|
|
const muscleProcessedData = ref({ volumeArr: [], setsArr: [] });
|
|
|
|
|
|
|
|
const lastMonthData = ref(Array(31).fill(0));
|
|
|
|
// ==================== 计算属性 ====================
|
|
|
|
|
|
|
|
// ✅ 修复版:强制生成完整的周/月日期,就算没数据也要显示横轴
|
|
|
|
const chartCategories = computed(() => {
|
|
|
|
if (!weeklyData.value) return []
|
|
|
|
/** 肌肉部位数据列表(支持容量/组数切换) */
|
|
|
|
const detailPartOptions = computed(() => {
|
|
|
|
if (!subCategorieList.value.length) return [];
|
|
|
|
|
|
|
|
const isVolume = partTabIndex.value === 0;
|
|
|
|
const dataMap = isVolume
|
|
|
|
? trainingData.value.totalVolumeMap
|
|
|
|
: trainingData.value.setCountMap;
|
|
|
|
|
|
|
|
return subCategorieList.value.map((item) => ({
|
|
|
|
id: item.id,
|
|
|
|
name: item.name,
|
|
|
|
value: dataMap[item.id] ?? 0,
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
// 周模式:生成周一到周日的完整日期(月/日格式)
|
|
|
|
const weekDates = getWeekFullDates(startDate.value).map(d => {
|
|
|
|
const month = d.getMonth() + 1
|
|
|
|
const day = d.getDate()
|
|
|
|
return `${month}/${day}`
|
|
|
|
})
|
|
|
|
return weekDates
|
|
|
|
} else {
|
|
|
|
// 月模式:生成当月1日到月底的完整日期(纯日期格式)
|
|
|
|
const monthDates = getMonthFullDates(startDate.value).map(d => {
|
|
|
|
return `${d.getDate()}`
|
|
|
|
})
|
|
|
|
return monthDates
|
|
|
|
}
|
|
|
|
})
|
|
|
|
/** 肌肉部位总计 */
|
|
|
|
const totalPartValue = computed(() =>
|
|
|
|
detailPartOptions.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0)
|
|
|
|
);
|
|
|
|
|
|
|
|
// 趋势图表动态柱子宽度:周20,月3
|
|
|
|
const trendChartColumnWidth = computed(() => {
|
|
|
|
return MainCurrentDateType.value === 'week' ? 15 : 3
|
|
|
|
})
|
|
|
|
/** 总计格子剩余 span(补齐最后一行) */
|
|
|
|
const remainSpan = computed(() => {
|
|
|
|
const mod = detailPartOptions.value.length % 4;
|
|
|
|
return mod === 0 ? 4 : 4 - mod;
|
|
|
|
});
|
|
|
|
|
|
|
|
/** 图表 X 轴日期标签 */
|
|
|
|
const chartCategories = computed(() => {
|
|
|
|
if (!weeklyData.value) return [];
|
|
|
|
|
|
|
|
const fullDates = MainCurrentDateType.value === 'week'
|
|
|
|
? getWeekFullDates(startDate.value)
|
|
|
|
: getMonthFullDates(startDate.value);
|
|
|
|
|
|
|
|
// 动态控制柱状图宽度:周15,月5
|
|
|
|
const muscleChartColumnWidth = computed(() => {
|
|
|
|
return MainCurrentDateType.value === 'week' ? 15 : 3
|
|
|
|
})
|
|
|
|
return MainCurrentDateType.value === 'week'
|
|
|
|
? fullDates.map((d) => d.format('M/D'))
|
|
|
|
: fullDates.map((d) => d.format('D'));
|
|
|
|
});
|
|
|
|
|
|
|
|
const trendTabIndex = ref(0);
|
|
|
|
/** 柱体宽度 */
|
|
|
|
const trendChartColumnWidth = computed(() =>
|
|
|
|
MainCurrentDateType.value === 'week' ? 15 : 3
|
|
|
|
);
|
|
|
|
|
|
|
|
// ✅ 修复版:对比表数据,直接从接口取上周/上月总数据
|
|
|
|
const listData = computed(() => {
|
|
|
|
if (!weeklyData.value) return []
|
|
|
|
|
|
|
|
const curr = weeklyData.value
|
|
|
|
// 上周/上月的总数据,接口里的字段名是 lastWeekTotalXXX
|
|
|
|
const last = weeklyData.value
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
label: '容量',
|
|
|
|
lastPeriod: last.lastWeekTotalVolume ?? 0,
|
|
|
|
currPeriod: curr.totalVolume ?? 0,
|
|
|
|
diff: (curr.totalVolume ?? 0) - (last.lastWeekTotalVolume ?? 0)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: '组数',
|
|
|
|
lastPeriod: last.lastWeekTotalSets ?? 0,
|
|
|
|
currPeriod: curr.totalSets ?? 0,
|
|
|
|
diff: (curr.totalSets ?? 0) - (last.lastWeekTotalSets ?? 0)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: '时长',
|
|
|
|
lastPeriod: last.lastWeekTotalDuration ?? 0,
|
|
|
|
currPeriod: curr.totalDuration ?? 0,
|
|
|
|
diff: (curr.totalDuration ?? 0) - (last.lastWeekTotalDuration ?? 0)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: '距离',
|
|
|
|
lastPeriod: last.lastWeekTotalDistance ?? 0,
|
|
|
|
currPeriod: curr.totalDistance ?? 0,
|
|
|
|
diff: (curr.totalDistance ?? 0) - (last.lastWeekTotalDistance ?? 0)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: '次数',
|
|
|
|
lastPeriod: last.lastWeekTotalCount ?? 0,
|
|
|
|
currPeriod: curr.totalCount ?? 0,
|
|
|
|
diff: (curr.totalCount ?? 0) - (last.lastWeekTotalCount ?? 0)
|
|
|
|
}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
const muscleChartColumnWidth = computed(() =>
|
|
|
|
MainCurrentDateType.value === 'week' ? 15 : 3
|
|
|
|
);
|
|
|
|
|
|
|
|
/** 趋势图 Series 数据 */
|
|
|
|
const chartSeries = computed(() => {
|
|
|
|
if (!weeklyData.value) return []
|
|
|
|
if (!weeklyData.value) return [];
|
|
|
|
|
|
|
|
// 1. 获取当前周/月完整日期列表
|
|
|
|
let fullDateList = MainCurrentDateType.value === 'week'
|
|
|
|
const isWeekly = MainCurrentDateType.value === 'week';
|
|
|
|
const fullDateList = isWeekly
|
|
|
|
? getWeekFullDates(startDate.value)
|
|
|
|
: getMonthFullDates(startDate.value)
|
|
|
|
|
|
|
|
// 转成 YYYY-MM-DD
|
|
|
|
const fullDateKeys = fullDateList.map(d => {
|
|
|
|
const y = d.getFullYear()
|
|
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
|
|
return `${y}-${m}-${day}`
|
|
|
|
})
|
|
|
|
// weeklyData是周/月返回的数据
|
|
|
|
const currDataMap = MainCurrentDateType.value === 'week'
|
|
|
|
: getMonthFullDates(startDate.value);
|
|
|
|
|
|
|
|
const fullDateKeys = fullDateList.map((d) => formatDateKey(d));
|
|
|
|
|
|
|
|
const currDataMap = isWeekly
|
|
|
|
? weeklyData.value.weeklyDataMap || {}
|
|
|
|
: weeklyData.value.monthlyDataMap || {};
|
|
|
|
|
|
|
|
const lastDataMap = MainCurrentDateType.value === 'week'
|
|
|
|
const lastDataMap = isWeekly
|
|
|
|
? weeklyData.value.lastWeekDataMap || {}
|
|
|
|
: weeklyData.value.lastMonthDataMap || {};
|
|
|
|
const keyMap = ['volume', 'setCount', 'duration', 'distance', 'count']
|
|
|
|
const dataKey = keyMap[NumberTabIndex.value] || 'volume'
|
|
|
|
|
|
|
|
// 本周/本月数据
|
|
|
|
const currData = fullDateKeys.map(dateStr => currDataMap[dateStr]?.[dataKey] ?? 0)
|
|
|
|
|
|
|
|
// ✅ 加这行,看控制台打印的 currData 数组
|
|
|
|
console.log('【月数据数组 currData】', currData);
|
|
|
|
console.log('【当前选中的 dataKey】', dataKey);
|
|
|
|
console.log('【接口返回的 monthlyDataMap】', weeklyData.value.monthlyDataMap);
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
// 核心:周减7天 / 月减1个月(自动适配大小月)
|
|
|
|
// ==========================================
|
|
|
|
const lastData = fullDateKeys.map(dateStr => {
|
|
|
|
const now = new Date(dateStr)
|
|
|
|
const target = new Date(now)
|
|
|
|
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
target.setDate(target.getDate() - 7)
|
|
|
|
} else {
|
|
|
|
target.setMonth(target.getMonth() - 1) // ✅ JS自动处理大小月
|
|
|
|
}
|
|
|
|
|
|
|
|
const y = target.getFullYear()
|
|
|
|
const m = String(target.getMonth() + 1).padStart(2, '0')
|
|
|
|
const day = String(target.getDate()).padStart(2, '0')
|
|
|
|
const key = `${y}-${m}-${day}`
|
|
|
|
const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
|
|
|
|
|
|
|
|
return lastDataMap[key]?.[dataKey] ?? 0
|
|
|
|
})
|
|
|
|
// 当前周期数据
|
|
|
|
const currData = fullDateKeys.map((dateStr) => currDataMap[dateStr]?.[dataKey] ?? 0);
|
|
|
|
|
|
|
|
const currName = MainCurrentDateType.value === 'week' ? '本周' : '本月'
|
|
|
|
const lastName = MainCurrentDateType.value === 'week' ? '上周' : '上月'
|
|
|
|
// 上一周期数据(按日期偏移匹配)
|
|
|
|
const lastData = fullDateKeys.map((dateStr) => {
|
|
|
|
const targetKey = isWeekly
|
|
|
|
? dayjs(dateStr).subtract(7, 'day').format('YYYY-MM-DD')
|
|
|
|
: dayjs(dateStr).subtract(1, 'month').format('YYYY-MM-DD');
|
|
|
|
return lastDataMap[targetKey]?.[dataKey] ?? 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
const currName = isWeekly ? '本周' : '本月';
|
|
|
|
const lastName = isWeekly ? '上周' : '上月';
|
|
|
|
|
|
|
|
return showLastPeriod.value
|
|
|
|
? [
|
|
|
|
{ name: lastName, data: lastData, color: '#5b8ff9' },
|
|
|
|
{ name: currName, data: currData, color: '#ffc53d' }
|
|
|
|
]
|
|
|
|
: [{ name: currName, data: currData, color: '#ffc53d' }]
|
|
|
|
})
|
|
|
|
|
|
|
|
// 处理肌肉弹窗的图标坐标数据
|
|
|
|
// 1. 根据当前日期,生成周模式的完整日期列表(周一到周日)
|
|
|
|
const getWeekFullDates = (baseDateStr) => {
|
|
|
|
const date = new Date(baseDateStr)
|
|
|
|
const day = date.getDay() // 0=周日, 1=周一...
|
|
|
|
const monday = new Date(date)
|
|
|
|
monday.setDate(date.getDate() - (day === 0 ? 6 : day - 1))
|
|
|
|
|
|
|
|
const dates = []
|
|
|
|
for (let i = 0; i < 7; i++) {
|
|
|
|
const d = new Date(monday)
|
|
|
|
d.setDate(monday.getDate() + i)
|
|
|
|
dates.push(d)
|
|
|
|
}
|
|
|
|
return dates
|
|
|
|
}
|
|
|
|
{ name: lastName, data: lastData, color: '#5b8ff9' },
|
|
|
|
{ name: currName, data: currData, color: '#ffc53d' },
|
|
|
|
]
|
|
|
|
: [{ name: currName, data: currData, color: '#ffc53d' }];
|
|
|
|
});
|
|
|
|
|
|
|
|
// 2. 根据当前日期,生成月模式的完整日期列表(当月1日到月底)
|
|
|
|
const getMonthFullDates = (baseDateStr) => {
|
|
|
|
const date = new Date(baseDateStr)
|
|
|
|
const year = date.getFullYear()
|
|
|
|
const month = date.getMonth()
|
|
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate()
|
|
|
|
|
|
|
|
const dates = []
|
|
|
|
for (let i = 1; i <= daysInMonth; i++) {
|
|
|
|
dates.push(new Date(year, month, i))
|
|
|
|
/** 周期对比数据 */
|
|
|
|
const listData = computed(() => {
|
|
|
|
if (!weeklyData.value) return [];
|
|
|
|
|
|
|
|
const d = weeklyData.value;
|
|
|
|
const fields = [
|
|
|
|
{ label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },
|
|
|
|
{ label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },
|
|
|
|
{ label: '时长', last: d.lastWeekTotalDuration, curr: d.totalDuration },
|
|
|
|
{ label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance },
|
|
|
|
{ label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount },
|
|
|
|
];
|
|
|
|
|
|
|
|
return fields.map(({ label, last, curr }) => {
|
|
|
|
const lastVal = last ?? 0;
|
|
|
|
const currVal = curr ?? 0;
|
|
|
|
return {
|
|
|
|
label,
|
|
|
|
lastPeriod: lastVal,
|
|
|
|
currPeriod: currVal,
|
|
|
|
diff: currVal - lastVal,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
/** 肌肉弹窗图表 X 轴 */
|
|
|
|
const muscleChartCategories = computed(() => {
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
return WEEKDAY_LABELS;
|
|
|
|
}
|
|
|
|
return dates
|
|
|
|
}
|
|
|
|
if (!muscleFullDates.value.length) return [];
|
|
|
|
return muscleFullDates.value.map((d) => dayjs(d).format('M/D'));
|
|
|
|
});
|
|
|
|
|
|
|
|
// 3. 接口返回的 date 数组 [年,月,日] 转成 Date 对象
|
|
|
|
const parseApiDate = (apiDateArr) => {
|
|
|
|
// apiDateArr 格式: [2026, 6, 1]
|
|
|
|
if (!Array.isArray(apiDateArr) || apiDateArr.length !== 3) return null
|
|
|
|
const [year, month, day] = apiDateArr
|
|
|
|
return new Date(year, month - 1, day) // 注意: 月份是0-11
|
|
|
|
}
|
|
|
|
const muscleVolumeArr = computed(() => muscleProcessedData.value.volumeArr || []);
|
|
|
|
const muscleSetsArr = computed(() => muscleProcessedData.value.setsArr || []);
|
|
|
|
|
|
|
|
/** 肌肉弹窗图表 Series */
|
|
|
|
const muscleChartSeries = computed(() => {
|
|
|
|
const data = muscleTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value;
|
|
|
|
const name = muscleTabIndex.value === 0 ? '容量' : '组数';
|
|
|
|
const color = muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9';
|
|
|
|
|
|
|
|
return [{ name, data: data || [], color }];
|
|
|
|
});
|
|
|
|
|
|
|
|
// 接口请求方法
|
|
|
|
// 根据动作分类获取指定日期所在周训练数据
|
|
|
|
// ==================== API 请求方法 ====================
|
|
|
|
|
|
|
|
/** 加载周分类趋势数据 */
|
|
|
|
const loadWeeklyByCategory = async (date) => {
|
|
|
|
if (weeklyLoading.value) return;
|
|
|
|
if (!date) return;
|
|
|
|
if (weeklyLoading.value || !date) return;
|
|
|
|
weeklyLoading.value = true;
|
|
|
|
try {
|
|
|
|
let categoryId = '0'
|
|
|
|
if (partId.value && partId.value !== '') {
|
|
|
|
categoryId = partId.value
|
|
|
|
}
|
|
|
|
const res = await TrainingDataApi.weeklyByCategory(date, categoryId, showLastPeriod.value ? 'true' : 'false');
|
|
|
|
console.log('===== 周数据接口完整返回 =====');
|
|
|
|
console.log(res.data);
|
|
|
|
console.log('本周weeklyDataMap:', res.data.weeklyDataMap);
|
|
|
|
console.log('上周lastWeekDataMap:', res.data.lastWeekDataMap);
|
|
|
|
console.log('上周总数据(lastWeekTotalXXX):', {
|
|
|
|
volume: res.data.lastWeekTotalVolume,
|
|
|
|
sets: res.data.lastWeekTotalSets,
|
|
|
|
duration: res.data.lastWeekTotalDuration,
|
|
|
|
distance: res.data.lastWeekTotalDistance,
|
|
|
|
count: res.data.lastWeekTotalCount
|
|
|
|
});
|
|
|
|
const categoryId = (partId.value && partId.value !== '') ? partId.value : '0';
|
|
|
|
const res = await TrainingDataApi.weeklyByCategory(
|
|
|
|
date,
|
|
|
|
categoryId,
|
|
|
|
showLastPeriod.value ? 'true' : 'false',
|
|
|
|
);
|
|
|
|
weeklyData.value = res.data;
|
|
|
|
} catch (err) {
|
|
|
|
console.error('动作分类周数据加载失败', err);
|
|
...
|
...
|
@@ -454,19 +434,17 @@ const loadWeeklyByCategory = async (date) => { |
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 根据动作分类获取指定日期所在月训练数据(可选择包含上月数据)
|
|
|
|
/** 加载月分类趋势数据 */
|
|
|
|
const loadMonthlyByCategory = async (date) => {
|
|
|
|
if (weeklyLoading.value) return;
|
|
|
|
if (!date) return;
|
|
|
|
if (weeklyLoading.value || !date) return;
|
|
|
|
weeklyLoading.value = true;
|
|
|
|
try {
|
|
|
|
let categoryId = '0'
|
|
|
|
if (partId.value && partId.value !== '') {
|
|
|
|
categoryId = partId.value
|
|
|
|
}
|
|
|
|
console.log('是否返回上个的数据:', showLastPeriod.value);
|
|
|
|
const res = await TrainingDataApi.monthlyByCategory(date, categoryId, showLastPeriod.value ? 'true' : 'false');
|
|
|
|
console.log('【动作分类月数据接口返回】', res.data);
|
|
|
|
const categoryId = (partId.value && partId.value !== '') ? partId.value : '0';
|
|
|
|
const res = await TrainingDataApi.monthlyByCategory(
|
|
|
|
date,
|
|
|
|
categoryId,
|
|
|
|
showLastPeriod.value ? 'true' : 'false',
|
|
|
|
);
|
|
|
|
weeklyData.value = res.data;
|
|
|
|
} catch (err) {
|
|
|
|
console.error('动作分类月数据加载失败', err);
|
|
...
|
...
|
@@ -475,56 +453,41 @@ const loadMonthlyByCategory = async (date) => { |
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 加载部位/动作分类
|
|
|
|
const loadbodyPartOptions = async () => {
|
|
|
|
const res = await LeftmotionApi.getAllCategories();
|
|
|
|
partOptions.value = [
|
|
|
|
{ id: '0', name: '全部' },
|
|
|
|
...res.data.map(item => {
|
|
|
|
return {
|
|
|
|
id: String(item.id),
|
|
|
|
name: item.name
|
|
|
|
}
|
|
|
|
})
|
|
|
|
]
|
|
|
|
console.log('部位列表:', partOptions.value);
|
|
|
|
};
|
|
|
|
const currentMuscleId = ref(null) // 当前点击的肌肉ID
|
|
|
|
// 加载肌肉数据
|
|
|
|
const subCategorieList = ref([])
|
|
|
|
// 训练数据(来自 loadTrainingData 接口)
|
|
|
|
const trainingData = ref({
|
|
|
|
setCountMap: {}, // 组数数据 { id: count }
|
|
|
|
totalVolumeMap: {} // 容量数据 { id: volume }
|
|
|
|
})
|
|
|
|
/** 加载肌肉部位分类列表 */
|
|
|
|
const loadGetSubCategorieList = async () => {
|
|
|
|
if (muscleListLoading.value) return;
|
|
|
|
muscleListLoading.value = true;
|
|
|
|
try {
|
|
|
|
const res = await TrainingDataApi.GetSubCategorieList();
|
|
|
|
subCategorieList.value = res.data;
|
|
|
|
console.log('肌肉数据:', subCategorieList.value);
|
|
|
|
subCategorieList.value = res.data || [];
|
|
|
|
} catch (err) {
|
|
|
|
console.error('加载失败:', err);
|
|
|
|
} finally {
|
|
|
|
muscleListLoading.value = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// 加载肌肉表格数据表格
|
|
|
|
|
|
|
|
/** 加载部位选项列表 */
|
|
|
|
const loadbodyPartOptions = async () => {
|
|
|
|
const res = await LeftmotionApi.getAllCategories();
|
|
|
|
partOptions.value = [
|
|
|
|
{ id: '0', name: '全部' },
|
|
|
|
...res.data.map((item) => ({ id: String(item.id), name: item.name })),
|
|
|
|
];
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 加载肌肉部位训练数据(容量/组数分布) */
|
|
|
|
const loadTrainingData = async (sDate, eDate) => {
|
|
|
|
if (trainingLoading.value) return;
|
|
|
|
trainingLoading.value = true;
|
|
|
|
const s = String(sDate)
|
|
|
|
const e = String(eDate)
|
|
|
|
if (!s || !e || s === 'undefined' || e === 'undefined') return
|
|
|
|
const s = String(sDate);
|
|
|
|
const e = String(eDate);
|
|
|
|
if (!s || !e || s === 'undefined' || e === 'undefined') return;
|
|
|
|
try {
|
|
|
|
const res = await TrainingDataApi.mainDataAnalysis(s, e);
|
|
|
|
console.log('训练数据:', res.data);
|
|
|
|
if (res.data) {
|
|
|
|
trainingData.value.setCountMap = res.data.setCountMap || {}
|
|
|
|
trainingData.value.totalVolumeMap = res.data.totalVolumeMap || {}
|
|
|
|
console.log('trainingData.value', trainingData.value);
|
|
|
|
|
|
|
|
trainingData.value.setCountMap = res.data.setCountMap || {};
|
|
|
|
trainingData.value.totalVolumeMap = res.data.totalVolumeMap || {};
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error('加载失败:', err);
|
|
...
|
...
|
@@ -532,395 +495,343 @@ const loadTrainingData = async (sDate, eDate) => { |
|
|
|
trainingLoading.value = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// 打开肌肉弹窗需要的数据
|
|
|
|
const openMuscleDetail = async (muscleId) => {
|
|
|
|
console.log('点击了肌肉ID:', muscleId)
|
|
|
|
currentMuscleId.value = muscleId
|
|
|
|
muscleShow.value = true
|
|
|
|
// 默认选中容量
|
|
|
|
muscleTabIndex.value = 0
|
|
|
|
|
|
|
|
// 从 subCategorieList 里找到肌肉名称
|
|
|
|
const muscle = subCategorieList.value.find(item => item.id === muscleId)
|
|
|
|
currentMuscleName.value = muscle?.name || ''
|
|
|
|
/** 加载单肌肉统计数据(周/月通用) */
|
|
|
|
const loadMuscleStats = async (muscleId, date, isWeekly) => {
|
|
|
|
try {
|
|
|
|
const res = isWeekly
|
|
|
|
? await TrainingDataApi.weeklyMuscleStats(muscleId, date)
|
|
|
|
: await TrainingDataApi.monthlyMuscleStats(muscleId, date);
|
|
|
|
|
|
|
|
// 根据当前是周/月模式,加载对应接口
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
await loadWeeklyMuscleStats(muscleId, startDate.value)
|
|
|
|
} else {
|
|
|
|
await loadMonthlyMuscleStats(muscleId, startDate.value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] };
|
|
|
|
|
|
|
|
const muscleDetailData = ref({
|
|
|
|
muscleId: 0,
|
|
|
|
dailyStatsList: []
|
|
|
|
})
|
|
|
|
const fullDates = isWeekly ? getWeekFullDates(date) : getMonthFullDates(date);
|
|
|
|
muscleFullDates.value = fullDates;
|
|
|
|
|
|
|
|
// 加载 单肌肉-周统计
|
|
|
|
const loadWeeklyMuscleStats = async (muscleId, date) => {
|
|
|
|
try {
|
|
|
|
const res = await TrainingDataApi.weeklyMuscleStats(muscleId, date)
|
|
|
|
console.log('单肌肉周数据:', res.data)
|
|
|
|
muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] }
|
|
|
|
|
|
|
|
// --- 核心:数据对齐处理 ---
|
|
|
|
// 1. 生成本周完整日期列表
|
|
|
|
const fullDates = getWeekFullDates(date)
|
|
|
|
muscleFullDates.value = fullDates
|
|
|
|
|
|
|
|
// 2. 把接口数据按日期存成映射表
|
|
|
|
const dateMap = {}
|
|
|
|
muscleDetailData.value.dailyStatsList.forEach(item => {
|
|
|
|
const d = parseApiDate(item.date)
|
|
|
|
// 将 API 返回的 dailyStatsList 按日期建立索引
|
|
|
|
const dateMap = {};
|
|
|
|
muscleDetailData.value.dailyStatsList.forEach((item) => {
|
|
|
|
const d = parseApiDate(item.date);
|
|
|
|
if (d) {
|
|
|
|
// 转成 YYYY-MM-DD 字符串当key
|
|
|
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
|
dateMap[key] = {
|
|
|
|
volume: item.volume,
|
|
|
|
sets: item.sets
|
|
|
|
}
|
|
|
|
dateMap[formatDateKey(d)] = { volume: item.volume, sets: item.sets };
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 3. 按完整日期列表填充数据,没有数据填0
|
|
|
|
const volumeArr = []
|
|
|
|
const setsArr = []
|
|
|
|
fullDates.forEach(d => {
|
|
|
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
|
const data = dateMap[key] || { volume: 0, sets: 0 }
|
|
|
|
volumeArr.push(data.volume)
|
|
|
|
setsArr.push(data.sets)
|
|
|
|
})
|
|
|
|
|
|
|
|
muscleProcessedData.value = { volumeArr, setsArr }
|
|
|
|
} catch (err) {
|
|
|
|
console.error('单肌肉周数据失败', err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 加载 单肌肉-月统计
|
|
|
|
const loadMonthlyMuscleStats = async (muscleId, date) => {
|
|
|
|
try {
|
|
|
|
const res = await TrainingDataApi.monthlyMuscleStats(muscleId, date)
|
|
|
|
console.log('单肌肉月数据:', res.data)
|
|
|
|
muscleDetailData.value = res.data || { muscleId: 0, dailyStatsList: [] }
|
|
|
|
|
|
|
|
// --- 核心:数据对齐处理 ---
|
|
|
|
// 1. 生成本月完整日期列表
|
|
|
|
const fullDates = getMonthFullDates(date)
|
|
|
|
muscleFullDates.value = fullDates
|
|
|
|
|
|
|
|
// 2. 把接口数据按日期存成映射表
|
|
|
|
const dateMap = {}
|
|
|
|
muscleDetailData.value.dailyStatsList.forEach(item => {
|
|
|
|
const d = parseApiDate(item.date)
|
|
|
|
if (d) {
|
|
|
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
|
dateMap[key] = {
|
|
|
|
volume: item.volume,
|
|
|
|
sets: item.sets
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 3. 按完整日期列表填充数据,没有数据填0
|
|
|
|
const volumeArr = []
|
|
|
|
const setsArr = []
|
|
|
|
fullDates.forEach(d => {
|
|
|
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
|
const data = dateMap[key] || { volume: 0, sets: 0 }
|
|
|
|
volumeArr.push(data.volume)
|
|
|
|
setsArr.push(data.sets)
|
|
|
|
})
|
|
|
|
|
|
|
|
muscleProcessedData.value = { volumeArr, setsArr }
|
|
|
|
} catch (err) {
|
|
|
|
console.error('单肌肉月数据失败', err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const closeMuscleDetail = () => {
|
|
|
|
|
|
|
|
muscleShow.value = false
|
|
|
|
// currentMuscleId.value = null
|
|
|
|
// currentMuscleName.value = ''
|
|
|
|
// muscleTabIndex.value = 0
|
|
|
|
// muscleDetailData.value = { muscleId: 0, dailyStatsList: [] }
|
|
|
|
// muscleFullDates.value = []
|
|
|
|
// muscleProcessedData.value = { volumeArr: [], setsArr: [] }
|
|
|
|
setTimeout(() => {
|
|
|
|
currentMuscleId.value = null;
|
|
|
|
currentMuscleName.value = '';
|
|
|
|
muscleDetailData.value = { muscleId: 0, dailyStatsList: [] };
|
|
|
|
muscleFullDates.value = [];
|
|
|
|
muscleProcessedData.value = { volumeArr: [], setsArr: [] };
|
|
|
|
}, 150);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 处理好的容量数组(已对齐完整日期)
|
|
|
|
const muscleVolumeArr = computed(() => {
|
|
|
|
return muscleProcessedData.value.volumeArr || []
|
|
|
|
})
|
|
|
|
|
|
|
|
// 处理好的组数数组(已对齐完整日期)
|
|
|
|
const muscleSetsArr = computed(() => {
|
|
|
|
return muscleProcessedData.value.setsArr || []
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
// 弹窗图表的类目(周:周一到周日 / 月:具体日期)
|
|
|
|
const muscleChartCategories = computed(() => {
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
return ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
|
|
|
} else {
|
|
|
|
// 月模式:如果日期列表为空,返回空数组避免报错
|
|
|
|
if (!muscleFullDates.value.length) return []
|
|
|
|
return muscleFullDates.value.map(d => `${d.getMonth() + 1}/${d.getDate()}`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
// 按完整日期列表填充数据,缺失的补 0
|
|
|
|
const volumeArr = [];
|
|
|
|
const setsArr = [];
|
|
|
|
fullDates.forEach((d) => {
|
|
|
|
const data = dateMap[formatDateKey(d)] || { volume: 0, sets: 0 };
|
|
|
|
volumeArr.push(data.volume);
|
|
|
|
setsArr.push(data.sets);
|
|
|
|
});
|
|
|
|
|
|
|
|
// 弹窗图表数据:根据 muscleTabIndex 只显示容量或组数
|
|
|
|
const muscleChartSeries = computed(() => {
|
|
|
|
const data = muscleTabIndex.value === 0
|
|
|
|
? muscleVolumeArr.value
|
|
|
|
: muscleSetsArr.value
|
|
|
|
|
|
|
|
// 兜底:如果数据为空,返回空数组
|
|
|
|
if (!data || !data.length) {
|
|
|
|
return [{
|
|
|
|
name: muscleTabIndex.value === 0 ? '容量' : '组数',
|
|
|
|
data: [],
|
|
|
|
color: muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9'
|
|
|
|
}]
|
|
|
|
muscleProcessedData.value = { volumeArr, setsArr };
|
|
|
|
} catch (err) {
|
|
|
|
console.error('单肌肉数据加载失败', err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return [{
|
|
|
|
name: muscleTabIndex.value === 0 ? '容量' : '组数',
|
|
|
|
data: data,
|
|
|
|
color: muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9'
|
|
|
|
}]
|
|
|
|
})
|
|
|
|
|
|
|
|
// ==================== 页面事件 ====================
|
|
|
|
|
|
|
|
// 事件方法
|
|
|
|
/** 筛选部位切换 */
|
|
|
|
const onPartChange = (item) => {
|
|
|
|
console.log('选中的项:', item);
|
|
|
|
partId.value = item.id;
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
loadWeeklyByCategory(startDate.value);
|
|
|
|
} else {
|
|
|
|
loadMonthlyByCategory(startDate.value);
|
|
|
|
}
|
|
|
|
loadCategoryData(startDate.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
// 当前的日期类型周/月
|
|
|
|
/** 时间类型切换(周/月) */
|
|
|
|
const handleTypeChange = async (type) => {
|
|
|
|
MainCurrentDateType.value = type;
|
|
|
|
showLastPeriod.value = false;
|
|
|
|
loadTrainingData(startDate.value, endDate.value)
|
|
|
|
loadTrainingData(startDate.value, endDate.value);
|
|
|
|
|
|
|
|
if (startDate.value) {
|
|
|
|
if (type === 'week') {
|
|
|
|
await loadWeeklyByCategory(startDate.value)
|
|
|
|
} else {
|
|
|
|
await loadMonthlyByCategory(startDate.value)
|
|
|
|
}
|
|
|
|
await loadCategoryData(startDate.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果弹窗打开,自动刷新肌肉数据
|
|
|
|
// 如果肌肉弹窗打开,同步刷新
|
|
|
|
if (muscleShow.value && currentMuscleId.value) {
|
|
|
|
if (type === 'week') {
|
|
|
|
await loadWeeklyMuscleStats(currentMuscleId.value, startDate.value)
|
|
|
|
} else {
|
|
|
|
await loadMonthlyMuscleStats(currentMuscleId.value, startDate.value)
|
|
|
|
}
|
|
|
|
await loadMuscleData(currentMuscleId.value, startDate.value);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// 处理日期改变
|
|
|
|
|
|
|
|
/** 日期范围变更 */
|
|
|
|
const handleDateChange = async (data) => {
|
|
|
|
console.log(data.startDate, data.endDate);
|
|
|
|
startDate.value = data.startDate;
|
|
|
|
endDate.value = data.endDate;
|
|
|
|
await loadTrainingData(startDate.value, endDate.value)
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
loadWeeklyByCategory(startDate.value);
|
|
|
|
} else {
|
|
|
|
loadMonthlyByCategory(startDate.value);
|
|
|
|
}
|
|
|
|
// 弹窗打开 → 刷新肌肉详情
|
|
|
|
await loadTrainingData(startDate.value, endDate.value);
|
|
|
|
loadCategoryData(startDate.value);
|
|
|
|
|
|
|
|
if (muscleShow.value && currentMuscleId.value) {
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
await loadWeeklyMuscleStats(currentMuscleId.value, startDate.value)
|
|
|
|
} else {
|
|
|
|
await loadMonthlyMuscleStats(currentMuscleId.value, startDate.value)
|
|
|
|
}
|
|
|
|
await loadMuscleData(currentMuscleId.value, startDate.value);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// 处理是否返回上周/月
|
|
|
|
|
|
|
|
/** 打开肌肉详情弹窗 */
|
|
|
|
const openMuscleDetail = async (muscleId) => {
|
|
|
|
currentMuscleId.value = muscleId;
|
|
|
|
muscleShow.value = true;
|
|
|
|
muscleTabIndex.value = 0;
|
|
|
|
|
|
|
|
const muscle = subCategorieList.value.find((item) => item.id === muscleId);
|
|
|
|
currentMuscleName.value = muscle?.name || '';
|
|
|
|
|
|
|
|
await loadMuscleData(muscleId, startDate.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 关闭肌肉详情弹窗 */
|
|
|
|
const closeMuscleDetail = () => {
|
|
|
|
muscleShow.value = false;
|
|
|
|
setTimeout(() => {
|
|
|
|
currentMuscleId.value = null;
|
|
|
|
currentMuscleName.value = '';
|
|
|
|
muscleDetailData.value = { muscleId: 0, dailyStatsList: [] };
|
|
|
|
muscleFullDates.value = [];
|
|
|
|
muscleProcessedData.value = { volumeArr: [], setsArr: [] };
|
|
|
|
}, 150);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** 切换显示上一周期数据 */
|
|
|
|
const toggleLastWeek = () => {
|
|
|
|
showLastPeriod.value = !showLastPeriod.value
|
|
|
|
showLastPeriod.value = !showLastPeriod.value;
|
|
|
|
if (startDate.value) {
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
loadWeeklyByCategory(startDate.value)
|
|
|
|
} else {
|
|
|
|
loadMonthlyByCategory(startDate.value)
|
|
|
|
}
|
|
|
|
loadCategoryData(startDate.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// ==================== 生命周期 ====================
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
await loadbodyPartOptions();
|
|
|
|
await loadGetSubCategorieList();
|
|
|
|
if (startDate.value && endDate.value) {
|
|
|
|
await loadTrainingData(startDate.value, endDate.value)
|
|
|
|
if (MainCurrentDateType.value === 'week') {
|
|
|
|
await loadWeeklyByCategory(startDate.value)
|
|
|
|
} else {
|
|
|
|
await loadMonthlyByCategory(startDate.value)
|
|
|
|
}
|
|
|
|
await loadTrainingData(startDate.value, endDate.value);
|
|
|
|
await loadCategoryData(startDate.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
// ==================== CSS 变量 ====================
|
|
|
|
$color-primary: #202932;
|
|
|
|
$color-bg: #f8fafc;
|
|
|
|
$color-card: #ffffff;
|
|
|
|
$color-border: #f1f5f9;
|
|
|
|
$color-text: #0f172a;
|
|
|
|
$color-sub: #475569;
|
|
|
|
$color-muted: #94a3b8;
|
|
|
|
$color-green: #10b981;
|
|
|
|
$color-green-bg: #ecfdf5;
|
|
|
|
$color-green-border: #a7f3d0;
|
|
|
|
$color-blue: #5b8ff9;
|
|
|
|
$color-yellow: #ffc53d;
|
|
|
|
$color-red: #ff7875;
|
|
|
|
$radius-card: 24rpx;
|
|
|
|
$radius-tab: 50rpx;
|
|
|
|
$radius-item: 12rpx;
|
|
|
|
|
|
|
|
// ==================== 公共 Tab 样式 ====================
|
|
|
|
.tab-group {
|
|
|
|
display: flex;
|
|
|
|
gap: 16rpx;
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
|
|
&.flex-wrap {
|
|
|
|
flex-wrap: wrap;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.tab-item {
|
|
|
|
border: 1rpx solid #e2e8f0;
|
|
|
|
border-radius: $radius-tab;
|
|
|
|
background-color: $color-card;
|
|
|
|
padding: 10rpx 24rpx;
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: $color-sub;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
background-color: $color-primary;
|
|
|
|
color: $color-card;
|
|
|
|
border-color: $color-primary;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
opacity: 0.8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.tip-text {
|
|
|
|
font-size: 22rpx;
|
|
|
|
color: $color-muted;
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ==================== 页面主容器 ====================
|
|
|
|
.training-data-page {
|
|
|
|
min-height: 100vh;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
padding: 30rpx 24rpx 0;
|
|
|
|
box-sizing: border-box;
|
|
|
|
overflow: auto;
|
|
|
|
|
|
|
|
.content-scroll {
|
|
|
|
height: calc(100vh - 400rpx);
|
|
|
|
// #ifdef H5
|
|
|
|
height: calc(100vh - 185px);
|
|
|
|
// #endif
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
width: 100vw;
|
|
|
|
background-color: $color-bg;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
// 顶部固定时间选择栏
|
|
|
|
.top-time-picker {
|
|
|
|
padding: 20rpx 24rpx;
|
|
|
|
box-sizing: border-box;
|
|
|
|
overflow: auto;
|
|
|
|
background-color: $color-card;
|
|
|
|
border-bottom: 1rpx solid $color-border;
|
|
|
|
z-index: 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
.section {
|
|
|
|
padding: 30rpx 20rpx;
|
|
|
|
box-sizing: border-box;
|
|
|
|
background-color: #fff;
|
|
|
|
border-radius: 15rpx;
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
margin-bottom: 110rpx;
|
|
|
|
}
|
|
|
|
// 主体可滚动区域
|
|
|
|
.main-content {
|
|
|
|
flex: 1;
|
|
|
|
height: 0;
|
|
|
|
min-height: 0;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
.title {
|
|
|
|
font-size: 30rpx;
|
|
|
|
font-weight: bold;
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
}
|
|
|
|
.scroll-area {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
.tip {
|
|
|
|
font-size: 22rpx;
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
.scroll-inner {
|
|
|
|
padding: 24rpx;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: 24rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.img {
|
|
|
|
width: 100%;
|
|
|
|
height: 400rpx;
|
|
|
|
}
|
|
|
|
// 卡片通用容器
|
|
|
|
.card-section {
|
|
|
|
background-color: $color-card;
|
|
|
|
border-radius: $radius-card;
|
|
|
|
padding: 28rpx 24rpx;
|
|
|
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
|
|
|
border: 1rpx solid $color-border;
|
|
|
|
|
|
|
|
.tabs {
|
|
|
|
.section-header {
|
|
|
|
display: flex;
|
|
|
|
gap: 20rpx;
|
|
|
|
align-items: center;
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
|
|
.tab {
|
|
|
|
border: 1rpx solid #eee;
|
|
|
|
border-radius: 50rpx;
|
|
|
|
background-color: #fff;
|
|
|
|
padding: 10rpx 20rpx;
|
|
|
|
font-size: 24rpx;
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
&.space-between {
|
|
|
|
justify-content: space-between;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
background-color: #202932;
|
|
|
|
color: #fff;
|
|
|
|
border: none;
|
|
|
|
}
|
|
|
|
.title {
|
|
|
|
font-size: 30rpx;
|
|
|
|
font-weight: 700;
|
|
|
|
color: $color-text;
|
|
|
|
}
|
|
|
|
|
|
|
|
.part-select-wrapper {
|
|
|
|
min-width: 140rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ========== 核心网格布局 已修复 ========== */
|
|
|
|
.detail-part {
|
|
|
|
// 肌肉部位网格
|
|
|
|
.muscle-grid {
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
gap: 5rpx;
|
|
|
|
gap: 10rpx;
|
|
|
|
|
|
|
|
.part-item {
|
|
|
|
.grid-item {
|
|
|
|
width: 100%;
|
|
|
|
height: 140rpx;
|
|
|
|
// aspect-ratio: 1 / 1;
|
|
|
|
/* 强制正方形 */
|
|
|
|
background-color: #eee;
|
|
|
|
background-color: $color-bg;
|
|
|
|
border: 1rpx solid $color-border;
|
|
|
|
border-radius: $radius-item;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
flex-direction: column;
|
|
|
|
font-size: 24rpx;
|
|
|
|
gap: 10rpx;
|
|
|
|
border-radius: 5rpx;
|
|
|
|
transition: transform 0.15s, background-color 0.15s;
|
|
|
|
|
|
|
|
.num {
|
|
|
|
font-weight: bold;
|
|
|
|
color: #000;
|
|
|
|
&:active {
|
|
|
|
transform: scale(0.96);
|
|
|
|
background-color: #f1f5f9;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 总计自适应占格 */
|
|
|
|
.total-item {
|
|
|
|
grid-column: span var(--remain-span);
|
|
|
|
height: 140rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.item-name {
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: $color-sub;
|
|
|
|
}
|
|
|
|
|
|
|
|
.header {
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
align-items: center;
|
|
|
|
.item-value {
|
|
|
|
font-size: 26rpx;
|
|
|
|
font-weight: 700;
|
|
|
|
color: #000000;
|
|
|
|
|
|
|
|
&.highlight {
|
|
|
|
color: $color-green;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.total-item {
|
|
|
|
background-color: $color-green-bg;
|
|
|
|
border-color: $color-green-border;
|
|
|
|
|
|
|
|
.item-name {
|
|
|
|
color: #047857;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.statistical {
|
|
|
|
// 图表容器
|
|
|
|
.chart-container {
|
|
|
|
width: 100%;
|
|
|
|
height: 400rpx;
|
|
|
|
}
|
|
|
|
|
|
|
|
.chose {
|
|
|
|
// 显示上期数据勾选
|
|
|
|
.toggle-checkbox {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
margin-top: 20rpx;
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: #333333;
|
|
|
|
|
|
|
|
.toggle-label {
|
|
|
|
margin-left: 8rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.compare {
|
|
|
|
// 周期对比区域
|
|
|
|
.compare-section {
|
|
|
|
margin-top: 30rpx;
|
|
|
|
|
|
|
|
.header-row {
|
|
|
|
.compare-header {
|
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
|
|
|
align-items: center;
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
|
|
.title {
|
|
|
|
.sub-title {
|
|
|
|
font-size: 36rpx;
|
|
|
|
font-weight: bold;
|
|
|
|
color: #333;
|
|
|
|
font-weight: 700;
|
|
|
|
color: #333333;
|
|
|
|
}
|
|
|
|
|
|
|
|
.legend-group {
|
|
|
|
display: flex;
|
|
|
|
gap: 100rpx;
|
|
|
|
gap: 32rpx;
|
|
|
|
|
|
|
|
.legend-item {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
font-size: 24rpx;
|
|
|
|
color: #666;
|
|
|
|
color: #666666;
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
width: 20rpx;
|
|
...
|
...
|
@@ -928,22 +839,18 @@ onMounted(async () => { |
|
|
|
border-radius: 4rpx;
|
|
|
|
margin-right: 8rpx;
|
|
|
|
|
|
|
|
&.blue {
|
|
|
|
background-color: #5b8ff9;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.yellow {
|
|
|
|
background-color: #ffc53d;
|
|
|
|
}
|
|
|
|
&.blue { background-color: $color-blue; }
|
|
|
|
&.yellow { background-color: $color-yellow; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.compare-card {
|
|
|
|
background-color: #ffffff;
|
|
|
|
background-color: $color-card;
|
|
|
|
border-radius: 20rpx;
|
|
|
|
border: 5rpx solid #efefef;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
.data-list {
|
|
|
|
.data-row {
|
|
...
|
...
|
@@ -953,31 +860,25 @@ onMounted(async () => { |
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
border-bottom: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:nth-child(odd) {
|
|
|
|
background-color: #f9fafc;
|
|
|
|
}
|
|
|
|
&:last-child { border-bottom: none; }
|
|
|
|
&:nth-child(odd) { background-color: #f9fafc; }
|
|
|
|
|
|
|
|
.label {
|
|
|
|
flex: 1.5;
|
|
|
|
color: #333;
|
|
|
|
color: #333333;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.value {
|
|
|
|
flex: 1;
|
|
|
|
text-align: center;
|
|
|
|
color: #333;
|
|
|
|
color: #333333;
|
|
|
|
|
|
|
|
&.diff {
|
|
|
|
color: #666;
|
|
|
|
}
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
&.negative {
|
|
|
|
color: #ff7875;
|
|
|
|
&.positive { color: $color-green; }
|
|
|
|
&.negative { color: $color-red; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
...
|
...
|
@@ -986,16 +887,26 @@ onMounted(async () => { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.muscleShow-popup {
|
|
|
|
// 底部安全间距
|
|
|
|
.bottom-spacer {
|
|
|
|
height: env(safe-area-inset-bottom);
|
|
|
|
padding-bottom: 110rpx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* #ifdef MP-WEIXIN */
|
|
|
|
min-height: 40vh;
|
|
|
|
/* #endif */
|
|
|
|
// ==================== 肌肉弹窗样式 ====================
|
|
|
|
.muscleShow-popup {
|
|
|
|
padding-bottom: 30rpx;
|
|
|
|
|
|
|
|
/* #ifdef H5 */
|
|
|
|
min-height: 55vh;
|
|
|
|
/* #endif */
|
|
|
|
/* #ifdef MP-WEIXIN */
|
|
|
|
min-height: 40vh;
|
|
|
|
/* #endif */
|
|
|
|
|
|
|
|
/* #ifdef H5 */
|
|
|
|
min-height: 55vh;
|
|
|
|
/* #endif */
|
|
|
|
|
|
|
|
.popup-body {
|
|
|
|
.muscle-name {
|
|
|
|
font-size: 32rpx;
|
|
|
|
font-weight: bold;
|
|
...
|
...
|
@@ -1006,25 +917,12 @@ onMounted(async () => { |
|
|
|
display: flex;
|
|
|
|
gap: 20rpx;
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
|
|
.tab {
|
|
|
|
border: 1rpx solid #eee;
|
|
|
|
border-radius: 50rpx;
|
|
|
|
background-color: #fff;
|
|
|
|
padding: 10rpx 20rpx;
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
background-color: #202932;
|
|
|
|
color: #fff;
|
|
|
|
border: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.tip {
|
|
|
|
font-size: 22rpx;
|
|
|
|
padding: 0 20rpx 20rpx;
|
|
|
|
color: $color-muted;
|
|
|
|
}
|
|
|
|
|
|
|
|
.statistical {
|
...
|
...
|
|