siluzan-tso-cli 1.1.35-beta.2 → 1.1.35-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1679 -1633
- package/dist/skill/AGENTS.md +11 -10
- package/dist/skill/SKILL.md +1 -1
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/campaign-create-template.md +15 -15
- package/dist/skill/assets/market-analysis-rules.md +14 -14
- package/dist/skill/assets/pmax-create-template.md +6 -6
- package/dist/skill/assets/pmax-lead-form-template.md +3 -3
- package/dist/skill/references/README.md +21 -21
- package/dist/skill/references/accounts/accounts.md +42 -42
- package/dist/skill/references/analytics/account-analytics.md +46 -39
- package/dist/skill/references/analytics/keyword-planner-workflows.md +14 -13
- package/dist/skill/references/analytics/rag.md +7 -7
- package/dist/skill/references/analytics/reporting.md +1 -1
- package/dist/skill/references/core/agent-conventions.md +20 -20
- package/dist/skill/references/core/intent-routing.md +116 -117
- package/dist/skill/references/core/subagent-orchestration.md +1 -1
- package/dist/skill/references/core/workflows.md +12 -12
- package/dist/skill/references/google-ads/google-ads-campaign-plan.md +29 -31
- package/dist/skill/references/google-ads/google-ads.md +7 -7
- package/dist/skill/references/google-ads/rules/google-ads-keyword-taxonomy.md +14 -14
- package/dist/skill/references/operations/hosted-automation-optimize-index.md +2 -2
- package/dist/skill/references/operations/hosted-automation-scenarios.md +2 -2
- package/dist/skill/references/operations/hosted-automation-self-control.md +6 -6
- package/dist/skill/references/operations/hosted-automation-user-catalog.md +17 -17
- package/dist/skill/references/report-templates/README.md +68 -0
- package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +2 -2
- package/dist/skill/references/report-templates/bing-period-report.md +53 -53
- package/dist/skill/references/report-templates/google-ads-diagnosis.md +28 -2
- package/dist/skill/references/report-templates/google-inquiry-analysis.md +8 -8
- package/dist/skill/references/report-templates/google-period-report-excel.md +2 -2
- package/dist/skill/references/report-templates/okki-weekly-google-client.md +55 -55
- package/dist/skill/references/report-templates/tiktok-period-report.md +22 -21
- package/dist/skill/report-templates/GoogleAdsDiagnosisReport.html +390 -892
- package/dist/skill/report-templates/REPORT-WORKFLOW.md +2 -2
- package/dist/skill/report-templates/bing-period-report.md +53 -53
- package/dist/skill/report-templates/google-ads-diagnosis.md +28 -2
- package/dist/skill/report-templates/google-inquiry-analysis.md +8 -8
- package/dist/skill/report-templates/google-period-report-excel.md +2 -2
- package/dist/skill/report-templates/meta-period-report.html +1 -4
- package/dist/skill/report-templates/okki-weekly-google-client.md +55 -55
- package/dist/skill/report-templates/report-template-academic.html +0 -2
- package/dist/skill/report-templates/report-template.html +0 -2
- package/dist/skill/report-templates/tiktok-period-report.md +22 -21
- package/dist/skill/report-templates/website-diagnosis-report.html +1 -5
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +2 -14
- package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +1 -3
- package/eval/cases/accounts-single-balance-not-bulk.scenario.json +3 -14
- package/eval/cases/budget-display-not-raw-micros.scenario.json +1 -8
- package/eval/cases/clue-meta-leads-json.scenario.json +2 -14
- package/eval/cases/clue-tiktok-leads-json.scenario.json +2 -11
- package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +3 -9
- package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +3 -9
- package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +3 -9
- package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +2 -11
- package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +3 -10
- package/eval/cases/facebook-analysis-period-default.scenario.json +2 -14
- package/eval/cases/finance-invoice-info-list.scenario.json +3 -11
- package/eval/cases/forewarning-list-google.scenario.json +3 -14
- package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +2 -6
- package/eval/cases/google-analysis-keywords-route.scenario.json +2 -14
- package/eval/cases/human-p1-multiturn.scenario.json +1 -5
- package/eval/cases/ir-p1-vs-p8-account-vs-url.scenario.json +5 -17
- package/eval/cases/ir-p4-period-not-diagnosis.scenario.json +3 -16
- package/eval/cases/ir-p8-network-diagnosis-with-url.scenario.json +3 -25
- package/eval/cases/ir-p8-website-monitor-report-with-url.scenario.json +3 -28
- package/eval/cases/ir-p8-website-test-report-no-url.scenario.json +3 -14
- package/eval/cases/ir-p9-industry-not-p8.scenario.json +3 -19
- package/eval/cases/ir-vague-report-clarify.scenario.json +3 -14
- package/eval/cases/ir-w3-vs-p8-from-website.scenario.json +2 -8
- package/eval/cases/ir-w5-keyword-planner-url-core-terms.scenario.json +2 -22
- package/eval/cases/meta-single-balance-not-bulk.scenario.json +3 -17
- package/eval/cases/no-legacy-json-flag.scenario.json +2 -6
- package/eval/cases/open-account-google-noninteractive.scenario.json +1 -3
- package/eval/cases/open-account-tiktok-license-file.scenario.json +1 -3
- package/eval/cases/optimize-list-by-account.scenario.json +3 -11
- package/eval/cases/p1-single-account-profile.scenario.json +1 -11
- package/eval/cases/p2-balance-scan-bulk.scenario.json +2 -9
- package/eval/cases/p3-accounts-digest.scenario.json +1 -5
- package/eval/cases/p4-fb-meta-period-report.scenario.json +2 -12
- package/eval/cases/p4-period-report-window.scenario.json +1 -8
- package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +2 -13
- package/eval/cases/pmax-brand-edit-routing.scenario.json +3 -16
- package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +2 -12
- package/eval/cases/pmax-enable-brand-guidelines.scenario.json +2 -12
- package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +3 -14
- package/eval/cases/rag-before-keyword-expand.scenario.json +1 -11
- package/eval/cases/rag-list-then-query.scenario.json +2 -14
- package/eval/cases/report-list-google.scenario.json +2 -11
- package/eval/cases/report-push-list-google.scenario.json +2 -11
- package/eval/cases/setup-login-or-env.scenario.json +1 -3
- package/eval/cases/setup-siluzan-data-permission-env.scenario.json +1 -3
- package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +2 -6
- package/eval/cases/time-range-user-delegates-default.scenario.json +1 -8
- package/eval/cases/tips-json-out-filtering.scenario.json +1 -3
- package/eval/cases/tips-large-json-pagination.scenario.json +1 -3
- package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +2 -11
- package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +1 -3
- package/eval/cases/uj-ad-website-search-plan-table-format.scenario.json +1 -3
- package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +6 -18
- package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +1 -8
- package/eval/cases/uj-analytics-ecommerce-industry-report.scenario.json +2 -14
- package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +2 -11
- package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +1 -3
- package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +2 -11
- package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +1 -4
- package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +2 -17
- package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +2 -14
- package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +2 -6
- package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +3 -17
- package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +1 -5
- package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +1 -3
- package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +3 -17
- package/eval/cases/uj-patrol-google-balances-low.scenario.json +2 -11
- package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +3 -14
- package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +2 -14
- package/package.json +1 -1
|
@@ -127,11 +127,7 @@
|
|
|
127
127
|
height: 3px;
|
|
128
128
|
margin-left: 18px;
|
|
129
129
|
border-radius: 999px;
|
|
130
|
-
background: linear-gradient(
|
|
131
|
-
135deg,
|
|
132
|
-
rgba(59, 104, 249, 0.4),
|
|
133
|
-
rgba(99, 102, 241, 0.2)
|
|
134
|
-
);
|
|
130
|
+
background: linear-gradient(135deg, rgba(59, 104, 249, 0.4), rgba(99, 102, 241, 0.2));
|
|
135
131
|
}
|
|
136
132
|
|
|
137
133
|
.section-subtitle {
|
|
@@ -1095,11 +1091,7 @@
|
|
|
1095
1091
|
.targeting-dimension {
|
|
1096
1092
|
padding: 24px;
|
|
1097
1093
|
border-radius: 16px;
|
|
1098
|
-
background: linear-gradient(
|
|
1099
|
-
145deg,
|
|
1100
|
-
rgba(59, 104, 249, 0.05),
|
|
1101
|
-
rgba(91, 143, 255, 0.08)
|
|
1102
|
-
);
|
|
1094
|
+
background: linear-gradient(145deg, rgba(59, 104, 249, 0.05), rgba(91, 143, 255, 0.08));
|
|
1103
1095
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
1104
1096
|
box-shadow: 0 16px 32px -24px rgba(15, 23, 42, 0.35);
|
|
1105
1097
|
}
|
|
@@ -1269,10 +1261,7 @@
|
|
|
1269
1261
|
margin-bottom: 15px;
|
|
1270
1262
|
}
|
|
1271
1263
|
|
|
1272
|
-
.content-wrapper[data-section-id]
|
|
1273
|
-
.diagnosis-overview
|
|
1274
|
-
.column-header
|
|
1275
|
-
.section-subtitle {
|
|
1264
|
+
.content-wrapper[data-section-id] .diagnosis-overview .column-header .section-subtitle {
|
|
1276
1265
|
font-size: 18px;
|
|
1277
1266
|
margin: 0;
|
|
1278
1267
|
line-height: 1.4;
|
|
@@ -1301,10 +1290,7 @@
|
|
|
1301
1290
|
background: #f3e8ff;
|
|
1302
1291
|
}
|
|
1303
1292
|
|
|
1304
|
-
.content-wrapper[data-section-id]
|
|
1305
|
-
.diagnosis-overview
|
|
1306
|
-
.list-item
|
|
1307
|
-
.item-number {
|
|
1293
|
+
.content-wrapper[data-section-id] .diagnosis-overview .list-item .item-number {
|
|
1308
1294
|
font-size: 14px;
|
|
1309
1295
|
font-weight: 600;
|
|
1310
1296
|
color: #7c3aed;
|
|
@@ -1313,30 +1299,21 @@
|
|
|
1313
1299
|
padding-top: 2px;
|
|
1314
1300
|
}
|
|
1315
1301
|
|
|
1316
|
-
.content-wrapper[data-section-id]
|
|
1317
|
-
.diagnosis-overview
|
|
1318
|
-
.list-item
|
|
1319
|
-
.item-content-wrapper {
|
|
1302
|
+
.content-wrapper[data-section-id] .diagnosis-overview .list-item .item-content-wrapper {
|
|
1320
1303
|
flex: 1;
|
|
1321
1304
|
display: flex;
|
|
1322
1305
|
flex-direction: column;
|
|
1323
1306
|
gap: 6px;
|
|
1324
1307
|
}
|
|
1325
1308
|
|
|
1326
|
-
.content-wrapper[data-section-id]
|
|
1327
|
-
.diagnosis-overview
|
|
1328
|
-
.list-item
|
|
1329
|
-
.item-title {
|
|
1309
|
+
.content-wrapper[data-section-id] .diagnosis-overview .list-item .item-title {
|
|
1330
1310
|
font-size: 15px;
|
|
1331
1311
|
font-weight: 600;
|
|
1332
1312
|
color: #374151;
|
|
1333
1313
|
line-height: 1.5;
|
|
1334
1314
|
}
|
|
1335
1315
|
|
|
1336
|
-
.content-wrapper[data-section-id]
|
|
1337
|
-
.diagnosis-overview
|
|
1338
|
-
.list-item
|
|
1339
|
-
.item-description {
|
|
1316
|
+
.content-wrapper[data-section-id] .diagnosis-overview .list-item .item-description {
|
|
1340
1317
|
font-size: 13px;
|
|
1341
1318
|
color: #6b7280;
|
|
1342
1319
|
line-height: 1.7;
|
|
@@ -1659,12 +1636,7 @@
|
|
|
1659
1636
|
white-space: nowrap;
|
|
1660
1637
|
}
|
|
1661
1638
|
|
|
1662
|
-
.kpi-section
|
|
1663
|
-
.key-metrics-section
|
|
1664
|
-
.table-block
|
|
1665
|
-
.data-table
|
|
1666
|
-
thead
|
|
1667
|
-
th:last-child {
|
|
1639
|
+
.kpi-section .key-metrics-section .table-block .data-table thead th:last-child {
|
|
1668
1640
|
border-right: none;
|
|
1669
1641
|
}
|
|
1670
1642
|
|
|
@@ -1672,12 +1644,7 @@
|
|
|
1672
1644
|
border-bottom: 1px solid #f0f0f0;
|
|
1673
1645
|
}
|
|
1674
1646
|
|
|
1675
|
-
.kpi-section
|
|
1676
|
-
.key-metrics-section
|
|
1677
|
-
.table-block
|
|
1678
|
-
.data-table
|
|
1679
|
-
tbody
|
|
1680
|
-
tr:last-child {
|
|
1647
|
+
.kpi-section .key-metrics-section .table-block .data-table tbody tr:last-child {
|
|
1681
1648
|
border-bottom: none;
|
|
1682
1649
|
}
|
|
1683
1650
|
|
|
@@ -1689,44 +1656,22 @@
|
|
|
1689
1656
|
border-right: 1px solid #f0f0f0;
|
|
1690
1657
|
}
|
|
1691
1658
|
|
|
1692
|
-
.kpi-section
|
|
1693
|
-
.key-metrics-section
|
|
1694
|
-
.table-block
|
|
1695
|
-
.data-table
|
|
1696
|
-
tbody
|
|
1697
|
-
td:last-child {
|
|
1659
|
+
.kpi-section .key-metrics-section .table-block .data-table tbody td:last-child {
|
|
1698
1660
|
border-right: none;
|
|
1699
1661
|
}
|
|
1700
1662
|
|
|
1701
|
-
.kpi-section
|
|
1702
|
-
.key-metrics-section
|
|
1703
|
-
.table-block
|
|
1704
|
-
.data-table
|
|
1705
|
-
tbody
|
|
1706
|
-
td
|
|
1707
|
-
.cell-text {
|
|
1663
|
+
.kpi-section .key-metrics-section .table-block .data-table tbody td .cell-text {
|
|
1708
1664
|
display: inline-block;
|
|
1709
1665
|
max-width: 100%;
|
|
1710
1666
|
white-space: normal;
|
|
1711
1667
|
word-break: break-word;
|
|
1712
1668
|
}
|
|
1713
1669
|
|
|
1714
|
-
.kpi-section
|
|
1715
|
-
.key-metrics-section
|
|
1716
|
-
.table-block
|
|
1717
|
-
.data-table
|
|
1718
|
-
tbody
|
|
1719
|
-
tr:nth-child(even) {
|
|
1670
|
+
.kpi-section .key-metrics-section .table-block .data-table tbody tr:nth-child(even) {
|
|
1720
1671
|
background: #f9fafb;
|
|
1721
1672
|
}
|
|
1722
1673
|
|
|
1723
|
-
.kpi-section
|
|
1724
|
-
.key-metrics-section
|
|
1725
|
-
.table-block
|
|
1726
|
-
.data-table
|
|
1727
|
-
tbody
|
|
1728
|
-
tr:nth-child(even)
|
|
1729
|
-
td {
|
|
1674
|
+
.kpi-section .key-metrics-section .table-block .data-table tbody tr:nth-child(even) td {
|
|
1730
1675
|
background: transparent;
|
|
1731
1676
|
}
|
|
1732
1677
|
|
|
@@ -1755,11 +1700,7 @@
|
|
|
1755
1700
|
padding: 36px 24px;
|
|
1756
1701
|
text-align: center;
|
|
1757
1702
|
color: #64748b;
|
|
1758
|
-
background: linear-gradient(
|
|
1759
|
-
135deg,
|
|
1760
|
-
rgba(226, 232, 240, 0.6),
|
|
1761
|
-
rgba(248, 250, 252, 0.9)
|
|
1762
|
-
);
|
|
1703
|
+
background: linear-gradient(135deg, rgba(226, 232, 240, 0.6), rgba(248, 250, 252, 0.9));
|
|
1763
1704
|
border: 1px dashed rgba(148, 163, 184, 0.6);
|
|
1764
1705
|
border-radius: 12px;
|
|
1765
1706
|
font-size: 14px;
|
|
@@ -1921,8 +1862,7 @@
|
|
|
1921
1862
|
|
|
1922
1863
|
#report-root {
|
|
1923
1864
|
padding: max(8px, env(safe-area-inset-left, 0px))
|
|
1924
|
-
max(8px, env(safe-area-inset-right, 0px))
|
|
1925
|
-
max(28px, env(safe-area-inset-bottom, 0px));
|
|
1865
|
+
max(8px, env(safe-area-inset-right, 0px)) max(28px, env(safe-area-inset-bottom, 0px));
|
|
1926
1866
|
max-width: 100%;
|
|
1927
1867
|
}
|
|
1928
1868
|
|
|
@@ -2049,9 +1989,10 @@
|
|
|
2049
1989
|
min-width: 520px;
|
|
2050
1990
|
}
|
|
2051
1991
|
|
|
2052
|
-
.chart-card
|
|
2053
|
-
.chart-container
|
|
1992
|
+
.chart-card .echarts-chart,
|
|
1993
|
+
.chart-container .echarts-chart {
|
|
2054
1994
|
max-width: 100%;
|
|
1995
|
+
width: 100%;
|
|
2055
1996
|
}
|
|
2056
1997
|
|
|
2057
1998
|
.insights-container,
|
|
@@ -2087,10 +2028,7 @@
|
|
|
2087
2028
|
gap: 15px;
|
|
2088
2029
|
}
|
|
2089
2030
|
|
|
2090
|
-
.content-wrapper[data-section-id]
|
|
2091
|
-
.diagnosis-overview
|
|
2092
|
-
.column-header
|
|
2093
|
-
.section-subtitle {
|
|
2031
|
+
.content-wrapper[data-section-id] .diagnosis-overview .column-header .section-subtitle {
|
|
2094
2032
|
font-size: 16px;
|
|
2095
2033
|
}
|
|
2096
2034
|
|
|
@@ -2106,15 +2044,12 @@
|
|
|
2106
2044
|
font-size: 14px;
|
|
2107
2045
|
}
|
|
2108
2046
|
|
|
2109
|
-
.content-wrapper[data-section-id]
|
|
2110
|
-
.diagnosis-overview
|
|
2111
|
-
.item-description {
|
|
2047
|
+
.content-wrapper[data-section-id] .diagnosis-overview .item-description {
|
|
2112
2048
|
font-size: 13px;
|
|
2113
2049
|
}
|
|
2114
2050
|
}
|
|
2115
|
-
|
|
2116
2051
|
</style>
|
|
2117
|
-
<script src="https://staticpn.siluzan.com/assets/slz/homeCDN/
|
|
2052
|
+
<script src="https://staticpn.siluzan.com/assets/slz/homeCDN/echarts.js"></script>
|
|
2118
2053
|
</head>
|
|
2119
2054
|
<body>
|
|
2120
2055
|
<div id="report-root">
|
|
@@ -2174,8 +2109,7 @@
|
|
|
2174
2109
|
|
|
2175
2110
|
const suggestions = {
|
|
2176
2111
|
转化追踪设置: "请设置转化追踪,这是衡量广告效果的基础",
|
|
2177
|
-
"G A与广告账户联结":
|
|
2178
|
-
"请将Google Analytics与Google Ads账户关联,以便更好地分析用户行为",
|
|
2112
|
+
"G A与广告账户联结": "请将Google Analytics与Google Ads账户关联,以便更好地分析用户行为",
|
|
2179
2113
|
文字广告: "请创建文字广告,这是搜索广告的基础形式",
|
|
2180
2114
|
自适应搜索广告: "请使用自适应搜索广告,能够自动优化广告展示",
|
|
2181
2115
|
自适应展示广告: "请使用自适应展示广告,提高展示网络的效果",
|
|
@@ -2329,29 +2263,23 @@
|
|
|
2329
2263
|
score: 75,
|
|
2330
2264
|
};
|
|
2331
2265
|
|
|
2332
|
-
const desktopFirstContentfulPaint =
|
|
2333
|
-
Number(desktop.firstContentfulPaint) || 0;
|
|
2266
|
+
const desktopFirstContentfulPaint = Number(desktop.firstContentfulPaint) || 0;
|
|
2334
2267
|
const desktopPassed =
|
|
2335
2268
|
desktopFirstContentfulPaint > 0 &&
|
|
2336
|
-
desktopFirstContentfulPaint <=
|
|
2337
|
-
healthStandards.desktopFirstContentfulPaint;
|
|
2269
|
+
desktopFirstContentfulPaint <= healthStandards.desktopFirstContentfulPaint;
|
|
2338
2270
|
|
|
2339
|
-
const mobileFirstContentfulPaint =
|
|
2340
|
-
Number(mobile.firstContentfulPaint) || 0;
|
|
2271
|
+
const mobileFirstContentfulPaint = Number(mobile.firstContentfulPaint) || 0;
|
|
2341
2272
|
const mobilePassed =
|
|
2342
2273
|
mobileFirstContentfulPaint > 0 &&
|
|
2343
|
-
mobileFirstContentfulPaint <=
|
|
2344
|
-
healthStandards.mobileFirstContentfulPaint;
|
|
2274
|
+
mobileFirstContentfulPaint <= healthStandards.mobileFirstContentfulPaint;
|
|
2345
2275
|
|
|
2346
2276
|
const mobileScore = Number(mobile.score) || 0;
|
|
2347
2277
|
const mobileScorePassed = mobileScore >= healthStandards.score;
|
|
2348
2278
|
|
|
2349
2279
|
const totalMetrics = 3;
|
|
2350
|
-
const passedMetrics = [
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
mobileScorePassed,
|
|
2354
|
-
].filter(Boolean).length;
|
|
2280
|
+
const passedMetrics = [desktopPassed, mobilePassed, mobileScorePassed].filter(
|
|
2281
|
+
Boolean,
|
|
2282
|
+
).length;
|
|
2355
2283
|
const passRate = `${((passedMetrics / totalMetrics) * 100).toFixed(1)}%`;
|
|
2356
2284
|
|
|
2357
2285
|
return [
|
|
@@ -2461,9 +2389,7 @@
|
|
|
2461
2389
|
metrics: { limit: 10 },
|
|
2462
2390
|
conversionGoals: {
|
|
2463
2391
|
getLimit: (items = []) => {
|
|
2464
|
-
const enabledCount = items.filter((goal) =>
|
|
2465
|
-
isEnabledStatus(goal?.status),
|
|
2466
|
-
).length;
|
|
2392
|
+
const enabledCount = items.filter((goal) => isEnabledStatus(goal?.status)).length;
|
|
2467
2393
|
if (enabledCount > 0) {
|
|
2468
2394
|
return Math.min(10, enabledCount);
|
|
2469
2395
|
}
|
|
@@ -2559,10 +2485,7 @@
|
|
|
2559
2485
|
`;
|
|
2560
2486
|
};
|
|
2561
2487
|
|
|
2562
|
-
const buildTableRows = (
|
|
2563
|
-
items,
|
|
2564
|
-
{ tableKey, configKey, emptyRow, rowBuilder },
|
|
2565
|
-
) => {
|
|
2488
|
+
const buildTableRows = (items, { tableKey, configKey, emptyRow, rowBuilder }) => {
|
|
2566
2489
|
const list = Array.isArray(items) ? items.slice() : [];
|
|
2567
2490
|
if (!list.length) {
|
|
2568
2491
|
registerTableState(tableKey, {
|
|
@@ -2579,13 +2502,9 @@
|
|
|
2579
2502
|
};
|
|
2580
2503
|
}
|
|
2581
2504
|
|
|
2582
|
-
const { displayItems, remainingItems } = applyTableLimit(
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
{
|
|
2586
|
-
configKey: configKey || tableKey,
|
|
2587
|
-
},
|
|
2588
|
-
);
|
|
2505
|
+
const { displayItems, remainingItems } = applyTableLimit(list, tableKey, {
|
|
2506
|
+
configKey: configKey || tableKey,
|
|
2507
|
+
});
|
|
2589
2508
|
const limitedRows = displayItems.map(rowBuilder).join("");
|
|
2590
2509
|
const remainingRows = remainingItems.map(rowBuilder).join("");
|
|
2591
2510
|
const remainingCount = remainingItems.length;
|
|
@@ -2599,9 +2518,7 @@
|
|
|
2599
2518
|
|
|
2600
2519
|
return {
|
|
2601
2520
|
bodyHtml: limitedRows || emptyRow,
|
|
2602
|
-
expandHtml: remainingCount
|
|
2603
|
-
? buildExpandButton(tableKey, remainingCount)
|
|
2604
|
-
: "",
|
|
2521
|
+
expandHtml: remainingCount ? buildExpandButton(tableKey, remainingCount) : "",
|
|
2605
2522
|
hasMore: remainingCount > 0,
|
|
2606
2523
|
remainingCount,
|
|
2607
2524
|
};
|
|
@@ -2637,8 +2554,7 @@
|
|
|
2637
2554
|
|
|
2638
2555
|
const getItemLabel = (item, key, fallback = "未命名") => {
|
|
2639
2556
|
const value = item?.[key];
|
|
2640
|
-
if (value === null || value === undefined || value === "")
|
|
2641
|
-
return fallback;
|
|
2557
|
+
if (value === null || value === undefined || value === "") return fallback;
|
|
2642
2558
|
return String(value);
|
|
2643
2559
|
};
|
|
2644
2560
|
|
|
@@ -2652,14 +2568,8 @@
|
|
|
2652
2568
|
impressions: Number(item?.impressions) || 0,
|
|
2653
2569
|
}));
|
|
2654
2570
|
|
|
2655
|
-
const totalCost = numericItems.reduce(
|
|
2656
|
-
|
|
2657
|
-
0,
|
|
2658
|
-
);
|
|
2659
|
-
const totalConversions = numericItems.reduce(
|
|
2660
|
-
(sum, item) => sum + item.conversions,
|
|
2661
|
-
0,
|
|
2662
|
-
);
|
|
2571
|
+
const totalCost = numericItems.reduce((sum, item) => sum + item.cost, 0);
|
|
2572
|
+
const totalConversions = numericItems.reduce((sum, item) => sum + item.conversions, 0);
|
|
2663
2573
|
const topCost = numericItems.reduce(
|
|
2664
2574
|
(max, item) => (item.cost > max.cost ? item : max),
|
|
2665
2575
|
numericItems[0],
|
|
@@ -2675,9 +2585,7 @@
|
|
|
2675
2585
|
const insights = [];
|
|
2676
2586
|
if (totalCost > 0 && topCost) {
|
|
2677
2587
|
const share = ((topCost.cost / totalCost) * 100).toFixed(1);
|
|
2678
|
-
insights.push(
|
|
2679
|
-
`预算主要集中在 ${topCost.label},占总消耗约 ${share}%。`,
|
|
2680
|
-
);
|
|
2588
|
+
insights.push(`预算主要集中在 ${topCost.label},占总消耗约 ${share}%。`);
|
|
2681
2589
|
}
|
|
2682
2590
|
if (totalConversions > 0) {
|
|
2683
2591
|
const topConversion = numericItems.reduce(
|
|
@@ -2711,10 +2619,7 @@
|
|
|
2711
2619
|
ctr: Number(item?.ctr) || 0,
|
|
2712
2620
|
conversions: Number(item?.conversions) || 0,
|
|
2713
2621
|
}));
|
|
2714
|
-
const totalCost = numericItems.reduce(
|
|
2715
|
-
(sum, item) => sum + item.cost,
|
|
2716
|
-
0,
|
|
2717
|
-
);
|
|
2622
|
+
const totalCost = numericItems.reduce((sum, item) => sum + item.cost, 0);
|
|
2718
2623
|
const suggestions = [];
|
|
2719
2624
|
if (totalCost > 0) {
|
|
2720
2625
|
const topRegion = numericItems.reduce(
|
|
@@ -2733,9 +2638,7 @@
|
|
|
2733
2638
|
numericItems[0],
|
|
2734
2639
|
);
|
|
2735
2640
|
if (highCtrRegion && highCtrRegion.ctr > 0) {
|
|
2736
|
-
suggestions.push(
|
|
2737
|
-
`${highCtrRegion.label} CTR 表现领先,可适当增加优质素材投放与预算。`,
|
|
2738
|
-
);
|
|
2641
|
+
suggestions.push(`${highCtrRegion.label} CTR 表现领先,可适当增加优质素材投放与预算。`);
|
|
2739
2642
|
}
|
|
2740
2643
|
const zeroConversionRegions = numericItems.filter(
|
|
2741
2644
|
(item) => item.cost > 0 && item.conversions === 0,
|
|
@@ -2839,11 +2742,7 @@
|
|
|
2839
2742
|
(min, item) => (item.cpa > 0 && item.cpa < min.cpa ? item : min),
|
|
2840
2743
|
{ ...numericItems[0], cpa: numericItems[0].cpa || Infinity },
|
|
2841
2744
|
);
|
|
2842
|
-
if (
|
|
2843
|
-
lowestCpa &&
|
|
2844
|
-
Number.isFinite(lowestCpa.cpa) &&
|
|
2845
|
-
lowestCpa.cpa > 0
|
|
2846
|
-
) {
|
|
2745
|
+
if (lowestCpa && Number.isFinite(lowestCpa.cpa) && lowestCpa.cpa > 0) {
|
|
2847
2746
|
insights.push(
|
|
2848
2747
|
`${lowestCpa.label} CPA 控制得当(${formatNumber(lowestCpa.cpa, 2)}),适合作为核心受众持续投入。`,
|
|
2849
2748
|
);
|
|
@@ -2873,18 +2772,12 @@
|
|
|
2873
2772
|
label: "",
|
|
2874
2773
|
cpa: Infinity,
|
|
2875
2774
|
});
|
|
2876
|
-
if (
|
|
2877
|
-
bestCpa &&
|
|
2878
|
-
Number.isFinite(bestCpa.cpa) &&
|
|
2879
|
-
bestCpa.cpa !== Infinity
|
|
2880
|
-
) {
|
|
2775
|
+
if (bestCpa && Number.isFinite(bestCpa.cpa) && bestCpa.cpa !== Infinity) {
|
|
2881
2776
|
suggestions.push(
|
|
2882
2777
|
`对 ${bestCpa.label} 受众保持预算倾斜,其 CPA 为 ${formatNumber(bestCpa.cpa, 2)},具备扩量空间。`,
|
|
2883
2778
|
);
|
|
2884
2779
|
}
|
|
2885
|
-
const zeroConversion = numericItems.filter(
|
|
2886
|
-
(item) => item.conversions === 0,
|
|
2887
|
-
);
|
|
2780
|
+
const zeroConversion = numericItems.filter((item) => item.conversions === 0);
|
|
2888
2781
|
if (zeroConversion.length) {
|
|
2889
2782
|
suggestions.push(
|
|
2890
2783
|
`建议定期清理 ${zeroConversion.length} 类零转化受众,并利用否定受众减少无效曝光。`,
|
|
@@ -2957,102 +2850,48 @@
|
|
|
2957
2850
|
const dataPoints = items.map((item) => Number(item?.cost) || 0);
|
|
2958
2851
|
if (!dataPoints.some((value) => value > 0)) return null;
|
|
2959
2852
|
return {
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
data: dataPoints,
|
|
2966
|
-
backgroundColor: [
|
|
2967
|
-
"#3b68f9",
|
|
2968
|
-
"#5b8fff",
|
|
2969
|
-
"#7ba7ff",
|
|
2970
|
-
"#9db9ff",
|
|
2971
|
-
"#bfcbff",
|
|
2972
|
-
"#dce5ff",
|
|
2973
|
-
],
|
|
2974
|
-
borderColor: "#ffffff",
|
|
2975
|
-
borderWidth: 2,
|
|
2976
|
-
},
|
|
2977
|
-
],
|
|
2853
|
+
color: ["#3b68f9", "#5b8fff", "#7ba7ff", "#9db9ff", "#bfcbff", "#dce5ff"],
|
|
2854
|
+
title: {
|
|
2855
|
+
text: "地理位置消费分布",
|
|
2856
|
+
left: "center",
|
|
2857
|
+
textStyle: { fontSize: 16, fontWeight: "bold" },
|
|
2978
2858
|
},
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
},
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
text: "地理位置消费分布",
|
|
2990
|
-
font: { size: 16, weight: "bold" },
|
|
2991
|
-
},
|
|
2859
|
+
tooltip: { trigger: "item" },
|
|
2860
|
+
legend: { bottom: 0, type: "scroll" },
|
|
2861
|
+
series: [
|
|
2862
|
+
{
|
|
2863
|
+
type: "pie",
|
|
2864
|
+
radius: ["40%", "70%"],
|
|
2865
|
+
center: ["50%", "48%"],
|
|
2866
|
+
itemStyle: { borderColor: "#ffffff", borderWidth: 2 },
|
|
2867
|
+
label: { show: false },
|
|
2868
|
+
data: labels.map((name, index) => ({ name, value: dataPoints[index] })),
|
|
2992
2869
|
},
|
|
2993
|
-
|
|
2870
|
+
],
|
|
2994
2871
|
};
|
|
2995
2872
|
};
|
|
2996
2873
|
|
|
2997
2874
|
const buildDeviceChartConfig = (items) => {
|
|
2998
2875
|
if (!Array.isArray(items) || !items.length) return null;
|
|
2999
|
-
const labels = items.map((item) =>
|
|
3000
|
-
getItemLabel(item, "deviceType", "未识别设备"),
|
|
3001
|
-
);
|
|
2876
|
+
const labels = items.map((item) => getItemLabel(item, "deviceType", "未识别设备"));
|
|
3002
2877
|
const costData = items.map((item) => Number(item?.cost) || 0);
|
|
3003
|
-
const conversionData = items.map(
|
|
3004
|
-
|
|
3005
|
-
);
|
|
3006
|
-
if (
|
|
3007
|
-
!costData.some((v) => v > 0) &&
|
|
3008
|
-
!conversionData.some((v) => v > 0)
|
|
3009
|
-
)
|
|
3010
|
-
return null;
|
|
2878
|
+
const conversionData = items.map((item) => Number(item?.conversions) || 0);
|
|
2879
|
+
if (!costData.some((v) => v > 0) && !conversionData.some((v) => v > 0)) return null;
|
|
3011
2880
|
return {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
{
|
|
3017
|
-
label: "消费",
|
|
3018
|
-
data: costData,
|
|
3019
|
-
backgroundColor: "#3b68f9",
|
|
3020
|
-
borderColor: "#3b68f9",
|
|
3021
|
-
borderWidth: 1,
|
|
3022
|
-
},
|
|
3023
|
-
{
|
|
3024
|
-
label: "转化次数",
|
|
3025
|
-
data: conversionData,
|
|
3026
|
-
backgroundColor: "#5b8fff",
|
|
3027
|
-
borderColor: "#5b8fff",
|
|
3028
|
-
borderWidth: 1,
|
|
3029
|
-
},
|
|
3030
|
-
],
|
|
3031
|
-
},
|
|
3032
|
-
options: {
|
|
3033
|
-
responsive: true,
|
|
3034
|
-
maintainAspectRatio: false,
|
|
3035
|
-
plugins: {
|
|
3036
|
-
legend: {
|
|
3037
|
-
position: "top",
|
|
3038
|
-
labels: { usePointStyle: true },
|
|
3039
|
-
},
|
|
3040
|
-
title: {
|
|
3041
|
-
display: true,
|
|
3042
|
-
text: "设备类型表现对比",
|
|
3043
|
-
font: { size: 16, weight: "bold" },
|
|
3044
|
-
},
|
|
3045
|
-
},
|
|
3046
|
-
scales: {
|
|
3047
|
-
y: {
|
|
3048
|
-
beginAtZero: true,
|
|
3049
|
-
grid: { color: "#f0f0f0" },
|
|
3050
|
-
},
|
|
3051
|
-
x: {
|
|
3052
|
-
grid: { display: false },
|
|
3053
|
-
},
|
|
3054
|
-
},
|
|
2881
|
+
title: {
|
|
2882
|
+
text: "设备类型表现对比",
|
|
2883
|
+
left: "center",
|
|
2884
|
+
textStyle: { fontSize: 16, fontWeight: "bold" },
|
|
3055
2885
|
},
|
|
2886
|
+
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
|
2887
|
+
legend: { top: 28 },
|
|
2888
|
+
grid: { left: "8%", right: "5%", top: 70, bottom: 30, containLabel: true },
|
|
2889
|
+
xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false } },
|
|
2890
|
+
yAxis: { type: "value", splitLine: { lineStyle: { color: "#f0f0f0" } } },
|
|
2891
|
+
series: [
|
|
2892
|
+
{ name: "消费", type: "bar", data: costData, itemStyle: { color: "#3b68f9" } },
|
|
2893
|
+
{ name: "转化次数", type: "bar", data: conversionData, itemStyle: { color: "#5b8fff" } },
|
|
2894
|
+
],
|
|
3056
2895
|
};
|
|
3057
2896
|
};
|
|
3058
2897
|
|
|
@@ -3062,50 +2901,30 @@
|
|
|
3062
2901
|
const dataPoints = items.map((item) => Number(item?.cost) || 0);
|
|
3063
2902
|
if (!dataPoints.some((value) => value > 0)) return null;
|
|
3064
2903
|
return {
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
data: dataPoints,
|
|
3071
|
-
backgroundColor: [
|
|
3072
|
-
"#ff6b6b",
|
|
3073
|
-
"#4ecdc4",
|
|
3074
|
-
"#45b7d1",
|
|
3075
|
-
"#96ceb4",
|
|
3076
|
-
"#feca57",
|
|
3077
|
-
"#ff9ff3",
|
|
3078
|
-
],
|
|
3079
|
-
borderColor: "#ffffff",
|
|
3080
|
-
borderWidth: 2,
|
|
3081
|
-
},
|
|
3082
|
-
],
|
|
2904
|
+
color: ["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3"],
|
|
2905
|
+
title: {
|
|
2906
|
+
text: "受众特征消费分布",
|
|
2907
|
+
left: "center",
|
|
2908
|
+
textStyle: { fontSize: 16, fontWeight: "bold" },
|
|
3083
2909
|
},
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
},
|
|
3092
|
-
|
|
3093
|
-
display: true,
|
|
3094
|
-
text: "受众特征消费分布",
|
|
3095
|
-
font: { size: 16, weight: "bold" },
|
|
3096
|
-
},
|
|
2910
|
+
tooltip: { trigger: "item" },
|
|
2911
|
+
legend: { orient: "vertical", right: 10, top: "middle", type: "scroll" },
|
|
2912
|
+
series: [
|
|
2913
|
+
{
|
|
2914
|
+
type: "pie",
|
|
2915
|
+
radius: "65%",
|
|
2916
|
+
center: ["40%", "55%"],
|
|
2917
|
+
itemStyle: { borderColor: "#ffffff", borderWidth: 2 },
|
|
2918
|
+
data: labels.map((name, index) => ({ name, value: dataPoints[index] })),
|
|
3097
2919
|
},
|
|
3098
|
-
|
|
2920
|
+
],
|
|
3099
2921
|
};
|
|
3100
2922
|
};
|
|
3101
2923
|
|
|
3102
2924
|
const buildCustomAudienceChartConfig = (items) => {
|
|
3103
2925
|
if (!Array.isArray(items) || !items.length) return null;
|
|
3104
2926
|
const labels = items.map((item) => getItemLabel(item, "audience"));
|
|
3105
|
-
const totalCost = items.reduce(
|
|
3106
|
-
(sum, item) => sum + (Number(item?.cost) || 0),
|
|
3107
|
-
0,
|
|
3108
|
-
);
|
|
2927
|
+
const totalCost = items.reduce((sum, item) => sum + (Number(item?.cost) || 0), 0);
|
|
3109
2928
|
const totalConversions = items.reduce(
|
|
3110
2929
|
(sum, item) => sum + (Number(item?.conversions) || 0),
|
|
3111
2930
|
0,
|
|
@@ -3122,62 +2941,29 @@
|
|
|
3122
2941
|
return Number(((conversions / totalConversions) * 100).toFixed(2));
|
|
3123
2942
|
});
|
|
3124
2943
|
return {
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
{
|
|
3130
|
-
label: "花费占比 (%)",
|
|
3131
|
-
data: costPercent,
|
|
3132
|
-
backgroundColor: "#3b68f9",
|
|
3133
|
-
borderColor: "#3b68f9",
|
|
3134
|
-
borderWidth: 1,
|
|
3135
|
-
},
|
|
3136
|
-
{
|
|
3137
|
-
label: "转化占比 (%)",
|
|
3138
|
-
data: conversionPercent,
|
|
3139
|
-
backgroundColor: "#5b8fff",
|
|
3140
|
-
borderColor: "#5b8fff",
|
|
3141
|
-
borderWidth: 1,
|
|
3142
|
-
},
|
|
3143
|
-
],
|
|
2944
|
+
title: {
|
|
2945
|
+
text: "自定义受众花费与转化占比",
|
|
2946
|
+
left: "center",
|
|
2947
|
+
textStyle: { fontSize: 16, fontWeight: "bold" },
|
|
3144
2948
|
},
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
position: "top",
|
|
3151
|
-
labels: { usePointStyle: true },
|
|
3152
|
-
},
|
|
3153
|
-
title: {
|
|
3154
|
-
display: true,
|
|
3155
|
-
text: "自定义受众花费与转化占比",
|
|
3156
|
-
font: { size: 16, weight: "bold" },
|
|
3157
|
-
},
|
|
3158
|
-
tooltip: {
|
|
3159
|
-
callbacks: {
|
|
3160
|
-
label: (context) => {
|
|
3161
|
-
const label = context.dataset?.label || "";
|
|
3162
|
-
const value = context.raw ?? 0;
|
|
3163
|
-
return `${label}: ${value}%`;
|
|
3164
|
-
},
|
|
3165
|
-
},
|
|
3166
|
-
},
|
|
3167
|
-
},
|
|
3168
|
-
scales: {
|
|
3169
|
-
y: {
|
|
3170
|
-
beginAtZero: true,
|
|
3171
|
-
grid: { color: "#f0f0f0" },
|
|
3172
|
-
ticks: {
|
|
3173
|
-
callback: (value) => `${value}%`,
|
|
3174
|
-
},
|
|
3175
|
-
},
|
|
3176
|
-
x: {
|
|
3177
|
-
grid: { display: false },
|
|
3178
|
-
},
|
|
3179
|
-
},
|
|
2949
|
+
tooltip: {
|
|
2950
|
+
trigger: "axis",
|
|
2951
|
+
axisPointer: { type: "shadow" },
|
|
2952
|
+
formatter: (params) =>
|
|
2953
|
+
params.map((p) => `${p.marker}${p.seriesName}: ${p.value}%`).join("<br/>"),
|
|
3180
2954
|
},
|
|
2955
|
+
legend: { top: 28 },
|
|
2956
|
+
grid: { left: "8%", right: "5%", top: 70, bottom: 30, containLabel: true },
|
|
2957
|
+
xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false } },
|
|
2958
|
+
yAxis: {
|
|
2959
|
+
type: "value",
|
|
2960
|
+
axisLabel: { formatter: "{value}%" },
|
|
2961
|
+
splitLine: { lineStyle: { color: "#f0f0f0" } },
|
|
2962
|
+
},
|
|
2963
|
+
series: [
|
|
2964
|
+
{ name: "花费占比 (%)", type: "bar", data: costPercent, itemStyle: { color: "#3b68f9" } },
|
|
2965
|
+
{ name: "转化占比 (%)", type: "bar", data: conversionPercent, itemStyle: { color: "#5b8fff" } },
|
|
2966
|
+
],
|
|
3181
2967
|
};
|
|
3182
2968
|
};
|
|
3183
2969
|
|
|
@@ -3211,18 +2997,11 @@
|
|
|
3211
2997
|
return '<li class="insight-item">暂无相关内容</li>';
|
|
3212
2998
|
}
|
|
3213
2999
|
return items
|
|
3214
|
-
.map(
|
|
3215
|
-
(item) =>
|
|
3216
|
-
`<li class="insight-item">${escapeHtml(String(item))}</li>`,
|
|
3217
|
-
)
|
|
3000
|
+
.map((item) => `<li class="insight-item">${escapeHtml(String(item))}</li>`)
|
|
3218
3001
|
.join("");
|
|
3219
3002
|
};
|
|
3220
3003
|
|
|
3221
|
-
const formatDisplayPercent = (
|
|
3222
|
-
value,
|
|
3223
|
-
fractionDigits = 2,
|
|
3224
|
-
suffix = "%",
|
|
3225
|
-
) => {
|
|
3004
|
+
const formatDisplayPercent = (value, fractionDigits = 2, suffix = "%") => {
|
|
3226
3005
|
const num = Number(value);
|
|
3227
3006
|
if (!Number.isFinite(num)) return "—";
|
|
3228
3007
|
const digits = Math.max(0, Math.min(4, fractionDigits));
|
|
@@ -3269,10 +3048,8 @@
|
|
|
3269
3048
|
}
|
|
3270
3049
|
|
|
3271
3050
|
const averageOf = (field) =>
|
|
3272
|
-
validKeywords.reduce(
|
|
3273
|
-
|
|
3274
|
-
0,
|
|
3275
|
-
) / validKeywords.length;
|
|
3051
|
+
validKeywords.reduce((sum, item) => sum + (Number(item?.[field]) || 0), 0) /
|
|
3052
|
+
validKeywords.length;
|
|
3276
3053
|
|
|
3277
3054
|
const avgCtr = averageOf("ctr");
|
|
3278
3055
|
const avgCvr = averageOf("cvr");
|
|
@@ -3282,37 +3059,25 @@
|
|
|
3282
3059
|
const suggestionPoints = [];
|
|
3283
3060
|
|
|
3284
3061
|
if (avgCtr < 2) {
|
|
3285
|
-
analysisPoints.push(
|
|
3286
|
-
"整体点击率低于 2%,关键词与广告文案匹配度不足,曝光利用率偏弱。",
|
|
3287
|
-
);
|
|
3062
|
+
analysisPoints.push("整体点击率低于 2%,关键词与广告文案匹配度不足,曝光利用率偏弱。");
|
|
3288
3063
|
suggestionPoints.push(
|
|
3289
3064
|
"优化高频关键词广告文案与落地页相关性,并适度提升出价以恢复 CTR。",
|
|
3290
3065
|
);
|
|
3291
3066
|
} else if (avgCtr > 5) {
|
|
3292
|
-
analysisPoints.push(
|
|
3293
|
-
"整体点击率高于 5%,流量吸引力良好,需要关注后续转化质量。",
|
|
3294
|
-
);
|
|
3067
|
+
analysisPoints.push("整体点击率高于 5%,流量吸引力良好,需要关注后续转化质量。");
|
|
3295
3068
|
suggestionPoints.push(
|
|
3296
3069
|
"保持高 CTR 关键词出价策略,同时配合落地页与转化链路优化,确保效率。",
|
|
3297
3070
|
);
|
|
3298
3071
|
}
|
|
3299
3072
|
|
|
3300
3073
|
if (avgCvr < 3) {
|
|
3301
|
-
analysisPoints.push(
|
|
3302
|
-
|
|
3303
|
-
);
|
|
3304
|
-
suggestionPoints.push(
|
|
3305
|
-
"增加否定关键词、优化匹配方式,并梳理落地页体验以提升 CVR。",
|
|
3306
|
-
);
|
|
3074
|
+
analysisPoints.push("整体转化率低于 3%,流量质量或后端承接存在优化空间。");
|
|
3075
|
+
suggestionPoints.push("增加否定关键词、优化匹配方式,并梳理落地页体验以提升 CVR。");
|
|
3307
3076
|
}
|
|
3308
3077
|
|
|
3309
3078
|
if (avgCpa > 100) {
|
|
3310
|
-
analysisPoints.push(
|
|
3311
|
-
|
|
3312
|
-
);
|
|
3313
|
-
suggestionPoints.push(
|
|
3314
|
-
"聚焦高效关键词,降低无效点击占比,并及时下调高成本关键词出价。",
|
|
3315
|
-
);
|
|
3079
|
+
analysisPoints.push(`平均每次转化费用约 ${avgCpa.toFixed(2)},成本偏高。`);
|
|
3080
|
+
suggestionPoints.push("聚焦高效关键词,降低无效点击占比,并及时下调高成本关键词出价。");
|
|
3316
3081
|
}
|
|
3317
3082
|
|
|
3318
3083
|
const withShares = validKeywords.map((item) => ({
|
|
@@ -3322,9 +3087,7 @@
|
|
|
3322
3087
|
}));
|
|
3323
3088
|
|
|
3324
3089
|
const highCostLowConv = withShares
|
|
3325
|
-
.filter(
|
|
3326
|
-
(entry) => entry.costShare >= 10 && entry.conversionsShare < 5,
|
|
3327
|
-
)
|
|
3090
|
+
.filter((entry) => entry.costShare >= 10 && entry.conversionsShare < 5)
|
|
3328
3091
|
.sort((a, b) => b.costShare - a.costShare)
|
|
3329
3092
|
.slice(0, 5)
|
|
3330
3093
|
.map(
|
|
@@ -3355,12 +3118,8 @@
|
|
|
3355
3118
|
);
|
|
3356
3119
|
|
|
3357
3120
|
if (efficient.length) {
|
|
3358
|
-
analysisPoints.push(
|
|
3359
|
-
|
|
3360
|
-
);
|
|
3361
|
-
suggestionPoints.push(
|
|
3362
|
-
"适度提升上述高效关键词预算或出价,放大优质转化人群。",
|
|
3363
|
-
);
|
|
3121
|
+
analysisPoints.push(`也有性价比较高的关键词:${efficient.join("、")},贡献优质转化。`);
|
|
3122
|
+
suggestionPoints.push("适度提升上述高效关键词预算或出价,放大优质转化人群。");
|
|
3364
3123
|
}
|
|
3365
3124
|
|
|
3366
3125
|
const topCostShare = withShares
|
|
@@ -3374,13 +3133,9 @@
|
|
|
3374
3133
|
`成本集中度:Top5 关键词合计花费约占 ${topCostShare.toFixed(1)}%。`,
|
|
3375
3134
|
);
|
|
3376
3135
|
if (topCostShare >= 60) {
|
|
3377
|
-
suggestionPoints.push(
|
|
3378
|
-
"花费集中度较高,建议分散预算并设置上限,降低单词波动风险。",
|
|
3379
|
-
);
|
|
3136
|
+
suggestionPoints.push("花费集中度较高,建议分散预算并设置上限,降低单词波动风险。");
|
|
3380
3137
|
} else {
|
|
3381
|
-
suggestionPoints.push(
|
|
3382
|
-
"成本分布较均衡,可保持当前结构并定期复盘关键词表现。",
|
|
3383
|
-
);
|
|
3138
|
+
suggestionPoints.push("成本分布较均衡,可保持当前结构并定期复盘关键词表现。");
|
|
3384
3139
|
}
|
|
3385
3140
|
}
|
|
3386
3141
|
|
|
@@ -3388,9 +3143,7 @@
|
|
|
3388
3143
|
analysisPoints.push("关键词表现整体稳定,各项指标在合理范围内。");
|
|
3389
3144
|
}
|
|
3390
3145
|
if (!suggestionPoints.length) {
|
|
3391
|
-
suggestionPoints.push(
|
|
3392
|
-
"继续保持当前优化策略,并定期监控关键词表现。",
|
|
3393
|
-
);
|
|
3146
|
+
suggestionPoints.push("继续保持当前优化策略,并定期监控关键词表现。");
|
|
3394
3147
|
}
|
|
3395
3148
|
|
|
3396
3149
|
return {
|
|
@@ -3433,40 +3186,28 @@
|
|
|
3433
3186
|
analysis.push("暂无关键词数据,无法计算广泛匹配占比。");
|
|
3434
3187
|
suggestions.push("请补充关键词结构并重新生成报告。");
|
|
3435
3188
|
} else if (broadMatchRatio === 0) {
|
|
3436
|
-
analysis.push(
|
|
3437
|
-
|
|
3438
|
-
);
|
|
3439
|
-
suggestions.push(
|
|
3440
|
-
"将广泛匹配占比提升至 20%-30%,在扩大流量的同时配合否定词维持质量。",
|
|
3441
|
-
);
|
|
3189
|
+
analysis.push("当前未启用广泛匹配关键词,流量覆盖范围可能受到限制。");
|
|
3190
|
+
suggestions.push("将广泛匹配占比提升至 20%-30%,在扩大流量的同时配合否定词维持质量。");
|
|
3442
3191
|
} else if (broadMatchRatio < 20) {
|
|
3443
3192
|
analysis.push(
|
|
3444
3193
|
`广泛匹配占比为 ${broadMatchRatio.toFixed(1)}%,策略偏保守,覆盖面有限。`,
|
|
3445
3194
|
);
|
|
3446
|
-
suggestions.push(
|
|
3447
|
-
"可逐步将广泛匹配占比提升至 25%-35%,获取更多潜在检索。",
|
|
3448
|
-
);
|
|
3195
|
+
suggestions.push("可逐步将广泛匹配占比提升至 25%-35%,获取更多潜在检索。");
|
|
3449
3196
|
} else if (broadMatchRatio <= 50) {
|
|
3450
3197
|
analysis.push(
|
|
3451
3198
|
`广泛匹配占比为 ${broadMatchRatio.toFixed(1)}%,处于合理区间,覆盖与质量较平衡。`,
|
|
3452
3199
|
);
|
|
3453
|
-
suggestions.push(
|
|
3454
|
-
"维持当前匹配结构,持续关注搜索词表现并及时优化否定关键词。",
|
|
3455
|
-
);
|
|
3200
|
+
suggestions.push("维持当前匹配结构,持续关注搜索词表现并及时优化否定关键词。");
|
|
3456
3201
|
} else if (broadMatchRatio <= 70) {
|
|
3457
3202
|
analysis.push(
|
|
3458
3203
|
`广泛匹配占比为 ${broadMatchRatio.toFixed(1)}%,偏高且存在流量稀释风险。`,
|
|
3459
3204
|
);
|
|
3460
|
-
suggestions.push(
|
|
3461
|
-
"建议将占比控制在 40% 以下,并加强否定词与出价管控,保障转化效率。",
|
|
3462
|
-
);
|
|
3205
|
+
suggestions.push("建议将占比控制在 40% 以下,并加强否定词与出价管控,保障转化效率。");
|
|
3463
3206
|
} else {
|
|
3464
3207
|
analysis.push(
|
|
3465
3208
|
`广泛匹配占比为 ${broadMatchRatio.toFixed(1)}%,远高于合理范围,流量质量风险显著。`,
|
|
3466
3209
|
);
|
|
3467
|
-
suggestions.push(
|
|
3468
|
-
"立即调低广泛匹配占比至 30% 内,并密集检查搜索词以筛除低质流量。",
|
|
3469
|
-
);
|
|
3210
|
+
suggestions.push("立即调低广泛匹配占比至 30% 内,并密集检查搜索词以筛除低质流量。");
|
|
3470
3211
|
}
|
|
3471
3212
|
|
|
3472
3213
|
return {
|
|
@@ -3478,21 +3219,14 @@
|
|
|
3478
3219
|
};
|
|
3479
3220
|
};
|
|
3480
3221
|
|
|
3481
|
-
const buildKeywordTableRows = (
|
|
3482
|
-
items,
|
|
3483
|
-
metrics,
|
|
3484
|
-
tableKey,
|
|
3485
|
-
sortByConversions = false,
|
|
3486
|
-
) => {
|
|
3222
|
+
const buildKeywordTableRows = (items, metrics, tableKey, sortByConversions = false) => {
|
|
3487
3223
|
const sourceItems = Array.isArray(items) ? items.slice() : [];
|
|
3488
3224
|
if (sortByConversions) {
|
|
3489
3225
|
sourceItems.sort(
|
|
3490
|
-
(a, b) =>
|
|
3491
|
-
(Number(b?.conversions) || 0) - (Number(a?.conversions) || 0),
|
|
3226
|
+
(a, b) => (Number(b?.conversions) || 0) - (Number(a?.conversions) || 0),
|
|
3492
3227
|
);
|
|
3493
3228
|
}
|
|
3494
|
-
const placeholder =
|
|
3495
|
-
'<tr><td colspan="11" class="empty-row">暂无关键词数据</td></tr>';
|
|
3229
|
+
const placeholder = '<tr><td colspan="11" class="empty-row">暂无关键词数据</td></tr>';
|
|
3496
3230
|
const accountCvr = Number(metrics?.cvr);
|
|
3497
3231
|
const accountCpa = Number(metrics?.cpa);
|
|
3498
3232
|
const rowBuilder = (item) => {
|
|
@@ -3513,12 +3247,8 @@
|
|
|
3513
3247
|
suffix: "",
|
|
3514
3248
|
label: "账户每次转化费用",
|
|
3515
3249
|
});
|
|
3516
|
-
const costShare = escapeHtml(
|
|
3517
|
-
|
|
3518
|
-
);
|
|
3519
|
-
const conversionsShare = escapeHtml(
|
|
3520
|
-
formatDisplayPercent(item?.conversionsShare, 2),
|
|
3521
|
-
);
|
|
3250
|
+
const costShare = escapeHtml(formatDisplayPercent(item?.costShare, 2));
|
|
3251
|
+
const conversionsShare = escapeHtml(formatDisplayPercent(item?.conversionsShare, 2));
|
|
3522
3252
|
|
|
3523
3253
|
return `
|
|
3524
3254
|
<tr>
|
|
@@ -3546,8 +3276,7 @@
|
|
|
3546
3276
|
|
|
3547
3277
|
const buildSearchTermRows = (items, tableKey) => {
|
|
3548
3278
|
const list = Array.isArray(items) ? items.slice() : [];
|
|
3549
|
-
const placeholder =
|
|
3550
|
-
'<tr><td colspan="7" class="empty-row">暂无搜索词数据</td></tr>';
|
|
3279
|
+
const placeholder = '<tr><td colspan="7" class="empty-row">暂无搜索词数据</td></tr>';
|
|
3551
3280
|
const rowBuilder = (item) => `
|
|
3552
3281
|
<tr>
|
|
3553
3282
|
<td data-label="搜索词">${escapeHtml(String(item?.searchTerm || "未命名"))}</td>
|
|
@@ -3570,45 +3299,25 @@
|
|
|
3570
3299
|
const buildKeywordChartConfig = (items) => {
|
|
3571
3300
|
if (!Array.isArray(items) || !items.length) {
|
|
3572
3301
|
return {
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
options: {
|
|
3586
|
-
responsive: true,
|
|
3587
|
-
maintainAspectRatio: false,
|
|
3588
|
-
plugins: {
|
|
3589
|
-
legend: { position: "top" },
|
|
3590
|
-
title: { display: true, text: "关键词表现趋势" },
|
|
3591
|
-
tooltip: {
|
|
3592
|
-
callbacks: {
|
|
3593
|
-
label: (context) =>
|
|
3594
|
-
`${context.dataset.label || ""}: ${context.raw}%`,
|
|
3595
|
-
},
|
|
3596
|
-
},
|
|
3597
|
-
},
|
|
3598
|
-
scales: {
|
|
3599
|
-
y: {
|
|
3600
|
-
beginAtZero: true,
|
|
3601
|
-
ticks: { callback: (value) => `${value}%` },
|
|
3602
|
-
},
|
|
3302
|
+
title: { text: "关键词表现趋势", left: "center" },
|
|
3303
|
+
tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, valueFormatter: (v) => `${v}%` },
|
|
3304
|
+
legend: { top: 28 },
|
|
3305
|
+
grid: { left: "8%", right: "5%", top: 60, bottom: 30, containLabel: true },
|
|
3306
|
+
xAxis: { type: "category", data: ["暂无数据"] },
|
|
3307
|
+
yAxis: { type: "value", axisLabel: { formatter: "{value}%" } },
|
|
3308
|
+
series: [
|
|
3309
|
+
{
|
|
3310
|
+
name: "花费占比 (%)",
|
|
3311
|
+
type: "bar",
|
|
3312
|
+
data: [0],
|
|
3313
|
+
itemStyle: { color: "rgba(59, 104, 249, 0.6)", borderRadius: 6 },
|
|
3603
3314
|
},
|
|
3604
|
-
|
|
3315
|
+
],
|
|
3605
3316
|
};
|
|
3606
3317
|
}
|
|
3607
3318
|
|
|
3608
3319
|
const topItems = items.slice(0, 20);
|
|
3609
|
-
const labels = topItems.map((item) =>
|
|
3610
|
-
String(item?.keyword || "未命名"),
|
|
3611
|
-
);
|
|
3320
|
+
const labels = topItems.map((item) => String(item?.keyword || "未命名"));
|
|
3612
3321
|
const costShareValues = topItems.map((item) => {
|
|
3613
3322
|
const value = Number(item?.costShare) || 0;
|
|
3614
3323
|
return Number.isFinite(value) ? Number(value.toFixed(2)) : 0;
|
|
@@ -3619,137 +3328,83 @@
|
|
|
3619
3328
|
});
|
|
3620
3329
|
|
|
3621
3330
|
return {
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
borderColor: "rgba(37, 99, 235, 0.9)",
|
|
3631
|
-
borderWidth: 1,
|
|
3632
|
-
borderRadius: 6,
|
|
3633
|
-
},
|
|
3634
|
-
{
|
|
3635
|
-
label: "转化占比 (%)",
|
|
3636
|
-
data: conversionShareValues,
|
|
3637
|
-
backgroundColor: "rgba(16, 185, 129, 0.7)",
|
|
3638
|
-
borderColor: "rgba(5, 150, 105, 0.9)",
|
|
3639
|
-
borderWidth: 1,
|
|
3640
|
-
borderRadius: 6,
|
|
3641
|
-
},
|
|
3642
|
-
],
|
|
3331
|
+
title: { text: "关键词表现趋势", left: "center" },
|
|
3332
|
+
tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, valueFormatter: (v) => `${v}%` },
|
|
3333
|
+
legend: { top: 28 },
|
|
3334
|
+
grid: { left: "8%", right: "5%", top: 60, bottom: 60, containLabel: true },
|
|
3335
|
+
xAxis: {
|
|
3336
|
+
type: "category",
|
|
3337
|
+
data: labels,
|
|
3338
|
+
axisLabel: { rotate: 45 },
|
|
3643
3339
|
},
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
tooltip: {
|
|
3652
|
-
callbacks: {
|
|
3653
|
-
label: (context) =>
|
|
3654
|
-
`${context.dataset.label || ""}: ${context.raw}%`,
|
|
3655
|
-
},
|
|
3656
|
-
},
|
|
3340
|
+
yAxis: { type: "value", axisLabel: { formatter: "{value}%" } },
|
|
3341
|
+
series: [
|
|
3342
|
+
{
|
|
3343
|
+
name: "花费占比 (%)",
|
|
3344
|
+
type: "bar",
|
|
3345
|
+
data: costShareValues,
|
|
3346
|
+
itemStyle: { color: "rgba(59, 104, 249, 0.7)", borderRadius: 6 },
|
|
3657
3347
|
},
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
},
|
|
3663
|
-
x: {
|
|
3664
|
-
ticks: {
|
|
3665
|
-
maxRotation: 45,
|
|
3666
|
-
minRotation: 0,
|
|
3667
|
-
},
|
|
3668
|
-
},
|
|
3348
|
+
{
|
|
3349
|
+
name: "转化占比 (%)",
|
|
3350
|
+
type: "bar",
|
|
3351
|
+
data: conversionShareValues,
|
|
3352
|
+
itemStyle: { color: "rgba(16, 185, 129, 0.7)", borderRadius: 6 },
|
|
3669
3353
|
},
|
|
3670
|
-
|
|
3354
|
+
],
|
|
3671
3355
|
};
|
|
3672
3356
|
};
|
|
3673
3357
|
|
|
3674
3358
|
const buildSearchTermsChartConfig = (items) => {
|
|
3675
3359
|
if (!Array.isArray(items) || !items.length) {
|
|
3676
3360
|
return {
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
options: {
|
|
3690
|
-
responsive: true,
|
|
3691
|
-
maintainAspectRatio: false,
|
|
3692
|
-
plugins: {
|
|
3693
|
-
legend: { position: "top" },
|
|
3694
|
-
title: { display: true, text: "搜索词表现趋势" },
|
|
3695
|
-
},
|
|
3696
|
-
scales: {
|
|
3697
|
-
y: { beginAtZero: true },
|
|
3361
|
+
title: { text: "搜索词表现趋势", left: "center" },
|
|
3362
|
+
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
|
3363
|
+
legend: { top: 28 },
|
|
3364
|
+
grid: { left: "8%", right: "5%", top: 60, bottom: 30, containLabel: true },
|
|
3365
|
+
xAxis: { type: "category", data: ["暂无数据"] },
|
|
3366
|
+
yAxis: { type: "value" },
|
|
3367
|
+
series: [
|
|
3368
|
+
{
|
|
3369
|
+
name: "展示次数",
|
|
3370
|
+
type: "bar",
|
|
3371
|
+
data: [0],
|
|
3372
|
+
itemStyle: { color: "rgba(245, 158, 11, 0.7)", borderRadius: 6 },
|
|
3698
3373
|
},
|
|
3699
|
-
|
|
3374
|
+
],
|
|
3700
3375
|
};
|
|
3701
3376
|
}
|
|
3702
3377
|
|
|
3703
3378
|
const topItems = items.slice(0, 10);
|
|
3704
|
-
const labels = topItems.map((item) =>
|
|
3705
|
-
|
|
3706
|
-
);
|
|
3707
|
-
const impressionsData = topItems.map(
|
|
3708
|
-
(item) => Number(item?.impressions) || 0,
|
|
3709
|
-
);
|
|
3379
|
+
const labels = topItems.map((item) => String(item?.keyword || "未命名"));
|
|
3380
|
+
const impressionsData = topItems.map((item) => Number(item?.impressions) || 0);
|
|
3710
3381
|
const clicksData = topItems.map((item) => Number(item?.clicks) || 0);
|
|
3711
3382
|
|
|
3712
3383
|
return {
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
borderColor: "rgba(217, 119, 6, 0.9)",
|
|
3722
|
-
borderWidth: 1,
|
|
3723
|
-
borderRadius: 6,
|
|
3724
|
-
},
|
|
3725
|
-
{
|
|
3726
|
-
label: "点击次数",
|
|
3727
|
-
data: clicksData,
|
|
3728
|
-
backgroundColor: "rgba(16, 185, 129, 0.75)",
|
|
3729
|
-
borderColor: "rgba(5, 150, 105, 0.9)",
|
|
3730
|
-
borderWidth: 1,
|
|
3731
|
-
borderRadius: 6,
|
|
3732
|
-
},
|
|
3733
|
-
],
|
|
3384
|
+
title: { text: "搜索词表现趋势", left: "center" },
|
|
3385
|
+
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
|
3386
|
+
legend: { top: 28 },
|
|
3387
|
+
grid: { left: "8%", right: "5%", top: 60, bottom: 60, containLabel: true },
|
|
3388
|
+
xAxis: {
|
|
3389
|
+
type: "category",
|
|
3390
|
+
data: labels,
|
|
3391
|
+
axisLabel: { rotate: 45 },
|
|
3734
3392
|
},
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3393
|
+
yAxis: { type: "value" },
|
|
3394
|
+
series: [
|
|
3395
|
+
{
|
|
3396
|
+
name: "展示次数",
|
|
3397
|
+
type: "bar",
|
|
3398
|
+
data: impressionsData,
|
|
3399
|
+
itemStyle: { color: "rgba(245, 158, 11, 0.75)", borderRadius: 6 },
|
|
3742
3400
|
},
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
minRotation: 0,
|
|
3749
|
-
},
|
|
3750
|
-
},
|
|
3401
|
+
{
|
|
3402
|
+
name: "点击次数",
|
|
3403
|
+
type: "bar",
|
|
3404
|
+
data: clicksData,
|
|
3405
|
+
itemStyle: { color: "rgba(16, 185, 129, 0.75)", borderRadius: 6 },
|
|
3751
3406
|
},
|
|
3752
|
-
|
|
3407
|
+
],
|
|
3753
3408
|
};
|
|
3754
3409
|
};
|
|
3755
3410
|
|
|
@@ -3816,9 +3471,7 @@
|
|
|
3816
3471
|
return false;
|
|
3817
3472
|
};
|
|
3818
3473
|
const rowBuilder = (item) => {
|
|
3819
|
-
const campaign = escapeHtml(
|
|
3820
|
-
String(item?.campaignName || "未命名系列"),
|
|
3821
|
-
);
|
|
3474
|
+
const campaign = escapeHtml(String(item?.campaignName || "未命名系列"));
|
|
3822
3475
|
const duration = escapeHtml(formatBiddingDuration(item));
|
|
3823
3476
|
const currentStrategy = buildStrategyTag(
|
|
3824
3477
|
item?.biddingStrategyType ?? item?.currentStrategy,
|
|
@@ -3859,11 +3512,7 @@
|
|
|
3859
3512
|
const value = String(status || "").trim();
|
|
3860
3513
|
if (value.includes("不达标")) return "is-error";
|
|
3861
3514
|
if (value.includes("待优化")) return "is-warning";
|
|
3862
|
-
if (
|
|
3863
|
-
value.includes("良好") ||
|
|
3864
|
-
value.includes("达标") ||
|
|
3865
|
-
value.includes("优秀")
|
|
3866
|
-
)
|
|
3515
|
+
if (value.includes("良好") || value.includes("达标") || value.includes("优秀"))
|
|
3867
3516
|
return "is-ok";
|
|
3868
3517
|
return "is-warning";
|
|
3869
3518
|
};
|
|
@@ -4008,9 +3657,7 @@
|
|
|
4008
3657
|
);
|
|
4009
3658
|
}
|
|
4010
3659
|
if (!suggestions.length) {
|
|
4011
|
-
suggestions.push(
|
|
4012
|
-
"广告素材覆盖良好,请持续保持,并定期复盘创意表现。",
|
|
4013
|
-
);
|
|
3660
|
+
suggestions.push("广告素材覆盖良好,请持续保持,并定期复盘创意表现。");
|
|
4014
3661
|
}
|
|
4015
3662
|
|
|
4016
3663
|
return {
|
|
@@ -4035,24 +3682,16 @@
|
|
|
4035
3682
|
const list = (Array.isArray(items) ? items : []).filter(
|
|
4036
3683
|
(item) => String(item?.strategy ?? "") !== "AiMax",
|
|
4037
3684
|
);
|
|
4038
|
-
const placeholder =
|
|
4039
|
-
'<tr><td colspan="3" class="empty-row">暂无新产品应用数据</td></tr>';
|
|
3685
|
+
const placeholder = '<tr><td colspan="3" class="empty-row">暂无新产品应用数据</td></tr>';
|
|
4040
3686
|
const rowBuilder = (item) => {
|
|
4041
3687
|
const name = escapeHtml(
|
|
4042
|
-
String(
|
|
4043
|
-
item?.strategyName ||
|
|
4044
|
-
item?.feature ||
|
|
4045
|
-
item?.strategy ||
|
|
4046
|
-
"未命名功能",
|
|
4047
|
-
),
|
|
3688
|
+
String(item?.strategyName || item?.feature || item?.strategy || "未命名功能"),
|
|
4048
3689
|
);
|
|
4049
3690
|
const enabled = resolveFeatureEnabled(item);
|
|
4050
3691
|
const status = enabled
|
|
4051
3692
|
? '<span class="feature-status is-enabled"><span class="feature-icon">✅</span>已启用</span>'
|
|
4052
3693
|
: '<span class="feature-status is-disabled"><span class="feature-icon">❌</span>未启用</span>';
|
|
4053
|
-
const recommendation = String(
|
|
4054
|
-
item?.optimizerRecommendation || "",
|
|
4055
|
-
).trim();
|
|
3694
|
+
const recommendation = String(item?.optimizerRecommendation || "").trim();
|
|
4056
3695
|
const recommendationHtml = recommendation
|
|
4057
3696
|
? `<span class="recommendation-text${enabled ? " is-excellent" : ""}">${escapeHtml(
|
|
4058
3697
|
recommendation,
|
|
@@ -4116,8 +3755,7 @@
|
|
|
4116
3755
|
const list = Array.isArray(items)
|
|
4117
3756
|
? items.map((item, index) => normalizeRoadmapItem(item, index))
|
|
4118
3757
|
: [];
|
|
4119
|
-
const placeholder =
|
|
4120
|
-
'<tr><td colspan="4" class="empty-row">暂无优化路线图数据</td></tr>';
|
|
3758
|
+
const placeholder = '<tr><td colspan="4" class="empty-row">暂无优化路线图数据</td></tr>';
|
|
4121
3759
|
const rowBuilder = (item) => {
|
|
4122
3760
|
const priority = escapeHtml(String(item?.priority || "未设置"));
|
|
4123
3761
|
const focusArea = escapeHtml(String(item?.focusArea || "未设置"));
|
|
@@ -4129,9 +3767,7 @@
|
|
|
4129
3767
|
)
|
|
4130
3768
|
.join("<br />")
|
|
4131
3769
|
: `<span class="creative-suggestion-text">${escapeHtml(String(item?.actionItems || "未提供"))}</span>`;
|
|
4132
|
-
const expectedOutcome = escapeHtml(
|
|
4133
|
-
String(item?.expectedOutcome || "未提供"),
|
|
4134
|
-
);
|
|
3770
|
+
const expectedOutcome = escapeHtml(String(item?.expectedOutcome || "未提供"));
|
|
4135
3771
|
|
|
4136
3772
|
return `
|
|
4137
3773
|
<tr>
|
|
@@ -4223,17 +3859,12 @@
|
|
|
4223
3859
|
const getStatusText = (status) => {
|
|
4224
3860
|
if (!status) return "未知";
|
|
4225
3861
|
const value = String(status);
|
|
4226
|
-
if (value.includes("启用") || value.includes("Enabled"))
|
|
4227
|
-
|
|
4228
|
-
if (value.includes("
|
|
4229
|
-
|
|
4230
|
-
if (value.includes("移除") || value.includes("Removed"))
|
|
4231
|
-
return "已移除";
|
|
4232
|
-
if (value.includes("未启用") || value.includes("Disabled"))
|
|
4233
|
-
return "已禁用";
|
|
3862
|
+
if (value.includes("启用") || value.includes("Enabled")) return "启用";
|
|
3863
|
+
if (value.includes("暂停") || value.includes("Paused")) return "已暂停";
|
|
3864
|
+
if (value.includes("移除") || value.includes("Removed")) return "已移除";
|
|
3865
|
+
if (value.includes("未启用") || value.includes("Disabled")) return "已禁用";
|
|
4234
3866
|
if (value.includes("设置") || value.includes("Set")) return "已设置";
|
|
4235
|
-
if (value.includes("隐藏") || value.includes("Hidden"))
|
|
4236
|
-
return "已隐藏";
|
|
3867
|
+
if (value.includes("隐藏") || value.includes("Hidden")) return "已隐藏";
|
|
4237
3868
|
const map = {
|
|
4238
3869
|
Enabled: "启用",
|
|
4239
3870
|
REMOVED: "已移除",
|
|
@@ -4292,7 +3923,7 @@
|
|
|
4292
3923
|
const renderPendingCharts = () => {
|
|
4293
3924
|
if (!chartJobs.length) return;
|
|
4294
3925
|
|
|
4295
|
-
if (typeof window.
|
|
3926
|
+
if (!window.echarts || typeof window.echarts.init !== "function") {
|
|
4296
3927
|
setTimeout(renderPendingCharts, 400);
|
|
4297
3928
|
return;
|
|
4298
3929
|
}
|
|
@@ -4300,23 +3931,20 @@
|
|
|
4300
3931
|
while (chartJobs.length) {
|
|
4301
3932
|
const job = chartJobs.shift();
|
|
4302
3933
|
if (!job) continue;
|
|
4303
|
-
const
|
|
4304
|
-
if (!
|
|
4305
|
-
const ctx = canvas.getContext("2d");
|
|
4306
|
-
if (!ctx) continue;
|
|
3934
|
+
const el = document.getElementById(job.id);
|
|
3935
|
+
if (!el) continue;
|
|
4307
3936
|
const existing = chartInstances.get(job.id);
|
|
4308
3937
|
if (existing) {
|
|
4309
|
-
existing.
|
|
3938
|
+
existing.dispose();
|
|
4310
3939
|
}
|
|
4311
|
-
const chart =
|
|
3940
|
+
const chart = window.echarts.init(el);
|
|
3941
|
+
chart.setOption(job.config);
|
|
4312
3942
|
chartInstances.set(job.id, chart);
|
|
4313
3943
|
}
|
|
4314
3944
|
};
|
|
4315
3945
|
|
|
4316
3946
|
const buildKpiChartConfig = (metrics, conversionCostItems) => {
|
|
4317
|
-
const items = Array.isArray(conversionCostItems)
|
|
4318
|
-
? conversionCostItems
|
|
4319
|
-
: [];
|
|
3947
|
+
const items = Array.isArray(conversionCostItems) ? conversionCostItems : [];
|
|
4320
3948
|
const formatDateLabel = (value, index) => {
|
|
4321
3949
|
if (!value) return `第 ${index + 1} 天`;
|
|
4322
3950
|
const date = new Date(value);
|
|
@@ -4326,9 +3954,7 @@
|
|
|
4326
3954
|
return `${month}-${day}`;
|
|
4327
3955
|
};
|
|
4328
3956
|
if (items.length) {
|
|
4329
|
-
const labels = items.map((item, index) =>
|
|
4330
|
-
formatDateLabel(item?.date, index),
|
|
4331
|
-
);
|
|
3957
|
+
const labels = items.map((item, index) => formatDateLabel(item?.date, index));
|
|
4332
3958
|
const totalConversions = items.reduce(
|
|
4333
3959
|
(sum, item) => sum + (Number(item?.conversions) || 0),
|
|
4334
3960
|
0,
|
|
@@ -4340,127 +3966,103 @@
|
|
|
4340
3966
|
const costData = items.map((item) => Number(item?.cost) || 0);
|
|
4341
3967
|
const clicksData = items.map((item) => Number(item?.clicks) || 0);
|
|
4342
3968
|
return {
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
pointRadius: 3,
|
|
4355
|
-
},
|
|
4356
|
-
{
|
|
4357
|
-
label: "点击次数",
|
|
4358
|
-
data: clicksData,
|
|
4359
|
-
borderColor: "#10b981",
|
|
4360
|
-
backgroundColor: "rgba(16, 185, 129, 0.12)",
|
|
4361
|
-
tension: 0.3,
|
|
4362
|
-
yAxisID: "y2",
|
|
4363
|
-
pointRadius: 3,
|
|
4364
|
-
},
|
|
4365
|
-
],
|
|
4366
|
-
},
|
|
4367
|
-
options: {
|
|
4368
|
-
responsive: true,
|
|
4369
|
-
maintainAspectRatio: false,
|
|
4370
|
-
interaction: { mode: "index", intersect: false },
|
|
4371
|
-
stacked: false,
|
|
4372
|
-
plugins: {
|
|
4373
|
-
legend: { labels: { font: { size: 12 } } },
|
|
3969
|
+
tooltip: { trigger: "axis" },
|
|
3970
|
+
legend: { top: 0, textStyle: { fontSize: 12 } },
|
|
3971
|
+
grid: { left: "8%", right: "8%", top: 40, bottom: 30, containLabel: true },
|
|
3972
|
+
xAxis: { type: "category", data: labels, axisLabel: { fontSize: 11 } },
|
|
3973
|
+
yAxis: [
|
|
3974
|
+
{
|
|
3975
|
+
type: "value",
|
|
3976
|
+
name: "消耗",
|
|
3977
|
+
position: "left",
|
|
3978
|
+
axisLabel: { fontSize: 11 },
|
|
3979
|
+
nameTextStyle: { fontSize: 12 },
|
|
4374
3980
|
},
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
},
|
|
4383
|
-
y2: {
|
|
4384
|
-
type: "linear",
|
|
4385
|
-
position: "right",
|
|
4386
|
-
beginAtZero: true,
|
|
4387
|
-
ticks: { font: { size: 11 } },
|
|
4388
|
-
grid: { drawOnChartArea: false },
|
|
4389
|
-
title: {
|
|
4390
|
-
display: true,
|
|
4391
|
-
text: "点击次数",
|
|
4392
|
-
font: { size: 12 },
|
|
4393
|
-
},
|
|
4394
|
-
},
|
|
4395
|
-
x: { ticks: { font: { size: 11 } } },
|
|
3981
|
+
{
|
|
3982
|
+
type: "value",
|
|
3983
|
+
name: "点击次数",
|
|
3984
|
+
position: "right",
|
|
3985
|
+
splitLine: { show: false },
|
|
3986
|
+
axisLabel: { fontSize: 11 },
|
|
3987
|
+
nameTextStyle: { fontSize: 12 },
|
|
4396
3988
|
},
|
|
4397
|
-
|
|
3989
|
+
],
|
|
3990
|
+
series: [
|
|
3991
|
+
{
|
|
3992
|
+
name: "消耗",
|
|
3993
|
+
type: "line",
|
|
3994
|
+
data: costData,
|
|
3995
|
+
smooth: 0.3,
|
|
3996
|
+
symbolSize: 6,
|
|
3997
|
+
yAxisIndex: 0,
|
|
3998
|
+
lineStyle: { color: "#3b82f6" },
|
|
3999
|
+
itemStyle: { color: "#3b82f6" },
|
|
4000
|
+
areaStyle: { color: "rgba(59, 130, 246, 0.12)" },
|
|
4001
|
+
},
|
|
4002
|
+
{
|
|
4003
|
+
name: "点击次数",
|
|
4004
|
+
type: "line",
|
|
4005
|
+
data: clicksData,
|
|
4006
|
+
smooth: 0.3,
|
|
4007
|
+
symbolSize: 6,
|
|
4008
|
+
yAxisIndex: 1,
|
|
4009
|
+
lineStyle: { color: "#10b981" },
|
|
4010
|
+
itemStyle: { color: "#10b981" },
|
|
4011
|
+
areaStyle: { color: "rgba(16, 185, 129, 0.12)" },
|
|
4012
|
+
},
|
|
4013
|
+
],
|
|
4398
4014
|
};
|
|
4399
4015
|
}
|
|
4400
4016
|
|
|
4401
4017
|
const cpaData = items.map((item) => Number(item?.cpa) || 0);
|
|
4402
|
-
const conversionsData = items.map((item) =>
|
|
4403
|
-
Number(item?.conversions ?? 0),
|
|
4404
|
-
);
|
|
4018
|
+
const conversionsData = items.map((item) => Number(item?.conversions ?? 0));
|
|
4405
4019
|
|
|
4406
4020
|
return {
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
pointRadius: 3,
|
|
4419
|
-
},
|
|
4420
|
-
{
|
|
4421
|
-
label: "转化次数",
|
|
4422
|
-
data: conversionsData,
|
|
4423
|
-
borderColor: "#f97316",
|
|
4424
|
-
backgroundColor: "rgba(249, 115, 22, 0.12)",
|
|
4425
|
-
tension: 0.3,
|
|
4426
|
-
yAxisID: "y2",
|
|
4427
|
-
pointRadius: 3,
|
|
4428
|
-
},
|
|
4429
|
-
],
|
|
4430
|
-
},
|
|
4431
|
-
options: {
|
|
4432
|
-
responsive: true,
|
|
4433
|
-
maintainAspectRatio: false,
|
|
4434
|
-
interaction: { mode: "index", intersect: false },
|
|
4435
|
-
stacked: false,
|
|
4436
|
-
plugins: {
|
|
4437
|
-
legend: {
|
|
4438
|
-
labels: { font: { size: 12 } },
|
|
4439
|
-
},
|
|
4021
|
+
tooltip: { trigger: "axis" },
|
|
4022
|
+
legend: { top: 0, textStyle: { fontSize: 12 } },
|
|
4023
|
+
grid: { left: "8%", right: "8%", top: 40, bottom: 30, containLabel: true },
|
|
4024
|
+
xAxis: { type: "category", data: labels, axisLabel: { fontSize: 11 } },
|
|
4025
|
+
yAxis: [
|
|
4026
|
+
{
|
|
4027
|
+
type: "value",
|
|
4028
|
+
name: "CPA",
|
|
4029
|
+
position: "left",
|
|
4030
|
+
axisLabel: { fontSize: 11 },
|
|
4031
|
+
nameTextStyle: { fontSize: 12 },
|
|
4440
4032
|
},
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
},
|
|
4449
|
-
y2: {
|
|
4450
|
-
type: "linear",
|
|
4451
|
-
position: "right",
|
|
4452
|
-
beginAtZero: true,
|
|
4453
|
-
ticks: { font: { size: 11 } },
|
|
4454
|
-
grid: { drawOnChartArea: false },
|
|
4455
|
-
title: {
|
|
4456
|
-
display: true,
|
|
4457
|
-
text: "转化次数",
|
|
4458
|
-
font: { size: 12 },
|
|
4459
|
-
},
|
|
4460
|
-
},
|
|
4461
|
-
x: { ticks: { font: { size: 11 } } },
|
|
4033
|
+
{
|
|
4034
|
+
type: "value",
|
|
4035
|
+
name: "转化次数",
|
|
4036
|
+
position: "right",
|
|
4037
|
+
splitLine: { show: false },
|
|
4038
|
+
axisLabel: { fontSize: 11 },
|
|
4039
|
+
nameTextStyle: { fontSize: 12 },
|
|
4462
4040
|
},
|
|
4463
|
-
|
|
4041
|
+
],
|
|
4042
|
+
series: [
|
|
4043
|
+
{
|
|
4044
|
+
name: "CPA",
|
|
4045
|
+
type: "line",
|
|
4046
|
+
data: cpaData,
|
|
4047
|
+
smooth: 0.3,
|
|
4048
|
+
symbolSize: 6,
|
|
4049
|
+
yAxisIndex: 0,
|
|
4050
|
+
lineStyle: { color: "#3b82f6" },
|
|
4051
|
+
itemStyle: { color: "#3b82f6" },
|
|
4052
|
+
areaStyle: { color: "rgba(59, 130, 246, 0.12)" },
|
|
4053
|
+
},
|
|
4054
|
+
{
|
|
4055
|
+
name: "转化次数",
|
|
4056
|
+
type: "line",
|
|
4057
|
+
data: conversionsData,
|
|
4058
|
+
smooth: 0.3,
|
|
4059
|
+
symbolSize: 6,
|
|
4060
|
+
yAxisIndex: 1,
|
|
4061
|
+
lineStyle: { color: "#f97316" },
|
|
4062
|
+
itemStyle: { color: "#f97316" },
|
|
4063
|
+
areaStyle: { color: "rgba(249, 115, 22, 0.12)" },
|
|
4064
|
+
},
|
|
4065
|
+
],
|
|
4464
4066
|
};
|
|
4465
4067
|
}
|
|
4466
4068
|
|
|
@@ -4472,34 +4074,21 @@
|
|
|
4472
4074
|
];
|
|
4473
4075
|
|
|
4474
4076
|
return {
|
|
4475
|
-
type: "
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4077
|
+
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
|
4078
|
+
grid: { left: "8%", right: "5%", top: 20, bottom: 30, containLabel: true },
|
|
4079
|
+
xAxis: { type: "category", data: labels, axisLabel: { fontSize: 11 } },
|
|
4080
|
+
yAxis: { type: "value", axisLabel: { fontSize: 11 } },
|
|
4081
|
+
series: [
|
|
4082
|
+
{
|
|
4083
|
+
name: "指标数值",
|
|
4084
|
+
type: "bar",
|
|
4085
|
+
data: datasetValues,
|
|
4086
|
+
itemStyle: {
|
|
4483
4087
|
borderRadius: 6,
|
|
4484
|
-
|
|
4485
|
-
],
|
|
4486
|
-
},
|
|
4487
|
-
options: {
|
|
4488
|
-
responsive: true,
|
|
4489
|
-
maintainAspectRatio: false,
|
|
4490
|
-
plugins: {
|
|
4491
|
-
legend: { display: false },
|
|
4492
|
-
},
|
|
4493
|
-
scales: {
|
|
4494
|
-
y: {
|
|
4495
|
-
beginAtZero: true,
|
|
4496
|
-
ticks: { font: { size: 11 } },
|
|
4497
|
-
},
|
|
4498
|
-
x: {
|
|
4499
|
-
ticks: { font: { size: 11 } },
|
|
4088
|
+
color: (params) => ["#3b82f6", "#10b981", "#f97316"][params.dataIndex],
|
|
4500
4089
|
},
|
|
4501
4090
|
},
|
|
4502
|
-
|
|
4091
|
+
],
|
|
4503
4092
|
};
|
|
4504
4093
|
};
|
|
4505
4094
|
|
|
@@ -4521,9 +4110,7 @@
|
|
|
4521
4110
|
const conversions = Number(item?.conversions ?? 0);
|
|
4522
4111
|
const cpa = Number(item?.cpa);
|
|
4523
4112
|
const cpaDisplay =
|
|
4524
|
-
conversions > 0 && Number.isFinite(cpa)
|
|
4525
|
-
? formatNumber(cpa, 2)
|
|
4526
|
-
: "—";
|
|
4113
|
+
conversions > 0 && Number.isFinite(cpa) ? formatNumber(cpa, 2) : "—";
|
|
4527
4114
|
return `
|
|
4528
4115
|
<tr>
|
|
4529
4116
|
<td data-label="日期">${escapeHtml(String(dateLabel || "—"))}</td>
|
|
@@ -4619,12 +4206,8 @@
|
|
|
4619
4206
|
if (!reportMain) return null;
|
|
4620
4207
|
|
|
4621
4208
|
const overview = data.diagnosisOverview || {};
|
|
4622
|
-
const advantages = Array.isArray(overview.advantages)
|
|
4623
|
-
|
|
4624
|
-
: [];
|
|
4625
|
-
const disadvantages = Array.isArray(overview.disadvantages)
|
|
4626
|
-
? overview.disadvantages
|
|
4627
|
-
: [];
|
|
4209
|
+
const advantages = Array.isArray(overview.advantages) ? overview.advantages : [];
|
|
4210
|
+
const disadvantages = Array.isArray(overview.disadvantages) ? overview.disadvantages : [];
|
|
4628
4211
|
|
|
4629
4212
|
const buildListItems = (items) => {
|
|
4630
4213
|
if (!items.length) {
|
|
@@ -4733,9 +4316,7 @@
|
|
|
4733
4316
|
|
|
4734
4317
|
const flowHtml = metricsFlowItems
|
|
4735
4318
|
.map((item, index) => {
|
|
4736
|
-
const badge = item.badge
|
|
4737
|
-
? `<span class="rate-badge">${item.badge}</span>`
|
|
4738
|
-
: "";
|
|
4319
|
+
const badge = item.badge ? `<span class="rate-badge">${item.badge}</span>` : "";
|
|
4739
4320
|
return `
|
|
4740
4321
|
<div class="metric-item">
|
|
4741
4322
|
<div class="metric-label">${escapeHtml(item.label)}</div>
|
|
@@ -4789,9 +4370,8 @@
|
|
|
4789
4370
|
)
|
|
4790
4371
|
.join("");
|
|
4791
4372
|
|
|
4792
|
-
const metricRowTemplates = metricDetectionConfig.map(
|
|
4793
|
-
|
|
4794
|
-
const row = `
|
|
4373
|
+
const metricRowTemplates = metricDetectionConfig.map(({ key, name, benchmarkKey }) => {
|
|
4374
|
+
const row = `
|
|
4795
4375
|
<tr>
|
|
4796
4376
|
<td data-label="指标 (Metric)">${escapeHtml(name)}</td>
|
|
4797
4377
|
<td data-label="数据 (Data)">${formatMetricValue(key, metrics[key])}</td>
|
|
@@ -4801,23 +4381,19 @@
|
|
|
4801
4381
|
)}</td>
|
|
4802
4382
|
</tr>
|
|
4803
4383
|
`;
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
);
|
|
4384
|
+
return row;
|
|
4385
|
+
});
|
|
4807
4386
|
const metricsTable = buildTableRows(metricRowTemplates, {
|
|
4808
4387
|
tableKey: "metrics",
|
|
4809
|
-
emptyRow:
|
|
4810
|
-
'<tr><td colspan="3" class="empty-row">暂无指标检测数据</td></tr>',
|
|
4388
|
+
emptyRow: '<tr><td colspan="3" class="empty-row">暂无指标检测数据</td></tr>',
|
|
4811
4389
|
rowBuilder: (row) => row,
|
|
4812
4390
|
});
|
|
4813
4391
|
const metricRows = metricsTable.bodyHtml;
|
|
4814
4392
|
|
|
4815
|
-
const metricsAnalysisList = toArray(metrics.analysis).map((item) =>
|
|
4393
|
+
const metricsAnalysisList = toArray(metrics.analysis).map((item) => escapeHtml(item));
|
|
4394
|
+
const metricsSuggestionsList = toArray(metrics.suggestions).map((item) =>
|
|
4816
4395
|
escapeHtml(item),
|
|
4817
4396
|
);
|
|
4818
|
-
const metricsSuggestionsList = toArray(metrics.suggestions).map(
|
|
4819
|
-
(item) => escapeHtml(item),
|
|
4820
|
-
);
|
|
4821
4397
|
|
|
4822
4398
|
const sortedConversionGoals = [...conversionGoals].sort((a, b) => {
|
|
4823
4399
|
const aStatus = getStatusText(a.status || "");
|
|
@@ -4854,17 +4430,12 @@
|
|
|
4854
4430
|
`;
|
|
4855
4431
|
};
|
|
4856
4432
|
|
|
4857
|
-
const {
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
configKey: "conversionGoals",
|
|
4862
|
-
});
|
|
4433
|
+
const { displayItems: primaryConversionGoals, remainingItems: additionalEnabledGoals } =
|
|
4434
|
+
applyTableLimit(enabledGoals, "conversion-goals", {
|
|
4435
|
+
configKey: "conversionGoals",
|
|
4436
|
+
});
|
|
4863
4437
|
|
|
4864
|
-
const remainingConversionGoals = [
|
|
4865
|
-
...additionalEnabledGoals,
|
|
4866
|
-
...disabledGoals,
|
|
4867
|
-
];
|
|
4438
|
+
const remainingConversionGoals = [...additionalEnabledGoals, ...disabledGoals];
|
|
4868
4439
|
const limitedConversionRows = primaryConversionGoals
|
|
4869
4440
|
.map(renderConversionGoalRow)
|
|
4870
4441
|
.join("");
|
|
@@ -4889,12 +4460,7 @@
|
|
|
4889
4460
|
remainingConversionGoals.length,
|
|
4890
4461
|
);
|
|
4891
4462
|
|
|
4892
|
-
const buildKeyMetricsSection = (
|
|
4893
|
-
dimensionTitle,
|
|
4894
|
-
columnLabel,
|
|
4895
|
-
dataset,
|
|
4896
|
-
tableKey,
|
|
4897
|
-
) => {
|
|
4463
|
+
const buildKeyMetricsSection = (dimensionTitle, columnLabel, dataset, tableKey) => {
|
|
4898
4464
|
if (!dataset) return "";
|
|
4899
4465
|
const items = Array.isArray(dataset.items) ? dataset.items : [];
|
|
4900
4466
|
const hasContent =
|
|
@@ -5005,26 +4571,22 @@
|
|
|
5005
4571
|
};
|
|
5006
4572
|
|
|
5007
4573
|
const chartId = generateChartId();
|
|
5008
|
-
const chartConfig = buildKpiChartConfig(
|
|
5009
|
-
metrics,
|
|
5010
|
-
conversionCost.items,
|
|
5011
|
-
);
|
|
4574
|
+
const chartConfig = buildKpiChartConfig(metrics, conversionCost.items);
|
|
5012
4575
|
if (chartConfig) {
|
|
5013
4576
|
chartJobs.push({ id: chartId, config: chartConfig });
|
|
5014
4577
|
}
|
|
5015
4578
|
|
|
5016
|
-
const conversionTrendAnalysisList = toArray(
|
|
5017
|
-
|
|
5018
|
-
)
|
|
5019
|
-
const conversionTrendSuggestionsList = toArray(
|
|
5020
|
-
|
|
5021
|
-
)
|
|
4579
|
+
const conversionTrendAnalysisList = toArray(conversionCost.analysis).map(
|
|
4580
|
+
(item) => `<li class="insight-item">${escapeHtml(item)}</li>`,
|
|
4581
|
+
);
|
|
4582
|
+
const conversionTrendSuggestionsList = toArray(conversionCost.suggestions).map(
|
|
4583
|
+
(item) => `<li class="insight-item">${escapeHtml(item)}</li>`,
|
|
4584
|
+
);
|
|
5022
4585
|
const conversionCostDailyItems = Array.isArray(conversionCost.items)
|
|
5023
4586
|
? conversionCost.items
|
|
5024
4587
|
: [];
|
|
5025
|
-
const conversionDailyTableHtml =
|
|
5026
|
-
conversionCostDailyItems
|
|
5027
|
-
);
|
|
4588
|
+
const conversionDailyTableHtml =
|
|
4589
|
+
buildConversionCostDailyTableHtml(conversionCostDailyItems);
|
|
5028
4590
|
const conversionTrendContent =
|
|
5029
4591
|
(chartConfig ||
|
|
5030
4592
|
conversionCostDailyItems.length ||
|
|
@@ -5061,9 +4623,7 @@
|
|
|
5061
4623
|
: "下图展示 CPA 与转化次数按日趋势。"
|
|
5062
4624
|
}</p>
|
|
5063
4625
|
<div class="chart-card conversion-trend-chart">
|
|
5064
|
-
<div style="height: 280px;">
|
|
5065
|
-
<canvas id="${chartId}" height="260"></canvas>
|
|
5066
|
-
</div>
|
|
4626
|
+
<div id="${chartId}" class="echarts-chart" style="height: 280px;"></div>
|
|
5067
4627
|
</div>
|
|
5068
4628
|
`
|
|
5069
4629
|
: ""
|
|
@@ -5094,24 +4654,14 @@
|
|
|
5094
4654
|
</div>
|
|
5095
4655
|
`;
|
|
5096
4656
|
const keyMetricsContent = [
|
|
5097
|
-
buildKeyMetricsSection(
|
|
5098
|
-
"广告系列分析",
|
|
5099
|
-
"广告系列",
|
|
5100
|
-
campaigns,
|
|
5101
|
-
"key-metrics-campaigns",
|
|
5102
|
-
),
|
|
4657
|
+
buildKeyMetricsSection("广告系列分析", "广告系列", campaigns, "key-metrics-campaigns"),
|
|
5103
4658
|
buildKeyMetricsSection(
|
|
5104
4659
|
"国家地区分析",
|
|
5105
4660
|
"国家/地区",
|
|
5106
4661
|
geographic,
|
|
5107
4662
|
"key-metrics-geographic",
|
|
5108
4663
|
),
|
|
5109
|
-
buildKeyMetricsSection(
|
|
5110
|
-
"关键词分析",
|
|
5111
|
-
"关键词",
|
|
5112
|
-
keywords,
|
|
5113
|
-
"key-metrics-keywords",
|
|
5114
|
-
),
|
|
4664
|
+
buildKeyMetricsSection("关键词分析", "关键词", keywords, "key-metrics-keywords"),
|
|
5115
4665
|
conversionTrendContent || "",
|
|
5116
4666
|
]
|
|
5117
4667
|
.filter((html) => html.trim().length)
|
|
@@ -5173,9 +4723,7 @@
|
|
|
5173
4723
|
${
|
|
5174
4724
|
metricsAnalysisList.length
|
|
5175
4725
|
? metricsAnalysisList
|
|
5176
|
-
.map(
|
|
5177
|
-
(item) => `<li class="insight-item">${item}</li>`,
|
|
5178
|
-
)
|
|
4726
|
+
.map((item) => `<li class="insight-item">${item}</li>`)
|
|
5179
4727
|
.join("")
|
|
5180
4728
|
: '<li class="insight-item">暂无分析内容</li>'
|
|
5181
4729
|
}
|
|
@@ -5187,9 +4735,7 @@
|
|
|
5187
4735
|
${
|
|
5188
4736
|
metricsSuggestionsList.length
|
|
5189
4737
|
? metricsSuggestionsList
|
|
5190
|
-
.map(
|
|
5191
|
-
(item) => `<li class="insight-item">${item}</li>`,
|
|
5192
|
-
)
|
|
4738
|
+
.map((item) => `<li class="insight-item">${item}</li>`)
|
|
5193
4739
|
.join("")
|
|
5194
4740
|
: '<li class="insight-item">暂无建议内容</li>'
|
|
5195
4741
|
}
|
|
@@ -5244,8 +4790,7 @@
|
|
|
5244
4790
|
? goldAccount.goldAccountUnqualifiedCount
|
|
5245
4791
|
: totalItems - qualifiedCount;
|
|
5246
4792
|
const scoreValue =
|
|
5247
|
-
goldAccount.goldAccountScore !== undefined &&
|
|
5248
|
-
goldAccount.goldAccountScore !== null
|
|
4793
|
+
goldAccount.goldAccountScore !== undefined && goldAccount.goldAccountScore !== null
|
|
5249
4794
|
? String(goldAccount.goldAccountScore)
|
|
5250
4795
|
: "--";
|
|
5251
4796
|
|
|
@@ -5540,17 +5085,13 @@
|
|
|
5540
5085
|
|
|
5541
5086
|
const dimensionSections = dimensionConfigs
|
|
5542
5087
|
.map((config) => {
|
|
5543
|
-
const items = Array.isArray(config.data?.items)
|
|
5544
|
-
? config.data.items
|
|
5545
|
-
: [];
|
|
5088
|
+
const items = Array.isArray(config.data?.items) ? config.data.items : [];
|
|
5546
5089
|
if (!items.length) return "";
|
|
5547
5090
|
|
|
5548
5091
|
const tableResult = buildTableRows(items, {
|
|
5549
5092
|
tableKey: config.tableKey,
|
|
5550
|
-
emptyRow:
|
|
5551
|
-
|
|
5552
|
-
rowBuilder: (item) =>
|
|
5553
|
-
buildTargetingTableRow(item, config.labelKey),
|
|
5093
|
+
emptyRow: '<tr><td colspan="9" class="empty-row">暂无数据</td></tr>',
|
|
5094
|
+
rowBuilder: (item) => buildTargetingTableRow(item, config.labelKey),
|
|
5554
5095
|
});
|
|
5555
5096
|
const tableRows = tableResult.bodyHtml;
|
|
5556
5097
|
const tableExpandHtml = tableResult.expandHtml;
|
|
@@ -5575,9 +5116,7 @@
|
|
|
5575
5116
|
chartHtml = `
|
|
5576
5117
|
<div class="chart-card">
|
|
5577
5118
|
<h4 class="chart-title">${escapeHtml(config.chartTitle)}</h4>
|
|
5578
|
-
<div style="height: ${chartHeight}px;">
|
|
5579
|
-
<canvas id="${chartId}" height="${chartHeight}"></canvas>
|
|
5580
|
-
</div>
|
|
5119
|
+
<div id="${chartId}" class="echarts-chart" style="height: ${chartHeight}px;"></div>
|
|
5581
5120
|
</div>
|
|
5582
5121
|
`;
|
|
5583
5122
|
}
|
|
@@ -5679,21 +5218,14 @@
|
|
|
5679
5218
|
: [];
|
|
5680
5219
|
const broadKeywordsCount = data.broadKeywordsCount || {};
|
|
5681
5220
|
|
|
5682
|
-
const keywordTable = buildKeywordTableRows(
|
|
5683
|
-
keywordItems,
|
|
5684
|
-
metrics,
|
|
5685
|
-
"keyword-performance",
|
|
5686
|
-
);
|
|
5221
|
+
const keywordTable = buildKeywordTableRows(keywordItems, metrics, "keyword-performance");
|
|
5687
5222
|
const keywordConversionsTable = buildKeywordTableRows(
|
|
5688
5223
|
keywordItems,
|
|
5689
5224
|
metrics,
|
|
5690
5225
|
"keyword-performance-conversions",
|
|
5691
5226
|
true,
|
|
5692
5227
|
);
|
|
5693
|
-
const searchTable = buildSearchTermRows(
|
|
5694
|
-
searchTermItems,
|
|
5695
|
-
"search-term-performance",
|
|
5696
|
-
);
|
|
5228
|
+
const searchTable = buildSearchTermRows(searchTermItems, "search-term-performance");
|
|
5697
5229
|
|
|
5698
5230
|
const keywordInsights = generateKeywordsAnalysis(keywordItems);
|
|
5699
5231
|
const keywordsAnalysisList = buildAnalysisList(
|
|
@@ -5707,8 +5239,7 @@
|
|
|
5707
5239
|
() => keywordInsights.suggestions,
|
|
5708
5240
|
);
|
|
5709
5241
|
|
|
5710
|
-
const searchTermInsights =
|
|
5711
|
-
generateSearchTermsAnalysis(searchTermItems);
|
|
5242
|
+
const searchTermInsights = generateSearchTermsAnalysis(searchTermItems);
|
|
5712
5243
|
const searchAnalysisList = buildAnalysisList(
|
|
5713
5244
|
data.fullSearchTerms?.analysis,
|
|
5714
5245
|
"搜索词",
|
|
@@ -5742,20 +5273,19 @@
|
|
|
5742
5273
|
chartJobs.push({ id: chartId, config: keywordChartConfig });
|
|
5743
5274
|
keywordChartHtml = `
|
|
5744
5275
|
<div class="chart-container">
|
|
5745
|
-
<
|
|
5276
|
+
<div id="${chartId}" class="echarts-chart" style="height: 340px;"></div>
|
|
5746
5277
|
</div>
|
|
5747
5278
|
`;
|
|
5748
5279
|
}
|
|
5749
5280
|
|
|
5750
|
-
const searchChartConfig =
|
|
5751
|
-
buildSearchTermsChartConfig(searchTermItems);
|
|
5281
|
+
const searchChartConfig = buildSearchTermsChartConfig(searchTermItems);
|
|
5752
5282
|
let searchChartHtml = "";
|
|
5753
5283
|
if (searchChartConfig) {
|
|
5754
5284
|
const chartId = generateChartId();
|
|
5755
5285
|
chartJobs.push({ id: chartId, config: searchChartConfig });
|
|
5756
5286
|
searchChartHtml = `
|
|
5757
5287
|
<div class="chart-container">
|
|
5758
|
-
<
|
|
5288
|
+
<div id="${chartId}" class="echarts-chart" style="height: 340px;"></div>
|
|
5759
5289
|
</div>
|
|
5760
5290
|
`;
|
|
5761
5291
|
}
|
|
@@ -5930,23 +5460,12 @@
|
|
|
5930
5460
|
if (!reportMain) return null;
|
|
5931
5461
|
|
|
5932
5462
|
const biddingStrategy = data.biddingStrategy || {};
|
|
5933
|
-
const strategyItems = Array.isArray(biddingStrategy.items)
|
|
5934
|
-
|
|
5935
|
-
: [];
|
|
5936
|
-
const hasStrategyIssues =
|
|
5937
|
-
Array.isArray(strategyItems) && strategyItems.length > 0;
|
|
5463
|
+
const strategyItems = Array.isArray(biddingStrategy.items) ? biddingStrategy.items : [];
|
|
5464
|
+
const hasStrategyIssues = Array.isArray(strategyItems) && strategyItems.length > 0;
|
|
5938
5465
|
const biddingTable = buildBiddingStrategyRows(strategyItems);
|
|
5939
5466
|
|
|
5940
|
-
const headers = [
|
|
5941
|
-
|
|
5942
|
-
"投放时长",
|
|
5943
|
-
"当前出价策略",
|
|
5944
|
-
"推荐出价策略",
|
|
5945
|
-
"状态",
|
|
5946
|
-
];
|
|
5947
|
-
const headerHtml = headers
|
|
5948
|
-
.map((header) => `<th>${escapeHtml(header)}</th>`)
|
|
5949
|
-
.join("");
|
|
5467
|
+
const headers = ["广告系列", "投放时长", "当前出价策略", "推荐出价策略", "状态"];
|
|
5468
|
+
const headerHtml = headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("");
|
|
5950
5469
|
|
|
5951
5470
|
const tableHtml = `
|
|
5952
5471
|
<div class="table-block" data-table-key="bidding-strategy">
|
|
@@ -6010,9 +5529,7 @@
|
|
|
6010
5529
|
const generatedInsights = generateAdCreativeInsights(items);
|
|
6011
5530
|
|
|
6012
5531
|
const headers = ["广告", "Headlines", "Descriptions", "优化建议"];
|
|
6013
|
-
const headerHtml = headers
|
|
6014
|
-
.map((header) => `<th>${escapeHtml(header)}</th>`)
|
|
6015
|
-
.join("");
|
|
5532
|
+
const headerHtml = headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("");
|
|
6016
5533
|
|
|
6017
5534
|
const tableHtml = `
|
|
6018
5535
|
<div class="table-block" data-table-key="ad-creative">
|
|
@@ -6112,9 +5629,7 @@
|
|
|
6112
5629
|
if (!reportMain) return null;
|
|
6113
5630
|
|
|
6114
5631
|
const newFeatures = data.newFeatures || {};
|
|
6115
|
-
const items = Array.isArray(newFeatures.items)
|
|
6116
|
-
? newFeatures.items
|
|
6117
|
-
: [];
|
|
5632
|
+
const items = Array.isArray(newFeatures.items) ? newFeatures.items : [];
|
|
6118
5633
|
const newFeaturesTable = buildNewFeaturesRows(items);
|
|
6119
5634
|
|
|
6120
5635
|
const headers = [
|
|
@@ -6122,9 +5637,7 @@
|
|
|
6122
5637
|
"账户状态 (Account Status)",
|
|
6123
5638
|
"优化师建议 (Senior Optimizer Recommendation)",
|
|
6124
5639
|
];
|
|
6125
|
-
const headerHtml = headers
|
|
6126
|
-
.map((header) => `<th>${escapeHtml(header)}</th>`)
|
|
6127
|
-
.join("");
|
|
5640
|
+
const headerHtml = headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("");
|
|
6128
5641
|
|
|
6129
5642
|
const fragment = document.createDocumentFragment();
|
|
6130
5643
|
const header = document.createElement("div");
|
|
@@ -6166,9 +5679,7 @@
|
|
|
6166
5679
|
if (!reportMain) return null;
|
|
6167
5680
|
|
|
6168
5681
|
const summary = data.summary || {};
|
|
6169
|
-
const keyIssues = Array.isArray(summary.keyIssues)
|
|
6170
|
-
? summary.keyIssues
|
|
6171
|
-
: [];
|
|
5682
|
+
const keyIssues = Array.isArray(summary.keyIssues) ? summary.keyIssues : [];
|
|
6172
5683
|
const roadmapItems = Array.isArray(summary.optimizationRoadmap)
|
|
6173
5684
|
? summary.optimizationRoadmap
|
|
6174
5685
|
: [];
|
|
@@ -6237,9 +5748,7 @@
|
|
|
6237
5748
|
const reportMain = document.getElementById("report-main");
|
|
6238
5749
|
if (!reportMain) return null;
|
|
6239
5750
|
|
|
6240
|
-
const landingPageItems = processLandingPageAnalysisData(
|
|
6241
|
-
data.landingPageAnalysis,
|
|
6242
|
-
);
|
|
5751
|
+
const landingPageItems = processLandingPageAnalysisData(data.landingPageAnalysis);
|
|
6243
5752
|
const landingPageRows = buildLandingPageRows(landingPageItems);
|
|
6244
5753
|
|
|
6245
5754
|
const fragment = document.createDocumentFragment();
|
|
@@ -6288,7 +5797,7 @@
|
|
|
6288
5797
|
if (!reportMain) return;
|
|
6289
5798
|
|
|
6290
5799
|
chartJobs.length = 0;
|
|
6291
|
-
chartInstances.forEach((chart) => chart.
|
|
5800
|
+
chartInstances.forEach((chart) => chart.dispose());
|
|
6292
5801
|
chartInstances.clear();
|
|
6293
5802
|
reportMain.innerHTML = "";
|
|
6294
5803
|
|
|
@@ -6306,27 +5815,21 @@
|
|
|
6306
5815
|
createNewFeaturesSection(),
|
|
6307
5816
|
createSummarySection(),
|
|
6308
5817
|
];
|
|
6309
|
-
sections
|
|
6310
|
-
.filter(Boolean)
|
|
6311
|
-
.forEach((section) => reportMain.appendChild(section));
|
|
5818
|
+
sections.filter(Boolean).forEach((section) => reportMain.appendChild(section));
|
|
6312
5819
|
renderPendingCharts();
|
|
6313
5820
|
};
|
|
6314
5821
|
|
|
6315
5822
|
renderSections();
|
|
6316
5823
|
|
|
6317
5824
|
const setupNavigation = () => {
|
|
6318
|
-
const navTags = Array.from(
|
|
6319
|
-
document.querySelectorAll(".diagnosis-navigation .nav-tag"),
|
|
6320
|
-
);
|
|
5825
|
+
const navTags = Array.from(document.querySelectorAll(".diagnosis-navigation .nav-tag"));
|
|
6321
5826
|
if (!navTags.length) return;
|
|
6322
5827
|
|
|
6323
5828
|
const observedSections = [];
|
|
6324
5829
|
const observedSet = new Set();
|
|
6325
5830
|
|
|
6326
5831
|
navItems.forEach((item) => {
|
|
6327
|
-
const contentEl = document.querySelector(
|
|
6328
|
-
`[data-section-id="${item.id}"]`,
|
|
6329
|
-
);
|
|
5832
|
+
const contentEl = document.querySelector(`[data-section-id="${item.id}"]`);
|
|
6330
5833
|
if (contentEl && !observedSet.has(contentEl)) {
|
|
6331
5834
|
observedSections.push(contentEl);
|
|
6332
5835
|
observedSet.add(contentEl);
|
|
@@ -6339,9 +5842,7 @@
|
|
|
6339
5842
|
});
|
|
6340
5843
|
|
|
6341
5844
|
const setActiveTag = (id) => {
|
|
6342
|
-
navTags.forEach((tag) =>
|
|
6343
|
-
tag.classList.toggle("active", tag.dataset.target === id),
|
|
6344
|
-
);
|
|
5845
|
+
navTags.forEach((tag) => tag.classList.toggle("active", tag.dataset.target === id));
|
|
6345
5846
|
};
|
|
6346
5847
|
|
|
6347
5848
|
const scrollToSection = (id) => {
|
|
@@ -6371,8 +5872,7 @@
|
|
|
6371
5872
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
|
6372
5873
|
|
|
6373
5874
|
if (visible.length) {
|
|
6374
|
-
const activeTarget =
|
|
6375
|
-
visible[0].target.dataset.sectionId || visible[0].target.id;
|
|
5875
|
+
const activeTarget = visible[0].target.dataset.sectionId || visible[0].target.id;
|
|
6376
5876
|
if (activeTarget) {
|
|
6377
5877
|
setActiveTag(activeTarget);
|
|
6378
5878
|
}
|
|
@@ -6397,9 +5897,7 @@
|
|
|
6397
5897
|
setupNavigation();
|
|
6398
5898
|
|
|
6399
5899
|
const setupTableExpanders = () => {
|
|
6400
|
-
const buttons = document.querySelectorAll(
|
|
6401
|
-
".expand-button[data-table-key]",
|
|
6402
|
-
);
|
|
5900
|
+
const buttons = document.querySelectorAll(".expand-button[data-table-key]");
|
|
6403
5901
|
if (!buttons.length) return;
|
|
6404
5902
|
|
|
6405
5903
|
buttons.forEach((button) => {
|