feat: filter market export by selected creators

This commit is contained in:
admin123 2026-04-23 18:58:59 +08:00
parent 96e93628bd
commit 7da1bcf255
3 changed files with 232 additions and 8 deletions

View File

@ -142,7 +142,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
setToolbarBusyState(toolbar, true);
try {
const records = await exportRecords(exportTarget.target);
const records = filterRecordsBySelection(
await exportRecords(exportTarget.target)
);
options.onCsvReady?.(buildCsv(records));
setToolbarExportStatus(toolbar, "");
} catch (error) {
@ -173,7 +175,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
setToolbarBusyState(toolbar, true);
try {
const records = await exportRecords(exportTarget.target, "提交中");
const records = filterRecordsBySelection(
await exportRecords(exportTarget.target, "提交中")
);
const authState = await getAuthState();
if (!authState.isAuthenticated) {
throw new Error("请先登录插件");
@ -492,6 +496,17 @@ export function createMarketController(options: CreateMarketControllerOptions) {
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> {
await runSyncCycle();
await harvestCurrentPageForExport();
@ -499,7 +514,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
}
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 scrollContainer = findCurrentPageScrollContainer(table);
@ -523,7 +544,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop)
) {
setScrollTop(scrollContainer, nextScrollTop);
await collectCurrentPageSnapshotsUntilSettled();
hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled();
if (
hydrationSnapshot.missingDefaultFieldCount === 0 &&
hydrationSnapshot.blankExportFieldCount === 0
) {
break;
}
if (nextScrollTop === maxScrollTop) {
break;
@ -532,7 +559,6 @@ export function createMarketController(options: CreateMarketControllerOptions) {
if (scrollContainer.scrollTop !== originalScrollTop) {
setScrollTop(scrollContainer, originalScrollTop);
await collectCurrentPageSnapshotsUntilSettled();
}
}
@ -648,10 +674,19 @@ export function createMarketController(options: CreateMarketControllerOptions) {
await Promise.resolve();
}
async function collectCurrentPageSnapshotsUntilSettled(): Promise<void> {
async function collectCurrentPageSnapshotsUntilSettled(): Promise<{
blankExportFieldCount: number;
fingerprint: string;
missingDefaultFieldCount: number;
}> {
let previousFingerprint = "";
let stablePassCount = 0;
let fingerprintStableSince = 0;
let lastSnapshot = {
blankExportFieldCount: 0,
fingerprint: "",
missingDefaultFieldCount: 0
};
for (let attempt = 0; attempt < 16; attempt += 1) {
await waitForDomSettled();
@ -667,6 +702,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
collectCurrentPageSnapshots();
const hydrationSnapshot = readVisibleRowHydrationSnapshot();
lastSnapshot = hydrationSnapshot;
if (!hydrationSnapshot.fingerprint) {
stablePassCount = 0;
previousFingerprint = "";
@ -687,7 +723,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
hydrationSnapshot.blankExportFieldCount === 0 &&
stablePassCount >= 2
) {
return;
return hydrationSnapshot;
}
if (
@ -696,9 +732,11 @@ export function createMarketController(options: CreateMarketControllerOptions) {
stablePassCount >= 2 &&
stableForMs >= 500
) {
return;
return hydrationSnapshot;
}
}
return lastSnapshot;
}
function readVisibleRowHydrationSnapshot(): {

View File

@ -1242,6 +1242,84 @@ describe("market-content-entry", () => {
).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 () => {
document.body.innerHTML = buildMarketFixture();
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 () => {
document.body.innerHTML = buildMarketFixture();
const promptBatchName = vi.fn(() => " ");

View File

@ -293,6 +293,20 @@ describe("market-dom-sync", () => {
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", () => {
document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes();