添加Web前端代码

This commit is contained in:
ypsdf1
2026-06-19 16:02:14 +08:00
parent 9bae160ce0
commit 885e70f0f5
20 changed files with 3239 additions and 585 deletions
+313 -40
View File
@@ -1,7 +1,35 @@
<?php
// ★ 强制缓存失效:用文件修改时间作为版本号
// 每次修改player.php后,文件时间戳变化 → URL不同 → 浏览器必须获取新内容
header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
header('X-Accel-Expires: 0'); // Nginx反代也禁缓存
$BUILD_VERSION = 'v' . filemtime(__FILE__); // 文件修改时间 = 版本号
$currentVersion = isset($_GET['_v']) ? $_GET['_v'] : '';
if ($currentVersion !== $BUILD_VERSION) {
// 版本不匹配 → 重定向到带正确版本号的URL(浏览器视为全新请求)
// 先移除旧的 _v 参数
$params = $_GET;
unset($params['_v']);
$queryString = http_build_query($params);
$path = strtok($_SERVER['REQUEST_URI'], '?');
$newUrl = $path . ($queryString ? '?' . $queryString . '&' : '?') . '_v=' . $BUILD_VERSION;
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
header('Location: ' . $newUrl, true, 302);
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDF1 - 玩家商城</title>
<style>
@@ -135,10 +163,10 @@
<div class="main">
<div class="sidebar">
<div class="sidebar-item active" data-page="shop" onclick="switchPage('shop')">🛒 商城</div>
<div class="sidebar-item active" data-page="dashboard" onclick="switchPage('dashboard')">📊 个人中心</div>
<div class="sidebar-item" data-page="shop" onclick="switchPage('shop')">🛒 商城</div>
<div class="sidebar-item" data-page="cdk" onclick="switchPage('cdk')">🎁 CDK兑换</div>
<div class="sidebar-item" data-page="balance" onclick="switchPage('balance')">💰 余额查询</div>
<div class="sidebar-item" data-page="account" onclick="switchPage('account')">👤 账号信息</div>
</div>
<div class="content" id="content">
@@ -158,6 +186,30 @@
</div>
<script>
// ★ 防止bfcache(前进/后退缓存)绕过安全检查
window.addEventListener('pageshow', function(e) {
if (e.persisted) {
// 页面从bfcache恢复 → 强制刷新以获取最新代码
console.log('[BFCACHE] Page restored from bfcache, forcing reload');
location.reload();
}
});
// ★ 版本检测:强制清除浏览器缓存的旧版JS
(function() {
const buildVersion = '<?php echo $BUILD_VERSION; ?>';
const lastVersion = localStorage.getItem('sdf1_build_version');
if (lastVersion && lastVersion !== buildVersion) {
// 版本变化 → 清除所有缓存,强制刷新
localStorage.removeItem('sdf1_token');
localStorage.removeItem('sdf1_player');
localStorage.removeItem('sdf1_build_version');
localStorage.setItem('sdf1_build_version', buildVersion);
console.log('[VERSION] Old version detected, clearing cache and reloading. Old:', lastVersion, 'New:', buildVersion);
location.reload();
return;
}
localStorage.setItem('sdf1_build_version', buildVersion);
})();
const API = 'api/';
let TOKEN = new URLSearchParams(location.search).get('token') || localStorage.getItem('sdf1_token') || '';
let IS_PREVIEW = !TOKEN;
@@ -165,13 +217,14 @@
let tokenPollTimer = null; // ★ 新增:token轮询定时器(用于续期检查)
let AUTH_TOKEN = TOKEN; // ★ 使用独立的认证token,避免localStorage被意外覆盖
let authVerified = false; // ★ 记录是否已通过首次验证
let currentPage = 'shop';
let currentPage = 'dashboard';
let AUTHENTICATED = false;
let NEED_PASSWORD = false;
// 初始化
// 初始化 — 回档9bae160版本的token处理逻辑
(function() {
const urlToken = new URLSearchParams(location.search).get('token');
if (urlToken) {
localStorage.setItem('sdf1_token', urlToken);
TOKEN = urlToken;
@@ -222,10 +275,10 @@
document.getElementById('playerInfo').textContent = '已登录: ' + currentPlayer;
authVerified = true; // ★ 标记已通过验证
if (callback) callback({ok: true, mode: data.data.mode});
// ★ 如果不需要密码,直接加载商品页
// ★ 如果不需要密码,直接加载个人中心
if (typeof NEED_PASSWORD !== 'undefined' && !NEED_PASSWORD) {
setTimeout(() => {
switchPage('shop');
switchPage('dashboard');
// ★ Token跟随Java端配置过期时间自动过期,不续费
// startTokenPolling() 不再调用,仅保留变量声明
}, 100);
@@ -565,30 +618,126 @@
function showLoginPrompt() {
const c = document.getElementById('content');
const urlLogin = new URLSearchParams(location.search).get('login') || currentPlayer || '';
c.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:40px">
<div style="font-size:64px;margin-bottom:24px">🎮</div>
<h2 style="color:var(--text);margin-bottom:12px">欢迎来到SDF1玩家商城</h2>
<p style="color:var(--dim);font-size:14px;margin-bottom:32px;max-width:400px">
请先登录游戏获取Web访问令牌<br>
然后通过令牌链接访问此页面
<div style="font-size:64px;margin-bottom:24px">🔐</div>
<h2 style="color:var(--text);margin-bottom:12px">登录到SDF1玩家商城</h2>
<p style="color:var(--dim);font-size:14px;margin-bottom:24px;max-width:400px">
输入游戏内密码登录,无需在游戏里敲指令
</p>
<div style="background:rgba(59,130,246,0.1);border:1px solid var(--accent);border-radius:8px;padding:20px;margin-bottom:32px;max-width:500px;text-align:left">
<h3 style="color:var(--accent);margin:0 0 12px 0;font-size:16px">📱 如何获取访问令牌?</h3>
<ol style="color:var(--dim);font-size:13px;margin:0;padding-left:20px;line-height:1.8">
<li>登录Minecraft游戏服务器</li>
<li>在聊天栏输入 <code style="background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:3px">/sdf1_login weblogin</code></li>
<li>复制返回的Web访问链接</li>
<li>在浏览器中打开该链接</li>
</ol>
<!-- 密码登录表单 -->
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:24px;width:360px;max-width:100%">
<div style="text-align:left;margin-bottom:16px">
<label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">玩家名</label>
<input type="text" id="loginPlayerName" value="${urlLogin}" placeholder="输入玩家名" maxlength="16"
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">
</div>
<div style="text-align:left;margin-bottom:20px">
<label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">游戏内密码</label>
<input type="password" id="loginPassword" placeholder="输入密码" maxlength="32"
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">
</div>
<div id="loginStatusBox" style="display:none;margin-bottom:16px"></div>
<button id="loginSubmitBtn" class="btn btn-primary" style="width:100%;padding:12px" onclick="doDirectLogin()">登录</button>
<p style="color:var(--dim);font-size:12px;margin-top:12px">密码由游戏服务器验证,Web端不存储密码</p>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
<button class="btn" onclick="location.reload()" style="padding:12px 24px">🔄 刷新页面</button>
<button class="btn btn-primary" onclick="showGuestMode()" style="padding:12px 24px">👁️ 预览模式</button>
<!-- 备用方案 -->
<div style="background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.3);border-radius:8px;padding:16px;margin-bottom:24px;max-width:400px;text-align:left">
<h3 style="color:var(--accent);margin:0 0 8px 0;font-size:14px">📱 也可以在游戏中获取链接</h3>
<p style="color:var(--dim);font-size:12px;margin:0;line-height:1.6">
在游戏聊天栏输入 <code style="background:rgba(255,255,255,0.1);padding:1px 4px;border-radius:3px">/sdf1_login weblogin</code>
</p>
</div>
<p style="color:var(--dim);font-size:12px;margin-top:24px">游客模式下可以浏览商品,但无法购买</p>
<button class="btn" onclick="showGuestMode()" style="padding:10px 20px;font-size:13px">👁️ 先逛逛(游客模式)</button>
</div>`;
document.querySelector('.sidebar').style.display = 'none';
// 自动聚焦密码框
setTimeout(() => { const pwd = document.getElementById('loginPassword'); if (pwd) pwd.focus(); }, 100);
}
// ★ 直接密码登录(不需要游戏内token)
async function doDirectLogin() {
const player = document.getElementById('loginPlayerName').value.trim();
const password = document.getElementById('loginPassword').value;
const statusBox = document.getElementById('loginStatusBox');
const submitBtn = document.getElementById('loginSubmitBtn');
if (!player) { showLoginStatus('请输入玩家名', 'error'); return; }
if (!password) { showLoginStatus('请输入密码', 'error'); return; }
submitBtn.disabled = true;
showLoginStatus('正在提交登录请求...', 'loading');
try {
// 1. 提交登录请求
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) {
showLoginStatus(reqData.message || '提交失败', 'error');
submitBtn.disabled = false;
return;
}
const requestId = reqData.data.request_id;
showLoginStatus('等待游戏服务器验证密码...', 'loading');
// 2. 轮询结果
let attempts = 0;
const pollInterval = setInterval(async () => {
attempts++;
if (attempts > 60) {
clearInterval(pollInterval);
showLoginStatus('验证超时,请稍后重试', 'error');
submitBtn.disabled = false;
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);
showLoginStatus('✅ 登录成功!正在跳转...', 'success');
// 存储token和玩家名
const token = result.token;
localStorage.setItem('sdf1_token', token);
localStorage.setItem('sdf1_player', player);
// 跳转到player.php带token
setTimeout(() => {
window.location.href = 'player.php?login=' + encodeURIComponent(player) + '&token=' + encodeURIComponent(token);
}, 500);
} else if (result.status === 'failed') {
clearInterval(pollInterval);
showLoginStatus('❌ ' + (result.message || '密码错误'), 'error');
submitBtn.disabled = false;
}
}
} catch (e) { /* 继续轮询 */ }
}, 1000);
} catch (e) {
showLoginStatus('连接失败: ' + e.message, 'error');
submitBtn.disabled = false;
}
}
function showLoginStatus(msg, type) {
const box = document.getElementById('loginStatusBox');
if (!box) return;
const colors = { loading: 'var(--accent)', success: 'var(--green)', error: 'var(--red)' };
const bgs = { loading: 'rgba(88,166,255,0.1)', success: 'rgba(63,185,80,0.1)', error: 'rgba(248,81,73,0.1)' };
box.style.display = 'block';
box.style.background = bgs[type] || bgs.loading;
box.style.border = '1px solid ' + (colors[type] || colors.loading);
box.style.borderRadius = '6px';
box.style.padding = '10px 14px';
box.style.fontSize = '13px';
box.style.color = colors[type] || 'var(--text)';
box.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;
}
function showGuestMode() {
@@ -601,7 +750,7 @@
document.getElementById('playerInfo').textContent = '游客模式';
document.getElementById('previewBadge').style.display = 'inline';
document.querySelector('.sidebar').style.display = 'block';
switchPage('shop');
switchPage('dashboard');
}
function switchPage(page) {
@@ -612,7 +761,8 @@
document.querySelector('.sidebar').style.display = 'block';
const c = document.getElementById('content');
if (!c) return;
if (page === 'shop') renderShop(c);
if (page === 'dashboard') renderDashboard(c);
else if (page === 'shop') renderShop(c);
else if (page === 'cdk') renderCDK(c);
else if (page === 'balance') renderBalance(c);
else if (page === 'account') renderAccount(c);
@@ -688,38 +838,83 @@
}
}
// ===== 商城 =====
// ===== 商城(分类标签页) =====
let shopItems = [];
let shopCategories = [];
let currentShopCat = '';
async function renderShop(el) {
el.innerHTML = '<div class="empty">加载中...</div>';
try {
const res = await api('shop.php', {action: 'list'});
if (!res.success) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + (res.message || '加载失败') + '</div>'; return; }
const items = res.data || [];
const categories = [...new Set(items.map(i => i.category))];
let html = '<div class="card"><h2>商品列表</h2>';
shopItems = res.data || [];
shopCategories = [...new Set(shopItems.map(i => i.category))];
currentShopCat = shopCategories[0] || '';
let html = '<div class="card">';
html += '<h2>商城</h2>';
if (res.preview) html += '<div class="preview-badge" style="margin-bottom:12px">预览模式 - 无法购买</div>';
if (categories.length === 0) {
if (shopCategories.length === 0) {
html += '<div class="empty">暂无商品</div>';
} else {
categories.forEach(cat => {
html += `<h3 style="color:var(--dim);margin:16px 0 8px;font-size:14px">${cat}</h3>`;
html += '<div class="grid">';
items.filter(i => i.category === cat).forEach(item => {
const matIcon = getMaterialIcon(item.material);
const stockText = item.stock == -1 ? '∞ 无限' : (item.stock == 0 ? '售罄' : item.stock + ' 个');
const stockClass = item.stock == 0 ? 'out' : '';
html += `<div class="item-card" onclick="showBuyModal('${item.id}','${item.display_name.replace(/'/g,"\\'")}',${item.buy_price},${item.stock})"><div class="icon">${matIcon}</div><div class="name">${item.display_name}</div><div class="price">${item.buy_price} 债券</div><div class="stock ${stockClass}">库存: ${stockText}</div></div>`;
});
html += '</div>';
// 分类标签
html += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px">';
shopCategories.forEach((cat, idx) => {
const active = idx === 0 ? 'color:#fff;background:var(--accent);border-color:var(--accent)' : '';
const catCount = shopItems.filter(i => i.category === cat).length;
html += `<div onclick="switchShopCat('${cat.replace(/'/g,"\\'")}')" style="padding:6px 14px;border:1px solid var(--border);border-radius:16px;cursor:pointer;font-size:13px;transition:all 0.2s;${active}" class="shop-cat-tab" data-cat="${cat}">${cat} <span style="opacity:0.7;font-size:11px">(${catCount})</span></div>`;
});
html += '</div>';
// 商品内容区
html += '<div id="shopContent"></div>';
}
html += '</div>';
el.innerHTML = html;
// 渲染当前分类的商品
renderShopContent();
} catch (e) {
el.innerHTML = '<div class="empty" style="color:var(--red)">商城加载失败: ' + e.message + '</div>';
}
}
function switchShopCat(cat) {
currentShopCat = cat;
// 更新标签样式
document.querySelectorAll('.shop-cat-tab').forEach(tab => {
const isActive = tab.dataset.cat === cat;
tab.style.background = isActive ? 'var(--accent)' : '';
tab.style.color = isActive ? '#fff' : '';
tab.style.borderColor = isActive ? 'var(--accent)' : 'var(--border)';
});
renderShopContent();
}
function renderShopContent() {
const contentEl = document.getElementById('shopContent');
if (!contentEl) return;
const items = shopItems.filter(i => i.category === currentShopCat);
if (items.length === 0) {
contentEl.innerHTML = '<div class="empty" style="padding:20px">该分类暂无商品</div>';
return;
}
let html = '<div class="grid">';
items.forEach(item => {
const matIcon = getMaterialIcon(item.material);
const stockText = item.stock == -1 ? '∞ 无限' : (item.stock == 0 ? '售罄' : item.stock + ' 个');
const stockClass = item.stock == 0 ? 'out' : '';
html += `<div class="item-card" onclick="showBuyModal('${item.id}','${item.display_name.replace(/'/g,"\\'")}',${item.buy_price},${item.stock})"><div class="icon">${matIcon}</div><div class="name">${item.display_name}</div><div class="price">${item.buy_price} 债券</div><div class="stock ${stockClass}">库存: ${stockText}</div></div>`;
});
html += '</div>';
contentEl.innerHTML = html;
}
function getMaterialIcon(mat) {
const icons = {'STONE':'🪨','DIAMOND':'💎','GOLD_INGOT':'🥇','IRON_INGOT':'🥈','EMERALD':'💚','REDSTONE':'🔴','COAL':'⬛','DIAMOND_SWORD':'⚔️','GOLDEN_APPLE':'🍎','END_CRYSTAL':'🔮','TOTEM_UNDYING':'🛡️','NETHERITE_INGOT':'⬛','ENCHANTED_BOOK':'📖','SHULKER_BOX':'📦','PLAYER_HEAD':'👤','PAPER':'📄','ARROW':'🏹','BOW':'🏹','TRIDENT':'🔱','ELYTRA':'🪽','SADDLE':'🐴','NAME_TAG':'🏷️'};
return icons[mat] || '🧱';
@@ -761,6 +956,84 @@
}
}
// ===== 个人中心仪表盘 =====
async function renderDashboard(el) {
if (IS_PREVIEW || !currentPlayer) {
el.innerHTML = `
<div class="card" style="text-align:center;padding:48px 20px">
<div style="font-size:64px;margin-bottom:16px">📊</div>
<h2 style="color:var(--accent);margin-bottom:8px">个人中心</h2>
<p style="color:var(--dim);font-size:14px;margin-bottom:20px">游客模式下无法查看个人信息</p>
<p style="color:var(--dim);font-size:12px">请先在游戏内登录,或输入密码登录</p>
</div>`;
return;
}
el.innerHTML = '<div class="card" style="text-align:center;padding:40px"><div class="empty">加载中...</div></div>';
try {
const res = await api('register.php', {action: 'query', player: currentPlayer, token: TOKEN});
if (!res.success) {
el.innerHTML = `<div class="card" style="border-color:var(--red)"><h2>加载失败</h2><p style="color:var(--dim);margin-top:8px">${res.message || '未知错误'}</p></div>`;
return;
}
const d = res.data;
const loginTime = d.last_login_time ? new Date(d.last_login_time * 1000).toLocaleString() : '暂无';
const regTime = d.register_time ? new Date(d.register_time * 1000).toLocaleString() : '暂无';
const hours = Math.floor((d.total_online_time || 0) / 3600);
const mins = Math.floor(((d.total_online_time || 0) % 3600) / 60);
const onlineText = hours > 0 ? `${hours}小时${mins}分钟` : `${mins}分钟`;
el.innerHTML = `
<div class="card" style="margin-bottom:0">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--purple));display:flex;align-items:center;justify-content:center;font-size:28px;color:#fff;font-weight:700">${d.player_name.charAt(0).toUpperCase()}</div>
<div>
<h2 style="margin:0;font-size:20px">${d.player_name}</h2>
<p style="color:var(--dim);font-size:12px;margin-top:2px">注册于 ${regTime}</p>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-top:12px">
<div class="stat-box" style="border-left:3px solid var(--accent)">
<div class="value" style="font-size:24px;color:var(--accent)">📧 ${d.masked_email || '未绑定'}</div>
<div class="label">绑定邮箱</div>
</div>
<div class="stat-box" style="border-left:3px solid var(--green)">
<div class="value" style="font-size:24px;color:var(--green)">${d.bonds ?? 0}</div>
<div class="label">债券余额</div>
</div>
<div class="stat-box" style="border-left:3px solid var(--yellow)">
<div class="value" style="font-size:24px;color:var(--yellow)">${d.points ?? 0}</div>
<div class="label">积分</div>
</div>
<div class="stat-box" style="border-left:3px solid var(--purple)">
<div class="value" style="font-size:24px;color:var(--purple)">${loginTime}</div>
<div class="label">最后登录时间</div>
</div>
<div class="stat-box" style="border-left:3px solid #58a6ff">
<div class="value" style="font-size:24px;color:#58a6ff">${d.total_online_days ?? 0} 天</div>
<div class="label">累计在线天数</div>
</div>
<div class="stat-box" style="border-left:3px solid #3fb950">
<div class="value" style="font-size:24px;color:#3fb950">${d.total_checkin_days ?? 0} 天</div>
<div class="label">累计签到天数</div>
</div>
</div>
<div class="card" style="margin-top:12px">
<h2>详细信息</h2>
<table class="table" style="margin-top:8px">
<tr><td style="color:var(--dim);width:120px">累计在线时长</td><td>${onlineText}</td></tr>
<tr><td style="color:var(--dim)">礼包阶段</td><td>${d.gift_stage ?? 0}</td></tr>
</table>
</div>`;
} catch (e) {
el.innerHTML = `<div class="card" style="border-color:var(--red)"><h2>加载异常</h2><p style="color:var(--dim);margin-top:8px">${e.message}</p></div>`;
}
}
// ===== CDK =====
function renderCDK(el) {
el.innerHTML = `