Add interactive revenue table controls

This commit is contained in:
Kiro
2026-10-07 16:58:10 +08:00
parent b1f77cd952
commit 23d41d0896
+128 -33
View File
@@ -247,7 +247,15 @@
<script> <script>
(() => { (() => {
'use strict'; 'use strict';
const state = { data: null, platform: '__all__', search: '', page: 1, pageSize: 50 }; const state = {
data: null,
platformQuery: '',
packageQuery: '',
sortKey: 'total',
sortDirection: 'desc',
page: 1,
pageSize: 50,
};
const numberFormatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const numberFormatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const compactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }); const compactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
const dateFormatter = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }); const dateFormatter = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' });
@@ -268,10 +276,11 @@
} }
function selectedRows() { function selectedRows() {
const query = state.search.trim().toLowerCase(); const platformQuery = state.platformQuery.trim().toLowerCase();
const packageQuery = state.packageQuery.trim().toLowerCase();
return (state.data?.rows || []).filter((row) => { return (state.data?.rows || []).filter((row) => {
const platformMatches = state.platform === '__all__' || row.attr_platform === state.platform; const platformMatches = !platformQuery || text(row.attr_platform).toLowerCase().includes(platformQuery);
const packageMatches = !query || text(row.pkg_name).toLowerCase().includes(query); const packageMatches = !packageQuery || text(row.pkg_name).toLowerCase().includes(packageQuery);
return platformMatches && packageMatches; return platformMatches && packageMatches;
}); });
} }
@@ -288,8 +297,28 @@
const values = dates.map((date) => entry.values.get(date) || 0); const values = dates.map((date) => entry.values.get(date) || 0);
const latest = values.length ? values[values.length - 1] : 0; const latest = values.length ? values[values.length - 1] : 0;
const previous = values.length > 1 ? values[values.length - 2] : null; const previous = values.length > 1 ? values[values.length - 2] : null;
return { ...entry, values, latest, difference: previous === null ? null : latest - previous }; const fourthPrevious = values.length >= 4 ? values[values.length - 4] : null;
}).sort((a, b) => b.latest - a.latest || a.name.localeCompare(b.name)); const total = values.reduce((sum, value) => sum + value, 0);
return {
...entry,
values,
total,
latest,
difference: previous === null ? null : latest - previous,
differenceT4: fourthPrevious === null ? null : latest - fourthPrevious,
};
});
}
function sortPivot(pivot, dates) {
const dateIndex = dates.indexOf(state.sortKey);
const useTotal = state.sortKey === 'total' || dateIndex < 0;
const direction = state.sortDirection === 'asc' ? 1 : -1;
return [...pivot].sort((left, right) => {
const leftValue = useTotal ? left.total : left.values[dateIndex];
const rightValue = useTotal ? right.total : right.values[dateIndex];
return direction * (leftValue - rightValue) || left.name.localeCompare(right.name);
});
} }
function totalsForDates(rows, dates) { function totalsForDates(rows, dates) {
@@ -352,6 +381,59 @@
}); });
} }
function createSortButton(date, direction) {
const isActive = state.sortKey === date && state.sortDirection === direction;
const button = document.createElement('button');
button.type = 'button';
button.textContent = direction === 'asc' ? '↑' : '↓';
button.title = `按 ${date} 收入${direction === 'asc' ? '升序' : '降序'}排列`;
button.setAttribute('aria-label', button.title);
button.setAttribute('aria-pressed', String(isActive));
Object.assign(button.style, {
width: '18px',
minWidth: '18px',
height: '18px',
padding: '0',
border: `1px solid ${isActive ? '#2563eb' : '#c8d4e4'}`,
borderRadius: '4px',
color: isActive ? '#fff' : '#58708e',
background: isActive ? '#2563eb' : '#fff',
fontSize: '11px',
fontWeight: '800',
lineHeight: '16px',
});
button.addEventListener('click', () => {
state.sortKey = date;
state.sortDirection = direction;
state.page = 1;
render();
});
return button;
}
function createDateHeader(date) {
const cell = document.createElement('th');
cell.style.minWidth = '138px';
const content = document.createElement('span');
Object.assign(content.style, { display: 'inline-flex', alignItems: 'center', gap: '4px' });
const label = document.createElement('span');
label.textContent = date;
content.append(label, createSortButton(date, 'asc'), createSortButton(date, 'desc'));
cell.append(content);
return cell;
}
function createDifferenceCell(value) {
const cell = document.createElement('td');
if (value === null) {
cell.textContent = '—';
} else {
cell.textContent = `${value > 0 ? '+' : ''}${money(value)}`;
cell.className = `diff ${value > 100 ? 'positive' : value < 100 ? 'negative' : 'neutral'}`;
}
return cell;
}
function renderTable(rows, dates, pivot) { function renderTable(rows, dates, pivot) {
const head = $('pivotHead'); const head = $('pivotHead');
const body = $('pivotBody'); const body = $('pivotBody');
@@ -361,14 +443,13 @@
const packageHeader = document.createElement('th'); const packageHeader = document.createElement('th');
packageHeader.textContent = 'Pkg name'; packageHeader.textContent = 'Pkg name';
headerRow.append(packageHeader); headerRow.append(packageHeader);
dates.forEach((date) => { dates.forEach((date) => headerRow.append(createDateHeader(date)));
const cell = document.createElement('th');
cell.textContent = date;
headerRow.append(cell);
});
const differenceHeader = document.createElement('th'); const differenceHeader = document.createElement('th');
differenceHeader.textContent = 'T-1 − T-2'; differenceHeader.textContent = 'T-1 − T-2';
headerRow.append(differenceHeader); headerRow.append(differenceHeader);
const differenceT4Header = document.createElement('th');
differenceT4Header.textContent = 'T-1 − T-4';
headerRow.append(differenceT4Header);
head.append(headerRow); head.append(headerRow);
const pageCount = Math.max(1, Math.ceil(pivot.length / state.pageSize)); const pageCount = Math.max(1, Math.ceil(pivot.length / state.pageSize));
@@ -379,7 +460,7 @@
const empty = document.createElement('tr'); const empty = document.createElement('tr');
const cell = document.createElement('td'); const cell = document.createElement('td');
cell.className = 'empty'; cell.className = 'empty';
cell.colSpan = dates.length + 2; cell.colSpan = dates.length + 3;
cell.textContent = '没有匹配当前筛选条件的包名'; cell.textContent = '没有匹配当前筛选条件的包名';
empty.append(cell); empty.append(cell);
body.append(empty); body.append(empty);
@@ -394,14 +475,7 @@
cell.textContent = money(value); cell.textContent = money(value);
row.append(cell); row.append(cell);
}); });
const difference = document.createElement('td'); row.append(createDifferenceCell(item.difference), createDifferenceCell(item.differenceT4));
if (item.difference === null) {
difference.textContent = '—';
} else {
difference.textContent = `${item.difference > 0 ? '+' : ''}${money(item.difference)}`;
difference.className = `diff ${item.difference > 100 ? 'positive' : item.difference < 100 ? 'negative' : 'neutral'}`;
}
row.append(difference);
body.append(row); body.append(row);
}); });
@@ -413,7 +487,7 @@
function render() { function render() {
const dates = dataDates(); const dates = dataDates();
const rows = selectedRows(); const rows = selectedRows();
const pivot = buildPivot(rows, dates); const pivot = sortPivot(buildPivot(rows, dates), dates);
renderSummary(rows, dates, pivot); renderSummary(rows, dates, pivot);
renderChart(rows, dates); renderChart(rows, dates);
renderTable(rows, dates, pivot); renderTable(rows, dates, pivot);
@@ -421,17 +495,29 @@
function populatePlatforms() { function populatePlatforms() {
const select = $('platformFilter'); const select = $('platformFilter');
if (!select || select.tagName === 'INPUT') return;
const field = select.parentElement;
const platforms = Array.isArray(state.data?.platforms) ? state.data.platforms : [...new Set((state.data?.rows || []).map((row) => row.attr_platform))].sort(); const platforms = Array.isArray(state.data?.platforms) ? state.data.platforms : [...new Set((state.data?.rows || []).map((row) => row.attr_platform))].sort();
select.replaceChildren(); const input = document.createElement('input');
const all = document.createElement('option'); input.id = 'platformFilter';
all.value = '__all__'; input.type = 'search';
all.textContent = '全部平台'; input.setAttribute('list', 'platformOptions');
select.append(all); input.setAttribute('aria-label', '模糊搜索归因平台');
input.placeholder = '输入归因平台,例如 appnextads';
input.autocomplete = 'off';
const options = document.createElement('datalist');
options.id = 'platformOptions';
platforms.forEach((platform) => { platforms.forEach((platform) => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = platform; option.value = platform;
option.textContent = platform; options.append(option);
select.append(option); });
select.replaceWith(input);
field.append(options);
input.addEventListener('input', (event) => {
state.platformQuery = event.target.value;
state.page = 1;
render();
}); });
} }
@@ -441,7 +527,7 @@
const dates = dataDates(); const dates = dataDates();
const dateLabel = dates.length ? dates.join(' ~ ') : '无可用日期'; const dateLabel = dates.length ? dates.join(' ~ ') : '无可用日期';
$('dataStatus').textContent = `已更新 · ${generated}`; $('dataStatus').textContent = `已更新 · ${generated}`;
$('dataNote').textContent = `查询范围:${source.timeRange || '—'} | 数据日期:${dateLabel} | 已返回 ${Number(state.data?.rawRowCount || 0).toLocaleString('en-US')} 行 | 行数上限:${source.rowLimit || 5000}`; $('dataNote').textContent = `查询范围:${source.timeRange || '—'} | 数据日期:${dateLabel} | 已返回 ${Number(state.data?.rawRowCount || 0).toLocaleString('en-US')} 行 | 行数上限:${source.rowLimit || 5000} | 默认按包名总收入降序`;
} }
async function load() { async function load() {
@@ -468,11 +554,20 @@
} }
} }
$('platformFilter').addEventListener('change', (event) => { state.platform = event.target.value; state.page = 1; render(); }); $('packageSearch').addEventListener('input', (event) => {
$('packageSearch').addEventListener('input', (event) => { state.search = event.target.value; state.page = 1; render(); }); state.packageQuery = event.target.value;
state.page = 1;
render();
});
$('clearFilters').addEventListener('click', () => { $('clearFilters').addEventListener('click', () => {
state.platform = '__all__'; state.search = ''; state.page = 1; state.platformQuery = '';
$('platformFilter').value = '__all__'; $('packageSearch').value = ''; render(); state.packageQuery = '';
state.sortKey = 'total';
state.sortDirection = 'desc';
state.page = 1;
$('platformFilter').value = '';
$('packageSearch').value = '';
render();
}); });
$('previousPage').addEventListener('click', () => { state.page -= 1; render(); }); $('previousPage').addEventListener('click', () => { state.page -= 1; render(); });
$('nextPage').addEventListener('click', () => { state.page += 1; render(); }); $('nextPage').addEventListener('click', () => { state.page += 1; render(); });