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/, ''),
},
},
},