feat: filter market export by selected creators
This commit is contained in:
parent
96e93628bd
commit
7da1bcf255
@ -142,7 +142,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
setToolbarBusyState(toolbar, true);
|
setToolbarBusyState(toolbar, true);
|
||||||
try {
|
try {
|
||||||
const records = await exportRecords(exportTarget.target);
|
const records = filterRecordsBySelection(
|
||||||
|
await exportRecords(exportTarget.target)
|
||||||
|
);
|
||||||
options.onCsvReady?.(buildCsv(records));
|
options.onCsvReady?.(buildCsv(records));
|
||||||
setToolbarExportStatus(toolbar, "");
|
setToolbarExportStatus(toolbar, "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -173,7 +175,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
setToolbarBusyState(toolbar, true);
|
setToolbarBusyState(toolbar, true);
|
||||||
try {
|
try {
|
||||||
const records = await exportRecords(exportTarget.target, "提交中");
|
const records = filterRecordsBySelection(
|
||||||
|
await exportRecords(exportTarget.target, "提交中")
|
||||||
|
);
|
||||||
const authState = await getAuthState();
|
const authState = await getAuthState();
|
||||||
if (!authState.isAuthenticated) {
|
if (!authState.isAuthenticated) {
|
||||||
throw new Error("请先登录插件");
|
throw new Error("请先登录插件");
|
||||||
@ -492,6 +496,17 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
return exportRangeController.exportRecords(target);
|
return exportRangeController.exportRecords(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterRecordsBySelection(records: MarketRecord[]): MarketRecord[] {
|
||||||
|
if (selectedAuthorIds.size === 0) {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedRecords = records.filter((record) =>
|
||||||
|
selectedAuthorIds.has(record.authorId)
|
||||||
|
);
|
||||||
|
return selectedRecords.length > 0 ? selectedRecords : records;
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareCurrentPageForExport(): Promise<void> {
|
async function prepareCurrentPageForExport(): Promise<void> {
|
||||||
await runSyncCycle();
|
await runSyncCycle();
|
||||||
await harvestCurrentPageForExport();
|
await harvestCurrentPageForExport();
|
||||||
@ -499,7 +514,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function harvestCurrentPageForExport(): Promise<void> {
|
async function harvestCurrentPageForExport(): Promise<void> {
|
||||||
await collectCurrentPageSnapshotsUntilSettled();
|
let hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled();
|
||||||
|
if (
|
||||||
|
hydrationSnapshot.missingDefaultFieldCount === 0 &&
|
||||||
|
hydrationSnapshot.blankExportFieldCount === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const table = syncMarketTable(options.document);
|
const table = syncMarketTable(options.document);
|
||||||
const scrollContainer = findCurrentPageScrollContainer(table);
|
const scrollContainer = findCurrentPageScrollContainer(table);
|
||||||
@ -523,7 +544,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop)
|
nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop)
|
||||||
) {
|
) {
|
||||||
setScrollTop(scrollContainer, nextScrollTop);
|
setScrollTop(scrollContainer, nextScrollTop);
|
||||||
await collectCurrentPageSnapshotsUntilSettled();
|
hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled();
|
||||||
|
if (
|
||||||
|
hydrationSnapshot.missingDefaultFieldCount === 0 &&
|
||||||
|
hydrationSnapshot.blankExportFieldCount === 0
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (nextScrollTop === maxScrollTop) {
|
if (nextScrollTop === maxScrollTop) {
|
||||||
break;
|
break;
|
||||||
@ -532,7 +559,6 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
if (scrollContainer.scrollTop !== originalScrollTop) {
|
if (scrollContainer.scrollTop !== originalScrollTop) {
|
||||||
setScrollTop(scrollContainer, originalScrollTop);
|
setScrollTop(scrollContainer, originalScrollTop);
|
||||||
await collectCurrentPageSnapshotsUntilSettled();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -648,10 +674,19 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectCurrentPageSnapshotsUntilSettled(): Promise<void> {
|
async function collectCurrentPageSnapshotsUntilSettled(): Promise<{
|
||||||
|
blankExportFieldCount: number;
|
||||||
|
fingerprint: string;
|
||||||
|
missingDefaultFieldCount: number;
|
||||||
|
}> {
|
||||||
let previousFingerprint = "";
|
let previousFingerprint = "";
|
||||||
let stablePassCount = 0;
|
let stablePassCount = 0;
|
||||||
let fingerprintStableSince = 0;
|
let fingerprintStableSince = 0;
|
||||||
|
let lastSnapshot = {
|
||||||
|
blankExportFieldCount: 0,
|
||||||
|
fingerprint: "",
|
||||||
|
missingDefaultFieldCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 16; attempt += 1) {
|
for (let attempt = 0; attempt < 16; attempt += 1) {
|
||||||
await waitForDomSettled();
|
await waitForDomSettled();
|
||||||
@ -667,6 +702,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
collectCurrentPageSnapshots();
|
collectCurrentPageSnapshots();
|
||||||
const hydrationSnapshot = readVisibleRowHydrationSnapshot();
|
const hydrationSnapshot = readVisibleRowHydrationSnapshot();
|
||||||
|
lastSnapshot = hydrationSnapshot;
|
||||||
if (!hydrationSnapshot.fingerprint) {
|
if (!hydrationSnapshot.fingerprint) {
|
||||||
stablePassCount = 0;
|
stablePassCount = 0;
|
||||||
previousFingerprint = "";
|
previousFingerprint = "";
|
||||||
@ -687,7 +723,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
hydrationSnapshot.blankExportFieldCount === 0 &&
|
hydrationSnapshot.blankExportFieldCount === 0 &&
|
||||||
stablePassCount >= 2
|
stablePassCount >= 2
|
||||||
) {
|
) {
|
||||||
return;
|
return hydrationSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -696,9 +732,11 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
stablePassCount >= 2 &&
|
stablePassCount >= 2 &&
|
||||||
stableForMs >= 500
|
stableForMs >= 500
|
||||||
) {
|
) {
|
||||||
return;
|
return hydrationSnapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return lastSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readVisibleRowHydrationSnapshot(): {
|
function readVisibleRowHydrationSnapshot(): {
|
||||||
|
|||||||
@ -1242,6 +1242,84 @@ describe("market-content-entry", () => {
|
|||||||
).toContain("有效页数");
|
).toContain("有效页数");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("selected export uses only creators selected in the current range", async () => {
|
||||||
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" },
|
||||||
|
{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }
|
||||||
|
]);
|
||||||
|
const buildCsv = vi.fn(() => "csv-output");
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
buildCsv,
|
||||||
|
document,
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
onCsvReady: vi.fn(),
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
clickSelectionCheckboxForAuthor("333");
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
|
||||||
|
click('[data-plugin-export="button"]');
|
||||||
|
await waitForMockCall(buildCsv, 40, 50);
|
||||||
|
|
||||||
|
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
|
||||||
|
"111",
|
||||||
|
"333"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selected export falls back to all creators in the current range when no selection matches", async () => {
|
||||||
|
const pages = [
|
||||||
|
[
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" },
|
||||||
|
{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }
|
||||||
|
]
|
||||||
|
];
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(pages[0]);
|
||||||
|
installAsyncPaginationHarness(pages);
|
||||||
|
const buildCsv = vi.fn(() => "csv-output");
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
buildCsv,
|
||||||
|
document,
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
onCsvReady: vi.fn(),
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
click('[data-testid="next-page"]');
|
||||||
|
await flushWithTimers();
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
|
||||||
|
click('[data-plugin-export="button"]');
|
||||||
|
await waitForMockCall(buildCsv, 40, 50);
|
||||||
|
|
||||||
|
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
|
||||||
|
"333",
|
||||||
|
"444"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("prompts for a batch name before submitting the current range", async () => {
|
test("prompts for a batch name before submitting the current range", async () => {
|
||||||
document.body.innerHTML = buildMarketFixture();
|
document.body.innerHTML = buildMarketFixture();
|
||||||
const promptBatchName = vi.fn(() => "618达人筛选第一批");
|
const promptBatchName = vi.fn(() => "618达人筛选第一批");
|
||||||
@ -1281,6 +1359,100 @@ describe("market-content-entry", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("selected batch submit uses only creators selected in the current range", async () => {
|
||||||
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" },
|
||||||
|
{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }
|
||||||
|
]);
|
||||||
|
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;
|
||||||
|
clickSelectionCheckboxForAuthor("222");
|
||||||
|
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(submitBatch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
authors: [{ authorId: "222", authorName: "达人 B" }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selected batch submit falls back to all creators in the current range when no selection matches", async () => {
|
||||||
|
const pages = [
|
||||||
|
[
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" },
|
||||||
|
{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }
|
||||||
|
]
|
||||||
|
];
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(pages[0]);
|
||||||
|
installAsyncPaginationHarness(pages);
|
||||||
|
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;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
click('[data-testid="next-page"]');
|
||||||
|
await flushWithTimers();
|
||||||
|
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(submitBatch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
authors: [
|
||||||
|
{ authorId: "333", authorName: "达人 C" },
|
||||||
|
{ authorId: "444", authorName: "达人 D" }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("shows an error when the batch name is blank", async () => {
|
test("shows an error when the batch name is blank", async () => {
|
||||||
document.body.innerHTML = buildMarketFixture();
|
document.body.innerHTML = buildMarketFixture();
|
||||||
const promptBatchName = vi.fn(() => " ");
|
const promptBatchName = vi.fn(() => " ");
|
||||||
|
|||||||
@ -293,6 +293,20 @@ describe("market-dom-sync", () => {
|
|||||||
expect(readScrollHintText()).toBe("横向滚动可查看看后搜率、秒探指标");
|
expect(readScrollHintText()).toBe("横向滚动可查看看后搜率、秒探指标");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps reading native author rows after the selection column is injected", () => {
|
||||||
|
document.body.innerHTML = buildRealMarketGridFixture();
|
||||||
|
|
||||||
|
expect(syncMarketTable(document)?.rows.map((row) => row.authorId)).toEqual([
|
||||||
|
"111",
|
||||||
|
"222"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const table = syncMarketTable(document);
|
||||||
|
|
||||||
|
expect(table?.rows.map((row) => row.authorId)).toEqual(["111", "222"]);
|
||||||
|
expect(readAuthorNames()).toEqual(["达人 A", "达人 B"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("uses native-like alignment styles for plugin cells", () => {
|
test("uses native-like alignment styles for plugin cells", () => {
|
||||||
document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes();
|
document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user