feat: add batch submit toolbar action
This commit is contained in:
parent
b75755e6a6
commit
3c672f8355
@ -117,6 +117,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
} finally {
|
||||
setToolbarBusyState(toolbar, false);
|
||||
}
|
||||
},
|
||||
onSubmitBatch: async () => {
|
||||
setToolbarExportStatus(toolbar, "批次提交功能开发中");
|
||||
}
|
||||
});
|
||||
|
||||
@ -366,56 +369,81 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
let previousFingerprint = "";
|
||||
let stablePassCount = 0;
|
||||
|
||||
for (let attempt = 0; attempt < 12; attempt += 1) {
|
||||
for (let attempt = 0; attempt < 9; attempt += 1) {
|
||||
await waitForDomSettled();
|
||||
if (attempt > 0) {
|
||||
await new Promise<void>((resolve) => {
|
||||
options.window.setTimeout(resolve, 100);
|
||||
options.window.setTimeout(
|
||||
resolve,
|
||||
previousFingerprint.includes("|missing:0") ? 25 : 50
|
||||
);
|
||||
});
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
collectCurrentPageSnapshots();
|
||||
const nextFingerprint = readVisibleRowHydrationFingerprint();
|
||||
if (!nextFingerprint) {
|
||||
const hydrationSnapshot = readVisibleRowHydrationSnapshot();
|
||||
if (!hydrationSnapshot.fingerprint) {
|
||||
stablePassCount = 0;
|
||||
previousFingerprint = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nextFingerprint === previousFingerprint) {
|
||||
if (hydrationSnapshot.fingerprint === previousFingerprint) {
|
||||
stablePassCount += 1;
|
||||
} else {
|
||||
previousFingerprint = nextFingerprint;
|
||||
previousFingerprint = hydrationSnapshot.fingerprint;
|
||||
stablePassCount = 1;
|
||||
}
|
||||
|
||||
if (stablePassCount >= 3) {
|
||||
if (hydrationSnapshot.missingDefaultFieldCount === 0 && stablePassCount >= 2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readVisibleRowHydrationFingerprint(): string {
|
||||
function readVisibleRowHydrationSnapshot(): {
|
||||
fingerprint: string;
|
||||
missingDefaultFieldCount: number;
|
||||
} {
|
||||
const table = syncMarketTable(options.document);
|
||||
if (!table || table.rows.length === 0) {
|
||||
return "";
|
||||
return {
|
||||
fingerprint: "",
|
||||
missingDefaultFieldCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
return table.rows
|
||||
.map((rowDom) => {
|
||||
const parts = table.rows.map((rowDom) => {
|
||||
const rowSnapshot = readRowSnapshot(rowDom);
|
||||
const populatedFieldCount = Object.values(rowSnapshot.exportFields ?? {}).filter(
|
||||
(value) => typeof value === "string" && value.trim().length > 0
|
||||
).length;
|
||||
const hasRepresentativeVideo = hasTextValue(
|
||||
rowSnapshot.exportFields?.["代表视频"]
|
||||
);
|
||||
const hasPriceField =
|
||||
hasTextValue(rowSnapshot.price21To60s) ||
|
||||
hasTextValue(rowSnapshot.exportFields?.["21-60s报价"]);
|
||||
const missingDefaultFieldCount =
|
||||
Number(!hasRepresentativeVideo) + Number(!hasPriceField);
|
||||
|
||||
return [
|
||||
rowSnapshot.authorId,
|
||||
populatedFieldCount,
|
||||
rowSnapshot.price21To60s?.trim() ? "price" : "no-price"
|
||||
hasRepresentativeVideo ? "video" : "no-video",
|
||||
hasPriceField ? "price" : "no-price",
|
||||
`missing:${missingDefaultFieldCount}`
|
||||
].join(":");
|
||||
})
|
||||
.join("|");
|
||||
});
|
||||
|
||||
return {
|
||||
fingerprint: parts.join("|"),
|
||||
missingDefaultFieldCount: parts.reduce((count, part) => {
|
||||
const match = part.match(/missing:(\d+)$/);
|
||||
return count + Number(match?.[1] ?? 0);
|
||||
}, 0)
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleSync(): void {
|
||||
|
||||
@ -4,9 +4,11 @@ export interface PluginToolbarHandlers {
|
||||
onApplyFilter(): Promise<void> | void;
|
||||
onApplySort(): Promise<void> | void;
|
||||
onExport(): Promise<void> | void;
|
||||
onSubmitBatch(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface PluginToolbarDom {
|
||||
batchSubmitButton: HTMLButtonElement;
|
||||
exportButton: HTMLButtonElement;
|
||||
exportCustomPagesInput: HTMLInputElement;
|
||||
exportRangeSelect: HTMLSelectElement;
|
||||
@ -70,6 +72,11 @@ export function ensurePluginToolbar(
|
||||
exportButton.dataset.pluginExport = "button";
|
||||
exportButton.textContent = "导出CSV";
|
||||
|
||||
const batchSubmitButton = document.createElement("button");
|
||||
batchSubmitButton.type = "button";
|
||||
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
||||
batchSubmitButton.textContent = "提交批次";
|
||||
|
||||
const exportRangeSelect = document.createElement("select");
|
||||
exportRangeSelect.dataset.pluginExportRange = "select";
|
||||
appendOption(exportRangeSelect, "current", "当前页");
|
||||
@ -98,7 +105,8 @@ export function ensurePluginToolbar(
|
||||
sortApplyButton,
|
||||
exportRangeSelect,
|
||||
exportCustomPagesInput,
|
||||
exportButton
|
||||
exportButton,
|
||||
batchSubmitButton
|
||||
);
|
||||
root.append(exportStatusText);
|
||||
document.body.prepend(root);
|
||||
@ -112,8 +120,12 @@ export function ensurePluginToolbar(
|
||||
exportButton.addEventListener("click", () => {
|
||||
void handlers.onExport();
|
||||
});
|
||||
batchSubmitButton.addEventListener("click", () => {
|
||||
void handlers.onSubmitBatch();
|
||||
});
|
||||
exportRangeSelect.addEventListener("change", () => {
|
||||
syncCustomPagesInputVisibility({
|
||||
batchSubmitButton,
|
||||
exportButton,
|
||||
exportCustomPagesInput,
|
||||
exportRangeSelect,
|
||||
@ -129,6 +141,7 @@ export function ensurePluginToolbar(
|
||||
});
|
||||
|
||||
const toolbarDom = {
|
||||
batchSubmitButton,
|
||||
exportButton,
|
||||
exportCustomPagesInput,
|
||||
exportRangeSelect,
|
||||
@ -159,6 +172,9 @@ function appendOption(
|
||||
|
||||
function readToolbarDom(root: HTMLElement): PluginToolbarDom {
|
||||
const toolbarDom = {
|
||||
batchSubmitButton: root.querySelector(
|
||||
'[data-plugin-batch-submit="button"]'
|
||||
) as HTMLButtonElement,
|
||||
exportButton: root.querySelector(
|
||||
'[data-plugin-export="button"]'
|
||||
) as HTMLButtonElement,
|
||||
@ -255,6 +271,7 @@ export function setToolbarBusyState(
|
||||
isBusy: boolean
|
||||
): void {
|
||||
[
|
||||
toolbar.batchSubmitButton,
|
||||
toolbar.exportButton,
|
||||
toolbar.filterApplyButton,
|
||||
toolbar.sortApplyButton,
|
||||
|
||||
@ -44,14 +44,21 @@ describe("market-content-entry", () => {
|
||||
const createMarketController = vi.fn(() => ({
|
||||
ready: Promise.resolve()
|
||||
}));
|
||||
const sendMessage = vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
}));
|
||||
|
||||
window.history.replaceState({}, "", "/ad/creator/market");
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
chrome?: { runtime?: object };
|
||||
chrome?: { runtime?: { sendMessage?: (message: unknown) => Promise<unknown> } };
|
||||
}
|
||||
).chrome = {
|
||||
runtime: {}
|
||||
runtime: {
|
||||
sendMessage
|
||||
}
|
||||
};
|
||||
|
||||
vi.doMock("../src/content/market/index", () => ({
|
||||
@ -72,7 +79,12 @@ describe("market-content-entry", () => {
|
||||
|
||||
const { bootContentScript } = await import("../src/content/index");
|
||||
await bootContentScript({
|
||||
createMarketController
|
||||
createMarketController,
|
||||
sendAuthMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
}))
|
||||
});
|
||||
|
||||
expect(createMarketController).toHaveBeenCalledTimes(1);
|
||||
@ -90,6 +102,11 @@ describe("market-content-entry", () => {
|
||||
await bootContentScript({
|
||||
createMarketController,
|
||||
document,
|
||||
sendAuthMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
})),
|
||||
window: {
|
||||
location: {
|
||||
href: "https://www.xingtu.cn/ad/creator/market"
|
||||
@ -128,7 +145,12 @@ describe("market-content-entry", () => {
|
||||
|
||||
const { bootContentScript } = await import("../src/content/index");
|
||||
await bootContentScript({
|
||||
createMarketController
|
||||
createMarketController,
|
||||
sendAuthMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
}))
|
||||
});
|
||||
|
||||
const controllerOptions = createMarketController.mock.calls[0]?.[0];
|
||||
@ -163,8 +185,14 @@ describe("market-content-entry", () => {
|
||||
};
|
||||
|
||||
const { bootContentScript } = await import("../src/content/index");
|
||||
sendMessage.mockClear();
|
||||
await bootContentScript({
|
||||
createMarketController
|
||||
createMarketController,
|
||||
sendAuthMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
}))
|
||||
});
|
||||
|
||||
const controllerOptions = createMarketController.mock.calls[0]?.[0];
|
||||
@ -213,6 +241,28 @@ describe("market-content-entry", () => {
|
||||
).toBe("0.03% - 0.2%");
|
||||
});
|
||||
|
||||
test("boots the controller only after auth succeeds", async () => {
|
||||
const createMarketController = vi.fn(() => ({
|
||||
ready: Promise.resolve()
|
||||
}));
|
||||
|
||||
window.history.replaceState({}, "", "/ad/creator/market");
|
||||
|
||||
const { bootContentScript } = await import("../src/content/index");
|
||||
await bootContentScript({
|
||||
createMarketController,
|
||||
document,
|
||||
sendAuthMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
})),
|
||||
window
|
||||
});
|
||||
|
||||
expect(createMarketController).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("hydrates the real div-grid market rows on start", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{
|
||||
@ -481,6 +531,9 @@ describe("market-content-entry", () => {
|
||||
|
||||
expect(exportRangeSelect?.value).toBe("first-5");
|
||||
expect(customPagesInput?.hidden).toBe(true);
|
||||
expect(
|
||||
document.querySelector('[data-plugin-batch-submit="button"]')
|
||||
).not.toBeNull();
|
||||
|
||||
setSelectValue('[data-plugin-export-range="select"]', "custom");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
@ -731,6 +784,7 @@ describe("market-content-entry", () => {
|
||||
|
||||
click('[data-plugin-export="button"]');
|
||||
|
||||
expectButtonDisabled('[data-plugin-batch-submit="button"]', true);
|
||||
expectButtonDisabled('[data-plugin-export="button"]', true);
|
||||
expectButtonDisabled('[data-plugin-filter-apply="button"]', true);
|
||||
expectButtonDisabled('[data-plugin-sort-apply="button"]', true);
|
||||
@ -742,6 +796,7 @@ describe("market-content-entry", () => {
|
||||
await waitForMockCall(buildCsv, 80, 100);
|
||||
|
||||
expect(pagination.getClicks()).toBe(2);
|
||||
expectButtonDisabled('[data-plugin-batch-submit="button"]', false);
|
||||
expectButtonDisabled('[data-plugin-export="button"]', false);
|
||||
expectSelectDisabled('[data-plugin-export-range="select"]', false);
|
||||
expect(buildCsv).toHaveBeenCalledTimes(1);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user