diff --git a/src/content/market/index.ts b/src/content/market/index.ts index 73a108f..c6034ac 100644 --- a/src/content/market/index.ts +++ b/src/content/market/index.ts @@ -1,4 +1,5 @@ import { buildMarketCsv } from "./csv-exporter"; +import { createBatchPayload, type BatchPayload } from "./batch-payload"; import { applyRowOrder, applyRowVisibility, @@ -16,6 +17,11 @@ import { setToolbarExportStatus } from "./plugin-toolbar"; import { createMarketResultStore } from "./result-store"; +import { + isAuthResponseMessage, + type AuthStateValue +} from "../../shared/auth-messages"; +import { createBatchSubmitClient } from "../../shared/batch-submit-client"; import type { MarketApiResult, MarketFilterState, @@ -33,24 +39,38 @@ interface MutationObserverLike { export interface CreateMarketControllerOptions { buildCsv?: (records: MarketRecord[]) => string; document: Document; + getAuthState?: () => Promise; loadAuthorMetrics?: (authorId: string) => Promise; mutationObserverFactory?: ( callback: MutationCallback ) => MutationObserverLike; onCsvReady?: (csv: string) => void; + promptBatchName?: () => string | null; resultStore?: ReturnType; + submitBatch?: (payload: BatchPayload) => Promise; window: Window; } export function createMarketController(options: CreateMarketControllerOptions) { const marketApiClient = createMarketApiClient(); + const sendRuntimeMessage = createRuntimeMessageSender(); const resultStore = options.resultStore ?? createMarketResultStore(); const loadAuthorMetrics = options.loadAuthorMetrics ?? marketApiClient.loadAuthorAseInfo; const buildCsv = options.buildCsv ?? buildMarketCsv; + const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage)); const mutationObserverFactory = options.mutationObserverFactory ?? ((callback: MutationCallback) => new MutationObserver(callback)); + const promptBatchName = + options.promptBatchName ?? + (() => options.window.prompt("请输入批次名称")); + const submitBatch = + options.submitBatch ?? + createBatchSubmitClient({ + baseUrl: "http://127.0.0.1:4319", + sendMessage: sendRuntimeMessage + }).submitBatch; const exportRangeController = createExportRangeController({ document: options.document, onProgress: ({ currentPage, totalPages }) => { @@ -119,7 +139,46 @@ export function createMarketController(options: CreateMarketControllerOptions) { } }, onSubmitBatch: async () => { - setToolbarExportStatus(toolbar, "批次提交功能开发中"); + const exportTarget = readToolbarExportTarget(toolbar); + if (!exportTarget.target) { + setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效"); + return; + } + + const batchName = promptBatchName(); + if (batchName === null) { + return; + } + + if (!batchName.trim()) { + setToolbarExportStatus(toolbar, "请输入批次名称"); + return; + } + + setToolbarBusyState(toolbar, true); + try { + const records = await exportRecords(exportTarget.target, "提交中"); + const authState = await getAuthState(); + if (!authState.isAuthenticated) { + throw new Error("请先登录插件"); + } + + const payload = createBatchPayload({ + authState, + batchName, + createdAt: new Date().toISOString(), + records + }); + await submitBatch(payload); + setToolbarExportStatus(toolbar, "批次提交成功"); + } catch (error) { + setToolbarExportStatus( + toolbar, + error instanceof Error ? error.message : "批次提交失败,请稍后重试" + ); + } finally { + setToolbarBusyState(toolbar, false); + } } }); @@ -228,9 +287,12 @@ export function createMarketController(options: CreateMarketControllerOptions) { }); } - async function exportRecords(target: MarketExportTarget): Promise { + async function exportRecords( + target: MarketExportTarget, + inProgressLabel = "导出中" + ): Promise { if (target.mode === "count" && target.pageCount <= 1) { - setToolbarExportStatus(toolbar, "导出中..."); + setToolbarExportStatus(toolbar, `${inProgressLabel}...`); await prepareCurrentPageForExport(); return getVisibleOrderedRecords(); } @@ -556,6 +618,32 @@ function mergeFieldMap>( return merged as T; } +function createRuntimeMessageSender(): (message: unknown) => Promise { + return (message: unknown) => + Promise.resolve( + ( + globalThis as typeof globalThis & { + chrome?: { + runtime?: { + sendMessage?: (payload: unknown) => Promise; + }; + }; + } + ).chrome?.runtime?.sendMessage?.(message) + ); +} + +async function readAuthState( + sendMessage: (message: unknown) => Promise +): Promise { + const response = await sendMessage({ type: "auth:get-state" }); + if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") { + throw new Error("请先登录插件"); + } + + return response.value; +} + function mergeStringValue( current: string | undefined, incoming: string | undefined diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index 17d626e..037f1d4 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -837,6 +837,107 @@ describe("market-content-entry", () => { ).toContain("有效页数"); }); + test("prompts for a batch name before submitting the current range", async () => { + document.body.innerHTML = buildMarketFixture(); + const promptBatchName = vi.fn(() => "618达人筛选第一批"); + 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" + }), + promptBatchName, + submitBatch, + window + })); + + await controller.ready; + setSelectValue('[data-plugin-export-range="select"]', "current"); + dispatchChange('[data-plugin-export-range="select"]'); + + click('[data-plugin-batch-submit="button"]'); + await waitForMockCall(submitBatch, 40, 50); + + expect(promptBatchName).toHaveBeenCalledTimes(1); + expect(submitBatch).toHaveBeenCalledWith( + expect.objectContaining({ + batchId: expect.stringContaining("618达人筛选第一批-"), + batchName: "618达人筛选第一批", + logtoUserId: "p7pdhhtde8kj" + }) + ); + }); + + test("shows an error when the batch name is blank", async () => { + document.body.innerHTML = buildMarketFixture(); + const promptBatchName = vi.fn(() => " "); + 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" + }), + promptBatchName, + submitBatch, + window + })); + + await controller.ready; + click('[data-plugin-batch-submit="button"]'); + await flush(); + + expect(submitBatch).not.toHaveBeenCalled(); + expect( + document.querySelector('[data-plugin-export-status="text"]')?.textContent + ).toContain("请输入批次名称"); + }); + + test("does nothing when the prompt is cancelled", async () => { + document.body.innerHTML = buildMarketFixture(); + const promptBatchName = vi.fn(() => null); + 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" + }), + promptBatchName, + submitBatch, + window + })); + + await controller.ready; + click('[data-plugin-batch-submit="button"]'); + await flush(); + + expect(promptBatchName).toHaveBeenCalledTimes(1); + expect(submitBatch).not.toHaveBeenCalled(); + }); + test("export only includes records that are present on the current page", async () => { document.body.innerHTML = buildMarketFixture(); const resultStore = createMarketResultStore();