From 50933af0a63e4e2b9e81ca44b2d00a8bf684ce6c Mon Sep 17 00:00:00 2001 From: admin123 Date: Tue, 19 May 2026 16:26:04 +0800 Subject: [PATCH] feat: allow selecting audience export fields --- dist-release/content/index.js | 469 ++++++++++++++++-- docs/aigc-user-guide.md | 60 ++- docs/【超简单版】插件安装使用指南.md | 8 + .../star-chart-search-enhancer-internal.zip | Bin 96438 -> 98757 bytes src/content/market/audience-profile-csv.ts | 95 +++- .../market/audience-profile-field-dialog.ts | 295 +++++++++++ src/content/market/csv-exporter.ts | 8 + src/content/market/index.ts | 93 +++- src/content/market/plugin-toolbar.ts | 25 + tests/audience-profile-csv.test.ts | 127 ++++- tests/market-content-entry.test.ts | 203 ++++++-- 11 files changed, 1273 insertions(+), 110 deletions(-) create mode 100644 src/content/market/audience-profile-field-dialog.ts diff --git a/dist-release/content/index.js b/dist-release/content/index.js index 194a655..ee2c3ad 100644 --- a/dist-release/content/index.js +++ b/dist-release/content/index.js @@ -141,6 +141,12 @@ 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) { const csvColumns = buildMarketCsvColumns(records); const headerLine = csvColumns.map((column) => column.header).join(","); @@ -227,27 +233,73 @@ { key: "expectedPlay", label: "\u9884\u671F\u64AD\u653E\u91CF" }, { key: "hotRate", label: "\u7206\u6587\u7387" } ]; - function buildAudienceProfileCsv(rows) { + function buildAudienceProfileCsv(rows, options = {}) { const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record)); - const csvColumns = [ + const csvColumns = filterAudienceProfileCsvColumns([ ...marketColumns.map(toMarketColumn), ...buildBusinessAbilityColumns(), ...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout)) - ]; + ], options.selectedHeaders); const headerLine = csvColumns.map((column) => column.header).join(","); const rowLines = rows.map( (row) => csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",") ); 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() { + return [...buildBusinessVideoColumns(), ...buildBusinessEstimateColumns()]; + } + function buildBusinessVideoColumns() { return [ ...BUSINESS_VIDEO_LAYOUTS.flatMap( (videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({ header: `${BUSINESS_VIDEO_SECTION_LABEL}-${videoLayout.label}-${metricLayout.label}`, readValue: (row) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key) })) - ), + ) + ]; + } + function buildBusinessEstimateColumns() { + return [ ...BUSINESS_ESTIMATE_LAYOUTS.flatMap( (durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({ header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`, @@ -813,24 +865,89 @@ return typeof value === "object" && value !== null; } - // src/content/market/author-id-dialog.ts - function promptForAuthorIds(document2) { + // src/content/market/audience-profile-field-dialog.ts + function promptForAudienceProfileFields(document2, groups, selectedHeaders) { 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"); - overlay.dataset.authorIdDialog = "overlay"; + overlay.dataset.audienceProfileFieldDialog = "overlay"; applyOverlayStyles(overlay); const dialog = document2.createElement("section"); applyDialogStyles(dialog); const title = document2.createElement("h2"); - title.textContent = "\u6309\u661F\u56FEID\u5BFC\u51FA\u753B\u50CFCSV"; 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"); - 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); + 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"); applyActionsStyles(actions); const cancelButton = document2.createElement("button"); @@ -839,24 +956,60 @@ applySecondaryButtonStyles(cancelButton); const confirmButton = document2.createElement("button"); confirmButton.type = "button"; - confirmButton.textContent = "\u5F00\u59CB\u5BFC\u51FA"; + confirmButton.dataset.audienceProfileFieldDialogSave = "button"; + confirmButton.textContent = "\u4FDD\u5B58"; applyPrimaryButtonStyles(confirmButton); actions.append(cancelButton, confirmButton); - dialog.append(title, hint, textarea, actions); + dialog.append(title, hint, toolbar, groupContainer, actions); overlay.append(dialog); 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(); 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", () => close(textarea.value)); + 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); } }); - 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) { @@ -869,8 +1022,11 @@ overlay.style.background = "rgba(15, 23, 42, 0.38)"; } function applyDialogStyles(dialog) { - dialog.style.width = "520px"; + 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)"; @@ -889,18 +1045,43 @@ 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 applyToolbarStyles(toolbar) { + toolbar.style.display = "flex"; + toolbar.style.gap = "8px"; + toolbar.style.marginBottom = "12px"; + } + function applyGroupContainerStyles(container) { + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = "10px"; + container.style.overflow = "auto"; + container.style.paddingRight = "4px"; + } + 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) { actions.style.display = "flex"; @@ -927,6 +1108,120 @@ 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 var DIALOG_STYLE_ID = "sces-batch-name-dialog-style"; var activeDialogs = /* @__PURE__ */ new WeakMap(); @@ -943,13 +1238,13 @@ dialogRoot.setAttribute("role", "dialog"); dialogRoot.setAttribute("aria-modal", "true"); dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title"); - applyOverlayStyles2(dialogRoot); + applyOverlayStyles3(dialogRoot); const dialogPanel = document2.createElement("div"); applyPanelStyles(dialogPanel); const title = document2.createElement("h2"); title.id = "sces-batch-name-title"; title.textContent = "\u63D0\u4EA4\u6279\u6B21"; - applyTitleStyles2(title); + applyTitleStyles3(title); const description = document2.createElement("p"); description.textContent = "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0\uFF0C\u4FBF\u4E8E\u540E\u7EED\u5728\u7CFB\u7EDF\u4E2D\u8BC6\u522B\u548C\u8FFD\u8E2A\u3002"; applyDescriptionStyles(description); @@ -968,12 +1263,12 @@ cancelButton.type = "button"; cancelButton.dataset.pluginBatchNameCancel = "button"; cancelButton.textContent = "\u53D6\u6D88"; - applySecondaryButtonStyles2(cancelButton); + applySecondaryButtonStyles3(cancelButton); const confirmButton = document2.createElement("button"); confirmButton.type = "button"; confirmButton.dataset.pluginBatchNameConfirm = "button"; confirmButton.textContent = "\u786E\u8BA4\u63D0\u4EA4"; - applyPrimaryButtonStyles2(confirmButton); + applyPrimaryButtonStyles3(confirmButton); buttonRow.append(cancelButton, confirmButton); dialogPanel.append(title, description, input, errorText, buttonRow); dialogRoot.appendChild(dialogPanel); @@ -1055,7 +1350,7 @@ `; document2.head.appendChild(style); } - function applyOverlayStyles2(root) { + function applyOverlayStyles3(root) { root.style.position = "fixed"; root.style.inset = "0"; root.style.background = "rgba(15, 23, 42, 0.38)"; @@ -1074,7 +1369,7 @@ panel.style.padding = "24px"; panel.style.boxSizing = "border-box"; } - function applyTitleStyles2(title) { + function applyTitleStyles3(title) { title.style.margin = "0"; title.style.color = "#4c0519"; title.style.fontSize = "20px"; @@ -1113,7 +1408,7 @@ buttonRow.style.gap = "10px"; buttonRow.style.marginTop = "18px"; } - function applySecondaryButtonStyles2(button) { + function applySecondaryButtonStyles3(button) { button.style.height = "36px"; button.style.padding = "0 16px"; button.style.border = "1px solid #d7dde6"; @@ -1123,7 +1418,7 @@ button.style.fontWeight = "600"; button.style.cursor = "pointer"; } - function applyPrimaryButtonStyles2(button) { + function applyPrimaryButtonStyles3(button) { button.style.height = "36px"; button.style.padding = "0 16px"; button.style.border = "1px solid #7f1d2d"; @@ -3291,6 +3586,10 @@ audienceProfileByIdExportButton.type = "button"; audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button"; 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"); batchSubmitButton.type = "button"; batchSubmitButton.dataset.pluginBatchSubmit = "button"; @@ -3304,6 +3603,7 @@ exportButton, audienceProfileExportButton, audienceProfileByIdExportButton, + audienceProfileFieldButton, batchSubmitButton, exportStatusText ); @@ -3311,6 +3611,7 @@ applyNativeControlStyles(document2, { audienceProfileExportButton, audienceProfileByIdExportButton, + audienceProfileFieldButton, batchSubmitButton, exportButton, exportCustomPagesInput, @@ -3326,12 +3627,16 @@ audienceProfileByIdExportButton.addEventListener("click", () => { void handlers.onExportAudienceProfileByIds(); }); + audienceProfileFieldButton.addEventListener("click", () => { + void handlers.onConfigureAudienceProfileFields(); + }); batchSubmitButton.addEventListener("click", () => { void handlers.onSubmitBatch(); }); exportRangeSelect.addEventListener("change", () => { syncCustomPagesInputVisibility({ batchSubmitButton, + audienceProfileFieldButton, audienceProfileByIdExportButton, audienceProfileExportButton, exportButton, @@ -3344,6 +3649,7 @@ const toolbarDom = { audienceProfileExportButton, audienceProfileByIdExportButton, + audienceProfileFieldButton, batchSubmitButton, exportButton, exportCustomPagesInput, @@ -3368,6 +3674,9 @@ audienceProfileExportButton: root.querySelector( '[data-plugin-export-audience-profile="button"]' ), + audienceProfileFieldButton: root.querySelector( + '[data-plugin-audience-profile-fields="button"]' + ), batchSubmitButton: root.querySelector( '[data-plugin-batch-submit="button"]' ), @@ -3437,6 +3746,7 @@ function setToolbarBusyState(toolbar, isBusy) { [ toolbar.batchSubmitButton, + toolbar.audienceProfileFieldButton, toolbar.audienceProfileByIdExportButton, toolbar.audienceProfileExportButton, toolbar.exportButton, @@ -3565,15 +3875,17 @@ controls.exportButton.className = nativeButton.className; controls.audienceProfileExportButton.className = nativeButton.className; controls.audienceProfileByIdExportButton.className = nativeButton.className; + controls.audienceProfileFieldButton.className = nativeButton.className; controls.batchSubmitButton.className = nativeButton.className; } [ controls.exportButton, controls.audienceProfileExportButton, controls.audienceProfileByIdExportButton, + controls.audienceProfileFieldButton, controls.batchSubmitButton ].forEach((button) => { - applyPrimaryButtonStyles3(button); + applyPrimaryButtonStyles4(button); button.style.whiteSpace = "nowrap"; }); [controls.exportRangeSelect, controls.exportCustomPagesInput].forEach((element) => { @@ -3588,7 +3900,7 @@ controls.exportRangeSelect.style.minWidth = "104px"; controls.exportCustomPagesInput.style.width = "72px"; } - function applyPrimaryButtonStyles3(button) { + function applyPrimaryButtonStyles4(button) { button.style.backgroundColor = "#7f1d2d"; button.style.border = "1px solid #7f1d2d"; button.style.borderRadius = "8px"; @@ -3616,6 +3928,7 @@ [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-audience-profile-fields="button"]:hover:not(:disabled), [data-plugin-batch-submit="button"]:hover:not(:disabled) { background-color: #6d1627 !important; border-color: #6d1627 !important; @@ -3624,6 +3937,7 @@ [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-audience-profile-fields="button"]:active:not(:disabled), [data-plugin-batch-submit="button"]:active:not(:disabled) { background-color: #58111f !important; border-color: #58111f !important; @@ -3633,6 +3947,7 @@ [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-audience-profile-fields="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; @@ -3641,6 +3956,7 @@ [data-plugin-export="button"]:disabled, [data-plugin-export-audience-profile="button"]:disabled, [data-plugin-export-audience-profile-by-id="button"]:disabled, + [data-plugin-audience-profile-fields="button"]:disabled, [data-plugin-batch-submit="button"]:disabled { background-color: #c89ca4 !important; border-color: #c89ca4 !important; @@ -4235,6 +4551,7 @@ } // src/content/market/index.ts + var AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY = "sces:audience-profile:selectedHeaders"; function createMarketController(options) { const marketApiClient = createMarketApiClient(); const audienceProfileClient = createAudienceProfileClient(); @@ -4379,7 +4696,12 @@ setToolbarExportStatus(toolbar, "\u753B\u50CF\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"); return; } - options.onCsvReady?.(buildAudienceCsv(rows), buildAudienceProfileFilename()); + options.onCsvReady?.( + buildAudienceCsv(rows, { + selectedHeaders: readAudienceProfileSelectedHeaders() + }), + buildAudienceProfileFilename() + ); setToolbarExportStatus(toolbar, ""); } catch (error) { setToolbarExportStatus( @@ -4422,7 +4744,9 @@ ); } options.onCsvReady?.( - buildAudienceCsv(rows), + buildAudienceCsv(rows, { + selectedHeaders: readAudienceProfileSelectedHeaders() + }), buildAudienceProfileFilename(/* @__PURE__ */ new Date(), "\u6309ID\u5BFC\u51FA") ); setToolbarExportStatus(toolbar, ""); @@ -4435,6 +4759,23 @@ 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 () => { syncSelectionStateFromDom(); const exportTarget = readToolbarExportTarget(toolbar); @@ -4880,6 +5221,46 @@ } 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() { await runSyncCycle(); await harvestCurrentPageForExport(); diff --git a/docs/aigc-user-guide.md b/docs/aigc-user-guide.md index 4aabe8d..2d24c25 100644 --- a/docs/aigc-user-guide.md +++ b/docs/aigc-user-guide.md @@ -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. 选择范围 @@ -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. 看不到插件按钮 diff --git a/docs/【超简单版】插件安装使用指南.md b/docs/【超简单版】插件安装使用指南.md index e94a4e1..76b0ed7 100644 --- a/docs/【超简单版】插件安装使用指南.md +++ b/docs/【超简单版】插件安装使用指南.md @@ -97,6 +97,14 @@ https://xingtu.cn/ad/creator/market - 效果预估:不同视频时长的预期CPM、预期CPE、预期播放量、爆文率 - 秒思api数据:看后搜率、看后搜数、新增A3数、新增A3率、CPA3、cp_search +**只导出部分字段**: +- 点击 **"画像字段"** +- 勾选你需要的字段,取消不需要的字段 +- 点击 **"保存"** +- 再点击 **"导出画像CSV"** 或 **"按ID导出画像CSV"** + +说明:达人ID、达人名称、导出状态、失败原因等基础字段会固定保留;你保存过一次后,下次导出会自动沿用这次勾选结果,不需要重新勾选。 + ### 3️⃣ 按ID导出画像数据 当你想批量查询特定达人ID的画像数据时使用: diff --git a/release/star-chart-search-enhancer-internal.zip b/release/star-chart-search-enhancer-internal.zip index ede2c1f8180c822f135396d3207fb6aa95b98bc6..dab936d4b05345966bde03e460e248eaa797389c 100644 GIT binary patch delta 42434 zcmV)0K+eCm@dd?$1`JS30|W{H00000?1GUDCj#t(u{FVE0_=j5_+=sj?1GaVX7mE= zg0m8590>yKf|D?CB!BFJvs?z3{eg0#005&%0{{>J003ieZggdCbT4UcWMz0RYIE#8 z+io1mb?^F$)$=`4@8P+EvxnJwuUNU>8}ya(_=%olBiMb?Tf`7Zw%~ zvZ6S2CvoB7onoAy%{}K;@xt*Is6=pC)+F$w*a9-M8Jd2IYph2WX<_5SV(FpOEOh)c^w^0f z=g>%ov1hojNPn6FhhG#AYnAom2gS{eBb$DfHcLm*{SP+bp9=ghv%$S8LBhzor&ci` z%6aDO9F{FFxJ2Pru<)m4YcZd*?_*#u0ei@RLKH7T-+F%UbpmzufmN~Yy-q-{K6p;s zU)_;ll)i=t#5h64>QVHddRDdy1;Xu>QShVCAtg%&z<*!N5DYaRfG3OD2?|-mL=<;v zx$Q!1A; zg+Wx*u-I`ha$llpv0W-L5i0VSJ7GkWa7T#VB!)>agO<*TN7`Z~3;IftA`-@*3bbjn z?pT|fLVp4ZFSI>rOn8rq9y*Q5XW0tfvvc~ba)z5Ete7|xNYp8V@#ORaPSQ}J`ih^Y*Eu%){tduO7uz$jq2>YC4)*mW5OF88hTSLT*cJ+&o zvJ5gn}YGMeq3TLuhK$LmL79Q@>ib^@>ZS| zmz~_Mo;qHX)7J!Fen-(uSTL52*b1YS;JF?C6MR|xYf7w6iPQq3%m4;@p{=j>X6?bf zs(-Y?rre)NbfLLCD4IBP)Q>Lu$n(-OuXTM4SWx=^abz}on7fph2N*qS|X*-Mf zJP5a^l<)F#%twpXT5WK&=&X-w*P`*nnV}_ATAgMM{%tmUD`NJ7i9^N~R`fz47ki^# zY1g8oMZ3{!;hu~-E7F*QuA@f%AOLWg4*Yxkkd4${?_i|?YBgIxyu03SA1&6q>wk3+ zW2d%7L<-_K5lIFlF1pFT!7!UGvNnCnknbbl##eW|*y!c@D}S#=68VX6;qd;CsZwOd`}4^U-Cm`Hmyi-|aQuk?dsdG~a>ZWIm3_FupTcss$$NC{|z3 zjbbbjSbu*^7>SD78ZR4B@Mq@5;w11EGoO&ApEm77I;zIlw`8`m$(X12b$@qX4A12)@TefX;&W;(>k#9(rwC)K%|We=#2KKHD3P z$JYI-1!l$CN3oiELoX-ZVk)D|0y`wD^$<*3x7+T@t99X62!<-jX%G$_2qF+=)oIe1Pv#dfRfmX zstI4sO3P!iX|w5fEZgW(Dt6Ms5@#`Wkv~C|c^I6!9;%3-3j0aBdrwE(JHzo#@AIe6 z4kQQ3a(yy-pMW*S3x5s?^{p4KKZV7I<1Cad&pAOJ4KLRFupX_A>K#~{T21Vy)cT`> zSiy2Jo;rS{VC(d|>sZt4gVnIn_?Dyd0vJ$thDJKt^|gMTBUjMM&f_OwDu#O?lda(n z5lMkG?%>t70qz8e0@gcC_FKU9J{ihD^=_rn#H|}^m1YfpG=J-rHS)1mue8WVtM(v) z?{6P`K0er1NR!@@IbQ>}rZe0c;Sj42{$FcYYp}(h)OY*&^|kf9`c`|el@GtZW~i6N z*57;j*}%Yhr;p>#Zhf?d#n$c8e-qT}wb6j0!{ak*;}C05PwJ^hx8AB_0kk^Sot_?K!pAs!=Q>!Y;qhA|gqH7(Fv|FpyF`CYrxz&27 z)nN5XB=T&3`^j*BfBfn8VEEK9(ZB|SYAQjFqO#`LOqvN4!Apn$<3$?0-LB&yYmbH+ zcn*_nnatvKhNiCL??RxaQd;W`@MtxG(t5o=5~DF%8-K8{Kxg{Y_1%bsLkgzW7FK48 zk9hDpoC_vS+IrFCwT{Q3(d%Xa&le})YM!I%c8q4KdBBQCRHcd8ZuN1j)lbXXxd7^C zXs`%L=0Zg=#D<~O!79;iVQpS-bw*hzcH9>z6C8rQb*$d4VHS8ln9ab}XJY7X_1Eyf zTfHm{`+qpomJL5@_t|vJRha!@|6u#+6FT`pUoe__w}nR%cL9$*{M&2~3n>J{{e$ht z*k3c?Xt&m25Zj&gKAE#?cu>HSX5+}x*uX~z!NDbnN>R<#0>)%5vnl&|R8k~s;U0)IGkXxg98)CG@-t=*}Q`n!))OeRn$ z6DqBI94ci260o#2NMSh-;?&&jG-_=O)a{75n+igSkKSV*$S!qiNca|9g7+WHaU<+O zga4{{?bIJy8q5I}>bt=gb;W{9n}lsYJZC25~oC{?Gi;Xv68WtT$hhw1Tg^?@(ozY zSdw&B40Cd(&BtOEyXHB1U0x}eO@4_;m`(JQMlUm86P{er4THSvolz&9v#dvp=Wju0co6umztf-u*=**@~WznYc5T;0y`dtIFB(tFo2hM+_xG=O!On=hq z7A7Mny1G>o%LDJ<8LlARBVUZUD1*7&MM<-G`WmL1f^uq&OHI+N9GwxL+1%p%pD_s2 zv6F=T^LwxJeCDglz1I?LI&E>GjGdJ}FH889{1Nw9 zpiB{vSPsjUOa%O}vZWA!l9E8q&3^^xK!(}KAG+nEq5JKI0hzKG%QPk#zVW*bi#Za( zgP<~TdxOJ8CXJ6q>c&H~R2P1dP=HS@AFobw>ZRFQX_8gFtG5Rus1?~!?8J+R93U%1 zi^&A43aOQ%P%z|Ibxef&=#Y2fc+)-<-C!xwTxsM8|9dMF#QGE+N?ln=GJk3eB33+Q z3hSGY%2H-bXmc0d;F&F@lf6&4l7tb%if z!|o(qoeDXZ%fKuFWB&^iP=Cz>SLwf=>gL^EnUTEiO+dY-&_OvNZ3_M*hcvBwnUv=t zI(1*tt%kBi3^d^)()}?~$lZSUHf;lu6^LTQ3kqFBaK^2TgPg6w+Vi}5h0A0XlgY(4 zd!j?k<+02rn{cJHUuVX~FexY`ub)-lz=mP6Y@*B5Npe=94t^_iCVz3t9e?Uh$r&JQ zV)!qr5@atgiG50&HaWsaj9={o^TMzi6Y3yq-G3x^-`eEWhcB4nJv_EHn4!AXmYoN` z<7C&4J1AS9j|bcPyWsbTeBfdAfBxp{w?F?iK3B=C&T*O5y#24={OJ$>P-O-{6o326 zzy0HX|NhO}pZ(k0Uw{0b{=jX3_=Sr%`4afA{^jj&|L*P2|MBf_|NhO}U;fA2ufDoa-a7r)cJQXhYkcqj*J)w2^m*Ej)bh?&%(2z3J9nf`3Hbw4wi?CO9IxkFanjb~v%&W8Nq;!zS%-t(-X}wnC#JIDI<;ZFhDgzWaqwym-z|vf zgGGpo#ip*T{~~FN_h&IEJqAjF>k=@oHv-B#u{wfQJ_0^G$<+>+r7_a86l87`v-7_A zaLBU|EP}{x@_!PTkf3^jcY(HOiFzpG6vdPC?b%!gP09dxicXz{7mtv*hQ#h$L4RR6 zdK^iBNf`ikQBzrnxS4dKSN;S<=IHrtb~K=D#ZGvJV)^(4cZ2*m3_K5oEY;CF2|{q@ zcxh2OGodiKJq3UW#Rv2!yNQ;dvmJ~Ghw7pS5~?_SrGK2{U|C%h&EW&*hn-8ul_V+q zP?j;1k4Hf})?DU8D7s%((7aZDSy)Y`pc1a*MP#dx_<)77)(Sl0^8lX}fN7GdjNCKd z@it^R&?2SXmGnyz3cqZ_q+kaxN*a%;llt_(Q^)ldA=*Pu6!^edi2iB;rm=#{(KO8@ zS#!wTA%FEMZ!$kJql`AVC7R^-7{5h2W`g}MffqJWYi?s_+zkwztFQ3x92BQ)@|OV>Z_yrRv#b)tS3haE6?5 zG_O*+W8Qu|RRA{906=NXfUs3o0l1)9A7rl@IJfB;)LkcZW_TAVE*1ig%f&7^7eX$` z+;ig7Ae@b9fAOkUFG#1iR+UDgnoHW_oUKl#B zY&RnR&#P=%Bck*Ln`Aoa7VX5DR=8o2=4{4%HQb+K9m_aN-r>wqq@$NjeiY8V&g#`vlc?j_>Ki>@`RW^?v?D*!lDks zc3TpN^hj}%a>vJbhhT-C@SsJaa4g?eO9x2FKxUCG>rKQ>q9oTL&2ag6Dr2W4pVF*f zL+560gm1_2VCtG`r^b1yNtPp&(77ZZWx30$tta=U=`kd7r=0@Olz|JMIf_ZD(|=9QzOcDG=;2p z@b4nYN64a01{Nf_Lt$_uQB1he2Dzl~^$EFe1p&AMJ}r>fIp#kHv4dA?X$);}JQouL zc4aXuTLKaMvL2A1Gzn3*YVu-6@PB+{I(f`9nyMyB^djtbszsD7<<)@h*KWpqO;gs#5u6C!4O%q_kKm){-p$XDFR*A_ua! z{V94`6w!T|-1xS+f|RUEMl-@d(oP--BL^bKk;jIXCCFzc)i{SGo=PJv=zj=QD01oM zsw91h4EZcuhUXewT#rLo+`=Zmo}QYXXkZON43#=~ptL=+)*G}o+i=p!%<{OajyKC} zrDUxph7Jimnjn9h(<>0g7TCtSA741xyi3KlO2GY3Z%PyK$ zmie#%D^6z#X#%x)ICo+&d4K*fne6Nby)w;7ls4_d`f=tX8|u6_=ni{81hnU|6QA2* z@+1g;f&?{7U?J+Mhe*T&DBFIF&cG-$G$ou$rJv#{k{Z7HsTbCb&yz@Np6OmXcFQ0N zP1dSHL>dz6 zD6)bBc4Z?Wl~%>g$ba3pTg<37Zje%K+92p+9L>c-MCmiJc`PH z{@p)6{PXXAar~gla$zw>Puw^1#e*lFEo8|->4k&@Kz~WtV!=GP#~vhOZAiLa7oyxk z_8ye((j$PhQL+M?)A;;#26F2ljW?0-b&wK-_$3ClISgSM%ayvWaMB6kasn+r<=)C> zCR0uM5!i}D&%mY*OVQKz2cPc_lhZKDZEJjzg_KHHs5h*}9rN)M{o=zd220Hw1WV0Z z4wkfTihs8$^jJxeq=->4hQGufvgAmbu8k0={39RI@To6>C8yw1;g~d#@0%rwR1QPp zT=uou;iuH|I5H9@)<+Q-#8SlcNG$Ur!CxxgTIE28N=2!}$|loGRVI{;o(KJJ#q%7R z?NXQ)bT+UDn;~T3$|hWKXH{k><*~_?^jgA(CVw%_;G|g~70Cv@B2g0$nzdSnP#y!0 z&KMJ4sTQC8)Vvv?yo=D&^NFh`Pi`%Bq9#t3rcD-j4l5z<(ws+TzGHc=<4T{CnW+t~ zwM)-|S-}hT;7z{zlXmo(9X940<}`*h>X}&#!<_n#&vl7j8%C9xDxLC(#wrv zNq;b`1!TIDBRZ?3FAPVlqB#zmeh3*1^$b%Ym|UP!FSz8Vp9-~n0)g+n8aol5V5_A( z$g9lc829yZ9LpBsYvrz(z9lKFv$GJLIkB3?BFP^Z8dV<{?Cl)3TRFdzzci`29Tu*E zUPG6mKfAPg00p9{+0|5Du;E7uK6CtHvVVWpC}PB$HqDHItS-(Vv=3Nk^FdtjB zB!&5r)xwVBM=k6#s&Z3~*cmt!>%*<^_Wa&!19@_|rG}ZfiqgkXjK=so;KqD7CT)YR z&C1cDGiu^LDzM(HUxTgtLP(ixVS{=z$zT9-1j#uO>iF>^tB%%82e>k%hkw?H^e#=O zxvr^wYP+d>=EhNW=SXs9#bZHgzgY?q+kxayez8+hjH*`d269wAjX{?dpqRN3gA~~?|6hCWw%taOBnZCOSA7=>t=p4^Gig4q%yO5 zI{UPW2zNI(H#avoH#aw9*M{MkXmE2NJFvkWBVoI|r$zB#Xhz3#aMstHG=RB>kq)n7 zqq^&Zp}7{sD@ZpAT7RDXJ#}NPCjs!G;PXB-5*FUE?HS1}4ifT|W+tmC@AA6C6n@rg zgY3pSiK+UyM00$G=TH0rvI<10Rkdrnb#F2m-oB9^k7u_-K^|4bnU{eE5yeXRr(4Vr zw(MGgmOwm{`V61QMFC&69hfK3LV?8A4#cYreYv9mCl(%5ZGXUL3as_j7KD~uYeR*- zLja5671rAj$A2CW;4}N97@i^&jMSz4csxAsO)ImSB))8<$uEBe7xC_JxHq58#-pMR z&asB=!)c1I5MfeOy=XOW3RfWC1dyIn;<{y z2i8`h#{oLM;pX-N_Fd{OlbG#9i&?v!xR+^IqF$yfB(M_Jw1Py%(ZOVCP!^IMy#hza zu0DAe?^q}|iz3AFNe)HZcBXb3($|k}5)#rClDViS)PFnzy^tPOilzHkY5(K-_;W4T z#YvfT&wnR9e+sabvm&I{K%PdK+H#1Rvn`4Z0?Z=-^4ZHQFC0H(Xhr8k-B3~401DIi zknuB9eY~tImXkS_GDg$_48c%^LWV1OkVpH<7Ul-+npVIHx{7|9D@W^>moGN_v!oX+ zO6BEzD4eaAk;iO>g4Gt`(ga&L9O{qF(pz(!zI6_;38cyGF3!8N*^}qqDZTl zcaEaKe!uhB6Anf{P%%)CFrlKV%yReKV{4;+D{n~wgUyD13r}$*a>ULV@8@}-r$RvR zbW`AQosHyu1p}uHvh3S;so`-5wPvF_8uaewYy zE>S{H(`)f3l|{-p=2(WUCptf`2IT25>wgT0(~Ct#wcR(s>i(5)uR%q|2|Krw=$>1cq$sy|MDL8 zjpagf`Z7w|=jK3v`j@5R09=97v<@r95UAHWd4D_s&j1hX5@ZC&dAgQ_Q#V~FY%+=tMToRFHU`Hcu|b8$J;Eboqrj8d;{(8 zaa4%G4RC0IQC$&DueTgZo0D;#DPvU)Z?={8!E1?$!rPEqD_H#Z7b*Zwt-Z3gxwf^s zv9=kkXoJ4hE{5snV4dDDyBz6AUo{9?l1_sa|2EHO*~Kl6u z%6cPduQrmkb|YyuSGSgGVSj3dt`YchmtB0y2Ed?6ZDXr7(PLvZu6v(<(UtnWVZXl8 zYTbNFmXp=?BpM3c3xNRu#)SrzzxkWG!5vL@rZhpKYA{rLj_3s%Hn zMn}CtHphwE3X|Y`{Q3B*HyD3HZc0`}wPbBw{1cdxb~`x0iP$N!5r1AOwIhUvUb4Td zN)oeWZME?y*uT?J(<>E3JrmQ*Y$UO6C3bYQ&V^vbW4yCVYAf+7znB@_Y_$S)#B7YG zNbZ%3_G)`Og0_3XqL9XGr4Y_5ZG#c1!*E^=)ffuB-!dT~IjLh&-T(n`9;0;d0!LF@ z-4YbBF|Tvkn{Za+#eZ`6dt;#*UE}aH0er-QcLjZOO1=~725#ljUwk8F_qIbBaT({qJO zsD4N^@Oorno8d>WWp@oM)q;#gRy04fM!{x7t9gt@3u8-~)_(~uXIS7r*?I7ZDoBWGW@V9PK{ohZi>dw>-fqK zR9%LsD52_7*rmrKe<`q6hJveh2VynI!WY}rdbmTF)D4BCTu^^nlv)wq5Whi9jm=|HRwmjoM168HoR8#sR_5)N8}%|ZHjB#e_MA_Oau z@K5EzH_DvSbYKl*lDwb4*~6WrBqEDAMzz19XH@$UTz{iNmQ?T}Sp^&F#s*}$Uf($E zzCa82AtXv)K`$rjZN!I9bnPuucv>C;=^ctdv_tki)77|!= z_r>tGw$}D_!WwDs<-?ZFS8LsMNcg?pK1AZIfFT*Yr`A^wr4N<(vbnnnt+!TzOqk_+ zdt=*iy?;4T*l0ys{d{a+w9Wl(@ccPg1vjs~T}X1=#+*Q_2QbXTZOFE>0jYhr4)?k+ zyuHZ4H@7yr(uYX+az)5ZB-)1>3c0y~1 zywDzgsn-7IzEx}gQLfeeVI0CzTHoH^qnuHCH-FatZ7#>ug8PcMVQLD%b`9Nbd( zz!$y{3Np~wzjcrhj09CrCp?P%&h~$U--Vc@yxy;k@ zgMSSy*Kfoy2-)uf(Bf?CRgO%XQ!6@k0ceN4>uiX^ah@$-k4NKtBJ}x2a(wto{8&Cp zFXuz>dU=(OhT}$JYJ?$HWa5a%_#egi)BaEE&x5T$zZ(tz$<06K5QJ+P0%kG$4}`Nk zc(MEP%@1eCCx3m}J$rEg0P}vDFMCavv43yu3EIKBxJji(0s{|4Nc|=4eT0{Vs2%#o zrakBa0tWlWCLI(-Xt`p-j1h=++7$XMX({Q13NLGB)m;9)DP0+u9vi76eZy=eWeRiZ zPNY>#X~g(%I?CtMlwXZI2vZZ)(Rf_-v4d@3j5L$s9Ii$*QLW`{Dr(idW}?aNOn)Xz zO6#J|5JU3F3V~jD_1-k=iH%{<8@iFRiksk-z%U)0-$FZ=(NOczfcyi*YTW`4Q15MT zRQw2okWUI}XrQOXf?%K*Fz`veEgOWEkQ^DU7cc{&xu^@qA8GJ`=Z7_dFVRj{nYB+* zAmj%m0aI! zZ-I&I5K~ScV4d~>z~A2h;}pJ=MAOyQA(*SS&?-nwRPx|sQPuNhzc3;!_C$t1jsYUn zXhCFU8T+S%MO%1w&$YuX7~%F-8z{B1yRovx?SPwm>wDUdc5|<7%%ebV27g+d*j_5{ zu4JP{h=b@Tve36CN`^vM#FLW8GlnNj?bz653`R~Y!~zQx76uB#L~db6c;KtNM)!0h zcKsuMGtl?5BbHkcS&axq_OVWJ({l4;T<2L086O{HdaPi0414ptBbSv*8vFKgV&%3% zkJ2HaJ+B5WmWCUuTP%s7cYlnQM5Htl^$N^zawBm$k8E_*Lg{ZL1?J4+{Y>f;$@8jr zZfL?Y+kVg($`z=A&}(L?r;<3M^9`qVM6%4WNPbLj2jfo=Q}e1f8Vu6|RsARVy@9Uc z3)o@hTe$i8b!Ot_gdMf@6Gq}@tzyR5&d|Nqdo@5M-@W(3+A{)AYk!WO3Zz2PyLuCC z<>;i6M%00qM&~87UQEH@WB@IT4ab=06{4eb^L}qa^I{^|u+C(P|9c=qw{XlUevWy$ zXjmZx(5AX*|AcYai#wM{v1fF0llR{S$dEoBr-foLhsGD>H^M~{x?E#1HhSoDi7!D$ zViUz4+1=|MMQ{!*Sbr)?AK`w3NLBxeBg~2ZMTeDfK1z!SI1Q0Y-kcs?ZS-)t>!=^< zJlWyf0RtKk7wT-v+ISY0!gHy@21sj(AZV+!iM`twgSuli@B&%UL$H$H&`qHa2AUVt zDAZ*SoTZ$uXu0z!4NK$`5~->9MvUk3Fnnsz?TsLeE5`eE&wp$ey+JCLS<-5*Y~)GW zvqyXUpZ_Yj;9@E&=FyhA6L=m%ZzLx@$PC*_T4pJz|DBCP)mC^l`1gaWTENS8rdl9{ zs$9#h)~95pO!44ecp*osc(-w+tnxh$tjT{{%xkFl;1UigfDF&>* z#Go)i4Ny@Pe}A|)8Q0mUz9zjz{Tnz<8rssgA@(k|L-hg{OiBOtrkH?jF#+o^0V`|4 zUL1oaxxM1m*!5i-uJ1NmW!HBULYVSyANRVvTdj0?S9ssH)^Dw^YzL~)+ZK9@vf$00 za~%`+2rjmL=-Xdn%W=mEw@gVZ(Mb4JgBNz8ijj*Zzkg!nuE`&qZ@bXDk5r$dcYn0p z8vfU;;h$FEw>DeBI{a>6Em)`cR*u&UT_SAtSNa>#+GNRvhf#EgKFiK0!-tyOwo={E2m!MHWTj9>AxZMEWM+eVekwn(M$I@ufy(v5g^ zvQXTi-~YC4b#-lJeZBf^n+s?@%_XGCI5V0?rHkR;jn36ChAYzfYupT1r1PJOuRe8{ z+)$=+v}dZ=y*lw8zlRhDhuQdDo=!2Vl+J($PJhwIGXbDEhoaHNc&8~2x;xC|gjNF5 z-X4@@S5u)pfoTtAXuXD*s-toOAQLf3^HJ}nmkl9~X(&rAn9d*rPY^9#7{EvuRYk}2 zQ-aUAwhbpVKcV{OqhX4w;vs)v=BF%AAh}aEwF$WkGUnzTgO5ymr((P5JcQ4`PwMvQ zrGNe1JBgbkBQ-LgtF7jKd@o2oyt1g5Ftp`GN5YX2^?eUJ)wNhkSYZ4yg1agU;x z{vbJcusgHEGoM%;jcO6|B3XBK3hTgMx*lO`-B;j~W*s?0C975b&oaE2TNZp^-4x#L z{?I*p{qEIX_vq}W?q4DG=6i%#6B1!6Ab-5=UFsZbwTbih>oNN^zotLWMq~a{6pNo2 z*3$HR{NlfNoqmUzU`W~$R15|wCm--{3ijm9$?nUu{WtGkpE!VL;~8A5lOGu2fPFdZ z<0l^=V$0!8Ff9^+fXMYdn3RI%L(aRMxOMPLZV;gvb6(+Gg4IkYB=xCPkJPIIZGX^& zctTT))(kIpY<^5*3yzp z>v}IBroBza&8618phDhrkN^+E&3|N1_fWKA@ZKl_h%=B>mV`HBv6;4XAL-}Gcq-wD zjRDiMNzh$eaM&9TLBGjEofwIH)yw6pbzrjm7$u!bl4wL?Ul|zf2wtjhg`g+Xte>`5 zPR1LpoHl+-z83C1 zlpdAZ9LkLv1!0TqZ&UykAb-QWcH=7@yStSJMsN4CiqOnHN`OV!-oO*^AzYF8Mstuf zl8=JDgNZ=aVhK_Ta=EyelRo4vDQ?kX`aF%P<1&dilGr+A*X43u{rfz{t9F@3M#TXC zbd?R$q>g1e6xH`M5S3c`a*c@AvSxmjU64y#heWD2yI6(k%0@Qc9DmJGA6(=C7>cH< z$c6L8*1aDMy1bf9Io5)86Fgd3uyysJh<0XL(MU&`@~d^nj3K zU8|!7k!M!Q$2241kM-e0Ig##;9u)Xq1&T6QoLr^X4xD{bQW4S! zBjCen2P2E>9EEaykrV4{l9w!U5Q;3u+Ate^Ji~wbquJT{?U~r)=YqFxYfJ0|tAbmO zhr{uwbZUY@et!*=bSFU!NcPdh$!M!t{&BFFOvgENmEEW`sAv7jbs$xgn=wjTifs&WP59SrPc_I?0zV2 z?r*}|i_NV)gCa=z>cG}lwji0;-rhEVgjg#A#jRR{Z32tncF+Zv_uX{}-2(4oRG^sk z&6Wg8_J4!wU^n--5WkfJ1B!|9>X5d#AWF@~=E~vz+gAb%k;V4`BK}?5MWj}{GJFkz zX@uz_9J3ASw%WV!qG)w%v(|8B;Q~{;2XBrfehQWqUJKA3Ll14NZ*3YdM%Yw^(q35> zYaW;Z7-Ow_2vDe;;5KG;GX0p|<_yvjIdKc6k$)*v+HmLJNtYn^(Il{?O7T@M2Q5;H zL)U~s0lXLOPm||v!dHeu02tRxwE;=$#!fO=YGx=>Ov_b8rjBYOw-TwvHt5)Xu&|}P z2oN`xvn{1Jm6_Po1n6!XWLJ}Y{g93Z=@g?&E{MS;wyfEk$omfld|J&o5ZbAUGj5-( zuzyKk;wu0L%(-2D`r9}g)#1DnXZVHme#R2B-laHI4|~cb76vU3*)Ow?CD%(aQUJ)a zse;Kbl<@3HdUS1Q6X{2C(gVj2DiTj3mh3K!h8Ul`OahI5K)zNa&zWdfsbRW88Dx^EHEkAfcM;Kx;EtlgZPZ9fQfbHKt z=4lJJSb6kgs(J+V1r%z=(V6m92S~`uC@&T~gaM427K4H$N!>Sn=(EHH^!soo??U4cozn>)V^FwSaIapo4do^|UoRWW2enCe&o3??O_rwj4q+ zeA>6v;}=bVMg4HRwIu!Z^X~dLUw@RAI0i8Y?*ed&Yw2kD-EqynxVr9r&aUUz8t;Sb zGMnWceb={i=L=ZJ^7D9TfAfV|yMD=LR~W_Jux9c!SxMKH?moqk3RC|p;Kq0@`PXEn z)mpmyml#^1MvsBjgJyU|YqHnk*cajeT|UoKJq?}YqBmqu@ZzKOr!Qn~|9@)O$8hxa z%<4BDaP*QF&&@196#dTp!=HOQ#`L7v6^Elw6Xd_3;334kSe`Y`Z1vCI4DwI0=Sz1iT`rA6en$(mrdhclESIO+(D-uIZ9 z+4Pp{4;~r+?fA{>CVq0Bp?@1R`B>sKTwo-E+%^IEUD8MQV2=|>6@Omdhp!#2s`h|6 z`GXVs>rs!Kj7x9h|4^i}$W>h91%&ejP&I)-Tzt%t0M7P$q(uLR*a(6uwA2lT5jZ8` zT|AiPm^|ek2mygg=4DZBJ?6DU@V=jCln$5zE|@=)5mOq{&Yrmp#ebR>-S3l%6ag|B zqcops77>uS3z0h>6S!!Q4eZU7ODE>nLf|j4;jjS_kip?t&nI265|92V=CVH+UoS(( zN9(JwgrdwO6TFw*4HB5K#HJ`?9-#^5V^Fr@D-} zm#LsaSo6i}A6|CdYJX9$RnzWm_ed;z^bn*iC+RdFk9xzxw!0FwAG^B;-J`RU?teRx zAz?Pw_7C6=Y<=HWeRuNXnr3Jo`3#VMr_+yTiKV_7$(cY z0yymL9{Euu)q9S-KOKWN$kYiU`R?`Z&$};P?(T_JN4qCoI)n`fnYOXsg;;2t7<8w* z-3qqUBz)C9IeM{w90OufAQ0X=da?WR#eeULwsI&(Z+{KYx7DeR+T{FFamtgxK)ZWdb<@RpYpAjQV=*qMKAFso{HspR@E6VOisE z4#toUa!_z?rOo}WQ3Wu4Hc|{3FCGgauf7<;OQbxF0o_>NgIoq%+uhyO3IIpxr`^@a z$T_IeQTHdkRS(Mk+uhZYR+30w%l<^Y8>tzAdg?KZVt+WxGFZ3SY=&5>p$j97B0aff z1Y?;@0RL=CSG+&Dk8?m=OZ(XgJmlj6K&0S@D#y2@*;P8r0(EzGu*N}l^TRY7c;!VF zVDxCe9Q8yuyl1tQ>yRdbCYR~#&8Lyr3e`6YoVex;vwnt9b({_ZVW+A8R6%ASiVcaQ zgiCuSHho3?c&!cm1H@20{9R zHVS7radu?vm^k}fMRe>nlJ^j*`1xPtm%p6qn>^=1N$Y^=>$@me_rWt_mXmdNkmVqE z2l#|V{AR}~=bOEcocagszRw9j4jHK+HuI_xM7TgK0(LK5<jtaMTuB*unU^k+|;fP<4-;G=G5BxvJkh_lpb+yF`?0yr9Dj;vb-~Vcr)y zV2(ymRgBN;2t{NM@<0NRo$zE*P|s3x`$f)U7GAUT8J@LG$`188=Kft4orJtjz3^@k zg2BT*Zk{P4Pz_HhEjTf49>_S+LgT4bA8r|ds;FP|GRc(p#C&~wbp;Z_wb$>De18ar zhkq$vs%?ed{bh&YO2m5mIm@&2Y?#e%>oZubjpPQt3{#_Wm@r#V+=TOyeJ3Tm4g{;s z@}DIbu<$zon^!%vVznCLvi*%g>jWb|l0Ic9Nat~u{Mfw827?s*cb*sojQN8_Yz)%k z4TMF}WWX3H>A)j{x_Taef=G#pjDOw>Sr7<$^utR{n)l9N8)}rNOHA9v9`|l_j|8?F zf{T*u$U;RGSN7<9JY^0s9vnkkGadenN|ZtD$f09?25heFJj}GspD4~5HEP1NN9j-FK!T6dWV4KiL zm4KB0IMQE)aNvB4aXpWP3MF%!%i~TXal-b5PuZ4r)Dty7g0_&1$iU-SSOHIx5Armj zj6qk3#WJ*eUi;CiRpr; zIB2^gPyWc8@~@pht)_vIbX1Tx8@L9W*`MRl$@o(2GSJTUNWWH_K&i$ZYvZm|=9EB{ zCf|TWt+=u1?N9^Xze@Wb&&Qt~1~r`%IqJyYXJruk@J9+QJ){dZuYWsw;FW`t40P;v zP##ZL?U`azg3}bG5I+YIIz@NRe0-g|4pip^74DTwaqJKr86-tN z{jkbHE*MEQqN{@;mE46yZ^BL}dc20iY)a-<;s#?9+CTsJ9Ywf$PlSKm=HY%|CEu$j zeZ~rQg>J6Fby{HiqkmeC>@_ShYXM1lT4QVk2@$F^hI*zct{&C#KmL(Cfx?PLBeRfy z)OgEC*k-gu;L08v!3(N}P?*_3BGk7X@Q$_3%)v*2tbmh$y?{wY*4OD7jMqe?le>s_THKNc)3lK&H zGvi^Mut3Z(`ROeNR%qlB{pu;i9W=Az_x~(#Ck5HR9Za2-FF%g|3&Xzf=Lrr2`}v=(m=R&?)Jk zox@3^fPYsMDdl}u^WF2WVjpn3ookV1K)ULR)0dt8KvOtVcwd8Ix-2Q2+P+3Tko0yD zJa9g{=y}Zqa_+Yx)ZDtRRlk7ywL7a9S?xHTW_U+8Fn3KfEe?;CN2A%9yGL#v)lXQ> z$FN&OY%Uq!-2b3d>2$>Wh99ccnkazIO`=-f5Py0qs?>lXO5IpLh^S%y`|Z3Fi0F%7 z>W5?~>GKifXCaO-#mB~6Oy=J98Bt!kewHp(@Ylj4!B*b9 zD}NW-Woe>M>~Eo&)ST;o=50;~xGx(&cgDK|-256JsqT_`byjPMK}B|>K&uK2(UVl1 zmHH;{+8HF}nuextaK-pMgQrS*VakI0uXT6Jk6EZJGsJEC9&0x_r}>N)T|s^)kFs_U z?3fCwud#@TeN7Keo7oKaR1z?9-i)aYMt=$nvUiKV2!?m8B}ZFabT^HFQ#D`d+?aQ- z9y|~4C2vvuXpN1u$BJZ_LQ0Dt9X6zJ;6;jcQlvXTpy|EXq3V`!oec*1*2&@uWPLkL zZ?f?`mx4u=1@M!RPQ1@W;#o`+dL5?fK2RY__)VXothj-S*oe<(gYjH+Yj6h`!GC~* zel#4AI$vw}ls&yg%_)!U9H0k$r#LReJ9|tdmT->8GXZXqnH=32llC=r8>~P&r0|*0 zUY~+b9>kZgsXUQN44P=sw-d0OP`S?y$tn>x8*2z9I*DwAaa5Ghc5wknAxs=qpvu6wWQ@g%*T43%3UojtnE26mFAW^Yzs zUcuE)1p}EVzPm!0VDiD$ybKzIdpR3nfyRZb))*#wxiDrnSjZ3b>MY1*0e^vIl0|sr zGt=Q+=xIh<;98H)*JC3Xx(T!>E_fg4GnO*UR^V_?^qSmA9!r!GH_qtB;{A0nDxhW@ zCw3#Ec)RfKs>;1p_^vg~KOx2mu}tV97Kf$`aOQ*OWt0@rxrEwfG>>%XP+b0J@8;~= zccxCkxpQA%0A4_$zrXwf`AI|6DXV`>MN+hCVAm?E%&SR~DreUp$J6ZZ&}MjyZ!(?p zP?Z+v9@c7q!{Q$^Wj<>pA<#a!@`7ylurSh`j;(E^T#T0HLcG0w;J=8!mtD;&LyJ4(z4T^`w~CLECP!Ci@e;r+vaH4#!MFNchP} zBtGzn8v!~jDOXoxj+PGJI^;>+!9Qj^=9(ht8B6i0(^ffgKSP7IGIVR|`e$I-Opo3@O;k zJscX0&<5)I7p zs+fMXf0urb;eF_x45_PIu@~Y!eFiJJ|G8ucRYV(7+*2?BMHj~1{@=nRNc<5D<pAkt%8qw}MN_X0vK#H(qgy(1 zlHFq9$eij?jA4Jo_Pt-w+25J)rdayvbhejXjHhW`_n<8{V%#N0E~*bz)n&iMBYwTHBEin*7gJXDyopZ?d z6+!*sc%<3v_^JolpX!u+UrY4hz~n(y8lWu~$puWRMaJ)N<W)XF9^k0TMWT9C?!d zAZ{jC7a?wzw-TD4_3cB1rL|RWA*PllfaOaFKVFTior`O zm(5^{xS4-iF>Nb{qvc18zs^}Y%&>F}aZR!A=)uXx7SOX)`e)2TMwZ zenk>6hE^H0i%s=?ilIs-P8-@60d6M=>!3LqPwGpM%JzDElh&7fcB6tJj!%%@_*!O| z&ZMC@fd|y;&+iOc`pdV^lNC4Qud`b%V#MP{ff;{y!(c-q4f%KW%cc@q@%o5wdd{|2 zK`(K(05Z~|J(m6;Eu#92SvPI2Jv@8Walhio(ZR)_$f6ydt$Qa@eNn#akA>3 zIFQp?Dl{j*seteZPCsQk`eGeuIxM12R^HxAX4lzGWAj?LZAX<6YAVBVm+a%7bn_AE z`$&J#pn9Q#c$|*wv(MLT~N zvvn<9v6LY|>GM=E zcL_zb=D(JTHnOMGk3w;b97Qc_FLZwxwM?qX2S-xqr|?AT1;TPmn&26a1e zyxR{AsgQrO(PVCRu6_w7{gMkYLQx=x)2L*l2_Exwe)dXqof;!XO`K}*rU--J z`tCNa-PErVID6LME(enE`p|U=YUkVdZdjCd;*akOqxdN7{HM;L>j(d_GckV@;-hDw zSyFc#F>6?CpKh=RXsBwr$V>K2UX;z%v#dGO_q9Nzmj)+1hhekz-LIbuJ+g*&;v z{r}-1E(#{~kRH|O0}Z`#>d=3W0cz=4?2rSLbh^44&xgtnLm4qi^Gb-b4Dh7!eRzh+ z3JN&LF>&{iE9yIVQ8!*y<{3+FrXgor9!V5NUkc0uDFp;~Cm zN_IXJDniKO$QQc?Lsol0+bs$NAOQp)W0Sq5783fZttTMt&=l~Z(Ur%q33v|o*~NfE zPib#?Oo|39SNEl9pM_ZXJ;@q+7z&zuN-uf)kEDz`7$p*EC10i;w#9j=CEd1fe$YvfK z(#xmF&PUhuW>N}@8T@B7^ zD)4cQH&B7sZ_??7V3hv`6?^m9I9SW{igtIXI=jk7#Z`sOmPf_u<$15ZvbNPoRyJ0} zKU-^!q}AMBUrMl~-QH*S7=b`2U#&u%i(_T5QVT9eOSF=pAk9|p_d9skkb0se@Wc*)R-&UpYkWssfr z2H8CCB-`8Dlh2QU+SUB}JdRqBQDeAulC~gRJ|2omjtzgT%4o#iDF3vK1zH%hrXY_* zt!Txs9fLbJ)J}>*5IL#J7@e2J`kw_%yA-&;LBm&v%_EsZpOvh{D}sDgS4U{$Z&{&n zk@`hDR;VfMYk`r&3(h}ptZlAso!7h?;VTzXA!1o3HGI1+Kw1{u=GPVKWoj=rXa@aFW=L3=Kmsa-#%!?<9Y0$07kv@Peg(*GWLb z#hJxli=s6`HdK#wXuNU2TjJ+DZAPg0$hgr)b!GR-)V2(9)WI|?? z<#|VR5Q%si$Kv<;vD*sGPl4!y+>GR=Phk|ew*ax)M|dpGd{-V04Zq3JjtAgeSn|nW zoK=6UMOBAMSuQ$8#mx5^;=&fO@)4#s#Aw0zY=k+f-44>cFI1JD>(@E3)e}DirV~`v z9jQf+fal_h!rv)?k0$gt7j;q}Jmi*FLStm{_T>1KHQ%0QwM(ZTdpaD>tglGG@Cy2G znp;_Ct``dSAzMf8LY+96eR=Bvy0OEgeR+SeA?Jok$WTmvxEzo`Vj?YM4>RT;XA6U> zKrdZfc&MSQ?Uv1zLD797*`;CQ-N@N zas>S|J>vj(qO!LGqtCX_5RX7^2g@h z>&qm=iF1P&E`kZcII6z#7fn_|fJA@OnUt}tnM0gD#vFHrOS5IrP=21Dm&o~qdPBv$;{nfQEm zMsG{RN}5kF$73!p41uzui(~t}u3*vE6>j4Ml|9qM@+YZ{JPj3j5E$FcbzOhRNkV#o z+JeQC}`#zJSne@vGYJGSn4N1AI9tH1uBBZ=3zqN z=)Rh^zA1K*m7*of@qg>K!BTbH4Cm;!JOxIUA7Sq7b@^pSRGhprb;0QZze^p(KE|gR zBFr;b7j2MYx^}qB)u8SHT9JMaQbQ)6~Yf*PNw6d}DNR z&`#ZtBvx3Wh=2$*n^uPpn1l$zL1|TzvW3Yk8@onD6C?Q>)L%-g;oE zc!~fBvtXU-N%|t-t?u&^Z0JzSH@&35SE7rEi|n-m!M&K1X7=6r|9Dd_BKD7Yhbq zi1US|aRVt_qbJQLva;Bu4zp1vwpR+vPgCM!ppStt+}S?r8U0&q-R7{oi3~W=27`(8 zhx0)?*psouq|-D$cl?wHZL?k&E0bJdD9f$vfgi=RKv91~m9M_UI+B0DI`Wg!@N$iW zQ{hPvdCZNjM#`Wp5?kJDdGC<#-UHlY@bfs=$pVZMZ^R2yzmY?!!dl{Jr5TfH?xIpU zRmWB7yP<+{?D!{kaZc#WI>t z_+!Dr_|t!=VpFK98^g%r@mvD0434#)L7WZABk@QJseEvc#)1AuT5@NPsaP1`wgqq>r6}4eU3*b<4Zvbz^TsA*ZVaC_A$K$Vgb1HCUsJC zqZJUoHbphL4!Zax3l@h{kkovg+f}$_6SyB2|9gKRP5t&%xabIADd~wb8UV{sw0tc|5p@SB(jl7sFi$By+W_;ZF_tlZU&IHz! zjJkjFBVoK0^TkUl{ak?{q+aRkcT$UZbY&y(w^}BSU8J6R623MGlwJ2e8*h%4R_kA4 z&xuJkQSvmh0w1}%O^};T!{Jp_E7>|{lCqLV_oOEE;Ia;v5%z#~ zoISFB^qoUMcRP~47kYG&^>39x-KNsJd_;M9rdq>uAk$INTOi$CTr8*x0nre^w7P$A zP>q9ZL}=h>nWmY?RZLLuS|-R%trf0dy!;~N()IkM@-kdAF6*;tZa&N=}U2O8xErV6axR%oNcE z;f!P0Do7aj?mqQXGI;TAUCZj*_}8?1wg(QiZ;eh0hS?^ z`AE5Fpjr_%k|jhXh8B*&tCPHYz5Da-iR>GQ z@$n3L*P1PJJnIcL%{UGw1W7)>ZX~t?XImDf$+t2(g6u?5ipF+sBrUSjlRm$D?mIv_ zv{57H6ZhepFBS^Ko_iLoPWXQz*uoF+K!=d>M(~vsF|u|*y)Ohn`sroOcw^s=`D}iq4Y23Lhr(qFB5dO zoZ>YxTWP8W8lyv%qE585wO5%!@){L(qaX%GZ0u9!VweJA7kvtd`Nn_q>dL#}Kx3WE zg*T@;OXvl0D5r~VY3p&)(<@{RlV2JKGwx`xEFTI7Op6TY@SQSi7tD}CXb>vb;O%X5 z5iLyi6yCA9jn>fk{c~Z)91S|0=Yw?J&FgUgO5Oc}^0ijliC` zQWzgaKa52rP!SObUxnJvh8A~$>1e;aodSO$ zRZB<9?~ZHsdhNRRIlG=;A5LYC*MsabgLF_W#iMs&HM-c3jPfLL?vrj?Of#q~3)GrU zp1X@Y1l7xu>@D~rm!bEx_NkZm*u+o8{)yABXd1|os_K6&8J97So*`*c)xk+SPbm*1 zf=yuBMqv8!Gu-LtUH;@L+VZS;;7vQvB^u(`F+e=Pn@u@dhKfNT6#ygt_}Or{=OuyF z7=ph^d$;xLeCu%Y06jwT=t~}bA!12;CGgO z9Crv^0mk9&2M!{E-41ekJGD*`mcbEXH5@5yFf_xFMY=z}2D7(n7^`YWfk@gRa4jNC z06lQJD>~l-hYU8x>olF^Cs)%nzZwrszyrnQco3|jX{k;NYQD-wa%^Y{s?ceXuH|-H zRM&rW98k>|7SJ#5lnt;({p_kt`dboHnp^@apTO}axD}a?z()-&YJk*B^VN2%&rDny zAnl*Xj?vxGeif%bjhZ*Ysw_y?hw|)(H@I5uL1%!bYd$wYJ7gcKLAAaDjHuu+@mL*L zaM%l>iI4c`DrI~ZG_nsJr%r44_uH{F7CUC;#JzAWP%iwNf((rYOUf*S{d zk8d3;EC+hPBddb3;ERFSW3t(8RBpO@M7V#ilNC?@sEFON*V5?`lobbBzvwUW zv`%#3vd23S6oSuaTv$v~DSSNxjvIdzWaQbDt9USX45PQ}3;kML-K=ppg<6q@=zC2s z#ueNLNtw;}B?zt|=t8^_8m6ia6AR|@!A_wIGAy~BS^-}75CMCBDJmwSO~G-oH_4X! zm>AAEB1)e*c~7)E$z<}ldi-z*w^Hw>L-H^~f5F(-`FMCEmK%Oq;~v}>sW^Wjy||vx zw)UH7t>P6O!nZZra+Y1Efy9loW1GoQJM=(AQH)^UN$2T)Mfy|HpB?&TjTS2HXP^>ch_PeLC9)&OeOn-+V;Xt z(QL`IcLFdlG@8m30}F;1h`E2uwZK&baiT|cI8EDhz^vDZntPaZY&1*Q_h3o zxo9u*DcP5FutIIyP|xUg&+!GG{%W7RFkZnhGI=^Xkw5Eob>KIKk{*DmFEuQ489?+o zy?w8qPsAUoVF|;zfOLb=Tr4RRNWEd+0rz8kRma|TU#DX515x-x_k@2H<-va*emN_PdkbKRRT202oQ z)g{3Uo_`aa)Cr@gC(reCIvr2x5ac-*=erAKx&{PWbm=4KGaWn@3G`~JICk1Y9DXT* z?L%^i!qyuO86ytGeyD%>*|2#a_zBx6t1et7>^wZeeR&Q=<3Fzt;l@m=3) z%ZcU$^=D&sx#g>V_map3bWnsYAxB?zb(V|1&dx1$RCCo4{wjYQ_xSAoq}pDPRME9N z>D_{~cEUFHW<41__N^=i2a~p`OH^B^>FGh1rDIFQER^%!U|91?X+i}D6nF;00FMBf&}QRD{j2cAdiP` zxi?H{=@Cx9=W$y$<$~Es=x>3i`stw;;J8JDWE6Yr$HHoJHJ?yJb)`M(AVu_`|AhCeVOsHDCHU`|hZFc9#6>Q`~{UrGx0c zBr^Wn-erGkANK1)(X*gSI(Q{!^Ll=bS#{?la`;i<*Fa)9QSAuWZ=Y*pn*7hctA9Hk zi|s$pcbavsUy|lrxkL|j_zZFqDbc(h57adF0Xa0{$`T04hzR@PhWH`C2@?nKEg_}q zJ(;FpIv_>$2z3}->Q+YDE;;)2>y*6?mn?~ zuqD21tRHUtIzK#Y?Tasahnu4PZtFmY0oh*Jhnjl_>mL|)ci|a3(pS~`7M@&EJM+X} z)$4#{j29u8n-}Nn83V)X=YIVL^@f-A>bzIue!~aIenlosApEK~yMo8$b%s`~{>~1M zk`;d>p7bOlwLHFv@vqIRhs)>t=N>=1>~n5!DFPF#j4~>#2psiW>JtN8zncVF)|5^-4+HD;;;#`{LC9UHVgph^ZrZJkJ47RF8BUx_|wxN5X z>_&CbRP}vE#T1bb+c@~PQ9w`Dm&1SUSH1kDIKri7qp5q7)Zl%D`HMb9iExkH$>(Z!YoQV^ECwr!Sm*BUfr`0j<}^In;aG>5 zm9t)_q9#usdg|rj_7zw+dKG_kq6(9$P}dj4cft9J=`)T!Ej*M;q$R#bzE=&O+}zbg zlxZGxO1LA@uRu*blXZzKU}SjSB9ufaIK?!4Hkl3EpLDzlQ8e^LkM#=yle8V%nIbWi}F20L)zR||Jw=t$;^ z^}ymD*zF@eVXBVl6fS@L=is^~@7kP}tQ6?rbkE9*V=Fi2H1QrOgP%sZR(1HWL zN~i@2H#QC7a0^anRRb=VNq0I?p%;QnNyCwE9u4O3!v!O&cyQ$BnDFO_nFq-lM)d8s zTcPxItu&e4@0Gpotokc^W#KA! ztaBfB`d;I73MT~fRiS(;j#Fm3Iy3`wXs~tx7IGe+2wNjMN&eM1Wg(UpntOpq&<(Ok z$&G2%_tSrhU7dfb1lP0iczE7ZH>NkeY4Y~vyB}V>K2!1c_uieHi2pf0`RmK>*^2`> zo9AK@b$V?1^BDUv`ThLSov$;8Y0E6Hxe zI7sIdu+1?>${4O~rZ%;ajFUp^Ji$s=*Ye? z;+$NG-aXWVL9uOCreF+Hl|GQq-}Aq~ApUYnvJ4c3#&%1>J(;+=sxNih-kT3H$TGS- znT{`nTv$H8UCsv2b;x=Za93bK#h9dv!v+)u$`1DUZ=%2S2CD7B_?i!jOT&awQ)Shx zFZnyh)Fpqm8+cV`epgIJqi|0sH#lLq@wu`p0u)qCQ4#=KPr7kOkaRCbW{64kBml3e zgBbC2zy&KC8}!oA;0>OU>mHy+Qd2^zCc^|AuCKyv_u7HH3DX#qLb=GM`D}SzK@siO z?*(9`Wg)Dz76j274pr-wjV;l()!nMxwm;8j<7`)j{vba`0OD7eoNqH$MT6FdBm}V00OOV3Ucl3 zaY1|zPc@F`=hxZ7jN>&8kKeDxhrMUZE3|(NFu*|zwxZoN@ppT38^+jP2}nVC-c#8z z;>raHG4Hkua2&RK{I`MV*Kqpt~MH24?W3M~B{d1OQ z=h+aRaoeMBbj?&*aFy3?B{>jYr)7UDwLn1ovX0&mk9BhPJBRE#vb#Rf6I0`V6loUQ z3ykSrQ->t4D_|!}D<`js`R-Y+*daGpC)*ntv?Y-CQ{J27i(^~}g=uMIWvw;HU*`Br zjMSd03ye^pRP9-;f{9g_vHNWXvHcro5Z zUeK?%tQP4G8!zYBx}cBrK~yXS?5@OnOq4VsErUCNAeJ;##JVGL;~qWX^QY(9&KK*$ zilBRRA+Py(vOIbZ`PGFPS^aVWo#2xU4a)J?>5uVLrY|=lmO5)CR}C=c&u~~P@QIKw zLFj|fB!LGc9of=+HGXTZPYQqON}F2m*PVQ<;`$&epRARI5Ygt!uYkzR(^mV%F5qupGEP^&+%0XlkH#eY1rLZpxBCd-MEOx?6uLiZzlfhmU%Y z?==pJpi%WpS18xuA~jBAv*ET<ku+G( z3`~9{=3=l*R*OpohzvmSmdtg0EwrIDpB?k|#XUC^);_m6X!yY!dAYypv_`1N0)g5z z8R>BQz0n{WfNLC2Z>M@gC|qye9S-ZY=4YWHL(*jP$MW)ZI>;a^or-X?tlV#wmn8}{ z6@%LiM^5(w^*4Ww#$c_-Dk#LyS^BA2m_JXP4nC%XeL6BbOfN~FExI!K2)FwX(}{w14TWgssEHwhVqZ7riL$Lh(RR5dM= zft5%nPJ~0lO}2_|=)QH-;YKixD0Icr!D}b1Wv3$I9;$!Ijbqy9#hEuJi;^eB7F*=p zzs#w-)9eq&xgexq?HKz?w_XdeN}C|w_Y@W95Vg2Bz2!1m*3n#$tH4*s;-J`vcprUJ zw0|Y2RF8mE6M^sk9Kn!?MHmF-p=+TzZdVLLa8;(OPKeXTSakueHf=dmmuJliQ%+I0?Fi%uSH-~g-Au7B!Pe{S zrlcF2ELJ5vs{2`7)vZ0EK>0~g$W10|p>qqiyJ3IIF?D$cRpb%blVUQGB-U~atI}*X z>ENJ2Ku6Gy1opk4PS7b~={=x%4OBQ+oFK@+_mFeXCt!(h8y@YODtKpJchp6UPEcIE zSa$_$hW5JkIM7=ek&QYkNI+cRPHZ0y-&2B+fG)V_`+`CCL*L0RE)x(&>G?t_@$xuC zkK})9uy~#<0iM5 z@b#gYf-Vr;4J55&3RIETu2z+R4l*UZ$Ip;VPRhgYSb*azS}GIM1=<;UKA}$UUI>ZXZ>FNMZKAXaX7wP`7`q zZp_A9fi7NEZ_rqQn23o!V6Q19-Lum0HP4D~?02|M7ovsLKytcNQV&m*gPA?D;4)9Xg76{>$UoAyR| zrrdC9mYBCA~Yx9+=Y!tEE2 z;2Ue3Yg^}PWL`ZWfgl$Oo)t@WuO28=KN?cCTk)ELH!ri%%k%^ zjc?LvXEdJGJNjBud4MeDsPsTUuESMTaHWn+RRM)vl!C3zJiCjzll*yOu(HwKOrB)d zFwCBfuSC~&ydJ*oEt@j9CA&!AVg-4H8V3dp3h(h z=_I4^C=Je%-XJ?k3;%CbXdlzd^Im`h1k zi1N88XdIaa_4sM_PRe)Xu`{(jab>sy&>eX z9RzBPg;tQwZac|J@k*`0O~ER#seh1v+Pxeu#F*1FZppsQl-ssKks9y`j4-be(9zTkhRp1sQ8#&TP3P3bz~ zSn{0Ez1VcFJG2u=fUw9X_c5W<+dJJ8i%!2}q zwEyE-_w~WsH!pu)pPaqjJ^2v;>1<1v@@D+0N}O{6nWtoar2OCKf;b3|`8>-YZ}1rD zNcMfKx{i4%<>@q&nN)O5c8bt{sY!ri%kE4F3bAHPoT^gV&~+d){CJ&uE4y$okUP&t z>vWy(B#+RHcG_2WnzLb=^%kd^0+1yCcKqgb6Cu~8r9?=Nu0{(v!hJ|?^~ zMF^E@aM5sK)>-X$aD%B@Y0}av6F`MP{_XRFlBnXa&Z76yld`ublRM zCrdC{Xhwh26XfYspqdckPN_}^W8#IWY?75`ddDw!;faY40Q1;k$0=laYVNFr^CmIK zZMy7va1MM*+6Fo%9jG;36_?IP7bjkw&rpt@1OL_EHxg)YDok= z?Ubk?Dq8_GC7E5NSAqGlPiBL&fq?ImS|=#%YMJO^hPM6bwviC4y7d*A|CSh65(v$7pjTOqNwh0b#z$uHSE!D8dXj9#uq;B zp>j;w#cka}b60ik?VDqpBo@~uCbpDF%6xW^%F-ff7oevG$4ABpB2~7Mm})i5{Jboc zFw}n;`So)yhCR^%^DV@k=GocJJUu%Tf7mKaA<`c!%3y&OzMbWqMZhra9BY$Ue_q02 zqk7LrVC(*Qo*tzatEydom0iq8LK&6-a5E6)(2v1?$GW4u$2-)Tzhw>DsqNF22h}Q7 z==RQurSY~`kV*!yJPVQp=(t5+Zjm?Yl;3}YlBR@pgVzH(WbBSg2nhR!U#bgLQkdbe zR-s<+d_13Z&WF9xM_+P-UYGf&!E z)>1`+jy3!fH8xhC8&l}pP==lP9!zz(`IS>0=3?QGX6A9Rlj{&8ckcesJ$wD`)n0%1 z=cVHMn zlEJhL)W^Dk?es|+-lN-lB&XE7=;wd8V%MTMqIe#!wO)r|FdEM0@VO{(=7rvlvF;{h_mTw(pUqvm`gB<@^Vy#tinv zBQTJPaxaZD#B-WTPasNU!S@A>SRZD4VzA+=(g0_m&7?co_KJ3xX<&C@ z>;QRh-hu5+S8ZBs!nUbA%(n&&fo;h9bp*??60pC;H;zI{U z3G9&ijB9p9vgN|~>uPij$2y{WHr~F|bRg6RF*A_pNgcn;Bq&>6Cy}ALm2Wz8 zA5~SmVLI_lE?yH^Am^CdfRxA?f3$q-=E#Mp&714n;@|GU77sluqmyjxueE7z+*;^- zXS(`ZJ|5M>RxrBUV3mLAI1-WDAIVkvRBD$cd%gKFy@hw1-T}!!BIpoifQev&bei=| z{y){7dfh5561Zy%@Jo)m&X0CMg`K7HATenq*}^wH5vyF#O2LgWuse)~h zN^CjU&$wga)M|eSF4L-^IYxN`ILv(7&~5qLuKZ*2H8}G7NP`E;fcvj})-t~MAF$*L zdk80>^pIw)BR=Aw@?w(NO_f}$~A9+f9u3(1$eXQ7$pw*(E~ zU+3tqK!kH|`O+RMM+JC9IH=Ae648?J#z_noyPyOsn(BXSK!YA7{=&2raEFe4!&hr6 z_pfE#WrOuJt$Zm(kg{R2s3NK>cw;2h>An=agU!d+DZPS?(jBWXx`(2Z_?oy;y!{zRmf}l&=SpAu0q6{L@;V>y+fN;)biFcuN?}!sibCGJ9Dhou`@Ni{@pfa@U<(;e zO~q%CHVA*TQUq#UjdLC2A0`a{a8B(!!U-fzlQgJ1B!Q$n{eC)`)fhiHwVJzF2mI#X zJyIrbke>bxH&Aesd@3d+6Mz5f(|@hGTv&>L9yFM53ek99Q%;)8Cs6e?gwNvLe_kGr z!IwJ6vu_CkPGv-4W$`|Hm!FpZmcyOVO-YBlqUL|U<;(j}6MGYSgwtPW4GQkd-F|5* zIt&WN7e|Mra&(j7sezcKhT`cglTH1rUxH(F_w=^)L%DZz067spJ)Zp`W!b+~~BA%}u!yxu8*7Y*qx9^&w1>1@MuYo z+y9yMc%iJv-b_4J+T-{{e3iV%-Z&OXd>mbW!7RvsGKIk8?Svw^gWi z47KX6pJ+uN5NCh6)O&T^+)jr)_3T}?tw!E7;S3s?gf0crXD0xRKJPAHg~6?l?9VaSXpckz**jB#-0xN+wbN z9dK0X+biIBEVH_k1;=6jb~vm2^6_=Z8Mv9gS@X(t6!dRHIgNK7IsfYp9 z{ujm~e7?|_1P&Hu*w90v#zrgp#i)eOoEnwDVY3VsPlzpmOkAQ#bRt5n7@>dQ;q-ac zBP?_qDl6zv3oU2h;nN?$mvWXuR2n9461r@(K14?BII>c7KnWSzoI1*iL+j1UzIX20kPOPh_MqfeXdg8WN*cOtUKPQTi+YrF{A}v^cX|<^cPK22T zIthgn5Fx*J5$G`O|B^NWW($8<|IKXyWmFGh=U=^28nMV1p^z4Z7K8PQ26pm>Jz_m@ zjR!I>%2VLno$P*Q z02o7*H@fhZPPkgLhbN2^=$;W*^|(}d1R#gg-(v)F0f1w!P*779X+Xb>4-Lbe;=qDa zTaQ?Cxc9`@9zRR}^RGf@;P+pU;pz7_q%6rkKP=tH3obdD3juJgSDt@q%|9QDih^YBy8vxz zC$CuPSI`e)rg;hp?1FNn_Hd`+`DFsCGfSk>Zl1mvU5xo4!S`*?Jw56Ug2kc_xX=j` zArXZ_XbXinSFTpQ0dHUT3EN0a6Y0j_%w#qk?B_QGFY`&@Lf+krFJ42QtWMuC%Z>K zbWe^=ZUt*YHefD#qr3_Ts7IO`1yg|;j>Vx$jP4M@nbK0Qv>iLFk}PO4dDLpdjma2bw|cXwbwj1*t-I!$PU7;B zrhXFv|4KJj570UI#tuL5f4#l{I2tFm{ z>TX#eiI|9a`&q^cegiRQ&GXm#Qmp;>lgb?~0pNcVXkH#zY#P`n!AB4kR#%X}ncYGy z$Y8__Gh2~>8h_O3Vgt0yek`!J-?IE56Gxrg^--5)$G4-tkS!3AHdcJxze)%5VW|DE zm(TQjB|*H)QBRyCFz!EDO`|j&z;O$FCp`$c1`a))h_yVP&+taZdNF97O6uEGVu!pc zApd_Pp@_p*7Pk%5^Qe+sj!lG z2o?9KM+{;0xJ8ocwVbYN6@}!9`izc6SuQg+IO)H2LU9?+IEoIP$V|pUhSVwQ<{kQS zn>rE1{vtzUs{pYE(6Zp6{>St2=jSzXD6D^-{zZC0Js6lur7GZ2i)vtIp2y}Et;r#} z!uJ><${9>A8dx*t#nY0{<2{)d6Ql5W)WFb?LjCXxoUpYLEF2|fa+#24R*Z3e6bJO7 ze2k$7Fdj*5U1d~UL6e=q3GVLhn&9qEAUFYn2lv4TcMTdW4DJLcxVr~;2{yPpS+d{m z{@Neix4ODco$7bqk9XdwzE^MgW7ThWxzp}JGZlncg{`^W?)znoIGQHC(wUNm+~%K3 zio7f>Sbo+=MPQ0Lq|bowf$= zgJOj8pKG#ZB}$%}G^F@mg6AbRoxW^6WghPH-us-*Po*2{jSSuh=V}IM#OAilnjmcg zVBTpC60D9J4jx-`0YFNB81lcz641uQ$eHdrzBRJ=>4WFcZG}e^mwe|BDy#+c1|uu` z2vJ?pQv8yYB_(q}bPGd4MckTE`M;zLpQ`>YJZycjHST=eShYvq(>Jzt51f8uMSJS@ zW>c;cidbhv*J(O^gk#1Ude1_RIG};>?wJQgT^hdU}29jb>g7yEt~;X6{}Go zo1PVVydrdQaol z0UlM&zQHm3;G2p{I5ag;k>aL-+lm4XXC)gm-=L?hf6P5`KX1C2@pZ^fT@vgVT#mVz z3~IfTJmM-rsJX4OCubr~@OVBg%OC4OPpT2Tp|c_!4M4xa`g90SfQ7hKd1$DdXwTPLZ4{RL8qHWN5A<33u$~Fg^9pLLn z*E8*_xGnwFcMmx6>mi&U0ed}VaeN0ZY{ga=^_87bQFS%T%l>b~sAW5orr%S|8W0d+ zg;3(21&F~t^_8kG3BOBUIX*0{3e10P#Fcvz>!~8<_NNHQ(vxH=w%VBRJP{ibJ&r%d zXh%GYmm^r(^@V!x9w6SxJaL+PWPt}lI(2i=j5)4d^o8iPJ@b)$?^9Zxg z)luyn`q=%&Kz_Ry_Rsze+n;r|7qYlZNlBbf^`G$b1O;Qeuq~A;Rj|~BHcejZ2KRJ_|0h0@c)>nRExjYF@nZJu$%ZK=+DI9%A+~hiy%t8}2w{$+fU5F3Uv5X}e2)=w&yEPw=7{E;#B4pu_{IKrit>X^e)~*N{`Tz_g;HyD?h`ot_xU+^E{Xkw z2C@;)!+|_0mcO;0E~Gjkg8C|u9lz-h{@W;~OyewxFhds#e{FIqI)R*Qf5<*GyiL(cKGRS)Db`w6;7_EFb?;=Uq#-;?8x z(^^8dpb7=CUPI7O_~nwPxpRRRYGBVU(u)vmq)sqcU{3+Bxs68jd0ads$c*n}lGKqK z+(*$(g*rJG+a(|(>Bmvm(I)rth%u5e4sTXFdc!*rchDEjSTF*)UhBJTvTA|`YTiIp z;0>#RC?Q)*;%yG@ijjQ`L>&|eOX!J}6mY+!dZ&Y-#l_ zjAx4h(iq|M!ANq4Ok^ryD?bQF1I)OdxYK+x=vpNY-;JH8IY?c zOzaO3?f6huB<4 z9GpOV%FD45RfD}e@{6*bdgf=Fnx@Fz8DfNifcIJ@%wlGnJ%Y&LM}z)MSJp@#rQ1td z(2oNowBTc$u?TX@i1VO#t^-M%nJQGhr^DQ<2mKBdbKPQ76e3@F2Hw?vO5j3;Q`^9v zGrpl`zJXm$Rz-6=@w*}?b@-F1kxW=TyZeFqv*N>QbPX-mbmG8>mk>D&N7t7vPwy|< zt^gi#)tN6j>@z9o9J+$E-!y-2k_~RpviDV)4A+4B`E>l67bGB~`rmQtfS| z+n}M3<$uGTaFO{uJv>g6_FQu{nNkWRt=LGgRLT2-R2kL^bG~Z{gxu z;la74`@|*|;`kG9e_Baf)y&(t7E5MdqxBLVfX9YzMv5WE!!yUWKQs`Hb=ysyI< zQS$VLU*Wb{6#VU$8Dya!HF4I3cN1Vzf5&)EmK1p_-p}abp00a}YJ4&3!q+RWcwVcf zm>aEg;cxHG^`R-k)L<*7=`Lw_#S@;Xd=G9znfR8tu*jP@ zsxQ~Z2+1#G#%RXSeAMbwv5YU6!xk)J-?Qbt5u?9BQg6&qB+bwHvX_3BHkKV+#sG<@48UdM)s4u zrlv7#)dxO7VI~4lNcFR@f;*16$-|j^{tDpD0nTp zs}jp;^L~IaET3C3^4D!7AbQQk7v2EN1`x)bWs1;Jaz#elb>^+t_Fi%l@4bU}<={cT zMd7>sDU3vx3Zo>}m?1UZj`qtAC}c<~C_2m0^?nk4h+OJV&6fM4mq!KrExh(^!ndKY zfyTq^>mWv43h6TWccAW@_5d11gFBi;{#uE(ESkOWW*d)CaV)d)C(c7XwzO*S0vc0L z?Mw}aGXwPtBNq=uA}VN#w@VAvwmq>gVBs2cn*1w)bWxTPzrV)$@oN}qMAwX}AJWLG zifDk4fE4`8;CJ7T%RM)qxzw*;N(hLW)bRtIVS~GAHv}B6m%pQX18PYn43AM6H30>w(L^{DMcRSy4BJmDg~U18q|RB=^lF z&}GG$siSBZ@T2{A7A^8wRgnz?9br+TuN-&@&MtqsU!4SfYkU!vSJXlfmI4kzx(k(?eTagg%j?Bffzu&;9cbMc|+hjQ;t$p7I_+rq)cWIQ}vCH&#%7ca2MybO%4r z=0wD>IOI1sQ;GMZfYvERK17`pn_CEnz&N;G#T%~D``&_+MG7x=RR zV#}Jq#+ibNl=-yowvph3!~DJg#mDQa#?xIEb3{4xpQn{leGL0bdF{T>?RTn1Sieya zqXqXIX6*A{d1T_)3TGoeIjNfN8kqLmhw+5z_dm}CxJh!i$}=uv66GRL`4x&GL4HMI zgKW`q1j#(WTT7a@J3p_+31EIJfljNES}}5*2NZZLVCfR!G(FKZ>52p8&}4wglKO4r zTIcSUYI%J1DzlJ{LLn|`G;rcYsND`fye}00NBB(x+9H<@Wsm;RIlw(qBpng)+>gHZ zPk3TBg4ZoqeI7?!Cz1hip)OJhb?vmb@OA1gy%!nS5Z_%%D(W*=Di;dRo9tKixO`|0 z2GS=_iI(>G>LZ;GhbYe(LIR}p1N9iaE2|qlQ{F6yq33qNCk-!nYh$+Rsv%uI{-CgO z#4u|kR3Dzj=CLj)p^wCXZFuLbNQ-}xzE@S-0ao1X*xDCdc!){9pElAyVLpv=> zyzWs4o6E6I`{YQpK5er}Mi)!2Vl^O)&SUO_Uu?>mnzIgkOFR4wgua*BxMJz~getLt z7d7(b;0+PK1=dEdwD8PK#isIf!dtxwAD{mOpbgIT0=HbKcLIQ+s*}L(zg` z8PWSWyzfbQ4-VSo@XbjBe|n(De)k##1vB|8lI)^_T1%hB%Muot+hX)%qWabH%Dzd7 z4KW6uzDb>0H*OWrREZX#A6;&bmzaG%=hCzIzwKiJQYJv3ig4ScrO1&)NEuJY+vHh@2r)2s;m&Wtv)hO& zvW0v$p>M>F(Q;K|W|QA6&9cRkr7P;q;P6v@!qB<)NHeo?1Pf9smkx&)cV zla)2&uU;Mp+A>djBp6-8y`M&)Y(M1?}_PYZ;hB zn2|FSpbuqF1<&|Tww#J=BCeFa6zQ5X( zj_yFZ-I9VRl^15m;(rou1sMG3#Fg)2D(Pbw_s0EhhA%Slmp{Mz+{e=17bCQu$oE~} z>Vo3xnJdqWDW>;g?l>T8oCDY2mdTA;eE<%JmEBs5J>WFGjhx0;t7~gV5YHwuW;mkW z;A(=nPmUV9-3qzdkF*JU8qDvPR(!b?wGmBKw~}oyw<;^&)VR8aZr*7k13S~mf~LB# zC-vKyYzhbBS$L9?bRo-PY^Ppt;i+36Cd4@@)fGMc)^-|nksz7FZkTvF1cWr>+Po5r zyx{(zrGcDe>CcZ5_KLTbY6&(G-#sH>v4|cyok7%=j-%f0hBI${01F)od~!;J00b7M z-o%%aS#G(0(2Tp0jrO!j2FPr7wqkxk36yF6_(X}1ZX8yB*?X4QaW%v%yRNkg4{sKD z+g0_lT0b0EKQuPj8>K@|WilNRT~iE$mwaO*#JZN+ytjN7i$K(dC4dkXZ}DoODxfMQ z9~3p*x|7-9t${+12yXqw^b~w=46WETH0vr?yvhAmKIXkZadekzV!hA*Q7|B^(#ZDU zaq_Ee^OEaTEz~2}4Q4`y7yGuD#o0s^N=mJRWNv9D@Ku~h$XT`Ai5;gjb*q=+smGA! zh}1Pp!+R0wd&IRy4-w*K7W@?L&gM{eNx}tg)KmuT_l}*-Oz{02-$JwBp3x6~>;!3# ztzu59dKWa-Dl{5KD6YvEYwszMVVE-#*%pzG!9vq^e*B?{ar}zc$nlI+D?VEw!H}Ca zfm|Po#5bm~uj+sOHso{PeL$SFx!+o+Y}WxO7cyC4wKa0#WhBE(T=1TMRZvHX1L&a# zGft)1`w2*m;(~eoT`WrayCHW^^M|UM&#(qzc;RtFXdH{PpEvZG+H=ON4mUkLgi!f~ zA}s3FYzaBh^VIsuMPI`U2E_*W4&4@8{g!bFqwyqG_#nro{IAzKZS9IuwOwt>7)XJ+ z_Gst};X9=X6MJwct93#5I_Qs1)Wd_lnX_BsR-5NGYT(c^?Hdw>(Us%XMgKCJEiyX` z7C_xQ)uKa{4>|kcl8l9#km^nSMmf}yq@B~7#uJK~gpzakv&Ps~Gn;37R zR0*B>RKOG@jCBSGa*SJI5v1?s0V5)KUR%~(ds8ER@%K%uuv;{Cz-AM8V{F`I;K zwPwCj1ao!rZyVIbUkPwRRJbU5AH?Oymc?*~1uKb#Ev%~t$CM2{95W1L4$2z6N zmFioLzB&_f!QS^rk4h`u5;Fx6-BzkLYR~$a2|OS^;}P>3vU6D8h7_>))YJYtfUo98 z>{@d5a#cX~3GUr}eT)n_#>v@^{L0z4*X;Ufq)4pABi1D{Pg zrAV&Ksp)pOuJzMLYpJu%ioWk}?Xk&&ILl9{mGE~As(n-oZZQg%Dc*c+_z*)*#{!J9 z8*o=!VopIEEWN)Ll}7|hNJHPP%#pYdU+d5+tx>I+&^8bzTg!BBtDfdnd|+u@~mK1%z~odySxmRn~-P3XiK3Fg@s zpF$6taFVV~Hp0P?a0Pp7CGk}#?$>J8Xt3Kh;L{XzyHI{twTLo+W_3%$LGn0Fg~x$a~iRstE&&T%$gVh2W{X}@Xv zc=8U>o*@P!6sl6(b9tf@B~={YpU&1~H<=tYzkby%!K%$kO{%gvMO_+Cg~5MN)C5DO z(D(cxS!O1=2>vnf3TMOSwCaVCVo)`qZ_hWzh#TzgIbm(1j~YV1D*{j8GOIRgT4fq2 z`Eg_?#IUsl_Q12LWu#%`W^Uf9PM#aWJ?Hnp@f#f-rVLJf9=@GrVa>Vi080UYKt{YQ zjNh{KUa0&9=80tmGc2e=Q(mhp5X|S)61A5XMFHC`oIW?B_u#qds$E4H9%+F4>(nv} zjrw#CG+(;~_1OBk>gTTD_7|CBjCW6X@MAWXFRa3H*PY`n0>qSEWF0cvpt?K6d5-_t zG|CLo=NU#y6Ff_XG)ueMIT8kogu5bPNkbreb+yE+vTG%$j}WWqYx$QG1)LblIrrXfQ7Loa4Fkv*hd( z{ahLu&rx14Tk8ylZBhFlH>y7jA!~@R^b1_S7+$rKDs5)+zqymPY|JL}4c=`h6;-m= zAb%DVR2BL5ZWmo7u{Xm>^WE zDkVD_g!7vxm2U~?!mLbMm}s$?cTQ3fq|jnfu09ESN(L$8!a&sGx~3oEMZ$xI&0|Su zeIVy6gAtMcu1JLezxyMXqi8EF3p+l+=vdnwSw6UlS#lUD<%%+D4b-E;$vTOHRw(qpxv>z&5V{5?Y5Zn#k)Vuc34x-eEaF_rW*5^5 zmA8Av^|k&`&C*)xC_b?Hpo07br}WK9e03zXOd7#nz8ec1zec=Kr!9^M+R&U~-O?;z zn$prDfZ&f5$dc9S!>pY5O2Qr|`F?P}*8JoU-MhCpSzs$OmAm_)ssxwNm6Xba3J6FR z@@`vBQH~)fIXeqZ_cb`s<#8VJrh|ncH>+)KsdGAFHl&POYzMPziWs90?ze9ZYo+o% z>EtQ4{LWIGI;SS`gp)YtMdR#n_$(uma)z<1K?%5jWlczr5VIq9*Qk4ikIf+aiUKv1 zWc3Wt=DC)jPePuH$E94;hE2uE2frVrpt~#o0Wc<3szE`{f%Em2=k~CoD<>bfld}$E znuzCGd8pz?#Rj|Zx)I;WFqSmwFKIa9L{+5Ew$o^qFA(kw^L4-DgeyoLuvj5Tu#2%p zKiN+1P)9(dbc8(d=C*E0wMab>SuZAU^_Sp@iT-NC%IpJB2y1xX=I!nXE0|YMZUFY~ zOyGI$kbLz-3QmS}5@Zv*nDV&pK7~tg(W$wTuHAdP?SbQ8`*tJhcJmn8dHqr%u5cA% zu(SCE;4@p-q|%#H(=%e(?m_MCD?udDP$*dS8irGW~ZyW`yJH|ojP;Ow6G5&ITbuKUJ>?U0Jr(J zy;b&)LN_OHo`w1+1?1iu4;=UizX8( z*=O$C>jl0+?J<((H#ZVww~jM8?(fJ7q^EL>ZSusNh#K5Ir$R+A`Tbnm+u;p6N2vRl zbiq@D*#J}==euzN+J<{$3GVxMZ~@g3*PrWtZ1^S^QvENv%cd)RV^J~pSFHOfBR1n6 z6RU?G=|p3eEeP4Kh?Jc*v+u9b?@){+Bv3JlV99WXK247j>3p5Mqo7sGaPSmlZfEpV zWaa@3Nx#pTQ~hhI1BT?SfU zifQnW#Lh4Y&tisN&O8cH=?&zMo^|?^y(SnlY`}_DRKrjiu-*GeF}58&iO=5jY9ak; zYDJ^nSXo|>C}!F!%xQ=tZK&x$#QgBq-gGelQe!AfyLzzukh*>9rSe)rM-DEnp4sZd z1!D+Jt&^Xev=-N*2KeVHnWv6VJx5FJ;}|66S0*a-F8>Jth-LCnmE6(&&65Fm#UcAq zcmxAD^6xd=^0U1{m! zm}ZJ%N=!~PAj-1-!WO9C-&V`{gT*91&ds*kTSv~XlKw*ss06z>t|Gmhl`jb z8_Z}|xiCH3-BNyf2&Thjw;TO=U`!NQ0%@T`FqG%AUikI7e;sCC0LMn9+Uvav$MaRx zlPRom3|ct;>N+opZZ6`p`J1*7uF4ksl)OqZ=th1VDfRZqiPV4;6E^=^@*FJ2ew}${=Gp`Bw)hRg5P}qTh`2DR zOp=uJNG-afOo8`zvZj|wo1s{wBFPM(dBMm3$~REip5Ok zXDVDb6b+}GTLd+*vlO9PiU;{m&+ahh`oi^)vodtpAv76O4*vk!q{{N(@>b!aGByGx zE`seFYLZd|0j=)iDpfpC2CR+(CpUlL+{A>*^^;Bz%q2{%P)pF@nC5%l*3S7wgy`X@ z*i>Ytvx$24$mxt+ex>T*kf34kjgH-y%CGn}WGA0v|6 z(Id;@b%Bc|h~`CeIyMdo(FQcT9QJ?T&4qvQ!0$4CDV3Y%0td!j1Dnl%Vc6BOvh9qP znkMf$b{NJKFNj=&ap$TS!Z#Z#SV@&G0b?Bc?`HLjG;CKUc_~BQHGFWGOvKJwd$=%| zfVvh;$k=IvJ(yLLzVO1bfnYV=#%Bd~wy^w>ZQsYGrvxn5JVX{+`)T`9mKvi6IFjd4 zF+#n2@!e7XF4cbZ7U@vPrrGMw)UmOb#8j4tlX`LuULD90%it4gfpb?Oq}1b9>_vs2 zXzfXP{9X=~)vd01lP-YDGPCC69G8R{sP*;M5!vz2UNPy#@I>TO0UjYLw0DxHq03dtwnmIbSHN7AM z{jp$j5N%37}2u@BothOTY5A zCOT&T0=~GqTDZA#{3ra~zeI13A`KJt7LLc5#t!;VxYFAiR-6%-rb+=M`k&nY7o7Mn z*pDTx8U&=L{8ye-%X`*5ApoHIZ4mu;%KuCSf#3PlZa_eWe-)5>zNK_=1EvMo0?E?! fJ%OnIyw?A`5+1-bHG3dYTCOJ$6QRZL-_iaHzU*Y5 delta 40097 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 bko|x6^0|OB-*|(`GaH~_Jd|3`=L!EG-ctiA diff --git a/src/content/market/audience-profile-csv.ts b/src/content/market/audience-profile-csv.ts index f0b2a58..a544283 100644 --- a/src/content/market/audience-profile-csv.ts +++ b/src/content/market/audience-profile-csv.ts @@ -1,5 +1,10 @@ import { escapeCsvCell } from "../../shared/csv"; -import { buildMarketCsvColumns, type CsvColumn } from "./csv-exporter"; +import { + buildMarketCsvColumns, + listBackendMetricCsvHeaders, + listRateCsvHeaders, + type CsvColumn +} from "./csv-exporter"; import type { AudienceProfileDistributionItem, AudienceProfileExportRow, @@ -16,6 +21,15 @@ type AudienceProfileCsvColumn = { readValue: (row: AudienceProfileExportRow) => string; }; +export interface AudienceProfileCsvOptions { + selectedHeaders?: string[]; +} + +export type AudienceProfileCsvFieldGroup = { + headers: string[]; + label: string; +}; + const PROFILE_LAYOUTS: Array<{ includeGender: boolean; kind: AudienceProfileKind; @@ -91,14 +105,15 @@ const BUSINESS_ESTIMATE_METRIC_LAYOUTS: Array<{ ]; export function buildAudienceProfileCsv( - rows: AudienceProfileExportRow[] + rows: AudienceProfileExportRow[], + options: AudienceProfileCsvOptions = {} ): string { const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record)); - const csvColumns = [ + const csvColumns = filterAudienceProfileCsvColumns([ ...marketColumns.map(toMarketColumn), ...buildBusinessAbilityColumns(), ...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout)) - ]; + ], options.selectedHeaders); const headerLine = csvColumns.map((column) => column.header).join(","); const rowLines = rows.map((row) => csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",") @@ -107,7 +122,72 @@ export function buildAudienceProfileCsv( 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[] { + return [...buildBusinessVideoColumns(), ...buildBusinessEstimateColumns()]; +} + +function buildBusinessVideoColumns(): AudienceProfileCsvColumn[] { return [ ...BUSINESS_VIDEO_LAYOUTS.flatMap((videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({ @@ -115,7 +195,12 @@ function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] { readValue: (row: AudienceProfileExportRow) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key) })) - ), + ) + ]; +} + +function buildBusinessEstimateColumns(): AudienceProfileCsvColumn[] { + return [ ...BUSINESS_ESTIMATE_LAYOUTS.flatMap((durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({ header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`, diff --git a/src/content/market/audience-profile-field-dialog.ts b/src/content/market/audience-profile-field-dialog.ts new file mode 100644 index 0000000..08c0926 --- /dev/null +++ b/src/content/market/audience-profile-field-dialog.ts @@ -0,0 +1,295 @@ +import type { AudienceProfileCsvFieldGroup } from "./audience-profile-csv"; + +export function promptForAudienceProfileFields( + document: Document, + groups: AudienceProfileCsvFieldGroup[], + selectedHeaders: string[] +): Promise { + 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"; +} diff --git a/src/content/market/csv-exporter.ts b/src/content/market/csv-exporter.ts index 471355f..00718b0 100644 --- a/src/content/market/csv-exporter.ts +++ b/src/content/market/csv-exporter.ts @@ -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 { const csvColumns = buildMarketCsvColumns(records); const headerLine = csvColumns.map((column) => column.header).join(","); diff --git a/src/content/market/index.ts b/src/content/market/index.ts index 964bd35..7058433 100644 --- a/src/content/market/index.ts +++ b/src/content/market/index.ts @@ -1,5 +1,9 @@ import { buildMarketCsv } from "./csv-exporter"; -import { buildAudienceProfileCsv } from "./audience-profile-csv"; +import { + buildAudienceProfileCsv, + listAudienceProfileSelectableFieldGroups, + type AudienceProfileCsvOptions +} from "./audience-profile-csv"; import { AUDIENCE_PROFILE_TARGETS, createAudienceProfileClient, @@ -8,6 +12,7 @@ import { import { createAuthorBaseClient } from "./author-base-client"; import { parseAuthorIds } from "./author-id-input"; import { createBusinessAbilityClient } from "./business-ability-client"; +import { promptForAudienceProfileFields } from "./audience-profile-field-dialog"; import { promptForAuthorIds } from "./author-id-dialog"; import { promptForBatchName } from "./batch-name-dialog"; import { createBatchPayload, type BatchPayload } from "./batch-payload"; @@ -58,7 +63,10 @@ interface MutationObserverLike { } export interface CreateMarketControllerOptions { - buildAudienceProfileCsv?: (rows: AudienceProfileExportRow[]) => string; + buildAudienceProfileCsv?: ( + rows: AudienceProfileExportRow[], + options?: AudienceProfileCsvOptions + ) => string; buildCsv?: (records: MarketRecord[]) => string; document: Document; getAuthState?: () => Promise; @@ -85,6 +93,9 @@ export interface CreateMarketControllerOptions { window: Window; } +const AUDIENCE_PROFILE_FIELD_SELECTION_STORAGE_KEY = + "sces:audience-profile:selectedHeaders"; + export function createMarketController(options: CreateMarketControllerOptions) { const marketApiClient = createMarketApiClient(); const audienceProfileClient = createAudienceProfileClient(); @@ -266,7 +277,12 @@ export function createMarketController(options: CreateMarketControllerOptions) { return; } - options.onCsvReady?.(buildAudienceCsv(rows), buildAudienceProfileFilename()); + options.onCsvReady?.( + buildAudienceCsv(rows, { + selectedHeaders: readAudienceProfileSelectedHeaders() + }), + buildAudienceProfileFilename() + ); setToolbarExportStatus(toolbar, ""); } catch (error) { setToolbarExportStatus( @@ -313,7 +329,9 @@ export function createMarketController(options: CreateMarketControllerOptions) { } options.onCsvReady?.( - buildAudienceCsv(rows), + buildAudienceCsv(rows, { + selectedHeaders: readAudienceProfileSelectedHeaders() + }), buildAudienceProfileFilename(new Date(), "按ID导出") ); setToolbarExportStatus(toolbar, ""); @@ -326,6 +344,24 @@ export function createMarketController(options: CreateMarketControllerOptions) { 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 () => { syncSelectionStateFromDom(); const exportTarget = readToolbarExportTarget(toolbar); @@ -909,6 +945,55 @@ export function createMarketController(options: CreateMarketControllerOptions) { 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 { await runSyncCycle(); await harvestCurrentPageForExport(); diff --git a/src/content/market/plugin-toolbar.ts b/src/content/market/plugin-toolbar.ts index 3de6348..97c9e18 100644 --- a/src/content/market/plugin-toolbar.ts +++ b/src/content/market/plugin-toolbar.ts @@ -7,12 +7,14 @@ export interface PluginToolbarHandlers { onExport(): Promise | void; onExportAudienceProfile(): Promise | void; onExportAudienceProfileByIds(): Promise | void; + onConfigureAudienceProfileFields(): Promise | void; onSubmitBatch(): Promise | void; } export interface PluginToolbarDom { audienceProfileByIdExportButton: HTMLButtonElement; audienceProfileExportButton: HTMLButtonElement; + audienceProfileFieldButton: HTMLButtonElement; batchSubmitButton: HTMLButtonElement; exportButton: HTMLButtonElement; exportCustomPagesInput: HTMLInputElement; @@ -89,6 +91,11 @@ export function ensurePluginToolbar( audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button"; audienceProfileByIdExportButton.textContent = "按ID导出画像CSV"; + const audienceProfileFieldButton = document.createElement("button"); + audienceProfileFieldButton.type = "button"; + audienceProfileFieldButton.dataset.pluginAudienceProfileFields = "button"; + audienceProfileFieldButton.textContent = "画像字段"; + const batchSubmitButton = document.createElement("button"); batchSubmitButton.type = "button"; batchSubmitButton.dataset.pluginBatchSubmit = "button"; @@ -104,6 +111,7 @@ export function ensurePluginToolbar( exportButton, audienceProfileExportButton, audienceProfileByIdExportButton, + audienceProfileFieldButton, batchSubmitButton, exportStatusText ); @@ -112,6 +120,7 @@ export function ensurePluginToolbar( applyNativeControlStyles(document, { audienceProfileExportButton, audienceProfileByIdExportButton, + audienceProfileFieldButton, batchSubmitButton, exportButton, exportCustomPagesInput, @@ -128,12 +137,16 @@ export function ensurePluginToolbar( audienceProfileByIdExportButton.addEventListener("click", () => { void handlers.onExportAudienceProfileByIds(); }); + audienceProfileFieldButton.addEventListener("click", () => { + void handlers.onConfigureAudienceProfileFields(); + }); batchSubmitButton.addEventListener("click", () => { void handlers.onSubmitBatch(); }); exportRangeSelect.addEventListener("change", () => { syncCustomPagesInputVisibility({ batchSubmitButton, + audienceProfileFieldButton, audienceProfileByIdExportButton, audienceProfileExportButton, exportButton, @@ -147,6 +160,7 @@ export function ensurePluginToolbar( const toolbarDom = { audienceProfileExportButton, audienceProfileByIdExportButton, + audienceProfileFieldButton, batchSubmitButton, exportButton, exportCustomPagesInput, @@ -178,6 +192,9 @@ function readToolbarDom(root: HTMLElement): PluginToolbarDom { audienceProfileExportButton: root.querySelector( '[data-plugin-export-audience-profile="button"]' ) as HTMLButtonElement, + audienceProfileFieldButton: root.querySelector( + '[data-plugin-audience-profile-fields="button"]' + ) as HTMLButtonElement, batchSubmitButton: root.querySelector( '[data-plugin-batch-submit="button"]' ) as HTMLButtonElement, @@ -260,6 +277,7 @@ export function setToolbarBusyState( ): void { [ toolbar.batchSubmitButton, + toolbar.audienceProfileFieldButton, toolbar.audienceProfileByIdExportButton, toolbar.audienceProfileExportButton, toolbar.exportButton, @@ -460,6 +478,7 @@ function applyNativeControlStyles( controls: { audienceProfileExportButton: HTMLButtonElement; audienceProfileByIdExportButton: HTMLButtonElement; + audienceProfileFieldButton: HTMLButtonElement; batchSubmitButton: HTMLButtonElement; exportButton: HTMLButtonElement; exportCustomPagesInput: HTMLInputElement; @@ -478,6 +497,7 @@ function applyNativeControlStyles( controls.exportButton.className = nativeButton.className; controls.audienceProfileExportButton.className = nativeButton.className; controls.audienceProfileByIdExportButton.className = nativeButton.className; + controls.audienceProfileFieldButton.className = nativeButton.className; controls.batchSubmitButton.className = nativeButton.className; } @@ -485,6 +505,7 @@ function applyNativeControlStyles( controls.exportButton, controls.audienceProfileExportButton, controls.audienceProfileByIdExportButton, + controls.audienceProfileFieldButton, controls.batchSubmitButton ].forEach((button) => { applyPrimaryButtonStyles(button); @@ -539,6 +560,7 @@ function ensurePluginActionButtonTheme(document: Document): void { [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-audience-profile-fields="button"]:hover:not(:disabled), [data-plugin-batch-submit="button"]:hover:not(:disabled) { background-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-audience-profile="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) { background-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-audience-profile="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 { outline: none !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-audience-profile="button"]:disabled, [data-plugin-export-audience-profile-by-id="button"]:disabled, + [data-plugin-audience-profile-fields="button"]:disabled, [data-plugin-batch-submit="button"]:disabled { background-color: #c89ca4 !important; border-color: #c89ca4 !important; diff --git a/tests/audience-profile-csv.test.ts b/tests/audience-profile-csv.test.ts index a42c370..e99e67e 100644 --- a/tests/audience-profile-csv.test.ts +++ b/tests/audience-profile-csv.test.ts @@ -1,6 +1,10 @@ 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"; describe("audience-profile-csv", () => { @@ -193,6 +197,83 @@ describe("audience-profile-csv", () => { expect(readCsvValue(csv, "铁粉画像-Z世代占比")).toBe("0%"); 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 { @@ -204,3 +285,47 @@ function readCsvValue(csv: string, header: string): string { expect(index).toBeGreaterThanOrEqual(0); return values[index] ?? ""; } + +function buildSuccessRow( + overrides: Partial = {} +): 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 + } + }; +} diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index ca3db74..4750a1a 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -13,6 +13,7 @@ describe("market-content-entry", () => { document.documentElement.removeAttribute("data-sces-market-rows"); document.documentElement.removeAttribute("data-sces-market-request-snapshot"); document.documentElement.removeAttribute("data-test-page-index"); + window.localStorage.clear(); window.history.replaceState({}, "", "/"); }); @@ -1689,17 +1690,22 @@ describe("market-content-entry", () => { { authorType: 1, source: "fansDistribution" }, { authorType: 5, source: "fansDistribution" } ]); - expect(buildAudienceProfileCsv).toHaveBeenCalledWith([ - { - profiles: { - audience: expect.objectContaining({ status: "success" }), - fans: expect.objectContaining({ status: "success" }), - longtimeFans: expect.objectContaining({ status: "success" }) - }, - businessAbility: expect.objectContaining({ status: "success" }), - record: expect.objectContaining({ authorId: "222" }) - } - ]); + expect(buildAudienceProfileCsv).toHaveBeenCalledWith( + [ + { + profiles: { + audience: expect.objectContaining({ status: "success" }), + fans: expect.objectContaining({ status: "success" }), + longtimeFans: expect.objectContaining({ status: "success" }) + }, + businessAbility: expect.objectContaining({ status: "success" }), + record: expect.objectContaining({ authorId: "222" }) + } + ], + expect.objectContaining({ + selectedHeaders: expect.arrayContaining(["秒思api-看后搜数"]) + }) + ); expect(onCsvReady).toHaveBeenCalledWith( "profile-csv", expect.stringMatching(/^达人连接用户画像_\d{8}_\d{4}\.csv$/) @@ -1790,56 +1796,149 @@ describe("market-content-entry", () => { "6866044569306267651", "7040323176106033165" ]); - expect(buildAudienceProfileCsv).toHaveBeenCalledWith([ - expect.objectContaining({ - record: expect.objectContaining({ - authorId: "6866044569306267651", - authorName: "小九儿", - backendMetrics: expect.objectContaining({ - a3IncreaseCount: "100", - afterViewSearchCount: "300", - afterViewSearchRate: "1.1%", - cpSearch: "10", - cpa3: "30", - newA3Rate: "3.3%" - }), - exportFields: { - 达人ID: "6866044569306267651", - 达人名称: "小九儿", - 导出状态: "成功", - 失败原因: "" - }, - rates: { - personalVideoAfterSearchRate: "12.3%", - singleVideoAfterSearchRate: "7.8%" - } + expect(buildAudienceProfileCsv).toHaveBeenCalledWith( + [ + expect.objectContaining({ + record: expect.objectContaining({ + authorId: "6866044569306267651", + authorName: "小九儿", + backendMetrics: expect.objectContaining({ + a3IncreaseCount: "100", + afterViewSearchCount: "300", + afterViewSearchRate: "1.1%", + cpSearch: "10", + cpa3: "30", + newA3Rate: "3.3%" + }), + exportFields: { + 达人ID: "6866044569306267651", + 达人名称: "小九儿", + 导出状态: "成功", + 失败原因: "" + }, + rates: { + 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({ - 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%" - } - }) + selectedHeaders: expect.arrayContaining(["秒思api-看后搜数"]) }) - ]); + ); expect(onCsvReady).toHaveBeenCalledWith( "profile-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( "selected export keeps a generic loading status while exporting the default paged range", async () => {