请添加图片描述

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

快递到哪了?订单什么状态?项目进展到哪一步了?

这些问题有个共同点:都是按时间顺序发生的一系列事件。用列表展示当然可以,但时间线能让用户更直观地看到"进度"和"流程"。

一条竖线,几个节点,每个节点旁边是时间和描述。简单,但很有效。

今天来看看这个 Timeline 组件是怎么写的。


代码全貌

文件在 src/components/ui/Timeline.tsx

import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { UITheme, ColorType } from './theme';

interface TimelineItem {
  title: string;
  description?: string;
  time?: string;
  icon?: string;
  color?: ColorType;
}

interface TimelineProps {
  items: TimelineItem[];
  variant?: 'default' | 'alternate' | 'right';
  lineColor?: string;
  style?: ViewStyle;
}

export const Timeline: React.FC<TimelineProps> = ({
  items,
  variant = 'default',
  lineColor = UITheme.colors.gray[200],
  style,
}) => {
  return (
    <View style={style}>
      {items.map((item, index) => {
        const isLast = index === items.length - 1;
        const dotColor = item.color ? UITheme.colors[item.color] : UITheme.colors.primary;
        const isRight = variant === 'right' || (variant === 'alternate' && index % 2 === 1);

        return (
          <View
            key={index}
            style={[styles.item, isRight && styles.itemRight]}
          >
            {variant === 'alternate' && (
              <View style={[styles.side, isRight ? styles.sideLeft : styles.sideRight]}>
                {!isRight && item.time && <Text style={styles.time}>{item.time}</Text>}
                {isRight && (
                  <>
                    <Text style={styles.title}>{item.title}</Text>
                    {item.description && <Text style={styles.description}>{item.description}</Text>}
                  </>
                )}
              </View>
            )}
            <View style={styles.dotContainer}>
              <View style={[styles.dot, { backgroundColor: dotColor }]}>
                {item.icon && <Text style={styles.dotIcon}>{item.icon}</Text>}
              </View>
              {!isLast && <View style={[styles.line, { backgroundColor: lineColor }]} />}
            </View>
            <View style={[styles.content, variant === 'right' && styles.contentRight]}>
              {variant !== 'alternate' && item.time && <Text style={styles.time}>{item.time}</Text>}
              {(variant !== 'alternate' || !isRight) && (
                <>
                  <Text style={styles.title}>{item.title}</Text>
                  {item.description && <Text style={styles.description}>{item.description}</Text>}
                </>
              )}
              {variant === 'alternate' && isRight && item.time && <Text style={styles.time}>{item.time}</Text>}
            </View>
          </View>
        );
      })}
    </View>
  );
};

const styles = StyleSheet.create({
  item: { flexDirection: 'row', minHeight: 60 },
  itemRight: { flexDirection: 'row-reverse' },
  side: { flex: 1, paddingHorizontal: UITheme.spacing.md },
  sideLeft: { alignItems: 'flex-end' },
  sideRight: { alignItems: 'flex-start' },
  dotContainer: { alignItems: 'center', width: 24 },
  dot: {
    width: 12,
    height: 12,
    borderRadius: 6,
    alignItems: 'center',
    justifyContent: 'center',
  },
  dotIcon: { fontSize: 8, color: UITheme.colors.white },
  line: { flex: 1, width: 2, marginVertical: 4 },
  content: { flex: 1, paddingLeft: UITheme.spacing.md, paddingBottom: UITheme.spacing.lg },
  contentRight: { paddingLeft: 0, paddingRight: UITheme.spacing.md, alignItems: 'flex-end' },
  time: { fontSize: UITheme.fontSize.xs, color: UITheme.colors.gray[400], marginBottom: 2 },
  title: { fontSize: UITheme.fontSize.md, fontWeight: '500', color: UITheme.colors.gray[800] },
  description: { fontSize: UITheme.fontSize.sm, color: UITheme.colors.gray[500], marginTop: 2 },
});

80 行左右,实现了三种布局模式。代码看着有点绕,别急,我们一块一块来。


数据结构设计

interface TimelineItem {
  title: string;
  description?: string;
  time?: string;
  icon?: string;
  color?: ColorType;
}

每个时间节点需要哪些信息?

title 是必须的,告诉用户这个节点是什么事件。比如"订单创建"、“已发货”、“已签收”。

description 是补充说明,可选。有时候标题不够,需要更多细节。比如"已发货"可以补充"从上海仓库发出"。

time 是时间戳,可选。有些时间线不需要显示具体时间,只需要展示流程顺序。

icon 是节点图标,可选。默认是个小圆点,加上图标可以让每个节点更有辨识度。

color 是节点颜色,可选。不同颜色可以表示不同状态,绿色表示完成,黄色表示进行中,灰色表示待处理。


组件属性

interface TimelineProps {
  items: TimelineItem[];
  variant?: 'default' | 'alternate' | 'right';
  lineColor?: string;
  style?: ViewStyle;
}

items 是时间节点数组,这是核心数据。

variant 控制布局模式,有三种选择:

  • default 内容在左边,这是最常见的
  • right 内容在右边
  • alternate 内容左右交替,适合展示对比或者让页面更有节奏感

lineColor 是连接线的颜色,默认浅灰色。一般不需要改,除非你的设计稿有特殊要求。


组件入口

export const Timeline: React.FC<TimelineProps> = ({
  items,
  variant = 'default',
  lineColor = UITheme.colors.gray[200],
  style,
}) => {
  return (
    <View style={style}>
      {items.map((item, index) => {

组件接收 items 数组,然后用 map 遍历渲染每个节点。

这里没什么特别的,就是标准的列表渲染模式。


关键变量计算

const isLast = index === items.length - 1;
const dotColor = item.color ? UITheme.colors[item.color] : UITheme.colors.primary;
const isRight = variant === 'right' || (variant === 'alternate' && index % 2 === 1);

每个节点渲染前,先算三个值。

isLast 判断是不是最后一个节点。最后一个节点下面不需要连接线,因为没有下一个节点了。

dotColor 是节点圆点的颜色。如果 item 指定了 color,就用指定的;没指定就用主题色。

isRight 判断内容是否显示在右边。两种情况会在右边:一是 variant 本身就是 right;二是 variant 是 alternate 且当前是奇数索引(0、2、4 在左边,1、3、5 在右边)。

index % 2 === 1 这个取模运算就是用来实现交替效果的。


节点容器

return (
  <View
    key={index}
    style={[styles.item, isRight && styles.itemRight]}
  >

每个节点是一个水平的 flex 容器。

styles.item 设置了 flexDirection: 'row',子元素从左到右排列。

isRight 为 true 时,加上 styles.itemRight,它设置了 flexDirection: 'row-reverse',子元素从右到左排列。这样就实现了内容在右边的效果,而不需要改变 DOM 结构。


交替模式的左侧区域

{variant === 'alternate' && (
  <View style={[styles.side, isRight ? styles.sideLeft : styles.sideRight]}>
    {!isRight && item.time && <Text style={styles.time}>{item.time}</Text>}
    {isRight && (
      <>
        <Text style={styles.title}>{item.title}</Text>
        {item.description && <Text style={styles.description}>{item.description}</Text>}
      </>
    )}
  </View>
)}

这段只在 alternate 模式下渲染。

交替模式的结构是:左侧区域 + 中间圆点线 + 右侧区域。内容在左右两边交替出现。

isRight 为 false 时(内容在左边),左侧区域显示时间。
isRight 为 true 时(内容在右边),左侧区域显示标题和描述。

有点绕对吧?其实就是:内容在哪边,时间就在另一边。这样时间和内容分开,视觉上更清晰。

styles.sideLeft 设置了 alignItems: 'flex-end',让内容靠右对齐,贴近中间的圆点。
styles.sideRight 设置了 alignItems: 'flex-start',让内容靠左对齐。


中间的圆点和连接线

<View style={styles.dotContainer}>
  <View style={[styles.dot, { backgroundColor: dotColor }]}>
    {item.icon && <Text style={styles.dotIcon}>{item.icon}</Text>}
  </View>
  {!isLast && <View style={[styles.line, { backgroundColor: lineColor }]} />}
</View>

这是时间线的"线"的部分。

dotContainer 是一个垂直的容器,宽度固定 24px,里面放圆点和线。

圆点是个 12x12 的小圆,borderRadius: 6 让它变成正圆。如果有 icon,就在圆点里显示图标。

连接线用 {!isLast && ...} 条件渲染,最后一个节点不显示。线的宽度是 2px,高度用 flex: 1 自动撑满剩余空间。

marginVertical: 4 让线和圆点之间有点间距,不会紧贴在一起。


内容区域

<View style={[styles.content, variant === 'right' && styles.contentRight]}>
  {variant !== 'alternate' && item.time && <Text style={styles.time}>{item.time}</Text>}
  {(variant !== 'alternate' || !isRight) && (
    <>
      <Text style={styles.title}>{item.title}</Text>
      {item.description && <Text style={styles.description}>{item.description}</Text>}
    </>
  )}
  {variant === 'alternate' && isRight && item.time && <Text style={styles.time}>{item.time}</Text>}
</View>

内容区域的渲染逻辑有点复杂,因为要处理三种模式。

default 和 right 模式比较简单:时间、标题、描述依次显示。

alternate 模式要分情况:

  • 内容在左边时(!isRight),显示标题和描述
  • 内容在右边时(isRight),只显示时间(因为标题和描述在左侧区域显示了)

styles.contentRight 设置了 alignItems: 'flex-end',让文字右对齐。


样式定义

const styles = StyleSheet.create({
  item: { flexDirection: 'row', minHeight: 60 },
  itemRight: { flexDirection: 'row-reverse' },

minHeight: 60 保证每个节点有最小高度,即使内容很少也不会太挤。

  side: { flex: 1, paddingHorizontal: UITheme.spacing.md },
  sideLeft: { alignItems: 'flex-end' },
  sideRight: { alignItems: 'flex-start' },

side 是交替模式下的侧边区域,flex: 1 让它和内容区域平分空间。

  dotContainer: { alignItems: 'center', width: 24 },
  dot: {
    width: 12,
    height: 12,
    borderRadius: 6,
    alignItems: 'center',
    justifyContent: 'center',
  },
  dotIcon: { fontSize: 8, color: UITheme.colors.white },
  line: { flex: 1, width: 2, marginVertical: 4 },

圆点容器固定宽度,圆点居中。线用 flex: 1 自动填充高度。

  content: { flex: 1, paddingLeft: UITheme.spacing.md, paddingBottom: UITheme.spacing.lg },
  contentRight: { paddingLeft: 0, paddingRight: UITheme.spacing.md, alignItems: 'flex-end' },

内容区域 flex: 1 占据剩余空间。paddingBottom 让节点之间有间距。

  time: { fontSize: UITheme.fontSize.xs, color: UITheme.colors.gray[400], marginBottom: 2 },
  title: { fontSize: UITheme.fontSize.md, fontWeight: '500', color: UITheme.colors.gray[800] },
  description: { fontSize: UITheme.fontSize.sm, color: UITheme.colors.gray[500], marginTop: 2 },
});

文字样式形成层次:时间最小最浅,标题中等加粗,描述小号浅色。


Demo 里怎么用

看看 src/screens/demos/TimelineDemo.tsx 里的例子。

准备数据

const items = [
  { title: '创建订单', description: '订单已创建成功', time: '09:00', color: 'success' as const },
  { title: '支付完成', description: '使用支付宝支付 ¥299.00', time: '09:05', color: 'success' as const },
  { title: '商家发货', description: '包裹已从上海发出', time: '10:30', color: 'primary' as const },
  { title: '等待收货', description: '预计明天送达', time: '待定', color: 'warning' as const },
];

一个典型的订单物流时间线。前两步已完成用 success 绿色,当前步骤用 primary 蓝色,待处理用 warning 黄色。

as const 是 TypeScript 的类型断言,让字符串字面量保持精确类型。

基础用法

<Timeline items={items} />

最简单的用法,传入数据就行。默认左对齐,圆点在左边,内容在右边。

右对齐

<Timeline items={items} variant="right" />

内容跑到左边去了,圆点在右边。适合从右往左阅读的场景,或者设计上需要变化的时候。

交替显示

<Timeline items={items} variant="alternate" />

内容左右交替,时间线在中间。这种布局更有设计感,适合展示比较长的时间线,避免视觉疲劳。

带图标

<Timeline
  items={[
    { title: '下单成功', time: '09:00', icon: '📦', color: 'success' },
    { title: '已发货', time: '10:00', icon: '🚚', color: 'primary' },
    { title: '运输中', time: '14:00', icon: '✈️', color: 'info' },
    { title: '已签收', time: '18:00', icon: '✅', color: 'success' },
  ]}
/>

每个节点加上图标,一眼就能看出是什么状态。图标比纯色圆点更有表现力。


实际场景

订单追踪

这是时间线最常见的用途。用户下单后,想知道包裹到哪了,时间线把每个环节串起来,一目了然。

const OrderTracking = ({ orderId }) => {
  const { data: trackingInfo } = useOrderTracking(orderId);
  
  const items = trackingInfo.map(info => ({
    title: info.status,
    description: info.location,
    time: formatTime(info.timestamp),
    color: info.isCurrent ? 'primary' : 'success',
  }));
  
  return <Timeline items={items} />;
};

操作日志

后台系统里,经常需要展示某条数据的操作历史。谁在什么时候做了什么操作。

const OperationLog = ({ logs }) => {
  const items = logs.map(log => ({
    title: `${log.operator} ${log.action}`,
    description: log.detail,
    time: log.time,
    icon: getActionIcon(log.action),
  }));
  
  return <Timeline items={items} />;
};

项目里程碑

项目管理工具里,展示项目的关键节点和进度。

const ProjectMilestones = ({ milestones }) => {
  const items = milestones.map(m => ({
    title: m.name,
    description: m.completed ? '已完成' : `预计 ${m.dueDate}`,
    color: m.completed ? 'success' : m.isOverdue ? 'danger' : 'warning',
  }));
  
  return <Timeline items={items} variant="alternate" />;
};

用户动态

社交应用里,展示用户的活动记录。

const UserActivity = ({ activities }) => {
  const items = activities.map(a => ({
    title: a.action,
    description: a.target,
    time: formatRelativeTime(a.timestamp),
    icon: getActivityIcon(a.type),
  }));
  
  return <Timeline items={items} />;
};

几个细节

写时间线组件,有几个容易忽略的点:

最后一个节点不要线

时间线的"线"是连接两个节点的,最后一个节点下面没有东西可连,所以不需要线。代码里用 !isLast 判断。

节点颜色要有意义

别随便用颜色,要让颜色传达状态。绿色表示完成,蓝色表示当前,黄色表示等待,红色表示异常。用户看一眼颜色就知道进度。

交替模式的对齐

交替模式下,左边的内容要右对齐,右边的内容要左对齐,这样才能贴近中间的时间线。代码里用 alignItems 控制。

最小高度

每个节点要有最小高度,不然内容少的节点会很矮,连接线会很短,看起来不协调。


写在最后

时间线组件的核心就是把一系列事件按顺序串起来。技术上不难,难的是处理好各种布局模式下的细节。

这个组件支持三种布局,代码里有不少条件判断,第一次看可能有点晕。但理解了每种模式的结构,再看代码就清楚了。

下次遇到需要展示流程、进度、历史记录的场景,试试时间线。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐