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:
2026-05-25 17:14:45 +08:00
co-authored by Cursor
parent 6be5b4c45d
commit 27c2ada4ec
3 changed files with 286 additions and 434 deletions
+286 -434
View File
@@ -17,99 +17,65 @@
</view> </view>
</view> </view>
<!-- ===== 配速 + 走路动画卡片 ===== --> <!-- ===== 跑步小人(无边框,跟白底融合) ===== -->
<view class="walker-card"> <view class="walker">
<view class="walker-header"> <view class="walker-clip">
<view class="badge" :class="badgeClass"> <video
<view class="badge-dot" /> id="runner-video"
<text>{{ walkerTip }}</text> class="runner-video"
</view> :src="RUNNER_VIDEO_URL"
<text class="status">{{ statusHint }}</text> :muted="true"
</view> :loop="true"
:autoplay="true"
<view class="walker" :class="{ playing: isPlaying }"> :show-controls="false"
<view class="runner-orb"> :show-fullscreen-btn="false"
<view class="orb-glow" :class="badgeClass" /> :show-play-btn="false"
<view class="orb-clip"> :show-center-play-btn="false"
<video :enable-progress-gesture="false"
id="runner-video" :show-mute-btn="false"
class="runner-video" :picture-in-picture-mode="[]"
:src="RUNNER_VIDEO_URL" object-fit="cover"
:muted="true" @error="onVideoError"
:loop="true" @loadedmetadata="onVideoLoaded"
: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> </view>
</view> </view>
<!-- ===== 控制卡片:BPM + 拍号 ===== --> <!-- ===== 推荐档位(3 1) ===== -->
<view class="control-card"> <view class="presets">
<view class="bpm-row"> <view
<view class="step-btn" @click="adjustBpm(-10)"> v-for="p in PRESETS"
<text class="step-num">10</text> :key="p.id"
</view> class="preset"
<view class="step-btn" @click="adjustBpm(-1)"> :class="{ active: activePreset === p.id }"
<text class="step-num">1</text> @click="applyPreset(p)"
</view> >
<view class="step-btn" @click="adjustBpm(1)"> <text class="preset-icon">{{ p.icon }}</text>
<text class="step-num">+1</text> <text class="preset-name">{{ p.name }}</text>
</view> <text class="preset-bpm">{{ p.bpm }} BPM</text>
<view class="step-btn" @click="adjustBpm(10)"> <text class="preset-desc">{{ p.desc }}</text>
<text class="step-num">+10</text>
</view>
</view> </view>
</view>
<view class="divider" /> <!-- ===== 右下角微调(不显眼) ===== -->
<view class="quick-adjust">
<view class="meter-row"> <view class="qa-group">
<text class="meter-label">每组拍数</text> <view class="qa-btn" @click.stop="adjustBpm(-1)"></view>
<view class="meter-options"> <text class="qa-label">微调</text>
<view <view class="qa-btn" @click.stop="adjustBpm(1)">+</view>
v-for="m in METER_OPTIONS" </view>
:key="m.value" <view class="qa-divider" />
class="meter-btn" <view class="qa-group">
:class="{ active: accentEvery === m.value }" <view
@click="onMeterChange(m.value)" v-for="m in METER_OPTIONS"
> :key="m.value"
<text>{{ m.label }}</text> class="qa-meter"
</view> :class="{ active: accentEvery === m.value }"
@click.stop="onMeterChange(m.value)"
>
{{ m.label }}
</view> </view>
<text class="qa-label"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -129,13 +95,42 @@ const RUNNER_VIDEO_URL =
'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260525/20260525164014ef89f8862.mp4' 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260525/20260525164014ef89f8862.mp4'
/** /**
* 视频原速对应的 BPM 基准 * 视频原速对应的 BPM 基准(参见 hooks 实现注释)
* runner.mp4 时长 0.97s, 一个 walk cycle (左右脚各一次) ≈ 124 BPM 步频 * playback-rate 范围 [0.5, 2.0]
* 用户调速时 playbackRate = bpm / VIDEO_BASE_BPM
* 微信小程序限制 playback-rate 范围 [0.5, 2.0]
*/ */
const VIDEO_BASE_BPM = 124 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 = [ const METER_OPTIONS = [
{ label: '2', value: 2 }, { label: '2', value: 2 },
{ label: '3', value: 3 }, { label: '3', value: 3 },
@@ -151,7 +146,6 @@ const metronome = useMetronome({
const { const {
bpm, bpm,
isPlaying, isPlaying,
beatIndex,
isAccent, isAccent,
accentEvery, accentEvery,
intervalMs, 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 同步 */ /* 视频实例与 BPM 同步 */
let videoCtx: UniApp.VideoContext | null = null let videoCtx: UniApp.VideoContext | null = null
@@ -190,7 +197,6 @@ const playbackRate = computed(() => {
onMounted(() => { onMounted(() => {
videoCtx = uni.createVideoContext('runner-video') videoCtx = uni.createVideoContext('runner-video')
/* 进页面默认让视频保持暂停,跟节拍器 isPlaying 状态同步 */
setTimeout(() => videoCtx?.pause(), 50) setTimeout(() => videoCtx?.pause(), 50)
}) })
@@ -205,13 +211,11 @@ const onVideoError = (e: any) => {
const onVideoLoaded = (e: any) => { const onVideoLoaded = (e: any) => {
console.log('[runner-video] metadata loaded:', e?.detail) console.log('[runner-video] metadata loaded:', e?.detail)
/* 元数据加载完后再设 playbackRate,部分基础库初始化时设非 1.0 会卡 */
try { try {
videoCtx?.playbackRate?.(playbackRate.value) videoCtx?.playbackRate?.(playbackRate.value)
} catch (_) {} } catch (_) {}
} }
/* 节拍器状态变化 → 同步视频播放/暂停 */
watch(isPlaying, (playing) => { watch(isPlaying, (playing) => {
if (playing) { if (playing) {
videoCtx?.play() videoCtx?.play()
@@ -220,7 +224,6 @@ watch(isPlaying, (playing) => {
} }
}) })
/* BPM 变化 → 调整视频播放速度(部分基础库需要手动 API 调用) */
watch(playbackRate, (rate) => { watch(playbackRate, (rate) => {
try { try {
videoCtx?.playbackRate?.(rate) videoCtx?.playbackRate?.(rate)
@@ -232,29 +235,6 @@ const rootStyle = computed(() => ({
'--step-duration': `${intervalMs.value}ms`, '--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(() => { onShow(() => {
preload() preload()
}) })
@@ -273,33 +253,37 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
/* ============================================================ /* ============================================================
* 设计 token * 设计 token —— 浅色清爽
* ============================================================ */ * ============================================================ */
$bg-grad: linear-gradient(180deg, #0a0f1c 0%, #141a2a 100%); $bg-grad: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
$card-bg: rgba(255, 255, 255, 0.035); $card-bg: #ffffff;
$card-border: rgba(255, 255, 255, 0.06); $card-border: rgba(15, 23, 42, 0.06);
$text-1: #f1f5f9; $text-1: #0f172a;
$text-2: #cbd5e1; $text-2: #475569;
$text-3: #64748b; $text-3: #94a3b8;
$brand: #22c55e; $brand: #10b981;
$brand-deep: #15803d; $brand-soft: #d1fae5;
$brand-deep: #047857;
$accent: #f59e0b; $accent: #f59e0b;
$accent-deep: #b45309; $accent-soft: #fef3c7;
$radius-card: 28rpx; $radius-card: 24rpx;
$radius-btn: 20rpx; $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 { .page {
position: relative;
height: 100vh; height: 100vh;
box-sizing: border-box; box-sizing: border-box;
padding: 30rpx 28rpx 60rpx; padding: 30rpx 28rpx 80rpx;
background: $bg-grad; background: $bg-grad;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 24rpx; gap: 22rpx;
color: $text-1; color: $text-1;
overflow: hidden; overflow: hidden;
} }
@@ -309,9 +293,9 @@ $radius-btn: 20rpx;
* ============================================================ */ * ============================================================ */
.stage { .stage {
position: relative; position: relative;
width: 540rpx; width: 480rpx;
height: 540rpx; height: 480rpx;
margin-top: 20rpx; margin-top: 16rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -326,7 +310,7 @@ $radius-btn: 20rpx;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%; border-radius: 50%;
border: 3rpx solid rgba(34, 197, 94, 0.14); border: 3rpx solid rgba(16, 185, 129, 0.12);
pointer-events: none; pointer-events: none;
&.playing { &.playing {
@@ -340,29 +324,29 @@ $radius-btn: 20rpx;
0% { 0% {
transform: scale(0.94); transform: scale(0.94);
opacity: 0.85; opacity: 0.85;
border-color: rgba(34, 197, 94, 0.45); border-color: rgba(16, 185, 129, 0.4);
} }
100% { 100% {
transform: scale(1.18); transform: scale(1.18);
opacity: 0; opacity: 0;
border-color: rgba(34, 197, 94, 0); border-color: rgba(16, 185, 129, 0);
} }
} }
.core { .core {
position: relative; position: relative;
width: 420rpx; width: 380rpx;
height: 420rpx; height: 380rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(140deg, $brand 0%, $brand-deep 100%); background: linear-gradient(140deg, #34d399 0%, $brand-deep 100%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: box-shadow:
inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.22), inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.18),
inset 0 16rpx 50rpx rgba(255, 255, 255, 0.1), inset 0 16rpx 50rpx rgba(255, 255, 255, 0.18),
0 20rpx 50rpx rgba(34, 197, 94, 0.32); 0 16rpx 40rpx rgba(16, 185, 129, 0.32);
transition: transform 0.12s, background 0.18s, box-shadow 0.18s; transition: transform 0.12s, background 0.18s, box-shadow 0.18s;
&.playing { &.playing {
@@ -370,34 +354,34 @@ $radius-btn: 20rpx;
} }
&.playing.accent { &.playing.accent {
background: linear-gradient(140deg, #fbbf24 0%, $accent-deep 100%); background: linear-gradient(140deg, #fbbf24 0%, #d97706 100%);
box-shadow: box-shadow:
inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.22), inset 0 -16rpx 50rpx rgba(0, 0, 0, 0.18),
inset 0 16rpx 50rpx rgba(255, 255, 255, 0.18), inset 0 16rpx 50rpx rgba(255, 255, 255, 0.22),
0 20rpx 50rpx rgba(245, 158, 11, 0.4); 0 16rpx 40rpx rgba(245, 158, 11, 0.4);
} }
.bpm-num { .bpm-num {
font-size: 180rpx; font-size: 168rpx;
font-weight: 800; font-weight: 800;
color: #fff; color: #fff;
line-height: 1; line-height: 1;
letter-spacing: -2rpx; letter-spacing: -2rpx;
font-variant-numeric: tabular-nums; 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 { .bpm-label {
font-size: 22rpx; font-size: 22rpx;
color: rgba(255, 255, 255, 0.65); color: rgba(255, 255, 255, 0.7);
letter-spacing: 6rpx; letter-spacing: 6rpx;
margin-top: 10rpx; margin-top: 10rpx;
font-weight: 600; font-weight: 600;
} }
.ctrl { .ctrl {
margin-top: 26rpx; margin-top: 22rpx;
height: 76rpx; height: 64rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -413,336 +397,204 @@ $radius-btn: 20rpx;
} }
} }
/* 播放/暂停 icon (CSS 几何) */ /* 播放/暂停 icon */
.icon-play { .icon-play {
width: 0; width: 0;
height: 0; height: 0;
border-left: 60rpx solid #fff; border-left: 52rpx solid #fff;
border-top: 38rpx solid transparent; border-top: 32rpx solid transparent;
border-bottom: 38rpx solid transparent; border-bottom: 32rpx solid transparent;
margin-left: 14rpx; margin-left: 12rpx;
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.2)); filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
} }
.icon-pause { .icon-pause {
display: flex; display: flex;
gap: 18rpx; gap: 16rpx;
height: 70rpx; height: 60rpx;
.bar { .bar {
width: 18rpx; width: 16rpx;
height: 100%; height: 100%;
background: #fff; background: #fff;
border-radius: 5rpx; border-radius: 4rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2); 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 { .walker {
position: relative;
width: 100%; width: 100%;
height: 240rpx;
display: flex;
align-items: center;
gap: 28rpx;
padding: 0 8rpx;
}
/* ============================================================
* 跑步小人 圆形蒙版(裁掉灰白底 + 右上角原生按钮)
* ============================================================ */
.runner-orb {
position: relative;
width: 220rpx;
height: 220rpx; height: 220rpx;
flex-shrink: 0;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
align-items: center;
} }
/* 外圈彩色光晕,跟 badge 颜色联动 */ .walker-clip {
.orb-glow { /* 圆形蒙版裁掉 video 的灰白边 + 隐藏右上角原生小窗按钮 */
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;
width: 220rpx; width: 220rpx;
height: 220rpx; height: 220rpx;
border-radius: 50%; border-radius: 50%;
overflow: hidden; 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 { .runner-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
/* video 比 orb 略大,object-fit:cover 配合定位让小人居中且裁掉灰边 */ transform: scale(1.1);
transform: scale(1.08);
background: transparent; background: transparent;
} }
/* ============================================================ /* ============================================================
* 走路节奏数据(右侧) * 三档位推荐
* ============================================================ */ * ============================================================ */
.walker-stats { .presets {
width: 100%;
display: flex;
gap: 16rpx;
}
.preset {
flex: 1; flex: 1;
background: $card-bg;
border: 2rpx solid $card-border;
border-radius: $radius-card;
padding: 22rpx 12rpx 20rpx;
display: flex; display: flex;
flex-direction: column; 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; align-items: center;
justify-content: center; gap: 6rpx;
transition: all 0.15s; box-shadow: $shadow-sm;
&: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;
transition: all 0.18s; transition: all 0.18s;
text { .preset-icon {
font-size: 44rpx;
line-height: 1;
margin-bottom: 6rpx;
}
.preset-name {
font-size: 30rpx; font-size: 30rpx;
font-weight: 700;
color: $text-1;
letter-spacing: 2rpx;
}
.preset-bpm {
font-size: 24rpx;
color: $text-2; color: $text-2;
font-weight: 600; 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 { &.active {
background: $brand; background: $brand-soft;
box-shadow: 0 4rpx 12rpx rgba(34, 197, 94, 0.4); 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 { .preset-name {
color: #fff; 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> </style>
Binary file not shown.
Binary file not shown.