前言

在将 Abricotine 适配到鸿蒙 PC 平台时,图像插入和 HTML 导出功能遇到了多个适配问题:Electron 渲染进程不支持 prompt() 函数,导致无法通过 URL 插入图像;HTML 导出功能需要处理路径格式、权限问题等。本文将详细记录我们如何通过纯前端模态对话框实现图像 URL 输入,以及如何完善 HTML 导出功能的适配。

关键词:鸿蒙PC、Electron适配、图像插入、HTML导出、纯前端对话框、prompt替代方案

目录

  1. 问题现象与分析
  2. 图像插入的两种实现方式
  3. 纯前端模态对话框实现
  4. HTML导出功能适配
  5. 最佳实践与注意事项
  6. 常见问题解答
  7. 总结与展望

问题现象与分析

1.1 问题现象

问题1:无法通过 URL 插入图像

在鸿蒙 PC 上,当用户尝试通过 URL 插入图像时:

用户操作:点击"插入图像" → 选择"通过URL"
预期:弹出输入框让用户输入图像URL
实际:❌ prompt() 函数不存在,报错或无法使用

错误信息

ReferenceError: prompt is not defined

问题2:HTML 导出功能不完善

HTML 导出功能在鸿蒙 PC 上遇到:

  • 路径格式处理错误
  • 权限问题导致文件夹创建失败
  • 图片复制失败

1.2 根本原因

Electron 渲染进程限制

  • ❌ 不支持 prompt() 函数(浏览器 API 限制)
  • ❌ 不支持 alert()confirm()(部分平台)
  • ✅ 需要使用纯前端方案替代

鸿蒙 PC 特殊限制

  • 文件系统权限限制
  • 路径格式特殊(file://docs/ 格式)
  • 文件夹创建权限问题

图像插入的两种实现方式

2.1 方式一:从电脑选择文件插入

实现函数imageFromComputer

代码位置commands.js 第418-420行

// commands.js

imageFromComputer: function(win, abrDoc, cm) {
    abrDoc.insertImage();
}

功能说明

  • ✅ 调用 abrDoc.insertImage() 函数
  • ✅ 打开文件选择对话框
  • ✅ 用户选择本地图像文件
  • ✅ 自动插入到编辑器中

使用场景

  • 插入本地存储的图像文件
  • 从电脑选择图片插入到文档

工作流程

用户点击"从电脑插入图像"
    ↓
abrDoc.insertImage() 被调用
    ↓
打开文件选择对话框
    ↓
用户选择图像文件
    ↓
图像插入到编辑器

2.2 方式二:通过 URL 插入图像

实现函数format("image", url)

代码位置commands.js 第297-318行

// commands.js

format: function (win, abrDoc, cm, param) {
    // ⚠️ HarmonyOS: 当插入图像时,如果是 "image" 参数且没有提供 URL,弹出对话框让用户输入 URL
    // Electron 渲染进程不支持 prompt(),使用纯前端模态对话框
    if (param === "image") {
        var promptTitle = abrDoc.localizer.get("insert-image") || "插入图像";
        var that = this;
    
        // 使用异步方式显示输入对话框
        this._showInputDialogAsync(promptTitle, "http://", function(imageUrl) {
            if (imageUrl && imageUrl.trim() !== "" && imageUrl.trim() !== "http://") {
                // 用户输入了有效的 URL,使用该 URL 插入图像
                cm.format("image", imageUrl.trim());
            }
            // 如果用户取消或输入为空,不执行任何操作
        });
        return;
    }
  
    if (typeof param !== "undefined") {
        cm.format(param);
    }
}

功能说明

  • ✅ 检测到 param === "image" 时,显示输入对话框
  • ✅ 用户输入图像 URL
  • ✅ 验证 URL 有效性
  • ✅ 调用 cm.format("image", url) 插入图像

使用场景

  • 插入网络上的图像(通过 URL)
  • 插入远程服务器上的图像
  • 插入在线图片链接

工作流程

用户点击"通过URL插入图像"
    ↓
format("image") 被调用
    ↓
显示纯前端输入对话框
    ↓
用户输入图像URL
    ↓
验证URL有效性
    ↓
cm.format("image", url) 插入图像

纯前端模态对话框实现

3.1 为什么需要纯前端对话框

Electron 渲染进程限制

  • prompt() 函数不存在
  • alert()confirm() 在某些平台不可用
  • ✅ 必须使用纯 HTML/CSS/JavaScript 实现

方案优势

  • ✅ 完全可控的 UI 样式
  • ✅ 支持自定义验证逻辑
  • ✅ 跨平台兼容性好
  • ✅ 用户体验更好

3.2 完整实现代码

_showInputDialogAsync 函数commands.js 第321-414行

// commands.js

// ⚠️ HarmonyOS: 纯前端异步输入对话框实现(替代 prompt)
_showInputDialogAsync: function (title, defaultValue, callback) {
    defaultValue = defaultValue || "";
    callback = callback || function() {};
  
    // 1. 创建模态框背景(遮罩层)
    var overlay = document.createElement("div");
    overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center;";
  
    // 2. 创建对话框容器
    var dialog = document.createElement("div");
    dialog.style.cssText = "background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); min-width: 400px; max-width: 90%;";
  
    // 3. 创建标题
    var titleEl = document.createElement("div");
    titleEl.textContent = title;
    titleEl.style.cssText = "font-size: 16px; font-weight: bold; margin-bottom: 15px; color: #333; white-space: pre-line;";
  
    // 4. 创建输入框
    var input = document.createElement("input");
    input.type = "text";
    input.value = defaultValue;
    input.style.cssText = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; box-sizing: border-box; margin-bottom: 15px;";
  
    // ⚠️ HarmonyOS: 根据标题设置不同的占位符
    var placeholder = "请输入";
    if (title.indexOf("文件名模板") !== -1 || title.indexOf("文件名") !== -1) {
        placeholder = "例如:image_1.jpg 或 photo_copy.jpg";
    } else if (title.indexOf("URL") !== -1 || title.indexOf("url") !== -1 || title.indexOf("图像") !== -1) {
        placeholder = "请输入图片 URL";
    }
    input.placeholder = placeholder;
  
    // 5. 创建按钮容器
    var buttonContainer = document.createElement("div");
    buttonContainer.style.cssText = "display: flex; justify-content: flex-end; gap: 10px;";
  
    // 6. 创建取消按钮
    var cancelBtn = document.createElement("button");
    cancelBtn.textContent = "取消";
    cancelBtn.style.cssText = "padding: 8px 16px; border: 1px solid #ccc; background: white; border-radius: 4px; cursor: pointer; font-size: 14px;";
  
    // 7. 创建确定按钮
    var okBtn = document.createElement("button");
    okBtn.textContent = "确定";
    okBtn.style.cssText = "padding: 8px 16px; border: none; background: #007AFF; color: white; border-radius: 4px; cursor: pointer; font-size: 14px;";
  
    // 8. 组装对话框
    dialog.appendChild(titleEl);
    dialog.appendChild(input);
    buttonContainer.appendChild(cancelBtn);
    buttonContainer.appendChild(okBtn);
    dialog.appendChild(buttonContainer);
    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
  
    // 9. 聚焦输入框
    input.focus();
    input.select();
  
    // 10. 清理函数
    var cleanup = function() {
        if (overlay.parentNode) {
            document.body.removeChild(overlay);
        }
    };
  
    // 11. 确认函数
    var confirm = function() {
        var value = input.value;
        cleanup();
        callback(value);
    };
  
    // 12. 取消函数
    var cancel = function() {
        cleanup();
        callback(null);
    };
  
    // 13. 绑定事件
    okBtn.onclick = confirm;
    cancelBtn.onclick = cancel;
    overlay.onclick = function(e) {
        if (e.target === overlay) {
            cancel();  // 点击背景关闭对话框
        }
    };
  
    // 14. 键盘事件处理
    input.onkeydown = function(e) {
        if (e.key === "Enter") {
            e.preventDefault();
            confirm();
        } else if (e.key === "Escape") {
            e.preventDefault();
            cancel();
        }
    };
}

3.3 对话框特性

UI 特性

  • 模态背景:半透明黑色遮罩层,阻止背景交互
  • 居中显示:对话框在屏幕中央显示
  • 响应式设计:最小宽度 400px,最大宽度 90%
  • 现代化样式:圆角、阴影、渐变背景

交互特性

  • 自动聚焦:对话框打开时自动聚焦输入框
  • 自动选中:自动选中默认值,方便用户修改
  • 键盘支持:Enter 确认,Escape 取消
  • 点击背景关闭:点击遮罩层关闭对话框

智能占位符

  • ✅ 根据标题自动设置占位符
  • ✅ 文件名模板:显示示例格式
  • ✅ URL 输入:提示输入图片 URL

HTML导出功能适配

4.1 导出功能调用

实现函数exportHtml

代码位置commands.js 第102-128行

// commands.js

exportHtml: function(win, abrDoc, cm, param) {
    console.log('[HarmonyOS Renderer] commands.exportHtml called');
    console.log('[HarmonyOS Renderer] param:', param);
  
    if (!abrDoc) {
        console.error('[HarmonyOS Renderer] ❌ abrDoc is null or undefined');
        return;
    }
  
    if (typeof abrDoc.exportHtml !== 'function') {
        console.error('[HarmonyOS Renderer] ❌ abrDoc.exportHtml is not a function');
        return;
    }
  
    try {
        console.log('[HarmonyOS Renderer] Calling abrDoc.exportHtml with param:', param);
        abrDoc.exportHtml(param);
        console.log('[HarmonyOS Renderer] abrDoc.exportHtml called successfully');
    } catch (error) {
        console.error('[HarmonyOS Renderer] ❌ Error calling abrDoc.exportHtml:', error);
        console.error('[HarmonyOS Renderer] Error stack:', error.stack);
    }
}

功能说明

  • ✅ 调用 abrDoc.exportHtml() 函数
  • ✅ 传递参数(模板名称、目标路径、选项等)
  • ✅ 完善的错误处理和日志

4.2 导出功能核心实现

核心函数exportHtml(在 export-html.js 中)

关键适配点

  1. 路径处理优化
// export-html.js 第74-83行

// ⚠️ HarmonyOS: 确保 destPath 是绝对路径
var destPathAbs = parsePath(destPath).isAbsolute ? destPath : pathModule.resolve(destPath);
var destDir = parsePath(destPathAbs).dirname;
var destBasename = parsePath(destPathAbs).basename;

// 移除扩展名,创建文件夹名
var folderBasename = destBasename.replace(/\.[^/.]+$/, "");
var exportFolder = pathModule.join(destDir, folderBasename + "_files");
var assetsPath = "./" + folderBasename + "_files";

// 更新 destPath 为绝对路径
destPath = destPathAbs;
  1. 权限检查和降级策略
// export-html.js 第121-133行

// ⚠️ HarmonyOS: 尝试创建文件夹,如果失败则跳过图片复制
var createdExportFolder = files.createDir(exportFolder);
var createdImgDir = files.createDir(imgDirAbs);

// ⚠️ HarmonyOS: 如果文件夹创建失败(权限问题),跳过图片复制和路径更新
if (!createdExportFolder || !createdImgDir) {
    console.warn('[HarmonyOS Export] ⚠️ Failed to create directories (permission denied), skipping image copy');
    console.warn('[HarmonyOS Export] Images will not be copied, keeping original image URLs in HTML');
    // ⚠️ 关键:如果无法创建文件夹,保持图片的原始路径(URL),不更新为相对路径
    // 这样浏览器仍然可以从原始 URL 加载图片
} else {
    // 正常复制图片...
}
  1. 图片复制策略
// export-html.js 第147-172行

// 复制图片到导出文件夹
abrDoc.imageImport(imgDirAbs, { 
    copyRemote: options.copyImagesRemote !== false,
    updateEditor: false,
    showDialog: false
});

// 更新 HTML 中的图片路径
var re = /(<img[^>]+src=['"])([^">]+)(['"])/gmi;
htmlContent = htmlContent.replace(re, function (str, p1, p2, p3) {
    if (options.copyImagesRemote === false && isUrl(p2)) {
        return str;  // 不复制远程图片
    }
    var basename = parsePath(p2).basename;
    var newPath = pathModule.join(assetsPath, "images", basename).replace(/\\/g, '/');
    return p1 + newPath + p3;
});

4.3 导出选项处理

toggleCopyImagesOnHtmlExport 函数:切换导出时是否复制图片

代码位置commands.js 第130-155行

// commands.js

toggleCopyImagesOnHtmlExport: function(win, abrDoc, cm) {
    console.log('[HarmonyOS Commands] toggleCopyImagesOnHtmlExport called');
  
    // ⚠️ HarmonyOS: 修复逻辑反了的问题
    // 当用户点击 checkbox 时,Electron 会自动切换菜单项的 checked 状态
    // 我们需要读取当前的配置值,然后设置为相反的值
  
    abrDoc.getConfig("copy-images-on-html-export", function(value) {
        var currentValue = value !== false;  // 默认值为 true
        var newValue = !currentValue;
    
        console.log('[HarmonyOS Commands] Current copy-images-on-html-export value:', currentValue);
        console.log('[HarmonyOS Commands] New value:', newValue);
    
        // 设置新值
        // menu 参数应该是菜单项的 id,即 "menu-copy-images-on-html-export"
        abrDoc.setConfig("copy-images-on-html-export", newValue, null, "menu-copy-images-on-html-export");
    });
}

最佳实践与注意事项

5.1 纯前端对话框最佳实践

推荐做法

// ✅ 好:使用异步回调模式
_showInputDialogAsync(title, defaultValue, function(value) {
    if (value) {
        // 处理用户输入
    }
});

// ❌ 不好:使用同步 prompt(不可用)
var value = prompt(title, defaultValue);  // 在 Electron 渲染进程中不可用

注意事项

  • ✅ 使用异步回调模式,不阻塞 UI
  • ✅ 提供清理函数,确保对话框正确移除
  • ✅ 支持键盘快捷键(Enter/Escape)
  • ✅ 点击背景关闭对话框

5.2 图像插入最佳实践

推荐做法

// ✅ 好:两种方式都支持
// 方式1:从电脑选择
imageFromComputer: function(win, abrDoc, cm) {
    abrDoc.insertImage();
}

// 方式2:通过URL
format: function(win, abrDoc, cm, param) {
    if (param === "image") {
        // 显示输入对话框
        this._showInputDialogAsync(title, "http://", function(url) {
            if (url) {
                cm.format("image", url);
            }
        });
    }
}

注意事项

  • ✅ 验证 URL 有效性
  • ✅ 处理用户取消的情况
  • ✅ 提供清晰的提示信息

5.3 HTML导出最佳实践

推荐做法

// ✅ 好:完善的错误处理和降级策略
if (!createdExportFolder || !createdImgDir) {
    // 降级:不复制图片,保持原始URL
    console.warn('Permission denied, skipping image copy');
} else {
    // 正常处理:复制图片
}

// ❌ 不好:直接失败,不处理
files.createDir(exportFolder);  // 可能抛出异常

注意事项

  • ✅ 检查权限后再操作
  • ✅ 失败时使用降级策略
  • ✅ 保持功能可用性(即使权限不足)

常见问题解答

Q1: 为什么不能使用 prompt()?

A: Electron 渲染进程不支持浏览器的一些 API,包括 prompt()alert()confirm()。必须使用纯前端方案替代。

Q2: 纯前端对话框的性能如何?

A: 性能很好。纯前端对话框使用原生 DOM API,性能开销很小,响应速度快。

Q3: HTML 导出时图片无法显示怎么办?

A: 如果权限不足无法创建文件夹,图片会保持原始 URL。确保网络连接正常,浏览器可以从原始 URL 加载图片。

Q4: 如何自定义对话框样式?

A: 修改 _showInputDialogAsync 函数中的 CSS 样式字符串,可以自定义对话框的外观。


总结与展望

7.1 核心要点总结

通过本文的深入分析,我们了解到:

  1. 图像插入的两种方式

    • 从电脑选择文件插入
    • 通过 URL 插入(使用纯前端对话框)
  2. 纯前端对话框实现:完全替代 prompt(),提供更好的用户体验

  3. HTML 导出功能适配:路径处理、权限检查、降级策略

7.2 技术价值

这个解决方案不仅解决了图像插入和 HTML 导出问题,还带来了以下好处:

  • 更好的用户体验:纯前端对话框比原生 prompt 更美观
  • 跨平台兼容:不依赖平台特定的 API
  • 功能完整性:即使权限不足也能部分工作

7.3 适用场景

这套方案适用于:

  • ✅ 所有需要用户输入的 Electron 应用
  • ✅ 需要替代 prompt/alert/confirm 的应用
  • ✅ 在鸿蒙 PC 上运行的 Electron 应用
  • ✅ 需要 HTML 导出功能的应用

相关资源

Electron 官方文档

MDN 文档

HarmonyOS 官方文档

鸿蒙PC开发资源

Logo

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

更多推荐