预览修改

This commit is contained in:
Yeomai committed 2026-09-12 22:00:51 +08:00
1 parent ef23e33ad0
commit 90e5a2b81e
4 files changed
+268 -6

No files matched your search

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

+134 -3
View File
@@ -14,6 +14,13 @@
--accent: #4fc1ff;
--border: #3c3c3c;
--error: #f14c4c;
/* 悬浮层填充与滚动条配色。Eagle 内置 Electron 22 / Chromium 108 不支持 color-mix(),
这些必须写成 rgba(),否则整条颜色声明会被丢弃。 */
--hover-fill: rgba(255, 255, 255, .07);
--scroll-thumb: rgba(255, 255, 255, .34);
--scroll-thumb-hover: rgba(255, 255, 255, .55);
--scroll-thumb-active: rgba(255, 255, 255, .72);
--scroll-track: rgba(255, 255, 255, .07);
}
[data-theme="light"] {
--bg: #f3f4f6;
@@ -24,6 +31,11 @@
--accent: #0078d4;
--border: #ddd;
--error: #d13438;
--hover-fill: rgba(0, 0, 0, .06);
--scroll-thumb: rgba(0, 0, 0, .30);
--scroll-thumb-hover: rgba(0, 0, 0, .46);
--scroll-thumb-active: rgba(0, 0, 0, .62);
--scroll-track: rgba(0, 0, 0, .06);
}
* {
box-sizing: border-box;
@@ -182,6 +194,50 @@
font: 12px/1.6 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
color: var(--text-dim); white-space: pre; word-break: normal;
}
/* 纯文本 / 普通 JSON 预览允许拖动选择与 Ctrl+C 复制;画布与查看器控件仍保持不可选中 */
#overlay.text-preview .title,
#overlay.text-preview .msg {
-webkit-user-select: text; user-select: text;
}
#overlay.text-preview .msg { cursor: text; }
/* 预览区滚动条:不使用原生样式。宽度 14px、滑块可见 8px,保证看得清。
颜色走 --scroll-* 变量(rgba 写死),不能用 color-mix():Eagle 内置 Chromium 108
不支持该函数,声明会整条失效,滑块就变成看不见。也不要写 scrollbar-width /
scrollbar-color,那两个标准属性在 108 上无效且易与 ::-webkit-scrollbar 混淆。 */
#overlay.text-preview .box::-webkit-scrollbar { width: 14px; height: 14px; }
#overlay.text-preview .box::-webkit-scrollbar-track { background: var(--scroll-track); }
#overlay.text-preview .box::-webkit-scrollbar-thumb {
min-height: 36px;
border: 3px solid transparent; border-radius: 999px;
background: var(--scroll-thumb);
background-clip: padding-box;
}
#overlay.text-preview .box::-webkit-scrollbar-thumb:hover {
background: var(--scroll-thumb-hover);
background-clip: padding-box;
}
#overlay.text-preview .box::-webkit-scrollbar-thumb:active {
background: var(--scroll-thumb-active);
background-clip: padding-box;
}
#overlay.text-preview .box::-webkit-scrollbar-corner { background: transparent; }
/* JSON 字段树:折叠标记与计数只用符号 / 数字,无需新增界面文案 */
#overlay.json-preview .msg { white-space: normal; }
.jt-node > summary {
list-style: none; cursor: pointer; border-radius: 4px;
}
.jt-node > summary::-webkit-details-marker { display: none; }
.jt-node > summary:hover { background: var(--hover-fill); }
.jt-node > summary::before {
content: '▾'; display: inline-block; width: 14px; color: var(--text-dim);
}
.jt-node:not([open]) > summary::before { content: '▸'; }
.jt-children { margin-left: 7px; padding-left: 11px; border-left: 1px solid var(--border); }
.jt-leaf { padding-left: 14px; }
.jt-key { color: var(--accent); }
.jt-str { color: var(--text); }
.jt-brace, .jt-count { color: var(--text-dim); }
.jt-count { margin: 0 3px; font-size: 11px; }
#overlay.hidden { display: none; }
#loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 4; background: var(--bg); }
#loading.hidden { display: none; }
@@ -421,6 +477,8 @@
function showError(title, msg) {
loadingEl.classList.add('hidden');
overlay.classList.remove('text-preview');
overlay.classList.remove('json-preview');
overlay.classList.remove('hidden');
document.getElementById('errorTitle').textContent = title;
document.getElementById('errorMsg').textContent = msg;
@@ -432,8 +490,78 @@
timelineBarEl.style.display = 'none';
animationPanel.style.display = 'none';
statusEl.style.display = 'none';
overlay.classList.add('text-preview');
showError(title, text);
overlay.classList.add('text-preview');
}
// ---------- 普通 JSON:轻量字段树(点击折叠 / 展开) ----------
const JSON_TREE_MAX_NODES = 20000; // 超过此规模退回纯文本,避免构建 DOM 卡顿
const JSON_TREE_OPEN_DEPTH = 1; // 默认展开的层级,更深的折叠
function jsonNodeCount(value, budget) {
let n = 0;
const walk = (node) => {
if (!node || typeof node !== 'object' || n > budget) return;
for (const key of Object.keys(node)) { n++; walk(node[key]); if (n > budget) return; }
};
walk(value);
return n;
}
function jsonKeySpan(key) {
const span = document.createElement('span');
span.className = 'jt-key';
span.textContent = (typeof key === 'number' ? '[' + key + ']: ' : JSON.stringify(key) + ': ');
return span;
}
function buildJsonTree(key, value, depth) {
if (value === null || typeof value !== 'object') {
const leaf = document.createElement('div');
leaf.className = 'jt-leaf';
if (key !== null) leaf.appendChild(jsonKeySpan(key));
const val = document.createElement('span');
val.textContent = typeof value === 'string' ? JSON.stringify(value) : String(value);
if (typeof value === 'string') val.className = 'jt-str';
leaf.appendChild(val);
return leaf;
}
const isArray = Array.isArray(value);
const keys = isArray ? value.map((_, i) => i) : Object.keys(value);
const node = document.createElement('details');
node.className = 'jt-node';
if (depth <= JSON_TREE_OPEN_DEPTH) node.open = true;
const summary = document.createElement('summary');
if (key !== null) summary.appendChild(jsonKeySpan(key));
const open = document.createElement('span');
open.className = 'jt-brace';
open.textContent = isArray ? '[' : '{';
const count = document.createElement('span');
count.className = 'jt-count';
count.textContent = String(keys.length);
const close = document.createElement('span');
close.className = 'jt-brace';
close.textContent = isArray ? ']' : '}';
summary.append(open, count, close);
node.appendChild(summary);
const children = document.createElement('div');
children.className = 'jt-children';
for (const k of keys) children.appendChild(buildJsonTree(k, value[k], depth + 1));
node.appendChild(children);
return node;
}
function showJsonPreview(title, value) {
const box = document.getElementById('errorMsg');
showTextPreview(title, '');
if (jsonNodeCount(value, JSON_TREE_MAX_NODES) > JSON_TREE_MAX_NODES) {
// 超大对象仍以纯文本呈现,保证打开速度与可选择性
box.textContent = JSON.stringify(value, null, 2);
return;
}
overlay.classList.add('json-preview');
box.textContent = '';
box.appendChild(buildJsonTree(null, value, 0));
}
function setStatus(msg, holdMs = 0) {
@@ -1063,10 +1191,13 @@
let count = 0;
try { count = fs.readSync(fd, buffer, 0, buffer.length, 0); } finally { fs.closeSync(fd); }
let text = buffer.subarray(0, count).toString('utf8');
const name = path.basename(filePath);
let parsed;
if (stat.size <= maxPreviewBytes) {
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { /* 保留 JSONC/原始文本 */ }
try { parsed = JSON.parse(text); } catch (e) { /* 保留 JSONC/原始文本 */ }
} else text += t('viewer.truncated1m');
showTextPreview(path.basename(filePath), text);
if (parsed !== null && typeof parsed === 'object') showJsonPreview(name, parsed);
else showTextPreview(name, parsed === undefined ? text : JSON.stringify(parsed, null, 2));
return;
}
const assets = utils.discoverAssets(filePath);
+134 -3
View File
@@ -14,6 +14,13 @@
--accent: #4fc1ff;
--border: #3c3c3c;
--error: #f14c4c;
/* 悬浮层填充与滚动条配色。Eagle 内置 Electron 22 / Chromium 108 不支持 color-mix(),
这些必须写成 rgba(),否则整条颜色声明会被丢弃。 */
--hover-fill: rgba(255, 255, 255, .07);
--scroll-thumb: rgba(255, 255, 255, .34);
--scroll-thumb-hover: rgba(255, 255, 255, .55);
--scroll-thumb-active: rgba(255, 255, 255, .72);
--scroll-track: rgba(255, 255, 255, .07);
}
[data-theme="light"] {
--bg: #f3f4f6;
@@ -24,6 +31,11 @@
--accent: #0078d4;
--border: #ddd;
--error: #d13438;
--hover-fill: rgba(0, 0, 0, .06);
--scroll-thumb: rgba(0, 0, 0, .30);
--scroll-thumb-hover: rgba(0, 0, 0, .46);
--scroll-thumb-active: rgba(0, 0, 0, .62);
--scroll-track: rgba(0, 0, 0, .06);
}
* {
box-sizing: border-box;
@@ -182,6 +194,50 @@
font: 12px/1.6 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
color: var(--text-dim); white-space: pre; word-break: normal;
}
/* 纯文本 / 普通 JSON 预览允许拖动选择与 Ctrl+C 复制;画布与查看器控件仍保持不可选中 */
#overlay.text-preview .title,
#overlay.text-preview .msg {
-webkit-user-select: text; user-select: text;
}
#overlay.text-preview .msg { cursor: text; }
/* 预览区滚动条:不使用原生样式。宽度 14px、滑块可见 8px,保证看得清。
颜色走 --scroll-* 变量(rgba 写死),不能用 color-mix():Eagle 内置 Chromium 108
不支持该函数,声明会整条失效,滑块就变成看不见。也不要写 scrollbar-width /
scrollbar-color,那两个标准属性在 108 上无效且易与 ::-webkit-scrollbar 混淆。 */
#overlay.text-preview .box::-webkit-scrollbar { width: 14px; height: 14px; }
#overlay.text-preview .box::-webkit-scrollbar-track { background: var(--scroll-track); }
#overlay.text-preview .box::-webkit-scrollbar-thumb {
min-height: 36px;
border: 3px solid transparent; border-radius: 999px;
background: var(--scroll-thumb);
background-clip: padding-box;
}
#overlay.text-preview .box::-webkit-scrollbar-thumb:hover {
background: var(--scroll-thumb-hover);
background-clip: padding-box;
}
#overlay.text-preview .box::-webkit-scrollbar-thumb:active {
background: var(--scroll-thumb-active);
background-clip: padding-box;
}
#overlay.text-preview .box::-webkit-scrollbar-corner { background: transparent; }
/* JSON 字段树:折叠标记与计数只用符号 / 数字,无需新增界面文案 */
#overlay.json-preview .msg { white-space: normal; }
.jt-node > summary {
list-style: none; cursor: pointer; border-radius: 4px;
}
.jt-node > summary::-webkit-details-marker { display: none; }
.jt-node > summary:hover { background: var(--hover-fill); }
.jt-node > summary::before {
content: '▾'; display: inline-block; width: 14px; color: var(--text-dim);
}
.jt-node:not([open]) > summary::before { content: '▸'; }
.jt-children { margin-left: 7px; padding-left: 11px; border-left: 1px solid var(--border); }
.jt-leaf { padding-left: 14px; }
.jt-key { color: var(--accent); }
.jt-str { color: var(--text); }
.jt-brace, .jt-count { color: var(--text-dim); }
.jt-count { margin: 0 3px; font-size: 11px; }
#overlay.hidden { display: none; }
#loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 4; background: var(--bg); }
#loading.hidden { display: none; }
@@ -421,6 +477,8 @@
function showError(title, msg) {
loadingEl.classList.add('hidden');
overlay.classList.remove('text-preview');
overlay.classList.remove('json-preview');
overlay.classList.remove('hidden');
document.getElementById('errorTitle').textContent = title;
document.getElementById('errorMsg').textContent = msg;
@@ -432,8 +490,78 @@
timelineBarEl.style.display = 'none';
animationPanel.style.display = 'none';
statusEl.style.display = 'none';
overlay.classList.add('text-preview');
showError(title, text);
overlay.classList.add('text-preview');
}
// ---------- 普通 JSON:轻量字段树(点击折叠 / 展开) ----------
const JSON_TREE_MAX_NODES = 20000; // 超过此规模退回纯文本,避免构建 DOM 卡顿
const JSON_TREE_OPEN_DEPTH = 1; // 默认展开的层级,更深的折叠
function jsonNodeCount(value, budget) {
let n = 0;
const walk = (node) => {
if (!node || typeof node !== 'object' || n > budget) return;
for (const key of Object.keys(node)) { n++; walk(node[key]); if (n > budget) return; }
};
walk(value);
return n;
}
function jsonKeySpan(key) {
const span = document.createElement('span');
span.className = 'jt-key';
span.textContent = (typeof key === 'number' ? '[' + key + ']: ' : JSON.stringify(key) + ': ');
return span;
}
function buildJsonTree(key, value, depth) {
if (value === null || typeof value !== 'object') {
const leaf = document.createElement('div');
leaf.className = 'jt-leaf';
if (key !== null) leaf.appendChild(jsonKeySpan(key));
const val = document.createElement('span');
val.textContent = typeof value === 'string' ? JSON.stringify(value) : String(value);
if (typeof value === 'string') val.className = 'jt-str';
leaf.appendChild(val);
return leaf;
}
const isArray = Array.isArray(value);
const keys = isArray ? value.map((_, i) => i) : Object.keys(value);
const node = document.createElement('details');
node.className = 'jt-node';
if (depth <= JSON_TREE_OPEN_DEPTH) node.open = true;
const summary = document.createElement('summary');
if (key !== null) summary.appendChild(jsonKeySpan(key));
const open = document.createElement('span');
open.className = 'jt-brace';
open.textContent = isArray ? '[' : '{';
const count = document.createElement('span');
count.className = 'jt-count';
count.textContent = String(keys.length);
const close = document.createElement('span');
close.className = 'jt-brace';
close.textContent = isArray ? ']' : '}';
summary.append(open, count, close);
node.appendChild(summary);
const children = document.createElement('div');
children.className = 'jt-children';
for (const k of keys) children.appendChild(buildJsonTree(k, value[k], depth + 1));
node.appendChild(children);
return node;
}
function showJsonPreview(title, value) {
const box = document.getElementById('errorMsg');
showTextPreview(title, '');
if (jsonNodeCount(value, JSON_TREE_MAX_NODES) > JSON_TREE_MAX_NODES) {
// 超大对象仍以纯文本呈现,保证打开速度与可选择性
box.textContent = JSON.stringify(value, null, 2);
return;
}
overlay.classList.add('json-preview');
box.textContent = '';
box.appendChild(buildJsonTree(null, value, 0));
}
function setStatus(msg, holdMs = 0) {
@@ -1063,10 +1191,13 @@
let count = 0;
try { count = fs.readSync(fd, buffer, 0, buffer.length, 0); } finally { fs.closeSync(fd); }
let text = buffer.subarray(0, count).toString('utf8');
const name = path.basename(filePath);
let parsed;
if (stat.size <= maxPreviewBytes) {
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { /* 保留 JSONC/原始文本 */ }
try { parsed = JSON.parse(text); } catch (e) { /* 保留 JSONC/原始文本 */ }
} else text += t('viewer.truncated1m');
showTextPreview(path.basename(filePath), text);
if (parsed !== null && typeof parsed === 'object') showJsonPreview(name, parsed);
else showTextPreview(name, parsed === undefined ? text : JSON.stringify(parsed, null, 2));
return;
}
const assets = utils.discoverAssets(filePath);