feat: 增加商品颜色匹配页面 (#294)

This commit is contained in:
chengma
2026-08-24 00:04:19 +08:00
parent 3cee6cbfaa
commit 8162ae5aca
12 changed files with 697 additions and 1 deletions
+181
View File
@@ -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();
}());