feat: finish bilingual learning auth cockpit
This commit is contained in:
329
target/classes/static/learning-shell.js
Normal file
329
target/classes/static/learning-shell.js
Normal file
@@ -0,0 +1,329 @@
|
||||
(function () {
|
||||
const STORAGE = {
|
||||
token: "learning-demo-token",
|
||||
username: "learning-demo-username",
|
||||
language: "learning-demo-language"
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
zh: {
|
||||
brand: "Spring \u5b66\u4e60\u5de5\u4f5c\u53f0",
|
||||
home: "\u9996\u9875",
|
||||
access: "\u767b\u5f55\u9875",
|
||||
loginReady: "\u5df2\u767b\u5f55\uff0c\u53ef\u8bbf\u95ee\u53d7\u4fdd\u62a4\u5b9e\u9a8c",
|
||||
loginMissing: "\u672a\u767b\u5f55\uff0c\u53d7\u4fdd\u62a4\u5b9e\u9a8c\u4f1a\u8fd4\u56de 401",
|
||||
currentUser: "\u5f53\u524d\u7528\u6237",
|
||||
logout: "\u9000\u51fa\u767b\u5f55",
|
||||
login: "\u53bb\u767b\u5f55",
|
||||
languageToggle: "EN",
|
||||
unauthorized: "\u672a\u767b\u5f55\u6216\u4ee4\u724c\u5df2\u5931\u6548\uff0c\u8bf7\u5148\u6253\u5f00\u767b\u5f55\u9875\u5b8c\u6210\u6f14\u793a\u767b\u5f55\u3002",
|
||||
requestFailed: "\u8bf7\u6c42\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002"
|
||||
},
|
||||
en: {
|
||||
brand: "Spring Learning Workspace",
|
||||
home: "Home",
|
||||
access: "Login",
|
||||
loginReady: "Authenticated, protected labs are available",
|
||||
loginMissing: "Not logged in, protected labs will return 401",
|
||||
currentUser: "User",
|
||||
logout: "Logout",
|
||||
login: "Login",
|
||||
languageToggle: "\u4e2d\u6587",
|
||||
unauthorized: "Not logged in or token expired. Open the login page first.",
|
||||
requestFailed: "Request failed. Please try again."
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeLanguage(language) {
|
||||
return language === "en" ? "en" : "zh";
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
return normalizeLanguage(localStorage.getItem(STORAGE.language));
|
||||
}
|
||||
|
||||
function setLanguage(language) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
localStorage.setItem(STORAGE.language, normalized);
|
||||
document.documentElement.lang = normalized === "zh" ? "zh-CN" : "en";
|
||||
window.dispatchEvent(new CustomEvent("learning-language-changed", {
|
||||
detail: { language: normalized }
|
||||
}));
|
||||
}
|
||||
|
||||
function t(key) {
|
||||
const lang = getLanguage();
|
||||
return (TEXT[lang] && TEXT[lang][key]) || key;
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(STORAGE.token) || "";
|
||||
}
|
||||
|
||||
function getUsername() {
|
||||
return localStorage.getItem(STORAGE.username) || "";
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return Boolean(getToken());
|
||||
}
|
||||
|
||||
function saveAuth(token, username) {
|
||||
localStorage.setItem(STORAGE.token, token || "");
|
||||
localStorage.setItem(STORAGE.username, username || "");
|
||||
window.dispatchEvent(new CustomEvent("learning-auth-changed", {
|
||||
detail: { loggedIn: isLoggedIn(), username: getUsername() }
|
||||
}));
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
localStorage.removeItem(STORAGE.token);
|
||||
localStorage.removeItem(STORAGE.username);
|
||||
window.dispatchEvent(new CustomEvent("learning-auth-changed", {
|
||||
detail: { loggedIn: false, username: "" }
|
||||
}));
|
||||
}
|
||||
|
||||
function ensureStyle() {
|
||||
if (document.getElementById("learning-shell-style")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "learning-shell-style";
|
||||
style.textContent = [
|
||||
".learning-shell {",
|
||||
" max-width: 1380px;",
|
||||
" margin: 18px auto 0;",
|
||||
" padding: 0 24px;",
|
||||
"}",
|
||||
".learning-shell-card {",
|
||||
" display: flex;",
|
||||
" justify-content: space-between;",
|
||||
" align-items: center;",
|
||||
" gap: 14px;",
|
||||
" flex-wrap: wrap;",
|
||||
" padding: 14px 18px;",
|
||||
" border-radius: 22px;",
|
||||
" border: 1px solid rgba(216, 228, 240, 0.9);",
|
||||
" background: rgba(255, 255, 255, 0.88);",
|
||||
" box-shadow: 0 12px 28px rgba(18, 32, 51, 0.08);",
|
||||
" backdrop-filter: blur(10px);",
|
||||
"}",
|
||||
".learning-shell-brand {",
|
||||
" display: flex;",
|
||||
" flex-direction: column;",
|
||||
" gap: 4px;",
|
||||
"}",
|
||||
".learning-shell-brand strong {",
|
||||
" font-size: 15px;",
|
||||
" color: #122033;",
|
||||
"}",
|
||||
".learning-shell-brand span {",
|
||||
" font-size: 13px;",
|
||||
" color: #5d7288;",
|
||||
"}",
|
||||
".learning-shell-actions {",
|
||||
" display: flex;",
|
||||
" align-items: center;",
|
||||
" gap: 10px;",
|
||||
" flex-wrap: wrap;",
|
||||
"}",
|
||||
".learning-shell-link,",
|
||||
".learning-shell-button,",
|
||||
".learning-shell-badge {",
|
||||
" display: inline-flex;",
|
||||
" align-items: center;",
|
||||
" justify-content: center;",
|
||||
" min-height: 36px;",
|
||||
" padding: 0 14px;",
|
||||
" border-radius: 999px;",
|
||||
" text-decoration: none;",
|
||||
" font-size: 13px;",
|
||||
" font-weight: 700;",
|
||||
"}",
|
||||
".learning-shell-link {",
|
||||
" color: #0f67b5;",
|
||||
" background: rgba(15, 103, 181, 0.09);",
|
||||
"}",
|
||||
".learning-shell-button {",
|
||||
" border: 0;",
|
||||
" cursor: pointer;",
|
||||
" color: #fff;",
|
||||
" background: linear-gradient(135deg, #177245, #35a465);",
|
||||
"}",
|
||||
".learning-shell-button.alt {",
|
||||
" color: #0f67b5;",
|
||||
" background: rgba(15, 103, 181, 0.09);",
|
||||
"}",
|
||||
".learning-shell-badge {",
|
||||
" color: #33485e;",
|
||||
" background: rgba(18, 32, 51, 0.06);",
|
||||
"}",
|
||||
"@media (max-width: 780px) {",
|
||||
" .learning-shell {",
|
||||
" padding: 0 14px;",
|
||||
" }",
|
||||
"}"
|
||||
].join("\n");
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function mountShell(options) {
|
||||
ensureStyle();
|
||||
|
||||
if (document.querySelector(".learning-shell")) {
|
||||
if (options && typeof options.onLanguageChange === "function") {
|
||||
options.onLanguageChange(getLanguage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const shell = document.createElement("div");
|
||||
shell.className = "learning-shell";
|
||||
shell.innerHTML = [
|
||||
'<div class="learning-shell-card">',
|
||||
' <div class="learning-shell-brand">',
|
||||
' <strong data-role="brand"></strong>',
|
||||
' <span data-role="status"></span>',
|
||||
" </div>",
|
||||
' <div class="learning-shell-actions">',
|
||||
' <a class="learning-shell-link" href="/" data-role="home"></a>',
|
||||
' <a class="learning-shell-link" href="/access.html" data-role="access"></a>',
|
||||
' <span class="learning-shell-badge" data-role="user"></span>',
|
||||
' <button class="learning-shell-button alt" type="button" data-role="language"></button>',
|
||||
' <button class="learning-shell-button alt" type="button" data-role="login"></button>',
|
||||
' <button class="learning-shell-button alt" type="button" data-role="logout"></button>',
|
||||
" </div>",
|
||||
"</div>"
|
||||
].join("");
|
||||
|
||||
const firstPage = document.querySelector(".page");
|
||||
if (firstPage) {
|
||||
document.body.insertBefore(shell, firstPage);
|
||||
} else {
|
||||
document.body.insertBefore(shell, document.body.firstChild);
|
||||
}
|
||||
|
||||
const els = {
|
||||
brand: shell.querySelector('[data-role="brand"]'),
|
||||
status: shell.querySelector('[data-role="status"]'),
|
||||
home: shell.querySelector('[data-role="home"]'),
|
||||
access: shell.querySelector('[data-role="access"]'),
|
||||
user: shell.querySelector('[data-role="user"]'),
|
||||
language: shell.querySelector('[data-role="language"]'),
|
||||
login: shell.querySelector('[data-role="login"]'),
|
||||
logout: shell.querySelector('[data-role="logout"]')
|
||||
};
|
||||
|
||||
function render() {
|
||||
const loggedIn = isLoggedIn();
|
||||
const username = getUsername();
|
||||
|
||||
els.brand.textContent = t("brand");
|
||||
els.status.textContent = loggedIn ? t("loginReady") : t("loginMissing");
|
||||
els.home.textContent = t("home");
|
||||
els.access.textContent = t("access");
|
||||
els.user.textContent = loggedIn ? t("currentUser") + ": " + username : t("loginMissing");
|
||||
els.language.textContent = t("languageToggle");
|
||||
els.login.textContent = t("login");
|
||||
els.logout.textContent = t("logout");
|
||||
els.login.style.display = loggedIn ? "none" : "inline-flex";
|
||||
els.logout.style.display = loggedIn ? "inline-flex" : "none";
|
||||
|
||||
if (options && typeof options.onLanguageChange === "function") {
|
||||
options.onLanguageChange(getLanguage());
|
||||
}
|
||||
if (options && typeof options.onAuthChange === "function") {
|
||||
options.onAuthChange({ loggedIn: loggedIn, username: username });
|
||||
}
|
||||
}
|
||||
|
||||
els.language.addEventListener("click", function () {
|
||||
setLanguage(getLanguage() === "zh" ? "en" : "zh");
|
||||
});
|
||||
|
||||
els.login.addEventListener("click", function () {
|
||||
window.location.href = "/access.html";
|
||||
});
|
||||
|
||||
els.logout.addEventListener("click", function () {
|
||||
clearAuth();
|
||||
render();
|
||||
});
|
||||
|
||||
window.addEventListener("learning-auth-changed", render);
|
||||
window.addEventListener("learning-language-changed", render);
|
||||
render();
|
||||
}
|
||||
|
||||
async function fetchWithAuth(url, options) {
|
||||
const requestOptions = options || {};
|
||||
const headers = new Headers(requestOptions.headers || {});
|
||||
const token = getToken();
|
||||
|
||||
if (token && !headers.has("Authorization")) {
|
||||
headers.set("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
return fetch(url, Object.assign({}, requestOptions, { headers: headers }));
|
||||
}
|
||||
|
||||
async function requestJson(url, options) {
|
||||
const response = await fetchWithAuth(url, options);
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json")
|
||||
? await response.json()
|
||||
: await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(
|
||||
typeof payload === "object" && payload && payload.message
|
||||
? payload.message
|
||||
: t("requestFailed")
|
||||
);
|
||||
error.status = response.status;
|
||||
error.payload = payload;
|
||||
error.details = typeof payload === "object" && payload ? payload.data : null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof payload === "object" &&
|
||||
payload &&
|
||||
Object.prototype.hasOwnProperty.call(payload, "code") &&
|
||||
payload.code !== 0
|
||||
) {
|
||||
const error = new Error(payload.message || t("requestFailed"));
|
||||
error.status = payload.code;
|
||||
error.payload = payload;
|
||||
error.details = payload.data;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function describeError(error) {
|
||||
if (error && Number(error.status) === 401) {
|
||||
return t("unauthorized");
|
||||
}
|
||||
return error && error.message ? error.message : t("requestFailed");
|
||||
}
|
||||
|
||||
setLanguage(getLanguage());
|
||||
|
||||
window.learningShell = {
|
||||
mountShell: mountShell,
|
||||
getLanguage: getLanguage,
|
||||
setLanguage: setLanguage,
|
||||
getToken: getToken,
|
||||
getUsername: getUsername,
|
||||
isLoggedIn: isLoggedIn,
|
||||
saveAuth: saveAuth,
|
||||
clearAuth: clearAuth,
|
||||
fetchWithAuth: fetchWithAuth,
|
||||
requestJson: requestJson,
|
||||
describeError: describeError
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user