330 lines
12 KiB
JavaScript
330 lines
12 KiB
JavaScript
|
|
(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
|
||
|
|
};
|
||
|
|
})();
|