- web/: React + Vite + TypeScript 前端 - backend/: Django + DRF + SimpleJWT 后端 - prototype/: HTML 设计原型 - docs/: PRD 和设计评审文档 - test: 单元测试 + E2E 极限测试
61 lines
1.7 KiB
TypeScript
61 lines
1.7 KiB
TypeScript
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
|
import styles from './Dropdown.module.css';
|
|
|
|
interface DropdownItem {
|
|
label: string;
|
|
value: string;
|
|
icon?: ReactNode;
|
|
}
|
|
|
|
interface DropdownProps {
|
|
items: DropdownItem[];
|
|
value: string;
|
|
onSelect: (value: string) => void;
|
|
trigger: ReactNode;
|
|
minWidth?: number;
|
|
}
|
|
|
|
export function Dropdown({ items, value, onSelect, trigger, minWidth = 150 }: DropdownProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
function handleClick(e: MouseEvent) {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handleClick);
|
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
}, []);
|
|
|
|
return (
|
|
<div className={styles.wrapper} ref={ref}>
|
|
<div onClick={() => setOpen(!open)}>
|
|
{trigger}
|
|
</div>
|
|
<div
|
|
className={`${styles.menu} ${open ? styles.open : ''}`}
|
|
style={{ minWidth }}
|
|
>
|
|
{items.map((item) => (
|
|
<div
|
|
key={item.value}
|
|
className={`${styles.item} ${value === item.value ? styles.selected : ''}`}
|
|
onClick={() => {
|
|
onSelect(item.value);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
{item.icon && <span className={styles.itemIcon}>{item.icon}</span>}
|
|
<span>{item.label}</span>
|
|
<svg className={styles.check} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|