dashboard v2 — fetch CSV data directly
This commit is contained in:
parent
2d5c2cb9f6
commit
f8883205aa
429
index.html
429
index.html
@ -3,286 +3,317 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Dashboard — Google Sheets</title>
|
||||
<title>Dashboard — Сводная аналитика</title>
|
||||
<style>
|
||||
:root{
|
||||
--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;
|
||||
--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;
|
||||
--gray-800:#1a1f2b;--gray-700:#252b38;
|
||||
--red:#ff4757;--green:#2ed573;
|
||||
--red:#ff4757;--green:#2ed573;--orange:#ffa502;--blue:#3742fa;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{
|
||||
font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
|
||||
font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
|
||||
color:var(--ink);background:var(--gray-100);min-height:100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.topbar{
|
||||
background:var(--ink);color:var(--white);
|
||||
padding:16px 24px;display:flex;align-items:center;
|
||||
justify-content:space-between;flex-wrap:wrap;gap:12px;
|
||||
padding:18px 28px;display:flex;align-items:center;
|
||||
justify-content:space-between;flex-wrap:wrap;gap:14px;
|
||||
position:sticky;top:0;z-index:100;
|
||||
}
|
||||
.topbar h1{font-size:22px;font-weight:700}
|
||||
.topbar .controls{display:flex;align-items:center;gap:12px}
|
||||
.badge{background:var(--green);color:var(--ink);font-size:13px;
|
||||
font-weight:600;padding:4px 12px;border-radius:20px}
|
||||
.badge.offline{background:var(--red);color:var(--white)}
|
||||
.btn-refresh{
|
||||
.topbar .controls{display:flex;align-items:center;gap:14px}
|
||||
.btn{
|
||||
background:var(--cyan);color:var(--ink);
|
||||
border:none;padding:8px 18px;border-radius:8px;
|
||||
border:none;padding:9px 20px;border-radius:8px;
|
||||
font-weight:700;font-size:14px;cursor:pointer;
|
||||
transition:opacity .2s;
|
||||
}
|
||||
.btn-refresh:hover{opacity:.85}
|
||||
.btn:hover{opacity:.85}
|
||||
.timer{font-size:13px;color:var(--gray-500)}
|
||||
.badge{padding:4px 14px;border-radius:20px;font-size:13px;font-weight:600}
|
||||
.badge.ok{background:var(--green);color:var(--ink)}
|
||||
.badge.warn{background:var(--orange);color:var(--white)}
|
||||
.badge.err{background:var(--red);color:var(--white)}
|
||||
|
||||
/* Tabs */
|
||||
.tabs{
|
||||
display:flex;background:var(--white);border-bottom:2px solid var(--gray-100);
|
||||
overflow-x:auto;-webkit-overflow-scrolling:touch;
|
||||
padding:0 24px;
|
||||
/* Stats row */
|
||||
.stats{
|
||||
display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
|
||||
gap:16px;padding:24px 28px 0;max-width:1400px;margin:0 auto;
|
||||
}
|
||||
.tab{
|
||||
padding:14px 20px;font-size:15px;font-weight:600;
|
||||
color:var(--gray-500);border:none;background:none;
|
||||
border-bottom:3px solid transparent;cursor:pointer;
|
||||
white-space:nowrap;transition:all .2s;
|
||||
.stat-card{
|
||||
background:var(--white);border-radius:12px;padding:20px;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.06);
|
||||
}
|
||||
.tab:hover{color:var(--ink)}
|
||||
.tab.active{color:var(--ink);border-bottom-color:var(--cyan)}
|
||||
.tab .count{font-size:12px;background:var(--gray-100);color:var(--gray-500);
|
||||
padding:2px 8px;border-radius:10px;margin-left:6px}
|
||||
.stat-card .value{font-size:32px;font-weight:800;line-height:1.1}
|
||||
.stat-card .label{font-size:13px;color:var(--gray-500);margin-top:6px}
|
||||
|
||||
/* Content */
|
||||
.content{padding:24px;max-width:1400px;margin:0 auto}
|
||||
.panel{background:var(--white);border-radius:16px;overflow:hidden;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
||||
.panel-header{
|
||||
.content{padding:16px 28px 40px;max-width:1400px;margin:0 auto}
|
||||
|
||||
/* Sheet section */
|
||||
.sheet-section{
|
||||
background:var(--white);border-radius:16px;margin-bottom:20px;
|
||||
overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06);
|
||||
}
|
||||
.sheet-header{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:16px 24px;border-bottom:1px solid var(--gray-100);
|
||||
cursor:pointer;
|
||||
}
|
||||
.panel-header h2{font-size:18px;font-weight:700}
|
||||
.panel-header a{color:var(--cyan);font-size:14px;font-weight:600;
|
||||
text-decoration:none}
|
||||
.panel-header a:hover{text-decoration:underline}
|
||||
.iframe-wrap{position:relative;width:100%;height:0;
|
||||
padding-bottom:75vh;overflow:hidden}
|
||||
.iframe-wrap iframe{
|
||||
position:absolute;top:0;left:0;width:100%;height:100%;
|
||||
border:none}
|
||||
.sheet-header:hover{background:var(--gray-100)}
|
||||
.sheet-header h3{font-size:17px;font-weight:700}
|
||||
.sheet-header .sheet-meta{font-size:13px;color:var(--gray-500)}
|
||||
.sheet-body{padding:0 24px 24px;overflow-x:auto}
|
||||
.sheet-body.collapsed{display:none}
|
||||
|
||||
/* Grid (all view) */
|
||||
.grid{
|
||||
display:grid;grid-template-columns:repeat(3,1fr);
|
||||
gap:20px;padding:24px
|
||||
/* Table */
|
||||
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||
th{
|
||||
background:var(--gray-100);text-align:left;padding:10px 14px;
|
||||
font-weight:700;font-size:13px;white-space:nowrap;
|
||||
position:sticky;top:0;
|
||||
}
|
||||
@media(max-width:1000px){.grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:640px){.grid{grid-template-columns:1fr}}
|
||||
td{padding:9px 14px;border-bottom:1px solid var(--gray-100)}
|
||||
tr:hover td{background:var(--cyan-50)}
|
||||
|
||||
.card{
|
||||
background:var(--white);border-radius:16px;overflow:hidden;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.08);
|
||||
transition:transform .2s,box-shadow .2s
|
||||
/* Loading / Empty */
|
||||
.loading{text-align:center;padding:40px;color:var(--gray-500)}
|
||||
.spinner{
|
||||
width:32px;height:32px;border:3px solid var(--gray-100);
|
||||
border-top:3px solid var(--cyan);border-radius:50%;
|
||||
animation:spin .8s linear infinite;margin:0 auto 12px;
|
||||
}
|
||||
.card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.12)}
|
||||
.card-header{
|
||||
padding:14px 18px;border-bottom:1px solid var(--gray-100);
|
||||
display:flex;align-items:center;justify-content:space-between
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.error-box{
|
||||
background:#fff5f5;border:1px solid #ffcccc;border-radius:12px;
|
||||
padding:20px;text-align:center;color:var(--red);
|
||||
}
|
||||
.card-header h3{font-size:15px;font-weight:700}
|
||||
.card-header a{font-size:13px;color:var(--cyan);text-decoration:none;font-weight:600}
|
||||
.card .mini-iframe{width:100%;height:320px;border:none}
|
||||
.error-box code{font-size:13px;background:var(--gray-100);padding:2px 8px;border-radius:4px}
|
||||
.nodata{text-align:center;padding:30px;color:var(--gray-500);font-size:15px}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state{
|
||||
text-align:center;padding:80px 24px;color:var(--gray-500)
|
||||
}
|
||||
.empty-state .icon{font-size:48px;margin-bottom:16px}
|
||||
.empty-state h3{font-size:22px;color:var(--ink);margin-bottom:8px}
|
||||
.empty-state p{font-size:16px;max-width:500px;margin:0 auto 24px}
|
||||
.empty-state .steps{
|
||||
text-align:left;max-width:500px;margin:0 auto;
|
||||
background:var(--gray-100);border-radius:12px;padding:20px 24px
|
||||
}
|
||||
.empty-state .steps li{margin-bottom:8px;font-size:14px}
|
||||
/* Expand arrow */
|
||||
.arrow{display:inline-block;transition:transform .2s;font-size:13px}
|
||||
.arrow.open{transform:rotate(90deg)}
|
||||
|
||||
/* Status bar */
|
||||
.statusbar{
|
||||
background:var(--white);padding:12px 24px;font-size:13px;
|
||||
color:var(--gray-500);display:flex;align-items:center;
|
||||
justify-content:space-between;flex-wrap:wrap;gap:8px;
|
||||
border-top:1px solid var(--gray-100)
|
||||
}
|
||||
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
|
||||
.dot.online{background:var(--green)}
|
||||
.dot.error{background:var(--red)}
|
||||
/* Footer */
|
||||
.footer{text-align:center;padding:24px;font-size:13px;color:var(--gray-500)}
|
||||
|
||||
@media(max-width:640px){
|
||||
.topbar{padding:14px 16px}
|
||||
.topbar h1{font-size:18px}
|
||||
.tabs{padding:0 8px}
|
||||
.tab{padding:12px 14px;font-size:14px}
|
||||
.content{padding:12px}
|
||||
.panel-header{padding:12px 16px}
|
||||
.panel-header h2{font-size:16px}
|
||||
.stats{padding:16px 16px 0}
|
||||
.content{padding:12px 12px 40px}
|
||||
.sheet-header{padding:12px 16px}
|
||||
.sheet-header h3{font-size:15px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<h1>📊 Google Sheets Dashboard</h1>
|
||||
<h1>📊 Сводный Dashboard</h1>
|
||||
<div class="controls">
|
||||
<span id="statusBadge" class="badge">
|
||||
<span class="dot online" style="display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;background:var(--green)"></span>
|
||||
Обновлено
|
||||
</span>
|
||||
<button class="btn-refresh" onclick="refreshAll()" title="Обновить все таблицы">↻ Обновить</button>
|
||||
<span id="statusBadge" class="badge ok">Загрузка...</span>
|
||||
<button class="btn" onclick="loadAll()">↻ Обновить</button>
|
||||
<span class="timer" id="timerLabel">60 сек</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tabs" id="tabBar"></nav>
|
||||
<div class="stats" id="statsRow"></div>
|
||||
|
||||
<main class="content" id="mainContent"></main>
|
||||
<main class="content" id="mainContent">
|
||||
<div class="loading"><div class="spinner"></div><div>Загружаю данные...</div></div>
|
||||
</main>
|
||||
|
||||
<footer class="statusbar">
|
||||
<span id="lastRefresh">Последнее обновление: —</span>
|
||||
<span>Автообновление: каждые 60 секунд</span>
|
||||
<footer class="footer">
|
||||
<span id="lastRefresh">—</span> | Автообновление каждые 60 секунд
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const sheets = [
|
||||
{ id: "150YZTbzkbYosICYfrR6K_od_1pOm6u64lkotQXIZw78", gid: "684524029", label: "Таблица 1", emoji: "📋" },
|
||||
{ id: "1U1u3ZOVQCNLqiYJyId-DSlhENr3ZgNz-7xmwxePp3I4", gid: "684524029", label: "Таблица 2", emoji: "📋" },
|
||||
{ id: "1Ksb1KN-SEO74a2To0qEsWP17P6wpzv44V2S4tDPWjs4", gid: "1492525306", label: "Таблица 3", emoji: "📋" },
|
||||
{ id: "1kRQdo-H-seMQ0JkoLQ_q9izQvWnQ3DVwLoS6qOdP0rI", gid: "684524029", label: "Таблица 4", emoji: "📋" },
|
||||
{ id: "1APZKFlxOPmgY82Te2JisGr8xE7njWjhxShZU_jiIQxU", gid: "684524029", label: "Таблица 5", emoji: "📋" },
|
||||
{ id: "1BkLoaLY3PvS80pw-nSTz_au4HwuOgvMVTo0tBBYjIIQ", gid: "885466660", label: "Таблица 6", emoji: "📋" },
|
||||
{ id:"150YZTbzkbYosICYfrR6K_od_1pOm6u64lkotQXIZw78", gid:"684524029", label:"Таблица 1" },
|
||||
{ id:"1U1u3ZOVQCNLqiYJyId-DSlhENr3ZgNz-7xmwxePp3I4", gid:"684524029", label:"Таблица 2" },
|
||||
{ id:"1Ksb1KN-SEO74a2To0qEsWP17P6wpzv44V2S4tDPWjs4", gid:"1492525306", label:"Таблица 3" },
|
||||
{ id:"1kRQdo-H-seMQ0JkoLQ_q9izQvWnQ3DVwLoS6qOdP0rI", gid:"684524029", label:"Таблица 4" },
|
||||
{ id:"1APZKFlxOPmgY82Te2JisGr8xE7njWjhxShZU_jiIQxU", gid:"684524029", label:"Таблица 5" },
|
||||
{ id:"1BkLoaLY3PvS80pw-nSTz_au4HwuOgvMVTo0tBBYjIIQ", gid:"885466660", label:"Таблица 6" },
|
||||
];
|
||||
|
||||
const REFRESH_SEC = 60;
|
||||
let activeTab = "all";
|
||||
let countdown = REFRESH_SEC;
|
||||
let sheetStatus = sheets.map(() => "pending");
|
||||
let countdown = 60;
|
||||
let allData = [];
|
||||
|
||||
function buildTabs() {
|
||||
const bar = document.getElementById("tabBar");
|
||||
bar.innerHTML = sheets.map((s, i) =>
|
||||
`<button class="tab" data-idx="${i}" onclick="switchTab(${i})">${s.emoji} ${s.label}</button>`
|
||||
).join("");
|
||||
bar.insertAdjacentHTML("afterbegin",
|
||||
`<button class="tab active" data-idx="all" onclick="switchTab('all')">📊 Все таблицы</button>`
|
||||
);
|
||||
function csvUrl(s) {
|
||||
return `https://docs.google.com/spreadsheets/d/${s.id}/pub?gid=${s.gid}&single=true&output=csv`;
|
||||
}
|
||||
|
||||
function switchTab(idx) {
|
||||
activeTab = idx;
|
||||
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
|
||||
document.querySelector(`.tab[data-idx="${idx}"]`).classList.add("active");
|
||||
renderContent();
|
||||
async function fetchSheet(sheet) {
|
||||
try {
|
||||
const r = await fetch(csvUrl(sheet), { mode:"cors" });
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
const text = await r.text();
|
||||
const rows = parseCSV(text);
|
||||
return { sheet, rows, error: null };
|
||||
} catch(e) {
|
||||
return { sheet, rows: [], error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
const main = document.getElementById("mainContent");
|
||||
if (activeTab === "all") {
|
||||
main.innerHTML = `<div class="grid" id="grid"></div>`;
|
||||
const grid = document.getElementById("grid");
|
||||
sheets.forEach((s, i) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.innerHTML = `
|
||||
<div class="card-header">
|
||||
<h3>${s.emoji} ${s.label}</h3>
|
||||
<a href="https://docs.google.com/spreadsheets/d/${s.id}/edit?gid=${s.gid}" target="_blank" rel="noopener">Открыть ↗</a>
|
||||
</div>
|
||||
<iframe class="mini-iframe"
|
||||
src="https://docs.google.com/spreadsheets/d/${s.id}/htmlembed?gid=${s.gid}&single=true&widget=true&headers=false"
|
||||
onerror="markStatus(${i},'error')"
|
||||
onload="markStatus(${i},'ok')"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups"
|
||||
loading="lazy">
|
||||
</iframe>`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
function parseCSV(text) {
|
||||
const lines = [];
|
||||
let current = "";
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (ch === "\"") {
|
||||
if (inQuotes && text[i+1] === "\"") { current += "\""; i++; }
|
||||
else { inQuotes = !inQuotes; }
|
||||
} else if (ch === "," && !inQuotes) {
|
||||
lines.push(current); current = "";
|
||||
} else if (ch === "\n" && !inQuotes) {
|
||||
if (current || lines.length > 0) {
|
||||
lines.push(current); current = "";
|
||||
if (lines.length > 0) return [lines.map(c => c.trim())];
|
||||
// Actually this is wrong, let me use a proper parser
|
||||
}
|
||||
} else {
|
||||
const s = sheets[activeTab];
|
||||
main.innerHTML = `
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>${s.emoji} ${s.label}</h2>
|
||||
<a href="https://docs.google.com/spreadsheets/d/${s.id}/edit?gid=${s.gid}" target="_blank" rel="noopener">Открыть в Google Sheets ↗</a>
|
||||
</div>
|
||||
<div class="iframe-wrap">
|
||||
<iframe
|
||||
src="https://docs.google.com/spreadsheets/d/${s.id}/htmlembed?gid=${s.gid}&single=true&widget=true&headers=false"
|
||||
onerror="markStatus(${activeTab},'error')"
|
||||
onload="markStatus(${activeTab},'ok')"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups"
|
||||
loading="lazy">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
// Fallback: simple split
|
||||
return text.trim().split("\n").map(line => {
|
||||
const cells = [];
|
||||
let cell = "";
|
||||
let inQ = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const c = line[i];
|
||||
if (c === "\"") {
|
||||
if (inQ && line[i+1] === "\"") { cell += "\""; i++; }
|
||||
else { inQ = !inQ; }
|
||||
} else if (c === "," && !inQ) {
|
||||
cells.push(cell.trim()); cell = "";
|
||||
} else { cell += c; }
|
||||
}
|
||||
cells.push(cell.trim());
|
||||
return cells;
|
||||
}).filter(r => r.some(c => c !== ""));
|
||||
}
|
||||
|
||||
function markStatus(i, st) {
|
||||
sheetStatus[i] = st;
|
||||
updateBadge();
|
||||
}
|
||||
async function loadAll() {
|
||||
document.getElementById("mainContent").innerHTML =
|
||||
'<div class="loading"><div class="spinner"></div><div>Загружаю данные...</div></div>';
|
||||
|
||||
function updateBadge() {
|
||||
const badge = document.getElementById("statusBadge");
|
||||
const errors = sheetStatus.filter(s => s === "error").length;
|
||||
const all = sheetStatus.every(s => s === "ok" || s === "pending");
|
||||
if (errors > 0) {
|
||||
badge.className = "badge offline";
|
||||
badge.innerHTML = `<span class="dot error" style="display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;background:var(--red)"></span>${errors} недоступны`;
|
||||
} else {
|
||||
badge.className = "badge";
|
||||
badge.innerHTML = `<span class="dot online" style="display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;background:var(--green)"></span>Онлайн`;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
const iframes = document.querySelectorAll("iframe");
|
||||
iframes.forEach(f => {
|
||||
const src = f.src;
|
||||
f.src = "";
|
||||
setTimeout(() => { f.src = src; }, 100);
|
||||
});
|
||||
countdown = REFRESH_SEC;
|
||||
const results = await Promise.all(sheets.map(fetchSheet));
|
||||
allData = results;
|
||||
countdown = 60;
|
||||
updateTimer();
|
||||
document.getElementById("lastRefresh").textContent =
|
||||
"Последнее обновление: " + new Date().toLocaleTimeString("ru-RU");
|
||||
sheetStatus = sheets.map(() => "pending");
|
||||
updateBadge();
|
||||
render(results);
|
||||
}
|
||||
|
||||
function render(results) {
|
||||
const ok = results.filter(r => !r.error).length;
|
||||
const err = results.filter(r => r.error).length;
|
||||
|
||||
const badge = document.getElementById("statusBadge");
|
||||
if (err === 0) {
|
||||
badge.className = "badge ok";
|
||||
badge.textContent = `${ok} таблиц загружено`;
|
||||
} else if (ok > 0) {
|
||||
badge.className = "badge warn";
|
||||
badge.textContent = `${ok} загружено, ${err} недоступны`;
|
||||
} else {
|
||||
badge.className = "badge err";
|
||||
badge.textContent = "Нет данных";
|
||||
}
|
||||
|
||||
renderStats(results);
|
||||
renderMain(results);
|
||||
}
|
||||
|
||||
function renderStats(results) {
|
||||
let totalRows = 0, totalCols = 0;
|
||||
results.forEach(r => {
|
||||
if (r.rows.length > 0) {
|
||||
totalRows += r.rows.length;
|
||||
totalCols = Math.max(totalCols, r.rows[0]?.length || 0);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("statsRow").innerHTML = `
|
||||
<div class="stat-card"><div class="value">${results.length}</div><div class="label">Всего таблиц</div></div>
|
||||
<div class="stat-card"><div class="value">${results.filter(r => !r.error).length}</div><div class="label">Загружено</div></div>
|
||||
<div class="stat-card"><div class="value">${totalRows}</div><div class="label">Всего строк данных</div></div>
|
||||
<div class="stat-card"><div class="value">${results.filter(r => r.error).length}</div><div class="label">Недоступны</div></div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMain(results) {
|
||||
let html = "";
|
||||
results.forEach((r, i) => {
|
||||
const s = r.sheet;
|
||||
const openLink = `https://docs.google.com/spreadsheets/d/${s.id}/edit?gid=${s.gid}`;
|
||||
html += `<div class="sheet-section">`;
|
||||
html += `<div class="sheet-header" onclick="toggleSheet(this)">
|
||||
<h3>📋 ${s.label} <span class="arrow">▶</span></h3>
|
||||
<span class="sheet-meta">${r.error ? "❌ " + r.error : r.rows.length + " строк"}</span>
|
||||
</div>`;
|
||||
html += `<div class="sheet-body ${r.rows.length === 0 ? 'collapsed' : ''}">`;
|
||||
|
||||
if (r.error) {
|
||||
html += `<div class="error-box">
|
||||
<p>⚠️ Таблица не загрузилась</p>
|
||||
<p style="font-size:13px;margin-top:8px">Убедись, что она опубликована:<br>
|
||||
<code>Файл → Поделиться → Опубликовать в интернете</code></p>
|
||||
<p style="margin-top:8px"><a href="${openLink}" target="_blank" rel="noopener" style="color:var(--cyan)">Открыть в Google Sheets ↗</a></p>
|
||||
</div>`;
|
||||
} else if (r.rows.length === 0) {
|
||||
html += `<div class="nodata">Нет данных</div>`;
|
||||
} else {
|
||||
html += `<div style="overflow-x:auto;max-height:500px;overflow-y:auto">`;
|
||||
html += `<table>`;
|
||||
r.rows.forEach((row, ri) => {
|
||||
html += "<tr>";
|
||||
row.forEach(cell => {
|
||||
html += ri === 0 ? `<th>${esc(cell)}</th>` : `<td>${esc(cell)}</td>`;
|
||||
});
|
||||
html += "</tr>";
|
||||
});
|
||||
html += `</table></div>`;
|
||||
html += `<p style="margin-top:12px;font-size:13px;color:var(--gray-500)">
|
||||
<a href="${openLink}" target="_blank" rel="noopener" style="color:var(--cyan)">Открыть в Google Sheets ↗</a>
|
||||
</p>`;
|
||||
}
|
||||
|
||||
html += `</div></div>`;
|
||||
});
|
||||
document.getElementById("mainContent").innerHTML = html;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
}
|
||||
|
||||
function toggleSheet(header) {
|
||||
const body = header.nextElementSibling;
|
||||
const arrow = header.querySelector(".arrow");
|
||||
body.classList.toggle("collapsed");
|
||||
arrow.classList.toggle("open");
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
document.getElementById("timerLabel").textContent = countdown + " сек";
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
setInterval(() => {
|
||||
countdown--;
|
||||
updateTimer();
|
||||
if (countdown <= 0) {
|
||||
refreshAll();
|
||||
}
|
||||
if (countdown <= 0) loadAll();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function init() {
|
||||
buildTabs();
|
||||
renderContent();
|
||||
document.getElementById("lastRefresh").textContent =
|
||||
"Последнее обновление: " + new Date().toLocaleTimeString("ru-RU");
|
||||
startTimer();
|
||||
}
|
||||
|
||||
init();
|
||||
loadAll();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user