rn_for_openharmony常用组件_Breadcrumb面包屑

项目开源地址:https://atomgit.com/nutpi/rn_for_openharmony_element

"面包屑"这个名字来自童话故事《汉塞尔与格莱特》。两个小孩在森林里撒面包屑标记来时的路,好找到回家的方向。

在 UI 设计里,面包屑导航做的是同样的事:告诉用户"你是怎么走到这里的",以及"怎么回去"。

首页 > 商品分类 > 手机数码 > iPhone 15

看到这行字,你立刻知道自己在哪个页面,也知道点击前面的任何一项就能回到那个层级。

今天来看看这个组件怎么实现。


源码

文件位置 src/components/ui/Breadcrumb.tsx

tsx 复制代码
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
import { UITheme, ColorType } from './theme';

interface BreadcrumbItem {
  label: string;
  onPress?: () => void;
  icon?: string;
}

interface BreadcrumbProps {
  items: BreadcrumbItem[];
  separator?: string;
  color?: ColorType;
  maxItems?: number;
  style?: ViewStyle;
}

export const Breadcrumb: React.FC<BreadcrumbProps> = ({
  items,
  separator = '/',
  color = 'primary',
  maxItems,
  style,
}) => {
  const colorValue = UITheme.colors[color];

  let displayItems = items;
  if (maxItems && items.length > maxItems) {
    const first = items.slice(0, 1);
    const last = items.slice(-maxItems + 2);
    displayItems = [...first, { label: '...' }, ...last];
  }

  return (
    <View style={[styles.container, style]}>
      {displayItems.map((item, index) => {
        const isLast = index === displayItems.length - 1;
        const isClickable = item.onPress && item.label !== '...';

        return (
          <View key={index} style={styles.itemContainer}>
            {isClickable ? (
              <TouchableOpacity onPress={item.onPress} style={styles.item}>
                {item.icon && <Text style={styles.icon}>{item.icon}</Text>}
                <Text style={[styles.label, { color: colorValue }]}>{item.label}</Text>
              </TouchableOpacity>
            ) : (
              <View style={styles.item}>
                {item.icon && <Text style={styles.icon}>{item.icon}</Text>}
                <Text style={[styles.label, isLast && styles.currentLabel]}>{item.label}</Text>
              </View>
            )}
            {!isLast && <Text style={styles.separator}>{separator}</Text>}
          </View>
        );
      })}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap' },
  itemContainer: { flexDirection: 'row', alignItems: 'center' },
  item: { flexDirection: 'row', alignItems: 'center' },
  icon: { fontSize: 14, marginRight: 4 },
  label: { fontSize: UITheme.fontSize.sm, color: UITheme.colors.gray[500] },
  currentLabel: { color: UITheme.colors.gray[800], fontWeight: '500' },
  separator: {
    fontSize: UITheme.fontSize.sm,
    color: UITheme.colors.gray[400],
    marginHorizontal: UITheme.spacing.sm,
  },
});

70 行代码,功能不复杂但细节不少。


单个面包屑项长什么样

tsx 复制代码
interface BreadcrumbItem {
  label: string;
  onPress?: () => void;
  icon?: string;
}

三个字段,很简洁。

label 是显示的文字,比如"首页"、"商品分类"。这是必填的,面包屑总得有文字吧。

onPress 是点击回调。有这个属性的项是可点击的,通常用来跳转到对应页面。最后一项一般不传 onPress,因为那就是当前页面,点了也没意义。

icon 是可选的图标。首页项经常会加个小房子图标,让用户更容易识别。


组件接收哪些参数

tsx 复制代码
interface BreadcrumbProps {
  items: BreadcrumbItem[];
  separator?: string;
  color?: ColorType;
  maxItems?: number;
  style?: ViewStyle;
}

items 是面包屑项的数组,按层级顺序排列。第一项是最顶层(通常是首页),最后一项是当前页面。

separator 是分隔符,默认是斜杠 /。你也可以换成 > 或者任何你喜欢的字符。

color 是可点击项的颜色。默认用主题色,让用户知道这些是可以点的。

maxItems 是最多显示几项。路径太长时,中间的会被折叠成 ...。这个后面详细讲。


默认值和颜色

tsx 复制代码
export const Breadcrumb: React.FC<BreadcrumbProps> = ({
  items,
  separator = '/',
  color = 'primary',
  maxItems,
  style,
}) => {
  const colorValue = UITheme.colors[color];

separator = '/' 斜杠是最常见的分隔符,Web 上的面包屑基本都用这个。

color = 'primary' 可点击的项用主题色,和普通文字区分开。

colorValue 从主题里取出具体的颜色值,后面渲染时用。


路径折叠逻辑

tsx 复制代码
let displayItems = items;
if (maxItems && items.length > maxItems) {
  const first = items.slice(0, 1);
  const last = items.slice(-maxItems + 2);
  displayItems = [...first, { label: '...' }, ...last];
}

这段代码处理路径过长的情况。

假设 maxItems = 4,items 有 6 项:首页、一级、二级、三级、四级、当前页。

直接显示 6 项太长了,需要折叠。折叠的规则是:保留第一项,保留最后几项,中间用 ... 代替。

items.slice(0, 1) 取第一项,就是"首页"。

items.slice(-maxItems + 2) 这个有点绕。-maxItems + 2 就是 -4 + 2 = -2slice(-2) 取最后两项,就是"四级"和"当前页"。

为什么是 maxItems + 2?因为要给第一项和 ... 留位置。maxItems 是 4,减去首页占 1 个,减去 ... 占 1 个,剩下 2 个位置给最后的项。

最后拼起来:[首页, ..., 四级, 当前页],正好 4 项。

如果 items 长度不超过 maxItems,就不折叠,直接用原数组。


渲染容器

tsx 复制代码
return (
  <View style={[styles.container, style]}>
    {displayItems.map((item, index) => {

外层容器用了 flexDirection: 'row' 让子元素水平排列,flexWrap: 'wrap' 允许换行。

为什么要允许换行?因为面包屑可能很长,在窄屏幕上一行放不下。允许换行比横向滚动更友好。

遍历 displayItems 而不是 items,因为可能经过了折叠处理。


判断每一项的状态

tsx 复制代码
const isLast = index === displayItems.length - 1;
const isClickable = item.onPress && item.label !== '...';

isLast 判断是不是最后一项。最后一项是当前页面,样式和行为都不一样。

isClickable 判断是否可点击。两个条件都要满足:有 onPress 回调,而且不是 ...

为什么 ... 不能点击?因为它只是个占位符,表示"这里省略了一些层级",点它没有意义。


可点击项的渲染

tsx 复制代码
{isClickable ? (
  <TouchableOpacity onPress={item.onPress} style={styles.item}>
    {item.icon && <Text style={styles.icon}>{item.icon}</Text>}
    <Text style={[styles.label, { color: colorValue }]}>{item.label}</Text>
  </TouchableOpacity>
)

可点击的项用 TouchableOpacity 包裹,点击时触发 onPress。

图标用条件渲染,有就显示。

文字颜色用 colorValue,就是主题色,让用户知道这是可以点的链接。


不可点击项的渲染

tsx 复制代码
: (
  <View style={styles.item}>
    {item.icon && <Text style={styles.icon}>{item.icon}</Text>}
    <Text style={[styles.label, isLast && styles.currentLabel]}>{item.label}</Text>
  </View>
)}

不可点击的项用普通 View,没有触摸反馈。

文字样式有个条件:如果是最后一项(当前页面),用 currentLabel 样式,颜色更深、字体更粗。这样当前页面在视觉上更突出。

如果是 ...,既不是最后一项也不可点击,就用默认的灰色样式。


分隔符渲染

tsx 复制代码
{!isLast && <Text style={styles.separator}>{separator}</Text>}

除了最后一项,每项后面都加分隔符。

separator 是从 props 传进来的,默认是 /

分隔符用浅灰色,不会太抢眼,只是起到分隔作用。


样式定义

tsx 复制代码
const styles = StyleSheet.create({
  container: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap' },

容器是水平 flex 布局,垂直居中,允许换行。

tsx 复制代码
  itemContainer: { flexDirection: 'row', alignItems: 'center' },
  item: { flexDirection: 'row', alignItems: 'center' },

每个面包屑项也是水平布局,因为图标和文字要并排显示。

itemContainer 包含了面包屑项和分隔符,item 只包含图标和文字。两层嵌套是为了让分隔符和面包屑项在同一行。

tsx 复制代码
  icon: { fontSize: 14, marginRight: 4 },

图标小一点,和文字之间有 4px 间距。

tsx 复制代码
  label: { fontSize: UITheme.fontSize.sm, color: UITheme.colors.gray[500] },
  currentLabel: { color: UITheme.colors.gray[800], fontWeight: '500' },

普通文字用小字号和浅灰色。当前页面的文字用深色和中等粗细,更醒目。

tsx 复制代码
  separator: {
    fontSize: UITheme.fontSize.sm,
    color: UITheme.colors.gray[400],
    marginHorizontal: UITheme.spacing.sm,
  },
});

分隔符用更浅的灰色,左右有间距,和面包屑项分开。


Demo 里的用法

看看 src/screens/demos/BreadcrumbDemo.tsx

基础用法

tsx 复制代码
<Breadcrumb
  items={[
    { label: '首页', onPress: () => {} },
    { label: '商品列表', onPress: () => {} },
    { label: '商品详情' },
  ]}
/>

三级路径。前两项有 onPress 可以点击,最后一项是当前页面不能点。

渲染出来是:首页 / 商品列表 / 商品详情

首页和商品列表是主题色,商品详情是深灰色加粗。

自定义分隔符

tsx 复制代码
<Breadcrumb items={items} separator=">" />
<Breadcrumb items={items} separator="→" />
<Breadcrumb items={items} separator="•" />

三种不同的分隔符风格。> 比较常见, 更有方向感, 比较简洁。

选哪个看设计稿,或者看个人喜好。

带图标

tsx 复制代码
<Breadcrumb
  items={[
    { label: '首页', icon: '🏠', onPress: () => {} },
    { label: '用户', icon: '👤', onPress: () => {} },
    { label: '设置', icon: '⚙️' },
  ]}
/>

每项都有图标,视觉上更丰富。首页用房子图标是约定俗成的。

不同颜色

tsx 复制代码
<Breadcrumb items={items} color="success" />
<Breadcrumb items={items} color="danger" />

可点击项用不同颜色。一般用主题色就行,特殊场景可以换。

折叠显示

tsx 复制代码
<Breadcrumb
  items={[
    { label: '首页', onPress: () => {} },
    { label: '一级分类', onPress: () => {} },
    { label: '二级分类', onPress: () => {} },
    { label: '三级分类', onPress: () => {} },
    { label: '商品详情' },
  ]}
  maxItems={4}
/>

5 项路径,设置 maxItems={4},中间会被折叠。

渲染出来是:首页 / ... / 三级分类 / 商品详情


实际场景

电商商品页

tsx 复制代码
const ProductPage = ({ product, category }) => {
  const breadcrumbItems = [
    { label: '首页', icon: '🏠', onPress: () => navigate('Home') },
    { label: category.parent.name, onPress: () => navigate('Category', { id: category.parent.id }) },
    { label: category.name, onPress: () => navigate('Category', { id: category.id }) },
    { label: product.name },
  ];
  
  return (
    <View>
      <Breadcrumb items={breadcrumbItems} />
      <ProductDetail product={product} />
    </View>
  );
};

根据商品的分类信息动态生成面包屑。点击任意层级可以跳转到对应的分类页。

后台管理系统

tsx 复制代码
const AdminPage = ({ route }) => {
  const pathSegments = route.path.split('/').filter(Boolean);
  
  const items = pathSegments.map((segment, index) => {
    const path = '/' + pathSegments.slice(0, index + 1).join('/');
    const isLast = index === pathSegments.length - 1;
    
    return {
      label: getPageTitle(segment),
      onPress: isLast ? undefined : () => navigate(path),
    };
  });
  
  return <Breadcrumb items={items} separator=">" />;
};

根据当前路由自动生成面包屑。这种方式不用手动维护每个页面的面包屑配置。

文件管理器

tsx 复制代码
const FileExplorer = ({ currentPath }) => {
  const folders = currentPath.split('/').filter(Boolean);
  
  const items = [
    { label: '根目录', icon: '📁', onPress: () => goToPath('/') },
    ...folders.map((folder, index) => ({
      label: folder,
      icon: '📂',
      onPress: index === folders.length - 1 
        ? undefined 
        : () => goToPath('/' + folders.slice(0, index + 1).join('/')),
    })),
  ];
  
  return <Breadcrumb items={items} maxItems={5} />;
};

文件路径可能很深,用 maxItems 折叠中间的层级。


移动端的特殊考虑

面包屑在 PC 端很常见,但在移动端用得少一些。为什么?

屏幕太窄。PC 上一行能放很长的面包屑,手机上可能放不下。虽然我们支持换行,但换行后的面包屑看起来不太优雅。

有返回按钮。移动端通常有顶部导航栏和返回按钮,用户习惯用返回键一级一级往回走,不太需要面包屑的"跳级"功能。

所以移动端的面包屑通常用在这些场景:

  • 层级很深,用户可能想直接跳回某一级
  • 电商商品页,用户想回到分类页浏览其他商品
  • 后台管理类应用,操作逻辑更接近 PC

如果你的应用层级不深,或者用户很少需要跳级,可能不需要面包屑。


小结

面包屑组件的核心功能:

  1. 显示当前页面的路径层级
  2. 可点击的项能跳转到对应层级
  3. 当前页面(最后一项)样式不同且不可点击
  4. 路径太长时可以折叠

代码实现上,主要是处理好"可点击/不可点击"和"普通项/当前项"这两组状态的区分。

面包屑虽然简单,但能显著提升用户的导航体验。特别是在层级较深的应用里,它就像一张地图,让用户随时知道自己在哪。


可以扩展的方向

这个实现已经覆盖了基本需求,如果想做得更完善,还可以考虑:

下拉菜单

点击 ... 时弹出下拉菜单,显示被折叠的层级。这样用户不用猜中间省略了什么。

tsx 复制代码
// 伪代码示意
{item.label === '...' && (
  <Dropdown
    items={collapsedItems}
    onSelect={(item) => item.onPress?.()}
  />
)}

自定义渲染

支持传入自定义的渲染函数,让使用者完全控制每一项的样式。

tsx 复制代码
interface BreadcrumbProps {
  renderItem?: (item: BreadcrumbItem, isLast: boolean) => React.ReactNode;
}

响应式折叠

根据容器宽度自动决定折叠多少项,而不是固定的 maxItems。这需要测量每一项的宽度,实现起来复杂一些。

动画效果

路径变化时加上过渡动画,比如新增的项从右边滑入。

这些都是锦上添花的功能,基础版本已经够用了。


和其他导航方式的配合

面包屑不是孤立存在的,它通常和其他导航元素配合使用。

和顶部导航栏

顶部导航栏显示当前页面标题和返回按钮,面包屑显示完整路径。两者功能有重叠,但面包屑能让用户跳过中间层级直接回到某一级。

和侧边栏菜单

后台系统常见的组合。侧边栏是主导航,面包屑显示当前在哪个菜单项下的哪个子页面。

和底部 Tab 栏

移动端常见的组合。Tab 栏切换主要模块,面包屑显示模块内的层级。不过这种组合比较少见,因为移动端通常层级不深。

选择哪种导航方式,要根据应用的信息架构和用户的使用习惯来决定。面包屑适合层级深、用户需要频繁跳级的场景。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
静听松涛1339 小时前
提示词注入攻击的防御机制
前端·javascript·easyui
御承扬9 小时前
鸿蒙原生系列之动画效果(帧动画)
c++·harmonyos·动画效果·ndk ui·鸿蒙原生
澄江静如练_9 小时前
优惠券提示文案表单项(原生div写的)
前端·javascript·vue.js
C_心欲无痕9 小时前
ts - 关于Object、object 和 {} 的解析与区别
开发语言·前端·javascript·typescript
全栈前端老曹10 小时前
【包管理】read-pkg-up 快速上手教程 - 读取最近的 package.json 文件
前端·javascript·npm·node.js·json·nrm·package.json
JQLvopkk10 小时前
Vue框架技术详细介绍及阐述
前端·javascript·vue.js
行者9610 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨10 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
笔COOL创始人10 小时前
requestAnimationFrame 动画优化实践指南
前端·javascript·面试