добавил в веб-интерфейс отображение рядом с номером игра фото, если фотографии у игрока нет

This commit is contained in:
2026-06-22 15:52:47 +03:00
parent f63061a06d
commit 3e6f767a12
7 changed files with 147 additions and 15 deletions

2
app.py
View File

@@ -558,6 +558,7 @@ class FormationPlayerPayload(BaseModel):
number: str = ""
position: str = ""
is_captain: bool = False
photo_enabled: bool = False
x: float
y: float
@@ -575,6 +576,7 @@ class SquadEditorPlayerPayload(BaseModel):
number: str = ""
position: str = ""
is_captain: bool = False
photo_enabled: bool = False
class SquadEditorCoachPayload(BaseModel):

View File

@@ -59,6 +59,7 @@ def _player_to_editor_dict(p: dict) -> dict:
"number": str(p.get("number", "") or ""),
"position": p.get("position", "") or "",
"is_captain": bool(p.get("is_captain")),
"photo_enabled": bool(p.get("photo_enabled")),
}
@@ -90,7 +91,8 @@ def get_match_lineup_for_editor(
COALESCE(p.first_name, '') AS first_name,
COALESCE(mlp.number::text, '') AS number,
COALESCE(p.position, '') AS position,
COALESCE(mlp.is_captain, FALSE) AS is_captain
COALESCE(mlp.is_captain, FALSE) AS is_captain,
COALESCE(p.photo_enabled, FALSE) AS photo_enabled
FROM match_lineup_players mlp
JOIN players p
ON p.id = mlp.player_id
@@ -141,6 +143,7 @@ def get_match_lineup_for_editor(
"number": row[7] or "",
"position": row[8] or "",
"is_captain": bool(row[9]),
"photo_enabled": bool(row[10]),
}
side = row[0]
@@ -232,11 +235,24 @@ def get_match_lineup_for_vmix(
COALESCE(mlp.is_captain, FALSE) AS is_captain,
COALESCE(p.position, '') AS pos,
p.photo,
COALESCE(p.photo_enabled, FALSE) AS photo_enabled,
CASE WHEN mlp.side = 'home' THEN %s ELSE %s END AS team_id
COALESCE(p.photo_enabled, FALSE) AS photo_enabled
FROM match_lineup_players mlp
LEFT JOIN players p
ON p.id = mlp.player_id
LEFT JOIN LATERAL (
SELECT p.*
FROM players p
WHERE p.team_id = CASE WHEN mlp.side = 'home' THEN %s ELSE %s END
AND (
p.id = mlp.player_id
OR (
COALESCE(p.number::text, '') <> ''
AND COALESCE(p.number::text, '') = COALESCE(mlp.number::text, '')
)
)
ORDER BY
CASE WHEN p.id = mlp.player_id THEN 0 ELSE 1 END,
p.id
LIMIT 1
) p ON TRUE
WHERE mlp.match_id = %s
ORDER BY
CASE
@@ -298,7 +314,7 @@ def get_match_lineup_for_vmix(
"pos": row[10] or "",
"photo": row[11] or "",
"photo_enabled": bool(row[12]),
"team_id": row[13],
"team_id": home_team_id if row[0] == "home" else away_team_id,
}
side = row[0]

View File

@@ -22,7 +22,8 @@ def get_team_players_for_match_editor(
COALESCE(p.first_name, '') AS first_name,
COALESCE(p.number::text, '') AS number,
COALESCE(p.position, '') AS position,
FALSE AS is_captain
FALSE AS is_captain,
COALESCE(p.photo_enabled, FALSE) AS photo_enabled
FROM players p
WHERE p.team_id = %s
ORDER BY
@@ -51,6 +52,7 @@ def get_team_players_for_match_editor(
"number": row[4] or "",
"position": row[5] or "",
"is_captain": bool(row[6]),
"photo_enabled": bool(row[7]),
}
for row in rows
]

View File

@@ -469,7 +469,48 @@ def get_vmix_team_formations(session_token: str, team_id: int):
mf.team_id,
COALESCE(mf.player_name, '') AS player_name
FROM match_formations mf
LEFT JOIN players p ON p.id = mf.player_id
LEFT JOIN LATERAL (
SELECT p.*
FROM players p
WHERE p.team_id = mf.team_id
AND (
p.id = mf.player_id
OR (
COALESCE(p.number::text, '') <> ''
AND COALESCE(p.number::text, '') = COALESCE(mf.number, '')
)
OR (
COALESCE(mf.player_name, '') <> ''
AND (
LOWER(COALESCE(p.full_name, '')) = LOWER(TRIM(mf.player_name))
OR LOWER(TRIM(COALESCE(p.last_name, '') || ' ' || COALESCE(p.first_name, ''))) = LOWER(TRIM(mf.player_name))
OR LOWER(TRIM(COALESCE(p.first_name, '') || ' ' || COALESCE(p.last_name, ''))) = LOWER(TRIM(mf.player_name))
)
)
)
ORDER BY
CASE
WHEN p.id = mf.player_id
AND (
COALESCE(mf.number, '') = ''
OR COALESCE(p.number::text, '') = COALESCE(mf.number, '')
OR LOWER(COALESCE(p.full_name, '')) = LOWER(TRIM(COALESCE(mf.player_name, '')))
OR LOWER(TRIM(COALESCE(p.last_name, '') || ' ' || COALESCE(p.first_name, ''))) = LOWER(TRIM(COALESCE(mf.player_name, '')))
OR LOWER(TRIM(COALESCE(p.first_name, '') || ' ' || COALESCE(p.last_name, ''))) = LOWER(TRIM(COALESCE(mf.player_name, '')))
)
THEN 0
WHEN COALESCE(p.number::text, '') = COALESCE(mf.number, '')
AND COALESCE(mf.player_name, '') <> ''
THEN 1
WHEN COALESCE(mf.player_name, '') <> ''
THEN 2
WHEN COALESCE(p.number::text, '') = COALESCE(mf.number, '')
THEN 3
ELSE 4
END,
p.id
LIMIT 1
) p ON TRUE
WHERE mf.match_id = %s and mf.team_id = %s
ORDER BY mf.id
"""

View File

@@ -2277,6 +2277,7 @@ function getRowPlayer(row) {
number: row.dataset.playerNumber || "",
position: row.dataset.playerPosition || "",
isCaptain: row.dataset.playerCaptain === "1",
photoEnabled: row.dataset.playerPhotoEnabled === "1",
disciplineType: row.dataset.disciplineType || ""
};
}
@@ -2289,6 +2290,7 @@ function setRowPlayer(row, player) {
row.dataset.playerNumber = player.number ?? "";
row.dataset.playerPosition = player.position ?? "";
row.dataset.playerCaptain = player.isCaptain ? "1" : "0";
row.dataset.playerPhotoEnabled = player.photoEnabled ? "1" : "0";
row.dataset.disciplineType = player.disciplineType ?? "";
}
@@ -2380,6 +2382,9 @@ function renderLineupRow(row) {
const idJs = JSON.stringify(player.id || player.number || "");
const nameJs = JSON.stringify(displayName);
const noPhotoIconHtml = player.photoEnabled
? ''
: '<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>';
let actionsHtml = '';
if (role === "starting") {
@@ -2411,7 +2416,12 @@ function renderLineupRow(row) {
}
row.innerHTML = `
<td class="num-cell">${escapeHtml(player.number || "")}</td>
<td class="num-cell">
<span class="num-with-photo-status">
${noPhotoIconHtml}
<span class="player-number-value">${escapeHtml(player.number || "")}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<button
type="button"
@@ -3264,7 +3274,8 @@ function normalizePlayer(item = {}) {
first_name: item.first_name || "",
number: item.number || "",
position: item.position || "",
is_captain: !!item.is_captain
is_captain: !!item.is_captain,
photo_enabled: item.photo_enabled === true || item.photo_enabled === 1 || item.photo_enabled === "1"
};
}
@@ -3651,6 +3662,7 @@ function rerenderLineupTable(side, role) {
number: player.number,
position: player.position,
isCaptain: !!player.is_captain,
photoEnabled: !!player.photo_enabled,
disciplineType: ""
});
renderLineupRow(row);
@@ -3680,6 +3692,7 @@ function rebuildFormationFromMatchData(side) {
position: p.position || "",
is_captain: !!p.is_captain,
photo: p.photo || "",
photo_enabled: p.photo_enabled === true || p.photo_enabled === 1 || p.photo_enabled === "1",
x: old?.x ?? 50,
y: old?.y ?? (12 + idx * 6)
};

View File

@@ -2489,3 +2489,29 @@ body:not(.edit-mode-on) .referee-search {
.btn-danger:hover {
background: #e14b5b;
}
.num-with-photo-status {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
min-width: 44px;
}
.no-photo-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 12px;
line-height: 1;
opacity: 0.65;
filter: grayscale(1);
cursor: help;
}
.player-number-value {
display: inline-block;
min-width: 16px;
text-align: center;
}

View File

@@ -402,8 +402,16 @@
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">{{ p.number }}</td>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
@@ -510,8 +518,16 @@ data-role="{{ p.position or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">{{ p.number }}</td>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
@@ -628,8 +644,16 @@ data-role="{{ p.position or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">{{ p.number }}</td>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
@@ -736,8 +760,16 @@ data-role="{{ p.position or '' }}"
data-player-number="{{ p.number or '' }}"
data-player-position="{{ p.position or '' }}"
data-player-captain="{{ 1 if p.is_captain else 0 }}"
data-player-photo-enabled="{{ 1 if p.photo_enabled else 0 }}"
>
<td class="num-cell">{{ p.number }}</td>
<td class="num-cell">
<span class="num-with-photo-status">
{% if not p.photo_enabled %}
<span class="no-photo-icon" title="Фото выключено: в JSON будет подставлена EMPTY.png" aria-label="Фото выключено">📷</span>
{% endif %}
<span class="player-number-value">{{ p.number }}</span>
</span>
</td>
<td class="player-cell" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"