dsh-long-plugins 1.3.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/client/client.js +284 -9
- package/client/vendor/xlsx.full.min.js +22 -0
- package/lib/index.js +408 -30
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -503,36 +503,95 @@ async function docxHtml(buf) {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
/**
|
|
506
|
+
/** Excel 列号(A=1, AA=27)。 */
|
|
507
|
+
function colToNum(letters) {
|
|
508
|
+
let n = 0;
|
|
509
|
+
for (const ch of String(letters).toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64);
|
|
510
|
+
return n;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** 渲染 .xlsx 为 HTML 表格(多工作表 + 合并单元格 + 表头样式,bound 保持预览量)。 */
|
|
507
514
|
async function xlsxHtml(buf) {
|
|
508
515
|
try {
|
|
509
516
|
const workbook = new ExcelJS.Workbook();
|
|
510
517
|
await workbook.xlsx.load(buf);
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
518
|
+
const sheets = workbook.worksheets.slice(0, 3);
|
|
519
|
+
if (sheets.length === 0) return "<p>(空工作簿)</p>";
|
|
520
|
+
let html = "";
|
|
521
|
+
for (const sheet of sheets) {
|
|
522
|
+
// 合并范围:topLeft -> {rowspan, colspan};并记录被覆盖的格子。
|
|
523
|
+
// exceljs 版本差异:mergedCells 可能是 getter;用 model.merges 兜底(格式 "A1:C1")。
|
|
524
|
+
const merges = (sheet.mergedCells && sheet.mergedCells.length ? sheet.mergedCells : (sheet.model && sheet.model.merges)) || [];
|
|
525
|
+
const merged = new Map();
|
|
526
|
+
const covered = new Set();
|
|
527
|
+
for (const range of merges) {
|
|
528
|
+
const m = /^([A-Z]+)(\d+):([A-Z]+)(\d+)$/.exec(String(range));
|
|
529
|
+
if (!m) continue;
|
|
530
|
+
const c1 = colToNum(m[1]), r1 = parseInt(m[2], 10), c2 = colToNum(m[3]), r2 = parseInt(m[4], 10);
|
|
531
|
+
merged.set(`${r1},${c1}`, { rowspan: r2 - r1 + 1, colspan: c2 - c1 + 1 });
|
|
532
|
+
for (let rr = r1; rr <= r2; rr++) for (let cc = c1; cc <= c2; cc++) if (!(rr === r1 && cc === c1)) covered.add(`${rr},${cc}`);
|
|
533
|
+
}
|
|
534
|
+
const rowLimit = 500, colLimit = 32;
|
|
535
|
+
const rows = [];
|
|
536
|
+
sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
|
|
537
|
+
if (rows.length >= rowLimit) return;
|
|
538
|
+
const rn = rowNumber;
|
|
539
|
+
const cells = [];
|
|
540
|
+
for (let col = 1; col <= Math.min(row.cellCount, colLimit); col += 1) {
|
|
541
|
+
if (covered.has(`${rn},${col}`)) continue;
|
|
542
|
+
let value = row.getCell(col).value;
|
|
543
|
+
if (value !== null && typeof value === "object") {
|
|
544
|
+
if (value.richText) value = value.richText.map((t) => t.text).join("");
|
|
545
|
+
else if (value.text !== undefined) value = value.text;
|
|
546
|
+
else if (value.result !== undefined) value = value.result;
|
|
547
|
+
else value = "";
|
|
548
|
+
}
|
|
549
|
+
const span = merged.get(`${rn},${col}`);
|
|
550
|
+
const attr = span ? (span.colspan > 1 ? ` colspan="${span.colspan}"` : "") + (span.rowspan > 1 ? ` rowspan="${span.rowspan}"` : "") : "";
|
|
551
|
+
cells.push(`<td${attr}>${escHtml(value ?? "")}</td>`);
|
|
527
552
|
}
|
|
528
|
-
|
|
553
|
+
rows.push(`<tr>${cells.join("")}</tr>`);
|
|
554
|
+
});
|
|
555
|
+
const header = rows.length > 0 ? `<thead>${rows[0].replace(/<td/g, "<th").replace(/<\/td>/g, "</th>")}</thead>` : "";
|
|
556
|
+
const body = rows.length > 1 ? `<tbody>${rows.slice(1).join("")}</tbody>` : "";
|
|
557
|
+
html += `<div class="sheet"><h3>${escHtml(sheet.name || "Sheet")}</h3><table>${header}${body}</table></div>`;
|
|
558
|
+
}
|
|
559
|
+
const css = "<style>table{border-collapse:collapse;width:100%;font-size:12px;margin-bottom:14px}th,td{border:1px solid #cfd5dd;padding:4px 8px;white-space:pre-wrap;word-break:break-all;vertical-align:top}thead th{background:#f0f3f8;font-weight:600;text-align:left}.sheet h3{font-size:14px;margin:8px 0 4px;color:#1f3a5f}</style>";
|
|
560
|
+
return `${css}${html}`;
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 解析 CSV/TSV 文本为 HTML 表格(首行当作表头,bound)。 */
|
|
567
|
+
function csvHtml(text) {
|
|
568
|
+
try {
|
|
569
|
+
const limit = 500;
|
|
570
|
+
const rows = [];
|
|
571
|
+
const parse = (line) => {
|
|
572
|
+
const out = [];
|
|
573
|
+
let cur = "", inQ = false;
|
|
574
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
575
|
+
const ch = line[i];
|
|
576
|
+
if (inQ) {
|
|
577
|
+
if (ch === '"') { if (line[i + 1] === '"') { cur += '"'; i += 1; } else inQ = false; }
|
|
578
|
+
else cur += ch;
|
|
579
|
+
} else if (ch === '"') { inQ = true; }
|
|
580
|
+
else if (ch === "," || ch === "\t") { out.push(cur); cur = ""; }
|
|
581
|
+
else cur += ch;
|
|
529
582
|
}
|
|
530
|
-
|
|
531
|
-
|
|
583
|
+
out.push(cur);
|
|
584
|
+
return out;
|
|
585
|
+
};
|
|
586
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
587
|
+
if (rows.length >= limit) break;
|
|
588
|
+
if (line.trim() === "") continue;
|
|
589
|
+
rows.push(parse(line));
|
|
532
590
|
}
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
const
|
|
591
|
+
if (rows.length === 0) return "<p>(空)</p>";
|
|
592
|
+
const header = `<thead><tr>${rows[0].map((c) => `<th>${escHtml(c)}</th>`).join("")}</tr></thead>`;
|
|
593
|
+
const body = rows.length > 1 ? `<tbody>${rows.slice(1).map((r) => `<tr>${r.map((c) => `<td>${escHtml(c)}</td>`).join("")}</tr>`).join("")}</tbody>` : "";
|
|
594
|
+
const css = "<style>table{border-collapse:collapse;width:100%;font-size:12px}th,td{border:1px solid #cfd5dd;padding:4px 8px;white-space:pre-wrap;word-break:break-all;vertical-align:top}thead th{background:#f0f3f8;font-weight:600;text-align:left}</style>";
|
|
536
595
|
return `${css}<table>${header}${body}</table>`;
|
|
537
596
|
} catch {
|
|
538
597
|
return null;
|
|
@@ -603,9 +662,36 @@ async function officePreviewHtml(name, buffer) {
|
|
|
603
662
|
if (ext === ".docx") return docxHtml(buffer);
|
|
604
663
|
if (ext === ".xlsx") return xlsxHtml(buffer);
|
|
605
664
|
if (ext === ".pptx") return pptxHtml(buffer);
|
|
665
|
+
if (ext === ".csv" || ext === ".tsv") return csvHtml(buffer.toString("utf8"));
|
|
606
666
|
return null;
|
|
607
667
|
}
|
|
608
668
|
|
|
669
|
+
/** 仅保留毛玻璃配置允许字段并 clamp,避免写入任意/超大内容。背景图只收 data:image/*;base64 且限 2MB。 */
|
|
670
|
+
function sanitizeGlass(input) {
|
|
671
|
+
const out = {};
|
|
672
|
+
if ("enabled" in input) out.enabled = input.enabled === true;
|
|
673
|
+
if ("blur" in input) { const n = Number(input.blur); out.blur = Number.isFinite(n) ? Math.max(0, Math.min(80, Math.round(n))) : undefined; }
|
|
674
|
+
if ("mask" in input) { const n = Number(input.mask); out.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
|
|
675
|
+
if ("color" in input) out.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(input.color)) ? String(input.color) : undefined;
|
|
676
|
+
const sanitizeUri = (v) => { const s = String(v ?? ""); return (/^data:image\/(png|jpe?g|webp|gif);base64,/.test(s) && s.length <= 2 * 1024 * 1024 + 256) ? s : undefined; };
|
|
677
|
+
if ("bgImage" in input) out.bgImage = sanitizeUri(input.bgImage);
|
|
678
|
+
// 会话区/输入区各自独立罩色+罩强度
|
|
679
|
+
if (input.zone && typeof input.zone === "object") {
|
|
680
|
+
const zo = {};
|
|
681
|
+
for (const key of ["session", "input"]) {
|
|
682
|
+
const z = input.zone[key];
|
|
683
|
+
if (z && typeof z === "object") {
|
|
684
|
+
const o = {};
|
|
685
|
+
if ("color" in z) o.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(z.color)) ? String(z.color) : undefined;
|
|
686
|
+
if ("mask" in z) { const n = Number(z.mask); o.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
|
|
687
|
+
zo[key] = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (Object.keys(zo).length) out.zone = zo;
|
|
691
|
+
}
|
|
692
|
+
return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined));
|
|
693
|
+
}
|
|
694
|
+
|
|
609
695
|
export function createHandlers(options = {}) {
|
|
610
696
|
const root = resolve(options.root || resolveUploadRoot());
|
|
611
697
|
const maxFileBytes = positiveInteger(options.maxFileBytes, resolveMaxFileBytes());
|
|
@@ -819,9 +905,11 @@ export function createHandlers(options = {}) {
|
|
|
819
905
|
const buffer = await readFile(full);
|
|
820
906
|
const binary = buffer.subarray(0, 8192).includes(0);
|
|
821
907
|
const truncated = buffer.length > PREVIEW_LIMIT;
|
|
822
|
-
|
|
908
|
+
// Office 二进制(docx/xlsx/pptx)或 CSV/TSV 文本 → 表格 HTML 预览
|
|
909
|
+
const tableHtml = (binary && OFFICE_EXTS.has(extname(name).toLowerCase())) || /\.(csv|tsv)$/i.test(name)
|
|
823
910
|
? await officePreviewHtml(name, buffer)
|
|
824
911
|
: undefined;
|
|
912
|
+
const officeHtml = tableHtml;
|
|
825
913
|
// Markdown 返回内联样式后的完整 HTML 片段(含 MD_CSS),供前端 srcDoc 渲染真实效果,
|
|
826
914
|
// 避免内嵌带独立头部的 workspace-preview 页面导致按钮重复、放大失效。
|
|
827
915
|
const mdHtml = !binary && /\.(md|markdown)$/i.test(name)
|
|
@@ -1270,7 +1358,274 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
|
|
|
1270
1358
|
}
|
|
1271
1359
|
};
|
|
1272
1360
|
|
|
1273
|
-
|
|
1361
|
+
// ---- xlsx 真实预览(浏览器端渲染,所见即所得)----
|
|
1362
|
+
// 返回一个自包含 HTML:引 SheetJS(xlsx.full.min.js) 解析 .xlsx,转换成 x-spreadsheet
|
|
1363
|
+
// 的 data 结构,用 x-spreadsheet 渲染成真实电子表格网格(行列表头/合并/冻结/缩放)。
|
|
1364
|
+
// 仅供参考;纯浏览器端渲染,不依赖 NAS 端转换,效果与 Excel/浏览器一致。
|
|
1365
|
+
const xlsxPreviewPage = async (req, res) => {
|
|
1366
|
+
try {
|
|
1367
|
+
requireTrusted(req);
|
|
1368
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1369
|
+
methodNotAllowed(res, ["GET", "HEAD"]);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const rel = (() => {
|
|
1373
|
+
try {
|
|
1374
|
+
return decodeURIComponent(new URL(req.url || "/", "http://dsh.internal").searchParams.get("path") || "");
|
|
1375
|
+
} catch {
|
|
1376
|
+
return "";
|
|
1377
|
+
}
|
|
1378
|
+
})();
|
|
1379
|
+
// 只允许 .xlsx(避免把别的文件喂给 SheetJS/x-spreadsheet)
|
|
1380
|
+
if (!/\.xlsx$/i.test(rel)) throw new HttpError(400, "only .xlsx supported");
|
|
1381
|
+
const full = safeResolve(workspaceRoot, rel);
|
|
1382
|
+
if (full === undefined) throw new HttpError(400, "invalid path");
|
|
1383
|
+
const info = await stat(full);
|
|
1384
|
+
if (!info.isFile()) throw new HttpError(400, "not a regular file");
|
|
1385
|
+
const name = rel.split("/").pop() || "file";
|
|
1386
|
+
const downloadHref = `workspace-file?path=${encodeURIComponent(rel)}&download=1`;
|
|
1387
|
+
const inlineHref = `workspace-file?path=${encodeURIComponent(rel)}&inline=1`;
|
|
1388
|
+
const assetBase = "/api/dsh-uploads/xlsx-preview-asset";
|
|
1389
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
1390
|
+
res.end(`<!DOCTYPE html>
|
|
1391
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1392
|
+
<title>预览 - ${escapeHtml(name)}</title>
|
|
1393
|
+
<style>
|
|
1394
|
+
html,body{margin:0;height:100%;background:#1a2530;color:#e5e7eb;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
|
|
1395
|
+
.bar{position:sticky;top:0;z-index:20;display:flex;gap:8px;padding:10px 14px;align-items:center;background:#1a2530;border-bottom:1px solid #2c3a47;flex-wrap:wrap}
|
|
1396
|
+
.bar strong{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e5e7eb;font-size:13px}
|
|
1397
|
+
.bar button,.bar a{padding:6px 12px;border:1px solid #2c3a47;border-radius:8px;background:transparent;color:#e5e7eb;text-decoration:none;font-size:13px;cursor:pointer;white-space:nowrap}
|
|
1398
|
+
.tabs{display:flex;gap:6px;padding:8px 14px;background:#141f2b;border-bottom:1px solid #2c3a47;overflow:auto}
|
|
1399
|
+
.tabs button{padding:4px 12px;border:1px solid #2c3a47;border-radius:7px;background:transparent;color:#9ca3af;font-size:12px;cursor:pointer;white-space:nowrap;flex:none}
|
|
1400
|
+
.tabs button.on{background:#2563eb;color:#fff;border-color:#2563eb}
|
|
1401
|
+
// 静态图:整表一次性画到 canvas。拖动=移动/滚动一张画好的图,不重绘;放大用 CSS width/height 变化(与 ppt 预览一致)。
|
|
1402
|
+
#stage{position:relative;overflow:auto;height:calc(100vh - 48px);background:#e9ebee;padding:18px}
|
|
1403
|
+
#wrap{display:block;width:max-content;margin:0 auto;background:#fff;box-shadow:0 2px 14px rgba(0,0,0,.18)}
|
|
1404
|
+
canvas{display:block}
|
|
1405
|
+
#msg{color:#9ca3af;padding:24px;text-align:center;font-size:14px;font-family:inherit}
|
|
1406
|
+
</style>
|
|
1407
|
+
</head><body>
|
|
1408
|
+
<div class="bar">
|
|
1409
|
+
<strong>${escapeHtml(name)}</strong>
|
|
1410
|
+
<span style="display:flex;gap:4px;align-items:center">
|
|
1411
|
+
<button type="button" title="缩小" onclick="zoomBy(0.8)">−</button>
|
|
1412
|
+
<button type="button" title="放大" onclick="zoomBy(1.25)">+</button>
|
|
1413
|
+
<button type="button" title="适合宽度" onclick="zoomFit()">适合</button>
|
|
1414
|
+
</span>
|
|
1415
|
+
<a href="${downloadHref}" download>下载</a>
|
|
1416
|
+
<button type="button" onclick="closePreview()">✕ 关闭</button>
|
|
1417
|
+
</div>
|
|
1418
|
+
<div class="tabs" id="tabs"></div>
|
|
1419
|
+
<div id="stage"><div id="wrap"><canvas id="canvas"></canvas><div id="msg">加载中…</div></div></div>
|
|
1420
|
+
<script src="${assetBase}?f=xlsx.full.min.js"></script>
|
|
1421
|
+
<script>
|
|
1422
|
+
function closePreview(){
|
|
1423
|
+
try{ if(window.self!==window.top&&window.parent){ window.parent.postMessage({type:'dsh-close-preview'},location.origin); return; } }catch(e){ /* 跨域忽略 */ }
|
|
1424
|
+
window.close();
|
|
1425
|
+
}
|
|
1426
|
+
const stage=document.getElementById('stage');
|
|
1427
|
+
const canvas=document.getElementById('canvas');
|
|
1428
|
+
const wrap=document.getElementById('wrap');
|
|
1429
|
+
const msg=document.getElementById('msg');
|
|
1430
|
+
const tabsEl=document.getElementById('tabs');
|
|
1431
|
+
const dpr=Math.min(window.devicePixelRatio||1, 2);
|
|
1432
|
+
const HDR=26, IDXW=48, ROWH=26;
|
|
1433
|
+
const FONT='13px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif';
|
|
1434
|
+
const FONTB='bold 13px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif';
|
|
1435
|
+
var zoom=1, fitMode=true, natW=800, natH=600, wb=null, sheetIdx=0;
|
|
1436
|
+
function fmt(v){
|
|
1437
|
+
if(v===null||v===undefined) return '';
|
|
1438
|
+
if(v instanceof Date){ const p=(n)=>String(n).padStart(2,'0'); return v.getFullYear()+'-'+p(v.getMonth()+1)+'-'+p(v.getDate()); }
|
|
1439
|
+
return String(v);
|
|
1440
|
+
}
|
|
1441
|
+
function colLetter(c){ c+=1; var s=''; while(c>0){ var m=(c-1)%26; s=String.fromCharCode(65+m)+s; c=(c-m-1)/26; } return s; }
|
|
1442
|
+
function applyZoom(){
|
|
1443
|
+
canvas.style.width=Math.round(natW*zoom)+'px';
|
|
1444
|
+
canvas.style.height=Math.round(natH*zoom)+'px';
|
|
1445
|
+
}
|
|
1446
|
+
function zoomFit(){
|
|
1447
|
+
zoom=Math.min(1,((stage.clientWidth-36)||600)/natW); fitMode=true; applyZoom();
|
|
1448
|
+
}
|
|
1449
|
+
function zoom100(){ zoom=1; fitMode=false; applyZoom(); }
|
|
1450
|
+
function zoomBy(f){ zoom=Math.min(8,Math.max(0.05,zoom*f)); fitMode=false; applyZoom(); }
|
|
1451
|
+
function renderTabs(){
|
|
1452
|
+
if(!wb) return;
|
|
1453
|
+
tabsEl.innerHTML='';
|
|
1454
|
+
wb.SheetNames.forEach(function(nm,i){
|
|
1455
|
+
var b=document.createElement('button'); b.textContent=nm;
|
|
1456
|
+
if(i===sheetIdx) b.className='on';
|
|
1457
|
+
b.onclick=function(){ sheetIdx=i; renderSheet(); };
|
|
1458
|
+
tabsEl.appendChild(b);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
function renderSheet(){
|
|
1462
|
+
msg.style.display='';
|
|
1463
|
+
var ws=wb.Sheets[wb.SheetNames[sheetIdx]];
|
|
1464
|
+
var range=ws['!ref']?XLSX.utils.decode_range(ws['!ref']):{s:{r:0,c:0},e:{r:0,c:0}};
|
|
1465
|
+
var mc=document.createElement('canvas').getContext('2d'); mc.font=FONT;
|
|
1466
|
+
var texts={}, maxCols={};
|
|
1467
|
+
for(var r=range.s.r;r<=range.e.r;r++){
|
|
1468
|
+
for(var c=range.s.c;c<=range.e.c;c++){
|
|
1469
|
+
var cell=ws[XLSX.utils.encode_cell({r:r,c:c})];
|
|
1470
|
+
var t='';
|
|
1471
|
+
if(cell){ if(cell.t==='n')t=String(cell.v); else if(cell.t==='b')t=cell.v?'TRUE':'FALSE'; else t=fmt(cell.v); }
|
|
1472
|
+
texts[r+'_'+c]=t;
|
|
1473
|
+
if(t){ var w=mc.measureText(t).width+18; if(!maxCols[c]||w>maxCols[c])maxCols[c]=w; }
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
var colW={};
|
|
1477
|
+
for(var c=range.s.c;c<=range.e.c;c++){ colW[c]=Math.max(64,Math.min(380,maxCols[c]||120)); }
|
|
1478
|
+
var colX={}, acc=IDXW;
|
|
1479
|
+
for(var c=range.s.c;c<=range.e.c;c++){ colX[c]=acc; acc+=colW[c]; }
|
|
1480
|
+
var rowY={}, accy=HDR;
|
|
1481
|
+
for(var r=range.s.r;r<=range.e.r;r++){ rowY[r]=accy; accy+=ROWH; }
|
|
1482
|
+
// 合并单元格:记录左上角跨的格数,其余标记为 covered(不画独立边框/文字)
|
|
1483
|
+
var mergeMap={}, covered={};
|
|
1484
|
+
(ws['!merges']||[]).forEach(function(m){
|
|
1485
|
+
mergeMap[m.s.r+'_'+m.s.c]={rs:m.e.r-m.s.r+1, cs:m.e.c-m.s.c+1};
|
|
1486
|
+
for(var rr=m.s.r;rr<=m.e.r;rr++)for(var cc=m.s.c;cc<=m.e.c;cc++){ if(rr===m.s.r&&cc===m.s.c)continue; covered[rr+'_'+cc]=1; }
|
|
1487
|
+
});
|
|
1488
|
+
var totalW=acc, totalH=accy; natW=totalW; natH=totalH;
|
|
1489
|
+
var w=Math.round(totalW*dpr), h=Math.round(totalH*dpr);
|
|
1490
|
+
var maxDim=16000;
|
|
1491
|
+
if(w>maxDim||h>maxDim){ var s=Math.min(maxDim/Math.max(1,w),maxDim/Math.max(1,h)); w=Math.round(totalW*dpr*s); h=Math.round(totalH*dpr*s); }
|
|
1492
|
+
canvas.width=w; canvas.height=h;
|
|
1493
|
+
var ctx=canvas.getContext('2d');
|
|
1494
|
+
ctx.setTransform(w/Math.max(1,totalW),0,0,h/Math.max(1,totalH),0,0);
|
|
1495
|
+
ctx.textBaseline='middle';
|
|
1496
|
+
ctx.fillStyle='#ffffff'; ctx.fillRect(0,0,totalW,totalH);
|
|
1497
|
+
// 左上角
|
|
1498
|
+
ctx.fillStyle='#e9ebee'; ctx.fillRect(0,0,IDXW,HDR);
|
|
1499
|
+
// 列标头
|
|
1500
|
+
ctx.font=FONTB; ctx.fillStyle='#374151';
|
|
1501
|
+
for(var c=range.s.c;c<=range.e.c;c++){
|
|
1502
|
+
ctx.fillStyle='#f3f4f7'; ctx.fillRect(colX[c],0,colW[c],HDR);
|
|
1503
|
+
ctx.strokeStyle='#dfe3ea'; ctx.beginPath(); ctx.moveTo(colX[c],0); ctx.lineTo(colX[c],HDR); ctx.stroke();
|
|
1504
|
+
ctx.beginPath(); ctx.moveTo(colX[c],HDR); ctx.lineTo(colX[c]+colW[c],HDR); ctx.stroke();
|
|
1505
|
+
var lx=colX[c]+colW[c]/2;
|
|
1506
|
+
ctx.fillStyle='#374151'; ctx.fillText(colLetter(c),lx,HDR/2+0.5);
|
|
1507
|
+
}
|
|
1508
|
+
// 行标头
|
|
1509
|
+
for(var r=range.s.r;r<=range.e.r;r++){
|
|
1510
|
+
ctx.fillStyle='#f3f4f7'; ctx.fillRect(0,rowY[r],IDXW,ROWH);
|
|
1511
|
+
ctx.strokeStyle='#dfe3ea'; ctx.beginPath(); ctx.moveTo(IDXW,rowY[r]); ctx.lineTo(IDXW,rowY[r]+ROWH); ctx.stroke();
|
|
1512
|
+
ctx.beginPath(); ctx.moveTo(0,rowY[r]+ROWH); ctx.lineTo(IDXW,rowY[r]+ROWH); ctx.stroke();
|
|
1513
|
+
ctx.fillStyle='#374151'; ctx.fillText(String(r+1),IDXW/2,rowY[r]+ROWH/2+0.5);
|
|
1514
|
+
}
|
|
1515
|
+
// 单元格
|
|
1516
|
+
ctx.font=FONT;
|
|
1517
|
+
for(var r=range.s.r;r<=range.e.r;r++){
|
|
1518
|
+
for(var c=range.s.c;c<=range.e.c;c++){
|
|
1519
|
+
var key=r+'_'+c;
|
|
1520
|
+
if(covered[key]) continue;
|
|
1521
|
+
var x=colX[c], y=rowY[r], ww=colW[c], hh=ROWH;
|
|
1522
|
+
var mk=mergeMap[key];
|
|
1523
|
+
if(mk){ for(var i=1;i<mk.cs;i++)ww+=colW[c+i]; for(var j=1;j<mk.rs;j++)hh+=ROWH; }
|
|
1524
|
+
ctx.fillStyle='#ffffff'; ctx.fillRect(x,y,ww,hh);
|
|
1525
|
+
ctx.strokeStyle='#e2e5ea'; ctx.strokeRect(x+0.5,y+0.5,ww-1,hh-1);
|
|
1526
|
+
var t=texts[key];
|
|
1527
|
+
if(t){
|
|
1528
|
+
var isNum=(ws[XLSX.utils.encode_cell({r:r,c:c})]||{}).t==='n';
|
|
1529
|
+
ctx.fillStyle='#1f2937';
|
|
1530
|
+
var xoff=isNum?(x+ww-6):(x+6);
|
|
1531
|
+
ctx.textAlign=isNum?'right':'left';
|
|
1532
|
+
ctx.fillText(t,xoff,y+hh/2+0.5);
|
|
1533
|
+
ctx.textAlign='left';
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
msg.style.display='none';
|
|
1538
|
+
renderTabs();
|
|
1539
|
+
zoomFit();
|
|
1540
|
+
}
|
|
1541
|
+
(async()=>{
|
|
1542
|
+
try{
|
|
1543
|
+
const r=await fetch(${JSON.stringify(inlineHref)},{cache:'no-store'});
|
|
1544
|
+
if(!r.ok) throw new Error('HTTP '+r.status);
|
|
1545
|
+
const buf=await r.arrayBuffer();
|
|
1546
|
+
wb=XLSX.read(new Uint8Array(buf),{type:'array',cellDates:true});
|
|
1547
|
+
renderSheet();
|
|
1548
|
+
}catch(e){ msg.style.display=''; msg.textContent='渲染失败:'+((e&&e.message)||e); }
|
|
1549
|
+
})();
|
|
1550
|
+
window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else applyZoom(); }catch(e){} });
|
|
1551
|
+
</script>
|
|
1552
|
+
|
|
1553
|
+
</body></html>`);
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
sendError(res, error, onError);
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
// serve SheetJS 前端库(从 PACKAGE_DIR/client/vendor 读)
|
|
1560
|
+
const xlsxPreviewAsset = async (req, res) => {
|
|
1561
|
+
try {
|
|
1562
|
+
requireTrusted(req);
|
|
1563
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1564
|
+
methodNotAllowed(res, ["GET", "HEAD"]);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const f = decodeURIComponent(new URL(req.url || "/", "http://dsh.internal").searchParams.get("f") || "");
|
|
1568
|
+
// 只允许 xlsx 预览页用到的 SheetJS 解析库(浏览器端把 .xlsx 转成静态表格图)。
|
|
1569
|
+
if (!["xlsx.full.min.js"].includes(f)) throw new HttpError(400, "unknown asset");
|
|
1570
|
+
const target = join(VENDOR_DIR, f);
|
|
1571
|
+
const buf = await readFile(target);
|
|
1572
|
+
res.writeHead(200, {
|
|
1573
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1574
|
+
"cache-control": "no-store",
|
|
1575
|
+
"x-content-type-options": "nosniff",
|
|
1576
|
+
});
|
|
1577
|
+
res.end(buf);
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
sendError(res, error, onError);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
// ---- 毛玻璃界面 (glass UI) 配置:读写 ~/.dsh-long-plugins/glass.json ----
|
|
1584
|
+
// 背景图以 data-URI 存进配置,随配置持久化。enabled=总开关;blur=磨砂模糊;mask=整页罩强度;
|
|
1585
|
+
// color=叠加/罩色;bgImage=共用背景图。顶栏/左栏的"罩"与整页共用 mask+color(不再单独存,简化)。
|
|
1586
|
+
const GLASS_DEFAULTS = {
|
|
1587
|
+
enabled: false, blur: 20, bgImage: "",
|
|
1588
|
+
zone: { session: { color: "#1a2332", mask: 0.45 }, input: { color: "#1a2332", mask: 0.6 } },
|
|
1589
|
+
};
|
|
1590
|
+
const glassDir = resolve(homedir(), ".dsh-long-plugins");
|
|
1591
|
+
const glassFile = join(glassDir, "glass.json");
|
|
1592
|
+
async function readGlassJSON() {
|
|
1593
|
+
try { const raw = JSON.parse(await readFile(glassFile, "utf8")); return { ...GLASS_DEFAULTS, ...(raw && typeof raw === "object" ? raw : {}) }; }
|
|
1594
|
+
catch { return { ...GLASS_DEFAULTS }; }
|
|
1595
|
+
}
|
|
1596
|
+
const glassConfig = async (req, res) => {
|
|
1597
|
+
try {
|
|
1598
|
+
requireTrusted(req);
|
|
1599
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
1600
|
+
const cfg = await readGlassJSON();
|
|
1601
|
+
sendJson(res, 200, {
|
|
1602
|
+
found: true,
|
|
1603
|
+
cfg: {
|
|
1604
|
+
enabled: cfg.enabled, blur: cfg.blur,
|
|
1605
|
+
session: cfg.zone?.session || { color: "#1a2332", mask: 0.45 },
|
|
1606
|
+
input: cfg.zone?.input || { color: "#1a2332", mask: 0.6 },
|
|
1607
|
+
bgImage: cfg.bgImage ? { present: true, bytes: Math.round((cfg.bgImage.length * 3) / 4) } : { present: false },
|
|
1608
|
+
},
|
|
1609
|
+
bgImage: cfg.bgImage,
|
|
1610
|
+
});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (req.method === "POST") {
|
|
1614
|
+
const body = await readJsonBody(req);
|
|
1615
|
+
const current = await readGlassJSON();
|
|
1616
|
+
const next = { ...current, ...sanitizeGlass(typeof body === "object" && body !== null ? body : {}) };
|
|
1617
|
+
await mkdir(glassDir, { recursive: true, mode: 0o700 });
|
|
1618
|
+
await writeFile(glassFile, JSON.stringify(next, null, 2), "utf8");
|
|
1619
|
+
sendJson(res, 200, { ok: true, saved: true });
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
methodNotAllowed(res, ["GET", "HEAD", "POST"]);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
sendError(res, error, onError);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig };
|
|
1274
1629
|
}
|
|
1275
1630
|
|
|
1276
1631
|
/** 人类可读文件大小。 */
|
|
@@ -1324,21 +1679,23 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
|
|
|
1324
1679
|
const isPdf = /\.pdf$/i.test(f.name);
|
|
1325
1680
|
const isDocx = /\.docx$/i.test(f.name);
|
|
1326
1681
|
const isPptx = /\.pptx$/i.test(f.name);
|
|
1682
|
+
const isXlsx = /\.xlsx$/i.test(f.name);
|
|
1327
1683
|
// PDF 直接嵌原始流(单层 iframe,浏览器原生查看器可滚动翻页);
|
|
1328
|
-
// docx 走 docx-preview、pptx 走 PptxViewJS
|
|
1684
|
+
// docx 走 docx-preview、pptx 走 PptxViewJS、xlsx 走 x-spreadsheet 真实渲染页
|
|
1685
|
+
// (浏览器端解析,所见即所得);
|
|
1329
1686
|
// 其它可预览类型走渲染页;不可预览 → 下载。
|
|
1330
1687
|
const viewHref = previewable
|
|
1331
|
-
? (isPdf ? `workspace-file?path=${rel}&inline=1` : isDocx ? `docx-preview?path=${rel}` : isPptx ? `pptx-preview?path=${rel}` : `workspace-preview?path=${rel}&from=list`)
|
|
1688
|
+
? (isPdf ? `workspace-file?path=${rel}&inline=1` : isDocx ? `docx-preview?path=${rel}` : isPptx ? `pptx-preview?path=${rel}` : isXlsx ? `xlsx-preview?path=${rel}` : `workspace-preview?path=${rel}&from=list`)
|
|
1332
1689
|
: `workspace-file?path=${rel}&download=1`;
|
|
1333
1690
|
return `<tr data-ts="${Math.floor(f.mtime)}" data-name="${escapeHtml(f.name)}" data-size="${f.size}">
|
|
1334
1691
|
<td class="name"><a class="flink" href="${viewHref}" data-preview="${escapeHtml(viewHref)}" title="${escapeHtml(f.path)}">${escapeHtml(f.name)}</a></td>
|
|
1335
1692
|
<td class="size">${humanSize(f.size)}</td>
|
|
1336
1693
|
<td class="time">${humanTime(f.mtime)}</td>
|
|
1337
1694
|
<td class="acts">
|
|
1338
|
-
|
|
1695
|
+
<button type="button" class="tag plain" data-copypath="${escapeHtml(f.path)}">复制路径</button>
|
|
1696
|
+
<button type="button" class="tag plain" data-rename="${escapeHtml(f.path)}">重命名</button>
|
|
1339
1697
|
<a class="tag dl" href="workspace-file?path=${rel}&download=1">${ICON_DL} 下载</a>
|
|
1340
|
-
|
|
1341
|
-
<button type="button" class="tag" data-copypath="${escapeHtml(f.path)}">复制路径</button>
|
|
1698
|
+
${previewable ? `<a class="tag" href="${viewHref}" data-preview="${escapeHtml(viewHref)}">${ICON_EYE} 预览</a>` : ""}
|
|
1342
1699
|
</td>
|
|
1343
1700
|
</tr>`;
|
|
1344
1701
|
}).join("");
|
|
@@ -1441,6 +1798,9 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
|
|
|
1441
1798
|
.tag { display:inline-flex; align-items:center; gap:4px; text-decoration:none; font-size:12px; padding:4px 10px; border-radius:6px; margin-right:6px; background:var(--lp-tag-bg); color:var(--lp-accent); }
|
|
1442
1799
|
.tag:hover { background:var(--lp-tag-hover); }
|
|
1443
1800
|
.tag.dl { color:var(--lp-ok); }
|
|
1801
|
+
/* 重命名/复制路径:不设强调色,做白色/常规文字按钮 */
|
|
1802
|
+
.tag.plain { background:transparent; color:var(--lp-fg); border:1px solid var(--lp-border); }
|
|
1803
|
+
.tag.plain:hover { background:var(--lp-hover); }
|
|
1444
1804
|
.empty { color:var(--lp-meta); text-align:center; padding:60px 0; }
|
|
1445
1805
|
.ic { width:14px; height:14px; flex:none; }
|
|
1446
1806
|
@media (max-width: 640px) {
|
|
@@ -2152,6 +2512,24 @@ export async function apply(ctx, config = {}) {
|
|
|
2152
2512
|
handler: handlers.pptxPreviewPage,
|
|
2153
2513
|
}), "dsh-long-plugins: pptx real-preview route");
|
|
2154
2514
|
|
|
2515
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2516
|
+
kind: "exact",
|
|
2517
|
+
path: "/api/dsh-uploads/xlsx-preview",
|
|
2518
|
+
handler: handlers.xlsxPreviewPage,
|
|
2519
|
+
}), "dsh-long-plugins: xlsx real-preview route");
|
|
2520
|
+
|
|
2521
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2522
|
+
kind: "exact",
|
|
2523
|
+
path: "/api/dsh-uploads/xlsx-preview-asset",
|
|
2524
|
+
handler: handlers.xlsxPreviewAsset,
|
|
2525
|
+
}), "dsh-long-plugins: xlsx-preview vendor assets");
|
|
2526
|
+
|
|
2527
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2528
|
+
kind: "exact",
|
|
2529
|
+
path: "/api/dsh-uploads/glass-config",
|
|
2530
|
+
handler: handlers.glassConfig,
|
|
2531
|
+
}), "dsh-long-plugins: glass-ui config route");
|
|
2532
|
+
|
|
2155
2533
|
ctx.effect(() => ctx.webServer.register({
|
|
2156
2534
|
kind: "exact",
|
|
2157
2535
|
path: "/api/dsh-uploads/workspace-browse",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-long-plugins",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, Markdown-to-Word (md2docx), and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Markdown 转 Word(md2docx)、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|