feat: Odoo login integration for all pages

This commit is contained in:
root 2026-07-07 17:05:36 +08:00
parent 424923c81e
commit d01703dd3f
8 changed files with 359 additions and 1 deletions

146
website/auth.css Normal file
View File

@ -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;
}

188
website/auth.js Normal file
View File

@ -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 =
'<span class="user-name">' +
escapeHtml(currentUser.name) +
'</span>' +
'<button class="auth-btn logout-btn" onclick="YuSenAuth.logout()">退出</button>';
} else {
container.innerHTML =
'<button class="auth-btn login-btn" onclick="YuSenAuth.showLoginModal()">登录</button>';
}
}
// ============ 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 =
'<div class="login-overlay" onclick="YuSenAuth.hideLoginModal(event)">' +
'<div class="login-box" onclick="event.stopPropagation()">' +
'<h2>登录宇森</h2>' +
'<p class="login-sub">使用 Odoo 账号登录</p>' +
'<form id="login-form" onsubmit="return false">' +
'<input type="text" id="login-username" placeholder="用户名" autocomplete="username" />' +
'<input type="password" id="login-password" placeholder="密码" autocomplete="current-password" />' +
'<div id="login-error" class="login-error"></div>' +
'<button type="submit" class="login-submit" id="login-submit-btn">登 录</button>' +
'</form>' +
'<p class="login-footer">还没有账号?前往 <a href="/admin/web/login" target="_blank">Odoo 注册</a></p>' +
'</div>' +
'</div>';
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,
};
})();

View File

@ -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;
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
@ -185,6 +186,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
<a href="world.html">世界</a>
<a href="races.html">种族</a>
</div>
<div id="auth-area" style="margin-left:auto; margin-right:8px;"></div>
<span class="coords" id="coords">坐标: (0, 0)</span>
</div>
@ -1078,5 +1080,7 @@ window.addEventListener('resize', () => {
generateMap();
init();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>

View File

@ -83,6 +83,7 @@ nav .links a:hover { color:#4af; }
.quick-card { padding:14px 18px; min-width:110px; }
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
@ -90,6 +91,7 @@ nav .links a:hover { color:#4af; }
<nav>
<span class="logo">宇森</span>
<div id="auth-area"></div>
<ul class="links">
<li><a href="intro.html">游戏介绍</a></li>
<li><a href="platform.html">平台架构</a></li>
@ -177,5 +179,7 @@ function animate() {
}
animate();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>

View File

@ -62,6 +62,7 @@ p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
.page { padding:80px 20px 40px; }
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
@ -69,6 +70,7 @@ p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
<nav>
<a href="index.html" class="logo">宇森</a>
<div id="auth-area"></div>
<ul class="links">
<li><a href="intro.html" class="active">游戏介绍</a></li>
<li><a href="platform.html">平台架构</a></li>
@ -146,5 +148,7 @@ for (let i=0;i<80;i++) particles.push(new Particle());
function animate() { ctx.clearRect(0,0,canvas.width,canvas.height); particles.forEach(p=>{p.update();p.draw();}); requestAnimationFrame(animate); }
animate();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>

View File

@ -56,6 +56,7 @@ h1 { font-size:32px; font-weight:bold; margin-bottom:8px; background:linear-grad
h1 { font-size:24px; } nav .links { display:none; } .page { padding:80px 20px 40px; }
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
@ -63,6 +64,7 @@ h1 { font-size:32px; font-weight:bold; margin-bottom:8px; background:linear-grad
<nav>
<a href="index.html" class="logo">宇森</a>
<div id="auth-area"></div>
<ul class="links">
<li><a href="intro.html">游戏介绍</a></li>
<li><a href="platform.html" class="active">平台架构</a></li>
@ -114,5 +116,7 @@ class Particle{constructor(){this.r();this.y=Math.random()*c.height}r(){this.x=M
for(let i=0;i<80;i++)P.push(new Particle());
(function A(){x.clearRect(0,0,c.width,c.height);P.forEach(p=>{p.u();p.d()});requestAnimationFrame(A)})();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>

View File

@ -49,6 +49,7 @@ h1 { font-size:32px; font-weight:bold; margin-bottom:8px; background:linear-grad
.races { grid-template-columns:1fr; } .race-card { padding:22px 18px; }
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
@ -56,6 +57,7 @@ h1 { font-size:32px; font-weight:bold; margin-bottom:8px; background:linear-grad
<nav>
<a href="index.html" class="logo">宇森</a>
<div id="auth-area"></div>
<ul class="links">
<li><a href="intro.html">游戏介绍</a></li>
<li><a href="platform.html">平台架构</a></li>
@ -126,5 +128,7 @@ class Particle{constructor(){this.r();this.y=Math.random()*c.height}r(){this.x=M
for(let i=0;i<80;i++)P.push(new Particle());
(function A(){x.clearRect(0,0,c.width,c.height);P.forEach(p=>{p.u();p.d()});requestAnimationFrame(A)})();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>

View File

@ -51,6 +51,7 @@ h1 { font-size:32px; font-weight:bold; margin-bottom:8px; background:linear-grad
.layer-icon { padding-top:0; }
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
@ -58,6 +59,7 @@ h1 { font-size:32px; font-weight:bold; margin-bottom:8px; background:linear-grad
<nav>
<a href="index.html" class="logo">宇森</a>
<div id="auth-area"></div>
<ul class="links">
<li><a href="intro.html">游戏介绍</a></li>
<li><a href="platform.html">平台架构</a></li>
@ -134,5 +136,7 @@ class Particle{constructor(){this.r();this.y=Math.random()*c.height}r(){this.x=M
for(let i=0;i<80;i++)P.push(new Particle());
(function A(){x.clearRect(0,0,c.width,c.height);P.forEach(p=>{p.u();p.d()});requestAnimationFrame(A)})();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>