鸿蒙PC平台 Dolphin 文件管理器适配实战:从 Linux KDE 到 HarmonyOS 的 Electron 迁移指南
Dolphin 原本是 KDE 桌面环境下的默认文件管理器,以简洁高效的双面板设计和丰富的文件操作功能著称。本文记录将其核心功能迁移到鸿蒙平台、采用 Electron 壳方案实现的完整流程,帮助开发者理解跨平台文件管理器的适配要点。
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_dolphin
一、技术架构分析
1.1 原始架构(Linux KDE)
- 技术栈:C++ / Qt / KDE Frameworks
- 核心逻辑:基于 Qt 的文件系统操作,通过 KIO 抽象层访问本地和网络文件
- UI 框架:Qt Widgets + KDE Breeze 主题
1.2 目标架构(鸿蒙 Electron)
- 技术栈:Electron + HTML/CSS/JavaScript + 鸿蒙 web_engine 模块
- 核心逻辑:通过 Electron 的 Node.js 文件系统 API 操作文件,IPC 通道隔离主进程与渲染进程
- UI 框架:纯 HTML/CSS 模拟 KDE Breeze 风格
1.3 架构差异
| 对比项 | KDE 原版 | 鸿蒙适配版 |
|---|---|---|
| UI 框架 | Qt Widgets | HTML/CSS |
| 文件操作 | KIO / QFile | Node.js fs 模块 |
| 进程模型 | 单进程 + KIO Slave | 主进程 + 渲染进程 + IPC |
| 主题系统 | KDE Breeze | CSS 变量模拟 |
| 部署方式 | RPM / Flatpak | HAP 包(鸿蒙应用) |
二、环境准备
2.1 开发环境要求
- 操作系统:Windows 10/11 或 macOS
- 开发工具:DevEco Studio(鸿蒙官方 IDE)
- HarmonyOS SDK:API 15
- Node.js:v24+(Electron 依赖)
2.2 项目结构
ohos_hap/
├── electron-apps/
│ └── Dolphin/ # Dolphin Electron 应用源码
│ ├── main.js # Electron 主进程(IPC 通道)
│ ├── renderer.js # 渲染进程(UI 逻辑)
│ ├── index.html # HTML 结构
│ ├── package.json # 项目配置
│ └── styles/
│ └── dolphin.css # KDE Breeze 风格主题
├── web_engine/ # 鸿蒙 web_engine 模块
│ └── src/main/resources/
│ └── resfile/resources/app/ # 部署目录
│ ├── main.js
│ ├── renderer.js
│ ├── index.html
│ ├── package.json
│ └── styles/dolphin.css
└── build-profile.json5 # 鸿蒙构建配置
三、核心适配流程
3.1 第一步:创建主进程与 IPC 通道
文件:electron-apps/Dolphin/main.js
const { app, BrowserWindow, ipcMain, dialog, screen } = require('electron');
const fs = require('fs');
const path = require('path');
let mainWindow = null;
function createWindow() {
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
mainWindow = new BrowserWindow({
width: Math.floor(width * 0.9),
height: Math.floor(height * 0.85),
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(createWindow);
主进程通过 ipcMain.handle 注册 9 个 IPC 通道,覆盖文件管理器的全部文件系统操作:
// 打开文件夹对话框
ipcMain.handle('dialog:openFolder', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
});
return result;
});
// 读取目录内容(含详细信息)
ipcMain.handle('dir:read', async (event, dirPath) => {
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
const items = [];
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
try {
const stat = fs.statSync(path.join(dirPath, entry.name));
items.push({
name: entry.name,
path: path.join(dirPath, entry.name),
isDirectory: entry.isDirectory(),
size: stat.size,
modified: stat.mtime.toISOString(),
created: stat.birthtime.toISOString()
});
} catch (e) {
// 跳过无法访问的文件
}
}
// 文件夹在前,按名称排序
items.sort((a, b) => {
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
return a.name.localeCompare(b.name, 'zh-CN');
});
return { success: true, items };
} catch (err) {
return { success: false, error: err.message };
}
});
其余 IPC 通道包括:file:info(获取文件属性)、file:read(读取文件内容)、file:create(新建文件)、dir:create(新建文件夹)、file:rename(重命名)、file:delete(删除)、file:copy(复制)。
关键要点:
- 所有 IPC 通道统一返回 { success: true, … } 或 { success: false, error: … } 格式
- 过滤以 . 开头的隐藏文件,避免显示 .git、.config 等目录
- 排序逻辑:文件夹始终在前,同类按名称中文排序(localeCompare(‘zh-CN’))
- 单个文件的 stat 失败不影响整个目录的读取
3.2 第二步:设计页面布局
文件:electron-apps/Dolphin/index.html
页面采用经典的文件管理器五段式布局:
<body>
<!-- 顶部工具栏:导航 + 路径面包屑 + 搜索 + 视图切换 -->
<div class="toolbar">
<div class="toolbar-left">
<button class="tool-btn" id="btn-back" title="后退">◀</button>
<button class="tool-btn" id="btn-forward" title="前进">▶</button>
<button class="tool-btn" id="btn-up" title="上级目录">⬆</button>
<button class="tool-btn" id="btn-refresh" title="刷新">⟳</button>
</div>
<div class="toolbar-center">
<div class="path-bar" id="pathBar">
<span class="path-placeholder">未打开文件夹</span>
</div>
</div>
<div class="toolbar-right">
<input type="text" class="search-input" id="searchInput" placeholder="搜索..." />
<button class="tool-btn" id="btn-new-file" title="新建文件">📄+</button>
<button class="tool-btn" id="btn-new-folder" title="新建文件夹">📁+</button>
<button class="tool-btn" id="btn-view-list" title="列表视图">☰</button>
<button class="tool-btn" id="btn-view-grid" title="图标视图">⊞</button>
<button class="tool-btn" id="btn-preview-toggle" title="预览面板">👁</button>
</div>
</div>
<!-- 主内容区:侧边栏 + 文件列表 + 预览面板 -->
<div class="main">
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<span>文件夹</span>
<button class="sidebar-btn" id="btn-open-folder">📂</button>
</div>
<div class="sidebar-tree" id="sidebarTree">
<div class="sidebar-empty">点击 📂 打开文件夹</div>
</div>
</div>
<div class="sidebar-resizer" id="sidebarResizer"></div>
<div class="content" id="content">
<!-- 列表视图 -->
<div class="list-view" id="listView">
<div class="list-header">
<div class="col-check"><input type="checkbox" id="checkAll" /></div>
<div class="col-name sortable" data-sort="name">名称</div>
<div class="col-size sortable" data-sort="size">大小</div>
<div class="col-type sortable" data-sort="type">类型</div>
<div class="col-modified sortable" data-sort="modified">修改时间</div>
</div>
<div class="list-body" id="listBody"></div>
</div>
<!-- 图标视图 -->
<div class="grid-view" id="gridView" style="display:none">
<div class="grid-body" id="gridBody"></div>
</div>
</div>
<div class="preview-resizer" id="previewResizer" style="display:none"></div>
<div class="preview-panel" id="previewPanel" style="display:none">
<div class="preview-header">预览</div>
<div class="preview-content" id="previewContent"></div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="statusbar">
<span id="statusItems">0 个项目</span>
<span id="statusSelected"></span>
<span id="statusSize"></span>
<span class="status-right" id="statusPath"></span>
</div>
<!-- 右键菜单 + 属性对话框 -->
<div class="context-menu" id="contextMenu" style="display:none">...</div>
<div class="modal-overlay" id="infoModal" style="display:none">...</div>
</body>
关键要点:
- 工具栏分为三组:左侧导航(后退/前进/上级/刷新)、中间路径面包屑、右侧功能按钮
- 列表视图和图标视图共用同一个 content 区域,通过 display 切换
- 侧边栏和预览面板都有独立的拖动条(resizer),支持调整宽度
- 右键菜单和属性对话框使用 position: fixed 的 div 实现,不使用原生弹窗
3.3 第三步:实现导航与文件列表渲染
文件:electron-apps/Dolphin/renderer.js
导航系统基于历史栈模式,支持后退、前进和上级目录:
// 全局状态
let currentDir = null;
let history = [];
let historyIndex = -1;
let allItems = [];
let selectedPaths = new Set();
async function navigateTo(dirPath, addToHistory = true) {
const result = await ipcRenderer.invoke('dir:read', dirPath);
if (!result.success) {
// 鸿蒙沙盒环境部分目录无权限,静默处理不弹窗
return;
}
currentDir = dirPath;
allItems = result.items;
selectedPaths.clear();
if (addToHistory) {
history = history.slice(0, historyIndex + 1);
history.push(dirPath);
historyIndex = history.length - 1;
}
renderPathBar();
renderFileList();
renderSidebarTree();
updateStatus();
updateNavButtons();
}
function goBack() {
if (historyIndex > 0) {
historyIndex--;
navigateTo(history[historyIndex], false);
}
}
function goForward() {
if (historyIndex < history.length - 1) {
historyIndex++;
navigateTo(history[historyIndex], false);
}
}
function goUp() {
if (currentDir) {
const parent = path.dirname(currentDir);
if (parent !== currentDir) navigateTo(parent);
}
}
路径面包屑支持点击任意层级快速跳转:
function renderPathBar() {
const bar = document.getElementById('pathBar');
bar.innerHTML = '';
if (!currentDir) return;
const parts = currentDir.split(path.sep).filter(Boolean);
// Windows 盘符处理
if (currentDir.match(/^[A-Z]:\\/)) {
parts[0] = parts[0] + '\\';
}
let accumulated = '';
parts.forEach((part, i) => {
accumulated = i === 0 ? part : path.join(accumulated, part);
const crumb = document.createElement('span');
crumb.className = 'path-crumb';
crumb.textContent = part;
crumb.dataset.path = accumulated;
crumb.addEventListener('click', () => navigateTo(accumulated));
bar.appendChild(crumb);
if (i < parts.length - 1) {
const sep = document.createElement('span');
sep.className = 'path-sep';
sep.textContent = '›';
bar.appendChild(sep);
}
});
}
列表视图渲染支持排序和多选:
function renderListView(items) {
const body = document.getElementById('listBody');
body.innerHTML = '';
if (items.length === 0) {
body.innerHTML = '<div class="empty-msg">此文件夹为空</div>';
return;
}
// 更新排序箭头
document.querySelectorAll('.sortable').forEach(el => {
const arrow = el.querySelector('.sort-arrow');
if (el.dataset.sort === sortField) {
arrow.textContent = sortOrder === 'asc' ? '▲' : '▼';
} else {
arrow.textContent = '';
}
});
items.forEach(item => {
const row = document.createElement('div');
row.className = 'list-row' + (selectedPaths.has(item.path) ? ' selected' : '');
row.dataset.path = item.path;
const check = document.createElement('div');
check.className = 'col-check';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = selectedPaths.has(item.path);
cb.addEventListener('change', (e) => {
e.stopPropagation();
if (cb.checked) {
selectedPaths.add(item.path);
} else {
selectedPaths.delete(item.path);
}
row.classList.toggle('selected', cb.checked);
updateStatus();
});
check.appendChild(cb);
const nameCell = document.createElement('div');
nameCell.className = 'col-name';
nameCell.innerHTML = `<span class="file-icon">${getFileIcon(item)}</span><span class="file-name">${escapeHtml(item.name)}</span>`;
const sizeCell = document.createElement('div');
sizeCell.className = 'col-size';
sizeCell.textContent = item.isDirectory ? '' : formatSize(item.size);
const typeCell = document.createElement('div');
typeCell.className = 'col-type';
typeCell.textContent = getFileType(item);
const modCell = document.createElement('div');
modCell.className = 'col-modified';
modCell.textContent = formatDate(item.modified);
row.appendChild(check);
row.appendChild(nameCell);
row.appendChild(sizeCell);
row.appendChild(typeCell);
row.appendChild(modCell);
// 单击选中
row.addEventListener('click', (e) => {
if (e.target.tagName === 'INPUT') return;
handleItemClick(item, e);
});
// 双击打开
row.addEventListener('dblclick', () => {
if (item.isDirectory) navigateTo(item.path);
});
// 右键菜单
row.addEventListener('contextmenu', (e) => {
e.preventDefault();
if (!selectedPaths.has(item.path)) {
selectedPaths.clear();
selectedPaths.add(item.path);
renderFileList();
}
showContextMenu(e, item);
});
body.appendChild(row);
});
// 同步全选复选框状态
const checkAll = document.getElementById('checkAll');
if (items.length > 0 && items.every(item => selectedPaths.has(item.path))) {
checkAll.checked = true;
} else {
checkAll.checked = false;
}
}
多选逻辑支持三种模式:单击选中、Ctrl+点击切换、Shift+点击范围选:
function handleItemClick(item, e) {
if (e.ctrlKey || e.metaKey) {
// Ctrl+点击:切换选中
if (selectedPaths.has(item.path)) {
selectedPaths.delete(item.path);
} else {
selectedPaths.add(item.path);
}
} else if (e.shiftKey && selectedPaths.size > 0) {
// Shift+点击:范围选中
const paths = allItems.map(i => i.path);
const lastSelected = [...selectedPaths].pop();
const startIdx = paths.indexOf(lastSelected);
const endIdx = paths.indexOf(item.path);
const [from, to] = startIdx < endIdx ? [startIdx, endIdx] : [endIdx, startIdx];
for (let i = from; i <= to; i++) {
selectedPaths.add(paths[i]);
}
} else {
selectedPaths.clear();
selectedPaths.add(item.path);
}
renderFileList();
updatePreview();
}
3.4 第四步:实现右键菜单与文件操作
右键菜单支持文件操作和空白区域操作,根据上下文智能启用/禁用:
function showContextMenu(e, item) {
contextTarget = item;
const menu = document.getElementById('contextMenu');
menu.style.display = 'block';
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
// 确保不超出屏幕
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth)
menu.style.left = (window.innerWidth - rect.width - 5) + 'px';
if (rect.bottom > window.innerHeight)
menu.style.top = (window.innerHeight - rect.height - 5) + 'px';
// 根据上下文启用/禁用菜单项
const hasItem = !!item;
const hasSelection = selectedPaths.size > 0;
menu.querySelector('[data-action="open"]')
.classList.toggle('disabled', !hasItem || !item.isDirectory);
menu.querySelector('[data-action="copy"]')
.classList.toggle('disabled', !hasSelection);
menu.querySelector('[data-action="rename"]')
.classList.toggle('disabled', selectedPaths.size !== 1);
menu.querySelector('[data-action="delete"]')
.classList.toggle('disabled', !hasSelection);
menu.querySelector('[data-action="info"]')
.classList.toggle('disabled', selectedPaths.size !== 1);
document.getElementById('ctxPaste')
.classList.toggle('disabled', !clipboard);
}
删除操作使用自定义确认对话框(避免鸿蒙原生弹窗崩溃):
deleteSelected = async function() {
if (selectedPaths.size === 0) return;
const count = selectedPaths.size;
const ok = await confirmCustom(`确定要删除 ${count} 个项目吗?`);
if (!ok) return;
const promises = [...selectedPaths].map(p =>
ipcRenderer.invoke('file:delete', p));
const results = await Promise.all(promises);
const failed = results.filter(r => !r.success);
if (failed.length > 0) {
showCustomAlert('部分删除失败: ' + failed[0].error);
}
selectedPaths.clear();
refresh();
};
3.5 第五步:自定义对话框替代原生弹窗
鸿蒙平台的一个关键约束是:原生 prompt()、confirm()、alert() 以及 select 元素会触发 SubWindow 崩溃。因此必须使用自定义 div 对话框替代。
三个核心对话框函数均为 Promise 模式:
// 确认对话框
function confirmCustom(msg) {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal modal-sm">
<div class="modal-header"><span>确认</span></div>
<div class="modal-body"><p>${escapeHtml(msg)}</p></div>
<div class="modal-footer">
<button class="modal-btn" id="confirmYes">确定</button>
<button class="modal-btn modal-cancel" id="confirmNo">取消</button>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.querySelector('#confirmYes')
.addEventListener('click', () => { overlay.remove(); resolve(true); });
overlay.querySelector('#confirmNo')
.addEventListener('click', () => { overlay.remove(); resolve(false); });
});
}
// 输入对话框
function promptCustom(msg, defaultVal = '') {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal modal-sm">
<div class="modal-header"><span>输入</span></div>
<div class="modal-body">
<p>${escapeHtml(msg)}</p>
<input type="text" class="modal-input"
id="promptInput" value="${escapeHtml(defaultVal)}" />
</div>
<div class="modal-footer">
<button class="modal-btn" id="promptOk">确定</button>
<button class="modal-btn modal-cancel" id="promptCancel">取消</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const input = overlay.querySelector('#promptInput');
input.focus();
input.select();
overlay.querySelector('#promptOk').addEventListener('click', () => {
const val = input.value.trim();
overlay.remove();
resolve(val || null);
});
overlay.querySelector('#promptCancel')
.addEventListener('click', () => { overlay.remove(); resolve(null); });
// 支持 Enter 确认、Escape 取消
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
overlay.querySelector('#promptOk').click();
}
if (e.key === 'Escape') {
overlay.querySelector('#promptCancel').click();
}
});
});
}
关键要点:
- 所有对话框动态创建 DOM 元素,使用后自动移除
- 输入框自动聚焦并全选,支持 Enter 确认和 Escape 取消
- 使用函数覆盖模式:先声明 let startCreate, startRename, deleteSelected;,再用自定义对话框版本赋值
3.6 第六步:KDE Breeze 风格主题
文件:electron-apps/Dolphin/styles/dolphin.css
使用 CSS 变量定义 KDE Breeze 配色方案:
:root {
--bg-primary: #ffffff;
--bg-secondary: #f7f8fa;
--bg-toolbar: #e8eaf0;
--bg-sidebar: #f0f1f5;
--bg-hover: #dce3f0;
--bg-selected: #c4d4f0;
--bg-active: #3daee9;
--text-primary: #232629;
--text-secondary: #6c7278;
--text-selected: #ffffff;
--border-color: #c9cdd3;
--accent: #3daee9;
--accent-hover: #2d9fd9;
--danger: #da4453;
--radius: 4px;
}
工具栏按钮的交互状态:
.tool-btn {
width: 32px;
height: 32px;
border: 1px solid transparent;
border-radius: var(--radius);
background: transparent;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
transition: background 0.15s;
}
.tool-btn:hover {
background: var(--bg-hover);
border-color: var(--border-color);
}
.tool-btn.active {
background: var(--accent);
color: var(--text-selected);
border-color: var(--accent);
}
.tool-btn:disabled {
opacity: 0.35;
cursor: default;
}
关键要点:
- 避免使用 -webkit-line-clamp、display: -webkit-box 等非标准属性,这些在鸿蒙 ArkWeb 引擎上会触发渲染崩溃
- 文本截断使用安全的 text-overflow: ellipsis + white-space: nowrap 组合
- 图标视图的 emoji 尺寸控制在 28px 以内,避免大尺寸 emoji 渲染异常
四、部署到鸿蒙平台效果展示
4.1 文件同步
将 Electron 应用文件复制到鸿蒙 web_engine 模块的部署目录:
# 使用 PowerShell 同步文件
Copy-Item "electron-apps\Dolphin\main.js" `
-Destination "web_engine\src\main\resources\resfile\resources\app\main.js" `
-Force
Copy-Item "electron-apps\Dolphin\renderer.js" `
-Destination "web_engine\src\main\resources\resfile\resources\app\renderer.js" `
-Force
Copy-Item "electron-apps\Dolphin\index.html" `
-Destination "web_engine\src\main\resources\resfile\resources\app\index.html" `
-Force
Copy-Item "electron-apps\Dolphin\styles\dolphin.css" `
-Destination "web_engine\src\main\resources\resfile\resources\app\styles\dolphin.css" `
-Force
Copy-Item "electron-apps\Dolphin\package.json" `
-Destination "web_engine\src\main\resources\resfile\resources\app\package.json" `
-Force
4.2 构建 HAP 包
在 DevEco Studio 中:
- 打开项目根目录
- 点击 Build > Build Hap(s)/APP(s)
- 选择 Build Hap(s)
- 等待构建完成
4.3 真机测试
- 连接鸿蒙设备(HUAWEI MateBook Pro 或启动模拟器)
- 点击 Run > Run ‘entry’
- 安装完成后,应用自动启动
- 点击侧边栏的 📂 按钮,通过系统对话框选择要浏览的文件夹
- 即可看到文件列表、侧边栏目录树、路径面包屑等完整功能



五、常见问题 FAQ
Q1:启动后显示"无法访问"或 EPERM 错误怎么办?
问题现象:应用启动后弹出错误提示,侧边栏无法加载目录
根本原因:鸿蒙沙盒环境下,os.homedir() 返回的路径无法通过 fs.readdirSync 直接读取
解决方案:
- 移除所有硬编码的系统路径(主目录、桌面、文档、下载)
- 不依赖 os.homedir() 获取初始目录
- 用户通过系统对话框(dialog.showOpenDialog)手动选择要浏览的文件夹
- 侧边栏目录树从当前目录开始渲染,而非磁盘根目录
// 从当前目录开始渲染(避免读取磁盘根目录导致 EPERM)
async function renderSidebarTree() {
const tree = document.getElementById('sidebarTree');
tree.innerHTML = '';
if (!currentDir) return;
expandedDirs.add(currentDir);
await renderTreeNode(tree, currentDir, 0);
}
Q2:点击"上级目录"按钮没有反应?
问题现象:点击 ⬆ 按钮后,页面没有变化
根本原因:当前目录的父级目录不在鸿蒙沙盒授权范围内,fs.readdirSync 无法读取
解决方案:导航失败时静默处理,不弹窗
async function navigateTo(dirPath, addToHistory = true) {
const result = await ipcRenderer.invoke('dir:read', dirPath);
if (!result.success) {
// 鸿蒙沙盒环境部分目录无权限,静默处理不弹窗
return;
}
currentDir = dirPath;
allItems = result.items;
// ...继续渲染路径栏、文件列表、侧边栏等
}
这是鸿蒙沙盒的固有限制——只能访问通过系统对话框明确授权的目录。
Q3:切换图标视图时应用闪退?
问题现象:点击 ⊞ 图标视图按钮后,应用直接崩溃
根本原因:网格视图 CSS 中使用了 -webkit-line-clamp + display: -webkit-box + -webkit-box-orient,这三个非标准属性组合在鸿蒙 ArkWeb 引擎上触发渲染层崩溃
解决方案:移除危险 CSS 属性,改用安全的文本截断方案
/* 错误写法(会导致 ArkWeb 崩溃) */
.grid-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 正确写法 */
.grid-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
Q4:删除/重命名/新建文件时应用崩溃?
问题现象:点击右键菜单中的删除、重命名等操作后,应用闪退
根本原因:代码中使用了原生 confirm()、prompt() 或 alert(),这些在鸿蒙平台会触发 SubWindow 崩溃
解决方案:全部替换为自定义 div 对话框(参见 3.5 节)
// 错误写法(鸿蒙平台必崩)
if (confirm('确定要删除吗?')) { ... }
const name = prompt('输入文件名:');
// 正确写法
const ok = await confirmCustom('确定要删除吗?');
const name = await promptCustom('输入文件名:');
Q5:如何同步文件到鸿蒙项目?
问题现象:修改了 electron-apps/Dolphin/ 下的文件,但构建后没有生效
根本原因:文件没有同步到鸿蒙 web_engine 模块的部署目录
解决方案:使用 PowerShell 脚本同步文件(参见 4.1 节)
部署路径:web_engine/src/main/resources/resfile/resources/app/
注意:每次修改代码后都需要同步,否则构建的 HAP 包不会包含最新代码。
Q6:侧边栏目录树展开后显示空白?
问题现象:点击侧边栏的文件夹箭头展开后,没有子目录显示
根本原因:子目录中可能存在无权限访问的路径,dir:read 返回失败
解决方案:renderTreeNode 中对失败结果静默跳过
async function renderTreeNode(container, dirPath, depth) {
const result = await ipcRenderer.invoke('dir:read', dirPath);
if (!result.success) return; // 静默跳过无权限目录
const dirs = result.items.filter(item => item.isDirectory);
dirs.forEach(dir => {
// ...渲染树节点
});
}
Q7:预览面板能预览哪些文件类型?
预览面板支持以下文本文件的实时预览(文件大小限制 100KB):
.txt .md .json .js .ts .css .html .xml .py .java .c .cpp .h .log .yaml .yml .toml .ini .cfg .sh .bat .ps1
对于文件夹,显示包含的文件/文件夹数量。对于不支持预览的文件类型,显示基本信息(名称、大小、修改时间)。
更多推荐



所有评论(0)