个人训练改版首页存档
@@ -57,6 +57,12 @@ const props = defineProps({
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/rank/rank-bg.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
class="bg-image"
|
||||
v-if="type === 7"
|
||||
src="@/static/app-bg6.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="bg-overlay" v-if="type === 0"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
200
src/components/training/AbilityRadarPanel.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<view class="radar-panel">
|
||||
<canvas
|
||||
:canvas-id="canvasId"
|
||||
:id="canvasId"
|
||||
class="radar-canvas"
|
||||
:style="{ width: `${displayWidth}rpx`, height: `${displayHeight}rpx` }"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
/>
|
||||
|
||||
<text class="radar-label top">{{ labels[0] }}</text>
|
||||
<text class="radar-label right">{{ labels[1] }}</text>
|
||||
<text class="radar-label bottom-right">{{ labels[2] }}</text>
|
||||
<text class="radar-label bottom-left">{{ labels[3] }}</text>
|
||||
<text class="radar-label left">{{ labels[4] }}</text>
|
||||
|
||||
<TrainingRecordBubble
|
||||
:surpass-text="surpassText"
|
||||
:record-text="recordText"
|
||||
@record="$emit('record')"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, nextTick, onMounted, watch } from 'vue'
|
||||
import TrainingRecordBubble from './TrainingRecordBubble.vue'
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
maxValue: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
surpassText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
recordText: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['record'])
|
||||
|
||||
const canvasId = 'training-radar'
|
||||
const canvasWidth = 280
|
||||
const canvasHeight = 230
|
||||
const displayWidth = 560
|
||||
const displayHeight = 460
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
function getPoint(centerX, centerY, radius, angle) {
|
||||
return {
|
||||
x: centerX + radius * Math.cos(angle),
|
||||
y: centerY + radius * Math.sin(angle)
|
||||
}
|
||||
}
|
||||
|
||||
function drawRadar() {
|
||||
const target = instance?.proxy
|
||||
const ctx = uni.createCanvasContext(canvasId, target)
|
||||
const centerX = canvasWidth / 2
|
||||
const centerY = 118
|
||||
const layerCount = 4
|
||||
const outerRadius = 88
|
||||
const angles = props.labels.map((_, index) => (-90 + index * 72) * (Math.PI / 180))
|
||||
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||
|
||||
for (let layer = layerCount; layer >= 1; layer -= 1) {
|
||||
const radius = (outerRadius / layerCount) * layer
|
||||
ctx.beginPath()
|
||||
angles.forEach((angle, index) => {
|
||||
const point = getPoint(centerX, centerY, radius, angle)
|
||||
if (index === 0) {
|
||||
ctx.moveTo(point.x, point.y)
|
||||
} else {
|
||||
ctx.lineTo(point.x, point.y)
|
||||
}
|
||||
})
|
||||
ctx.closePath()
|
||||
ctx.setStrokeStyle(layer === layerCount ? 'rgba(125,107,83,0.75)' : 'rgba(125,107,83,0.4)')
|
||||
ctx.setLineWidth(1)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
angles.forEach((angle) => {
|
||||
const point = getPoint(centerX, centerY, outerRadius, angle)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(centerX, centerY)
|
||||
ctx.lineTo(point.x, point.y)
|
||||
ctx.setStrokeStyle('rgba(125,107,83,0.36)')
|
||||
ctx.setLineWidth(1)
|
||||
ctx.stroke()
|
||||
})
|
||||
|
||||
const points = props.values.map((value, index) => {
|
||||
const radius = (Math.max(0, Math.min(value, props.maxValue)) / props.maxValue) * outerRadius
|
||||
return getPoint(centerX, centerY, radius, 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(212, 156, 89, 0.12)')
|
||||
ctx.fill()
|
||||
ctx.setStrokeStyle('rgba(212, 156, 89, 0.95)')
|
||||
ctx.setLineWidth(2)
|
||||
ctx.stroke()
|
||||
|
||||
points.forEach((point) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI)
|
||||
ctx.setFillStyle('rgba(212, 156, 89, 1)')
|
||||
ctx.fill()
|
||||
})
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(centerX, centerY, 4, 0, 2 * Math.PI)
|
||||
ctx.setFillStyle('rgba(125,107,83,0.7)')
|
||||
ctx.fill()
|
||||
|
||||
ctx.draw()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(drawRadar)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.values,
|
||||
() => {
|
||||
nextTick(drawRadar)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.radar-panel {
|
||||
position: relative;
|
||||
margin-top: 24rpx;
|
||||
padding: 26rpx 0 18rpx;
|
||||
min-height: 560rpx;
|
||||
}
|
||||
|
||||
.radar-canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.radar-label {
|
||||
position: absolute;
|
||||
color: rgba(243, 224, 185, 0.8);
|
||||
font-size: 34rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.top {
|
||||
top: 26rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 208rpx;
|
||||
right: 28rpx;
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
right: 140rpx;
|
||||
bottom: 86rpx;
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
left: 142rpx;
|
||||
bottom: 86rpx;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 208rpx;
|
||||
left: 28rpx;
|
||||
}
|
||||
</style>
|
||||
139
src/components/training/FeaturedTrainingBanner.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view class="featured-banner" @tap="$emit('tap-banner')">
|
||||
<view class="banner-copy">
|
||||
<text class="banner-title">{{ title }}</text>
|
||||
<text class="banner-progress">{{ progressText }}</text>
|
||||
<text class="banner-desc">{{ description }}</text>
|
||||
</view>
|
||||
|
||||
<view class="banner-ornament">
|
||||
<view class="ornament-ring ring-large"></view>
|
||||
<view class="ornament-ring ring-small"></view>
|
||||
<view class="ornament-core">训</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
progressText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['tap-banner'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.featured-banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 22rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
border-radius: 26rpx;
|
||||
background:
|
||||
radial-gradient(circle at 78% 25%, rgba(255, 238, 186, 0.82), rgba(255, 238, 186, 0) 26%),
|
||||
linear-gradient(135deg, #f3ca98 0%, #e8be8d 48%, #e6b97f 100%);
|
||||
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.featured-banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -36rpx;
|
||||
bottom: -52rpx;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255, 217, 71, 0.55) 0%, rgba(255, 217, 71, 0) 70%);
|
||||
}
|
||||
|
||||
.banner-copy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 390rpx;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
display: inline-block;
|
||||
color: #b86c11;
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.banner-progress {
|
||||
display: inline-block;
|
||||
margin-left: 14rpx;
|
||||
color: #895409;
|
||||
font-size: 32rpx;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: rgba(137, 84, 9, 0.94);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.banner-ornament {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 152rpx;
|
||||
height: 112rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ornament-ring {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.ring-large {
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 124rpx;
|
||||
height: 124rpx;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.ring-small {
|
||||
right: 32rpx;
|
||||
top: 18rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-color: rgba(184, 108, 17, 0.5);
|
||||
}
|
||||
|
||||
.ornament-core {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
top: 24rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, rgba(255, 217, 71, 1) 0%, rgba(231, 186, 128, 1) 100%);
|
||||
color: #6f4300;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 20rpx rgba(184, 108, 17, 0.28);
|
||||
}
|
||||
</style>
|
||||
147
src/components/training/HomeNavBar.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<view class="nav-shell" :style="{ paddingTop: `${statusBarHeight}px` }">
|
||||
<view class="top-row">
|
||||
<text class="time-text">{{ currentTime }}</text>
|
||||
<view class="capsule">
|
||||
<view class="capsule-dots">
|
||||
<text class="dot"></text>
|
||||
<text class="dot active"></text>
|
||||
<text class="dot"></text>
|
||||
</view>
|
||||
<view class="capsule-divider"></view>
|
||||
<view class="capsule-circle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<button class="back-button" hover-class="none" @tap="$emit('back')">
|
||||
<image class="back-icon" src="/static/training-home/icons/back.svg" mode="aspectFit" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['back'])
|
||||
|
||||
const currentTime = ref('9:41')
|
||||
const statusBarHeight = ref(24)
|
||||
let timer = null
|
||||
|
||||
function formatTime(date) {
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
function syncTime() {
|
||||
currentTime.value = formatTime(new Date())
|
||||
}
|
||||
|
||||
function syncSafeArea() {
|
||||
const info = typeof uni.getSystemInfoSync === 'function' ? uni.getSystemInfoSync() : null
|
||||
statusBarHeight.value = info?.statusBarHeight || 24
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncSafeArea()
|
||||
syncTime()
|
||||
timer = setInterval(syncTime, 60 * 1000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-shell {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-left: 20rpx;
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 176rpx;
|
||||
height: 58rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 44rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.capsule-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.capsule-divider {
|
||||
width: 1rpx;
|
||||
height: 26rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.capsule-circle {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: inset 0 0 0 4rpx rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 18rpx;
|
||||
min-height: 48rpx;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
</style>
|
||||
32
src/components/training/RecommendBadge.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<view class="badge">
|
||||
<text class="badge-text">{{ text }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '推荐'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 8rpx 16rpx 6rpx;
|
||||
border-radius: 16rpx 4rpx 20rpx 4rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 209, 154, 1) 0%, rgba(161, 118, 54, 1) 100%);
|
||||
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
color: #000000;
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
89
src/components/training/TrainingModeCard.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<view class="mode-card" :class="{ disabled }" @tap="$emit('select')">
|
||||
<RecommendBadge v-if="recommended" />
|
||||
|
||||
<view class="copy-wrap">
|
||||
<text class="mode-title">{{ title }}</text>
|
||||
<text class="mode-subtitle">{{ subtitle }}</text>
|
||||
</view>
|
||||
|
||||
<image class="mode-icon" :src="icon" mode="aspectFit" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RecommendBadge from './RecommendBadge.vue'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
recommended: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mode-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 148rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 1rpx solid rgba(255, 217, 71, 0.24);
|
||||
background:
|
||||
radial-gradient(circle at 78% 26%, rgba(255, 244, 205, 0.14), rgba(255, 244, 205, 0) 25%),
|
||||
linear-gradient(180deg, rgba(34, 34, 46, 0.92) 0%, rgba(17, 19, 27, 0.96) 100%);
|
||||
box-shadow: inset 0 2rpx 8rpx rgba(255, 255, 255, 0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mode-card.disabled {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.copy-wrap {
|
||||
max-width: 250rpx;
|
||||
}
|
||||
|
||||
.mode-title {
|
||||
display: block;
|
||||
color: rgba(231, 186, 128, 1);
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.mode-subtitle {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: rgba(252, 206, 150, 0.78);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
36
src/components/training/TrainingModeGrid.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<view class="grid-wrap">
|
||||
<TrainingModeCard
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
:title="item.title"
|
||||
:subtitle="item.subtitle"
|
||||
:icon="item.icon"
|
||||
:recommended="item.recommended"
|
||||
:disabled="item.disabled"
|
||||
@select="$emit('select', item)"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TrainingModeCard from './TrainingModeCard.vue'
|
||||
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
</style>
|
||||
86
src/components/training/TrainingRecordBubble.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<view class="bubble-wrap" @tap="$emit('record')">
|
||||
<view class="bubble-body">
|
||||
<text class="bubble-main">{{ surpassText }}</text>
|
||||
<view class="bubble-row">
|
||||
<text class="bubble-sub">{{ recordText }}</text>
|
||||
<text class="bubble-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bubble-badge">L</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
surpassText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
recordText: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['record'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bubble-wrap {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
padding: 14rpx 18rpx;
|
||||
border: 2rpx dashed rgba(255, 255, 255, 0.24);
|
||||
border-radius: 32rpx;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bubble-main {
|
||||
display: block;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bubble-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.bubble-sub {
|
||||
color: rgba(255, 217, 71, 1);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
margin-left: 8rpx;
|
||||
color: rgba(255, 217, 71, 1);
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bubble-badge {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, rgba(255, 217, 71, 1) 0%, rgba(231, 186, 128, 1) 100%);
|
||||
color: #6f4300;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 16rpx rgba(255, 217, 71, 0.22);
|
||||
}
|
||||
</style>
|
||||
87
src/components/training/TrainingStatSummary.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<view class="summary-card">
|
||||
<text class="quote quote-left">“</text>
|
||||
<text class="quote quote-right">”</text>
|
||||
|
||||
<view v-for="item in items" :key="item.key" class="summary-item">
|
||||
<view class="value-row">
|
||||
<text class="value-text">{{ item.value }}</text>
|
||||
<text class="unit-text">{{ item.unit }}</text>
|
||||
</view>
|
||||
<text class="label-text">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin-top: 28rpx;
|
||||
padding: 30rpx 18rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(34, 34, 46, 0.76);
|
||||
box-shadow: inset 0 2rpx 6rpx rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 2rpx;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
color: #ffffff;
|
||||
font-size: 50rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
color: rgba(243, 224, 185, 1);
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
color: rgba(252, 206, 150, 1);
|
||||
font-size: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quote {
|
||||
position: absolute;
|
||||
color: rgba(255, 255, 255, 0.08);
|
||||
font-size: 66rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.quote-left {
|
||||
left: 12rpx;
|
||||
top: 4rpx;
|
||||
}
|
||||
|
||||
.quote-right {
|
||||
right: 16rpx;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
97
src/components/training/WeekCheckinBar.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<view class="week-bar">
|
||||
<view
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
class="day-card"
|
||||
:class="[`status-${item.status}`]"
|
||||
>
|
||||
<view class="icon-wrap">
|
||||
<image
|
||||
v-if="item.status === 'done'"
|
||||
class="state-icon"
|
||||
src="/static/training-home/icons/check.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
v-else-if="item.status === 'failed'"
|
||||
class="state-icon"
|
||||
src="/static/training-home/icons/close.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="day-text">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.week-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.day-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 96rpx;
|
||||
padding: 14rpx 0 10rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(180deg, rgba(47, 45, 43, 0.9) 0%, rgba(37, 40, 49, 0.92) 100%);
|
||||
box-shadow: inset 0 2rpx 6rpx rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-done .icon-wrap {
|
||||
background: linear-gradient(180deg, rgba(255, 217, 71, 1) 0%, rgba(231, 186, 128, 1) 100%);
|
||||
}
|
||||
|
||||
.status-failed .icon-wrap {
|
||||
background: rgba(202, 202, 202, 0.92);
|
||||
}
|
||||
|
||||
.status-current .icon-wrap {
|
||||
border: 2rpx solid rgba(255, 217, 71, 0.45);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
}
|
||||
|
||||
.day-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.status-done .day-text {
|
||||
color: rgba(231, 186, 128, 1);
|
||||
}
|
||||
|
||||
.status-current .day-text {
|
||||
color: rgba(255, 217, 71, 1);
|
||||
}
|
||||
</style>
|
||||
104
src/mock/index.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// 首页一周打卡展示数据,直接对应顶部 7 个日期卡片。
|
||||
export const trainingHomeWeekSchedule = [
|
||||
{
|
||||
key: "mon",
|
||||
label: "周一",
|
||||
status: "done",
|
||||
icon: "../../static/training-home/slices/done.png",
|
||||
},
|
||||
{
|
||||
key: "tue",
|
||||
label: "周二",
|
||||
status: "done",
|
||||
icon: "../../static/training-home/slices/done.png",
|
||||
},
|
||||
{
|
||||
key: "wed",
|
||||
label: "周三",
|
||||
status: "missed",
|
||||
icon: "../../static/training-home/slices/missed.png",
|
||||
},
|
||||
{
|
||||
key: "thu",
|
||||
label: "周四",
|
||||
status: "missed",
|
||||
icon: "../../static/training-home/slices/missed.png",
|
||||
},
|
||||
{
|
||||
key: "fri",
|
||||
label: "周五",
|
||||
status: "done",
|
||||
icon: "../../static/training-home/slices/done.png",
|
||||
},
|
||||
{
|
||||
key: "sat",
|
||||
label: "周六",
|
||||
status: "done",
|
||||
icon: "../../static/training-home/slices/done.png",
|
||||
},
|
||||
{
|
||||
key: "sun",
|
||||
label: "周日",
|
||||
status: "missed",
|
||||
icon: "../../static/training-home/slices/missed.png",
|
||||
},
|
||||
];
|
||||
|
||||
// 首页统计卡数据,按设计稿从左到右展示。
|
||||
export const trainingHomeStats = [
|
||||
{ key: "days", value: "12", unit: "天", label: "共训练" },
|
||||
{ key: "shots", value: "112", unit: "支", label: "累计射箭" },
|
||||
{ key: "hitRate", value: "30", unit: "%", label: "命中率" },
|
||||
{ key: "endurance", value: "6", unit: "支/分钟", label: "耐力射击" },
|
||||
{ key: "calories", value: "31W", unit: "卡路里", label: "共消耗" },
|
||||
];
|
||||
|
||||
// 雷达图区文案与数值配置。
|
||||
export const trainingHomeRadar = {
|
||||
labels: ["基础", "精准", "力量", "节奏", "耐力"],
|
||||
values: [5.5, 6.3, 10, 4.5, 6],
|
||||
maxValue: 10,
|
||||
surpassValue: '80%'
|
||||
};
|
||||
|
||||
// 首页主推荐训练卡数据。
|
||||
export const trainingHomeFeatured = {
|
||||
title: "基础训练",
|
||||
progressText: "当前进度 LV7 >",
|
||||
};
|
||||
|
||||
// 首页四个训练入口卡片数据。
|
||||
export const trainingHomeModes = [
|
||||
{
|
||||
key: "endurance",
|
||||
title: "耐力训练",
|
||||
progressText: "当前进度 LV5 >",
|
||||
icon: "../../static/training-home/slices/img_3.png",
|
||||
recommended: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "precision",
|
||||
title: "精准训练",
|
||||
progressText: "当前进度 LV3 >",
|
||||
icon: "../../static/training-home/slices/img_4.png",
|
||||
recommended: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "rhythm",
|
||||
title: "节奏训练",
|
||||
progressText: "当前进度 LV6 >",
|
||||
icon: "../../static/training-home/slices/img_5.png",
|
||||
recommended: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "power",
|
||||
title: "力量训练",
|
||||
progressText: "Coming! LV10",
|
||||
icon: "../../static/training-home/slices/img_6.png",
|
||||
recommended: false,
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
@@ -116,6 +116,9 @@
|
||||
},
|
||||
{
|
||||
"path": "pages/mine-bow-data"
|
||||
},
|
||||
{
|
||||
"path": "pages/training/index"
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -196,7 +196,8 @@ onShareTimeline(() => {
|
||||
</BubbleTip>
|
||||
</view>
|
||||
<view class="play-card">
|
||||
<view @click="() => toPage('/pages/practise')">
|
||||
<!-- toPage('/pages/practise') -->
|
||||
<view @click="() => toPage('/pages/training/index')">
|
||||
<image src="../static/my-practise.png" mode="widthFix"/>
|
||||
</view>
|
||||
<view @click="() => toPage('/pages/friend-battle')">
|
||||
|
||||
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>
|
||||
BIN
src/static/app-bg6.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/static/training-home/slices/done.png
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
src/static/training-home/slices/img_16.png
Normal file
|
After Width: | Height: | Size: 184 B |
BIN
src/static/training-home/slices/img_17.png
Normal file
|
After Width: | Height: | Size: 192 B |
BIN
src/static/training-home/slices/img_19.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static/training-home/slices/img_20.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/static/training-home/slices/img_21.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/static/training-home/slices/img_22.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/static/training-home/slices/img_28.png
Normal file
|
After Width: | Height: | Size: 468 B |
BIN
src/static/training-home/slices/img_3.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static/training-home/slices/img_4.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/static/training-home/slices/img_5.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/static/training-home/slices/img_6.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static/training-home/slices/img_7.png
Normal file
|
After Width: | Height: | Size: 173 B |
BIN
src/static/training-home/slices/missed.png
Normal file
|
After Width: | Height: | Size: 351 B |