【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Skeleton 骨架屏(用于在内容加载过程中展示一组占位图形)
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
在跨端应用开发领域,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)是上述所有基础组件的集大成者,展示了如何在实际业务场景中组合使用各类组件,同时处理复杂的交互逻辑。
该组件的核心亮点在于:
- 长按交互:通过
onLongPress事件获取触摸坐标,精准定位弹出框位置,适配不同终端的触摸交互特性 - 类型化设计:通过
NoticeItem接口定义数据结构,保证跨端数据传递的一致性 - 动态更新:在
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 适配鸿蒙系统的过程中,核心是保证组件的参数化、状态分离和动画适配:
- 参数化设计:所有可变属性通过 props 暴露,便于根据不同终端动态调整
- 状态分离:将UI状态与业务逻辑分离,保证跨端数据处理的一致性
- 动画适配:区分原生驱动和JS驱动的动画类型,优化跨端动画性能
这些设计原则不仅适用于 React Native 与鸿蒙的跨端开发,也适用于其他跨端框架的组件设计。在实际项目中,还需要结合具体的终端特性进行细节适配,例如鸿蒙的分布式数据管理、多设备协同等特性,但基础组件的设计思路是相通的——通过组件化、参数化和标准化,实现真正意义上的跨端复用。
骨架屏组件系统
骨架屏(Skeleton Screen)是现代移动应用中提升用户体验的重要技术,通过在数据加载过程中显示占位元素,减少用户的等待感知。本文将深入分析一个功能完整的 React Native 骨架屏实现。
核心实现
Skeleton 组件采用了 TypeScript 接口定义 props,提供了灵活的配置选项:
interface SkeletonProps {
width?: number | string;
height?: number | string;
borderRadius?: number;
animate?: boolean;
}
这种设计使得组件可以适应各种尺寸和形状的占位需求。特别值得注意的是,组件支持动画效果,通过 useRef 和 Animated 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,因为布局属性的动画不能在原生线程中执行。
进度计算与显示
组件通过计算 value 和 maxValue 的比例,得到进度百分比,并支持显示百分比文本:
<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 的跨端兼容性:
- 组件兼容性:使用的
View、Text、TouchableOpacity、Modal、Image等组件在 React Native 和 HarmonyOS 中都有对应实现。 - API 兼容性:使用的
useState、useEffect、useRef等 Hooks 在两个平台上都可用。 - 动画 API 兼容性:使用的
AnimatedAPI 在两个平台上都有实现,虽然可能存在一些细微的差异,但核心功能是一致的。 - 样式兼容性:使用的
StyleSheetAPI 在两个平台上的使用方式基本一致,flexbox 布局在两个平台上都得到了良好的支持。
跨端建议
- 平台特定代码处理:对于一些平台特定的功能,可以使用
Platform.OS进行条件判断,实现平台特定的优化。 - 性能优化:在 HarmonyOS 上,由于硬件和系统的差异,可能需要对动画性能进行额外的优化,如减少动画的复杂度、使用更高效的动画方式等。
- 样式适配:不同平台的默认样式可能存在差异,需要进行适当的样式适配,确保在两个平台上的视觉效果一致。
- 测试覆盖:在两个平台上进行充分的测试,确保组件在不同平台上的表现一致,特别是动画效果和交互体验。
组件设计模式
- 可配置性:所有组件都提供了丰富的配置选项,通过 TypeScript 接口定义了清晰的 props 类型,提高了组件的可复用性和可维护性。
- 组合式设计:SkeletonCard 组件通过组合 Skeleton 组件实现了更复杂的 UI,展示了组合式设计的优势。
- 动画效果:组件实现了丰富的动画效果,增强了用户体验,同时通过
useNativeDriver优化了动画性能。 - 生命周期管理:使用
useEffect钩子管理组件的生命周期,特别是动画的启动和停止,避免了内存泄漏。
性能
- 动画性能:合理使用
useNativeDriver,对于可以在原生线程中执行的动画(如透明度、缩放),设置useNativeDriver: true,提高动画性能。 - 条件渲染:Popover 组件通过
if (!visible) return null;实现了条件渲染,当弹出框不可见时,不渲染任何内容,提高了性能。 - 内存管理:在
useEffect的清理函数中停止动画,避免了动画在组件卸载后继续运行导致的内存泄漏。 - 样式优化:使用
StyleSheet.create定义样式,避免了在每次渲染时重新创建样式对象,提高了渲染性能。
代码质量
- TypeScript 类型定义:使用 TypeScript 接口定义了组件的 props 类型,提高了代码的可读性和可维护性,减少了运行时错误。
- 模块化设计:将不同功能的组件拆分为独立的模块,提高了代码的模块化程度,便于维护和扩展。
- 命名规范:使用清晰、语义化的命名,提高了代码的可读性。
- 注释:添加了适当的注释,解释了组件的功能和实现细节,提高了代码的可维护性。
骨架屏组件
骨架屏组件可以应用于以下场景:
- 列表加载:在加载列表数据时,显示骨架屏占位,减少用户的等待感知。
- 详情页加载:在加载详情页数据时,显示骨架屏占位,提供更好的视觉反馈。
- 搜索结果加载:在搜索过程中,显示骨架屏占位,提高搜索体验。
进度条组件
进度条组件可以应用于以下场景:
- 任务进度:显示任务的完成情况,如文件上传、下载进度等。
- 评分显示:显示用户评分或商品评分。
- 加载进度:显示应用或页面的加载进度。
气泡弹出框组件
气泡弹出框组件可以应用于以下场景:
- 上下文菜单:显示与当前操作相关的选项菜单。
- 设置选项:显示应用或功能的设置选项。
- 操作菜单:显示与当前元素相关的操作选项,如分享、编辑、删除等。
骨架屏组件
- 性能优化:当页面中有多个骨架屏组件时,可以考虑使用
useSharedValue和withTiming等 React Native Reanimated API,进一步提高动画性能。 - 样式扩展:可以添加更多的样式选项,如背景颜色、边框等,提高组件的灵活性。
- 内容变体:可以实现更多类型的骨架屏内容,如圆形、椭圆形等,适应不同的 UI 需求。
进度条组件
- 动画控制:可以添加动画速度控制选项,让用户可以根据需要调整动画的快慢。
- 样式扩展:可以添加更多的样式选项,如边框、阴影等,提高组件的视觉效果。
- 标签支持:可以添加标签支持,显示进度的具体数值或文本描述。
气泡弹出框组件
- 位置自动计算:可以实现位置自动计算功能,根据触发元素的位置和屏幕边界,自动调整弹出框的位置,避免弹出框超出屏幕范围。
- 样式扩展:可以添加更多的样式选项,如背景颜色、边框、阴影等,提高组件的视觉效果。
- 交互增强:可以添加更多的交互选项,如长按、滑动等,提高组件的交互体验。
总结
本文分析的三个 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工程目录去:

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

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



所有评论(0)