feat: add orders info tab (Campaign list + new trials integrated)
This commit is contained in:
+190
-2
@@ -236,6 +236,86 @@
|
|||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 14px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 28px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #667eea;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.tab-panel { display: none; }
|
||||||
|
.tab-panel.active { display: block; }
|
||||||
|
|
||||||
|
/* Order status badges */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.status-active {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
border: 1px solid #86efac;
|
||||||
|
}
|
||||||
|
.status-pause {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fcd34d;
|
||||||
|
}
|
||||||
|
.pay-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
.channel-chip {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #4338ca;
|
||||||
|
border: 1px solid #c7d2fe;
|
||||||
|
}
|
||||||
|
.no-channel {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.order-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.charts-grid { grid-template-columns: 1fr; }
|
.charts-grid { grid-template-columns: 1fr; }
|
||||||
.summary-row { grid-template-columns: repeat(2, 1fr); }
|
.summary-row { grid-template-columns: repeat(2, 1fr); }
|
||||||
@@ -254,6 +334,12 @@
|
|||||||
<p>数据概览与趋势分析</p>
|
<p>数据概览与趋势分析</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="performance">投放数据</button>
|
||||||
|
<button class="tab-btn" data-tab="orders">订单信息</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel active" id="tab-performance">
|
||||||
<div class="summary-row" id="summaryRow"></div>
|
<div class="summary-row" id="summaryRow"></div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@@ -313,6 +399,34 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- /tab-performance -->
|
||||||
|
|
||||||
|
<div class="tab-panel" id="tab-orders">
|
||||||
|
<div class="order-stats" id="orderStats"></div>
|
||||||
|
<div class="table-section">
|
||||||
|
<h2>投放订单明细(Campaign list + 新一轮尝试链接整合)</h2>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="orderTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Pkg / 包名</th>
|
||||||
|
<th>平台</th>
|
||||||
|
<th>地区</th>
|
||||||
|
<th>付费类型</th>
|
||||||
|
<th>单价(USD)</th>
|
||||||
|
<th>日预算</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>MMP</th>
|
||||||
|
<th>已铺投放渠道</th>
|
||||||
|
<th>数据来源</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /tab-orders -->
|
||||||
|
|
||||||
<div class="updated-time" id="updatedTime"></div>
|
<div class="updated-time" id="updatedTime"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,12 +438,17 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
let rawData = [];
|
let rawData = [];
|
||||||
|
let orderData = [];
|
||||||
let charts = {};
|
let charts = {};
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('data.json?' + Date.now());
|
const [dResp, oResp] = await Promise.all([
|
||||||
rawData = await resp.json();
|
fetch('data.json?' + Date.now()),
|
||||||
|
fetch('orders.json?' + Date.now())
|
||||||
|
]);
|
||||||
|
rawData = await dResp.json();
|
||||||
|
orderData = await oResp.json();
|
||||||
init();
|
init();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('Failed to load data:', e);
|
console.error('Failed to load data:', e);
|
||||||
@@ -339,11 +458,80 @@
|
|||||||
function init() {
|
function init() {
|
||||||
populateFilters();
|
populateFilters();
|
||||||
renderAll();
|
renderAll();
|
||||||
|
renderOrders();
|
||||||
|
setupTabs();
|
||||||
document.getElementById('pkgFilter').addEventListener('change', renderAll);
|
document.getElementById('pkgFilter').addEventListener('change', renderAll);
|
||||||
document.getElementById('dateFilter').addEventListener('change', renderAll);
|
document.getElementById('dateFilter').addEventListener('change', renderAll);
|
||||||
document.getElementById('updatedTime').textContent = '最后刷新:' + new Date().toLocaleString('zh-CN');
|
document.getElementById('updatedTime').textContent = '最后刷新:' + new Date().toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupTabs() {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOrders() {
|
||||||
|
// Stats
|
||||||
|
const total = orderData.length;
|
||||||
|
const active = orderData.filter(o => o.status === 'Active').length;
|
||||||
|
const products = new Set(orderData.map(o => o.product)).size;
|
||||||
|
const allChannels = new Set();
|
||||||
|
orderData.forEach(o => (o.channels || []).forEach(c => allChannels.add(c)));
|
||||||
|
|
||||||
|
document.getElementById('orderStats').innerHTML = `
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">订单条目</div>
|
||||||
|
<div class="value">${total}</div>
|
||||||
|
<div class="sub">${products} 个独立产品</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">在投中 (Active)</div>
|
||||||
|
<div class="value" style="color:#16a34a">${active}</div>
|
||||||
|
<div class="sub">${total - active} 个 Pause</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">已铺投放渠道</div>
|
||||||
|
<div class="value">${allChannels.size}</div>
|
||||||
|
<div class="sub">媒体/代投渠道</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">MMP 归因</div>
|
||||||
|
<div class="value" style="font-size:20px">AppsFlyer</div>
|
||||||
|
<div class="sub">全线统一</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Table
|
||||||
|
const tbody = document.querySelector('#orderTable tbody');
|
||||||
|
tbody.innerHTML = orderData.map(o => {
|
||||||
|
const statusClass = o.status === 'Active' ? 'status-active' : 'status-pause';
|
||||||
|
const channels = (o.channels && o.channels.length)
|
||||||
|
? o.channels.map(c => `<span class="channel-chip">${c}</span>`).join('')
|
||||||
|
: '<span class="no-channel">—</span>';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:600">${o.product}</td>
|
||||||
|
<td class="num-cell" style="font-size:11px">${o.pkg}</td>
|
||||||
|
<td>${o.platform}</td>
|
||||||
|
<td>${o.geo}</td>
|
||||||
|
<td><span class="pay-badge">${o.pay_mode}</span></td>
|
||||||
|
<td class="num-cell">${o.price}</td>
|
||||||
|
<td class="num-cell">${o.budget}</td>
|
||||||
|
<td><span class="status-badge ${statusClass}">${o.status}</span></td>
|
||||||
|
<td>${o.mmp}</td>
|
||||||
|
<td style="max-width:260px;white-space:normal">${channels}</td>
|
||||||
|
<td style="font-size:11px;color:#94a3b8">${o.source}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function populateFilters() {
|
function populateFilters() {
|
||||||
const pkgs = [...new Set(rawData.map(d => d.pkg))];
|
const pkgs = [...new Set(rawData.map(d => d.pkg))];
|
||||||
const sel = document.getElementById('pkgFilter');
|
const sel = document.getElementById('pkgFilter');
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"product":"Honor of Kings","pkg":"com.levelinfinite.sgameGlobal","platform":"Android","geo":"IN / PH","pay_mode":"CPI","price":"0.15","budget":"$100/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"Campaign list"},
|
||||||
|
{"product":"IGG (Lords Mobile)","pkg":"com.igg.android.lordsmobile","platform":"Android","geo":"RU","pay_mode":"CPI","price":"0.5","budget":"$100/day","status":"Active","mmp":"AppsFlyer","channels":["Eagllwin-pubz","MediaGo","传音","PropellerAds代投"],"source":"Campaign list + 新一轮尝试"},
|
||||||
|
{"product":"Shahid","pkg":"net.mbc.shahid","platform":"Android","geo":"SA","pay_mode":"CPA","price":"5.4","budget":"$100/day","status":"Active","mmp":"AppsFlyer","channels":["Eagllwin-pubz","MediaGo","传音","PropellerAds代投","Smartyads","Adsterra"],"source":"Campaign list + 新一轮尝试"},
|
||||||
|
{"product":"灵犀互动","pkg":"com.sialiagames.xczyw.gp.tw","platform":"Android","geo":"TW","pay_mode":"CPI","price":"5","budget":"$150/day","status":"Active","mmp":"AppsFlyer","channels":["Eagllwin-pubz","MediaGo","Smartyads","Adsterra","RollerAds","PropellerAds代投"],"source":"Campaign list + 新一轮尝试"},
|
||||||
|
{"product":"灵犀互动","pkg":"com.qookkagames.xczyw.gp.hk","platform":"Android","geo":"HK","pay_mode":"CPI","price":"5","budget":"$150/day","status":"Active","mmp":"AppsFlyer","channels":["Eagllwin-pubz","MediaGo","Smartyads","Adsterra","RollerAds","PropellerAds代投"],"source":"Campaign list + 新一轮尝试"},
|
||||||
|
{"product":"Water Sort! Color Sort Puzzle","pkg":"water.sort.color.puzzle.offline.games","platform":"Android","geo":"US","pay_mode":"CPI","price":"-","budget":"$100/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"Campaign list"},
|
||||||
|
{"product":"Magalu_BR","pkg":"com.luizalabs.mlapp / id891130066","platform":"Android + iOS","geo":"BR","pay_mode":"CPI","price":"0.12","budget":"$36/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"Campaign list"},
|
||||||
|
{"product":"Netshoes_BR","pkg":"br.com.netshoes.app / id886443637","platform":"Android + iOS","geo":"BR","pay_mode":"CPI","price":"0.12","budget":"$100/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"Campaign list"},
|
||||||
|
{"product":"AUTODOC","pkg":"de.autodoc.gmbh / id1014949597","platform":"Android + iOS","geo":"26个欧洲国家","pay_mode":"CPA","price":"20","budget":"$100/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"Campaign list"},
|
||||||
|
{"product":"Zombie","pkg":"com.ddup.zombiewaves.zw","platform":"Android","geo":"US","pay_mode":"CPI","price":"$1-2","budget":"$100/day","status":"Pause","mmp":"AppsFlyer","channels":["Eagllwin-pubz","Newsbreak","Mintegral","Bigo"],"source":"新一轮尝试"},
|
||||||
|
{"product":"Xena","pkg":"com.xparty.androidapp.IQ","platform":"Android","geo":"IQ","pay_mode":"CPI","price":"0.2","budget":"$30/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"新一轮尝试"},
|
||||||
|
{"product":"Xena","pkg":"com.xparty.androidapp.EG","platform":"Android","geo":"EG","pay_mode":"CPI","price":"0.2","budget":"$30/day","status":"Pause","mmp":"AppsFlyer","channels":[],"source":"新一轮尝试"}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user