Design System 重构继续完成

This commit is contained in:
2026-02-10 04:58:50 +08:00
parent 73d0b0d5fe
commit b8cb884e9e
56 changed files with 403 additions and 1033 deletions

View File

@@ -41,7 +41,7 @@ import { cn } from "@/design-system/lib/utils";
*/
const buttonVariants = cva(
// 基础样式
"inline-flex items-center justify-center gap-2 rounded-xl font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 rounded-md font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
@@ -51,6 +51,7 @@ const buttonVariants = cva(
warning: "bg-warning-500 text-white hover:bg-warning-600 shadow-md",
error: "bg-error-500 text-white hover:bg-error-600 shadow-md",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 shadow-none",
"ghost-light": "bg-transparent text-white hover:bg-white/10 shadow-none",
outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50 shadow-none",
link: "text-primary-500 hover:text-primary-600 hover:underline shadow-none px-0",
},
@@ -138,15 +139,18 @@ export function Button({
type = "button",
...props
}: ButtonProps) {
// 确保 size 有默认值
const actualSize = size ?? "md";
// 计算样式
const computedClass = cn(
buttonVariants({ variant, size, fullWidth }),
buttonVariants({ variant, size: actualSize, fullWidth }),
selected && variant === "secondary" && "bg-gray-200",
className
);
// 图标尺寸映射
const iconSize = { sm: 14, md: 16, lg: 20 }[size];
const iconSize = { sm: 14, md: 16, lg: 20 }[actualSize];
// 渲染 SVG 图标
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
@@ -248,6 +252,9 @@ export const SecondaryButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="secondary" {...props} />
);
// LightButton: 次要按钮的别名(向后兼容)
export const LightButton = SecondaryButton;
export const SuccessButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="success" {...props} />
);
@@ -264,6 +271,11 @@ export const GhostButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="ghost" {...props} />
);
// GhostLightButton: 透明按钮(白色文字,用于深色背景)
export const GhostLightButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="ghost-light" {...props} />
);
export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="outline" {...props} />
);
@@ -271,3 +283,69 @@ export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
export const LinkButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="link" {...props} />
);
// ========== 其他便捷组件 ==========
// IconButton: SVG 图标按钮(使用 ghost 变体)
export const IconButton = (props: Omit<ButtonProps, "variant"> & { icon?: React.ReactNode }) => {
const { icon, ...rest } = props;
return <Button variant="ghost" leftIcon={icon} {...rest} />;
};
// IconClick: 图片图标按钮(支持 Next.js Image
export const IconClick = (props: Omit<ButtonProps, "variant"> & {
src?: string;
alt?: string;
size?: number | "sm" | "md" | "lg";
disableOnHoverBgChange?: boolean;
}) => {
const { src, alt, size, disableOnHoverBgChange, className, ...rest } = props;
let buttonSize: "sm" | "md" | "lg" = "md";
if (typeof size === "number") {
if (size <= 20) buttonSize = "sm";
else if (size >= 32) buttonSize = "lg";
} else if (typeof size === "string") {
buttonSize = (size === "sm" || size === "md" || size === "lg") ? size : "md";
}
const hoverClass = disableOnHoverBgChange ? "hover:bg-black/30" : "";
return (
<Button
variant="ghost"
iconSrc={src}
iconAlt={alt}
size={buttonSize}
className={`${hoverClass} ${className || ""}`}
{...rest}
/>
);
};
// CircleButton: 圆形图标按钮
export const CircleButton = (props: Omit<ButtonProps, "variant"> & { icon?: React.ReactNode }) => {
const { icon, className, ...rest } = props;
return <Button variant="ghost" leftIcon={icon} className={`rounded-full ${className || ""}`} {...rest} />;
};
// CircleToggleButton: 带选中状态的圆形切换按钮
export const CircleToggleButton = (props: Omit<ButtonProps, "variant"> & { selected?: boolean }) => {
const { selected, className, children, ...rest } = props;
const selectedClass = selected
? "bg-primary-500 text-white"
: "bg-gray-200 text-gray-600 hover:bg-gray-300";
return (
<Button
variant="secondary"
className={`rounded-full px-3 py-1 text-sm transition-colors ${selectedClass} ${className || ""}`}
{...rest}
>
{children}
</Button>
);
};
// DashedButton: 虚线边框按钮(使用 outline 变体近似)
export const DashedButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="outline" className="border-dashed" {...props} />
);

View File

@@ -36,7 +36,7 @@ import { cn } from "@/design-system/lib/utils";
*/
const cardVariants = cva(
// 基础样式
"rounded-2xl bg-white transition-all duration-250",
"rounded-lg bg-white transition-all duration-250",
{
variants: {
variant: {

View File

@@ -34,11 +34,11 @@ import { cn } from "@/design-system/lib/utils";
*/
const inputVariants = cva(
// 基础样式
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex w-full rounded-md border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
default: "border-b-2 border-gray-300 bg-transparent rounded-t-md",
bordered: "border-gray-300 bg-white",
filled: "border-transparent bg-gray-100",
search: "border-gray-200 bg-white pl-10 rounded-full",

View File

@@ -186,12 +186,13 @@ export function RadioGroup({
// 为每个 Radio 注入 name 和 onChange
const enhancedChildren = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
const childProps = child.props as { value?: string; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void };
return React.cloneElement(child as React.ReactElement<any>, {
name,
checked: value !== undefined ? child.props.value === value : undefined,
checked: value !== undefined ? childProps.value === value : undefined,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e.target.value);
child.props.onChange?.(e);
childProps.onChange?.(e);
},
});
}

View File

@@ -24,11 +24,11 @@ import { cn } from "@/design-system/lib/utils";
*/
const selectVariants = cva(
// 基础样式
"flex w-full appearance-none items-center justify-between rounded-xl border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex w-full appearance-none items-center justify-between rounded-md border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
default: "border-b-2 border-gray-300 bg-transparent rounded-t-md",
bordered: "border-gray-300 bg-white",
filled: "border-transparent bg-gray-100",
},

View File

@@ -104,19 +104,22 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
onChange?.(e);
};
// 确保 size 有默认值
const actualSize = size ?? "md";
// 滑块大小
const thumbSize = {
sm: "h-3.5 w-3.5",
md: "h-4 w-4",
lg: "h-5 w-5",
}[size];
}[actualSize];
// 滑块位移
const thumbTranslate = {
sm: isChecked ? "translate-x-4" : "translate-x-0.5",
md: isChecked ? "translate-x-5" : "translate-x-0.5",
lg: isChecked ? "translate-x-6" : "translate-x-0.5",
}[size];
}[actualSize];
const renderSwitch = () => (
<div className="relative inline-block">

View File

@@ -27,7 +27,7 @@ import { cn } from "@/design-system/lib/utils";
*/
const textareaVariants = cva(
// 基础样式
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
"flex w-full rounded-md border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
{
variants: {
variant: {

View File

@@ -98,13 +98,16 @@ export function Badge({
info: "bg-info-500",
};
// 确保 variant 有默认值
const actualVariant = variant ?? "default";
return (
<div className={cn(badgeVariants({ variant, size, dot }), className)} {...props}>
<div className={cn(badgeVariants({ variant: actualVariant, size, dot }), className)} {...props}>
{dot && (
<span
className={cn(
"h-2 w-2 rounded-full",
dotColor || dotColors[variant]
dotColor || dotColors[actualVariant]
)}
/>
)}

View File

@@ -95,7 +95,7 @@ export function Divider({
<div
className={cn(dividerVariants({ variant, orientation }), className)}
role="separator"
aria-orientation={orientation}
aria-orientation={orientation as "horizontal" | "vertical"}
{...props}
/>
);

View File

@@ -34,7 +34,7 @@ import { cn } from "@/design-system/lib/utils";
*/
const alertVariants = cva(
// 基础样式
"rounded-xl border-2 px-4 py-3 shadow-sm transition-all duration-250",
"rounded-lg border-2 px-4 py-3 shadow-sm transition-all duration-250",
{
variants: {
variant: {
@@ -127,6 +127,9 @@ export function Alert({
if (!visible) return null;
// 确保 variant 有默认值
const actualVariant = variant ?? "info";
// 图标颜色
const iconColors = {
info: "text-info-500",
@@ -137,14 +140,14 @@ export function Alert({
return (
<div
className={cn(alertVariants({ variant }), className)}
className={cn(alertVariants({ variant: actualVariant }), className)}
role="alert"
{...props}
>
<div className="flex items-start gap-3">
{/* 图标 */}
<div className={cn("shrink-0", iconColors[variant])}>
{icon || defaultIcons[variant]}
<div className={cn("shrink-0", iconColors[actualVariant])}>
{icon || defaultIcons[actualVariant]}
</div>
{/* 内容 */}

View File

@@ -102,7 +102,8 @@ export function Progress({
warning: "bg-warning-500",
error: "bg-error-500",
};
return colors[variant];
const actualVariant = variant ?? "default";
return colors[actualVariant];
};
// 格式化标签
@@ -175,7 +176,7 @@ export function CircularProgress({
warning: "#f59e0b",
error: "#ef4444",
};
const strokeColor = colors[variant];
const strokeColor = colors[variant ?? "default"];
return (
<div className={cn("inline-flex items-center justify-center", className)}>

View File

@@ -125,6 +125,7 @@ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonE
value: string;
children: React.ReactNode;
variant?: "line" | "enclosed" | "soft";
"data-state"?: string;
}
const triggerVariants = cva(
@@ -148,14 +149,15 @@ export function TabsTrigger({
children,
variant = "line",
className,
"data-state": dataState,
...props
}: TabsTriggerProps) {
return (
<button
type="button"
role="tab"
aria-selected={props["data-state"] === "active"}
data-state={props["data-state"]}
aria-selected={dataState === "active"}
data-state={dataState}
className={cn(triggerVariants({ variant }), className)}
{...props}
>
@@ -170,21 +172,24 @@ export function TabsTrigger({
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
children: React.ReactNode;
"data-state"?: string;
}
export function TabsContent({
value,
children,
className,
"data-state": dataState,
...props
}: TabsContentProps) {
if (value !== props["data-state"]) return null;
if (value !== dataState) return null;
return (
<div
role="tabpanel"
className={cn("mt-4 focus:outline-none", className)}
tabIndex={0}
data-state={dataState}
{...props}
>
{children}

View File

@@ -105,7 +105,7 @@ export function Modal({
{/* 模态框内容 */}
<div
className={cn(
"relative z-10 w-full bg-white rounded-2xl shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
"relative z-10 w-full bg-white rounded-lg shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
sizeClasses[size],
className
)}