feat: rbac初步对接完成

This commit is contained in:
Blizzard
2026-04-30 22:53:46 +08:00
parent 3ed0b76fc2
commit e3e38800aa
45 changed files with 1637 additions and 467 deletions
+1 -1
View File
@@ -1 +1 @@
VITE_USE_MOCK=true VITE_USE_MOCK=false
+1050
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -85,6 +85,14 @@ function AppRoutes() {
{hasFetchedMenus && dynamicRoutes.length === 0 && ( {hasFetchedMenus && dynamicRoutes.length === 0 && (
<Route path="*" element={<NoPermission />} /> <Route path="*" element={<NoPermission />} />
)} )}
{hasFetchedMenus && dynamicRoutes.length > 0 && (
<Route path="*" element={
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center w-full">
<h2 className="text-2xl font-bold mb-2 text-foreground"></h2>
<p className="text-muted-foreground"></p>
</div>
} />
)}
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
+55 -36
View File
@@ -1,25 +1,42 @@
import { get, post } from '@/lib/request'
// ==================== Types ==================== // ==================== Types ====================
export interface SystemUser { export interface SystemUser {
id: string; account: string; name: string; nickName?: string; phone?: string id: string
avatar?: SystemOss; avatarId?: string; clientId?: string; tenantId?: string name: string
createdAt: string; updatedAt: string; roles?: string[] // Adjusted based on backend info account: string
menus?: any[] nickName?: string
gender?: number phone?: string
avatarId?: string
gender?: number // 0=未知 1=男 2=女
roles?: string[] // 角色 code 列表(仅 /auth/info 返回)
menus?: SystemMenu[] // 菜单树(仅 /auth/info 返回)
createdAt?: number // Unix 时间戳(秒)
roleIds?: string[] // 关联角色 ID(用户管理接口)
} }
export interface SystemRole { export interface SystemRole {
id: string; name: string; code: string; sort?: number id: string
menus?: SystemMenu[]; createdAt: string; updatedAt: string name: string
code: string
sort?: number
menuIds?: string[] // 关联菜单 ID 列表
} }
export interface SystemMenu { export interface SystemMenu {
id: string; name: string; title?: string; code?: string; path?: string id: string
icon?: string; locale?: string; parentId?: string; permission?: string parentId?: string
sort?: number; category?: number; children?: SystemMenu[] category?: number // 1=菜单 2=按钮/权限
createdAt: string; updatedAt: string name: string // 路由名(英文)
title?: string // 显示标题(中文)
code?: string // 权限标识
path?: string // 路由路径
permission?: string // 操作权限标识
locale?: string // 国际化 key
icon?: string // 图标名称
sort?: number // 排序
children?: SystemMenu[]
} }
export interface SystemOss { export interface SystemOss {
@@ -29,35 +46,37 @@ export interface SystemOss {
} }
export interface SystemClient { export interface SystemClient {
id: string; clientId: string; name: string; grantType?: string id: string
activeTimeout?: number; additionalInfo?: string clientId: string
createdAt: string; updatedAt: string name: string
grantType?: string
additionalInfo?: string
activeTimeout?: number // Token有效期(秒)
} }
export interface OperationLog { export interface OperationLog {
id: string; operatorId: string; operatorName: string id: string
clientId: string; clientName: string clientId: string
method: string; path: string; title: string ip: string
statusCode: number; duration: number // ms method: string
ip: string; userAgent?: string path: string
requestBody?: string; responseBody?: string status: number
createdAt: string latency: number // 纳秒
agent: string
errorMessage: string
body: string // 请求体 JSON 字符串
resp: string // 响应体 JSON 字符串
userId: string
createdAt: number // Unix 时间戳(秒)
} }
export interface CaptchaRes { captchaImg: string; captchaId: string } export interface DictInfo {
export interface LoginParams { account: string; password: string; captcha: string; captchaId: string } id: string
export interface LoginResponse { token: string; userInfo: SystemUser } type: string
label: string
// ==================== Auth ==================== value: string
sort: number
export async function getCaptcha() { desc: string
return get<{ code: number; data: CaptchaRes; msg: string }>('/auth/captcha')
} }
export async function login(data: LoginParams) {
return post<{ code: number; data: LoginResponse; msg: string }>('/auth/login', data)
}
export async function logout() {
return get<{ code: number; data: null; msg: string }>('/auth/logout') // If backend doesn't have it, we just clear local token
}
+29
View File
@@ -0,0 +1,29 @@
import { get, post } from '@/lib/request'
import type { SystemUser } from '../system'
export interface CaptchaRes { captchaImg: string; captchaId: string }
export interface LoginParams { account: string; password: string; captcha: string; captchaId: string }
export interface LoginResponse { token: string; userInfo: SystemUser }
const BASE_URL = '/auth'
export async function getCaptcha() {
return get<CaptchaRes>(`${BASE_URL}/captcha`)
}
export async function login(data: LoginParams) {
return post<LoginResponse>(`${BASE_URL}/login`, data)
}
export async function getUserInfo() {
return get<SystemUser>(`${BASE_URL}/info`)
}
export async function updateProfile(data: Partial<Pick<SystemUser, 'name' | 'nickName' | 'phone' | 'avatarId'>>) {
return post<null>(`${BASE_URL}/update`, data)
}
export async function logout() {
// 后端无 logout 接口,仅清理本地状态
return Promise.resolve()
}
+37
View File
@@ -0,0 +1,37 @@
import { post } from '@/lib/request'
import { USE_MOCK, delay, paginate } from '@/mock'
import type { SystemClient } from '../system'
import type { PageResult } from '@/lib/request'
const BASE_URL = '/sys/client'
const mockClients: SystemClient[] = [
{ id: '1', clientId: 'sundynix-admin', name: 'Web管理端', grantType: 'password', activeTimeout: 7200 },
{ id: '2', clientId: 'mini-app', name: '微信小程序', grantType: 'wechat', activeTimeout: 3600 },
{ id: '3', clientId: 'plant', name: 'Plant 花园服务', grantType: 'password', activeTimeout: 3600 },
]
export async function getClientList(params: { current: number; pageSize: number; name?: string }) {
if (USE_MOCK) {
await delay()
let list = [...mockClients]
if (params.name) list = list.filter(c => c.name.includes(params.name!))
return paginate(list, params.current, params.pageSize)
}
return post<PageResult<SystemClient>>(`${BASE_URL}/list`, params)
}
export async function createClient(data: Partial<SystemClient>) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/create`, data)
}
export async function updateClient(data: Partial<SystemClient> & { id: string }) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/update`, data)
}
export async function deleteClient(ids: string[]) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/delete`, { ids })
}
+37
View File
@@ -0,0 +1,37 @@
import { post } from '@/lib/request'
import { USE_MOCK, delay, paginate } from '@/mock'
import type { DictInfo } from '../system'
import type { PageResult } from '@/lib/request'
const BASE_URL = '/sys/dict'
const mockDicts: DictInfo[] = [
{ id: '1', type: 'gender', label: '男', value: '1', sort: 1, desc: '性别-男' },
{ id: '2', type: 'gender', label: '女', value: '2', sort: 2, desc: '性别-女' },
{ id: '3', type: 'gender', label: '未知', value: '0', sort: 3, desc: '性别-未知' },
]
export async function getDictList(params: { current: number; pageSize: number; type?: string }) {
if (USE_MOCK) {
await delay()
let list = [...mockDicts]
if (params.type) list = list.filter(d => d.type === params.type)
return paginate(list, params.current, params.pageSize)
}
return post<PageResult<DictInfo>>(`${BASE_URL}/list`, params)
}
export async function createDict(data: Partial<DictInfo>) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/create`, data)
}
export async function updateDict(data: Partial<DictInfo> & { id: string }) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/update`, data)
}
export async function deleteDict(ids: string[]) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/delete`, { ids })
}
+49
View File
@@ -0,0 +1,49 @@
import { post } from '@/lib/request'
import { USE_MOCK, delay, paginate, mockId } from '@/mock'
import type { SystemOss } from '../system'
import type { PageResult } from '@/lib/request'
const BASE_URL = '/file/oss'
const mockFiles: SystemOss[] = [
{ id: '1', name: 'avatar.jpg', key: 'avatars/1.jpg', url: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=800&q=80', suffix: 'jpg', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
{ id: '2', name: 'bg.png', key: 'bg/1.png', url: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80', suffix: 'png', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
]
export async function getFileList(params: { current: number; pageSize: number; keyword?: string }) {
if (USE_MOCK) {
await delay()
let list = [...mockFiles]
if (params.keyword) list = list.filter(f => f.name.includes(params.keyword!))
return paginate(list, params.current, params.pageSize)
}
return post<PageResult<SystemOss>>(`${BASE_URL}/getFileList`, params)
}
export async function uploadFile(_file: File) {
if (USE_MOCK) {
await delay(1000)
const newFile: SystemOss = {
id: mockId(), name: _file.name, key: `upload/${_file.name}`,
url: URL.createObjectURL(_file), suffix: _file.name.split('.').pop() || '',
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString()
}
mockFiles.unshift(newFile)
return { file: newFile }
}
const formData = new FormData()
formData.append('file', _file)
return post<{ file: SystemOss }>(`${BASE_URL}/upload`, formData)
}
export async function deleteFile(ids: string[]) {
if (USE_MOCK) {
await delay()
const idSet = new Set(ids)
for (let i = mockFiles.length - 1; i >= 0; i--) {
if (idSet.has(mockFiles[i].id)) mockFiles.splice(i, 1)
}
return null
}
return post<null>(`${BASE_URL}/delete`, { ids })
}
+15 -9
View File
@@ -1,25 +1,31 @@
import { get } from '@/lib/request' import { post } from '@/lib/request'
import { USE_MOCK, delay, paginate, mockResponse } from '@/mock' import { USE_MOCK, delay, paginate } from '@/mock'
import { mockLogs } from '@/mock/system/logs' import { mockLogs } from '@/mock/system/logs'
import type { OperationLog } from '../system' import type { OperationLog } from '../system'
import type { PageResult } from '@/lib/request'
const BASE_URL = '/sys/log'
export async function getLogList(params: { export async function getLogList(params: {
current: number; current: number;
pageSize: number; pageSize: number;
operatorName?: string; method?: string;
clientId?: string;
path?: string; path?: string;
status?: number; status?: number;
}) { }) {
if (USE_MOCK) { if (USE_MOCK) {
await delay() await delay()
let list = [...mockLogs] let list = [...mockLogs]
if (params.operatorName) list = list.filter(l => l.operatorName?.includes(params.operatorName!)) if (params.method) list = list.filter(l => l.method === params.method)
if (params.clientId && params.clientId !== 'all') list = list.filter(l => l.clientId === params.clientId)
if (params.path) list = list.filter(l => l.path.includes(params.path!)) if (params.path) list = list.filter(l => l.path.includes(params.path!))
if (params.status) list = list.filter(l => l.statusCode === params.status) if (params.status) list = list.filter(l => l.status === params.status)
return mockResponse(paginate(list, params.current, params.pageSize)) return paginate(list, params.current, params.pageSize)
} }
return get<{ code: number; data: { list: OperationLog[]; total: number }; msg: string }>('/system/logs', params) return post<PageResult<OperationLog>>(`${BASE_URL}/list`, params)
}
export async function deleteLog(ids: string[]) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/delete`, { ids })
} }
+19 -12
View File
@@ -1,24 +1,31 @@
import { get, post, put, del } from '@/lib/request' import { get, post } from '@/lib/request'
import { USE_MOCK, delay, mockResponse } from '@/mock' import { USE_MOCK, delay } from '@/mock'
import { mockMenuTree } from '@/mock/system/menus' import { mockMenuTree } from '@/mock/system/menus'
import type { SystemMenu } from '../system' import type { SystemMenu } from '../system'
const BASE_URL = '/sys/menu'
export async function getMenuTree() { export async function getMenuTree() {
if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } if (USE_MOCK) { await delay(); return mockMenuTree }
return get<{ code: number; data: SystemMenu[]; msg: string }>('/system/menus/tree') return get<SystemMenu[]>(`${BASE_URL}/list`)
}
export async function getMenuByRole(id: string) {
if (USE_MOCK) { await delay(); return mockMenuTree }
return post<SystemMenu[]>(`${BASE_URL}/byRole`, { id })
} }
export async function createMenu(data: Partial<SystemMenu>) { export async function createMenu(data: Partial<SystemMenu>) {
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } if (USE_MOCK) { await delay(); return null }
return post<{ code: number; data: null; msg: string }>('/system/menus', data) return post<null>(`${BASE_URL}/create`, data)
} }
export async function updateMenu(id: string, data: Partial<SystemMenu>) { export async function updateMenu(data: Partial<SystemMenu> & { id: string }) {
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } if (USE_MOCK) { await delay(); return null }
return put<{ code: number; data: null; msg: string }>(`/system/menus/${id}`, data) return post<null>(`${BASE_URL}/update`, data)
} }
export async function deleteMenu(id: string) { export async function deleteMenu(ids: string[]) {
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } if (USE_MOCK) { await delay(); return null }
return del<{ code: number; data: null; msg: string }>(`/system/menus/${id}`) return post<null>(`${BASE_URL}/delete`, { ids })
} }
+20 -17
View File
@@ -1,13 +1,16 @@
import { get, post, put, del } from '@/lib/request' import { post } from '@/lib/request'
import { USE_MOCK, delay, paginate, mockDate, mockResponse } from '@/mock' import { USE_MOCK, delay, paginate } from '@/mock'
import type { SystemRole } from '../system' import type { SystemRole } from '../system'
import type { PageResult } from '@/lib/request'
const BASE_URL = '/sys/role'
const mockRoles: SystemRole[] = [ const mockRoles: SystemRole[] = [
{ id: '1', name: '超级管理员', code: 'super_admin', sort: 1, createdAt: mockDate(120), updatedAt: mockDate(1) }, { id: '1', name: '超级管理员', code: 'super_admin', sort: 1 },
{ id: '2', name: '系统管理员', code: 'admin', sort: 2, createdAt: mockDate(120), updatedAt: mockDate(1) }, { id: '2', name: '系统管理员', code: 'admin', sort: 2 },
{ id: '3', name: '运营专员', code: 'operator', sort: 3, createdAt: mockDate(90), updatedAt: mockDate(1) }, { id: '3', name: '运营专员', code: 'operator', sort: 3 },
{ id: '4', name: '内容审核', code: 'auditor', sort: 4, createdAt: mockDate(90), updatedAt: mockDate(1) }, { id: '4', name: '内容审核', code: 'auditor', sort: 4 },
{ id: '5', name: '客服', code: 'customer_service', sort: 5, createdAt: mockDate(30), updatedAt: mockDate(1) }, { id: '5', name: '客服', code: 'customer_service', sort: 5 },
] ]
export async function getRoleList(params: { current: number; pageSize: number; name?: string }) { export async function getRoleList(params: { current: number; pageSize: number; name?: string }) {
@@ -15,22 +18,22 @@ export async function getRoleList(params: { current: number; pageSize: number; n
await delay() await delay()
let list = [...mockRoles] let list = [...mockRoles]
if (params.name) list = list.filter(r => r.name.includes(params.name!)) if (params.name) list = list.filter(r => r.name.includes(params.name!))
return mockResponse(paginate(list, params.current, params.pageSize)) return paginate(list, params.current, params.pageSize)
} }
return get<{ code: number; data: { list: SystemRole[]; total: number }; msg: string }>('/system/roles', params) return post<PageResult<SystemRole>>(`${BASE_URL}/list`, params)
} }
export async function createRole(data: Partial<SystemRole>) { export async function createRole(data: Partial<SystemRole>) {
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } if (USE_MOCK) { await delay(); return null }
return post<{ code: number; data: null; msg: string }>('/system/roles', data) return post<null>(`${BASE_URL}/create`, data)
} }
export async function updateRole(id: string, data: Partial<SystemRole>) { export async function updateRole(data: Partial<SystemRole> & { id: string }) {
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } if (USE_MOCK) { await delay(); return null }
return put<{ code: number; data: null; msg: string }>(`/system/roles/${id}`, data) return post<null>(`${BASE_URL}/update`, data)
} }
export async function deleteRole(id: string) { export async function deleteRole(ids: string[]) {
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } if (USE_MOCK) { await delay(); return null }
return del<{ code: number; data: null; msg: string }>(`/system/roles/${id}`) return post<null>(`${BASE_URL}/delete`, { ids })
} }
+21 -13
View File
@@ -1,7 +1,10 @@
import { get, post, put, del } from '@/lib/request' import { post } from '@/lib/request'
import { USE_MOCK, delay, paginate, mockResponse } from '@/mock' import { USE_MOCK, delay, paginate } from '@/mock'
import { mockUsers } from '@/mock/system/users' import { mockUsers } from '@/mock/system/users'
import type { SystemUser } from '../system' import type { SystemUser } from '../system'
import type { PageResult } from '@/lib/request'
const BASE_URL = '/sys/user'
export async function getUserList(params: { current: number; pageSize: number; account?: string; name?: string }) { export async function getUserList(params: { current: number; pageSize: number; account?: string; name?: string }) {
if (USE_MOCK) { if (USE_MOCK) {
@@ -9,22 +12,27 @@ export async function getUserList(params: { current: number; pageSize: number; a
let list = [...mockUsers] let list = [...mockUsers]
if (params.account) list = list.filter(u => u.account.includes(params.account!)) if (params.account) list = list.filter(u => u.account.includes(params.account!))
if (params.name) list = list.filter(u => u.name.includes(params.name!)) if (params.name) list = list.filter(u => u.name.includes(params.name!))
return mockResponse(paginate(list, params.current, params.pageSize)) return paginate(list, params.current, params.pageSize)
} }
return get<{ code: number; data: { list: SystemUser[]; total: number }; msg: string }>('/system/users', params) return post<PageResult<SystemUser>>(`${BASE_URL}/list`, params)
} }
export async function createUser(data: Partial<SystemUser>) { export async function createUser(data: Partial<SystemUser> & { password?: string }) {
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } if (USE_MOCK) { await delay(); return null }
return post<{ code: number; data: null; msg: string }>('/system/users', data) return post<null>(`${BASE_URL}/create`, data)
} }
export async function updateUser(id: string, data: Partial<SystemUser>) { export async function updateUser(data: Partial<SystemUser> & { id: string }) {
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } if (USE_MOCK) { await delay(); return null }
return put<{ code: number; data: null; msg: string }>(`/system/users/${id}`, data) return post<null>(`${BASE_URL}/update`, data)
} }
export async function deleteUser(id: string) { export async function deleteUser(ids: string[]) {
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } if (USE_MOCK) { await delay(); return null }
return del<{ code: number; data: null; msg: string }>(`/system/users/${id}`) return post<null>(`${BASE_URL}/delete`, { ids })
}
export async function resetPassword(data: { id: string; password: string }) {
if (USE_MOCK) { await delay(); return null }
return post<null>(`${BASE_URL}/resetPassword`, data)
} }
-120
View File
@@ -1,120 +0,0 @@
import { get, post, type PageResult, type PageParams } from '@/lib/request'
import type { SystemUser, SystemRole, SystemMenu, SystemClient, SystemOss, OperationLog } from './system'
// ==================== User ====================
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<SystemUser> }>('/sys/user/list', reqData)
}
export async function saveUser(data: Partial<SystemUser>) {
return post<{ msg: string }>('/sys/user/create', data)
}
export async function updateUser(data: Partial<SystemUser>) {
return post<{ msg: string }>('/sys/user/update', data)
}
export async function deleteUser(ids: string[]) {
return post<{ msg: string }>('/sys/user/delete', { ids })
}
export async function changePassword(data: { id: string; newPwd: string }) {
// 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[] }) {
// 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 }) {
return post<{ data: PageResult<SystemRole> }>('/sys/role/list', { ...data, name: data.keyword })
}
export async function getAllRoles() {
return post<{ data: { list: SystemRole[] } }>('/sys/role/list', { current: 1, pageSize: 1000 })
}
export async function saveRole(data: Partial<SystemRole>) {
return post<{ msg: string }>('/sys/role/create', data)
}
export async function updateRole(data: Partial<SystemRole>) {
return post<{ msg: string }>('/sys/role/update', data)
}
export async function deleteRole(ids: string[]) {
return post<{ msg: string }>('/sys/role/delete', { ids })
}
export async function grantMenu(data: { roleId: string; menuIds: string[] }) {
return post<{ msg: string }>('/sys/role/update', { id: data.roleId, menuIds: data.menuIds })
}
// ==================== Menu ====================
export async function getAllMenuTree() {
return get<{ data: { list: SystemMenu[] } }>('/sys/menu/list')
}
export async function getUserMenuTree() {
// Get current user info (including menus)
return get<{ data: SystemUser }>('/auth/info')
}
export async function saveMenu(data: Partial<SystemMenu>) {
return post<{ msg: string }>('/sys/menu/create', data)
}
export async function updateMenu(data: Partial<SystemMenu>) {
return post<{ msg: string }>('/sys/menu/update', data)
}
export async function deleteMenu(id: string) {
return post<{ msg: string }>('/sys/menu/delete', { ids: [id] })
}
// ==================== Client ====================
export async function getClientList(data: PageParams & { clientId?: string; name?: string }) {
return post<{ data: PageResult<SystemClient> }>('/sys/client/list', { ...data, name: data.keyword })
}
export async function saveClient(data: Partial<SystemClient>) {
return post<{ msg: string }>('/sys/client/create', data)
}
export async function updateClient(data: Partial<SystemClient>) {
return post<{ msg: string }>('/sys/client/update', data)
}
export async function deleteClient(ids: string[]) {
return post<{ msg: string }>('/sys/client/delete', { ids })
}
// ==================== File ====================
export async function getFileList(data: PageParams & { name?: string }) {
return post<{ data: PageResult<SystemOss> }>('/file/oss/getFileList', data)
}
export async function uploadFile(_file: File) {
const formData = new FormData(); formData.append('file', _file)
return post<{ data: { file: SystemOss }; msg: string }>('/file/oss/upload', formData)
}
export async function deleteFile(ids: string[]) {
return post<{ msg: string }>('/file/oss/delete', { ids })
}
// ==================== Operation Log ====================
export async function getOperationLogList(data: PageParams & { clientId?: string; method?: string; statusCode?: number }) {
return post<{ data: PageResult<OperationLog> }>('/sys/log/list', data)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
export default function AIBanner() { export default function AIBanner() {
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Command } from 'cmdk' import { Command } from 'cmdk'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Search, Monitor, Moon, Sun, Laptop, Palette } from 'lucide-react' import { Search, Monitor, Moon, Sun, Laptop } from 'lucide-react'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import './cmdk.css' // We'll add some styles for cmdk import './cmdk.css' // We'll add some styles for cmdk
@@ -10,7 +10,7 @@ export default function CommandPalette() {
const { cmdKOpen, setCmdKOpen, setThemeHue } = useAppStore() const { cmdKOpen, setCmdKOpen, setThemeHue } = useAppStore()
const menus = useAuthStore(s => s.menus) const menus = useAuthStore(s => s.menus)
const navigate = useNavigate() const navigate = useNavigate()
const [theme, setTheme] = useState(document.documentElement.classList.contains('dark') ? 'dark' : 'light') const [, setTheme] = useState(document.documentElement.classList.contains('dark') ? 'dark' : 'light')
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
interface Particle { interface Particle {
x: number; x: number;
+1 -1
View File
@@ -32,7 +32,7 @@ export default function TabBar() {
// Auto-register tab on route change // Auto-register tab on route change
useEffect(() => { useEffect(() => {
const path = location.pathname const path = location.pathname
if (path === '/login') return if (path === '/login' || path === '/') return
const title = resolveTitle(menus || [], path) const title = resolveTitle(menus || [], path)
|| path.split('/').filter(Boolean).pop()?.replace(/^\w/, c => c.toUpperCase()) || 'Page' || path.split('/').filter(Boolean).pop()?.replace(/^\w/, c => c.toUpperCase()) || 'Page'
addTab({ path, title, closable: path !== '/dashboard' }) addTab({ path, title, closable: path !== '/dashboard' })
+29
View File
@@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
+6 -8
View File
@@ -1,5 +1,5 @@
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom' import { NavLink, useNavigate, useLocation, useOutlet } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion' import { motion } from 'framer-motion'
import CommandPalette from '@/components/CommandPalette' import CommandPalette from '@/components/CommandPalette'
import { import {
LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf, LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf,
@@ -210,6 +210,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
}) { }) {
const { setCmdKOpen } = useAppStore() const { setCmdKOpen } = useAppStore()
const location = useLocation() const location = useLocation()
const outlet = useOutlet()
const spotlightRef = useRef<HTMLDivElement>(null) const spotlightRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
@@ -272,7 +273,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className={cn("flex w-full items-center gap-3 rounded-xl p-2 transition-all duration-200 hover:bg-white/50 dark:hover:bg-white/5 outline-none", !sidebarOpen && "justify-center")}> <button className={cn("flex w-full items-center gap-3 rounded-xl p-2 transition-all duration-200 hover:bg-white/50 dark:hover:bg-white/5 outline-none", !sidebarOpen && "justify-center")}>
<Avatar className="h-9 w-9 ring-2 ring-white/80 dark:ring-white/10 shadow-sm"> <Avatar className="h-9 w-9 ring-2 ring-white/80 dark:ring-white/10 shadow-sm">
<AvatarImage src={user?.avatar?.url} alt={user?.name || user?.account} /> <AvatarImage alt={user?.name || user?.account} />
<AvatarFallback className="bg-emerald-100 text-emerald-700 text-xs font-bold">{(user?.name || user?.account)?.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback className="bg-emerald-100 text-emerald-700 text-xs font-bold">{(user?.name || user?.account)?.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar> </Avatar>
{sidebarOpen && ( {sidebarOpen && (
@@ -345,17 +346,14 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<main className="p-4 lg:p-6 relative z-0 min-h-full"> <main className="p-4 lg:p-6 relative z-0 min-h-full">
<div className="mx-auto w-full max-w-[1600px] space-y-6"> <div className="mx-auto w-full max-w-[1600px] space-y-6">
<AnimatePresence mode="wait">
<motion.div <motion.div
key={location.pathname} key={location.pathname}
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }} transition={{ duration: 0.3 }}
transition={{ duration: 0.2 }}
> >
<Outlet /> {outlet}
</motion.div> </motion.div>
</AnimatePresence>
</div> </div>
</main> </main>
</ScrollArea> </ScrollArea>
+7 -2
View File
@@ -28,6 +28,10 @@ request.interceptors.request.use(
request.interceptors.response.use( request.interceptors.response.use(
response => { response => {
const newToken = response.headers['x-refresh-token']
if (newToken) {
localStorage.setItem('token', newToken) // 静默替换
}
const res = response.data const res = response.data
if (res.code !== undefined && res.code !== 200) { if (res.code !== undefined && res.code !== 200) {
if (res.code === 401) { if (res.code === 401) {
@@ -37,7 +41,8 @@ request.interceptors.response.use(
} }
return Promise.reject(new Error(res.msg || '请求失败')) return Promise.reject(new Error(res.msg || '请求失败'))
} }
return res // 统一返回 data 字段,调用方直接拿到业务数据
return res.data
}, },
(error: AxiosError) => { (error: AxiosError) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
@@ -68,5 +73,5 @@ export function del<T = unknown>(url: string, config?: AxiosRequestConfig): Prom
export default request export default request
export interface ApiResponse<T = unknown> { code: number; data: T; msg: string } export interface ApiResponse<T = unknown> { code: number; data: T; msg: string }
export interface PageResult<T = unknown> { list: T[]; page: number; pageSize: number; total: number } export interface PageResult<T = unknown> { list: T[]; total: number; current?: number; size?: number }
export interface PageParams { current: number; pageSize: number; keyword?: string } export interface PageParams { current: number; pageSize: number; keyword?: string }
-2
View File
@@ -4,7 +4,5 @@ import './index.css'
import App from './App' import App from './App'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode>
<App /> <App />
</StrictMode>,
) )
+3 -4
View File
@@ -1,8 +1,7 @@
import { mockDate } from '../index'
import type { SystemClient } from '@/api/system' import type { SystemClient } from '@/api/system'
export const mockClients: SystemClient[] = [ export const mockClients: SystemClient[] = [
{ id: '1', clientId: 'plant', name: 'Plant 花园服务', grantType: 'password,refresh_token', activeTimeout: 7200, createdAt: mockDate(90), updatedAt: mockDate(1) }, { id: '1', clientId: 'plant', name: 'Plant 花园服务', grantType: 'password,refresh_token', activeTimeout: 7200 },
{ id: '2', clientId: 'radio', name: 'Radio 电台服务', grantType: 'password,refresh_token', activeTimeout: 7200, createdAt: mockDate(90), updatedAt: mockDate(1) }, { id: '2', clientId: 'radio', name: 'Radio 电台服务', grantType: 'password,refresh_token', activeTimeout: 7200 },
{ id: '3', clientId: 'admin', name: '管理后台', grantType: 'password', activeTimeout: 3600, createdAt: mockDate(120), updatedAt: mockDate(1) }, { id: '3', clientId: 'admin', name: '管理后台', grantType: 'password', activeTimeout: 3600 },
] ]
+16 -64
View File
@@ -1,70 +1,22 @@
import { mockDate, mockId } from '../index'
import type { OperationLog } from '@/api/system' import type { OperationLog } from '@/api/system'
const methods = ['GET', 'POST', 'PUT', 'DELETE'] const clients = ['sundynix-admin', 'plant', 'radio', 'system']
const clients = [
{ id: 'gateway', name: 'API 网关' },
{ id: 'plant', name: 'Plant 服务' },
{ id: 'radio', name: 'Radio 服务' },
]
const operators = [
{ id: 'u1', name: '超级管理员' },
{ id: 'u2', name: '张三' },
{ id: 'u3', name: '李四' },
]
const apis = [
{ method: 'POST', path: '/api/auth/login', title: '用户登录', status: 200 },
{ method: 'GET', path: '/api/auth/captcha', title: '获取验证码', status: 200 },
{ method: 'POST', path: '/api/user/getUserList', title: '查询用户列表', status: 200 },
{ method: 'POST', path: '/api/user/save', title: '创建用户', status: 200 },
{ method: 'POST', path: '/api/user/update', title: '更新用户', status: 200 },
{ method: 'POST', path: '/api/user/delete', title: '删除用户', status: 200 },
{ method: 'POST', path: '/api/user/grantRole', title: '分配角色', status: 200 },
{ method: 'POST', path: '/api/role/getRoleList', title: '查询角色列表', status: 200 },
{ method: 'POST', path: '/api/role/grantMenu', title: '角色授权菜单', status: 200 },
{ method: 'POST', path: '/api/menu/getAllMenuTree', title: '查询菜单树', status: 200 },
{ method: 'POST', path: '/api/oss/upload', title: '上传文件', status: 200 },
{ method: 'POST', path: '/api/plant/wiki/list', title: '百科列表', status: 200 },
{ method: 'POST', path: '/api/plant/wiki/save', title: '新增百科', status: 200 },
{ method: 'POST', path: '/api/plant/banner/list', title: '轮播图列表', status: 200 },
{ method: 'POST', path: '/api/radio/channel/list', title: '频道列表', status: 200 },
{ method: 'POST', path: '/api/radio/program/save', title: '新增节目', status: 200 },
{ method: 'GET', path: '/api/radio/subscription/list', title: '订阅列表', status: 200 },
{ method: 'POST', path: '/api/user/changePassword', title: '修改密码', status: 200 },
{ method: 'GET', path: '/api/client/getClientList', title: '客户端列表', status: 200 },
{ method: 'POST', path: '/api/auth/logout', title: '用户登出', status: 200 },
{ method: 'POST', path: '/api/user/save', title: '创建用户', status: 400 },
{ method: 'POST', path: '/api/oss/upload', title: '上传文件', status: 500 },
]
const ips = ['192.168.1.100', '10.0.0.15', '172.16.0.42', '192.168.2.88']
const uas = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'PostmanRuntime/7.37.3',
]
function rand<T>(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)] }
export const mockLogs: OperationLog[] = Array.from({ length: 120 }, (_, i) => { export const mockLogs: OperationLog[] = Array.from({ length: 120 }, (_, i) => {
const api = rand(apis) const method = ['GET', 'POST', 'PUT', 'DELETE'][Math.floor(Math.random() * 4)]
const client = rand(clients)
const op = rand(operators)
return { return {
id: mockId(), id: `log-${i + 1}`,
operatorId: op.id, userId: 'admin',
operatorName: op.name, clientId: clients[Math.floor(Math.random() * clients.length)],
clientId: client.id, ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
clientName: client.name, method,
method: api.method, path: `/api/v1/resource/${Math.floor(Math.random() * 10)}`,
path: api.path, status: Math.random() > 0.1 ? 200 : 500,
title: api.title, latency: Math.floor(Math.random() * 500) * 1000000,
statusCode: api.status, agent: 'Mozilla/5.0',
duration: Math.floor(Math.random() * 500) + 5, errorMessage: '',
ip: rand(ips), body: method === 'GET' ? '' : '{"key": "value"}',
userAgent: rand(uas), resp: '{"code": 200}',
requestBody: api.method === 'GET' ? undefined : '{"page":1}', createdAt: Date.now() / 1000 - Math.floor(Math.random() * 86400 * 30),
responseBody: api.status === 200 ? '{"code":0,"msg":"ok"}' : '{"code":1,"msg":"error"}',
createdAt: mockDate(i * 0.3),
} }
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) }).sort((a, b) => b.createdAt - a.createdAt)
+31 -34
View File
@@ -1,68 +1,65 @@
import { mockDate } from '../index'
import type { SystemMenu } from '@/api/system' import type { SystemMenu } from '@/api/system'
const d = (days: number) => mockDate(days)
export const mockMenuTree: SystemMenu[] = [ export const mockMenuTree: SystemMenu[] = [
{ {
id: '1', name: 'dashboard', title: '仪表盘', path: '/dashboard', icon: 'dashboard', id: '1', name: 'dashboard', title: '仪表盘', path: '/dashboard', icon: 'dashboard',
sort: 0, category: 1, createdAt: d(90), updatedAt: d(1), sort: 0, category: 1
}, },
{ {
id: '10', name: 'system', title: '系统管理', path: '/system', icon: 'settings', id: '10', name: 'system', title: '系统管理', path: '/system', icon: 'settings',
sort: 1, category: 1, createdAt: d(90), updatedAt: d(1), sort: 1, category: 1,
children: [ children: [
{ id: '11', name: 'users', title: '用户管理', path: '/system/users', icon: 'users', parentId: '10', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '11', name: 'users', title: '用户管理', path: '/system/users', icon: 'users', parentId: '10', sort: 0, category: 1 },
{ id: '12', name: 'roles', title: '角色管理', path: '/system/roles', icon: 'shield', parentId: '10', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '12', name: 'roles', title: '角色管理', path: '/system/roles', icon: 'shield', parentId: '10', sort: 1, category: 1 },
{ id: '13', name: 'menus', title: '菜单管理', path: '/system/menus', icon: 'menu', parentId: '10', sort: 2, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '13', name: 'menus', title: '菜单管理', path: '/system/menus', icon: 'menu', parentId: '10', sort: 2, category: 1 },
{ id: '14', name: 'clients', title: '客户端管理', path: '/system/clients', icon: 'monitor', parentId: '10', sort: 3, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '14', name: 'clients', title: '客户端管理', path: '/system/clients', icon: 'monitor', parentId: '10', sort: 3, category: 1 },
{ id: '15', name: 'files', title: '文件管理', path: '/system/files', icon: 'folder', parentId: '10', sort: 4, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '15', name: 'files', title: '文件管理', path: '/system/files', icon: 'folder', parentId: '10', sort: 4, category: 1 },
{ id: '16', name: 'logs', title: '操作日志', path: '/system/logs', icon: 'scroll', parentId: '10', sort: 5, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '16', name: 'logs', title: '操作日志', path: '/system/logs', icon: 'scroll', parentId: '10', sort: 5, category: 1 },
{ id: '17', name: 'dict', title: '字典管理', path: '/system/dict', icon: 'book', parentId: '10', sort: 6, category: 1 },
], ],
}, },
{ {
id: '20', name: 'plant', title: '植趣', path: '/plant', icon: 'leaf', id: '20', name: 'plant', title: 'Plant 服务', icon: 'leaf', sort: 2, category: 1,
sort: 2, category: 1, createdAt: d(90), updatedAt: d(1),
children: [ children: [
{ {
id: '200', name: 'achievement', title: '成就配置', path: '/plant/achievement', icon: 'trophy', parentId: '20', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1), id: '200', name: 'achievement', title: '成就系统', path: '/plant/achievement', icon: 'award', parentId: '20', sort: 0, category: 1,
children: [ children: [
{ id: '201', name: 'badge', title: '徽章配置', path: '/plant/achievement/badge', icon: 'award', parentId: '200', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '201', name: 'badge', title: '徽章配置', path: '/plant/achievement/badge', icon: 'award', parentId: '200', sort: 0, category: 1 },
{ id: '202', name: 'level', title: '等级配置', path: '/plant/achievement/level', icon: 'star', parentId: '200', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '202', name: 'level', title: '等级配置', path: '/plant/achievement/level', icon: 'star', parentId: '200', sort: 1, category: 1 },
], ],
}, },
{ {
id: '210', name: 'community', title: '社区管理', path: '/plant/community', icon: 'message', parentId: '20', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1), id: '210', name: 'community', title: '社区管理', path: '/plant/community', icon: 'users', parentId: '20', sort: 1, category: 1,
children: [ children: [
{ id: '211', name: 'topic', title: '话题管理', path: '/plant/community/topic', icon: 'message', parentId: '210', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '211', name: 'topic', title: '话题管理', path: '/plant/community/topic', icon: 'message', parentId: '210', sort: 0, category: 1 },
{ id: '212', name: 'post', title: '帖子管理', path: '/plant/community/post', icon: 'file-text', parentId: '210', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '212', name: 'post', title: '帖子管理', path: '/plant/community/post', icon: 'file-text', parentId: '210', sort: 1, category: 1 },
], ]
}, },
{ {
id: '220', name: 'wiki', title: '百科管理', path: '/plant/wiki', icon: 'book', parentId: '20', sort: 2, category: 1, createdAt: d(90), updatedAt: d(1), id: '220', name: 'wiki', title: '百科管理', path: '/plant/wiki', icon: 'book-open', parentId: '20', sort: 2, category: 1,
children: [ children: [
{ id: '221', name: 'wikiClass', title: '分类管理', path: '/plant/wiki/class', icon: 'tree', parentId: '220', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '221', name: 'wikiClass', title: '分类管理', path: '/plant/wiki/class', icon: 'tree', parentId: '220', sort: 0, category: 1 },
{ id: '222', name: 'wikiList', title: '植物百科', path: '/plant/wiki/wiki', icon: 'book', parentId: '220', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '222', name: 'wikiList', title: '植物百科', path: '/plant/wiki/wiki', icon: 'book', parentId: '220', sort: 1, category: 1 },
], ]
}, },
{ {
id: '230', name: 'exchange', title: '兑换中心', path: '/plant/exchange', icon: 'gift', parentId: '20', sort: 3, category: 1, createdAt: d(90), updatedAt: d(1), id: '230', name: 'exchange', title: '积分商城', path: '/plant/exchange', icon: 'shopping-bag', parentId: '20', sort: 3, category: 1,
children: [ children: [
{ id: '231', name: 'exchangeOrder', title: '兑换订单', path: '/plant/exchange/order', icon: 'list', parentId: '230', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '231', name: 'exchangeOrder', title: '兑换订单', path: '/plant/exchange/order', icon: 'list', parentId: '230', sort: 0, category: 1 },
{ id: '232', name: 'exchangeConfig', title: '兑换配置', path: '/plant/exchange/config', icon: 'settings', parentId: '230', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '232', name: 'exchangeConfig', title: '兑换配置', path: '/plant/exchange/config', icon: 'settings', parentId: '230', sort: 1, category: 1 },
], ]
}, },
{ id: '240', name: 'banner', title: '小程序首页轮播', path: '/plant/banner', icon: 'image', parentId: '20', sort: 4, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '240', name: 'banner', title: '小程序首页轮播', path: '/plant/banner', icon: 'image', parentId: '20', sort: 4, category: 1 },
], ],
}, },
{ {
id: '30', name: 'radio', title: 'Radio 服务', path: '/radio', icon: 'radio', id: '30', name: 'radio', title: 'Radio 服务', path: '/radio', icon: 'radio',
sort: 3, category: 1, createdAt: d(90), updatedAt: d(1), sort: 3, category: 1,
children: [ children: [
{ id: '31', name: 'channel', title: '频道管理', path: '/radio/channel', icon: 'radio', parentId: '30', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '31', name: 'channel', title: '频道管理', path: '/radio/channel', icon: 'radio', parentId: '30', sort: 0, category: 1 },
{ id: '32', name: 'program', title: '节目管理', path: '/radio/program', icon: 'music', parentId: '30', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '32', name: 'program', title: '节目管理', path: '/radio/program', icon: 'music', parentId: '30', sort: 1, category: 1 },
{ id: '33', name: 'radioCategory', title: '频道分类', path: '/radio/category', icon: 'category', parentId: '30', sort: 2, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '33', name: 'radioCategory', title: '频道分类', path: '/radio/category', icon: 'category', parentId: '30', sort: 2, category: 1 },
{ id: '34', name: 'subscription', title: '订阅管理', path: '/radio/subscription', icon: 'user', parentId: '30', sort: 3, category: 1, createdAt: d(90), updatedAt: d(1) }, { id: '34', name: 'subscription', title: '订阅管理', path: '/radio/subscription', icon: 'user', parentId: '30', sort: 3, category: 1 },
], ],
}, },
] ]
+5 -5
View File
@@ -1,9 +1,9 @@
import { mockDate } from '../index'
import type { SystemRole } from '@/api/system' import type { SystemRole } from '@/api/system'
export const mockRoles: SystemRole[] = [ export const mockRoles: SystemRole[] = [
{ id: '1', name: '超级管理员', code: 'super_admin', sort: 0, createdAt: mockDate(120), updatedAt: mockDate(1) }, { id: '1', name: '超级管理员', code: 'super_admin', sort: 0 },
{ id: '2', name: '运营', code: 'operator', sort: 1, createdAt: mockDate(90), updatedAt: mockDate(1) }, { id: '2', name: '运营', code: 'operator', sort: 1 },
{ id: '3', name: '编辑', code: 'editor', sort: 2, createdAt: mockDate(90), updatedAt: mockDate(1) }, { id: '3', name: '编辑', code: 'editor', sort: 2 },
{ id: '4', name: '访客', code: 'guest', sort: 3, createdAt: mockDate(60), updatedAt: mockDate(1) }, { id: '4', name: '访客', code: 'guest', sort: 3 },
] ]
+6 -6
View File
@@ -1,10 +1,10 @@
import { mockDate } from '../index'
import type { SystemUser } from '@/api/system' import type { SystemUser } from '@/api/system'
export const mockUsers: SystemUser[] = [ export const mockUsers: SystemUser[] = [
{ id: '1', account: 'admin', name: '超级管理员', phone: '13800138000', createdAt: mockDate(120), updatedAt: mockDate(1), roles: [{ id: '1', name: '超级管理员', code: 'super_admin', createdAt: mockDate(120), updatedAt: mockDate(1) }] }, { id: '1', account: 'admin', name: '系统管理员', phone: '13800138000', roles: ['super_admin'] },
{ id: '2', account: 'zhangsan', name: '张三', phone: '13900139000', clientId: 'plant', createdAt: mockDate(60), updatedAt: mockDate(5), roles: [{ id: '2', name: '运营', code: 'operator', createdAt: mockDate(90), updatedAt: mockDate(1) }] }, { id: '2', account: 'zhangsan', name: '张三', phone: '13900139000', roles: ['operator'] },
{ id: '3', account: 'lisi', name: '李四', phone: '13700137000', clientId: 'radio', createdAt: mockDate(30), updatedAt: mockDate(2), roles: [{ id: '3', name: '编辑', code: 'editor', createdAt: mockDate(90), updatedAt: mockDate(1) }] }, { id: '3', account: 'lisi', name: '李四', phone: '13700137000', roles: ['editor'] },
{ id: '4', account: 'wangwu', name: '王五', phone: '13600136000', clientId: 'plant', createdAt: mockDate(15), updatedAt: mockDate(3) }, { id: '4', account: 'wangwu', name: '王五', phone: '13600136000' },
{ id: '5', account: 'zhaoliu', name: '赵六', phone: '13500135000', createdAt: mockDate(7), updatedAt: mockDate(1) }, { id: '5', account: 'zhaoliu', name: '赵六', phone: '13500135000' },
] ]
+2 -9
View File
@@ -2,8 +2,8 @@ import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { import {
Users, Leaf, Radio, Folder, Activity, BarChart3, Users, Leaf, Radio, Folder, Activity, BarChart3,
RefreshCw, Wifi, WifiOff, Zap, Clock, HardDrive, RefreshCw, Wifi, Zap, Clock,
Cpu, MemoryStick, ArrowUpRight, TrendingUp, Server, Cpu, ArrowUpRight, TrendingUp, Server,
CheckCircle2, AlertTriangle, XCircle, Globe, Database, CheckCircle2, AlertTriangle, XCircle, Globe, Database,
Gauge, Gauge,
} from 'lucide-react' } from 'lucide-react'
@@ -119,13 +119,6 @@ const statusConfig = {
down: { label: '离线', icon: XCircle, color: 'text-red-500', bg: 'bg-red-500/10', dot: 'bg-red-500' }, down: { label: '离线', icon: XCircle, color: 'text-red-500', bg: 'bg-red-500/10', dot: 'bg-red-500' },
} }
function formatUptime(seconds: number): string {
const d = Math.floor(seconds / 86400)
const h = Math.floor((seconds % 86400) / 3600)
if (d > 0) return `${d}${h}`
return `${h}小时`
}
const svcIcons: Record<string, React.ReactNode> = { const svcIcons: Record<string, React.ReactNode> = {
'svc-plant': <Leaf className="h-4 w-4" />, 'svc-plant': <Leaf className="h-4 w-4" />,
'svc-radio': <Radio className="h-4 w-4" />, 'svc-radio': <Radio className="h-4 w-4" />,
+3 -5
View File
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import { login as apiLogin, getCaptcha } from '@/api/system' import { login as apiLogin, getCaptcha } from '@/api/system/auth'
import AIBanner from '@/components/AIBanner' import AIBanner from '@/components/AIBanner'
import LoginCharacters from '@/components/LoginCharacters' import LoginCharacters from '@/components/LoginCharacters'
import ParticleBackground from '@/components/ParticleBackground' import ParticleBackground from '@/components/ParticleBackground'
@@ -32,8 +32,7 @@ export default function LoginPage() {
const fetchCaptcha = async () => { const fetchCaptcha = async () => {
try { try {
const res = await getCaptcha() const d = await getCaptcha()
const d = (res as any).data
setCaptchaId(d.captchaId) setCaptchaId(d.captchaId)
setCaptchaImg(d.captchaImg) setCaptchaImg(d.captchaImg)
} catch { /* ignore */ } } catch { /* ignore */ }
@@ -46,8 +45,7 @@ export default function LoginPage() {
if (!account || !password) { setError('请输入账号和密码'); return } if (!account || !password) { setError('请输入账号和密码'); return }
setLoading(true); setError('') setLoading(true); setError('')
try { try {
const res = await apiLogin({ account, password, captcha, captchaId }) const d = await apiLogin({ account, password, captcha, captchaId })
const d = (res as any).data
loginStore(d.userInfo, d.token) loginStore(d.userInfo, d.token)
navigate('/dashboard', { replace: true }) navigate('/dashboard', { replace: true })
} catch (err: any) { } catch (err: any) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Plus, Search, Edit, Trash2, Award, RefreshCw, MoreHorizontal, Image } from 'lucide-react' import { Plus, Search, Edit, Trash2, Award, MoreHorizontal } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Plus, Search, Edit, Trash2, Star, MoreHorizontal, ArrowUp } from 'lucide-react' import { Plus, Edit, Trash2, Star, MoreHorizontal, ArrowUp } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
+4 -4
View File
@@ -1,17 +1,17 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Search, Pencil, Trash2, Loader2, MoreHorizontal, GalleryHorizontalEnd, Eye, Upload } from 'lucide-react' import { Plus, Search, Pencil, Trash2, Loader2, MoreHorizontal, GalleryHorizontalEnd } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { getBannerList, saveBanner, updateBanner, deleteBanner } from '@/api/plant' import { getBannerList, saveBanner, updateBanner, deleteBanner } from '@/api/plant'
import type { Banner } from '@/api/plant' import type { Banner } from '@/api/plant'
import { cn } from '@/lib/utils'
export default function BannerPage() { export default function BannerPage() {
const [banners, setBanners] = useState<Banner[]>([]) const [banners, setBanners] = useState<Banner[]>([])
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Search, RefreshCw, Eye, Package, Clock, CheckCircle2, XCircle, Truck } from 'lucide-react' import { Search, Eye, Package, Clock, CheckCircle2, XCircle, Truck } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Search, Trash2, Loader2, MoreHorizontal, Hash, Pencil } from 'lucide-react' import { Plus, Trash2, Loader2, MoreHorizontal, Hash } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
@@ -16,9 +16,9 @@ export default function ExchangePage() {
const [items, setItems] = useState<ExchangeItem[]>([]) const [items, setItems] = useState<ExchangeItem[]>([])
const [orders, setOrders] = useState<ExchangeOrder[]>([]) const [orders, setOrders] = useState<ExchangeOrder[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0) const [, setTotal] = useState(0)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [search, setSearch] = useState('') const [search] = useState('')
const [tab, setTab] = useState('items') const [tab, setTab] = useState('items')
const [dialogOpen, setDialogOpen] = useState(false) const [dialogOpen, setDialogOpen] = useState(false)
const [form, setForm] = useState({ name: '', description: '', points: 100, stock: 10, sort: 0 }) const [form, setForm] = useState({ name: '', description: '', points: 100, stock: 10, sort: 0 })
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Plus, Pencil, Trash2, Loader2, FolderTree } from 'lucide-react' import { Plus, Trash2, Loader2, FolderTree } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
+1 -1
View File
@@ -5,7 +5,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Search, Pencil, Trash2, Loader2, MoreHorizontal, Music, Play, Clock } from 'lucide-react' import { Plus, Search, Trash2, Loader2, MoreHorizontal, Music, Play, Clock } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
+2 -3
View File
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Search, Loader2, Users, Calendar, CreditCard } from 'lucide-react' import { Loader2, Users, Calendar, CreditCard } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
@@ -22,7 +21,7 @@ export default function SubscriptionPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [search, setSearch] = useState('') const [search] = useState('')
const [statusFilter, setStatusFilter] = useState('all') const [statusFilter, setStatusFilter] = useState('all')
const fetchList = useCallback(async () => { const fetchList = useCallback(async () => {
+3 -3
View File
@@ -8,7 +8,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { getClientList, saveClient, updateClient, deleteClient } from '@/api/systemCrud' import { getClientList, createClient, updateClient, deleteClient } from '@/api/system/client'
import type { SystemClient } from '@/api/system' import type { SystemClient } from '@/api/system'
export default function ClientsPage() { export default function ClientsPage() {
@@ -26,7 +26,7 @@ export default function ClientsPage() {
const fetchList = useCallback(async () => { const fetchList = useCallback(async () => {
setLoading(true) setLoading(true)
try { const res = await getClientList({ current: page, pageSize: 10, keyword: search || undefined }); if (res?.data) { setClients(res.data.list || []); setTotal(res.data.total || 0) } } try { const res = await getClientList({ current: page, pageSize: 10, name: search || undefined }); if (res) { setClients(res.list || []); setTotal(res.total || 0) } }
catch (e) { console.error(e) } finally { setLoading(false) } catch (e) { console.error(e) } finally { setLoading(false) }
}, [page, search]) }, [page, search])
@@ -37,7 +37,7 @@ export default function ClientsPage() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!form.clientId || !form.name) return; setSubmitting(true) if (!form.clientId || !form.name) return; setSubmitting(true)
try { if (isEdit) await updateClient(form); else await saveClient(form); setDialogOpen(false); fetchList() } try { if (isEdit) await updateClient({ ...form, id: form.id! }); else await createClient(form); setDialogOpen(false); fetchList() }
catch (e) { console.error(e) } finally { setSubmitting(false) } catch (e) { console.error(e) } finally { setSubmitting(false) }
} }
+2 -2
View File
@@ -7,7 +7,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { getFileList, uploadFile, deleteFile } from '@/api/systemCrud' import { getFileList, uploadFile, deleteFile } from '@/api/system/file'
import type { SystemOss } from '@/api/system' import type { SystemOss } from '@/api/system'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -35,7 +35,7 @@ export default function FilesPage() {
const fetchList = useCallback(async () => { const fetchList = useCallback(async () => {
setLoading(true) setLoading(true)
try { const res = await getFileList({ current: page, pageSize: 20, keyword: search || undefined }); if (res?.data) { setFiles(res.data.list || []); setTotal(res.data.total || 0) } } try { const res = await getFileList({ current: page, pageSize: 20, keyword: search || undefined }); if (res) { setFiles(res.list || []); setTotal(res.total || 0) } }
catch (e) { console.error(e) } finally { setLoading(false) } catch (e) { console.error(e) } finally { setLoading(false) }
}, [page, search]) }, [page, search])
+31 -30
View File
@@ -16,7 +16,7 @@ export default function Logs() {
const [logs, setLogs] = useState<OperationLog[]>([]) const [logs, setLogs] = useState<OperationLog[]>([])
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [search, setSearch] = useState({ operatorName: '', clientId: 'all', path: '' }) const [search, setSearch] = useState({ path: '', method: '', status: undefined as number | undefined })
const [pagination, setPagination] = useState({ current: 1, pageSize: 12 }) const [pagination, setPagination] = useState({ current: 1, pageSize: 12 })
// Dialog State for viewing details // Dialog State for viewing details
@@ -27,9 +27,9 @@ export default function Logs() {
setLoading(true) setLoading(true)
try { try {
const res = await getLogList({ ...pagination, ...search }) const res = await getLogList({ ...pagination, ...search })
if (res.data) { if (res) {
setLogs(res.data.list) setLogs(res.list)
setTotal(res.data.total) setTotal(res.total)
} }
} finally { } finally {
setLoading(false) setLoading(false)
@@ -44,7 +44,7 @@ export default function Logs() {
} }
const handleReset = () => { const handleReset = () => {
setSearch({ operatorName: '', clientId: 'all', path: '' }) setSearch({ path: '', method: '', status: undefined })
if (pagination.current === 1) setTimeout(() => fetchLogs(), 0) if (pagination.current === 1) setTimeout(() => fetchLogs(), 0)
else setPagination({ ...pagination, current: 1 }) else setPagination({ ...pagination, current: 1 })
} }
@@ -79,25 +79,27 @@ export default function Logs() {
</CardTitle> </CardTitle>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<Select value={search.clientId} onValueChange={v => setSearch({ ...search, clientId: v })}> <Select value={search.method || 'all'} onValueChange={v => setSearch({ ...search, method: v === 'all' ? '' : v })}>
<SelectTrigger className="w-[140px] h-9"> <SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="客户端来源" /> <SelectValue placeholder="请求方法" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all"></SelectItem> <SelectItem value="all"></SelectItem>
<SelectItem value="gateway">API </SelectItem> <SelectItem value="GET">GET</SelectItem>
<SelectItem value="plant">Plant </SelectItem> <SelectItem value="POST">POST</SelectItem>
<SelectItem value="radio">Radio </SelectItem> <SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<div className="relative"> <div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="操作者账号..." placeholder="状态码 (如 200, 400)..."
type="number"
className="pl-9 w-40 h-9" className="pl-9 w-40 h-9"
value={search.operatorName} value={search.status || ''}
onChange={e => setSearch({ ...search, operatorName: e.target.value })} onChange={e => setSearch({ ...search, status: e.target.value ? Number(e.target.value) : undefined })}
onKeyDown={e => e.key === 'Enter' && handleSearch()} onKeyDown={e => e.key === 'Enter' && handleSearch()}
/> />
</div> </div>
@@ -145,10 +147,10 @@ export default function Logs() {
) : ( ) : (
logs.map(log => ( logs.map(log => (
<TableRow key={log.id} className="hover:bg-muted/20"> <TableRow key={log.id} className="hover:bg-muted/20">
<TableCell className="font-medium pl-6">{log.operatorName || '-'}</TableCell> <TableCell className="font-medium pl-6">{log.userId || '-'}</TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="bg-primary/5 font-normal"> <Badge variant="outline" className="bg-primary/5 font-normal">
{log.clientName || log.clientId || 'System'} {log.clientId || 'System'}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -158,29 +160,28 @@ export default function Logs() {
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="font-medium text-sm">{log.title}</span> <span className="font-medium text-sm text-muted-foreground font-mono">{log.path}</span>
<span className="text-xs text-muted-foreground font-mono">{log.path}</span>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
{log.statusCode === 200 ? ( {log.status === 200 ? (
<div className="flex items-center text-emerald-600 text-sm"> <div className="flex items-center text-emerald-600 text-sm">
<Activity className="w-4 h-4 mr-1" /> 200 OK <Activity className="w-4 h-4 mr-1" /> 200 OK
</div> </div>
) : ( ) : (
<div className="flex items-center text-red-500 text-sm font-medium"> <div className="flex items-center text-red-500 text-sm font-medium">
<AlertCircle className="w-4 h-4 mr-1" /> {log.statusCode} <AlertCircle className="w-4 h-4 mr-1" /> {log.status}
</div> </div>
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className={`flex items-center text-sm font-mono ${log.duration > 200 ? 'text-amber-500 font-medium' : 'text-muted-foreground'}`}> <div className={`flex items-center text-sm font-mono ${log.latency > 200000000 ? 'text-amber-500 font-medium' : 'text-muted-foreground'}`}>
<Clock className="w-3.5 h-3.5 mr-1.5 opacity-70" /> <Clock className="w-3.5 h-3.5 mr-1.5 opacity-70" />
{log.duration}ms {(log.latency / 1000000).toFixed(0)}ms
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-muted-foreground text-sm font-mono"> <TableCell className="text-muted-foreground text-sm font-mono">
{new Date(log.createdAt).toLocaleString('zh-CN')} {new Date(log.createdAt * 1000).toLocaleString('zh-CN')}
</TableCell> </TableCell>
<TableCell className="text-right pr-6"> <TableCell className="text-right pr-6">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-blue-500" onClick={() => openDetail(log)}> <Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-blue-500" onClick={() => openDetail(log)}>
@@ -232,29 +233,29 @@ export default function Logs() {
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-muted/30 border border-border/50 text-sm"> <div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-muted/30 border border-border/50 text-sm">
<div><span className="text-muted-foreground inline-block w-20">:</span> <span className="font-mono font-medium">{activeLog.path}</span></div> <div><span className="text-muted-foreground inline-block w-20">:</span> <span className="font-mono font-medium">{activeLog.path}</span></div>
<div><span className="text-muted-foreground inline-block w-20">:</span> <span className="font-medium">{activeLog.operatorName}</span></div> <div><span className="text-muted-foreground inline-block w-20">ID:</span> <span className="font-medium">{activeLog.userId}</span></div>
<div><span className="text-muted-foreground inline-block w-20">:</span> {activeLog.clientName}</div> <div><span className="text-muted-foreground inline-block w-20">:</span> {activeLog.clientId}</div>
<div><span className="text-muted-foreground inline-block w-20">IP地址:</span> <span className="font-mono">{activeLog.ip}</span></div> <div><span className="text-muted-foreground inline-block w-20">IP地址:</span> <span className="font-mono">{activeLog.ip}</span></div>
<div className="col-span-2"><span className="text-muted-foreground inline-block w-20">User-Agent:</span> <span className="text-xs text-muted-foreground">{activeLog.userAgent}</span></div> <div className="col-span-2"><span className="text-muted-foreground inline-block w-20">User-Agent:</span> <span className="text-xs text-muted-foreground">{activeLog.agent}</span></div>
</div> </div>
{activeLog.requestBody && ( {activeLog.body && (
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-semibold flex items-center gap-2"><Activity className="w-4 h-4" /> Request Payload</h4> <h4 className="text-sm font-semibold flex items-center gap-2"><Activity className="w-4 h-4" /> Request Payload</h4>
<ScrollArea className="h-[120px] w-full rounded-md border bg-zinc-950 p-4"> <ScrollArea className="h-[120px] w-full rounded-md border bg-zinc-950 p-4">
<pre className="text-xs text-emerald-400 font-mono leading-relaxed"> <pre className="text-xs text-emerald-400 font-mono leading-relaxed">
{JSON.stringify(JSON.parse(activeLog.requestBody), null, 2)} {JSON.stringify(JSON.parse(activeLog.body), null, 2)}
</pre> </pre>
</ScrollArea> </ScrollArea>
</div> </div>
)} )}
{activeLog.responseBody && ( {activeLog.resp && (
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-semibold flex items-center gap-2"><Activity className="w-4 h-4" /> Response Data</h4> <h4 className="text-sm font-semibold flex items-center gap-2"><Activity className="w-4 h-4" /> Response Data</h4>
<ScrollArea className="h-[150px] w-full rounded-md border bg-zinc-950 p-4"> <ScrollArea className="h-[150px] w-full rounded-md border bg-zinc-950 p-4">
<pre className="text-xs text-blue-400 font-mono leading-relaxed"> <pre className="text-xs text-blue-400 font-mono leading-relaxed">
{JSON.stringify(JSON.parse(activeLog.responseBody), null, 2)} {JSON.stringify(JSON.parse(activeLog.resp), null, 2)}
</pre> </pre>
</ScrollArea> </ScrollArea>
</div> </div>
+87 -15
View File
@@ -1,13 +1,14 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Plus, RefreshCw, Edit, Trash2, MenuSquare, ChevronRight, ChevronDown } from 'lucide-react' import { Plus, RefreshCw, Edit, Trash2, MenuSquare } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { getMenuTree, createMenu, updateMenu, deleteMenu } from '@/api/system/menu' import { getMenuTree, createMenu, updateMenu, deleteMenu } from '@/api/system/menu'
import type { SystemMenu } from '@/api/system' import type { SystemMenu } from '@/api/system'
@@ -21,7 +22,11 @@ function MenuRow({
}) { }) {
const [expanded, setExpanded] = useState(true) const [expanded, setExpanded] = useState(true)
const hasChildren = item.children && item.children.length > 0 const hasChildren = item.children && item.children.length > 0
const IconCmp = (Icons as any)[item.icon ? item.icon.charAt(0).toUpperCase() + item.icon.slice(1) : 'Circle'] || Icons.Circle
// Try to find the exact icon, otherwise try stripping 'Icon' prefix (if from old DB data), fallback to Circle
const exactIcon = (Icons as any)[item.icon || '']
const strippedIcon = item.icon?.startsWith('Icon') ? (Icons as any)[item.icon.substring(4)] : undefined
const IconCmp = exactIcon || strippedIcon || Icons.Circle
return ( return (
<> <>
@@ -82,6 +87,39 @@ function MenuRow({
) )
} }
const commonIcons = [
'LayoutDashboard', 'Users', 'User', 'Shield', 'MessageSquare', 'FolderTree', 'Leaf',
'Settings', 'Folder', 'Book', 'Home', 'Monitor', 'FileText', 'Bot', 'Radio',
'Image', 'Music', 'ScrollText', 'Trophy', 'Award', 'Star', 'Gift', 'List', 'Menu',
'Link', 'Activity', 'BarChart2', 'PieChart', 'Calendar', 'Camera', 'Check', 'Circle'
]
function IconPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const IconCmp = (Icons as any)[value] || Icons.Circle
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start gap-2">
{value ? <IconCmp className="h-4 w-4" /> : <Icons.Circle className="h-4 w-4 opacity-50" />}
<span className="truncate">{value || '选择图标'}</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-2" side="right" align="start">
<div className="grid grid-cols-8 gap-1">
{commonIcons.map(name => {
const Cmp = (Icons as any)[name]
return (
<Button key={name} variant="ghost" size="icon" className={`h-8 w-8 rounded ${value === name ? 'bg-primary/20 text-primary hover:bg-primary/30' : 'hover:bg-muted'}`} onClick={() => onChange(name)} title={name}>
{Cmp && <Cmp className="h-4 w-4" />}
</Button>
)
})}
</div>
</PopoverContent>
</Popover>
)
}
export default function Menus() { export default function Menus() {
const [menus, setMenus] = useState<SystemMenu[]>([]) const [menus, setMenus] = useState<SystemMenu[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -89,13 +127,13 @@ export default function Menus() {
// Dialog State // Dialog State
const [dialogOpen, setDialogOpen] = useState(false) const [dialogOpen, setDialogOpen] = useState(false)
const [editingMenu, setEditingMenu] = useState<SystemMenu | null>(null) const [editingMenu, setEditingMenu] = useState<SystemMenu | null>(null)
const [formData, setFormData] = useState({ title: '', name: '', path: '', icon: '', sort: 0, category: 1, parentId: '' }) const [formData, setFormData] = useState({ title: '', name: '', path: '', code: '', permission: '', icon: '', sort: 0, category: 1, parentId: '' })
const fetchMenus = async () => { const fetchMenus = async () => {
setLoading(true) setLoading(true)
try { try {
const res = await getMenuTree() const res = await getMenuTree()
if (res.data) setMenus(res.data) if (res) setMenus(res)
} finally { setLoading(false) } } finally { setLoading(false) }
} }
@@ -116,18 +154,23 @@ export default function Menus() {
const openCreateDialog = (parentId: string = '') => { const openCreateDialog = (parentId: string = '') => {
setEditingMenu(null) setEditingMenu(null)
setFormData({ title: '', name: '', path: '', icon: '', sort: 1, category: 1, parentId }) setFormData({ title: '', name: '', path: '', code: '', permission: '', icon: '', sort: 1, category: 1, parentId })
setDialogOpen(true) setDialogOpen(true)
} }
const openEditDialog = (menu: SystemMenu) => { const openEditDialog = (menu: SystemMenu) => {
setEditingMenu(menu) setEditingMenu(menu)
setFormData({ title: menu.title || '', name: menu.name || '', path: menu.path || '', icon: menu.icon || '', sort: menu.sort || 1, category: menu.category || 1, parentId: menu.parentId || '' }) setFormData({
title: menu.title || '', name: menu.name || '', path: menu.path || '',
code: menu.code || '', permission: menu.permission || '',
icon: menu.icon || '', sort: menu.sort || 1,
category: menu.category || 1, parentId: menu.parentId || ''
})
setDialogOpen(true) setDialogOpen(true)
} }
const handleSave = async () => { const handleSave = async () => {
if (editingMenu) await updateMenu(editingMenu.id, formData) if (editingMenu) await updateMenu({ id: editingMenu.id, ...formData })
else await createMenu(formData) else await createMenu(formData)
setDialogOpen(false) setDialogOpen(false)
fetchMenus() fetchMenus()
@@ -135,7 +178,7 @@ export default function Menus() {
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (confirm('确定要删除这个菜单节点吗?其子节点也会受到影响。')) { if (confirm('确定要删除这个菜单节点吗?其子节点也会受到影响。')) {
await deleteMenu(id) await deleteMenu([id])
fetchMenus() fetchMenus()
} }
} }
@@ -191,11 +234,24 @@ export default function Menus() {
</Card> </Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle>{editingMenu ? '编辑菜单' : '新增菜单'}</DialogTitle> <DialogTitle>{editingMenu ? '编辑菜单' : '新增菜单'}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"></Label>
<div className="col-span-3 flex items-center gap-6">
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" className="accent-primary" checked={formData.category === 1} onChange={() => setFormData({ ...formData, category: 1, permission: '' })} />
<span className="text-sm font-medium">/</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" className="accent-primary" checked={formData.category === 2} onChange={() => setFormData({ ...formData, category: 2, path: '', icon: '' })} />
<span className="text-sm font-medium">/</span>
</label>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"></Label> <Label className="text-right"></Label>
<div className="col-span-3"> <div className="col-span-3">
@@ -213,21 +269,37 @@ export default function Menus() {
</div> </div>
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right"></Label> <Label htmlFor="title" className="text-right">{formData.category === 1 ? '菜单名称' : '权限名称'}</Label>
<Input id="title" value={formData.title} onChange={e => setFormData({ ...formData, title: e.target.value })} className="col-span-3" /> <Input id="title" value={formData.title} onChange={e => setFormData({ ...formData, title: e.target.value })} className="col-span-3" placeholder={formData.category === 1 ? '如: 用户管理' : '如: 新增用户'} />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right"> Name</Label> <Label htmlFor="name" className="text-right"> Name</Label>
<Input id="name" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} className="col-span-3" placeholder="如: system_users" /> <Input id="name" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} className="col-span-3" placeholder="如: UserList" />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="code" className="text-right"> Code</Label>
<Input id="code" value={formData.code} onChange={e => setFormData({ ...formData, code: e.target.value })} className="col-span-3" placeholder="如: systemRole" />
</div>
{formData.category === 1 && (
<>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="path" className="text-right"></Label> <Label htmlFor="path" className="text-right"></Label>
<Input id="path" value={formData.path} onChange={e => setFormData({ ...formData, path: e.target.value })} className="col-span-3" placeholder="/system/users" /> <Input id="path" value={formData.path} onChange={e => setFormData({ ...formData, path: e.target.value })} className="col-span-3" placeholder="如: /system/users" />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="icon" className="text-right"></Label> <Label htmlFor="icon" className="text-right"></Label>
<Input id="icon" value={formData.icon} onChange={e => setFormData({ ...formData, icon: e.target.value })} className="col-span-3" placeholder="lucide-react 图标名" /> <div className="col-span-3">
<IconPicker value={formData.icon} onChange={v => setFormData({ ...formData, icon: v })} />
</div> </div>
</div>
</>
)}
{formData.category === 2 && (
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="permission" className="text-right"></Label>
<Input id="permission" value={formData.permission} onChange={e => setFormData({ ...formData, permission: e.target.value })} className="col-span-3" placeholder="如: sys:user:add" />
</div>
)}
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="sort" className="text-right"></Label> <Label htmlFor="sort" className="text-right"></Label>
<Input id="sort" type="number" value={formData.sort} onChange={e => setFormData({ ...formData, sort: parseInt(e.target.value) || 0 })} className="col-span-3" /> <Input id="sort" type="number" value={formData.sort} onChange={e => setFormData({ ...formData, sort: parseInt(e.target.value) || 0 })} className="col-span-3" />
+13 -10
View File
@@ -34,9 +34,9 @@ export default function Roles() {
setLoading(true) setLoading(true)
try { try {
const res = await getRoleList({ current: 1, pageSize: 10, ...search }) const res = await getRoleList({ current: 1, pageSize: 10, ...search })
if (res.data) { if (res) {
setRoles(res.data.list) setRoles(res.list)
setTotal(res.data.total) setTotal(res.total)
} }
} finally { } finally {
setLoading(false) setLoading(false)
@@ -45,7 +45,7 @@ export default function Roles() {
const fetchMenus = async () => { const fetchMenus = async () => {
const res = await getMenuTree() const res = await getMenuTree()
if (res.data) setAllMenus(res.data) if (res) setAllMenus(res)
} }
useEffect(() => { useEffect(() => {
@@ -69,7 +69,7 @@ export default function Roles() {
} }
const handleSave = async () => { const handleSave = async () => {
if (editingRole) await updateRole(editingRole.id, formData) if (editingRole) await updateRole({ id: editingRole.id, ...formData })
else await createRole(formData) else await createRole(formData)
setDialogOpen(false) setDialogOpen(false)
fetchRoles() fetchRoles()
@@ -77,20 +77,23 @@ export default function Roles() {
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (confirm('确定要删除这个角色吗?')) { if (confirm('确定要删除这个角色吗?')) {
await deleteRole(id) await deleteRole([id])
fetchRoles() fetchRoles()
} }
} }
const openPermDialog = (role: SystemRole) => { const openPermDialog = (role: SystemRole) => {
setActiveRole(role) setActiveRole(role)
// mock some selected ids // Actually get the role's menus
setSelectedMenuIds(['1', '10', '11', '12']) setSelectedMenuIds(role.menuIds || [])
setPermDialogOpen(true) setPermDialogOpen(true)
} }
const handleSavePerms = async () => { const handleSavePerms = async () => {
// In real app, call assignMenusToRole API if (activeRole) {
await updateRole({ id: activeRole.id, menuIds: selectedMenuIds })
fetchRoles()
}
setPermDialogOpen(false) setPermDialogOpen(false)
} }
@@ -182,7 +185,7 @@ export default function Roles() {
<TableCell className="font-mono text-xs">{role.code}</TableCell> <TableCell className="font-mono text-xs">{role.code}</TableCell>
<TableCell>{role.sort}</TableCell> <TableCell>{role.sort}</TableCell>
<TableCell className="text-muted-foreground text-sm"> <TableCell className="text-muted-foreground text-sm">
{new Date(role.createdAt).toLocaleDateString('zh-CN')} -
</TableCell> </TableCell>
<TableCell className="text-right pr-6"> <TableCell className="text-right pr-6">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
+17 -24
View File
@@ -32,9 +32,9 @@ export default function UserManage() {
setLoading(true) setLoading(true)
try { try {
const res = await getUserList({ current: 1, pageSize: 10, ...search }) const res = await getUserList({ current: 1, pageSize: 10, ...search })
if (res.data) { if (res) {
setUsers(res.data.list) setUsers(res.list)
setTotal(res.data.total) setTotal(res.total)
} }
} finally { } finally {
setLoading(false) setLoading(false)
@@ -43,7 +43,7 @@ export default function UserManage() {
const fetchRoles = async () => { const fetchRoles = async () => {
const res = await getRoleList({ current: 1, pageSize: 100 }) const res = await getRoleList({ current: 1, pageSize: 100 })
if (res.data) setAllRoles(res.data.list) if (res) setAllRoles(res.list)
} }
useEffect(() => { useEffect(() => {
@@ -67,17 +67,17 @@ export default function UserManage() {
const openEditDialog = (user: SystemUser) => { const openEditDialog = (user: SystemUser) => {
setEditingUser(user) setEditingUser(user)
setFormData({ setFormData({
account: user.account, name: user.name, phone: user.phone || '', clientId: user.clientId || '', account: user.account, name: user.name, phone: user.phone || '', clientId: '',
roleIds: user.roles?.map(r => r.id) || [] roleIds: user.roles || []
}) })
setDialogOpen(true) setDialogOpen(true)
} }
const handleSave = async () => { const handleSave = async () => {
if (editingUser) { if (editingUser) {
await updateUser(editingUser.id, formData) await updateUser({ id: editingUser.id, ...formData })
} else { } else {
await createUser(formData) await createUser({ ...formData, password: '123' }) // 默认密码
} }
setDialogOpen(false) setDialogOpen(false)
fetchUsers() fetchUsers()
@@ -85,7 +85,7 @@ export default function UserManage() {
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (confirm('确定要删除这个用户吗?')) { if (confirm('确定要删除这个用户吗?')) {
await deleteUser(id) await deleteUser([id])
fetchUsers() fetchUsers()
} }
} }
@@ -147,7 +147,6 @@ export default function UserManage() {
<TableHead className="w-[100px] pl-6"></TableHead> <TableHead className="w-[100px] pl-6"></TableHead>
<TableHead></TableHead> <TableHead></TableHead>
<TableHead></TableHead> <TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead> <TableHead></TableHead>
<TableHead></TableHead> <TableHead></TableHead>
<TableHead className="text-right pr-6"></TableHead> <TableHead className="text-right pr-6"></TableHead>
@@ -171,26 +170,20 @@ export default function UserManage() {
<TableCell className="font-medium pl-6">{user.account}</TableCell> <TableCell className="font-medium pl-6">{user.account}</TableCell>
<TableCell>{user.name}</TableCell> <TableCell>{user.name}</TableCell>
<TableCell>{user.phone || '-'}</TableCell> <TableCell>{user.phone || '-'}</TableCell>
<TableCell>
{user.clientId ? (
<Badge variant="outline" className="bg-primary/5 text-primary border-primary/20">
{user.clientId}
</Badge>
) : (
<Badge variant="secondary" className="bg-muted text-muted-foreground">System</Badge>
)}
</TableCell>
<TableCell> <TableCell>
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{user.roles?.map(r => ( {user.roles?.map(code => {
<Badge key={r.id} variant="default" className="text-[10px] h-5 font-normal bg-emerald-500 hover:bg-emerald-600 text-white"> const role = allRoles.find(r => r.code === code)
{r.name} return (
<Badge key={code} variant="default" className="text-[10px] h-5 font-normal bg-emerald-500 hover:bg-emerald-600 text-white">
{role ? role.name : code}
</Badge> </Badge>
)) || <span className="text-muted-foreground text-xs"></span>} )
}) || <span className="text-muted-foreground text-xs"></span>}
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-muted-foreground text-sm"> <TableCell className="text-muted-foreground text-sm">
{new Date(user.createdAt).toLocaleDateString('zh-CN')} {user.createdAt ? new Date(user.createdAt * 1000).toLocaleDateString('zh-CN') : '-'}
</TableCell> </TableCell>
<TableCell className="text-right pr-6"> <TableCell className="text-right pr-6">
<DropdownMenu> <DropdownMenu>
+3 -4
View File
@@ -1,7 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import type { SystemUser, SystemMenu } from '@/api/system' import type { SystemUser, SystemMenu } from '@/api/system'
import { getUserMenuTree } from '@/api/systemCrud' import { getUserInfo, logout as apiLogout } from '@/api/system/auth'
import { logout as apiLogout } from '@/api/system'
const TOKEN_KEY = 'token' const TOKEN_KEY = 'token'
const USER_KEY = 'user' const USER_KEY = 'user'
@@ -66,8 +65,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
refreshMenus: async () => { refreshMenus: async () => {
if (!get().isAuthenticated) return if (!get().isAuthenticated) return
try { try {
const res = await getUserMenuTree() const userInfo = await getUserInfo()
const menus = (res.data as any).menus || [] const menus = userInfo.menus || []
set({ menus, permissions: extractPermissions(menus), hasFetchedMenus: true }) set({ menus, permissions: extractPermissions(menus), hasFetchedMenus: true })
} catch (e) { } catch (e) {
console.error('获取菜单失败:', e) console.error('获取菜单失败:', e)
+1
View File
@@ -8,6 +8,7 @@
"paths": { "@/*": ["./src/*"] }, "paths": { "@/*": ["./src/*"] },
"types": ["vite/client"], "types": ["vite/client"],
"skipLibCheck": true, "skipLibCheck": true,
"ignoreDeprecations": "6.0",
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",