Add interactive revenue table controls
This commit is contained in:
+128
-33
@@ -247,7 +247,15 @@
|
||||
<script>
|
||||
(() => {
|
||||
'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 compactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
|
||||
const dateFormatter = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' });
|
||||
@@ -268,10 +276,11 @@
|
||||
}
|
||||
|
||||
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) => {
|
||||
const platformMatches = state.platform === '__all__' || row.attr_platform === state.platform;
|
||||
const packageMatches = !query || text(row.pkg_name).toLowerCase().includes(query);
|
||||
const platformMatches = !platformQuery || text(row.attr_platform).toLowerCase().includes(platformQuery);
|
||||
const packageMatches = !packageQuery || text(row.pkg_name).toLowerCase().includes(packageQuery);
|
||||
return platformMatches && packageMatches;
|
||||
});
|
||||
}
|
||||
@@ -288,8 +297,28 @@
|
||||
const values = dates.map((date) => entry.values.get(date) || 0);
|
||||
const latest = values.length ? values[values.length - 1] : 0;
|
||||
const previous = values.length > 1 ? values[values.length - 2] : null;
|
||||
return { ...entry, values, latest, difference: previous === null ? null : latest - previous };
|
||||
}).sort((a, b) => b.latest - a.latest || a.name.localeCompare(b.name));
|
||||
const fourthPrevious = values.length >= 4 ? values[values.length - 4] : null;
|
||||
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) {
|
||||
@@ -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) {
|
||||
const head = $('pivotHead');
|
||||
const body = $('pivotBody');
|
||||
@@ -361,14 +443,13 @@
|
||||
const packageHeader = document.createElement('th');
|
||||
packageHeader.textContent = 'Pkg name';
|
||||
headerRow.append(packageHeader);
|
||||
dates.forEach((date) => {
|
||||
const cell = document.createElement('th');
|
||||
cell.textContent = date;
|
||||
headerRow.append(cell);
|
||||
});
|
||||
dates.forEach((date) => headerRow.append(createDateHeader(date)));
|
||||
const differenceHeader = document.createElement('th');
|
||||
differenceHeader.textContent = 'T-1 − T-2';
|
||||
headerRow.append(differenceHeader);
|
||||
const differenceT4Header = document.createElement('th');
|
||||
differenceT4Header.textContent = 'T-1 − T-4';
|
||||
headerRow.append(differenceT4Header);
|
||||
head.append(headerRow);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(pivot.length / state.pageSize));
|
||||
@@ -379,7 +460,7 @@
|
||||
const empty = document.createElement('tr');
|
||||
const cell = document.createElement('td');
|
||||
cell.className = 'empty';
|
||||
cell.colSpan = dates.length + 2;
|
||||
cell.colSpan = dates.length + 3;
|
||||
cell.textContent = '没有匹配当前筛选条件的包名';
|
||||
empty.append(cell);
|
||||
body.append(empty);
|
||||
@@ -394,14 +475,7 @@
|
||||
cell.textContent = money(value);
|
||||
row.append(cell);
|
||||
});
|
||||
const difference = document.createElement('td');
|
||||
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);
|
||||
row.append(createDifferenceCell(item.difference), createDifferenceCell(item.differenceT4));
|
||||
body.append(row);
|
||||
});
|
||||
|
||||
@@ -413,7 +487,7 @@
|
||||
function render() {
|
||||
const dates = dataDates();
|
||||
const rows = selectedRows();
|
||||
const pivot = buildPivot(rows, dates);
|
||||
const pivot = sortPivot(buildPivot(rows, dates), dates);
|
||||
renderSummary(rows, dates, pivot);
|
||||
renderChart(rows, dates);
|
||||
renderTable(rows, dates, pivot);
|
||||
@@ -421,17 +495,29 @@
|
||||
|
||||
function populatePlatforms() {
|
||||
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();
|
||||
select.replaceChildren();
|
||||
const all = document.createElement('option');
|
||||
all.value = '__all__';
|
||||
all.textContent = '全部平台';
|
||||
select.append(all);
|
||||
const input = document.createElement('input');
|
||||
input.id = 'platformFilter';
|
||||
input.type = 'search';
|
||||
input.setAttribute('list', 'platformOptions');
|
||||
input.setAttribute('aria-label', '模糊搜索归因平台');
|
||||
input.placeholder = '输入归因平台,例如 appnextads';
|
||||
input.autocomplete = 'off';
|
||||
const options = document.createElement('datalist');
|
||||
options.id = 'platformOptions';
|
||||
platforms.forEach((platform) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = platform;
|
||||
option.textContent = platform;
|
||||
select.append(option);
|
||||
options.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 dateLabel = dates.length ? dates.join(' ~ ') : '无可用日期';
|
||||
$('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() {
|
||||
@@ -468,11 +554,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
$('platformFilter').addEventListener('change', (event) => { state.platform = event.target.value; state.page = 1; render(); });
|
||||
$('packageSearch').addEventListener('input', (event) => { state.search = event.target.value; state.page = 1; render(); });
|
||||
$('packageSearch').addEventListener('input', (event) => {
|
||||
state.packageQuery = event.target.value;
|
||||
state.page = 1;
|
||||
render();
|
||||
});
|
||||
$('clearFilters').addEventListener('click', () => {
|
||||
state.platform = '__all__'; state.search = ''; state.page = 1;
|
||||
$('platformFilter').value = '__all__'; $('packageSearch').value = ''; render();
|
||||
state.platformQuery = '';
|
||||
state.packageQuery = '';
|
||||
state.sortKey = 'total';
|
||||
state.sortDirection = 'desc';
|
||||
state.page = 1;
|
||||
$('platformFilter').value = '';
|
||||
$('packageSearch').value = '';
|
||||
render();
|
||||
});
|
||||
$('previousPage').addEventListener('click', () => { state.page -= 1; render(); });
|
||||
$('nextPage').addEventListener('click', () => { state.page += 1; render(); });
|
||||
|
||||
Reference in New Issue
Block a user