feat: add web UI with sidebar TOC and CLI args for json_to_excel
This commit is contained in:
+200
-4
@@ -6,18 +6,212 @@
|
||||
<title>x-data-spreadsheet</title>
|
||||
<style>
|
||||
body { margin:0; padding:20px; font-family:Arial,sans-serif; background-color:#f5f5f5; }
|
||||
#xspreadsheet { width:100%; height:600px; box-shadow:0 2px 10px rgba(0,0,0,0.1); background-color:white; }
|
||||
.layout { display:flex; gap:20px; align-items:flex-start; }
|
||||
.main { flex:1 1 auto; min-width:0; }
|
||||
#xspreadsheet { width:100%; height:calc(100vh - 140px); min-height:600px; box-shadow:0 2px 10px rgba(0,0,0,0.1); background-color:white; }
|
||||
.toc-sidebar {
|
||||
flex: 0 0 200px;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||||
padding: 12px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toc-sidebar h3 {
|
||||
margin: 0 14px 8px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 0 14px 8px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.toc-sheet-group { margin-bottom: 10px; }
|
||||
.toc-sheet-name {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin: 4px 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.toc-list { list-style: none; padding: 0; margin: 0; }
|
||||
.toc-list li { margin: 0; }
|
||||
.toc-link {
|
||||
display: block;
|
||||
padding: 5px 14px;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
border-left: 2px solid transparent;
|
||||
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.toc-link:hover { background: #f0f7ff; color: #1677ff; }
|
||||
.toc-link.active {
|
||||
background: #e6f4ff;
|
||||
color: #1677ff;
|
||||
border-left-color: #1677ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.toc-empty { font-size: 12px; color: #999; padding: 8px 14px; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://unpkg.com/x-data-spreadsheet@1.1.9/dist/xspreadsheet.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>x-data-spreadsheet</h1>
|
||||
<div id="xspreadsheet"></div>
|
||||
<div class="layout">
|
||||
<div class="main">
|
||||
<div id="xspreadsheet"></div>
|
||||
</div>
|
||||
<aside class="toc-sidebar">
|
||||
<h3>目录</h3>
|
||||
<div id="toc-container"></div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/x-data-spreadsheet@1.1.9/dist/xspreadsheet.js"></script>
|
||||
<script>
|
||||
const sheetData = {{DATA}};
|
||||
const rawData = {{DATA}};
|
||||
// Normalize: x_spreadsheet.loadData accepts an array of sheets or a single sheet
|
||||
const sheets = Array.isArray(rawData) ? rawData : [rawData];
|
||||
|
||||
// ---- TOC builder ----
|
||||
function cellText(row, col) {
|
||||
if (!row || !row.cells) return null;
|
||||
const c = row.cells[col];
|
||||
if (!c) return null;
|
||||
return (c.text || '').trim() || null;
|
||||
}
|
||||
|
||||
function extractToc(sheet) {
|
||||
const items = [];
|
||||
const rows = sheet.rows || {};
|
||||
const len = rows.len || 0;
|
||||
const seen = new Set();
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const r = rows[i];
|
||||
if (!r || !r.cells) continue;
|
||||
|
||||
const c0 = cellText(r, 0);
|
||||
const c1 = cellText(r, 1);
|
||||
const c2 = cellText(r, 2);
|
||||
|
||||
// 1) Top-level labels in column 0 (基本要求 / 外观质量 / 工程质量等级评定 / ...)
|
||||
if (c0 && !seen.has('c0:' + c0)) {
|
||||
items.push({ label: c0, row: i });
|
||||
seen.add('c0:' + c0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) Numbered items in column 1 (项次: 1A, 2A, 3, 4, 5, 6, 7, 8, ...)
|
||||
if (c1 && /^[0-9]+[A-Za-z]?$/.test(c1)) {
|
||||
const label = c1 + (c2 ? ' ' + c2 : '');
|
||||
if (!seen.has('c1:' + c1)) {
|
||||
items.push({ label, row: i });
|
||||
seen.add('c1:' + c1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function renderToc(sheets) {
|
||||
const container = document.getElementById('toc-container');
|
||||
container.innerHTML = '';
|
||||
let hasAny = false;
|
||||
|
||||
sheets.forEach((sheet, sheetIdx) => {
|
||||
const items = extractToc(sheet);
|
||||
if (!items.length && sheets.length === 1) return;
|
||||
|
||||
hasAny = true;
|
||||
const group = document.createElement('div');
|
||||
group.className = 'toc-sheet-group';
|
||||
|
||||
if (sheets.length > 1) {
|
||||
const name = document.createElement('div');
|
||||
name.className = 'toc-sheet-name';
|
||||
name.textContent = sheet.name || ('Sheet ' + (sheetIdx + 1));
|
||||
group.appendChild(name);
|
||||
}
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
ul.className = 'toc-list';
|
||||
items.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.className = 'toc-link';
|
||||
a.href = 'javascript:void(0)';
|
||||
a.dataset.sheet = sheetIdx;
|
||||
a.dataset.row = item.row;
|
||||
a.title = item.label + ' (row ' + item.row + ')';
|
||||
a.innerHTML = item.label + '<span class="row-tag" style="color:#bbb;font-size:10.5px;margin-left:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;">·' + item.row + '</span>';
|
||||
a.addEventListener('click', () => scrollToRow(sheetIdx, item.row, a));
|
||||
li.appendChild(a);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
group.appendChild(ul);
|
||||
container.appendChild(group);
|
||||
});
|
||||
|
||||
if (!hasAny) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'toc-empty';
|
||||
empty.textContent = '(未识别到目录项)';
|
||||
container.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scroll handling ----
|
||||
// x-data-spreadsheet renders the grid on a canvas; rows are NOT in the DOM.
|
||||
// We scroll by setting the vertical scrollbar's scrollTop, which fires
|
||||
// the library's internal moveFn -> scrolly() -> re-render.
|
||||
let __ssRef = null; // populated after load
|
||||
|
||||
function getSheetData(sheetIdx) {
|
||||
if (!__ssRef) return null;
|
||||
// spreadsheet.datas[] holds the pt instances; .data is shorthand for datas[0]
|
||||
if (Array.isArray(__ssRef.datas) && __ssRef.datas[sheetIdx]) return __ssRef.datas[sheetIdx];
|
||||
if (sheetIdx === 0 && __ssRef.data) return __ssRef.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
function scrollToRow(sheetIdx, rowIndex, linkEl) {
|
||||
if (!__ssRef) return;
|
||||
|
||||
// Mark active
|
||||
document.querySelectorAll('.toc-link.active').forEach(el => el.classList.remove('active'));
|
||||
if (linkEl) linkEl.classList.add('active');
|
||||
|
||||
try {
|
||||
const liveData = getSheetData(sheetIdx);
|
||||
if (!liveData || !liveData.rows || typeof liveData.rows.sumHeight !== 'function') {
|
||||
console.warn('No live sheet data for index', sheetIdx);
|
||||
return;
|
||||
}
|
||||
const y = liveData.rows.sumHeight(0, rowIndex);
|
||||
const vbar = __ssRef.sheet && __ssRef.sheet.verticalScrollbar;
|
||||
if (vbar && vbar.el && vbar.el[0]) {
|
||||
vbar.el[0].scrollTop = y;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('scrollToRow failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Init ----
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof x_spreadsheet === 'undefined') {
|
||||
document.body.innerHTML += '<p style="color:red;margin-top:10px;">Error: x-spreadsheet library failed to load. Check internet connection.</p>';
|
||||
@@ -25,7 +219,9 @@
|
||||
}
|
||||
try {
|
||||
const spreadsheet = x_spreadsheet('#xspreadsheet');
|
||||
spreadsheet.loadData(sheetData);
|
||||
spreadsheet.loadData(sheets);
|
||||
__ssRef = spreadsheet;
|
||||
renderToc(sheets);
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
document.body.innerHTML += '<p style="color:red;margin-top:10px;">Error: ' + e.message + '</p>';
|
||||
|
||||
Reference in New Issue
Block a user