Skip to content

Commit 90da347

Browse files
committed
feat: 添加自定义速率选择器组件,替换原生select
1 parent c1d026d commit 90da347

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
.container {
2+
display: flex;
3+
align-items: center;
4+
gap: 8px;
5+
margin-left: 12px;
6+
padding-left: 12px;
7+
border-left: 1px solid #3d3d5c;
8+
}
9+
10+
.label {
11+
color: #888;
12+
font-size: 0.875rem;
13+
}
14+
15+
.selector {
16+
position: relative;
17+
}
18+
19+
.trigger {
20+
display: flex;
21+
align-items: center;
22+
gap: 8px;
23+
padding: 8px 12px;
24+
background: #2d2d44;
25+
color: #fff;
26+
border: 1px solid #3d3d5c;
27+
border-radius: 6px;
28+
cursor: pointer;
29+
transition: all 0.2s;
30+
min-width: 80px;
31+
justify-content: space-between;
32+
}
33+
34+
.trigger:hover {
35+
background: #3d3d5c;
36+
border-color: #4caf50;
37+
}
38+
39+
.value {
40+
font-size: 0.875rem;
41+
font-weight: 500;
42+
}
43+
44+
.arrow {
45+
font-size: 0.625rem;
46+
color: #888;
47+
transition: transform 0.2s;
48+
}
49+
50+
.arrowUp {
51+
transform: rotate(180deg);
52+
}
53+
54+
.dropdown {
55+
position: absolute;
56+
bottom: 100%;
57+
left: 0;
58+
right: 0;
59+
margin-bottom: 4px;
60+
background: #2d2d44;
61+
border: 1px solid #3d3d5c;
62+
border-radius: 6px;
63+
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
64+
overflow: hidden;
65+
z-index: 100;
66+
}
67+
68+
.option {
69+
display: flex;
70+
align-items: center;
71+
justify-content: space-between;
72+
width: 100%;
73+
padding: 10px 12px;
74+
background: transparent;
75+
color: #ccc;
76+
border: none;
77+
cursor: pointer;
78+
transition: all 0.15s;
79+
font-size: 0.875rem;
80+
text-align: left;
81+
}
82+
83+
.option:hover {
84+
background: #3d3d5c;
85+
color: #fff;
86+
}
87+
88+
.option.selected {
89+
background: rgba(76, 175, 80, 0.2);
90+
color: #4caf50;
91+
}
92+
93+
.checkmark {
94+
color: #4caf50;
95+
font-size: 0.75rem;
96+
}

src/components/SpeedSelector.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useState, useRef, useEffect } from 'react'
2+
import type { PlaybackRate } from '../types'
3+
import { PLAYBACK_RATES } from '../hooks/useAlgorithmPlayer'
4+
import styles from './SpeedSelector.module.css'
5+
6+
interface SpeedSelectorProps {
7+
value: PlaybackRate
8+
onChange: (rate: PlaybackRate) => void
9+
}
10+
11+
export function SpeedSelector({ value, onChange }: SpeedSelectorProps) {
12+
const [isOpen, setIsOpen] = useState(false)
13+
const containerRef = useRef<HTMLDivElement>(null)
14+
15+
// 点击外部关闭下拉菜单
16+
useEffect(() => {
17+
const handleClickOutside = (event: MouseEvent) => {
18+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
19+
setIsOpen(false)
20+
}
21+
}
22+
23+
document.addEventListener('mousedown', handleClickOutside)
24+
return () => document.removeEventListener('mousedown', handleClickOutside)
25+
}, [])
26+
27+
const handleSelect = (rate: PlaybackRate) => {
28+
onChange(rate)
29+
setIsOpen(false)
30+
}
31+
32+
return (
33+
<div className={styles.container} ref={containerRef}>
34+
<span className={styles.label}>速率:</span>
35+
<div className={styles.selector}>
36+
<button
37+
className={styles.trigger}
38+
onClick={() => setIsOpen(!isOpen)}
39+
aria-expanded={isOpen}
40+
aria-haspopup="listbox"
41+
>
42+
<span className={styles.value}>{value}x</span>
43+
<span className={`${styles.arrow} ${isOpen ? styles.arrowUp : ''}`}></span>
44+
</button>
45+
46+
{isOpen && (
47+
<div className={styles.dropdown} role="listbox">
48+
{PLAYBACK_RATES.map((rate) => (
49+
<button
50+
key={rate}
51+
className={`${styles.option} ${rate === value ? styles.selected : ''}`}
52+
onClick={() => handleSelect(rate)}
53+
role="option"
54+
aria-selected={rate === value}
55+
>
56+
{rate}x
57+
{rate === value && <span className={styles.checkmark}></span>}
58+
</button>
59+
))}
60+
</div>
61+
)}
62+
</div>
63+
</div>
64+
)
65+
}

0 commit comments

Comments
 (0)