CDK兑换修复:maxlength扩展+日期搜索+商品分类+同IP折叠+数据库锁修复

This commit is contained in:
ypsdf1
2026-06-20 14:02:41 +08:00
parent 885e70f0f5
commit d7a986894c
6 changed files with 793 additions and 74 deletions
+145 -22
View File
@@ -113,7 +113,7 @@ if ('serviceWorker' in navigator) {
}
const A = 'api/admin.php';
const _BUILD_TS = 1750220000; // 版本号,用于缓存失效
const _BUILD_TS = 1781928829; // 版本号,用于缓存失效
console.log('[INIT] Admin panel loaded, build:', _BUILD_TS);
// ★ 自检:验证新代码是否加载
@@ -232,15 +232,45 @@ async function loadDashboard(el) {
let onlineHtml = '';
if (onlineR.success && onlineR.data && onlineR.data.length > 0) {
const players = onlineR.data;
// 同IP段折叠:按/24子网分组
const subnetGroups = {};
const subnetOrder = [];
players.forEach(p => {
const ip = p.ip_address || '-';
let subnet = ip;
if (ip !== '-') {
const parts = ip.split('.');
if (parts.length === 4) {
subnet = parts[0] + '.' + parts[1] + '.' + parts[2];
}
}
if (!subnetGroups[subnet]) {
subnetGroups[subnet] = [];
subnetOrder.push(subnet);
}
subnetGroups[subnet].push(p);
});
// 构建显示列表:每组只显示第一个,后面标注折叠数量
const displayRows = [];
subnetOrder.forEach(subnet => {
const group = subnetGroups[subnet];
const first = group[0];
const hiddenCount = group.length - 1;
displayRows.push({player: first, hiddenCount});
});
onlineHtml = `
<div class="card">
<h2>实时在线玩家 <span style="color:var(--dim);font-size:12px">(${players.length}人)</span></h2>
<table class="table">
<tr><th>玩家名</th><th>登录时间</th><th>在线时长</th></tr>
${players.map(p => {
<tr><th>玩家名</th><th>IP地址</th><th>登录时间</th><th>在线时长</th></tr>
${displayRows.map(({player: p, hiddenCount}) => {
const loginTime = p.login_time ? new Date(p.login_time*1000).toLocaleString() : '-';
const mins = Math.floor((Date.now()/1000 - p.login_time)/60);
return `<tr><td class="player-online">🟢 ${p.player_name}</td><td>${loginTime}</td><td>${mins}分钟</td></tr>`;
const ip = p.ip_address || '-';
const suffix = hiddenCount > 0 ? ` <span style="color:var(--yellow);font-size:12px">(折叠同ip段玩家${hiddenCount}名)</span>` : '';
return `<tr><td class="player-online">🟢 ${p.player_name}${suffix}</td><td style="font-size:12px;font-family:monospace">${ip}</td><td>${loginTime}</td><td>${mins}分钟</td></tr>`;
}).join('')}
</table>
</div>`;
@@ -306,21 +336,32 @@ async function doQueryBond() {
}
// ===== 商品管理 =====
let _shopItems = [];
let _shopCategories = [];
let _currentShopCat = '';
async function loadShop(el) {
const r = await jsonApi('shop.php?action=list');
const items = r.data || [];
_shopItems = r.data || [];
_shopCategories = [...new Set(_shopItems.map(i => i.category || '默认'))];
_currentShopCat = _shopCategories[0] || '';
let catTabs = '';
if (_shopCategories.length > 0) {
catTabs = '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px">';
_shopCategories.forEach((cat, idx) => {
const active = idx === 0 ? 'color:#fff;background:var(--accent);border-color:var(--accent)' : '';
const catCount = _shopItems.filter(i => (i.category || '默认') === cat).length;
catTabs += `<div onclick="switchShopCat('${cat.replace(/'/g,"\\'")}')" style="padding:6px 14px;border:1px solid var(--border);border-radius:16px;cursor:pointer;font-size:13px;transition:all 0.2s;${active}" class="shop-cat-tab" data-cat="${cat}">${cat} <span style="opacity:0.7;font-size:11px">(${catCount})</span></div>`;
});
catTabs += '</div>';
}
el.innerHTML = `
<div class="card">
<h2>商品管理 <button class="btn btn-blue" style="float:right" onclick="showAddShop()">+ 添加商品</button></h2>
<table class="table">
<tr><th>ID</th><th>名称</th><th>分类</th><th>购入价</th><th>库存</th><th>销量</th><th>操作</th></tr>
${items.map(i=>`<tr>
<td>${i.id}</td><td>${i.display_name}</td><td>${i.category}</td>
<td>${i.buy_price}</td><td>${i.stock==-1?'∞':i.stock}</td><td>${i.total_sales}</td>
<td><button class="btn btn-yellow" onclick="editStock('${i.id}',${i.stock})">改库存</button>
<button class="btn btn-red" onclick="removeShop('${i.id}')">删除</button></td>
</tr>`).join('')}
</table>
${catTabs}
<div id="shopAdminContent"></div>
</div>
<div class="card" id="addShopForm" style="display:none">
<h2>添加商品</h2>
@@ -332,6 +373,37 @@ async function loadShop(el) {
<div class="form-row"><label>库存</label><input id="asStock" type="number" value="-1" placeholder="-1无限"></div>
<button class="btn btn-green" onclick="doAddShop()">确认添加</button>
</div>`;
renderShopAdminContent();
}
function switchShopCat(cat) {
_currentShopCat = cat;
document.querySelectorAll('.shop-cat-tab').forEach(tab => {
const isActive = tab.dataset.cat === cat;
tab.style.background = isActive ? 'var(--accent)' : '';
tab.style.color = isActive ? '#fff' : '';
tab.style.borderColor = isActive ? 'var(--accent)' : 'var(--border)';
});
renderShopAdminContent();
}
function renderShopAdminContent() {
const div = document.getElementById('shopAdminContent');
if (!div) return;
const items = _shopItems.filter(i => (i.category || '默认') === _currentShopCat);
if (items.length === 0) {
div.innerHTML = '<div class="empty" style="padding:20px">该分类暂无商品</div>';
return;
}
div.innerHTML = `<table class="table">
<tr><th>ID</th><th>名称</th><th>分类</th><th>购入价</th><th>库存</th><th>销量</th><th>操作</th></tr>
${items.map(i=>`<tr>
<td>${i.id}</td><td>${i.display_name}</td><td>${i.category || '默认'}</td>
<td>${i.buy_price}</td><td>${i.stock==-1?'∞':i.stock}</td><td>${i.total_sales}</td>
<td><button class="btn btn-yellow" onclick="editStock('${i.id}',${i.stock})">改库存</button>
<button class="btn btn-red" onclick="removeShop('${i.id}')">删除</button></td>
</tr>`).join('')}
</table>`;
}
function showAddShop() { document.getElementById('addShopForm').style.display='block'; }
@@ -374,7 +446,7 @@ async function loadCDK(el) {
<div style="display:flex;gap:12px;margin-bottom:12px">
<div class="card" style="flex:1;margin:0">
<h2>生成CDK</h2>
<div class="form-row"><label>金额</label><input id="cAmount" type="number" value="100"></div>
<div class="form-row"><label>金额</label><input id="cAmount" type="text" value="100" placeholder="固定金额或区间(如100-200)"></div>
<div class="form-row"><label>数量</label><input id="cCount" type="number" value="10" min="1" max="100"></div>
<button class="btn btn-green" onclick="doBatchCDK()">批量生成</button>
</div>
@@ -393,10 +465,34 @@ async function loadCDK(el) {
}
async function doBatchCDK() {
const amount = parseInt(document.getElementById('cAmount').value);
const amountStr = document.getElementById('cAmount').value.trim();
const count = parseInt(document.getElementById('cCount').value);
if (!amount||!count) { toast('请填写完整','err'); return; }
const r = await postApi('cdk_batch', {amount, count});
if (!amountStr||!count) { toast('请填写完整','err'); return; }
// 解析金额:支持固定金额或区间
let amountData = {};
const separators = [',', ',', '-', '-', '/', '/', '~', '~'];
let foundSep = null;
for (const sep of separators) {
if (amountStr.includes(sep)) {
foundSep = sep;
break;
}
}
if (foundSep) {
const parts = amountStr.split(foundSep).map(s => parseInt(s.trim())).filter(n => !isNaN(n));
if (parts.length === 2 && parts[0] <= parts[1]) {
amountData = {min: parts[0], max: parts[1]};
} else {
toast('区间格式错误,如100-200','err'); return;
}
} else {
const amount = parseInt(amountStr);
if (isNaN(amount) || amount <= 0) { toast('金额必须为正整数','err'); return; }
amountData = {amount: amount};
}
const r = await postApi('cdk_batch', {...amountData, count});
if (r.success) {
toast('生成了'+count+'个CDK','ok');
loadCDK(document.getElementById('C'));
@@ -718,8 +814,34 @@ function lazyLoadUsersPage(page, limit, search, onlineSet) {
return;
}
// 追加用户行
users.forEach(u => {
// ★ 判断是否为日期搜索:日期搜索时折叠同IP段玩家
const s = (search || '').toLowerCase();
const isDateSearch = /今天|昨日|today|yesterday|今天|当日|前一日/.test(s) || /^\d{4}[-\/]\d{1,2}[-\/]\d{1,2}$/.test(s);
// 追加用户行(日期搜索时按/24子网折叠)
let displayUsers = users;
if (isDateSearch && users.length > 0) {
// 按/24子网分组
const subnetMap = {};
const subnetOrder = [];
users.forEach(u => {
const ip = u.ip_address || '-';
let subnet = ip;
if (ip !== '-') {
const parts = ip.split('.');
if (parts.length === 4) subnet = parts[0] + '.' + parts[1] + '.' + parts[2];
}
if (!subnetMap[subnet]) { subnetMap[subnet] = []; subnetOrder.push(subnet); }
subnetMap[subnet].push(u);
});
displayUsers = [];
subnetOrder.forEach(sub => {
const group = subnetMap[sub];
displayUsers.push({...group[0], _foldCount: group.length - 1});
});
}
displayUsers.forEach(u => {
const isOnline = onlineSet && onlineSet.has((u.player_name || '').toLowerCase());
const playerNameClass = isOnline ? 'player-online' : '';
const regTime = u.register_time ? new Date(u.register_time * 1000).toLocaleString() : '-';
@@ -727,11 +849,12 @@ function lazyLoadUsersPage(page, limit, search, onlineSet) {
const hours = Math.floor((u.total_online_time || 0) / 3600);
const ip = u.ip_address || '-';
const ipLoc = u.ip_location || '-';
const foldSuffix = u._foldCount > 0 ? ` <span style="color:var(--yellow);font-size:12px">(同ip段${u._foldCount}名)</span>` : '';
const tr = document.createElement('tr');
tr.setAttribute('data-name', (u.player_name || '').toLowerCase());
tr.innerHTML = `
<td class="${playerNameClass}">${isOnline ? '🟢 ' : ''}${u.player_name}</td>
<td class="${playerNameClass}">${isOnline ? '🟢 ' : ''}${u.player_name}${foldSuffix}</td>
<td>${regTime}</td>
<td>${loginTime}</td>
<td>${u.points || 0}</td>