feat: add market row selection column

This commit is contained in:
admin123 2026-04-23 18:20:52 +08:00
parent 45e5bb781b
commit 91d8347b76
2 changed files with 126 additions and 7 deletions

View File

@ -11,6 +11,7 @@ import type {
} from "./types"; } from "./types";
const BACKEND_COLUMN_KEY = "backendMetrics"; const BACKEND_COLUMN_KEY = "backendMetrics";
const SELECTION_COLUMN_KEY = "selection";
const SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate"; const SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate";
const PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate"; const PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate";
const ACTION_HEADER_TEXT = "操作"; const ACTION_HEADER_TEXT = "操作";
@ -86,12 +87,14 @@ export interface MarketRowDom {
price21To60s?: string; price21To60s?: string;
rates?: AfterSearchRates; rates?: AfterSearchRates;
row: HTMLElement; row: HTMLElement;
selectionCheckbox: HTMLInputElement;
singleCell: HTMLElement; singleCell: HTMLElement;
visibilityTargets: HTMLElement[]; visibilityTargets: HTMLElement[];
orderTargets: RowOrderTarget[]; orderTargets: RowOrderTarget[];
} }
export interface MarketTableDom { export interface MarketTableDom {
headerSelectionCheckbox: HTMLInputElement | null;
rows: MarketRowDom[]; rows: MarketRowDom[];
} }
@ -263,6 +266,8 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
return null; return null;
} }
const selectionHeader = ensureSyntheticHeaderCell(header, SELECTION_COLUMN_KEY, "");
const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeader);
ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "单视频看后搜率"); ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "单视频看后搜率");
ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "个人视频看后搜率"); ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "个人视频看后搜率");
BACKEND_METRIC_COLUMNS.forEach(({ field, label }) => { BACKEND_METRIC_COLUMNS.forEach(({ field, label }) => {
@ -273,6 +278,8 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
const rows = Array.from(body.querySelectorAll("[data-market-row]")).map( const rows = Array.from(body.querySelectorAll("[data-market-row]")).map(
(rowElement) => { (rowElement) => {
const row = rowElement as HTMLElement; const row = rowElement as HTMLElement;
const selectionCell = ensureSyntheticRowCell(row, SELECTION_COLUMN_KEY);
const selectionCheckbox = ensureSelectionRowControl(selectionCell);
const singleCell = ensureSyntheticRowCell(row, SINGLE_COLUMN_KEY); const singleCell = ensureSyntheticRowCell(row, SINGLE_COLUMN_KEY);
const personalCell = ensureSyntheticRowCell(row, PERSONAL_COLUMN_KEY); const personalCell = ensureSyntheticRowCell(row, PERSONAL_COLUMN_KEY);
const backendMetricsCells = Object.fromEntries( const backendMetricsCells = Object.fromEntries(
@ -303,6 +310,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
?.textContent?.trim() ?? "", ?.textContent?.trim() ?? "",
rates: undefined, rates: undefined,
row, row,
selectionCheckbox,
singleCell, singleCell,
visibilityTargets: [row] visibilityTargets: [row]
} satisfies MarketRowDom; } satisfies MarketRowDom;
@ -310,6 +318,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
); );
return { return {
headerSelectionCheckbox,
rows rows
}; };
} }
@ -435,12 +444,16 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
bodySection, bodySection,
getDirectChildIndex(headerSection, authorHeader) getDirectChildIndex(headerSection, authorHeader)
); );
const authorHeaderSection = getIndexedChild(
headerSection,
getDirectChildIndex(headerSection, authorHeader)
);
const rightSection = getIndexedChild( const rightSection = getIndexedChild(
bodySection, bodySection,
getDirectChildIndex(headerSection, actionHeader) getDirectChildIndex(headerSection, actionHeader)
); );
if (!authorSection || !rightSection) { if (!authorSection || !authorHeaderSection || !rightSection) {
return null; return null;
} }
const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection; const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection;
@ -462,6 +475,19 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
} }
const rowCount = getDirectContentCells(authorColumn).length; const rowCount = getDirectContentCells(authorColumn).length;
const selectionHeaderCell = ensureDivHeaderCell(
authorHeaderSection,
authorHeader,
SELECTION_COLUMN_KEY,
""
);
const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeaderCell);
const selectionColumn = ensureDivBodyColumn(
authorSection,
authorColumn,
SELECTION_COLUMN_KEY,
rowCount
);
const headerTemplateCell = const headerTemplateCell =
getDirectHeaderCells(middleHeaderSection).at(-1) ?? getDirectHeaderCells(middleHeaderSection).at(-1) ??
findPreviousHeaderCell(actionHeader) ?? findPreviousHeaderCell(actionHeader) ??
@ -510,6 +536,8 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
) as Record<BackendMetricField, HTMLElement>; ) as Record<BackendMetricField, HTMLElement>;
syncContainerWidth(pluginHeaderSection); syncContainerWidth(pluginHeaderSection);
syncContainerWidth(pluginBodySection); syncContainerWidth(pluginBodySection);
syncContainerWidth(authorHeaderSection);
syncContainerWidth(authorSection);
ensureVisibleHorizontalScroll(headerSection); ensureVisibleHorizontalScroll(headerSection);
ensureVisibleHorizontalScroll(bodySection); ensureVisibleHorizontalScroll(bodySection);
ensureScrollHint(root, headerSection); ensureScrollHint(root, headerSection);
@ -525,6 +553,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
: [] : []
); );
const authorCells = getDirectContentCells(authorColumn); const authorCells = getDirectContentCells(authorColumn);
const selectionCells = getDirectContentCells(selectionColumn);
const singleCells = getDirectContentCells(singleColumn); const singleCells = getDirectContentCells(singleColumn);
const personalCells = getDirectContentCells(personalColumn); const personalCells = getDirectContentCells(personalColumn);
const backendMetricCellsByField = Object.fromEntries( const backendMetricCellsByField = Object.fromEntries(
@ -539,6 +568,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
const remainingSerializedMarketRows = [...readSerializedMarketRows(root.ownerDocument)]; const remainingSerializedMarketRows = [...readSerializedMarketRows(root.ownerDocument)];
const rows = authorCells.flatMap((authorCell, index) => { const rows = authorCells.flatMap((authorCell, index) => {
const selectionCell = selectionCells[index] ?? null;
const singleCell = singleCells[index] ?? null; const singleCell = singleCells[index] ?? null;
const personalCell = personalCells[index] ?? null; const personalCell = personalCells[index] ?? null;
const backendMetricsCells = Object.fromEntries( const backendMetricsCells = Object.fromEntries(
@ -548,12 +578,14 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
]) ])
) as Record<BackendMetricField, HTMLElement | null>; ) as Record<BackendMetricField, HTMLElement | null>;
if ( if (
!selectionCell ||
!singleCell || !singleCell ||
!personalCell || !personalCell ||
Object.values(backendMetricsCells).some((cell) => cell === null) Object.values(backendMetricsCells).some((cell) => cell === null)
) { ) {
return []; return [];
} }
const selectionCheckbox = ensureSelectionRowControl(selectionCell);
const alignedRowCells = allBodyColumns.map( const alignedRowCells = allBodyColumns.map(
(column) => getDirectContentCells(column)[index] ?? null (column) => getDirectContentCells(column)[index] ?? null
@ -612,6 +644,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
price21To60s, price21To60s,
rates: fallbackMarketRow?.rates, rates: fallbackMarketRow?.rates,
row: authorCell, row: authorCell,
selectionCheckbox,
singleCell, singleCell,
visibilityTargets: rowCells visibilityTargets: rowCells
} satisfies MarketRowDom } satisfies MarketRowDom
@ -619,6 +652,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
}); });
return { return {
headerSelectionCheckbox,
rows rows
}; };
} }
@ -640,7 +674,11 @@ function ensureSyntheticHeaderCell(
const nextCell = header.ownerDocument.createElement("div"); const nextCell = header.ownerDocument.createElement("div");
nextCell.dataset.marketHeaderCell = field; nextCell.dataset.marketHeaderCell = field;
nextCell.textContent = label; nextCell.textContent = label;
if (field === SELECTION_COLUMN_KEY) {
header.insertBefore(nextCell, header.firstChild);
} else {
header.appendChild(nextCell); header.appendChild(nextCell);
}
return nextCell; return nextCell;
} }
@ -655,7 +693,11 @@ function ensureSyntheticRowCell(row: HTMLElement, field: string): HTMLElement {
const nextCell = row.ownerDocument.createElement(field === BACKEND_COLUMN_KEY ? "div" : "span"); const nextCell = row.ownerDocument.createElement(field === BACKEND_COLUMN_KEY ? "div" : "span");
nextCell.dataset.marketRowCell = field; nextCell.dataset.marketRowCell = field;
if (field === SELECTION_COLUMN_KEY) {
row.insertBefore(nextCell, row.firstChild);
} else {
row.appendChild(nextCell); row.appendChild(nextCell);
}
return nextCell; return nextCell;
} }
@ -727,7 +769,10 @@ function syncDivColumnCells(
const templateCell = const templateCell =
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null; templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
const nextCell = templateCell const nextCell =
field === SELECTION_COLUMN_KEY
? createBareContentCell(column.ownerDocument)
: templateCell
? cloneElementShallow(templateCell) ? cloneElementShallow(templateCell)
: createBareContentCell(column.ownerDocument); : createBareContentCell(column.ownerDocument);
nextCell.dataset.marketRowCell = field; nextCell.dataset.marketRowCell = field;
@ -756,6 +801,54 @@ function applyPluginContentCellStyles(cell: HTMLElement): void {
cell.style.whiteSpace = "nowrap"; cell.style.whiteSpace = "nowrap";
} }
function ensureSelectionHeaderControl(cell: HTMLElement): HTMLInputElement {
cell.textContent = "";
cell.style.gap = "6px";
cell.style.justifyContent = "center";
const checkbox = ensureSelectionCheckbox(cell, "header");
const label = cell.querySelector(
'[data-market-selection-label="header"]'
) as HTMLElement | null;
if (label) {
label.textContent = "全选";
return checkbox;
}
const nextLabel = cell.ownerDocument.createElement("span");
nextLabel.dataset.marketSelectionLabel = "header";
nextLabel.textContent = "全选";
nextLabel.style.fontSize = "12px";
cell.appendChild(nextLabel);
return checkbox;
}
function ensureSelectionRowControl(cell: HTMLElement): HTMLInputElement {
cell.textContent = "";
cell.style.justifyContent = "center";
return ensureSelectionCheckbox(cell, "row");
}
function ensureSelectionCheckbox(
container: HTMLElement,
kind: "header" | "row"
): HTMLInputElement {
const existingCheckbox = container.querySelector(
`[data-market-selection-checkbox="${kind}"]`
) as HTMLInputElement | null;
if (existingCheckbox) {
existingCheckbox.type = "checkbox";
return existingCheckbox;
}
const checkbox = container.ownerDocument.createElement("input");
checkbox.type = "checkbox";
checkbox.dataset.marketSelectionCheckbox = kind;
checkbox.style.cursor = "pointer";
container.appendChild(checkbox);
return checkbox;
}
function getOwnerDocument(root: ParentNode): Document | null { function getOwnerDocument(root: ParentNode): Document | null {
if ("ownerDocument" in root && root.ownerDocument) { if ("ownerDocument" in root && root.ownerDocument) {
return root.ownerDocument; return root.ownerDocument;
@ -1434,6 +1527,11 @@ function readRateCellText(value: string | undefined): string {
} }
function applyColumnWidth(element: HTMLElement, field: string): void { function applyColumnWidth(element: HTMLElement, field: string): void {
if (field === SELECTION_COLUMN_KEY) {
element.style.minWidth = "56px";
element.style.width = "56px";
}
if (field === BACKEND_COLUMN_KEY) { if (field === BACKEND_COLUMN_KEY) {
element.style.minWidth = "240px"; element.style.minWidth = "240px";
element.style.width = "240px"; element.style.width = "240px";

View File

@ -39,6 +39,9 @@ describe("market-dom-sync", () => {
const table = syncMarketTable(document); const table = syncMarketTable(document);
expect(table).not.toBeNull(); expect(table).not.toBeNull();
expect(
document.querySelector('[data-market-header-cell="selection"]')
).not.toBeNull();
expect( expect(
document.querySelector('[data-market-header-cell="singleVideoAfterSearchRate"]') document.querySelector('[data-market-header-cell="singleVideoAfterSearchRate"]')
).not.toBeNull(); ).not.toBeNull();
@ -63,7 +66,9 @@ describe("market-dom-sync", () => {
expect( expect(
document.querySelector('[data-market-header-cell="cpSearch"]') document.querySelector('[data-market-header-cell="cpSearch"]')
).not.toBeNull(); ).not.toBeNull();
expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(16); expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(18);
expect(table?.headerSelectionCheckbox).not.toBeNull();
expect(table?.rows[0]?.selectionCheckbox).not.toBeNull();
}); });
test("renders loading, success, missing, and failed states", () => { test("renders loading, success, missing, and failed states", () => {
@ -170,7 +175,11 @@ describe("market-dom-sync", () => {
throw new Error("Expected market table"); throw new Error("Expected market table");
} }
expect(table.headerSelectionCheckbox).not.toBeNull();
expect(table.rows[0]?.selectionCheckbox).not.toBeNull();
expect(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]); expect(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]);
expect(readSelectionHeaderText()).toBe("全选");
expect(readSelectionRowCheckboxCount()).toBe(2);
expect(readPluginHeaderTexts()).toEqual([ expect(readPluginHeaderTexts()).toEqual([
"单视频看后搜率", "单视频看后搜率",
"个人视频看后搜率", "个人视频看后搜率",
@ -926,6 +935,18 @@ function readScrollHintText() {
); );
} }
function readSelectionHeaderText() {
return (
document
.querySelector('[data-market-header-cell="selection"]')
?.textContent?.trim() ?? ""
);
}
function readSelectionRowCheckboxCount() {
return document.querySelectorAll('[data-market-selection-checkbox="row"]').length;
}
function readAuthorNames() { function readAuthorNames() {
const authorColumn = document.querySelector( const authorColumn = document.querySelector(
'[data-testid="author-section"] .content-column' '[data-testid="author-section"] .content-column'