feat(metronome): 浅色清爽改版 + 三档位 + 阳光节拍音
整页配色从暗色系换成浅色清爽: - 背景 #f8fafc → #eef2f7 渐变 - 卡片纯白 + 柔和阴影 - 主圆 emerald 翡翠绿,重音切琥珀色 视频区域: - 移除卡片外框/光晕/阴影,保留圆形蒙版 (圆形蒙版仍负责裁掉 video 灰白边 + 隐藏微信原生右上角小窗按钮) - video 透明背景,跟白底融合 新增 3 档位推荐(主操作): - 慢走 80 BPM · 健走 110 BPM · 快走 130 BPM - 横排白卡,选中变浅绿 + emerald 描边 - 一键同步 BPM 和拍号,免去手动调 右下角微调(次要操作): - absolute 定位胶囊,7 成透明 - ± BPM 微调 + 2/3/4 拍号 - 不抢主视觉,需要精确时才点 替换节拍音色(ffmpeg 合成): - 普通拍 C6 1046Hz + C7 泛音,清脆"叮" - 重音 G6 1568Hz + D7 + G7,更明亮"叮咚" - 加 5ms attack + exp decay + 轻微回声,模拟钢片琴 - 旧的阴沉鼓点声备份在 audio/_backup/ Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,99 +17,65 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 配速 + 走路动画卡片 ===== -->
|
||||
<view class="walker-card">
|
||||
<view class="walker-header">
|
||||
<view class="badge" :class="badgeClass">
|
||||
<view class="badge-dot" />
|
||||
<text>{{ walkerTip }}</text>
|
||||
</view>
|
||||
<text class="status">{{ statusHint }}</text>
|
||||
</view>
|
||||
|
||||
<view class="walker" :class="{ playing: isPlaying }">
|
||||
<view class="runner-orb">
|
||||
<view class="orb-glow" :class="badgeClass" />
|
||||
<view class="orb-clip">
|
||||
<video
|
||||
id="runner-video"
|
||||
class="runner-video"
|
||||
:src="RUNNER_VIDEO_URL"
|
||||
:muted="true"
|
||||
:loop="true"
|
||||
:autoplay="true"
|
||||
:show-controls="false"
|
||||
:show-fullscreen-btn="false"
|
||||
:show-play-btn="false"
|
||||
:show-center-play-btn="false"
|
||||
:enable-progress-gesture="false"
|
||||
:show-mute-btn="false"
|
||||
:picture-in-picture-mode="[]"
|
||||
object-fit="cover"
|
||||
@error="onVideoError"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="walker-stats">
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">每分钟</text>
|
||||
<view class="stat-value">
|
||||
<text class="stat-num">{{ bpm }}</text>
|
||||
<text class="stat-unit">步</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">每步耗时</text>
|
||||
<view class="stat-value">
|
||||
<text class="stat-num">{{ intervalMs }}</text>
|
||||
<text class="stat-unit">ms</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">动画速度</text>
|
||||
<view class="stat-value">
|
||||
<text class="stat-num">{{ playbackRate.toFixed(2) }}</text>
|
||||
<text class="stat-unit">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- ===== 跑步小人(无边框,跟白底融合) ===== -->
|
||||
<view class="walker">
|
||||
<view class="walker-clip">
|
||||
<video
|
||||
id="runner-video"
|
||||
class="runner-video"
|
||||
:src="RUNNER_VIDEO_URL"
|
||||
:muted="true"
|
||||
:loop="true"
|
||||
:autoplay="true"
|
||||
:show-controls="false"
|
||||
:show-fullscreen-btn="false"
|
||||
:show-play-btn="false"
|
||||
:show-center-play-btn="false"
|
||||
:enable-progress-gesture="false"
|
||||
:show-mute-btn="false"
|
||||
:picture-in-picture-mode="[]"
|
||||
object-fit="cover"
|
||||
@error="onVideoError"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 控制卡片:BPM + 拍号 ===== -->
|
||||
<view class="control-card">
|
||||
<view class="bpm-row">
|
||||
<view class="step-btn" @click="adjustBpm(-10)">
|
||||
<text class="step-num">−10</text>
|
||||
</view>
|
||||
<view class="step-btn" @click="adjustBpm(-1)">
|
||||
<text class="step-num">−1</text>
|
||||
</view>
|
||||
<view class="step-btn" @click="adjustBpm(1)">
|
||||
<text class="step-num">+1</text>
|
||||
</view>
|
||||
<view class="step-btn" @click="adjustBpm(10)">
|
||||
<text class="step-num">+10</text>
|
||||
</view>
|
||||
<!-- ===== 推荐档位(3 选 1) ===== -->
|
||||
<view class="presets">
|
||||
<view
|
||||
v-for="p in PRESETS"
|
||||
:key="p.id"
|
||||
class="preset"
|
||||
:class="{ active: activePreset === p.id }"
|
||||
@click="applyPreset(p)"
|
||||
>
|
||||
<text class="preset-icon">{{ p.icon }}</text>
|
||||
<text class="preset-name">{{ p.name }}</text>
|
||||
<text class="preset-bpm">{{ p.bpm }} BPM</text>
|
||||
<text class="preset-desc">{{ p.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="divider" />
|
||||
|
||||
<view class="meter-row">
|
||||
<text class="meter-label">每组拍数</text>
|
||||
<view class="meter-options">
|
||||
<view
|
||||
v-for="m in METER_OPTIONS"
|
||||
:key="m.value"
|
||||
class="meter-btn"
|
||||
:class="{ active: accentEvery === m.value }"
|
||||
@click="onMeterChange(m.value)"
|
||||
>
|
||||
<text>{{ m.label }}</text>
|
||||
</view>
|
||||
<!-- ===== 右下角微调(不显眼) ===== -->
|
||||
<view class="quick-adjust">
|
||||
<view class="qa-group">
|
||||
<view class="qa-btn" @click.stop="adjustBpm(-1)">−</view>
|
||||
<text class="qa-label">微调</text>
|
||||
<view class="qa-btn" @click.stop="adjustBpm(1)">+</view>
|
||||
</view>
|
||||
<view class="qa-divider" />
|
||||
<view class="qa-group">
|
||||
<view
|
||||
v-for="m in METER_OPTIONS"
|
||||
:key="m.value"
|
||||
class="qa-meter"
|
||||
:class="{ active: accentEvery === m.value }"
|
||||
@click.stop="onMeterChange(m.value)"
|
||||
>
|
||||
{{ m.label }}
|
||||
</view>
|
||||
<text class="qa-label">拍</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -129,13 +95,42 @@ const RUNNER_VIDEO_URL =
|
||||
'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260525/20260525164014ef89f8862.mp4'
|
||||
|
||||
/**
|
||||
* 视频原速对应的 BPM 基准
|
||||
* runner.mp4 时长 0.97s, 一个 walk cycle (左右脚各一次) ≈ 124 BPM 步频
|
||||
* 用户调速时 playbackRate = bpm / VIDEO_BASE_BPM
|
||||
* 微信小程序限制 playback-rate 范围 [0.5, 2.0]
|
||||
* 视频原速对应的 BPM 基准(参见 hooks 实现注释)
|
||||
* playback-rate 范围 [0.5, 2.0]
|
||||
*/
|
||||
const VIDEO_BASE_BPM = 124
|
||||
|
||||
/**
|
||||
* 推荐档位:覆盖大多数老人健走场景,一键切换 BPM + 拍号
|
||||
* 不在档位的中间数值用右下角微调即可
|
||||
*/
|
||||
const PRESETS = [
|
||||
{
|
||||
id: 'slow',
|
||||
icon: '🚶',
|
||||
name: '慢走',
|
||||
bpm: 80,
|
||||
accent: 4,
|
||||
desc: '热身 · 恢复',
|
||||
},
|
||||
{
|
||||
id: 'normal',
|
||||
icon: '🏃♀️',
|
||||
name: '健走',
|
||||
bpm: 110,
|
||||
accent: 4,
|
||||
desc: '日常 · 通勤',
|
||||
},
|
||||
{
|
||||
id: 'brisk',
|
||||
icon: '🏃',
|
||||
name: '快走',
|
||||
bpm: 130,
|
||||
accent: 4,
|
||||
desc: '提速 · 燃脂',
|
||||
},
|
||||
] as const
|
||||
|
||||
const METER_OPTIONS = [
|
||||
{ label: '2', value: 2 },
|
||||
{ label: '3', value: 3 },
|
||||
@@ -151,7 +146,6 @@ const metronome = useMetronome({
|
||||
const {
|
||||
bpm,
|
||||
isPlaying,
|
||||
beatIndex,
|
||||
isAccent,
|
||||
accentEvery,
|
||||
intervalMs,
|
||||
@@ -180,6 +174,19 @@ const togglePlay = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 当前命中的档位(BPM + 拍号都匹配才算选中) */
|
||||
const activePreset = computed(() => {
|
||||
const hit = PRESETS.find(
|
||||
(p) => p.bpm === bpm.value && p.accent === accentEvery.value,
|
||||
)
|
||||
return hit?.id ?? null
|
||||
})
|
||||
|
||||
const applyPreset = (p: (typeof PRESETS)[number]) => {
|
||||
setBpm(p.bpm)
|
||||
setAccentEvery(p.accent)
|
||||
}
|
||||
|
||||
/* 视频实例与 BPM 同步 */
|
||||
let videoCtx: UniApp.VideoContext | null = null
|
||||
|
||||
@@ -190,7 +197,6 @@ const playbackRate = computed(() => {
|
||||
|
||||
onMounted(() => {
|
||||
videoCtx = uni.createVideoContext('runner-video')
|
||||
/* 进页面默认让视频保持暂停,跟节拍器 isPlaying 状态同步 */
|
||||
setTimeout(() => videoCtx?.pause(), 50)
|
||||
})
|
||||
|
||||
@@ -205,13 +211,11 @@ const onVideoError = (e: any) => {
|
||||
|
||||
const onVideoLoaded = (e: any) => {
|
||||
console.log('[runner-video] metadata loaded:', e?.detail)
|
||||
/* 元数据加载完后再设 playbackRate,部分基础库初始化时设非 1.0 会卡 */
|
||||
try {
|
||||
videoCtx?.playbackRate?.(playbackRate.value)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/* 节拍器状态变化 → 同步视频播放/暂停 */
|
||||
watch(isPlaying, (playing) => {
|
||||
if (playing) {
|
||||
videoCtx?.play()
|
||||
@@ -220,7 +224,6 @@ watch(isPlaying, (playing) => {
|
||||
}
|
||||
})
|
||||
|
||||
/* BPM 变化 → 调整视频播放速度(部分基础库需要手动 API 调用) */
|
||||
watch(playbackRate, (rate) => {
|
||||
try {
|
||||
videoCtx?.playbackRate?.(rate)
|
||||
@@ -232,29 +235,6 @@ const rootStyle = computed(() => ({
|
||||
'--step-duration': `${intervalMs.value}ms`,
|
||||
}))
|
||||
|
||||
const statusHint = computed(() => {
|
||||
if (isPlaying.value) {
|
||||
const cur = ((beatIndex.value - 1) % accentEvery.value) + 1
|
||||
return `第 ${cur} 拍 / 共 ${accentEvery.value} 拍`
|
||||
}
|
||||
return '轻点中央圆点开始'
|
||||
})
|
||||
|
||||
const walkerTip = computed(() => {
|
||||
if (bpm.value < 70) return '慢走 · 热身'
|
||||
if (bpm.value < 100) return '健走 · 日常'
|
||||
if (bpm.value < 130) return '快走 · 提速'
|
||||
if (bpm.value < 170) return '慢跑 · 有氧'
|
||||
return '快跑 · 高强'
|
||||
})
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
if (bpm.value < 100) return 'badge-mild'
|
||||
if (bpm.value < 130) return 'badge-mid'
|
||||
if (bpm.value < 170) return 'badge-strong'
|
||||
return 'badge-hot'
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
preload()
|
||||
})
|
||||
@@ -273,33 +253,37 @@ onUnmounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================================
|
||||
* 设计 token
|
||||
* 设计 token —— 浅色清爽
|
||||
* ============================================================ */
|
||||
$bg-grad: linear-gradient(180deg, #0a0f1c 0%, #141a2a 100%);
|
||||
$card-bg: rgba(255, 255, 255, 0.035);
|
||||
$card-border: rgba(255, 255, 255, 0.06);
|
||||
$text-1: #f1f5f9;
|
||||
$text-2: #cbd5e1;
|
||||
$text-3: #64748b;
|
||||
$brand: #22c55e;
|
||||
$brand-deep: #15803d;
|
||||
$bg-grad: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
||||
$card-bg: #ffffff;
|
||||
$card-border: rgba(15, 23, 42, 0.06);
|
||||
$text-1: #0f172a;
|
||||
$text-2: #475569;
|
||||
$text-3: #94a3b8;
|
||||
$brand: #10b981;
|
||||
$brand-soft: #d1fae5;
|
||||
$brand-deep: #047857;
|
||||
$accent: #f59e0b;
|
||||
$accent-deep: #b45309;
|
||||
$radius-card: 28rpx;
|
||||
$radius-btn: 20rpx;
|
||||
$accent-soft: #fef3c7;
|
||||
$radius-card: 24rpx;
|
||||
$radius-btn: 18rpx;
|
||||
$shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04);
|
||||
$shadow-md: 0 12rpx 28rpx rgba(15, 23, 42, 0.08);
|
||||
|
||||
/* ============================================================
|
||||
* 页面容器
|
||||
* ============================================================ */
|
||||
.page {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 30rpx 28rpx 60rpx;
|
||||
padding: 30rpx 28rpx 80rpx;
|
||||
background: $bg-grad;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
gap: 22rpx;
|
||||
color: $text-1;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -309,9 +293,9 @@ $radius-btn: 20rpx;
|
||||
* ============================================================ */
|
||||
.stage {
|
||||
position: relative;
|
||||
width: 540rpx;
|
||||
height: 540rpx;
|
||||
margin-top: 20rpx;
|
||||
width: 480rpx;
|
||||
height: 480rpx;
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -326,7 +310,7 @@ $radius-btn: 20rpx;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid rgba(34, 197, 94, 0.14);
|
||||
border: 3rpx solid rgba(16, 185, 129, 0.12);
|
||||
pointer-events: none;
|
||||
|
||||
&.playing {
|
||||
@@ -340,29 +324,29 @@ $radius-btn: 20rpx;
|
||||
0% {
|
||||
transform: scale(0.94);
|
||||
opacity: 0.85;
|
||||
border-color: rgba(34, 197, 94, 0.45);
|
||||
border-color: rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.18);
|
||||
opacity: 0;
|
||||
border-color: rgba(34, 197, 94, 0);
|
||||
border-color: rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.core {
|
||||
position: relative;
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
width: 380rpx;
|
||||
height: 380rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(140deg, $brand 0%, $brand-deep 100%);
|
||||
background: linear-gradient(140deg, #34d399 0%, $brand-deep 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow:
|
||||
inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.22),
|
||||
inset 0 16rpx 50rpx rgba(255, 255, 255, 0.1),
|
||||
0 20rpx 50rpx rgba(34, 197, 94, 0.32);
|
||||
inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.18),
|
||||
inset 0 16rpx 50rpx rgba(255, 255, 255, 0.18),
|
||||
0 16rpx 40rpx rgba(16, 185, 129, 0.32);
|
||||
transition: transform 0.12s, background 0.18s, box-shadow 0.18s;
|
||||
|
||||
&.playing {
|
||||
@@ -370,34 +354,34 @@ $radius-btn: 20rpx;
|
||||
}
|
||||
|
||||
&.playing.accent {
|
||||
background: linear-gradient(140deg, #fbbf24 0%, $accent-deep 100%);
|
||||
background: linear-gradient(140deg, #fbbf24 0%, #d97706 100%);
|
||||
box-shadow:
|
||||
inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.22),
|
||||
inset 0 16rpx 50rpx rgba(255, 255, 255, 0.18),
|
||||
0 20rpx 50rpx rgba(245, 158, 11, 0.4);
|
||||
inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.18),
|
||||
inset 0 16rpx 50rpx rgba(255, 255, 255, 0.22),
|
||||
0 16rpx 40rpx rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.bpm-num {
|
||||
font-size: 180rpx;
|
||||
font-size: 168rpx;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
letter-spacing: -2rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.18);
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.bpm-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 6rpx;
|
||||
margin-top: 10rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctrl {
|
||||
margin-top: 26rpx;
|
||||
height: 76rpx;
|
||||
margin-top: 22rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -413,336 +397,204 @@ $radius-btn: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 播放/暂停 icon (CSS 几何) */
|
||||
/* 播放/暂停 icon */
|
||||
.icon-play {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 60rpx solid #fff;
|
||||
border-top: 38rpx solid transparent;
|
||||
border-bottom: 38rpx solid transparent;
|
||||
margin-left: 14rpx;
|
||||
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.2));
|
||||
border-left: 52rpx solid #fff;
|
||||
border-top: 32rpx solid transparent;
|
||||
border-bottom: 32rpx solid transparent;
|
||||
margin-left: 12rpx;
|
||||
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
.icon-pause {
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
height: 70rpx;
|
||||
gap: 16rpx;
|
||||
height: 60rpx;
|
||||
|
||||
.bar {
|
||||
width: 18rpx;
|
||||
width: 16rpx;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 5rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 走路动画卡片
|
||||
* 跑步小人(无边框,跟白底融合)
|
||||
* ============================================================ */
|
||||
.walker-card {
|
||||
width: 100%;
|
||||
background: $card-bg;
|
||||
border: 1rpx solid $card-border;
|
||||
border-radius: $radius-card;
|
||||
padding: 24rpx 28rpx 32rpx;
|
||||
backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
.walker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
border: 1rpx solid rgba(34, 197, 94, 0.3);
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
color: $brand;
|
||||
font-weight: 600;
|
||||
|
||||
.badge-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 8rpx currentColor;
|
||||
}
|
||||
|
||||
&.badge-mild {
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
color: #4ade80;
|
||||
}
|
||||
&.badge-mid {
|
||||
background: rgba(251, 191, 36, 0.12);
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
color: #fbbf24;
|
||||
}
|
||||
&.badge-strong {
|
||||
background: rgba(249, 115, 22, 0.12);
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
color: #fb923c;
|
||||
}
|
||||
&.badge-hot {
|
||||
background: rgba(239, 68, 68, 0.14);
|
||||
border-color: rgba(239, 68, 68, 0.32);
|
||||
color: #f87171;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 24rpx;
|
||||
color: $text-3;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.walker {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 跑步小人 圆形蒙版(裁掉灰白底 + 右上角原生按钮)
|
||||
* ============================================================ */
|
||||
.runner-orb {
|
||||
position: relative;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 外圈彩色光晕,跟 badge 颜色联动 */
|
||||
.orb-glow {
|
||||
position: absolute;
|
||||
inset: -10rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(34, 197, 94, 0.32) 0%,
|
||||
rgba(34, 197, 94, 0.12) 55%,
|
||||
transparent 75%
|
||||
);
|
||||
filter: blur(8rpx);
|
||||
transition: background 0.3s;
|
||||
|
||||
&.badge-mid {
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(251, 191, 36, 0.32) 0%,
|
||||
rgba(251, 191, 36, 0.12) 55%,
|
||||
transparent 75%
|
||||
);
|
||||
}
|
||||
&.badge-strong {
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(249, 115, 22, 0.34) 0%,
|
||||
rgba(249, 115, 22, 0.14) 55%,
|
||||
transparent 75%
|
||||
);
|
||||
}
|
||||
&.badge-hot {
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(239, 68, 68, 0.36) 0%,
|
||||
rgba(239, 68, 68, 0.14) 55%,
|
||||
transparent 75%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.walker.playing .orb-glow {
|
||||
animation: orb-pulse var(--beat-duration, 750ms) ease-in-out infinite;
|
||||
}
|
||||
@keyframes orb-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆形裁切容器 */
|
||||
.orb-clip {
|
||||
position: relative;
|
||||
.walker-clip {
|
||||
/* 圆形蒙版裁掉 video 的灰白边 + 隐藏右上角原生小窗按钮 */
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(160deg, #f5f7fa 0%, #c9d1dc 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 2rpx rgba(255, 255, 255, 0.5),
|
||||
inset 0 -20rpx 36rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.runner-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* video 比 orb 略大,object-fit:cover 配合定位让小人居中且裁掉灰边 */
|
||||
transform: scale(1.08);
|
||||
transform: scale(1.1);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 走路节奏数据(右侧)
|
||||
* 三档位推荐
|
||||
* ============================================================ */
|
||||
.walker-stats {
|
||||
.presets {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.preset {
|
||||
flex: 1;
|
||||
background: $card-bg;
|
||||
border: 2rpx solid $card-border;
|
||||
border-radius: $radius-card;
|
||||
padding: 22rpx 12rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: $text-3;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: $text-1;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 22rpx;
|
||||
color: $text-3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 控制卡片(BPM + 拍号)
|
||||
* ============================================================ */
|
||||
.control-card {
|
||||
width: 100%;
|
||||
background: $card-bg;
|
||||
border: 1rpx solid $card-border;
|
||||
border-radius: $radius-card;
|
||||
padding: 28rpx;
|
||||
backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
.bpm-row {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.step-btn {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
border-radius: $radius-btn;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
background: rgba(34, 197, 94, 0.18);
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.step-num {
|
||||
font-size: 38rpx;
|
||||
color: $text-1;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
margin: 24rpx 0;
|
||||
}
|
||||
|
||||
.meter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.meter-label {
|
||||
font-size: 26rpx;
|
||||
color: $text-3;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.meter-options {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 6rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.meter-btn {
|
||||
min-width: 84rpx;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: all 0.18s;
|
||||
|
||||
text {
|
||||
.preset-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.preset-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: $text-1;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.preset-bpm {
|
||||
font-size: 24rpx;
|
||||
color: $text-2;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.preset-desc {
|
||||
font-size: 20rpx;
|
||||
color: $text-3;
|
||||
letter-spacing: 1rpx;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $brand;
|
||||
box-shadow: 0 4rpx 12rpx rgba(34, 197, 94, 0.4);
|
||||
background: $brand-soft;
|
||||
border-color: $brand;
|
||||
box-shadow:
|
||||
0 8rpx 20rpx rgba(16, 185, 129, 0.18),
|
||||
inset 0 0 0 2rpx rgba(16, 185, 129, 0.4);
|
||||
|
||||
text {
|
||||
color: #fff;
|
||||
.preset-name {
|
||||
color: $brand-deep;
|
||||
}
|
||||
.preset-bpm {
|
||||
color: $brand-deep;
|
||||
}
|
||||
.preset-desc {
|
||||
color: $brand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 右下角微调(不显眼)
|
||||
* ============================================================ */
|
||||
.quick-adjust {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
bottom: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
padding: 10rpx 18rpx;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
border-radius: 999rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.qa-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.qa-label {
|
||||
font-size: 20rpx;
|
||||
color: $text-3;
|
||||
margin: 0 4rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.qa-btn {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 23, 42, 0.05);
|
||||
color: $text-2;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
|
||||
&:active {
|
||||
background: rgba(16, 185, 129, 0.18);
|
||||
color: $brand-deep;
|
||||
}
|
||||
}
|
||||
|
||||
.qa-meter {
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 10rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
color: $text-3;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
transition: all 0.15s;
|
||||
|
||||
&.active {
|
||||
background: $brand;
|
||||
color: #fff;
|
||||
box-shadow: 0 2rpx 6rpx rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.qa-divider {
|
||||
width: 1rpx;
|
||||
height: 22rpx;
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user