feat: 功能迁移过来了,接下来优化样式

This commit is contained in:
许永平
2025-07-06 13:18:08 +08:00
parent ae6821d917
commit 2cdbd4dd73
14 changed files with 2288 additions and 127 deletions

View File

@@ -0,0 +1,45 @@
import React from 'react';
interface AvatarProps {
children: React.ReactNode;
className?: string;
}
export function Avatar({ children, className = '' }: AvatarProps) {
return (
<div className={`relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full ${className}`}>
{children}
</div>
);
}
interface AvatarImageProps {
src?: string;
alt?: string;
className?: string;
}
export function AvatarImage({ src, alt, className = '' }: AvatarImageProps) {
if (!src) return null;
return (
<img
src={src}
alt={alt || '头像'}
className={`aspect-square h-full w-full object-cover ${className}`}
/>
);
}
interface AvatarFallbackProps {
children: React.ReactNode;
className?: string;
}
export function AvatarFallback({ children, className = '' }: AvatarFallbackProps) {
return (
<div className={`flex h-full w-full items-center justify-center rounded-full bg-gray-100 text-gray-600 ${className}`}>
{children}
</div>
);
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'secondary' | 'success' | 'destructive';
variant?: 'default' | 'secondary' | 'success' | 'destructive' | 'outline';
className?: string;
onClick?: (e: React.MouseEvent) => void;
}
@@ -19,7 +19,8 @@ export function Badge({
default: 'bg-blue-100 text-blue-800',
secondary: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
destructive: 'bg-red-100 text-red-800'
destructive: 'bg-red-100 text-red-800',
outline: 'border border-gray-300 bg-white text-gray-700'
};
const classes = `${baseClasses} ${variantClasses[variant]} ${className}`;

View File

@@ -0,0 +1,109 @@
import React, { useState, useRef, useEffect } from 'react';
interface DropdownMenuProps {
children: React.ReactNode;
}
export function DropdownMenu({ children }: DropdownMenuProps) {
return <>{children}</>;
}
interface DropdownMenuTriggerProps {
children: React.ReactNode;
asChild?: boolean;
}
export function DropdownMenuTrigger({ children }: DropdownMenuTriggerProps) {
return <>{children}</>;
}
interface DropdownMenuContentProps {
children: React.ReactNode;
align?: 'start' | 'center' | 'end';
}
export function DropdownMenuContent({ children, align = 'end' }: DropdownMenuContentProps) {
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const trigger = triggerRef.current;
if (!trigger) return;
const handleClick = () => setIsOpen(!isOpen);
trigger.addEventListener('click', handleClick);
return () => {
trigger.removeEventListener('click', handleClick);
};
}, [isOpen]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
contentRef.current &&
!contentRef.current.contains(event.target as Node) &&
triggerRef.current &&
!triggerRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative">
<div ref={triggerRef}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
...child.props,
children: (
<>
{child.props.children}
{isOpen && (
<div
ref={contentRef}
className={`absolute z-50 mt-2 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 shadow-md ${
align === 'start' ? 'left-0' : align === 'center' ? 'left-1/2 transform -translate-x-1/2' : 'right-0'
}`}
>
{children}
</div>
)}
</>
)
});
}
return child;
})}
</div>
</div>
);
}
interface DropdownMenuItemProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export function DropdownMenuItem({ children, onClick, disabled = false }: DropdownMenuItemProps) {
return (
<button
className={`relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100 disabled:pointer-events-none disabled:opacity-50 ${
disabled ? 'cursor-not-allowed opacity-50' : ''
}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}

View File

@@ -3,6 +3,7 @@ import React from 'react';
interface InputProps {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
placeholder?: string;
className?: string;
readOnly?: boolean;
@@ -12,6 +13,7 @@ interface InputProps {
export function Input({
value,
onChange,
onKeyDown,
placeholder,
className = '',
readOnly = false,
@@ -23,6 +25,7 @@ export function Input({
type="text"
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={placeholder}
readOnly={readOnly}
className={`flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}

View File

@@ -0,0 +1,17 @@
import React from 'react';
interface ProgressProps {
value: number;
className?: string;
}
export function Progress({ value, className = '' }: ProgressProps) {
return (
<div className={`w-full bg-gray-200 rounded-full h-2 ${className}`}>
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
/>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
interface SwitchProps {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}
export function Switch({ checked, onCheckedChange, disabled = false, className = '' }: SwitchProps) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onCheckedChange(!checked)}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50
${checked ? 'bg-blue-600' : 'bg-gray-200'}
${className}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${checked ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
);
}

View File

@@ -0,0 +1,52 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };