在 React Native 中,实现一个类似 Windows 回收站的功能(即允许用户撤销删除的操作)可以通过多种方式实现,例如使用状态管理、持久化存储或者利用现有的库来辅助。下面我将介绍几种常见的方法来实现这样一个功能:

  1. 使用状态管理

最直接的方法是使用 React 的状态(state)来管理组件的删除状态。你可以创建一个数组来存储所有组件的列表,并在删除时将组件标记为已删除但暂时不从数组中移除。

示例代码:

import React, { useState } from 'react';
import { View, Text, Button, FlatList } from 'react-native';

const RecycleBin = ({ items, onDelete, onUndo }) => {
  return (
    <FlatList
      data={items}
      keyExtractor={item => item.id}
      renderItem={({ item }) => (
        <View>
          <Text>{item.name}</Text>
          <Button title="撤销" onPress={() => onUndo(item.id)} />
        </View>
      )}
    />
  );
};

const App = () => {
  const [items, setItems] = useState([
    { id: '1', name: 'Item 1' },
    { id: '2', name: 'Item 2' },
    { id: '3', name: 'Item 3' }
  ]);
  const [deletedItems, setDeletedItems] = useState([]);

  const handleDelete = (id) => {
    const item = items.find(item => item.id === id);
    setDeletedItems(prev => [...prev, item]);
    setItems(items.filter(item => item.id !== id));
  };

  const handleUndo = (id) => {
    const item = deletedItems.find(item => item.id === id);
    setDeletedItems(deletedItems.filter(item => item.id !== id));
    setItems(prev => [...prev, item]);
  };

  return (
    <View>
      <FlatList
        data={items}
        keyExtractor={item => item.id}
        renderItem={({ item }) => (
          <View>
            <Text>{item.name}</Text>
            <Button title="删除" onPress={() => handleDelete(item.id)} />
          </View>
        )}
      />
      <RecycleBin items={deletedItems} onUndo={handleUndo} />
    </View>
  );
};

export default App;
  1. 使用持久化存储(如 AsyncStorage)

对于需要持久化存储删除项的情况,可以使用 AsyncStorage 来保存删除的项。这样即使在应用重启后,用户也可以从回收站恢复已删除的项。

示例代码:

import AsyncStorage from '@react-native-async-storage/async-storage';
import { useState, useEffect } from 'react';
import { View, Text, Button, FlatList } from 'react-native';

const App = () => {
  const [items, setItems] = useState([]);
  const [deletedItems, setDeletedItems] = useState([]);
  const [isLoading, setIsLoading] = useState(true); // 控制加载状态,防止在数据加载完成前渲染组件导致错误。

  useEffect(() => {
    const loadData = async () => {
      try {
        const storedItems = await AsyncStorage.getItem('items'); // 假设之前已经存储了数据。
        const storedDeletedItems = await AsyncStorage.getItem('deletedItems'); // 假设之前已经存储了数据。
        if (storedItems !== null) { setItems(JSON.parse(storedItems)); }
        if (storedDeletedItems !== null) { setDeletedItems(JSON.parse(storedDeletedItems)); } 
      } catch (e) { console.log(e); } finally { setIsLoading(false); } // 加载完成后设置加载状态为false。 避免在数据加载完成前渲染组件。 
    };
    loadData(); // 调用加载数据的函数。 每次组件挂载时都会尝试加载数据。 可以在卸载组件时清除这些数据以节省空间。 例如:componentWillUnmount(){} 中使用 AsyncStorage.clear()。 但在Hooks中使用时,可以使用useEffect的清理函数。 例如:return () => AsyncStorage.clear(); }。

真实实际场景代码:

// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, Alert } from 'react-native';

// Base64 图标库
const ICONS = {
  trash: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik02IDhoMTJ2MTJhMiAyIDAgMDEtMiAyaC04YTIgMiAwIDAxLTItMlY4eiIvPjxwYXRoIGQ9Ik00IDZoMTZ2Mkg0eiIvPjxwYXRoIGQ9Ik0xMCAxMXY2aDJ2LTZoLTJ6Ii8+PHBhdGggZD0iTTE0IDExdjZoMnYtNkgxNHoiLz48cGF0aCBkPSJNMTAgN2gydjJoLTJWN3oiLz48L3N2Zz4=',
  restore: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiA4djRoLTZ2LTZoMnYyaDR6Ii8+PHBhdGggZD0iTTEyIDE0djRoNnYtMmgtMnYtMmgtNHoiLz48cGF0aCBkPSJNMTIgMTB2LTRoLTJ2NGgtNHYyaDR2Mmg0di0yaC00eiIvPjxwYXRoIGQ9Ik02IDE0aC0ydjJoMnYyaDJ2LTJoMnYtMkg2di0yeiIvPjxwYXRoIGQ9Ik0xOCAxMGgydi0yaC0ydi0yaC0ydjJoLTJ2Mmg0eiIvPjxwYXRoIGQ9Ik02IDZoMTJ2MTJINlY2eiIvPjwvc3ZnPg==',
  delete: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik02IDE5YzAgMS4xLjkgMiAyIDJoOGMxLjEgMCAyLS45IDItMlY3SDZ2MTJ6bTgtMTBoLTR2LTJoNHYyaDB6Ii8+PHBhdGggZD0iTTE4IDRoLTR2MWgtNHYtMUg2Yy0xLjEgMC0yIC45LTIgMnYxYzAgLjYgMSAxLjUgMSAyaDE0YzAgLS41IDEtMS40IDEtMlY2YzAtMS4xLTEuOS0yLTItMnoiLz48L3N2Zz4=',
  file: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik02IDJoMTJ2Nmw2IDZ2MTJINlYyeiIvPjxwYXRoIGQ9Ik0xNCAyaDJ2Nmg2bC02LTZ6Ii8+PC9zdmc+',
  image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik04LjUgMTMuNWwyLjUgMyAzLjUtNC41IDQuNSA2SDVtMTYtMTVIM2MtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxOGMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yeiIvPjwvc3ZnPg==',
  document: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xNCAyaC00djJoLTR2MmgxMlY0aC00VjJ6bS00IDZoMTJ2MUgxMHYtMXptMCAyaDEydjFINTB2LTF6bTAgMmgxMnYxSDEwdi0xem0wIDJoMTJ2MUgxMHYtMXoiLz48L3N2Zz4=',
  folder: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMCA0SDRjLTEuMSAwLTIgLjktMiAydjEyYzAgMS4xLjkgMiAyIDJoMTZjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMmgtOGwtLTItMnoiLz48L3N2Zz4=',
  empty: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiA1LjY5Yy0uMzYgMC0uNy4wNS0xLjA2LjEzbDEuNTkgMS41OGMuMDUuMDUuMTMuMDQuMTgtLjAybDEuNTktMS41OGMuMDUtLjA1LjA0LS4xMy0uMDItLjE4Yy0uMzUtLjA4LS43LS4xMy0xLjA2LS4xM3oiLz48cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMCAxMC00LjQ4IDEwLTEwUzE3LjUyIDIgMTIgMnptMCAxOGMtNC40MSAwLTgtMy41OS04LThzMy41OS04IDgtOCA4IDMuNTkgOCA4LTMuNTkgOC04IDgiLz48L3N2Zz4=',
  close: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSA2LjQxTDE3LjU5IDUgMTIgMTAuNTkgNi40MSA1IDUgNi40MSAxMC41OSAxMiA1IDE3LjU5IDYuNDEgMTkgMTIgMTMuNDEgMTcuNTkgMTkgMTkgMTcuNTkgMTMuNDEgMTJ6Ii8+PC9zdmc+'
};

// 默认文件数据
const DEFAULT_FILES = [
  { id: '1', name: '项目报告.docx', type: 'document', size: '2.4 MB', deletedAt: '2023-06-15 14:30' },
  { id: '2', name: '度假照片.jpg', type: 'image', size: '5.1 MB', deletedAt: '2023-06-14 09:15' },
  { id: '3', name: '财务报表.xlsx', type: 'document', size: '1.2 MB', deletedAt: '2023-06-13 16:45' },
  { id: '4', name: '工作资料', type: 'folder', size: '15.7 MB', deletedAt: '2023-06-12 11:20' },
];

const RecycleBin: React.FC = () => {
  const [files, setFiles] = useState(DEFAULT_FILES);
  const [selectedFile, setSelectedFile] = useState<any>(null);
  const [modalVisible, setModalVisible] = useState(false);

  // 获取文件图标
  const getFileIcon = (fileType: string) => {
    switch (fileType) {
      case 'image': return ICONS.image;
      case 'document': return ICONS.document;
      case 'folder': return ICONS.folder;
      default: return ICONS.file;
    }
  };

  // 获取文件图标颜色
  const getFileIconColor = (fileType: string) => {
    switch (fileType) {
      case 'image': return '#45B7D1';
      case 'document': return '#4361ee';
      case 'folder': return '#f72585';
      default: return '#6c757d';
    }
  };

  // 恢复文件
  const restoreFile = (id: string) => {
    setFiles(files.filter(file => file.id !== id));
    Alert.alert('恢复成功', '文件已恢复到原来位置');
  };

  // 彻底删除文件
  const permanentlyDeleteFile = (id: string) => {
    Alert.alert(
      '彻底删除',
      '确定要彻底删除这个文件吗?此操作无法撤销。',
      [
        { text: '取消', style: 'cancel' },
        { 
          text: '删除', 
          style: 'destructive', 
          onPress: () => {
            setFiles(files.filter(file => file.id !== id));
            Alert.alert('删除成功', '文件已彻底删除');
          } 
        }
      ]
    );
  };

  // 清空回收站
  const clearRecycleBin = () => {
    if (files.length === 0) {
      Alert.alert('提示', '回收站已经是空的');
      return;
    }

    Alert.alert(
      '清空回收站',
      `确定要彻底删除回收站中的全部${files.length}个项目吗?`,
      [
        { text: '取消', style: 'cancel' },
        { 
          text: '清空', 
          style: 'destructive', 
          onPress: () => {
            setFiles([]);
            Alert.alert('清空成功', '回收站已清空');
          } 
        }
      ]
    );
  };

  // 查看文件详情
  const viewFileDetails = (file: any) => {
    setSelectedFile(file);
    setModalVisible(true);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <View style={styles.headerTop}>
          <Text style={styles.title}>回收站</Text>
          <TouchableOpacity 
            style={styles.clearButton}
            onPress={clearRecycleBin}
            disabled={files.length === 0}
          >
            <Text style={[styles.clearButtonText, files.length === 0 && styles.disabledText]}>清空</Text>
          </TouchableOpacity>
        </View>
        <Text style={styles.subtitle}>已删除的文件将在此保留30</Text>
        <View style={styles.statsContainer}>
          <View style={styles.statBox}>
            <Text style={styles.statNumber}>{files.length}</Text>
            <Text style={styles.statLabel}>项目</Text>
          </View>
          <View style={styles.statBox}>
            <Text style={styles.statNumber}>
              {files.reduce((total, file) => {
                const size = parseFloat(file.size);
                return total + (isNaN(size) ? 0 : size);
              }, 0).toFixed(1)} MB
            </Text>
            <Text style={styles.statLabel}>占用空间</Text>
          </View>
        </View>
      </View>

      <ScrollView contentContainerStyle={styles.content}>
        {files.length === 0 ? (
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyIcon}>{decodeURIComponent(escape(atob(ICONS.empty.split(',')[1])))}</Text>
            <Text style={styles.emptyText}>回收站为空</Text>
            <Text style={styles.emptySubtext}>没有找到已删除的文件</Text>
          </View>
        ) : (
          files.map((file) => (
            <View key={file.id} style={styles.fileCard}>
              <View style={styles.fileInfo}>
                <Text style={[styles.fileIcon, { color: getFileIconColor(file.type) }]}>
                  {decodeURIComponent(escape(atob(getFileIcon(file.type).split(',')[1])))}
                </Text>
                <View style={styles.fileDetails}>
                  <Text style={styles.fileName}>{file.name}</Text>
                  <Text style={styles.fileMeta}>{file.size} · {file.deletedAt}</Text>
                </View>
              </View>
              
              <View style={styles.fileActions}>
                <TouchableOpacity 
                  style={[styles.actionButton, styles.restoreButton]}
                  onPress={() => restoreFile(file.id)}
                >
                  <Text style={styles.restoreIcon}>
                    {decodeURIComponent(escape(atob(ICONS.restore.split(',')[1])))}
                  </Text>
                  <Text style={styles.actionText}>恢复</Text>
                </TouchableOpacity>
                
                <TouchableOpacity 
                  style={[styles.actionButton, styles.deleteButton]}
                  onPress={() => permanentlyDeleteFile(file.id)}
                >
                  <Text style={styles.deleteIcon}>
                    {decodeURIComponent(escape(atob(ICONS.delete.split(',')[1])))}
                  </Text>
                  <Text style={styles.actionText}>删除</Text>
                </TouchableOpacity>
                
                <TouchableOpacity 
                  style={[styles.actionButton, styles.infoButton]}
                  onPress={() => viewFileDetails(file)}
                >
                  <Text style={styles.infoIcon}>...</Text>
                </TouchableOpacity>
              </View>
            </View>
          ))
        )}
      </ScrollView>

      {/* 文件详情模态框 */}
      <Modal
        animationType="slide"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => setModalVisible(false)}
      >
        <View style={styles.modalOverlay}>
          <View style={styles.modalContent}>
            <View style={styles.modalHeader}>
              <Text style={styles.modalTitle}>文件详情</Text>
              <TouchableOpacity onPress={() => setModalVisible(false)}>
                <Text style={styles.closeButton}>×</Text>
              </TouchableOpacity>
            </View>
            
            {selectedFile && (
              <View style={styles.modalBody}>
                <Text style={[styles.detailIcon, { color: getFileIconColor(selectedFile.type) }]}>
                  {decodeURIComponent(escape(atob(getFileIcon(selectedFile.type).split(',')[1])))}
                </Text>
                <Text style={styles.detailName}>{selectedFile.name}</Text>
                <View style={styles.detailRow}>
                  <Text style={styles.detailLabel}>文件类型:</Text>
                  <Text style={styles.detailValue}>{selectedFile.type}</Text>
                </View>
                <View style={styles.detailRow}>
                  <Text style={styles.detailLabel}>文件大小:</Text>
                  <Text style={styles.detailValue}>{selectedFile.size}</Text>
                </View>
                <View style={styles.detailRow}>
                  <Text style={styles.detailLabel}>删除时间:</Text>
                  <Text style={styles.detailValue}>{selectedFile.deletedAt}</Text>
                </View>
              </View>
            )}
            
            <View style={styles.modalActions}>
              <TouchableOpacity 
                style={[styles.modalButton, styles.modalRestoreButton]}
                onPress={() => {
                  restoreFile(selectedFile?.id);
                  setModalVisible(false);
                }}
              >
                <Text style={styles.modalButtonText}>恢复文件</Text>
              </TouchableOpacity>
              <TouchableOpacity 
                style={[styles.modalButton, styles.modalDeleteButton]}
                onPress={() => {
                  permanentlyDeleteFile(selectedFile?.id);
                  setModalVisible(false);
                }}
              >
                <Text style={styles.modalButtonText}>彻底删除</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  header: {
    paddingTop: 30,
    paddingBottom: 20,
    paddingHorizontal: 20,
    backgroundColor: '#ffffff',
    borderBottomWidth: 1,
    borderBottomColor: '#e9ecef',
  },
  headerTop: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 8,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#212529',
  },
  clearButton: {
    paddingVertical: 6,
    paddingHorizontal: 12,
    borderRadius: 6,
    backgroundColor: '#e9ecef',
  },
  clearButtonText: {
    fontSize: 14,
    color: '#495057',
    fontWeight: '600',
  },
  disabledText: {
    color: '#adb5bd',
  },
  subtitle: {
    fontSize: 14,
    color: '#6c757d',
    marginBottom: 15,
  },
  statsContainer: {
    flexDirection: 'row',
    backgroundColor: '#e9ecef',
    borderRadius: 12,
    padding: 15,
  },
  statBox: {
    flex: 1,
    alignItems: 'center',
  },
  statNumber: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#212529',
  },
  statLabel: {
    fontSize: 14,
    color: '#6c757d',
    marginTop: 4,
  },
  content: {
    padding: 16,
  },
  emptyContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 60,
  },
  emptyIcon: {
    fontSize: 64,
    color: '#ced4da',
    marginBottom: 20,
  },
  emptyText: {
    fontSize: 20,
    fontWeight: '600',
    color: '#495057',
    marginBottom: 8,
  },
  emptySubtext: {
    fontSize: 16,
    color: '#6c757d',
  },
  fileCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  fileInfo: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 15,
  },
  fileIcon: {
    fontSize: 28,
    marginRight: 15,
  },
  fileDetails: {
    flex: 1,
  },
  fileName: {
    fontSize: 16,
    fontWeight: '600',
    color: '#212529',
    marginBottom: 4,
  },
  fileMeta: {
    fontSize: 14,
    color: '#6c757d',
  },
  fileActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  actionButton: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 8,
    paddingHorizontal: 12,
    borderRadius: 6,
  },
  restoreButton: {
    backgroundColor: '#d1fae5',
  },
  deleteButton: {
    backgroundColor: '#fee2e2',
  },
  infoButton: {
    backgroundColor: '#e0f2fe',
  },
  restoreIcon: {
    fontSize: 16,
    color: '#10b981',
    marginRight: 6,
  },
  deleteIcon: {
    fontSize: 16,
    color: '#ef4444',
    marginRight: 6,
  },
  infoIcon: {
    fontSize: 16,
    color: '#0ea5e9',
  },
  actionText: {
    fontSize: 14,
    fontWeight: '600',
  },
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContent: {
    backgroundColor: '#ffffff',
    width: '85%',
    borderRadius: 20,
    padding: 25,
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 20,
  },
  modalTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#212529',
  },
  closeButton: {
    fontSize: 30,
    color: '#adb5bd',
    fontWeight: '200',
  },
  modalBody: {
    alignItems: 'center',
    marginBottom: 25,
  },
  detailIcon: {
    fontSize: 48,
    marginBottom: 15,
  },
  detailName: {
    fontSize: 18,
    fontWeight: '600',
    color: '#212529',
    marginBottom: 20,
  },
  detailRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '100%',
    marginBottom: 12,
  },
  detailLabel: {
    fontSize: 16,
    color: '#6c757d',
  },
  detailValue: {
    fontSize: 16,
    color: '#212529',
    fontWeight: '600',
  },
  modalActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  modalButton: {
    flex: 1,
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
    marginHorizontal: 5,
  },
  modalRestoreButton: {
    backgroundColor: '#10b981',
  },
  modalDeleteButton: {
    backgroundColor: '#ef4444',
  },
  modalButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#ffffff',
  },
});

export default RecycleBin;

这段代码实现了一个回收站管理界面,主要用于文件管理应用中的已删除文件管理功能。从鸿蒙开发的角度来看,这个组件体现了现代移动应用开发中的核心设计理念。

在数据结构设计上,DEFAULT_FILES数组采用了ID、名称、类型、大小和删除时间的组合方式来定义文件对象。这种设计在鸿蒙应用开发中同样适用,鸿蒙的ArkTS语言支持类似的对象数组结构,可以使用interface来定义文件对象的类型结构,确保数据的一致性和类型安全。鸿蒙开发中推荐使用资源管理机制来处理图标,将图标文件放置在resources目录下,通过$r(‘app.media.icon_name’)方式引用。

在状态管理方面,React使用useState来维护组件状态,包括文件列表、选中文件、模态框显示状态等。鸿蒙开发中可以使用@State装饰器实现类似的状态管理机制,通过状态变量的变更来驱动UI的自动更新。鸿蒙的声明式UI框架同样具有高效的渲染机制,通过状态变化自动计算最小渲染代价来更新界面。

UI布局采用了卡片列表设计,通过ScrollView容器展示文件卡片。在鸿蒙开发中,可以使用Column和Row组合配合ForEach循环渲染来实现类似的列表布局效果。每个文件卡片包含了文件信息展示区和操作按钮区,这种模块化的设计便于维护和扩展。

文件图标处理根据文件类型动态选择不同的图标和颜色,这种设计在鸿蒙应用中同样重要。鸿蒙支持通过条件渲染来实现类似的功能,根据文件类型动态绑定不同的图标资源和样式属性。颜色管理方面,为不同文件类型指定了特定的主题色,这在鸿蒙应用中可以通过资源文件统一管理颜色值。

模态框的实现体现了良好的用户体验设计,通过透明遮罩来突出操作焦点。鸿蒙系统提供了丰富的弹窗组件,可以实现更加原生和一致的用户交互体验。文件详情查看功能可以通过鸿蒙的Sheet组件或者自定义弹窗来实现。

数据持久化方面,虽然代码中没有直接体现,但在实际应用中会将回收站数据存储在本地或云端。鸿蒙提供了多种数据存储方案,包括Preferences轻量级数据存储、KVStore分布式数据存储等,可以根据应用需求选择合适的存储方式。

在交互设计上,恢复文件和彻底删除操作都提供了确认机制,防止误操作。鸿蒙系统有内置的AlertDialog组件,可以提供更加原生和一致的用户交互体验。清空回收站功能通过批量操作来提高用户效率,这种设计在鸿蒙应用中同样适用。

统计信息展示区域显示了回收站中的项目数量和占用空间,这种数据可视化在鸿蒙应用中可以通过Text组件结合状态计算来实现。鸿蒙支持响应式布局,可以根据屏幕尺寸自动调整统计信息的展示方式。

文件操作方面,恢复和删除功能通过过滤数组来更新文件列表,这种数据处理方式在鸿蒙开发中同样适用。鸿蒙的数组操作API提供了丰富的函数式编程支持,可以实现类似的文件管理逻辑。

空状态处理通过条件渲染展示空回收站提示,这种用户体验设计在鸿蒙应用中同样重要。鸿蒙支持通过if-else条件渲染来实现不同的界面状态,确保用户在任何情况下都有清晰的操作指引。


打包

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

在这里插入图片描述

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

在这里插入图片描述

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

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐