import { normalizeFractionRateDisplay, normalizeRateDisplay } from "../../shared/rate-normalizer"; import type { AfterSearchRates, BackendMetrics, MarketRecord, MarketSortField, MarketSortState } 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 = "操作"; const AUTHOR_HEADER_TEXT = "达人信息"; const BACKEND_HEADER_TEXT = "秒探指标"; const MARKET_SCROLL_HINT_TEXT = "横向滚动可查看看后搜率、秒探指标"; const MARKET_SCROLLBAR_STYLE_ID = "sces-market-scrollbar-style"; const UNAVAILABLE_RATE_TEXT = "暂无来源"; const UNAVAILABLE_BACKEND_METRICS_TEXT = "暂无数据"; const SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows"; const SORTABLE_RATE_FIELDS = [SINGLE_COLUMN_KEY, PERSONAL_COLUMN_KEY] as const; const BACKEND_METRIC_COLUMNS = [ { field: "afterViewSearchRate", label: "看后搜率" }, { field: "afterViewSearchCount", label: "看后搜数" }, { field: "a3IncreaseCount", label: "新增A3数" }, { field: "newA3Rate", label: "新增A3率" }, { field: "cpa3", label: "CPA3" }, { field: "cpSearch", label: "cp_search" } ] as const satisfies Array<{ field: keyof BackendMetrics; label: string; }>; type BackendMetricField = (typeof BACKEND_METRIC_COLUMNS)[number]["field"]; const SORTABLE_MARKET_FIELDS = [ ...SORTABLE_RATE_FIELDS, ...BACKEND_METRIC_COLUMNS.map((column) => column.field) ] as const satisfies readonly MarketSortField[]; type RowOrderTarget = { container: HTMLElement; mode: "css" | "dom"; node: HTMLElement; }; type MarketDataRow = { authorId: string; authorName: string; exportFields?: Record; hasDirectRatesSource: boolean; location?: string; price21To60s?: string; rates?: AfterSearchRates; }; export interface MarketRowDom { authorId: string; authorName: string; backendMetricsCells: Record; exportFields?: Record; hasDirectRatesSource?: boolean; location?: string; personalCell: HTMLElement; price21To60s?: string; rates?: AfterSearchRates; row: HTMLElement; selectionCheckbox: HTMLInputElement; singleCell: HTMLElement; visibilityTargets: HTMLElement[]; orderTargets: RowOrderTarget[]; } export interface MarketTableDom { headerSelectionCheckbox: HTMLInputElement | null; rows: MarketRowDom[]; } export function syncMarketTable(root: ParentNode): MarketTableDom | null { return syncSyntheticMarketTable(root) ?? syncDivGridMarketTable(root); } export function readMarketPageSignature(root: ParentNode): string { const document = getOwnerDocument(root); const explicitPageIndex = document?.documentElement.getAttribute("data-test-page-index") ?? ""; const activePageIndex = document ?.querySelector(".el-pagination .number.active, .xt-pagination .number.active") ?.textContent?.trim() ?? ""; const authorIds = readRawAuthorIds(root).join("|"); return `${explicitPageIndex || activePageIndex}::${authorIds}`; } export function findNextPageControl(root: ParentNode): HTMLElement | null { const document = getOwnerDocument(root); if (!document) { return null; } const explicitControl = document.querySelector('[data-testid="next-page"]'); if (explicitControl instanceof document.defaultView!.HTMLElement) { return explicitControl; } const paginationNextControl = document.querySelector( ".el-pagination .btn-next, .xt-pagination .btn-next" ); if (paginationNextControl instanceof document.defaultView!.HTMLElement) { return paginationNextControl; } const candidates = Array.from( document.querySelectorAll("button, a, [role='button']") ).filter( (element): element is HTMLElement => element instanceof document.defaultView!.HTMLElement ); return ( candidates.find((element) => /下一页|next/i.test(normalizeExportCellText(element.textContent)) ) ?? null ); } export function isPageControlDisabled(control: HTMLElement | null): boolean { if (!control) { return true; } if (control instanceof HTMLButtonElement) { return control.disabled; } return control.getAttribute("aria-disabled") === "true"; } export function renderMarketRowState( rowDom: MarketRowDom, record: MarketRecord ): void { renderBackendMetricsCells(rowDom.backendMetricsCells, record); if (record.status === "success" && record.rates) { rowDom.singleCell.textContent = readRateCellText( record.rates.singleVideoAfterSearchRate ); rowDom.personalCell.textContent = readRateCellText( record.rates.personalVideoAfterSearchRate ); return; } if (record.status === "loading") { rowDom.singleCell.textContent = "加载中..."; rowDom.personalCell.textContent = "加载中..."; return; } if (record.status === "failed") { rowDom.singleCell.textContent = "加载失败"; rowDom.personalCell.textContent = "加载失败"; return; } rowDom.singleCell.textContent = ""; rowDom.personalCell.textContent = ""; } export function applyRowVisibility( table: MarketTableDom, visibleAuthorIds: Set ): void { table.rows.forEach((rowDom) => { const isVisible = visibleAuthorIds.has(rowDom.authorId); rowDom.visibilityTargets.forEach((target) => { target.hidden = !isVisible; }); }); } export function applyRowOrder( table: MarketTableDom, orderedAuthorIds: string[] ): void { const rowById = new Map(table.rows.map((rowDom) => [rowDom.authorId, rowDom])); const orderByAuthorId = new Map( orderedAuthorIds.map((authorId, index) => [authorId, index]) ); orderedAuthorIds.forEach((authorId) => { const rowDom = rowById.get(authorId); if (!rowDom) { return; } rowDom.orderTargets.forEach(({ container, mode, node }) => { const visualOrder = orderByAuthorId.get(authorId) ?? orderedAuthorIds.length; if (mode === "css") { container.dataset.marketOrderMode = "css"; container.style.display = "flex"; container.style.flexDirection = "column"; node.style.order = String(visualOrder); return; } container.dataset.marketOrderMode = "dom"; container.appendChild(node); }); }); } export function syncPluginSortHeaders( root: ParentNode, options: { activeSort?: MarketSortState; onToggleSort: (field: MarketSortField) => void; } ): void { SORTABLE_MARKET_FIELDS.forEach((field) => { const cell = root.querySelector( `[data-market-header-cell="${field}"]` ) as HTMLElement | null; if (!cell) { return; } syncSortableHeaderCell(cell, { direction: options.activeSort?.field === field ? options.activeSort.direction : "none", field, onToggleSort: options.onToggleSort }); }); } export function syncMarketSelectionState( table: MarketTableDom, selectedAuthorIds: Set ): void { table.rows.forEach((rowDom) => { rowDom.selectionCheckbox.dataset.marketSelectionAuthorId = rowDom.authorId; rowDom.selectionCheckbox.checked = selectedAuthorIds.has(rowDom.authorId); }); if (!table.headerSelectionCheckbox) { return; } const visibleRows = table.rows.filter((rowDom) => rowDom.visibilityTargets.some((target) => !target.hidden) ); const scopedRows = visibleRows.length > 0 ? visibleRows : table.rows; const selectedCount = scopedRows.filter((rowDom) => selectedAuthorIds.has(rowDom.authorId) ).length; table.headerSelectionCheckbox.indeterminate = selectedCount > 0 && selectedCount < scopedRows.length; table.headerSelectionCheckbox.checked = scopedRows.length > 0 && selectedCount === scopedRows.length; table.headerSelectionCheckbox.disabled = scopedRows.length === 0; } function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null { const header = root.querySelector("[data-market-header]") as HTMLElement | null; const body = root.querySelector("[data-market-body]") as HTMLElement | null; if (!header || !body) { 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 }) => { ensureSyntheticHeaderCell(header, field, label); }); const headerLabelsByField = readSyntheticHeaderLabels(header); 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( BACKEND_METRIC_COLUMNS.map(({ field }) => [field, ensureSyntheticRowCell(row, field)]) ) as Record; const authorId = row.dataset.authorId ?? ""; selectionCheckbox.dataset.marketSelectionAuthorId = authorId; return { authorId, authorName: row.querySelector('[data-market-field="authorName"]')?.textContent?.trim() ?? "", backendMetricsCells, exportFields: readSyntheticExportFields(row, headerLabelsByField), hasDirectRatesSource: false, location: row.querySelector('[data-market-field="location"]')?.textContent?.trim() ?? "", orderTargets: [ { container: body, mode: "dom", node: row } ], personalCell, price21To60s: row .querySelector('[data-market-field="price21To60s"]') ?.textContent?.trim() ?? "", rates: undefined, row, selectionCheckbox, singleCell, visibilityTargets: [row] } satisfies MarketRowDom; } ); return { headerSelectionCheckbox, rows }; } function syncDivGridMarketTable(root: ParentNode): MarketTableDom | null { const document = getOwnerDocument(root); if (!document) { return null; } for (const marketRoot of document.querySelectorAll(".base-author-list")) { if (!(marketRoot instanceof document.defaultView!.HTMLElement)) { continue; } const syncedTable = syncDivGridRoot(marketRoot); if (syncedTable) { return syncedTable; } } return null; } function readRawAuthorIds(root: ParentNode): string[] { const document = getOwnerDocument(root); const syntheticAuthorIds = readSyntheticAuthorIds(root); if (syntheticAuthorIds && syntheticAuthorIds.length > 0) { return syntheticAuthorIds; } const divGridAuthorIds = readDivGridAuthorIds(root); if (divGridAuthorIds && divGridAuthorIds.length > 0) { return divGridAuthorIds; } if (!document) { return []; } return readSerializedMarketRows(document) .map((row) => row.authorId) .filter((authorId) => Boolean(authorId)); } function readSyntheticAuthorIds(root: ParentNode): string[] | null { const body = root.querySelector("[data-market-body]") as HTMLElement | null; if (!body) { return null; } return Array.from(body.querySelectorAll("[data-market-row]")) .map((row) => row instanceof HTMLElement ? row.dataset.authorId ?? "" : "" ) .filter((authorId) => Boolean(authorId)); } function readDivGridAuthorIds(root: ParentNode): string[] | null { const document = getOwnerDocument(root); if (!document) { return null; } const marketRoot = document.querySelector(".base-author-list"); if (!(marketRoot instanceof document.defaultView!.HTMLElement)) { return null; } const bodySection = Array.from(marketRoot.querySelectorAll(".section-wrapper")).find( (section): section is HTMLElement => section instanceof document.defaultView!.HTMLElement && !section.classList.contains("sticky-header") ); const authorSection = bodySection ? Array.from(bodySection.children).find( (child): child is HTMLElement => child instanceof document.defaultView!.HTMLElement && child.querySelector(".content-column .content-cell") ) ?? null : null; const authorColumn = authorSection ? getNativeAuthorColumn(authorSection) : null; if (!authorColumn) { return null; } return getDirectContentCells(authorColumn) .map((cell) => extractAuthorId(cell)) .filter((authorId) => Boolean(authorId)); } function syncDivGridRoot(root: HTMLElement): MarketTableDom | null { const headerSection = root.querySelector( ".section-wrapper.sticky-header" ) as HTMLElement | null; const bodySection = Array.from(root.querySelectorAll(".section-wrapper")).find( (section): section is HTMLElement => section instanceof root.ownerDocument.defaultView!.HTMLElement && !section.classList.contains("sticky-header") ); if (!headerSection || !bodySection) { return null; } const authorHeader = findCellByText(getDirectHeaderCells(headerSection), AUTHOR_HEADER_TEXT); const actionHeader = findCellByText(getDirectHeaderCells(headerSection), ACTION_HEADER_TEXT); if (!authorHeader || !actionHeader) { return null; } const rightHeaderSection = actionHeader.parentElement; if (!(rightHeaderSection instanceof root.ownerDocument.defaultView!.HTMLElement)) { return null; } const middleHeaderSection = findPreviousNativeSection(rightHeaderSection) ?? rightHeaderSection; const authorSection = getIndexedChild( bodySection, getDirectChildIndex(headerSection, authorHeader) ); const authorHeaderSection = getIndexedChild( headerSection, getDirectChildIndex(headerSection, authorHeader) ); const rightSection = getIndexedChild( bodySection, getDirectChildIndex(headerSection, actionHeader) ); if (!authorSection || !authorHeaderSection || !rightSection) { return null; } const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection; const pluginHeaderSection = ensurePluginSection(headerSection, rightHeaderSection, { testId: "plugin-header", type: "header" }); const pluginBodySection = ensurePluginSection(bodySection, rightSection, { testId: "plugin-section", type: "body" }); const authorColumn = getNativeAuthorColumn(authorSection); const actionColumn = getActionColumn(rightSection); if (!authorColumn || !actionColumn) { return 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) ?? actionHeader; const bodyTemplateColumn = getDirectContentColumns(middleBodySection).at(-1) ?? findPreviousColumn(actionColumn) ?? actionColumn; ensureDivHeaderCell( pluginHeaderSection, headerTemplateCell, SINGLE_COLUMN_KEY, "单视频看后搜率" ); ensureDivHeaderCell( pluginHeaderSection, headerTemplateCell, PERSONAL_COLUMN_KEY, "个人视频看后搜率" ); const singleColumn = ensureDivBodyColumn( pluginBodySection, bodyTemplateColumn, SINGLE_COLUMN_KEY, rowCount ); const personalColumn = ensureDivBodyColumn( pluginBodySection, bodyTemplateColumn, PERSONAL_COLUMN_KEY, rowCount ); const backendMetricColumns = Object.fromEntries( BACKEND_METRIC_COLUMNS.map(({ field, label }) => { ensureDivHeaderCell(pluginHeaderSection, headerTemplateCell, field, label); return [ field, ensureDivBodyColumn( pluginBodySection, bodyTemplateColumn, field, rowCount ) ]; }) ) as Record; syncContainerWidth(pluginHeaderSection); syncContainerWidth(pluginBodySection); syncContainerWidth(authorHeaderSection); syncContainerWidth(authorSection); ensureVisibleHorizontalScroll(headerSection); ensureVisibleHorizontalScroll(bodySection); ensureScrollHint(root, headerSection); const allBodyColumns = Array.from(bodySection.children).flatMap((section) => section instanceof root.ownerDocument.defaultView!.HTMLElement ? getDirectContentColumns(section) : [] ); const allHeaderCells = Array.from(headerSection.children).flatMap((section) => section instanceof root.ownerDocument.defaultView!.HTMLElement ? getDirectHeaderCells(section) : [] ); const authorCells = getDirectContentCells(authorColumn); const selectionCells = getDirectContentCells(selectionColumn); const singleCells = getDirectContentCells(singleColumn); const personalCells = getDirectContentCells(personalColumn); const backendMetricCellsByField = Object.fromEntries( BACKEND_METRIC_COLUMNS.map(({ field }) => [ field, getDirectContentCells(backendMetricColumns[field]) ]) ) as Record; const priceColumn = findPreviousColumn(actionColumn); const priceCells = priceColumn ? getDirectContentCells(priceColumn) : []; const remainingVueMarketRows = [...readVueMarketRows(root)]; 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( BACKEND_METRIC_COLUMNS.map(({ field }) => [ field, backendMetricCellsByField[field][index] ?? null ]) ) as Record; 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 ); const rowCells = alignedRowCells.filter( (cell): cell is HTMLElement => cell !== null ); const directAuthorId = extractAuthorId(authorCell) || ""; const directAuthorName = extractAuthorName(authorCell) || ""; const vueMarketRow = takeMatchedMarketDataRow( remainingVueMarketRows, directAuthorId, directAuthorName ); const serializedMarketRow = takeMatchedMarketDataRow( remainingSerializedMarketRows, directAuthorId, directAuthorName ); const fallbackMarketRow = mergeMarketDataRows(serializedMarketRow, vueMarketRow); const exportFields = mergeExportFieldMaps( readExportFieldsForDivGridRow(allHeaderCells, alignedRowCells), fallbackMarketRow?.exportFields ); const authorId = directAuthorId || fallbackMarketRow?.authorId || ""; const authorName = directAuthorName || fallbackMarketRow?.authorName || ""; const price21To60s = mergeNonEmptyString( readDivGridPriceDisplay(priceCells[index]?.textContent), fallbackMarketRow?.price21To60s ); selectionCheckbox.dataset.marketSelectionAuthorId = authorId; return [ { authorId, authorName, backendMetricsCells: backendMetricsCells as Record, exportFields, hasDirectRatesSource: fallbackMarketRow?.hasDirectRatesSource ?? false, location: fallbackMarketRow?.location, orderTargets: rowCells .map((cell) => { const container = cell.parentElement; if (!(container instanceof root.ownerDocument.defaultView!.HTMLElement)) { return null; } return { container, mode: "css", node: cell }; }) .filter((target): target is RowOrderTarget => target !== null), personalCell, price21To60s, rates: fallbackMarketRow?.rates, row: authorCell, selectionCheckbox, singleCell, visibilityTargets: rowCells } satisfies MarketRowDom ]; }); return { headerSelectionCheckbox, rows }; } function ensureSyntheticHeaderCell( header: HTMLElement, field: string, label: string ): HTMLElement { const existingCell = header.querySelector( `[data-market-header-cell="${field}"]` ) as HTMLElement | null; if (existingCell) { existingCell.textContent = label; return existingCell; } 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; } function ensureSyntheticRowCell(row: HTMLElement, field: string): HTMLElement { const existingCell = row.querySelector( `[data-market-row-cell="${field}"]` ) as HTMLElement | null; if (existingCell) { return existingCell; } 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; } function ensureDivHeaderCell( container: HTMLElement, templateCell: HTMLElement, field: string, label: string ): HTMLElement { const existingCell = container.querySelector( `[data-market-header-cell="${field}"]` ) as HTMLElement | null; if (existingCell) { existingCell.textContent = label; applyPluginHeaderCellStyles(existingCell); return existingCell; } const nextCell = cloneElementShallow(templateCell); nextCell.dataset.marketHeaderCell = field; nextCell.textContent = label; applyColumnWidth(nextCell, field); applyPluginHeaderCellStyles(nextCell); if (field === SELECTION_COLUMN_KEY) { container.insertBefore(nextCell, templateCell); } else { container.appendChild(nextCell); } return nextCell; } function ensureDivBodyColumn( container: HTMLElement, templateColumn: HTMLElement, field: string, rowCount: number ): HTMLElement { const existingColumn = container.querySelector( `[data-market-column-group="${field}"]` ) as HTMLElement | null; if (existingColumn) { syncDivColumnCells(existingColumn, templateColumn, field, rowCount); return existingColumn; } const nextColumn = cloneElementShallow(templateColumn); nextColumn.dataset.marketColumnGroup = field; applyColumnWidth(nextColumn, field); syncDivColumnCells(nextColumn, templateColumn, field, rowCount); if (field === SELECTION_COLUMN_KEY) { container.insertBefore(nextColumn, templateColumn); } else { container.appendChild(nextColumn); } return nextColumn; } function syncDivColumnCells( column: HTMLElement, templateColumn: HTMLElement, field: string, rowCount: number ): void { const currentCells = getDirectContentCells(column); while (currentCells.length > rowCount) { currentCells.pop()?.remove(); } const templateCells = getDirectContentCells(templateColumn); for (let index = 0; index < rowCount; index += 1) { const existingCell = getDirectContentCells(column)[index] ?? null; if (existingCell) { existingCell.dataset.marketRowCell = field; applyPluginContentCellStyles(existingCell); continue; } const templateCell = templateCells[index] ?? templateCells[templateCells.length - 1] ?? null; const nextCell = field === SELECTION_COLUMN_KEY ? templateCell ? createSelectionContentCell(templateCell) : createBareContentCell(column.ownerDocument) : templateCell ? cloneElementShallow(templateCell) : createBareContentCell(column.ownerDocument); nextCell.dataset.marketRowCell = field; applyColumnWidth(nextCell, field); applyPluginContentCellStyles(nextCell); nextCell.textContent = ""; column.appendChild(nextCell); } } function applyPluginHeaderCellStyles(cell: HTMLElement): void { cell.style.display = "flex"; cell.style.alignItems = "center"; cell.style.justifyContent = "normal"; cell.style.cursor = "pointer"; cell.style.whiteSpace = "nowrap"; } function applyPluginContentCellStyles(cell: HTMLElement): void { cell.style.display = "flex"; cell.style.alignItems = "center"; cell.style.justifyContent = "normal"; cell.style.paddingTop = "12px"; cell.style.paddingBottom = "12px"; cell.style.boxSizing = "border-box"; 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; } return "nodeType" in root && root.nodeType === 9 ? (root as Document) : null; } function readSyntheticHeaderLabels(header: HTMLElement): Record { return Array.from(header.querySelectorAll("[data-market-header-cell]")).reduce< Record >((labels, cell) => { if (!(cell instanceof header.ownerDocument.defaultView!.HTMLElement)) { return labels; } const field = cell.dataset.marketHeaderCell; if (!field) { return labels; } labels[field] = normalizeExportCellText(cell.textContent); return labels; }, {}); } function readSyntheticExportFields( row: HTMLElement, headerLabelsByField: Record ): Record { const exportFields: Record = {}; for (const cell of row.querySelectorAll("[data-market-field]")) { if (!(cell instanceof row.ownerDocument.defaultView!.HTMLElement)) { continue; } const field = cell.dataset.marketField; const headerLabel = field ? headerLabelsByField[field] : ""; if (!shouldExportColumn(headerLabel)) { continue; } exportFields[headerLabel] = normalizeExportCellText(cell.textContent); } return exportFields; } function readExportFieldsForDivGridRow( headerCells: HTMLElement[], rowCells: Array ): Record { const exportFields: Record = {}; rowCells.forEach((cell, index) => { const headerLabel = normalizeExportCellText(headerCells[index]?.textContent); if (!shouldExportColumn(headerLabel)) { return; } exportFields[headerLabel] = headerLabel === "21-60s报价" ? readDivGridPriceDisplay(cell?.textContent) ?? "" : normalizeExportCellText(cell?.textContent); }); return exportFields; } function findPreviousHeaderCell(cell: HTMLElement): HTMLElement | null { let current = cell.previousElementSibling; while (current) { if ( current instanceof cell.ownerDocument.defaultView!.HTMLElement && current.classList.contains("header-cell") ) { return current; } current = current.previousElementSibling; } return null; } function findPreviousColumn(column: HTMLElement): HTMLElement | null { let current = column.previousElementSibling; while (current) { if ( current instanceof column.ownerDocument.defaultView!.HTMLElement && current.classList.contains("content-column") ) { return current; } current = current.previousElementSibling; } return null; } function ensurePluginSection( rootSection: HTMLElement, referenceSection: HTMLElement, options: { testId: string; type: "header" | "body"; } ): HTMLElement { const existingSection = rootSection.querySelector( `[data-market-plugin-section="${options.type}"]` ) as HTMLElement | null; if (existingSection) { existingSection.dataset.testid = options.testId; existingSection.setAttribute("data-testid", options.testId); return existingSection; } const templateSection = findPreviousSection(referenceSection) ?? referenceSection; const nextSection = cloneElementShallow(templateSection); nextSection.dataset.marketPluginSection = options.type; nextSection.dataset.testid = options.testId; nextSection.setAttribute("data-testid", options.testId); resetStickySectionStyles(nextSection); rootSection.insertBefore(nextSection, referenceSection); return nextSection; } function ensureVisibleHorizontalScroll(section: HTMLElement): void { ensureVisibleScrollbarStyles(section.ownerDocument); section.classList.remove("hide-scrollbar"); section.dataset.marketScrollbar = "visible"; section.style.overflowX = "auto"; section.style.scrollbarWidth = "thin"; section.style.scrollbarColor = "rgba(148, 163, 184, 0.95) rgba(226, 232, 240, 0.9)"; } function ensureVisibleScrollbarStyles(document: Document): void { if (document.getElementById(MARKET_SCROLLBAR_STYLE_ID)) { return; } const style = document.createElement("style"); style.id = MARKET_SCROLLBAR_STYLE_ID; style.textContent = ` [data-market-scrollbar="visible"]::-webkit-scrollbar { display: block !important; height: 10px !important; } [data-market-scrollbar="visible"]::-webkit-scrollbar-track { background: rgba(226, 232, 240, 0.9) !important; border-radius: 999px; } [data-market-scrollbar="visible"]::-webkit-scrollbar-thumb { background: rgba(148, 163, 184, 0.95) !important; border: 2px solid rgba(226, 232, 240, 0.9); border-radius: 999px; } `; document.head.appendChild(style); } function ensureScrollHint(root: HTMLElement, headerSection: HTMLElement): void { const existingHint = root.querySelector( '[data-testid="market-scroll-hint"]' ) as HTMLElement | null; if (existingHint) { existingHint.textContent = MARKET_SCROLL_HINT_TEXT; return; } const hint = root.ownerDocument.createElement("div"); hint.dataset.testid = "market-scroll-hint"; hint.setAttribute("data-testid", "market-scroll-hint"); hint.textContent = MARKET_SCROLL_HINT_TEXT; hint.style.color = "#64748b"; hint.style.display = "flex"; hint.style.fontSize = "12px"; hint.style.justifyContent = "flex-end"; hint.style.lineHeight = "18px"; hint.style.padding = "0 12px 8px"; root.insertBefore(hint, headerSection); } function findPreviousSection(section: HTMLElement): HTMLElement | null { let current = section.previousElementSibling; while (current) { if (current instanceof section.ownerDocument.defaultView!.HTMLElement) { return current; } current = current.previousElementSibling; } return null; } function findPreviousNativeSection(section: HTMLElement): HTMLElement | null { let current = section.previousElementSibling; while (current) { if ( current instanceof section.ownerDocument.defaultView!.HTMLElement && !current.hasAttribute("data-market-plugin-section") ) { return current; } current = current.previousElementSibling; } return null; } function resetStickySectionStyles(section: HTMLElement): void { section.style.position = ""; section.style.left = ""; section.style.right = ""; section.style.zIndex = ""; section.style.width = ""; section.style.minWidth = ""; } function getActionColumn(bodySection: HTMLElement): HTMLElement | null { const columns = getDirectContentColumns(bodySection); return columns[columns.length - 1] ?? null; } function getNativeAuthorColumn(authorSection: HTMLElement): HTMLElement | null { return ( getDirectContentColumns(authorSection).find( (column) => !column.dataset.marketColumnGroup && getDirectContentCells(column).some( (cell) => cell.querySelector("a") || cell.querySelector(".author-nickname") || Boolean(cell.dataset.authorId) ) ) ?? null ); } function getDirectHeaderCells(section: Element): HTMLElement[] { return Array.from(section.querySelectorAll(".header-cell")).filter( (cell): cell is HTMLElement => cell instanceof section.ownerDocument.defaultView!.HTMLElement ); } function getDirectContentColumns(section: Element): HTMLElement[] { return Array.from(section.children).filter( (child): child is HTMLElement => child instanceof section.ownerDocument.defaultView!.HTMLElement && child.classList.contains("content-column") ); } function getDirectContentCells(column: Element): HTMLElement[] { return Array.from(column.children).filter( (child): child is HTMLElement => child instanceof column.ownerDocument.defaultView!.HTMLElement && child.classList.contains("content-cell") ); } function getDirectChildIndex(root: HTMLElement, descendant: HTMLElement): number { const directChild = Array.from(root.children).find((child) => child.contains(descendant)); return directChild ? Array.from(root.children).indexOf(directChild) : -1; } function getIndexedChild(root: HTMLElement, index: number): HTMLElement | null { if (index < 0) { return null; } const child = root.children[index] ?? null; return child instanceof root.ownerDocument.defaultView!.HTMLElement ? child : null; } function findCellByText(cells: HTMLElement[], text: string): HTMLElement | null { return cells.find((cell) => cell.textContent?.trim() === text) ?? null; } function cloneElementShallow(reference: HTMLElement): HTMLElement { const clone = reference.ownerDocument.createElement(reference.tagName); Array.from(reference.attributes).forEach((attribute) => { clone.setAttribute(attribute.name, attribute.value); }); return clone; } function createBareContentCell(document: Document): HTMLElement { const cell = document.createElement("div"); cell.className = "content-cell"; return cell; } function createSelectionContentCell(templateCell: HTMLElement): HTMLElement { const cell = cloneElementShallow(templateCell); cell.removeAttribute("data-testid"); cell.removeAttribute("data-author-id"); return cell; } function extractAuthorId(authorCell: HTMLElement): string { const explicitAuthorId = authorCell.dataset.authorId; if (explicitAuthorId) { return explicitAuthorId; } const linkedAuthorId = Array.from(authorCell.querySelectorAll("a")) .map((link) => extractAuthorIdFromHref((link as HTMLAnchorElement).href)) .find((value): value is string => Boolean(value)); if (linkedAuthorId) { return linkedAuthorId; } const fallbackAuthorId = authorCell .querySelector("[data-author-id]") ?.getAttribute("data-author-id"); return fallbackAuthorId ?? ""; } function extractAuthorName(authorCell: HTMLElement): string { return ( authorCell.querySelector(".author-nickname")?.textContent?.trim() ?? authorCell.textContent?.trim() ?? "" ); } function extractAuthorIdFromHref(href: string): string | null { const match = href.match(/\/author-homepage\/[^/]+\/(\d+)/); return match?.[1] ?? null; } function readVueMarketRows( marketRoot: HTMLElement ): MarketDataRow[] { const vueRoot = ( marketRoot as HTMLElement & { __vue__?: { $children?: unknown[]; _setupState?: Record; }; } ).__vue__; const setupStates = collectVueSetupStates(vueRoot); for (const setupState of setupStates) { for (const value of Object.values(setupState)) { const candidate = unwrapVueRef(value); if (!candidate || typeof candidate !== "object") { continue; } const marketList = unwrapVueRef( (candidate as Record).marketList ); if (!Array.isArray(marketList)) { continue; } return marketList.map((row) => { const record = isRecord(row) ? row : {}; const attributeDatas = readMarketAttributeDatas(record); const singleVideoAfterSearchRate = normalizeMarketListRate( readMarketFieldValue(record, attributeDatas, "avg_search_after_view_rate_30d") ); return { authorId: readString(readMarketFieldValue(record, attributeDatas, "star_id")) ?? readString(readMarketFieldValue(record, attributeDatas, "id")) ?? "", authorName: readString(readMarketFieldValue(record, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(record, attributeDatas, "nick_name")) ?? "", exportFields: buildMarketExportFieldFallbacks(record, attributeDatas), hasDirectRatesSource: true, location: readMarketLocation(record, attributeDatas), price21To60s: readMarketPrice21To60s(record, attributeDatas), rates: singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : undefined }; }); } } return []; } function collectVueSetupStates( vueRoot: | { $children?: unknown[]; _setupState?: Record; } | undefined ): Array> { if (!vueRoot) { return []; } const queue: unknown[] = [vueRoot]; const setupStates: Array> = []; while (queue.length > 0) { const current = queue.shift(); if (!isRecord(current)) { continue; } if (isRecord(current._setupState)) { setupStates.push(current._setupState); } const children = Array.isArray(current.$children) ? current.$children : []; queue.push(...children); } return setupStates; } function readSerializedMarketRows( document: Document ): MarketDataRow[] { const serializedRows = document.documentElement.getAttribute( SERIALIZED_MARKET_ROWS_ATTRIBUTE ); if (!serializedRows) { return []; } try { const parsedRows = JSON.parse(serializedRows); if (!Array.isArray(parsedRows)) { return []; } return parsedRows .map((row) => { const record = isRecord(row) ? row : {}; const singleVideoAfterSearchRate = readString( record.singleVideoAfterSearchRate ); return { authorId: readString(record.authorId) ?? "", authorName: readString(record.authorName) ?? "", exportFields: readSerializedExportFields(record), hasDirectRatesSource: Boolean(singleVideoAfterSearchRate), location: readString(record.location) ?? undefined, price21To60s: readString(record.price21To60s) ?? undefined, rates: singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : undefined }; }) .filter((row) => Boolean(row.authorId || row.authorName)); } catch { return []; } } function unwrapVueRef(value: unknown): unknown { if (isRecord(value) && "value" in value) { return value.value; } return value; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function readMarketAttributeDatas( record: Record ): Record { return isRecord(record.attribute_datas) ? record.attribute_datas : {}; } function readMarketFieldValue( record: Record, attributeDatas: Record, field: string ): unknown { return record[field] ?? attributeDatas[field]; } function readString(value: unknown): string | null { return typeof value === "string" ? value : null; } function normalizeMarketListRate(value: unknown): string | null { if (typeof value === "number") { return normalizeFractionRateDisplay(String(value)); } return typeof value === "string" ? normalizeFractionRateDisplay(value) : null; } function normalizeExportCellText(value: string | null | undefined): string { return value?.replace(/\s+/g, " ").trim() ?? ""; } function readDivGridPriceDisplay(value: string | null | undefined): string | undefined { const normalizedValue = normalizeExportCellText(value); if (!normalizedValue) { return undefined; } const match = normalizedValue.match(/^¥?\s*([\d,]+(?:\.\d+)?)$/); if (!match) { return undefined; } const numericValue = Number(match[1].replace(/,/g, "")); if (!Number.isFinite(numericValue)) { return undefined; } return formatCurrencyValue(numericValue); } function shouldExportColumn(label: string): boolean { const excludedBackendLabels = new Set(BACKEND_METRIC_COLUMNS.map((column) => column.label)); return Boolean( label && label !== ACTION_HEADER_TEXT && label !== BACKEND_HEADER_TEXT && !excludedBackendLabels.has(label) && label !== "单视频看后搜率" && label !== "个人视频看后搜率" ); } function syncSortableHeaderCell( cell: HTMLElement, options: { direction: MarketSortState["direction"] | "none"; field: MarketSortField; onToggleSort: (field: MarketSortField) => void; } ): void { const label = readSortableHeaderLabel(cell); const sorterRoot = ensureHeaderSorterRoot(cell); const text = ensureHeaderSorterText(sorterRoot); const icon = ensureHeaderSorterIcon(sorterRoot); const upTriangle = ensureHeaderTriangle(icon, "up"); const downTriangle = ensureHeaderTriangle(icon, "down"); text.textContent = label; cell.dataset.marketSortField = options.field; cell.dataset.marketSortDirection = options.direction; cell.setAttribute("role", "button"); cell.tabIndex = 0; cell.onclick = () => { options.onToggleSort(options.field); }; cell.onkeydown = (event) => { if (event.key !== "Enter" && event.key !== " ") { return; } event.preventDefault(); options.onToggleSort(options.field); }; syncTriangleStyles(upTriangle, { active: options.direction === "asc", direction: "up" }); syncTriangleStyles(downTriangle, { active: options.direction === "desc", direction: "down" }); } function readSortableHeaderLabel(cell: HTMLElement): string { return ( cell.dataset.marketHeaderLabel ?? normalizeExportCellText(cell.textContent) ?? "" ); } function ensureHeaderSorterRoot(cell: HTMLElement): HTMLElement { const existingRoot = cell.querySelector( '[data-market-sorter="root"]' ) as HTMLElement | null; if (existingRoot) { return existingRoot; } cell.dataset.marketHeaderLabel = normalizeExportCellText(cell.textContent); cell.replaceChildren(); const root = cell.ownerDocument.createElement("span"); root.dataset.marketSorter = "root"; root.style.alignItems = "center"; root.style.display = "inline-flex"; root.style.gap = "4px"; root.style.maxWidth = "100%"; cell.appendChild(root); return root; } function ensureHeaderSorterText(sorterRoot: HTMLElement): HTMLElement { const existingText = sorterRoot.querySelector( '[data-market-sorter="text"]' ) as HTMLElement | null; if (existingText) { return existingText; } const text = sorterRoot.ownerDocument.createElement("span"); text.dataset.marketSorter = "text"; text.style.display = "inline-block"; text.style.lineHeight = "20px"; text.style.whiteSpace = "nowrap"; sorterRoot.appendChild(text); return text; } function ensureHeaderSorterIcon(sorterRoot: HTMLElement): HTMLElement { const existingIcon = sorterRoot.querySelector( '[data-market-sorter="icon"]' ) as HTMLElement | null; if (existingIcon) { return existingIcon; } const icon = sorterRoot.ownerDocument.createElement("span"); icon.dataset.marketSorter = "icon"; icon.style.display = "inline-flex"; icon.style.flexDirection = "column"; icon.style.gap = "2px"; icon.style.justifyContent = "center"; icon.style.minWidth = "8px"; sorterRoot.appendChild(icon); return icon; } function ensureHeaderTriangle( iconRoot: HTMLElement, direction: "up" | "down" ): HTMLElement { const existingTriangle = iconRoot.querySelector( `[data-market-sorter-triangle="${direction}"]` ) as HTMLElement | null; if (existingTriangle) { return existingTriangle; } const triangle = iconRoot.ownerDocument.createElement("span"); triangle.dataset.marketSorterTriangle = direction; triangle.style.display = "block"; triangle.style.height = "0"; triangle.style.width = "0"; triangle.style.borderLeft = "4px solid transparent"; triangle.style.borderRight = "4px solid transparent"; iconRoot.appendChild(triangle); return triangle; } function syncTriangleStyles( triangle: HTMLElement, options: { active: boolean; direction: "up" | "down"; } ): void { const activeColor = "#1f2329"; const inactiveColor = "#c9cdd4"; if (options.direction === "up") { triangle.style.borderBottom = `5px solid ${ options.active ? activeColor : inactiveColor }`; triangle.style.borderTop = "0 solid transparent"; } else { triangle.style.borderTop = `5px solid ${ options.active ? activeColor : inactiveColor }`; triangle.style.borderBottom = "0 solid transparent"; } } function readRateCellText(value: string | undefined): string { return value ? normalizeRateDisplay(value) : UNAVAILABLE_RATE_TEXT; } 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"; } if (field === SINGLE_COLUMN_KEY || field === PERSONAL_COLUMN_KEY) { element.style.minWidth = "160px"; element.style.width = "160px"; } if (BACKEND_METRIC_COLUMNS.some((column) => column.field === field)) { element.style.minWidth = "120px"; element.style.width = "120px"; } } function syncContainerWidth(container: Element | null): void { if (!(container instanceof HTMLElement)) { return; } const directChildren = Array.from(container.children).filter( (child): child is HTMLElement => child instanceof HTMLElement ); const totalWidth = directChildren.reduce((sum, child) => { return sum + readElementWidth(child); }, 0); if (totalWidth <= 0) { return; } container.style.width = `${totalWidth}px`; container.style.minWidth = `${totalWidth}px`; } function readElementWidth(element: HTMLElement): number { const styleWidth = Number.parseFloat(element.style.width || ""); if (Number.isFinite(styleWidth) && styleWidth > 0) { return styleWidth; } const minWidth = Number.parseFloat(element.style.minWidth || ""); if (Number.isFinite(minWidth) && minWidth > 0) { return minWidth; } return 0; } function mergeMarketDataRows( baseRow: MarketDataRow | null, preferredRow: MarketDataRow | null ): MarketDataRow | null { if (!baseRow && !preferredRow) { return null; } if (!baseRow) { return preferredRow; } if (!preferredRow) { return baseRow; } return { authorId: preferredRow.authorId || baseRow.authorId, authorName: preferredRow.authorName || baseRow.authorName, exportFields: mergeExportFieldMaps(baseRow.exportFields, preferredRow.exportFields), hasDirectRatesSource: preferredRow.hasDirectRatesSource || baseRow.hasDirectRatesSource, location: mergeNonEmptyString(baseRow.location, preferredRow.location), price21To60s: mergeNonEmptyString( baseRow.price21To60s, preferredRow.price21To60s ), rates: mergeRates(baseRow.rates, preferredRow.rates) }; } function takeMatchedMarketDataRow( remainingRows: MarketDataRow[], authorId: string, authorName: string ): MarketDataRow | null { if (remainingRows.length === 0) { return null; } const matchedIndex = remainingRows.findIndex((row) => { if (authorId && row.authorId === authorId) { return true; } if (authorName && row.authorName === authorName) { return true; } return false; }); if (matchedIndex >= 0) { return remainingRows.splice(matchedIndex, 1)[0] ?? null; } if (!authorId && !authorName) { return remainingRows.shift() ?? null; } return null; } function mergeExportFieldMaps( current: Record | undefined, fallback: Record | undefined ): Record | undefined { if (!current && !fallback) { return undefined; } const nextFields: Record = { ...(current ?? {}) }; Object.entries(fallback ?? {}).forEach(([key, value]) => { if (!hasTextValue(nextFields[key]) && hasTextValue(value)) { nextFields[key] = value; } }); return nextFields; } function mergeRates( current: AfterSearchRates | undefined, fallback: AfterSearchRates | undefined ): AfterSearchRates | undefined { if (!current && !fallback) { return undefined; } return { singleVideoAfterSearchRate: current?.singleVideoAfterSearchRate ?? fallback?.singleVideoAfterSearchRate, personalVideoAfterSearchRate: current?.personalVideoAfterSearchRate ?? fallback?.personalVideoAfterSearchRate }; } function mergeNonEmptyString( current: string | undefined, fallback: string | undefined ): string | undefined { return hasTextValue(current) ? current : fallback; } function buildMarketExportFieldFallbacks( record: Record, attributeDatas: Record ): Record | undefined { const exportFields: Record = {}; const authorInfo = buildMarketAuthorInfo(record, attributeDatas); const authorType = buildMarketAuthorType(record, attributeDatas); const contentTheme = buildMarketContentTheme(record, attributeDatas); const connectedUsers = formatWanValue( readNumericValue(readMarketFieldValue(record, attributeDatas, "link_link_cnt_by_industry")) ); const followerCount = formatWanValue( readNumericValue(readMarketFieldValue(record, attributeDatas, "follower")) ); const expectedCpm = formatDecimalDisplay( readNumericValue(readMarketFieldValue(record, attributeDatas, "prospective_20_60_cpm")) ); const expectedPlayCount = formatWanValue( readNumericValue(readMarketFieldValue(record, attributeDatas, "expected_play_num")) ); const interactionRate = formatFractionPercent( readNumericValue(readMarketFieldValue(record, attributeDatas, "interact_rate_within_30d")) ); const finishRate = formatFractionPercent( readNumericValue(readMarketFieldValue(record, attributeDatas, "play_over_rate_within_30d")) ); const burstRate = readBurstRateDisplay( readNumericValue(readMarketFieldValue(record, attributeDatas, "burst_text_rate")) ); const price21To60s = readMarketPrice21To60s(record, attributeDatas); const representativeVideo = readMarketRepresentativeVideo(record, attributeDatas); assignExportField(exportFields, "达人信息", authorInfo); assignExportField(exportFields, "代表视频", representativeVideo); assignExportField(exportFields, "达人类型", authorType); assignExportField(exportFields, "内容主题", contentTheme); assignExportField(exportFields, "连接用户数", connectedUsers); assignExportField(exportFields, "粉丝数", followerCount); assignExportField(exportFields, "预期CPM", expectedCpm); assignExportField(exportFields, "预期播放量", expectedPlayCount); assignExportField(exportFields, "互动率", interactionRate); assignExportField(exportFields, "完播率", finishRate); assignExportField(exportFields, "爆文率", burstRate); assignExportField(exportFields, "21-60s报价", price21To60s); return Object.keys(exportFields).length > 0 ? exportFields : undefined; } function assignExportField( exportFields: Record, key: string, value: string | undefined ): void { if (hasTextValue(value)) { exportFields[key] = value; } } function buildMarketAuthorInfo( record: Record, attributeDatas: Record ): string | undefined { const nickname = readString(readMarketFieldValue(record, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(record, attributeDatas, "nick_name")) ?? ""; const parts = [ nickname, readMarketGenderLabel(readMarketFieldValue(record, attributeDatas, "gender")), readString(readMarketFieldValue(record, attributeDatas, "city")) ?? "" ].filter((value) => Boolean(value)); return parts.length > 0 ? parts.join(" ") : undefined; } function buildMarketAuthorType( record: Record, attributeDatas: Record ): string | undefined { const tagsRelation = readRecordLike( readMarketFieldValue(record, attributeDatas, "tags_relation") ); if (tagsRelation) { const primaryTag = Object.keys(tagsRelation)[0]; if (hasTextValue(primaryTag)) { return primaryTag; } } return undefined; } function buildMarketContentTheme( record: Record, attributeDatas: Record ): string | undefined { const themes = readStringArray( readMarketFieldValue(record, attributeDatas, "content_theme_labels_180d") ); if (themes.length === 0) { return undefined; } if (themes.length <= 2) { return themes.join(" "); } return `${themes.slice(0, 2).join(" ")} ${themes.length - 2}+`; } function readMarketLocation( record: Record, attributeDatas: Record ): string | undefined { return readString(readMarketFieldValue(record, attributeDatas, "city")) ?? undefined; } function readMarketPrice21To60s( record: Record, attributeDatas: Record ): string | undefined { return formatCurrencyValue( readNumericValue(readMarketFieldValue(record, attributeDatas, "price_20_60")) ); } function readMarketRepresentativeVideo( record: Record, attributeDatas: Record ): string | undefined { const items = readArrayLike(readMarketFieldValue(record, attributeDatas, "items")); for (const item of items) { if (!isRecord(item)) { continue; } const title = readString(item.title); if (hasTextValue(title)) { return normalizeExportCellText(title); } } return undefined; } function readMarketGenderLabel(value: unknown): string | undefined { const rawValue = typeof value === "number" ? String(value) : readString(value); if (rawValue === "1") { return "男"; } if (rawValue === "2") { return "女"; } return undefined; } function readBurstRateDisplay(value: number | null): string | undefined { if (value === null) { return undefined; } if (value <= 0) { return "-"; } return formatFractionPercent(value); } function formatCurrencyValue(value: number | null): string | undefined { if (value === null) { return undefined; } return `¥${value.toLocaleString("en-US", { maximumFractionDigits: 0 })}`; } function formatWanValue(value: number | null): string | undefined { if (value === null) { return undefined; } return `${formatDecimalWithGrouping(value / 10000)}w`; } function formatFractionPercent(value: number | null): string | undefined { if (value === null) { return undefined; } return `${formatDecimalDisplay(value * 100)}%`; } function formatDecimalDisplay(value: number | null): string | undefined { if (value === null) { return undefined; } return value.toLocaleString("en-US", { maximumFractionDigits: 1, minimumFractionDigits: 0, useGrouping: false }); } function formatDecimalWithGrouping(value: number): string { return value.toLocaleString("en-US", { maximumFractionDigits: 1, minimumFractionDigits: 0 }); } function readNumericValue(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string") { const trimmedValue = value.trim(); if (!trimmedValue) { return null; } const parsedValue = Number(trimmedValue); return Number.isFinite(parsedValue) ? parsedValue : null; } return null; } function readStringArray(value: unknown): string[] { if (Array.isArray(value)) { return value.filter((item): item is string => typeof item === "string"); } if (typeof value === "string") { try { const parsedValue = JSON.parse(value); return Array.isArray(parsedValue) ? parsedValue.filter((item): item is string => typeof item === "string") : []; } catch { return []; } } return []; } function readArrayLike(value: unknown): unknown[] { if (Array.isArray(value)) { return value; } if (typeof value === "string") { try { const parsedValue = JSON.parse(value); return Array.isArray(parsedValue) ? parsedValue : []; } catch { return []; } } return []; } function readRecordLike(value: unknown): Record | null { if (isRecord(value)) { return value; } if (typeof value === "string") { try { const parsedValue = JSON.parse(value); return isRecord(parsedValue) ? parsedValue : null; } catch { return null; } } return null; } function readSerializedExportFields( record: Record ): Record | undefined { if (!isRecord(record.exportFields)) { return undefined; } const entries = Object.entries(record.exportFields).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : [] ); return entries.length > 0 ? Object.fromEntries(entries) : undefined; } function hasTextValue(value: string | undefined): boolean { return typeof value === "string" && value.trim().length > 0; } function renderBackendMetricsCells( cells: Record, record: MarketRecord ): void { if ( record.backendMetricsStatus === "loading" || (record.status === "loading" && !record.backendMetricsStatus) ) { fillBackendMetricCells(cells, "加载中..."); return; } if (record.backendMetricsStatus === "failed") { fillBackendMetricCells(cells, "加载失败"); return; } if (record.backendMetricsStatus === "missing") { fillBackendMetricCells(cells, UNAVAILABLE_BACKEND_METRICS_TEXT); return; } if (record.backendMetricsStatus !== "success" || !record.backendMetrics) { fillBackendMetricCells(cells, ""); return; } BACKEND_METRIC_COLUMNS.forEach(({ field }) => { cells[field].textContent = record.backendMetrics?.[field] ?? ""; }); } function fillBackendMetricCells( cells: Record, value: string ): void { BACKEND_METRIC_COLUMNS.forEach(({ field }) => { cells[field].textContent = value; }); }