2026-08-04 18:24:39 +08:00
|
|
|
(() => {
|
|
|
|
|
"use strict";
|
|
|
|
|
const form = document.querySelector("[data-start-purchases]");
|
|
|
|
|
if (!form) return;
|
|
|
|
|
const all = form.querySelector("[data-select-all]");
|
|
|
|
|
const summary = form.querySelector("[data-selection-summary]");
|
|
|
|
|
const button = form.querySelector("[data-start-button]");
|
|
|
|
|
const feedback = form.querySelector("[data-start-feedback]");
|
|
|
|
|
const boxes = () => [...form.querySelectorAll("input[data-task-id]")];
|
|
|
|
|
let selectionFrozen = false;
|
|
|
|
|
const parseCents = (value) => {
|
|
|
|
|
const match = /^(0|[1-9]\d*)\.(\d{2})$/.exec(value);
|
|
|
|
|
return match ? BigInt(match[1] + match[2]) : null;
|
|
|
|
|
};
|
|
|
|
|
const refresh = () => {
|
|
|
|
|
const available = boxes();
|
|
|
|
|
const selected = available.filter((box) => box.checked);
|
|
|
|
|
let cents = 0n;
|
|
|
|
|
let pricesValid = true;
|
|
|
|
|
selected.forEach((box) => {
|
|
|
|
|
const price = parseCents(box.dataset.price);
|
|
|
|
|
if (price === null) pricesValid = false;
|
|
|
|
|
else cents += price;
|
|
|
|
|
});
|
|
|
|
|
summary.textContent = `已选 ${selected.length} 条,最高总额 ¥${cents / 100n}.${(cents % 100n).toString().padStart(2, "0")}`;
|
|
|
|
|
button.disabled = !selected.length || !pricesValid;
|
|
|
|
|
if (!pricesValid) feedback.textContent = "所选任务金额无法安全汇总,请刷新后重选。";
|
|
|
|
|
if (all) {
|
|
|
|
|
all.checked = selected.length > 0 && selected.length === available.length;
|
|
|
|
|
all.indeterminate = selected.length > 0 && selected.length < available.length;
|
|
|
|
|
all.disabled = selectionFrozen || available.length === 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const freezeSelection = (frozen) => {
|
|
|
|
|
selectionFrozen = frozen;
|
|
|
|
|
boxes().forEach((box) => { box.disabled = frozen; });
|
|
|
|
|
refresh();
|
|
|
|
|
};
|
|
|
|
|
boxes().forEach((box) => box.addEventListener("change", refresh));
|
|
|
|
|
if (all) all.addEventListener("change", () => { boxes().forEach((box) => { box.checked = all.checked; }); refresh(); });
|
|
|
|
|
let frozenPayload = null;
|
|
|
|
|
let inFlight = false;
|
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const selected = boxes().filter((box) => box.checked);
|
|
|
|
|
if (!selected.length || inFlight) return;
|
|
|
|
|
const tasks = selected.map((box) => ({task_id: box.dataset.taskId, expected_task_version: Number(box.dataset.taskVersion)}));
|
|
|
|
|
if (tasks.some((item) => !Number.isSafeInteger(item.expected_task_version) || item.expected_task_version < 1)) { feedback.textContent = "任务版本无效,请刷新后重选。"; return; }
|
|
|
|
|
frozenPayload = frozenPayload || JSON.stringify({start_key: form.dataset.startKey, tasks});
|
|
|
|
|
inFlight = true; freezeSelection(true); button.disabled = true; button.textContent = "正在授权…";
|
|
|
|
|
try { const response = await fetch("/tasks/start-purchases", {method:"POST", headers:{"Content-Type":"application/json", "X-CSRF-Token":form.dataset.csrf}, body:frozenPayload});
|
|
|
|
|
if (response.ok) { window.location.reload(); return; }
|
|
|
|
|
if (response.status === 409) { feedback.textContent = "任务已变化,请刷新后重选。"; frozenPayload = null; freezeSelection(false); boxes().forEach((box) => { box.checked = false; }); refresh(); return; }
|
|
|
|
|
if (response.status === 400 || response.status === 401 || response.status === 403) { feedback.textContent = "请求未被接受,请刷新页面后重试。"; frozenPayload = null; freezeSelection(false); return; }
|
|
|
|
|
feedback.textContent = "结果暂时不明确,只能使用同一按钮原样重放。";
|
|
|
|
|
} catch (_) { feedback.textContent = "网络结果不明确,请使用同一按钮原样重试。"; }
|
|
|
|
|
finally { inFlight = false; button.textContent = "开始采购(只创建待付款订单)"; if (frozenPayload) button.disabled = false; }
|
|
|
|
|
});
|
|
|
|
|
refresh();
|
|
|
|
|
})();
|
2026-08-04 19:42:35 +08:00
|
|
|
|
|
|
|
|
(() => {
|
|
|
|
|
"use strict";
|
|
|
|
|
const drawer = document.querySelector("[data-detail-drawer]");
|
|
|
|
|
if (!drawer) return;
|
|
|
|
|
const body = drawer.querySelector("[data-detail-body]");
|
|
|
|
|
const closeButton = drawer.querySelector("[data-close-detail]");
|
|
|
|
|
const rows = [...document.querySelectorAll("[data-task-row]")];
|
|
|
|
|
const initialURL = window.location.pathname + window.location.search;
|
|
|
|
|
let focusTrigger = null;
|
|
|
|
|
let scrollPosition = window.scrollY;
|
|
|
|
|
let activeRequest = null;
|
|
|
|
|
|
|
|
|
|
const isInteractive = (target) => Boolean(target && typeof target.closest === "function" && target.closest("a,button,input,select,textarea,label,[contenteditable=true]"));
|
|
|
|
|
const showDrawer = () => {
|
|
|
|
|
if (!drawer.open) drawer.showModal();
|
|
|
|
|
};
|
|
|
|
|
const restoreList = () => {
|
|
|
|
|
if (activeRequest) {
|
|
|
|
|
activeRequest.abort();
|
|
|
|
|
activeRequest = null;
|
|
|
|
|
}
|
|
|
|
|
if (drawer.open) drawer.close();
|
|
|
|
|
window.scrollTo({top: scrollPosition, behavior: "auto"});
|
|
|
|
|
if (focusTrigger && document.contains(focusTrigger)) focusTrigger.focus({preventScroll: true});
|
|
|
|
|
};
|
|
|
|
|
const showError = (url, row, requestedFocus, pushHistory) => {
|
|
|
|
|
body.replaceChildren();
|
|
|
|
|
const message = document.createElement("p");
|
|
|
|
|
message.className = "drawer-feedback";
|
|
|
|
|
message.setAttribute("role", "alert");
|
|
|
|
|
message.textContent = "任务详情加载失败。请重试,或在完整页打开。";
|
|
|
|
|
const actions = document.createElement("p");
|
|
|
|
|
const retry = document.createElement("button");
|
|
|
|
|
retry.className = "button primary";
|
|
|
|
|
retry.type = "button";
|
|
|
|
|
retry.textContent = "重试";
|
|
|
|
|
retry.addEventListener("click", () => loadDetail(url, row, requestedFocus, pushHistory));
|
|
|
|
|
const fallback = document.createElement("a");
|
|
|
|
|
fallback.className = "button";
|
|
|
|
|
fallback.href = url;
|
|
|
|
|
fallback.textContent = "在完整页打开";
|
|
|
|
|
actions.className = "actions";
|
|
|
|
|
actions.append(retry, fallback);
|
|
|
|
|
body.append(message, actions);
|
|
|
|
|
};
|
|
|
|
|
const loadDetail = async (url, row, requestedFocus, pushHistory) => {
|
|
|
|
|
if (activeRequest) activeRequest.abort();
|
|
|
|
|
const requestController = new AbortController();
|
|
|
|
|
activeRequest = requestController;
|
|
|
|
|
focusTrigger = requestedFocus || focusTrigger;
|
|
|
|
|
if (pushHistory) scrollPosition = window.scrollY;
|
|
|
|
|
body.innerHTML = '<p class="drawer-feedback" role="status">正在加载任务详情…</p>';
|
|
|
|
|
showDrawer();
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(url, {headers: {"X-CMBuyer-View": "drawer", "Accept": "text/html"}, credentials: "same-origin", signal: requestController.signal});
|
|
|
|
|
if (!response.ok || !String(response.headers.get("Content-Type") || "").toLowerCase().startsWith("text/html")) throw new Error("detail request rejected");
|
|
|
|
|
const fragment = await response.text();
|
|
|
|
|
if (!fragment.includes("data-task-detail-content")) throw new Error("detail fragment missing");
|
|
|
|
|
body.innerHTML = fragment;
|
|
|
|
|
if (pushHistory) window.history.pushState({cmbuyerDrawer: true, detailURL: url, focusTarget: requestedFocus === row ? "row" : "button"}, "", url);
|
|
|
|
|
closeButton.focus();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.name !== "AbortError") showError(url, row, requestedFocus, pushHistory);
|
|
|
|
|
} finally {
|
|
|
|
|
if (activeRequest === requestController) activeRequest = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const requestClose = () => {
|
|
|
|
|
if (window.history.state && window.history.state.cmbuyerDrawer) window.history.back();
|
|
|
|
|
else restoreList();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.history.replaceState({cmbuyerList: true, listURL: initialURL}, "", initialURL);
|
|
|
|
|
rows.forEach((row) => {
|
|
|
|
|
const url = row.dataset.detailUrl;
|
|
|
|
|
row.addEventListener("dblclick", (event) => {
|
|
|
|
|
if (!isInteractive(event.target)) loadDetail(url, row, row, true);
|
|
|
|
|
});
|
|
|
|
|
row.addEventListener("keydown", (event) => {
|
|
|
|
|
if (event.key === "Enter" && event.target === row) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
loadDetail(url, row, row, true);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const button = row.querySelector("[data-open-detail]");
|
|
|
|
|
if (button) button.addEventListener("click", () => loadDetail(url, row, button, true));
|
|
|
|
|
});
|
|
|
|
|
closeButton.addEventListener("click", requestClose);
|
|
|
|
|
drawer.addEventListener("cancel", (event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
requestClose();
|
|
|
|
|
});
|
|
|
|
|
window.addEventListener("popstate", (event) => {
|
|
|
|
|
if (event.state && event.state.cmbuyerDrawer) {
|
|
|
|
|
const row = rows.find((candidate) => candidate.dataset.detailUrl === event.state.detailURL);
|
|
|
|
|
if (!row) {
|
|
|
|
|
restoreList();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const requestedFocus = event.state.focusTarget === "button" ? row.querySelector("[data-open-detail]") || row : row;
|
|
|
|
|
loadDetail(event.state.detailURL, row, requestedFocus, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
restoreList();
|
|
|
|
|
});
|
|
|
|
|
})();
|