diff --git a/admin.php b/admin.php
index 07fb700..341b185 100644
--- a/admin.php
+++ b/admin.php
@@ -397,8 +397,61 @@ async function loadShop(el) {
+
+
+
🛒 购物车结算配置
+
配置购物车结算时「塞背包」折扣与「潜影盒打包」加价系数,系数作用于商品原价合计。
+
+
+
+
+
+
`;
renderShopAdminContent();
+ loadCartConfig();
+}
+
+// 加载购物车结算配置(读公开 cart_config 接口)
+async function loadCartConfig() {
+ try {
+ const r = await jsonApi('shop.php?action=cart_config');
+ if (r && r.success && r.data) {
+ const bp = document.getElementById('cfgBackpack');
+ const sh = document.getElementById('cfgShulker');
+ if (bp && r.data.backpack_rate != null) bp.value = r.data.backpack_rate;
+ if (sh && r.data.shulker_rate != null) sh.value = r.data.shulker_rate;
+ updateCartCfgHint();
+ }
+ } catch (e) {}
+}
+
+// 实时计算并显示折扣/加价说明
+function updateCartCfgHint() {
+ const bpEl = document.getElementById('cfgBackpack');
+ const shEl = document.getElementById('cfgShulker');
+ const hint = document.getElementById('cartCfgHint');
+ if (!bpEl || !shEl || !hint) return;
+ const bp = parseFloat(bpEl.value);
+ const sh = parseFloat(shEl.value);
+ if (isNaN(bp) || isNaN(sh)) { hint.textContent = ''; return; }
+ const bpDisc = (100 - bp * 100).toFixed(0);
+ const shAdd = ((sh - 1) * 100).toFixed(0);
+ hint.textContent = `塞背包:打 ${(bp * 10).toFixed(1)} 折(-${bpDisc}%) | 潜影盒:加价 ${shAdd}%`;
+}
+
+// 保存购物车结算配置
+async function saveCartConfig() {
+ const bpEl = document.getElementById('cfgBackpack');
+ const shEl = document.getElementById('cfgShulker');
+ const bp = parseFloat(bpEl.value);
+ const sh = parseFloat(shEl.value);
+ if (isNaN(bp) || isNaN(sh)) { toast('请输入有效数字', 'err'); return; }
+ if (bp <= 0 || bp > 1) { toast('塞背包折扣系数应在 0.01 ~ 1.00 之间', 'err'); return; }
+ if (sh < 1 || sh > 3) { toast('潜影盒加价系数应在 1.00 ~ 3.00 之间', 'err'); return; }
+ const r = await postApi('save_shop_config', { cart_backpack_rate: bp, cart_shulker_rate: sh });
+ toast(r.message || (r.success ? '已保存' : '保存失败'), r.success ? 'ok' : 'err');
+ if (r.success) updateCartCfgHint();
}
function switchShopCat(cat) {
diff --git a/api/admin.php b/api/admin.php
index 631773a..bbb402d 100644
--- a/api/admin.php
+++ b/api/admin.php
@@ -53,6 +53,7 @@ switch ($action) {
case 'shop_update': adminShopUpdate(); break;
case 'shop_add': adminShopAdd(); break;
case 'shop_remove': adminShopRemove(); break;
+ case 'save_shop_config': adminSaveShopConfig(); break;
case 'stats': adminStats(); break;
case 'all_tx': adminAllTx(); break;
case 'player_tx': adminPlayerTx(); break;
@@ -201,14 +202,40 @@ function adminShopUpdate() {
$itemId = getParam('id');
if (!$itemId) error('缺少商品ID');
$stock = getParam('stock');
+ $buyPrice = getParam('buy_price');
+ $sellPrice = getParam('sell_price');
// ★ 库存改动写入admin_stock(防止Java全量同步覆盖)
// admin_stock非NULL时pullShopStock优先返回此值给Java
- // 前端只发送id和stock,所以只更新stock和admin_stock
- $stmt = $db->prepare("UPDATE shop_items SET stock=:st, admin_stock=:ast, last_modified=:lm WHERE id=:id");
- $stmt->bindValue(':st', getParam('stock'));
- $stmt->bindValue(':ast', $stock, $stock !== null ? SQLITE3_INTEGER : SQLITE3_NULL);
- $stmt->bindValue(':lm', time(), SQLITE3_INTEGER);
- $stmt->bindValue(':id', $itemId, SQLITE3_TEXT);
+ // ★ 价格改动写入admin_buy_price/admin_sell_price(同理)
+ $sql = "UPDATE shop_items SET last_modified=:lm";
+ $params = [':lm' => time(), ':id' => $itemId];
+ $types = [':lm' => SQLITE3_INTEGER, ':id' => SQLITE3_TEXT];
+ if ($stock !== null) {
+ $sql .= ", stock=:st, admin_stock=:ast";
+ $params[':st'] = $stock;
+ $params[':ast'] = $stock;
+ $types[':st'] = SQLITE3_INTEGER;
+ $types[':ast'] = SQLITE3_INTEGER;
+ }
+ if ($buyPrice !== null) {
+ $sql .= ", buy_price=:bp, admin_buy_price=:abp";
+ $params[':bp'] = $buyPrice;
+ $params[':abp'] = $buyPrice;
+ $types[':bp'] = SQLITE3_INTEGER;
+ $types[':abp'] = SQLITE3_INTEGER;
+ }
+ if ($sellPrice !== null) {
+ $sql .= ", sell_price=:sp, admin_sell_price=:asp";
+ $params[':sp'] = $sellPrice;
+ $params[':asp'] = $sellPrice;
+ $types[':sp'] = SQLITE3_INTEGER;
+ $types[':asp'] = SQLITE3_INTEGER;
+ }
+ $sql .= " WHERE id=:id";
+ $stmt = $db->prepare($sql);
+ foreach ($params as $key => $value) {
+ $stmt->bindValue($key, $value, $types[$key] ?? SQLITE3_TEXT);
+ }
$stmt->execute();
$rowsChanged = $db->changes();
// ★ 同步通知Java插件立即拉取库存改动
@@ -262,6 +289,43 @@ function adminShopRemove() {
}
}
+// ===== 购物车结算折扣/加价配置 =====
+function adminSaveShopConfig() {
+ requireAdminSession();
+ $db = getDB();
+ try {
+ $bp = getParam('cart_backpack_rate');
+ $sh = getParam('cart_shulker_rate');
+ if ($bp === null || $sh === null) error('缺少配置参数');
+ $bp = (float)$bp;
+ $sh = (float)$sh;
+ // 校验范围:塞背包折扣系数 0.01~1.00;潜影盒加价系数 1.00~3.00
+ if ($bp <= 0 || $bp > 1.0) error('塞背包折扣系数必须介于 0.01 ~ 1.00 之间');
+ if ($sh < 1.0 || $sh > 3.0) error('潜影盒加价系数必须介于 1.00 ~ 3.00 之间');
+
+ $db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)");
+ $upsert = $db->prepare("INSERT OR REPLACE INTO shop_config (cfg_key, cfg_value) VALUES (:k, :v)");
+ $upsert->bindValue(':k', 'cart_backpack_rate', SQLITE3_TEXT);
+ $upsert->bindValue(':v', number_format($bp, 2, '.', ''), SQLITE3_TEXT);
+ $upsert->execute();
+ $upsert->bindValue(':k', 'cart_shulker_rate', SQLITE3_TEXT);
+ $upsert->bindValue(':v', number_format($sh, 2, '.', ''), SQLITE3_TEXT);
+ $upsert->execute();
+
+ exit(json_encode([
+ 'success' => true,
+ 'message' => '购物车结算配置已保存',
+ 'data' => [
+ 'backpack_rate' => $bp,
+ 'shulker_rate' => $sh
+ ]
+ ], JSON_UNESCAPED_UNICODE));
+ } catch (\Throwable $e) {
+ @error_log('[save_shop_config] Error: ' . $e->getMessage());
+ exit(json_encode(['success' => false, 'message' => '保存失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE));
+ }
+}
+
function adminStats() {
requireAdminSession();
$db = getDB();
diff --git a/api/shop.php b/api/shop.php
index ddb8e24..b6a4e3f 100644
--- a/api/shop.php
+++ b/api/shop.php
@@ -22,6 +22,12 @@ switch ($action) {
case 'buy':
shopBuy($token);
break;
+ case 'buy_cart':
+ shopBuyCart($token);
+ break;
+ case 'cart_config':
+ cartConfig();
+ break;
case 'categories':
shopCategories($token);
break;
@@ -241,6 +247,205 @@ function shopBuy($token) {
], '购买成功');
}
+// ===== 购物车配置读取 =====
+function getShopConfig($key, $default = null) {
+ try {
+ $db = getDB();
+ $db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)");
+ $stmt = $db->prepare("SELECT cfg_value FROM shop_config WHERE cfg_key = :k");
+ $stmt->bindValue(':k', $key, SQLITE3_TEXT);
+ $result = $stmt->execute();
+ $row = $result->fetchArray(SQLITE3_ASSOC);
+ return $row ? $row['cfg_value'] : $default;
+ } catch (Exception $e) {
+ return $default;
+ }
+}
+
+// 返回购物车折扣/加价配置(前端展示用)
+function cartConfig() {
+ success([
+ 'backpack_rate' => (float)getShopConfig('cart_backpack_rate', '0.98'),
+ 'shulker_rate' => (float)getShopConfig('cart_shulker_rate', '1.00')
+ ]);
+}
+
+// ===== 购物车结算(多商品一次性购买) =====
+function shopBuyCart($token) {
+ $rawItems = getParam('items');
+ $settlement = getParam('settlement', 'backpack'); // backpack=塞背包, shulker=潜影盒打包
+ $player = getParam('player');
+ $password = getParam('password');
+ $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+
+ if (!$rawItems) error('购物车为空');
+ $items = json_decode($rawItems, true);
+ if (!is_array($items) || count($items) === 0) error('购物车格式错误');
+
+ // 校验每一项
+ $parsed = [];
+ foreach ($items as $it) {
+ if (!isset($it['item_id']) || !isset($it['amount'])) error('商品项缺少字段');
+ $pid = (string)$it['item_id'];
+ $amt = (int)$it['amount'];
+ if ($amt < 1) error('数量无效: ' . $pid);
+ $parsed[] = ['item_id' => $pid, 'amount' => $amt];
+ }
+ if (!$player) error('缺少player');
+
+ // ★ 双保险验证(与单件购买一致)
+ $isPreview = true;
+ $isOnline = false;
+ $isRegistered = false;
+
+ if ($token) {
+ $tokenInfo = validateToken($token);
+ if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
+ $isPreview = false;
+ if (validateToken($token)) validateAndUseToken($token);
+ } else {
+ $accessResult = validateWebAccess($token, 'buy', $password, $ipAddress);
+ if ($accessResult['ok']) {
+ $isPreview = false;
+ $player = $accessResult['player'];
+ $isOnline = $accessResult['online'] ?? false;
+ $isRegistered = $accessResult['registered'] ?? false;
+ } elseif (($accessResult['mode'] ?? '') === 'need_password') {
+ jsonResponse(['success' => false, 'need_password' => true, 'player' => $accessResult['player'], 'message' => $accessResult['message']], 401);
+ } elseif (($accessResult['mode'] ?? '') === 'need_game_login') {
+ jsonResponse(['success' => false, 'need_game_login' => true, 'player' => $accessResult['player'], 'message' => $accessResult['message']], 401);
+ } elseif (($accessResult['mode'] ?? '') === 'need_register') {
+ jsonResponse(['success' => false, 'need_register' => true, 'player' => $accessResult['player'], 'message' => $accessResult['message']], 401);
+ } else {
+ error($accessResult['message'], 401);
+ }
+ }
+ }
+
+ // 结算模式对应的折扣/加价系数
+ if ($settlement === 'shulker') {
+ $rate = (float)getShopConfig('cart_shulker_rate', '1.00');
+ $modeName = '潜影盒打包';
+ } else {
+ $settlement = 'backpack';
+ $rate = (float)getShopConfig('cart_backpack_rate', '0.98');
+ $modeName = '塞背包';
+ }
+
+ $db = getDB();
+ $lines = [];
+ $subtotal = 0;
+ foreach ($parsed as &$p) {
+ $stmt = $db->prepare("SELECT * FROM shop_items WHERE id = :id");
+ $stmt->bindValue(':id', $p['item_id'], SQLITE3_TEXT);
+ $result = $stmt->execute();
+ $item = $result->fetchArray(SQLITE3_ASSOC);
+ if (!$item) error('商品不存在: ' . $p['item_id']);
+ if ($item['stock'] == 0) error('商品已售罄: ' . $item['display_name']);
+ if ($item['stock'] > 0 && $item['stock'] < $p['amount']) error('库存不足: ' . $item['display_name'] . '(剩余' . $item['stock'] . ')');
+ $lineTotal = (int)$item['buy_price'] * $p['amount'];
+ $subtotal += $lineTotal;
+ $p['item'] = $item;
+ $lines[] = [
+ 'item_id' => $p['item_id'],
+ 'name' => $item['display_name'],
+ 'unit_price' => (int)$item['buy_price'],
+ 'amount' => $p['amount'],
+ 'line_total' => $lineTotal
+ ];
+ }
+ unset($p);
+
+ $totalPrice = (int)round($subtotal * $rate);
+ $saved = $subtotal - $totalPrice; // 折扣省下的(潜影盒加价时为负)
+
+ // 检查余额
+ if (!$isPreview && $totalPrice > 0) {
+ $balanceStmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :player");
+ $balanceStmt->bindValue(':player', $player, SQLITE3_TEXT);
+ $balanceResult = $balanceStmt->execute();
+ $balanceRow = $balanceResult->fetchArray(SQLITE3_ASSOC);
+ if (!$balanceRow) error('玩家余额查询失败');
+ $balance = (int)$balanceRow['amount'];
+ if ($balance < $totalPrice) {
+ error('余额不足,当前余额: ' . $balance . ' 债券,需要: ' . $totalPrice . ' 债券');
+ }
+ }
+
+ if ($isPreview) {
+ preview([
+ 'settlement' => $settlement,
+ 'mode_name' => $modeName,
+ 'rate' => $rate,
+ 'items' => $lines,
+ 'subtotal' => $subtotal,
+ 'total_price' => $totalPrice,
+ 'saved' => $saved,
+ 'player' => $player
+ ], '预览模式 - 结算不会生效');
+ }
+
+ // ★ 实际购买 — 包装在事务中
+ $db->exec('BEGIN IMMEDIATE');
+ try {
+ foreach ($parsed as $p) {
+ $item = $p['item'];
+ if ($item['stock'] > 0) {
+ $newStock = $item['stock'] - $p['amount'];
+ $stmt = $db->prepare("UPDATE shop_items SET stock = :ns, admin_stock = :ns, hourly_sales = hourly_sales + :amount, total_sales = total_sales + :amount WHERE id = :id");
+ $stmt->bindValue(':ns', $newStock, SQLITE3_INTEGER);
+ $stmt->bindValue(':amount', $p['amount'], SQLITE3_INTEGER);
+ $stmt->bindValue(':id', $p['item_id'], SQLITE3_TEXT);
+ $stmt->execute();
+ } else {
+ $stmt = $db->prepare("UPDATE shop_items SET hourly_sales = hourly_sales + :amount, total_sales = total_sales + :amount WHERE id = :id");
+ $stmt->bindValue(':amount', $p['amount'], SQLITE3_INTEGER);
+ $stmt->bindValue(':id', $p['item_id'], SQLITE3_TEXT);
+ $stmt->execute();
+ }
+ }
+
+ // 记录一条合并交易
+ $reasonItems = array_map(function ($l) { return $l['name'] . ' x' . $l['amount']; }, $lines);
+ $stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'shop_cart', :amount, :reason, :detail, 'pending', :time)");
+ $stmt->bindValue(':player', $player, SQLITE3_TEXT);
+ $stmt->bindValue(':amount', $totalPrice, SQLITE3_INTEGER);
+ $stmt->bindValue(':reason', "购物车结算({$modeName}): " . implode('、', $reasonItems), SQLITE3_TEXT);
+ $stmt->bindValue(':detail', json_encode([
+ 'settlement' => $settlement,
+ 'rate' => $rate,
+ 'subtotal' => $subtotal,
+ 'total_price' => $totalPrice,
+ 'items' => $lines
+ ], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
+ $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
+ $stmt->execute();
+
+ // 写入sync_requests
+ $db->exec("CREATE TABLE IF NOT EXISTS sync_requests (player_name TEXT PRIMARY KEY, created_at INTEGER NOT NULL)");
+ $stmt2 = $db->prepare("INSERT OR REPLACE INTO sync_requests (player_name, created_at) VALUES (:player, :time)");
+ $stmt2->bindValue(':player', $player, SQLITE3_TEXT);
+ $stmt2->bindValue(':time', time(), SQLITE3_INTEGER);
+ $stmt2->execute();
+
+ $db->exec('COMMIT');
+ } catch (Exception $e) {
+ try { $db->exec('ROLLBACK'); } catch (Exception $e2) {}
+ error('购物车结算失败: ' . $e->getMessage(), 500);
+ }
+
+ success([
+ 'settlement' => $settlement,
+ 'mode_name' => $modeName,
+ 'rate' => $rate,
+ 'items' => $lines,
+ 'subtotal' => $subtotal,
+ 'total_price' => $totalPrice,
+ 'saved' => $saved,
+ 'player' => $player
+ ], '购物车结算成功');
+}
+
// ===== Weblogin Token验证(用于商城购买)=====
function validateWebloginTokenForShop($webToken) {
$db = getDB();
diff --git a/api/sync.php b/api/sync.php
index 84e3a6f..ddb4505 100644
--- a/api/sync.php
+++ b/api/sync.php
@@ -269,6 +269,15 @@ switch ($action) {
case 'check_shop_stock_changed':
checkShopStockChanged();
break;
+ case 'pull_shop_prices':
+ pullShopPrices();
+ break;
+ case 'clear_admin_prices':
+ clearAdminPrices();
+ break;
+ case 'check_shop_prices_changed':
+ checkShopPricesChanged();
+ break;
case 'notify_sync':
notifySync();
break;
@@ -2750,6 +2759,67 @@ function clearAdminStock() {
success([], '已清除管理员库存标记');
}
+// ===== 拉取价格改动:admin_buy_price/admin_sell_price非NULL时返回给Java =====
+function pullShopPrices() {
+ $secret = getParam('secret');
+ if (!$secret || $secret !== SECRET_KEY) error('认证失败');
+
+ $db = getDB();
+ // ★ 只返回有价格改动的商品(admin_buy_price或admin_sell_price非NULL)
+ $stmt = $db->prepare("SELECT id, buy_price, sell_price, admin_buy_price, admin_sell_price FROM shop_items WHERE admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL");
+ $result = $stmt->execute();
+ $items = [];
+ while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
+ // ★ 如果admin字段有值,用admin值替代原始价格返回给Java
+ if ($row['admin_buy_price'] !== null && $row['admin_buy_price'] !== '') {
+ $row['buy_price'] = (int)$row['admin_buy_price'];
+ $row['admin_buy_price_override'] = true;
+ }
+ if ($row['admin_sell_price'] !== null && $row['admin_sell_price'] !== '') {
+ $row['sell_price'] = (int)$row['admin_sell_price'];
+ $row['admin_sell_price_override'] = true;
+ }
+ unset($row['admin_buy_price'], $row['admin_sell_price']); // 不暴露内部字段
+ $items[] = $row;
+ }
+
+ // ★ 成功获取数据后立即清除admin价格标记(同一次请求完成,消除竞态)
+ $db->exec("UPDATE shop_items SET admin_buy_price = NULL, admin_sell_price = NULL WHERE admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL");
+
+ success(['items' => $items, 'count' => count($items)]);
+}
+
+// ===== 插件确认已应用管理员价格改动 =====
+function clearAdminPrices() {
+ $secret = getParam('secret');
+ if (!$secret || $secret !== SECRET_KEY) error('认证失败');
+
+ $db = getDB();
+ $db->exec("UPDATE shop_items SET admin_buy_price = NULL, admin_sell_price = NULL WHERE admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL");
+ success([], '已清除管理员价格标记');
+}
+
+// ===== 高频轮询:检测价格是否有改动(轻量级接口) =====
+function checkShopPricesChanged() {
+ $secret = getParam('secret');
+ if (!$secret || $secret !== SECRET_KEY) error('认证失败');
+
+ $db = getDB();
+ try {
+ $stmt = $db->prepare("SELECT SUM(CASE WHEN admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL THEN 1 ELSE 0 END) as admin_price_count FROM shop_items");
+ $result = $stmt->execute();
+ $row = $result->fetchArray(SQLITE3_ASSOC);
+ $adminPriceCount = $row['admin_price_count'] ? (int)$row['admin_price_count'] : 0;
+ } catch (\Throwable $e) {
+ @error_log("[checkShopPricesChanged] error: " . $e->getMessage());
+ $adminPriceCount = 0;
+ }
+
+ // ★ 如果admin价格非NULL(管理员改了价格但Java还没拉取),返回changed=true
+ $changed = ($adminPriceCount > 0);
+ success(['changed' => $changed, 'admin_price_count' => $adminPriceCount]);
+}
+
// ===== 高频轮询:检测库存是否有改动(轻量级接口) =====
function checkShopStockChanged() {
$secret = getParam('secret');
diff --git a/core.php b/core.php
index 3b5c34f..73d7772 100644
--- a/core.php
+++ b/core.php
@@ -222,7 +222,9 @@ function initTables(SQLite3 $db) {
hourly_sales INTEGER DEFAULT 0,
total_sales INTEGER DEFAULT 0,
last_sync INTEGER DEFAULT 0,
- admin_stock INTEGER DEFAULT NULL
+ admin_stock INTEGER DEFAULT NULL,
+ admin_buy_price INTEGER DEFAULT NULL,
+ admin_sell_price INTEGER DEFAULT NULL
)");
// ★ 兼容升级:给shop_items加admin_stock字段(已存在则跳过)
@@ -233,6 +235,13 @@ function initTables(SQLite3 $db) {
try {
$db->exec("ALTER TABLE shop_items ADD COLUMN last_modified INTEGER DEFAULT 0");
} catch (\Throwable $e) { /* 字段已存在,忽略 */ }
+ // ★ 兼容升级:给shop_items加admin价格覆盖字段(PHP后台修改价格时写入,Java拉取后清除)
+ try {
+ $db->exec("ALTER TABLE shop_items ADD COLUMN admin_buy_price INTEGER DEFAULT NULL");
+ } catch (\Throwable $e) { /* 字段已存在,忽略 */ }
+ try {
+ $db->exec("ALTER TABLE shop_items ADD COLUMN admin_sell_price INTEGER DEFAULT NULL");
+ } catch (\Throwable $e) { /* 字段已存在,忽略 */ }
// CDK表
$db->exec("CREATE TABLE IF NOT EXISTS cdk (
diff --git a/player.php b/player.php
index 299efe3..f814a3b 100644
--- a/player.php
+++ b/player.php
@@ -1154,9 +1154,11 @@ if ($currentVersion !== $BUILD_VERSION) {
let shopItems = [];
let shopCategories = [];
let currentShopCat = '';
+ let currentBuyItem = null; // 当前购买弹窗对应的商品,供"加入购物车"使用
async function renderShop(el) {
el.innerHTML = '加载中...
';
+ loadCart();
try {
const res = await api('shop.php', {action: 'list'});
if (!res.success) { el.innerHTML = '' + (res.message || '加载失败') + '
'; return; }
@@ -1189,6 +1191,7 @@ if ($currentVersion !== $BUILD_VERSION) {
// 渲染当前分类的商品
renderShopContent();
+ ensureCartButton();
} catch (e) {
el.innerHTML = '商城加载失败: ' + e.message + '
';
}
@@ -1221,7 +1224,7 @@ if ($currentVersion !== $BUILD_VERSION) {
const matIcon = getMaterialIcon(item.material);
const stockText = item.stock == -1 ? '∞ 无限' : (item.stock == 0 ? '售罄' : item.stock + ' 个');
const stockClass = item.stock == 0 ? 'out' : '';
- html += `${matIcon}
${item.display_name}
${item.buy_price} 债券
库存: ${stockText}
`;
+ html += `${matIcon}
${item.display_name}
${item.buy_price} 债券
库存: ${stockText}
`;
});
html += '';
contentEl.innerHTML = html;
@@ -1232,9 +1235,10 @@ if ($currentVersion !== $BUILD_VERSION) {
return icons[mat] || '🧱';
}
- function showBuyModal(id, name, price, stock) {
+ function showBuyModal(id, name, price, stock, material) {
if (IS_PREVIEW) { toast('预览模式下无法购买', 'error'); return; }
if (stock == 0) { toast('商品已售罄', 'error'); return; }
+ currentBuyItem = {id, name, price, stock, material: material || ''};
document.getElementById('modalTitle').textContent = '购买: ' + name;
const needPwd = price > 1000;
const maxQty = stock > 0 ? stock : 999;
@@ -1243,6 +1247,11 @@ if ($currentVersion !== $BUILD_VERSION) {
${needPwd ? '' : '小额交易(≤1000债券),免密码确认
'}`;
+ // 加入购物车按钮
+ const cartBtn = document.createElement('div');
+ cartBtn.style.cssText = 'margin-top:12px';
+ cartBtn.innerHTML = '';
+ document.getElementById('modalBody').appendChild(cartBtn);
document.getElementById('modalConfirm').onclick = () => doBuy(id);
document.getElementById('buyAmount').oninput = function() {
const total = this.value * price;
@@ -1292,6 +1301,230 @@ if ($currentVersion !== $BUILD_VERSION) {
}
}
+ // ===== 购物车系统 =====
+ let cart = [];
+ let cartCfg = {backpack_rate: 0.98, shulker_rate: 1.0};
+ let cartSettlement = 'backpack';
+ let cartSubtotalVal = 0;
+
+ function loadCart() {
+ try { cart = JSON.parse(localStorage.getItem('sdf1_cart') || '[]'); } catch (e) { cart = []; }
+ if (!Array.isArray(cart)) cart = [];
+ updateCartButton();
+ }
+ function saveCart() { try { localStorage.setItem('sdf1_cart', JSON.stringify(cart)); } catch (e) {} updateCartButton(); }
+
+ function updateCartButton() {
+ const btn = document.getElementById('cartFab');
+ if (!btn) return;
+ const count = cart.reduce((s, i) => s + (i.amount || 1), 0);
+ const total = cart.reduce((s, i) => s + i.price * i.amount, 0);
+ btn.querySelector('.cart-fab-count').textContent = count;
+ btn.querySelector('.cart-fab-total').textContent = total;
+ btn.style.display = count > 0 ? 'flex' : 'none';
+ }
+
+ function ensureCartButton() {
+ if (document.getElementById('cartFab')) { updateCartButton(); return; }
+ const btn = document.createElement('div');
+ btn.id = 'cartFab';
+ btn.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:9000;display:none;align-items:center;gap:8px;padding:12px 18px;background:var(--accent);color:#fff;border-radius:30px;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,0.35);font-size:14px;transition:transform .2s cubic-bezier(0.16,1,0.3,1)';
+ btn.innerHTML = '🛒0件0债券';
+ btn.onclick = openCartDrawer;
+ btn.onmouseenter = () => btn.style.transform = 'scale(1.06)';
+ btn.onmouseleave = () => btn.style.transform = 'scale(1)';
+ document.body.appendChild(btn);
+ updateCartButton();
+ }
+
+ function addToCartFromModal() {
+ if (!currentBuyItem) return;
+ const amount = parseInt(document.getElementById('buyAmount')?.value || '1') || 1;
+ addToCart(currentBuyItem.id, currentBuyItem.name, currentBuyItem.price, currentBuyItem.material || '', currentBuyItem.stock, amount);
+ closeModal();
+ }
+
+ function addToCart(id, name, price, material, stock, amount) {
+ if (IS_PREVIEW) { toast('预览模式下无法加入购物车', 'error'); return; }
+ amount = Math.max(1, parseInt(amount) || 1);
+ if (stock > 0 && amount > stock) amount = stock;
+ const exist = cart.find(i => i.item_id == id);
+ if (exist) {
+ const max = stock > 0 ? stock : 9999;
+ exist.amount = Math.min((exist.amount || 1) + amount, max);
+ } else {
+ cart.push({item_id: id, name: name, price: parseInt(price), material: material, stock: parseInt(stock), amount: amount});
+ }
+ saveCart();
+ toast('已加入购物车', 'success');
+ }
+
+ function changeCartQty(id, delta) {
+ const i = cart.find(x => x.item_id == id);
+ if (!i) return;
+ const max = i.stock > 0 ? i.stock : 9999;
+ i.amount = Math.min(Math.max((i.amount || 1) + delta, 1), max);
+ saveCart();
+ renderCartList();
+ }
+ function removeFromCart(id) {
+ cart = cart.filter(x => x.item_id != id);
+ saveCart();
+ renderCartList();
+ }
+ function clearCart() {
+ cart = [];
+ saveCart();
+ renderCartList();
+ }
+
+ function openCartDrawer() {
+ if (cart.length === 0) { toast('购物车是空的', 'info'); return; }
+ document.getElementById('cartDrawer')?.remove();
+ const overlay = document.createElement('div');
+ overlay.id = 'cartDrawer';
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);z-index:9500;display:flex;justify-content:flex-end;animation:glassFadeIn .2s ease';
+ overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
+ let html = '';
+ html += '
🛒 购物车
';
+ html += '
';
+ html += '
';
+ html += '
商品原价合计0 债券
';
+ html += '
预估总价0 债券
';
+ html += '
';
+ html += '
';
+ overlay.innerHTML = html;
+ document.body.appendChild(overlay);
+ renderCartList();
+ }
+
+ function renderCartList() {
+ const list = document.getElementById('cartList');
+ if (!list) return;
+ if (cart.length === 0) {
+ list.innerHTML = '购物车空空如也
';
+ } else {
+ let h = '';
+ cart.forEach(i => {
+ const matIcon = getMaterialIcon(i.material);
+ h += '';
+ h += '
' + matIcon + '
';
+ h += '
' + escapeHtml(i.name) + '
';
+ h += '
' + i.price + ' 债券/个
';
+ h += '
';
+ h += '' + i.amount + '';
+ h += '
';
+ h += '
' + (i.price * i.amount) + '
';
+ h += '
';
+ });
+ list.innerHTML = h;
+ }
+ const subtotal = cart.reduce((s, i) => s + i.price * i.amount, 0);
+ cartSubtotalVal = subtotal;
+ const grand = document.getElementById('cartGrand');
+ if (grand) grand.textContent = subtotal + ' 债券';
+ const sub = document.getElementById('cartSubtotal');
+ if (sub) sub.textContent = subtotal + ' 债券';
+ updateCartButton();
+ }
+
+ async function openSettlement() {
+ if (cart.length === 0) { toast('购物车是空的', 'info'); return; }
+ if (IS_PREVIEW) { toast('预览模式无法结算', 'error'); return; }
+ try { const r = await api('shop.php', {action: 'cart_config'}); if (r.success && r.data) cartCfg = r.data; } catch (e) {}
+ const subtotal = cart.reduce((s, i) => s + i.price * i.amount, 0);
+ cartSubtotalVal = subtotal;
+ const bpTotal = Math.round(subtotal * cartCfg.backpack_rate);
+ const shTotal = Math.round(subtotal * cartCfg.shulker_rate);
+ const overlay = document.createElement('div');
+ overlay.id = 'cartSettle';
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:9800;display:flex;justify-content:center;align-items:center;animation:glassFadeIn .2s ease';
+ overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
+ let html = '';
+ html += '
🧾 选择结算方式
';
+ html += '
共 ' + cart.reduce((s, i) => s + i.amount, 0) + ' 件 · 原价 ' + subtotal + ' 债券
';
+ html += '
';
+ html += settleModeHtml('backpack', '🎒 塞背包', cartCfg.backpack_rate, bpTotal, subtotal, '直接发放到背包,享 ' + (cartCfg.backpack_rate * 10).toFixed(1) + ' 折');
+ html += settleModeHtml('shulker', '📦 潜影盒打包', cartCfg.shulker_rate, shTotal, subtotal, shulkerDesc(cartCfg.shulker_rate, subtotal, shTotal));
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ overlay.innerHTML = html;
+ document.body.appendChild(overlay);
+ selectSettleMode('backpack');
+ }
+
+ function shulkerDesc(rate, subtotal, total) {
+ const diff = total - subtotal;
+ if (diff > 0) return '打包进潜影盒,加价 ' + diff + ' 债券';
+ if (diff < 0) return '打包进潜影盒,优惠 ' + (-diff) + ' 债券';
+ return '打包进潜影盒,原价结算';
+ }
+
+ function settleModeHtml(mode, title, rate, total, subtotal, desc) {
+ const saved = subtotal - total;
+ let badge;
+ if (saved > 0) badge = '省 ' + saved + ' 债券';
+ else if (saved < 0) badge = '加价 ' + (-saved) + ' 债券';
+ else badge = '原价';
+ return '' + title + '
' + total + ' 债券
' + badge + '
' + desc + '
';
+ }
+
+ function selectSettleMode(mode) {
+ cartSettlement = mode;
+ document.querySelectorAll('.settle-mode').forEach(c => {
+ const active = c.dataset.mode === mode;
+ c.style.borderColor = active ? 'var(--accent)' : '';
+ c.style.background = active ? 'color-mix(in srgb, var(--accent) 10%, transparent)' : '';
+ });
+ updateSettlePwd();
+ }
+
+ function updateSettlePwd() {
+ const wrap = document.getElementById('settlePwdWrap');
+ if (!wrap) return;
+ const total = Math.round(cartSubtotalVal * (cartSettlement === 'shulker' ? cartCfg.shulker_rate : cartCfg.backpack_rate));
+ if (total > 1000) {
+ wrap.innerHTML = '';
+ } else {
+ wrap.innerHTML = '小额结算(≤1000债券),免密码确认
';
+ }
+ }
+
+ async function doSettle() {
+ const subtotal = cart.reduce((s, i) => s + i.price * i.amount, 0);
+ const total = Math.round(subtotal * (cartSettlement === 'shulker' ? cartCfg.shulker_rate : cartCfg.backpack_rate));
+ const password = document.getElementById('settlePassword')?.value || '';
+ if (total > 1000 && !password) { toast('请输入密码确认', 'error'); return; }
+ const itemsPayload = cart.map(i => ({item_id: i.item_id, amount: i.amount}));
+ const btn = document.getElementById('settleConfirmBtn');
+ if (btn) { btn.disabled = true; btn.textContent = '结算中...'; }
+ try {
+ const res = await fetch(API + 'shop.php?action=buy_cart', {
+ method: 'POST', headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({token: TOKEN, items: JSON.stringify(itemsPayload), settlement: cartSettlement, player: currentPlayer, password: password || undefined})
+ });
+ const data = await res.json();
+ if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }
+ document.getElementById('cartSettle')?.remove();
+ if (data.success) {
+ const paid = data.data.total_price;
+ const modeName = data.data.mode_name;
+ cart = [];
+ saveCart();
+ toast('结算成功(' + modeName + '):' + paid + ' 债券', 'success');
+ setTimeout(() => { notifyJavaSync(); switchPage('shop'); }, 500);
+ } else {
+ toast(data.message || '结算失败', 'error');
+ }
+ } catch (e) {
+ toast('结算请求失败: ' + e.message, 'error');
+ } finally {
+ if (btn) { btn.disabled = false; btn.textContent = '确认结算'; }
+ }
+ }
+
// ===== 个人中心仪表盘 =====
async function renderDashboard(el) {
if (IS_PREVIEW || !currentPlayer || (!AUTHENTICATED && NEED_PASSWORD)) {