HarmonyOS NEXT 文件详情页面开发:操作工具栏与全功能文件操作实现
HarmonyOS NEXT 文件详情页面开发:操作工具栏与全功能文件操作实现
前言
文件详情页面是 HarmonyExplorer 中展示文件完整属性和提供文件操作入口的关键页面。早期的详情页仅实现了信息展示,复制、移动、删除、重命名、分享、收藏等操作按钮虽然存在但未连接到实际逻辑。本文基于完整的实现过程,详细介绍 ActionToolbar 操作工具栏、FileOperationDialog 统一对话框、FileDetailViewModel 操作编排、PreviewCard 预览组件 以及 openFile 文件打开 的完整实现,参考 ArkUI 组件开发文档。
文件详情页从"纯展示"升级到"全功能操作",核心在于将 ActionToolbar 的六个操作按钮与 FileOperationService、ShareService、PreferenceUtil 完整串联。
一、文件详情页布局设计
1.1 页面结构规划
文件详情页采用卡片式布局,从上到下依次展示导航栏、预览区、操作按钮、信息卡片和底部工具栏:
- 顶部导航栏:返回按钮、文件名标题
- 文件预览区:PreviewCard 组件展示文件缩略图
- 打开文件按钮:根据文件类型跳转对应查看器
- 基本信息卡片:文件名、类型、大小、路径
- 时间信息卡片:创建时间、修改时间
- 底部操作工具栏:分享、复制、移动、重命名、收藏、删除

图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 | 音频播放器 |
| 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 操作链路完整性
从用户点击到操作完成,完整的链路如下:
- ActionToolbar 检测点击,通过
onAction回调传递 actionId - FileDetailPage 的
handleAction方法分发到具体处理函数 - FileOperationDialog 展示确认/输入对话框(复制、移动、重命名、删除)
- FileDetailViewModel 调用对应的 Service 执行操作
- FileOperationService / ShareService 执行底层文件操作
- 操作完成后通过 Toast 反馈结果,必要时刷新页面或返回
提示:收藏操作是唯一不需要对话框确认的操作,点击后直接执行状态切换,通过 ActionToolbar 的
isFavorite属性绑定实现图标即时更新。
总结
本文详细介绍了 HarmonyExplorer 文件详情页从纯展示到全功能操作的完整实现过程。核心设计在于 ActionToolbar 的统一操作分发、FileOperationDialog 的对话框复用、FileDetailViewModel 的操作编排三者的协作。六个文件操作(分享、复制、移动、重命名、收藏、删除)均与底层 Service 完整串联,打开文件功能根据类型路由到对应查看器,操作后的状态刷新策略针对不同操作类型分别处理,确保用户体验的连贯性。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)