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.
Files changed (120) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1679 -1633
  3. package/dist/skill/AGENTS.md +11 -10
  4. package/dist/skill/SKILL.md +1 -1
  5. package/dist/skill/_meta.json +2 -2
  6. package/dist/skill/assets/campaign-create-template.md +15 -15
  7. package/dist/skill/assets/market-analysis-rules.md +14 -14
  8. package/dist/skill/assets/pmax-create-template.md +6 -6
  9. package/dist/skill/assets/pmax-lead-form-template.md +3 -3
  10. package/dist/skill/references/README.md +21 -21
  11. package/dist/skill/references/accounts/accounts.md +42 -42
  12. package/dist/skill/references/analytics/account-analytics.md +46 -39
  13. package/dist/skill/references/analytics/keyword-planner-workflows.md +14 -13
  14. package/dist/skill/references/analytics/rag.md +7 -7
  15. package/dist/skill/references/analytics/reporting.md +1 -1
  16. package/dist/skill/references/core/agent-conventions.md +20 -20
  17. package/dist/skill/references/core/intent-routing.md +116 -117
  18. package/dist/skill/references/core/subagent-orchestration.md +1 -1
  19. package/dist/skill/references/core/workflows.md +12 -12
  20. package/dist/skill/references/google-ads/google-ads-campaign-plan.md +29 -31
  21. package/dist/skill/references/google-ads/google-ads.md +7 -7
  22. package/dist/skill/references/google-ads/rules/google-ads-keyword-taxonomy.md +14 -14
  23. package/dist/skill/references/operations/hosted-automation-optimize-index.md +2 -2
  24. package/dist/skill/references/operations/hosted-automation-scenarios.md +2 -2
  25. package/dist/skill/references/operations/hosted-automation-self-control.md +6 -6
  26. package/dist/skill/references/operations/hosted-automation-user-catalog.md +17 -17
  27. package/dist/skill/references/report-templates/README.md +68 -0
  28. package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +2 -2
  29. package/dist/skill/references/report-templates/bing-period-report.md +53 -53
  30. package/dist/skill/references/report-templates/google-ads-diagnosis.md +28 -2
  31. package/dist/skill/references/report-templates/google-inquiry-analysis.md +8 -8
  32. package/dist/skill/references/report-templates/google-period-report-excel.md +2 -2
  33. package/dist/skill/references/report-templates/okki-weekly-google-client.md +55 -55
  34. package/dist/skill/references/report-templates/tiktok-period-report.md +22 -21
  35. package/dist/skill/report-templates/GoogleAdsDiagnosisReport.html +390 -892
  36. package/dist/skill/report-templates/REPORT-WORKFLOW.md +2 -2
  37. package/dist/skill/report-templates/bing-period-report.md +53 -53
  38. package/dist/skill/report-templates/google-ads-diagnosis.md +28 -2
  39. package/dist/skill/report-templates/google-inquiry-analysis.md +8 -8
  40. package/dist/skill/report-templates/google-period-report-excel.md +2 -2
  41. package/dist/skill/report-templates/meta-period-report.html +1 -4
  42. package/dist/skill/report-templates/okki-weekly-google-client.md +55 -55
  43. package/dist/skill/report-templates/report-template-academic.html +0 -2
  44. package/dist/skill/report-templates/report-template.html +0 -2
  45. package/dist/skill/report-templates/tiktok-period-report.md +22 -21
  46. package/dist/skill/report-templates/website-diagnosis-report.html +1 -5
  47. package/dist/skill/scripts/install.ps1 +1 -1
  48. package/dist/skill/scripts/install.sh +1 -1
  49. package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +2 -14
  50. package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +1 -3
  51. package/eval/cases/accounts-single-balance-not-bulk.scenario.json +3 -14
  52. package/eval/cases/budget-display-not-raw-micros.scenario.json +1 -8
  53. package/eval/cases/clue-meta-leads-json.scenario.json +2 -14
  54. package/eval/cases/clue-tiktok-leads-json.scenario.json +2 -11
  55. package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +3 -9
  56. package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +3 -9
  57. package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +3 -9
  58. package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +2 -11
  59. package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +3 -10
  60. package/eval/cases/facebook-analysis-period-default.scenario.json +2 -14
  61. package/eval/cases/finance-invoice-info-list.scenario.json +3 -11
  62. package/eval/cases/forewarning-list-google.scenario.json +3 -14
  63. package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +2 -6
  64. package/eval/cases/google-analysis-keywords-route.scenario.json +2 -14
  65. package/eval/cases/human-p1-multiturn.scenario.json +1 -5
  66. package/eval/cases/ir-p1-vs-p8-account-vs-url.scenario.json +5 -17
  67. package/eval/cases/ir-p4-period-not-diagnosis.scenario.json +3 -16
  68. package/eval/cases/ir-p8-network-diagnosis-with-url.scenario.json +3 -25
  69. package/eval/cases/ir-p8-website-monitor-report-with-url.scenario.json +3 -28
  70. package/eval/cases/ir-p8-website-test-report-no-url.scenario.json +3 -14
  71. package/eval/cases/ir-p9-industry-not-p8.scenario.json +3 -19
  72. package/eval/cases/ir-vague-report-clarify.scenario.json +3 -14
  73. package/eval/cases/ir-w3-vs-p8-from-website.scenario.json +2 -8
  74. package/eval/cases/ir-w5-keyword-planner-url-core-terms.scenario.json +2 -22
  75. package/eval/cases/meta-single-balance-not-bulk.scenario.json +3 -17
  76. package/eval/cases/no-legacy-json-flag.scenario.json +2 -6
  77. package/eval/cases/open-account-google-noninteractive.scenario.json +1 -3
  78. package/eval/cases/open-account-tiktok-license-file.scenario.json +1 -3
  79. package/eval/cases/optimize-list-by-account.scenario.json +3 -11
  80. package/eval/cases/p1-single-account-profile.scenario.json +1 -11
  81. package/eval/cases/p2-balance-scan-bulk.scenario.json +2 -9
  82. package/eval/cases/p3-accounts-digest.scenario.json +1 -5
  83. package/eval/cases/p4-fb-meta-period-report.scenario.json +2 -12
  84. package/eval/cases/p4-period-report-window.scenario.json +1 -8
  85. package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +2 -13
  86. package/eval/cases/pmax-brand-edit-routing.scenario.json +3 -16
  87. package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +2 -12
  88. package/eval/cases/pmax-enable-brand-guidelines.scenario.json +2 -12
  89. package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +3 -14
  90. package/eval/cases/rag-before-keyword-expand.scenario.json +1 -11
  91. package/eval/cases/rag-list-then-query.scenario.json +2 -14
  92. package/eval/cases/report-list-google.scenario.json +2 -11
  93. package/eval/cases/report-push-list-google.scenario.json +2 -11
  94. package/eval/cases/setup-login-or-env.scenario.json +1 -3
  95. package/eval/cases/setup-siluzan-data-permission-env.scenario.json +1 -3
  96. package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +2 -6
  97. package/eval/cases/time-range-user-delegates-default.scenario.json +1 -8
  98. package/eval/cases/tips-json-out-filtering.scenario.json +1 -3
  99. package/eval/cases/tips-large-json-pagination.scenario.json +1 -3
  100. package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +2 -11
  101. package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +1 -3
  102. package/eval/cases/uj-ad-website-search-plan-table-format.scenario.json +1 -3
  103. package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +6 -18
  104. package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +1 -8
  105. package/eval/cases/uj-analytics-ecommerce-industry-report.scenario.json +2 -14
  106. package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +2 -11
  107. package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +1 -3
  108. package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +2 -11
  109. package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +1 -4
  110. package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +2 -17
  111. package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +2 -14
  112. package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +2 -6
  113. package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +3 -17
  114. package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +1 -5
  115. package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +1 -3
  116. package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +3 -17
  117. package/eval/cases/uj-patrol-google-balances-low.scenario.json +2 -11
  118. package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +3 -14
  119. package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +2 -14
  120. 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 canvas,
2053
- .chart-container canvas {
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/chart.umd.min.js"></script>
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
- desktopPassed,
2352
- mobilePassed,
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
- list,
2584
- tableKey,
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
- (sum, item) => sum + item.cost,
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
- type: "doughnut",
2961
- data: {
2962
- labels,
2963
- datasets: [
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
- options: {
2980
- responsive: true,
2981
- maintainAspectRatio: false,
2982
- plugins: {
2983
- legend: {
2984
- position: "bottom",
2985
- labels: { usePointStyle: true },
2986
- },
2987
- title: {
2988
- display: true,
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
- (item) => Number(item?.conversions) || 0,
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
- type: "bar",
3013
- data: {
3014
- labels,
3015
- datasets: [
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
- type: "pie",
3066
- data: {
3067
- labels,
3068
- datasets: [
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
- options: {
3085
- responsive: true,
3086
- maintainAspectRatio: false,
3087
- plugins: {
3088
- legend: {
3089
- position: "right",
3090
- labels: { usePointStyle: true },
3091
- },
3092
- title: {
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
- type: "bar",
3126
- data: {
3127
- labels,
3128
- datasets: [
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
- options: {
3146
- responsive: true,
3147
- maintainAspectRatio: false,
3148
- plugins: {
3149
- legend: {
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
- (sum, item) => sum + (Number(item?.[field]) || 0),
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
- "整体转化率低于 3%,流量质量或后端承接存在优化空间。",
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
- `平均每次转化费用约 ${avgCpa.toFixed(2)},成本偏高。`,
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
- `也有性价比较高的关键词:${efficient.join("")},贡献优质转化。`,
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
- formatDisplayPercent(item?.costShare, 2),
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
- type: "bar",
3574
- data: {
3575
- labels: ["暂无数据"],
3576
- datasets: [
3577
- {
3578
- label: "花费占比 (%)",
3579
- data: [0],
3580
- backgroundColor: "rgba(59, 104, 249, 0.6)",
3581
- borderRadius: 6,
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
- type: "bar",
3623
- data: {
3624
- labels,
3625
- datasets: [
3626
- {
3627
- label: "花费占比 (%)",
3628
- data: costShareValues,
3629
- backgroundColor: "rgba(59, 104, 249, 0.7)",
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
- options: {
3645
- responsive: true,
3646
- maintainAspectRatio: false,
3647
- interaction: { mode: "index", intersect: false },
3648
- plugins: {
3649
- legend: { position: "top" },
3650
- title: { display: true, text: "关键词表现趋势" },
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
- scales: {
3659
- y: {
3660
- beginAtZero: true,
3661
- ticks: { callback: (value) => `${value}%` },
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
- type: "bar",
3678
- data: {
3679
- labels: ["暂无数据"],
3680
- datasets: [
3681
- {
3682
- label: "展示次数",
3683
- data: [0],
3684
- backgroundColor: "rgba(245, 158, 11, 0.7)",
3685
- borderRadius: 6,
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
- String(item?.keyword || "未命名"),
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
- type: "bar",
3714
- data: {
3715
- labels,
3716
- datasets: [
3717
- {
3718
- label: "展示次数",
3719
- data: impressionsData,
3720
- backgroundColor: "rgba(245, 158, 11, 0.75)",
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
- options: {
3736
- responsive: true,
3737
- maintainAspectRatio: false,
3738
- interaction: { mode: "index", intersect: false },
3739
- plugins: {
3740
- legend: { position: "top" },
3741
- title: { display: true, text: "搜索词表现趋势" },
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
- scales: {
3744
- y: { beginAtZero: true },
3745
- x: {
3746
- ticks: {
3747
- maxRotation: 45,
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
- return "启用";
4228
- if (value.includes("暂停") || value.includes("Paused"))
4229
- return "已暂停";
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.Chart !== "function") {
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 canvas = document.getElementById(job.id);
4304
- if (!canvas) continue;
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.destroy();
3938
+ existing.dispose();
4310
3939
  }
4311
- const chart = new window.Chart(ctx, job.config);
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
- type: "line",
4344
- data: {
4345
- labels,
4346
- datasets: [
4347
- {
4348
- label: "消耗",
4349
- data: costData,
4350
- borderColor: "#3b82f6",
4351
- backgroundColor: "rgba(59, 130, 246, 0.12)",
4352
- tension: 0.3,
4353
- yAxisID: "y1",
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
- scales: {
4376
- y1: {
4377
- type: "linear",
4378
- position: "left",
4379
- beginAtZero: true,
4380
- ticks: { font: { size: 11 } },
4381
- title: { display: true, text: "消耗", font: { size: 12 } },
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
- type: "line",
4408
- data: {
4409
- labels,
4410
- datasets: [
4411
- {
4412
- label: "CPA",
4413
- data: cpaData,
4414
- borderColor: "#3b82f6",
4415
- backgroundColor: "rgba(59, 130, 246, 0.12)",
4416
- tension: 0.3,
4417
- yAxisID: "y1",
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
- scales: {
4442
- y1: {
4443
- type: "linear",
4444
- position: "left",
4445
- beginAtZero: true,
4446
- ticks: { font: { size: 11 } },
4447
- title: { display: true, text: "CPA", font: { size: 12 } },
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: "bar",
4476
- data: {
4477
- labels,
4478
- datasets: [
4479
- {
4480
- label: "指标数值",
4481
- data: datasetValues,
4482
- backgroundColor: ["#3b82f6", "#10b981", "#f97316"],
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
- ? overview.advantages
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
- ({ key, name, benchmarkKey }) => {
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
- return row;
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
- displayItems: primaryConversionGoals,
4859
- remainingItems: additionalEnabledGoals,
4860
- } = applyTableLimit(enabledGoals, "conversion-goals", {
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
- conversionCost.analysis,
5018
- ).map((item) => `<li class="insight-item">${escapeHtml(item)}</li>`);
5019
- const conversionTrendSuggestionsList = toArray(
5020
- conversionCost.suggestions,
5021
- ).map((item) => `<li class="insight-item">${escapeHtml(item)}</li>`);
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 = buildConversionCostDailyTableHtml(
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
- '<tr><td colspan="9" class="empty-row">暂无数据</td></tr>',
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
- <canvas id="${chartId}" height="340"></canvas>
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
- <canvas id="${chartId}" height="340"></canvas>
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
- ? biddingStrategy.items
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.destroy());
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) => {