HarmonyOS NEXT 文件详情页面开发:操作工具栏与全功能文件操作实现

前言

文件详情页面是 HarmonyExplorer 中展示文件完整属性和提供文件操作入口的关键页面。早期的详情页仅实现了信息展示,复制、移动、删除、重命名、分享、收藏等操作按钮虽然存在但未连接到实际逻辑。本文基于完整的实现过程,详细介绍 ActionToolbar 操作工具栏FileOperationDialog 统一对话框FileDetailViewModel 操作编排PreviewCard 预览组件 以及 openFile 文件打开 的完整实现,参考 ArkUI 组件开发文档

文件详情页从"纯展示"升级到"全功能操作",核心在于将 ActionToolbar 的六个操作按钮与 FileOperationService、ShareService、PreferenceUtil 完整串联。

一、文件详情页布局设计

1.1 页面结构规划

文件详情页采用卡片式布局,从上到下依次展示导航栏、预览区、操作按钮、信息卡片和底部工具栏:

  1. 顶部导航栏:返回按钮、文件名标题
  2. 文件预览区:PreviewCard 组件展示文件缩略图
  3. 打开文件按钮:根据文件类型跳转对应查看器
  4. 基本信息卡片:文件名、类型、大小、路径
  5. 时间信息卡片:创建时间、修改时间
  6. 底部操作工具栏:分享、复制、移动、重命名、收藏、删除

在这里插入图片描述

图1:FileDetailPage 页面布局,底部 ActionToolbar 提供六大文件操作

1.2 信息展示规划

信息类型 展示内容 数据来源
基本信息 文件名、类型、大小、路径 FileInfo
时间信息 创建时间、修改时间 FileInfo
收藏状态 是否已收藏 PreferenceUtil
文件类型 图片/视频/音频/PDF/文本 FileType 枚举

1.3 操作功能规划

操作 触发方式 实现路径 反馈方式
分享 ActionToolbar 点击 ShareService 系统面板拉起
复制 确认对话框 FileOperationService Toast 提示
移动 确认对话框 FileOperationService Toast + 刷新
重命名 输入对话框 FileOperationService Toast + 刷新
收藏 直接执行 PreferenceUtil Toast + 图标切换
删除 确认对话框 FileOperationService Toast + 返回
打开 独立按钮 路由跳转 页面跳转

二、FileDetailPage 页面实现

2.1 页面骨架代码

FileDetailPage 使用 @State 管理 ViewModel 实例,通过 aboutToAppear 生命周期加载文件详情:

// pages/FileDetailPage.ets
@Entry
@Component
struct FileDetailPage {
  @State viewModel: FileDetailViewModel = new FileDetailViewModel();
  private dialogController: CustomDialogController | null = null;
  private pickerParam: DirectoryPickerParam = { destDir: '', isMove: false };

  aboutToAppear(): void {
    LogUtil.info(TAG, 'FileDetailPage appeared');
    const params: Object | undefined = RouterUtil.getParams();
    if (params !== undefined) {
      const paramRecord: Record<string, string> = params as Record<string, string>;
      const pathStr: string = paramRecord['path'];
      if (pathStr !== undefined && pathStr.length > 0) {
        this.viewModel.loadFileDetail(pathStr);
      }
    }
  }

  build() {
    Column() {
      this.buildNavigationBar()
      Scroll() {
        Column() {
          if (this.viewModel.isLoading) {
            this.buildLoadingView()
          } else {
            PreviewCard({ fileInfo: this.viewModel.fileInfo })
            this.buildOpenButton()
            this.buildInfoCard()
            this.buildTimeCard()
          }
        }
      }
      .layoutWeight(1)
      .scrollBar(BarState.Auto)

      ActionToolbar({
        isFavorite: this.viewModel.isFavorite,
        onAction: (actionId: string) => {
          this.handleAction(actionId);
        }
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(AppTheme.COLOR_BG_LIGHT)
  }
}

2.2 路由参数接收

页面通过 RouterUtil.getParams() 接收路由参数,使用显式类型转换避免 ArkTS 严格模式报错:

// 路由参数接口声明
interface DirectoryPickerParam {
  destDir: string;
  isMove: boolean;
}

// 参数接收与类型安全转换
const params: Object | undefined = RouterUtil.getParams();
if (params !== undefined) {
  const paramRecord: Record<string, string> = params as Record<string, string>;
  const pathStr: string = paramRecord['path'];
  if (pathStr !== undefined && pathStr.length > 0) {
    this.viewModel.loadFileDetail(pathStr);
  }
}

提示:ArkTS 严格模式下,RouterUtil.getParams() 返回 Object 类型,必须通过 as 转换为 Record<string, string> 后才能访问属性,直接使用点操作符会报编译错误。

2.3 导航栏实现

导航栏包含返回按钮和页面标题,使用 Row 布局水平排列:

// pages/FileDetailPage.ets
@Builder
buildNavigationBar(): void {
  Row() {
    Image($r('app.media.ic_arrow_left'))
      .width(24)
      .height(24)
      .fillColor(AppTheme.COLOR_TEXT_LIGHT)
      .onClick(() => { RouterUtil.back(); })

    Text('文件详情')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor(AppTheme.COLOR_TEXT_LIGHT)
      .layoutWeight(1)
      .margin({ left: AppTheme.SPACING_SMALL })
  }
  .width('100%')
  .height(56)
  .padding({ left: AppTheme.SPACING_MEDIUM, right: AppTheme.SPACING_MEDIUM })
  .backgroundColor(AppTheme.COLOR_CARD_LIGHT)
  .alignItems(VerticalAlign.Center)
}

三、打开文件功能

3.1 打开文件按钮

"打开文件"按钮位于预览区下方,点击后根据文件类型跳转到对应的查看器页面:

// pages/FileDetailPage.ets
@Builder
buildOpenButton(): void {
  Row() {
    Text('打开文件')
      .fontSize(16)
      .fontColor(AppTheme.COLOR_CARD_LIGHT)
      .fontWeight(FontWeight.Medium)
  }
  .width('100%')
  .height(48)
  .margin({ left: AppTheme.SPACING_MEDIUM, right: AppTheme.SPACING_MEDIUM, top: AppTheme.SPACING_SMALL })
  .justifyContent(FlexAlign.Center)
  .backgroundColor(AppTheme.COLOR_PRIMARY)
  .borderRadius(AppTheme.RADIUS_MEDIUM)
  .onClick(() => {
    this.viewModel.openFile();
  })
}

3.2 文件类型路由分发

ViewModel 中的 openFile 方法根据 FileType 枚举值分发到不同的查看器页面,使用统一的 FileRouteParam 接口传递路径参数:

// viewmodel/FileDetailViewModel.ets
interface FileRouteParam {
  path: string;
}

openFile(): void {
  const param: FileRouteParam = { path: this.filePath };
  switch (this.fileInfo.type) {
    case FileType.IMAGE:
      RouterUtil.push(RouterConstants.ROUTE_IMAGE_VIEWER, param);
      break;
    case FileType.VIDEO:
      RouterUtil.push(RouterConstants.ROUTE_VIDEO_PLAYER, param);
      break;
    case FileType.AUDIO:
      RouterUtil.push(RouterConstants.ROUTE_AUDIO_PLAYER, param);
      break;
    case FileType.PDF:
      RouterUtil.push(RouterConstants.ROUTE_PDF_VIEWER, param);
      break;
    case FileType.TEXT:
      RouterUtil.push(RouterConstants.ROUTE_TEXT_VIEWER, param);
      break;
    default:
      ToastUtil.show('不支持打开此类型文件');
      break;
  }
}

3.3 文件类型与查看器映射

FileType 枚举值 目标路由 说明
IMAGE 2 ROUTE_IMAGE_VIEWER 图片查看器
VIDEO 3 ROUTE_VIDEO_PLAYER 视频播放器
AUDIO 4 ROUTE_AUDIO_PLAYER 音频播放器
PDF 5 ROUTE_PDF_VIEWER PDF 查看器
TEXT 6 ROUTE_TEXT_VIEWER 文本查看器
其他 - Toast 提示 不支持打开

四、FileDetailViewModel 操作编排

4.1 ViewModel 完整结构

FileDetailViewModel 持有三个 Service 实例,分别负责文件读取、文件操作和分享:

// viewmodel/FileDetailViewModel.ets
export class FileDetailViewModel {
  filePath: string = '';
  fileInfo: FileInfo = new FileInfo('', '未知', '', 0, 0, 0, 0, false);
  isLoading: boolean = false;
  permissions: FilePermission = { canRead: true, canWrite: true, canExecute: false };
  isFavorite: boolean = false;

  private service: FileService = new FileService();
  private operationService: FileOperationService = new FileOperationService();
  private shareService: ShareService = new ShareService();
  private recentRepo: RecentRepository = new RecentRepository();
}

4.2 文件详情加载

loadFileDetail 方法负责加载文件属性、检查收藏状态、记录最近访问:

// viewmodel/FileDetailViewModel.ets
async loadFileDetail(path: string): Promise<void> {
  this.isLoading = true;
  this.filePath = path;
  try {
    const exists: boolean = await this.service.exists(path);
    if (!exists) {
      ToastUtil.show('文件不存在');
      this.isLoading = false;
      return;
    }
    const stats = await this.service.listFiles(this.service.getParentPath(path));
    const matched = stats.find((s) => s.path === path);
    if (matched) {
      this.fileInfo = FileInfo.fromStat(matched);
    } else {
      this.fileInfo = new FileInfo(path, this.service.getBaseName(path), path, 0, 0, 0, 0, false);
    }
    await this.checkFavoriteStatus(path);
    await this.recentRepo.addRecentFile(this.fileInfo);
  } catch (err) {
    const errorMsg: string = err instanceof Error ? err.message : String(err);
    LogUtil.error(TAG, 'Failed to load file detail: ' + errorMsg);
    ToastUtil.show('获取文件信息失败');
  } finally {
    this.isLoading = false;
  }
}

4.3 收藏管理

收藏功能使用 PreferenceUtil 持久化收藏记录,采用 JSON 数组存储:

// viewmodel/FileDetailViewModel.ets
interface FavoriteRecord {
  path: string;
  name: string;
  favoriteTime: number;
}

async toggleFavorite(): Promise<boolean> {
  try {
    const json: string = await PreferenceUtil.get('key_favorites', '');
    let records: Array<FavoriteRecord> = new Array<FavoriteRecord>();
    if (json.length > 0) {
      records = JSON.parse(json) as Array<FavoriteRecord>;
    }

    if (this.isFavorite) {
      records = records.filter((r: FavoriteRecord) => r.path !== this.filePath);
      await PreferenceUtil.put('key_favorites', JSON.stringify(records));
      this.isFavorite = false;
      ToastUtil.show('已取消收藏');
    } else {
      const newRecord: FavoriteRecord = {
        path: this.filePath,
        name: this.fileInfo.name,
        favoriteTime: Date.now()
      };
      records.unshift(newRecord);
      await PreferenceUtil.put('key_favorites', JSON.stringify(records));
      this.isFavorite = true;
      ToastUtil.show('收藏成功');
    }
    return true;
  } catch (err) {
    LogUtil.error(TAG, 'Failed to toggle favorite');
    ToastUtil.show('操作失败');
    return false;
  }
}

提示:FavoriteRecord 接口必须显式声明,否则 JSON.parse 的返回值在 ArkTS 严格模式下无法通过类型检查。

4.4 重命名操作

重命名操作通过 FileOperationService.renameFile 实现,成功后同步更新 ViewModel 中的文件路径和名称:

// viewmodel/FileDetailViewModel.ets
async renameFile(newName: string): Promise<boolean> {
  if (newName.length === 0) {
    ToastUtil.show('文件名不能为空');
    return false;
  }
  if (newName === this.fileInfo.name) {
    ToastUtil.show('文件名未改变');
    return false;
  }
  try {
    const parentPath: string = this.service.getParentPath(this.filePath);
    const newPath: string = parentPath + '/' + newName;
    await this.operationService.renameFile(this.filePath, newPath);
    this.filePath = newPath;
    this.fileInfo.name = newName;
    this.fileInfo.path = newPath;
    this.fileInfo.id = newPath;
    ToastUtil.show('重命名成功');
    return true;
  } catch (err) {
    const errorMsg: string = err instanceof Error ? err.message : String(err);
    LogUtil.error(TAG, 'Rename failed: ' + errorMsg);
    ToastUtil.show('重命名失败');
    return false;
  }
}

4.5 复制与移动操作

复制和移动操作的实现结构相似,区别在于移动操作需要更新 ViewModel 中的路径:

// viewmodel/FileDetailViewModel.ets
async copyFile(destDir: string): Promise<boolean> {
  try {
    const destPath: string = destDir + '/' + this.fileInfo.name;
    await this.operationService.copyFile(this.filePath, destPath);
    ToastUtil.show('复制成功');
    return true;
  } catch (err) {
    LogUtil.error(TAG, 'Copy failed: ' + errorMsg);
    ToastUtil.show('复制失败');
    return false;
  }
}

async moveFile(destDir: string): Promise<boolean> {
  try {
    const destPath: string = destDir + '/' + this.fileInfo.name;
    await this.operationService.moveFile(this.filePath, destPath);
    this.filePath = destPath;
    this.fileInfo.path = destPath;
    ToastUtil.show('移动成功');
    return true;
  } catch (err) {
    LogUtil.error(TAG, 'Move failed: ' + errorMsg);
    ToastUtil.show('移动失败');
    return false;
  }
}

4.6 删除操作

删除成功后延迟 500ms 返回上一页,给 Toast 提示留出展示时间:

// viewmodel/FileDetailViewModel.ets
async deleteFile(): Promise<boolean> {
  try {
    await this.operationService.deleteFile(this.filePath);
    ToastUtil.show('删除成功');
    return true;
  } catch (err) {
    LogUtil.error(TAG, 'Delete failed: ' + errorMsg);
    ToastUtil.show('删除失败');
    return false;
  }
}

// 页面层删除后返回
private async executeDelete(): Promise<void> {
  const success: boolean = await this.viewModel.deleteFile();
  if (success) {
    setTimeout(() => {
      RouterUtil.back();
    }, 500);
  }
}

五、ActionToolbar 操作工具栏

5.1 组件设计

ActionToolbar 是文件详情页的核心交互组件,集中展示六个操作按钮,通过 onAction 回调将点击事件传递给页面:

// components/ActionToolbar.ets
interface ActionItem {
  icon: Resource;
  label: string;
  actionId: string;
}

@Component
export struct ActionToolbar {
  onAction: (actionId: string) => void = () => {};
  isFavorite: boolean = false;

  private getActions(): ActionItem[] {
    const actions: ActionItem[] = [
      { icon: $r('app.media.ic_share'), label: '分享', actionId: 'share' },
      { icon: $r('app.media.ic_copy'), label: '复制', actionId: 'copy' },
      { icon: $r('app.media.ic_move'), label: '移动', actionId: 'move' },
      { icon: $r('app.media.ic_edit'), label: '重命名', actionId: 'rename' },
      {
        icon: this.isFavorite ? $r('app.media.ic_favor_filled') : $r('app.media.ic_favor'),
        label: this.isFavorite ? '已收藏' : '收藏',
        actionId: 'favorite'
      },
      { icon: $r('app.media.ic_remove'), label: '删除', actionId: 'delete' },
    ];
    return actions;
  }
}

5.2 动态收藏状态

收藏按钮根据 isFavorite 状态动态切换图标和标签,并通过颜色变化提供视觉反馈:

// components/ActionToolbar.ets
private getItemColor(actionId: string): string {
  if (actionId === 'delete') {
    return '#F44336';
  }
  if (actionId === 'favorite' && this.isFavorite) {
    return AppTheme.COLOR_PRIMARY;
  }
  return '#666666';
}
操作状态 图标 标签 文字颜色
未收藏 ic_favor(空心) 收藏 #666666
已收藏 ic_favor_filled(实心) 已收藏 主题色
删除 ic_remove 删除 #F44336(红色)

5.3 布局实现

ActionToolbar 使用 Row + ForEach 实现等宽分布的操作按钮:

// components/ActionToolbar.ets
build() {
  Row() {
    ForEach(this.getActions(), (action: ActionItem) => {
      Column() {
        Image(action.icon)
          .width(24)
          .height(24)
          .fillColor(this.getItemColor(action.actionId))
        Text(action.label)
          .fontSize(11)
          .fontColor(this.getItemColor(action.actionId))
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .height('100%')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.onAction(action.actionId);
      })
    }, (action: ActionItem) => action.actionId)
  }
  .width('100%')
  .height(72)
  .backgroundColor(AppTheme.COLOR_CARD_LIGHT)
  .borderRadius({ topLeft: AppTheme.RADIUS_MEDIUM, bottomRight: AppTheme.RADIUS_MEDIUM })
  .padding({ left: AppTheme.SPACING_SMALL, right: AppTheme.SPACING_SMALL })
}

提示:ForEach 的 getActions() 方法每次调用都返回新的数组,但 actionId 作为键值保证列表项的稳定性,不会因数组引用变化导致不必要的重渲染。

六、FileOperationDialog 统一对话框

6.1 对话框类型设计

FileOperationDialog 使用 @CustomDialog 装饰器,通过 DialogType 枚举区分四种操作类型:

// components/FileOperationDialog.ets
export enum DialogType {
  COPY = 'copy',
  MOVE = 'move',
  DELETE = 'delete',
  RENAME = 'rename'
}

export interface DialogParams {
  type: DialogType;
  title: string;
  message: string;
  filePath: string;
  defaultValue: string;
}

6.2 对话框 UI 实现

重命名类型显示 TextInput 输入框,其他类型仅显示提示信息和确认/取消按钮:

// components/FileOperationDialog.ets
@CustomDialog
export struct FileOperationDialog {
  params: DialogParams = {
    type: DialogType.DELETE, title: '', message: '', filePath: '', defaultValue: ''
  };
  onConfirm: (value: string) => void = () => {};
  onCancel: () => void = () => {};
  @State inputValue: string = '';
  dialogController: CustomDialogController = new CustomDialogController({ builder: FileOperationDialog({}) });

  aboutToAppear(): void {
    this.inputValue = this.params.defaultValue;
  }

  build() {
    Column() {
      Text(this.params.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(AppTheme.COLOR_TEXT_LIGHT)
        .margin({ bottom: 12 })

      Text(this.params.message)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 16 })

      if (this.params.type === DialogType.RENAME) {
        TextInput({ placeholder: '请输入新名称', text: this.inputValue })
          .fontSize(15)
          .width('100%')
          .height(44)
          .backgroundColor('#F5F5F5')
          .borderRadius(8)
          .onChange((value: string) => { this.inputValue = value; })
          .margin({ bottom: 16 })
      }

      Row() {
        Button('取消')
          .layoutWeight(1)
          .height(40)
          .onClick(() => { this.dialogController.close(); this.onCancel(); })

        Button('确定')
          .layoutWeight(1)
          .height(40)
          .backgroundColor(this.params.type === DialogType.DELETE ? '#F44336' : AppTheme.COLOR_PRIMARY)
          .margin({ left: 12 })
          .onClick(() => { this.dialogController.close(); this.onConfirm(this.inputValue); })
      }
      .width('100%')
    }
    .padding(24)
    .backgroundColor(AppTheme.COLOR_CARD_LIGHT)
    .borderRadius(16)
  }
}

6.3 对话框类型对照

DialogType 触发场景 确认按钮颜色 特殊 UI
RENAME 重命名文件 主题蓝 TextInput 输入框
DELETE 删除文件确认 红色 #F44336
COPY 复制文件确认 主题蓝
MOVE 移动文件确认 主题蓝

七、页面操作分发与对话框集成

7.1 handleAction 操作分发

页面通过 handleAction 方法将 ActionToolbar 的 actionId 分发到对应的处理逻辑:

// pages/FileDetailPage.ets
private handleAction(actionId: string): void {
  LogUtil.info(TAG, 'Action clicked: ' + actionId);
  switch (actionId) {
    case 'share':
      this.shareFile();
      break;
    case 'copy':
      this.showCopyPicker();
      break;
    case 'move':
      this.showMovePicker();
      break;
    case 'rename':
      this.showRenameDialog();
      break;
    case 'favorite':
      this.viewModel.toggleFavorite();
      break;
    case 'delete':
      this.showDeleteDialog();
      break;
    default:
      break;
  }
}

7.2 重命名对话框调用

重命名对话框需要传入当前文件名作为默认值,确认后执行重命名并刷新详情:

// pages/FileDetailPage.ets
private showRenameDialog(): void {
  this.dialogController = new CustomDialogController({
    builder: FileOperationDialog({
      params: {
        type: DialogType.RENAME,
        title: '重命名',
        message: '请输入新的文件名',
        filePath: this.viewModel.filePath,
        defaultValue: this.viewModel.fileInfo.name
      },
      onConfirm: (value: string) => {
        this.executeRename(value);
      },
      onCancel: () => {}
    }),
    autoCancel: true,
    alignment: DialogAlignment.Center
  });
  this.dialogController.open();
}

private async executeRename(newName: string): Promise<void> {
  const success: boolean = await this.viewModel.renameFile(newName);
  if (success) {
    await this.viewModel.loadFileDetail(this.viewModel.filePath);
  }
}

7.3 删除对话框调用

删除确认对话框使用红色确认按钮,删除成功后延迟返回上一页:

// pages/FileDetailPage.ets
private showDeleteDialog(): void {
  this.dialogController = new CustomDialogController({
    builder: FileOperationDialog({
      params: {
        type: DialogType.DELETE,
        title: '删除文件',
        message: '确定要删除「' + this.viewModel.fileInfo.name + '」吗?此操作不可撤销。',
        filePath: this.viewModel.filePath,
        defaultValue: ''
      },
      onConfirm: () => {
        this.executeDelete();
      },
      onCancel: () => {}
    }),
    autoCancel: true,
    alignment: DialogAlignment.Center
  });
  this.dialogController.open();
}

7.4 复制与移动对话框

复制和移动操作共用 showOperationConfirmDialog 方法,通过 pickerParam.isMove 区分操作类型:

// pages/FileDetailPage.ets
private showCopyPicker(): void {
  this.pickerParam = { destDir: AppConstants.getFilesDir(), isMove: false };
  this.showOperationConfirmDialog('复制文件',
    '将「' + this.viewModel.fileInfo.name + '」复制到「我的文件」根目录?');
}

private showMovePicker(): void {
  this.pickerParam = { destDir: AppConstants.getFilesDir(), isMove: true };
  this.showOperationConfirmDialog('移动文件',
    '将「' + this.viewModel.fileInfo.name + '」移动到「我的文件」根目录?');
}

private async executeCopyOrMove(): Promise<void> {
  if (this.pickerParam.isMove) {
    const success: boolean = await this.viewModel.moveFile(this.pickerParam.destDir);
    if (success) {
      await this.viewModel.loadFileDetail(this.viewModel.filePath);
    }
  } else {
    await this.viewModel.copyFile(this.pickerParam.destDir);
  }
}

提示:复制和移动操作的目标目录统一使用应用沙箱根目录 AppConstants.getFilesDir(),简化了交互流程。如需支持用户自选目录,可接入 DocumentViewPicker。

八、PreviewCard 预览组件

8.1 预览组件实现

PreviewCard 根据文件类型展示不同预览效果,图片文件直接显示缩略图:

// components/PreviewCard.ets
@Component
export struct PreviewCard {
  fileInfo: FileInfo = new FileInfo('', '未知', '', 0, 0, 0, 0, false);

  build() {
    Column() {
      if (this.fileInfo.type === FileType.IMAGE) {
        Image(this.fileInfo.path)
          .width('100%')
          .height(200)
          .objectFit(ImageFit.Cover)
          .borderRadius(AppTheme.RADIUS_MEDIUM)
      } else {
        Image(this.getFileIcon())
          .width(80)
          .height(80)
          .objectFit(ImageFit.Contain)
      }
      Text(this.fileInfo.name)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ top: 8 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .width('100%')
    .padding(AppTheme.SPACING_MEDIUM)
    .backgroundColor(AppTheme.COLOR_CARD_LIGHT)
    .borderRadius(AppTheme.RADIUS_MEDIUM)
  }
}

8.2 预览类型适配

文件类型 预览方式 尺寸 适配模式
图片 (IMAGE) 直接显示图片内容 100% x 200 Cover 裁剪
视频 (VIDEO) 显示文件图标 80 x 80 Contain
音频 (AUDIO) 显示文件图标 80 x 80 Contain
PDF/文本 显示文件图标 80 x 80 Contain

九、信息卡片展示

9.1 基本信息卡片

基本信息卡片使用 buildInfoRow Builder 方法构建键值对行:

// pages/FileDetailPage.ets
@Builder
buildInfoCard(): void {
  Column() {
    Text('基本信息')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor(AppTheme.COLOR_TEXT_LIGHT)
      .width('100%')
      .margin({ bottom: AppTheme.SPACING_SMALL })

    this.buildInfoRow('文件名', this.viewModel.fileInfo.name)
    this.buildInfoRow('类型', this.getTypeName())
    this.buildInfoRow('大小', FileUtil.formatFileSize(this.viewModel.fileInfo.size))
    this.buildInfoRow('路径', this.viewModel.fileInfo.path)
  }
  .width('100%')
  .padding(AppTheme.SPACING_MEDIUM)
  .backgroundColor(AppTheme.COLOR_CARD_LIGHT)
  .borderRadius(AppTheme.RADIUS_MEDIUM)
  .margin({ left: AppTheme.SPACING_MEDIUM, right: AppTheme.SPACING_MEDIUM, top: AppTheme.SPACING_SMALL })
}

@Builder
buildInfoRow(label: string, value: string): void {
  Row() {
    Text(label)
      .fontSize(14)
      .fontColor('#999999')
      .width(80)
    Text(value)
      .fontSize(14)
      .fontColor(AppTheme.COLOR_TEXT_LIGHT)
      .layoutWeight(1)
      .textAlign(TextAlign.End)
      .maxLines(3)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .width('100%')
  .padding({ top: 8, bottom: 8 })
}

9.2 文件类型名称映射

getTypeName 方法将 FileType 枚举值映射为中文类型名称:

// pages/FileDetailPage.ets
private getTypeName(): string {
  switch (this.viewModel.fileInfo.type) {
    case 1: return '文件夹';
    case 2: return '图片文件';
    case 3: return '视频文件';
    case 4: return '音频文件';
    case 5: return 'PDF文档';
    case 6: return '文本文件';
    case 7: return '压缩文件';
    default: return '未知类型';
  }
}

十、操作后状态刷新策略

10.1 各操作的状态刷新方式

不同操作对页面状态的影响不同,刷新策略需要区分处理:

操作 ViewModel 状态变更 页面刷新方式
重命名 更新 filePath、fileInfo loadFileDetail 重新加载
移动 更新 filePath、fileInfo.path loadFileDetail 重新加载
复制 无变更 无需刷新
删除 文件已不存在 延迟 500ms 返回上一页
收藏 更新 isFavorite ActionToolbar 自动响应
分享 无变更 无需刷新
打开 无变更 路由跳转

10.2 操作链路完整性

从用户点击到操作完成,完整的链路如下:

  1. ActionToolbar 检测点击,通过 onAction 回调传递 actionId
  2. FileDetailPagehandleAction 方法分发到具体处理函数
  3. FileOperationDialog 展示确认/输入对话框(复制、移动、重命名、删除)
  4. FileDetailViewModel 调用对应的 Service 执行操作
  5. FileOperationService / ShareService 执行底层文件操作
  6. 操作完成后通过 Toast 反馈结果,必要时刷新页面或返回

提示:收藏操作是唯一不需要对话框确认的操作,点击后直接执行状态切换,通过 ActionToolbar 的 isFavorite 属性绑定实现图标即时更新。

总结

本文详细介绍了 HarmonyExplorer 文件详情页从纯展示到全功能操作的完整实现过程。核心设计在于 ActionToolbar 的统一操作分发、FileOperationDialog 的对话框复用、FileDetailViewModel 的操作编排三者的协作。六个文件操作(分享、复制、移动、重命名、收藏、删除)均与底层 Service 完整串联,打开文件功能根据类型路由到对应查看器,操作后的状态刷新策略针对不同操作类型分别处理,确保用户体验的连贯性。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

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

更多推荐