欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。


在跨端应用开发领域,React Native 凭借其"一次编写,多端运行"的特性成为主流选择,而鸿蒙(HarmonyOS)作为新兴的分布式操作系统,也与 React Native 有着天然的适配性。本文将深度解读一套面向 React Native/鸿蒙跨端场景设计的组件体系,涵盖骨架屏、进度条、气泡弹出框、懒加载图片及瀑布流通知等核心组件,剖析其设计思路、动画实现及跨端适配要点。

组件

这套组件体系遵循 React Native 组件化设计原则,同时兼顾鸿蒙系统的跨端特性,所有组件均采用 TypeScript 强类型定义,保证代码的可维护性和可扩展性。组件设计上采用"单一职责"原则,每个组件聚焦解决特定问题,同时通过组合模式实现复杂功能,例如瀑布流通知组件整合了懒加载图片、进度条和气泡弹出框等子组件,形成完整的业务单元。

骨架屏组件

骨架屏(Skeleton)是提升移动端应用用户体验的核心组件,尤其在网络请求或数据加载阶段,能够有效减少用户的等待焦虑。本实现中,Skeleton 组件采用 React Native 原生动画库 Animated 实现渐变动画,适配 React Native 与鸿蒙的动画渲染机制。

const Skeleton: React.FC<SkeletonProps> = ({ 
  width = 100, 
  height = 20, 
  borderRadius = 4,
  animate = true
}) => {
  const fadeAnim = useRef(new Animated.Value(0.5)).current;

  useEffect(() => {
    if (animate) {
      const animation = Animated.loop(
        Animated.sequence([
          Animated.timing(fadeAnim, {
            toValue: 1,
            duration: 800,
            useNativeDriver: true,
          }),
          Animated.timing(fadeAnim, {
            toValue: 0.5,
            duration: 800,
            useNativeDriver: true,
          }),
        ])
      );
      animation.start();

      return () => animation.stop();
    }
  }, [animate]);
  
  // 渲染逻辑...
};

在跨端实现中,useNativeDriver: true 是关键优化点——该配置让动画在原生线程执行,而非 JavaScript 线程,这对 React Native 适配鸿蒙系统尤为重要,能够避免 JS 线程阻塞导致的动画卡顿。同时,组件提供了灵活的样式配置(宽高、圆角),适配不同终端的UI规范,无论是手机、平板还是鸿蒙智慧屏,都能通过参数调整实现视觉统一。

SkeletonCard 作为骨架屏的组合组件,演示了如何通过基础 Skeleton 组件构建复杂的占位布局,这种组合模式在跨端开发中尤为重要,能够基于基础组件快速构建符合不同终端交互规范的复合组件。

进度条组件

进度条(ProgressBar)组件是展示任务进度的核心控件,本实现中重点解决了跨端动画适配和数值计算的准确性问题。

const ProgressBar: React.FC<ProgressProps> = ({
  value,
  maxValue = 100,
  height = 8,
  color = '#3b82f6',
  backgroundColor = '#334155',
  borderRadius = 4,
  showPercentage = true,
  animated = true
}) => {
  const [progress, setProgress] = useState(0);
  const progressAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const percentage = (value / maxValue) * 100;
    
    if (animated) {
      Animated.timing(progressAnim, {
        toValue: percentage,
        duration: 500,
        useNativeDriver: false
      }).start();
    } else {
      setProgress(percentage);
    }
  }, [value, maxValue, animated]);
  
  // 渲染逻辑...
};

值得注意的是,进度条的宽度计算采用了 interpolate 插值方法,将 0-100 的数值范围映射为 0%-100% 的宽度范围,这种实现方式在 React Native 和鸿蒙中都能保持一致的表现。而 useNativeDriver: false 的配置则是因为宽度变化属于布局属性,无法通过原生驱动执行,这是跨端动画开发中需要特别注意的细节。

在鸿蒙跨端场景下,进度条组件还考虑了不同设备的交互特性——例如在大屏设备上,进度条高度自动适配,百分比文字大小随设备尺寸调整,这些适配逻辑虽然未在基础实现中完全体现,但组件的参数化设计为后续适配预留了扩展空间。

气泡弹出框组件

气泡弹出框(Popover)是移动端常见的交互组件,本实现通过 Animated 库的 parallel 并行动画实现了缩放和渐变的组合效果,同时精准控制弹出位置。

useEffect(() => {
  if (visible) {
    Animated.parallel([
      Animated.spring(scaleAnim, {
        toValue: 1,
        tension: 300,
        friction: 20,
        useNativeDriver: true
      }),
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 200,
        useNativeDriver: true
      })
    ]).start();
  } else {
    // 隐藏动画逻辑...
  }
}, [visible]);

在跨端实现中,弹出框的位置计算是核心难点。本实现通过触摸事件的 pageX/pageY 获取绝对坐标,再通过偏移量调整弹出位置,这种方式在 React Native 和鸿蒙中都能保证位置的准确性。同时,弹出框采用 Modal 组件实现遮罩层,这是跨端开发中实现浮层的标准方式,能够保证在不同终端上的交互一致性。

懒加载图片组件

图片懒加载(LazyImage)是移动端性能优化的关键,本实现通过状态管理实现了加载中、加载失败和加载完成三种状态的切换,同时适配 React Native 的图片加载机制。

const LazyImage = ({ source, style }: { source: { uri: string }; style: any }) => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  return (
    <View style={style}>
      {loading && !error && (
        <View style={[StyleSheet.absoluteFill, { backgroundColor: '#334155', justifyContent: 'center', alignItems: 'center' }]}>
          <Image source={{ uri: SKELETON_ICONS.loading }} style={{ width: 30, height: 30, tintColor: '#94a3b8' }} />
        </View>
      )}
      {error && (
        // 错误状态渲染...
      )}
      <Image
        source={source}
        style={StyleSheet.absoluteFill}
        onLoad={() => setLoading(false)}
        onError={() => {
          setLoading(false);
          setError(true);
        }}
        resizeMode="cover"
      />
    </View>
  );
};

在跨端场景下,图片懒加载需要考虑不同终端的网络环境和性能特性——例如在鸿蒙的分布式网络环境下,可根据网络类型调整图片加载策略;在低性能设备上,可降低加载中的动画复杂度。本组件的状态分离设计为这些适配策略提供了扩展基础,同时 StyleSheet.absoluteFill 的使用保证了图片在不同终端上的布局一致性。

瀑布流通知组件

瀑布流通知(WaterfallNoticeItem)是上述所有基础组件的集大成者,展示了如何在实际业务场景中组合使用各类组件,同时处理复杂的交互逻辑。

该组件的核心亮点在于:

  1. 长按交互:通过 onLongPress 事件获取触摸坐标,精准定位弹出框位置,适配不同终端的触摸交互特性
  2. 类型化设计:通过 NoticeItem 接口定义数据结构,保证跨端数据传递的一致性
  3. 动态更新:在 App 组件中通过定时器模拟进度更新,展示了如何在跨端应用中处理动态数据变化
useEffect(() => {
  const interval = setInterval(() => {
    setWaterfallNotices(prev => 
      prev.map(notice => {
        if (notice.progress < 100) {
          const increment = Math.random() * 5;
          const newProgress = Math.min(100, notice.progress + increment);
          return { ...notice, progress: newProgress };
        }
        return notice;
      })
    );
  }, 2000);

  return () => clearInterval(interval);
}, []);

在鸿蒙跨端适配中,瀑布流布局还需要考虑不同设备的列数调整——例如在手机上显示2列,平板上显示3列,智慧屏上显示4列,这些适配逻辑可通过 Dimensions API 获取设备尺寸后动态调整 numColumns 属性实现。


这套组件体系在设计时充分考虑了 React Native 与鸿蒙的跨端适配需求,核心体现在以下几个方面:

1. 动画

  • 区分可使用原生驱动的动画(如透明度、缩放)和不可使用原生驱动的动画(如宽度、位置),分别设置 useNativeDriver
  • 采用 React Native 原生动画库,而非第三方动画库,保证鸿蒙适配的兼容性
  • 动画参数化配置,便于根据不同终端调整动画时长和缓动曲线

2. 样式

  • 所有样式属性通过 props 暴露,便于根据终端类型动态调整
  • 使用弹性布局(Flexbox)而非固定尺寸,适配不同屏幕尺寸
  • 颜色值采用主题化设计思路,便于适配不同终端的系统主题

3. 交互

  • 事件处理采用 React Native 标准事件系统,保证跨端交互的一致性
  • 复杂交互(如长按弹出框)通过坐标计算实现,而非依赖特定终端的API
  • 组件状态管理采用 React 原生 Hooks,避免引入复杂的状态管理库,降低跨端适配成本

总结

这套 React Native 组件体系不仅展示了基础UI组件的实现方式,更重要的是提供了一套面向跨端开发的设计思路。在 React Native 适配鸿蒙系统的过程中,核心是保证组件的参数化、状态分离和动画适配

  1. 参数化设计:所有可变属性通过 props 暴露,便于根据不同终端动态调整
  2. 状态分离:将UI状态与业务逻辑分离,保证跨端数据处理的一致性
  3. 动画适配:区分原生驱动和JS驱动的动画类型,优化跨端动画性能

这些设计原则不仅适用于 React Native 与鸿蒙的跨端开发,也适用于其他跨端框架的组件设计。在实际项目中,还需要结合具体的终端特性进行细节适配,例如鸿蒙的分布式数据管理、多设备协同等特性,但基础组件的设计思路是相通的——通过组件化、参数化和标准化,实现真正意义上的跨端复用。


骨架屏组件系统

骨架屏(Skeleton Screen)是现代移动应用中提升用户体验的重要技术,通过在数据加载过程中显示占位元素,减少用户的等待感知。本文将深入分析一个功能完整的 React Native 骨架屏实现。

核心实现

Skeleton 组件采用了 TypeScript 接口定义 props,提供了灵活的配置选项:

interface SkeletonProps {
  width?: number | string;
  height?: number | string;
  borderRadius?: number;
  animate?: boolean;
}

这种设计使得组件可以适应各种尺寸和形状的占位需求。特别值得注意的是,组件支持动画效果,通过 useRefAnimated API 实现了平滑的淡入淡出动画:

const fadeAnim = useRef(new Animated.Value(0.5)).current;

useEffect(() => {
  if (animate) {
    const animation = Animated.loop(
      Animated.sequence([
        Animated.timing(fadeAnim, {
          toValue: 1,
          duration: 800,
          useNativeDriver: true,
        }),
        Animated.timing(fadeAnim, {
          toValue: 0.5,
          duration: 800,
          useNativeDriver: true,
        }),
      ])
    );
    animation.start();

    return () => animation.stop();
  }
}, [animate]);

这里使用 useNativeDriver: true 来提高动画性能,因为透明度动画可以在原生线程中执行,避免了 JavaScript 线程和原生线程之间的通信开销。同时,通过 useEffect 的清理函数,确保在组件卸载时停止动画,防止内存泄漏。

骨架屏卡片

SkeletonCard 组件是对 Skeleton 组件的组合使用,实现了一个完整的卡片骨架屏:

const SkeletonCard: React.FC = () => {
  return (
    <View style={styles.skeletonCard}>
      <Skeleton width="100%" height={150} borderRadius={8} />
      <View style={styles.skeletonCardContent}>
        <Skeleton width="80%" height={20} />
        <Skeleton width="60%" height={16} style={styles.skeletonCardSubtext} />
        <Skeleton width="40%" height={14} style={styles.skeletonCardSubtext} />
      </View>
    </View>
  );
};

这种组合式设计使得骨架屏可以灵活适应不同的 UI 布局,提高了代码的复用性。

进度条组件

进度条是应用中常见的 UI 元素,用于显示任务的完成情况。本文分析的 ProgressBar 组件实现了一个功能丰富、支持动画效果的进度条。

核心

ProgressBar 组件通过 TypeScript 接口定义了丰富的配置选项:

interface ProgressProps {
  value: number;
  maxValue?: number;
  height?: number;
  color?: string;
  backgroundColor?: string;
  borderRadius?: number;
  showPercentage?: boolean;
  animated?: boolean;
}

组件实现了两种进度更新方式:动画更新和直接更新。当 animated 为 true 时,使用 Animated API 实现平滑的进度动画:

const [progress, setProgress] = useState(0);
const progressAnim = useRef(new Animated.Value(0)).current;

useEffect(() => {
  const percentage = (value / maxValue) * 100;
  
  if (animated) {
    Animated.timing(progressAnim, {
      toValue: percentage,
      duration: 500,
      useNativeDriver: false
    }).start();
  } else {
    setProgress(percentage);
  }
}, [value, maxValue, animated]);

这里需要注意的是,由于进度条的宽度变化动画,useNativeDriver 必须设置为 false,因为布局属性的动画不能在原生线程中执行。

进度计算与显示

组件通过计算 valuemaxValue 的比例,得到进度百分比,并支持显示百分比文本:

<Text style={styles.progressPercentage}>
  {Math.round((value / maxValue) * 100)}%
</Text>

同时,通过 interpolate 方法将动画值映射到宽度百分比:

width: animated ? actualProgress.interpolate({
  inputRange: [0, 100],
  outputRange: ['0%', '100%']
}) : `${Math.min(100, Math.max(0, (value / maxValue) * 100))}%`

这种实现方式确保了进度条的显示始终在合理范围内,即使传入的 value 超出了 maxValue 的范围。

气泡弹出框组件

气泡弹出框(Popover)是一种常见的 UI 元素,用于显示上下文相关的选项菜单。本文分析的 Popover 组件实现了一个支持动画效果、可自定义位置的气泡弹出框。

核心实现原理

Popover 组件通过 TypeScript 接口定义了配置选项:

interface PopoverOption {
  id: string;
  label: string;
  icon?: string;
  action: () => void;
}

interface PopoverProps {
  visible: boolean;
  options: PopoverOption[];
  onClose: () => void;
  anchorPosition: { x: number; y: number };
}

组件使用 Modal 组件实现了弹出框的遮罩效果,通过 TouchableOpacity 实现了点击遮罩关闭弹出框的功能:

<Modal transparent={true} animationType="none" visible={visible} onRequestClose={onClose}>
  <TouchableOpacity 
    style={styles.popoverBackdrop} 
    activeOpacity={1} 
    onPress={onClose}
  >
    {/* 弹出框内容 */}
  </TouchableOpacity>
</Modal>
动画效果实现

组件实现了弹出框的缩放和淡入淡出动画效果:

const scaleAnim = useRef(new Animated.Value(0)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;

useEffect(() => {
  if (visible) {
    Animated.parallel([
      Animated.spring(scaleAnim, {
        toValue: 1,
        tension: 300,
        friction: 20,
        useNativeDriver: true
      }),
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 200,
        useNativeDriver: true
      })
    ]).start();
  } else {
    Animated.parallel([
      Animated.timing(scaleAnim, {
        toValue: 0,
        duration: 150,
        useNativeDriver: true
      }),
      Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 150,
        useNativeDriver: true
      })
    ]).start();
  }
}, [visible]);

这里使用了 Animated.parallel 同时执行缩放和淡入淡出动画,使用 useNativeDriver: true 提高动画性能。

位置计算与三角形指示

组件通过 anchorPosition 属性设置弹出框的位置,并添加了三角形指示,增强了视觉效果:

<View style={styles.popoverTriangle} />

这种设计使得弹出框与触发元素之间的关系更加清晰,提高了用户体验。


这些组件在设计时充分考虑了 React Native 与 HarmonyOS 的跨端兼容性:

  1. 组件兼容性:使用的 ViewTextTouchableOpacityModalImage 等组件在 React Native 和 HarmonyOS 中都有对应实现。
  2. API 兼容性:使用的 useStateuseEffectuseRef 等 Hooks 在两个平台上都可用。
  3. 动画 API 兼容性:使用的 Animated API 在两个平台上都有实现,虽然可能存在一些细微的差异,但核心功能是一致的。
  4. 样式兼容性:使用的 StyleSheet API 在两个平台上的使用方式基本一致,flexbox 布局在两个平台上都得到了良好的支持。

跨端建议

  1. 平台特定代码处理:对于一些平台特定的功能,可以使用 Platform.OS 进行条件判断,实现平台特定的优化。
  2. 性能优化:在 HarmonyOS 上,由于硬件和系统的差异,可能需要对动画性能进行额外的优化,如减少动画的复杂度、使用更高效的动画方式等。
  3. 样式适配:不同平台的默认样式可能存在差异,需要进行适当的样式适配,确保在两个平台上的视觉效果一致。
  4. 测试覆盖:在两个平台上进行充分的测试,确保组件在不同平台上的表现一致,特别是动画效果和交互体验。

组件设计模式

  1. 可配置性:所有组件都提供了丰富的配置选项,通过 TypeScript 接口定义了清晰的 props 类型,提高了组件的可复用性和可维护性。
  2. 组合式设计:SkeletonCard 组件通过组合 Skeleton 组件实现了更复杂的 UI,展示了组合式设计的优势。
  3. 动画效果:组件实现了丰富的动画效果,增强了用户体验,同时通过 useNativeDriver 优化了动画性能。
  4. 生命周期管理:使用 useEffect 钩子管理组件的生命周期,特别是动画的启动和停止,避免了内存泄漏。

性能

  1. 动画性能:合理使用 useNativeDriver,对于可以在原生线程中执行的动画(如透明度、缩放),设置 useNativeDriver: true,提高动画性能。
  2. 条件渲染:Popover 组件通过 if (!visible) return null; 实现了条件渲染,当弹出框不可见时,不渲染任何内容,提高了性能。
  3. 内存管理:在 useEffect 的清理函数中停止动画,避免了动画在组件卸载后继续运行导致的内存泄漏。
  4. 样式优化:使用 StyleSheet.create 定义样式,避免了在每次渲染时重新创建样式对象,提高了渲染性能。

代码质量

  1. TypeScript 类型定义:使用 TypeScript 接口定义了组件的 props 类型,提高了代码的可读性和可维护性,减少了运行时错误。
  2. 模块化设计:将不同功能的组件拆分为独立的模块,提高了代码的模块化程度,便于维护和扩展。
  3. 命名规范:使用清晰、语义化的命名,提高了代码的可读性。
  4. 注释:添加了适当的注释,解释了组件的功能和实现细节,提高了代码的可维护性。

骨架屏组件

骨架屏组件可以应用于以下场景:

  1. 列表加载:在加载列表数据时,显示骨架屏占位,减少用户的等待感知。
  2. 详情页加载:在加载详情页数据时,显示骨架屏占位,提供更好的视觉反馈。
  3. 搜索结果加载:在搜索过程中,显示骨架屏占位,提高搜索体验。

进度条组件

进度条组件可以应用于以下场景:

  1. 任务进度:显示任务的完成情况,如文件上传、下载进度等。
  2. 评分显示:显示用户评分或商品评分。
  3. 加载进度:显示应用或页面的加载进度。

气泡弹出框组件

气泡弹出框组件可以应用于以下场景:

  1. 上下文菜单:显示与当前操作相关的选项菜单。
  2. 设置选项:显示应用或功能的设置选项。
  3. 操作菜单:显示与当前元素相关的操作选项,如分享、编辑、删除等。

骨架屏组件

  1. 性能优化:当页面中有多个骨架屏组件时,可以考虑使用 useSharedValuewithTiming 等 React Native Reanimated API,进一步提高动画性能。
  2. 样式扩展:可以添加更多的样式选项,如背景颜色、边框等,提高组件的灵活性。
  3. 内容变体:可以实现更多类型的骨架屏内容,如圆形、椭圆形等,适应不同的 UI 需求。

进度条组件

  1. 动画控制:可以添加动画速度控制选项,让用户可以根据需要调整动画的快慢。
  2. 样式扩展:可以添加更多的样式选项,如边框、阴影等,提高组件的视觉效果。
  3. 标签支持:可以添加标签支持,显示进度的具体数值或文本描述。

气泡弹出框组件

  1. 位置自动计算:可以实现位置自动计算功能,根据触发元素的位置和屏幕边界,自动调整弹出框的位置,避免弹出框超出屏幕范围。
  2. 样式扩展:可以添加更多的样式选项,如背景颜色、边框、阴影等,提高组件的视觉效果。
  3. 交互增强:可以添加更多的交互选项,如长按、滑动等,提高组件的交互体验。

总结

本文分析的三个 React Native 组件展示了如何实现功能丰富、性能优化的 UI 组件,同时考虑了 React Native 与 HarmonyOS 的跨端兼容性。这些组件不仅功能完整,而且代码质量高,是学习 React Native 组件开发的优秀范例。


真实演示案例代码:





// App.tsx
import React, { useState, useRef, useEffect } from 'react';
import { 
  View, 
  Text, 
  StyleSheet, 
  ScrollView, 
  SafeAreaView,
  Image,
  Dimensions,
  TouchableOpacity,
  Animated,
  FlatList,
  Modal
} from 'react-native';

// Base64 Icons for skeleton components
const SKELETON_ICONS = {
  loading: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU1OjQ5KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1NjoxNCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1NjoxNCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjA5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYwOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYwOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjA5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTU6NDlaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
  success: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU2OjIwKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1Njo0NSswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1Njo0NSswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjE5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYxOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYxOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjE5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTY6MjBaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
  error: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU2OjUxKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1NzoxNiswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1NzoxNiswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjI5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYyOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYyOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjI5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTY6NTFaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
  menu: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU3OjIyKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1Nzo0OCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1Nzo0OCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjM5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYzOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYzOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjM5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTc6MjJaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
  settings: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU3OjUzKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODoxOCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODoxOCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjQ5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmY0OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmY0OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjQ5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTc6NTNaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
  share: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU4OjI0KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODo0OSswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODo0OSswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjU5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmY1OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmY1OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjU5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTg6MjRaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......'
};

// 骨架屏组件
interface SkeletonProps {
  width?: number | string;
  height?: number | string;
  borderRadius?: number;
  animate?: boolean;
}

const Skeleton: React.FC<SkeletonProps> = ({ 
  width = 100, 
  height = 20, 
  borderRadius = 4,
  animate = true
}) => {
  const fadeAnim = useRef(new Animated.Value(0.5)).current;

  useEffect(() => {
    if (animate) {
      const animation = Animated.loop(
        Animated.sequence([
          Animated.timing(fadeAnim, {
            toValue: 1,
            duration: 800,
            useNativeDriver: true,
          }),
          Animated.timing(fadeAnim, {
            toValue: 0.5,
            duration: 800,
            useNativeDriver: true,
          }),
        ])
      );
      animation.start();

      return () => animation.stop();
    }
  }, [animate]);

  return (
    <Animated.View
      style={[
        styles.skeleton,
        {
          width,
          height,
          borderRadius,
          opacity: animate ? fadeAnim : 1,
        }
      ]}
    />
  );
};

// 骨架屏卡片组件
const SkeletonCard: React.FC = () => {
  return (
    <View style={styles.skeletonCard}>
      <Skeleton width="100%" height={150} borderRadius={8} />
      <View style={styles.skeletonCardContent}>
        <Skeleton width="80%" height={20} />
        <Skeleton width="60%" height={16} style={styles.skeletonCardSubtext} />
        <Skeleton width="40%" height={14} style={styles.skeletonCardSubtext} />
      </View>
    </View>
  );
};

// 进度条组件
interface ProgressProps {
  value: number;
  maxValue?: number;
  height?: number;
  color?: string;
  backgroundColor?: string;
  borderRadius?: number;
  showPercentage?: boolean;
  animated?: boolean;
}

const ProgressBar: React.FC<ProgressProps> = ({
  value,
  maxValue = 100,
  height = 8,
  color = '#3b82f6',
  backgroundColor = '#334155',
  borderRadius = 4,
  showPercentage = true,
  animated = true
}) => {
  const [progress, setProgress] = useState(0);
  const progressAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const percentage = (value / maxValue) * 100;
    
    if (animated) {
      Animated.timing(progressAnim, {
        toValue: percentage,
        duration: 500,
        useNativeDriver: false
      }).start();
    } else {
      setProgress(percentage);
    }
  }, [value, maxValue, animated]);

  const actualProgress = animated ? progressAnim : progress;

  return (
    <View style={[styles.progressBarContainer, { height, backgroundColor, borderRadius }]}>
      <Animated.View
        style={[
          styles.progressBarFill,
          {
            backgroundColor: color,
            borderRadius,
            width: animated ? actualProgress.interpolate({
              inputRange: [0, 100],
              outputRange: ['0%', '100%']
            }) : `${Math.min(100, Math.max(0, (value / maxValue) * 100))}%`
          }
        ]}
      />
      {showPercentage && (
        <Text style={styles.progressPercentage}>
          {Math.round((value / maxValue) * 100)}%
        </Text>
      )}
    </View>
  );
};

// 气泡弹出框组件
interface PopoverOption {
  id: string;
  label: string;
  icon?: string;
  action: () => void;
}

interface PopoverProps {
  visible: boolean;
  options: PopoverOption[];
  onClose: () => void;
  anchorPosition: { x: number; y: number };
}

const Popover: React.FC<PopoverProps> = ({ visible, options, onClose, anchorPosition }) => {
  const scaleAnim = useRef(new Animated.Value(0)).current;
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    if (visible) {
      Animated.parallel([
        Animated.spring(scaleAnim, {
          toValue: 1,
          tension: 300,
          friction: 20,
          useNativeDriver: true
        }),
        Animated.timing(fadeAnim, {
          toValue: 1,
          duration: 200,
          useNativeDriver: true
        })
      ]).start();
    } else {
      Animated.parallel([
        Animated.timing(scaleAnim, {
          toValue: 0,
          duration: 150,
          useNativeDriver: true
        }),
        Animated.timing(fadeAnim, {
          toValue: 0,
          duration: 150,
          useNativeDriver: true
        })
      ]).start();
    }
  }, [visible]);

  if (!visible) return null;

  return (
    <Modal transparent={true} animationType="none" visible={visible} onRequestClose={onClose}>
      <TouchableOpacity 
        style={styles.popoverBackdrop} 
        activeOpacity={1} 
        onPress={onClose}
      >
        <Animated.View 
          style={[
            styles.popoverContainer,
            {
              top: anchorPosition.y,
              left: anchorPosition.x,
              transform: [{ scale: scaleAnim }],
              opacity: fadeAnim
            }
          ]}
        >
          <View style={styles.popoverTriangle} />
          <View style={styles.popoverContent}>
            {options.map((option, index) => (
              <TouchableOpacity
                key={option.id}
                style={[styles.popoverItem, index === options.length - 1 && styles.lastItem]}
                onPress={() => {
                  option.action();
                  onClose();
                }}
              >
                {option.icon && (
                  <Image source={{ uri: option.icon }} style={styles.popoverItemIcon} />
                )}
                <Text style={styles.popoverItemText}>{option.label}</Text>
              </TouchableOpacity>
            ))}
          </View>
        </Animated.View>
      </TouchableOpacity>
    </Modal>
  );
};

// 懒加载图片组件
const LazyImage = ({ source, style }: { source: { uri: string }; style: any }) => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  return (
    <View style={style}>
      {loading && !error && (
        <View style={[StyleSheet.absoluteFill, { backgroundColor: '#334155', justifyContent: 'center', alignItems: 'center' }]}>
          <Image source={{ uri: SKELETON_ICONS.loading }} style={{ width: 30, height: 30, tintColor: '#94a3b8' }} />
        </View>
      )}
      {error && (
        <View style={[StyleSheet.absoluteFill, { backgroundColor: '#dc2626', justifyContent: 'center', alignItems: 'center' }]}>
          <Image source={{ uri: SKELETON_ICONS.error }} style={{ width: 30, height: 30, tintColor: '#ffffff' }} />
        </View>
      )}
      <Image
        source={source}
        style={StyleSheet.absoluteFill}
        onLoad={() => setLoading(false)}
        onError={() => {
          setLoading(false);
          setError(true);
        }}
        resizeMode="cover"
      />
    </View>
  );
};

// 通知项类型
interface NoticeItem {
  id: string;
  type: 'announcement' | 'warning' | 'info' | 'discount';
  title: string;
  content: string;
  time: string;
  read: boolean;
  progress: number;
  imageUrl?: string;
}

// 瀑布流通知项组件
const WaterfallNoticeItem = ({ notice, onLongPress }: { notice: NoticeItem; onLongPress: (notice: NoticeItem) => void }) => {
  const [anchorPosition, setAnchorPosition] = useState({ x: 0, y: 0 });
  const [showPopover, setShowPopover] = useState(false);

  const handleLongPress = (event: any) => {
    const { pageX, pageY } = event.nativeEvent;
    setAnchorPosition({ x: pageX - 150, y: pageY - 200 }); // Adjust position to center the popover
    setShowPopover(true);
  };

  const popoverOptions: PopoverOption[] = [
    {
      id: 'view-details',
      label: '查看详情',
      icon: SKELETON_ICONS.settings,
      action: () => {
        alert(`正在查看 "${notice.title}" 的详情`);
      }
    },
    {
      id: 'share',
      label: '分享',
      icon: SKELETON_ICONS.share,
      action: () => {
        alert(`正在分享 "${notice.title}"`);
      }
    },
    {
      id: 'more',
      label: '更多操作',
      icon: SKELETON_ICONS.menu,
      action: () => {
        alert(`更多操作: "${notice.title}"`);
      }
    }
  ];

  return (
    <TouchableOpacity 
      style={styles.waterfallNoticeItem} 
      onLongPress={handleLongPress}
    >
      <View style={styles.waterfallNoticeHeader}>
        <View style={[styles.waterfallNoticeTypeBadge, { backgroundColor: `${getTypeColor(notice.type)}20` }]}>
          <Image 
            source={{ uri: getTypeIcon(notice.type) }} 
            style={[styles.waterfallNoticeTypeIcon, { tintColor: getTypeColor(notice.type) }]} 
          />
        </View>
        <View style={styles.waterfallNoticeInfo}>
          <Text style={styles.waterfallNoticeTitle}>{notice.title}</Text>
          <Text style={styles.waterfallNoticeTime}>{notice.time}</Text>
        </View>
        {!notice.read && <View style={styles.unreadDot} />}
      </View>
      
      <Text style={styles.waterfallNoticeContent}>{notice.content}</Text>
      
      <View style={styles.progressContainer}>
        <Text style={styles.progressLabel}>进度: {Math.round(notice.progress)}%</Text>
        <ProgressBar 
          value={notice.progress} 
          maxValue={100} 
          height={6} 
          color={getTypeColor(notice.type)}
          showPercentage={false}
        />
      </View>
      
      {notice.imageUrl && (
        <LazyImage
          source={{ uri: notice.imageUrl }}
          style={styles.waterfallNoticeImage}
        />
      )}
      
      <View style={styles.waterfallNoticeActions}>
        <TouchableOpacity style={styles.waterfallNoticeActionButton}>
          <Image source={{ uri: SKELETON_ICONS.settings }} style={styles.waterfallNoticeActionIcon} />
          <Text style={styles.waterfallNoticeActionText}>设置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.waterfallNoticeActionButton}>
          <Image source={{ uri: SKELETON_ICONS.share }} style={styles.waterfallNoticeActionIcon} />
          <Text style={styles.waterfallNoticeActionText}>分享</Text>
        </TouchableOpacity>
      </View>
      
      <Popover
        visible={showPopover}
        options={popoverOptions}
        onClose={() => setShowPopover(false)}
        anchorPosition={anchorPosition}
      />
    </TouchableOpacity>
  );
};

// 类型辅助函数
const getTypeColor = (type: string) => {
  switch(type) {
    case 'announcement': return '#3b82f6';
    case 'warning': return '#ef4444';
    case 'info': return '#10b981';
    case 'discount': return '#f59e0b';
    default: return '#64748b';
  }
};

const getTypeIcon = (type: string) => {
  switch(type) {
    case 'announcement': return SKELETON_ICONS.settings;
    case 'warning': return SKELETON_ICONS.error;
    case 'info': return SKELETON_ICONS.loading;
    case 'discount': return SKELETON_ICONS.share;
    default: return SKELETON_ICONS.loading;
  }
};

// 主应用组件
const App = () => {
  const [waterfallNotices, setWaterfallNotices] = useState<NoticeItem[]>([
    {
      id: '1',
      type: 'announcement',
      title: '文件下载进度',
      content: '正在下载大型软件包,请耐心等待...',
      time: '刚刚',
      read: false,
      progress: 65,
      imageUrl: 'https://picsum.photos/seed/1/300/200'
    },
    {
      id: '2',
      type: 'warning',
      title: '系统更新',
      content: '系统更新正在进行中,预计还需要15分钟完成',
      time: '2分钟前',
      read: false,
      progress: 30,
      imageUrl: 'https://picsum.photos/seed/2/300/400'
    },
    {
      id: '3',
      type: 'info',
      title: '备份任务',
      content: '正在备份您的照片和文档到云端',
      time: '5分钟前',
      read: true,
      progress: 85,
      imageUrl: 'https://picsum.photos/seed/3/300/300'
    },
    {
      id: '4',
      type: 'discount',
      title: '视频渲染',
      content: '正在处理您的视频文件,渲染完成后将自动保存',
      time: '8分钟前',
      read: false,
      progress: 42,
      imageUrl: 'https://picsum.photos/seed/4/300/250'
    },
    {
      id: '5',
      type: 'info',
      title: '数据同步',
      content: '正在同步您的联系人和日历数据',
      time: '10分钟前',
      read: true,
      progress: 95,
      imageUrl: 'https://picsum.photos/seed/5/300/350'
    },
    {
      id: '6',
      type: 'announcement',
      title: '文件上传',
      content: '正在上传您的文档到服务器',
      time: '15分钟前',
      read: true,
      progress: 20,
      imageUrl: 'https://picsum.photos/seed/6/300/200'
    }
  ]);

  // 模拟进度更新
  useEffect(() => {
    const interval = setInterval(() => {
      setWaterfallNotices(prev => 
        prev.map(notice => {
          // 只对未完成的任务更新进度
          if (notice.progress < 100) {
            const increment = Math.random() * 5; // 随机增加0-5%的进度
            const newProgress = Math.min(100, notice.progress + increment);
            
            return {
              ...notice,
              progress: newProgress
            };
          }
          return notice;
        })
      );
    }, 2000);

    return () => clearInterval(interval);
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>骨架屏组件</Text>
        <Text style={styles.headerSubtitle}>内容加载过程中的占位图形</Text>
      </View>
      
      <ScrollView contentContainerStyle={styles.contentContainer}>
        <View style={styles.skeletonSection}>
          <Text style={styles.sectionTitle}>骨架屏示例</Text>
          
          <View style={styles.skeletonExample}>
            <Skeleton width={60} height={60} borderRadius={30} />
            <View style={styles.skeletonTextContainer}>
              <Skeleton width={150} height={20} style={styles.skeletonText} />
              <Skeleton width={120} height={16} style={styles.skeletonText} />
            </View>
          </View>
          
          <View style={styles.skeletonCardsContainer}>
            <SkeletonCard />
            <SkeletonCard />
          </View>
          
          <View style={styles.skeletonProgressBar}>
            <Skeleton width="100%" height={8} borderRadius={4} />
          </View>
        </View>
        
        <View style={styles.progressBarSection}>
          <Text style={styles.sectionTitle}>进度条示例</Text>
          
          <View style={styles.progressExample}>
            <Text style={styles.progressExampleLabel}>文件下载</Text>
            <ProgressBar value={75} maxValue={100} color="#3b82f6" />
          </View>
          
          <View style={styles.progressExample}>
            <Text style={styles.progressExampleLabel}>系统更新</Text>
            <ProgressBar value={45} maxValue={100} color="#ef4444" />
          </View>
          
          <View style={styles.progressExample}>
            <Text style={styles.progressExampleLabel}>数据备份</Text>
            <ProgressBar value={90} maxValue={100} color="#10b981" />
          </View>
          
          <View style={styles.progressExample}>
            <Text style={styles.progressExampleLabel}>视频渲染</Text>
            <ProgressBar value={30} maxValue={100} color="#f59e0b" />
          </View>
        </View>
        
        <View style={styles.waterfallSection}>
          <Text style={styles.sectionTitle}>瀑布流通知</Text>
          <Text style={styles.sectionSubtitle}>长按任意通知项查看操作菜单</Text>
          
          <FlatList
            data={waterfallNotices}
            renderItem={({ item }) => <WaterfallNoticeItem notice={item} onLongPress={() => {}} />}
            keyExtractor={item => item.id}
            numColumns={2}
            contentContainerStyle={styles.waterfallListContainer}
            showsVerticalScrollIndicator={false}
          />
        </View>
        
        <View style={styles.featuresSection}>
          <Text style={styles.featuresTitle}>功能特性</Text>
          <View style={styles.featureList}>
            <View style={styles.featureItem}>
              <Text style={styles.featureBullet}></Text>
              <Text style={styles.featureText}>内容加载占位</Text>
            </View>
            <View style={styles.featureItem}>
              <Text style={styles.featureBullet}></Text>
              <Text style={styles.featureText}>动态进度展示</Text>
            </View>
            <View style={styles.featureItem}>
              <Text style={styles.featureBullet}></Text>
              <Text style={styles.featureText}>长按触发气泡菜单</Text>
            </View>
            <View style={styles.featureItem}>
              <Text style={styles.featureBullet}></Text>
              <Text style={styles.featureText}>懒加载图片优化性能</Text>
            </View>
            <View style={styles.featureItem}>
              <Text style={styles.featureBullet}></Text>
              <Text style={styles.featureText}>瀑布流布局展示</Text>
            </View>
            <View style={styles.featureItem}>
              <Text style={styles.featureBullet}></Text>
              <Text style={styles.featureText}>丰富的Base64图标库</Text>
            </View>
          </View>
        </View>
        
        <View style={styles.usageSection}>
          <Text style={styles.usageTitle}>使用说明</Text>
          <Text style={styles.usageText}>
            骨架屏组件用于在内容加载过程中展示占位图形,
            提供更好的用户体验,减少加载过程中的空白感。
          </Text>
        </View>
      </ScrollView>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 骨架屏组件. All rights reserved.</Text>
      </View>
    </SafeAreaView>
  );
};

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#0f172a',
  },
  header: {
    backgroundColor: '#1e293b',
    paddingTop: 20,
    paddingBottom: 25,
    paddingHorizontal: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#334155',
  },
  headerTitle: {
    fontSize: 26,
    fontWeight: '700',
    color: '#f1f5f9',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 15,
    color: '#94a3b8',
    textAlign: 'center',
  },
  contentContainer: {
    padding: 16,
  },
  skeletonSection: {
    marginBottom: 20,
  },
  sectionTitle: {
    fontSize: 22,
    fontWeight: '700',
    color: '#f1f5f9',
    marginBottom: 15,
    paddingLeft: 10,
    borderLeftWidth: 4,
    borderLeftColor: '#3b82f6',
  },
  skeletonExample: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
    padding: 15,
    backgroundColor: '#1e293b',
    borderRadius: 12,
  },
  skeletonTextContainer: {
    marginLeft: 15,
    flex: 1,
  },
  skeletonText: {
    marginBottom: 8,
  },
  skeletonCardsContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 20,
  },
  skeletonCard: {
    backgroundColor: '#1e293b',
    borderRadius: 12,
    width: (width - 46) / 2,
    overflow: 'hidden',
    borderWidth: 1,
    borderColor: '#334155',
  },
  skeletonCardContent: {
    padding: 12,
  },
  skeletonCardSubtext: {
    marginTop: 8,
  },
  skeletonProgressBar: {
    padding: 15,
    backgroundColor: '#1e293b',
    borderRadius: 12,
    borderWidth: 1,
    borderColor: '#334155',
  },
  progressBarSection: {
    marginBottom: 20,
  },
  progressExample: {
    marginBottom: 20,
  },
  progressExampleLabel: {
    fontSize: 14,
    color: '#cbd5e1',
    marginBottom: 8,
  },
  waterfallSection: {
    marginBottom: 20,
  },
  sectionSubtitle: {
    fontSize: 14,
    color: '#94a3b8',
    marginBottom: 15,
    paddingLeft: 14,
  },
  waterfallListContainer: {
    padding: 5,
  },
  waterfallNoticeItem: {
    backgroundColor: '#1e293b',
    margin: 5,
    borderRadius: 12,
    overflow: 'hidden',
    borderWidth: 1,
    borderColor: '#334155',
    position: 'relative',
  },
  waterfallNoticeHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
  },
  waterfallNoticeTypeBadge: {
    width: 30,
    height: 30,
    borderRadius: 15,
    justifyContent: 'center',
    alignItems: 'center',
    marginRight: 10,
  },
  waterfallNoticeTypeIcon: {
    width: 16,
    height: 16,
  },
  waterfallNoticeInfo: {
    flex: 1,
  },
  waterfallNoticeTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#f1f5f9',
  },
  waterfallNoticeTime: {
    fontSize: 10,
    color: '#94a3b8',
  },
  waterfallNoticeContent: {
    fontSize: 12,
    color: '#cbd5e1',
    lineHeight: 18,
    paddingHorizontal: 12,
    marginBottom: 10,
  },
  progressContainer: {
    paddingHorizontal: 12,
    marginBottom: 10,
  },
  progressLabel: {
    fontSize: 12,
    color: '#94a3b8',
    marginBottom: 4,
  },
  waterfallNoticeImage: {
    height: 120,
    marginHorizontal: 12,
    marginBottom: 10,
    borderRadius: 8,
  },
  waterfallNoticeActions: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    paddingVertical: 10,
    borderTopWidth: 1,
    borderTopColor: '#334155',
  },
  waterfallNoticeActionButton: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  waterfallNoticeActionIcon: {
    width: 14,
    height: 14,
    tintColor: '#94a3b8',
    marginRight: 4,
  },
  waterfallNoticeActionText: {
    fontSize: 12,
    color: '#94a3b8',
  },
  unreadDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: '#ef4444',
  },
  skeleton: {
    backgroundColor: '#334155',
    marginRight: 8,
  },
  progressBarContainer: {
    backgroundColor: '#334155',
    overflow: 'hidden',
    position: 'relative',
  },
  progressBarFill: {
    height: '100%',
    backgroundColor: '#3b82f6',
  },
  progressPercentage: {
    position: 'absolute',
    right: 10,
    top: '50%',
    transform: [{ translateY: -10 }],
    color: '#ffffff',
    fontSize: 10,
    fontWeight: 'bold',
  },
  popoverBackdrop: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.5)',
  },
  popoverContainer: {
    position: 'absolute',
    zIndex: 1000,
  },
  popoverTriangle: {
    position: 'absolute',
    top: -10,
    left: 20,
    width: 0,
    height: 0,
    backgroundColor: 'transparent',
    borderStyle: 'solid',
    borderLeftWidth: 10,
    borderRightWidth: 10,
    borderBottomWidth: 10,
    borderLeftColor: 'transparent',
    borderRightColor: 'transparent',
    borderBottomColor: '#1e293b',
    transform: [{ rotate: '180deg' }],
  },
  popoverContent: {
    backgroundColor: '#1e293b',
    borderRadius: 8,
    paddingVertical: 4,
    minWidth: 150,
    borderWidth: 1,
    borderColor: '#334155',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 5,
  },
  popoverItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#334155',
  },
  lastItem: {
    borderBottomWidth: 0,
  },
  popoverItemIcon: {
    width: 16,
    height: 16,
    tintColor: '#94a3b8',
    marginRight: 10,
  },
  popoverItemText: {
    fontSize: 14,
    color: '#f1f5f9',
  },
  featuresSection: {
    backgroundColor: '#1e293b',
    borderRadius: 16,
    padding: 20,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: '#334155',
  },
  featuresTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#f1f5f9',
    marginBottom: 15,
    textAlign: 'center',
  },
  featureList: {
    paddingLeft: 10,
  },
  featureItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
  },
  featureBullet: {
    fontSize: 18,
    color: '#3b82f6',
    marginRight: 10,
  },
  featureText: {
    fontSize: 16,
    color: '#cbd5e1',
    flex: 1,
  },
  usageSection: {
    backgroundColor: '#1e293b',
    borderRadius: 16,
    padding: 20,
    borderWidth: 1,
    borderColor: '#334155',
  },
  usageTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#f1f5f9',
    marginBottom: 15,
    textAlign: 'center',
  },
  usageText: {
    fontSize: 16,
    color: '#cbd5e1',
    lineHeight: 24,
    textAlign: 'center',
  },
  footer: {
    paddingVertical: 15,
    alignItems: 'center',
    borderTopWidth: 1,
    borderTopColor: '#334155',
    backgroundColor: '#1e293b',
  },
  footerText: {
    fontSize: 14,
    color: '#94a3b8',
    fontWeight: '500',
  },
});

export default App;


请添加图片描述


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

在这里插入图片描述

最后运行效果图如下显示:

请添加图片描述

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐