feat: add TaurusX CPM CPC and CPI
This commit is contained in:
+22
-8
@@ -91,6 +91,10 @@ def build_dataset(
|
||||
for api_row in api_rows:
|
||||
campaign_name = str(api_row.get("campaign_name") or "").strip()
|
||||
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(
|
||||
{
|
||||
"date": str(api_row.get("date") or ""),
|
||||
@@ -98,14 +102,21 @@ def build_dataset(
|
||||
"campaign_name": campaign_name,
|
||||
"pkg": extract_pkg(campaign_name, country),
|
||||
"country": country,
|
||||
"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),
|
||||
"impressions": impressions,
|
||||
"clicks": clicks,
|
||||
"conversions": conversions,
|
||||
"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)
|
||||
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")
|
||||
return {
|
||||
"meta": {
|
||||
@@ -119,10 +130,13 @@ def build_dataset(
|
||||
"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"]}),
|
||||
"unparsed_pkg_rows": sum(not row["pkg"] for row in records),
|
||||
"impressions": sum(row["impressions"] for row in records),
|
||||
"clicks": sum(row["clicks"] for row in records),
|
||||
"conversions": sum(row["conversions"] for row in records),
|
||||
"cost": round(sum(row["cost"] for row in records), 4),
|
||||
"impressions": total_impressions,
|
||||
"clicks": total_clicks,
|
||||
"conversions": total_conversions,
|
||||
"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,
|
||||
}
|
||||
|
||||
+24
-8
@@ -142,6 +142,11 @@
|
||||
function money(value) {
|
||||
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) {
|
||||
const date = new Date(value);
|
||||
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('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`),
|
||||
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');
|
||||
@@ -192,14 +200,15 @@
|
||||
[
|
||||
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
|
||||
['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)));
|
||||
const exportButton = el('button', 'button', '导出 TaurusX 数据');
|
||||
exportButton.type = 'button'; exportButton.id = 'exportButton';
|
||||
filters.append(search, reportDate, country, sort, exportButton);
|
||||
header.append(heading, filters);
|
||||
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);
|
||||
[reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter));
|
||||
@@ -255,6 +264,9 @@
|
||||
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
|
||||
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(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');
|
||||
meta.textContent = '';
|
||||
@@ -268,12 +280,12 @@
|
||||
wrap.textContent = '';
|
||||
const table = el('table');
|
||||
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)));
|
||||
thead.append(header); table.append(thead);
|
||||
const tbody = el('tbody');
|
||||
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);
|
||||
}
|
||||
rows.forEach(record => {
|
||||
@@ -288,7 +300,10 @@
|
||||
el('td', '', nf.format(record.impressions || 0)),
|
||||
el('td', '', nf.format(record.clicks || 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);
|
||||
});
|
||||
@@ -306,10 +321,11 @@
|
||||
}
|
||||
|
||||
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 => [
|
||||
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 url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
||||
|
||||
+673
-169
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user