siluzan-tso-cli 1.1.45 → 1.1.46-beta.2

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 (27) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +480 -106
  3. package/dist/skill/_meta.json +2 -2
  4. package/dist/skill/references/accounts/accounts-balance-stats.md +10 -10
  5. package/dist/skill/references/accounts/accounts-list.md +1 -1
  6. package/dist/skill/references/accounts/accounts-permissions.md +2 -2
  7. package/dist/skill/references/accounts/finance.md +1 -1
  8. package/dist/skill/references/accounts/open-account-by-media.md +1 -1
  9. package/dist/skill/references/accounts/open-account-google-ui.md +1 -1
  10. package/dist/skill/references/analytics/account-analytics.md +74 -13
  11. package/dist/skill/references/analytics/rag.md +1 -1
  12. package/dist/skill/references/analytics/reporting.md +5 -5
  13. package/dist/skill/references/core/setup.md +5 -5
  14. package/dist/skill/references/google-ads/rules/google-ads-keyword-strategy.md +8 -8
  15. package/dist/skill/references/misc/tso-home.md +2 -2
  16. package/dist/skill/references/operations/hosted-automation-monitoring-json.md +9 -9
  17. package/dist/skill/references/operations/hosted-automation-scenarios.md +2 -0
  18. package/dist/skill/references/report-templates/REPORT-WORKFLOW.md +3 -3
  19. package/dist/skill/references/report-templates/yandex-period-report-excel.md +45 -45
  20. package/dist/skill/references/report-templates/yandex-period-report.md +26 -27
  21. package/dist/skill/report-templates/REPORT-WORKFLOW.md +3 -3
  22. package/dist/skill/report-templates/yandex-period-report-excel.md +45 -45
  23. package/dist/skill/report-templates/yandex-period-report.html +1377 -1164
  24. package/dist/skill/report-templates/yandex-period-report.md +26 -27
  25. package/dist/skill/scripts/install.ps1 +3 -3
  26. package/dist/skill/scripts/install.sh +3 -3
  27. package/package.json +1 -1
@@ -4,190 +4,416 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>Yandex Ads 账户分析报告</title>
7
- <style>
8
- :root {
9
- --bg: #f3f1ec;
10
- --surface: #fffdf8;
11
- --ink: #1c1b19;
12
- --muted: #5c584f;
13
- --line: #d9d3c6;
14
- --yandex: #fc3f1d;
15
- --yandex-deep: #d92f12;
16
- --ok: #1f7a4c;
17
- --ok-bg: #e5f4eb;
18
- --danger: #b42318;
19
- --danger-bg: #fdeceb;
20
- --info: #175cd3;
21
- --info-bg: #e8f1fc;
22
- --warn: #b54708;
23
- --warn-bg: #fff4e5;
24
- --gray: #667085;
25
- --shadow: 0 18px 40px rgba(28, 27, 25, 0.08);
26
- --radius: 18px;
27
- --max: 1180px;
28
- }
29
- * { box-sizing: border-box; }
30
- html { scroll-behavior: smooth; }
31
- body {
32
- margin: 0;
33
- color: var(--ink);
34
- background:
35
- radial-gradient(1200px 500px at 10% -10%, rgba(252, 63, 29, 0.10), transparent 55%),
36
- radial-gradient(900px 400px at 90% 0%, rgba(23, 92, 211, 0.08), transparent 50%),
37
- linear-gradient(180deg, #f7f4ee 0%, var(--bg) 40%, #ebe7de 100%);
38
- font-family: "Noto Sans SC", "IBM Plex Sans", sans-serif;
39
- line-height: 1.55;
40
- }
41
- a { color: inherit; text-decoration: none; }
42
- .wrap { width: min(var(--max), calc(100% - 32px)); margin: 0 auto; }
43
- .topbar {
44
- position: sticky; top: 0; z-index: 20;
45
- backdrop-filter: blur(14px);
46
- background: rgba(243, 241, 236, 0.86);
47
- border-bottom: 1px solid rgba(217, 211, 198, 0.9);
48
- }
49
- .topbar-inner {
50
- display: flex; align-items: center; justify-content: space-between;
51
- gap: 16px; min-height: 64px;
52
- }
53
- .brand { display: flex; align-items: center; gap: 12px; font-weight: 700; }
54
- .brand-mark {
55
- width: 34px; height: 34px; border-radius: 10px;
56
- background: linear-gradient(145deg, var(--yandex), #ff7a3d);
57
- display: grid; place-items: center; color: white;
58
- font-family: "IBM Plex Sans", sans-serif; font-weight: 700;
59
- box-shadow: 0 8px 18px rgba(252, 63, 29, 0.28);
60
- }
61
- .nav { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
62
- .nav a {
63
- padding: 7px 10px; border-radius: 999px; color: var(--muted);
64
- font-size: 12px; font-weight: 500;
65
- }
66
- .nav a:hover, .nav a.active {
67
- background: var(--surface); color: var(--ink);
68
- box-shadow: inset 0 0 0 1px var(--line);
69
- }
70
- .hero { padding: 36px 0 10px; }
71
- .eyebrow {
72
- display: inline-flex; align-items: center; gap: 8px;
73
- color: var(--yandex-deep); font-size: 13px; font-weight: 600; margin-bottom: 10px;
74
- }
75
- .eyebrow::before {
76
- content: ""; width: 8px; height: 8px; border-radius: 50%;
77
- background: var(--yandex); box-shadow: 0 0 0 4px rgba(252, 63, 29, 0.15);
78
- }
79
- h1 {
80
- margin: 0 0 10px; font-size: clamp(26px, 4vw, 36px);
81
- line-height: 1.2; letter-spacing: -0.02em;
82
- }
83
- .hero-sub { color: var(--muted); margin: 0 0 22px; font-size: 15px; }
84
- .meta-row { display: flex; flex-wrap: wrap; gap: 10px; }
85
- .chip {
86
- display: inline-flex; align-items: center; gap: 8px;
87
- padding: 8px 12px; border-radius: 999px; background: var(--surface);
88
- border: 1px solid var(--line); font-size: 13px; color: var(--muted);
89
- }
90
- .chip strong { color: var(--ink); font-weight: 600; }
91
- section { padding: 20px 0; }
92
- .section-head {
93
- display: flex; align-items: end; justify-content: space-between;
94
- gap: 12px; margin-bottom: 14px;
95
- }
96
- .section-head h2 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
97
- .section-head p { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
98
- .panel {
99
- background: var(--surface); border: 1px solid var(--line);
100
- border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
101
- }
102
- .kpi-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
103
- .kpi {
104
- padding: 18px 18px 16px; border-radius: 16px; background: var(--surface);
105
- border: 1px solid var(--line); box-shadow: var(--shadow);
106
- }
107
- .kpi .label { color: var(--muted); font-size: 12px; font-weight: 500; }
108
- .kpi .value {
109
- margin-top: 6px; font-family: "IBM Plex Sans", sans-serif;
110
- font-size: 26px; font-weight: 700; letter-spacing: -0.03em;
111
- }
112
- .kpi .delta { margin-top: 8px; font-size: 12px; font-weight: 500; color: var(--muted); }
113
- .meta-grid {
114
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 18px;
115
- padding: 18px 20px; border-bottom: 1px solid var(--line); font-size: 13px;
116
- }
117
- .meta-grid dt { color: var(--muted); margin: 0; }
118
- .meta-grid dd { margin: 2px 0 0; font-weight: 600; }
119
- .table-wrap { overflow-x: auto; }
120
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
121
- th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
122
- th {
123
- background: #f7f3ea; color: var(--muted); font-weight: 600;
124
- font-size: 12px; white-space: nowrap;
125
- }
126
- tr:last-child td { border-bottom: none; }
127
- tr.total td { font-weight: 700; background: #faf7f0; }
128
- .num { font-family: "IBM Plex Sans", sans-serif; white-space: nowrap; }
129
- .badge {
130
- display: inline-flex; align-items: center;
131
- padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap;
132
- }
133
- .b-ok { background: var(--ok-bg); color: var(--ok); }
134
- .b-warn { background: var(--warn-bg); color: var(--warn); }
135
- .b-danger { background: var(--danger-bg); color: var(--danger); }
136
- .b-info { background: var(--info-bg); color: var(--info); }
137
- .b-gray { background: #f2f4f7; color: var(--gray); }
138
- .issue-card {
139
- margin: 14px 0 0; padding: 16px 18px; border-radius: 14px;
140
- border: 1px solid var(--line); background: #fff;
141
- }
142
- .issue-card h3 { margin: 0 0 8px; font-size: 15px; }
143
- .issue-card .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
144
- .issue-card p { margin: 0 0 8px; color: var(--muted); font-size: 13px; }
145
- .issue-card ol { margin: 8px 0 0; padding-left: 18px; font-size: 13px; }
146
- .narrative {
147
- padding: 18px 20px; border-top: 1px solid var(--line);
148
- }
149
- .narrative h3 {
150
- margin: 0 0 10px; font-size: 15px; display: flex; align-items: center; gap: 8px;
151
- }
152
- .narrative ul { margin: 0; padding-left: 18px; color: var(--ink); font-size: 14px; }
153
- .narrative li { margin-bottom: 8px; }
154
- .narrative + .narrative { border-top: 1px dashed var(--line); }
155
- details.fold { margin-top: 12px; }
156
- details.fold > summary {
157
- cursor: pointer; list-style: none; padding: 12px 16px;
158
- background: #f7f3ea; font-weight: 600; font-size: 13px;
159
- border-top: 1px solid var(--line);
160
- }
161
- details.fold > summary::-webkit-details-marker { display: none; }
162
- details.fold[open] > summary { border-bottom: 1px solid var(--line); }
163
- .todo-list { display: grid; gap: 8px; padding: 4px 0 0; }
164
- .todo-item {
165
- display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: start;
166
- padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); background: #fff;
167
- font-size: 13px;
168
- }
169
- .todo-item a { color: var(--info); font-weight: 600; }
170
- footer {
171
- padding: 28px 0 48px; color: var(--muted); font-size: 12px; text-align: center;
172
- }
173
- @media (max-width: 960px) {
174
- .kpi-grid, .meta-grid { grid-template-columns: repeat(2, 1fr); }
175
- .todo-item { grid-template-columns: 1fr; }
176
- }
177
- @media (max-width: 640px) {
178
- .kpi-grid, .meta-grid { grid-template-columns: 1fr; }
179
- .topbar-inner { flex-direction: column; align-items: flex-start; padding: 10px 0; }
180
- }
181
- </style>
182
7
  <style>
183
- /* 补充:开发样例未覆盖的极少数辅助样式(其余全部复用样例的 .panel/.narrative/.issue-card/.todo-item/.badge) */
184
- .empty-note {
185
- color: var(--muted);
186
- padding: 18px;
187
- text-align: center;
188
- font-size: 13px;
189
- }
190
- .panel > .table-wrap:first-child table { margin-top: 0; }
8
+ :root {
9
+ --bg: #f3f1ec;
10
+ --surface: #fffdf8;
11
+ --ink: #1c1b19;
12
+ --muted: #5c584f;
13
+ --line: #d9d3c6;
14
+ --yandex: #fc3f1d;
15
+ --yandex-deep: #d92f12;
16
+ --ok: #1f7a4c;
17
+ --ok-bg: #e5f4eb;
18
+ --danger: #b42318;
19
+ --danger-bg: #fdeceb;
20
+ --info: #175cd3;
21
+ --info-bg: #e8f1fc;
22
+ --warn: #b54708;
23
+ --warn-bg: #fff4e5;
24
+ --gray: #667085;
25
+ --shadow: 0 18px 40px rgba(28, 27, 25, 0.08);
26
+ --radius: 18px;
27
+ --max: 1180px;
28
+ }
29
+ * {
30
+ box-sizing: border-box;
31
+ }
32
+ html {
33
+ scroll-behavior: smooth;
34
+ }
35
+ body {
36
+ margin: 0;
37
+ color: var(--ink);
38
+ background:
39
+ radial-gradient(1200px 500px at 10% -10%, rgba(252, 63, 29, 0.1), transparent 55%),
40
+ radial-gradient(900px 400px at 90% 0%, rgba(23, 92, 211, 0.08), transparent 50%),
41
+ linear-gradient(180deg, #f7f4ee 0%, var(--bg) 40%, #ebe7de 100%);
42
+ font-family: "Noto Sans SC", "IBM Plex Sans", sans-serif;
43
+ line-height: 1.55;
44
+ }
45
+ a {
46
+ color: inherit;
47
+ text-decoration: none;
48
+ }
49
+ .wrap {
50
+ width: min(var(--max), calc(100% - 32px));
51
+ margin: 0 auto;
52
+ }
53
+ .topbar {
54
+ position: sticky;
55
+ top: 0;
56
+ z-index: 20;
57
+ backdrop-filter: blur(14px);
58
+ background: rgba(243, 241, 236, 0.86);
59
+ border-bottom: 1px solid rgba(217, 211, 198, 0.9);
60
+ }
61
+ .topbar-inner {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: space-between;
65
+ gap: 16px;
66
+ min-height: 64px;
67
+ }
68
+ .brand {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 12px;
72
+ font-weight: 700;
73
+ }
74
+ .brand-mark {
75
+ width: 34px;
76
+ height: 34px;
77
+ border-radius: 10px;
78
+ background: linear-gradient(145deg, var(--yandex), #ff7a3d);
79
+ display: grid;
80
+ place-items: center;
81
+ color: white;
82
+ font-family: "IBM Plex Sans", sans-serif;
83
+ font-weight: 700;
84
+ box-shadow: 0 8px 18px rgba(252, 63, 29, 0.28);
85
+ }
86
+ .nav {
87
+ display: flex;
88
+ flex-wrap: wrap;
89
+ gap: 4px;
90
+ justify-content: flex-end;
91
+ }
92
+ .nav a {
93
+ padding: 7px 10px;
94
+ border-radius: 999px;
95
+ color: var(--muted);
96
+ font-size: 12px;
97
+ font-weight: 500;
98
+ }
99
+ .nav a:hover,
100
+ .nav a.active {
101
+ background: var(--surface);
102
+ color: var(--ink);
103
+ box-shadow: inset 0 0 0 1px var(--line);
104
+ }
105
+ .hero {
106
+ padding: 36px 0 10px;
107
+ }
108
+ .eyebrow {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ gap: 8px;
112
+ color: var(--yandex-deep);
113
+ font-size: 13px;
114
+ font-weight: 600;
115
+ margin-bottom: 10px;
116
+ }
117
+ .eyebrow::before {
118
+ content: "";
119
+ width: 8px;
120
+ height: 8px;
121
+ border-radius: 50%;
122
+ background: var(--yandex);
123
+ box-shadow: 0 0 0 4px rgba(252, 63, 29, 0.15);
124
+ }
125
+ h1 {
126
+ margin: 0 0 10px;
127
+ font-size: clamp(26px, 4vw, 36px);
128
+ line-height: 1.2;
129
+ letter-spacing: -0.02em;
130
+ }
131
+ .hero-sub {
132
+ color: var(--muted);
133
+ margin: 0 0 22px;
134
+ font-size: 15px;
135
+ }
136
+ .meta-row {
137
+ display: flex;
138
+ flex-wrap: wrap;
139
+ gap: 10px;
140
+ }
141
+ .chip {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ gap: 8px;
145
+ padding: 8px 12px;
146
+ border-radius: 999px;
147
+ background: var(--surface);
148
+ border: 1px solid var(--line);
149
+ font-size: 13px;
150
+ color: var(--muted);
151
+ }
152
+ .chip strong {
153
+ color: var(--ink);
154
+ font-weight: 600;
155
+ }
156
+ section {
157
+ padding: 20px 0;
158
+ }
159
+ .section-head {
160
+ display: flex;
161
+ align-items: end;
162
+ justify-content: space-between;
163
+ gap: 12px;
164
+ margin-bottom: 14px;
165
+ }
166
+ .section-head h2 {
167
+ margin: 0;
168
+ font-size: 22px;
169
+ letter-spacing: -0.02em;
170
+ }
171
+ .section-head p {
172
+ margin: 4px 0 0;
173
+ color: var(--muted);
174
+ font-size: 13px;
175
+ }
176
+ .panel {
177
+ background: var(--surface);
178
+ border: 1px solid var(--line);
179
+ border-radius: var(--radius);
180
+ box-shadow: var(--shadow);
181
+ overflow: hidden;
182
+ }
183
+ .kpi-grid {
184
+ display: grid;
185
+ grid-template-columns: repeat(5, 1fr);
186
+ gap: 12px;
187
+ }
188
+ .kpi {
189
+ padding: 18px 18px 16px;
190
+ border-radius: 16px;
191
+ background: var(--surface);
192
+ border: 1px solid var(--line);
193
+ box-shadow: var(--shadow);
194
+ }
195
+ .kpi .label {
196
+ color: var(--muted);
197
+ font-size: 12px;
198
+ font-weight: 500;
199
+ }
200
+ .kpi .value {
201
+ margin-top: 6px;
202
+ font-family: "IBM Plex Sans", sans-serif;
203
+ font-size: 26px;
204
+ font-weight: 700;
205
+ letter-spacing: -0.03em;
206
+ }
207
+ .kpi .delta {
208
+ margin-top: 8px;
209
+ font-size: 12px;
210
+ font-weight: 500;
211
+ color: var(--muted);
212
+ }
213
+ .meta-grid {
214
+ display: grid;
215
+ grid-template-columns: repeat(3, 1fr);
216
+ gap: 10px 18px;
217
+ padding: 18px 20px;
218
+ border-bottom: 1px solid var(--line);
219
+ font-size: 13px;
220
+ }
221
+ .meta-grid dt {
222
+ color: var(--muted);
223
+ margin: 0;
224
+ }
225
+ .meta-grid dd {
226
+ margin: 2px 0 0;
227
+ font-weight: 600;
228
+ }
229
+ .table-wrap {
230
+ overflow-x: auto;
231
+ }
232
+ table {
233
+ width: 100%;
234
+ border-collapse: collapse;
235
+ font-size: 13px;
236
+ }
237
+ th,
238
+ td {
239
+ padding: 10px 12px;
240
+ text-align: left;
241
+ border-bottom: 1px solid var(--line);
242
+ vertical-align: top;
243
+ }
244
+ th {
245
+ background: #f7f3ea;
246
+ color: var(--muted);
247
+ font-weight: 600;
248
+ font-size: 12px;
249
+ white-space: nowrap;
250
+ }
251
+ tr:last-child td {
252
+ border-bottom: none;
253
+ }
254
+ tr.total td {
255
+ font-weight: 700;
256
+ background: #faf7f0;
257
+ }
258
+ .num {
259
+ font-family: "IBM Plex Sans", sans-serif;
260
+ white-space: nowrap;
261
+ }
262
+ .badge {
263
+ display: inline-flex;
264
+ align-items: center;
265
+ padding: 4px 9px;
266
+ border-radius: 999px;
267
+ font-size: 12px;
268
+ font-weight: 600;
269
+ white-space: nowrap;
270
+ }
271
+ .b-ok {
272
+ background: var(--ok-bg);
273
+ color: var(--ok);
274
+ }
275
+ .b-warn {
276
+ background: var(--warn-bg);
277
+ color: var(--warn);
278
+ }
279
+ .b-danger {
280
+ background: var(--danger-bg);
281
+ color: var(--danger);
282
+ }
283
+ .b-info {
284
+ background: var(--info-bg);
285
+ color: var(--info);
286
+ }
287
+ .b-gray {
288
+ background: #f2f4f7;
289
+ color: var(--gray);
290
+ }
291
+ .issue-card {
292
+ margin: 14px 0 0;
293
+ padding: 16px 18px;
294
+ border-radius: 14px;
295
+ border: 1px solid var(--line);
296
+ background: #fff;
297
+ }
298
+ .issue-card h3 {
299
+ margin: 0 0 8px;
300
+ font-size: 15px;
301
+ }
302
+ .issue-card .meta {
303
+ display: flex;
304
+ flex-wrap: wrap;
305
+ gap: 8px;
306
+ margin-bottom: 10px;
307
+ }
308
+ .issue-card p {
309
+ margin: 0 0 8px;
310
+ color: var(--muted);
311
+ font-size: 13px;
312
+ }
313
+ .issue-card ol {
314
+ margin: 8px 0 0;
315
+ padding-left: 18px;
316
+ font-size: 13px;
317
+ }
318
+ .narrative {
319
+ padding: 18px 20px;
320
+ border-top: 1px solid var(--line);
321
+ }
322
+ .narrative h3 {
323
+ margin: 0 0 10px;
324
+ font-size: 15px;
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 8px;
328
+ }
329
+ .narrative ul {
330
+ margin: 0;
331
+ padding-left: 18px;
332
+ color: var(--ink);
333
+ font-size: 14px;
334
+ }
335
+ .narrative li {
336
+ margin-bottom: 8px;
337
+ }
338
+ .narrative + .narrative {
339
+ border-top: 1px dashed var(--line);
340
+ }
341
+ details.fold {
342
+ margin-top: 12px;
343
+ }
344
+ details.fold > summary {
345
+ cursor: pointer;
346
+ list-style: none;
347
+ padding: 12px 16px;
348
+ background: #f7f3ea;
349
+ font-weight: 600;
350
+ font-size: 13px;
351
+ border-top: 1px solid var(--line);
352
+ }
353
+ details.fold > summary::-webkit-details-marker {
354
+ display: none;
355
+ }
356
+ details.fold[open] > summary {
357
+ border-bottom: 1px solid var(--line);
358
+ }
359
+ .todo-list {
360
+ display: grid;
361
+ gap: 8px;
362
+ padding: 4px 0 0;
363
+ }
364
+ .todo-item {
365
+ display: grid;
366
+ grid-template-columns: auto 1fr auto;
367
+ gap: 12px;
368
+ align-items: start;
369
+ padding: 12px 14px;
370
+ border-radius: 12px;
371
+ border: 1px solid var(--line);
372
+ background: #fff;
373
+ font-size: 13px;
374
+ }
375
+ .todo-item a {
376
+ color: var(--info);
377
+ font-weight: 600;
378
+ }
379
+ footer {
380
+ padding: 28px 0 48px;
381
+ color: var(--muted);
382
+ font-size: 12px;
383
+ text-align: center;
384
+ }
385
+ @media (max-width: 960px) {
386
+ .kpi-grid,
387
+ .meta-grid {
388
+ grid-template-columns: repeat(2, 1fr);
389
+ }
390
+ .todo-item {
391
+ grid-template-columns: 1fr;
392
+ }
393
+ }
394
+ @media (max-width: 640px) {
395
+ .kpi-grid,
396
+ .meta-grid {
397
+ grid-template-columns: 1fr;
398
+ }
399
+ .topbar-inner {
400
+ flex-direction: column;
401
+ align-items: flex-start;
402
+ padding: 10px 0;
403
+ }
404
+ }
405
+ </style>
406
+ <style>
407
+ /* 补充:开发样例未覆盖的极少数辅助样式(其余全部复用样例的 .panel/.narrative/.issue-card/.todo-item/.badge) */
408
+ .empty-note {
409
+ color: var(--muted);
410
+ padding: 18px;
411
+ text-align: center;
412
+ font-size: 13px;
413
+ }
414
+ .panel > .table-wrap:first-child table {
415
+ margin-top: 0;
416
+ }
191
417
  </style>
192
418
  </head>
193
419
  <body>
@@ -216,1040 +442,1027 @@
216
442
  window.__YANDEX_PERIOD_REPORT__ = window.__YANDEX_PERIOD_REPORT__ || null;
217
443
  </script>
218
444
  <script>
445
+ (function () {
446
+ "use strict";
219
447
 
220
- (function () {
221
- "use strict";
448
+ const EMPTY_NOTE = "本节暂无数据";
222
449
 
223
- const EMPTY_NOTE = "本节暂无数据";
450
+ function escapeHtml(s) {
451
+ return String(s ?? "")
452
+ .replace(/&/g, "&amp;")
453
+ .replace(/</g, "&lt;")
454
+ .replace(/>/g, "&gt;")
455
+ .replace(/"/g, "&quot;");
456
+ }
224
457
 
225
- function escapeHtml(s) {
226
- return String(s ?? "")
227
- .replace(/&/g, "&amp;")
228
- .replace(/</g, "&lt;")
229
- .replace(/>/g, "&gt;")
230
- .replace(/"/g, "&quot;");
231
- }
458
+ /**
459
+ * 统一的“有效数字”判定:null / undefined / "" 一律视为无数据(返回 undefined),
460
+ * 避免 `Number(null) === 0` 把「无转化时 CPA 为空」误显示成 "$0.00" / "0.00%"。
461
+ */
462
+ function toNum(v) {
463
+ if (v === null || v === undefined || v === "") return undefined;
464
+ const n = Number(v);
465
+ return Number.isFinite(n) ? n : undefined;
466
+ }
232
467
 
233
- /**
234
- * 统一的“有效数字”判定:null / undefined / "" 一律视为无数据(返回 undefined),
235
- * 避免 `Number(null) === 0` 把「无转化时 CPA 为空」误显示成 "$0.00" / "0.00%"。
236
- */
237
- function toNum(v) {
238
- if (v === null || v === undefined || v === "") return undefined;
239
- const n = Number(v);
240
- return Number.isFinite(n) ? n : undefined;
241
- }
468
+ function int(v) {
469
+ const n = toNum(v);
470
+ return n === undefined ? "—" : Math.round(n).toLocaleString("en-US");
471
+ }
242
472
 
243
- function int(v) {
244
- const n = toNum(v);
245
- return n === undefined ? "—" : Math.round(n).toLocaleString("en-US");
246
- }
473
+ function money(v, currency) {
474
+ const n = toNum(v);
475
+ if (n === undefined) return "—";
476
+ const c = currency || "USD";
477
+ if (c === "USD" || c === "RUB" || c === "EUR") {
478
+ try {
479
+ return new Intl.NumberFormat("en-US", {
480
+ style: "currency",
481
+ currency: c,
482
+ maximumFractionDigits: 2,
483
+ }).format(n);
484
+ } catch (_) {}
485
+ }
486
+ return c + " " + n.toFixed(2);
487
+ }
247
488
 
248
- function money(v, currency) {
249
- const n = toNum(v);
250
- if (n === undefined) return "—";
251
- const c = currency || "USD";
252
- if (c === "USD" || c === "RUB" || c === "EUR") {
253
- try {
254
- return new Intl.NumberFormat("en-US", {
255
- style: "currency",
256
- currency: c,
257
- maximumFractionDigits: 2,
258
- }).format(n);
259
- } catch (_) {}
260
- }
261
- return c + " " + n.toFixed(2);
262
- }
489
+ function pct(v) {
490
+ const n = toNum(v);
491
+ if (n === undefined) return "—";
492
+ return (n * 100).toFixed(2) + "%";
493
+ }
263
494
 
264
- function pct(v) {
265
- const n = toNum(v);
266
- if (n === undefined) return "—";
267
- return (n * 100).toFixed(2) + "%";
268
- }
495
+ function num2(v) {
496
+ const n = toNum(v);
497
+ return n === undefined ? "—" : n.toFixed(2);
498
+ }
269
499
 
270
- function num2(v) {
271
- const n = toNum(v);
272
- return n === undefined ? "—" : n.toFixed(2);
273
- }
500
+ /** 接口未回 CTR/CPC/转化率时,用展示/点击/消耗/转化补算(与 merge-snapshot 口径一致)。 */
501
+ function derivedCtr(r) {
502
+ const existing = toNum(r && r.ctr);
503
+ if (existing !== undefined) return existing;
504
+ const impressions = toNum(r && r.impressions);
505
+ const clicks = toNum(r && r.clicks);
506
+ return impressions > 0 && clicks !== undefined ? clicks / impressions : undefined;
507
+ }
274
508
 
275
- /** 接口未回 CTR/CPC/转化率时,用展示/点击/消耗/转化补算(与 merge-snapshot 口径一致)。 */
276
- function derivedCtr(r) {
277
- const existing = toNum(r && r.ctr);
278
- if (existing !== undefined) return existing;
279
- const impressions = toNum(r && r.impressions);
280
- const clicks = toNum(r && r.clicks);
281
- return impressions > 0 && clicks !== undefined ? clicks / impressions : undefined;
282
- }
509
+ function derivedCpc(r) {
510
+ const existing = toNum(r && r.averageCpc);
511
+ if (existing !== undefined) return existing;
512
+ const spend = toNum(r && r.spend);
513
+ const clicks = toNum(r && r.clicks);
514
+ return clicks > 0 && spend !== undefined ? spend / clicks : undefined;
515
+ }
283
516
 
284
- function derivedCpc(r) {
285
- const existing = toNum(r && r.averageCpc);
286
- if (existing !== undefined) return existing;
287
- const spend = toNum(r && r.spend);
288
- const clicks = toNum(r && r.clicks);
289
- return clicks > 0 && spend !== undefined ? spend / clicks : undefined;
290
- }
517
+ function derivedCr(r) {
518
+ const existing = toNum(r && r.conversionRate);
519
+ if (existing !== undefined) return existing;
520
+ const clicks = toNum(r && r.clicks);
521
+ const conversions = toNum(r && r.conversions);
522
+ return clicks > 0 && conversions !== undefined ? conversions / clicks : undefined;
523
+ }
291
524
 
292
- function derivedCr(r) {
293
- const existing = toNum(r && r.conversionRate);
294
- if (existing !== undefined) return existing;
295
- const clicks = toNum(r && r.clicks);
296
- const conversions = toNum(r && r.conversions);
297
- return clicks > 0 && conversions !== undefined ? conversions / clicks : undefined;
298
- }
525
+ function derivedCpa(r) {
526
+ const existing = toNum(r && r.costPerConversion);
527
+ if (existing !== undefined) return existing;
528
+ const spend = toNum(r && r.spend);
529
+ const conversions = toNum(r && r.conversions);
530
+ return conversions > 0 && spend !== undefined ? spend / conversions : undefined;
531
+ }
299
532
 
300
- function derivedCpa(r) {
301
- const existing = toNum(r && r.costPerConversion);
302
- if (existing !== undefined) return existing;
303
- const spend = toNum(r && r.spend);
304
- const conversions = toNum(r && r.conversions);
305
- return conversions > 0 && spend !== undefined ? spend / conversions : undefined;
306
- }
533
+ function asArr(v) {
534
+ return Array.isArray(v) ? v : [];
535
+ }
307
536
 
308
- function asArr(v) {
309
- return Array.isArray(v) ? v : [];
310
- }
537
+ function currencyOf(data) {
538
+ return data.meta?.currency || data.kpis?.currency || "USD";
539
+ }
311
540
 
312
- function currencyOf(data) {
313
- return data.meta?.currency || data.kpis?.currency || "USD";
314
- }
541
+ /**
542
+ * 分析 + 建议区块——样式对齐开发样例的 `.narrative`(badge「叙述」/「建议」+ ul),
543
+ * 而不是本文件之前自造的 `.insight` 盒子。
544
+ */
545
+ function analysisBlock(section) {
546
+ const analysis = asArr(section?.analysis).filter((x) => String(x || "").trim());
547
+ const suggestions = asArr(section?.suggestions).filter((x) => String(x || "").trim());
548
+ const parts = [];
549
+ if (analysis.length) {
550
+ parts.push(
551
+ '<div class="narrative"><h3><span class="badge b-info">叙述</span> 数据解读</h3><ul>' +
552
+ analysis.map((a) => "<li>" + escapeHtml(a) + "</li>").join("") +
553
+ "</ul></div>",
554
+ );
555
+ } else {
556
+ parts.push('<div class="empty-note">' + EMPTY_NOTE + "</div>");
557
+ }
558
+ if (suggestions.length) {
559
+ parts.push(
560
+ '<div class="narrative"><h3><span class="badge b-warn">建议</span> 优化建议</h3><ul>' +
561
+ suggestions.map((s) => "<li>" + escapeHtml(s) + "</li>").join("") +
562
+ "</ul></div>",
563
+ );
564
+ }
565
+ return parts.join("");
566
+ }
315
567
 
316
- /**
317
- * 分析 + 建议区块——样式对齐开发样例的 `.narrative`(badge「叙述」/「建议」+ ul),
318
- * 而不是本文件之前自造的 `.insight` 盒子。
319
- */
320
- function analysisBlock(section) {
321
- const analysis = asArr(section?.analysis).filter((x) => String(x || "").trim());
322
- const suggestions = asArr(section?.suggestions).filter((x) => String(x || "").trim());
323
- const parts = [];
324
- if (analysis.length) {
325
- parts.push(
326
- '<div class="narrative"><h3><span class="badge b-info">叙述</span> 数据解读</h3><ul>' +
327
- analysis.map((a) => "<li>" + escapeHtml(a) + "</li>").join("") +
328
- "</ul></div>"
329
- );
330
- } else {
331
- parts.push('<div class="empty-note">' + EMPTY_NOTE + "</div>");
332
- }
333
- if (suggestions.length) {
334
- parts.push(
335
- '<div class="narrative"><h3><span class="badge b-warn">建议</span> 优化建议</h3><ul>' +
336
- suggestions.map((s) => "<li>" + escapeHtml(s) + "</li>").join("") +
337
- "</ul></div>"
338
- );
339
- }
340
- return parts.join("");
341
- }
568
+ function sectionHead(title, desc) {
569
+ return (
570
+ '<div class="section-head"><div><h2>' +
571
+ escapeHtml(title) +
572
+ "</h2>" +
573
+ (desc ? "<p>" + escapeHtml(desc) + "</p>" : "") +
574
+ "</div></div>"
575
+ );
576
+ }
342
577
 
343
- function sectionHead(title, desc) {
344
- return (
345
- '<div class="section-head"><div><h2>' +
346
- escapeHtml(title) +
347
- "</h2>" +
348
- (desc ? '<p>' + escapeHtml(desc) + "</p>" : "") +
349
- "</div></div>"
350
- );
351
- }
578
+ /** 仅返回 `.table-wrap`(不含外层 `.panel`),供调用方与叙述区块共用同一个 `.panel` 容器,对齐开发样例结构。 */
579
+ function tableHtml(headers, rowsHtml) {
580
+ if (!rowsHtml) return '<div class="empty-note">' + EMPTY_NOTE + "</div>";
581
+ return (
582
+ '<div class="table-wrap"><table><thead><tr>' +
583
+ headers
584
+ .map(function (h) {
585
+ const cls = h.num ? ' class="num"' : "";
586
+ return "<th" + cls + ">" + escapeHtml(h.label) + "</th>";
587
+ })
588
+ .join("") +
589
+ "</tr></thead><tbody>" +
590
+ rowsHtml +
591
+ "</tbody></table></div>"
592
+ );
593
+ }
352
594
 
353
- /** 仅返回 `.table-wrap`(不含外层 `.panel`),供调用方与叙述区块共用同一个 `.panel` 容器,对齐开发样例结构。 */
354
- function tableHtml(headers, rowsHtml) {
355
- if (!rowsHtml) return '<div class="empty-note">' + EMPTY_NOTE + "</div>";
356
- return (
357
- '<div class="table-wrap"><table><thead><tr>' +
358
- headers
359
- .map(function (h) {
360
- const cls = h.num ? ' class="num"' : "";
361
- return "<th" + cls + ">" + escapeHtml(h.label) + "</th>";
362
- })
363
- .join("") +
364
- "</tr></thead><tbody>" +
365
- rowsHtml +
366
- "</tbody></table></div>"
367
- );
368
- }
595
+ function topBySpend(rows, n) {
596
+ return asArr(rows)
597
+ .slice()
598
+ .sort(function (a, b) {
599
+ return (Number(b.spend) || 0) - (Number(a.spend) || 0);
600
+ })
601
+ .slice(0, n);
602
+ }
369
603
 
370
- function topBySpend(rows, n) {
371
- return asArr(rows)
372
- .slice()
373
- .sort(function (a, b) {
374
- return (Number(b.spend) || 0) - (Number(a.spend) || 0);
375
- })
376
- .slice(0, n);
377
- }
604
+ function normKey(v) {
605
+ return String(v ?? "")
606
+ .trim()
607
+ .toLowerCase();
608
+ }
378
609
 
379
- function normKey(v) {
380
- return String(v ?? "").trim().toLowerCase();
381
- }
610
+ /** 周区间文案:含 NaN 时回退,避免脚本误聚周把 Invalid Date 写进表格。 */
611
+ function weekRangeText(r) {
612
+ const start = String((r && r.startDate) || "").trim();
613
+ const end = String((r && r.endDate) || "").trim();
614
+ const joined = start && end ? start + " ~ " + end : start || end;
615
+ if (joined && !/nan/i.test(joined)) return joined;
616
+ const label = String((r && r.weekLabel) || "").trim();
617
+ if (label && !/nan/i.test(label)) return label;
618
+ return "—";
619
+ }
382
620
 
383
- /** 周区间文案:含 NaN 时回退,避免脚本误聚周把 Invalid Date 写进表格。 */
384
- function weekRangeText(r) {
385
- const start = String((r && r.startDate) || "").trim();
386
- const end = String((r && r.endDate) || "").trim();
387
- const joined = start && end ? start + " ~ " + end : start || end;
388
- if (joined && !/nan/i.test(joined)) return joined;
389
- const label = String((r && r.weekLabel) || "").trim();
390
- if (label && !/nan/i.test(label)) return label;
391
- return "—";
392
- }
621
+ function weekLabelText(r) {
622
+ const label = String((r && (r.isoWeek || r.weekLabel)) || "").trim();
623
+ if (label && !/nan/i.test(label)) return label;
624
+ return "—";
625
+ }
393
626
 
394
- function weekLabelText(r) {
395
- const label = String((r && (r.isoWeek || r.weekLabel)) || "").trim();
396
- if (label && !/nan/i.test(label)) return label;
397
- return "—";
398
- }
627
+ /**
628
+ * 从 Agent 提供的 `narrative.rowInsights.<dim>[]` 按主字段匹配行洞察(目前仅关键词中文翻译)。
629
+ * 找不到返回 null——渲染层显示「—」,不编造内容。
630
+ * 每条洞察支持 `{match:{...}, ...}` 或扁平写法 `{keyword, campaignName, ...}`。
631
+ */
632
+ function findInsight(list, primaryField, primaryValue, row) {
633
+ const target = normKey(primaryValue);
634
+ if (!target) return null;
635
+ const candidates = asArr(list).filter(function (it) {
636
+ const m = (it && it.match) || it || {};
637
+ return normKey(m[primaryField]) === target;
638
+ });
639
+ if (candidates.length <= 1) return candidates[0] || null;
640
+ const cn = normKey(row && row.campaignName);
641
+ const narrowed = candidates.find(function (it) {
642
+ const m = (it && it.match) || it || {};
643
+ return normKey(m.campaignName) === cn;
644
+ });
645
+ return narrowed || candidates[0];
646
+ }
399
647
 
400
- /**
401
- * 从 Agent 提供的 `narrative.rowInsights.<dim>[]` 按主字段匹配行洞察(目前仅关键词中文翻译)。
402
- * 找不到返回 null——渲染层显示「—」,不编造内容。
403
- * 每条洞察支持 `{match:{...}, ...}` 或扁平写法 `{keyword, campaignName, ...}`。
404
- */
405
- function findInsight(list, primaryField, primaryValue, row) {
406
- const target = normKey(primaryValue);
407
- if (!target) return null;
408
- const candidates = asArr(list).filter(function (it) {
409
- const m = (it && it.match) || it || {};
410
- return normKey(m[primaryField]) === target;
411
- });
412
- if (candidates.length <= 1) return candidates[0] || null;
413
- const cn = normKey(row && row.campaignName);
414
- const narrowed = candidates.find(function (it) {
415
- const m = (it && it.match) || it || {};
416
- return normKey(m.campaignName) === cn;
417
- });
418
- return narrowed || candidates[0];
419
- }
648
+ function rowInsightsOf(data, key) {
649
+ return asArr(((data.narrative || {}).rowInsights || {})[key]);
650
+ }
420
651
 
421
- function rowInsightsOf(data, key) {
422
- return asArr(((data.narrative || {}).rowInsights || {})[key]);
423
- }
652
+ /**
653
+ * 「样本」徽标——纯阈值判断(基于点击量),不依赖大模型/不编造:
654
+ * 点击 <30 视为极小样本,效率类结论不可用;<100 转化类结论慎出;否则样本充足。
655
+ */
656
+ function sampleBadge(clicks) {
657
+ const c = toNum(clicks) || 0;
658
+ if (c < 30) {
659
+ return (
660
+ '<span class="badge b-danger">点击 ' + int(c) + ",样本极小,效率类结论不可用</span>"
661
+ );
662
+ }
663
+ if (c < 100) {
664
+ return '<span class="badge b-warn">点击 ' + int(c) + ",转化类结论慎出</span>";
665
+ }
666
+ return '<span class="badge b-ok">点击 ' + int(c) + ",样本充足</span>";
667
+ }
424
668
 
425
- /**
426
- * 「样本」徽标——纯阈值判断(基于点击量),不依赖大模型/不编造:
427
- * 点击 <30 视为极小样本,效率类结论不可用;<100 转化类结论慎出;否则样本充足。
428
- */
429
- function sampleBadge(clicks) {
430
- const c = toNum(clicks) || 0;
431
- if (c < 30) {
432
- return '<span class="badge b-danger">点击 ' + int(c) + ",样本极小,效率类结论不可用</span>";
433
- }
434
- if (c < 100) {
435
- return '<span class="badge b-warn">点击 ' + int(c) + ",转化类结论慎出</span>";
436
- }
437
- return '<span class="badge b-ok">点击 ' + int(c) + ",样本充足</span>";
438
- }
669
+ /** Campaign 状态徽标——只用真实 status/state 字段做颜色归类。 */
670
+ function campaignStatusBadge(r) {
671
+ const raw = r.status || r.state || "";
672
+ if (!raw) return '<span class="badge b-gray">—</span>';
673
+ const low = String(raw).toLowerCase();
674
+ let cls = "b-gray";
675
+ if (/(active|running|accept|enable)/.test(low)) cls = "b-ok";
676
+ else if (/(reject|declin|error)/.test(low)) cls = "b-danger";
677
+ else if (/(moderat|review|pending)/.test(low)) cls = "b-warn";
678
+ return '<span class="badge ' + cls + '">' + escapeHtml(String(raw)) + "</span>";
679
+ }
439
680
 
440
- /** Campaign 状态徽标——只用真实 status/state 字段做颜色归类。 */
441
- function campaignStatusBadge(r) {
442
- const raw = r.status || r.state || "";
443
- if (!raw) return '<span class="badge b-gray">—</span>';
444
- const low = String(raw).toLowerCase();
445
- let cls = "b-gray";
446
- if (/(active|running|accept|enable)/.test(low)) cls = "b-ok";
447
- else if (/(reject|declin|error)/.test(low)) cls = "b-danger";
448
- else if (/(moderat|review|pending)/.test(low)) cls = "b-warn";
449
- return '<span class="badge ' + cls + '">' + escapeHtml(String(raw)) + "</span>";
450
- }
681
+ function renderHero(data) {
682
+ const meta = data.meta || {};
683
+ const period =
684
+ meta.startDate && meta.endDate
685
+ ? meta.startDate + " ~ " + meta.endDate
686
+ : "统计区间未提供";
687
+ const title =
688
+ meta.title ||
689
+ (meta.accountName || "Yandex 广告账户") +
690
+ " · Yandex " +
691
+ (meta.startDate ? String(meta.startDate).slice(0, 7) : "") +
692
+ " 数据报告";
693
+ return (
694
+ '<div class="wrap hero">' +
695
+ '<div class="eyebrow">YANDEX DIRECT · 账户分析报告</div>' +
696
+ "<h1>" +
697
+ escapeHtml(title) +
698
+ "</h1>" +
699
+ '<div class="meta-row">' +
700
+ '<div class="chip">账户 <strong>' +
701
+ escapeHtml(meta.accountId || "—") +
702
+ "</strong></div>" +
703
+ '<div class="chip">货币 <strong>' +
704
+ escapeHtml(currencyOf(data)) +
705
+ "</strong></div>" +
706
+ (meta.region
707
+ ? '<div class="chip">地区 <strong>' + escapeHtml(meta.region) + "</strong></div>"
708
+ : "") +
709
+ '<div class="chip">区间 <strong>' +
710
+ escapeHtml(period) +
711
+ "</strong></div>" +
712
+ "</div></div>"
713
+ );
714
+ }
451
715
 
452
- function renderHero(data) {
453
- const meta = data.meta || {};
454
- const period =
455
- meta.startDate && meta.endDate
456
- ? meta.startDate + " ~ " + meta.endDate
457
- : "统计区间未提供";
458
- const title =
459
- meta.title ||
460
- (meta.accountName || "Yandex 广告账户") +
461
- " · Yandex " +
462
- (meta.startDate ? String(meta.startDate).slice(0, 7) : "") +
463
- " 数据报告";
464
- return (
465
- '<div class="wrap hero">' +
466
- '<div class="eyebrow">YANDEX DIRECT · 账户分析报告</div>' +
467
- "<h1>" +
468
- escapeHtml(title) +
469
- "</h1>" +
470
- '<div class="meta-row">' +
471
- '<div class="chip">账户 <strong>' +
472
- escapeHtml(meta.accountId || "—") +
473
- "</strong></div>" +
474
- '<div class="chip">货币 <strong>' +
475
- escapeHtml(currencyOf(data)) +
476
- "</strong></div>" +
477
- (meta.region
478
- ? '<div class="chip">地区 <strong>' + escapeHtml(meta.region) + "</strong></div>"
479
- : "") +
480
- '<div class="chip">区间 <strong>' +
481
- escapeHtml(period) +
482
- "</strong></div>" +
483
- "</div></div>"
484
- );
485
- }
716
+ function renderOverview(data) {
717
+ const meta = data.meta || {};
718
+ const kpis = data.kpis || {};
719
+ const currency = currencyOf(data);
720
+ const narrative = data.narrative || {};
721
+ const sec = (narrative.sections || {}).overview || {};
722
+ const balAsOf = kpis.balanceAsOf ? String(kpis.balanceAsOf).slice(0, 10) : "";
723
+ const runway =
724
+ kpis.runwayDays != null ? "按日均约 " + int(kpis.runwayDays) + " 天(估算)" : "";
725
+ const activeHint = kpis.activeDays != null ? int(kpis.activeDays) + " 个有效投放日" : "";
486
726
 
487
- function renderOverview(data) {
488
- const meta = data.meta || {};
489
- const kpis = data.kpis || {};
490
- const currency = currencyOf(data);
491
- const narrative = data.narrative || {};
492
- const sec = (narrative.sections || {}).overview || {};
493
- const balAsOf = kpis.balanceAsOf
494
- ? String(kpis.balanceAsOf).slice(0, 10)
495
- : "";
496
- const runway =
497
- kpis.runwayDays != null
498
- ? "按日均约 " + int(kpis.runwayDays) + " 天(估算)"
499
- : "";
500
- const activeHint =
501
- kpis.activeDays != null
502
- ? int(kpis.activeDays) + " 个有效投放日"
503
- : "";
727
+ // 对齐开发样例:5 张 KPI 卡(余额/消耗/日均消耗/点击-CTR/转化-CPA),展示量并入「点击 / CTR」的 delta 文案。
728
+ const kpiCards = [
729
+ {
730
+ label: "账户余额" + (balAsOf ? "(截至 " + balAsOf + ")" : ""),
731
+ value: money(kpis.balance, currency),
732
+ delta: runway,
733
+ },
734
+ {
735
+ label: "本期消耗",
736
+ value: money(kpis.spend, currency),
737
+ delta: activeHint,
738
+ },
739
+ {
740
+ label: "日均消耗",
741
+ value: money(kpis.avgDailySpend, currency),
742
+ delta: activeHint,
743
+ },
744
+ {
745
+ label: "点击 / CTR",
746
+ value: int(kpis.clicks),
747
+ delta:
748
+ "CTR " +
749
+ pct(kpis.ctr) +
750
+ " · CPC " +
751
+ money(kpis.averageCpc, currency) +
752
+ (kpis.impressions != null ? " · 展示 " + int(kpis.impressions) : ""),
753
+ },
754
+ {
755
+ label: "转化 / CPA",
756
+ value: num2(kpis.conversions),
757
+ delta:
758
+ kpis.costPerConversion != null
759
+ ? "CPA " + money(kpis.costPerConversion, currency)
760
+ : "",
761
+ },
762
+ ]
763
+ .map(function (c) {
764
+ return (
765
+ '<div class="kpi"><div class="label">' +
766
+ escapeHtml(c.label) +
767
+ '</div><div class="value">' +
768
+ c.value +
769
+ "</div>" +
770
+ (c.delta ? '<div class="delta">' + escapeHtml(c.delta) + "</div>" : "") +
771
+ "</div>"
772
+ );
773
+ })
774
+ .join("");
504
775
 
505
- // 对齐开发样例:5 张 KPI 卡(余额/消耗/日均消耗/点击-CTR/转化-CPA),展示量并入「点击 / CTR」的 delta 文案。
506
- const kpiCards = [
507
- {
508
- label: "账户余额" + (balAsOf ? "(截至 " + balAsOf + ")" : ""),
509
- value: money(kpis.balance, currency),
510
- delta: runway,
511
- },
512
- {
513
- label: "本期消耗",
514
- value: money(kpis.spend, currency),
515
- delta: activeHint,
516
- },
517
- {
518
- label: "日均消耗",
519
- value: money(kpis.avgDailySpend, currency),
520
- delta: activeHint,
521
- },
522
- {
523
- label: "点击 / CTR",
524
- value: int(kpis.clicks),
525
- delta:
526
- "CTR " +
527
- pct(kpis.ctr) +
528
- " · CPC " +
529
- money(kpis.averageCpc, currency) +
530
- (kpis.impressions != null ? " · 展示 " + int(kpis.impressions) : ""),
531
- },
532
- {
533
- label: "转化 / CPA",
534
- value: num2(kpis.conversions),
535
- delta:
536
- kpis.costPerConversion != null
537
- ? "CPA " + money(kpis.costPerConversion, currency)
538
- : "",
539
- },
540
- ]
541
- .map(function (c) {
542
- return (
543
- '<div class="kpi"><div class="label">' +
544
- escapeHtml(c.label) +
545
- '</div><div class="value">' +
546
- c.value +
547
- "</div>" +
548
- (c.delta ? '<div class="delta">' + escapeHtml(c.delta) + "</div>" : "") +
549
- "</div>"
550
- );
551
- })
552
- .join("");
776
+ const weekly = asArr(data.tables?.weekly);
777
+ let weeklyTable = "";
778
+ if (weekly.length) {
779
+ const rows = weekly
780
+ .map(function (r) {
781
+ return (
782
+ "<tr><td>" +
783
+ escapeHtml(weekLabelText(r)) +
784
+ "</td><td>" +
785
+ escapeHtml(weekRangeText(r)) +
786
+ '</td><td class="num">' +
787
+ int(r.days) +
788
+ '</td><td class="num">' +
789
+ money(r.spend, currency) +
790
+ '</td><td class="num">' +
791
+ int(r.impressions) +
792
+ '</td><td class="num">' +
793
+ int(r.clicks) +
794
+ '</td><td class="num">' +
795
+ pct(r.ctr) +
796
+ '</td><td class="num">' +
797
+ money(r.averageCpc, currency) +
798
+ '</td><td class="num">' +
799
+ num2(r.conversions) +
800
+ "</td></tr>"
801
+ );
802
+ })
803
+ .join("");
804
+ weeklyTable = tableHtml(
805
+ [
806
+ { label: "周次" },
807
+ { label: "区间" },
808
+ { label: "天数", num: true },
809
+ { label: "消耗", num: true },
810
+ { label: "展示", num: true },
811
+ { label: "点击", num: true },
812
+ { label: "CTR", num: true },
813
+ { label: "CPC", num: true },
814
+ { label: "转化", num: true },
815
+ ],
816
+ rows,
817
+ );
818
+ }
553
819
 
554
- const weekly = asArr(data.tables?.weekly);
555
- let weeklyTable = "";
556
- if (weekly.length) {
557
- const rows = weekly
558
- .map(function (r) {
559
- return (
560
- "<tr><td>" +
561
- escapeHtml(weekLabelText(r)) +
562
- "</td><td>" +
563
- escapeHtml(weekRangeText(r)) +
564
- '</td><td class="num">' +
565
- int(r.days) +
566
- '</td><td class="num">' +
567
- money(r.spend, currency) +
568
- '</td><td class="num">' +
569
- int(r.impressions) +
570
- '</td><td class="num">' +
571
- int(r.clicks) +
572
- '</td><td class="num">' +
573
- pct(r.ctr) +
574
- '</td><td class="num">' +
575
- money(r.averageCpc, currency) +
576
- '</td><td class="num">' +
577
- num2(r.conversions) +
578
- "</td></tr>"
579
- );
580
- })
581
- .join("");
582
- weeklyTable = tableHtml(
583
- [
584
- { label: "周次" },
585
- { label: "区间" },
586
- { label: "天数", num: true },
587
- { label: "消耗", num: true },
588
- { label: "展示", num: true },
589
- { label: "点击", num: true },
590
- { label: "CTR", num: true },
591
- { label: "CPC", num: true },
592
- { label: "转化", num: true },
593
- ],
594
- rows
595
- );
596
- }
820
+ const daily = asArr(data.tables?.daily);
821
+ let dailyFold = "";
822
+ if (daily.length) {
823
+ const rows = daily
824
+ .map(function (r) {
825
+ return (
826
+ "<tr><td>" +
827
+ escapeHtml(r.date || "") +
828
+ '</td><td class="num">' +
829
+ money(r.spend, currency) +
830
+ '</td><td class="num">' +
831
+ int(r.impressions) +
832
+ '</td><td class="num">' +
833
+ int(r.clicks) +
834
+ '</td><td class="num">' +
835
+ pct(r.ctr) +
836
+ '</td><td class="num">' +
837
+ money(r.averageCpc, currency) +
838
+ '</td><td class="num">' +
839
+ num2(r.conversions) +
840
+ "</td></tr>"
841
+ );
842
+ })
843
+ .join("");
844
+ dailyFold =
845
+ '<details class="fold"><summary>每日消费数据概览(点击展开,共 ' +
846
+ daily.length +
847
+ " 天)</summary>" +
848
+ tableHtml(
849
+ [
850
+ { label: "日期" },
851
+ { label: "消耗", num: true },
852
+ { label: "展示", num: true },
853
+ { label: "点击", num: true },
854
+ { label: "CTR", num: true },
855
+ { label: "CPC", num: true },
856
+ { label: "转化", num: true },
857
+ ],
858
+ rows,
859
+ ) +
860
+ "</details>";
861
+ }
597
862
 
598
- const daily = asArr(data.tables?.daily);
599
- let dailyFold = "";
600
- if (daily.length) {
601
- const rows = daily
602
- .map(function (r) {
603
863
  return (
604
- "<tr><td>" +
605
- escapeHtml(r.date || "") +
606
- '</td><td class="num">' +
607
- money(r.spend, currency) +
608
- '</td><td class="num">' +
609
- int(r.impressions) +
610
- '</td><td class="num">' +
611
- int(r.clicks) +
612
- '</td><td class="num">' +
613
- pct(r.ctr) +
614
- '</td><td class="num">' +
615
- money(r.averageCpc, currency) +
616
- '</td><td class="num">' +
617
- num2(r.conversions) +
618
- "</td></tr>"
864
+ '<section id="overview"><div class="wrap">' +
865
+ sectionHead("账户总览") +
866
+ '<div class="panel" style="margin-bottom:14px"><dl class="meta-grid">' +
867
+ "<div><dt>账户 ID</dt><dd>" +
868
+ escapeHtml(meta.accountId || "—") +
869
+ "</dd></div>" +
870
+ "<div><dt>账户名称</dt><dd>" +
871
+ escapeHtml(meta.accountName || "—") +
872
+ "</dd></div>" +
873
+ "<div><dt>媒体平台</dt><dd>" +
874
+ escapeHtml(meta.platform || "Yandex Direct") +
875
+ "</dd></div>" +
876
+ "<div><dt>统计区间</dt><dd>" +
877
+ escapeHtml(
878
+ meta.startDate && meta.endDate ? meta.startDate + " ~ " + meta.endDate : "—",
879
+ ) +
880
+ "</dd></div>" +
881
+ "<div><dt>货币</dt><dd>" +
882
+ escapeHtml(currency) +
883
+ "</dd></div>" +
884
+ "<div><dt>投放地区</dt><dd>" +
885
+ escapeHtml(meta.region || "—") +
886
+ "</dd></div>" +
887
+ "</dl></div>" +
888
+ '<div class="kpi-grid" style="margin-bottom:14px">' +
889
+ kpiCards +
890
+ "</div>" +
891
+ '<div class="panel">' +
892
+ (weeklyTable
893
+ ? '<div style="padding:14px 16px 0; font-weight:600; font-size:14px;">汇总数据概览</div>' +
894
+ weeklyTable
895
+ : "") +
896
+ dailyFold +
897
+ analysisBlock(sec) +
898
+ nextNarrative(data) +
899
+ "</div>" +
900
+ "</div></section>"
619
901
  );
620
- })
621
- .join("");
622
- dailyFold =
623
- '<details class="fold"><summary>每日消费数据概览(点击展开,共 ' +
624
- daily.length +
625
- " 天)</summary>" +
626
- tableHtml(
627
- [
628
- { label: "日期" },
629
- { label: "消耗", num: true },
630
- { label: "展示", num: true },
631
- { label: "点击", num: true },
632
- { label: "CTR", num: true },
633
- { label: "CPC", num: true },
634
- { label: "转化", num: true },
635
- ],
636
- rows
637
- ) +
638
- "</details>";
639
- }
640
-
641
- const exec = asArr(narrative.executiveSummary)
642
- .filter(function (p) {
643
- return String(p || "").trim();
644
- })
645
- .map(function (p) {
646
- return "<p>" + escapeHtml(p) + "</p>";
647
- })
648
- .join("");
649
-
650
- return (
651
- '<section id="overview"><div class="wrap">' +
652
- sectionHead("账户总览") +
653
- '<div class="panel" style="margin-bottom:14px"><dl class="meta-grid">' +
654
- "<div><dt>账户 ID</dt><dd>" +
655
- escapeHtml(meta.accountId || "—") +
656
- "</dd></div>" +
657
- "<div><dt>账户名称</dt><dd>" +
658
- escapeHtml(meta.accountName || "—") +
659
- "</dd></div>" +
660
- "<div><dt>媒体平台</dt><dd>" +
661
- escapeHtml(meta.platform || "Yandex Direct") +
662
- "</dd></div>" +
663
- "<div><dt>统计区间</dt><dd>" +
664
- escapeHtml(
665
- meta.startDate && meta.endDate
666
- ? meta.startDate + " ~ " + meta.endDate
667
- : "—"
668
- ) +
669
- "</dd></div>" +
670
- "<div><dt>货币</dt><dd>" +
671
- escapeHtml(currency) +
672
- "</dd></div>" +
673
- "<div><dt>投放地区</dt><dd>" +
674
- escapeHtml(meta.region || "—") +
675
- "</dd></div>" +
676
- "</dl></div>" +
677
- '<div class="kpi-grid" style="margin-bottom:14px">' +
678
- kpiCards +
679
- "</div>" +
680
- '<div class="panel">' +
681
- (exec
682
- ? '<div style="padding:14px 16px 0; font-weight:600; font-size:14px;">一句话数据概况</div><div style="padding:6px 16px 0">' +
683
- exec +
684
- "</div>"
685
- : "") +
686
- (weeklyTable
687
- ? '<div style="padding:14px 16px 0; font-weight:600; font-size:14px;">汇总数据概览(周)</div>' +
688
- weeklyTable
689
- : "") +
690
- dailyFold +
691
- analysisBlock(sec) +
692
- nextNarrative(data) +
693
- "</div>" +
694
- "</div></section>"
695
- );
696
- }
902
+ }
697
903
 
698
- /** 将 priority(P0/P1/P2 或 high/medium/low)归一为开发样例的徽标文案 + 颜色类。 */
699
- function priorityBadge(pri, idx) {
700
- const raw = String(pri || "").trim().toLowerCase();
701
- if (raw === "high" || raw === "p0") return { label: "P0", cls: "b-danger" };
702
- if (raw === "low" || raw === "p2") return { label: "P2", cls: "b-info" };
703
- if (raw === "medium" || raw === "p1" || !raw) return { label: "P1", cls: "b-warn" };
704
- return { label: String(pri), cls: "b-warn" };
705
- }
904
+ /** 将 priority(P0/P1/P2 或 high/medium/low)归一为开发样例的徽标文案 + 颜色类。 */
905
+ function priorityBadge(pri, idx) {
906
+ const raw = String(pri || "")
907
+ .trim()
908
+ .toLowerCase();
909
+ if (raw === "high" || raw === "p0") return { label: "P0", cls: "b-danger" };
910
+ if (raw === "low" || raw === "p2") return { label: "P2", cls: "b-info" };
911
+ if (raw === "medium" || raw === "p1" || !raw) return { label: "P1", cls: "b-warn" };
912
+ return { label: String(pri), cls: "b-warn" };
913
+ }
706
914
 
707
- /**
708
- * 「后续优化动作」——对齐开发样例:不是独立 `<section>`,而是嵌在「账户总览」
709
- * 同一个 `.panel` 内、紧跟数据概述之后的第二个 `.narrative`(id="next" 供导航锚点定位)。
710
- */
711
- function nextNarrative(data) {
712
- const recs = asArr((data.narrative || {}).recommendations);
713
- if (!recs.length) {
714
- return (
715
- '<div class="narrative" id="next"><h3><span class="badge b-warn">待办</span> 后续优化动作与展望</h3>' +
716
- '<div class="empty-note">' +
717
- EMPTY_NOTE +
718
- "</div></div>"
719
- );
720
- }
721
- // 对齐开发样例的 `.todo-item`(badge + 文案 + 锚点链接三栏网格),而非普通 <li> 列表。
722
- const items = recs
723
- .map(function (r, i) {
724
- if (typeof r === "string") {
725
- const b = priorityBadge(null, i);
915
+ /**
916
+ * 「后续优化动作」——对齐开发样例:不是独立 `<section>`,而是嵌在「账户总览」
917
+ * 同一个 `.panel` 内、紧跟数据概述之后的第二个 `.narrative`(id="next" 供导航锚点定位)。
918
+ */
919
+ function nextNarrative(data) {
920
+ const recs = asArr((data.narrative || {}).recommendations);
921
+ if (!recs.length) {
922
+ return (
923
+ '<div class="narrative" id="next"><h3><span class="badge b-warn">待办</span> 后续优化动作与展望</h3>' +
924
+ '<div class="empty-note">' +
925
+ EMPTY_NOTE +
926
+ "</div></div>"
927
+ );
928
+ }
929
+ // 对齐开发样例的 `.todo-item`(badge + 文案 + 锚点链接三栏网格),而非普通 <li> 列表。
930
+ const items = recs
931
+ .map(function (r, i) {
932
+ if (typeof r === "string") {
933
+ const b = priorityBadge(null, i);
934
+ return (
935
+ '<div class="todo-item"><span class="badge ' +
936
+ b.cls +
937
+ '">' +
938
+ b.label +
939
+ "</span><div>" +
940
+ escapeHtml(r) +
941
+ "</div><span></span></div>"
942
+ );
943
+ }
944
+ const b = priorityBadge(r.priority, i);
945
+ const text = r.text || r.content || "";
946
+ const anchor = r.anchor
947
+ ? '<a href="' + escapeHtml(r.anchor) + '">→</a>'
948
+ : "<span></span>";
949
+ return (
950
+ '<div class="todo-item"><span class="badge ' +
951
+ b.cls +
952
+ '">' +
953
+ b.label +
954
+ "</span><div>" +
955
+ escapeHtml(text) +
956
+ "</div>" +
957
+ anchor +
958
+ "</div>"
959
+ );
960
+ })
961
+ .join("");
726
962
  return (
727
- '<div class="todo-item"><span class="badge ' +
728
- b.cls +
729
- '">' +
730
- b.label +
731
- "</span><div>" +
732
- escapeHtml(r) +
733
- "</div><span></span></div>"
963
+ '<div class="narrative" id="next"><h3><span class="badge b-warn">待办</span> 后续优化动作与展望</h3>' +
964
+ '<div class="todo-list">' +
965
+ items +
966
+ "</div></div>"
734
967
  );
735
968
  }
736
- const b = priorityBadge(r.priority, i);
737
- const text = r.text || r.content || "";
738
- const anchor = r.anchor
739
- ? '<a href="' + escapeHtml(r.anchor) + '">→</a>'
740
- : "<span></span>";
741
- return (
742
- '<div class="todo-item"><span class="badge ' +
743
- b.cls +
744
- '">' +
745
- b.label +
746
- "</span><div>" +
747
- escapeHtml(text) +
748
- "</div>" +
749
- anchor +
750
- "</div>"
751
- );
752
- })
753
- .join("");
754
- return (
755
- '<div class="narrative" id="next"><h3><span class="badge b-warn">待办</span> 后续优化动作与展望</h3>' +
756
- '<div class="todo-list">' +
757
- items +
758
- "</div></div>"
759
- );
760
- }
761
969
 
762
- /**
763
- * 通用维度表格:`opts.headers` / `opts.rowCells(r, currency, data)` 由调用方给出
764
- * 完整列定义与单元格 HTML,逐维对齐开发样例的列顺序(而不是共用一套固定指标列)。
765
- */
766
- function dimSection(opts) {
767
- const data = opts.data;
768
- const currency = currencyOf(data);
769
- const sec = ((data.narrative || {}).sections || {})[opts.narrativeKey] || {};
770
- const allRows = asArr(data.tables?.[opts.tableKey]);
771
- const limit = opts.limit || 20;
772
- const rows = topBySpend(allRows, limit);
773
- let body = "";
774
- if (rows.length) {
775
- const rowHtml = rows
776
- .map(function (r) {
777
- return "<tr>" + opts.rowCells(r, currency, data) + "</tr>";
778
- })
779
- .join("");
780
- body = tableHtml(opts.headers, rowHtml);
781
- } else {
782
- body = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
783
- }
784
- // 行数提示:明确区分「共 N 行仅显示 Top M」与「本维度确实无数据」,避免误判为报告漏了该维度。
785
- const countHint =
786
- allRows.length > rows.length
787
- ? "共 " + allRows.length + " 行,按消耗降序显示 Top " + rows.length + "。"
788
- : allRows.length > 0
789
- ? "共 " + allRows.length + " 行。"
790
- : "";
791
- const desc = [opts.desc, countHint].filter(Boolean).join(" ");
792
- return (
793
- '<section id="' +
794
- opts.id +
795
- '"><div class="wrap">' +
796
- sectionHead(opts.title, desc) +
797
- '<div class="panel">' +
798
- body +
799
- (opts.footnote
800
- ? '<div class="empty-note" style="text-align:left;padding:0 16px 14px;">' +
801
- escapeHtml(opts.footnote) +
802
- "</div>"
803
- : "") +
804
- analysisBlock(sec) +
805
- "</div>" +
806
- "</div></section>"
807
- );
808
- }
970
+ /**
971
+ * 通用维度表格:`opts.headers` / `opts.rowCells(r, currency, data)` 由调用方给出
972
+ * 完整列定义与单元格 HTML,逐维对齐开发样例的列顺序(而不是共用一套固定指标列)。
973
+ */
974
+ function dimSection(opts) {
975
+ const data = opts.data;
976
+ const currency = currencyOf(data);
977
+ const sec = ((data.narrative || {}).sections || {})[opts.narrativeKey] || {};
978
+ const allRows = asArr(data.tables?.[opts.tableKey]);
979
+ const limit = opts.limit || 20;
980
+ const rows = topBySpend(allRows, limit);
981
+ let body = "";
982
+ if (rows.length) {
983
+ const rowHtml = rows
984
+ .map(function (r) {
985
+ return "<tr>" + opts.rowCells(r, currency, data) + "</tr>";
986
+ })
987
+ .join("");
988
+ body = tableHtml(opts.headers, rowHtml);
989
+ } else {
990
+ body = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
991
+ }
992
+ // 行数提示:明确区分「共 N 行仅显示 Top M」与「本维度确实无数据」,避免误判为报告漏了该维度。
993
+ const countHint =
994
+ allRows.length > rows.length
995
+ ? "共 " + allRows.length + " 行,按消耗降序显示 Top " + rows.length + "。"
996
+ : allRows.length > 0
997
+ ? "共 " + allRows.length + " 行。"
998
+ : "";
999
+ const desc = [opts.desc, countHint].filter(Boolean).join(" ");
1000
+ return (
1001
+ '<section id="' +
1002
+ opts.id +
1003
+ '"><div class="wrap">' +
1004
+ sectionHead(opts.title, desc) +
1005
+ '<div class="panel">' +
1006
+ body +
1007
+ (opts.footnote
1008
+ ? '<div class="empty-note" style="text-align:left;padding:0 16px 14px;">' +
1009
+ escapeHtml(opts.footnote) +
1010
+ "</div>"
1011
+ : "") +
1012
+ analysisBlock(sec) +
1013
+ "</div>" +
1014
+ "</div></section>"
1015
+ );
1016
+ }
809
1017
 
810
- function renderDailyWeekly(data) {
811
- const currency = currencyOf(data);
812
- const sec = ((data.narrative || {}).sections || {}).daily || {};
813
- const daily = asArr(data.tables?.daily);
814
- const weekly = asArr(data.tables?.weekly);
1018
+ function renderDailyWeekly(data) {
1019
+ const currency = currencyOf(data);
1020
+ const sec = ((data.narrative || {}).sections || {}).daily || {};
1021
+ const daily = asArr(data.tables?.daily);
1022
+ const weekly = asArr(data.tables?.weekly);
815
1023
 
816
- let dailyBody = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
817
- if (daily.length) {
818
- dailyBody = tableHtml(
819
- [
820
- { label: "日期" },
821
- { label: "消耗", num: true },
822
- { label: "展示", num: true },
823
- { label: "点击", num: true },
824
- { label: "CTR", num: true },
825
- { label: "CPC", num: true },
826
- { label: "转化", num: true },
827
- { label: "CPA", num: true },
828
- ],
829
- daily
830
- .map(function (r) {
831
- return (
832
- "<tr><td>" +
833
- escapeHtml(r.date || "") +
834
- '</td><td class="num">' +
835
- money(r.spend, currency) +
836
- '</td><td class="num">' +
837
- int(r.impressions) +
838
- '</td><td class="num">' +
839
- int(r.clicks) +
840
- '</td><td class="num">' +
841
- pct(r.ctr) +
842
- '</td><td class="num">' +
843
- money(r.averageCpc, currency) +
844
- '</td><td class="num">' +
845
- num2(r.conversions) +
846
- '</td><td class="num">' +
847
- money(r.costPerConversion, currency) +
848
- "</td></tr>"
1024
+ let dailyBody = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
1025
+ if (daily.length) {
1026
+ dailyBody = tableHtml(
1027
+ [
1028
+ { label: "日期" },
1029
+ { label: "消耗", num: true },
1030
+ { label: "展示", num: true },
1031
+ { label: "点击", num: true },
1032
+ { label: "CTR", num: true },
1033
+ { label: "CPC", num: true },
1034
+ { label: "转化", num: true },
1035
+ { label: "CPA", num: true },
1036
+ ],
1037
+ daily
1038
+ .map(function (r) {
1039
+ return (
1040
+ "<tr><td>" +
1041
+ escapeHtml(r.date || "") +
1042
+ '</td><td class="num">' +
1043
+ money(r.spend, currency) +
1044
+ '</td><td class="num">' +
1045
+ int(r.impressions) +
1046
+ '</td><td class="num">' +
1047
+ int(r.clicks) +
1048
+ '</td><td class="num">' +
1049
+ pct(r.ctr) +
1050
+ '</td><td class="num">' +
1051
+ money(r.averageCpc, currency) +
1052
+ '</td><td class="num">' +
1053
+ num2(r.conversions) +
1054
+ '</td><td class="num">' +
1055
+ money(r.costPerConversion, currency) +
1056
+ "</td></tr>"
1057
+ );
1058
+ })
1059
+ .join(""),
849
1060
  );
850
- })
851
- .join("")
852
- );
853
- }
1061
+ }
854
1062
 
855
- let weeklyBody = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
856
- if (weekly.length) {
857
- weeklyBody = tableHtml(
858
- [
859
- { label: "周次" },
860
- { label: "区间" },
861
- { label: "天数", num: true },
862
- { label: "消耗", num: true },
863
- { label: "点击", num: true },
864
- { label: "CTR", num: true },
865
- { label: "CPC", num: true },
866
- { label: "转化", num: true },
867
- ],
868
- weekly
869
- .map(function (r) {
870
- return (
871
- "<tr><td>" +
872
- escapeHtml(weekLabelText(r)) +
873
- "</td><td>" +
874
- escapeHtml(weekRangeText(r)) +
875
- '</td><td class="num">' +
876
- int(r.days) +
877
- '</td><td class="num">' +
878
- money(r.spend, currency) +
879
- '</td><td class="num">' +
880
- int(r.clicks) +
881
- '</td><td class="num">' +
882
- pct(r.ctr) +
883
- '</td><td class="num">' +
884
- money(r.averageCpc, currency) +
885
- '</td><td class="num">' +
886
- num2(r.conversions) +
887
- "</td></tr>"
1063
+ let weeklyBody = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
1064
+ if (weekly.length) {
1065
+ weeklyBody = tableHtml(
1066
+ [
1067
+ { label: "周次" },
1068
+ { label: "区间" },
1069
+ { label: "天数", num: true },
1070
+ { label: "消耗", num: true },
1071
+ { label: "点击", num: true },
1072
+ { label: "CTR", num: true },
1073
+ { label: "CPC", num: true },
1074
+ { label: "转化", num: true },
1075
+ ],
1076
+ weekly
1077
+ .map(function (r) {
1078
+ return (
1079
+ "<tr><td>" +
1080
+ escapeHtml(weekLabelText(r)) +
1081
+ "</td><td>" +
1082
+ escapeHtml(weekRangeText(r)) +
1083
+ '</td><td class="num">' +
1084
+ int(r.days) +
1085
+ '</td><td class="num">' +
1086
+ money(r.spend, currency) +
1087
+ '</td><td class="num">' +
1088
+ int(r.clicks) +
1089
+ '</td><td class="num">' +
1090
+ pct(r.ctr) +
1091
+ '</td><td class="num">' +
1092
+ money(r.averageCpc, currency) +
1093
+ '</td><td class="num">' +
1094
+ num2(r.conversions) +
1095
+ "</td></tr>"
1096
+ );
1097
+ })
1098
+ .join(""),
888
1099
  );
889
- })
890
- .join("")
891
- );
892
- }
1100
+ }
893
1101
 
894
- return (
895
- '<section id="daily"><div class="wrap">' +
896
- sectionHead("日报表", "按日消耗/展示/点击/转化明细") +
897
- '<div class="panel">' +
898
- dailyBody +
899
- analysisBlock(sec) +
900
- "</div>" +
901
- "</div></section>" +
902
- '<section id="weekly"><div class="wrap">' +
903
- sectionHead("周报表", "由 CLI 从 daily 按 ISO 周聚合(禁止脚本重算)") +
904
- '<div class="panel">' +
905
- weeklyBody +
906
- "</div>" +
907
- "</div></section>"
908
- );
909
- }
910
-
911
- function renderAll(data) {
912
- const currency = currencyOf(data);
913
- const parts = [];
914
- parts.push(renderHero(data));
915
- parts.push(renderOverview(data));
916
- parts.push(
917
- dimSection({
918
- data: data,
919
- id: "search",
920
- narrativeKey: "search",
921
- tableKey: "search",
922
- title: "广告类型 · Search",
923
- desc: "搜索网络(Search)投放汇总",
924
- headers: [
925
- { label: "Network" },
926
- { label: "Spend", num: true },
927
- { label: "Impressions", num: true },
928
- { label: "Clicks", num: true },
929
- { label: "CPC", num: true },
930
- { label: "Conversions", num: true },
931
- { label: "CPA", num: true },
932
- { label: "ROI", num: true },
933
- { label: "CTR", num: true },
934
- { label: "样本" },
935
- ],
936
- rowCells: function (r, currency) {
937
1102
  return (
938
- "<td>" +
939
- escapeHtml(r.network || "Search") +
940
- '</td><td class="num">' +
941
- money(r.spend, currency) +
942
- '</td><td class="num">' +
943
- int(r.impressions) +
944
- '</td><td class="num">' +
945
- int(r.clicks) +
946
- '</td><td class="num">' +
947
- money(r.averageCpc, currency) +
948
- '</td><td class="num">' +
949
- num2(r.conversions) +
950
- '</td><td class="num">' +
951
- money(r.costPerConversion, currency) +
952
- '</td><td class="num">' +
953
- pct(r.roi) +
954
- '</td><td class="num">' +
955
- pct(r.ctr) +
956
- "</td><td>" +
957
- sampleBadge(r.clicks) +
958
- "</td>"
1103
+ '<section id="daily"><div class="wrap">' +
1104
+ sectionHead("日报表", "按日消耗/展示/点击/转化明细") +
1105
+ '<div class="panel">' +
1106
+ dailyBody +
1107
+ analysisBlock(sec) +
1108
+ "</div>" +
1109
+ "</div></section>" +
1110
+ '<section id="weekly"><div class="wrap">' +
1111
+ sectionHead("周报表", "由 CLI 从 daily 按 ISO 周聚合(禁止脚本重算)") +
1112
+ '<div class="panel">' +
1113
+ weeklyBody +
1114
+ "</div>" +
1115
+ "</div></section>"
959
1116
  );
960
- },
961
- })
962
- );
963
- parts.push(
964
- dimSection({
965
- data: data,
966
- id: "campaign",
967
- narrativeKey: "campaigns",
968
- tableKey: "campaigns",
969
- title: "Campaign",
970
- desc: "按消耗排序的系列明细",
971
- headers: [
972
- { label: "Campaign" },
973
- { label: "预算", num: true },
974
- { label: "战略" },
975
- { label: "目标CPA(API)", num: true },
976
- { label: "Spend", num: true },
977
- { label: "Impr", num: true },
978
- { label: "Clicks", num: true },
979
- { label: "CPC", num: true },
980
- { label: "Conv", num: true },
981
- { label: "CPA", num: true },
982
- { label: "状态" },
983
- ],
984
- rowCells: function (r, currency) {
985
- return (
986
- "<td>" +
987
- escapeHtml(r.campaignName || r.campaignId || "—") +
988
- '</td><td class="num">' +
989
- (r.budget != null ? money(r.budget, currency) : "—") +
990
- "</td><td>" +
991
- escapeHtml(r.strategy || "—") +
992
- '</td><td class="num">' +
993
- (r.targetCpa != null ? money(r.targetCpa, currency) : "—") +
994
- '</td><td class="num">' +
995
- money(r.spend, currency) +
996
- '</td><td class="num">' +
997
- int(r.impressions) +
998
- '</td><td class="num">' +
999
- int(r.clicks) +
1000
- '</td><td class="num">' +
1001
- money(r.averageCpc, currency) +
1002
- '</td><td class="num">' +
1003
- num2(r.conversions) +
1004
- '</td><td class="num">' +
1005
- money(r.costPerConversion, currency) +
1006
- "</td><td>" +
1007
- campaignStatusBadge(r) +
1008
- "</td>"
1117
+ }
1118
+
1119
+ function renderAll(data) {
1120
+ const currency = currencyOf(data);
1121
+ const parts = [];
1122
+ parts.push(renderHero(data));
1123
+ parts.push(renderOverview(data));
1124
+ parts.push(
1125
+ dimSection({
1126
+ data: data,
1127
+ id: "search",
1128
+ narrativeKey: "search",
1129
+ tableKey: "search",
1130
+ title: "广告类型 · Search",
1131
+ desc: "搜索网络(Search)投放汇总",
1132
+ headers: [
1133
+ { label: "Network" },
1134
+ { label: "Spend", num: true },
1135
+ { label: "Impressions", num: true },
1136
+ { label: "Clicks", num: true },
1137
+ { label: "CPC", num: true },
1138
+ { label: "Conversions", num: true },
1139
+ { label: "CPA", num: true },
1140
+ { label: "ROI", num: true },
1141
+ { label: "CTR", num: true },
1142
+ { label: "样本" },
1143
+ ],
1144
+ rowCells: function (r, currency) {
1145
+ return (
1146
+ "<td>" +
1147
+ escapeHtml(r.network || "Search") +
1148
+ '</td><td class="num">' +
1149
+ money(r.spend, currency) +
1150
+ '</td><td class="num">' +
1151
+ int(r.impressions) +
1152
+ '</td><td class="num">' +
1153
+ int(r.clicks) +
1154
+ '</td><td class="num">' +
1155
+ money(r.averageCpc, currency) +
1156
+ '</td><td class="num">' +
1157
+ num2(r.conversions) +
1158
+ '</td><td class="num">' +
1159
+ money(r.costPerConversion, currency) +
1160
+ '</td><td class="num">' +
1161
+ pct(r.roi) +
1162
+ '</td><td class="num">' +
1163
+ pct(r.ctr) +
1164
+ "</td><td>" +
1165
+ sampleBadge(r.clicks) +
1166
+ "</td>"
1167
+ );
1168
+ },
1169
+ }),
1009
1170
  );
1010
- },
1011
- limit: 30,
1012
- })
1013
- );
1014
- parts.push(
1015
- dimSection({
1016
- data: data,
1017
- id: "keyword",
1018
- narrativeKey: "keywords",
1019
- tableKey: "keywords",
1020
- title: "关键词",
1021
- desc: "按消耗排序的关键词明细",
1022
- footnote:
1023
- "「中文翻译」为 Agent 撰写报告时提供(narrative.rowInsights.keywords),未提供的行显示「—」,不做机械占位。",
1024
- headers: [
1025
- { label: "Campaign" },
1026
- { label: "Group" },
1027
- { label: "Keyword" },
1028
- { label: "中文翻译" },
1029
- { label: "Impr", num: true },
1030
- { label: "Clicks", num: true },
1031
- { label: "Spend", num: true },
1032
- { label: "Conv", num: true },
1033
- { label: "CR%", num: true },
1034
- { label: "CPA", num: true },
1035
- { label: "CTR", num: true },
1036
- { label: "CPC", num: true },
1037
- ],
1038
- rowCells: function (r, currency, data) {
1039
- const insight = findInsight(rowInsightsOf(data, "keywords"), "keyword", r.keyword, r);
1040
- return (
1041
- "<td>" +
1042
- escapeHtml(r.campaignName || "—") +
1043
- "</td><td>" +
1044
- escapeHtml(r.adGroupId != null ? String(r.adGroupId) : "—") +
1045
- "</td><td>" +
1046
- escapeHtml(r.keyword || "—") +
1047
- "</td><td>" +
1048
- escapeHtml((insight && insight.translation) || "—") +
1049
- '</td><td class="num">' +
1050
- int(r.impressions) +
1051
- '</td><td class="num">' +
1052
- int(r.clicks) +
1053
- '</td><td class="num">' +
1054
- money(r.spend, currency) +
1055
- '</td><td class="num">' +
1056
- num2(r.conversions) +
1057
- '</td><td class="num">' +
1058
- pct(derivedCr(r)) +
1059
- '</td><td class="num">' +
1060
- money(derivedCpa(r), currency) +
1061
- '</td><td class="num">' +
1062
- pct(derivedCtr(r)) +
1063
- '</td><td class="num">' +
1064
- money(derivedCpc(r), currency) +
1065
- "</td>"
1171
+ parts.push(
1172
+ dimSection({
1173
+ data: data,
1174
+ id: "campaign",
1175
+ narrativeKey: "campaigns",
1176
+ tableKey: "campaigns",
1177
+ title: "Campaign",
1178
+ desc: "按消耗排序的系列明细",
1179
+ headers: [
1180
+ { label: "Campaign" },
1181
+ { label: "预算", num: true },
1182
+ { label: "战略" },
1183
+ { label: "目标CPA(API)", num: true },
1184
+ { label: "Spend", num: true },
1185
+ { label: "Impr", num: true },
1186
+ { label: "Clicks", num: true },
1187
+ { label: "CPC", num: true },
1188
+ { label: "Conv", num: true },
1189
+ { label: "CPA", num: true },
1190
+ { label: "状态" },
1191
+ ],
1192
+ rowCells: function (r, currency) {
1193
+ return (
1194
+ "<td>" +
1195
+ escapeHtml(r.campaignName || r.campaignId || "—") +
1196
+ '</td><td class="num">' +
1197
+ (r.budget != null ? money(r.budget, currency) : "—") +
1198
+ "</td><td>" +
1199
+ escapeHtml(r.strategy || "—") +
1200
+ '</td><td class="num">' +
1201
+ (r.targetCpa != null ? money(r.targetCpa, currency) : "—") +
1202
+ '</td><td class="num">' +
1203
+ money(r.spend, currency) +
1204
+ '</td><td class="num">' +
1205
+ int(r.impressions) +
1206
+ '</td><td class="num">' +
1207
+ int(r.clicks) +
1208
+ '</td><td class="num">' +
1209
+ money(r.averageCpc, currency) +
1210
+ '</td><td class="num">' +
1211
+ num2(r.conversions) +
1212
+ '</td><td class="num">' +
1213
+ money(r.costPerConversion, currency) +
1214
+ "</td><td>" +
1215
+ campaignStatusBadge(r) +
1216
+ "</td>"
1217
+ );
1218
+ },
1219
+ limit: 30,
1220
+ }),
1066
1221
  );
1067
- },
1068
- limit: 40,
1069
- })
1070
- );
1071
- parts.push(
1072
- dimSection({
1073
- data: data,
1074
- id: "query",
1075
- narrativeKey: "searchTerms",
1076
- tableKey: "searchTerms",
1077
- title: "搜索词",
1078
- desc: "按消耗排序的搜索词明细(仅近 180 天数据)",
1079
- headers: [
1080
- { label: "Search Query" },
1081
- { label: "匹配关键词" },
1082
- { label: "Impr", num: true },
1083
- { label: "Clicks", num: true },
1084
- { label: "CTR", num: true },
1085
- { label: "CPC", num: true },
1086
- { label: "Spend", num: true },
1087
- { label: "Conv", num: true },
1088
- { label: "cr%", num: true },
1089
- { label: "CPA", num: true },
1090
- ],
1091
- rowCells: function (r, currency) {
1092
- return (
1093
- "<td>" +
1094
- escapeHtml(r.searchQuery || "—") +
1095
- "</td><td>" +
1096
- escapeHtml(r.matchedKeyword || "—") +
1097
- '</td><td class="num">' +
1098
- int(r.impressions) +
1099
- '</td><td class="num">' +
1100
- int(r.clicks) +
1101
- '</td><td class="num">' +
1102
- pct(derivedCtr(r)) +
1103
- '</td><td class="num">' +
1104
- money(derivedCpc(r), currency) +
1105
- '</td><td class="num">' +
1106
- money(r.spend, currency) +
1107
- '</td><td class="num">' +
1108
- num2(r.conversions) +
1109
- '</td><td class="num">' +
1110
- pct(derivedCr(r)) +
1111
- '</td><td class="num">' +
1112
- money(derivedCpa(r), currency) +
1113
- "</td>"
1222
+ parts.push(
1223
+ dimSection({
1224
+ data: data,
1225
+ id: "keyword",
1226
+ narrativeKey: "keywords",
1227
+ tableKey: "keywords",
1228
+ title: "关键词",
1229
+ desc: "按消耗排序的关键词明细",
1230
+ footnote:
1231
+ "「中文翻译」为 Agent 撰写报告时提供(narrative.rowInsights.keywords),未提供的行显示「—」,不做机械占位。",
1232
+ headers: [
1233
+ { label: "Campaign" },
1234
+ { label: "Group" },
1235
+ { label: "Keyword" },
1236
+ { label: "中文翻译" },
1237
+ { label: "Impr", num: true },
1238
+ { label: "Clicks", num: true },
1239
+ { label: "Spend", num: true },
1240
+ { label: "Conv", num: true },
1241
+ { label: "CR%", num: true },
1242
+ { label: "CPA", num: true },
1243
+ { label: "CTR", num: true },
1244
+ { label: "CPC", num: true },
1245
+ ],
1246
+ rowCells: function (r, currency, data) {
1247
+ const insight = findInsight(
1248
+ rowInsightsOf(data, "keywords"),
1249
+ "keyword",
1250
+ r.keyword,
1251
+ r,
1252
+ );
1253
+ return (
1254
+ "<td>" +
1255
+ escapeHtml(r.campaignName || "—") +
1256
+ "</td><td>" +
1257
+ escapeHtml(r.adGroupId != null ? String(r.adGroupId) : "—") +
1258
+ "</td><td>" +
1259
+ escapeHtml(r.keyword || "—") +
1260
+ "</td><td>" +
1261
+ escapeHtml((insight && insight.translation) || "—") +
1262
+ '</td><td class="num">' +
1263
+ int(r.impressions) +
1264
+ '</td><td class="num">' +
1265
+ int(r.clicks) +
1266
+ '</td><td class="num">' +
1267
+ money(r.spend, currency) +
1268
+ '</td><td class="num">' +
1269
+ num2(r.conversions) +
1270
+ '</td><td class="num">' +
1271
+ pct(derivedCr(r)) +
1272
+ '</td><td class="num">' +
1273
+ money(derivedCpa(r), currency) +
1274
+ '</td><td class="num">' +
1275
+ pct(derivedCtr(r)) +
1276
+ '</td><td class="num">' +
1277
+ money(derivedCpc(r), currency) +
1278
+ "</td>"
1279
+ );
1280
+ },
1281
+ limit: 40,
1282
+ }),
1114
1283
  );
1115
- },
1116
- limit: 40,
1117
- })
1118
- );
1119
- parts.push(
1120
- dimSection({
1121
- data: data,
1122
- id: "geo",
1123
- narrativeKey: "geo",
1124
- tableKey: "geo",
1125
- title: "地域",
1126
- desc: "按消耗排序的地域明细",
1127
- footnote: "「国家」取自账户投放地区(meta.region);「City」为 Yandex 返回的地域名称。",
1128
- headers: [
1129
- { label: "国家" },
1130
- { label: "City" },
1131
- { label: "Spend", num: true },
1132
- { label: "Clicks", num: true },
1133
- { label: "Impr", num: true },
1134
- { label: "CTR", num: true },
1135
- { label: "CPC", num: true },
1136
- { label: "Conv", num: true },
1137
- { label: "cr%", num: true },
1138
- { label: "CPA", num: true },
1139
- ],
1140
- rowCells: function (r, currency, data) {
1141
- const cityName =
1142
- r.locationName ||
1143
- r.targetingLocationName ||
1144
- r.location ||
1145
- r.name ||
1146
- (r.locationId != null ? "地域 " + r.locationId : "—");
1147
- return (
1148
- "<td>" +
1149
- escapeHtml((data.meta && data.meta.region) || "—") +
1150
- "</td><td>" +
1151
- escapeHtml(cityName) +
1152
- '</td><td class="num">' +
1153
- money(r.spend, currency) +
1154
- '</td><td class="num">' +
1155
- int(r.clicks) +
1156
- '</td><td class="num">' +
1157
- int(r.impressions) +
1158
- '</td><td class="num">' +
1159
- pct(derivedCtr(r)) +
1160
- '</td><td class="num">' +
1161
- money(derivedCpc(r), currency) +
1162
- '</td><td class="num">' +
1163
- num2(r.conversions) +
1164
- '</td><td class="num">' +
1165
- pct(derivedCr(r)) +
1166
- '</td><td class="num">' +
1167
- money(derivedCpa(r), currency) +
1168
- "</td>"
1284
+ parts.push(
1285
+ dimSection({
1286
+ data: data,
1287
+ id: "query",
1288
+ narrativeKey: "searchTerms",
1289
+ tableKey: "searchTerms",
1290
+ title: "搜索词",
1291
+ desc: "按消耗排序的搜索词明细(仅近 180 天数据)",
1292
+ headers: [
1293
+ { label: "Search Query" },
1294
+ { label: "匹配关键词" },
1295
+ { label: "Impr", num: true },
1296
+ { label: "Clicks", num: true },
1297
+ { label: "CTR", num: true },
1298
+ { label: "CPC", num: true },
1299
+ { label: "Spend", num: true },
1300
+ { label: "Conv", num: true },
1301
+ { label: "cr%", num: true },
1302
+ { label: "CPA", num: true },
1303
+ ],
1304
+ rowCells: function (r, currency) {
1305
+ return (
1306
+ "<td>" +
1307
+ escapeHtml(r.searchQuery || "—") +
1308
+ "</td><td>" +
1309
+ escapeHtml(r.matchedKeyword || "—") +
1310
+ '</td><td class="num">' +
1311
+ int(r.impressions) +
1312
+ '</td><td class="num">' +
1313
+ int(r.clicks) +
1314
+ '</td><td class="num">' +
1315
+ pct(derivedCtr(r)) +
1316
+ '</td><td class="num">' +
1317
+ money(derivedCpc(r), currency) +
1318
+ '</td><td class="num">' +
1319
+ money(r.spend, currency) +
1320
+ '</td><td class="num">' +
1321
+ num2(r.conversions) +
1322
+ '</td><td class="num">' +
1323
+ pct(derivedCr(r)) +
1324
+ '</td><td class="num">' +
1325
+ money(derivedCpa(r), currency) +
1326
+ "</td>"
1327
+ );
1328
+ },
1329
+ limit: 40,
1330
+ }),
1169
1331
  );
1170
- },
1171
- limit: 30,
1172
- })
1173
- );
1174
- parts.push(
1175
- dimSection({
1176
- data: data,
1177
- id: "device",
1178
- narrativeKey: "devices",
1179
- tableKey: "devices",
1180
- title: "设备",
1181
- desc: "按消耗排序的设备明细",
1182
- headers: [
1183
- { label: "Device" },
1184
- { label: "Spend", num: true },
1185
- { label: "impr", num: true },
1186
- { label: "Clicks", num: true },
1187
- { label: "ctr", num: true },
1188
- { label: "CPC", num: true },
1189
- { label: "Conv", num: true },
1190
- { label: "cr%", num: true },
1191
- { label: "CPA", num: true },
1192
- ],
1193
- rowCells: function (r, currency) {
1194
- return (
1195
- "<td>" +
1196
- escapeHtml(r.device || "—") +
1197
- '</td><td class="num">' +
1198
- money(r.spend, currency) +
1199
- '</td><td class="num">' +
1200
- int(r.impressions) +
1201
- '</td><td class="num">' +
1202
- int(r.clicks) +
1203
- '</td><td class="num">' +
1204
- pct(derivedCtr(r)) +
1205
- '</td><td class="num">' +
1206
- money(derivedCpc(r), currency) +
1207
- '</td><td class="num">' +
1208
- num2(r.conversions) +
1209
- '</td><td class="num">' +
1210
- pct(derivedCr(r)) +
1211
- '</td><td class="num">' +
1212
- money(derivedCpa(r), currency) +
1213
- "</td>"
1332
+ parts.push(
1333
+ dimSection({
1334
+ data: data,
1335
+ id: "geo",
1336
+ narrativeKey: "geo",
1337
+ tableKey: "geo",
1338
+ title: "地域",
1339
+ desc: "按消耗排序的地域明细",
1340
+ footnote:
1341
+ "「国家」取自账户投放地区(meta.region);「City」为 Yandex 返回的地域名称。",
1342
+ headers: [
1343
+ { label: "国家" },
1344
+ { label: "City" },
1345
+ { label: "Spend", num: true },
1346
+ { label: "Clicks", num: true },
1347
+ { label: "Impr", num: true },
1348
+ { label: "CTR", num: true },
1349
+ { label: "CPC", num: true },
1350
+ { label: "Conv", num: true },
1351
+ { label: "cr%", num: true },
1352
+ { label: "CPA", num: true },
1353
+ ],
1354
+ rowCells: function (r, currency, data) {
1355
+ const cityName =
1356
+ r.locationName ||
1357
+ r.targetingLocationName ||
1358
+ r.location ||
1359
+ r.name ||
1360
+ (r.locationId != null ? "地域 " + r.locationId : "—");
1361
+ return (
1362
+ "<td>" +
1363
+ escapeHtml((data.meta && data.meta.region) || "—") +
1364
+ "</td><td>" +
1365
+ escapeHtml(cityName) +
1366
+ '</td><td class="num">' +
1367
+ money(r.spend, currency) +
1368
+ '</td><td class="num">' +
1369
+ int(r.clicks) +
1370
+ '</td><td class="num">' +
1371
+ int(r.impressions) +
1372
+ '</td><td class="num">' +
1373
+ pct(derivedCtr(r)) +
1374
+ '</td><td class="num">' +
1375
+ money(derivedCpc(r), currency) +
1376
+ '</td><td class="num">' +
1377
+ num2(r.conversions) +
1378
+ '</td><td class="num">' +
1379
+ pct(derivedCr(r)) +
1380
+ '</td><td class="num">' +
1381
+ money(derivedCpa(r), currency) +
1382
+ "</td>"
1383
+ );
1384
+ },
1385
+ limit: 30,
1386
+ }),
1214
1387
  );
1215
- },
1216
- limit: 20,
1217
- })
1218
- );
1219
- parts.push(renderDailyWeekly(data));
1220
- void currency;
1221
- return parts.join("\n");
1222
- }
1223
-
1224
- function bindNav() {
1225
- const links = document.querySelectorAll(".nav a");
1226
- const sections = [...document.querySelectorAll("main section[id]")];
1227
- function onScroll() {
1228
- let current = sections[0]?.id;
1229
- for (const s of sections) {
1230
- if (s.getBoundingClientRect().top <= 100) current = s.id;
1231
- }
1232
- links.forEach(function (a) {
1233
- a.classList.toggle("active", a.getAttribute("href") === "#" + current);
1234
- });
1235
- }
1236
- window.addEventListener("scroll", onScroll, { passive: true });
1237
- onScroll();
1238
- }
1388
+ parts.push(
1389
+ dimSection({
1390
+ data: data,
1391
+ id: "device",
1392
+ narrativeKey: "devices",
1393
+ tableKey: "devices",
1394
+ title: "设备",
1395
+ desc: "按消耗排序的设备明细",
1396
+ headers: [
1397
+ { label: "Device" },
1398
+ { label: "Spend", num: true },
1399
+ { label: "impr", num: true },
1400
+ { label: "Clicks", num: true },
1401
+ { label: "ctr", num: true },
1402
+ { label: "CPC", num: true },
1403
+ { label: "Conv", num: true },
1404
+ { label: "cr%", num: true },
1405
+ { label: "CPA", num: true },
1406
+ ],
1407
+ rowCells: function (r, currency) {
1408
+ return (
1409
+ "<td>" +
1410
+ escapeHtml(r.device || "—") +
1411
+ '</td><td class="num">' +
1412
+ money(r.spend, currency) +
1413
+ '</td><td class="num">' +
1414
+ int(r.impressions) +
1415
+ '</td><td class="num">' +
1416
+ int(r.clicks) +
1417
+ '</td><td class="num">' +
1418
+ pct(derivedCtr(r)) +
1419
+ '</td><td class="num">' +
1420
+ money(derivedCpc(r), currency) +
1421
+ '</td><td class="num">' +
1422
+ num2(r.conversions) +
1423
+ '</td><td class="num">' +
1424
+ pct(derivedCr(r)) +
1425
+ '</td><td class="num">' +
1426
+ money(derivedCpa(r), currency) +
1427
+ "</td>"
1428
+ );
1429
+ },
1430
+ limit: 20,
1431
+ }),
1432
+ );
1433
+ parts.push(renderDailyWeekly(data));
1434
+ void currency;
1435
+ return parts.join("\n");
1436
+ }
1239
1437
 
1240
- document.addEventListener("DOMContentLoaded", function () {
1241
- const root = document.getElementById("report-root");
1242
- const data = window.__YANDEX_PERIOD_REPORT__;
1243
- if (!data) {
1244
- root.innerHTML =
1245
- '<div class="wrap"><div class="panel empty-note">未找到报告数据(window.__YANDEX_PERIOD_REPORT__ 为空),请通过 `yandex-analysis render` 生成本文件。</div></div>';
1246
- return;
1247
- }
1248
- root.innerHTML = renderAll(data);
1249
- bindNav();
1250
- });
1251
- })();
1438
+ function bindNav() {
1439
+ const links = document.querySelectorAll(".nav a");
1440
+ const sections = [...document.querySelectorAll("main section[id]")];
1441
+ function onScroll() {
1442
+ let current = sections[0]?.id;
1443
+ for (const s of sections) {
1444
+ if (s.getBoundingClientRect().top <= 100) current = s.id;
1445
+ }
1446
+ links.forEach(function (a) {
1447
+ a.classList.toggle("active", a.getAttribute("href") === "#" + current);
1448
+ });
1449
+ }
1450
+ window.addEventListener("scroll", onScroll, { passive: true });
1451
+ onScroll();
1452
+ }
1252
1453
 
1454
+ document.addEventListener("DOMContentLoaded", function () {
1455
+ const root = document.getElementById("report-root");
1456
+ const data = window.__YANDEX_PERIOD_REPORT__;
1457
+ if (!data) {
1458
+ root.innerHTML =
1459
+ '<div class="wrap"><div class="panel empty-note">未找到报告数据(window.__YANDEX_PERIOD_REPORT__ 为空),请通过 `yandex-analysis render` 生成本文件。</div></div>';
1460
+ return;
1461
+ }
1462
+ root.innerHTML = renderAll(data);
1463
+ bindNav();
1464
+ });
1465
+ })();
1253
1466
  </script>
1254
1467
  </body>
1255
1468
  </html>