Files
ftb-project-management/apps/web/components/MonthPicker.tsx
Script Generator 9a0b16a8f1 feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择
- 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出
- 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置
- 角色管理:卡片列表、系统角色保护、CRUD
- 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页
- 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 18:16:18 +08:00

120 lines
4.2 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
interface MonthPickerProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
const MONTHS = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
export function MonthPicker({ value, onChange, placeholder = '选择月份' }: MonthPickerProps) {
const [open, setOpen] = useState(false);
const [year, setYear] = useState(() => {
if (value) return parseInt(value.slice(0, 4));
return new Date().getFullYear();
});
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const selectedMonth = value ? parseInt(value.slice(5, 7)) : null;
const selectedYear = value ? parseInt(value.slice(0, 4)) : null;
const handleSelect = (month: number) => {
const m = String(month).padStart(2, '0');
onChange(`${year}-${m}`);
setOpen(false);
};
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
onChange('');
setOpen(false);
};
const displayText = value
? `${selectedYear}${selectedMonth}`
: '';
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex h-8 items-center gap-2 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[12px] text-[var(--ink-soft)] hover:border-[var(--accent)] transition-colors min-w-[120px]"
>
<Calendar className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
<span className={displayText ? 'text-[var(--ink)]' : 'text-[var(--ink-muted)]'}>
{displayText || placeholder}
</span>
{value && (
<span
onClick={handleClear}
className="ml-auto text-[var(--ink-muted)] hover:text-[var(--ink)] text-[10px] leading-none"
>
</span>
)}
</button>
{open && (
<div className="absolute top-full left-0 mt-1 z-50 w-[240px] rounded-xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-md)] p-3">
{/* Year nav */}
<div className="flex items-center justify-between mb-3">
<button
type="button"
onClick={() => setYear(year - 1)}
className="p-1 rounded-md hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)] hover:text-[var(--ink)]"
>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-[13px] font-semibold text-[var(--ink)]">{year}</span>
<button
type="button"
onClick={() => setYear(year + 1)}
className="p-1 rounded-md hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)] hover:text-[var(--ink)]"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
{/* Month grid */}
<div className="grid grid-cols-3 gap-1.5">
{MONTHS.map((label, i) => {
const month = i + 1;
const isSelected = selectedYear === year && selectedMonth === month;
const isCurrent = new Date().getFullYear() === year && new Date().getMonth() === i;
return (
<button
key={month}
type="button"
onClick={() => handleSelect(month)}
className={`h-8 rounded-lg text-[12px] font-medium transition-colors ${
isSelected
? 'bg-[var(--accent)] text-white'
: isCurrent
? 'border border-[var(--accent)] text-[var(--accent)] hover:bg-[var(--accent-soft)]'
: 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]'
}`}
>
{label}
</button>
);
})}
</div>
</div>
)}
</div>
);
}