feat: allow selecting audience export fields
This commit is contained in:
parent
db95a1f565
commit
50933af0a6
@ -141,6 +141,12 @@
|
|||||||
readValue: (record) => record.backendMetrics?.cpSearch ?? ""
|
readValue: (record) => record.backendMetrics?.cpSearch ?? ""
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
function listRateCsvHeaders() {
|
||||||
|
return RATE_COLUMNS.map((column) => column.header);
|
||||||
|
}
|
||||||
|
function listBackendMetricCsvHeaders() {
|
||||||
|
return BACKEND_METRIC_COLUMNS.map((column) => column.header);
|
||||||
|
}
|
||||||
function buildMarketCsv(records) {
|
function buildMarketCsv(records) {
|
||||||
const csvColumns = buildMarketCsvColumns(records);
|
const csvColumns = buildMarketCsvColumns(records);
|
||||||
const headerLine = csvColumns.map((column) => column.header).join(",");
|
const headerLine = csvColumns.map((column) => column.header).join(",");
|
||||||
@ -227,27 +233,73 @@
|
|||||||
{ key: "expectedPlay", label: "\u9884\u671F\u64AD\u653E\u91CF" },
|
{ key: "expectedPlay", label: "\u9884\u671F\u64AD\u653E\u91CF" },
|
||||||
{ key: "hotRate", label: "\u7206\u6587\u7387" }
|
{ key: "hotRate", label: "\u7206\u6587\u7387" }
|
||||||
];
|
];
|
||||||
function buildAudienceProfileCsv(rows) {
|
function buildAudienceProfileCsv(rows, options = {}) {
|
||||||
const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record));
|
const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record));
|
||||||
const csvColumns = [
|
const csvColumns = filterAudienceProfileCsvColumns([
|
||||||
...marketColumns.map(toMarketColumn),
|
...marketColumns.map(toMarketColumn),
|
||||||
...buildBusinessAbilityColumns(),
|
...buildBusinessAbilityColumns(),
|
||||||
...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout))
|
...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout))
|
||||||
];
|
], options.selectedHeaders);
|
||||||
const headerLine = csvColumns.map((column) => column.header).join(",");
|
const headerLine = csvColumns.map((column) => column.header).join(",");
|
||||||
const rowLines = rows.map(
|
const rowLines = rows.map(
|
||||||
(row) => csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",")
|
(row) => csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",")
|
||||||
);
|
);
|
||||||
return [headerLine, ...rowLines].join("\n");
|
return [headerLine, ...rowLines].join("\n");
|
||||||
}
|
}
|
||||||
|
function listAudienceProfileSelectableFieldGroups() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
headers: listRateCsvHeaders(),
|
||||||
|
label: "\u770B\u540E\u641C\u7387"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: listBackendMetricCsvHeaders(),
|
||||||
|
label: "\u79D2\u601Dapi\u6570\u636E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: buildBusinessVideoColumns().map((column) => column.header),
|
||||||
|
label: "\u5185\u5BB9\u6570\u636E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: buildBusinessEstimateColumns().map((column) => column.header),
|
||||||
|
label: "\u6548\u679C\u9884\u4F30"
|
||||||
|
},
|
||||||
|
...PROFILE_LAYOUTS.map((layout) => ({
|
||||||
|
headers: buildProfileColumns(layout).map((column) => column.header),
|
||||||
|
label: layout.label
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function filterAudienceProfileCsvColumns(columns, selectedHeaders) {
|
||||||
|
if (!selectedHeaders) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
const selectableHeaderSet = new Set(listAudienceProfileSelectableHeaders());
|
||||||
|
const selectedHeaderSet = new Set(selectedHeaders);
|
||||||
|
return columns.filter(
|
||||||
|
(column) => !selectableHeaderSet.has(column.header) || selectedHeaderSet.has(column.header)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function listAudienceProfileSelectableHeaders() {
|
||||||
|
return listAudienceProfileSelectableFieldGroups().flatMap(
|
||||||
|
(group) => group.headers
|
||||||
|
);
|
||||||
|
}
|
||||||
function buildBusinessAbilityColumns() {
|
function buildBusinessAbilityColumns() {
|
||||||
|
return [...buildBusinessVideoColumns(), ...buildBusinessEstimateColumns()];
|
||||||
|
}
|
||||||
|
function buildBusinessVideoColumns() {
|
||||||
return [
|
return [
|
||||||
...BUSINESS_VIDEO_LAYOUTS.flatMap(
|
...BUSINESS_VIDEO_LAYOUTS.flatMap(
|
||||||
(videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({
|
(videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({
|
||||||
header: `${BUSINESS_VIDEO_SECTION_LABEL}-${videoLayout.label}-${metricLayout.label}`,
|
header: `${BUSINESS_VIDEO_SECTION_LABEL}-${videoLayout.label}-${metricLayout.label}`,
|
||||||
readValue: (row) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key)
|
readValue: (row) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key)
|
||||||
}))
|
}))
|
||||||
),
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function buildBusinessEstimateColumns() {
|
||||||
|
return [
|
||||||
...BUSINESS_ESTIMATE_LAYOUTS.flatMap(
|
...BUSINESS_ESTIMATE_LAYOUTS.flatMap(
|
||||||
(durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({
|
(durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({
|
||||||
header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`,
|
header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`,
|
||||||
@ -813,24 +865,89 @@
|
|||||||
return typeof value === "object" && value !== null;
|
return typeof value === "object" && value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/content/market/author-id-dialog.ts
|
// src/content/market/audience-profile-field-dialog.ts
|
||||||
function promptForAuthorIds(document2) {
|
function promptForAudienceProfileFields(document2, groups, selectedHeaders) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
const selectableHeaders = groups.flatMap((group) => group.headers);
|
||||||
|
const selectedHeaderSet = new Set(
|
||||||
|
selectedHeaders.filter((header) => selectableHeaders.includes(header))
|
||||||
|
);
|
||||||
|
if (selectedHeaderSet.size === 0) {
|
||||||
|
selectableHeaders.forEach((header) => selectedHeaderSet.add(header));
|
||||||
|
}
|
||||||
const overlay = document2.createElement("div");
|
const overlay = document2.createElement("div");
|
||||||
overlay.dataset.authorIdDialog = "overlay";
|
overlay.dataset.audienceProfileFieldDialog = "overlay";
|
||||||
applyOverlayStyles(overlay);
|
applyOverlayStyles(overlay);
|
||||||
const dialog = document2.createElement("section");
|
const dialog = document2.createElement("section");
|
||||||
applyDialogStyles(dialog);
|
applyDialogStyles(dialog);
|
||||||
const title = document2.createElement("h2");
|
const title = document2.createElement("h2");
|
||||||
title.textContent = "\u6309\u661F\u56FEID\u5BFC\u51FA\u753B\u50CFCSV";
|
|
||||||
applyTitleStyles(title);
|
applyTitleStyles(title);
|
||||||
const textarea = document2.createElement("textarea");
|
|
||||||
textarea.dataset.authorIdDialogInput = "textarea";
|
|
||||||
textarea.placeholder = "\u6BCF\u884C\u4E00\u4E2A\u661F\u56FEID\uFF0C\u4E5F\u652F\u6301\u9017\u53F7\u3001\u7A7A\u683C\u5206\u9694";
|
|
||||||
applyTextareaStyles(textarea);
|
|
||||||
const hint = document2.createElement("p");
|
const hint = document2.createElement("p");
|
||||||
hint.textContent = "\u7C98\u8D34\u5BA2\u6237\u63D0\u4F9B\u7684\u8FBE\u4EBA\u661F\u56FEID\uFF0C\u786E\u8BA4\u540E\u5C06\u6279\u91CF\u5BFC\u51FA\u753B\u50CF\u548C\u5546\u4E1A\u80FD\u529B\u6570\u636E\u3002";
|
hint.textContent = "\u57FA\u7840\u5B57\u6BB5\u4F1A\u56FA\u5B9A\u4FDD\u7559\u3002\u53D6\u6D88\u52FE\u9009\u540E\uFF0C\u672C\u6B21\u53CA\u540E\u7EED\u753B\u50CFCSV\u5C06\u4E0D\u5305\u542B\u5BF9\u5E94\u5217\u3002";
|
||||||
applyHintStyles(hint);
|
applyHintStyles(hint);
|
||||||
|
const toolbar = document2.createElement("div");
|
||||||
|
applyToolbarStyles(toolbar);
|
||||||
|
const selectAllButton = document2.createElement("button");
|
||||||
|
selectAllButton.type = "button";
|
||||||
|
selectAllButton.textContent = "\u5168\u9009";
|
||||||
|
applySecondaryButtonStyles(selectAllButton);
|
||||||
|
const resetButton = document2.createElement("button");
|
||||||
|
resetButton.type = "button";
|
||||||
|
resetButton.textContent = "\u6062\u590D\u9ED8\u8BA4";
|
||||||
|
applySecondaryButtonStyles(resetButton);
|
||||||
|
toolbar.append(selectAllButton, resetButton);
|
||||||
|
const groupContainer = document2.createElement("div");
|
||||||
|
applyGroupContainerStyles(groupContainer);
|
||||||
|
const fieldInputs = [];
|
||||||
|
groups.forEach((group) => {
|
||||||
|
const groupSection = document2.createElement("section");
|
||||||
|
groupSection.dataset.audienceProfileFieldDialogGroup = "section";
|
||||||
|
applyGroupSectionStyles(groupSection);
|
||||||
|
const groupHeader = document2.createElement("label");
|
||||||
|
applyGroupHeaderStyles(groupHeader);
|
||||||
|
const groupInput = document2.createElement("input");
|
||||||
|
groupInput.type = "checkbox";
|
||||||
|
const groupTitle = document2.createElement("span");
|
||||||
|
groupTitle.textContent = group.label;
|
||||||
|
groupHeader.append(groupInput, groupTitle);
|
||||||
|
const fieldList = document2.createElement("div");
|
||||||
|
applyFieldListStyles(fieldList);
|
||||||
|
const groupFieldInputs = group.headers.map((header) => {
|
||||||
|
const fieldLabel = document2.createElement("label");
|
||||||
|
applyFieldLabelStyles(fieldLabel);
|
||||||
|
const input = document2.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.value = header;
|
||||||
|
input.dataset.audienceProfileFieldDialogField = "checkbox";
|
||||||
|
input.checked = selectedHeaderSet.has(header);
|
||||||
|
const text = document2.createElement("span");
|
||||||
|
text.textContent = header;
|
||||||
|
fieldLabel.append(input, text);
|
||||||
|
fieldList.append(fieldLabel);
|
||||||
|
fieldInputs.push(input);
|
||||||
|
return input;
|
||||||
|
});
|
||||||
|
const syncGroupInput = () => {
|
||||||
|
const checkedCount = groupFieldInputs.filter((input) => input.checked).length;
|
||||||
|
groupInput.checked = checkedCount === groupFieldInputs.length;
|
||||||
|
groupInput.indeterminate = checkedCount > 0 && checkedCount < groupFieldInputs.length;
|
||||||
|
};
|
||||||
|
groupInput.addEventListener("change", () => {
|
||||||
|
groupFieldInputs.forEach((input) => {
|
||||||
|
input.checked = groupInput.checked;
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
});
|
||||||
|
groupFieldInputs.forEach((input) => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
syncGroupInput();
|
||||||
|
syncTitle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
syncGroupInput();
|
||||||
|
groupSection.append(groupHeader, fieldList);
|
||||||
|
groupContainer.append(groupSection);
|
||||||
|
});
|
||||||
const actions = document2.createElement("div");
|
const actions = document2.createElement("div");
|
||||||
applyActionsStyles(actions);
|
applyActionsStyles(actions);
|
||||||
const cancelButton = document2.createElement("button");
|
const cancelButton = document2.createElement("button");
|
||||||
@ -839,24 +956,60 @@
|
|||||||
applySecondaryButtonStyles(cancelButton);
|
applySecondaryButtonStyles(cancelButton);
|
||||||
const confirmButton = document2.createElement("button");
|
const confirmButton = document2.createElement("button");
|
||||||
confirmButton.type = "button";
|
confirmButton.type = "button";
|
||||||
confirmButton.textContent = "\u5F00\u59CB\u5BFC\u51FA";
|
confirmButton.dataset.audienceProfileFieldDialogSave = "button";
|
||||||
|
confirmButton.textContent = "\u4FDD\u5B58";
|
||||||
applyPrimaryButtonStyles(confirmButton);
|
applyPrimaryButtonStyles(confirmButton);
|
||||||
actions.append(cancelButton, confirmButton);
|
actions.append(cancelButton, confirmButton);
|
||||||
dialog.append(title, hint, textarea, actions);
|
dialog.append(title, hint, toolbar, groupContainer, actions);
|
||||||
overlay.append(dialog);
|
overlay.append(dialog);
|
||||||
document2.body.appendChild(overlay);
|
document2.body.appendChild(overlay);
|
||||||
const close = (value) => {
|
function syncTitle() {
|
||||||
|
const checkedCount = fieldInputs.filter((input) => input.checked).length;
|
||||||
|
title.textContent = `\u53EF\u9009\u5B57\u6BB5\uFF08\u5DF2\u9009 ${checkedCount}/${fieldInputs.length} \u4E2A\u5B57\u6BB5\uFF09`;
|
||||||
|
}
|
||||||
|
function close(value) {
|
||||||
overlay.remove();
|
overlay.remove();
|
||||||
resolve(value);
|
resolve(value);
|
||||||
};
|
}
|
||||||
|
selectAllButton.addEventListener("click", () => {
|
||||||
|
fieldInputs.forEach((input) => {
|
||||||
|
input.checked = true;
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
syncAllGroupInputs(dialog);
|
||||||
|
});
|
||||||
|
resetButton.addEventListener("click", () => {
|
||||||
|
fieldInputs.forEach((input) => {
|
||||||
|
input.checked = true;
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
syncAllGroupInputs(dialog);
|
||||||
|
});
|
||||||
cancelButton.addEventListener("click", () => close(null));
|
cancelButton.addEventListener("click", () => close(null));
|
||||||
confirmButton.addEventListener("click", () => close(textarea.value));
|
confirmButton.addEventListener("click", () => {
|
||||||
|
const nextHeaders = fieldInputs.filter((input) => input.checked).map((input) => input.value);
|
||||||
|
close(nextHeaders);
|
||||||
|
});
|
||||||
overlay.addEventListener("click", (event) => {
|
overlay.addEventListener("click", (event) => {
|
||||||
if (event.target === overlay) {
|
if (event.target === overlay) {
|
||||||
close(null);
|
close(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
textarea.focus();
|
syncTitle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function syncAllGroupInputs(dialog) {
|
||||||
|
dialog.querySelectorAll('[data-audience-profile-field-dialog-group="section"]').forEach((section) => {
|
||||||
|
const groupInput = section.querySelector(":scope > label > input");
|
||||||
|
const fieldInputs = Array.from(
|
||||||
|
section.querySelectorAll(":scope > div input")
|
||||||
|
);
|
||||||
|
if (!(groupInput instanceof HTMLInputElement) || fieldInputs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const checkedCount = fieldInputs.filter((input) => input.checked).length;
|
||||||
|
groupInput.checked = checkedCount === fieldInputs.length;
|
||||||
|
groupInput.indeterminate = checkedCount > 0 && checkedCount < fieldInputs.length;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function applyOverlayStyles(overlay) {
|
function applyOverlayStyles(overlay) {
|
||||||
@ -869,8 +1022,11 @@
|
|||||||
overlay.style.background = "rgba(15, 23, 42, 0.38)";
|
overlay.style.background = "rgba(15, 23, 42, 0.38)";
|
||||||
}
|
}
|
||||||
function applyDialogStyles(dialog) {
|
function applyDialogStyles(dialog) {
|
||||||
dialog.style.width = "520px";
|
dialog.style.width = "680px";
|
||||||
dialog.style.maxWidth = "calc(100vw - 32px)";
|
dialog.style.maxWidth = "calc(100vw - 32px)";
|
||||||
|
dialog.style.maxHeight = "calc(100vh - 48px)";
|
||||||
|
dialog.style.display = "flex";
|
||||||
|
dialog.style.flexDirection = "column";
|
||||||
dialog.style.background = "#ffffff";
|
dialog.style.background = "#ffffff";
|
||||||
dialog.style.borderRadius = "8px";
|
dialog.style.borderRadius = "8px";
|
||||||
dialog.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)";
|
dialog.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)";
|
||||||
@ -889,18 +1045,43 @@
|
|||||||
hint.style.lineHeight = "20px";
|
hint.style.lineHeight = "20px";
|
||||||
hint.style.color = "#64748b";
|
hint.style.color = "#64748b";
|
||||||
}
|
}
|
||||||
function applyTextareaStyles(textarea) {
|
function applyToolbarStyles(toolbar) {
|
||||||
textarea.style.width = "100%";
|
toolbar.style.display = "flex";
|
||||||
textarea.style.height = "220px";
|
toolbar.style.gap = "8px";
|
||||||
textarea.style.resize = "vertical";
|
toolbar.style.marginBottom = "12px";
|
||||||
textarea.style.border = "1px solid #d0d7de";
|
}
|
||||||
textarea.style.borderRadius = "6px";
|
function applyGroupContainerStyles(container) {
|
||||||
textarea.style.padding = "10px";
|
container.style.display = "flex";
|
||||||
textarea.style.boxSizing = "border-box";
|
container.style.flexDirection = "column";
|
||||||
textarea.style.fontSize = "13px";
|
container.style.gap = "10px";
|
||||||
textarea.style.lineHeight = "20px";
|
container.style.overflow = "auto";
|
||||||
textarea.style.fontFamily = "ui-monospace, SFMono-Regular, Menlo, monospace";
|
container.style.paddingRight = "4px";
|
||||||
textarea.style.color = "#1f2329";
|
}
|
||||||
|
function applyGroupSectionStyles(section) {
|
||||||
|
section.style.border = "1px solid #e5e7eb";
|
||||||
|
section.style.borderRadius = "8px";
|
||||||
|
section.style.padding = "10px";
|
||||||
|
}
|
||||||
|
function applyGroupHeaderStyles(label) {
|
||||||
|
label.style.display = "flex";
|
||||||
|
label.style.alignItems = "center";
|
||||||
|
label.style.gap = "8px";
|
||||||
|
label.style.fontWeight = "700";
|
||||||
|
label.style.color = "#1f2329";
|
||||||
|
label.style.marginBottom = "8px";
|
||||||
|
}
|
||||||
|
function applyFieldListStyles(list) {
|
||||||
|
list.style.display = "grid";
|
||||||
|
list.style.gridTemplateColumns = "repeat(auto-fit, minmax(220px, 1fr))";
|
||||||
|
list.style.gap = "8px";
|
||||||
|
}
|
||||||
|
function applyFieldLabelStyles(label) {
|
||||||
|
label.style.display = "flex";
|
||||||
|
label.style.alignItems = "center";
|
||||||
|
label.style.gap = "6px";
|
||||||
|
label.style.fontSize = "13px";
|
||||||
|
label.style.lineHeight = "18px";
|
||||||
|
label.style.color = "#374151";
|
||||||
}
|
}
|
||||||
function applyActionsStyles(actions) {
|
function applyActionsStyles(actions) {
|
||||||
actions.style.display = "flex";
|
actions.style.display = "flex";
|
||||||
@ -927,6 +1108,120 @@
|
|||||||
button.style.fontWeight = "600";
|
button.style.fontWeight = "600";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/content/market/author-id-dialog.ts
|
||||||
|
function promptForAuthorIds(document2) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const overlay = document2.createElement("div");
|
||||||
|
overlay.dataset.authorIdDialog = "overlay";
|
||||||
|
applyOverlayStyles2(overlay);
|
||||||
|
const dialog = document2.createElement("section");
|
||||||
|
applyDialogStyles2(dialog);
|
||||||
|
const title = document2.createElement("h2");
|
||||||
|
title.textContent = "\u6309\u661F\u56FEID\u5BFC\u51FA\u753B\u50CFCSV";
|
||||||
|
applyTitleStyles2(title);
|
||||||
|
const textarea = document2.createElement("textarea");
|
||||||
|
textarea.dataset.authorIdDialogInput = "textarea";
|
||||||
|
textarea.placeholder = "\u6BCF\u884C\u4E00\u4E2A\u661F\u56FEID\uFF0C\u4E5F\u652F\u6301\u9017\u53F7\u3001\u7A7A\u683C\u5206\u9694";
|
||||||
|
applyTextareaStyles(textarea);
|
||||||
|
const hint = document2.createElement("p");
|
||||||
|
hint.textContent = "\u7C98\u8D34\u5BA2\u6237\u63D0\u4F9B\u7684\u8FBE\u4EBA\u661F\u56FEID\uFF0C\u786E\u8BA4\u540E\u5C06\u6279\u91CF\u5BFC\u51FA\u753B\u50CF\u548C\u5546\u4E1A\u80FD\u529B\u6570\u636E\u3002";
|
||||||
|
applyHintStyles2(hint);
|
||||||
|
const actions = document2.createElement("div");
|
||||||
|
applyActionsStyles2(actions);
|
||||||
|
const cancelButton = document2.createElement("button");
|
||||||
|
cancelButton.type = "button";
|
||||||
|
cancelButton.textContent = "\u53D6\u6D88";
|
||||||
|
applySecondaryButtonStyles2(cancelButton);
|
||||||
|
const confirmButton = document2.createElement("button");
|
||||||
|
confirmButton.type = "button";
|
||||||
|
confirmButton.textContent = "\u5F00\u59CB\u5BFC\u51FA";
|
||||||
|
applyPrimaryButtonStyles2(confirmButton);
|
||||||
|
actions.append(cancelButton, confirmButton);
|
||||||
|
dialog.append(title, hint, textarea, actions);
|
||||||
|
overlay.append(dialog);
|
||||||
|
document2.body.appendChild(overlay);
|
||||||
|
const close = (value) => {
|
||||||
|
overlay.remove();
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
cancelButton.addEventListener("click", () => close(null));
|
||||||
|
confirmButton.addEventListener("click", () => close(textarea.value));
|
||||||
|
overlay.addEventListener("click", (event) => {
|
||||||
|
if (event.target === overlay) {
|
||||||
|
close(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
textarea.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function applyOverlayStyles2(overlay) {
|
||||||
|
overlay.style.position = "fixed";
|
||||||
|
overlay.style.inset = "0";
|
||||||
|
overlay.style.zIndex = "2147483647";
|
||||||
|
overlay.style.display = "flex";
|
||||||
|
overlay.style.alignItems = "center";
|
||||||
|
overlay.style.justifyContent = "center";
|
||||||
|
overlay.style.background = "rgba(15, 23, 42, 0.38)";
|
||||||
|
}
|
||||||
|
function applyDialogStyles2(dialog) {
|
||||||
|
dialog.style.width = "520px";
|
||||||
|
dialog.style.maxWidth = "calc(100vw - 32px)";
|
||||||
|
dialog.style.background = "#ffffff";
|
||||||
|
dialog.style.borderRadius = "8px";
|
||||||
|
dialog.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)";
|
||||||
|
dialog.style.padding = "20px";
|
||||||
|
dialog.style.boxSizing = "border-box";
|
||||||
|
}
|
||||||
|
function applyTitleStyles2(title) {
|
||||||
|
title.style.margin = "0 0 8px";
|
||||||
|
title.style.fontSize = "18px";
|
||||||
|
title.style.fontWeight = "700";
|
||||||
|
title.style.color = "#1f2329";
|
||||||
|
}
|
||||||
|
function applyHintStyles2(hint) {
|
||||||
|
hint.style.margin = "0 0 12px";
|
||||||
|
hint.style.fontSize = "13px";
|
||||||
|
hint.style.lineHeight = "20px";
|
||||||
|
hint.style.color = "#64748b";
|
||||||
|
}
|
||||||
|
function applyTextareaStyles(textarea) {
|
||||||
|
textarea.style.width = "100%";
|
||||||
|
textarea.style.height = "220px";
|
||||||
|
textarea.style.resize = "vertical";
|
||||||
|
textarea.style.border = "1px solid #d0d7de";
|
||||||
|
textarea.style.borderRadius = "6px";
|
||||||
|
textarea.style.padding = "10px";
|
||||||
|
textarea.style.boxSizing = "border-box";
|
||||||
|
textarea.style.fontSize = "13px";
|
||||||
|
textarea.style.lineHeight = "20px";
|
||||||
|
textarea.style.fontFamily = "ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||||
|
textarea.style.color = "#1f2329";
|
||||||
|
}
|
||||||
|
function applyActionsStyles2(actions) {
|
||||||
|
actions.style.display = "flex";
|
||||||
|
actions.style.justifyContent = "flex-end";
|
||||||
|
actions.style.columnGap = "8px";
|
||||||
|
actions.style.marginTop = "14px";
|
||||||
|
}
|
||||||
|
function applyPrimaryButtonStyles2(button) {
|
||||||
|
button.style.height = "32px";
|
||||||
|
button.style.padding = "0 15px";
|
||||||
|
button.style.border = "1px solid #7f1d2d";
|
||||||
|
button.style.borderRadius = "8px";
|
||||||
|
button.style.background = "#7f1d2d";
|
||||||
|
button.style.color = "#ffffff";
|
||||||
|
button.style.fontWeight = "600";
|
||||||
|
}
|
||||||
|
function applySecondaryButtonStyles2(button) {
|
||||||
|
button.style.height = "32px";
|
||||||
|
button.style.padding = "0 15px";
|
||||||
|
button.style.border = "1px solid #d0d7de";
|
||||||
|
button.style.borderRadius = "8px";
|
||||||
|
button.style.background = "#ffffff";
|
||||||
|
button.style.color = "#1f2329";
|
||||||
|
button.style.fontWeight = "600";
|
||||||
|
}
|
||||||
|
|
||||||
// src/content/market/batch-name-dialog.ts
|
// src/content/market/batch-name-dialog.ts
|
||||||
var DIALOG_STYLE_ID = "sces-batch-name-dialog-style";
|
var DIALOG_STYLE_ID = "sces-batch-name-dialog-style";
|
||||||
var activeDialogs = /* @__PURE__ */ new WeakMap();
|
var activeDialogs = /* @__PURE__ */ new WeakMap();
|
||||||
@ -943,13 +1238,13 @@
|
|||||||
dialogRoot.setAttribute("role", "dialog");
|
dialogRoot.setAttribute("role", "dialog");
|
||||||
dialogRoot.setAttribute("aria-modal", "true");
|
dialogRoot.setAttribute("aria-modal", "true");
|
||||||
dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title");
|
dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title");
|
||||||
applyOverlayStyles2(dialogRoot);
|
applyOverlayStyles3(dialogRoot);
|
||||||
const dialogPanel = document2.createElement("div");
|
const dialogPanel = document2.createElement("div");
|
||||||
applyPanelStyles(dialogPanel);
|
applyPanelStyles(dialogPanel);
|
||||||
const title = document2.createElement("h2");
|
const title = document2.createElement("h2");
|
||||||
title.id = "sces-batch-name-title";
|
title.id = "sces-batch-name-title";
|
||||||
title.textContent = "\u63D0\u4EA4\u6279\u6B21";
|
title.textContent = "\u63D0\u4EA4\u6279\u6B21";
|
||||||
applyTitleStyles2(title);
|
applyTitleStyles3(title);
|
||||||
const description = document2.createElement("p");
|
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";
|
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);
|
applyDescriptionStyles(description);
|
||||||
@ -968,12 +1263,12 @@
|
|||||||
cancelButton.type = "button";
|
cancelButton.type = "button";
|
||||||
cancelButton.dataset.pluginBatchNameCancel = "button";
|
cancelButton.dataset.pluginBatchNameCancel = "button";
|
||||||
cancelButton.textContent = "\u53D6\u6D88";
|
cancelButton.textContent = "\u53D6\u6D88";
|
||||||
applySecondaryButtonStyles2(cancelButton);
|
applySecondaryButtonStyles3(cancelButton);
|
||||||
const confirmButton = document2.createElement("button");
|
const confirmButton = document2.createElement("button");
|
||||||
confirmButton.type = "button";
|
confirmButton.type = "button";
|
||||||
confirmButton.dataset.pluginBatchNameConfirm = "button";
|
confirmButton.dataset.pluginBatchNameConfirm = "button";
|
||||||
confirmButton.textContent = "\u786E\u8BA4\u63D0\u4EA4";
|
confirmButton.textContent = "\u786E\u8BA4\u63D0\u4EA4";
|
||||||
applyPrimaryButtonStyles2(confirmButton);
|
applyPrimaryButtonStyles3(confirmButton);
|
||||||
buttonRow.append(cancelButton, confirmButton);
|
buttonRow.append(cancelButton, confirmButton);
|
||||||
dialogPanel.append(title, description, input, errorText, buttonRow);
|
dialogPanel.append(title, description, input, errorText, buttonRow);
|
||||||
dialogRoot.appendChild(dialogPanel);
|
dialogRoot.appendChild(dialogPanel);
|
||||||
@ -1055,7 +1350,7 @@
|
|||||||
`;
|
`;
|
||||||
document2.head.appendChild(style);
|
document2.head.appendChild(style);
|
||||||
}
|
}
|
||||||
function applyOverlayStyles2(root) {
|
function applyOverlayStyles3(root) {
|
||||||
root.style.position = "fixed";
|
root.style.position = "fixed";
|
||||||
root.style.inset = "0";
|
root.style.inset = "0";
|
||||||
root.style.background = "rgba(15, 23, 42, 0.38)";
|
root.style.background = "rgba(15, 23, 42, 0.38)";
|
||||||
@ -1074,7 +1369,7 @@
|
|||||||
panel.style.padding = "24px";
|
panel.style.padding = "24px";
|
||||||
panel.style.boxSizing = "border-box";
|
panel.style.boxSizing = "border-box";
|
||||||
}
|
}
|
||||||
function applyTitleStyles2(title) {
|
function applyTitleStyles3(title) {
|
||||||
title.style.margin = "0";
|
title.style.margin = "0";
|
||||||
title.style.color = "#4c0519";
|
title.style.color = "#4c0519";
|
||||||
title.style.fontSize = "20px";
|
title.style.fontSize = "20px";
|
||||||
@ -1113,7 +1408,7 @@
|
|||||||
buttonRow.style.gap = "10px";
|
buttonRow.style.gap = "10px";
|
||||||
buttonRow.style.marginTop = "18px";
|
buttonRow.style.marginTop = "18px";
|
||||||
}
|
}
|
||||||
function applySecondaryButtonStyles2(button) {
|
function applySecondaryButtonStyles3(button) {
|
||||||
button.style.height = "36px";
|
button.style.height = "36px";
|
||||||
button.style.padding = "0 16px";
|
button.style.padding = "0 16px";
|
||||||
button.style.border = "1px solid #d7dde6";
|
button.style.border = "1px solid #d7dde6";
|
||||||
@ -1123,7 +1418,7 @@
|
|||||||
button.style.fontWeight = "600";
|
button.style.fontWeight = "600";
|
||||||
button.style.cursor = "pointer";
|
button.style.cursor = "pointer";
|
||||||
}
|
}
|
||||||
function applyPrimaryButtonStyles2(button) {
|
function applyPrimaryButtonStyles3(button) {
|
||||||
button.style.height = "36px";
|
button.style.height = "36px";
|
||||||
button.style.padding = "0 16px";
|
button.style.padding = "0 16px";
|
||||||
button.style.border = "1px solid #7f1d2d";
|
button.style.border = "1px solid #7f1d2d";
|
||||||
@ -3291,6 +3586,10 @@
|
|||||||
audienceProfileByIdExportButton.type = "button";
|
audienceProfileByIdExportButton.type = "button";
|
||||||
audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button";
|
audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button";
|
||||||
audienceProfileByIdExportButton.textContent = "\u6309ID\u5BFC\u51FA\u753B\u50CFCSV";
|
audienceProfileByIdExportButton.textContent = "\u6309ID\u5BFC\u51FA\u753B\u50CFCSV";
|
||||||
|
const audienceProfileFieldButton = document2.createElement("button");
|
||||||
|
audienceProfileFieldButton.type = "button";
|
||||||
|
audienceProfileFieldButton.dataset.pluginAudienceProfileFields = "button";
|
||||||
|
audienceProfileFieldButton.textContent = "\u753B\u50CF\u5B57\u6BB5";
|
||||||
const batchSubmitButton = document2.createElement("button");
|
const batchSubmitButton = document2.createElement("button");
|
||||||
batchSubmitButton.type = "button";
|
batchSubmitButton.type = "button";
|
||||||
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
||||||
@ -3304,6 +3603,7 @@
|
|||||||
exportButton,
|
exportButton,
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportStatusText
|
exportStatusText
|
||||||
);
|
);
|
||||||
@ -3311,6 +3611,7 @@
|
|||||||
applyNativeControlStyles(document2, {
|
applyNativeControlStyles(document2, {
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
@ -3326,12 +3627,16 @@
|
|||||||
audienceProfileByIdExportButton.addEventListener("click", () => {
|
audienceProfileByIdExportButton.addEventListener("click", () => {
|
||||||
void handlers.onExportAudienceProfileByIds();
|
void handlers.onExportAudienceProfileByIds();
|
||||||
});
|
});
|
||||||
|
audienceProfileFieldButton.addEventListener("click", () => {
|
||||||
|
void handlers.onConfigureAudienceProfileFields();
|
||||||
|
});
|
||||||
batchSubmitButton.addEventListener("click", () => {
|
batchSubmitButton.addEventListener("click", () => {
|
||||||
void handlers.onSubmitBatch();
|
void handlers.onSubmitBatch();
|
||||||
});
|
});
|
||||||
exportRangeSelect.addEventListener("change", () => {
|
exportRangeSelect.addEventListener("change", () => {
|
||||||
syncCustomPagesInputVisibility({
|
syncCustomPagesInputVisibility({
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
@ -3344,6 +3649,7 @@
|
|||||||
const toolbarDom = {
|
const toolbarDom = {
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
@ -3368,6 +3674,9 @@
|
|||||||
audienceProfileExportButton: root.querySelector(
|
audienceProfileExportButton: root.querySelector(
|
||||||
'[data-plugin-export-audience-profile="button"]'
|
'[data-plugin-export-audience-profile="button"]'
|
||||||
),
|
),
|
||||||
|
audienceProfileFieldButton: root.querySelector(
|
||||||
|
'[data-plugin-audience-profile-fields="button"]'
|
||||||
|
),
|
||||||
batchSubmitButton: root.querySelector(
|
batchSubmitButton: root.querySelector(
|
||||||
'[data-plugin-batch-submit="button"]'
|
'[data-plugin-batch-submit="button"]'
|
||||||
),
|
),
|
||||||
@ -3437,6 +3746,7 @@
|
|||||||
function setToolbarBusyState(toolbar, isBusy) {
|
function setToolbarBusyState(toolbar, isBusy) {
|
||||||
[
|
[
|
||||||
toolbar.batchSubmitButton,
|
toolbar.batchSubmitButton,
|
||||||
|
toolbar.audienceProfileFieldButton,
|
||||||
toolbar.audienceProfileByIdExportButton,
|
toolbar.audienceProfileByIdExportButton,
|
||||||
toolbar.audienceProfileExportButton,
|
toolbar.audienceProfileExportButton,
|
||||||
toolbar.exportButton,
|
toolbar.exportButton,
|
||||||
@ -3565,15 +3875,17 @@
|
|||||||
controls.exportButton.className = nativeButton.className;
|
controls.exportButton.className = nativeButton.className;
|
||||||
controls.audienceProfileExportButton.className = nativeButton.className;
|
controls.audienceProfileExportButton.className = nativeButton.className;
|
||||||
controls.audienceProfileByIdExportButton.className = nativeButton.className;
|
controls.audienceProfileByIdExportButton.className = nativeButton.className;
|
||||||
|
controls.audienceProfileFieldButton.className = nativeButton.className;
|
||||||
controls.batchSubmitButton.className = nativeButton.className;
|
controls.batchSubmitButton.className = nativeButton.className;
|
||||||
}
|
}
|
||||||
[
|
[
|
||||||
controls.exportButton,
|
controls.exportButton,
|
||||||
controls.audienceProfileExportButton,
|
controls.audienceProfileExportButton,
|
||||||
controls.audienceProfileByIdExportButton,
|
controls.audienceProfileByIdExportButton,
|
||||||
|
controls.audienceProfileFieldButton,
|
||||||
controls.batchSubmitButton
|
controls.batchSubmitButton
|
||||||
].forEach((button) => {
|
].forEach((button) => {
|
||||||
applyPrimaryButtonStyles3(button);
|
applyPrimaryButtonStyles4(button);
|
||||||
button.style.whiteSpace = "nowrap";
|
button.style.whiteSpace = "nowrap";
|
||||||
});
|
});
|
||||||
[controls.exportRangeSelect, controls.exportCustomPagesInput].forEach((element) => {
|
[controls.exportRangeSelect, controls.exportCustomPagesInput].forEach((element) => {
|
||||||
@ -3588,7 +3900,7 @@
|
|||||||
controls.exportRangeSelect.style.minWidth = "104px";
|
controls.exportRangeSelect.style.minWidth = "104px";
|
||||||
controls.exportCustomPagesInput.style.width = "72px";
|
controls.exportCustomPagesInput.style.width = "72px";
|
||||||
}
|
}
|
||||||
function applyPrimaryButtonStyles3(button) {
|
function applyPrimaryButtonStyles4(button) {
|
||||||
button.style.backgroundColor = "#7f1d2d";
|
button.style.backgroundColor = "#7f1d2d";
|
||||||
button.style.border = "1px solid #7f1d2d";
|
button.style.border = "1px solid #7f1d2d";
|
||||||
button.style.borderRadius = "8px";
|
button.style.borderRadius = "8px";
|
||||||
@ -3616,6 +3928,7 @@
|
|||||||
[data-plugin-export="button"]:hover:not(:disabled),
|
[data-plugin-export="button"]:hover:not(:disabled),
|
||||||
[data-plugin-export-audience-profile="button"]:hover:not(:disabled),
|
[data-plugin-export-audience-profile="button"]:hover:not(:disabled),
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
|
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
|
||||||
|
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled),
|
||||||
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
|
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
|
||||||
background-color: #6d1627 !important;
|
background-color: #6d1627 !important;
|
||||||
border-color: #6d1627 !important;
|
border-color: #6d1627 !important;
|
||||||
@ -3624,6 +3937,7 @@
|
|||||||
[data-plugin-export="button"]:active:not(:disabled),
|
[data-plugin-export="button"]:active:not(:disabled),
|
||||||
[data-plugin-export-audience-profile="button"]:active:not(:disabled),
|
[data-plugin-export-audience-profile="button"]:active:not(:disabled),
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
|
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
|
||||||
|
[data-plugin-audience-profile-fields="button"]:active:not(:disabled),
|
||||||
[data-plugin-batch-submit="button"]:active:not(:disabled) {
|
[data-plugin-batch-submit="button"]:active:not(:disabled) {
|
||||||
background-color: #58111f !important;
|
background-color: #58111f !important;
|
||||||
border-color: #58111f !important;
|
border-color: #58111f !important;
|
||||||
@ -3633,6 +3947,7 @@
|
|||||||
[data-plugin-export="button"]:focus-visible,
|
[data-plugin-export="button"]:focus-visible,
|
||||||
[data-plugin-export-audience-profile="button"]:focus-visible,
|
[data-plugin-export-audience-profile="button"]:focus-visible,
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
|
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
|
||||||
|
[data-plugin-audience-profile-fields="button"]:focus-visible,
|
||||||
[data-plugin-batch-submit="button"]:focus-visible {
|
[data-plugin-batch-submit="button"]:focus-visible {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
||||||
@ -3641,6 +3956,7 @@
|
|||||||
[data-plugin-export="button"]:disabled,
|
[data-plugin-export="button"]:disabled,
|
||||||
[data-plugin-export-audience-profile="button"]:disabled,
|
[data-plugin-export-audience-profile="button"]:disabled,
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:disabled,
|
[data-plugin-export-audience-profile-by-id="button"]:disabled,
|
||||||
|
[data-plugin-audience-profile-fields="button"]:disabled,
|
||||||
[data-plugin-batch-submit="button"]:disabled {
|
[data-plugin-batch-submit="button"]:disabled {
|
||||||
background-color: #c89ca4 !important;
|
background-color: #c89ca4 !important;
|
||||||
border-color: #c89ca4 !important;
|
border-color: #c89ca4 !important;
|
||||||
@ -4235,6 +4551,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// src/content/market/index.ts
|
// src/content/market/index.ts
|
||||||
|
var AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY = "sces:audience-profile:selectedHeaders";
|
||||||
function createMarketController(options) {
|
function createMarketController(options) {
|
||||||
const marketApiClient = createMarketApiClient();
|
const marketApiClient = createMarketApiClient();
|
||||||
const audienceProfileClient = createAudienceProfileClient();
|
const audienceProfileClient = createAudienceProfileClient();
|
||||||
@ -4379,7 +4696,12 @@
|
|||||||
setToolbarExportStatus(toolbar, "\u753B\u50CF\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
setToolbarExportStatus(toolbar, "\u753B\u50CF\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
options.onCsvReady?.(buildAudienceCsv(rows), buildAudienceProfileFilename());
|
options.onCsvReady?.(
|
||||||
|
buildAudienceCsv(rows, {
|
||||||
|
selectedHeaders: readAudienceProfileSelectedHeaders()
|
||||||
|
}),
|
||||||
|
buildAudienceProfileFilename()
|
||||||
|
);
|
||||||
setToolbarExportStatus(toolbar, "");
|
setToolbarExportStatus(toolbar, "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToolbarExportStatus(
|
setToolbarExportStatus(
|
||||||
@ -4422,7 +4744,9 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
options.onCsvReady?.(
|
options.onCsvReady?.(
|
||||||
buildAudienceCsv(rows),
|
buildAudienceCsv(rows, {
|
||||||
|
selectedHeaders: readAudienceProfileSelectedHeaders()
|
||||||
|
}),
|
||||||
buildAudienceProfileFilename(/* @__PURE__ */ new Date(), "\u6309ID\u5BFC\u51FA")
|
buildAudienceProfileFilename(/* @__PURE__ */ new Date(), "\u6309ID\u5BFC\u51FA")
|
||||||
);
|
);
|
||||||
setToolbarExportStatus(toolbar, "");
|
setToolbarExportStatus(toolbar, "");
|
||||||
@ -4435,6 +4759,23 @@
|
|||||||
setToolbarBusyState(toolbar, false);
|
setToolbarBusyState(toolbar, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onConfigureAudienceProfileFields: async () => {
|
||||||
|
const groups = listAudienceProfileSelectableFieldGroups();
|
||||||
|
const selectedHeaders = readAudienceProfileSelectedHeaders();
|
||||||
|
const nextHeaders = await promptForAudienceProfileFields(
|
||||||
|
options.document,
|
||||||
|
groups,
|
||||||
|
selectedHeaders
|
||||||
|
);
|
||||||
|
if (nextHeaders === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveAudienceProfileSelectedHeaders(nextHeaders);
|
||||||
|
setToolbarExportStatus(
|
||||||
|
toolbar,
|
||||||
|
`\u753B\u50CF\u5B57\u6BB5\u5DF2\u4FDD\u5B58\uFF08\u5DF2\u9009 ${nextHeaders.length}/${readAudienceProfileSelectableHeaders().length} \u4E2A\u5B57\u6BB5\uFF09`
|
||||||
|
);
|
||||||
|
},
|
||||||
onSubmitBatch: async () => {
|
onSubmitBatch: async () => {
|
||||||
syncSelectionStateFromDom();
|
syncSelectionStateFromDom();
|
||||||
const exportTarget = readToolbarExportTarget(toolbar);
|
const exportTarget = readToolbarExportTarget(toolbar);
|
||||||
@ -4880,6 +5221,46 @@
|
|||||||
}
|
}
|
||||||
return "\u94C1\u7C89\u753B\u50CF";
|
return "\u94C1\u7C89\u753B\u50CF";
|
||||||
}
|
}
|
||||||
|
function readAudienceProfileSelectableHeaders() {
|
||||||
|
return listAudienceProfileSelectableFieldGroups().flatMap(
|
||||||
|
(group) => group.headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function readAudienceProfileSelectedHeaders() {
|
||||||
|
const selectableHeaders = readAudienceProfileSelectableHeaders();
|
||||||
|
const selectableHeaderSet = new Set(selectableHeaders);
|
||||||
|
try {
|
||||||
|
const rawValue = options.window.localStorage?.getItem(
|
||||||
|
AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY
|
||||||
|
);
|
||||||
|
if (!rawValue) {
|
||||||
|
return selectableHeaders;
|
||||||
|
}
|
||||||
|
const parsedValue = JSON.parse(rawValue);
|
||||||
|
if (!Array.isArray(parsedValue)) {
|
||||||
|
return selectableHeaders;
|
||||||
|
}
|
||||||
|
const selectedHeaders = parsedValue.filter(
|
||||||
|
(header) => typeof header === "string" && selectableHeaderSet.has(header)
|
||||||
|
);
|
||||||
|
return selectedHeaders.length > 0 ? selectedHeaders : selectableHeaders;
|
||||||
|
} catch {
|
||||||
|
return selectableHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveAudienceProfileSelectedHeaders(headers) {
|
||||||
|
const selectableHeaderSet = new Set(readAudienceProfileSelectableHeaders());
|
||||||
|
const selectedHeaders = headers.filter(
|
||||||
|
(header) => selectableHeaderSet.has(header)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
options.window.localStorage?.setItem(
|
||||||
|
AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY,
|
||||||
|
JSON.stringify(selectedHeaders)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
async function prepareCurrentPageForExport() {
|
async function prepareCurrentPageForExport() {
|
||||||
await runSyncCycle();
|
await runSyncCycle();
|
||||||
await harvestCurrentPageForExport();
|
await harvestCurrentPageForExport();
|
||||||
|
|||||||
@ -264,7 +264,59 @@ https://xingtu.cn/ad/creator/market
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十、提交批次的方法
|
## 十、导出画像 CSV 的方法
|
||||||
|
|
||||||
|
画像 CSV 用来导出达人画像、内容数据、效果预估和秒思 api 数据。
|
||||||
|
|
||||||
|
### 1. 勾选达人
|
||||||
|
|
||||||
|
画像导出必须先勾选达人。
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 画像导出会额外读取达人详情页数据
|
||||||
|
- 为了避免请求太多,只处理你勾选的达人
|
||||||
|
|
||||||
|
### 2. 可选:选择需要导出的字段
|
||||||
|
|
||||||
|
如果只想导出一部分画像字段,点击:
|
||||||
|
|
||||||
|
- `画像字段`
|
||||||
|
|
||||||
|
在弹出的窗口里:
|
||||||
|
|
||||||
|
1. 勾选需要的字段
|
||||||
|
2. 取消不需要的字段
|
||||||
|
3. 点击 `保存`
|
||||||
|
|
||||||
|
基础字段会固定保留,例如:
|
||||||
|
|
||||||
|
- 达人ID
|
||||||
|
- 达人名称
|
||||||
|
- 导出状态
|
||||||
|
- 失败原因
|
||||||
|
|
||||||
|
保存后,下次再导出画像 CSV,会自动沿用这次勾选结果,不需要重新勾选。
|
||||||
|
|
||||||
|
### 3. 点击导出
|
||||||
|
|
||||||
|
勾选达人后,点击:
|
||||||
|
|
||||||
|
- `导出画像CSV`
|
||||||
|
|
||||||
|
如果已经有达人星图 ID 列表,也可以点击:
|
||||||
|
|
||||||
|
- `按ID导出画像CSV`
|
||||||
|
|
||||||
|
然后把达人 ID 粘贴进去,每行一个。
|
||||||
|
|
||||||
|
### 4. 等待下载
|
||||||
|
|
||||||
|
导出完成后,浏览器会自动下载 CSV 文件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、提交批次的方法
|
||||||
|
|
||||||
### 1. 选择范围
|
### 1. 选择范围
|
||||||
|
|
||||||
@ -321,7 +373,7 @@ https://xingtu.cn/ad/creator/market
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十一、批次名称填写建议
|
## 十二、批次名称填写建议
|
||||||
|
|
||||||
建议使用容易看懂的命名方式:
|
建议使用容易看懂的命名方式:
|
||||||
|
|
||||||
@ -348,7 +400,7 @@ https://xingtu.cn/ad/creator/market
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十二、如何更新插件
|
## 十三、如何更新插件
|
||||||
|
|
||||||
当你收到新的插件压缩包时,不需要重新从零安装。
|
当你收到新的插件压缩包时,不需要重新从零安装。
|
||||||
|
|
||||||
@ -380,7 +432,7 @@ https://xingtu.cn/ad/creator/market
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十三、常见问题
|
## 十四、常见问题
|
||||||
|
|
||||||
### 1. 看不到插件按钮
|
### 1. 看不到插件按钮
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,14 @@ https://xingtu.cn/ad/creator/market
|
|||||||
- 效果预估:不同视频时长的预期CPM、预期CPE、预期播放量、爆文率
|
- 效果预估:不同视频时长的预期CPM、预期CPE、预期播放量、爆文率
|
||||||
- 秒思api数据:看后搜率、看后搜数、新增A3数、新增A3率、CPA3、cp_search
|
- 秒思api数据:看后搜率、看后搜数、新增A3数、新增A3率、CPA3、cp_search
|
||||||
|
|
||||||
|
**只导出部分字段**:
|
||||||
|
- 点击 **"画像字段"**
|
||||||
|
- 勾选你需要的字段,取消不需要的字段
|
||||||
|
- 点击 **"保存"**
|
||||||
|
- 再点击 **"导出画像CSV"** 或 **"按ID导出画像CSV"**
|
||||||
|
|
||||||
|
说明:达人ID、达人名称、导出状态、失败原因等基础字段会固定保留;你保存过一次后,下次导出会自动沿用这次勾选结果,不需要重新勾选。
|
||||||
|
|
||||||
### 3️⃣ 按ID导出画像数据
|
### 3️⃣ 按ID导出画像数据
|
||||||
|
|
||||||
当你想批量查询特定达人ID的画像数据时使用:
|
当你想批量查询特定达人ID的画像数据时使用:
|
||||||
|
|||||||
Binary file not shown.
@ -1,5 +1,10 @@
|
|||||||
import { escapeCsvCell } from "../../shared/csv";
|
import { escapeCsvCell } from "../../shared/csv";
|
||||||
import { buildMarketCsvColumns, type CsvColumn } from "./csv-exporter";
|
import {
|
||||||
|
buildMarketCsvColumns,
|
||||||
|
listBackendMetricCsvHeaders,
|
||||||
|
listRateCsvHeaders,
|
||||||
|
type CsvColumn
|
||||||
|
} from "./csv-exporter";
|
||||||
import type {
|
import type {
|
||||||
AudienceProfileDistributionItem,
|
AudienceProfileDistributionItem,
|
||||||
AudienceProfileExportRow,
|
AudienceProfileExportRow,
|
||||||
@ -16,6 +21,15 @@ type AudienceProfileCsvColumn = {
|
|||||||
readValue: (row: AudienceProfileExportRow) => string;
|
readValue: (row: AudienceProfileExportRow) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AudienceProfileCsvOptions {
|
||||||
|
selectedHeaders?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AudienceProfileCsvFieldGroup = {
|
||||||
|
headers: string[];
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
const PROFILE_LAYOUTS: Array<{
|
const PROFILE_LAYOUTS: Array<{
|
||||||
includeGender: boolean;
|
includeGender: boolean;
|
||||||
kind: AudienceProfileKind;
|
kind: AudienceProfileKind;
|
||||||
@ -91,14 +105,15 @@ const BUSINESS_ESTIMATE_METRIC_LAYOUTS: Array<{
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function buildAudienceProfileCsv(
|
export function buildAudienceProfileCsv(
|
||||||
rows: AudienceProfileExportRow[]
|
rows: AudienceProfileExportRow[],
|
||||||
|
options: AudienceProfileCsvOptions = {}
|
||||||
): string {
|
): string {
|
||||||
const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record));
|
const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record));
|
||||||
const csvColumns = [
|
const csvColumns = filterAudienceProfileCsvColumns([
|
||||||
...marketColumns.map(toMarketColumn),
|
...marketColumns.map(toMarketColumn),
|
||||||
...buildBusinessAbilityColumns(),
|
...buildBusinessAbilityColumns(),
|
||||||
...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout))
|
...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout))
|
||||||
];
|
], options.selectedHeaders);
|
||||||
const headerLine = csvColumns.map((column) => column.header).join(",");
|
const headerLine = csvColumns.map((column) => column.header).join(",");
|
||||||
const rowLines = rows.map((row) =>
|
const rowLines = rows.map((row) =>
|
||||||
csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",")
|
csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",")
|
||||||
@ -107,7 +122,72 @@ export function buildAudienceProfileCsv(
|
|||||||
return [headerLine, ...rowLines].join("\n");
|
return [headerLine, ...rowLines].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listAudienceProfileCsvHeaders(
|
||||||
|
rows: AudienceProfileExportRow[] = []
|
||||||
|
): string[] {
|
||||||
|
const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record));
|
||||||
|
return [
|
||||||
|
...marketColumns.map((column) => column.header),
|
||||||
|
...buildBusinessAbilityColumns().map((column) => column.header),
|
||||||
|
...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout)).map(
|
||||||
|
(column) => column.header
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAudienceProfileSelectableFieldGroups(): AudienceProfileCsvFieldGroup[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
headers: listRateCsvHeaders(),
|
||||||
|
label: "看后搜率"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: listBackendMetricCsvHeaders(),
|
||||||
|
label: "秒思api数据"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: buildBusinessVideoColumns().map((column) => column.header),
|
||||||
|
label: "内容数据"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: buildBusinessEstimateColumns().map((column) => column.header),
|
||||||
|
label: "效果预估"
|
||||||
|
},
|
||||||
|
...PROFILE_LAYOUTS.map((layout) => ({
|
||||||
|
headers: buildProfileColumns(layout).map((column) => column.header),
|
||||||
|
label: layout.label
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAudienceProfileCsvColumns(
|
||||||
|
columns: AudienceProfileCsvColumn[],
|
||||||
|
selectedHeaders: string[] | undefined
|
||||||
|
): AudienceProfileCsvColumn[] {
|
||||||
|
if (!selectedHeaders) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectableHeaderSet = new Set(listAudienceProfileSelectableHeaders());
|
||||||
|
const selectedHeaderSet = new Set(selectedHeaders);
|
||||||
|
return columns.filter(
|
||||||
|
(column) =>
|
||||||
|
!selectableHeaderSet.has(column.header) ||
|
||||||
|
selectedHeaderSet.has(column.header)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listAudienceProfileSelectableHeaders(): string[] {
|
||||||
|
return listAudienceProfileSelectableFieldGroups().flatMap(
|
||||||
|
(group) => group.headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] {
|
function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] {
|
||||||
|
return [...buildBusinessVideoColumns(), ...buildBusinessEstimateColumns()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBusinessVideoColumns(): AudienceProfileCsvColumn[] {
|
||||||
return [
|
return [
|
||||||
...BUSINESS_VIDEO_LAYOUTS.flatMap((videoLayout) =>
|
...BUSINESS_VIDEO_LAYOUTS.flatMap((videoLayout) =>
|
||||||
BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({
|
BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({
|
||||||
@ -115,7 +195,12 @@ function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] {
|
|||||||
readValue: (row: AudienceProfileExportRow) =>
|
readValue: (row: AudienceProfileExportRow) =>
|
||||||
readBusinessVideoValue(row, videoLayout.key, metricLayout.key)
|
readBusinessVideoValue(row, videoLayout.key, metricLayout.key)
|
||||||
}))
|
}))
|
||||||
),
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBusinessEstimateColumns(): AudienceProfileCsvColumn[] {
|
||||||
|
return [
|
||||||
...BUSINESS_ESTIMATE_LAYOUTS.flatMap((durationLayout) =>
|
...BUSINESS_ESTIMATE_LAYOUTS.flatMap((durationLayout) =>
|
||||||
BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({
|
BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({
|
||||||
header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`,
|
header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`,
|
||||||
|
|||||||
295
src/content/market/audience-profile-field-dialog.ts
Normal file
295
src/content/market/audience-profile-field-dialog.ts
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import type { AudienceProfileCsvFieldGroup } from "./audience-profile-csv";
|
||||||
|
|
||||||
|
export function promptForAudienceProfileFields(
|
||||||
|
document: Document,
|
||||||
|
groups: AudienceProfileCsvFieldGroup[],
|
||||||
|
selectedHeaders: string[]
|
||||||
|
): Promise<string[] | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const selectableHeaders = groups.flatMap((group) => group.headers);
|
||||||
|
const selectedHeaderSet = new Set(
|
||||||
|
selectedHeaders.filter((header) => selectableHeaders.includes(header))
|
||||||
|
);
|
||||||
|
if (selectedHeaderSet.size === 0) {
|
||||||
|
selectableHeaders.forEach((header) => selectedHeaderSet.add(header));
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.dataset.audienceProfileFieldDialog = "overlay";
|
||||||
|
applyOverlayStyles(overlay);
|
||||||
|
|
||||||
|
const dialog = document.createElement("section");
|
||||||
|
applyDialogStyles(dialog);
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
applyTitleStyles(title);
|
||||||
|
|
||||||
|
const hint = document.createElement("p");
|
||||||
|
hint.textContent = "基础字段会固定保留。取消勾选后,本次及后续画像CSV将不包含对应列。";
|
||||||
|
applyHintStyles(hint);
|
||||||
|
|
||||||
|
const toolbar = document.createElement("div");
|
||||||
|
applyToolbarStyles(toolbar);
|
||||||
|
|
||||||
|
const selectAllButton = document.createElement("button");
|
||||||
|
selectAllButton.type = "button";
|
||||||
|
selectAllButton.textContent = "全选";
|
||||||
|
applySecondaryButtonStyles(selectAllButton);
|
||||||
|
|
||||||
|
const resetButton = document.createElement("button");
|
||||||
|
resetButton.type = "button";
|
||||||
|
resetButton.textContent = "恢复默认";
|
||||||
|
applySecondaryButtonStyles(resetButton);
|
||||||
|
|
||||||
|
toolbar.append(selectAllButton, resetButton);
|
||||||
|
|
||||||
|
const groupContainer = document.createElement("div");
|
||||||
|
applyGroupContainerStyles(groupContainer);
|
||||||
|
|
||||||
|
const fieldInputs: HTMLInputElement[] = [];
|
||||||
|
groups.forEach((group) => {
|
||||||
|
const groupSection = document.createElement("section");
|
||||||
|
groupSection.dataset.audienceProfileFieldDialogGroup = "section";
|
||||||
|
applyGroupSectionStyles(groupSection);
|
||||||
|
|
||||||
|
const groupHeader = document.createElement("label");
|
||||||
|
applyGroupHeaderStyles(groupHeader);
|
||||||
|
|
||||||
|
const groupInput = document.createElement("input");
|
||||||
|
groupInput.type = "checkbox";
|
||||||
|
|
||||||
|
const groupTitle = document.createElement("span");
|
||||||
|
groupTitle.textContent = group.label;
|
||||||
|
|
||||||
|
groupHeader.append(groupInput, groupTitle);
|
||||||
|
|
||||||
|
const fieldList = document.createElement("div");
|
||||||
|
applyFieldListStyles(fieldList);
|
||||||
|
|
||||||
|
const groupFieldInputs = group.headers.map((header) => {
|
||||||
|
const fieldLabel = document.createElement("label");
|
||||||
|
applyFieldLabelStyles(fieldLabel);
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.value = header;
|
||||||
|
input.dataset.audienceProfileFieldDialogField = "checkbox";
|
||||||
|
input.checked = selectedHeaderSet.has(header);
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.textContent = header;
|
||||||
|
|
||||||
|
fieldLabel.append(input, text);
|
||||||
|
fieldList.append(fieldLabel);
|
||||||
|
fieldInputs.push(input);
|
||||||
|
return input;
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncGroupInput = () => {
|
||||||
|
const checkedCount = groupFieldInputs.filter((input) => input.checked).length;
|
||||||
|
groupInput.checked = checkedCount === groupFieldInputs.length;
|
||||||
|
groupInput.indeterminate = checkedCount > 0 && checkedCount < groupFieldInputs.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
groupInput.addEventListener("change", () => {
|
||||||
|
groupFieldInputs.forEach((input) => {
|
||||||
|
input.checked = groupInput.checked;
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
});
|
||||||
|
groupFieldInputs.forEach((input) => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
syncGroupInput();
|
||||||
|
syncTitle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
syncGroupInput();
|
||||||
|
|
||||||
|
groupSection.append(groupHeader, fieldList);
|
||||||
|
groupContainer.append(groupSection);
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
applyActionsStyles(actions);
|
||||||
|
|
||||||
|
const cancelButton = document.createElement("button");
|
||||||
|
cancelButton.type = "button";
|
||||||
|
cancelButton.textContent = "取消";
|
||||||
|
applySecondaryButtonStyles(cancelButton);
|
||||||
|
|
||||||
|
const confirmButton = document.createElement("button");
|
||||||
|
confirmButton.type = "button";
|
||||||
|
confirmButton.dataset.audienceProfileFieldDialogSave = "button";
|
||||||
|
confirmButton.textContent = "保存";
|
||||||
|
applyPrimaryButtonStyles(confirmButton);
|
||||||
|
|
||||||
|
actions.append(cancelButton, confirmButton);
|
||||||
|
dialog.append(title, hint, toolbar, groupContainer, actions);
|
||||||
|
overlay.append(dialog);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
function syncTitle() {
|
||||||
|
const checkedCount = fieldInputs.filter((input) => input.checked).length;
|
||||||
|
title.textContent = `可选字段(已选 ${checkedCount}/${fieldInputs.length} 个字段)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(value: string[] | null) {
|
||||||
|
overlay.remove();
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAllButton.addEventListener("click", () => {
|
||||||
|
fieldInputs.forEach((input) => {
|
||||||
|
input.checked = true;
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
syncAllGroupInputs(dialog);
|
||||||
|
});
|
||||||
|
resetButton.addEventListener("click", () => {
|
||||||
|
fieldInputs.forEach((input) => {
|
||||||
|
input.checked = true;
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
syncAllGroupInputs(dialog);
|
||||||
|
});
|
||||||
|
cancelButton.addEventListener("click", () => close(null));
|
||||||
|
confirmButton.addEventListener("click", () => {
|
||||||
|
const nextHeaders = fieldInputs
|
||||||
|
.filter((input) => input.checked)
|
||||||
|
.map((input) => input.value);
|
||||||
|
close(nextHeaders);
|
||||||
|
});
|
||||||
|
overlay.addEventListener("click", (event) => {
|
||||||
|
if (event.target === overlay) {
|
||||||
|
close(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
syncTitle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAllGroupInputs(dialog: HTMLElement): void {
|
||||||
|
dialog
|
||||||
|
.querySelectorAll('[data-audience-profile-field-dialog-group="section"]')
|
||||||
|
.forEach((section) => {
|
||||||
|
const groupInput = section.querySelector(":scope > label > input");
|
||||||
|
const fieldInputs = Array.from(
|
||||||
|
section.querySelectorAll(":scope > div input")
|
||||||
|
) as HTMLInputElement[];
|
||||||
|
if (!(groupInput instanceof HTMLInputElement) || fieldInputs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedCount = fieldInputs.filter((input) => input.checked).length;
|
||||||
|
groupInput.checked = checkedCount === fieldInputs.length;
|
||||||
|
groupInput.indeterminate = checkedCount > 0 && checkedCount < fieldInputs.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOverlayStyles(overlay: HTMLElement): void {
|
||||||
|
overlay.style.position = "fixed";
|
||||||
|
overlay.style.inset = "0";
|
||||||
|
overlay.style.zIndex = "2147483647";
|
||||||
|
overlay.style.display = "flex";
|
||||||
|
overlay.style.alignItems = "center";
|
||||||
|
overlay.style.justifyContent = "center";
|
||||||
|
overlay.style.background = "rgba(15, 23, 42, 0.38)";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDialogStyles(dialog: HTMLElement): void {
|
||||||
|
dialog.style.width = "680px";
|
||||||
|
dialog.style.maxWidth = "calc(100vw - 32px)";
|
||||||
|
dialog.style.maxHeight = "calc(100vh - 48px)";
|
||||||
|
dialog.style.display = "flex";
|
||||||
|
dialog.style.flexDirection = "column";
|
||||||
|
dialog.style.background = "#ffffff";
|
||||||
|
dialog.style.borderRadius = "8px";
|
||||||
|
dialog.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)";
|
||||||
|
dialog.style.padding = "20px";
|
||||||
|
dialog.style.boxSizing = "border-box";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTitleStyles(title: HTMLElement): void {
|
||||||
|
title.style.margin = "0 0 8px";
|
||||||
|
title.style.fontSize = "18px";
|
||||||
|
title.style.fontWeight = "700";
|
||||||
|
title.style.color = "#1f2329";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHintStyles(hint: HTMLElement): void {
|
||||||
|
hint.style.margin = "0 0 12px";
|
||||||
|
hint.style.fontSize = "13px";
|
||||||
|
hint.style.lineHeight = "20px";
|
||||||
|
hint.style.color = "#64748b";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyToolbarStyles(toolbar: HTMLElement): void {
|
||||||
|
toolbar.style.display = "flex";
|
||||||
|
toolbar.style.gap = "8px";
|
||||||
|
toolbar.style.marginBottom = "12px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGroupContainerStyles(container: HTMLElement): void {
|
||||||
|
container.style.display = "flex";
|
||||||
|
container.style.flexDirection = "column";
|
||||||
|
container.style.gap = "10px";
|
||||||
|
container.style.overflow = "auto";
|
||||||
|
container.style.paddingRight = "4px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGroupSectionStyles(section: HTMLElement): void {
|
||||||
|
section.style.border = "1px solid #e5e7eb";
|
||||||
|
section.style.borderRadius = "8px";
|
||||||
|
section.style.padding = "10px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGroupHeaderStyles(label: HTMLElement): void {
|
||||||
|
label.style.display = "flex";
|
||||||
|
label.style.alignItems = "center";
|
||||||
|
label.style.gap = "8px";
|
||||||
|
label.style.fontWeight = "700";
|
||||||
|
label.style.color = "#1f2329";
|
||||||
|
label.style.marginBottom = "8px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFieldListStyles(list: HTMLElement): void {
|
||||||
|
list.style.display = "grid";
|
||||||
|
list.style.gridTemplateColumns = "repeat(auto-fit, minmax(220px, 1fr))";
|
||||||
|
list.style.gap = "8px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFieldLabelStyles(label: HTMLElement): void {
|
||||||
|
label.style.display = "flex";
|
||||||
|
label.style.alignItems = "center";
|
||||||
|
label.style.gap = "6px";
|
||||||
|
label.style.fontSize = "13px";
|
||||||
|
label.style.lineHeight = "18px";
|
||||||
|
label.style.color = "#374151";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyActionsStyles(actions: HTMLElement): void {
|
||||||
|
actions.style.display = "flex";
|
||||||
|
actions.style.justifyContent = "flex-end";
|
||||||
|
actions.style.columnGap = "8px";
|
||||||
|
actions.style.marginTop = "14px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPrimaryButtonStyles(button: HTMLButtonElement): void {
|
||||||
|
button.style.height = "32px";
|
||||||
|
button.style.padding = "0 15px";
|
||||||
|
button.style.border = "1px solid #7f1d2d";
|
||||||
|
button.style.borderRadius = "8px";
|
||||||
|
button.style.background = "#7f1d2d";
|
||||||
|
button.style.color = "#ffffff";
|
||||||
|
button.style.fontWeight = "600";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
|
||||||
|
button.style.height = "32px";
|
||||||
|
button.style.padding = "0 15px";
|
||||||
|
button.style.border = "1px solid #d0d7de";
|
||||||
|
button.style.borderRadius = "8px";
|
||||||
|
button.style.background = "#ffffff";
|
||||||
|
button.style.color = "#1f2329";
|
||||||
|
button.style.fontWeight = "600";
|
||||||
|
}
|
||||||
@ -74,6 +74,14 @@ const BACKEND_METRIC_COLUMNS: CsvColumn[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function listRateCsvHeaders(): string[] {
|
||||||
|
return RATE_COLUMNS.map((column) => column.header);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listBackendMetricCsvHeaders(): string[] {
|
||||||
|
return BACKEND_METRIC_COLUMNS.map((column) => column.header);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildMarketCsv(records: MarketRecord[]): string {
|
export function buildMarketCsv(records: MarketRecord[]): string {
|
||||||
const csvColumns = buildMarketCsvColumns(records);
|
const csvColumns = buildMarketCsvColumns(records);
|
||||||
const headerLine = csvColumns.map((column) => column.header).join(",");
|
const headerLine = csvColumns.map((column) => column.header).join(",");
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { buildMarketCsv } from "./csv-exporter";
|
import { buildMarketCsv } from "./csv-exporter";
|
||||||
import { buildAudienceProfileCsv } from "./audience-profile-csv";
|
import {
|
||||||
|
buildAudienceProfileCsv,
|
||||||
|
listAudienceProfileSelectableFieldGroups,
|
||||||
|
type AudienceProfileCsvOptions
|
||||||
|
} from "./audience-profile-csv";
|
||||||
import {
|
import {
|
||||||
AUDIENCE_PROFILE_TARGETS,
|
AUDIENCE_PROFILE_TARGETS,
|
||||||
createAudienceProfileClient,
|
createAudienceProfileClient,
|
||||||
@ -8,6 +12,7 @@ import {
|
|||||||
import { createAuthorBaseClient } from "./author-base-client";
|
import { createAuthorBaseClient } from "./author-base-client";
|
||||||
import { parseAuthorIds } from "./author-id-input";
|
import { parseAuthorIds } from "./author-id-input";
|
||||||
import { createBusinessAbilityClient } from "./business-ability-client";
|
import { createBusinessAbilityClient } from "./business-ability-client";
|
||||||
|
import { promptForAudienceProfileFields } from "./audience-profile-field-dialog";
|
||||||
import { promptForAuthorIds } from "./author-id-dialog";
|
import { promptForAuthorIds } from "./author-id-dialog";
|
||||||
import { promptForBatchName } from "./batch-name-dialog";
|
import { promptForBatchName } from "./batch-name-dialog";
|
||||||
import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
||||||
@ -58,7 +63,10 @@ interface MutationObserverLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateMarketControllerOptions {
|
export interface CreateMarketControllerOptions {
|
||||||
buildAudienceProfileCsv?: (rows: AudienceProfileExportRow[]) => string;
|
buildAudienceProfileCsv?: (
|
||||||
|
rows: AudienceProfileExportRow[],
|
||||||
|
options?: AudienceProfileCsvOptions
|
||||||
|
) => string;
|
||||||
buildCsv?: (records: MarketRecord[]) => string;
|
buildCsv?: (records: MarketRecord[]) => string;
|
||||||
document: Document;
|
document: Document;
|
||||||
getAuthState?: () => Promise<AuthStateValue>;
|
getAuthState?: () => Promise<AuthStateValue>;
|
||||||
@ -85,6 +93,9 @@ export interface CreateMarketControllerOptions {
|
|||||||
window: Window;
|
window: Window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY =
|
||||||
|
"sces:audience-profile:selectedHeaders";
|
||||||
|
|
||||||
export function createMarketController(options: CreateMarketControllerOptions) {
|
export function createMarketController(options: CreateMarketControllerOptions) {
|
||||||
const marketApiClient = createMarketApiClient();
|
const marketApiClient = createMarketApiClient();
|
||||||
const audienceProfileClient = createAudienceProfileClient();
|
const audienceProfileClient = createAudienceProfileClient();
|
||||||
@ -266,7 +277,12 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.onCsvReady?.(buildAudienceCsv(rows), buildAudienceProfileFilename());
|
options.onCsvReady?.(
|
||||||
|
buildAudienceCsv(rows, {
|
||||||
|
selectedHeaders: readAudienceProfileSelectedHeaders()
|
||||||
|
}),
|
||||||
|
buildAudienceProfileFilename()
|
||||||
|
);
|
||||||
setToolbarExportStatus(toolbar, "");
|
setToolbarExportStatus(toolbar, "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToolbarExportStatus(
|
setToolbarExportStatus(
|
||||||
@ -313,7 +329,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
options.onCsvReady?.(
|
options.onCsvReady?.(
|
||||||
buildAudienceCsv(rows),
|
buildAudienceCsv(rows, {
|
||||||
|
selectedHeaders: readAudienceProfileSelectedHeaders()
|
||||||
|
}),
|
||||||
buildAudienceProfileFilename(new Date(), "按ID导出")
|
buildAudienceProfileFilename(new Date(), "按ID导出")
|
||||||
);
|
);
|
||||||
setToolbarExportStatus(toolbar, "");
|
setToolbarExportStatus(toolbar, "");
|
||||||
@ -326,6 +344,24 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
setToolbarBusyState(toolbar, false);
|
setToolbarBusyState(toolbar, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onConfigureAudienceProfileFields: async () => {
|
||||||
|
const groups = listAudienceProfileSelectableFieldGroups();
|
||||||
|
const selectedHeaders = readAudienceProfileSelectedHeaders();
|
||||||
|
const nextHeaders = await promptForAudienceProfileFields(
|
||||||
|
options.document,
|
||||||
|
groups,
|
||||||
|
selectedHeaders
|
||||||
|
);
|
||||||
|
if (nextHeaders === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAudienceProfileSelectedHeaders(nextHeaders);
|
||||||
|
setToolbarExportStatus(
|
||||||
|
toolbar,
|
||||||
|
`画像字段已保存(已选 ${nextHeaders.length}/${readAudienceProfileSelectableHeaders().length} 个字段)`
|
||||||
|
);
|
||||||
|
},
|
||||||
onSubmitBatch: async () => {
|
onSubmitBatch: async () => {
|
||||||
syncSelectionStateFromDom();
|
syncSelectionStateFromDom();
|
||||||
const exportTarget = readToolbarExportTarget(toolbar);
|
const exportTarget = readToolbarExportTarget(toolbar);
|
||||||
@ -909,6 +945,55 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
return "铁粉画像";
|
return "铁粉画像";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readAudienceProfileSelectableHeaders(): string[] {
|
||||||
|
return listAudienceProfileSelectableFieldGroups().flatMap(
|
||||||
|
(group) => group.headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAudienceProfileSelectedHeaders(): string[] {
|
||||||
|
const selectableHeaders = readAudienceProfileSelectableHeaders();
|
||||||
|
const selectableHeaderSet = new Set(selectableHeaders);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawValue = options.window.localStorage?.getItem(
|
||||||
|
AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY
|
||||||
|
);
|
||||||
|
if (!rawValue) {
|
||||||
|
return selectableHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = JSON.parse(rawValue) as unknown;
|
||||||
|
if (!Array.isArray(parsedValue)) {
|
||||||
|
return selectableHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedHeaders = parsedValue.filter(
|
||||||
|
(header): header is string =>
|
||||||
|
typeof header === "string" && selectableHeaderSet.has(header)
|
||||||
|
);
|
||||||
|
return selectedHeaders.length > 0 ? selectedHeaders : selectableHeaders;
|
||||||
|
} catch {
|
||||||
|
return selectableHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAudienceProfileSelectedHeaders(headers: string[]): void {
|
||||||
|
const selectableHeaderSet = new Set(readAudienceProfileSelectableHeaders());
|
||||||
|
const selectedHeaders = headers.filter((header) =>
|
||||||
|
selectableHeaderSet.has(header)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
options.window.localStorage?.setItem(
|
||||||
|
AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY,
|
||||||
|
JSON.stringify(selectedHeaders)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable in hardened browser contexts.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareCurrentPageForExport(): Promise<void> {
|
async function prepareCurrentPageForExport(): Promise<void> {
|
||||||
await runSyncCycle();
|
await runSyncCycle();
|
||||||
await harvestCurrentPageForExport();
|
await harvestCurrentPageForExport();
|
||||||
|
|||||||
@ -7,12 +7,14 @@ export interface PluginToolbarHandlers {
|
|||||||
onExport(): Promise<void> | void;
|
onExport(): Promise<void> | void;
|
||||||
onExportAudienceProfile(): Promise<void> | void;
|
onExportAudienceProfile(): Promise<void> | void;
|
||||||
onExportAudienceProfileByIds(): Promise<void> | void;
|
onExportAudienceProfileByIds(): Promise<void> | void;
|
||||||
|
onConfigureAudienceProfileFields(): Promise<void> | void;
|
||||||
onSubmitBatch(): Promise<void> | void;
|
onSubmitBatch(): Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginToolbarDom {
|
export interface PluginToolbarDom {
|
||||||
audienceProfileByIdExportButton: HTMLButtonElement;
|
audienceProfileByIdExportButton: HTMLButtonElement;
|
||||||
audienceProfileExportButton: HTMLButtonElement;
|
audienceProfileExportButton: HTMLButtonElement;
|
||||||
|
audienceProfileFieldButton: HTMLButtonElement;
|
||||||
batchSubmitButton: HTMLButtonElement;
|
batchSubmitButton: HTMLButtonElement;
|
||||||
exportButton: HTMLButtonElement;
|
exportButton: HTMLButtonElement;
|
||||||
exportCustomPagesInput: HTMLInputElement;
|
exportCustomPagesInput: HTMLInputElement;
|
||||||
@ -89,6 +91,11 @@ export function ensurePluginToolbar(
|
|||||||
audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button";
|
audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button";
|
||||||
audienceProfileByIdExportButton.textContent = "按ID导出画像CSV";
|
audienceProfileByIdExportButton.textContent = "按ID导出画像CSV";
|
||||||
|
|
||||||
|
const audienceProfileFieldButton = document.createElement("button");
|
||||||
|
audienceProfileFieldButton.type = "button";
|
||||||
|
audienceProfileFieldButton.dataset.pluginAudienceProfileFields = "button";
|
||||||
|
audienceProfileFieldButton.textContent = "画像字段";
|
||||||
|
|
||||||
const batchSubmitButton = document.createElement("button");
|
const batchSubmitButton = document.createElement("button");
|
||||||
batchSubmitButton.type = "button";
|
batchSubmitButton.type = "button";
|
||||||
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
||||||
@ -104,6 +111,7 @@ export function ensurePluginToolbar(
|
|||||||
exportButton,
|
exportButton,
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportStatusText
|
exportStatusText
|
||||||
);
|
);
|
||||||
@ -112,6 +120,7 @@ export function ensurePluginToolbar(
|
|||||||
applyNativeControlStyles(document, {
|
applyNativeControlStyles(document, {
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
@ -128,12 +137,16 @@ export function ensurePluginToolbar(
|
|||||||
audienceProfileByIdExportButton.addEventListener("click", () => {
|
audienceProfileByIdExportButton.addEventListener("click", () => {
|
||||||
void handlers.onExportAudienceProfileByIds();
|
void handlers.onExportAudienceProfileByIds();
|
||||||
});
|
});
|
||||||
|
audienceProfileFieldButton.addEventListener("click", () => {
|
||||||
|
void handlers.onConfigureAudienceProfileFields();
|
||||||
|
});
|
||||||
batchSubmitButton.addEventListener("click", () => {
|
batchSubmitButton.addEventListener("click", () => {
|
||||||
void handlers.onSubmitBatch();
|
void handlers.onSubmitBatch();
|
||||||
});
|
});
|
||||||
exportRangeSelect.addEventListener("change", () => {
|
exportRangeSelect.addEventListener("change", () => {
|
||||||
syncCustomPagesInputVisibility({
|
syncCustomPagesInputVisibility({
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
@ -147,6 +160,7 @@ export function ensurePluginToolbar(
|
|||||||
const toolbarDom = {
|
const toolbarDom = {
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
@ -178,6 +192,9 @@ function readToolbarDom(root: HTMLElement): PluginToolbarDom {
|
|||||||
audienceProfileExportButton: root.querySelector(
|
audienceProfileExportButton: root.querySelector(
|
||||||
'[data-plugin-export-audience-profile="button"]'
|
'[data-plugin-export-audience-profile="button"]'
|
||||||
) as HTMLButtonElement,
|
) as HTMLButtonElement,
|
||||||
|
audienceProfileFieldButton: root.querySelector(
|
||||||
|
'[data-plugin-audience-profile-fields="button"]'
|
||||||
|
) as HTMLButtonElement,
|
||||||
batchSubmitButton: root.querySelector(
|
batchSubmitButton: root.querySelector(
|
||||||
'[data-plugin-batch-submit="button"]'
|
'[data-plugin-batch-submit="button"]'
|
||||||
) as HTMLButtonElement,
|
) as HTMLButtonElement,
|
||||||
@ -260,6 +277,7 @@ export function setToolbarBusyState(
|
|||||||
): void {
|
): void {
|
||||||
[
|
[
|
||||||
toolbar.batchSubmitButton,
|
toolbar.batchSubmitButton,
|
||||||
|
toolbar.audienceProfileFieldButton,
|
||||||
toolbar.audienceProfileByIdExportButton,
|
toolbar.audienceProfileByIdExportButton,
|
||||||
toolbar.audienceProfileExportButton,
|
toolbar.audienceProfileExportButton,
|
||||||
toolbar.exportButton,
|
toolbar.exportButton,
|
||||||
@ -460,6 +478,7 @@ function applyNativeControlStyles(
|
|||||||
controls: {
|
controls: {
|
||||||
audienceProfileExportButton: HTMLButtonElement;
|
audienceProfileExportButton: HTMLButtonElement;
|
||||||
audienceProfileByIdExportButton: HTMLButtonElement;
|
audienceProfileByIdExportButton: HTMLButtonElement;
|
||||||
|
audienceProfileFieldButton: HTMLButtonElement;
|
||||||
batchSubmitButton: HTMLButtonElement;
|
batchSubmitButton: HTMLButtonElement;
|
||||||
exportButton: HTMLButtonElement;
|
exportButton: HTMLButtonElement;
|
||||||
exportCustomPagesInput: HTMLInputElement;
|
exportCustomPagesInput: HTMLInputElement;
|
||||||
@ -478,6 +497,7 @@ function applyNativeControlStyles(
|
|||||||
controls.exportButton.className = nativeButton.className;
|
controls.exportButton.className = nativeButton.className;
|
||||||
controls.audienceProfileExportButton.className = nativeButton.className;
|
controls.audienceProfileExportButton.className = nativeButton.className;
|
||||||
controls.audienceProfileByIdExportButton.className = nativeButton.className;
|
controls.audienceProfileByIdExportButton.className = nativeButton.className;
|
||||||
|
controls.audienceProfileFieldButton.className = nativeButton.className;
|
||||||
controls.batchSubmitButton.className = nativeButton.className;
|
controls.batchSubmitButton.className = nativeButton.className;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,6 +505,7 @@ function applyNativeControlStyles(
|
|||||||
controls.exportButton,
|
controls.exportButton,
|
||||||
controls.audienceProfileExportButton,
|
controls.audienceProfileExportButton,
|
||||||
controls.audienceProfileByIdExportButton,
|
controls.audienceProfileByIdExportButton,
|
||||||
|
controls.audienceProfileFieldButton,
|
||||||
controls.batchSubmitButton
|
controls.batchSubmitButton
|
||||||
].forEach((button) => {
|
].forEach((button) => {
|
||||||
applyPrimaryButtonStyles(button);
|
applyPrimaryButtonStyles(button);
|
||||||
@ -539,6 +560,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
|||||||
[data-plugin-export="button"]:hover:not(:disabled),
|
[data-plugin-export="button"]:hover:not(:disabled),
|
||||||
[data-plugin-export-audience-profile="button"]:hover:not(:disabled),
|
[data-plugin-export-audience-profile="button"]:hover:not(:disabled),
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
|
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
|
||||||
|
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled),
|
||||||
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
|
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
|
||||||
background-color: #6d1627 !important;
|
background-color: #6d1627 !important;
|
||||||
border-color: #6d1627 !important;
|
border-color: #6d1627 !important;
|
||||||
@ -547,6 +569,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
|||||||
[data-plugin-export="button"]:active:not(:disabled),
|
[data-plugin-export="button"]:active:not(:disabled),
|
||||||
[data-plugin-export-audience-profile="button"]:active:not(:disabled),
|
[data-plugin-export-audience-profile="button"]:active:not(:disabled),
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
|
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
|
||||||
|
[data-plugin-audience-profile-fields="button"]:active:not(:disabled),
|
||||||
[data-plugin-batch-submit="button"]:active:not(:disabled) {
|
[data-plugin-batch-submit="button"]:active:not(:disabled) {
|
||||||
background-color: #58111f !important;
|
background-color: #58111f !important;
|
||||||
border-color: #58111f !important;
|
border-color: #58111f !important;
|
||||||
@ -556,6 +579,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
|||||||
[data-plugin-export="button"]:focus-visible,
|
[data-plugin-export="button"]:focus-visible,
|
||||||
[data-plugin-export-audience-profile="button"]:focus-visible,
|
[data-plugin-export-audience-profile="button"]:focus-visible,
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
|
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
|
||||||
|
[data-plugin-audience-profile-fields="button"]:focus-visible,
|
||||||
[data-plugin-batch-submit="button"]:focus-visible {
|
[data-plugin-batch-submit="button"]:focus-visible {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
||||||
@ -564,6 +588,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
|||||||
[data-plugin-export="button"]:disabled,
|
[data-plugin-export="button"]:disabled,
|
||||||
[data-plugin-export-audience-profile="button"]:disabled,
|
[data-plugin-export-audience-profile="button"]:disabled,
|
||||||
[data-plugin-export-audience-profile-by-id="button"]:disabled,
|
[data-plugin-export-audience-profile-by-id="button"]:disabled,
|
||||||
|
[data-plugin-audience-profile-fields="button"]:disabled,
|
||||||
[data-plugin-batch-submit="button"]:disabled {
|
[data-plugin-batch-submit="button"]:disabled {
|
||||||
background-color: #c89ca4 !important;
|
background-color: #c89ca4 !important;
|
||||||
border-color: #c89ca4 !important;
|
border-color: #c89ca4 !important;
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { buildAudienceProfileCsv } from "../src/content/market/audience-profile-csv";
|
import {
|
||||||
|
buildAudienceProfileCsv,
|
||||||
|
listAudienceProfileSelectableFieldGroups,
|
||||||
|
listAudienceProfileCsvHeaders
|
||||||
|
} from "../src/content/market/audience-profile-csv";
|
||||||
import type { AudienceProfileExportRow } from "../src/content/market/audience-profile-types";
|
import type { AudienceProfileExportRow } from "../src/content/market/audience-profile-types";
|
||||||
|
|
||||||
describe("audience-profile-csv", () => {
|
describe("audience-profile-csv", () => {
|
||||||
@ -193,6 +197,83 @@ describe("audience-profile-csv", () => {
|
|||||||
expect(readCsvValue(csv, "铁粉画像-Z世代占比")).toBe("0%");
|
expect(readCsvValue(csv, "铁粉画像-Z世代占比")).toBe("0%");
|
||||||
expect(csv.split("\n")[0]).not.toContain("新一线城市占比");
|
expect(csv.split("\n")[0]).not.toContain("新一线城市占比");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("filters export columns by selected headers", () => {
|
||||||
|
const row = buildSuccessRow();
|
||||||
|
const csv = buildAudienceProfileCsv([row], {
|
||||||
|
selectedHeaders: [
|
||||||
|
"内容数据-个人视频-播放量中位数",
|
||||||
|
"观众画像-男性占比"
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const [headerLine, rowLine] = csv.split("\n");
|
||||||
|
|
||||||
|
expect(headerLine).toBe(
|
||||||
|
"达人信息,连接用户数,内容数据-个人视频-播放量中位数,观众画像-男性占比"
|
||||||
|
);
|
||||||
|
expect(rowLine).toBe("达人 A,300w,3738.4w,71.7%");
|
||||||
|
expect(headerLine).not.toContain("秒思api-看后搜数");
|
||||||
|
expect(headerLine).not.toContain("粉丝画像-女性占比");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("always keeps fixed id export headers when filtering", () => {
|
||||||
|
const row = buildSuccessRow({
|
||||||
|
exportFields: {
|
||||||
|
达人ID: "123",
|
||||||
|
达人名称: "达人 A",
|
||||||
|
导出状态: "成功",
|
||||||
|
失败原因: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const csv = buildAudienceProfileCsv([row], {
|
||||||
|
selectedHeaders: ["内容数据-个人视频-播放量中位数"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const [headerLine, rowLine] = csv.split("\n");
|
||||||
|
|
||||||
|
expect(headerLine).toBe(
|
||||||
|
"达人ID,达人名称,导出状态,失败原因,内容数据-个人视频-播放量中位数"
|
||||||
|
);
|
||||||
|
expect(rowLine).toBe("123,达人 A,成功,,3738.4w");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lists headers for field picker defaults", () => {
|
||||||
|
expect(listAudienceProfileCsvHeaders([buildSuccessRow()])).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"达人信息",
|
||||||
|
"连接用户数",
|
||||||
|
"秒思api-看后搜数",
|
||||||
|
"内容数据-个人视频-播放量中位数",
|
||||||
|
"效果预估-20-60s视频-预期CPM",
|
||||||
|
"观众画像-男性占比",
|
||||||
|
"铁粉画像-小镇青年占比"
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("groups selectable profile export fields", () => {
|
||||||
|
expect(listAudienceProfileSelectableFieldGroups()).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.arrayContaining(["秒思api-看后搜数"]),
|
||||||
|
label: "秒思api数据"
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.arrayContaining(["内容数据-个人视频-播放量中位数"]),
|
||||||
|
label: "内容数据"
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.arrayContaining(["效果预估-20-60s视频-预期CPM"]),
|
||||||
|
label: "效果预估"
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.arrayContaining(["观众画像-男性占比"]),
|
||||||
|
label: "观众画像"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function readCsvValue(csv: string, header: string): string {
|
function readCsvValue(csv: string, header: string): string {
|
||||||
@ -204,3 +285,47 @@ function readCsvValue(csv: string, header: string): string {
|
|||||||
expect(index).toBeGreaterThanOrEqual(0);
|
expect(index).toBeGreaterThanOrEqual(0);
|
||||||
return values[index] ?? "";
|
return values[index] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSuccessRow(
|
||||||
|
overrides: Partial<AudienceProfileExportRow["record"]> = {}
|
||||||
|
): AudienceProfileExportRow {
|
||||||
|
return {
|
||||||
|
profiles: {
|
||||||
|
audience: {
|
||||||
|
age: [{ label: "31-40", value: "50%" }],
|
||||||
|
cityTier: [{ label: "一线城市", value: "100%" }],
|
||||||
|
crowd: [{ label: "都市蓝领", value: "100%" }],
|
||||||
|
gender: [{ label: "男性", value: "71.7%" }],
|
||||||
|
status: "success"
|
||||||
|
},
|
||||||
|
fans: { status: "success" },
|
||||||
|
longtimeFans: { status: "success" }
|
||||||
|
},
|
||||||
|
businessAbility: {
|
||||||
|
estimates: {
|
||||||
|
twentyToSixty: {
|
||||||
|
expectedCpe: "3.7",
|
||||||
|
expectedCpm: "212.0",
|
||||||
|
expectedPlay: "250w",
|
||||||
|
hotRate: "缺失"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: "success",
|
||||||
|
videos: {
|
||||||
|
personalVideo: {
|
||||||
|
medianPlay: "3738.4w"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "达人 A",
|
||||||
|
exportFields: {
|
||||||
|
达人信息: "达人 A",
|
||||||
|
连接用户数: "300w"
|
||||||
|
},
|
||||||
|
status: "success",
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ describe("market-content-entry", () => {
|
|||||||
document.documentElement.removeAttribute("data-sces-market-rows");
|
document.documentElement.removeAttribute("data-sces-market-rows");
|
||||||
document.documentElement.removeAttribute("data-sces-market-request-snapshot");
|
document.documentElement.removeAttribute("data-sces-market-request-snapshot");
|
||||||
document.documentElement.removeAttribute("data-test-page-index");
|
document.documentElement.removeAttribute("data-test-page-index");
|
||||||
|
window.localStorage.clear();
|
||||||
window.history.replaceState({}, "", "/");
|
window.history.replaceState({}, "", "/");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1689,17 +1690,22 @@ describe("market-content-entry", () => {
|
|||||||
{ authorType: 1, source: "fansDistribution" },
|
{ authorType: 1, source: "fansDistribution" },
|
||||||
{ authorType: 5, source: "fansDistribution" }
|
{ authorType: 5, source: "fansDistribution" }
|
||||||
]);
|
]);
|
||||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
expect(buildAudienceProfileCsv).toHaveBeenCalledWith(
|
||||||
{
|
[
|
||||||
profiles: {
|
{
|
||||||
audience: expect.objectContaining({ status: "success" }),
|
profiles: {
|
||||||
fans: expect.objectContaining({ status: "success" }),
|
audience: expect.objectContaining({ status: "success" }),
|
||||||
longtimeFans: expect.objectContaining({ status: "success" })
|
fans: expect.objectContaining({ status: "success" }),
|
||||||
},
|
longtimeFans: expect.objectContaining({ status: "success" })
|
||||||
businessAbility: expect.objectContaining({ status: "success" }),
|
},
|
||||||
record: expect.objectContaining({ authorId: "222" })
|
businessAbility: expect.objectContaining({ status: "success" }),
|
||||||
}
|
record: expect.objectContaining({ authorId: "222" })
|
||||||
]);
|
}
|
||||||
|
],
|
||||||
|
expect.objectContaining({
|
||||||
|
selectedHeaders: expect.arrayContaining(["秒思api-看后搜数"])
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(onCsvReady).toHaveBeenCalledWith(
|
expect(onCsvReady).toHaveBeenCalledWith(
|
||||||
"profile-csv",
|
"profile-csv",
|
||||||
expect.stringMatching(/^达人连接用户画像_\d{8}_\d{4}\.csv$/)
|
expect.stringMatching(/^达人连接用户画像_\d{8}_\d{4}\.csv$/)
|
||||||
@ -1790,56 +1796,149 @@ describe("market-content-entry", () => {
|
|||||||
"6866044569306267651",
|
"6866044569306267651",
|
||||||
"7040323176106033165"
|
"7040323176106033165"
|
||||||
]);
|
]);
|
||||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
expect(buildAudienceProfileCsv).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
[
|
||||||
record: expect.objectContaining({
|
expect.objectContaining({
|
||||||
authorId: "6866044569306267651",
|
record: expect.objectContaining({
|
||||||
authorName: "小九儿",
|
authorId: "6866044569306267651",
|
||||||
backendMetrics: expect.objectContaining({
|
authorName: "小九儿",
|
||||||
a3IncreaseCount: "100",
|
backendMetrics: expect.objectContaining({
|
||||||
afterViewSearchCount: "300",
|
a3IncreaseCount: "100",
|
||||||
afterViewSearchRate: "1.1%",
|
afterViewSearchCount: "300",
|
||||||
cpSearch: "10",
|
afterViewSearchRate: "1.1%",
|
||||||
cpa3: "30",
|
cpSearch: "10",
|
||||||
newA3Rate: "3.3%"
|
cpa3: "30",
|
||||||
}),
|
newA3Rate: "3.3%"
|
||||||
exportFields: {
|
}),
|
||||||
达人ID: "6866044569306267651",
|
exportFields: {
|
||||||
达人名称: "小九儿",
|
达人ID: "6866044569306267651",
|
||||||
导出状态: "成功",
|
达人名称: "小九儿",
|
||||||
失败原因: ""
|
导出状态: "成功",
|
||||||
},
|
失败原因: ""
|
||||||
rates: {
|
},
|
||||||
personalVideoAfterSearchRate: "12.3%",
|
rates: {
|
||||||
singleVideoAfterSearchRate: "7.8%"
|
personalVideoAfterSearchRate: "12.3%",
|
||||||
}
|
singleVideoAfterSearchRate: "7.8%"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
record: expect.objectContaining({
|
||||||
|
authorId: "7040323176106033165",
|
||||||
|
authorName: "达人 B",
|
||||||
|
backendMetrics: expect.objectContaining({
|
||||||
|
a3IncreaseCount: "200",
|
||||||
|
afterViewSearchCount: "400",
|
||||||
|
afterViewSearchRate: "2.2%",
|
||||||
|
cpSearch: "20",
|
||||||
|
cpa3: "40",
|
||||||
|
newA3Rate: "4.4%"
|
||||||
|
}),
|
||||||
|
rates: {
|
||||||
|
personalVideoAfterSearchRate: "45.6%",
|
||||||
|
singleVideoAfterSearchRate: "9.1%"
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}),
|
],
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
record: expect.objectContaining({
|
selectedHeaders: expect.arrayContaining(["秒思api-看后搜数"])
|
||||||
authorId: "7040323176106033165",
|
|
||||||
authorName: "达人 B",
|
|
||||||
backendMetrics: expect.objectContaining({
|
|
||||||
a3IncreaseCount: "200",
|
|
||||||
afterViewSearchCount: "400",
|
|
||||||
afterViewSearchRate: "2.2%",
|
|
||||||
cpSearch: "20",
|
|
||||||
cpa3: "40",
|
|
||||||
newA3Rate: "4.4%"
|
|
||||||
}),
|
|
||||||
rates: {
|
|
||||||
personalVideoAfterSearchRate: "45.6%",
|
|
||||||
singleVideoAfterSearchRate: "9.1%"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
]);
|
);
|
||||||
expect(onCsvReady).toHaveBeenCalledWith(
|
expect(onCsvReady).toHaveBeenCalledWith(
|
||||||
"profile-csv",
|
"profile-csv",
|
||||||
expect.stringMatching(/^达人连接用户画像_按ID导出_\d{8}_\d{4}\.csv$/)
|
expect.stringMatching(/^达人连接用户画像_按ID导出_\d{8}_\d{4}\.csv$/)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("audience profile export uses persisted selected csv fields", async () => {
|
||||||
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||||
|
]);
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"sces:audience-profile:selectedHeaders",
|
||||||
|
JSON.stringify(["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"])
|
||||||
|
);
|
||||||
|
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||||
|
const loadBusinessAbility = vi.fn(async () => ({
|
||||||
|
estimates: {},
|
||||||
|
status: "success" as const,
|
||||||
|
videos: {}
|
||||||
|
}));
|
||||||
|
const loadAudienceProfile = vi.fn(async () => ({
|
||||||
|
age: [],
|
||||||
|
crowd: [],
|
||||||
|
cityTier: [],
|
||||||
|
gender: [],
|
||||||
|
status: "success" as const
|
||||||
|
}));
|
||||||
|
const onCsvReady = vi.fn();
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
buildAudienceProfileCsv,
|
||||||
|
document,
|
||||||
|
loadBusinessAbility,
|
||||||
|
loadAudienceProfile,
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
onCsvReady,
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
|
||||||
|
click('[data-plugin-export-audience-profile="button"]');
|
||||||
|
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||||
|
|
||||||
|
expect(buildAudienceProfileCsv.mock.calls[0][1]).toEqual({
|
||||||
|
selectedHeaders: ["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("audience profile field picker persists the next selected fields", async () => {
|
||||||
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
document,
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
click('[data-plugin-audience-profile-fields="button"]');
|
||||||
|
|
||||||
|
const afterSearchCountInput = document.querySelector(
|
||||||
|
'input[data-audience-profile-field-dialog-field="checkbox"][value="秒思api-看后搜数"]'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
expect(afterSearchCountInput).not.toBeNull();
|
||||||
|
afterSearchCountInput!.checked = false;
|
||||||
|
afterSearchCountInput!.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
click('[data-audience-profile-field-dialog-save="button"]');
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
const savedHeaders = JSON.parse(
|
||||||
|
window.localStorage.getItem("sces:audience-profile:selectedHeaders") ?? "[]"
|
||||||
|
) as string[];
|
||||||
|
expect(savedHeaders).not.toContain("秒思api-看后搜数");
|
||||||
|
expect(savedHeaders).toContain("内容数据-个人视频-播放量中位数");
|
||||||
|
expect(
|
||||||
|
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||||
|
).toContain("画像字段已保存");
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"selected export keeps a generic loading status while exporting the default paged range",
|
"selected export keeps a generic loading status while exporting the default paged range",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user