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

View file

@ -11,14 +11,14 @@
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
height: 4px; height: 4px;
background: var(--color-neutral-200); background: var(--color-control-bg);
border-radius: 9999px; border-radius: 9999px;
} }
.sliderRange { .sliderRange {
position: absolute; position: absolute;
height: 100%; height: 100%;
background: var(--color-neutral-800); background: var(--color-control-progress);
border-radius: 9999px; border-radius: 9999px;
} }
@ -27,18 +27,18 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
cursor: pointer; cursor: pointer;
background: var(--bg-tertiary); background: var(--color-control-progress);
border: 1px solid var(--color-border); border: 2px solid var(--color-control-progress);
border-radius: 50%; 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 { .sliderThumb:hover {
background: var(--bg-secondary); background: var(--color-control-bg-hover);
border-color: var(--color-foreground); border-color: var(--color-control-bg-active);
} }
.sliderThumb:focus { .sliderThumb:focus {
outline: none; 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; opacity: 1;
} }
/* 当进度条禁用时,容器内的图标也要相应调整 */ /* 当滑块禁用时,容器内的图标也要相应调整 */
.volumeContainer:has(.range:disabled) .volumeIcon, .volumeContainer:has(.slider:disabled) .volumeIcon,
.speedContainer:has(.range:disabled) .speedIcon, .speedContainer:has(.slider:disabled) .speedIcon,
.rateContainer:has(.range:disabled) .rateIcon { .rateContainer:has(.slider:disabled) .rateIcon {
opacity: 0.4; opacity: 0.4;
} }
.range { .slider {
width: 100%; width: 100%;
flex: 1; 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 { FaVolumeUp, FaTachometerAlt, FaMusic } from 'react-icons/fa/index';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { Slider } from '@/components/slider';
import styles from './range.module.css'; import styles from './range.module.css';
@ -24,55 +25,38 @@ export function Range({ id, label }: RangeProps) {
<div className={styles.controlsContainer}> <div className={styles.controlsContainer}>
<div className={styles.volumeContainer}> <div className={styles.volumeContainer}>
<FaVolumeUp className={styles.volumeIcon} /> <FaVolumeUp className={styles.volumeIcon} />
<input <Slider
aria-label={`${label} ${t('volume').toLowerCase()}`}
autoComplete="off"
className={styles.range}
disabled={!isSelected} disabled={!isSelected}
max={100} max={1}
min={0} min={0}
type="range" step={0.01}
value={volume * 100} value={volume}
onClick={e => e.stopPropagation()} onChange={value => !locked && isSelected && setVolume(id, value)}
onChange={e => className={styles.slider}
!locked && isSelected && setVolume(id, Number(e.target.value) / 100)
}
/> />
</div> </div>
<div className={styles.speedContainer}> <div className={styles.speedContainer}>
<FaTachometerAlt className={styles.speedIcon} /> <FaTachometerAlt className={styles.speedIcon} />
<input <Slider
aria-label={`${label} speed`}
autoComplete="off"
className={styles.range}
disabled={!isSelected} disabled={!isSelected}
max={200} max={2}
min={50} min={0.5}
step={10} step={0.1}
type="range" value={speed}
value={speed * 100} onChange={value => !locked && isSelected && setSpeed(id, value)}
onClick={e => e.stopPropagation()} className={styles.slider}
onChange={e =>
!locked && isSelected && setSpeed(id, Number(e.target.value) / 100)
}
/> />
</div> </div>
<div className={styles.rateContainer}> <div className={styles.rateContainer}>
<FaMusic className={styles.rateIcon} /> <FaMusic className={styles.rateIcon} />
<input <Slider
aria-label={`${label} rate`}
autoComplete="off"
className={styles.range}
disabled={!isSelected} disabled={!isSelected}
max={200} max={2}
min={50} min={0.5}
step={10} step={0.1}
type="range" value={rate}
value={rate * 100} onChange={value => !locked && isSelected && setRate(id, value)}
onClick={e => e.stopPropagation()} className={styles.slider}
onChange={e =>
!locked && isSelected && setRate(id, Number(e.target.value) / 100)
}
/> />
</div> </div>
</div> </div>

View file

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

View file

@ -113,8 +113,8 @@
.musicContent { .musicContent {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 8px; gap: 12px;
} }
.playButton { .playButton {
@ -165,7 +165,22 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; 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; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
height: 20px; height: 24px;
line-height: 1; line-height: 1;
margin-left: 4px;
} }
.expandButton:hover { .expandButton:hover {
@ -189,16 +205,16 @@
color: var(--color-foreground); color: var(--color-foreground);
} }
/* 声音名字显示 */
.soundNames {
font-size: 12px;
color: var(--color-foreground-subtle);
line-height: 1.3;
word-break: break-all;
}
.soundName { .soundName {
color: var(--color-foreground-subtle); 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 { .noSounds {
@ -211,11 +227,13 @@
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
padding: 6px 8px; padding: 4px 6px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 11px;
transition: all 0.2s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
height: 24px;
margin-top: 2px;
} }
.deleteButton:hover { .deleteButton:hover {

View file

@ -26,3 +26,40 @@ body {
color: var(--color-foreground); color: var(--color-foreground);
background-color: var(--color-neutral-300); 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: var(--color-neutral-950);
--color-foreground-subtle: var(--color-neutral-600); --color-foreground-subtle: var(--color-neutral-600);
--color-foreground-subtler: var(--color-neutral-500); --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);
} }