feat: 统一拖动条样式并优化UI配色方案

重构所有拖动条使用统一的Slider组件,建立一致的视觉体验:
- 统一所有音量、速度、倍速拖动条使用相同的Slider组件
- 创建统一的控件背景色CSS变量,与声音图标保持一致
- 优化拖动条配色:已选中部分和拖动点使用相同的温和色调
- 添加明亮模式下的滚动条样式,提升视觉体验
- 调整音乐列表布局,优化声音名称显示和展开按钮
- 精简CSS代码,减少重复样式定义

技术改进:
- 移除重复的range input样式代码(从122行减至46行)
- 使用CSS变量统一管理控件配色方案
- 保持组件间的一致性和可维护性
This commit is contained in:
walle 2025-11-18 12:26:41 +08:00
parent a464745a9f
commit 71ab17a39e
8 changed files with 122 additions and 145 deletions

View file

@ -198,6 +198,7 @@ export function SelectedSoundsDisplay() {
});
};
// 播放保存的音乐
const playSavedMusic = async (music: SavedMusic) => {
// 清除当前所有声音选择
@ -485,16 +486,7 @@ export function SelectedSoundsDisplay() {
>
{music.name}
</span>
<button
onClick={() => toggleMusicExpansion(music.id)}
className={styles.expandButton}
title="展开/收起声音详情"
>
{expandedMusic.has(music.id) ? '收起 ▲' : '展开 ▼'}
</button>
</div>
{/* 展开时显示收录的声音名字 */}
{expandedMusic.has(music.id) && (
{/* 默认显示收录的声音名字 */}
<div className={styles.soundNames}>
{music.sounds && music.sounds.length > 0 ? (
music.sounds.map((soundId: string, index: number) => {
@ -513,7 +505,7 @@ export function SelectedSoundsDisplay() {
<span className={styles.noSounds}></span>
)}
</div>
)}
</div>
</div>
<button
onClick={() => deleteMusic(music.id.toString())}
@ -522,6 +514,13 @@ export function SelectedSoundsDisplay() {
>
<FaTrash />
</button>
<button
onClick={() => toggleMusicExpansion(music.id)}
className={styles.expandButton}
title="展开/收起声音详情"
>
{expandedMusic.has(music.id) ? '收起 ▲' : '展开 ▼'}
</button>
</div>
)}
</div>

View file

@ -11,14 +11,14 @@
position: relative;
flex-grow: 1;
height: 4px;
background: var(--color-neutral-200);
background: var(--color-control-bg);
border-radius: 9999px;
}
.sliderRange {
position: absolute;
height: 100%;
background: var(--color-neutral-800);
background: var(--color-control-progress);
border-radius: 9999px;
}
@ -27,18 +27,18 @@
width: 16px;
height: 16px;
cursor: pointer;
background: var(--bg-tertiary);
border: 1px solid var(--color-border);
background: var(--color-control-progress);
border: 2px solid var(--color-control-progress);
border-radius: 50%;
box-shadow: 0 0 3px var(--color-neutral-50);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
}
.sliderThumb:hover {
background: var(--bg-secondary);
border-color: var(--color-foreground);
background: var(--color-control-bg-hover);
border-color: var(--color-control-bg-active);
}
.sliderThumb:focus {
outline: none;
box-shadow: 0 0 0 3px var(--color-neutral-400);
box-shadow: 0 0 0 3px var(--color-foreground-subtler);
}

View file

@ -32,83 +32,14 @@
opacity: 1;
}
/* 当进度条禁用时,容器内的图标也要相应调整 */
.volumeContainer:has(.range:disabled) .volumeIcon,
.speedContainer:has(.range:disabled) .speedIcon,
.rateContainer:has(.range:disabled) .rateIcon {
/* 当滑块禁用时,容器内的图标也要相应调整 */
.volumeContainer:has(.slider:disabled) .volumeIcon,
.speedContainer:has(.slider:disabled) .speedIcon,
.rateContainer:has(.slider:disabled) .rateIcon {
opacity: 0.4;
}
.range {
.slider {
width: 100%;
flex: 1;
/********** Range Input Styles **********/
/* Range Reset */
appearance: none;
cursor: pointer;
background: transparent;
/* Removes default focus */
&:focus {
outline: none;
}
&:disabled {
pointer-events: none;
cursor: default;
opacity: 0.5;
}
/***** Chrome, Safari, Opera and Edge Chromium styles *****/
&::-webkit-slider-runnable-track {
height: 0.5rem;
background-color: var(--bg-secondary);
border-radius: 0.5rem;
}
&::-webkit-slider-thumb {
width: 14px;
height: 14px;
margin-top: -3px;
appearance: none;
background-color: var(--color-neutral-700);
border: 1px solid var(--color-border);
border-radius: 50%;
box-shadow: 0 0 2px var(--color-neutral-400);
}
&:not(:disabled):focus::-webkit-slider-thumb {
border: 1px solid var(--color-foreground);
outline: 3px solid var(--color-foreground);
outline-offset: 0.125rem;
}
/******** Firefox styles ********/
&::-moz-range-track {
height: 0.5rem;
background-color: var(--bg-secondary);
border-radius: 0.5rem;
}
&::-moz-range-thumb {
width: 14px;
height: 14px;
margin-top: -3px;
background-color: var(--color-neutral-700);
border: none;
border: 1px solid var(--color-border);
border-radius: 0;
border-radius: 50%;
box-shadow: 0 0 2px var(--color-neutral-400);
}
&:not(:disabled):focus::-moz-range-thumb {
border: 1px solid var(--color-foreground);
outline: 3px solid var(--color-foreground);
outline-offset: 0.125rem;
}
}

View file

@ -1,6 +1,7 @@
import { FaVolumeUp, FaTachometerAlt, FaMusic } from 'react-icons/fa/index';
import { useSoundStore } from '@/stores/sound';
import { useTranslation } from '@/hooks/useTranslation';
import { Slider } from '@/components/slider';
import styles from './range.module.css';
@ -24,55 +25,38 @@ export function Range({ id, label }: RangeProps) {
<div className={styles.controlsContainer}>
<div className={styles.volumeContainer}>
<FaVolumeUp className={styles.volumeIcon} />
<input
aria-label={`${label} ${t('volume').toLowerCase()}`}
autoComplete="off"
className={styles.range}
<Slider
disabled={!isSelected}
max={100}
max={1}
min={0}
type="range"
value={volume * 100}
onClick={e => e.stopPropagation()}
onChange={e =>
!locked && isSelected && setVolume(id, Number(e.target.value) / 100)
}
step={0.01}
value={volume}
onChange={value => !locked && isSelected && setVolume(id, value)}
className={styles.slider}
/>
</div>
<div className={styles.speedContainer}>
<FaTachometerAlt className={styles.speedIcon} />
<input
aria-label={`${label} speed`}
autoComplete="off"
className={styles.range}
<Slider
disabled={!isSelected}
max={200}
min={50}
step={10}
type="range"
value={speed * 100}
onClick={e => e.stopPropagation()}
onChange={e =>
!locked && isSelected && setSpeed(id, Number(e.target.value) / 100)
}
max={2}
min={0.5}
step={0.1}
value={speed}
onChange={value => !locked && isSelected && setSpeed(id, value)}
className={styles.slider}
/>
</div>
<div className={styles.rateContainer}>
<FaMusic className={styles.rateIcon} />
<input
aria-label={`${label} rate`}
autoComplete="off"
className={styles.range}
<Slider
disabled={!isSelected}
max={200}
min={50}
step={10}
type="range"
value={rate * 100}
onClick={e => e.stopPropagation()}
onChange={e =>
!locked && isSelected && setRate(id, Number(e.target.value) / 100)
}
max={2}
min={0.5}
step={0.1}
value={rate}
onChange={value => !locked && isSelected && setRate(id, value)}
className={styles.slider}
/>
</div>
</div>

View file

@ -60,7 +60,7 @@
width: 100%;
height: 100%;
content: '';
background-color: var(--bg-tertiary);
background-color: var(--color-control-bg);
border-radius: 50%;
}

View file

@ -113,8 +113,8 @@
.musicContent {
display: flex;
align-items: center;
gap: 8px;
align-items: flex-start;
gap: 12px;
}
.playButton {
@ -165,7 +165,22 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
gap: 12px;
flex-wrap: wrap;
}
/* 声音名字显示 */
.soundNames {
font-size: 12px;
color: var(--color-foreground-subtle);
line-height: 1.4;
display: flex;
flex-wrap: wrap;
gap: 2px;
align-items: center;
padding: 1px 4px;
flex: 1;
justify-content: center;
}
/* 展开按钮 */
@ -179,8 +194,9 @@
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
height: 20px;
height: 24px;
line-height: 1;
margin-left: 4px;
}
.expandButton:hover {
@ -189,16 +205,16 @@
color: var(--color-foreground);
}
/* 声音名字显示 */
.soundNames {
font-size: 12px;
color: var(--color-foreground-subtle);
line-height: 1.3;
word-break: break-all;
}
.soundName {
color: var(--color-foreground-subtle);
background: rgba(var(--color-muted-rgb), 0.3);
padding: 1px 4px;
border-radius: 2px;
margin-right: 2px;
display: inline-block;
margin-bottom: 2px;
font-size: 11px;
border: 1px solid rgba(var(--color-border-rgb), 0.3);
}
.noSounds {
@ -211,11 +227,13 @@
color: white;
border: none;
border-radius: 4px;
padding: 6px 8px;
padding: 4px 6px;
cursor: pointer;
font-size: 12px;
font-size: 11px;
transition: all 0.2s ease;
flex-shrink: 0;
height: 24px;
margin-top: 2px;
}
.deleteButton:hover {

View file

@ -26,3 +26,40 @@ body {
color: var(--color-foreground);
background-color: var(--color-neutral-300);
}
/* 滚动条样式 - 明亮模式下使用更浅的颜色 */
[data-theme="light"] ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[data-theme="light"] ::-webkit-scrollbar-track {
background: #f3f4f6;
border-radius: 4px;
}
[data-theme="light"] ::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
[data-theme="light"] ::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
border-color: #d1d5db;
}
[data-theme="light"] ::-webkit-scrollbar-thumb:active {
background: #6b7280;
border-color: #9ca3af;
}
/* Firefox 滚动条样式 - 明亮模式 */
[data-theme="light"] * {
scrollbar-width: thin;
scrollbar-color: #d1d5db #f3f4f6;
}
[data-theme="light"] *::-webkit-scrollbar-thumb {
background-color: #d1d5db;
}

View file

@ -15,4 +15,12 @@
--color-foreground: var(--color-neutral-950);
--color-foreground-subtle: var(--color-neutral-600);
--color-foreground-subtler: var(--color-neutral-500);
/* 统一控件背景色 - 使用与声音图标一致的配色方案 */
--color-control-bg: var(--bg-tertiary);
--color-control-bg-hover: var(--bg-quaternary);
--color-control-bg-active: var(--bg-secondary);
/* 拖动条已选中部分颜色 - 只比背景色深一点点 */
--color-control-progress: var(--color-neutral-800);
}