别再为设计系统头疼了!这个开源方案让设计师和开发者终于能无缝协作

别再为设计系统头疼了!这个开源方案让设计师和开发者终于能无缝协作

别再为设计系统头疼了!这个开源方案让设计师和开发者终于能无缝协作

从痛点到解法,一文搞懂 nexu-io/open-design 的完整生态体系


为什么值得关注

在现代软件开发中,设计与开发的协作效率直接影响产品的交付质量。设计师用 Figma、Sketch 完成视觉稿,开发者在代码中重新实现这些设计,而设计变更时两边需要反复沟通确认。这种模式不仅效率低下,还容易出现视觉不一致的问题。

nexu-io/open-design 正是为了解决这一痛点而生的开源项目。它提供了一套完整的设计系统解决方案,让设计稿能够直接转换为可用的代码,同时保持设计意图和实现的一致性。无论是 UI 组件库、设计令牌管理,还是设计到代码的工作流,这个项目都提供了开箱即用的工具和最佳实践。

对于中小型团队来说,从头构建一套设计系统需要投入大量时间和资源。open-design 的出现降低了这一门槛,开发者可以直接基于该项目构建自己的设计系统,设计师也能获得一套可直接使用的设计资产。

本篇文章将带你从零开始,深入了解这个项目的每一个功能模块,通过实际案例演示如何将它应用到真实项目中。无论你是前端开发者、UI 设计师,还是技术负责人,都能从中获得实用的知识和经验。


项目概述与核心定位

nexu-io/open-design 是一个面向现代 Web 应用的开放设计系统。它不仅仅是一套 UI 组件,更是一个完整的生态系统,包含了设计令牌的标准化定义、React 组件库、设计工具插件以及开发工具链。

这个项目的设计理念强调「设计即代码,代码即设计」。传统的工作流中,设计稿和代码实现往往是两个独立维护的实体,设计变更需要手动同步到代码中。而 open-design 通过统一的设计令牌机制,确保设计侧的色彩、字体、间距等属性能够自动同步到代码实现中。

项目的技术栈选择也体现了对现代开发生态的适配。前端组件基于 React 和 TypeScript 构建,确保类型安全的同时提供了良好的开发体验。样式方案支持 CSS-in-JS 和传统 CSS 两种模式,团队可以根据现有技术栈灵活选择。设计工具方面,提供了 Figma 插件用于设计令牌的同步和管理。

值得注意的是,这个项目采用了 MIT 开源许可证,这意味着任何人都可以自由使用、修改和商业化。这种开放的姿态使得项目能够获得社区的广泛参与和贡献,持续迭代改进。


环境准备与快速上手

在开始使用 open-design 之前,我们需要确保开发环境满足基本要求。项目对运行环境有以下要求:

首先,Node.js 版本需要 18.0 或更高版本。推荐使用 LTS 版本以确保稳定性。可以通过以下命令检查当前 Node.js 版本:

node --version

如果版本不符合要求,建议使用 nvm(Node Version Manager)来管理多个 Node.js 版本。在 macOS 和 Linux 系统中,可以这样安装和使用 nvm:

# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# 安装 Node.js LTS 版本
nvm install --lts

# 切换到 LTS 版本
nvm use --lts

# 验证安装
node --version
npm --version

在 Windows 系统中,可以使用 nvm-windows 来实现类似的功能。安装完成后,继续安装项目的核心依赖。

创建新项目时,推荐使用官方提供的初始化脚本来快速搭建项目结构。这个脚商会自动配置好 TypeScript、样式解决方案以及必要的开发工具:

# 使用 npm 创建新项目
npm create @open-design/app@latest my-design-system

# 进入项目目录
cd my-design-system

# 安装依赖
npm install

# 启动开发服务器
npm run dev

初始化脚本会询问几个问题来定制项目配置,包括包名称、项目描述、首选的样式解决方案以及是否需要示例组件。这些配置可以在后续通过修改配置文件来调整。

对于已有项目希望引入 open-design 的情况,可以单独安装所需的包:

# 安装核心包
npm install @open-design/core

# 安装组件库
npm install @open-design/components

# 安装设计令牌
npm install @open-design/tokens

项目结构遵循统一的目录规范,主要包含以下几个核心目录:

  • src/components:存放所有 UI 组件的源代码
  • src/tokens:设计令牌的定义文件
  • src/styles:全局样式和主题配置
  • src/utils:工具函数和辅助方法
  • docs:文档和示例代码

了解项目结构后,我们来深入了解设计令牌这一核心概念。


设计令牌体系详解

设计令牌是 open-design 体系中的基石概念。简单来说,设计令牌就是设计的原子单位,它将视觉设计中的各种属性提取为可复用的变量。这些变量可以在设计工具和代码之间无缝传递,确保视觉一致性的同时简化了维护工作。

open-design 的设计令牌体系分为三个层级:全局令牌、语义令牌和组件特定令牌。

全局令牌是最基础的层级,包含了所有设计决策的具体数值。例如,一个品牌的主色调可能是十六进制值 #3B82F6,字体大小可能是 16 像素。这些都是全局令牌的具体取值。

语义令牌则赋予全局令牌以业务含义。比如,primary-color 这个语义令牌可能引用了全局令牌中的 #3B82F6,但它的含义是「主要的交互颜色」,而不是简单的「蓝色」。当品牌需要调整主色调时,只需要修改 primary-color 引用的具体值,所有使用这个语义令牌的地方都会自动更新。

组件特定令牌用于处理组件内部的细节差异。比如,按钮组件可能有自己的 border-radius-token 用于控制圆角大小,这个令牌在语义层面属于按钮组件,但在实现层面仍然是基于全局令牌定义的。

在代码中使用设计令牌非常简单。以下是一个完整的示例,演示如何定义和使用各类设计令牌:

# Python 示例:设计令牌的定义与使用
# 这是概念演示,实际使用时令器通过 JavaScript/TypeScript 访问

# 定义全局令牌
GLOBAL_TOKENS = {
    "color": {
        "blue": {
            "50": "#EFF6FF",
            "100": "#DBEAFE",
            "500": "#3B82F6",
            "900": "#1E3A8A"
        },
        "gray": {
            "50": "#F9FAFB",
            "100": "#F3F4F6",
            "500": "#6B7280",
            "900": "#111827"
        }
    },
    "spacing": {
        "xs": "4px",
        "sm": "8px",
        "md": "16px",
        "lg": "24px",
        "xl": "32px"
    },
    "fontSize": {
        "xs": "12px",
        "sm": "14px",
        "base": "16px",
        "lg": "18px",
        "xl": "20px",
        "2xl": "24px"
    }
}

# 定义语义令牌(引用全局令牌)
SEMANTIC_TOKENS = {
    "color": {
        "primary": {
            "background": "color.blue.500",
            "foreground": "color.gray.50",
            "hover": "color.blue.900"
        },
        "text": {
            "primary": "color.gray.900",
            "secondary": "color.gray.500",
            "disabled": "color.gray.300"
        },
        "border": {
            "default": "color.gray.200",
            "focus": "color.blue.500"
        }
    },
    "spacing": {
        "componentPadding": "spacing.md",
        "componentGap": "spacing.sm"
    }
}

# 使用示例函数
def get_button_styles():
    """
    获取按钮组件的样式配置
    使用语义令牌确保设计一致性
    """
    return {
        "backgroundColor": SEMANTIC_TOKENS["color"]["primary"]["background"],
        "color": SEMANTIC_TOKENS["color"]["primary"]["foreground"],
        "padding": f"{SEMANTIC_TOKENS['spacing']['componentPadding']} {SEMANTIC_TOKENS['spacing']['componentGap']}",
        "borderRadius": GLOBAL_TOKENS["spacing"]["md"],
        "fontSize": GLOBAL_TOKENS["fontSize"]["base"],
        "transition": "all 0.2s ease"
    }

在实际项目中,设计令牌通常以 JSON 或 TypeScript 文件的形式存储在仓库中。open-design 提供了令牌转换工具,可以将这些定义自动生成为不同格式的输出:

# 转换令牌为 CSS 变量
npx open-design tokens --format css --output ./src/styles/tokens.css

# 转换令牌为 JavaScript 模块
npx open-design tokens --format js --output ./src/tokens/index.ts

# 转换令牌为 Figma 兼容格式
npx open-design tokens --format figma --output ./tokens.fig.json

生成的 CSS 变量文件示例如下:

:root {
  /* 全局颜色令牌 */
  --color-blue-50: #EFF6FF;
  --color-blue-100: #DBEAFE;
  --color-blue-500: #3B82F6;
  --color-blue-900: #1E3A8A;

  /* 语义颜色令牌 */
  --color-primary-bg: var(--color-blue-500);
  --color-primary-fg: var(--color-gray-50);

  /* 间距令牌 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;

  /* 字体大小令牌 */
  --font-size-xs: 12px;
  --font-size-sm: 14px;
  --font-size-base: 16px;
}

这种多格式输出的能力使得设计令牌能够在不同平台和工具之间流通,真正实现了设计开发的协同。


核心组件库实践

open-design 的组件库提供了丰富的 UI 组件,涵盖了现代 Web 应用中最常用的交互元素。每个组件都经过精心设计,遵循一致的设计语言,同时保持了良好的可访问性和响应式行为。

按钮组件是最基础也是使用频率最高的组件之一。open-design 的 Button 组件支持多种变体和尺寸,能够满足各种场景的需求:

# 按钮组件使用示例(概念演示)
# 实际使用请参考 React 组件 API

class ButtonVariants:
    """按钮变体定义"""
    SOLID = "solid"      # 实心按钮,用于主要操作
    OUTLINE = "outline"  # 描边按钮,用于次要操作
    GHOST = "ghost"      # 幽灵按钮,用于低优先级操作
    LINK = "link"       # 链接样式,用于文字内操作

class ButtonSizes:
    """按钮尺寸定义"""
    XS = "xs"   # 超小尺寸
    SM = "sm"   # 小尺寸
    MD = "md"   # 中等尺寸(默认)
    LG = "lg"   # 大尺寸

# 示例配置
button_config = {
    "primary_action": {
        "variant": ButtonVariants.SOLID,
        "size": ButtonSizes.MD,
        "colorScheme": "blue",
        "isLoading": False,
        "leftIcon": "save"  # 可选的左侧图标
    },
    "secondary_action": {
        "variant": ButtonVariants.OUTLINE,
        "size": ButtonSizes.MD,
        "colorScheme": "gray"
    },
    "danger_action": {
        "variant": ButtonVariants.SOLID,
        "size": ButtonSizes.MD,
        "colorScheme": "red",
        "leftIcon": "warning"
    }
}

在实际的 React 代码中,按钮组件的典型用法如下:

import { Button } from '@open-design/components';
import { SaveIcon, DeleteIcon } from './icons';

// 主要操作按钮
function PrimaryButton() {
  return (
    <Button
      variant="solid"
      colorScheme="blue"
      leftIcon={<SaveIcon />}
      onClick={handleSave}
    >
      保存修改
    </Button>
  );
}

// 次要操作按钮
function SecondaryButton() {
  return (
    <Button
      variant="outline"
      colorScheme="gray"
      onClick={handleCancel}
    >
      取消
    </Button>
  );
}

// 危险操作按钮(带确认)
function DangerButton() {
  const [isConfirming, setIsConfirming] = React.useState(false);

  const handleClick = () => {
    if (!isConfirming) {
      setIsConfirming(true);
      // 3秒后自动取消确认状态
      setTimeout(() => setIsConfirming(false), 3000);
    } else {
      // 确认执行删除
      executeDelete();
    }
  };

  return (
    <Button
      variant="solid"
      colorScheme="red"
      leftIcon={<DeleteIcon />}
      onClick={handleClick}
    >
      {isConfirming ? '确认删除' : '删除'}
    </Button>
  );
}

表单组件是另一个核心模块。open-design 提供了完整的表单控件,包括输入框、选择器、复选框、单选框、开关等。这些组件都与表单验证系统深度集成,提供了开箱即用的体验:

# 表单组件配置示例
# 展示一个典型的用户注册表单配置

form_fields = [
    {
        "name": "username",
        "type": "text",
        "label": "用户名",
        "placeholder": "请输入用户名",
        "rules": [
            {"type": "required", "message": "用户名不能为空"},
            {"type": "minLength", "value": 3, "message": "用户名至少3个字符"},
            {"type": "maxLength", "value": 20, "message": "用户名最多20个字符"},
            {
                "type": "pattern",
                "value": "^[a-zA-Z0-9_]+$",
                "message": "用户名只能包含字母、数字和下划线"
            }
        ],
        "helpText": "用于登录和展示,长度3-20个字符"
    },
    {
        "name": "email",
        "type": "email",
        "label": "邮箱地址",
        "placeholder": "example@domain.com",
        "rules": [
            {"type": "required", "message": "邮箱不能为空"},
            {"type": "email", "message": "请输入有效的邮箱地址"}
        ]
    },
    {
        "name": "password",
        "type": "password",
        "label": "密码",
        "placeholder": "请设置密码",
        "rules": [
            {"type": "required", "message": "密码不能为空"},
            {"type": "minLength", "value": 8, "message": "密码至少8个字符"},
            {
                "type": "pattern",
                "value": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
                "message": "密码需包含大小写字母和数字"
            }
        ],
        "suffix": {
            "type": "passwordStrength",
            "levels": [
                {"label": "弱", "color": "red"},
                {"label": "中", "color": "yellow"},
                {"label": "强", "color": "green"}
            ]
        }
    },
    {
        "name": "confirmPassword",
        "type": "password",
        "label": "确认密码",
        "placeholder": "请再次输入密码",
        "rules": [
            {"type": "required", "message": "请确认密码"},
            {
                "type": "custom",
                "validator": lambda value, form: value == form["password"],
                "message": "两次输入的密码不一致"
            }
        ]
    },
    {
        "name": "gender",
        "type": "radio-group",
        "label": "性别",
        "options": [
            {"value": "male", "label": "男"},
            {"value": "female", "label": "女"},
            {"value": "other", "label": "其他"}
        ],
        "layout": "inline"  # 行内排列
    },
    {
        "name": "interests",
        "type": "checkbox-group",
        "label": "兴趣领域",
        "options": [
            {"value": "frontend", "label": "前端开发"},
            {"value": "backend", "label": "后端开发"},
            {"value": "design", "label": "UI/UX设计"},
            {"value": "devops", "label": "DevOps"}
        ],
        "rules": [
            {"type": "required", "message": "请至少选择一个兴趣领域"}
        ]
    },
    {
        "name": "subscribe",
        "type": "switch",
        "label": "订阅产品更新邮件",
        "defaultValue": True  # 默认开启
    }
]

表单组件的 React 使用示例:

import { Form, Input, RadioGroup, CheckboxGroup, Switch } from '@open-design/components';
import { useForm } from '@open-design/hooks';

function RegistrationForm() {
  const form = useForm({
    defaultValues: {
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      gender: 'male',
      interests: [],
      subscribe: true
    }
  });

  const handleSubmit = async (values) => {
    try {
      await registerUser(values);
      toast.success('注册成功!');
      navigate('/login');
    } catch (error) {
      toast.error(error.message);
    }
  };

  return (
    <Form
      form={form}
      onSubmit={handleSubmit}
      layout="vertical"
    >
      <Form.Field name="username" label="用户名" rules={[
        { required: true, message: '用户名不能为空' },
        { minLength: 3, message: '用户名至少3个字符' }
      ]}>
        <Input placeholder="请输入用户名" />
      </Form.Field>

      <Form.Field name="email" label="邮箱地址" rules={[
        { required: true, message: '邮箱不能为空' },
        { type: 'email', message: '请输入有效的邮箱地址' }
      ]}>
        <Input type="email" placeholder="example@domain.com" />
      </Form.Field>

      <Form.Field name="password" label="密码" rules={[
        { required: true, message: '密码不能为空' },
        { minLength: 8, message: '密码至少8个字符' }
      ]}>
        <Input.Password placeholder="请设置密码" showStrength />
      </Form.Field>

      <Form.Field
        name="confirmPassword"
        label="确认密码"
        rules={[
          { required: true, message: '请确认密码' },
          {
            validator: (value) => value === form.getValues('password'),
            message: '两次输入的密码不一致'
          }
        ]}
      >
        <Input.Password placeholder="请再次输入密码" />
      </Form.Field>

      <Form.Field name="gender" label="性别">
        <RadioGroup options={[
          { value: 'male', label: '男' },
          { value: 'female', label: '女' },
          { value: 'other', label: '其他' }
        ]} />
      </Form.Field>

      <Form.Field name="interests" label="兴趣领域">
        <CheckboxGroup options={[
          { value: 'frontend', label: '前端开发' },
          { value: 'backend', label: '后端开发' },
          { value: 'design', label: 'UI/UX设计' },
          { value: 'devops', label: 'DevOps' }
        ]} />
      </Form.Field>

      <Form.Field name="subscribe" valuePropName="checked">
        <Switch>订阅产品更新邮件</Switch>
      </Form.Field>

      <Button type="submit" variant="solid" colorScheme="blue">
        注册
      </Button>
    </Form>
  );
}

对话框和模态框是复杂交互中不可或缺的组件。open-design 提供了多种对话框类型,满足不同的使用场景:

# 对话框组件类型和配置
dialog_config = {
    "alert": {
        "type": "alert",
        "title": "操作提示",
        "description": "确定要执行此操作吗?",
        "confirmText": "确定",
        "cancelText": "取消",
        "variant": "warning"  # info, success, warning, error
    },
    "confirm": {
        "type": "confirm",
        "title": "确认删除",
        "description": "删除后数据将无法恢复,是否继续?",
        "confirmText": "删除",
        "cancelText": "取消",
        "confirmButtonVariant": "solid",
        "confirmButtonColorScheme": "red",
        "isDestructive": True
    },
    "form_dialog": {
        "type": "dialog",
        "title": "编辑用户信息",
        "description": "修改用户的基本信息",
        "size": "md",  # xs, sm, md, lg, xl, full
        "closeOnOverlayClick": False,  # 点击遮罩不关闭
        "closeOnEsc": True,  # ESC 键关闭
        "isCentered": True,  # 垂直居中显示
        "children": "表单组件内容"
    },
    "drawer": {
        "type": "drawer",
        "title": "侧边抽屉",
        "placement": "right",  # left, right, top, bottom
        "size": "md",  # 宽度或高度(取决于 placement)
        "children": "抽屉内容"
    }
}

主题定制与品牌适配

每个产品都有自己独特的品牌视觉需求,open-design 的主题系统提供了灵活的自定义能力,让团队能够基于设计系统构建符合自身品牌的产品界面。

主题配置通过一个集中的配置文件进行管理。这个文件定义了所有可自定义的设计属性,包括颜色、字体、阴影、动画等。修改这个文件后,整个应用的主题会统一更新,无需逐个组件调整。

# 主题配置结构
# 这个文件定义了应用的所有视觉属性

theme_config = {
    # 颜色方案配置
    "colors": {
        # 品牌主色
        "brand": {
            "50": "#F0F9FF",    # 最浅色调
            "100": "#E0F2FE",
            "200": "#BAE6FD",
            "300": "#7DD3FC",
            "400": "#38BDF8",
            "500": "#0EA5E9",   # 主色调
            "600": "#0284C7",
            "700": "#0369A1",
            "800": "#075985",
            "900": "#0C4A6E",   # 最深色调
            "DEFAULT": "brand.500"  # 默认引用
        },
        # 语义颜色
        "semantic": {
            "primary": "brand.500",
            "secondary": "gray.500",
            "success": "green.500",
            "warning": "yellow.500",
            "error": "red.500",
            "info": "blue.500"
        }
    },

    # 字体配置
    "fonts": {
        "heading": {
            "family": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
            "weight": {
                "normal": "400",
                "medium": "500",
                "semibold": "600",
                "bold": "700"
            },
            "sizes": {
                "xs": "12px",
                "sm": "14px",
                "md": "16px",
                "lg": "18px",
                "xl": "20px",
                "2xl": "24px",
                "3xl": "30px",
                "4xl": "36px"
            }
        },
        "body": {
            "family": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
            "weight": {
                "normal": "400",
                "medium": "500"
            },
            "sizes": {
                "xs": "12px",
                "sm": "14px",
                "base": "16px",
                "lg": "18px"
            }
        },
        "mono": {
            "family": "'JetBrains Mono', 'Fira Code', monospace"
        }
    },

    # 间距系统
    "spacing": {
        "px": "1px",
        "0.5": "2px",
        "1": "4px",
        "2": "8px",
        "3": "12px",
        "4": "16px",
        "5": "20px",
        "6": "24px",
        "8": "32px",
        "10": "40px",
        "12": "48px",
        "16": "64px"
    },

    # 圆角配置
    "radii": {
        "none": "0",
        "sm": "2px",
        "md": "4px",
        "lg": "8px",
        "xl": "12px",
        "full": "9999px"
    },

    # 阴影配置
    "shadows": {
        "xs": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
        "sm": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
        "md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
        "lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
        "xl": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
    },

    # 动画配置
    "transitions": {
        "fast": "150ms",
        "normal": "200ms",
        "slow": "300ms",
        "easing": {
            "default": "cubic-bezier(0.4, 0, 0.2, 1)",
            "ease-in": "cubic-bezier(0.4, 0, 1, 1)",
            "ease-out": "cubic-bezier(0, 0, 0.2, 1)",
            "bounce": "cubic-bezier(0.68, -0.55, 0.265, 1.55)"
        }
    }
}

# 品牌适配示例
# 假设我们要为三个不同的产品定制主题

product_themes = {
    "企业后台": {
        "colors": {
            "brand": {
                "500": "#1a56db"  # 深蓝色,体现专业稳重
            }
        },
        "fonts": {
            "body": {
                "family": "'Microsoft YaHei', sans-serif"  # 中文优化
            }
        }
    },
    "社交应用": {
        "colors": {
            "brand": {
                "500": "#f43f5e"  # 玫红色,体现活力时尚
            },
            "semantic": {
                "success": "pink.500"  # 社交场景的成功色
            }
        },
        "radii": {
            "lg": "16px"  # 更圆润的圆角
        }
    },
    "数据看板": {
        "colors": {
            "brand": {
                "500": "#8b5cf6"  # 紫色,体现科技感
            }
        },
        "shadows": {
            "lg": "0 20px 25px -5px rgb(139 92 246 / 0.15)"  # 品牌色阴影
        }
    }
}

在实际应用中,主题配置会通过 open-design 提供的 Provider 组件注入到应用中:

import { ThemeProvider, extendTheme } from '@open-design/core';
import { defaultTheme } from '@open-design/tokens';

// 基于默认主题扩展自定义配置
const customTheme = extendTheme(defaultTheme, {
  colors: {
    brand: {
      50: '#F0F9FF',
      100: '#E0F2FE',
      200: '#BAE6FD',
      300: '#7DD3FC',
      400: '#38BDF8',
      500: '#0EA5E9',  // 你的品牌主色
      600: '#0284C7',
      700: '#0369A1',
      800: '#075985',
      900: '#0C4A6E'
    }
  },
  fonts: {
    heading: {
      family: "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif"
    },
    body: {
      family: "'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif"
    }
  },
  radii: {
    md: '8px',
    lg: '12px'
  }
});

function App() {
  return (
    <ThemeProvider theme={customTheme}>
      <YourApp />
    </ThemeProvider>
  );
}

// 多主题切换支持
function MultiThemeApp() {
  const [currentTheme, setCurrentTheme] = React.useState('light');

  const themes = {
    light: extendTheme(defaultTheme, { /* 浅色主题配置 */ }),
    dark: extendTheme(defaultTheme, { /* 深色主题配置 */ }),
    highContrast: extendTheme(defaultTheme, { /* 高对比度主题配置 */ })
  };

  return (
    <ThemeProvider theme={themes[currentTheme]}>
      <ThemeSwitcher onChange={setCurrentTheme} />
      <YourApp />
    </ThemeProvider>
  );
}

深色模式是现代应用常见的需求。open-design 的主题系统原生支持深色模式的配置和切换:

// 深色主题配置
const darkTheme = extendTheme(defaultTheme, {
  config: {
    colorMode: 'dark'  // 启用深色模式
  },
  colors: {
    // 覆盖不适合深色模式的颜色
    gray: {
      50: '#111827',
      100: '#1F2937',
      200: '#374151',
      300: '#4B5563',
      400: '#6B7280',
      500: '#9CA3AF',
      600: '#D1D5DB',
      700: '#E5E7EB',
      800: '#F3F4F6',
      900: '#F9FAFB'
    }
  }
});

// 自动检测系统偏好
function AutoThemeApp() {
  return (
    <ThemeProvider
      theme={defaultTheme}
      config={{
        initialColorMode: 'system',  // 'light' | 'dark' | 'system'
        respectSSRLocalStorage: true
      }}
    >
      <YourApp />
    </ThemeProvider>
  );
}

完整项目实战教程

现在让我们通过一个完整的项目实例,将前面学到的知识综合起来。这个实战项目是一个「任务管理应用」,涵盖了组件使用、表单处理、主题定制等核心场景。

首先,初始化项目并安装必要的依赖:

# 创建新项目
npm create @open-design/app@latest task-manager
cd task-manager

# 安装额外依赖
npm install @open-design/icons date-fns zustand

# 启动开发服务器
npm run dev

接下来创建项目的目录结构和核心文件。整个应用采用功能模块化的组织方式,每个功能模块包含组件、样式和逻辑:

# 项目结构说明
# 展示完整的项目目录布局

project_structure = """
task-manager/
├── public/
│   └── favicon.ico
├── src/
│   ├── components/           # 通用组件
│   │   ├── layout/          # 布局组件
│   │   │   ├── AppLayout.tsx
│   │   │   ├── Header.tsx
│   │   │   └── Sidebar.tsx
│   │   ├── task/            # 任务相关组件
│   │   │   ├── TaskList.tsx
│   │   │   ├── TaskCard.tsx
│   │   │   ├── TaskForm.tsx
│   │   │   └── TaskFilter.tsx
│   │   └── ui/              # 基础 UI 组件
│   │       ├── EmptyState.tsx
│   │       └── LoadingSpinner.tsx
│   ├── pages/               # 页面组件
│   │   ├── Dashboard.tsx
│   │   ├── Tasks.tsx
│   │   └── Settings.tsx
│   ├── hooks/               # 自定义 Hooks
│   │   ├── useTasks.ts
│   │   ├── useTheme.ts
│   │   └── useLocalStorage.ts
│   ├── store/               # 状态管理
│   │   └── taskStore.ts
│   ├── styles/              # 样式文件
│   │   └── global.css
│   ├── types/               # TypeScript 类型定义
│   │   └── task.ts
│   ├── utils/               # 工具函数
│   │   ├── date.ts
│   │   └── storage.ts
│   ├── theme/               # 主题配置
│   │   ├── index.ts
│   │   ├── colors.ts
│   │   └── components.ts
│   ├── App.tsx
│   └── main.tsx
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
"""

# 类型定义文件
type_definitions = """
// src/types/task.ts

export type TaskPriority = 'low' | 'medium' | 'high';
export type TaskStatus = 'todo' | 'in-progress' | 'done';

export interface Task {
  id: string;
  title: string;
  description: string;
  priority: TaskPriority;
  status: TaskStatus;
  dueDate: string | null;
  tags: string[];
  createdAt: string;
  updatedAt: string;
}

export interface TaskFilter {
  status?: TaskStatus;
  priority?: TaskPriority;
  search?: string;
  tags?: string[];
}

export interface TaskFormData {
  title: string;
  description: string;
  priority: TaskPriority;
  dueDate: string | null;
  tags: string[];
}

export const PRIORITY_CONFIG = {
  low: {
    label: '低',
    colorScheme: 'green',
    icon: 'arrow-down'
  },
  medium: {
    label: '中',
    colorScheme: 'yellow',
    icon: 'minus'
  },
  high: {
    label: '高',
    colorScheme: 'red',
    icon: 'arrow-up'
  }
};

export const STATUS_CONFIG = {
  'todo': {
    label: '待办',
    colorScheme: 'gray'
  },
  'in-progress': {
    label: '进行中',
    colorScheme: 'blue'
  },
  'done': {
    label: '已完成',
    colorScheme: 'green'
  }
};
"""

状态管理采用 Zustand,这是一个轻量级但功能强大的状态管理库,与 open-design 配合使用非常顺畅:

# 状态管理配置
# 展示任务状态管理的完整配置

task_store_config = {
    "store_name": "taskStore",
    "state_properties": [
        "tasks",        # 任务列表
        "filter",       # 当前筛选条件
        "isLoading",    # 加载状态
        "error"         # 错误信息
    ],
    "computed_properties": [
        "filteredTasks",      # 过滤后的任务列表
        "taskCount",          # 各状态的任务数量
        "overdueTasks"        # 逾期任务
    ],
    "actions": [
        "addTask",            # 添加任务
        "updateTask",         # 更新任务
        "deleteTask",         # 删除任务
        "toggleTaskStatus",   # 切换任务状态
        "setFilter",          # 设置筛选条件
        "clearFilter"         # 清除筛选
    ]
}
// src/store/taskStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Task, TaskFilter, TaskFormData, TaskStatus } from '../types/task';
import { generateId } from '../utils/helpers';

interface TaskState {
  // 状态
  tasks: Task[];
  filter: TaskFilter;
  isLoading: boolean;
  error: string | null;

  // 计算属性
  filteredTasks: () => Task[];
  taskCount: () => Record<TaskStatus, number>;
  overdueTasks: () => Task[];

  // 操作方法
  addTask: (data: TaskFormData) => void;
  updateTask: (id: string, data: Partial<TaskFormData>) => void;
  deleteTask: (id: string) => void;
  toggleTaskStatus: (id: string) => void;
  setFilter: (filter: TaskFilter) => void;
  clearFilter: () => void;
}

export const useTaskStore = create<TaskState>()(
  persist(
    (set, get) => ({
      tasks: [],
      filter: {},
      isLoading: false,
      error: null,

      // 过滤后的任务列表
      filteredTasks: () => {
        const { tasks, filter } = get();
        return tasks.filter(task => {
          // 按状态过滤
          if (filter.status && task.status !== filter.status) {
            return false;
          }
          // 按优先级过滤
          if (filter.priority && task.priority !== filter.priority) {
            return false;
          }
          // 按搜索词过滤
          if (filter.search) {
            const searchLower = filter.search.toLowerCase();
            const matchesTitle = task.title.toLowerCase().includes(searchLower);
            const matchesDesc = task.description.toLowerCase().includes(searchLower);
            if (!matchesTitle && !matchesDesc) {
              return false;
            }
          }
          // 按标签过滤
          if (filter.tags && filter.tags.length > 0) {
            const hasMatchingTag = task.tags.some(tag => filter.tags!.includes(tag));
            if (!hasMatchingTag) {
              return false;
            }
          }
          return true;
        });
      },

      // 任务统计
      taskCount: () => {
        const { tasks } = get();
        return {
          'todo': tasks.filter(t => t.status === 'todo').length,
          'in-progress': tasks.filter(t => t.status === 'in-progress').length,
          'done': tasks.filter(t => t.status === 'done').length
        };
      },

      // 逾期任务
      overdueTasks: () => {
        const { tasks } = get();
        const today = new Date();
        today.setHours(0, 0, 0, 0);

        return tasks.filter(task => {
          if (!task.dueDate || task.status === 'done') {
            return false;
          }
          const dueDate = new Date(task.dueDate);
          return dueDate < today;
        });
      },

      // 添加任务
      addTask: (data) => {
        const now = new Date().toISOString();
        const newTask: Task = {
          id: generateId(),
          ...data,
          status: 'todo',
          createdAt: now,
          updatedAt: now
        };

        set(state => ({
          tasks: [...state.tasks, newTask]
        }));
      },

      // 更新任务
      updateTask: (id, data) => {
        set(state => ({
          tasks: state.tasks.map(task =>
            task.id === id
              ? { ...task, ...data, updatedAt: new Date().toISOString() }
              : task
          )
        }));
      },

      // 删除任务
      deleteTask: (id) => {
        set(state => ({
          tasks: state.tasks.filter(task => task.id !== id)
        }));
      },

      // 切换任务状态
      toggleTaskStatus: (id) => {
        set(state => ({
          tasks: state.tasks.map(task => {
            if (task.id !== id) return task;

            const statusFlow: TaskStatus[] = ['todo', 'in-progress', 'done'];
            const currentIndex = statusFlow.indexOf(task.status);
            const nextStatus = statusFlow[(currentIndex + 1) % 3];

            return {
              ...task,
              status: nextStatus,
              updatedAt: new Date().toISOString()
            };
          })
        }));
      },

      // 设置筛选
      setFilter: (filter) => {
        set({ filter });
      },

      // 清除筛选
      clearFilter: () => {
        set({ filter: {} });
      }
    }),
    {
      name: 'task-storage',  // localStorage 的键名
      partialize: (state) => ({ tasks: state.tasks })  // 只持久化任务列表
    }
  )
);

任务卡片组件是展示任务信息的主要单元,它需要展示任务的标题、描述、优先级、截止日期等关键信息:

// src/components/task/TaskCard.tsx
import { Box, Text, Badge, IconButton, HStack, VStack } from '@open-design/components';
import { PRIORITY_CONFIG, STATUS_CONFIG, Task } from '../../types/task';
import { format, isPast, isToday } from 'date-fns';
import { zhCN } from 'date-fns/locale';

interface TaskCardProps {
  task: Task;
  onEdit: (task: Task) => void;
  onDelete: (id: string) => void;
  onToggleStatus: (id: string) => void;
}

export function TaskCard({ task, onEdit, onDelete, onToggleStatus }: TaskCardProps) {
  const priorityConfig = PRIORITY_CONFIG[task.priority];
  const statusConfig = STATUS_CONFIG[task.status];
  const isOverdue = task.dueDate && isPast(new Date(task.dueDate)) && task.status !== 'done';
  const isDueToday = task.dueDate && isToday(new Date(task.dueDate));

  return (
    <Box
      bg="white"
      borderRadius="lg"
      borderWidth="1px"
      borderColor={isOverdue ? 'red.200' : 'gray.200'}
      p={4}
      _hover={{
        shadow: 'md',
        borderColor: 'blue.200',
        transform: 'translateY(-2px)',
        transition: 'all 0.2s'
      }}
      transition="all 0.2s"
      cursor="pointer"
      onClick={() => onEdit(task)}
    >
      <VStack align="stretch" spacing={3}>
        {/* 标题行 */}
        <HStack justify="space-between" align="start">
          <Text
            fontWeight="semibold"
            fontSize="md"
            color={task.status === 'done' ? 'gray.400' : 'gray.800'}
            textDecoration={task.status === 'done' ? 'line-through' : 'none'}
          >
            {task.title}
          </Text>

          <Badge
            colorScheme={priorityConfig.colorScheme}
            variant="subtle"
            fontSize="xs"
          >
            {priorityConfig.label}
          </Badge>
        </HStack>

        {/* 描述 */}
        {task.description && (
          <Text fontSize="sm" color="gray.600" noOfLines={2}>
            {task.description}
          </Text>
        )}

        {/* 标签 */}
        {task.tags.length > 0 && (
          <HStack spacing={2} flexWrap="wrap">
            {task.tags.map(tag => (
              <Badge key={tag} variant="outline" colorScheme="gray" fontSize="xs">
                {tag}
              </Badge>
            ))}
          </HStack>
        )}

        {/* 底部信息栏 */}
        <HStack justify="space-between" align="center" pt={2}>
          {/* 截止日期 */}
          {task.dueDate && (
            <HStack spacing={1}>
              <Badge
                colorScheme={
                  isOverdue ? 'red' :
                  isDueToday ? 'yellow' :
                  'gray'
                }
                variant="subtle"
                fontSize="xs"
              >
                {isOverdue && '⚠️ '}
                {format(new Date(task.dueDate), 'MM月dd日', { locale: zhCN })}
              </Badge>
            </HStack>
          )}

          {/* 状态和操作 */}
          <HStack spacing={1} ml="auto">
            <IconButton
              aria-label="切换状态"
              icon={getStatusIcon(task.status)}
              size="sm"
              variant="ghost"
              colorScheme={statusConfig.colorScheme}
              onClick={(e) => {
                e.stopPropagation();
                onToggleStatus(task.id);
              }}
            />

            <IconButton
              aria-label="删除任务"
              icon={<DeleteIcon />}
              size="sm"
              variant="ghost"
              colorScheme="red"
              onClick={(e) => {
                e.stopPropagation();
                onDelete(task.id);
              }}
            />
          </HStack>
        </HStack>
      </VStack>
    </Box>
  );
}

// 状态图标组件
function getStatusIcon(status: TaskStatus) {
  switch (status) {
    case 'todo':
      return <CircleIcon />;
    case 'in-progress':
      return <PlayIcon />;
    case 'done':
      return <CheckCircleIcon />;
  }
}

// 图标组件(简化版)
function CircleIcon() { return <Box as="span"></Box>; }
function PlayIcon() { return <Box as="span"></Box>; }
function CheckCircleIcon() { return <Box as="span"></Box>; }
function DeleteIcon() { return <Box as="span">🗑</Box>; }

任务表单组件用于创建和编辑任务,它整合了前面介绍的所有表单控件:

// src/components/task/TaskForm.tsx
import { useEffect } from 'react';
import {
  Modal,
  ModalOverlay,
  ModalContent,
  ModalHeader,
  ModalBody,
  ModalFooter,
  ModalCloseButton,
  Form,
  FormField,
  Input,
  Textarea,
  Select,
  Button,
  VStack,
  HStack,
  useToast
} from '@open-design/components';
import { useForm } from '@open-design/hooks';
import { Task, TaskFormData, TaskPriority, PRIORITY_CONFIG } from '../../types/task';

interface TaskFormProps {
  isOpen: boolean;
  onClose: () => void;
  task?: Task | null;
  onSubmit: (data: TaskFormData) => void;
}

export function TaskForm({ isOpen, onClose, task, onSubmit }: TaskFormProps) {
  const toast = useToast();
  const isEditing = !!task;

  const form = useForm<TaskFormData>({
    defaultValues: {
      title: '',
      description: '',
      priority: 'medium',
      dueDate: null,
      tags: []
    }
  });

  // 编辑时填充表单
  useEffect(() => {
    if (task) {
      form.reset({
        title: task.title,
        description: task.description,
        priority: task.priority,
        dueDate: task.dueDate,
        tags: task.tags
      });
    } else {
      form.reset({
        title: '',
        description: '',
        priority: 'medium',
        dueDate: null,
        tags: []
      });
    }
  }, [task, form]);

  const handleSubmit = async (values: TaskFormData) => {
    try {
      onSubmit(values);
      toast({
        title: isEditing ? '任务已更新' : '任务已创建',
        status: 'success',
        duration: 2000
      });
      onClose();
    } catch (error) {
      toast({
        title: '操作失败',
        description: error instanceof Error ? error.message : '请重试',
        status: 'error',
        duration: 3000
      });
    }
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered>
      <ModalOverlay />
      <ModalContent>
        <ModalHeader>{isEditing ? '编辑任务' : '新建任务'}</ModalHeader>
        <ModalCloseButton />

        <form onSubmit={form.handleSubmit(handleSubmit)}>
          <ModalBody>
            <VStack spacing={4} align="stretch">
              {/* 标题 */}
              <FormField
                name="title"
                label="任务标题"
                isRequired
                rules={{
                  required: '请输入任务标题',
                  maxLength: {
                    value: 100,
                    message: '标题不能超过100个字符'
                  }
                }}
              >
                <Input
                  placeholder="请输入任务标题"
                  focusBorderColor="blue.500"
                />
              </FormField>

              {/* 描述 */}
              <FormField
                name="description"
                label="任务描述"
                rules={{
                  maxLength: {
                    value: 500,
                    message: '描述不能超过500个字符'
                  }
                }}
              >
                <Textarea
                  placeholder="详细描述任务内容..."
                  rows={4}
                  focusBorderColor="blue.500"
                />
              </FormField>

              {/* 优先级和截止日期 */}
              <HStack spacing={4} align="start">
                <FormField
                  name="priority"
                  label="优先级"
                  rules={{ required: '请选择优先级' }}
                >
                  <Select>
                    {(Object.entries(PRIORITY_CONFIG) as [TaskPriority, typeof PRIORITY_CONFIG.medium][]).map(
                      ([key, config]) => (
                        <option key={key} value={key}>
                          {config.label}
                        </option>
                      )
                    )}
                  </Select>
                </FormField>

                <FormField
                  name="dueDate"
                  label="截止日期"
                >
                  <Input type="date" focusBorderColor="blue.500" />
                </FormField>
              </HStack>

              {/* 标签 */}
              <FormField
                name="tags"
                label="标签"
              >
                <TagInput
                  placeholder="输入标签后按回车添加"
                  defaultValues={form.getValues('tags')}
                  onChange={(tags) => form.setValue('tags', tags)}
                />
              </FormField>
            </VStack>
          </ModalBody>

          <ModalFooter>
            <HStack spacing={3}>
              <Button variant="ghost" onClick={onClose}>
                取消
              </Button>
              <Button
                type="submit"
                variant="solid"
                colorScheme="blue"
                isLoading={form.formState.isSubmitting}
              >
                {isEditing ? '保存修改' : '创建任务'}
              </Button>
            </HStack>
          </ModalFooter>
        </form>
      </ModalContent>
    </Modal>
  );
}

// 标签输入组件
function TagInput({
  placeholder,
  defaultValues = [],
  onChange
}: {
  placeholder: string;
  defaultValues?: string[];
  onChange: (tags: string[]) => void;
}) {
  const [inputValue, setInputValue] = React.useState('');

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && inputValue.trim()) {
      e.preventDefault();
      const newTag = inputValue.trim();
      if (!defaultValues.includes(newTag)) {
        onChange([...defaultValues, newTag]);
      }
      setInputValue('');
    }
  };

  const removeTag = (tagToRemove: string) => {
    onChange(defaultValues.filter(tag => tag !== tagToRemove));
  };

  return (
    <VStack align="stretch" spacing={2}>
      <Input
        placeholder={placeholder}
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyDown={handleKeyDown}
        focusBorderColor="blue.500"
      />
      {defaultValues.length > 0 && (
        <HStack spacing={2} flexWrap="wrap">
          {defaultValues.map(tag => (
            <Badge
              key={tag}
              variant="subtle"
              colorScheme="blue"
              px={2}
              py={1}
              cursor="pointer"
              onClick={() => removeTag(tag)}
              _hover={{ opacity: 0.7 }}
            >
              {tag} ×
            </Badge>
          ))}
        </HStack>
      )}
    </VStack>
  );
}

任务列表页面整合了所有的子组件,提供完整的任务管理功能:

// src/pages/Tasks.tsx
import { useState } from 'react';
import {
  Box,
  Container,
  Heading,
  HStack,
  VStack,
  Button,
  Input,
  InputGroup,
  InputLeftElement,
  Select,
  SimpleGrid,
  Stat,
  StatLabel,
  StatNumber,
  StatHelpText,
  useDisclosure,
  AlertDialog,
  AlertDialogOverlay,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogBody,
  AlertDialogFooter,
  useToast
} from '@open-design/components';
import { useRef } from 'react';
import { useTaskStore } from '../store/taskStore';
import { TaskCard } from '../components/task/TaskCard';
import { TaskForm } from '../components/task/TaskForm';
import { Task, TaskFormData, TaskStatus, STATUS_CONFIG } from '../types/task';
import { SearchIcon, PlusIcon } from '../components/icons';

export function TasksPage() {
  const toast = useToast();
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);
  const [taskToDelete, setTaskToDelete] = useState<string | null>(null);

  const {
    filteredTasks,
    taskCount,
    overdueTasks,
    filter,
    addTask,
    updateTask,
    deleteTask,
    toggleTaskStatus,
    setFilter,
    clearFilter
  } = useTaskStore();

  const tasks = filteredTasks();
  const counts = taskCount();
  const overdue = overdueTasks();

  const {
    isOpen: isFormOpen,
    onOpen: onFormOpen,
    onClose: onFormClose
  } = useDisclosure();

  const cancelRef = useRef<HTMLButtonElement>(null);

  // 创建新任务
  const handleCreateTask = (data: TaskFormData) => {
    addTask(data);
  };

  // 编辑任务
  const handleEditTask = (task: Task) => {
    setSelectedTask(task);
    onFormOpen();
  };

  // 保存编辑
  const handleSaveTask = (data: TaskFormData) => {
    if (selectedTask) {
      updateTask(selectedTask.id, data);
    }
    setSelectedTask(null);
  };

  // 确认删除
  const handleDeleteConfirm = () => {
    if (taskToDelete) {
      deleteTask(taskToDelete);
      toast({
        title: '任务已删除',
        status: 'info',
        duration: 2000
      });
    }
    setTaskToDelete(null);
  };

  // 关闭表单
  const handleFormClose = () => {
    setSelectedTask(null);
    onFormClose();
  };

  return (
    <Container maxW="container.xl" py={6}>
      <VStack spacing={6} align="stretch">
        {/* 页面标题 */}
        <HStack justify="space-between" align="center">
          <Heading size="lg">任务管理</Heading>
          <Button
            leftIcon={<PlusIcon />}
            colorScheme="blue"
            onClick={() => {
              setSelectedTask(null);
              onFormOpen();
            }}
          >
            新建任务
          </Button>
        </HStack>

        {/* 统计卡片 */}
        <SimpleGrid columns={{ base: 1, md: 4 }} spacing={4}>
          <StatCard
            label="待办"
            value={counts.todo}
            colorScheme="gray"
          />
          <StatCard
            label="进行中"
            value={counts['in-progress']}
            colorScheme="blue"
          />
          <StatCard
            label="已完成"
            value={counts.done}
            colorScheme="green"
          />
          <StatCard
            label="逾期"
            value={overdue.length}
            colorScheme="red"
            helpText={overdue.length > 0 ? '需要尽快处理' : '没有逾期任务'}
          />
        </SimpleGrid>

        {/* 筛选栏 */}
        <HStack
          spacing={4}
          p={4}
          bg="gray.50"
          borderRadius="lg"
          flexWrap="wrap"
        >
          <InputGroup maxW="300px">
            <InputLeftElement pointerEvents="none">
              <SearchIcon color="gray.400" />
            </InputLeftElement>
            <Input
              placeholder="搜索任务..."
              bg="white"
              value={filter.search || ''}
              onChange={(e) => setFilter({ ...filter, search: e.target.value })}
            />
          </InputGroup>

          <Select
            placeholder="全部状态"
            maxW="150px"
            bg="white"
            value={filter.status || ''}
            onChange={(e) => setFilter({
              ...filter,
              status: e.target.value as TaskStatus || undefined
            })}
          >
            {(Object.entries(STATUS_CONFIG) as [TaskStatus, typeof STATUS_CONFIG.todo][]).map(
              ([key, config]) => (
                <option key={key} value={key}>
                  {config.label}
                </option>
              )
            )}
          </Select>

          {(filter.status || filter.search) && (
            <Button
              variant="ghost"
              size="sm"
              onClick={clearFilter}
            >
              清除筛选
            </Button>
          )}
        </HStack>

        {/* 任务列表 */}
        {tasks.length > 0 ? (
          <SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
            {tasks.map(task => (
              <TaskCard
                key={task.id}
                task={task}
                onEdit={handleEditTask}
                onDelete={(id) => setTaskToDelete(id)}
                onToggleStatus={toggleTaskStatus}
              />
            ))}
          </SimpleGrid>
        ) : (
          <EmptyState
            title="暂无任务"
            description="点击上方按钮创建你的第一个任务"
          />
        )}

        {/* 任务表单弹窗 */}
        <TaskForm
          isOpen={isFormOpen}
          onClose={handleFormClose}
          task={selectedTask}
          onSubmit={selectedTask ? handleSaveTask : handleCreateTask}
        />

        {/* 删除确认弹窗 */}
        <AlertDialog
          isOpen={!!taskToDelete}
          leastDestructiveRef={cancelRef}
          onClose={() => setTaskToDelete(null)}
          isCentered
        >
          <AlertDialogOverlay>
            <AlertDialogContent>
              <AlertDialogHeader fontSize="lg" fontWeight="bold">
                删除任务
              </AlertDialogHeader>

              <AlertDialogBody>
                确定要删除这个任务吗此操作无法撤销
              </AlertDialogBody>

              <AlertDialogFooter>
                <Button ref={cancelRef} onClick={() => setTaskToDelete(null)}>
                  取消
                </Button>
                <Button colorScheme="red" onClick={handleDeleteConfirm} ml={3}>
                  删除
                </Button>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialogOverlay>
        </AlertDialog>
      </VStack>
    </Container>
  );
}

// 统计卡片组件
function StatCard({
  label,
  value,
  colorScheme,
  helpText
}: {
  label: string;
  value: number;
  colorScheme: string;
  helpText?: string;
}) {
  return (
    <Box bg="white" p={4} borderRadius="lg" shadow="sm">
      <Stat>
        <StatLabel color="gray.500">{label}</StatLabel>
        <StatNumber color={`${colorScheme}.500`} fontSize="3xl">
          {value}
        </StatNumber>
        {helpText && (
          <StatHelpText mb={0}>{helpText}</StatHelpText>
        )}
      </Stat>
    </Box>
  );
}

// 空状态组件
function EmptyState({
  title,
  description
}: {
  title: string;
  description: string;
}) {
  return (
    <VStack
      py={16}
      spacing={4}
      color="gray.400"
    >
      <Box fontSize="4xl">📋</Box>
      <Heading size="md">{title}</Heading>
      <Text>{description}</Text>
    </VStack>
  );
}

这个实战项目涵盖了 open-design 的核心用法,包括组件的使用、表单处理、状态管理、主题定制等。通过这个例子,你应该对如何在实际项目中使用 open-design 有了清晰的认识。


常见使用场景与最佳实践

在实际开发中,open-design 的组件和工具可以组合使用来解决各种复杂的产品需求。以下是几个典型场景的最佳实践总结。

数据展示与可视化是企业级应用中常见的场景。open-design 提供了 Table 组件,支持排序、筛选、分页等高级功能:

# 数据表格使用配置
# 展示一个典型的产品列表表格配置

table_config = {
    "columns": [
        {
            "key": "id",
            "header": "ID",
            "width": "80px",
            "sortable": True,
            "hidden": True  # 隐藏列,仅用于内部标识
        },
        {
            "key": "name",
            "header": "产品名称",
            "width": "200px",
            "sortable": True,
            "filterable": True
        },
        {
            "key": "category",
            "header": "分类",
            "width": "120px",
            "render": "badge"  # 使用 Badge 组件渲染
        },
        {
            "key": "price",
            "header": "价格",
            "width": "100px",
            "align": "right",
            "sortable": True,
            "render": "currency"  # 货币格式化
        },
        {
            "key": "stock",
            "header": "库存",
            "width": "100px",
            "align": "right",
            "render": "stock-indicator"  # 库存指示器
        },
        {
            "key": "status",
            "header": "状态",
            "width": "100px",
            "render": "status-badge"
        },
        {
            "key": "actions",
            "header": "操作",
            "width": "150px",
            "align": "center",
            "render": "action-buttons"
        }
    ],
    "features": {
        "sorting": {
            "enabled": True,
            "mode": "server"  # server 或 client
        },
        "filtering": {
            "enabled": True,
            "mode": "server"
        },
        "pagination": {
            "enabled": True,
            "pageSize": 20,
            "pageSizeOptions": [10, 20, 50, 100]
        },
        "selection": {
            "enabled": True,
            "mode": "multiple"  # single 或 multiple
        },
        "export": {
            "enabled": True,
            "formats": ["csv", "excel", "pdf"]
        }
    }
}

# 排序配置
sorting_config = {
    "mode": "server",
    "columns": {
        "name": {
            "key": "name",
            "direction": "asc"  # asc 或 desc
        },
        "price": {
            "key": "price",
            "direction": "desc"
        }
    }
}

# 筛选配置
filtering_config = {
    "mode": "server",
    "filters": {
        "category": ["电子产品", "服装"],
        "status": ["active"],
        "priceRange": {
            "min": 100,
            "max": 1000
        },
        "search": "手机"
    }
}

导航系统是应用架构的重要组成部分。open-design 的导航组件支持多级菜单、面包屑、标签页等模式:

import {
  Box,
  Flex,
  HStack,
  VStack,
  Link,
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  Tabs,
  TabList,
  Tab,
  Menu,
  MenuButton,
  MenuList,
  MenuItem
} from '@open-design/components';
import { ChevronRightIcon } from '../icons';

// 顶部导航栏
function Header({ user, onLogout }) {
  return (
    <Flex
      as="header"
      align="center"
      justify="space-between"
      px={6}
      py={3}
      bg="white"
      borderBottomWidth="1px"
      borderColor="gray.200"
    >
      <HStack spacing={8}>
        <Link href="/" fontWeight="bold" fontSize="xl">
          <Logo />
        </Link>

        <HStack as="nav" spacing={4} display={{ base: 'none', md: 'flex' }}>
          <NavLink href="/dashboard">概览</NavLink>
          <NavLink href="/tasks" isActive>任务</NavLink>
          <NavLink href="/projects">项目</NavLink>
          <NavLink href="/settings">设置</NavLink>
        </HStack>
      </HStack>

      <HStack spacing={4}>
        <NotificationBell />

        <Menu>
          <MenuButton>
            <Avatar size="sm" name={user.name} src={user.avatar} />
          </MenuButton>
          <MenuList>
            <MenuItem href="/profile">个人资料</MenuItem>
            <MenuItem href="/settings">账号设置</MenuItem>
            <MenuDivider />
            <MenuItem onClick={onLogout}>退出登录</MenuItem>
          </MenuList>
        </Menu>
      </HStack>
    </Flex>
  );
}

// 面包屑导航
function ProductBreadcrumb({ product, category }) {
  return (
    <Breadcrumb
      spacing={2}
      separator={<ChevronRightIcon color="gray.400" />}
      fontSize="sm"
    >
      <BreadcrumbItem>
        <BreadcrumbLink href="/">首页</BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="/products">产品</BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href={`/categories/${category.id}`}>
          {category.name}
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem isCurrentPage>
        <BreadcrumbLink href={`/products/${product.id}`}>
          {product.name}
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
  );
}

// 标签页导航
function ProjectTabs({ activeTab, onTabChange }) {
  const tabs = [
    { id: 'overview', label: '概览' },
    { id: 'tasks', label: '任务' },
    { id: 'members', label: '成员' },
    { id: 'settings', label: '设置' }
  ];

  return (
    <Tabs
      variant="enclosed"
      index={tabs.findIndex(t => t.id === activeTab)}
      onChange={onTabChange}
    >
      <TabList borderBottomWidth="1px" borderColor="gray.200">
        {tabs.map(tab => (
          <Tab
            key={tab.id}
            _selected={{
              color: 'blue.500',
              borderColor: 'blue.500',
              borderBottomColor: 'transparent'
            }}
          >
            {tab.label}
          </Tab>
        ))}
      </TabList>
    </Tabs>
  );
}

响应式设计是现代 Web 应用的基本要求。open-design 的组件都内置了响应式支持,但了解如何优雅地处理不同屏幕尺寸仍然很重要:

import { Box, Grid, GridItem, Show, Hide } from '@open-design/components';

// 响应式布局示例
function ResponsiveDashboard() {
  return (
    <Grid
      templateAreas={{
        base: `"header"
               "main"
               "footer"`,
        lg: `"header header header"
             "sidebar main aside"
             "footer footer footer"`
      }}
      gridTemplateColumns={{
        base: '1fr',
        lg: '240px 1fr 200px'
      }}
      gap={4}
    >
      {/* 头部 - 所有尺寸都显示 */}
      <GridItem area="header">
        <Header />
      </GridItem>

      {/* 侧边栏 - 大屏幕显示,小屏幕隐藏 */}
      <GridItem area="sidebar">
        <Show above="lg">
          <Sidebar />
        </Show>
      </GridItem>

      {/* 主内容区 */}
      <GridItem area="main">
        <MainContent />
      </GridItem>

      {/* 右侧栏 - 大屏幕显示 */}
      <GridItem area="aside">
        <Show above="lg">
          <RightSidebar />
        </Show>
      </GridItem>

      {/* 底部 - 所有尺寸都显示 */}
      <GridItem area="footer">
        <Footer />
      </GridItem>
    </Grid>
  );
}

// 响应式显示/隐藏
function ResponsiveComponents() {
  return (
    <VStack spacing={4} align="stretch">
      {/* 手机上显示,平板上隐藏 */}
      <Hide above="md">
        <MobileOnlyBanner>仅在手机上显示</MobileOnlyBanner>
      </Hide>

      {/* 平板及以上显示 */}
      <Show above="md">
        <DesktopBanner>在平板及以上尺寸显示</DesktopBanner>
      </Show>

      {/* 自定义断点 */}
      <Show above="xl">
        <LargeScreenOnly>超大屏幕专属内容</LargeScreenOnly>
      </Show>
    </VStack>
  );
}

可访问性是现代应用不可忽视的方面。open-design 的组件在设计时充分考虑了无障碍访问,但开发者也需要遵循一些最佳实践:

// 可访问性最佳实践

// 1. 语义化 HTML
function SemanticExample() {
  return (
    // 使用 <main> 标记主要内容区域
    <main role="main">
      {/* 使用 <nav> 标记导航区域 */}
      <nav aria-label="主导航">
        <Navigation />
      </nav>

      {/* 使用 <article> 标记文章内容 */}
      <article>
        <h1>文章标题</h1>
        <p>文章内容...</p>
      </article>

      {/* 使用 <aside> 标记侧边栏 */}
      <aside aria-label="相关推荐">
        <RelatedArticles />
      </aside>

      {/* 使用 <footer> 标记页脚 */}
      <footer>
        <FooterContent />
      </footer>
    </main>
  );
}

// 2. 表单标签关联
function AccessibleForm() {
  return (
    <Form>
      {/* 正确:使用 aria-label */}
      <FormField>
        <label id="email-label" htmlFor="email-input">
          邮箱地址
        </label>
        <Input
          id="email-input"
          aria-labelledby="email-label"
          type="email"
          aria-required="true"
        />
      </FormField>

      {/* 错误提示 */}
      <FormField>
        <label id="password-label" htmlFor="password-input">
          密码
        </label>
        <Input
          id="password-input"
          aria-labelledby="password-label"
          type="password"
          aria-describedby="password-error password-hint"
          aria-invalid="true"
        />
        <p id="password-error" style={{ color: 'red' }}>
          密码不能为空
        </p>
        <p id="password-hint" style={{ color: 'gray' }}>
          密码至少8个字符
        </p>
      </FormField>
    </Form>
  );
}

// 3. 键盘导航支持
function AccessibleModal() {
  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <ModalOverlay />
      <ModalContent
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        aria-describedby="modal-description"
      >
        <ModalHeader id="modal-title">确认操作</ModalHeader>
        <ModalCloseButton />
        <ModalBody id="modal-description">
          <Text>确定要执行此操作吗</Text>
        </ModalBody>
        <ModalFooter>
          {/* 确保按钮可以通过 Tab 聚焦 */}
          <Button onClick={onClose}>取消</Button>
          <Button colorScheme="blue" onClick={handleConfirm}>
            确认
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
}

// 4. 颜色对比度
function AccessibleColors() {
  return (
    <Box>
      {/* 正常文本:对比度至少 4.5:1 */}
      <Text color="gray.800" bg="white">
        这是正常的可访问文本
      </Text>

      {/* 大文本:对比度至少 3:1 */}
      <Text fontSize="xl" color="gray.700" bg="white">
        这是大号可访问文本
      </Text>

      {/* 使用语义颜色表示状态 */}
      <Badge colorScheme="red">错误</Badge>
      <Badge colorScheme="green">成功</Badge>
      <Badge colorScheme="yellow">警告</Badge>
      {/* 不要仅依赖颜色传达信息 */}
      <HStack>
        <Badge colorScheme="red">错误</Badge>
        <Icon name="error" aria-hidden="true" />
        <VisuallyHidden>出现错误</VisuallyHidden>
      </HStack>
    </Box>
  );
}

// 5. 屏幕阅读器友好内容
function ScreenReaderContent() {
  return (
    <Box>
      {/* 使用 VisuallyHidden 组件隐藏视觉内容但保留屏幕阅读 */}
      <VisuallyHidden>
        跳转到主要内容
      </VisuallyHidden>
      <Button
        variant="link"
        onClick={scrollToMain}
        style={{
          position: 'absolute',
          left: '-9999px'
        }}
        onFocus={(e) => {
          e.currentTarget.style.position = 'fixed';
          e.currentTarget.style.left = '10px';
          e.currentTarget.style.top = '10px';
        }}
        onBlur={(e) => {
          e.currentTarget.style.position = 'absolute';
          e.currentTarget.style.left = '-9999px';
        }}
      >
        跳转到主要内容
      </Button>

      {/* 加载状态 */}
      <Box role="status" aria-live="polite">
        {isLoading ? (
          <Spinner aria-label="加载中" />
        ) : (
          <Content />
        )}
      </Box>

      {/* 图片替代文本 */}
      <Image
        src="/logo.png"
        alt="公司Logo"
      />

      {/* 装饰性图片 */}
      <Image
        src="/decoration.svg"
        alt=""
        aria-hidden="true"
      />
    </Box>
  );
}

性能优化与生产部署

将基于 open-design 构建的应用部署到生产环境时,需要注意一些性能优化的要点。这些优化不仅能提升用户体验,还能降低服务器成本和带宽消耗。

组件级别的性能优化主要是避免不必要的重新渲染。以下是一些关键的优化模式:

import { memo, useMemo, useCallback } from 'react';
import { Box, Text, Button } from '@open-design/components';

// 1. 使用 React.memo 包装纯展示组件
const TaskCard = memo(function TaskCard({ task, onEdit, onDelete }) {
  // 组件只有在 task, onEdit, onDelete 变化时才会重新渲染
  return (
    <Box borderWidth="1px" p={4} borderRadius="md">
      <Text>{task.title}</Text>
      <HStack>
        <Button onClick={() => onEdit(task.id)}>编辑</Button>
        <Button onClick={() => onDelete(task.id)}>删除</Button>
      </HStack>
    </Box>
  );
});

// 2. 使用 useMemo 缓存计算结果
function TaskList({ tasks, filter }) {
  // 只有当 tasks 或 filter 变化时才重新计算
  const filteredTasks = useMemo(() => {
    console.log('计算过滤任务...');
    return tasks.filter(task => {
      if (filter.status && task.status !== filter.status) {
        return false;
      }
      if (filter.priority && task.priority !== filter.priority) {
        return false;
      }
      return true;
    });
  }, [tasks, filter]);

  // 缓存分组结果
  const groupedTasks = useMemo(() => {
    const groups = {
      todo: [],
      'in-progress': [],
      done: []
    };
    filteredTasks.forEach(task => {
      groups[task.status].push(task);
    });
    return groups;
  }, [filteredTasks]);

  return (
    <VStack>
      {filteredTasks.map(task => (
        <TaskCard key={task.id} task={task} />
      ))}
    </VStack>
  );
}

// 3. 使用 useCallback 稳定函数引用
function ParentComponent() {
  const [count, setCount] = useState(0);

  // 每次渲染都会创建新函数,导致子组件重新渲染
  // const handleClick = () => console.log('clicked');

  // 使用 useCallback 稳定函数引用
  const handleEdit = useCallback((id) => {
    console.log('编辑:', id);
  }, []);

  const handleDelete = useCallback((id) => {
    console.log('删除:', id);
    // 删除逻辑
  }, []);

  const handleStatusChange = useCallback((id, status) => {
    console.log('状态变更:', id, status);
  }, []);

  return (
    <>
      <TaskList onEdit={handleEdit} onDelete={handleDelete} />
      <Stats onStatusChange={handleStatusChange} />
    </>
  );
}

// 4. 列表渲染优化
function OptimizedTaskList({ tasks }) {
  return (
    <VStack spacing={2}>
      {tasks.map((task) => (
        // 使用稳定的 key
        <TaskCard
          key={task.id}
          task={task}
        />
      ))}
    </VStack>
  );
}

// 5. 延迟加载非关键组件
function LazyLoadedPage() {
  return (
    <Box>
      <CriticalContent />

      {/* 使用 React.lazy 延迟加载 */}
      <Suspense fallback={<Spinner />}>
        <lazy(() => import('./HeavyChart')) />
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <lazy(() => import('./CommentSection')) />
      </Suspense>
    </Box>
  );
}

// 6. 图片优化
function OptimizedImages() {
  return (
    <Box>
      {/* 使用 responsive 图片 */}
      <Image
        src="/hero-mobile.jpg"
        srcSet="/hero-mobile.jpg 400w, /hero-tablet.jpg 800w, /hero-desktop.jpg 1200w"
        sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
        loading="lazy"
        alt="Hero image"
      />

      {/* 占位符 */}
      <Image
        src={actualSrc}
        placeholder={blurDataUrl}
        loading="lazy"
      />
    </Box>
  );
}

构建优化主要关注减少包体积和提升加载速度:

// vite.config.js 优化配置
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      // 启用 Babel 的 JSX 运行时转换
      babel: {
        plugins: [
          // 按需引入 open-design 组件
          ['import', {
            libraryName: '@open-design/components',
            libraryDirectory: 'esm/components',
            camel2DashComponentName: false
          }, '@open-design/components']
        ]
      }
    })
  ],
  build: {
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          // 将 open-design 组件库单独打包
          'open-design': ['@open-design/components', '@open-design/core'],
          // 将第三方库单独打包
          'vendor': ['react', 'react-dom', 'zustand']
        }
      }
    },
    // 启用 CSS 代码分割
    cssCodeSplit: true,
    // 启用 gzip 压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,  // 生产环境移除 console
        drop_debugger: true
      }
    },
    // 生成 sourcemap(可选,生产环境可关闭)
    sourcemap: false
  },
  // 优化依赖预构建
  optimizeDeps: {
    include: ['react', 'react-dom', '@open-design/components']
  }
});

Tree Shaking 是现代打包工具的重要特性,open-design 完全支持这一功能。为了最大化 Tree Shaking 的效果,在导入组件时应使用命名导入而非全量导入:

// 推荐的导入方式 - 支持 Tree Shaking
import { Button, Input, Modal } from '@open-design/components';
import { useDisclosure } from '@open-design/hooks';

// 不推荐的导入方式 - 无法 Tree Shaking
import OpenDesign from '@open-design/components';
const { Button } = OpenDesign;

样式优化也是重要的环节。open-design 支持多种样式方案,可以根据项目需求选择最适合的方案:

// 方案一:CSS-in-JS(默认,推荐)
// 所有样式以 JavaScript 对象的形式内联在组件中
import { Box, Button } from '@open-design/components';

// 方案二:传统 CSS
// 组件只输出结构,样式由外部 CSS 文件控制
import { Box, Button } from '@open-design/components/styles-only';

// 生成的 CSS 类名可以通过 Design System Token 定制
// 在 theme config 中添加
const theme = {
  classPrefix: 'myapp',  // 自定义类名前缀
  // ...
};

项目配置与自定义

深入理解 open-design 的配置系统,可以让你更好地定制和扩展这个设计系统。以下是一些高级配置选项和最佳实践。

组件默认属性的全局配置:

# 组件默认配置
# 这个配置文件定义了所有组件的默认行为

component_defaults = {
    "Button": {
        "size": "md",
        "variant": "solid",
        "colorScheme": "blue",
        "borderRadius": "md",
        "fontWeight": "medium",
        "transition": "all 0.2s",
        "_disabled": {
            "opacity": 0.5,
            "cursor": "not-allowed",
            "pointerEvents": "none"
        },
        "_hover": {
            "transform": "translateY(-1px)",
            "shadow": "md"
        }
    },
    "Input": {
        "size": "md",
        "borderRadius": "md",
        "_focus": {
            "borderColor": "blue.500",
            "boxShadow": "0 0 0 1px var(--color-blue-500)"
        },
        "_placeholder": {
            "color": "gray.400"
        }
    },
    "Card": {
        "borderRadius": "lg",
        "shadow": "sm",
        "bg": "white",
        "_hover": {
            "shadow": "md"
        }
    }
}

# 组件变体配置
component_variants = {
    "Button": {
        "variants": {
            "solid": {
                "bg": "blue.500",
                "color": "white",
                "_hover": {
                    "bg": "blue.600"
                }
            },
            "outline": {
                "borderWidth": "1px",
                "borderColor": "blue.500",
                "color": "blue.500",
                "_hover": {
                    "bg": "blue.50"
                }
            },
            "ghost": {
                "color": "blue.500",
                "_hover": {
                    "bg": "blue.50"
                }
            },
            "link": {
                "color": "blue.500",
                "textDecoration": "underline",
                "_hover": {
                    "color": "blue.600"
                }
            }
        },
        "sizes": {
            "xs": {
                "fontSize": "xs",
                "px": 2,
                "py": 1
            },
            "sm": {
                "fontSize": "sm",
                "px": 3,
                "py": 1.5
            },
            "md": {
                "fontSize": "md",
                "px": 4,
                "py": 2
            },
            "lg": {
                "fontSize": "lg",
                "px": 6,
                "py": 3
            }
        }
    }
}

样式重置和全局样式配置:

// src/styles/global.css
// 全局样式重置和基础配置

:root {
  /* 颜色系统 */
  --color-primary: #0EA5E9;
  --color-primary-hover: #0284C7;
  --color-success: #22C55E;
  --color-warning: #F59E0B;
  --color-error: #EF4444;

  /* 间距基准 */
  --spacing-unit: 4px;

  /* 圆角 */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-full: 9999px;

  /* 阴影 */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);

  /* 过渡 */
  --transition-fast: 150ms;
  --transition-normal: 200ms;
  --transition-slow: 300ms;
}

/* 全局样式重置 */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: smooth;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.5;
  color: var(--color-gray-900);
  background-color: var(--color-gray-50);
}

/* 焦点样式 */
:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* 选中样式 */
::selection {
  background-color: var(--color-primary);
  color: white;
}

/* 滚动条样式 */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: var(--color-gray-100);
}

::-webkit-scrollbar-thumb {
  background: var(--color-gray-300);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--color-gray-400);
}

/* 媒体查询辅助类 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

TypeScript 类型扩展允许你为设计系统添加自定义属性:

// src/types/open-design.d.ts
// 扩展 open-design 的类型定义

import '@open-design/components';

declare module '@open-design/components' {
  // 扩展主题类型
  interface ThemeConfig {
    colors: {
      brand: {
        50: string;
        100: string;
        200: string;
        300: string;
        400: string;
        500: string;
        600: string;
        700: string;
        800: string;
        900: string;
      };
      accent: {
        primary: string;
        secondary: string;
      };
    };
  }

  // 扩展组件属性
  interface ButtonProps {
    isLoading?: boolean;
    loadingText?: string;
    leftIcon?: React.ReactNode;
    rightIcon?: React.ReactNode;
  }

  interface InputProps {
    prefix?: React.ReactNode;
    suffix?: React.ReactNode;
  }
}

// 扩展组件变体
declare module '@open-design/system' {
  interface ComponentVariants {
    Button: {
      variants: 'solid' | 'outline' | 'ghost' | 'link' | 'gradient';
      sizes: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
    };
    Card: {
      variants: 'elevated' | 'outlined' | 'filled';
    };
  }
}

自定义组件的开发模式让你可以基于 open-design 的基础组件构建符合业务需求的高级组件:

// src/components/extended/ExtendedButton.tsx
// 基于 Button 构建的业务按钮组件

import { Button, ButtonProps } from '@open-design/components';
import { forwardRef } from 'react';

interface ExtendedButtonProps extends ButtonProps {
  /** 业务按钮类型 */
  businessType?: 'approve' | 'reject' | 'submit' | 'cancel';
  /** 是否显示加载状态文字 */
  showLoadingText?: boolean;
  /** 自定义图标 */
  icon?: React.ReactNode;
}

// 业务按钮颜色映射
const BUSINESS_TYPE_COLORS = {
  approve: 'green',
  reject: 'red',
  submit: 'blue',
  cancel: 'gray'
};

export const ExtendedButton = forwardRef<HTMLButtonElement, ExtendedButtonProps>(
  function ExtendedButton(
    {
      businessType,
      isLoading,
      loadingText,
      children,
      icon,
      ...props
    },
    ref
  ) {
    // 根据业务类型自动设置颜色方案
    const colorScheme = props.colorScheme || 
      (businessType ? BUSINESS_TYPE_COLORS[businessType] : undefined);

    // 根据业务类型设置变体
    const variant = props.variant || 
      (businessType === 'cancel' ? 'outline' : 'solid');

    return (
      <Button
        ref={ref}
        colorScheme={colorScheme}
        variant={variant}
        isLoading={isLoading}
        loadingText={showLoadingText ? loadingText : undefined}
        {...props}
      >
        {icon && <span className="mr-2">{icon}</span>}
        {children}
      </Button>
    );
  }
);

// 使用示例
function BusinessForm() {
  return (
    <div>
      <ExtendedButton
        businessType="submit"
        onClick={handleSubmit}
      >
        提交申请
      </ExtendedButton>

      <ExtendedButton
        businessType="approve"
        isLoading={isApproving}
        loadingText="审批中..."
      >
        审批通过
      </ExtendedButton>

      <ExtendedButton
        businessType="reject"
        icon={<WarningIcon />}
      >
        驳回申请
      </ExtendedButton>

      <ExtendedButton businessType="cancel">
        取消
      </ExtendedButton>
    </div>
  );
}

测试与质量保障

为了确保基于 open-design 构建的组件和功能的质量,编写测试是不可或缺的环节。以下是针对 open-design 组件的测试策略和实践。

# 测试策略概览
# 展示完整的测试方案

test_strategy = {
    "测试层次": {
        "单元测试": {
            "目标": "测试独立的组件和函数",
            "工具": ["Jest", "React Testing Library"],
            "覆盖率目标": "80%+"
        },
        "集成测试": {
            "目标": "测试组件之间的交互",
            "工具": ["Jest", "React Testing Library"],
            "关注点": ["表单提交", "状态管理", "路由跳转"]
        },
        "端到端测试": {
            "目标": "测试完整的用户流程",
            "工具": ["Cypress", "Playwright"],
            "覆盖率目标": "核心流程 100%"
        },
        "视觉回归测试": {
            "目标": "确保 UI 不出现意外的视觉变化",
            "工具": ["Storybook", "Chromatic", "Percy"],
            "触发时机": "每次代码变更时"
        }
    },
    "测试文件组织": {
        "单元测试": "src/components/__tests__/ComponentName.test.tsx",
        "集成测试": "src/pages/__tests__/PageName.test.tsx",
        "E2E测试": "cypress/e2e/**/*.cy.ts",
        "组件文档": "src/components/ComponentName.stories.tsx"
    }
}

单元测试示例:

// src/components/task/TaskCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { TaskCard } from '../TaskCard';
import { Task } from '../../types/task';

// Mock 数据
const mockTask: Task = {
  id: '1',
  title: '完成项目报告',
  description: '整理本周工作内容并提交周报',
  priority: 'high',
  status: 'todo',
  dueDate: '2024-12-31',
  tags: ['工作', '重要'],
  createdAt: '2024-01-01T00:00:00Z',
  updatedAt: '2024-01-01T00:00:00Z'
};

describe('TaskCard', () => {
  // 基本渲染测试
  test('正确渲染任务标题', () => {
    render(
      <TaskCard
        task={mockTask}
        onEdit={jest.fn()}
        onDelete={jest.fn()}
        onToggleStatus={jest.fn()}
      />
    );

    expect(screen.getByText('完成项目报告')).toBeInTheDocument();
  });

  test('正确渲染任务描述', () => {
    render(
      <TaskCard
        task={mockTask}
        onEdit={jest.fn()}
        onDelete={jest.fn()}
        onToggleStatus={jest.fn()}
      />
    );

    expect(screen.getByText('整理本周工作内容并提交周报')).toBeInTheDocument();
  });

  test('正确渲染优先级标签', () => {
    render(
      <TaskCard
        task={mockTask}
        onEdit={jest.fn()}
        onDelete={jest.fn()}
        onToggleStatus={jest.fn()}
      />
    );

    expect(screen.getByText('高')).toBeInTheDocument();
  });

  test('正确渲染标签', () => {
    render(
      <TaskCard
        task={mockTask}
        onEdit={jest.fn()}
        onDelete={jest.fn()}
        onToggleStatus={jest.fn()}
      />
    );

    expect(screen.getByText('工作')).toBeInTheDocument();
    expect(screen.getByText('重要')).toBeInTheDocument();
  });

  test('点击卡片调用 onEdit 回调', () => {
    const onEdit = jest.fn();
    render(
      <TaskCard
        task={mockTask}
        onEdit={onEdit}
        onDelete={jest.fn()}
        onToggleStatus={jest.fn()}
      />
    );

    fireEvent.click(screen.getByText('完成项目报告'));
    expect(onEdit).toHaveBeenCalledWith(mockTask);
  });

  test('点击删除按钮调用 onDelete 回调', () => {
    const onDelete = jest.fn();
    render(
      <TaskCard
        task={mockTask}
        onEdit={jest.fn()}
        onDelete={onDelete}
        onToggleStatus={jest.fn()}
      />
    );

    fireEvent.click(screen.getByLabelText('删除任务'));
    expect(onDelete).toHaveBeenCalledWith(mockTask.id);
  });

  test('点击状态按钮调用 onToggleStatus 回调', () => {
    const onToggleStatus = jest.fn();
    render(
      <TaskCard
        task={mockTask}
        onEdit={jest.fn()}
        onDelete={jest.fn()}
        onToggleStatus={onToggleStatus}
      />
    );

    fireEvent.click(screen.getByLabelText('切换状态'));
    expect(onToggleStatus).toHaveBeenCalledWith(mockTask.id);
  });

  test('已完成任务显示删除线样式', () => {
    const doneTask = { ...mockTask, status: 'done' as const };
    render(
      <TaskCard
        task={doneTask}
        onEdit={jest.fn()}
        onDelete={jest.fn()}
        onToggleStatus={jest.fn()}
      />
    );

    const title = screen.getByText('完成项目报告');
    expect(title).toHaveStyle({ textDecoration: 'line-through' });
  });
});

集成测试示例:

// src/pages/__tests__/TasksPage.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TasksPage } from '../TasksPage';
import { useTaskStore } from '../../store/taskStore';

// Mock 状态管理
jest.mock('../../store/taskStore', () => ({
  useTaskStore: jest.fn()
}));

const mockTasks = [
  {
    id: '1',
    title: '任务一',
    description: '描述一',
    priority: 'high' as const,
    status: 'todo' as const,
    dueDate: null,
    tags: [],
    createdAt: '2024-01-01',
    updatedAt: '2024-01-01'
  },
  {
    id: '2',
    title: '任务二',
    description: '描述二',
    priority: 'low' as const,
    status: 'done' as const,
    dueDate: null,
    tags: ['标签一'],
    createdAt: '2024-01-01',
    updatedAt: '2024-01-01'
  }
];

describe('TasksPage', () => {
  beforeEach(() => {
    (useTaskStore as jest.Mock).mockReturnValue({
      tasks: mockTasks,
      filteredTasks: () => mockTasks,
      taskCount: () => ({ todo: 1, 'in-progress': 0, done: 1 }),
      overdueTasks: () => [],
      filter: {},
      addTask: jest.fn(),
      updateTask: jest.fn(),
      deleteTask: jest.fn(),
      toggleTaskStatus: jest.fn(),
      setFilter: jest.fn(),
      clearFilter: jest.fn()
    });
  });

  test('显示任务统计信息', () => {
    render(<TasksPage />);

    expect(screen.getByText('待办')).toBeInTheDocument();
    expect(screen.getByText('1')).toBeInTheDocument();
    expect(screen.getByText('进行中')).toBeInTheDocument();
    expect(screen.getByText('已完成')).toBeInTheDocument();
  });

  test('点击新建任务按钮打开表单', async () => {
    render(<TasksPage />);

    const addButton = screen.getByText('新建任务');
    fireEvent.click(addButton);

    await waitFor(() => {
      expect(screen.getByText('新建任务')).toBeInTheDocument();
    });
  });

  test('搜索功能正常工作', async () => {
    render(<TasksPage />);

    const searchInput = screen.getByPlaceholderText('搜索任务...');
    await userEvent.type(searchInput, '任务一');

    expect(useTaskStore().setFilter).toHaveBeenCalledWith({
      filter: expect.any(Object),
      search: '任务一'
    });
  });
});

社区参与与持续迭代

open-design 是一个活跃的开源项目,社区的参与对项目的持续发展至关重要。无论你是设计系统的使用者还是贡献者,都有多种方式可以参与到这个项目中来。

报告问题是帮助项目改进的第一步。当你发现组件的 bug、文档的错误或者缺失的功能时,可以在 GitHub 仓库中提交 issue。好的 issue 应该包含以下信息:

  • 清晰的问题描述
  • 复现步骤
  • 期望行为和实际行为
  • 相关的代码片段或链接
  • 你的环境和版本信息

对于 bug 报告,使用项目提供的 issue 模板可以获得更规范的问题描述格式。功能请求则应该清楚地说明这个功能的使用场景和它能解决的问题。

代码贡献是开源社区最宝贵的支持方式。在提交代码之前,建议先阅读项目的贡献指南文档,了解代码规范、提交流程和测试要求。开发新功能或修复 bug 时,应该:

# Fork 仓库到你的账号
# 克隆你 fork 的仓库
git clone https://github.com/YOUR_USERNAME/open-design.git

# 添加上游仓库
git remote add upstream https://github.com/nexu-io/open-design.git

# 创建功能分支
git checkout -b feature/your-feature-name

# 安装开发依赖
npm install

# 运行开发服务器
npm run dev

# 运行测试
npm test

# 确保测试通过后再提交代码
git add .
git commit -m "feat: 添加新功能描述"

# 同步上游最新代码
git fetch upstream
git rebase upstream/main

# 推送分支到你的 fork
git push origin feature/your-feature-name

# 在 GitHub 上创建 Pull Request

文档改进也是重要的贡献方式。如果你发现文档有错误、不清晰或者缺失的部分,可以直接提交文档修改。高质量的文档对于帮助新用户快速上手至关重要。

设计系统的发展离不开设计社区的支持。如果你有设计背景,可以参与设计令牌的审核、设计规范的制定或者新组件的视觉设计。设计资源通常以 Figma 文件的形式共享,可以在设计工具中直接贡献。

参与讨论和帮助其他用户也是很有价值的贡献。在 GitHub Discussions、Discord 服务器或者其他社区渠道中回答问题、分享经验,都能帮助构建更活跃的社区生态。


总结与展望

通过这篇文章,我们全面了解了 nexu-io/open-design 这个开源设计系统的各个方面。从项目的核心理念到具体的使用方法,从基础组件到高级定制,从性能优化到测试策略,再到社区参与,这份教程涵盖了设计系统从入门到进阶的全部内容。

open-design 的核心价值在于它提供了一套完整的设计开发协作解决方案。通过统一的设计令牌机制,设计师和开发者可以在同一套语言下工作,大大减少了沟通成本和视觉不一致的问题。丰富的组件库和灵活的定制能力,使得团队可以快速构建高质量的用户界面。

对于新接触这个项目的开发者,建议从以下方面开始学习:

首先,深入理解设计令牌的概念和作用。设计令牌是整个系统的基石,理解好这个概念可以让你更好地使用和维护设计系统。

其次,熟悉核心组件的使用方法。按钮、表单、对话框、导航等基础组件的使用频率最高,掌握这些组件的属性和最佳实践可以让开发效率大幅提升。

再次,学习主题定制的能力。每个产品都有自己的品牌特色,通过主题系统定制出符合自己品牌的设计系统,是将 open-design 真正融入项目的关键。

最后,关注性能优化和质量保障。即使有了优秀的组件库,高质量的代码和充分的测试仍然是保证产品质量的基础。

展望未来,设计系统的发展趋势可能包括以下几个方面:

  • 更多的跨平台支持,包括移动端和桌面应用
  • 与 AI 工具的深度集成,提高设计和开发的自动化程度
  • 更加智能的主题生成和适配能力
  • 更强的无障碍访问支持
  • 与设计工具(如 Figma)的实时同步能力

nexu-io/open-design 作为开源社区的一个重要项目,正在朝着这些方向不断发展。期待更多的开发者和设计师加入到这个项目中来,共同推动设计系统生态的进步。


相关资源链接

官方资源

  • GitHub 仓库:https://github.com/nexu-io/open-design
  • 官方文档:https://open-design.nexu.io/docs
  • 组件库演示:https://open-design.nexu.io/components
  • 设计令牌文档:https://open-design.nexu.io/tokens
  • 更新日志:https://github.com/nexu-io/open-design/releases

社区资源

  • Discord 社区:https://discord.gg/open-design
  • GitHub Discussions:https://github.com/nexu-io/open-design/discussions
  • Stack Overflow 标签:https://stackoverflow.com/questions/tagged/open-design

相关开源项目

  • Radix UI:无头组件库,与 open-design 可以互补使用
  • Chakra UI:另一个流行的 React 设计系统,可以参考其设计思路
  • Tailwind CSS:实用优先的 CSS 框架,与 open-design 可以集成使用
  • Storybook:组件文档和开发环境,是展示 open-design 组件的好工具
  • Chromatic:视觉回归测试工具,帮助保持 UI 的一致性

学习资源

  • 设计系统基础:学习设计系统的核心概念和最佳实践
  • React 官方文档:深入学习 React 的组件化和Hooks机制
  • TypeScript 官方文档:掌握类型安全对大型项目的重要性
  • 无障碍访问指南:MDN Web Docs 的无障碍文档

希望这篇教程能帮助你更好地理解和使用 open-design。如果你有任何问题或建议,欢迎在评论区交流讨论。祝你的项目开发顺利!

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注