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 WidgetsHTML/CSS
文件操作KIO / QFileNode.js fs 模块
进程模型单进程 + KIO Slave主进程 + 渲染进程 + IPC
主题系统KDE BreezeCSS 变量模拟
部署方式RPM / FlatpakHAP 包(鸿蒙应用)

二、环境准备

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 中:

  1. 打开项目根目录
  2. 点击 Build > Build Hap(s)/APP(s)
  3. 选择 Build Hap(s)
  4. 等待构建完成

4.3 真机测试

  1. 连接鸿蒙设备(HUAWEI MateBook Pro 或启动模拟器)
  2. 点击 Run > Run ‘entry’
  3. 安装完成后,应用自动启动
  4. 点击侧边栏的 📂 按钮,通过系统对话框选择要浏览的文件夹
  5. 即可看到文件列表、侧边栏目录树、路径面包屑等完整功能

五、常见问题 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

对于文件夹,显示包含的文件/文件夹数量。对于不支持预览的文件类型,显示基本信息(名称、大小、修改时间)。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐