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";
const BACKEND_COLUMN_KEY = "backendMetrics";
const SELECTION_COLUMN_KEY = "selection";
const SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate";
const PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate";
const ACTION_HEADER_TEXT = "操作";
@ -86,12 +87,14 @@ export interface MarketRowDom {
price21To60s?: string;
rates?: AfterSearchRates;
row: HTMLElement;
selectionCheckbox: HTMLInputElement;
singleCell: HTMLElement;
visibilityTargets: HTMLElement[];
orderTargets: RowOrderTarget[];
}
export interface MarketTableDom {
headerSelectionCheckbox: HTMLInputElement | null;
rows: MarketRowDom[];
}
@ -263,6 +266,8 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
return null;
}
const selectionHeader = ensureSyntheticHeaderCell(header, SELECTION_COLUMN_KEY, "");
const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeader);
ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "单视频看后搜率");
ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "个人视频看后搜率");
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(
(rowElement) => {
const row = rowElement as HTMLElement;
const selectionCell = ensureSyntheticRowCell(row, SELECTION_COLUMN_KEY);
const selectionCheckbox = ensureSelectionRowControl(selectionCell);
const singleCell = ensureSyntheticRowCell(row, SINGLE_COLUMN_KEY);
const personalCell = ensureSyntheticRowCell(row, PERSONAL_COLUMN_KEY);
const backendMetricsCells = Object.fromEntries(
@ -303,6 +310,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
?.textContent?.trim() ?? "",
rates: undefined,
row,
selectionCheckbox,
singleCell,
visibilityTargets: [row]
} satisfies MarketRowDom;
@ -310,6 +318,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
);
return {
headerSelectionCheckbox,
rows
};
}
@ -435,12 +444,16 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
bodySection,
getDirectChildIndex(headerSection, authorHeader)
);
const authorHeaderSection = getIndexedChild(
headerSection,
getDirectChildIndex(headerSection, authorHeader)
);
const rightSection = getIndexedChild(
bodySection,
getDirectChildIndex(headerSection, actionHeader)
);
if (!authorSection || !rightSection) {
if (!authorSection || !authorHeaderSection || !rightSection) {
return null;
}
const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection;
@ -462,6 +475,19 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
}
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 =
getDirectHeaderCells(middleHeaderSection).at(-1) ??
findPreviousHeaderCell(actionHeader) ??
@ -510,6 +536,8 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
) as Record<BackendMetricField, HTMLElement>;
syncContainerWidth(pluginHeaderSection);
syncContainerWidth(pluginBodySection);
syncContainerWidth(authorHeaderSection);
syncContainerWidth(authorSection);
ensureVisibleHorizontalScroll(headerSection);
ensureVisibleHorizontalScroll(bodySection);
ensureScrollHint(root, headerSection);
@ -525,6 +553,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
: []
);
const authorCells = getDirectContentCells(authorColumn);
const selectionCells = getDirectContentCells(selectionColumn);
const singleCells = getDirectContentCells(singleColumn);
const personalCells = getDirectContentCells(personalColumn);
const backendMetricCellsByField = Object.fromEntries(
@ -539,6 +568,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
const remainingSerializedMarketRows = [...readSerializedMarketRows(root.ownerDocument)];
const rows = authorCells.flatMap((authorCell, index) => {
const selectionCell = selectionCells[index] ?? null;
const singleCell = singleCells[index] ?? null;
const personalCell = personalCells[index] ?? null;
const backendMetricsCells = Object.fromEntries(
@ -548,12 +578,14 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
])
) as Record<BackendMetricField, HTMLElement | null>;
if (
!selectionCell ||
!singleCell ||
!personalCell ||
Object.values(backendMetricsCells).some((cell) => cell === null)
) {
return [];
}
const selectionCheckbox = ensureSelectionRowControl(selectionCell);
const alignedRowCells = allBodyColumns.map(
(column) => getDirectContentCells(column)[index] ?? null
@ -612,6 +644,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
price21To60s,
rates: fallbackMarketRow?.rates,
row: authorCell,
selectionCheckbox,
singleCell,
visibilityTargets: rowCells
} satisfies MarketRowDom
@ -619,6 +652,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
});
return {
headerSelectionCheckbox,
rows
};
}
@ -640,7 +674,11 @@ function ensureSyntheticHeaderCell(
const nextCell = header.ownerDocument.createElement("div");
nextCell.dataset.marketHeaderCell = field;
nextCell.textContent = label;
if (field === SELECTION_COLUMN_KEY) {
header.insertBefore(nextCell, header.firstChild);
} else {
header.appendChild(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");
nextCell.dataset.marketRowCell = field;
if (field === SELECTION_COLUMN_KEY) {
row.insertBefore(nextCell, row.firstChild);
} else {
row.appendChild(nextCell);
}
return nextCell;
}
@ -727,7 +769,10 @@ function syncDivColumnCells(
const templateCell =
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
const nextCell = templateCell
const nextCell =
field === SELECTION_COLUMN_KEY
? createBareContentCell(column.ownerDocument)
: templateCell
? cloneElementShallow(templateCell)
: createBareContentCell(column.ownerDocument);
nextCell.dataset.marketRowCell = field;
@ -756,6 +801,54 @@ function applyPluginContentCellStyles(cell: HTMLElement): void {
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 {
if ("ownerDocument" in root && root.ownerDocument) {
return root.ownerDocument;
@ -1434,6 +1527,11 @@ function readRateCellText(value: string | undefined): string {
}
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) {
element.style.minWidth = "240px";
element.style.width = "240px";

View File

@ -39,6 +39,9 @@ describe("market-dom-sync", () => {
const table = syncMarketTable(document);
expect(table).not.toBeNull();
expect(
document.querySelector('[data-market-header-cell="selection"]')
).not.toBeNull();
expect(
document.querySelector('[data-market-header-cell="singleVideoAfterSearchRate"]')
).not.toBeNull();
@ -63,7 +66,9 @@ describe("market-dom-sync", () => {
expect(
document.querySelector('[data-market-header-cell="cpSearch"]')
).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", () => {
@ -170,7 +175,11 @@ describe("market-dom-sync", () => {
throw new Error("Expected market table");
}
expect(table.headerSelectionCheckbox).not.toBeNull();
expect(table.rows[0]?.selectionCheckbox).not.toBeNull();
expect(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]);
expect(readSelectionHeaderText()).toBe("全选");
expect(readSelectionRowCheckboxCount()).toBe(2);
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() {
const authorColumn = document.querySelector(
'[data-testid="author-section"] .content-column'