feat: export audience profiles by author ids
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import type { MarketRecord } from "./types";
|
||||
|
||||
interface FetchResponseLike {
|
||||
json(): Promise<unknown>;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
type FetchLike = (
|
||||
input: string,
|
||||
init?: RequestInit
|
||||
) => Promise<FetchResponseLike>;
|
||||
|
||||
interface AuthorBaseClientOptions {
|
||||
baseUrl?: string;
|
||||
fetchImpl?: FetchLike;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export function createAuthorBaseClient(options: AuthorBaseClientOptions = {}) {
|
||||
const baseUrl = options.baseUrl ?? resolveBaseUrl();
|
||||
const fetchImpl = options.fetchImpl ?? defaultFetch;
|
||||
const timeoutMs = options.timeoutMs ?? 8000;
|
||||
|
||||
return {
|
||||
async loadAuthorBaseInfo(authorId: string): Promise<MarketRecord> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetchImpl(
|
||||
buildAuthorBaseInfoUrl(authorId, baseUrl),
|
||||
{
|
||||
credentials: "include",
|
||||
method: "GET",
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return buildFailedRecord(authorId, "request-failed");
|
||||
}
|
||||
|
||||
return mapAuthorBaseInfoResponse(authorId, await response.json());
|
||||
} catch (error) {
|
||||
return buildFailedRecord(
|
||||
authorId,
|
||||
error instanceof Error && error.name === "AbortError"
|
||||
? "timeout"
|
||||
: "request-failed"
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAuthorBaseInfoUrl(
|
||||
authorId: string,
|
||||
baseUrl: string
|
||||
): string {
|
||||
const url = new URL("/gw/api/author/get_author_base_info", baseUrl);
|
||||
url.searchParams.set("o_author_id", authorId);
|
||||
url.searchParams.set("platform_source", "1");
|
||||
url.searchParams.set("platform_channel", "1");
|
||||
url.searchParams.set("recommend", "true");
|
||||
url.searchParams.set("need_sec_uid", "true");
|
||||
url.searchParams.set("need_linkage_info", "true");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function mapAuthorBaseInfoResponse(
|
||||
authorId: string,
|
||||
payload: unknown
|
||||
): MarketRecord {
|
||||
if (!isRecord(payload)) {
|
||||
return buildFailedRecord(authorId, "bad-response");
|
||||
}
|
||||
|
||||
const authorName = readString(payload.nick_name);
|
||||
if (!authorName) {
|
||||
return buildFailedRecord(authorId, "missing-rate");
|
||||
}
|
||||
|
||||
return {
|
||||
authorId,
|
||||
authorName,
|
||||
status: "success"
|
||||
};
|
||||
}
|
||||
|
||||
function buildFailedRecord(
|
||||
authorId: string,
|
||||
failureReason: MarketRecord["failureReason"]
|
||||
): MarketRecord {
|
||||
return {
|
||||
authorId,
|
||||
authorName: "",
|
||||
failureReason,
|
||||
status: "failed"
|
||||
};
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function resolveBaseUrl(): string {
|
||||
if (typeof location !== "undefined" && location.origin) {
|
||||
return location.origin;
|
||||
}
|
||||
|
||||
return "https://xingtu.cn";
|
||||
}
|
||||
|
||||
async function defaultFetch(input: string, init?: RequestInit) {
|
||||
return fetch(input, init);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
export function promptForAuthorIds(document: Document): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.dataset.authorIdDialog = "overlay";
|
||||
applyOverlayStyles(overlay);
|
||||
|
||||
const dialog = document.createElement("section");
|
||||
applyDialogStyles(dialog);
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = "按星图ID导出画像CSV";
|
||||
applyTitleStyles(title);
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.dataset.authorIdDialogInput = "textarea";
|
||||
textarea.placeholder = "每行一个星图ID,也支持逗号、空格分隔";
|
||||
applyTextareaStyles(textarea);
|
||||
|
||||
const hint = document.createElement("p");
|
||||
hint.textContent = "粘贴客户提供的达人星图ID,确认后将批量导出画像和商业能力数据。";
|
||||
applyHintStyles(hint);
|
||||
|
||||
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.textContent = "开始导出";
|
||||
applyPrimaryButtonStyles(confirmButton);
|
||||
|
||||
actions.append(cancelButton, confirmButton);
|
||||
dialog.append(title, hint, textarea, actions);
|
||||
overlay.append(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const close = (value: string | null) => {
|
||||
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 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 = "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 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 applyTextareaStyles(textarea: HTMLTextAreaElement): void {
|
||||
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 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";
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export interface ParsedAuthorIds {
|
||||
duplicates: string[];
|
||||
invalidTokens: string[];
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
const AUTHOR_ID_PATTERN = /^\d{16,20}$/;
|
||||
|
||||
export function parseAuthorIds(input: string): ParsedAuthorIds {
|
||||
const ids: string[] = [];
|
||||
const duplicates: string[] = [];
|
||||
const invalidTokens: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
input
|
||||
.split(/[\s,,;;]+/)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((token) => {
|
||||
if (!/^\d+$/.test(token) || !AUTHOR_ID_PATTERN.test(token)) {
|
||||
invalidTokens.push(token);
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen.has(token)) {
|
||||
duplicates.push(token);
|
||||
return;
|
||||
}
|
||||
|
||||
seen.add(token);
|
||||
ids.push(token);
|
||||
});
|
||||
|
||||
return {
|
||||
duplicates,
|
||||
ids,
|
||||
invalidTokens
|
||||
};
|
||||
}
|
||||
+161
-2
@@ -5,7 +5,10 @@ import {
|
||||
createAudienceProfileClient,
|
||||
type AudienceProfileRequestTarget
|
||||
} from "./audience-profile-client";
|
||||
import { createAuthorBaseClient } from "./author-base-client";
|
||||
import { parseAuthorIds } from "./author-id-input";
|
||||
import { createBusinessAbilityClient } from "./business-ability-client";
|
||||
import { promptForAuthorIds } from "./author-id-dialog";
|
||||
import { promptForBatchName } from "./batch-name-dialog";
|
||||
import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
||||
import {
|
||||
@@ -59,6 +62,7 @@ export interface CreateMarketControllerOptions {
|
||||
buildCsv?: (records: MarketRecord[]) => string;
|
||||
document: Document;
|
||||
getAuthState?: () => Promise<AuthStateValue>;
|
||||
loadAuthorBaseInfo?: (authorId: string) => Promise<MarketRecord>;
|
||||
loadBusinessAbility?: (
|
||||
record: MarketRecord
|
||||
) => Promise<BusinessAbilityResult>;
|
||||
@@ -74,6 +78,7 @@ export interface CreateMarketControllerOptions {
|
||||
callback: MutationCallback
|
||||
) => MutationObserverLike;
|
||||
onCsvReady?: (csv: string, filename?: string) => void;
|
||||
promptAuthorIds?: () => Promise<string | null> | string | null;
|
||||
promptBatchName?: () => Promise<string | null> | string | null;
|
||||
resultStore?: ReturnType<typeof createMarketResultStore>;
|
||||
submitBatch?: (payload: BatchPayload) => Promise<unknown>;
|
||||
@@ -83,6 +88,7 @@ export interface CreateMarketControllerOptions {
|
||||
export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
const marketApiClient = createMarketApiClient();
|
||||
const audienceProfileClient = createAudienceProfileClient();
|
||||
const authorBaseClient = createAuthorBaseClient();
|
||||
const businessAbilityClient = createBusinessAbilityClient();
|
||||
const sendRuntimeMessage = createRuntimeMessageSender();
|
||||
const resultStore = options.resultStore ?? createMarketResultStore();
|
||||
@@ -95,6 +101,8 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
const buildAudienceCsv = options.buildAudienceProfileCsv ?? buildAudienceProfileCsv;
|
||||
const loadAudienceProfile =
|
||||
options.loadAudienceProfile ?? audienceProfileClient.loadAudienceProfile;
|
||||
const loadAuthorBaseInfo =
|
||||
options.loadAuthorBaseInfo ?? authorBaseClient.loadAuthorBaseInfo;
|
||||
const loadBusinessAbility =
|
||||
options.loadBusinessAbility ?? businessAbilityClient.loadBusinessAbility;
|
||||
const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage));
|
||||
@@ -104,6 +112,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
const promptBatchName =
|
||||
options.promptBatchName ??
|
||||
(() => promptForBatchName(options.document));
|
||||
const promptAuthorIds =
|
||||
options.promptAuthorIds ??
|
||||
(() => promptForAuthorIds(options.document));
|
||||
const submitBatch =
|
||||
options.submitBatch ??
|
||||
((payload: BatchPayload) =>
|
||||
@@ -266,6 +277,49 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
setToolbarBusyState(toolbar, false);
|
||||
}
|
||||
},
|
||||
onExportAudienceProfileByIds: async () => {
|
||||
const input = await promptAuthorIds();
|
||||
if (input === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseAuthorIds(input);
|
||||
if (parsed.ids.length === 0) {
|
||||
setToolbarExportStatus(toolbar, "请输入有效的达人星图ID");
|
||||
return;
|
||||
}
|
||||
|
||||
setToolbarBusyState(toolbar, true);
|
||||
try {
|
||||
setToolbarExportStatus(
|
||||
toolbar,
|
||||
`识别 ${parsed.ids.length + parsed.duplicates.length + parsed.invalidTokens.length} 个,去重后 ${parsed.ids.length} 个,非法 ${parsed.invalidTokens.length} 个`
|
||||
);
|
||||
|
||||
const rows: AudienceProfileExportRow[] = [];
|
||||
for (let index = 0; index < parsed.ids.length; index += 1) {
|
||||
const authorId = parsed.ids[index];
|
||||
setToolbarExportStatus(
|
||||
toolbar,
|
||||
`按ID画像导出中 ${index + 1}/${parsed.ids.length}...`
|
||||
);
|
||||
rows.push(await loadAudienceProfileRowById(authorId));
|
||||
}
|
||||
|
||||
options.onCsvReady?.(
|
||||
buildAudienceCsv(rows),
|
||||
buildAudienceProfileFilename(new Date(), "按ID导出")
|
||||
);
|
||||
setToolbarExportStatus(toolbar, "");
|
||||
} catch (error) {
|
||||
setToolbarExportStatus(
|
||||
toolbar,
|
||||
error instanceof Error ? error.message : "按ID导出失败,请稍后重试"
|
||||
);
|
||||
} finally {
|
||||
setToolbarBusyState(toolbar, false);
|
||||
}
|
||||
},
|
||||
onSubmitBatch: async () => {
|
||||
syncSelectionStateFromDom();
|
||||
const exportTarget = readToolbarExportTarget(toolbar);
|
||||
@@ -706,6 +760,107 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAudienceProfileRowById(
|
||||
authorId: string
|
||||
): Promise<AudienceProfileExportRow> {
|
||||
const baseRecord = await loadAuthorBaseInfoSafe(authorId);
|
||||
const recordForRequests = {
|
||||
...baseRecord,
|
||||
authorName: baseRecord.authorName || authorId
|
||||
};
|
||||
const [profiles, businessAbility] = await Promise.all([
|
||||
loadAudienceProfileSet(recordForRequests),
|
||||
loadBusinessAbilitySafe(recordForRequests)
|
||||
]);
|
||||
const failureReasons = collectAudienceProfileRowFailures(
|
||||
baseRecord,
|
||||
profiles,
|
||||
businessAbility
|
||||
);
|
||||
const rowStatus =
|
||||
failureReasons.length === 0
|
||||
? "成功"
|
||||
: hasAudienceProfileRowSuccess(baseRecord, profiles, businessAbility)
|
||||
? "部分成功"
|
||||
: "失败";
|
||||
const authorName = baseRecord.authorName || "";
|
||||
|
||||
return {
|
||||
businessAbility,
|
||||
profiles,
|
||||
record: {
|
||||
...recordForRequests,
|
||||
exportFields: {
|
||||
达人ID: authorId,
|
||||
达人名称: authorName,
|
||||
导出状态: rowStatus,
|
||||
失败原因: failureReasons.join("; ")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAuthorBaseInfoSafe(authorId: string): Promise<MarketRecord> {
|
||||
try {
|
||||
return await loadAuthorBaseInfo(authorId);
|
||||
} catch (error) {
|
||||
return {
|
||||
authorId,
|
||||
authorName: "",
|
||||
failureReason:
|
||||
error instanceof Error ? "request-failed" : "request-failed",
|
||||
status: "failed"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function collectAudienceProfileRowFailures(
|
||||
baseRecord: MarketRecord,
|
||||
profiles: AudienceProfileExportRow["profiles"],
|
||||
businessAbility: BusinessAbilityResult
|
||||
): string[] {
|
||||
const failures: string[] = [];
|
||||
if (baseRecord.status === "failed") {
|
||||
failures.push(`基础信息:${baseRecord.failureReason ?? "request-failed"}`);
|
||||
}
|
||||
|
||||
Object.entries(profiles).forEach(([kind, profile]) => {
|
||||
if (profile.status === "failed") {
|
||||
failures.push(`${readAudienceProfileKindLabel(kind as AudienceProfileKind)}:${profile.failureReason ?? "request-failed"}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (businessAbility.status === "failed") {
|
||||
failures.push(`商业能力:${businessAbility.failureReason ?? "request-failed"}`);
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
function hasAudienceProfileRowSuccess(
|
||||
baseRecord: MarketRecord,
|
||||
profiles: AudienceProfileExportRow["profiles"],
|
||||
businessAbility: BusinessAbilityResult
|
||||
): boolean {
|
||||
return (
|
||||
baseRecord.status === "success" ||
|
||||
businessAbility.status === "success" ||
|
||||
Object.values(profiles).some((profile) => profile.status === "success")
|
||||
);
|
||||
}
|
||||
|
||||
function readAudienceProfileKindLabel(kind: AudienceProfileKind): string {
|
||||
if (kind === "audience") {
|
||||
return "观众画像";
|
||||
}
|
||||
|
||||
if (kind === "fans") {
|
||||
return "粉丝画像";
|
||||
}
|
||||
|
||||
return "铁粉画像";
|
||||
}
|
||||
|
||||
async function prepareCurrentPageForExport(): Promise<void> {
|
||||
await runSyncCycle();
|
||||
await harvestCurrentPageForExport();
|
||||
@@ -1415,11 +1570,15 @@ function hasRuntimeMessageSender(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function buildAudienceProfileFilename(date = new Date()): string {
|
||||
function buildAudienceProfileFilename(
|
||||
date = new Date(),
|
||||
label?: string
|
||||
): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hour = String(date.getHours()).padStart(2, "0");
|
||||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||||
return `达人连接用户画像_${year}${month}${day}_${hour}${minute}.csv`;
|
||||
const labelPart = label ? `_${label}` : "";
|
||||
return `达人连接用户画像${labelPart}_${year}${month}${day}_${hour}${minute}.csv`;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import type {
|
||||
export interface PluginToolbarHandlers {
|
||||
onExport(): Promise<void> | void;
|
||||
onExportAudienceProfile(): Promise<void> | void;
|
||||
onExportAudienceProfileByIds(): Promise<void> | void;
|
||||
onSubmitBatch(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface PluginToolbarDom {
|
||||
audienceProfileByIdExportButton: HTMLButtonElement;
|
||||
audienceProfileExportButton: HTMLButtonElement;
|
||||
batchSubmitButton: HTMLButtonElement;
|
||||
exportButton: HTMLButtonElement;
|
||||
@@ -39,8 +41,16 @@ export function ensurePluginToolbar(
|
||||
"[data-plugin-toolbar='root']"
|
||||
) as HTMLElement | null;
|
||||
if (existingRoot) {
|
||||
ensureToolbarMounted(existingRoot, document);
|
||||
return readToolbarDom(existingRoot);
|
||||
if (
|
||||
existingRoot.querySelector(
|
||||
'[data-plugin-export-audience-profile-by-id="button"]'
|
||||
)
|
||||
) {
|
||||
ensureToolbarMounted(existingRoot, document);
|
||||
return readToolbarDom(existingRoot);
|
||||
}
|
||||
|
||||
existingRoot.remove();
|
||||
}
|
||||
|
||||
const root = document.createElement("section");
|
||||
@@ -74,6 +84,11 @@ export function ensurePluginToolbar(
|
||||
audienceProfileExportButton.dataset.pluginExportAudienceProfile = "button";
|
||||
audienceProfileExportButton.textContent = "导出画像CSV";
|
||||
|
||||
const audienceProfileByIdExportButton = document.createElement("button");
|
||||
audienceProfileByIdExportButton.type = "button";
|
||||
audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button";
|
||||
audienceProfileByIdExportButton.textContent = "按ID导出画像CSV";
|
||||
|
||||
const batchSubmitButton = document.createElement("button");
|
||||
batchSubmitButton.type = "button";
|
||||
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
||||
@@ -88,6 +103,7 @@ export function ensurePluginToolbar(
|
||||
exportCustomPagesInput,
|
||||
exportButton,
|
||||
audienceProfileExportButton,
|
||||
audienceProfileByIdExportButton,
|
||||
batchSubmitButton,
|
||||
exportStatusText
|
||||
);
|
||||
@@ -95,6 +111,7 @@ export function ensurePluginToolbar(
|
||||
document.body.appendChild(root);
|
||||
applyNativeControlStyles(document, {
|
||||
audienceProfileExportButton,
|
||||
audienceProfileByIdExportButton,
|
||||
batchSubmitButton,
|
||||
exportButton,
|
||||
exportCustomPagesInput,
|
||||
@@ -108,12 +125,16 @@ export function ensurePluginToolbar(
|
||||
audienceProfileExportButton.addEventListener("click", () => {
|
||||
void handlers.onExportAudienceProfile();
|
||||
});
|
||||
audienceProfileByIdExportButton.addEventListener("click", () => {
|
||||
void handlers.onExportAudienceProfileByIds();
|
||||
});
|
||||
batchSubmitButton.addEventListener("click", () => {
|
||||
void handlers.onSubmitBatch();
|
||||
});
|
||||
exportRangeSelect.addEventListener("change", () => {
|
||||
syncCustomPagesInputVisibility({
|
||||
batchSubmitButton,
|
||||
audienceProfileByIdExportButton,
|
||||
audienceProfileExportButton,
|
||||
exportButton,
|
||||
exportCustomPagesInput,
|
||||
@@ -125,6 +146,7 @@ export function ensurePluginToolbar(
|
||||
|
||||
const toolbarDom = {
|
||||
audienceProfileExportButton,
|
||||
audienceProfileByIdExportButton,
|
||||
batchSubmitButton,
|
||||
exportButton,
|
||||
exportCustomPagesInput,
|
||||
@@ -150,6 +172,9 @@ function appendOption(
|
||||
|
||||
function readToolbarDom(root: HTMLElement): PluginToolbarDom {
|
||||
const toolbarDom = {
|
||||
audienceProfileByIdExportButton: root.querySelector(
|
||||
'[data-plugin-export-audience-profile-by-id="button"]'
|
||||
) as HTMLButtonElement,
|
||||
audienceProfileExportButton: root.querySelector(
|
||||
'[data-plugin-export-audience-profile="button"]'
|
||||
) as HTMLButtonElement,
|
||||
@@ -235,6 +260,7 @@ export function setToolbarBusyState(
|
||||
): void {
|
||||
[
|
||||
toolbar.batchSubmitButton,
|
||||
toolbar.audienceProfileByIdExportButton,
|
||||
toolbar.audienceProfileExportButton,
|
||||
toolbar.exportButton,
|
||||
toolbar.exportRangeSelect,
|
||||
@@ -433,6 +459,7 @@ function applyNativeControlStyles(
|
||||
document: Document,
|
||||
controls: {
|
||||
audienceProfileExportButton: HTMLButtonElement;
|
||||
audienceProfileByIdExportButton: HTMLButtonElement;
|
||||
batchSubmitButton: HTMLButtonElement;
|
||||
exportButton: HTMLButtonElement;
|
||||
exportCustomPagesInput: HTMLInputElement;
|
||||
@@ -450,12 +477,14 @@ function applyNativeControlStyles(
|
||||
if (nativeButton) {
|
||||
controls.exportButton.className = nativeButton.className;
|
||||
controls.audienceProfileExportButton.className = nativeButton.className;
|
||||
controls.audienceProfileByIdExportButton.className = nativeButton.className;
|
||||
controls.batchSubmitButton.className = nativeButton.className;
|
||||
}
|
||||
|
||||
[
|
||||
controls.exportButton,
|
||||
controls.audienceProfileExportButton,
|
||||
controls.audienceProfileByIdExportButton,
|
||||
controls.batchSubmitButton
|
||||
].forEach((button) => {
|
||||
applyPrimaryButtonStyles(button);
|
||||
@@ -509,6 +538,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
style.textContent = `
|
||||
[data-plugin-export="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-batch-submit="button"]:hover:not(:disabled) {
|
||||
background-color: #6d1627 !important;
|
||||
border-color: #6d1627 !important;
|
||||
@@ -516,6 +546,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
|
||||
[data-plugin-export="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-batch-submit="button"]:active:not(:disabled) {
|
||||
background-color: #58111f !important;
|
||||
border-color: #58111f !important;
|
||||
@@ -524,6 +555,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
|
||||
[data-plugin-export="button"]:focus-visible,
|
||||
[data-plugin-export-audience-profile="button"]:focus-visible,
|
||||
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
|
||||
[data-plugin-batch-submit="button"]:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
||||
@@ -531,6 +563,7 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
|
||||
[data-plugin-export="button"]:disabled,
|
||||
[data-plugin-export-audience-profile="button"]:disabled,
|
||||
[data-plugin-export-audience-profile-by-id="button"]:disabled,
|
||||
[data-plugin-batch-submit="button"]:disabled {
|
||||
background-color: #c89ca4 !important;
|
||||
border-color: #c89ca4 !important;
|
||||
|
||||
Reference in New Issue
Block a user