diff --git a/website/auth.css b/website/auth.css new file mode 100644 index 0000000..60085fa --- /dev/null +++ b/website/auth.css @@ -0,0 +1,146 @@ +/* 宇森 - 登录认证样式 */ + +/* ---- Nav bar auth area ---- */ +#auth-area { + display: flex; + align-items: center; + gap: 10px; +} + +.auth-btn { + background: rgba(100, 200, 255, 0.15); + border: 1px solid rgba(100, 200, 255, 0.35); + color: #c8e6ff; + padding: 6px 16px; + border-radius: 20px; + cursor: pointer; + font-size: 13px; + transition: all 0.25s; +} +.auth-btn:hover { + background: rgba(100, 200, 255, 0.25); + border-color: rgba(100, 200, 255, 0.6); + color: #fff; +} + +.login-btn {} +.logout-btn { + background: rgba(255, 100, 100, 0.12); + border-color: rgba(255, 100, 100, 0.3); + color: #fcc; +} +.logout-btn:hover { + background: rgba(255, 100, 100, 0.22); + border-color: rgba(255, 100, 100, 0.5); + color: #fff; +} + +.user-name { + color: #a8e6cf; + font-size: 13px; + font-weight: 500; +} + +/* ---- Login Modal ---- */ +#login-modal { + position: fixed; + inset: 0; + z-index: 9999; +} + +.login-overlay { + position: absolute; + inset: 0; + background: rgba(5, 8, 16, 0.8); + backdrop-filter: blur(6px); + display: flex; + align-items: center; + justify-content: center; +} + +.login-box { + background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(10, 18, 35, 0.98)); + border: 1px solid rgba(100, 200, 255, 0.2); + border-radius: 16px; + padding: 36px 32px; + width: 360px; + max-width: 90vw; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 80px rgba(100, 200, 255, 0.08); +} + +.login-box h2 { + margin: 0 0 4px; + font-size: 22px; + background: linear-gradient(90deg, #4af, #a8e6cf); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.login-sub { + margin: 0 0 24px; + color: #6a8; + font-size: 13px; +} + +.login-box input { + width: 100%; + box-sizing: border-box; + padding: 10px 14px; + margin-bottom: 12px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(100, 200, 255, 0.2); + border-radius: 8px; + color: #e0e8f0; + font-size: 14px; + outline: none; + transition: border 0.2s; +} +.login-box input:focus { + border-color: rgba(100, 200, 255, 0.5); + background: rgba(255, 255, 255, 0.08); +} + +.login-error { + color: #f66; + font-size: 12px; + min-height: 18px; + margin-bottom: 4px; +} + +.login-submit { + width: 100%; + padding: 11px; + margin-top: 4px; + background: linear-gradient(135deg, #2a6, #4af); + border: none; + border-radius: 8px; + color: #fff; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.25s; +} +.login-submit:hover { + transform: translateY(-1px); + box-shadow: 0 4px 20px rgba(68, 170, 255, 0.3); +} +.login-submit:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.login-footer { + margin: 16px 0 0; + text-align: center; + font-size: 12px; + color: #557; +} +.login-footer a { + color: #4af; + text-decoration: none; +} +.login-footer a:hover { + text-decoration: underline; +} diff --git a/website/auth.js b/website/auth.js new file mode 100644 index 0000000..bf6b183 --- /dev/null +++ b/website/auth.js @@ -0,0 +1,188 @@ +/** + * 宇森 - Odoo 用户认证模块 + * 通过 Odoo JSON-RPC 实现登录/登出,与 Odoo 用户系统打通 + */ +(function () { + 'use strict'; + + const AUTH_URL = '/web/session/authenticate'; + const DESTROY_URL = '/web/session/destroy'; + const SESSION_URL = '/web/session/get_session_info'; + + // ============ State ============ + let currentUser = null; + + // ============ Init ============ + async function init() { + try { + const resp = await fetch(SESSION_URL, { credentials: 'same-origin' }); + if (resp.ok) { + const data = await resp.json(); + if (data.result && data.result.uid) { + currentUser = { + uid: data.result.uid, + username: data.result.username, + name: data.result.name || data.result.username, + }; + } + } + } catch (e) { + // Not logged in — that's fine + } + renderAuthUI(); + } + + // ============ Login ============ + async function login(username, password) { + const body = { + jsonrpc: '2.0', + method: 'call', + params: { + db: 'game', + login: username, + password: password, + }, + id: Date.now(), + }; + + const resp = await fetch(AUTH_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + credentials: 'same-origin', + }); + + const data = await resp.json(); + if (data.error) { + throw new Error(data.error.data?.message || '登录失败,请检查用户名和密码'); + } + + if (data.result && data.result.uid) { + currentUser = { + uid: data.result.uid, + username: data.result.username, + name: data.result.name || data.result.username, + }; + renderAuthUI(); + return currentUser; + } + throw new Error('登录失败'); + } + + // ============ Logout ============ + async function logout() { + try { + await fetch(DESTROY_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: {}, id: Date.now() }), + credentials: 'same-origin', + }); + } catch (e) { + // ignore + } + currentUser = null; + renderAuthUI(); + } + + // ============ Render ============ + function renderAuthUI() { + const container = document.getElementById('auth-area'); + if (!container) return; + + if (currentUser) { + container.innerHTML = + '' + + escapeHtml(currentUser.name) + + '' + + ''; + } else { + container.innerHTML = + ''; + } + } + + // ============ Login Modal ============ + function showLoginModal() { + // Remove existing modal + const existing = document.getElementById('login-modal'); + if (existing) existing.remove(); + + const modal = document.createElement('div'); + modal.id = 'login-modal'; + modal.innerHTML = + '
'; + + document.body.appendChild(modal); + + const form = modal.querySelector('#login-form'); + form.onsubmit = handleLogin; + + // Focus username + setTimeout(() => { + const u = document.getElementById('login-username'); + if (u) u.focus(); + }, 100); + } + + function hideLoginModal(e) { + if (e && e.target !== e.currentTarget) return; + const modal = document.getElementById('login-modal'); + if (modal) modal.remove(); + } + + async function handleLogin() { + const username = document.getElementById('login-username').value.trim(); + const password = document.getElementById('login-password').value; + const errEl = document.getElementById('login-error'); + const btn = document.getElementById('login-submit-btn'); + + if (!username || !password) { + errEl.textContent = '请输入用户名和密码'; + return; + } + + btn.disabled = true; + btn.textContent = '登录中...'; + errEl.textContent = ''; + + try { + await login(username, password); + hideLoginModal({ target: document.querySelector('.login-overlay') }); + } catch (e) { + errEl.textContent = e.message; + } finally { + btn.disabled = false; + btn.textContent = '登 录'; + } + } + + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + // ============ Public API ============ + window.YuSenAuth = { + init, + login, + logout, + showLoginModal, + hideLoginModal, + getCurrentUser: () => currentUser, + isLoggedIn: () => !!currentUser, + }; +})(); diff --git a/website/demo.html b/website/demo.html index 4134448..0bbcbe7 100644 --- a/website/demo.html +++ b/website/demo.html @@ -28,7 +28,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa #topBar .nav-links { display:flex; gap:16px; margin-left:24px; } #topBar .nav-links a { color:#667788; text-decoration:none; font-size:12px; transition:color 0.2s; } #topBar .nav-links a:hover { color:#4af; } -#topBar .coords { margin-left:auto; font-size:12px; color:#8899aa; } +#topBar .coords { margin-left:8px; font-size:12px; color:#8899aa; } /* 微观世界视图 */ #microView { @@ -164,6 +164,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa cursor: pointer; font-size: 13px; } + @@ -185,6 +186,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa 世界 种族 + 坐标: (0, 0) @@ -1078,5 +1080,7 @@ window.addEventListener('resize', () => { generateMap(); init(); + +