rn_for_openharmony常用组件_Empty空状态

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

列表里没有数据,你打算显示什么?

很多开发者的第一反应是:什么都不显示呗,反正没数据。但用户看到的是一片空白,然后开始怀疑:加载完了吗?是不是出 bug 了?网络有问题?

一个精心设计的空状态,能把"什么都没有"变成一次沟通机会。它告诉用户三件事:

  1. 这里确实没有内容(不是 bug)
  2. 为什么没有内容(可选)
  3. 你可以做什么来改变这个状态(可选)

今天我们来看看 Empty 组件是怎么实现的。代码不多,但设计上有不少讲究。


源码全貌

先把完整代码贴出来,让你有个整体印象。文件位置:src/components/ui/Empty.tsx

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

interface EmptyProps {
  icon?: string;
  title?: string;
  description?: string;
  action?: { label: string; onPress: () => void };
  variant?: 'default' | 'compact' | 'large';
  style?: ViewStyle;
}

export const Empty: React.FC<EmptyProps> = ({
  icon = '📭',
  title = '暂无数据',
  description,
  action,
  variant = 'default',
  style,
}) => {
  const sizeMap: Record<string, { iconSize: number; titleSize: number; padding: number }> = {
    compact: { iconSize: 32, titleSize: 14, padding: 16 },
    default: { iconSize: 48, titleSize: 16, padding: 32 },
    large: { iconSize: 64, titleSize: 18, padding: 48 },
  };

  const { iconSize, titleSize, padding } = sizeMap[variant];

  return (
    <View style={[styles.container, { padding }, style]}>
      <Text style={[styles.icon, { fontSize: iconSize }]}>{icon}</Text>
      <Text style={[styles.title, { fontSize: titleSize }]}>{title}</Text>
      {description && <Text style={styles.description}>{description}</Text>}
      {action && (
        <Button
          title={action.label}
          onPress={action.onPress}
          variant="outline"
          size="sm"
          style={{ marginTop: UITheme.spacing.lg }}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { alignItems: 'center', justifyContent: 'center' },
  icon: { marginBottom: UITheme.spacing.md },
  title: { fontWeight: '500', color: UITheme.colors.gray[600], textAlign: 'center' },
  description: {
    fontSize: UITheme.fontSize.sm,
    color: UITheme.colors.gray[400],
    textAlign: 'center',
    marginTop: UITheme.spacing.xs,
  },
});

50 行代码,一个完整的空状态组件。下面我们逐段拆解。


第一部分:依赖引入

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

引入说明

  • ViewText:基础布局组件,Empty 的结构很简单,就是垂直排列的几个元素
  • UITheme:主题配置,用于获取统一的颜色、间距、字号
  • Button:复用项目里的按钮组件,用于"操作引导"功能

Empty 组件依赖了 Button,这是组件复用的好例子。空状态里的按钮不需要自己实现,直接用现成的。


第二部分:类型定义

tsx 复制代码
interface EmptyProps {
  icon?: string;
  title?: string;
  description?: string;
  action?: { label: string; onPress: () => void };
  variant?: 'default' | 'compact' | 'large';
  style?: ViewStyle;
}

属性解读

属性 说明 为什么这样设计
icon 图标,用 emoji 字符串 简单直接,不需要引入图标库
title 主标题 告诉用户"这里是什么"
description 补充说明 解释"为什么是空的"或"怎么办"
action 操作按钮配置 引导用户采取行动
variant 尺寸变体 适应不同大小的容器
style 自定义样式 灵活性保障

所有属性都是可选的。这意味着你可以直接写 <Empty /> 就能得到一个可用的空状态。


第三部分:默认值设置

tsx 复制代码
export const Empty: React.FC<EmptyProps> = ({
  icon = '📭',
  title = '暂无数据',
  description,
  action,
  variant = 'default',
  style,
}) => {

默认值策略

  • icon = '📭':邮箱图标,表示"空空如也"
  • title = '暂无数据':最通用的空状态文案
  • variant = 'default':中等尺寸,适合大多数场景

descriptionaction 没有默认值,因为它们是"增强功能",不是每个空状态都需要。

这组默认值的设计理念是:零配置可用。开发者不传任何参数,也能得到一个合理的空状态展示。


第四部分:尺寸映射表

tsx 复制代码
const sizeMap: Record<string, { iconSize: number; titleSize: number; padding: number }> = {
  compact: { iconSize: 32, titleSize: 14, padding: 16 },
  default: { iconSize: 48, titleSize: 16, padding: 32 },
  large: { iconSize: 64, titleSize: 18, padding: 48 },
};

const { iconSize, titleSize, padding } = sizeMap[variant];

尺寸配置详解

这个映射表定义了三种尺寸变体的具体数值:

compact 紧凑模式

  • 图标 32px,标题 14px,内边距 16px
  • 适合空间有限的场景,比如列表项内部、侧边栏

default 默认模式

  • 图标 48px,标题 16px,内边距 32px
  • 通用尺寸,适合大多数页面区域

large 大号模式

  • 图标 64px,标题 18px,内边距 48px
  • 适合整页空状态,比如首次使用时的引导页

为什么用映射表而不是 switch 语句?因为映射表更简洁,而且方便后续扩展。如果要加一个 mini 尺寸,只需要在对象里加一行。

解构赋值 const { iconSize, titleSize, padding } = sizeMap[variant] 让后续代码更清晰,不用写 sizeMap[variant].iconSize 这种冗长的访问方式。


第五部分:渲染结构

tsx 复制代码
return (
  <View style={[styles.container, { padding }, style]}>
    <Text style={[styles.icon, { fontSize: iconSize }]}>{icon}</Text>
    <Text style={[styles.title, { fontSize: titleSize }]}>{title}</Text>
    {description && <Text style={styles.description}>{description}</Text>}
    {action && (
      <Button
        title={action.label}
        onPress={action.onPress}
        variant="outline"
        size="sm"
        style={{ marginTop: UITheme.spacing.lg }}
      />
    )}
  </View>
);

渲染逻辑拆解

整个组件的 DOM 结构非常简单,就是一个垂直居中的容器,里面放着:

  1. 图标层 - 用 Text 组件渲染 emoji,通过 fontSize 控制大小
  2. 标题层 - 主要信息,告诉用户当前状态
  3. 描述层 - 可选,用条件渲染 {description && ...}
  4. 操作层 - 可选,复用 Button 组件

样式合并的写法 style={[styles.container, { padding }, style]} 是 React Native 的常见模式。数组里的样式会从左到右依次合并,后面的覆盖前面的。这样既保留了基础样式,又能动态调整 padding,还支持外部自定义。

条件渲染用的是短路求值:{description && <Text>...</Text>}。当 description 为空字符串或 undefined 时,整个表达式返回 falsy 值,React 不会渲染任何内容。


第六部分:样式定义

tsx 复制代码
const styles = StyleSheet.create({
  container: { alignItems: 'center', justifyContent: 'center' },
  icon: { marginBottom: UITheme.spacing.md },
  title: { fontWeight: '500', color: UITheme.colors.gray[600], textAlign: 'center' },
  description: {
    fontSize: UITheme.fontSize.sm,
    color: UITheme.colors.gray[400],
    textAlign: 'center',
    marginTop: UITheme.spacing.xs,
  },
});

样式设计思路

container 容器样式

  • alignItems: 'center' 水平居中
  • justifyContent: 'center' 垂直居中
  • 没有设置固定宽高,让组件自适应内容

icon 图标样式

  • 只设置了底部间距,字号在渲染时动态传入
  • 使用主题变量 UITheme.spacing.md 保持间距一致性

title 标题样式

  • fontWeight: '500' 中等粗细,不会太抢眼
  • 颜色用 gray[600],比正文稍浅,符合"次要信息"的定位

description 描述样式

  • 字号更小 fontSize.sm
  • 颜色更浅 gray[400]
  • 形成视觉层次:图标 > 标题 > 描述

整个样式表只有 4 个样式对象,非常精简。空状态组件不需要复杂的视觉效果,简洁清晰才是正道。


Demo 实战解析

看完组件实现,我们来看看实际怎么用。文件位置:src/screens/demos/EmptyDemo.tsx

最简用法

tsx 复制代码
<Empty />

什么参数都不传,直接用。组件会显示默认的邮箱图标和"暂无数据"文字。这种写法适合快速原型开发,或者你懒得想文案的时候。

自定义内容

tsx 复制代码
<Empty 
  icon="🔍" 
  title="未找到结果" 
  description="尝试使用其他关键词搜索" 
/>

搜索场景的空状态。放大镜图标暗示"搜索",标题说明结果为空,描述给出建议。三层信息递进,用户一眼就能理解发生了什么。

带操作引导

tsx 复制代码
<Empty
  icon="📝"
  title="暂无文章"
  description="点击下方按钮创建第一篇文章"
  action={{ label: '创建文章', onPress: () => {} }}
/>

这是空状态的"完全体"。不仅告诉用户没有内容,还提供了解决方案。action 属性接收一个对象,包含按钮文字和点击回调。

按钮使用了 variant="outline"size="sm",这是组件内部的设计决策。outline 样式不会太抢眼,sm 尺寸和空状态的整体比例协调。

尺寸对比

tsx 复制代码
<Empty variant="compact" title="紧凑模式" />
<View style={{ height: 24 }} />
<Empty variant="default" title="默认模式" />
<View style={{ height: 24 }} />
<Empty variant="large" title="大号模式" />

三种尺寸并排展示,方便对比选择。中间用 View 加间距,避免挤在一起。实际项目中,你需要根据容器大小选择合适的 variant。

场景矩阵

tsx 复制代码
<View style={styles.grid}>
  <View style={styles.gridItem}>
    <Empty icon="🛒" title="购物车为空" variant="compact" />
  </View>
  <View style={styles.gridItem}>
    <Empty icon="💬" title="暂无消息" variant="compact" />
  </View>
  <View style={styles.gridItem}>
    <Empty icon="❤️" title="暂无收藏" variant="compact" />
  </View>
  <View style={styles.gridItem}>
    <Empty icon="📦" title="暂无订单" variant="compact" />
  </View>
</View>

用网格布局展示多个场景。每个场景用不同的 emoji 图标,让用户一眼就能区分。这种展示方式也给开发者提供了灵感:原来空状态可以这么玩。

网格样式的实现:

tsx 复制代码
const styles = StyleSheet.create({
  grid: { flexDirection: 'row', flexWrap: 'wrap', marginHorizontal: -8 },
  gridItem: { width: '50%', padding: 8 },
});

flexWrap: 'wrap' 让子元素自动换行,width: '50%' 实现两列布局。负 margin 配合子元素 padding 是经典的网格间距处理技巧。


什么时候该用空状态

空状态不是"没数据就显示"这么简单。以下几种情况特别需要精心设计:

首次使用

用户刚注册,什么数据都没有。这时候的空状态是引导用户的好机会。比如笔记应用可以显示"创建你的第一条笔记",配上醒目的按钮。

搜索无结果

用户主动搜索却没找到,心情可能有点沮丧。空状态应该给出建议:换个关键词试试?检查一下拼写?或者提供热门搜索推荐。

筛选后为空

用户设置了筛选条件,结果列表变空了。这时候要让用户知道"不是没有数据,是筛选条件太严格了",并提供清除筛选的快捷方式。

网络错误

严格来说这不算"空状态",但很多团队会复用 Empty 组件来处理。显示一个断网图标,加上"重试"按钮。

权限不足

用户没有权限查看某些内容。空状态可以解释原因,并引导用户申请权限或联系管理员。


设计上的取舍

这个 Empty 组件做了一些有意思的设计决策:

用 emoji 而不是图标库

优点是零依赖、跨平台一致、开发者容易上手。缺点是样式不可控、不能改颜色。对于空状态这种"点缀性"的场景,emoji 够用了。

固定的按钮样式

action 按钮强制使用 outlinesm,不允许自定义。这是为了保持视觉一致性。如果你需要不同样式的按钮,可以不用 action 属性,自己在外面包一层。

没有图片支持

有些空状态设计会用插画图片,这个组件不支持。如果需要,可以考虑扩展 icon 属性,让它支持 ReactNode 类型。

没有动画

空状态出现时没有淡入效果。对于这种静态展示组件,动画不是必需的,但加上会更精致。


实际项目中的用法

来看几个真实场景的代码示例。

订单列表为空

tsx 复制代码
const OrderList = () => {
  const [orders, setOrders] = useState([]);
  
  if (orders.length === 0) {
    return (
      <Empty
        icon="📦"
        title="还没有订单"
        description="去逛逛,发现喜欢的商品吧"
        action={{
          label: '去购物',
          onPress: () => navigation.navigate('Shop')
        }}
      />
    );
  }
  
  return <FlatList data={orders} renderItem={...} />;
};

这是最常见的用法。先判断数据是否为空,为空就渲染 Empty,否则渲染正常列表。action 按钮引导用户去购物,形成闭环。

搜索结果为空

tsx 复制代码
const SearchResult = ({ keyword, results }) => {
  if (results.length === 0) {
    return (
      <Empty
        icon="🔍"
        title={`未找到"${keyword}"相关内容`}
        description="换个关键词试试?"
        variant="large"
      />
    );
  }
  
  return <ResultList data={results} />;
};

搜索场景用 large 尺寸,因为搜索结果页通常是全屏的。标题里嵌入用户的搜索词,让反馈更具体。

收藏夹为空

tsx 复制代码
const Favorites = () => {
  const { favorites, isLoading } = useFavorites();
  
  if (isLoading) {
    return <Spinner />;
  }
  
  if (favorites.length === 0) {
    return (
      <Empty
        icon="❤️"
        title="收藏夹是空的"
        description="看到喜欢的内容,点击爱心收藏"
      />
    );
  }
  
  return <FavoriteGrid items={favorites} />;
};

注意这里先处理 loading 状态,再处理空状态。顺序很重要,不然用户会先看到空状态闪一下,体验不好。

嵌套在卡片里

tsx 复制代码
const RecentActivity = () => {
  const activities = [];
  
  return (
    <Card title="最近动态">
      {activities.length > 0 ? (
        <ActivityList data={activities} />
      ) : (
        <Empty
          variant="compact"
          icon="📋"
          title="暂无动态"
        />
      )}
    </Card>
  );
};

空状态不一定要占满整个页面。在卡片、侧边栏这种小空间里,用 compact 尺寸刚刚好。


扩展思路

如果你觉得这个 Empty 组件功能不够,这里有几个扩展方向:

支持自定义图标组件

tsx 复制代码
interface EmptyProps {
  icon?: string | React.ReactNode;  // 支持传入组件
  // ...
}

这样就能用 SVG 图标或者 Lottie 动画了。

添加重试功能

tsx 复制代码
interface EmptyProps {
  onRetry?: () => void;  // 重试回调
  retryText?: string;    // 重试按钮文字
  // ...
}

网络错误场景特别需要这个。

支持插槽

tsx 复制代码
interface EmptyProps {
  children?: React.ReactNode;  // 自定义底部内容
  // ...
}

有时候你需要放两个按钮,或者放一段富文本,children 插槽能满足这种需求。


小结

Empty 组件的代码量很小,但覆盖了空状态的核心需求:

  • 图标 + 标题 + 描述的信息层次
  • 可选的操作按钮引导用户
  • 三种尺寸适应不同场景
  • 零配置可用的默认值

写空状态组件的关键不是技术实现,而是理解它的设计目的。空状态是产品和用户沟通的机会,别浪费了。

下次你的列表没数据时,别再显示一片空白了。


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

相关推荐
cn_mengbei18 小时前
鸿蒙PC开发指南:从零配置Qt环境到实战部署完整流程
qt·华为·harmonyos
zhengxianyi51518 小时前
数据大屏-单点登录ruoyi-vue-pro
前端·javascript·vue.js
前端世界18 小时前
从能跑到好跑:基于 DevEco Studio 的鸿蒙应用兼容性实践总结
华为·harmonyos
我想回家种地18 小时前
python期末复习重点
前端·javascript·python
行者9618 小时前
Flutter适配OpenHarmony:高效数据筛选组件的设计与实现
开发语言·前端·flutter·harmonyos·鸿蒙
Van_Moonlight18 小时前
RN for OpenHarmony 实战 TodoList 项目:底部 Tab 栏
javascript·开源·harmonyos
Van_Moonlight18 小时前
RN for OpenHarmony 实战 TodoList 项目:浮动添加按钮 FAB
javascript·开源·harmonyos
frontend_frank19 小时前
脱离 Electron autoUpdater:uni-app跨端更新:Windows+Android统一实现方案
android·前端·javascript·electron·uni-app
hqzing19 小时前
低成本玩转鸿蒙容器的丐版方案
docker·harmonyos