fix: use readonly preview for code resource tabs
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
# 5-43 大 JSON 资源 tab 打开后卡死且无法关闭
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
- 状态:done
|
||||||
|
- owner:05-editor-mainline
|
||||||
|
- 发现时间:2026-06-04
|
||||||
|
- 修复时间:2026-06-04
|
||||||
|
|
||||||
|
## 现象
|
||||||
|
|
||||||
|
在 local-folder 文件树中打开 OCR sidecar 的 source-map JSON,例如:
|
||||||
|
|
||||||
|
- `/mnt/Data1T/research/DHA_cosmetics/literature/SCCS_1612_19_DHA.ocr/SCCS_1612_19_DHA.pdf.source-map.json`
|
||||||
|
|
||||||
|
页面会把 JSON 当作可编辑 code 资源加载进 Tiptap resource tab。大 JSON 打开后主线程明显卡顿,标签页关闭按钮无响应或被资源编辑 session 的关闭保护阻止;其它普通资源 tab 仍可关闭。
|
||||||
|
|
||||||
|
## 根因
|
||||||
|
|
||||||
|
`document-resource-tab-runtime.js` 将 `.json` 归类为 `code`,并且 `markdown/text/code` 统一走 `openTiptapResourceTab`。这会为 JSON 创建完整 Tiptap 编辑 session、序列化大文档、挂本地文件事件 channel,并把 code 资源纳入 dirty close guard。
|
||||||
|
|
||||||
|
OCR source-map JSON 是机器生成的只读索引文件,不适合作为页面编辑器文档打开;大文件走 Tiptap 会放大渲染和关闭保护成本。
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
|
||||||
|
- `code` 资源不再声明为 editable。
|
||||||
|
- `.json` 等 code 资源改走轻量只读 `<pre><code>` 预览,不创建 Tiptap editor session。
|
||||||
|
- 只读预览显示文件大小,超过预览上限时截断提示。
|
||||||
|
- Markdown 和普通 text resource tab 仍保留原可编辑 Tiptap 路径。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- `node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js`
|
||||||
|
- `cargo build -p mnote-web --manifest-path rust/Cargo.toml`
|
||||||
|
- 临时启动 `127.0.0.1:3023` 后用 Playwright 打开真实页面:
|
||||||
|
- `rootUri=file:///mnt/Data1T/research/DHA_cosmetics`
|
||||||
|
- 页面:`literature/dha_computational_modeling_literature.md`
|
||||||
|
- 资源:`literature/SCCS_1612_19_DHA.ocr/SCCS_1612_19_DHA.pdf.source-map.json`
|
||||||
|
- 浏览器验证结果:
|
||||||
|
- resource tab 成功打开只读 code preview。
|
||||||
|
- 当前 panel 不存在 `mnote-leptos-tiptap-island-editor-root`,证明没有挂 Tiptap 编辑器。
|
||||||
|
- 预览加载 `970708` 字符,meta 显示 `只读预览 · 971,098 bytes`。
|
||||||
|
- 点击 tab close 后 `openResourcePanels=0`,主页面 tab 恢复选中。
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
`task505-resource-tab-watch-contract-smoke.js` 在本次临时 standalone 服务上未通过,失败点是 Markdown resource 外部文件变更 watcher 未把写盘更新同步回 resource editor;该路径与本次 JSON 只读预览不同,未作为本 bug 验收条件。
|
||||||
@@ -64,6 +64,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
const resourceTabRegistry = new Map();
|
const resourceTabRegistry = new Map();
|
||||||
const resourceTabMru = { primary: [], secondary: [] };
|
const resourceTabMru = { primary: [], secondary: [] };
|
||||||
const resourceTabMruMax = 20;
|
const resourceTabMruMax = 20;
|
||||||
|
const readonlyCodePreviewMaxChars = 1_500_000;
|
||||||
const resourceTabCloseGuardAttribute = 'data-resource-tab-close-guarded';
|
const resourceTabCloseGuardAttribute = 'data-resource-tab-close-guarded';
|
||||||
let onlyofficeBridgeReadyListenerBound = false;
|
let onlyofficeBridgeReadyListenerBound = false;
|
||||||
|
|
||||||
@@ -263,7 +264,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
const badgeKind = resourceTabBadgeKind(input, kind);
|
const badgeKind = resourceTabBadgeKind(input, kind);
|
||||||
const rawTarget = String(input?.openTarget || '').trim().toLowerCase();
|
const rawTarget = String(input?.openTarget || '').trim().toLowerCase();
|
||||||
const openTarget = rawTarget === 'new-window' || rawTarget === 'side' ? rawTarget : 'active-tab';
|
const openTarget = rawTarget === 'new-window' || rawTarget === 'side' ? rawTarget : 'active-tab';
|
||||||
const editable = kind === 'markdown' || kind === 'text' || kind === 'code';
|
const editable = kind === 'markdown' || kind === 'text';
|
||||||
const defaultOpenMode = kind === 'office' && openTarget === 'active-tab' ? 'active-tab-iframe' : openTarget;
|
const defaultOpenMode = kind === 'office' && openTarget === 'active-tab' ? 'active-tab-iframe' : openTarget;
|
||||||
const viewerUrl = kind === 'office'
|
const viewerUrl = kind === 'office'
|
||||||
? String(input?.officeUrl || input?.href || '').trim()
|
? String(input?.officeUrl || input?.href || '').trim()
|
||||||
@@ -2064,6 +2065,46 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
markIntendedSlashRoot(entry);
|
markIntendedSlashRoot(entry);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openReadonlyCodeResourceTab = async (entry, input) => {
|
||||||
|
entry.panel.innerHTML = '';
|
||||||
|
const shell = document.createElement('div');
|
||||||
|
shell.className = 'mnote-resource-tab-code-shell';
|
||||||
|
shell.setAttribute('data-mnote-resource-readonly-preview', 'true');
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'mnote-resource-tab-code-header';
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'mnote-resource-tab-code-title';
|
||||||
|
title.textContent = entry.title;
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'mnote-resource-tab-code-meta';
|
||||||
|
meta.textContent = '只读预览';
|
||||||
|
header.append(title, meta);
|
||||||
|
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
pre.className = 'mnote-resource-tab-code-preview';
|
||||||
|
const code = document.createElement('code');
|
||||||
|
pre.append(code);
|
||||||
|
shell.append(header, pre);
|
||||||
|
entry.panel.append(shell);
|
||||||
|
|
||||||
|
const response = await fetch(localResourceReadUrl(input.rootUri, input.path), { cache: 'no-store', headers: { accept: 'application/json' } });
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
if (!response.ok || !payload || payload.ok !== true) throw new Error(payload?.error?.message || `resource_read_failed_${response.status}`);
|
||||||
|
const readResult = payload.result || {};
|
||||||
|
const rawText = typeof readResult.text === 'string' ? readResult.text : '';
|
||||||
|
const truncated = rawText.length > readonlyCodePreviewMaxChars;
|
||||||
|
code.textContent = truncated
|
||||||
|
? `${rawText.slice(0, readonlyCodePreviewMaxChars)}\n\n/* 预览已截断,文件过大,请用外部编辑器查看完整内容。 */`
|
||||||
|
: rawText;
|
||||||
|
const fileSize = new Blob([rawText]).size;
|
||||||
|
meta.textContent = truncated
|
||||||
|
? `只读预览 · 已截断 · ${fileSize.toLocaleString()} bytes`
|
||||||
|
: `只读预览 · ${fileSize.toLocaleString()} bytes`;
|
||||||
|
entry.session = null;
|
||||||
|
entry.view = null;
|
||||||
|
};
|
||||||
|
|
||||||
const ensureOnlyofficeBridgeReadyListener = () => {
|
const ensureOnlyofficeBridgeReadyListener = () => {
|
||||||
if (onlyofficeBridgeReadyListenerBound) return;
|
if (onlyofficeBridgeReadyListenerBound) return;
|
||||||
onlyofficeBridgeReadyListenerBound = true;
|
onlyofficeBridgeReadyListenerBound = true;
|
||||||
@@ -2245,8 +2286,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
|||||||
try {
|
try {
|
||||||
if (entry.kind === 'mindmap') {
|
if (entry.kind === 'mindmap') {
|
||||||
await openMindmapResourceTab(entry, input);
|
await openMindmapResourceTab(entry, input);
|
||||||
} else if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
|
} else if (entry.kind === 'markdown' || entry.kind === 'text') {
|
||||||
await openTiptapResourceTab(entry, input);
|
await openTiptapResourceTab(entry, input);
|
||||||
|
} else if (entry.kind === 'code') {
|
||||||
|
await openReadonlyCodeResourceTab(entry, input);
|
||||||
} else {
|
} else {
|
||||||
await openPassiveResourceTab(entry, input);
|
await openPassiveResourceTab(entry, input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2938,6 +2938,56 @@ body {
|
|||||||
padding: 34px 48px;
|
padding: 34px 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mnote-resource-tab-code-shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
background: #FFF;
|
||||||
|
color: #37352F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-resource-tab-code-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-bottom: 1px solid rgba(55, 53, 47, 0.1);
|
||||||
|
background: #FAFAF8;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-resource-tab-code-title {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-resource-tab-code-meta {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #73726E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnote-resource-tab-code-preview {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 18px 22px 48px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #1F2328;
|
||||||
|
color: #F0F3F6;
|
||||||
|
font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
white-space: pre;
|
||||||
|
tab-size: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.mnote-resource-tab-error {
|
.mnote-resource-tab-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user