BUILD130 — 4 цвета + HEX/RGB ввод

This commit is contained in:
2026-09-10 11:20:08 +03:00
parent a9278b5251
commit 837d40d6ce
7 changed files with 70 additions and 12 deletions

2
app.py
View File

@@ -29,7 +29,7 @@ from ui_builder import install_ui_builder
from khl_site.khl_data_center import APP as khl_site_app from khl_site.khl_data_center import APP as khl_site_app
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
BUILD_VERSION = "2026.09.10.1" BUILD_VERSION = "2026.09.10.2"
# compatibility: BUILD_VERSION = "2026.08.26.1" # compatibility: BUILD_VERSION = "2026.08.26.1"
# compatibility: BUILD_VERSION = "2026.08.25.1" # compatibility: BUILD_VERSION = "2026.08.25.1"
# compatibility: BUILD_VERSION = "2026.08.24.15" # compatibility: BUILD_VERSION = "2026.08.24.15"

View File

@@ -886,7 +886,7 @@ class MappingDataService:
) )
result: list[dict[str, Any]] = [] result: list[dict[str, Any]] = []
# BUILD129: operator-managed team colours. Enabled is deliberately a # BUILD130: operator-managed team colours. Enabled is deliberately a
# separate value from the HEX string so a colour may remain configured # separate value from the HEX string so a colour may remain configured
# while being disabled for the current graphics logic. # while being disabled for the current graphics logic.
for side, team, side_label in (("home", home_team, "Хозяева"), ("away", away_team, "Гости")): for side, team, side_label in (("home", home_team, "Хозяева"), ("away", away_team, "Гости")):
@@ -894,6 +894,7 @@ class MappingDataService:
(1, "color_hex", "color_enabled"), (1, "color_hex", "color_enabled"),
(2, "color_2_hex", "color_2_enabled"), (2, "color_2_hex", "color_2_enabled"),
(3, "color_3_hex", "color_3_enabled"), (3, "color_3_hex", "color_3_enabled"),
(4, "color_4_hex", "color_4_enabled"),
) )
for index, color_field, enabled_field in color_specs: for index, color_field, enabled_field in color_specs:
raw_color = str(getattr(team, color_field, "") or "") if team is not None else "" raw_color = str(getattr(team, color_field, "") or "") if team is not None else ""

View File

@@ -254,7 +254,7 @@ class Team(Base):
short_name_en: Mapped[str] = mapped_column(String(255), nullable=False, default="") short_name_en: Mapped[str] = mapped_column(String(255), nullable=False, default="")
city_ru: Mapped[str] = mapped_column(String(255), nullable=False, default="") city_ru: Mapped[str] = mapped_column(String(255), nullable=False, default="")
city_en: Mapped[str] = mapped_column(String(255), nullable=False, default="") city_en: Mapped[str] = mapped_column(String(255), nullable=False, default="")
# BUILD129: three operator-editable team colours. The legacy ``color_hex`` # BUILD130: four operator-editable team colours. The legacy ``color_hex``
# column remains colour #1 for backwards compatibility with existing SQL. # column remains colour #1 for backwards compatibility with existing SQL.
color_hex: Mapped[str] = mapped_column(String(9), nullable=False, default="") color_hex: Mapped[str] = mapped_column(String(9), nullable=False, default="")
color_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) color_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
@@ -262,6 +262,8 @@ class Team(Base):
color_2_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) color_2_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
color_3_hex: Mapped[str] = mapped_column(String(9), nullable=False, default="") color_3_hex: Mapped[str] = mapped_column(String(9), nullable=False, default="")
color_3_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) color_3_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
color_4_hex: Mapped[str] = mapped_column(String(9), nullable=False, default="")
color_4_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
logo_url: Mapped[str] = mapped_column(Text, nullable=False, default="") logo_url: Mapped[str] = mapped_column(Text, nullable=False, default="")
raw_payload: Mapped[str] = mapped_column(Text, nullable=False, default="") raw_payload: Mapped[str] = mapped_column(Text, nullable=False, default="")
synced_at: Mapped[datetime] = mapped_column( synced_at: Mapped[datetime] = mapped_column(

View File

@@ -171,6 +171,8 @@ class TeamDirectoryPayload(BaseModel):
color_2_enabled: bool = False color_2_enabled: bool = False
color_3_hex: str = Field(default="", max_length=9) color_3_hex: str = Field(default="", max_length=9)
color_3_enabled: bool = False color_3_enabled: bool = False
color_4_hex: str = Field(default="", max_length=9)
color_4_enabled: bool = False
logo_url: str = Field(default="", max_length=2000) logo_url: str = Field(default="", max_length=2000)
active: bool = True active: bool = True

View File

@@ -614,10 +614,13 @@ class HockeyDataService:
"color_2_enabled": bool(team.color_2_enabled), "color_2_enabled": bool(team.color_2_enabled),
"color_3_hex": team.color_3_hex, "color_3_hex": team.color_3_hex,
"color_3_enabled": bool(team.color_3_enabled), "color_3_enabled": bool(team.color_3_enabled),
"color_4_hex": team.color_4_hex,
"color_4_enabled": bool(team.color_4_enabled),
"colors": [ "colors": [
{"index": 1, "hex": team.color_hex, "enabled": bool(team.color_enabled)}, {"index": 1, "hex": team.color_hex, "enabled": bool(team.color_enabled)},
{"index": 2, "hex": team.color_2_hex, "enabled": bool(team.color_2_enabled)}, {"index": 2, "hex": team.color_2_hex, "enabled": bool(team.color_2_enabled)},
{"index": 3, "hex": team.color_3_hex, "enabled": bool(team.color_3_enabled)}, {"index": 3, "hex": team.color_3_hex, "enabled": bool(team.color_3_enabled)},
{"index": 4, "hex": team.color_4_hex, "enabled": bool(team.color_4_enabled)},
], ],
"logo_url": team.logo_url, "logo_url": team.logo_url,
"source": membership.source, "source": membership.source,
@@ -770,6 +773,7 @@ class HockeyDataService:
("color_hex", "color_enabled"), ("color_hex", "color_enabled"),
("color_2_hex", "color_2_enabled"), ("color_2_hex", "color_2_enabled"),
("color_3_hex", "color_3_enabled"), ("color_3_hex", "color_3_enabled"),
("color_4_hex", "color_4_enabled"),
) )
for color_field, enabled_field in color_fields: for color_field, enabled_field in color_fields:
if color_field in payload and payload[color_field] is not None: if color_field in payload and payload[color_field] is not None:

View File

@@ -1613,7 +1613,7 @@
.hockey-team-color-control { .hockey-team-color-control {
display: grid; display: grid;
grid-template-columns: 42px minmax(0, 1fr) 34px; grid-template-columns: 42px minmax(0, 1fr) minmax(0, 1fr) 34px;
gap: 7px; gap: 7px;
align-items: center; align-items: center;
} }
@@ -1657,12 +1657,17 @@
cursor: pointer; cursor: pointer;
} }
.hockey-team-color-control input[data-team-color-text] { .hockey-team-color-value{display:grid!important;gap:3px!important;margin:0!important;min-width:0}
.hockey-team-color-value>small{font-size:7px!important;font-weight:900;letter-spacing:.5px;color:#71869a!important}
.hockey-team-color-value>input{width:100%;min-width:0}
.hockey-team-color-control input[data-team-color-text],
.hockey-team-color-control input[data-team-color-rgb] {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 800; font-weight: 800;
letter-spacing: .5px; letter-spacing: .5px;
text-transform: uppercase; text-transform: uppercase;
} }
.hockey-team-color-control input[data-team-color-rgb]{text-transform:none;letter-spacing:0}
.hockey-team-color-clear { .hockey-team-color-clear {
color: #879bb1; color: #879bb1;
@@ -1746,11 +1751,11 @@
.hockey-map-sql-table-ref code{grid-column:1/-1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:8px Consolas,monospace;color:#738da6} .hockey-map-sql-table-ref code{grid-column:1/-1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:8px Consolas,monospace;color:#738da6}
@media(max-width:1050px){.hockey-map-sql-table-list{grid-template-columns:1fr}} @media(max-width:1050px){.hockey-map-sql-table-list{grid-template-columns:1fr}}
/* BUILD129three optional team colours with explicit enable flags */ /* BUILD130four optional team colours with explicit HEX/RGB entry */
.hockey-team-color-field-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:3px} .hockey-team-color-field-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:3px}
.hockey-team-color-field-head>span{color:#c8d7e6;font-size:10px;font-weight:900} .hockey-team-color-field-head>span{color:#c8d7e6;font-size:10px;font-weight:900}
.hockey-team-color-field-head>small{max-width:520px;text-align:right;line-height:1.35} .hockey-team-color-field-head>small{max-width:520px;text-align:right;line-height:1.35}
.hockey-team-colors-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px} .hockey-team-colors-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.hockey-team-color-item{min-width:0;padding:8px;border:1px solid #2b4057;border-radius:10px;background:#0b1725;opacity:.72;transition:border-color .15s ease,opacity .15s ease,background .15s ease} .hockey-team-color-item{min-width:0;padding:8px;border:1px solid #2b4057;border-radius:10px;background:#0b1725;opacity:.72;transition:border-color .15s ease,opacity .15s ease,background .15s ease}
.hockey-team-color-item.is-enabled{border-color:#347662;background:#0d201f;opacity:1} .hockey-team-color-item.is-enabled{border-color:#347662;background:#0d201f;opacity:1}
.hockey-team-color-enabled{display:grid!important;grid-template-columns:auto minmax(0,1fr) auto!important;align-items:center!important;gap:6px!important;margin:0 0 6px!important} .hockey-team-color-enabled{display:grid!important;grid-template-columns:auto minmax(0,1fr) auto!important;align-items:center!important;gap:6px!important;margin:0 0 6px!important}
@@ -1767,3 +1772,4 @@
.hockey-team-color-chip.is-enabled>b{background:#183a32;color:#71e2c7} .hockey-team-color-chip.is-enabled>b{background:#183a32;color:#71e2c7}
.hockey-team-color-chip.is-enabled>code{color:#bed2e2} .hockey-team-color-chip.is-enabled>code{color:#bed2e2}
@media(max-width:1180px){.hockey-team-colors-grid{grid-template-columns:1fr}.hockey-team-color-field-head{align-items:flex-start;flex-direction:column}.hockey-team-color-field-head>small{text-align:left}} @media(max-width:1180px){.hockey-team-colors-grid{grid-template-columns:1fr}.hockey-team-color-field-head{align-items:flex-start;flex-direction:column}.hockey-team-color-field-head>small{text-align:left}}
@media(max-width:720px){.hockey-team-color-control{grid-template-columns:42px minmax(0,1fr) 34px}.hockey-team-color-value:nth-of-type(2){grid-column:2/3}}

View File

@@ -80,6 +80,23 @@
return color || "#FFFFFF"; return color || "#FFFFFF";
}; };
const teamColorHexToRgb = (value) => {
const color = normalizeTeamColorHex(value);
if (!color) return "";
return [1, 3, 5].map((offset) => parseInt(color.slice(offset, offset + 2), 16)).join(", ");
};
const teamColorRgbToHex = (value) => {
let raw = String(value ?? "").trim();
if (!raw) return "";
raw = raw.replace(/^rgb\s*\(/i, "").replace(/\)$/, "").trim();
const parts = raw.includes(",") ? raw.split(",") : raw.split(/\s+/);
if (parts.length !== 3) return "";
const rgb = parts.map((part) => Number(String(part).trim()));
if (rgb.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return "";
return `#${rgb.map((part) => part.toString(16).padStart(2, "0")).join("").toUpperCase()}`;
};
const teamColorFieldName = (index) => index === 1 ? "color_hex" : `color_${index}_hex`; const teamColorFieldName = (index) => index === 1 ? "color_hex" : `color_${index}_hex`;
const teamColorEnabledName = (index) => index === 1 ? "color_enabled" : `color_${index}_enabled`; const teamColorEnabledName = (index) => index === 1 ? "color_enabled" : `color_${index}_enabled`;
@@ -102,14 +119,15 @@
<i data-team-color-preview="${index}" style="--team-color:${escapeHtml(teamColorPreview(value))}"></i> <i data-team-color-preview="${index}" style="--team-color:${escapeHtml(teamColorPreview(value))}"></i>
<input type="color" data-team-color-picker="${index}" value="${escapeHtml(teamColorPreview(value))}" tabindex="-1" aria-label="Выбрать цвет ${index} команды"> <input type="color" data-team-color-picker="${index}" value="${escapeHtml(teamColorPreview(value))}" tabindex="-1" aria-label="Выбрать цвет ${index} команды">
</button> </button>
<input ${fieldNameAttr} data-team-color-text="${index}" maxlength="7" placeholder="#282E66" value="${escapeHtml(value)}" autocomplete="off" spellcheck="false"> <label class="hockey-team-color-value"><small>HEX</small><input ${fieldNameAttr} data-team-color-text="${index}" maxlength="7" placeholder="#282E66" value="${escapeHtml(value)}" autocomplete="off" spellcheck="false"></label>
<label class="hockey-team-color-value"><small>RGB</small><input data-team-color-rgb="${index}" maxlength="18" placeholder="40, 46, 102" value="${escapeHtml(teamColorHexToRgb(value))}" autocomplete="off" spellcheck="false"></label>
<button type="button" class="hockey-team-color-clear" data-team-color-clear="${index}" title="Очистить цвет ${index}">×</button> <button type="button" class="hockey-team-color-clear" data-team-color-clear="${index}" title="Очистить цвет ${index}">×</button>
</div> </div>
</div> </div>
`; `;
}; };
const teamColorsCellMarkup = (item) => [1, 2, 3].map((index) => { const teamColorsCellMarkup = (item) => [1, 2, 3, 4].map((index) => {
const field = teamColorFieldName(index); const field = teamColorFieldName(index);
const enabledField = teamColorEnabledName(index); const enabledField = teamColorEnabledName(index);
const value = normalizeTeamColorHex(item?.[field]); const value = normalizeTeamColorHex(item?.[field]);
@@ -420,12 +438,13 @@
<div class="hockey-team-color-field"> <div class="hockey-team-color-field">
<div class="hockey-team-color-field-head"> <div class="hockey-team-color-field-head">
<span>Цвета команды</span> <span>Цвета команды</span>
<small>HEX можно сохранить заранее, а галочкой отдельно включать/выключать цвет в данных Mapping.</small> <small>Цвет можно выбрать мышкой или ввести вручную в HEX / RGB; галочка отдельно включает или выключает его в Mapping.</small>
</div> </div>
<div class="hockey-team-colors-grid"> <div class="hockey-team-colors-grid">
${teamColorEditorMarkup(editing, 1)} ${teamColorEditorMarkup(editing, 1)}
${teamColorEditorMarkup(editing, 2)} ${teamColorEditorMarkup(editing, 2)}
${teamColorEditorMarkup(editing, 3)} ${teamColorEditorMarkup(editing, 3)}
${teamColorEditorMarkup(editing, 4)}
</div> </div>
</div> </div>
<label class="hockey-directory-check"><input type="checkbox" name="active" ${editing?.active === false ? "" : "checked"}><span>Активна</span></label> <label class="hockey-directory-check"><input type="checkbox" name="active" ${editing?.active === false ? "" : "checked"}><span>Активна</span></label>
@@ -3126,6 +3145,7 @@
const item = modal.querySelector(`[data-team-color-item="${index}"]`); const item = modal.querySelector(`[data-team-color-item="${index}"]`);
const colorText = modal.querySelector(`[data-team-color-text="${index}"]`); const colorText = modal.querySelector(`[data-team-color-text="${index}"]`);
const colorPicker = modal.querySelector(`[data-team-color-picker="${index}"]`); const colorPicker = modal.querySelector(`[data-team-color-picker="${index}"]`);
const colorRgb = modal.querySelector(`[data-team-color-rgb="${index}"]`);
const colorPreview = modal.querySelector(`[data-team-color-preview="${index}"]`); const colorPreview = modal.querySelector(`[data-team-color-preview="${index}"]`);
const colorEnabled = modal.querySelector(`[data-team-color-enabled="${index}"]`); const colorEnabled = modal.querySelector(`[data-team-color-enabled="${index}"]`);
const enabledCaption = item?.querySelector(".hockey-team-color-enabled small"); const enabledCaption = item?.querySelector(".hockey-team-color-enabled small");
@@ -3139,6 +3159,7 @@
if (normalized) { if (normalized) {
if (colorText && (commit || colorText.value !== rawValue)) colorText.value = normalized; if (colorText && (commit || colorText.value !== rawValue)) colorText.value = normalized;
if (colorPicker) colorPicker.value = normalized; if (colorPicker) colorPicker.value = normalized;
if (colorRgb) colorRgb.value = teamColorHexToRgb(normalized);
if (colorPreview) colorPreview.style.setProperty("--team-color", normalized); if (colorPreview) colorPreview.style.setProperty("--team-color", normalized);
return normalized; return normalized;
} }
@@ -3148,6 +3169,7 @@
if (!String(rawValue || "").trim()) { if (!String(rawValue || "").trim()) {
if (colorText && commit) colorText.value = ""; if (colorText && commit) colorText.value = "";
if (colorPicker) colorPicker.value = "#FFFFFF"; if (colorPicker) colorPicker.value = "#FFFFFF";
if (colorRgb && commit) colorRgb.value = "";
if (colorPreview) colorPreview.style.setProperty("--team-color", "#FFFFFF"); if (colorPreview) colorPreview.style.setProperty("--team-color", "#FFFFFF");
} }
return ""; return "";
@@ -3167,9 +3189,28 @@
if (normalized) syncTeamColorUi(normalized); if (normalized) syncTeamColorUi(normalized);
}); });
colorText?.addEventListener("blur", () => syncTeamColorUi(colorText.value, { commit: true })); colorText?.addEventListener("blur", () => syncTeamColorUi(colorText.value, { commit: true }));
colorRgb?.addEventListener("input", () => {
const normalized = teamColorRgbToHex(colorRgb.value);
if (normalized) syncTeamColorUi(normalized);
});
colorRgb?.addEventListener("blur", () => {
const raw = String(colorRgb.value || "").trim();
if (!raw) {
colorRgb.value = teamColorHexToRgb(colorText?.value || "");
return;
}
const normalized = teamColorRgbToHex(raw);
if (!normalized) {
setStatus(`RGB цвета ${index} должен содержать три целых числа 0255, например 40, 46, 102`, true);
colorRgb.focus();
return;
}
syncTeamColorUi(normalized, { commit: true });
});
colorEnabled?.addEventListener("change", updateEnabledUi); colorEnabled?.addEventListener("change", updateEnabledUi);
modal.querySelector(`[data-team-color-clear="${index}"]`)?.addEventListener("click", () => { modal.querySelector(`[data-team-color-clear="${index}"]`)?.addEventListener("click", () => {
if (colorText) colorText.value = ""; if (colorText) colorText.value = "";
if (colorRgb) colorRgb.value = "";
if (colorEnabled) colorEnabled.checked = false; if (colorEnabled) colorEnabled.checked = false;
updateEnabledUi(); updateEnabledUi();
syncTeamColorUi("", { commit: true }); syncTeamColorUi("", { commit: true });
@@ -3177,14 +3218,14 @@
}); });
updateEnabledUi(); updateEnabledUi();
}; };
[1, 2, 3].forEach(bindTeamColor); [1, 2, 3, 4].forEach(bindTeamColor);
modal.querySelector("[data-team-form]")?.addEventListener("submit", async (event) => { modal.querySelector("[data-team-form]")?.addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
const form = event.currentTarget; const form = event.currentTarget;
const values = new FormData(form); const values = new FormData(form);
const normalizedColors = {}; const normalizedColors = {};
for (const index of [1, 2, 3]) { for (const index of [1, 2, 3, 4]) {
const field = teamColorFieldName(index); const field = teamColorFieldName(index);
const enabledField = teamColorEnabledName(index); const enabledField = teamColorEnabledName(index);
const raw = String(values.get(field) || "").trim(); const raw = String(values.get(field) || "").trim();
@@ -3214,6 +3255,8 @@
color_2_enabled: normalizedColors.color_2_enabled, color_2_enabled: normalizedColors.color_2_enabled,
color_3_hex: normalizedColors.color_3_hex, color_3_hex: normalizedColors.color_3_hex,
color_3_enabled: normalizedColors.color_3_enabled, color_3_enabled: normalizedColors.color_3_enabled,
color_4_hex: normalizedColors.color_4_hex,
color_4_enabled: normalizedColors.color_4_enabled,
logo_url: values.get("logo_url") || "", logo_url: values.get("logo_url") || "",
active: values.has("active"), active: values.has("active"),
}; };