feat: 增加商品颜色匹配页面 (#294)
This commit is contained in:
@@ -682,6 +682,90 @@ input.wide { width: 100%; }
|
||||
.inner-code-toolbar .inner-code-filter-form { flex-basis: 100%; }
|
||||
}
|
||||
|
||||
/* ── 商品级颜色匹配 ─────────────────────
|
||||
独立页面沿用 Admin 的紧凑表格视觉;规则限定在本页,不改变其他主列表。 */
|
||||
.color-mapping-toolbar { flex-wrap: nowrap; }
|
||||
.color-mapping-toolbar h1 {
|
||||
margin: 0 8px 0 0;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.color-mapping-toolbar .color-mapping-change-count {
|
||||
margin-left: auto;
|
||||
color: #667085;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.feedback,
|
||||
.guard-note {
|
||||
margin: 0 0 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #b8d4fa;
|
||||
border-left: 4px solid #1f6feb;
|
||||
border-radius: 4px;
|
||||
color: #174ea6;
|
||||
background: #eef6ff;
|
||||
}
|
||||
.feedback.success { color: #176b36; border-color: #a7d8b7; border-left-color: #176b36; background: #effaf2; }
|
||||
.feedback.error { color: #8f1d14; border-color: #efb2ac; border-left-color: #b42318; background: #fff5f4; }
|
||||
.guard-note { color: #5c4400; border-color: #f0d88b; border-left-color: #c58a00; background: #fffbe6; }
|
||||
.color-mapping-summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 180px;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.color-mapping-summary-card {
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
.color-mapping-summary-card > span,
|
||||
.color-mapping-summary-card > small { display: block; color: #667085; font-size: 12px; }
|
||||
.color-mapping-summary-card > strong {
|
||||
display: block;
|
||||
margin: 4px 0 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.color-mapping-progress { display: grid; place-content: center; text-align: center; }
|
||||
.color-mapping-progress > strong { color: #174ea6; font-size: 22px; font-variant-numeric: tabular-nums; }
|
||||
.color-mapping-table-wrap {
|
||||
min-height: 240px;
|
||||
max-height: calc(100vh - 330px);
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.color-mapping-table { min-width: 1050px; }
|
||||
.color-mapping-table th { position: sticky; top: 0; z-index: 1; }
|
||||
.color-mapping-table td { vertical-align: middle; }
|
||||
.color-mapping-table td:first-child { min-width: 170px; }
|
||||
.color-mapping-table td:nth-child(2) { min-width: 140px; }
|
||||
.color-mapping-table td:nth-child(4) { min-width: 390px; }
|
||||
.color-mapping-table td:nth-child(5) { min-width: 190px; }
|
||||
.color-mapping-key,
|
||||
.color-mapping-source { display: block; margin-top: 4px; color: #667085; font-size: 12px; }
|
||||
.color-mapping-arrow { width: 44px; color: #8a94a1; text-align: center; font-size: 18px; }
|
||||
.color-mapping-select { width: 100%; min-width: 360px; }
|
||||
.color-mapping-target-label { display: block; color: #667085; font-size: 12px; }
|
||||
.color-mapping-target-label .color-mapping-select { display: block; margin-top: 4px; color: #222; font-size: 14px; }
|
||||
.status-mapped { color: #176b36; background: #effaf2; }
|
||||
.status-pending { color: #57606a; background: #f3f4f6; }
|
||||
.status-invalid { color: #b42318; background: #fff1f0; }
|
||||
.status-changed { color: #0969da; background: #eef6ff; }
|
||||
.color-mapping-table tr.row-changed { background: #eef6ff; }
|
||||
.color-mapping-table tr.row-changed:hover { background: #e2f0ff; }
|
||||
.color-mapping-empty-filter { text-align: center; }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.color-mapping-toolbar { flex-wrap: wrap; }
|
||||
.color-mapping-toolbar .color-mapping-change-count { margin-left: 0; }
|
||||
.color-mapping-summary { grid-template-columns: 1fr; }
|
||||
.color-mapping-table-wrap { max-height: none; }
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #ccd1d6;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* 商品级颜色匹配页面的轻量交互。
|
||||
* 业务身份、可购买性和保存校验全部在服务端;这里仅分组候选、收集变更和防重复提交。
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var form = document.querySelector("[data-color-mapping-form]");
|
||||
if (!form) return;
|
||||
|
||||
var selects = Array.prototype.slice.call(document.querySelectorAll("[data-color-mapping-select]"));
|
||||
var saveButton = document.querySelector("[data-color-mapping-save]");
|
||||
var changeCount = document.querySelector("[data-color-mapping-change-count]");
|
||||
var filter = document.querySelector("[data-color-mapping-filter]");
|
||||
var emptyFilter = document.querySelector("[data-color-mapping-empty-filter]");
|
||||
var unavailable = form.getAttribute("data-unavailable") === "1";
|
||||
var submitting = false;
|
||||
|
||||
function candidatesFor(select) {
|
||||
var seen = {};
|
||||
return Array.prototype.slice.call(select.querySelectorAll("option[data-candidate]")).map(function (option) {
|
||||
return { value: option.value, label: option.getAttribute("data-base-label") || option.textContent };
|
||||
}).filter(function (candidate) {
|
||||
if (seen[candidate.value]) return false;
|
||||
seen[candidate.value] = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
selects.forEach(function (select) {
|
||||
select._colorCandidates = candidatesFor(select);
|
||||
});
|
||||
|
||||
function appendGroup(select, label, candidates, selected, occupancy, showOccupants) {
|
||||
if (!candidates.length) return;
|
||||
var group = document.createElement("optgroup");
|
||||
group.label = label;
|
||||
candidates.forEach(function (candidate) {
|
||||
var option = document.createElement("option");
|
||||
option.value = candidate.value;
|
||||
option.textContent = candidate.label;
|
||||
option.setAttribute("data-candidate", "");
|
||||
option.setAttribute("data-base-label", candidate.label);
|
||||
if (showOccupants && occupancy[candidate.value] && occupancy[candidate.value].length) {
|
||||
option.textContent += "(已匹配:" + occupancy[candidate.value].join(" / ") + ")";
|
||||
}
|
||||
option.selected = candidate.value === selected;
|
||||
group.appendChild(option);
|
||||
});
|
||||
select.appendChild(group);
|
||||
}
|
||||
|
||||
function rebuildGroups() {
|
||||
var occupancy = {};
|
||||
selects.forEach(function (select) {
|
||||
if (!select.value) return;
|
||||
occupancy[select.value] = occupancy[select.value] || [];
|
||||
occupancy[select.value].push(select.getAttribute("data-color-raw") || select.getAttribute("data-color-key"));
|
||||
});
|
||||
Object.keys(occupancy).forEach(function (value) { occupancy[value].sort(); });
|
||||
|
||||
selects.forEach(function (select) {
|
||||
var selected = select.value;
|
||||
var candidates = select._colorCandidates || [];
|
||||
var validSelected = candidates.some(function (candidate) { return candidate.value === selected; });
|
||||
var current = candidates.filter(function (candidate) { return candidate.value === selected; });
|
||||
var unused = candidates.filter(function (candidate) { return candidate.value !== selected && !occupancy[candidate.value]; });
|
||||
var used = candidates.filter(function (candidate) { return candidate.value !== selected && occupancy[candidate.value]; });
|
||||
select.textContent = "";
|
||||
var placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "请选择 PDD 颜色";
|
||||
placeholder.selected = selected === "";
|
||||
select.appendChild(placeholder);
|
||||
if (selected && !validSelected) {
|
||||
var stale = document.createElement("option");
|
||||
stale.value = selected;
|
||||
stale.textContent = "原选择已不可用:" + selected;
|
||||
stale.selected = true;
|
||||
stale.disabled = true;
|
||||
select.appendChild(stale);
|
||||
}
|
||||
appendGroup(select, "当前选择", current, selected, occupancy, false);
|
||||
appendGroup(select, "未使用的颜色", unused, selected, occupancy, false);
|
||||
appendGroup(select, "已经匹配(仍可选择)", used, selected, occupancy, true);
|
||||
});
|
||||
}
|
||||
|
||||
function dirtySelects() {
|
||||
return selects.filter(function (select) {
|
||||
return select.value !== (select.getAttribute("data-original-value") || "");
|
||||
});
|
||||
}
|
||||
|
||||
function updateState() {
|
||||
var dirty = dirtySelects();
|
||||
selects.forEach(function (select) {
|
||||
var row = select.closest("[data-color-mapping-row]");
|
||||
var changed = dirty.indexOf(select) !== -1;
|
||||
if (row) row.classList.toggle("row-changed", changed);
|
||||
var status = row && row.querySelector("[data-color-mapping-status]");
|
||||
if (status && changed) {
|
||||
status.textContent = "待保存";
|
||||
status.className = "status-pill status-changed";
|
||||
} else if (status && row) {
|
||||
var saved = row.getAttribute("data-saved-status") || "pending";
|
||||
status.className = "status-pill status-" + saved;
|
||||
status.textContent = saved === "mapped" ? "已匹配" : (saved === "invalid" ? "映射已失效" : "待匹配");
|
||||
}
|
||||
var clear = row && row.querySelector("[data-color-mapping-clear]");
|
||||
if (clear) clear.disabled = unavailable || select.value === "";
|
||||
});
|
||||
if (changeCount) changeCount.textContent = dirty.length ? "已修改 " + dirty.length + " 条,尚未保存" : "尚未修改";
|
||||
if (saveButton) saveButton.disabled = unavailable || submitting || dirty.length === 0;
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
var value = filter ? filter.value : "all";
|
||||
var shown = 0;
|
||||
document.querySelectorAll("[data-color-mapping-row]").forEach(function (row) {
|
||||
var visible = value === "all" || row.getAttribute("data-saved-status") === value;
|
||||
row.hidden = !visible;
|
||||
if (visible) shown += 1;
|
||||
});
|
||||
if (emptyFilter) emptyFilter.hidden = shown !== 0;
|
||||
}
|
||||
|
||||
selects.forEach(function (select) {
|
||||
select.addEventListener("change", function () {
|
||||
rebuildGroups();
|
||||
updateState();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-color-mapping-clear]").forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
var row = button.closest("[data-color-mapping-row]");
|
||||
var select = row && row.querySelector("[data-color-mapping-select]");
|
||||
if (!select) return;
|
||||
select.value = "";
|
||||
rebuildGroups();
|
||||
updateState();
|
||||
select.focus();
|
||||
});
|
||||
});
|
||||
if (filter) filter.addEventListener("change", applyFilter);
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
var dirty = dirtySelects();
|
||||
if (!dirty.length || submitting) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
form.querySelectorAll("[data-color-mapping-generated]").forEach(function (input) { input.remove(); });
|
||||
dirty.forEach(function (select) {
|
||||
[["color_key", select.getAttribute("data-color-key") || ""], ["target_value", select.value]].forEach(function (pair) {
|
||||
var input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = pair[0];
|
||||
input.value = pair[1];
|
||||
input.setAttribute("data-color-mapping-generated", "");
|
||||
form.appendChild(input);
|
||||
});
|
||||
});
|
||||
submitting = true;
|
||||
if (saveButton) {
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = "保存中…";
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", function (event) {
|
||||
if (submitting || dirtySelects().length === 0) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
});
|
||||
|
||||
rebuildGroups();
|
||||
updateState();
|
||||
var alertBox = document.querySelector("[data-color-mapping-alert]");
|
||||
if (alertBox) alertBox.focus();
|
||||
}());
|
||||
Reference in New Issue
Block a user