siluzan-tso-cli 1.1.52 → 1.1.53-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.
- package/README.md +2 -1
- package/dist/index.js +2378 -1046
- package/dist/skill/SKILL.md +6 -5
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/google-quarterly-report.schema.json +46 -0
- package/dist/skill/references/README.md +2 -0
- package/dist/skill/references/accounts/accounts-balance-stats.md +9 -5
- package/dist/skill/references/accounts/accounts-list.md +1 -1
- package/dist/skill/references/accounts/accounts-permissions.md +2 -2
- package/dist/skill/references/accounts/finance.md +1 -1
- package/dist/skill/references/accounts/open-account-by-media.md +1 -1
- package/dist/skill/references/accounts/open-account-google-ui.md +1 -1
- package/dist/skill/references/analytics/account-analytics.md +1 -0
- package/dist/skill/references/analytics/rag.md +1 -1
- package/dist/skill/references/analytics/reporting.md +5 -5
- package/dist/skill/references/core/agent-conventions.md +1 -1
- package/dist/skill/references/core/cli-enums.md +9 -0
- package/dist/skill/references/core/intent-routing.md +14 -7
- package/dist/skill/references/core/playbooks.md +28 -6
- package/dist/skill/references/core/setup.md +5 -5
- package/dist/skill/references/core/tips.md +9 -5
- package/dist/skill/references/misc/tso-home.md +2 -2
- package/dist/skill/references/report-templates/README.md +1 -0
- package/dist/skill/references/report-templates/google-quarterly-report.md +100 -0
- package/dist/skill/report-templates/README.md +1 -0
- package/dist/skill/report-templates/google-quarterly-report.html +585 -0
- package/dist/skill/report-templates/google-quarterly-report.md +100 -0
- package/dist/skill/scripts/install.ps1 +3 -3
- package/dist/skill/scripts/install.sh +3 -3
- package/eval/cases/ir-p10-dazu-client-not-p4.scenario.json +27 -0
- package/eval/cases/ir-p10-monthly-plan-not-p4.scenario.json +27 -0
- package/eval/cases/ir-p10-quarterly-client-report.scenario.json +27 -0
- package/eval/cases/ir-p10-quarterly-not-p4.scenario.json +27 -0
- package/eval/cases/ir-p4-period-not-diagnosis.scenario.json +4 -1
- package/eval/cases/p10-quarterly-report.scenario.json +28 -0
- package/eval/cases/p3-cpa-scan-max-cpa.scenario.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Google 季度经营报告</title>
|
|
7
|
+
<script src="https://staticpn.siluzan.com/assets/slz/homeCDN/echarts.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--brand: #3370FF;
|
|
11
|
+
--brand-hover: #4E83FD;
|
|
12
|
+
--brand-soft: #E8F0FF;
|
|
13
|
+
--success: #2EA121;
|
|
14
|
+
--success-soft: #E6F7E6;
|
|
15
|
+
--warning: #FF8800;
|
|
16
|
+
--warning-soft: #FFF5E8;
|
|
17
|
+
--danger: #F54A45;
|
|
18
|
+
--danger-soft: #FEECEC;
|
|
19
|
+
--info: #14C0C0;
|
|
20
|
+
--purple: #7B61FF;
|
|
21
|
+
--text: #1F2329;
|
|
22
|
+
--text-2: #646A73;
|
|
23
|
+
--text-3: #8F959E;
|
|
24
|
+
--bg: #F5F6F7;
|
|
25
|
+
--card: #FFFFFF;
|
|
26
|
+
--line: #DEE0E3;
|
|
27
|
+
--line-light: #EFF0F1;
|
|
28
|
+
--shadow: 0 1px 2px rgba(31, 35, 41, 0.06);
|
|
29
|
+
--shadow-md: 0 6px 20px rgba(31, 35, 41, 0.06);
|
|
30
|
+
--radius: 8px;
|
|
31
|
+
--radius-lg: 12px;
|
|
32
|
+
--max: 1160px;
|
|
33
|
+
--nav-h: 56px;
|
|
34
|
+
--font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
35
|
+
}
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
html { scroll-behavior: smooth; }
|
|
38
|
+
body {
|
|
39
|
+
margin: 0;
|
|
40
|
+
font-family: var(--font);
|
|
41
|
+
color: var(--text);
|
|
42
|
+
background: var(--bg);
|
|
43
|
+
line-height: 1.6;
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
-webkit-font-smoothing: antialiased;
|
|
46
|
+
}
|
|
47
|
+
a { color: var(--brand); text-decoration: none; }
|
|
48
|
+
a:hover { color: var(--brand-hover); }
|
|
49
|
+
.topnav {
|
|
50
|
+
position: sticky; top: 0; z-index: 100;
|
|
51
|
+
height: var(--nav-h); background: #fff;
|
|
52
|
+
border-bottom: 1px solid var(--line-light);
|
|
53
|
+
}
|
|
54
|
+
.topnav-inner {
|
|
55
|
+
max-width: var(--max); margin: 0 auto; height: 100%;
|
|
56
|
+
padding: 0 24px; display: flex; align-items: center; gap: 6px;
|
|
57
|
+
overflow-x: auto;
|
|
58
|
+
}
|
|
59
|
+
.brand-mark {
|
|
60
|
+
display: flex; align-items: center; gap: 8px;
|
|
61
|
+
font-weight: 600; font-size: 14px; color: var(--text);
|
|
62
|
+
margin-right: 8px; padding-right: 14px; border-right: 1px solid var(--line);
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
}
|
|
65
|
+
.brand-mark i {
|
|
66
|
+
width: 22px; height: 22px; border-radius: 6px; display: block;
|
|
67
|
+
background: linear-gradient(135deg, #3370FF, #14C0C0);
|
|
68
|
+
}
|
|
69
|
+
.brand-mark span { color: var(--brand); }
|
|
70
|
+
.topnav a {
|
|
71
|
+
font-size: 13px; color: var(--text-2); padding: 6px 10px; border-radius: 6px; white-space: nowrap;
|
|
72
|
+
}
|
|
73
|
+
.topnav a:hover { background: var(--brand-soft); color: var(--brand); }
|
|
74
|
+
.wrap { max-width: var(--max); margin: 0 auto; padding: 20px 24px 72px; }
|
|
75
|
+
.cover {
|
|
76
|
+
background: #fff; border: 1px solid var(--line-light); border-radius: var(--radius-lg);
|
|
77
|
+
box-shadow: var(--shadow-md); padding: 56px 48px; margin-bottom: 20px;
|
|
78
|
+
position: relative; overflow: hidden;
|
|
79
|
+
}
|
|
80
|
+
.cover::before {
|
|
81
|
+
content: ""; position: absolute; inset: 0;
|
|
82
|
+
background:
|
|
83
|
+
radial-gradient(ellipse 55% 80% at 100% 0%, rgba(51,112,255,.10), transparent 55%),
|
|
84
|
+
radial-gradient(ellipse 40% 50% at 0% 100%, rgba(20,192,192,.07), transparent 50%);
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
}
|
|
87
|
+
.cover > * { position: relative; }
|
|
88
|
+
.cover-tag {
|
|
89
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
90
|
+
background: var(--brand-soft); color: var(--brand);
|
|
91
|
+
font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 4px; margin-bottom: 18px;
|
|
92
|
+
}
|
|
93
|
+
.cover h1 { margin: 0 0 10px; font-size: 40px; font-weight: 600; letter-spacing: -.02em; }
|
|
94
|
+
.cover .subtitle { margin: 0 0 20px; font-size: 20px; color: var(--text-2); font-weight: 400; }
|
|
95
|
+
.cover-meta { display: flex; flex-wrap: wrap; gap: 8px 24px; color: var(--text-3); font-size: 13px; }
|
|
96
|
+
.cover-meta strong { color: var(--text); font-weight: 600; }
|
|
97
|
+
.story { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 20px; }
|
|
98
|
+
@media (max-width: 900px) { .story { grid-template-columns: 1fr 1fr; } }
|
|
99
|
+
.story-item {
|
|
100
|
+
background: #fff; border: 1px solid var(--line-light); border-radius: var(--radius);
|
|
101
|
+
padding: 14px 16px; box-shadow: var(--shadow);
|
|
102
|
+
}
|
|
103
|
+
.story-item .n { font-size: 12px; font-weight: 600; color: var(--brand); margin-bottom: 4px; }
|
|
104
|
+
.story-item .t { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
|
|
105
|
+
.story-item .d { font-size: 12px; color: var(--text-3); margin: 0; }
|
|
106
|
+
.page {
|
|
107
|
+
background: #fff; border: 1px solid var(--line-light); border-radius: var(--radius-lg);
|
|
108
|
+
padding: 28px 28px 24px; margin-bottom: 16px; box-shadow: var(--shadow);
|
|
109
|
+
}
|
|
110
|
+
.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 8px; }
|
|
111
|
+
.page-head h2 {
|
|
112
|
+
margin: 0; font-size: 18px; font-weight: 600;
|
|
113
|
+
display: flex; align-items: center; gap: 8px;
|
|
114
|
+
}
|
|
115
|
+
.page-head h2::before { content: ""; width: 3px; height: 16px; border-radius: 2px; background: var(--brand); }
|
|
116
|
+
.lead {
|
|
117
|
+
margin: 0 0 20px; color: var(--text-2); font-size: 13px;
|
|
118
|
+
padding: 10px 12px; background: var(--bg); border-radius: 6px;
|
|
119
|
+
border-left: 3px solid var(--brand);
|
|
120
|
+
}
|
|
121
|
+
.chip {
|
|
122
|
+
font-size: 12px; color: var(--brand); background: var(--brand-soft);
|
|
123
|
+
padding: 2px 8px; border-radius: 4px; font-weight: 500; white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
.chapter {
|
|
126
|
+
background: #fff; border: 1px solid var(--line-light); border-radius: var(--radius-lg);
|
|
127
|
+
padding: 28px 28px 28px 32px; margin: 24px 0 16px; box-shadow: var(--shadow);
|
|
128
|
+
border-left: 4px solid var(--brand);
|
|
129
|
+
}
|
|
130
|
+
.chapter .num { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--brand); letter-spacing: .04em; }
|
|
131
|
+
.chapter h2 { margin: 0 0 6px; font-size: 24px; font-weight: 600; }
|
|
132
|
+
.chapter p { margin: 0; color: var(--text-3); font-size: 13px; }
|
|
133
|
+
h3.st { margin: 22px 0 12px; font-size: 14px; font-weight: 600; color: var(--text); }
|
|
134
|
+
h3.st:first-child { margin-top: 0; }
|
|
135
|
+
.kpi-row { display: grid; gap: 10px; margin-bottom: 16px; }
|
|
136
|
+
.kpi-row.c7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
|
137
|
+
@media (max-width: 1000px) { .kpi-row.c7 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
|
138
|
+
@media (max-width: 640px) { .kpi-row.c7 { grid-template-columns: 1fr 1fr; } }
|
|
139
|
+
.kpi {
|
|
140
|
+
background: #fff; border: 1px solid var(--line-light); border-radius: var(--radius);
|
|
141
|
+
padding: 12px 14px; min-width: 0;
|
|
142
|
+
}
|
|
143
|
+
.kpi .label { font-size: 12px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
144
|
+
.kpi .val { font-size: 20px; font-weight: 600; color: var(--text); margin: 6px 0 4px; letter-spacing: -.02em; word-break: break-all; }
|
|
145
|
+
.kpi .delta { font-size: 12px; font-weight: 500; }
|
|
146
|
+
.up { color: var(--success); }
|
|
147
|
+
.down { color: var(--danger); }
|
|
148
|
+
.flat { color: var(--text-3); }
|
|
149
|
+
.table-wrap {
|
|
150
|
+
width: 100%; overflow-x: auto; border: 1px solid var(--line-light);
|
|
151
|
+
border-radius: var(--radius); background: #fff; margin: 0 0 12px;
|
|
152
|
+
}
|
|
153
|
+
table.data { width: 100%; min-width: 720px; border-collapse: collapse; font-size: 13px; }
|
|
154
|
+
table.data th, table.data td { padding: 10px 12px; border-bottom: 1px solid var(--line-light); vertical-align: middle; }
|
|
155
|
+
table.data thead th { background: #F8F9FA; color: var(--text-2); font-weight: 600; font-size: 12px; white-space: nowrap; }
|
|
156
|
+
table.data tbody td { color: var(--text); background: #fff; }
|
|
157
|
+
table.data tbody tr:hover td { background: #F8F9FA; }
|
|
158
|
+
table.data th.num, table.data td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
159
|
+
table.data tr.total td { background: #F5F6F7 !important; font-weight: 600; }
|
|
160
|
+
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
161
|
+
@media (max-width: 860px) { .chart-grid { grid-template-columns: 1fr; } }
|
|
162
|
+
.chart-card { border: 1px solid var(--line-light); border-radius: var(--radius); background: #fff; padding: 12px 12px 8px; }
|
|
163
|
+
.chart-card .ct { font-size: 13px; font-weight: 600; color: var(--text); margin: 0 0 8px; padding: 0 4px; }
|
|
164
|
+
.chart-box { position: relative; height: 280px; }
|
|
165
|
+
.insight {
|
|
166
|
+
display: flex; gap: 0; align-items: flex-start;
|
|
167
|
+
margin-top: 14px; padding: 12px 14px;
|
|
168
|
+
background: var(--brand-soft); border: 1px solid #BACEFD; border-radius: var(--radius);
|
|
169
|
+
font-size: 13px; color: var(--text);
|
|
170
|
+
}
|
|
171
|
+
.insight .tag { flex: 0 0 auto; color: var(--brand); font-weight: 600; margin-right: 8px; }
|
|
172
|
+
.list-card { border: 1px solid var(--line-light); border-radius: var(--radius); padding: 4px 16px 4px 32px; background: var(--bg); }
|
|
173
|
+
.list-card li { margin: 10px 0; color: var(--text); }
|
|
174
|
+
.budget { display: grid; grid-template-columns: 1.2fr 1fr; gap: 18px; align-items: center; }
|
|
175
|
+
@media (max-width: 800px) { .budget { grid-template-columns: 1fr; } }
|
|
176
|
+
.budget-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
177
|
+
.bcell {
|
|
178
|
+
background: var(--brand-soft); border: 1px solid #BACEFD; border-radius: var(--radius);
|
|
179
|
+
padding: 16px; text-align: center;
|
|
180
|
+
}
|
|
181
|
+
.bcell .p { font-size: 22px; font-weight: 600; color: var(--brand); }
|
|
182
|
+
.bcell .n { font-size: 13px; color: var(--text-2); margin-top: 4px; }
|
|
183
|
+
.closing {
|
|
184
|
+
text-align: center; background: #fff; border: 1px solid var(--line-light);
|
|
185
|
+
border-radius: var(--radius-lg); padding: 56px 24px; margin: 20px 0 8px; box-shadow: var(--shadow);
|
|
186
|
+
}
|
|
187
|
+
.closing h2 { margin: 0 0 10px; font-size: 36px; font-weight: 600; }
|
|
188
|
+
.closing p { margin: 0; color: var(--text-3); font-size: 13px; }
|
|
189
|
+
.footnote { text-align: center; color: var(--text-3); font-size: 12px; padding: 12px 0 24px; }
|
|
190
|
+
.hidden { display: none !important; }
|
|
191
|
+
@media print { .topnav { display: none; } .page, .chapter, .cover { break-inside: avoid; box-shadow: none; } }
|
|
192
|
+
</style>
|
|
193
|
+
</head>
|
|
194
|
+
<body>
|
|
195
|
+
<nav class="topnav">
|
|
196
|
+
<div class="topnav-inner">
|
|
197
|
+
<div class="brand-mark"><i></i><span id="nav-brand">Google Ads</span></div>
|
|
198
|
+
<a href="#cover">封面</a>
|
|
199
|
+
<a href="#agenda">浏览逻辑</a>
|
|
200
|
+
<a href="#exec">核心结论</a>
|
|
201
|
+
<a href="#s1">本期表现</a>
|
|
202
|
+
<a href="#explore" id="nav-explore">专项广告</a>
|
|
203
|
+
<a href="#s2">总结</a>
|
|
204
|
+
<a href="#s3">下期规划</a>
|
|
205
|
+
</div>
|
|
206
|
+
</nav>
|
|
207
|
+
<div class="wrap" id="report-root"></div>
|
|
208
|
+
<script>
|
|
209
|
+
window.__GOOGLE_QUARTERLY_REPORT__ = window.__GOOGLE_QUARTERLY_REPORT__ || null;
|
|
210
|
+
</script>
|
|
211
|
+
<script>
|
|
212
|
+
(function () {
|
|
213
|
+
"use strict";
|
|
214
|
+
const PALETTE = ["#3370FF", "#14C0C0", "#7B61FF", "#FF8800", "#2EA121", "#F54A45", "#8F959E"];
|
|
215
|
+
const charts = [];
|
|
216
|
+
|
|
217
|
+
function esc(s) {
|
|
218
|
+
return String(s ?? "")
|
|
219
|
+
.replace(/&/g, "&")
|
|
220
|
+
.replace(/</g, "<")
|
|
221
|
+
.replace(/>/g, ">")
|
|
222
|
+
.replace(/"/g, """);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function num(v) {
|
|
226
|
+
const n = Number(v);
|
|
227
|
+
return Number.isFinite(n) ? n : 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function money(v, currency) {
|
|
231
|
+
const n = Number(v);
|
|
232
|
+
if (!Number.isFinite(n)) return "—";
|
|
233
|
+
const prefix = currency && currency !== "CNY" ? currency + " " : "¥";
|
|
234
|
+
return prefix + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function int(v) {
|
|
238
|
+
const n = Number(v);
|
|
239
|
+
return Number.isFinite(n) ? Math.round(n).toLocaleString("en-US") : "—";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function pct(v) {
|
|
243
|
+
const n = Number(v);
|
|
244
|
+
if (!Number.isFinite(n)) return "—";
|
|
245
|
+
return (n * 100).toFixed(2) + "%";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function deltaHtml(v) {
|
|
249
|
+
if (v == null || !Number.isFinite(Number(v))) return '<span class="flat">—</span>';
|
|
250
|
+
const n = Number(v) * 100;
|
|
251
|
+
const cls = n > 0 ? "up" : n < 0 ? "down" : "flat";
|
|
252
|
+
const arrow = n > 0 ? "▲" : n < 0 ? "▼" : "•";
|
|
253
|
+
return `<span class="${cls}">${arrow} ${Math.abs(n).toFixed(1)}%</span>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function lines(arr) {
|
|
257
|
+
return (Array.isArray(arr) ? arr : []).map((x) => String(x).trim()).filter(Boolean);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function periodLabel(range) {
|
|
261
|
+
if (!range || !range.start) return "—";
|
|
262
|
+
return `${String(range.start).slice(0, 10)} ~ ${String(range.end).slice(0, 10)}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function initChart(el, option) {
|
|
266
|
+
if (!el || typeof echarts === "undefined") return;
|
|
267
|
+
const chart = echarts.init(el);
|
|
268
|
+
chart.setOption(option);
|
|
269
|
+
charts.push(chart);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function render(data) {
|
|
273
|
+
const meta = data.meta || {};
|
|
274
|
+
const kpis = data.kpis || {};
|
|
275
|
+
const cur = kpis.current || {};
|
|
276
|
+
const vs = kpis.vsPrevious || {};
|
|
277
|
+
const narrative = data.narrative || {};
|
|
278
|
+
const sections = narrative.sections || {};
|
|
279
|
+
const plan = narrative.plan || {};
|
|
280
|
+
const special = data.specialAds || { source: "none", items: [] };
|
|
281
|
+
const planDefaults = data.planDefaults || {};
|
|
282
|
+
const currency = meta.currency || "";
|
|
283
|
+
const name = meta.accountName || ("账户 " + (meta.accountId || ""));
|
|
284
|
+
const hideSpecial = !special.source || special.source === "none" || !special.items || !special.items.length;
|
|
285
|
+
const drillCountry = meta.drillCountry || "";
|
|
286
|
+
const adminLabel = meta.drillAdminLabel || "一级行政区";
|
|
287
|
+
const drillTitle = drillCountry ? drillCountry + "下钻" : "消耗最高国家下钻";
|
|
288
|
+
const drillLead = meta.usGeoSource === "subdivisions"
|
|
289
|
+
? `消耗最高国家的${adminLabel},来自 geographic_view + geo_target_state / province / region。`
|
|
290
|
+
: meta.usGeoSource === "country-fallback"
|
|
291
|
+
? `该国没有${adminLabel}分段,回退到国家级一行。`
|
|
292
|
+
: "本期没有地理行。";
|
|
293
|
+
document.title = name + " · Google 季度经营报告";
|
|
294
|
+
document.getElementById("nav-brand").innerHTML = esc(name) + ' · <span>Google Ads</span>';
|
|
295
|
+
document.getElementById("nav-explore").classList.toggle("hidden", hideSpecial);
|
|
296
|
+
|
|
297
|
+
const agenda = lines(narrative.agenda).length
|
|
298
|
+
? lines(narrative.agenda).map((t, i) => ({ n: "0" + (i + 1), t, d: "" }))
|
|
299
|
+
: [
|
|
300
|
+
{ n: "01", t: "先看结论", d: "本期是否达标,花费与询盘怎么走" },
|
|
301
|
+
{ n: "02", t: "拆结构", d: "广告类型、地域、月度节奏" },
|
|
302
|
+
{ n: "03", t: "看专项", d: "视频 / 非搜索若有量再展开" },
|
|
303
|
+
{ n: "04", t: "定下期", d: "默认花费 +30%~+50%,再写动作" },
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const exec = lines(narrative.executiveSummary);
|
|
307
|
+
const root = document.getElementById("report-root");
|
|
308
|
+
root.innerHTML = `
|
|
309
|
+
<header class="cover" id="cover">
|
|
310
|
+
<div class="cover-tag">GOOGLE ADS · QUARTERLY REPORT</div>
|
|
311
|
+
<h1>${esc(name)}</h1>
|
|
312
|
+
<p class="subtitle">季度数据汇总 & 下期规划</p>
|
|
313
|
+
<div class="cover-meta">
|
|
314
|
+
<span>账户:<strong>${esc(meta.accountId || "—")}</strong></span>
|
|
315
|
+
<span>周期:<strong>${esc(periodLabel(meta.current))}</strong></span>
|
|
316
|
+
<span>货币:<strong>${esc(currency || "—")}</strong></span>
|
|
317
|
+
</div>
|
|
318
|
+
</header>
|
|
319
|
+
<section class="page" id="agenda">
|
|
320
|
+
<div class="page-head"><h2>报告浏览逻辑</h2><span class="chip">阅读路径</span></div>
|
|
321
|
+
<p class="lead">建议按「结论 → 证据 → 动作」阅读:先看结果,再拆结构和专项,最后落到下一期怎么干。</p>
|
|
322
|
+
<div class="story">
|
|
323
|
+
${agenda.map((a) => `<div class="story-item"><div class="n">${esc(a.n)}</div><div class="t">${esc(a.t)}</div><p class="d">${esc(a.d)}</p></div>`).join("")}
|
|
324
|
+
</div>
|
|
325
|
+
</section>
|
|
326
|
+
<section class="page" id="exec">
|
|
327
|
+
<div class="page-head"><h2>核心结论</h2><span class="chip">Executive</span></div>
|
|
328
|
+
<ol class="list-card">${exec.map((x) => `<li>${esc(x)}</li>`).join("") || "<li>暂无结论</li>"}</ol>
|
|
329
|
+
</section>
|
|
330
|
+
<section class="chapter" id="s1">
|
|
331
|
+
<p class="num">CHAPTER 01</p>
|
|
332
|
+
<h2>本期表现</h2>
|
|
333
|
+
<p>花费 / 点击 / 转化来自系列加总;询盘口径见页脚。${esc(meta.inquiryNote || "")}</p>
|
|
334
|
+
</section>
|
|
335
|
+
<section class="page">
|
|
336
|
+
<div class="kpi-row c7">
|
|
337
|
+
${kpiCard("花费", money(cur.spend, currency), vs.spend)}
|
|
338
|
+
${kpiCard("展示", int(cur.impressions), vs.impressions)}
|
|
339
|
+
${kpiCard("点击", int(cur.clicks), vs.clicks)}
|
|
340
|
+
${kpiCard("CTR", pct(cur.ctr), vs.ctr)}
|
|
341
|
+
${kpiCard("CPC", money(cur.cpc, currency), vs.cpc)}
|
|
342
|
+
${kpiCard("转化", int(cur.conversions), vs.conversions)}
|
|
343
|
+
${kpiCard("询盘", kpis.inquiries == null ? "—" : int(kpis.inquiries), null)}
|
|
344
|
+
</div>
|
|
345
|
+
<h3 class="st">月度节奏</h3>
|
|
346
|
+
<div class="chart-grid">
|
|
347
|
+
<div class="chart-card"><p class="ct">月度花费</p><div class="chart-box" id="chart-month-spend"></div></div>
|
|
348
|
+
<div class="chart-card"><p class="ct">广告类型花费</p><div class="chart-box" id="chart-adtype"></div></div>
|
|
349
|
+
</div>
|
|
350
|
+
${tableWrap(monthTable(data.monthly || [], currency))}
|
|
351
|
+
<h3 class="st">区域询盘</h3>
|
|
352
|
+
<div class="chart-grid">
|
|
353
|
+
<div class="chart-card"><p class="ct">本期 vs 上期大区</p><div class="chart-box" id="chart-region"></div></div>
|
|
354
|
+
<div class="chart-card"><p class="ct">国家花费 Top</p><div class="chart-box" id="chart-country"></div></div>
|
|
355
|
+
</div>
|
|
356
|
+
${tableWrap(countryTable(data.countries || [], currency))}
|
|
357
|
+
<h3 class="st">${esc(drillTitle)}</h3>
|
|
358
|
+
<p class="lead">${drillLead}</p>
|
|
359
|
+
${tableWrap(stateTable(data.usStates || [], adminLabel))}
|
|
360
|
+
${analysisBlock(sections.efficiency, "投放效率")}
|
|
361
|
+
${analysisBlock(sections.adType, "广告类型")}
|
|
362
|
+
${analysisBlock(sections.region, "区域")}
|
|
363
|
+
${keywordBlock(data.keywordSeeds)}
|
|
364
|
+
</section>
|
|
365
|
+
<section class="chapter ${hideSpecial ? "hidden" : ""}" id="explore">
|
|
366
|
+
<p class="num">CHAPTER 01B</p>
|
|
367
|
+
<h2>${esc(special.label || "专项广告")}</h2>
|
|
368
|
+
<p>${special.source === "youtube"
|
|
369
|
+
? "指标来自 Google Ads 视频报表。频道订阅总数、订阅来源、视频类型本报告不做。"
|
|
370
|
+
: "有 YouTube/视频则用视频名;否则效果最大化;再否则其他非搜索。"}</p>
|
|
371
|
+
</section>
|
|
372
|
+
<section class="page ${hideSpecial ? "hidden" : ""}">
|
|
373
|
+
${special.source === "youtube" ? youtubeAdsKpis(data.videosSummary || {}, currency) : ""}
|
|
374
|
+
${tableWrap(specialTable(special.items || [], currency, special.source))}
|
|
375
|
+
</section>
|
|
376
|
+
<section class="chapter" id="s2">
|
|
377
|
+
<p class="num">CHAPTER 02</p>
|
|
378
|
+
<h2>总结</h2>
|
|
379
|
+
<p>把证据收束成可执行判断,不重复堆数字。</p>
|
|
380
|
+
</section>
|
|
381
|
+
<section class="page">
|
|
382
|
+
${analysisBlock(sections.summary, "总结")}
|
|
383
|
+
</section>
|
|
384
|
+
<section class="chapter" id="s3">
|
|
385
|
+
<p class="num">CHAPTER 03</p>
|
|
386
|
+
<h2>下期规划</h2>
|
|
387
|
+
<p>${esc(planDefaults.note || "默认目标:花费较本期提升 30%~50%")}</p>
|
|
388
|
+
</section>
|
|
389
|
+
<section class="page">
|
|
390
|
+
<div class="budget">
|
|
391
|
+
<div>
|
|
392
|
+
<p class="lead">数字目标只默认花费;CPA / 询盘目标不自动外推,由 Agent 在 narrative.plan 里写。</p>
|
|
393
|
+
${analysisBlock(plan, "规划")}
|
|
394
|
+
</div>
|
|
395
|
+
<div class="budget-grid">
|
|
396
|
+
<div class="bcell"><div class="p">${money(planDefaults.spendLow, currency)}</div><div class="n">花费下限 +30%</div></div>
|
|
397
|
+
<div class="bcell"><div class="p">${money(planDefaults.spendHigh, currency)}</div><div class="n">花费上限 +50%</div></div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</section>
|
|
401
|
+
<section class="closing">
|
|
402
|
+
<h2>谢谢</h2>
|
|
403
|
+
<p>${esc(name)} · ${esc(periodLabel(meta.current))}</p>
|
|
404
|
+
</section>
|
|
405
|
+
<p class="footnote">${esc(meta.inquiryNote || "")}${meta.usGeoSource === "country-fallback" ? ` · ${esc(drillCountry || "消耗最高国家")}下钻为国家回退` : ""}</p>
|
|
406
|
+
`;
|
|
407
|
+
|
|
408
|
+
const months = data.monthly || [];
|
|
409
|
+
initChart(document.getElementById("chart-month-spend"), {
|
|
410
|
+
color: PALETTE,
|
|
411
|
+
tooltip: { trigger: "axis" },
|
|
412
|
+
grid: { left: 48, right: 16, top: 24, bottom: 32 },
|
|
413
|
+
xAxis: { type: "category", data: months.map((m) => m.label || m.yearMonth) },
|
|
414
|
+
yAxis: { type: "value" },
|
|
415
|
+
series: [{ type: "bar", name: "花费", data: months.map((m) => num((m.metrics || {}).spend)), barMaxWidth: 36 }],
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const channelSum = {};
|
|
419
|
+
months.forEach((m) => {
|
|
420
|
+
Object.entries(m.spendByChannel || {}).forEach(([k, v]) => {
|
|
421
|
+
channelSum[k] = (channelSum[k] || 0) + num(v);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
initChart(document.getElementById("chart-adtype"), {
|
|
425
|
+
color: PALETTE,
|
|
426
|
+
tooltip: { trigger: "item" },
|
|
427
|
+
series: [{
|
|
428
|
+
type: "pie",
|
|
429
|
+
radius: ["36%", "68%"],
|
|
430
|
+
data: Object.entries(channelSum).filter(([, v]) => v > 0).map(([name, value]) => ({ name, value })),
|
|
431
|
+
}],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const regionKeys = ["欧美", "东南亚", "印度", "印尼", "中东", "非洲", "其他"];
|
|
435
|
+
const regions = data.regions || {};
|
|
436
|
+
initChart(document.getElementById("chart-region"), {
|
|
437
|
+
color: PALETTE,
|
|
438
|
+
tooltip: { trigger: "axis" },
|
|
439
|
+
legend: { data: ["本期", "上期"] },
|
|
440
|
+
grid: { left: 40, right: 12, top: 32, bottom: 28 },
|
|
441
|
+
xAxis: { type: "category", data: regionKeys },
|
|
442
|
+
yAxis: { type: "value" },
|
|
443
|
+
series: [
|
|
444
|
+
{ type: "bar", name: "本期", data: regionKeys.map((k) => num((regions.current || {})[k])) },
|
|
445
|
+
{ type: "bar", name: "上期", data: regionKeys.map((k) => num((regions.previous || {})[k])) },
|
|
446
|
+
],
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const topCountries = (data.countries || []).slice(0, 8);
|
|
450
|
+
initChart(document.getElementById("chart-country"), {
|
|
451
|
+
color: PALETTE,
|
|
452
|
+
tooltip: { trigger: "axis" },
|
|
453
|
+
grid: { left: 72, right: 16, top: 16, bottom: 24 },
|
|
454
|
+
xAxis: { type: "value" },
|
|
455
|
+
yAxis: { type: "category", data: topCountries.map((c) => c.country).reverse() },
|
|
456
|
+
series: [{ type: "bar", data: topCountries.map((c) => num(c.spend)).reverse(), barMaxWidth: 18 }],
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function kpiCard(label, value, delta) {
|
|
461
|
+
return `<div class="kpi"><div class="label">${esc(label)}</div><div class="val">${value}</div><div class="delta">${deltaHtml(delta)}</div></div>`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function analysisBlock(block, fallbackTitle) {
|
|
465
|
+
const items = lines((block && (block.analysis || block.goals)) || (typeof (block && block.text) === "string" ? [block.text] : []));
|
|
466
|
+
if (!items.length) return "";
|
|
467
|
+
return `<div class="insight"><div class="tag">${esc(fallbackTitle)}</div><div>${items.map((x) => esc(x)).join("<br/>")}</div></div>`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function tableWrap(inner) {
|
|
471
|
+
return `<div class="table-wrap">${inner}</div>`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function monthTable(rows, currency) {
|
|
475
|
+
const body = rows.map((r) => {
|
|
476
|
+
const m = r.metrics || {};
|
|
477
|
+
return `<tr><td>${esc(r.label || r.yearMonth)}</td><td class="num">${money(m.spend, currency)}</td><td class="num">${int(m.clicks)}</td><td class="num">${int(m.conversions)}</td><td class="num">${r.inquiries == null ? "—" : int(r.inquiries)}</td><td class="num">${r.inquiryCost == null ? "—" : money(r.inquiryCost, currency)}</td></tr>`;
|
|
478
|
+
}).join("");
|
|
479
|
+
return `<table class="data"><thead><tr><th>月份</th><th class="num">花费</th><th class="num">点击</th><th class="num">转化</th><th class="num">询盘</th><th class="num">询盘成本</th></tr></thead><tbody>${body}</tbody></table>`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function countryTable(rows, currency) {
|
|
483
|
+
const body = rows.slice(0, 20).map((r) =>
|
|
484
|
+
`<tr><td>${esc(r.country)}</td><td class="num">${money(r.spend, currency)}</td><td class="num">${r.inquiries == null ? "—" : int(r.inquiries)}</td><td class="num">${r.inquiryCost == null ? "—" : money(r.inquiryCost, currency)}</td></tr>`
|
|
485
|
+
).join("");
|
|
486
|
+
return `<table class="data"><thead><tr><th>国家</th><th class="num">花费</th><th class="num">询盘</th><th class="num">询盘成本</th></tr></thead><tbody>${body}</tbody></table>`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function stateTable(rows, adminLabel) {
|
|
490
|
+
const col = adminLabel || "一级行政区";
|
|
491
|
+
if (!rows.length) return `<p class="lead">本期没有该国${esc(col)}数据。</p>`;
|
|
492
|
+
const body = rows.map((r) =>
|
|
493
|
+
`<tr><td>${esc(r.name)}</td><td class="num">${int(r.impressions)}</td><td class="num">${int(r.clicks)}</td><td class="num">${pct(r.ctr)}</td><td class="num">${money(r.spend)}</td></tr>`
|
|
494
|
+
).join("");
|
|
495
|
+
return `<table class="data"><thead><tr><th>${esc(col)}</th><th class="num">展示</th><th class="num">点击</th><th class="num">CTR</th><th class="num">花费</th></tr></thead><tbody>${body}</tbody></table>`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function durationLabel(ms) {
|
|
499
|
+
const n = Number(ms);
|
|
500
|
+
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
501
|
+
const sec = Math.round(n / 1000);
|
|
502
|
+
const m = Math.floor(sec / 60);
|
|
503
|
+
const s = sec % 60;
|
|
504
|
+
return m > 0 ? `${m}:${String(s).padStart(2, "0")}` : `${s}s`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function youtubeAdsKpis(summary, currency) {
|
|
508
|
+
if (!summary || !summary.count) return "";
|
|
509
|
+
const cards = [
|
|
510
|
+
kpiCard("花费", money(summary.spend, currency), null),
|
|
511
|
+
kpiCard("展示", int(summary.impressions), null),
|
|
512
|
+
kpiCard("TrueView", int(summary.videoViews), null),
|
|
513
|
+
kpiCard("TrueView 率", pct(summary.viewRate), null),
|
|
514
|
+
kpiCard("完整观看率", pct(summary.completeRate), null),
|
|
515
|
+
kpiCard("CPV", summary.averageCpv == null ? "—" : money(summary.averageCpv, currency), null),
|
|
516
|
+
kpiCard("视频数", int(summary.count), null),
|
|
517
|
+
];
|
|
518
|
+
if (summary.adSubscriptions != null) {
|
|
519
|
+
cards.push(kpiCard("广告带来订阅", int(summary.adSubscriptions), null));
|
|
520
|
+
}
|
|
521
|
+
const channels = Array.isArray(summary.channelIds) ? summary.channelIds.filter(Boolean) : [];
|
|
522
|
+
const note = summary.adSubscriptionNote
|
|
523
|
+
? esc(summary.adSubscriptionNote)
|
|
524
|
+
: "口径=Google Ads 视频报表(TrueView / 四分位完播),不是 YouTube Analytics。";
|
|
525
|
+
return `<div class="kpi-row c7">${cards.join("")}</div>
|
|
526
|
+
<p class="lead">${note}${channels.length ? ` · 频道 ${channels.map(esc).join("、")}` : ""}</p>`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function keywordBlock(seeds) {
|
|
530
|
+
const items = (seeds && seeds.items) || [];
|
|
531
|
+
if (!items.length) return "";
|
|
532
|
+
const body = items.slice(0, 20).map((r) =>
|
|
533
|
+
`<tr><td>${esc(r.keyword)}</td><td class="num">${int(r.monthlySearches)}</td></tr>`
|
|
534
|
+
).join("");
|
|
535
|
+
return `<h3 class="st">账户种子词(Keyword Planner)</h3>
|
|
536
|
+
<p class="lead">${esc((seeds && seeds.note) || "数据源=Keyword Planner,不是行业指数")}</p>
|
|
537
|
+
${tableWrap(`<table class="data"><thead><tr><th>关键词</th><th class="num">月搜索量</th></tr></thead><tbody>${body}</tbody></table>`)}`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function specialTable(rows, currency, source) {
|
|
541
|
+
if (source === "youtube") {
|
|
542
|
+
const body = rows.map((r) =>
|
|
543
|
+
`<tr>
|
|
544
|
+
<td>${esc(r.title)}</td>
|
|
545
|
+
<td>${esc(r.channelId || "—")}</td>
|
|
546
|
+
<td class="num">${durationLabel(r.durationMillis)}</td>
|
|
547
|
+
<td class="num">${money(r.spend, currency)}</td>
|
|
548
|
+
<td class="num">${int(r.impressions)}</td>
|
|
549
|
+
<td class="num">${int(r.videoViews)}</td>
|
|
550
|
+
<td class="num">${pct(r.viewRate)}</td>
|
|
551
|
+
<td class="num">${pct(r.completeRate)}</td>
|
|
552
|
+
<td class="num">${int(r.clicks)}</td>
|
|
553
|
+
<td class="num">${int(r.conversions)}</td>
|
|
554
|
+
<td class="num">${r.averageCpv ? money(r.averageCpv, currency) : "—"}</td>
|
|
555
|
+
<td class="num">${int(r.viewThroughConversions)}</td>
|
|
556
|
+
</tr>`
|
|
557
|
+
).join("");
|
|
558
|
+
return `<table class="data"><thead><tr>
|
|
559
|
+
<th>视频</th><th>频道</th><th class="num">时长</th><th class="num">花费</th>
|
|
560
|
+
<th class="num">展示</th><th class="num">TrueView</th><th class="num">TrueView 率</th>
|
|
561
|
+
<th class="num">完播率</th><th class="num">点击</th><th class="num">转化</th>
|
|
562
|
+
<th class="num">CPV</th><th class="num">观后转化</th>
|
|
563
|
+
</tr></thead><tbody>${body}</tbody></table>`;
|
|
564
|
+
}
|
|
565
|
+
const body = rows.map((r) =>
|
|
566
|
+
`<tr><td>${esc(r.title)}</td><td class="num">${money(r.spend, currency)}</td><td class="num">${int(r.impressions)}</td><td class="num">${int(r.clicks)}</td><td class="num">${int(r.conversions)}</td></tr>`
|
|
567
|
+
).join("");
|
|
568
|
+
return `<table class="data"><thead><tr><th>名称</th><th class="num">花费</th><th class="num">展示</th><th class="num">点击</th><th class="num">转化</th></tr></thead><tbody>${body}</tbody></table>`;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
window.addEventListener("resize", function () {
|
|
572
|
+
charts.forEach((c) => c.resize());
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const payload = window.__GOOGLE_QUARTERLY_REPORT__;
|
|
576
|
+
if (!payload) {
|
|
577
|
+
document.getElementById("report-root").innerHTML =
|
|
578
|
+
'<div class="page">未找到报告数据(window.__GOOGLE_QUARTERLY_REPORT__ 为空),请通过 `google-quarterly render` 生成本文件。</div>';
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
render(payload);
|
|
582
|
+
})();
|
|
583
|
+
</script>
|
|
584
|
+
</body>
|
|
585
|
+
</html>
|