diff --git a/src/App.tsx b/src/App.tsx index 7a8c7f4..365cfc0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,8 @@ import AdminLayout from '@/layouts/AdminLayout' import LoginPage from '@/pages/LoginPage' import ErrorBoundary from '@/components/ErrorBoundary' import { Suspense, useMemo, lazy, useEffect } from 'react' -import { Loader2 } from 'lucide-react' +import { Loader2, Shield } from 'lucide-react' +import { Button } from '@/components/ui/button' import type { SystemMenu } from '@/api/system' const pages = import.meta.glob('./pages/**/*.tsx') @@ -25,14 +26,27 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { function PublicRoute({ children }: { children: React.ReactNode }) { const isAuthenticated = useAuthStore(s => s.isAuthenticated) - if (isAuthenticated) return + if (isAuthenticated) return return <>{children} } +function NoPermission() { + const logout = useAuthStore(s => s.logout) + return ( +
+ +

访问受限

+

抱歉,您当前暂无任何系统权限,请联系管理员为您分配相关菜单与角色。

+ +
+ ) +} + function AppRoutes() { const menus = useAuthStore(s => s.menus) const isAuthenticated = useAuthStore(s => s.isAuthenticated) const refreshMenus = useAuthStore(s => s.refreshMenus) + const hasFetchedMenus = useAuthStore(s => s.hasFetchedMenus) useEffect(() => { if (isAuthenticated && menus.length === 0) refreshMenus() @@ -59,17 +73,17 @@ function AppRoutes() { } /> }> - } /> + 0 ? : + ) : Loading + } /> {dynamicRoutes.map(({ path, Component }) => ( } /> ))} - {!dynamicRoutes.some(r => r.path === '/dashboard') && dynamicComponentMap['/dashboard'] && ( - - {(() => { const D = dynamicComponentMap['/dashboard']; return })()} - - } /> + {hasFetchedMenus && dynamicRoutes.length === 0 && ( + } /> )} } /> diff --git a/src/api/system.ts b/src/api/system.ts index adb1fbf..8813d3f 100644 --- a/src/api/system.ts +++ b/src/api/system.ts @@ -1,13 +1,13 @@ import { get, post } from '@/lib/request' -import { USE_MOCK, delay, mockResponse } from '@/mock' -import { mockUsers } from '@/mock/system/users' // ==================== Types ==================== export interface SystemUser { id: string; account: string; name: string; nickName?: string; phone?: string avatar?: SystemOss; avatarId?: string; clientId?: string; tenantId?: string - createdAt: string; updatedAt: string; roles?: SystemRole[] + createdAt: string; updatedAt: string; roles?: string[] // Adjusted based on backend info + menus?: any[] + gender?: number } export interface SystemRole { @@ -44,26 +44,20 @@ export interface OperationLog { createdAt: string } -export interface CaptchaRes { captcha: string; captchaId: string } +export interface CaptchaRes { captchaImg: string; captchaId: string } export interface LoginParams { account: string; password: string; captcha: string; captchaId: string } -export interface LoginResponse { token: string; expiresAt: number; user: SystemUser } +export interface LoginResponse { token: string; userInfo: SystemUser } // ==================== Auth ==================== export async function getCaptcha() { - if (USE_MOCK) { await delay(200); return mockResponse({ captcha: 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=', captchaId: 'mock-captcha-id' }) } return get<{ code: number; data: CaptchaRes; msg: string }>('/auth/captcha') } export async function login(data: LoginParams) { - if (USE_MOCK) { await delay(500); return mockResponse({ token: 'mock-token-xxx', expiresAt: Date.now() + 7200000, user: mockUsers[0] }, '登录成功') } return post<{ code: number; data: LoginResponse; msg: string }>('/auth/login', data) } export async function logout() { - if (USE_MOCK) { await delay(100); return mockResponse(null, '已登出') } - return get<{ code: number; data: null; msg: string }>('/auth/logout') + return get<{ code: number; data: null; msg: string }>('/auth/logout') // If backend doesn't have it, we just clear local token } - -// ==================== 以下在各自文件中实现 ==================== -// 这里只导出类型,具体 CRUD 在 api/system/*.ts 子文件中 diff --git a/src/api/systemCrud.ts b/src/api/systemCrud.ts index 2e4e2d3..df9851d 100644 --- a/src/api/systemCrud.ts +++ b/src/api/systemCrud.ts @@ -1,188 +1,120 @@ import { get, post, type PageResult, type PageParams } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockId, mockResponse } from '@/mock' -import { mockUsers } from '@/mock/system/users' -import { mockRoles } from '@/mock/system/roles' -import { mockMenuTree } from '@/mock/system/menus' -import { mockClients } from '@/mock/system/clients' -import { mockFiles } from '@/mock/system/files' -import { mockLogs } from '@/mock/system/logs' import type { SystemUser, SystemRole, SystemMenu, SystemClient, SystemOss, OperationLog } from './system' // ==================== User ==================== -export async function getUserList(data: PageParams & { account?: string; phone?: string }) { - if (USE_MOCK) { - await delay() - let filtered = [...mockUsers] - if (data.keyword) filtered = filtered.filter(u => u.name.includes(data.keyword!) || u.account.includes(data.keyword!)) - return mockResponse(paginate(filtered, data.current, data.pageSize)) - } - return post<{ data: PageResult }>('/user/getUserList', data) +export async function getUserList(data: PageParams & { account?: string; phone?: string; name?: string }) { + // map keyword to name or account if needed, or backend can handle it + const reqData = { ...data, name: data.keyword, account: data.keyword } + return post<{ data: PageResult }>('/sys/user/list', reqData) } export async function saveUser(data: Partial) { - if (USE_MOCK) { await delay(); mockUsers.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemUser); return mockResponse(null, '创建成功') } - return post<{ msg: string }>('/user/save', data) + return post<{ msg: string }>('/sys/user/create', data) } export async function updateUser(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockUsers.findIndex(u => u.id === data.id); if (i >= 0) Object.assign(mockUsers[i], data); return mockResponse(null, '更新成功') } - return post<{ msg: string }>('/user/update', data) + return post<{ msg: string }>('/sys/user/update', data) } export async function deleteUser(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockUsers.findIndex(u => u.id === id); if (i >= 0) mockUsers.splice(i, 1) }); return mockResponse(null, '删除成功') } - return post<{ msg: string }>('/user/delete', { ids }) + return post<{ msg: string }>('/sys/user/delete', { ids }) } export async function changePassword(data: { id: string; newPwd: string }) { - if (USE_MOCK) { await delay(); return mockResponse(null, '密码修改成功') } - return post<{ msg: string }>('/user/changePassword', data) + // admin reset password + return post<{ msg: string }>('/sys/user/resetPassword', { id: data.id, password: data.newPwd }) } export async function grantRole(data: { userId: string; roleIds: string[] }) { - if (USE_MOCK) { - await delay() - const user = mockUsers.find(u => u.id === data.userId) - if (user) user.roles = mockRoles.filter(r => data.roleIds.includes(r.id)).map(r => ({ ...r })) - return mockResponse(null, '角色分配成功') - } - return post<{ msg: string }>('/user/grantRole', data) + // backend UserUpdateReq has RoleIds + return post<{ msg: string }>('/sys/user/update', { id: data.userId, roleIds: data.roleIds }) } // ==================== Role ==================== export async function getRoleList(data: PageParams & { name?: string }) { - if (USE_MOCK) { - await delay() - let filtered = [...mockRoles] - if (data.keyword) filtered = filtered.filter(r => r.name.includes(data.keyword!)) - return mockResponse(paginate(filtered, data.current, data.pageSize)) - } - return post<{ data: PageResult }>('/role/getRoleList', data) + return post<{ data: PageResult }>('/sys/role/list', { ...data, name: data.keyword }) } export async function getAllRoles() { - if (USE_MOCK) { await delay(100); return mockResponse([...mockRoles]) } - return post<{ data: SystemRole[] }>('/role/getAllRoles', {}) + return post<{ data: { list: SystemRole[] } }>('/sys/role/list', { current: 1, pageSize: 1000 }) } export async function saveRole(data: Partial) { - if (USE_MOCK) { await delay(); mockRoles.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemRole); return mockResponse(null, '创建成功') } - return post<{ msg: string }>('/role/save', data) + return post<{ msg: string }>('/sys/role/create', data) } export async function updateRole(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockRoles.findIndex(r => r.id === data.id); if (i >= 0) Object.assign(mockRoles[i], data); return mockResponse(null, '更新成功') } - return post<{ msg: string }>('/role/update', data) + return post<{ msg: string }>('/sys/role/update', data) } export async function deleteRole(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockRoles.findIndex(r => r.id === id); if (i >= 0) mockRoles.splice(i, 1) }); return mockResponse(null, '删除成功') } - return post<{ msg: string }>('/role/delete', { ids }) + return post<{ msg: string }>('/sys/role/delete', { ids }) } export async function grantMenu(data: { roleId: string; menuIds: string[] }) { - if (USE_MOCK) { - await delay() - const role = mockRoles.find(r => r.id === data.roleId) - if (role) { - const flatMenus = (items: SystemMenu[]): SystemMenu[] => items.flatMap(m => [m, ...(m.children ? flatMenus(m.children) : [])]) - role.menus = flatMenus(mockMenuTree).filter(m => data.menuIds.includes(m.id)) - } - return mockResponse(null, '菜单授权成功') - } - return post<{ msg: string }>('/role/grantMenu', data) + return post<{ msg: string }>('/sys/role/update', { id: data.roleId, menuIds: data.menuIds }) } // ==================== Menu ==================== export async function getAllMenuTree() { - if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } - return post<{ data: SystemMenu[] }>('/menu/getAllMenuTree', {}) + return get<{ data: { list: SystemMenu[] } }>('/sys/menu/list') } export async function getUserMenuTree() { - if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } - return get<{ data: SystemMenu[] }>('/menu/getUserMenuTree') + // Get current user info (including menus) + return get<{ data: SystemUser }>('/auth/info') } export async function saveMenu(data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } - return post<{ msg: string }>('/menu/save', data) + return post<{ msg: string }>('/sys/menu/create', data) } export async function updateMenu(data: Partial) { - if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } - return post<{ msg: string }>('/menu/update', data) + return post<{ msg: string }>('/sys/menu/update', data) } export async function deleteMenu(id: string) { - if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } - return get<{ msg: string }>('/menu/delete', { id }) + return post<{ msg: string }>('/sys/menu/delete', { ids: [id] }) } // ==================== Client ==================== export async function getClientList(data: PageParams & { clientId?: string; name?: string }) { - if (USE_MOCK) { - await delay() - let filtered = [...mockClients] - if (data.keyword) filtered = filtered.filter(c => c.name.includes(data.keyword!) || c.clientId.includes(data.keyword!)) - return mockResponse(paginate(filtered, data.current, data.pageSize)) - } - return post<{ data: PageResult }>('/client/getClientList', data) + return post<{ data: PageResult }>('/sys/client/list', { ...data, name: data.keyword }) } export async function saveClient(data: Partial) { - if (USE_MOCK) { await delay(); mockClients.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemClient); return mockResponse(null, '创建成功') } - return post<{ msg: string }>('/client/save', data) + return post<{ msg: string }>('/sys/client/create', data) } export async function updateClient(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockClients.findIndex(c => c.id === data.id); if (i >= 0) Object.assign(mockClients[i], data); return mockResponse(null, '更新成功') } - return post<{ msg: string }>('/client/update', data) + return post<{ msg: string }>('/sys/client/update', data) } export async function deleteClient(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockClients.findIndex(c => c.id === id); if (i >= 0) mockClients.splice(i, 1) }); return mockResponse(null, '删除成功') } - return post<{ msg: string }>('/client/delete', { ids }) + return post<{ msg: string }>('/sys/client/delete', { ids }) } // ==================== File ==================== export async function getFileList(data: PageParams & { name?: string }) { - if (USE_MOCK) { - await delay() - let filtered = [...mockFiles] - if (data.keyword) filtered = filtered.filter(f => f.name.includes(data.keyword!)) - return mockResponse(paginate(filtered, data.current, data.pageSize)) - } - return post<{ data: PageResult }>('/oss/getFileList', data) + return post<{ data: PageResult }>('/file/oss/getFileList', data) } export async function uploadFile(_file: File) { - if (USE_MOCK) { await delay(800); const f: SystemOss = { id: mockId(), name: _file.name, key: `uploads/${_file.name}`, url: `https://picsum.photos/seed/${Date.now()}/400/300`, suffix: _file.name.split('.').pop() || '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; mockFiles.unshift(f); return mockResponse({ file: f }, '上传成功') } const formData = new FormData(); formData.append('file', _file) - return post<{ data: { file: SystemOss }; msg: string }>('/oss/upload', formData) + return post<{ data: { file: SystemOss }; msg: string }>('/file/oss/upload', formData) } export async function deleteFile(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockFiles.findIndex(f => f.id === id); if (i >= 0) mockFiles.splice(i, 1) }); return mockResponse(null, '删除成功') } - return post<{ msg: string }>('/oss/delete', { ids }) + return post<{ msg: string }>('/file/oss/delete', { ids }) } // ==================== Operation Log ==================== export async function getOperationLogList(data: PageParams & { clientId?: string; method?: string; statusCode?: number }) { - if (USE_MOCK) { - await delay() - let filtered = [...mockLogs] - if (data.clientId) filtered = filtered.filter(l => l.clientId === data.clientId) - if (data.method) filtered = filtered.filter(l => l.method === data.method) - if (data.statusCode !== undefined) filtered = filtered.filter(l => l.statusCode === data.statusCode) - if (data.keyword) filtered = filtered.filter(l => l.path.includes(data.keyword!) || l.title.includes(data.keyword!) || l.operatorName.includes(data.keyword!)) - return mockResponse(paginate(filtered, data.current, data.pageSize)) - } - return post<{ data: PageResult }>('/log/getOperationLogList', data) + return post<{ data: PageResult }>('/sys/log/list', data) } diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index 8beb2ff..d8e2c26 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -80,7 +80,7 @@ function convertMenuToNavItem(menu: SystemMenu): NavItem { href: menu.path || menu.code || `/${menu.name.toLowerCase()}`, icon: getIcon(menu.icon), permission: menu.permission, - children: menu.children?.map(convertMenuToNavItem), + children: menu.children?.filter(c => c.category !== 2).length ? menu.children.filter(c => c.category !== 2).map(convertMenuToNavItem) : undefined, } } @@ -191,8 +191,8 @@ export default function AdminLayout() { const navigate = useNavigate() const navItems = useMemo(() => { - if (menus?.length) return menus.map(convertMenuToNavItem) - return [{ title: '仪表盘', href: '/dashboard', icon: }] + if (menus?.length) return menus.filter(m => m.category !== 2).map(convertMenuToNavItem) + return [] }, [menus]) const handleLogout = async () => { await logout(); navigate('/login') } diff --git a/src/lib/request.ts b/src/lib/request.ts index e9b83fb..f1f0f4e 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -12,6 +12,7 @@ const request = axios.create({ request.interceptors.request.use( (config: InternalAxiosRequestConfig) => { + config.headers['X-Client-Id'] = 'sundynix-admin' const isWhitelisted = AUTH_WHITELIST.some(path => config.url?.startsWith(path)) if (!isWhitelisted) { const token = localStorage.getItem('token') diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 5690b41..4570bff 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -35,7 +35,7 @@ export default function LoginPage() { const res = await getCaptcha() const d = (res as any).data setCaptchaId(d.captchaId) - setCaptchaImg(d.captcha) + setCaptchaImg(d.captchaImg) } catch { /* ignore */ } } @@ -48,7 +48,7 @@ export default function LoginPage() { try { const res = await apiLogin({ account, password, captcha, captchaId }) const d = (res as any).data - loginStore(d.user, d.token) + loginStore(d.userInfo, d.token) navigate('/dashboard', { replace: true }) } catch (err: any) { setError(err?.message || '登录失败') @@ -171,8 +171,6 @@ export default function LoginPage() { : '登 录'} - -

Mock 模式 · 任意账号密码均可登录

diff --git a/src/store/auth.ts b/src/store/auth.ts index ab934d2..ee89448 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -12,6 +12,7 @@ interface AuthState { isAuthenticated: boolean menus: SystemMenu[] permissions: string[] + hasFetchedMenus: boolean login: (user: SystemUser, token: string) => void logout: () => Promise refreshMenus: () => Promise @@ -47,27 +48,31 @@ export const useAuthStore = create((set, get) => ({ isAuthenticated: initial.isAuthenticated, menus: [], permissions: [], + hasFetchedMenus: false, login: (user, token) => { localStorage.setItem(TOKEN_KEY, token) localStorage.setItem(USER_KEY, JSON.stringify(user)) - set({ user, token, isAuthenticated: true, menus: [], permissions: [] }) + set({ user, token, isAuthenticated: true, menus: [], permissions: [], hasFetchedMenus: false }) }, logout: async () => { try { await apiLogout() } catch { /* ignore */ } localStorage.removeItem(TOKEN_KEY) localStorage.removeItem(USER_KEY) - set({ user: null, token: null, isAuthenticated: false, menus: [], permissions: [] }) + set({ user: null, token: null, isAuthenticated: false, menus: [], permissions: [], hasFetchedMenus: false }) }, refreshMenus: async () => { if (!get().isAuthenticated) return try { const res = await getUserMenuTree() - const menus = res.data || [] - set({ menus, permissions: extractPermissions(menus) }) - } catch (e) { console.error('获取菜单失败:', e) } + const menus = (res.data as any).menus || [] + set({ menus, permissions: extractPermissions(menus), hasFetchedMenus: true }) + } catch (e) { + console.error('获取菜单失败:', e) + set({ hasFetchedMenus: true }) + } }, hasPermission: (permission) => { diff --git a/vite.config.ts b/vite.config.ts index 66eadac..0f6261f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,6 @@ export default defineConfig({ '/api': { target: 'http://127.0.0.1:8888', changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), }, }, },