Replace batch prompt with custom dialog
This commit is contained in:
parent
37e29bd6b8
commit
376c7b510e
245
src/content/market/batch-name-dialog.ts
Normal file
245
src/content/market/batch-name-dialog.ts
Normal file
@ -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;
|
||||
}
|
||||
|
||||
@ -1728,6 +1728,114 @@ describe("market-content-entry", () => {
|
||||
expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
|
||||
});
|
||||
|
||||
test("opens a custom batch name dialog before submitting", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
document,
|
||||
getAuthState: async () => ({
|
||||
isAuthenticated: true,
|
||||
resource: "https://talent-search.intelligrow.cn",
|
||||
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||
}),
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
submitBatch,
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
|
||||
expect(submitBatch).not.toHaveBeenCalled();
|
||||
expect(
|
||||
document.querySelector('[data-plugin-batch-name-dialog="root"]')
|
||||
).not.toBeNull();
|
||||
|
||||
setInputValue('[data-plugin-batch-name-input="input"]', "618达人筛选第一批");
|
||||
dispatchInput('[data-plugin-batch-name-input="input"]');
|
||||
click('[data-plugin-batch-name-confirm="button"]');
|
||||
await waitForMockCall(submitBatch, 40, 50);
|
||||
|
||||
expect(submitBatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
batchName: "618达人筛选第一批"
|
||||
})
|
||||
);
|
||||
expect(
|
||||
document.querySelector('[data-plugin-batch-name-dialog="root"]')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("keeps the custom batch name dialog open and shows an inline error for blank values", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
document,
|
||||
getAuthState: async () => ({
|
||||
isAuthenticated: true,
|
||||
resource: "https://talent-search.intelligrow.cn",
|
||||
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||
}),
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
submitBatch,
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
click('[data-plugin-batch-name-confirm="button"]');
|
||||
await flush();
|
||||
|
||||
expect(submitBatch).not.toHaveBeenCalled();
|
||||
expect(
|
||||
document.querySelector('[data-plugin-batch-name-error="text"]')?.textContent
|
||||
).toContain("请输入批次名称");
|
||||
expect(
|
||||
document.querySelector('[data-plugin-batch-name-dialog="root"]')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
test("closes the custom batch name dialog when cancelled", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
document,
|
||||
getAuthState: async () => ({
|
||||
isAuthenticated: true,
|
||||
resource: "https://talent-search.intelligrow.cn",
|
||||
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||
}),
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
submitBatch,
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
click('[data-plugin-batch-name-cancel="button"]');
|
||||
await flush();
|
||||
|
||||
expect(submitBatch).not.toHaveBeenCalled();
|
||||
expect(
|
||||
document.querySelector('[data-plugin-batch-name-dialog="root"]')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("selected batch submit uses only creators selected in the current range", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||
@ -3873,6 +3981,15 @@ function setInputValue(selector: string, value: string) {
|
||||
element.value = value;
|
||||
}
|
||||
|
||||
function dispatchInput(selector: string) {
|
||||
const element = document.querySelector(selector) as HTMLElement | null;
|
||||
if (!element) {
|
||||
throw new Error(`Missing element: ${selector}`);
|
||||
}
|
||||
|
||||
element.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
function setSelectValue(selector: string, value: string) {
|
||||
const element = document.querySelector(selector) as HTMLSelectElement | null;
|
||||
if (!element) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user