fix: refine market toolbar filters

This commit is contained in:
wxs
2026-07-09 21:33:58 +08:00
parent b4440e2ca3
commit 370cc0170b
4 changed files with 72 additions and 74 deletions
+4
View File
@@ -35,6 +35,10 @@ export function createBatchPayload(options: {
throw new Error("batch submit batch name is required"); throw new Error("batch submit batch name is required");
} }
if (options.records.length === 0) {
throw new Error("当前没有可提交的达人");
}
return { return {
authors: options.records.map((record) => ({ authors: options.records.map((record) => ({
authorId: record.authorId, authorId: record.authorId,
+32 -49
View File
@@ -12,6 +12,13 @@ export interface PluginToolbarHandlers {
onSubmitBatch(): Promise<void> | void; onSubmitBatch(): Promise<void> | void;
} }
type VisibleSpreadThresholdKey = Extract<
keyof SpreadThresholdFilter["thresholds"],
"finishRate" | "interactionRate"
>;
type SpreadThresholdInputMap = Record<VisibleSpreadThresholdKey, HTMLInputElement>;
export interface PluginToolbarDom { export interface PluginToolbarDom {
audienceProfileByIdExportButton: HTMLButtonElement; audienceProfileByIdExportButton: HTMLButtonElement;
audienceProfileExportButton: HTMLButtonElement; audienceProfileExportButton: HTMLButtonElement;
@@ -25,7 +32,7 @@ export interface PluginToolbarDom {
spreadFilterOnlyAssignSelect: HTMLSelectElement; spreadFilterOnlyAssignSelect: HTMLSelectElement;
spreadFilterRangeSelect: HTMLSelectElement; spreadFilterRangeSelect: HTMLSelectElement;
spreadFilterTypeSelect: HTMLSelectElement; spreadFilterTypeSelect: HTMLSelectElement;
spreadThresholdInputs: Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement>; spreadThresholdInputs: SpreadThresholdInputMap;
root: HTMLElement; root: HTMLElement;
} }
@@ -67,7 +74,6 @@ export function ensurePluginToolbar(
const exportRangeSelect = document.createElement("select"); const exportRangeSelect = document.createElement("select");
exportRangeSelect.dataset.pluginExportRange = "select"; exportRangeSelect.dataset.pluginExportRange = "select";
exportRangeSelect.hidden = true;
appendOption(exportRangeSelect, "current", "当前页"); appendOption(exportRangeSelect, "current", "当前页");
appendOption(exportRangeSelect, "first-5", "前5页"); appendOption(exportRangeSelect, "first-5", "前5页");
appendOption(exportRangeSelect, "first-10", "前10页"); appendOption(exportRangeSelect, "first-10", "前10页");
@@ -86,8 +92,8 @@ export function ensurePluginToolbar(
const exportButton = document.createElement("button"); const exportButton = document.createElement("button");
exportButton.type = "button"; exportButton.type = "button";
exportButton.dataset.pluginExport = "button"; exportButton.dataset.pluginExport = "button";
exportButton.hidden = true; exportButton.textContent = "导出CSV";
exportButton.tabIndex = -1; exportButton.title = "按所选页数范围导出当前筛选结果";
const audienceProfileExportButton = document.createElement("button"); const audienceProfileExportButton = document.createElement("button");
audienceProfileExportButton.type = "button"; audienceProfileExportButton.type = "button";
@@ -162,6 +168,9 @@ export function ensurePluginToolbar(
dataGroup.dataset.pluginToolbarGroup = "data"; dataGroup.dataset.pluginToolbarGroup = "data";
applyToolbarGroupStyles(dataGroup); applyToolbarGroupStyles(dataGroup);
dataGroup.append( dataGroup.append(
exportButton,
exportRangeSelect,
exportCustomPagesInput,
audienceProfileExportButton, audienceProfileExportButton,
audienceProfileByIdExportButton, audienceProfileByIdExportButton,
audienceProfileFieldButton, audienceProfileFieldButton,
@@ -191,12 +200,7 @@ export function ensurePluginToolbar(
secondRow.append(thresholdTitle, thresholdGroup); secondRow.append(thresholdTitle, thresholdGroup);
panel.append(firstRow, secondRow); panel.append(firstRow, secondRow);
root.append( root.append(panel);
exportRangeSelect,
exportCustomPagesInput,
exportButton,
panel
);
document.body.appendChild(root); document.body.appendChild(root);
applyNativeControlStyles(document, { applyNativeControlStyles(document, {
@@ -302,33 +306,16 @@ function appendOption(
function createSpreadThresholdInputs( function createSpreadThresholdInputs(
document: Document document: Document
): Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement> { ): SpreadThresholdInputMap {
return { return {
averageCommentCount: createSpreadThresholdInput(
document,
"averageCommentCount"
),
averageDuration: createSpreadThresholdInput(
document,
"averageDuration"
),
averageLikeCount: createSpreadThresholdInput(
document,
"averageLikeCount"
),
averageShareCount: createSpreadThresholdInput(
document,
"averageShareCount"
),
finishRate: createSpreadThresholdInput(document, "finishRate"), finishRate: createSpreadThresholdInput(document, "finishRate"),
interactionRate: createSpreadThresholdInput(document, "interactionRate"), interactionRate: createSpreadThresholdInput(document, "interactionRate")
playMedian: createSpreadThresholdInput(document, "playMedian")
}; };
} }
function createSpreadThresholdInput( function createSpreadThresholdInput(
document: Document, document: Document,
key: keyof SpreadThresholdFilter["thresholds"] key: VisibleSpreadThresholdKey
): HTMLInputElement { ): HTMLInputElement {
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "number"; input.type = "number";
@@ -339,24 +326,19 @@ function createSpreadThresholdInput(
} }
function getSpreadThresholdStep( function getSpreadThresholdStep(
key: keyof SpreadThresholdFilter["thresholds"] key: VisibleSpreadThresholdKey
): string { ): string {
return key === "finishRate" || key === "interactionRate" ? "0.1" : "1"; return key === "finishRate" || key === "interactionRate" ? "0.1" : "1";
} }
function createSpreadThresholdControls( function createSpreadThresholdControls(
document: Document, document: Document,
inputs: Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement> inputs: SpreadThresholdInputMap
): HTMLElement[] { ): HTMLElement[] {
const controls: HTMLElement[] = []; const controls: HTMLElement[] = [];
const entries: Array<[string, string, HTMLInputElement]> = [ const entries: Array<[string, string, HTMLInputElement]> = [
["评论", "条", inputs.averageCommentCount],
["时长", "秒", inputs.averageDuration],
["点赞", "次", inputs.averageLikeCount],
["转发", "次", inputs.averageShareCount],
["完播率", "%", inputs.finishRate], ["完播率", "%", inputs.finishRate],
["互动率", "%", inputs.interactionRate], ["互动率", "%", inputs.interactionRate]
["播放中位数", "次", inputs.playMedian]
]; ];
entries.forEach(([label, unit, input], index) => { entries.forEach(([label, unit, input], index) => {
@@ -396,21 +378,16 @@ function createSpreadThresholdConjunction(document: Document): HTMLElement {
function readSpreadThresholdInputs( function readSpreadThresholdInputs(
root: HTMLElement root: HTMLElement
): Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement> { ): SpreadThresholdInputMap {
return { return {
averageCommentCount: readSpreadThresholdInput(root, "averageCommentCount"),
averageDuration: readSpreadThresholdInput(root, "averageDuration"),
averageLikeCount: readSpreadThresholdInput(root, "averageLikeCount"),
averageShareCount: readSpreadThresholdInput(root, "averageShareCount"),
finishRate: readSpreadThresholdInput(root, "finishRate"), finishRate: readSpreadThresholdInput(root, "finishRate"),
interactionRate: readSpreadThresholdInput(root, "interactionRate"), interactionRate: readSpreadThresholdInput(root, "interactionRate")
playMedian: readSpreadThresholdInput(root, "playMedian")
}; };
} }
function readSpreadThresholdInput( function readSpreadThresholdInput(
root: HTMLElement, root: HTMLElement,
key: keyof SpreadThresholdFilter["thresholds"] key: VisibleSpreadThresholdKey
): HTMLInputElement { ): HTMLInputElement {
return root.querySelector( return root.querySelector(
`[data-plugin-spread-threshold="${key}"]` `[data-plugin-spread-threshold="${key}"]`
@@ -596,8 +573,8 @@ export function setToolbarExportStatus(
} }
function syncCustomPagesInputVisibility(toolbar: PluginToolbarDom): void { function syncCustomPagesInputVisibility(toolbar: PluginToolbarDom): void {
toolbar.exportRangeSelect.hidden = true; toolbar.exportRangeSelect.hidden = false;
toolbar.exportCustomPagesInput.hidden = true; toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom";
} }
function syncSpreadFilterControlState(toolbar: PluginToolbarDom): void { function syncSpreadFilterControlState(toolbar: PluginToolbarDom): void {
@@ -862,7 +839,7 @@ function applyNativeControlStyles(
spreadFilterRangeSelect: HTMLSelectElement; spreadFilterRangeSelect: HTMLSelectElement;
spreadFilterTypeSelect: HTMLSelectElement; spreadFilterTypeSelect: HTMLSelectElement;
} & Record< } & Record<
keyof SpreadThresholdFilter["thresholds"], VisibleSpreadThresholdKey,
HTMLInputElement HTMLInputElement
> >
): void { ): void {
@@ -875,6 +852,7 @@ function applyNativeControlStyles(
findNativeActionButton(document, "导出"); findNativeActionButton(document, "导出");
if (nativeButton) { if (nativeButton) {
controls.exportButton.className = nativeButton.className;
controls.audienceProfileExportButton.className = nativeButton.className; controls.audienceProfileExportButton.className = nativeButton.className;
controls.audienceProfileByIdExportButton.className = nativeButton.className; controls.audienceProfileByIdExportButton.className = nativeButton.className;
controls.audienceProfileFieldButton.className = nativeButton.className; controls.audienceProfileFieldButton.className = nativeButton.className;
@@ -882,6 +860,7 @@ function applyNativeControlStyles(
} }
[ [
controls.exportButton,
controls.audienceProfileExportButton, controls.audienceProfileExportButton,
controls.audienceProfileByIdExportButton, controls.audienceProfileByIdExportButton,
controls.audienceProfileFieldButton, controls.audienceProfileFieldButton,
@@ -993,6 +972,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
const style = document.createElement("style"); const style = document.createElement("style");
style.id = PLUGIN_ACTION_BUTTON_STYLE_ID; style.id = PLUGIN_ACTION_BUTTON_STYLE_ID;
style.textContent = ` style.textContent = `
[data-plugin-export="button"]:hover:not(:disabled),
[data-plugin-export-audience-profile="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-export-audience-profile-by-id="button"]:hover:not(:disabled),
[data-plugin-audience-profile-fields="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; 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="button"]:active:not(:disabled),
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled), [data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
[data-plugin-audience-profile-fields="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); transform: translateY(1px);
} }
[data-plugin-export="button"]:focus-visible,
[data-plugin-export-audience-profile="button"]:focus-visible, [data-plugin-export-audience-profile="button"]:focus-visible,
[data-plugin-export-audience-profile-by-id="button"]:focus-visible, [data-plugin-export-audience-profile-by-id="button"]:focus-visible,
[data-plugin-audience-profile-fields="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; 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="button"]:disabled,
[data-plugin-export-audience-profile-by-id="button"]:disabled, [data-plugin-export-audience-profile-by-id="button"]:disabled,
[data-plugin-audience-profile-fields="button"]:disabled, [data-plugin-audience-profile-fields="button"]:disabled,
+18
View File
@@ -55,4 +55,22 @@ describe("batch-payload", () => {
}) })
).toThrow(/user/i); ).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("当前没有可提交的达人");
});
}); });
+18 -25
View File
@@ -331,7 +331,7 @@ describe("market-content-entry", () => {
expect( expect(
(document.querySelector('[data-plugin-export-range="select"]') as HTMLSelectElement | null) (document.querySelector('[data-plugin-export-range="select"]') as HTMLSelectElement | null)
?.hidden ?.hidden
).toBe(true); ).toBe(false);
expect( expect(
( (
document.querySelector( document.querySelector(
@@ -342,7 +342,7 @@ describe("market-content-entry", () => {
expect( expect(
(document.querySelector('[data-plugin-export="button"]') as HTMLButtonElement | null) (document.querySelector('[data-plugin-export="button"]') as HTMLButtonElement | null)
?.hidden ?.hidden
).toBe(true); ).toBe(false);
expect( expect(
document.querySelector('[data-plugin-export-audience-profile="button"]') document.querySelector('[data-plugin-export-audience-profile="button"]')
).not.toBeNull(); ).not.toBeNull();
@@ -464,39 +464,32 @@ describe("market-content-entry", () => {
expect(titles[0]?.style.background).toBe("rgb(238, 245, 255)"); expect(titles[0]?.style.background).toBe("rgb(238, 245, 255)");
expect(titles[1]?.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(document.querySelector("[data-plugin-spread-threshold-rule]")).toBeNull();
expect(operators).toEqual(["≥", "≥", "≥", "≥", "≥", "≥", "≥"]); expect(operators).toEqual(["≥", "≥"]);
expect(conjunctions).toEqual(["且", "且", "且", "且", "且", "且"]); expect(conjunctions).toEqual(["且"]);
expect(thresholdInputs.map((input) => input.placeholder)).toEqual([ expect(thresholdInputs.map((input) => input.placeholder)).toEqual([
"",
"",
"",
"",
"",
"", "",
"" ""
]); ]);
expect(thresholdInputs.map((input) => input.step)).toEqual([ expect(thresholdInputs.map((input) => input.step)).toEqual([
"1",
"1",
"1",
"1",
"0.1", "0.1",
"0.1", "0.1"
"1"
]); ]);
expect(thresholdControls.map((control) => control.textContent)).toEqual([ expect(thresholdControls.map((control) => control.textContent)).toEqual([
"评论≥条",
"时长≥秒",
"点赞≥次",
"转发≥次",
"完播率≥%", "完播率≥%",
"互动率≥%", "互动率≥%"
"播放中位数≥次" ]);
expect(buttons.map((button) => button.textContent)).toEqual([
"导出CSV",
"导出选中达人数据",
"按星图ID导出",
"选择字段",
"提交批次"
]); ]);
expect(buttons.map((button) => button.style.backgroundColor)).toEqual([ 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)", "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"]); 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(); document.body.innerHTML = buildMarketFixture();
const { createMarketController } = await import("../src/content/market/index"); const { createMarketController } = await import("../src/content/market/index");
@@ -1276,7 +1269,7 @@ describe("market-content-entry", () => {
) as HTMLInputElement | null; ) as HTMLInputElement | null;
expect(exportRangeSelect?.value).toBe("first-5"); expect(exportRangeSelect?.value).toBe("first-5");
expect(exportRangeSelect?.hidden).toBe(true); expect(exportRangeSelect?.hidden).toBe(false);
expect(customPagesInput?.hidden).toBe(true); expect(customPagesInput?.hidden).toBe(true);
expect( expect(
document.querySelector('[data-plugin-batch-submit="button"]') document.querySelector('[data-plugin-batch-submit="button"]')
@@ -1285,8 +1278,8 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "custom"); setSelectValue('[data-plugin-export-range="select"]', "custom");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
expect(exportRangeSelect?.hidden).toBe(true); expect(exportRangeSelect?.hidden).toBe(false);
expect(customPagesInput?.hidden).toBe(true); expect(customPagesInput?.hidden).toBe(false);
}); });
test("toolbar exposes spread threshold filters and disables fixed personal-video controls", async () => { test("toolbar exposes spread threshold filters and disables fixed personal-video controls", async () => {