第 7 章

界面交互

掌握 UI 组件设计、交互模式、响应式设计和用户体验原则,打造优秀的用户界面。

使用 Sequential Thinking 学习 AI 驱动的界面交互

AI 在界面交互中的应用涉及多个层面,使用结构化思考方法可以帮助你系统掌握:

1
界面设计基础概览
Design Tokens、主题系统、组件架构快速了解
2
AI 辅助组件开发
组件代码生成、样式优化、组件库选型
3
AI 辅助交互实现
状态管理代码生成、表单验证、动画效果
4
AI 辅助响应式与可访问性
响应式布局生成、无障碍检查、WCAG 合规性验证
5
AI 驱动的性能优化
性能瓶颈分析、优化方案生成、性能监控

设计系统(Design System)

设计系统是一套可复用的设计标准和组件库,确保产品的一致性和可维护性。

Design Tokens 设计令牌

Design Tokens 是设计决策的最小单位,通过 CSS 变量统一管理颜色、间距、字体等设计元素。

颜色系统(OKLCH 颜色空间)

使用 OKLCH 颜色空间可以更好地处理颜色对比度和主题切换。项目中的颜色定义:

:root {
  --primary: oklch(0.7 0.15 200);
  --background: oklch(0.09 0 0);
  --foreground: oklch(0.98 0 0);
  --border: oklch(0.25 0 0);
  /* ... 更多颜色变量 */
}

间距系统

Tailwind 默认间距比例(4px 基准):0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64

0
0px
1
4px
2
8px
4
16px
8
32px
12
48px
16
64px
24
96px

字体系统

字体族
--font-sans: 'Geist'
--font-mono: 'Geist Mono'
字号比例
xs: 0.75rem, sm: 0.875rem, base: 1rem, lg: 1.125rem, xl: 1.25rem, 2xl: 1.5rem

圆角系统

--radius-sm: calc(var(--radius) - 4px)
--radius-md: calc(var(--radius) - 2px)
--radius-lg: var(--radius)
--radius-xl: calc(var(--radius) + 4px)

主题系统

暗色模式实现

使用 next-themes 实现主题切换:

// app/layout.tsx
import { ThemeProvider } from '@/components/theme-provider'

export default function RootLayout({ children }) {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  )
}

CSS 变量主题切换

通过 .dark 类切换主题变量:

.dark {
  --background: oklch(0.09 0 0);
  --foreground: oklch(0.98 0 0);
  /* 暗色模式下的所有颜色变量 */
}

组件架构

原子设计方法论

原子
按钮、输入框、标签
Button, Input
分子
搜索框、表单字段
SearchBox, FormField
组织
导航栏、表单、卡片列表
Header, Form, CardList
模板
页面布局结构
DashboardLayout
页面
最终的用户界面
DashboardPage

组件变体系统(CVA)

使用 class-variance-authority 管理组件变体:

import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground',
        destructive: 'bg-destructive text-destructive-foreground',
        outline: 'border border-input',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
)

AI 在界面交互中的最佳实践

利用 AI 提升前端 UI 开发效率,从组件设计到性能优化的全流程 AI 辅助实践。

AI 辅助组件开发

使用 AI 生成组件代码

向 AI 描述组件需求,让它生成完整的 React/Next.js 组件:

Prompt 模板:

我需要创建一个用户卡片组件,包含以下功能:
1. 显示用户头像、姓名、邮箱
2. 支持点击查看详情
3. 使用 Tailwind CSS 样式
4. 支持暗色模式
5. 响应式设计(移动端和桌面端)
6. 使用 TypeScript
7. 遵循 shadcn/ui 设计规范

请生成完整的组件代码,包含:
- 类型定义
- Props 接口
- 样式类名
- 响应式断点
- 可访问性属性

AI 生成的组件示例

// components/user-card.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Card, CardContent } from '@/components/ui/card'

interface UserCardProps {
  name: string
  email: string
  avatar?: string
  onClick?: () => void
}

export function UserCard({ name, email, avatar, onClick }: UserCardProps) {
  const initials = name
    .split(' ')
    .map(n => n[0])
    .join('')
    .toUpperCase()
    .slice(0, 2)

  return (
    <Card 
      className="cursor-pointer transition-shadow hover:shadow-md"
      onClick={onClick}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          onClick?.()
        }
      }}
    >
      <CardContent className="p-4 flex items-center gap-4">
        <Avatar className="h-12 w-12">
          <AvatarImage src={avatar} alt={name} />
          <AvatarFallback>{initials}</AvatarFallback>
        </Avatar>
        <div className="flex-1 min-w-0">
          <h3 className="font-semibold text-foreground truncate">{name}</h3>
          <p className="text-sm text-muted-foreground truncate">{email}</p>
        </div>
      </CardContent>
    </Card>
  )
}

AI 辅助样式优化

使用 AI 优化 Tailwind CSS 类名,生成更简洁高效的样式:

Prompt:

优化以下 Tailwind CSS 类名,使其更简洁高效:

当前代码:
<div className="flex flex-row items-center justify-between p-4 m-2 bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200">

请:
1. 合并重复的类名
2. 使用更简洁的写法
3. 确保响应式和暗色模式支持
4. 保持相同的视觉效果

AI 辅助组件库选型

Prompt:

我需要为 Next.js 16 + TypeScript 项目选择组件库,需求如下:
1. 支持暗色模式
2. 可定制性强
3. 类型安全
4. 性能优秀
5. 社区活跃

请对比以下组件库:
- shadcn/ui
- Material-UI (MUI)
- Ant Design
- Chakra UI

给出推荐和理由,并说明如何集成到 Next.js 项目中。

UI 组件设计

组件化是现代前端开发的核心思想,通过组合可复用的组件构建复杂的用户界面。

Tailwind CSS 深度配置

配置文件结构

// tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        background: 'var(--background)',
        foreground: 'var(--foreground)',
        // 使用 CSS 变量
      },
      borderRadius: {
        lg: 'var(--radius)',
        md: 'calc(var(--radius) - 2px)',
        sm: 'calc(var(--radius) - 4px)',
      },
    },
  },
  plugins: [],
}

export default config

自定义工具类

// 在 globals.css 中添加自定义工具类
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
  
  .scrollbar-hide {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
  
  .scrollbar-hide::-webkit-scrollbar {
    display: none;
  }
}

响应式断点自定义

// tailwind.config.ts
theme: {
  screens: {
    'xs': '475px',
    'sm': '640px',
    'md': '768px',
    'lg': '1024px',
    'xl': '1280px',
    '2xl': '1536px',
  },
}

暗色模式配置

使用 dark: 前缀:

<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
  {/* 内容 */}
</div>

组件库对比与选型

组件库特点适用场景
shadcn/ui可复制粘贴、完全可控、基于 Radix UI需要高度定制的项目、Next.js 项目
Material-UI组件丰富、文档完善、Material Design企业级应用、快速原型开发
Ant Design企业级组件、中后台应用、中文文档中后台系统、管理平台
Chakra UI简洁、模块化、TypeScript 支持现代 Web 应用、需要灵活性的项目

交互模式

良好的交互设计让用户操作更流畅、更直观。掌握状态管理、表单处理和动画技巧。

状态管理详细指南

useState 最佳实践

// 使用函数式更新避免闭包问题
const [count, setCount] = useState(0)

// 正确:使用函数式更新
setCount(prev => prev + 1)

// 避免:直接依赖 state
setCount(count + 1) // 可能不准确

// 复杂状态使用 useReducer
const [state, dispatch] = useReducer(reducer, initialState)

表单处理最佳实践

表单验证示例

// 完整的表单组件示例
'use client'

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'

const formSchema = z.object({
  username: z.string().min(2, '用户名至少 2 个字符'),
  email: z.string().email('无效的邮箱地址'),
})

export function UserForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: '',
      email: '',
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>用户名</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">提交</Button>
      </form>
    </Form>
  )
}

表单性能优化

  • 使用 mode: 'onBlur' 减少验证次数
  • 复杂表单使用 useFieldArray 管理动态字段
  • 大表单使用 shouldUnregister 优化内存

用户反馈系统

Toast 通知

import { toast } from 'sonner'

toast.success('操作成功')
toast.error('操作失败')
toast.info('提示信息')

Loading 状态

// Skeleton 加载
<Skeleton className="h-4 w-[250px]" />

// Spinner
<Loader2 className="h-4 w-4 animate-spin" />

动画与过渡

Tailwind 动画

// 使用 tailwindcss-animate
<div className="animate-in fade-in slide-in-from-top-4">
  {/* 内容 */}
</div>

// 常用动画类
animate-in          // 进入动画
animate-out         // 退出动画
fade-in             // 淡入
slide-in-from-top  // 从顶部滑入
duration-300        // 持续时间

性能考虑

  • 使用 transformopacity 触发 GPU 加速
  • 避免动画 widthheight 等属性
  • 使用 will-change 提示浏览器优化

响应式设计

确保界面在不同设备上都能提供良好的用户体验。采用移动优先策略,使用灵活的布局技术。

移动优先策略

移动端设计原则

  • 先设计移动端,再逐步增强桌面端体验
  • 简化移动端交互,减少操作步骤
  • 优化触摸目标大小(最小 44x44px)
  • 减少移动端内容,突出核心功能

移动端导航模式

底部导航
适合主要功能入口
汉堡菜单
适合次要功能
标签页
适合内容分类

布局技术

Flexbox 高级用法

// 响应式 Flexbox
<div className="flex flex-col md:flex-row gap-4">
  <div className="flex-1">内容 1</div>
  <div className="flex-1">内容 2</div>
</div>

// 对齐方式
<div className="flex items-center justify-between">
  {/* 垂直居中,水平两端对齐 */}
</div>

断点策略

sm
≥640px
小屏设备
md
≥768px
平板
lg
≥1024px
笔记本
xl
≥1280px
桌面
2xl
≥1536px
大屏
// 断点使用最佳实践
// 移动优先:默认样式适用于移动端
<div className="text-sm md:text-base lg:text-lg">
  {/* 从小屏到大屏逐步增强 */}
</div>

// 避免:桌面优先(不推荐)
<div className="text-lg md:text-base sm:text-sm">
  {/* 这样会增加移动端负担 */}
</div>

移动端优化

视口配置

// app/layout.tsx
export const metadata = {
  viewport: {
    width: 'device-width',
    initialScale: 1,
    maximumScale: 5,
  },
}

触摸事件处理

  • 使用 touch-action 优化触摸响应
  • 避免 hover 状态在移动端的误触
  • 使用 @media (hover: hover) 检测设备是否支持悬停

可访问性(Accessibility)

可访问性确保所有用户都能使用你的应用,包括使用辅助技术的用户。遵循 WCAG 标准,让产品更加包容。

WCAG 标准

WCAG 2.1 级别

级别 A
最低要求,基本可访问性
级别 AA(推荐)
大多数网站应达到的标准
级别 AAA
最高标准,特殊需求

关键原则(POUR)

可感知
信息可通过多种方式感知
可操作
界面组件和导航可操作
可理解
信息和操作可理解
可健壮
内容可被各种辅助技术解释

键盘导航

Tab 顺序管理

// 使用 tabIndex 控制 Tab 顺序
<button tabIndex={1}>第一个</button>
<button tabIndex={2}>第二个</button>

// 跳过不可交互元素
<div tabIndex={-1}>跳过此元素</div>

// 移除 Tab 顺序(不推荐,除非必要)
<button tabIndex={-1}>不可通过 Tab 访问</button>

焦点管理

// 焦点陷阱(Focus Trap)- 模态框中使用
import { FocusTrap } from '@radix-ui/react-focus-trap'

<FocusTrap>
  <Dialog>
    {/* 焦点被限制在对话框内 */}
  </Dialog>
</FocusTrap>

// 焦点可见样式
<button className="focus-visible:ring-2 focus-visible:ring-primary">
  {/* 键盘导航时显示焦点环 */}
</button>

快捷键支持

  • Enter/Space:激活按钮或链接
  • Esc:关闭模态框或菜单
  • Arrow Keys:导航列表或菜单项
  • Tab:在可聚焦元素间移动

屏幕阅读器支持

ARIA 标签使用

// aria-label:为元素提供标签
<button aria-label="关闭对话框">
  <X />
</button>

// aria-labelledby:引用其他元素的 ID
<div aria-labelledby="dialog-title">
  <h2 id="dialog-title">对话框标题</h2>
</div>

// aria-describedby:提供额外描述
<input 
  aria-describedby="email-help"
  aria-invalid={hasError}
/>
<span id="email-help">请输入有效的邮箱地址</span>

语义化 HTML

// 使用语义化标签
<nav>导航</nav>
<main>主要内容</main>
<article>文章</article>
<section>章节</section>
<aside>侧边栏</aside>
<header>页头</header>
<footer>页脚</footer>

// 避免:使用 div 代替语义标签
<div className="nav"> {/* 不推荐 */}
<nav> {/* 推荐 */}

角色定义

// 使用 role 属性(当语义标签不可用时)
<div role="button" tabIndex={0} onClick={handleClick}>
  自定义按钮
</div>

<div role="alert" aria-live="polite">
  重要通知
</div>

<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  {/* 模态对话框 */}
</div>

颜色对比度

WCAG AA 标准

正常文本
对比度 ≥ 4.5:1
大文本(18px+ 或 14px+ 粗体)
对比度 ≥ 3:1

工具检查

  • WebAIM Contrast Checker:在线对比度检查工具
  • Lighthouse:Chrome DevTools 内置检查
  • axe DevTools:浏览器扩展

不依赖颜色传达信息

// 错误:仅用颜色表示状态
<span className="text-red-500">错误</span>

// 正确:颜色 + 图标/文字
<span className="text-red-500 flex items-center gap-1">
  <AlertCircle />
  错误:请输入有效值
</span>

无障碍性测试

自动化测试工具

axe
npm install @axe-core/react
Lighthouse
Chrome DevTools
WAVE
浏览器扩展

手动测试流程

  1. 仅使用键盘导航整个应用
  2. 使用屏幕阅读器(NVDA、JAWS、VoiceOver)测试
  3. 检查颜色对比度
  4. 验证所有图片都有 alt 文本
  5. 测试表单错误提示

键盘测试清单

  • 所有交互元素都可以通过 Tab 访问
  • 焦点顺序逻辑合理
  • 焦点可见(有焦点环)
  • 模态框可以 Esc 关闭
  • 菜单可以通过方向键导航

性能优化

性能直接影响用户体验。优化渲染性能、资源加载和运行时性能,确保应用快速响应。

渲染性能

React 渲染优化

// 使用 memo 避免不必要的重渲染
const ExpensiveComponent = React.memo(({ data }) => {
  return <div>{/* 复杂渲染 */}</div>
})

// 使用 useMemo 缓存计算结果
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b)
}, [a, b])

// 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
  doSomething(id)
}, [id])

虚拟滚动

对于大量列表,使用虚拟滚动只渲染可见项:

// 使用 react-window 或 react-virtualized
import { FixedSizeList } from 'react-window'

<FixedSizeList
  height={600}
  itemCount={10000}
  itemSize={50}
  width="100%"
>
  {Row}
</FixedSizeList>

代码分割

// 动态导入组件
import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Skeleton />,
  ssr: false, // 如果需要禁用 SSR
})

// 路由级别的代码分割(Next.js 自动处理)
// app/dashboard/page.tsx 会自动分割

资源优化

图片优化

// Next.js Image 组件自动优化
import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // 首屏图片
  loading="lazy" // 非首屏图片
  placeholder="blur" // 模糊占位符
/>

// 支持 WebP、AVIF 等现代格式
// 自动生成响应式图片

字体优化

// 使用 next/font 优化字体
import { GeistSans } from 'next/font/google'

const geistSans = GeistSans({
  subsets: ['latin'],
  display: 'swap', // 字体加载策略
  preload: true,
})

// 字体子集化,只加载需要的字符

资源预加载

// 预连接第三方域名
<link rel="preconnect" href="https://fonts.googleapis.com" />

// 预加载关键资源
<link rel="preload" href="/fonts/geist.woff2" as="font" />

// 预获取下一页资源
<link rel="prefetch" href="/dashboard" />

运行时性能

Bundle 大小优化

  • 使用 bundle analyzer 分析包大小
  • 按需导入库(import { debounce } from 'lodash-es'
  • 移除未使用的依赖

Tree Shaking

Next.js 和现代打包工具自动进行 Tree Shaking,移除未使用的代码。

性能监控

Core Web Vitals

LCP
最大内容绘制 < 2.5s
FID
首次输入延迟 < 100ms
CLS
累积布局偏移 < 0.1

性能分析工具

  • React DevTools Profiler:分析组件渲染性能
  • Lighthouse:Chrome DevTools 性能审计
  • Web Vitals Extension:实时监控 Core Web Vitals

实战示例

通过实际代码示例,学习如何构建高质量的 UI 组件和交互。

完整表单组件示例

'use client'

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
import { toast } from 'sonner'

const formSchema = z.object({
  name: z.string().min(2, '姓名至少 2 个字符'),
  email: z.string().email('无效的邮箱地址'),
  age: z.number().min(18, '年龄必须大于 18').max(100),
})

export function UserForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: '',
      email: '',
      age: 18,
    },
  })

  async function onSubmit(values: z.infer<typeof formSchema>) {
    try {
      await saveUser(values)
      toast.success('用户创建成功')
      form.reset()
    } catch (error) {
      toast.error('创建失败,请重试')
    }
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>姓名</FormLabel>
              <FormControl>
                <Input placeholder="请输入姓名" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? '提交中...' : '提交'}
        </Button>
      </form>
    </Form>
  )
}

学习成果

完成本章后,你将:

  • 1理解设计系统基础,掌握 Design Tokens、主题系统和组件架构
  • 2熟练使用 Tailwind CSS 和 shadcn/ui,能够配置和自定义组件库
  • 3掌握状态管理、表单处理和动画技巧,构建流畅的交互体验
  • 4掌握响应式设计方法,使用移动优先策略和现代布局技术
  • 5理解可访问性标准(WCAG),能够实现键盘导航和屏幕阅读器支持
  • 6掌握性能优化技巧,包括渲染优化、资源加载和性能监控
  • 7能够构建完整的表单组件、响应式布局和交互式功能