2520 lines
140 KiB
PHP
2520 lines
140 KiB
PHP
<?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>
|
||
:root {
|
||
--bg: #0d1117; --card: #161b22; --border: #30363d;
|
||
--text: #e6edf3; --dim: #8b949e; --accent: #58a6ff;
|
||
--green: #3fb950; --red: #f85149; --yellow: #d29922;
|
||
--purple: #bc8cff;
|
||
}
|
||
.light-theme {
|
||
--bg: #f6f8fa; --card: #ffffff; --border: #d0d7de;
|
||
--text: #1f2328; --dim: #656d76; --accent: #0969da;
|
||
--green: #1a7f37; --red: #cf222e; --yellow: #9a6700;
|
||
--purple: #8250df;
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; transition: background 0.3s, color 0.3s; }
|
||
.header {
|
||
background: linear-gradient(135deg, #1a1e2e 0%, #0d1117 100%);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 16px 24px;
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
}
|
||
.header h1 { font-size: 20px; color: var(--accent); }
|
||
.header .status { font-size: 13px; }
|
||
.preview-badge {
|
||
background: var(--yellow); color: #000; padding: 4px 12px;
|
||
border-radius: 20px; font-size: 12px; font-weight: 600;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.6; } }
|
||
.main { display: flex; gap: 0; min-height: calc(100vh - 56px); }
|
||
.sidebar {
|
||
width: 220px; background: var(--card); border-right: 1px solid var(--border);
|
||
padding: 12px 0; flex-shrink: 0; transition: background 0.3s;
|
||
}
|
||
.sidebar-item {
|
||
padding: 10px 20px; cursor: pointer; color: var(--dim);
|
||
transition: all 0.2s; font-size: 14px; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.sidebar-item:hover { background: rgba(88,166,255,0.1); color: var(--text); }
|
||
.sidebar-item.active { color: var(--accent); border-right: 3px solid var(--accent); background: rgba(88,166,255,0.05); }
|
||
.content { flex: 1; padding: 24px; overflow-y: auto; max-height: calc(100vh - 56px); }
|
||
.card {
|
||
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
||
padding: 20px; margin-bottom: 16px;
|
||
}
|
||
.card h2 { font-size: 16px; margin-bottom: 12px; color: var(--accent); }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
|
||
.item-card {
|
||
background: rgba(88,166,255,0.05); border: 1px solid var(--border);
|
||
border-radius: 8px; padding: 16px; cursor: pointer;
|
||
transition: all 0.2s; position: relative;
|
||
}
|
||
.item-card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
||
.item-card .icon { font-size: 32px; margin-bottom: 8px; }
|
||
.item-card .name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
||
.item-card .price { color: var(--green); font-size: 13px; }
|
||
.item-card .stock { color: var(--dim); font-size: 12px; }
|
||
.item-card .stock.out { color: var(--red); }
|
||
.stat-row { display: flex; gap: 16px; margin-bottom: 16px; }
|
||
.stat-box {
|
||
flex: 1; background: var(--card); border: 1px solid var(--border);
|
||
border-radius: 8px; padding: 16px; text-align: center;
|
||
}
|
||
.stat-box .value { font-size: 28px; font-weight: 700; color: var(--accent); }
|
||
.stat-box .label { font-size: 12px; color: var(--dim); margin-top: 4px; }
|
||
.cdk-input {
|
||
display: flex; gap: 8px; margin-top: 12px;
|
||
}
|
||
.cdk-input input {
|
||
flex: 1; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border);
|
||
border-radius: 6px; color: var(--text); font-size: 14px; outline: none;
|
||
}
|
||
.cdk-input input:focus { border-color: var(--accent); }
|
||
.btn {
|
||
padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer;
|
||
font-size: 14px; font-weight: 600; transition: all 0.2s;
|
||
}
|
||
.btn-primary { background: var(--accent); color: #fff; }
|
||
.btn-primary:hover { background: #79c0ff; }
|
||
.btn-green { background: var(--green); color: #fff; }
|
||
.btn-green:hover { background: #56d364; }
|
||
.btn-red { background: var(--red); color: #fff; }
|
||
.btn-orange { background: var(--yellow); color: #000; }
|
||
.btn-dim { background: var(--card); color: var(--dim); border: 1px solid var(--border); }
|
||
.btn-dim:hover { color: var(--text); border-color: var(--accent); }
|
||
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.table th { text-align: left; padding: 10px 12px; color: var(--dim); border-bottom: 1px solid var(--border); }
|
||
.table td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||
.table tr:hover td { background: rgba(88,166,255,0.05); }
|
||
.empty { text-align: center; color: var(--dim); padding: 40px; }
|
||
.modal-overlay {
|
||
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: 999; display: none;
|
||
}
|
||
.modal {
|
||
background: var(--card); border: 1px solid var(--border); border-radius: 12px;
|
||
padding: 24px; width: 400px; max-width: 90%;
|
||
}
|
||
.modal h3 { margin-bottom: 16px; }
|
||
.modal .row { margin-bottom: 12px; }
|
||
.modal label { display: block; font-size: 13px; color: var(--dim); margin-bottom: 4px; }
|
||
.modal input, .modal select {
|
||
width: 100%; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border);
|
||
border-radius: 6px; color: var(--text); font-size: 14px;
|
||
}
|
||
.modal .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||
.toast {
|
||
position: fixed; top: 20px; right: 20px; padding: 12px 20px;
|
||
border-radius: 8px; font-size: 14px; z-index: 200;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
.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); }
|
||
}
|
||
/* ★ 毛玻璃Alert弹窗 */
|
||
.glass-alert-overlay {
|
||
display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.45);
|
||
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
||
z-index: 5000; justify-content: center; align-items: center;
|
||
animation: glassFadeIn 0.25s ease;
|
||
}
|
||
.glass-alert-overlay.show { display: flex; }
|
||
.glass-alert-card {
|
||
background: rgba(22,27,34,0.92);
|
||
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
|
||
border: 1px solid rgba(88,166,255,0.25);
|
||
border-radius: 16px; padding: 28px 28px 20px; width: 360px; max-width: 88%;
|
||
box-shadow: 0 12px 48px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
|
||
animation: glassSlideUp 0.3s ease;
|
||
text-align: center;
|
||
}
|
||
.glass-alert-card .alert-icon { font-size: 36px; margin-bottom: 12px; }
|
||
.glass-alert-card .alert-msg { font-size: 14px; color: #e6edf3; line-height: 1.6; margin-bottom: 20px; word-break: break-word; }
|
||
.glass-alert-card .alert-btns { display: flex; gap: 10px; justify-content: center; }
|
||
.glass-alert-card .alert-btns button {
|
||
padding: 8px 24px; border: none; border-radius: 8px; font-size: 13px; font-weight: 600;
|
||
cursor: pointer; transition: all 0.2s;
|
||
}
|
||
.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); }
|
||
@media (max-width: 768px) {
|
||
.sidebar { display: none; }
|
||
.grid { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>SDF1 玩家商城</h1>
|
||
<div class="status">
|
||
<span id="previewBadge" class="preview-badge" style="display:none">预览模式</span>
|
||
<span id="playerInfo" style="margin-left:12px;color:var(--dim)"></span>
|
||
<button class="btn btn-yellow" onclick="showThemePicker()" style="margin-left:16px;font-size:12px;padding:4px 8px">🎨 主题</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="sidebar">
|
||
<div class="sidebar-item" data-page="dashboard" onclick="switchPage('dashboard')">📊 个人中心</div>
|
||
<div class="sidebar-item" data-page="shop" onclick="switchPage('shop')">🛒 商城</div>
|
||
<div class="sidebar-item" data-page="lands" onclick="switchPage('lands')">🏡 领地管理</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="ticket" onclick="switchPage('ticket')">📋 工单系统</div>
|
||
</div>
|
||
|
||
<div class="content" id="content">
|
||
<!-- 动态内容 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ★ 游客模式C位登录按钮 -->
|
||
<div class="guest-center-login" id="guestCenterLogin">
|
||
<button class="glow-btn" onclick="openGlassLogin()">🔑 立即登录</button>
|
||
</div>
|
||
|
||
<!-- ★ 毛玻璃Alert弹窗 -->
|
||
<div class="glass-alert-overlay" id="glassAlertOverlay">
|
||
<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-btns" id="glassAlertBtns">
|
||
<button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ★ 毛玻璃登录弹窗 -->
|
||
<div class="glass-login-overlay" id="glassLoginOverlay" onclick="closeGlassLogin(event)">
|
||
<div class="glass-card" onclick="event.stopPropagation()">
|
||
<button class="x-btn" onclick="closeGlassLogin()">×</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>
|
||
<div id="modalBody"></div>
|
||
<div class="actions">
|
||
<button class="btn" onclick="closeModal()">取消</button>
|
||
<button class="btn btn-primary" id="modalConfirm" onclick="confirmModal()">确认</button>
|
||
</div>
|
||
</div>
|
||
</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;
|
||
let currentPlayer = localStorage.getItem('sdf1_player') || new URLSearchParams(location.search).get('login') || '';
|
||
let tokenPollTimer = null; // ★ 新增:token轮询定时器(用于续期检查)
|
||
let AUTH_TOKEN = TOKEN; // ★ 使用独立的认证token,避免localStorage被意外覆盖
|
||
let authVerified = false; // ★ 记录是否已通过首次验证
|
||
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;
|
||
IS_PREVIEW = false;
|
||
// ★ 从URL中移除token参数,避免URL暴露
|
||
const params = new URLSearchParams(location.search);
|
||
params.delete('token');
|
||
const newUrl = location.pathname + (params.toString() ? '?' + params.toString() : '');
|
||
window.history.replaceState({}, '', newUrl);
|
||
}
|
||
|
||
// ★ 安全机制:没有token → 进入游客模式(预览)
|
||
if (!TOKEN) {
|
||
IS_PREVIEW = true;
|
||
currentPlayer = '';
|
||
AUTHENTICATED = false;
|
||
document.getElementById('playerInfo').textContent = '游客模式';
|
||
document.getElementById('previewBadge').style.display = 'inline';
|
||
showLoginPrompt();
|
||
return;
|
||
}
|
||
|
||
// 有token → 验证token
|
||
document.getElementById('playerInfo').textContent = '验证中...';
|
||
checkAccess('view');
|
||
})();
|
||
|
||
// ★ 安全验证入口
|
||
async function checkAccess(action, callback) {
|
||
if (IS_PREVIEW || !TOKEN) {
|
||
document.getElementById('playerInfo').textContent = '游客模式';
|
||
if (callback) callback({ok: false, mode: 'preview'});
|
||
return;
|
||
}
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'web_access_check');
|
||
url.searchParams.set('web_token', TOKEN);
|
||
url.searchParams.set('access_action', action);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
AUTHENTICATED = true;
|
||
NEED_PASSWORD = false;
|
||
currentPlayer = data.data.player || currentPlayer;
|
||
localStorage.setItem('sdf1_player', currentPlayer);
|
||
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('dashboard');
|
||
// ★ Token跟随Java端配置过期时间自动过期,不续费
|
||
// startTokenPolling() 不再调用,仅保留变量声明
|
||
}, 100);
|
||
}
|
||
} else if (data.need_password) {
|
||
NEED_PASSWORD = true;
|
||
currentPlayer = data.player || currentPlayer;
|
||
localStorage.setItem('sdf1_player', currentPlayer);
|
||
document.getElementById('playerInfo').textContent = '等待登录';
|
||
if (callback) callback({ok: false, need_password: true, player: currentPlayer});
|
||
showPasswordModal(data.message || '请输入游戏登录密码');
|
||
} else if (data.need_game_login) {
|
||
AUTHENTICATED = false;
|
||
currentPlayer = data.player || currentPlayer;
|
||
localStorage.setItem('sdf1_player', currentPlayer);
|
||
document.getElementById('playerInfo').textContent = '请先在游戏内登录';
|
||
if (callback) callback({ok: false, need_game_login: true, player: currentPlayer});
|
||
showGameLoginMessage(data.message || '请先在游戏内登录');
|
||
} else if (data.need_register) {
|
||
AUTHENTICATED = false;
|
||
currentPlayer = data.player || currentPlayer;
|
||
localStorage.setItem('sdf1_player', currentPlayer);
|
||
document.getElementById('playerInfo').textContent = '请先在游戏内注册';
|
||
if (callback) callback({ok: false, need_register: true, player: currentPlayer});
|
||
showRegisterMessage(data.message || '请先在游戏内注册账号');
|
||
} else {
|
||
// Token无效 → 清除缓存,进入游客模式
|
||
AUTHENTICATED = false;
|
||
TOKEN = '';
|
||
IS_PREVIEW = true;
|
||
localStorage.removeItem('sdf1_token');
|
||
localStorage.removeItem('sdf1_player');
|
||
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) {
|
||
AUTHENTICATED = false;
|
||
TOKEN = '';
|
||
IS_PREVIEW = true;
|
||
localStorage.removeItem('sdf1_token');
|
||
localStorage.removeItem('sdf1_player');
|
||
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});
|
||
}
|
||
}
|
||
|
||
function showGameLoginMessage(message) {
|
||
document.getElementById('modalTitle').textContent = '需要游戏登录';
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<div style="text-align:center;padding:20px">
|
||
<div style="font-size:48px;margin-bottom:16px">🎮</div>
|
||
<p style="color:var(--dim);font-size:14px;margin-bottom:20px">${message}</p>
|
||
<div style="background:rgba(59,130,246,0.1);border:1px solid var(--accent);border-radius:8px;padding:16px;margin-bottom:20px">
|
||
<p style="color:var(--text);font-size:13px;margin:0">请在Minecraft游戏中使用 <code style="background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:3px">/sdf1_login</code> 命令登录后再访问Web端</p>
|
||
</div>
|
||
<button class="btn" onclick="location.reload()" style="margin-top:8px">刷新页面</button>
|
||
</div>`;
|
||
document.getElementById('modalOverlay').style.display = 'flex';
|
||
}
|
||
|
||
function showRegisterMessage(message) {
|
||
document.getElementById('modalTitle').textContent = '注册新账号';
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">玩家 <b style="color:var(--accent)">${currentPlayer}</b> 尚未注册,请设置密码完成注册。</p>
|
||
<div class="row"><label>设置密码</label><input type="password" id="regPassword" placeholder="至少6位" autofocus></div>
|
||
<div class="row"><label>确认密码</label><input type="password" id="regPassword2" placeholder="再次输入密码"></div>
|
||
<div class="row"><label>绑定邮箱(可选)</label><input type="email" id="regEmail" placeholder="用于找回密码"></div>
|
||
<div id="regError" style="color:var(--red);font-size:12px;margin-top:4px"></div>
|
||
<div style="background:rgba(245,158,11,0.1);border:1px solid #f59e0b;border-radius:8px;padding:12px;margin-top:8px">
|
||
<p style="color:var(--text);font-size:12px;margin:0">💡 注册后需等待游戏服务器同步创建账号(通常几秒内完成)</p>
|
||
</div>`;
|
||
document.getElementById('modalConfirm').textContent = '提交注册';
|
||
document.getElementById('modalConfirm').onclick = () => doWebRegister();
|
||
document.getElementById('regPassword').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('regPassword2').focus(); });
|
||
document.getElementById('regPassword2').addEventListener('keydown', e => { if (e.key === 'Enter') doWebRegister(); });
|
||
document.getElementById('modalOverlay').style.display = 'flex';
|
||
setTimeout(() => document.getElementById('regPassword').focus(), 100);
|
||
}
|
||
|
||
async function doWebRegister() {
|
||
const pwd = document.getElementById('regPassword').value;
|
||
const pwd2 = document.getElementById('regPassword2').value;
|
||
const email = document.getElementById('regEmail').value.trim();
|
||
const errEl = document.getElementById('regError');
|
||
|
||
if (!pwd) { errEl.textContent = '请输入密码'; return; }
|
||
if (pwd.length < 6) { errEl.textContent = '密码至少6位'; return; }
|
||
if (pwd !== pwd2) { errEl.textContent = '两次密码不一致'; return; }
|
||
|
||
errEl.textContent = '注册中...';
|
||
document.getElementById('modalConfirm').disabled = true;
|
||
|
||
try {
|
||
const url = new URL(API + 'register.php', location.href);
|
||
url.searchParams.set('action', 'web_register');
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({web_token: TOKEN, password: pwd, email: email})
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
document.getElementById('modalConfirm').disabled = false;
|
||
closeModal();
|
||
document.getElementById('modalTitle').textContent = '注册请求已提交';
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<div style="text-align:center;padding:20px">
|
||
<div style="font-size:48px;margin-bottom:16px">✅</div>
|
||
<p style="color:var(--dim);font-size:14px;margin-bottom:16px">注册请求已提交到游戏服务器</p>
|
||
<div style="background:rgba(59,130,246,0.1);border:1px solid var(--accent);border-radius:8px;padding:16px;margin-bottom:16px">
|
||
<p style="color:var(--text);font-size:13px;margin:0">请在游戏内输入 <code style="background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:3px">/sdf1_login</code> 完成登录</p>
|
||
</div>
|
||
<p style="color:var(--dim);font-size:12px">系统将自动检测注册完成,届时页面将自动刷新</p>
|
||
<button class="btn btn-primary" onclick="location.reload()" style="margin-top:12px">刷新页面</button>
|
||
</div>`;
|
||
document.getElementById('modalOverlay').style.display = 'flex';
|
||
toast('注册请求已提交', 'success');
|
||
} else {
|
||
errEl.textContent = data.message || '注册失败';
|
||
document.getElementById('modalConfirm').disabled = false;
|
||
}
|
||
} catch (e) {
|
||
errEl.textContent = '连接失败: ' + e.message;
|
||
document.getElementById('modalConfirm').disabled = false;
|
||
}
|
||
}
|
||
|
||
async function loadBoundEmail() {
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'get_player_email');
|
||
url.searchParams.set('player', currentPlayer);
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'}
|
||
});
|
||
const data = await res.json();
|
||
if (data.success && data.data) {
|
||
document.getElementById('authEmail').value = data.data.masked_email || data.data.email || '';
|
||
if (data.data.masked_email) {
|
||
document.getElementById('authEmail').placeholder = '已绑定邮箱';
|
||
} else {
|
||
document.getElementById('authEmail').placeholder = '未绑定邮箱';
|
||
}
|
||
} else {
|
||
document.getElementById('authEmail').value = '未绑定邮箱';
|
||
document.getElementById('authEmail').placeholder = '未绑定邮箱';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('authEmail').value = '加载失败';
|
||
}
|
||
}
|
||
|
||
function showPasswordModal(message) {
|
||
document.getElementById('modalTitle').textContent = '安全验证';
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">${message}</p>
|
||
<div class="row"><label>玩家名</label><input type="text" id="authPlayer" value="${currentPlayer}" readonly style="background:#21262d;cursor:not-allowed"></div>
|
||
<div style="display:flex;gap:8px;margin-bottom:12px">
|
||
<button class="btn btn-blue" id="tabPassword" onclick="switchAuthTab('password')" style="flex:1">游戏密码</button>
|
||
<button class="btn" id="tabEmail" onclick="switchAuthTab('email')" style="flex:1">邮箱验证码</button>
|
||
</div>
|
||
<div id="authPasswordTab">
|
||
<div class="row"><label>游戏登录密码</label><input type="password" id="authPassword" placeholder="输入游戏内密码" autofocus></div>
|
||
<div style="text-align:right;margin-top:4px"><a href="javascript:void(0)" onclick="showResetPasswordModal()" style="color:var(--accent);font-size:12px">忘记密码?</a></div>
|
||
</div>
|
||
<div id="authEmailTab" style="display:none">
|
||
<div class="row"><label>绑定邮箱</label>
|
||
<input type="text" id="authEmail" readonly style="background:#21262d;cursor:not-allowed;opacity:0.7" placeholder="加载中...">
|
||
</div>
|
||
<div class="row" style="display:flex;gap:8px">
|
||
<div style="flex:1"><label>验证码</label><input type="text" id="authCode" placeholder="6位验证码" maxlength="6"></div>
|
||
<div style="display:flex;align-items:flex-end"><button class="btn btn-blue" id="sendCodeBtn" onclick="sendEmailCode()" style="white-space:nowrap">发送验证码</button></div>
|
||
</div>
|
||
</div>
|
||
<div id="authError" style="color:var(--red);font-size:12px;margin-top:4px"></div>`;
|
||
document.getElementById('modalConfirm').onclick = () => doAuth();
|
||
document.getElementById('authPassword').addEventListener('keydown', e => { if (e.key === 'Enter') doAuth(); });
|
||
document.getElementById('authCode').addEventListener('keydown', e => { if (e.key === 'Enter') doAuth(); });
|
||
document.getElementById('modalOverlay').style.display = 'flex';
|
||
setTimeout(() => {
|
||
document.getElementById('authPassword').focus();
|
||
loadBoundEmail();
|
||
}, 100);
|
||
switchAuthTab('password');
|
||
}
|
||
|
||
let currentAuthTab = 'password';
|
||
function switchAuthTab(tab) {
|
||
currentAuthTab = tab;
|
||
document.getElementById('authPasswordTab').style.display = tab === 'password' ? 'block' : 'none';
|
||
document.getElementById('authEmailTab').style.display = tab === 'email' ? 'block' : 'none';
|
||
document.getElementById('tabPassword').className = tab === 'password' ? 'btn btn-blue' : 'btn';
|
||
document.getElementById('tabEmail').className = tab === 'email' ? 'btn btn-blue' : 'btn';
|
||
document.getElementById('authError').textContent = '';
|
||
}
|
||
|
||
async function sendEmailCode() {
|
||
const btn = document.getElementById('sendCodeBtn');
|
||
btn.disabled = true;
|
||
btn.textContent = '发送中...';
|
||
document.getElementById('authError').textContent = '';
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'send_email_code');
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({player: currentPlayer, web_token: TOKEN})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
const maskedEmail = data.data.masked_email || '**@*';
|
||
document.getElementById('authError').innerHTML = '<span style="color:var(--green)">验证码已发送至 ' + maskedEmail + '</span>';
|
||
let countdown = 60;
|
||
const timer = setInterval(() => {
|
||
btn.textContent = countdown + '秒后重发';
|
||
countdown--;
|
||
if (countdown <= 0) {
|
||
clearInterval(timer);
|
||
btn.disabled = false;
|
||
btn.textContent = '发送验证码';
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
document.getElementById('authError').textContent = data.message || '发送失败';
|
||
btn.disabled = false;
|
||
btn.textContent = '发送验证码';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('authError').textContent = '连接失败: ' + e.message;
|
||
btn.disabled = false;
|
||
btn.textContent = '发送验证码';
|
||
}
|
||
}
|
||
|
||
async function doAuth() {
|
||
if (currentAuthTab === 'password') {
|
||
await doPasswordAuth();
|
||
} else {
|
||
await doEmailAuth();
|
||
}
|
||
}
|
||
|
||
async function doPasswordAuth() {
|
||
const password = document.getElementById('authPassword').value;
|
||
if (!password) { document.getElementById('authError').textContent = '请输入密码'; return; }
|
||
document.getElementById('authError').textContent = '正在提交登录请求...';
|
||
try {
|
||
// ★ 必须走web_login_request流程(Java验证密码),不能用verify_web_password(PHP直接验证=绕过Java)
|
||
const reqRes = await fetch(API + 'sync.php?action=web_login_request', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({player: currentPlayer, password: password})
|
||
});
|
||
const reqData = await reqRes.json();
|
||
if (!reqData.success) {
|
||
document.getElementById('authError').textContent = reqData.message || '提交失败';
|
||
return;
|
||
}
|
||
const requestId = reqData.data.request_id;
|
||
document.getElementById('authError').textContent = '等待游戏服务器验证密码...';
|
||
|
||
// 轮询结果
|
||
let attempts = 0;
|
||
const pollInterval = setInterval(async () => {
|
||
attempts++;
|
||
if (attempts > 60) {
|
||
clearInterval(pollInterval);
|
||
document.getElementById('authError').textContent = '验证超时,请稍后重试';
|
||
return;
|
||
}
|
||
try {
|
||
const pollRes = await fetch(API + 'sync.php?action=check_web_login_result&player=' + encodeURIComponent(currentPlayer) + '&request_id=' + requestId);
|
||
const pollData = await pollRes.json();
|
||
if (pollData.success && pollData.data) {
|
||
const result = pollData.data;
|
||
if (result.status === 'success') {
|
||
clearInterval(pollInterval);
|
||
AUTHENTICATED = true;
|
||
NEED_PASSWORD = false;
|
||
const newToken = result.token;
|
||
TOKEN = newToken;
|
||
localStorage.setItem('sdf1_token', newToken);
|
||
localStorage.setItem('sdf1_player', currentPlayer);
|
||
document.getElementById('playerInfo').textContent = '已登录: ' + currentPlayer;
|
||
closeModal();
|
||
toast('验证成功', 'success');
|
||
switchPage(currentPage);
|
||
} else if (result.status === 'failed') {
|
||
clearInterval(pollInterval);
|
||
document.getElementById('authError').textContent = result.message || '密码错误';
|
||
}
|
||
}
|
||
} catch (e) { /* 继续轮询 */ }
|
||
}, 1000);
|
||
} catch (e) {
|
||
document.getElementById('authError').textContent = '连接失败: ' + e.message;
|
||
}
|
||
}
|
||
|
||
async function doEmailAuth() {
|
||
const code = document.getElementById('authCode').value.trim();
|
||
if (!code || code.length !== 6) { document.getElementById('authError').textContent = '请输入6位验证码'; return; }
|
||
document.getElementById('authError').textContent = '验证中...';
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'verify_email_code');
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({player: currentPlayer, code: code, web_token: TOKEN})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
AUTHENTICATED = true;
|
||
NEED_PASSWORD = false;
|
||
currentPlayer = data.data.player || currentPlayer;
|
||
localStorage.setItem('sdf1_player', currentPlayer);
|
||
document.getElementById('playerInfo').textContent = '已登录: ' + currentPlayer;
|
||
closeModal();
|
||
toast('邮箱验证成功', 'success');
|
||
switchPage(currentPage);
|
||
} else {
|
||
document.getElementById('authError').textContent = data.message || '验证失败';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('authError').textContent = '连接失败: ' + e.message;
|
||
}
|
||
}
|
||
|
||
async function requirePasswordForAction(action) {
|
||
if (AUTHENTICATED) return true;
|
||
return new Promise((resolve) => {
|
||
checkAccess(action, (result) => {
|
||
if (result.ok) resolve(true);
|
||
else if (result.need_password) resolve(false);
|
||
else { toast(result.message || '验证失败', 'error'); resolve(false); }
|
||
});
|
||
});
|
||
}
|
||
|
||
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:24px;max-width:400px">
|
||
输入游戏内密码登录,无需在游戏里敲指令
|
||
</p>
|
||
<!-- 密码登录表单 -->
|
||
<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="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>
|
||
<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() {
|
||
IS_PREVIEW = true;
|
||
TOKEN = '';
|
||
currentPlayer = '';
|
||
AUTHENTICATED = false;
|
||
localStorage.removeItem('sdf1_token');
|
||
localStorage.removeItem('sdf1_player');
|
||
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;
|
||
// 隐藏C位登录按钮
|
||
document.querySelector('.guest-center-login').classList.remove('show');
|
||
// 停止定时弹窗
|
||
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();
|
||
// 重新显示C位登录按钮(如果还是游客模式)
|
||
if (IS_PREVIEW) document.querySelector('.guest-center-login').classList.add('show');
|
||
// 重新启动定时弹窗
|
||
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; }
|
||
// 只要游客模式就弹窗,忽略glassLoginDismissed
|
||
openGlassLogin();
|
||
}, 60000);
|
||
}
|
||
|
||
function switchPage(page) {
|
||
currentPage = page;
|
||
document.querySelectorAll('.sidebar-item').forEach(el => {
|
||
el.classList.toggle('active', el.dataset.page === page);
|
||
});
|
||
document.querySelector('.sidebar').style.display = 'block';
|
||
const c = document.getElementById('content');
|
||
if (!c) return;
|
||
if (page === 'dashboard') renderDashboard(c);
|
||
else if (page === 'shop') renderShop(c);
|
||
else if (page === 'lands') renderLands(c);
|
||
else if (page === 'cdk') renderCDK(c);
|
||
else if (page === 'balance') renderBalance(c);
|
||
else if (page === 'account') renderAccount(c);
|
||
else if (page === 'ticket') renderTicket(c);
|
||
}
|
||
|
||
// ★ 新增:Token轮询检查(不续期,只检查是否被Java端主动销毁)
|
||
function startTokenPolling() {
|
||
if (tokenPollTimer) clearInterval(tokenPollTimer);
|
||
|
||
// 每1分钟检查一次token是否仍然有效
|
||
tokenPollTimer = setInterval(() => {
|
||
if (TOKEN && !IS_PREVIEW && authVerified) {
|
||
checkTokenValidity();
|
||
}
|
||
}, 60 * 1000); // 1分钟
|
||
}
|
||
|
||
// ★ 检查token有效性(不续期,只检查)
|
||
async function checkTokenValidity() {
|
||
if (!TOKEN || IS_PREVIEW) return;
|
||
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'web_access_check');
|
||
url.searchParams.set('web_token', TOKEN);
|
||
url.searchParams.set('access_action', 'view');
|
||
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
|
||
if (!data.success) {
|
||
// Token可能已失效或过期
|
||
if (data.need_password || data.need_game_login || data.need_register) {
|
||
// 需要进一步验证,不视为token无效
|
||
return;
|
||
}
|
||
|
||
// Token确实无效(可能被Java端主动销毁)
|
||
console.log('[Token] Token验证失败:', data.message);
|
||
TOKEN = '';
|
||
AUTHENTICATED = false;
|
||
localStorage.removeItem('sdf1_token');
|
||
localStorage.removeItem('sdf1_player');
|
||
currentPlayer = '';
|
||
IS_PREVIEW = true;
|
||
authVerified = false;
|
||
document.getElementById('playerInfo').textContent = '游客模式';
|
||
document.getElementById('previewBadge').style.display = 'inline';
|
||
showLoginPrompt();
|
||
}
|
||
} catch (e) {
|
||
console.log('[Token] Token检查异常:', e.message);
|
||
}
|
||
}
|
||
|
||
async function api(endpoint, params = {}) {
|
||
if (TOKEN) params.token = TOKEN;
|
||
const url = new URL(API + endpoint, location.href);
|
||
Object.entries(params).forEach(([k,v]) => url.searchParams.set(k, v));
|
||
const res = await fetch(url);
|
||
return await res.json();
|
||
}
|
||
|
||
// 通知Java插件立即同步数据
|
||
async function notifyJavaSync() {
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'request_immediate_sync');
|
||
url.searchParams.set('player', currentPlayer);
|
||
await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'} });
|
||
} catch(e) {
|
||
// 静默失败,不影响用户体验
|
||
}
|
||
}
|
||
|
||
// ===== 商城(分类标签页) =====
|
||
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; }
|
||
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 (shopCategories.length === 0) {
|
||
html += '<div class="empty">暂无商品</div>';
|
||
} else {
|
||
// 分类标签
|
||
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] || '🧱';
|
||
}
|
||
|
||
function showBuyModal(id, name, price, stock) {
|
||
if (IS_PREVIEW) { toast('预览模式下无法购买', 'error'); return; }
|
||
if (stock == 0) { toast('商品已售罄', 'error'); return; }
|
||
document.getElementById('modalTitle').textContent = '购买: ' + name;
|
||
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>`;
|
||
document.getElementById('modalConfirm').onclick = () => doBuy(id);
|
||
document.getElementById('buyAmount').oninput = function() { document.getElementById('buyTotal').textContent = this.value * price; };
|
||
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 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})
|
||
});
|
||
const data = await res.json();
|
||
if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }
|
||
closeModal();
|
||
toast(data.message, data.success ? 'success' : 'error');
|
||
if (data.success) {
|
||
// 购买成功 → 通知Java插件立即同步 + 刷新当前页
|
||
setTimeout(() => {
|
||
notifyJavaSync();
|
||
document.getElementById('balanceResult')?.parentElement && switchPage('balance');
|
||
setTimeout(() => switchPage('shop'), 500);
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
// ===== 个人中心仪表盘 =====
|
||
async function renderDashboard(el) {
|
||
if (IS_PREVIEW || !currentPlayer || (!AUTHENTICATED && NEED_PASSWORD)) {
|
||
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 = `
|
||
<div class="card">
|
||
<h2>CDK兑换</h2>
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">输入CDK兑换码,获得债券奖励</p>
|
||
${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>
|
||
</div>`;
|
||
}
|
||
|
||
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})
|
||
});
|
||
const data = await res.json();
|
||
if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }
|
||
if (data.success) {
|
||
document.getElementById('cdkResult').innerHTML = `<div class="card" style="border-color:var(--green)"><h2 style="color:var(--green)">兑换成功!</h2><p>获得 ${data.data.amount} 债券</p><p>当前余额: ${data.data.balance_after} 债券</p><p style="color:var(--dim);font-size:12px;margin-top:8px">数据已同步,请刷新页面查看最新余额</p></div>`;
|
||
// CDK兑换成功后 → 通知Java同步 + 3秒刷新余额
|
||
setTimeout(() => notifyJavaSync(), 500);
|
||
setTimeout(() => doQueryBalance(), 3000);
|
||
} else {
|
||
document.getElementById('cdkResult').innerHTML = `<div class="card" style="border-color:var(--red)"><h2 style="color:var(--red)">兑换失败</h2><p>${data.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
// ===== 余额 =====
|
||
function renderBalance(el) {
|
||
if (!currentPlayer || (!AUTHENTICATED && NEED_PASSWORD)) {
|
||
el.innerHTML = '<div class="card"><h2>余额查询</h2><div style="text-align:center;padding:40px 20px"><div style="font-size:48px;margin-bottom:16px">🔒</div><p style="color:var(--dim);font-size:14px;margin-bottom:20px">请先登录游戏</p></div></div>';
|
||
return;
|
||
}
|
||
el.innerHTML = `
|
||
<div class="card">
|
||
<h2>余额查询</h2>
|
||
<div id="balanceResult" style="margin-top:16px"></div>
|
||
</div>`;
|
||
doQueryBalance();
|
||
}
|
||
|
||
async function doQueryBalance() {
|
||
if (!currentPlayer) return;
|
||
try {
|
||
const url = new URL(API + 'balance.php', location.href);
|
||
url.searchParams.set('action', 'query');
|
||
url.searchParams.set('player', currentPlayer);
|
||
if (TOKEN) url.searchParams.set('token', TOKEN);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
const div = document.getElementById('balanceResult');
|
||
if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }
|
||
if (data.success) {
|
||
const d = data.data;
|
||
div.innerHTML = `<div class="stat-row"><div class="stat-box"><div class="value" style="color:var(--green)">${d.bonds}</div><div class="label">债券</div></div><div class="stat-box"><div class="value" style="color:var(--purple)">${d.points}</div><div class="label">积分</div></div></div><div style="text-align:center;color:var(--dim);font-size:12px;margin-top:8px">数据${d.freshness || '状态未知'} | 以游戏内实际数据为准</div>`;
|
||
} else {
|
||
div.innerHTML = `<div class="card" style="border-color:var(--red)">${data.message}</div>`;
|
||
}
|
||
} catch(e) {
|
||
document.getElementById('balanceResult').innerHTML = '<div class="card" style="border-color:var(--red)">查询失败: '+e.message+'</div>';
|
||
}
|
||
}
|
||
|
||
// ===== 账号 =====
|
||
function renderAccount(el) {
|
||
if (IS_PREVIEW || (!AUTHENTICATED && NEED_PASSWORD)) {
|
||
el.innerHTML = `<div class="card"><h2>账号信息</h2><div style="text-align:center;padding:40px 20px"><div style="font-size:48px;margin-bottom:16px">🔒</div><p style="color:var(--dim);font-size:14px;margin-bottom:20px">游客模式下无法查看账号信息</p><p style="color:var(--dim);font-size:12px">请先登录游戏获取Web访问令牌</p></div></div>`;
|
||
return;
|
||
}
|
||
if (!currentPlayer) {
|
||
el.innerHTML = '<div class="card"><h2>账号信息</h2><div style="text-align:center;padding:40px 20px"><div style="font-size:48px;margin-bottom:16px">🔒</div><p style="color:var(--dim);font-size:14px;margin-bottom:20px">请先登录游戏</p></div></div>';
|
||
return;
|
||
}
|
||
el.innerHTML = `
|
||
<div class="card">
|
||
<h2>账号信息</h2>
|
||
<div id="accountResult" style="margin-top:16px"></div>
|
||
</div>`;
|
||
doQueryAccount();
|
||
}
|
||
|
||
async function doQueryAccount() {
|
||
if (!currentPlayer) return;
|
||
const res = await api('register.php', {action: 'query', player: currentPlayer, token: TOKEN});
|
||
const div = document.getElementById('accountResult');
|
||
if (res.success) {
|
||
const d = res.data;
|
||
const regTime = d.register_time ? new Date(d.register_time * 1000).toLocaleString() : '未知';
|
||
const loginTime = d.last_login_time ? new Date(d.last_login_time * 1000).toLocaleString() : '未知';
|
||
div.innerHTML = `<table class="table"><tr><td style="color:var(--dim)">用户名</td><td>${d.player_name}</td></tr><tr><td style="color:var(--dim)">注册时间</td><td>${regTime}</td></tr><tr><td style="color:var(--dim)">最后登录</td><td>${loginTime}</td></tr><tr><td style="color:var(--dim)">积分</td><td>${d.points || 0}</td></tr><tr><td style="color:var(--dim)">礼包阶段</td><td>${d.gift_stage || 0}</td></tr><tr><td style="color:var(--dim)">在线时长</td><td>${Math.floor((d.total_online_time||0)/3600)}小时</td></tr></table>`;
|
||
} else {
|
||
div.innerHTML = `<div class="card" style="border-color:var(--red)">${res.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ===== 通用 =====
|
||
function toast(msg, type = 'success') {
|
||
const t = document.createElement('div');
|
||
t.className = 'toast ' + type;
|
||
t.textContent = msg;
|
||
document.body.appendChild(t);
|
||
setTimeout(() => t.remove(), 3000);
|
||
}
|
||
|
||
// ★ 毛玻璃Alert/Confirm弹窗(替代原生alert/confirm)
|
||
let _glassAlertResolve = null;
|
||
function glassAlertResolve(val) {
|
||
document.getElementById('glassAlertOverlay').classList.remove('show');
|
||
if (_glassAlertResolve) { _glassAlertResolve(val); _glassAlertResolve = null; }
|
||
}
|
||
function glassAlert(msg, icon = '⚠️') {
|
||
return new Promise(resolve => {
|
||
_glassAlertResolve = resolve;
|
||
document.getElementById('glassAlertIcon').textContent = icon;
|
||
document.getElementById('glassAlertMsg').textContent = msg;
|
||
document.getElementById('glassAlertBtns').innerHTML = '<button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>';
|
||
document.getElementById('glassAlertOverlay').classList.add('show');
|
||
});
|
||
}
|
||
function glassConfirm(msg, icon = '❓') {
|
||
return new Promise(resolve => {
|
||
_glassAlertResolve = resolve;
|
||
document.getElementById('glassAlertIcon').textContent = icon;
|
||
document.getElementById('glassAlertMsg').textContent = msg;
|
||
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 toggleLightTheme() {
|
||
document.body.classList.toggle('light-theme');
|
||
const isLight = document.body.classList.contains('light-theme');
|
||
localStorage.setItem('sdf1_player_theme', isLight ? 'light' : 'dark');
|
||
toast(isLight ? '浅色主题' : '深色主题', 'success');
|
||
}
|
||
|
||
function showThemePicker() {
|
||
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:2000';
|
||
overlay.id = 'themePickerOverlay';
|
||
overlay.innerHTML = `
|
||
<div id="themePickerModal" style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;width:500px;max-width:90%;position:relative">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||
<h3 style="margin:0">🎨 选择背景颜色</h3>
|
||
<button onclick="document.getElementById('themePickerOverlay').remove()" style="background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer">✕</button>
|
||
</div>
|
||
<div class="theme-picker" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#0d1117;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#0d1117')"></div>
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#1a1e2e;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#1a1e2e')"></div>
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#0f4c75;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#0f4c75')"></div>
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#1b2631;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#1b2631')"></div>
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#2c3e50;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#2c3e50')"></div>
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#23272a;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#23272a')"></div>
|
||
<div class="color-btn" style="width:36px;height:36px;border-radius:6px;background:#ffffff;border:2px solid var(--border);cursor:pointer" onclick="setPlayerTheme('#ffffff')"></div>
|
||
</div>
|
||
<div style="margin-top:16px">
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<span style="color:var(--dim);font-size:12px;white-space:nowrap">R</span>
|
||
<input type="number" id="rgbR" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
|
||
<span style="color:var(--dim);font-size:12px;white-space:nowrap">G</span>
|
||
<input type="number" id="rgbG" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
|
||
<span style="color:var(--dim);font-size:12px;white-space:nowrap">B</span>
|
||
<input type="number" id="rgbB" min="0" max="255" placeholder="0-255" style="width:70px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;text-align:center">
|
||
<span style="color:var(--dim);font-size:11px">或</span>
|
||
<input type="text" id="customColor" placeholder="#1a237e" class="color-input" style="flex:1;min-width:120px">
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
|
||
<button class="btn" onclick="document.getElementById('themePickerOverlay').remove()">取消</button>
|
||
<button class="btn btn-blue" onclick="applyPlayerCustomColor()">应用</button>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(overlay);
|
||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||
}
|
||
|
||
function setPlayerTheme(color) {
|
||
document.documentElement.style.setProperty('--bg', color);
|
||
document.body.style.background = color;
|
||
localStorage.setItem('sdf1_player_bg', color);
|
||
toast('背景颜色已应用', 'success');
|
||
}
|
||
|
||
// 浅色主题下的卡片/边框颜色也自动调整
|
||
document.querySelectorAll('.light-theme').forEach(el => {});
|
||
|
||
// 自动切换card/border颜色配合自定义背景
|
||
function applyBgToTheme(bgColor) {
|
||
// 如果背景是浅色,自动切换card颜色
|
||
document.body.classList.add('light-theme');
|
||
document.body.style.background = bgColor;
|
||
document.documentElement.style.setProperty('--bg', bgColor);
|
||
}
|
||
|
||
function applyPlayerCustomColor() {
|
||
let rVal = document.getElementById('rgbR').value.trim();
|
||
let gVal = document.getElementById('rgbG').value.trim();
|
||
let bVal = document.getElementById('rgbB').value.trim();
|
||
const hexVal = document.getElementById('customColor').value.trim();
|
||
|
||
let color = null;
|
||
|
||
if (hexVal) {
|
||
if (/^[0-9A-Fa-f]{6}$/.test(hexVal)) {
|
||
color = '#' + hexVal;
|
||
} else if (/^#[0-9A-Fa-f]{6}$/.test(hexVal)) {
|
||
color = hexVal;
|
||
} else {
|
||
toast('请输入有效的十六进制颜色,如 1a237e 或 #1a237e', 'err');
|
||
return;
|
||
}
|
||
}
|
||
else if (rVal !== '' || gVal !== '' || bVal !== '') {
|
||
if (rVal === '') rVal = 0;
|
||
if (gVal === '') gVal = 0;
|
||
if (bVal === '') bVal = 0;
|
||
const r = parseInt(rVal);
|
||
const g = parseInt(gVal);
|
||
const b = parseInt(bVal);
|
||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||
color = 'rgb(' + r + ',' + g + ',' + b + ')';
|
||
} else {
|
||
toast('RGB 值必须在 0-255 之间', 'err');
|
||
return;
|
||
}
|
||
}
|
||
else {
|
||
toast('请输入十六进制颜色(如 1a237e)或填写任意一个 RGB 值', 'err');
|
||
return;
|
||
}
|
||
|
||
if (color) {
|
||
setPlayerTheme(color);
|
||
document.querySelector('[onclick*="overlay.remove"]').click();
|
||
}
|
||
}
|
||
|
||
// 加载保存的主题
|
||
(function() {
|
||
const savedBg = localStorage.getItem('sdf1_player_bg');
|
||
if (savedBg) {
|
||
document.documentElement.style.setProperty('--bg', savedBg);
|
||
document.body.style.background = savedBg;
|
||
}
|
||
})();
|
||
|
||
function closeModal() {
|
||
document.getElementById('modalOverlay').style.display = 'none';
|
||
// ★ 密码验证被取消 → 清除token,进入游客模式(防止未认证玩家数据泄露)
|
||
if (NEED_PASSWORD && !AUTHENTICATED) {
|
||
showGuestMode();
|
||
}
|
||
}
|
||
|
||
function confirmModal() {}
|
||
|
||
// ===== 忘记密码 =====
|
||
function showResetPasswordModal() {
|
||
closeModal();
|
||
document.getElementById('modalTitle').textContent = '重置密码';
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">系统会自动读取您注册时绑定的邮箱发送重置链接</p>
|
||
<div class="row"><label>玩家名</label><input type="text" id="resetPlayer" value="${currentPlayer}" readonly style="width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px"></div>
|
||
<div id="resetError" style="color:var(--red);font-size:12px;margin-top:4px"></div>`;
|
||
document.getElementById('modalConfirm').textContent = '发送重置链接';
|
||
document.getElementById('modalConfirm').onclick = () => doResetPassword();
|
||
document.getElementById('modalOverlay').style.display = 'flex';
|
||
}
|
||
|
||
// ===== 修改密码 =====
|
||
function showChangePasswordModal() {
|
||
closeModal();
|
||
document.getElementById('modalTitle').textContent = '修改密码';
|
||
document.getElementById('modalBody').innerHTML = `
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">修改您的登录密码</p>
|
||
<div class="row"><label>当前密码</label><input type="password" id="changePwdOld" style="width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px"></div>
|
||
<div class="row"><label>新密码</label><input type="password" id="changePwdNew" placeholder="6-20位" style="width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px"></div>
|
||
<div class="row"><label>确认新密码</label><input type="password" id="changePwdConfirm" style="width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px"></div>
|
||
<div id="changePwdError" style="color:var(--red);font-size:12px;margin-top:4px"></div>`;
|
||
document.getElementById('modalConfirm').textContent = '修改密码';
|
||
document.getElementById('modalConfirm').onclick = () => doChangePassword();
|
||
document.getElementById('modalOverlay').style.display = 'flex';
|
||
}
|
||
|
||
async function doChangePassword() {
|
||
const oldPwd = document.getElementById('changePwdOld').value.trim();
|
||
const newPwd = document.getElementById('changePwdNew').value.trim();
|
||
const confirmPwd = document.getElementById('changePwdConfirm').value.trim();
|
||
|
||
if (!oldPwd || !newPwd || !confirmPwd) { document.getElementById('changePwdError').textContent = '请填写所有字段'; return; }
|
||
if (newPwd !== confirmPwd) { document.getElementById('changePwdError').textContent = '两次输入的密码不一致'; return; }
|
||
if (newPwd.length < 6 || newPwd.length > 20) { document.getElementById('changePwdError').textContent = '密码长度必须为6-20位'; return; }
|
||
|
||
document.getElementById('changePwdError').textContent = '处理中...';
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'change_password');
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({player: currentPlayer, old_password: oldPwd, new_password: newPwd})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
document.getElementById('changePwdError').innerHTML = '<span style="color:var(--green)">密码修改成功,请重新登录</span>';
|
||
setTimeout(() => { closeModal(); location.reload(); }, 2000);
|
||
} else {
|
||
document.getElementById('changePwdError').textContent = data.message || '修改失败';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('changePwdError').textContent = '连接失败: ' + e.message;
|
||
}
|
||
}
|
||
|
||
// ===== 重置密码(后端判断有无邮箱) =====
|
||
async function doResetPassword() {
|
||
const player = document.getElementById('resetPlayer').value.trim();
|
||
if (!player) { document.getElementById('resetError').textContent = '请输入玩家名'; return; }
|
||
document.getElementById('resetError').textContent = '发送中...';
|
||
try {
|
||
const url = new URL(API + 'sync.php', location.href);
|
||
url.searchParams.set('action', 'send_reset_password_link');
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({player: player})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
document.getElementById('resetError').innerHTML = '<span style="color:var(--green)">✅ 重置链接已发送到邮箱,请查收</span>';
|
||
setTimeout(() => { closeModal(); }, 2000);
|
||
} else {
|
||
// 根据错误信息判断情况
|
||
if (data.message && data.message.includes('未绑定邮箱')) {
|
||
// 玩家没有绑定邮箱 → 显示管理员审核提示
|
||
document.getElementById('resetError').innerHTML = `
|
||
<div style="text-align:center">
|
||
<div style="font-size:32px;margin-bottom:12px">📧</div>
|
||
<p style="color:var(--text);font-size:14px;margin-bottom:8px">该账号未绑定邮箱</p>
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:16px">
|
||
您的账号还没有绑定邮箱,无法通过邮件重置密码。<br>
|
||
已自动提交申请,<b style="color:var(--accent)">请联系管理员</b>验证身份后帮您重置密码。<br>
|
||
<span style="color:var(--yellow);font-size:12px">审核通常需要 1-24 小时</span>
|
||
</p>
|
||
<div style="background:rgba(59,130,246,0.1);border:1px solid var(--accent);border-radius:8px;padding:12px;text-align:left">
|
||
<p style="color:var(--text);font-size:12px;margin:0"><b>管理员联系方式:</b></p>
|
||
<p style="color:var(--dim);font-size:12px;margin:4px 0 0 0">请联系服务器管理群或在线管理获取帮助</p>
|
||
</div>
|
||
</div>`;
|
||
} else {
|
||
document.getElementById('resetError').textContent = data.message || '发送失败';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('resetError').textContent = '连接失败: ' + e.message;
|
||
}
|
||
}
|
||
|
||
// ===== 工单系统 =====
|
||
let ticketState = { view: 'list', filter: 'all' };
|
||
|
||
async function renderTicket(el) {
|
||
if (IS_PREVIEW || !AUTHENTICATED) {
|
||
el.innerHTML = `<div class="card"><h2>📋 工单系统</h2><p style="color:var(--dim)">请先登录后使用工单功能</p></div>`;
|
||
return;
|
||
}
|
||
if (ticketState.view === 'list') await renderTicketList(el);
|
||
else if (ticketState.view === 'create') renderTicketCreate(el);
|
||
else if (ticketState.view === 'detail') await renderTicketDetail(el, ticketState.ticketId);
|
||
}
|
||
|
||
function renderTicketNav() {
|
||
const f = ticketState.filter;
|
||
return `<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center">
|
||
<button class="btn btn-primary" onclick="ticketState.view='create';renderTicket(document.getElementById('content'))" style="font-size:13px">✏️ 提交工单</button>
|
||
<button class="btn ${f==='all'?'':'btn-dim'}" onclick="ticketState.filter='all';ticketState.view='list';renderTicket(document.getElementById('content'))" style="font-size:12px;padding:5px 12px">全部</button>
|
||
<button class="btn ${f==='submitted'?'':'btn-dim'}" onclick="ticketState.filter='submitted';ticketState.view='list';renderTicket(document.getElementById('content'))" style="font-size:12px;padding:5px 12px">⏳ 已提交</button>
|
||
<button class="btn ${f==='replied'?'':'btn-dim'}" onclick="ticketState.filter='replied';ticketState.view='list';renderTicket(document.getElementById('content'))" style="font-size:12px;padding:5px 12px">💬 已回复</button>
|
||
<button class="btn ${f==='withdrawn'?'':'btn-dim'}" onclick="ticketState.filter='withdrawn';ticketState.view='list';renderTicket(document.getElementById('content'))" style="font-size:12px;padding:5px 12px">🔙 已撤销</button>
|
||
<button class="btn ${f==='rejected'?'':'btn-dim'}" onclick="ticketState.filter='rejected';ticketState.view='list';renderTicket(document.getElementById('content'))" style="font-size:12px;padding:5px 12px">🚫 已驳回</button>
|
||
</div>`;
|
||
}
|
||
|
||
async function renderTicketList(el) {
|
||
el.innerHTML = `<div class="card"><h2>📋 工单系统</h2>${renderTicketNav()}<p style="color:var(--dim)">加载中...</p></div>`;
|
||
try {
|
||
const url = new URL(API + 'ticket.php', location.href);
|
||
url.searchParams.set('action', 'my_list');
|
||
url.searchParams.set('token', TOKEN);
|
||
if (ticketState.filter !== 'all') url.searchParams.set('status', ticketState.filter);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
if (!data.success) { el.innerHTML = `<div class="card"><h2>📋 工单系统</h2><p style="color:var(--red)">${data.message}</p></div>`; return; }
|
||
|
||
const list = data.data.list;
|
||
let html = `<div class="card"><h2>📋 我的工单 (${data.data.total})</h2>${renderTicketNav()}`;
|
||
if (list.length === 0) {
|
||
html += `<p style="color:var(--dim);text-align:center;padding:40px 0">暂无工单</p>`;
|
||
} else {
|
||
const typeMap = {bug:'🐛 Bug反馈',help:'❓ 求助',report:'📢 举报',apply:'📝 申请',other:'📎 其他'};
|
||
const statusMap = {submitted:{text:'⏳ 已提交',color:'var(--yellow)'},replied:{text:'💬 已回复',color:'var(--accent)'},completed:{text:'✅ 已完结',color:'var(--green)'},withdrawn:{text:'🔙 已撤销',color:'var(--dim)'},rejected:{text:'🚫 已驳回',color:'var(--red)'}};
|
||
html += `<div style="display:flex;flex-direction:column;gap:8px">`;
|
||
for (const t of list) {
|
||
const s = statusMap[t.status] || {text: t.status, color: 'var(--dim)'};
|
||
const date = new Date(t.created_at * 1000).toLocaleString('zh-CN');
|
||
html += `<div onclick="ticketState.view='detail';ticketState.ticketId=${t.id};renderTicket(document.getElementById('content'))" style="background:rgba(88,166,255,0.05);border:1px solid var(--border);border-radius:8px;padding:14px 16px;cursor:pointer;transition:all 0.2s" onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||
<span style="font-weight:600;font-size:14px">#${t.id} ${escHtml(t.title)}</span>
|
||
<span style="color:${s.color};font-size:12px">${s.text}</span>
|
||
</div>
|
||
<div style="display:flex;gap:16px;font-size:12px;color:var(--dim)">
|
||
<span>${typeMap[t.type]||t.type}</span>
|
||
<span>${date}</span>
|
||
${t.assigned_to ? '<span>👤 '+escHtml(t.assigned_to)+'</span>' : ''}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
html += `</div>`;
|
||
}
|
||
html += `</div>`;
|
||
el.innerHTML = html;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="card"><h2>📋 工单系统</h2><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
function renderTicketCreate(el) {
|
||
const types = [{v:'bug',l:'🐛 Bug反馈'},{v:'help',l:'❓ 求助'},{v:'report',l:'📢 举报'},{v:'apply',l:'📝 申请'},{v:'other',l:'📎 其他'}];
|
||
let options = types.map(t => `<option value="${t.v}">${t.l}</option>`).join('');
|
||
el.innerHTML = `<div class="card">
|
||
<h2>✏️ 提交工单</h2>
|
||
<div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">工单类型</label>
|
||
<select id="ticketType" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px">${options}</select></div>
|
||
<div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">标题</label>
|
||
<input id="ticketTitle" placeholder="简要描述您的问题" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px"></div>
|
||
<div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">详细描述</label>
|
||
<textarea id="ticketDesc" rows="6" placeholder="详细描述您的问题,支持Markdown格式" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px;resize:vertical;font-family:monospace"></textarea></div>
|
||
<p style="color:var(--dim);font-size:12px;margin-bottom:12px">💡 支持标准Markdown语法:**粗体**、*斜体*、\`代码\`、列表、标题等</p>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn btn-primary" onclick="submitTicket()">提交工单</button>
|
||
<button class="btn" onclick="ticketState.view='list';renderTicket(document.getElementById('content'))">取消</button>
|
||
</div>
|
||
<p id="ticketError" style="color:var(--red);margin-top:8px;font-size:13px"></p>
|
||
</div>`;
|
||
}
|
||
|
||
async function submitTicket() {
|
||
const type = document.getElementById('ticketType').value;
|
||
const title = document.getElementById('ticketTitle').value.trim();
|
||
const desc = document.getElementById('ticketDesc').value.trim();
|
||
const errEl = document.getElementById('ticketError');
|
||
if (!title) { errEl.textContent = '请输入标题'; return; }
|
||
if (!desc) { errEl.textContent = '请输入描述'; return; }
|
||
|
||
try {
|
||
const url = new URL(API + 'ticket.php', location.href);
|
||
url.searchParams.set('action', 'create');
|
||
url.searchParams.set('token', TOKEN);
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({type, title, description: desc})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
ticketState.view = 'list';
|
||
renderTicket(document.getElementById('content'));
|
||
} else {
|
||
errEl.textContent = data.message;
|
||
}
|
||
} catch (e) {
|
||
errEl.textContent = '提交失败: ' + e.message;
|
||
}
|
||
}
|
||
|
||
async function renderTicketDetail(el, id) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载中...</p></div>`;
|
||
try {
|
||
const url = new URL(API + 'ticket.php', location.href);
|
||
url.searchParams.set('action', 'detail');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('id', id);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
if (!data.success) { el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.message}</p><button class="btn" onclick="ticketState.view='list';renderTicket(document.getElementById('content'))">返回</button></div>`; return; }
|
||
|
||
const t = data.data;
|
||
const typeMap = {bug:'🐛 Bug反馈',help:'❓ 求助',report:'📢 举报',apply:'📝 申请',other:'📎 其他'};
|
||
const statusMap = {submitted:{text:'⏳ 已提交',color:'var(--yellow)'},replied:{text:'💬 已回复',color:'var(--accent)'},completed:{text:'✅ 已完结',color:'var(--green)'},withdrawn:{text:'🔙 已撤销',color:'var(--dim)'},rejected:{text:'🚫 已驳回',color:'var(--red)'}};
|
||
const s = statusMap[t.status] || {text: t.status, color: 'var(--dim)'};
|
||
const date = new Date(t.created_at * 1000).toLocaleString('zh-CN');
|
||
const canReply = !['withdrawn','rejected','completed'].includes(t.status);
|
||
|
||
let html = `<div class="card">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||
<h2 style="margin:0">#${t.id} ${escHtml(t.title)}</h2>
|
||
<span style="color:${s.color};font-size:13px">${s.text}</span>
|
||
</div>
|
||
<div style="display:flex;gap:16px;font-size:13px;color:var(--dim);margin-bottom:16px">
|
||
<span>${typeMap[t.type]||t.type}</span>
|
||
<span>👤 ${escHtml(t.requester)}</span>
|
||
<span>📅 ${date}</span>
|
||
${t.assigned_to ? '<span>🧑💻 '+escHtml(t.assigned_to)+'</span>' : ''}
|
||
</div>`;
|
||
|
||
// 描述
|
||
html += `<div style="background:rgba(88,166,255,0.05);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px">
|
||
<div style="color:var(--dim);font-size:12px;margin-bottom:8px">描述</div>
|
||
<div style="font-size:14px;line-height:1.6">${renderMarkdown(t.description || '无描述')}</div>
|
||
</div>`;
|
||
|
||
// 驳回原因
|
||
if (t.reject_reason) {
|
||
html += `<div style="background:rgba(248,81,73,0.1);border:1px solid var(--red);border-radius:8px;padding:16px;margin-bottom:16px">
|
||
<div style="color:var(--red);font-size:12px;margin-bottom:8px">🚫 驳回原因</div>
|
||
<div style="font-size:14px">${renderMarkdown(t.reject_reason)}</div>
|
||
</div>`;
|
||
}
|
||
|
||
// 回复列表
|
||
html += `<div style="margin-bottom:16px"><div style="color:var(--dim);font-size:12px;margin-bottom:8px">💬 回复记录 (${t.replies ? t.replies.length : 0})</div>`;
|
||
if (t.replies && t.replies.length > 0) {
|
||
for (const r of t.replies) {
|
||
const rDate = new Date(r.created_at * 1000).toLocaleString('zh-CN');
|
||
const roleColors = {user:'var(--accent)',admin:'var(--red)',provider:'var(--green)'};
|
||
const roleNames = {user:'玩家',admin:'管理员',provider:'服务商'};
|
||
const bgColor = r.role === 'admin' ? 'rgba(248,81,73,0.08)' : r.role === 'provider' ? 'rgba(63,185,80,0.08)' : 'rgba(88,166,255,0.08)';
|
||
html += `<div style="background:${bgColor};border-radius:8px;padding:12px 16px;margin-bottom:8px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||
<span style="font-size:13px"><span style="color:${roleColors[r.role]||'var(--text)'};font-weight:600">${escHtml(r.sender)}</span> <span style="color:var(--dim);font-size:11px">(${roleNames[r.role]||r.role})</span></span>
|
||
<span style="color:var(--dim);font-size:11px">${rDate}</span>
|
||
</div>
|
||
<div style="font-size:14px;line-height:1.6">${renderMarkdown(r.message)}</div>
|
||
</div>`;
|
||
}
|
||
} else {
|
||
html += `<p style="color:var(--dim);font-size:13px">暂无回复</p>`;
|
||
}
|
||
html += `</div>`;
|
||
|
||
// 回复框
|
||
if (canReply) {
|
||
html += `<div style="margin-bottom:16px">
|
||
<textarea id="ticketReplyMsg" rows="3" placeholder="输入回复内容(支持Markdown)" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px;resize:vertical;font-family:monospace"></textarea>
|
||
<p id="ticketReplyError" style="color:var(--red);font-size:12px;margin-top:4px"></p>
|
||
</div>`;
|
||
}
|
||
|
||
// 操作按钮
|
||
html += `<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn" onclick="ticketState.view='list';renderTicket(document.getElementById('content'))">← 返回列表</button>`;
|
||
if (canReply) {
|
||
html += `<button class="btn btn-primary" onclick="submitTicketReply(${t.id})">发送回复</button>`;
|
||
}
|
||
if (t.status === 'submitted' || t.status === 'replied') {
|
||
html += `<button class="btn" style="color:var(--yellow);border-color:var(--yellow)" onclick="withdrawTicket(${t.id})">撤销工单</button>`;
|
||
}
|
||
html += `</div></div>`;
|
||
|
||
el.innerHTML = html;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p><button class="btn" onclick="ticketState.view='list';renderTicket(document.getElementById('content'))">返回</button></div>`;
|
||
}
|
||
}
|
||
|
||
async function submitTicketReply(id) {
|
||
const msg = document.getElementById('ticketReplyMsg').value.trim();
|
||
const errEl = document.getElementById('ticketReplyError');
|
||
if (!msg) { errEl.textContent = '请输入回复内容'; return; }
|
||
|
||
try {
|
||
const url = new URL(API + 'ticket.php', location.href);
|
||
url.searchParams.set('action', 'reply');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('id', id);
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({message: msg})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
renderTicket(document.getElementById('content'));
|
||
} else {
|
||
errEl.textContent = data.message;
|
||
}
|
||
} catch (e) {
|
||
errEl.textContent = '发送失败: ' + e.message;
|
||
}
|
||
}
|
||
|
||
async function withdrawTicket(id) {
|
||
if (!await glassConfirm('确定要撤销此工单吗?')) return;
|
||
try {
|
||
const url = new URL(API + 'ticket.php', location.href);
|
||
url.searchParams.set('action', 'withdraw');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('id', id);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
ticketState.view = 'list';
|
||
renderTicket(document.getElementById('content'));
|
||
} else {
|
||
glassAlert(data.message);
|
||
}
|
||
} catch (e) {
|
||
glassAlert('撤销失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ★ 简易Markdown渲染器
|
||
function renderInlineMd(text) {
|
||
let s = text;
|
||
s = s.replace(/^######\s+(.+)$/, '<h6 style="margin:2px 0;font-size:12px">$1</h6>');
|
||
s = s.replace(/^#####\s+(.+)$/, '<h5 style="margin:3px 0;font-size:13px">$1</h5>');
|
||
s = s.replace(/^####\s+(.+)$/, '<h4 style="margin:3px 0 2px;font-size:14px">$1</h4>');
|
||
s = s.replace(/^###\s+(.+)$/, '<h4 style="margin:4px 0 3px;font-size:14px">$1</h4>');
|
||
s = s.replace(/^##\s+(.+)$/, '<h3 style="margin:6px 0 4px;font-size:15px">$1</h3>');
|
||
s = s.replace(/^#\s+(.+)$/, '<h2 style="margin:8px 0 5px;font-size:16px">$1</h2>');
|
||
s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||
s = s.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
||
s = s.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
||
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:6px;margin:4px 0">');
|
||
s = s.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--accent)">$1</a>');
|
||
return s;
|
||
}
|
||
|
||
function renderMarkdown(text) {
|
||
if (!text) return '';
|
||
text = text.replace(/\r/g, '');
|
||
try {
|
||
let s = escHtml(text);
|
||
// 1. 代码块提取保护
|
||
let cbs = [];
|
||
s = s.replace(/```(\w*)\n?([\s\S]*?)```/g, function(m, lang, code) {
|
||
let idx = cbs.length;
|
||
cbs.push('<pre style="background:rgba(88,166,255,0.1);padding:12px;border-radius:6px;overflow-x:auto;font-size:13px;white-space:pre-wrap">' + code.replace(/\n$/, '') + '</pre>');
|
||
return '\x00CB' + idx + '\x00';
|
||
});
|
||
// 2. 行内代码保护
|
||
s = s.replace(/`([^`]+)`/g, '<code style="background:rgba(88,166,255,0.15);padding:2px 6px;border-radius:3px;font-size:13px">$1</code>');
|
||
// 3. 引用块处理:提取连续引用行,构建嵌套结构
|
||
let lines = s.split('\n');
|
||
let result = [];
|
||
let bqBuf = [];
|
||
function flushBq() {
|
||
if (bqBuf.length === 0) return;
|
||
let html = '';
|
||
let openDivs = 0;
|
||
for (let bi = 0; bi < bqBuf.length; bi++) {
|
||
let {level, content} = bqBuf[bi];
|
||
let prevLevel = (bi > 0) ? bqBuf[bi - 1].level : 0;
|
||
if (level > prevLevel) {
|
||
for (let j = prevLevel; j < level; j++) {
|
||
let pad = Math.min((j + 1) * 8, 32);
|
||
html += '<div style="border-left:3px solid var(--accent);padding:4px 10px;margin:2px 0 2px ' + pad + 'px;background:rgba(255,255,255,0.03)">';
|
||
openDivs++;
|
||
}
|
||
} else if (level < prevLevel) {
|
||
for (let j = prevLevel; j > level; j--) { html += '</div>'; openDivs--; }
|
||
} else if (bi > 0) {
|
||
html += '<br>';
|
||
}
|
||
let text = renderInlineMd(content);
|
||
if (!text.trim()) text = ' ';
|
||
html += text;
|
||
}
|
||
for (let j = 0; j < openDivs; j++) html += '</div>';
|
||
result.push(html);
|
||
bqBuf = [];
|
||
}
|
||
for (let i = 0; i < lines.length; i++) {
|
||
let m = lines[i].match(/^((?:>[ \t]*)+)(.*)$/);
|
||
if (m) {
|
||
bqBuf.push({ level: (m[1].match(/>/g) || []).length, content: m[2] });
|
||
} else if (bqBuf.length > 0 && !lines[i].trim()) {
|
||
// ★ 空行在引用块内:向前看下一行是否仍是引用
|
||
let isContinuation = false;
|
||
for (let j = i + 1; j < lines.length; j++) {
|
||
if (lines[j].trim()) {
|
||
isContinuation = /^((?:>[ \t]*)+)(.*)$/.test(lines[j]);
|
||
break;
|
||
}
|
||
}
|
||
if (isContinuation) {
|
||
bqBuf.push({ level: bqBuf[bqBuf.length - 1].level, content: '' });
|
||
} else {
|
||
flushBq();
|
||
}
|
||
} else {
|
||
flushBq();
|
||
result.push(lines[i]);
|
||
}
|
||
}
|
||
flushBq();
|
||
s = result.join('\n');
|
||
// 4. 非引用行的MD处理
|
||
s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||
s = s.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
||
s = s.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
||
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:6px;margin:6px 0">');
|
||
s = s.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--accent)">$1</a>');
|
||
s = s.replace(/^######\s+(.+)$/gm, '<h6 style="margin:4px 0;font-size:12px">$1</h6>');
|
||
s = s.replace(/^#####\s+(.+)$/gm, '<h5 style="margin:5px 0;font-size:13px">$1</h5>');
|
||
s = s.replace(/^####\s+(.+)$/gm, '<h4 style="margin:6px 0 4px;font-size:14px">$1</h4>');
|
||
s = s.replace(/^###\s+(.+)$/gm, '<h4 style="margin:8px 0 4px;font-size:14px">$1</h4>');
|
||
s = s.replace(/^##\s+(.+)$/gm, '<h3 style="margin:10px 0 6px;font-size:15px">$1</h3>');
|
||
s = s.replace(/^#\s+(.+)$/gm, '<h2 style="margin:12px 0 8px;font-size:16px">$1</h2>');
|
||
// 5. 表格
|
||
let inTable = false;
|
||
let tableHtml = '';
|
||
let tLines = s.split('\n');
|
||
let tResult = [];
|
||
for (let i = 0; i < tLines.length; i++) {
|
||
let line = tLines[i].trim();
|
||
if (line.match(/^\|(.+)\|$/)) {
|
||
if (line.match(/^\|[\s\-:|]+\|$/)) { inTable = true; continue; }
|
||
if (!inTable && i + 1 < tLines.length && tLines[i+1].trim().match(/^\|[\s\-:|]+\|$/)) {
|
||
inTable = true;
|
||
let cells = line.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
||
tableHtml = '<table style="border-collapse:collapse;width:100%;margin:8px 0;font-size:13px"><tr style="background:rgba(88,166,255,0.15)">';
|
||
for (let c of cells) tableHtml += '<th style="border:1px solid var(--border);padding:6px 10px;text-align:left">' + c + '</th>';
|
||
tableHtml += '</tr>';
|
||
continue;
|
||
}
|
||
if (inTable) {
|
||
let cells = line.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
||
tableHtml += '<tr>';
|
||
for (let c of cells) tableHtml += '<td style="border:1px solid var(--border);padding:6px 10px">' + c + '</td>';
|
||
tableHtml += '</tr>';
|
||
continue;
|
||
}
|
||
}
|
||
if (inTable && tableHtml) {
|
||
tableHtml += '</table>';
|
||
tResult.push(tableHtml);
|
||
tableHtml = '';
|
||
inTable = false;
|
||
}
|
||
tResult.push(tLines[i]);
|
||
}
|
||
if (tableHtml) { tableHtml += '</table>'; tResult.push(tableHtml); }
|
||
s = tResult.join('\n');
|
||
// 6. 列表、分割线、换行
|
||
s = s.replace(/^- (.+)$/gm, '<li style="margin-left:20px;font-size:13px">$1</li>');
|
||
s = s.replace(/^---$/gm, '<hr style="border:none;border-top:1px solid var(--border);margin:12px 0">');
|
||
s = s.replace(/\n/g, '<br>');
|
||
// 7. 还原代码块
|
||
for (let i = 0; i < cbs.length; i++) {
|
||
s = s.replace('\x00CB' + i + '\x00', cbs[i]);
|
||
}
|
||
return s;
|
||
} catch(e) { console.error('[MD_RENDER] Error:', e); return escHtml(text).replace(/\n/g, '<br>'); }
|
||
}
|
||
|
||
// ==================== 领地管理 ====================
|
||
|
||
let landsState = { view: 'list', currentLand: null, currentMember: null };
|
||
|
||
async function renderLands(el) {
|
||
if (!TOKEN) {
|
||
el.innerHTML = `<div class="card" style="text-align:center;padding:40px">
|
||
<div style="font-size:48px;margin-bottom:16px">🏡</div>
|
||
<h2 style="margin:0 0 8px;color:var(--fg)">领地管理</h2>
|
||
<p style="color:var(--dim);margin-bottom:16px">请先登录以管理你的领地</p>
|
||
<button class="btn btn-blue" onclick="openGlassLogin()">🔑 登录</button>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
if (landsState.view === 'detail' && landsState.currentLand) {
|
||
await renderLandDetail(el, landsState.currentLand);
|
||
return;
|
||
}
|
||
|
||
if (landsState.view === 'member_perms' && landsState.currentLand) {
|
||
await renderMemberPerms(el, landsState.currentLand);
|
||
return;
|
||
}
|
||
|
||
if (landsState.view === 'member_perm_detail' && landsState.currentLand && landsState.currentMember) {
|
||
await renderMemberPermDetail(el, landsState.currentLand, landsState.currentMember);
|
||
return;
|
||
}
|
||
|
||
// 加载我的领地列表
|
||
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载领地中...</p></div>`;
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'my_lands');
|
||
url.searchParams.set('token', TOKEN);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
|
||
if (data.needLogin) {
|
||
el.innerHTML = `<div class="card" style="text-align:center;padding:40px">
|
||
<div style="font-size:48px;margin-bottom:16px">🏡</div>
|
||
<h2 style="margin:0 0 8px;color:var(--fg)">领地管理</h2>
|
||
<p style="color:var(--dim);margin-bottom:16px">请先登录以管理你的领地</p>
|
||
<button class="btn btn-blue" onclick="openGlassLogin()">🔑 登录</button>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
if (!data.success) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.error}</p></div>`;
|
||
return;
|
||
}
|
||
|
||
const lands = data.lands || [];
|
||
let html = `<div class="card">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||
<h2 style="margin:0;color:var(--fg)">🏡 我的领地 (${lands.length})</h2>
|
||
<span style="color:var(--dim);font-size:13px">玩家: ${escHtml(data.player)}</span>
|
||
</div>`;
|
||
|
||
if (lands.length === 0) {
|
||
html += `<div style="text-align:center;padding:32px;color:var(--dim)">
|
||
<div style="font-size:40px;margin-bottom:12px">🏗️</div>
|
||
<p>你还没有领地</p>
|
||
<p style="font-size:12px;margin-top:8px">在游戏中输入 <code style="background:rgba(88,166,255,0.1);padding:2px 6px;border-radius:4px;color:var(--accent)">/protect create <领地名></code> 来创建领地</p>
|
||
</div>`;
|
||
} else {
|
||
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">`;
|
||
for (const land of lands) {
|
||
const areaSize = land.area_size || 0;
|
||
const createdAt = land.created_at ? new Date(land.created_at * 1000).toLocaleDateString('zh-CN') : '未知';
|
||
const coordStr = `(${land.x1},${land.z1}) → (${land.x2},${land.z2})`;
|
||
html += `<div onclick="landsState.view='detail';landsState.currentLand='${escHtml(land.name)}';renderLands(document.getElementById('content'))"
|
||
style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:16px;cursor:pointer;transition:all 0.2s"
|
||
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||
<h3 style="margin:0;color:var(--fg);font-size:16px">📍 ${escHtml(land.name)}</h3>
|
||
<span style="color:var(--green);font-size:11px">✦ 所有者</span>
|
||
</div>
|
||
<div style="font-size:13px;color:var(--dim);margin-bottom:4px">🌍 ${escHtml(land.world || '未知')}</div>
|
||
<div style="font-size:13px;color:var(--dim);margin-bottom:4px">📐 ${coordStr}</div>
|
||
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--dim);margin-top:8px">
|
||
<span>📏 ${areaSize} 格²</span>
|
||
<span>📅 ${createdAt}</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
html += `</div>`;
|
||
}
|
||
html += `</div>`;
|
||
|
||
// 领地商店
|
||
html += `<div class="card" style="margin-top:12px">
|
||
<h3 style="margin:0 0 12px;color:var(--fg)">🛍️ 领地权限商店</h3>`;
|
||
try {
|
||
const shopUrl = new URL(API + 'land_api.php', location.href);
|
||
shopUrl.searchParams.set('action', 'land_shop');
|
||
const shopRes = await fetch(shopUrl);
|
||
const shopData = await shopRes.json();
|
||
if (shopData.success && shopData.items && shopData.items.length > 0) {
|
||
html += `<table class="table"><thead><tr>
|
||
<th>领地</th><th>卖家</th><th>权限</th><th>价格(债券)</th><th>时长</th><th>操作</th>
|
||
</tr></thead><tbody>`;
|
||
for (const item of shopData.items) {
|
||
const dur = item.duration >= 86400 ? Math.floor(item.duration/86400)+'天' : Math.floor(item.duration/3600)+'小时';
|
||
html += `<tr>
|
||
<td>${escHtml(item.land_name)}</td>
|
||
<td>${escHtml(item.seller)}</td>
|
||
<td><span style="color:var(--accent)">${escHtml(item.permission)}</span></td>
|
||
<td style="color:var(--green)">${item.price}</td>
|
||
<td>${dur}</td>
|
||
<td><button class="btn btn-blue" style="font-size:11px;padding:3px 8px" onclick="buyLandPermission('${escHtml(item.land_name)}',${item.id})">购买</button></td>
|
||
</tr>`;
|
||
}
|
||
html += `</tbody></table>`;
|
||
} else {
|
||
html += `<p style="color:var(--dim);font-size:13px">暂无在售的领地权限</p>`;
|
||
}
|
||
} catch (e) {
|
||
html += `<p style="color:var(--dim);font-size:13px">商店加载失败</p>`;
|
||
}
|
||
html += `</div>`;
|
||
|
||
el.innerHTML = html;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
async function renderLandDetail(el, landName) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载领地详情...</p></div>`;
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'land_detail');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('name', landName);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
if (!data.success) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.error}</p><button class="btn" onclick="landsState.view='list';landsState.currentLand=null;renderLands(document.getElementById('content'))">← 返回</button></div>`;
|
||
return;
|
||
}
|
||
|
||
const land = data.land;
|
||
const visitors = data.visitors || [];
|
||
const coordStr = `(${land.x1},${land.z1}) → (${land.x2},${land.z2})`;
|
||
|
||
let html = `<div class="card">
|
||
<button class="btn" onclick="landsState.view='list';landsState.currentLand=null;renderLands(document.getElementById('content'))" style="margin-bottom:12px;font-size:13px">← 返回我的领地</button>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||
<h2 style="margin:0;color:var(--fg)">📍 ${escHtml(land.name)}</h2>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:16px">
|
||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
|
||
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">🌍 世界</div>
|
||
<div style="color:var(--fg);font-size:14px">${escHtml(land.world || '未知')}</div>
|
||
</div>
|
||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
|
||
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">📐 坐标</div>
|
||
<div style="color:var(--fg);font-size:14px">${coordStr}</div>
|
||
</div>
|
||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
|
||
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">📏 面积</div>
|
||
<div style="color:var(--fg);font-size:14px">${land.area_size || 0} 格²</div>
|
||
</div>
|
||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
|
||
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">📅 创建时间</div>
|
||
<div style="color:var(--fg);font-size:14px">${land.created_at ? new Date(land.created_at*1000).toLocaleDateString('zh-CN') : '未知'}</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 访客管理
|
||
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">
|
||
<button class="btn btn-blue" onclick="addLandVisitor('${escHtml(land.name)}')">添加访客</button>
|
||
</div>`;
|
||
|
||
if (visitors.length === 0) {
|
||
html += `<p style="color:var(--dim);font-size:13px">暂无访客</p>`;
|
||
} else {
|
||
html += `<table class="table"><thead><tr><th>玩家名</th><th>角色</th><th>授权时间</th><th>操作</th></tr></thead><tbody>`;
|
||
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><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>
|
||
</tr>`;
|
||
}
|
||
html += `</tbody></table>`;
|
||
}
|
||
html += `</div></div>`;
|
||
el.innerHTML = html;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
async function renderMemberPerms(el, landName) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载成员权限...</p></div>`;
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'get_member_perms');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('land', landName);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
if (!data.success) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.error}</p><button class="btn" onclick="landsState.view='detail';renderLands(document.getElementById('content'))">← 返回</button></div>`;
|
||
return;
|
||
}
|
||
|
||
const land = data.land;
|
||
const members = data.members || [];
|
||
const permTypes = data.perm_types || {};
|
||
|
||
let html = `<div class="card">
|
||
<button class="btn" onclick="landsState.view='detail';landsState.currentLand='${escHtml(landName)}';landsState.currentMember=null;renderLands(document.getElementById('content'))" style="margin-bottom:12px;font-size:13px">← 返回领地详情</button>
|
||
<h2 style="margin:0 0 8px;color:var(--fg)">🎯 ${escHtml(landName)} 成员权限</h2>
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:16px">为每个成员设置独立的权限覆盖。启用=覆盖默认值,禁用=使用领地默认值。</p>`;
|
||
|
||
if (members.length === 0) {
|
||
html += `<div style="text-align:center;padding:32px;color:var(--dim)">
|
||
<div style="font-size:40px;margin-bottom:12px">👥</div>
|
||
<p>暂无成员</p>
|
||
<p style="font-size:12px;margin-top:8px">请先在访客管理中添加成员</p>
|
||
</div>`;
|
||
} else {
|
||
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px">`;
|
||
for (const member of members) {
|
||
const permCount = member.perm_map ? Object.keys(member.perm_map).length : 0;
|
||
const hasCustom = permCount > 0;
|
||
html += `<div onclick="landsState.view='member_perm_detail';landsState.currentMember='${escHtml(member.player_name)}';renderLands(document.getElementById('content'))"
|
||
style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:16px;cursor:pointer;transition:all 0.2s"
|
||
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||
<h3 style="margin:0;color:var(--fg);font-size:15px">👤 ${escHtml(member.player_name)}</h3>
|
||
${hasCustom ? '<span style="background:var(--accent);color:#fff;padding:2px 8px;border-radius:12px;font-size:11px">★ 自定义</span>' : '<span style="color:var(--dim);font-size:12px">默认权限</span>'}
|
||
</div>
|
||
<div style="font-size:12px;color:var(--dim)">${permCount} 项自定义权限</div>
|
||
</div>`;
|
||
}
|
||
html += `</div>`;
|
||
}
|
||
html += `</div>`;
|
||
el.innerHTML = html;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
async function addLandVisitor(landName) {
|
||
const input = document.getElementById('newVisitorName');
|
||
if (!input) return;
|
||
const visitor = input.value.trim();
|
||
if (!visitor) return;
|
||
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'add_visitor');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('name', landName);
|
||
url.searchParams.set('visitor', visitor);
|
||
const res = await fetch(url);
|
||
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 removeLandVisitor(landName, player) {
|
||
if (!await glassConfirm(`确定要移除访客 ${player} 吗?`)) return;
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'remove_visitor');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('name', landName);
|
||
url.searchParams.set('visitor', player);
|
||
const res = await fetch(url);
|
||
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);
|
||
}
|
||
}
|
||
|
||
function buyLandPermission(landName, itemId) {
|
||
glassAlert('请在游戏中使用 /protect shop 购买领地权限', '🎮');
|
||
}
|
||
|
||
async function renderMemberPermDetail(el, landName, targetPlayer) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载权限详情...</p></div>`;
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'get_member_perms');
|
||
url.searchParams.set('token', TOKEN);
|
||
url.searchParams.set('land', landName);
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
if (!data.success) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.error}</p><button class="btn" onclick="landsState.view='member_perms';renderLands(document.getElementById('content'))">← 返回</button></div>`;
|
||
return;
|
||
}
|
||
|
||
const land = data.land;
|
||
const permTypes = data.perm_types || {};
|
||
const members = data.members || [];
|
||
const member = members.find(m => m.player_name === targetPlayer);
|
||
|
||
if (!member) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">成员不存在</p><button class="btn" onclick="landsState.view='member_perms';renderLands(document.getElementById('content'))">← 返回</button></div>`;
|
||
return;
|
||
}
|
||
|
||
const permMap = member.perm_map || {};
|
||
const permEntries = Object.entries(permTypes);
|
||
|
||
let html = `<div class="card">
|
||
<button class="btn" onclick="landsState.view='member_perms';landsState.currentMember=null;renderLands(document.getElementById('content'))" style="margin-bottom:12px;font-size:13px">← 返回成员列表</button>
|
||
<h2 style="margin:0 0 8px;color:var(--fg)">👤 ${escHtml(targetPlayer)} 权限</h2>
|
||
<p style="color:var(--dim);font-size:13px;margin-bottom:16px">在 ${escHtml(landName)} 中的独立权限设置</p>
|
||
<div style="display:flex;gap:8px;margin-bottom:16px">
|
||
<button class="btn" onclick="clearAllMemberPerms('${escHtml(landName)}','${escHtml(targetPlayer)}')" style="color:var(--red)">🗑️ 清除所有自定义</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px">`;
|
||
|
||
for (const [key, label] of permEntries) {
|
||
const isCustom = permMap.hasOwnProperty(key);
|
||
const enabled = isCustom ? permMap[key] : false;
|
||
const defaultVal = land[key] || false;
|
||
|
||
html += `<div onclick="toggleMemberPerm('${escHtml(landName)}','${escHtml(targetPlayer)}','${key}',this)"
|
||
style="background:${isCustom ? (enabled ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)') : 'var(--bg)'};
|
||
border:1px solid ${isCustom ? (enabled ? 'var(--green)' : 'var(--red)') : 'var(--border)'};
|
||
border-radius:8px;padding:12px;cursor:pointer;transition:all 0.2s">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<span style="color:var(--fg);font-size:13px;font-weight:500">${label}</span>
|
||
<span style="font-size:11px;color:${isCustom ? (enabled ? 'var(--green)' : 'var(--red)') : 'var(--dim)'}">
|
||
${isCustom ? (enabled ? '✓ 启用' : '✗ 禁用') : '默认'}
|
||
</span>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--dim)">
|
||
${isCustom ? '★ 自定义' : `领地默认: ${defaultVal ? '禁止' : '允许'}`}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
html += `</div></div>`;
|
||
el.innerHTML = html;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
async function toggleMemberPerm(landName, targetPlayer, permKey, el) {
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'update_member_perm');
|
||
url.searchParams.set('token', TOKEN);
|
||
|
||
const body = new URLSearchParams();
|
||
body.set('land', landName);
|
||
body.set('player', targetPlayer);
|
||
body.set('perm', permKey);
|
||
|
||
// 从元素中获取当前状态
|
||
const statusSpan = el.querySelector('span:last-child');
|
||
const isCustom = statusSpan.textContent.includes('自定义');
|
||
const isEnabled = statusSpan.textContent.includes('启用');
|
||
|
||
if (isCustom) {
|
||
// 已有自定义:切换启用/禁用
|
||
body.set('enabled', (!isEnabled).toString());
|
||
} else {
|
||
// 无自定义:设置为启用
|
||
body.set('enabled', 'true');
|
||
}
|
||
|
||
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) {
|
||
// 刷新页面
|
||
await renderMemberPermDetail(document.getElementById('content'), landName, targetPlayer);
|
||
} else {
|
||
glassAlert(data.error || '操作失败');
|
||
}
|
||
} catch (e) {
|
||
glassAlert('操作失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function clearAllMemberPerms(landName, targetPlayer) {
|
||
if (!await glassConfirm(`确定要清除 ${targetPlayer} 的所有自定义权限吗?`)) return;
|
||
try {
|
||
const url = new URL(API + 'land_api.php', location.href);
|
||
url.searchParams.set('action', 'clear_member_perm');
|
||
url.searchParams.set('token', TOKEN);
|
||
|
||
const body = new URLSearchParams();
|
||
body.set('land', landName);
|
||
body.set('player', targetPlayer);
|
||
|
||
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) {
|
||
glassAlert('已清除所有自定义权限');
|
||
await renderMemberPermDetail(document.getElementById('content'), landName, targetPlayer);
|
||
} else {
|
||
glassAlert(data.error || '操作失败');
|
||
}
|
||
} catch (e) {
|
||
glassAlert('操作失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function escHtml(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|