问题10修复: CDK兑换不到账+假密码+隐藏密码框
player.php: - 商城购买:总价≤1000时隐藏密码输入框,动态切换 - CDK兑换:完全移除密码输入框(CDK免验证) - doBuy:小额交易不强制密码,大额才要求 - doCDKExchange:移除密码字段 shop.php: - 移除硬性密码检查,交给validateWebAccess处理 cdk.php: - 远程CDK兑换:改为cdk_redeem+pending(之前completed被Java跳过) - 远程CDK兑换同时更新bond_cache+sync_requests
This commit is contained in:
@@ -75,6 +75,10 @@
|
||||
.glass-alert-card .alert-btns .ag-ok{background:#58a6ff;color:#fff}
|
||||
.glass-alert-card .alert-btns .ag-cancel{background:rgba(255,255,255,0.08);color:#8b949e}
|
||||
.glass-alert-card .alert-btns button:hover{opacity:0.85;transform:scale(1.03)}
|
||||
.glass-alert-card .alert-input{width:100%;padding:10px 12px;background:rgba(13,17,23,0.8);border:1px solid rgba(88,166,255,0.3);border-radius:8px;color:#e6edf3;font-size:13px;outline:none;box-sizing:border-box;transition:border 0.2s;text-align:center;margin-bottom:16px}
|
||||
.glass-alert-card .alert-input:focus{border-color:#58a6ff;box-shadow:0 0 0 2px rgba(88,166,255,0.15)}
|
||||
.glass-alert-card .alert-hint{font-size:11px;color:#8b949e;margin:-12px 0 16px;text-align:left}
|
||||
.glass-alert-card .alert-label{font-size:12px;color:#8b949e;margin-bottom:4px;text-align:left}
|
||||
@keyframes glassFadeIn{from{opacity:0}to{opacity:1}}
|
||||
@keyframes glassSlideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||||
</style>
|
||||
@@ -2394,13 +2398,20 @@ async function saveLandConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function changeLandOwner(name, currentOwner) {
|
||||
const newOwner = prompt('领地 [' + name + '] 当前所有者: ' + (currentOwner||'无') + '\n输入新所有者:', currentOwner);
|
||||
if (newOwner === null || newOwner === currentOwner) return;
|
||||
async function changeLandOwner(name, currentOwner) {
|
||||
const newOwner = await glassPrompt('领地 [' + name + '] 新所有者', currentOwner||'', '仅允许英文字母、数字和下划线,2-16位');
|
||||
if (newOwner === null || newOwner === false || newOwner === undefined) return;
|
||||
const trimmed = newOwner.trim();
|
||||
if (trimmed === '' || trimmed === currentOwner) return;
|
||||
// 前端验证:Minecraft玩家名格式
|
||||
if (!/^[a-zA-Z0-9_]{2,16}$/.test(trimmed)) {
|
||||
glassAlert('玩家名格式无效:仅允许英文字母、数字和下划线,2-16位');
|
||||
return;
|
||||
}
|
||||
fetch('api/land_api.php?action=update_land_owner', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(newOwner) + '&secret=sdf1_web_comm_2026_ypshidifu'
|
||||
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(trimmed) + '&secret=sdf1_web_comm_2026_ypshidifu'
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.success) loadLands(document.getElementById('C'));
|
||||
else glassAlert('失败: ' + (d.error||''));
|
||||
@@ -2436,6 +2447,9 @@ async function deleteShopItem(id) {
|
||||
<div class="glass-alert-card" onclick="event.stopPropagation()">
|
||||
<div class="alert-icon" id="glassAlertIcon">⚠️</div>
|
||||
<div class="alert-msg" id="glassAlertMsg"></div>
|
||||
<div class="alert-label" id="glassAlertLabel" style="display:none"></div>
|
||||
<input class="alert-input" id="glassAlertInput" style="display:none" onkeydown="if(event.key==='Enter')glassAlertResolve(true)">
|
||||
<div class="alert-hint" id="glassAlertHint" style="display:none"></div>
|
||||
<div class="alert-btns" id="glassAlertBtns">
|
||||
<button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>
|
||||
</div>
|
||||
@@ -2444,14 +2458,25 @@ async function deleteShopItem(id) {
|
||||
<script>
|
||||
let _glassAlertResolve = null;
|
||||
function glassAlertResolve(val) {
|
||||
document.getElementById('glassAlertOverlay').classList.remove('show');
|
||||
if (_glassAlertResolve) { _glassAlertResolve(val); _glassAlertResolve = null; }
|
||||
const overlay = document.getElementById('glassAlertOverlay');
|
||||
overlay.classList.remove('show');
|
||||
if (_glassAlertResolve) {
|
||||
const input = document.getElementById('glassAlertInput');
|
||||
// 如果输入框可见且点了确定,返回输入值
|
||||
const result = (val && input.style.display !== 'none') ? input.value : val;
|
||||
_glassAlertResolve(result);
|
||||
_glassAlertResolve = null;
|
||||
}
|
||||
}
|
||||
function glassAlert(msg, icon = '⚠️') {
|
||||
return new Promise(resolve => {
|
||||
_glassAlertResolve = resolve;
|
||||
document.getElementById('glassAlertIcon').textContent = icon;
|
||||
document.getElementById('glassAlertMsg').textContent = msg;
|
||||
document.getElementById('glassAlertMsg').style.display = '';
|
||||
document.getElementById('glassAlertLabel').style.display = 'none';
|
||||
document.getElementById('glassAlertInput').style.display = 'none';
|
||||
document.getElementById('glassAlertHint').style.display = 'none';
|
||||
document.getElementById('glassAlertBtns').innerHTML = '<button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>';
|
||||
document.getElementById('glassAlertOverlay').classList.add('show');
|
||||
});
|
||||
@@ -2461,10 +2486,33 @@ function glassConfirm(msg, icon = '❓') {
|
||||
_glassAlertResolve = resolve;
|
||||
document.getElementById('glassAlertIcon').textContent = icon;
|
||||
document.getElementById('glassAlertMsg').textContent = msg;
|
||||
document.getElementById('glassAlertMsg').style.display = '';
|
||||
document.getElementById('glassAlertLabel').style.display = 'none';
|
||||
document.getElementById('glassAlertInput').style.display = 'none';
|
||||
document.getElementById('glassAlertHint').style.display = 'none';
|
||||
document.getElementById('glassAlertBtns').innerHTML = '<button class="ag-cancel" onclick="glassAlertResolve(false)">取消</button><button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>';
|
||||
document.getElementById('glassAlertOverlay').classList.add('show');
|
||||
});
|
||||
}
|
||||
function glassPrompt(label, currentVal, hint = '') {
|
||||
return new Promise(resolve => {
|
||||
_glassAlertResolve = resolve;
|
||||
document.getElementById('glassAlertIcon').textContent = '✏️';
|
||||
document.getElementById('glassAlertMsg').style.display = 'none';
|
||||
document.getElementById('glassAlertLabel').textContent = label;
|
||||
document.getElementById('glassAlertLabel').style.display = '';
|
||||
const input = document.getElementById('glassAlertInput');
|
||||
input.value = currentVal;
|
||||
input.style.display = '';
|
||||
input.selectionStart = input.selectionEnd = input.value.length;
|
||||
const hintEl = document.getElementById('glassAlertHint');
|
||||
if (hint) { hintEl.textContent = '💡 ' + hint; hintEl.style.display = ''; }
|
||||
else { hintEl.style.display = 'none'; }
|
||||
document.getElementById('glassAlertBtns').innerHTML = '<button class="ag-cancel" onclick="glassAlertResolve(false)">取消</button><button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>';
|
||||
document.getElementById('glassAlertOverlay').classList.add('show');
|
||||
setTimeout(() => input.focus(), 100);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+24
-2
@@ -182,14 +182,36 @@ function cdkExchange($token) {
|
||||
$stmt2->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt2->execute();
|
||||
} else {
|
||||
// 远程CDK:只记录日志流水(status=completed,不走pending同步)
|
||||
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'cdk_redeem_remote', :amount, :reason, :detail, 'completed', :time)");
|
||||
// 远程CDK:PHP也更新bond_cache + 写pending交易,与本地兑换逻辑一致
|
||||
// 注意:远程CDK已在sdf1侧标记used,PHP只需要同步债券
|
||||
if ($row) {
|
||||
$stmt = $db->prepare("UPDATE bond_cache SET amount = :amount, updated_at = :time WHERE player_name = :name");
|
||||
$stmt->bindValue(':amount', $newBalance, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$stmt->execute();
|
||||
} else {
|
||||
$stmt = $db->prepare("INSERT INTO bond_cache (player_name, amount, updated_at) VALUES (:name, :amount, :time)");
|
||||
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':amount', $newBalance, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
// 写pending交易,Java端pullPendingTransactions拉取后通知sdf1加券
|
||||
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'cdk_redeem', :amount, :reason, :detail, 'pending', :time)");
|
||||
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':amount', $cdk['amount'], SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':reason', "CDK远程兑换: {$code}", SQLITE3_TEXT);
|
||||
$stmt->bindValue(':detail', json_encode(['code' => $code, 'remote' => true], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
|
||||
$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', $now, SQLITE3_INTEGER);
|
||||
$stmt2->execute();
|
||||
}
|
||||
|
||||
$db->exec('COMMIT');
|
||||
|
||||
+93
-1
@@ -33,6 +33,8 @@ try {
|
||||
$adminActions = ['list_lands', 'list_shop', 'get_config', 'update_config', 'delete_land', 'update_land_owner', 'delete_shop_item'];
|
||||
// 玩家端action:需要token
|
||||
$playerActions = ['my_lands', 'land_detail', 'add_visitor', 'remove_visitor', 'list_visitors', 'land_shop', 'buy_permission'];
|
||||
// ★ 玩家端领地字段更新(效果管理、开关等)
|
||||
$playerFieldActions = ['update_land_field'];
|
||||
// ★ 玩家端权限操作action
|
||||
$playerPermActions = ['update_visitor_perm', 'get_visitor_perm'];
|
||||
// ★ 成员独立权限操作action(领地所有者编辑成员权限)
|
||||
@@ -62,7 +64,7 @@ try {
|
||||
echo json_encode(['success' => false, 'error' => '需要管理员认证']);
|
||||
exit;
|
||||
}
|
||||
} elseif (in_array($action, $playerActions) || in_array($action, $playerPermActions) || in_array($action, $memberPermActions)) {
|
||||
} elseif (in_array($action, $playerActions) || in_array($action, $playerPermActions) || in_array($action, $memberPermActions) || in_array($action, $playerFieldActions)) {
|
||||
// 玩家端:需要token
|
||||
$token = $_GET['token'] ?? '';
|
||||
if (empty($token)) {
|
||||
@@ -224,6 +226,15 @@ try {
|
||||
handleClearMemberPerm($db, $playerName, $_POST);
|
||||
break;
|
||||
|
||||
// ===== 玩家端:更新领地字段(效果管理、开关等)=====
|
||||
case 'update_land_field':
|
||||
if ($method !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
handleUpdateLandField($db, $playerName, $_POST);
|
||||
break;
|
||||
|
||||
// ===== Java端轮询PHP管理员变更 =====
|
||||
case 'poll_admin_changes':
|
||||
handlePollAdminChanges($db, $_GET);
|
||||
@@ -257,6 +268,14 @@ function initLandTables($db) {
|
||||
synced_at INTEGER DEFAULT 0
|
||||
)");
|
||||
|
||||
// ★ 效果管理字段迁移(兼容已有表)
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN clear_effects TEXT DEFAULT ''"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN give_effects TEXT DEFAULT ''"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN clear_all_bad_effects INTEGER DEFAULT 0"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN deny_all_effects INTEGER DEFAULT 0"); } catch (\Throwable $e) {}
|
||||
// ★ 管理变更标记
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN admin_changed INTEGER DEFAULT 0"); } catch (\Throwable $e) {}
|
||||
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS web_area_shop (
|
||||
id INTEGER PRIMARY KEY,
|
||||
land_id INTEGER NOT NULL,
|
||||
@@ -443,6 +462,20 @@ function handleUpdateLandOwner($db, $post) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 验证新所有者
|
||||
if (empty($owner)) {
|
||||
echo json_encode(['success' => false, 'error' => '新所有者不能为空']);
|
||||
return;
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_]{2,16}$/', $owner)) {
|
||||
echo json_encode(['success' => false, 'error' => '玩家名格式无效:仅允许英文字母、数字和下划线,2-16位']);
|
||||
return;
|
||||
}
|
||||
if (!playerExists($db, $owner)) {
|
||||
echo json_encode(['success' => false, 'error' => "玩家 §e{$owner} §f未注册,请确认后再试"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取旧所有者
|
||||
$stmt = $db->prepare("SELECT owner FROM web_area_lands WHERE name = :name");
|
||||
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
@@ -450,6 +483,11 @@ function handleUpdateLandOwner($db, $post) {
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
$oldOwner = $row ? ($row['owner'] ?? '') : '';
|
||||
|
||||
if ($oldOwner === $owner) {
|
||||
echo json_encode(['success' => false, 'error' => '新旧所有者相同,无需更改']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新所有者
|
||||
$stmt2 = $db->prepare("UPDATE web_area_lands SET owner = :owner WHERE name = :name");
|
||||
$stmt2->bindValue(':owner', $owner, SQLITE3_TEXT);
|
||||
@@ -667,6 +705,60 @@ function playerExists($db, $playerName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== 玩家端:更新领地字段(效果管理)====================
|
||||
|
||||
function handleUpdateLandField($db, $playerName, $post) {
|
||||
$name = $post['name'] ?? '';
|
||||
$field = $post['field'] ?? '';
|
||||
$value = $post['value'] ?? '';
|
||||
|
||||
if (empty($name) || empty($field)) {
|
||||
echo json_encode(['success' => false, 'error' => '缺少参数']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 允许的字段白名单
|
||||
$allowedFields = ['clear_effects', 'give_effects', 'clear_all_bad_effects', 'deny_all_effects'];
|
||||
if (!in_array($field, $allowedFields)) {
|
||||
echo json_encode(['success' => false, 'error' => '不允许的字段: ' . $field]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验领地所有权
|
||||
$stmt = $db->prepare("SELECT * FROM web_area_lands WHERE name = :name");
|
||||
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$result = $stmt->execute();
|
||||
$land = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$land) {
|
||||
echo json_encode(['success' => false, 'error' => '领地不存在']);
|
||||
return;
|
||||
}
|
||||
if ($land['owner'] !== $playerName) {
|
||||
echo json_encode(['success' => false, 'error' => '你不是此领地的所有者']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保字段存在(容错)
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN {$field} TEXT DEFAULT ''"); } catch (\Throwable $e) {}
|
||||
|
||||
// 更新
|
||||
$stmt2 = $db->prepare("UPDATE web_area_lands SET {$field} = :value, admin_changed = 1 WHERE name = :name");
|
||||
$stmt2->bindValue(':value', $value, SQLITE3_TEXT);
|
||||
$stmt2->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$stmt2->execute();
|
||||
|
||||
// 记录变更到变更队列(Java端轮询)
|
||||
$stmt3 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at) VALUES ('land_field_change', :lid, :name, :data, :now)");
|
||||
$stmt3->bindValue(':lid', (int)$land['id'], SQLITE3_INTEGER);
|
||||
$stmt3->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$stmt3->bindValue(':data', json_encode(['field' => $field, 'value' => $value]), SQLITE3_TEXT);
|
||||
$stmt3->bindValue(':now', time(), SQLITE3_INTEGER);
|
||||
$stmt3->execute();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
|
||||
// ==================== 玩家端:移除访客 ====================
|
||||
|
||||
function handleRemoveVisitor($db, $playerName, $req) {
|
||||
|
||||
+41
-2
@@ -457,10 +457,33 @@ function syncLands() {
|
||||
synced_at INTEGER DEFAULT 0
|
||||
)");
|
||||
|
||||
// ★ 效果迁移
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN clear_effects TEXT DEFAULT ''"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN give_effects TEXT DEFAULT ''"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN clear_all_bad_effects INTEGER DEFAULT 0"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN deny_all_effects INTEGER DEFAULT 0"); } catch (\Throwable $e) {}
|
||||
try { $db->exec("ALTER TABLE web_area_lands ADD COLUMN admin_changed INTEGER DEFAULT 0"); } catch (\Throwable $e) {}
|
||||
|
||||
$now = time();
|
||||
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_lands
|
||||
(id, name, owner, world, x1, z1, x2, z2, y_min, y_max, area_size, created_at, synced_at)
|
||||
VALUES (:id, :name, :owner, :world, :x1, :z1, :x2, :z2, :ymin, :ymax, :size, :created, :synced)");
|
||||
(id, name, owner, world, x1, z1, x2, z2, y_min, y_max, area_size, created_at, synced_at,
|
||||
peace_mode, peace_mode_duration, peace_whitelist, enforce_game_mode, mode_exempt,
|
||||
enter_msg, leave_msg, confiscate_msg, enable_announce, announce_template, txt_content,
|
||||
deny_block_break, deny_block_place, deny_fluid, deny_pvp, deny_fire_spread, deny_all_effects,
|
||||
deny_item_frame, deny_move, deny_pickup, deny_drop, deny_explosion, deny_fall_damage, deny_hunger,
|
||||
deny_all_damage, clear_effects, give_effects, clear_all_bad_effects, deny_all_effects,
|
||||
admin_changed, deny_thrown_projectiles, deny_glowing, deny_redstone_interaction, deny_door_interaction,
|
||||
deny_noteblock_jukebox, deny_lead, deny_crop_harvest, deny_wool_shear, deny_animal_feeding,
|
||||
warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world)
|
||||
VALUES (:id, :name, :owner, :world, :x1, :z1, :x2, :z2, :ymin, :ymax, :size, :created, :synced,
|
||||
0, :peace_dur, :peace_wl, :enforce_gm, :mode_exempt,
|
||||
:enter_msg, :leave_msg, :confiscate_msg, :announce, :announce_tpl, :txt_content,
|
||||
1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0,
|
||||
0, '', '', 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
:warp_x, :warp_y, :warp_z, :warp_yaw, :warp_pitch, :warp_world)");
|
||||
$count = 0;
|
||||
foreach ($lands as $land) {
|
||||
$stmt->bindValue(':id', (int)($land['id'] ?? 0), SQLITE3_INTEGER);
|
||||
@@ -476,6 +499,22 @@ function syncLands() {
|
||||
$stmt->bindValue(':size', (int)($land['area_size'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':created', (int)($land['created_at'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':peace_dur', (int)($land['peace_mode_duration'] ?? 5), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':peace_wl', $land['peace_whitelist'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':enforce_gm', $land['enforce_game_mode'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':mode_exempt', $land['mode_exempt'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':enter_msg', $land['enter_msg'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':leave_msg', $land['leave_msg'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':confiscate_msg', $land['confiscate_msg'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':announce', (int)($land['enable_announce'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':announce_tpl', $land['announce_template'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':txt_content', $land['txt_content'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':warp_x', (double)($land['warp_x'] ?? 0), SQLITE3_DOUBLE);
|
||||
$stmt->bindValue(':warp_y', (double)($land['warp_y'] ?? 0), SQLITE3_DOUBLE);
|
||||
$stmt->bindValue(':warp_z', (double)($land['warp_z'] ?? 0), SQLITE3_DOUBLE);
|
||||
$stmt->bindValue(':warp_yaw', (float)($land['warp_yaw'] ?? 0), SQLITE3_FLOAT);
|
||||
$stmt->bindValue(':warp_pitch', (float)($land['warp_pitch'] ?? 0), SQLITE3_FLOAT);
|
||||
$stmt->bindValue(':warp_world', $land['warp_world'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->execute();
|
||||
$count++;
|
||||
}
|
||||
|
||||
+255
-11
@@ -1195,23 +1195,46 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
if (IS_PREVIEW) { toast('预览模式下无法购买', 'error'); return; }
|
||||
if (stock == 0) { toast('商品已售罄', 'error'); return; }
|
||||
document.getElementById('modalTitle').textContent = '购买: ' + name;
|
||||
const needPwd = price > 1000;
|
||||
document.getElementById('modalBody').innerHTML = `
|
||||
<div class="row"><label>单价: ${price} 债券</label></div>
|
||||
<div class="row"><label>数量</label><input type="number" id="buyAmount" value="1" min="1" max="${stock > 0 ? stock : 64}"></div>
|
||||
<div class="row"><label>小计: <span id="buyTotal">${price}</span> 债券</label></div>
|
||||
<div class="row"><label>密码确认</label><input type="password" id="buyPassword" placeholder="输入游戏登录密码"></div>`;
|
||||
${needPwd ? '<div class="row"><label>密码确认</label><input type="password" id="buyPassword" placeholder="输入游戏登录密码"></div>' : '<div style="color:var(--dim);font-size:12px;text-align:center;margin-top:4px">小额交易(≤1000债券),免密码确认</div>'}`;
|
||||
document.getElementById('modalConfirm').onclick = () => doBuy(id);
|
||||
document.getElementById('buyAmount').oninput = function() { document.getElementById('buyTotal').textContent = this.value * price; };
|
||||
document.getElementById('buyAmount').oninput = function() {
|
||||
const total = this.value * price;
|
||||
document.getElementById('buyTotal').textContent = total;
|
||||
// 动态切换密码框
|
||||
const pwdRow = document.getElementById('buyPassword');
|
||||
const hintEl = document.querySelector('#modalBody > div:last-child');
|
||||
if (total > 1000 && !pwdRow) {
|
||||
// 需要密码框
|
||||
const newDiv = document.createElement('div');
|
||||
newDiv.className = 'row';
|
||||
newDiv.innerHTML = '<label>密码确认</label><input type="password" id="buyPassword" placeholder="输入游戏登录密码">';
|
||||
document.getElementById('buyTotal').parentElement.parentElement.insertAdjacentElement('afterend', newDiv);
|
||||
if (hintEl) hintEl.remove();
|
||||
} else if (total <= 1000 && pwdRow) {
|
||||
// 不需要密码框
|
||||
pwdRow.parentElement.remove();
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = 'color:var(--dim);font-size:12px;text-align:center;margin-top:4px';
|
||||
hint.textContent = '小额交易(≤1000债券),免密码确认';
|
||||
document.getElementById('buyTotal').parentElement.parentElement.insertAdjacentElement('afterend', hint);
|
||||
}
|
||||
};
|
||||
document.getElementById('modalOverlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function doBuy(itemId) {
|
||||
const amount = parseInt(document.getElementById('buyAmount').value);
|
||||
const password = document.getElementById('buyPassword').value;
|
||||
if (!password) { toast('请输入密码确认', 'error'); return; }
|
||||
const total = parseInt(document.getElementById('buyTotal').textContent);
|
||||
const password = document.getElementById('buyPassword')?.value || '';
|
||||
if (total > 1000 && !password) { toast('请输入密码确认', 'error'); return; }
|
||||
const res = await fetch(API + 'shop.php?action=buy', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({token: TOKEN, item_id: itemId, amount: amount, player: currentPlayer, password: password})
|
||||
body: JSON.stringify({token: TOKEN, item_id: itemId, amount: amount, player: currentPlayer, password: password || undefined})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }
|
||||
@@ -1314,7 +1337,6 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
${IS_PREVIEW ? '<div class="preview-badge" style="margin-bottom:12px">预览模式 - 兑换不会生效</div>' : ''}
|
||||
<div class="cdk-input" style="flex-direction:column;gap:8px">
|
||||
<input type="text" id="cdkCode" placeholder="请输入CDK兑换码..." maxlength="64">
|
||||
<input type="password" id="cdkPassword" placeholder="输入游戏登录密码" style="width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;box-sizing:border-box">
|
||||
<button class="btn btn-green" onclick="doCDKExchange()">兑换</button>
|
||||
</div>
|
||||
<div id="cdkResult" style="margin-top:12px"></div>
|
||||
@@ -1323,12 +1345,10 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
|
||||
async function doCDKExchange() {
|
||||
const code = document.getElementById('cdkCode').value.trim();
|
||||
const password = document.getElementById('cdkPassword').value;
|
||||
if (!code) { toast('请输入CDK码', 'error'); return; }
|
||||
if (!password) { toast('请输入密码确认', 'error'); return; }
|
||||
const res = await fetch(API + 'cdk.php?action=exchange', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({token: TOKEN, code: code, player: currentPlayer, password: password})
|
||||
body: JSON.stringify({token: TOKEN, code: code, player: currentPlayer})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }
|
||||
@@ -2248,7 +2268,6 @@ async function renderLandDetail(el, landName) {
|
||||
html += `<div style="margin-bottom:16px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<h3 style="margin:0;color:var(--fg)">👥 访客管理 (${visitors.length})</h3>
|
||||
<button class="btn" style="font-size:12px;padding:4px 12px" onclick="landsState.view='member_perms';landsState.currentLand='${escHtml(land.name)}';renderLands(document.getElementById('content'))">🎯 成员权限</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:12px">
|
||||
<input type="text" id="newVisitorName" placeholder="输入玩家名" style="flex:1;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px">
|
||||
@@ -2262,7 +2281,7 @@ async function renderLandDetail(el, landName) {
|
||||
for (const v of visitors) {
|
||||
const grantDate = v.granted_at ? new Date(v.granted_at * 1000).toLocaleString('zh-CN') : '未知';
|
||||
html += `<tr>
|
||||
<td><strong>${escHtml(v.player_name)}</strong></td>
|
||||
<td><strong>${escHtml(v.player_name)}</strong> <button class="btn" style="font-size:10px;padding:1px 6px;color:var(--accent);border-color:var(--accent)" onclick="event.stopPropagation();landsState.view='member_perm_detail';landsState.currentLand='${escHtml(land.name)}';landsState.currentMember='${escHtml(v.player_name)}';renderLands(document.getElementById('content'))">权限</button></td>
|
||||
<td><span style="color:var(--accent)">${escHtml(v.role)}</span></td>
|
||||
<td style="font-size:12px;color:var(--dim)">${grantDate}</td>
|
||||
<td><button class="btn" style="color:var(--red);font-size:11px;padding:3px 8px" onclick="removeLandVisitor('${escHtml(land.name)}','${escHtml(v.player_name)}')">移除</button></td>
|
||||
@@ -2271,6 +2290,75 @@ async function renderLandDetail(el, landName) {
|
||||
html += `</tbody></table>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
|
||||
// ★★★ 效果管理 ★★★
|
||||
const clearEffects = land.clear_effects ? JSON.parse(land.clear_effects) : [];
|
||||
const giveEffects = land.give_effects ? JSON.parse(land.give_effects) : [];
|
||||
const clearAllBad = !!parseInt(land.clear_all_bad_effects || 0);
|
||||
const denyAll = !!parseInt(land.deny_all_effects || 0);
|
||||
|
||||
html += `<div class="card" style="margin-top:12px">
|
||||
<h3 style="margin:0 0 12px;color:var(--fg)">✨ 效果管理</h3>
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
|
||||
<span onclick="toggleLandField('${escHtml(land.name)}','clear_all_bad_effects')"
|
||||
style="padding:6px 14px;border-radius:20px;font-size:12px;cursor:pointer;transition:all 0.2s;
|
||||
background:${clearAllBad ? 'var(--green)' : 'var(--bg)'};color:${clearAllBad ? '#fff' : 'var(--dim)'};
|
||||
border:1px solid ${clearAllBad ? 'var(--green)' : 'var(--border)'}">
|
||||
🧹 全清负面 ${clearAllBad ? '✓' : ''}
|
||||
</span>
|
||||
<span onclick="toggleLandField('${escHtml(land.name)}','deny_all_effects')"
|
||||
style="padding:6px 14px;border-radius:20px;font-size:12px;cursor:pointer;transition:all 0.2s;
|
||||
background:${denyAll ? 'var(--accent)' : 'var(--bg)'};color:${denyAll ? '#fff' : 'var(--dim)'};
|
||||
border:1px solid ${denyAll ? 'var(--accent)' : 'var(--border)'}">
|
||||
🚫 禁止所有效果 ${denyAll ? '✓' : ''}
|
||||
</span>
|
||||
</div>`;
|
||||
|
||||
// 清除效果列表
|
||||
html += `<div style="margin-bottom:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<span style="color:var(--fg);font-size:13px;font-weight:500">清除效果 (${clearEffects.length})</span>
|
||||
<button class="btn" style="font-size:11px;padding:2px 8px" onclick="addClearEffect('${escHtml(land.name)}')">+ 添加</button>
|
||||
</div>`;
|
||||
if (clearEffects.length === 0) {
|
||||
html += `<p style="color:var(--dim);font-size:12px">暂无清除效果</p>`;
|
||||
} else {
|
||||
html += `<div style="display:flex;gap:6px;flex-wrap:wrap">`;
|
||||
for (const eff of clearEffects) {
|
||||
html += `<span style="background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:12px;padding:3px 10px;font-size:12px;color:var(--fg)">
|
||||
${escHtml(eff)}
|
||||
<span onclick="removeLandEffect('${escHtml(land.name)}','clear_effects','${escHtml(eff)}')" style="cursor:pointer;color:var(--red);margin-left:4px">✕</span>
|
||||
</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
// 增益效果列表
|
||||
html += `<div style="margin-bottom:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<span style="color:var(--fg);font-size:13px;font-weight:500">增益效果 (${giveEffects.length})</span>
|
||||
<button class="btn" style="font-size:11px;padding:2px 8px" onclick="addGiveEffect('${escHtml(land.name)}')">+ 添加</button>
|
||||
</div>`;
|
||||
if (giveEffects.length === 0) {
|
||||
html += `<p style="color:var(--dim);font-size:12px">暂无增益效果</p>`;
|
||||
} else {
|
||||
html += `<div style="display:flex;gap:6px;flex-wrap:wrap">`;
|
||||
for (const eff of giveEffects) {
|
||||
const effName = Array.isArray(eff) ? eff[0] : eff;
|
||||
const effLevel = Array.isArray(eff) && eff[1] ? ' Lv' + eff[1] : '';
|
||||
const effDur = Array.isArray(eff) && eff[2] ? ' ' + eff[2] + 's' : '';
|
||||
html += `<span style="background:rgba(34,197,94,0.1);border:1px solid rgba(34,197,94,0.3);border-radius:12px;padding:3px 10px;font-size:12px;color:var(--fg)">
|
||||
${escHtml(effName)}${effLevel}${effDur}
|
||||
<span onclick="removeLandEffect('${escHtml(land.name)}','give_effects','${escHtml(Array.isArray(eff) ? JSON.stringify(eff) : eff)}')" style="cursor:pointer;color:var(--red);margin-left:4px">✕</span>
|
||||
</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
el.innerHTML = html;
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
|
||||
@@ -2380,6 +2468,162 @@ function buyLandPermission(landName, itemId) {
|
||||
glassAlert('请在游戏中使用 /protect shop 购买领地权限', '🎮');
|
||||
}
|
||||
|
||||
// ★★★ 效果管理辅助函数 ★★★
|
||||
|
||||
async function toggleLandField(landName, field) {
|
||||
try {
|
||||
// 先读取当前状态
|
||||
const detailUrl = new URL(API + 'land_api.php', location.href);
|
||||
detailUrl.searchParams.set('action', 'land_detail');
|
||||
detailUrl.searchParams.set('token', TOKEN);
|
||||
detailUrl.searchParams.set('name', landName);
|
||||
const detailRes = await fetch(detailUrl);
|
||||
const detailData = await detailRes.json();
|
||||
if (!detailData.success) { glassAlert(detailData.error); return; }
|
||||
|
||||
const currentVal = !!parseInt(detailData.land[field] || 0);
|
||||
const newVal = currentVal ? '0' : '1';
|
||||
|
||||
const url = new URL(API + 'land_api.php', location.href);
|
||||
url.searchParams.set('action', 'update_land_field');
|
||||
url.searchParams.set('token', TOKEN);
|
||||
const body = new URLSearchParams();
|
||||
body.set('name', landName);
|
||||
body.set('field', field);
|
||||
body.set('value', newVal);
|
||||
const res = await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: body.toString() });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
landsState.currentLand = landName;
|
||||
await renderLandDetail(document.getElementById('content'), landName);
|
||||
} else {
|
||||
glassAlert(data.error || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
glassAlert('操作失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function addClearEffect(landName) {
|
||||
const eff = await glassPrompt('输入要清除的效果英文名', '', '如: SLOWNESS, WEAKNESS, POISON');
|
||||
if (!eff || eff === false) return;
|
||||
const trimmed = eff.trim().toUpperCase();
|
||||
if (!trimmed) return;
|
||||
|
||||
try {
|
||||
const url = new URL(API + 'land_api.php', location.href);
|
||||
const detailUrl = new URL(API + 'land_api.php', location.href);
|
||||
detailUrl.searchParams.set('action', 'land_detail');
|
||||
detailUrl.searchParams.set('token', TOKEN);
|
||||
detailUrl.searchParams.set('name', landName);
|
||||
const detailRes = await fetch(detailUrl);
|
||||
const detailData = await detailRes.json();
|
||||
if (!detailData.success) { glassAlert(detailData.error); return; }
|
||||
const current = detailData.land.clear_effects ? JSON.parse(detailData.land.clear_effects) : [];
|
||||
if (current.includes(trimmed)) { glassAlert('该效果已在清除列表中'); return; }
|
||||
current.push(trimmed);
|
||||
|
||||
const body = new URLSearchParams();
|
||||
body.set('name', landName);
|
||||
body.set('field', 'clear_effects');
|
||||
body.set('value', JSON.stringify(current));
|
||||
const res = await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: body.toString() });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
landsState.currentLand = landName;
|
||||
await renderLandDetail(document.getElementById('content'), landName);
|
||||
} else {
|
||||
glassAlert(data.error || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
glassAlert('操作失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function addGiveEffect(landName) {
|
||||
const eff = await glassPrompt('输入效果英文名', '', '如: SPEED, STRENGTH, REGENERATION');
|
||||
if (!eff || eff === false) return;
|
||||
const trimmed = eff.trim().toUpperCase();
|
||||
if (!trimmed) return;
|
||||
|
||||
const levelStr = await glassPrompt('等级 (1-255)', '1', '默认1级,管理员最高255级');
|
||||
if (levelStr === null || levelStr === false) return;
|
||||
const level = parseInt(levelStr) || 1;
|
||||
|
||||
const durStr = await glassPrompt('时长(秒)', '300', '默认300秒');
|
||||
if (durStr === null || durStr === false) return;
|
||||
const duration = parseInt(durStr) || 300;
|
||||
|
||||
const entry = JSON.stringify([trimmed, Math.min(Math.max(level,1),255), Math.min(Math.max(duration,1),3600)]);
|
||||
|
||||
try {
|
||||
const url = new URL(API + 'land_api.php', location.href);
|
||||
const detailUrl = new URL(API + 'land_api.php', location.href);
|
||||
detailUrl.searchParams.set('action', 'land_detail');
|
||||
detailUrl.searchParams.set('token', TOKEN);
|
||||
detailUrl.searchParams.set('name', landName);
|
||||
const detailRes = await fetch(detailUrl);
|
||||
const detailData = await detailRes.json();
|
||||
if (!detailData.success) { glassAlert(detailData.error); return; }
|
||||
const current = detailData.land.give_effects ? JSON.parse(detailData.land.give_effects) : [];
|
||||
current.push([trimmed, Math.min(Math.max(level,1),255), Math.min(Math.max(duration,1),3600)]);
|
||||
|
||||
const body = new URLSearchParams();
|
||||
body.set('name', landName);
|
||||
body.set('field', 'give_effects');
|
||||
body.set('value', JSON.stringify(current));
|
||||
const res = await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: body.toString() });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
landsState.currentLand = landName;
|
||||
await renderLandDetail(document.getElementById('content'), landName);
|
||||
} else {
|
||||
glassAlert(data.error || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
glassAlert('操作失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLandEffect(landName, field, effValue) {
|
||||
try {
|
||||
const url = new URL(API + 'land_api.php', location.href);
|
||||
const detailUrl = new URL(API + 'land_api.php', location.href);
|
||||
detailUrl.searchParams.set('action', 'land_detail');
|
||||
detailUrl.searchParams.set('token', TOKEN);
|
||||
detailUrl.searchParams.set('name', landName);
|
||||
const detailRes = await fetch(detailUrl);
|
||||
const detailData = await detailRes.json();
|
||||
if (!detailData.success) { glassAlert(detailData.error); return; }
|
||||
|
||||
let current = detailData.land[field] ? JSON.parse(detailData.land[field]) : [];
|
||||
if (field === 'clear_effects') {
|
||||
current = current.filter(e => e !== effValue);
|
||||
} else {
|
||||
// give_effects: try matching by name or full JSON
|
||||
current = current.filter(e => {
|
||||
const jsonStr = JSON.stringify(e);
|
||||
return jsonStr !== effValue && e[0] !== effValue;
|
||||
});
|
||||
}
|
||||
|
||||
const body = new URLSearchParams();
|
||||
body.set('name', landName);
|
||||
body.set('field', field);
|
||||
body.set('value', JSON.stringify(current));
|
||||
const res = await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: body.toString() });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
landsState.currentLand = landName;
|
||||
await renderLandDetail(document.getElementById('content'), landName);
|
||||
} else {
|
||||
glassAlert(data.error || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
glassAlert('操作失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderMemberPermDetail(el, landName, targetPlayer) {
|
||||
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载权限详情...</p></div>`;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user