December 26, 2025
4 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

背景

完成状态管理重构后,我在实际使用中发现了多个 UI 细节问题,同时需要实现”选择网盘文件发送”功能,该功能与文件移动的选择器可以复用。

一、气泡颜色修复

问题

浅色模式下,聊天气泡背景色与页面背景对比度太低,几乎看不清。

分析

之前使用 primaryContainer,但 Material 3 的 ColorScheme.fromSeed 生成的 primaryContainer 是浅粉色,与我们主题的 primary(橙色)不匹配。

我这边指出:气泡应该使用设置页面 Switch 开关开启时的填充色

在 Material 3 中,Switch 开启时的轨道颜色是 primary,所以气泡也应该使用 primary 带透明度。

解决方案

// chat_widgets.dart - MessageBubble
final bubbleColor = isSent
    ? theme.colorScheme.primary.withValues(alpha: 0.25)  // 与 Switch 轨道一致
    : theme.colorScheme.surfaceContainerHighest;
 
final textColor = theme.colorScheme.onSurface;

二、垃圾桶图标透出问题

问题

由于气泡使用了透明度,滑动删除时底层的垃圾桶图标会透过气泡显示出来。

解决方案

按需渲染:只有在拖动时才渲染垃圾桶图标,未拖动时完全不渲染(不是设为透明)。

// SwipeToDeleteMessage
final isDragging = _dragExtent != 0 || _animController.isAnimating;
 
// 只在拖动时显示,性能更好
if (isDragging)
  Positioned.fill(
    child: Icon(Icons.delete_outline, color: iconColor),
  ),

优点

  1. 解决透出问题
  2. 性能更好(不渲染 > 设透明)

三、纯黑白默认主题

需求

我这边认为 Nord 配色作为”默认”不够”默认”,需要一个纯粹的黑白主题作为系统默认。

实现

新增 defaultDarkdefaultLight 纯黑白配色,Nord 保留为可选项:

// DarkThemeType
enum DarkThemeType {
  defaultDark,  // 纯黑白(新默认)
  nord,         // Nord 风格
  inkSmoke,
  // ...
}
 
// 纯黑白暗色
static const defaultDark = AppColorScheme(
  name: '默认',
  description: '纯黑白,经典简洁',
  primary: Color(0xFFFFFFFF),
  background: Color(0xFF000000),
  surface: Color(0xFF1A1A1A),
  // ...
);
 
// 纯黑白浅色
static const defaultLight = AppColorScheme(
  name: '默认',
  description: '纯黑白,经典简洁',
  primary: Color(0xFF000000),
  background: Color(0xFFFFFFFF),
  surface: Color(0xFFFFFFFF),
  // ...
);

四、通用文件选择器组件

需求

  1. 选择网盘文件发送(聊天中引用文件)
  2. 文件移动时选择目标文件夹

两个功能共用一个选择器组件,区别:

  • 移动文件夹:只显示文件夹
  • 发送文件:显示所有文件和文件夹

设计

创建 FilePickerDialog 组件:

/// 文件选择器模式
enum FilePickerMode {
  folderOnly,  // 只选择文件夹(移动/复制)
  fileOnly,    // 选择文件(发送)
  all,         // 选择所有
}
 
/// 使用示例
// 选择文件夹
final path = await FilePickerDialog.pickFolder(context, title: '选择目标');
 
// 选择文件
final file = await FilePickerDialog.pickFile(context, title: '选择文件');

特点

  1. 复用现有风格:使用与文件列表一致的图标和颜色
  2. 导航支持:可以进入子文件夹,有面包屑导航
  3. 美观的弹窗:底部弹出,占屏幕 70% 高度
  4. 排除选项:可以排除已选中的文件(防止自己移动到自己)

文件结构

client/lib/ui/widgets/
├── file_picker_dialog.dart // 新增
├── chat_widgets.dart
└── app_bar_widgets.dart

五、聊天发送网盘文件

需求

我更希望在聊天中可以直接引用已有的网盘文件发送,而不是每次都重新上传。

实现

在附件面板添加”网盘文件”选项:

// _buildAttachmentPanel
Row(
  children: [
    _buildAttachmentOption(icon: Icons.camera_alt_outlined, label: '拍照', ...),
    _buildAttachmentOption(icon: Icons.photo_library_outlined, label: '照片', ...),
    _buildAttachmentOption(icon: Icons.upload_file_outlined, label: '上传文件', ...),
  ],
),
const SizedBox(height: 12),
Row(
  children: [
    _buildAttachmentOption(icon: Icons.cloud_outlined, label: '网盘文件', onTap: _pickCloudFile),
    // 占位
  ],
),

发送逻辑

Future<void> _sendCloudFileMessage(FileMetadata file) async {
  // 直接引用已有的 fileId,不需要上传
  final result = await appState.api.addSendMessage(
    sessionId: widget.session.id,
    type: 'file',
    fileId: file.id,
    fileName: file.name,
    fileSize: file.size,
    isLocalFile: false,  // 标记为网盘文件
  );
  // ...
}

优点

  • 无需重复上传,节省流量和时间
  • 复用已有的预览逻辑

六、替换文件移动选择器

之前

使用简陋的 SimpleDialog,所有文件夹平铺显示,没有层级导航:

SimpleDialog(
  title: const Text('选择目标文件夹'),
  children: [
    SimpleDialogOption(child: Text('/'), ...),
    SimpleDialogOption(child: Text('/folder1/'), ...),
    // ...
  ],
)

之后

使用新的 FilePickerDialog,支持导航进入子文件夹:

Future<String?> _pickTargetPath(AppState state) async {
  return await FilePickerDialog.pickFolder(
    context,
    title: '选择目标文件夹',
    allowRoot: true,
    excludeIds: _selectedIds,
  );
}

修改的文件

文件修改内容
chat_widgets.dart气泡颜色使用 primary.withOpacity;垃圾桶按需渲染
app_theme.dart新增纯黑白默认主题;Nord 改为可选
file_picker_dialog.dart新建通用文件选择器组件
chat_page.dart添加”网盘文件”入口和发送逻辑
files_page.dart使用新选择器替换 SimpleDialog

七、文件卡片上传进度显示

需求

我更希望在发送文件时,文件卡片能够显示上传进度,而不是只显示发送中的外部指示器。

实现

  1. 扩展 FileMessageContent 组件,添加进度显示功能
  2. chat_page.dart 中跟踪上传进度
  3. 当文件正在上传时,文件卡片内部显示进度环
// FileMessageContent 新增参数
FileMessageContent(
  fileName: message.fileName ?? '未知文件',
  fileSize: message.fileSize,
  icon: getFileIcon(getFileType(message.fileName ?? '')),
  isSent: true,
  uploadProgress: progress,  // 新增进度参数
)
 
// 在聊天页面中跟踪上传进度
final Map<String, double> _uploadProgress = {};
 
// 构建消息气泡时检查进度
final progress = _uploadProgress[message.id];
bubble = MessageBubble(
  // ... 其他参数
  onTap: progress == null ? () => _openFile(message) : null,  // 上传中时不可点击
  customContent: FileMessageContent(
    // ... 参数
    uploadProgress: progress,
  ),
);

进度环设计

  • 上传中显示圆形进度指示器
  • 进度环中心显示暂停图标
  • 点击进度环可以暂停上传
  • 进度文字显示”上传中 XX%“

八、修复状态栏颜色问题

问题

聊天页面进入后会改变状态栏字体颜色,导致浅色模式下状态栏字体变成白色,与背景融为一体。

解决方案

AppBar 中设置 systemOverlayStyle 属性:

AppBar(
  // ... 其他参数
  systemOverlayStyle: isDark
      ? SystemUiOverlayStyle.light  // 深色背景使用白色图标
      : SystemUiOverlayStyle.dark,  // 浅色背景使用黑色图标
)

效果

  • 状态栏图标颜色与页面背景协调
  • 与其他页面保持一致的视觉效果

总结

本次优化解决了几个我这边用的时候发现的 UI 细节问题:

  1. 气泡颜色 - 使用与 Switch 一致的 primary 色带透明度
  2. 垃圾桶透出 - 按需渲染,性能更好
  3. 默认主题 - 纯黑白更符合”默认”定位
  4. 文件上传进度 - 文件卡片显示内部进度环
  5. 状态栏颜色 - 修复状态栏图标颜色问题

同时实现了文件选择器复用架构,一个组件支持两种使用场景(选文件夹/选文件),提升了代码复用性和使用体验。