Feature: загрузка данных из Excel (.xlsx, .xls) через SheetJS
Made-with: Cursor
This commit is contained in:
12
HISTORY.md
12
HISTORY.md
@@ -1,5 +1,17 @@
|
||||
# История изменений
|
||||
|
||||
## 2026-04-06 14:00 UTC – Загрузка из Excel
|
||||
|
||||
**Проблема:** Нужна загрузка выгрузки в формате Excel при той же структуре колонок, что у CSV/JSON.
|
||||
|
||||
**Решение:** Подключён SheetJS (`xlsx` с CDN), разбор первого листа через `sheet_to_json`; расширены `accept` у поля файла и подсказки в шапке.
|
||||
|
||||
**Изменения:** `index.html` (скрипт, `parseExcelToRows`, ветка `handleFile` для `.xlsx`/`.xls`); README.
|
||||
|
||||
**Проверка:** Загрузка таблицы, сохранённой из Excel с заголовками как в CSV.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-06 08:00 UTC – Публикация на sd.gen7x.ru
|
||||
|
||||
**Проблема:** Нужен доступ к дашборду по постоянному URL извне.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Публичный URL (прод):** https://sd.gen7x.ru/ — раздача через Nginx с сервера (`/opt/gen7x/nginx/conf.d/sd.gen7x.ru.conf`).
|
||||
|
||||
Одностраничный дашборд для мониторинга инцидентов ServiceDesk: KPI, диаграммы, таблица и разбивка по сотрудникам. Работает в браузере без бэкенда — данные загружаются из CSV или JSON.
|
||||
Одностраничный дашборд для мониторинга инцидентов ServiceDesk: KPI, диаграммы, таблица и разбивка по сотрудникам. Работает в браузере без бэкенда — данные загружаются из CSV, JSON или Excel (.xlsx / .xls) с той же таблицей колонок.
|
||||
|
||||
## Запуск
|
||||
|
||||
@@ -46,6 +46,10 @@ docker compose up -d
|
||||
|
||||
Массив объектов с теми же ключами полей, что и в CSV.
|
||||
|
||||
### Excel
|
||||
|
||||
Первый лист книги; первая строка — заголовки, совпадающие с колонками CSV (те же русские названия). Поддерживаются `.xlsx` и устаревший `.xls`. Разбор в браузере через SheetJS (`xlsx`, CDN).
|
||||
|
||||
## Недельная динамика
|
||||
|
||||
График «хронология по неделям» и таблица под ним заполняются константой `WEEK_STATS` внутри `index.html` и **не** зависят от загружаемого файла. Обновляйте массив вручную при необходимости.
|
||||
@@ -54,6 +58,7 @@ docker compose up -d
|
||||
|
||||
- Chart.js (CDN)
|
||||
- PapaParse 5.4.1 (CDN)
|
||||
- SheetJS xlsx 0.18.5 (CDN) — Excel
|
||||
- Шрифты: Barlow Condensed, IBM Plex Sans, IBM Plex Mono (Google Fonts)
|
||||
|
||||
## Логи и бэкапы
|
||||
|
||||
74
index.html
74
index.html
@@ -9,6 +9,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eef1f6;
|
||||
@@ -964,15 +965,15 @@
|
||||
<span class="section-head-label">Подсистема</span>
|
||||
<span class="section-head-title">Мониторинг инцидентов</span>
|
||||
<span class="section-head-rule"></span>
|
||||
<span class="section-head-info">Формат загрузки: CSV · JSON</span>
|
||||
<span class="section-head-info">Формат загрузки: CSV · JSON · Excel</span>
|
||||
</div>
|
||||
|
||||
<main class="main">
|
||||
<div class="upload-panel">
|
||||
<span class="lbl">Источник данных</span>
|
||||
<button type="button" class="btn-upload" id="btn-upload">↑ Загрузить файл</button>
|
||||
<span class="upload-hint" id="upload-hint">Файл не выбран — CSV или JSON с колонками экспорта ServiceDesk</span>
|
||||
<input type="file" id="file-input" accept=".csv,.json" hidden />
|
||||
<span class="upload-hint" id="upload-hint">Файл не выбран — CSV, JSON или Excel (.xlsx, .xls) с теми же колонками, что в экспорте ServiceDesk</span>
|
||||
<input type="file" id="file-input" accept=".csv,.json,.xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" hidden />
|
||||
</div>
|
||||
|
||||
<div id="success" class="msg msg-ok"></div>
|
||||
@@ -2021,6 +2022,23 @@
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function parseExcelToRows(buffer) {
|
||||
if (typeof XLSX === "undefined") {
|
||||
throw new Error("Библиотека Excel не загружена (XLSX). Проверьте сеть и CDN.");
|
||||
}
|
||||
const wb = XLSX.read(buffer, { type: "array" });
|
||||
if (!wb.SheetNames || !wb.SheetNames.length) {
|
||||
throw new Error("В книге Excel нет листов");
|
||||
}
|
||||
const ws = wb.Sheets[wb.SheetNames[0]];
|
||||
const rows = XLSX.utils.sheet_to_json(ws, {
|
||||
defval: "",
|
||||
raw: false,
|
||||
blankrows: false
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file) return;
|
||||
el("upload-hint").textContent = file.name;
|
||||
@@ -2029,11 +2047,45 @@
|
||||
el("loading").classList.add("show");
|
||||
el("loading").textContent = "Загрузка и разбор файла…";
|
||||
|
||||
const lower = file.name.toLowerCase();
|
||||
const isExcel = lower.endsWith(".xlsx") || lower.endsWith(".xls");
|
||||
|
||||
function finish(rows) {
|
||||
if (!rows.length) throw new Error("Нет строк данных");
|
||||
processData(rows);
|
||||
el("loading").classList.remove("show");
|
||||
el("success").classList.add("show");
|
||||
el("success").textContent = "✓ Загружено записей: " + rows.length;
|
||||
}
|
||||
|
||||
function onFail(err) {
|
||||
el("loading").classList.remove("show");
|
||||
el("error").classList.add("show");
|
||||
el("error").textContent = "✗ " + (err && err.message ? err.message : String(err));
|
||||
}
|
||||
|
||||
if (isExcel) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
try {
|
||||
const buf = ev.target.result;
|
||||
const rows = parseExcelToRows(new Uint8Array(buf));
|
||||
finish(rows);
|
||||
} catch (err) {
|
||||
onFail(err);
|
||||
}
|
||||
};
|
||||
reader.onerror = function () {
|
||||
onFail(new Error("Не удалось прочитать файл"));
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
try {
|
||||
const text = String(ev.target.result || "");
|
||||
const lower = file.name.toLowerCase();
|
||||
let rows;
|
||||
if (lower.endsWith(".json")) {
|
||||
const data = JSON.parse(text);
|
||||
@@ -2046,21 +2098,13 @@
|
||||
}
|
||||
rows = parsed.data || [];
|
||||
}
|
||||
if (!rows.length) throw new Error("Нет строк данных");
|
||||
processData(rows);
|
||||
el("loading").classList.remove("show");
|
||||
el("success").classList.add("show");
|
||||
el("success").textContent = "✓ Загружено записей: " + rows.length;
|
||||
finish(rows);
|
||||
} catch (err) {
|
||||
el("loading").classList.remove("show");
|
||||
el("error").classList.add("show");
|
||||
el("error").textContent = "✗ " + (err && err.message ? err.message : String(err));
|
||||
onFail(err);
|
||||
}
|
||||
};
|
||||
reader.onerror = function () {
|
||||
el("loading").classList.remove("show");
|
||||
el("error").classList.add("show");
|
||||
el("error").textContent = "✗ Не удалось прочитать файл";
|
||||
onFail(new Error("Не удалось прочитать файл"));
|
||||
};
|
||||
reader.readAsText(file, "UTF-8");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user