feat: add MTG 10/13 performance data
This commit is contained in:
+161
@@ -456,6 +456,166 @@
|
||||
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const mtgState = { filtered: [], page: 1 };
|
||||
|
||||
function money(value) {
|
||||
return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function mtgMatchStatus(record) {
|
||||
if (!record.order_match) return { key: 'unmatched', label: '未关联订单' };
|
||||
if (record.missing_target) return { key: 'missing', label: '待搬单' };
|
||||
if (record.target_present) return { key: 'covered', label: '已覆盖' };
|
||||
return { key: 'matched', label: '已关联' };
|
||||
}
|
||||
|
||||
function renderMtg1013() {
|
||||
const data = state.data.mtg1013;
|
||||
if (!data || !Array.isArray(data.records)) return;
|
||||
const panel = el('section', 'panel workspace');
|
||||
panel.id = 'mtg1013Panel';
|
||||
const header = el('div', 'workspace-head');
|
||||
const head = el('div', 'panel-head');
|
||||
const title = el('div');
|
||||
title.append(
|
||||
el('h2', '', 'MTG 10/13 投放表现'),
|
||||
el('div', 'panel-desc', `数据源:${data.meta.source} · ${nf.format(data.meta.rows)} 条 Adping 明细 · 按 Pkg + 国家关联订单`)
|
||||
);
|
||||
head.append(title);
|
||||
|
||||
const summary = el('section', 'kpis');
|
||||
summary.append(
|
||||
kpi('Valid Clicks', nf.format(data.meta.valid_clicks), `${nf.format(data.meta.rows)} 条明细`),
|
||||
kpi('Postback Installs', nf.format(data.meta.installs), `安装率 ${data.meta.install_rate === null ? '-' : percent(data.meta.install_rate)}`),
|
||||
kpi('Revenue', money(data.meta.revenue), `利润 ${money(data.meta.profit)}`, 'success'),
|
||||
kpi('Cost', money(data.meta.cost), `${nf.format(data.meta.matched_order_rows)} 条已关联订单`),
|
||||
kpi('ROAS', data.meta.roas === null ? '-' : percent(data.meta.roas), `${nf.format(data.meta.unmatched_order_rows)} 条未关联订单`)
|
||||
);
|
||||
summary.style.marginBottom = '16px';
|
||||
|
||||
const filters = el('div', 'filters');
|
||||
const search = el('input', 'control');
|
||||
search.id = 'mtgSearch'; search.type = 'search'; search.placeholder = '搜索 Adping ID 或 Pkg'; search.autocomplete = 'off';
|
||||
const country = selectControl('mtgCountry', '全部国家');
|
||||
[...new Set(data.records.map(row => row.country))].sort().forEach(value => country.append(new Option(value, value)));
|
||||
const match = selectControl('mtgMatch', '');
|
||||
[['all','全部关联状态'], ['matched','已关联订单'], ['missing','关联且待搬单'], ['covered','关联且已覆盖'], ['unmatched','未关联订单']]
|
||||
.forEach(([value,label]) => match.append(new Option(label, value)));
|
||||
const exportButton = el('button', 'button', '导出 MTG 数据');
|
||||
exportButton.id = 'mtgExport'; exportButton.type = 'button';
|
||||
filters.append(search, country, match, exportButton);
|
||||
header.append(head, summary, filters);
|
||||
|
||||
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
|
||||
const footer = document.querySelector('footer');
|
||||
document.getElementById('app').insertBefore(panel, footer);
|
||||
|
||||
search.addEventListener('input', () => { mtgState.page = 1; applyMtgFilters(); });
|
||||
country.addEventListener('change', () => { mtgState.page = 1; applyMtgFilters(); });
|
||||
match.addEventListener('change', () => { mtgState.page = 1; applyMtgFilters(); });
|
||||
exportButton.addEventListener('click', exportMtgCsv);
|
||||
applyMtgFilters();
|
||||
}
|
||||
|
||||
function applyMtgFilters() {
|
||||
const query = document.getElementById('mtgSearch').value.trim().toLocaleLowerCase();
|
||||
const country = document.getElementById('mtgCountry').value;
|
||||
const match = document.getElementById('mtgMatch').value;
|
||||
mtgState.filtered = state.data.mtg1013.records.filter(row => {
|
||||
const status = mtgMatchStatus(row).key;
|
||||
const matched = match === 'all'
|
||||
|| status === match
|
||||
|| (match === 'matched' && row.order_match);
|
||||
return (!query || `${row.adpingid} ${row.pkg}`.toLocaleLowerCase().includes(query))
|
||||
&& (country === 'all' || row.country === country)
|
||||
&& matched;
|
||||
});
|
||||
renderMtgResults();
|
||||
}
|
||||
|
||||
function renderMtgResults() {
|
||||
const panel = document.getElementById('mtg1013Panel');
|
||||
const totalPages = Math.max(1, Math.ceil(mtgState.filtered.length / PAGE_SIZE));
|
||||
mtgState.page = Math.min(mtgState.page, totalPages);
|
||||
const start = (mtgState.page - 1) * PAGE_SIZE;
|
||||
const rows = mtgState.filtered.slice(start, start + PAGE_SIZE);
|
||||
const meta = panel.querySelector('.result-meta');
|
||||
meta.textContent = '';
|
||||
meta.append(
|
||||
el('span', '', `筛选结果 ${nf.format(mtgState.filtered.length)} 条`),
|
||||
el('span', '', `第 ${mtgState.page} / ${totalPages} 页`)
|
||||
);
|
||||
|
||||
const wrap = panel.querySelector('.table-wrap');
|
||||
wrap.textContent = '';
|
||||
const table = el('table');
|
||||
const thead = el('thead'); const header = el('tr');
|
||||
['Adping ID', 'Pkg', '国家', 'Valid Clicks', 'Postback Installs', '安装率', 'Revenue', 'Cost', '利润', 'ROAS', '订单关联', 'TaurusxDSP 状态']
|
||||
.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', '没有符合当前条件的 MTG 记录'); cell.colSpan = 12;
|
||||
const row = el('tr'); row.append(cell); tbody.append(row);
|
||||
}
|
||||
rows.forEach(record => {
|
||||
const row = el('tr');
|
||||
const country = el('td'); country.append(el('span', 'country-pill', record.country));
|
||||
const match = mtgMatchStatus(record);
|
||||
const orderStatus = el('td');
|
||||
orderStatus.append(el('span', `badge ${record.order_match ? 'covered' : 'target-only'}`, record.order_match ? (record.adpingid_match ? 'ID 已匹配' : '仅 Pkg_国家匹配') : '未关联'));
|
||||
const targetStatus = el('td');
|
||||
targetStatus.append(el('span', `badge ${match.key === 'unmatched' ? 'target-only' : match.key}`, match.label));
|
||||
row.append(
|
||||
el('td', 'pkg', record.adpingid),
|
||||
el('td', 'pkg', record.pkg),
|
||||
country,
|
||||
el('td', '', nf.format(record.valid_clicks)),
|
||||
el('td', '', nf.format(record.installs)),
|
||||
el('td', '', record.install_rate === null ? '-' : percent(record.install_rate)),
|
||||
el('td', '', money(record.revenue)),
|
||||
el('td', '', money(record.cost)),
|
||||
el('td', '', money(record.profit)),
|
||||
el('td', '', record.roas === null ? '-' : percent(record.roas)),
|
||||
orderStatus,
|
||||
targetStatus
|
||||
);
|
||||
tbody.append(row);
|
||||
});
|
||||
table.append(tbody); wrap.append(table);
|
||||
|
||||
const pager = panel.querySelector('.pagination');
|
||||
pager.textContent = '';
|
||||
const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = mtgState.page <= 1;
|
||||
const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = mtgState.page >= totalPages;
|
||||
prev.addEventListener('click', () => { mtgState.page -= 1; renderMtgResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
|
||||
next.addEventListener('click', () => { mtgState.page += 1; renderMtgResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
|
||||
pager.append(prev, el('span', 'panel-desc', `${mtgState.page} / ${totalPages}`), next);
|
||||
}
|
||||
|
||||
function exportMtgCsv() {
|
||||
const headers = ['Adping ID', 'Pkg', '国家', 'Valid Clicks', 'Postback Installs', '安装率', 'Revenue', 'Cost', '利润', 'ROAS', '订单关联', 'TaurusxDSP状态'];
|
||||
const rows = [headers, ...mtgState.filtered.map(record => [
|
||||
record.adpingid,
|
||||
record.pkg,
|
||||
record.country,
|
||||
record.valid_clicks,
|
||||
record.installs,
|
||||
record.install_rate === null ? '' : record.install_rate,
|
||||
record.revenue,
|
||||
record.cost,
|
||||
record.profit,
|
||||
record.roas === null ? '' : record.roas,
|
||||
record.order_match ? (record.adpingid_match ? 'ID已匹配' : '仅Pkg_国家匹配') : '未关联',
|
||||
mtgMatchStatus(record).label
|
||||
])];
|
||||
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 link = document.createElement('a');
|
||||
link.href = url; link.download = `MTG1013_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
fetch(`data.json?v=${Date.now()}`, { cache: 'no-store' })
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
@@ -465,6 +625,7 @@
|
||||
if (!data.meta || !Array.isArray(data.records)) throw new Error('data.json 格式不正确');
|
||||
state.data = data;
|
||||
renderShell();
|
||||
renderMtg1013();
|
||||
})
|
||||
.catch(error => {
|
||||
const app = document.getElementById('app');
|
||||
|
||||
Reference in New Issue
Block a user