dialog、notification 与 contextMenu
这三个模块有个共同点:它们跳出你的 HTML 界面,直接调用系统层或宿主应用层的能力。设计插件交互时,网页内元素负责常规操作,这三个模块负责「需要确认」和「需要打断」的场合。
dialog:系统级对话框
eagle.dialog 提供四个方法,语义和 Electron 的 dialog 模块基本对齐。
这四个方法各自的定位:
| 方法 | 用途 |
|---|---|
showOpenDialog |
让用户选文件或目录 |
showSaveDialog |
让用户选一个保存目标 |
showMessageBox |
让用户在若干选项中做一个决定 |
showErrorBox |
告知不可继续的错误 |
showErrorBox 弹出的框在部分平台上关不掉,只应该在真正无法继续时用。
打开文件对话框
let result = await eagle.dialog.showOpenDialog({
properties: ['openFile', 'openDirectory']
});
// 带过滤器
let filtered = await eagle.dialog.showOpenDialog({
title: '选择要处理的素材',
defaultPath: eagle.library.path,
buttonLabel: '导入',
filters: [
{ name: 'Images', extensions: ['jpg', 'png', 'gif'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile', 'multiSelections']
});
// 返回:{ canceled: false, filePaths: ['/path/a.jpg'] }
properties 支持的值:openFile(选文件)、openDirectory(选文件夹)、multiSelections(多选)、showHiddenFiles(显示隐藏文件)、createDirectory(macOS,创建新目录)、promptToCreate(Windows,输入不存在的路径时提示创建)。
promptToCreate 值得注意:它不是在那个路径上真的创建文件,而是允许返回一个不存在的地址,把创建文件的决定权交给你的程序。这在「导出到指定文件名」的场景里很实用。
保存文件对话框
let result = await eagle.dialog.showSaveDialog({
properties: ['openDirectory']
});
// 返回:{ canceled: false, filePath: '/path/to/save.png' }
// 取消时 filePath 是 undefined
两个对话框返回结构不同:open 返回 filePaths 数组,save 返回单个 filePath,取消时是 undefined。这个不一致是沿袭了 Electron 的设计,写代码时务必先判断 canceled。
消息框
let result = await eagle.dialog.showMessageBox({
title: "Messagebox title",
message: "Message from the Plugin process",
detail: "Ultra message here",
buttons: ["确定", "取消"],
type: "info"
});
console.log(result); // { response: 0 }
type 可选 none、info、error、question、warning。返回的 response 是用户点击的按钮索引——所以按钮顺序很重要:确认类操作必须把「确认」放第一个,这样按回车默认就是执行。
错误框
await eagle.dialog.showErrorBox("Error box title", "Error message from the Plugin process");
一个规范的确认流程
破坏性操作的标准写法:先确认,再执行,失败要有兜底:
async function confirmAndDelete(items) {
const result = await eagle.dialog.showMessageBox({
title: '确认删除',
message: `即将把 ${items.length} 个素材移到垃圾桶`,
detail: items.map(i => i.name).join('\n'),
buttons: ['移到垃圾桶', '取消'],
type: 'warning'
});
// response 是按钮索引,0 为确认
if (result.response !== 0) return;
let failed = 0;
for (const item of items) {
try {
await item.moveToTrash();
}
catch (err) {
failed++;
eagle.log.error(`删除 ${item.id} 失败`);
}
}
if (failed > 0) {
await eagle.dialog.showErrorBox('操作未完成', `${failed} 个素材未能删除,详情见 Eagle 日志`);
}
else {
await eagle.notification.show({
title: '处理完成',
body: `已处理 ${items.length} 个素材`,
duration: 3000
});
}
}
注意 detail 里列出了具体文件名——用户点确认前必须知道自己在删什么,这是审核标准里「执行不可逆操作前让用户明确确认」的要求。
notification:角落通知
await eagle.notification.show({
title: "Basic Notification",
body: "Notification from the Plugin process",
mute: false,
duration: 3000,
icon: "https://"
});
四个字段:title 标题必填、body 描述必填、icon 支持链接或 base64 格式、mute 是否静音、duration 自动隐藏时间(毫秒)。后三个可选。
通知适合表达「操作已完成」这类结果性信息,不适合做需要用户决策的提示——用户没法在通知上点按钮。需要决策的必须用 showMessageBox。
contextMenu:原生右键菜单
eagle.contextMenu.open([
{
id: "edit",
label: "编辑",
submenu: [
{ id: "resize", label: "调整大小", click: () => { /* ... */ } },
{ id: "crop", label: "裁剪", click: () => { /* ... */ } },
{ id: "rotate", label: "旋转", click: () => { /* ... */ } }
]
},
{
id: "effects",
label: "效果",
submenu: [
{ id: "grayscale", label: "灰度", click: () => { /* ... */ } },
{ id: "sepia", label: "深褐色", click: () => { /* ... */ } },
{ id: "invert", label: "反色", click: () => { /* ... */ } }
]
},
{
id: "export",
label: "导出",
click: () => { /* ... */ }
}
])
菜单项的结构是 id、label、submenu,以及 click 回调。顶层数组的每一项是顶级菜单项,嵌套 submenu 形成层级。
实际用法里,菜单项应该按当前选中的素材动态生成,而不是写死一套:
eagle.onPluginCreate(async () => {
document.querySelector('#btn-menu').addEventListener('click', async () => {
const items = await eagle.item.getSelected();
eagle.contextMenu.open([
{
id: 'batch-tag',
label: `为 ${items.length} 个素材打标签`,
click: async () => {
for (const item of items) {
item.tags = [...new Set([...item.tags, 'reviewed'])];
await item.save();
}
}
},
{
id: 'info',
label: '查看详情',
submenu: items.slice(0, 10).map((item, i) => ({
id: `detail-${i}`,
label: item.name,
click: () => { console.log(item.filePath); }
}))
}
]);
});
});
菜单项在弹出时就把素材数据捕获进闭包,点击时不用重新查询。但如果菜单弹出后用户切换了资源库,这些捕获的引用就失效了——click 回调里涉及写操作时,最好重新 getById 一次。
开发流程
- 先梳理业务里所有「需要用户决策」的点,逐个决定用哪个方法。
- 破坏性操作统一走
showMessageBox,在detail里列出具体对象。 - 长任务完成后用
notification.show汇报结果,设置合理的duration。 - 菜单项按当前选中素材动态生成,路径自己拼。
- 每个对话框返回后第一时间判断
canceled。 - 全流程在六套主题下各看一遍,确认弹窗不违和。
常见坑
showOpenDialog返回后不判断canceled。 用户点取消时filePaths是空数组,直接取[0]会拿到 undefined,后续操作报奇怪的错。把
showErrorBox当普通提示用。 这个错误框在某些平台上关不掉。用它做「已保存」之类的提示会让插件显得很粗暴,常规结果提示用notification.show。延伸注意: * 在
click回调里用alert()。 原生菜单弹出时,HTML 页面的alert表现可能异常。改用notification.show或在页面上更新状态。整理人:WorkBuddy
