Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/main/resources/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TeamUp 课程组队平台</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./styles/base.css">
<link rel="stylesheet" href="./styles/theme.css">
<link rel="stylesheet" href="./styles/skeleton.css">
</head>
<body data-theme="light">
<div class="bg-decoration"></div>
<header class="topbar">
<div class="brand">
<span class="brand-mark">TU</span>
<div class="brand-text">
<h1>TeamUp</h1>
<p id="routeLabel">队伍广场</p>
</div>
</div>
<div class="topbar-status">
<p id="sessionHint">游客模式:可浏览、搜索</p>
</div>
<div class="topbar-actions">
<button id="themeBtn" class="btn btn-ghost" type="button">切换暗色</button>
<button id="refreshBtn" class="btn btn-ghost" type="button">刷新当前页</button>
<button id="logoutBtn" class="btn btn-danger hidden" type="button">退出登录</button>
</div>
</header>

<main id="app" class="page-shell"></main>

<div id="toastContainer" class="toast-container"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
70 changes: 70 additions & 0 deletions src/main/resources/static/src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { clearSession, isLoggedIn, setUserMap, state } from "./state.js";
import { isSuccess } from "./utils.js";

export async function request(path, { method = "GET", body, auth = method !== "GET" } = {}) {
const headers = {};
if (body !== undefined) {
headers["Content-Type"] = "application/json";
}
if (auth && isLoggedIn()) {
headers.Authorization = `Bearer ${state.token}`;
}
try {
const response = await fetch(path, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined
});
const contentType = response.headers.get("content-type") || "";
let payload;
if (contentType.includes("application/json")) {
payload = await response.json();
} else {
payload = {
code: response.ok ? 200001 : 200000,
data: null,
msg: (await response.text()) || ""
};
}
if (!response.ok && !payload.msg) {
payload.msg = `请求失败(HTTP ${response.status})`;
}
if (payload?.code === 200000 && /(token|bearer|unauthorized|expired)/i.test(payload.msg || "")) {
clearSession();
}
return payload;
} catch (error) {
return {
code: 200000,
data: null,
msg: error instanceof Error ? error.message : "网络错误"
};
}
}

export async function loadUsers() {
const result = await request("/users", { auth: false });
if (!isSuccess(result.code) || !Array.isArray(result.data)) {
return result;
}
const map = new Map();
for (const item of result.data) {
if (item?.user?.id != null) {
map.set(Number(item.user.id), item.user.name || `用户#${item.user.id}`);
}
}
setUserMap(map);
return result;
}

export async function loadTeams(keyword) {
const value = String(keyword || "").trim();
if (value) {
return request("/info/search", {
method: "POST",
auth: false,
body: { content: value }
});
}
return request("/teams", { auth: false });
}
83 changes: 83 additions & 0 deletions src/main/resources/static/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { request } from "./api.js";
import { clearSession, getUserName, isLoggedIn, state, toggleTheme } from "./state.js";
import { createRouter } from "./router.js";
import { showToast } from "./ui/toast.js";
import { renderTeamDetailPage } from "./pages/teams/detailPage.js";
import { renderTeamEditPage } from "./pages/teams/editPage.js";
import { renderTeamsListPage } from "./pages/teams/listPage.js";

const routeLabelEl = document.getElementById("routeLabel");
const sessionHintEl = document.getElementById("sessionHint");
const themeBtnEl = document.getElementById("themeBtn");
const refreshBtnEl = document.getElementById("refreshBtn");
const logoutBtnEl = document.getElementById("logoutBtn");
const app = document.getElementById("app");

function applyTheme() {
document.body.dataset.theme = state.theme;
themeBtnEl.textContent = state.theme === "dark" ? "切换浅色" : "切换暗色";
}

function updateSessionHint() {
if (!isLoggedIn()) {
sessionHintEl.textContent = "游客模式:可浏览、搜索";
logoutBtnEl.classList.add("hidden");
return;
}
sessionHintEl.textContent = `已登录:${getUserName(state.uid)}(UID: ${state.uid})`;
logoutBtnEl.classList.remove("hidden");
}

const router = createRouter({
app,
routes: [
{
pattern: /^\/teams$/,
label: "队伍广场",
render: renderTeamsListPage
},
{
pattern: /^\/teams\/(\d+)\/edit$/,
label: "编辑队伍",
mapParams: (match) => ({ teamId: Number(match[1]) }),
render: renderTeamEditPage
},
{
pattern: /^\/teams\/(\d+)$/,
label: "队伍详情",
mapParams: (match) => ({ teamId: Number(match[1]) }),
render: renderTeamDetailPage
}
],
onRouteResolved: (label) => {
routeLabelEl.textContent = label;
}
});

document.addEventListener("teamup:session", () => {
updateSessionHint();
});

document.addEventListener("teamup:theme", () => {
applyTheme();
});

themeBtnEl.addEventListener("click", () => {
toggleTheme();
});

refreshBtnEl.addEventListener("click", () => {
router.reload();
showToast("已刷新当前页面", "info");
});

logoutBtnEl.addEventListener("click", async () => {
await request("/logout", { method: "POST" });
clearSession();
showToast("已退出登录", "info");
router.navigate("/teams");
});

applyTheme();
updateSessionHint();
router.start();
Loading