diff --git a/admin.php b/admin.php index 341b185..cb46973 100644 --- a/admin.php +++ b/admin.php @@ -110,6 +110,7 @@
📋 工单管理
🏡 领地管理
👥 用户组
+
🧾 收银台
@@ -174,6 +175,7 @@ function go(p) { else if (p==='tickets') loadTickets(c); else if (p==='lands') loadLands(c); else if (p==='usergroups') loadUserGroups(c); + else if (p==='cashier') loadCashier(c); } // 检查登录状态 @@ -3124,5 +3126,6 @@ function glassPrompt(label, currentVal, hint = '') { }); } + diff --git a/api/admin.php b/api/admin.php index bbb402d..e171182 100644 --- a/api/admin.php +++ b/api/admin.php @@ -58,6 +58,7 @@ switch ($action) { case 'all_tx': adminAllTx(); break; case 'player_tx': adminPlayerTx(); break; case 'gen_token': adminGenToken(); break; + case 'cashier_player_check': adminCashierPlayerCheck(); break; case 'cdk_batch': adminGenCDK(); break; case 'notify_sync': adminNotifySync(); break; case 'sync_now': adminSyncNow(); break; @@ -483,6 +484,43 @@ function adminGenToken() { } } +// ===== 收银台:查询目标玩家余额/在线状态 ===== +function adminCashierPlayerCheck() { + requireAdminSession(); + $player = trim(getParam('player')); + if (!$player) exit(json_encode(['success' => false, 'message' => '请输入玩家名'], JSON_UNESCAPED_UNICODE)); + if (!preg_match('/^[a-zA-Z0-9_]{3,16}$/', $player)) { + exit(json_encode(['success' => false, 'message' => '玩家名格式不正确(3-16位英文/数字/下划线)'], JSON_UNESCAPED_UNICODE)); + } + $db = getDB(); + $db->exec("CREATE TABLE IF NOT EXISTS bond_cache (player_name TEXT PRIMARY KEY, amount INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)"); + try { + // 确保玩家有余额记录(不存在则建0余额行,便于后续代购扣减) + $db->exec("INSERT OR IGNORE INTO bond_cache (player_name, amount, updated_at) VALUES ('" . str_replace("'", "''", $player) . "', 0, " . time() . ")"); + $stmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :name"); + $stmt->bindValue(':name', $player, SQLITE3_TEXT); + $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); + $balance = $row ? (int)$row['amount'] : 0; + + // 在线状态(表不存在则忽略) + $online = false; + try { + $ostmt = $db->prepare("SELECT player_name FROM online_players WHERE player_name = :name"); + $ostmt->bindValue(':name', $player, SQLITE3_TEXT); + $orow = $ostmt->execute()->fetchArray(SQLITE3_ASSOC); + $online = $orow ? true : false; + } catch (\Throwable $e) {} + + exit(json_encode([ + 'success' => true, + 'data' => ['player' => $player, 'exists' => true, 'balance' => $balance, 'online' => $online] + ], JSON_UNESCAPED_UNICODE)); + } catch (Exception $e) { + @error_log('[Cashier] Player check error: ' . $e->getMessage()); + exit(json_encode(['success' => false, 'message' => '查询失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE)); + } +} + // ===== 批量生成CDK ===== function adminGenCDK() { requireAdminSession(); diff --git a/cashier.js b/cashier.js new file mode 100644 index 0000000..bdebfa7 --- /dev/null +++ b/cashier.js @@ -0,0 +1,378 @@ +// ===== 收银台(管理员/收银员为玩家代购)===== +// 与 admin.php 共享全局作用域,依赖 jsonApi / postApi / toast / escAdmHtml 等全局函数。 +let _cashierCart = []; +let _cashierCfg = {backpack_rate:0.98, shulker_rate:1.00}; +let _cashierSettlement = 'backpack'; +let _cashierColor = 'purple'; +let _cashierItems = []; +let _cashierCategories = []; +let _cashierTarget = ''; +let _cashierTargetBalance = null; + +const CASHIER_COLORS = [ + {id:'purple', name:'紫色', price:0, css:'#AA00FF'}, + {id:'white', name:'白色', price:2, css:'#E8E8E8'}, + {id:'black', name:'黑色', price:2, css:'#2A2A2A'}, + {id:'red', name:'红色', price:2, css:'#FF4444'}, + {id:'blue', name:'蓝色', price:2, css:'#4488FF'}, + {id:'green', name:'绿色', price:2, css:'#44FF77'}, + {id:'yellow', name:'黄色', price:2, css:'#FFEE44'}, + {id:'orange', name:'橙色', price:2, css:'#FF9944'} +]; + +function cashierMatIcon(mat) { + const m = (mat || '').toUpperCase(); + if (m.indexOf('DIAMOND') >= 0) return '💎'; + if (m.indexOf('IRON') >= 0) return '⛏️'; + if (m.indexOf('GOLD') >= 0) return '🪙'; + if (m.indexOf('EMERALD') >= 0) return '🟢'; + if (m.indexOf('NETHERITE') >= 0) return '⬛'; + if (m.indexOf('WOOD') >= 0 || m.indexOf('PLANK') >= 0 || m.indexOf('LOG') >= 0) return '🪵'; + if (m.indexOf('STONE') >= 0 || m.indexOf('COBBLE') >= 0) return '🪨'; + if (m.indexOf('APPLE') >= 0 || m.indexOf('BREAD') >= 0 || m.indexOf('MEAT') >= 0 || m.indexOf('FISH') >= 0 || m.indexOf('PORK') >= 0 || m.indexOf('BEEF') >= 0 || m.indexOf('CHICKEN') >= 0) return '🍖'; + if (m.indexOf('POTION') >= 0 || m.indexOf('BOTTLE') >= 0) return '🧪'; + if (m.indexOf('BOOK') >= 0 || m.indexOf('ENCHANT') >= 0) return '📕'; + if (m.indexOf('SWORD') >= 0 || m.indexOf('AXE') >= 0 || m.indexOf('PICK') >= 0 || m.indexOf('SHOVEL') >= 0 || m.indexOf('HOE') >= 0) return '🗡️'; + if (m.indexOf('HELMET') >= 0 || m.indexOf('CHEST') >= 0 || m.indexOf('LEGGING') >= 0 || m.indexOf('BOOT') >= 0) return '🛡️'; + if (m.indexOf('BED') >= 0) return '🛏️'; + if (m.indexOf('TNT') >= 0) return '🧨'; + return '📦'; +} + +async function loadCashier(el) { + el.innerHTML = '
加载中...
'; + try { const r = await jsonApi('shop.php?action=cart_config'); if (r && r.success && r.data) _cashierCfg = r.data; } catch (e) {} + try { const r = await jsonApi('shop.php?action=list'); _cashierItems = (r && r.data) ? r.data : []; } catch (e) { _cashierItems = []; } + _cashierCategories = [...new Set(_cashierItems.map(i => i.category || '默认'))]; + _cashierTarget = ''; + _cashierTargetBalance = null; + _cashierCart = []; + _cashierSettlement = 'backpack'; + _cashierColor = 'purple'; + + let catTabs = ''; + if (_cashierCategories.length > 0) { + catTabs = '
'; + _cashierCategories.forEach((cat, idx) => { + const active = idx === 0 ? 'color:#fff;background:var(--accent);border-color:var(--accent)' : ''; + const cnt = _cashierItems.filter(i => (i.category || '默认') === cat).length; + catTabs += '
' + escAdmHtml(cat) + ' (' + cnt + ')
'; + }); + catTabs += '
'; + } + + el.innerHTML = ` +
+

🧾 收银台 为玩家代购商品

+
+
+
+
+

① 选择目标玩家

+
+ + +
+
输入玩家名后查询其债券余额
+
+
+

② 选择商品

+ ${catTabs} +
+
+
+
+

🛒 代购清单

+
+
+
+
`; + + document.querySelectorAll('.cashier-cat-tab').forEach(tab => { + tab.addEventListener('click', () => cashierSwitchCat(tab.dataset.cat)); + }); + cashierSwitchCat(_cashierCategories[0] || ''); + renderCashierCart(); +} + +function cashierSwitchCat(cat) { + document.querySelectorAll('.cashier-cat-tab').forEach(c => { + const active = c.dataset.cat === cat; + c.style.background = active ? 'var(--accent)' : ''; + c.style.color = active ? '#fff' : ''; + c.style.borderColor = active ? 'var(--accent)' : ''; + }); + const wrap = document.getElementById('cashierItems'); + if (!wrap) return; + const items = _cashierItems.filter(i => (i.category || '默认') === cat); + if (items.length === 0) { wrap.innerHTML = '
该分类暂无商品
'; return; } + let h = ''; + items.forEach(i => { + const price = parseInt(i.buy_price) || 0; + const stock = parseInt(i.stock); + const stockTxt = stock > 0 ? ('库存 ' + stock) : '无限'; + h += '
'; + h += '
' + cashierMatIcon(i.material) + '' + escAdmHtml(i.display_name || i.id) + '
'; + h += '
' + price + ' 债券 · ' + stockTxt + '
'; + h += '
'; + h += ''; + h += ''; + h += ''; + h += ''; + h += '
'; + }); + wrap.innerHTML = h; + wrap.querySelectorAll('[data-minus]').forEach(b => b.addEventListener('click', () => cashierItemQty(b.dataset.minus, -1))); + wrap.querySelectorAll('[data-plus]').forEach(b => b.addEventListener('click', () => cashierItemQty(b.dataset.plus, 1))); + wrap.querySelectorAll('[data-add]').forEach(b => b.addEventListener('click', () => cashierAddItem(b.dataset.add))); +} + +function cashierItemQty(id, delta) { + const inp = document.getElementById('cqty_' + id); + if (!inp) return; + let v = parseInt(inp.value) || 1; + v = Math.max(1, v + delta); + inp.value = v; +} + +function cashierAddItem(id) { + const item = _cashierItems.find(i => i.id == id); + if (!item) return; + const inp = document.getElementById('cqty_' + id); + let amount = parseInt(inp ? inp.value : 1) || 1; + amount = Math.max(1, amount); + const price = parseInt(item.buy_price) || 0; + const stock = parseInt(item.stock); + const exist = _cashierCart.find(c => c.item_id == id); + if (exist) { + const max = stock > 0 ? stock : 9999; + exist.amount = Math.min(exist.amount + amount, max); + } else { + _cashierCart.push({item_id: id, name: item.display_name || item.id, price: price, material: item.material || '', stock: stock, amount: amount}); + } + renderCashierCart(); + toast('已加入代购清单: ' + (item.display_name || item.id), 'ok'); +} + +function cashierChangeQty(id, delta) { + const i = _cashierCart.find(c => c.item_id == id); + if (!i) return; + const max = i.stock > 0 ? i.stock : 9999; + i.amount = Math.min(Math.max(i.amount + delta, 1), max); + renderCashierCart(); +} +function cashierRemoveItem(id) { + _cashierCart = _cashierCart.filter(c => c.item_id != id); + renderCashierCart(); +} +function cashierClearCart() { + _cashierCart = []; + renderCashierCart(); +} + +function cashierColorFee() { + const c = CASHIER_COLORS.find(x => x.id === _cashierColor); + return c ? c.price : 0; +} +function cashierSubtotal() { + return _cashierCart.reduce((s, i) => s + i.price * i.amount, 0); +} +function cashierTotal() { + const sub = cashierSubtotal(); + const rate = _cashierSettlement === 'shulker' ? _cashierCfg.shulker_rate : _cashierCfg.backpack_rate; + return Math.round(sub * rate) + (_cashierSettlement === 'shulker' ? cashierColorFee() : 0); +} + +function cashierModeHtml(mode, title, total, desc) { + const sel = _cashierSettlement === mode; + return '
' + title + '' + total + ' 债券
' + desc + '
'; +} + +function renderCashierCart() { + const list = document.getElementById('cashierCartList'); + const settleWrap = document.getElementById('cashierSettleWrap'); + if (!list || !settleWrap) return; + if (_cashierCart.length === 0) { + list.innerHTML = '
清单为空,请从左侧添加商品
'; + } else { + let h = ''; + _cashierCart.forEach(i => { + h += '
'; + h += '' + cashierMatIcon(i.material) + ''; + h += '
' + escAdmHtml(i.name) + '
'; + h += '
' + i.price + ' 债券/个
'; + h += '
'; + h += '' + i.amount + ''; + h += '
'; + h += '
' + (i.price * i.amount) + '
'; + h += '
'; + }); + list.innerHTML = h; + } + + const sub = cashierSubtotal(); + const total = cashierTotal(); + let html = ''; + html += '
原价合计' + sub + ' 债券
'; + html += '
'; + const bpTotal = Math.round(sub * _cashierCfg.backpack_rate); + const shTotal = Math.round(sub * _cashierCfg.shulker_rate) + cashierColorFee(); + html += cashierModeHtml('backpack', '🎒 塞背包', bpTotal, '享 ' + (_cashierCfg.backpack_rate * 10).toFixed(1) + ' 折'); + html += cashierModeHtml('shulker', '📦 潜影盒打包', shTotal, '加价 ' + ((_cashierCfg.shulker_rate - 1) * 100).toFixed(0) + '%' + (cashierColorFee() > 0 ? (' +' + cashierColorFee() + '元') : '')); + html += '
'; + if (_cashierSettlement === 'shulker') { + html += '
'; + html += '
🎨 潜影盒颜色 (紫色免费,其它+2元)
'; + html += '
'; + CASHIER_COLORS.forEach(c => { + const sel = c.id === _cashierColor; + const extra = c.price > 0 ? ' (+' + c.price + ')' : ''; + html += ''; + }); + html += '
'; + } + html += '
预估总价' + total + ' 债券
'; + html += '
'; + html += '
'; + settleWrap.innerHTML = html; + + const balHint = document.getElementById('cashierBalanceHint'); + if (balHint) { + if (!_cashierTarget) balHint.textContent = '未选择目标玩家'; + else if (_cashierTargetBalance != null) { + balHint.textContent = '目标余额: ' + _cashierTargetBalance + ' 债券' + (_cashierTargetBalance < total ? '(余额不足!)' : ''); + balHint.style.color = _cashierTargetBalance < total ? 'var(--red)' : 'var(--dim)'; + } + } +} + +function cashierSelectMode(mode) { + _cashierSettlement = mode; + renderCashierCart(); +} +function cashierSelectColor(id) { + _cashierColor = id; + renderCashierCart(); +} + +async function cashierCheckPlayer() { + const inp = document.getElementById('cashierPlayer'); + const info = document.getElementById('cashierTargetInfo'); + if (!inp || !info) return; + const player = inp.value.trim(); + if (!player) { info.textContent = '请输入玩家名'; info.style.color = 'var(--red)'; return; } + info.textContent = '查询中...'; + info.style.color = 'var(--dim)'; + try { + const r = await postApi('cashier_player_check', {player: player}); + if (r && r.success && r.data) { + _cashierTarget = r.data.player; + _cashierTargetBalance = r.data.balance; + const online = r.data.online ? ' ●在线' : ' ○离线'; + info.innerHTML = '目标玩家: ' + escAdmHtml(r.data.player) + '' + online + ' 债券余额: ' + r.data.balance + ''; + info.style.color = 'var(--text)'; + } else { + _cashierTarget = ''; + _cashierTargetBalance = null; + info.textContent = '查询失败: ' + (r ? r.message : '未知错误'); + info.style.color = 'var(--red)'; + } + } catch (e) { + _cashierTarget = ''; + _cashierTargetBalance = null; + info.textContent = '查询失败: ' + e.message; + info.style.color = 'var(--red)'; + } + renderCashierCart(); +} + +async function cashierSettle() { + if (_cashierCart.length === 0) { toast('代购清单为空', 'err'); return; } + if (!_cashierTarget) { toast('请先选择并查询目标玩家', 'err'); return; } + const total = cashierTotal(); + const sub = cashierSubtotal(); + if (_cashierTargetBalance != null && _cashierTargetBalance < total) { + toast('目标玩家余额不足(' + _cashierTargetBalance + ' < ' + total + ')', 'err'); + return; + } + const lines = _cashierCart.map(i => '· ' + i.name + ' x' + i.amount + ' = ' + (i.price * i.amount) + ' 债券').join('\n'); + const modeName = _cashierSettlement === 'shulker' ? ('潜影盒打包(' + (CASHIER_COLORS.find(c => c.id === _cashierColor) ? CASHIER_COLORS.find(c => c.id === _cashierColor).name : '') + ')') : '塞背包'; + const summary = '目标玩家: ' + _cashierTarget + '\n结算方式: ' + modeName + '\n原价: ' + sub + ' 债券\n预估总价: ' + total + ' 债券\n\n' + lines; + cashierShowConfirm(total, summary); +} + +function cashierShowConfirm(total, summary) { + const needPwd = total > 1000; + let overlay = document.getElementById('cashierConfirmOverlay'); + if (overlay) overlay.remove(); + overlay = document.createElement('div'); + overlay.id = 'cashierConfirmOverlay'; + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:6000;display:flex;justify-content:center;align-items:center;animation:glassFadeIn .25s ease'; + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; + let html = '
'; + html += '

🧾 确认代购

'; + html += '
' + escAdmHtml(summary) + '
'; + if (needPwd) { + html += ''; + html += ''; + } + html += '
'; + html += '
'; + overlay.innerHTML = html; + document.body.appendChild(overlay); + if (needPwd) { const p = document.getElementById('cashierPwd'); if (p) p.focus(); } +} + +async function cashierConfirmBuy() { + const total = cashierTotal(); + const pwdEl = document.getElementById('cashierPwd'); + const password = pwdEl ? pwdEl.value : ''; + if (total > 1000 && !password) { toast('请输入管理员密码确认', 'err'); if (pwdEl) pwdEl.focus(); return; } + const btn = document.getElementById('cashierConfirmBtn'); + if (btn) { btn.disabled = true; btn.textContent = '代购中...'; } + await cashierDoBuy(total, password); + const ov = document.getElementById('cashierConfirmOverlay'); + if (ov) ov.remove(); +} + +async function cashierDoBuy(total, password) { + try { + // 1) 获取管理员代购令牌(all 权限,单次使用) + const tR = await postApi('gen_token', {player: _cashierTarget, purpose: 'all'}); + if (!tR || !tR.success || !tR.data || !tR.data.token) { + toast('获取代购令牌失败', 'err'); + return; + } + const token = tR.data.token; + const items = _cashierCart.map(i => ({item_id: i.item_id, amount: i.amount})); + const body = { + token: token, + items: JSON.stringify(items), + settlement: _cashierSettlement, + player: _cashierTarget, + password: password || undefined + }; + if (_cashierSettlement === 'shulker') body.shulker_color = _cashierColor; + const res = await fetch('api/shop.php?action=buy_cart', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(body) + }); + const data = await res.json(); + if (data && data.need_password) { + toast('请重新输入密码确认', 'err'); + cashierShowConfirm(total, '目标玩家: ' + _cashierTarget + '\n(需密码确认)'); + return; + } + if (data && data.success) { + toast('代购成功:' + _cashierTarget + ' 已获得商品', 'ok'); + _cashierCart = []; + renderCashierCart(); + cashierCheckPlayer(); + } else { + toast('代购失败: ' + (data ? data.message : '未知错误'), 'err'); + } + } catch (e) { + toast('代购异常: ' + e.message, 'err'); + } +}