копировать маппинг
This commit is contained in:
@@ -1693,15 +1693,36 @@
|
||||
|
||||
/* Build 74: reusable Mapping configs + one-click apply to current Agent. */
|
||||
.hockey-map-config-hint{font-size:11px;line-height:1.45;color:#8ea0b6;background:#0d1520;border:1px solid #2b394c;border-radius:10px;padding:9px 10px}
|
||||
.hockey-mapping-profile-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:stretch;border-radius:11px}
|
||||
.hockey-mapping-profile-row.is-active{box-shadow:0 0 0 1px rgba(56,189,248,.18)}
|
||||
.hockey-mapping-profile-row .hockey-mapping-profile{min-width:0;width:100%}
|
||||
.hockey-map-use-profile{min-width:106px;border:1px solid #2f5870;background:#102433;color:#9bdcff;border-radius:10px;padding:7px 9px;font-size:11px;font-weight:700;cursor:pointer;white-space:normal;line-height:1.25}
|
||||
.hockey-mapping-profile-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:5px;align-items:center;border-radius:9px;padding:2px}
|
||||
.hockey-mapping-profile-row.is-active{background:rgba(56,189,248,.045);box-shadow:0 0 0 1px rgba(56,189,248,.18)}
|
||||
.hockey-mapping-profile-row .hockey-mapping-profile{min-width:0;width:100%;padding:7px 8px;border-radius:8px}
|
||||
.hockey-mapping-profile-row .hockey-mapping-profile strong{font-size:11px;line-height:1.25}
|
||||
.hockey-mapping-profile-row .hockey-mapping-profile small{font-size:9px;line-height:1.2;opacity:.82}
|
||||
.hockey-map-profile-actions{display:flex;align-items:center;gap:4px;padding-right:2px}
|
||||
.hockey-map-profile-icon{width:28px;height:28px;min-width:28px;display:grid;place-items:center;border:1px solid #31465b;background:#0f1d2a;color:#9cb4c8;border-radius:8px;padding:0;font-size:12px;font-weight:850;line-height:1;cursor:pointer;transition:border-color .12s ease,background .12s ease,color .12s ease,transform .12s ease}
|
||||
.hockey-map-profile-icon:hover{border-color:#4d718e;background:#14283a;color:#e7f4ff;transform:translateY(-1px)}
|
||||
.hockey-map-use-profile{color:#8fdcff}
|
||||
.hockey-map-use-profile:hover{border-color:#38bdf8;background:#123047;color:#d9f4ff}
|
||||
.hockey-map-use-profile.is-used{border-color:#276749;background:#102b20;color:#8ff0b5}
|
||||
.hockey-map-copy-profile{font-size:15px;color:#b9c9da}
|
||||
.hockey-map-copy-profile:hover{border-color:#7c5cc7;background:#211a38;color:#ddceff}
|
||||
.hockey-mapping-create-wrap{border:1px solid #334155;background:#101722;border-radius:12px;overflow:hidden}
|
||||
.hockey-mapping-create-wrap>summary{cursor:pointer;padding:10px 12px;color:#b9c7d8;font-size:12px;font-weight:700;list-style:none}
|
||||
.hockey-mapping-create-wrap>summary::-webkit-details-marker{display:none}
|
||||
.hockey-mapping-create-wrap[open]>summary{border-bottom:1px solid #2e3b4e}
|
||||
.hockey-mapping-create-wrap .hockey-mapping-create{border:0;border-radius:0;background:transparent}
|
||||
@media(max-width:520px){.hockey-mapping-profile-row{grid-template-columns:1fr}.hockey-map-use-profile{min-height:34px}}
|
||||
@media(max-width:520px){.hockey-mapping-profile-row{grid-template-columns:minmax(0,1fr) auto}.hockey-map-profile-icon{width:30px;height:30px;min-width:30px}}
|
||||
|
||||
|
||||
/* Build 75: compact Mapping data browser. Heavy SQL tools and match/period
|
||||
data groups start collapsed and remember their open state while editing. */
|
||||
.hockey-map-sql-cell-quick>summary,.hockey-map-table-picker>summary{list-style:none;cursor:pointer;user-select:none}
|
||||
.hockey-map-sql-cell-quick>summary::-webkit-details-marker,.hockey-map-table-picker>summary::-webkit-details-marker{display:none}
|
||||
.hockey-map-sql-cell-quick>summary{display:flex;align-items:flex-end;justify-content:space-between;gap:10px}
|
||||
.hockey-map-sql-cell-quick>summary>div{display:grid;gap:2px}.hockey-map-sql-cell-quick>summary span{font-size:8px;font-weight:950;letter-spacing:.12em;color:#69c9ef}.hockey-map-sql-cell-quick.is-target-ready>summary span{color:#48dfbd}.hockey-map-sql-cell-quick>summary strong{font-size:12px;color:#edf5ff}.hockey-map-sql-cell-quick>summary small{max-width:48%;font-size:9px;color:#7e96ad;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.hockey-map-sql-cell-quick>summary::after,.hockey-map-table-picker>summary::after,.hockey-map-data-group>summary::after{content:"▸";color:#6f879e;font-size:11px;transition:transform .12s ease}
|
||||
.hockey-map-sql-cell-quick[open]>summary::after,.hockey-map-table-picker[open]>summary::after,.hockey-map-data-group[open]>summary::after{transform:rotate(90deg)}
|
||||
.hockey-map-table-picker>summary{display:flex;align-items:center;justify-content:space-between;gap:10px}.hockey-map-table-picker>summary>div{display:grid;gap:2px;min-width:0}.hockey-map-table-picker>summary span{font-size:8px;font-weight:950;letter-spacing:.12em;color:#69c9ef}.hockey-map-table-picker>summary strong{font-size:11px;color:#dceaf6}.hockey-map-table-picker>summary>b{margin-left:auto;padding:4px 7px;border-radius:999px;background:#173148;color:#8ed5ef;font-size:9px;white-space:nowrap}
|
||||
.hockey-map-sql-cell-quick:not([open]),.hockey-map-table-picker:not([open]){min-height:0!important;height:auto!important;display:block;padding:10px}
|
||||
.hockey-map-sql-cell-quick:not([open])>summary,.hockey-map-table-picker:not([open])>summary{margin:0}
|
||||
@media(max-width:1100px){.hockey-map-sql-cell-quick>summary{align-items:flex-start;flex-direction:column}.hockey-map-sql-cell-quick>summary small{max-width:none;text-align:left}}
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
mappingSelectedTableRow: 1,
|
||||
mappingSelectedTableColumn: "",
|
||||
mappingTableRowSearch: "",
|
||||
mappingSqlCellOpen: false,
|
||||
mappingSqlTableOpen: false,
|
||||
mappingOpenDataGroups: {},
|
||||
mappingTestDeviceId: "",
|
||||
mappingWorkspaceTab: "links",
|
||||
mappingCatalog: null,
|
||||
@@ -1837,8 +1840,8 @@
|
||||
category.items.push(item);
|
||||
}
|
||||
const selectedSource = state.mappingSelectedSourceKey;
|
||||
const groups = categories.map((category, index) => `
|
||||
<details class="hockey-map-data-group" ${index < 3 || query ? "open" : ""}>
|
||||
const groups = categories.map((category) => `
|
||||
<details class="hockey-map-data-group" data-map-data-group="${escapeHtml(category.name)}" ${query || state.mappingOpenDataGroups[category.name] ? "open" : ""}>
|
||||
<summary><span>${escapeHtml(category.name)}</span><b>${category.items.length}</b></summary>
|
||||
<div>${category.items.map((item) => `
|
||||
<button type="button" class="hockey-map-data-item ${selectedSource === item.key ? "is-selected" : ""}" data-map-source="${escapeHtml(item.key)}">
|
||||
@@ -1872,11 +1875,11 @@
|
||||
let quickTableMarkup = "";
|
||||
if (tables.length) {
|
||||
quickTableMarkup = `
|
||||
<section class="hockey-map-sql-cell-quick ${targetField ? "is-target-ready" : ""}">
|
||||
<header>
|
||||
<details class="hockey-map-sql-cell-quick ${targetField ? "is-target-ready" : ""}" data-map-sql-cell-details ${state.mappingSqlCellOpen ? "open" : ""}>
|
||||
<summary>
|
||||
<div><span>SQL ЯЧЕЙКА</span><strong>Источник → строка → столбец</strong></div>
|
||||
<small>${targetField ? `Для ${escapeHtml(currentInput?.title || "Input")} → ${escapeHtml(targetField.name || "")}` : "Сначала выберите поле vMix справа"}</small>
|
||||
</header>
|
||||
</summary>
|
||||
<div class="hockey-map-sql-cell-grid">
|
||||
<label><span>SQL источник</span><select data-map-quick-table-source>${tables.map((table) => `<option value="${escapeHtml(table.code)}" ${String(table.code) === String(selectedTable?.code) ? "selected" : ""}>${escapeHtml(table.name || table.code)}</option>`).join("")}</select></label>
|
||||
<label><span>Строка</span><select data-map-quick-table-row ${selectedTableRows.length ? "" : "disabled"}>${selectedTableRows.map((row) => `<option value="${Number(row.index || 0)}" ${Number(row.index || 0) === Number(selectedTableRow?.index || 0) ? "selected" : ""}>#${Number(row.index || 0)} · ${escapeHtml(row.label || `Строка ${row.index}`)}</option>`).join("")}</select></label>
|
||||
@@ -1886,7 +1889,7 @@
|
||||
<div><span>Текущее значение</span><strong>${escapeHtml(mappingDisplayValue(quickCellSource?.value, quickCellSource?.kind || "text"))}</strong><code>${escapeHtml(quickCellKey || "—")}</code></div>
|
||||
<button type="button" data-map-quick-cell-link ${quickLinkEnabled ? "" : "disabled"}>${targetField ? `Связать с ${escapeHtml(targetField.name || "полем")}` : "Выберите поле vMix"}</button>
|
||||
</div>
|
||||
</section>`;
|
||||
</details>`;
|
||||
const rowSearch = String(state.mappingTableRowSearch || "").trim().toLowerCase();
|
||||
const allRows = selectedTable?.rows || [];
|
||||
const filteredRows = allRows.filter((row) => {
|
||||
@@ -1908,8 +1911,8 @@
|
||||
}).join("")}
|
||||
</tr>`).join("");
|
||||
tableMarkup = `
|
||||
<section class="hockey-map-table-picker">
|
||||
<header><div><span>ТАБЛИЦА SQL</span><strong>Выберите конкретную строку и столбец</strong></div><b>${Number(selectedTable?.row_count || 0)} строк</b></header>
|
||||
<details class="hockey-map-table-picker" data-map-sql-table-details ${state.mappingSqlTableOpen ? "open" : ""}>
|
||||
<summary><div><span>ТАБЛИЦА SQL</span><strong>Выберите конкретную строку и столбец</strong></div><b>${Number(selectedTable?.row_count || 0)} строк</b></summary>
|
||||
<div class="hockey-map-table-controls">
|
||||
<select data-map-table-source>${tables.map((table) => `<option value="${escapeHtml(table.code)}" ${String(table.code) === String(selectedTable?.code) ? "selected" : ""}>${escapeHtml(table.name || table.code)} · ${Number(table.row_count || 0)} строк</option>`).join("")}</select>
|
||||
<label><span>⌕</span><input data-map-table-row-search value="${escapeHtml(state.mappingTableRowSearch)}" placeholder="Поиск строки по любому значению…"></label>
|
||||
@@ -1917,7 +1920,7 @@
|
||||
${selectedTable?.skipped ? `<div class="hockey-directory-empty">Не хватает параметров: ${escapeHtml((selectedTable.missing || []).map((x) => `:${x}`).join(", "))}</div>` : selectedTable?.error ? `<div class="hockey-directory-empty is-error">${escapeHtml(selectedTable.error)}</div>` : `
|
||||
<div class="hockey-map-table-grid" style="height:${tableGridHeight}px"><table><thead><tr><th>Строка</th>${columns.map((column) => `<th><strong>${escapeHtml(column.label || column.key)}${column.localized ? ` <i class="hockey-map-lang-auto">AUTO ${String(column.language || selectedTable?.language || "ru").toUpperCase()}</i>` : ""}</strong><small>${escapeHtml(column.key)}</small></th>`).join("")}</tr></thead><tbody>${cells || `<tr><td colspan="${columns.length + 1}">Нет строк</td></tr>`}</tbody></table></div>
|
||||
${filteredRows.length > visibleRows.length ? `<small class="hockey-map-table-limit">Показаны первые ${visibleRows.length} из ${filteredRows.length} строк. Используйте поиск строки.</small>` : ""}`}
|
||||
</section>`;
|
||||
</details>`;
|
||||
}
|
||||
|
||||
const targetHint = targetField
|
||||
@@ -2160,11 +2163,14 @@
|
||||
const isUsed = Boolean(selectedDevice?.mapping && Number(selectedDevice.mapping.source_profile_id || selectedDevice.mapping.id || 0) === Number(item.id));
|
||||
return `
|
||||
<div class="hockey-mapping-profile-row ${profile?.id === item.id ? "is-active" : ""}">
|
||||
<button type="button" class="hockey-mapping-profile ${profile?.id === item.id ? "is-active" : ""}" data-mapping-profile="${item.id}">
|
||||
<button type="button" class="hockey-mapping-profile ${profile?.id === item.id ? "is-active" : ""}" data-mapping-profile="${item.id}" title="Открыть и редактировать конфиг">
|
||||
<strong>${escapeHtml(item.name)}</strong>
|
||||
<small>v${Number(item.version || 1)} · ${Number(item.field_count || 0)} связей${item.created_by ? ` · ${escapeHtml(item.created_by)}` : ""}</small>
|
||||
</button>
|
||||
<button type="button" class="hockey-map-use-profile ${isUsed ? "is-used" : ""}" data-map-use-profile="${item.id}" ${isUsed ? 'title="Этот конфиг уже выбран на вашем Agent"' : ""}>${isUsed ? "✓ Используется" : "Применить к моему Agent"}</button>
|
||||
<div class="hockey-map-profile-actions">
|
||||
<button type="button" class="hockey-map-profile-icon hockey-map-use-profile ${isUsed ? "is-used" : ""}" data-map-use-profile="${item.id}" title="${isUsed ? "Этот конфиг уже используется на моём Agent" : "Применить к моему Agent"}" aria-label="${isUsed ? "Используется на моём Agent" : "Применить к моему Agent"}">${isUsed ? "✓" : "▶"}</button>
|
||||
<button type="button" class="hockey-map-profile-icon hockey-map-copy-profile" data-map-copy-profile="${item.id}" title="Копировать конфиг" aria-label="Копировать конфиг">⧉</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("") : `<div class="hockey-directory-empty">Mapping-конфигов ещё нет.</div>`;
|
||||
|
||||
@@ -2220,7 +2226,7 @@
|
||||
<h3>vMix проекты</h3>
|
||||
<div class="hockey-mapping-devices">${deviceRows}</div>
|
||||
<h3>Конфиги Mapping</h3>
|
||||
<div class="hockey-map-config-hint">Выберите любой готовый конфиг — свой или чужой — и нажмите «Применить к моему Agent». Он будет использован для текущего открытого матча.</div>
|
||||
<div class="hockey-map-config-hint">▶ применяет готовый конфиг к вашему Agent. ⧉ создаёт независимую копию со всеми связями — её можно переименовать, пересчитать под другой vMix и отредактировать.</div>
|
||||
<div class="hockey-mapping-profiles">${profileButtons}</div>
|
||||
<details class="hockey-mapping-create-wrap">
|
||||
<summary>+ Создать новый конфиг из vMix</summary>
|
||||
@@ -2456,6 +2462,27 @@
|
||||
renderMapping();
|
||||
}));
|
||||
|
||||
modal.querySelectorAll("[data-map-copy-profile]").forEach((button) => button.addEventListener("click", async (event) => {
|
||||
event.stopPropagation();
|
||||
const profileId = Number(button.dataset.mapCopyProfile || 0);
|
||||
if (!profileId) return;
|
||||
const source = (state.mappingProfiles?.profiles || []).find((item) => Number(item.id) === profileId);
|
||||
button.disabled = true;
|
||||
button.textContent = "…";
|
||||
try {
|
||||
const created = await request(`/api/hockey/admin/vmix-mapping/profiles/${profileId}/duplicate`, { method: "POST" });
|
||||
state.mappingSelectedInputKey = "";
|
||||
state.mappingTargetField = "";
|
||||
state.mappingSelectedSourceKey = "";
|
||||
state.mappingVmixScrollTop = 0;
|
||||
await loadMapping(created.id);
|
||||
setStatus(`Копия «${created.name}» создана · ${Number(created.copied_fields || created.fields?.length || 0)} связей. Можно переименовать, считать структуру другого vMix и редактировать.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message || `Не удалось скопировать конфиг «${source?.name || profileId}»`, true);
|
||||
}
|
||||
renderMapping();
|
||||
}));
|
||||
|
||||
modal.querySelectorAll("[data-mapping-profile]").forEach((button) => button.addEventListener("click", async () => {
|
||||
try {
|
||||
state.mappingActiveProfile = await request(`/api/hockey/admin/vmix-mapping/profiles/${button.dataset.mappingProfile}`);
|
||||
@@ -2536,6 +2563,16 @@
|
||||
state.mappingTableRowSearch = "";
|
||||
renderMapping();
|
||||
});
|
||||
modal.querySelectorAll("[data-map-data-group]").forEach((details) => details.addEventListener("toggle", (event) => {
|
||||
const key = String(event.currentTarget.dataset.mapDataGroup || "");
|
||||
if (key) state.mappingOpenDataGroups[key] = Boolean(event.currentTarget.open);
|
||||
}));
|
||||
modal.querySelector("[data-map-sql-cell-details]")?.addEventListener("toggle", (event) => {
|
||||
state.mappingSqlCellOpen = Boolean(event.currentTarget.open);
|
||||
});
|
||||
modal.querySelector("[data-map-sql-table-details]")?.addEventListener("toggle", (event) => {
|
||||
state.mappingSqlTableOpen = Boolean(event.currentTarget.open);
|
||||
});
|
||||
modal.querySelector("[data-map-quick-table-source]")?.addEventListener("change", (event) => {
|
||||
state.mappingSelectedTableSource = event.currentTarget.value;
|
||||
state.mappingSelectedTableRow = 1;
|
||||
@@ -2817,7 +2854,14 @@
|
||||
if (!deviceId) return setStatus("Выберите Agent, из которого нужно считать структуру.", true), renderMapping();
|
||||
try {
|
||||
const result = await request(`/api/hockey/admin/vmix-mapping/profiles/${state.mappingActiveProfile.id}/inventory`, { method: "POST", body: JSON.stringify({ device_id: deviceId }) });
|
||||
await loadMapping(result.id); state.mappingSelectedInputKey = ""; state.mappingVmixScrollTop = 0; setStatus("Структура vMix обновлена.");
|
||||
await loadMapping(result.id); state.mappingSelectedInputKey = ""; state.mappingVmixScrollTop = 0;
|
||||
const report = result.refresh_report || {};
|
||||
const preserved = Number(report.preserved || 0);
|
||||
const total = Number(report.total_links || preserved);
|
||||
const addedInputs = Number(report.new_inputs || 0);
|
||||
const addedFields = Number(report.new_fields || 0);
|
||||
const unresolved = Number(report.unresolved || 0);
|
||||
setStatus(`Структура vMix обновлена · связи ${preserved}/${total} сохранены${addedInputs ? ` · новых Inputs ${addedInputs}` : ""}${addedFields ? ` · новых полей ${addedFields}` : ""}${unresolved ? ` · требуют проверки ${unresolved}` : ""}.`, unresolved > 0);
|
||||
} catch (error) { setStatus(error.message, true); }
|
||||
renderMapping();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user