- 档位模式用 BackgroundAudioManager+预合成循环 mp3, 锁屏可继续播放 - 自定义模式保留 InnerAudio, 支持任意 BPM(40~240)/拍号(2/3/4), 仅前台 - UI 折叠区区分两种模式, preset 默认进, 点"⚙ 自定义节奏"切换 - 音频资产全部迁移到腾讯云 COS, 删除本地 6 个 mp3/jpg (-210KB) - server 上传白名单补 mp3/wav/m4a/aac/amr/wma 格式
736 lines
20 KiB
Vue
736 lines
20 KiB
Vue
<template>
|
||
<view class="page" :style="rootStyle">
|
||
<!-- ===== 主舞台:节拍器圆 ===== -->
|
||
<view class="stage" @click="onCenterTap">
|
||
<view class="ring r1" :class="{ playing: isPlaying }" />
|
||
<view class="ring r2" :class="{ playing: isPlaying }" />
|
||
<view class="core" :class="{ playing: isPlaying }">
|
||
<text class="bpm-num">{{ currentBpm }}</text>
|
||
<text class="bpm-label">BPM</text>
|
||
<view class="ctrl">
|
||
<view v-if="isPlaying" class="icon-pause">
|
||
<view class="bar" />
|
||
<view class="bar" />
|
||
</view>
|
||
<view v-else class="icon-play" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ===== 跑步小人(矩形完整显示,跟白底融合) ===== -->
|
||
<view class="walker">
|
||
<view class="walker-box">
|
||
<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="contain"
|
||
@error="onVideoError"
|
||
@loadedmetadata="onVideoLoaded"
|
||
/>
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<cover-view class="pip-mask"> </cover-view>
|
||
<!-- #endif -->
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ===== 推荐档位(3 选 1) ===== -->
|
||
<view class="presets">
|
||
<view
|
||
v-for="p in LOOP_PRESETS"
|
||
:key="p.id"
|
||
class="preset"
|
||
:class="{ active: mode === 'preset' && currentLoop === p.id }"
|
||
@click="onPresetTap(p.id)"
|
||
>
|
||
<text class="preset-icon">{{ presetIcon(p.id) }}</text>
|
||
<text class="preset-name">{{ presetName(p.id) }}</text>
|
||
<text class="preset-bpm">{{ p.bpm }} BPM</text>
|
||
<text class="preset-desc">{{ presetDesc(p.id) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ===== 自定义节奏(高级折叠区) ===== -->
|
||
<view v-if="customExpanded" class="custom-panel">
|
||
<view class="custom-warn">
|
||
<text class="warn-dot">●</text>
|
||
<text class="warn-text">前台模式 · 切后台会暂停</text>
|
||
</view>
|
||
|
||
<!-- BPM 微调 -->
|
||
<view class="row">
|
||
<text class="row-label">BPM</text>
|
||
<view class="bpm-stepper">
|
||
<view class="step-btn" @click="onBpmDelta(-5)">−5</view>
|
||
<view class="step-btn" @click="onBpmDelta(-1)">−1</view>
|
||
<view class="step-val">{{ customBpm }}</view>
|
||
<view class="step-btn" @click="onBpmDelta(1)">+1</view>
|
||
<view class="step-btn" @click="onBpmDelta(5)">+5</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 拍号 -->
|
||
<view class="row">
|
||
<text class="row-label">拍号</text>
|
||
<view class="meter-tabs">
|
||
<view
|
||
v-for="n in [2, 3, 4]"
|
||
:key="n"
|
||
class="meter-tab"
|
||
:class="{ active: customAccent === n }"
|
||
@click="onAccentSelect(n)"
|
||
>{{ n }}/4</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部行:自定义入口 + 提示 -->
|
||
<view class="bottom-row">
|
||
<text class="bottom-link" @click="onToggleCustom">
|
||
{{ customExpanded ? '✕ 收起自定义' : '⚙ 自定义节奏' }}
|
||
</text>
|
||
<text class="bottom-hint" v-if="mode === 'preset'">🔒 锁屏可继续播放</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||
import { onShow, onHide } from '@dcloudio/uni-app'
|
||
import { useMetronomeBg, type LoopId } from '../hooks/useMetronomeBg'
|
||
import { useMetronome } from '../hooks/useMetronome'
|
||
|
||
/**
|
||
* 跑步动画视频(腾讯云 COS CDN)
|
||
* 注意:小程序后台需将 cos.ap-guangzhou.myqcloud.com 加入 downloadFile 合法域名,
|
||
* 否则正式发布版会被拦截(开发版默认不校验)
|
||
*/
|
||
const RUNNER_VIDEO_URL =
|
||
'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260525/20260525164014ef89f8862.mp4'
|
||
|
||
/** 视频原速对应的 BPM 基准, playback-rate 范围 [0.5, 2.0] */
|
||
const VIDEO_BASE_BPM = 124
|
||
|
||
/* ============================================================
|
||
* 双引擎设计
|
||
* ------------------------------------------------------------
|
||
* - mode='preset': 用 BgAudio 播预合成 mp3,支持后台/锁屏(走路场景)
|
||
* - mode='custom': 用 InnerAudio 实时合成,支持任意 BPM/拍号,但仅前台
|
||
* - 切模式时停掉对侧引擎,避免双声道叠加
|
||
* ============================================================ */
|
||
type Mode = 'preset' | 'custom'
|
||
const mode = ref<Mode>('preset')
|
||
const customExpanded = ref<boolean>(false)
|
||
|
||
/* click-wood.mp3 (CDN), 木鱼质感的敲击音 */
|
||
const CLICK_WOOD_URL =
|
||
'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260526/202605261051282a0a94508.mp3'
|
||
|
||
const bg = useMetronomeBg()
|
||
const fg = useMetronome({
|
||
initialBpm: 100,
|
||
accentEvery: 2,
|
||
clickSrc: CLICK_WOOD_URL,
|
||
accentSrc: CLICK_WOOD_URL,
|
||
poolSize: 4,
|
||
})
|
||
|
||
const { LOOP_PRESETS } = bg
|
||
const currentLoop = bg.currentLoop
|
||
|
||
const customBpm = computed(() => fg.bpm.value)
|
||
const customAccent = computed(() => fg.accentEvery.value)
|
||
|
||
/* 统一对外暴露的 isPlaying / currentBpm,根据 mode 选择信号源 */
|
||
const isPlaying = computed(() =>
|
||
mode.value === 'preset' ? bg.isPlaying.value : fg.isPlaying.value,
|
||
)
|
||
|
||
const currentBpm = computed(() => {
|
||
if (mode.value === 'preset') {
|
||
const p = LOOP_PRESETS.find((p) => p.id === currentLoop.value)
|
||
return p?.bpm ?? 110
|
||
}
|
||
return fg.bpm.value
|
||
})
|
||
|
||
const intervalMs = computed(() => 60000 / currentBpm.value)
|
||
|
||
/* ============================================================
|
||
* 档位元数据
|
||
* ============================================================ */
|
||
const presetIcon = (id: LoopId) =>
|
||
id === 'slow' ? '🚶' : id === 'normal' ? '🏃♀️' : '🏃'
|
||
const presetName = (id: LoopId) =>
|
||
id === 'slow' ? '慢走' : id === 'normal' ? '健走' : '快走'
|
||
const presetDesc = (id: LoopId) =>
|
||
id === 'slow' ? '热身 · 恢复' : id === 'normal' ? '日常 · 通勤' : '提速 · 燃脂'
|
||
|
||
/* ============================================================
|
||
* 中央圆按钮:在两个模式下分别工作
|
||
* ============================================================ */
|
||
const onCenterTap = () => {
|
||
if (mode.value === 'preset') {
|
||
if (!currentLoop.value) {
|
||
bg.playLoop('normal')
|
||
uni.setKeepScreenOn({ keepScreenOn: true })
|
||
return
|
||
}
|
||
if (bg.isPlaying.value) {
|
||
bg.pause()
|
||
uni.setKeepScreenOn({ keepScreenOn: false })
|
||
} else {
|
||
bg.resume()
|
||
uni.setKeepScreenOn({ keepScreenOn: true })
|
||
}
|
||
} else {
|
||
/* custom 模式 */
|
||
if (fg.isPlaying.value) {
|
||
fg.stop()
|
||
uni.setKeepScreenOn({ keepScreenOn: false })
|
||
} else {
|
||
fg.start()
|
||
uni.setKeepScreenOn({ keepScreenOn: true })
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 档位卡片:点了 → 切回 preset 模式 + 停掉 custom
|
||
* ============================================================ */
|
||
const onPresetTap = (id: LoopId) => {
|
||
if (mode.value === 'custom') {
|
||
fg.stop()
|
||
mode.value = 'preset'
|
||
customExpanded.value = false
|
||
}
|
||
bg.playLoop(id)
|
||
uni.setKeepScreenOn({ keepScreenOn: true })
|
||
}
|
||
|
||
/* ============================================================
|
||
* 自定义折叠区切换
|
||
* ============================================================ */
|
||
const onToggleCustom = () => {
|
||
if (!customExpanded.value) {
|
||
/* 展开:进入 custom 模式 + 停掉 preset */
|
||
if (bg.isPlaying.value || currentLoop.value) {
|
||
bg.stop()
|
||
}
|
||
mode.value = 'custom'
|
||
/* 把当前显示 BPM 带过去当起始值 */
|
||
const seed =
|
||
LOOP_PRESETS.find((p) => p.id === (currentLoop.value ?? 'normal'))?.bpm ?? 100
|
||
fg.setBpm(seed)
|
||
fg.setAccentEvery(2)
|
||
/* 立即预热 InnerAudio,避免首拍冷启动延迟 */
|
||
fg.preload()
|
||
customExpanded.value = true
|
||
} else {
|
||
/* 收起:回到 preset,停掉 fg */
|
||
fg.stop()
|
||
mode.value = 'preset'
|
||
customExpanded.value = false
|
||
uni.setKeepScreenOn({ keepScreenOn: false })
|
||
}
|
||
}
|
||
|
||
const onBpmDelta = (delta: number) => {
|
||
fg.setBpm(fg.bpm.value + delta)
|
||
}
|
||
|
||
const onAccentSelect = (n: number) => {
|
||
fg.setAccentEvery(n)
|
||
}
|
||
|
||
/* ============================================================
|
||
* 视频实例与 BPM 同步
|
||
* ============================================================ */
|
||
let videoCtx: UniApp.VideoContext | null = null
|
||
|
||
const playbackRate = computed(() => {
|
||
const rate = currentBpm.value / VIDEO_BASE_BPM
|
||
return Math.max(0.5, Math.min(2.0, +rate.toFixed(2)))
|
||
})
|
||
|
||
onMounted(() => {
|
||
videoCtx = uni.createVideoContext('runner-video')
|
||
setTimeout(() => videoCtx?.pause(), 50)
|
||
})
|
||
|
||
const onVideoError = (e: any) => {
|
||
console.error('[runner-video] error:', e?.detail || e)
|
||
}
|
||
|
||
const onVideoLoaded = (e: any) => {
|
||
console.log('[runner-video] metadata loaded:', e?.detail)
|
||
try {
|
||
videoCtx?.playbackRate?.(playbackRate.value)
|
||
} catch (_) {}
|
||
}
|
||
|
||
watch(isPlaying, (playing) => {
|
||
if (playing) {
|
||
videoCtx?.play()
|
||
} else {
|
||
videoCtx?.pause()
|
||
}
|
||
})
|
||
|
||
watch(playbackRate, (rate) => {
|
||
try {
|
||
videoCtx?.playbackRate?.(rate)
|
||
} catch (_) {}
|
||
})
|
||
|
||
const rootStyle = computed(() => ({
|
||
'--beat-duration': `${intervalMs.value}ms`,
|
||
}))
|
||
|
||
onShow(() => {
|
||
/* custom 模式回到前台时主动预加载,避免冷启动延迟 */
|
||
if (mode.value === 'custom') {
|
||
fg.preload()
|
||
}
|
||
})
|
||
|
||
/**
|
||
* onHide:差异化处理
|
||
* - preset 模式: 不停,BgAudio 接管后台播放
|
||
* - custom 模式: 立即停掉,InnerAudio 后台不工作,留着会被 iOS 挂起后状态错乱
|
||
*/
|
||
onHide(() => {
|
||
if (mode.value === 'custom') {
|
||
fg.stop()
|
||
uni.setKeepScreenOn({ keepScreenOn: false })
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
/* 页面销毁时全部清理 */
|
||
if (mode.value === 'preset' && bg.isPlaying.value) bg.stop()
|
||
if (mode.value === 'custom') fg.stop()
|
||
uni.setKeepScreenOn({ keepScreenOn: false })
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================================
|
||
* 设计 token —— 浅色清爽
|
||
* ============================================================ */
|
||
$bg-color: #f8fafc;
|
||
$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;
|
||
$warn: #f59e0b;
|
||
$warn-soft: #fef3c7;
|
||
$radius-card: 24rpx;
|
||
$shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04);
|
||
|
||
/* ============================================================
|
||
* 页面容器
|
||
* ============================================================ */
|
||
.page {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
box-sizing: border-box;
|
||
padding: 30rpx 28rpx 50rpx;
|
||
background: $bg-color;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
color: $text-1;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 节拍器主舞台
|
||
* ============================================================ */
|
||
.stage {
|
||
position: relative;
|
||
width: 480rpx;
|
||
height: 480rpx;
|
||
margin-top: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:active .core {
|
||
transform: scale(0.97);
|
||
}
|
||
}
|
||
|
||
.ring {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
border: 3rpx solid rgba(16, 185, 129, 0.12);
|
||
pointer-events: none;
|
||
|
||
&.playing {
|
||
animation: ring-pulse var(--beat-duration, 750ms) ease-out infinite;
|
||
}
|
||
&.r2.playing {
|
||
animation-delay: calc(var(--beat-duration, 750ms) * -0.5);
|
||
}
|
||
}
|
||
@keyframes ring-pulse {
|
||
0% {
|
||
transform: scale(0.94);
|
||
opacity: 0.85;
|
||
border-color: rgba(16, 185, 129, 0.4);
|
||
}
|
||
100% {
|
||
transform: scale(1.18);
|
||
opacity: 0;
|
||
border-color: rgba(16, 185, 129, 0);
|
||
}
|
||
}
|
||
|
||
.core {
|
||
position: relative;
|
||
width: 380rpx;
|
||
height: 380rpx;
|
||
border-radius: 50%;
|
||
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.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 {
|
||
animation: core-beat var(--beat-duration, 750ms) ease-in-out infinite;
|
||
}
|
||
|
||
.bpm-num {
|
||
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.16);
|
||
}
|
||
|
||
.bpm-label {
|
||
font-size: 22rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
letter-spacing: 6rpx;
|
||
margin-top: 10rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ctrl {
|
||
margin-top: 22rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
@keyframes core-beat {
|
||
0%,
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
transform: scale(1.04);
|
||
}
|
||
}
|
||
|
||
/* 播放/暂停 icon */
|
||
.icon-play {
|
||
width: 0;
|
||
height: 0;
|
||
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: 16rpx;
|
||
height: 60rpx;
|
||
|
||
.bar {
|
||
width: 16rpx;
|
||
height: 100%;
|
||
background: #fff;
|
||
border-radius: 4rpx;
|
||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.18);
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 跑步小人
|
||
* ============================================================ */
|
||
.walker {
|
||
width: 100%;
|
||
height: 240rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.walker-box {
|
||
position: relative;
|
||
width: 240rpx;
|
||
height: 240rpx;
|
||
background: #e8ecf2;
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.runner-video {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #e8ecf2;
|
||
}
|
||
|
||
.pip-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 80rpx;
|
||
height: 56rpx;
|
||
background-color: #dde2eb;
|
||
border-bottom-left-radius: 18rpx;
|
||
border-top-right-radius: 24rpx;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 三档位推荐
|
||
* ============================================================ */
|
||
.presets {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.preset {
|
||
flex: 1;
|
||
background: $card-bg;
|
||
border: 2rpx solid $card-border;
|
||
border-radius: $radius-card;
|
||
padding: 18rpx 8rpx 16rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
box-shadow: $shadow-sm;
|
||
transition: all 0.18s;
|
||
|
||
.preset-icon {
|
||
font-size: 40rpx;
|
||
line-height: 1;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
.preset-name {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: $text-1;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
.preset-bpm {
|
||
font-size: 22rpx;
|
||
color: $text-2;
|
||
font-weight: 600;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.preset-desc {
|
||
font-size: 18rpx;
|
||
color: $text-3;
|
||
letter-spacing: 1rpx;
|
||
margin-top: 2rpx;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.97);
|
||
}
|
||
|
||
&.active {
|
||
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);
|
||
|
||
.preset-name {
|
||
color: $brand-deep;
|
||
}
|
||
.preset-bpm {
|
||
color: $brand-deep;
|
||
}
|
||
.preset-desc {
|
||
color: $brand;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 自定义节奏面板(仅 customExpanded 时显示)
|
||
* ============================================================ */
|
||
.custom-panel {
|
||
width: 100%;
|
||
background: $card-bg;
|
||
border: 2rpx solid rgba(245, 158, 11, 0.25);
|
||
border-radius: $radius-card;
|
||
box-shadow: 0 6rpx 18rpx rgba(245, 158, 11, 0.08);
|
||
padding: 20rpx 24rpx 22rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.custom-warn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
margin-bottom: 4rpx;
|
||
|
||
.warn-dot {
|
||
font-size: 18rpx;
|
||
color: $warn;
|
||
}
|
||
.warn-text {
|
||
font-size: 22rpx;
|
||
color: #b45309;
|
||
letter-spacing: 0.5rpx;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
|
||
.row-label {
|
||
font-size: 24rpx;
|
||
color: $text-2;
|
||
font-weight: 600;
|
||
letter-spacing: 1rpx;
|
||
flex-shrink: 0;
|
||
width: 70rpx;
|
||
}
|
||
}
|
||
|
||
.bpm-stepper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
flex: 1;
|
||
justify-content: flex-end;
|
||
|
||
.step-btn {
|
||
min-width: 56rpx;
|
||
height: 52rpx;
|
||
line-height: 52rpx;
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
color: $text-2;
|
||
background: #f1f5f9;
|
||
border-radius: 10rpx;
|
||
font-weight: 600;
|
||
padding: 0 10rpx;
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
&:active {
|
||
background: #e2e8f0;
|
||
transform: scale(0.94);
|
||
}
|
||
}
|
||
.step-val {
|
||
min-width: 84rpx;
|
||
height: 52rpx;
|
||
line-height: 52rpx;
|
||
text-align: center;
|
||
font-size: 30rpx;
|
||
font-weight: 800;
|
||
color: $text-1;
|
||
font-variant-numeric: tabular-nums;
|
||
background: $warn-soft;
|
||
border-radius: 10rpx;
|
||
}
|
||
}
|
||
|
||
.meter-tabs {
|
||
display: flex;
|
||
gap: 8rpx;
|
||
|
||
.meter-tab {
|
||
height: 52rpx;
|
||
line-height: 52rpx;
|
||
padding: 0 18rpx;
|
||
font-size: 22rpx;
|
||
color: $text-2;
|
||
background: #f1f5f9;
|
||
border-radius: 10rpx;
|
||
font-weight: 600;
|
||
letter-spacing: 1rpx;
|
||
|
||
&.active {
|
||
background: $warn;
|
||
color: #fff;
|
||
box-shadow: 0 4rpx 10rpx rgba(245, 158, 11, 0.3);
|
||
}
|
||
&:active {
|
||
transform: scale(0.94);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 底部行(自定义入口 + 锁屏提示,横向排布)
|
||
* ============================================================ */
|
||
.bottom-row {
|
||
width: 100%;
|
||
margin-top: 6rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 6rpx;
|
||
}
|
||
|
||
.bottom-link {
|
||
font-size: 22rpx;
|
||
color: $text-3;
|
||
letter-spacing: 0.5rpx;
|
||
padding: 8rpx 4rpx;
|
||
|
||
&:active {
|
||
color: $text-2;
|
||
}
|
||
}
|
||
|
||
.bottom-hint {
|
||
font-size: 22rpx;
|
||
color: $text-3;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
</style>
|