963 lines
24 KiB
Vue
963 lines
24 KiB
Vue
<script setup>
|
|
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 =
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/done.png";
|
|
const missedIcon =
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/missed.png";
|
|
// 后端训练项目 id 与难度页 mode 参数的映射关系。
|
|
const trainingModeRouteMap = {
|
|
base: "basic",
|
|
endurance: "endurance",
|
|
precision: "precision",
|
|
rhythm: "rhythm",
|
|
strength: "power",
|
|
};
|
|
// 训练项目卡片右侧主图标。
|
|
const trainingModeIconMap = {
|
|
base_bow:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_3.png",
|
|
bow: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_4.png",
|
|
target:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_5.png",
|
|
wave: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_6.png",
|
|
muscle:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_6.png",
|
|
};
|
|
// 训练项目卡片标题图,按接口 id 映射 CDN 资源。
|
|
const trainingModeTitleImageMap = {
|
|
base:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/jichuxunlian.png",
|
|
endurance:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/nailixunlian.png",
|
|
precision:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/jingzhunxunlian.png",
|
|
rhythm:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/jiezouxunlian.png",
|
|
strength:
|
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/liliangxulian.png",
|
|
};
|
|
const defaultWeekDays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
|
const defaultRadarDimensions = [
|
|
{ name: "基础", score: 0 },
|
|
{ name: "精准", score: 0 },
|
|
{ name: "力量", score: 0 },
|
|
{ name: "节奏", score: 0 },
|
|
{ name: "耐力", score: 0 },
|
|
];
|
|
const radarDimensionTrainingIdMap = Object.freeze({
|
|
基础: "base",
|
|
精准: "precision",
|
|
力量: "strength",
|
|
节奏: "rhythm",
|
|
耐力: "endurance",
|
|
});
|
|
|
|
// 页面始终直接消费接口字段,这里只保留一份兜底结构,避免模板访问空值。
|
|
const createDefaultTrainingData = () => ({
|
|
week_days: defaultWeekDays.map((day) => ({ day, status: "cross" })),
|
|
stats: {
|
|
total_training_days: 0,
|
|
total_arrows: 0,
|
|
hit_rate: 0,
|
|
endurance_shoot_speed: 0,
|
|
total_calories: 0,
|
|
overtake_rate: 0,
|
|
},
|
|
radar_max: 0,
|
|
radar: {
|
|
dimensions: defaultRadarDimensions,
|
|
},
|
|
training_items: [],
|
|
});
|
|
|
|
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";
|
|
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);
|
|
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 formatValue = (value, digits = 1) => {
|
|
const numberValue = Number(value);
|
|
if (!Number.isFinite(numberValue)) return "--";
|
|
return String(Number(numberValue.toFixed(digits)));
|
|
};
|
|
|
|
const getLevelText = (item) => {
|
|
if (!item) return "";
|
|
const level = Number(item.current_level) || 0;
|
|
return item.is_locked ? `Coming! LV${level}` : `当前进度 LV${level} >`;
|
|
};
|
|
|
|
// 卡路里字段按需求做 K / W 缩写展示。
|
|
const getCaloriesValue = (value) => {
|
|
const numberValue = Number(value);
|
|
if (!Number.isFinite(numberValue)) return "--";
|
|
if (numberValue >= 10000) return `${formatValue(numberValue / 10000)}W`;
|
|
if (numberValue >= 1000) return `${formatValue(numberValue / 1000)}K`;
|
|
return formatValue(numberValue, 0);
|
|
};
|
|
|
|
const getTrainingIcon = (item = {}) =>
|
|
trainingModeIconMap[item.icon] || trainingModeIconMap.bow;
|
|
|
|
const getTrainingTitleImage = (item = {}) =>
|
|
trainingModeTitleImageMap[item.id] || "";
|
|
|
|
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,按后端 radar_max 等比映射到顶点位置。
|
|
const drawRadar = () => {
|
|
const dimensions = Array.isArray(trainingData.value.radar?.dimensions)
|
|
? trainingData.value.radar.dimensions.slice(0, 5)
|
|
: [];
|
|
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)
|
|
);
|
|
|
|
const points = dimensions.map((item, index) => {
|
|
const normalized = Math.max(
|
|
0,
|
|
Math.min(Number(item.score) || 0, radarMaxValue)
|
|
);
|
|
const progress = normalized / radarMaxValue;
|
|
|
|
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);
|
|
else 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();
|
|
};
|
|
|
|
// 小程序 canvas 首次渲染时机不稳定,延后一帧再绘制更稳。
|
|
const refreshRadar = async () => {
|
|
await nextTick();
|
|
setTimeout(() => {
|
|
drawRadar();
|
|
}, 30);
|
|
};
|
|
|
|
const loadPersonalTrainingData = async () => {
|
|
try {
|
|
const result = await getPersonalTrainingAPI();
|
|
trainingData.value = {
|
|
week_days:
|
|
Array.isArray(result?.week_days) && result.week_days.length
|
|
? result.week_days
|
|
: createDefaultTrainingData().week_days,
|
|
stats: {
|
|
total_training_days: result?.stats?.total_training_days ?? 0,
|
|
total_arrows: result?.stats?.total_arrows ?? 0,
|
|
hit_rate: result?.stats?.hit_rate ?? 0,
|
|
endurance_shoot_speed: result?.stats?.endurance_shoot_speed ?? 0,
|
|
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) &&
|
|
result.radar.dimensions.length === 5
|
|
? result.radar.dimensions
|
|
: createDefaultTrainingData().radar.dimensions,
|
|
},
|
|
training_items: Array.isArray(result?.training_items)
|
|
? result.training_items
|
|
: [],
|
|
};
|
|
} catch (error) {
|
|
console.log("personal training load failed", error);
|
|
trainingData.value = createDefaultTrainingData();
|
|
} finally {
|
|
updateRecommendedTraining();
|
|
await refreshRadar();
|
|
}
|
|
};
|
|
|
|
const openTrainingRecord = () => {
|
|
uni.navigateTo({
|
|
url: "/pages/my-growth?tab=2",
|
|
});
|
|
};
|
|
|
|
const openTrainingItem = (item = {}) => {
|
|
const mode = getTrainingMode(item);
|
|
if (!mode) return;
|
|
|
|
if (item.is_locked) {
|
|
uni.showToast({
|
|
title: `${item.name || "训练"} 暂未开放`,
|
|
icon: "none",
|
|
});
|
|
return;
|
|
}
|
|
|
|
uni.navigateTo({
|
|
url: `/pages/training/difficulty?mode=${mode}`,
|
|
});
|
|
};
|
|
|
|
const openRoutineTraining = () => {
|
|
showRoutineTargetPicker.value = true;
|
|
};
|
|
|
|
const handleRoutineTargetConfirm = (target) => {
|
|
showRoutineTargetPicker.value = false;
|
|
uni.navigateTo({
|
|
url: `/pages/practise-one?target=${target}`,
|
|
});
|
|
};
|
|
|
|
// 首次进入页面时拉取数据并完成雷达图初始化。
|
|
onMounted(async () => {
|
|
await loadPersonalTrainingData();
|
|
pageMounted.value = true;
|
|
});
|
|
|
|
// 从其他页面返回时刷新训练数据,保持进度与推荐状态最新。
|
|
onShow(async () => {
|
|
if (!pageMounted.value) return;
|
|
await loadPersonalTrainingData();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Container :showBackToGame="true" :bgType="7" bgColor="#050b19">
|
|
<view class="training-home">
|
|
<view class="week-grid">
|
|
<view
|
|
v-for="item in trainingData.week_days"
|
|
:key="item.day"
|
|
class="week-item"
|
|
>
|
|
<view class="week-item-bg"></view>
|
|
<image
|
|
class="week-item-icon"
|
|
:src="item.status === 'checked' ? checkedIcon : missedIcon"
|
|
mode="widthFix"
|
|
/>
|
|
<text
|
|
class="week-item-label"
|
|
:class="{ 'week-item-label-active': item.status === 'checked' }"
|
|
>
|
|
{{ item.day }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="stats-card">
|
|
<view class="stats-card-bg"></view>
|
|
<image
|
|
class="stats-quote stats-quote-left"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_17.png"
|
|
mode="widthFix"
|
|
/>
|
|
<image
|
|
class="stats-quote stats-quote-right"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_16.png"
|
|
mode="widthFix"
|
|
/>
|
|
<view class="stats-grid">
|
|
<view class="stats-item">
|
|
<view class="stats-value-row">
|
|
<view class="stats-value-group">
|
|
<text class="stats-value">
|
|
{{ formatValue(trainingData.stats.total_training_days, 0) }}
|
|
</text>
|
|
<text class="stats-unit">天</text>
|
|
<view class="stats-value-decoration"></view>
|
|
</view>
|
|
</view>
|
|
<text class="stats-label">共训练</text>
|
|
</view>
|
|
|
|
<view class="stats-item">
|
|
<view class="stats-value-row">
|
|
<view class="stats-value-group">
|
|
<text class="stats-value">
|
|
{{ formatValue(trainingData.stats.total_arrows, 0) }}
|
|
</text>
|
|
<text class="stats-unit">支</text>
|
|
<view class="stats-value-decoration"></view>
|
|
</view>
|
|
</view>
|
|
<text class="stats-label">累计射箭</text>
|
|
</view>
|
|
|
|
<view class="stats-item">
|
|
<view class="stats-value-row">
|
|
<view class="stats-value-group">
|
|
<text class="stats-value">
|
|
{{ formatValue(trainingData.stats.hit_rate) }}
|
|
</text>
|
|
<text class="stats-unit">%</text>
|
|
<view class="stats-value-decoration"></view>
|
|
</view>
|
|
</view>
|
|
<text class="stats-label">命中率</text>
|
|
</view>
|
|
|
|
<view class="stats-item">
|
|
<view class="stats-value-row">
|
|
<view class="stats-value-group">
|
|
<text class="stats-value">
|
|
{{ formatValue(trainingData.stats.endurance_shoot_speed, 0) }}
|
|
</text>
|
|
<text class="stats-unit">支/分钟</text>
|
|
<view class="stats-value-decoration"></view>
|
|
</view>
|
|
</view>
|
|
<text class="stats-label">耐力射击</text>
|
|
</view>
|
|
|
|
<view class="stats-item">
|
|
<view class="stats-value-row">
|
|
<view class="stats-value-group">
|
|
<text class="stats-value">
|
|
{{ getCaloriesValue(trainingData.stats.total_calories) }}
|
|
</text>
|
|
<text class="stats-unit">卡路里</text>
|
|
<view class="stats-value-decoration"></view>
|
|
</view>
|
|
</view>
|
|
<text class="stats-label">共消耗</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="radar-section">
|
|
<view class="record-bubble" @click="openTrainingRecord">
|
|
<image
|
|
class="record-bubble-bg"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_28.png"
|
|
mode="widthFix"
|
|
/>
|
|
<view class="record-bubble-copy">
|
|
<view class="record-main">
|
|
已超越<text class="record-main-highlight">{{ formatValue(trainingData.stats.overtake_rate) }}%</text>对手
|
|
</view>
|
|
<view class="record-sub-row">
|
|
<text class="record-sub-text">我的训练记录</text>
|
|
<image
|
|
class="record-arrow"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_7.png"
|
|
mode="widthFix"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="radar-board">
|
|
<text class="radar-label radar-label-top">
|
|
{{ trainingData.radar.dimensions[0].name }}
|
|
</text>
|
|
<text class="radar-label radar-label-right">
|
|
{{ trainingData.radar.dimensions[1].name }}
|
|
</text>
|
|
<text class="radar-label radar-label-bottom-right">
|
|
{{ trainingData.radar.dimensions[2].name }}
|
|
</text>
|
|
<text class="radar-label radar-label-bottom-left">
|
|
{{ trainingData.radar.dimensions[3].name }}
|
|
</text>
|
|
<text class="radar-label radar-label-left">
|
|
{{ trainingData.radar.dimensions[4].name }}
|
|
</text>
|
|
|
|
<view class="radar-figure" :style="radarFigureStyle">
|
|
<image
|
|
class="radar-grid-image"
|
|
:style="radarFigureStyle"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_19.png"
|
|
/>
|
|
<canvas
|
|
:canvas-id="trainingRadarCanvasId"
|
|
:id="trainingRadarCanvasId"
|
|
class="radar-canvas"
|
|
:style="radarFigureStyle"
|
|
:width="radarCanvasWidth"
|
|
:height="radarCanvasHeight"
|
|
/>
|
|
<image
|
|
class="radar-mascot"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_21.png"
|
|
mode="widthFix"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="featured-card" @click="openRoutineTraining">
|
|
<image
|
|
class="featured-card-bg"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_22.png"
|
|
mode="widthFix"
|
|
/>
|
|
<view class="featured-card-mask"></view>
|
|
<view class="featured-card-copy">
|
|
<text class="featured-card-subtitle">12箭练习</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="mode-grid">
|
|
<view
|
|
v-for="item in visibleTrainingItems"
|
|
:key="item.id"
|
|
class="mode-card"
|
|
@click="openTrainingItem(item)"
|
|
>
|
|
<view v-if="item.id === recommendedTrainingId" class="mode-tag">
|
|
推荐
|
|
</view>
|
|
<view class="mode-card-copy">
|
|
<image
|
|
v-if="getTrainingTitleImage(item)"
|
|
class="mode-card-title-image"
|
|
:src="getTrainingTitleImage(item)"
|
|
mode="widthFix"
|
|
/>
|
|
<text v-else class="mode-card-title">{{ item.name }}</text>
|
|
<text class="mode-card-progress">{{ getLevelText(item) }}</text>
|
|
</view>
|
|
<image
|
|
class="mode-card-icon"
|
|
:src="getTrainingIcon(item)"
|
|
mode="aspectFit"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<TargetPicker
|
|
:show="showRoutineTargetPicker"
|
|
:onClose="() => (showRoutineTargetPicker = false)"
|
|
:onConfirm="handleRoutineTargetConfirm"
|
|
/>
|
|
</Container>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.training-home {
|
|
position: relative;
|
|
overflow: hidden;
|
|
padding: 18rpx 20rpx 60rpx 20rpx;
|
|
}
|
|
|
|
.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: rgba(255, 255, 255, 0.6);
|
|
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;
|
|
inset: 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;
|
|
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: 46rpx;
|
|
}
|
|
|
|
.stats-unit {
|
|
position: relative;
|
|
z-index: 1;
|
|
margin-left: 4rpx;
|
|
padding-bottom: 8rpx;
|
|
color: #fff;
|
|
font-size: 20rpx;
|
|
line-height: 28rpx;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.stats-label {
|
|
display: inline-block;
|
|
margin-top: 6rpx;
|
|
color: #fcce96;
|
|
font-size: 20rpx;
|
|
line-height: 28rpx;
|
|
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: 24rpx;
|
|
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: 24rpx;
|
|
}
|
|
|
|
.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;
|
|
border-radius: 16rpx;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.featured-card-bg {
|
|
width: 100%;
|
|
}
|
|
|
|
.featured-card-mask {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 278rpx;
|
|
height: 150rpx;
|
|
}
|
|
|
|
.featured-card-copy {
|
|
position: absolute;
|
|
left: 174rpx;
|
|
top: 58rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.featured-card-title {
|
|
display: block;
|
|
color: #895409;
|
|
font-size: 34rpx;
|
|
font-family: "AlimamaShuHeiTi-Bold", "PingFang SC", sans-serif;
|
|
font-weight: 700;
|
|
line-height: 42rpx;
|
|
}
|
|
|
|
.featured-card-subtitle {
|
|
display: block;
|
|
margin-top: 10rpx;
|
|
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-tag {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 72rpx;
|
|
height: 34rpx;
|
|
line-height: 34rpx;
|
|
text-align: center;
|
|
font-size: 20rpx;
|
|
color: #000;
|
|
border-bottom-right-radius: 16rpx;
|
|
background: linear-gradient(133deg, #ffd19a 0%, #a17636 100%);
|
|
}
|
|
|
|
.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-title-image {
|
|
display: block;
|
|
width: 128rpx;
|
|
}
|
|
|
|
.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>
|