From 376c7b510e7413dfc43c9855063b823098f45146 Mon Sep 17 00:00:00 2001 From: admin123 Date: Wed, 29 Apr 2026 17:23:04 +0800 Subject: [PATCH] Replace batch prompt with custom dialog --- src/content/market/batch-name-dialog.ts | 245 ++++++++++++++++++++++++ src/content/market/index.ts | 7 +- tests/market-content-entry.test.ts | 117 +++++++++++ 3 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 src/content/market/batch-name-dialog.ts diff --git a/src/content/market/batch-name-dialog.ts b/src/content/market/batch-name-dialog.ts new file mode 100644 index 0000000..54a23e2 --- /dev/null +++ b/src/content/market/batch-name-dialog.ts @@ -0,0 +1,245 @@ +const DIALOG_STYLE_ID = "sces-batch-name-dialog-style"; + +const activeDialogs = new WeakMap< + Document, + { + input: HTMLInputElement; + promise: Promise; + } +>(); + +export function promptForBatchName(document: Document): Promise { + 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((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"; +} diff --git a/src/content/market/index.ts b/src/content/market/index.ts index ae6d8ce..6848e42 100644 --- a/src/content/market/index.ts +++ b/src/content/market/index.ts @@ -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; resultStore?: ReturnType; submitBatch?: (payload: BatchPayload) => Promise; 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; } diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index b4278c2..a599251 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -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) {