初版编写交叉评查页面,增加非范围日期选择器

This commit is contained in:
2025-07-16 19:01:13 +08:00
parent 328f326db3
commit 277c54f34d
4 changed files with 687 additions and 174 deletions
+83
View File
@@ -0,0 +1,83 @@
import React, { useState, useEffect, useRef } from 'react';
type Option = {
value: string;
label: string;
children?: Option[];
};
type CascaderProps = {
options: Option[];
defaultValue?: string[];
onChange?: (value: string[]) => void;
placeholder?: string;
};
const Cascader: React.FC<CascaderProps> = ({ options, defaultValue = [], onChange, placeholder = 'Select' }) => {
const [visible, setVisible] = useState(false);
const [selected, setSelected] = useState<string[]>(defaultValue);
const [menus, setMenus] = useState<Option[][]>([options]);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setVisible(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleItemClick = (option: Option, level: number) => {
const newSelected = selected.slice(0, level).concat(option.value);
setSelected(newSelected);
if (option.children) {
setMenus(menus.slice(0, level + 1).concat([option.children]));
} else {
setMenus(menus.slice(0, level + 1));
setVisible(false);
onChange?.(newSelected);
}
};
const getDisplayText = () => {
if (selected.length === 0) return placeholder;
let currentOptions = options;
return selected.map(val => {
const opt = currentOptions.find(o => o.value === val);
currentOptions = opt?.children || [];
return opt?.label || '';
}).join(' / ');
};
return (
<div ref={containerRef} className="relative inline-block">
<div
className="border border-gray-300 rounded px-3 py-1 cursor-pointer"
onClick={() => setVisible(!visible)}
>
{getDisplayText()}
</div>
{visible && (
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 flex">
{menus.map((menu, level) => (
<ul key={level} className="list-none m-0 p-0 min-w-[150px] border-r border-gray-200 last:border-r-0">
{menu.map(option => (
<li
key={option.value}
className={`px-3 py-1 cursor-pointer hover:bg-gray-100 ${selected[level] === option.value ? 'bg-gray-200' : ''}`}
onClick={() => handleItemClick(option, level)}
>
{option.label}
</li>
))}
</ul>
))}
</div>
)}
</div>
);
};
export default Cascader;
+123 -1
View File
@@ -16,6 +16,17 @@ export interface DateRangePickerProps {
colorMode?: 'light' | 'dark' | 'auto';
}
export interface SingleDatePickerProps {
date: string;
onDateChange: (value: string) => void;
label?: string;
className?: string;
id?: string;
format?: string;
placeholder?: string;
colorMode?: 'light' | 'dark' | 'auto';
}
export function links() {
return [
{ rel: "stylesheet", href: dateRangePickerStyles }
@@ -199,6 +210,117 @@ export function DateRangePicker({
);
}
/**
* 单日期选择器组件
* 用于选择单个日期
*/
export function SingleDatePicker({
date,
onDateChange,
label = "日期",
className = "",
id = "date-single",
format = "yyyy-MM-dd",
placeholder = "请选择日期",
colorMode = 'auto'
}: SingleDatePickerProps) {
// 使用ref获取input元素
const inputRef = useRef<HTMLInputElement>(null);
// 添加状态跟踪日期输入框是否被聚焦
const [isFocused, setIsFocused] = useState<boolean>(false);
// 格式化显示日期
const formattedDate = date ? formatDate(date, format) : "";
// 保持原始日期输入框的显示格式
useEffect(() => {
if (inputRef.current) {
inputRef.current.setAttribute('data-display-value', formattedDate || placeholder);
}
}, [formattedDate, placeholder]);
// 处理日期输入框全局点击
const handleWrapperClick = () => {
if (inputRef.current) {
// 点击整个包装器区域时,触发输入框点击
inputRef.current.focus();
inputRef.current.click();
try {
// 检查并使用showPicker API
const hasShowPicker = typeof inputRef.current.showPicker === 'function';
// 尝试使用showPicker API
if (hasShowPicker) {
inputRef.current.showPicker();
}
} catch (error) {
console.error('Failed to show date picker:', error);
}
}
};
// 处理键盘事件
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleWrapperClick();
}
};
// 处理聚焦和失焦
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
setIsFocused(false);
};
// 获取颜色模式类名
const getColorModeClass = () => {
if (colorMode === 'light') return 'color-mode-light';
if (colorMode === 'dark') return 'color-mode-dark';
return ''; // auto模式不添加额外类名
};
return (
<div className={`date-picker single-date-picker ${getColorModeClass()} ${className}`}>
<div className="date-field">
<label htmlFor={id} className="date-label">{label}</label>
<div
className={`date-input-wrapper ${isFocused ? 'focused' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleWrapperClick();
}}
onKeyDown={handleKeyDown}
tabIndex={0}
role="button"
aria-label={`选择${label}`}
>
<input
id={id}
ref={inputRef}
type="date"
className="date-input"
value={date}
onChange={(e) => onDateChange(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
/>
<div
className={`date-display ${!formattedDate ? 'placeholder' : ''}`}
>
{formattedDate || placeholder}
</div>
</div>
</div>
</div>
);
}
// 简化版日期范围选择器,适用于紧凑布局,不显示标签
export function SimpleDateRangePicker({
startDate,
@@ -344,4 +466,4 @@ export function SimpleDateRangePicker({
</div>
</div>
);
}
}