Files
BusSchedule127/web/index.html
ua1zbe 269a651325 Считается прибытие в Кандалакшу
Для направления Полярные Зори → Кандалакша время прибытия в таблице
расписания отсутствует, поэтому оно теперь рассчитывается: отправление
из Полярных Зорь плюс 50 минут. Время в пути взято из самой таблицы —
в 12 строках из 13 Кандалакша → Полярные Зори занимает ровно 50 минут.

Заодно исправлена ошибка из v2.2: в карточке Зашейка колонка 3 подписана
как «прибытие Кандалакша», хотя это время в Полярных Зорях (20–30 минут
после прибытия в Зашеек, а не час до Кандалакши). Теперь в карточке
Зашейка показываются оба прибытия: Полярные Зори, затем Кандалакша.

Модель направлений переведена на список прибытий arrs, где время берётся
либо из колонки таблицы (col), либо считается как offset от отправления.
2026-09-28 06:44:56 +00:00

284 lines
12 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="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Автобус №127 · Кандалакша — Полярные Зори — Зашеек</title>
<style>
:root{
--bg:#0d1220; --bg2:#131a2e; --card:#182238; --card2:#1e2a45;
--line:#2a3757; --txt:#e8edf7; --muted:#8b98b8;
--accent:#38d39f; --accent2:#4f9cff; --warn:#ffb648;
}
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,sans-serif;
background:radial-gradient(1200px 600px at 70% -10%,#1b2a4d 0%,var(--bg) 55%);
color:var(--txt);min-height:100vh;padding:24px 16px 48px}
.wrap{max-width:760px;margin:0 auto}
header{text-align:center;margin-bottom:18px}
.route{font-size:12px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
h1{font-size:clamp(22px,4.5vw,30px);margin:6px 0 4px;font-weight:700}
.sub{color:var(--muted);font-size:14px}
.clock{font-size:22px;font-weight:600;margin-top:10px;color:var(--accent)}
.clock small{color:var(--muted);font-weight:400;font-size:12px}
.day{display:inline-block;margin-top:8px;padding:4px 12px;border-radius:999px;
font-size:12px;border:1px solid var(--line);color:var(--muted)}
.day b{color:var(--accent2)}
.pickers{display:flex;gap:8px;margin:16px 0}
.pick{flex:1;padding:13px 8px;border-radius:12px;border:1px solid var(--line);
background:var(--card);color:var(--muted);font-size:14px;cursor:pointer;
transition:.15s;text-align:center;font-weight:600}
.pick:hover{border-color:var(--accent2);color:var(--txt)}
.pick.active{background:linear-gradient(135deg,var(--accent2),var(--accent));
color:#04101c;border-color:transparent;box-shadow:0 6px 20px rgba(79,156,255,.25)}
.where{text-align:center;color:var(--muted);font-size:13px;margin:2px 0 12px}
.dir{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.dir .card:only-child{grid-column:1 / -1} /* одна карточка — во всю ширину */
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px}
.card h3{margin:0 0 4px;font-size:15px;font-weight:700}
.card h3 .arr{color:var(--accent)}
.card .via{color:var(--muted);font-size:11px;margin-bottom:10px;letter-spacing:.03em}
.trip{display:flex;align-items:baseline;justify-content:space-between;padding:8px 0;
border-bottom:1px dashed var(--line);font-size:15px}
.trip:last-child{border-bottom:none}
.trip .t{font-variant-numeric:tabular-nums;font-weight:700;font-size:19px}
.trip .t small{font-weight:400;color:var(--muted);font-size:11px;display:block}
.trip .w{color:var(--accent);font-size:12px;font-weight:600;white-space:nowrap}
.trip.soon .w{color:var(--warn)}
.empty{color:var(--muted);font-size:13px;padding:6px 0}
/* кнопка полного расписания */
.fullbtn{display:block;width:100%;margin:16px 0 6px;padding:12px;border-radius:12px;
border:1px solid var(--line);background:var(--card2);color:var(--txt);
font-size:14px;font-weight:600;cursor:pointer;transition:.15s}
.fullbtn:hover{border-color:var(--accent2)}
.fullbtn .chev{float:right;transition:.2s;color:var(--muted)}
.fullbtn.open .chev{transform:rotate(180deg)}
.tablecard{background:var(--card);border:1px solid var(--line);border-radius:14px;
overflow:hidden;display:none;margin-bottom:8px}
.thead{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;background:var(--card2);
font-size:11px;color:var(--muted);text-align:center}
.thead div{padding:11px 6px;border-left:1px solid var(--line)}
.thead div:first-child{border-left:none}
.thead b{display:block;color:var(--txt);font-size:12px;font-weight:600}
.row{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;text-align:center;border-top:1px solid var(--line);
font-variant-numeric:tabular-nums;font-size:15px}
.row div{padding:10px 6px;border-left:1px solid var(--line)}
.row div:first-child{border-left:none}
.row:nth-child(even){background:#15203a}
footer{margin-top:22px;text-align:center;color:var(--muted);font-size:12px}
@media(max-width:560px){.dir{grid-template-columns:1fr}.pickers{gap:6px}}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="route">Мурманская область · регулярный маршрут</div>
<h1>Автобус №127</h1>
<div class="sub">Кандалакша — Полярные Зори — Зашеек</div>
<div class="clock" id="clock">--:--:-- <small>МСК</small></div>
<div class="day" id="dayBadge"></div>
</header>
<div class="pickers">
<button class="pick" data-stop="Кандалакша">Кандалакша</button>
<button class="pick active" data-stop="Полярные Зори">Полярные Зори</button>
<button class="pick" data-stop="Зашеек">Зашеек</button>
</div>
<div class="where" id="whereLabel">Вы в Полярных Зорях — доступные направления:</div>
<section class="dir" id="dirBox"></section>
<button class="fullbtn" id="fullBtn">Полное расписание <span class="chev">▾</span></button>
<section class="tablecard" id="fullTable">
<div class="thead">
<div><b>Кандалакша</b>отпр.</div>
<div><b>Полярные Зори</b>→ Зашеек</div>
<div><b>Зашеек</b>прибытие</div>
<div><b>Полярные Зори</b>→ Кандалакша</div>
</div>
<div id="tableBody"></div>
</section>
<footer>Расписание действует с 30.09.2020 · время московское (МСК)</footer>
</div>
<script>
const SCHEDULE = [
["05:20","06:20","06:30","06:50"],
["06:10","07:00","07:25","07:45"],
["07:40","08:30","09:00","09:20"],
["08:00","08:50","09:20","09:50"],
["09:45","10:35","11:05","11:25"],
["11:00","11:50","12:20","12:40"],
["12:30","13:20","13:50","14:10"],
["13:30","14:20","14:50","15:10"],
["14:00","14:50","15:25","15:50"],
["15:30","16:20","17:00","17:25"],
["16:50","17:40","18:10","18:35"],
["18:20","19:10","19:40","20:05"],
["20:20","21:10","21:40","22:05"]
];
const TRIPS_SHOWN = 5; // сколько ближайших рейсов показывать в карточке
// Время в пути Полярные Зори ↔ Кандалакша. В таблице расписания его нет:
// в 12 строках из 13 Кандалакша → ПЗ занимает ровно 50 минут
// (в первой строке 60 — по всей видимости опечатка).
const RIDE_PZ_KAND = 50;
// arrs — список прибытий: {label, col} — время из колонки таблицы,
// {label, offset} — «время в пути» от отправления (если col не задан).
const DIRECTIONS = {
// От Кандалакши и Зашейка все рейсы идут через Полярные Зори — одна карточка
"Кандалакша": [
{to:"Полярные Зори, Зашеек", dep:0, via:"через Полярные Зори",
arrs:[{label:"Зашеек", col:2}]}
],
"Полярные Зори": [
{to:"Зашеек", dep:1, via:"≈ 30 мин в пути",
arrs:[{label:"Зашеек", col:2}]},
{to:"Кандалакша", dep:3, via:"≈ 50 мин в пути",
arrs:[{label:"Кандалакша", offset:RIDE_PZ_KAND}]}
],
"Зашеек": [
{to:"Полярные Зори, Кандалакша", dep:2, via:"через Полярные Зори",
arrs:[{label:"Полярные Зори", col:3},
{label:"Кандалакша", col:3, offset:RIDE_PZ_KAND}]}
]
};
const DAYS = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
function toMin(t){ const [h,m]=t.split(":").map(Number); return h*60+m; }
// Текущее время по МСК, независимо от часового пояса устройства
const MSK_TZ = "Europe/Moscow";
let mskFmt = null;
try {
mskFmt = new Intl.DateTimeFormat("en-US", {
timeZone: MSK_TZ, hourCycle: "h23",
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", second: "2-digit"
});
} catch(e){ mskFmt = null; }
function nowMs(){
if (mskFmt) {
try {
const parts = {};
mskFmt.formatToParts(new Date()).forEach(x => parts[x.type] = x.value);
const hour = Number(parts.hour) % 24; // на части движков полночь приходит как "24"
return {
hour: hour,
minute: Number(parts.minute),
second: Number(parts.second),
day: new Date(Date.UTC(Number(parts.year), Number(parts.month) - 1,
Number(parts.day))).getUTCDay()
};
} catch(e){ /* ниже — запасной вариант */ }
}
// Запасной вариант: МСК = UTC+3 (переход на летнее время отменён)
const d = new Date(Date.now() + 3*3600*1000);
return { hour: d.getUTCHours(), minute: d.getUTCMinutes(),
second: d.getUTCSeconds(), day: d.getUTCDay() };
}
function fmtHM(m){
const p = n=>String(n).padStart(2,"0");
m = ((m % 1440) + 1440) % 1440; // на случай перехода через полночь
return `${p(Math.floor(m/60))}:${p(m%60)}`;
}
function fmtWait(m){
if(m <= 0) return "отправление сейчас";
const h = Math.floor(m/60), mm = m%60;
if(h === 0) return `через ${mm} мин`;
if(mm === 0) return `через ${h} ч`;
return `через ${h} ч ${mm} мин`;
}
let currentStop = "Полярные Зори";
function render(){
const now = nowMs();
const curMin = now.hour*60 + now.minute;
const dirs = DIRECTIONS[currentStop];
document.getElementById("dirBox").innerHTML = dirs.map(d=>{
const upcoming = SCHEDULE
.map((r,ri)=>{
const dep = toMin(r[d.dep]);
const arrs = d.arrs.map(a=>{
const base = a.col!==undefined ? toMin(r[a.col]) : dep;
return {label:a.label, t:fmtHM(base + (a.offset||0))};
});
return {dep, arrs, ri};
})
.filter(x=>x.dep >= curMin)
.sort((a,b)=>a.dep-b.dep)
.slice(0,TRIPS_SHOWN)
.map(x=>{
const w = x.dep - curMin;
// .trip .t small имеет display:block, поэтому каждый <small> — своя строка
const arr = x.arrs.map(a=>`<small>прибытие ${a.label} ${a.t}</small>`).join("");
return `<div class="trip ${w<15?'soon':''}">
<span class="t">${SCHEDULE[x.ri][d.dep]}${arr}</span>
<span class="w">${fmtWait(w)}</span>
</div>`;
}).join("") || `<div class="empty">на сегодня всё — первый завтра в ${SCHEDULE[0][d.dep]}</div>`;
return `<div class="card">
<h3>→ <span class="arr">${d.to}</span></h3>
<div class="via">${d.via}</div>
${upcoming}
</div>`;
}).join("");
}
function renderTable(){
document.getElementById("tableBody").innerHTML = SCHEDULE.map(row =>
`<div class="row"><div>${row[0]}</div><div>${row[1]}</div><div>${row[2]}</div><div>${row[3]}</div></div>`
).join("");
}
function renderDay(){
const dow = nowMs().day;
document.getElementById("dayBadge").innerHTML = `Сегодня <b>${DAYS[dow]}</b>`;
}
function tickClock(){
const d = nowMs();
const p = n=>String(n).padStart(2,"0");
document.getElementById("clock").innerHTML = `${p(d.hour)}:${p(d.minute)}:${p(d.second)} <small>МСК</small>`;
}
document.querySelectorAll(".pick").forEach(btn=>{
btn.addEventListener("click",()=>{
document.querySelectorAll(".pick").forEach(b=>b.classList.remove("active"));
btn.classList.add("active");
currentStop = btn.dataset.stop;
const w = {Кандалакша:"в Кандалакше", "Полярные Зори":"в Полярных Зорях", Зашеек:"в Зашейке"};
document.getElementById("whereLabel").textContent =
`Вы ${w[currentStop]} — доступные направления:`;
render();
});
});
document.getElementById("fullBtn").addEventListener("click",()=>{
const t = document.getElementById("fullTable");
const b = document.getElementById("fullBtn");
const open = t.style.display === "block";
t.style.display = open ? "none" : "block";
b.classList.toggle("open", !open);
});
tickClock(); renderDay(); render(); renderTable();
setInterval(tickClock,1000);
setInterval(render,30000);
setInterval(renderDay,60000);
</script>
</body>
</html>