fix(editor): 新文档回到顶部 + 超链接用系统浏览器打开 + 链接气泡仅选中时弹出

This commit is contained in:
hunter-dev
2026-09-25 22:42:03 +08:00
parent dc1d6b43dd
commit 1e30abfac4
6 changed files with 49 additions and 2 deletions
+14 -1
View File
@@ -1,5 +1,5 @@
// main 进程入口:创建窗口 + 初始化库 + 注册 IPC + 迁移检测
import { app, BrowserWindow, ipcMain, protocol, net, dialog } from 'electron';
import { app, BrowserWindow, ipcMain, protocol, net, dialog, shell } from 'electron';
import nodePath from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { promises as fs } from 'node:fs';
@@ -31,6 +31,19 @@ function createWindow() {
});
if (!win) win = w;
w.on('ready-to-show', () => w.show());
// 安全:外部链接一律交给系统默认浏览器,不在应用窗口内导航(避免丢失编辑器状态)
w.webContents.setWindowOpenHandler(({ url }) => {
if (/^https?:\/\//i.test(url)) void shell.openExternal(url);
return { action: 'deny' };
});
w.webContents.on('will-navigate', (e, url) => {
const current = w.webContents.getURL();
// 仅拦截跳转到其它 http(s) 站点的导航;应用自身 (vaultasset/file/app) 不拦
if (/^https?:\/\//i.test(url) && url !== current) {
e.preventDefault();
void shell.openExternal(url);
}
});
// 转发渲染进程控制台消息到主进程终端(便于开发调试)
w.webContents.on('console-message', (e, level, message, line, sourceId) => {
const lv = ['log', 'info', 'warn', 'error'][level] || 'log';
+9
View File
@@ -93,4 +93,13 @@ export function registerIpc(vault: VaultService) {
if (!fs2) return;
shell.showItemInFolder(nodePath.join(fs2.getRoot(), p.path));
});
// 超链接用系统默认浏览器打开(不用 Electron 内置窗口)
ipcMain.handle(IPC.openExternal, async (_e, p: { url: string }) => {
const url = p?.url?.trim();
if (!url) return;
// 仅允许 http/https,防止把本地协议或 file: 路径交给外部程序
if (!/^https?:\/\//i.test(url)) return;
await shell.openExternal(url);
});
}
@@ -16,7 +16,9 @@ export function LinkBubbleMenu({ editor }: { editor: Editor }) {
const apply = () => { const u = href.trim(); if (u) editor.chain().focus().extendMarkRange('link').setLink({ href: u }).run(); };
const remove = () => editor.chain().focus().extendMarkRange('link').unsetLink().run();
return (
<BubbleMenu editor={editor} shouldShow={({ editor }) => editor.isActive('link')} className="link-bubble"
<BubbleMenu editor={editor} shouldShow={({ editor, state }) =>
// 只在「选中了链接文字」时才弹出,避免光标停在链接上就弹气泡打断编辑
editor.isActive('link') && !state.selection.empty} className="link-bubble"
style={{ display: 'flex', gap: 4, alignItems: 'center', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 6, padding: 4, boxShadow: '0 4px 16px rgba(0,0,0,0.4)' }}>
<input value={href} onChange={e => setHref(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); apply(); } }}
placeholder="链接地址" style={{ width: 180, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '3px 8px', fontSize: 13, outline: 'none' }} />
@@ -141,6 +141,19 @@ export function TiptapEditor() {
}
return false;
},
// 点击超链接 → 用系统默认浏览器打开(openOnClick 关闭,手动接管)
handleClick: (view, _pos, event) => {
const target = (event.target as HTMLElement | null)?.closest?.('a[href]');
if (!target) return false;
const href = (target as HTMLAnchorElement).getAttribute('href') ?? '';
// 仅接管纯单击(不含 Ctrl/Cmd/Shift/Alt),避免影响文字选取编辑
if (/^https?:\/\//i.test(href) && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {
event.preventDefault();
void invoke(IPC.openExternal, { url: href });
return true;
}
return false;
},
},
onUpdate: ({ editor }) => {
if (skipUpdate.current) return;
@@ -166,6 +179,12 @@ export function TiptapEditor() {
skipUpdate.current = true;
editor.commands.setContent(body || '', { emitUpdate: false });
skipUpdate.current = false;
// 切到新文档时回到顶部(编辑器实例复用,滚动容器不随文档重建,需手动复位)
const scroller = editor.view.dom.parentElement as HTMLElement | null;
if (scroller) {
// 内容换入后再复位,确保 scrollTop 真正落到顶部
requestAnimationFrame(() => { scroller.scrollTop = 0; });
}
}, [currentPath, editor]);
// FR-E13:长文档底部书写——光标行距编辑区底部保持留白,避免输入内容贴着页面底缘
@@ -46,7 +46,9 @@ test('FR-E6:链接气泡编辑(添加+移除)', async () => {
let html: string = await a.page.evaluate(() => document.querySelector('.ProseMirror')?.innerHTML ?? '');
expect(html).toContain('href="https://example.com"');
// 气泡只在「选中链接文字」时弹出(纯点击链接改为调用系统浏览器打开)
await a.page.locator('.ProseMirror a').click();
await a.page.keyboard.press('Control+A');
await expect(a.page.locator('.link-bubble')).toBeVisible({ timeout: 5000 });
await expect(a.page.locator('.link-bubble input')).toHaveValue('https://example.com');