个人训练改版首页存档

This commit is contained in:
2026-05-13 10:49:31 +08:00
parent 19391808ef
commit 1bca5977c1
29 changed files with 1729 additions and 1 deletions

View File

@@ -0,0 +1,701 @@
<script setup>
import { nextTick, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import {
trainingHomeFeatured,
trainingHomeModes,
trainingHomeRadar,
trainingHomeStats,
trainingHomeWeekSchedule,
} from "@/mock/index.js";
// 雷达图绘制仍使用原生 number 尺寸,样式展示统一使用 rpx。
const trainingRadarCanvasId = "training-home-radar";
const radarImageWidth = 225;
const radarImageHeight = 224;
const radarFigureWidthRpx = 448;
const radarFigureHeightRpx = Math.round(
(radarFigureWidthRpx * radarImageHeight) / radarImageWidth
);
const radarCanvasWidth = Math.round(uni.upx2px(radarFigureWidthRpx));
const radarCanvasHeight = Math.round(uni.upx2px(radarFigureHeightRpx));
const radarScaleX = radarCanvasWidth / radarImageWidth;
const radarScaleY = radarCanvasHeight / radarImageHeight;
const radarScale = Math.min(radarScaleX, radarScaleY);
// Fit from img_19.png so value=10 lands on the actual outer circle.
const radarCenterX = 112.0624 * radarScaleX;
const radarCenterY = 111.4645 * radarScaleY;
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 radarFigureStyle = {
width: `${radarFigureWidthRpx}rpx`,
height: `${radarFigureHeightRpx}rpx`,
};
const getRadarPoint = (centerX, centerY, radiusX, radiusY, angle) => {
return {
x: centerX + radiusX * Math.cos(angle),
y: centerY + radiusY * Math.sin(angle),
};
};
const drawRadar = () => {
const ctx = uni.createCanvasContext(trainingRadarCanvasId);
const angles = trainingHomeRadar.labels.map(
(_, index) => (-90 + index * 72) * (Math.PI / 180)
);
ctx.clearRect(0, 0, radarCanvasWidth, radarCanvasHeight);
// 五边形底图已经由设计切图承载,这里只叠加能力值多边形和节点。
const points = trainingHomeRadar.values.map((value, index) => {
const normalized = Math.max(0, Math.min(value, trainingHomeRadar.maxValue));
const progress = normalized / trainingHomeRadar.maxValue;
return getRadarPoint(
radarCenterX,
radarCenterY,
radarOuterRadiusX * progress,
radarOuterRadiusY * progress,
angles[index]
);
});
ctx.beginPath();
points.forEach((point, index) => {
if (index === 0) {
ctx.moveTo(point.x, point.y);
return;
}
ctx.lineTo(point.x, point.y);
});
ctx.closePath();
ctx.setFillStyle("rgba(255, 209, 154, 0.26)");
ctx.fill();
ctx.setStrokeStyle("rgba(220, 162, 92, 0.92)");
ctx.setLineWidth(radarStrokeWidth);
ctx.stroke();
points.forEach((point) => {
ctx.beginPath();
ctx.arc(point.x, point.y, radarPointRadius, 0, 2 * Math.PI);
ctx.setFillStyle("rgba(221, 162, 90, 1)");
ctx.fill();
});
ctx.beginPath();
ctx.arc(radarCenterX, radarCenterY, radarPointRadius, 0, 2 * Math.PI);
ctx.setFillStyle("rgba(125, 107, 83, 0.65)");
ctx.fill();
ctx.draw();
};
// 这些入口先保留占位行为,等后续页面接入后再替换成真实跳转。
const openTrainingRecord = () => {
uni.showToast({
title: "训练记录待接入",
icon: "none",
});
};
const openFeaturedTraining = () => {
uni.showToast({
title: `进入${trainingHomeFeatured.title}`,
icon: "none",
});
};
const openTrainingMode = (item) => {
if (item.disabled) {
uni.showToast({
title: `${item.title} 暂未开放`,
icon: "none",
});
return;
}
uni.showToast({
title: `进入${item.title}`,
icon: "none",
});
};
onMounted(() => {
nextTick(drawRadar);
});
onShow(() => {
nextTick(drawRadar);
});
</script>
<template>
<Container :showBackToGame="true" :bgType="7" bgColor="#050b19">
<view class="training-home">
<!-- 周打卡区域 -->
<view class="week-grid">
<view
v-for="item in trainingHomeWeekSchedule"
:key="item.key"
class="week-item"
>
<view class="week-item-bg"></view>
<image class="week-item-icon" :src="item.icon" mode="widthFix" />
<text
class="week-item-label"
:class="{ 'week-item-label-active': item.status === 'done' }"
>
{{ item.label }}
</text>
</view>
</view>
<!-- 训练统计卡片 -->
<view class="stats-card">
<view class="stats-card-bg"></view>
<image
class="stats-quote stats-quote-left"
src="../../static/training-home/slices/img_17.png"
mode="widthFix"
/>
<image
class="stats-quote stats-quote-right"
src="../../static/training-home/slices/img_16.png"
mode="widthFix"
/>
<view class="stats-grid">
<view
v-for="item in trainingHomeStats"
:key="item.key"
class="stats-item"
>
<view class="stats-value-row">
<view class="stats-value-group">
<text class="stats-value">{{ item.value }}</text>
<text class="stats-unit">{{ item.unit }}</text>
<view class="stats-value-decoration"></view>
</view>
</view>
<text class="stats-label">{{ item.label }}</text>
</view>
</view>
</view>
<!-- 雷达图与训练记录入口 -->
<view class="radar-section">
<view class="record-bubble" @click="openTrainingRecord">
<image
class="record-bubble-bg"
src="../../static/training-home/slices/img_28.png"
mode="widthFix"
/>
<view class="record-bubble-copy">
<view class="record-main">
已超越<text class="record-main-highlight">
80%
</text>对手
</view>
<view class="record-sub-row">
<text class="record-sub-text">我的训练记录</text>
<image class="record-arrow" src="../../static/training-home/slices/img_7.png" mode="widthFix" />
</view>
</view>
</view>
<view class="radar-board">
<text class="radar-label radar-label-top">{{ trainingHomeRadar.labels[0] }}</text>
<text class="radar-label radar-label-right">{{ trainingHomeRadar.labels[1] }}</text>
<text class="radar-label radar-label-bottom-right">
{{ trainingHomeRadar.labels[2] }}
</text>
<text class="radar-label radar-label-bottom-left">
{{ trainingHomeRadar.labels[3] }}
</text>
<text class="radar-label radar-label-left">{{ trainingHomeRadar.labels[4] }}</text>
<view class="radar-figure" :style="radarFigureStyle">
<image
class="radar-grid-image"
:style="radarFigureStyle"
src="../../static/training-home/slices/img_19.png"
/>
<canvas
:canvas-id="trainingRadarCanvasId"
:id="trainingRadarCanvasId"
class="radar-canvas"
:style="radarFigureStyle"
:width="radarCanvasWidth"
:height="radarCanvasHeight"
/>
<image
class="radar-mascot"
src="../../static/training-home/slices/img_21.png"
mode="widthFix"
/>
</view>
</view>
</view>
<!-- 主推荐训练入口 -->
<view class="featured-card" @click="openFeaturedTraining">
<image
class="featured-card-bg"
src="../../static/training-home/slices/img_22.png"
mode="widthFix"
/>
<view class="featured-card-copy">
<text class="featured-card-title"></text>
<text class="featured-card-progress">{{ trainingHomeFeatured.progressText }}</text>
</view>
</view>
<!-- 四个训练模式入口 -->
<view class="mode-grid">
<view
v-for="item in trainingHomeModes"
:key="item.key"
class="mode-card"
@click="openTrainingMode(item)"
>
<view v-if="item.recommended" class="mode-tag">推荐</view>
<view class="mode-card-copy">
<text class="mode-card-title">{{ item.title }}</text>
<text class="mode-card-progress">{{ item.progressText }}</text>
</view>
<image class="mode-card-icon" :src="item.icon" mode="aspectFit" />
</view>
</view>
</view>
</Container>
</template>
<style scoped>
.training-home {
position: relative;
overflow: hidden;
padding: 18rpx 20rpx 60rpx 20rpx;
}
.top-background {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.top-background {
z-index: 1;
}
.nav-row {
display: flex;
align-items: center;
}
.back-button {
display: flex;
align-items: center;
}
.back-icon {
width: 80rpx;
}
.week-grid {
display: flex;
justify-content: space-between;
margin-top: 18rpx;
}
.week-item {
position: relative;
width: 92rpx;
height: 96rpx;
border-radius: 16rpx;
overflow: hidden;
}
.week-item-bg {
width: 100%;
height: 100%;
background: linear-gradient( 180deg, #2F2D2B 0%, #252831 100%);
opacity: 0.5;
}
.week-item-icon {
position: absolute;
left: 28rpx;
top: 14rpx;
width: 36rpx;
}
.week-item-label {
position: absolute;
left: 0;
right: 0;
bottom: 10rpx;
color: #fff;
font-size: 20rpx;
text-align: center;
line-height: 28rpx;
}
.week-item-label-active {
color: #e7ba80;
}
.stats-card {
position: relative;
margin-top: 32rpx;
width: 100%;
height: 124rpx;
overflow: hidden;
border-radius: 24rpx;
}
.stats-card-bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient( 180deg, #2F2D2B 0%, #252831 100%);
opacity: 0.5;
}
.stats-quote {
position: absolute;
z-index: 1;
width: 53rpx;
height: 50rpx;
}
.stats-quote-left {
left: 4rpx;
top: 4rpx;
}
.stats-quote-right {
right: 4rpx;
bottom: 4rpx;
}
.stats-grid {
position: absolute;
z-index: 1;
left: 36rpx;
right: 36rpx;
top: 22rpx;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.stats-item {
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.stats-value-row {
display: flex;
align-items: flex-end;
justify-content: center;
width: 100%;
height: 48rpx;
line-height: 48rpx;
}
.stats-value-group {
position: relative;
display: inline-flex;
align-items: flex-end;
justify-content: center;
min-width: 72rpx;
white-space: nowrap;
}
.stats-value-decoration {
position: absolute;
left: 0;
right: 0;
bottom: 6rpx;
width: auto;
min-width: 72rpx;
height: 12rpx;
border-radius: 6rpx;
background: linear-gradient(133deg, #ffd19a 0%, #a17636 100%);
opacity: 0.5;
}
.stats-value {
position: relative;
z-index: 1;
color: #fff;
font-size: 34rpx;
font-family: Helvetica, Arial, sans-serif;
font-weight: 500;
line-height: 48rpx;
text-align: left;
font-style: normal;
}
.stats-unit {
position: relative;
z-index: 1;
margin-left: 4rpx;
padding-bottom: 8rpx;
color: #ffffff;
font-size: 20rpx;
font-weight: 400;
line-height: 28rpx;
text-align: left;
font-style: normal;
opacity: 0.6;
}
.stats-label {
display: inline-block;
margin-top: 6rpx;
color: #fcce96;
font-size: 20rpx;
font-weight: 400;
line-height: 28rpx;
text-align: right;
font-style: normal;
white-space: nowrap;
opacity: 0.6;
}
.radar-section {
position: relative;
padding-top: 34rpx;
}
.record-bubble {
position: absolute;
right: 0;
top: 10rpx;
width: 202rpx;
height: 122rpx;
z-index: 3;
}
.record-bubble-bg {
width: 202rpx;
}
.record-bubble-copy {
position: absolute;
left: 0;
right: 0;
top: 18rpx;
text-align: center;
}
.record-main {
color: #fff;
font-size: 24rpx;
line-height: 30rpx;
}
.record-main-highlight {
color: #e7ba80;
}
.record-sub-row {
margin-top: 4rpx;
display: flex;
align-items: center;
justify-content: center;
}
.record-sub-text {
color: #ffd947;
font-size: 24rpx;
height: 30rpx;
line-height: 30rpx;
}
.record-arrow {
width: 12rpx;
margin-left: 8rpx;
}
.radar-board {
position: relative;
width: 100%;
height: 514rpx;
}
.radar-label {
position: absolute;
color: rgba(255, 255, 255, 0.78);
font-size: 28rpx;
line-height: 40rpx;
}
.radar-label-top {
left: 350rpx;
top: 14rpx;
transform: translateX(-50%);
opacity: 0.5;
}
.radar-label-right {
right: 77rpx;
top: 190rpx;
opacity: 0.5;
}
.radar-label-bottom-right {
right: 170rpx;
bottom: 18rpx;
opacity: 0.5;
}
.radar-label-bottom-left {
left: 170rpx;
bottom: 18rpx;
opacity: 0.5;
}
.radar-label-left {
left: 75rpx;
top: 180rpx;
opacity: 0.5;
}
.radar-figure {
position: absolute;
left: 50%;
top: 54rpx;
transform: translateX(-50%);
overflow: visible;
}
.radar-grid-image,
.radar-canvas {
position: absolute;
left: 0;
top: 0;
}
.radar-mascot {
position: absolute;
right: 38rpx;
top: 0;
width: 92rpx;
}
.featured-card {
position: relative;
width: 100%;
height: 150rpx;
margin-top: 70rpx;
}
.featured-card-bg {
width: 100%;
}
.featured-card-copy {
position: absolute;
left: 160rpx;
top: 68rpx;
display: flex;
align-items: center;
}
.featured-card-progress {
margin-left: 18rpx;
color: #895409;
font-size: 22rpx;
line-height: 32rpx;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx 18rpx;
margin-top: 16rpx;
}
.mode-card {
position: relative;
height: 150rpx;
box-shadow: inset 2rpx 2rpx 6rpx 0rpx rgba(255,255,255,0.27);
border-radius: 16rpx;
border: 2rpx solid rgba(235, 184, 123, 0.5);
background: rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.mode-card-bg {
width: 100%;
}
.mode-tag {
position: absolute;
left: 0;
top: 0;
width: 72rpx;
background: linear-gradient( 133deg, #FFD19A 0%, #A17636 100%);
height: 34rpx;
line-height: 34rpx;
text-align: center;
font-size: 20rpx;
color: #000;
border-bottom-right-radius: 16rpx;
}
.mode-card-copy {
position: absolute;
left: 30rpx;
top: 40rpx;
}
.mode-card-title {
display: block;
background-image: linear-gradient(
133deg,
rgba(235, 184, 123, 0.8) 0%,
rgba(181, 140, 78, 0.8) 100%
);
color: #e7ba80;
font-size: 32rpx;
font-family: "AlimamaShuHeiTi-Bold", "PingFang SC", sans-serif;
font-weight: 700;
line-height: 38rpx;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.mode-card-progress {
display: block;
margin-top: 14rpx;
color: #fcce96;
font-size: 22rpx;
line-height: 32rpx;
opacity: 0.5;
}
.mode-card-icon {
position: absolute;
right: 12rpx;
top: 14rpx;
width: 124rpx;
height: 124rpx;
}
</style>