Add interactive revenue table controls
This commit is contained in:
+128
-33
@@ -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(); });
|
||||||
|
|||||||
Reference in New Issue
Block a user