- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|