4476 lines
156 KiB
JavaScript
4476 lines
156 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
// src/shared/rate-normalizer.ts
|
|
function normalizeRateDisplay(value) {
|
|
const trimmedValue = value.trim();
|
|
const rangeMatch = trimmedValue.match(
|
|
/^([0-9]+(?:\.[0-9]+)?)\s*%?\s*-\s*([0-9]+(?:\.[0-9]+)?)\s*%$/
|
|
);
|
|
if (rangeMatch) {
|
|
const [, lowerBound, upperBound] = rangeMatch;
|
|
return `${lowerBound}% - ${upperBound}%`;
|
|
}
|
|
return trimmedValue.replace(/\s+/g, "");
|
|
}
|
|
function normalizeFractionRateDisplay(value) {
|
|
const numericValue = Number(value);
|
|
if (!Number.isFinite(numericValue)) {
|
|
return null;
|
|
}
|
|
const percentageValue = numericValue * 100;
|
|
return `${trimTrailingZeros(percentageValue.toFixed(6))}%`;
|
|
}
|
|
function parseRateLowerBound(value) {
|
|
const comparableRate = toComparableRate(value);
|
|
return comparableRate?.numeric ?? null;
|
|
}
|
|
function compareRateValues(leftValue, rightValue) {
|
|
const leftComparable = toComparableRate(leftValue);
|
|
const rightComparable = toComparableRate(rightValue);
|
|
if (!leftComparable && !rightComparable) {
|
|
return 0;
|
|
}
|
|
if (!leftComparable) {
|
|
return 1;
|
|
}
|
|
if (!rightComparable) {
|
|
return -1;
|
|
}
|
|
if (leftComparable.numeric !== rightComparable.numeric) {
|
|
return leftComparable.numeric - rightComparable.numeric;
|
|
}
|
|
if (leftComparable.isLessThan === rightComparable.isLessThan) {
|
|
return 0;
|
|
}
|
|
return leftComparable.isLessThan ? -1 : 1;
|
|
}
|
|
function toComparableRate(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const normalizedValue = normalizeRateDisplay(value);
|
|
const lessThanMatch = normalizedValue.match(/^<\s*([0-9]+(?:\.[0-9]+)?)%$/);
|
|
if (lessThanMatch) {
|
|
return {
|
|
isLessThan: true,
|
|
numeric: Number(lessThanMatch[1])
|
|
};
|
|
}
|
|
const rangeMatch = normalizedValue.match(
|
|
/^([0-9]+(?:\.[0-9]+)?)%\s*-\s*([0-9]+(?:\.[0-9]+)?)%$/
|
|
);
|
|
if (rangeMatch) {
|
|
return {
|
|
isLessThan: false,
|
|
numeric: Number(rangeMatch[1])
|
|
};
|
|
}
|
|
const exactMatch = normalizedValue.match(/^([0-9]+(?:\.[0-9]+)?)%$/);
|
|
if (exactMatch) {
|
|
return {
|
|
isLessThan: false,
|
|
numeric: Number(exactMatch[1])
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function trimTrailingZeros(value) {
|
|
return value.replace(/\.?0+$/, "");
|
|
}
|
|
|
|
// src/shared/csv.ts
|
|
function escapeCsvCell(value) {
|
|
if (/[",\n]/.test(value)) {
|
|
return `"${value.replace(/"/g, '""')}"`;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// src/content/market/csv-exporter.ts
|
|
var FALLBACK_BASE_COLUMNS = [
|
|
{
|
|
header: "\u8FBE\u4EBAID",
|
|
readValue: (record) => record.authorId
|
|
},
|
|
{
|
|
header: "\u8FBE\u4EBA\u540D\u79F0",
|
|
readValue: (record) => record.authorName
|
|
},
|
|
{
|
|
header: "\u5730\u533A",
|
|
readValue: (record) => record.location ?? ""
|
|
},
|
|
{
|
|
header: "21-60s\u62A5\u4EF7",
|
|
readValue: (record) => record.price21To60s ?? ""
|
|
}
|
|
];
|
|
var RATE_COLUMNS = [
|
|
{
|
|
header: "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387",
|
|
readValue: (record) => record.rates?.singleVideoAfterSearchRate ? normalizeRateDisplay(record.rates.singleVideoAfterSearchRate) : ""
|
|
},
|
|
{
|
|
header: "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387",
|
|
readValue: (record) => record.rates?.personalVideoAfterSearchRate ? normalizeRateDisplay(record.rates.personalVideoAfterSearchRate) : ""
|
|
}
|
|
];
|
|
var BACKEND_METRIC_COLUMNS = [
|
|
{
|
|
header: "\u770B\u540E\u641C\u7387",
|
|
readValue: (record) => record.backendMetrics?.afterViewSearchRate ?? ""
|
|
},
|
|
{
|
|
header: "\u770B\u540E\u641C\u6570",
|
|
readValue: (record) => record.backendMetrics?.afterViewSearchCount ?? ""
|
|
},
|
|
{
|
|
header: "\u65B0\u589EA3\u6570",
|
|
readValue: (record) => record.backendMetrics?.a3IncreaseCount ?? ""
|
|
},
|
|
{
|
|
header: "\u65B0\u589EA3\u7387",
|
|
readValue: (record) => record.backendMetrics?.newA3Rate ?? ""
|
|
},
|
|
{
|
|
header: "CPA3",
|
|
readValue: (record) => record.backendMetrics?.cpa3 ?? ""
|
|
},
|
|
{
|
|
header: "cp_search",
|
|
readValue: (record) => record.backendMetrics?.cpSearch ?? ""
|
|
}
|
|
];
|
|
function buildMarketCsv(records) {
|
|
const baseColumns = buildBaseColumns(records);
|
|
const csvColumns = [...baseColumns, ...RATE_COLUMNS, ...BACKEND_METRIC_COLUMNS];
|
|
const headerLine = csvColumns.map((column) => column.header).join(",");
|
|
const rowLines = records.map(
|
|
(record) => csvColumns.map((column) => escapeCsvCell(column.readValue(record))).join(",")
|
|
);
|
|
return [headerLine, ...rowLines].join("\n");
|
|
}
|
|
function buildBaseColumns(records) {
|
|
const orderedHeaders = [];
|
|
const seenHeaders = /* @__PURE__ */ new Set();
|
|
const excludedHeaders = /* @__PURE__ */ new Set(["\u4EE3\u8868\u89C6\u9891"]);
|
|
records.forEach((record) => {
|
|
Object.keys(record.exportFields ?? {}).forEach((header) => {
|
|
if (seenHeaders.has(header) || excludedHeaders.has(header)) {
|
|
return;
|
|
}
|
|
seenHeaders.add(header);
|
|
orderedHeaders.push(header);
|
|
});
|
|
});
|
|
if (orderedHeaders.length === 0) {
|
|
return FALLBACK_BASE_COLUMNS;
|
|
}
|
|
return orderedHeaders.map((header) => ({
|
|
header,
|
|
readValue: (record) => record.exportFields?.[header] ?? ""
|
|
}));
|
|
}
|
|
|
|
// src/content/market/batch-name-dialog.ts
|
|
var DIALOG_STYLE_ID = "sces-batch-name-dialog-style";
|
|
var activeDialogs = /* @__PURE__ */ new WeakMap();
|
|
function promptForBatchName(document2) {
|
|
const existingDialog = activeDialogs.get(document2);
|
|
if (existingDialog) {
|
|
existingDialog.input.focus();
|
|
existingDialog.input.select();
|
|
return existingDialog.promise;
|
|
}
|
|
ensureDialogStyles(document2);
|
|
const dialogRoot = document2.createElement("div");
|
|
dialogRoot.dataset.pluginBatchNameDialog = "root";
|
|
dialogRoot.setAttribute("role", "dialog");
|
|
dialogRoot.setAttribute("aria-modal", "true");
|
|
dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title");
|
|
applyOverlayStyles(dialogRoot);
|
|
const dialogPanel = document2.createElement("div");
|
|
applyPanelStyles(dialogPanel);
|
|
const title = document2.createElement("h2");
|
|
title.id = "sces-batch-name-title";
|
|
title.textContent = "\u63D0\u4EA4\u6279\u6B21";
|
|
applyTitleStyles(title);
|
|
const description = document2.createElement("p");
|
|
description.textContent = "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0\uFF0C\u4FBF\u4E8E\u540E\u7EED\u5728\u7CFB\u7EDF\u4E2D\u8BC6\u522B\u548C\u8FFD\u8E2A\u3002";
|
|
applyDescriptionStyles(description);
|
|
const input = document2.createElement("input");
|
|
input.type = "text";
|
|
input.dataset.pluginBatchNameInput = "input";
|
|
input.placeholder = "\u4F8B\u5982\uFF1A618\u8FBE\u4EBA\u7B5B\u9009\u7B2C\u4E00\u6279";
|
|
input.maxLength = 60;
|
|
applyInputStyles(input);
|
|
const errorText = document2.createElement("p");
|
|
errorText.dataset.pluginBatchNameError = "text";
|
|
applyErrorStyles(errorText);
|
|
const buttonRow = document2.createElement("div");
|
|
applyButtonRowStyles(buttonRow);
|
|
const cancelButton = document2.createElement("button");
|
|
cancelButton.type = "button";
|
|
cancelButton.dataset.pluginBatchNameCancel = "button";
|
|
cancelButton.textContent = "\u53D6\u6D88";
|
|
applySecondaryButtonStyles(cancelButton);
|
|
const confirmButton = document2.createElement("button");
|
|
confirmButton.type = "button";
|
|
confirmButton.dataset.pluginBatchNameConfirm = "button";
|
|
confirmButton.textContent = "\u786E\u8BA4\u63D0\u4EA4";
|
|
applyPrimaryButtonStyles(confirmButton);
|
|
buttonRow.append(cancelButton, confirmButton);
|
|
dialogPanel.append(title, description, input, errorText, buttonRow);
|
|
dialogRoot.appendChild(dialogPanel);
|
|
document2.body.appendChild(dialogRoot);
|
|
const dialogPromise = new Promise((resolve) => {
|
|
const closeDialog = (value) => {
|
|
activeDialogs.delete(document2);
|
|
dialogRoot.remove();
|
|
document2.removeEventListener("keydown", handleDocumentKeydown, true);
|
|
resolve(value);
|
|
};
|
|
const submitValue = () => {
|
|
const value = input.value.trim();
|
|
if (!value) {
|
|
errorText.textContent = "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0";
|
|
input.setAttribute("aria-invalid", "true");
|
|
input.focus();
|
|
return;
|
|
}
|
|
closeDialog(value);
|
|
};
|
|
const handleDocumentKeydown = (event) => {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
closeDialog(null);
|
|
return;
|
|
}
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
submitValue();
|
|
}
|
|
};
|
|
input.addEventListener("input", () => {
|
|
if (!input.value.trim()) {
|
|
return;
|
|
}
|
|
errorText.textContent = "";
|
|
input.removeAttribute("aria-invalid");
|
|
});
|
|
cancelButton.addEventListener("click", () => {
|
|
closeDialog(null);
|
|
});
|
|
confirmButton.addEventListener("click", () => {
|
|
submitValue();
|
|
});
|
|
dialogRoot.addEventListener("click", (event) => {
|
|
if (event.target === dialogRoot) {
|
|
closeDialog(null);
|
|
}
|
|
});
|
|
document2.addEventListener("keydown", handleDocumentKeydown, true);
|
|
});
|
|
activeDialogs.set(document2, {
|
|
input,
|
|
promise: dialogPromise
|
|
});
|
|
input.focus();
|
|
return dialogPromise;
|
|
}
|
|
function ensureDialogStyles(document2) {
|
|
if (document2.getElementById(DIALOG_STYLE_ID)) {
|
|
return;
|
|
}
|
|
const style = document2.createElement("style");
|
|
style.id = DIALOG_STYLE_ID;
|
|
style.textContent = `
|
|
[data-plugin-batch-name-dialog="root"] {
|
|
animation: sces-batch-name-fade-in 0.16s ease;
|
|
}
|
|
|
|
@keyframes sces-batch-name-fade-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
`;
|
|
document2.head.appendChild(style);
|
|
}
|
|
function applyOverlayStyles(root) {
|
|
root.style.position = "fixed";
|
|
root.style.inset = "0";
|
|
root.style.background = "rgba(15, 23, 42, 0.38)";
|
|
root.style.display = "flex";
|
|
root.style.alignItems = "center";
|
|
root.style.justifyContent = "center";
|
|
root.style.padding = "24px";
|
|
root.style.zIndex = "2147483647";
|
|
}
|
|
function applyPanelStyles(panel) {
|
|
panel.style.width = "min(420px, calc(100vw - 32px))";
|
|
panel.style.background = "#fffaf9";
|
|
panel.style.border = "1px solid rgba(127, 29, 45, 0.14)";
|
|
panel.style.borderRadius = "18px";
|
|
panel.style.boxShadow = "0 28px 70px rgba(15, 23, 42, 0.22)";
|
|
panel.style.padding = "24px";
|
|
panel.style.boxSizing = "border-box";
|
|
}
|
|
function applyTitleStyles(title) {
|
|
title.style.margin = "0";
|
|
title.style.color = "#4c0519";
|
|
title.style.fontSize = "20px";
|
|
title.style.fontWeight = "700";
|
|
title.style.lineHeight = "28px";
|
|
}
|
|
function applyDescriptionStyles(description) {
|
|
description.style.margin = "10px 0 0";
|
|
description.style.color = "#64748b";
|
|
description.style.fontSize = "13px";
|
|
description.style.lineHeight = "20px";
|
|
}
|
|
function applyInputStyles(input) {
|
|
input.style.width = "100%";
|
|
input.style.height = "42px";
|
|
input.style.marginTop = "18px";
|
|
input.style.padding = "0 14px";
|
|
input.style.boxSizing = "border-box";
|
|
input.style.border = "1px solid #d8c1c6";
|
|
input.style.borderRadius = "12px";
|
|
input.style.background = "#ffffff";
|
|
input.style.color = "#1f2937";
|
|
input.style.fontSize = "14px";
|
|
input.style.outline = "none";
|
|
}
|
|
function applyErrorStyles(errorText) {
|
|
errorText.style.minHeight = "20px";
|
|
errorText.style.margin = "8px 0 0";
|
|
errorText.style.color = "#b91c1c";
|
|
errorText.style.fontSize = "12px";
|
|
errorText.style.lineHeight = "18px";
|
|
}
|
|
function applyButtonRowStyles(buttonRow) {
|
|
buttonRow.style.display = "flex";
|
|
buttonRow.style.justifyContent = "flex-end";
|
|
buttonRow.style.gap = "10px";
|
|
buttonRow.style.marginTop = "18px";
|
|
}
|
|
function applySecondaryButtonStyles(button) {
|
|
button.style.height = "36px";
|
|
button.style.padding = "0 16px";
|
|
button.style.border = "1px solid #d7dde6";
|
|
button.style.borderRadius = "10px";
|
|
button.style.background = "#ffffff";
|
|
button.style.color = "#334155";
|
|
button.style.fontWeight = "600";
|
|
button.style.cursor = "pointer";
|
|
}
|
|
function applyPrimaryButtonStyles(button) {
|
|
button.style.height = "36px";
|
|
button.style.padding = "0 16px";
|
|
button.style.border = "1px solid #7f1d2d";
|
|
button.style.borderRadius = "10px";
|
|
button.style.background = "#7f1d2d";
|
|
button.style.color = "#ffffff";
|
|
button.style.fontWeight = "600";
|
|
button.style.cursor = "pointer";
|
|
}
|
|
|
|
// src/content/market/batch-payload.ts
|
|
function createBatchPayload(options) {
|
|
const logtoUserId = options.authState.userInfo?.sub?.trim();
|
|
if (!logtoUserId) {
|
|
throw new Error("batch submit user id unavailable");
|
|
}
|
|
const resource = options.authState.resource?.trim();
|
|
if (!resource) {
|
|
throw new Error("batch submit resource unavailable");
|
|
}
|
|
const batchName = options.batchName.trim();
|
|
if (!batchName) {
|
|
throw new Error("batch submit batch name is required");
|
|
}
|
|
return {
|
|
authors: options.records.map((record) => ({
|
|
authorId: record.authorId,
|
|
authorName: record.authorName,
|
|
...record.coreUserId ? { authorUid: record.coreUserId } : {}
|
|
})),
|
|
batchName,
|
|
createdAt: options.createdAt,
|
|
creatorName: options.authState.userInfo?.name ?? options.authState.userInfo?.username ?? logtoUserId,
|
|
logtoUserId,
|
|
resource
|
|
};
|
|
}
|
|
|
|
// src/content/market/market-list-row.ts
|
|
var PAGE_NUMBER_KEYS = [
|
|
"currentPage",
|
|
"page",
|
|
"pageNo",
|
|
"pageNum",
|
|
"page_no",
|
|
"page_num"
|
|
];
|
|
var PAGE_SIZE_KEYS = [
|
|
"limit",
|
|
"pageSize",
|
|
"page_size",
|
|
"size"
|
|
];
|
|
var TOTAL_COUNT_KEYS = [
|
|
"total",
|
|
"totalCount",
|
|
"total_count"
|
|
];
|
|
var TOTAL_PAGE_KEYS = [
|
|
"pageCount",
|
|
"page_count",
|
|
"totalPage",
|
|
"totalPages",
|
|
"total_page",
|
|
"total_pages"
|
|
];
|
|
function mapMarketListRow(row) {
|
|
const attributeDatas = readMarketAttributeDatas(row);
|
|
const singleVideoAfterSearchRate = normalizeMarketListRate(
|
|
readMarketFieldValue(row, attributeDatas, "avg_search_after_view_rate_30d")
|
|
);
|
|
return {
|
|
authorId: readString(readMarketFieldValue(row, attributeDatas, "star_id")) ?? readString(readMarketFieldValue(row, attributeDatas, "id")) ?? "",
|
|
authorName: readString(readMarketFieldValue(row, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(row, attributeDatas, "nick_name")) ?? "",
|
|
coreUserId: readString(readMarketFieldValue(row, attributeDatas, "core_user_id")) ?? void 0,
|
|
exportFields: buildMarketExportFieldFallbacks(row, attributeDatas),
|
|
hasDirectRatesSource: true,
|
|
location: readMarketLocation(row, attributeDatas),
|
|
price21To60s: readMarketPrice21To60s(row, attributeDatas),
|
|
rates: singleVideoAfterSearchRate ? {
|
|
singleVideoAfterSearchRate
|
|
} : void 0
|
|
};
|
|
}
|
|
function parseMarketListResponse(payload) {
|
|
const container = findMarketListContainer(payload);
|
|
if (!container) {
|
|
return null;
|
|
}
|
|
const marketList = readMarketListArray(container);
|
|
if (!marketList) {
|
|
return null;
|
|
}
|
|
return {
|
|
currentPage: readKnownNumberDeep(container, PAGE_NUMBER_KEYS) ?? void 0,
|
|
pageSize: readKnownNumberDeep(container, PAGE_SIZE_KEYS) ?? void 0,
|
|
records: marketList.map((row) => isRecord(row) ? mapMarketListRow(row) : null).filter(
|
|
(row) => row !== null && Boolean(row.authorId || row.authorName)
|
|
),
|
|
totalCount: readKnownNumberDeep(container, TOTAL_COUNT_KEYS) ?? void 0,
|
|
totalPages: readKnownNumberDeep(container, TOTAL_PAGE_KEYS) ?? void 0
|
|
};
|
|
}
|
|
function readKnownPaginationNumber(value, kind) {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
return readKnownNumberDeep(value, kind === "page" ? PAGE_NUMBER_KEYS : PAGE_SIZE_KEYS);
|
|
}
|
|
function findMarketListContainer(value) {
|
|
const queue = [value];
|
|
while (queue.length > 0) {
|
|
const current = queue.shift();
|
|
if (!isRecord(current)) {
|
|
continue;
|
|
}
|
|
if (readMarketListArray(current)) {
|
|
return current;
|
|
}
|
|
Object.values(current).forEach((entry) => {
|
|
queue.push(unwrapVueRef(entry));
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
function readMarketListArray(record) {
|
|
const marketList = unwrapVueRef(record.marketList);
|
|
if (Array.isArray(marketList)) {
|
|
return marketList;
|
|
}
|
|
const authors = unwrapVueRef(record.authors);
|
|
if (Array.isArray(authors)) {
|
|
return authors;
|
|
}
|
|
return null;
|
|
}
|
|
function unwrapVueRef(value) {
|
|
if (isRecord(value) && "value" in value) {
|
|
return value.value;
|
|
}
|
|
return value;
|
|
}
|
|
function isRecord(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
function readMarketAttributeDatas(record) {
|
|
return isRecord(record.attribute_datas) ? record.attribute_datas : {};
|
|
}
|
|
function readMarketFieldValue(record, attributeDatas, field) {
|
|
return record[field] ?? attributeDatas[field];
|
|
}
|
|
function readString(value) {
|
|
return typeof value === "string" ? value : null;
|
|
}
|
|
function normalizeMarketListRate(value) {
|
|
if (typeof value === "number") {
|
|
return normalizeFractionRateDisplay(String(value));
|
|
}
|
|
return typeof value === "string" ? normalizeFractionRateDisplay(value) : null;
|
|
}
|
|
function normalizeExportCellText(value) {
|
|
return value?.replace(/\s+/g, " ").trim() ?? "";
|
|
}
|
|
function buildMarketExportFieldFallbacks(record, attributeDatas) {
|
|
const exportFields = {};
|
|
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, "\u8FBE\u4EBA\u4FE1\u606F", authorInfo);
|
|
assignExportField(exportFields, "\u4EE3\u8868\u89C6\u9891", representativeVideo);
|
|
assignExportField(exportFields, "\u8FBE\u4EBA\u7C7B\u578B", authorType);
|
|
assignExportField(exportFields, "\u5185\u5BB9\u4E3B\u9898", contentTheme);
|
|
assignExportField(exportFields, "\u8FDE\u63A5\u7528\u6237\u6570", connectedUsers);
|
|
assignExportField(exportFields, "\u7C89\u4E1D\u6570", followerCount);
|
|
assignExportField(exportFields, "\u9884\u671FCPM", expectedCpm);
|
|
assignExportField(exportFields, "\u9884\u671F\u64AD\u653E\u91CF", expectedPlayCount);
|
|
assignExportField(exportFields, "\u4E92\u52A8\u7387", interactionRate);
|
|
assignExportField(exportFields, "\u5B8C\u64AD\u7387", finishRate);
|
|
assignExportField(exportFields, "\u7206\u6587\u7387", burstRate);
|
|
assignExportField(exportFields, "21-60s\u62A5\u4EF7", price21To60s);
|
|
return Object.keys(exportFields).length > 0 ? exportFields : void 0;
|
|
}
|
|
function assignExportField(exportFields, key, value) {
|
|
if (hasTextValue(value)) {
|
|
exportFields[key] = value;
|
|
}
|
|
}
|
|
function hasTextValue(value) {
|
|
return Boolean(value && value.trim().length > 0);
|
|
}
|
|
function buildMarketAuthorInfo(record, attributeDatas) {
|
|
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(" ") : void 0;
|
|
}
|
|
function buildMarketAuthorType(record, attributeDatas) {
|
|
const tagsRelation = readRecordLike(
|
|
readMarketFieldValue(record, attributeDatas, "tags_relation")
|
|
);
|
|
if (tagsRelation) {
|
|
const primaryTag = Object.keys(tagsRelation)[0];
|
|
if (hasTextValue(primaryTag)) {
|
|
return primaryTag;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
function buildMarketContentTheme(record, attributeDatas) {
|
|
const themes = readStringArray(
|
|
readMarketFieldValue(record, attributeDatas, "content_theme_labels_180d")
|
|
);
|
|
if (themes.length === 0) {
|
|
return void 0;
|
|
}
|
|
if (themes.length <= 2) {
|
|
return themes.join(" ");
|
|
}
|
|
return `${themes.slice(0, 2).join(" ")} ${themes.length - 2}+`;
|
|
}
|
|
function readMarketLocation(record, attributeDatas) {
|
|
return readString(readMarketFieldValue(record, attributeDatas, "city")) ?? void 0;
|
|
}
|
|
function readMarketPrice21To60s(record, attributeDatas) {
|
|
return formatCurrencyValue(
|
|
readNumericValue(readMarketFieldValue(record, attributeDatas, "price_20_60"))
|
|
);
|
|
}
|
|
function readMarketRepresentativeVideo(record, attributeDatas) {
|
|
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 void 0;
|
|
}
|
|
function readMarketGenderLabel(value) {
|
|
const rawValue = typeof value === "number" ? String(value) : readString(value);
|
|
if (rawValue === "1") {
|
|
return "\u7537";
|
|
}
|
|
if (rawValue === "2") {
|
|
return "\u5973";
|
|
}
|
|
return void 0;
|
|
}
|
|
function readBurstRateDisplay(value) {
|
|
if (value === null) {
|
|
return void 0;
|
|
}
|
|
if (value <= 0) {
|
|
return "-";
|
|
}
|
|
return formatFractionPercent(value);
|
|
}
|
|
function formatCurrencyValue(value) {
|
|
if (value === null) {
|
|
return void 0;
|
|
}
|
|
return `\xA5${value.toLocaleString("en-US", {
|
|
maximumFractionDigits: 0
|
|
})}`;
|
|
}
|
|
function formatWanValue(value) {
|
|
if (value === null) {
|
|
return void 0;
|
|
}
|
|
return `${formatDecimalWithGrouping(value / 1e4)}w`;
|
|
}
|
|
function formatFractionPercent(value) {
|
|
if (value === null) {
|
|
return void 0;
|
|
}
|
|
return `${formatDecimalDisplay(value * 100)}%`;
|
|
}
|
|
function formatDecimalDisplay(value) {
|
|
if (value === null) {
|
|
return void 0;
|
|
}
|
|
return value.toLocaleString("en-US", {
|
|
maximumFractionDigits: 1,
|
|
minimumFractionDigits: 0,
|
|
useGrouping: false
|
|
});
|
|
}
|
|
function formatDecimalWithGrouping(value) {
|
|
return value.toLocaleString("en-US", {
|
|
maximumFractionDigits: 1,
|
|
minimumFractionDigits: 0
|
|
});
|
|
}
|
|
function readNumericValue(value) {
|
|
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) {
|
|
if (Array.isArray(value)) {
|
|
return value.filter((item) => typeof item === "string");
|
|
}
|
|
if (typeof value === "string") {
|
|
try {
|
|
const parsedValue = JSON.parse(value);
|
|
return Array.isArray(parsedValue) ? parsedValue.filter((item) => typeof item === "string") : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
function readArrayLike(value) {
|
|
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) {
|
|
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 readKnownNumber(record, keys) {
|
|
for (const key of keys) {
|
|
const value = readNumericValue(record[key]);
|
|
if (value !== null) {
|
|
return value;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
function readKnownNumberDeep(value, keys) {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const directValue = readKnownNumber(value, keys);
|
|
if (typeof directValue === "number") {
|
|
return directValue;
|
|
}
|
|
for (const nestedValue of Object.values(value)) {
|
|
const candidate = readKnownNumberDeep(unwrapVueRef(nestedValue), keys);
|
|
if (typeof candidate === "number") {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// src/content/market/dom-sync.ts
|
|
var BACKEND_COLUMN_KEY = "backendMetrics";
|
|
var SELECTION_COLUMN_KEY = "selection";
|
|
var SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate";
|
|
var PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate";
|
|
var ACTION_HEADER_TEXT = "\u64CD\u4F5C";
|
|
var AUTHOR_HEADER_TEXT = "\u8FBE\u4EBA\u4FE1\u606F";
|
|
var BACKEND_HEADER_TEXT = "\u79D2\u63A2\u6307\u6807";
|
|
var MARKET_SCROLL_HINT_TEXT = "\u6A2A\u5411\u6EDA\u52A8\u53EF\u67E5\u770B\u770B\u540E\u641C\u7387\u3001\u79D2\u63A2\u6307\u6807";
|
|
var MARKET_SCROLLBAR_STYLE_ID = "sces-market-scrollbar-style";
|
|
var UNAVAILABLE_RATE_TEXT = "\u6682\u65E0\u6765\u6E90";
|
|
var UNAVAILABLE_BACKEND_METRICS_TEXT = "\u6682\u65E0\u6570\u636E";
|
|
var SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows";
|
|
var SORTABLE_RATE_FIELDS = [SINGLE_COLUMN_KEY, PERSONAL_COLUMN_KEY];
|
|
var BACKEND_METRIC_COLUMNS2 = [
|
|
{
|
|
field: "afterViewSearchRate",
|
|
label: "\u770B\u540E\u641C\u7387"
|
|
},
|
|
{
|
|
field: "afterViewSearchCount",
|
|
label: "\u770B\u540E\u641C\u6570"
|
|
},
|
|
{
|
|
field: "a3IncreaseCount",
|
|
label: "\u65B0\u589EA3\u6570"
|
|
},
|
|
{
|
|
field: "newA3Rate",
|
|
label: "\u65B0\u589EA3\u7387"
|
|
},
|
|
{
|
|
field: "cpa3",
|
|
label: "CPA3"
|
|
},
|
|
{
|
|
field: "cpSearch",
|
|
label: "cp_search"
|
|
}
|
|
];
|
|
var SORTABLE_MARKET_FIELDS = [
|
|
...SORTABLE_RATE_FIELDS,
|
|
...BACKEND_METRIC_COLUMNS2.map((column) => column.field)
|
|
];
|
|
function syncMarketTable(root) {
|
|
return syncSyntheticMarketTable(root) ?? syncDivGridMarketTable(root);
|
|
}
|
|
function readMarketPageSignature(root) {
|
|
const document2 = getOwnerDocument(root);
|
|
const explicitPageIndex = document2?.documentElement.getAttribute("data-test-page-index") ?? "";
|
|
const activePageIndex = document2?.querySelector(".el-pagination .number.active, .xt-pagination .number.active")?.textContent?.trim() ?? "";
|
|
const authorIds = readRawAuthorIds(root).join("|");
|
|
return `${explicitPageIndex || activePageIndex}::${authorIds}`;
|
|
}
|
|
function findNextPageControl(root) {
|
|
const document2 = getOwnerDocument(root);
|
|
if (!document2) {
|
|
return null;
|
|
}
|
|
const explicitControl = document2.querySelector('[data-testid="next-page"]');
|
|
if (explicitControl instanceof document2.defaultView.HTMLElement) {
|
|
return explicitControl;
|
|
}
|
|
const paginationNextControl = document2.querySelector(
|
|
".el-pagination .btn-next, .xt-pagination .btn-next"
|
|
);
|
|
if (paginationNextControl instanceof document2.defaultView.HTMLElement) {
|
|
return paginationNextControl;
|
|
}
|
|
const candidates = Array.from(
|
|
document2.querySelectorAll("button, a, [role='button']")
|
|
).filter(
|
|
(element) => element instanceof document2.defaultView.HTMLElement
|
|
);
|
|
return candidates.find(
|
|
(element) => /下一页|next/i.test(normalizeExportCellText2(element.textContent))
|
|
) ?? null;
|
|
}
|
|
function isPageControlDisabled(control) {
|
|
if (!control) {
|
|
return true;
|
|
}
|
|
if (control instanceof HTMLButtonElement) {
|
|
return control.disabled;
|
|
}
|
|
return control.getAttribute("aria-disabled") === "true";
|
|
}
|
|
function renderMarketRowState(rowDom, record) {
|
|
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 = "\u52A0\u8F7D\u4E2D...";
|
|
rowDom.personalCell.textContent = "\u52A0\u8F7D\u4E2D...";
|
|
return;
|
|
}
|
|
if (record.status === "failed") {
|
|
rowDom.singleCell.textContent = "\u52A0\u8F7D\u5931\u8D25";
|
|
rowDom.personalCell.textContent = "\u52A0\u8F7D\u5931\u8D25";
|
|
return;
|
|
}
|
|
rowDom.singleCell.textContent = "";
|
|
rowDom.personalCell.textContent = "";
|
|
}
|
|
function applyRowVisibility(table, visibleAuthorIds) {
|
|
table.rows.forEach((rowDom) => {
|
|
const isVisible = visibleAuthorIds.has(rowDom.authorId);
|
|
rowDom.visibilityTargets.forEach((target) => {
|
|
target.hidden = !isVisible;
|
|
});
|
|
});
|
|
}
|
|
function applyRowOrder(table, orderedAuthorIds) {
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
function syncPluginSortHeaders(root, options) {
|
|
SORTABLE_MARKET_FIELDS.forEach((field) => {
|
|
const cell = root.querySelector(
|
|
`[data-market-header-cell="${field}"]`
|
|
);
|
|
if (!cell) {
|
|
return;
|
|
}
|
|
syncSortableHeaderCell(cell, {
|
|
direction: options.activeSort?.field === field ? options.activeSort.direction : "none",
|
|
field,
|
|
onToggleSort: options.onToggleSort
|
|
});
|
|
});
|
|
}
|
|
function syncMarketSelectionState(table, selectedAuthorIds) {
|
|
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) {
|
|
const header = root.querySelector("[data-market-header]");
|
|
const body = root.querySelector("[data-market-body]");
|
|
if (!header || !body) {
|
|
return null;
|
|
}
|
|
const selectionHeader = ensureSyntheticHeaderCell(header, SELECTION_COLUMN_KEY, "");
|
|
const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeader);
|
|
ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387");
|
|
ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387");
|
|
BACKEND_METRIC_COLUMNS2.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;
|
|
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_COLUMNS2.map(({ field }) => [field, ensureSyntheticRowCell(row, field)])
|
|
);
|
|
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: void 0,
|
|
row,
|
|
selectionCheckbox,
|
|
singleCell,
|
|
visibilityTargets: [row]
|
|
};
|
|
}
|
|
);
|
|
return {
|
|
headerSelectionCheckbox,
|
|
rows
|
|
};
|
|
}
|
|
function syncDivGridMarketTable(root) {
|
|
const document2 = getOwnerDocument(root);
|
|
if (!document2) {
|
|
return null;
|
|
}
|
|
for (const marketRoot of document2.querySelectorAll(".base-author-list")) {
|
|
if (!(marketRoot instanceof document2.defaultView.HTMLElement)) {
|
|
continue;
|
|
}
|
|
const syncedTable = syncDivGridRoot(marketRoot);
|
|
if (syncedTable) {
|
|
return syncedTable;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function readRawAuthorIds(root) {
|
|
const document2 = 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 (!document2) {
|
|
return [];
|
|
}
|
|
return readSerializedMarketRows(document2).map((row) => row.authorId).filter((authorId) => Boolean(authorId));
|
|
}
|
|
function readSyntheticAuthorIds(root) {
|
|
const body = root.querySelector("[data-market-body]");
|
|
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) {
|
|
const document2 = getOwnerDocument(root);
|
|
if (!document2) {
|
|
return null;
|
|
}
|
|
const marketRoot = document2.querySelector(".base-author-list");
|
|
if (!(marketRoot instanceof document2.defaultView.HTMLElement)) {
|
|
return null;
|
|
}
|
|
const bodySection = Array.from(marketRoot.querySelectorAll(".section-wrapper")).find(
|
|
(section) => section instanceof document2.defaultView.HTMLElement && !section.classList.contains("sticky-header")
|
|
);
|
|
const authorSection = bodySection ? Array.from(bodySection.children).find(
|
|
(child) => child instanceof document2.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) {
|
|
const headerSection = root.querySelector(
|
|
".section-wrapper.sticky-header"
|
|
);
|
|
const bodySection = Array.from(root.querySelectorAll(".section-wrapper")).find(
|
|
(section) => 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,
|
|
"\u5355\u89C6\u9891\u770B\u540E\u641C\u7387"
|
|
);
|
|
ensureDivHeaderCell(
|
|
pluginHeaderSection,
|
|
headerTemplateCell,
|
|
PERSONAL_COLUMN_KEY,
|
|
"\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387"
|
|
);
|
|
const singleColumn = ensureDivBodyColumn(
|
|
pluginBodySection,
|
|
bodyTemplateColumn,
|
|
SINGLE_COLUMN_KEY,
|
|
rowCount
|
|
);
|
|
const personalColumn = ensureDivBodyColumn(
|
|
pluginBodySection,
|
|
bodyTemplateColumn,
|
|
PERSONAL_COLUMN_KEY,
|
|
rowCount
|
|
);
|
|
const backendMetricColumns = Object.fromEntries(
|
|
BACKEND_METRIC_COLUMNS2.map(({ field, label }) => {
|
|
ensureDivHeaderCell(pluginHeaderSection, headerTemplateCell, field, label);
|
|
return [
|
|
field,
|
|
ensureDivBodyColumn(
|
|
pluginBodySection,
|
|
bodyTemplateColumn,
|
|
field,
|
|
rowCount
|
|
)
|
|
];
|
|
})
|
|
);
|
|
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_COLUMNS2.map(({ field }) => [
|
|
field,
|
|
getDirectContentCells(backendMetricColumns[field])
|
|
])
|
|
);
|
|
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_COLUMNS2.map(({ field }) => [
|
|
field,
|
|
backendMetricCellsByField[field][index] ?? 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
|
|
);
|
|
const rowCells = alignedRowCells.filter(
|
|
(cell) => 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,
|
|
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 !== null),
|
|
personalCell,
|
|
price21To60s,
|
|
rates: fallbackMarketRow?.rates,
|
|
row: authorCell,
|
|
selectionCheckbox,
|
|
singleCell,
|
|
visibilityTargets: rowCells
|
|
}
|
|
];
|
|
});
|
|
return {
|
|
headerSelectionCheckbox,
|
|
rows
|
|
};
|
|
}
|
|
function ensureSyntheticHeaderCell(header, field, label) {
|
|
const existingCell = header.querySelector(
|
|
`[data-market-header-cell="${field}"]`
|
|
);
|
|
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, field) {
|
|
const existingCell = row.querySelector(
|
|
`[data-market-row-cell="${field}"]`
|
|
);
|
|
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, templateCell, field, label) {
|
|
const existingCell = container.querySelector(
|
|
`[data-market-header-cell="${field}"]`
|
|
);
|
|
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, templateColumn, field, rowCount) {
|
|
const existingColumn = container.querySelector(
|
|
`[data-market-column-group="${field}"]`
|
|
);
|
|
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, templateColumn, field, rowCount) {
|
|
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;
|
|
const templateCell = templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
|
|
if (existingCell) {
|
|
existingCell.dataset.marketRowCell = field;
|
|
applyPluginContentCellStyles(existingCell);
|
|
syncContentCellHeight(existingCell, templateCell);
|
|
continue;
|
|
}
|
|
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);
|
|
syncContentCellHeight(nextCell, templateCell);
|
|
nextCell.textContent = "";
|
|
column.appendChild(nextCell);
|
|
}
|
|
}
|
|
function syncContentCellHeight(cell, templateCell) {
|
|
if (!templateCell) {
|
|
return;
|
|
}
|
|
const measuredHeight = Math.round(templateCell.getBoundingClientRect().height);
|
|
const nextHeight = measuredHeight > 0 ? `${measuredHeight}px` : templateCell.style.height;
|
|
if (nextHeight) {
|
|
cell.style.height = nextHeight;
|
|
} else {
|
|
cell.style.removeProperty("height");
|
|
}
|
|
}
|
|
function applyPluginHeaderCellStyles(cell) {
|
|
cell.style.display = "flex";
|
|
cell.style.alignItems = "center";
|
|
cell.style.justifyContent = "normal";
|
|
cell.style.cursor = "pointer";
|
|
cell.style.whiteSpace = "nowrap";
|
|
}
|
|
function applyPluginContentCellStyles(cell) {
|
|
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) {
|
|
cell.textContent = "";
|
|
cell.style.gap = "6px";
|
|
cell.style.justifyContent = "center";
|
|
const checkbox = ensureSelectionCheckbox(cell, "header");
|
|
const label = cell.querySelector(
|
|
'[data-market-selection-label="header"]'
|
|
);
|
|
if (label) {
|
|
label.textContent = "\u5168\u9009";
|
|
return checkbox;
|
|
}
|
|
const nextLabel = cell.ownerDocument.createElement("span");
|
|
nextLabel.dataset.marketSelectionLabel = "header";
|
|
nextLabel.textContent = "\u5168\u9009";
|
|
nextLabel.style.fontSize = "12px";
|
|
cell.appendChild(nextLabel);
|
|
return checkbox;
|
|
}
|
|
function ensureSelectionRowControl(cell) {
|
|
cell.textContent = "";
|
|
cell.style.justifyContent = "center";
|
|
return ensureSelectionCheckbox(cell, "row");
|
|
}
|
|
function ensureSelectionCheckbox(container, kind) {
|
|
const existingCheckbox = container.querySelector(
|
|
`[data-market-selection-checkbox="${kind}"]`
|
|
);
|
|
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) {
|
|
if ("ownerDocument" in root && root.ownerDocument) {
|
|
return root.ownerDocument;
|
|
}
|
|
return "nodeType" in root && root.nodeType === 9 ? root : null;
|
|
}
|
|
function readSyntheticHeaderLabels(header) {
|
|
return Array.from(header.querySelectorAll("[data-market-header-cell]")).reduce((labels, cell) => {
|
|
if (!(cell instanceof header.ownerDocument.defaultView.HTMLElement)) {
|
|
return labels;
|
|
}
|
|
const field = cell.dataset.marketHeaderCell;
|
|
if (!field) {
|
|
return labels;
|
|
}
|
|
labels[field] = normalizeExportCellText2(cell.textContent);
|
|
return labels;
|
|
}, {});
|
|
}
|
|
function readSyntheticExportFields(row, headerLabelsByField) {
|
|
const exportFields = {};
|
|
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] = normalizeExportCellText2(cell.textContent);
|
|
}
|
|
return exportFields;
|
|
}
|
|
function readExportFieldsForDivGridRow(headerCells, rowCells) {
|
|
const exportFields = {};
|
|
rowCells.forEach((cell, index) => {
|
|
const headerLabel = normalizeExportCellText2(headerCells[index]?.textContent);
|
|
if (!shouldExportColumn(headerLabel)) {
|
|
return;
|
|
}
|
|
exportFields[headerLabel] = headerLabel === "21-60s\u62A5\u4EF7" ? readDivGridPriceDisplay(cell?.textContent) ?? "" : normalizeExportCellText2(cell?.textContent);
|
|
});
|
|
return exportFields;
|
|
}
|
|
function findPreviousHeaderCell(cell) {
|
|
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) {
|
|
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, referenceSection, options) {
|
|
const existingSection = rootSection.querySelector(
|
|
`[data-market-plugin-section="${options.type}"]`
|
|
);
|
|
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) {
|
|
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(document2) {
|
|
if (document2.getElementById(MARKET_SCROLLBAR_STYLE_ID)) {
|
|
return;
|
|
}
|
|
const style = document2.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;
|
|
}
|
|
`;
|
|
document2.head.appendChild(style);
|
|
}
|
|
function ensureScrollHint(root, headerSection) {
|
|
const existingHint = root.querySelector(
|
|
'[data-testid="market-scroll-hint"]'
|
|
);
|
|
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) {
|
|
let current = section.previousElementSibling;
|
|
while (current) {
|
|
if (current instanceof section.ownerDocument.defaultView.HTMLElement) {
|
|
return current;
|
|
}
|
|
current = current.previousElementSibling;
|
|
}
|
|
return null;
|
|
}
|
|
function findPreviousNativeSection(section) {
|
|
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) {
|
|
section.style.position = "";
|
|
section.style.left = "";
|
|
section.style.right = "";
|
|
section.style.zIndex = "";
|
|
section.style.width = "";
|
|
section.style.minWidth = "";
|
|
}
|
|
function getActionColumn(bodySection) {
|
|
const columns = getDirectContentColumns(bodySection);
|
|
return columns[columns.length - 1] ?? null;
|
|
}
|
|
function getNativeAuthorColumn(authorSection) {
|
|
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) {
|
|
return Array.from(section.querySelectorAll(".header-cell")).filter(
|
|
(cell) => cell instanceof section.ownerDocument.defaultView.HTMLElement
|
|
);
|
|
}
|
|
function getDirectContentColumns(section) {
|
|
return Array.from(section.children).filter(
|
|
(child) => child instanceof section.ownerDocument.defaultView.HTMLElement && child.classList.contains("content-column")
|
|
);
|
|
}
|
|
function getDirectContentCells(column) {
|
|
return Array.from(column.children).filter(
|
|
(child) => child instanceof column.ownerDocument.defaultView.HTMLElement && child.classList.contains("content-cell")
|
|
);
|
|
}
|
|
function getDirectChildIndex(root, descendant) {
|
|
const directChild = Array.from(root.children).find((child) => child.contains(descendant));
|
|
return directChild ? Array.from(root.children).indexOf(directChild) : -1;
|
|
}
|
|
function getIndexedChild(root, index) {
|
|
if (index < 0) {
|
|
return null;
|
|
}
|
|
const child = root.children[index] ?? null;
|
|
return child instanceof root.ownerDocument.defaultView.HTMLElement ? child : null;
|
|
}
|
|
function findCellByText(cells, text) {
|
|
return cells.find((cell) => cell.textContent?.trim() === text) ?? null;
|
|
}
|
|
function cloneElementShallow(reference) {
|
|
const clone = reference.ownerDocument.createElement(reference.tagName);
|
|
Array.from(reference.attributes).forEach((attribute) => {
|
|
clone.setAttribute(attribute.name, attribute.value);
|
|
});
|
|
return clone;
|
|
}
|
|
function createBareContentCell(document2) {
|
|
const cell = document2.createElement("div");
|
|
cell.className = "content-cell";
|
|
return cell;
|
|
}
|
|
function createSelectionContentCell(templateCell) {
|
|
const cell = cloneElementShallow(templateCell);
|
|
cell.removeAttribute("data-testid");
|
|
cell.removeAttribute("data-author-id");
|
|
return cell;
|
|
}
|
|
function extractAuthorId(authorCell) {
|
|
const explicitAuthorId = authorCell.dataset.authorId;
|
|
if (explicitAuthorId) {
|
|
return explicitAuthorId;
|
|
}
|
|
const linkedAuthorId = Array.from(authorCell.querySelectorAll("a")).map((link) => extractAuthorIdFromHref(link.href)).find((value) => Boolean(value));
|
|
if (linkedAuthorId) {
|
|
return linkedAuthorId;
|
|
}
|
|
const fallbackAuthorId = authorCell.querySelector("[data-author-id]")?.getAttribute("data-author-id");
|
|
return fallbackAuthorId ?? "";
|
|
}
|
|
function extractAuthorName(authorCell) {
|
|
return authorCell.querySelector(".author-nickname")?.textContent?.trim() ?? authorCell.textContent?.trim() ?? "";
|
|
}
|
|
function extractAuthorIdFromHref(href) {
|
|
const match = href.match(/\/author-homepage\/[^/]+\/(\d+)/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
function readVueMarketRows(marketRoot) {
|
|
const vueRoot = marketRoot.__vue__;
|
|
const setupStates = collectVueSetupStates(vueRoot);
|
|
for (const setupState of setupStates) {
|
|
for (const value of Object.values(setupState)) {
|
|
const candidate = unwrapVueRef2(value);
|
|
if (!candidate || typeof candidate !== "object") {
|
|
continue;
|
|
}
|
|
const marketList = unwrapVueRef2(
|
|
candidate.marketList
|
|
);
|
|
if (!Array.isArray(marketList)) {
|
|
continue;
|
|
}
|
|
return marketList.map((row) => isRecord2(row) ? mapMarketListRow(row) : null).filter((row) => row !== null);
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
function collectVueSetupStates(vueRoot) {
|
|
if (!vueRoot) {
|
|
return [];
|
|
}
|
|
const queue = [vueRoot];
|
|
const setupStates = [];
|
|
while (queue.length > 0) {
|
|
const current = queue.shift();
|
|
if (!isRecord2(current)) {
|
|
continue;
|
|
}
|
|
if (isRecord2(current._setupState)) {
|
|
setupStates.push(current._setupState);
|
|
}
|
|
const children = Array.isArray(current.$children) ? current.$children : [];
|
|
queue.push(...children);
|
|
}
|
|
return setupStates;
|
|
}
|
|
function readSerializedMarketRows(document2) {
|
|
const serializedRows = document2.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 = isRecord2(row) ? row : {};
|
|
const singleVideoAfterSearchRate = readString2(
|
|
record.singleVideoAfterSearchRate
|
|
);
|
|
return {
|
|
authorId: readString2(record.authorId) ?? "",
|
|
authorName: readString2(record.authorName) ?? "",
|
|
coreUserId: readString2(record.coreUserId) ?? void 0,
|
|
exportFields: readSerializedExportFields(record),
|
|
hasDirectRatesSource: Boolean(singleVideoAfterSearchRate),
|
|
location: readString2(record.location) ?? void 0,
|
|
price21To60s: readString2(record.price21To60s) ?? void 0,
|
|
rates: singleVideoAfterSearchRate ? {
|
|
singleVideoAfterSearchRate
|
|
} : void 0
|
|
};
|
|
}).filter((row) => Boolean(row.authorId || row.authorName));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
function unwrapVueRef2(value) {
|
|
if (isRecord2(value) && "value" in value) {
|
|
return value.value;
|
|
}
|
|
return value;
|
|
}
|
|
function isRecord2(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
function readString2(value) {
|
|
return typeof value === "string" ? value : null;
|
|
}
|
|
function normalizeExportCellText2(value) {
|
|
return value?.replace(/\s+/g, " ").trim() ?? "";
|
|
}
|
|
function readDivGridPriceDisplay(value) {
|
|
const normalizedValue = normalizeExportCellText2(value);
|
|
if (!normalizedValue) {
|
|
return void 0;
|
|
}
|
|
const match = normalizedValue.match(/^¥?\s*([\d,]+(?:\.\d+)?)$/);
|
|
if (!match) {
|
|
return void 0;
|
|
}
|
|
const numericValue = Number(match[1].replace(/,/g, ""));
|
|
if (!Number.isFinite(numericValue)) {
|
|
return void 0;
|
|
}
|
|
return formatCurrencyValue2(numericValue);
|
|
}
|
|
function shouldExportColumn(label) {
|
|
const excludedBackendLabels = new Set(BACKEND_METRIC_COLUMNS2.map((column) => column.label));
|
|
return Boolean(
|
|
label && label !== ACTION_HEADER_TEXT && label !== BACKEND_HEADER_TEXT && !excludedBackendLabels.has(label) && label !== "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387" && label !== "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387"
|
|
);
|
|
}
|
|
function syncSortableHeaderCell(cell, options) {
|
|
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) {
|
|
return cell.dataset.marketHeaderLabel ?? normalizeExportCellText2(cell.textContent) ?? "";
|
|
}
|
|
function ensureHeaderSorterRoot(cell) {
|
|
const existingRoot = cell.querySelector(
|
|
'[data-market-sorter="root"]'
|
|
);
|
|
if (existingRoot) {
|
|
return existingRoot;
|
|
}
|
|
cell.dataset.marketHeaderLabel = normalizeExportCellText2(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) {
|
|
const existingText = sorterRoot.querySelector(
|
|
'[data-market-sorter="text"]'
|
|
);
|
|
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) {
|
|
const existingIcon = sorterRoot.querySelector(
|
|
'[data-market-sorter="icon"]'
|
|
);
|
|
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, direction) {
|
|
const existingTriangle = iconRoot.querySelector(
|
|
`[data-market-sorter-triangle="${direction}"]`
|
|
);
|
|
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, options) {
|
|
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) {
|
|
return value ? normalizeRateDisplay(value) : UNAVAILABLE_RATE_TEXT;
|
|
}
|
|
function applyColumnWidth(element, field) {
|
|
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_COLUMNS2.some((column) => column.field === field)) {
|
|
element.style.minWidth = "120px";
|
|
element.style.width = "120px";
|
|
}
|
|
}
|
|
function syncContainerWidth(container) {
|
|
if (!(container instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const directChildren = Array.from(container.children).filter(
|
|
(child) => 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) {
|
|
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, preferredRow) {
|
|
if (!baseRow && !preferredRow) {
|
|
return null;
|
|
}
|
|
if (!baseRow) {
|
|
return preferredRow;
|
|
}
|
|
if (!preferredRow) {
|
|
return baseRow;
|
|
}
|
|
return {
|
|
authorId: preferredRow.authorId || baseRow.authorId,
|
|
authorName: preferredRow.authorName || baseRow.authorName,
|
|
coreUserId: mergeNonEmptyString(baseRow.coreUserId, preferredRow.coreUserId),
|
|
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, authorId, authorName) {
|
|
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, fallback) {
|
|
if (!current && !fallback) {
|
|
return void 0;
|
|
}
|
|
const nextFields = {
|
|
...current ?? {}
|
|
};
|
|
Object.entries(fallback ?? {}).forEach(([key, value]) => {
|
|
if (!hasTextValue2(nextFields[key]) && hasTextValue2(value)) {
|
|
nextFields[key] = value;
|
|
}
|
|
});
|
|
return nextFields;
|
|
}
|
|
function mergeRates(current, fallback) {
|
|
if (!current && !fallback) {
|
|
return void 0;
|
|
}
|
|
return {
|
|
singleVideoAfterSearchRate: current?.singleVideoAfterSearchRate ?? fallback?.singleVideoAfterSearchRate,
|
|
personalVideoAfterSearchRate: current?.personalVideoAfterSearchRate ?? fallback?.personalVideoAfterSearchRate
|
|
};
|
|
}
|
|
function mergeNonEmptyString(current, fallback) {
|
|
return hasTextValue2(current) ? current : fallback;
|
|
}
|
|
function formatCurrencyValue2(value) {
|
|
if (value === null) {
|
|
return void 0;
|
|
}
|
|
return `\xA5${value.toLocaleString("en-US", {
|
|
maximumFractionDigits: 0
|
|
})}`;
|
|
}
|
|
function readSerializedExportFields(record) {
|
|
if (!isRecord2(record.exportFields)) {
|
|
return void 0;
|
|
}
|
|
const entries = Object.entries(record.exportFields).flatMap(
|
|
([key, value]) => typeof value === "string" ? [[key, value]] : []
|
|
);
|
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
}
|
|
function hasTextValue2(value) {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
function renderBackendMetricsCells(cells, record) {
|
|
if (record.backendMetricsStatus === "loading" || record.status === "loading" && !record.backendMetricsStatus) {
|
|
fillBackendMetricCells(cells, "\u52A0\u8F7D\u4E2D...");
|
|
return;
|
|
}
|
|
if (record.backendMetricsStatus === "failed") {
|
|
fillBackendMetricCells(cells, "\u52A0\u8F7D\u5931\u8D25");
|
|
return;
|
|
}
|
|
if (record.backendMetricsStatus === "missing") {
|
|
fillBackendMetricCells(cells, UNAVAILABLE_BACKEND_METRICS_TEXT);
|
|
return;
|
|
}
|
|
if (record.backendMetricsStatus !== "success" || !record.backendMetrics) {
|
|
fillBackendMetricCells(cells, "");
|
|
return;
|
|
}
|
|
BACKEND_METRIC_COLUMNS2.forEach(({ field }) => {
|
|
cells[field].textContent = record.backendMetrics?.[field] ?? "";
|
|
});
|
|
}
|
|
function fillBackendMetricCells(cells, value) {
|
|
BACKEND_METRIC_COLUMNS2.forEach(({ field }) => {
|
|
cells[field].textContent = value;
|
|
});
|
|
}
|
|
|
|
// src/content/market/filter-sort-controller.ts
|
|
function applyFilterAndSort(records, options = {}) {
|
|
const filteredRecords = records.filter(
|
|
(record) => matchesFilters(record, options.filters)
|
|
);
|
|
if (!options.sort) {
|
|
return filteredRecords;
|
|
}
|
|
return [...filteredRecords].sort(
|
|
(leftRecord, rightRecord) => compareRecords(leftRecord, rightRecord, options.sort)
|
|
);
|
|
}
|
|
function matchesFilters(record, filters) {
|
|
if (!filters) {
|
|
return true;
|
|
}
|
|
return meetsThreshold(
|
|
record.rates?.singleVideoAfterSearchRate,
|
|
filters.singleVideoAfterSearchRateMin
|
|
) && meetsThreshold(
|
|
record.rates?.personalVideoAfterSearchRate,
|
|
filters.personalVideoAfterSearchRateMin
|
|
);
|
|
}
|
|
function meetsThreshold(rateValue, minValue) {
|
|
if (minValue == null) {
|
|
return true;
|
|
}
|
|
const lowerBound = parseRateLowerBound(rateValue ?? null);
|
|
return lowerBound != null && lowerBound >= minValue;
|
|
}
|
|
function compareRecords(leftRecord, rightRecord, sort) {
|
|
if (isRateSortField(sort.field)) {
|
|
return compareRateSortRecords(leftRecord, rightRecord, sort);
|
|
}
|
|
return compareBackendMetricRecords(leftRecord, rightRecord, sort);
|
|
}
|
|
function compareRateSortRecords(leftRecord, rightRecord, sort) {
|
|
const field = sort.field;
|
|
const leftValue = leftRecord.rates?.[field];
|
|
const rightValue = rightRecord.rates?.[field];
|
|
const leftLowerBound = parseRateLowerBound(leftValue ?? null);
|
|
const rightLowerBound = parseRateLowerBound(rightValue ?? null);
|
|
if (leftLowerBound == null && rightLowerBound == null) {
|
|
return compareRecordIdentity(leftRecord, rightRecord);
|
|
}
|
|
if (leftLowerBound == null) {
|
|
return 1;
|
|
}
|
|
if (rightLowerBound == null) {
|
|
return -1;
|
|
}
|
|
if (leftLowerBound !== rightLowerBound) {
|
|
return sort.direction === "asc" ? leftLowerBound - rightLowerBound : rightLowerBound - leftLowerBound;
|
|
}
|
|
const tieBreak = compareRateValues(leftValue, rightValue);
|
|
if (tieBreak !== 0) {
|
|
return sort.direction === "asc" ? tieBreak : -tieBreak;
|
|
}
|
|
return compareRecordIdentity(leftRecord, rightRecord);
|
|
}
|
|
function compareBackendMetricRecords(leftRecord, rightRecord, sort) {
|
|
const field = sort.field;
|
|
const leftValue = parseBackendMetricValue(leftRecord.backendMetrics?.[field]);
|
|
const rightValue = parseBackendMetricValue(rightRecord.backendMetrics?.[field]);
|
|
if (leftValue == null && rightValue == null) {
|
|
return compareRecordIdentity(leftRecord, rightRecord);
|
|
}
|
|
if (leftValue == null) {
|
|
return 1;
|
|
}
|
|
if (rightValue == null) {
|
|
return -1;
|
|
}
|
|
if (leftValue !== rightValue) {
|
|
return sort.direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
|
|
}
|
|
return compareRecordIdentity(leftRecord, rightRecord);
|
|
}
|
|
function parseBackendMetricValue(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const normalizedValue = value.replace(/,/g, "").replace(/%/g, "").trim();
|
|
if (!normalizedValue) {
|
|
return null;
|
|
}
|
|
const numericValue = Number(normalizedValue);
|
|
return Number.isFinite(numericValue) ? numericValue : null;
|
|
}
|
|
function isRateSortField(field) {
|
|
return field === "singleVideoAfterSearchRate" || field === "personalVideoAfterSearchRate";
|
|
}
|
|
function compareRecordIdentity(leftRecord, rightRecord) {
|
|
const authorIdCompare = leftRecord.authorId.localeCompare(rightRecord.authorId);
|
|
if (authorIdCompare !== 0) {
|
|
return authorIdCompare;
|
|
}
|
|
return leftRecord.authorName.localeCompare(rightRecord.authorName);
|
|
}
|
|
|
|
// src/content/market/api-client.ts
|
|
function createMarketApiClient(options = {}) {
|
|
const baseUrl = options.baseUrl ?? resolveBaseUrl();
|
|
const fetchImpl = options.fetchImpl ?? defaultFetch;
|
|
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
return {
|
|
async loadAuthorAseInfo(authorId) {
|
|
const primaryResult = await loadAuthorMetricsFromUrl(
|
|
buildAuthorCommerceSeedBaseInfoUrl(authorId, baseUrl)
|
|
);
|
|
if (primaryResult.success || primaryResult.reason === "timeout") {
|
|
return primaryResult;
|
|
}
|
|
return loadAuthorMetricsFromUrl(buildAuthorAseInfoUrl(authorId, baseUrl));
|
|
}
|
|
};
|
|
async function loadAuthorMetricsFromUrl(url) {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const response = await fetchImpl(url, {
|
|
credentials: "include",
|
|
method: "GET",
|
|
signal: controller.signal
|
|
});
|
|
if (!response.ok) {
|
|
return {
|
|
success: false,
|
|
reason: "request-failed"
|
|
};
|
|
}
|
|
return mapAuthorAseInfoResponse(await response.json());
|
|
} catch (error) {
|
|
if (isAbortError(error) || controller.signal.aborted) {
|
|
return {
|
|
success: false,
|
|
reason: "timeout"
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
reason: "request-failed"
|
|
};
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
}
|
|
function buildAuthorAseInfoUrl(authorId, baseUrl) {
|
|
const url = new URL("/gw/api/aggregator/get_author_ase_info", baseUrl);
|
|
url.searchParams.set("author_id", authorId);
|
|
url.searchParams.set("range", "30");
|
|
return url.toString();
|
|
}
|
|
function buildAuthorCommerceSeedBaseInfoUrl(authorId, baseUrl) {
|
|
const url = new URL(
|
|
"/gw/api/aggregator/get_author_commerce_seed_base_info",
|
|
baseUrl
|
|
);
|
|
url.searchParams.set("o_author_id", authorId);
|
|
url.searchParams.set("range", "90");
|
|
return url.toString();
|
|
}
|
|
function mapAuthorAseInfoResponse(payload) {
|
|
const data = getPayloadData(payload);
|
|
if (!data) {
|
|
return {
|
|
success: false,
|
|
reason: "bad-response"
|
|
};
|
|
}
|
|
const singleVideoAfterSearchRate = readNormalizedRate(
|
|
data.avg_search_after_view_rate
|
|
);
|
|
const personalVideoAfterSearchRate = readNormalizedRate(
|
|
data.personal_avg_search_after_view_rate
|
|
);
|
|
if (!singleVideoAfterSearchRate && !personalVideoAfterSearchRate) {
|
|
return {
|
|
success: false,
|
|
reason: "missing-rate"
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
rates: {
|
|
...singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : {},
|
|
...personalVideoAfterSearchRate ? { personalVideoAfterSearchRate } : {}
|
|
}
|
|
};
|
|
}
|
|
function getPayloadData(payload) {
|
|
if (!isRecord3(payload)) {
|
|
return null;
|
|
}
|
|
return isRecord3(payload.data) ? payload.data : payload;
|
|
}
|
|
function readNormalizedRate(value) {
|
|
return typeof value === "string" ? normalizeRateDisplay(value) : null;
|
|
}
|
|
function resolveBaseUrl() {
|
|
if (typeof location !== "undefined" && location.origin) {
|
|
return location.origin;
|
|
}
|
|
return "https://xingtu.cn";
|
|
}
|
|
async function defaultFetch(input, init) {
|
|
return fetch(input, init);
|
|
}
|
|
function isAbortError(error) {
|
|
return error instanceof Error && error.name === "AbortError";
|
|
}
|
|
function isRecord3(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
// src/content/market/export-range-controller.ts
|
|
function createExportRangeController(options) {
|
|
return {
|
|
async exportRecords(target) {
|
|
const mergedRecords = /* @__PURE__ */ new Map();
|
|
let currentPage = 0;
|
|
let expectedMinimumRowCount;
|
|
while (true) {
|
|
currentPage += 1;
|
|
options.onProgress?.({
|
|
currentPage,
|
|
totalPages: target.mode === "count" ? target.pageCount : void 0
|
|
});
|
|
const currentPageRecords = await preparePageRecords(expectedMinimumRowCount);
|
|
if (!currentPageRecords) {
|
|
throw new Error(`\u7B2C ${currentPage} \u9875\u52A0\u8F7D\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`);
|
|
}
|
|
currentPageRecords.forEach((record) => {
|
|
const existingRecord = mergedRecords.get(record.authorId);
|
|
mergedRecords.set(record.authorId, mergeMarketRecord(existingRecord, record));
|
|
});
|
|
expectedMinimumRowCount = Math.max(
|
|
expectedMinimumRowCount ?? 0,
|
|
currentPageRecords.length
|
|
);
|
|
if (target.mode === "count" && currentPage >= target.pageCount) {
|
|
break;
|
|
}
|
|
const previousSignature = readMarketPageSignature(options.document);
|
|
const nextPageControl = findNextPageControl(options.document);
|
|
if (!nextPageControl || isPageControlDisabled(nextPageControl)) {
|
|
break;
|
|
}
|
|
nextPageControl.click();
|
|
const pageChanged = await waitForPageChange(previousSignature);
|
|
if (!pageChanged) {
|
|
throw new Error(`\u7B2C ${currentPage + 1} \u9875\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`);
|
|
}
|
|
}
|
|
return Array.from(mergedRecords.values());
|
|
}
|
|
};
|
|
async function preparePageRecords(expectedMinimumRowCount) {
|
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
const currentPageReady = await waitForCurrentPageReady();
|
|
if (!currentPageReady) {
|
|
return null;
|
|
}
|
|
await options.prepareCurrentPageForExport();
|
|
const currentPageRecords = options.readCurrentPageRecords();
|
|
if (currentPageRecords.length > 0 && (typeof expectedMinimumRowCount !== "number" || expectedMinimumRowCount <= 0 || isCurrentPageTerminal() || currentPageRecords.length >= expectedMinimumRowCount)) {
|
|
return currentPageRecords;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function waitForPageChange(previousSignature) {
|
|
const previousPageState = parsePageSignature(previousSignature);
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
await new Promise((resolve) => {
|
|
options.window.setTimeout(resolve, 50);
|
|
});
|
|
await Promise.resolve();
|
|
const nextSignature = readMarketPageSignature(options.document);
|
|
const nextPageState = parsePageSignature(nextSignature);
|
|
if (hasLoadedNextPage(previousPageState, nextPageState)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
async function waitForCurrentPageReady() {
|
|
let stableAttemptCount = 0;
|
|
let lastReadyFingerprint = "";
|
|
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
await new Promise((resolve) => {
|
|
options.window.setTimeout(resolve, 150);
|
|
});
|
|
await Promise.resolve();
|
|
const pageState = readCurrentPageState();
|
|
if (!pageState.authorIds || pageState.rowCount <= 0) {
|
|
stableAttemptCount = 0;
|
|
lastReadyFingerprint = "";
|
|
continue;
|
|
}
|
|
const readyFingerprint = [
|
|
pageState.pageToken,
|
|
pageState.authorIds,
|
|
String(pageState.rowCount),
|
|
pageState.isTerminalPage ? "terminal" : "paged"
|
|
].join("::");
|
|
if (readyFingerprint === lastReadyFingerprint) {
|
|
stableAttemptCount += 1;
|
|
} else {
|
|
lastReadyFingerprint = readyFingerprint;
|
|
stableAttemptCount = 1;
|
|
}
|
|
if (stableAttemptCount >= 6) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function readCurrentPageState() {
|
|
const pageSignature = parsePageSignature(readMarketPageSignature(options.document));
|
|
const nextPageControl = findNextPageControl(options.document);
|
|
return {
|
|
authorIds: pageSignature.authorIds,
|
|
isTerminalPage: isPageControlDisabled(nextPageControl),
|
|
pageToken: pageSignature.pageToken,
|
|
rowCount: options.readCurrentPageRowCount()
|
|
};
|
|
}
|
|
function isCurrentPageTerminal() {
|
|
return isPageControlDisabled(findNextPageControl(options.document));
|
|
}
|
|
}
|
|
function parsePageSignature(signature) {
|
|
const separatorIndex = signature.indexOf("::");
|
|
if (separatorIndex < 0) {
|
|
return {
|
|
authorIds: "",
|
|
pageToken: signature.trim()
|
|
};
|
|
}
|
|
return {
|
|
authorIds: signature.slice(separatorIndex + 2).trim(),
|
|
pageToken: signature.slice(0, separatorIndex).trim()
|
|
};
|
|
}
|
|
function hasLoadedNextPage(previousPageState, nextPageState) {
|
|
if (!nextPageState.authorIds) {
|
|
return false;
|
|
}
|
|
if (nextPageState.pageToken || previousPageState.pageToken) {
|
|
return nextPageState.pageToken !== previousPageState.pageToken;
|
|
}
|
|
return nextPageState.authorIds !== previousPageState.authorIds;
|
|
}
|
|
function mergeMarketRecord(existingRecord, incomingRecord) {
|
|
if (!existingRecord) {
|
|
return {
|
|
...incomingRecord,
|
|
exportFields: mergeFieldMap(void 0, incomingRecord.exportFields),
|
|
rates: mergeFieldMap(void 0, incomingRecord.rates)
|
|
};
|
|
}
|
|
return {
|
|
...existingRecord,
|
|
...incomingRecord,
|
|
authorName: mergeStringValue(existingRecord.authorName, incomingRecord.authorName) ?? "",
|
|
coreUserId: mergeStringValue(existingRecord.coreUserId, incomingRecord.coreUserId),
|
|
exportFields: mergeFieldMap(
|
|
existingRecord.exportFields,
|
|
incomingRecord.exportFields
|
|
),
|
|
failureReason: incomingRecord.failureReason ?? existingRecord.failureReason,
|
|
hasDirectRatesSource: existingRecord.hasDirectRatesSource || incomingRecord.hasDirectRatesSource,
|
|
location: mergeStringValue(existingRecord.location, incomingRecord.location),
|
|
price21To60s: mergeStringValue(
|
|
existingRecord.price21To60s,
|
|
incomingRecord.price21To60s
|
|
),
|
|
rates: mergeFieldMap(existingRecord.rates, incomingRecord.rates),
|
|
status: mergeStatus(existingRecord.status, incomingRecord.status)
|
|
};
|
|
}
|
|
function mergeFieldMap(current, incoming) {
|
|
if (!current && !incoming) {
|
|
return void 0;
|
|
}
|
|
const merged = {
|
|
...current ?? {}
|
|
};
|
|
Object.entries(incoming ?? {}).forEach(([key, value]) => {
|
|
const currentValue = merged[key];
|
|
if (hasTextValue3(value) || !hasTextValue3(currentValue)) {
|
|
merged[key] = value;
|
|
}
|
|
});
|
|
return merged;
|
|
}
|
|
function mergeStatus(current, incoming) {
|
|
const priority = {
|
|
failed: 1,
|
|
idle: 0,
|
|
loading: 2,
|
|
missing: -1,
|
|
success: 3
|
|
};
|
|
return priority[incoming] >= priority[current] ? incoming : current;
|
|
}
|
|
function mergeStringValue(current, incoming) {
|
|
if (hasTextValue3(incoming) || !hasTextValue3(current)) {
|
|
return incoming ?? current;
|
|
}
|
|
return current;
|
|
}
|
|
function hasTextValue3(value) {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
// src/content/market/plugin-toolbar.ts
|
|
var PLUGIN_ACTION_BUTTON_STYLE_ID = "sces-plugin-action-button-style";
|
|
function isPluginToolbarMounted(root, document2) {
|
|
const actionRow = findNativeActionRow(document2);
|
|
return Boolean(actionRow && root.parentElement === actionRow && !root.hidden);
|
|
}
|
|
function ensurePluginToolbar(document2, handlers) {
|
|
ensurePluginActionButtonTheme(document2);
|
|
const existingRoot = document2.querySelector(
|
|
"[data-plugin-toolbar='root']"
|
|
);
|
|
if (existingRoot) {
|
|
ensureToolbarMounted(existingRoot, document2);
|
|
return readToolbarDom(existingRoot);
|
|
}
|
|
const root = document2.createElement("section");
|
|
root.dataset.pluginToolbar = "root";
|
|
applyToolbarRootStyles(root);
|
|
const exportRangeSelect = document2.createElement("select");
|
|
exportRangeSelect.dataset.pluginExportRange = "select";
|
|
appendOption(exportRangeSelect, "current", "\u5F53\u524D\u9875");
|
|
appendOption(exportRangeSelect, "first-5", "\u524D5\u9875");
|
|
appendOption(exportRangeSelect, "first-10", "\u524D10\u9875");
|
|
appendOption(exportRangeSelect, "all", "\u5168\u90E8");
|
|
appendOption(exportRangeSelect, "custom", "\u81EA\u5B9A\u4E49");
|
|
exportRangeSelect.value = "first-5";
|
|
const exportCustomPagesInput = document2.createElement("input");
|
|
exportCustomPagesInput.type = "number";
|
|
exportCustomPagesInput.min = "1";
|
|
exportCustomPagesInput.step = "1";
|
|
exportCustomPagesInput.hidden = true;
|
|
exportCustomPagesInput.placeholder = "\u9875\u6570";
|
|
exportCustomPagesInput.dataset.pluginExportCustomPages = "input";
|
|
const exportButton = document2.createElement("button");
|
|
exportButton.type = "button";
|
|
exportButton.dataset.pluginExport = "button";
|
|
exportButton.textContent = "\u5BFC\u51FACSV";
|
|
const batchSubmitButton = document2.createElement("button");
|
|
batchSubmitButton.type = "button";
|
|
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
|
batchSubmitButton.textContent = "\u63D0\u4EA4\u6279\u6B21";
|
|
const exportStatusText = document2.createElement("span");
|
|
exportStatusText.dataset.pluginExportStatus = "text";
|
|
applyStatusStyles(exportStatusText);
|
|
root.append(
|
|
exportRangeSelect,
|
|
exportCustomPagesInput,
|
|
exportButton,
|
|
batchSubmitButton,
|
|
exportStatusText
|
|
);
|
|
document2.body.appendChild(root);
|
|
applyNativeControlStyles(document2, {
|
|
batchSubmitButton,
|
|
exportButton,
|
|
exportCustomPagesInput,
|
|
exportRangeSelect
|
|
});
|
|
ensureToolbarMounted(root, document2);
|
|
exportButton.addEventListener("click", () => {
|
|
void handlers.onExport();
|
|
});
|
|
batchSubmitButton.addEventListener("click", () => {
|
|
void handlers.onSubmitBatch();
|
|
});
|
|
exportRangeSelect.addEventListener("change", () => {
|
|
syncCustomPagesInputVisibility({
|
|
batchSubmitButton,
|
|
exportButton,
|
|
exportCustomPagesInput,
|
|
exportRangeSelect,
|
|
exportStatusText,
|
|
root
|
|
});
|
|
});
|
|
const toolbarDom = {
|
|
batchSubmitButton,
|
|
exportButton,
|
|
exportCustomPagesInput,
|
|
exportRangeSelect,
|
|
exportStatusText,
|
|
root
|
|
};
|
|
syncCustomPagesInputVisibility(toolbarDom);
|
|
return toolbarDom;
|
|
}
|
|
function appendOption(select, value, label) {
|
|
const option = select.ownerDocument.createElement("option");
|
|
option.value = value;
|
|
option.textContent = label;
|
|
select.appendChild(option);
|
|
}
|
|
function readToolbarDom(root) {
|
|
const toolbarDom = {
|
|
batchSubmitButton: root.querySelector(
|
|
'[data-plugin-batch-submit="button"]'
|
|
),
|
|
exportButton: root.querySelector(
|
|
'[data-plugin-export="button"]'
|
|
),
|
|
exportCustomPagesInput: root.querySelector(
|
|
'[data-plugin-export-custom-pages="input"]'
|
|
),
|
|
exportRangeSelect: root.querySelector(
|
|
'[data-plugin-export-range="select"]'
|
|
),
|
|
exportStatusText: root.querySelector(
|
|
'[data-plugin-export-status="text"]'
|
|
),
|
|
root
|
|
};
|
|
syncCustomPagesInputVisibility(toolbarDom);
|
|
return toolbarDom;
|
|
}
|
|
function readToolbarExportTarget(toolbar) {
|
|
const scope = toolbar.exportRangeSelect.value;
|
|
if (scope === "all") {
|
|
return {
|
|
target: {
|
|
mode: "all"
|
|
}
|
|
};
|
|
}
|
|
if (scope === "current") {
|
|
return {
|
|
target: {
|
|
mode: "count",
|
|
pageCount: 1
|
|
}
|
|
};
|
|
}
|
|
if (scope === "first-5") {
|
|
return {
|
|
target: {
|
|
mode: "count",
|
|
pageCount: 5
|
|
}
|
|
};
|
|
}
|
|
if (scope === "first-10") {
|
|
return {
|
|
target: {
|
|
mode: "count",
|
|
pageCount: 10
|
|
}
|
|
};
|
|
}
|
|
const pageCount = Number(toolbar.exportCustomPagesInput.value);
|
|
if (!Number.isInteger(pageCount) || pageCount < 1) {
|
|
return {
|
|
error: "\u8BF7\u8F93\u5165\u6709\u6548\u9875\u6570"
|
|
};
|
|
}
|
|
return {
|
|
target: {
|
|
mode: "count",
|
|
pageCount
|
|
}
|
|
};
|
|
}
|
|
function setToolbarBusyState(toolbar, isBusy) {
|
|
[
|
|
toolbar.batchSubmitButton,
|
|
toolbar.exportButton,
|
|
toolbar.exportRangeSelect,
|
|
toolbar.exportCustomPagesInput
|
|
].forEach((element) => {
|
|
element.disabled = isBusy;
|
|
});
|
|
}
|
|
function setToolbarExportStatus(toolbar, text) {
|
|
toolbar.exportStatusText.textContent = text;
|
|
}
|
|
function syncCustomPagesInputVisibility(toolbar) {
|
|
toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom";
|
|
}
|
|
function ensureToolbarMounted(root, document2) {
|
|
const actionRow = findNativeActionRow(document2);
|
|
if (!actionRow) {
|
|
root.hidden = true;
|
|
return;
|
|
}
|
|
const customizeButton = findNativeActionButton(actionRow, "\u81EA\u5B9A\u4E49\u6307\u6807");
|
|
const insertionAnchor = customizeButton ? findDirectChildAnchor(actionRow, customizeButton) : null;
|
|
if (insertionAnchor) {
|
|
actionRow.insertBefore(root, insertionAnchor);
|
|
} else if (root.parentElement !== actionRow) {
|
|
actionRow.prepend(root);
|
|
}
|
|
root.hidden = false;
|
|
}
|
|
function findNativeActionRow(document2) {
|
|
const customizeButton = findNativeActionButton(document2, "\u81EA\u5B9A\u4E49\u6307\u6807");
|
|
const exportButton = findNativeActionButton(document2, "\u5BFC\u51FA");
|
|
const header = findHeaderContainer(customizeButton, exportButton);
|
|
const sharedActionRow = customizeButton && exportButton ? findSmallestSharedActionRow(customizeButton, exportButton, header) : null;
|
|
if (sharedActionRow) {
|
|
return sharedActionRow;
|
|
}
|
|
const scope = header ?? document2;
|
|
const candidates = Array.from(
|
|
scope.querySelectorAll(".xt-space.xt-space--medium, .search-content--header")
|
|
).filter(
|
|
(element) => element instanceof document2.defaultView.HTMLElement
|
|
);
|
|
const rankedCandidates = candidates.filter(
|
|
(candidate) => isNativeActionRowCandidate(candidate, customizeButton, exportButton)
|
|
).sort((left, right) => {
|
|
const depthDelta = getDepthWithinAncestor(right, header) - getDepthWithinAncestor(left, header);
|
|
if (depthDelta !== 0) {
|
|
return depthDelta;
|
|
}
|
|
return normalizeText(left.textContent).length - normalizeText(right.textContent).length;
|
|
});
|
|
return rankedCandidates[0] ?? null;
|
|
}
|
|
function findHeaderContainer(customizeButton, exportButton) {
|
|
return customizeButton?.closest(".search-content--header") ?? exportButton?.closest(".search-content--header");
|
|
}
|
|
function findSmallestSharedActionRow(customizeButton, exportButton, boundary) {
|
|
const exportAncestors = new Set(collectAncestorChain(exportButton, boundary));
|
|
for (const candidate of collectAncestorChain(customizeButton, boundary)) {
|
|
if (exportAncestors.has(candidate) && isNativeActionRowCandidate(candidate, customizeButton, exportButton)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function collectAncestorChain(element, boundary) {
|
|
const ancestors = [];
|
|
let current = element.parentElement;
|
|
while (current) {
|
|
ancestors.push(current);
|
|
if (current === boundary) {
|
|
break;
|
|
}
|
|
current = current.parentElement;
|
|
}
|
|
return ancestors;
|
|
}
|
|
function isNativeActionRowCandidate(candidate, customizeButton, exportButton) {
|
|
if (customizeButton && !candidate.contains(customizeButton)) {
|
|
return false;
|
|
}
|
|
if (exportButton && !candidate.contains(exportButton)) {
|
|
return false;
|
|
}
|
|
const directChildLabels = Array.from(candidate.children).flatMap((child) => {
|
|
const buttons = [];
|
|
if (child instanceof candidate.ownerDocument.defaultView.HTMLButtonElement) {
|
|
buttons.push(child);
|
|
}
|
|
buttons.push(...Array.from(child.querySelectorAll("button")));
|
|
return buttons;
|
|
}).map((button) => normalizeText(button.textContent));
|
|
return directChildLabels.includes("\u5BFC\u51FA") && (directChildLabels.includes("\u81EA\u5B9A\u4E49\u6307\u6807") || Boolean(customizeButton));
|
|
}
|
|
function getDepthWithinAncestor(element, boundary) {
|
|
let depth = 0;
|
|
let current = element.parentElement;
|
|
while (current && current !== boundary) {
|
|
depth += 1;
|
|
current = current.parentElement;
|
|
}
|
|
return depth;
|
|
}
|
|
function findNativeActionButton(root, text) {
|
|
const document2 = root instanceof Document ? root : root.ownerDocument;
|
|
if (!document2) {
|
|
return null;
|
|
}
|
|
const candidates = Array.from(root.querySelectorAll("button")).filter(
|
|
(element) => element instanceof document2.defaultView.HTMLElement
|
|
);
|
|
return candidates.find((element) => normalizeText(element.textContent) === text) ?? null;
|
|
}
|
|
function applyToolbarRootStyles(root) {
|
|
root.style.display = "inline-flex";
|
|
root.style.alignItems = "center";
|
|
root.style.columnGap = "8px";
|
|
root.style.flexWrap = "wrap";
|
|
}
|
|
function applyNativeControlStyles(document2, controls) {
|
|
const primaryButton = findButtonContainingText(document2, "\u53D1\u5E03\u4EFB\u52A1") ?? findButtonContainingText(document2, "+\u53D1\u5E03\u4EFB\u52A1");
|
|
const nativeButton = primaryButton ?? findNativeActionButton(document2, "\u81EA\u5B9A\u4E49\u6307\u6807") ?? findNativeActionButton(document2, "\u5BFC\u51FA");
|
|
if (nativeButton) {
|
|
controls.exportButton.className = nativeButton.className;
|
|
controls.batchSubmitButton.className = nativeButton.className;
|
|
}
|
|
[controls.exportButton, controls.batchSubmitButton].forEach((button) => {
|
|
applyPrimaryButtonStyles2(button);
|
|
button.style.whiteSpace = "nowrap";
|
|
});
|
|
[controls.exportRangeSelect, controls.exportCustomPagesInput].forEach((element) => {
|
|
element.style.height = "32px";
|
|
element.style.border = "1px solid #d0d7de";
|
|
element.style.borderRadius = "6px";
|
|
element.style.padding = "0 10px";
|
|
element.style.background = "#fff";
|
|
element.style.color = "#1f2329";
|
|
element.style.boxSizing = "border-box";
|
|
});
|
|
controls.exportRangeSelect.style.minWidth = "104px";
|
|
controls.exportCustomPagesInput.style.width = "72px";
|
|
}
|
|
function applyPrimaryButtonStyles2(button) {
|
|
button.style.backgroundColor = "#7f1d2d";
|
|
button.style.border = "1px solid #7f1d2d";
|
|
button.style.borderRadius = "8px";
|
|
button.style.color = "#ffffff";
|
|
button.style.height = "32px";
|
|
button.style.padding = "0 15px";
|
|
button.style.boxSizing = "border-box";
|
|
button.style.fontWeight = "600";
|
|
button.style.transition = "background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
|
|
}
|
|
function applyStatusStyles(statusText) {
|
|
statusText.style.color = "#64748b";
|
|
statusText.style.fontSize = "12px";
|
|
statusText.style.lineHeight = "20px";
|
|
statusText.style.marginLeft = "4px";
|
|
statusText.style.whiteSpace = "nowrap";
|
|
}
|
|
function ensurePluginActionButtonTheme(document2) {
|
|
if (document2.getElementById(PLUGIN_ACTION_BUTTON_STYLE_ID)) {
|
|
return;
|
|
}
|
|
const style = document2.createElement("style");
|
|
style.id = PLUGIN_ACTION_BUTTON_STYLE_ID;
|
|
style.textContent = `
|
|
[data-plugin-export="button"]:hover:not(:disabled),
|
|
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
|
|
background-color: #6d1627 !important;
|
|
border-color: #6d1627 !important;
|
|
}
|
|
|
|
[data-plugin-export="button"]:active:not(:disabled),
|
|
[data-plugin-batch-submit="button"]:active:not(:disabled) {
|
|
background-color: #58111f !important;
|
|
border-color: #58111f !important;
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
[data-plugin-export="button"]:focus-visible,
|
|
[data-plugin-batch-submit="button"]:focus-visible {
|
|
outline: none !important;
|
|
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
|
}
|
|
|
|
[data-plugin-export="button"]:disabled,
|
|
[data-plugin-batch-submit="button"]:disabled {
|
|
background-color: #c89ca4 !important;
|
|
border-color: #c89ca4 !important;
|
|
color: rgba(255, 255, 255, 0.95) !important;
|
|
cursor: not-allowed !important;
|
|
opacity: 1 !important;
|
|
transform: none !important;
|
|
box-shadow: none !important;
|
|
}
|
|
`;
|
|
document2.head.appendChild(style);
|
|
}
|
|
function normalizeText(value) {
|
|
return value?.replace(/\s+/g, " ").trim() ?? "";
|
|
}
|
|
function findButtonContainingText(root, text) {
|
|
const document2 = root instanceof Document ? root : root.ownerDocument;
|
|
if (!document2) {
|
|
return null;
|
|
}
|
|
const candidates = Array.from(root.querySelectorAll("button")).filter(
|
|
(element) => element instanceof document2.defaultView.HTMLElement
|
|
);
|
|
return candidates.find((element) => normalizeText(element.textContent).includes(text)) ?? null;
|
|
}
|
|
function findDirectChildAnchor(ancestor, descendant) {
|
|
let current = descendant;
|
|
let previous = null;
|
|
while (current && current !== ancestor) {
|
|
previous = current;
|
|
current = current.parentElement;
|
|
}
|
|
return current === ancestor ? previous : null;
|
|
}
|
|
|
|
// src/content/market/market-list-request-snapshot.ts
|
|
var MARKET_REQUEST_SNAPSHOT_ATTRIBUTE = "data-sces-market-request-snapshot";
|
|
var MARKET_SEARCH_ENDPOINT_PATH = "/gw/api/gsearch/search_for_author_square";
|
|
function readMarketListRequestSnapshot(document2) {
|
|
const serializedSnapshot = document2.documentElement.getAttribute(
|
|
MARKET_REQUEST_SNAPSHOT_ATTRIBUTE
|
|
);
|
|
if (!serializedSnapshot) {
|
|
return readMarketListRequestSnapshotFromPageState(document2);
|
|
}
|
|
try {
|
|
const parsedSnapshot = normalizeMarketListRequestSnapshot(
|
|
JSON.parse(serializedSnapshot)
|
|
);
|
|
if (!parsedSnapshot) {
|
|
return readMarketListRequestSnapshotFromPageState(document2);
|
|
}
|
|
return parsedSnapshot;
|
|
} catch {
|
|
return readMarketListRequestSnapshotFromPageState(document2);
|
|
}
|
|
}
|
|
function isMarketListRequestSnapshot(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
const candidate = value;
|
|
return typeof candidate.method === "string" && typeof candidate.url === "string" && (!("body" in candidate) || typeof candidate.body === "string") && (!("headers" in candidate) || isStringRecord(candidate.headers));
|
|
}
|
|
function normalizeMarketListRequestSnapshot(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return null;
|
|
}
|
|
const candidate = value;
|
|
const normalizedSnapshot = {
|
|
body: typeof candidate.body === "string" ? candidate.body : void 0,
|
|
method: typeof candidate.method === "string" ? candidate.method : void 0,
|
|
url: typeof candidate.url === "string" ? candidate.url : void 0
|
|
};
|
|
if (candidate.headers && typeof candidate.headers === "object") {
|
|
normalizedSnapshot.headers = Object.fromEntries(
|
|
Object.entries(candidate.headers).filter(
|
|
([, entry]) => ["string", "number", "boolean"].includes(typeof entry)
|
|
).map(([key, entry]) => [key, String(entry)])
|
|
);
|
|
}
|
|
return isMarketListRequestSnapshot(normalizedSnapshot) ? normalizedSnapshot : null;
|
|
}
|
|
function isStringRecord(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
return Object.values(value).every((entry) => typeof entry === "string");
|
|
}
|
|
function readMarketListRequestSnapshotFromPageState(document2) {
|
|
const reqParams = findMarketReqParams(document2);
|
|
if (!reqParams) {
|
|
return null;
|
|
}
|
|
return {
|
|
body: JSON.stringify(reqParams),
|
|
method: "POST",
|
|
url: buildMarketSearchUrl(document2)
|
|
};
|
|
}
|
|
function findMarketReqParams(document2) {
|
|
const marketRoot = document2.querySelector(".base-author-list");
|
|
const setupState = marketRoot?.__vue__?._setupState;
|
|
if (!setupState) {
|
|
return null;
|
|
}
|
|
const queue = Object.values(setupState);
|
|
while (queue.length > 0) {
|
|
const current = unwrapVueRef3(queue.shift());
|
|
if (!isRecord4(current)) {
|
|
continue;
|
|
}
|
|
const reqParams = unwrapVueRef3(current.reqParams);
|
|
if (isRecord4(reqParams)) {
|
|
return reqParams;
|
|
}
|
|
Object.values(current).forEach((value) => {
|
|
queue.push(value);
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
function buildMarketSearchUrl(document2) {
|
|
if (document2.location?.origin && document2.location.origin !== "null" && document2.location.origin !== "about:blank") {
|
|
return document2.location.origin.includes("xingtu.cn") ? MARKET_SEARCH_ENDPOINT_PATH : new URL(MARKET_SEARCH_ENDPOINT_PATH, document2.location.origin).toString();
|
|
}
|
|
return MARKET_SEARCH_ENDPOINT_PATH;
|
|
}
|
|
function unwrapVueRef3(value) {
|
|
if (isRecord4(value) && "value" in value) {
|
|
return value.value;
|
|
}
|
|
return value;
|
|
}
|
|
function isRecord4(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
// src/content/market/silent-export-controller.ts
|
|
var PAGE_NUMBER_KEYS2 = [
|
|
"currentPage",
|
|
"page",
|
|
"pageNo",
|
|
"pageNum",
|
|
"page_no",
|
|
"page_num"
|
|
];
|
|
function createSilentExportController(options) {
|
|
const fetchImpl = options.fetchImpl ?? defaultFetch2;
|
|
return {
|
|
async exportRecords(target) {
|
|
const snapshot = readMarketListRequestSnapshot(options.document);
|
|
if (!snapshot) {
|
|
return null;
|
|
}
|
|
const baseRequest = createPagedRequest(snapshot);
|
|
if (!baseRequest) {
|
|
return null;
|
|
}
|
|
const mergedRecords = /* @__PURE__ */ new Map();
|
|
const maxPageCount = target.mode === "count" ? target.pageCount : 200;
|
|
let totalPagesHint;
|
|
for (let offset = 0; offset < maxPageCount; offset += 1) {
|
|
const pageNumber = baseRequest.initialPage + offset;
|
|
options.onProgress?.({
|
|
currentPage: offset + 1,
|
|
totalPages: target.mode === "count" ? target.pageCount : totalPagesHint
|
|
});
|
|
const payload = await fetchPagePayload(fetchImpl, baseRequest, pageNumber);
|
|
const parsedResponse = parseMarketListResponse(payload);
|
|
if (!parsedResponse) {
|
|
return null;
|
|
}
|
|
totalPagesHint = parsedResponse.totalPages ?? totalPagesHint;
|
|
if (parsedResponse.records.length === 0) {
|
|
break;
|
|
}
|
|
parsedResponse.records.forEach((record) => {
|
|
const existingRecord = mergedRecords.get(record.authorId);
|
|
mergedRecords.set(record.authorId, mergeMarketRecord2(existingRecord, record));
|
|
});
|
|
if (target.mode === "count" && offset + 1 >= target.pageCount) {
|
|
break;
|
|
}
|
|
if (target.mode === "all") {
|
|
if (typeof parsedResponse.totalPages === "number" && pageNumber >= parsedResponse.totalPages) {
|
|
break;
|
|
}
|
|
if (typeof parsedResponse.pageSize === "number" && parsedResponse.records.length < parsedResponse.pageSize) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return Array.from(mergedRecords.values());
|
|
}
|
|
};
|
|
}
|
|
function createPagedRequest(snapshot) {
|
|
const bodyPage = readPageFromBody(snapshot.body);
|
|
if (bodyPage !== null) {
|
|
return {
|
|
initialPage: bodyPage,
|
|
pageSource: "body",
|
|
snapshot
|
|
};
|
|
}
|
|
const urlPage = readPageFromUrl(snapshot.url);
|
|
if (urlPage !== null) {
|
|
return {
|
|
initialPage: urlPage,
|
|
pageSource: "url",
|
|
snapshot
|
|
};
|
|
}
|
|
return {
|
|
initialPage: 1,
|
|
pageSource: "none",
|
|
snapshot
|
|
};
|
|
}
|
|
async function fetchPagePayload(fetchImpl, request, pageNumber) {
|
|
const nextUrl = request.pageSource === "url" ? mutateUrlPage(request.snapshot.url, pageNumber) : request.snapshot.url;
|
|
const nextBody = mutateBodyPage(request.snapshot.body, pageNumber);
|
|
const response = await fetchImpl(nextUrl, {
|
|
body: nextBody,
|
|
credentials: "include",
|
|
headers: filterReplayHeaders(request.snapshot.headers, nextBody),
|
|
method: request.snapshot.method
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error("\u9759\u9ED8\u5BFC\u51FA\u8BF7\u6C42\u5931\u8D25");
|
|
}
|
|
return response.json();
|
|
}
|
|
function readPageFromUrl(url) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
for (const key of PAGE_NUMBER_KEYS2) {
|
|
const value = readNumericString(parsedUrl.searchParams.get(key));
|
|
if (value !== null) {
|
|
return value;
|
|
}
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
function mutateUrlPage(url, pageNumber) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
for (const key of PAGE_NUMBER_KEYS2) {
|
|
if (!parsedUrl.searchParams.has(key)) {
|
|
continue;
|
|
}
|
|
parsedUrl.searchParams.set(key, String(pageNumber));
|
|
return parsedUrl.toString();
|
|
}
|
|
parsedUrl.searchParams.set("page", String(pageNumber));
|
|
return parsedUrl.toString();
|
|
} catch {
|
|
return url;
|
|
}
|
|
}
|
|
function readPageFromBody(body) {
|
|
const parsedBody = parseBody(body);
|
|
if (!parsedBody) {
|
|
return null;
|
|
}
|
|
return readKnownPaginationNumber(parsedBody, "page");
|
|
}
|
|
function mutateBodyPage(body, pageNumber) {
|
|
if (!body) {
|
|
return body;
|
|
}
|
|
const trimmedBody = body.trim();
|
|
if (!trimmedBody) {
|
|
return body;
|
|
}
|
|
try {
|
|
const parsedJson = JSON.parse(trimmedBody);
|
|
if (!replacePageNumberInValue(parsedJson, pageNumber) && isRecord5(parsedJson)) {
|
|
parsedJson.page = pageNumber;
|
|
}
|
|
return JSON.stringify(parsedJson);
|
|
} catch {
|
|
const searchParams = new URLSearchParams(trimmedBody);
|
|
for (const key of PAGE_NUMBER_KEYS2) {
|
|
if (!searchParams.has(key)) {
|
|
continue;
|
|
}
|
|
searchParams.set(key, String(pageNumber));
|
|
return searchParams.toString();
|
|
}
|
|
searchParams.set("page", String(pageNumber));
|
|
return searchParams.toString();
|
|
}
|
|
}
|
|
function parseBody(body) {
|
|
if (!body) {
|
|
return null;
|
|
}
|
|
const trimmedBody = body.trim();
|
|
if (!trimmedBody) {
|
|
return null;
|
|
}
|
|
try {
|
|
const parsedBody = JSON.parse(trimmedBody);
|
|
return isRecord5(parsedBody) ? parsedBody : null;
|
|
} catch {
|
|
const searchParams = new URLSearchParams(trimmedBody);
|
|
const payload = {};
|
|
searchParams.forEach((value, key) => {
|
|
payload[key] = value;
|
|
});
|
|
return payload;
|
|
}
|
|
}
|
|
function replacePageNumberInValue(value, pageNumber) {
|
|
if (!isRecord5(value)) {
|
|
return false;
|
|
}
|
|
let replaced = false;
|
|
PAGE_NUMBER_KEYS2.forEach((key) => {
|
|
if (!(key in value)) {
|
|
return;
|
|
}
|
|
value[key] = pageNumber;
|
|
replaced = true;
|
|
});
|
|
if (replaced) {
|
|
return true;
|
|
}
|
|
return Object.values(value).some((entry) => replacePageNumberInValue(entry, pageNumber));
|
|
}
|
|
function filterReplayHeaders(headers, body) {
|
|
const filteredHeaders = Object.fromEntries(
|
|
Object.entries(headers ?? {}).filter(([key]) => {
|
|
const normalizedKey = key.toLowerCase();
|
|
return normalizedKey !== "content-length" && normalizedKey !== "host";
|
|
})
|
|
);
|
|
if (body) {
|
|
if (!hasHeader(filteredHeaders, "accept")) {
|
|
filteredHeaders.Accept = "application/json, text/plain, */*";
|
|
}
|
|
if (!hasHeader(filteredHeaders, "content-type")) {
|
|
filteredHeaders["Content-Type"] = "application/json";
|
|
}
|
|
if (!hasHeader(filteredHeaders, "x-login-source")) {
|
|
filteredHeaders["x-login-source"] = "1";
|
|
}
|
|
if (!hasHeader(filteredHeaders, "agw-js-conv")) {
|
|
filteredHeaders["Agw-Js-Conv"] = "str";
|
|
}
|
|
}
|
|
return Object.keys(filteredHeaders).length > 0 ? filteredHeaders : void 0;
|
|
}
|
|
function hasHeader(headers, key) {
|
|
return Object.keys(headers).some((headerKey) => headerKey.toLowerCase() === key);
|
|
}
|
|
function readNumericString(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const parsedValue = Number(value);
|
|
return Number.isFinite(parsedValue) ? parsedValue : null;
|
|
}
|
|
async function defaultFetch2(input, init) {
|
|
return fetch(input, init);
|
|
}
|
|
function isRecord5(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
function mergeMarketRecord2(existingRecord, incomingRecord) {
|
|
if (!existingRecord) {
|
|
return {
|
|
...incomingRecord,
|
|
exportFields: mergeFieldMap2(void 0, incomingRecord.exportFields),
|
|
rates: mergeFieldMap2(void 0, incomingRecord.rates),
|
|
status: incomingRecord.status ?? "idle"
|
|
};
|
|
}
|
|
return {
|
|
...existingRecord,
|
|
...incomingRecord,
|
|
authorName: mergeStringValue2(existingRecord.authorName, incomingRecord.authorName) ?? "",
|
|
coreUserId: mergeStringValue2(existingRecord.coreUserId, incomingRecord.coreUserId),
|
|
exportFields: mergeFieldMap2(
|
|
existingRecord.exportFields,
|
|
incomingRecord.exportFields
|
|
),
|
|
failureReason: incomingRecord.failureReason ?? existingRecord.failureReason,
|
|
hasDirectRatesSource: existingRecord.hasDirectRatesSource || incomingRecord.hasDirectRatesSource,
|
|
location: mergeStringValue2(existingRecord.location, incomingRecord.location),
|
|
price21To60s: mergeStringValue2(
|
|
existingRecord.price21To60s,
|
|
incomingRecord.price21To60s
|
|
),
|
|
rates: mergeFieldMap2(existingRecord.rates, incomingRecord.rates),
|
|
status: incomingRecord.status ?? existingRecord.status
|
|
};
|
|
}
|
|
function mergeFieldMap2(current, incoming) {
|
|
if (!current && !incoming) {
|
|
return void 0;
|
|
}
|
|
const merged = {
|
|
...current ?? {}
|
|
};
|
|
Object.entries(incoming ?? {}).forEach(([key, value]) => {
|
|
const currentValue = merged[key];
|
|
if (hasTextValue4(value) || !hasTextValue4(currentValue)) {
|
|
merged[key] = value;
|
|
}
|
|
});
|
|
return merged;
|
|
}
|
|
function mergeStringValue2(current, incoming) {
|
|
return hasTextValue4(incoming) ? incoming : current;
|
|
}
|
|
function hasTextValue4(value) {
|
|
return Boolean(value && value.trim().length > 0);
|
|
}
|
|
|
|
// src/content/market/result-store.ts
|
|
function createMarketResultStore() {
|
|
const records = /* @__PURE__ */ new Map();
|
|
return {
|
|
getRecord(authorId) {
|
|
return records.get(authorId) ?? null;
|
|
},
|
|
listRecords() {
|
|
return Array.from(records.values());
|
|
},
|
|
setAuthorFailed(authorId, reason) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.status = "failed";
|
|
existingRecord.failureReason = reason;
|
|
},
|
|
setAuthorLoading(authorId) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.status = "loading";
|
|
delete existingRecord.failureReason;
|
|
},
|
|
setBackendMetricsFailed(authorId) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.backendMetricsStatus = "failed";
|
|
},
|
|
setBackendMetricsLoading(authorId) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.backendMetricsStatus = "loading";
|
|
},
|
|
setBackendMetricsMissing(authorId) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.backendMetricsStatus = "missing";
|
|
},
|
|
setBackendMetricsSuccess(authorId, backendMetrics) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.backendMetricsStatus = "success";
|
|
existingRecord.backendMetrics = {
|
|
...existingRecord.backendMetrics,
|
|
...backendMetrics
|
|
};
|
|
},
|
|
setAuthorSuccess(authorId, rates) {
|
|
const existingRecord = ensureRecord(authorId);
|
|
existingRecord.status = "success";
|
|
existingRecord.rates = {
|
|
...existingRecord.rates,
|
|
...rates
|
|
};
|
|
delete existingRecord.failureReason;
|
|
},
|
|
upsertMarketRow(row) {
|
|
const existingRecord = records.get(row.authorId);
|
|
if (existingRecord) {
|
|
existingRecord.authorName = mergeStringValue3(existingRecord.authorName, row.authorName) ?? existingRecord.authorName;
|
|
existingRecord.coreUserId = mergeStringValue3(
|
|
existingRecord.coreUserId,
|
|
row.coreUserId
|
|
);
|
|
existingRecord.location = mergeStringValue3(
|
|
existingRecord.location,
|
|
row.location
|
|
);
|
|
existingRecord.price21To60s = mergeStringValue3(
|
|
existingRecord.price21To60s,
|
|
row.price21To60s
|
|
);
|
|
existingRecord.exportFields = mergeFieldMap3(
|
|
existingRecord.exportFields,
|
|
row.exportFields
|
|
);
|
|
existingRecord.backendMetrics = mergeFieldMap3(
|
|
existingRecord.backendMetrics,
|
|
row.backendMetrics
|
|
);
|
|
existingRecord.hasDirectRatesSource = existingRecord.hasDirectRatesSource || row.hasDirectRatesSource;
|
|
existingRecord.rates = mergeFieldMap3(existingRecord.rates, row.rates);
|
|
return existingRecord;
|
|
}
|
|
const nextRecord = {
|
|
...row,
|
|
backendMetricsStatus: "idle",
|
|
status: "idle"
|
|
};
|
|
records.set(row.authorId, nextRecord);
|
|
return nextRecord;
|
|
}
|
|
};
|
|
function ensureRecord(authorId) {
|
|
const existingRecord = records.get(authorId);
|
|
if (existingRecord) {
|
|
return existingRecord;
|
|
}
|
|
const nextRecord = {
|
|
authorId,
|
|
authorName: authorId,
|
|
backendMetricsStatus: "idle",
|
|
status: "idle"
|
|
};
|
|
records.set(authorId, nextRecord);
|
|
return nextRecord;
|
|
}
|
|
}
|
|
function mergeFieldMap3(current, incoming) {
|
|
if (!current && !incoming) {
|
|
return void 0;
|
|
}
|
|
const merged = {
|
|
...current ?? {}
|
|
};
|
|
Object.entries(incoming ?? {}).forEach(([key, value]) => {
|
|
const currentValue = merged[key];
|
|
if (!hasTextValue5(currentValue)) {
|
|
merged[key] = value;
|
|
}
|
|
});
|
|
return merged;
|
|
}
|
|
function mergeStringValue3(current, incoming) {
|
|
if (!hasTextValue5(current)) {
|
|
return incoming ?? current;
|
|
}
|
|
return current;
|
|
}
|
|
function hasTextValue5(value) {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
// src/shared/auth-messages.ts
|
|
function isAuthResponseMessage(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
const candidate = value;
|
|
if (candidate.ok === false) {
|
|
return candidate.type === "auth:error" && typeof candidate.error === "string";
|
|
}
|
|
if (candidate.ok !== true || typeof candidate.type !== "string") {
|
|
return false;
|
|
}
|
|
if (candidate.type === "auth:ack") {
|
|
return true;
|
|
}
|
|
if (candidate.type === "auth:token") {
|
|
return Boolean(
|
|
candidate.value && typeof candidate.value === "object" && typeof candidate.value.accessToken === "string"
|
|
);
|
|
}
|
|
if (candidate.type === "auth:state") {
|
|
return Boolean(
|
|
candidate.value && typeof candidate.value === "object" && typeof candidate.value.isAuthenticated === "boolean"
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// src/shared/backend-metrics-messages.ts
|
|
function isBackendMetricsResponseMessage(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
const candidate = value;
|
|
if (candidate.ok === false) {
|
|
return candidate.type === "backend-metrics:error" && typeof candidate.error === "string";
|
|
}
|
|
return Boolean(
|
|
candidate.ok === true && candidate.type === "backend-metrics:result" && candidate.value && typeof candidate.value === "object" && Array.isArray(candidate.value.rows)
|
|
);
|
|
}
|
|
|
|
// src/content/market/index.ts
|
|
function createMarketController(options) {
|
|
const marketApiClient = createMarketApiClient();
|
|
const sendRuntimeMessage = createRuntimeMessageSender();
|
|
const resultStore = options.resultStore ?? createMarketResultStore();
|
|
const loadAuthorMetrics = options.loadAuthorMetrics ?? marketApiClient.loadAuthorAseInfo;
|
|
const searchBackendMetrics = options.searchBackendMetrics ?? (hasRuntimeMessageSender() ? (starIds) => readBackendMetrics(sendRuntimeMessage, starIds) : null);
|
|
const buildCsv = options.buildCsv ?? buildMarketCsv;
|
|
const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage));
|
|
const mutationObserverFactory = options.mutationObserverFactory ?? ((callback) => new MutationObserver(callback));
|
|
const promptBatchName = options.promptBatchName ?? (() => promptForBatchName(options.document));
|
|
const submitBatch = options.submitBatch ?? ((payload) => readBatchSubmitAck(sendRuntimeMessage, payload));
|
|
let activeProgressLabel = "\u5BFC\u51FA\u4E2D";
|
|
let shouldShowDetailedProgress = true;
|
|
const exportRangeController = createExportRangeController({
|
|
document: options.document,
|
|
onProgress: ({ currentPage, totalPages }) => {
|
|
updateToolbarProgress(currentPage, totalPages);
|
|
},
|
|
prepareCurrentPageForExport,
|
|
readCurrentPageRecords: () => getVisibleOrderedRecords(),
|
|
readCurrentPageRowCount: () => countCurrentPageRows(options.document),
|
|
window: options.window
|
|
});
|
|
const silentExportController = createSilentExportController({
|
|
document: options.document,
|
|
onProgress: ({ currentPage, totalPages }) => {
|
|
updateToolbarProgress(currentPage, totalPages);
|
|
}
|
|
});
|
|
let activeSort;
|
|
let isDisposed = false;
|
|
let isSyncRunning = false;
|
|
let isSyncScheduled = false;
|
|
let lastKnownPageSignature = "";
|
|
let needsResync = false;
|
|
let scheduledSyncTimeoutId = null;
|
|
const selectedAuthorIds = /* @__PURE__ */ new Set();
|
|
let toolbar;
|
|
const observer = mutationObserverFactory(() => {
|
|
if (isDisposed) {
|
|
return;
|
|
}
|
|
let nextPageSignature = lastKnownPageSignature;
|
|
try {
|
|
nextPageSignature = readMarketPageSignature(options.document);
|
|
} catch {
|
|
return;
|
|
}
|
|
const toolbarNeedsRemount = !toolbar || !isPluginToolbarMounted(toolbar.root, options.document);
|
|
const selectionControlsMissing = !options.document.querySelector('[data-market-selection-checkbox="row"]') || !options.document.querySelector('[data-market-selection-checkbox="header"]');
|
|
if (nextPageSignature === lastKnownPageSignature && !toolbarNeedsRemount && !selectionControlsMissing) {
|
|
return;
|
|
}
|
|
scheduleSync();
|
|
});
|
|
const observationRoot = options.document.body ?? options.document.documentElement;
|
|
startObserving();
|
|
const toolbarHandlers = {
|
|
onExport: async () => {
|
|
syncSelectionStateFromDom();
|
|
const exportTarget = readToolbarExportTarget(toolbar);
|
|
if (!exportTarget.target) {
|
|
setToolbarExportStatus(toolbar, exportTarget.error ?? "\u5BFC\u51FA\u914D\u7F6E\u65E0\u6548");
|
|
return;
|
|
}
|
|
setToolbarBusyState(toolbar, true);
|
|
try {
|
|
const records = filterRecordsBySelection(
|
|
await exportRecords(exportTarget.target, "\u5BFC\u51FA\u4E2D", {
|
|
showDetailedProgress: selectedAuthorIds.size === 0
|
|
})
|
|
);
|
|
options.onCsvReady?.(buildCsv(records));
|
|
setToolbarExportStatus(toolbar, "");
|
|
} catch (error) {
|
|
setToolbarExportStatus(
|
|
toolbar,
|
|
error instanceof Error ? error.message : "\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
|
|
);
|
|
} finally {
|
|
setToolbarBusyState(toolbar, false);
|
|
}
|
|
},
|
|
onSubmitBatch: async () => {
|
|
syncSelectionStateFromDom();
|
|
const exportTarget = readToolbarExportTarget(toolbar);
|
|
if (!exportTarget.target) {
|
|
setToolbarExportStatus(toolbar, exportTarget.error ?? "\u5BFC\u51FA\u914D\u7F6E\u65E0\u6548");
|
|
return;
|
|
}
|
|
const batchName = await promptBatchName();
|
|
if (batchName === null) {
|
|
return;
|
|
}
|
|
if (!batchName.trim()) {
|
|
setToolbarExportStatus(toolbar, "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0");
|
|
return;
|
|
}
|
|
setToolbarBusyState(toolbar, true);
|
|
try {
|
|
const hasSelectedAuthors = selectedAuthorIds.size > 0;
|
|
const records = filterRecordsBySelection(
|
|
await exportRecords(
|
|
exportTarget.target,
|
|
hasSelectedAuthors ? "\u63D0\u4EA4\u5DF2\u9009\u8FBE\u4EBA\u4E2D" : "\u63D0\u4EA4\u4E2D",
|
|
{
|
|
showDetailedProgress: !hasSelectedAuthors
|
|
}
|
|
)
|
|
);
|
|
const authState = await getAuthState();
|
|
if (!authState.isAuthenticated) {
|
|
throw new Error("\u8BF7\u5148\u767B\u5F55\u63D2\u4EF6");
|
|
}
|
|
const payload = createBatchPayload({
|
|
authState,
|
|
batchName,
|
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
records
|
|
});
|
|
await submitBatch(payload);
|
|
setToolbarExportStatus(toolbar, "\u6279\u6B21\u63D0\u4EA4\u6210\u529F");
|
|
} catch (error) {
|
|
setToolbarExportStatus(
|
|
toolbar,
|
|
error instanceof Error ? error.message : "\u6279\u6B21\u63D0\u4EA4\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
|
|
);
|
|
} finally {
|
|
setToolbarBusyState(toolbar, false);
|
|
}
|
|
}
|
|
};
|
|
toolbar = ensurePluginToolbar(options.document, toolbarHandlers);
|
|
const ready = runSyncCycle();
|
|
return {
|
|
dispose() {
|
|
isDisposed = true;
|
|
observer.disconnect();
|
|
if (scheduledSyncTimeoutId !== null) {
|
|
options.window.clearTimeout(scheduledSyncTimeoutId);
|
|
scheduledSyncTimeoutId = null;
|
|
}
|
|
},
|
|
ready
|
|
};
|
|
async function hydrateCurrentPage() {
|
|
const table = syncMarketTable(options.document);
|
|
if (!table) {
|
|
return;
|
|
}
|
|
const pageRows = [];
|
|
for (const rowDom of table.rows) {
|
|
const rowSnapshot = readRowSnapshot(rowDom);
|
|
if (!rowSnapshot.authorId || !hasTextValue6(rowSnapshot.authorName)) {
|
|
continue;
|
|
}
|
|
pageRows.push({
|
|
rowDom,
|
|
rowSnapshot
|
|
});
|
|
resultStore.upsertMarketRow(rowSnapshot);
|
|
}
|
|
const pendingRateRows = [];
|
|
const rowsNeedingBackendMetrics = [];
|
|
pageRows.forEach(({ rowDom, rowSnapshot }) => {
|
|
if (rowSnapshot.hasDirectRatesSource) {
|
|
resultStore.setAuthorSuccess(rowSnapshot.authorId, rowSnapshot.rates ?? {});
|
|
}
|
|
const existingRecord = resultStore.getRecord(rowSnapshot.authorId);
|
|
const needsRateFetch = !hasSettledRateState(existingRecord) && !hasCompleteRates(existingRecord?.rates);
|
|
const needsBackendMetrics = Boolean(searchBackendMetrics) && !hasSettledBackendMetricsState(existingRecord);
|
|
if (needsRateFetch) {
|
|
resultStore.setAuthorLoading(rowSnapshot.authorId);
|
|
pendingRateRows.push({
|
|
rowDom,
|
|
rowSnapshot
|
|
});
|
|
}
|
|
if (needsBackendMetrics) {
|
|
resultStore.setBackendMetricsLoading(rowSnapshot.authorId);
|
|
rowsNeedingBackendMetrics.push({
|
|
rowDom,
|
|
rowSnapshot
|
|
});
|
|
}
|
|
if (needsRateFetch || needsBackendMetrics) {
|
|
renderMarketRowState(rowDom, {
|
|
...existingRecord ?? {
|
|
authorId: rowSnapshot.authorId,
|
|
authorName: rowSnapshot.authorName,
|
|
status: "idle"
|
|
},
|
|
...rowSnapshot,
|
|
backendMetricsStatus: needsBackendMetrics ? "loading" : existingRecord?.backendMetricsStatus,
|
|
rates: existingRecord?.rates,
|
|
status: needsRateFetch || needsBackendMetrics ? "loading" : existingRecord?.status ?? "idle"
|
|
});
|
|
return;
|
|
}
|
|
if (existingRecord) {
|
|
renderMarketRowState(rowDom, existingRecord);
|
|
}
|
|
});
|
|
await Promise.all([
|
|
hydrateRatesForRows(pendingRateRows),
|
|
hydrateBackendMetricsForPage(rowsNeedingBackendMetrics)
|
|
]);
|
|
pageRows.forEach(({ rowDom, rowSnapshot }) => {
|
|
const record = resultStore.getRecord(rowSnapshot.authorId);
|
|
if (!record) {
|
|
return;
|
|
}
|
|
renderMarketRowState(rowDom, record);
|
|
});
|
|
}
|
|
async function hydrateRatesForRows(pageRows) {
|
|
if (pageRows.length === 0) {
|
|
return;
|
|
}
|
|
await Promise.all(
|
|
pageRows.map(async ({ rowSnapshot }) => {
|
|
const metricsResult = await loadAuthorMetrics(rowSnapshot.authorId);
|
|
if (metricsResult.success) {
|
|
resultStore.setAuthorSuccess(rowSnapshot.authorId, metricsResult.rates);
|
|
return;
|
|
}
|
|
resultStore.setAuthorFailed(rowSnapshot.authorId, metricsResult.reason);
|
|
})
|
|
);
|
|
}
|
|
async function hydrateBackendMetricsForPage(pageRows) {
|
|
if (!searchBackendMetrics || pageRows.length === 0) {
|
|
return;
|
|
}
|
|
try {
|
|
const rows = await searchBackendMetrics(
|
|
pageRows.map(({ rowSnapshot }) => rowSnapshot.authorId)
|
|
);
|
|
const rowMap = new Map(rows.map((row) => [row.starId, row]));
|
|
pageRows.forEach(({ rowSnapshot }) => {
|
|
const backendMetrics = rowMap.get(rowSnapshot.authorId);
|
|
if (backendMetrics) {
|
|
resultStore.setBackendMetricsSuccess(rowSnapshot.authorId, backendMetrics);
|
|
} else {
|
|
resultStore.setBackendMetricsMissing(rowSnapshot.authorId);
|
|
}
|
|
});
|
|
} catch {
|
|
pageRows.forEach(({ rowSnapshot }) => {
|
|
resultStore.setBackendMetricsFailed(rowSnapshot.authorId);
|
|
});
|
|
}
|
|
}
|
|
function applyCurrentView() {
|
|
runWithoutMutationSync(() => {
|
|
toolbar = ensurePluginToolbar(options.document, toolbarHandlers);
|
|
const table = syncMarketTable(options.document);
|
|
if (!table) {
|
|
return;
|
|
}
|
|
syncPluginSortHeaders(options.document, {
|
|
activeSort,
|
|
onToggleSort: toggleSortFromHeader
|
|
});
|
|
const records = getVisibleOrderedRecords(table);
|
|
applyRowVisibility(table, new Set(records.map((record) => record.authorId)));
|
|
applyRowOrder(table, records.map((record) => record.authorId));
|
|
bindSelectionControls(table);
|
|
syncMarketSelectionState(table, selectedAuthorIds);
|
|
lastKnownPageSignature = readMarketPageSignature(options.document);
|
|
});
|
|
}
|
|
function bindSelectionControls(table) {
|
|
if (!table) {
|
|
return;
|
|
}
|
|
table.rows.forEach((rowDom) => {
|
|
rowDom.selectionCheckbox.dataset.marketSelectionAuthorId = rowDom.authorId;
|
|
if (rowDom.selectionCheckbox.dataset.marketSelectionBound === "true") {
|
|
return;
|
|
}
|
|
rowDom.selectionCheckbox.dataset.marketSelectionBound = "true";
|
|
rowDom.selectionCheckbox.addEventListener("change", () => {
|
|
if (rowDom.selectionCheckbox.checked) {
|
|
selectedAuthorIds.add(rowDom.authorId);
|
|
} else {
|
|
selectedAuthorIds.delete(rowDom.authorId);
|
|
}
|
|
refreshSelectionControls();
|
|
});
|
|
});
|
|
if (!table.headerSelectionCheckbox) {
|
|
return;
|
|
}
|
|
if (table.headerSelectionCheckbox.dataset.marketSelectionBound === "true") {
|
|
return;
|
|
}
|
|
table.headerSelectionCheckbox.dataset.marketSelectionBound = "true";
|
|
table.headerSelectionCheckbox.addEventListener("change", () => {
|
|
const currentTable = syncMarketTable(options.document);
|
|
if (!currentTable) {
|
|
return;
|
|
}
|
|
const visibleRows = currentTable.rows.filter(
|
|
(rowDom) => rowDom.visibilityTargets.some((target) => !target.hidden)
|
|
);
|
|
const scopedRows = visibleRows.length > 0 ? visibleRows : currentTable.rows;
|
|
if (table.headerSelectionCheckbox?.checked) {
|
|
scopedRows.forEach((rowDom) => {
|
|
selectedAuthorIds.add(rowDom.authorId);
|
|
});
|
|
} else {
|
|
scopedRows.forEach((rowDom) => {
|
|
selectedAuthorIds.delete(rowDom.authorId);
|
|
});
|
|
}
|
|
refreshSelectionControls();
|
|
});
|
|
}
|
|
function refreshSelectionControls() {
|
|
const table = syncMarketTable(options.document);
|
|
if (!table) {
|
|
return;
|
|
}
|
|
bindSelectionControls(table);
|
|
syncMarketSelectionState(table, selectedAuthorIds);
|
|
}
|
|
function syncSelectionStateFromDom() {
|
|
const rowSelectionCheckboxes = Array.from(
|
|
options.document.querySelectorAll('[data-market-selection-checkbox="row"]')
|
|
).filter(
|
|
(element) => element instanceof HTMLInputElement
|
|
);
|
|
if (rowSelectionCheckboxes.length === 0) {
|
|
return;
|
|
}
|
|
rowSelectionCheckboxes.forEach((checkbox) => {
|
|
const authorId = checkbox.dataset.marketSelectionAuthorId?.trim();
|
|
if (!authorId) {
|
|
return;
|
|
}
|
|
if (checkbox.checked) {
|
|
selectedAuthorIds.add(authorId);
|
|
} else {
|
|
selectedAuthorIds.delete(authorId);
|
|
}
|
|
});
|
|
refreshSelectionControls();
|
|
}
|
|
function toggleSortFromHeader(field) {
|
|
activeSort = getNextSortState(activeSort, field);
|
|
applyCurrentView();
|
|
}
|
|
function getVisibleOrderedRecords(table = syncMarketTable(options.document)) {
|
|
const currentPageRecords = readCurrentPageRecords(table);
|
|
return applyFilterAndSort(currentPageRecords, {
|
|
sort: activeSort
|
|
});
|
|
}
|
|
async function exportRecords(target, inProgressLabel = "\u5BFC\u51FA\u4E2D", progressOptions = {}) {
|
|
activeProgressLabel = inProgressLabel;
|
|
shouldShowDetailedProgress = progressOptions.showDetailedProgress ?? true;
|
|
setToolbarExportStatus(toolbar, `${inProgressLabel}...`);
|
|
if (target.mode === "count" && target.pageCount <= 1) {
|
|
await prepareCurrentPageForExport();
|
|
return getVisibleOrderedRecords();
|
|
}
|
|
const silentExportRecords = await silentExportController.exportRecords(target);
|
|
if (silentExportRecords) {
|
|
return hydrateExportRecords(
|
|
silentExportRecords.map((record) => ({
|
|
...record,
|
|
status: record.status ?? "idle"
|
|
}))
|
|
);
|
|
}
|
|
return exportRangeController.exportRecords(target);
|
|
}
|
|
function updateToolbarProgress(currentPage, totalPages) {
|
|
if (!shouldShowDetailedProgress) {
|
|
setToolbarExportStatus(toolbar, `${activeProgressLabel}...`);
|
|
return;
|
|
}
|
|
setToolbarExportStatus(
|
|
toolbar,
|
|
totalPages ? `${activeProgressLabel} ${currentPage}/${totalPages} \u9875...` : `${activeProgressLabel} \u7B2C${currentPage}\u9875...`
|
|
);
|
|
}
|
|
function filterRecordsBySelection(records) {
|
|
if (selectedAuthorIds.size === 0) {
|
|
return records;
|
|
}
|
|
const selectedRecords = records.filter(
|
|
(record) => selectedAuthorIds.has(record.authorId)
|
|
);
|
|
return selectedRecords.length > 0 ? selectedRecords : records;
|
|
}
|
|
async function prepareCurrentPageForExport() {
|
|
await runSyncCycle();
|
|
await harvestCurrentPageForExport();
|
|
await runSyncCycle();
|
|
}
|
|
async function hydrateExportRecords(records) {
|
|
for (const record of records) {
|
|
resultStore.upsertMarketRow(record);
|
|
const existingRecord = resultStore.getRecord(record.authorId);
|
|
if (existingRecord?.status === "success" && existingRecord.rates) {
|
|
continue;
|
|
}
|
|
if (record.hasDirectRatesSource) {
|
|
const directRates = record.rates ?? {};
|
|
const hasAllRates = Boolean(directRates.singleVideoAfterSearchRate) && Boolean(directRates.personalVideoAfterSearchRate);
|
|
resultStore.setAuthorSuccess(record.authorId, directRates);
|
|
if (hasAllRates) {
|
|
continue;
|
|
}
|
|
} else {
|
|
resultStore.setAuthorLoading(record.authorId);
|
|
}
|
|
const metricsResult = await loadAuthorMetrics(record.authorId);
|
|
if (metricsResult.success) {
|
|
resultStore.setAuthorSuccess(record.authorId, metricsResult.rates);
|
|
} else {
|
|
resultStore.setAuthorFailed(record.authorId, metricsResult.reason);
|
|
}
|
|
}
|
|
if (searchBackendMetrics) {
|
|
const backendTargetRecords = records.filter((record) => {
|
|
const existingRecord = resultStore.getRecord(record.authorId);
|
|
return !(existingRecord?.backendMetricsStatus === "success" || existingRecord?.backendMetricsStatus === "missing");
|
|
});
|
|
if (backendTargetRecords.length > 0) {
|
|
backendTargetRecords.forEach((record) => {
|
|
resultStore.setBackendMetricsLoading(record.authorId);
|
|
});
|
|
try {
|
|
const backendRows = await searchBackendMetrics(
|
|
backendTargetRecords.map((record) => record.authorId)
|
|
);
|
|
const backendRowMap = new Map(backendRows.map((row) => [row.starId, row]));
|
|
backendTargetRecords.forEach((record) => {
|
|
const backendMetrics = backendRowMap.get(record.authorId);
|
|
if (backendMetrics) {
|
|
resultStore.setBackendMetricsSuccess(record.authorId, backendMetrics);
|
|
} else {
|
|
resultStore.setBackendMetricsMissing(record.authorId);
|
|
}
|
|
});
|
|
} catch {
|
|
backendTargetRecords.forEach((record) => {
|
|
resultStore.setBackendMetricsFailed(record.authorId);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return records.map((record) => toMarketRecord(record));
|
|
}
|
|
async function harvestCurrentPageForExport() {
|
|
let hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled();
|
|
if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount === 0) {
|
|
return;
|
|
}
|
|
const table = syncMarketTable(options.document);
|
|
const scrollContainer = findCurrentPageScrollContainer(table);
|
|
if (!scrollContainer) {
|
|
return;
|
|
}
|
|
const originalScrollTop = scrollContainer.scrollTop;
|
|
const maxScrollTop = Math.max(
|
|
0,
|
|
scrollContainer.scrollHeight - scrollContainer.clientHeight
|
|
);
|
|
if (maxScrollTop <= 0) {
|
|
return;
|
|
}
|
|
const step = Math.max(scrollContainer.clientHeight, 240);
|
|
for (let nextScrollTop = Math.min(originalScrollTop + step, maxScrollTop); nextScrollTop > originalScrollTop && nextScrollTop <= maxScrollTop; nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop)) {
|
|
setScrollTop(scrollContainer, nextScrollTop);
|
|
hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled();
|
|
if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount === 0) {
|
|
break;
|
|
}
|
|
if (nextScrollTop === maxScrollTop) {
|
|
break;
|
|
}
|
|
}
|
|
if (scrollContainer.scrollTop !== originalScrollTop) {
|
|
setScrollTop(scrollContainer, originalScrollTop);
|
|
}
|
|
}
|
|
function readCurrentPageRecords(table) {
|
|
if (!table) {
|
|
return [];
|
|
}
|
|
return table.rows.map((rowDom) => {
|
|
const rowSnapshot = readRowSnapshot(rowDom);
|
|
if (!rowSnapshot.authorId || !hasTextValue6(rowSnapshot.authorName)) {
|
|
return null;
|
|
}
|
|
return toMarketRecord(rowSnapshot);
|
|
}).filter((record) => record !== null);
|
|
}
|
|
function toMarketRecord(rowSnapshot) {
|
|
const existingRecord = resultStore.getRecord(rowSnapshot.authorId);
|
|
const authorName = mergeStringValue4(existingRecord?.authorName, rowSnapshot.authorName) ?? "";
|
|
const coreUserId = mergeStringValue4(
|
|
existingRecord?.coreUserId,
|
|
rowSnapshot.coreUserId
|
|
);
|
|
const location2 = mergeStringValue4(existingRecord?.location, rowSnapshot.location);
|
|
const price21To60s = mergeStringValue4(
|
|
existingRecord?.price21To60s,
|
|
rowSnapshot.price21To60s
|
|
);
|
|
return {
|
|
...existingRecord,
|
|
...rowSnapshot,
|
|
authorName,
|
|
backendMetrics: mergeFieldMap4(
|
|
existingRecord?.backendMetrics,
|
|
rowSnapshot.backendMetrics
|
|
),
|
|
backendMetricsStatus: existingRecord?.backendMetricsStatus ?? "idle",
|
|
coreUserId,
|
|
exportFields: withExportFieldFallbacks(
|
|
mergeFieldMap4(existingRecord?.exportFields, rowSnapshot.exportFields),
|
|
{
|
|
authorName,
|
|
location: location2,
|
|
price21To60s
|
|
}
|
|
),
|
|
location: location2,
|
|
price21To60s,
|
|
rates: mergeFieldMap4(existingRecord?.rates, rowSnapshot.rates),
|
|
status: existingRecord?.status ?? "idle"
|
|
};
|
|
}
|
|
function collectCurrentPageSnapshots() {
|
|
readCurrentPageRows(options.document).forEach((rowSnapshot) => {
|
|
resultStore.upsertMarketRow(rowSnapshot);
|
|
});
|
|
}
|
|
function findCurrentPageScrollContainer(table) {
|
|
if (!table) {
|
|
return null;
|
|
}
|
|
const candidateScores = /* @__PURE__ */ new Map();
|
|
const candidateRoots = table.rows.map((rowDom) => rowDom.row).filter((row) => row instanceof options.window.HTMLElement);
|
|
for (const rootElement of candidateRoots) {
|
|
let currentElement = rootElement.parentElement;
|
|
let depth = 0;
|
|
while (currentElement) {
|
|
if (isScrollableContainer(currentElement)) {
|
|
const scrollRange = currentElement.scrollHeight - currentElement.clientHeight;
|
|
const existingScore = candidateScores.get(currentElement);
|
|
if (!existingScore || depth < existingScore.depth) {
|
|
candidateScores.set(currentElement, {
|
|
depth,
|
|
scrollRange
|
|
});
|
|
}
|
|
}
|
|
depth += 1;
|
|
currentElement = currentElement.parentElement;
|
|
}
|
|
}
|
|
const rankedCandidates = Array.from(candidateScores.entries()).sort((left, right) => {
|
|
const [, leftScore] = left;
|
|
const [, rightScore] = right;
|
|
if (rightScore.scrollRange !== leftScore.scrollRange) {
|
|
return rightScore.scrollRange - leftScore.scrollRange;
|
|
}
|
|
return leftScore.depth - rightScore.depth;
|
|
});
|
|
return rankedCandidates[0]?.[0] ?? null;
|
|
}
|
|
function isScrollableContainer(element) {
|
|
const computedStyle = options.window.getComputedStyle(element);
|
|
return /auto|scroll|overlay/.test(computedStyle.overflowY) && element.scrollHeight > element.clientHeight;
|
|
}
|
|
async function waitForDomSettled() {
|
|
await new Promise((resolve) => {
|
|
options.window.setTimeout(resolve, 0);
|
|
});
|
|
await Promise.resolve();
|
|
}
|
|
async function collectCurrentPageSnapshotsUntilSettled() {
|
|
let previousFingerprint = "";
|
|
let stablePassCount = 0;
|
|
let fingerprintStableSince = 0;
|
|
let lastSnapshot = {
|
|
blankExportFieldCount: 0,
|
|
fingerprint: "",
|
|
missingDefaultFieldCount: 0
|
|
};
|
|
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
await waitForDomSettled();
|
|
if (attempt > 0) {
|
|
await new Promise((resolve) => {
|
|
options.window.setTimeout(
|
|
resolve,
|
|
previousFingerprint.includes("|missing:0") ? 25 : 50
|
|
);
|
|
});
|
|
await Promise.resolve();
|
|
}
|
|
collectCurrentPageSnapshots();
|
|
const hydrationSnapshot = readVisibleRowHydrationSnapshot();
|
|
lastSnapshot = hydrationSnapshot;
|
|
if (!hydrationSnapshot.fingerprint) {
|
|
stablePassCount = 0;
|
|
previousFingerprint = "";
|
|
continue;
|
|
}
|
|
if (hydrationSnapshot.fingerprint === previousFingerprint) {
|
|
stablePassCount += 1;
|
|
} else {
|
|
previousFingerprint = hydrationSnapshot.fingerprint;
|
|
stablePassCount = 1;
|
|
fingerprintStableSince = options.window.Date.now();
|
|
}
|
|
const stableForMs = options.window.Date.now() - fingerprintStableSince;
|
|
if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount === 0 && stablePassCount >= 2) {
|
|
return hydrationSnapshot;
|
|
}
|
|
if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount > 0 && stablePassCount >= 2 && stableForMs >= 500) {
|
|
return hydrationSnapshot;
|
|
}
|
|
}
|
|
return lastSnapshot;
|
|
}
|
|
function readVisibleRowHydrationSnapshot() {
|
|
const table = syncMarketTable(options.document);
|
|
if (!table || table.rows.length === 0) {
|
|
return {
|
|
blankExportFieldCount: 0,
|
|
fingerprint: "",
|
|
missingDefaultFieldCount: 0
|
|
};
|
|
}
|
|
const parts = table.rows.map((rowDom) => {
|
|
const rowSnapshot = readRowSnapshot(rowDom);
|
|
const populatedFieldCount = Object.values(rowSnapshot.exportFields ?? {}).filter(
|
|
(value) => typeof value === "string" && value.trim().length > 0
|
|
).length;
|
|
const blankExportFieldCount = Object.values(rowSnapshot.exportFields ?? {}).filter(
|
|
(value) => typeof value !== "string" || value.trim().length === 0
|
|
).length;
|
|
const hasAuthorField = hasTextValue6(rowSnapshot.exportFields?.["\u8FBE\u4EBA\u4FE1\u606F"]);
|
|
const hasRepresentativeVideo = hasTextValue6(
|
|
rowSnapshot.exportFields?.["\u4EE3\u8868\u89C6\u9891"]
|
|
);
|
|
const hasPriceField = hasTextValue6(rowSnapshot.price21To60s) || hasTextValue6(rowSnapshot.exportFields?.["21-60s\u62A5\u4EF7"]);
|
|
const missingDefaultFieldCount = Number(!hasAuthorField) + Number(!hasRepresentativeVideo) + Number(!hasPriceField);
|
|
return [
|
|
rowSnapshot.authorId,
|
|
populatedFieldCount,
|
|
`blank:${blankExportFieldCount}`,
|
|
hasAuthorField ? "author" : "no-author",
|
|
hasRepresentativeVideo ? "video" : "no-video",
|
|
hasPriceField ? "price" : "no-price",
|
|
`missing:${missingDefaultFieldCount}`
|
|
].join(":");
|
|
});
|
|
return {
|
|
blankExportFieldCount: parts.reduce((count, part) => {
|
|
const match = part.match(/:blank:(\d+):/);
|
|
return count + Number(match?.[1] ?? 0);
|
|
}, 0),
|
|
fingerprint: parts.join("|"),
|
|
missingDefaultFieldCount: parts.reduce((count, part) => {
|
|
const match = part.match(/missing:(\d+)$/);
|
|
return count + Number(match?.[1] ?? 0);
|
|
}, 0)
|
|
};
|
|
}
|
|
function scheduleSync() {
|
|
if (isDisposed) {
|
|
return;
|
|
}
|
|
if (isSyncRunning) {
|
|
needsResync = true;
|
|
return;
|
|
}
|
|
if (isSyncScheduled) {
|
|
return;
|
|
}
|
|
isSyncScheduled = true;
|
|
scheduledSyncTimeoutId = options.window.setTimeout(() => {
|
|
scheduledSyncTimeoutId = null;
|
|
isSyncScheduled = false;
|
|
if (isDisposed) {
|
|
return;
|
|
}
|
|
void runSyncCycle();
|
|
}, 0);
|
|
}
|
|
function runWithoutMutationSync(callback) {
|
|
if (isDisposed) {
|
|
return;
|
|
}
|
|
observer.disconnect();
|
|
try {
|
|
callback();
|
|
} finally {
|
|
startObserving();
|
|
}
|
|
}
|
|
function startObserving() {
|
|
if (isDisposed || !observationRoot) {
|
|
return;
|
|
}
|
|
observer.observe(observationRoot, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
async function runSyncCycle() {
|
|
if (isDisposed) {
|
|
return;
|
|
}
|
|
if (isSyncRunning) {
|
|
needsResync = true;
|
|
return;
|
|
}
|
|
isSyncRunning = true;
|
|
try {
|
|
toolbar = ensurePluginToolbar(options.document, toolbarHandlers);
|
|
await hydrateCurrentPage();
|
|
applyCurrentView();
|
|
lastKnownPageSignature = readMarketPageSignature(options.document);
|
|
} finally {
|
|
isSyncRunning = false;
|
|
if (isDisposed) {
|
|
return;
|
|
}
|
|
if (needsResync) {
|
|
needsResync = false;
|
|
scheduleSync();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function setScrollTop(element, top) {
|
|
element.scrollTop = top;
|
|
element.dispatchEvent(new Event("scroll"));
|
|
}
|
|
function readCurrentPageRows(document2) {
|
|
const table = syncMarketTable(document2);
|
|
if (!table) {
|
|
return [];
|
|
}
|
|
return table.rows.map((rowDom) => readRowSnapshot(rowDom)).filter(
|
|
(row) => Boolean(row.authorId) && hasTextValue6(row.authorName)
|
|
);
|
|
}
|
|
function countCurrentPageRows(document2) {
|
|
const table = syncMarketTable(document2);
|
|
if (!table) {
|
|
return 0;
|
|
}
|
|
return table.rows.filter((rowDom) => Boolean(rowDom.authorId)).length;
|
|
}
|
|
function readRowSnapshot(rowDom) {
|
|
return {
|
|
authorId: rowDom.authorId,
|
|
authorName: rowDom.authorName,
|
|
exportFields: rowDom.exportFields,
|
|
hasDirectRatesSource: rowDom.hasDirectRatesSource,
|
|
location: rowDom.location,
|
|
price21To60s: rowDom.price21To60s,
|
|
rates: rowDom.rates
|
|
};
|
|
}
|
|
function getNextSortState(currentSort, field) {
|
|
if (!currentSort || currentSort.field !== field) {
|
|
return {
|
|
direction: "desc",
|
|
field
|
|
};
|
|
}
|
|
if (currentSort.direction === "desc") {
|
|
return {
|
|
direction: "asc",
|
|
field
|
|
};
|
|
}
|
|
return void 0;
|
|
}
|
|
function hasCompleteRates(rates) {
|
|
return Boolean(
|
|
rates?.singleVideoAfterSearchRate && rates?.personalVideoAfterSearchRate
|
|
);
|
|
}
|
|
function hasSettledRateState(record) {
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
return record.status === "failed" || hasCompleteRates(record.rates);
|
|
}
|
|
function hasSettledBackendMetricsState(record) {
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
return record.backendMetricsStatus === "success" || record.backendMetricsStatus === "missing" || record.backendMetricsStatus === "failed";
|
|
}
|
|
function mergeFieldMap4(current, incoming) {
|
|
if (!current && !incoming) {
|
|
return void 0;
|
|
}
|
|
const merged = {
|
|
...current ?? {}
|
|
};
|
|
Object.entries(incoming ?? {}).forEach(([key, value]) => {
|
|
const currentValue = merged[key];
|
|
if (hasTextValue6(value) || !hasTextValue6(currentValue)) {
|
|
merged[key] = value;
|
|
}
|
|
});
|
|
return merged;
|
|
}
|
|
function createRuntimeMessageSender() {
|
|
return (message) => Promise.resolve(
|
|
globalThis.chrome?.runtime?.sendMessage?.(message)
|
|
);
|
|
}
|
|
async function readAuthState(sendMessage) {
|
|
const response = await sendMessage({ type: "auth:get-state" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") {
|
|
throw new Error("\u8BF7\u5148\u767B\u5F55\u63D2\u4EF6");
|
|
}
|
|
return response.value;
|
|
}
|
|
async function readBatchSubmitAck(sendMessage, payload) {
|
|
const response = await sendMessage({
|
|
payload,
|
|
type: "batch:submit"
|
|
});
|
|
if (response && typeof response === "object" && response.ok === true) {
|
|
return response.value;
|
|
}
|
|
if (response && typeof response === "object" && response.ok === false && typeof response.error === "string") {
|
|
throw new Error(response.error);
|
|
}
|
|
throw new Error("\u6279\u6B21\u63D0\u4EA4\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
}
|
|
async function readBackendMetrics(sendMessage, starIds) {
|
|
const response = await sendMessage({
|
|
type: "backend-metrics:search",
|
|
value: {
|
|
starIds
|
|
}
|
|
});
|
|
if (isBackendMetricsResponseMessage(response) && response.ok && response.type === "backend-metrics:result") {
|
|
return response.value.rows;
|
|
}
|
|
throw new Error("\u540E\u7AEF\u6307\u6807\u52A0\u8F7D\u5931\u8D25");
|
|
}
|
|
function mergeStringValue4(current, incoming) {
|
|
if (hasTextValue6(incoming) || !hasTextValue6(current)) {
|
|
return incoming ?? current;
|
|
}
|
|
return current;
|
|
}
|
|
function withExportFieldFallbacks(exportFields, fallbackValues) {
|
|
if (!exportFields) {
|
|
return void 0;
|
|
}
|
|
const nextExportFields = {
|
|
...exportFields
|
|
};
|
|
if ("\u8FBE\u4EBA\u4FE1\u606F" in nextExportFields && !hasTextValue6(nextExportFields["\u8FBE\u4EBA\u4FE1\u606F"]) && hasTextValue6(fallbackValues.authorName)) {
|
|
nextExportFields["\u8FBE\u4EBA\u4FE1\u606F"] = fallbackValues.authorName;
|
|
}
|
|
if ("\u5730\u533A" in nextExportFields && !hasTextValue6(nextExportFields["\u5730\u533A"]) && hasTextValue6(fallbackValues.location)) {
|
|
nextExportFields["\u5730\u533A"] = fallbackValues.location;
|
|
}
|
|
if ("21-60s\u62A5\u4EF7" in nextExportFields && !hasTextValue6(nextExportFields["21-60s\u62A5\u4EF7"]) && hasTextValue6(fallbackValues.price21To60s)) {
|
|
nextExportFields["21-60s\u62A5\u4EF7"] = fallbackValues.price21To60s;
|
|
}
|
|
return nextExportFields;
|
|
}
|
|
function hasTextValue6(value) {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
function hasRuntimeMessageSender() {
|
|
return Boolean(
|
|
globalThis.chrome?.runtime?.sendMessage
|
|
);
|
|
}
|
|
|
|
// src/content/market/auth-gate.ts
|
|
function renderMarketAuthGate(document2, currentWindow) {
|
|
const existingGate = document2.querySelector(
|
|
'[data-market-auth-gate="root"]'
|
|
);
|
|
if (existingGate) {
|
|
return existingGate;
|
|
}
|
|
const root = document2.createElement("section");
|
|
root.dataset.marketAuthGate = "root";
|
|
root.innerHTML = `
|
|
<strong>\u8BF7\u5148\u767B\u5F55\u63D2\u4EF6</strong>
|
|
<p>\u6253\u5F00\u6269\u5C55\u5F39\u7A97\u5B8C\u6210\u767B\u5F55\u540E\u5237\u65B0\u672C\u9875</p>
|
|
<button type="button" data-market-auth-help="button">\u53BB\u767B\u5F55</button>
|
|
`;
|
|
root.querySelector('[data-market-auth-help="button"]')?.addEventListener("click", () => {
|
|
currentWindow.alert("\u8BF7\u70B9\u51FB\u6D4F\u89C8\u5668\u5DE5\u5177\u680F\u4E2D\u7684\u6269\u5C55\u56FE\u6807\u5B8C\u6210\u767B\u5F55");
|
|
});
|
|
document2.body.prepend(root);
|
|
return root;
|
|
}
|
|
|
|
// src/content/index.ts
|
|
var DOWNLOAD_MARKET_CSV_MESSAGE = "download-market-csv";
|
|
async function bootContentScript(options = {}) {
|
|
const currentWindow = options.window ?? window;
|
|
const currentDocument = options.document ?? document;
|
|
const controllerFactory = options.createMarketController ?? createMarketController;
|
|
const sendAuthMessage = options.sendAuthMessage ?? createRuntimeMessageSender2();
|
|
if (!isMarketPage(currentWindow.location.href)) {
|
|
return null;
|
|
}
|
|
installMarketPageBridge(currentDocument);
|
|
const authState = await readAuthState2(sendAuthMessage);
|
|
if (!authState?.isAuthenticated) {
|
|
await waitForBodyReady(currentDocument, currentWindow);
|
|
renderMarketAuthGate(currentDocument, currentWindow);
|
|
return {
|
|
ready: Promise.resolve()
|
|
};
|
|
}
|
|
await waitForBodyReady(currentDocument, currentWindow);
|
|
return controllerFactory({
|
|
document: currentDocument,
|
|
onCsvReady: (csv) => {
|
|
if (requestCsvDownload(csv)) {
|
|
return;
|
|
}
|
|
downloadCsv(currentDocument, currentWindow, csv);
|
|
},
|
|
window: currentWindow
|
|
});
|
|
}
|
|
async function readAuthState2(sendMessage) {
|
|
const response = await sendMessage({ type: "auth:get-state" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") {
|
|
return null;
|
|
}
|
|
return response.value;
|
|
}
|
|
function isMarketPage(url) {
|
|
const parsedUrl = new URL(url);
|
|
const isXingtuHost = parsedUrl.hostname === "xingtu.cn" || parsedUrl.hostname.endsWith(".xingtu.cn");
|
|
return isXingtuHost && parsedUrl.pathname.startsWith("/ad/creator/market");
|
|
}
|
|
function bootstrapContentScript() {
|
|
const runtime = globalThis.chrome?.runtime;
|
|
if (!runtime || typeof window === "undefined" || typeof document === "undefined") {
|
|
return;
|
|
}
|
|
const marker = "__starChartSearchEnhancerContentController";
|
|
const scopedWindow = window;
|
|
if (scopedWindow[marker]) {
|
|
return;
|
|
}
|
|
scopedWindow[marker] = true;
|
|
void bootContentScript().then((controller) => {
|
|
scopedWindow[marker] = controller;
|
|
});
|
|
}
|
|
bootstrapContentScript();
|
|
function requestCsvDownload(csv) {
|
|
const runtime = globalThis.chrome?.runtime;
|
|
if (!runtime?.id || typeof runtime.sendMessage !== "function") {
|
|
return false;
|
|
}
|
|
runtime.sendMessage({
|
|
csv,
|
|
filename: `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`,
|
|
type: DOWNLOAD_MARKET_CSV_MESSAGE
|
|
});
|
|
return true;
|
|
}
|
|
function createRuntimeMessageSender2() {
|
|
return async (message) => {
|
|
const runtime = globalThis.chrome?.runtime;
|
|
if (typeof runtime?.sendMessage !== "function") {
|
|
return null;
|
|
}
|
|
return runtime.sendMessage(message);
|
|
};
|
|
}
|
|
async function waitForBodyReady(document2, currentWindow) {
|
|
if (document2.body) {
|
|
return;
|
|
}
|
|
await new Promise((resolve) => {
|
|
const handleReady = () => {
|
|
if (document2.body) {
|
|
document2.removeEventListener("DOMContentLoaded", handleReady);
|
|
resolve();
|
|
}
|
|
};
|
|
document2.addEventListener("DOMContentLoaded", handleReady);
|
|
currentWindow.setTimeout(handleReady, 0);
|
|
});
|
|
}
|
|
function downloadCsv(document2, window2, csv) {
|
|
const blob = new Blob(["\uFEFF", csv], {
|
|
type: "text/csv;charset=utf-8"
|
|
});
|
|
const objectUrl = window2.URL.createObjectURL(blob);
|
|
const link = document2.createElement("a");
|
|
link.href = objectUrl;
|
|
link.download = `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`;
|
|
document2.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
window2.URL.revokeObjectURL(objectUrl);
|
|
}
|
|
function formatTimestampForFilename() {
|
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
}
|
|
function installMarketPageBridge(document2) {
|
|
if (document2.documentElement.querySelector(
|
|
'[data-sces-market-bridge="script"]'
|
|
)) {
|
|
return;
|
|
}
|
|
const script = document2.createElement("script");
|
|
script.dataset.scesMarketBridge = "script";
|
|
const runtime = globalThis.chrome?.runtime;
|
|
const bridgeUrl = runtime?.getURL?.("content/market-page-bridge.js");
|
|
if (bridgeUrl) {
|
|
script.src = bridgeUrl;
|
|
} else {
|
|
script.textContent = "";
|
|
}
|
|
(document2.head ?? document2.documentElement).appendChild(script);
|
|
}
|
|
})();
|