在这里插入图片描述

1. 这个功能解决什么问题

批量导入帮助管理员“快速导入”大量点位或工单数据,核心价值体现在以下维度:

  • 效率提升:替代手动逐条录入,单次可处理成百上千条数据,大幅降低操作成本
  • 流程闭环:覆盖“导入-校验-反馈-记录”全流程,确保数据导入可追溯、可管控
  • 体验优化:通过进度可视化、状态提示,让用户清晰感知导入过程
  • 兼容性预留:虽暂未接入文件选择插件,但UI层预留扩展接口,便于后续对接真实文件解析逻辑

本次实现聚焦四大核心环节:

  • 导入说明:当前工程未引入文件选择插件,仅展示 UI 流程
  • 进度模拟:模拟导入耗时与进度更新
  • 状态管理:防止重复点击
  • 用户反馈:导入完成提示

这个页面是典型的“进度+异步”组合,适合做 LinearProgressIndicator + Future.delayed 的最佳实践示例。

2. 相关文件一览

  • lib/feature_pages.dartBatchImportPage):核心页面载体,包含UI渲染、异步逻辑、状态管理全量代码

3. 状态定义

批量导入页面的核心状态需满足两个基础诉求:

  • 进度感知:用0~1的浮点数精准反映导入进度,支持百分比换算展示
  • 操作管控:通过布尔值控制按钮可点击状态,避免重复触发导入流程
class _BatchImportPageState extends State<BatchImportPage> {
  double _progress = 0.0;
  bool _running = false;
}

状态设计的关键考量:

  • _progress 初始值设为0.0,确保进度条初始状态为空
  • _running 默认为false,保证页面加载完成后按钮可正常点击
  • 两个状态均挂载在State类中,依托setState实现UI实时刷新

4. 导入说明卡片

导入说明卡片的设计目标是“清晰告知、降低误解”,核心设计要点:

  • 视觉引导:用上传图标强化“导入”操作认知
  • 信息透明:明确标注“仅展示UI流程”,避免用户误以为功能缺失
  • 布局规范:采用Card+ListTile组合,贴合Material Design设计规范,保持与整体APP风格统一
Card(
  child: ListTile(
    leading: const Icon(Icons.file_upload_outlined),
    title: const Text('导入来源'),
    subtitle: const Text('当前工程未引入文件选择依赖;此处展示流程UI'),
  ),
)

细节优化说明:

  • leading图标选用Icons.file_upload_outlined(轮廓版),视觉更轻盈,符合移动端设计趋势
  • subtitle文字简洁直白,无专业术语,降低用户理解成本

5. 进度卡片

进度卡片是用户感知导入过程的核心载体,设计逻辑拆解:

  • 信息层级:进度百分比文本在上,进度条居中,操作按钮在下,符合用户视觉浏览顺序
  • 交互管控:按钮状态与_running绑定,运行时自动禁用,从源头避免重复操作
  • 空间留白:通过SizedBox设置合理间距,避免元素拥挤,提升视觉舒适度
Card(
  child: Padding(
    padding: const EdgeInsets.all(12),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('进度: ${(100 * _progress).round()}%'),
        const SizedBox(height: 10),
        LinearProgressIndicator(value: _progress),
      ],
    ),
  ),
)

进度展示的关键细节:

  • 百分比计算用round()取整,避免小数位过多导致视觉杂乱
  • LinearProgressIndicator不设置固定高度,使用默认样式保证兼容性
  • Padding设为12px,与Card组件的内边距规范保持一致
        const SizedBox(height: 12),
        FilledButton(
          onPressed: _running ? null : _start,
          child: Text(_running ? '导入中...' : '开始导入'),
        ),

按钮交互设计要点:

  • 按钮文本动态切换,直观反馈当前操作状态
  • FilledButton为Material 3新增组件,相比RaisedButton视觉层级更清晰
  • onPressed回调通过三元表达式简化逻辑,避免额外的条件判断代码

6. 导入逻辑

导入核心逻辑需兼顾“流程模拟”与“异常防护”,核心设计点:

  • 分步执行:将导入拆分为20个步骤,每步延时120ms,模拟真实的文件解析、数据校验、入库流程
  • 生命周期防护:每次更新状态前检查mounted,避免组件销毁后触发setState导致崩溃
  • 反馈闭环:导入完成后通过SnackBar给出明确提示,完成用户操作闭环
Future<void> _start() async {
  setState(() {
    _running = true;
    _progress = 0.0;
  });
  for (var i = 0; i < 20; i++) {
    await Future<void>.delayed(const Duration(milliseconds: 120));
    if (!mounted) return;

初始化与循环设计说明:

  • 启动时重置进度为0.0,确保每次导入都是全新的进度计数
  • 循环次数设为20次,兼顾“进度粒度”与“总耗时”(20*120ms=2.4秒),符合用户等待心理阈值
  • Future.delayed选用milliseconds级别的延时,模拟更细腻的处理过程
    setState(() => _progress = (i + 1) / 20.0);
  }
  if (!mounted) return;
  setState(() => _running = false);
  ScaffoldMessenger.of(context)
      .showSnackBar(const SnackBar(content: Text('导入完成(模拟)')));
}

收尾逻辑设计要点:

  • 循环结束后再次检查mounted,防止循环过程中组件被销毁
  • 重置_running为false,恢复按钮可点击状态
  • SnackBar提示文字标注“模拟”,与导入说明保持信息一致

7. 完整页面代码(工程真实内容)

完整页面采用“状态管理+UI渲染”分离的设计思路,核心结构拆解:

  • 页面结构:StatefulWidget承载可变状态,符合Flutter“数据驱动UI”的核心思想
  • 方法封装:导入逻辑封装为_start方法,使build方法更简洁,聚焦UI渲染
  • 布局设计:ListView作为根布局,适配小屏设备,避免内容溢出
class BatchImportPage extends StatefulWidget {
  const BatchImportPage({super.key});

  
  State<BatchImportPage> createState() => _BatchImportPageState();
}

StatefulWidget设计说明:

  • 构造方法添加super.key,符合Flutter 3.0+的最佳实践
  • 独立的State类拆分,使状态管理逻辑与页面配置分离
class _BatchImportPageState extends State<BatchImportPage> {
  double _progress = 0.0;
  bool _running = false;

  Future<void> _start() async {
    setState(() {
      _running = true;
      _progress = 0.0;
    });

状态与方法初始化说明:

  • 状态变量声明在State类顶部,便于集中管理和查阅
  • _start方法标记为async,支持异步操作,符合导入流程的耗时特性
    for (var i = 0; i < 20; i++) {
      await Future<void>.delayed(const Duration(milliseconds: 120));
      if (!mounted) return;
      setState(() => _progress = (i + 1) / 20.0);
    }

循环逻辑优化点:

  • 每次循环仅更新进度值,逻辑单一职责,便于维护
  • 进度计算采用(i+1)/20.0,确保最终进度为1.0(100%)
    if (!mounted) return;
    setState(() => _running = false);
    ScaffoldMessenger.of(context)
        .showSnackBar(const SnackBar(content: Text('导入完成(模拟)')));
  }

收尾逻辑说明:

  • SnackBar通过ScaffoldMessenger调用,替代旧版Scaffold.of(context),兼容最新Flutter版本
  • 提示文本简洁明了,用户可快速感知操作结果
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('批量导入(模拟)')),
      body: ListView(
        padding: const EdgeInsets.all(12),
        children: [

页面布局核心设计:

  • Scaffold作为根布局,提供AppBar、body等标准结构
  • ListView的padding设为12px,与内部Card组件的间距形成呼应
  • AppBar标题标注“模拟”,与功能说明保持一致
          Card(
            child: ListTile(
              leading: const Icon(Icons.file_upload_outlined),
              title: const Text('导入来源'),
              subtitle: const Text('当前工程未引入文件选择依赖;此处展示流程UI'),
            ),
          ),

导入说明卡片复用:

  • 直接复用前文定义的Card组件,保证代码一致性
  • ListTile的subtitle明确说明功能状态,降低用户疑惑
          const SizedBox(height: 12),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('进度: ${(100 * _progress).round()}%'),

进度卡片布局说明:

  • CrossAxisAlignment设为start,使文本左对齐,符合阅读习惯
  • 进度百分比通过计算得到,动态展示当前导入进度
                  const SizedBox(height: 10),
                  LinearProgressIndicator(value: _progress),
                  const SizedBox(height: 12),
                  FilledButton(
                    onPressed: _running ? null : _start,
                    child: Text(_running ? '导入中...' : '开始导入'),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

按钮与进度条布局说明:

  • 多个SizedBox分隔元素,保证视觉间距均匀
  • FilledButton的onPressed状态与_running绑定,实现操作管控

8. 批量导入数据模型

复杂的批量导入场景需要完善的数据模型支撑,设计原则:

  • 全量覆盖:包含任务全生命周期的核心信息,从创建到完成
  • 可扩展:预留metadata字段,支持自定义扩展属性
  • 易维护:通过枚举限定状态/类型,避免魔法值
class ImportTask {
  final String id;
  final String fileName;
  final ImportType type;
  final ImportStatus status;
  final DateTime createdAt;

核心字段设计说明:

  • id:唯一标识,采用时间戳+前缀的生成规则(后文实现)
  • fileName:记录导入文件名,便于追溯
  • type/status:枚举类型,限定取值范围,降低错误率
  final DateTime? startedAt;
  final DateTime? completedAt;
  final double progress;
  final int totalRecords;
  final int processedRecords;

时间与进度字段说明:

  • 时间字段采用可空类型(DateTime?),区分“未开始”和“已完成”状态
  • progress保持0~1的浮点数格式,与前端进度条适配
  • 记录数相关字段支持统计成功/失败数量,便于数据核对
  final int successRecords;
  final int failedRecords;
  final List<ImportError> errors;
  final Map<String, dynamic> metadata;

  const ImportTask({
    required this.id,
    required this.fileName,
    required this.type,

错误与扩展字段说明:

  • errors:存储导入错误详情,支持后续展示错误列表
  • metadata:键值对格式,可存储文件大小、导入人等扩展信息
  • 构造方法使用required关键字,强制必填字段,避免空值错误
    required this.status,
    required this.createdAt,
    this.startedAt,
    this.completedAt,
    this.progress = 0.0,
    this.totalRecords = 0,

默认值设计:

  • 非必填字段设置合理默认值,简化对象创建
  • progress默认0.0,符合任务初始状态
  • totalRecords默认0,避免空指针异常
    this.processedRecords = 0,
    this.successRecords = 0,
    this.failedRecords = 0,
    this.errors = const [],
    this.metadata = const {},
  });
}

集合字段默认值:

  • errors和metadata默认空集合,避免调用时判空
  • 构造方法使用const关键字,提升性能
class ImportError {
  final int row;
  final String field;
  final String value;
  final String message;
  final ImportErrorType type;

  const ImportError({
    required this.row,
    required this.field,

错误模型设计要点:

  • row:记录错误行号(Excel行号从2开始),便于用户定位问题
  • field:标记错误字段名,精准提示问题位置
  • message:人性化错误提示,指导用户修正
    required this.value,
    required this.message,
    required this.type,
  });
}

enum ImportType {
  manholes,
  workOrders,
  users,
}

枚举设计说明:

  • ImportType限定导入数据类型,覆盖井盖、工单、用户三大核心场景
  • 枚举值采用小写+下划线命名,符合Dart编码规范
enum ImportStatus {
  pending,
  running,
  completed,
  failed,
  cancelled,
}

enum ImportErrorType {
  validation,
  duplicate,
  format,
  required,
}

状态与错误枚举说明:

  • ImportStatus覆盖任务全生命周期状态,支持取消、失败等异常场景
  • ImportErrorType分类错误类型,便于前端按类型展示不同提示

9. 批量导入状态管理

使用Provider实现跨组件状态管理,核心优势:

  • 响应式:状态变更自动触发UI刷新
  • 解耦:业务逻辑与UI渲染分离
  • 可复用:导入状态可在多个页面共享
class BatchImportProvider extends ChangeNotifier {
  List<ImportTask> _tasks = [];
  ImportTask? _currentTask;
  bool _loading = false;
  String? _error;

状态存储设计:

  • _tasks:存储所有导入任务,支持历史记录展示
  • _currentTask:标记当前正在执行的任务,便于聚焦展示
  • _loading/_error:全局加载状态和错误信息,统一管理
  List<ImportTask> get tasks => _tasks;
  ImportTask? get currentTask => _currentTask;
  bool get loading => _loading;
  String? get error => _error;

Getter方法设计:

  • 对外暴露只读属性,避免外部直接修改状态
  • 保持状态封装性,符合面向对象设计原则
  Future<void> startImport({
    required String fileName,
    required ImportType type,
    required List<Map<String, dynamic>> data,
  }) async {
    final task = ImportTask(
      id: 'IMPORT_${DateTime.now().millisecondsSinceEpoch}',
      fileName: fileName,
      type: type,

任务创建逻辑:

  • 任务ID采用“前缀+时间戳”,确保唯一性
  • 接收fileName/type/data三个核心参数,满足导入基础需求
  • async标记方法,支持异步处理
      status: ImportStatus.pending,
      createdAt: DateTime.now(),
      totalRecords: data.length,
      metadata: {
        'fileSize': data.length,
        'importedBy': 'current_user',
      },
    );

任务初始化:

  • 初始状态设为pending,符合任务执行流程
  • metadata记录文件大小、导入人等基础信息,便于追溯
  • totalRecords从data.length获取,无需额外计算
    _currentTask = task;
    _tasks.insert(0, task);
    notifyListeners();

    await _processImport(task, data);
  }

状态更新与处理:

  • _tasks.insert(0, task)将新任务插入列表头部,符合用户查看习惯
  • notifyListeners()触发UI刷新,展示最新任务状态
  • 调用_processImport方法处理具体导入逻辑,职责分离
  Future<void> _processImport(ImportTask task, List<Map<String, dynamic>> data) async {
    try {
      _currentTask = task.copyWith(
        status: ImportStatus.running,
        startedAt: DateTime.now(),
      );
      notifyListeners();

任务启动处理:

  • 复制任务并更新状态为running,避免修改原对象
  • 记录任务开始时间,完善生命周期数据
  • 再次调用notifyListeners,确保状态同步
      int successCount = 0;
      int failedCount = 0;
      List<ImportError> errors = [];

      for (int i = 0; i < data.length; i++) {
        final record = data[i];
        final progress = (i + 1) / data.length;

循环处理初始化:

  • 初始化计数变量,分别统计成功/失败数量
  • 进度计算基于数据长度,确保进度精准
  • 逐行处理数据,模拟真实导入场景
        try {
          await _processRecord(record, task.type);
          successCount++;
        } catch (e) {
          failedCount++;
          errors.add(ImportError(
            row: i + 2,
            field: 'unknown',
            value: record.toString(),

异常处理逻辑:

  • 捕获单条记录处理异常,不中断整体导入流程
  • 错误行号设为i+2,适配Excel标题行(第一行)的场景
  • 记录错误字段、值和信息,便于用户排查
            message: e.toString(),
            type: ImportErrorType.validation,
          ));
        }

        _currentTask = _currentTask!.copyWith(
          progress: progress,
          processedRecords: i + 1,
          successRecords: successCount,

进度更新:

  • 每次处理完记录后更新任务状态,实时反馈进度
  • 复制任务对象更新,避免直接修改原对象
  • 同步更新已处理、成功、失败数量
          failedRecords: failedCount,
          errors: errors,
        );
        notifyListeners();

        await Future.delayed(const Duration(milliseconds: 100));
      }

延时与刷新:

  • 100ms延时模拟真实数据处理耗时
  • 每次循环后调用notifyListeners,确保UI实时刷新
      _currentTask = _currentTask!.copyWith(
        status: failedCount == 0 ? ImportStatus.completed : ImportStatus.completed,
        progress: 1.0,
        completedAt: DateTime.now(),
      );
      notifyListeners();

任务完成处理:

  • 无论是否有失败,最终状态均设为completed(可扩展为partially_completed)
  • 进度强制设为1.0,确保进度条100%
  • 记录任务完成时间,完善生命周期
    } catch (e) {
      _currentTask = _currentTask!.copyWith(
        status: ImportStatus.failed,
        completedAt: DateTime.now(),
      );
      _error = e.toString();
      notifyListeners();
    }
  }

全局异常捕获:

  • 捕获_processImport方法的全局异常,标记任务为failed
  • 记录错误信息,便于前端展示
  • 确保任务状态最终更新,避免卡死在running状态
  Future<void> _processRecord(Map<String, dynamic> record, ImportType type) async {
    await Future.delayed(const Duration(milliseconds: 50));

    switch (type) {
      case ImportType.manholes:
        _validateManholeRecord(record);
        break;

单条记录处理:

  • 50ms延时模拟数据校验耗时
  • 根据导入类型调用对应校验方法,职责分离
  • switch语句覆盖所有ImportType类型,避免遗漏
      case ImportType.workOrders:
        _validateWorkOrderRecord(record);
        break;
      case ImportType.users:
        _validateUserRecord(record);
        break;
    }
  }

类型分支处理:

  • 每个分支调用独立的校验方法,代码结构清晰
  • 符合“单一职责”原则,便于后续修改某类数据的校验规则
  void _validateManholeRecord(Map<String, dynamic> record) {
    if (!record.containsKey('name') || record['name'].toString().trim().isEmpty) {
      throw Exception('井盖名称不能为空');
    }
    if (!record.containsKey('latitude') || record['latitude'] == null) {
      throw Exception('纬度不能为空');
    }

井盖数据校验:

  • 校验名称、纬度、经度三个核心字段
  • 先检查字段是否存在,再检查值是否有效
  • 抛出人性化异常信息,便于用户理解
    if (!record.containsKey('longitude') || record['longitude'] == null) {
      throw Exception('经度不能为空');
    }
  }

  void _validateWorkOrderRecord(Map<String, dynamic> record) {
    if (!record.containsKey('title') || record['title'].toString().trim().isEmpty) {
      throw Exception('工单标题不能为空');
    }

工单数据校验:

  • 聚焦标题、片区两个核心字段
  • 同样采用“存在性+有效性”双重校验
  • 异常信息与字段对应,精准提示
    if (!record.containsKey('district') || record['district'].toString().trim().isEmpty) {
      throw Exception('片区不能为空');
    }
  }

  void _validateUserRecord(Map<String, dynamic> record) {
    if (!record.containsKey('name') || record['name'].toString().trim().isEmpty) {
      throw Exception('用户名不能为空');
    }

用户数据校验:

  • 校验用户名、邮箱两个核心字段
  • 保持与其他校验方法一致的风格,便于维护
  • 异常信息统一格式,提升用户体验
    if (!record.containsKey('email') || record['email'].toString().trim().isEmpty) {
      throw Exception('邮箱不能为空');
    }
  }

  Future<void> cancelImport() async {
    if (_currentTask == null || _currentTask!.status != ImportStatus.running) {
      return;
    }

取消导入逻辑:

  • 先判断当前任务状态,仅允许取消running状态的任务
  • 提前return避免无效操作,提升性能
  • 符合用户操作直觉,避免取消已完成/失败的任务
    _currentTask = _currentTask!.copyWith(
      status: ImportStatus.cancelled,
      completedAt: DateTime.now(),
    );
    notifyListeners();
  }

取消任务处理:

  • 更新任务状态为cancelled,记录完成时间
  • 调用notifyListeners刷新UI,反馈取消结果
  • 保持与其他状态更新一致的逻辑
  void clearError() {
    _error = null;
    notifyListeners();
  }

  void clearCompleted() {
    _tasks.removeWhere((task) => task.status == ImportStatus.completed);
    notifyListeners();
  }
}

辅助方法设计:

  • clearError:清空全局错误信息,用于错误提示关闭
  • clearCompleted:移除已完成任务,清理历史记录
  • 均调用notifyListeners,确保UI同步更新

10. 高级批量导入组件

高级组件在基础版本上扩展了更多实用功能,核心升级点:

  • 类型选择:支持井盖、工单、用户三种数据类型
  • 数据预览:导入前预览数据,降低错误率
  • 历史记录:展示过往导入任务,便于追溯
  • 模板下载:提供标准模板,规范导入数据格式
class AdvancedBatchImportWidget extends StatefulWidget {
  const AdvancedBatchImportWidget({super.key});

  
  State<AdvancedBatchImportWidget> createState() => _AdvancedBatchImportWidgetState();
}

组件基础结构:

  • 采用StatefulWidget,管理组件内部状态
  • 构造方法添加key参数,符合Flutter最佳实践
class _AdvancedBatchImportWidgetState extends State<AdvancedBatchImportWidget> {
  final _fileController = TextEditingController();
  ImportType _selectedType = ImportType.manholes;
  List<Map<String, dynamic>> _previewData = [];
  bool _showPreview = false;

内部状态管理:

  • _fileController:控制文件选择输入框
  • _selectedType:记录选中的导入类型,默认井盖数据
  • _previewData:存储预览数据,支持导入前核对
  • _showPreview:控制是否展示预览区域
  
  void dispose() {
    _fileController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Consumer<BatchImportProvider>(
      builder: (context, provider, child) {
        return SingleChildScrollView(
          padding: const EdgeInsets.all(16),

生命周期与构建方法:

  • dispose方法释放TextEditingController,避免内存泄漏
  • Consumer监听Provider状态变化,实现响应式更新
  • SingleChildScrollView适配小屏设备,避免内容溢出
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildHeader(context),
              const SizedBox(height: 24),
              _buildImportTypeSection(context),
              const SizedBox(height: 24),

布局结构设计:

  • Column作为根布局,按顺序排列各个功能区
  • 每个功能区间隔24px,保证视觉呼吸感
  • 调用独立方法构建各功能区,代码结构清晰
              _buildFileSelectionSection(context),
              const SizedBox(height: 24),
              if (_showPreview) ...[
                _buildPreviewSection(context),
                const SizedBox(height: 24),
              ],

条件渲染预览区:

  • 仅当_showPreview为true时展示预览区域
  • 采用展开运算符(…),语法简洁
  • 预览区同样间隔24px,保持布局一致性
              _buildCurrentTaskSection(context, provider),
              const SizedBox(height: 24),
              _buildHistorySection(context, provider),
            ],
          ),
        );
      },
    );
  }

任务与历史记录区:

  • 展示当前任务和历史记录,完善功能闭环
  • 传递provider参数,获取导入状态
  • 保持与其他区域一致的间距规范
  Widget _buildHeader(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                CircleAvatar(
                  backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
                  child: Icon(
                    Icons.file_upload,
                    color: Theme.of(context).primaryColor,
                  ),
                ),

头部区域设计:

  • CircleAvatar作为图标容器,视觉层次更丰富
  • 采用主题色作为背景和图标色,保持风格统一
  • Padding设为16px,与整体布局规范一致
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        '批量导入',
                        style: Theme.of(context).textTheme.titleLarge?.copyWith(
                          fontWeight: FontWeight.bold,
                        ),
                      ),

标题与描述设计:

  • Expanded确保文本区域占满剩余空间
  • 使用主题文本样式,保持与APP整体风格一致
  • 标题加粗,提升视觉层级
                      const SizedBox(height: 4),
                      Text(
                        '支持 Excel、CSV 格式文件导入',
                        style: TextStyle(
                          color: Colors.grey.shade600,
                          fontSize: 14,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

描述文本设计:

  • 灰色文本降低视觉权重,突出标题
  • 字体大小14px,符合辅助文本规范
  • 明确标注支持的文件格式,降低用户疑惑
  Widget _buildImportTypeSection(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '导入类型',
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),

导入类型区标题:

  • 标题使用titleMedium样式,加粗突出
  • Card包裹,与其他功能区视觉统一
  • Padding保持16px,布局规范一致
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              children: ImportType.values.map((type) {
                final isSelected = _selectedType == type;
                return FilterChip(
                  label: Text(_getTypeDisplayName(type)),
                  selected: isSelected,

类型选择芯片设计:

  • Wrap布局适配不同屏幕宽度,避免溢出
  • FilterChip替代普通按钮,选中状态更直观
  • spacing设为8px,芯片间距合理
                  onSelected: (selected) {
                    setState(() {
                      _selectedType = type;
                      _previewData.clear();
                      _showPreview = false;
                    });
                  },

芯片选中逻辑:

  • 选中后清空预览数据,避免类型不匹配
  • 隐藏预览区域,引导用户重新预览
  • setState更新状态,触发UI刷新
                  backgroundColor: isSelected 
                      ? Theme.of(context).primaryColor.withOpacity(0.1)
                      : null,
                  selectedColor: Theme.of(context).primaryColor.withOpacity(0.2),
                );
              }).toList(),
            ),

芯片样式设计:

  • 选中状态添加主题色背景,视觉反馈清晰
  • 未选中状态使用默认背景,降低干扰
  • 所有芯片样式统一,提升美观度
            const SizedBox(height: 12),
            Text(
              _getTypeDescription(_selectedType),
              style: TextStyle(
                color: Colors.grey.shade600,
                fontSize: 12,
              ),
            ),
          ],
        ),
      ),
    );
  }

类型描述文本:

  • 12px小号字体,作为辅助说明
  • 灰色文本,不抢焦点
  • 根据选中类型动态展示描述,信息精准
  String _getTypeDisplayName(ImportType type) {
    switch (type) {
      case ImportType.manholes:
        return '井盖数据';
      case ImportType.workOrders:
        return '工单数据';
      case ImportType.users:
        return '用户数据';
    }
  }

类型名称转换:

  • 将枚举值转换为中文名称,提升用户体验
  • switch覆盖所有类型,避免遗漏
  • 返回值简洁,直接用于UI展示
  String _getTypeDescription(ImportType type) {
    switch (type) {
      case ImportType.manholes:
        return '导入井盖位置、状态、维护记录等信息';
      case ImportType.workOrders:
        return '导入工单标题、状态、优先级、分配信息等';
      case ImportType.users:
        return '导入用户基本信息、权限、联系方式等';
    }
  }

类型描述转换:

  • 详细说明每种类型可导入的内容,指导用户操作
  • 描述语言通俗,无专业术语
  • 与类型名称一一对应,信息准确

11. 小结

批量导入的实现展现了 Flutter 开发的几个重要原则:

数据模型:复杂的导入任务结构和错误管理

状态管理:使用 Provider 管理导入状态和进度

用户体验:实时进度、数据预览、错误处理

组件化设计:可复用的导入组件和验证逻辑

交互设计:文件选择、模板下载、任务管理

这样的设计不仅满足了批量导入的基本需求,还为后续的功能扩展(如多文件导入、断点续传、实时同步等)提供了良好的基础架构。


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

Logo

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

更多推荐