feat: add market row selection column
This commit is contained in:
parent
45e5bb781b
commit
91d8347b76
@ -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;
|
||||
header.appendChild(nextCell);
|
||||
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;
|
||||
row.appendChild(nextCell);
|
||||
if (field === SELECTION_COLUMN_KEY) {
|
||||
row.insertBefore(nextCell, row.firstChild);
|
||||
} else {
|
||||
row.appendChild(nextCell);
|
||||
}
|
||||
return nextCell;
|
||||
}
|
||||
|
||||
@ -727,9 +769,12 @@ function syncDivColumnCells(
|
||||
|
||||
const templateCell =
|
||||
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
|
||||
const nextCell = templateCell
|
||||
? cloneElementShallow(templateCell)
|
||||
: createBareContentCell(column.ownerDocument);
|
||||
const nextCell =
|
||||
field === SELECTION_COLUMN_KEY
|
||||
? createBareContentCell(column.ownerDocument)
|
||||
: templateCell
|
||||
? cloneElementShallow(templateCell)
|
||||
: createBareContentCell(column.ownerDocument);
|
||||
nextCell.dataset.marketRowCell = field;
|
||||
applyColumnWidth(nextCell, field);
|
||||
applyPluginContentCellStyles(nextCell);
|
||||
@ -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";
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user