360 lines
9.3 KiB
HTML
360 lines
9.3 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Excel 查看器</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html, body {
|
||
width: 100%;
|
||
height: 100%;
|
||
font-family: system-ui, -apple-system, sans-serif;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
#app {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.header {
|
||
background: #fff;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
padding: 12px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.header .meta {
|
||
font-size: 13px;
|
||
color: #666;
|
||
}
|
||
|
||
.sheet-tabs {
|
||
display: flex;
|
||
background: #fff;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
padding: 0 20px;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.sheet-tab {
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
background: none;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.sheet-tab:hover {
|
||
color: #333;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.sheet-tab.active {
|
||
color: #1a73e8;
|
||
border-bottom-color: #1a73e8;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.content {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.table-container {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: max-content;
|
||
min-width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 14px;
|
||
}
|
||
|
||
thead {
|
||
background: #f8f9fa;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
th {
|
||
padding: 12px 16px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #333;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
td {
|
||
padding: 10px 16px;
|
||
color: #444;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
tbody tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
tbody tr.highlighted {
|
||
background: #fff3cd !important;
|
||
}
|
||
|
||
tbody tr.highlighted td {
|
||
font-weight: 500;
|
||
color: #856404;
|
||
}
|
||
|
||
.row-num {
|
||
color: #999;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
width: 50px;
|
||
border-right: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.loading, .error {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: #666;
|
||
gap: 12px;
|
||
}
|
||
|
||
.error {
|
||
color: #c00;
|
||
}
|
||
|
||
.spinner {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 3px solid #e0e0e0;
|
||
border-top-color: #1a73e8;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.truncated-banner {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
padding: 10px 20px;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
border-bottom: 1px solid #ffeaa7;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<span>加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
const HEADERS = {
|
||
"x-user-id": "user1",
|
||
"x-user-name": "42tr",
|
||
"x-role": "admin",
|
||
};
|
||
|
||
const params = new URLSearchParams(window.location.search);
|
||
const fileId = params.get("file_id") || params.get("fileId");
|
||
const sliceId = params.get("slice_id") || params.get("sliceId");
|
||
const legacySheet = params.get("sheet_name") || "";
|
||
const legacyRow = parseInt(params.get("row_num") || "0", 10);
|
||
|
||
const app = document.getElementById("app");
|
||
|
||
const showError = (msg) => {
|
||
app.innerHTML = `<div class="error"><div>❌ ${msg}</div></div>`;
|
||
};
|
||
|
||
if (!fileId) {
|
||
showError("缺少 file_id 参数");
|
||
return;
|
||
}
|
||
|
||
async function loadExcelData() {
|
||
const res = await fetch(`/api/v1/knowledge/files/${fileId}/excel-data`, { headers: HEADERS });
|
||
if (!res.ok) {
|
||
throw new Error(`请求失败: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
async function resolveHighlight() {
|
||
if (!sliceId) {
|
||
return { targetSheet: legacySheet, targetRow: legacyRow };
|
||
}
|
||
try {
|
||
const res = await fetch(
|
||
`/api/v1/knowledge/files/${fileId}/slices/${sliceId}/highlight`,
|
||
{ headers: HEADERS }
|
||
);
|
||
if (!res.ok) {
|
||
return { targetSheet: "", targetRow: 0 };
|
||
}
|
||
const data = await res.json();
|
||
const pos = data.positions?.[0];
|
||
return {
|
||
targetSheet: pos?.sheet_name || "",
|
||
targetRow: pos?.row_num || 0,
|
||
};
|
||
} catch (err) {
|
||
return { targetSheet: "", targetRow: 0 };
|
||
}
|
||
}
|
||
|
||
Promise.all([loadExcelData(), resolveHighlight()])
|
||
.then(([data, highlight]) => {
|
||
render(data, highlight.targetSheet, highlight.targetRow);
|
||
})
|
||
.catch((err) => {
|
||
showError(err.message || "加载 Excel 数据失败");
|
||
});
|
||
|
||
function render(data, targetSheet, targetRow) {
|
||
if (!data.sheets || data.sheets.length === 0) {
|
||
showError("Excel 文件为空或没有数据");
|
||
return;
|
||
}
|
||
|
||
// 找到目标 sheet,如果没有匹配则默认第一个
|
||
let activeIndex = 0;
|
||
if (targetSheet) {
|
||
const idx = data.sheets.findIndex((s) => s.name === targetSheet);
|
||
if (idx >= 0) activeIndex = idx;
|
||
}
|
||
|
||
const renderSheet = (index) => {
|
||
const sheet = data.sheets[index];
|
||
const tableHtml = buildTable(sheet, targetRow);
|
||
document.getElementById("table-area").innerHTML = tableHtml;
|
||
|
||
// 更新 tab 样式
|
||
document.querySelectorAll(".sheet-tab").forEach((tab, i) => {
|
||
tab.classList.toggle("active", i === index);
|
||
});
|
||
|
||
// 滚动到高亮行
|
||
if (targetRow > 0) {
|
||
const highlighted = document.querySelector("tr.highlighted");
|
||
if (highlighted) {
|
||
highlighted.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
}
|
||
}
|
||
};
|
||
|
||
const tabsHtml = data.sheets
|
||
.map((s, i) => `<button class="sheet-tab ${i === activeIndex ? "active" : ""}" data-index="${i}">${escapeHtml(s.name)}</button>`)
|
||
.join("");
|
||
|
||
app.innerHTML = `
|
||
<div class="header">
|
||
<h1>${escapeHtml(data.filename)}</h1>
|
||
<span class="meta">${data.sheets.length} 个 sheet</span>
|
||
</div>
|
||
<div class="sheet-tabs">${tabsHtml}</div>
|
||
<div class="content">
|
||
<div id="table-area"></div>
|
||
</div>
|
||
`;
|
||
|
||
// Tab 切换事件
|
||
document.querySelectorAll(".sheet-tab").forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
renderSheet(parseInt(tab.dataset.index, 10));
|
||
});
|
||
});
|
||
|
||
renderSheet(activeIndex);
|
||
}
|
||
|
||
function buildTable(sheet, targetRow) {
|
||
const truncatedBanner = sheet.truncated
|
||
? `<div class="truncated-banner">数据量较大,仅展示前 ${sheet.rows.length} 行</div>`
|
||
: "";
|
||
|
||
const headerHtml = sheet.headers
|
||
.map((h) => `<th>${escapeHtml(h)}</th>`)
|
||
.join("");
|
||
|
||
const rowsHtml = sheet.rows
|
||
.map((row, idx) => {
|
||
const rowNum = idx + 2; // 1-based row number (header is row 1)
|
||
const isHighlighted = rowNum === targetRow;
|
||
const cells = row
|
||
.map((cell) => `<td>${escapeHtml(cell)}</td>`)
|
||
.join("");
|
||
return `<tr class="${isHighlighted ? "highlighted" : ""}"><td class="row-num">${rowNum}</td>${cells}</tr>`;
|
||
})
|
||
.join("");
|
||
|
||
return `
|
||
${truncatedBanner}
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th class="row-num">#</th>
|
||
${headerHtml}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (str == null) return "";
|
||
return String(str)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|