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
|
## 2026-04-06 08:00 UTC – Публикация на sd.gen7x.ru
|
||||||
|
|
||||||
**Проблема:** Нужен доступ к дашборду по постоянному URL извне.
|
**Проблема:** Нужен доступ к дашборду по постоянному URL извне.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Публичный URL (прод):** https://sd.gen7x.ru/ — раздача через Nginx с сервера (`/opt/gen7x/nginx/conf.d/sd.gen7x.ru.conf`).
|
**Публичный 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.
|
Массив объектов с теми же ключами полей, что и в CSV.
|
||||||
|
|
||||||
|
### Excel
|
||||||
|
|
||||||
|
Первый лист книги; первая строка — заголовки, совпадающие с колонками CSV (те же русские названия). Поддерживаются `.xlsx` и устаревший `.xls`. Разбор в браузере через SheetJS (`xlsx`, CDN).
|
||||||
|
|
||||||
## Недельная динамика
|
## Недельная динамика
|
||||||
|
|
||||||
График «хронология по неделям» и таблица под ним заполняются константой `WEEK_STATS` внутри `index.html` и **не** зависят от загружаемого файла. Обновляйте массив вручную при необходимости.
|
График «хронология по неделям» и таблица под ним заполняются константой `WEEK_STATS` внутри `index.html` и **не** зависят от загружаемого файла. Обновляйте массив вручную при необходимости.
|
||||||
@@ -54,6 +58,7 @@ docker compose up -d
|
|||||||
|
|
||||||
- Chart.js (CDN)
|
- Chart.js (CDN)
|
||||||
- PapaParse 5.4.1 (CDN)
|
- PapaParse 5.4.1 (CDN)
|
||||||
|
- SheetJS xlsx 0.18.5 (CDN) — Excel
|
||||||
- Шрифты: Barlow Condensed, IBM Plex Sans, IBM Plex Mono (Google Fonts)
|
- Шрифты: 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" />
|
<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/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/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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #eef1f6;
|
--bg: #eef1f6;
|
||||||
@@ -964,15 +965,15 @@
|
|||||||
<span class="section-head-label">Подсистема</span>
|
<span class="section-head-label">Подсистема</span>
|
||||||
<span class="section-head-title">Мониторинг инцидентов</span>
|
<span class="section-head-title">Мониторинг инцидентов</span>
|
||||||
<span class="section-head-rule"></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>
|
</div>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="upload-panel">
|
<div class="upload-panel">
|
||||||
<span class="lbl">Источник данных</span>
|
<span class="lbl">Источник данных</span>
|
||||||
<button type="button" class="btn-upload" id="btn-upload">↑ Загрузить файл</button>
|
<button type="button" class="btn-upload" id="btn-upload">↑ Загрузить файл</button>
|
||||||
<span class="upload-hint" id="upload-hint">Файл не выбран — CSV или JSON с колонками экспорта ServiceDesk</span>
|
<span class="upload-hint" id="upload-hint">Файл не выбран — CSV, JSON или Excel (.xlsx, .xls) с теми же колонками, что в экспорте ServiceDesk</span>
|
||||||
<input type="file" id="file-input" accept=".csv,.json" hidden />
|
<input type="file" id="file-input" accept=".csv,.json,.xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" hidden />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="success" class="msg msg-ok"></div>
|
<div id="success" class="msg msg-ok"></div>
|
||||||
@@ -2021,6 +2022,23 @@
|
|||||||
applyFilters();
|
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) {
|
function handleFile(file) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
el("upload-hint").textContent = file.name;
|
el("upload-hint").textContent = file.name;
|
||||||
@@ -2029,11 +2047,45 @@
|
|||||||
el("loading").classList.add("show");
|
el("loading").classList.add("show");
|
||||||
el("loading").textContent = "Загрузка и разбор файла…";
|
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();
|
const reader = new FileReader();
|
||||||
reader.onload = function (ev) {
|
reader.onload = function (ev) {
|
||||||
try {
|
try {
|
||||||
const text = String(ev.target.result || "");
|
const text = String(ev.target.result || "");
|
||||||
const lower = file.name.toLowerCase();
|
|
||||||
let rows;
|
let rows;
|
||||||
if (lower.endsWith(".json")) {
|
if (lower.endsWith(".json")) {
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
@@ -2046,21 +2098,13 @@
|
|||||||
}
|
}
|
||||||
rows = parsed.data || [];
|
rows = parsed.data || [];
|
||||||
}
|
}
|
||||||
if (!rows.length) throw new Error("Нет строк данных");
|
finish(rows);
|
||||||
processData(rows);
|
|
||||||
el("loading").classList.remove("show");
|
|
||||||
el("success").classList.add("show");
|
|
||||||
el("success").textContent = "✓ Загружено записей: " + rows.length;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
el("loading").classList.remove("show");
|
onFail(err);
|
||||||
el("error").classList.add("show");
|
|
||||||
el("error").textContent = "✗ " + (err && err.message ? err.message : String(err));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.onerror = function () {
|
reader.onerror = function () {
|
||||||
el("loading").classList.remove("show");
|
onFail(new Error("Не удалось прочитать файл"));
|
||||||
el("error").classList.add("show");
|
|
||||||
el("error").textContent = "✗ Не удалось прочитать файл";
|
|
||||||
};
|
};
|
||||||
reader.readAsText(file, "UTF-8");
|
reader.readAsText(file, "UTF-8");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user