feat: add vue admin transition shell
This commit is contained in:
1 parent
2cdf7f9d7a
commit
f07a61f3a8
30 files changed
+2127
-7
No files matched your search
@@ -341,6 +341,8 @@ export function clearToken() {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
let refreshPromise: Promise<string | null> | null = null;
|
||||
|
||||
function handleAuthExpired() {
|
||||
clearToken();
|
||||
notifyAuthExpired();
|
||||
@@ -401,7 +403,30 @@ async function readApiResponse(response: Response): Promise<unknown> {
|
||||
return response.json().catch(() => null);
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
async function refreshAdminToken(): Promise<string | null> {
|
||||
if (refreshPromise) return refreshPromise;
|
||||
refreshPromise = (async () => {
|
||||
const response = await fetch(`${API_BASE}/api/admin/auth/refresh`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
try {
|
||||
const body = await readApiResponse(response);
|
||||
const session = parseApiResponse<{ token: string; accessToken?: string }>(body, response.status);
|
||||
const token = session.accessToken ?? session.token;
|
||||
setToken(token);
|
||||
return token;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})().finally(() => {
|
||||
refreshPromise = null;
|
||||
});
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit = {}, allowRefresh = true): Promise<T> {
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set("Content-Type", "application/json");
|
||||
const token = getToken();
|
||||
@@ -410,10 +435,12 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (path !== "/api/admin/auth/login" && shouldExpireAdminSession(response.status, Boolean(token))) {
|
||||
if (allowRefresh && path !== "/api/admin/auth/login" && shouldExpireAdminSession(response.status, Boolean(token))) {
|
||||
if (await refreshAdminToken()) return request<T>(path, options, false);
|
||||
handleAuthExpired();
|
||||
}
|
||||
const body = await readApiResponse(response);
|
||||
@@ -424,12 +451,16 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
}
|
||||
|
||||
export async function login(email: string, password: string) {
|
||||
return request<{ token: string; user: { name: string; email: string; role: string } }>("/api/admin/auth/login", {
|
||||
return request<{ token: string; accessToken?: string; user: { name: string; email: string; role: string } }>("/api/admin/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function logoutAdmin() {
|
||||
return request<{ ok: boolean }>("/api/admin/auth/logout", { method: "POST" }, false);
|
||||
}
|
||||
|
||||
export async function uploadMediaAsset(file: File, group = "site-config") {
|
||||
const form = new FormData();
|
||||
form.set("file", file);
|
||||
@@ -442,9 +473,13 @@ export async function uploadMediaAsset(file: File, group = "site-config") {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: form,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (shouldExpireAdminSession(response.status, Boolean(token)) && (await refreshAdminToken())) {
|
||||
return uploadMediaAsset(file, group);
|
||||
}
|
||||
if (shouldExpireAdminSession(response.status, Boolean(token))) {
|
||||
handleAuthExpired();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { ClipboardList, Headset, Home, List, LogOut } from "lucide-react";
|
||||
|
||||
import { clearToken } from "@/api";
|
||||
import { clearToken, logoutAdmin } from "@/api";
|
||||
import { ToastStack } from "@/components/admin/ToastStack";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ConciergePage } from "@/pages/structure/ConciergePage";
|
||||
@@ -56,8 +56,10 @@ export function AdminShell({ onLogout }: { onLogout: () => void }) {
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
clearToken();
|
||||
onLogout();
|
||||
void logoutAdmin().finally(() => {
|
||||
clearToken();
|
||||
onLogout();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,7 @@ export function LoginPage({ onDone }: { onDone: () => void }) {
|
||||
setError("");
|
||||
try {
|
||||
const result = await login(email, password);
|
||||
setToken(result.token);
|
||||
setToken(result.accessToken ?? result.token);
|
||||
onDone();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "登录失败");
|
||||
|
||||
Reference in new issue
Block a user