WuYongJie 5821d73291 初始提交:3D家具安装展示系统
包含:
- furniture-3d-system:Next.js 16 全栈应用(资产中心、产品管理、3D装配动画)
- autocad-plugin:AutoCAD 家具导出插件源码
- 资产中心改造:编辑弹窗化、删除功能、Toast反馈、自定义Select组件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 14:00:19 +08:00

15 KiB
Raw Blame History

Apple 风格 UI 组件使用指南

⚠️ 重要提示: 本文档提供代码示例,但设计标准以 docs/设计规范/苹果风格视觉标准.md 为准。在使用代码示例时,请确保符合最新的设计 Token(如颜色、字体等)。

本指南提供了 Apple 风格 UI 设计系统中所有组件的详细使用说明和最佳实践。

目录

  1. 设计令牌
  2. 基础组件
  3. 导航组件
  4. 数据展示组件
  5. 反馈组件
  6. 媒体组件
  7. 布局组件
  8. 动画组件
  9. 可访问性工具
  10. 性能优化

设计令牌

字体系统

import { LargeTitle, Title1, Body, Caption1 } from '@/components/ui/Typography'

function MyComponent() {
  return (
    <div>
      <LargeTitle>大标题 (34pt)</LargeTitle>
      <Title1>标题 1 (28pt)</Title1>
      <Body>正文内容 (17pt)</Body>
      <Caption1>说明文字 (12pt)</Caption1>
    </div>
  )
}

最佳实践:

  • 使用语义化的字体组件而不是直接的 CSS 类
  • 遵循苹果的字体层级,不要跳级使用
  • 中英文混排时使用 fontType="chinese" 属性

颜色系统

// 使用 Tailwind 类名
<div className="bg-apple-blue-light text-white">
  蓝色背景
</div>

// 使用设计令牌
import { appleColorTokens } from '@/lib/design-tokens'
const blueColor = appleColorTokens.system.blue.light

最佳实践:

  • 优先使用语义化颜色(如 apple-bg-primary)
  • 深色模式会自动切换颜色
  • 使用系统颜色保持一致性

基础组件

Button 按钮

import Button from '@/components/ui/Button'

function ButtonExample() {
  return (
    <div className="space-x-4">
      {/* 四种变体 */}
      <Button variant="filled">Filled</Button>
      <Button variant="tinted">Tinted</Button>
      <Button variant="gray">Gray</Button>
      <Button variant="plain">Plain</Button>
      
      {/* 三种尺寸 */}
      <Button size="small">Small (32px)</Button>
      <Button size="regular">Regular (44px)</Button>
      <Button size="large">Large (50px)</Button>
      
      {/* 状态 */}
      <Button loading>Loading</Button>
      <Button disabled>Disabled</Button>
    </div>
  )
}

最佳实践:

  • 主要操作使用 filled 变体
  • 次要操作使用 tinted 或 gray 变体
  • 确保按钮最小尺寸为 44px(苹果触摸目标标准)
  • 使用 loading 状态而不是禁用按钮

Input 输入框

import Input from '@/components/ui/Input'

function InputExample() {
  return (
    <div className="space-y-4">
      {/* 基础输入框 */}
      <Input
        label="用户名"
        placeholder="请输入用户名"
        helperText="用户名必须唯一"
        required
      />
      
      {/* 状态变体 */}
      <Input
        label="邮箱"
        type="email"
        state="success"
        helperText="邮箱格式正确"
      />
      
      <Input
        label="密码"
        type="password"
        errorText="密码长度至少 8 位"
      />
      
      {/* 带图标 */}
      <Input
        label="搜索"
        leftIcon="🔍"
        rightIcon="✕"
        onRightIconClick={() => console.log('清除')}
      />
    </div>
  )
}

最佳实践:

  • 始终提供 label 属性以支持可访问性
  • 使用 helperText 提供额外说明
  • 错误状态使用 errorText 而不是 helperText
  • 合理使用图标增强用户体验

Card 卡片

import Card, { CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/Card'

function CardExample() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
      {/* 简单卡片 */}
      <Card className="p-6">
        <h3 className="text-lg font-semibold mb-2">卡片标题</h3>
        <p className="text-gray-600">卡片内容</p>
      </Card>
      
      {/* 组合卡片 */}
      <Card variant="elevated">
        <CardHeader>
          <CardTitle>高级卡片</CardTitle>
        </CardHeader>
        <CardContent>
          <p>使用组合组件构建的卡片</p>
        </CardContent>
        <CardFooter>
          <Button size="small">操作</Button>
        </CardFooter>
      </Card>
      
      {/* 交互式卡片 */}
      <Card 
        interactive 
        onClick={() => console.log('卡片点击')}
        className="cursor-pointer"
      >
        <div className="p-6">
          <h3>可点击卡片</h3>
          <p>支持 hover 和 active 动画</p>
        </div>
      </Card>
    </div>
  )
}

最佳实践:

  • 使用 variant="elevated" 创建层次感
  • 交互式卡片添加 interactive 属性
  • 保持卡片内容简洁,避免过度嵌套

导航组件

NavigationBar 导航栏

import NavigationBar, { BackButton, MenuButton } from '@/components/ui/NavigationBar'

function NavigationExample() {
  return (
    <div className="space-y-4">
      {/* 大标题模式 */}
      <NavigationBar
        mode="large"
        title="大标题导航"
        subtitle="副标题说明"
        leftContent={<BackButton onBack={() => history.back()} />}
        rightContent={<MenuButton onMenu={() => console.log('菜单')} />}
      />
      
      {/* 标准模式 */}
      <NavigationBar
        mode="standard"
        title="标准导航"
        leftContent={<BackButton />}
        rightContent={
          <div className="flex gap-2">
            <Button size="small" variant="plain">编辑</Button>
            <Button size="small" variant="tinted">保存</Button>
          </div>
        }
      />
    </div>
  )
}

TabBar 标签栏

import TabBar, { useTabBar } from '@/components/ui/TabBar'

function TabBarExample() {
  const tabBar = useTabBar('home')
  
  const tabs = [
    { id: 'home', label: '首页', icon: '🏠' },
    { id: 'search', label: '搜索', icon: '🔍' },
    { id: 'favorites', label: '收藏', icon: '❤️', badge: 3 },
    { id: 'profile', label: '我的', icon: '👤' },
  ]
  
  return (
    <TabBar
      items={tabs}
      activeId={tabBar.activeTab}
      onTabChange={tabBar.switchTab}
    />
  )
}

最佳实践:

  • 标签栏最多显示 5 个标签
  • 使用 useTabBar Hook 管理状态
  • 为重要标签添加徽章提示

数据展示组件

Badge 徽章

import Badge, { Tag, NumberBadge, StatusDot } from '@/components/ui/Badge'

function BadgeExample() {
  return (
    <div className="space-y-4">
      {/* 基础徽章 */}
      <div className="flex gap-2">
        <Badge>Default</Badge>
        <Badge type="success">Success</Badge>
        <Badge type="error">Error</Badge>
        <Badge type="warning">Warning</Badge>
        <Badge type="info">Info</Badge>
      </div>
      
      {/* 数字徽章 */}
      <div className="flex gap-2">
        <NumberBadge count={5} />
        <NumberBadge count={99} max={99} />
        <NumberBadge count={999} max={99} />
      </div>
      
      {/* 标签 */}
      <div className="flex gap-2">
        <Tag closable onClose={() => console.log('关闭')}>
          可关闭标签
        </Tag>
        <Tag icon="🔥" type="success">
          带图标标签
        </Tag>
      </div>
      
      {/* 状态点 */}
      <div className="flex gap-2">
        <StatusDot status="online" />
        <StatusDot status="busy" />
        <StatusDot status="away" />
        <StatusDot status="offline" />
      </div>
    </div>
  )
}

ProgressBar 进度条

import ProgressBar, { StepProgress } from '@/components/ui/ProgressBar'

function ProgressExample() {
  const [progress, setProgress] = useState(45)
  
  return (
    <div className="space-y-6">
      {/* 线性进度条 */}
      <ProgressBar
        value={progress}
        showLabel
        showPercentage
        label="下载进度"
        animated
      />
      
      {/* 环形进度条 */}
      <ProgressBar
        value={progress}
        variant="circular"
        size="large"
        type="success"
        showPercentage
      />
      
      {/* 步骤进度 */}
      <StepProgress
        current={2}
        total={4}
        showLabels
        labels={['开始', '配置', '确认', '完成']}
      />
    </div>
  )
}

反馈组件

Modal 模态框

import Modal, { ConfirmModal } from '@/components/ui/Modal'

function ModalExample() {
  const [showModal, setShowModal] = useState(false)
  const [showConfirm, setShowConfirm] = useState(false)
  
  return (
    <div>
      <Button onClick={() => setShowModal(true)}>
        打开模态框
      </Button>
      
      <Modal
        open={showModal}
        onClose={() => setShowModal(false)}
        title="模态框标题"
        description="模态框描述"
        size="medium"
      >
        <div className="space-y-4">
          <p>模态框内容</p>
          <div className="flex gap-3">
            <Button variant="filled">确认</Button>
            <Button variant="gray" onClick={() => setShowModal(false)}>
              取消
            </Button>
          </div>
        </div>
      </Modal>
      
      <ConfirmModal
        open={showConfirm}
        onClose={() => setShowConfirm(false)}
        title="确认删除"
        description="此操作无法撤销"
        onConfirm={() => console.log('确认删除')}
      />
    </div>
  )
}

Toast 提示

import { useToast, ToastContainer } from '@/components/ui/Toast'

function ToastExample() {
  const toast = useToast()
  
  return (
    <div>
      <div className="space-x-2">
        <Button onClick={() => toast.success('操作成功')}>
          成功提示
        </Button>
        <Button onClick={() => toast.error('操作失败')}>
          错误提示
        </Button>
        <Button onClick={() => toast.info('新消息', {
          title: '系统通知',
          action: {
            label: '查看',
            onClick: () => console.log('查看消息')
          }
        })}>
          信息提示
        </Button>
      </div>
      
      <ToastContainer
        toasts={toast.toasts}
        onClose={toast.removeToast}
      />
    </div>
  )
}

动画组件

AnimatedEntry 进入动画

import AnimatedEntry, { SlideInUp, StaggeredAnimation } from '@/components/ui/AnimatedEntry'

function AnimationExample() {
  return (
    <div className="space-y-8">
      {/* 基础进入动画 */}
      <AnimatedEntry direction="up" duration="normal">
        <Card className="p-6">
          <h3>从下方滑入的卡片</h3>
        </Card>
      </AnimatedEntry>
      
      {/* 预配置动画 */}
      <SlideInUp delay={200}>
        <div>延迟 200ms 的滑入动画</div>
      </SlideInUp>
      
      {/* 交错动画 */}
      <StaggeredAnimation staggerDelay={100}>
        {[1, 2, 3, 4].map(i => (
          <Card key={i} className="p-4">
            <p>卡片 {i}</p>
          </Card>
        ))}
      </StaggeredAnimation>
    </div>
  )
}

最佳实践:

  • 使用 trigger="visible" 实现滚动触发动画
  • 交错动画的延迟时间建议 50-150ms
  • 响应用户的减少动画偏好设置

可访问性工具

用户偏好设置

import { PreferencesProvider, AccessibilityPanel, useUserPreferences } from '@/lib/preferences'

function App() {
  return (
    <PreferencesProvider>
      <div className="app">
        <AccessibilityPanel />
        <MyContent />
      </div>
    </PreferencesProvider>
  )
}

function MyContent() {
  const { preferences } = useUserPreferences()
  
  return (
    <div>
      <p>当前字体大小: {preferences.fontSize}%</p>
      <p>减少动画: {preferences.prefersReducedMotion ? '是' : '否'}</p>
    </div>
  )
}

包容性设计

import { createInclusiveStatusIndicator } from '@/lib/accessibility'

function StatusExample() {
  const successIndicator = createInclusiveStatusIndicator('success', {
    showIcon: true,
    showShape: true,
    size: 'medium'
  })
  
  return (
    <div 
      className="status-indicator success shape-circle"
      style={successIndicator.style}
      aria-label={successIndicator.ariaLabel}
    >
      {successIndicator.icon}
    </div>
  )
}

性能优化

虚拟滚动

import VirtualList from '@/components/ui/VirtualList'

function VirtualListExample() {
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `项目 ${i + 1}`,
    description: `描述 ${i + 1}`
  }))
  
  return (
    <VirtualList
      items={items}
      itemHeight={60}
      height={400}
      renderItem={(item, index) => (
        <div className="flex items-center p-4 border-b">
          <div className="flex-1">
            <h4>{item.name}</h4>
            <p className="text-sm text-gray-500">{item.description}</p>
          </div>
        </div>
      )}
    />
  )
}

性能监控

import { usePerformanceMonitor, useMemoryMonitor } from '@/lib/performance'

function PerformanceExample() {
  usePerformanceMonitor('MyComponent')
  const memoryInfo = useMemoryMonitor()
  
  return (
    <div>
      {memoryInfo && (
        <p>内存使用: {Math.round(memoryInfo.usedJSHeapSize / 1024 / 1024)}MB</p>
      )}
    </div>
  )
}

迁移指南

从传统 CSS 迁移

// 旧方式
<div className="bg-blue-500 text-white p-4 rounded-lg">
  内容
</div>

// 新方式
<Card variant="filled" className="bg-apple-blue-light text-white">
  <div className="p-apple-4">
    内容
  </div>
</Card>

从其他 UI 库迁移

// Material-UI Button
<Button variant="contained" color="primary">
  按钮
</Button>

// Apple UI Button
<Button variant="filled">
  按钮
</Button>

常见问题

Q: 如何自定义主题颜色?

A: 修改 tailwind.config.ts 中的颜色令牌,或在 CSS 中覆盖 CSS 变量。

Q: 动画不生效怎么办?

A: 检查用户是否启用了减少动画偏好,或者组件是否设置了 disabled 属性。

Q: 如何确保可访问性?

A: 使用语义化 HTML,提供 ARIA 属性,确保键盘导航,测试对比度。

Q: 性能优化建议?

A: 使用 React.memo,避免不必要的重渲染,大列表使用虚拟滚动。

Slider 滑动条(统一元组件)

Slider 元组件位于 src/components/ui/Slider.tsx,用于统一替代原生 input[type="range"]。

import Slider from '@/components/ui/Slider'

function DemoSlider() {
  return (
    <Slider
      min={0}
      max={1}
      step={0.01}
      value={0.45}
      onValueChange={(value) => console.log(value)}
      aria-label="爆炸程度"
      className="w-40"
    />
  )
}

最佳实践:

  • 必须使用 onValueChange 接收数值,避免在业务层反复做字符串转数字。
  • 必须补齐 aria-label,保证键盘与读屏可访问性。
  • 禁止新增独立的 range 样式分叉,统一使用 apple-slider 视觉规范。

总结

Apple 风格 UI 设计系统提供了完整的组件库和工具集,遵循苹果 Human Interface Guidelines 2025 标准。通过合理使用这些组件和工具,可以构建出美观、易用、高性能的用户界面。

记住始终考虑可访问性、性能和用户体验,让每个用户都能享受到优质的交互体验。