v0.8.2: DatePicker/Select 暗色主题、公告跑马灯、Toast 全局化、失败原因 tooltip v0.8.3: 团队详情抽屉→弹窗重构 + 修改秒数池功能 + member_count 修复 v0.8.4: AdminAuditLog 模型 + 12 处管理操作埋点 + 日志查询页面(/admin/logs) 审计日志覆盖所有管理员 mutation 操作(充值、修改额度、创建/禁用用户等), 记录操作人、变更前后值、IP 地址,支持按操作类型/操作人/日期筛选。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import styles from './Select.module.css';
|
|
|
|
interface SelectOption {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
interface SelectProps {
|
|
options: SelectOption[];
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
minWidth?: number;
|
|
}
|
|
|
|
export function Select({ options, value, onChange, placeholder = '请选择', minWidth = 120 }: SelectProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const handle = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handle);
|
|
return () => document.removeEventListener('mousedown', handle);
|
|
}, [open]);
|
|
|
|
const selected = options.find((o) => o.value === value);
|
|
|
|
return (
|
|
<div className={styles.wrapper} ref={ref}>
|
|
<button
|
|
type="button"
|
|
className={styles.trigger}
|
|
style={{ minWidth }}
|
|
onClick={() => setOpen(!open)}
|
|
>
|
|
{selected ? (
|
|
<span className={styles.label}>{selected.label}</span>
|
|
) : (
|
|
<span className={styles.placeholder}>{placeholder}</span>
|
|
)}
|
|
<svg
|
|
className={`${styles.arrow} ${open ? styles.arrowOpen : ''}`}
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<polyline points="6 9 12 15 18 9" />
|
|
</svg>
|
|
</button>
|
|
<div
|
|
className={`${styles.menu} ${open ? styles.open : ''}`}
|
|
style={{ minWidth }}
|
|
>
|
|
{options.map((opt) => (
|
|
<div
|
|
key={opt.value}
|
|
className={`${styles.item} ${value === opt.value ? styles.selected : ''}`}
|
|
onClick={() => {
|
|
onChange(opt.value);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<span>{opt.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>
|
|
);
|
|
}
|