Files
leaudit-platform-frontend/app/components/ui/DateRangePicker.tsx
T

347 lines
11 KiB
TypeScript

import { useRef, useEffect, useState } from "react";
import dateRangePickerStyles from "~/styles/components/date-range-picker.css?url";
export interface DateRangePickerProps {
startDate: string;
endDate: string;
onStartDateChange: (value: string) => void;
onEndDateChange: (value: string) => void;
startLabel?: string;
endLabel?: string;
className?: string;
startId?: string;
endId?: string;
format?: string;
placeholder?: string;
colorMode?: 'light' | 'dark' | 'auto';
}
export function links() {
return [
{ rel: "stylesheet", href: dateRangePickerStyles }
];
}
// 格式化日期函数
function formatDate(dateString: string, format = "yyyy-MM-dd"): string {
if (!dateString) return "";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return dateString;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return format
.replace("yyyy", year.toString())
.replace("MM", month)
.replace("dd", day);
} catch (e) {
return dateString;
}
}
/**
* 日期范围选择器组件
* 用于选择日期范围,如开始日期和结束日期
*/
export function DateRangePicker({
startDate,
endDate,
onStartDateChange,
onEndDateChange,
startLabel = "从",
endLabel = "至",
className = "",
startId = "date-start",
endId = "date-end",
format = "yyyy-MM-dd",
placeholder = "请选择时间",
colorMode = 'auto'
}: DateRangePickerProps) {
// 使用ref获取input元素
const startInputRef = useRef<HTMLInputElement>(null);
const endInputRef = useRef<HTMLInputElement>(null);
// 添加状态跟踪哪个日期输入框被聚焦
const [focusedInput, setFocusedInput] = useState<string | null>(null);
// 格式化显示日期
const formattedStartDate = startDate ? formatDate(startDate, format) : "";
const formattedEndDate = endDate ? formatDate(endDate, format) : "";
// 保持原始日期输入框的显示格式
useEffect(() => {
if (startInputRef.current) {
startInputRef.current.setAttribute('data-display-value', formattedStartDate || placeholder);
}
if (endInputRef.current) {
endInputRef.current.setAttribute('data-display-value', formattedEndDate || placeholder);
}
}, [formattedStartDate, formattedEndDate, placeholder]);
// 处理日期输入框全局点击
const handleWrapperClick = (inputRef: React.RefObject<HTMLInputElement>) => {
if (inputRef.current) {
// console.log('Wrapper clicked, triggering date input');
// 点击整个包装器区域时,触发输入框点击
inputRef.current.focus();
inputRef.current.click();
try {
// 检查并记录showPicker是否可用
const hasShowPicker = typeof inputRef.current.showPicker === 'function';
// console.log('showPicker API available:', hasShowPicker);
// 尝试使用showPicker API
if (hasShowPicker) {
inputRef.current.showPicker();
// console.log('showPicker called successfully');
}
} catch (error) {
console.error('Failed to show date picker:', error);
}
}
};
// 处理键盘事件
const handleKeyDown = (e: React.KeyboardEvent, inputRef: React.RefObject<HTMLInputElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleWrapperClick(inputRef);
}
};
// 处理聚焦和失焦
const handleFocus = (id: string) => {
setFocusedInput(id);
};
const handleBlur = () => {
setFocusedInput(null);
};
// 获取颜色模式类名
const getColorModeClass = () => {
if (colorMode === 'light') return 'color-mode-light';
if (colorMode === 'dark') return 'color-mode-dark';
return ''; // auto模式不添加额外类名
};
return (
<div className={`date-range-picker ${getColorModeClass()} ${className}`}>
<div className="date-range-fields">
<div className="date-field">
<label htmlFor={startId} className="date-label">{startLabel}</label>
<div
className={`date-input-wrapper ${focusedInput === startId ? 'focused' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleWrapperClick(startInputRef);
}}
onKeyDown={(e) => handleKeyDown(e, startInputRef)}
tabIndex={0}
role="button"
aria-label={`选择${startLabel}日期`}
>
<input
id={startId}
ref={startInputRef}
type="date"
className="date-input"
value={startDate}
onChange={(e) => onStartDateChange(e.target.value)}
onFocus={() => handleFocus(startId)}
onBlur={handleBlur}
/>
<div
className={`date-display ${!formattedStartDate ? 'placeholder' : ''}`}
>
{formattedStartDate || placeholder}
</div>
</div>
</div>
<span className="date-separator"></span>
<div className="date-field">
<label htmlFor={endId} className="date-label">{endLabel}</label>
<div
className={`date-input-wrapper ${focusedInput === endId ? 'focused' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleWrapperClick(endInputRef);
}}
onKeyDown={(e) => handleKeyDown(e, endInputRef)}
tabIndex={0}
role="button"
aria-label={`选择${endLabel}日期`}
>
<input
id={endId}
ref={endInputRef}
type="date"
className="date-input"
value={endDate}
onChange={(e) => onEndDateChange(e.target.value)}
onFocus={() => handleFocus(endId)}
onBlur={handleBlur}
/>
<div
className={`date-display ${!formattedEndDate ? 'placeholder' : ''}`}
>
{formattedEndDate || placeholder}
</div>
</div>
</div>
</div>
</div>
);
}
// 简化版日期范围选择器,适用于紧凑布局,不显示标签
export function SimpleDateRangePicker({
startDate,
endDate,
onStartDateChange,
onEndDateChange,
className = "",
startId = "date-start-simple",
endId = "date-end-simple",
format = "yyyy-MM-dd",
placeholder = "请选择时间",
colorMode = 'auto'
}: Omit<DateRangePickerProps, 'startLabel' | 'endLabel'>) {
// 使用ref获取input元素
const startInputRef = useRef<HTMLInputElement>(null);
const endInputRef = useRef<HTMLInputElement>(null);
// 添加状态跟踪哪个日期输入框被聚焦
const [focusedInput, setFocusedInput] = useState<string | null>(null);
// 格式化显示日期
const formattedStartDate = startDate ? formatDate(startDate, format) : "";
const formattedEndDate = endDate ? formatDate(endDate, format) : "";
// 保持原始日期输入框的显示格式
useEffect(() => {
if (startInputRef.current) {
startInputRef.current.setAttribute('data-display-value', formattedStartDate || placeholder);
}
if (endInputRef.current) {
endInputRef.current.setAttribute('data-display-value', formattedEndDate || placeholder);
}
}, [formattedStartDate, formattedEndDate, placeholder]);
// 处理日期输入框全局点击
const handleWrapperClick = (inputRef: React.RefObject<HTMLInputElement>) => {
if (inputRef.current) {
// console.log('Wrapper clicked, triggering date input');
// 点击整个包装器区域时,触发输入框点击
inputRef.current.focus();
inputRef.current.click();
try {
// 检查并记录showPicker是否可用
const hasShowPicker = typeof inputRef.current.showPicker === 'function';
// console.log('showPicker API available:', hasShowPicker);
// 尝试使用showPicker API
if (hasShowPicker) {
inputRef.current.showPicker();
// console.log('showPicker called successfully');
}
} catch (error) {
console.error('Failed to show date picker:', error);
}
}
};
// 处理键盘事件
const handleKeyDown = (e: React.KeyboardEvent, inputRef: React.RefObject<HTMLInputElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleWrapperClick(inputRef);
}
};
// 处理聚焦和失焦
const handleFocus = (id: string) => {
setFocusedInput(id);
};
const handleBlur = () => {
setFocusedInput(null);
};
// 获取颜色模式类名
const getColorModeClass = () => {
if (colorMode === 'light') return 'color-mode-light';
if (colorMode === 'dark') return 'color-mode-dark';
return ''; // auto模式不添加额外类名
};
return (
<div className={`date-range-picker simple-date-range-picker ${getColorModeClass()} ${className}`}>
<div className="date-range-fields">
<div
className={`date-input-wrapper ${focusedInput === startId ? 'focused' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleWrapperClick(startInputRef);
}}
onKeyDown={(e) => handleKeyDown(e, startInputRef)}
tabIndex={0}
role="button"
aria-label="选择开始日期"
>
<input
id={startId}
ref={startInputRef}
type="date"
className="date-input"
value={startDate}
onChange={(e) => onStartDateChange(e.target.value)}
onFocus={() => handleFocus(startId)}
onBlur={handleBlur}
aria-label="开始日期"
/>
<div
className={`date-display ${!formattedStartDate ? 'placeholder' : ''}`}
>
{formattedStartDate || placeholder}
</div>
</div>
<span className="date-separator"></span>
<div
className={`date-input-wrapper ${focusedInput === endId ? 'focused' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleWrapperClick(endInputRef);
}}
onKeyDown={(e) => handleKeyDown(e, endInputRef)}
tabIndex={0}
role="button"
aria-label="选择结束日期"
>
<input
id={endId}
ref={endInputRef}
type="date"
className="date-input"
value={endDate}
onChange={(e) => onEndDateChange(e.target.value)}
onFocus={() => handleFocus(endId)}
onBlur={handleBlur}
aria-label="结束日期"
/>
<div
className={`date-display ${!formattedEndDate ? 'placeholder' : ''}`}
>
{formattedEndDate || placeholder}
</div>
</div>
</div>
</div>
);
}