feat: 游客模式毛玻璃登录弹窗 + C位登录按钮 + 60秒定时弹窗

- admin.php: 修复go()缺失reset_requests路由(黑屏bug)
- player.php: 毛玻璃登录弹窗(backdrop-filter:blur)
- player.php: 游客模式C位大按钮(脉冲动画)
- player.php: 60秒定时弹窗(未登录自动弹出)
- sync.php: 添加领地同步接口(sync_lands/sync_land_shop)
- land_api.php: 领地管理API(新建)
This commit is contained in:
ypsdf1
2026-06-22 22:56:06 +08:00
parent 66cc7b8925
commit ed74d09b6a
4 changed files with 816 additions and 17 deletions
+210 -17
View File
@@ -92,6 +92,7 @@
<div class="si" data-p="active" onclick="go('active')">⏱️ 活跃用户</div>
<div class="si" data-p="reset_requests" onclick="go('reset_requests')">🔑 密码重置审核</div>
<div class="si" data-p="tickets" onclick="go('tickets')">📋 工单管理</div>
<div class="si" data-p="lands" onclick="go('lands')">🏡 领地管理</div></div>
</div>
<div class="content" id="C"></div>
</div>
@@ -152,7 +153,9 @@ function go(p) {
else if (p==='users') loadUsers(c);
else if (p==='online') loadOnlinePlayers(c);
else if (p==='active') loadActivePlayers(c);
else if (p==='reset_requests') loadResetRequests(c);
else if (p==='tickets') loadTickets(c);
else if (p==='lands') loadLands(c);
}
// 检查登录状态
@@ -1716,27 +1719,40 @@ async function doLogout() {
}
// ===== 模态框输入(替代prompt)=====
function showModal(title, message, defaultValue) {
function showModal(title, message, defaultValue, customHtml) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;justify-content:center;align-items:center;z-index:1000';
overlay.innerHTML = `
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:400px;max-width:90%">
<h3 style="margin-bottom:12px">${title}</h3>
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">${message}</p>
<input type="text" id="modalInput" value="${defaultValue}" style="width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;box-sizing:border-box">
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn" id="modalCancel">取消</button>
<button class="btn btn-blue" id="modalConfirm">确认</button>
</div>
</div>`;
if (customHtml) {
overlay.innerHTML = `
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:400px;max-width:90%">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0">${title}</h3>
<span class="modal-close" style="cursor:pointer;font-size:18px;color:var(--dim)" onclick="this.closest('div[style*=fixed]').remove()">✕</span>
</div>
${customHtml}
</div>`;
} else {
overlay.innerHTML = `
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:400px;max-width:90%">
<h3 style="margin-bottom:12px">${title}</h3>
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">${message}</p>
<input type="text" id="modalInput" value="${defaultValue}" style="width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;box-sizing:border-box">
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn" id="modalCancel">取消</button>
<button class="btn btn-blue" id="modalConfirm">确认</button>
</div>
</div>`;
}
document.body.appendChild(overlay);
const input = document.getElementById('modalInput');
input.focus();
input.select();
input.addEventListener('keydown', e => { if (e.key === 'Enter') { resolve(input.value); overlay.remove(); } });
document.getElementById('modalCancel').onclick = () => { overlay.remove(); resolve(null); };
document.getElementById('modalConfirm').onclick = () => { resolve(input.value); overlay.remove(); };
if (!customHtml) {
const input = document.getElementById('modalInput');
input.focus();
input.select();
input.addEventListener('keydown', e => { if (e.key === 'Enter') { resolve(input.value); overlay.remove(); } });
document.getElementById('modalCancel').onclick = () => { overlay.remove(); resolve(null); };
document.getElementById('modalConfirm').onclick = () => { resolve(input.value); overlay.remove(); };
}
overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } };
});
}
@@ -2225,6 +2241,183 @@ function adminRenderMd(text) {
return s;
} catch(e) { console.error('[MD_RENDER] Error:', e); return escAdmHtml(text).replace(/\n/g, '<br>'); }
}
// ==================== 领地管理 ====================
async function loadLands(el) {
el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--dim)">加载中...</div>';
try {
const [landsRes, shopRes, configRes] = await Promise.all([
fetch('api/land_api.php?action=list_lands').then(r => r.json()),
fetch('api/land_api.php?action=list_shop').then(r => r.json()),
fetch('api/land_api.php?action=get_config').then(r => r.json())
]);
let html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">';
html += '<h2 style="margin:0;color:var(--fg)">🏡 领地管理</h2>';
html += '<div style="display:flex;gap:8px">';
html += `<button onclick="showLandConfig()" style="padding:6px 12px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px">⚙️ 配置</button>`;
html += `<button onclick="loadLands(document.getElementById('C'))" style="padding:6px 12px;background:var(--card);color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:12px">🔄 刷新</button>`;
html += '</div></div>';
// 配置信息
const cfg = configRes.config || {};
html += `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:20px">`;
const lands = landsRes.lands || [];
html += `<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center">
<div style="font-size:24px;font-weight:bold;color:var(--accent)">${lands.length}</div>
<div style="font-size:12px;color:var(--dim);margin-top:4px">总领地数</div>
</div>`;
const shopItems = (shopRes.items || []).filter(i => i.status === 'active' && !i.buyer);
html += `<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center">
<div style="font-size:24px;font-weight:bold;color:#4caf50">${shopItems.length}</div>
<div style="font-size:12px;color:var(--dim);margin-top:4px">在售权限</div>
</div>`;
const pricePerSqM = cfg.land_price_per_sqm || '10';
const maxLands = cfg.max_lands || '5';
html += `<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center">
<div style="font-size:24px;font-weight:bold;color:#ff9800">${pricePerSqM}</div>
<div style="font-size:12px;color:var(--dim);margin-top:4px">每平米价格(债券)</div>
</div>`;
html += `<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center">
<div style="font-size:24px;font-weight:bold;color:#e91e63">${maxLands}</div>
<div style="font-size:12px;color:var(--dim);margin-top:4px">最大领地数/人</div>
</div>`;
html += '</div>';
// 领地列表
html += '<h3 style="color:var(--fg);margin-bottom:8px">📍 领地列表</h3>';
if (lands.length === 0) {
html += '<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--dim)">暂无领地数据<br><span style="font-size:11px">Java端会自动同步领地数据到此</span></div>';
} else {
html += '<div style="overflow-x:auto"><table class="table"><tr>';
html += '<th>ID</th><th>名称</th><th>所有者</th><th>世界</th><th>坐标</th><th>面积</th><th>操作</th>';
html += '</tr>';
for (const l of lands) {
const size = l.area_size || Math.abs((l.x2-l.x1+1)*(l.z2-l.z1+1));
html += `<tr>
<td>${l.id}</td>
<td><strong>${escAdmHtml(l.name)}</strong></td>
<td>${escAdmHtml(l.owner || '无')}</td>
<td>${escAdmHtml(l.world)}</td>
<td style="font-size:11px">${l.x1},${l.z1} → ${l.x2},${l.z2}</td>
<td>${size} 格²</td>
<td>
<button onclick="changeLandOwner('${escAdmHtml(l.name)}','${escAdmHtml(l.owner)}')" style="padding:2px 8px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">改主</button>
<button onclick="deleteLand('${escAdmHtml(l.name)}')" style="padding:2px 8px;background:#f44336;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">删除</button>
</td>
</tr>`;
}
html += '</table></div>';
}
// 权限商店
html += '<h3 style="color:var(--fg);margin:20px 0 8px">🛒 权限商店</h3>';
const allShop = shopRes.items || [];
if (allShop.length === 0) {
html += '<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--dim)">暂无权限商品</div>';
} else {
html += '<div style="overflow-x:auto"><table class="table"><tr>';
html += '<th>ID</th><th>领地</th><th>卖家</th><th>价格</th><th>时长</th><th>状态</th><th>买家</th><th>操作</th>';
html += '</tr>';
for (const s of allShop) {
const dur = s.duration >= 86400 ? Math.floor(s.duration/86400)+'天' : s.duration >= 3600 ? Math.floor(s.duration/3600)+'小时' : Math.floor(s.duration/60)+'分钟';
const statusColor = s.status === 'active' ? '#4caf50' : s.status === 'sold' ? '#ff9800' : '#f44336';
html += `<tr>
<td>${s.id}</td>
<td>${escAdmHtml(s.land_name)}</td>
<td>${escAdmHtml(s.seller)}</td>
<td style="color:#ff9800">${s.price}💰</td>
<td>${dur}</td>
<td style="color:${statusColor}">${s.status === 'active' ? '在售' : s.status === 'sold' ? '已售' : '已下架'}</td>
<td>${s.buyer ? escAdmHtml(s.buyer) : '-'}</td>
<td>
${s.status === 'active' ? `<button onclick="deleteShopItem(${s.id})" style="padding:2px 8px;background:#f44336;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">下架</button>` : ''}
</td>
</tr>`;
}
html += '</table></div>';
}
el.innerHTML = html;
} catch(e) {
console.error('[LANDS] Error:', e);
el.innerHTML = `<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--dim)">加载失败: ${escAdmHtml(e.message)}</div>`;
}
}
function showLandConfig() {
const html = `<div style="padding:16px">
<h3 style="margin:0 0 12px;color:var(--fg)">⚙️ 领地配置</h3>
<div style="margin-bottom:12px">
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">每平米价格(债券)</label>
<input id="cfgPrice" type="number" value="10" min="1" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
<div style="margin-bottom:12px">
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">每人最大领地数</label>
<input id="cfgMaxLands" type="number" value="5" min="1" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
<button onclick="saveLandConfig()" style="width:100%;padding:8px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer">保存</button>
</div>`;
showModal('领地配置', '', null, html);
}
async function saveLandConfig() {
const price = document.getElementById('cfgPrice')?.value || '10';
const maxLands = document.getElementById('cfgMaxLands')?.value || '5';
try {
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=land_price_per_sqm&value=' + encodeURIComponent(price)
});
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=max_lands&value=' + encodeURIComponent(maxLands)
});
document.querySelector('.modal-close')?.click();
loadLands(document.getElementById('C'));
} catch(e) {
alert('保存失败: ' + e.message);
}
}
function changeLandOwner(name, currentOwner) {
const newOwner = prompt('领地 [' + name + '] 当前所有者: ' + (currentOwner||'无') + '\n输入新所有者:', currentOwner);
if (newOwner === null || newOwner === currentOwner) 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)
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else alert('失败: ' + (d.error||''));
});
}
function deleteLand(name) {
if (!confirm('确定删除领地 [' + name + '] ?')) return;
fetch('api/land_api.php?action=delete_land', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name)
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else alert('失败: ' + (d.error||''));
});
}
function deleteShopItem(id) {
if (!confirm('确定下架商品 #' + id + ' ?')) return;
fetch('api/land_api.php?action=delete_shop_item', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'id=' + id
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else alert('失败: ' + (d.error||''));
});
}
</script>
</body>
</html>
+289
View File
@@ -0,0 +1,289 @@
<?php
/**
* 领地系统API - 接收Java端领地数据 + 管理面板
* POST: Java端推送领地数据
* GET: admin面板读取领地数据
*/
header('Content-Type: application/json; charset=utf-8');
error_reporting(E_ERROR | E_PARSE);
try {
// ★ 加载core.php(与sync.php相同的加载方式)
$coreFile = dirname(__DIR__) . '/core.php';
if (!file_exists($coreFile)) {
$coreFile = dirname(__DIR__) . '/plugin/core.php';
}
require_once $coreFile;
$action = $_GET['action'] ?? $_POST['action'] ?? '';
$secret = $_GET['secret'] ?? $_POST['secret'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
// ===== 验证 =====
if ($method === 'POST') {
if (!validateSecret($secret)) {
echo json_encode(['success' => false, 'error' => 'invalid secret']);
exit;
}
}
$db = getDB();
// 建表
initLandTables($db);
switch ($action) {
// ===== Java端推送领地数据 =====
case 'sync_lands':
handleSyncLands($db, $_POST);
break;
// ===== Java端推送权限商店数据 =====
case 'sync_shop':
handleSyncShop($db, $_POST);
break;
// ===== 管理面板:获取所有领地 =====
case 'list_lands':
if ($method !== 'GET') {
echo json_encode(['success' => false, 'error' => 'GET only']);
exit;
}
handleListLands($db);
break;
// ===== 管理面板:获取权限商店 =====
case 'list_shop':
if ($method !== 'GET') {
echo json_encode(['success' => false, 'error' => 'GET only']);
exit;
}
handleListShop($db);
break;
// ===== 管理面板:获取配置 =====
case 'get_config':
handleGetConfig($db);
break;
// ===== 管理面板:更新配置 =====
case 'update_config':
handleUpdateConfig($db, $_POST);
break;
// ===== 管理面板:删除领地 =====
case 'delete_land':
handleDeleteLand($db, $_POST);
break;
// ===== 管理面板:更新领地所有者 =====
case 'update_land_owner':
handleUpdateLandOwner($db, $_POST);
break;
// ===== 管理面板:删除权限商品 =====
case 'delete_shop_item':
handleDeleteShopItem($db, $_POST);
break;
default:
echo json_encode(['success' => false, 'error' => 'unknown action']);
}
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
// ==================== 函数 ====================
function initLandTables($db) {
$db->exec("CREATE TABLE IF NOT EXISTS web_area_lands (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
owner TEXT DEFAULT '',
world TEXT DEFAULT '',
x1 INTEGER DEFAULT 0, z1 INTEGER DEFAULT 0,
x2 INTEGER DEFAULT 0, z2 INTEGER DEFAULT 0,
y_min INTEGER DEFAULT 0, y_max INTEGER DEFAULT 255,
area_size INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_area_shop (
id INTEGER PRIMARY KEY,
land_id INTEGER NOT NULL,
land_name TEXT DEFAULT '',
seller TEXT NOT NULL,
permission TEXT DEFAULT 'visitor',
price INTEGER DEFAULT 0,
duration INTEGER DEFAULT 86400,
status TEXT DEFAULT 'active',
buyer TEXT DEFAULT '',
bought_at INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_area_config (
key TEXT PRIMARY KEY,
value TEXT DEFAULT ''
)");
}
function handleSyncLands($db, $post) {
$lands = json_decode($post['lands'] ?? '[]', true);
if (!is_array($lands)) {
echo json_encode(['success' => false, 'error' => 'invalid lands data']);
return;
}
$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)");
foreach ($lands as $land) {
$stmt->bindValue(':id', (int)($land['id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':name', $land['name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':owner', $land['owner'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':world', $land['world'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':x1', (int)($land['x1'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':z1', (int)($land['z1'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':x2', (int)($land['x2'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':z2', (int)($land['z2'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':ymin', (int)($land['y_min'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':ymax', (int)($land['y_max'] ?? 255), SQLITE3_INTEGER);
$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->execute();
}
echo json_encode(['success' => true, 'count' => count($lands)]);
}
function handleSyncShop($db, $post) {
$items = json_decode($post['items'] ?? '[]', true);
if (!is_array($items)) {
echo json_encode(['success' => false, 'error' => 'invalid shop data']);
return;
}
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_shop
(id, land_id, land_name, seller, permission, price, duration, status, buyer, bought_at, created_at, synced_at)
VALUES (:id, :land_id, :land_name, :seller, :perm, :price, :dur, :status, :buyer, :bought, :created, :synced)");
foreach ($items as $item) {
$stmt->bindValue(':id', (int)($item['id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_id', (int)($item['land_id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_name', $item['land_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':seller', $item['seller'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':perm', $item['permission'] ?? 'visitor', SQLITE3_TEXT);
$stmt->bindValue(':price', (int)($item['price'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':dur', (int)($item['duration'] ?? 86400), SQLITE3_INTEGER);
$stmt->bindValue(':status', $item['status'] ?? 'active', SQLITE3_TEXT);
$stmt->bindValue(':buyer', $item['buyer'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':bought', (int)($item['bought_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':created', (int)($item['created_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
}
echo json_encode(['success' => true, 'count' => count($items)]);
}
function handleListLands($db) {
$result = $db->query("SELECT * FROM web_area_lands ORDER BY synced_at DESC");
$lands = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$lands[] = $row;
}
echo json_encode(['success' => true, 'lands' => $lands]);
}
function handleListShop($db) {
$result = $db->query("SELECT * FROM web_area_shop ORDER BY created_at DESC LIMIT 100");
$items = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$items[] = $row;
}
echo json_encode(['success' => true, 'items' => $items]);
}
function handleGetConfig($db) {
$config = [];
$result = $db->query("SELECT * FROM web_area_config");
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$config[$row['key']] = $row['value'];
}
echo json_encode(['success' => true, 'config' => $config]);
}
function handleUpdateConfig($db, $post) {
$key = $post['key'] ?? '';
$value = $post['value'] ?? '';
if (empty($key)) {
echo json_encode(['success' => false, 'error' => 'missing key']);
return;
}
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_config (key, value) VALUES (:key, :val)");
$stmt->bindValue(':key', $key, SQLITE3_TEXT);
$stmt->bindValue(':val', $value, SQLITE3_TEXT);
$stmt->execute();
echo json_encode(['success' => true]);
}
function handleDeleteLand($db, $post) {
$name = $post['name'] ?? '';
if (empty($name)) {
echo json_encode(['success' => false, 'error' => 'missing name']);
return;
}
$stmt = $db->prepare("DELETE FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
$stmt->execute();
echo json_encode(['success' => true]);
}
function handleUpdateLandOwner($db, $post) {
$name = $post['name'] ?? '';
$owner = $post['owner'] ?? '';
if (empty($name)) {
echo json_encode(['success' => false, 'error' => 'missing name']);
return;
}
$stmt = $db->prepare("UPDATE web_area_lands SET owner = :owner WHERE name = :name");
$stmt->bindValue(':owner', $owner, SQLITE3_TEXT);
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
$stmt->execute();
echo json_encode(['success' => true]);
}
function handleDeleteShopItem($db, $post) {
$id = (int)($post['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'error' => 'invalid id']);
return;
}
$stmt = $db->prepare("UPDATE web_area_shop SET status = 'removed' WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
echo json_encode(['success' => true]);
}
function validateSecret($secret) {
if (empty($secret)) return false;
$validSecrets = [
'sdf1_web_comm_2026_ypshidifu'
];
return in_array($secret, $validSecrets);
}
+106
View File
@@ -299,6 +299,12 @@ switch ($action) {
case 'push_cdk_validate_result':
pushCdkValidateResult();
break;
case 'sync_lands':
syncLands();
break;
case 'sync_land_shop':
syncLandShop();
break;
default:
error('未知操作: ' . $action);
}
@@ -422,6 +428,106 @@ function syncServiceProviders() {
}
}
// ===== 领地数据同步 =====
function syncLands() {
$secret = getParam('secret');
if ($secret !== SECRET_KEY) error('密钥验证失败', 403);
$landsRaw = getParam('lands');
if (!$landsRaw) error('缺少lands参数');
$lands = is_array($landsRaw) ? $landsRaw : json_decode($landsRaw, true);
if (!is_array($lands)) error('lands格式无效');
$db = getDB();
// 建表
$db->exec("CREATE TABLE IF NOT EXISTS web_area_lands (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
owner TEXT DEFAULT '',
world TEXT DEFAULT '',
x1 INTEGER DEFAULT 0, z1 INTEGER DEFAULT 0,
x2 INTEGER DEFAULT 0, z2 INTEGER DEFAULT 0,
y_min INTEGER DEFAULT 0, y_max INTEGER DEFAULT 255,
area_size INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$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)");
$count = 0;
foreach ($lands as $land) {
$stmt->bindValue(':id', (int)($land['id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':name', $land['name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':owner', $land['owner'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':world', $land['world'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':x1', (int)($land['x1'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':z1', (int)($land['z1'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':x2', (int)($land['x2'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':z2', (int)($land['z2'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':ymin', (int)($land['y_min'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':ymax', (int)($land['y_max'] ?? 255), SQLITE3_INTEGER);
$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->execute();
$count++;
}
success("领地同步成功: {$count}个");
}
// ===== 领地权限商店同步 =====
function syncLandShop() {
$secret = getParam('secret');
if ($secret !== SECRET_KEY) error('密钥验证失败', 403);
$itemsRaw = getParam('items');
if (!$itemsRaw) error('缺少items参数');
$items = is_array($itemsRaw) ? $itemsRaw : json_decode($itemsRaw, true);
if (!is_array($items)) error('items格式无效');
$db = getDB();
$db->exec("CREATE TABLE IF NOT EXISTS web_area_shop (
id INTEGER PRIMARY KEY,
land_id INTEGER NOT NULL,
land_name TEXT DEFAULT '',
seller TEXT NOT NULL,
permission TEXT DEFAULT 'visitor',
price INTEGER DEFAULT 0,
duration INTEGER DEFAULT 86400,
status TEXT DEFAULT 'active',
buyer TEXT DEFAULT '',
bought_at INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_shop
(id, land_id, land_name, seller, permission, price, duration, status, buyer, bought_at, created_at, synced_at)
VALUES (:id, :land_id, :land_name, :seller, :perm, :price, :dur, :status, :buyer, :bought, :created, :synced)");
$count = 0;
foreach ($items as $item) {
$stmt->bindValue(':id', (int)($item['id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_id', (int)($item['land_id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_name', $item['land_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':seller', $item['seller'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':perm', $item['permission'] ?? 'visitor', SQLITE3_TEXT);
$stmt->bindValue(':price', (int)($item['price'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':dur', (int)($item['duration'] ?? 86400), SQLITE3_INTEGER);
$stmt->bindValue(':status', $item['status'] ?? 'active', SQLITE3_TEXT);
$stmt->bindValue(':buyer', $item['buyer'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':bought', (int)($item['bought_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':created', (int)($item['created_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
$count++;
}
success("权限商店同步成功: {$count}个");
}
// ===== 插件推送债券余额 =====
function syncBonds() {
// ★ 支持token或SECRET_KEY认证
+211
View File
@@ -147,6 +147,81 @@ if ($currentVersion !== $BUILD_VERSION) {
.toast.success { background: var(--green); color: #fff; }
.toast.error { background: var(--red); color: #fff; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* ★ 游客模式C位登录按钮 */
.guest-center-login {
display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
z-index: 50; pointer-events: none;
}
.guest-center-login.show { display: flex; justify-content: center; align-items: center; }
.guest-center-login .glow-btn {
pointer-events: auto;
background: linear-gradient(135deg, var(--accent), #79c0ff);
color: #fff; border: none; border-radius: 16px;
padding: 20px 48px; font-size: 18px; font-weight: 700;
cursor: pointer; box-shadow: 0 8px 32px rgba(88,166,255,0.4);
animation: glowPulse 2s ease-in-out infinite;
transition: all 0.3s; text-decoration: none;
}
.guest-center-login .glow-btn:hover { transform: scale(1.08); box-shadow: 0 12px 48px rgba(88,166,255,0.6); }
@keyframes glowPulse {
0%,100% { box-shadow: 0 8px 32px rgba(88,166,255,0.4); }
50% { box-shadow: 0 8px 48px rgba(88,166,255,0.7); }
}
/* ★ 毛玻璃登录弹窗 */
.glass-login-overlay {
display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.35);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
z-index: 2000; justify-content: center; align-items: center;
animation: glassFadeIn 0.3s ease;
}
.glass-login-overlay.show { display: flex; }
.glass-card {
background: rgba(22,27,34,0.88);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(88,166,255,0.25);
border-radius: 20px; padding: 36px 32px; width: 400px; max-width: 90%;
box-shadow: 0 16px 64px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
animation: glassSlideUp 0.35s ease;
position: relative;
}
.glass-card .x-btn {
position: absolute; top: 12px; right: 16px; background: none; border: none;
color: var(--dim); font-size: 22px; cursor: pointer; padding: 4px 8px;
border-radius: 6px; transition: all 0.2s; line-height: 1;
}
.glass-card .x-btn:hover { color: var(--text); background: rgba(255,255,255,0.1); }
.glass-card .logo { text-align: center; font-size: 48px; margin-bottom: 16px; }
.glass-card h2 { text-align: center; color: var(--text); margin-bottom: 6px; font-size: 20px; }
.glass-card .subtitle { text-align: center; color: var(--dim); font-size: 13px; margin-bottom: 24px; }
.glass-card input[type=text], .glass-card input[type=password] {
width: 100%; padding: 12px 16px; background: rgba(13,17,23,0.6);
border: 1px solid rgba(88,166,255,0.2); border-radius: 10px;
color: var(--text); font-size: 14px; outline: none; margin-bottom: 12px;
transition: border-color 0.2s; box-sizing: border-box;
}
.glass-card input:focus { border-color: var(--accent); }
.glass-card .login-btn {
width: 100%; padding: 14px; background: linear-gradient(135deg, var(--accent), #79c0ff);
color: #fff; border: none; border-radius: 10px; font-size: 15px;
font-weight: 700; cursor: pointer; transition: all 0.2s; margin-top: 4px;
}
.glass-card .login-btn:hover { opacity: 0.9; transform: translateY(-1px); }
.glass-card .login-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.glass-card .hint { text-align: center; color: var(--dim); font-size: 12px; margin-top: 16px; }
.glass-card .hint code {
background: rgba(88,166,255,0.1); padding: 1px 5px; border-radius: 4px;
color: var(--accent); font-size: 11px;
}
.glass-card .status-msg {
text-align: center; padding: 8px; border-radius: 8px; font-size: 13px;
margin-bottom: 12px; display: none;
}
@keyframes glassFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes glassSlideUp {
from { opacity: 0; transform: translateY(30px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@media (max-width: 768px) {
.sidebar { display: none; }
.grid { grid-template-columns: 1fr; }
@@ -177,6 +252,26 @@ if ($currentVersion !== $BUILD_VERSION) {
</div>
</div>
<!-- ★ 游客模式C位登录按钮 -->
<div class="guest-center-login" id="guestCenterLogin">
<button class="glow-btn" onclick="openGlassLogin()">🔑 立即登录</button>
</div>
<!-- ★ 毛玻璃登录弹窗 -->
<div class="glass-login-overlay" id="glassLoginOverlay" onclick="closeGlassLogin(event)">
<div class="glass-card" onclick="event.stopPropagation()">
<button class="x-btn" onclick="closeGlassLogin()">&times;</button>
<div class="logo">🔐</div>
<h2>登录到SDF1</h2>
<p class="subtitle">使用游戏内密码登录,无需在游戏里敲指令</p>
<div class="status-msg" id="glassLoginStatus"></div>
<input type="text" id="glassPlayerName" placeholder="玩家名" maxlength="16" autocomplete="username">
<input type="password" id="glassPassword" placeholder="游戏内密码" maxlength="32" autocomplete="current-password">
<button class="login-btn" id="glassLoginBtn" onclick="doGlassLogin()">登 录</button>
<p class="hint">也可在游戏中输入 <code>/sdf1_login weblogin</code> 获取链接</p>
</div>
</div>
<div class="modal-overlay" id="modalOverlay" onclick="closeModal()">
<div class="modal" onclick="event.stopPropagation()">
<h3 id="modalTitle">确认</h3>
@@ -317,7 +412,9 @@ if ($currentVersion !== $BUILD_VERSION) {
currentPlayer = '';
document.getElementById('playerInfo').textContent = '游客模式';
document.getElementById('previewBadge').style.display = 'inline';
document.querySelector('.guest-center-login').classList.add('show');
showLoginPrompt();
startGuestLoginTimer();
if (callback) callback({ok: false, message: data.message});
}
} catch (e) {
@@ -329,7 +426,9 @@ if ($currentVersion !== $BUILD_VERSION) {
currentPlayer = '';
document.getElementById('playerInfo').textContent = '游客模式';
document.getElementById('previewBadge').style.display = 'inline';
document.querySelector('.guest-center-login').classList.add('show');
showLoginPrompt();
startGuestLoginTimer();
if (callback) callback({ok: false, message: e.message});
}
}
@@ -753,7 +852,119 @@ if ($currentVersion !== $BUILD_VERSION) {
document.getElementById('playerInfo').textContent = '游客模式';
document.getElementById('previewBadge').style.display = 'inline';
document.querySelector('.sidebar').style.display = 'block';
document.querySelector('.guest-center-login').classList.add('show');
switchPage('dashboard');
// ★ 启动60秒定时弹窗
startGuestLoginTimer();
}
// ==================== 毛玻璃登录弹窗 ====================
let guestLoginTimer = null;
let glassLoginDismissed = false;
function openGlassLogin() {
const overlay = document.getElementById('glassLoginOverlay');
overlay.classList.add('show');
glassLoginDismissed = false;
// 停止定时弹窗
if (guestLoginTimer) { clearInterval(guestLoginTimer); guestLoginTimer = null; }
setTimeout(() => {
const nameInput = document.getElementById('glassPlayerName');
const savedPlayer = localStorage.getItem('sdf1_player') || currentPlayer || '';
if (nameInput) { nameInput.value = savedPlayer; nameInput.focus(); }
}, 100);
}
function closeGlassLogin(e) {
if (e && e.target !== document.getElementById('glassLoginOverlay')) return;
document.getElementById('glassLoginOverlay').classList.remove('show');
glassLoginDismissed = true;
glassLoginReset();
// 重新启动定时弹窗
if (IS_PREVIEW) startGuestLoginTimer();
}
function glassLoginReset() {
const btn = document.getElementById('glassLoginBtn');
const st = document.getElementById('glassLoginStatus');
const pwd = document.getElementById('glassPassword');
if (btn) { btn.disabled = false; btn.textContent = '登 录'; }
if (st) { st.style.display = 'none'; st.textContent = ''; }
if (pwd) pwd.value = '';
}
function setGlassStatus(msg, type) {
const st = document.getElementById('glassLoginStatus');
if (!st) return;
const colors = { loading: 'var(--accent)', success: 'var(--green)', error: 'var(--red)' };
const bgs = { loading: 'rgba(88,166,255,0.12)', success: 'rgba(63,185,80,0.12)', error: 'rgba(248,81,73,0.12)' };
st.style.display = 'block';
st.style.background = bgs[type] || bgs.loading;
st.style.border = '1px solid ' + (colors[type] || colors.loading);
st.style.borderRadius = '8px';
st.style.color = colors[type] || 'var(--text)';
st.innerHTML = type === 'loading'
? '<span style="display:inline-block;width:14px;height:14px;border:2px solid var(--dim);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite;vertical-align:middle;margin-right:6px"></span> ' + msg
: msg;
}
async function doGlassLogin() {
const player = document.getElementById('glassPlayerName').value.trim();
const password = document.getElementById('glassPassword').value;
const btn = document.getElementById('glassLoginBtn');
if (!player) { setGlassStatus('请输入玩家名', 'error'); return; }
if (!password) { setGlassStatus('请输入密码', 'error'); return; }
btn.disabled = true; btn.textContent = '登录中...';
setGlassStatus('正在提交登录请求...', 'loading');
try {
const reqRes = await fetch(API + 'sync.php?action=web_login_request', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player, password})
});
const reqData = await reqRes.json();
if (!reqData.success) { setGlassStatus(reqData.message || '提交失败', 'error'); btn.disabled = false; btn.textContent = '登 录'; return; }
const requestId = reqData.data.request_id;
setGlassStatus('等待游戏服务器验证...', 'loading');
let attempts = 0;
const pollInterval = setInterval(async () => {
attempts++;
if (attempts > 60) { clearInterval(pollInterval); setGlassStatus('验证超时,请稍后重试', 'error'); btn.disabled = false; btn.textContent = '登 录'; return; }
try {
const pollRes = await fetch(API + 'sync.php?action=check_web_login_result&player=' + encodeURIComponent(player) + '&request_id=' + requestId);
const pollData = await pollRes.json();
if (pollData.success && pollData.data) {
const result = pollData.data;
if (result.status === 'success') {
clearInterval(pollInterval);
setGlassStatus('登录成功!正在跳转...', 'success');
const token = result.token;
localStorage.setItem('sdf1_token', token);
localStorage.setItem('sdf1_player', player);
setTimeout(() => { window.location.href = 'player.php?login=' + encodeURIComponent(player) + '&token=' + encodeURIComponent(token); }, 600);
} else if (result.status === 'failed') {
clearInterval(pollInterval);
setGlassStatus(result.message || '密码错误', 'error');
btn.disabled = false; btn.textContent = '登 录';
}
}
} catch (e) { /* 继续轮询 */ }
}, 1000);
} catch (e) {
setGlassStatus('连接失败: ' + e.message, 'error');
btn.disabled = false; btn.textContent = '登 录';
}
}
// ★ 60秒定时弹窗(游客模式专用)
function startGuestLoginTimer() {
if (guestLoginTimer) clearInterval(guestLoginTimer);
guestLoginTimer = setInterval(() => {
if (!IS_PREVIEW) { clearInterval(guestLoginTimer); guestLoginTimer = null; return; }
if (!glassLoginDismissed) {
openGlassLogin();
}
}, 60000);
}
function switchPage(page) {