update:对接个人训练改版
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import TargetPicker from "@/components/TargetPicker.vue";
|
||||
import { getPersonalTrainingAPI } from "@/apis";
|
||||
|
||||
const checkedIcon = "../../static/training-home/done.png";
|
||||
const missedIcon = "../../static/training-home/missed.png";
|
||||
const checkedIcon = "./static/training-home/done.png";
|
||||
const missedIcon = "./static/training-home/missed.png";
|
||||
// 后端训练项目 id 与难度页 mode 参数的映射关系。
|
||||
const trainingModeRouteMap = {
|
||||
base: "basic",
|
||||
@@ -17,18 +17,18 @@ const trainingModeRouteMap = {
|
||||
};
|
||||
// 训练项目卡片右侧主图标。
|
||||
const trainingModeIconMap = {
|
||||
base_bow: "../../static/training-home/img_22.png",
|
||||
bow: "../../static/training-home/img_3.png",
|
||||
target: "../../static/training-home/img_4.png",
|
||||
wave: "../../static/training-home/img_5.png",
|
||||
muscle: "../../static/training-home/img_6.png",
|
||||
base_bow: "./static/training-home/img_22.png",
|
||||
bow: "./static/training-home/img_3.png",
|
||||
target: "./static/training-home/img_4.png",
|
||||
wave: "./static/training-home/img_5.png",
|
||||
muscle: "./static/training-home/img_6.png",
|
||||
};
|
||||
// 训练项目卡片标题图,按接口 id 映射本地资源。
|
||||
const trainingModeTitleImageMap = {
|
||||
endurance: "../../static/training-home/nailixunlian.png",
|
||||
precision: "../../static/training-home/jingzhunxunlian.png",
|
||||
rhythm: "../../static/training-home/jiezouxunlian.png",
|
||||
strength: "../../static/training-home/liliangxulian.png",
|
||||
endurance: "./static/training-home/nailixunlian.png",
|
||||
precision: "./static/training-home/jingzhunxunlian.png",
|
||||
rhythm: "./static/training-home/jiezouxunlian.png",
|
||||
strength: "./static/training-home/liliangxulian.png",
|
||||
};
|
||||
const defaultWeekDays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
const defaultRadarDimensions = [
|
||||
@@ -38,6 +38,13 @@ const defaultRadarDimensions = [
|
||||
{ name: "节奏", score: 0 },
|
||||
{ name: "耐力", score: 0 },
|
||||
];
|
||||
const radarDimensionTrainingIdMap = Object.freeze({
|
||||
基础: "base",
|
||||
精准: "precision",
|
||||
力量: "strength",
|
||||
节奏: "rhythm",
|
||||
耐力: "endurance",
|
||||
});
|
||||
|
||||
// 页面始终直接消费接口字段,这里只保留一份兜底结构,避免模板访问空值。
|
||||
const createDefaultTrainingData = () => ({
|
||||
@@ -50,6 +57,7 @@ const createDefaultTrainingData = () => ({
|
||||
total_calories: 0,
|
||||
overtake_rate: 0,
|
||||
},
|
||||
radar_max: 0,
|
||||
radar: {
|
||||
dimensions: defaultRadarDimensions,
|
||||
},
|
||||
@@ -57,6 +65,12 @@ const createDefaultTrainingData = () => ({
|
||||
});
|
||||
|
||||
const trainingData = ref(createDefaultTrainingData());
|
||||
const recommendedTrainingId = ref("");
|
||||
const visibleTrainingItems = computed(() =>
|
||||
Array.isArray(trainingData.value.training_items)
|
||||
? trainingData.value.training_items.filter((item) => item.id !== "strength")
|
||||
: []
|
||||
);
|
||||
const pageMounted = ref(false);
|
||||
const showRoutineTargetPicker = ref(false);
|
||||
const trainingRadarCanvasId = "training-home-radar";
|
||||
@@ -77,7 +91,6 @@ const radarStrokeWidth = Math.max(1, 2 * radarScale);
|
||||
const radarPointRadius = Math.max(2.5, 3.5 * radarScale);
|
||||
const radarOuterRadiusX = 110.7089 * radarScaleX;
|
||||
const radarOuterRadiusY = 110.7089 * radarScaleY;
|
||||
const radarMaxValue = 100;
|
||||
const radarFigureStyle = {
|
||||
width: `${radarFigureWidthRpx}rpx`,
|
||||
height: `${radarFigureHeightRpx}rpx`,
|
||||
@@ -113,26 +126,78 @@ const getTrainingTitleImage = (item = {}) =>
|
||||
const getTrainingMode = (item = {}) =>
|
||||
trainingModeRouteMap[item.id] || item.id || "";
|
||||
|
||||
const updateRecommendedTraining = () => {
|
||||
recommendedTrainingId.value = "";
|
||||
|
||||
const radarMaxValue = Number(trainingData.value.radar_max);
|
||||
const dimensions = trainingData.value.radar?.dimensions;
|
||||
if (
|
||||
!Number.isFinite(radarMaxValue) ||
|
||||
radarMaxValue <= 0 ||
|
||||
!Array.isArray(dimensions) ||
|
||||
dimensions.length !== 5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleTrainingIds = visibleTrainingItems.value.map((item) => item.id);
|
||||
const candidates = dimensions.reduce((result, item) => {
|
||||
const trainingId = radarDimensionTrainingIdMap[item?.name];
|
||||
const rawScore = item?.score;
|
||||
if (
|
||||
!visibleTrainingIds.includes(trainingId) ||
|
||||
rawScore === undefined ||
|
||||
rawScore === null ||
|
||||
rawScore === ""
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const score = Number(rawScore);
|
||||
if (!Number.isFinite(score) || score < 0) return result;
|
||||
|
||||
result.push({ trainingId, score });
|
||||
return result;
|
||||
}, []);
|
||||
if (!candidates.length) return;
|
||||
|
||||
const lowestScore = Math.min(...candidates.map((item) => item.score));
|
||||
const lowestCandidates = candidates.filter(
|
||||
(item) => item.score === lowestScore
|
||||
);
|
||||
const selectedIndex = Math.floor(Math.random() * lowestCandidates.length);
|
||||
recommendedTrainingId.value =
|
||||
lowestCandidates[selectedIndex]?.trainingId || "";
|
||||
};
|
||||
|
||||
const getRadarPoint = (centerX, centerY, radiusX, radiusY, angle) => ({
|
||||
x: centerX + radiusX * Math.cos(angle),
|
||||
y: centerY + radiusY * Math.sin(angle),
|
||||
});
|
||||
|
||||
// 雷达图直接使用接口的 5 维 score,按 0-100 等比映射到顶点位置。
|
||||
// 雷达图直接使用接口的 5 维 score,按后端 radar_max 等比映射到顶点位置。
|
||||
const drawRadar = () => {
|
||||
const dimensions = Array.isArray(trainingData.value.radar?.dimensions)
|
||||
? trainingData.value.radar.dimensions.slice(0, 5)
|
||||
: [];
|
||||
|
||||
if (dimensions.length !== 5) return;
|
||||
const radarMaxValue = Number(trainingData.value.radar_max);
|
||||
|
||||
const ctx = uni.createCanvasContext(trainingRadarCanvasId);
|
||||
ctx.clearRect(0, 0, radarCanvasWidth, radarCanvasHeight);
|
||||
|
||||
if (
|
||||
dimensions.length !== 5 ||
|
||||
!Number.isFinite(radarMaxValue) ||
|
||||
radarMaxValue <= 0
|
||||
) {
|
||||
ctx.draw();
|
||||
return;
|
||||
}
|
||||
|
||||
const angles = dimensions.map(
|
||||
(_, index) => (-90 + index * 72) * (Math.PI / 180)
|
||||
);
|
||||
|
||||
ctx.clearRect(0, 0, radarCanvasWidth, radarCanvasHeight);
|
||||
|
||||
const points = dimensions.map((item, index) => {
|
||||
const normalized = Math.max(
|
||||
0,
|
||||
@@ -199,6 +264,7 @@ const loadPersonalTrainingData = async () => {
|
||||
total_calories: result?.stats?.total_calories ?? 0,
|
||||
overtake_rate: result?.stats?.overtake_rate ?? 0,
|
||||
},
|
||||
radar_max: result?.radar_max ?? 0,
|
||||
radar: {
|
||||
dimensions:
|
||||
Array.isArray(result?.radar?.dimensions) &&
|
||||
@@ -214,6 +280,7 @@ const loadPersonalTrainingData = async () => {
|
||||
console.log("personal training load failed", error);
|
||||
trainingData.value = createDefaultTrainingData();
|
||||
} finally {
|
||||
updateRecommendedTraining();
|
||||
await refreshRadar();
|
||||
}
|
||||
};
|
||||
@@ -293,12 +360,12 @@ onShow(async () => {
|
||||
<view class="stats-card-bg"></view>
|
||||
<image
|
||||
class="stats-quote stats-quote-left"
|
||||
src="../../static/training-home/img_17.png"
|
||||
src="./static/training-home/img_17.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
class="stats-quote stats-quote-right"
|
||||
src="../../static/training-home/img_16.png"
|
||||
src="./static/training-home/img_16.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="stats-grid">
|
||||
@@ -373,7 +440,7 @@ onShow(async () => {
|
||||
<view class="record-bubble" @click="openTrainingRecord">
|
||||
<image
|
||||
class="record-bubble-bg"
|
||||
src="../../static/training-home/img_28.png"
|
||||
src="./static/training-home/img_28.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="record-bubble-copy">
|
||||
@@ -384,7 +451,7 @@ onShow(async () => {
|
||||
<text class="record-sub-text">我的训练记录</text>
|
||||
<image
|
||||
class="record-arrow"
|
||||
src="../../static/training-home/img_7.png"
|
||||
src="./static/training-home/img_7.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
@@ -412,7 +479,7 @@ onShow(async () => {
|
||||
<image
|
||||
class="radar-grid-image"
|
||||
:style="radarFigureStyle"
|
||||
src="../../static/training-home/img_19.png"
|
||||
src="./static/training-home/img_19.png"
|
||||
/>
|
||||
<canvas
|
||||
:canvas-id="trainingRadarCanvasId"
|
||||
@@ -424,7 +491,7 @@ onShow(async () => {
|
||||
/>
|
||||
<image
|
||||
class="radar-mascot"
|
||||
src="../../static/training-home/img_21.png"
|
||||
src="./static/training-home/img_21.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
@@ -434,7 +501,7 @@ onShow(async () => {
|
||||
<view class="featured-card" @click="openRoutineTraining">
|
||||
<image
|
||||
class="featured-card-bg"
|
||||
src="../../static/training-home/img_22.png"
|
||||
src="./static/training-home/img_22.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="featured-card-mask"></view>
|
||||
@@ -446,12 +513,14 @@ onShow(async () => {
|
||||
|
||||
<view class="mode-grid">
|
||||
<view
|
||||
v-for="item in trainingData.training_items.filter((item) => item.id !== 'strength')"
|
||||
v-for="item in visibleTrainingItems"
|
||||
:key="item.id"
|
||||
class="mode-card"
|
||||
@click="openTrainingItem(item)"
|
||||
>
|
||||
<view v-if="item.is_recommended" class="mode-tag">推荐</view>
|
||||
<view v-if="item.id === recommendedTrainingId" class="mode-tag">
|
||||
推荐
|
||||
</view>
|
||||
<view class="mode-card-copy">
|
||||
<image
|
||||
v-if="getTrainingTitleImage(item)"
|
||||
|
||||
Reference in New Issue
Block a user