8 UI 组件库与设计系统:Token 驱动 + 暗色模式 + 4 种主题色

一个人的项目也需要设计系统------不然 UI 会越改越乱。

组件层次

scss 复制代码
┌─────────────────────────────────────────────┐

│  页面组件 (app/*.tsx)                        │

│  Dashboard / Accounts / Assets / Settings    │

├─────────────────────────────────────────────┤

│  复合组件 (src/components/)                  │

│  InteractiveTrendChart / HoldingCostBreakdown│

│  AddAssetModal / SettlementModal / LockScreen│

│  NetWorthExplainer / TimeRangeSheet          │

├─────────────────────────────────────────────┤

│  基础组件 (src/components/ui/)               │

│  AppButton / AppCard / AppChip / AppTextInput│

│  Icon / ListItem / FAB / EmptyState / Toast  │

│  BottomSheet / DatePickerField               │

├─────────────────────────────────────────────┤

│  设计 Token (src/theme/)                     │

│  colors.ts / tokens.ts / typography.ts       │

│  icons.ts / ThemeProvider.tsx                │

└─────────────────────────────────────────────┘

分层原则

  • 基础组件 ui/ 只封装 Paper 组件 + 项目默认样式,不含业务逻辑

  • 复合组件组合基础组件,处理特定场景的交互

  • 页面组件编排复合组件,连接 Store 数据

设计 Token

间距(4 的倍数)

typescript 复制代码
// src/theme/tokens.ts

export const spacing = {

  xs: 4,

  sm: 8,

  md: 16,

  lg: 24,

  xl: 32,

  xxl: 48,

} as const;

所有组件的 marginpaddinggap 都用这些值,不允许写硬编码数字。

圆角

typescript 复制代码
export const radius = {

  sm: 8,

  md: 12,

  lg: 16,

  xl: 20,

  full: 9999,

} as const;

阴影

typescript 复制代码
export const shadows = {

  none: { elevation: 0 },

  sm: { elevation: 1 },

  md: { elevation: 3 },

  lg: { elevation: 6 },

  xl: { elevation: 10 },

} as const;

语义色(非主题色)

typescript 复制代码
export const semanticColors = {

  positive: '#00B894',  // 收益、增长 → 绿色

  negative: '#EA3943',  // 亏损、下降 → 红色

  neutral: '#636E72',   // 中性信息 → 灰色

} as const;

这些颜色不随主题色切换而变化------涨永远是绿色,跌永远是红色。

颜色系统:4 种主题色 × 明暗模式

用户可以选 4 种主题色:紫色、蓝色、绿色、橙色。每种在明暗模式下有不同的色值:

typescript 复制代码
// src/theme/colors.ts

const THEME_PRIMARIES = {

  purple: { primary: '#6C5CE7', primaryLight: '#A29BFE', primaryContainer: '#E8E5FD' },

  blue:   { primary: '#0984E3', primaryLight: '#74B9FF', primaryContainer: '#D6ECFA' },

  green:  { primary: '#00B894', primaryLight: '#55EFC4', primaryContainer: '#D0F5EC' },

  orange: { primary: '#E17055', primaryLight: '#FAB1A0', primaryContainer: '#FDE8E2' },

};

  


const THEME_PRIMARIES_DARK = {

  purple: { primary: '#A29BFE', primaryLight: '#6C5CE7', primaryContainer: '#2D2654' },

  blue:   { primary: '#74B9FF', primaryLight: '#0984E3', primaryContainer: '#0A3A5C' },

  green:  { primary: '#55EFC4', primaryLight: '#00B894', primaryContainer: '#0A3D33' },

  orange: { primary: '#FAB1A0', primaryLight: '#E17055', primaryContainer: '#4A2520' },

};

暗色模式不是简单反转颜色------主色和浅色互换,Container 色用深色低饱和度版本。

buildColors 函数

typescript 复制代码
export function buildColors(themeColorHex: string, isDark: boolean) {

  const key = THEME_COLOR_MAP[themeColorHex] ?? 'purple';

  const primaries = isDark ? THEME_PRIMARIES_DARK[key] : THEME_PRIMARIES[key];

  const shared = isDark ? SHARED_DARK : SHARED_LIGHT;

  


  return {

    primary: primaries.primary,

    onPrimary: isDark ? '#1A1A2E' : '#FFFFFF',

    primaryContainer: primaries.primaryContainer,

    background: shared.background,

    surface: shared.surface,

    onSurface: shared.onSurface,

    error: shared.error,

    // ... 完整的 MD3 色板

  };

}

输出是一个完整的 Material Design 3 色板对象,直接传给 react-native-paperPaperProvider

暗色模式实现

三种模式:跟随系统 / 手动浅色 / 手动深色

typescript 复制代码
// src/theme/ThemeProvider.tsx

export function ThemeProvider({ children }) {

  const themeColor = useSettingsStore((s) => s.themeColor);

  const darkMode = useSettingsStore((s) => s.darkMode);

  const systemScheme = useColorScheme();

  


  const isDark = useMemo(() => {

    if (darkMode === 'dark') return true;

    if (darkMode === 'light') return false;

    return systemScheme === 'dark';  // 'auto' 模式:跟随系统

  }, [darkMode, systemScheme]);

  


  const theme = useMemo(() => buildTheme(themeColor, isDark), [themeColor, isDark]);

  


  return <PaperProvider theme={theme}>{children}</PaperProvider>;

}

关键设计点

  1. useMemo 缓存 :主题对象只在 themeColordarkMode 变化时重建,避免每次渲染都重新创建

  2. useColorScheme 监听:系统深色模式切换时自动响应

  3. 设置持久化darkMode 存在 SQLite 的 settings 表里,重启后保持用户选择

基础组件示例

AppButton:5 种变体

typescript 复制代码
// src/components/ui/Button.tsx

type ButtonVariant = 'primary' | 'secondary' | 'text' | 'icon' | 'danger';

  


export function AppButton({ title, variant = 'primary', icon, onPress, ... }) {

  const getPaperMode = () => {

    switch (variant) {

      case 'primary':

      case 'danger':  return 'contained';

      case 'secondary': return 'outlined';

      case 'text':

      case 'icon':    return 'text';

    }

  };

  


  return (

    <PaperButton

      mode={getPaperMode()}

      buttonColor={variant === 'danger' ? theme.colors.error : undefined}

      style={[styles.button, style]}

      {...props}

    >

      {title}

    </PaperButton>

  );

}

设计决策danger 不是 Paper 的原生变体,而是通过 buttonColor={theme.colors.error} 实现的。这保持了组件 API 的简洁------调用方只需传 variant="danger"

AppCard:可点击 + 选中态

typescript 复制代码
// src/components/ui/Card.tsx

export function AppCard({ children, onPress, selected = false, elevation = 1, style }) {

  const cardStyle = {

    borderRadius: radius.lg,

    backgroundColor: theme.colors.surface,

    borderWidth: selected ? 2 : 0,

    borderColor: selected ? theme.colors.primary : 'transparent',

  };

  


  if (onPress) {

    return (

      <PaperCard mode="elevated" onPress={onPress} style={[cardStyle, { elevation }]}>

        <PaperCard.Content>{children}</PaperCard.Content>

      </PaperCard>

    );

  }

  


  return (

    <PaperCard mode="elevated" style={[cardStyle, { elevation }]}>

      <PaperCard.Content>{children}</PaperCard.Content>

    </PaperCard>

  );

}

设计决策selected 态用 primary 色 2px 边框表示,而不是改背景色------因为在暗色模式下改背景色容易和 surface 色混淆。

3 个可复用模式

模式 1:Token 驱动样式

typescript 复制代码
// 不要这样:

const styles = StyleSheet.create({

  card: { padding: 16, borderRadius: 12, margin: 8 }

});

  


// 要这样:

import { spacing, radius } from '@/theme/tokens';

const styles = StyleSheet.create({

  card: { padding: spacing.md, borderRadius: radius.md, margin: spacing.sm }

});

好处 :全局调整间距只需要改 tokens.ts 一个文件。

模式 2:组件包装 Paper + 项目默认值

typescript 复制代码
// 不要每个页面都重复写 Paper 配置

<PaperCard mode="elevated" style={{ borderRadius: 16, backgroundColor: theme.colors.surface }}>

  


// 包装一次,全项目复用

<AppCard elevation={1}>

好处 :统一修改 Card 的默认圆角、颜色时,只改 AppCard 一个文件。

模式 3:ThemeProvider + useMemo 缓存

typescript 复制代码
// 主题对象创建成本高(颜色映射、对象组合),用 useMemo 避免重复创建

const theme = useMemo(() => buildTheme(themeColor, isDark), [themeColor, isDark]);

好处:主题切换时只重建一次,不会因为父组件 re-render 而反复创建。

🔗 源码 : github.com/callmebg/wo... | theme/ | ui/


CSDN 标签 : 设计系统 组件化 暗色模式 React Native UI
掘金话题 : 前端 设计系统 组件化 React Native

相关推荐
CodeSheep9 小时前
有这4个迹象,你就该离职了!
前端·后端·程序员
用户059540174469 小时前
把大模型记忆回归测试从手工检查换成 Playwright+VCR,误判率从 40% 降到 0
前端·css
倾颜9 小时前
pnpm 负责依赖,Turbo 负责任务:一次小型 Monorepo 治理的边界取舍
前端·next.js
程序员爱钓鱼9 小时前
Rust 元组 Tuple 详解:组合不同类型的数据
前端·后端·rust
IT_陈寒9 小时前
React的useEffect为什么经常执行两次?
前端·人工智能·后端
国服第二切图仔9 小时前
12-Web工具
大数据·前端
爱分享的程序猿-Clark9 小时前
【前端分享】大前端监控体系搭建实战
前端
夕除9 小时前
sign 是什么
java·前端
An_s9 小时前
Android仿真翻页(一),基于pagecurl二开
前端·javascript·html