экспорт маппинга
This commit is contained in:
@@ -1590,3 +1590,12 @@
|
||||
.hockey-period-status-row input { width:100%; min-height:36px; box-sizing:border-box; padding:0 10px; color:#eef7ff; border:1px solid #304b67; border-radius:8px; outline:none; background:#101f31; font-size:11px; font-weight:720; }
|
||||
.hockey-period-status-row input:focus { border-color:#48dfbd; box-shadow:0 0 0 3px rgba(72,223,189,.09); }
|
||||
@media (max-width:760px) { .hockey-period-status-row { grid-template-columns:90px 1fr; } .hockey-period-status-row.is-head span:nth-child(3) { display:none; } .hockey-period-status-row input:last-child { grid-column:2; } }
|
||||
|
||||
/* BUILD72 — portable vMix Mapping transfer/import */
|
||||
.hockey-map-transfer-inline{display:grid;grid-template-columns:minmax(190px,1fr) auto;gap:6px;align-items:center}
|
||||
.hockey-map-transfer-inline select{min-width:190px}
|
||||
.hockey-mapping-import{border-top:1px solid rgba(57,80,104,.7);padding-top:12px;margin-top:12px}
|
||||
.hockey-mapping-import input[type="file"]{height:auto;min-height:38px;padding:7px;background:#0b1723;color:#9fb5c9}
|
||||
.hockey-mapping-import small{display:block;color:#6f879d;font-size:8px;line-height:1.35}
|
||||
.hockey-mapping-import .hockey-directory-check{margin:2px 0}
|
||||
@media(max-width:900px){.hockey-map-transfer-inline{grid-template-columns:1fr}.hockey-map-transfer-inline select{min-width:0;width:100%}}
|
||||
|
||||
@@ -86,6 +86,31 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function downloadJson(filename, data) {
|
||||
const safe = String(filename || "mapping.json").replace(/[\\/:*?"<>|]+/g, "_");
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url; anchor.download = safe; document.body.appendChild(anchor); anchor.click(); anchor.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
function mappingTransferReport(result, action = "Mapping перенесён") {
|
||||
const report = result?.report || {};
|
||||
const mapped = Number(report.mapped || 0);
|
||||
const total = Number(report.total || mapped);
|
||||
const skipped = Number(report.skipped || 0);
|
||||
const by = report.matched_by || {};
|
||||
const methods = [
|
||||
Number(by.key || 0) ? `key ${Number(by.key || 0)}` : "",
|
||||
Number(by.title || 0) ? `название ${Number(by.title || 0)}` : "",
|
||||
Number(by.number || 0) ? `номер ${Number(by.number || 0)}` : "",
|
||||
].filter(Boolean).join(" · ");
|
||||
return `${action}: ${mapped}/${total} связей${skipped ? ` · пропущено ${skipped}` : ""}${methods ? ` · ${methods}` : ""}`;
|
||||
}
|
||||
|
||||
function selectedContext() {
|
||||
const data = window.UIBuilderRuntime?.getData?.() || {};
|
||||
const game = data.hockey?.selected_game || null;
|
||||
@@ -2148,6 +2173,11 @@
|
||||
<div class="hockey-map-maintenance">
|
||||
<select data-map-refresh-device><option value="">Обновить структуру из Agent…</option>${usableDevices.map((item) => `<option value="${escapeHtml(item.device_id)}">${escapeHtml(item.name || item.device_id)} · ${Number(item.input_count || 0)}/${Number(item.field_count || 0)}</option>`).join("")}</select>
|
||||
<button type="button" data-map-refresh>Считать vMix заново</button>
|
||||
<div class="hockey-map-transfer-inline">
|
||||
<select data-map-copy-device><option value="">Перенести на другой Agent…</option>${usableDevices.map((item) => `<option value="${escapeHtml(item.device_id)}">${escapeHtml(item.name || item.device_id)} · ${Number(item.input_count || 0)} Inputs</option>`).join("")}</select>
|
||||
<button type="button" data-map-copy-to-device>Копировать на Agent</button>
|
||||
</div>
|
||||
<button type="button" data-map-export>Экспорт JSON</button>
|
||||
<button type="button" class="danger" data-map-delete>Удалить профиль</button>
|
||||
</div>
|
||||
<div><label class="hockey-directory-check"><input type="checkbox" data-mapping-active ${profile.active !== false ? "checked" : ""}><span>Профиль активен</span></label><button type="button" data-map-apply-now title="Применяются сохранённые на сервере связи">Применить весь Mapping</button><button type="button" class="is-accent" data-map-save>Сохранить mapping</button></div>
|
||||
@@ -2166,6 +2196,15 @@
|
||||
<label>Описание<input name="description" placeholder="Основной графический пакет"></label>
|
||||
<button type="submit" class="is-accent" ${usableDevices.length ? "" : "disabled"}>Создать из vMix</button>
|
||||
</form>
|
||||
<form class="hockey-mapping-create hockey-mapping-import" data-mapping-import>
|
||||
<h3>Импорт готового Mapping</h3>
|
||||
<label>Файл Mapping<input type="file" name="file" accept="application/json,.json" required></label>
|
||||
<label>Применить к Agent<select name="device_id" required><option value="">Выберите устройство</option>${usableDevices.map((item) => `<option value="${escapeHtml(item.device_id)}">${escapeHtml(item.name || item.device_id)} · ${Number(item.input_count || 0)} Inputs</option>`).join("")}</select></label>
|
||||
<label>Новое название<input name="name" placeholder="Оставить название из файла"></label>
|
||||
<label class="hockey-directory-check"><input type="checkbox" name="replace_existing"><span>Заменить активный Mapping этого vMix</span></label>
|
||||
<button type="submit" ${usableDevices.length ? "" : "disabled"}>Загрузить и применить</button>
|
||||
<small>Input ищется по key → названию → номеру. Поле должно совпасть по имени.</small>
|
||||
</form>
|
||||
<h3>Профили</h3>
|
||||
<div class="hockey-mapping-profiles">${profileButtons}</div>
|
||||
</aside>
|
||||
@@ -2390,6 +2429,31 @@
|
||||
} catch (error) { setStatus(error.message, true); }
|
||||
renderMapping();
|
||||
});
|
||||
|
||||
modal.querySelector("[data-mapping-import]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File) || !file.size) return setStatus("Выберите JSON-файл Mapping.", true), renderMapping();
|
||||
if (file.size > 8 * 1024 * 1024) return setStatus("Файл Mapping слишком большой.", true), renderMapping();
|
||||
try {
|
||||
const document = JSON.parse(await file.text());
|
||||
const result = await request("/api/hockey/admin/vmix-mapping/import", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
device_id: String(form.get("device_id") || ""),
|
||||
name: String(form.get("name") || ""),
|
||||
replace_existing: Boolean(form.get("replace_existing")),
|
||||
apply_now: true,
|
||||
document,
|
||||
}),
|
||||
});
|
||||
await loadMapping(result.profile?.id || null);
|
||||
state.mappingSelectedInputKey = ""; state.mappingVmixScrollTop = 0;
|
||||
setStatus(mappingTransferReport(result, `Mapping «${result.profile?.name || ""}» импортирован`), Number(result.report?.skipped || 0) > 0);
|
||||
} catch (error) { setStatus(error.message || "Не удалось прочитать Mapping JSON", true); }
|
||||
renderMapping();
|
||||
});
|
||||
modal.querySelector("[data-map-input-picker]")?.addEventListener("change", (event) => {
|
||||
state.mappingSelectedInputKey = event.currentTarget.value;
|
||||
state.mappingTargetField = "";
|
||||
@@ -2646,6 +2710,36 @@
|
||||
} catch (error) { setStatus(error.message, true); }
|
||||
renderMapping();
|
||||
});
|
||||
modal.querySelector("[data-map-export]")?.addEventListener("click", async () => {
|
||||
if (!state.mappingActiveProfile) return;
|
||||
try {
|
||||
const document = await request(`/api/hockey/admin/vmix-mapping/profiles/${state.mappingActiveProfile.id}/export`);
|
||||
const base = String(state.mappingActiveProfile.name || "mapping").trim().replace(/\s+/g, "_");
|
||||
downloadJson(`${base}.hockey-mapping.json`, document);
|
||||
setStatus(`Mapping «${state.mappingActiveProfile.name}» экспортирован.`);
|
||||
} catch (error) { setStatus(error.message, true); }
|
||||
renderMapping();
|
||||
});
|
||||
modal.querySelector("[data-map-copy-to-device]")?.addEventListener("click", async () => {
|
||||
if (!state.mappingActiveProfile) return;
|
||||
const deviceId = modal.querySelector("[data-map-copy-device]")?.value || "";
|
||||
if (!deviceId) return setStatus("Выберите Agent, на который нужно перенести Mapping.", true), renderMapping();
|
||||
const target = (state.mappingDevices?.devices || []).find((item) => item.device_id === deviceId);
|
||||
const replaceExisting = Boolean(target?.mapping && target.mapping.id !== state.mappingActiveProfile.id)
|
||||
? window.confirm(`На «${target?.name || deviceId}» уже назначен Mapping «${target.mapping.name}». Заменить его новой копией?`)
|
||||
: false;
|
||||
if (target?.mapping && target.mapping.id !== state.mappingActiveProfile.id && !replaceExisting) return;
|
||||
try {
|
||||
const result = await request(`/api/hockey/admin/vmix-mapping/profiles/${state.mappingActiveProfile.id}/copy-to-device`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ device_id: deviceId, replace_existing: replaceExisting, apply_now: true }),
|
||||
});
|
||||
await loadMapping(result.profile?.id || state.mappingActiveProfile.id);
|
||||
state.mappingSelectedInputKey = ""; state.mappingVmixScrollTop = 0;
|
||||
setStatus(mappingTransferReport(result, `Mapping перенесён на ${target?.name || deviceId}`), Number(result.report?.skipped || 0) > 0);
|
||||
} catch (error) { setStatus(error.message, true); }
|
||||
renderMapping();
|
||||
});
|
||||
modal.querySelector("[data-map-apply-now]")?.addEventListener("click", async () => {
|
||||
const deviceId = state.mappingTestDeviceId || modal.querySelector("[data-map-test-device]")?.value || "";
|
||||
if (!deviceId) return setStatus("Выберите online Agent для применения Mapping.", true), renderMapping();
|
||||
|
||||
@@ -317,6 +317,9 @@
|
||||
}
|
||||
state.currentUser = user;
|
||||
state.isAdmin = Boolean(user?.is_admin);
|
||||
const editorButton = document.getElementById("runtimeEditorBtn");
|
||||
editorButton?.classList.toggle("hidden", !state.isAdmin);
|
||||
document.body.dataset.hockeyAdmin = state.isAdmin ? "1" : "0";
|
||||
const currentAccountId = String(user?.id || "").trim();
|
||||
if (!currentAccountId) return;
|
||||
const previousAccountId = String(localStorage.getItem(storage.account) || "").trim();
|
||||
|
||||
Reference in New Issue
Block a user