Replace batch prompt with custom dialog
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
const DIALOG_STYLE_ID = "sces-batch-name-dialog-style";
|
||||
|
||||
const activeDialogs = new WeakMap<
|
||||
Document,
|
||||
{
|
||||
input: HTMLInputElement;
|
||||
promise: Promise<string | null>;
|
||||
}
|
||||
>();
|
||||
|
||||
export function promptForBatchName(document: Document): Promise<string | null> {
|
||||
const existingDialog = activeDialogs.get(document);
|
||||
if (existingDialog) {
|
||||
existingDialog.input.focus();
|
||||
existingDialog.input.select();
|
||||
return existingDialog.promise;
|
||||
}
|
||||
|
||||
ensureDialogStyles(document);
|
||||
|
||||
const dialogRoot = document.createElement("div");
|
||||
dialogRoot.dataset.pluginBatchNameDialog = "root";
|
||||
dialogRoot.setAttribute("role", "dialog");
|
||||
dialogRoot.setAttribute("aria-modal", "true");
|
||||
dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title");
|
||||
applyOverlayStyles(dialogRoot);
|
||||
|
||||
const dialogPanel = document.createElement("div");
|
||||
applyPanelStyles(dialogPanel);
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.id = "sces-batch-name-title";
|
||||
title.textContent = "提交批次";
|
||||
applyTitleStyles(title);
|
||||
|
||||
const description = document.createElement("p");
|
||||
description.textContent = "请输入批次名称,便于后续在系统中识别和追踪。";
|
||||
applyDescriptionStyles(description);
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.dataset.pluginBatchNameInput = "input";
|
||||
input.placeholder = "例如:618达人筛选第一批";
|
||||
input.maxLength = 60;
|
||||
applyInputStyles(input);
|
||||
|
||||
const errorText = document.createElement("p");
|
||||
errorText.dataset.pluginBatchNameError = "text";
|
||||
applyErrorStyles(errorText);
|
||||
|
||||
const buttonRow = document.createElement("div");
|
||||
applyButtonRowStyles(buttonRow);
|
||||
|
||||
const cancelButton = document.createElement("button");
|
||||
cancelButton.type = "button";
|
||||
cancelButton.dataset.pluginBatchNameCancel = "button";
|
||||
cancelButton.textContent = "取消";
|
||||
applySecondaryButtonStyles(cancelButton);
|
||||
|
||||
const confirmButton = document.createElement("button");
|
||||
confirmButton.type = "button";
|
||||
confirmButton.dataset.pluginBatchNameConfirm = "button";
|
||||
confirmButton.textContent = "确认提交";
|
||||
applyPrimaryButtonStyles(confirmButton);
|
||||
|
||||
buttonRow.append(cancelButton, confirmButton);
|
||||
dialogPanel.append(title, description, input, errorText, buttonRow);
|
||||
dialogRoot.appendChild(dialogPanel);
|
||||
document.body.appendChild(dialogRoot);
|
||||
|
||||
const dialogPromise = new Promise<string | null>((resolve) => {
|
||||
const closeDialog = (value: string | null) => {
|
||||
activeDialogs.delete(document);
|
||||
dialogRoot.remove();
|
||||
document.removeEventListener("keydown", handleDocumentKeydown, true);
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const submitValue = () => {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
errorText.textContent = "请输入批次名称";
|
||||
input.setAttribute("aria-invalid", "true");
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog(value);
|
||||
};
|
||||
|
||||
const handleDocumentKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeDialog(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
submitValue();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
if (!input.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
errorText.textContent = "";
|
||||
input.removeAttribute("aria-invalid");
|
||||
});
|
||||
cancelButton.addEventListener("click", () => {
|
||||
closeDialog(null);
|
||||
});
|
||||
confirmButton.addEventListener("click", () => {
|
||||
submitValue();
|
||||
});
|
||||
dialogRoot.addEventListener("click", (event) => {
|
||||
if (event.target === dialogRoot) {
|
||||
closeDialog(null);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", handleDocumentKeydown, true);
|
||||
});
|
||||
|
||||
activeDialogs.set(document, {
|
||||
input,
|
||||
promise: dialogPromise
|
||||
});
|
||||
|
||||
input.focus();
|
||||
|
||||
return dialogPromise;
|
||||
}
|
||||
|
||||
function ensureDialogStyles(document: Document): void {
|
||||
if (document.getElementById(DIALOG_STYLE_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = DIALOG_STYLE_ID;
|
||||
style.textContent = `
|
||||
[data-plugin-batch-name-dialog="root"] {
|
||||
animation: sces-batch-name-fade-in 0.16s ease;
|
||||
}
|
||||
|
||||
@keyframes sces-batch-name-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function applyOverlayStyles(root: HTMLElement): void {
|
||||
root.style.position = "fixed";
|
||||
root.style.inset = "0";
|
||||
root.style.background = "rgba(15, 23, 42, 0.38)";
|
||||
root.style.display = "flex";
|
||||
root.style.alignItems = "center";
|
||||
root.style.justifyContent = "center";
|
||||
root.style.padding = "24px";
|
||||
root.style.zIndex = "2147483647";
|
||||
}
|
||||
|
||||
function applyPanelStyles(panel: HTMLElement): void {
|
||||
panel.style.width = "min(420px, calc(100vw - 32px))";
|
||||
panel.style.background = "#fffaf9";
|
||||
panel.style.border = "1px solid rgba(127, 29, 45, 0.14)";
|
||||
panel.style.borderRadius = "18px";
|
||||
panel.style.boxShadow = "0 28px 70px rgba(15, 23, 42, 0.22)";
|
||||
panel.style.padding = "24px";
|
||||
panel.style.boxSizing = "border-box";
|
||||
}
|
||||
|
||||
function applyTitleStyles(title: HTMLElement): void {
|
||||
title.style.margin = "0";
|
||||
title.style.color = "#4c0519";
|
||||
title.style.fontSize = "20px";
|
||||
title.style.fontWeight = "700";
|
||||
title.style.lineHeight = "28px";
|
||||
}
|
||||
|
||||
function applyDescriptionStyles(description: HTMLElement): void {
|
||||
description.style.margin = "10px 0 0";
|
||||
description.style.color = "#64748b";
|
||||
description.style.fontSize = "13px";
|
||||
description.style.lineHeight = "20px";
|
||||
}
|
||||
|
||||
function applyInputStyles(input: HTMLInputElement): void {
|
||||
input.style.width = "100%";
|
||||
input.style.height = "42px";
|
||||
input.style.marginTop = "18px";
|
||||
input.style.padding = "0 14px";
|
||||
input.style.boxSizing = "border-box";
|
||||
input.style.border = "1px solid #d8c1c6";
|
||||
input.style.borderRadius = "12px";
|
||||
input.style.background = "#ffffff";
|
||||
input.style.color = "#1f2937";
|
||||
input.style.fontSize = "14px";
|
||||
input.style.outline = "none";
|
||||
}
|
||||
|
||||
function applyErrorStyles(errorText: HTMLElement): void {
|
||||
errorText.style.minHeight = "20px";
|
||||
errorText.style.margin = "8px 0 0";
|
||||
errorText.style.color = "#b91c1c";
|
||||
errorText.style.fontSize = "12px";
|
||||
errorText.style.lineHeight = "18px";
|
||||
}
|
||||
|
||||
function applyButtonRowStyles(buttonRow: HTMLElement): void {
|
||||
buttonRow.style.display = "flex";
|
||||
buttonRow.style.justifyContent = "flex-end";
|
||||
buttonRow.style.gap = "10px";
|
||||
buttonRow.style.marginTop = "18px";
|
||||
}
|
||||
|
||||
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
|
||||
button.style.height = "36px";
|
||||
button.style.padding = "0 16px";
|
||||
button.style.border = "1px solid #d7dde6";
|
||||
button.style.borderRadius = "10px";
|
||||
button.style.background = "#ffffff";
|
||||
button.style.color = "#334155";
|
||||
button.style.fontWeight = "600";
|
||||
button.style.cursor = "pointer";
|
||||
}
|
||||
|
||||
function applyPrimaryButtonStyles(button: HTMLButtonElement): void {
|
||||
button.style.height = "36px";
|
||||
button.style.padding = "0 16px";
|
||||
button.style.border = "1px solid #7f1d2d";
|
||||
button.style.borderRadius = "10px";
|
||||
button.style.background = "#7f1d2d";
|
||||
button.style.color = "#ffffff";
|
||||
button.style.fontWeight = "600";
|
||||
button.style.cursor = "pointer";
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { buildMarketCsv } from "./csv-exporter";
|
||||
import { promptForBatchName } from "./batch-name-dialog";
|
||||
import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
||||
import {
|
||||
applyRowOrder,
|
||||
@@ -52,7 +53,7 @@ export interface CreateMarketControllerOptions {
|
||||
callback: MutationCallback
|
||||
) => MutationObserverLike;
|
||||
onCsvReady?: (csv: string) => void;
|
||||
promptBatchName?: () => string | null;
|
||||
promptBatchName?: () => Promise<string | null> | string | null;
|
||||
resultStore?: ReturnType<typeof createMarketResultStore>;
|
||||
submitBatch?: (payload: BatchPayload) => Promise<unknown>;
|
||||
window: Window;
|
||||
@@ -74,7 +75,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
((callback: MutationCallback) => new MutationObserver(callback));
|
||||
const promptBatchName =
|
||||
options.promptBatchName ??
|
||||
(() => options.window.prompt("请输入批次名称"));
|
||||
(() => promptForBatchName(options.document));
|
||||
const submitBatch =
|
||||
options.submitBatch ??
|
||||
((payload: BatchPayload) =>
|
||||
@@ -169,7 +170,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batchName = promptBatchName();
|
||||
const batchName = await promptBatchName();
|
||||
if (batchName === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user