mirror of
https://github.com/patdelphi/suanming.git
synced 2026-03-07 00:53:11 +08:00
Merge dev branch: Complete UI optimization with Chinese design system
- Implement comprehensive Chinese-style component library - Add unified typography system with semantic font classes - Optimize all pages with responsive design and Chinese aesthetics - Fix button styling and enhance user experience - Add loading states, empty states, and toast notifications - Complete 12 Palaces Details optimization - Establish consistent color scheme and visual hierarchy
This commit is contained in:
98
src/components/ui/ChineseButton.tsx
Normal file
98
src/components/ui/ChineseButton.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface ChineseButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseButton = React.forwardRef<HTMLButtonElement, ChineseButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', children, ...props }, ref) => {
|
||||
const baseClasses = [
|
||||
'inline-flex items-center justify-center',
|
||||
'font-chinese font-medium',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'relative overflow-hidden',
|
||||
'active:scale-95 hover-lift',
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
primary: [
|
||||
'bg-gradient-to-r from-red-600 to-red-700 !text-white',
|
||||
'border border-red-600',
|
||||
'shadow-lg hover:shadow-xl',
|
||||
'hover:scale-105 active:scale-95 hover:!text-white',
|
||||
'focus:ring-red-500',
|
||||
'relative overflow-hidden',
|
||||
'before:absolute before:inset-0',
|
||||
'before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent',
|
||||
'before:translate-x-[-100%] hover:before:translate-x-[100%]',
|
||||
'before:transition-transform before:duration-700',
|
||||
],
|
||||
secondary: [
|
||||
'bg-gradient-to-r from-yellow-400 to-yellow-500 text-gray-900',
|
||||
'border border-yellow-500',
|
||||
'shadow-lg hover:shadow-xl',
|
||||
'hover:scale-105 active:scale-95',
|
||||
'focus:ring-yellow-500',
|
||||
],
|
||||
outline: [
|
||||
'bg-transparent text-red-600',
|
||||
'border-2 border-red-600',
|
||||
'hover:bg-red-600 hover:text-white',
|
||||
'focus:ring-red-500',
|
||||
],
|
||||
ghost: [
|
||||
'bg-transparent text-gray-700',
|
||||
'hover:bg-gray-100 hover:text-red-600',
|
||||
'focus:ring-gray-500',
|
||||
],
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: [
|
||||
'px-3 py-1.5 text-button-sm rounded-md',
|
||||
'min-h-[36px]', // 移动端友好的最小高度
|
||||
],
|
||||
md: [
|
||||
'px-6 py-2.5 text-button-md rounded-lg',
|
||||
'min-h-[44px]', // 移动端友好的最小高度
|
||||
],
|
||||
lg: [
|
||||
'px-8 py-3 text-button-lg rounded-xl',
|
||||
'min-h-[52px]', // 移动端友好的最小高度
|
||||
],
|
||||
};
|
||||
|
||||
// 移动端响应式调整
|
||||
const responsiveClasses = [
|
||||
'md:hover:scale-105', // 只在桌面端启用悬停缩放
|
||||
'active:scale-95', // 所有设备都有点击反馈
|
||||
'touch-manipulation', // 优化触摸体验
|
||||
];
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
baseClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
responsiveClasses,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseButton.displayName = 'ChineseButton';
|
||||
|
||||
export { ChineseButton };
|
||||
export type { ChineseButtonProps };
|
||||
233
src/components/ui/ChineseCard.tsx
Normal file
233
src/components/ui/ChineseCard.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface ChineseCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'elevated' | 'bordered' | 'golden';
|
||||
padding?: 'sm' | 'md' | 'lg';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseCard = React.forwardRef<HTMLDivElement, ChineseCardProps>(
|
||||
({ className, variant = 'default', padding = 'md', children, ...props }, ref) => {
|
||||
const baseClasses = [
|
||||
'relative',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
'font-chinese hover-lift animate-fade-in-up',
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
default: [
|
||||
'bg-white/90 backdrop-blur-sm',
|
||||
'border border-paper-300',
|
||||
'rounded-lg',
|
||||
'shadow-chinese-sm hover:shadow-chinese',
|
||||
],
|
||||
elevated: [
|
||||
'bg-white/95 backdrop-blur-md',
|
||||
'border border-cinnabar-200',
|
||||
'rounded-xl',
|
||||
'shadow-chinese hover:shadow-chinese-md',
|
||||
'hover:-translate-y-1',
|
||||
],
|
||||
bordered: [
|
||||
'bg-paper-50/80 backdrop-blur-sm',
|
||||
'border-2 border-cinnabar-300',
|
||||
'rounded-lg',
|
||||
'shadow-paper',
|
||||
// 传统边框装饰
|
||||
'before:absolute before:inset-2',
|
||||
'before:border before:border-gold-300/50',
|
||||
'before:rounded-md before:pointer-events-none',
|
||||
],
|
||||
golden: [
|
||||
'bg-gold-gradient',
|
||||
'border-2 border-gold-600',
|
||||
'rounded-xl',
|
||||
'shadow-gold hover:shadow-gold',
|
||||
'text-ink-900',
|
||||
// 金色光晕效果
|
||||
'before:absolute before:inset-0',
|
||||
'before:bg-gradient-to-br before:from-white/20 before:to-transparent',
|
||||
'before:rounded-xl before:pointer-events-none',
|
||||
],
|
||||
};
|
||||
|
||||
const paddingClasses = {
|
||||
sm: 'p-4',
|
||||
md: 'p-6',
|
||||
lg: 'p-8',
|
||||
};
|
||||
|
||||
// 移动端响应式调整
|
||||
const responsiveClasses = [
|
||||
// 移动端减少内边距
|
||||
'max-md:p-4',
|
||||
// 移动端优化圆角
|
||||
'max-md:rounded-lg',
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
baseClasses,
|
||||
variantClasses[variant],
|
||||
paddingClasses[padding],
|
||||
responsiveClasses,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseCard.displayName = 'ChineseCard';
|
||||
|
||||
// 卡片标题组件
|
||||
interface ChineseCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseCardHeader = React.forwardRef<HTMLDivElement, ChineseCardHeaderProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5',
|
||||
'pb-4 mb-4',
|
||||
'border-b border-cinnabar-200',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseCardHeader.displayName = 'ChineseCardHeader';
|
||||
|
||||
// 卡片标题文字组件
|
||||
interface ChineseCardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseCardTitle = React.forwardRef<HTMLParagraphElement, ChineseCardTitleProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
'text-heading-md font-semibold leading-none tracking-tight',
|
||||
'text-cinnabar-500',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseCardTitle.displayName = 'ChineseCardTitle';
|
||||
|
||||
// 卡片描述组件
|
||||
interface ChineseCardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseCardDescription = React.forwardRef<HTMLParagraphElement, ChineseCardDescriptionProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
'text-body-md text-ink-500',
|
||||
'font-chinese',
|
||||
'leading-relaxed',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseCardDescription.displayName = 'ChineseCardDescription';
|
||||
|
||||
// 卡片内容组件
|
||||
interface ChineseCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseCardContent = React.forwardRef<HTMLDivElement, ChineseCardContentProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-ink-900',
|
||||
'leading-relaxed',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseCardContent.displayName = 'ChineseCardContent';
|
||||
|
||||
// 卡片底部组件
|
||||
interface ChineseCardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChineseCardFooter = React.forwardRef<HTMLDivElement, ChineseCardFooterProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
'pt-4 mt-4',
|
||||
'border-t border-paper-300',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseCardFooter.displayName = 'ChineseCardFooter';
|
||||
|
||||
export {
|
||||
ChineseCard,
|
||||
ChineseCardHeader,
|
||||
ChineseCardTitle,
|
||||
ChineseCardDescription,
|
||||
ChineseCardContent,
|
||||
ChineseCardFooter,
|
||||
};
|
||||
|
||||
export type {
|
||||
ChineseCardProps,
|
||||
ChineseCardHeaderProps,
|
||||
ChineseCardTitleProps,
|
||||
ChineseCardDescriptionProps,
|
||||
ChineseCardContentProps,
|
||||
ChineseCardFooterProps,
|
||||
};
|
||||
91
src/components/ui/ChineseEmpty.tsx
Normal file
91
src/components/ui/ChineseEmpty.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { FileX, Search, Inbox, AlertCircle } from 'lucide-react';
|
||||
import { ChineseButton } from './ChineseButton';
|
||||
|
||||
interface ChineseEmptyProps {
|
||||
type?: 'default' | 'search' | 'data' | 'error';
|
||||
title?: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChineseEmpty: React.FC<ChineseEmptyProps> = ({
|
||||
type = 'default',
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className
|
||||
}) => {
|
||||
const iconMap = {
|
||||
default: Inbox,
|
||||
search: Search,
|
||||
data: FileX,
|
||||
error: AlertCircle
|
||||
};
|
||||
|
||||
const defaultTitles = {
|
||||
default: '暂无数据',
|
||||
search: '未找到相关内容',
|
||||
data: '暂无记录',
|
||||
error: '加载失败'
|
||||
};
|
||||
|
||||
const defaultDescriptions = {
|
||||
default: '这里还没有任何内容',
|
||||
search: '请尝试其他关键词或调整筛选条件',
|
||||
data: '您还没有创建任何记录',
|
||||
error: '数据加载出现问题,请稍后重试'
|
||||
};
|
||||
|
||||
const colorMap = {
|
||||
default: 'text-gray-400',
|
||||
search: 'text-blue-400',
|
||||
data: 'text-yellow-400',
|
||||
error: 'text-red-400'
|
||||
};
|
||||
|
||||
const Icon = iconMap[type];
|
||||
const displayTitle = title || defaultTitles[type];
|
||||
const displayDescription = description || defaultDescriptions[type];
|
||||
const iconColor = colorMap[type];
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex flex-col items-center justify-center py-12 px-4 text-center',
|
||||
className
|
||||
)}>
|
||||
{/* 图标 */}
|
||||
<div className="mb-4">
|
||||
<Icon className={cn('h-16 w-16', iconColor)} />
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2 font-chinese">
|
||||
{displayTitle}
|
||||
</h3>
|
||||
|
||||
{/* 描述 */}
|
||||
<p className="text-gray-600 mb-6 max-w-sm font-chinese leading-relaxed">
|
||||
{displayDescription}
|
||||
</p>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{action && (
|
||||
<ChineseButton
|
||||
variant={type === 'error' ? 'primary' : 'secondary'}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.label}
|
||||
</ChineseButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ChineseEmpty };
|
||||
export type { ChineseEmptyProps };
|
||||
115
src/components/ui/ChineseInput.tsx
Normal file
115
src/components/ui/ChineseInput.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface ChineseInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
variant?: 'default' | 'bordered' | 'filled';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const ChineseInput = React.forwardRef<HTMLInputElement, ChineseInputProps>(
|
||||
({ className, label, error, helperText, variant = 'default', size = 'md', ...props }, ref) => {
|
||||
const baseClasses = [
|
||||
'w-full font-chinese transition-all duration-200 ease-in-out',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-1',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'placeholder:text-gray-400',
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
default: [
|
||||
'bg-white border border-gray-300',
|
||||
'hover:border-red-400 focus:border-red-500 focus:ring-red-500/20',
|
||||
error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||
],
|
||||
bordered: [
|
||||
'bg-transparent border-2 border-red-300',
|
||||
'hover:border-red-500 focus:border-red-600 focus:ring-red-500/20',
|
||||
error ? 'border-red-500 focus:border-red-600 focus:ring-red-500/20' : '',
|
||||
],
|
||||
filled: [
|
||||
'bg-red-50 border border-red-200',
|
||||
'hover:bg-red-100 hover:border-red-300',
|
||||
'focus:bg-white focus:border-red-500 focus:ring-red-500/20',
|
||||
error ? 'bg-red-100 border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||
],
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: [
|
||||
'px-3 py-2 text-body-md rounded-md',
|
||||
'min-h-[36px]', // 移动端友好
|
||||
],
|
||||
md: [
|
||||
'px-4 py-2.5 text-body-lg rounded-lg',
|
||||
'min-h-[44px]', // 移动端友好
|
||||
],
|
||||
lg: [
|
||||
'px-5 py-3 text-body-xl rounded-xl',
|
||||
'min-h-[52px]', // 移动端友好
|
||||
],
|
||||
};
|
||||
|
||||
// 移动端响应式调整
|
||||
const responsiveClasses = [
|
||||
'touch-manipulation', // 优化触摸体验
|
||||
'max-md:text-base', // 移动端字体调整
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* 标签 */}
|
||||
{label && (
|
||||
<label className="block text-label-lg font-medium text-gray-700 mb-2 font-chinese">
|
||||
{label}
|
||||
{props.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 输入框 */}
|
||||
<div className="relative">
|
||||
<input
|
||||
className={cn(
|
||||
baseClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
responsiveClasses,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{/* 错误状态图标 */}
|
||||
{error && (
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 错误信息或帮助文本 */}
|
||||
{(error || helperText) && (
|
||||
<div className="mt-1.5">
|
||||
{error ? (
|
||||
<p className="text-body-sm text-red-600 font-chinese">{error}</p>
|
||||
) : (
|
||||
helperText && (
|
||||
<p className="text-body-sm text-gray-500 font-chinese">{helperText}</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseInput.displayName = 'ChineseInput';
|
||||
|
||||
export { ChineseInput };
|
||||
export type { ChineseInputProps };
|
||||
99
src/components/ui/ChineseLoading.tsx
Normal file
99
src/components/ui/ChineseLoading.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Loader2, Sparkles } from 'lucide-react';
|
||||
|
||||
interface ChineseLoadingProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'spinner' | 'dots' | 'chinese';
|
||||
text?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChineseLoading: React.FC<ChineseLoadingProps> = ({
|
||||
size = 'md',
|
||||
variant = 'chinese',
|
||||
text,
|
||||
className
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-8 w-8',
|
||||
lg: 'h-12 w-12'
|
||||
};
|
||||
|
||||
const textSizeClasses = {
|
||||
sm: 'text-sm',
|
||||
md: 'text-base',
|
||||
lg: 'text-lg'
|
||||
};
|
||||
|
||||
const renderSpinner = () => (
|
||||
<Loader2 className={cn(
|
||||
'animate-spin text-red-600',
|
||||
sizeClasses[size]
|
||||
)} />
|
||||
);
|
||||
|
||||
const renderDots = () => (
|
||||
<div className="flex space-x-1">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'bg-red-600 rounded-full animate-pulse',
|
||||
size === 'sm' ? 'h-2 w-2' : size === 'md' ? 'h-3 w-3' : 'h-4 w-4'
|
||||
)}
|
||||
style={{
|
||||
animationDelay: `${i * 0.2}s`,
|
||||
animationDuration: '1s'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderChinese = () => (
|
||||
<div className="relative">
|
||||
<div className={cn(
|
||||
'border-4 border-red-200 border-t-red-600 rounded-full animate-spin',
|
||||
sizeClasses[size]
|
||||
)} />
|
||||
<Sparkles className={cn(
|
||||
'absolute inset-0 m-auto text-yellow-500 animate-pulse',
|
||||
size === 'sm' ? 'h-2 w-2' : size === 'md' ? 'h-4 w-4' : 'h-6 w-6'
|
||||
)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderVariant = () => {
|
||||
switch (variant) {
|
||||
case 'spinner':
|
||||
return renderSpinner();
|
||||
case 'dots':
|
||||
return renderDots();
|
||||
case 'chinese':
|
||||
default:
|
||||
return renderChinese();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex flex-col items-center justify-center space-y-3',
|
||||
className
|
||||
)}>
|
||||
{renderVariant()}
|
||||
{text && (
|
||||
<p className={cn(
|
||||
'text-gray-600 font-chinese',
|
||||
textSizeClasses[size]
|
||||
)}>
|
||||
{text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ChineseLoading };
|
||||
export type { ChineseLoadingProps };
|
||||
160
src/components/ui/ChineseSelect.tsx
Normal file
160
src/components/ui/ChineseSelect.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
interface ChineseSelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ChineseSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
variant?: 'default' | 'bordered' | 'filled';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
options: ChineseSelectOption[];
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const ChineseSelect = React.forwardRef<HTMLSelectElement, ChineseSelectProps>(
|
||||
({ className, label, error, helperText, variant = 'default', size = 'md', options, placeholder, ...props }, ref) => {
|
||||
const baseClasses = [
|
||||
'w-full font-chinese transition-all duration-200 ease-in-out',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-1',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'appearance-none cursor-pointer',
|
||||
'bg-no-repeat bg-right',
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
default: [
|
||||
'bg-white border border-gray-300',
|
||||
'hover:border-red-400 focus:border-red-500 focus:ring-red-500/20',
|
||||
error ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||
],
|
||||
bordered: [
|
||||
'bg-transparent border-2 border-red-300',
|
||||
'hover:border-red-500 focus:border-red-600 focus:ring-red-500/20',
|
||||
error ? 'border-red-500 focus:border-red-600 focus:ring-red-500/20' : '',
|
||||
],
|
||||
filled: [
|
||||
'bg-red-50 border border-red-200',
|
||||
'hover:bg-red-100 hover:border-red-300',
|
||||
'focus:bg-white focus:border-red-500 focus:ring-red-500/20',
|
||||
error ? 'bg-red-100 border-red-500 focus:border-red-500 focus:ring-red-500/20' : '',
|
||||
],
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: [
|
||||
'px-3 py-2 pr-8 text-sm rounded-md',
|
||||
'min-h-[36px]', // 移动端友好
|
||||
],
|
||||
md: [
|
||||
'px-4 py-2.5 pr-10 text-base rounded-lg',
|
||||
'min-h-[44px]', // 移动端友好
|
||||
],
|
||||
lg: [
|
||||
'px-5 py-3 pr-12 text-lg rounded-xl',
|
||||
'min-h-[52px]', // 移动端友好
|
||||
],
|
||||
};
|
||||
|
||||
// 移动端响应式调整
|
||||
const responsiveClasses = [
|
||||
'touch-manipulation', // 优化触摸体验
|
||||
'max-md:text-base', // 移动端字体调整
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* 标签 */}
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2 font-chinese">
|
||||
{label}
|
||||
{props.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 选择器容器 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
className={cn(
|
||||
baseClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
responsiveClasses,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{/* 占位符选项 */}
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
|
||||
{/* 选项列表 */}
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
className="font-chinese"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* 下拉箭头 */}
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pointer-events-none">
|
||||
<div className={cn(
|
||||
'pr-2',
|
||||
size === 'sm' ? 'pr-2' : size === 'md' ? 'pr-3' : 'pr-4'
|
||||
)}>
|
||||
<ChevronDown className={cn(
|
||||
'text-gray-400',
|
||||
size === 'sm' ? 'h-4 w-4' : size === 'md' ? 'h-5 w-5' : 'h-6 w-6'
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误状态图标 */}
|
||||
{error && (
|
||||
<div className={cn(
|
||||
'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||
size === 'sm' ? 'pr-7' : size === 'md' ? 'pr-9' : 'pr-11'
|
||||
)}>
|
||||
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 错误信息或帮助文本 */}
|
||||
{(error || helperText) && (
|
||||
<div className="mt-1.5">
|
||||
{error ? (
|
||||
<p className="text-sm text-red-600 font-chinese">{error}</p>
|
||||
) : (
|
||||
helperText && (
|
||||
<p className="text-sm text-gray-500 font-chinese">{helperText}</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChineseSelect.displayName = 'ChineseSelect';
|
||||
|
||||
export { ChineseSelect };
|
||||
export type { ChineseSelectProps, ChineseSelectOption };
|
||||
109
src/components/ui/ChineseToast.tsx
Normal file
109
src/components/ui/ChineseToast.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react';
|
||||
|
||||
interface ChineseToastProps {
|
||||
type?: 'success' | 'error' | 'warning' | 'info';
|
||||
title?: string;
|
||||
message: string;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChineseToast: React.FC<ChineseToastProps> = ({
|
||||
type = 'info',
|
||||
title,
|
||||
message,
|
||||
onClose,
|
||||
className
|
||||
}) => {
|
||||
const iconMap = {
|
||||
success: CheckCircle,
|
||||
error: XCircle,
|
||||
warning: AlertCircle,
|
||||
info: Info
|
||||
};
|
||||
|
||||
const colorMap = {
|
||||
success: {
|
||||
bg: 'bg-green-50',
|
||||
border: 'border-green-200',
|
||||
icon: 'text-green-600',
|
||||
title: 'text-green-800',
|
||||
message: 'text-green-700'
|
||||
},
|
||||
error: {
|
||||
bg: 'bg-red-50',
|
||||
border: 'border-red-200',
|
||||
icon: 'text-red-600',
|
||||
title: 'text-red-800',
|
||||
message: 'text-red-700'
|
||||
},
|
||||
warning: {
|
||||
bg: 'bg-yellow-50',
|
||||
border: 'border-yellow-200',
|
||||
icon: 'text-yellow-600',
|
||||
title: 'text-yellow-800',
|
||||
message: 'text-yellow-700'
|
||||
},
|
||||
info: {
|
||||
bg: 'bg-blue-50',
|
||||
border: 'border-blue-200',
|
||||
icon: 'text-blue-600',
|
||||
title: 'text-blue-800',
|
||||
message: 'text-blue-700'
|
||||
}
|
||||
};
|
||||
|
||||
const Icon = iconMap[type];
|
||||
const colors = colorMap[type];
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-start p-4 rounded-lg border shadow-lg',
|
||||
'animate-in slide-in-from-top-2 duration-300',
|
||||
colors.bg,
|
||||
colors.border,
|
||||
className
|
||||
)}>
|
||||
{/* 图标 */}
|
||||
<div className="flex-shrink-0">
|
||||
<Icon className={cn('h-5 w-5', colors.icon)} />
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="ml-3 flex-1">
|
||||
{title && (
|
||||
<h4 className={cn(
|
||||
'text-sm font-semibold font-chinese mb-1',
|
||||
colors.title
|
||||
)}>
|
||||
{title}
|
||||
</h4>
|
||||
)}
|
||||
<p className={cn(
|
||||
'text-sm font-chinese leading-relaxed',
|
||||
colors.message
|
||||
)}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'flex-shrink-0 ml-4 p-1 rounded-md hover:bg-white/50 transition-colors',
|
||||
colors.icon
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ChineseToast };
|
||||
export type { ChineseToastProps };
|
||||
Reference in New Issue
Block a user