feat: optimize volume input control

This commit is contained in:
AZMIAO authored and DiheChen committed 2026-10-30 17:36:36 +08:00
1 parent 11a56cf44e
commit 6b1256cfb2
4 files changed
+94 -20

No files matched your search

+7 -2
View File
@@ -13,13 +13,18 @@
.progress-track { height: 3px; flex: 1; background: var(--color-border-hover); border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--color-foreground); transform: scaleX(0); transform-origin: left; }
.player-tools { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
.player-tools .volume-slider { width: 90px; margin-right: 16px; accent-color: var(--color-foreground); }
.player-tools .volume-control { display: flex; align-items: center; gap: 6px; margin-right: 16px; }
.player-tools .volume-slider { width: 90px; accent-color: var(--color-foreground); }
.player-tools .volume-input { box-sizing: border-box; width: 4.5ch; min-width: 0; padding: 4px 3px; text-align: center; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
.player-tools .volume-input::-webkit-inner-spin-button, .player-tools .volume-input::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; }
.player-tools .volume-unit { color: var(--color-foreground-subtle); font-size: var(--text-ui-xs); }
.player-panel-toggle { color: var(--color-foreground-subtle); }
.player-panel-toggle:hover:not(:disabled), .player-panel-toggle:focus-visible:not(:disabled), .player-panel-toggle[aria-pressed='true'] { color: var(--color-brand); background: transparent; }
.player-panel-toggle:active:not(:disabled) { color: var(--color-brand); background: transparent; transform: none; }
@media (max-width: 1100px) {
.player-bar { gap: 12px; }
.player-tools .volume-slider { width: 60px; margin-right: 4px; }
.player-tools .volume-control { margin-right: 4px; }
.player-tools .volume-slider { width: 60px; }
}
@media (max-width: 767px) {
/* 窄屏的播放器栏由内容撑开高度;内容比封面矮时(比如出错提示),仍保持封面加上下内边距的高度。 */
+63 -15
View File
@@ -1,5 +1,5 @@
import { ListMusic, MessageCircle, SkipForward, Volume2, VolumeX } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import { roomStore } from '@/core/room'
import { sourceName } from '@/features/queue'
import { bookTitle } from '@/shared/lib/book-title'
@@ -65,7 +65,26 @@ export function PlayerBar({ onSkip, onLyrics, onQueue, onChat, queueOpen, chatOp
const connected = useStore(roomStore, s => s.status === 'open')
const volume = useStore(volumeStore, s => s.volume)
const muted = useStore(volumeStore, s => s.muted)
const [volumeDraft, setVolumeDraft] = useState(String(volume))
const volumeEditingRef = useRef(false)
const meta = nowPlaying?.entry.meta
useEffect(() => {
if (!volumeEditingRef.current)
setVolumeDraft(String(volume))
}, [volume])
const commitVolumeDraft = () => {
const parsed = Number(volumeDraft)
if (volumeDraft.trim() === '' || !Number.isFinite(parsed)) {
setVolumeDraft(String(volume))
return
}
const next = Math.min(100, Math.max(0, Math.round(parsed)))
volumeStore.setVolume(next)
setVolumeDraft(String(next))
if (muted && next > 0)
volumeStore.setMuted(false)
}
const source = nowPlaying && sourceName(nowPlaying.entry.track)
return (
<section className="player-bar" aria-label="播放器">
@@ -95,20 +114,49 @@ export function PlayerBar({ onSkip, onLyrics, onQueue, onChat, queueOpen, chatOp
</div>
<div className="player-tools">
<IconButton icon={muted ? VolumeX : Volume2} label={muted ? '取消静音' : '静音'} onClick={() => volumeStore.setMuted(!muted)} />
<input
className="volume-slider"
type="range"
min="0"
max="100"
value={muted ? 0 : volume}
aria-label="音量"
onChange={(event) => {
const next = Number(event.target.value)
volumeStore.setVolume(next)
if (muted && next > 0)
volumeStore.setMuted(false)
}}
/>
<div className="volume-control">
<input
className="volume-slider"
type="range"
min="0"
max="100"
value={muted ? 0 : volume}
aria-label="音量"
onChange={(event) => {
const next = Number(event.target.value)
volumeStore.setVolume(next)
setVolumeDraft(String(next))
if (muted && next > 0)
volumeStore.setMuted(false)
}}
/>
<input
className="volume-input"
type="number"
min="0"
max="100"
step="1"
inputMode="numeric"
value={volumeDraft}
aria-label="音量百分比"
onFocus={() => {
volumeEditingRef.current = true
}}
onChange={event => setVolumeDraft(event.target.value)}
onBlur={() => {
volumeEditingRef.current = false
commitVolumeDraft()
}}
onKeyDown={(event) => {
if (event.key !== 'Enter')
return
event.preventDefault()
commitVolumeDraft()
event.currentTarget.blur()
}}
/>
<span className="volume-unit" aria-hidden="true">%</span>
</div>
<IconButton icon={MessageCircle} label="房间聊天" className="player-panel-toggle" disabled={!canOpenPanels} aria-pressed={chatOpen} onClick={onChat} />
<IconButton icon={ListMusic} label="播放列表" className="player-panel-toggle" disabled={!canOpenPanels} aria-pressed={queueOpen} onClick={onQueue} />
</div>
+19 -2
View File
@@ -209,8 +209,25 @@ export function SettingsPage() {
<div className="settings-group">
<SettingRow id="settings-volume" title="音量" description="只调整这台设备的播放音量。" field>
<div className="settings-volume-control">
<input id="settings-volume" aria-describedby="settings-volume-description" type="range" min="0" max="100" value={draft.volume} onChange={event => update('volume', Number(event.target.value))} />
<output htmlFor="settings-volume" className="text-ui-caption muted">{`${draft.volume}%`}</output>
<input id="settings-volume" aria-describedby="settings-volume-description" type="range" min="0" max="100" value={draft.volume} aria-label="音量滑块" onChange={event => update('volume', Number(event.target.value))} />
<div className="settings-volume-input-control">
<input
className="settings-volume-input"
type="number"
min="0"
max="100"
step="1"
inputMode="numeric"
value={draft.volume}
aria-label="音量百分比"
onChange={(event) => {
const value = Number(event.target.value)
if (Number.isFinite(value))
update('volume', Math.min(100, Math.max(0, Math.round(value))))
}}
/>
<span className="settings-volume-unit" aria-hidden="true">%</span>
</div>
</div>
</SettingRow>
<SettingRow id="settings-muted" title="静音" description="暂时关闭这台设备的声音。">
+5 -1
View File
@@ -18,7 +18,11 @@
.settings-row-control select:disabled, .settings-row-control input:disabled { opacity: .5; cursor: not-allowed; }
.settings-row-field .settings-row-control { width: 280px; }
.settings-volume-control { display: flex; align-items: center; gap: 16px; width: 100%; }
.settings-volume-control output { min-width: 4ch; text-align: right; font-variant-numeric: tabular-nums; }
.settings-volume-input-control { display: flex; align-items: center; gap: 4px; }
.settings-volume-input { -moz-appearance: textfield; }
.settings-volume-input::-webkit-inner-spin-button, .settings-volume-input::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; }
.settings-volume-input { box-sizing: border-box; width: 5ch; padding: 6px 4px; text-align: center; font-variant-numeric: tabular-nums; }
.settings-volume-unit { color: var(--color-foreground-subtle); font-size: var(--text-ui-xs); }
.settings-switch[type='checkbox'] { appearance: none; position: relative; width: 40px; height: 24px; margin: 0; border: 1px solid var(--color-border-hover); border-radius: 999px; background: var(--color-selected); cursor: pointer; }
.settings-switch[type='checkbox']::before { content: ''; display: block; width: 18px; height: 18px; margin: 2px; border-radius: 50%; background: var(--color-foreground); transform: translateX(0); transition: transform 150ms ease; }
.settings-switch[type='checkbox']:checked { border-color: var(--color-primary); background: var(--color-primary); }