From 370cc0170b9a8d5f6564b5b6d0dd641c463124ef Mon Sep 17 00:00:00 2001 From: wxs Date: Thu, 9 Jul 2026 21:33:58 +0800 Subject: [PATCH] fix: refine market toolbar filters --- src/content/market/batch-payload.ts | 4 ++ src/content/market/plugin-toolbar.ts | 81 +++++++++++----------------- tests/batch-payload.test.ts | 18 +++++++ tests/market-content-entry.test.ts | 43 +++++++-------- 4 files changed, 72 insertions(+), 74 deletions(-) diff --git a/src/content/market/batch-payload.ts b/src/content/market/batch-payload.ts index f184a8b..b4dd69d 100644 --- a/src/content/market/batch-payload.ts +++ b/src/content/market/batch-payload.ts @@ -35,6 +35,10 @@ export function createBatchPayload(options: { throw new Error("batch submit batch name is required"); } + if (options.records.length === 0) { + throw new Error("当前没有可提交的达人"); + } + return { authors: options.records.map((record) => ({ authorId: record.authorId, diff --git a/src/content/market/plugin-toolbar.ts b/src/content/market/plugin-toolbar.ts index 311d555..999abb2 100644 --- a/src/content/market/plugin-toolbar.ts +++ b/src/content/market/plugin-toolbar.ts @@ -12,6 +12,13 @@ export interface PluginToolbarHandlers { onSubmitBatch(): Promise | void; } +type VisibleSpreadThresholdKey = Extract< + keyof SpreadThresholdFilter["thresholds"], + "finishRate" | "interactionRate" +>; + +type SpreadThresholdInputMap = Record; + export interface PluginToolbarDom { audienceProfileByIdExportButton: HTMLButtonElement; audienceProfileExportButton: HTMLButtonElement; @@ -25,7 +32,7 @@ export interface PluginToolbarDom { spreadFilterOnlyAssignSelect: HTMLSelectElement; spreadFilterRangeSelect: HTMLSelectElement; spreadFilterTypeSelect: HTMLSelectElement; - spreadThresholdInputs: Record; + spreadThresholdInputs: SpreadThresholdInputMap; root: HTMLElement; } @@ -67,7 +74,6 @@ export function ensurePluginToolbar( const exportRangeSelect = document.createElement("select"); exportRangeSelect.dataset.pluginExportRange = "select"; - exportRangeSelect.hidden = true; appendOption(exportRangeSelect, "current", "当前页"); appendOption(exportRangeSelect, "first-5", "前5页"); appendOption(exportRangeSelect, "first-10", "前10页"); @@ -86,8 +92,8 @@ export function ensurePluginToolbar( const exportButton = document.createElement("button"); exportButton.type = "button"; exportButton.dataset.pluginExport = "button"; - exportButton.hidden = true; - exportButton.tabIndex = -1; + exportButton.textContent = "导出CSV"; + exportButton.title = "按所选页数范围导出当前筛选结果"; const audienceProfileExportButton = document.createElement("button"); audienceProfileExportButton.type = "button"; @@ -162,6 +168,9 @@ export function ensurePluginToolbar( dataGroup.dataset.pluginToolbarGroup = "data"; applyToolbarGroupStyles(dataGroup); dataGroup.append( + exportButton, + exportRangeSelect, + exportCustomPagesInput, audienceProfileExportButton, audienceProfileByIdExportButton, audienceProfileFieldButton, @@ -191,12 +200,7 @@ export function ensurePluginToolbar( secondRow.append(thresholdTitle, thresholdGroup); panel.append(firstRow, secondRow); - root.append( - exportRangeSelect, - exportCustomPagesInput, - exportButton, - panel - ); + root.append(panel); document.body.appendChild(root); applyNativeControlStyles(document, { @@ -302,33 +306,16 @@ function appendOption( function createSpreadThresholdInputs( document: Document -): Record { +): SpreadThresholdInputMap { return { - averageCommentCount: createSpreadThresholdInput( - document, - "averageCommentCount" - ), - averageDuration: createSpreadThresholdInput( - document, - "averageDuration" - ), - averageLikeCount: createSpreadThresholdInput( - document, - "averageLikeCount" - ), - averageShareCount: createSpreadThresholdInput( - document, - "averageShareCount" - ), finishRate: createSpreadThresholdInput(document, "finishRate"), - interactionRate: createSpreadThresholdInput(document, "interactionRate"), - playMedian: createSpreadThresholdInput(document, "playMedian") + interactionRate: createSpreadThresholdInput(document, "interactionRate") }; } function createSpreadThresholdInput( document: Document, - key: keyof SpreadThresholdFilter["thresholds"] + key: VisibleSpreadThresholdKey ): HTMLInputElement { const input = document.createElement("input"); input.type = "number"; @@ -339,24 +326,19 @@ function createSpreadThresholdInput( } function getSpreadThresholdStep( - key: keyof SpreadThresholdFilter["thresholds"] + key: VisibleSpreadThresholdKey ): string { return key === "finishRate" || key === "interactionRate" ? "0.1" : "1"; } function createSpreadThresholdControls( document: Document, - inputs: Record + inputs: SpreadThresholdInputMap ): HTMLElement[] { const controls: HTMLElement[] = []; const entries: Array<[string, string, HTMLInputElement]> = [ - ["评论", "条", inputs.averageCommentCount], - ["时长", "秒", inputs.averageDuration], - ["点赞", "次", inputs.averageLikeCount], - ["转发", "次", inputs.averageShareCount], ["完播率", "%", inputs.finishRate], - ["互动率", "%", inputs.interactionRate], - ["播放中位数", "次", inputs.playMedian] + ["互动率", "%", inputs.interactionRate] ]; entries.forEach(([label, unit, input], index) => { @@ -396,21 +378,16 @@ function createSpreadThresholdConjunction(document: Document): HTMLElement { function readSpreadThresholdInputs( root: HTMLElement -): Record { +): SpreadThresholdInputMap { return { - averageCommentCount: readSpreadThresholdInput(root, "averageCommentCount"), - averageDuration: readSpreadThresholdInput(root, "averageDuration"), - averageLikeCount: readSpreadThresholdInput(root, "averageLikeCount"), - averageShareCount: readSpreadThresholdInput(root, "averageShareCount"), finishRate: readSpreadThresholdInput(root, "finishRate"), - interactionRate: readSpreadThresholdInput(root, "interactionRate"), - playMedian: readSpreadThresholdInput(root, "playMedian") + interactionRate: readSpreadThresholdInput(root, "interactionRate") }; } function readSpreadThresholdInput( root: HTMLElement, - key: keyof SpreadThresholdFilter["thresholds"] + key: VisibleSpreadThresholdKey ): HTMLInputElement { return root.querySelector( `[data-plugin-spread-threshold="${key}"]` @@ -596,8 +573,8 @@ export function setToolbarExportStatus( } function syncCustomPagesInputVisibility(toolbar: PluginToolbarDom): void { - toolbar.exportRangeSelect.hidden = true; - toolbar.exportCustomPagesInput.hidden = true; + toolbar.exportRangeSelect.hidden = false; + toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom"; } function syncSpreadFilterControlState(toolbar: PluginToolbarDom): void { @@ -862,7 +839,7 @@ function applyNativeControlStyles( spreadFilterRangeSelect: HTMLSelectElement; spreadFilterTypeSelect: HTMLSelectElement; } & Record< - keyof SpreadThresholdFilter["thresholds"], + VisibleSpreadThresholdKey, HTMLInputElement > ): void { @@ -875,6 +852,7 @@ function applyNativeControlStyles( findNativeActionButton(document, "导出"); if (nativeButton) { + controls.exportButton.className = nativeButton.className; controls.audienceProfileExportButton.className = nativeButton.className; controls.audienceProfileByIdExportButton.className = nativeButton.className; controls.audienceProfileFieldButton.className = nativeButton.className; @@ -882,6 +860,7 @@ function applyNativeControlStyles( } [ + controls.exportButton, controls.audienceProfileExportButton, controls.audienceProfileByIdExportButton, controls.audienceProfileFieldButton, @@ -993,6 +972,7 @@ function ensurePluginActionButtonTheme(document: Document): void { const style = document.createElement("style"); style.id = PLUGIN_ACTION_BUTTON_STYLE_ID; style.textContent = ` + [data-plugin-export="button"]:hover:not(:disabled), [data-plugin-export-audience-profile="button"]:hover:not(:disabled), [data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled), [data-plugin-audience-profile-fields="button"]:hover:not(:disabled), @@ -1001,6 +981,7 @@ function ensurePluginActionButtonTheme(document: Document): void { border-color: #6d1627 !important; } + [data-plugin-export="button"]:active:not(:disabled), [data-plugin-export-audience-profile="button"]:active:not(:disabled), [data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled), [data-plugin-audience-profile-fields="button"]:active:not(:disabled), @@ -1010,6 +991,7 @@ function ensurePluginActionButtonTheme(document: Document): void { transform: translateY(1px); } + [data-plugin-export="button"]:focus-visible, [data-plugin-export-audience-profile="button"]:focus-visible, [data-plugin-export-audience-profile-by-id="button"]:focus-visible, [data-plugin-audience-profile-fields="button"]:focus-visible, @@ -1018,6 +1000,7 @@ function ensurePluginActionButtonTheme(document: Document): void { box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important; } + [data-plugin-export="button"]:disabled, [data-plugin-export-audience-profile="button"]:disabled, [data-plugin-export-audience-profile-by-id="button"]:disabled, [data-plugin-audience-profile-fields="button"]:disabled, diff --git a/tests/batch-payload.test.ts b/tests/batch-payload.test.ts index 5a652cc..cfe46dc 100644 --- a/tests/batch-payload.test.ts +++ b/tests/batch-payload.test.ts @@ -55,4 +55,22 @@ describe("batch-payload", () => { }) ).toThrow(/user/i); }); + + test("throws when no creator records are available", () => { + expect(() => + createBatchPayload({ + authState: { + isAuthenticated: true, + resource: "https://talent-search.intelligrow.cn", + userInfo: { + name: "王少卿", + sub: "p7pdhhtde8kj" + } + }, + batchName: "空结果批次", + createdAt: "2026-07-03T03:05:52.432Z", + records: [] + }) + ).toThrow("当前没有可提交的达人"); + }); }); diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index cd3a7b5..cafd3e2 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -331,7 +331,7 @@ describe("market-content-entry", () => { expect( (document.querySelector('[data-plugin-export-range="select"]') as HTMLSelectElement | null) ?.hidden - ).toBe(true); + ).toBe(false); expect( ( document.querySelector( @@ -342,7 +342,7 @@ describe("market-content-entry", () => { expect( (document.querySelector('[data-plugin-export="button"]') as HTMLButtonElement | null) ?.hidden - ).toBe(true); + ).toBe(false); expect( document.querySelector('[data-plugin-export-audience-profile="button"]') ).not.toBeNull(); @@ -464,39 +464,32 @@ describe("market-content-entry", () => { expect(titles[0]?.style.background).toBe("rgb(238, 245, 255)"); expect(titles[1]?.style.background).toBe("rgb(238, 245, 255)"); expect(document.querySelector("[data-plugin-spread-threshold-rule]")).toBeNull(); - expect(operators).toEqual(["≥", "≥", "≥", "≥", "≥", "≥", "≥"]); - expect(conjunctions).toEqual(["且", "且", "且", "且", "且", "且"]); + expect(operators).toEqual(["≥", "≥"]); + expect(conjunctions).toEqual(["且"]); expect(thresholdInputs.map((input) => input.placeholder)).toEqual([ - "", - "", - "", - "", - "", "", "" ]); expect(thresholdInputs.map((input) => input.step)).toEqual([ - "1", - "1", - "1", - "1", "0.1", - "0.1", - "1" + "0.1" ]); expect(thresholdControls.map((control) => control.textContent)).toEqual([ - "评论≥条", - "时长≥秒", - "点赞≥次", - "转发≥次", "完播率≥%", - "互动率≥%", - "播放中位数≥次" + "互动率≥%" + ]); + expect(buttons.map((button) => button.textContent)).toEqual([ + "导出CSV", + "导出选中达人数据", + "按星图ID导出", + "选择字段", + "提交批次" ]); expect(buttons.map((button) => button.style.backgroundColor)).toEqual([ "rgb(127, 29, 45)", "rgb(127, 29, 45)", "rgb(127, 29, 45)", + "rgb(127, 29, 45)", "rgb(127, 29, 45)" ]); }); @@ -1253,7 +1246,7 @@ describe("market-content-entry", () => { expect(readDivAuthorOrder()).toEqual(["达人 B", "达人 A"]); }); - test("toolbar keeps export range controls hidden while retaining the internal default range", async () => { + test("toolbar shows export range controls and reveals custom page input only for custom range", async () => { document.body.innerHTML = buildMarketFixture(); const { createMarketController } = await import("../src/content/market/index"); @@ -1276,7 +1269,7 @@ describe("market-content-entry", () => { ) as HTMLInputElement | null; expect(exportRangeSelect?.value).toBe("first-5"); - expect(exportRangeSelect?.hidden).toBe(true); + expect(exportRangeSelect?.hidden).toBe(false); expect(customPagesInput?.hidden).toBe(true); expect( document.querySelector('[data-plugin-batch-submit="button"]') @@ -1285,8 +1278,8 @@ describe("market-content-entry", () => { setSelectValue('[data-plugin-export-range="select"]', "custom"); dispatchChange('[data-plugin-export-range="select"]'); - expect(exportRangeSelect?.hidden).toBe(true); - expect(customPagesInput?.hidden).toBe(true); + expect(exportRangeSelect?.hidden).toBe(false); + expect(customPagesInput?.hidden).toBe(false); }); test("toolbar exposes spread threshold filters and disables fixed personal-video controls", async () => {