обновил поиск следующей игры для титра и поправил commentary
This commit is contained in:
528
get_data.py
528
get_data.py
@@ -1,6 +1,5 @@
|
|||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
from fastapi.responses import Response, HTMLResponse, StreamingResponse, JSONResponse
|
from fastapi.responses import Response, HTMLResponse, StreamingResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import requests, uvicorn, json
|
import requests, uvicorn, json
|
||||||
import threading, queue
|
import threading, queue
|
||||||
@@ -16,7 +15,7 @@ import nasio
|
|||||||
import io, os, platform, time
|
import io, os, platform, time
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import re
|
import re
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
@@ -2263,6 +2262,8 @@ async def team(who: str):
|
|||||||
)
|
)
|
||||||
+ ".svg"
|
+ ".svg"
|
||||||
),
|
),
|
||||||
|
"country": (item.get("countryName") or "").strip(),
|
||||||
|
"photo_site": (item.get("photo") or "").strip(),
|
||||||
"photoGFX": (
|
"photoGFX": (
|
||||||
os.path.join(
|
os.path.join(
|
||||||
"D:\\Photos",
|
"D:\\Photos",
|
||||||
@@ -3766,6 +3767,7 @@ async def last_5_games():
|
|||||||
|
|
||||||
return wl_list[::-1]
|
return wl_list[::-1]
|
||||||
|
|
||||||
|
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
|
||||||
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
|
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
|
||||||
def find_next_game(team_id: int):
|
def find_next_game(team_id: int):
|
||||||
if not CALENDAR or "items" not in CALENDAR:
|
if not CALENDAR or "items" not in CALENDAR:
|
||||||
@@ -3806,6 +3808,11 @@ async def last_5_games():
|
|||||||
if team_id not in (t1.get("teamId"), t2.get("teamId")):
|
if team_id not in (t1.get("teamId"), t2.get("teamId")):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 👇 пропускаем ТЕКУЩИЙ матч (тот, что в GAME_ID)
|
||||||
|
gid = game_info.get("id")
|
||||||
|
if gid is not None and GAME_ID is not None and str(gid) == str(GAME_ID):
|
||||||
|
continue
|
||||||
|
|
||||||
dt_str = game_info.get("defaultZoneDateTime") or ""
|
dt_str = game_info.get("defaultZoneDateTime") or ""
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(dt_str)
|
dt = datetime.fromisoformat(dt_str)
|
||||||
@@ -3815,11 +3822,11 @@ async def last_5_games():
|
|||||||
# убираем tzinfo, чтобы можно было сравнивать с now
|
# убираем tzinfo, чтобы можно было сравнивать с now
|
||||||
if dt.tzinfo is not None:
|
if dt.tzinfo is not None:
|
||||||
dt = dt.replace(tzinfo=None)
|
dt = dt.replace(tzinfo=None)
|
||||||
# только будущие матчи
|
|
||||||
|
# только матчи ПОСЛЕ текущего момента
|
||||||
if dt <= now:
|
if dt <= now:
|
||||||
print(dt, now)
|
|
||||||
continue
|
continue
|
||||||
print(item)
|
|
||||||
# определяем соперника и место
|
# определяем соперника и место
|
||||||
if team_id == t1.get("teamId"):
|
if team_id == t1.get("teamId"):
|
||||||
opp_name = t2.get("name", "")
|
opp_name = t2.get("name", "")
|
||||||
@@ -3852,7 +3859,7 @@ async def last_5_games():
|
|||||||
"date": best["dt"].strftime("%Y-%m-%d %H:%M"),
|
"date": best["dt"].strftime("%Y-%m-%d %H:%M"),
|
||||||
"place": best["place"], # "home" / "away"
|
"place": best["place"], # "home" / "away"
|
||||||
"place_ru": place_ru, # "дома" / "в гостях"
|
"place_ru": place_ru, # "дома" / "в гостях"
|
||||||
"formatted": formatted, # 🆕 "wednesday, march 26, at home against astana"
|
"formatted": formatted, # "wednesday, march 26, at home against astana"
|
||||||
}
|
}
|
||||||
|
|
||||||
# последние 5 игр и результаты
|
# последние 5 игр и результаты
|
||||||
@@ -4031,6 +4038,8 @@ async def commentary():
|
|||||||
<th>Игрок</th>
|
<th>Игрок</th>
|
||||||
<th>PTS</th>
|
<th>PTS</th>
|
||||||
<th>REB</th>
|
<th>REB</th>
|
||||||
|
<th class="extra-col">OREB</th>
|
||||||
|
<th class="extra-col">DREB</th>
|
||||||
<th>AST</th>
|
<th>AST</th>
|
||||||
<th>STL</th>
|
<th>STL</th>
|
||||||
<th>BLK</th>
|
<th>BLK</th>
|
||||||
@@ -4069,9 +4078,11 @@ async def commentary():
|
|||||||
|
|
||||||
# REB: если нет 'reb', то dreb + oreb
|
# REB: если нет 'reb', то dreb + oreb
|
||||||
reb = p.get("reb")
|
reb = p.get("reb")
|
||||||
|
dreb_raw = p.get("dreb")
|
||||||
|
oreb_raw = p.get("oreb")
|
||||||
if reb in ("", None):
|
if reb in ("", None):
|
||||||
dreb = p.get("dreb") or 0
|
dreb = dreb_raw or 0
|
||||||
oreb = p.get("oreb") or 0
|
oreb = oreb_raw or 0
|
||||||
reb_val = dreb + oreb
|
reb_val = dreb + oreb
|
||||||
else:
|
else:
|
||||||
reb_val = reb
|
reb_val = reb
|
||||||
@@ -4090,6 +4101,10 @@ async def commentary():
|
|||||||
foul_raw = p.get("foul")
|
foul_raw = p.get("foul")
|
||||||
foul_str = "" if foul_raw is None else str(foul_raw)
|
foul_str = "" if foul_raw is None else str(foul_raw)
|
||||||
|
|
||||||
|
# значения для отдельных колонок
|
||||||
|
oreb_val = "" if oreb_raw in (None, "") else oreb_raw
|
||||||
|
dreb_val = "" if dreb_raw in (None, "") else dreb_raw
|
||||||
|
|
||||||
plus_minus = p.get("plusMinus") or ""
|
plus_minus = p.get("plusMinus") or ""
|
||||||
kpi = p.get("kpi") or ""
|
kpi = p.get("kpi") or ""
|
||||||
|
|
||||||
@@ -4119,6 +4134,8 @@ async def commentary():
|
|||||||
</td>
|
</td>
|
||||||
<td>{pts}</td>
|
<td>{pts}</td>
|
||||||
<td>{reb_val}</td>
|
<td>{reb_val}</td>
|
||||||
|
<td class="extra-col">{oreb_val}</td>
|
||||||
|
<td class="extra-col">{dreb_val}</td>
|
||||||
<td>{ast}</td>
|
<td>{ast}</td>
|
||||||
<td>{stl}</td>
|
<td>{stl}</td>
|
||||||
<td>{blk}</td>
|
<td>{blk}</td>
|
||||||
@@ -4139,7 +4156,7 @@ async def commentary():
|
|||||||
# Если вдруг ни одного игрока не прошло фильтр – покажем заглушку
|
# Если вдруг ни одного игрока не прошло фильтр – покажем заглушку
|
||||||
if not rows_html:
|
if not rows_html:
|
||||||
rows_html.append(
|
rows_html.append(
|
||||||
'<tr><td colspan="16" class="meta">Нет данных по игрокам.</td></tr>'
|
'<tr><td colspan="18" class="meta">Нет данных по игрокам.</td></tr>'
|
||||||
)
|
)
|
||||||
|
|
||||||
table_footer = """
|
table_footer = """
|
||||||
@@ -4324,6 +4341,19 @@ async def commentary():
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
.player-name.selected-player-left,
|
||||||
|
.player-name.selected-player-right {{
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(59,130,246,0.12);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.player-name.selected-player-right {{
|
||||||
|
background: rgba(16,185,129,0.12); /* можно оставить одинаковый, если не хочется различать */
|
||||||
|
}}
|
||||||
.extra-col {{
|
.extra-col {{
|
||||||
display: none; /* по умолчанию скрыто */
|
display: none; /* по умолчанию скрыто */
|
||||||
}}
|
}}
|
||||||
@@ -4337,6 +4367,75 @@ async def commentary():
|
|||||||
#player-details table {{
|
#player-details table {{
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}}
|
}}
|
||||||
|
.compare-header {{
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* делаем блок игрока flex-контейнером: слева текст, справа фото */
|
||||||
|
.compare-player-block {{
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* левая часть — текст (имя, амплуа и т.п.) */
|
||||||
|
.compare-player-info {{
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* правая часть — фото */
|
||||||
|
.compare-player-photo {{
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}}
|
||||||
|
.compare-player-photo img {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.compare-name-row {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}}
|
||||||
|
.compare-name {{
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
.compare-flag {{
|
||||||
|
width: 20px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
object-fit: cover;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
|
||||||
|
}}
|
||||||
|
.compare-meta-list {{
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1.4;
|
||||||
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -4528,16 +4627,39 @@ async def commentary():
|
|||||||
["team1_table", "team2_table"].forEach(function(tableId) {{
|
["team1_table", "team2_table"].forEach(function(tableId) {{
|
||||||
var table = document.getElementById(tableId);
|
var table = document.getElementById(tableId);
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
|
|
||||||
var tbody = table.querySelector("tbody");
|
var tbody = table.querySelector("tbody");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
|
|
||||||
|
// Находим индексы нужных колонок по заголовкам
|
||||||
|
var headers = table.querySelectorAll("thead th");
|
||||||
|
var ptsIndex = -1;
|
||||||
|
var foulIndex = -1;
|
||||||
|
|
||||||
|
headers.forEach(function(th, idx) {{
|
||||||
|
var text = (th.innerText || th.textContent || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
if (text === "pts") {{
|
||||||
|
ptsIndex = idx;
|
||||||
|
}}
|
||||||
|
// если вдруг подпишешь по-русски — можно добавить варианты
|
||||||
|
if (text === "foul" || text === "фол" || text === "фолы") {{
|
||||||
|
foulIndex = idx;
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (ptsIndex < 0 || foulIndex < 0) {{
|
||||||
|
// не нашли нужные колонки – тихо выходим
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
Array.from(tbody.rows).forEach(function(row) {{
|
Array.from(tbody.rows).forEach(function(row) {{
|
||||||
var cells = row.cells;
|
var cells = row.cells;
|
||||||
if (!cells || cells.length < 14) return;
|
if (!cells) return;
|
||||||
|
if (cells.length <= ptsIndex || cells.length <= foulIndex) return;
|
||||||
|
|
||||||
// индексы: PTS = 2, Foul = 13
|
var ptsCell = cells[ptsIndex];
|
||||||
var ptsCell = cells[2];
|
var foulCell = cells[foulIndex];
|
||||||
var foulCell = cells[13];
|
|
||||||
if (!ptsCell || !foulCell) return;
|
if (!ptsCell || !foulCell) return;
|
||||||
|
|
||||||
// --- 1) зелёная подсветка при изменении PTS / Foul ---
|
// --- 1) зелёная подсветка при изменении PTS / Foul ---
|
||||||
@@ -4609,7 +4731,7 @@ async def commentary():
|
|||||||
}});
|
}});
|
||||||
}})();
|
}})();
|
||||||
|
|
||||||
// === клик по игроку + компактные таблицы сезон/карьера ===
|
// === клик по игроку + компактные таблицы сезон/карьера + сравнение левого/правого ===
|
||||||
(function() {{
|
(function() {{
|
||||||
var details = document.getElementById("player-details");
|
var details = document.getElementById("player-details");
|
||||||
if (!details) return;
|
if (!details) return;
|
||||||
@@ -4620,6 +4742,8 @@ async def commentary():
|
|||||||
|
|
||||||
function buildMetrics(p) {{
|
function buildMetrics(p) {{
|
||||||
var seasonAvg = [
|
var seasonAvg = [
|
||||||
|
["Игры", p.TGameCount],
|
||||||
|
["В старте", p.TStartCount],
|
||||||
["Очки", p.AvgPoints],
|
["Очки", p.AvgPoints],
|
||||||
["Передачи", p.AvgAssist],
|
["Передачи", p.AvgAssist],
|
||||||
["Подборы", p.AvgRebound],
|
["Подборы", p.AvgRebound],
|
||||||
@@ -4635,6 +4759,8 @@ async def commentary():
|
|||||||
["С игры, %", p.Shot23Percent]
|
["С игры, %", p.Shot23Percent]
|
||||||
];
|
];
|
||||||
var seasonTot = [
|
var seasonTot = [
|
||||||
|
["Игры", p.TGameCount],
|
||||||
|
["В старте", p.TStartCount],
|
||||||
["Очки", p.TPoints],
|
["Очки", p.TPoints],
|
||||||
["Передачи", p.TAssist],
|
["Передачи", p.TAssist],
|
||||||
["Подборы", p.TRebound],
|
["Подборы", p.TRebound],
|
||||||
@@ -4650,6 +4776,8 @@ async def commentary():
|
|||||||
["С игры (goal/shot)", p.TShots23]
|
["С игры (goal/shot)", p.TShots23]
|
||||||
];
|
];
|
||||||
var careerAvg = [
|
var careerAvg = [
|
||||||
|
["Игры", p.Career_TGameCount],
|
||||||
|
["В старте", p.Career_TStartCount],
|
||||||
["Очки", p.Career_AvgPoints],
|
["Очки", p.Career_AvgPoints],
|
||||||
["Передачи", p.Career_AvgAssist],
|
["Передачи", p.Career_AvgAssist],
|
||||||
["Подборы", p.Career_AvgRebound],
|
["Подборы", p.Career_AvgRebound],
|
||||||
@@ -4665,6 +4793,8 @@ async def commentary():
|
|||||||
["С игры, %", p.Career_Shot23Percent]
|
["С игры, %", p.Career_Shot23Percent]
|
||||||
];
|
];
|
||||||
var careerTot = [
|
var careerTot = [
|
||||||
|
["Игры", p.Career_TGameCount],
|
||||||
|
["В старте", p.Career_TStartCount],
|
||||||
["Очки", p.Career_TPoints],
|
["Очки", p.Career_TPoints],
|
||||||
["Передачи", p.Career_TAssist],
|
["Передачи", p.Career_TAssist],
|
||||||
["Подборы", p.Career_TRebound],
|
["Подборы", p.Career_TRebound],
|
||||||
@@ -4694,12 +4824,32 @@ async def commentary():
|
|||||||
return metrics;
|
return metrics;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// выбранные игроки слева/справа
|
||||||
|
var selectedLeft = null;
|
||||||
|
var selectedRight = null;
|
||||||
|
|
||||||
|
// одиночное отображение (как было раньше) — НЕ трогаем таблицу, тут всё ок
|
||||||
function renderPlayerDetails(p) {{
|
function renderPlayerDetails(p) {{
|
||||||
if (!p) return "";
|
if (!p) return "";
|
||||||
var metrics = buildMetrics(p);
|
var metrics = buildMetrics(p);
|
||||||
var html = "";
|
var html = "";
|
||||||
html += "<h2>" + safe(p.NameGFX || p.name) + "</h2>";
|
|
||||||
|
var metaParts = [];
|
||||||
|
if (p.age) metaParts.push(p.age + " лет");
|
||||||
|
if (p.height) metaParts.push(p.height.height || p.height);
|
||||||
|
if (p.weight) metaParts.push(p.weight);
|
||||||
|
|
||||||
|
html += "<h2 style='display:flex;align-items:center;gap:6px;'>"
|
||||||
|
+ safe(p.NameGFX || p.name)
|
||||||
|
+ (p.flag ? ("<img src='" + p.flag + "' style='width:22px;height:16px;border-radius:3px;'>") : "")
|
||||||
|
+ "</h2>";
|
||||||
|
|
||||||
|
if (metaParts.length) {{
|
||||||
|
html += "<div class='player-meta'>" + safe(metaParts.join(" · ")) + "</div>";
|
||||||
|
}}
|
||||||
|
|
||||||
html += "<h3>Сезон / карьера — компактно</h3>";
|
html += "<h3>Сезон / карьера — компактно</h3>";
|
||||||
|
|
||||||
html += "<table><thead><tr>";
|
html += "<table><thead><tr>";
|
||||||
html += "<th></th>";
|
html += "<th></th>";
|
||||||
for (var j = 0; j < metrics.length; j++) {{
|
for (var j = 0; j < metrics.length; j++) {{
|
||||||
@@ -4735,6 +4885,156 @@ async def commentary():
|
|||||||
return html;
|
return html;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// === Сравнение игроков: шапка + СТАРАЯ вертикальная таблица с отдельными колонками ===
|
||||||
|
function renderPlayersComparison(leftP, rightP) {{
|
||||||
|
// если вообще никого нет — ничего не показываем
|
||||||
|
if (!leftP && !rightP) {{
|
||||||
|
return "";
|
||||||
|
}}
|
||||||
|
|
||||||
|
// --- метрики ---
|
||||||
|
var LM = leftP ? buildMetrics(leftP) : null;
|
||||||
|
var RM = rightP ? buildMetrics(rightP) : null;
|
||||||
|
|
||||||
|
// базовый набор строк (под один из игроков)
|
||||||
|
var base = LM || RM || [];
|
||||||
|
|
||||||
|
// если одного игрока нет — делаем для него пустой набор метрик
|
||||||
|
function makeEmptyMetrics(from) {{
|
||||||
|
return from.map(function (m) {{
|
||||||
|
return {{
|
||||||
|
label: m.label,
|
||||||
|
sAvg: "",
|
||||||
|
sTot: "",
|
||||||
|
cAvg: "",
|
||||||
|
cTot: ""
|
||||||
|
}};
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
|
||||||
|
if (!LM) LM = makeEmptyMetrics(base);
|
||||||
|
if (!RM) RM = makeEmptyMetrics(base);
|
||||||
|
|
||||||
|
function metaList(p) {{
|
||||||
|
if (!p) return [];
|
||||||
|
var arr = [];
|
||||||
|
if (p.roleFull || p.role) arr.push("Амплуа: " + (p.roleFull || p.role));
|
||||||
|
if (p.age) arr.push("Возраст: " + p.age);
|
||||||
|
if (p.height) arr.push("Рост: " + p.height);
|
||||||
|
if (p.weight) arr.push("Вес: " + p.weight);
|
||||||
|
if (p.teamName) arr.push("Команда: " + p.teamName);
|
||||||
|
return arr;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function renderPlayerHeader(p) {{
|
||||||
|
var html = "";
|
||||||
|
html += "<div class='compare-player-block'>";
|
||||||
|
|
||||||
|
// левая часть: текст
|
||||||
|
html += " <div class='compare-player-info'>";
|
||||||
|
html += " <div class='compare-name-row'>";
|
||||||
|
html += " <span class='compare-name'>" + safe((p && (p.NameGFX || p.name)) || "") + "</span>";
|
||||||
|
if (p && p.flag) {{
|
||||||
|
var title = safe(p.country || "").replace(/\"/g, """);
|
||||||
|
html += " <img src='" + p.flag + "' class='compare-flag' title='" + title + "'>";
|
||||||
|
}}
|
||||||
|
html += " </div>";
|
||||||
|
html += " <ul class='compare-meta-list'>";
|
||||||
|
metaList(p || {{}}).forEach(function (txt) {{
|
||||||
|
html += " <li>" + safe(txt) + "</li>";
|
||||||
|
}});
|
||||||
|
html += " </ul>";
|
||||||
|
html += " </div>";
|
||||||
|
|
||||||
|
// правая часть: фото из p.photo_site (если есть)
|
||||||
|
if (p && p.photo_site) {{
|
||||||
|
html += " <div class='compare-player-photo'>";
|
||||||
|
html += " <img src='" + safe(p.photo_site) + "' alt='" + safe((p && (p.NameGFX || p.name)) || \"\") + "'>";
|
||||||
|
html += " </div>";
|
||||||
|
}}
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
return html;
|
||||||
|
}}
|
||||||
|
|
||||||
|
var html = "";
|
||||||
|
|
||||||
|
html += "<h2>Сравнение игроков</h2>";
|
||||||
|
html += "<div class='compare-header'>";
|
||||||
|
html += renderPlayerHeader(leftP || {{}});
|
||||||
|
html += renderPlayerHeader(rightP || {{}});
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
// --- СТАРАЯ таблица: 4 колонки слева + 4 справа ---
|
||||||
|
html += "<table class='details-wide-table' style='width:100%;border-collapse:collapse;'>";
|
||||||
|
|
||||||
|
// шапка таблицы
|
||||||
|
html += "<thead>";
|
||||||
|
html += "<tr>";
|
||||||
|
html += "<th colspan='4' style='text-align:center;border-bottom:2px solid #333;'>Левый игрок</th>";
|
||||||
|
html += "<th></th>";
|
||||||
|
html += "<th colspan='4' style='text-align:center;border-bottom:2px solid #333;'>Правый игрок</th>";
|
||||||
|
html += "</tr>";
|
||||||
|
|
||||||
|
html += "<tr>";
|
||||||
|
html += "<th>Сезон (сред.)</th>";
|
||||||
|
html += "<th>Сезон (тотал)</th>";
|
||||||
|
html += "<th>Карьера (сред.)</th>";
|
||||||
|
html += "<th>Карьера (тотал)</th>";
|
||||||
|
|
||||||
|
html += "<th>Показатель</th>";
|
||||||
|
|
||||||
|
html += "<th>Сезон (сред.)</th>";
|
||||||
|
html += "<th>Сезон (тотал)</th>";
|
||||||
|
html += "<th>Карьера (сред.)</th>";
|
||||||
|
html += "<th>Карьера (тотал)</th>";
|
||||||
|
html += "</tr>";
|
||||||
|
html += "</thead>";
|
||||||
|
|
||||||
|
html += "<tbody>";
|
||||||
|
|
||||||
|
// строки таблицы — одна строка = один показатель
|
||||||
|
for (var i = 0; i < base.length; i++) {{
|
||||||
|
var L = LM[i] || {{}};
|
||||||
|
var R = RM[i] || {{}};
|
||||||
|
|
||||||
|
html += "<tr>";
|
||||||
|
|
||||||
|
// левый
|
||||||
|
html += "<td style='text-align:center;'>" + safe(L.sAvg) + "</td>";
|
||||||
|
html += "<td style='text-align:center;'>" + safe(L.sTot) + "</td>";
|
||||||
|
html += "<td style='text-align:center;'>" + safe(L.cAvg) + "</td>";
|
||||||
|
html += "<td style='text-align:center;'>" + safe(L.cTot) + "</td>";
|
||||||
|
|
||||||
|
// показатель
|
||||||
|
html += "<td style='text-align:center;font-weight:600;'>" + safe(base[i].label || "") + "</td>";
|
||||||
|
|
||||||
|
// правый
|
||||||
|
html += "<td style='text-align:center;'>" + safe(R.sAvg) + "</td>";
|
||||||
|
html += "<td style='text-align:center;'>" + safe(R.sTot) + "</td>";
|
||||||
|
html += "<td style='text-align:center;'>" + safe(R.cAvg) + "</td>";
|
||||||
|
html += "<td style='text-align:center;'>" + safe(R.cTot) + "</td>";
|
||||||
|
|
||||||
|
html += "</tr>";
|
||||||
|
}}
|
||||||
|
|
||||||
|
html += "</tbody></table>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// переотрисовка блока с деталями/сравнением
|
||||||
|
function rerenderDetails() {{
|
||||||
|
if (selectedLeft || selectedRight) {{
|
||||||
|
// Всегда рисуем вертикальную сравнительную таблицу.
|
||||||
|
// Если один из игроков не выбран — его половина будет пустой.
|
||||||
|
details.innerHTML = renderPlayersComparison(selectedLeft, selectedRight);
|
||||||
|
}} else {{
|
||||||
|
details.innerHTML = '<div class="meta">Кликни по фамилии игрока, чтобы показать сезон/карьеру.</div>';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// выбор/снятие выбора игрока
|
||||||
function selectPlayer(teamKey, pid, noScroll, noSave) {{
|
function selectPlayer(teamKey, pid, noScroll, noSave) {{
|
||||||
var data = window.PLAYER_DATA || {{}};
|
var data = window.PLAYER_DATA || {{}};
|
||||||
var list = data[teamKey] || [];
|
var list = data[teamKey] || [];
|
||||||
@@ -4745,16 +5045,77 @@ async def commentary():
|
|||||||
break;
|
break;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// если игрок не найден в данных — просто выходим
|
||||||
if (!found) return;
|
if (!found) return;
|
||||||
details.innerHTML = renderPlayerDetails(found);
|
|
||||||
|
var pidStr = String(pid || "");
|
||||||
|
|
||||||
|
if (teamKey === "team1") {{
|
||||||
|
// повторный клик по тому же игроку слева — снимаем выбор
|
||||||
|
if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
|
||||||
|
selectedLeft = null;
|
||||||
|
}} else {{
|
||||||
|
selectedLeft = found;
|
||||||
|
}}
|
||||||
|
}} else if (teamKey === "team2") {{
|
||||||
|
// повторный клик по тому же игроку справа — снимаем выбор
|
||||||
|
if (selectedRight && String(selectedRight.id || "") === pidStr) {{
|
||||||
|
selectedRight = null;
|
||||||
|
}} else {{
|
||||||
|
selectedRight = found;
|
||||||
|
}}
|
||||||
|
}} else {{
|
||||||
|
// запасной вариант — считаем как левую сторону
|
||||||
|
if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
|
||||||
|
selectedLeft = null;
|
||||||
|
}} else {{
|
||||||
|
selectedLeft = found;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// подсветка выбранных фамилий
|
||||||
|
var allCells = document.querySelectorAll(".player-name");
|
||||||
|
allCells.forEach(function (cell) {{
|
||||||
|
cell.classList.remove("selected-player-left", "selected-player-right");
|
||||||
|
|
||||||
|
var cPid = cell.getAttribute("data-player-id");
|
||||||
|
var cTeam = cell.getAttribute("data-team");
|
||||||
|
|
||||||
|
if (selectedLeft && cTeam === "team1" &&
|
||||||
|
String(selectedLeft.id || "") === String(cPid || "")) {{
|
||||||
|
cell.classList.add("selected-player-left");
|
||||||
|
}}
|
||||||
|
|
||||||
|
if (selectedRight && cTeam === "team2" &&
|
||||||
|
String(selectedRight.id || "") === String(cPid || "")) {{
|
||||||
|
cell.classList.add("selected-player-right");
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
rerenderDetails();
|
||||||
|
|
||||||
if (!noSave) {{
|
if (!noSave) {{
|
||||||
localStorage.setItem("commentary_last_player", JSON.stringify({{ team: teamKey, id: pid }}));
|
try {{
|
||||||
|
if (selectedLeft || selectedRight) {{
|
||||||
|
// запоминаем последнего кликнутого
|
||||||
|
localStorage.setItem(
|
||||||
|
"commentary_last_player",
|
||||||
|
JSON.stringify({{ team: teamKey, id: pid }})
|
||||||
|
);
|
||||||
|
}} else {{
|
||||||
|
// если вообще никого не осталось выбранным — чистим
|
||||||
|
localStorage.removeItem("commentary_last_player");
|
||||||
|
}}
|
||||||
|
}} catch (e) {{}}
|
||||||
}}
|
}}
|
||||||
if (!noScroll) {{
|
if (!noScroll) {{
|
||||||
details.scrollIntoView({{ behavior: "smooth", block: "start" }});
|
details.scrollIntoView({{ behavior: "smooth", block: "start" }});
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
window.selectPlayer = selectPlayer;
|
||||||
|
|
||||||
// клики по фамилиям
|
// клики по фамилиям
|
||||||
var cells = document.querySelectorAll(".player-name");
|
var cells = document.querySelectorAll(".player-name");
|
||||||
for (var i = 0; i < cells.length; i++) {{
|
for (var i = 0; i < cells.length; i++) {{
|
||||||
@@ -4765,7 +5126,7 @@ async def commentary():
|
|||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// восстановить последнего выбранного
|
// восстановить последнего выбранного (если был)
|
||||||
var saved = localStorage.getItem("commentary_last_player");
|
var saved = localStorage.getItem("commentary_last_player");
|
||||||
if (saved) {{
|
if (saved) {{
|
||||||
try {{
|
try {{
|
||||||
@@ -4788,9 +5149,10 @@ async def commentary():
|
|||||||
return (v === undefined || v === null) ? "" : v;
|
return (v === undefined || v === null) ? "" : v;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// все ячейки с фамилиями для нужной команды
|
||||||
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
|
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
|
||||||
|
|
||||||
cells.forEach(function(cell) {{
|
cells.forEach(function (cell) {{
|
||||||
var pid = cell.getAttribute("data-player-id");
|
var pid = cell.getAttribute("data-player-id");
|
||||||
var newP = map[String(pid)];
|
var newP = map[String(pid)];
|
||||||
if (!newP) return;
|
if (!newP) return;
|
||||||
@@ -4798,42 +5160,64 @@ async def commentary():
|
|||||||
var row = cell.parentElement;
|
var row = cell.parentElement;
|
||||||
var tds = row.children;
|
var tds = row.children;
|
||||||
|
|
||||||
// обновляем data-on-court
|
// обновляем data-on-court (для раскраски строк)
|
||||||
var onCourt = !!newP.isOnCourt;
|
var onCourt = !!newP.isOnCourt;
|
||||||
row.dataset.onCourt = onCourt ? "true" : "false";
|
row.dataset.onCourt = onCourt ? "true" : "false";
|
||||||
|
|
||||||
|
// Порядок колонок в таблице:
|
||||||
|
// 0 #
|
||||||
|
// 1 Игрок
|
||||||
|
// 2 PTS
|
||||||
|
// 3 REB
|
||||||
|
// 4 AST
|
||||||
|
// 5 STL
|
||||||
|
// 6 BLK
|
||||||
|
// 7 MIN
|
||||||
|
// 8 OREB (extra-col)
|
||||||
|
// 9 DREB (extra-col)
|
||||||
|
// 10 FG (extra-col)
|
||||||
|
// 11 2PT (extra-col)
|
||||||
|
// 12 3PT (extra-col)
|
||||||
|
// 13 1PT (extra-col)
|
||||||
|
// 14 TO (extra-col)
|
||||||
|
// 15 Foul
|
||||||
|
// 16 +/-
|
||||||
|
// 17 KPI
|
||||||
var fields = [
|
var fields = [
|
||||||
[2, "pts"],
|
[2, "pts"],
|
||||||
[3, "reb"],
|
[3, "reb"], // пересчёт ниже
|
||||||
[4, "ast"],
|
[4, "oreb"],
|
||||||
[5, "stl"],
|
[5, "dreb"],
|
||||||
[6, "blk"],
|
[6, "ast"],
|
||||||
[7, "time"],
|
[7, "stl"],
|
||||||
[8, "fg"],
|
[8, "blk"],
|
||||||
[9, "pt-2"],
|
[9, "time"],
|
||||||
[10, "pt-3"],
|
[10, "fg"],
|
||||||
[11, "pt-1"],
|
[11, "pt-2"],
|
||||||
[12, "to"],
|
[12, "pt-3"],
|
||||||
[13, "foul"],
|
[13, "pt-1"],
|
||||||
[14, "plusMinus"],
|
[14, "to"],
|
||||||
[15, "kpi"]
|
[15, "foul"],
|
||||||
|
[16, "plusMinus"],
|
||||||
|
[17, "kpi"]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// REB: если нет newP.reb, считаем как dreb + oreb
|
||||||
var newReb = newP.reb;
|
var newReb = newP.reb;
|
||||||
if (newReb === undefined) {{
|
if (newReb === undefined || newReb === null || newReb === "") {{
|
||||||
newReb = (newP.dreb || 0) + (newP.oreb || 0);
|
var dreb = newP.dreb || 0;
|
||||||
|
var oreb = newP.oreb || 0;
|
||||||
|
newReb = dreb + oreb;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
function applyUpdate(td, newVal) {{
|
function applyUpdate(td, newVal) {{
|
||||||
newVal = safe(newVal);
|
td.textContent = safe(newVal);
|
||||||
td.textContent = newVal;
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
fields.forEach(function(item) {{
|
fields.forEach(function (item) {{
|
||||||
var index = item[0];
|
var index = item[0];
|
||||||
var key = item[1];
|
var key = item[1];
|
||||||
var td = tds[index];
|
var td = tds[index];
|
||||||
|
|
||||||
if (!td) return;
|
if (!td) return;
|
||||||
|
|
||||||
if (key === "reb") {{
|
if (key === "reb") {{
|
||||||
@@ -4845,16 +5229,45 @@ async def commentary():
|
|||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
function refreshSelected() {{
|
|
||||||
var saved = localStorage.getItem("commentary_last_player");
|
|
||||||
if (!saved) return;
|
|
||||||
try {{
|
|
||||||
var obj = JSON.parse(saved);
|
function refreshSelected() {{
|
||||||
if (obj && obj.team && obj.id) {{
|
// если никого не выбрали – нечего обновлять
|
||||||
selectPlayer(obj.team, obj.id, true, true);
|
if (!selectedLeft && !selectedRight) {{
|
||||||
|
return;
|
||||||
}}
|
}}
|
||||||
}} catch (e) {{}}
|
|
||||||
|
var data = window.PLAYER_DATA || {{}};
|
||||||
|
var team1 = Array.isArray(data.team1) ? data.team1 : [];
|
||||||
|
var team2 = Array.isArray(data.team2) ? data.team2 : [];
|
||||||
|
|
||||||
|
// обновляем ссылку на объект для левого игрока (team1)
|
||||||
|
if (selectedLeft) {{
|
||||||
|
var leftId = String(selectedLeft.id || "");
|
||||||
|
var updatedLeft = team1.find(function (p) {{
|
||||||
|
return String(p.id || "") === leftId;
|
||||||
|
}});
|
||||||
|
if (updatedLeft) {{
|
||||||
|
selectedLeft = updatedLeft;
|
||||||
}}
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// обновляем ссылку на объект для правого игрока (team2)
|
||||||
|
if (selectedRight) {{
|
||||||
|
var rightId = String(selectedRight.id || "");
|
||||||
|
var updatedRight = team2.find(function (p) {{
|
||||||
|
return String(p.id || "") === rightId;
|
||||||
|
}});
|
||||||
|
if (updatedRight) {{
|
||||||
|
selectedRight = updatedRight;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// просто перерисовываем блок сравнения, без скролла и без записи в localStorage
|
||||||
|
rerenderDetails();
|
||||||
|
}}
|
||||||
|
|
||||||
setInterval(function() {{
|
setInterval(function() {{
|
||||||
fetch('/team1').then(function(r) {{
|
fetch('/team1').then(function(r) {{
|
||||||
@@ -4884,8 +5297,9 @@ async def commentary():
|
|||||||
// 🔁 обновляем шапку и четверти
|
// 🔁 обновляем шапку и четверти
|
||||||
updateHeaderFromLiveStatus(); // счёт + фолы из live_status
|
updateHeaderFromLiveStatus(); // счёт + фолы из live_status
|
||||||
updateQuartersFromScores(); // счёт по четвертям из /scores
|
updateQuartersFromScores(); // счёт по четвертям из /scores
|
||||||
|
updateTimeoutsFromTeamStats(); // тайм-ауты обновление
|
||||||
|
|
||||||
}}, 1000);
|
}}, 1500);
|
||||||
updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
|
updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
|
||||||
|
|
||||||
}})();
|
}})();
|
||||||
@@ -5002,7 +5416,7 @@ async def commentary():
|
|||||||
}}
|
}}
|
||||||
// запустим автообновление счёта и четвертей
|
// запустим автообновление счёта и четвертей
|
||||||
updateScoreAndQuarters();
|
updateScoreAndQuarters();
|
||||||
setInterval(updateScoreAndQuarters, 1000);
|
setInterval(updateScoreAndQuarters, 1500);
|
||||||
|
|
||||||
makeTableSortable("team1_table");
|
makeTableSortable("team1_table");
|
||||||
makeTableSortable("team2_table");
|
makeTableSortable("team2_table");
|
||||||
@@ -5010,7 +5424,7 @@ async def commentary():
|
|||||||
// первый старт стилей сразу
|
// первый старт стилей сразу
|
||||||
updateRowStyles();
|
updateRowStyles();
|
||||||
// и периодически обновляем (на случай любых внешних изменений)
|
// и периодически обновляем (на случай любых внешних изменений)
|
||||||
setInterval(updateRowStyles, 1000);
|
setInterval(updateRowStyles, 1500);
|
||||||
}});
|
}});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -5563,6 +5977,18 @@ async def milestones_ui():
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
.player-flag {
|
||||||
|
width: 22px;
|
||||||
|
height: 16px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-meta,
|
||||||
|
.player-meta-inline {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
Reference in New Issue
Block a user