样式规范
核心原则
必须使用 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>响应式设计
断点
| 断点 | 最小宽度 | 说明 |
|---|---|---|
sm | 640px | 小屏幕 |
md | 768px | 中等屏幕 |
lg | 1024px | 大屏幕 |
xl | 1280px | 超大屏幕 |
2xl | 1536px | 超超大屏幕 |
移动优先原则
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) - ❌ 禁止直接使用
clsx或classnames,必须使用cn() - ❌ 禁止在移动端优先的响应式设计中使用
max-*断点