Skip to content

样式规范

核心原则

必须使用 Tailwind CSS 作为样式方案,遵循原子化 CSS 理念,禁止使用内联样式或传统 CSS 文件。

Tailwind CSS 配置

入口样式文件

css
/* src/index.css */
@import "tailwindcss";
@import "tw-animate-css";

@theme inline {
  /* 基础颜色 */
  --color-background: oklch(100% 0 0);
  --color-foreground: oklch(14.5% 0 0);

  /* 主色调 */
  --color-primary: oklch(14.5% 0 0);
  --color-primary-foreground: oklch(98.5% 0 0);

  /* 次要色 */
  --color-secondary: oklch(96.5% 0 0);
  --color-secondary-foreground: oklch(14.5% 0 0);

  /* 静音色 */
  --color-muted: oklch(96.5% 0 0);
  --color-muted-foreground: oklch(55.6% 0 0);

  /* 强调色 */
  --color-accent: oklch(96.5% 0 0);
  --color-accent-foreground: oklch(14.5% 0 0);

  /* 危险色 */
  --color-destructive: oklch(57.7% 0.245 27.325);

  /* 边框和输入框 */
  --color-border: oklch(91.4% 0 0);
  --color-input: oklch(91.4% 0 0);
  --color-ring: oklch(70.8% 0 0);

  /* 卡片 */
  --color-card: oklch(100% 0 0);
  --color-card-foreground: oklch(14.5% 0 0);

  /* 弹出层 */
  --color-popover: oklch(100% 0 0);
  --color-popover-foreground: oklch(14.5% 0 0);

  /* 圆角 */
  --radius: 0.625rem;
}

/* 暗色主题 */
.dark {
  --color-background: oklch(14.5% 0 0);
  --color-foreground: oklch(98.5% 0 0);
  --color-primary: oklch(98.5% 0 0);
  --color-primary-foreground: oklch(14.5% 0 0);
  --color-secondary: oklch(26.9% 0 0);
  --color-secondary-foreground: oklch(98.5% 0 0);
  --color-muted: oklch(26.9% 0 0);
  --color-muted-foreground: oklch(70.8% 0 0);
  --color-accent: oklch(26.9% 0 0);
  --color-accent-foreground: oklch(98.5% 0 0);
  --color-destructive: oklch(57.7% 0.245 27.325);
  --color-border: oklch(26.9% 0 0);
  --color-input: oklch(26.9% 0 0);
  --color-ring: oklch(83.9% 0 0);
  --color-card: oklch(14.5% 0 0);
  --color-card-foreground: oklch(98.5% 0 0);
  --color-popover: oklch(14.5% 0 0);
  --color-popover-foreground: oklch(98.5% 0 0);
}

cn() 工具函数

定义

typescript
// src/lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

使用场景

typescript
import { cn } from '@/lib/utils';

// 合并基础类和条件类
function Button({ className, variant, ...props }) {
  return (
    <button
      className={cn(
        'px-4 py-2 rounded-md font-medium',
        variant === 'primary' && 'bg-primary text-primary-foreground',
        variant === 'secondary' && 'bg-secondary text-secondary-foreground',
        className
      )}
      {...props}
    />
  );
}

// 合并外部传入的类名
function Card({ className, children }) {
  return (
    <div className={cn('rounded-lg border bg-card p-6', className)}>
      {children}
    </div>
  );
}

原子类使用

布局

typescript
// Flexbox 布局
<div className="flex items-center justify-between gap-4">
  <div className="flex-1">内容</div>
  <div className="flex-shrink-0">固定宽度</div>
</div>

// Grid 布局
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <div>卡片 1</div>
  <div>卡片 2</div>
  <div>卡片 3</div>
</div>

// 容器和间距
<div className="container mx-auto px-4 py-8">
  <div className="space-y-6">
    <section>部分 1</section>
    <section>部分 2</section>
  </div>
</div>

排版

typescript
// 标题
<h1 className="text-4xl font-bold tracking-tight">主标题</h1>
<h2 className="text-2xl font-semibold">副标题</h2>

// 正文
<p className="text-base text-muted-foreground leading-relaxed">
  段落内容
</p>

// 文本截断
<p className="truncate">很长的文本会被截断...</p>
<p className="line-clamp-2">最多显示两行的文本...</p>

颜色

typescript
// ✅ 正确 - 使用语义化颜色
<div className="bg-background text-foreground">
  <p className="text-primary">主色文本</p>
  <p className="text-muted-foreground">静音色文本</p>
  <p className="text-destructive">危险色文本</p>
</div>

// ❌ 错误 - 硬编码颜色
<div className="bg-white text-black">
  <p className="text-blue-500">蓝色文本</p>
</div>

边框和圆角

typescript
// 边框
<div className="border border-border rounded-lg">
  带边框的容器
</div>

// 分割线
<div className="divide-y divide-border">
  <div className="py-4">项目 1</div>
  <div className="py-4">项目 2</div>
</div>

// 圆角(使用 CSS 变量)
<div className="rounded-[--radius]">使用主题圆角</div>

响应式设计

断点

断点最小宽度说明
sm640px小屏幕
md768px中等屏幕
lg1024px大屏幕
xl1280px超大屏幕
2xl1536px超超大屏幕

移动优先原则

typescript
// ✅ 正确 - 移动优先,逐步增强
<div className="flex flex-col md:flex-row">
  <aside className="w-full md:w-64">侧边栏</aside>
  <main className="flex-1">主内容</main>
</div>

// ✅ 正确 - 响应式网格
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
  {items.map((item) => (
    <Card key={item.id}>{item.title}</Card>
  ))}
</div>

// ✅ 正确 - 响应式隐藏/显示
<nav className="hidden md:flex">桌面导航</nav>
<button className="md:hidden">移动菜单按钮</button>

响应式间距

typescript
// 响应式内边距
<div className="p-4 md:p-6 lg:p-8">
  内容
</div>

// 响应式外边距
<section className="my-8 md:my-12 lg:my-16">
  内容
</section>

暗色模式

配置暗色模式

typescript
// 在 HTML 根元素添加 dark 类
<html className="dark">
  ...
</html>

主题切换组件

typescript
// src/components/ThemeToggle.tsx
import { useUIStore } from '@/stores/ui';
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';
import { useEffect } from 'react';

export function ThemeToggle() {
  const { theme, setTheme } = useUIStore();

  useEffect(() => {
    const root = document.documentElement;
    if (theme === 'dark') {
      root.classList.add('dark');
    } else if (theme === 'light') {
      root.classList.remove('dark');
    } else {
      // system
      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      root.classList.toggle('dark', isDark);
    }
  }, [theme]);

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">切换主题</span>
    </Button>
  );
}

暗色模式样式

typescript
// 自动适配暗色模式(使用语义化颜色)
<div className="bg-background text-foreground">
  自动适配暗色模式
</div>

// 手动指定暗色模式样式
<div className="bg-white dark:bg-gray-900">
  手动指定暗色样式
</div>

主题定制

品牌色定制

css
/* src/index.css */
@theme inline {
  /* 自定义品牌主色(蓝色系) */
  --color-primary: oklch(55% 0.2 250);
  --color-primary-foreground: oklch(98% 0 0);
}

扩展颜色

css
/* src/index.css */
@theme inline {
  /* 成功色 */
  --color-success: oklch(65% 0.2 145);
  --color-success-foreground: oklch(98% 0 0);

  /* 警告色 */
  --color-warning: oklch(75% 0.15 85);
  --color-warning-foreground: oklch(20% 0 0);

  /* 信息色 */
  --color-info: oklch(60% 0.15 250);
  --color-info-foreground: oklch(98% 0 0);
}

动画

过渡效果

typescript
// 基础过渡
<button className="transition-colors hover:bg-primary/90">
  悬停变色
</button>

// 多属性过渡
<div className="transition-all duration-300 ease-in-out hover:scale-105 hover:shadow-lg">
  悬停放大
</div>

常用动画

typescript
// 旋转动画
<Loader className="animate-spin h-5 w-5" />

// 脉冲动画
<div className="animate-pulse bg-muted h-4 w-32 rounded" />

// 弹跳动画
<div className="animate-bounce"></div>

组件样式模式

变体模式

typescript
// src/components/ui/badge.tsx
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';

const badgeVariants = cva(
  'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground',
        secondary: 'bg-secondary text-secondary-foreground',
        destructive: 'bg-destructive text-destructive-foreground',
        outline: 'border border-input bg-background',
      },
    },
    defaultVariants: {
      variant: 'default',
    },
  }
);

interface BadgeProps extends VariantProps<typeof badgeVariants> {
  className?: string;
  children: React.ReactNode;
}

export function Badge({ className, variant, children }: BadgeProps) {
  return (
    <span className={cn(badgeVariants({ variant }), className)}>
      {children}
    </span>
  );
}

禁止的做法

  • 禁止使用内联样式(style={object}
  • 禁止使用传统 CSS 文件或 CSS Modules
  • 禁止使用 CSS-in-JS 库(styled-components、emotion 等)
  • 禁止在组件中硬编码颜色值(如 text-blue-500
  • 禁止直接使用 clsxclassnames必须使用 cn()
  • 禁止在移动端优先的响应式设计中使用 max-* 断点