экспорт маппинга

This commit is contained in:
2026-08-19 17:32:13 +03:00
parent c643a183d8
commit 614a61168a
17 changed files with 1012 additions and 35 deletions

View File

@@ -5951,8 +5951,14 @@ function openTimerQuickEditor(focusActionId = "") {
function normalizeHockeyPlayer(component, row, side, index) {
const props = component.props || {};
const get = (fieldName, fallback = "") => formatValue(getByPath(row, props[fieldName]), fallback);
const externalId = String(get("idField", "") || row?.external_id || row?.id || "");
const databaseId = String(row?.db_id ?? row?.database_id ?? "");
return {
id: String(get("idField", `${side}-${index + 1}`)),
// `id` остаётся external_id для совместимости со старым Mapping.
// `dbId` — реальный PK hockey_players.id из нашей базы.
id: externalId || `${side}-${index + 1}`,
externalId,
dbId: databaseId,
side,
number: get("numberField", ""),
name: get("nameField", `Игрок ${index + 1}`),
@@ -6028,6 +6034,9 @@ function openTimerQuickEditor(focusActionId = "") {
selectedInfraction: null,
selectedPreset: null,
selectedEventId: null,
// Последний выбранный для редактирования штраф остаётся общим, а
// preview/Mapping-выбор хранится отдельно для левой и правой команды.
selectedPreviewEventIds: { home: "", away: "" },
penalties: [],
history: [],
historyOpen: false,
@@ -6128,11 +6137,15 @@ function openTimerQuickEditor(focusActionId = "") {
}
function hockeyPenaltyContext(event) {
const playerExternalId = String(event.player?.externalId || event.player?.external_id || event.player?.id || "");
const playerDbId = String(event.player?.dbId || event.player?.db_id || event.player?.database_id || event.player?.raw?.db_id || "");
return {
penalty_id: event.id,
event_time: event.eventTime,
event_time_ms: event.eventTimeMs,
player: event.player ? clone(event.player) : null,
player_id: playerExternalId,
player_db_id: playerDbId,
team_penalty: Boolean(event.teamPenalty),
infraction: event.infraction ? clone(event.infraction) : null,
preset: event.preset,
@@ -6458,6 +6471,7 @@ function openTimerQuickEditor(focusActionId = "") {
const side = String(event.player?.side || event.side || "").toLowerCase();
const remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
if (board.selectedEventId === event.id) board.selectedEventId = null;
if (board.selectedPreviewEventIds?.[side] === event.id) board.selectedPreviewEventIds[side] = "";
persistHockeyBoard(component, board, true);
refreshHockeyBoardNodes(component);
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
@@ -6476,6 +6490,10 @@ function openTimerQuickEditor(focusActionId = "") {
} else if (command === "remove") {
board.penalties = board.penalties.filter((item) => item.id !== eventId);
if (board.selectedEventId === eventId) board.selectedEventId = null;
{
const removedSide = String(event.player?.side || event.side || "").toLowerCase();
if (board.selectedPreviewEventIds?.[removedSide] === eventId) board.selectedPreviewEventIds[removedSide] = "";
}
addHockeyHistory(
component,
board,
@@ -7043,21 +7061,71 @@ function hockeyInfractionDropdown(component, runtime) {
return field;
}
function hockeyPenaltyPlayerIdentifiers(component, event) {
const side = String(event.player?.side || event.side || "").toLowerCase();
let externalId = String(
event.player?.externalId
|| event.player?.external_id
|| event.player?.id
|| event.player?.raw?.external_id
|| event.player?.raw?.id
|| ""
);
let dbId = String(
event.player?.dbId
|| event.player?.db_id
|| event.player?.database_id
|| event.player?.raw?.db_id
|| event.player?.raw?.database_id
|| ""
);
// Для сохранённых удалений из старых сборок dbId мог отсутствовать.
// В этом случае восстанавливаем его по актуальному составу из БД.
if (externalId && !dbId && ["home", "away"].includes(side)) {
const rosterPlayer = hockeyRoster(component, side).find((player) =>
String(player.externalId || player.id || "") === externalId
);
if (rosterPlayer) {
dbId = String(rosterPlayer.dbId || rosterPlayer.raw?.db_id || "");
externalId = String(rosterPlayer.externalId || rosterPlayer.id || externalId);
}
}
return { externalId, dbId };
}
function hockeyPenaltyIsPreviewSelected(board, event) {
const side = String(event.player?.side || event.side || "").toLowerCase();
return ["home", "away"].includes(side)
&& String(board.selectedPreviewEventIds?.[side] || "") === String(event.id || "");
}
async function hockeySelectPenaltyForPreview(component, event) {
const board = ensureHockeyBoardState(component);
board.selectedEventId = event.id;
if (!board.selectedPreviewEventIds || typeof board.selectedPreviewEventIds !== "object") {
board.selectedPreviewEventIds = { home: "", away: "" };
}
const side = String(event.player?.side || event.side || "").toLowerCase();
if (["home", "away"].includes(side)) board.selectedPreviewEventIds[side] = event.id;
const teamId = side === "home"
? String(getByPath(state.data, "hockey.selected_game.home.id") || getByPath(state.data, "hockey.selected_game.home.external_id") || "")
: side === "away"
? String(getByPath(state.data, "hockey.selected_game.away.id") || getByPath(state.data, "hockey.selected_game.away.external_id") || "")
: "";
const playerIds = hockeyPenaltyPlayerIdentifiers(component, event);
const detail = {
item_id: String(event.external_id || event.id || ""),
value: String(event.external_id || event.id || ""),
penalty_id: String(event.external_id || event.id || ""),
player_id: String(event.player?.id || ""),
// Старый player_id остаётся external_id, чтобы не ломать существующие SQL.
player_id: playerIds.externalId,
player_external_id: playerIds.externalId,
player_db_id: playerIds.dbId,
team_id: teamId,
team_penalty: Boolean(event.teamPenalty),
infraction_id: String(event.infraction?.id || ""),
preset_id: String(event.preset || ""),
side,
@@ -7067,7 +7135,14 @@ async function hockeySelectPenaltyForPreview(component, event) {
status: event.finished ? "finished" : event.running ? "running" : hockeyEventReady(event) ? "ready" : "draft",
selected: true,
};
window.UIBuilderRuntime?.patchData?.({ hockey: { selected_penalty: detail } }, { render: false });
const sidePatch = ["home", "away"].includes(side) ? { [side]: detail } : {};
window.UIBuilderRuntime?.patchData?.({
hockey: {
selected_penalty: detail,
selected_penalties: sidePatch,
}
}, { render: false });
refreshHockeyBoardNodes(component);
const gameId = hockeyTimerSelectedGameId();
@@ -7075,24 +7150,45 @@ async function hockeySelectPenaltyForPreview(component, event) {
emitInteraction(component, "penalty_selected", detail);
return detail;
}
const values = {
selected_penalty_id: detail.penalty_id,
selected_penalty_player_id: detail.player_id,
selected_penalty_player_db_id: detail.player_db_id,
selected_penalty_team_penalty: detail.team_penalty ? "1" : "0",
selected_penalty_team_id: detail.team_id,
selected_penalty_infraction_id: detail.infraction_id,
selected_penalty_preset_id: detail.preset_id,
selected_penalty_side: detail.side,
selected_penalty_event_time: detail.event_time,
selected_penalty_status: detail.status,
selected_penalty_remaining_ms: String(detail.remaining_ms),
selected_penalty_duration_ms: String(detail.duration_ms),
selected_event_id: detail.penalty_id,
};
if (side === "home") {
Object.assign(values, {
selected_home_penalty_id: detail.penalty_id,
selected_home_penalty_player_id: detail.player_id,
selected_home_penalty_player_db_id: detail.player_db_id,
selected_home_penalty_team_penalty: detail.team_penalty ? "1" : "0",
});
} else if (side === "away") {
Object.assign(values, {
selected_away_penalty_id: detail.penalty_id,
selected_away_penalty_player_id: detail.player_id,
selected_away_penalty_player_db_id: detail.player_db_id,
selected_away_penalty_team_penalty: detail.team_penalty ? "1" : "0",
});
}
try {
const response = await fetch("/api/hockey/context/batch", {
method: "POST", cache: "no-store", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
values: {
selected_penalty_id: detail.penalty_id,
selected_penalty_player_id: detail.player_id,
selected_penalty_team_id: detail.team_id,
selected_penalty_infraction_id: detail.infraction_id,
selected_penalty_preset_id: detail.preset_id,
selected_penalty_side: detail.side,
selected_penalty_event_time: detail.event_time,
selected_penalty_status: detail.status,
selected_penalty_remaining_ms: String(detail.remaining_ms),
selected_penalty_duration_ms: String(detail.duration_ms),
selected_event_id: detail.penalty_id,
},
values,
context: {
game_id: gameId,
device_id: currentRuntimeVmixDeviceId(),
@@ -7119,6 +7215,7 @@ function hockeyPenaltyCard(component, event, runtime) {
const card = div(`hpd-team-penalty-card team-${side}`);
card.dataset.penaltyId = event.id;
card.classList.toggle("is-selected", board.selectedEventId === event.id);
card.classList.toggle("is-preview-selected", hockeyPenaltyIsPreviewSelected(board, event));
card.classList.toggle("is-running", event.running);
card.classList.toggle("is-finished", event.finished);
card.classList.toggle("is-ready", hockeyEventReady(event) && !event.running && !event.finished);
@@ -7134,6 +7231,8 @@ function hockeyPenaltyCard(component, event, runtime) {
const presetText = event.preset
? `${escapeHtml(event.preset)} · ${escapeHtml(formatHockeyPenaltyTime(event.durationMs))}`
: "Выберите длительность";
const previewSelected = hockeyPenaltyIsPreviewSelected(board, event);
const playerIds = hockeyPenaltyPlayerIdentifiers(component, event);
card.innerHTML = `
<div class="hpd-team-card-accent"></div>
@@ -7165,9 +7264,18 @@ function hockeyPenaltyCard(component, event, runtime) {
<span>Командное удаление — без игрока</span>
</label>
${previewSelected ? `
<div class="hpd-team-card-identifiers" title="Идентификаторы выбранного удаления для Mapping">
<span><b>ID удаления</b>${escapeHtml(String(event.external_id || event.id || "—"))}</span>
<span><b>Player external ID</b>${escapeHtml(playerIds.externalId || "—")}</span>
<span><b>Player DB ID</b>${escapeHtml(playerIds.dbId || "—")}</span>
<span><b>Командное</b>${event.teamPenalty ? "1" : "0"}</span>
</div>
` : ""}
<div class="hpd-team-card-footer">
<span class="hpd-team-card-status">${
board.selectedEventId === event.id
previewSelected
? "Выбрано · подсмотр"
: event.finished
? "Завершено"
@@ -11116,7 +11224,25 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
el.runtimeSizer.style.height = `${Math.round(canvasHeight * scale * 1000) / 1000}px`;
el.runtimeStage.style.width = `${canvasWidth}px`;
el.runtimeStage.style.height = `${canvasHeight}px`;
el.runtimeStage.style.transform = `translate3d(0, 0, 0) scale(${scale})`;
// BUILD 70: scale the operator UI with CSS zoom when available. Unlike a
// fractional transform: scale(), CSS zoom asks Chromium to lay out and
// rasterize text/1px borders at the requested size instead of enlarging a
// pre-rendered texture. This keeps every runtime tab visibly sharper.
const canUseCssZoom = typeof CSS !== "undefined"
&& typeof CSS.supports === "function"
&& CSS.supports("zoom", "1");
if (canUseCssZoom) {
el.runtimeStage.style.zoom = String(scale);
el.runtimeStage.style.transform = "none";
el.runtimeStage.style.willChange = "auto";
} else {
// Old embedded browsers still get the previous responsive behaviour,
// but avoid translate3d so we do not force an extra GPU texture layer.
el.runtimeStage.style.zoom = "";
el.runtimeStage.style.transform = `scale(${scale})`;
el.runtimeStage.style.willChange = "transform";
}
el.runtimeStage.style.background = state.config.canvas.background || "#0c1421";
el.runtimeStage.dataset.runtimeScale = String(scale);
}
@@ -12718,6 +12844,9 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
state.modifierShortcutChordUsedKey = true;
}
if (editorHotkeyPressed(event)) {
// In runtime the editor entry button is revealed only for admins by
// the hockey account bootstrap. Hidden means editor access is denied.
if (boot.mode !== "editor" && el.runtimeEditorBtn?.classList.contains("hidden")) return;
event.preventDefault();
event.stopPropagation();
if (boot.mode === "editor") window.location.href = boot.runtimeUrl;