feat: add file

This commit is contained in:
li
2025-09-17 17:24:35 +08:00
parent d4aacd2133
commit 1fdfe40cd3
4 changed files with 168 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
import { ChevronDown, ChevronUp } from "lucide-react";
import { useState } from "react";
interface IProps {
title?: string | React.ReactNode;
className?: string;
children: React.ReactNode;
}
const CollapsibleCard: React.FC<IProps> = (props) => {
const { title = '', className = '', children } = props;
const [isOpen, setIsOpen] = useState(false);
return (
<div className={`border border-gray-200 dark:border-gray-600 rounded-lg p-4 ${className}`}>
<div className="flex justify-between items-center mb-4">
<div>
{
typeof title === 'string' ? <h3 className="text-sm text-gray-900 dark:text-white font-medium mb-4">{title}</h3> : title
}
</div>
<button
onClick={() => setIsOpen(!isOpen)}
>
{
isOpen ? <ChevronUp /> : <ChevronDown />
}
</button>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
{
isOpen && children
}
</div>
</div>
)
}
export default CollapsibleCard;
+60
View File
@@ -0,0 +1,60 @@
import { useState } from "react";
import { debounce } from "lodash";
interface IProps {
isPassword?: boolean;
value?: string;
setValue?: (value: string) => void;
setIsValueValid?: (value: string) => void;
placeholder?: string;
className?: string;
}
const Input: React.FC<IProps> = (props) => {
const { isPassword = false, value = '', setValue = () => {}, setIsValueValid, placeholder = '', className = '' } = props;
const [showPassword, setShowPassword] = useState(!isPassword);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
setIsValueValid?.(e.target.value);
}
return (
<div className={`relative ${className}`}>
<input
type={showPassword ? "text" : "password"}
value={value}
onChange={handleChange}
placeholder={placeholder}
className="w-full px-4 py-3 border border-gray-200 dark:border-gray-600 rounded-lg
bg-gray-50 dark:bg-gray-700
text-gray-900 dark:text-white
placeholder-gray-500 dark:placeholder-gray-400
focus:border-blue-500 dark:focus:border-blue-400
focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20
focus:outline-none"
/>
{
isPassword && <button
type="button"
className="absolute inset-y-0 right-4 flex items-center text-gray-500 dark:text-gray-400
hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
)}
</button>
}
</div>
)
}
export default Input;
+12
View File
@@ -0,0 +1,12 @@
const LoadingIcon = () => {
return (
<span>
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
);
};
export default LoadingIcon;
+56
View File
@@ -0,0 +1,56 @@
import { X } from "lucide-react";
import React, { useEffect } from "react";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string; // 可选:自定义内容区样式
overlayClassName?: string; // 可选:自定义遮罩样式
autoClose?: boolean; // 可选:是否自动关闭
}
const Modal: React.FC<ModalProps> = ({
open,
onClose,
children,
className = "",
overlayClassName = "",
autoClose = true,
}) => {
useEffect(() => {
let timer: NodeJS.Timeout;
if (open && autoClose) {
timer = setTimeout(() => {
onClose();
}, 3000);
}
return () => clearTimeout(timer);
}, [open, autoClose]);
if (!open) return null;
return (
<div
className={`fixed inset-0 z-50 flex items-center justify-center bg-black/40 ${overlayClassName}`}
onClick={onClose}
>
<div
className={`relative bg-white rounded-lg shadow-lg p-6 min-w-[300px] max-w-full ${className}`}
onClick={e => e.stopPropagation()} // 阻止冒泡,点击内容区不关闭
>
<button
className="absolute top-2 right-4 bg-transparent border-none text-gray-400 hover:text-gray-600"
onClick={onClose}
aria-label="Close"
>
<X size={16}/>
</button>
{children}
</div>
</div>
);
};
export default Modal;