个人训练改版首页存档
This commit is contained in:
701
src/pages/training/index.vue
Normal file
701
src/pages/training/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user