feat: wire market batch submission flow

This commit is contained in:
admin123 2026-04-22 13:57:24 +08:00
parent 3c672f8355
commit 766c6a624f
2 changed files with 192 additions and 3 deletions

View File

@ -1,4 +1,5 @@
import { buildMarketCsv } from "./csv-exporter"; import { buildMarketCsv } from "./csv-exporter";
import { createBatchPayload, type BatchPayload } from "./batch-payload";
import { import {
applyRowOrder, applyRowOrder,
applyRowVisibility, applyRowVisibility,
@ -16,6 +17,11 @@ import {
setToolbarExportStatus setToolbarExportStatus
} from "./plugin-toolbar"; } from "./plugin-toolbar";
import { createMarketResultStore } from "./result-store"; import { createMarketResultStore } from "./result-store";
import {
isAuthResponseMessage,
type AuthStateValue
} from "../../shared/auth-messages";
import { createBatchSubmitClient } from "../../shared/batch-submit-client";
import type { import type {
MarketApiResult, MarketApiResult,
MarketFilterState, MarketFilterState,
@ -33,24 +39,38 @@ interface MutationObserverLike {
export interface CreateMarketControllerOptions { export interface CreateMarketControllerOptions {
buildCsv?: (records: MarketRecord[]) => string; buildCsv?: (records: MarketRecord[]) => string;
document: Document; document: Document;
getAuthState?: () => Promise<AuthStateValue>;
loadAuthorMetrics?: (authorId: string) => Promise<MarketApiResult>; loadAuthorMetrics?: (authorId: string) => Promise<MarketApiResult>;
mutationObserverFactory?: ( mutationObserverFactory?: (
callback: MutationCallback callback: MutationCallback
) => MutationObserverLike; ) => MutationObserverLike;
onCsvReady?: (csv: string) => void; onCsvReady?: (csv: string) => void;
promptBatchName?: () => string | null;
resultStore?: ReturnType<typeof createMarketResultStore>; resultStore?: ReturnType<typeof createMarketResultStore>;
submitBatch?: (payload: BatchPayload) => Promise<unknown>;
window: Window; window: Window;
} }
export function createMarketController(options: CreateMarketControllerOptions) { export function createMarketController(options: CreateMarketControllerOptions) {
const marketApiClient = createMarketApiClient(); const marketApiClient = createMarketApiClient();
const sendRuntimeMessage = createRuntimeMessageSender();
const resultStore = options.resultStore ?? createMarketResultStore(); const resultStore = options.resultStore ?? createMarketResultStore();
const loadAuthorMetrics = const loadAuthorMetrics =
options.loadAuthorMetrics ?? marketApiClient.loadAuthorAseInfo; options.loadAuthorMetrics ?? marketApiClient.loadAuthorAseInfo;
const buildCsv = options.buildCsv ?? buildMarketCsv; const buildCsv = options.buildCsv ?? buildMarketCsv;
const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage));
const mutationObserverFactory = const mutationObserverFactory =
options.mutationObserverFactory ?? options.mutationObserverFactory ??
((callback: MutationCallback) => new MutationObserver(callback)); ((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({ const exportRangeController = createExportRangeController({
document: options.document, document: options.document,
onProgress: ({ currentPage, totalPages }) => { onProgress: ({ currentPage, totalPages }) => {
@ -119,7 +139,46 @@ export function createMarketController(options: CreateMarketControllerOptions) {
} }
}, },
onSubmitBatch: async () => { 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<MarketRecord[]> { async function exportRecords(
target: MarketExportTarget,
inProgressLabel = "导出中"
): Promise<MarketRecord[]> {
if (target.mode === "count" && target.pageCount <= 1) { if (target.mode === "count" && target.pageCount <= 1) {
setToolbarExportStatus(toolbar, "导出中..."); setToolbarExportStatus(toolbar, `${inProgressLabel}...`);
await prepareCurrentPageForExport(); await prepareCurrentPageForExport();
return getVisibleOrderedRecords(); return getVisibleOrderedRecords();
} }
@ -556,6 +618,32 @@ function mergeFieldMap<T extends Record<string, string | undefined>>(
return merged as T; return merged as T;
} }
function createRuntimeMessageSender(): (message: unknown) => Promise<unknown> {
return (message: unknown) =>
Promise.resolve(
(
globalThis as typeof globalThis & {
chrome?: {
runtime?: {
sendMessage?: (payload: unknown) => Promise<unknown>;
};
};
}
).chrome?.runtime?.sendMessage?.(message)
);
}
async function readAuthState(
sendMessage: (message: unknown) => Promise<unknown>
): Promise<AuthStateValue> {
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( function mergeStringValue(
current: string | undefined, current: string | undefined,
incoming: string | undefined incoming: string | undefined

View File

@ -837,6 +837,107 @@ describe("market-content-entry", () => {
).toContain("有效页数"); ).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 () => { test("export only includes records that are present on the current page", async () => {
document.body.innerHTML = buildMarketFixture(); document.body.innerHTML = buildMarketFixture();
const resultStore = createMarketResultStore(); const resultStore = createMarketResultStore();