mirror of
https://github.com/zuiidea/antd-admin
synced 2026-10-28 20:03:57 +00:00
chore: scaffold apps/basic from with-lingui
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import type { MenuItem } from "@/api/schemas";
|
||||
|
||||
/**
|
||||
* Built-in menu tree. Visibility is computed with {@link filterMenuTreeByPermissions}
|
||||
* from `GET /api/auth/permissions` (must stay consistent with route permission map below).
|
||||
*/
|
||||
export const APP_MENU_TREE: MenuItem[] = [
|
||||
{
|
||||
id: "g-platform",
|
||||
kind: "group",
|
||||
name: "Platform",
|
||||
path: null,
|
||||
icon: "IconLucideSparkles",
|
||||
permissions: null,
|
||||
sort: 0,
|
||||
hidden: false,
|
||||
children: [
|
||||
{
|
||||
id: "1",
|
||||
kind: "item",
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
icon: "IconLucideLayoutDashboard",
|
||||
children: null,
|
||||
permissions: null,
|
||||
sort: 0,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
kind: "item",
|
||||
name: "Users",
|
||||
path: "/users",
|
||||
icon: "IconLucideUsers",
|
||||
children: null,
|
||||
permissions: ["user:view"],
|
||||
sort: 1,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "g-projects",
|
||||
kind: "group",
|
||||
name: "Projects",
|
||||
path: null,
|
||||
icon: "IconLucideFolderKanban",
|
||||
children: [
|
||||
{
|
||||
id: "3",
|
||||
kind: "item",
|
||||
name: "Design Engineering",
|
||||
path: "/design-engineering",
|
||||
icon: "IconLucideBookOpen",
|
||||
children: null,
|
||||
permissions: null,
|
||||
sort: 0,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
kind: "item",
|
||||
name: "Sales & Marketing",
|
||||
path: "/sales-marketing",
|
||||
icon: "IconLucideBriefcase",
|
||||
children: null,
|
||||
permissions: ["user:view"],
|
||||
sort: 1,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
permissions: null,
|
||||
sort: 1,
|
||||
hidden: false,
|
||||
},
|
||||
];
|
||||
|
||||
function hasRequiredPermissions(
|
||||
required: string[] | null | undefined,
|
||||
granted: Set<string>,
|
||||
): boolean {
|
||||
if (!required || required.length === 0) return true;
|
||||
return required.every((p) => granted.has(p));
|
||||
}
|
||||
|
||||
export function filterMenuTreeByPermissions(
|
||||
nodes: MenuItem[],
|
||||
permissionList: string[],
|
||||
): MenuItem[] {
|
||||
const granted = new Set(permissionList);
|
||||
|
||||
const walk = (list: MenuItem[]): MenuItem[] =>
|
||||
list
|
||||
.map((node) => {
|
||||
if (!hasRequiredPermissions(node.permissions ?? null, granted)) return null;
|
||||
|
||||
if (node.kind === "group") {
|
||||
const children = walk(node.children);
|
||||
if (children.length === 0) return null;
|
||||
return { ...node, children };
|
||||
}
|
||||
|
||||
if (node.children?.length) {
|
||||
const children = walk(node.children);
|
||||
if (children.length === 0) return null;
|
||||
return { ...node, children };
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
.filter((n): n is MenuItem => n != null);
|
||||
|
||||
return walk(nodes);
|
||||
}
|
||||
|
||||
/** Normalized pathname (no trailing slash except `/`) */
|
||||
export function normalizeAppPath(pathname: string): string {
|
||||
if (pathname === "/") return pathname;
|
||||
return pathname.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Route → permission required to open the page. `null` = no permission gate.
|
||||
* Keep in sync with {@link APP_MENU_TREE} paths.
|
||||
*/
|
||||
export function requiredPermissionForPath(pathname: string): string | null {
|
||||
const p = normalizeAppPath(pathname);
|
||||
const map: Record<string, string | null> = {
|
||||
"/dashboard": null,
|
||||
"/users": "user:view",
|
||||
"/design-engineering": null,
|
||||
"/sales-marketing": "user:view",
|
||||
"/403": null,
|
||||
};
|
||||
return map[p] ?? null;
|
||||
}
|
||||
|
||||
export function canAccessPath(pathname: string, permissions: string[] | undefined): boolean {
|
||||
const required = requiredPermissionForPath(pathname);
|
||||
if (required == null) return true;
|
||||
if (!permissions?.length) return false;
|
||||
return permissions.includes(required);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "";
|
||||
|
||||
/** Favicon path under `public/` (Vite serves as site root). */
|
||||
export const APP_FAVICON_SRC = "/favicon.svg";
|
||||
|
||||
/** Product / brand name (login header, sidebar logo text, etc.). */
|
||||
export const APP_BRAND_NAME = "Antd Admin";
|
||||
@@ -0,0 +1,89 @@
|
||||
import { API_BASE_URL } from "./constants";
|
||||
|
||||
export class ApiError extends Error {
|
||||
code: number;
|
||||
constructor(code: number, message: string) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
status: number;
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.name = "HttpError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
type RequestOptions = Omit<RequestInit, "method" | "body"> & {
|
||||
params?: Record<string, string | number | null | undefined>;
|
||||
};
|
||||
|
||||
function buildUrl(path: string, params?: RequestOptions["params"]): string {
|
||||
const base = path.startsWith("http") ? path : `${API_BASE_URL}${path}`;
|
||||
if (!params) return base;
|
||||
const url = new URL(base, window.location.origin);
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value != null) url.searchParams.set(key, String(value));
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
try {
|
||||
const raw = localStorage.getItem("auth-storage");
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw);
|
||||
const token = parsed?.state?.tokens?.accessToken;
|
||||
if (token) return { Authorization: `Bearer ${token}` };
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
options?: RequestOptions,
|
||||
): Promise<T> {
|
||||
const url = buildUrl(path, options?.params);
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...getAuthHeaders(),
|
||||
...(options?.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new HttpError(res.status, `HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.code !== undefined && json.code !== 0) {
|
||||
throw new ApiError(json.code, json.message ?? "Unknown error");
|
||||
}
|
||||
|
||||
return json.data !== undefined ? json.data : json;
|
||||
}
|
||||
|
||||
export const httpClient = {
|
||||
get: <T>(path: string, options?: RequestOptions) => request<T>("GET", path, undefined, options),
|
||||
post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
|
||||
request<T>("POST", path, body, options),
|
||||
put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
|
||||
request<T>("PUT", path, body, options),
|
||||
delete: <T>(path: string, options?: RequestOptions) =>
|
||||
request<T>("DELETE", path, undefined, options),
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { httpClient } from "@/utils/http";
|
||||
import { AUTH_ENDPOINTS } from "@/api/auth";
|
||||
import { AuthUserResponseSchema, PermissionsListSchema, UserSchema } from "@/api/schemas";
|
||||
import { APP_MENU_TREE, filterMenuTreeByPermissions } from "@/utils/appMenu";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
/** Fetches `/api/auth/user` and `/api/auth/permissions`, then updates `user` and sidebar `menus`. */
|
||||
export async function fetchSessionAndApplyToStore(): Promise<void> {
|
||||
const [userBase, permissions] = await Promise.all([
|
||||
httpClient.get(AUTH_ENDPOINTS.user).then((d) => AuthUserResponseSchema.parse(d)),
|
||||
httpClient.get(AUTH_ENDPOINTS.permissions).then((d) => PermissionsListSchema.parse(d)),
|
||||
]);
|
||||
const user = UserSchema.parse({ ...userBase, permissions });
|
||||
const menus = filterMenuTreeByPermissions(APP_MENU_TREE, permissions);
|
||||
const { setUser, setMenus } = useAuthStore.getState();
|
||||
setUser(user);
|
||||
setMenus(menus);
|
||||
}
|
||||
Reference in New Issue
Block a user