重新构建路由和配置样式文件
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Link } from '@remix-run/react';
|
||||
|
||||
type ButtonType = 'primary' | 'default' | 'danger';
|
||||
type ButtonSize = 'small' | 'medium' | 'large';
|
||||
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
type?: ButtonType;
|
||||
size?: ButtonSize;
|
||||
to?: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
type = 'default',
|
||||
size = 'medium',
|
||||
to,
|
||||
icon,
|
||||
disabled = false,
|
||||
className = '',
|
||||
onClick,
|
||||
...rest
|
||||
}: ButtonProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type'>) {
|
||||
const baseClasses = 'ant-btn';
|
||||
|
||||
const typeClasses = {
|
||||
primary: 'ant-btn-primary',
|
||||
default: 'ant-btn-default',
|
||||
danger: 'ant-btn-danger'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'ant-btn-sm',
|
||||
medium: '',
|
||||
large: 'text-base px-5 py-2.5'
|
||||
};
|
||||
|
||||
const classes = [
|
||||
baseClasses,
|
||||
typeClasses[type],
|
||||
sizeClasses[size],
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : '',
|
||||
className
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={classes}
|
||||
{...(rest as any)}
|
||||
>
|
||||
{icon && <i className={`${icon} mr-1.5`}></i>}
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <i className={`${icon} mr-1.5`}></i>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CardProps {
|
||||
children: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
icon?: string;
|
||||
extra?: React.ReactNode;
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
noDivider?: boolean;
|
||||
}
|
||||
|
||||
export function Card({
|
||||
children,
|
||||
title,
|
||||
icon,
|
||||
extra,
|
||||
className = '',
|
||||
bodyClassName = '',
|
||||
noDivider = true,
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div className={`ant-card ${className} bg-white shadow`}>
|
||||
{(title || extra) && (
|
||||
<div className={`flex justify-between items-center px-5 py-3 ${noDivider ? '' : 'border-b border-gray-100'}`}>
|
||||
{title && (
|
||||
<div className="card-title !mb-0">
|
||||
{icon && <i className={`${icon} mr-2`}></i>}
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
)}
|
||||
{extra && (
|
||||
<div className="card-extra">
|
||||
{extra}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={`ant-card-body ${bodyClassName}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TableColumn<T> {
|
||||
title: React.ReactNode;
|
||||
dataIndex?: keyof T;
|
||||
key?: string;
|
||||
width?: number | string;
|
||||
render?: (value: any, record: T, index: number) => React.ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TableProps<T> {
|
||||
columns: TableColumn<T>[];
|
||||
dataSource: T[];
|
||||
rowKey: keyof T | ((record: T) => string);
|
||||
loading?: boolean;
|
||||
bordered?: boolean;
|
||||
emptyText?: React.ReactNode;
|
||||
className?: string;
|
||||
onRow?: (record: T, index: number) => React.HTMLAttributes<HTMLTableRowElement>;
|
||||
}
|
||||
|
||||
export function Table<T extends Record<string, any>>({
|
||||
columns,
|
||||
dataSource,
|
||||
rowKey,
|
||||
loading = false,
|
||||
bordered = false,
|
||||
emptyText = '暂无数据',
|
||||
className = '',
|
||||
onRow,
|
||||
}: TableProps<T>) {
|
||||
const getRowKey = (record: T, index: number): string => {
|
||||
if (typeof rowKey === 'function') {
|
||||
return rowKey(record);
|
||||
}
|
||||
return String(record[rowKey]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`ant-table-wrapper ${className} ${loading ? 'opacity-70' : ''}`}>
|
||||
<table className="ant-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={column.key || column.dataIndex?.toString() || index}
|
||||
className={column.className}
|
||||
style={{
|
||||
width: column.width,
|
||||
textAlign: column.align || 'left',
|
||||
}}
|
||||
>
|
||||
{column.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataSource.length > 0 ? (
|
||||
dataSource.map((record, index) => (
|
||||
<tr
|
||||
key={getRowKey(record, index)}
|
||||
{...(onRow ? onRow(record, index) : {})}
|
||||
>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td
|
||||
key={column.key || column.dataIndex?.toString() || colIndex}
|
||||
style={{ textAlign: column.align || 'left' }}
|
||||
>
|
||||
{column.render
|
||||
? column.render(
|
||||
column.dataIndex ? record[column.dataIndex] : undefined,
|
||||
record,
|
||||
index
|
||||
)
|
||||
: column.dataIndex
|
||||
? record[column.dataIndex]
|
||||
: undefined}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="py-6 text-center text-gray-500"
|
||||
>
|
||||
{emptyText}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-60 z-10">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-gray-600">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
|
||||
export type TagColor = 'blue' | 'green' | 'cyan' | 'purple' | 'orange' | 'red' | 'default';
|
||||
|
||||
interface TagProps {
|
||||
children: React.ReactNode;
|
||||
color?: TagColor;
|
||||
closable?: boolean;
|
||||
onClose?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Tag({
|
||||
children,
|
||||
color = 'default',
|
||||
closable = false,
|
||||
onClose,
|
||||
className = '',
|
||||
}: TagProps) {
|
||||
const baseClasses = 'ant-tag';
|
||||
|
||||
const colorClasses = {
|
||||
default: '',
|
||||
blue: 'ant-tag-blue',
|
||||
green: 'ant-tag-green',
|
||||
cyan: 'ant-tag-cyan',
|
||||
purple: 'ant-tag-purple',
|
||||
orange: 'ant-tag-orange',
|
||||
red: 'ant-tag-red'
|
||||
};
|
||||
|
||||
const classes = [
|
||||
baseClasses,
|
||||
colorClasses[color],
|
||||
className
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClose?.(e as unknown as React.MouseEvent<HTMLElement>);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={classes}>
|
||||
{children}
|
||||
{closable && (
|
||||
<i
|
||||
className="ri-close-line ml-1 cursor-pointer text-opacity-60 hover:text-opacity-100"
|
||||
onClick={onClose}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="关闭标签"
|
||||
></i>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user