09-09 起消耗改从 MIDAS 取(C00000196_TECDO 账户投放中 campaign),09-08 前用冻结表格
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
push.sh
|
||||
build_data.py
|
||||
fetch_midas.js
|
||||
baseline_sheet.json
|
||||
midas_latest.json
|
||||
midas_history.json
|
||||
.DS_Store
|
||||
|
||||
+75
-22
@@ -11,7 +11,7 @@
|
||||
--page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781;
|
||||
--grid: #e1e0d9; --axis: #c3c2b7; --border: rgba(11,11,11,.10);
|
||||
--s1: #2a78d6; --s2: #eb6834; --s3: #1baf7a; --s4: #eda100; --s5: #e87ba4; --other: #a3a29b;
|
||||
--warn: #fab219; --crit: #d03b3b;
|
||||
--good: #006300; --warn: #fab219; --crit: #d03b3b;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:where(:not([data-theme="light"])) {
|
||||
@@ -19,6 +19,7 @@
|
||||
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
|
||||
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10);
|
||||
--s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --other: #6b6a65;
|
||||
--good: #0ca30c;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
@@ -26,6 +27,7 @@
|
||||
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
|
||||
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10);
|
||||
--s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --other: #6b6a65;
|
||||
--good: #0ca30c;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||
@@ -71,12 +73,14 @@ table { border-collapse: collapse; width: 100%; font-size: 13px; white-space: no
|
||||
th { text-align: left; color: var(--muted); font-weight: 500; font-size: 12px; padding: 6px 10px; border-bottom: 1px solid var(--axis); }
|
||||
td { padding: 7px 10px; border-bottom: 1px solid var(--grid); }
|
||||
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
td.muted { color: var(--muted); }
|
||||
tr.sub td { font-weight: 600; border-top: 1px solid var(--axis); }
|
||||
tfoot td { font-weight: 600; border-bottom: none; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 5px; color: var(--ink); }
|
||||
.badge-icon { display: inline-grid; place-items: center; width: 15px; height: 15px; border-radius: 50%; font-size: 10px; font-weight: 700; color: #0b0b0b; }
|
||||
.badge.warn .badge-icon { background: var(--warn); }
|
||||
.badge.crit .badge-icon { background: var(--crit); color: #fff; }
|
||||
.badge.good .badge-icon { background: var(--good); color: #fff; }
|
||||
footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
||||
#err { background: var(--surface); border: 1px solid var(--crit); border-radius: 8px; padding: 14px; margin-top: 20px; }
|
||||
</style>
|
||||
@@ -95,7 +99,7 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>每日消耗</h2>
|
||||
<p class="desc">按包堆叠,悬停查看当天每个包的消耗、注册和首充</p>
|
||||
<p class="desc" id="spend-desc">按包堆叠,悬停查看当天每个包的数据</p>
|
||||
</div>
|
||||
<div class="seg" id="range" role="group" aria-label="时间范围">
|
||||
<button type="button" data-range="7" aria-pressed="true">近 7 天</button>
|
||||
@@ -113,9 +117,20 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
||||
<div class="chart" id="balance-chart"><div class="tip" hidden></div></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>MIDAS campaign</h2>
|
||||
<p class="desc" id="camps-desc">账户 C00000196_TECDO HONG KONG LIMITED,DSP投放页状态为「投放中」的 campaign</p>
|
||||
<div class="table-wrap">
|
||||
<table id="camps">
|
||||
<thead><tr><th>广告活动</th><th>ID</th><th>推广包</th><th>状态</th><th class="num">安装</th><th class="num">消耗</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>分包汇总</h2>
|
||||
<p class="desc">全部历史,按消耗从高到低;成本 = 消耗 ÷ 首充</p>
|
||||
<p class="desc">全部历史,按消耗从高到低;成本 = 消耗 ÷ 首充。MIDAS 没有注册和首充,带 * 的数只统计表格部分</p>
|
||||
<div class="table-wrap">
|
||||
<table id="pkgs">
|
||||
<thead><tr><th>产品名</th><th>投放日期</th><th class="num">安装</th><th class="num">注册</th><th class="num">首充</th><th class="num">消耗</th><th class="num">成本</th></tr></thead>
|
||||
@@ -126,7 +141,7 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
||||
|
||||
<section class="card">
|
||||
<h2>分月汇总</h2>
|
||||
<p class="desc">每月先列合计,再列当月有数据的包</p>
|
||||
<p class="desc">每月先列合计,再列当月有数据的包;带 * 的注册、首充、成本只统计表格部分</p>
|
||||
<div class="table-wrap">
|
||||
<table id="months">
|
||||
<thead><tr><th>月份</th><th>产品名</th><th class="num">安装</th><th class="num">注册</th><th class="num">首充</th><th class="num">消耗</th><th class="num">成本</th></tr></thead>
|
||||
@@ -139,13 +154,13 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>分天明细</h2>
|
||||
<p class="desc">按日期倒序</p>
|
||||
<p class="desc">按日期倒序;MIDAS 的天没有注册和首充</p>
|
||||
</div>
|
||||
<select id="pkg-filter" aria-label="按包筛选"></select>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="daily">
|
||||
<thead><tr><th>日期</th><th>产品名</th><th class="num">安装</th><th class="num">注册</th><th class="num">首充</th><th class="num">消耗</th><th class="num">成本</th></tr></thead>
|
||||
<thead><tr><th>日期</th><th>产品名</th><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></tbody><tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
@@ -173,7 +188,6 @@ const h = (tag, cls, text) => { const e = document.createElement(tag); if (cls)
|
||||
const r2 = v => Math.round(v * 100) / 100;
|
||||
const usd = (v, dp = 2) => (v < 0 ? '−$' : '$') + Math.abs(v).toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp });
|
||||
const int = v => Number(v || 0).toLocaleString('en-US');
|
||||
const costText = (spend, ftd) => ftd ? usd(spend / ftd) : '—';
|
||||
const shortDate = dt => dt.slice(5).replace('-', '/');
|
||||
const addDays = (s, n) => { const t = new Date(s + 'T00:00:00Z'); t.setUTCDate(t.getUTCDate() + n); return t.toISOString().slice(0, 10); };
|
||||
const dayRange = (a, b) => { const out = []; for (let s = a; s <= b; s = addDays(s, 1)) out.push(s); return out; };
|
||||
@@ -215,16 +229,30 @@ function tr(cells) {
|
||||
cells.forEach(c => {
|
||||
const td = h('td', c.cls || '');
|
||||
if (c.color) td.append(swatch(c.color));
|
||||
td.append(document.createTextNode(c.text));
|
||||
if (c.node) td.append(c.node); else td.append(document.createTextNode(c.text));
|
||||
row.append(td);
|
||||
});
|
||||
return row;
|
||||
}
|
||||
const sumRows = rows => rows.reduce((a, r) => ({ installs: a.installs + r.installs, reg: a.reg + r.reg, ftd: a.ftd + r.ftd, spend: r2(a.spend + r.spend) }), { installs: 0, reg: 0, ftd: 0, spend: 0 });
|
||||
const metricCells = t => [
|
||||
{ text: int(t.installs), cls: 'num' }, { text: int(t.reg), cls: 'num' }, { text: int(t.ftd), cls: 'num' },
|
||||
{ text: usd(t.spend), cls: 'num' }, { text: costText(t.spend, t.ftd), cls: 'num' },
|
||||
];
|
||||
|
||||
// 注册、首充只有表格部分有(MIDAS 为 null):成本只用有首充数据的那部分消耗来算
|
||||
const sumRows = rows => rows.reduce((a, r) => {
|
||||
a.installs += r.installs;
|
||||
a.spend = r2(a.spend + r.spend);
|
||||
if (r.ftd == null) a.unknown++;
|
||||
else { a.known++; a.reg += r.reg || 0; a.ftd += r.ftd; a.costSpend = r2(a.costSpend + r.spend); }
|
||||
return a;
|
||||
}, { installs: 0, reg: 0, ftd: 0, spend: 0, costSpend: 0, known: 0, unknown: 0 });
|
||||
const metricCells = t => {
|
||||
const star = t.known && t.unknown ? '*' : '';
|
||||
return [
|
||||
{ text: int(t.installs), cls: 'num' },
|
||||
{ text: t.known ? int(t.reg) + star : '—', cls: 'num' + (t.known ? '' : ' muted') },
|
||||
{ text: t.known ? int(t.ftd) + star : '—', cls: 'num' + (t.known ? '' : ' muted') },
|
||||
{ text: usd(t.spend), cls: 'num' },
|
||||
{ text: t.known && t.ftd ? usd(t.costSpend / t.ftd) + star : '—', cls: 'num' + (t.known && t.ftd ? '' : ' muted') },
|
||||
];
|
||||
};
|
||||
|
||||
function derive(d) {
|
||||
const end = d.range.end;
|
||||
@@ -242,7 +270,7 @@ function derive(d) {
|
||||
if (pkgs.length > SLOTS) series.push({ label: `其他 ${pkgs.length - SLOTS} 个包`, color: 'var(--other)', members: pkgs.slice(SLOTS).map(p => p.pkg) });
|
||||
const seriesOf = {};
|
||||
series.forEach((s, i) => s.members.forEach(p => { seriesOf[p] = i; }));
|
||||
const colorOf = pkg => series[seriesOf[pkg]].color;
|
||||
const colorOf = pkg => series[seriesOf[pkg]] ? series[seriesOf[pkg]].color : 'var(--other)';
|
||||
pkgs.forEach(p => { p.color = colorOf(p.pkg); });
|
||||
|
||||
const byDate = {};
|
||||
@@ -284,8 +312,8 @@ function renderTiles(S, d) {
|
||||
else sub = isFinite(days) ? `按近 7 天日均约可用 ${Math.floor(days)} 天` : '近 7 天没有消耗';
|
||||
add('当前余额', usd(S.balance), sub);
|
||||
add('近 7 天消耗', usd(S.w7.spend), `${shortDate(addDays(S.end, -6))}–${shortDate(S.end)},日均 ${usd(S.avg7)}`);
|
||||
add('近 7 天首充', int(S.w7.ftd), `成本 ${costText(S.w7.spend, S.w7.ftd)} · 注册 ${int(S.w7.reg)}`);
|
||||
add('累计消耗', usd(S.total.spend), `${d.range.start} 起 · 成本 ${costText(S.total.spend, S.total.ftd)}`);
|
||||
add('近 7 天安装', int(S.w7.installs), S.w7.installs ? `eCPI ${usd(S.w7.spend / S.w7.installs)}` : '暂无安装');
|
||||
add('累计消耗', usd(S.total.spend), `${d.range.start} 起 · 首充成本 ${S.total.ftd ? usd(S.total.costSpend / S.total.ftd) : '—'}(表格部分)`);
|
||||
add('累计到账', usd(S.received), `打款 ${usd(S.paid, 0)} · 共 ${S.recharges.length} 笔`);
|
||||
}
|
||||
|
||||
@@ -313,7 +341,7 @@ function drawSpend(S, d) {
|
||||
return e;
|
||||
}));
|
||||
|
||||
const W = Math.max(300, wrap.clientWidth), H = 260, m = { l: 56, r: 14, t: 24, b: 30 };
|
||||
const W = Math.max(300, wrap.clientWidth), H = 260, m = { l: 56, r: 14, t: 28, b: 30 };
|
||||
const pw = W - m.l - m.r, ph = H - m.t - m.b;
|
||||
const sc = scaleY(0, Math.max(...totals, 1));
|
||||
const y = v => m.t + ph - (v - sc.lo) / (sc.hi - sc.lo) * ph;
|
||||
@@ -326,6 +354,16 @@ function drawSpend(S, d) {
|
||||
el('line', { x1: m.l, x2: W - m.r, y1: y(t), y2: y(t), class: t === 0 ? 'axis' : 'grid' }, svg);
|
||||
el('text', { x: m.l - 8, y: y(t) + 4, 'text-anchor': 'end', class: 'tick' }, svg).textContent = usd(t, sc.dp);
|
||||
});
|
||||
|
||||
// 表格数据和 MIDAS 数据的分界线
|
||||
const k = dates.indexOf(d.midas_from);
|
||||
if (k > 0) {
|
||||
const xk = m.l + band * k;
|
||||
el('line', { x1: xk, x2: xk, y1: m.t - 6, y2: m.t + ph, class: 'axis' }, svg);
|
||||
el('text', { x: xk + 4, y: m.t - 10, 'text-anchor': 'start', class: 'tick' }, svg).textContent = 'MIDAS →';
|
||||
el('text', { x: xk - 4, y: m.t - 10, 'text-anchor': 'end', class: 'tick' }, svg).textContent = '← 表格';
|
||||
}
|
||||
|
||||
const step = labelStep(n, pw);
|
||||
const maxI = totals.indexOf(Math.max(...totals)), lastI = n - 1;
|
||||
dates.forEach((dt, i) => {
|
||||
@@ -367,7 +405,8 @@ function spendTip(S, dt, total) {
|
||||
rows.forEach(r => {
|
||||
const row = h('div', 'tip-row');
|
||||
row.append(swatch(S.colorOf(r.pkg)), h('span', 'tip-name', r.pkg), h('span', 'tip-val', usd(r.spend)));
|
||||
f.append(row, h('div', 'tip-sub', `安装 ${int(r.installs)} · 注册 ${int(r.reg)} · 首充 ${int(r.ftd)}`));
|
||||
const detail = r.ftd == null ? `安装 ${int(r.installs)} · 来自 MIDAS` : `安装 ${int(r.installs)} · 注册 ${int(r.reg)} · 首充 ${int(r.ftd)}`;
|
||||
f.append(row, h('div', 'tip-sub', detail));
|
||||
});
|
||||
if (rows.length > 1) {
|
||||
const tot = h('div', 'tip-row tip-total');
|
||||
@@ -427,6 +466,14 @@ function drawBalance(S) {
|
||||
}
|
||||
|
||||
function renderTables(S, d) {
|
||||
const camps = d.campaigns || [];
|
||||
$('#camps-desc').textContent = `账户 C00000196_TECDO HONG KONG LIMITED,${shortDate(d.midas_from)} 起的数据;列出状态为「投放中」的 campaign,以及期间内有过消耗、现在已停的`;
|
||||
$('#camps tbody').replaceChildren(...(camps.length ? camps.map(c => tr([
|
||||
{ text: c.name }, { text: c.id }, { text: c.pkg || c.app || '—', color: c.pkg ? S.colorOf(c.pkg) : null },
|
||||
{ node: c.active ? badge('good', '✓', '投放中') : document.createTextNode('已停') },
|
||||
{ text: int(c.installs), cls: 'num' }, { text: usd(c.spend), cls: 'num' },
|
||||
])) : [tr([{ text: `还没有 ${d.midas_from} 起的 MIDAS 数据` }, { text: '' }, { text: '' }, { text: '' }, { text: '' }, { text: '' }])]));
|
||||
|
||||
$('#pkgs tbody').replaceChildren(...S.pkgs.map(p => tr([{ text: p.pkg, color: p.color }, { text: `${p.first} ~ ${p.last}` }, ...metricCells(p)])));
|
||||
$('#pkgs tfoot').replaceChildren(tr([{ text: '合计' }, { text: `${d.range.start} ~ ${d.range.end}` }, ...metricCells(S.total)]));
|
||||
|
||||
@@ -446,7 +493,9 @@ function renderTables(S, d) {
|
||||
});
|
||||
|
||||
const sel = $('#pkg-filter');
|
||||
const current = sel.value;
|
||||
sel.replaceChildren(new Option('全部包', ''), ...S.pkgs.map(p => new Option(p.pkg, p.pkg)));
|
||||
sel.value = current;
|
||||
sel.onchange = () => renderDaily(S, d);
|
||||
renderDaily(S, d);
|
||||
|
||||
@@ -461,8 +510,10 @@ function renderTables(S, d) {
|
||||
function renderDaily(S, d) {
|
||||
const f = $('#pkg-filter').value;
|
||||
const rows = d.daily.filter(r => !f || r.pkg === f).sort((a, b) => b.dt.localeCompare(a.dt) || b.spend - a.spend);
|
||||
$('#daily tbody').replaceChildren(...rows.map(r => tr([{ text: r.dt }, { text: r.pkg, color: S.colorOf(r.pkg) }, ...metricCells(r)])));
|
||||
$('#daily tfoot').replaceChildren(tr([{ text: '合计' }, { text: f || '全部包' }, ...metricCells(sumRows(rows))]));
|
||||
$('#daily tbody').replaceChildren(...rows.map(r => tr([
|
||||
{ text: r.dt }, { text: r.pkg, color: S.colorOf(r.pkg) }, { text: r.src === 'midas' ? 'MIDAS' : '表格' }, ...metricCells(sumRows([r])),
|
||||
])));
|
||||
$('#daily tfoot').replaceChildren(tr([{ text: '合计' }, { text: f || '全部包' }, { text: '' }, ...metricCells(sumRows(rows))]));
|
||||
}
|
||||
|
||||
let S, DATA;
|
||||
@@ -485,12 +536,14 @@ fetch('data.json', { cache: 'no-store' })
|
||||
$('#meta').replaceChildren(
|
||||
h('span', '', `全账户 ${S.pkgs.length} 个包`),
|
||||
h('span', '', `数据区间 ${d.range.start} ~ ${d.range.end}`),
|
||||
h('span', '', `表格更新于 ${updated}`),
|
||||
h('span', '', `${shortDate(d.sheet_until)} 及以前来自表格,${shortDate(d.midas_from)} 起来自 MIDAS`),
|
||||
h('span', '', `更新于 ${updated}`),
|
||||
);
|
||||
$('#spend-desc').textContent = `按包堆叠,悬停查看当天每个包的数据;竖线右边(${shortDate(d.midas_from)} 起)来自 MIDAS`;
|
||||
renderTiles(S, d);
|
||||
renderTables(S, d);
|
||||
drawCharts();
|
||||
$('#foot').textContent = `数据来源:${d.source}。消耗按表格口径,成本 = 消耗 ÷ 首充,余额按到账金额计算。每天 11:00 检查表格,有更新就自动刷新。`;
|
||||
$('#foot').textContent = `数据来源:${d.sheet_until} 及以前是 tecdo_数据.xlsx(计费口径);${d.midas_from} 起是 MIDAS 账户 C00000196_TECDO HONG KONG LIMITED 的 campaign 消耗(UTC+0,截至前一天),MIDAS 没有注册和首充。充值来自 钛动预付款.xlsx,余额按到账金额计算。每天 11:00 自动刷新。`;
|
||||
})
|
||||
.catch(e => {
|
||||
$('#meta').textContent = '';
|
||||
|
||||
Reference in New Issue
Block a user