feat: add TaurusX CPM CPC and CPI

This commit is contained in:
王源
2026-10-15 17:53:45 +08:00
parent 01b4e3ddae
commit ecad390e63
3 changed files with 719 additions and 185 deletions
+22 -8
View File
@@ -91,6 +91,10 @@ def build_dataset(
for api_row in api_rows: for api_row in api_rows:
campaign_name = str(api_row.get("campaign_name") or "").strip() campaign_name = str(api_row.get("campaign_name") or "").strip()
country = str(api_row.get("country") or "").strip().upper() country = str(api_row.get("country") or "").strip().upper()
impressions = int(float(api_row.get("impressions") or 0))
clicks = int(float(api_row.get("clicks") or 0))
conversions = int(float(api_row.get("conversions") or 0))
cost = round(float(api_row.get("cost") or 0), 4)
records.append( records.append(
{ {
"date": str(api_row.get("date") or ""), "date": str(api_row.get("date") or ""),
@@ -98,14 +102,21 @@ def build_dataset(
"campaign_name": campaign_name, "campaign_name": campaign_name,
"pkg": extract_pkg(campaign_name, country), "pkg": extract_pkg(campaign_name, country),
"country": country, "country": country,
"impressions": int(float(api_row.get("impressions") or 0)), "impressions": impressions,
"clicks": int(float(api_row.get("clicks") or 0)), "clicks": clicks,
"conversions": int(float(api_row.get("conversions") or 0)), "conversions": conversions,
"cost": round(float(api_row.get("cost") or 0), 4), "cost": cost,
"cpm": round(cost / impressions * 1000, 6) if impressions else None,
"cpc": round(cost / clicks, 6) if clicks else None,
"cpi": round(cost / conversions, 6) if conversions else None,
} }
) )
records.sort(key=lambda row: (row["date"], row["cost"], row["clicks"]), reverse=True) records.sort(key=lambda row: (row["date"], row["cost"], row["clicks"]), reverse=True)
total_impressions = sum(row["impressions"] for row in records)
total_clicks = sum(row["clicks"] for row in records)
total_conversions = sum(row["conversions"] for row in records)
total_cost = round(sum(row["cost"] for row in records), 4)
fetched_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds") fetched_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
return { return {
"meta": { "meta": {
@@ -119,10 +130,13 @@ def build_dataset(
"pkg_country_count": len({(row["pkg"].casefold(), row["country"]) for row in records if row["pkg"]}), "pkg_country_count": len({(row["pkg"].casefold(), row["country"]) for row in records if row["pkg"]}),
"countries": len({row["country"] for row in records if row["country"]}), "countries": len({row["country"] for row in records if row["country"]}),
"unparsed_pkg_rows": sum(not row["pkg"] for row in records), "unparsed_pkg_rows": sum(not row["pkg"] for row in records),
"impressions": sum(row["impressions"] for row in records), "impressions": total_impressions,
"clicks": sum(row["clicks"] for row in records), "clicks": total_clicks,
"conversions": sum(row["conversions"] for row in records), "conversions": total_conversions,
"cost": round(sum(row["cost"] for row in records), 4), "cost": total_cost,
"cpm": round(total_cost / total_impressions * 1000, 6) if total_impressions else None,
"cpc": round(total_cost / total_clicks, 6) if total_clicks else None,
"cpi": round(total_cost / total_conversions, 6) if total_conversions else None,
}, },
"records": records, "records": records,
} }
+24 -8
View File
@@ -142,6 +142,11 @@
function money(value) { function money(value) {
return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
} }
function unitCost(value) {
return value === null || value === undefined
? '-'
: `$${Number(value).toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 })}`;
}
function formatTime(value) { function formatTime(value) {
const date = new Date(value); const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false }); return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false });
@@ -170,7 +175,10 @@
kpi('Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`), kpi('Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`),
kpi('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`), kpi('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`),
kpi('Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`), kpi('Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`),
kpi('Cost', money(meta.cost), `${nf.format(meta.rows)} 条投放明细`) kpi('Cost', money(meta.cost), `${nf.format(meta.rows)} 条投放明细`),
kpi('CPM', unitCost(meta.cpm), 'Cost / Impressions × 1000'),
kpi('CPC', unitCost(meta.cpc), 'Cost / Clicks'),
kpi('CPI', unitCost(meta.cpi), 'Cost / Conversions')
); );
const panel = el('section', 'panel workspace'); const panel = el('section', 'panel workspace');
@@ -192,14 +200,15 @@
[ [
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'], ['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'], ['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'],
['conversions-desc','Conversions:高到低'], ['pkg-asc','Pkg:A-Z'] ['conversions-desc','Conversions:高到低'], ['cpm-desc','CPM:高到低'],
['cpc-desc','CPC:高到低'], ['cpi-desc','CPI:高到低'], ['pkg-asc','Pkg:A-Z']
].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value))); ].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value)));
const exportButton = el('button', 'button', '导出 TaurusX 数据'); const exportButton = el('button', 'button', '导出 TaurusX 数据');
exportButton.type = 'button'; exportButton.id = 'exportButton'; exportButton.type = 'button'; exportButton.id = 'exportButton';
filters.append(search, reportDate, country, sort, exportButton); filters.append(search, reportDate, country, sort, exportButton);
header.append(heading, filters); header.append(heading, filters);
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination')); panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
app.append(summary, panel, el('footer', '', 'TaurusX 投放数据 · Impressions、Clicks、Conversions、Cost')); app.append(summary, panel, el('footer', '', 'TaurusX 投放数据 · Impressions、Clicks、Conversions、Cost、CPM、CPC、CPI'));
search.addEventListener('input', resetAndFilter); search.addEventListener('input', resetAndFilter);
[reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter)); [reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter));
@@ -255,6 +264,9 @@
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`); setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`); setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
setCard(3, money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`); setCard(3, money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`);
setCard(4, unitCost(impressions ? cost / impressions * 1000 : null), 'Cost / Impressions × 1000');
setCard(5, unitCost(clicks ? cost / clicks : null), 'Cost / Clicks');
setCard(6, unitCost(conversions ? cost / conversions : null), 'Cost / Conversions');
const meta = document.querySelector('.result-meta'); const meta = document.querySelector('.result-meta');
meta.textContent = ''; meta.textContent = '';
@@ -268,12 +280,12 @@
wrap.textContent = ''; wrap.textContent = '';
const table = el('table'); const table = el('table');
const thead = el('thead'); const header = el('tr'); const thead = el('thead'); const header = el('tr');
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost'] ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI']
.forEach(label => header.append(el('th', '', label))); .forEach(label => header.append(el('th', '', label)));
thead.append(header); table.append(thead); thead.append(header); table.append(thead);
const tbody = el('tbody'); const tbody = el('tbody');
if (!rows.length) { if (!rows.length) {
const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 9; const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 12;
const row = el('tr'); row.append(cell); tbody.append(row); const row = el('tr'); row.append(cell); tbody.append(row);
} }
rows.forEach(record => { rows.forEach(record => {
@@ -288,7 +300,10 @@
el('td', '', nf.format(record.impressions || 0)), el('td', '', nf.format(record.impressions || 0)),
el('td', '', nf.format(record.clicks || 0)), el('td', '', nf.format(record.clicks || 0)),
el('td', '', nf.format(record.conversions || 0)), el('td', '', nf.format(record.conversions || 0)),
el('td', '', money(record.cost)) el('td', '', money(record.cost)),
el('td', '', unitCost(record.cpm)),
el('td', '', unitCost(record.cpc)),
el('td', '', unitCost(record.cpi))
); );
tbody.append(row); tbody.append(row);
}); });
@@ -306,10 +321,11 @@
} }
function exportCsv() { function exportCsv() {
const headers = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost']; const headers = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI'];
const rows = [headers, ...state.filtered.map(record => [ const rows = [headers, ...state.filtered.map(record => [
record.date, record.campaign_id, record.campaign_name, record.pkg, record.country, record.date, record.campaign_id, record.campaign_name, record.pkg, record.country,
record.impressions, record.clicks, record.conversions, record.cost record.impressions, record.clicks, record.conversions, record.cost,
record.cpm ?? '', record.cpc ?? '', record.cpi ?? ''
])]; ])];
const csv = '\ufeff' + rows.map(row => row.map(value => `"${String(value ?? '').replaceAll('"', '""')}"`).join(',')).join('\n'); const csv = '\ufeff' + rows.map(row => row.map(value => `"${String(value ?? '').replaceAll('"', '""')}"`).join(',')).join('\n');
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' })); const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
+673 -169
View File
File diff suppressed because it is too large Load Diff