htknow/frontend/public/excel-viewer.html
Defeng fe8f0a875a
Some checks failed
CI / Frontend (Vite) (push) Successful in 24s
CI / Docker Release Tar (push) Has been skipped
CI / Backend (Rust) (push) Failing after 10m15s
Initial project import
2026-07-24 10:28:31 +08:00

360 lines
9.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
})();
</script>
</body>
</html>