feat(web): add mobile-first read-only dashboard frontend

This commit is contained in:
kairee committed 2026-10-31 23:46:55 +08:00
1 parent 257fbe009b
commit cbba405271
18 files changed
+2628 -2

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
export class ApiError extends Error {
constructor(message, status = 0, code = "error") {
super(message);
this.name = "ApiError";
this.status = status;
this.code = code;
}
}
function buildUrl(path, params) {
const url = new URL(path, window.location.origin);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value === null || value === undefined || value === "") continue;
url.searchParams.set(key, String(value));
}
}
return url;
}
export async function api(path, params) {
const response = await fetch(buildUrl(path, params), {
headers: { Accept: "application/json" },
credentials: "same-origin",
});
if (response.status === 401) {
window.location.href = "/login";
throw new ApiError("登录已失效,请重新登录", 401, "unauthorized");
}
let payload;
try {
payload = await response.json();
} catch {
throw new ApiError(`接口返回异常(HTTP ${response.status})`, response.status);
}
if (!response.ok || !payload.ok) {
const error = payload.error || {};
throw new ApiError(
error.message || `请求失败(HTTP ${response.status})`,
response.status,
error.code || "error",
);
}
return payload.data;
}
export function apiUrl(path, params) {
return buildUrl(path, params).toString();
}
export async function health() {
const response = await fetch("/api/health", { credentials: "same-origin" });
if (!response.ok) throw new ApiError("无法连接看板服务", response.status);
const payload = await response.json();
return payload?.data ?? payload;
}